experimental-a2 0.16.1 → 0.18.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.
- package/CHANGELOG.md +373 -0
- package/README.md +1 -1
- package/dist/{actor-ohPC-81x.d.ts → actor-Bjce9_5U.d.ts} +21 -11
- package/dist/actor-Bjce9_5U.d.ts.map +1 -0
- package/dist/actor-client.d.ts +2 -5
- package/dist/actor-client.d.ts.map +1 -1
- package/dist/actor-client.js +13 -4
- package/dist/actor-client.js.map +1 -1
- package/dist/actor-react.d.ts +5 -3
- package/dist/actor-react.d.ts.map +1 -1
- package/dist/actor-react.js +20 -10
- package/dist/actor-react.js.map +1 -1
- package/dist/{actor-shared-B5tJfzt-.js → actor-shared-t7w8xIKP.js} +12 -7
- package/dist/actor-shared-t7w8xIKP.js.map +1 -0
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +42 -23
- package/dist/actor.js.map +1 -1
- package/dist/{ai-DgOBltJ_.js → ai-BpOGm-zB.js} +621 -216
- package/dist/ai-BpOGm-zB.js.map +1 -0
- package/dist/{ai-CrEf6p_W.d.ts → ai-DrUaMyRt.d.ts} +114 -100
- package/dist/ai-DrUaMyRt.d.ts.map +1 -0
- package/dist/ai-client.d.ts +27 -0
- package/dist/ai-client.d.ts.map +1 -0
- package/dist/ai-client.js +29 -0
- package/dist/ai-client.js.map +1 -0
- package/dist/ai-fx.browser.d.ts +1 -0
- package/dist/ai-fx.browser.js +6 -0
- package/dist/ai-fx.browser.js.map +1 -0
- package/dist/ai-fx.d.ts +10 -0
- package/dist/ai-fx.d.ts.map +1 -0
- package/dist/ai-fx.js +634 -0
- package/dist/ai-fx.js.map +1 -0
- package/dist/ai-harness-BuLh_EYi.js +273 -0
- package/dist/ai-harness-BuLh_EYi.js.map +1 -0
- package/dist/ai-harness-DvsPeJeB.d.ts +26 -0
- package/dist/ai-harness-DvsPeJeB.d.ts.map +1 -0
- package/dist/ai-pi.browser.d.ts +1 -0
- package/dist/ai-pi.browser.js +6 -0
- package/dist/ai-pi.browser.js.map +1 -0
- package/dist/ai-pi.d.ts +17 -0
- package/dist/ai-pi.d.ts.map +1 -0
- package/dist/ai-pi.js +904 -0
- package/dist/ai-pi.js.map +1 -0
- package/dist/ai-react.d.ts +96 -0
- package/dist/ai-react.d.ts.map +1 -0
- package/dist/ai-react.js +142 -0
- package/dist/ai-react.js.map +1 -0
- package/dist/ai-server.browser.js +2 -2
- package/dist/ai-server.browser.js.map +1 -1
- package/dist/ai-server.d.ts +109 -43
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +526 -283
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +2 -2
- package/dist/ai.js +2 -2
- package/dist/bounded-cache-CPHWF-8h.js +41 -0
- package/dist/bounded-cache-CPHWF-8h.js.map +1 -0
- package/dist/{client-BYzHjkwU.js → client-BReKqwZC.js} +447 -80
- package/dist/client-BReKqwZC.js.map +1 -0
- package/dist/{client-CzyacQpJ.d.ts → client-h_DKFAjj.d.ts} +14 -13
- package/dist/client-h_DKFAjj.d.ts.map +1 -0
- package/dist/client.d.ts +4 -2
- package/dist/client.js +1 -1
- package/dist/{contract-CKRg_E4q.js → contract-BYKASlCV.js} +12 -21
- package/dist/contract-BYKASlCV.js.map +1 -0
- package/dist/{idempotent-replay-DVOlyYbx.js → idempotent-replay-JdgMQRRm.js} +2 -2
- package/dist/{idempotent-replay-DVOlyYbx.js.map → idempotent-replay-JdgMQRRm.js.map} +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/{errors-DvhSXnxk.d.ts → initial-snapshot-Bx-c0pDP.d.ts} +7 -3
- package/dist/initial-snapshot-Bx-c0pDP.d.ts.map +1 -0
- package/dist/{internal-Dq2qYxou.js → internal-B2T3U4Rq.js} +29 -2
- package/dist/internal-B2T3U4Rq.js.map +1 -0
- package/dist/otel.d.ts +1 -1
- package/dist/react-BBXgHWFc.d.ts +86 -0
- package/dist/react-BBXgHWFc.d.ts.map +1 -0
- package/dist/react.d.ts +2 -122
- package/dist/react.js +107 -100
- package/dist/react.js.map +1 -1
- package/dist/reducer-BAXc8JyF.js +87 -0
- package/dist/reducer-BAXc8JyF.js.map +1 -0
- package/dist/{reducer-otzHjuJj.d.ts → reducer-CqA59Q6U.d.ts} +46 -30
- package/dist/reducer-CqA59Q6U.d.ts.map +1 -0
- package/dist/reducer-runtime-DQwz_J59.js +28 -0
- package/dist/reducer-runtime-DQwz_J59.js.map +1 -0
- package/dist/scheduler-qstash.d.ts +3 -3
- package/dist/scheduler-qstash.d.ts.map +1 -1
- package/dist/scheduler-qstash.js +33 -10
- package/dist/scheduler-qstash.js.map +1 -1
- package/dist/{scheduler-task-BpzhPnRS.js → scheduler-task-X1SToDjP.js} +18 -3
- package/dist/scheduler-task-X1SToDjP.js.map +1 -0
- package/dist/scheduler-vercel.d.ts +4 -9
- package/dist/scheduler-vercel.d.ts.map +1 -1
- package/dist/scheduler-vercel.js +25 -44
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/{server-BD5ckxZb.js → server-Dw_k_7kO.js} +904 -219
- package/dist/server-Dw_k_7kO.js.map +1 -0
- package/dist/{server-DbD7IJVK.d.ts → server-En7cbi3v.d.ts} +116 -26
- package/dist/server-En7cbi3v.d.ts.map +1 -0
- package/dist/server.d.ts +4 -4
- package/dist/server.js +2 -2
- package/dist/session-clear-Bo-XTFEy.d.ts +12 -0
- package/dist/session-clear-Bo-XTFEy.d.ts.map +1 -0
- package/dist/{errors-DCk6ch5n.js → session-clear-DGI4jTQ5.js} +17 -2
- package/dist/session-clear-DGI4jTQ5.js.map +1 -0
- package/dist/{store-DyZM6fS5.d.ts → store-Djdv0bnZ.d.ts} +73 -3
- package/dist/store-Djdv0bnZ.d.ts.map +1 -0
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.d.ts.map +1 -1
- package/dist/store-memory.js +205 -66
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-CmxUbV93.js → store-polling-B5FcwJfo.js} +21 -4
- package/dist/store-polling-B5FcwJfo.js.map +1 -0
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.d.ts.map +1 -1
- package/dist/store-postgres.js +450 -108
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-presence-polling-C7-XZyW9.js → store-presence-polling-BAwS8RWn.js} +3 -3
- package/dist/{store-presence-polling-C7-XZyW9.js.map → store-presence-polling-BAwS8RWn.js.map} +1 -1
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.d.ts.map +1 -1
- package/dist/store-redis-http.js +293 -11
- package/dist/store-redis-http.js.map +1 -1
- package/dist/store-redis-notify-BRITti_o.js +2493 -0
- package/dist/store-redis-notify-BRITti_o.js.map +1 -0
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.d.ts.map +1 -1
- package/dist/store-redis.js +11 -11
- package/dist/store-redis.js.map +1 -1
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.d.ts.map +1 -1
- package/dist/store-sqlite.js +197 -33
- package/dist/store-sqlite.js.map +1 -1
- package/dist/stream-buffer-Cx7AAPGq.js +23 -0
- package/dist/stream-buffer-Cx7AAPGq.js.map +1 -0
- package/dist/{telemetry-B5jzpy6y.d.ts → telemetry-Cyfqo6Qt.d.ts} +2 -2
- package/dist/telemetry-Cyfqo6Qt.d.ts.map +1 -0
- package/dist/{wire-BO5wWCb1.js → wire-rSUglYo8.js} +132 -10
- package/dist/wire-rSUglYo8.js.map +1 -0
- package/docs/01-quickstart.mdx +50 -84
- package/docs/{guides/07-examples.mdx → 02-examples.mdx} +10 -20
- package/docs/actors/01-introduction.mdx +48 -82
- package/docs/actors/02-concurrency.mdx +43 -70
- package/docs/actors/03-timers.mdx +28 -42
- package/docs/actors/04-routes.mdx +110 -137
- package/docs/actors/meta.ts +1 -1
- package/docs/agents/01-introduction.mdx +257 -0
- package/docs/agents/02-tools.mdx +247 -0
- package/docs/agents/03-conversations.mdx +538 -0
- package/docs/agents/04-harnesses.mdx +136 -0
- package/docs/agents/meta.ts +1 -0
- package/docs/client/01-react.mdx +294 -0
- package/docs/client/02-transports.mdx +86 -0
- package/docs/client/03-presence.mdx +224 -0
- package/docs/client/meta.ts +1 -0
- package/docs/contract/01-events.mdx +57 -0
- package/docs/contract/02-state.mdx +142 -0
- package/docs/contract/03-sessions.mdx +175 -0
- package/docs/index.mdx +112 -156
- package/docs/reference/01-core.mdx +139 -0
- package/docs/reference/02-server.mdx +855 -0
- package/docs/reference/03-client.mdx +301 -0
- package/docs/reference/04-ai.mdx +722 -0
- package/docs/reference/05-adapters.mdx +351 -0
- package/docs/reference/06-errors.mdx +138 -0
- package/docs/reference/meta.ts +1 -1
- package/docs/server/01-handlers.mdx +200 -0
- package/docs/server/02-timers.mdx +130 -0
- package/docs/server/03-cancellation.mdx +141 -0
- package/docs/server/04-routes.mdx +243 -0
- package/docs/server/05-production.mdx +322 -0
- package/docs/server/06-guarantees.mdx +123 -0
- package/examples/playground/DEPLOY.md +53 -7
- package/examples/playground/app/a0/[appId]/loading.tsx +16 -0
- package/examples/playground/app/a0/[appId]/page.tsx +53 -0
- package/examples/playground/app/a0/a0.css +818 -0
- package/examples/playground/app/a0/app-list.tsx +218 -0
- package/examples/playground/app/a0/app-view.tsx +63 -0
- package/examples/playground/app/a0/apps/[appId]/preview/route.ts +27 -0
- package/examples/playground/app/a0/apps/[appId]/route.ts +28 -0
- package/examples/playground/app/a0/apps.test.ts +128 -0
- package/examples/playground/app/a0/apps.ts +102 -0
- package/examples/playground/app/a0/chat.tsx +410 -0
- package/examples/playground/app/a0/columns.tsx +109 -0
- package/examples/playground/app/a0/context-menu.tsx +116 -0
- package/examples/playground/app/a0/cookies.ts +46 -0
- package/examples/playground/app/a0/crash-on-request.ts +72 -0
- package/examples/playground/app/a0/crashes/route.ts +3 -0
- package/examples/playground/app/a0/crashes.test.ts +33 -0
- package/examples/playground/app/a0/crashes.ts +28 -0
- package/examples/playground/app/a0/devtools.tsx +164 -0
- package/examples/playground/app/a0/events/route.ts +6 -0
- package/examples/playground/app/a0/flags.ts +30 -0
- package/examples/playground/app/a0/fx/events/route.ts +9 -0
- package/examples/playground/app/a0/fx/scheduler/route.ts +7 -0
- package/examples/playground/app/a0/fx/short/events/route.ts +11 -0
- package/examples/playground/app/a0/layout.tsx +38 -0
- package/examples/playground/app/a0/list/route.ts +10 -0
- package/examples/playground/app/a0/model.test.ts +73 -0
- package/examples/playground/app/a0/model.ts +95 -0
- package/examples/playground/app/a0/no-photon.cjs +4 -0
- package/examples/playground/app/a0/no-quickjs.cjs +3 -0
- package/examples/playground/app/a0/page.tsx +14 -0
- package/examples/playground/app/a0/pi/events/route.ts +9 -0
- package/examples/playground/app/a0/pi/scheduler/route.ts +7 -0
- package/examples/playground/app/a0/pi/short/events/route.ts +11 -0
- package/examples/playground/app/a0/preview-pane.tsx +145 -0
- package/examples/playground/app/a0/preview.test.ts +53 -0
- package/examples/playground/app/a0/preview.ts +26 -0
- package/examples/playground/app/a0/sandbox-log.ts +75 -0
- package/examples/playground/app/a0/sandbox-session.ts +152 -0
- package/examples/playground/app/a0/sandbox.ts +191 -0
- package/examples/playground/app/a0/sandboxes/scheduler/route.ts +6 -0
- package/examples/playground/app/a0/scheduler/route.ts +7 -0
- package/examples/playground/app/a0/server.ts +213 -0
- package/examples/playground/app/a0/session.ts +59 -0
- package/examples/playground/app/a0/shell.tsx +158 -0
- package/examples/playground/app/a0/short/events/route.ts +9 -0
- package/examples/playground/app/a0/splitter.tsx +120 -0
- package/examples/playground/app/agent/[agentId]/agent-client.tsx +144 -174
- package/examples/playground/app/agent/[agentId]/command-receipt.test.tsx +65 -0
- package/examples/playground/app/agent/[agentId]/command-receipt.tsx +27 -0
- package/examples/playground/app/agent/[agentId]/compaction-event.tsx +25 -24
- package/examples/playground/app/agent/[agentId]/compaction-panel.test.tsx +119 -0
- package/examples/playground/app/agent/[agentId]/compaction-panel.tsx +208 -165
- package/examples/playground/app/agent/[agentId]/page.tsx +2 -16
- package/examples/playground/app/agent/events/route.ts +5 -2
- package/examples/playground/app/agent/ingress.test.ts +101 -0
- package/examples/playground/app/agent/model-input.test.tsx +58 -0
- package/examples/playground/app/agent/model-input.tsx +115 -0
- package/examples/playground/app/agent/model.test.ts +73 -0
- package/examples/playground/app/agent/model.ts +58 -26
- package/examples/playground/app/agent/new-agent-session.tsx +37 -19
- package/examples/playground/app/agent/page.tsx +4 -4
- package/examples/playground/app/agent/server.ts +19 -15
- package/examples/playground/app/agent/session.ts +5 -6
- package/examples/playground/app/agent/telemetry.test.ts +189 -0
- package/examples/playground/app/agent/telemetry.ts +30 -0
- package/examples/playground/app/agent/trace.test.ts +23 -0
- package/examples/playground/app/agent/trace.ts +18 -0
- package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +12 -12
- package/examples/playground/app/canvas/[canvasId]/page.tsx +1 -2
- package/examples/playground/app/canvas/[canvasId]/session.ts +2 -2
- package/examples/playground/app/canvas/model.ts +53 -53
- package/examples/playground/app/chat/[chatId]/agent-state.test.ts +176 -0
- package/examples/playground/app/chat/[chatId]/agent-stream-drawer.tsx +16 -1
- package/examples/playground/app/chat/[chatId]/chat-client.tsx +55 -77
- package/examples/playground/app/chat/[chatId]/chat-view.ts +37 -3
- package/examples/playground/app/chat/[chatId]/page.tsx +7 -15
- package/examples/playground/app/chat/[chatId]/session.ts +2 -19
- package/examples/playground/app/chat/agent/route.ts +9 -0
- package/examples/playground/app/chat/model.test.ts +46 -119
- package/examples/playground/app/chat/model.ts +82 -128
- package/examples/playground/app/chat/scheduler/route.ts +2 -2
- package/examples/playground/app/chat/server.test.ts +112 -0
- package/examples/playground/app/chat/server.ts +113 -117
- package/examples/playground/app/components/activity-feed.tsx +30 -1
- package/examples/playground/app/components/activity-history.test.tsx +35 -0
- package/examples/playground/app/components/demo-icons.tsx +89 -0
- package/examples/playground/app/components/demos.tsx +33 -0
- package/examples/playground/app/components/fast-link.test.tsx +58 -0
- package/examples/playground/app/components/fast-link.tsx +55 -0
- package/examples/playground/app/components/site-header.tsx +96 -0
- package/examples/playground/app/counter/counter-client.tsx +3 -3
- package/examples/playground/app/counter/model.test.ts +3 -3
- package/examples/playground/app/counter/model.ts +11 -21
- package/examples/playground/app/counter/page.tsx +2 -15
- package/examples/playground/app/counter/session.ts +2 -2
- package/examples/playground/app/documents/[documentId]/code-editor.tsx +12 -1
- package/examples/playground/app/documents/[documentId]/document-client.tsx +10 -11
- package/examples/playground/app/documents/[documentId]/page.tsx +1 -2
- package/examples/playground/app/documents/[documentId]/session.ts +2 -2
- package/examples/playground/app/documents/model.ts +27 -27
- package/examples/playground/app/globals.css +420 -207
- package/examples/playground/app/initial-state.test.tsx +54 -0
- package/examples/playground/app/layout.tsx +7 -23
- package/examples/playground/app/orders/[orderId]/page.tsx +1 -2
- package/examples/playground/app/orders/[orderId]/session.ts +2 -2
- package/examples/playground/app/orders/model.ts +49 -49
- package/examples/playground/app/page.tsx +13 -102
- package/examples/playground/app/recovery/[recoveryId]/page.tsx +1 -2
- package/examples/playground/app/recovery/[recoveryId]/session.ts +2 -2
- package/examples/playground/app/recovery/model.ts +30 -30
- package/examples/playground/instrumentation.ts +5 -0
- package/examples/playground/next.config.ts +17 -5
- package/examples/playground/package.json +13 -2
- package/examples/playground/scripts/a0-template.ts +77 -0
- package/examples/playground/vercel.json +36 -0
- package/package.json +29 -3
- package/src/actor-client.ts +16 -4
- package/src/actor-react.ts +25 -11
- package/src/actor-shared.ts +21 -6
- package/src/actor.ts +76 -31
- package/src/ai-agent-actions.ts +219 -0
- package/src/ai-agent.ts +2517 -0
- package/src/ai-app-state.ts +91 -0
- package/src/ai-client-state.ts +6 -3
- package/src/ai-client.ts +62 -0
- package/src/ai-context-schema.ts +25 -5
- package/src/ai-context.ts +90 -40
- package/src/ai-control-server.ts +92 -72
- package/src/ai-coordinator.ts +23 -22
- package/src/ai-fx.browser.ts +5 -0
- package/src/ai-fx.ts +833 -0
- package/src/ai-harness.ts +518 -0
- package/src/ai-message-base.ts +209 -0
- package/src/ai-message-projection.ts +262 -64
- package/src/ai-model-metadata.ts +11 -6
- package/src/ai-pi.browser.ts +5 -0
- package/src/ai-pi.ts +1263 -0
- package/src/ai-progress-batches.ts +26 -0
- package/src/ai-projector.ts +95 -14
- package/src/ai-react.ts +146 -0
- package/src/ai-sdk-step.ts +2 -2
- package/src/ai-server.browser.ts +2 -2
- package/src/ai-server.ts +23 -1965
- package/src/ai-stored-state.ts +48 -163
- package/src/ai.ts +339 -155
- package/src/bootstrap-stream.ts +72 -0
- package/src/bounded-cache.ts +55 -0
- package/src/client-render-session.ts +49 -0
- package/src/client.ts +688 -151
- package/src/contract.ts +92 -42
- package/src/deterministic-id.ts +37 -0
- package/src/equal-json.ts +32 -0
- package/src/errors.ts +13 -1
- package/src/index.ts +5 -1
- package/src/initial-snapshot.ts +39 -0
- package/src/internal.ts +52 -0
- package/src/libfx.d.ts +34 -0
- package/src/postgres-notification-scope.ts +14 -3
- package/src/react.ts +171 -206
- package/src/redis-http-reply.ts +256 -0
- package/src/redis-http-stream.ts +73 -0
- package/src/redis-http-subscriptions.ts +13 -5
- package/src/reducer-runtime.ts +52 -0
- package/src/reducer.ts +93 -11
- package/src/scheduler-qstash.ts +49 -20
- package/src/scheduler-task.ts +34 -2
- package/src/scheduler-vercel.ts +32 -84
- package/src/server-fetch.ts +139 -35
- package/src/server.ts +1133 -255
- package/src/session-clear.ts +23 -0
- package/src/session-socket.ts +154 -49
- package/src/sse.ts +71 -4
- package/src/store-memory.ts +252 -75
- package/src/store-polling.ts +18 -4
- package/src/store-postgres.ts +541 -114
- package/src/store-redis-core.ts +1450 -493
- package/src/store-redis-http.ts +18 -2
- package/src/store-redis-notify.ts +262 -85
- package/src/store-redis.ts +19 -9
- package/src/store-sqlite.ts +319 -45
- package/src/store.ts +79 -1
- package/src/stream-buffer.ts +43 -0
- package/src/telemetry.ts +14 -1
- package/src/wire.ts +241 -13
- package/dist/actor-ohPC-81x.d.ts.map +0 -1
- package/dist/actor-shared-B5tJfzt-.js.map +0 -1
- package/dist/ai-CrEf6p_W.d.ts.map +0 -1
- package/dist/ai-DgOBltJ_.js.map +0 -1
- package/dist/client-BYzHjkwU.js.map +0 -1
- package/dist/client-CzyacQpJ.d.ts.map +0 -1
- package/dist/contract-CKRg_E4q.js.map +0 -1
- package/dist/errors-DCk6ch5n.js.map +0 -1
- package/dist/errors-DvhSXnxk.d.ts.map +0 -1
- package/dist/internal-Dq2qYxou.js.map +0 -1
- package/dist/react.d.ts.map +0 -1
- package/dist/reducer-DEMjEY_O.js +0 -29
- package/dist/reducer-DEMjEY_O.js.map +0 -1
- package/dist/reducer-otzHjuJj.d.ts.map +0 -1
- package/dist/scheduler-task-BpzhPnRS.js.map +0 -1
- package/dist/server-BD5ckxZb.js.map +0 -1
- package/dist/server-DbD7IJVK.d.ts.map +0 -1
- package/dist/store-DyZM6fS5.d.ts.map +0 -1
- package/dist/store-polling-CmxUbV93.js.map +0 -1
- package/dist/store-redis-notify-BVLUfI3j.js +0 -1583
- package/dist/store-redis-notify-BVLUfI3j.js.map +0 -1
- package/dist/telemetry-B5jzpy6y.d.ts.map +0 -1
- package/dist/validate-XKT4FSNn.js +0 -30
- package/dist/validate-XKT4FSNn.js.map +0 -1
- package/dist/wire-BO5wWCb1.js.map +0 -1
- package/docs/concepts/01-contracts.mdx +0 -142
- package/docs/concepts/02-handlers.mdx +0 -284
- package/docs/concepts/03-durability.mdx +0 -314
- package/docs/concepts/04-state.mdx +0 -209
- package/docs/guides/01-timers.mdx +0 -185
- package/docs/guides/02-cancellation.mdx +0 -161
- package/docs/guides/03-react.mdx +0 -384
- package/docs/guides/04-local-first.mdx +0 -88
- package/docs/guides/05-production.mdx +0 -521
- package/docs/guides/06-ai-agents.mdx +0 -1318
- package/docs/guides/08-application-data.mdx +0 -113
- package/docs/guides/09-presence.mdx +0 -266
- package/docs/guides/10-transports.mdx +0 -221
- package/docs/reference/01-api.mdx +0 -1896
- package/docs/reference/02-errors.mdx +0 -149
- package/examples/playground/app/agent/[agentId]/compaction/route.ts +0 -14
- package/examples/playground/app/agent/compaction-settings.test.ts +0 -144
- package/examples/playground/app/agent/compaction-settings.ts +0 -49
- package/examples/playground/app/agent/compaction-timeline.test.ts +0 -377
- package/examples/playground/app/agent/compaction-timeline.ts +0 -208
- /package/docs/{concepts → contract}/meta.ts +0 -0
- /package/docs/{guides → server}/meta.ts +0 -0
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { n as
|
|
2
|
-
import { n as
|
|
3
|
-
import {
|
|
4
|
-
import { i as
|
|
1
|
+
import { a as NonRetriableError, i as A2Error, n as isSessionCleared, o as asStoreUnavailable, r as sessionClearedError } from "./session-clear-DGI4jTQ5.js";
|
|
2
|
+
import { n as foldEvent, s as validateSync, t as cloneInitial } from "./reducer-BAXc8JyF.js";
|
|
3
|
+
import { n as encodeState, t as decodeState } from "./reducer-runtime-DQwz_J59.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-B2T3U4Rq.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-B5FcwJfo.js";
|
|
7
|
+
import { i as takeBuffered, n as isSerializedEvent, r as parseSerializedEvent, t as bufferedEvents } from "./stream-buffer-Cx7AAPGq.js";
|
|
5
8
|
import { r as platformWaitUntil, t as invocationDeadlineMs } from "./platform-B4TnJtWu.js";
|
|
6
|
-
import {
|
|
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-rSUglYo8.js";
|
|
7
10
|
import { t as retryableLazy } from "./retryable-lazy-DZWmHpii.js";
|
|
8
11
|
//#region src/push-envelope.ts
|
|
9
12
|
/**
|
|
@@ -191,25 +194,43 @@ function socketShell(socket, release, onFrame, deadlineAt) {
|
|
|
191
194
|
track
|
|
192
195
|
};
|
|
193
196
|
}
|
|
194
|
-
const socketSubscription = (
|
|
195
|
-
target,
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
})
|
|
197
|
+
const socketSubscription = (options) => {
|
|
198
|
+
const { target, startAfter, presence, attempt } = options;
|
|
199
|
+
return {
|
|
200
|
+
target,
|
|
201
|
+
iterator: (presence ? target.stream({
|
|
202
|
+
startAfter,
|
|
203
|
+
presence: true
|
|
204
|
+
}) : target.stream({ startAfter }))[Symbol.asyncIterator](),
|
|
205
|
+
stopped: false,
|
|
206
|
+
...attempt === void 0 ? {} : { attempt }
|
|
207
|
+
};
|
|
208
|
+
};
|
|
202
209
|
const stopSubscription = (subscription) => {
|
|
203
210
|
subscription.stopped = true;
|
|
204
211
|
subscription.iterator.return?.()?.catch(() => {});
|
|
205
212
|
};
|
|
206
|
-
|
|
213
|
+
/**
|
|
214
|
+
* Sends a subscription's items until it ends. With `batch`, an event and
|
|
215
|
+
* the events its iterator already holds go out as one frame.
|
|
216
|
+
*/
|
|
217
|
+
const pumpSubscription = async (options) => {
|
|
218
|
+
const { shell, subscription, frameFor, batch } = options;
|
|
207
219
|
for (;;) {
|
|
208
220
|
while (!shell.torn() && !subscription.stopped && shell.buffered() > SOCKET_TIMINGS.highWaterMarkBytes) await new Promise((wake) => setTimeout(wake, SOCKET_TIMINGS.resumePollMs));
|
|
209
221
|
if (shell.torn() || subscription.stopped) return;
|
|
210
222
|
const { value, done } = await subscription.iterator.next();
|
|
211
223
|
if (shell.torn() || subscription.stopped || done) return;
|
|
212
|
-
|
|
224
|
+
if (!batch || !isStreamEvent(value)) {
|
|
225
|
+
shell.send(frameFor(value));
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const { events } = heldEventBatch({
|
|
229
|
+
first: value,
|
|
230
|
+
iterator: subscription.iterator,
|
|
231
|
+
encode: compactEventJson
|
|
232
|
+
});
|
|
233
|
+
shell.send(batch(events));
|
|
213
234
|
}
|
|
214
235
|
};
|
|
215
236
|
const handleSocketPush = async (shell, target, sessionId, frame, req, ackSessionId, gatePush, activate) => {
|
|
@@ -255,7 +276,11 @@ const applySocketPresence = async (shell, target, sessionId, frame, gatePush) =>
|
|
|
255
276
|
};
|
|
256
277
|
function sessionSocket(sessionId, session, socket, options) {
|
|
257
278
|
const target = session;
|
|
258
|
-
const subscription = socketSubscription(
|
|
279
|
+
const subscription = socketSubscription({
|
|
280
|
+
target,
|
|
281
|
+
startAfter: options?.startAfter ?? 0,
|
|
282
|
+
presence: options?.presence === true
|
|
283
|
+
});
|
|
259
284
|
const handleFrame = (frame) => {
|
|
260
285
|
switch (frame["kind"]) {
|
|
261
286
|
case "push": {
|
|
@@ -286,20 +311,41 @@ function sessionSocket(sessionId, session, socket, options) {
|
|
|
286
311
|
}
|
|
287
312
|
};
|
|
288
313
|
const shell = socketShell(socket, () => stopSubscription(subscription), handleFrame, options?.deadline);
|
|
289
|
-
pumpSubscription(
|
|
314
|
+
pumpSubscription({
|
|
315
|
+
shell,
|
|
316
|
+
subscription,
|
|
317
|
+
frameFor: socketFrameFor,
|
|
318
|
+
...options?.wire === 2 ? { batch: (events) => socketEventsFor(events) } : {}
|
|
319
|
+
}).then(() => shell.shutdown(1e3), () => shell.shutdown(1011, "stream failed"));
|
|
290
320
|
}
|
|
291
321
|
const parseSubscribeEntries = (value) => {
|
|
292
322
|
if (!Array.isArray(value) || value.length === 0) return null;
|
|
293
323
|
const entries = [];
|
|
294
324
|
for (const entry of value) {
|
|
295
325
|
if (entry === null || typeof entry !== "object") return null;
|
|
296
|
-
const { id, index } = entry;
|
|
326
|
+
const { id, attempt, index, bootstrap, reducer } = entry;
|
|
297
327
|
if (typeof id !== "string" || id.length === 0) return null;
|
|
298
|
-
if (typeof
|
|
299
|
-
|
|
328
|
+
if (attempt !== void 0 && (typeof attempt !== "number" || !Number.isSafeInteger(attempt) || attempt < 0)) return null;
|
|
329
|
+
if (reducer !== void 0 && (typeof reducer !== "string" || reducer.length === 0)) return null;
|
|
330
|
+
const lane = {
|
|
300
331
|
id,
|
|
301
|
-
|
|
302
|
-
}
|
|
332
|
+
...attempt === void 0 ? {} : { attempt }
|
|
333
|
+
};
|
|
334
|
+
if (bootstrap === true) {
|
|
335
|
+
if (index !== void 0 || typeof reducer !== "string") return null;
|
|
336
|
+
entries.push({
|
|
337
|
+
...lane,
|
|
338
|
+
bootstrap: true,
|
|
339
|
+
reducer
|
|
340
|
+
});
|
|
341
|
+
} else {
|
|
342
|
+
if (bootstrap !== void 0 || typeof index !== "number" || !Number.isSafeInteger(index) || index < 0) return null;
|
|
343
|
+
entries.push({
|
|
344
|
+
...lane,
|
|
345
|
+
index,
|
|
346
|
+
...reducer === void 0 ? {} : { reducer }
|
|
347
|
+
});
|
|
348
|
+
}
|
|
303
349
|
}
|
|
304
350
|
return entries;
|
|
305
351
|
};
|
|
@@ -322,18 +368,34 @@ function sessionsSocket(resolve, socket, options) {
|
|
|
322
368
|
if (subscriptions.get(id) === subscription) subscriptions.delete(id);
|
|
323
369
|
};
|
|
324
370
|
const runSubscription = async (id, subscription) => {
|
|
371
|
+
const { attempt } = subscription;
|
|
325
372
|
try {
|
|
326
|
-
await pumpSubscription(
|
|
373
|
+
await pumpSubscription({
|
|
374
|
+
shell,
|
|
375
|
+
subscription,
|
|
376
|
+
frameFor: (item) => socketFrameFor(item, id),
|
|
377
|
+
...options?.wire === 2 ? { batch: (events) => socketEventsFor(events, id) } : {}
|
|
378
|
+
});
|
|
327
379
|
if (shell.torn() || subscription.stopped) return;
|
|
328
380
|
retire(id, subscription);
|
|
329
|
-
shell.send(socketUnsubscribedFor(
|
|
381
|
+
shell.send(socketUnsubscribedFor({
|
|
382
|
+
sessionId: id,
|
|
383
|
+
attempt
|
|
384
|
+
}));
|
|
330
385
|
} catch (error) {
|
|
331
386
|
if (shell.torn() || subscription.stopped) return;
|
|
332
387
|
retire(id, subscription);
|
|
333
|
-
shell.send(socketUnsubscribedFor(
|
|
388
|
+
shell.send(socketUnsubscribedFor({
|
|
389
|
+
sessionId: id,
|
|
390
|
+
attempt,
|
|
391
|
+
reason: "stream failed",
|
|
392
|
+
error: asA2Error(error)
|
|
393
|
+
}));
|
|
334
394
|
}
|
|
335
395
|
};
|
|
336
|
-
const startSubscription = async (
|
|
396
|
+
const startSubscription = async (entry) => {
|
|
397
|
+
const { id, attempt } = entry;
|
|
398
|
+
const index = entry.index ?? 0;
|
|
337
399
|
const existing = subscriptions.get(id);
|
|
338
400
|
if (existing) {
|
|
339
401
|
stopSubscription(existing);
|
|
@@ -341,19 +403,37 @@ function sessionsSocket(resolve, socket, options) {
|
|
|
341
403
|
}
|
|
342
404
|
let session;
|
|
343
405
|
try {
|
|
344
|
-
session = await resolve(id, index);
|
|
406
|
+
session = await resolve(id, index, entry);
|
|
345
407
|
} catch (error) {
|
|
346
|
-
if (!shell.torn()) shell.send(socketUnsubscribedFor(
|
|
408
|
+
if (!shell.torn()) shell.send(socketUnsubscribedFor({
|
|
409
|
+
sessionId: id,
|
|
410
|
+
attempt,
|
|
411
|
+
reason: "stream failed",
|
|
412
|
+
error: asA2Error(error)
|
|
413
|
+
}));
|
|
347
414
|
return;
|
|
348
415
|
}
|
|
349
416
|
if (shell.torn()) return;
|
|
350
417
|
if (session === null) {
|
|
351
|
-
shell.send(socketUnsubscribedFor(
|
|
418
|
+
shell.send(socketUnsubscribedFor({
|
|
419
|
+
sessionId: id,
|
|
420
|
+
attempt,
|
|
421
|
+
reason: "subscribe rejected",
|
|
422
|
+
error: new A2Error("FORBIDDEN", "the subscription is not authorized")
|
|
423
|
+
}));
|
|
352
424
|
return;
|
|
353
425
|
}
|
|
354
|
-
const subscription = socketSubscription(
|
|
426
|
+
const subscription = socketSubscription({
|
|
427
|
+
target: session,
|
|
428
|
+
startAfter: index,
|
|
429
|
+
presence,
|
|
430
|
+
attempt
|
|
431
|
+
});
|
|
355
432
|
subscriptions.set(id, subscription);
|
|
356
|
-
shell.send(socketSubscribedFor(
|
|
433
|
+
shell.send(socketSubscribedFor({
|
|
434
|
+
sessionId: id,
|
|
435
|
+
attempt
|
|
436
|
+
}));
|
|
357
437
|
runSubscription(id, subscription);
|
|
358
438
|
};
|
|
359
439
|
let controlTail = Promise.resolve();
|
|
@@ -384,7 +464,7 @@ function sessionsSocket(resolve, socket, options) {
|
|
|
384
464
|
shell.malformedFrame("malformed subscribe frame");
|
|
385
465
|
return;
|
|
386
466
|
}
|
|
387
|
-
for (const entry of entries) control(() => startSubscription(entry
|
|
467
|
+
for (const entry of entries) control(() => startSubscription(entry));
|
|
388
468
|
return;
|
|
389
469
|
}
|
|
390
470
|
case "unsubscribe": {
|
|
@@ -449,7 +529,10 @@ function sessionsSocket(resolve, socket, options) {
|
|
|
449
529
|
*/
|
|
450
530
|
/**
|
|
451
531
|
* Pipe a live event iterable into an SSE `Response`. Each event is one
|
|
452
|
-
* frame — `id:` carries the event-log index, `data:` the JSON event.
|
|
532
|
+
* frame — `id:` carries the event-log index, `data:` the JSON event.
|
|
533
|
+
* Frames for events the iterable already holds are written together. With
|
|
534
|
+
* `wire: 2` those events share one frame instead: `data:` is a JSON array of
|
|
535
|
+
* `CompactWireEvent`s and `id:` the last one's index. A
|
|
453
536
|
* `stream({ presence: true })` iterable also yields presence items,
|
|
454
537
|
* which ride as named frames old clients skip: `event:
|
|
455
538
|
* presence-snapshot` first (the map, ISO dates), then `event: presence`
|
|
@@ -463,7 +546,8 @@ function sessionsSocket(resolve, socket, options) {
|
|
|
463
546
|
* closes cleanly one second before it so the client reconnects without
|
|
464
547
|
* a platform-timeout failure.
|
|
465
548
|
*/
|
|
466
|
-
function sseResponse(iterable) {
|
|
549
|
+
function sseResponse(iterable, options = {}) {
|
|
550
|
+
const write = options.wire === 2 ? batchedFrames : eventFrames;
|
|
467
551
|
const iterator = iterable[Symbol.asyncIterator]();
|
|
468
552
|
const encoder = new TextEncoder();
|
|
469
553
|
let heartbeat;
|
|
@@ -529,7 +613,7 @@ function sseResponse(iterable) {
|
|
|
529
613
|
controller.close();
|
|
530
614
|
return;
|
|
531
615
|
}
|
|
532
|
-
controller.enqueue(encoder.encode(
|
|
616
|
+
controller.enqueue(encoder.encode(write(value, iterator)));
|
|
533
617
|
} catch (error) {
|
|
534
618
|
if (closed) return;
|
|
535
619
|
closed = true;
|
|
@@ -554,12 +638,114 @@ function sseResponse(iterable) {
|
|
|
554
638
|
}
|
|
555
639
|
});
|
|
556
640
|
}
|
|
641
|
+
/** Version 1: one frame per event, then each event the iterator holds. */
|
|
642
|
+
function eventFrames(first, iterator) {
|
|
643
|
+
let frames = frameFor(first);
|
|
644
|
+
while (frames.length < EVENT_WRITE_CHARS) {
|
|
645
|
+
const event = takeBuffered(iterator);
|
|
646
|
+
if (!event) break;
|
|
647
|
+
frames += frameFor(event);
|
|
648
|
+
}
|
|
649
|
+
return frames;
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Version 2: events this write carries share one frame, a JSON array whose
|
|
653
|
+
* `id:` is its last index. Held events are always events, so only `first`
|
|
654
|
+
* can be a snapshot or presence frame.
|
|
655
|
+
*/
|
|
656
|
+
function batchedFrames(first, iterator) {
|
|
657
|
+
const lead = isStreamEvent(first) ? "" : frameFor(first);
|
|
658
|
+
const head = isStreamEvent(first) ? first : takeBuffered(iterator);
|
|
659
|
+
if (!head) return lead;
|
|
660
|
+
const { events, last } = heldEventBatch({
|
|
661
|
+
first: head,
|
|
662
|
+
iterator,
|
|
663
|
+
encode: compactJsonLine
|
|
664
|
+
});
|
|
665
|
+
return `${lead}id: ${last.index}\ndata: [${events.join(",")}]\n\n`;
|
|
666
|
+
}
|
|
667
|
+
const compactJsonLine = (event) => isSerializedEvent(event) && /[\r\n]/.test(event.payloadJson) ? compactEventJson(parseSerializedEvent(event)) : compactEventJson(event);
|
|
557
668
|
function frameFor(item) {
|
|
669
|
+
if ("kind" in item) return `event: a2-snapshot\ndata: ${JSON.stringify(stateSnapshotToWire(item))}\n\n`;
|
|
558
670
|
if ("snapshot" in item) return `event: presence-snapshot\ndata: ${JSON.stringify(presenceSnapshotToWire(item))}\n\n`;
|
|
559
671
|
if ("participant" in item) return `event: presence\ndata: ${JSON.stringify(presencePatchToWire(item))}\n\n`;
|
|
672
|
+
if (isSerializedEvent(item)) return /[\r\n]/.test(item.payloadJson) ? `id: ${item.index}\ndata: ${JSON.stringify(eventToWire(parseSerializedEvent(item)))}\n\n` : `id: ${item.index}\ndata: ${serializedEventToWireJson(item)}\n\n`;
|
|
560
673
|
return `id: ${item.index}\ndata: ${JSON.stringify(eventToWire(item))}\n\n`;
|
|
561
674
|
}
|
|
562
675
|
//#endregion
|
|
676
|
+
//#region src/bootstrap-stream.ts
|
|
677
|
+
function bootstrapStream(options) {
|
|
678
|
+
return { [Symbol.asyncIterator]() {
|
|
679
|
+
let closed = false;
|
|
680
|
+
let index;
|
|
681
|
+
let source;
|
|
682
|
+
let stopActivity;
|
|
683
|
+
const abort = new AbortController();
|
|
684
|
+
const activity = createStreamActivity(() => {
|
|
685
|
+
if (source) streamActivities.get(source)?.activate();
|
|
686
|
+
});
|
|
687
|
+
const iterator = {
|
|
688
|
+
async next() {
|
|
689
|
+
if (closed) return {
|
|
690
|
+
done: true,
|
|
691
|
+
value: void 0
|
|
692
|
+
};
|
|
693
|
+
if (index === void 0) {
|
|
694
|
+
let cancel;
|
|
695
|
+
const cancelled = new Promise((resolve) => {
|
|
696
|
+
cancel = () => resolve(void 0);
|
|
697
|
+
abort.signal.addEventListener("abort", cancel, { once: true });
|
|
698
|
+
});
|
|
699
|
+
try {
|
|
700
|
+
const snapshot = await Promise.race([options.snapshot(), cancelled]);
|
|
701
|
+
if (closed || snapshot === void 0) return {
|
|
702
|
+
done: true,
|
|
703
|
+
value: void 0
|
|
704
|
+
};
|
|
705
|
+
index = snapshot.index;
|
|
706
|
+
return {
|
|
707
|
+
done: false,
|
|
708
|
+
value: snapshot
|
|
709
|
+
};
|
|
710
|
+
} finally {
|
|
711
|
+
if (cancel) abort.signal.removeEventListener("abort", cancel);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
if (!source) {
|
|
715
|
+
source = options.stream(index)[Symbol.asyncIterator]();
|
|
716
|
+
const sourceActivity = streamActivities.get(source);
|
|
717
|
+
if (sourceActivity) {
|
|
718
|
+
activity.setIdle(sourceActivity.idle);
|
|
719
|
+
stopActivity = sourceActivity.subscribe(activity.setIdle);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
const result = await source.next();
|
|
723
|
+
return closed ? {
|
|
724
|
+
done: true,
|
|
725
|
+
value: void 0
|
|
726
|
+
} : result;
|
|
727
|
+
},
|
|
728
|
+
async return() {
|
|
729
|
+
if (closed) return {
|
|
730
|
+
done: true,
|
|
731
|
+
value: void 0
|
|
732
|
+
};
|
|
733
|
+
closed = true;
|
|
734
|
+
abort.abort();
|
|
735
|
+
stopActivity?.();
|
|
736
|
+
await source?.return?.();
|
|
737
|
+
return {
|
|
738
|
+
done: true,
|
|
739
|
+
value: void 0
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
streamActivities.set(iterator, activity);
|
|
744
|
+
bufferedEvents.set(iterator, () => closed || !source ? void 0 : takeBuffered(source));
|
|
745
|
+
return iterator;
|
|
746
|
+
} };
|
|
747
|
+
}
|
|
748
|
+
//#endregion
|
|
563
749
|
//#region src/server-fetch.ts
|
|
564
750
|
const serverFetchHooks = /* @__PURE__ */ new WeakMap();
|
|
565
751
|
function setServerFetchHooks(server, hooks) {
|
|
@@ -581,6 +767,23 @@ const parseResumeIndex = (raw) => {
|
|
|
581
767
|
if (!Number.isSafeInteger(index) || index < 0) throw new A2Error("INVALID_PAYLOAD", "stream index must be a non-negative safe integer");
|
|
582
768
|
return index;
|
|
583
769
|
};
|
|
770
|
+
const parseSubscription = (params) => {
|
|
771
|
+
const reducer = params.get("reducer");
|
|
772
|
+
const bootstrap = params.get("bootstrap");
|
|
773
|
+
const index = params.get("index");
|
|
774
|
+
if (reducer === "") throw new A2Error("INVALID_PAYLOAD", "empty reducer name");
|
|
775
|
+
if (bootstrap !== null) {
|
|
776
|
+
if (bootstrap !== "1" || index !== null || reducer === null) throw new A2Error("INVALID_PAYLOAD", "bootstrap requires a reducer and no index");
|
|
777
|
+
return {
|
|
778
|
+
bootstrap: true,
|
|
779
|
+
reducer
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
return {
|
|
783
|
+
index: parseResumeIndex(index),
|
|
784
|
+
...reducer === null ? {} : { reducer }
|
|
785
|
+
};
|
|
786
|
+
};
|
|
584
787
|
const errorResponse = (error) => {
|
|
585
788
|
const a2error = asA2Error(error);
|
|
586
789
|
return Response.json(errorToWire(a2error), { status: errorStatus(a2error.code) });
|
|
@@ -630,17 +833,54 @@ function createServerFetch(server) {
|
|
|
630
833
|
return asA2Error(error);
|
|
631
834
|
}
|
|
632
835
|
};
|
|
633
|
-
const
|
|
836
|
+
const selectedSession = (sessionId, selection) => {
|
|
837
|
+
const reducer = server.contract.reducer;
|
|
838
|
+
if (selection.reducer !== void 0 && reducer !== void 0 && reducer.name !== selection.reducer) throw new A2Error("INVALID_PAYLOAD", "client reducer does not match the public contract reducer");
|
|
839
|
+
const target = server.session(sessionId);
|
|
840
|
+
const streamTarget = target;
|
|
841
|
+
if (!selection.bootstrap) return streamTarget;
|
|
842
|
+
if (!reducer) throw new A2Error("INVALID_PAYLOAD", "contract has no public reducer");
|
|
843
|
+
return {
|
|
844
|
+
...streamTarget,
|
|
845
|
+
stream: (streamOptions) => bootstrapStream({
|
|
846
|
+
snapshot: async () => {
|
|
847
|
+
const initial = await target.state(reducer);
|
|
848
|
+
return {
|
|
849
|
+
kind: "snapshot",
|
|
850
|
+
sessionId,
|
|
851
|
+
name: reducer.name,
|
|
852
|
+
index: initial.index,
|
|
853
|
+
state: encodeState({
|
|
854
|
+
reducer,
|
|
855
|
+
state: initial.state
|
|
856
|
+
}),
|
|
857
|
+
...initial.clearIndex === void 0 ? {} : { clearIndex: initial.clearIndex }
|
|
858
|
+
};
|
|
859
|
+
},
|
|
860
|
+
stream: (index) => {
|
|
861
|
+
const bounds = {
|
|
862
|
+
startAfter: index,
|
|
863
|
+
presence: streamOptions.presence === true
|
|
864
|
+
};
|
|
865
|
+
return target.stream(bounds);
|
|
866
|
+
}
|
|
867
|
+
})
|
|
868
|
+
};
|
|
869
|
+
};
|
|
870
|
+
const attachMultiplexed = (wire) => (socket) => {
|
|
634
871
|
const deadline = invocationDeadlineMs();
|
|
635
|
-
sessionsSocket(async (sessionId, startAfter) => {
|
|
636
|
-
|
|
872
|
+
sessionsSocket(async (sessionId, startAfter, selection = { index: startAfter }) => {
|
|
873
|
+
const operation = {
|
|
637
874
|
type: "stream",
|
|
638
875
|
sessionId,
|
|
639
876
|
startAfter,
|
|
877
|
+
...selection.bootstrap ? { bootstrap: true } : {},
|
|
640
878
|
transport: "websocket"
|
|
641
|
-
}
|
|
879
|
+
};
|
|
880
|
+
return await authorized(operation) ? selectedSession(sessionId, selection) : null;
|
|
642
881
|
}, socket, {
|
|
643
882
|
presence,
|
|
883
|
+
wire,
|
|
644
884
|
...deadline === null ? {} : { deadline },
|
|
645
885
|
gatePush: gateSocketPush
|
|
646
886
|
});
|
|
@@ -649,23 +889,27 @@ function createServerFetch(server) {
|
|
|
649
889
|
if (request.method === "GET") {
|
|
650
890
|
if (request.headers.get("upgrade")?.toLowerCase() === "websocket") {
|
|
651
891
|
if (options?.upgradeWebSocket === void 0) return new Response("WebSocket upgrade requested, but server.fetch() has no upgrade implementation", { status: 426 });
|
|
652
|
-
if (options.multiplexWebSocket === true) return await options.upgradeWebSocket(attachMultiplexed);
|
|
653
892
|
const { searchParams } = new URL(request.url);
|
|
893
|
+
const wire = wireVersion(searchParams.get("wire"));
|
|
894
|
+
if (options.multiplexWebSocket === true) return await options.upgradeWebSocket(attachMultiplexed(wire));
|
|
654
895
|
const sessionId = searchParams.get("sessionId");
|
|
655
896
|
if (sessionId === null || sessionId.length === 0) throw new A2Error("INVALID_PAYLOAD", "missing sessionId");
|
|
656
|
-
const
|
|
897
|
+
const selection = parseSubscription(searchParams);
|
|
898
|
+
const startAfter = selection.index ?? 0;
|
|
657
899
|
await authorize({
|
|
658
900
|
type: "stream",
|
|
659
901
|
sessionId,
|
|
660
902
|
startAfter,
|
|
903
|
+
...selection.bootstrap ? { bootstrap: true } : {},
|
|
661
904
|
transport: "websocket"
|
|
662
905
|
});
|
|
663
|
-
const target =
|
|
906
|
+
const target = selectedSession(sessionId, selection);
|
|
664
907
|
const attach = (socket) => {
|
|
665
908
|
const deadline = invocationDeadlineMs();
|
|
666
909
|
sessionSocket(sessionId, target, socket, {
|
|
667
910
|
startAfter,
|
|
668
911
|
presence,
|
|
912
|
+
wire,
|
|
669
913
|
...deadline === null ? {} : { deadline },
|
|
670
914
|
gatePush: gateSocketPush
|
|
671
915
|
});
|
|
@@ -685,22 +929,28 @@ function createServerFetch(server) {
|
|
|
685
929
|
...bounds,
|
|
686
930
|
transport: "http"
|
|
687
931
|
});
|
|
688
|
-
const events = await server.
|
|
689
|
-
const
|
|
690
|
-
|
|
932
|
+
const { events, clearIndex } = await server.historyPage(sessionId, bounds);
|
|
933
|
+
const first = Math.max(bounds.gte, (clearIndex ?? 0) + 1);
|
|
934
|
+
const covered = events.length === Math.max(0, bounds.lte - first + 1);
|
|
935
|
+
return Response.json(events.map(eventToWire), { headers: {
|
|
936
|
+
"a2-history-covered": String(covered),
|
|
937
|
+
...clearIndex === void 0 ? {} : { "a2-clear-index": String(clearIndex) }
|
|
938
|
+
} });
|
|
691
939
|
}
|
|
692
|
-
const
|
|
940
|
+
const selection = parseSubscription(searchParams);
|
|
941
|
+
const startAfter = selection.index ?? 0;
|
|
693
942
|
await authorize({
|
|
694
943
|
type: "stream",
|
|
695
944
|
sessionId,
|
|
696
945
|
startAfter,
|
|
946
|
+
...selection.bootstrap ? { bootstrap: true } : {},
|
|
697
947
|
transport: "http"
|
|
698
948
|
});
|
|
699
|
-
const target =
|
|
949
|
+
const target = selectedSession(sessionId, selection);
|
|
700
950
|
return sseResponse(presence ? target.stream({
|
|
701
951
|
startAfter,
|
|
702
952
|
presence: true
|
|
703
|
-
}) : target.stream({ startAfter }));
|
|
953
|
+
}) : target.stream({ startAfter }), { wire: wireVersion(searchParams.get("wire")) });
|
|
704
954
|
}
|
|
705
955
|
if (request.method === "POST") {
|
|
706
956
|
const body = await parsePushBody(request);
|
|
@@ -746,6 +996,7 @@ const APPEND_NAMESPACE = "a2:handler-append:v2";
|
|
|
746
996
|
const RETURN_NAMESPACE = "a2:handler-return:v1";
|
|
747
997
|
const SCHEDULE_NAMESPACE = "a2:schedule:v1";
|
|
748
998
|
const SCHEDULED_EVENT_NAMESPACE = "a2:scheduled-event:v1";
|
|
999
|
+
const SCHEDULE_CLEAR_NAMESPACE = "a2:schedule-clear:v1";
|
|
749
1000
|
async function deterministicIdFromInput(input) {
|
|
750
1001
|
const digest = await crypto.subtle.digest("SHA-1", new TextEncoder().encode(input));
|
|
751
1002
|
const bytes = new Uint8Array(digest).slice(0, 16);
|
|
@@ -773,13 +1024,31 @@ async function deterministicReturnedEventId(triggerEventId, itemOrdinal) {
|
|
|
773
1024
|
itemOrdinal
|
|
774
1025
|
]);
|
|
775
1026
|
}
|
|
776
|
-
|
|
1027
|
+
/**
|
|
1028
|
+
* The id of a scheduled append. `clearIndex` is the lifetime the trigger was
|
|
1029
|
+
* claimed in; it joins the input only when positive, so a trigger id reused
|
|
1030
|
+
* after a clear schedules a distinct task while sessions never cleared keep
|
|
1031
|
+
* the ids they always had.
|
|
1032
|
+
*/
|
|
1033
|
+
async function deterministicScheduleId(contract, sessionId, triggerEventId, name, clearIndex = 0) {
|
|
777
1034
|
return deterministicIdFromInput(JSON.stringify([
|
|
778
1035
|
SCHEDULE_NAMESPACE,
|
|
779
1036
|
contract,
|
|
780
1037
|
sessionId,
|
|
781
1038
|
triggerEventId,
|
|
782
|
-
name
|
|
1039
|
+
name,
|
|
1040
|
+
...clearIndex > 0 ? [clearIndex] : []
|
|
1041
|
+
]));
|
|
1042
|
+
}
|
|
1043
|
+
async function deterministicScheduleClearId({ contract, sessionId, triggerEventId, clearIndex, name, ifLastIndex }) {
|
|
1044
|
+
return deterministicIdFromInput(JSON.stringify([
|
|
1045
|
+
SCHEDULE_CLEAR_NAMESPACE,
|
|
1046
|
+
contract,
|
|
1047
|
+
sessionId,
|
|
1048
|
+
triggerEventId,
|
|
1049
|
+
clearIndex,
|
|
1050
|
+
name,
|
|
1051
|
+
ifLastIndex
|
|
783
1052
|
]));
|
|
784
1053
|
}
|
|
785
1054
|
async function deterministicScheduledEventId(scheduleId, itemOrdinal) {
|
|
@@ -812,14 +1081,42 @@ const CONSOLE_TELEMETRY = { span: (name, attributes, fn) => fn({
|
|
|
812
1081
|
*/
|
|
813
1082
|
async function deliverSchedulerAppend(server, task) {
|
|
814
1083
|
if (server.contract.name !== task.contract) throw new TypeError(`a2 scheduler: append task for contract '${task.contract}' cannot be delivered to '${server.contract.name}'`);
|
|
1084
|
+
const { originIndex } = task;
|
|
1085
|
+
if (originIndex !== void 0 && (!Number.isSafeInteger(originIndex) || originIndex < 1)) throw new TypeError("a2 scheduler: append task originIndex must be a positive safe integer");
|
|
815
1086
|
const internals = serverInternals.get(server);
|
|
816
1087
|
if (!internals) throw new TypeError(`a2 scheduler: server for contract '${task.contract}' cannot receive scheduled appends`);
|
|
817
|
-
await internals.schedulerAppend(task.sessionId, task.events);
|
|
1088
|
+
await internals.schedulerAppend(task.sessionId, task.events, originIndex);
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Deliver one authenticated scheduler clear: a conditional
|
|
1092
|
+
* `clear({ ifLastIndex })` that resolves after its deletion finishes, so a
|
|
1093
|
+
* provider retry resumes an interrupted deletion. Custom scheduler adapters
|
|
1094
|
+
* call this after validating their transport envelope; application code uses
|
|
1095
|
+
* `session.schedule.clear`.
|
|
1096
|
+
*/
|
|
1097
|
+
async function deliverSchedulerClear(server, task) {
|
|
1098
|
+
if (server.contract.name !== task.contract) throw new TypeError(`a2 scheduler: clear task for contract '${task.contract}' cannot be delivered to '${server.contract.name}'`);
|
|
1099
|
+
if (task.kind !== "clear" || !Number.isSafeInteger(task.ifLastIndex) || task.ifLastIndex < 0) throw new TypeError("a2 scheduler: a clear task needs kind \"clear\" and a non-negative safe integer ifLastIndex");
|
|
1100
|
+
const internals = serverInternals.get(server);
|
|
1101
|
+
if (!internals) throw new TypeError(`a2 scheduler: server for contract '${task.contract}' cannot receive scheduled clears`);
|
|
1102
|
+
await internals.schedulerClear(task.sessionId, task.ifLastIndex);
|
|
818
1103
|
}
|
|
819
1104
|
function handler(entry) {
|
|
820
1105
|
return typeof entry === "function" ? { handler: entry } : entry;
|
|
821
1106
|
}
|
|
822
1107
|
const MAX_FAILURES = 10;
|
|
1108
|
+
/** Abort a lost attempt's handler and release its drain slot. */
|
|
1109
|
+
function endSuperseded(entry, message) {
|
|
1110
|
+
entry.supersede.abort(new A2Error("SUPERSEDED_ATTEMPT", message));
|
|
1111
|
+
entry.fence({
|
|
1112
|
+
status: "superseded",
|
|
1113
|
+
opensLane: false,
|
|
1114
|
+
appendsEvents: false
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
/** Pause between clear-deletion batches that found only locked rows. */
|
|
1118
|
+
const RECLAIM_STALL_MS = 20;
|
|
1119
|
+
const RECLAIM_STALL_LIMIT = 50;
|
|
823
1120
|
/** Default: how long a presence value survives without a refreshing set. */
|
|
824
1121
|
const PRESENCE_TTL_MS = 6e4;
|
|
825
1122
|
function createDrainSignal() {
|
|
@@ -901,6 +1198,19 @@ function currentSchedulerSlot(now) {
|
|
|
901
1198
|
*/
|
|
902
1199
|
const NS = "";
|
|
903
1200
|
const devDefaultStore = retryableLazy(() => import("./store-sqlite.js").then((m) => m.sqlite()));
|
|
1201
|
+
/** Waits for a scheduler arm for at most `schedulerArmTimeoutMs`. */
|
|
1202
|
+
async function joinSchedulerArm(arm) {
|
|
1203
|
+
let timeout = null;
|
|
1204
|
+
try {
|
|
1205
|
+
await Promise.race([arm, new Promise((_, reject) => {
|
|
1206
|
+
timeout = setTimeout(() => {
|
|
1207
|
+
reject(/* @__PURE__ */ new Error(`a2 scheduler arm timed out after ${DRAIN_TIMINGS.schedulerArmTimeoutMs}ms`));
|
|
1208
|
+
}, DRAIN_TIMINGS.schedulerArmTimeoutMs);
|
|
1209
|
+
})]);
|
|
1210
|
+
} finally {
|
|
1211
|
+
if (timeout) clearTimeout(timeout);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
904
1214
|
function environment() {
|
|
905
1215
|
const env = typeof process === "undefined" ? void 0 : process.env?.["NODE_ENV"];
|
|
906
1216
|
if (env === "test") return "test";
|
|
@@ -927,6 +1237,17 @@ function foldPresenceRows(rows) {
|
|
|
927
1237
|
};
|
|
928
1238
|
return map;
|
|
929
1239
|
}
|
|
1240
|
+
const addSnapshotWrite = (writes, index, state, pinEventIndex) => {
|
|
1241
|
+
let write = writes.get(index);
|
|
1242
|
+
if (!write) {
|
|
1243
|
+
write = {
|
|
1244
|
+
state: cloneInitial(state),
|
|
1245
|
+
pinEventIndexes: /* @__PURE__ */ new Set()
|
|
1246
|
+
};
|
|
1247
|
+
writes.set(index, write);
|
|
1248
|
+
}
|
|
1249
|
+
if (pinEventIndex !== void 0) write.pinEventIndexes.add(pinEventIndex);
|
|
1250
|
+
};
|
|
930
1251
|
/** Implement a contract: bind its vocabulary to storage and reactions. */
|
|
931
1252
|
function createServer(options) {
|
|
932
1253
|
const serverContract = options?.contract;
|
|
@@ -1107,7 +1428,7 @@ function createServer(options) {
|
|
|
1107
1428
|
* the drain it schedules is created in the append's active context —
|
|
1108
1429
|
* with a real tracer, causal work remains connected.
|
|
1109
1430
|
*/
|
|
1110
|
-
const appendCore = (sessionId, events, source, mode, onAppended, cause, generatedIds, requireDurableInitialArm = false) => telemetry.span("a2.append", {
|
|
1431
|
+
const appendCore = (sessionId, events, source, mode, onAppended, cause, generatedIds, requireDurableInitialArm = false, originIndex) => telemetry.span("a2.append", {
|
|
1111
1432
|
"a2.contract": name,
|
|
1112
1433
|
"a2.session_id": sessionId,
|
|
1113
1434
|
"a2.append.source": source,
|
|
@@ -1126,9 +1447,9 @@ function createServer(options) {
|
|
|
1126
1447
|
let rows;
|
|
1127
1448
|
let shouldHealSession;
|
|
1128
1449
|
try {
|
|
1129
|
-
const result = await store.append(nsId(sessionId), validated);
|
|
1450
|
+
const result = await store.append(nsId(sessionId), validated, originIndex === void 0 ? void 0 : { originIndex });
|
|
1130
1451
|
rows = result.events;
|
|
1131
|
-
shouldHealSession = result.hasPending;
|
|
1452
|
+
shouldHealSession = result.hasPending && !(source === "handler" && validated.every((event) => event.settled === true));
|
|
1132
1453
|
} catch (err) {
|
|
1133
1454
|
throw asStoreUnavailable(err);
|
|
1134
1455
|
}
|
|
@@ -1138,21 +1459,12 @@ function createServer(options) {
|
|
|
1138
1459
|
const dispatchArm = shouldHealSession && mode === "dispatch" ? startSchedulerArm(sessionId, currentSchedulerSlot(Date.now())) : null;
|
|
1139
1460
|
if (shouldHealSession && mode === "inline") onAppended(appended, initialWatchdogAt);
|
|
1140
1461
|
if (dispatchArm) await dispatchArm;
|
|
1141
|
-
if (initialArm) {
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
}, DRAIN_TIMINGS.schedulerArmTimeoutMs);
|
|
1148
|
-
})]);
|
|
1149
|
-
} catch (err) {
|
|
1150
|
-
span.setAttribute("a2.append.armed", false);
|
|
1151
|
-
span.recordError(err);
|
|
1152
|
-
if (requireDurableInitialArm) throw err;
|
|
1153
|
-
} finally {
|
|
1154
|
-
if (timeout) clearTimeout(timeout);
|
|
1155
|
-
}
|
|
1462
|
+
if (initialArm) try {
|
|
1463
|
+
await joinSchedulerArm(initialArm);
|
|
1464
|
+
} catch (err) {
|
|
1465
|
+
span.setAttribute("a2.append.armed", false);
|
|
1466
|
+
span.recordError(err);
|
|
1467
|
+
if (requireDurableInitialArm) throw err;
|
|
1156
1468
|
}
|
|
1157
1469
|
return appended;
|
|
1158
1470
|
});
|
|
@@ -1204,113 +1516,263 @@ function createServer(options) {
|
|
|
1204
1516
|
}
|
|
1205
1517
|
return plans;
|
|
1206
1518
|
};
|
|
1519
|
+
const persistSnapshots = ({ store, reducer, sessionId, snapshots }) => {
|
|
1520
|
+
const persistence = Promise.resolve().then(() => telemetry.span("a2.snapshot", {
|
|
1521
|
+
"a2.contract": name,
|
|
1522
|
+
"a2.session_id": sessionId,
|
|
1523
|
+
"a2.snapshot.reducer": reducer.name,
|
|
1524
|
+
"a2.snapshot.index": snapshots.at(-1).index,
|
|
1525
|
+
"a2.snapshot.count": snapshots.length
|
|
1526
|
+
}, async (span) => {
|
|
1527
|
+
try {
|
|
1528
|
+
await store.putSnapshots(nsId(sessionId), reducer.name, snapshots);
|
|
1529
|
+
} catch (error) {
|
|
1530
|
+
try {
|
|
1531
|
+
span.recordError(error);
|
|
1532
|
+
} catch (telemetryError) {
|
|
1533
|
+
throw new AggregateError([error, telemetryError], "Snapshot write and error reporting failed", { cause: telemetryError });
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
})).catch((error) => {
|
|
1537
|
+
console.error("[a2] error in a2.snapshot telemetry", {
|
|
1538
|
+
sessionId,
|
|
1539
|
+
reducer: reducer.name,
|
|
1540
|
+
index: snapshots.at(-1).index
|
|
1541
|
+
}, error);
|
|
1542
|
+
});
|
|
1543
|
+
track(persistence);
|
|
1544
|
+
platformWaitUntil(persistence);
|
|
1545
|
+
};
|
|
1546
|
+
const stateCache = boundedCache(STATE_CACHE);
|
|
1547
|
+
const cacheState = (key, next) => {
|
|
1548
|
+
const current = stateCache.get(key);
|
|
1549
|
+
if (current?.reducer === next.reducer && (current.index > next.index || current.index === next.index && current.persistedIndex >= next.persistedIndex)) return;
|
|
1550
|
+
stateCache.set(key, next, next.bytes);
|
|
1551
|
+
};
|
|
1552
|
+
const writeSnapshots = ({ store, reducer, sessionId, writes }) => {
|
|
1553
|
+
const snapshots = [...writes].toSorted(([a], [b]) => a - b).map(([index, write]) => {
|
|
1554
|
+
const snapshot = {
|
|
1555
|
+
index,
|
|
1556
|
+
state: encodeState({
|
|
1557
|
+
reducer,
|
|
1558
|
+
state: write.state
|
|
1559
|
+
})
|
|
1560
|
+
};
|
|
1561
|
+
if (write.pinEventIndexes.size > 0) snapshot.pinEventIndexes = [...write.pinEventIndexes];
|
|
1562
|
+
return snapshot;
|
|
1563
|
+
});
|
|
1564
|
+
if (snapshots.length > 0) persistSnapshots({
|
|
1565
|
+
store,
|
|
1566
|
+
reducer,
|
|
1567
|
+
sessionId,
|
|
1568
|
+
snapshots
|
|
1569
|
+
});
|
|
1570
|
+
return snapshots;
|
|
1571
|
+
};
|
|
1572
|
+
/**
|
|
1573
|
+
* Folds `rows` from `position` in bound order, resolving each entry with a
|
|
1574
|
+
* clone once the rows through its bound are folded. `visit` sees each
|
|
1575
|
+
* entry's state before later rows are folded into it.
|
|
1576
|
+
*/
|
|
1577
|
+
const foldEntries = ({ reducer, entries, rows, position, state, index, clearIndex, snapshot, visit }) => {
|
|
1578
|
+
const start = position;
|
|
1579
|
+
const ordered = entries.toSorted((a, b) => (a.throughIndex ?? Number.POSITIVE_INFINITY) - (b.throughIndex ?? Number.POSITIVE_INFINITY));
|
|
1580
|
+
for (const entry of ordered) {
|
|
1581
|
+
const target = entry.throughIndex ?? Number.POSITIVE_INFINITY;
|
|
1582
|
+
while (position < rows.length && rows[position].index <= target) {
|
|
1583
|
+
const row = rows[position];
|
|
1584
|
+
state = foldEvent(reducer, state, toPublic(row));
|
|
1585
|
+
index = row.index;
|
|
1586
|
+
position += 1;
|
|
1587
|
+
}
|
|
1588
|
+
if (entry.triggerIndex !== void 0 && entry.triggerIndex <= clearIndex || entry.throughIndex !== void 0 && entry.throughIndex > 0 && entry.throughIndex < clearIndex) {
|
|
1589
|
+
entry.reject(sessionClearedError(`session '${entry.sessionId}' was cleared at index ${clearIndex}`));
|
|
1590
|
+
continue;
|
|
1591
|
+
}
|
|
1592
|
+
entry.span.setAttribute("a2.state.snapshot", snapshot);
|
|
1593
|
+
entry.span.setAttribute("a2.state.folded", position - start);
|
|
1594
|
+
entry.span.setAttribute("a2.state.index", index);
|
|
1595
|
+
visit(entry, state, index, position - start);
|
|
1596
|
+
entry.resolve({
|
|
1597
|
+
state: cloneInitial(state),
|
|
1598
|
+
index,
|
|
1599
|
+
...clearIndex > 0 && index >= clearIndex ? { clearIndex } : {}
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
return {
|
|
1603
|
+
state,
|
|
1604
|
+
index,
|
|
1605
|
+
position
|
|
1606
|
+
};
|
|
1607
|
+
};
|
|
1608
|
+
/**
|
|
1609
|
+
* Events from `fromIndex` (inclusive) through `throughIndex` or the newest
|
|
1610
|
+
* event, or null when the tail is too long to be worth folding in memory.
|
|
1611
|
+
*/
|
|
1612
|
+
const readCachedTail = async (store, sessionId, fromIndex, throughIndex) => {
|
|
1613
|
+
const readTail = stateTailReaders.get(store);
|
|
1614
|
+
if (readTail) try {
|
|
1615
|
+
return await readTail(nsId(sessionId), fromIndex, throughIndex);
|
|
1616
|
+
} catch (err) {
|
|
1617
|
+
throw asStoreUnavailable(err);
|
|
1618
|
+
}
|
|
1619
|
+
const limit = fromIndex + STATE_CACHE.maxTail;
|
|
1620
|
+
const capped = throughIndex === void 0 || throughIndex > limit;
|
|
1621
|
+
const rows = await readHistory(sessionId, {
|
|
1622
|
+
gte: fromIndex,
|
|
1623
|
+
lte: capped ? limit : throughIndex
|
|
1624
|
+
});
|
|
1625
|
+
return capped && rows.at(-1)?.index === limit ? null : rows;
|
|
1626
|
+
};
|
|
1627
|
+
/**
|
|
1628
|
+
* Serves a plan from the cached state when every read is at or past the
|
|
1629
|
+
* cached index. A read bounded at the cached index by an event whose id
|
|
1630
|
+
* matches the cached one (a handler's own trigger) needs no store request;
|
|
1631
|
+
* any other read fetches the tail from the cached event onward and checks
|
|
1632
|
+
* that event's id. Returns false when the store must answer instead.
|
|
1633
|
+
*/
|
|
1634
|
+
const foldCachedPlan = async (store, reducer, plan) => {
|
|
1635
|
+
const key = stateCacheKey(plan.sessionId, reducer.name);
|
|
1636
|
+
const cached = stateCache.get(key);
|
|
1637
|
+
if (cached?.reducer !== reducer) return false;
|
|
1638
|
+
if (plan.entries.some((entry) => entry.throughIndex !== void 0 && entry.throughIndex < cached.index)) return false;
|
|
1639
|
+
let rows = [];
|
|
1640
|
+
if (plan.entries.some((entry) => entry.throughIndex !== cached.index || entry.throughEventId !== cached.lastId)) {
|
|
1641
|
+
const tail = await readCachedTail(store, plan.sessionId, cached.index, plan.throughIndex);
|
|
1642
|
+
if (!tail) return false;
|
|
1643
|
+
if (tail[0]?.index !== cached.index || tail[0].id !== cached.lastId || tail.some((row, position) => position > 0 && isSessionCleared(row))) {
|
|
1644
|
+
stateCache.delete(key);
|
|
1645
|
+
return false;
|
|
1646
|
+
}
|
|
1647
|
+
rows = tail;
|
|
1648
|
+
}
|
|
1649
|
+
const writes = /* @__PURE__ */ new Map();
|
|
1650
|
+
const folded = foldEntries({
|
|
1651
|
+
reducer,
|
|
1652
|
+
entries: plan.entries,
|
|
1653
|
+
rows,
|
|
1654
|
+
position: 1,
|
|
1655
|
+
state: rows.length > 1 ? cloneInitial(cached.state) : cached.state,
|
|
1656
|
+
index: cached.index,
|
|
1657
|
+
clearIndex: cached.clearIndex,
|
|
1658
|
+
snapshot: "memory",
|
|
1659
|
+
visit: (entry, state, index) => {
|
|
1660
|
+
if (entry.pinEventIndex !== void 0) addSnapshotWrite(writes, index, state, entry.pinEventIndex);
|
|
1661
|
+
}
|
|
1662
|
+
});
|
|
1663
|
+
const added = Math.max(0, folded.position - 1);
|
|
1664
|
+
const rewritesHead = folded.index - cached.persistedIndex >= STATE_CACHE.snapshotEvery;
|
|
1665
|
+
if (rewritesHead && !writes.has(folded.index)) writes.set(folded.index, {
|
|
1666
|
+
state: folded.state,
|
|
1667
|
+
pinEventIndexes: /* @__PURE__ */ new Set()
|
|
1668
|
+
});
|
|
1669
|
+
const snapshots = writeSnapshots({
|
|
1670
|
+
store,
|
|
1671
|
+
reducer,
|
|
1672
|
+
sessionId: plan.sessionId,
|
|
1673
|
+
writes
|
|
1674
|
+
});
|
|
1675
|
+
const measure = rewritesHead || cached.unmeasured + added >= STATE_CACHE.measureEvery;
|
|
1676
|
+
let grown = 0;
|
|
1677
|
+
if (!measure) for (let position = 1; position < folded.position; position += 1) grown += JSON.stringify(rows[position].payload)?.length ?? 0;
|
|
1678
|
+
cacheState(key, {
|
|
1679
|
+
reducer,
|
|
1680
|
+
index: folded.index,
|
|
1681
|
+
lastId: added > 0 ? rows[folded.position - 1].id : cached.lastId,
|
|
1682
|
+
state: folded.state,
|
|
1683
|
+
bytes: measure ? measuredBytes(reducer, folded.state, folded.index, snapshots) : cached.bytes + grown,
|
|
1684
|
+
unmeasured: measure ? 0 : cached.unmeasured + added,
|
|
1685
|
+
persistedIndex: Math.max(cached.persistedIndex, snapshots.at(-1)?.index ?? 0),
|
|
1686
|
+
clearIndex: cached.clearIndex
|
|
1687
|
+
});
|
|
1688
|
+
return true;
|
|
1689
|
+
};
|
|
1207
1690
|
const foldStatePlan = async (store, reducer, plan, stateRead) => {
|
|
1208
1691
|
let state = cloneInitial(reducer.initialState);
|
|
1209
1692
|
let index = 0;
|
|
1210
1693
|
let snapshotOutcome = "miss";
|
|
1211
1694
|
const snap = stateRead.snapshot;
|
|
1212
1695
|
let rows = stateRead.events;
|
|
1696
|
+
let clearIndex = stateRead.clearIndex ?? 0;
|
|
1213
1697
|
if (snap) {
|
|
1214
|
-
|
|
1215
|
-
|
|
1698
|
+
let decoded;
|
|
1699
|
+
try {
|
|
1700
|
+
decoded = decodeState({
|
|
1701
|
+
reducer,
|
|
1702
|
+
state: snap.state
|
|
1703
|
+
});
|
|
1704
|
+
} catch {
|
|
1705
|
+
snapshotOutcome = "rejected";
|
|
1706
|
+
}
|
|
1707
|
+
if (snapshotOutcome !== "rejected" && reducer.stateSchema) {
|
|
1708
|
+
const result = validateSync(reducer.stateSchema, decoded, "the stateSchema");
|
|
1216
1709
|
if (result.issues) snapshotOutcome = "rejected";
|
|
1217
1710
|
else {
|
|
1218
1711
|
state = result.value;
|
|
1219
1712
|
index = snap.index;
|
|
1220
1713
|
snapshotOutcome = "hit";
|
|
1221
1714
|
}
|
|
1222
|
-
} else {
|
|
1223
|
-
state =
|
|
1715
|
+
} else if (snapshotOutcome !== "rejected") {
|
|
1716
|
+
state = decoded;
|
|
1224
1717
|
index = snap.index;
|
|
1225
1718
|
snapshotOutcome = "hit";
|
|
1226
1719
|
}
|
|
1227
1720
|
}
|
|
1228
|
-
if (snapshotOutcome === "rejected")
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
const ordered = plan.entries.toSorted((a, b) => (a.throughIndex ?? Number.POSITIVE_INFINITY) - (b.throughIndex ?? Number.POSITIVE_INFINITY));
|
|
1234
|
-
const writes = /* @__PURE__ */ new Map();
|
|
1235
|
-
let rowPosition = 0;
|
|
1236
|
-
for (const entry of ordered) {
|
|
1237
|
-
const target = entry.throughIndex ?? Number.POSITIVE_INFINITY;
|
|
1238
|
-
while (rowPosition < rows.length && rows[rowPosition].index <= target) {
|
|
1239
|
-
const row = rows[rowPosition];
|
|
1240
|
-
state = reducer.fold(state, toPublic(row));
|
|
1241
|
-
index = row.index;
|
|
1242
|
-
rowPosition += 1;
|
|
1243
|
-
}
|
|
1244
|
-
const resultState = cloneInitial(state);
|
|
1245
|
-
const folded = rowPosition;
|
|
1246
|
-
entry.span.setAttribute("a2.state.snapshot", snapshotOutcome);
|
|
1247
|
-
entry.span.setAttribute("a2.state.folded", folded);
|
|
1248
|
-
entry.span.setAttribute("a2.state.index", index);
|
|
1249
|
-
if (index > 0 && (folded > 0 || entry.pinEventIndex !== void 0)) {
|
|
1250
|
-
const advancesHead = stateRead.headIndex === null || index > stateRead.headIndex;
|
|
1251
|
-
const repairsHead = snapshotOutcome === "rejected" && index === stateRead.headIndex;
|
|
1252
|
-
if (!advancesHead && !repairsHead && entry.pinEventIndex === void 0) {
|
|
1253
|
-
entry.resolve({
|
|
1254
|
-
state: resultState,
|
|
1255
|
-
index
|
|
1256
|
-
});
|
|
1257
|
-
continue;
|
|
1258
|
-
}
|
|
1259
|
-
let write = writes.get(index);
|
|
1260
|
-
if (!write) {
|
|
1261
|
-
write = {
|
|
1262
|
-
state: cloneInitial(resultState),
|
|
1263
|
-
pinEventIndexes: /* @__PURE__ */ new Set()
|
|
1264
|
-
};
|
|
1265
|
-
writes.set(index, write);
|
|
1266
|
-
}
|
|
1267
|
-
if (entry.pinEventIndex !== void 0) write.pinEventIndexes.add(entry.pinEventIndex);
|
|
1721
|
+
if (snapshotOutcome === "rejected") {
|
|
1722
|
+
try {
|
|
1723
|
+
rows = await store.read(nsId(plan.sessionId), plan.throughIndex === void 0 ? void 0 : { throughIndex: plan.throughIndex });
|
|
1724
|
+
} catch (err) {
|
|
1725
|
+
throw asStoreUnavailable(err);
|
|
1268
1726
|
}
|
|
1269
|
-
|
|
1270
|
-
state: resultState,
|
|
1271
|
-
index
|
|
1272
|
-
});
|
|
1727
|
+
if (rows[0] && isSessionCleared(rows[0])) clearIndex = Math.max(clearIndex, rows[0].index);
|
|
1273
1728
|
}
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1729
|
+
const writes = /* @__PURE__ */ new Map();
|
|
1730
|
+
const folded = foldEntries({
|
|
1731
|
+
reducer,
|
|
1732
|
+
entries: plan.entries,
|
|
1733
|
+
rows,
|
|
1734
|
+
position: 0,
|
|
1735
|
+
state,
|
|
1736
|
+
index,
|
|
1737
|
+
clearIndex,
|
|
1738
|
+
snapshot: snapshotOutcome,
|
|
1739
|
+
visit: (entry, entryState, entryIndex, count) => {
|
|
1740
|
+
if (entryIndex === 0) return;
|
|
1741
|
+
if (count === 0 && entry.pinEventIndex === void 0) return;
|
|
1742
|
+
const advancesHead = stateRead.headIndex === null || entryIndex > stateRead.headIndex;
|
|
1743
|
+
const repairsHead = snapshotOutcome === "rejected" && entryIndex === stateRead.headIndex;
|
|
1744
|
+
if (!advancesHead && !repairsHead && entry.pinEventIndex === void 0) return;
|
|
1745
|
+
addSnapshotWrite(writes, entryIndex, entryState, entry.pinEventIndex);
|
|
1283
1746
|
}
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
console.error("[a2] error in a2.snapshot telemetry", {
|
|
1302
|
-
sessionId: plan.sessionId,
|
|
1303
|
-
reducer: reducer.name,
|
|
1304
|
-
index: snapshots.at(-1).index
|
|
1305
|
-
}, error);
|
|
1306
|
-
});
|
|
1307
|
-
track(persistence);
|
|
1308
|
-
platformWaitUntil(persistence);
|
|
1309
|
-
}
|
|
1747
|
+
});
|
|
1748
|
+
const snapshots = writeSnapshots({
|
|
1749
|
+
store,
|
|
1750
|
+
reducer,
|
|
1751
|
+
sessionId: plan.sessionId,
|
|
1752
|
+
writes
|
|
1753
|
+
});
|
|
1754
|
+
if (folded.position > 0 && STATE_CACHE.maxEntries > 0) cacheState(stateCacheKey(plan.sessionId, reducer.name), {
|
|
1755
|
+
reducer,
|
|
1756
|
+
index: folded.index,
|
|
1757
|
+
lastId: rows[folded.position - 1].id,
|
|
1758
|
+
state: folded.state,
|
|
1759
|
+
bytes: measuredBytes(reducer, folded.state, folded.index, snapshots),
|
|
1760
|
+
unmeasured: 0,
|
|
1761
|
+
persistedIndex: Math.min(Math.max(stateRead.headIndex ?? 0, ...writes.keys()), folded.index),
|
|
1762
|
+
clearIndex
|
|
1763
|
+
});
|
|
1310
1764
|
};
|
|
1311
1765
|
const pendingStateReads = /* @__PURE__ */ new Map();
|
|
1312
1766
|
const flushStateReads = async (store, reducer, batch) => {
|
|
1313
|
-
const plans =
|
|
1767
|
+
const plans = [];
|
|
1768
|
+
await Promise.all(planStateReads(batch).map(async (plan) => {
|
|
1769
|
+
try {
|
|
1770
|
+
if (!await foldCachedPlan(store, reducer, plan)) plans.push(plan);
|
|
1771
|
+
} catch (error) {
|
|
1772
|
+
for (const entry of plan.entries) entry.reject(error);
|
|
1773
|
+
}
|
|
1774
|
+
}));
|
|
1775
|
+
if (plans.length === 0) return;
|
|
1314
1776
|
let reads;
|
|
1315
1777
|
try {
|
|
1316
1778
|
reads = store.readStates ? await store.readStates(plans.map((plan) => ({
|
|
@@ -1321,7 +1783,7 @@ function createServer(options) {
|
|
|
1321
1783
|
if (reads.length !== plans.length) throw new TypeError("store.readStates must return one result per request");
|
|
1322
1784
|
} catch (error) {
|
|
1323
1785
|
const failure = asStoreUnavailable(error);
|
|
1324
|
-
for (const entry of
|
|
1786
|
+
for (const plan of plans) for (const entry of plan.entries) entry.reject(failure);
|
|
1325
1787
|
return;
|
|
1326
1788
|
}
|
|
1327
1789
|
await Promise.all(plans.map(async (plan, position) => {
|
|
@@ -1332,7 +1794,7 @@ function createServer(options) {
|
|
|
1332
1794
|
}
|
|
1333
1795
|
}));
|
|
1334
1796
|
};
|
|
1335
|
-
const enqueueStateRead = (store, sessionId, reducer,
|
|
1797
|
+
const enqueueStateRead = (store, sessionId, reducer, bounds, span) => new Promise((resolve, reject) => {
|
|
1336
1798
|
const reducerName = reducer.name;
|
|
1337
1799
|
let batch = pendingStateReads.get(reducerName);
|
|
1338
1800
|
if (!batch) {
|
|
@@ -1346,25 +1808,25 @@ function createServer(options) {
|
|
|
1346
1808
|
}
|
|
1347
1809
|
batch.push({
|
|
1348
1810
|
sessionId,
|
|
1349
|
-
...
|
|
1350
|
-
...pinEventIndex === void 0 ? {} : { pinEventIndex },
|
|
1811
|
+
...bounds,
|
|
1351
1812
|
span,
|
|
1352
1813
|
resolve,
|
|
1353
1814
|
reject
|
|
1354
1815
|
});
|
|
1355
1816
|
});
|
|
1356
|
-
const readState = async (sessionId, reducer,
|
|
1817
|
+
const readState = async (sessionId, reducer, bounds) => {
|
|
1357
1818
|
const store = await resolveStore();
|
|
1358
1819
|
return telemetry.span("a2.state", {
|
|
1359
1820
|
"a2.contract": name,
|
|
1360
1821
|
"a2.session_id": sessionId,
|
|
1361
1822
|
"a2.state.reducer": reducer.name
|
|
1362
|
-
}, (span) => enqueueStateRead(store, sessionId, reducer,
|
|
1823
|
+
}, (span) => enqueueStateRead(store, sessionId, reducer, bounds, span));
|
|
1363
1824
|
};
|
|
1364
1825
|
const makeSchedule = (sessionId, trigger) => {
|
|
1365
1826
|
const triggerEventId = trigger?.id ?? null;
|
|
1366
1827
|
const triggerCreatedAt = trigger?.createdAt.getTime();
|
|
1367
|
-
|
|
1828
|
+
const clearIndex = trigger?.clearIndex ?? 0;
|
|
1829
|
+
const schedule = async (scheduleName, timing, ...events) => {
|
|
1368
1830
|
if (!scheduler) throw new TypeError("session.schedule requires a configured scheduler adapter");
|
|
1369
1831
|
assertScheduleName(scheduleName);
|
|
1370
1832
|
if (events.length === 0) throw new TypeError("schedule requires at least one event");
|
|
@@ -1379,7 +1841,7 @@ function createServer(options) {
|
|
|
1379
1841
|
id: event.id
|
|
1380
1842
|
});
|
|
1381
1843
|
const validated = validateEvents(sessionId, structuredClone(snapshottedEvents), false);
|
|
1382
|
-
const scheduleId = await deterministicScheduleId(name, sessionId, triggerEventId, scheduleName);
|
|
1844
|
+
const scheduleId = await deterministicScheduleId(name, sessionId, triggerEventId, scheduleName, clearIndex);
|
|
1383
1845
|
const scheduledEvents = await Promise.all(validated.map(async (event, item) => ({
|
|
1384
1846
|
id: event.id ?? await deterministicScheduledEventId(scheduleId, item),
|
|
1385
1847
|
type: event.type,
|
|
@@ -1394,12 +1856,45 @@ function createServer(options) {
|
|
|
1394
1856
|
contract: name,
|
|
1395
1857
|
sessionId,
|
|
1396
1858
|
dueAt,
|
|
1397
|
-
events: scheduledEvents
|
|
1859
|
+
events: scheduledEvents,
|
|
1860
|
+
...trigger ? { originIndex: trigger.index } : {}
|
|
1398
1861
|
});
|
|
1399
1862
|
} catch (error) {
|
|
1400
1863
|
throw markSchedulerSendFailure(error);
|
|
1401
1864
|
}
|
|
1402
1865
|
};
|
|
1866
|
+
const clear = async (scheduleName, clearOptions) => {
|
|
1867
|
+
if (!scheduler) throw new TypeError("session.schedule.clear requires a configured scheduler adapter");
|
|
1868
|
+
assertScheduleName(scheduleName);
|
|
1869
|
+
if (clearOptions === null || typeof clearOptions !== "object") throw new TypeError("schedule.clear requires an ifLastIndex");
|
|
1870
|
+
const { ifLastIndex, ...timing } = clearOptions;
|
|
1871
|
+
assertClearIndex(ifLastIndex);
|
|
1872
|
+
const dueAt = scheduleDueAt(timing, triggerCreatedAt ?? Date.now());
|
|
1873
|
+
const store = await resolveStore();
|
|
1874
|
+
if (!store.clear || !store.reclaim) throw new TypeError("session.schedule.clear requires a store that supports clearing");
|
|
1875
|
+
const id = await deterministicScheduleClearId({
|
|
1876
|
+
contract: name,
|
|
1877
|
+
sessionId,
|
|
1878
|
+
triggerEventId,
|
|
1879
|
+
clearIndex,
|
|
1880
|
+
name: scheduleName,
|
|
1881
|
+
ifLastIndex
|
|
1882
|
+
});
|
|
1883
|
+
try {
|
|
1884
|
+
await scheduler.schedule({
|
|
1885
|
+
version: 1,
|
|
1886
|
+
kind: "clear",
|
|
1887
|
+
id,
|
|
1888
|
+
contract: name,
|
|
1889
|
+
sessionId,
|
|
1890
|
+
dueAt,
|
|
1891
|
+
ifLastIndex
|
|
1892
|
+
});
|
|
1893
|
+
} catch (error) {
|
|
1894
|
+
throw markSchedulerSendFailure(error);
|
|
1895
|
+
}
|
|
1896
|
+
};
|
|
1897
|
+
return Object.assign(schedule, { clear });
|
|
1403
1898
|
};
|
|
1404
1899
|
const toSessionEvent = (id, event) => ({
|
|
1405
1900
|
id: event.id,
|
|
@@ -1459,6 +1954,13 @@ function createServer(options) {
|
|
|
1459
1954
|
}
|
|
1460
1955
|
};
|
|
1461
1956
|
streamActivities.set(stream, activity);
|
|
1957
|
+
bufferedEvents.set(stream, () => {
|
|
1958
|
+
const event = closed || !inner ? void 0 : takeBuffered(inner);
|
|
1959
|
+
if (!event) return void 0;
|
|
1960
|
+
if (!isSerializedEvent(event)) return toSessionEvent(id, event);
|
|
1961
|
+
event.sessionId = id;
|
|
1962
|
+
return event;
|
|
1963
|
+
});
|
|
1462
1964
|
return stream;
|
|
1463
1965
|
} };
|
|
1464
1966
|
};
|
|
@@ -1514,6 +2016,7 @@ function createServer(options) {
|
|
|
1514
2016
|
const ns = nsId(id);
|
|
1515
2017
|
let closed = false;
|
|
1516
2018
|
let events;
|
|
2019
|
+
let pendingEvent = null;
|
|
1517
2020
|
let unsubscribe;
|
|
1518
2021
|
let sleeper = null;
|
|
1519
2022
|
let wakeQueue = null;
|
|
@@ -1655,7 +2158,6 @@ function createServer(options) {
|
|
|
1655
2158
|
}
|
|
1656
2159
|
return null;
|
|
1657
2160
|
};
|
|
1658
|
-
let pendingEvent = null;
|
|
1659
2161
|
try {
|
|
1660
2162
|
unsubscribe = api.subscribe?.(ns, enqueue);
|
|
1661
2163
|
events = store.stream(ns, { startAfter })[Symbol.asyncIterator]();
|
|
@@ -1745,18 +2247,144 @@ function createServer(options) {
|
|
|
1745
2247
|
}
|
|
1746
2248
|
};
|
|
1747
2249
|
streamActivities.set(stream, activity);
|
|
2250
|
+
bufferedEvents.set(stream, () => {
|
|
2251
|
+
if (closed || !events || pendingEvent) return void 0;
|
|
2252
|
+
const event = takeBuffered(events);
|
|
2253
|
+
if (!event) return void 0;
|
|
2254
|
+
if (!isSerializedEvent(event)) return toSessionEvent(id, event);
|
|
2255
|
+
event.sessionId = id;
|
|
2256
|
+
return event;
|
|
2257
|
+
});
|
|
1748
2258
|
return stream;
|
|
1749
2259
|
} };
|
|
1750
2260
|
};
|
|
1751
|
-
const
|
|
2261
|
+
const reclaimAll = async (store, ns) => {
|
|
2262
|
+
for (let stalls = 0;;) {
|
|
2263
|
+
let batch;
|
|
2264
|
+
try {
|
|
2265
|
+
batch = await store.reclaim(ns);
|
|
2266
|
+
} catch (err) {
|
|
2267
|
+
throw asStoreUnavailable(err);
|
|
2268
|
+
}
|
|
2269
|
+
if (batch.done) return;
|
|
2270
|
+
if (batch.deleted > 0) {
|
|
2271
|
+
stalls = 0;
|
|
2272
|
+
continue;
|
|
2273
|
+
}
|
|
2274
|
+
stalls += 1;
|
|
2275
|
+
if (stalls === RECLAIM_STALL_LIMIT) throw new A2Error("STORE_UNAVAILABLE", `deleting the cleared history of session '${stripNs(ns)}' made no progress`);
|
|
2276
|
+
await defaultSleep(RECLAIM_STALL_MS).promise;
|
|
2277
|
+
}
|
|
2278
|
+
};
|
|
2279
|
+
const clearSession = async (sessionId, clearOptions, attempt) => {
|
|
2280
|
+
const ifLastIndex = clearOptions?.ifLastIndex;
|
|
2281
|
+
if (ifLastIndex !== void 0) assertClearIndex(ifLastIndex);
|
|
2282
|
+
const store = await resolveStore();
|
|
2283
|
+
if (!store.clear || !store.reclaim) throw new TypeError("session.clear requires a store that supports clearing");
|
|
2284
|
+
const ns = nsId(sessionId);
|
|
2285
|
+
let result;
|
|
2286
|
+
try {
|
|
2287
|
+
result = await store.clear({
|
|
2288
|
+
sessionId: ns,
|
|
2289
|
+
...ifLastIndex === void 0 ? {} : { ifLastIndex },
|
|
2290
|
+
...attempt ? { cause: attempt.cause } : {}
|
|
2291
|
+
});
|
|
2292
|
+
} catch (err) {
|
|
2293
|
+
throw asStoreUnavailable(err);
|
|
2294
|
+
}
|
|
2295
|
+
if (result.cleared) {
|
|
2296
|
+
const cachePrefix = stateCacheKey(sessionId, "");
|
|
2297
|
+
stateCache.deleteWhere((key) => key.startsWith(cachePrefix));
|
|
2298
|
+
}
|
|
2299
|
+
const deletion = reclaimAll(store, ns);
|
|
2300
|
+
if (attempt) {
|
|
2301
|
+
if (attempt.cause.index <= result.clearIndex) attempt.end();
|
|
2302
|
+
const kept = deletion.catch(() => void 0);
|
|
2303
|
+
track(kept);
|
|
2304
|
+
platformWaitUntil(kept);
|
|
2305
|
+
}
|
|
2306
|
+
await deletion;
|
|
2307
|
+
return {
|
|
2308
|
+
cleared: result.cleared,
|
|
2309
|
+
index: result.index
|
|
2310
|
+
};
|
|
2311
|
+
};
|
|
2312
|
+
const redrivingStore = async (method) => {
|
|
2313
|
+
const store = await resolveStore();
|
|
2314
|
+
if (!store.deadLetters || !store.redrive) throw new TypeError(`session.${method} requires a store that supports redriving dead letters`);
|
|
2315
|
+
return store;
|
|
2316
|
+
};
|
|
2317
|
+
const listDeadLetters = async (sessionId) => {
|
|
2318
|
+
const store = await redrivingStore("deadLetters");
|
|
2319
|
+
let rows;
|
|
2320
|
+
try {
|
|
2321
|
+
rows = await store.deadLetters(nsId(sessionId));
|
|
2322
|
+
} catch (err) {
|
|
2323
|
+
throw asStoreUnavailable(err);
|
|
2324
|
+
}
|
|
2325
|
+
return rows.flatMap((row) => row.failedAt === null ? [] : [{
|
|
2326
|
+
...toPublic(row),
|
|
2327
|
+
lane: row.lane,
|
|
2328
|
+
attemptCount: row.attemptCount,
|
|
2329
|
+
failureCount: row.failureCount,
|
|
2330
|
+
lastError: row.lastError,
|
|
2331
|
+
failedAt: row.failedAt
|
|
2332
|
+
}]);
|
|
2333
|
+
};
|
|
2334
|
+
const redriveEvent = async (sessionId, index, redriveOptions) => {
|
|
2335
|
+
assertRedriveIndex(index);
|
|
2336
|
+
const action = redriveOptions?.action;
|
|
2337
|
+
if (action !== "retry" && action !== "skip") throw new TypeError("redrive requires action 'retry' or 'skip'");
|
|
2338
|
+
return telemetry.span("a2.redrive", {
|
|
2339
|
+
"a2.contract": name,
|
|
2340
|
+
"a2.session_id": sessionId,
|
|
2341
|
+
"a2.redrive.index": index,
|
|
2342
|
+
"a2.redrive.action": action
|
|
2343
|
+
}, async (span) => {
|
|
2344
|
+
const store = await redrivingStore("redrive");
|
|
2345
|
+
let result;
|
|
2346
|
+
try {
|
|
2347
|
+
result = await store.redrive({
|
|
2348
|
+
sessionId: nsId(sessionId),
|
|
2349
|
+
index,
|
|
2350
|
+
action
|
|
2351
|
+
});
|
|
2352
|
+
} catch (err) {
|
|
2353
|
+
throw asStoreUnavailable(err);
|
|
2354
|
+
}
|
|
2355
|
+
span.setAttribute("a2.redrive.redriven", result.redriven);
|
|
2356
|
+
if (result.hasPending) {
|
|
2357
|
+
const watchdogAt = scheduler ? schedulerSlot(nextClaimWindow().watchdogAtMs) : void 0;
|
|
2358
|
+
const arm = watchdogAt === void 0 ? null : startSchedulerArm(sessionId, watchdogAt);
|
|
2359
|
+
scheduleDrain(sessionId, watchdogAt);
|
|
2360
|
+
if (arm) try {
|
|
2361
|
+
await joinSchedulerArm(arm);
|
|
2362
|
+
} catch (err) {
|
|
2363
|
+
span.setAttribute("a2.redrive.armed", false);
|
|
2364
|
+
span.recordError(err);
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
return { redriven: result.redriven };
|
|
2368
|
+
});
|
|
2369
|
+
};
|
|
2370
|
+
const makeSession = (id, append, trigger, attempt) => ({
|
|
1752
2371
|
id,
|
|
1753
2372
|
append,
|
|
1754
2373
|
schedule: makeSchedule(id, trigger),
|
|
1755
|
-
|
|
1756
|
-
|
|
2374
|
+
clear: (clearOptions) => clearSession(id, clearOptions, attempt),
|
|
2375
|
+
history: async (bounds) => (await readHistory(id, bounds)).filter((row) => !isSessionCleared(row)).map(toPublic),
|
|
2376
|
+
state: (reducer = serverContract.reducer, stateOptions) => {
|
|
2377
|
+
if (!reducer) throw new A2Error("INVALID_PAYLOAD", "contract has no public reducer");
|
|
1757
2378
|
const through = stateOptions?.through ?? trigger?.index;
|
|
1758
2379
|
if (through !== void 0 && through !== "latest") assertStateIndex(through);
|
|
1759
|
-
|
|
2380
|
+
const bounds = {};
|
|
2381
|
+
if (through !== void 0 && through !== "latest") {
|
|
2382
|
+
bounds.throughIndex = through;
|
|
2383
|
+
if (trigger) bounds.pinEventIndex = trigger.index;
|
|
2384
|
+
if (through === trigger?.index && !attempt?.signal.aborted) bounds.throughEventId = trigger.id;
|
|
2385
|
+
}
|
|
2386
|
+
if (trigger) bounds.triggerIndex = trigger.index;
|
|
2387
|
+
return readState(id, reducer, bounds);
|
|
1760
2388
|
},
|
|
1761
2389
|
stream: (opts) => {
|
|
1762
2390
|
const startAfter = opts?.startAfter ?? 0;
|
|
@@ -1768,7 +2396,7 @@ function createServer(options) {
|
|
|
1768
2396
|
presence: () => readPresenceMap(id)
|
|
1769
2397
|
} : {}
|
|
1770
2398
|
});
|
|
1771
|
-
const makeCtx = (trigger, attempt, signal) => {
|
|
2399
|
+
const makeCtx = (trigger, attempt, signal, endAttempt) => {
|
|
1772
2400
|
const publicSessionId = stripNs(trigger.sessionId);
|
|
1773
2401
|
const append = async (appendName, ...events) => {
|
|
1774
2402
|
assertAppendName(appendName);
|
|
@@ -1786,12 +2414,21 @@ function createServer(options) {
|
|
|
1786
2414
|
attempt
|
|
1787
2415
|
}, generatedIds);
|
|
1788
2416
|
};
|
|
1789
|
-
|
|
2417
|
+
const ctx = {
|
|
1790
2418
|
event: toPublic(trigger),
|
|
1791
2419
|
attempt,
|
|
1792
|
-
session: makeSession(publicSessionId, append, trigger
|
|
2420
|
+
session: makeSession(publicSessionId, append, trigger, {
|
|
2421
|
+
cause: {
|
|
2422
|
+
index: trigger.index,
|
|
2423
|
+
attempt
|
|
2424
|
+
},
|
|
2425
|
+
end: endAttempt,
|
|
2426
|
+
signal
|
|
2427
|
+
}),
|
|
1793
2428
|
signal
|
|
1794
2429
|
};
|
|
2430
|
+
if (attempt > 1 && trigger.lastFailedAttempt === attempt - 1) failedPreviousAttempt.add(ctx);
|
|
2431
|
+
return ctx;
|
|
1795
2432
|
};
|
|
1796
2433
|
const abortHubs = /* @__PURE__ */ new Map();
|
|
1797
2434
|
const createAbortHub = (store, ns, startAfter) => {
|
|
@@ -1988,12 +2625,7 @@ function createServer(options) {
|
|
|
1988
2625
|
for (const index of result.superseded) {
|
|
1989
2626
|
const entry = active.get(index);
|
|
1990
2627
|
if (!entry) continue;
|
|
1991
|
-
entry
|
|
1992
|
-
entry.fence({
|
|
1993
|
-
status: "superseded",
|
|
1994
|
-
opensLane: false,
|
|
1995
|
-
appendsEvents: false
|
|
1996
|
-
});
|
|
2628
|
+
endSuperseded(entry, `a newer attempt owns event ${index} in session '${sessionId}'`);
|
|
1997
2629
|
}
|
|
1998
2630
|
if (window.deadlineCapped) stopRenewing = true;
|
|
1999
2631
|
scheduleLapse();
|
|
@@ -2007,7 +2639,7 @@ function createServer(options) {
|
|
|
2007
2639
|
renewal = tracked;
|
|
2008
2640
|
return tracked;
|
|
2009
2641
|
};
|
|
2010
|
-
const runEvent = async (event, supersedeSignal) => {
|
|
2642
|
+
const runEvent = async (event, supersedeSignal, endAttempt) => {
|
|
2011
2643
|
const registration = handlers.get(event.type);
|
|
2012
2644
|
return telemetry.span("a2.event", {
|
|
2013
2645
|
"a2.contract": name,
|
|
@@ -2027,7 +2659,7 @@ function createServer(options) {
|
|
|
2027
2659
|
subscription = await subscribeAbort(store, ns, event, registration.abortOn, event.attemptCount);
|
|
2028
2660
|
handlerSignal = AbortSignal.any([supersedeSignal, subscription.signal]);
|
|
2029
2661
|
}
|
|
2030
|
-
const operation = registration?.handler(makeCtx(event, event.attemptCount, handlerSignal));
|
|
2662
|
+
const operation = registration?.handler(makeCtx(event, event.attemptCount, handlerSignal, endAttempt));
|
|
2031
2663
|
const result = operation ? await (subscription ? subscription.monitor(operation) : operation) : void 0;
|
|
2032
2664
|
if (subscription) {
|
|
2033
2665
|
await subscription.close();
|
|
@@ -2044,12 +2676,22 @@ function createServer(options) {
|
|
|
2044
2676
|
};
|
|
2045
2677
|
}
|
|
2046
2678
|
eventSpan.recordError(err);
|
|
2047
|
-
const failure = await
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2679
|
+
const failure = await telemetry.span("a2.commit", {
|
|
2680
|
+
"a2.contract": name,
|
|
2681
|
+
"a2.session_id": sessionId,
|
|
2682
|
+
"a2.commit.operation": "fail",
|
|
2683
|
+
"a2.commit.index": event.index,
|
|
2684
|
+
"a2.commit.attempt": event.attemptCount
|
|
2685
|
+
}, async (commitSpan) => {
|
|
2686
|
+
const result = await store.failAttempt({
|
|
2687
|
+
sessionId: ns,
|
|
2688
|
+
index: event.index,
|
|
2689
|
+
attempt: event.attemptCount,
|
|
2690
|
+
error: describeError(err),
|
|
2691
|
+
maxFailures: err instanceof NonRetriableError ? 1 : MAX_FAILURES
|
|
2692
|
+
});
|
|
2693
|
+
commitSpan.setAttribute("a2.commit.outcome", result.outcome);
|
|
2694
|
+
return result;
|
|
2053
2695
|
});
|
|
2054
2696
|
eventSpan.setAttribute("a2.event.outcome", failure.outcome);
|
|
2055
2697
|
return {
|
|
@@ -2061,11 +2703,21 @@ function createServer(options) {
|
|
|
2061
2703
|
await subscription?.close();
|
|
2062
2704
|
}
|
|
2063
2705
|
await renewal;
|
|
2064
|
-
const completion = await
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2706
|
+
const completion = await telemetry.span("a2.commit", {
|
|
2707
|
+
"a2.contract": name,
|
|
2708
|
+
"a2.session_id": sessionId,
|
|
2709
|
+
"a2.commit.operation": "complete",
|
|
2710
|
+
"a2.commit.index": event.index,
|
|
2711
|
+
"a2.commit.attempt": event.attemptCount
|
|
2712
|
+
}, async (commitSpan) => {
|
|
2713
|
+
const result = await store.completeAttempt({
|
|
2714
|
+
sessionId: ns,
|
|
2715
|
+
index: event.index,
|
|
2716
|
+
attempt: event.attemptCount,
|
|
2717
|
+
events: returned
|
|
2718
|
+
});
|
|
2719
|
+
commitSpan.setAttribute("a2.commit.outcome", result.outcome);
|
|
2720
|
+
return result;
|
|
2069
2721
|
});
|
|
2070
2722
|
eventSpan.setAttribute("a2.event.outcome", completion.outcome === "completed" ? "processed" : completion.outcome);
|
|
2071
2723
|
if (completion.outcome === "superseded") return {
|
|
@@ -2084,21 +2736,18 @@ function createServer(options) {
|
|
|
2084
2736
|
let claimAgain = false;
|
|
2085
2737
|
const start = (event, expiresAtMs) => {
|
|
2086
2738
|
const previous = active.get(event.index);
|
|
2087
|
-
if (previous) {
|
|
2088
|
-
previous.supersede.abort(new A2Error("SUPERSEDED_ATTEMPT", `a newer attempt owns event ${event.index} in session '${sessionId}'`));
|
|
2089
|
-
previous.fence({
|
|
2090
|
-
status: "superseded",
|
|
2091
|
-
opensLane: false,
|
|
2092
|
-
appendsEvents: false
|
|
2093
|
-
});
|
|
2094
|
-
}
|
|
2739
|
+
if (previous) endSuperseded(previous, `a newer attempt owns event ${event.index} in session '${sessionId}'`);
|
|
2095
2740
|
const supersede = new AbortController();
|
|
2096
2741
|
let fence;
|
|
2097
2742
|
const fenced = new Promise((resolve) => {
|
|
2098
2743
|
fence = resolve;
|
|
2099
2744
|
});
|
|
2745
|
+
const endAttempt = () => endSuperseded({
|
|
2746
|
+
supersede,
|
|
2747
|
+
fence
|
|
2748
|
+
}, `event ${event.index} in session '${sessionId}' was cleared`);
|
|
2100
2749
|
let entry;
|
|
2101
|
-
const execution = Promise.race([runEvent(event, supersede.signal), fenced]).then((eventOutcome) => {
|
|
2750
|
+
const execution = Promise.race([runEvent(event, supersede.signal, endAttempt), fenced]).then((eventOutcome) => {
|
|
2102
2751
|
if (active.get(event.index) === entry) active.delete(event.index);
|
|
2103
2752
|
if (eventOutcome.status === "processed") processed += 1;
|
|
2104
2753
|
if (eventOutcome.status === "failed") {
|
|
@@ -2131,12 +2780,20 @@ function createServer(options) {
|
|
|
2131
2780
|
let claim;
|
|
2132
2781
|
if (shouldClaim) {
|
|
2133
2782
|
const window = nextClaimWindow();
|
|
2134
|
-
claim = await
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2783
|
+
claim = await telemetry.span("a2.claim", {
|
|
2784
|
+
"a2.contract": name,
|
|
2785
|
+
"a2.session_id": sessionId
|
|
2786
|
+
}, async (claimSpan) => {
|
|
2787
|
+
const result = await store.claimAvailable({
|
|
2788
|
+
sessionId: ns,
|
|
2789
|
+
holder,
|
|
2790
|
+
ttlMs: window.ttlMs,
|
|
2791
|
+
...window.deadlineCapped ? { expiresAtMs: window.expiresAtMs } : {},
|
|
2792
|
+
...excluded.size === 0 ? {} : { excludeIndexes: [...excluded] }
|
|
2793
|
+
});
|
|
2794
|
+
claimSpan.setAttribute("a2.claim.outcome", result.outcome);
|
|
2795
|
+
claimSpan.setAttribute("a2.claim.count", result.outcome === "claimed" ? result.events.length : 0);
|
|
2796
|
+
return result;
|
|
2140
2797
|
});
|
|
2141
2798
|
if (claim.outcome === "claimed") {
|
|
2142
2799
|
requestWatchdog(window.watchdogAtMs);
|
|
@@ -2195,21 +2852,34 @@ function createServer(options) {
|
|
|
2195
2852
|
...schedulerArm ? { schedulerArm } : {}
|
|
2196
2853
|
};
|
|
2197
2854
|
});
|
|
2198
|
-
const appendExternal = async (sessionId, mode, events, requireDurableInitialArm = false) => {
|
|
2199
|
-
return appendCore(sessionId, events, "external", mode, (_rows, watchdogDueAt) => scheduleDrain(sessionId, watchdogDueAt), void 0, void 0, requireDurableInitialArm);
|
|
2855
|
+
const appendExternal = async (sessionId, mode, events, requireDurableInitialArm = false, originIndex) => {
|
|
2856
|
+
return appendCore(sessionId, events, "external", mode, (_rows, watchdogDueAt) => scheduleDrain(sessionId, watchdogDueAt), void 0, void 0, requireDurableInitialArm, originIndex);
|
|
2200
2857
|
};
|
|
2201
2858
|
const session = (id) => {
|
|
2202
2859
|
assertSessionId(id);
|
|
2203
2860
|
const appendInline = async (...events) => appendExternal(id, "inline", events);
|
|
2204
2861
|
const appendDispatch = async (...events) => appendExternal(id, "dispatch", events);
|
|
2205
2862
|
const append = Object.assign(appendInline, { dispatch: appendDispatch });
|
|
2206
|
-
return makeSession(id, append, null)
|
|
2863
|
+
return Object.assign(makeSession(id, append, null), {
|
|
2864
|
+
deadLetters: () => listDeadLetters(id),
|
|
2865
|
+
redrive: (index, redriveOptions) => redriveEvent(id, index, redriveOptions)
|
|
2866
|
+
});
|
|
2867
|
+
};
|
|
2868
|
+
const historyPage = async (sessionId, bounds) => {
|
|
2869
|
+
assertSessionId(sessionId);
|
|
2870
|
+
const rows = await readHistory(sessionId, bounds);
|
|
2871
|
+
const marker = rows[0] && isSessionCleared(rows[0]) ? rows[0] : void 0;
|
|
2872
|
+
return {
|
|
2873
|
+
events: (marker ? rows.slice(1) : rows).map(toPublic),
|
|
2874
|
+
...marker ? { clearIndex: marker.index } : {}
|
|
2875
|
+
};
|
|
2207
2876
|
};
|
|
2208
2877
|
const self = {
|
|
2209
2878
|
contract: serverContract,
|
|
2210
2879
|
fetch: createServerFetch({
|
|
2211
2880
|
contract: serverContract,
|
|
2212
|
-
session
|
|
2881
|
+
session,
|
|
2882
|
+
historyPage
|
|
2213
2883
|
}),
|
|
2214
2884
|
session,
|
|
2215
2885
|
async drain(sessionId) {
|
|
@@ -2226,10 +2896,21 @@ function createServer(options) {
|
|
|
2226
2896
|
assertSessionId(sessionId);
|
|
2227
2897
|
return drainSession(sessionId, opts?.watchdogDueAt);
|
|
2228
2898
|
},
|
|
2229
|
-
schedulerAppend: async (sessionId, events) => {
|
|
2899
|
+
schedulerAppend: async (sessionId, events, originIndex) => {
|
|
2230
2900
|
if (!scheduler) throw new TypeError("scheduled append delivery requires a configured scheduler adapter");
|
|
2231
2901
|
assertSessionId(sessionId);
|
|
2232
|
-
|
|
2902
|
+
try {
|
|
2903
|
+
await appendExternal(sessionId, "inline", [...events], true, originIndex);
|
|
2904
|
+
} catch (error) {
|
|
2905
|
+
if (originIndex !== void 0 && error instanceof A2Error && error.code === "SESSION_CLEARED") return;
|
|
2906
|
+
throw error;
|
|
2907
|
+
}
|
|
2908
|
+
},
|
|
2909
|
+
historyWithMarkers: async (sessionId, gte) => (await readHistory(sessionId, { gte })).map(toPublic),
|
|
2910
|
+
schedulerClear: async (sessionId, ifLastIndex) => {
|
|
2911
|
+
assertSessionId(sessionId);
|
|
2912
|
+
assertClearIndex(ifLastIndex);
|
|
2913
|
+
await clearSession(sessionId, { ifLastIndex }, void 0);
|
|
2233
2914
|
}
|
|
2234
2915
|
});
|
|
2235
2916
|
return self;
|
|
@@ -2247,6 +2928,12 @@ function assertStreamIndex(value) {
|
|
|
2247
2928
|
function assertStateIndex(value) {
|
|
2248
2929
|
if (!Number.isSafeInteger(value) || value < 0) throw new TypeError("state.through must be a non-negative safe integer");
|
|
2249
2930
|
}
|
|
2931
|
+
function assertRedriveIndex(value) {
|
|
2932
|
+
if (!Number.isSafeInteger(value) || value < 1) throw new TypeError("redrive requires a positive safe-integer index");
|
|
2933
|
+
}
|
|
2934
|
+
function assertClearIndex(value) {
|
|
2935
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new TypeError("ifLastIndex must be a non-negative safe integer");
|
|
2936
|
+
}
|
|
2250
2937
|
function assertAppendName(name) {
|
|
2251
2938
|
if (typeof name !== "string" || name.length === 0) throw new TypeError("handler append name must be a non-empty string");
|
|
2252
2939
|
}
|
|
@@ -2328,14 +3015,12 @@ function isJsonValue(value, ancestors) {
|
|
|
2328
3015
|
ancestors.delete(value);
|
|
2329
3016
|
}
|
|
2330
3017
|
}
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
}
|
|
2337
|
-
}
|
|
3018
|
+
const measuredBytes = (reducer, state, index, snapshots) => JSON.stringify(snapshots.find((snapshot) => snapshot.index === index)?.state ?? encodeState({
|
|
3019
|
+
reducer,
|
|
3020
|
+
state
|
|
3021
|
+
}))?.length ?? 0;
|
|
3022
|
+
const stateCacheKey = (sessionId, reducerName) => `${sessionId}\u0000${reducerName}`;
|
|
2338
3023
|
//#endregion
|
|
2339
|
-
export {
|
|
3024
|
+
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 };
|
|
2340
3025
|
|
|
2341
|
-
//# sourceMappingURL=server-
|
|
3026
|
+
//# sourceMappingURL=server-Dw_k_7kO.js.map
|