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/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,8 @@ 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!,
|
|
246
295
|
|
|
247
296
|
presence: {
|
|
248
297
|
async set(ns, participant, values, meta) {
|
|
@@ -311,18 +360,68 @@ export function createRedisNotifyStore(options: {
|
|
|
311
360
|
return {
|
|
312
361
|
[Symbol.asyncIterator](): AsyncIterator<Event> {
|
|
313
362
|
let last = startAfter
|
|
314
|
-
let buffer:
|
|
363
|
+
let buffer: HeldEvent[] = []
|
|
315
364
|
let closed = false
|
|
316
365
|
let subscribed = false
|
|
317
366
|
let pending: Sleeper | null = null
|
|
318
|
-
let interrupt: (() => void) | null = null
|
|
319
367
|
let poller: AsyncIterator<Event> | undefined
|
|
320
368
|
let stopActivity: (() => void) | undefined
|
|
369
|
+
let catchup:
|
|
370
|
+
| {
|
|
371
|
+
controller: AbortController
|
|
372
|
+
iterator: AsyncGenerator<SerializedEvent[], void>
|
|
373
|
+
delivered: boolean
|
|
374
|
+
}
|
|
375
|
+
| undefined
|
|
376
|
+
let stoppingRead: Promise<void> | undefined
|
|
377
|
+
const stopRead = (): Promise<void> => {
|
|
378
|
+
const reading = catchup
|
|
379
|
+
catchup = undefined
|
|
380
|
+
if (reading) {
|
|
381
|
+
reading.controller.abort()
|
|
382
|
+
stoppingRead = reading.iterator
|
|
383
|
+
.return(undefined)
|
|
384
|
+
.then(() => undefined)
|
|
385
|
+
.catch(() => {})
|
|
386
|
+
}
|
|
387
|
+
return stoppingRead ?? Promise.resolve()
|
|
388
|
+
}
|
|
389
|
+
const nextRead = async (
|
|
390
|
+
after: number,
|
|
391
|
+
): Promise<IteratorResult<SerializedEvent[], void>> => {
|
|
392
|
+
if (!catchup) {
|
|
393
|
+
const controller = new AbortController()
|
|
394
|
+
catchup = {
|
|
395
|
+
controller,
|
|
396
|
+
delivered: false,
|
|
397
|
+
iterator: core.streamEvents!({
|
|
398
|
+
sessionId,
|
|
399
|
+
afterIndex: after,
|
|
400
|
+
signal: controller.signal,
|
|
401
|
+
})[Symbol.asyncIterator](),
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const reading = catchup
|
|
405
|
+
const result = await reading.iterator.next()
|
|
406
|
+
if (result.done && catchup === reading) {
|
|
407
|
+
catchup = undefined
|
|
408
|
+
if (reading.delivered) feed.dirty = true
|
|
409
|
+
} else if (!result.done) reading.delivered = true
|
|
410
|
+
return result
|
|
411
|
+
}
|
|
412
|
+
const feed: Feed = {
|
|
413
|
+
inbox: [],
|
|
414
|
+
bytes: 0,
|
|
415
|
+
dirty: true,
|
|
416
|
+
wake: null,
|
|
417
|
+
stopRead,
|
|
418
|
+
}
|
|
321
419
|
const activity = createStreamActivity(() => {
|
|
322
420
|
if (poller) streamActivities.get(poller)?.activate()
|
|
323
421
|
})
|
|
324
422
|
|
|
325
423
|
const release = (): void => {
|
|
424
|
+
void stopRead()
|
|
326
425
|
stopActivity?.()
|
|
327
426
|
if (poller) {
|
|
328
427
|
void poller.return?.()
|
|
@@ -330,9 +429,46 @@ export function createRedisNotifyStore(options: {
|
|
|
330
429
|
}
|
|
331
430
|
if (!subscribed) return
|
|
332
431
|
subscribed = false
|
|
432
|
+
const set = feeds.get(channel)
|
|
433
|
+
set?.delete(feed)
|
|
434
|
+
if (set?.size === 0) feeds.delete(channel)
|
|
333
435
|
releaseChannel(channel)
|
|
334
436
|
}
|
|
335
437
|
|
|
438
|
+
/**
|
|
439
|
+
* Moves carried events that continue this feed's sequence into
|
|
440
|
+
* the buffer. A gap, a message without events, or an unreadable
|
|
441
|
+
* message marks the feed dirty so it catches up with XRANGE.
|
|
442
|
+
*/
|
|
443
|
+
const applyInbox = (): void => {
|
|
444
|
+
feed.bytes = 0
|
|
445
|
+
for (const message of feed.inbox.splice(0)) {
|
|
446
|
+
const wake = parseWake(message)
|
|
447
|
+
if (!wake) {
|
|
448
|
+
feed.dirty = true
|
|
449
|
+
continue
|
|
450
|
+
}
|
|
451
|
+
for (const [index, id, type, payload] of wake.entries) {
|
|
452
|
+
const expected = (buffer.at(-1)?.index ?? last) + 1
|
|
453
|
+
if (index < expected) continue
|
|
454
|
+
if (index > expected) {
|
|
455
|
+
feed.dirty = true
|
|
456
|
+
break
|
|
457
|
+
}
|
|
458
|
+
buffer.push({
|
|
459
|
+
id,
|
|
460
|
+
type,
|
|
461
|
+
payload,
|
|
462
|
+
index,
|
|
463
|
+
sessionId,
|
|
464
|
+
createdAt: new Date(wake.createdAt),
|
|
465
|
+
})
|
|
466
|
+
}
|
|
467
|
+
if (wake.frontier > (buffer.at(-1)?.index ?? last))
|
|
468
|
+
feed.dirty = true
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
336
472
|
const iterator: AsyncIterator<Event> = {
|
|
337
473
|
async next(): Promise<IteratorResult<Event>> {
|
|
338
474
|
for (;;) {
|
|
@@ -342,21 +478,41 @@ export function createRedisNotifyStore(options: {
|
|
|
342
478
|
}
|
|
343
479
|
const row = buffer.shift()
|
|
344
480
|
if (row) {
|
|
481
|
+
let event: Event
|
|
482
|
+
try {
|
|
483
|
+
event = isSerializedEvent(row)
|
|
484
|
+
? parseSerializedEvent(row)
|
|
485
|
+
: row
|
|
486
|
+
} catch (cause) {
|
|
487
|
+
// A retry re-reads from the last delivered event.
|
|
488
|
+
buffer = []
|
|
489
|
+
feed.dirty = true
|
|
490
|
+
release()
|
|
491
|
+
throw new A2Error(
|
|
492
|
+
'STORE_UNAVAILABLE',
|
|
493
|
+
'redis stream read failed',
|
|
494
|
+
{ cause },
|
|
495
|
+
)
|
|
496
|
+
}
|
|
345
497
|
last = row.index
|
|
346
|
-
return { value:
|
|
498
|
+
return { value: event, done: false }
|
|
347
499
|
}
|
|
348
500
|
if (severed) {
|
|
349
501
|
release()
|
|
350
502
|
throw new A2Error('STORE_UNAVAILABLE', 'the store was closed')
|
|
351
503
|
}
|
|
352
|
-
const waker = armWaker(channel)
|
|
353
|
-
interrupt = waker.wake
|
|
354
504
|
try {
|
|
355
505
|
if (!subscribed) {
|
|
356
|
-
//
|
|
357
|
-
// after
|
|
358
|
-
//
|
|
506
|
+
// Register and subscribe before the first read:
|
|
507
|
+
// anything appended after it arrives in the inbox,
|
|
508
|
+
// so nothing slips between catch-up and park.
|
|
359
509
|
subscribed = true
|
|
510
|
+
let set = feeds.get(channel)
|
|
511
|
+
if (!set) {
|
|
512
|
+
set = new Set()
|
|
513
|
+
feeds.set(channel, set)
|
|
514
|
+
}
|
|
515
|
+
set.add(feed)
|
|
360
516
|
// oxlint-disable-next-line no-await-in-loop -- one-time setup
|
|
361
517
|
await acquireChannel(channel)
|
|
362
518
|
if (closed || severed) continue
|
|
@@ -385,37 +541,43 @@ export function createRedisNotifyStore(options: {
|
|
|
385
541
|
)
|
|
386
542
|
return result
|
|
387
543
|
}
|
|
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)
|
|
544
|
+
if (catchup) {
|
|
545
|
+
// oxlint-disable-next-line no-await-in-loop -- decode the next catch-up batch on demand
|
|
546
|
+
const result = await nextRead(last)
|
|
547
|
+
if (closed || severed) continue
|
|
548
|
+
if (!result.done) buffer = result.value
|
|
549
|
+
continue
|
|
405
550
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
551
|
+
applyInbox()
|
|
552
|
+
if (buffer.length > 0) continue
|
|
553
|
+
if (feed.dirty) {
|
|
554
|
+
feed.dirty = false
|
|
555
|
+
if (core.streamEvents) {
|
|
556
|
+
// oxlint-disable-next-line no-await-in-loop -- begin an incremental catch-up
|
|
557
|
+
const result = await nextRead(last)
|
|
558
|
+
if (closed || severed) continue
|
|
559
|
+
if (!result.done) buffer = result.value
|
|
560
|
+
continue
|
|
561
|
+
}
|
|
414
562
|
// oxlint-disable-next-line no-await-in-loop -- live-feed loop
|
|
415
|
-
await
|
|
416
|
-
|
|
417
|
-
|
|
563
|
+
buffer = await core.readFeedPage(sessionId, last)
|
|
564
|
+
if (buffer.length >= FEED_PAGE_SIZE) feed.dirty = true
|
|
565
|
+
continue
|
|
418
566
|
}
|
|
567
|
+
// Caught up: park until a message, a reconnect, or the
|
|
568
|
+
// safety read that heals a lost message.
|
|
569
|
+
const woken = new Promise<'wake'>((resolve) => {
|
|
570
|
+
feed.wake = () => resolve('wake')
|
|
571
|
+
})
|
|
572
|
+
pending = defaultSleep(NOTIFY_TIMINGS.safetyReadMs)
|
|
573
|
+
// oxlint-disable-next-line no-await-in-loop -- live-feed loop
|
|
574
|
+
const outcome = await Promise.race([
|
|
575
|
+
woken,
|
|
576
|
+
pending.promise.then(() => 'timer' as const),
|
|
577
|
+
])
|
|
578
|
+
pending.cancel()
|
|
579
|
+
pending = null
|
|
580
|
+
if (outcome === 'timer') feed.dirty = true
|
|
419
581
|
} catch (err) {
|
|
420
582
|
release()
|
|
421
583
|
if (closed) return { value: undefined, done: true }
|
|
@@ -426,20 +588,26 @@ export function createRedisNotifyStore(options: {
|
|
|
426
588
|
{ cause: err },
|
|
427
589
|
)
|
|
428
590
|
} finally {
|
|
429
|
-
|
|
430
|
-
interrupt = null
|
|
591
|
+
feed.wake = null
|
|
431
592
|
}
|
|
432
593
|
}
|
|
433
594
|
},
|
|
434
595
|
async return(): Promise<IteratorResult<Event>> {
|
|
435
596
|
closed = true
|
|
436
597
|
pending?.cancel()
|
|
437
|
-
|
|
598
|
+
feed.wake?.() // wakes an in-flight next() immediately
|
|
438
599
|
release()
|
|
600
|
+
await stopRead()
|
|
439
601
|
return { value: undefined, done: true }
|
|
440
602
|
},
|
|
441
603
|
}
|
|
442
604
|
streamActivities.set(iterator, activity)
|
|
605
|
+
bufferedEvents.set(iterator, () => {
|
|
606
|
+
if (closed) return undefined
|
|
607
|
+
const row = buffer.shift()
|
|
608
|
+
if (row) last = row.index
|
|
609
|
+
return row
|
|
610
|
+
})
|
|
443
611
|
return iterator
|
|
444
612
|
},
|
|
445
613
|
}
|
|
@@ -452,9 +620,14 @@ export function createRedisNotifyStore(options: {
|
|
|
452
620
|
presenceSubscriptions.clear()
|
|
453
621
|
for (const poller of pollingStreams) void poller.return?.()
|
|
454
622
|
pollingStreams.clear()
|
|
455
|
-
|
|
456
|
-
|
|
623
|
+
const reads: Promise<void>[] = []
|
|
624
|
+
for (const set of feeds.values()) {
|
|
625
|
+
for (const feed of set) {
|
|
626
|
+
feed.wake?.()
|
|
627
|
+
reads.push(feed.stopRead())
|
|
628
|
+
}
|
|
457
629
|
}
|
|
630
|
+
await Promise.allSettled(reads)
|
|
458
631
|
if (subscriber) {
|
|
459
632
|
const sub = await subscriber.catch(() => null)
|
|
460
633
|
sub?.disconnect()
|
|
@@ -462,4 +635,6 @@ export function createRedisNotifyStore(options: {
|
|
|
462
635
|
await options.closeClient()
|
|
463
636
|
},
|
|
464
637
|
}
|
|
638
|
+
stateTailReaders.set(store, core.readTail)
|
|
639
|
+
return store
|
|
465
640
|
}
|
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,
|