experimental-a2 0.17.0 → 0.18.1

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 (221) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +1 -1
  3. package/dist/{actor-DBbe5xeE.d.ts → actor-C7b-1arn.d.ts} +5 -5
  4. package/dist/{actor-DBbe5xeE.d.ts.map → actor-C7b-1arn.d.ts.map} +1 -1
  5. package/dist/actor-client.d.ts +1 -1
  6. package/dist/actor-react.d.ts +3 -3
  7. package/dist/actor-react.js +1 -1
  8. package/dist/actor.d.ts +1 -1
  9. package/dist/actor.js +2 -2
  10. package/dist/{ai-vpGtcaCB.d.ts → ai-BMLlI2Ff.d.ts} +3 -3
  11. package/dist/{ai-vpGtcaCB.d.ts.map → ai-BMLlI2Ff.d.ts.map} +1 -1
  12. package/dist/{ai-UrWfKeuv.js → ai-D6mGlO8j.js} +2 -2
  13. package/dist/{ai-UrWfKeuv.js.map → ai-D6mGlO8j.js.map} +1 -1
  14. package/dist/ai-client.d.ts +2 -2
  15. package/dist/ai-client.js +2 -2
  16. package/dist/ai-fx.browser.d.ts +1 -0
  17. package/dist/ai-fx.browser.js +6 -0
  18. package/dist/ai-fx.browser.js.map +1 -0
  19. package/dist/ai-fx.d.ts +10 -0
  20. package/dist/ai-fx.d.ts.map +1 -0
  21. package/dist/ai-fx.js +692 -0
  22. package/dist/ai-fx.js.map +1 -0
  23. package/dist/ai-harness-B2suP2Qh.d.ts +26 -0
  24. package/dist/ai-harness-B2suP2Qh.d.ts.map +1 -0
  25. package/dist/ai-harness-BuLh_EYi.js +273 -0
  26. package/dist/ai-harness-BuLh_EYi.js.map +1 -0
  27. package/dist/ai-pi.browser.d.ts +1 -0
  28. package/dist/ai-pi.browser.js +6 -0
  29. package/dist/ai-pi.browser.js.map +1 -0
  30. package/dist/ai-pi.d.ts +17 -0
  31. package/dist/ai-pi.d.ts.map +1 -0
  32. package/dist/ai-pi.js +950 -0
  33. package/dist/ai-pi.js.map +1 -0
  34. package/dist/ai-react.d.ts +5 -5
  35. package/dist/ai-react.js +2 -2
  36. package/dist/ai-server.d.ts +47 -25
  37. package/dist/ai-server.d.ts.map +1 -1
  38. package/dist/ai-server.js +113 -40
  39. package/dist/ai-server.js.map +1 -1
  40. package/dist/ai-tool-input-DmSqMUQi.js +40 -0
  41. package/dist/ai-tool-input-DmSqMUQi.js.map +1 -0
  42. package/dist/ai.d.ts +1 -1
  43. package/dist/ai.js +1 -1
  44. package/dist/bounded-cache-CPHWF-8h.js +41 -0
  45. package/dist/bounded-cache-CPHWF-8h.js.map +1 -0
  46. package/dist/{client-COaiaDub.js → client-BhHLoNKF.js} +3 -3
  47. package/dist/{client-COaiaDub.js.map → client-BhHLoNKF.js.map} +1 -1
  48. package/dist/{client-qRnRUzBS.d.ts → client-h_DKFAjj.d.ts} +3 -3
  49. package/dist/{client-qRnRUzBS.d.ts.map → client-h_DKFAjj.d.ts.map} +1 -1
  50. package/dist/client.d.ts +3 -3
  51. package/dist/client.js +1 -1
  52. package/dist/index.d.ts +5 -5
  53. package/dist/{initial-snapshot-BrLso5x_.d.ts → initial-snapshot-Bx-c0pDP.d.ts} +2 -2
  54. package/dist/{initial-snapshot-BrLso5x_.d.ts.map → initial-snapshot-Bx-c0pDP.d.ts.map} +1 -1
  55. package/dist/{internal-CbVvEwad.js → internal-CscSY7K_.js} +11 -3
  56. package/dist/internal-CscSY7K_.js.map +1 -0
  57. package/dist/otel.d.ts +1 -1
  58. package/dist/{react-BbJ-g6Uu.d.ts → react-BBXgHWFc.d.ts} +4 -4
  59. package/dist/{react-BbJ-g6Uu.d.ts.map → react-BBXgHWFc.d.ts.map} +1 -1
  60. package/dist/react.d.ts +1 -1
  61. package/dist/react.js +1 -1
  62. package/dist/reducer-BAXc8JyF.js.map +1 -1
  63. package/dist/{reducer-BYu6DT7h.d.ts → reducer-CqA59Q6U.d.ts} +2 -2
  64. package/dist/{reducer-BYu6DT7h.d.ts.map → reducer-CqA59Q6U.d.ts.map} +1 -1
  65. package/dist/scheduler-qstash.d.ts +2 -2
  66. package/dist/scheduler-qstash.js +3 -3
  67. package/dist/scheduler-qstash.js.map +1 -1
  68. package/dist/scheduler-vercel.d.ts +7 -7
  69. package/dist/scheduler-vercel.d.ts.map +1 -1
  70. package/dist/scheduler-vercel.js +9 -10
  71. package/dist/scheduler-vercel.js.map +1 -1
  72. package/dist/{server-Rj64fNvX.js → server-D-Y4pqwu.js} +126 -67
  73. package/dist/server-D-Y4pqwu.js.map +1 -0
  74. package/dist/{server-CGM8WzRG.d.ts → server-DYAATFuT.d.ts} +42 -7
  75. package/dist/server-DYAATFuT.d.ts.map +1 -0
  76. package/dist/server.d.ts +4 -4
  77. package/dist/server.js +1 -1
  78. package/dist/{session-clear-Dshpuhff.d.ts → session-clear-C-QwTXV1.d.ts} +2 -2
  79. package/dist/session-clear-C-QwTXV1.d.ts.map +1 -0
  80. package/dist/{store-Gpkq7WBo.d.ts → store-CyLUy86v.d.ts} +53 -5
  81. package/dist/store-CyLUy86v.d.ts.map +1 -0
  82. package/dist/store-N8PXxDAS.js.map +1 -1
  83. package/dist/store-memory.d.ts +1 -1
  84. package/dist/store-memory.js +56 -11
  85. package/dist/store-memory.js.map +1 -1
  86. package/dist/{store-polling-4_y8lETm.js → store-polling-7WJV0YWP.js} +2 -2
  87. package/dist/{store-polling-4_y8lETm.js.map → store-polling-7WJV0YWP.js.map} +1 -1
  88. package/dist/store-postgres.d.ts +1 -1
  89. package/dist/store-postgres.d.ts.map +1 -1
  90. package/dist/store-postgres.js +106 -25
  91. package/dist/store-postgres.js.map +1 -1
  92. package/dist/{store-presence-polling-Q1C-k2dd.js → store-presence-polling-B1ixeqIS.js} +3 -3
  93. package/dist/{store-presence-polling-Q1C-k2dd.js.map → store-presence-polling-B1ixeqIS.js.map} +1 -1
  94. package/dist/store-redis-http.d.ts +1 -1
  95. package/dist/store-redis-http.js +3 -3
  96. package/dist/{store-redis-notify-tqWNnMRu.js → store-redis-notify-CM0AfLY0.js} +271 -101
  97. package/dist/store-redis-notify-CM0AfLY0.js.map +1 -0
  98. package/dist/store-redis.d.ts +1 -1
  99. package/dist/store-redis.js +1 -1
  100. package/dist/store-sqlite.d.ts +1 -1
  101. package/dist/store-sqlite.js +66 -21
  102. package/dist/store-sqlite.js.map +1 -1
  103. package/dist/{telemetry-cZH-cibj.d.ts → telemetry-Cyfqo6Qt.d.ts} +2 -2
  104. package/dist/telemetry-Cyfqo6Qt.d.ts.map +1 -0
  105. package/dist/{wire-DjfHOvdW.js → wire-BKxH1vbP.js} +2 -2
  106. package/dist/{wire-DjfHOvdW.js.map → wire-BKxH1vbP.js.map} +1 -1
  107. package/docs/01-quickstart.mdx +49 -83
  108. package/docs/{guides/07-examples.mdx → 02-examples.mdx} +10 -20
  109. package/docs/actors/01-introduction.mdx +48 -86
  110. package/docs/actors/02-concurrency.mdx +43 -70
  111. package/docs/actors/03-timers.mdx +28 -42
  112. package/docs/actors/04-routes.mdx +110 -150
  113. package/docs/actors/meta.ts +1 -1
  114. package/docs/agents/01-introduction.mdx +257 -0
  115. package/docs/agents/02-tools.mdx +247 -0
  116. package/docs/agents/03-conversations.mdx +538 -0
  117. package/docs/agents/04-harnesses.mdx +136 -0
  118. package/docs/agents/meta.ts +1 -0
  119. package/docs/client/01-react.mdx +294 -0
  120. package/docs/client/02-transports.mdx +86 -0
  121. package/docs/client/03-presence.mdx +224 -0
  122. package/docs/client/meta.ts +1 -0
  123. package/docs/contract/01-events.mdx +57 -0
  124. package/docs/contract/02-state.mdx +142 -0
  125. package/docs/contract/03-sessions.mdx +175 -0
  126. package/docs/index.mdx +109 -145
  127. package/docs/reference/01-core.mdx +139 -0
  128. package/docs/reference/02-server.mdx +860 -0
  129. package/docs/reference/03-client.mdx +301 -0
  130. package/docs/reference/04-ai.mdx +731 -0
  131. package/docs/reference/05-adapters.mdx +351 -0
  132. package/docs/reference/06-errors.mdx +138 -0
  133. package/docs/reference/meta.ts +1 -1
  134. package/docs/server/01-handlers.mdx +200 -0
  135. package/docs/server/02-timers.mdx +130 -0
  136. package/docs/server/03-cancellation.mdx +142 -0
  137. package/docs/server/04-routes.mdx +243 -0
  138. package/docs/server/05-production.mdx +323 -0
  139. package/docs/server/06-guarantees.mdx +132 -0
  140. package/examples/playground/DEPLOY.md +3 -5
  141. package/examples/playground/app/a0/[appId]/page.tsx +12 -13
  142. package/examples/playground/app/a0/app-list.tsx +9 -2
  143. package/examples/playground/app/a0/app-view.tsx +9 -1
  144. package/examples/playground/app/a0/apps/[appId]/route.ts +10 -5
  145. package/examples/playground/app/a0/apps.test.ts +12 -0
  146. package/examples/playground/app/a0/apps.ts +22 -7
  147. package/examples/playground/app/a0/chat.tsx +47 -1
  148. package/examples/playground/app/a0/devtools.tsx +2 -8
  149. package/examples/playground/app/a0/fx/events/route.ts +9 -0
  150. package/examples/playground/app/a0/fx/scheduler/route.ts +7 -0
  151. package/examples/playground/app/a0/fx/short/events/route.ts +11 -0
  152. package/examples/playground/app/a0/model.test.ts +18 -1
  153. package/examples/playground/app/a0/model.ts +47 -10
  154. package/examples/playground/app/a0/no-photon.cjs +4 -0
  155. package/examples/playground/app/a0/no-quickjs.cjs +3 -0
  156. package/examples/playground/app/a0/pi/events/route.ts +9 -0
  157. package/examples/playground/app/a0/pi/scheduler/route.ts +7 -0
  158. package/examples/playground/app/a0/pi/short/events/route.ts +11 -0
  159. package/examples/playground/app/a0/sandbox-log.ts +10 -4
  160. package/examples/playground/app/a0/sandbox-session.ts +152 -0
  161. package/examples/playground/app/a0/scheduler/route.ts +2 -2
  162. package/examples/playground/app/a0/server.ts +98 -54
  163. package/examples/playground/app/a0/session.ts +17 -5
  164. package/examples/playground/app/a0/shell.tsx +11 -3
  165. package/examples/playground/app/agent/ingress.test.ts +6 -3
  166. package/examples/playground/app/agent/server.ts +9 -6
  167. package/examples/playground/app/agent/telemetry.test.ts +71 -50
  168. package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +1 -1
  169. package/examples/playground/app/chat/server.test.ts +30 -26
  170. package/examples/playground/app/chat/server.ts +46 -46
  171. package/examples/playground/next.config.ts +17 -5
  172. package/examples/playground/package.json +4 -1
  173. package/examples/playground/vercel.json +18 -0
  174. package/package.json +27 -3
  175. package/src/ai-agent.ts +271 -90
  176. package/src/ai-fx.browser.ts +5 -0
  177. package/src/ai-fx.ts +912 -0
  178. package/src/ai-harness.ts +518 -0
  179. package/src/ai-model-metadata.ts +11 -6
  180. package/src/ai-pi.browser.ts +5 -0
  181. package/src/ai-pi.ts +1322 -0
  182. package/src/ai-sdk-step.ts +2 -2
  183. package/src/ai-server.ts +3 -1
  184. package/src/ai-tool-input.ts +45 -0
  185. package/src/internal.ts +13 -0
  186. package/src/libfx.d.ts +34 -0
  187. package/src/reducer.ts +1 -1
  188. package/src/scheduler-qstash.ts +5 -5
  189. package/src/scheduler-vercel.ts +14 -15
  190. package/src/server.ts +233 -29
  191. package/src/store-memory.ts +92 -16
  192. package/src/store-postgres.ts +156 -25
  193. package/src/store-redis-core.ts +322 -110
  194. package/src/store-redis-notify.ts +2 -0
  195. package/src/store-sqlite.ts +121 -28
  196. package/src/store.ts +55 -3
  197. package/src/telemetry.ts +1 -0
  198. package/dist/internal-CbVvEwad.js.map +0 -1
  199. package/dist/server-CGM8WzRG.d.ts.map +0 -1
  200. package/dist/server-Rj64fNvX.js.map +0 -1
  201. package/dist/session-clear-Dshpuhff.d.ts.map +0 -1
  202. package/dist/store-Gpkq7WBo.d.ts.map +0 -1
  203. package/dist/store-redis-notify-tqWNnMRu.js.map +0 -1
  204. package/dist/telemetry-cZH-cibj.d.ts.map +0 -1
  205. package/docs/concepts/01-contracts.mdx +0 -237
  206. package/docs/concepts/02-handlers.mdx +0 -284
  207. package/docs/concepts/03-durability.mdx +0 -314
  208. package/docs/concepts/04-state.mdx +0 -209
  209. package/docs/guides/01-timers.mdx +0 -193
  210. package/docs/guides/02-cancellation.mdx +0 -161
  211. package/docs/guides/03-react.mdx +0 -324
  212. package/docs/guides/04-local-first.mdx +0 -47
  213. package/docs/guides/05-production.mdx +0 -534
  214. package/docs/guides/06-ai-agents.mdx +0 -1408
  215. package/docs/guides/08-application-data.mdx +0 -113
  216. package/docs/guides/09-presence.mdx +0 -262
  217. package/docs/guides/10-transports.mdx +0 -221
  218. package/docs/reference/01-api.mdx +0 -2358
  219. package/docs/reference/02-errors.mdx +0 -150
  220. /package/docs/{concepts → contract}/meta.ts +0 -0
  221. /package/docs/{guides → server}/meta.ts +0 -0
@@ -1,5 +1,5 @@
1
- import { _ as serverInternals, d as assertSchedulerTargets, f as classifySchedulerSendFailure, t as DRAIN_TIMINGS } from "./internal-CbVvEwad.js";
2
- import { n as deliverSchedulerAppend, r as deliverSchedulerClear } from "./server-Rj64fNvX.js";
1
+ import { d as assertSchedulerTargets, f as classifySchedulerSendFailure, t as DRAIN_TIMINGS, v as serverInternals } from "./internal-CscSY7K_.js";
2
+ import { n as deliverSchedulerAppend, r as deliverSchedulerClear } from "./server-D-Y4pqwu.js";
3
3
  import { t as retryableLazy } from "./retryable-lazy-DZWmHpii.js";
4
4
  import { n as SYSTEM_CLOCK } from "./store-N8PXxDAS.js";
5
5
  import { t as normalizeSchedulerTask } from "./scheduler-task-X1SToDjP.js";
@@ -21,20 +21,19 @@ import { createHash } from "node:crypto";
21
21
  * the configured deployment scope for sending and receiving.
22
22
  * - `handler(...servers)` is the delivery route: look up the server for
23
23
  * the contract named in the message and drain it. Settled drains ack;
24
- * contention arms a later watchdog before acking; handler failure keeps
25
- * the current message and redelivers with backoff.
24
+ * contention and handler failure arm a later watchdog before acking.
26
25
  *
27
26
  * `@vercel/queue` is an optional peer dependency, imported lazily. The
28
27
  * `transport` option is the injection seam — tests run a real little
29
28
  * in-memory queue through the same interface.
30
29
  */
31
30
  /**
32
- * The "not settled yet" signal for a stalled handler — an expected state,
33
- * not a transport failure. The default transport classifies it in
34
- * `@vercel/queue`'s `retry` option and reschedules the delivery quietly
31
+ * The "not settled yet" signal for a drain whose outcome is unknown — an
32
+ * expected state, not a transport failure. The default transport classifies
33
+ * it in `@vercel/queue`'s `retry` option and reschedules the delivery quietly
35
34
  * (`{ afterSeconds }` → visibility change → 200 response, no error log)
36
- * instead of letting it surface as a thrown 500. Busy deliveries use a
37
- * fresh watchdog instead. Custom transports should treat this class the
35
+ * instead of letting it surface as a thrown 500. Busy and stalled deliveries
36
+ * use a fresh watchdog instead. Custom transports should treat this class the
38
37
  * same way when their protocol has a polite reschedule.
39
38
  */
40
39
  var DrainUnsettled = class extends Error {};
@@ -186,7 +185,7 @@ function vercelQueues(options = {}) {
186
185
  outcome: "unknown"
187
186
  };
188
187
  if (result.settled) return;
189
- if (result.outcome === "busy") {
188
+ if (result.outcome === "busy" || result.outcome === "stalled") {
190
189
  if (result.schedulerArm) await result.schedulerArm;
191
190
  else await sendTask({
192
191
  version: 1,
@@ -1 +1 @@
1
- {"version":3,"file":"scheduler-vercel.js","names":[],"sources":["../src/scheduler-vercel.ts"],"sourcesContent":["/**\n * experimental-a2/scheduler-vercel — queue-backed scheduler over Vercel Queues.\n *\n * `vercelQueues()` returns an `A2Scheduler` (a2-implementation.md §9):\n *\n * - drain tasks round the requested watchdog time up to a one-second slot.\n * One message per `(topic, contract, session, dueAt)` coalesces appends,\n * claim renewals and racing callbacks. `DuplicateMessageError` is\n * success because that due-time slot is already durable.\n * - append tasks hash their stable task id with the topic for provider\n * deduplication. Their six-day maximum delay leaves a full day for\n * delivery and retries inside the provider's seven-day retention.\n * Clear tasks follow the same identity and delay rules. All tasks use\n * the configured deployment scope for sending and receiving.\n * - `handler(...servers)` is the delivery route: look up the server for\n * the contract named in the message and drain it. Settled drains ack;\n * contention arms a later watchdog before acking; handler failure keeps\n * the current message and redelivers with backoff.\n *\n * `@vercel/queue` is an optional peer dependency, imported lazily. The\n * `transport` option is the injection seam — tests run a real little\n * in-memory queue through the same interface.\n */\n\nimport { createHash } from 'node:crypto'\nimport { SYSTEM_CLOCK, type Clock } from './store.ts'\nimport {\n assertSchedulerTargets,\n classifySchedulerSendFailure,\n DRAIN_TIMINGS,\n serverInternals,\n} from './internal.ts'\nimport { retryableLazy } from './retryable-lazy.ts'\nimport {\n normalizeSchedulerTask,\n type SchedulerDrainTask,\n type SchedulerTask,\n} from './scheduler-task.ts'\nimport {\n deliverSchedulerAppend,\n deliverSchedulerClear,\n type A2Scheduler,\n type DrainableServer,\n} from './server.ts'\n\n/**\n * The \"not settled yet\" signal for a stalled handler — an expected state,\n * not a transport failure. The default transport classifies it in\n * `@vercel/queue`'s `retry` option and reschedules the delivery quietly\n * (`{ afterSeconds }` → visibility change → 200 response, no error log)\n * instead of letting it surface as a thrown 500. Busy deliveries use a\n * fresh watchdog instead. Custom transports should treat this class the\n * same way when their protocol has a polite reschedule.\n */\nexport class DrainUnsettled extends Error {}\n\nexport type QueueDeliveryMetadata = {\n messageId: string\n deliveryCount: number\n}\n\n/** What this module needs from a Vercel Queues-compatible transport. */\nexport type QueueTransport = {\n send(\n topic: string,\n message: SchedulerTask,\n options?: {\n delaySeconds?: number\n idempotencyKey?: string\n retentionSeconds?: number\n },\n ): Promise<unknown>\n /**\n * Wrap the delivery handler into a route. The handler throws\n * `DrainUnsettled` when the session needs redelivery — transports\n * with a polite reschedule (visibility change) should use it there\n * instead of surfacing an error; anything else thrown is a genuine\n * failure and should stay loud.\n */\n handleCallback(\n handler: (\n message: unknown,\n metadata: QueueDeliveryMetadata,\n ) => Promise<void>,\n ): (req: Request) => Promise<Response>\n /** Is this send error the idempotency-key collision? (= success) */\n isDuplicate(error: unknown): boolean\n}\n\nexport type VercelQueuesOptions = {\n /** Queue topic matching `[A-Za-z0-9_-]+`. Default `'a2'`. */\n topic?: string\n /** Queue SDK deployment scope for all tasks. Default: VERCEL_DEPLOYMENT_ID; null opts out. */\n deploymentId?: string | null\n /** Injectable transport (tests). Default: `@vercel/queue`. */\n transport?: QueueTransport\n /** Injectable clock for due-time calculations (tests). */\n clock?: Clock\n}\n\n/** Vercel Queues rejects shorter visibility changes. */\nconst MIN_VISIBILITY_SECONDS = 30\n\n/** Vercel Queues' maximum message retention and delivery delay. */\nconst MAX_RETENTION_SECONDS = 7 * 24 * 60 * 60\n\n/** Leave the final retention day for delivery and retries. */\nconst MAX_DELAY_SECONDS = 6 * 24 * 60 * 60\n\nconst TOPIC_PATTERN = /^[A-Za-z0-9_-]+$/\nconst RETRYABLE_HTTP_CLIENT_STATUSES = new Set([408, 409, 425, 429])\n/** The SDK's send path maps these statuses to name-only error instances. */\nconst TERMINAL_VERCEL_SEND_ERROR_NAMES = new Set([\n 'BadRequestError',\n 'UnauthorizedError',\n 'ForbiddenError',\n])\n\nconst recordOf = (value: unknown): Record<string, unknown> | null =>\n value !== null && typeof value === 'object' && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : null\n\nconst httpStatusOf = (error: unknown): number | undefined => {\n try {\n const record = recordOf(error)\n if (!record) return undefined\n const response = recordOf(record['response'])\n const statuses = [\n record['status'],\n record['statusCode'],\n response?.['status'],\n ].filter(\n (value): value is number =>\n typeof value === 'number' &&\n Number.isInteger(value) &&\n value >= 100 &&\n value <= 599,\n )\n const [status] = statuses\n if (status === undefined) return undefined\n return statuses.every((candidate) => candidate === status)\n ? status\n : undefined\n } catch {\n return undefined\n }\n}\n\nconst errorNameOf = (error: unknown): string | undefined => {\n try {\n const name = recordOf(error)?.['name']\n return typeof name === 'string' ? name : undefined\n } catch {\n return undefined\n }\n}\n\nconst classifyVercelSendFailure = (error: unknown): unknown => {\n const status = httpStatusOf(error)\n const terminal =\n status === undefined\n ? TERMINAL_VERCEL_SEND_ERROR_NAMES.has(errorNameOf(error) ?? '')\n : status >= 400 &&\n status < 500 &&\n !RETRYABLE_HTTP_CLIENT_STATUSES.has(status)\n return classifySchedulerSendFailure(\n error,\n terminal ? 'terminal' : 'retryable',\n )\n}\n\nconst digestId = (parts: readonly string[]): string =>\n `a2_${createHash('sha256').update(JSON.stringify(parts)).digest('hex')}`\n\n/** Base of the unsettled redelivery backoff, doubled per delivery. */\nconst UNSETTLED_BACKOFF_BASE_SECONDS = 5\n\n/** Redelivery backoff for retry states: base, 2×, 4×… bounded. */\nconst unsettledBackoffSeconds = (deliveryCount: number): number =>\n Math.min(\n Math.max(\n UNSETTLED_BACKOFF_BASE_SECONDS * 2 ** Math.max(deliveryCount - 1, 0),\n MIN_VISIBILITY_SECONDS,\n ),\n 60,\n )\n\nconst vercelQueueTransport = async (\n clientOptions: Pick<VercelQueuesOptions, 'deploymentId'>,\n): Promise<QueueTransport> => {\n const queue = await import('@vercel/queue').catch(() => {\n throw classifySchedulerSendFailure(\n new Error(\n \"experimental-a2/scheduler-vercel needs the '@vercel/queue' package (optional peer dependency) — install it, or inject a transport\",\n ),\n 'terminal',\n )\n })\n const client = new queue.QueueClient(clientOptions)\n return {\n send: (topic, message, options) => client.send(topic, message, options),\n handleCallback: (handler) =>\n client.handleCallback(handler, {\n // A hard-killed callback normally has a fresh claim watchdog. If\n // its successor send also failed, make the old unacked delivery's\n // fallback as prompt as Vercel Queues permits.\n visibilityTimeoutSeconds: MIN_VISIBILITY_SECONDS,\n // Not-settled-yet is an expected state: reschedule the (still\n // unacked) delivery via a visibility change — 200, no error\n // log. Everything else returns undefined → the SDK's default\n // throw path, which is loud on purpose.\n retry: (error, metadata) =>\n error instanceof DrainUnsettled\n ? {\n afterSeconds: unsettledBackoffSeconds(metadata.deliveryCount),\n }\n : undefined,\n }) as (req: Request) => Promise<Response>,\n isDuplicate: (error) => error instanceof queue.DuplicateMessageError,\n }\n}\n\nexport function vercelQueues(options: VercelQueuesOptions = {}): A2Scheduler {\n const topic = options.topic ?? 'a2'\n if (typeof topic !== 'string' || !TOPIC_PATTERN.test(topic)) {\n throw new TypeError('vercelQueues() topic must match [A-Za-z0-9_-]+')\n }\n const clock = options.clock ?? SYSTEM_CLOCK\n const clientOptions =\n options.deploymentId === undefined\n ? {}\n : { deploymentId: options.deploymentId }\n\n // Lazy: constructing the scheduler value does no I/O and works without\n // the peer dep until the first task or delivery.\n const transport = retryableLazy(() =>\n options.transport\n ? Promise.resolve(options.transport)\n : vercelQueueTransport(clientOptions),\n ).get\n\n const sendTask = async (task: SchedulerTask): Promise<void> => {\n const nowMs = clock.now().getTime()\n const requestedDueAt = task.dueAt\n if (!Number.isFinite(requestedDueAt)) {\n throw classifySchedulerSendFailure(\n new TypeError('vercel scheduler dueAt must be a finite number'),\n 'terminal',\n )\n }\n const dueAt =\n requestedDueAt <= nowMs\n ? Math.floor(nowMs / 1_000) * 1_000\n : Math.ceil(requestedDueAt / 1_000) * 1_000\n const deliveryTargetAt = task.kind === 'drain' ? dueAt : requestedDueAt\n const deliveryDelaySeconds = Math.max(\n 0,\n Math.ceil((deliveryTargetAt - nowMs) / 1_000),\n )\n if (deliveryDelaySeconds > MAX_DELAY_SECONDS) {\n throw classifySchedulerSendFailure(\n new RangeError(\n 'vercelQueues() tasks cannot be delayed more than 6 days',\n ),\n 'terminal',\n )\n }\n const message: SchedulerTask = { ...task, dueAt }\n const idempotencyKey =\n task.kind === 'drain'\n ? digestId([\n 'a2:scheduler-vercel:drain:v1',\n topic,\n task.contract,\n task.sessionId,\n String(dueAt),\n ])\n : task.kind === 'append'\n ? digestId(['a2:scheduler-vercel:append:v1', topic, task.id])\n : digestId(['a2:scheduler-vercel:clear:v1', topic, task.id])\n const t = await transport()\n try {\n await t.send(topic, message, {\n delaySeconds: deliveryDelaySeconds,\n idempotencyKey,\n retentionSeconds: MAX_RETENTION_SECONDS,\n })\n } catch (err) {\n if (t.isDuplicate(err)) return\n throw classifyVercelSendFailure(err)\n }\n }\n\n const successorDueAt = (message: SchedulerDrainTask): number => {\n const minimumDueAt =\n clock.now().getTime() +\n DRAIN_TIMINGS.claimTtlMs +\n DRAIN_TIMINGS.watchdogGraceMs\n const beats = Math.max(\n 1,\n Math.ceil(\n (minimumDueAt - message.dueAt) / DRAIN_TIMINGS.claimHeartbeatMs,\n ),\n )\n return message.dueAt + beats * DRAIN_TIMINGS.claimHeartbeatMs\n }\n\n return {\n async schedule(task) {\n await sendTask(task)\n },\n\n handler(...servers: DrainableServer[]) {\n assertSchedulerTargets(servers)\n const byContract = new Map<string, DrainableServer>()\n for (const server of servers) {\n byContract.set(server.contract.name, server)\n }\n return async (req: Request): Promise<Response> => {\n const t = await transport()\n return t.handleCallback(async (delivered) => {\n const task = normalizeSchedulerTask(delivered)\n if (!task) {\n throw new TypeError('a2 scheduler received an invalid task')\n }\n const server = byContract.get(task.contract)\n if (!server) {\n // A message for a contract this route doesn't serve is a\n // wiring bug — throw so it redelivers and stays visible in\n // queue observability instead of vanishing on an ack.\n throw new Error(\n `a2 scheduler: no server for contract '${task.contract}' — pass it to scheduler.handler(...)`,\n )\n }\n const internals = serverInternals.get(server)\n if (task.kind === 'append') {\n await deliverSchedulerAppend(server, task)\n return\n }\n if (task.kind === 'clear') {\n await deliverSchedulerClear(server, task)\n return\n }\n\n const nextDueAt = successorDueAt(task)\n const result = internals\n ? await internals.schedulerDrain(task.sessionId, {\n watchdogDueAt: nextDueAt,\n })\n : {\n ...(await server.drain(task.sessionId)),\n outcome: 'unknown' as const,\n }\n if (result.settled) return\n\n if (result.outcome === 'busy') {\n // The live owner normally arms its own claim windows.\n // Ensure a later slot here as well before relinquishing this\n // durable delivery. If the queue is down, the throw leaves the\n // current message unacked for visibility-timeout redelivery.\n if (result.schedulerArm) await result.schedulerArm\n else {\n await sendTask({\n version: 1,\n kind: 'drain',\n contract: task.contract,\n sessionId: task.sessionId,\n dueAt: nextDueAt,\n })\n }\n return\n }\n\n // Handler failure keeps the current durable message and uses the\n // transport retry path. Dead-lettered sessions count as settled\n // above, so retries stop there.\n throw new DrainUnsettled(\n `a2 scheduler: session '${task.sessionId}' of '${task.contract}' not settled yet (${result.outcome}) — rescheduled for redelivery`,\n )\n })(req)\n }\n },\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"scheduler-vercel.js","names":[],"sources":["../src/scheduler-vercel.ts"],"sourcesContent":["/**\n * experimental-a2/scheduler-vercel — queue-backed scheduler over Vercel Queues.\n *\n * `vercelQueues()` returns an `A2Scheduler` (a2-implementation.md §9):\n *\n * - drain tasks round the requested watchdog time up to a one-second slot.\n * One message per `(topic, contract, session, dueAt)` coalesces appends,\n * claim renewals and racing callbacks. `DuplicateMessageError` is\n * success because that due-time slot is already durable.\n * - append tasks hash their stable task id with the topic for provider\n * deduplication. Their six-day maximum delay leaves a full day for\n * delivery and retries inside the provider's seven-day retention.\n * Clear tasks follow the same identity and delay rules. All tasks use\n * the configured deployment scope for sending and receiving.\n * - `handler(...servers)` is the delivery route: look up the server for\n * the contract named in the message and drain it. Settled drains ack;\n * contention and handler failure arm a later watchdog before acking.\n *\n * `@vercel/queue` is an optional peer dependency, imported lazily. The\n * `transport` option is the injection seam — tests run a real little\n * in-memory queue through the same interface.\n */\n\nimport { createHash } from 'node:crypto'\nimport { SYSTEM_CLOCK, type Clock } from './store.ts'\nimport {\n assertSchedulerTargets,\n classifySchedulerSendFailure,\n DRAIN_TIMINGS,\n serverInternals,\n} from './internal.ts'\nimport { retryableLazy } from './retryable-lazy.ts'\nimport {\n normalizeSchedulerTask,\n type SchedulerDrainTask,\n type SchedulerTask,\n} from './scheduler-task.ts'\nimport {\n deliverSchedulerAppend,\n deliverSchedulerClear,\n type A2Scheduler,\n type DrainableServer,\n} from './server.ts'\n\n/**\n * The \"not settled yet\" signal for a drain whose outcome is unknown — an\n * expected state, not a transport failure. The default transport classifies\n * it in `@vercel/queue`'s `retry` option and reschedules the delivery quietly\n * (`{ afterSeconds }` → visibility change → 200 response, no error log)\n * instead of letting it surface as a thrown 500. Busy and stalled deliveries\n * use a fresh watchdog instead. Custom transports should treat this class the\n * same way when their protocol has a polite reschedule.\n */\nexport class DrainUnsettled extends Error {}\n\nexport type QueueDeliveryMetadata = {\n messageId: string\n deliveryCount: number\n}\n\n/** What this module needs from a Vercel Queues-compatible transport. */\nexport type QueueTransport = {\n send(\n topic: string,\n message: SchedulerTask,\n options?: {\n delaySeconds?: number\n idempotencyKey?: string\n retentionSeconds?: number\n },\n ): Promise<unknown>\n /**\n * Wrap the delivery handler into a route. The handler throws\n * `DrainUnsettled` when the session needs redelivery — transports\n * with a polite reschedule (visibility change) should use it there\n * instead of surfacing an error; anything else thrown is a genuine\n * failure and should stay loud.\n */\n handleCallback(\n handler: (\n message: unknown,\n metadata: QueueDeliveryMetadata,\n ) => Promise<void>,\n ): (req: Request) => Promise<Response>\n /** Is this send error the idempotency-key collision? (= success) */\n isDuplicate(error: unknown): boolean\n}\n\nexport type VercelQueuesOptions = {\n /** Queue topic matching `[A-Za-z0-9_-]+`. Default `'a2'`. */\n topic?: string\n /** Queue SDK deployment scope for all tasks. Default: VERCEL_DEPLOYMENT_ID; null opts out. */\n deploymentId?: string | null\n /** Injectable transport (tests). Default: `@vercel/queue`. */\n transport?: QueueTransport\n /** Injectable clock for due-time calculations (tests). */\n clock?: Clock\n}\n\n/** Vercel Queues rejects shorter visibility changes. */\nconst MIN_VISIBILITY_SECONDS = 30\n\n/** Vercel Queues' maximum message retention and delivery delay. */\nconst MAX_RETENTION_SECONDS = 7 * 24 * 60 * 60\n\n/** Leave the final retention day for delivery and retries. */\nconst MAX_DELAY_SECONDS = 6 * 24 * 60 * 60\n\nconst TOPIC_PATTERN = /^[A-Za-z0-9_-]+$/\nconst RETRYABLE_HTTP_CLIENT_STATUSES = new Set([408, 409, 425, 429])\n/** The SDK's send path maps these statuses to name-only error instances. */\nconst TERMINAL_VERCEL_SEND_ERROR_NAMES = new Set([\n 'BadRequestError',\n 'UnauthorizedError',\n 'ForbiddenError',\n])\n\nconst recordOf = (value: unknown): Record<string, unknown> | null =>\n value !== null && typeof value === 'object' && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : null\n\nconst httpStatusOf = (error: unknown): number | undefined => {\n try {\n const record = recordOf(error)\n if (!record) return undefined\n const response = recordOf(record['response'])\n const statuses = [\n record['status'],\n record['statusCode'],\n response?.['status'],\n ].filter(\n (value): value is number =>\n typeof value === 'number' &&\n Number.isInteger(value) &&\n value >= 100 &&\n value <= 599,\n )\n const [status] = statuses\n if (status === undefined) return undefined\n return statuses.every((candidate) => candidate === status)\n ? status\n : undefined\n } catch {\n return undefined\n }\n}\n\nconst errorNameOf = (error: unknown): string | undefined => {\n try {\n const name = recordOf(error)?.['name']\n return typeof name === 'string' ? name : undefined\n } catch {\n return undefined\n }\n}\n\nconst classifyVercelSendFailure = (error: unknown): unknown => {\n const status = httpStatusOf(error)\n const terminal =\n status === undefined\n ? TERMINAL_VERCEL_SEND_ERROR_NAMES.has(errorNameOf(error) ?? '')\n : status >= 400 &&\n status < 500 &&\n !RETRYABLE_HTTP_CLIENT_STATUSES.has(status)\n return classifySchedulerSendFailure(\n error,\n terminal ? 'terminal' : 'retryable',\n )\n}\n\nconst digestId = (parts: readonly string[]): string =>\n `a2_${createHash('sha256').update(JSON.stringify(parts)).digest('hex')}`\n\n/** Base of the unsettled redelivery backoff, doubled per delivery. */\nconst UNSETTLED_BACKOFF_BASE_SECONDS = 5\n\n/** Redelivery backoff for retry states: base, 2×, 4×… bounded. */\nconst unsettledBackoffSeconds = (deliveryCount: number): number =>\n Math.min(\n Math.max(\n UNSETTLED_BACKOFF_BASE_SECONDS * 2 ** Math.max(deliveryCount - 1, 0),\n MIN_VISIBILITY_SECONDS,\n ),\n 60,\n )\n\nconst vercelQueueTransport = async (\n clientOptions: Pick<VercelQueuesOptions, 'deploymentId'>,\n): Promise<QueueTransport> => {\n const queue = await import('@vercel/queue').catch(() => {\n throw classifySchedulerSendFailure(\n new Error(\n \"experimental-a2/scheduler-vercel needs the '@vercel/queue' package (optional peer dependency) — install it, or inject a transport\",\n ),\n 'terminal',\n )\n })\n const client = new queue.QueueClient(clientOptions)\n return {\n send: (topic, message, options) => client.send(topic, message, options),\n handleCallback: (handler) =>\n client.handleCallback(handler, {\n // A hard-killed callback normally has a fresh claim watchdog. If\n // its successor send also failed, make the old unacked delivery's\n // fallback as prompt as Vercel Queues permits.\n visibilityTimeoutSeconds: MIN_VISIBILITY_SECONDS,\n // Not-settled-yet is an expected state: reschedule the (still\n // unacked) delivery via a visibility change — 200, no error\n // log. Everything else returns undefined → the SDK's default\n // throw path, which is loud on purpose.\n retry: (error, metadata) =>\n error instanceof DrainUnsettled\n ? {\n afterSeconds: unsettledBackoffSeconds(metadata.deliveryCount),\n }\n : undefined,\n }) as (req: Request) => Promise<Response>,\n isDuplicate: (error) => error instanceof queue.DuplicateMessageError,\n }\n}\n\nexport function vercelQueues(options: VercelQueuesOptions = {}): A2Scheduler {\n const topic = options.topic ?? 'a2'\n if (typeof topic !== 'string' || !TOPIC_PATTERN.test(topic)) {\n throw new TypeError('vercelQueues() topic must match [A-Za-z0-9_-]+')\n }\n const clock = options.clock ?? SYSTEM_CLOCK\n const clientOptions =\n options.deploymentId === undefined\n ? {}\n : { deploymentId: options.deploymentId }\n\n // Lazy: constructing the scheduler value does no I/O and works without\n // the peer dep until the first task or delivery.\n const transport = retryableLazy(() =>\n options.transport\n ? Promise.resolve(options.transport)\n : vercelQueueTransport(clientOptions),\n ).get\n\n const sendTask = async (task: SchedulerTask): Promise<void> => {\n const nowMs = clock.now().getTime()\n const requestedDueAt = task.dueAt\n if (!Number.isFinite(requestedDueAt)) {\n throw classifySchedulerSendFailure(\n new TypeError('vercel scheduler dueAt must be a finite number'),\n 'terminal',\n )\n }\n const dueAt =\n requestedDueAt <= nowMs\n ? Math.floor(nowMs / 1_000) * 1_000\n : Math.ceil(requestedDueAt / 1_000) * 1_000\n const deliveryTargetAt = task.kind === 'drain' ? dueAt : requestedDueAt\n const deliveryDelaySeconds = Math.max(\n 0,\n Math.ceil((deliveryTargetAt - nowMs) / 1_000),\n )\n if (deliveryDelaySeconds > MAX_DELAY_SECONDS) {\n throw classifySchedulerSendFailure(\n new RangeError(\n 'vercelQueues() tasks cannot be delayed more than 6 days',\n ),\n 'terminal',\n )\n }\n const message: SchedulerTask = { ...task, dueAt }\n const idempotencyKey =\n task.kind === 'drain'\n ? digestId([\n 'a2:scheduler-vercel:drain:v1',\n topic,\n task.contract,\n task.sessionId,\n String(dueAt),\n ])\n : task.kind === 'append'\n ? digestId(['a2:scheduler-vercel:append:v1', topic, task.id])\n : digestId(['a2:scheduler-vercel:clear:v1', topic, task.id])\n const t = await transport()\n try {\n await t.send(topic, message, {\n delaySeconds: deliveryDelaySeconds,\n idempotencyKey,\n retentionSeconds: MAX_RETENTION_SECONDS,\n })\n } catch (err) {\n if (t.isDuplicate(err)) return\n throw classifyVercelSendFailure(err)\n }\n }\n\n const successorDueAt = (message: SchedulerDrainTask): number => {\n const minimumDueAt =\n clock.now().getTime() +\n DRAIN_TIMINGS.claimTtlMs +\n DRAIN_TIMINGS.watchdogGraceMs\n const beats = Math.max(\n 1,\n Math.ceil(\n (minimumDueAt - message.dueAt) / DRAIN_TIMINGS.claimHeartbeatMs,\n ),\n )\n return message.dueAt + beats * DRAIN_TIMINGS.claimHeartbeatMs\n }\n\n return {\n async schedule(task) {\n await sendTask(task)\n },\n\n handler(...servers: DrainableServer[]) {\n assertSchedulerTargets(servers)\n const byContract = new Map<string, DrainableServer>()\n for (const server of servers) {\n byContract.set(server.contract.name, server)\n }\n return async (req: Request): Promise<Response> => {\n const t = await transport()\n return t.handleCallback(async (delivered) => {\n const task = normalizeSchedulerTask(delivered)\n if (!task) {\n throw new TypeError('a2 scheduler received an invalid task')\n }\n const server = byContract.get(task.contract)\n if (!server) {\n // A message for a contract this route doesn't serve is a\n // wiring bug — throw so it redelivers and stays visible in\n // queue observability instead of vanishing on an ack.\n throw new Error(\n `a2 scheduler: no server for contract '${task.contract}' — pass it to scheduler.handler(...)`,\n )\n }\n const internals = serverInternals.get(server)\n if (task.kind === 'append') {\n await deliverSchedulerAppend(server, task)\n return\n }\n if (task.kind === 'clear') {\n await deliverSchedulerClear(server, task)\n return\n }\n\n const nextDueAt = successorDueAt(task)\n const result = internals\n ? await internals.schedulerDrain(task.sessionId, {\n watchdogDueAt: nextDueAt,\n })\n : {\n ...(await server.drain(task.sessionId)),\n outcome: 'unknown' as const,\n }\n if (result.settled) return\n\n if (result.outcome === 'busy' || result.outcome === 'stalled') {\n // The live owner arms its own claim windows, and the store holds\n // a failed event until its retry time. Ensure a later slot here\n // as well before relinquishing this durable delivery. If the queue is down, the\n // throw leaves the current message unacked for visibility-timeout\n // redelivery.\n if (result.schedulerArm) await result.schedulerArm\n else {\n await sendTask({\n version: 1,\n kind: 'drain',\n contract: task.contract,\n sessionId: task.sessionId,\n dueAt: nextDueAt,\n })\n }\n return\n }\n\n // A drain whose outcome is unknown keeps the current durable message\n // and uses the transport retry path.\n throw new DrainUnsettled(\n `a2 scheduler: session '${task.sessionId}' of '${task.contract}' not settled yet (${result.outcome}) — rescheduled for redelivery`,\n )\n })(req)\n }\n },\n }\n}\n"],"mappings":"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"}
@@ -1,50 +1,13 @@
1
1
  import { a as NonRetriableError, i as A2Error, n as isSessionCleared, o as asStoreUnavailable, r as sessionClearedError } from "./session-clear-DGI4jTQ5.js";
2
2
  import { n as foldEvent, s as validateSync, t as cloneInitial } from "./reducer-BAXc8JyF.js";
3
3
  import { n as encodeState, t as decodeState } from "./reducer-runtime-DQwz_J59.js";
4
- import { _ as serverInternals, c as STATE_CACHE, g as nullProtoRecord, h as markSchedulerSendFailure, i as POLL_TIMINGS, l as STREAM_TIMINGS, o as RESERVED_PARTICIPANT_IDS, s as SOCKET_TIMINGS, t as DRAIN_TIMINGS, v as stateTailReaders } from "./internal-CbVvEwad.js";
5
- import { i as streamActivities, r as createStreamActivity, t as defaultSleep } from "./store-polling-4_y8lETm.js";
4
+ import { _ as nullProtoRecord, c as STATE_CACHE, g as markSchedulerSendFailure, i as POLL_TIMINGS, l as STREAM_TIMINGS, m as failedPreviousAttempt, o as RESERVED_PARTICIPANT_IDS, s as SOCKET_TIMINGS, t as DRAIN_TIMINGS, v as serverInternals, y as stateTailReaders } from "./internal-CscSY7K_.js";
5
+ import { t as boundedCache } from "./bounded-cache-CPHWF-8h.js";
6
+ import { i as streamActivities, r as createStreamActivity, t as defaultSleep } from "./store-polling-7WJV0YWP.js";
6
7
  import { i as takeBuffered, n as isSerializedEvent, r as parseSerializedEvent, t as bufferedEvents } from "./stream-buffer-Cx7AAPGq.js";
7
8
  import { r as platformWaitUntil, t as invocationDeadlineMs } from "./platform-B4TnJtWu.js";
8
- import { A as stateSnapshotToWire, C as serializedEventToWireJson, D as socketFrameFor, E as socketEventsFor, O as socketSubscribedFor, S as presenceSnapshotToWire, T as socketErrorAckFor, b as presencePatchToWire, d as heldEventBatch, i as compactEventJson, j as wireVersion, k as socketUnsubscribedFor, m as isStreamEvent, n as SOCKET_PING_FRAME, o as errorStatus, r as asA2Error, s as errorToWire, t as EVENT_WRITE_CHARS, u as eventToWire, w as socketAckFor } from "./wire-DjfHOvdW.js";
9
+ import { A as stateSnapshotToWire, C as serializedEventToWireJson, D as socketFrameFor, E as socketEventsFor, O as socketSubscribedFor, S as presenceSnapshotToWire, T as socketErrorAckFor, b as presencePatchToWire, d as heldEventBatch, i as compactEventJson, j as wireVersion, k as socketUnsubscribedFor, m as isStreamEvent, n as SOCKET_PING_FRAME, o as errorStatus, r as asA2Error, s as errorToWire, t as EVENT_WRITE_CHARS, u as eventToWire, w as socketAckFor } from "./wire-BKxH1vbP.js";
9
10
  import { t as retryableLazy } from "./retryable-lazy-DZWmHpii.js";
10
- //#region src/bounded-cache.ts
11
- function boundedCache({ maxEntries, maxBytes }) {
12
- const entries = /* @__PURE__ */ new Map();
13
- let total = 0;
14
- const remove = (key) => {
15
- const entry = entries.get(key);
16
- if (!entry) return;
17
- entries.delete(key);
18
- total -= entry.bytes;
19
- };
20
- return {
21
- get(key) {
22
- const entry = entries.get(key);
23
- if (!entry) return void 0;
24
- entries.delete(key);
25
- entries.set(key, entry);
26
- return entry.value;
27
- },
28
- set(key, value, bytes) {
29
- remove(key);
30
- if (bytes > maxBytes / 4) return;
31
- entries.set(key, {
32
- value,
33
- bytes
34
- });
35
- total += bytes;
36
- for (const oldest of entries.keys()) {
37
- if (entries.size <= maxEntries && total <= maxBytes) break;
38
- remove(oldest);
39
- }
40
- },
41
- delete: remove,
42
- deleteWhere(match) {
43
- for (const key of entries.keys()) if (match(key)) remove(key);
44
- }
45
- };
46
- }
47
- //#endregion
48
11
  //#region src/push-envelope.ts
49
12
  /**
50
13
  * The push envelope's trust boundary for `{ sessionId, events, presence? }`,
@@ -1142,6 +1105,8 @@ function handler(entry) {
1142
1105
  return typeof entry === "function" ? { handler: entry } : entry;
1143
1106
  }
1144
1107
  const MAX_FAILURES = 10;
1108
+ /** How long an event waits after its `failureCount`th failed attempt. */
1109
+ const failureRetryDelayMs = (failureCount) => Math.min(DRAIN_TIMINGS.failureRetryBaseMs * 2 ** Math.max(failureCount - 1, 0), DRAIN_TIMINGS.failureRetryMaxMs);
1145
1110
  /** Abort a lost attempt's handler and release its drain slot. */
1146
1111
  function endSuperseded(entry, message) {
1147
1112
  entry.supersede.abort(new A2Error("SUPERSEDED_ATTEMPT", message));
@@ -1202,9 +1167,11 @@ function pruneAbortHub(hub) {
1202
1167
  for (const [index, event] of hub.seen) if (index <= minimum || !types.has(event.type)) hub.seen.delete(index);
1203
1168
  }
1204
1169
  /**
1205
- * Deadlines tighten the claim/watchdog window; they never decide whether a
1206
- * handler starts. A platform timeout therefore follows the same path as any
1207
- * other process death: the in-flight event remains pending and is retried.
1170
+ * Deadlines tighten the claim/watchdog window, and a passed deadline ends
1171
+ * claiming: a claim then could not run, only lapse and be claimed again until
1172
+ * the platform ends the process. A platform timeout otherwise follows the
1173
+ * same path as any other process death: the in-flight event remains pending
1174
+ * and a fresh invocation retries it.
1208
1175
  */
1209
1176
  function nextClaimWindow() {
1210
1177
  const nowMs = Date.now();
@@ -1217,7 +1184,8 @@ function nextClaimWindow() {
1217
1184
  ttlMs,
1218
1185
  expiresAtMs,
1219
1186
  watchdogAtMs: expiresAtMs + DRAIN_TIMINGS.watchdogGraceMs,
1220
- deadlineCapped
1187
+ deadlineCapped,
1188
+ pastDeadline: deadlineMs !== null && deadlineTtlMs <= 0
1221
1189
  };
1222
1190
  }
1223
1191
  function schedulerSlot(dueAt) {
@@ -1235,6 +1203,19 @@ function currentSchedulerSlot(now) {
1235
1203
  */
1236
1204
  const NS = "";
1237
1205
  const devDefaultStore = retryableLazy(() => import("./store-sqlite.js").then((m) => m.sqlite()));
1206
+ /** Waits for a scheduler arm for at most `schedulerArmTimeoutMs`. */
1207
+ async function joinSchedulerArm(arm) {
1208
+ let timeout = null;
1209
+ try {
1210
+ await Promise.race([arm, new Promise((_, reject) => {
1211
+ timeout = setTimeout(() => {
1212
+ reject(/* @__PURE__ */ new Error(`a2 scheduler arm timed out after ${DRAIN_TIMINGS.schedulerArmTimeoutMs}ms`));
1213
+ }, DRAIN_TIMINGS.schedulerArmTimeoutMs);
1214
+ })]);
1215
+ } finally {
1216
+ if (timeout) clearTimeout(timeout);
1217
+ }
1218
+ }
1238
1219
  function environment() {
1239
1220
  const env = typeof process === "undefined" ? void 0 : process.env?.["NODE_ENV"];
1240
1221
  if (env === "test") return "test";
@@ -1483,21 +1464,12 @@ function createServer(options) {
1483
1464
  const dispatchArm = shouldHealSession && mode === "dispatch" ? startSchedulerArm(sessionId, currentSchedulerSlot(Date.now())) : null;
1484
1465
  if (shouldHealSession && mode === "inline") onAppended(appended, initialWatchdogAt);
1485
1466
  if (dispatchArm) await dispatchArm;
1486
- if (initialArm) {
1487
- let timeout = null;
1488
- try {
1489
- await Promise.race([initialArm, new Promise((_, reject) => {
1490
- timeout = setTimeout(() => {
1491
- reject(/* @__PURE__ */ new Error(`a2 scheduler arm timed out after ${DRAIN_TIMINGS.schedulerArmTimeoutMs}ms`));
1492
- }, DRAIN_TIMINGS.schedulerArmTimeoutMs);
1493
- })]);
1494
- } catch (err) {
1495
- span.setAttribute("a2.append.armed", false);
1496
- span.recordError(err);
1497
- if (requireDurableInitialArm) throw err;
1498
- } finally {
1499
- if (timeout) clearTimeout(timeout);
1500
- }
1467
+ if (initialArm) try {
1468
+ await joinSchedulerArm(initialArm);
1469
+ } catch (err) {
1470
+ span.setAttribute("a2.append.armed", false);
1471
+ span.recordError(err);
1472
+ if (requireDurableInitialArm) throw err;
1501
1473
  }
1502
1474
  return appended;
1503
1475
  });
@@ -2342,6 +2314,64 @@ function createServer(options) {
2342
2314
  index: result.index
2343
2315
  };
2344
2316
  };
2317
+ const redrivingStore = async (method) => {
2318
+ const store = await resolveStore();
2319
+ if (!store.deadLetters || !store.redrive) throw new TypeError(`session.${method} requires a store that supports redriving dead letters`);
2320
+ return store;
2321
+ };
2322
+ const listDeadLetters = async (sessionId) => {
2323
+ const store = await redrivingStore("deadLetters");
2324
+ let rows;
2325
+ try {
2326
+ rows = await store.deadLetters(nsId(sessionId));
2327
+ } catch (err) {
2328
+ throw asStoreUnavailable(err);
2329
+ }
2330
+ return rows.flatMap((row) => row.failedAt === null ? [] : [{
2331
+ ...toPublic(row),
2332
+ lane: row.lane,
2333
+ attemptCount: row.attemptCount,
2334
+ failureCount: row.failureCount,
2335
+ lastError: row.lastError,
2336
+ failedAt: row.failedAt
2337
+ }]);
2338
+ };
2339
+ const redriveEvent = async (sessionId, index, redriveOptions) => {
2340
+ assertRedriveIndex(index);
2341
+ const action = redriveOptions?.action;
2342
+ if (action !== "retry" && action !== "skip") throw new TypeError("redrive requires action 'retry' or 'skip'");
2343
+ return telemetry.span("a2.redrive", {
2344
+ "a2.contract": name,
2345
+ "a2.session_id": sessionId,
2346
+ "a2.redrive.index": index,
2347
+ "a2.redrive.action": action
2348
+ }, async (span) => {
2349
+ const store = await redrivingStore("redrive");
2350
+ let result;
2351
+ try {
2352
+ result = await store.redrive({
2353
+ sessionId: nsId(sessionId),
2354
+ index,
2355
+ action
2356
+ });
2357
+ } catch (err) {
2358
+ throw asStoreUnavailable(err);
2359
+ }
2360
+ span.setAttribute("a2.redrive.redriven", result.redriven);
2361
+ if (result.hasPending) {
2362
+ const watchdogAt = scheduler ? schedulerSlot(nextClaimWindow().watchdogAtMs) : void 0;
2363
+ const arm = watchdogAt === void 0 ? null : startSchedulerArm(sessionId, watchdogAt);
2364
+ scheduleDrain(sessionId, watchdogAt);
2365
+ if (arm) try {
2366
+ await joinSchedulerArm(arm);
2367
+ } catch (err) {
2368
+ span.setAttribute("a2.redrive.armed", false);
2369
+ span.recordError(err);
2370
+ }
2371
+ }
2372
+ return { redriven: result.redriven };
2373
+ });
2374
+ };
2345
2375
  const makeSession = (id, append, trigger, attempt) => ({
2346
2376
  id,
2347
2377
  append,
@@ -2389,7 +2419,7 @@ function createServer(options) {
2389
2419
  attempt
2390
2420
  }, generatedIds);
2391
2421
  };
2392
- return {
2422
+ const ctx = {
2393
2423
  event: toPublic(trigger),
2394
2424
  attempt,
2395
2425
  session: makeSession(publicSessionId, append, trigger, {
@@ -2402,6 +2432,8 @@ function createServer(options) {
2402
2432
  }),
2403
2433
  signal
2404
2434
  };
2435
+ if (attempt > 1 && trigger.lastFailedAttempt === attempt - 1) failedPreviousAttempt.add(ctx);
2436
+ return ctx;
2405
2437
  };
2406
2438
  const abortHubs = /* @__PURE__ */ new Map();
2407
2439
  const createAbortHub = (store, ns, startAfter) => {
@@ -2612,6 +2644,19 @@ function createServer(options) {
2612
2644
  renewal = tracked;
2613
2645
  return tracked;
2614
2646
  };
2647
+ /** Records an event the store dead-lettered on takeover; it never runs. */
2648
+ const reportExhausted = (event) => telemetry.span("a2.event", {
2649
+ "a2.contract": name,
2650
+ "a2.session_id": sessionId,
2651
+ "a2.event.type": event.type,
2652
+ "a2.event.index": event.index,
2653
+ "a2.event.id": event.id,
2654
+ "a2.event.attempt": event.attemptCount,
2655
+ "a2.event.handled": handlers.has(event.type),
2656
+ ...event.lane === null ? {} : { "a2.event.lane": event.lane }
2657
+ }, async (eventSpan) => {
2658
+ eventSpan.setAttribute("a2.event.outcome", "dead_lettered");
2659
+ });
2615
2660
  const runEvent = async (event, supersedeSignal, endAttempt) => {
2616
2661
  const registration = handlers.get(event.type);
2617
2662
  return telemetry.span("a2.event", {
@@ -2661,7 +2706,8 @@ function createServer(options) {
2661
2706
  index: event.index,
2662
2707
  attempt: event.attemptCount,
2663
2708
  error: describeError(err),
2664
- maxFailures: err instanceof NonRetriableError ? 1 : MAX_FAILURES
2709
+ maxFailures: err instanceof NonRetriableError ? 1 : MAX_FAILURES,
2710
+ retryAfterMs: failureRetryDelayMs(event.failureCount + 1)
2665
2711
  });
2666
2712
  commitSpan.setAttribute("a2.commit.outcome", result.outcome);
2667
2713
  return result;
@@ -2753,6 +2799,10 @@ function createServer(options) {
2753
2799
  let claim;
2754
2800
  if (shouldClaim) {
2755
2801
  const window = nextClaimWindow();
2802
+ if (window.pastDeadline) {
2803
+ outcome = "busy";
2804
+ break;
2805
+ }
2756
2806
  claim = await telemetry.span("a2.claim", {
2757
2807
  "a2.contract": name,
2758
2808
  "a2.session_id": sessionId
@@ -2761,6 +2811,7 @@ function createServer(options) {
2761
2811
  sessionId: ns,
2762
2812
  holder,
2763
2813
  ttlMs: window.ttlMs,
2814
+ maxFailures: MAX_FAILURES,
2764
2815
  ...window.deadlineCapped ? { expiresAtMs: window.expiresAtMs } : {},
2765
2816
  ...excluded.size === 0 ? {} : { excludeIndexes: [...excluded] }
2766
2817
  });
@@ -2769,9 +2820,11 @@ function createServer(options) {
2769
2820
  return result;
2770
2821
  });
2771
2822
  if (claim.outcome === "claimed") {
2772
- requestWatchdog(window.watchdogAtMs);
2823
+ const runnable = claim.events.filter((event) => event.failedAt === null);
2824
+ for (const event of claim.events) if (event.failedAt !== null) await reportExhausted(event);
2825
+ if (runnable.length > 0) requestWatchdog(window.watchdogAtMs);
2773
2826
  if (window.deadlineCapped) stopRenewing = true;
2774
- for (const event of claim.events) start(event, window.expiresAtMs);
2827
+ for (const event of runnable) start(event, window.expiresAtMs);
2775
2828
  shouldClaim = false;
2776
2829
  }
2777
2830
  }
@@ -2833,7 +2886,10 @@ function createServer(options) {
2833
2886
  const appendInline = async (...events) => appendExternal(id, "inline", events);
2834
2887
  const appendDispatch = async (...events) => appendExternal(id, "dispatch", events);
2835
2888
  const append = Object.assign(appendInline, { dispatch: appendDispatch });
2836
- return makeSession(id, append, null);
2889
+ return Object.assign(makeSession(id, append, null), {
2890
+ deadLetters: () => listDeadLetters(id),
2891
+ redrive: (index, redriveOptions) => redriveEvent(id, index, redriveOptions)
2892
+ });
2837
2893
  };
2838
2894
  const historyPage = async (sessionId, bounds) => {
2839
2895
  assertSessionId(sessionId);
@@ -2898,6 +2954,9 @@ function assertStreamIndex(value) {
2898
2954
  function assertStateIndex(value) {
2899
2955
  if (!Number.isSafeInteger(value) || value < 0) throw new TypeError("state.through must be a non-negative safe integer");
2900
2956
  }
2957
+ function assertRedriveIndex(value) {
2958
+ if (!Number.isSafeInteger(value) || value < 1) throw new TypeError("redrive requires a positive safe-integer index");
2959
+ }
2901
2960
  function assertClearIndex(value) {
2902
2961
  if (!Number.isSafeInteger(value) || value < 0) throw new TypeError("ifLastIndex must be a non-negative safe integer");
2903
2962
  }
@@ -2988,6 +3047,6 @@ const measuredBytes = (reducer, state, index, snapshots) => JSON.stringify(snaps
2988
3047
  }))?.length ?? 0;
2989
3048
  const stateCacheKey = (sessionId, reducerName) => `${sessionId}\u0000${reducerName}`;
2990
3049
  //#endregion
2991
- export { CONSOLE_TELEMETRY as a, parsePresenceSibling as c, handler as i, boundedCache as l, deliverSchedulerAppend as n, setServerFetchHooks as o, deliverSchedulerClear as r, sseResponse as s, createServer as t };
3050
+ export { CONSOLE_TELEMETRY as a, parsePresenceSibling as c, handler as i, deliverSchedulerAppend as n, setServerFetchHooks as o, deliverSchedulerClear as r, sseResponse as s, createServer as t };
2992
3051
 
2993
- //# sourceMappingURL=server-Rj64fNvX.js.map
3052
+ //# sourceMappingURL=server-D-Y4pqwu.js.map