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
package/src/client.ts
CHANGED
|
@@ -14,7 +14,14 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
// oxlint-disable no-await-in-loop -- retry/backoff/stream loops are sequential by nature
|
|
17
|
+
import { decodeState, projectClientState } from './reducer-runtime.ts'
|
|
17
18
|
import { A2ClientError, asClientError } from './client-errors.ts'
|
|
19
|
+
import {
|
|
20
|
+
isInitialPromise,
|
|
21
|
+
observeInitial,
|
|
22
|
+
type InitialSnapshot,
|
|
23
|
+
} from './initial-snapshot.ts'
|
|
24
|
+
import { registerRenderSessionFactory } from './client-render-session.ts'
|
|
18
25
|
import {
|
|
19
26
|
PRESENCE_TIMINGS,
|
|
20
27
|
RESERVED_PARTICIPANT_IDS,
|
|
@@ -23,6 +30,7 @@ import {
|
|
|
23
30
|
} from './internal.ts'
|
|
24
31
|
import type {
|
|
25
32
|
AppendInput,
|
|
33
|
+
Contract,
|
|
26
34
|
ContractEvent,
|
|
27
35
|
EventDefs,
|
|
28
36
|
PresenceDefs,
|
|
@@ -30,11 +38,17 @@ import type {
|
|
|
30
38
|
PresencePatch,
|
|
31
39
|
WithPresence,
|
|
32
40
|
} from './contract.ts'
|
|
33
|
-
import {
|
|
41
|
+
import { foldEvent, type Reducer, type StateSnapshot } from './reducer.ts'
|
|
42
|
+
import { isSessionCleared } from './session-clear.ts'
|
|
34
43
|
import { validateSync } from './validate.ts'
|
|
35
44
|
import {
|
|
45
|
+
isStateSnapshotFrame,
|
|
46
|
+
type StateSnapshotFrame,
|
|
47
|
+
type SubscriptionStart,
|
|
36
48
|
errorFromWire,
|
|
49
|
+
eventFromCompactWire,
|
|
37
50
|
eventFromWire,
|
|
51
|
+
isCompactWireEvent,
|
|
38
52
|
isWireEvent,
|
|
39
53
|
isWirePresencePatch,
|
|
40
54
|
isWirePresenceSnapshot,
|
|
@@ -50,8 +64,11 @@ import {
|
|
|
50
64
|
type WirePresencePatch,
|
|
51
65
|
type WirePresenceSnapshot,
|
|
52
66
|
} from './wire.ts'
|
|
67
|
+
import type { Event } from './store.ts'
|
|
53
68
|
|
|
54
69
|
export { A2ClientError } from './client-errors.ts'
|
|
70
|
+
export type { StateSnapshot } from './reducer.ts'
|
|
71
|
+
export type { InitialSnapshot } from './initial-snapshot.ts'
|
|
55
72
|
export type { A2ClientErrorOptions } from './client-errors.ts'
|
|
56
73
|
|
|
57
74
|
export type ReconnectPolicy = (failure: {
|
|
@@ -129,6 +146,8 @@ export type SessionSnapshot<
|
|
|
129
146
|
> & {
|
|
130
147
|
/** The live view: server events folded, optimistic pushes applied. */
|
|
131
148
|
state: S
|
|
149
|
+
/** A state/index snapshot has been applied; independent of connection status. */
|
|
150
|
+
hydrated: boolean
|
|
132
151
|
/** The raw event feed: server truth — observed, seeded, or
|
|
133
152
|
* backscrolled via `loadHistory` — plus pending optimistic events
|
|
134
153
|
* (provisional indexes past the frontier). `state` never folds
|
|
@@ -164,11 +183,10 @@ export type SessionClient<
|
|
|
164
183
|
D extends EventDefs,
|
|
165
184
|
S,
|
|
166
185
|
P extends PresenceDefs = Record<never, never>,
|
|
167
|
-
V = S,
|
|
168
186
|
> = WithPresence<P, SessionClientPresence<P>> & {
|
|
169
187
|
readonly sessionId: string
|
|
170
188
|
subscribe(listener: () => void): () => void
|
|
171
|
-
getSnapshot(): SessionSnapshot<D,
|
|
189
|
+
getSnapshot(): SessionSnapshot<D, S, P>
|
|
172
190
|
/**
|
|
173
191
|
* Optimistic append: validates locally against the reducer's event
|
|
174
192
|
* schemas (instant `INVALID_PAYLOAD`, no flicker), applies to the
|
|
@@ -212,25 +230,53 @@ export type SessionClient<
|
|
|
212
230
|
}
|
|
213
231
|
|
|
214
232
|
export type SessionOptions<D extends EventDefs, S> = {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
/** Server-rendered history through `initialIndex`. Seeds the event feed. */
|
|
233
|
+
initial?: InitialSnapshot<S>
|
|
234
|
+
/** Server-rendered history through `initial.index`. Seeds the event feed. */
|
|
218
235
|
initialEvents?: ContractEvent<D>[]
|
|
219
236
|
/** Overrides the client's `participant` for this session handle.
|
|
220
237
|
* Bound once per handle. */
|
|
221
238
|
participant?: string
|
|
222
239
|
}
|
|
223
240
|
|
|
241
|
+
function assertSessionSeed(options: { initial?: unknown } | undefined): void {
|
|
242
|
+
const initial = options?.initial
|
|
243
|
+
if (initial == null || isInitialPromise(initial as InitialSnapshot<unknown>))
|
|
244
|
+
return
|
|
245
|
+
if (
|
|
246
|
+
initial === null ||
|
|
247
|
+
typeof initial !== 'object' ||
|
|
248
|
+
!Object.hasOwn(initial, 'state') ||
|
|
249
|
+
!Object.hasOwn(initial, 'index') ||
|
|
250
|
+
!('index' in initial) ||
|
|
251
|
+
typeof initial.index !== 'number' ||
|
|
252
|
+
!Number.isSafeInteger(initial.index) ||
|
|
253
|
+
initial.index < 0
|
|
254
|
+
)
|
|
255
|
+
throw new TypeError(
|
|
256
|
+
'initial must contain state and a non-negative safe integer index',
|
|
257
|
+
)
|
|
258
|
+
const clearIndex = (initial as { clearIndex?: unknown }).clearIndex
|
|
259
|
+
if (
|
|
260
|
+
clearIndex !== undefined &&
|
|
261
|
+
(typeof clearIndex !== 'number' ||
|
|
262
|
+
!Number.isSafeInteger(clearIndex) ||
|
|
263
|
+
clearIndex < 0 ||
|
|
264
|
+
clearIndex > initial.index)
|
|
265
|
+
)
|
|
266
|
+
throw new TypeError(
|
|
267
|
+
'initial.clearIndex must be a non-negative safe integer no greater than its index',
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
|
|
224
271
|
export type A2Client<
|
|
225
272
|
D extends EventDefs,
|
|
226
273
|
S,
|
|
227
274
|
P extends PresenceDefs = Record<never, never>,
|
|
228
|
-
V = S,
|
|
229
275
|
> = {
|
|
230
276
|
session(
|
|
231
277
|
sessionId: string,
|
|
232
278
|
options?: SessionOptions<D, S>,
|
|
233
|
-
): SessionClient<D, S, P
|
|
279
|
+
): SessionClient<D, S, P>
|
|
234
280
|
}
|
|
235
281
|
|
|
236
282
|
/**
|
|
@@ -277,9 +323,8 @@ export type CreateClientOptions<
|
|
|
277
323
|
D extends EventDefs,
|
|
278
324
|
S,
|
|
279
325
|
P extends PresenceDefs = Record<never, never>,
|
|
280
|
-
V = S,
|
|
281
326
|
> = {
|
|
282
|
-
|
|
327
|
+
contract: Contract<D, P, Reducer<D, S, P>>
|
|
283
328
|
/** The route (or routes) the client speaks — see `ClientApi`. */
|
|
284
329
|
api: ClientApi
|
|
285
330
|
/** Delay the next connection attempt in milliseconds, or pause with false. */
|
|
@@ -321,7 +366,9 @@ const DEFAULT_HISTORY_LIMIT = 50
|
|
|
321
366
|
* discrimination today, the ws `kind` discriminant tomorrow. `ping`
|
|
322
367
|
* carries no data — it exists so the watchdog sees liveness. */
|
|
323
368
|
type TransportFrame =
|
|
369
|
+
| StateSnapshotFrame
|
|
324
370
|
| { kind: 'event'; event: WireEvent }
|
|
371
|
+
| { kind: 'events'; events: Event[] }
|
|
325
372
|
| { kind: 'presence'; patch: WirePresencePatch }
|
|
326
373
|
| { kind: 'presence-snapshot'; snapshot: WirePresenceSnapshot }
|
|
327
374
|
| { kind: 'activity'; idle: boolean }
|
|
@@ -351,7 +398,8 @@ type Transport = {
|
|
|
351
398
|
*/
|
|
352
399
|
connect(opts: {
|
|
353
400
|
sessionId: string
|
|
354
|
-
startAfter: number
|
|
401
|
+
startAfter: number | undefined
|
|
402
|
+
reducer: string
|
|
355
403
|
signal: AbortSignal
|
|
356
404
|
}): AsyncIterable<TransportFrame>
|
|
357
405
|
/** One attempt (the retry loop lives above the seam): resolves with
|
|
@@ -364,7 +412,7 @@ type Transport = {
|
|
|
364
412
|
sessionId: string
|
|
365
413
|
gte: number
|
|
366
414
|
lte: number
|
|
367
|
-
}): Promise<WireEvent[]>
|
|
415
|
+
}): Promise<{ events: WireEvent[]; clearIndex?: number }>
|
|
368
416
|
/** Fire-and-forget; failures are dropped (presence repaints). */
|
|
369
417
|
sendPresence(envelope: TransportPresenceEnvelope): void
|
|
370
418
|
/**
|
|
@@ -409,11 +457,11 @@ const httpTransport = (
|
|
|
409
457
|
// exactly like unknown named frames.
|
|
410
458
|
presence: boolean,
|
|
411
459
|
): Transport => ({
|
|
412
|
-
async *connect({ sessionId, startAfter, signal }) {
|
|
460
|
+
async *connect({ sessionId, startAfter, reducer: reducerName, signal }) {
|
|
413
461
|
const streamUrl = sessionUrl(routes.stream, sessionId)
|
|
414
462
|
const sep = streamUrl.includes('?') ? '&' : '?'
|
|
415
463
|
const res = await fetchImpl(
|
|
416
|
-
`${streamUrl}${sep}
|
|
464
|
+
`${streamUrl}${sep}${new URLSearchParams({ sessionId, reducer: reducerName, wire: '2', ...(startAfter === undefined ? { bootstrap: '1' } : { index: String(startAfter) }) })}`,
|
|
417
465
|
{ headers: { accept: 'text/event-stream' }, signal },
|
|
418
466
|
)
|
|
419
467
|
if (!res.ok) throw await httpError(res)
|
|
@@ -453,7 +501,30 @@ const httpTransport = (
|
|
|
453
501
|
if (!data) continue
|
|
454
502
|
if (eventName === undefined) {
|
|
455
503
|
const parsed: unknown = JSON.parse(data)
|
|
456
|
-
|
|
504
|
+
// A version 2 server batches events; an older one sends one.
|
|
505
|
+
if (Array.isArray(parsed)) {
|
|
506
|
+
// Dropping a batch would leave a gap the frontier then skips past.
|
|
507
|
+
if (!parsed.every(isCompactWireEvent))
|
|
508
|
+
throw new A2ClientError(
|
|
509
|
+
'INVALID_PAYLOAD',
|
|
510
|
+
'invalid event batch',
|
|
511
|
+
)
|
|
512
|
+
yield {
|
|
513
|
+
kind: 'events',
|
|
514
|
+
events: parsed.map((row) =>
|
|
515
|
+
eventFromCompactWire(row, sessionId),
|
|
516
|
+
),
|
|
517
|
+
}
|
|
518
|
+
} else if (isWireEvent(parsed))
|
|
519
|
+
yield { kind: 'event', event: parsed }
|
|
520
|
+
} else if (eventName === 'a2-snapshot') {
|
|
521
|
+
const parsed: unknown = JSON.parse(data)
|
|
522
|
+
if (!isStateSnapshotFrame(parsed))
|
|
523
|
+
throw new A2ClientError(
|
|
524
|
+
'INVALID_PAYLOAD',
|
|
525
|
+
'invalid bootstrap snapshot',
|
|
526
|
+
)
|
|
527
|
+
yield parsed
|
|
457
528
|
} else if (presence && eventName === 'presence-snapshot') {
|
|
458
529
|
const parsed: unknown = JSON.parse(data)
|
|
459
530
|
if (isWirePresenceSnapshot(parsed)) {
|
|
@@ -515,7 +586,10 @@ const httpTransport = (
|
|
|
515
586
|
'history response was not a list of events',
|
|
516
587
|
)
|
|
517
588
|
}
|
|
518
|
-
|
|
589
|
+
const clearIndex = Number(res.headers.get('a2-clear-index') ?? Number.NaN)
|
|
590
|
+
return Number.isSafeInteger(clearIndex) && clearIndex > 0
|
|
591
|
+
? { events: rows, clearIndex }
|
|
592
|
+
: { events: rows }
|
|
519
593
|
},
|
|
520
594
|
|
|
521
595
|
sendPresence(envelope) {
|
|
@@ -622,21 +696,32 @@ const sessionWsTransport = (
|
|
|
622
696
|
const ready = socketReadyWaiters()
|
|
623
697
|
let nextReq = 1
|
|
624
698
|
|
|
625
|
-
const socketUrl = (
|
|
699
|
+
const socketUrl = (
|
|
700
|
+
sessionId: string,
|
|
701
|
+
startAfter: number | undefined,
|
|
702
|
+
reducerName: string,
|
|
703
|
+
): string => {
|
|
626
704
|
const resolved = webSocketUrl(sessionUrl(url, sessionId))
|
|
627
705
|
resolved.searchParams.set('sessionId', sessionId)
|
|
628
|
-
resolved.searchParams.
|
|
706
|
+
resolved.searchParams.delete('index')
|
|
707
|
+
resolved.searchParams.delete('bootstrap')
|
|
708
|
+
resolved.searchParams.set('reducer', reducerName)
|
|
709
|
+
resolved.searchParams.set('wire', '2')
|
|
710
|
+
if (startAfter === undefined) resolved.searchParams.set('bootstrap', '1')
|
|
711
|
+
else resolved.searchParams.set('index', String(startAfter))
|
|
629
712
|
return resolved.href
|
|
630
713
|
}
|
|
631
714
|
|
|
632
715
|
return {
|
|
633
|
-
async *connect({ sessionId, startAfter, signal }) {
|
|
716
|
+
async *connect({ sessionId, startAfter, reducer: reducerName, signal }) {
|
|
634
717
|
if (webSocketImpl === undefined) {
|
|
635
718
|
throw new TypeError(
|
|
636
719
|
'no WebSocket implementation available — pass one via createClient({ webSocket }) where the global is missing',
|
|
637
720
|
)
|
|
638
721
|
}
|
|
639
|
-
const socket = new webSocketImpl(
|
|
722
|
+
const socket = new webSocketImpl(
|
|
723
|
+
socketUrl(sessionId, startAfter, reducerName),
|
|
724
|
+
)
|
|
640
725
|
const connection: SocketConnection = { socket, pending: new Map() }
|
|
641
726
|
connections.set(sessionId, connection)
|
|
642
727
|
|
|
@@ -654,6 +739,7 @@ const sessionWsTransport = (
|
|
|
654
739
|
failure = error
|
|
655
740
|
if (connections.get(sessionId) === connection) {
|
|
656
741
|
connections.delete(sessionId)
|
|
742
|
+
ready.notify(sessionId)
|
|
657
743
|
}
|
|
658
744
|
rejectPendingAcks(connection.pending)
|
|
659
745
|
notify()
|
|
@@ -668,9 +754,18 @@ const sessionWsTransport = (
|
|
|
668
754
|
socket.addEventListener('message', (event) => {
|
|
669
755
|
if (connections.get(sessionId) !== connection || ended) return
|
|
670
756
|
if (typeof event.data !== 'string') return
|
|
671
|
-
|
|
757
|
+
let frame: SocketDownFrame | null
|
|
758
|
+
try {
|
|
759
|
+
frame = parseSocketFrame(event.data)
|
|
760
|
+
} catch (error) {
|
|
761
|
+
finish(asClientError(error))
|
|
762
|
+
return
|
|
763
|
+
}
|
|
672
764
|
if (frame === null) return
|
|
673
765
|
switch (frame.kind) {
|
|
766
|
+
case 'snapshot':
|
|
767
|
+
queue.push(frame)
|
|
768
|
+
break
|
|
674
769
|
case 'ack': {
|
|
675
770
|
const waiter = connection.pending.get(frame.req)
|
|
676
771
|
if (waiter === undefined) return
|
|
@@ -696,6 +791,15 @@ const sessionWsTransport = (
|
|
|
696
791
|
if (frame.event.sessionId !== sessionId) return
|
|
697
792
|
queue.push({ kind: 'event', event: frame.event })
|
|
698
793
|
break
|
|
794
|
+
case 'events':
|
|
795
|
+
if (frame.sessionId !== undefined) return
|
|
796
|
+
queue.push({
|
|
797
|
+
kind: 'events',
|
|
798
|
+
events: frame.events.map((row) =>
|
|
799
|
+
eventFromCompactWire(row, sessionId),
|
|
800
|
+
),
|
|
801
|
+
})
|
|
802
|
+
break
|
|
699
803
|
case 'presence':
|
|
700
804
|
if (presence && frame.sessionId === undefined) {
|
|
701
805
|
queue.push({ kind: 'presence', patch: frame.patch })
|
|
@@ -834,7 +938,12 @@ const sessionWsTransport = (
|
|
|
834
938
|
* attempt, so `startAfter` is that attempt's resume frontier. */
|
|
835
939
|
type WsChannel = {
|
|
836
940
|
sessionId: string
|
|
837
|
-
|
|
941
|
+
/** Echoed on this lane's subscribe answers. An answer to an earlier
|
|
942
|
+
* attempt for the same session must not confirm or end this one;
|
|
943
|
+
* data frames from that attempt precede this attempt's confirmation. */
|
|
944
|
+
attempt: number
|
|
945
|
+
startAfter: number | undefined
|
|
946
|
+
reducer: string
|
|
838
947
|
queue: TransportFrame[]
|
|
839
948
|
/** The server confirmed the subscribe — pushes and presence ride. */
|
|
840
949
|
live: boolean
|
|
@@ -850,9 +959,18 @@ const wakeChannel = (channel: WsChannel): void => {
|
|
|
850
959
|
channel.wake = null
|
|
851
960
|
}
|
|
852
961
|
|
|
853
|
-
const
|
|
854
|
-
|
|
855
|
-
|
|
962
|
+
const answersAttempt = (
|
|
963
|
+
frame: { attempt?: number },
|
|
964
|
+
channel: WsChannel,
|
|
965
|
+
): boolean => frame.attempt === undefined || frame.attempt === channel.attempt
|
|
966
|
+
|
|
967
|
+
const subscriptionStart = (
|
|
968
|
+
index: number | undefined,
|
|
969
|
+
reducer: string,
|
|
970
|
+
): SubscriptionStart =>
|
|
971
|
+
index === undefined ? { bootstrap: true, reducer } : { index, reducer }
|
|
972
|
+
|
|
973
|
+
const subscribeFrame = (sessions: SocketSubscribeFrame['sessions']): string => {
|
|
856
974
|
const frame: SocketSubscribeFrame = { kind: 'subscribe', sessions }
|
|
857
975
|
return JSON.stringify(frame)
|
|
858
976
|
}
|
|
@@ -874,6 +992,7 @@ const multiplexedWsTransport = (
|
|
|
874
992
|
let socketOpen = false
|
|
875
993
|
let watchdog: ReturnType<typeof setTimeout> | undefined
|
|
876
994
|
let nextReq = 1
|
|
995
|
+
let nextAttempt = 1
|
|
877
996
|
|
|
878
997
|
const trySend = (data: string): void => {
|
|
879
998
|
if (socket === null || !socketOpen) return
|
|
@@ -926,6 +1045,7 @@ const multiplexedWsTransport = (
|
|
|
926
1045
|
const detach = (channel: WsChannel): void => {
|
|
927
1046
|
if (channels.get(channel.sessionId) !== channel) return
|
|
928
1047
|
channels.delete(channel.sessionId)
|
|
1048
|
+
ready.notify(channel.sessionId)
|
|
929
1049
|
if (!channel.dropped) {
|
|
930
1050
|
const frame: SocketUnsubscribeFrame = {
|
|
931
1051
|
kind: 'unsubscribe',
|
|
@@ -964,6 +1084,11 @@ const multiplexedWsTransport = (
|
|
|
964
1084
|
|
|
965
1085
|
const route = (frame: SocketDownFrame): void => {
|
|
966
1086
|
switch (frame.kind) {
|
|
1087
|
+
case 'snapshot': {
|
|
1088
|
+
const channel = channels.get(frame.sessionId)
|
|
1089
|
+
if (channel && channel.live && !channel.ended) deliver(channel, frame)
|
|
1090
|
+
return
|
|
1091
|
+
}
|
|
967
1092
|
case 'ping': {
|
|
968
1093
|
// Global liveness — every live channel's stall watchdog feeds
|
|
969
1094
|
// on it, the way each SSE stream feeds on its own heartbeat.
|
|
@@ -976,7 +1101,13 @@ const multiplexedWsTransport = (
|
|
|
976
1101
|
}
|
|
977
1102
|
case 'subscribed': {
|
|
978
1103
|
const channel = channels.get(frame.sessionId)
|
|
979
|
-
if (
|
|
1104
|
+
if (
|
|
1105
|
+
channel === undefined ||
|
|
1106
|
+
channel.ended ||
|
|
1107
|
+
channel.live ||
|
|
1108
|
+
!answersAttempt(frame, channel)
|
|
1109
|
+
)
|
|
1110
|
+
return
|
|
980
1111
|
channel.live = true
|
|
981
1112
|
deliver(channel, { kind: 'ping' }) // the connected signal
|
|
982
1113
|
ready.notify(frame.sessionId)
|
|
@@ -984,7 +1115,7 @@ const multiplexedWsTransport = (
|
|
|
984
1115
|
}
|
|
985
1116
|
case 'unsubscribed': {
|
|
986
1117
|
const channel = channels.get(frame.sessionId)
|
|
987
|
-
if (channel === undefined) return
|
|
1118
|
+
if (channel === undefined || !answersAttempt(frame, channel)) return
|
|
988
1119
|
channel.dropped = true
|
|
989
1120
|
settleChannel(
|
|
990
1121
|
channel,
|
|
@@ -1013,6 +1144,20 @@ const multiplexedWsTransport = (
|
|
|
1013
1144
|
}
|
|
1014
1145
|
return
|
|
1015
1146
|
}
|
|
1147
|
+
case 'events': {
|
|
1148
|
+
const { sessionId } = frame
|
|
1149
|
+
if (sessionId === undefined) return
|
|
1150
|
+
const channel = channels.get(sessionId)
|
|
1151
|
+
if (channel !== undefined && channel.live && !channel.ended) {
|
|
1152
|
+
deliver(channel, {
|
|
1153
|
+
kind: 'events',
|
|
1154
|
+
events: frame.events.map((row) =>
|
|
1155
|
+
eventFromCompactWire(row, sessionId),
|
|
1156
|
+
),
|
|
1157
|
+
})
|
|
1158
|
+
}
|
|
1159
|
+
return
|
|
1160
|
+
}
|
|
1016
1161
|
case 'presence': {
|
|
1017
1162
|
// Contracts that declare no presence skip presence frames,
|
|
1018
1163
|
// exactly like the http transport.
|
|
@@ -1044,7 +1189,9 @@ const multiplexedWsTransport = (
|
|
|
1044
1189
|
'no WebSocket implementation available — pass one via createClient({ webSocket }) where the global is missing',
|
|
1045
1190
|
)
|
|
1046
1191
|
}
|
|
1047
|
-
const
|
|
1192
|
+
const target = webSocketUrl(url)
|
|
1193
|
+
target.searchParams.set('wire', '2')
|
|
1194
|
+
const created = new webSocketImpl(target.href)
|
|
1048
1195
|
socket = created
|
|
1049
1196
|
socketOpen = false
|
|
1050
1197
|
armWatchdog(created)
|
|
@@ -1056,7 +1203,8 @@ const multiplexedWsTransport = (
|
|
|
1056
1203
|
.filter((channel) => !channel.ended)
|
|
1057
1204
|
.map((channel) => ({
|
|
1058
1205
|
id: channel.sessionId,
|
|
1059
|
-
|
|
1206
|
+
attempt: channel.attempt,
|
|
1207
|
+
...subscriptionStart(channel.startAfter, channel.reducer),
|
|
1060
1208
|
}))
|
|
1061
1209
|
if (sessions.length > 0) trySend(subscribeFrame(sessions))
|
|
1062
1210
|
})
|
|
@@ -1064,7 +1212,13 @@ const multiplexedWsTransport = (
|
|
|
1064
1212
|
if (socket !== created) return
|
|
1065
1213
|
if (typeof event.data !== 'string') return
|
|
1066
1214
|
armWatchdog(created)
|
|
1067
|
-
|
|
1215
|
+
let frame: SocketDownFrame | null
|
|
1216
|
+
try {
|
|
1217
|
+
frame = parseSocketFrame(event.data)
|
|
1218
|
+
} catch (error) {
|
|
1219
|
+
failSocket(asClientError(error))
|
|
1220
|
+
return
|
|
1221
|
+
}
|
|
1068
1222
|
if (frame === null) return
|
|
1069
1223
|
route(frame)
|
|
1070
1224
|
})
|
|
@@ -1087,10 +1241,12 @@ const multiplexedWsTransport = (
|
|
|
1087
1241
|
}
|
|
1088
1242
|
|
|
1089
1243
|
return {
|
|
1090
|
-
async *connect({ sessionId, startAfter, signal }) {
|
|
1244
|
+
async *connect({ sessionId, startAfter, reducer: reducerName, signal }) {
|
|
1091
1245
|
const channel: WsChannel = {
|
|
1092
1246
|
sessionId,
|
|
1247
|
+
attempt: nextAttempt++,
|
|
1093
1248
|
startAfter,
|
|
1249
|
+
reducer: reducerName,
|
|
1094
1250
|
queue: [],
|
|
1095
1251
|
live: false,
|
|
1096
1252
|
dropped: false,
|
|
@@ -1102,7 +1258,15 @@ const multiplexedWsTransport = (
|
|
|
1102
1258
|
try {
|
|
1103
1259
|
ensureSocket()
|
|
1104
1260
|
if (socketOpen) {
|
|
1105
|
-
trySend(
|
|
1261
|
+
trySend(
|
|
1262
|
+
subscribeFrame([
|
|
1263
|
+
{
|
|
1264
|
+
id: sessionId,
|
|
1265
|
+
attempt: channel.attempt,
|
|
1266
|
+
...subscriptionStart(startAfter, reducerName),
|
|
1267
|
+
},
|
|
1268
|
+
]),
|
|
1269
|
+
)
|
|
1106
1270
|
}
|
|
1107
1271
|
} catch (error) {
|
|
1108
1272
|
if (channels.get(sessionId) === channel) channels.delete(sessionId)
|
|
@@ -1261,20 +1425,40 @@ type Pending<D extends EventDefs> = {
|
|
|
1261
1425
|
/** Set once the POST ack returns; the entry leaves the overlay when
|
|
1262
1426
|
* the stream delivers its index. */
|
|
1263
1427
|
acked?: ContractEvent<D>
|
|
1428
|
+
sent?: true
|
|
1429
|
+
/** Sent before the current baseline was adopted and not yet acked:
|
|
1430
|
+
* the baseline may already contain it, so only its ack index can
|
|
1431
|
+
* place it in the overlay. */
|
|
1432
|
+
heldUntilAck?: true
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
type ResolvedSessionOptions<D extends EventDefs, S> = Omit<
|
|
1436
|
+
SessionOptions<D, S>,
|
|
1437
|
+
'initial'
|
|
1438
|
+
> & {
|
|
1439
|
+
initial?: StateSnapshot<S> | null | undefined
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
type PendingInitial<D extends EventDefs, S> = {
|
|
1443
|
+
source: Promise<StateSnapshot<S> | null | undefined>
|
|
1444
|
+
version: number
|
|
1445
|
+
settings: Omit<SessionOptions<D, S>, 'initial'>
|
|
1446
|
+
cancel(): void
|
|
1447
|
+
error?: A2ClientError
|
|
1264
1448
|
}
|
|
1265
1449
|
|
|
1266
1450
|
type SessionRuntime<
|
|
1267
1451
|
D extends EventDefs,
|
|
1268
1452
|
S,
|
|
1269
1453
|
P extends PresenceDefs,
|
|
1270
|
-
|
|
1271
|
-
> = SessionClient<D, S, P, V> & {
|
|
1454
|
+
> = SessionClient<D, S, P> & {
|
|
1272
1455
|
hydrate(options?: SessionOptions<D, S>): void
|
|
1456
|
+
resolveInitial(options: { source: object; value: unknown }): void
|
|
1273
1457
|
canEvict(): boolean
|
|
1274
1458
|
}
|
|
1275
1459
|
|
|
1276
|
-
type CachedSession<D extends EventDefs, S, P extends PresenceDefs
|
|
1277
|
-
runtime: SessionRuntime<D, S, P
|
|
1460
|
+
type CachedSession<D extends EventDefs, S, P extends PresenceDefs> = {
|
|
1461
|
+
runtime: SessionRuntime<D, S, P>
|
|
1278
1462
|
timer?: ReturnType<typeof setTimeout>
|
|
1279
1463
|
}
|
|
1280
1464
|
|
|
@@ -1282,10 +1466,14 @@ export function createClient<
|
|
|
1282
1466
|
D extends EventDefs,
|
|
1283
1467
|
S,
|
|
1284
1468
|
P extends PresenceDefs = Record<never, never>,
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
const
|
|
1288
|
-
|
|
1469
|
+
>(options: CreateClientOptions<D, S, P>): A2Client<D, S, P> {
|
|
1470
|
+
const { contract, api } = options
|
|
1471
|
+
const reducer = contract?.reducer
|
|
1472
|
+
if (!reducer)
|
|
1473
|
+
throw new TypeError(
|
|
1474
|
+
'createClient requires a contract with a public reducer',
|
|
1475
|
+
)
|
|
1476
|
+
const presenceDefs: Readonly<PresenceDefs> = contract.presence
|
|
1289
1477
|
const declaresPresence = Object.keys(presenceDefs).length > 0
|
|
1290
1478
|
const fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis)
|
|
1291
1479
|
const clientFetch: typeof globalThis.fetch = async (input, init) => {
|
|
@@ -1308,7 +1496,7 @@ export function createClient<
|
|
|
1308
1496
|
if (Number.isNaN(gcTime) || gcTime < 0) {
|
|
1309
1497
|
throw new RangeError('gcTime must be a non-negative number or Infinity')
|
|
1310
1498
|
}
|
|
1311
|
-
const sessions = new Map<string, CachedSession<D, S, P
|
|
1499
|
+
const sessions = new Map<string, CachedSession<D, S, P>>()
|
|
1312
1500
|
|
|
1313
1501
|
const scheduleGc = (sessionId: string, delay = gcTime): void => {
|
|
1314
1502
|
const entry = sessions.get(sessionId)
|
|
@@ -1319,6 +1507,8 @@ export function createClient<
|
|
|
1319
1507
|
const current = sessions.get(sessionId)
|
|
1320
1508
|
if (current !== entry) return
|
|
1321
1509
|
if (entry.runtime.canEvict()) {
|
|
1510
|
+
entry.runtime.close()
|
|
1511
|
+
clearTimeout(entry.timer)
|
|
1322
1512
|
sessions.delete(sessionId)
|
|
1323
1513
|
return
|
|
1324
1514
|
}
|
|
@@ -1336,21 +1526,26 @@ export function createClient<
|
|
|
1336
1526
|
const makeSession = (
|
|
1337
1527
|
sessionId: string,
|
|
1338
1528
|
sessionOptions?: SessionOptions<D, S>,
|
|
1339
|
-
): SessionRuntime<D, S, P
|
|
1529
|
+
): SessionRuntime<D, S, P> => {
|
|
1530
|
+
assertSessionSeed(sessionOptions)
|
|
1340
1531
|
// ── server truth ─────────────────────────────────────────────
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
let
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1532
|
+
const supplied = sessionOptions?.initial
|
|
1533
|
+
const initial = isInitialPromise(supplied) ? undefined : supplied
|
|
1534
|
+
let hydrated = initial != null
|
|
1535
|
+
let bootstrapGeneration: number | undefined
|
|
1536
|
+
let stateIndex = initial?.index ?? 0
|
|
1537
|
+
let streamIndex = initial?.index
|
|
1538
|
+
let foldedState: S = initial == null ? reducer.initialState : initial.state
|
|
1539
|
+
/** The latest clear marker this session has learned of, or 0. */
|
|
1540
|
+
let clearIndex = initial?.clearIndex ?? 0
|
|
1541
|
+
let initialVersion = 0
|
|
1542
|
+
const seenInitials = new WeakSet<object>()
|
|
1543
|
+
if (initial != null) seenInitials.add(initial)
|
|
1544
|
+
let preload: PendingInitial<D, S> | undefined
|
|
1350
1545
|
const serverEvents: ContractEvent<D>[] = (
|
|
1351
1546
|
sessionOptions?.initialEvents ?? []
|
|
1352
1547
|
)
|
|
1353
|
-
.filter((event) => event.index <= stateIndex)
|
|
1548
|
+
.filter((event) => event.index <= stateIndex && event.index > clearIndex)
|
|
1354
1549
|
.toSorted((a, b) => a.index - b.index)
|
|
1355
1550
|
|
|
1356
1551
|
// ── optimistic overlay ───────────────────────────────────────
|
|
@@ -1378,13 +1573,15 @@ export function createClient<
|
|
|
1378
1573
|
|
|
1379
1574
|
// ── store plumbing ───────────────────────────────────────────
|
|
1380
1575
|
const listeners = new Set<() => void>()
|
|
1381
|
-
let snapshot: SessionSnapshot<D,
|
|
1576
|
+
let snapshot: SessionSnapshot<D, S, P> | null = null
|
|
1382
1577
|
let status: ConnectionStatus = 'idle'
|
|
1383
1578
|
let reconnects = 0
|
|
1384
1579
|
let lastError: A2ClientError | null = null
|
|
1385
1580
|
let inFlightPushes = 0
|
|
1386
1581
|
let pushWireTail: Promise<void> = Promise.resolve()
|
|
1387
|
-
let deferredHydration:
|
|
1582
|
+
let deferredHydration:
|
|
1583
|
+
{ settings: ResolvedSessionOptions<D, S>; version?: number } | undefined
|
|
1584
|
+
let resumeInitialError: A2ClientError | undefined
|
|
1388
1585
|
let hydrationNotification: ReturnType<typeof setTimeout> | undefined
|
|
1389
1586
|
|
|
1390
1587
|
const emit = (): void => {
|
|
@@ -1398,14 +1595,21 @@ export function createClient<
|
|
|
1398
1595
|
emit()
|
|
1399
1596
|
}
|
|
1400
1597
|
|
|
1401
|
-
const notifyHydrated = (): void => {
|
|
1598
|
+
const notifyHydrated = (resume?: A2ClientError): void => {
|
|
1599
|
+
resumeInitialError ??= resume
|
|
1402
1600
|
// `session(id, serverFold)` is also called while a destination
|
|
1403
1601
|
// provider renders. Its own snapshot must advance synchronously,
|
|
1404
1602
|
// but notifying an outgoing provider during that render would be a
|
|
1405
1603
|
// React setState-in-render violation.
|
|
1406
1604
|
snapshot = null
|
|
1407
1605
|
clearTimeout(hydrationNotification)
|
|
1408
|
-
hydrationNotification = setTimeout(
|
|
1606
|
+
hydrationNotification = setTimeout(() => {
|
|
1607
|
+
const failure = resumeInitialError
|
|
1608
|
+
resumeInitialError = undefined
|
|
1609
|
+
if (failure && active && status === 'paused' && lastError === failure)
|
|
1610
|
+
reconnectStream()
|
|
1611
|
+
else emit()
|
|
1612
|
+
}, 0)
|
|
1409
1613
|
;(hydrationNotification as { unref?: () => void }).unref?.()
|
|
1410
1614
|
}
|
|
1411
1615
|
|
|
@@ -1417,7 +1621,8 @@ export function createClient<
|
|
|
1417
1621
|
const byIndex = new Map(serverEvents.map((event) => [event.index, event]))
|
|
1418
1622
|
let changed = false
|
|
1419
1623
|
for (const event of events) {
|
|
1420
|
-
|
|
1624
|
+
// Also drops rows from responses that began before a newer clear.
|
|
1625
|
+
if (event.index > throughIndex || event.index <= clearIndex) continue
|
|
1421
1626
|
const existing = byIndex.get(event.index)
|
|
1422
1627
|
if (existing?.id === event.id) continue
|
|
1423
1628
|
byIndex.set(event.index, event)
|
|
@@ -1432,6 +1637,22 @@ export function createClient<
|
|
|
1432
1637
|
return true
|
|
1433
1638
|
}
|
|
1434
1639
|
|
|
1640
|
+
/**
|
|
1641
|
+
* Raise the latest known clear marker (a2-implementation.md §17.4):
|
|
1642
|
+
* drop the served history it ended and every push acked below it.
|
|
1643
|
+
* The boundary never moves backward.
|
|
1644
|
+
*/
|
|
1645
|
+
const applyClearBoundary = (index: number): boolean => {
|
|
1646
|
+
if (index <= clearIndex) return false
|
|
1647
|
+
clearIndex = index
|
|
1648
|
+
const kept = serverEvents.filter((event) => event.index > index)
|
|
1649
|
+
serverEvents.splice(0, serverEvents.length, ...kept)
|
|
1650
|
+
pending = pending.filter(
|
|
1651
|
+
(entry) => entry.acked === undefined || entry.acked.index > index,
|
|
1652
|
+
)
|
|
1653
|
+
return true
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1435
1656
|
const overlayEvents = (): ContractEvent<D>[] => {
|
|
1436
1657
|
// Acked entries first (they have a server-assigned order), then
|
|
1437
1658
|
// unacked in push order with provisional indexes.
|
|
@@ -1440,19 +1661,20 @@ export function createClient<
|
|
|
1440
1661
|
.map((p) => p.acked!)
|
|
1441
1662
|
.toSorted((a, b) => a.index - b.index)
|
|
1442
1663
|
const maxKnown = Math.max(stateIndex, acked.at(-1)?.index ?? stateIndex)
|
|
1443
|
-
const
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1664
|
+
const waiting = pending.filter((p) => !p.acked)
|
|
1665
|
+
const held = waiting.findIndex((p) => p.heldUntilAck)
|
|
1666
|
+
// Later pushes may depend on a held one, so they stay hidden with it.
|
|
1667
|
+
const unacked = (held === -1 ? waiting : waiting.slice(0, held)).map(
|
|
1668
|
+
(p, i) =>
|
|
1669
|
+
({
|
|
1670
|
+
id: p.id,
|
|
1671
|
+
type: p.type,
|
|
1672
|
+
payload: p.payload,
|
|
1673
|
+
index: maxKnown + 1 + i, // provisional
|
|
1674
|
+
sessionId,
|
|
1675
|
+
createdAt: p.createdAt,
|
|
1676
|
+
}) as ContractEvent<D>,
|
|
1677
|
+
)
|
|
1456
1678
|
return [...acked, ...unacked]
|
|
1457
1679
|
}
|
|
1458
1680
|
|
|
@@ -1474,17 +1696,20 @@ export function createClient<
|
|
|
1474
1696
|
const oldestLoaded = serverEvents[0]?.index ?? null
|
|
1475
1697
|
return {
|
|
1476
1698
|
loading: historyLoads > 0,
|
|
1477
|
-
//
|
|
1478
|
-
//
|
|
1479
|
-
|
|
1699
|
+
// The first event of the lifetime loaded — or a state index at
|
|
1700
|
+
// its start, an empty lifetime as far as this client knows —
|
|
1701
|
+
// means nothing older is left to fetch.
|
|
1702
|
+
complete:
|
|
1703
|
+
hydrated &&
|
|
1704
|
+
(oldestLoaded === clearIndex + 1 || stateIndex === clearIndex),
|
|
1480
1705
|
oldestLoaded,
|
|
1481
1706
|
}
|
|
1482
1707
|
}
|
|
1483
1708
|
|
|
1484
|
-
const buildSnapshot = (): SessionSnapshot<D,
|
|
1709
|
+
const buildSnapshot = (): SessionSnapshot<D, S, P> => {
|
|
1485
1710
|
const overlay = overlayEvents()
|
|
1486
1711
|
let state = foldedState
|
|
1487
|
-
for (const event of overlay) {
|
|
1712
|
+
for (const event of hydrated ? overlay : []) {
|
|
1488
1713
|
if (event.index > stateIndex) state = reducer.fold(state, event)
|
|
1489
1714
|
}
|
|
1490
1715
|
const displayedOverlay = overlay.filter((event) => {
|
|
@@ -1501,8 +1726,8 @@ export function createClient<
|
|
|
1501
1726
|
return true
|
|
1502
1727
|
})
|
|
1503
1728
|
return {
|
|
1504
|
-
state:
|
|
1505
|
-
|
|
1729
|
+
state: projectClientState({ reducer, state }),
|
|
1730
|
+
hydrated,
|
|
1506
1731
|
events: [...serverEvents, ...displayedOverlay],
|
|
1507
1732
|
index: stateIndex,
|
|
1508
1733
|
history: historyState(),
|
|
@@ -1510,7 +1735,7 @@ export function createClient<
|
|
|
1510
1735
|
...(declaresPresence
|
|
1511
1736
|
? { presence: presenceState as PresenceMap<P> }
|
|
1512
1737
|
: {}),
|
|
1513
|
-
} as SessionSnapshot<D,
|
|
1738
|
+
} as SessionSnapshot<D, S, P>
|
|
1514
1739
|
}
|
|
1515
1740
|
|
|
1516
1741
|
// ── presence ─────────────────────────────────────────────────
|
|
@@ -1696,28 +1921,93 @@ export function createClient<
|
|
|
1696
1921
|
touch(sessionId)
|
|
1697
1922
|
}
|
|
1698
1923
|
|
|
1699
|
-
/** Pushes awaiting stream confirmation — resolved
|
|
1700
|
-
*
|
|
1701
|
-
let confirmWatchers: Array<{
|
|
1924
|
+
/** Pushes awaiting stream confirmation — resolved the moment the
|
|
1925
|
+
* stream, by event or bootstrap snapshot, passes their batch. */
|
|
1926
|
+
let confirmWatchers: Array<{
|
|
1927
|
+
index: number
|
|
1928
|
+
resolve: () => void
|
|
1929
|
+
lease?: true
|
|
1930
|
+
}> = []
|
|
1931
|
+
|
|
1932
|
+
const resolveConfirmed = (): void => {
|
|
1933
|
+
const delivered = streamIndex ?? 0
|
|
1934
|
+
const due = confirmWatchers.filter(
|
|
1935
|
+
(watcher) => watcher.index <= delivered,
|
|
1936
|
+
)
|
|
1937
|
+
if (due.length === 0) return
|
|
1938
|
+
confirmWatchers = confirmWatchers.filter(
|
|
1939
|
+
(watcher) => watcher.index > delivered,
|
|
1940
|
+
)
|
|
1941
|
+
for (const watcher of due) watcher.resolve()
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
// State hydration does not subsume delivery: unobserved ACKs still
|
|
1945
|
+
// keep an idle stream catching up even when their state is covered.
|
|
1946
|
+
const retireCoveredPending = (): void => {
|
|
1947
|
+
pending = pending.filter(
|
|
1948
|
+
(entry) =>
|
|
1949
|
+
!entry.acked ||
|
|
1950
|
+
entry.acked.index > Math.min(stateIndex, streamIndex ?? stateIndex),
|
|
1951
|
+
)
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
const adoptBaseline = (baseline: StateSnapshot<S>): void => {
|
|
1955
|
+
hydrated = true
|
|
1956
|
+
stateIndex = baseline.index
|
|
1957
|
+
foldedState = baseline.state
|
|
1958
|
+
for (const entry of pending)
|
|
1959
|
+
if (entry.sent && !entry.acked) entry.heldUntilAck = true
|
|
1960
|
+
retireCoveredPending()
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
const bootstrapState = (frame: StateSnapshotFrame): S => {
|
|
1964
|
+
let state: S
|
|
1965
|
+
try {
|
|
1966
|
+
state = decodeState({
|
|
1967
|
+
reducer,
|
|
1968
|
+
state: frame.stateUndefined ? undefined : frame.state,
|
|
1969
|
+
}) as S
|
|
1970
|
+
} catch (error) {
|
|
1971
|
+
throw asClientError(error, { code: 'INVALID_PAYLOAD' })
|
|
1972
|
+
}
|
|
1973
|
+
if (!reducer.stateSchema) return state
|
|
1974
|
+
const result = validateSync(
|
|
1975
|
+
reducer.stateSchema,
|
|
1976
|
+
state,
|
|
1977
|
+
'bootstrap stateSchema',
|
|
1978
|
+
)
|
|
1979
|
+
if (result.issues)
|
|
1980
|
+
throw new A2ClientError(
|
|
1981
|
+
'INVALID_PAYLOAD',
|
|
1982
|
+
'bootstrap state failed stateSchema',
|
|
1983
|
+
{ details: result.issues },
|
|
1984
|
+
)
|
|
1985
|
+
return result.value
|
|
1986
|
+
}
|
|
1702
1987
|
|
|
1703
1988
|
/** The single ingest point: every server-confirmed event, in log
|
|
1704
1989
|
* order, from the stream. */
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1990
|
+
// A batch is folded whole before listeners hear about it, once.
|
|
1991
|
+
const ingest = (events: readonly ContractEvent<D>[]): void => {
|
|
1992
|
+
let delivered = false
|
|
1993
|
+
for (const event of events) {
|
|
1994
|
+
if (event.index <= (streamIndex ?? 0)) continue
|
|
1995
|
+
delivered = true
|
|
1996
|
+
streamIndex = event.index
|
|
1997
|
+
const cleared = isSessionCleared(event)
|
|
1998
|
+
// A marker at or below a newer hydrated fold is already reflected in it.
|
|
1999
|
+
if (event.index > stateIndex) {
|
|
2000
|
+
stateIndex = event.index
|
|
2001
|
+
if (!cleared) serverEvents.push(event)
|
|
2002
|
+
foldedState = foldEvent(reducer, foldedState, event)
|
|
2003
|
+
} else if (!cleared) {
|
|
2004
|
+
mergeServerEvents([event], stateIndex)
|
|
2005
|
+
}
|
|
2006
|
+
if (cleared) applyClearBoundary(event.index)
|
|
2007
|
+
pending = pending.filter((p) => p.id !== event.id)
|
|
1720
2008
|
}
|
|
2009
|
+
if (!delivered) return
|
|
2010
|
+
resolveConfirmed()
|
|
1721
2011
|
notify()
|
|
1722
2012
|
touch(sessionId)
|
|
1723
2013
|
}
|
|
@@ -1727,8 +2017,8 @@ export function createClient<
|
|
|
1727
2017
|
events: AppendInput<D>[],
|
|
1728
2018
|
): Array<{ id: string; type: string; payload: unknown }> =>
|
|
1729
2019
|
events.map((event) => {
|
|
1730
|
-
const schema = Object.hasOwn(
|
|
1731
|
-
?
|
|
2020
|
+
const schema = Object.hasOwn(contract.events, event.type)
|
|
2021
|
+
? contract.events[event.type]
|
|
1732
2022
|
: undefined
|
|
1733
2023
|
if (!schema) {
|
|
1734
2024
|
throw new A2ClientError(
|
|
@@ -1804,7 +2094,7 @@ export function createClient<
|
|
|
1804
2094
|
(acked) =>
|
|
1805
2095
|
new Promise<ContractEvent<D>[]>((resolve) => {
|
|
1806
2096
|
const last = acked.at(-1)?.index ?? 0
|
|
1807
|
-
if (
|
|
2097
|
+
if ((streamIndex ?? 0) >= last) {
|
|
1808
2098
|
resolve(acked)
|
|
1809
2099
|
return
|
|
1810
2100
|
}
|
|
@@ -1819,6 +2109,26 @@ export function createClient<
|
|
|
1819
2109
|
})
|
|
1820
2110
|
}
|
|
1821
2111
|
|
|
2112
|
+
// A cold socket opens only after a pending initial Promise settles.
|
|
2113
|
+
const initialSettled = (epoch: number): Promise<void> =>
|
|
2114
|
+
new Promise((resolve, reject) => {
|
|
2115
|
+
const check = (): void => {
|
|
2116
|
+
const failure =
|
|
2117
|
+
leaseEpoch !== epoch || status === 'closed'
|
|
2118
|
+
? new DOMException('session closed', 'AbortError')
|
|
2119
|
+
: !hydrated && preload?.error
|
|
2120
|
+
if (failure) {
|
|
2121
|
+
listeners.delete(check)
|
|
2122
|
+
reject(failure)
|
|
2123
|
+
} else if (hydrated || !preload) {
|
|
2124
|
+
listeners.delete(check)
|
|
2125
|
+
resolve()
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
listeners.add(check)
|
|
2129
|
+
check()
|
|
2130
|
+
})
|
|
2131
|
+
|
|
1822
2132
|
const push = (...events: AppendInput<D>[]): PushResult<D> => {
|
|
1823
2133
|
touch(sessionId)
|
|
1824
2134
|
return withConfirmed(
|
|
@@ -1841,11 +2151,27 @@ export function createClient<
|
|
|
1841
2151
|
})
|
|
1842
2152
|
pending.push(...entries)
|
|
1843
2153
|
inFlightPushes += 1
|
|
2154
|
+
let acknowledgedIndex: number | undefined
|
|
2155
|
+
const pushEpoch = leaseEpoch
|
|
2156
|
+
const releaseConnection =
|
|
2157
|
+
!hydrated || transport.awaitReady || active
|
|
2158
|
+
? runtime.connect()
|
|
2159
|
+
: undefined
|
|
1844
2160
|
|
|
1845
2161
|
try {
|
|
1846
2162
|
refreshIdleStream()
|
|
1847
2163
|
notify() // optimistic — the view updates before the network moves
|
|
1848
2164
|
await waitForPriorPush
|
|
2165
|
+
if (transport.awaitReady) {
|
|
2166
|
+
await initialSettled(pushEpoch)
|
|
2167
|
+
await transport.awaitReady(
|
|
2168
|
+
sessionId,
|
|
2169
|
+
STREAM_TIMINGS.stallTimeoutMs,
|
|
2170
|
+
)
|
|
2171
|
+
}
|
|
2172
|
+
if (leaseEpoch !== pushEpoch)
|
|
2173
|
+
throw new DOMException('session closed', 'AbortError')
|
|
2174
|
+
for (const entry of entries) entry.sent = true
|
|
1849
2175
|
const acked = await post({
|
|
1850
2176
|
sessionId,
|
|
1851
2177
|
events: entries.map(({ id, type, payload }) => ({
|
|
@@ -1854,16 +2180,12 @@ export function createClient<
|
|
|
1854
2180
|
payload,
|
|
1855
2181
|
})),
|
|
1856
2182
|
})
|
|
2183
|
+
acknowledgedIndex = acked.at(-1)?.index
|
|
1857
2184
|
for (const event of acked) {
|
|
1858
2185
|
const entry = pending.find((p) => p.id === event.id)
|
|
1859
2186
|
if (entry) entry.acked = event // stream retires it at its index
|
|
1860
2187
|
}
|
|
1861
|
-
|
|
1862
|
-
(entry) =>
|
|
1863
|
-
entry.acked === undefined ||
|
|
1864
|
-
entry.acked.index >
|
|
1865
|
-
Math.min(stateIndex, streamIndex ?? stateIndex),
|
|
1866
|
-
)
|
|
2188
|
+
retireCoveredPending()
|
|
1867
2189
|
if ((acked.at(-1)?.index ?? 0) > (streamIndex ?? stateIndex))
|
|
1868
2190
|
refreshIdleStream()
|
|
1869
2191
|
notify()
|
|
@@ -1875,12 +2197,25 @@ export function createClient<
|
|
|
1875
2197
|
notify()
|
|
1876
2198
|
throw err
|
|
1877
2199
|
} finally {
|
|
2200
|
+
if (
|
|
2201
|
+
releaseConnection &&
|
|
2202
|
+
pushEpoch === leaseEpoch &&
|
|
2203
|
+
acknowledgedIndex !== undefined &&
|
|
2204
|
+
acknowledgedIndex > (streamIndex ?? 0)
|
|
2205
|
+
) {
|
|
2206
|
+
confirmWatchers.push({
|
|
2207
|
+
index: acknowledgedIndex,
|
|
2208
|
+
resolve: releaseConnection,
|
|
2209
|
+
lease: true,
|
|
2210
|
+
})
|
|
2211
|
+
} else releaseConnection?.()
|
|
1878
2212
|
releasePush()
|
|
1879
2213
|
inFlightPushes -= 1
|
|
1880
2214
|
if (inFlightPushes === 0 && deferredHydration) {
|
|
1881
2215
|
const next = deferredHydration
|
|
1882
2216
|
deferredHydration = undefined
|
|
1883
|
-
|
|
2217
|
+
if (next.version === undefined || next.version === initialVersion)
|
|
2218
|
+
if (applyInitial(next)) notifyHydrated()
|
|
1884
2219
|
}
|
|
1885
2220
|
touch(sessionId)
|
|
1886
2221
|
}
|
|
@@ -1944,16 +2279,20 @@ export function createClient<
|
|
|
1944
2279
|
// everything above it, and the optimistic overlay must never
|
|
1945
2280
|
// collide with a backscrolled row.
|
|
1946
2281
|
const lte = Math.min(upper - 1, stateIndex)
|
|
1947
|
-
const gte = Math.max(
|
|
2282
|
+
const gte = Math.max(
|
|
2283
|
+
clearIndex + 1,
|
|
2284
|
+
upper - (limit ?? DEFAULT_HISTORY_LIMIT),
|
|
2285
|
+
)
|
|
1948
2286
|
if (gte > lte) return []
|
|
1949
2287
|
const missing = missingBetween(gte, lte)
|
|
1950
2288
|
if (missing === null) return eventsBetween(gte, lte)
|
|
1951
2289
|
historyLoads += 1
|
|
1952
2290
|
notify()
|
|
1953
2291
|
try {
|
|
1954
|
-
const
|
|
2292
|
+
const page = await readHistory({ sessionId, ...missing })
|
|
2293
|
+
if (page.clearIndex !== undefined) applyClearBoundary(page.clearIndex)
|
|
1955
2294
|
mergeServerEvents(
|
|
1956
|
-
|
|
2295
|
+
page.events.map((row) => eventFromWire(row) as ContractEvent<D>),
|
|
1957
2296
|
stateIndex,
|
|
1958
2297
|
)
|
|
1959
2298
|
return eventsBetween(gte, lte)
|
|
@@ -2031,9 +2370,26 @@ export function createClient<
|
|
|
2031
2370
|
}
|
|
2032
2371
|
let wasLive = false
|
|
2033
2372
|
try {
|
|
2373
|
+
// oxlint-disable-next-line no-unmodified-loop-condition -- hydration changes while this connection waits
|
|
2374
|
+
while (!hydrated && preload && !preload.error)
|
|
2375
|
+
await waitForInitial(controller.signal)
|
|
2376
|
+
if (generation !== run || controller.signal.aborted) break
|
|
2377
|
+
if (!hydrated && preload?.error) {
|
|
2378
|
+
lastError = preload.error
|
|
2379
|
+
status = 'paused'
|
|
2380
|
+
notify()
|
|
2381
|
+
return
|
|
2382
|
+
}
|
|
2383
|
+
if (hydrated && streamIndex === undefined) streamIndex = stateIndex
|
|
2384
|
+
// Acks at or before this start are covered by the baseline and
|
|
2385
|
+
// are never streamed.
|
|
2386
|
+
resolveConfirmed()
|
|
2387
|
+
if (generation !== run) break
|
|
2388
|
+
bootstrapGeneration = streamIndex === undefined ? run : undefined
|
|
2034
2389
|
const frames = transport.connect({
|
|
2035
2390
|
sessionId,
|
|
2036
|
-
startAfter:
|
|
2391
|
+
startAfter: streamIndex,
|
|
2392
|
+
reducer: reducer.name,
|
|
2037
2393
|
signal: controller.signal,
|
|
2038
2394
|
})
|
|
2039
2395
|
for await (const frame of frames) {
|
|
@@ -2041,6 +2397,41 @@ export function createClient<
|
|
|
2041
2397
|
// Every frame counts — heartbeats included. Liveness is
|
|
2042
2398
|
// about the connection, not about events.
|
|
2043
2399
|
armStall()
|
|
2400
|
+
if (frame.kind === 'snapshot') {
|
|
2401
|
+
if (
|
|
2402
|
+
frame.sessionId !== sessionId ||
|
|
2403
|
+
frame.name !== reducer.name ||
|
|
2404
|
+
!isStateSnapshotFrame(frame)
|
|
2405
|
+
)
|
|
2406
|
+
throw new A2ClientError(
|
|
2407
|
+
'INVALID_PAYLOAD',
|
|
2408
|
+
'bootstrap snapshot belongs to another session or reducer',
|
|
2409
|
+
)
|
|
2410
|
+
if (bootstrapGeneration !== run)
|
|
2411
|
+
throw new A2ClientError(
|
|
2412
|
+
'INVALID_PAYLOAD',
|
|
2413
|
+
'unexpected bootstrap snapshot',
|
|
2414
|
+
)
|
|
2415
|
+
const baseline =
|
|
2416
|
+
!hydrated || frame.index >= stateIndex
|
|
2417
|
+
? { state: bootstrapState(frame), index: frame.index }
|
|
2418
|
+
: undefined
|
|
2419
|
+
streamIndex = frame.index
|
|
2420
|
+
bootstrapGeneration = undefined
|
|
2421
|
+
applyClearBoundary(frame.clearIndex ?? 0)
|
|
2422
|
+
if (baseline) adoptBaseline(baseline)
|
|
2423
|
+
resolveConfirmed()
|
|
2424
|
+
notify()
|
|
2425
|
+
if (generation !== run) break
|
|
2426
|
+
}
|
|
2427
|
+
if (!hydrated || streamIndex === undefined) {
|
|
2428
|
+
if (frame.kind === 'event' || frame.kind === 'events')
|
|
2429
|
+
throw new A2ClientError(
|
|
2430
|
+
'INVALID_PAYLOAD',
|
|
2431
|
+
'received an event before the bootstrap snapshot',
|
|
2432
|
+
)
|
|
2433
|
+
continue
|
|
2434
|
+
}
|
|
2044
2435
|
if (!wasLive) {
|
|
2045
2436
|
// The first frame is the connection signal: the transport
|
|
2046
2437
|
// yields it the moment the stream is established.
|
|
@@ -2059,7 +2450,10 @@ export function createClient<
|
|
|
2059
2450
|
}
|
|
2060
2451
|
if (frame.kind === 'event') {
|
|
2061
2452
|
streamIdle = false
|
|
2062
|
-
ingest(eventFromWire(frame.event) as ContractEvent<D>)
|
|
2453
|
+
ingest([eventFromWire(frame.event) as ContractEvent<D>])
|
|
2454
|
+
} else if (frame.kind === 'events') {
|
|
2455
|
+
streamIdle = false
|
|
2456
|
+
ingest(frame.events as ContractEvent<D>[])
|
|
2063
2457
|
} else if (frame.kind === 'activity') {
|
|
2064
2458
|
streamIdle = frame.idle
|
|
2065
2459
|
if (
|
|
@@ -2091,11 +2485,15 @@ export function createClient<
|
|
|
2091
2485
|
? new A2ClientError(
|
|
2092
2486
|
'TIMEOUT',
|
|
2093
2487
|
`stream stalled: no data for ${STREAM_TIMINGS.stallTimeoutMs}ms`,
|
|
2094
|
-
{
|
|
2488
|
+
{
|
|
2489
|
+
phase: 'stream',
|
|
2490
|
+
timeoutMs: STREAM_TIMINGS.stallTimeoutMs,
|
|
2491
|
+
},
|
|
2095
2492
|
)
|
|
2096
2493
|
: asClientError(err)
|
|
2097
2494
|
}
|
|
2098
2495
|
} finally {
|
|
2496
|
+
if (bootstrapGeneration === run) bootstrapGeneration = undefined
|
|
2099
2497
|
clearTimeout(stall)
|
|
2100
2498
|
}
|
|
2101
2499
|
if (generation !== run) break
|
|
@@ -2173,7 +2571,114 @@ export function createClient<
|
|
|
2173
2571
|
previous?.abort()
|
|
2174
2572
|
}
|
|
2175
2573
|
|
|
2176
|
-
const
|
|
2574
|
+
const applyInitial = ({
|
|
2575
|
+
settings: next,
|
|
2576
|
+
version,
|
|
2577
|
+
}: {
|
|
2578
|
+
settings: ResolvedSessionOptions<D, S>
|
|
2579
|
+
version?: number
|
|
2580
|
+
}): boolean => {
|
|
2581
|
+
const nextIndex = next.initial?.index ?? 0
|
|
2582
|
+
if (hydrated && nextIndex > stateIndex && inFlightPushes > 0) {
|
|
2583
|
+
if (
|
|
2584
|
+
!deferredHydration ||
|
|
2585
|
+
nextIndex > (deferredHydration.settings.initial?.index ?? 0)
|
|
2586
|
+
) {
|
|
2587
|
+
deferredHydration = {
|
|
2588
|
+
settings: next,
|
|
2589
|
+
...(version === undefined ? {} : { version }),
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
return false
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
// The boundary applies even to an older fold: it only removes history.
|
|
2596
|
+
const boundaryChanged = applyClearBoundary(next.initial?.clearIndex ?? 0)
|
|
2597
|
+
const historyChanged =
|
|
2598
|
+
mergeServerEvents(
|
|
2599
|
+
next.initialEvents,
|
|
2600
|
+
Math.min(nextIndex, stateIndex),
|
|
2601
|
+
) || boundaryChanged
|
|
2602
|
+
if (next.initial != null && bootstrapGeneration === undefined)
|
|
2603
|
+
streamIndex ??= nextIndex
|
|
2604
|
+
if (next.initial == null || (hydrated && nextIndex <= stateIndex)) {
|
|
2605
|
+
return historyChanged
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
adoptBaseline(next.initial)
|
|
2609
|
+
mergeServerEvents(next.initialEvents, stateIndex)
|
|
2610
|
+
return true
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
const invalidateInitial = (): void => {
|
|
2614
|
+
initialVersion++
|
|
2615
|
+
preload?.cancel()
|
|
2616
|
+
preload = undefined
|
|
2617
|
+
if (deferredHydration?.version !== undefined)
|
|
2618
|
+
deferredHydration = undefined
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
const settleInitial = (
|
|
2622
|
+
result:
|
|
2623
|
+
{ source: object; value: unknown } | { source: object; error: unknown },
|
|
2624
|
+
): void => {
|
|
2625
|
+
const entry = preload
|
|
2626
|
+
if (
|
|
2627
|
+
!entry ||
|
|
2628
|
+
entry.source !== result.source ||
|
|
2629
|
+
entry.version !== initialVersion
|
|
2630
|
+
)
|
|
2631
|
+
return
|
|
2632
|
+
entry.cancel()
|
|
2633
|
+
const cold = !hydrated
|
|
2634
|
+
let changed = false
|
|
2635
|
+
if ('error' in result) {
|
|
2636
|
+
if (cold) entry.error = asClientError(result.error)
|
|
2637
|
+
else preload = undefined
|
|
2638
|
+
} else {
|
|
2639
|
+
try {
|
|
2640
|
+
assertSessionSeed({ initial: result.value })
|
|
2641
|
+
if (isInitialPromise(result.value as InitialSnapshot<unknown>))
|
|
2642
|
+
throw new TypeError('an initial Promise must resolve to a snapshot')
|
|
2643
|
+
preload = undefined
|
|
2644
|
+
changed = applyInitial({
|
|
2645
|
+
settings: {
|
|
2646
|
+
...entry.settings,
|
|
2647
|
+
initial: result.value as StateSnapshot<S> | null | undefined,
|
|
2648
|
+
},
|
|
2649
|
+
version: entry.version,
|
|
2650
|
+
})
|
|
2651
|
+
} catch (error) {
|
|
2652
|
+
if (cold) {
|
|
2653
|
+
preload = entry
|
|
2654
|
+
entry.error = asClientError(error, { code: 'INVALID_PAYLOAD' })
|
|
2655
|
+
} else preload = undefined
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
if (cold || changed) notifyHydrated()
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
const waitForInitial = (signal: AbortSignal): Promise<void> =>
|
|
2662
|
+
new Promise((resolve, reject) => {
|
|
2663
|
+
let settled = false
|
|
2664
|
+
const finish = (): void => {
|
|
2665
|
+
if (
|
|
2666
|
+
settled ||
|
|
2667
|
+
(!signal.aborted && !hydrated && preload && !preload.error)
|
|
2668
|
+
)
|
|
2669
|
+
return
|
|
2670
|
+
settled = true
|
|
2671
|
+
listeners.delete(finish)
|
|
2672
|
+
signal.removeEventListener('abort', finish)
|
|
2673
|
+
if (signal.aborted) reject(signal.reason)
|
|
2674
|
+
else resolve()
|
|
2675
|
+
}
|
|
2676
|
+
listeners.add(finish)
|
|
2677
|
+
signal.addEventListener('abort', finish, { once: true })
|
|
2678
|
+
finish()
|
|
2679
|
+
})
|
|
2680
|
+
|
|
2681
|
+
const runtime: SessionRuntime<D, S, P> = {
|
|
2177
2682
|
sessionId,
|
|
2178
2683
|
// Only on presence-declaring contracts — the type surface
|
|
2179
2684
|
// promises no runtime-inert members (a2-api.md §12).
|
|
@@ -2217,55 +2722,77 @@ export function createClient<
|
|
|
2217
2722
|
}
|
|
2218
2723
|
},
|
|
2219
2724
|
close() {
|
|
2725
|
+
invalidateInitial()
|
|
2726
|
+
resumeInitialError = undefined
|
|
2220
2727
|
leases = 0
|
|
2221
2728
|
leaseEpoch += 1
|
|
2729
|
+
confirmWatchers = confirmWatchers.filter((watcher) => !watcher.lease)
|
|
2222
2730
|
stopStream()
|
|
2223
2731
|
},
|
|
2732
|
+
resolveInitial: settleInitial,
|
|
2224
2733
|
hydrate(next) {
|
|
2734
|
+
assertSessionSeed(next)
|
|
2225
2735
|
touch(sessionId)
|
|
2226
2736
|
if (!next) return
|
|
2227
2737
|
participant ??= next.participant
|
|
2228
|
-
const
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2738
|
+
const input = next.initial
|
|
2739
|
+
const resume =
|
|
2740
|
+
!hydrated && status === 'paused' && preload?.error === lastError
|
|
2741
|
+
? preload.error
|
|
2742
|
+
: undefined
|
|
2743
|
+
if (isInitialPromise(input)) {
|
|
2744
|
+
if (seenInitials.has(input)) {
|
|
2745
|
+
if (preload?.source === input && next.initialEvents !== undefined)
|
|
2746
|
+
preload.settings.initialEvents = next.initialEvents
|
|
2747
|
+
if (mergeServerEvents(next.initialEvents, stateIndex))
|
|
2748
|
+
notifyHydrated()
|
|
2749
|
+
return
|
|
2235
2750
|
}
|
|
2751
|
+
invalidateInitial()
|
|
2752
|
+
seenInitials.add(input)
|
|
2753
|
+
const { initial: _initial, ...settings } = next
|
|
2754
|
+
const entry: PendingInitial<D, S> = {
|
|
2755
|
+
source: input,
|
|
2756
|
+
version: initialVersion,
|
|
2757
|
+
settings,
|
|
2758
|
+
cancel() {},
|
|
2759
|
+
}
|
|
2760
|
+
preload = entry
|
|
2761
|
+
entry.cancel = observeInitial({
|
|
2762
|
+
promise: input,
|
|
2763
|
+
settled: (result) => settleInitial({ source: input, ...result }),
|
|
2764
|
+
})
|
|
2765
|
+
if (resume) notifyHydrated(resume)
|
|
2236
2766
|
return
|
|
2237
2767
|
}
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2768
|
+
if (input != null) {
|
|
2769
|
+
const repeated = seenInitials.has(input)
|
|
2770
|
+
seenInitials.add(input)
|
|
2771
|
+
if (repeated || (hydrated && input.index <= stateIndex)) {
|
|
2772
|
+
if (
|
|
2773
|
+
deferredHydration?.settings.initial === input &&
|
|
2774
|
+
next.initialEvents !== undefined
|
|
2775
|
+
)
|
|
2776
|
+
deferredHydration.settings.initialEvents = next.initialEvents
|
|
2777
|
+
const boundaryChanged = applyClearBoundary(input.clearIndex ?? 0)
|
|
2778
|
+
if (
|
|
2779
|
+
mergeServerEvents(
|
|
2780
|
+
next.initialEvents,
|
|
2781
|
+
Math.min(input.index, stateIndex),
|
|
2782
|
+
) ||
|
|
2783
|
+
boundaryChanged
|
|
2784
|
+
)
|
|
2785
|
+
notifyHydrated()
|
|
2786
|
+
return
|
|
2787
|
+
}
|
|
2247
2788
|
}
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
mergeServerEvents(next.initialEvents, stateIndex)
|
|
2254
|
-
pending = pending.filter(
|
|
2255
|
-
(entry) =>
|
|
2256
|
-
!entry.acked ||
|
|
2257
|
-
entry.acked.index > Math.min(stateIndex, streamIndex ?? stateIndex),
|
|
2789
|
+
if (input !== undefined) invalidateInitial()
|
|
2790
|
+
if (
|
|
2791
|
+
applyInitial({ settings: { ...next, initial: input } }) ||
|
|
2792
|
+
resume ||
|
|
2793
|
+
input === null
|
|
2258
2794
|
)
|
|
2259
|
-
|
|
2260
|
-
const due = confirmWatchers.filter(
|
|
2261
|
-
(watcher) => watcher.index <= stateIndex,
|
|
2262
|
-
)
|
|
2263
|
-
confirmWatchers = confirmWatchers.filter(
|
|
2264
|
-
(watcher) => watcher.index > stateIndex,
|
|
2265
|
-
)
|
|
2266
|
-
for (const watcher of due) watcher.resolve()
|
|
2267
|
-
}
|
|
2268
|
-
notifyHydrated()
|
|
2795
|
+
notifyHydrated(resume)
|
|
2269
2796
|
},
|
|
2270
2797
|
canEvict() {
|
|
2271
2798
|
return (
|
|
@@ -2277,12 +2804,13 @@ export function createClient<
|
|
|
2277
2804
|
presenceTimer === undefined
|
|
2278
2805
|
)
|
|
2279
2806
|
},
|
|
2280
|
-
} as SessionRuntime<D, S, P
|
|
2807
|
+
} as SessionRuntime<D, S, P>
|
|
2281
2808
|
|
|
2809
|
+
if (isInitialPromise(supplied)) runtime.hydrate(sessionOptions)
|
|
2282
2810
|
return runtime
|
|
2283
2811
|
}
|
|
2284
2812
|
|
|
2285
|
-
|
|
2813
|
+
const client: A2Client<D, S, P> = {
|
|
2286
2814
|
session(sessionId, sessionOptions) {
|
|
2287
2815
|
const existing = sessions.get(sessionId)
|
|
2288
2816
|
if (existing) {
|
|
@@ -2297,4 +2825,13 @@ export function createClient<
|
|
|
2297
2825
|
return runtime
|
|
2298
2826
|
},
|
|
2299
2827
|
}
|
|
2828
|
+
registerRenderSessionFactory({
|
|
2829
|
+
client,
|
|
2830
|
+
factory: (sessionId, settings) =>
|
|
2831
|
+
makeSession(sessionId, settings as SessionOptions<D, S>),
|
|
2832
|
+
resolveInitial: ({ session, source, value }) => {
|
|
2833
|
+
;(session as SessionRuntime<D, S, P>).resolveInitial({ source, value })
|
|
2834
|
+
},
|
|
2835
|
+
})
|
|
2836
|
+
return client
|
|
2300
2837
|
}
|