Impact
The single-item and bulk-item APIs handle the exact same failure mode — a job not reaching a terminal state before the deadline — with two incompatible contracts. A developer who validates their error-handling against the bulk API (which returns a normal result object) and assumes the single-item API behaves the same way will ship code with an unhandled exception in production. This is especially likely because both methods are part of the same class, documented side-by-side, and conceptually identical.
Expected Behavior
Both methods should handle a polling timeout the same way — either both throw, or both resolve with a "timeout" status result. Given that RememberBulkItemResult already models status: "done" | "failed" | "timeout" as a legitimate, non-exceptional outcome, the single-item path should follow the same contract.
Actual Behavior
Single-item (memwal.js, waitForRememberJob) — throws: ```
throw Object.assign(new Error(remember job timed out after ${timeoutMs}ms (job_id=${jobId})), { status: 504, jobId });
Bulk (memwal.js, waitForRememberJobs) — returns gracefully:```
const results = jobIds.map((jobId, idx) => ({
id: jobId,
blob_id: "",
status: "timeout",
namespace: namespaces[idx] ?? this.namespace,
error: `polling timed out after ${timeoutMs}ms`,
}));
Additionally, waitForRememberJob's default timeoutMs is 60,000ms (60 seconds) — a value used implicitly by the class-level JSDoc's documented usage example (await memwal.waitForRememberJob(accepted.job_id), no options passed) — meaning any developer following the documented example exactly is exposed to this throw-on-timeout behavior with no indication from the docs that it can happen, or that raising timeoutMs is advisable.
Reproduction
// Bulk: resolves gracefully with status "timeout" in the result array
const bulkResult = await memwal.waitForRememberJobs([slowJobId], undefined, { timeoutMs: 100 });
console.log(bulkResult.results[0].status); // "timeout" — no exception
// Single: throws instead
try {
await memwal.waitForRememberJob(slowJobId, { timeoutMs: 100 });
} catch (e) {
console.log(e.message); // "remember job timed out after 100ms (job_id=...)"
}
Real-world evidence
Encountered directly during development: waitForRememberJob threw after both a 60s and a 180s explicit timeoutMs on a checkpoint write, while the underlying write later succeeded (confirmed via a separate recall() call — the write existed server-side despite the client throwing).
Live reproduction (fresh run, forced with timeoutMs: 1 so both paths are guaranteed to hit the timeout condition deterministically)
--- SINGLE: waitForRememberJob with timeoutMs=1 ---
single: THREW -- remember job timed out after 1ms (job_id=8e669930-ae7c-431a-920b-fdbc61f4208d)
--- BULK: waitForRememberJobs with timeoutMs=1, same condition ---
bulk: resolved gracefully, result:
{
"id": "d7807054-209d-417f-a00c-aa21e6f75e4f",
"blob_id": "",
"status": "timeout",
"namespace": "bug-repro",
"error": "polling timed out after 1ms"
}
[
— live terminal run: Showing the single call throw and the bulk call's graceful result side by side]
Suggested Fix
Change waitForRememberJob to resolve with a { status: "timeout", ... } result object matching the shape already used by waitForRememberJobs, rather than throwing. If throwing is intentionally preferred for the single-item case, document the discrepancy explicitly in both methods' JSDoc, and note the default timeoutMs value directly in the class-level @example.
Impact
The single-item and bulk-item APIs handle the exact same failure mode — a job not reaching a terminal state before the deadline — with two incompatible contracts. A developer who validates their error-handling against the bulk API (which returns a normal result object) and assumes the single-item API behaves the same way will ship code with an unhandled exception in production. This is especially likely because both methods are part of the same class, documented side-by-side, and conceptually identical.
Expected Behavior
Both methods should handle a polling timeout the same way — either both throw, or both resolve with a "timeout" status result. Given that RememberBulkItemResult already models status: "done" | "failed" | "timeout" as a legitimate, non-exceptional outcome, the single-item path should follow the same contract.
Actual Behavior
Single-item (memwal.js, waitForRememberJob) — throws: ```
throw Object.assign(new Error(
remember job timed out after ${timeoutMs}ms (job_id=${jobId})), { status: 504, jobId });Additionally, waitForRememberJob's default timeoutMs is 60,000ms (60 seconds) — a value used implicitly by the class-level JSDoc's documented usage example (await memwal.waitForRememberJob(accepted.job_id), no options passed) — meaning any developer following the documented example exactly is exposed to this throw-on-timeout behavior with no indication from the docs that it can happen, or that raising timeoutMs is advisable.
Reproduction
Real-world evidence
Encountered directly during development: waitForRememberJob threw after both a 60s and a 180s explicit timeoutMs on a checkpoint write, while the underlying write later succeeded (confirmed via a separate recall() call — the write existed server-side despite the client throwing).
Live reproduction (fresh run, forced with timeoutMs: 1 so both paths are guaranteed to hit the timeout condition deterministically)
[
— live terminal run: Showing the single call throw and the bulk call's graceful result side by side]
Suggested Fix
Change waitForRememberJob to resolve with a { status: "timeout", ... } result object matching the shape already used by waitForRememberJobs, rather than throwing. If throwing is intentionally preferred for the single-item case, document the discrepancy explicitly in both methods' JSDoc, and note the default timeoutMs value directly in the class-level @example.