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
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Handlers
|
|
3
|
+
description: Plain async functions that react to events. Concurrent by default, serialized by lanes, retried after failures.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
```ts server/orders.ts
|
|
7
|
+
import { createServer } from 'experimental-a2/server'
|
|
8
|
+
import { orders } from '@/contracts'
|
|
9
|
+
|
|
10
|
+
export const ordersServer = createServer({
|
|
11
|
+
contract: orders,
|
|
12
|
+
handlers: {
|
|
13
|
+
created: async ({ event }) => {
|
|
14
|
+
// your side effect, e.g. email the shop, idempotent via event.id:
|
|
15
|
+
// await sendEmailToShop(event.payload, { idempotencyKey: event.id })
|
|
16
|
+
return { type: 'shop.notified', payload: {} }
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
})
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
A handler runs when its event lands: a plain async function that can call a
|
|
23
|
+
database, hit an API, or stream a model response. A2 claims the event while it
|
|
24
|
+
runs and retries it after a crash.
|
|
25
|
+
|
|
26
|
+
`createServer` from `experimental-a2/server` binds the contract to a
|
|
27
|
+
[store](/server/production#1-choose-a-store) and one complete handler table,
|
|
28
|
+
so a handler can't go missing because its module was never imported. Compose
|
|
29
|
+
the table across files by spreading objects into `handlers`. It is the only
|
|
30
|
+
entry point that touches a store, and it fails loudly in a browser bundle.
|
|
31
|
+
|
|
32
|
+
## Events without handlers
|
|
33
|
+
|
|
34
|
+
```ts server/order-history.ts
|
|
35
|
+
import { createServer } from 'experimental-a2/server'
|
|
36
|
+
import { postgres } from 'experimental-a2/store-postgres'
|
|
37
|
+
import { orders } from '@/contracts'
|
|
38
|
+
|
|
39
|
+
export const orderHistory = createServer({
|
|
40
|
+
contract: orders,
|
|
41
|
+
store: postgres({ connectionString: process.env.DATABASE_URL }),
|
|
42
|
+
})
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Handlers are optional per event type. An event without one is still logged,
|
|
46
|
+
folded into state, and streamed; it settles in its append transaction, with no
|
|
47
|
+
claim or scheduler message. Adding a handler later doesn't replay settled
|
|
48
|
+
events. Append a new event or run a migration when you want that.
|
|
49
|
+
|
|
50
|
+
## The context
|
|
51
|
+
|
|
52
|
+
| Property | What it is |
|
|
53
|
+
| ------------- | ---------- |
|
|
54
|
+
| `ctx.event` | The triggering event: `{ id, type, payload, index, sessionId, createdAt }`. |
|
|
55
|
+
| `ctx.attempt` | The durable 1-based dispatch claim for this event. |
|
|
56
|
+
| `ctx.session` | This session's handle: `id`, `append`, `schedule`, `clear`, `history`, `state`, `stream`, and `setPresence` (presence contracts only). |
|
|
57
|
+
| `ctx.signal` | An `AbortSignal`. Fires on matching `abortOn` events and when the attempt loses its claim (`CLAIM_EXPIRED`, `SUPERSEDED_ATTEMPT`). See [Cancellation](/server/cancellation). |
|
|
58
|
+
|
|
59
|
+
`ctx.attempt` increments on every durable claim. It can skip a number when a
|
|
60
|
+
process dies between the claim and handler entry.
|
|
61
|
+
|
|
62
|
+
[`ctx.session.state(reducer)`](/contract/state) folds through
|
|
63
|
+
`ctx.event.index`, so later concurrent appends don't change what this event
|
|
64
|
+
sees. Pass `{ through: 'latest' }` to read the current committed frontier
|
|
65
|
+
instead.
|
|
66
|
+
|
|
67
|
+
## Return what happens after success
|
|
68
|
+
|
|
69
|
+
```ts server/return-many.ts
|
|
70
|
+
import { createServer } from 'experimental-a2/server'
|
|
71
|
+
import { orders } from '@/contracts'
|
|
72
|
+
|
|
73
|
+
export const fanout = createServer({
|
|
74
|
+
contract: orders,
|
|
75
|
+
handlers: {
|
|
76
|
+
'shop.started': async () => {
|
|
77
|
+
return orders.batch(
|
|
78
|
+
{ type: 'shop.notified', payload: {} },
|
|
79
|
+
{ type: 'driver.notified', payload: { driverId: 'driver-7' } },
|
|
80
|
+
)
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
})
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Return one event or an array when it should exist only if the handler
|
|
87
|
+
succeeds. A2 marks the trigger processed and appends the batch in one atomic
|
|
88
|
+
operation, at consecutive positions. If the handler throws or the process dies
|
|
89
|
+
first, none of it exists. Returned events get deterministic ids, so a retry
|
|
90
|
+
after a lost acknowledgement doesn't duplicate them.
|
|
91
|
+
|
|
92
|
+
`contract.batch(...)` is a type helper that keeps each event's literal type;
|
|
93
|
+
A2 still validates at commit.
|
|
94
|
+
|
|
95
|
+
## Append what should happen now
|
|
96
|
+
|
|
97
|
+
```ts server/append-now.ts
|
|
98
|
+
import { createServer } from 'experimental-a2/server'
|
|
99
|
+
import { orders } from '@/contracts'
|
|
100
|
+
|
|
101
|
+
export const immediate = createServer({
|
|
102
|
+
contract: orders,
|
|
103
|
+
handlers: {
|
|
104
|
+
created: async ({ session }) => {
|
|
105
|
+
await session.append('notify-shop', {
|
|
106
|
+
type: 'shop.notified',
|
|
107
|
+
payload: {},
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
// The shop.notified handler may already be running here.
|
|
111
|
+
// await continueCurrentWork()
|
|
112
|
+
},
|
|
113
|
+
'shop.notified': async () => {},
|
|
114
|
+
},
|
|
115
|
+
})
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`ctx.session.append()` commits immediately, so the appended event can start
|
|
119
|
+
while its parent is still running. Use it for streaming progress, independent
|
|
120
|
+
fan-out, or any fact that stays true even if the handler later fails. Each
|
|
121
|
+
call is atomic.
|
|
122
|
+
|
|
123
|
+
The first argument names the append within the triggering event. A2 derives
|
|
124
|
+
the ids of events without an `id` from the trigger, the name, and each
|
|
125
|
+
event's position, so a retry gets the existing events back, whatever order
|
|
126
|
+
parallel branches finish in. Keep the name stable; reusing it with a different
|
|
127
|
+
batch conflicts. An explicit `id` wins, so different triggers can converge on
|
|
128
|
+
one fact. Top-level `server.session(id).append(...events)` takes no name.
|
|
129
|
+
|
|
130
|
+
## Concurrent by default
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
// anywhere on the server:
|
|
134
|
+
await ordersServer.session('order-42').append(
|
|
135
|
+
{ type: 'created', payload: { shopId: 'shop-7', items: [] } },
|
|
136
|
+
{ type: 'shop.started', payload: {} },
|
|
137
|
+
)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Every eligible event has its own claim, so both handlers start without
|
|
141
|
+
waiting for each other. Log order stays stable; handler order doesn't. A
|
|
142
|
+
failure retries that event without holding up unlaned events or other lanes.
|
|
143
|
+
|
|
144
|
+
## Lanes
|
|
145
|
+
|
|
146
|
+
```ts server/serial-orders.ts
|
|
147
|
+
import { createServer } from 'experimental-a2/server'
|
|
148
|
+
import { orders } from '@/contracts'
|
|
149
|
+
|
|
150
|
+
export const serialOrders = createServer({
|
|
151
|
+
contract: orders,
|
|
152
|
+
handlers: {
|
|
153
|
+
created: {
|
|
154
|
+
lane: 'order-work',
|
|
155
|
+
handler: async () => {},
|
|
156
|
+
},
|
|
157
|
+
'shop.started': {
|
|
158
|
+
lane: 'order-work',
|
|
159
|
+
handler: async () => {},
|
|
160
|
+
},
|
|
161
|
+
'driver.notified': {
|
|
162
|
+
lane: ({ event }) => `driver:${event.payload.driverId}`,
|
|
163
|
+
handler: async ({ event }) => {
|
|
164
|
+
// your work for this driver, using event.payload.driverId
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
})
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Use a lane when events share a resource and must not overlap. Within one
|
|
172
|
+
session, same-lane events run one at a time in log order; different lanes and
|
|
173
|
+
unlaned events stay concurrent. A lane is a constant (a serial queue) or a
|
|
174
|
+
function of the validated event.
|
|
175
|
+
|
|
176
|
+
A2 stores the resolved lane at append, so a deployment can't reinterpret
|
|
177
|
+
pending work. Lanes are per session, not distributed locks across sessions. If
|
|
178
|
+
a lane's first unfinished event fails, later events in that lane wait; other
|
|
179
|
+
lanes continue.
|
|
180
|
+
|
|
181
|
+
## Handlers can run twice
|
|
182
|
+
|
|
183
|
+
Handlers are at-least-once. A crash after your side effect but before A2
|
|
184
|
+
records completion runs the handler again.
|
|
185
|
+
|
|
186
|
+
- **External side effects take an idempotency key.** `event.id` is stable
|
|
187
|
+
across attempts.
|
|
188
|
+
- **Named appends are deduplicated.** A retry gets the existing events back.
|
|
189
|
+
- **Returned events commit with completion.** They all exist with the
|
|
190
|
+
processed marker, or none do.
|
|
191
|
+
|
|
192
|
+
## When handlers throw
|
|
193
|
+
|
|
194
|
+
Throwing means "retry me." A2 records the failure and retries with backoff.
|
|
195
|
+
The failed event blocks only its lane. After ten caught failures it
|
|
196
|
+
dead-letters and keeps blocking its lane until
|
|
197
|
+
[you retry or skip it](/server/production#3-when-an-event-dead-letters).
|
|
198
|
+
|
|
199
|
+
When someone presses stop, catch the abort and return normally instead. See
|
|
200
|
+
[Cancellation](/server/cancellation).
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Timers
|
|
3
|
+
description: Schedule a typed session event for later, through the same scheduler that handles recovery.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Schedule from a handler
|
|
7
|
+
|
|
8
|
+
```ts server/orders.ts
|
|
9
|
+
import { createServer } from 'experimental-a2/server'
|
|
10
|
+
import { vercelQueues } from 'experimental-a2/scheduler-vercel'
|
|
11
|
+
import { orders } from '@/contracts'
|
|
12
|
+
|
|
13
|
+
export const scheduler = vercelQueues()
|
|
14
|
+
|
|
15
|
+
export const ordersServer = createServer({
|
|
16
|
+
contract: orders,
|
|
17
|
+
scheduler,
|
|
18
|
+
handlers: {
|
|
19
|
+
created: async ({ session }) => {
|
|
20
|
+
await session.schedule(
|
|
21
|
+
'expire-order',
|
|
22
|
+
{ delay: '5d' },
|
|
23
|
+
{ type: 'expired', payload: {} },
|
|
24
|
+
)
|
|
25
|
+
},
|
|
26
|
+
expired: async ({ session }) => {
|
|
27
|
+
const events = await session.history()
|
|
28
|
+
if (events.some((event) => event.type === 'shop.started')) return
|
|
29
|
+
// your expiry side effect goes here
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Configure a scheduler, then call `session.schedule(name, timing, ...events)`.
|
|
36
|
+
The scheduler route that handles recovery delivers timers too;
|
|
37
|
+
[mount it once](/server/production#2-add-a-scheduler-for-handlers-and-timers).
|
|
38
|
+
|
|
39
|
+
`timing` is `{ delay }` (units `ms`, `s`, `m`, `h`, `d`, checked by the type)
|
|
40
|
+
or `{ at: Date }`. A delay must be a finite, unsigned base-10 decimal above
|
|
41
|
+
zero with no leading zeros; A2 rejects `0s`, signs, exponent notation,
|
|
42
|
+
malformed strings, and invalid dates.
|
|
43
|
+
|
|
44
|
+
The call resolves once the provider accepts the task; nothing enters the log
|
|
45
|
+
until delivery. The target is the earliest delivery time, not a deadline:
|
|
46
|
+
adapters round to whole seconds, and an already-due target sends immediately.
|
|
47
|
+
|
|
48
|
+
## Schedule from ordinary server code
|
|
49
|
+
|
|
50
|
+
```ts app/api/trials/route.ts
|
|
51
|
+
import { z } from 'zod'
|
|
52
|
+
import { ordersServer } from '@/server/orders'
|
|
53
|
+
|
|
54
|
+
const scheduleTrial = z.object({
|
|
55
|
+
orderId: z.string(),
|
|
56
|
+
expiresAt: z.iso.datetime().transform((value) => new Date(value)),
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
export async function POST(request: Request) {
|
|
60
|
+
const body = scheduleTrial.parse(await request.json())
|
|
61
|
+
await ordersServer.session(body.orderId).schedule(
|
|
62
|
+
'expire-order',
|
|
63
|
+
{ at: body.expiresAt },
|
|
64
|
+
{ type: 'expired', payload: {} },
|
|
65
|
+
)
|
|
66
|
+
return Response.json({ scheduled: true }, { status: 202 })
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Root sessions have the same method. A root `{ delay: '30s' }` counts from that
|
|
71
|
+
call's clock, so a retry 10 seconds later delivers 10 seconds later too. Use
|
|
72
|
+
`{ at }` when a caller may retry. Inside a handler, a delay counts from the
|
|
73
|
+
triggering event's durable `createdAt`, so retries keep the same target.
|
|
74
|
+
|
|
75
|
+
## Identity and retries
|
|
76
|
+
|
|
77
|
+
`name` identifies one schedule: per contract and session at the root, plus the
|
|
78
|
+
triggering event id in a handler. Different events can share a name like
|
|
79
|
+
`expire-order`, while retries of one event converge.
|
|
80
|
+
|
|
81
|
+
Keep the same name, timing, and events on every retry. Omitted event ids are
|
|
82
|
+
derived and stable, so duplicate deliveries append once per session lifetime,
|
|
83
|
+
even after the provider's deduplication window. A
|
|
84
|
+
[clear](/contract/sessions#clearing-a-session) frees those ids, so a root task
|
|
85
|
+
redelivered after one appends again. Changed input under the same name
|
|
86
|
+
conflicts at delivery when it produces a different validated event.
|
|
87
|
+
|
|
88
|
+
## What arrives later
|
|
89
|
+
|
|
90
|
+
Payloads travel in the provider message as plain JSON: encode a `Date` as an
|
|
91
|
+
ISO string and a class as plain data. A2 rejects `undefined`, functions,
|
|
92
|
+
symbols, bigints, cycles, accessors, and non-plain prototypes, and shared
|
|
93
|
+
references arrive as copies. At delivery, an ordinary top-level append with
|
|
94
|
+
fixed ids validates the original input, so a schema transform runs on that
|
|
95
|
+
input, not on an earlier output.
|
|
96
|
+
|
|
97
|
+
Scheduled events enter the log only when delivered, with `cause: null`. From
|
|
98
|
+
there they behave like any server append: handlers, lanes, reducers, streams,
|
|
99
|
+
and live clients see them normally, and their stable ids absorb at-least-once
|
|
100
|
+
delivery.
|
|
101
|
+
|
|
102
|
+
If the receiving deployment changed or removed the event's schema, delivery
|
|
103
|
+
can fail validation and then keeps retrying as a scheduler error. The Vercel
|
|
104
|
+
adapter pins timers to their publishing deployment by default; with
|
|
105
|
+
`deploymentId: null` or a stable URL like QStash's, keep schemas compatible.
|
|
106
|
+
See [Deploy safely](/server/production#4-deploy-safely).
|
|
107
|
+
|
|
108
|
+
## Stale timers
|
|
109
|
+
|
|
110
|
+
The order shipped on day two; its expiry still arrives on day five. Use the
|
|
111
|
+
guard from the first example: let the event land, read the session, and return
|
|
112
|
+
when it no longer applies. The log still records that the timer fired.
|
|
113
|
+
|
|
114
|
+
A2 has no cancellation, reschedule, cron, or timer-listing API. `name` is an
|
|
115
|
+
idempotency key, not a handle. For repeating work, have each occurrence's
|
|
116
|
+
handler schedule the next one.
|
|
117
|
+
|
|
118
|
+
## Provider limits
|
|
119
|
+
|
|
120
|
+
| Adapter | Maximum one-shot delay |
|
|
121
|
+
| --- | --- |
|
|
122
|
+
| Vercel Queues | 6 days |
|
|
123
|
+
| QStash free | 7 days |
|
|
124
|
+
| QStash usage-based | 1 year |
|
|
125
|
+
| QStash fixed | no fixed maximum |
|
|
126
|
+
|
|
127
|
+
A2 does not split a longer delay into several tasks. Vercel Queues keeps
|
|
128
|
+
messages for seven days, and A2 reserves the final day for delivery and
|
|
129
|
+
retries. At a QStash maximum, leave at least one second of margin: its
|
|
130
|
+
`notBefore` uses whole Unix seconds.
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Cancellation
|
|
3
|
+
description: There is no abort() API. Cancelling is appending, and the running handler finds out through a signal.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Cancelling is appending
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
// in the browser: push from useSession
|
|
10
|
+
push({ type: 'cancelled', payload: { lastSeenIndex: index } })
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
A user presses stop on a streaming AI response. That's an event: same route,
|
|
14
|
+
same auth, same log as every other event. Two things then happen, at two
|
|
15
|
+
speeds.
|
|
16
|
+
|
|
17
|
+
## The running handler stops
|
|
18
|
+
|
|
19
|
+
```ts server/chat.ts
|
|
20
|
+
import { createServer } from 'experimental-a2/server'
|
|
21
|
+
import { chat } from '@/contracts'
|
|
22
|
+
|
|
23
|
+
export const chatServer = createServer({
|
|
24
|
+
contract: chat,
|
|
25
|
+
handlers: {
|
|
26
|
+
generate: {
|
|
27
|
+
abortOn: ['cancelled'],
|
|
28
|
+
handler: async (ctx) => {
|
|
29
|
+
// your abortable work; hand it ctx.signal:
|
|
30
|
+
// await streamText({ signal: ctx.signal, ... })
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`abortOn` names the events that interrupt the handler. When a `cancelled`
|
|
38
|
+
event lands, or had already landed before the handler started, `ctx.signal`
|
|
39
|
+
fires. Handlers without `abortOn` hold no subscription.
|
|
40
|
+
|
|
41
|
+
When one session runs several generations over its lifetime, match the
|
|
42
|
+
instance with a predicate:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
// server/chat.ts, the generate entry, targeted by instance:
|
|
46
|
+
generate: {
|
|
47
|
+
abortOn: {
|
|
48
|
+
// only the cancel that names this run fires the signal
|
|
49
|
+
cancelled: (event, trigger) => event.payload.of === trigger.id,
|
|
50
|
+
},
|
|
51
|
+
handler: async (ctx) => {
|
|
52
|
+
// ...
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The pushing side already knows the id it's cancelling: it's in the hook's
|
|
58
|
+
`events` feed.
|
|
59
|
+
|
|
60
|
+
## When a run loses its claim
|
|
61
|
+
|
|
62
|
+
Every handler's signal, with or without `abortOn`, also fires when the run
|
|
63
|
+
loses its claim: `CLAIM_EXPIRED` when the lease lapses without renewal
|
|
64
|
+
(decided locally, so it works while the store is unreachable), or
|
|
65
|
+
`SUPERSEDED_ATTEMPT` when a recovery run has claimed the event. Either way, a
|
|
66
|
+
successor is taking over: stop external work. A superseded run can no longer
|
|
67
|
+
write to the log or complete; passing `ctx.signal` to your external calls is
|
|
68
|
+
what stops it racing its successor outside A2.
|
|
69
|
+
|
|
70
|
+
Pass `ctx.signal` to your I/O and let the abort propagate. That handles every
|
|
71
|
+
case:
|
|
72
|
+
|
|
73
|
+
- **A propagated lease abort** surrenders the attempt: no completion, no
|
|
74
|
+
failure budget spent, normal recovery.
|
|
75
|
+
- **A user cancellation** (`abortOn`) is the one to catch and return normally
|
|
76
|
+
from. The stop is the outcome, so the completion counts.
|
|
77
|
+
- **Return normally after a lease abort** only when the work is done. A lapsed
|
|
78
|
+
run that finishes before any successor claims the event still completes.
|
|
79
|
+
- **Any other throw** is a real failure and spends the retry budget.
|
|
80
|
+
|
|
81
|
+
## Clean up after the handler
|
|
82
|
+
|
|
83
|
+
A `cancelled` handler can run while the interrupted one is still unwinding:
|
|
84
|
+
the signal is the preemption channel, the event is the durable record. When
|
|
85
|
+
cleanup must wait, put both event types in one lane. The event still fires the
|
|
86
|
+
signal on append, but its own handler waits for earlier work in the lane:
|
|
87
|
+
|
|
88
|
+
```ts server/chat-with-cleanup.ts
|
|
89
|
+
import { createServer } from 'experimental-a2/server'
|
|
90
|
+
import { chat } from '@/contracts'
|
|
91
|
+
|
|
92
|
+
export const chatServer = createServer({
|
|
93
|
+
contract: chat,
|
|
94
|
+
handlers: {
|
|
95
|
+
generate: {
|
|
96
|
+
lane: 'generation',
|
|
97
|
+
abortOn: ['cancelled'],
|
|
98
|
+
handler: async (ctx) => {
|
|
99
|
+
// your abortable work; hand it ctx.signal:
|
|
100
|
+
// await streamText({ signal: ctx.signal, ... })
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
cancelled: {
|
|
104
|
+
lane: 'generation',
|
|
105
|
+
handler: async () => {
|
|
106
|
+
// your cleanup, after generate has returned
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
})
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## The view cuts
|
|
114
|
+
|
|
115
|
+
```ts reducer.ts
|
|
116
|
+
case 'chunk':
|
|
117
|
+
return state.cancelled
|
|
118
|
+
? state
|
|
119
|
+
: { ...state, chunks: [...state.chunks, { index: event.index, delta: event.payload.delta }] }
|
|
120
|
+
case 'cancelled':
|
|
121
|
+
return {
|
|
122
|
+
...state,
|
|
123
|
+
cancelled: true,
|
|
124
|
+
chunks: state.chunks.filter((c) => c.index <= event.payload.lastSeenIndex),
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Chunks in flight during the cancel round trip still land after `cancelled`.
|
|
129
|
+
They're true history; keeping them out of the view is the reducer's job.
|
|
130
|
+
|
|
131
|
+
`lastSeenIndex` is the client's stream frontier, which it already tracks. The
|
|
132
|
+
cut is exact and clock-free: chunks the user never saw are removed, so the
|
|
133
|
+
optimistic view and the authoritative refold agree. Because the client pushes
|
|
134
|
+
`cancelled` [optimistically](/client/react#events-and-history), its own fold
|
|
135
|
+
cuts instantly, and stragglers hit `state.cancelled` and vanish.
|
|
136
|
+
|
|
137
|
+
:::tip
|
|
138
|
+
Keep chunks at per-chunk granularity in state until the session reaches a
|
|
139
|
+
terminal event, then compact them into one string. That's reducer design,
|
|
140
|
+
fully yours.
|
|
141
|
+
:::
|