@mlx-node/server 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/endpoints/messages.d.ts +13 -0
  2. package/dist/endpoints/messages.d.ts.map +1 -0
  3. package/dist/endpoints/messages.js +511 -0
  4. package/dist/endpoints/models.d.ts +5 -0
  5. package/dist/endpoints/models.d.ts.map +1 -0
  6. package/dist/endpoints/models.js +10 -0
  7. package/dist/endpoints/responses.d.ts +79 -0
  8. package/dist/endpoints/responses.d.ts.map +1 -0
  9. package/dist/endpoints/responses.js +2816 -0
  10. package/dist/errors.d.ts +43 -0
  11. package/dist/errors.d.ts.map +1 -0
  12. package/dist/errors.js +84 -0
  13. package/dist/handler.d.ts +18 -0
  14. package/dist/handler.d.ts.map +1 -0
  15. package/dist/handler.js +35 -0
  16. package/dist/index.d.ts +23 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +16 -0
  19. package/dist/mappers/anthropic-request.d.ts +9 -0
  20. package/dist/mappers/anthropic-request.d.ts.map +1 -0
  21. package/dist/mappers/anthropic-request.js +241 -0
  22. package/dist/mappers/anthropic-response.d.ts +14 -0
  23. package/dist/mappers/anthropic-response.d.ts.map +1 -0
  24. package/dist/mappers/anthropic-response.js +112 -0
  25. package/dist/mappers/request.d.ts +18 -0
  26. package/dist/mappers/request.d.ts.map +1 -0
  27. package/dist/mappers/request.js +206 -0
  28. package/dist/mappers/response.d.ts +13 -0
  29. package/dist/mappers/response.d.ts.map +1 -0
  30. package/dist/mappers/response.js +116 -0
  31. package/dist/pending-writes.d.ts +337 -0
  32. package/dist/pending-writes.d.ts.map +1 -0
  33. package/dist/pending-writes.js +468 -0
  34. package/dist/registry.d.ts +363 -0
  35. package/dist/registry.d.ts.map +1 -0
  36. package/dist/registry.js +497 -0
  37. package/dist/router.d.ts +6 -0
  38. package/dist/router.d.ts.map +1 -0
  39. package/dist/router.js +78 -0
  40. package/dist/server.d.ts +80 -0
  41. package/dist/server.d.ts.map +1 -0
  42. package/dist/server.js +158 -0
  43. package/dist/session-registry.d.ts +297 -0
  44. package/dist/session-registry.d.ts.map +1 -0
  45. package/dist/session-registry.js +403 -0
  46. package/dist/streaming.d.ts +7 -0
  47. package/dist/streaming.d.ts.map +1 -0
  48. package/dist/streaming.js +16 -0
  49. package/dist/tool-call-buffer.d.ts +26 -0
  50. package/dist/tool-call-buffer.d.ts.map +1 -0
  51. package/dist/tool-call-buffer.js +51 -0
  52. package/dist/transport-visibility.d.ts +56 -0
  53. package/dist/transport-visibility.d.ts.map +1 -0
  54. package/dist/transport-visibility.js +161 -0
  55. package/dist/types-anthropic.d.ts +144 -0
  56. package/dist/types-anthropic.d.ts.map +1 -0
  57. package/dist/types-anthropic.js +2 -0
  58. package/dist/types.d.ts +220 -0
  59. package/dist/types.d.ts.map +1 -0
  60. package/dist/types.js +2 -0
  61. package/package.json +36 -0
@@ -0,0 +1,468 @@
1
+ /**
2
+ * PendingResponseWrites — per-store in-memory index of response
3
+ * records whose `ResponseStore.store(...)` promise has been initiated
4
+ * but has not yet resolved.
5
+ *
6
+ * ## Why this exists
7
+ *
8
+ * The responses endpoint starts `store.store(record)` synchronously
9
+ * inside the per-model `withExclusive` block (so the tracker observes
10
+ * the in-flight write before the mutex releases) but does NOT await
11
+ * it on the critical path. Without this tracker, a client that fires
12
+ * a follow-up request carrying `previous_response_id: A` immediately
13
+ * after seeing `response.completed` could race the off-lock
14
+ * `store.store()` for A and be rejected with a spurious
15
+ * `404 Previous response not found` because `getChain()` had not yet
16
+ * seen the row.
17
+ *
18
+ * The chain-lookup path consults `awaitPending(id)` BEFORE treating
19
+ * `getChain(id).length === 0` as a 404. If a write is still in flight,
20
+ * it awaits and retries `getChain`; the retry is guaranteed to see
21
+ * the row because the promise resolves only after the store's own
22
+ * serialization queue has accepted the insert.
23
+ *
24
+ * ## Semantics
25
+ *
26
+ * `track(id, promise)` registers `promise` under `id` and removes the
27
+ * entry when the promise settles (fulfill OR reject — a failed write
28
+ * leaves the tracker empty and the subsequent `getChain()` returns
29
+ * empty, which is the correct 404 shape).
30
+ *
31
+ * `awaitPending(id)` returns the tracked in-flight promise, or
32
+ * undefined. It is the SAME promise that was registered; our removal
33
+ * handler is attached via `.finally(...)` so the caller's rejection
34
+ * behaviour is unaffected. Callers typically swallow rejections with
35
+ * `await …catch(() => {})` before retrying `getChain()` because the
36
+ * rejection is already surfaced through the registering awaiter.
37
+ *
38
+ * ## Hard-timeout marker state
39
+ *
40
+ * When the responses-endpoint breaker decides a pending write is
41
+ * wedged it calls `markHardTimedOut(id, ttlMs, absoluteExpiresAt)`,
42
+ * which removes the id from `pending` (so the closure chain is
43
+ * reclaimable) and adds it to a lightweight `hardTimedOut` map. The
44
+ * continuation path then classifies a missing chain as retryable 503
45
+ * `storage_timeout` instead of permanent 404 while the marker is live.
46
+ *
47
+ * Marker lifetime is bounded by:
48
+ *
49
+ * min(write settlement, last continuation + TTL, absoluteExpiresAt)
50
+ *
51
+ * Five cleanup/refresh paths keep memory bounded and classification
52
+ * honest:
53
+ *
54
+ * 1. Fast path — `track()`'s `.finally(...)` deletes the marker as
55
+ * soon as the wedged write settles.
56
+ * 2. Read-refresh path — `isHardTimedOut(id)` slides `expiresAt`
57
+ * forward by `ttlMs` on every live hit (clamped at
58
+ * `absoluteExpiresAt`). Actively retried chains stay recoverable
59
+ * as long as the underlying write might still land.
60
+ * 3. Read-expire path — a full TTL elapse without refresh lazily
61
+ * deletes the entry and classifies the id as permanent 404.
62
+ * 4. Read-absolute-cap path — once `Date.now() >= absoluteExpiresAt`,
63
+ * the marker is deleted unconditionally. `ResponseStore.getChain()`
64
+ * hides the row past its own row TTL, so the retryable-503
65
+ * classification would be factually wrong.
66
+ * 5. Write-sweep path — `markHardTimedOut()` drains expired entries
67
+ * before inserting, bounded to `MAX_SWEEP_PER_INSERT` visits per
68
+ * call to keep the transition O(1) amortized even when the map
69
+ * is large. `isHardTimedOut()` moves refreshed entries to the
70
+ * Map tail so the bounded sweep cannot be starved by a stable
71
+ * head cohort of hot entries (natural LRU behaviour).
72
+ *
73
+ * The caller-side `absoluteExpiresAt` is the MINIMUM `expiresAt`
74
+ * across the whole resolved chain, not just the child record.
75
+ * `ResponseStore.getChain()` walks ancestors and aborts on the first
76
+ * expired link (see `crates/mlx-db/src/response_store/reader.rs:44-59`),
77
+ * so a child whose parent expires sooner is unrecoverable at the
78
+ * parent's expiry — not the child's. This module just receives the
79
+ * min-clamped value.
80
+ *
81
+ * ## Pending-entry earliest-expiry metadata
82
+ *
83
+ * The pre-breaker `awaitPending` timeout/probe path in `responses.ts`
84
+ * would otherwise classify any unresolved pending write as retryable
85
+ * `storage_timeout`, even when the resolved chain's earliest ancestor
86
+ * has already expired. A continuation whose parent is already past
87
+ * its row TTL cannot ever succeed via `getChain()`, so looping the
88
+ * client on 503 until the hard breaker fires is wasted.
89
+ *
90
+ * `track(id, promise, earliestExpiresAtMs?)` records the earliest
91
+ * recoverable expiry alongside the tracked promise in a per-id side
92
+ * map (`earliestExpiresByPending`). `getEarliestExpiresAtMs(id)`
93
+ * exposes it so the endpoint can short-circuit to 404 once
94
+ * `Date.now() >= earliestExpiresAtMs`. The side map is keyed
95
+ * identically to `pending` and cleared on the same `.finally(...)`
96
+ * hook — no extra lifecycle surface.
97
+ *
98
+ * ## Scope
99
+ *
100
+ * One tracker per `ResponseStore` instance is attached via a
101
+ * `WeakMap`, so callers never need to thread the tracker through the
102
+ * handler plumbing explicitly. This keeps the same (store, tracker)
103
+ * pair alive for the lifetime of the store and avoids leaks across
104
+ * test suites that recreate the store per describe block.
105
+ */
106
+ /**
107
+ * Per-store tracker for in-flight `store.store(...)` writes.
108
+ *
109
+ * Thread-safety: Node.js is single-threaded within one event loop
110
+ * tick, so the internal `Map` is safe against concurrent mutation by
111
+ * design. Every mutation (`track`, `awaitPending`, `.finally(...)`
112
+ * cleanup) runs synchronously within a tick.
113
+ */
114
+ export class PendingResponseWrites {
115
+ pending = new Map();
116
+ /**
117
+ * Per-pending-entry scalar recording the EARLIEST recoverable
118
+ * wall-clock expiry across (record + resolved ancestor chain) at
119
+ * `track()` time. Keyed identically to `pending` and cleared on
120
+ * the same `.finally(...)` settlement hook.
121
+ *
122
+ * The pre-breaker `awaitPending` timeout/probe path consults this
123
+ * via `getEarliestExpiresAtMs(id)`: once `Date.now()` has passed
124
+ * the earliest ancestor expiry, `getChain()` can never succeed,
125
+ * so the continuation short-circuits to 404 rather than looping
126
+ * the client on retryable 503.
127
+ *
128
+ * Optional: callers that pass `undefined` do not populate the
129
+ * side map; `getEarliestExpiresAtMs(id)` then returns `undefined`
130
+ * and the caller falls back to emitting retryable 503.
131
+ */
132
+ earliestExpiresByPending = new Map();
133
+ /**
134
+ * Ids that crossed the hard-timeout breaker in `responses.ts`
135
+ * while their `store.store(...)` promise was still unresolved.
136
+ *
137
+ * Each entry records `{ expiresAt, ttlMs, absoluteExpiresAt }` in
138
+ * epoch-ms. `expiresAt` is the TTL-based sliding window;
139
+ * `absoluteExpiresAt` is the record row's own wall-clock expiry
140
+ * (`record.expiresAt * 1000`). See the module header for the full
141
+ * cleanup/refresh path inventory.
142
+ *
143
+ * Invariant: a marker is only meaningful for the SPECIFIC write
144
+ * that was live when `markHardTimedOut` was called. If a later
145
+ * `track(id, newPromise)` reuses the same id after a marker was
146
+ * set, the original promise's `.finally(...)` will still clear
147
+ * the marker on its settlement (clearing the wrong state for the
148
+ * new promise). In practice the responses endpoint scopes
149
+ * response ids to a single persist each, so this collision cannot
150
+ * arise.
151
+ */
152
+ hardTimedOut = new Map();
153
+ /**
154
+ * Per-call visit budget for the opportunistic sweep invoked from
155
+ * `markHardTimedOut()`. Without a cap, refresh-on-read could keep
156
+ * the map arbitrarily large and every transition would pay O(N) on
157
+ * the main event loop (amortized O(N^2) across N wedged writes).
158
+ *
159
+ * Cap of 64 makes each transition O(1) with a small constant.
160
+ * JavaScript `Map` iterates in insertion order, so the sweep
161
+ * naturally drains the oldest markers first — which is where
162
+ * same-TTL expiries cluster. A backlog of K expired markers drains
163
+ * fully across ceil(K / 64) subsequent inserts, adequate because
164
+ * the read-path deletions are the authoritative cleanup signals
165
+ * for ids that actually receive continuation traffic.
166
+ *
167
+ * NOTE: the budget is a VISIT limit, not a delete limit. We stop
168
+ * after visiting MAX_SWEEP_PER_INSERT entries regardless of how
169
+ * many were expired, so cost stays bounded even when none of the
170
+ * first 64 entries are expired.
171
+ */
172
+ static MAX_SWEEP_PER_INSERT = 64;
173
+ /**
174
+ * Register an in-flight write under `id`. The caller must pass the
175
+ * raw `Promise<void>` returned by `store.store(record)` BEFORE
176
+ * awaiting it — otherwise the race window we are trying to close
177
+ * reopens.
178
+ *
179
+ * The tracker attaches its own `.finally(...)` handler to remove
180
+ * the entry when the promise settles. The caller's own handling of
181
+ * the promise (await / catch / log) is unaffected because
182
+ * `.finally` returns a new promise chain that does not steal the
183
+ * rejection.
184
+ *
185
+ * `earliestExpiresAtMs` is the EARLIEST wall-clock expiry (epoch-ms)
186
+ * across the record being persisted AND every resolved ancestor in
187
+ * its chain. When provided, it is stored in
188
+ * `earliestExpiresByPending` so the pre-breaker `awaitPending`
189
+ * timeout/probe path can short-circuit to 404 once
190
+ * `Date.now() >= earliestExpiresAtMs` rather than emit retryable
191
+ * 503 for a chain that cannot be recovered via `getChain()`.
192
+ * `Number.isFinite(...)` guards for rows lacking explicit
193
+ * `expiresAt`.
194
+ */
195
+ track(id, writePromise, earliestExpiresAtMs) {
196
+ this.pending.set(id, writePromise);
197
+ if (earliestExpiresAtMs !== undefined && Number.isFinite(earliestExpiresAtMs)) {
198
+ this.earliestExpiresByPending.set(id, earliestExpiresAtMs);
199
+ }
200
+ // Use `.finally` rather than `then`+`catch` so registration lifetime
201
+ // is symmetric across fulfill/reject — a failed write should still
202
+ // clear the tracker so subsequent chain lookups see an empty
203
+ // getChain() result and 404 cleanly. The trailing `.catch` on the
204
+ // returned chain only silences unhandled-rejection diagnostics on
205
+ // this cleanup fork; the rejection is still surfaced to whoever
206
+ // awaits `writePromise` directly.
207
+ void writePromise
208
+ .finally(() => {
209
+ // Only remove if WE are still the registered entry — the id
210
+ // may have been re-registered after this write resolved.
211
+ if (this.pending.get(id) === writePromise) {
212
+ this.pending.delete(id);
213
+ // Drop the earliest-expiry side entry in lockstep so we
214
+ // never serve a stale scalar for a freshly-registered id
215
+ // that reuses the same string. When the write crosses the
216
+ // hard-timeout breaker, `markHardTimedOut()` already
217
+ // removed the pending entry synchronously, so this guard
218
+ // is false by the time the wedged write eventually
219
+ // settles — that case is handled authoritatively in
220
+ // `markHardTimedOut()` itself.
221
+ this.earliestExpiresByPending.delete(id);
222
+ }
223
+ // Fast path for the hard-timeout marker: clear unconditionally
224
+ // because the marker is keyed on id (not promise reference),
225
+ // so whoever later re-registers the id must explicitly
226
+ // re-mark if they want the retryable window open again.
227
+ // Under a truly wedged store this handler never fires; the
228
+ // TTL path in `isHardTimedOut` is the slow-path bound.
229
+ this.hardTimedOut.delete(id);
230
+ })
231
+ .catch(() => {
232
+ // Terminal handler to silence unhandled-rejection warnings;
233
+ // the real rejection is handled by the registering awaiter.
234
+ });
235
+ }
236
+ /**
237
+ * Return the in-flight write promise for `id`, or `undefined` if
238
+ * none is tracked. Callers typically await with rejection
239
+ * suppressed (the tracker promise's rejection is already handled
240
+ * by the separate awaiter that initiated the write) and then
241
+ * retry `store.getChain(id)`.
242
+ */
243
+ awaitPending(id) {
244
+ return this.pending.get(id);
245
+ }
246
+ /**
247
+ * Return the EARLIEST wall-clock expiry (epoch-ms) captured
248
+ * alongside the in-flight write for `id` at `track()` time, or
249
+ * `undefined` if no pending write is tracked and no live marker
250
+ * covers this id.
251
+ *
252
+ * Consulted by the pre-breaker `awaitPending` timeout/probe path in
253
+ * `responses.ts` to distinguish a transient storage slowdown
254
+ * (retryable 503) from an unrecoverable chain where the earliest
255
+ * ancestor has already aged out (permanent 404).
256
+ *
257
+ * Falls back to the marker's `absoluteExpiresAt` when the pending
258
+ * entry has already been drained by `markHardTimedOut()` — otherwise
259
+ * a waiter that straddled the `pending -> hardTimedOut` transition
260
+ * would see `undefined` and fall through to retryable 503 for an
261
+ * unrecoverable chain. Both sites are fed
262
+ * `min(recordExpiresAtMs, chainEarliestExpiresAtMs)` by
263
+ * `initiatePersist` in `responses.ts`, so this is lossless.
264
+ *
265
+ * The fallback is gated on the shared `isMarkerLive` predicate so
266
+ * a marker whose TTL or absolute expiry has already passed cannot
267
+ * hand back a future scalar that contradicts `isHardTimedOut()`.
268
+ * Dead markers return `0` (sentinel meaning "already expired") —
269
+ * the consumer's `Date.now() >= earliestMs` guard always trips for
270
+ * `0`, producing a permanent 404 instead of falling through to the
271
+ * retryable-503 branch. This read path stays side-effect-free;
272
+ * `sweepExpired()` is the authoritative reaper.
273
+ */
274
+ getEarliestExpiresAtMs(id) {
275
+ const pendingValue = this.earliestExpiresByPending.get(id);
276
+ if (pendingValue !== undefined)
277
+ return pendingValue;
278
+ const entry = this.hardTimedOut.get(id);
279
+ if (entry === undefined)
280
+ return undefined;
281
+ if (!PendingResponseWrites.isMarkerLive(entry, Date.now())) {
282
+ return 0;
283
+ }
284
+ return entry.absoluteExpiresAt;
285
+ }
286
+ /**
287
+ * Shared, side-effect-free liveness predicate for hard-timeout
288
+ * markers: live iff `now < expiresAt` AND `now < absoluteExpiresAt`.
289
+ *
290
+ * Extracted so `getEarliestExpiresAtMs()` (read-only) and the
291
+ * mutating `isHardTimedOut()` poll agree on liveness without either
292
+ * invoking the other — `isHardTimedOut()` has refresh + move-to-tail
293
+ * side effects only correct for the polling-side caller.
294
+ */
295
+ static isMarkerLive(entry, nowMs) {
296
+ return nowMs < entry.expiresAt && nowMs < entry.absoluteExpiresAt;
297
+ }
298
+ /**
299
+ * Transition a pending entry to the hard-timed-out marker state.
300
+ *
301
+ * Called by the hard-timeout breaker in `responses.ts` when an
302
+ * in-flight `store.store(...)` has crossed the hard timeout and is
303
+ * presumed wedged. The `pending` entry is removed so `awaitPending`
304
+ * stops handing out the stale promise and the closure chain is
305
+ * reclaimable. The id is added to the marker map so the
306
+ * continuation path classifies missing chains as retryable 503
307
+ * `storage_timeout` instead of permanent 404 while the marker is
308
+ * live.
309
+ *
310
+ * `ttlMs` caps the marker lifetime independently of whether the
311
+ * underlying write settles. The caller (`responses.ts`) reads it
312
+ * from `MLX_HARD_TIMEOUT_MARKER_TTL_MS`; passing it in keeps this
313
+ * module env-free.
314
+ *
315
+ * `absoluteExpiresAt` is the response record's row expiry
316
+ * (`record.expiresAt * 1000`). The initial expiry is
317
+ * `min(Date.now() + ttlMs, absoluteExpiresAt)` so a short-lived
318
+ * record cannot have its marker outlive its row.
319
+ * `isHardTimedOut()` also consults `absoluteExpiresAt` on every
320
+ * read and hard-stops at that bound regardless of refreshes.
321
+ *
322
+ * Returns true if the id was an active pending entry and was moved
323
+ * to the marker; false if no pending entry existed at call time.
324
+ * A false return does NOT add the id to the marker — a marker
325
+ * without a backing promise has no fast cleanup signal (beyond
326
+ * TTL / absolute cap) and would produce spurious retryable-503
327
+ * signals in the meantime if the caller mis-routes ids.
328
+ */
329
+ markHardTimedOut(id, ttlMs, absoluteExpiresAt) {
330
+ // Drain expired entries on the write path so bounded memory does
331
+ // not depend on any continuation ever reading this map. Runs
332
+ // BEFORE the insert so the new entry (whose `expiresAt` is in
333
+ // the future by construction) is not considered for expiry.
334
+ this.sweepExpired();
335
+ const wasPending = this.pending.delete(id);
336
+ // Drain the earliest-expiry side map in lockstep with the
337
+ // pending delete above. The `.finally(...)` cleanup inside
338
+ // `track()` guards on `pending.get(id) === writePromise` — false
339
+ // once we remove the pending entry, and for never-settling
340
+ // writes the `.finally` never fires at all. Without this
341
+ // authoritative drain the side map would grow unboundedly under
342
+ // a wedged store.
343
+ this.earliestExpiresByPending.delete(id);
344
+ if (wasPending) {
345
+ // Clamp initial expiry at the row's absolute expiry so we
346
+ // never return retryable-503 for a window past the point
347
+ // where the row could be recovered.
348
+ const expiresAt = Math.min(Date.now() + ttlMs, absoluteExpiresAt);
349
+ this.hardTimedOut.set(id, { expiresAt, ttlMs, absoluteExpiresAt });
350
+ }
351
+ return wasPending;
352
+ }
353
+ /**
354
+ * Drain expired marker entries (`expiresAt <= now` or
355
+ * `absoluteExpiresAt <= now`). Visits at most
356
+ * `MAX_SWEEP_PER_INSERT` entries per call so a caller cannot
357
+ * trigger an unbounded linear walk. `Map` insertion order makes
358
+ * this drain the oldest (most likely expired) markers first. The
359
+ * budget is a VISIT limit, not a delete limit.
360
+ */
361
+ sweepExpired() {
362
+ const now = Date.now();
363
+ let visited = 0;
364
+ for (const [id, entry] of this.hardTimedOut) {
365
+ if (visited >= PendingResponseWrites.MAX_SWEEP_PER_INSERT)
366
+ break;
367
+ visited += 1;
368
+ if (entry.absoluteExpiresAt <= now || entry.expiresAt <= now) {
369
+ this.hardTimedOut.delete(id);
370
+ }
371
+ }
372
+ }
373
+ /**
374
+ * Whether `id` is currently flagged as hard-timed-out. Used by the
375
+ * `previous_response_id` continuation path to classify a missing
376
+ * chain as retryable 503 `storage_timeout` vs. permanent 404.
377
+ *
378
+ * Read-path cleanup + refresh semantics:
379
+ *
380
+ * - Absolute cap is authoritative: once `now >= absoluteExpiresAt`
381
+ * the marker is deleted unconditionally. `ResponseStore.getChain()`
382
+ * hides the row past its own row TTL, so retryable-503 would
383
+ * lie to the client.
384
+ * - TTL-expired: lazy delete + return false.
385
+ * - Live hit: refresh `expiresAt = min(now + ttlMs, absoluteExpiresAt)`
386
+ * so actively-retried chains stay recoverable while the write
387
+ * might still land, without ever outliving the row.
388
+ * - On every live hit, move the entry to the Map tail (O(1)
389
+ * `delete` + `set` using insertion-order semantics). Without
390
+ * the rotation a stable head cohort of hot refreshed entries
391
+ * could indefinitely block the bounded `sweepExpired()` from
392
+ * reaching expired markers behind them. LRU rotation lets the
393
+ * sweep make forward progress.
394
+ */
395
+ isHardTimedOut(id) {
396
+ const entry = this.hardTimedOut.get(id);
397
+ if (entry === undefined)
398
+ return false;
399
+ const now = Date.now();
400
+ if (now >= entry.absoluteExpiresAt) {
401
+ this.hardTimedOut.delete(id);
402
+ return false;
403
+ }
404
+ if (entry.expiresAt <= now) {
405
+ this.hardTimedOut.delete(id);
406
+ return false;
407
+ }
408
+ entry.expiresAt = Math.min(now + entry.ttlMs, entry.absoluteExpiresAt);
409
+ // Move refreshed entry to the tail so the bounded sweep can
410
+ // progress past actively-refreshed ids. `set` on an existing
411
+ // key preserves the entry reference, so no copy is made.
412
+ this.hardTimedOut.delete(id);
413
+ this.hardTimedOut.set(id, entry);
414
+ return true;
415
+ }
416
+ /** Number of writes currently in flight. Primarily for tests. */
417
+ get size() {
418
+ return this.pending.size;
419
+ }
420
+ /**
421
+ * Number of ids currently in the hard-timed-out marker state.
422
+ * Primarily for tests. Delegates to the shared `sweepExpired()`
423
+ * helper so read-count and write-sweep stay in lockstep.
424
+ *
425
+ * Caveat: because the sweep is bounded (`MAX_SWEEP_PER_INSERT`
426
+ * visits per call), the reported size may include still-present
427
+ * expired entries that sit past the per-call visit budget.
428
+ * Callers needing exact reclaimed-count semantics should drive
429
+ * further `markHardTimedOut()` inserts (each drains another
430
+ * batch) or call `isHardTimedOut(id)` directly — the read-path
431
+ * deletion is authoritative and unbounded per-id.
432
+ */
433
+ get hardTimedOutSize() {
434
+ this.sweepExpired();
435
+ return this.hardTimedOut.size;
436
+ }
437
+ /**
438
+ * Number of ids currently holding a scalar entry in the
439
+ * pending-side earliest-expiry map. Primarily for tests —
440
+ * regressions that need to validate the pending-side map is
441
+ * drained (independent of the marker-map fallback in
442
+ * `getEarliestExpiresAtMs`) require a direct readout.
443
+ */
444
+ get earliestExpiresByPendingSize() {
445
+ return this.earliestExpiresByPending.size;
446
+ }
447
+ }
448
+ /**
449
+ * Stable `WeakMap` keyed on `ResponseStore` instances so every
450
+ * caller gets the SAME tracker for a given store without having to
451
+ * thread it through handler options. A `WeakMap` is safe here
452
+ * because neither the store nor the tracker retain strong
453
+ * references into the tracker map's keyset — if the store is GC'd
454
+ * the tracker goes with it.
455
+ */
456
+ const STORE_TRACKERS = new WeakMap();
457
+ /**
458
+ * Fetch (or lazily create) the tracker for a given store. Always
459
+ * returns the same tracker for the same store instance.
460
+ */
461
+ export function getPendingWritesFor(store) {
462
+ let tracker = STORE_TRACKERS.get(store);
463
+ if (tracker === undefined) {
464
+ tracker = new PendingResponseWrites();
465
+ STORE_TRACKERS.set(store, tracker);
466
+ }
467
+ return tracker;
468
+ }