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/docs/guides/03-react.mdx
DELETED
|
@@ -1,384 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Live UI with React
|
|
3
|
-
description: "Synchronize one session from server to browser: render its state, stream new events, and push writes optimistically through one reducer."
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
## The model
|
|
7
|
-
|
|
8
|
-
A session can be read live from the browser. The server renders the first
|
|
9
|
-
paint, a stream keeps it fresh, and writes apply instantly. One reducer
|
|
10
|
-
produces every one of those views; server and client fold the same log
|
|
11
|
-
with the same function, so they can't disagree.
|
|
12
|
-
|
|
13
|
-
That's the trick, really. The client receives one state snapshot and its log
|
|
14
|
-
position, then syncs later events and folds them locally. It does not keep
|
|
15
|
-
synchronizing state. An append-only log with positions is simpler.
|
|
16
|
-
|
|
17
|
-
This is session sync. The client follows one known session id; it does not
|
|
18
|
-
subscribe to database tables or queries across many sessions. Keep those
|
|
19
|
-
collection views in your [application database](/guides/application-data).
|
|
20
|
-
|
|
21
|
-
The pieces: two route handlers (read and write), a factory that binds your
|
|
22
|
-
reducer, and the provider + hook it returns.
|
|
23
|
-
|
|
24
|
-
## The API route
|
|
25
|
-
|
|
26
|
-
`server.fetch` exposes a session over HTTP. It is bound, so the route can
|
|
27
|
-
export it directly: `GET` streams events and serves history slices;
|
|
28
|
-
`POST` appends.
|
|
29
|
-
|
|
30
|
-
```ts app/api/order-events/route.ts
|
|
31
|
-
import { ordersServer } from '@/server/orders'
|
|
32
|
-
|
|
33
|
-
export const GET = ordersServer.fetch
|
|
34
|
-
export const POST = ordersServer.fetch
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
`GET` is the read path. A plain `GET` is the live stream: a server-sent
|
|
38
|
-
events response of one session's events, resumed after the `index` query
|
|
39
|
-
parameter, so clients pick up exactly where they left off after a first
|
|
40
|
-
paint or a dropped connection. A `GET` carrying `gte`/`lte` query
|
|
41
|
-
parameters is a history slice instead: the bounded log range as JSON,
|
|
42
|
-
the cold read [`loadHistory`](#the-client-component) rides.
|
|
43
|
-
|
|
44
|
-
`POST` is the write path. The push envelope is validated, then `append` does the
|
|
45
|
-
rest. The response is the appended events: an ack, not a stream. Thrown
|
|
46
|
-
[`A2Error`s](/reference/errors#over-the-wire) serialize onto the wire so
|
|
47
|
-
the client can branch on the same codes.
|
|
48
|
-
|
|
49
|
-
Wrap `server.fetch` when the route needs authentication, per-operation
|
|
50
|
-
authorization, or response decoration. `authorize` receives the parsed
|
|
51
|
-
A2 operation after envelope parsing and before schema validation or I/O.
|
|
52
|
-
Return `false` to answer `FORBIDDEN`. A thrown error is treated as an
|
|
53
|
-
infrastructure failure.
|
|
54
|
-
|
|
55
|
-
```ts
|
|
56
|
-
// app/api/order-events/route.ts, now with response decoration:
|
|
57
|
-
import { ordersServer } from '@/server/orders'
|
|
58
|
-
|
|
59
|
-
export async function GET(request: Request): Promise<Response> {
|
|
60
|
-
const response = await ordersServer.fetch(request)
|
|
61
|
-
if (response.headers.get('a2-history-covered') === 'true') {
|
|
62
|
-
response.headers.set('cache-control', 'private, max-age=31536000')
|
|
63
|
-
}
|
|
64
|
-
return response
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export const POST = ordersServer.fetch
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
## The session module
|
|
71
|
-
|
|
72
|
-
Bind your reducer once, in a `'use client'` file, and export the pair:
|
|
73
|
-
|
|
74
|
-
```tsx app/orders/[orderId]/session.ts
|
|
75
|
-
'use client'
|
|
76
|
-
import { createClient } from 'experimental-a2/client'
|
|
77
|
-
import { createReact } from 'experimental-a2/react'
|
|
78
|
-
import { ordersReducer } from '@/reducer'
|
|
79
|
-
|
|
80
|
-
export const ordersClient = createClient({
|
|
81
|
-
reducer: ordersReducer,
|
|
82
|
-
api: '/api/order-events',
|
|
83
|
-
})
|
|
84
|
-
export const { SessionProvider, useSession } = createReact({ client: ordersClient })
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
This is the whole client/server seam. `createReact` is a factory (like
|
|
88
|
-
`createContext`): everything it returns is typed by the reducer value;
|
|
89
|
-
the state from its fold, the pushable events from its vocabulary. No type
|
|
90
|
-
arguments anywhere, and nothing in this file touches the server module.
|
|
91
|
-
The client also gives every session a stable in-memory identity. Calling
|
|
92
|
-
`ordersClient.session(orderId)` before navigation and mounting the provider
|
|
93
|
-
after navigation reaches the same optimistic state, with no handoff store.
|
|
94
|
-
|
|
95
|
-
## The server component
|
|
96
|
-
|
|
97
|
-
The first paint costs no client JavaScript: fold on the server, pass the
|
|
98
|
-
result down.
|
|
99
|
-
|
|
100
|
-
```tsx app/orders/[orderId]/page.tsx
|
|
101
|
-
import { ordersServer } from '@/server/orders'
|
|
102
|
-
import { ordersReducer } from '@/reducer'
|
|
103
|
-
import { SessionProvider } from './session'
|
|
104
|
-
import { OrderClient } from './order-client'
|
|
105
|
-
|
|
106
|
-
export default async function OrderPage({
|
|
107
|
-
params,
|
|
108
|
-
}: {
|
|
109
|
-
params: Promise<{ orderId: string }>
|
|
110
|
-
}) {
|
|
111
|
-
const { orderId } = await params
|
|
112
|
-
const session = ordersServer.session(orderId)
|
|
113
|
-
const { state, index } = await session.state(ordersReducer)
|
|
114
|
-
|
|
115
|
-
return (
|
|
116
|
-
<SessionProvider
|
|
117
|
-
sessionId={orderId}
|
|
118
|
-
initialState={state}
|
|
119
|
-
initialIndex={index}
|
|
120
|
-
>
|
|
121
|
-
<OrderClient />
|
|
122
|
-
</SessionProvider>
|
|
123
|
-
)
|
|
124
|
-
}
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
Two props do the heavy lifting. `initialState` is the fold. `initialIndex` is
|
|
128
|
-
its frontier, the log position the fold reflects. The client opens its stream
|
|
129
|
-
after that position. The stream catches up any events appended after the server
|
|
130
|
-
fold, then stays live. No history read is needed to hydrate state.
|
|
131
|
-
(No `reducer` or `api` prop: the provider got both from the shared client.)
|
|
132
|
-
|
|
133
|
-
## The client component
|
|
134
|
-
|
|
135
|
-
```tsx app/orders/[orderId]/order-client.tsx
|
|
136
|
-
'use client'
|
|
137
|
-
import { useSession } from './session'
|
|
138
|
-
|
|
139
|
-
export function OrderClient() {
|
|
140
|
-
const { state, push } = useSession()
|
|
141
|
-
|
|
142
|
-
return (
|
|
143
|
-
<button onClick={() => push({ type: 'shop.started', payload: {} })}>
|
|
144
|
-
Start
|
|
145
|
-
</button>
|
|
146
|
-
)
|
|
147
|
-
}
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
What the hook gives you:
|
|
151
|
-
|
|
152
|
-
- **`state`**: the live view, folded through the shared reducer. It
|
|
153
|
-
updates three ways: optimistically when you push, corrected when the
|
|
154
|
-
server acks, and continuously as the stream delivers events, yours and
|
|
155
|
-
everyone else's.
|
|
156
|
-
- **`push`**: typed against the contract's vocabulary (via the reducer). Payloads are
|
|
157
|
-
validated locally before anything leaves the browser, apply instantly,
|
|
158
|
-
swap in the real event when the `POST` acks, and refold without it if
|
|
159
|
-
the server says no. Rejections carry the same
|
|
160
|
-
[`A2Error` codes](/reference/errors) the server throws, so both sides
|
|
161
|
-
branch on `code`. Awaiting `push(...)` gives the server ack; the
|
|
162
|
-
result also carries **`.confirmed`**, a promise for the moment the
|
|
163
|
-
live stream delivers the batch back and the view shows server truth.
|
|
164
|
-
`await push(...).confirmed` is "continue once this is real"; two
|
|
165
|
-
`performance.now()` calls around the two awaits are a complete
|
|
166
|
-
push→ack→stream latency meter. Overlapping calls on the same
|
|
167
|
-
session still apply immediately, then enter the transport in call
|
|
168
|
-
order. Controls do not need to wait or disable themselves to preserve
|
|
169
|
-
that order.
|
|
170
|
-
- **`events`**: the raw events this client has observed or was explicitly
|
|
171
|
-
seeded with. With only the server snapshot, it begins after `initialIndex`.
|
|
172
|
-
Use it for UI that wants the log itself: an activity feed, a debug panel.
|
|
173
|
-
Earlier events are not needed to hydrate `state`. Later hydration can
|
|
174
|
-
advance state ahead of this feed; the stream keeps its own resume position
|
|
175
|
-
and adds delayed events without folding them again.
|
|
176
|
-
- **`loadHistory`**: backscroll. `loadHistory({ before?, limit? })`
|
|
177
|
-
fetches a bounded slice of the log from below the frontier (the same
|
|
178
|
-
route, `gte`/`lte` query parameters) and merges it into `events`:
|
|
179
|
-
deduped, ordered, shared across every handle of the session. By
|
|
180
|
-
default each call walks backward 50 events at a time from the oldest
|
|
181
|
-
one loaded, including after a newer snapshot hydrates the session.
|
|
182
|
-
Pass an explicit `before` to read another range. It never touches `state`
|
|
183
|
-
or the optimistic overlay; backscrolled events are display data.
|
|
184
|
-
Calls serialize per session, so
|
|
185
|
-
a double-tap never fetches the same range twice. The `ws` api has no
|
|
186
|
-
history lane; `loadHistory` throws a `TypeError` there.
|
|
187
|
-
- **`history`**: backscroll progress, `{ loading, complete,
|
|
188
|
-
oldestLoaded }`. `complete` means the feed reaches index 1 (or the
|
|
189
|
-
log is empty): nothing older is left, hide the "load older" button.
|
|
190
|
-
- **`index`**: the latest server-confirmed state index, advanced by
|
|
191
|
-
snapshots and streamed events. This is the `lastSeenIndex` that makes
|
|
192
|
-
[cancellation](/guides/cancellation) exact.
|
|
193
|
-
- **`connection`**: a discriminated union of `{ status: 'idle' }`,
|
|
194
|
-
`{ status: 'connecting', reconnects, error }`,
|
|
195
|
-
`{ status: 'paused', reconnects, error }`,
|
|
196
|
-
`{ status: 'live', reconnects }`, or `{ status: 'closed' }`. A status
|
|
197
|
-
dot is one ternary away; "reconnecting…" is
|
|
198
|
-
`status === 'connecting' && reconnects > 0`; `error` (why the last
|
|
199
|
-
connection ended) only exists while disconnected, so impossible
|
|
200
|
-
states don't compile. It's honest about silence, too: the server
|
|
201
|
-
heartbeats the stream (`: ping` every 15s), and a client that hears
|
|
202
|
-
nothing for two beats treats the connection as dead and reconnects, so
|
|
203
|
-
`live` means bytes are flowing, not "the socket hasn't errored yet".
|
|
204
|
-
|
|
205
|
-
The provider opens the stream on mount, closes it on unmount, and
|
|
206
|
-
reconnects with backoff, resuming from `index`, when the connection
|
|
207
|
-
drops. Components that only write don't need the hook; `POST` to the
|
|
208
|
-
route directly, or call `ordersClient.session(id).push(...)` so a later
|
|
209
|
-
provider sees the same optimistic session.
|
|
210
|
-
|
|
211
|
-
Polling streams start fast and back off to a two-second interval when idle.
|
|
212
|
-
Idle is a polling state; the connection remains live and heartbeats continue.
|
|
213
|
-
When you call `push()` on an idle SSE stream, the client refreshes that stream
|
|
214
|
-
immediately, without reconnect backoff. It resumes from its existing `index`,
|
|
215
|
-
so concurrent events before the write's acknowledged index are still read.
|
|
216
|
-
Overlapping pushes share the refreshed stream. If a slow push is still pending
|
|
217
|
-
when the stream idles again, or its acknowledged events have not arrived, the
|
|
218
|
-
client refreshes again. WebSockets wake the session's attached read loop
|
|
219
|
-
without replacing the socket. This is internal and requires no component
|
|
220
|
-
configuration.
|
|
221
|
-
|
|
222
|
-
Use `reconnect()` after a custom REST request commits a change outside
|
|
223
|
-
`push()`. It is available on the session client and the `useSession()` result.
|
|
224
|
-
It immediately restarts the live subscription from its current `index`, even
|
|
225
|
-
when the stream is active. State, optimistic writes, and connection leases
|
|
226
|
-
are preserved. Without an active lease it does nothing.
|
|
227
|
-
|
|
228
|
-
`reconnect()` returns `void`; observe `connection` and `index` for progress.
|
|
229
|
-
SSE opens a fresh GET. A single-session WebSocket opens a fresh socket; a
|
|
230
|
-
multiplexed WebSocket re-subscribes only this session on its existing socket.
|
|
231
|
-
Other subscribed sessions keep running. Any outstanding write interrupted by
|
|
232
|
-
a socket replacement uses the normal idempotent retry path.
|
|
233
|
-
|
|
234
|
-
The server stops the old session lane before authorizing its replacement.
|
|
235
|
-
That lane stays closed if authorization denies access or fails. Successful
|
|
236
|
-
authorization catches up from the cursor. Already accepted writes can finish,
|
|
237
|
-
and failed subscription attempts follow the normal reconnect retry policy.
|
|
238
|
-
|
|
239
|
-
Configure `createClient({ reconnect })` on a shared client to choose each
|
|
240
|
-
retry's delay or return `false` to pause automatic reconnection. The callback
|
|
241
|
-
receives `{ sessionId, error, attempt }`; `error` is an `A2ClientError`, or
|
|
242
|
-
`null` when the stream ended cleanly. `attempt` starts at 1 and resets when
|
|
243
|
-
the connection becomes live or you call `reconnect()` explicitly.
|
|
244
|
-
|
|
245
|
-
A paused connection keeps the hydrated session, its writer, and its existing
|
|
246
|
-
state. Observe `connection.status === 'paused'` and use the hook's
|
|
247
|
-
`reconnect()` to resume when your application is ready. New consumers and
|
|
248
|
-
pushes do not automatically resume it. Configure the policy where you create
|
|
249
|
-
the client, without changing the hook's `hydrate` input.
|
|
250
|
-
|
|
251
|
-
That identity is an in-memory L1, not another source of truth. Repeated
|
|
252
|
-
`session(id)` calls reuse it while active and for five idle minutes by
|
|
253
|
-
default. A newer server fold advances it, pending pushes stay overlaid,
|
|
254
|
-
and a stale server render cannot rewind it. The optional IndexedDB cache
|
|
255
|
-
is the L2: it survives reloads; the memory runtime does not. The
|
|
256
|
-
stream is shared the same way: `connect()` takes a lease and returns
|
|
257
|
-
its release, so however many hooks, providers, or plain calls hold one
|
|
258
|
-
session open, the first lease connects and the last release closes
|
|
259
|
-
(`close()` is the hard stop).
|
|
260
|
-
|
|
261
|
-
One detail worth knowing: every push carries a client-generated event id.
|
|
262
|
-
That id is how the ack finds its optimistic entry, and it makes retrying
|
|
263
|
-
a failed `POST` idempotent. `push` retries `STORE_UNAVAILABLE` and client
|
|
264
|
-
transport failures, with a three-attempt budget.
|
|
265
|
-
|
|
266
|
-
Optimistic pushes are also what make [cancellation](/guides/cancellation)
|
|
267
|
-
feel instant: the `cancelled` event folds locally before the server ever
|
|
268
|
-
sees it.
|
|
269
|
-
|
|
270
|
-
Sessions can outlive the tab, too: hand `createReact` a `cache` and
|
|
271
|
-
revisits paint from the local copy, the stream resumes from the cached
|
|
272
|
-
frontier, and offline pushes queue and replay. See
|
|
273
|
-
[Local-first](/guides/local-first).
|
|
274
|
-
|
|
275
|
-
## Client-first sessions
|
|
276
|
-
|
|
277
|
-
Everything above assumes a server component hands the first fold down.
|
|
278
|
-
A client-rendered app often has nothing to hand: a sidebar showing ten
|
|
279
|
-
channel sessions, a user session read from deep inside a menu, panes
|
|
280
|
-
that mount from client-side navigation. No server fold, and no natural
|
|
281
|
-
provider boundary for components that reach sessions ad hoc.
|
|
282
|
-
|
|
283
|
-
For that shape, `experimental-a2/react` exports a standalone hook. Any
|
|
284
|
-
component can mount any session, no provider needed:
|
|
285
|
-
|
|
286
|
-
```tsx app/orders/[orderId]/order-badge.tsx
|
|
287
|
-
'use client'
|
|
288
|
-
import { useSession } from 'experimental-a2/react'
|
|
289
|
-
import { ordersClient } from './session'
|
|
290
|
-
|
|
291
|
-
export function OrderBadge({ orderId }: { orderId: string }) {
|
|
292
|
-
// your fetch (SWR, a router loader, a parent's payload): anything
|
|
293
|
-
// that lands the { state, index } your route returns (below), e.g.
|
|
294
|
-
// const { data } = useSWR(`/api/orders/${orderId}/state`)
|
|
295
|
-
const data = undefined // still loading
|
|
296
|
-
|
|
297
|
-
const { state } = useSession(ordersClient, orderId, { hydrate: data })
|
|
298
|
-
if (!data) return <span>…</span> // your loading state IS the pending state
|
|
299
|
-
return <span>{state.status}</span>
|
|
300
|
-
}
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
The fold comes from your own route, and the route is one line of A2:
|
|
304
|
-
|
|
305
|
-
```ts app/api/orders/[orderId]/state/route.ts
|
|
306
|
-
import { ordersServer } from '@/server/orders'
|
|
307
|
-
import { ordersReducer } from '@/reducer'
|
|
308
|
-
|
|
309
|
-
export async function GET(
|
|
310
|
-
_req: Request,
|
|
311
|
-
{ params }: { params: Promise<{ orderId: string }> },
|
|
312
|
-
) {
|
|
313
|
-
const { orderId } = await params
|
|
314
|
-
// here's where you'd do auth, or any other checks
|
|
315
|
-
return Response.json(
|
|
316
|
-
await ordersServer.session(orderId).state(ordersReducer),
|
|
317
|
-
)
|
|
318
|
-
}
|
|
319
|
-
```
|
|
320
|
-
|
|
321
|
-
Note what is not on the wire: a reducer name. The route calls
|
|
322
|
-
`session.state(reducer)` with the reducer by reference, the same
|
|
323
|
-
module your client bundle folds with. That matters because renaming a
|
|
324
|
-
reducer is the [snapshot invalidation knob](/concepts/state): if the
|
|
325
|
-
name traveled on the wire, every rename would open a version-skew
|
|
326
|
-
window between deployed clients and servers. By reference, the knob
|
|
327
|
-
stays free. And unlike a history slice, which is immutable once read
|
|
328
|
-
and earns a standard wire shape, a state read is a moving snapshot;
|
|
329
|
-
there is nothing for the library to standardize, so the read stays
|
|
330
|
-
behind your route, under your auth and caching policy.
|
|
331
|
-
|
|
332
|
-
`hydrate` is one atomic option: the `{ state, index }` pair
|
|
333
|
-
`session.state(reducer)` returns. State and its frontier travel
|
|
334
|
-
together or not at all; a half-present handoff cannot compile. While
|
|
335
|
-
it is `undefined` (your fetch has not landed), the hook holds the
|
|
336
|
-
stream and `state` is the reducer's `initialState`. There is no
|
|
337
|
-
pending flag on the result: whether the fold has been handed over is
|
|
338
|
-
your own input, so your data layer's loading state is the pending
|
|
339
|
-
state. When the value arrives, the session hydrates
|
|
340
|
-
and the stream connects at that frontier. The hold is deliberate:
|
|
341
|
-
connecting without a fold means replaying the whole log through the
|
|
342
|
-
browser, and the library never picks the expensive path silently. When
|
|
343
|
-
a full replay is what you want (a short log, a debug view), hand the
|
|
344
|
-
fold's true starting point:
|
|
345
|
-
`hydrate: { state: ordersReducer.initialState, index: 0 }`. State at
|
|
346
|
-
index 0 is the reducer's seed by definition, so the explicit replay
|
|
347
|
-
needs no special vocabulary; the stream then delivers every event
|
|
348
|
-
live.
|
|
349
|
-
|
|
350
|
-
The rest behaves like the provider. The handle is identity-mapped
|
|
351
|
-
(every hook and provider mounting the same session shares one runtime)
|
|
352
|
-
and each mount holds a lease on the shared stream (see
|
|
353
|
-
[the client identity note](#the-client-component)). A later, further
|
|
354
|
-
fold handed to the same session
|
|
355
|
-
advances it and a stale one is ignored, the same never-move-backward
|
|
356
|
-
rule as everywhere else. For presence, identity comes from the
|
|
357
|
-
client's `participant` (set once on `createClient`) or the hook's
|
|
358
|
-
`participant` option, which overrides it. The
|
|
359
|
-
result is exactly the shape the bound hook returns. There is no
|
|
360
|
-
loading or error member: the
|
|
361
|
-
library performs no fetch here, so your data layer's states are the
|
|
362
|
-
states, retried however your data layer retries.
|
|
363
|
-
|
|
364
|
-
`SessionProvider` keeps its required `initialState`/`initialIndex`.
|
|
365
|
-
The provider is the server-handoff tool; the hook is the client-first
|
|
366
|
-
tool.
|
|
367
|
-
|
|
368
|
-
## Keep the backend out of the bundle
|
|
369
|
-
|
|
370
|
-
The split is structural, not disciplinary. Core `experimental-a2` (the contract,
|
|
371
|
-
reducers, errors) imports no backend, ever; only `experimental-a2/server` can reach
|
|
372
|
-
one, and its exports map resolves to a loud error under the browser
|
|
373
|
-
condition. The layout above rides that:
|
|
374
|
-
|
|
375
|
-
1. `contracts.ts` and `reducer.ts` import nothing but schemas and `experimental-a2`,
|
|
376
|
-
isomorphic by construction.
|
|
377
|
-
2. The session module imports the reducer, never `experimental-a2/server`. Client
|
|
378
|
-
components don't need the server's *type*, even: the factory carries
|
|
379
|
-
all the typing.
|
|
380
|
-
|
|
381
|
-
Belt and suspenders: put `import 'server-only'` at the top of your
|
|
382
|
-
server module and enable `verbatimModuleSyntax` in your tsconfig; an
|
|
383
|
-
accidental value import then fails your build too, with your own error
|
|
384
|
-
message.
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Local-first
|
|
3
|
-
description: "Cache one A2 session in the browser: instant revisits, stream resumption, and offline writes."
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
## One option
|
|
7
|
-
|
|
8
|
-
```tsx app/orders/[orderId]/session.ts
|
|
9
|
-
'use client'
|
|
10
|
-
import { createReact } from 'experimental-a2/react'
|
|
11
|
-
import { indexedDb } from 'experimental-a2/cache-indexeddb'
|
|
12
|
-
import { ordersReducer } from '@/reducer'
|
|
13
|
-
|
|
14
|
-
export const { SessionProvider, useSession } = createReact({
|
|
15
|
-
reducer: ordersReducer,
|
|
16
|
-
cache: indexedDb(),
|
|
17
|
-
})
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
This is the [session module](/guides/react#the-session-module) from Live
|
|
21
|
-
UI with one addition. The cache is created here, in client code, where
|
|
22
|
-
live objects belong; server components keep importing `SessionProvider`
|
|
23
|
-
exactly as before; nothing crosses the server boundary.
|
|
24
|
-
|
|
25
|
-
With `cache` set:
|
|
26
|
-
|
|
27
|
-
- **Revisits paint instantly.** State folds from the local copy before the
|
|
28
|
-
network says a word. Works offline too.
|
|
29
|
-
- **The stream resumes from the cached frontier.** Reconnects fetch only
|
|
30
|
-
what this browser hasn't seen, no matter how long it's been.
|
|
31
|
-
- **Pushes queue while offline**, and replay, in order, when the
|
|
32
|
-
connection returns.
|
|
33
|
-
|
|
34
|
-
Without it, nothing changes; the provider behaves exactly as before.
|
|
35
|
-
|
|
36
|
-
## Why this is safe
|
|
37
|
-
|
|
38
|
-
Local-first is usually hard because it means syncing state, and state
|
|
39
|
-
needs merging. A2 caches an append-only log with server-assigned positions
|
|
40
|
-
instead. A cached event can never be *wrong*; this browser can only be
|
|
41
|
-
*behind*. Catching up is fetching events after an index, which is what
|
|
42
|
-
`stream({ startAfter })` does anyway. There's no merge function because
|
|
43
|
-
there's nothing to merge.
|
|
44
|
-
|
|
45
|
-
## What gets stored
|
|
46
|
-
|
|
47
|
-
Three things, in IndexedDB:
|
|
48
|
-
|
|
49
|
-
| Stored | Invalidated by |
|
|
50
|
-
| --------------------------------------------- | ------------------------------------------------------- |
|
|
51
|
-
| the session's events | nothing; they're immutable |
|
|
52
|
-
| a folded snapshot, keyed by reducer name | a reducer rename (or a `stateSchema` mismatch, when the reducer declares one): discarded, refolded from cached events |
|
|
53
|
-
| pending pushes | the server's ack |
|
|
54
|
-
|
|
55
|
-
Note the middle row: the reducer's `name` does the same job here that it
|
|
56
|
-
does for [server snapshots](/concepts/state#snapshots-are-a-cache). One
|
|
57
|
-
string, both sides of the wire.
|
|
58
|
-
|
|
59
|
-
Deleting the cache is always safe. It's a replica of what this browser
|
|
60
|
-
already saw; the log on the server stays the only authority.
|
|
61
|
-
|
|
62
|
-
## Offline writes
|
|
63
|
-
|
|
64
|
-
`push` while offline persists the events, client ids and all, and
|
|
65
|
-
applies them optimistically, same as always. On reconnect they replay in
|
|
66
|
-
order. The replay is idempotent by construction: an identical batch is an
|
|
67
|
-
idempotent success, so a flaky reconnect, or a second tab replaying the
|
|
68
|
-
same queue, causes no duplicates. Rejections refold without the event,
|
|
69
|
-
exactly like an online push the server refused.
|
|
70
|
-
|
|
71
|
-
What this is *not*: local execution. Handlers run on the server, when the
|
|
72
|
-
events arrive. An offline push is a queued intent, not a completed
|
|
73
|
-
action. Design the UI accordingly ("sending…" is honest; "sent" is not).
|
|
74
|
-
|
|
75
|
-
## Logout
|
|
76
|
-
|
|
77
|
-
Cached events outlive the session cookie. If your app authenticates,
|
|
78
|
-
clear the cache when the user leaves:
|
|
79
|
-
|
|
80
|
-
```ts
|
|
81
|
-
// in the browser, wherever your logout flow runs:
|
|
82
|
-
await cache.clear() // everything, or cache.clear(sessionId) for one session
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
That's the one obligation `cache` puts on you. The cache never *grants*
|
|
86
|
-
access (your routes still authorize every read and write) but it
|
|
87
|
-
*retains* what was legitimately seen, and retained data belongs to the
|
|
88
|
-
person who saw it.
|