experimental-a2 0.16.0 → 0.17.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 +360 -0
- package/dist/{actor-ohPC-81x.d.ts → actor-DBbe5xeE.d.ts} +21 -11
- package/dist/actor-DBbe5xeE.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-CrLYwNEx.js → ai-UrWfKeuv.js} +966 -541
- package/dist/ai-UrWfKeuv.js.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-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 +80 -36
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +466 -265
- package/dist/ai-server.js.map +1 -1
- package/dist/{ai-HJ9fHfYI.d.ts → ai-vpGtcaCB.d.ts} +115 -101
- package/dist/ai-vpGtcaCB.d.ts.map +1 -0
- package/dist/ai.d.ts +2 -2
- package/dist/ai.js +2 -2
- package/dist/{client-BYzHjkwU.js → client-COaiaDub.js} +447 -80
- package/dist/client-COaiaDub.js.map +1 -0
- package/dist/{client-CzyacQpJ.d.ts → client-qRnRUzBS.d.ts} +14 -13
- package/dist/client-qRnRUzBS.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-BrLso5x_.d.ts} +7 -3
- package/dist/initial-snapshot-BrLso5x_.d.ts.map +1 -0
- package/dist/{internal-Dq2qYxou.js → internal-CbVvEwad.js} +23 -2
- package/dist/internal-CbVvEwad.js.map +1 -0
- package/dist/otel.d.ts +1 -1
- package/dist/react-BbJ-g6Uu.d.ts +86 -0
- package/dist/react-BbJ-g6Uu.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-BYu6DT7h.d.ts} +45 -29
- package/dist/reducer-BYu6DT7h.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-DbD7IJVK.d.ts → server-CGM8WzRG.d.ts} +87 -26
- package/dist/server-CGM8WzRG.d.ts.map +1 -0
- package/dist/{server-BD5ckxZb.js → server-Rj64fNvX.js} +854 -202
- package/dist/server-Rj64fNvX.js.map +1 -0
- package/dist/server.d.ts +4 -4
- package/dist/server.js +2 -2
- package/dist/{errors-DCk6ch5n.js → session-clear-DGI4jTQ5.js} +17 -2
- package/dist/session-clear-DGI4jTQ5.js.map +1 -0
- package/dist/session-clear-Dshpuhff.d.ts +12 -0
- package/dist/session-clear-Dshpuhff.d.ts.map +1 -0
- package/dist/{store-DyZM6fS5.d.ts → store-Gpkq7WBo.d.ts} +44 -3
- package/dist/store-Gpkq7WBo.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 +182 -69
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-CmxUbV93.js → store-polling-4_y8lETm.js} +21 -4
- package/dist/store-polling-4_y8lETm.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 +399 -108
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-presence-polling-C7-XZyW9.js → store-presence-polling-Q1C-k2dd.js} +3 -3
- package/dist/{store-presence-polling-C7-XZyW9.js.map → store-presence-polling-Q1C-k2dd.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-tqWNnMRu.js +2374 -0
- package/dist/store-redis-notify-tqWNnMRu.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 +166 -32
- 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-cZH-cibj.d.ts} +2 -2
- package/dist/telemetry-cZH-cibj.d.ts.map +1 -0
- package/dist/{wire-BO5wWCb1.js → wire-DjfHOvdW.js} +132 -10
- package/dist/wire-DjfHOvdW.js.map +1 -0
- package/docs/01-quickstart.mdx +2 -2
- package/docs/actors/01-introduction.mdx +4 -0
- package/docs/actors/04-routes.mdx +14 -1
- package/docs/concepts/01-contracts.mdx +95 -0
- package/docs/concepts/04-state.mdx +3 -3
- package/docs/guides/01-timers.mdx +22 -14
- package/docs/guides/02-cancellation.mdx +1 -1
- package/docs/guides/03-react.mdx +268 -328
- package/docs/guides/04-local-first.mdx +32 -73
- package/docs/guides/05-production.mdx +40 -27
- package/docs/guides/06-ai-agents.mdx +680 -580
- package/docs/guides/09-presence.mdx +3 -7
- package/docs/guides/10-transports.mdx +5 -5
- package/docs/index.mdx +10 -18
- package/docs/reference/01-api.mdx +662 -195
- package/docs/reference/02-errors.mdx +2 -1
- package/examples/playground/DEPLOY.md +52 -6
- package/examples/playground/app/a0/[appId]/loading.tsx +16 -0
- package/examples/playground/app/a0/[appId]/page.tsx +48 -0
- package/examples/playground/app/a0/a0.css +818 -0
- package/examples/playground/app/a0/app-list.tsx +211 -0
- package/examples/playground/app/a0/app-view.tsx +55 -0
- package/examples/playground/app/a0/apps/[appId]/preview/route.ts +27 -0
- package/examples/playground/app/a0/apps/[appId]/route.ts +23 -0
- package/examples/playground/app/a0/apps.test.ts +116 -0
- package/examples/playground/app/a0/apps.ts +87 -0
- package/examples/playground/app/a0/chat.tsx +364 -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/layout.tsx +38 -0
- package/examples/playground/app/a0/list/route.ts +10 -0
- package/examples/playground/app/a0/model.test.ts +56 -0
- package/examples/playground/app/a0/model.ts +58 -0
- package/examples/playground/app/a0/page.tsx +14 -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 +69 -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 +158 -0
- package/examples/playground/app/a0/session.ts +47 -0
- package/examples/playground/app/a0/shell.tsx +150 -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 +98 -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 +15 -14
- package/examples/playground/app/agent/session.ts +5 -6
- package/examples/playground/app/agent/telemetry.test.ts +168 -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 +11 -11
- 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 +108 -0
- package/examples/playground/app/chat/server.ts +69 -73
- 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/package.json +10 -2
- package/examples/playground/scripts/a0-template.ts +77 -0
- package/examples/playground/vercel.json +18 -0
- package/package.json +3 -1
- 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 +2343 -0
- package/src/ai-app-state.ts +91 -0
- package/src/ai-client-state.ts +39 -2
- 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 +94 -55
- package/src/ai-control.ts +2 -0
- package/src/ai-coordinator.ts +23 -22
- package/src/ai-message-base.ts +209 -0
- package/src/ai-message-projection.ts +382 -58
- 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-server.browser.ts +2 -2
- package/src/ai-server.ts +20 -1951
- package/src/ai-stored-state.ts +48 -163
- package/src/ai.ts +343 -276
- 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 +45 -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 +92 -10
- 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 +957 -237
- 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 +220 -78
- package/src/store-polling.ts +18 -4
- package/src/store-postgres.ts +461 -114
- package/src/store-redis-core.ts +1260 -434
- package/src/store-redis-http.ts +18 -2
- package/src/store-redis-notify.ts +260 -85
- package/src/store-redis.ts +19 -9
- package/src/store-sqlite.ts +259 -45
- package/src/store.ts +46 -1
- package/src/stream-buffer.ts +43 -0
- package/src/telemetry.ts +13 -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-CrLYwNEx.js.map +0 -1
- package/dist/ai-HJ9fHfYI.d.ts.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/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
|
@@ -1,10 +1,50 @@
|
|
|
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 serverInternals, c as STATE_CACHE, g as nullProtoRecord, h as markSchedulerSendFailure, i as POLL_TIMINGS, l as STREAM_TIMINGS, o as RESERVED_PARTICIPANT_IDS, s as SOCKET_TIMINGS, t as DRAIN_TIMINGS, v as stateTailReaders } from "./internal-CbVvEwad.js";
|
|
5
|
+
import { i as streamActivities, r as createStreamActivity, t as defaultSleep } from "./store-polling-4_y8lETm.js";
|
|
6
|
+
import { i as takeBuffered, n as isSerializedEvent, r as parseSerializedEvent, t as bufferedEvents } from "./stream-buffer-Cx7AAPGq.js";
|
|
5
7
|
import { r as platformWaitUntil, t as invocationDeadlineMs } from "./platform-B4TnJtWu.js";
|
|
6
|
-
import {
|
|
8
|
+
import { A as stateSnapshotToWire, C as serializedEventToWireJson, D as socketFrameFor, E as socketEventsFor, O as socketSubscribedFor, S as presenceSnapshotToWire, T as socketErrorAckFor, b as presencePatchToWire, d as heldEventBatch, i as compactEventJson, j as wireVersion, k as socketUnsubscribedFor, m as isStreamEvent, n as SOCKET_PING_FRAME, o as errorStatus, r as asA2Error, s as errorToWire, t as EVENT_WRITE_CHARS, u as eventToWire, w as socketAckFor } from "./wire-DjfHOvdW.js";
|
|
7
9
|
import { t as retryableLazy } from "./retryable-lazy-DZWmHpii.js";
|
|
10
|
+
//#region src/bounded-cache.ts
|
|
11
|
+
function boundedCache({ maxEntries, maxBytes }) {
|
|
12
|
+
const entries = /* @__PURE__ */ new Map();
|
|
13
|
+
let total = 0;
|
|
14
|
+
const remove = (key) => {
|
|
15
|
+
const entry = entries.get(key);
|
|
16
|
+
if (!entry) return;
|
|
17
|
+
entries.delete(key);
|
|
18
|
+
total -= entry.bytes;
|
|
19
|
+
};
|
|
20
|
+
return {
|
|
21
|
+
get(key) {
|
|
22
|
+
const entry = entries.get(key);
|
|
23
|
+
if (!entry) return void 0;
|
|
24
|
+
entries.delete(key);
|
|
25
|
+
entries.set(key, entry);
|
|
26
|
+
return entry.value;
|
|
27
|
+
},
|
|
28
|
+
set(key, value, bytes) {
|
|
29
|
+
remove(key);
|
|
30
|
+
if (bytes > maxBytes / 4) return;
|
|
31
|
+
entries.set(key, {
|
|
32
|
+
value,
|
|
33
|
+
bytes
|
|
34
|
+
});
|
|
35
|
+
total += bytes;
|
|
36
|
+
for (const oldest of entries.keys()) {
|
|
37
|
+
if (entries.size <= maxEntries && total <= maxBytes) break;
|
|
38
|
+
remove(oldest);
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
delete: remove,
|
|
42
|
+
deleteWhere(match) {
|
|
43
|
+
for (const key of entries.keys()) if (match(key)) remove(key);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
8
48
|
//#region src/push-envelope.ts
|
|
9
49
|
/**
|
|
10
50
|
* The push envelope's trust boundary for `{ sessionId, events, presence? }`,
|
|
@@ -191,25 +231,43 @@ function socketShell(socket, release, onFrame, deadlineAt) {
|
|
|
191
231
|
track
|
|
192
232
|
};
|
|
193
233
|
}
|
|
194
|
-
const socketSubscription = (
|
|
195
|
-
target,
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
})
|
|
234
|
+
const socketSubscription = (options) => {
|
|
235
|
+
const { target, startAfter, presence, attempt } = options;
|
|
236
|
+
return {
|
|
237
|
+
target,
|
|
238
|
+
iterator: (presence ? target.stream({
|
|
239
|
+
startAfter,
|
|
240
|
+
presence: true
|
|
241
|
+
}) : target.stream({ startAfter }))[Symbol.asyncIterator](),
|
|
242
|
+
stopped: false,
|
|
243
|
+
...attempt === void 0 ? {} : { attempt }
|
|
244
|
+
};
|
|
245
|
+
};
|
|
202
246
|
const stopSubscription = (subscription) => {
|
|
203
247
|
subscription.stopped = true;
|
|
204
248
|
subscription.iterator.return?.()?.catch(() => {});
|
|
205
249
|
};
|
|
206
|
-
|
|
250
|
+
/**
|
|
251
|
+
* Sends a subscription's items until it ends. With `batch`, an event and
|
|
252
|
+
* the events its iterator already holds go out as one frame.
|
|
253
|
+
*/
|
|
254
|
+
const pumpSubscription = async (options) => {
|
|
255
|
+
const { shell, subscription, frameFor, batch } = options;
|
|
207
256
|
for (;;) {
|
|
208
257
|
while (!shell.torn() && !subscription.stopped && shell.buffered() > SOCKET_TIMINGS.highWaterMarkBytes) await new Promise((wake) => setTimeout(wake, SOCKET_TIMINGS.resumePollMs));
|
|
209
258
|
if (shell.torn() || subscription.stopped) return;
|
|
210
259
|
const { value, done } = await subscription.iterator.next();
|
|
211
260
|
if (shell.torn() || subscription.stopped || done) return;
|
|
212
|
-
|
|
261
|
+
if (!batch || !isStreamEvent(value)) {
|
|
262
|
+
shell.send(frameFor(value));
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
const { events } = heldEventBatch({
|
|
266
|
+
first: value,
|
|
267
|
+
iterator: subscription.iterator,
|
|
268
|
+
encode: compactEventJson
|
|
269
|
+
});
|
|
270
|
+
shell.send(batch(events));
|
|
213
271
|
}
|
|
214
272
|
};
|
|
215
273
|
const handleSocketPush = async (shell, target, sessionId, frame, req, ackSessionId, gatePush, activate) => {
|
|
@@ -255,7 +313,11 @@ const applySocketPresence = async (shell, target, sessionId, frame, gatePush) =>
|
|
|
255
313
|
};
|
|
256
314
|
function sessionSocket(sessionId, session, socket, options) {
|
|
257
315
|
const target = session;
|
|
258
|
-
const subscription = socketSubscription(
|
|
316
|
+
const subscription = socketSubscription({
|
|
317
|
+
target,
|
|
318
|
+
startAfter: options?.startAfter ?? 0,
|
|
319
|
+
presence: options?.presence === true
|
|
320
|
+
});
|
|
259
321
|
const handleFrame = (frame) => {
|
|
260
322
|
switch (frame["kind"]) {
|
|
261
323
|
case "push": {
|
|
@@ -286,20 +348,41 @@ function sessionSocket(sessionId, session, socket, options) {
|
|
|
286
348
|
}
|
|
287
349
|
};
|
|
288
350
|
const shell = socketShell(socket, () => stopSubscription(subscription), handleFrame, options?.deadline);
|
|
289
|
-
pumpSubscription(
|
|
351
|
+
pumpSubscription({
|
|
352
|
+
shell,
|
|
353
|
+
subscription,
|
|
354
|
+
frameFor: socketFrameFor,
|
|
355
|
+
...options?.wire === 2 ? { batch: (events) => socketEventsFor(events) } : {}
|
|
356
|
+
}).then(() => shell.shutdown(1e3), () => shell.shutdown(1011, "stream failed"));
|
|
290
357
|
}
|
|
291
358
|
const parseSubscribeEntries = (value) => {
|
|
292
359
|
if (!Array.isArray(value) || value.length === 0) return null;
|
|
293
360
|
const entries = [];
|
|
294
361
|
for (const entry of value) {
|
|
295
362
|
if (entry === null || typeof entry !== "object") return null;
|
|
296
|
-
const { id, index } = entry;
|
|
363
|
+
const { id, attempt, index, bootstrap, reducer } = entry;
|
|
297
364
|
if (typeof id !== "string" || id.length === 0) return null;
|
|
298
|
-
if (typeof
|
|
299
|
-
|
|
365
|
+
if (attempt !== void 0 && (typeof attempt !== "number" || !Number.isSafeInteger(attempt) || attempt < 0)) return null;
|
|
366
|
+
if (reducer !== void 0 && (typeof reducer !== "string" || reducer.length === 0)) return null;
|
|
367
|
+
const lane = {
|
|
300
368
|
id,
|
|
301
|
-
|
|
302
|
-
}
|
|
369
|
+
...attempt === void 0 ? {} : { attempt }
|
|
370
|
+
};
|
|
371
|
+
if (bootstrap === true) {
|
|
372
|
+
if (index !== void 0 || typeof reducer !== "string") return null;
|
|
373
|
+
entries.push({
|
|
374
|
+
...lane,
|
|
375
|
+
bootstrap: true,
|
|
376
|
+
reducer
|
|
377
|
+
});
|
|
378
|
+
} else {
|
|
379
|
+
if (bootstrap !== void 0 || typeof index !== "number" || !Number.isSafeInteger(index) || index < 0) return null;
|
|
380
|
+
entries.push({
|
|
381
|
+
...lane,
|
|
382
|
+
index,
|
|
383
|
+
...reducer === void 0 ? {} : { reducer }
|
|
384
|
+
});
|
|
385
|
+
}
|
|
303
386
|
}
|
|
304
387
|
return entries;
|
|
305
388
|
};
|
|
@@ -322,18 +405,34 @@ function sessionsSocket(resolve, socket, options) {
|
|
|
322
405
|
if (subscriptions.get(id) === subscription) subscriptions.delete(id);
|
|
323
406
|
};
|
|
324
407
|
const runSubscription = async (id, subscription) => {
|
|
408
|
+
const { attempt } = subscription;
|
|
325
409
|
try {
|
|
326
|
-
await pumpSubscription(
|
|
410
|
+
await pumpSubscription({
|
|
411
|
+
shell,
|
|
412
|
+
subscription,
|
|
413
|
+
frameFor: (item) => socketFrameFor(item, id),
|
|
414
|
+
...options?.wire === 2 ? { batch: (events) => socketEventsFor(events, id) } : {}
|
|
415
|
+
});
|
|
327
416
|
if (shell.torn() || subscription.stopped) return;
|
|
328
417
|
retire(id, subscription);
|
|
329
|
-
shell.send(socketUnsubscribedFor(
|
|
418
|
+
shell.send(socketUnsubscribedFor({
|
|
419
|
+
sessionId: id,
|
|
420
|
+
attempt
|
|
421
|
+
}));
|
|
330
422
|
} catch (error) {
|
|
331
423
|
if (shell.torn() || subscription.stopped) return;
|
|
332
424
|
retire(id, subscription);
|
|
333
|
-
shell.send(socketUnsubscribedFor(
|
|
425
|
+
shell.send(socketUnsubscribedFor({
|
|
426
|
+
sessionId: id,
|
|
427
|
+
attempt,
|
|
428
|
+
reason: "stream failed",
|
|
429
|
+
error: asA2Error(error)
|
|
430
|
+
}));
|
|
334
431
|
}
|
|
335
432
|
};
|
|
336
|
-
const startSubscription = async (
|
|
433
|
+
const startSubscription = async (entry) => {
|
|
434
|
+
const { id, attempt } = entry;
|
|
435
|
+
const index = entry.index ?? 0;
|
|
337
436
|
const existing = subscriptions.get(id);
|
|
338
437
|
if (existing) {
|
|
339
438
|
stopSubscription(existing);
|
|
@@ -341,19 +440,37 @@ function sessionsSocket(resolve, socket, options) {
|
|
|
341
440
|
}
|
|
342
441
|
let session;
|
|
343
442
|
try {
|
|
344
|
-
session = await resolve(id, index);
|
|
443
|
+
session = await resolve(id, index, entry);
|
|
345
444
|
} catch (error) {
|
|
346
|
-
if (!shell.torn()) shell.send(socketUnsubscribedFor(
|
|
445
|
+
if (!shell.torn()) shell.send(socketUnsubscribedFor({
|
|
446
|
+
sessionId: id,
|
|
447
|
+
attempt,
|
|
448
|
+
reason: "stream failed",
|
|
449
|
+
error: asA2Error(error)
|
|
450
|
+
}));
|
|
347
451
|
return;
|
|
348
452
|
}
|
|
349
453
|
if (shell.torn()) return;
|
|
350
454
|
if (session === null) {
|
|
351
|
-
shell.send(socketUnsubscribedFor(
|
|
455
|
+
shell.send(socketUnsubscribedFor({
|
|
456
|
+
sessionId: id,
|
|
457
|
+
attempt,
|
|
458
|
+
reason: "subscribe rejected",
|
|
459
|
+
error: new A2Error("FORBIDDEN", "the subscription is not authorized")
|
|
460
|
+
}));
|
|
352
461
|
return;
|
|
353
462
|
}
|
|
354
|
-
const subscription = socketSubscription(
|
|
463
|
+
const subscription = socketSubscription({
|
|
464
|
+
target: session,
|
|
465
|
+
startAfter: index,
|
|
466
|
+
presence,
|
|
467
|
+
attempt
|
|
468
|
+
});
|
|
355
469
|
subscriptions.set(id, subscription);
|
|
356
|
-
shell.send(socketSubscribedFor(
|
|
470
|
+
shell.send(socketSubscribedFor({
|
|
471
|
+
sessionId: id,
|
|
472
|
+
attempt
|
|
473
|
+
}));
|
|
357
474
|
runSubscription(id, subscription);
|
|
358
475
|
};
|
|
359
476
|
let controlTail = Promise.resolve();
|
|
@@ -384,7 +501,7 @@ function sessionsSocket(resolve, socket, options) {
|
|
|
384
501
|
shell.malformedFrame("malformed subscribe frame");
|
|
385
502
|
return;
|
|
386
503
|
}
|
|
387
|
-
for (const entry of entries) control(() => startSubscription(entry
|
|
504
|
+
for (const entry of entries) control(() => startSubscription(entry));
|
|
388
505
|
return;
|
|
389
506
|
}
|
|
390
507
|
case "unsubscribe": {
|
|
@@ -449,7 +566,10 @@ function sessionsSocket(resolve, socket, options) {
|
|
|
449
566
|
*/
|
|
450
567
|
/**
|
|
451
568
|
* 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.
|
|
569
|
+
* frame — `id:` carries the event-log index, `data:` the JSON event.
|
|
570
|
+
* Frames for events the iterable already holds are written together. With
|
|
571
|
+
* `wire: 2` those events share one frame instead: `data:` is a JSON array of
|
|
572
|
+
* `CompactWireEvent`s and `id:` the last one's index. A
|
|
453
573
|
* `stream({ presence: true })` iterable also yields presence items,
|
|
454
574
|
* which ride as named frames old clients skip: `event:
|
|
455
575
|
* presence-snapshot` first (the map, ISO dates), then `event: presence`
|
|
@@ -463,7 +583,8 @@ function sessionsSocket(resolve, socket, options) {
|
|
|
463
583
|
* closes cleanly one second before it so the client reconnects without
|
|
464
584
|
* a platform-timeout failure.
|
|
465
585
|
*/
|
|
466
|
-
function sseResponse(iterable) {
|
|
586
|
+
function sseResponse(iterable, options = {}) {
|
|
587
|
+
const write = options.wire === 2 ? batchedFrames : eventFrames;
|
|
467
588
|
const iterator = iterable[Symbol.asyncIterator]();
|
|
468
589
|
const encoder = new TextEncoder();
|
|
469
590
|
let heartbeat;
|
|
@@ -529,7 +650,7 @@ function sseResponse(iterable) {
|
|
|
529
650
|
controller.close();
|
|
530
651
|
return;
|
|
531
652
|
}
|
|
532
|
-
controller.enqueue(encoder.encode(
|
|
653
|
+
controller.enqueue(encoder.encode(write(value, iterator)));
|
|
533
654
|
} catch (error) {
|
|
534
655
|
if (closed) return;
|
|
535
656
|
closed = true;
|
|
@@ -554,12 +675,114 @@ function sseResponse(iterable) {
|
|
|
554
675
|
}
|
|
555
676
|
});
|
|
556
677
|
}
|
|
678
|
+
/** Version 1: one frame per event, then each event the iterator holds. */
|
|
679
|
+
function eventFrames(first, iterator) {
|
|
680
|
+
let frames = frameFor(first);
|
|
681
|
+
while (frames.length < EVENT_WRITE_CHARS) {
|
|
682
|
+
const event = takeBuffered(iterator);
|
|
683
|
+
if (!event) break;
|
|
684
|
+
frames += frameFor(event);
|
|
685
|
+
}
|
|
686
|
+
return frames;
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Version 2: events this write carries share one frame, a JSON array whose
|
|
690
|
+
* `id:` is its last index. Held events are always events, so only `first`
|
|
691
|
+
* can be a snapshot or presence frame.
|
|
692
|
+
*/
|
|
693
|
+
function batchedFrames(first, iterator) {
|
|
694
|
+
const lead = isStreamEvent(first) ? "" : frameFor(first);
|
|
695
|
+
const head = isStreamEvent(first) ? first : takeBuffered(iterator);
|
|
696
|
+
if (!head) return lead;
|
|
697
|
+
const { events, last } = heldEventBatch({
|
|
698
|
+
first: head,
|
|
699
|
+
iterator,
|
|
700
|
+
encode: compactJsonLine
|
|
701
|
+
});
|
|
702
|
+
return `${lead}id: ${last.index}\ndata: [${events.join(",")}]\n\n`;
|
|
703
|
+
}
|
|
704
|
+
const compactJsonLine = (event) => isSerializedEvent(event) && /[\r\n]/.test(event.payloadJson) ? compactEventJson(parseSerializedEvent(event)) : compactEventJson(event);
|
|
557
705
|
function frameFor(item) {
|
|
706
|
+
if ("kind" in item) return `event: a2-snapshot\ndata: ${JSON.stringify(stateSnapshotToWire(item))}\n\n`;
|
|
558
707
|
if ("snapshot" in item) return `event: presence-snapshot\ndata: ${JSON.stringify(presenceSnapshotToWire(item))}\n\n`;
|
|
559
708
|
if ("participant" in item) return `event: presence\ndata: ${JSON.stringify(presencePatchToWire(item))}\n\n`;
|
|
709
|
+
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
710
|
return `id: ${item.index}\ndata: ${JSON.stringify(eventToWire(item))}\n\n`;
|
|
561
711
|
}
|
|
562
712
|
//#endregion
|
|
713
|
+
//#region src/bootstrap-stream.ts
|
|
714
|
+
function bootstrapStream(options) {
|
|
715
|
+
return { [Symbol.asyncIterator]() {
|
|
716
|
+
let closed = false;
|
|
717
|
+
let index;
|
|
718
|
+
let source;
|
|
719
|
+
let stopActivity;
|
|
720
|
+
const abort = new AbortController();
|
|
721
|
+
const activity = createStreamActivity(() => {
|
|
722
|
+
if (source) streamActivities.get(source)?.activate();
|
|
723
|
+
});
|
|
724
|
+
const iterator = {
|
|
725
|
+
async next() {
|
|
726
|
+
if (closed) return {
|
|
727
|
+
done: true,
|
|
728
|
+
value: void 0
|
|
729
|
+
};
|
|
730
|
+
if (index === void 0) {
|
|
731
|
+
let cancel;
|
|
732
|
+
const cancelled = new Promise((resolve) => {
|
|
733
|
+
cancel = () => resolve(void 0);
|
|
734
|
+
abort.signal.addEventListener("abort", cancel, { once: true });
|
|
735
|
+
});
|
|
736
|
+
try {
|
|
737
|
+
const snapshot = await Promise.race([options.snapshot(), cancelled]);
|
|
738
|
+
if (closed || snapshot === void 0) return {
|
|
739
|
+
done: true,
|
|
740
|
+
value: void 0
|
|
741
|
+
};
|
|
742
|
+
index = snapshot.index;
|
|
743
|
+
return {
|
|
744
|
+
done: false,
|
|
745
|
+
value: snapshot
|
|
746
|
+
};
|
|
747
|
+
} finally {
|
|
748
|
+
if (cancel) abort.signal.removeEventListener("abort", cancel);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
if (!source) {
|
|
752
|
+
source = options.stream(index)[Symbol.asyncIterator]();
|
|
753
|
+
const sourceActivity = streamActivities.get(source);
|
|
754
|
+
if (sourceActivity) {
|
|
755
|
+
activity.setIdle(sourceActivity.idle);
|
|
756
|
+
stopActivity = sourceActivity.subscribe(activity.setIdle);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
const result = await source.next();
|
|
760
|
+
return closed ? {
|
|
761
|
+
done: true,
|
|
762
|
+
value: void 0
|
|
763
|
+
} : result;
|
|
764
|
+
},
|
|
765
|
+
async return() {
|
|
766
|
+
if (closed) return {
|
|
767
|
+
done: true,
|
|
768
|
+
value: void 0
|
|
769
|
+
};
|
|
770
|
+
closed = true;
|
|
771
|
+
abort.abort();
|
|
772
|
+
stopActivity?.();
|
|
773
|
+
await source?.return?.();
|
|
774
|
+
return {
|
|
775
|
+
done: true,
|
|
776
|
+
value: void 0
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
streamActivities.set(iterator, activity);
|
|
781
|
+
bufferedEvents.set(iterator, () => closed || !source ? void 0 : takeBuffered(source));
|
|
782
|
+
return iterator;
|
|
783
|
+
} };
|
|
784
|
+
}
|
|
785
|
+
//#endregion
|
|
563
786
|
//#region src/server-fetch.ts
|
|
564
787
|
const serverFetchHooks = /* @__PURE__ */ new WeakMap();
|
|
565
788
|
function setServerFetchHooks(server, hooks) {
|
|
@@ -581,6 +804,23 @@ const parseResumeIndex = (raw) => {
|
|
|
581
804
|
if (!Number.isSafeInteger(index) || index < 0) throw new A2Error("INVALID_PAYLOAD", "stream index must be a non-negative safe integer");
|
|
582
805
|
return index;
|
|
583
806
|
};
|
|
807
|
+
const parseSubscription = (params) => {
|
|
808
|
+
const reducer = params.get("reducer");
|
|
809
|
+
const bootstrap = params.get("bootstrap");
|
|
810
|
+
const index = params.get("index");
|
|
811
|
+
if (reducer === "") throw new A2Error("INVALID_PAYLOAD", "empty reducer name");
|
|
812
|
+
if (bootstrap !== null) {
|
|
813
|
+
if (bootstrap !== "1" || index !== null || reducer === null) throw new A2Error("INVALID_PAYLOAD", "bootstrap requires a reducer and no index");
|
|
814
|
+
return {
|
|
815
|
+
bootstrap: true,
|
|
816
|
+
reducer
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
return {
|
|
820
|
+
index: parseResumeIndex(index),
|
|
821
|
+
...reducer === null ? {} : { reducer }
|
|
822
|
+
};
|
|
823
|
+
};
|
|
584
824
|
const errorResponse = (error) => {
|
|
585
825
|
const a2error = asA2Error(error);
|
|
586
826
|
return Response.json(errorToWire(a2error), { status: errorStatus(a2error.code) });
|
|
@@ -630,17 +870,54 @@ function createServerFetch(server) {
|
|
|
630
870
|
return asA2Error(error);
|
|
631
871
|
}
|
|
632
872
|
};
|
|
633
|
-
const
|
|
873
|
+
const selectedSession = (sessionId, selection) => {
|
|
874
|
+
const reducer = server.contract.reducer;
|
|
875
|
+
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");
|
|
876
|
+
const target = server.session(sessionId);
|
|
877
|
+
const streamTarget = target;
|
|
878
|
+
if (!selection.bootstrap) return streamTarget;
|
|
879
|
+
if (!reducer) throw new A2Error("INVALID_PAYLOAD", "contract has no public reducer");
|
|
880
|
+
return {
|
|
881
|
+
...streamTarget,
|
|
882
|
+
stream: (streamOptions) => bootstrapStream({
|
|
883
|
+
snapshot: async () => {
|
|
884
|
+
const initial = await target.state(reducer);
|
|
885
|
+
return {
|
|
886
|
+
kind: "snapshot",
|
|
887
|
+
sessionId,
|
|
888
|
+
name: reducer.name,
|
|
889
|
+
index: initial.index,
|
|
890
|
+
state: encodeState({
|
|
891
|
+
reducer,
|
|
892
|
+
state: initial.state
|
|
893
|
+
}),
|
|
894
|
+
...initial.clearIndex === void 0 ? {} : { clearIndex: initial.clearIndex }
|
|
895
|
+
};
|
|
896
|
+
},
|
|
897
|
+
stream: (index) => {
|
|
898
|
+
const bounds = {
|
|
899
|
+
startAfter: index,
|
|
900
|
+
presence: streamOptions.presence === true
|
|
901
|
+
};
|
|
902
|
+
return target.stream(bounds);
|
|
903
|
+
}
|
|
904
|
+
})
|
|
905
|
+
};
|
|
906
|
+
};
|
|
907
|
+
const attachMultiplexed = (wire) => (socket) => {
|
|
634
908
|
const deadline = invocationDeadlineMs();
|
|
635
|
-
sessionsSocket(async (sessionId, startAfter) => {
|
|
636
|
-
|
|
909
|
+
sessionsSocket(async (sessionId, startAfter, selection = { index: startAfter }) => {
|
|
910
|
+
const operation = {
|
|
637
911
|
type: "stream",
|
|
638
912
|
sessionId,
|
|
639
913
|
startAfter,
|
|
914
|
+
...selection.bootstrap ? { bootstrap: true } : {},
|
|
640
915
|
transport: "websocket"
|
|
641
|
-
}
|
|
916
|
+
};
|
|
917
|
+
return await authorized(operation) ? selectedSession(sessionId, selection) : null;
|
|
642
918
|
}, socket, {
|
|
643
919
|
presence,
|
|
920
|
+
wire,
|
|
644
921
|
...deadline === null ? {} : { deadline },
|
|
645
922
|
gatePush: gateSocketPush
|
|
646
923
|
});
|
|
@@ -649,23 +926,27 @@ function createServerFetch(server) {
|
|
|
649
926
|
if (request.method === "GET") {
|
|
650
927
|
if (request.headers.get("upgrade")?.toLowerCase() === "websocket") {
|
|
651
928
|
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
929
|
const { searchParams } = new URL(request.url);
|
|
930
|
+
const wire = wireVersion(searchParams.get("wire"));
|
|
931
|
+
if (options.multiplexWebSocket === true) return await options.upgradeWebSocket(attachMultiplexed(wire));
|
|
654
932
|
const sessionId = searchParams.get("sessionId");
|
|
655
933
|
if (sessionId === null || sessionId.length === 0) throw new A2Error("INVALID_PAYLOAD", "missing sessionId");
|
|
656
|
-
const
|
|
934
|
+
const selection = parseSubscription(searchParams);
|
|
935
|
+
const startAfter = selection.index ?? 0;
|
|
657
936
|
await authorize({
|
|
658
937
|
type: "stream",
|
|
659
938
|
sessionId,
|
|
660
939
|
startAfter,
|
|
940
|
+
...selection.bootstrap ? { bootstrap: true } : {},
|
|
661
941
|
transport: "websocket"
|
|
662
942
|
});
|
|
663
|
-
const target =
|
|
943
|
+
const target = selectedSession(sessionId, selection);
|
|
664
944
|
const attach = (socket) => {
|
|
665
945
|
const deadline = invocationDeadlineMs();
|
|
666
946
|
sessionSocket(sessionId, target, socket, {
|
|
667
947
|
startAfter,
|
|
668
948
|
presence,
|
|
949
|
+
wire,
|
|
669
950
|
...deadline === null ? {} : { deadline },
|
|
670
951
|
gatePush: gateSocketPush
|
|
671
952
|
});
|
|
@@ -685,22 +966,28 @@ function createServerFetch(server) {
|
|
|
685
966
|
...bounds,
|
|
686
967
|
transport: "http"
|
|
687
968
|
});
|
|
688
|
-
const events = await server.
|
|
689
|
-
const
|
|
690
|
-
|
|
969
|
+
const { events, clearIndex } = await server.historyPage(sessionId, bounds);
|
|
970
|
+
const first = Math.max(bounds.gte, (clearIndex ?? 0) + 1);
|
|
971
|
+
const covered = events.length === Math.max(0, bounds.lte - first + 1);
|
|
972
|
+
return Response.json(events.map(eventToWire), { headers: {
|
|
973
|
+
"a2-history-covered": String(covered),
|
|
974
|
+
...clearIndex === void 0 ? {} : { "a2-clear-index": String(clearIndex) }
|
|
975
|
+
} });
|
|
691
976
|
}
|
|
692
|
-
const
|
|
977
|
+
const selection = parseSubscription(searchParams);
|
|
978
|
+
const startAfter = selection.index ?? 0;
|
|
693
979
|
await authorize({
|
|
694
980
|
type: "stream",
|
|
695
981
|
sessionId,
|
|
696
982
|
startAfter,
|
|
983
|
+
...selection.bootstrap ? { bootstrap: true } : {},
|
|
697
984
|
transport: "http"
|
|
698
985
|
});
|
|
699
|
-
const target =
|
|
986
|
+
const target = selectedSession(sessionId, selection);
|
|
700
987
|
return sseResponse(presence ? target.stream({
|
|
701
988
|
startAfter,
|
|
702
989
|
presence: true
|
|
703
|
-
}) : target.stream({ startAfter }));
|
|
990
|
+
}) : target.stream({ startAfter }), { wire: wireVersion(searchParams.get("wire")) });
|
|
704
991
|
}
|
|
705
992
|
if (request.method === "POST") {
|
|
706
993
|
const body = await parsePushBody(request);
|
|
@@ -746,6 +1033,7 @@ const APPEND_NAMESPACE = "a2:handler-append:v2";
|
|
|
746
1033
|
const RETURN_NAMESPACE = "a2:handler-return:v1";
|
|
747
1034
|
const SCHEDULE_NAMESPACE = "a2:schedule:v1";
|
|
748
1035
|
const SCHEDULED_EVENT_NAMESPACE = "a2:scheduled-event:v1";
|
|
1036
|
+
const SCHEDULE_CLEAR_NAMESPACE = "a2:schedule-clear:v1";
|
|
749
1037
|
async function deterministicIdFromInput(input) {
|
|
750
1038
|
const digest = await crypto.subtle.digest("SHA-1", new TextEncoder().encode(input));
|
|
751
1039
|
const bytes = new Uint8Array(digest).slice(0, 16);
|
|
@@ -773,13 +1061,31 @@ async function deterministicReturnedEventId(triggerEventId, itemOrdinal) {
|
|
|
773
1061
|
itemOrdinal
|
|
774
1062
|
]);
|
|
775
1063
|
}
|
|
776
|
-
|
|
1064
|
+
/**
|
|
1065
|
+
* The id of a scheduled append. `clearIndex` is the lifetime the trigger was
|
|
1066
|
+
* claimed in; it joins the input only when positive, so a trigger id reused
|
|
1067
|
+
* after a clear schedules a distinct task while sessions never cleared keep
|
|
1068
|
+
* the ids they always had.
|
|
1069
|
+
*/
|
|
1070
|
+
async function deterministicScheduleId(contract, sessionId, triggerEventId, name, clearIndex = 0) {
|
|
777
1071
|
return deterministicIdFromInput(JSON.stringify([
|
|
778
1072
|
SCHEDULE_NAMESPACE,
|
|
779
1073
|
contract,
|
|
780
1074
|
sessionId,
|
|
781
1075
|
triggerEventId,
|
|
782
|
-
name
|
|
1076
|
+
name,
|
|
1077
|
+
...clearIndex > 0 ? [clearIndex] : []
|
|
1078
|
+
]));
|
|
1079
|
+
}
|
|
1080
|
+
async function deterministicScheduleClearId({ contract, sessionId, triggerEventId, clearIndex, name, ifLastIndex }) {
|
|
1081
|
+
return deterministicIdFromInput(JSON.stringify([
|
|
1082
|
+
SCHEDULE_CLEAR_NAMESPACE,
|
|
1083
|
+
contract,
|
|
1084
|
+
sessionId,
|
|
1085
|
+
triggerEventId,
|
|
1086
|
+
clearIndex,
|
|
1087
|
+
name,
|
|
1088
|
+
ifLastIndex
|
|
783
1089
|
]));
|
|
784
1090
|
}
|
|
785
1091
|
async function deterministicScheduledEventId(scheduleId, itemOrdinal) {
|
|
@@ -812,14 +1118,42 @@ const CONSOLE_TELEMETRY = { span: (name, attributes, fn) => fn({
|
|
|
812
1118
|
*/
|
|
813
1119
|
async function deliverSchedulerAppend(server, task) {
|
|
814
1120
|
if (server.contract.name !== task.contract) throw new TypeError(`a2 scheduler: append task for contract '${task.contract}' cannot be delivered to '${server.contract.name}'`);
|
|
1121
|
+
const { originIndex } = task;
|
|
1122
|
+
if (originIndex !== void 0 && (!Number.isSafeInteger(originIndex) || originIndex < 1)) throw new TypeError("a2 scheduler: append task originIndex must be a positive safe integer");
|
|
815
1123
|
const internals = serverInternals.get(server);
|
|
816
1124
|
if (!internals) throw new TypeError(`a2 scheduler: server for contract '${task.contract}' cannot receive scheduled appends`);
|
|
817
|
-
await internals.schedulerAppend(task.sessionId, task.events);
|
|
1125
|
+
await internals.schedulerAppend(task.sessionId, task.events, originIndex);
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Deliver one authenticated scheduler clear: a conditional
|
|
1129
|
+
* `clear({ ifLastIndex })` that resolves after its deletion finishes, so a
|
|
1130
|
+
* provider retry resumes an interrupted deletion. Custom scheduler adapters
|
|
1131
|
+
* call this after validating their transport envelope; application code uses
|
|
1132
|
+
* `session.schedule.clear`.
|
|
1133
|
+
*/
|
|
1134
|
+
async function deliverSchedulerClear(server, task) {
|
|
1135
|
+
if (server.contract.name !== task.contract) throw new TypeError(`a2 scheduler: clear task for contract '${task.contract}' cannot be delivered to '${server.contract.name}'`);
|
|
1136
|
+
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");
|
|
1137
|
+
const internals = serverInternals.get(server);
|
|
1138
|
+
if (!internals) throw new TypeError(`a2 scheduler: server for contract '${task.contract}' cannot receive scheduled clears`);
|
|
1139
|
+
await internals.schedulerClear(task.sessionId, task.ifLastIndex);
|
|
818
1140
|
}
|
|
819
1141
|
function handler(entry) {
|
|
820
1142
|
return typeof entry === "function" ? { handler: entry } : entry;
|
|
821
1143
|
}
|
|
822
1144
|
const MAX_FAILURES = 10;
|
|
1145
|
+
/** Abort a lost attempt's handler and release its drain slot. */
|
|
1146
|
+
function endSuperseded(entry, message) {
|
|
1147
|
+
entry.supersede.abort(new A2Error("SUPERSEDED_ATTEMPT", message));
|
|
1148
|
+
entry.fence({
|
|
1149
|
+
status: "superseded",
|
|
1150
|
+
opensLane: false,
|
|
1151
|
+
appendsEvents: false
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
/** Pause between clear-deletion batches that found only locked rows. */
|
|
1155
|
+
const RECLAIM_STALL_MS = 20;
|
|
1156
|
+
const RECLAIM_STALL_LIMIT = 50;
|
|
823
1157
|
/** Default: how long a presence value survives without a refreshing set. */
|
|
824
1158
|
const PRESENCE_TTL_MS = 6e4;
|
|
825
1159
|
function createDrainSignal() {
|
|
@@ -927,6 +1261,17 @@ function foldPresenceRows(rows) {
|
|
|
927
1261
|
};
|
|
928
1262
|
return map;
|
|
929
1263
|
}
|
|
1264
|
+
const addSnapshotWrite = (writes, index, state, pinEventIndex) => {
|
|
1265
|
+
let write = writes.get(index);
|
|
1266
|
+
if (!write) {
|
|
1267
|
+
write = {
|
|
1268
|
+
state: cloneInitial(state),
|
|
1269
|
+
pinEventIndexes: /* @__PURE__ */ new Set()
|
|
1270
|
+
};
|
|
1271
|
+
writes.set(index, write);
|
|
1272
|
+
}
|
|
1273
|
+
if (pinEventIndex !== void 0) write.pinEventIndexes.add(pinEventIndex);
|
|
1274
|
+
};
|
|
930
1275
|
/** Implement a contract: bind its vocabulary to storage and reactions. */
|
|
931
1276
|
function createServer(options) {
|
|
932
1277
|
const serverContract = options?.contract;
|
|
@@ -1107,7 +1452,7 @@ function createServer(options) {
|
|
|
1107
1452
|
* the drain it schedules is created in the append's active context —
|
|
1108
1453
|
* with a real tracer, causal work remains connected.
|
|
1109
1454
|
*/
|
|
1110
|
-
const appendCore = (sessionId, events, source, mode, onAppended, cause, generatedIds, requireDurableInitialArm = false) => telemetry.span("a2.append", {
|
|
1455
|
+
const appendCore = (sessionId, events, source, mode, onAppended, cause, generatedIds, requireDurableInitialArm = false, originIndex) => telemetry.span("a2.append", {
|
|
1111
1456
|
"a2.contract": name,
|
|
1112
1457
|
"a2.session_id": sessionId,
|
|
1113
1458
|
"a2.append.source": source,
|
|
@@ -1126,9 +1471,9 @@ function createServer(options) {
|
|
|
1126
1471
|
let rows;
|
|
1127
1472
|
let shouldHealSession;
|
|
1128
1473
|
try {
|
|
1129
|
-
const result = await store.append(nsId(sessionId), validated);
|
|
1474
|
+
const result = await store.append(nsId(sessionId), validated, originIndex === void 0 ? void 0 : { originIndex });
|
|
1130
1475
|
rows = result.events;
|
|
1131
|
-
shouldHealSession = result.hasPending;
|
|
1476
|
+
shouldHealSession = result.hasPending && !(source === "handler" && validated.every((event) => event.settled === true));
|
|
1132
1477
|
} catch (err) {
|
|
1133
1478
|
throw asStoreUnavailable(err);
|
|
1134
1479
|
}
|
|
@@ -1204,113 +1549,263 @@ function createServer(options) {
|
|
|
1204
1549
|
}
|
|
1205
1550
|
return plans;
|
|
1206
1551
|
};
|
|
1552
|
+
const persistSnapshots = ({ store, reducer, sessionId, snapshots }) => {
|
|
1553
|
+
const persistence = Promise.resolve().then(() => telemetry.span("a2.snapshot", {
|
|
1554
|
+
"a2.contract": name,
|
|
1555
|
+
"a2.session_id": sessionId,
|
|
1556
|
+
"a2.snapshot.reducer": reducer.name,
|
|
1557
|
+
"a2.snapshot.index": snapshots.at(-1).index,
|
|
1558
|
+
"a2.snapshot.count": snapshots.length
|
|
1559
|
+
}, async (span) => {
|
|
1560
|
+
try {
|
|
1561
|
+
await store.putSnapshots(nsId(sessionId), reducer.name, snapshots);
|
|
1562
|
+
} catch (error) {
|
|
1563
|
+
try {
|
|
1564
|
+
span.recordError(error);
|
|
1565
|
+
} catch (telemetryError) {
|
|
1566
|
+
throw new AggregateError([error, telemetryError], "Snapshot write and error reporting failed", { cause: telemetryError });
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
})).catch((error) => {
|
|
1570
|
+
console.error("[a2] error in a2.snapshot telemetry", {
|
|
1571
|
+
sessionId,
|
|
1572
|
+
reducer: reducer.name,
|
|
1573
|
+
index: snapshots.at(-1).index
|
|
1574
|
+
}, error);
|
|
1575
|
+
});
|
|
1576
|
+
track(persistence);
|
|
1577
|
+
platformWaitUntil(persistence);
|
|
1578
|
+
};
|
|
1579
|
+
const stateCache = boundedCache(STATE_CACHE);
|
|
1580
|
+
const cacheState = (key, next) => {
|
|
1581
|
+
const current = stateCache.get(key);
|
|
1582
|
+
if (current?.reducer === next.reducer && (current.index > next.index || current.index === next.index && current.persistedIndex >= next.persistedIndex)) return;
|
|
1583
|
+
stateCache.set(key, next, next.bytes);
|
|
1584
|
+
};
|
|
1585
|
+
const writeSnapshots = ({ store, reducer, sessionId, writes }) => {
|
|
1586
|
+
const snapshots = [...writes].toSorted(([a], [b]) => a - b).map(([index, write]) => {
|
|
1587
|
+
const snapshot = {
|
|
1588
|
+
index,
|
|
1589
|
+
state: encodeState({
|
|
1590
|
+
reducer,
|
|
1591
|
+
state: write.state
|
|
1592
|
+
})
|
|
1593
|
+
};
|
|
1594
|
+
if (write.pinEventIndexes.size > 0) snapshot.pinEventIndexes = [...write.pinEventIndexes];
|
|
1595
|
+
return snapshot;
|
|
1596
|
+
});
|
|
1597
|
+
if (snapshots.length > 0) persistSnapshots({
|
|
1598
|
+
store,
|
|
1599
|
+
reducer,
|
|
1600
|
+
sessionId,
|
|
1601
|
+
snapshots
|
|
1602
|
+
});
|
|
1603
|
+
return snapshots;
|
|
1604
|
+
};
|
|
1605
|
+
/**
|
|
1606
|
+
* Folds `rows` from `position` in bound order, resolving each entry with a
|
|
1607
|
+
* clone once the rows through its bound are folded. `visit` sees each
|
|
1608
|
+
* entry's state before later rows are folded into it.
|
|
1609
|
+
*/
|
|
1610
|
+
const foldEntries = ({ reducer, entries, rows, position, state, index, clearIndex, snapshot, visit }) => {
|
|
1611
|
+
const start = position;
|
|
1612
|
+
const ordered = entries.toSorted((a, b) => (a.throughIndex ?? Number.POSITIVE_INFINITY) - (b.throughIndex ?? Number.POSITIVE_INFINITY));
|
|
1613
|
+
for (const entry of ordered) {
|
|
1614
|
+
const target = entry.throughIndex ?? Number.POSITIVE_INFINITY;
|
|
1615
|
+
while (position < rows.length && rows[position].index <= target) {
|
|
1616
|
+
const row = rows[position];
|
|
1617
|
+
state = foldEvent(reducer, state, toPublic(row));
|
|
1618
|
+
index = row.index;
|
|
1619
|
+
position += 1;
|
|
1620
|
+
}
|
|
1621
|
+
if (entry.triggerIndex !== void 0 && entry.triggerIndex <= clearIndex || entry.throughIndex !== void 0 && entry.throughIndex > 0 && entry.throughIndex < clearIndex) {
|
|
1622
|
+
entry.reject(sessionClearedError(`session '${entry.sessionId}' was cleared at index ${clearIndex}`));
|
|
1623
|
+
continue;
|
|
1624
|
+
}
|
|
1625
|
+
entry.span.setAttribute("a2.state.snapshot", snapshot);
|
|
1626
|
+
entry.span.setAttribute("a2.state.folded", position - start);
|
|
1627
|
+
entry.span.setAttribute("a2.state.index", index);
|
|
1628
|
+
visit(entry, state, index, position - start);
|
|
1629
|
+
entry.resolve({
|
|
1630
|
+
state: cloneInitial(state),
|
|
1631
|
+
index,
|
|
1632
|
+
...clearIndex > 0 && index >= clearIndex ? { clearIndex } : {}
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1635
|
+
return {
|
|
1636
|
+
state,
|
|
1637
|
+
index,
|
|
1638
|
+
position
|
|
1639
|
+
};
|
|
1640
|
+
};
|
|
1641
|
+
/**
|
|
1642
|
+
* Events from `fromIndex` (inclusive) through `throughIndex` or the newest
|
|
1643
|
+
* event, or null when the tail is too long to be worth folding in memory.
|
|
1644
|
+
*/
|
|
1645
|
+
const readCachedTail = async (store, sessionId, fromIndex, throughIndex) => {
|
|
1646
|
+
const readTail = stateTailReaders.get(store);
|
|
1647
|
+
if (readTail) try {
|
|
1648
|
+
return await readTail(nsId(sessionId), fromIndex, throughIndex);
|
|
1649
|
+
} catch (err) {
|
|
1650
|
+
throw asStoreUnavailable(err);
|
|
1651
|
+
}
|
|
1652
|
+
const limit = fromIndex + STATE_CACHE.maxTail;
|
|
1653
|
+
const capped = throughIndex === void 0 || throughIndex > limit;
|
|
1654
|
+
const rows = await readHistory(sessionId, {
|
|
1655
|
+
gte: fromIndex,
|
|
1656
|
+
lte: capped ? limit : throughIndex
|
|
1657
|
+
});
|
|
1658
|
+
return capped && rows.at(-1)?.index === limit ? null : rows;
|
|
1659
|
+
};
|
|
1660
|
+
/**
|
|
1661
|
+
* Serves a plan from the cached state when every read is at or past the
|
|
1662
|
+
* cached index. A read bounded at the cached index by an event whose id
|
|
1663
|
+
* matches the cached one (a handler's own trigger) needs no store request;
|
|
1664
|
+
* any other read fetches the tail from the cached event onward and checks
|
|
1665
|
+
* that event's id. Returns false when the store must answer instead.
|
|
1666
|
+
*/
|
|
1667
|
+
const foldCachedPlan = async (store, reducer, plan) => {
|
|
1668
|
+
const key = stateCacheKey(plan.sessionId, reducer.name);
|
|
1669
|
+
const cached = stateCache.get(key);
|
|
1670
|
+
if (cached?.reducer !== reducer) return false;
|
|
1671
|
+
if (plan.entries.some((entry) => entry.throughIndex !== void 0 && entry.throughIndex < cached.index)) return false;
|
|
1672
|
+
let rows = [];
|
|
1673
|
+
if (plan.entries.some((entry) => entry.throughIndex !== cached.index || entry.throughEventId !== cached.lastId)) {
|
|
1674
|
+
const tail = await readCachedTail(store, plan.sessionId, cached.index, plan.throughIndex);
|
|
1675
|
+
if (!tail) return false;
|
|
1676
|
+
if (tail[0]?.index !== cached.index || tail[0].id !== cached.lastId || tail.some((row, position) => position > 0 && isSessionCleared(row))) {
|
|
1677
|
+
stateCache.delete(key);
|
|
1678
|
+
return false;
|
|
1679
|
+
}
|
|
1680
|
+
rows = tail;
|
|
1681
|
+
}
|
|
1682
|
+
const writes = /* @__PURE__ */ new Map();
|
|
1683
|
+
const folded = foldEntries({
|
|
1684
|
+
reducer,
|
|
1685
|
+
entries: plan.entries,
|
|
1686
|
+
rows,
|
|
1687
|
+
position: 1,
|
|
1688
|
+
state: rows.length > 1 ? cloneInitial(cached.state) : cached.state,
|
|
1689
|
+
index: cached.index,
|
|
1690
|
+
clearIndex: cached.clearIndex,
|
|
1691
|
+
snapshot: "memory",
|
|
1692
|
+
visit: (entry, state, index) => {
|
|
1693
|
+
if (entry.pinEventIndex !== void 0) addSnapshotWrite(writes, index, state, entry.pinEventIndex);
|
|
1694
|
+
}
|
|
1695
|
+
});
|
|
1696
|
+
const added = Math.max(0, folded.position - 1);
|
|
1697
|
+
const rewritesHead = folded.index - cached.persistedIndex >= STATE_CACHE.snapshotEvery;
|
|
1698
|
+
if (rewritesHead && !writes.has(folded.index)) writes.set(folded.index, {
|
|
1699
|
+
state: folded.state,
|
|
1700
|
+
pinEventIndexes: /* @__PURE__ */ new Set()
|
|
1701
|
+
});
|
|
1702
|
+
const snapshots = writeSnapshots({
|
|
1703
|
+
store,
|
|
1704
|
+
reducer,
|
|
1705
|
+
sessionId: plan.sessionId,
|
|
1706
|
+
writes
|
|
1707
|
+
});
|
|
1708
|
+
const measure = rewritesHead || cached.unmeasured + added >= STATE_CACHE.measureEvery;
|
|
1709
|
+
let grown = 0;
|
|
1710
|
+
if (!measure) for (let position = 1; position < folded.position; position += 1) grown += JSON.stringify(rows[position].payload)?.length ?? 0;
|
|
1711
|
+
cacheState(key, {
|
|
1712
|
+
reducer,
|
|
1713
|
+
index: folded.index,
|
|
1714
|
+
lastId: added > 0 ? rows[folded.position - 1].id : cached.lastId,
|
|
1715
|
+
state: folded.state,
|
|
1716
|
+
bytes: measure ? measuredBytes(reducer, folded.state, folded.index, snapshots) : cached.bytes + grown,
|
|
1717
|
+
unmeasured: measure ? 0 : cached.unmeasured + added,
|
|
1718
|
+
persistedIndex: Math.max(cached.persistedIndex, snapshots.at(-1)?.index ?? 0),
|
|
1719
|
+
clearIndex: cached.clearIndex
|
|
1720
|
+
});
|
|
1721
|
+
return true;
|
|
1722
|
+
};
|
|
1207
1723
|
const foldStatePlan = async (store, reducer, plan, stateRead) => {
|
|
1208
1724
|
let state = cloneInitial(reducer.initialState);
|
|
1209
1725
|
let index = 0;
|
|
1210
1726
|
let snapshotOutcome = "miss";
|
|
1211
1727
|
const snap = stateRead.snapshot;
|
|
1212
1728
|
let rows = stateRead.events;
|
|
1729
|
+
let clearIndex = stateRead.clearIndex ?? 0;
|
|
1213
1730
|
if (snap) {
|
|
1214
|
-
|
|
1215
|
-
|
|
1731
|
+
let decoded;
|
|
1732
|
+
try {
|
|
1733
|
+
decoded = decodeState({
|
|
1734
|
+
reducer,
|
|
1735
|
+
state: snap.state
|
|
1736
|
+
});
|
|
1737
|
+
} catch {
|
|
1738
|
+
snapshotOutcome = "rejected";
|
|
1739
|
+
}
|
|
1740
|
+
if (snapshotOutcome !== "rejected" && reducer.stateSchema) {
|
|
1741
|
+
const result = validateSync(reducer.stateSchema, decoded, "the stateSchema");
|
|
1216
1742
|
if (result.issues) snapshotOutcome = "rejected";
|
|
1217
1743
|
else {
|
|
1218
1744
|
state = result.value;
|
|
1219
1745
|
index = snap.index;
|
|
1220
1746
|
snapshotOutcome = "hit";
|
|
1221
1747
|
}
|
|
1222
|
-
} else {
|
|
1223
|
-
state =
|
|
1748
|
+
} else if (snapshotOutcome !== "rejected") {
|
|
1749
|
+
state = decoded;
|
|
1224
1750
|
index = snap.index;
|
|
1225
1751
|
snapshotOutcome = "hit";
|
|
1226
1752
|
}
|
|
1227
1753
|
}
|
|
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);
|
|
1754
|
+
if (snapshotOutcome === "rejected") {
|
|
1755
|
+
try {
|
|
1756
|
+
rows = await store.read(nsId(plan.sessionId), plan.throughIndex === void 0 ? void 0 : { throughIndex: plan.throughIndex });
|
|
1757
|
+
} catch (err) {
|
|
1758
|
+
throw asStoreUnavailable(err);
|
|
1268
1759
|
}
|
|
1269
|
-
|
|
1270
|
-
state: resultState,
|
|
1271
|
-
index
|
|
1272
|
-
});
|
|
1760
|
+
if (rows[0] && isSessionCleared(rows[0])) clearIndex = Math.max(clearIndex, rows[0].index);
|
|
1273
1761
|
}
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1762
|
+
const writes = /* @__PURE__ */ new Map();
|
|
1763
|
+
const folded = foldEntries({
|
|
1764
|
+
reducer,
|
|
1765
|
+
entries: plan.entries,
|
|
1766
|
+
rows,
|
|
1767
|
+
position: 0,
|
|
1768
|
+
state,
|
|
1769
|
+
index,
|
|
1770
|
+
clearIndex,
|
|
1771
|
+
snapshot: snapshotOutcome,
|
|
1772
|
+
visit: (entry, entryState, entryIndex, count) => {
|
|
1773
|
+
if (entryIndex === 0) return;
|
|
1774
|
+
if (count === 0 && entry.pinEventIndex === void 0) return;
|
|
1775
|
+
const advancesHead = stateRead.headIndex === null || entryIndex > stateRead.headIndex;
|
|
1776
|
+
const repairsHead = snapshotOutcome === "rejected" && entryIndex === stateRead.headIndex;
|
|
1777
|
+
if (!advancesHead && !repairsHead && entry.pinEventIndex === void 0) return;
|
|
1778
|
+
addSnapshotWrite(writes, entryIndex, entryState, entry.pinEventIndex);
|
|
1283
1779
|
}
|
|
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
|
-
}
|
|
1780
|
+
});
|
|
1781
|
+
const snapshots = writeSnapshots({
|
|
1782
|
+
store,
|
|
1783
|
+
reducer,
|
|
1784
|
+
sessionId: plan.sessionId,
|
|
1785
|
+
writes
|
|
1786
|
+
});
|
|
1787
|
+
if (folded.position > 0 && STATE_CACHE.maxEntries > 0) cacheState(stateCacheKey(plan.sessionId, reducer.name), {
|
|
1788
|
+
reducer,
|
|
1789
|
+
index: folded.index,
|
|
1790
|
+
lastId: rows[folded.position - 1].id,
|
|
1791
|
+
state: folded.state,
|
|
1792
|
+
bytes: measuredBytes(reducer, folded.state, folded.index, snapshots),
|
|
1793
|
+
unmeasured: 0,
|
|
1794
|
+
persistedIndex: Math.min(Math.max(stateRead.headIndex ?? 0, ...writes.keys()), folded.index),
|
|
1795
|
+
clearIndex
|
|
1796
|
+
});
|
|
1310
1797
|
};
|
|
1311
1798
|
const pendingStateReads = /* @__PURE__ */ new Map();
|
|
1312
1799
|
const flushStateReads = async (store, reducer, batch) => {
|
|
1313
|
-
const plans =
|
|
1800
|
+
const plans = [];
|
|
1801
|
+
await Promise.all(planStateReads(batch).map(async (plan) => {
|
|
1802
|
+
try {
|
|
1803
|
+
if (!await foldCachedPlan(store, reducer, plan)) plans.push(plan);
|
|
1804
|
+
} catch (error) {
|
|
1805
|
+
for (const entry of plan.entries) entry.reject(error);
|
|
1806
|
+
}
|
|
1807
|
+
}));
|
|
1808
|
+
if (plans.length === 0) return;
|
|
1314
1809
|
let reads;
|
|
1315
1810
|
try {
|
|
1316
1811
|
reads = store.readStates ? await store.readStates(plans.map((plan) => ({
|
|
@@ -1321,7 +1816,7 @@ function createServer(options) {
|
|
|
1321
1816
|
if (reads.length !== plans.length) throw new TypeError("store.readStates must return one result per request");
|
|
1322
1817
|
} catch (error) {
|
|
1323
1818
|
const failure = asStoreUnavailable(error);
|
|
1324
|
-
for (const entry of
|
|
1819
|
+
for (const plan of plans) for (const entry of plan.entries) entry.reject(failure);
|
|
1325
1820
|
return;
|
|
1326
1821
|
}
|
|
1327
1822
|
await Promise.all(plans.map(async (plan, position) => {
|
|
@@ -1332,7 +1827,7 @@ function createServer(options) {
|
|
|
1332
1827
|
}
|
|
1333
1828
|
}));
|
|
1334
1829
|
};
|
|
1335
|
-
const enqueueStateRead = (store, sessionId, reducer,
|
|
1830
|
+
const enqueueStateRead = (store, sessionId, reducer, bounds, span) => new Promise((resolve, reject) => {
|
|
1336
1831
|
const reducerName = reducer.name;
|
|
1337
1832
|
let batch = pendingStateReads.get(reducerName);
|
|
1338
1833
|
if (!batch) {
|
|
@@ -1346,25 +1841,25 @@ function createServer(options) {
|
|
|
1346
1841
|
}
|
|
1347
1842
|
batch.push({
|
|
1348
1843
|
sessionId,
|
|
1349
|
-
...
|
|
1350
|
-
...pinEventIndex === void 0 ? {} : { pinEventIndex },
|
|
1844
|
+
...bounds,
|
|
1351
1845
|
span,
|
|
1352
1846
|
resolve,
|
|
1353
1847
|
reject
|
|
1354
1848
|
});
|
|
1355
1849
|
});
|
|
1356
|
-
const readState = async (sessionId, reducer,
|
|
1850
|
+
const readState = async (sessionId, reducer, bounds) => {
|
|
1357
1851
|
const store = await resolveStore();
|
|
1358
1852
|
return telemetry.span("a2.state", {
|
|
1359
1853
|
"a2.contract": name,
|
|
1360
1854
|
"a2.session_id": sessionId,
|
|
1361
1855
|
"a2.state.reducer": reducer.name
|
|
1362
|
-
}, (span) => enqueueStateRead(store, sessionId, reducer,
|
|
1856
|
+
}, (span) => enqueueStateRead(store, sessionId, reducer, bounds, span));
|
|
1363
1857
|
};
|
|
1364
1858
|
const makeSchedule = (sessionId, trigger) => {
|
|
1365
1859
|
const triggerEventId = trigger?.id ?? null;
|
|
1366
1860
|
const triggerCreatedAt = trigger?.createdAt.getTime();
|
|
1367
|
-
|
|
1861
|
+
const clearIndex = trigger?.clearIndex ?? 0;
|
|
1862
|
+
const schedule = async (scheduleName, timing, ...events) => {
|
|
1368
1863
|
if (!scheduler) throw new TypeError("session.schedule requires a configured scheduler adapter");
|
|
1369
1864
|
assertScheduleName(scheduleName);
|
|
1370
1865
|
if (events.length === 0) throw new TypeError("schedule requires at least one event");
|
|
@@ -1379,7 +1874,7 @@ function createServer(options) {
|
|
|
1379
1874
|
id: event.id
|
|
1380
1875
|
});
|
|
1381
1876
|
const validated = validateEvents(sessionId, structuredClone(snapshottedEvents), false);
|
|
1382
|
-
const scheduleId = await deterministicScheduleId(name, sessionId, triggerEventId, scheduleName);
|
|
1877
|
+
const scheduleId = await deterministicScheduleId(name, sessionId, triggerEventId, scheduleName, clearIndex);
|
|
1383
1878
|
const scheduledEvents = await Promise.all(validated.map(async (event, item) => ({
|
|
1384
1879
|
id: event.id ?? await deterministicScheduledEventId(scheduleId, item),
|
|
1385
1880
|
type: event.type,
|
|
@@ -1394,12 +1889,45 @@ function createServer(options) {
|
|
|
1394
1889
|
contract: name,
|
|
1395
1890
|
sessionId,
|
|
1396
1891
|
dueAt,
|
|
1397
|
-
events: scheduledEvents
|
|
1892
|
+
events: scheduledEvents,
|
|
1893
|
+
...trigger ? { originIndex: trigger.index } : {}
|
|
1398
1894
|
});
|
|
1399
1895
|
} catch (error) {
|
|
1400
1896
|
throw markSchedulerSendFailure(error);
|
|
1401
1897
|
}
|
|
1402
1898
|
};
|
|
1899
|
+
const clear = async (scheduleName, clearOptions) => {
|
|
1900
|
+
if (!scheduler) throw new TypeError("session.schedule.clear requires a configured scheduler adapter");
|
|
1901
|
+
assertScheduleName(scheduleName);
|
|
1902
|
+
if (clearOptions === null || typeof clearOptions !== "object") throw new TypeError("schedule.clear requires an ifLastIndex");
|
|
1903
|
+
const { ifLastIndex, ...timing } = clearOptions;
|
|
1904
|
+
assertClearIndex(ifLastIndex);
|
|
1905
|
+
const dueAt = scheduleDueAt(timing, triggerCreatedAt ?? Date.now());
|
|
1906
|
+
const store = await resolveStore();
|
|
1907
|
+
if (!store.clear || !store.reclaim) throw new TypeError("session.schedule.clear requires a store that supports clearing");
|
|
1908
|
+
const id = await deterministicScheduleClearId({
|
|
1909
|
+
contract: name,
|
|
1910
|
+
sessionId,
|
|
1911
|
+
triggerEventId,
|
|
1912
|
+
clearIndex,
|
|
1913
|
+
name: scheduleName,
|
|
1914
|
+
ifLastIndex
|
|
1915
|
+
});
|
|
1916
|
+
try {
|
|
1917
|
+
await scheduler.schedule({
|
|
1918
|
+
version: 1,
|
|
1919
|
+
kind: "clear",
|
|
1920
|
+
id,
|
|
1921
|
+
contract: name,
|
|
1922
|
+
sessionId,
|
|
1923
|
+
dueAt,
|
|
1924
|
+
ifLastIndex
|
|
1925
|
+
});
|
|
1926
|
+
} catch (error) {
|
|
1927
|
+
throw markSchedulerSendFailure(error);
|
|
1928
|
+
}
|
|
1929
|
+
};
|
|
1930
|
+
return Object.assign(schedule, { clear });
|
|
1403
1931
|
};
|
|
1404
1932
|
const toSessionEvent = (id, event) => ({
|
|
1405
1933
|
id: event.id,
|
|
@@ -1459,6 +1987,13 @@ function createServer(options) {
|
|
|
1459
1987
|
}
|
|
1460
1988
|
};
|
|
1461
1989
|
streamActivities.set(stream, activity);
|
|
1990
|
+
bufferedEvents.set(stream, () => {
|
|
1991
|
+
const event = closed || !inner ? void 0 : takeBuffered(inner);
|
|
1992
|
+
if (!event) return void 0;
|
|
1993
|
+
if (!isSerializedEvent(event)) return toSessionEvent(id, event);
|
|
1994
|
+
event.sessionId = id;
|
|
1995
|
+
return event;
|
|
1996
|
+
});
|
|
1462
1997
|
return stream;
|
|
1463
1998
|
} };
|
|
1464
1999
|
};
|
|
@@ -1514,6 +2049,7 @@ function createServer(options) {
|
|
|
1514
2049
|
const ns = nsId(id);
|
|
1515
2050
|
let closed = false;
|
|
1516
2051
|
let events;
|
|
2052
|
+
let pendingEvent = null;
|
|
1517
2053
|
let unsubscribe;
|
|
1518
2054
|
let sleeper = null;
|
|
1519
2055
|
let wakeQueue = null;
|
|
@@ -1655,7 +2191,6 @@ function createServer(options) {
|
|
|
1655
2191
|
}
|
|
1656
2192
|
return null;
|
|
1657
2193
|
};
|
|
1658
|
-
let pendingEvent = null;
|
|
1659
2194
|
try {
|
|
1660
2195
|
unsubscribe = api.subscribe?.(ns, enqueue);
|
|
1661
2196
|
events = store.stream(ns, { startAfter })[Symbol.asyncIterator]();
|
|
@@ -1745,18 +2280,86 @@ function createServer(options) {
|
|
|
1745
2280
|
}
|
|
1746
2281
|
};
|
|
1747
2282
|
streamActivities.set(stream, activity);
|
|
2283
|
+
bufferedEvents.set(stream, () => {
|
|
2284
|
+
if (closed || !events || pendingEvent) return void 0;
|
|
2285
|
+
const event = takeBuffered(events);
|
|
2286
|
+
if (!event) return void 0;
|
|
2287
|
+
if (!isSerializedEvent(event)) return toSessionEvent(id, event);
|
|
2288
|
+
event.sessionId = id;
|
|
2289
|
+
return event;
|
|
2290
|
+
});
|
|
1748
2291
|
return stream;
|
|
1749
2292
|
} };
|
|
1750
2293
|
};
|
|
1751
|
-
const
|
|
2294
|
+
const reclaimAll = async (store, ns) => {
|
|
2295
|
+
for (let stalls = 0;;) {
|
|
2296
|
+
let batch;
|
|
2297
|
+
try {
|
|
2298
|
+
batch = await store.reclaim(ns);
|
|
2299
|
+
} catch (err) {
|
|
2300
|
+
throw asStoreUnavailable(err);
|
|
2301
|
+
}
|
|
2302
|
+
if (batch.done) return;
|
|
2303
|
+
if (batch.deleted > 0) {
|
|
2304
|
+
stalls = 0;
|
|
2305
|
+
continue;
|
|
2306
|
+
}
|
|
2307
|
+
stalls += 1;
|
|
2308
|
+
if (stalls === RECLAIM_STALL_LIMIT) throw new A2Error("STORE_UNAVAILABLE", `deleting the cleared history of session '${stripNs(ns)}' made no progress`);
|
|
2309
|
+
await defaultSleep(RECLAIM_STALL_MS).promise;
|
|
2310
|
+
}
|
|
2311
|
+
};
|
|
2312
|
+
const clearSession = async (sessionId, clearOptions, attempt) => {
|
|
2313
|
+
const ifLastIndex = clearOptions?.ifLastIndex;
|
|
2314
|
+
if (ifLastIndex !== void 0) assertClearIndex(ifLastIndex);
|
|
2315
|
+
const store = await resolveStore();
|
|
2316
|
+
if (!store.clear || !store.reclaim) throw new TypeError("session.clear requires a store that supports clearing");
|
|
2317
|
+
const ns = nsId(sessionId);
|
|
2318
|
+
let result;
|
|
2319
|
+
try {
|
|
2320
|
+
result = await store.clear({
|
|
2321
|
+
sessionId: ns,
|
|
2322
|
+
...ifLastIndex === void 0 ? {} : { ifLastIndex },
|
|
2323
|
+
...attempt ? { cause: attempt.cause } : {}
|
|
2324
|
+
});
|
|
2325
|
+
} catch (err) {
|
|
2326
|
+
throw asStoreUnavailable(err);
|
|
2327
|
+
}
|
|
2328
|
+
if (result.cleared) {
|
|
2329
|
+
const cachePrefix = stateCacheKey(sessionId, "");
|
|
2330
|
+
stateCache.deleteWhere((key) => key.startsWith(cachePrefix));
|
|
2331
|
+
}
|
|
2332
|
+
const deletion = reclaimAll(store, ns);
|
|
2333
|
+
if (attempt) {
|
|
2334
|
+
if (attempt.cause.index <= result.clearIndex) attempt.end();
|
|
2335
|
+
const kept = deletion.catch(() => void 0);
|
|
2336
|
+
track(kept);
|
|
2337
|
+
platformWaitUntil(kept);
|
|
2338
|
+
}
|
|
2339
|
+
await deletion;
|
|
2340
|
+
return {
|
|
2341
|
+
cleared: result.cleared,
|
|
2342
|
+
index: result.index
|
|
2343
|
+
};
|
|
2344
|
+
};
|
|
2345
|
+
const makeSession = (id, append, trigger, attempt) => ({
|
|
1752
2346
|
id,
|
|
1753
2347
|
append,
|
|
1754
2348
|
schedule: makeSchedule(id, trigger),
|
|
1755
|
-
|
|
1756
|
-
|
|
2349
|
+
clear: (clearOptions) => clearSession(id, clearOptions, attempt),
|
|
2350
|
+
history: async (bounds) => (await readHistory(id, bounds)).filter((row) => !isSessionCleared(row)).map(toPublic),
|
|
2351
|
+
state: (reducer = serverContract.reducer, stateOptions) => {
|
|
2352
|
+
if (!reducer) throw new A2Error("INVALID_PAYLOAD", "contract has no public reducer");
|
|
1757
2353
|
const through = stateOptions?.through ?? trigger?.index;
|
|
1758
2354
|
if (through !== void 0 && through !== "latest") assertStateIndex(through);
|
|
1759
|
-
|
|
2355
|
+
const bounds = {};
|
|
2356
|
+
if (through !== void 0 && through !== "latest") {
|
|
2357
|
+
bounds.throughIndex = through;
|
|
2358
|
+
if (trigger) bounds.pinEventIndex = trigger.index;
|
|
2359
|
+
if (through === trigger?.index && !attempt?.signal.aborted) bounds.throughEventId = trigger.id;
|
|
2360
|
+
}
|
|
2361
|
+
if (trigger) bounds.triggerIndex = trigger.index;
|
|
2362
|
+
return readState(id, reducer, bounds);
|
|
1760
2363
|
},
|
|
1761
2364
|
stream: (opts) => {
|
|
1762
2365
|
const startAfter = opts?.startAfter ?? 0;
|
|
@@ -1768,7 +2371,7 @@ function createServer(options) {
|
|
|
1768
2371
|
presence: () => readPresenceMap(id)
|
|
1769
2372
|
} : {}
|
|
1770
2373
|
});
|
|
1771
|
-
const makeCtx = (trigger, attempt, signal) => {
|
|
2374
|
+
const makeCtx = (trigger, attempt, signal, endAttempt) => {
|
|
1772
2375
|
const publicSessionId = stripNs(trigger.sessionId);
|
|
1773
2376
|
const append = async (appendName, ...events) => {
|
|
1774
2377
|
assertAppendName(appendName);
|
|
@@ -1789,7 +2392,14 @@ function createServer(options) {
|
|
|
1789
2392
|
return {
|
|
1790
2393
|
event: toPublic(trigger),
|
|
1791
2394
|
attempt,
|
|
1792
|
-
session: makeSession(publicSessionId, append, trigger
|
|
2395
|
+
session: makeSession(publicSessionId, append, trigger, {
|
|
2396
|
+
cause: {
|
|
2397
|
+
index: trigger.index,
|
|
2398
|
+
attempt
|
|
2399
|
+
},
|
|
2400
|
+
end: endAttempt,
|
|
2401
|
+
signal
|
|
2402
|
+
}),
|
|
1793
2403
|
signal
|
|
1794
2404
|
};
|
|
1795
2405
|
};
|
|
@@ -1988,12 +2598,7 @@ function createServer(options) {
|
|
|
1988
2598
|
for (const index of result.superseded) {
|
|
1989
2599
|
const entry = active.get(index);
|
|
1990
2600
|
if (!entry) continue;
|
|
1991
|
-
entry
|
|
1992
|
-
entry.fence({
|
|
1993
|
-
status: "superseded",
|
|
1994
|
-
opensLane: false,
|
|
1995
|
-
appendsEvents: false
|
|
1996
|
-
});
|
|
2601
|
+
endSuperseded(entry, `a newer attempt owns event ${index} in session '${sessionId}'`);
|
|
1997
2602
|
}
|
|
1998
2603
|
if (window.deadlineCapped) stopRenewing = true;
|
|
1999
2604
|
scheduleLapse();
|
|
@@ -2007,7 +2612,7 @@ function createServer(options) {
|
|
|
2007
2612
|
renewal = tracked;
|
|
2008
2613
|
return tracked;
|
|
2009
2614
|
};
|
|
2010
|
-
const runEvent = async (event, supersedeSignal) => {
|
|
2615
|
+
const runEvent = async (event, supersedeSignal, endAttempt) => {
|
|
2011
2616
|
const registration = handlers.get(event.type);
|
|
2012
2617
|
return telemetry.span("a2.event", {
|
|
2013
2618
|
"a2.contract": name,
|
|
@@ -2027,7 +2632,7 @@ function createServer(options) {
|
|
|
2027
2632
|
subscription = await subscribeAbort(store, ns, event, registration.abortOn, event.attemptCount);
|
|
2028
2633
|
handlerSignal = AbortSignal.any([supersedeSignal, subscription.signal]);
|
|
2029
2634
|
}
|
|
2030
|
-
const operation = registration?.handler(makeCtx(event, event.attemptCount, handlerSignal));
|
|
2635
|
+
const operation = registration?.handler(makeCtx(event, event.attemptCount, handlerSignal, endAttempt));
|
|
2031
2636
|
const result = operation ? await (subscription ? subscription.monitor(operation) : operation) : void 0;
|
|
2032
2637
|
if (subscription) {
|
|
2033
2638
|
await subscription.close();
|
|
@@ -2044,12 +2649,22 @@ function createServer(options) {
|
|
|
2044
2649
|
};
|
|
2045
2650
|
}
|
|
2046
2651
|
eventSpan.recordError(err);
|
|
2047
|
-
const failure = await
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2652
|
+
const failure = await telemetry.span("a2.commit", {
|
|
2653
|
+
"a2.contract": name,
|
|
2654
|
+
"a2.session_id": sessionId,
|
|
2655
|
+
"a2.commit.operation": "fail",
|
|
2656
|
+
"a2.commit.index": event.index,
|
|
2657
|
+
"a2.commit.attempt": event.attemptCount
|
|
2658
|
+
}, async (commitSpan) => {
|
|
2659
|
+
const result = await store.failAttempt({
|
|
2660
|
+
sessionId: ns,
|
|
2661
|
+
index: event.index,
|
|
2662
|
+
attempt: event.attemptCount,
|
|
2663
|
+
error: describeError(err),
|
|
2664
|
+
maxFailures: err instanceof NonRetriableError ? 1 : MAX_FAILURES
|
|
2665
|
+
});
|
|
2666
|
+
commitSpan.setAttribute("a2.commit.outcome", result.outcome);
|
|
2667
|
+
return result;
|
|
2053
2668
|
});
|
|
2054
2669
|
eventSpan.setAttribute("a2.event.outcome", failure.outcome);
|
|
2055
2670
|
return {
|
|
@@ -2061,11 +2676,21 @@ function createServer(options) {
|
|
|
2061
2676
|
await subscription?.close();
|
|
2062
2677
|
}
|
|
2063
2678
|
await renewal;
|
|
2064
|
-
const completion = await
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2679
|
+
const completion = await telemetry.span("a2.commit", {
|
|
2680
|
+
"a2.contract": name,
|
|
2681
|
+
"a2.session_id": sessionId,
|
|
2682
|
+
"a2.commit.operation": "complete",
|
|
2683
|
+
"a2.commit.index": event.index,
|
|
2684
|
+
"a2.commit.attempt": event.attemptCount
|
|
2685
|
+
}, async (commitSpan) => {
|
|
2686
|
+
const result = await store.completeAttempt({
|
|
2687
|
+
sessionId: ns,
|
|
2688
|
+
index: event.index,
|
|
2689
|
+
attempt: event.attemptCount,
|
|
2690
|
+
events: returned
|
|
2691
|
+
});
|
|
2692
|
+
commitSpan.setAttribute("a2.commit.outcome", result.outcome);
|
|
2693
|
+
return result;
|
|
2069
2694
|
});
|
|
2070
2695
|
eventSpan.setAttribute("a2.event.outcome", completion.outcome === "completed" ? "processed" : completion.outcome);
|
|
2071
2696
|
if (completion.outcome === "superseded") return {
|
|
@@ -2084,21 +2709,18 @@ function createServer(options) {
|
|
|
2084
2709
|
let claimAgain = false;
|
|
2085
2710
|
const start = (event, expiresAtMs) => {
|
|
2086
2711
|
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
|
-
}
|
|
2712
|
+
if (previous) endSuperseded(previous, `a newer attempt owns event ${event.index} in session '${sessionId}'`);
|
|
2095
2713
|
const supersede = new AbortController();
|
|
2096
2714
|
let fence;
|
|
2097
2715
|
const fenced = new Promise((resolve) => {
|
|
2098
2716
|
fence = resolve;
|
|
2099
2717
|
});
|
|
2718
|
+
const endAttempt = () => endSuperseded({
|
|
2719
|
+
supersede,
|
|
2720
|
+
fence
|
|
2721
|
+
}, `event ${event.index} in session '${sessionId}' was cleared`);
|
|
2100
2722
|
let entry;
|
|
2101
|
-
const execution = Promise.race([runEvent(event, supersede.signal), fenced]).then((eventOutcome) => {
|
|
2723
|
+
const execution = Promise.race([runEvent(event, supersede.signal, endAttempt), fenced]).then((eventOutcome) => {
|
|
2102
2724
|
if (active.get(event.index) === entry) active.delete(event.index);
|
|
2103
2725
|
if (eventOutcome.status === "processed") processed += 1;
|
|
2104
2726
|
if (eventOutcome.status === "failed") {
|
|
@@ -2131,12 +2753,20 @@ function createServer(options) {
|
|
|
2131
2753
|
let claim;
|
|
2132
2754
|
if (shouldClaim) {
|
|
2133
2755
|
const window = nextClaimWindow();
|
|
2134
|
-
claim = await
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2756
|
+
claim = await telemetry.span("a2.claim", {
|
|
2757
|
+
"a2.contract": name,
|
|
2758
|
+
"a2.session_id": sessionId
|
|
2759
|
+
}, async (claimSpan) => {
|
|
2760
|
+
const result = await store.claimAvailable({
|
|
2761
|
+
sessionId: ns,
|
|
2762
|
+
holder,
|
|
2763
|
+
ttlMs: window.ttlMs,
|
|
2764
|
+
...window.deadlineCapped ? { expiresAtMs: window.expiresAtMs } : {},
|
|
2765
|
+
...excluded.size === 0 ? {} : { excludeIndexes: [...excluded] }
|
|
2766
|
+
});
|
|
2767
|
+
claimSpan.setAttribute("a2.claim.outcome", result.outcome);
|
|
2768
|
+
claimSpan.setAttribute("a2.claim.count", result.outcome === "claimed" ? result.events.length : 0);
|
|
2769
|
+
return result;
|
|
2140
2770
|
});
|
|
2141
2771
|
if (claim.outcome === "claimed") {
|
|
2142
2772
|
requestWatchdog(window.watchdogAtMs);
|
|
@@ -2195,8 +2825,8 @@ function createServer(options) {
|
|
|
2195
2825
|
...schedulerArm ? { schedulerArm } : {}
|
|
2196
2826
|
};
|
|
2197
2827
|
});
|
|
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);
|
|
2828
|
+
const appendExternal = async (sessionId, mode, events, requireDurableInitialArm = false, originIndex) => {
|
|
2829
|
+
return appendCore(sessionId, events, "external", mode, (_rows, watchdogDueAt) => scheduleDrain(sessionId, watchdogDueAt), void 0, void 0, requireDurableInitialArm, originIndex);
|
|
2200
2830
|
};
|
|
2201
2831
|
const session = (id) => {
|
|
2202
2832
|
assertSessionId(id);
|
|
@@ -2205,11 +2835,21 @@ function createServer(options) {
|
|
|
2205
2835
|
const append = Object.assign(appendInline, { dispatch: appendDispatch });
|
|
2206
2836
|
return makeSession(id, append, null);
|
|
2207
2837
|
};
|
|
2838
|
+
const historyPage = async (sessionId, bounds) => {
|
|
2839
|
+
assertSessionId(sessionId);
|
|
2840
|
+
const rows = await readHistory(sessionId, bounds);
|
|
2841
|
+
const marker = rows[0] && isSessionCleared(rows[0]) ? rows[0] : void 0;
|
|
2842
|
+
return {
|
|
2843
|
+
events: (marker ? rows.slice(1) : rows).map(toPublic),
|
|
2844
|
+
...marker ? { clearIndex: marker.index } : {}
|
|
2845
|
+
};
|
|
2846
|
+
};
|
|
2208
2847
|
const self = {
|
|
2209
2848
|
contract: serverContract,
|
|
2210
2849
|
fetch: createServerFetch({
|
|
2211
2850
|
contract: serverContract,
|
|
2212
|
-
session
|
|
2851
|
+
session,
|
|
2852
|
+
historyPage
|
|
2213
2853
|
}),
|
|
2214
2854
|
session,
|
|
2215
2855
|
async drain(sessionId) {
|
|
@@ -2226,10 +2866,21 @@ function createServer(options) {
|
|
|
2226
2866
|
assertSessionId(sessionId);
|
|
2227
2867
|
return drainSession(sessionId, opts?.watchdogDueAt);
|
|
2228
2868
|
},
|
|
2229
|
-
schedulerAppend: async (sessionId, events) => {
|
|
2869
|
+
schedulerAppend: async (sessionId, events, originIndex) => {
|
|
2230
2870
|
if (!scheduler) throw new TypeError("scheduled append delivery requires a configured scheduler adapter");
|
|
2231
2871
|
assertSessionId(sessionId);
|
|
2232
|
-
|
|
2872
|
+
try {
|
|
2873
|
+
await appendExternal(sessionId, "inline", [...events], true, originIndex);
|
|
2874
|
+
} catch (error) {
|
|
2875
|
+
if (originIndex !== void 0 && error instanceof A2Error && error.code === "SESSION_CLEARED") return;
|
|
2876
|
+
throw error;
|
|
2877
|
+
}
|
|
2878
|
+
},
|
|
2879
|
+
historyWithMarkers: async (sessionId, gte) => (await readHistory(sessionId, { gte })).map(toPublic),
|
|
2880
|
+
schedulerClear: async (sessionId, ifLastIndex) => {
|
|
2881
|
+
assertSessionId(sessionId);
|
|
2882
|
+
assertClearIndex(ifLastIndex);
|
|
2883
|
+
await clearSession(sessionId, { ifLastIndex }, void 0);
|
|
2233
2884
|
}
|
|
2234
2885
|
});
|
|
2235
2886
|
return self;
|
|
@@ -2247,6 +2898,9 @@ function assertStreamIndex(value) {
|
|
|
2247
2898
|
function assertStateIndex(value) {
|
|
2248
2899
|
if (!Number.isSafeInteger(value) || value < 0) throw new TypeError("state.through must be a non-negative safe integer");
|
|
2249
2900
|
}
|
|
2901
|
+
function assertClearIndex(value) {
|
|
2902
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new TypeError("ifLastIndex must be a non-negative safe integer");
|
|
2903
|
+
}
|
|
2250
2904
|
function assertAppendName(name) {
|
|
2251
2905
|
if (typeof name !== "string" || name.length === 0) throw new TypeError("handler append name must be a non-empty string");
|
|
2252
2906
|
}
|
|
@@ -2328,14 +2982,12 @@ function isJsonValue(value, ancestors) {
|
|
|
2328
2982
|
ancestors.delete(value);
|
|
2329
2983
|
}
|
|
2330
2984
|
}
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
}
|
|
2337
|
-
}
|
|
2985
|
+
const measuredBytes = (reducer, state, index, snapshots) => JSON.stringify(snapshots.find((snapshot) => snapshot.index === index)?.state ?? encodeState({
|
|
2986
|
+
reducer,
|
|
2987
|
+
state
|
|
2988
|
+
}))?.length ?? 0;
|
|
2989
|
+
const stateCacheKey = (sessionId, reducerName) => `${sessionId}\u0000${reducerName}`;
|
|
2338
2990
|
//#endregion
|
|
2339
|
-
export {
|
|
2991
|
+
export { CONSOLE_TELEMETRY as a, parsePresenceSibling as c, handler as i, boundedCache as l, deliverSchedulerAppend as n, setServerFetchHooks as o, deliverSchedulerClear as r, sseResponse as s, createServer as t };
|
|
2340
2992
|
|
|
2341
|
-
//# sourceMappingURL=server-
|
|
2993
|
+
//# sourceMappingURL=server-Rj64fNvX.js.map
|