experimental-a2 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +360 -0
- package/dist/{actor-ohPC-81x.d.ts → actor-DBbe5xeE.d.ts} +21 -11
- package/dist/actor-DBbe5xeE.d.ts.map +1 -0
- package/dist/actor-client.d.ts +2 -5
- package/dist/actor-client.d.ts.map +1 -1
- package/dist/actor-client.js +13 -4
- package/dist/actor-client.js.map +1 -1
- package/dist/actor-react.d.ts +5 -3
- package/dist/actor-react.d.ts.map +1 -1
- package/dist/actor-react.js +20 -10
- package/dist/actor-react.js.map +1 -1
- package/dist/{actor-shared-B5tJfzt-.js → actor-shared-t7w8xIKP.js} +12 -7
- package/dist/actor-shared-t7w8xIKP.js.map +1 -0
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +42 -23
- package/dist/actor.js.map +1 -1
- package/dist/{ai-CrLYwNEx.js → ai-UrWfKeuv.js} +966 -541
- package/dist/ai-UrWfKeuv.js.map +1 -0
- package/dist/ai-client.d.ts +27 -0
- package/dist/ai-client.d.ts.map +1 -0
- package/dist/ai-client.js +29 -0
- package/dist/ai-client.js.map +1 -0
- package/dist/ai-react.d.ts +96 -0
- package/dist/ai-react.d.ts.map +1 -0
- package/dist/ai-react.js +142 -0
- package/dist/ai-react.js.map +1 -0
- package/dist/ai-server.browser.js +2 -2
- package/dist/ai-server.browser.js.map +1 -1
- package/dist/ai-server.d.ts +80 -36
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +466 -265
- package/dist/ai-server.js.map +1 -1
- package/dist/{ai-HJ9fHfYI.d.ts → ai-vpGtcaCB.d.ts} +115 -101
- package/dist/ai-vpGtcaCB.d.ts.map +1 -0
- package/dist/ai.d.ts +2 -2
- package/dist/ai.js +2 -2
- package/dist/{client-BYzHjkwU.js → client-COaiaDub.js} +447 -80
- package/dist/client-COaiaDub.js.map +1 -0
- package/dist/{client-CzyacQpJ.d.ts → client-qRnRUzBS.d.ts} +14 -13
- package/dist/client-qRnRUzBS.d.ts.map +1 -0
- package/dist/client.d.ts +4 -2
- package/dist/client.js +1 -1
- package/dist/{contract-CKRg_E4q.js → contract-BYKASlCV.js} +12 -21
- package/dist/contract-BYKASlCV.js.map +1 -0
- package/dist/{idempotent-replay-DVOlyYbx.js → idempotent-replay-JdgMQRRm.js} +2 -2
- package/dist/{idempotent-replay-DVOlyYbx.js.map → idempotent-replay-JdgMQRRm.js.map} +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/{errors-DvhSXnxk.d.ts → initial-snapshot-BrLso5x_.d.ts} +7 -3
- package/dist/initial-snapshot-BrLso5x_.d.ts.map +1 -0
- package/dist/{internal-Dq2qYxou.js → internal-CbVvEwad.js} +23 -2
- package/dist/internal-CbVvEwad.js.map +1 -0
- package/dist/otel.d.ts +1 -1
- package/dist/react-BbJ-g6Uu.d.ts +86 -0
- package/dist/react-BbJ-g6Uu.d.ts.map +1 -0
- package/dist/react.d.ts +2 -122
- package/dist/react.js +107 -100
- package/dist/react.js.map +1 -1
- package/dist/reducer-BAXc8JyF.js +87 -0
- package/dist/reducer-BAXc8JyF.js.map +1 -0
- package/dist/{reducer-otzHjuJj.d.ts → reducer-BYu6DT7h.d.ts} +45 -29
- package/dist/reducer-BYu6DT7h.d.ts.map +1 -0
- package/dist/reducer-runtime-DQwz_J59.js +28 -0
- package/dist/reducer-runtime-DQwz_J59.js.map +1 -0
- package/dist/scheduler-qstash.d.ts +3 -3
- package/dist/scheduler-qstash.d.ts.map +1 -1
- package/dist/scheduler-qstash.js +33 -10
- package/dist/scheduler-qstash.js.map +1 -1
- package/dist/{scheduler-task-BpzhPnRS.js → scheduler-task-X1SToDjP.js} +18 -3
- package/dist/scheduler-task-X1SToDjP.js.map +1 -0
- package/dist/scheduler-vercel.d.ts +4 -9
- package/dist/scheduler-vercel.d.ts.map +1 -1
- package/dist/scheduler-vercel.js +25 -44
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/{server-DbD7IJVK.d.ts → server-CGM8WzRG.d.ts} +87 -26
- package/dist/server-CGM8WzRG.d.ts.map +1 -0
- package/dist/{server-BD5ckxZb.js → server-Rj64fNvX.js} +854 -202
- package/dist/server-Rj64fNvX.js.map +1 -0
- package/dist/server.d.ts +4 -4
- package/dist/server.js +2 -2
- package/dist/{errors-DCk6ch5n.js → session-clear-DGI4jTQ5.js} +17 -2
- package/dist/session-clear-DGI4jTQ5.js.map +1 -0
- package/dist/session-clear-Dshpuhff.d.ts +12 -0
- package/dist/session-clear-Dshpuhff.d.ts.map +1 -0
- package/dist/{store-DyZM6fS5.d.ts → store-Gpkq7WBo.d.ts} +44 -3
- package/dist/store-Gpkq7WBo.d.ts.map +1 -0
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.d.ts.map +1 -1
- package/dist/store-memory.js +182 -69
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-CmxUbV93.js → store-polling-4_y8lETm.js} +21 -4
- package/dist/store-polling-4_y8lETm.js.map +1 -0
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.d.ts.map +1 -1
- package/dist/store-postgres.js +399 -108
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-presence-polling-C7-XZyW9.js → store-presence-polling-Q1C-k2dd.js} +3 -3
- package/dist/{store-presence-polling-C7-XZyW9.js.map → store-presence-polling-Q1C-k2dd.js.map} +1 -1
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.d.ts.map +1 -1
- package/dist/store-redis-http.js +293 -11
- package/dist/store-redis-http.js.map +1 -1
- package/dist/store-redis-notify-tqWNnMRu.js +2374 -0
- package/dist/store-redis-notify-tqWNnMRu.js.map +1 -0
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.d.ts.map +1 -1
- package/dist/store-redis.js +11 -11
- package/dist/store-redis.js.map +1 -1
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.d.ts.map +1 -1
- package/dist/store-sqlite.js +166 -32
- package/dist/store-sqlite.js.map +1 -1
- package/dist/stream-buffer-Cx7AAPGq.js +23 -0
- package/dist/stream-buffer-Cx7AAPGq.js.map +1 -0
- package/dist/{telemetry-B5jzpy6y.d.ts → telemetry-cZH-cibj.d.ts} +2 -2
- package/dist/telemetry-cZH-cibj.d.ts.map +1 -0
- package/dist/{wire-BO5wWCb1.js → wire-DjfHOvdW.js} +132 -10
- package/dist/wire-DjfHOvdW.js.map +1 -0
- package/docs/01-quickstart.mdx +2 -2
- package/docs/actors/01-introduction.mdx +4 -0
- package/docs/actors/04-routes.mdx +14 -1
- package/docs/concepts/01-contracts.mdx +95 -0
- package/docs/concepts/04-state.mdx +3 -3
- package/docs/guides/01-timers.mdx +22 -14
- package/docs/guides/02-cancellation.mdx +1 -1
- package/docs/guides/03-react.mdx +268 -328
- package/docs/guides/04-local-first.mdx +32 -73
- package/docs/guides/05-production.mdx +40 -27
- package/docs/guides/06-ai-agents.mdx +680 -580
- package/docs/guides/09-presence.mdx +3 -7
- package/docs/guides/10-transports.mdx +5 -5
- package/docs/index.mdx +10 -18
- package/docs/reference/01-api.mdx +662 -195
- package/docs/reference/02-errors.mdx +2 -1
- package/examples/playground/DEPLOY.md +52 -6
- package/examples/playground/app/a0/[appId]/loading.tsx +16 -0
- package/examples/playground/app/a0/[appId]/page.tsx +48 -0
- package/examples/playground/app/a0/a0.css +818 -0
- package/examples/playground/app/a0/app-list.tsx +211 -0
- package/examples/playground/app/a0/app-view.tsx +55 -0
- package/examples/playground/app/a0/apps/[appId]/preview/route.ts +27 -0
- package/examples/playground/app/a0/apps/[appId]/route.ts +23 -0
- package/examples/playground/app/a0/apps.test.ts +116 -0
- package/examples/playground/app/a0/apps.ts +87 -0
- package/examples/playground/app/a0/chat.tsx +364 -0
- package/examples/playground/app/a0/columns.tsx +109 -0
- package/examples/playground/app/a0/context-menu.tsx +116 -0
- package/examples/playground/app/a0/cookies.ts +46 -0
- package/examples/playground/app/a0/crash-on-request.ts +72 -0
- package/examples/playground/app/a0/crashes/route.ts +3 -0
- package/examples/playground/app/a0/crashes.test.ts +33 -0
- package/examples/playground/app/a0/crashes.ts +28 -0
- package/examples/playground/app/a0/devtools.tsx +164 -0
- package/examples/playground/app/a0/events/route.ts +6 -0
- package/examples/playground/app/a0/flags.ts +30 -0
- package/examples/playground/app/a0/layout.tsx +38 -0
- package/examples/playground/app/a0/list/route.ts +10 -0
- package/examples/playground/app/a0/model.test.ts +56 -0
- package/examples/playground/app/a0/model.ts +58 -0
- package/examples/playground/app/a0/page.tsx +14 -0
- package/examples/playground/app/a0/preview-pane.tsx +145 -0
- package/examples/playground/app/a0/preview.test.ts +53 -0
- package/examples/playground/app/a0/preview.ts +26 -0
- package/examples/playground/app/a0/sandbox-log.ts +69 -0
- package/examples/playground/app/a0/sandbox.ts +191 -0
- package/examples/playground/app/a0/sandboxes/scheduler/route.ts +6 -0
- package/examples/playground/app/a0/scheduler/route.ts +7 -0
- package/examples/playground/app/a0/server.ts +158 -0
- package/examples/playground/app/a0/session.ts +47 -0
- package/examples/playground/app/a0/shell.tsx +150 -0
- package/examples/playground/app/a0/short/events/route.ts +9 -0
- package/examples/playground/app/a0/splitter.tsx +120 -0
- package/examples/playground/app/agent/[agentId]/agent-client.tsx +144 -174
- package/examples/playground/app/agent/[agentId]/command-receipt.test.tsx +65 -0
- package/examples/playground/app/agent/[agentId]/command-receipt.tsx +27 -0
- package/examples/playground/app/agent/[agentId]/compaction-event.tsx +25 -24
- package/examples/playground/app/agent/[agentId]/compaction-panel.test.tsx +119 -0
- package/examples/playground/app/agent/[agentId]/compaction-panel.tsx +208 -165
- package/examples/playground/app/agent/[agentId]/page.tsx +2 -16
- package/examples/playground/app/agent/events/route.ts +5 -2
- package/examples/playground/app/agent/ingress.test.ts +98 -0
- package/examples/playground/app/agent/model-input.test.tsx +58 -0
- package/examples/playground/app/agent/model-input.tsx +115 -0
- package/examples/playground/app/agent/model.test.ts +73 -0
- package/examples/playground/app/agent/model.ts +58 -26
- package/examples/playground/app/agent/new-agent-session.tsx +37 -19
- package/examples/playground/app/agent/page.tsx +4 -4
- package/examples/playground/app/agent/server.ts +15 -14
- package/examples/playground/app/agent/session.ts +5 -6
- package/examples/playground/app/agent/telemetry.test.ts +168 -0
- package/examples/playground/app/agent/telemetry.ts +30 -0
- package/examples/playground/app/agent/trace.test.ts +23 -0
- package/examples/playground/app/agent/trace.ts +18 -0
- package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +11 -11
- package/examples/playground/app/canvas/[canvasId]/page.tsx +1 -2
- package/examples/playground/app/canvas/[canvasId]/session.ts +2 -2
- package/examples/playground/app/canvas/model.ts +53 -53
- package/examples/playground/app/chat/[chatId]/agent-state.test.ts +176 -0
- package/examples/playground/app/chat/[chatId]/agent-stream-drawer.tsx +16 -1
- package/examples/playground/app/chat/[chatId]/chat-client.tsx +55 -77
- package/examples/playground/app/chat/[chatId]/chat-view.ts +37 -3
- package/examples/playground/app/chat/[chatId]/page.tsx +7 -15
- package/examples/playground/app/chat/[chatId]/session.ts +2 -19
- package/examples/playground/app/chat/agent/route.ts +9 -0
- package/examples/playground/app/chat/model.test.ts +46 -119
- package/examples/playground/app/chat/model.ts +82 -128
- package/examples/playground/app/chat/scheduler/route.ts +2 -2
- package/examples/playground/app/chat/server.test.ts +108 -0
- package/examples/playground/app/chat/server.ts +69 -73
- package/examples/playground/app/components/activity-feed.tsx +30 -1
- package/examples/playground/app/components/activity-history.test.tsx +35 -0
- package/examples/playground/app/components/demo-icons.tsx +89 -0
- package/examples/playground/app/components/demos.tsx +33 -0
- package/examples/playground/app/components/fast-link.test.tsx +58 -0
- package/examples/playground/app/components/fast-link.tsx +55 -0
- package/examples/playground/app/components/site-header.tsx +96 -0
- package/examples/playground/app/counter/counter-client.tsx +3 -3
- package/examples/playground/app/counter/model.test.ts +3 -3
- package/examples/playground/app/counter/model.ts +11 -21
- package/examples/playground/app/counter/page.tsx +2 -15
- package/examples/playground/app/counter/session.ts +2 -2
- package/examples/playground/app/documents/[documentId]/code-editor.tsx +12 -1
- package/examples/playground/app/documents/[documentId]/document-client.tsx +10 -11
- package/examples/playground/app/documents/[documentId]/page.tsx +1 -2
- package/examples/playground/app/documents/[documentId]/session.ts +2 -2
- package/examples/playground/app/documents/model.ts +27 -27
- package/examples/playground/app/globals.css +420 -207
- package/examples/playground/app/initial-state.test.tsx +54 -0
- package/examples/playground/app/layout.tsx +7 -23
- package/examples/playground/app/orders/[orderId]/page.tsx +1 -2
- package/examples/playground/app/orders/[orderId]/session.ts +2 -2
- package/examples/playground/app/orders/model.ts +49 -49
- package/examples/playground/app/page.tsx +13 -102
- package/examples/playground/app/recovery/[recoveryId]/page.tsx +1 -2
- package/examples/playground/app/recovery/[recoveryId]/session.ts +2 -2
- package/examples/playground/app/recovery/model.ts +30 -30
- package/examples/playground/instrumentation.ts +5 -0
- package/examples/playground/package.json +10 -2
- package/examples/playground/scripts/a0-template.ts +77 -0
- package/examples/playground/vercel.json +18 -0
- package/package.json +3 -1
- package/src/actor-client.ts +16 -4
- package/src/actor-react.ts +25 -11
- package/src/actor-shared.ts +21 -6
- package/src/actor.ts +76 -31
- package/src/ai-agent-actions.ts +219 -0
- package/src/ai-agent.ts +2343 -0
- package/src/ai-app-state.ts +91 -0
- package/src/ai-client-state.ts +39 -2
- package/src/ai-client.ts +62 -0
- package/src/ai-context-schema.ts +25 -5
- package/src/ai-context.ts +90 -40
- package/src/ai-control-server.ts +94 -55
- package/src/ai-control.ts +2 -0
- package/src/ai-coordinator.ts +23 -22
- package/src/ai-message-base.ts +209 -0
- package/src/ai-message-projection.ts +382 -58
- package/src/ai-progress-batches.ts +26 -0
- package/src/ai-projector.ts +95 -14
- package/src/ai-react.ts +146 -0
- package/src/ai-server.browser.ts +2 -2
- package/src/ai-server.ts +20 -1951
- package/src/ai-stored-state.ts +48 -163
- package/src/ai.ts +343 -276
- package/src/bootstrap-stream.ts +72 -0
- package/src/bounded-cache.ts +55 -0
- package/src/client-render-session.ts +49 -0
- package/src/client.ts +688 -151
- package/src/contract.ts +92 -42
- package/src/deterministic-id.ts +37 -0
- package/src/equal-json.ts +32 -0
- package/src/errors.ts +13 -1
- package/src/index.ts +5 -1
- package/src/initial-snapshot.ts +39 -0
- package/src/internal.ts +45 -0
- package/src/postgres-notification-scope.ts +14 -3
- package/src/react.ts +171 -206
- package/src/redis-http-reply.ts +256 -0
- package/src/redis-http-stream.ts +73 -0
- package/src/redis-http-subscriptions.ts +13 -5
- package/src/reducer-runtime.ts +52 -0
- package/src/reducer.ts +92 -10
- package/src/scheduler-qstash.ts +49 -20
- package/src/scheduler-task.ts +34 -2
- package/src/scheduler-vercel.ts +32 -84
- package/src/server-fetch.ts +139 -35
- package/src/server.ts +957 -237
- package/src/session-clear.ts +23 -0
- package/src/session-socket.ts +154 -49
- package/src/sse.ts +71 -4
- package/src/store-memory.ts +220 -78
- package/src/store-polling.ts +18 -4
- package/src/store-postgres.ts +461 -114
- package/src/store-redis-core.ts +1260 -434
- package/src/store-redis-http.ts +18 -2
- package/src/store-redis-notify.ts +260 -85
- package/src/store-redis.ts +19 -9
- package/src/store-sqlite.ts +259 -45
- package/src/store.ts +46 -1
- package/src/stream-buffer.ts +43 -0
- package/src/telemetry.ts +13 -1
- package/src/wire.ts +241 -13
- package/dist/actor-ohPC-81x.d.ts.map +0 -1
- package/dist/actor-shared-B5tJfzt-.js.map +0 -1
- package/dist/ai-CrLYwNEx.js.map +0 -1
- package/dist/ai-HJ9fHfYI.d.ts.map +0 -1
- package/dist/client-BYzHjkwU.js.map +0 -1
- package/dist/client-CzyacQpJ.d.ts.map +0 -1
- package/dist/contract-CKRg_E4q.js.map +0 -1
- package/dist/errors-DCk6ch5n.js.map +0 -1
- package/dist/errors-DvhSXnxk.d.ts.map +0 -1
- package/dist/internal-Dq2qYxou.js.map +0 -1
- package/dist/react.d.ts.map +0 -1
- package/dist/reducer-DEMjEY_O.js +0 -29
- package/dist/reducer-DEMjEY_O.js.map +0 -1
- package/dist/reducer-otzHjuJj.d.ts.map +0 -1
- package/dist/scheduler-task-BpzhPnRS.js.map +0 -1
- package/dist/server-BD5ckxZb.js.map +0 -1
- package/dist/server-DbD7IJVK.d.ts.map +0 -1
- package/dist/store-DyZM6fS5.d.ts.map +0 -1
- package/dist/store-polling-CmxUbV93.js.map +0 -1
- package/dist/store-redis-notify-BVLUfI3j.js +0 -1583
- package/dist/store-redis-notify-BVLUfI3j.js.map +0 -1
- package/dist/telemetry-B5jzpy6y.d.ts.map +0 -1
- package/dist/validate-XKT4FSNn.js +0 -30
- package/dist/validate-XKT4FSNn.js.map +0 -1
- package/dist/wire-BO5wWCb1.js.map +0 -1
- package/examples/playground/app/agent/[agentId]/compaction/route.ts +0 -14
- package/examples/playground/app/agent/compaction-settings.test.ts +0 -144
- package/examples/playground/app/agent/compaction-settings.ts +0 -49
- package/examples/playground/app/agent/compaction-timeline.test.ts +0 -377
- package/examples/playground/app/agent/compaction-timeline.ts +0 -208
package/dist/react.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react.js","names":[],"sources":["../src/react.ts"],"sourcesContent":["'use client'\n/**\n * experimental-a2/react — React bindings over experimental-a2/client.\n *\n * `createReact({ client })` is a factory (like createContext): call it\n * once in a `'use client'` module with a shared A2 client and export the\n * bound pair. Server components import `SessionProvider` from that module\n * as a client reference; client components import `useSession` from the\n * same file. The shared client also makes imperative pushes and provider\n * renders resolve the same live session. Everything is typed by the\n * reducer value — no type arguments, and nothing here ever touches the\n * machine module.\n *\n * The standalone `useSession(client, sessionId, options?)` is the\n * provider-less primitive for client-first apps: identity-mapped\n * handles, one leased stream per session, and an app-owned hydration\n * input — the hook holds the stream until the app hands it a\n * `{ state, index }` fold.\n */\n\nimport {\n createContext,\n createElement,\n useContext,\n useEffect,\n useMemo,\n useSyncExternalStore,\n type ReactElement,\n type ReactNode,\n} from 'react'\nimport {\n createClient,\n type A2Client,\n type Connection,\n type HistoryState,\n type LoadHistoryOptions,\n type PushResult,\n type SessionClient,\n type SessionClientPresence,\n type SessionSnapshot,\n} from './client.ts'\nimport type {\n AppendInput,\n ContractEvent,\n EventDefs,\n PresenceDefs,\n PresenceMap,\n PresencePatch,\n WithPresence,\n} from './contract.ts'\nimport type { Reducer } from './reducer.ts'\n\nexport type UseSessionOptions<S> = {\n /** Overrides the client's `participant` for this session — for apps\n * whose identity is only known at mount time. Bound once per\n * session handle. */\n participant?: string\n /**\n * The server fold this session mounts from: the `{ state, index }`\n * pair `session.state(reducer)` returns, fetched by the app through\n * its own route. Atomic on purpose — the fold and its frontier\n * travel together or not at all. While `undefined` (the app's fetch\n * has not landed), the hook holds the stream; when the value\n * arrives, the session hydrates through the ordinary re-hydration\n * seam and the stream connects at that frontier. To fold from the log's start\n * deliberately, hand the fold's true starting point:\n * `{ state: reducer.initialState, index: 0 }` — state at index 0 is\n * the reducer's seed by definition, so the explicit replay needs no\n * special vocabulary. There is no pending/ready echo on the result:\n * whether the fold has been handed over is this option — the\n * caller's own input — so the data layer's loading state IS the\n * pending state.\n */\n hydrate?: { state: S; index: number } | undefined\n}\n\nexport type SessionProviderProps<D extends EventDefs, S> = {\n /** Which session to subscribe to. */\n sessionId: string\n /** Base path of the route exposing GET (stream) and POST (push). */\n api: string\n /** The server-rendered fold — the first paint, no client JS needed. */\n initialState: S\n /** The fold's frontier — the stream resumes exactly there. */\n initialIndex: number\n /** Server-rendered event history through `initialIndex`. */\n initialEvents?: ContractEvent<D>[]\n /** Overrides the client's `participant` for this session — for apps\n * whose identity is only known at mount time. */\n participant?: string\n children?: ReactNode\n}\n\nexport type BoundSessionProviderProps<D extends EventDefs, S> = Omit<\n SessionProviderProps<D, S>,\n 'api'\n>\n\n/**\n * The presence members of the hook result — intersected in via\n * `WithPresence`, so they exist exactly when the contract declares\n * presence fields (the reducer is the client's typed handle on it).\n */\nexport type UseSessionPresence<P extends PresenceDefs> = {\n /** The replicated presence map, this client included (local echo). */\n presence: PresenceMap<P>\n /**\n * Fire-and-forget: validated locally, echoed into the map\n * immediately, coalesced on the wire to a fixed cadence (a leading\n * send immediately, then at most one merged send per interval;\n * later values win per field), stamped with the frontier at send\n * time as `seen`. Requires the provider's `participant`.\n */\n setPresence: (values: PresencePatch<P>['values']) => void\n}\n\nexport type UseSessionResult<\n D extends EventDefs,\n S,\n P extends PresenceDefs = Record<never, never>,\n> = WithPresence<P, UseSessionPresence<P>> & {\n /** The live view, folded through the shared reducer. */\n state: S\n /** The raw event feed — observed, seeded, or backscrolled. */\n events: ContractEvent<D>[]\n /** The stream frontier — `lastSeenIndex` for cancellation. */\n index: number\n /**\n * The connection, as a discriminated union — `error` exists only\n * while disconnected; \"reconnecting…\" is\n * `status === 'connecting' && reconnects > 0`.\n */\n connection: Connection\n /**\n * Typed optimistic append. Awaiting it gives the server ack;\n * `.confirmed` resolves when the live stream has delivered the batch\n * back. Rejects with the server's A2Error codes.\n */\n push: (...events: AppendInput<D>[]) => PushResult<D>\n /** Restart the live subscription without resetting its state or cursor. */\n reconnect: () => void\n /**\n * Backscroll: fetch a bounded slice of older events into `events`.\n * Defaults page backward 50 at a time from the oldest loaded event.\n * Display data only — `state` and the optimistic overlay never move.\n */\n loadHistory: (options?: LoadHistoryOptions) => Promise<ContractEvent<D>[]>\n /** Backscroll progress: `complete` means the feed reaches index 1. */\n history: HistoryState\n}\n\nexport type A2React<\n D extends EventDefs,\n S,\n P extends PresenceDefs = Record<never, never>,\n V = S,\n> = {\n SessionProvider: (props: SessionProviderProps<D, S>) => ReactElement\n useSession: () => UseSessionResult<D, V, P>\n}\n\nexport type BoundA2React<\n D extends EventDefs,\n S,\n P extends PresenceDefs = Record<never, never>,\n V = S,\n> = {\n SessionProvider: (props: BoundSessionProviderProps<D, S>) => ReactElement\n useSession: () => UseSessionResult<D, V, P>\n}\n\nexport function createReact<\n D extends EventDefs,\n S,\n P extends PresenceDefs = Record<never, never>,\n V = S,\n>(options: { client: A2Client<D, S, P, V> }): BoundA2React<D, S, P, V>\nexport function createReact<\n D extends EventDefs,\n S,\n P extends PresenceDefs = Record<never, never>,\n V = S,\n>(options: { reducer: Reducer<D, S, P, V> }): A2React<D, S, P, V>\nexport function createReact<\n D extends EventDefs,\n S,\n P extends PresenceDefs = Record<never, never>,\n V = S,\n>(\n options: { client: A2Client<D, S, P, V> } | { reducer: Reducer<D, S, P, V> },\n): BoundA2React<D, S, P, V> | A2React<D, S, P, V> {\n const Context = createContext<SessionClient<D, S, P, V> | null>(null)\n const resolveSession: (\n sessionId: string,\n api: string | undefined,\n initialState: S,\n initialIndex: number,\n initialEvents: ContractEvent<D>[] | undefined,\n participant: string | undefined,\n ) => SessionClient<D, S, P, V> =\n 'client' in options\n ? (\n sessionId,\n _api,\n initialState,\n initialIndex,\n initialEvents,\n participant,\n ) =>\n options.client.session(sessionId, {\n initialState,\n initialIndex,\n ...(initialEvents === undefined ? {} : { initialEvents }),\n ...(participant === undefined ? {} : { participant }),\n })\n : (() => {\n const clients = new Map<string, A2Client<D, S, P, V>>()\n return (\n sessionId: string,\n api: string | undefined,\n initialState: S,\n initialIndex: number,\n initialEvents: ContractEvent<D>[] | undefined,\n participant: string | undefined,\n ) => {\n if (api === undefined) {\n throw new TypeError(\n 'SessionProvider requires api when createReact binds a reducer',\n )\n }\n let client = clients.get(api)\n if (!client) {\n client = createClient({ reducer: options.reducer, api })\n clients.set(api, client)\n }\n return client.session(sessionId, {\n initialState,\n initialIndex,\n ...(initialEvents === undefined ? {} : { initialEvents }),\n ...(participant === undefined ? {} : { participant }),\n })\n }\n })()\n\n function SessionProvider(\n props: SessionProviderProps<D, S> | BoundSessionProviderProps<D, S>,\n ): ReactElement {\n const {\n sessionId,\n initialState,\n initialIndex,\n initialEvents,\n participant,\n } = props\n const api = 'api' in props ? props.api : undefined\n const client = useMemo(\n () =>\n resolveSession(\n sessionId,\n api,\n initialState,\n initialIndex,\n initialEvents,\n participant,\n ),\n [sessionId, api, initialState, initialIndex, initialEvents, participant],\n )\n useSessionLifecycle(client, true)\n return createElement(Context.Provider, { value: client }, props.children)\n }\n\n function useBoundSession(): UseSessionResult<D, V, P> {\n const client = useContext(Context)\n if (!client) {\n throw new Error(\n 'useSession must be rendered inside its matching SessionProvider',\n )\n }\n return useSessionResult(client)\n }\n\n return { SessionProvider, useSession: useBoundSession }\n}\n\n// ── the shared session lifecycle ───────────────────────────────\n\n/**\n * Hold a stream lease while mounted and `live` — the client refcounts\n * leases per handle, so however many providers and hooks mount one\n * session, the first connects and the last release closes\n * (StrictMode-safe: the lease is retaken on remount). A standalone\n * hook without a `hydrate` value passes `live: false` and holds the\n * stream — connecting an unhydrated session would replay the whole\n * log, and the library never picks the expensive path silently.\n */\nfunction useSessionLifecycle<D extends EventDefs, S, P extends PresenceDefs, V>(\n session: SessionClient<D, S, P, V>,\n live: boolean,\n): void {\n useEffect(() => {\n if (!live) return\n return session.connect()\n }, [session, live])\n}\n\nfunction useSessionResult<D extends EventDefs, S, P extends PresenceDefs, V>(\n session: SessionClient<D, S, P, V>,\n): UseSessionResult<D, V, P> {\n const snapshot: SessionSnapshot<D, V, P> = useSyncExternalStore(\n session.subscribe,\n session.getSnapshot,\n session.getSnapshot,\n )\n return useMemo(() => {\n // Runtime mirror of the WithPresence surface: the client attaches\n // setPresence (and the snapshot carries presence) exactly when the\n // reducer declares presence fields — no runtime-inert members.\n const { setPresence } = session as Partial<SessionClientPresence<P>>\n const { presence } = snapshot as Partial<{ presence: PresenceMap<P> }>\n return {\n state: snapshot.state,\n events: snapshot.events,\n index: snapshot.index,\n connection: snapshot.connection,\n push: session.push,\n reconnect: session.reconnect,\n loadHistory: session.loadHistory,\n history: snapshot.history,\n ...(setPresence && presence ? { presence, setPresence } : {}),\n } as UseSessionResult<D, V, P>\n }, [snapshot, session])\n}\n\n/**\n * The standalone, provider-less hook — for apps whose components reach\n * sessions ad hoc (a sidebar of channel sessions, a user session read\n * from a menu). Takes the shared client and a session id; the handle\n * is identity-mapped, so every hook and provider mounting the same\n * session shares one runtime and one leased stream. Hydration is\n * app-owned: pass `hydrate` when your fetch lands; until then the\n * stream is held, and your data layer's loading state is the pending\n * state.\n */\nexport function useSession<\n D extends EventDefs,\n S,\n P extends PresenceDefs = Record<never, never>,\n V = S,\n>(\n client: A2Client<D, S, P, V>,\n sessionId: string,\n options?: UseSessionOptions<S>,\n): UseSessionResult<D, V, P> {\n const { participant, hydrate } = options ?? {}\n const session = useMemo(\n () =>\n client.session(sessionId, {\n ...(hydrate === undefined\n ? {}\n : { initialState: hydrate.state, initialIndex: hydrate.index }),\n ...(participant === undefined ? {} : { participant }),\n }),\n [client, sessionId, hydrate, participant],\n )\n useSessionLifecycle(session, hydrate !== undefined)\n return useSessionResult(session)\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAuLA,SAAgB,YAMd,SACgD;CAChD,MAAM,UAAU,cAAgD,IAAI;CACpE,MAAM,iBAQJ,YAAY,WAEN,WACA,MACA,cACA,cACA,eACA,gBAEA,QAAQ,OAAO,QAAQ,WAAW;EAChC;EACA;EACA,GAAI,kBAAkB,KAAA,IAAY,CAAC,IAAI,EAAE,cAAc;EACvD,GAAI,gBAAgB,KAAA,IAAY,CAAC,IAAI,EAAE,YAAY;CACrD,CAAC,WACI;EACL,MAAM,0BAAU,IAAI,IAAkC;EACtD,QACE,WACA,KACA,cACA,cACA,eACA,gBACG;GACH,IAAI,QAAQ,KAAA,GACV,MAAM,IAAI,UACR,+DACF;GAEF,IAAI,SAAS,QAAQ,IAAI,GAAG;GAC5B,IAAI,CAAC,QAAQ;IACX,SAAS,aAAa;KAAE,SAAS,QAAQ;KAAS;IAAI,CAAC;IACvD,QAAQ,IAAI,KAAK,MAAM;GACzB;GACA,OAAO,OAAO,QAAQ,WAAW;IAC/B;IACA;IACA,GAAI,kBAAkB,KAAA,IAAY,CAAC,IAAI,EAAE,cAAc;IACvD,GAAI,gBAAgB,KAAA,IAAY,CAAC,IAAI,EAAE,YAAY;GACrD,CAAC;EACH;CACF,EAAA,CAAG;CAET,SAAS,gBACP,OACc;EACd,MAAM,EACJ,WACA,cACA,cACA,eACA,gBACE;EACJ,MAAM,MAAM,SAAS,QAAQ,MAAM,MAAM,KAAA;EACzC,MAAM,SAAS,cAEX,eACE,WACA,KACA,cACA,cACA,eACA,WACF,GACF;GAAC;GAAW;GAAK;GAAc;GAAc;GAAe;EAAW,CACzE;EACA,oBAAoB,QAAQ,IAAI;EAChC,OAAO,cAAc,QAAQ,UAAU,EAAE,OAAO,OAAO,GAAG,MAAM,QAAQ;CAC1E;CAEA,SAAS,kBAA6C;EACpD,MAAM,SAAS,WAAW,OAAO;EACjC,IAAI,CAAC,QACH,MAAM,IAAI,MACR,iEACF;EAEF,OAAO,iBAAiB,MAAM;CAChC;CAEA,OAAO;EAAE;EAAiB,YAAY;CAAgB;AACxD;;;;;;;;;;AAaA,SAAS,oBACP,SACA,MACM;CACN,gBAAgB;EACd,IAAI,CAAC,MAAM;EACX,OAAO,QAAQ,QAAQ;CACzB,GAAG,CAAC,SAAS,IAAI,CAAC;AACpB;AAEA,SAAS,iBACP,SAC2B;CAC3B,MAAM,WAAqC,qBACzC,QAAQ,WACR,QAAQ,aACR,QAAQ,WACV;CACA,OAAO,cAAc;EAInB,MAAM,EAAE,gBAAgB;EACxB,MAAM,EAAE,aAAa;EACrB,OAAO;GACL,OAAO,SAAS;GAChB,QAAQ,SAAS;GACjB,OAAO,SAAS;GAChB,YAAY,SAAS;GACrB,MAAM,QAAQ;GACd,WAAW,QAAQ;GACnB,aAAa,QAAQ;GACrB,SAAS,SAAS;GAClB,GAAI,eAAe,WAAW;IAAE;IAAU;GAAY,IAAI,CAAC;EAC7D;CACF,GAAG,CAAC,UAAU,OAAO,CAAC;AACxB;;;;;;;;;;;AAYA,SAAgB,WAMd,QACA,WACA,SAC2B;CAC3B,MAAM,EAAE,aAAa,YAAY,WAAW,CAAC;CAC7C,MAAM,UAAU,cAEZ,OAAO,QAAQ,WAAW;EACxB,GAAI,YAAY,KAAA,IACZ,CAAC,IACD;GAAE,cAAc,QAAQ;GAAO,cAAc,QAAQ;EAAM;EAC/D,GAAI,gBAAgB,KAAA,IAAY,CAAC,IAAI,EAAE,YAAY;CACrD,CAAC,GACH;EAAC;EAAQ;EAAW;EAAS;CAAW,CAC1C;CACA,oBAAoB,SAAS,YAAY,KAAA,CAAS;CAClD,OAAO,iBAAiB,OAAO;AACjC"}
|
|
1
|
+
{"version":3,"file":"react.js","names":[],"sources":["../src/react.ts"],"sourcesContent":["'use client'\nimport {\n createContext,\n createElement,\n useContext,\n use,\n useEffect,\n useMemo,\n useSyncExternalStore,\n type ReactElement,\n type ReactNode,\n} from 'react'\nimport {\n createClient,\n type A2Client,\n type Connection,\n type HistoryState,\n type LoadHistoryOptions,\n type PushResult,\n type SessionClient,\n type SessionClientPresence,\n type SessionOptions,\n} from './client.ts'\nimport type {\n AppendInput,\n Contract,\n ContractEvent,\n EventDefs,\n PresenceDefs,\n PresenceMap,\n PresencePatch,\n WithPresence,\n} from './contract.ts'\nimport type { Reducer } from './reducer.ts'\nimport {\n createRenderSession,\n resolveInitialForReact,\n} from './client-render-session.ts'\nimport { isInitialPromise, type InitialSnapshot } from './initial-snapshot.ts'\n\nexport type UseSessionOptions<S> = {\n participant?: string\n initial?: InitialSnapshot<S>\n enabled?: boolean\n suspend?: boolean\n}\n\nexport type SessionProviderProps<D extends EventDefs, S> = {\n sessionId: string\n api: string\n initial?: InitialSnapshot<S>\n initialEvents?: ContractEvent<D>[]\n participant?: string\n children?: ReactNode\n}\n\nexport type BoundSessionProviderProps<D extends EventDefs, S> = Omit<\n SessionProviderProps<D, S>,\n 'api'\n>\n\nexport type UseSessionPresence<P extends PresenceDefs> = {\n /** The replicated presence map, this client included (local echo). */\n presence: PresenceMap<P>\n /**\n * Fire-and-forget: validated locally, echoed into the map\n * immediately, coalesced on the wire to a fixed cadence (a leading\n * send immediately, then at most one merged send per interval;\n * later values win per field), stamped with the frontier at send\n * time as `seen`. Requires the provider's `participant`.\n */\n setPresence: (values: PresencePatch<P>['values']) => void\n}\n\nexport type UseSessionResult<\n D extends EventDefs,\n S,\n P extends PresenceDefs = Record<never, never>,\n> = WithPresence<P, UseSessionPresence<P>> & {\n /** The live view, folded through the shared reducer. */\n state: S\n /** A state/index snapshot has been applied; independent of connection status. */\n hydrated: boolean\n /** The raw event feed — observed, seeded, or backscrolled. */\n events: ContractEvent<D>[]\n /** The stream frontier — `lastSeenIndex` for cancellation. */\n index: number\n /**\n * The connection, as a discriminated union — `error` exists only\n * while disconnected; \"reconnecting…\" is\n * `status === 'connecting' && reconnects > 0`.\n */\n connection: Connection\n /**\n * Typed optimistic append. Awaiting it gives the server ack;\n * `.confirmed` resolves when the live stream has delivered the batch\n * back. Rejects with the server's A2Error codes.\n */\n push: (...events: AppendInput<D>[]) => PushResult<D>\n /** Restart the live subscription without resetting its state or cursor. */\n reconnect: () => void\n /**\n * Backscroll: fetch a bounded slice of older events into `events`.\n * Defaults page backward 50 at a time from the oldest loaded event.\n * Display data only — `state` and the optimistic overlay never move.\n */\n loadHistory: (options?: LoadHistoryOptions) => Promise<ContractEvent<D>[]>\n /** Backscroll progress: `complete` means the feed reaches index 1. */\n history: HistoryState\n}\n\nexport type BoundSessionOptions<\n D extends EventDefs,\n S,\n P extends PresenceDefs,\n> = UseSessionOptions<S> & {\n sessionId?: string\n client?: A2Client<D, S, P>\n}\n\nexport type A2React<\n D extends EventDefs,\n S,\n P extends PresenceDefs = Record<never, never>,\n> = {\n SessionProvider: (props: SessionProviderProps<D, S>) => ReactElement\n useSession: (\n options?: BoundSessionOptions<D, S, P>,\n ) => UseSessionResult<D, S, P>\n}\n\nexport type BoundA2React<\n D extends EventDefs,\n S,\n P extends PresenceDefs = Record<never, never>,\n> = {\n SessionProvider: (props: BoundSessionProviderProps<D, S>) => ReactElement\n useSession: (\n options?: BoundSessionOptions<D, S, P>,\n ) => UseSessionResult<D, S, P>\n}\n\nexport function createReact<\n D extends EventDefs,\n S,\n P extends PresenceDefs = Record<never, never>,\n>(options: { client: A2Client<D, S, P> }): BoundA2React<D, S, P>\nexport function createReact<\n D extends EventDefs,\n S,\n P extends PresenceDefs = Record<never, never>,\n>(options: { contract: Contract<D, P, Reducer<D, S, P>> }): A2React<D, S, P>\nexport function createReact<\n D extends EventDefs,\n S,\n P extends PresenceDefs = Record<never, never>,\n>(\n options:\n | { client: A2Client<D, S, P> }\n | { contract: Contract<D, P, Reducer<D, S, P>> },\n): BoundA2React<D, S, P> | A2React<D, S, P> {\n type Scope = {\n client: A2Client<D, S, P>\n sessionId: string\n settings: SessionOptions<D, S>\n }\n const Context = createContext<Scope | null>(null)\n const clients = new Map<string, A2Client<D, S, P>>()\n const resolveClient = (api: string | undefined): A2Client<D, S, P> => {\n if ('client' in options) return options.client\n if (api === undefined)\n throw new TypeError(\n 'SessionProvider requires api when createReact binds a contract',\n )\n let client = clients.get(api)\n if (!client) {\n client = createClient({ contract: options.contract, api })\n clients.set(api, client)\n }\n return client\n }\n\n function SessionProvider(\n props: SessionProviderProps<D, S> | BoundSessionProviderProps<D, S>,\n ): ReactElement {\n const { sessionId, initial, initialEvents, participant } = props\n const api = 'api' in props ? props.api : undefined\n const scope = useMemo(\n () => ({\n client: resolveClient(api),\n sessionId,\n settings: {\n ...(initial === undefined ? {} : { initial }),\n ...(initialEvents === undefined ? {} : { initialEvents }),\n ...(participant === undefined ? {} : { participant }),\n },\n }),\n [sessionId, api, initial, initialEvents, participant],\n )\n return createElement(Context.Provider, { value: scope }, props.children)\n }\n\n function useBoundSession(\n overrides: BoundSessionOptions<D, S, P> = {},\n ): UseSessionResult<D, S, P> {\n const scope = useContext(Context)\n const client =\n overrides.client ??\n scope?.client ??\n ('client' in options ? options.client : undefined)\n const sessionId = overrides.sessionId ?? scope?.sessionId\n if (!client || sessionId === undefined)\n throw new TypeError(\n 'provide client and sessionId through SessionProvider or hook options',\n )\n const inherits = client === scope?.client && sessionId === scope?.sessionId\n const inherited = inherits ? scope.settings : undefined\n const { initial, participant } = overrides\n const settings = useMemo(\n () => ({\n ...inherited,\n ...(initial === undefined ? {} : { initial }),\n ...(participant === undefined ? {} : { participant }),\n }),\n [inherited, initial, participant],\n )\n return useLiveSession({\n client,\n sessionId,\n settings,\n enabled: overrides.enabled ?? true,\n suspend: overrides.suspend ?? false,\n })\n }\n\n return { SessionProvider, useSession: useBoundSession }\n}\n\nfunction useLiveSession<\n D extends EventDefs,\n S,\n P extends PresenceDefs,\n>(options: {\n client: A2Client<D, S, P>\n sessionId: string\n settings: SessionOptions<D, S>\n enabled: boolean\n suspend: boolean\n}): UseSessionResult<D, S, P> {\n const { client, sessionId, settings, enabled, suspend } = options\n const browserSession = useMemo(() => {\n if (typeof window === 'undefined') {\n if (isInitialPromise(settings.initial))\n void settings.initial.catch(() => {})\n return undefined\n }\n return client.session(sessionId, settings)\n }, [client, sessionId, settings])\n const promise = isInitialPromise(settings.initial)\n ? settings.initial\n : undefined\n const resolved = promise && suspend ? use(promise) : undefined\n const renderInitial = promise ? resolved : settings.initial\n const renderSession = useMemo(() => {\n let session: SessionClient<D, S, P> | undefined\n return () => {\n session ??= createRenderSession({\n client,\n sessionId,\n settings: { ...settings, initial: renderInitial },\n })\n return session\n }\n }, [client, sessionId, settings, renderInitial])\n const session = browserSession ?? renderSession()\n if (browserSession && promise && suspend)\n resolveInitialForReact({\n client,\n session: browserSession,\n source: promise,\n value: resolved,\n })\n const serverSnapshot = useMemo(\n () => () => renderSession().getSnapshot(),\n [renderSession],\n )\n const snapshot = useSyncExternalStore(\n session.subscribe,\n session.getSnapshot,\n serverSnapshot,\n )\n useEffect(() => {\n if (enabled) return session.connect()\n }, [session, enabled])\n return useMemo(() => {\n const { setPresence } = session as Partial<SessionClientPresence<P>>\n const { presence } = snapshot as Partial<{ presence: PresenceMap<P> }>\n return {\n ...snapshot,\n push: session.push,\n reconnect: session.reconnect,\n loadHistory: session.loadHistory,\n ...(setPresence && presence ? { presence, setPresence } : {}),\n } as UseSessionResult<D, S, P>\n }, [session, snapshot])\n}\n\nexport function useSession<\n D extends EventDefs,\n S,\n P extends PresenceDefs = Record<never, never>,\n>(\n client: A2Client<D, S, P>,\n sessionId: string,\n options: UseSessionOptions<NoInfer<S>> = {},\n): UseSessionResult<D, S, P> {\n const { initial, participant } = options\n const settings = useMemo(\n () => ({\n ...(initial === undefined ? {} : { initial }),\n ...(participant === undefined ? {} : { participant }),\n }),\n [initial, participant],\n )\n return useLiveSession({\n client,\n sessionId,\n settings,\n enabled: options.enabled ?? true,\n suspend: options.suspend ?? false,\n })\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { n as isSessionCleared } from "./session-clear-DGI4jTQ5.js";
|
|
2
|
+
//#region src/validate.ts
|
|
3
|
+
/**
|
|
4
|
+
* Run a Standard Schema validator, requiring a synchronous result.
|
|
5
|
+
* Validators must be synchronous (specs/a2-api.md §1) — an async one is
|
|
6
|
+
* a definition bug, rejected at definition time where possible and here
|
|
7
|
+
* as a backstop. `what` names the schema in error messages, e.g.
|
|
8
|
+
* "event 'created'" or "the stateSchema".
|
|
9
|
+
*/
|
|
10
|
+
function validateSync(schema, value, what) {
|
|
11
|
+
const result = schema["~standard"].validate(value);
|
|
12
|
+
if (result instanceof Promise) throw new TypeError(`the schema for ${what} is asynchronous — a2 schemas must validate synchronously`);
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Probe a schema to reject always-async validators early. The probe
|
|
17
|
+
* value is deliberately garbage — we only care whether the result is a
|
|
18
|
+
* Promise, not whether it passes.
|
|
19
|
+
*/
|
|
20
|
+
function assertSyncSchema(schema, what) {
|
|
21
|
+
const probe = Symbol("a2.schema-probe");
|
|
22
|
+
const result = schema["~standard"].validate(probe);
|
|
23
|
+
if (result instanceof Promise) {
|
|
24
|
+
result.catch(() => {});
|
|
25
|
+
throw new TypeError(`the schema for ${what} is asynchronous — a2 schemas must validate synchronously`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region src/reducer.ts
|
|
30
|
+
/**
|
|
31
|
+
* A fresh copy of a reducer's seed, so a fold that mutates in place
|
|
32
|
+
* cannot corrupt the shared `initialState`. Non-cloneable seeds
|
|
33
|
+
* (functions, class instances, proxies) fall back to the original.
|
|
34
|
+
*/
|
|
35
|
+
function cloneInitial(initial) {
|
|
36
|
+
try {
|
|
37
|
+
return structuredClone(initial);
|
|
38
|
+
} catch {
|
|
39
|
+
return initial;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const markerFolds = /* @__PURE__ */ new WeakSet();
|
|
43
|
+
/**
|
|
44
|
+
* Mark a fold that receives clear markers itself instead of resetting to its
|
|
45
|
+
* `initialState`. The actor follower has no seed of its own; its stream carries
|
|
46
|
+
* the reset state in the marker.
|
|
47
|
+
*/
|
|
48
|
+
function foldsClearMarkers(fold) {
|
|
49
|
+
markerFolds.add(fold);
|
|
50
|
+
return fold;
|
|
51
|
+
}
|
|
52
|
+
/** One fold step; a clear marker resets to the seed on server and client alike. */
|
|
53
|
+
function foldEvent(source, state, event) {
|
|
54
|
+
if (!isSessionCleared(event)) return source.fold(state, event);
|
|
55
|
+
return markerFolds.has(source.fold) ? source.fold(state, event) : cloneInitial(source.initialState);
|
|
56
|
+
}
|
|
57
|
+
/** Internal — shared validation for inline and standalone reducer definitions. */
|
|
58
|
+
function makeReducerBuilder(events, presence, options) {
|
|
59
|
+
const { name } = options;
|
|
60
|
+
if (typeof name !== "string" || name.length === 0) throw new TypeError("reducer name must be a non-empty string — it identifies the reducer and keys cached snapshots");
|
|
61
|
+
let initialState = options.initialState;
|
|
62
|
+
if (options.stateSchema) {
|
|
63
|
+
const result = validateSync(options.stateSchema, options.initialState, "the stateSchema");
|
|
64
|
+
if (result.issues) throw new TypeError(`reducer initialState does not match its stateSchema: ${result.issues.map((issue) => issue.message).join("; ")}`);
|
|
65
|
+
initialState = result.value;
|
|
66
|
+
const restored = validateSync(options.stateSchema, initialState, "the stateSchema");
|
|
67
|
+
if (restored.issues || JSON.stringify(restored.value) !== JSON.stringify(initialState)) throw new TypeError("stateSchema must accept and preserve its own output so snapshots can be restored");
|
|
68
|
+
}
|
|
69
|
+
return { fold(fold) {
|
|
70
|
+
if (typeof fold !== "function") throw new TypeError("reducer fold must be a function");
|
|
71
|
+
return {
|
|
72
|
+
name,
|
|
73
|
+
events,
|
|
74
|
+
presence,
|
|
75
|
+
fold,
|
|
76
|
+
initialState,
|
|
77
|
+
stateSchema: options.stateSchema
|
|
78
|
+
};
|
|
79
|
+
} };
|
|
80
|
+
}
|
|
81
|
+
function reducer(options) {
|
|
82
|
+
return makeReducerBuilder(options.contract.events, options.contract.presence, options);
|
|
83
|
+
}
|
|
84
|
+
//#endregion
|
|
85
|
+
export { reducer as a, makeReducerBuilder as i, foldEvent as n, assertSyncSchema as o, foldsClearMarkers as r, validateSync as s, cloneInitial as t };
|
|
86
|
+
|
|
87
|
+
//# sourceMappingURL=reducer-BAXc8JyF.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reducer-BAXc8JyF.js","names":[],"sources":["../src/validate.ts","../src/reducer.ts"],"sourcesContent":["import type { StandardSchemaV1 } from './standard-schema.ts'\n\n/**\n * Run a Standard Schema validator, requiring a synchronous result.\n * Validators must be synchronous (specs/a2-api.md §1) — an async one is\n * a definition bug, rejected at definition time where possible and here\n * as a backstop. `what` names the schema in error messages, e.g.\n * \"event 'created'\" or \"the stateSchema\".\n */\nexport function validateSync<Input, Output>(\n schema: StandardSchemaV1<Input, Output>,\n value: unknown,\n what: string,\n): StandardSchemaV1.Result<Output> {\n const result = schema['~standard'].validate(value)\n if (result instanceof Promise) {\n throw new TypeError(\n `the schema for ${what} is asynchronous — a2 schemas must validate synchronously`,\n )\n }\n return result\n}\n\n/**\n * Probe a schema to reject always-async validators early. The probe\n * value is deliberately garbage — we only care whether the result is a\n * Promise, not whether it passes.\n */\nexport function assertSyncSchema(schema: StandardSchemaV1, what: string): void {\n const probe = Symbol('a2.schema-probe')\n const result = schema['~standard'].validate(probe)\n if (result instanceof Promise) {\n // Swallow the eventual rejection — the probe input was garbage.\n result.catch(() => {})\n throw new TypeError(\n `the schema for ${what} is asynchronous — a2 schemas must validate synchronously`,\n )\n }\n}\n","import type {\n ContractEvent,\n EventDefs,\n PresenceDefs,\n ReducerOptions,\n} from './contract.ts'\nimport { isSessionCleared, type SessionCleared } from './session-clear.ts'\nimport type { StandardSchemaV1 } from './standard-schema.ts'\nimport { validateSync } from './validate.ts'\n\nexport type StateSnapshot<S> = {\n state: S\n index: number\n /** The session's latest clear marker, once it has been cleared. */\n clearIndex?: number | undefined\n}\n\n/**\n * A fresh copy of a reducer's seed, so a fold that mutates in place\n * cannot corrupt the shared `initialState`. Non-cloneable seeds\n * (functions, class instances, proxies) fall back to the original.\n */\nexport function cloneInitial<S>(initial: S): S {\n try {\n return structuredClone(initial)\n } catch {\n return initial\n }\n}\n\nconst markerFolds = new WeakSet<object>()\n\n/**\n * Mark a fold that receives clear markers itself instead of resetting to its\n * `initialState`. The actor follower has no seed of its own; its stream carries\n * the reset state in the marker.\n */\nexport function foldsClearMarkers<\n F extends (state: never, event: never) => unknown,\n>(fold: F): F {\n markerFolds.add(fold)\n return fold\n}\n\n/** One fold step; a clear marker resets to the seed on server and client alike. */\nexport function foldEvent<D extends EventDefs, S>(\n source: Pick<Reducer<D, S>, 'fold' | 'initialState'>,\n state: S,\n event: ContractEvent<D> | SessionCleared,\n): S {\n if (!isSessionCleared(event)) return source.fold(state, event)\n return markerFolds.has(source.fold)\n ? source.fold(state, event as unknown as ContractEvent<D>)\n : cloneInitial(source.initialState)\n}\n\n/**\n * A pure fold over a session's events — `(state, event) => state` —\n * published inline in a contract or built with `reducer(options).fold(fn)`.\n *\n * The name is the reducer's identity. Cached folds (server snapshots,\n * the browser cache) are keyed by it, which makes it the invalidation\n * knob too: changed the fold's logic? Change the name. Snapshots under\n * the old name are ignored and the next read refolds from raw events.\n * See docs/concepts/04-state.mdx.\n */\nexport type Reducer<\n D extends EventDefs = EventDefs,\n S = unknown,\n P extends PresenceDefs = Record<never, never>,\n> = {\n /** Identity and cache key. Rename when the fold's logic changes. */\n readonly name: string\n /** The contract's vocabulary — types every event in the fold. */\n readonly events: Readonly<D>\n /**\n * The contract's presence vocabulary — carried because the reducer\n * is the client's typed handle on the contract. The fold never sees\n * presence.\n */\n readonly presence: Readonly<P>\n readonly fold: (state: S, event: ContractEvent<D>) => S\n readonly initialState: S\n /**\n * When present: `initialState` was validated against it at\n * definition, and any cached fold a2 rehydrates (server snapshots,\n * the browser cache) is checked against it — failing snapshots are\n * discarded and refolded, catching shape drift a stale name can't.\n */\n readonly stateSchema: StandardSchemaV1<unknown, S> | undefined\n}\n\n/** The second step of `reducer` — see the method's doc. */\nexport type ReducerBuilder<\n D extends EventDefs,\n S,\n P extends PresenceDefs = Record<never, never>,\n> = {\n fold(fold: (state: S, event: ContractEvent<D>) => S): Reducer<D, S, P>\n}\n\n/** Internal — shared validation for inline and standalone reducer definitions. */\nexport function makeReducerBuilder<\n D extends EventDefs,\n P extends PresenceDefs,\n Seed,\n S,\n>(\n events: Readonly<D>,\n presence: Readonly<P>,\n options: {\n name: string\n initialState: Seed\n stateSchema?: StandardSchemaV1<Seed, S>\n },\n): ReducerBuilder<D, S, P> {\n const { name } = options\n if (typeof name !== 'string' || name.length === 0) {\n throw new TypeError(\n 'reducer name must be a non-empty string — it identifies the reducer and keys cached snapshots',\n )\n }\n\n // Without a schema, S = Seed (the type parameter default), so the\n // seed is the state. With one, the seed is the schema's input and the\n // validated output (defaults applied) becomes the fold's actual seed.\n let initialState = options.initialState as unknown as S\n if (options.stateSchema) {\n const result = validateSync(\n options.stateSchema,\n options.initialState,\n 'the stateSchema',\n )\n if (result.issues) {\n throw new TypeError(\n `reducer initialState does not match its stateSchema: ${result.issues\n .map((issue) => issue.message)\n .join('; ')}`,\n )\n }\n initialState = result.value as S\n const restored = validateSync(\n options.stateSchema,\n initialState,\n 'the stateSchema',\n )\n if (\n restored.issues ||\n JSON.stringify(restored.value) !== JSON.stringify(initialState)\n ) {\n throw new TypeError(\n 'stateSchema must accept and preserve its own output so snapshots can be restored',\n )\n }\n }\n\n return {\n fold(fold) {\n if (typeof fold !== 'function') {\n throw new TypeError('reducer fold must be a function')\n }\n return {\n name,\n events,\n presence,\n fold,\n initialState,\n stateSchema: options.stateSchema as\n StandardSchemaV1<unknown, S> | undefined,\n }\n },\n }\n}\n\nexport type RegisteredReducer<\n D extends EventDefs,\n P extends PresenceDefs,\n> = Omit<Reducer<D, unknown, P>, 'fold'> & {\n readonly fold: (state: never, event: never) => unknown\n}\n\nexport function reducer<\n D extends EventDefs,\n P extends PresenceDefs,\n Seed,\n S = Seed,\n>(\n options: ReducerOptions<Seed, S> & {\n contract: { events: Readonly<D>; presence: Readonly<P> }\n },\n): ReducerBuilder<D, S, P> {\n return makeReducerBuilder(\n options.contract.events,\n options.contract.presence,\n options,\n )\n}\n"],"mappings":";;;;;;;;;AASA,SAAgB,aACd,QACA,OACA,MACiC;CACjC,MAAM,SAAS,OAAO,YAAY,CAAC,SAAS,KAAK;CACjD,IAAI,kBAAkB,SACpB,MAAM,IAAI,UACR,kBAAkB,KAAK,0DACzB;CAEF,OAAO;AACT;;;;;;AAOA,SAAgB,iBAAiB,QAA0B,MAAoB;CAC7E,MAAM,QAAQ,OAAO,iBAAiB;CACtC,MAAM,SAAS,OAAO,YAAY,CAAC,SAAS,KAAK;CACjD,IAAI,kBAAkB,SAAS;EAE7B,OAAO,YAAY,CAAC,CAAC;EACrB,MAAM,IAAI,UACR,kBAAkB,KAAK,0DACzB;CACF;AACF;;;;;;;;AChBA,SAAgB,aAAgB,SAAe;CAC7C,IAAI;EACF,OAAO,gBAAgB,OAAO;CAChC,QAAQ;EACN,OAAO;CACT;AACF;AAEA,MAAM,8BAAc,IAAI,QAAgB;;;;;;AAOxC,SAAgB,kBAEd,MAAY;CACZ,YAAY,IAAI,IAAI;CACpB,OAAO;AACT;;AAGA,SAAgB,UACd,QACA,OACA,OACG;CACH,IAAI,CAAC,iBAAiB,KAAK,GAAG,OAAO,OAAO,KAAK,OAAO,KAAK;CAC7D,OAAO,YAAY,IAAI,OAAO,IAAI,IAC9B,OAAO,KAAK,OAAO,KAAoC,IACvD,aAAa,OAAO,YAAY;AACtC;;AAgDA,SAAgB,mBAMd,QACA,UACA,SAKyB;CACzB,MAAM,EAAE,SAAS;CACjB,IAAI,OAAO,SAAS,YAAY,KAAK,WAAW,GAC9C,MAAM,IAAI,UACR,+FACF;CAMF,IAAI,eAAe,QAAQ;CAC3B,IAAI,QAAQ,aAAa;EACvB,MAAM,SAAS,aACb,QAAQ,aACR,QAAQ,cACR,iBACF;EACA,IAAI,OAAO,QACT,MAAM,IAAI,UACR,wDAAwD,OAAO,OAC5D,KAAK,UAAU,MAAM,OAAO,CAAC,CAC7B,KAAK,IAAI,GACd;EAEF,eAAe,OAAO;EACtB,MAAM,WAAW,aACf,QAAQ,aACR,cACA,iBACF;EACA,IACE,SAAS,UACT,KAAK,UAAU,SAAS,KAAK,MAAM,KAAK,UAAU,YAAY,GAE9D,MAAM,IAAI,UACR,kFACF;CAEJ;CAEA,OAAO,EACL,KAAK,MAAM;EACT,IAAI,OAAO,SAAS,YAClB,MAAM,IAAI,UAAU,iCAAiC;EAEvD,OAAO;GACL;GACA;GACA;GACA;GACA;GACA,aAAa,QAAQ;EAEvB;CACF,EACF;AACF;AASA,SAAgB,QAMd,SAGyB;CACzB,OAAO,mBACL,QAAQ,SAAS,QACjB,QAAQ,SAAS,UACjB,OACF;AACF"}
|
|
@@ -139,7 +139,7 @@ type ReducerOptions<Seed, S> = {
|
|
|
139
139
|
*/
|
|
140
140
|
stateSchema?: StandardSchemaV1<Seed, S>;
|
|
141
141
|
};
|
|
142
|
-
type Contract<D extends EventDefs = EventDefs, P extends PresenceDefs = Record<never, never
|
|
142
|
+
type Contract<D extends EventDefs = EventDefs, P extends PresenceDefs = Record<never, never>, R = RegisteredReducer<D, P> | undefined> = {
|
|
143
143
|
/** Identity — prefixes storage keys and queue messages. */
|
|
144
144
|
readonly name: string;
|
|
145
145
|
/** The vocabulary. */
|
|
@@ -148,41 +148,50 @@ type Contract<D extends EventDefs = EventDefs, P extends PresenceDefs = Record<n
|
|
|
148
148
|
readonly presence: Readonly<P>;
|
|
149
149
|
/** Preserve literal types for a handler-returned event batch. */
|
|
150
150
|
readonly batch: EventBatchFactory<D>;
|
|
151
|
-
|
|
152
|
-
* Derive a view from this contract. The reducer's `name` is its
|
|
153
|
-
* identity and snapshot cache key: rename it when the fold's logic
|
|
154
|
-
* changes. Two steps by design: this call fixes the state and event
|
|
155
|
-
* types, so `.fold()` receives fully concrete types — literal unions
|
|
156
|
-
* in your state survive, with zero annotations. (A single call can't
|
|
157
|
-
* do this: TypeScript never contextually types the returns of a
|
|
158
|
-
* context-sensitive argument against generics inferred in the same
|
|
159
|
-
* call.)
|
|
160
|
-
*/
|
|
161
|
-
reducer<Seed, S = Seed>(options: ReducerOptions<Seed, S>): ReducerBuilder<D, S, P>;
|
|
151
|
+
readonly reducer: R;
|
|
162
152
|
};
|
|
163
153
|
/**
|
|
164
154
|
* Define a contract: a name plus the events it understands. Validators
|
|
165
155
|
* must be synchronous — async ones are rejected here, at definition
|
|
166
156
|
* time. The result is a plain, importable, isomorphic value.
|
|
167
157
|
*/
|
|
168
|
-
|
|
169
|
-
/** Identity — prefixes storage keys and queue messages. */
|
|
158
|
+
type ContractOptions<D extends EventDefs, P extends PresenceDefs> = {
|
|
170
159
|
name: string;
|
|
171
|
-
/** The vocabulary: event name → payload schema (Standard Schema). */
|
|
172
160
|
events: D;
|
|
173
|
-
/**
|
|
174
|
-
* Optional second vocabulary: presence field → value schema.
|
|
175
|
-
* Per-participant ephemeral state, replicated last-writer-wins and
|
|
176
|
-
* never stored in the log — reducers cannot see it.
|
|
177
|
-
*/
|
|
178
161
|
presence?: P;
|
|
179
|
-
}
|
|
162
|
+
};
|
|
163
|
+
declare function contract<D extends EventDefs, P extends PresenceDefs = Record<never, never>, Schema extends StandardSchemaV1 = StandardSchemaV1>(options: ContractOptions<D, P> & {
|
|
164
|
+
reducer: {
|
|
165
|
+
name: string;
|
|
166
|
+
initialState: StandardSchemaV1.InferInput<NoInfer<Schema>>;
|
|
167
|
+
stateSchema: Schema;
|
|
168
|
+
fold: (state: StandardSchemaV1.InferOutput<Schema>, event: ContractEvent<NoInfer<D>>) => StandardSchemaV1.InferOutput<Schema>;
|
|
169
|
+
};
|
|
170
|
+
}): Contract<D, P, Reducer<D, StandardSchemaV1.InferOutput<Schema>, P>>;
|
|
171
|
+
declare function contract<D extends EventDefs, P extends PresenceDefs = Record<never, never>, S = unknown>(options: ContractOptions<D, P> & {
|
|
172
|
+
reducer: {
|
|
173
|
+
name: string;
|
|
174
|
+
initialState: S;
|
|
175
|
+
fold: (state: NoInfer<S>, event: ContractEvent<NoInfer<D>>) => NoInfer<S>;
|
|
176
|
+
};
|
|
177
|
+
}): Contract<D, P, Reducer<D, S, P>>;
|
|
178
|
+
declare function contract<D extends EventDefs, P extends PresenceDefs = Record<never, never>, R extends RegisteredReducer<D, P> = RegisteredReducer<D, P>>(options: ContractOptions<D, P> & {
|
|
179
|
+
reducer: R;
|
|
180
|
+
}): Contract<D, P, R>;
|
|
181
|
+
declare function contract<D extends EventDefs, P extends PresenceDefs = Record<never, never>>(options: ContractOptions<D, P> & {
|
|
182
|
+
reducer?: undefined;
|
|
183
|
+
}): Contract<D, P, undefined>;
|
|
180
184
|
//#endregion
|
|
181
185
|
//#region src/reducer.d.ts
|
|
182
|
-
|
|
186
|
+
type StateSnapshot<S> = {
|
|
187
|
+
state: S;
|
|
188
|
+
index: number;
|
|
189
|
+
/** The session's latest clear marker, once it has been cleared. */
|
|
190
|
+
clearIndex?: number | undefined;
|
|
191
|
+
};
|
|
183
192
|
/**
|
|
184
193
|
* A pure fold over a session's events — `(state, event) => state` —
|
|
185
|
-
*
|
|
194
|
+
* published inline in a contract or built with `reducer(options).fold(fn)`.
|
|
186
195
|
*
|
|
187
196
|
* The name is the reducer's identity. Cached folds (server snapshots,
|
|
188
197
|
* the browser cache) are keyed by it, which makes it the invalidation
|
|
@@ -190,7 +199,7 @@ declare const clientStateProjection: unique symbol;
|
|
|
190
199
|
* the old name are ignored and the next read refolds from raw events.
|
|
191
200
|
* See docs/concepts/04-state.mdx.
|
|
192
201
|
*/
|
|
193
|
-
type Reducer<D extends EventDefs = EventDefs, S = unknown, P extends PresenceDefs = Record<never, never
|
|
202
|
+
type Reducer<D extends EventDefs = EventDefs, S = unknown, P extends PresenceDefs = Record<never, never>> = {
|
|
194
203
|
/** Identity and cache key. Rename when the fold's logic changes. */
|
|
195
204
|
readonly name: string;
|
|
196
205
|
/** The contract's vocabulary — types every event in the fold. */
|
|
@@ -203,8 +212,6 @@ type Reducer<D extends EventDefs = EventDefs, S = unknown, P extends PresenceDef
|
|
|
203
212
|
readonly presence: Readonly<P>;
|
|
204
213
|
readonly fold: (state: S, event: ContractEvent<D>) => S;
|
|
205
214
|
readonly initialState: S;
|
|
206
|
-
/** Internal read-side projection. Never used as a fold input or persisted. */
|
|
207
|
-
readonly [clientStateProjection]?: (state: S) => V;
|
|
208
215
|
/**
|
|
209
216
|
* When present: `initialState` was validated against it at
|
|
210
217
|
* definition, and any cached fold a2 rehydrates (server snapshots,
|
|
@@ -213,10 +220,19 @@ type Reducer<D extends EventDefs = EventDefs, S = unknown, P extends PresenceDef
|
|
|
213
220
|
*/
|
|
214
221
|
readonly stateSchema: StandardSchemaV1<unknown, S> | undefined;
|
|
215
222
|
};
|
|
216
|
-
/** The second step of `
|
|
223
|
+
/** The second step of `reducer` — see the method's doc. */
|
|
217
224
|
type ReducerBuilder<D extends EventDefs, S, P extends PresenceDefs = Record<never, never>> = {
|
|
218
225
|
fold(fold: (state: S, event: ContractEvent<D>) => S): Reducer<D, S, P>;
|
|
219
226
|
};
|
|
227
|
+
type RegisteredReducer<D extends EventDefs, P extends PresenceDefs> = Omit<Reducer<D, unknown, P>, "fold"> & {
|
|
228
|
+
readonly fold: (state: never, event: never) => unknown;
|
|
229
|
+
};
|
|
230
|
+
declare function reducer<D extends EventDefs, P extends PresenceDefs, Seed, S = Seed>(options: ReducerOptions<Seed, S> & {
|
|
231
|
+
contract: {
|
|
232
|
+
events: Readonly<D>;
|
|
233
|
+
presence: Readonly<P>;
|
|
234
|
+
};
|
|
235
|
+
}): ReducerBuilder<D, S, P>;
|
|
220
236
|
//#endregion
|
|
221
|
-
export {
|
|
222
|
-
//# sourceMappingURL=reducer-
|
|
237
|
+
export { contract as _, reducer as a, ContractEvent as c, PresenceDefs as d, PresenceMap as f, WithPresence as g, ReducerOptions as h, StateSnapshot as i, EventBatchFactory as l, PresenceSnapshot as m, ReducerBuilder as n, AppendInput as o, PresencePatch as p, RegisteredReducer as r, Contract as s, Reducer as t, EventDefs as u, StandardSchemaV1 as v };
|
|
238
|
+
//# sourceMappingURL=reducer-BYu6DT7h.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reducer-BYu6DT7h.d.ts","names":[],"sources":["../src/standard-schema.ts","../src/contract.ts","../src/reducer.ts"],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
//#region src/reducer-runtime.ts
|
|
2
|
+
const runtimeKey = Symbol("a2.reducerRuntime");
|
|
3
|
+
function withReducerRuntime(options) {
|
|
4
|
+
const { reducer, ...runtime } = options;
|
|
5
|
+
return {
|
|
6
|
+
...reducer,
|
|
7
|
+
[runtimeKey]: {
|
|
8
|
+
...reducer[runtimeKey],
|
|
9
|
+
...runtime
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function encodeState(options) {
|
|
14
|
+
const storage = options.reducer[runtimeKey]?.storage;
|
|
15
|
+
return storage ? storage.encode(options.state) : options.state;
|
|
16
|
+
}
|
|
17
|
+
function decodeState(options) {
|
|
18
|
+
const storage = options.reducer[runtimeKey]?.storage;
|
|
19
|
+
return storage ? storage.decode(options.state) : options.state;
|
|
20
|
+
}
|
|
21
|
+
function projectClientState(options) {
|
|
22
|
+
const project = options.reducer[runtimeKey]?.project;
|
|
23
|
+
return project ? project(options.state) : options.state;
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
export { withReducerRuntime as i, encodeState as n, projectClientState as r, decodeState as t };
|
|
27
|
+
|
|
28
|
+
//# sourceMappingURL=reducer-runtime-DQwz_J59.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reducer-runtime-DQwz_J59.js","names":[],"sources":["../src/reducer-runtime.ts"],"sourcesContent":["const runtimeKey: unique symbol = Symbol('a2.reducerRuntime')\n\ntype StateStorage<S> = {\n encode(state: S): unknown\n decode(stored: unknown): S\n}\n\ntype ReducerRuntime<S> = {\n storage?: StateStorage<S>\n project?: (state: S) => S\n}\n\ntype Carrier = { [runtimeKey]?: ReducerRuntime<unknown> }\n\nexport function withReducerRuntime<R extends object, S>(options: {\n reducer: R\n storage?: StateStorage<S>\n project?: (state: S) => S\n}): R {\n const { reducer, ...runtime } = options\n return {\n ...reducer,\n [runtimeKey]: {\n ...(reducer as Carrier)[runtimeKey],\n ...runtime,\n },\n }\n}\n\nexport function encodeState(options: {\n reducer: object\n state: unknown\n}): unknown {\n const storage = (options.reducer as Carrier)[runtimeKey]?.storage\n return storage ? storage.encode(options.state) : options.state\n}\n\nexport function decodeState(options: {\n reducer: object\n state: unknown\n}): unknown {\n const storage = (options.reducer as Carrier)[runtimeKey]?.storage\n return storage ? storage.decode(options.state) : options.state\n}\n\nexport function projectClientState<S>(options: {\n reducer: object\n state: S\n}): S {\n const project = (options.reducer as Carrier)[runtimeKey]?.project\n return project ? (project(options.state) as S) : options.state\n}\n"],"mappings":";AAAA,MAAM,aAA4B,OAAO,mBAAmB;AAc5D,SAAgB,mBAAwC,SAIlD;CACJ,MAAM,EAAE,SAAS,GAAG,YAAY;CAChC,OAAO;EACL,GAAG;GACF,aAAa;GACZ,GAAI,QAAoB;GACxB,GAAG;EACL;CACF;AACF;AAEA,SAAgB,YAAY,SAGhB;CACV,MAAM,UAAW,QAAQ,QAAoB,WAAW,EAAE;CAC1D,OAAO,UAAU,QAAQ,OAAO,QAAQ,KAAK,IAAI,QAAQ;AAC3D;AAEA,SAAgB,YAAY,SAGhB;CACV,MAAM,UAAW,QAAQ,QAAoB,WAAW,EAAE;CAC1D,OAAO,UAAU,QAAQ,OAAO,QAAQ,KAAK,IAAI,QAAQ;AAC3D;AAEA,SAAgB,mBAAsB,SAGhC;CACJ,MAAM,UAAW,QAAQ,QAAoB,WAAW,EAAE;CAC1D,OAAO,UAAW,QAAQ,QAAQ,KAAK,IAAU,QAAQ;AAC3D"}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { r as Clock } from "./store-
|
|
2
|
-
import {
|
|
1
|
+
import { r as Clock } from "./store-Gpkq7WBo.js";
|
|
2
|
+
import { A as SchedulerTask, t as A2Scheduler } from "./server-CGM8WzRG.js";
|
|
3
3
|
//#region src/scheduler-qstash.d.ts
|
|
4
4
|
type QStashStoreOutage = {
|
|
5
5
|
version: 1;
|
|
6
6
|
/** Zero is the first successor created for a store outage. */
|
|
7
7
|
generation: number;
|
|
8
|
-
/** The first failing drain task's due time. Append identity lives on the task. */
|
|
8
|
+
/** The first failing drain task's due time. Append and clear identity lives on the task. */
|
|
9
9
|
rootDueAt?: number;
|
|
10
10
|
};
|
|
11
11
|
type QStashMessage = SchedulerTask & {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scheduler-qstash.d.ts","names":[],"sources":["../src/scheduler-qstash.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"scheduler-qstash.d.ts","names":[],"sources":["../src/scheduler-qstash.ts"],"mappings":";;;KAqCK;EACH;;EAEA;;EAEA;;KAGG,gBAAgB;;EAEnB,YAAY;;KAGF;EACV;EACA,MAAM;;EAEN,UAAU,SAAS;;EAEnB;IACE;;;EAGF;EACA;;EAEA;;;KAIU,6BAA6B,KACvC;EAGA,UAAU;EACV;IACE;;;;KAKQ;EACV,YAAY,SAAS,6BAA6B;;KAGxC;EACV;;EAEA;;EAEA;EACA;;;KAIU;EACV,QAAQ,SAAS,uBAAuB;EACxC,OAAO,SAAS,sBAAsB;;KAG5B;;EAEV;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA,UAAU,SAAS;;EAEnB,SAAS;;EAET,YAAY;;EAEZ,QAAQ;;iBA4eM,OAAO,UAAS,yBAA8B"}
|
package/dist/scheduler-qstash.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { i as A2Error } from "./session-clear-DGI4jTQ5.js";
|
|
2
|
+
import { _ as serverInternals, d as assertSchedulerTargets, f as classifySchedulerSendFailure, t as DRAIN_TIMINGS } from "./internal-CbVvEwad.js";
|
|
3
|
+
import { n as deliverSchedulerAppend, r as deliverSchedulerClear } from "./server-Rj64fNvX.js";
|
|
3
4
|
import { n as platformVercelOidcToken } from "./platform-B4TnJtWu.js";
|
|
4
|
-
import { n as deliverSchedulerAppend } from "./server-BD5ckxZb.js";
|
|
5
5
|
import { t as retryableLazy } from "./retryable-lazy-DZWmHpii.js";
|
|
6
6
|
import { n as SYSTEM_CLOCK } from "./store-N8PXxDAS.js";
|
|
7
|
-
import { t as normalizeSchedulerTask } from "./scheduler-task-
|
|
7
|
+
import { t as normalizeSchedulerTask } from "./scheduler-task-X1SToDjP.js";
|
|
8
8
|
//#region src/scheduler-qstash.ts
|
|
9
9
|
/**
|
|
10
10
|
* experimental-a2/scheduler-qstash — HTTP scheduler over Upstash QStash.
|
|
11
11
|
*
|
|
12
12
|
* QStash publishes one signed HTTP delivery per scheduler task. Drain tasks
|
|
13
13
|
* use an absolute, whole-second `notBefore` and a fixed-size deduplication id;
|
|
14
|
-
* append tasks hash their stable task id with the callback URL
|
|
14
|
+
* append and clear tasks hash their stable task id with the callback URL, each
|
|
15
|
+
* under its own version tag.
|
|
15
16
|
* Store-outage successors carry a signed private generation and back off to
|
|
16
17
|
* one day without deriving their deduplication identity from the local clock.
|
|
17
18
|
* `handler(...servers)` verifies the signature over the raw request body
|
|
@@ -308,6 +309,14 @@ function qstash(options = {}) {
|
|
|
308
309
|
const clock = options.clock ?? SYSTEM_CLOCK;
|
|
309
310
|
const transport = retryableLazy(() => options.transport ? Promise.resolve(options.transport) : qstashTransport(options, developmentMode)).get;
|
|
310
311
|
const storeOutageDeduplicationId = (task, outage) => {
|
|
312
|
+
if (task.kind === "clear") return digestId([
|
|
313
|
+
"a2:scheduler-qstash:store-outage:clear:v1",
|
|
314
|
+
url,
|
|
315
|
+
task.contract,
|
|
316
|
+
task.sessionId,
|
|
317
|
+
task.id,
|
|
318
|
+
outage.generation
|
|
319
|
+
]);
|
|
311
320
|
if (task.kind === "append") return digestId([
|
|
312
321
|
"a2:scheduler-qstash:store-outage:append:v1",
|
|
313
322
|
url,
|
|
@@ -339,7 +348,16 @@ function qstash(options = {}) {
|
|
|
339
348
|
contract: task.contract,
|
|
340
349
|
sessionId: task.sessionId,
|
|
341
350
|
dueAt,
|
|
342
|
-
events: task.events
|
|
351
|
+
events: task.events,
|
|
352
|
+
...task.originIndex === void 0 ? {} : { originIndex: task.originIndex }
|
|
353
|
+
} : task.kind === "clear" ? {
|
|
354
|
+
version: 1,
|
|
355
|
+
kind: "clear",
|
|
356
|
+
id: task.id,
|
|
357
|
+
contract: task.contract,
|
|
358
|
+
sessionId: task.sessionId,
|
|
359
|
+
dueAt,
|
|
360
|
+
ifLastIndex: task.ifLastIndex
|
|
343
361
|
} : {
|
|
344
362
|
version: 1,
|
|
345
363
|
kind: "drain",
|
|
@@ -353,7 +371,7 @@ function qstash(options = {}) {
|
|
|
353
371
|
};
|
|
354
372
|
const sizeCheckedMessage = outage === void 0 ? {
|
|
355
373
|
...normalizedTask,
|
|
356
|
-
_a2QStash: normalizedTask.kind
|
|
374
|
+
_a2QStash: normalizedTask.kind !== "drain" ? {
|
|
357
375
|
version: 1,
|
|
358
376
|
generation: Number.MAX_SAFE_INTEGER
|
|
359
377
|
} : {
|
|
@@ -373,6 +391,10 @@ function qstash(options = {}) {
|
|
|
373
391
|
"a2:scheduler-qstash:append:v1",
|
|
374
392
|
url,
|
|
375
393
|
task.id
|
|
394
|
+
]) : task.kind === "clear" ? await digestId([
|
|
395
|
+
"a2:scheduler-qstash:clear:v1",
|
|
396
|
+
url,
|
|
397
|
+
task.id
|
|
376
398
|
]) : await digestId([
|
|
377
399
|
"a2:scheduler-qstash:v1",
|
|
378
400
|
url,
|
|
@@ -401,7 +423,7 @@ function qstash(options = {}) {
|
|
|
401
423
|
const delayMs = Math.min(STORE_OUTAGE_MAX_DELAY_MS, STORE_OUTAGE_BASE_DELAY_MS * 4 ** Math.min(generation, 6));
|
|
402
424
|
const nowMs = clock.now().getTime();
|
|
403
425
|
const dueAt = Math.ceil((nowMs + delayMs) / 1e3) * 1e3;
|
|
404
|
-
const nextOutage = task.kind
|
|
426
|
+
const nextOutage = task.kind !== "drain" ? {
|
|
405
427
|
version: 1,
|
|
406
428
|
generation
|
|
407
429
|
} : {
|
|
@@ -450,9 +472,10 @@ function qstash(options = {}) {
|
|
|
450
472
|
const server = byContract.get(task.contract);
|
|
451
473
|
if (!server) throw new Error(`a2 scheduler: no server for contract '${task.contract}' — pass it to scheduler.handler(...)`);
|
|
452
474
|
const internals = serverInternals.get(server);
|
|
453
|
-
if (task.kind
|
|
475
|
+
if (task.kind !== "drain") {
|
|
454
476
|
try {
|
|
455
|
-
await deliverSchedulerAppend(server, task);
|
|
477
|
+
if (task.kind === "append") await deliverSchedulerAppend(server, task);
|
|
478
|
+
else await deliverSchedulerClear(server, task);
|
|
456
479
|
} catch (error) {
|
|
457
480
|
if (!storeUnavailable(error)) throw error;
|
|
458
481
|
await sendStoreOutageSuccessor(task, outage);
|