experimental-a2 0.16.1 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +352 -0
- package/dist/{actor-ohPC-81x.d.ts → actor-DBbe5xeE.d.ts} +21 -11
- package/dist/actor-DBbe5xeE.d.ts.map +1 -0
- package/dist/actor-client.d.ts +2 -5
- package/dist/actor-client.d.ts.map +1 -1
- package/dist/actor-client.js +13 -4
- package/dist/actor-client.js.map +1 -1
- package/dist/actor-react.d.ts +5 -3
- package/dist/actor-react.d.ts.map +1 -1
- package/dist/actor-react.js +20 -10
- package/dist/actor-react.js.map +1 -1
- package/dist/{actor-shared-B5tJfzt-.js → actor-shared-t7w8xIKP.js} +12 -7
- package/dist/actor-shared-t7w8xIKP.js.map +1 -0
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +42 -23
- package/dist/actor.js.map +1 -1
- package/dist/{ai-DgOBltJ_.js → ai-UrWfKeuv.js} +621 -216
- package/dist/ai-UrWfKeuv.js.map +1 -0
- package/dist/ai-client.d.ts +27 -0
- package/dist/ai-client.d.ts.map +1 -0
- package/dist/ai-client.js +29 -0
- package/dist/ai-client.js.map +1 -0
- package/dist/ai-react.d.ts +96 -0
- package/dist/ai-react.d.ts.map +1 -0
- package/dist/ai-react.js +142 -0
- package/dist/ai-react.js.map +1 -0
- package/dist/ai-server.browser.js +2 -2
- package/dist/ai-server.browser.js.map +1 -1
- package/dist/ai-server.d.ts +80 -36
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +432 -258
- package/dist/ai-server.js.map +1 -1
- package/dist/{ai-CrEf6p_W.d.ts → ai-vpGtcaCB.d.ts} +114 -100
- package/dist/ai-vpGtcaCB.d.ts.map +1 -0
- package/dist/ai.d.ts +2 -2
- package/dist/ai.js +2 -2
- package/dist/{client-BYzHjkwU.js → client-COaiaDub.js} +447 -80
- package/dist/client-COaiaDub.js.map +1 -0
- package/dist/{client-CzyacQpJ.d.ts → client-qRnRUzBS.d.ts} +14 -13
- package/dist/client-qRnRUzBS.d.ts.map +1 -0
- package/dist/client.d.ts +4 -2
- package/dist/client.js +1 -1
- package/dist/{contract-CKRg_E4q.js → contract-BYKASlCV.js} +12 -21
- package/dist/contract-BYKASlCV.js.map +1 -0
- package/dist/{idempotent-replay-DVOlyYbx.js → idempotent-replay-JdgMQRRm.js} +2 -2
- package/dist/{idempotent-replay-DVOlyYbx.js.map → idempotent-replay-JdgMQRRm.js.map} +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/{errors-DvhSXnxk.d.ts → initial-snapshot-BrLso5x_.d.ts} +7 -3
- package/dist/initial-snapshot-BrLso5x_.d.ts.map +1 -0
- package/dist/{internal-Dq2qYxou.js → internal-CbVvEwad.js} +23 -2
- package/dist/internal-CbVvEwad.js.map +1 -0
- package/dist/otel.d.ts +1 -1
- package/dist/react-BbJ-g6Uu.d.ts +86 -0
- package/dist/react-BbJ-g6Uu.d.ts.map +1 -0
- package/dist/react.d.ts +2 -122
- package/dist/react.js +107 -100
- package/dist/react.js.map +1 -1
- package/dist/reducer-BAXc8JyF.js +87 -0
- package/dist/reducer-BAXc8JyF.js.map +1 -0
- package/dist/{reducer-otzHjuJj.d.ts → reducer-BYu6DT7h.d.ts} +45 -29
- package/dist/reducer-BYu6DT7h.d.ts.map +1 -0
- package/dist/reducer-runtime-DQwz_J59.js +28 -0
- package/dist/reducer-runtime-DQwz_J59.js.map +1 -0
- package/dist/scheduler-qstash.d.ts +3 -3
- package/dist/scheduler-qstash.d.ts.map +1 -1
- package/dist/scheduler-qstash.js +33 -10
- package/dist/scheduler-qstash.js.map +1 -1
- package/dist/{scheduler-task-BpzhPnRS.js → scheduler-task-X1SToDjP.js} +18 -3
- package/dist/scheduler-task-X1SToDjP.js.map +1 -0
- package/dist/scheduler-vercel.d.ts +4 -9
- package/dist/scheduler-vercel.d.ts.map +1 -1
- package/dist/scheduler-vercel.js +25 -44
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/{server-DbD7IJVK.d.ts → server-CGM8WzRG.d.ts} +87 -26
- package/dist/server-CGM8WzRG.d.ts.map +1 -0
- package/dist/{server-BD5ckxZb.js → server-Rj64fNvX.js} +854 -202
- package/dist/server-Rj64fNvX.js.map +1 -0
- package/dist/server.d.ts +4 -4
- package/dist/server.js +2 -2
- package/dist/{errors-DCk6ch5n.js → session-clear-DGI4jTQ5.js} +17 -2
- package/dist/session-clear-DGI4jTQ5.js.map +1 -0
- package/dist/session-clear-Dshpuhff.d.ts +12 -0
- package/dist/session-clear-Dshpuhff.d.ts.map +1 -0
- package/dist/{store-DyZM6fS5.d.ts → store-Gpkq7WBo.d.ts} +44 -3
- package/dist/store-Gpkq7WBo.d.ts.map +1 -0
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.d.ts.map +1 -1
- package/dist/store-memory.js +182 -69
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-CmxUbV93.js → store-polling-4_y8lETm.js} +21 -4
- package/dist/store-polling-4_y8lETm.js.map +1 -0
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.d.ts.map +1 -1
- package/dist/store-postgres.js +399 -108
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-presence-polling-C7-XZyW9.js → store-presence-polling-Q1C-k2dd.js} +3 -3
- package/dist/{store-presence-polling-C7-XZyW9.js.map → store-presence-polling-Q1C-k2dd.js.map} +1 -1
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.d.ts.map +1 -1
- package/dist/store-redis-http.js +293 -11
- package/dist/store-redis-http.js.map +1 -1
- package/dist/store-redis-notify-tqWNnMRu.js +2374 -0
- package/dist/store-redis-notify-tqWNnMRu.js.map +1 -0
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.d.ts.map +1 -1
- package/dist/store-redis.js +11 -11
- package/dist/store-redis.js.map +1 -1
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.d.ts.map +1 -1
- package/dist/store-sqlite.js +166 -32
- package/dist/store-sqlite.js.map +1 -1
- package/dist/stream-buffer-Cx7AAPGq.js +23 -0
- package/dist/stream-buffer-Cx7AAPGq.js.map +1 -0
- package/dist/{telemetry-B5jzpy6y.d.ts → telemetry-cZH-cibj.d.ts} +2 -2
- package/dist/telemetry-cZH-cibj.d.ts.map +1 -0
- package/dist/{wire-BO5wWCb1.js → wire-DjfHOvdW.js} +132 -10
- package/dist/wire-DjfHOvdW.js.map +1 -0
- package/docs/01-quickstart.mdx +2 -2
- package/docs/actors/01-introduction.mdx +4 -0
- package/docs/actors/04-routes.mdx +14 -1
- package/docs/concepts/01-contracts.mdx +95 -0
- package/docs/concepts/04-state.mdx +3 -3
- package/docs/guides/01-timers.mdx +22 -14
- package/docs/guides/02-cancellation.mdx +1 -1
- package/docs/guides/03-react.mdx +268 -328
- package/docs/guides/04-local-first.mdx +32 -73
- package/docs/guides/05-production.mdx +40 -27
- package/docs/guides/06-ai-agents.mdx +671 -581
- package/docs/guides/09-presence.mdx +3 -7
- package/docs/guides/10-transports.mdx +5 -5
- package/docs/index.mdx +10 -18
- package/docs/reference/01-api.mdx +656 -194
- package/docs/reference/02-errors.mdx +2 -1
- package/examples/playground/DEPLOY.md +52 -6
- package/examples/playground/app/a0/[appId]/loading.tsx +16 -0
- package/examples/playground/app/a0/[appId]/page.tsx +48 -0
- package/examples/playground/app/a0/a0.css +818 -0
- package/examples/playground/app/a0/app-list.tsx +211 -0
- package/examples/playground/app/a0/app-view.tsx +55 -0
- package/examples/playground/app/a0/apps/[appId]/preview/route.ts +27 -0
- package/examples/playground/app/a0/apps/[appId]/route.ts +23 -0
- package/examples/playground/app/a0/apps.test.ts +116 -0
- package/examples/playground/app/a0/apps.ts +87 -0
- package/examples/playground/app/a0/chat.tsx +364 -0
- package/examples/playground/app/a0/columns.tsx +109 -0
- package/examples/playground/app/a0/context-menu.tsx +116 -0
- package/examples/playground/app/a0/cookies.ts +46 -0
- package/examples/playground/app/a0/crash-on-request.ts +72 -0
- package/examples/playground/app/a0/crashes/route.ts +3 -0
- package/examples/playground/app/a0/crashes.test.ts +33 -0
- package/examples/playground/app/a0/crashes.ts +28 -0
- package/examples/playground/app/a0/devtools.tsx +164 -0
- package/examples/playground/app/a0/events/route.ts +6 -0
- package/examples/playground/app/a0/flags.ts +30 -0
- package/examples/playground/app/a0/layout.tsx +38 -0
- package/examples/playground/app/a0/list/route.ts +10 -0
- package/examples/playground/app/a0/model.test.ts +56 -0
- package/examples/playground/app/a0/model.ts +58 -0
- package/examples/playground/app/a0/page.tsx +14 -0
- package/examples/playground/app/a0/preview-pane.tsx +145 -0
- package/examples/playground/app/a0/preview.test.ts +53 -0
- package/examples/playground/app/a0/preview.ts +26 -0
- package/examples/playground/app/a0/sandbox-log.ts +69 -0
- package/examples/playground/app/a0/sandbox.ts +191 -0
- package/examples/playground/app/a0/sandboxes/scheduler/route.ts +6 -0
- package/examples/playground/app/a0/scheduler/route.ts +7 -0
- package/examples/playground/app/a0/server.ts +158 -0
- package/examples/playground/app/a0/session.ts +47 -0
- package/examples/playground/app/a0/shell.tsx +150 -0
- package/examples/playground/app/a0/short/events/route.ts +9 -0
- package/examples/playground/app/a0/splitter.tsx +120 -0
- package/examples/playground/app/agent/[agentId]/agent-client.tsx +144 -174
- package/examples/playground/app/agent/[agentId]/command-receipt.test.tsx +65 -0
- package/examples/playground/app/agent/[agentId]/command-receipt.tsx +27 -0
- package/examples/playground/app/agent/[agentId]/compaction-event.tsx +25 -24
- package/examples/playground/app/agent/[agentId]/compaction-panel.test.tsx +119 -0
- package/examples/playground/app/agent/[agentId]/compaction-panel.tsx +208 -165
- package/examples/playground/app/agent/[agentId]/page.tsx +2 -16
- package/examples/playground/app/agent/events/route.ts +5 -2
- package/examples/playground/app/agent/ingress.test.ts +98 -0
- package/examples/playground/app/agent/model-input.test.tsx +58 -0
- package/examples/playground/app/agent/model-input.tsx +115 -0
- package/examples/playground/app/agent/model.test.ts +73 -0
- package/examples/playground/app/agent/model.ts +58 -26
- package/examples/playground/app/agent/new-agent-session.tsx +37 -19
- package/examples/playground/app/agent/page.tsx +4 -4
- package/examples/playground/app/agent/server.ts +15 -14
- package/examples/playground/app/agent/session.ts +5 -6
- package/examples/playground/app/agent/telemetry.test.ts +168 -0
- package/examples/playground/app/agent/telemetry.ts +30 -0
- package/examples/playground/app/agent/trace.test.ts +23 -0
- package/examples/playground/app/agent/trace.ts +18 -0
- package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +11 -11
- package/examples/playground/app/canvas/[canvasId]/page.tsx +1 -2
- package/examples/playground/app/canvas/[canvasId]/session.ts +2 -2
- package/examples/playground/app/canvas/model.ts +53 -53
- package/examples/playground/app/chat/[chatId]/agent-state.test.ts +176 -0
- package/examples/playground/app/chat/[chatId]/agent-stream-drawer.tsx +16 -1
- package/examples/playground/app/chat/[chatId]/chat-client.tsx +55 -77
- package/examples/playground/app/chat/[chatId]/chat-view.ts +37 -3
- package/examples/playground/app/chat/[chatId]/page.tsx +7 -15
- package/examples/playground/app/chat/[chatId]/session.ts +2 -19
- package/examples/playground/app/chat/agent/route.ts +9 -0
- package/examples/playground/app/chat/model.test.ts +46 -119
- package/examples/playground/app/chat/model.ts +82 -128
- package/examples/playground/app/chat/scheduler/route.ts +2 -2
- package/examples/playground/app/chat/server.test.ts +108 -0
- package/examples/playground/app/chat/server.ts +69 -73
- package/examples/playground/app/components/activity-feed.tsx +30 -1
- package/examples/playground/app/components/activity-history.test.tsx +35 -0
- package/examples/playground/app/components/demo-icons.tsx +89 -0
- package/examples/playground/app/components/demos.tsx +33 -0
- package/examples/playground/app/components/fast-link.test.tsx +58 -0
- package/examples/playground/app/components/fast-link.tsx +55 -0
- package/examples/playground/app/components/site-header.tsx +96 -0
- package/examples/playground/app/counter/counter-client.tsx +3 -3
- package/examples/playground/app/counter/model.test.ts +3 -3
- package/examples/playground/app/counter/model.ts +11 -21
- package/examples/playground/app/counter/page.tsx +2 -15
- package/examples/playground/app/counter/session.ts +2 -2
- package/examples/playground/app/documents/[documentId]/code-editor.tsx +12 -1
- package/examples/playground/app/documents/[documentId]/document-client.tsx +10 -11
- package/examples/playground/app/documents/[documentId]/page.tsx +1 -2
- package/examples/playground/app/documents/[documentId]/session.ts +2 -2
- package/examples/playground/app/documents/model.ts +27 -27
- package/examples/playground/app/globals.css +420 -207
- package/examples/playground/app/initial-state.test.tsx +54 -0
- package/examples/playground/app/layout.tsx +7 -23
- package/examples/playground/app/orders/[orderId]/page.tsx +1 -2
- package/examples/playground/app/orders/[orderId]/session.ts +2 -2
- package/examples/playground/app/orders/model.ts +49 -49
- package/examples/playground/app/page.tsx +13 -102
- package/examples/playground/app/recovery/[recoveryId]/page.tsx +1 -2
- package/examples/playground/app/recovery/[recoveryId]/session.ts +2 -2
- package/examples/playground/app/recovery/model.ts +30 -30
- package/examples/playground/instrumentation.ts +5 -0
- package/examples/playground/package.json +10 -2
- package/examples/playground/scripts/a0-template.ts +77 -0
- package/examples/playground/vercel.json +18 -0
- package/package.json +3 -1
- package/src/actor-client.ts +16 -4
- package/src/actor-react.ts +25 -11
- package/src/actor-shared.ts +21 -6
- package/src/actor.ts +76 -31
- package/src/ai-agent-actions.ts +219 -0
- package/src/ai-agent.ts +2343 -0
- package/src/ai-app-state.ts +91 -0
- package/src/ai-client-state.ts +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-message-base.ts +209 -0
- package/src/ai-message-projection.ts +262 -64
- package/src/ai-progress-batches.ts +26 -0
- package/src/ai-projector.ts +95 -14
- package/src/ai-react.ts +146 -0
- package/src/ai-server.browser.ts +2 -2
- package/src/ai-server.ts +20 -1964
- 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 +45 -0
- package/src/postgres-notification-scope.ts +14 -3
- package/src/react.ts +171 -206
- package/src/redis-http-reply.ts +256 -0
- package/src/redis-http-stream.ts +73 -0
- package/src/redis-http-subscriptions.ts +13 -5
- package/src/reducer-runtime.ts +52 -0
- package/src/reducer.ts +92 -10
- package/src/scheduler-qstash.ts +49 -20
- package/src/scheduler-task.ts +34 -2
- package/src/scheduler-vercel.ts +32 -84
- package/src/server-fetch.ts +139 -35
- package/src/server.ts +957 -237
- package/src/session-clear.ts +23 -0
- package/src/session-socket.ts +154 -49
- package/src/sse.ts +71 -4
- package/src/store-memory.ts +220 -78
- package/src/store-polling.ts +18 -4
- package/src/store-postgres.ts +461 -114
- package/src/store-redis-core.ts +1260 -434
- package/src/store-redis-http.ts +18 -2
- package/src/store-redis-notify.ts +260 -85
- package/src/store-redis.ts +19 -9
- package/src/store-sqlite.ts +259 -45
- package/src/store.ts +46 -1
- package/src/stream-buffer.ts +43 -0
- package/src/telemetry.ts +13 -1
- package/src/wire.ts +241 -13
- package/dist/actor-ohPC-81x.d.ts.map +0 -1
- package/dist/actor-shared-B5tJfzt-.js.map +0 -1
- package/dist/ai-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/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/guides/03-react.mdx
CHANGED
|
@@ -1,384 +1,324 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Live UI with React
|
|
3
|
-
description: "
|
|
3
|
+
description: "Start from a snapshot, fold new events, and push optimistic writes through one shared session."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
##
|
|
6
|
+
## One session, one state
|
|
7
7
|
|
|
8
|
-
A
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
A contract can publish one reducer. The client starts from a snapshot of
|
|
9
|
+
that state and folds subsequent events locally. Initial data from a server
|
|
10
|
+
component is optional. Without it, the server sends a snapshot as the first
|
|
11
|
+
data frame of the subscription, then streams events after its index.
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
The same connection carries bootstrap and live events, over SSE or WebSocket.
|
|
14
|
+
Events appended while the snapshot is read remain in the durable log and
|
|
15
|
+
are delivered after its index. Reconnecting resumes after the last delivered
|
|
16
|
+
position without fetching another snapshot.
|
|
16
17
|
|
|
17
|
-
This is session sync.
|
|
18
|
-
|
|
19
|
-
collection views in your [application database](/guides/application-data).
|
|
18
|
+
This is session sync. A client follows a known session ID. Keep collection
|
|
19
|
+
queries across sessions in your [application database](/guides/application-data).
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
reducer, and the provider + hook it returns.
|
|
21
|
+
## Publish the state
|
|
23
22
|
|
|
24
|
-
|
|
23
|
+
The reducer lives with the event vocabulary, in a shared module:
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
```ts model.ts
|
|
26
|
+
import { contract } from 'experimental-a2'
|
|
27
|
+
import { z } from 'zod'
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
export const counter = contract({
|
|
30
|
+
name: 'counter',
|
|
31
|
+
events: { added: z.number() },
|
|
32
|
+
reducer: {
|
|
33
|
+
name: 'count.v1',
|
|
34
|
+
initialState: 0,
|
|
35
|
+
fold: (state, event) => state + event.payload,
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The reducer name identifies its snapshot format. Change it when the fold's
|
|
41
|
+
meaning or stored shape changes. The server and client must use the same
|
|
42
|
+
version. A fold returning a discriminated union may need `as const` on a
|
|
43
|
+
returned literal or an explicit return type. The standalone `reducer`
|
|
44
|
+
builder fixes the state type before its `.fold()` call.
|
|
45
|
+
|
|
46
|
+
Additional server-side reducers can be read with `session.state(reducer)`;
|
|
47
|
+
those reads do not publish another client state.
|
|
48
|
+
|
|
49
|
+
```ts server/counter.ts
|
|
50
|
+
import { createServer } from 'experimental-a2/server'
|
|
51
|
+
import { counter } from '../model'
|
|
32
52
|
|
|
33
|
-
export const
|
|
34
|
-
export const POST = ordersServer.fetch
|
|
53
|
+
export const counterServer = createServer({ contract: counter })
|
|
35
54
|
```
|
|
36
55
|
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
}
|
|
56
|
+
```ts app/api/counter/route.ts
|
|
57
|
+
import { counterServer } from '../../../server/counter'
|
|
66
58
|
|
|
67
|
-
export const
|
|
59
|
+
export const GET = counterServer.fetch
|
|
60
|
+
export const POST = counterServer.fetch
|
|
68
61
|
```
|
|
69
62
|
|
|
70
|
-
|
|
63
|
+
`GET` serves the subscription and bounded history reads. `POST` appends
|
|
64
|
+
writes. Wrap `server.fetch` with its `authorize` option for application
|
|
65
|
+
checks. Bootstrap is authorized as a stream operation with `bootstrap: true`
|
|
66
|
+
before reading state.
|
|
67
|
+
|
|
68
|
+
## Bind React once
|
|
71
69
|
|
|
72
|
-
|
|
70
|
+
The factory binds the client's types into the Provider and hook. It does
|
|
71
|
+
not create a separate connection pool.
|
|
73
72
|
|
|
74
|
-
```tsx app/
|
|
73
|
+
```tsx app/counter/session.ts
|
|
75
74
|
'use client'
|
|
76
75
|
import { createClient } from 'experimental-a2/client'
|
|
77
76
|
import { createReact } from 'experimental-a2/react'
|
|
78
|
-
import {
|
|
77
|
+
import { counter } from '../../model'
|
|
79
78
|
|
|
80
|
-
export const
|
|
81
|
-
|
|
82
|
-
api: '/api/
|
|
79
|
+
export const counterClient = createClient({
|
|
80
|
+
contract: counter,
|
|
81
|
+
api: '/api/counter',
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
export const { SessionProvider, useSession } = createReact({
|
|
85
|
+
client: counterClient,
|
|
83
86
|
})
|
|
84
|
-
export const { SessionProvider, useSession } = createReact({ client: ordersClient })
|
|
85
87
|
```
|
|
86
88
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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.
|
|
89
|
+
```tsx app/counter/counter.tsx
|
|
90
|
+
'use client'
|
|
91
|
+
import { useSession } from './session'
|
|
94
92
|
|
|
95
|
-
|
|
93
|
+
export function Counter() {
|
|
94
|
+
const { state, connection, push } = useSession()
|
|
95
|
+
const error = 'error' in connection ? connection.error : null
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
return (
|
|
98
|
+
<>
|
|
99
|
+
<button onClick={() => void push({ type: 'added', payload: 1 })}>
|
|
100
|
+
Count: {state}
|
|
101
|
+
</button>
|
|
102
|
+
{error && <p role="alert">{error.message}</p>}
|
|
103
|
+
</>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
```
|
|
99
107
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
108
|
+
`state` has the reducer's client state type. A supplied snapshot seeds the
|
|
109
|
+
first render; otherwise the client displays `reducer.initialState` (through
|
|
110
|
+
the reducer's client projection when present). The connection snapshot
|
|
111
|
+
replaces that seed atomically, then new events update it. The seed can look
|
|
112
|
+
empty while an existing session loads. Supply a server snapshot to render
|
|
113
|
+
its current state immediately.
|
|
114
|
+
|
|
115
|
+
`connection` stays `connecting` until bootstrap completes. Failures use its
|
|
116
|
+
existing error and retry policy. A slow snapshot can keep loading while the
|
|
117
|
+
connection sends heartbeats. The ordinary stream stall watchdog handles a
|
|
118
|
+
silent connection. A reconnect preserves the last state.
|
|
119
|
+
|
|
120
|
+
A push made during bootstrap is sent right away; it does not wait for the
|
|
121
|
+
snapshot. Its optimistic update appears once the snapshot is applied. If
|
|
122
|
+
the push was already sent when the snapshot arrives and has no
|
|
123
|
+
acknowledgement yet, it stays hidden until the acknowledgement arrives,
|
|
124
|
+
because the snapshot may already contain it. Pushes queued behind it stay
|
|
125
|
+
hidden with it, so the view never skips a write. As usual, the push promise
|
|
126
|
+
resolves on the server acknowledgement; `.confirmed` waits for delivery
|
|
127
|
+
through the stream, and the bootstrap snapshot counts as delivery through
|
|
128
|
+
its index.
|
|
129
|
+
|
|
130
|
+
## Without server rendering
|
|
131
|
+
|
|
132
|
+
A Provider only needs the session ID. It supplies scope and optional data;
|
|
133
|
+
mounted hooks own connection leases.
|
|
134
|
+
|
|
135
|
+
```tsx app/counter/page.tsx
|
|
136
|
+
import { Counter } from './counter'
|
|
103
137
|
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
138
|
|
|
139
|
+
export default function Page() {
|
|
115
140
|
return (
|
|
116
|
-
<SessionProvider
|
|
117
|
-
|
|
118
|
-
initialState={state}
|
|
119
|
-
initialIndex={index}
|
|
120
|
-
>
|
|
121
|
-
<OrderClient />
|
|
141
|
+
<SessionProvider sessionId="shared">
|
|
142
|
+
<Counter />
|
|
122
143
|
</SessionProvider>
|
|
123
144
|
)
|
|
124
145
|
}
|
|
125
146
|
```
|
|
126
147
|
|
|
127
|
-
|
|
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.)
|
|
148
|
+
You can also omit the Provider. The bound hook already knows its client:
|
|
132
149
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
```tsx app/orders/[orderId]/order-client.tsx
|
|
150
|
+
```tsx app/counter/standalone.tsx
|
|
136
151
|
'use client'
|
|
137
152
|
import { useSession } from './session'
|
|
138
153
|
|
|
139
|
-
export function
|
|
140
|
-
const { state, push } = useSession()
|
|
141
|
-
|
|
154
|
+
export function StandaloneCounter({ sessionId }: { sessionId: string }) {
|
|
155
|
+
const { state, push } = useSession({ sessionId })
|
|
142
156
|
return (
|
|
143
|
-
<button
|
|
144
|
-
|
|
157
|
+
<button
|
|
158
|
+
onClick={() => void push({ type: 'added', payload: 1 })}
|
|
159
|
+
>
|
|
160
|
+
{state}
|
|
145
161
|
</button>
|
|
146
162
|
)
|
|
147
163
|
}
|
|
148
164
|
```
|
|
149
165
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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>
|
|
166
|
+
The direct `useSession(client, sessionId, options?)` export is available
|
|
167
|
+
when you do not use the factory. It has the same bootstrap and connection
|
|
168
|
+
ownership behavior.
|
|
169
|
+
|
|
170
|
+
## Seed the first render
|
|
171
|
+
|
|
172
|
+
Server rendering can skip the bootstrap read. Supply the state and its
|
|
173
|
+
index together:
|
|
174
|
+
|
|
175
|
+
```tsx app/seeded-counter/page.tsx
|
|
176
|
+
import { counterServer } from '../../server/counter'
|
|
177
|
+
import { Counter } from '../counter/counter'
|
|
178
|
+
import { SessionProvider } from '../counter/session'
|
|
179
|
+
|
|
180
|
+
export default async function Page() {
|
|
181
|
+
const initial = await counterServer.session('shared').state()
|
|
182
|
+
return (
|
|
183
|
+
<SessionProvider
|
|
184
|
+
sessionId="shared"
|
|
185
|
+
initial={initial}
|
|
186
|
+
>
|
|
187
|
+
<Counter />
|
|
188
|
+
</SessionProvider>
|
|
189
|
+
)
|
|
300
190
|
}
|
|
301
191
|
```
|
|
302
192
|
|
|
303
|
-
|
|
193
|
+
Index 10 means this state already includes events through 10. The stream
|
|
194
|
+
starts after 10. Index 0 is valid when paired with the reducer's initial
|
|
195
|
+
state; that explicitly requests replay from the beginning. An index by
|
|
196
|
+
itself cannot describe a reducer state.
|
|
197
|
+
|
|
198
|
+
A standalone hook can receive the same pair as `initial`. Omitting
|
|
199
|
+
it enables automatic bootstrap. For an asynchronous server read, pass its
|
|
200
|
+
Promise as described below.
|
|
201
|
+
|
|
202
|
+
SSR uses render-local snapshots. One request's seed does not modify another
|
|
203
|
+
request's state, and browser hydration first renders the supplied snapshot
|
|
204
|
+
even when the shared browser cache has already advanced.
|
|
205
|
+
|
|
206
|
+
`hydrated` distinguishes the reducer seed from a known state/index snapshot.
|
|
207
|
+
It is `false` until a supplied snapshot or subscription bootstrap initializes
|
|
208
|
+
the session, including while a cold preload is pending or rejected. A snapshot
|
|
209
|
+
at index 0 counts. Once initialized, it stays `true` through disconnections,
|
|
210
|
+
reconnects, and disabled readers while the session retains its state. A new
|
|
211
|
+
session without a seed starts at `false` again.
|
|
212
|
+
|
|
213
|
+
Use `hydrated` when a decision needs initialized state, such as distinguishing
|
|
214
|
+
an empty queue from one that has not loaded. `connection` describes the
|
|
215
|
+
transport independently. A hydrated snapshot may be stale and does not imply
|
|
216
|
+
permission to write. Imperative code reads the same flag from
|
|
217
|
+
`client.session(id).getSnapshot().hydrated`.
|
|
218
|
+
|
|
219
|
+
## Start a preload on the server
|
|
220
|
+
|
|
221
|
+
`initial` also accepts a stable Promise of a snapshot. A pending preload
|
|
222
|
+
blocks only cold startup: A2 waits before opening its subscription, displaying
|
|
223
|
+
`reducer.initialState` without suspending the UI by default. A snapshot result
|
|
224
|
+
replaces that seed and skips the
|
|
225
|
+
bootstrap read. A `null` or `undefined` result uses ordinary subscription
|
|
226
|
+
bootstrap. No additional request is started to resolve the Promise.
|
|
227
|
+
|
|
228
|
+
```tsx app/preloaded-counter/page.tsx
|
|
229
|
+
import { Suspense } from 'react'
|
|
230
|
+
import { counterServer } from '../../server/counter'
|
|
231
|
+
import { SessionProvider } from '../counter/session'
|
|
232
|
+
import { PreloadedCounter } from './counter'
|
|
233
|
+
|
|
234
|
+
export default function Page() {
|
|
235
|
+
const initial = counterServer.session('shared').state()
|
|
236
|
+
return (
|
|
237
|
+
<SessionProvider sessionId="shared" initial={initial}>
|
|
238
|
+
<h1>Counter</h1>
|
|
239
|
+
<Suspense fallback={<p>Loading counter…</p>}>
|
|
240
|
+
<PreloadedCounter />
|
|
241
|
+
</Suspense>
|
|
242
|
+
</SessionProvider>
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
```
|
|
304
246
|
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
import {
|
|
247
|
+
```tsx app/preloaded-counter/counter.tsx
|
|
248
|
+
'use client'
|
|
249
|
+
import { useSession } from '../counter/session'
|
|
308
250
|
|
|
309
|
-
export
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
251
|
+
export function PreloadedCounter() {
|
|
252
|
+
const { state, push } = useSession({ suspend: true })
|
|
253
|
+
return (
|
|
254
|
+
<button
|
|
255
|
+
onClick={() => void push({ type: 'added', payload: 1 })}
|
|
256
|
+
>
|
|
257
|
+
{state}
|
|
258
|
+
</button>
|
|
317
259
|
)
|
|
318
260
|
}
|
|
319
261
|
```
|
|
320
262
|
|
|
321
|
-
|
|
322
|
-
`
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
accidental value import then fails your build too, with your own error
|
|
384
|
-
message.
|
|
263
|
+
A rejected cold preload pauses startup and reports its error through
|
|
264
|
+
`connection`. A2 cannot retry an externally created Promise: supply a new
|
|
265
|
+
Promise, a snapshot, or `null` to continue. The caller can catch a preload
|
|
266
|
+
failure and return `null` when bootstrap fallback is appropriate.
|
|
267
|
+
|
|
268
|
+
A warm session keeps its state and connection while another initial Promise
|
|
269
|
+
is pending. A newer successful snapshot follows the normal hydration rules;
|
|
270
|
+
a rejected late preload does not disrupt a working session. Replaced sources
|
|
271
|
+
and hard close invalidate pending results, including deferred hydration.
|
|
272
|
+
Unmounting releases that hook's connection lease; it does not cancel the
|
|
273
|
+
caller's fetch. Within a cached session, repeating the same Promise does not
|
|
274
|
+
restart cancelled work.
|
|
275
|
+
|
|
276
|
+
Each hook can opt into React Suspense with `useSession({ suspend: true })`.
|
|
277
|
+
It reads the supplied Promise with React `use()`. The nearest Suspense and
|
|
278
|
+
Error boundaries handle its pending and rejected states. The Provider itself
|
|
279
|
+
does not suspend. Without a Promise, or when it resolves to no snapshot,
|
|
280
|
+
ordinary subscription bootstrap displays the reducer seed without suspending.
|
|
281
|
+
`enabled` continues to control connection ownership separately.
|
|
282
|
+
|
|
283
|
+
A non-suspending preload renders the reducer seed on the server. Await the
|
|
284
|
+
snapshot upstream or use the suspending hook within an appropriate boundary
|
|
285
|
+
to render the loaded session in the server response. Keep
|
|
286
|
+
Promise identity stable across client renders; do not create it inside the
|
|
287
|
+
component that consumes it.
|
|
288
|
+
|
|
289
|
+
## Connections and activation
|
|
290
|
+
|
|
291
|
+
Hooks using the same client instance and session ID share one runtime,
|
|
292
|
+
connection, state, and optimistic queue. Providers do not determine deduplication.
|
|
293
|
+
Another client instance is isolated, even when its URL matches.
|
|
294
|
+
|
|
295
|
+
`useSession({ enabled: false })` observes cached or initial data without
|
|
296
|
+
acquiring an automatic connection lease. The default is `true`. Disabling
|
|
297
|
+
or unmounting a hook releases only its own lease. Another enabled consumer
|
|
298
|
+
continues normally. Explicit pushes still work and acquire a connection
|
|
299
|
+
when needed; disabling is not an authorization boundary.
|
|
300
|
+
|
|
301
|
+
The bound hook accepts `sessionId`, `client`, `participant`, `initial`,
|
|
302
|
+
`enabled`, and `suspend` options. Initial data and participant defaults are inherited
|
|
303
|
+
only when the resolved client and session ID match the Provider's scope.
|
|
304
|
+
|
|
305
|
+
## Events and history
|
|
306
|
+
|
|
307
|
+
`useSession()` returns `state`, `events`, `index`, `connection`, `push`,
|
|
308
|
+
`reconnect`, `loadHistory`, and `history`. Presence contracts also return
|
|
309
|
+
`presence` and `setPresence`.
|
|
310
|
+
|
|
311
|
+
`events` contains supplied history, loaded history, received events, and
|
|
312
|
+
pending optimistic events. A bootstrap snapshot does not invent historical
|
|
313
|
+
rows. Without `initialEvents`, the array starts empty and receives events
|
|
314
|
+
after the snapshot's index.
|
|
315
|
+
|
|
316
|
+
`loadHistory({ before, limit })` reads older log entries into that array.
|
|
317
|
+
Omit the options to load up to 50 events before the oldest loaded event.
|
|
318
|
+
History reads do not change state or rewind the live cursor. `history`
|
|
319
|
+
reports loading, the oldest loaded index, and whether the beginning is
|
|
320
|
+
loaded. History reads currently require the HTTP transport.
|
|
321
|
+
|
|
322
|
+
The array is an in-memory collection, not a paginated projection. Long-lived
|
|
323
|
+
sessions can accumulate many events; a state snapshot bounds replay work,
|
|
324
|
+
not the size of state or retained history.
|