experimental-a2 0.16.1 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +373 -0
- package/README.md +1 -1
- package/dist/{actor-ohPC-81x.d.ts → actor-Bjce9_5U.d.ts} +21 -11
- package/dist/actor-Bjce9_5U.d.ts.map +1 -0
- package/dist/actor-client.d.ts +2 -5
- package/dist/actor-client.d.ts.map +1 -1
- package/dist/actor-client.js +13 -4
- package/dist/actor-client.js.map +1 -1
- package/dist/actor-react.d.ts +5 -3
- package/dist/actor-react.d.ts.map +1 -1
- package/dist/actor-react.js +20 -10
- package/dist/actor-react.js.map +1 -1
- package/dist/{actor-shared-B5tJfzt-.js → actor-shared-t7w8xIKP.js} +12 -7
- package/dist/actor-shared-t7w8xIKP.js.map +1 -0
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +42 -23
- package/dist/actor.js.map +1 -1
- package/dist/{ai-DgOBltJ_.js → ai-BpOGm-zB.js} +621 -216
- package/dist/ai-BpOGm-zB.js.map +1 -0
- package/dist/{ai-CrEf6p_W.d.ts → ai-DrUaMyRt.d.ts} +114 -100
- package/dist/ai-DrUaMyRt.d.ts.map +1 -0
- package/dist/ai-client.d.ts +27 -0
- package/dist/ai-client.d.ts.map +1 -0
- package/dist/ai-client.js +29 -0
- package/dist/ai-client.js.map +1 -0
- package/dist/ai-fx.browser.d.ts +1 -0
- package/dist/ai-fx.browser.js +6 -0
- package/dist/ai-fx.browser.js.map +1 -0
- package/dist/ai-fx.d.ts +10 -0
- package/dist/ai-fx.d.ts.map +1 -0
- package/dist/ai-fx.js +634 -0
- package/dist/ai-fx.js.map +1 -0
- package/dist/ai-harness-BuLh_EYi.js +273 -0
- package/dist/ai-harness-BuLh_EYi.js.map +1 -0
- package/dist/ai-harness-DvsPeJeB.d.ts +26 -0
- package/dist/ai-harness-DvsPeJeB.d.ts.map +1 -0
- package/dist/ai-pi.browser.d.ts +1 -0
- package/dist/ai-pi.browser.js +6 -0
- package/dist/ai-pi.browser.js.map +1 -0
- package/dist/ai-pi.d.ts +17 -0
- package/dist/ai-pi.d.ts.map +1 -0
- package/dist/ai-pi.js +904 -0
- package/dist/ai-pi.js.map +1 -0
- package/dist/ai-react.d.ts +96 -0
- package/dist/ai-react.d.ts.map +1 -0
- package/dist/ai-react.js +142 -0
- package/dist/ai-react.js.map +1 -0
- package/dist/ai-server.browser.js +2 -2
- package/dist/ai-server.browser.js.map +1 -1
- package/dist/ai-server.d.ts +109 -43
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +526 -283
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +2 -2
- package/dist/ai.js +2 -2
- package/dist/bounded-cache-CPHWF-8h.js +41 -0
- package/dist/bounded-cache-CPHWF-8h.js.map +1 -0
- package/dist/{client-BYzHjkwU.js → client-BReKqwZC.js} +447 -80
- package/dist/client-BReKqwZC.js.map +1 -0
- package/dist/{client-CzyacQpJ.d.ts → client-h_DKFAjj.d.ts} +14 -13
- package/dist/client-h_DKFAjj.d.ts.map +1 -0
- package/dist/client.d.ts +4 -2
- package/dist/client.js +1 -1
- package/dist/{contract-CKRg_E4q.js → contract-BYKASlCV.js} +12 -21
- package/dist/contract-BYKASlCV.js.map +1 -0
- package/dist/{idempotent-replay-DVOlyYbx.js → idempotent-replay-JdgMQRRm.js} +2 -2
- package/dist/{idempotent-replay-DVOlyYbx.js.map → idempotent-replay-JdgMQRRm.js.map} +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/{errors-DvhSXnxk.d.ts → initial-snapshot-Bx-c0pDP.d.ts} +7 -3
- package/dist/initial-snapshot-Bx-c0pDP.d.ts.map +1 -0
- package/dist/{internal-Dq2qYxou.js → internal-B2T3U4Rq.js} +29 -2
- package/dist/internal-B2T3U4Rq.js.map +1 -0
- package/dist/otel.d.ts +1 -1
- package/dist/react-BBXgHWFc.d.ts +86 -0
- package/dist/react-BBXgHWFc.d.ts.map +1 -0
- package/dist/react.d.ts +2 -122
- package/dist/react.js +107 -100
- package/dist/react.js.map +1 -1
- package/dist/reducer-BAXc8JyF.js +87 -0
- package/dist/reducer-BAXc8JyF.js.map +1 -0
- package/dist/{reducer-otzHjuJj.d.ts → reducer-CqA59Q6U.d.ts} +46 -30
- package/dist/reducer-CqA59Q6U.d.ts.map +1 -0
- package/dist/reducer-runtime-DQwz_J59.js +28 -0
- package/dist/reducer-runtime-DQwz_J59.js.map +1 -0
- package/dist/scheduler-qstash.d.ts +3 -3
- package/dist/scheduler-qstash.d.ts.map +1 -1
- package/dist/scheduler-qstash.js +33 -10
- package/dist/scheduler-qstash.js.map +1 -1
- package/dist/{scheduler-task-BpzhPnRS.js → scheduler-task-X1SToDjP.js} +18 -3
- package/dist/scheduler-task-X1SToDjP.js.map +1 -0
- package/dist/scheduler-vercel.d.ts +4 -9
- package/dist/scheduler-vercel.d.ts.map +1 -1
- package/dist/scheduler-vercel.js +25 -44
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/{server-BD5ckxZb.js → server-Dw_k_7kO.js} +904 -219
- package/dist/server-Dw_k_7kO.js.map +1 -0
- package/dist/{server-DbD7IJVK.d.ts → server-En7cbi3v.d.ts} +116 -26
- package/dist/server-En7cbi3v.d.ts.map +1 -0
- package/dist/server.d.ts +4 -4
- package/dist/server.js +2 -2
- package/dist/session-clear-Bo-XTFEy.d.ts +12 -0
- package/dist/session-clear-Bo-XTFEy.d.ts.map +1 -0
- package/dist/{errors-DCk6ch5n.js → session-clear-DGI4jTQ5.js} +17 -2
- package/dist/session-clear-DGI4jTQ5.js.map +1 -0
- package/dist/{store-DyZM6fS5.d.ts → store-Djdv0bnZ.d.ts} +73 -3
- package/dist/store-Djdv0bnZ.d.ts.map +1 -0
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.d.ts.map +1 -1
- package/dist/store-memory.js +205 -66
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-CmxUbV93.js → store-polling-B5FcwJfo.js} +21 -4
- package/dist/store-polling-B5FcwJfo.js.map +1 -0
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.d.ts.map +1 -1
- package/dist/store-postgres.js +450 -108
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-presence-polling-C7-XZyW9.js → store-presence-polling-BAwS8RWn.js} +3 -3
- package/dist/{store-presence-polling-C7-XZyW9.js.map → store-presence-polling-BAwS8RWn.js.map} +1 -1
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.d.ts.map +1 -1
- package/dist/store-redis-http.js +293 -11
- package/dist/store-redis-http.js.map +1 -1
- package/dist/store-redis-notify-BRITti_o.js +2493 -0
- package/dist/store-redis-notify-BRITti_o.js.map +1 -0
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.d.ts.map +1 -1
- package/dist/store-redis.js +11 -11
- package/dist/store-redis.js.map +1 -1
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.d.ts.map +1 -1
- package/dist/store-sqlite.js +197 -33
- package/dist/store-sqlite.js.map +1 -1
- package/dist/stream-buffer-Cx7AAPGq.js +23 -0
- package/dist/stream-buffer-Cx7AAPGq.js.map +1 -0
- package/dist/{telemetry-B5jzpy6y.d.ts → telemetry-Cyfqo6Qt.d.ts} +2 -2
- package/dist/telemetry-Cyfqo6Qt.d.ts.map +1 -0
- package/dist/{wire-BO5wWCb1.js → wire-rSUglYo8.js} +132 -10
- package/dist/wire-rSUglYo8.js.map +1 -0
- package/docs/01-quickstart.mdx +50 -84
- package/docs/{guides/07-examples.mdx → 02-examples.mdx} +10 -20
- package/docs/actors/01-introduction.mdx +48 -82
- package/docs/actors/02-concurrency.mdx +43 -70
- package/docs/actors/03-timers.mdx +28 -42
- package/docs/actors/04-routes.mdx +110 -137
- package/docs/actors/meta.ts +1 -1
- package/docs/agents/01-introduction.mdx +257 -0
- package/docs/agents/02-tools.mdx +247 -0
- package/docs/agents/03-conversations.mdx +538 -0
- package/docs/agents/04-harnesses.mdx +136 -0
- package/docs/agents/meta.ts +1 -0
- package/docs/client/01-react.mdx +294 -0
- package/docs/client/02-transports.mdx +86 -0
- package/docs/client/03-presence.mdx +224 -0
- package/docs/client/meta.ts +1 -0
- package/docs/contract/01-events.mdx +57 -0
- package/docs/contract/02-state.mdx +142 -0
- package/docs/contract/03-sessions.mdx +175 -0
- package/docs/index.mdx +112 -156
- package/docs/reference/01-core.mdx +139 -0
- package/docs/reference/02-server.mdx +855 -0
- package/docs/reference/03-client.mdx +301 -0
- package/docs/reference/04-ai.mdx +722 -0
- package/docs/reference/05-adapters.mdx +351 -0
- package/docs/reference/06-errors.mdx +138 -0
- package/docs/reference/meta.ts +1 -1
- package/docs/server/01-handlers.mdx +200 -0
- package/docs/server/02-timers.mdx +130 -0
- package/docs/server/03-cancellation.mdx +141 -0
- package/docs/server/04-routes.mdx +243 -0
- package/docs/server/05-production.mdx +322 -0
- package/docs/server/06-guarantees.mdx +123 -0
- package/examples/playground/DEPLOY.md +53 -7
- package/examples/playground/app/a0/[appId]/loading.tsx +16 -0
- package/examples/playground/app/a0/[appId]/page.tsx +53 -0
- package/examples/playground/app/a0/a0.css +818 -0
- package/examples/playground/app/a0/app-list.tsx +218 -0
- package/examples/playground/app/a0/app-view.tsx +63 -0
- package/examples/playground/app/a0/apps/[appId]/preview/route.ts +27 -0
- package/examples/playground/app/a0/apps/[appId]/route.ts +28 -0
- package/examples/playground/app/a0/apps.test.ts +128 -0
- package/examples/playground/app/a0/apps.ts +102 -0
- package/examples/playground/app/a0/chat.tsx +410 -0
- package/examples/playground/app/a0/columns.tsx +109 -0
- package/examples/playground/app/a0/context-menu.tsx +116 -0
- package/examples/playground/app/a0/cookies.ts +46 -0
- package/examples/playground/app/a0/crash-on-request.ts +72 -0
- package/examples/playground/app/a0/crashes/route.ts +3 -0
- package/examples/playground/app/a0/crashes.test.ts +33 -0
- package/examples/playground/app/a0/crashes.ts +28 -0
- package/examples/playground/app/a0/devtools.tsx +164 -0
- package/examples/playground/app/a0/events/route.ts +6 -0
- package/examples/playground/app/a0/flags.ts +30 -0
- package/examples/playground/app/a0/fx/events/route.ts +9 -0
- package/examples/playground/app/a0/fx/scheduler/route.ts +7 -0
- package/examples/playground/app/a0/fx/short/events/route.ts +11 -0
- package/examples/playground/app/a0/layout.tsx +38 -0
- package/examples/playground/app/a0/list/route.ts +10 -0
- package/examples/playground/app/a0/model.test.ts +73 -0
- package/examples/playground/app/a0/model.ts +95 -0
- package/examples/playground/app/a0/no-photon.cjs +4 -0
- package/examples/playground/app/a0/no-quickjs.cjs +3 -0
- package/examples/playground/app/a0/page.tsx +14 -0
- package/examples/playground/app/a0/pi/events/route.ts +9 -0
- package/examples/playground/app/a0/pi/scheduler/route.ts +7 -0
- package/examples/playground/app/a0/pi/short/events/route.ts +11 -0
- package/examples/playground/app/a0/preview-pane.tsx +145 -0
- package/examples/playground/app/a0/preview.test.ts +53 -0
- package/examples/playground/app/a0/preview.ts +26 -0
- package/examples/playground/app/a0/sandbox-log.ts +75 -0
- package/examples/playground/app/a0/sandbox-session.ts +152 -0
- package/examples/playground/app/a0/sandbox.ts +191 -0
- package/examples/playground/app/a0/sandboxes/scheduler/route.ts +6 -0
- package/examples/playground/app/a0/scheduler/route.ts +7 -0
- package/examples/playground/app/a0/server.ts +213 -0
- package/examples/playground/app/a0/session.ts +59 -0
- package/examples/playground/app/a0/shell.tsx +158 -0
- package/examples/playground/app/a0/short/events/route.ts +9 -0
- package/examples/playground/app/a0/splitter.tsx +120 -0
- package/examples/playground/app/agent/[agentId]/agent-client.tsx +144 -174
- package/examples/playground/app/agent/[agentId]/command-receipt.test.tsx +65 -0
- package/examples/playground/app/agent/[agentId]/command-receipt.tsx +27 -0
- package/examples/playground/app/agent/[agentId]/compaction-event.tsx +25 -24
- package/examples/playground/app/agent/[agentId]/compaction-panel.test.tsx +119 -0
- package/examples/playground/app/agent/[agentId]/compaction-panel.tsx +208 -165
- package/examples/playground/app/agent/[agentId]/page.tsx +2 -16
- package/examples/playground/app/agent/events/route.ts +5 -2
- package/examples/playground/app/agent/ingress.test.ts +101 -0
- package/examples/playground/app/agent/model-input.test.tsx +58 -0
- package/examples/playground/app/agent/model-input.tsx +115 -0
- package/examples/playground/app/agent/model.test.ts +73 -0
- package/examples/playground/app/agent/model.ts +58 -26
- package/examples/playground/app/agent/new-agent-session.tsx +37 -19
- package/examples/playground/app/agent/page.tsx +4 -4
- package/examples/playground/app/agent/server.ts +19 -15
- package/examples/playground/app/agent/session.ts +5 -6
- package/examples/playground/app/agent/telemetry.test.ts +189 -0
- package/examples/playground/app/agent/telemetry.ts +30 -0
- package/examples/playground/app/agent/trace.test.ts +23 -0
- package/examples/playground/app/agent/trace.ts +18 -0
- package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +12 -12
- package/examples/playground/app/canvas/[canvasId]/page.tsx +1 -2
- package/examples/playground/app/canvas/[canvasId]/session.ts +2 -2
- package/examples/playground/app/canvas/model.ts +53 -53
- package/examples/playground/app/chat/[chatId]/agent-state.test.ts +176 -0
- package/examples/playground/app/chat/[chatId]/agent-stream-drawer.tsx +16 -1
- package/examples/playground/app/chat/[chatId]/chat-client.tsx +55 -77
- package/examples/playground/app/chat/[chatId]/chat-view.ts +37 -3
- package/examples/playground/app/chat/[chatId]/page.tsx +7 -15
- package/examples/playground/app/chat/[chatId]/session.ts +2 -19
- package/examples/playground/app/chat/agent/route.ts +9 -0
- package/examples/playground/app/chat/model.test.ts +46 -119
- package/examples/playground/app/chat/model.ts +82 -128
- package/examples/playground/app/chat/scheduler/route.ts +2 -2
- package/examples/playground/app/chat/server.test.ts +112 -0
- package/examples/playground/app/chat/server.ts +113 -117
- package/examples/playground/app/components/activity-feed.tsx +30 -1
- package/examples/playground/app/components/activity-history.test.tsx +35 -0
- package/examples/playground/app/components/demo-icons.tsx +89 -0
- package/examples/playground/app/components/demos.tsx +33 -0
- package/examples/playground/app/components/fast-link.test.tsx +58 -0
- package/examples/playground/app/components/fast-link.tsx +55 -0
- package/examples/playground/app/components/site-header.tsx +96 -0
- package/examples/playground/app/counter/counter-client.tsx +3 -3
- package/examples/playground/app/counter/model.test.ts +3 -3
- package/examples/playground/app/counter/model.ts +11 -21
- package/examples/playground/app/counter/page.tsx +2 -15
- package/examples/playground/app/counter/session.ts +2 -2
- package/examples/playground/app/documents/[documentId]/code-editor.tsx +12 -1
- package/examples/playground/app/documents/[documentId]/document-client.tsx +10 -11
- package/examples/playground/app/documents/[documentId]/page.tsx +1 -2
- package/examples/playground/app/documents/[documentId]/session.ts +2 -2
- package/examples/playground/app/documents/model.ts +27 -27
- package/examples/playground/app/globals.css +420 -207
- package/examples/playground/app/initial-state.test.tsx +54 -0
- package/examples/playground/app/layout.tsx +7 -23
- package/examples/playground/app/orders/[orderId]/page.tsx +1 -2
- package/examples/playground/app/orders/[orderId]/session.ts +2 -2
- package/examples/playground/app/orders/model.ts +49 -49
- package/examples/playground/app/page.tsx +13 -102
- package/examples/playground/app/recovery/[recoveryId]/page.tsx +1 -2
- package/examples/playground/app/recovery/[recoveryId]/session.ts +2 -2
- package/examples/playground/app/recovery/model.ts +30 -30
- package/examples/playground/instrumentation.ts +5 -0
- package/examples/playground/next.config.ts +17 -5
- package/examples/playground/package.json +13 -2
- package/examples/playground/scripts/a0-template.ts +77 -0
- package/examples/playground/vercel.json +36 -0
- package/package.json +29 -3
- package/src/actor-client.ts +16 -4
- package/src/actor-react.ts +25 -11
- package/src/actor-shared.ts +21 -6
- package/src/actor.ts +76 -31
- package/src/ai-agent-actions.ts +219 -0
- package/src/ai-agent.ts +2517 -0
- package/src/ai-app-state.ts +91 -0
- package/src/ai-client-state.ts +6 -3
- package/src/ai-client.ts +62 -0
- package/src/ai-context-schema.ts +25 -5
- package/src/ai-context.ts +90 -40
- package/src/ai-control-server.ts +92 -72
- package/src/ai-coordinator.ts +23 -22
- package/src/ai-fx.browser.ts +5 -0
- package/src/ai-fx.ts +833 -0
- package/src/ai-harness.ts +518 -0
- package/src/ai-message-base.ts +209 -0
- package/src/ai-message-projection.ts +262 -64
- package/src/ai-model-metadata.ts +11 -6
- package/src/ai-pi.browser.ts +5 -0
- package/src/ai-pi.ts +1263 -0
- package/src/ai-progress-batches.ts +26 -0
- package/src/ai-projector.ts +95 -14
- package/src/ai-react.ts +146 -0
- package/src/ai-sdk-step.ts +2 -2
- package/src/ai-server.browser.ts +2 -2
- package/src/ai-server.ts +23 -1965
- package/src/ai-stored-state.ts +48 -163
- package/src/ai.ts +339 -155
- package/src/bootstrap-stream.ts +72 -0
- package/src/bounded-cache.ts +55 -0
- package/src/client-render-session.ts +49 -0
- package/src/client.ts +688 -151
- package/src/contract.ts +92 -42
- package/src/deterministic-id.ts +37 -0
- package/src/equal-json.ts +32 -0
- package/src/errors.ts +13 -1
- package/src/index.ts +5 -1
- package/src/initial-snapshot.ts +39 -0
- package/src/internal.ts +52 -0
- package/src/libfx.d.ts +34 -0
- package/src/postgres-notification-scope.ts +14 -3
- package/src/react.ts +171 -206
- package/src/redis-http-reply.ts +256 -0
- package/src/redis-http-stream.ts +73 -0
- package/src/redis-http-subscriptions.ts +13 -5
- package/src/reducer-runtime.ts +52 -0
- package/src/reducer.ts +93 -11
- package/src/scheduler-qstash.ts +49 -20
- package/src/scheduler-task.ts +34 -2
- package/src/scheduler-vercel.ts +32 -84
- package/src/server-fetch.ts +139 -35
- package/src/server.ts +1133 -255
- package/src/session-clear.ts +23 -0
- package/src/session-socket.ts +154 -49
- package/src/sse.ts +71 -4
- package/src/store-memory.ts +252 -75
- package/src/store-polling.ts +18 -4
- package/src/store-postgres.ts +541 -114
- package/src/store-redis-core.ts +1450 -493
- package/src/store-redis-http.ts +18 -2
- package/src/store-redis-notify.ts +262 -85
- package/src/store-redis.ts +19 -9
- package/src/store-sqlite.ts +319 -45
- package/src/store.ts +79 -1
- package/src/stream-buffer.ts +43 -0
- package/src/telemetry.ts +14 -1
- package/src/wire.ts +241 -13
- package/dist/actor-ohPC-81x.d.ts.map +0 -1
- package/dist/actor-shared-B5tJfzt-.js.map +0 -1
- package/dist/ai-CrEf6p_W.d.ts.map +0 -1
- package/dist/ai-DgOBltJ_.js.map +0 -1
- package/dist/client-BYzHjkwU.js.map +0 -1
- package/dist/client-CzyacQpJ.d.ts.map +0 -1
- package/dist/contract-CKRg_E4q.js.map +0 -1
- package/dist/errors-DCk6ch5n.js.map +0 -1
- package/dist/errors-DvhSXnxk.d.ts.map +0 -1
- package/dist/internal-Dq2qYxou.js.map +0 -1
- package/dist/react.d.ts.map +0 -1
- package/dist/reducer-DEMjEY_O.js +0 -29
- package/dist/reducer-DEMjEY_O.js.map +0 -1
- package/dist/reducer-otzHjuJj.d.ts.map +0 -1
- package/dist/scheduler-task-BpzhPnRS.js.map +0 -1
- package/dist/server-BD5ckxZb.js.map +0 -1
- package/dist/server-DbD7IJVK.d.ts.map +0 -1
- package/dist/store-DyZM6fS5.d.ts.map +0 -1
- package/dist/store-polling-CmxUbV93.js.map +0 -1
- package/dist/store-redis-notify-BVLUfI3j.js +0 -1583
- package/dist/store-redis-notify-BVLUfI3j.js.map +0 -1
- package/dist/telemetry-B5jzpy6y.d.ts.map +0 -1
- package/dist/validate-XKT4FSNn.js +0 -30
- package/dist/validate-XKT4FSNn.js.map +0 -1
- package/dist/wire-BO5wWCb1.js.map +0 -1
- package/docs/concepts/01-contracts.mdx +0 -142
- package/docs/concepts/02-handlers.mdx +0 -284
- package/docs/concepts/03-durability.mdx +0 -314
- package/docs/concepts/04-state.mdx +0 -209
- package/docs/guides/01-timers.mdx +0 -185
- package/docs/guides/02-cancellation.mdx +0 -161
- package/docs/guides/03-react.mdx +0 -384
- package/docs/guides/04-local-first.mdx +0 -88
- package/docs/guides/05-production.mdx +0 -521
- package/docs/guides/06-ai-agents.mdx +0 -1318
- package/docs/guides/08-application-data.mdx +0 -113
- package/docs/guides/09-presence.mdx +0 -266
- package/docs/guides/10-transports.mdx +0 -221
- package/docs/reference/01-api.mdx +0 -1896
- package/docs/reference/02-errors.mdx +0 -149
- package/examples/playground/app/agent/[agentId]/compaction/route.ts +0 -14
- package/examples/playground/app/agent/compaction-settings.test.ts +0 -144
- package/examples/playground/app/agent/compaction-settings.ts +0 -49
- package/examples/playground/app/agent/compaction-timeline.test.ts +0 -377
- package/examples/playground/app/agent/compaction-timeline.ts +0 -208
- /package/docs/{concepts → contract}/meta.ts +0 -0
- /package/docs/{guides → server}/meta.ts +0 -0
package/src/store-redis-http.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { A2Error } from './errors.ts'
|
|
2
2
|
import { NOTIFY_TIMINGS } from './internal.ts'
|
|
3
3
|
import { redisHttpSubscriptions } from './redis-http-subscriptions.ts'
|
|
4
|
+
import {
|
|
5
|
+
readRedisStreamPage,
|
|
6
|
+
REDIS_HTTP_STREAM_READ_COUNT,
|
|
7
|
+
} from './redis-http-stream.ts'
|
|
8
|
+
import type { RedisStreamRead } from './store-redis-core.ts'
|
|
4
9
|
import {
|
|
5
10
|
createRedisNotifyStore,
|
|
6
11
|
type RedisConnection,
|
|
@@ -74,6 +79,14 @@ export function redisHttp(options: RedisHttpStoreOptions): RedisHttpStore {
|
|
|
74
79
|
return body.result
|
|
75
80
|
}
|
|
76
81
|
|
|
82
|
+
const readStream: RedisStreamRead = (read) =>
|
|
83
|
+
readRedisStreamPage({
|
|
84
|
+
...read,
|
|
85
|
+
url,
|
|
86
|
+
token,
|
|
87
|
+
count: REDIS_HTTP_STREAM_READ_COUNT,
|
|
88
|
+
})
|
|
89
|
+
|
|
77
90
|
let subscriber: RedisConnection | undefined
|
|
78
91
|
let onReconnect: ((channel: string) => void) | undefined
|
|
79
92
|
const client: RedisConnection = {
|
|
@@ -85,7 +98,7 @@ export function redisHttp(options: RedisHttpStoreOptions): RedisHttpStore {
|
|
|
85
98
|
onReconnect: (channel) => onReconnect?.(channel),
|
|
86
99
|
fallback: {
|
|
87
100
|
enabled: () => polling,
|
|
88
|
-
|
|
101
|
+
checkCommands() {
|
|
89
102
|
fallbackCheck ??= call('PING')
|
|
90
103
|
.then((result) => {
|
|
91
104
|
if (result !== 'PONG')
|
|
@@ -93,7 +106,6 @@ export function redisHttp(options: RedisHttpStoreOptions): RedisHttpStore {
|
|
|
93
106
|
'STORE_UNAVAILABLE',
|
|
94
107
|
'redis rest capability check failed',
|
|
95
108
|
)
|
|
96
|
-
polling = true
|
|
97
109
|
return undefined
|
|
98
110
|
})
|
|
99
111
|
.catch((error: unknown) => {
|
|
@@ -102,6 +114,9 @@ export function redisHttp(options: RedisHttpStoreOptions): RedisHttpStore {
|
|
|
102
114
|
})
|
|
103
115
|
return fallbackCheck
|
|
104
116
|
},
|
|
117
|
+
enable() {
|
|
118
|
+
polling = true
|
|
119
|
+
},
|
|
105
120
|
},
|
|
106
121
|
})),
|
|
107
122
|
on() {},
|
|
@@ -117,5 +132,6 @@ export function redisHttp(options: RedisHttpStoreOptions): RedisHttpStore {
|
|
|
117
132
|
clock: options.clock ?? SYSTEM_CLOCK,
|
|
118
133
|
ids: options.ids ?? RANDOM_IDS,
|
|
119
134
|
keyPrefix: options.keyPrefix ?? 'a2',
|
|
135
|
+
readStream,
|
|
120
136
|
})
|
|
121
137
|
}
|
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
import type { PresencePatch } from './contract.ts'
|
|
2
2
|
import { A2Error } from './errors.ts'
|
|
3
|
-
import { NOTIFY_TIMINGS } from './internal.ts'
|
|
3
|
+
import { NOTIFY_TIMINGS, stateTailReaders } from './internal.ts'
|
|
4
4
|
import { defaultSleep, pollingStream, type Sleeper } from './store-polling.ts'
|
|
5
5
|
import { pollPresence } from './store-presence-polling.ts'
|
|
6
6
|
import { presenceRecovery } from './presence-recovery.ts'
|
|
7
7
|
import { createStreamActivity, streamActivities } from './stream-activity.ts'
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
bufferedEvents,
|
|
10
|
+
isSerializedEvent,
|
|
11
|
+
parseSerializedEvent,
|
|
12
|
+
type HeldEvent,
|
|
13
|
+
type SerializedEvent,
|
|
14
|
+
} from './stream-buffer.ts'
|
|
15
|
+
import {
|
|
16
|
+
createRedisStoreCore,
|
|
17
|
+
FEED_PAGE_SIZE,
|
|
18
|
+
type RedisCall,
|
|
19
|
+
type RedisStreamRead,
|
|
20
|
+
} from './store-redis-core.ts'
|
|
9
21
|
import type { A2Store, Clock, Event, IdSource } from './store.ts'
|
|
10
22
|
|
|
11
23
|
export type RedisConnection = {
|
|
@@ -32,14 +44,72 @@ type PresenceSubscription = {
|
|
|
32
44
|
recovery?: ReturnType<typeof presenceRecovery> | undefined
|
|
33
45
|
}
|
|
34
46
|
|
|
47
|
+
/**
|
|
48
|
+
* One live event feed's view of its channel: wake-up messages received
|
|
49
|
+
* since it subscribed, whether it must catch up with XRANGE before trusting
|
|
50
|
+
* them, and the resolver of its park, if parked.
|
|
51
|
+
*/
|
|
52
|
+
type Feed = {
|
|
53
|
+
inbox: string[]
|
|
54
|
+
bytes: number
|
|
55
|
+
dirty: boolean
|
|
56
|
+
wake: (() => void) | null
|
|
57
|
+
stopRead: () => Promise<void>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A feed that falls this far behind drops its inbox and catches up. */
|
|
61
|
+
const INBOX_BYTES = 512 * 1024
|
|
62
|
+
|
|
63
|
+
type WakeEntry = [index: number, id: string, type: string, payload: unknown]
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Parses a wake-up message: `{"f":frontier}` or a bare frontier number
|
|
67
|
+
* announces events without carrying them; `{"f","t","e"}` carries them.
|
|
68
|
+
* Returns null for anything else, which callers treat as a hint to catch up.
|
|
69
|
+
*/
|
|
70
|
+
const parseWake = (
|
|
71
|
+
message: string,
|
|
72
|
+
): { frontier: number; createdAt: number; entries: WakeEntry[] } | null => {
|
|
73
|
+
let parsed: unknown
|
|
74
|
+
try {
|
|
75
|
+
parsed = JSON.parse(message)
|
|
76
|
+
} catch {
|
|
77
|
+
return null
|
|
78
|
+
}
|
|
79
|
+
if (typeof parsed === 'number') {
|
|
80
|
+
return Number.isSafeInteger(parsed)
|
|
81
|
+
? { frontier: parsed, createdAt: 0, entries: [] }
|
|
82
|
+
: null
|
|
83
|
+
}
|
|
84
|
+
if (typeof parsed !== 'object' || parsed === null) return null
|
|
85
|
+
const { f, t, e } = parsed as { f?: unknown; t?: unknown; e?: unknown }
|
|
86
|
+
if (typeof f !== 'number' || !Number.isSafeInteger(f)) return null
|
|
87
|
+
if (e === undefined) return { frontier: f, createdAt: 0, entries: [] }
|
|
88
|
+
if (!Array.isArray(e) || typeof t !== 'number' || !Number.isFinite(t))
|
|
89
|
+
return null
|
|
90
|
+
for (const entry of e) {
|
|
91
|
+
if (
|
|
92
|
+
!Array.isArray(entry) ||
|
|
93
|
+
entry.length !== 4 ||
|
|
94
|
+
!Number.isSafeInteger(entry[0]) ||
|
|
95
|
+
typeof entry[1] !== 'string' ||
|
|
96
|
+
typeof entry[2] !== 'string'
|
|
97
|
+
)
|
|
98
|
+
return null
|
|
99
|
+
}
|
|
100
|
+
return { frontier: f, createdAt: t, entries: e as WakeEntry[] }
|
|
101
|
+
}
|
|
102
|
+
|
|
35
103
|
export function createRedisNotifyStore(options: {
|
|
36
104
|
client: () => Promise<RedisConnection>
|
|
105
|
+
callBytes?: RedisCall
|
|
37
106
|
closeClient: () => Promise<void>
|
|
38
107
|
clock: Clock
|
|
39
108
|
ids: IdSource
|
|
40
109
|
keyPrefix: string
|
|
41
110
|
onReconnect?: (listener: (channel: string) => void) => void
|
|
42
111
|
polling?: () => boolean
|
|
112
|
+
readStream?: RedisStreamRead
|
|
43
113
|
}): A2Store & { close(): Promise<void> } {
|
|
44
114
|
const { client, clock, ids, keyPrefix: prefix } = options
|
|
45
115
|
const notifyChannel = (sessionId: string): string =>
|
|
@@ -56,31 +126,27 @@ export function createRedisNotifyStore(options: {
|
|
|
56
126
|
ids,
|
|
57
127
|
keyPrefix: prefix,
|
|
58
128
|
presenceSource: instanceId,
|
|
129
|
+
...(options.callBytes ? { callBytes: options.callBytes } : {}),
|
|
130
|
+
...(options.readStream ? { readStream: options.readStream } : {}),
|
|
59
131
|
})
|
|
60
132
|
|
|
61
133
|
let severed = false
|
|
62
134
|
let subscriber: Promise<RedisConnection> | null = null
|
|
63
|
-
/** channel →
|
|
64
|
-
const
|
|
135
|
+
/** channel → live event feeds, registered before they subscribe. */
|
|
136
|
+
const feeds = new Map<string, Set<Feed>>()
|
|
65
137
|
/** channel → refcounted subscription held for each live iterator. */
|
|
66
138
|
const subscriptions = new Map<
|
|
67
139
|
string,
|
|
68
140
|
{ refs: number; ready: Promise<unknown> }
|
|
69
141
|
>()
|
|
70
|
-
/**
|
|
71
|
-
* channel → last presence-marker refresh, throttling the SET.
|
|
72
|
-
* Deliberately real time (`Date.now()`), not the injected clock:
|
|
73
|
-
* the marker's PX expiry runs on server real time, and the throttle
|
|
74
|
-
* must tick with the TTL it refreshes or time-traveling tests would
|
|
75
|
-
* desync the two. Process-local and never stored, so the injected
|
|
76
|
-
* clock's every-stored-timestamp pledge is untouched.
|
|
77
|
-
*/
|
|
78
|
-
const watched = new Map<string, { at: number; ready: Promise<void> }>()
|
|
79
142
|
const presenceSubscriptions = new Map<string, PresenceSubscription>()
|
|
80
143
|
const pollingStreams = new Set<AsyncIterator<Event>>()
|
|
81
144
|
|
|
82
145
|
options.onReconnect?.((channel) => {
|
|
83
|
-
for (const
|
|
146
|
+
for (const feed of feeds.get(channel) ?? []) {
|
|
147
|
+
feed.dirty = true
|
|
148
|
+
feed.wake?.()
|
|
149
|
+
}
|
|
84
150
|
void presenceSubscriptions
|
|
85
151
|
.get(channel)
|
|
86
152
|
?.recovery?.reconnect()
|
|
@@ -115,11 +181,16 @@ export function createRedisNotifyStore(options: {
|
|
|
115
181
|
subscriber ??= (async () => {
|
|
116
182
|
const conn = (await client()).duplicate()
|
|
117
183
|
conn.on('message', (channel, message) => {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
184
|
+
for (const feed of feeds.get(channel) ?? []) {
|
|
185
|
+
if (feed.bytes + message.length <= INBOX_BYTES) {
|
|
186
|
+
feed.inbox.push(message)
|
|
187
|
+
feed.bytes += message.length
|
|
188
|
+
} else {
|
|
189
|
+
feed.inbox.length = 0
|
|
190
|
+
feed.bytes = 0
|
|
191
|
+
feed.dirty = true
|
|
192
|
+
}
|
|
193
|
+
feed.wake?.()
|
|
123
194
|
}
|
|
124
195
|
const subscription = presenceSubscriptions.get(channel)
|
|
125
196
|
if (subscription) deliverRemotePresence({ subscription, message })
|
|
@@ -204,36 +275,12 @@ export function createRedisNotifyStore(options: {
|
|
|
204
275
|
lease.refs -= 1
|
|
205
276
|
if (lease.refs > 0) return
|
|
206
277
|
subscriptions.delete(channel)
|
|
207
|
-
watched.delete(channel)
|
|
208
278
|
void gatedSubscriber()
|
|
209
279
|
.then((conn) => conn.call('unsubscribe', channel))
|
|
210
280
|
.catch(() => {})
|
|
211
281
|
}
|
|
212
282
|
|
|
213
|
-
const
|
|
214
|
-
channel: string,
|
|
215
|
-
): { wakeup: Promise<void>; wake: () => void; disarm: () => void } => {
|
|
216
|
-
let wake!: () => void
|
|
217
|
-
const wakeup = new Promise<void>((resolve) => {
|
|
218
|
-
wake = resolve // assigned synchronously by the executor
|
|
219
|
-
})
|
|
220
|
-
let set = wakers.get(channel)
|
|
221
|
-
if (!set) {
|
|
222
|
-
set = new Set()
|
|
223
|
-
wakers.set(channel, set)
|
|
224
|
-
}
|
|
225
|
-
set.add(wake)
|
|
226
|
-
return {
|
|
227
|
-
wakeup,
|
|
228
|
-
wake,
|
|
229
|
-
disarm: () => {
|
|
230
|
-
set.delete(wake)
|
|
231
|
-
if (set.size === 0) wakers.delete(channel)
|
|
232
|
-
},
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
return {
|
|
283
|
+
const store: A2Store & { close(): Promise<void> } = {
|
|
237
284
|
append: core.append,
|
|
238
285
|
read: core.read,
|
|
239
286
|
readHistory: core.readHistory!,
|
|
@@ -243,6 +290,10 @@ export function createRedisNotifyStore(options: {
|
|
|
243
290
|
failAttempt: core.failAttempt,
|
|
244
291
|
readState: core.readState,
|
|
245
292
|
putSnapshots: core.putSnapshots,
|
|
293
|
+
clear: core.clear!,
|
|
294
|
+
reclaim: core.reclaim!,
|
|
295
|
+
deadLetters: core.deadLetters!,
|
|
296
|
+
redrive: core.redrive!,
|
|
246
297
|
|
|
247
298
|
presence: {
|
|
248
299
|
async set(ns, participant, values, meta) {
|
|
@@ -311,18 +362,68 @@ export function createRedisNotifyStore(options: {
|
|
|
311
362
|
return {
|
|
312
363
|
[Symbol.asyncIterator](): AsyncIterator<Event> {
|
|
313
364
|
let last = startAfter
|
|
314
|
-
let buffer:
|
|
365
|
+
let buffer: HeldEvent[] = []
|
|
315
366
|
let closed = false
|
|
316
367
|
let subscribed = false
|
|
317
368
|
let pending: Sleeper | null = null
|
|
318
|
-
let interrupt: (() => void) | null = null
|
|
319
369
|
let poller: AsyncIterator<Event> | undefined
|
|
320
370
|
let stopActivity: (() => void) | undefined
|
|
371
|
+
let catchup:
|
|
372
|
+
| {
|
|
373
|
+
controller: AbortController
|
|
374
|
+
iterator: AsyncGenerator<SerializedEvent[], void>
|
|
375
|
+
delivered: boolean
|
|
376
|
+
}
|
|
377
|
+
| undefined
|
|
378
|
+
let stoppingRead: Promise<void> | undefined
|
|
379
|
+
const stopRead = (): Promise<void> => {
|
|
380
|
+
const reading = catchup
|
|
381
|
+
catchup = undefined
|
|
382
|
+
if (reading) {
|
|
383
|
+
reading.controller.abort()
|
|
384
|
+
stoppingRead = reading.iterator
|
|
385
|
+
.return(undefined)
|
|
386
|
+
.then(() => undefined)
|
|
387
|
+
.catch(() => {})
|
|
388
|
+
}
|
|
389
|
+
return stoppingRead ?? Promise.resolve()
|
|
390
|
+
}
|
|
391
|
+
const nextRead = async (
|
|
392
|
+
after: number,
|
|
393
|
+
): Promise<IteratorResult<SerializedEvent[], void>> => {
|
|
394
|
+
if (!catchup) {
|
|
395
|
+
const controller = new AbortController()
|
|
396
|
+
catchup = {
|
|
397
|
+
controller,
|
|
398
|
+
delivered: false,
|
|
399
|
+
iterator: core.streamEvents!({
|
|
400
|
+
sessionId,
|
|
401
|
+
afterIndex: after,
|
|
402
|
+
signal: controller.signal,
|
|
403
|
+
})[Symbol.asyncIterator](),
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const reading = catchup
|
|
407
|
+
const result = await reading.iterator.next()
|
|
408
|
+
if (result.done && catchup === reading) {
|
|
409
|
+
catchup = undefined
|
|
410
|
+
if (reading.delivered) feed.dirty = true
|
|
411
|
+
} else if (!result.done) reading.delivered = true
|
|
412
|
+
return result
|
|
413
|
+
}
|
|
414
|
+
const feed: Feed = {
|
|
415
|
+
inbox: [],
|
|
416
|
+
bytes: 0,
|
|
417
|
+
dirty: true,
|
|
418
|
+
wake: null,
|
|
419
|
+
stopRead,
|
|
420
|
+
}
|
|
321
421
|
const activity = createStreamActivity(() => {
|
|
322
422
|
if (poller) streamActivities.get(poller)?.activate()
|
|
323
423
|
})
|
|
324
424
|
|
|
325
425
|
const release = (): void => {
|
|
426
|
+
void stopRead()
|
|
326
427
|
stopActivity?.()
|
|
327
428
|
if (poller) {
|
|
328
429
|
void poller.return?.()
|
|
@@ -330,9 +431,46 @@ export function createRedisNotifyStore(options: {
|
|
|
330
431
|
}
|
|
331
432
|
if (!subscribed) return
|
|
332
433
|
subscribed = false
|
|
434
|
+
const set = feeds.get(channel)
|
|
435
|
+
set?.delete(feed)
|
|
436
|
+
if (set?.size === 0) feeds.delete(channel)
|
|
333
437
|
releaseChannel(channel)
|
|
334
438
|
}
|
|
335
439
|
|
|
440
|
+
/**
|
|
441
|
+
* Moves carried events that continue this feed's sequence into
|
|
442
|
+
* the buffer. A gap, a message without events, or an unreadable
|
|
443
|
+
* message marks the feed dirty so it catches up with XRANGE.
|
|
444
|
+
*/
|
|
445
|
+
const applyInbox = (): void => {
|
|
446
|
+
feed.bytes = 0
|
|
447
|
+
for (const message of feed.inbox.splice(0)) {
|
|
448
|
+
const wake = parseWake(message)
|
|
449
|
+
if (!wake) {
|
|
450
|
+
feed.dirty = true
|
|
451
|
+
continue
|
|
452
|
+
}
|
|
453
|
+
for (const [index, id, type, payload] of wake.entries) {
|
|
454
|
+
const expected = (buffer.at(-1)?.index ?? last) + 1
|
|
455
|
+
if (index < expected) continue
|
|
456
|
+
if (index > expected) {
|
|
457
|
+
feed.dirty = true
|
|
458
|
+
break
|
|
459
|
+
}
|
|
460
|
+
buffer.push({
|
|
461
|
+
id,
|
|
462
|
+
type,
|
|
463
|
+
payload,
|
|
464
|
+
index,
|
|
465
|
+
sessionId,
|
|
466
|
+
createdAt: new Date(wake.createdAt),
|
|
467
|
+
})
|
|
468
|
+
}
|
|
469
|
+
if (wake.frontier > (buffer.at(-1)?.index ?? last))
|
|
470
|
+
feed.dirty = true
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
336
474
|
const iterator: AsyncIterator<Event> = {
|
|
337
475
|
async next(): Promise<IteratorResult<Event>> {
|
|
338
476
|
for (;;) {
|
|
@@ -342,21 +480,41 @@ export function createRedisNotifyStore(options: {
|
|
|
342
480
|
}
|
|
343
481
|
const row = buffer.shift()
|
|
344
482
|
if (row) {
|
|
483
|
+
let event: Event
|
|
484
|
+
try {
|
|
485
|
+
event = isSerializedEvent(row)
|
|
486
|
+
? parseSerializedEvent(row)
|
|
487
|
+
: row
|
|
488
|
+
} catch (cause) {
|
|
489
|
+
// A retry re-reads from the last delivered event.
|
|
490
|
+
buffer = []
|
|
491
|
+
feed.dirty = true
|
|
492
|
+
release()
|
|
493
|
+
throw new A2Error(
|
|
494
|
+
'STORE_UNAVAILABLE',
|
|
495
|
+
'redis stream read failed',
|
|
496
|
+
{ cause },
|
|
497
|
+
)
|
|
498
|
+
}
|
|
345
499
|
last = row.index
|
|
346
|
-
return { value:
|
|
500
|
+
return { value: event, done: false }
|
|
347
501
|
}
|
|
348
502
|
if (severed) {
|
|
349
503
|
release()
|
|
350
504
|
throw new A2Error('STORE_UNAVAILABLE', 'the store was closed')
|
|
351
505
|
}
|
|
352
|
-
const waker = armWaker(channel)
|
|
353
|
-
interrupt = waker.wake
|
|
354
506
|
try {
|
|
355
507
|
if (!subscribed) {
|
|
356
|
-
//
|
|
357
|
-
// after
|
|
358
|
-
//
|
|
508
|
+
// Register and subscribe before the first read:
|
|
509
|
+
// anything appended after it arrives in the inbox,
|
|
510
|
+
// so nothing slips between catch-up and park.
|
|
359
511
|
subscribed = true
|
|
512
|
+
let set = feeds.get(channel)
|
|
513
|
+
if (!set) {
|
|
514
|
+
set = new Set()
|
|
515
|
+
feeds.set(channel, set)
|
|
516
|
+
}
|
|
517
|
+
set.add(feed)
|
|
360
518
|
// oxlint-disable-next-line no-await-in-loop -- one-time setup
|
|
361
519
|
await acquireChannel(channel)
|
|
362
520
|
if (closed || severed) continue
|
|
@@ -385,37 +543,43 @@ export function createRedisNotifyStore(options: {
|
|
|
385
543
|
)
|
|
386
544
|
return result
|
|
387
545
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
!marker ||
|
|
395
|
-
Date.now() - marker.at >= NOTIFY_TIMINGS.safetyReadMs
|
|
396
|
-
) {
|
|
397
|
-
marker = {
|
|
398
|
-
at: Date.now(),
|
|
399
|
-
ready: core.markWatched(
|
|
400
|
-
sessionId,
|
|
401
|
-
NOTIFY_TIMINGS.safetyReadMs * 3,
|
|
402
|
-
),
|
|
403
|
-
}
|
|
404
|
-
watched.set(channel, marker)
|
|
546
|
+
if (catchup) {
|
|
547
|
+
// oxlint-disable-next-line no-await-in-loop -- decode the next catch-up batch on demand
|
|
548
|
+
const result = await nextRead(last)
|
|
549
|
+
if (closed || severed) continue
|
|
550
|
+
if (!result.done) buffer = result.value
|
|
551
|
+
continue
|
|
405
552
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
553
|
+
applyInbox()
|
|
554
|
+
if (buffer.length > 0) continue
|
|
555
|
+
if (feed.dirty) {
|
|
556
|
+
feed.dirty = false
|
|
557
|
+
if (core.streamEvents) {
|
|
558
|
+
// oxlint-disable-next-line no-await-in-loop -- begin an incremental catch-up
|
|
559
|
+
const result = await nextRead(last)
|
|
560
|
+
if (closed || severed) continue
|
|
561
|
+
if (!result.done) buffer = result.value
|
|
562
|
+
continue
|
|
563
|
+
}
|
|
414
564
|
// oxlint-disable-next-line no-await-in-loop -- live-feed loop
|
|
415
|
-
await
|
|
416
|
-
|
|
417
|
-
|
|
565
|
+
buffer = await core.readFeedPage(sessionId, last)
|
|
566
|
+
if (buffer.length >= FEED_PAGE_SIZE) feed.dirty = true
|
|
567
|
+
continue
|
|
418
568
|
}
|
|
569
|
+
// Caught up: park until a message, a reconnect, or the
|
|
570
|
+
// safety read that heals a lost message.
|
|
571
|
+
const woken = new Promise<'wake'>((resolve) => {
|
|
572
|
+
feed.wake = () => resolve('wake')
|
|
573
|
+
})
|
|
574
|
+
pending = defaultSleep(NOTIFY_TIMINGS.safetyReadMs)
|
|
575
|
+
// oxlint-disable-next-line no-await-in-loop -- live-feed loop
|
|
576
|
+
const outcome = await Promise.race([
|
|
577
|
+
woken,
|
|
578
|
+
pending.promise.then(() => 'timer' as const),
|
|
579
|
+
])
|
|
580
|
+
pending.cancel()
|
|
581
|
+
pending = null
|
|
582
|
+
if (outcome === 'timer') feed.dirty = true
|
|
419
583
|
} catch (err) {
|
|
420
584
|
release()
|
|
421
585
|
if (closed) return { value: undefined, done: true }
|
|
@@ -426,20 +590,26 @@ export function createRedisNotifyStore(options: {
|
|
|
426
590
|
{ cause: err },
|
|
427
591
|
)
|
|
428
592
|
} finally {
|
|
429
|
-
|
|
430
|
-
interrupt = null
|
|
593
|
+
feed.wake = null
|
|
431
594
|
}
|
|
432
595
|
}
|
|
433
596
|
},
|
|
434
597
|
async return(): Promise<IteratorResult<Event>> {
|
|
435
598
|
closed = true
|
|
436
599
|
pending?.cancel()
|
|
437
|
-
|
|
600
|
+
feed.wake?.() // wakes an in-flight next() immediately
|
|
438
601
|
release()
|
|
602
|
+
await stopRead()
|
|
439
603
|
return { value: undefined, done: true }
|
|
440
604
|
},
|
|
441
605
|
}
|
|
442
606
|
streamActivities.set(iterator, activity)
|
|
607
|
+
bufferedEvents.set(iterator, () => {
|
|
608
|
+
if (closed) return undefined
|
|
609
|
+
const row = buffer.shift()
|
|
610
|
+
if (row) last = row.index
|
|
611
|
+
return row
|
|
612
|
+
})
|
|
443
613
|
return iterator
|
|
444
614
|
},
|
|
445
615
|
}
|
|
@@ -452,9 +622,14 @@ export function createRedisNotifyStore(options: {
|
|
|
452
622
|
presenceSubscriptions.clear()
|
|
453
623
|
for (const poller of pollingStreams) void poller.return?.()
|
|
454
624
|
pollingStreams.clear()
|
|
455
|
-
|
|
456
|
-
|
|
625
|
+
const reads: Promise<void>[] = []
|
|
626
|
+
for (const set of feeds.values()) {
|
|
627
|
+
for (const feed of set) {
|
|
628
|
+
feed.wake?.()
|
|
629
|
+
reads.push(feed.stopRead())
|
|
630
|
+
}
|
|
457
631
|
}
|
|
632
|
+
await Promise.allSettled(reads)
|
|
458
633
|
if (subscriber) {
|
|
459
634
|
const sub = await subscriber.catch(() => null)
|
|
460
635
|
sub?.disconnect()
|
|
@@ -462,4 +637,6 @@ export function createRedisNotifyStore(options: {
|
|
|
462
637
|
await options.closeClient()
|
|
463
638
|
},
|
|
464
639
|
}
|
|
640
|
+
stateTailReaders.set(store, core.readTail)
|
|
641
|
+
return store
|
|
465
642
|
}
|
package/src/store-redis.ts
CHANGED
|
@@ -4,15 +4,14 @@
|
|
|
4
4
|
* with experimental-a2/store-redis-http); this module owns the connection and
|
|
5
5
|
* the live feed.
|
|
6
6
|
*
|
|
7
|
-
* `stream()` is notify-driven:
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* one subscriber), not with concurrent viewers.
|
|
7
|
+
* `stream()` is notify-driven: every write publishes a wake-up that
|
|
8
|
+
* carries the appended events, one shared subscriber connection per backend
|
|
9
|
+
* serves every local feed, and a caught-up feed applies carried events that
|
|
10
|
+
* continue its sequence without reading. A gap, an oversized batch, a
|
|
11
|
+
* reconnect, or the safety re-read (NOTIFY_TIMINGS) triggers an `XRANGE`
|
|
12
|
+
* catch-up, so a lost wake-up costs latency, never delivery. Connections
|
|
13
|
+
* scale with processes (one command client plus one subscriber), not with
|
|
14
|
+
* concurrent viewers.
|
|
16
15
|
*
|
|
17
16
|
* Works with any Redis-protocol server on a single instance or a
|
|
18
17
|
* non-cluster provider (Upstash — durable by default — Redis, Valkey).
|
|
@@ -27,6 +26,7 @@ import {
|
|
|
27
26
|
type RedisConnection,
|
|
28
27
|
} from './store-redis-notify.ts'
|
|
29
28
|
import { retryableLazy } from './retryable-lazy.ts'
|
|
29
|
+
import type { RedisCall } from './store-redis-core.ts'
|
|
30
30
|
import {
|
|
31
31
|
RANDOM_IDS,
|
|
32
32
|
SYSTEM_CLOCK,
|
|
@@ -89,6 +89,16 @@ export function redis(options: RedisStoreOptions = {}): RedisStore {
|
|
|
89
89
|
|
|
90
90
|
return createRedisNotifyStore({
|
|
91
91
|
client,
|
|
92
|
+
// Only a client created from `url` is known to be ioredis, whose
|
|
93
|
+
// `callBuffer` returns bulk replies as raw bytes.
|
|
94
|
+
...(options.client
|
|
95
|
+
? {}
|
|
96
|
+
: {
|
|
97
|
+
callBytes: async (command, ...args) =>
|
|
98
|
+
(
|
|
99
|
+
(await client()) as RedisConnection & { callBuffer: RedisCall }
|
|
100
|
+
).callBuffer(command, ...args),
|
|
101
|
+
}),
|
|
92
102
|
clock,
|
|
93
103
|
ids,
|
|
94
104
|
keyPrefix: prefix,
|