experimental-a2 0.6.0 → 0.8.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 +41 -0
- package/dist/ai-server.d.ts +4 -5
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +20 -17
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +334 -2
- package/dist/ai.d.ts.map +1 -0
- package/dist/ai.js +1 -1
- package/dist/client.d.ts +202 -2
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +1025 -1
- package/dist/client.js.map +1 -0
- package/dist/errors-BQuJpe82.js.map +1 -1
- package/dist/index.d.ts +22 -3
- package/dist/index.d.ts.map +1 -0
- package/dist/{internal-DstsI6Re.js → internal-DRXJ56EI.js} +5 -28
- package/dist/internal-DRXJ56EI.js.map +1 -0
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/scheduler-qstash.d.ts +3 -3
- package/dist/scheduler-qstash.js +4 -5
- package/dist/scheduler-qstash.js.map +1 -1
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.js +4 -4
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/{server-Duw6MVlB.js → server-B2XNevQA.js} +830 -131
- package/dist/server-B2XNevQA.js.map +1 -0
- package/dist/{server-DpvjhdoE.d.ts → server-DjPhHnbI.d.ts} +71 -50
- package/dist/server-DjPhHnbI.d.ts.map +1 -0
- package/dist/server.d.ts +3 -3
- package/dist/server.js +1 -1
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/{store-DysUkTH3.d.ts → store-RJO35BMj.d.ts} +24 -62
- package/dist/store-RJO35BMj.d.ts.map +1 -0
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.d.ts.map +1 -1
- package/dist/store-memory.js +80 -78
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-dSeLxzfb.js → store-polling-6DW7F1DT.js} +2 -2
- package/dist/{store-polling-dSeLxzfb.js.map → store-polling-6DW7F1DT.js.map} +1 -1
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.d.ts.map +1 -1
- package/dist/store-postgres.js +231 -182
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-redis-core-BFLwz0Wj.js → store-redis-core-DT01r4GZ.js} +213 -161
- package/dist/store-redis-core-DT01r4GZ.js.map +1 -0
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +3 -4
- package/dist/store-redis-http.js.map +1 -1
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.js +4 -5
- 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 +104 -91
- package/dist/store-sqlite.js.map +1 -1
- package/dist/{wire-BFQmSJ-9.js → wire-B6te_wns.js} +4 -3
- package/dist/wire-B6te_wns.js.map +1 -0
- package/docs/concepts/02-handlers.mdx +4 -0
- package/docs/concepts/04-state.mdx +57 -9
- package/docs/guides/03-react.mdx +20 -28
- package/docs/guides/05-production.mdx +9 -11
- package/docs/guides/06-ai-agents.mdx +10 -15
- package/docs/guides/09-presence.mdx +14 -19
- package/docs/guides/10-transports.mdx +104 -86
- package/docs/reference/01-api.mdx +182 -293
- package/docs/reference/02-errors.mdx +5 -7
- package/package.json +1 -14
- package/src/ai-server.ts +36 -15
- package/src/client.ts +2 -2
- package/src/errors.ts +1 -0
- package/src/internal.ts +3 -62
- package/src/push-envelope.ts +24 -21
- package/src/scheduler-qstash.ts +3 -3
- package/src/scheduler-vercel.ts +2 -2
- package/src/server-fetch.ts +344 -0
- package/src/server.ts +315 -312
- package/src/session-socket.ts +36 -20
- package/src/sse.ts +2 -2
- package/src/store-memory.ts +138 -101
- package/src/store-postgres.ts +355 -238
- package/src/store-redis-core.ts +247 -237
- package/src/store-redis-http.ts +1 -2
- package/src/store-redis.ts +1 -2
- package/src/store-sqlite.ts +191 -153
- package/src/store.ts +24 -66
- package/src/wire.ts +2 -1
- package/dist/ai-D_PGS-JR.d.ts +0 -334
- package/dist/ai-D_PGS-JR.d.ts.map +0 -1
- package/dist/cli-B3VuxoDe.js +0 -599
- package/dist/cli-B3VuxoDe.js.map +0 -1
- package/dist/cli-bin.d.ts +0 -1
- package/dist/cli-bin.js +0 -7
- package/dist/cli-bin.js.map +0 -1
- package/dist/cli.d.ts +0 -20
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -2
- package/dist/client-CdMqi7mC.d.ts +0 -202
- package/dist/client-CdMqi7mC.d.ts.map +0 -1
- package/dist/client-Dj5d3SP_.js +0 -1026
- package/dist/client-Dj5d3SP_.js.map +0 -1
- package/dist/devtools-J_jZ2vQf.d.ts +0 -152
- package/dist/devtools-J_jZ2vQf.d.ts.map +0 -1
- package/dist/devtools-kJJaORn-.js +0 -340
- package/dist/devtools-kJJaORn-.js.map +0 -1
- package/dist/devtools-server.browser.d.ts +0 -1
- package/dist/devtools-server.browser.js +0 -6
- package/dist/devtools-server.browser.js.map +0 -1
- package/dist/devtools-server.d.ts +0 -23
- package/dist/devtools-server.d.ts.map +0 -1
- package/dist/devtools-server.js +0 -1270
- package/dist/devtools-server.js.map +0 -1
- package/dist/devtools.d.ts +0 -2
- package/dist/devtools.js +0 -2
- package/dist/errors-W6nwJ-fm.d.ts +0 -21
- package/dist/errors-W6nwJ-fm.d.ts.map +0 -1
- package/dist/http.d.ts +0 -151
- package/dist/http.d.ts.map +0 -1
- package/dist/http.js +0 -706
- package/dist/http.js.map +0 -1
- package/dist/inspection-DaxB5jM2.js +0 -13
- package/dist/inspection-DaxB5jM2.js.map +0 -1
- package/dist/internal-DstsI6Re.js.map +0 -1
- package/dist/platform-B4TnJtWu.js +0 -34
- package/dist/platform-B4TnJtWu.js.map +0 -1
- package/dist/server-DpvjhdoE.d.ts.map +0 -1
- package/dist/server-Duw6MVlB.js.map +0 -1
- package/dist/store-DysUkTH3.d.ts.map +0 -1
- package/dist/store-redis-core-BFLwz0Wj.js.map +0 -1
- package/dist/testing.browser.d.ts +0 -1
- package/dist/testing.browser.js +0 -6
- package/dist/testing.browser.js.map +0 -1
- package/dist/testing.d.ts +0 -32
- package/dist/testing.d.ts.map +0 -1
- package/dist/testing.js +0 -103
- package/dist/testing.js.map +0 -1
- package/dist/wire-BFQmSJ-9.js.map +0 -1
- package/docs/guides/07-devtools.mdx +0 -229
- package/src/cli-bin.ts +0 -5
- package/src/cli.ts +0 -1046
- package/src/devtools-app.ts +0 -989
- package/src/devtools-server.browser.ts +0 -5
- package/src/devtools-server.ts +0 -604
- package/src/devtools.ts +0 -716
- package/src/http.ts +0 -394
- package/src/inspection.ts +0 -39
- package/src/testing.browser.ts +0 -5
- package/src/testing.ts +0 -185
package/src/store-redis-core.ts
CHANGED
|
@@ -23,7 +23,6 @@ import { nullProtoRecord } from './internal.ts'
|
|
|
23
23
|
import { decodeReturnedEventIds } from './store-codec.ts'
|
|
24
24
|
import type {
|
|
25
25
|
A2Store,
|
|
26
|
-
A2StoreInspection,
|
|
27
26
|
Clock,
|
|
28
27
|
CompleteAttemptResult,
|
|
29
28
|
Event,
|
|
@@ -56,11 +55,7 @@ export type RedisStoreCoreOptions = {
|
|
|
56
55
|
notify?: (sessionId: string, count: number) => void
|
|
57
56
|
}
|
|
58
57
|
|
|
59
|
-
export type RedisStoreCore = Omit<
|
|
60
|
-
A2Store,
|
|
61
|
-
'stream' | 'inspect' | 'presence'
|
|
62
|
-
> & {
|
|
63
|
-
inspect: A2StoreInspection
|
|
58
|
+
export type RedisStoreCore = Omit<A2Store, 'stream' | 'presence'> & {
|
|
64
59
|
/**
|
|
65
60
|
* The storage half of the presence capability, shared by both
|
|
66
61
|
* transports. `set` returns the applied patch — null when every
|
|
@@ -98,8 +93,8 @@ const READ_PAGE_SIZE = 256
|
|
|
98
93
|
// ── Lua scripts (each runs atomically) ───────────────────────────────
|
|
99
94
|
|
|
100
95
|
/**
|
|
101
|
-
* KEYS: store stream, counter, global id hash,
|
|
102
|
-
*
|
|
96
|
+
* KEYS: store stream, counter, global id hash, metadata, pending, ready,
|
|
97
|
+
* watched.
|
|
103
98
|
* ARGV: sessionId, nowMs, n, then per event:
|
|
104
99
|
* id, type, payloadJson, causeJson, lane, settled.
|
|
105
100
|
* Returns ['empty', hasPending] | ['ok', firstIndex, hasPending, watched] |
|
|
@@ -111,24 +106,24 @@ local function lane_field(lane, suffix)
|
|
|
111
106
|
return '__lane:' .. tostring(string.len(lane)) .. ':' .. lane .. ':' .. suffix
|
|
112
107
|
end
|
|
113
108
|
local function enqueue(field, idx, lane)
|
|
114
|
-
redis.call('ZADD', KEYS[
|
|
109
|
+
redis.call('ZADD', KEYS[5], idx, field)
|
|
115
110
|
if lane == '' then
|
|
116
|
-
redis.call('ZADD', KEYS[
|
|
111
|
+
redis.call('ZADD', KEYS[6], idx, field)
|
|
117
112
|
return
|
|
118
113
|
end
|
|
119
114
|
local tail_key = lane_field(lane, 'tail')
|
|
120
|
-
local tail = redis.call('HGET', KEYS[
|
|
115
|
+
local tail = redis.call('HGET', KEYS[4], tail_key)
|
|
121
116
|
if tail then
|
|
122
|
-
redis.call('HSET', KEYS[
|
|
117
|
+
redis.call('HSET', KEYS[4], tail .. ':next', field)
|
|
123
118
|
else
|
|
124
|
-
redis.call('HSET', KEYS[
|
|
125
|
-
redis.call('ZADD', KEYS[
|
|
119
|
+
redis.call('HSET', KEYS[4], lane_field(lane, 'head'), field)
|
|
120
|
+
redis.call('ZADD', KEYS[6], idx, field)
|
|
126
121
|
end
|
|
127
|
-
redis.call('HSET', KEYS[
|
|
122
|
+
redis.call('HSET', KEYS[4], tail_key, field)
|
|
128
123
|
end
|
|
129
124
|
local n = tonumber(ARGV[3])
|
|
130
125
|
local base = tonumber(redis.call('GET', KEYS[2]) or '0')
|
|
131
|
-
local has_pending = redis.call('ZCARD', KEYS[
|
|
126
|
+
local has_pending = redis.call('ZCARD', KEYS[5]) > 0 and '1' or '0'
|
|
132
127
|
if n == 0 then return { 'empty', has_pending } end
|
|
133
128
|
local dups = {}
|
|
134
129
|
for i = 1, n do
|
|
@@ -155,9 +150,9 @@ for i = 1, n do
|
|
|
155
150
|
return { 'no_parent', tostring(parsed.index) }
|
|
156
151
|
end
|
|
157
152
|
local field = tostring(pidx)
|
|
158
|
-
local dispatches = tonumber(redis.call('HGET', KEYS[
|
|
153
|
+
local dispatches = tonumber(redis.call('HGET', KEYS[4], field .. ':d') or '0')
|
|
159
154
|
if dispatches ~= tonumber(parsed.attempt) or
|
|
160
|
-
redis.call('HEXISTS', KEYS[
|
|
155
|
+
redis.call('HEXISTS', KEYS[4], field .. ':f') == 1 then
|
|
161
156
|
return { 'superseded', field }
|
|
162
157
|
end
|
|
163
158
|
end
|
|
@@ -174,22 +169,37 @@ for i = 1, n do
|
|
|
174
169
|
'id', id, 'type', typ, 'payload', payload, 'created_at', ARGV[2],
|
|
175
170
|
'cause', cause, 'lane', lane)
|
|
176
171
|
redis.call('HSET', KEYS[3], id, cjson.encode({ ARGV[1], idx }))
|
|
177
|
-
redis.call('HSET', KEYS[
|
|
172
|
+
redis.call('HSET', KEYS[4], tostring(idx) .. ':l', lane)
|
|
178
173
|
if settled == '1' then
|
|
179
|
-
redis.call('HSET', KEYS[
|
|
174
|
+
redis.call('HSET', KEYS[4], tostring(idx) .. ':p', ARGV[2])
|
|
180
175
|
else
|
|
181
176
|
enqueue(tostring(idx), idx, lane)
|
|
182
177
|
end
|
|
183
178
|
end
|
|
184
179
|
redis.call('SET', KEYS[2], tostring(base + n))
|
|
185
|
-
redis.call('
|
|
186
|
-
has_pending = redis.call('ZCARD', KEYS[6]) > 0 and '1' or '0'
|
|
180
|
+
has_pending = redis.call('ZCARD', KEYS[5]) > 0 and '1' or '0'
|
|
187
181
|
return { 'ok', tostring(base + 1), has_pending,
|
|
188
|
-
tostring(redis.call('EXISTS', KEYS[
|
|
182
|
+
tostring(redis.call('EXISTS', KEYS[7])) }
|
|
189
183
|
`
|
|
190
184
|
|
|
191
|
-
/**
|
|
185
|
+
/**
|
|
186
|
+
* KEYS: counter, meta, pending, ready, event snapshot pins, snapshot pin counts.
|
|
187
|
+
* ARGV: idx, attempt, error, maxFailures, nowMs.
|
|
188
|
+
*/
|
|
192
189
|
const FAIL_ATTEMPT_LUA = `
|
|
190
|
+
local function release_snapshot_pins()
|
|
191
|
+
local refs = redis.call('SMEMBERS', KEYS[5])
|
|
192
|
+
for _, ref in ipairs(refs) do
|
|
193
|
+
local count = redis.call('HINCRBY', KEYS[6], ref, -1)
|
|
194
|
+
if count <= 0 then
|
|
195
|
+
redis.call('HDEL', KEYS[6], ref)
|
|
196
|
+
local checkpoint = cjson.decode(ref)
|
|
197
|
+
redis.call('ZREMRANGEBYSCORE', checkpoint[1], checkpoint[2], checkpoint[2])
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
redis.call('DEL', KEYS[5])
|
|
201
|
+
if redis.call('HLEN', KEYS[6]) == 0 then redis.call('DEL', KEYS[6]) end
|
|
202
|
+
end
|
|
193
203
|
local function lane_field(lane, suffix)
|
|
194
204
|
return '__lane:' .. tostring(string.len(lane)) .. ':' .. lane .. ':' .. suffix
|
|
195
205
|
end
|
|
@@ -243,6 +253,7 @@ redis.call('HDEL', KEYS[2], field .. ':ch', field .. ':ce')
|
|
|
243
253
|
if failures >= tonumber(ARGV[4]) then
|
|
244
254
|
redis.call('HSET', KEYS[2], field .. ':f', ARGV[5])
|
|
245
255
|
redis.call('ZREM', KEYS[4], field)
|
|
256
|
+
release_snapshot_pins()
|
|
246
257
|
return { 'dead_lettered', tostring(failures) }
|
|
247
258
|
end
|
|
248
259
|
return { 'failed', tostring(failures) }
|
|
@@ -336,61 +347,75 @@ return result
|
|
|
336
347
|
`
|
|
337
348
|
|
|
338
349
|
/**
|
|
339
|
-
* KEYS: store, counter, ids,
|
|
350
|
+
* KEYS: store, counter, ids, meta, pending, ready, watched,
|
|
351
|
+
* event snapshot pins, snapshot pin counts.
|
|
340
352
|
* ARGV: sessionId, nowMs, parent index, attempt, n, then per child:
|
|
341
353
|
* id, type, payloadJson, lane, settled.
|
|
342
354
|
*/
|
|
343
355
|
const COMPLETE_ATTEMPT_LUA = `
|
|
356
|
+
local function release_snapshot_pins()
|
|
357
|
+
local refs = redis.call('SMEMBERS', KEYS[8])
|
|
358
|
+
for _, ref in ipairs(refs) do
|
|
359
|
+
local count = redis.call('HINCRBY', KEYS[9], ref, -1)
|
|
360
|
+
if count <= 0 then
|
|
361
|
+
redis.call('HDEL', KEYS[9], ref)
|
|
362
|
+
local checkpoint = cjson.decode(ref)
|
|
363
|
+
redis.call('ZREMRANGEBYSCORE', checkpoint[1], checkpoint[2], checkpoint[2])
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
redis.call('DEL', KEYS[8])
|
|
367
|
+
if redis.call('HLEN', KEYS[9]) == 0 then redis.call('DEL', KEYS[9]) end
|
|
368
|
+
end
|
|
344
369
|
local function lane_field(lane, suffix)
|
|
345
370
|
return '__lane:' .. tostring(string.len(lane)) .. ':' .. lane .. ':' .. suffix
|
|
346
371
|
end
|
|
347
372
|
local function settle(field)
|
|
373
|
+
redis.call('ZREM', KEYS[5], field)
|
|
348
374
|
redis.call('ZREM', KEYS[6], field)
|
|
349
|
-
redis.call('
|
|
350
|
-
local lane = redis.call('HGET', KEYS[5], field .. ':l') or ''
|
|
375
|
+
local lane = redis.call('HGET', KEYS[4], field .. ':l') or ''
|
|
351
376
|
if lane == '' then return end
|
|
352
377
|
local head_key = lane_field(lane, 'head')
|
|
353
|
-
if redis.call('HGET', KEYS[
|
|
354
|
-
local next_field = redis.call('HGET', KEYS[
|
|
355
|
-
while next_field and redis.call('HEXISTS', KEYS[
|
|
356
|
-
next_field = redis.call('HGET', KEYS[
|
|
378
|
+
if redis.call('HGET', KEYS[4], head_key) ~= field then return end
|
|
379
|
+
local next_field = redis.call('HGET', KEYS[4], field .. ':next')
|
|
380
|
+
while next_field and redis.call('HEXISTS', KEYS[4], next_field .. ':p') == 1 do
|
|
381
|
+
next_field = redis.call('HGET', KEYS[4], next_field .. ':next')
|
|
357
382
|
end
|
|
358
383
|
if next_field then
|
|
359
|
-
redis.call('HSET', KEYS[
|
|
360
|
-
if redis.call('HEXISTS', KEYS[
|
|
361
|
-
redis.call('ZADD', KEYS[
|
|
384
|
+
redis.call('HSET', KEYS[4], head_key, next_field)
|
|
385
|
+
if redis.call('HEXISTS', KEYS[4], next_field .. ':f') == 0 then
|
|
386
|
+
redis.call('ZADD', KEYS[6], tonumber(next_field), next_field)
|
|
362
387
|
end
|
|
363
388
|
else
|
|
364
|
-
redis.call('HDEL', KEYS[
|
|
389
|
+
redis.call('HDEL', KEYS[4], head_key, lane_field(lane, 'tail'))
|
|
365
390
|
end
|
|
366
391
|
end
|
|
367
392
|
local function enqueue(field, idx, lane)
|
|
368
|
-
redis.call('ZADD', KEYS[
|
|
393
|
+
redis.call('ZADD', KEYS[5], idx, field)
|
|
369
394
|
if lane == '' then
|
|
370
|
-
redis.call('ZADD', KEYS[
|
|
395
|
+
redis.call('ZADD', KEYS[6], idx, field)
|
|
371
396
|
return
|
|
372
397
|
end
|
|
373
398
|
local tail_key = lane_field(lane, 'tail')
|
|
374
|
-
local tail = redis.call('HGET', KEYS[
|
|
399
|
+
local tail = redis.call('HGET', KEYS[4], tail_key)
|
|
375
400
|
if tail then
|
|
376
|
-
redis.call('HSET', KEYS[
|
|
401
|
+
redis.call('HSET', KEYS[4], tail .. ':next', field)
|
|
377
402
|
else
|
|
378
|
-
redis.call('HSET', KEYS[
|
|
379
|
-
redis.call('ZADD', KEYS[
|
|
403
|
+
redis.call('HSET', KEYS[4], lane_field(lane, 'head'), field)
|
|
404
|
+
redis.call('ZADD', KEYS[6], idx, field)
|
|
380
405
|
end
|
|
381
|
-
redis.call('HSET', KEYS[
|
|
406
|
+
redis.call('HSET', KEYS[4], tail_key, field)
|
|
382
407
|
end
|
|
383
408
|
local parent = ARGV[3]
|
|
384
409
|
local attempt = tonumber(ARGV[4])
|
|
385
410
|
local n = tonumber(ARGV[5])
|
|
386
411
|
local total = tonumber(redis.call('GET', KEYS[2]) or '0')
|
|
387
412
|
if tonumber(parent) < 1 or tonumber(parent) > total then return { 'missing' } end
|
|
388
|
-
local dispatches = tonumber(redis.call('HGET', KEYS[
|
|
389
|
-
local processed = redis.call('HGET', KEYS[
|
|
390
|
-
local processed_attempt = tonumber(redis.call('HGET', KEYS[
|
|
413
|
+
local dispatches = tonumber(redis.call('HGET', KEYS[4], parent .. ':d') or '0')
|
|
414
|
+
local processed = redis.call('HGET', KEYS[4], parent .. ':p')
|
|
415
|
+
local processed_attempt = tonumber(redis.call('HGET', KEYS[4], parent .. ':pa') or '0')
|
|
391
416
|
if processed then
|
|
392
417
|
if processed_attempt ~= attempt then return { 'superseded' } end
|
|
393
|
-
local raw_recorded_ids = redis.call('HGET', KEYS[
|
|
418
|
+
local raw_recorded_ids = redis.call('HGET', KEYS[4], parent .. ':ri')
|
|
394
419
|
if not raw_recorded_ids then return { 'partial' } end
|
|
395
420
|
local recorded_ids = cjson.decode(raw_recorded_ids)
|
|
396
421
|
if #recorded_ids ~= n then return { 'partial' } end
|
|
@@ -420,9 +445,9 @@ if processed then
|
|
|
420
445
|
return result
|
|
421
446
|
end
|
|
422
447
|
if dispatches ~= attempt then return { 'superseded' } end
|
|
423
|
-
if redis.call('HEXISTS', KEYS[
|
|
424
|
-
redis.call('HEXISTS', KEYS[
|
|
425
|
-
tonumber(redis.call('HGET', KEYS[
|
|
448
|
+
if redis.call('HEXISTS', KEYS[4], parent .. ':ch') == 0 or
|
|
449
|
+
redis.call('HEXISTS', KEYS[4], parent .. ':f') == 1 or
|
|
450
|
+
tonumber(redis.call('HGET', KEYS[4], parent .. ':lfa') or '0') == attempt then
|
|
426
451
|
return { 'superseded' }
|
|
427
452
|
end
|
|
428
453
|
for i = 1, n do
|
|
@@ -433,12 +458,13 @@ local returned_ids = {}
|
|
|
433
458
|
for i = 1, n do returned_ids[i] = ARGV[5 + (i - 1) * 5 + 1] end
|
|
434
459
|
local returned_json = '[]'
|
|
435
460
|
if n > 0 then returned_json = cjson.encode(returned_ids) end
|
|
436
|
-
redis.call('HSET', KEYS[
|
|
461
|
+
redis.call('HSET', KEYS[4],
|
|
437
462
|
parent .. ':p', ARGV[2],
|
|
438
463
|
parent .. ':pa', ARGV[4],
|
|
439
464
|
parent .. ':ri', returned_json)
|
|
440
|
-
redis.call('HDEL', KEYS[
|
|
465
|
+
redis.call('HDEL', KEYS[4], parent .. ':ch', parent .. ':ce')
|
|
441
466
|
settle(parent)
|
|
467
|
+
release_snapshot_pins()
|
|
442
468
|
local cause = cjson.encode({ index = tonumber(parent), attempt = attempt })
|
|
443
469
|
for i = 1, n do
|
|
444
470
|
local id = ARGV[5 + (i - 1) * 5 + 1]
|
|
@@ -451,26 +477,73 @@ for i = 1, n do
|
|
|
451
477
|
'id', id, 'type', typ, 'payload', payload, 'created_at', ARGV[2],
|
|
452
478
|
'cause', cause, 'lane', lane)
|
|
453
479
|
redis.call('HSET', KEYS[3], id, cjson.encode({ ARGV[1], idx }))
|
|
454
|
-
redis.call('HSET', KEYS[
|
|
480
|
+
redis.call('HSET', KEYS[4], tostring(idx) .. ':l', lane)
|
|
455
481
|
if settled == '1' then
|
|
456
|
-
redis.call('HSET', KEYS[
|
|
482
|
+
redis.call('HSET', KEYS[4], tostring(idx) .. ':p', ARGV[2])
|
|
457
483
|
else
|
|
458
484
|
enqueue(tostring(idx), idx, lane)
|
|
459
485
|
end
|
|
460
486
|
end
|
|
461
487
|
redis.call('SET', KEYS[2], tostring(total + n))
|
|
462
|
-
redis.call('ZADD', KEYS[4], 0, ARGV[1])
|
|
463
488
|
return { 'ok', tostring(total + 1),
|
|
464
|
-
tostring(redis.call('EXISTS', KEYS[
|
|
489
|
+
tostring(redis.call('EXISTS', KEYS[7])) }
|
|
465
490
|
`
|
|
466
491
|
|
|
467
|
-
/**
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
492
|
+
/**
|
|
493
|
+
* KEYS: head, history, event counter, event meta, pin counts.
|
|
494
|
+
* ARGV: nowMs, event pin key prefix, write count, then per write:
|
|
495
|
+
* index, snapshot JSON, pin count, pin event indexes.
|
|
496
|
+
*/
|
|
497
|
+
const PUT_SNAPSHOTS_LUA = `
|
|
498
|
+
local total = tonumber(redis.call('GET', KEYS[3]) or '0')
|
|
499
|
+
local writes = {}
|
|
500
|
+
local offset = 4
|
|
501
|
+
for i = 1, tonumber(ARGV[3]) do
|
|
502
|
+
local index = tonumber(ARGV[offset])
|
|
503
|
+
local snapshot = ARGV[offset + 1]
|
|
504
|
+
local pin_count = tonumber(ARGV[offset + 2])
|
|
505
|
+
local ref = cjson.encode({ KEYS[2], tostring(index) })
|
|
506
|
+
for p = 1, pin_count do
|
|
507
|
+
local event_index = tonumber(ARGV[offset + 2 + p])
|
|
508
|
+
if event_index >= 1 and event_index <= total and
|
|
509
|
+
redis.call('HEXISTS', KEYS[4], tostring(event_index) .. ':p') == 0 and
|
|
510
|
+
redis.call('HEXISTS', KEYS[4], tostring(event_index) .. ':f') == 0 then
|
|
511
|
+
local pin_key = ARGV[2] .. tostring(event_index)
|
|
512
|
+
if redis.call('SADD', pin_key, ref) == 1 then
|
|
513
|
+
redis.call('HINCRBY', KEYS[5], ref, 1)
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
end
|
|
517
|
+
writes[#writes + 1] = { index = index, snapshot = snapshot, ref = ref }
|
|
518
|
+
offset = offset + 3 + pin_count
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
local head = redis.call('GET', KEYS[1])
|
|
522
|
+
local head_index = nil
|
|
523
|
+
if head then head_index = tonumber(cjson.decode(head)['index']) end
|
|
524
|
+
local latest = writes[#writes]
|
|
525
|
+
local writes_head = not head_index or latest.index >= head_index
|
|
526
|
+
local advances = not head_index or latest.index > head_index
|
|
527
|
+
local final_head_index = advances and latest.index or head_index
|
|
528
|
+
|
|
529
|
+
if advances and head and tonumber(redis.call('HGET', KEYS[5], cjson.encode({ KEYS[2], tostring(head_index) })) or '0') > 0 then
|
|
530
|
+
redis.call('ZREMRANGEBYSCORE', KEYS[2], head_index, head_index)
|
|
531
|
+
redis.call('ZADD', KEYS[2], head_index, head)
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
for _, write in ipairs(writes) do
|
|
535
|
+
if write.index < final_head_index then
|
|
536
|
+
redis.call('ZREMRANGEBYSCORE', KEYS[2], write.index, write.index)
|
|
537
|
+
if tonumber(redis.call('HGET', KEYS[5], write.ref) or '0') > 0 then
|
|
538
|
+
redis.call('ZADD', KEYS[2], write.index, write.snapshot)
|
|
539
|
+
end
|
|
540
|
+
end
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
if writes_head then
|
|
544
|
+
redis.call('SET', KEYS[1], latest.snapshot)
|
|
545
|
+
redis.call('ZREMRANGEBYSCORE', KEYS[2], latest.index, latest.index)
|
|
472
546
|
end
|
|
473
|
-
redis.call('SET', KEYS[1], ARGV[2])
|
|
474
547
|
return 1
|
|
475
548
|
`
|
|
476
549
|
|
|
@@ -527,11 +600,26 @@ end
|
|
|
527
600
|
return live
|
|
528
601
|
`
|
|
529
602
|
|
|
530
|
-
/**
|
|
603
|
+
/**
|
|
604
|
+
* KEYS: head, history, event counter. ARGV: snapshot frontier or ''.
|
|
605
|
+
* Returns one atomic multiversion-cache-plus-log-frontier read.
|
|
606
|
+
*/
|
|
531
607
|
const READ_STATE_LUA = `
|
|
532
|
-
local
|
|
533
|
-
local
|
|
534
|
-
|
|
608
|
+
local head = redis.call('GET', KEYS[1])
|
|
609
|
+
local cap = ARGV[1]
|
|
610
|
+
local snapshot = nil
|
|
611
|
+
if head and (cap == '' or tonumber(cjson.decode(head)['index']) <= tonumber(cap)) then
|
|
612
|
+
snapshot = head
|
|
613
|
+
end
|
|
614
|
+
local upper = cap == '' and '+inf' or cap
|
|
615
|
+
local historical = redis.call('ZREVRANGEBYSCORE', KEYS[2], upper, '-inf', 'LIMIT', 0, 1)
|
|
616
|
+
if #historical > 0 then
|
|
617
|
+
if not snapshot or tonumber(cjson.decode(historical[1])['index']) > tonumber(cjson.decode(snapshot)['index']) then
|
|
618
|
+
snapshot = historical[1]
|
|
619
|
+
end
|
|
620
|
+
end
|
|
621
|
+
local frontier = redis.call('GET', KEYS[3]) or '0'
|
|
622
|
+
return { head or '', snapshot or '', frontier }
|
|
535
623
|
`
|
|
536
624
|
|
|
537
625
|
// ── wire helpers ─────────────────────────────────────────────────────
|
|
@@ -579,14 +667,6 @@ const toEvent = (sessionId: string, entry: StreamEntry): Event => {
|
|
|
579
667
|
|
|
580
668
|
const json = (value: unknown): string => JSON.stringify(value) ?? 'null'
|
|
581
669
|
|
|
582
|
-
const escapeGlob = (value: string): string =>
|
|
583
|
-
value
|
|
584
|
-
.replaceAll('\\', '\\\\')
|
|
585
|
-
.replaceAll('*', '\\*')
|
|
586
|
-
.replaceAll('?', '\\?')
|
|
587
|
-
.replaceAll('[', '\\[')
|
|
588
|
-
.replaceAll(']', '\\]')
|
|
589
|
-
|
|
590
670
|
const toCause = (raw: string | undefined): EventCause | null => {
|
|
591
671
|
if (raw === '' || raw === undefined) return null
|
|
592
672
|
const value = JSON.parse(raw) as {
|
|
@@ -696,10 +776,17 @@ export function createRedisStoreCore(
|
|
|
696
776
|
const pendingKey = (s: string): string => `${prefix}:${s}:pending`
|
|
697
777
|
const readyKey = (s: string): string => `${prefix}:${s}:ready`
|
|
698
778
|
const snapKey = (s: string, r: string): string => `${prefix}:${s}:snap:${r}`
|
|
779
|
+
const snapHistoryKey = (s: string, r: string): string =>
|
|
780
|
+
`${prefix}:${s}:snap-history:${r}`
|
|
781
|
+
const snapshotEventPinsKey = (s: string, index: number): string =>
|
|
782
|
+
`${prefix}:${s}:snapshot-pins:${index}`
|
|
783
|
+
const snapshotEventPinsPrefix = (s: string): string =>
|
|
784
|
+
`${prefix}:${s}:snapshot-pins:`
|
|
785
|
+
const snapshotPinCountsKey = (s: string): string =>
|
|
786
|
+
`${prefix}:${s}:snapshot-pin-counts`
|
|
699
787
|
const watchedKey = (s: string): string => `${prefix}:${s}:watched`
|
|
700
788
|
const presenceKey = (s: string): string => `${prefix}:${s}:presence`
|
|
701
789
|
const idsKey = `${prefix}:ids`
|
|
702
|
-
const sessionsKey = `${prefix}:sessions`
|
|
703
790
|
|
|
704
791
|
const evalScript = (
|
|
705
792
|
script: string,
|
|
@@ -924,7 +1011,6 @@ export function createRedisStoreCore(
|
|
|
924
1011
|
storeKey(sessionId),
|
|
925
1012
|
countKey(sessionId),
|
|
926
1013
|
idsKey,
|
|
927
|
-
sessionsKey,
|
|
928
1014
|
metaKey(sessionId),
|
|
929
1015
|
pendingKey(sessionId),
|
|
930
1016
|
readyKey(sessionId),
|
|
@@ -1081,11 +1167,12 @@ export function createRedisStoreCore(
|
|
|
1081
1167
|
storeKey(sessionId),
|
|
1082
1168
|
countKey(sessionId),
|
|
1083
1169
|
idsKey,
|
|
1084
|
-
sessionsKey,
|
|
1085
1170
|
metaKey(sessionId),
|
|
1086
1171
|
pendingKey(sessionId),
|
|
1087
1172
|
readyKey(sessionId),
|
|
1088
1173
|
watchedKey(sessionId),
|
|
1174
|
+
snapshotEventPinsKey(sessionId, index),
|
|
1175
|
+
snapshotPinCountsKey(sessionId),
|
|
1089
1176
|
],
|
|
1090
1177
|
[
|
|
1091
1178
|
sessionId,
|
|
@@ -1178,6 +1265,8 @@ export function createRedisStoreCore(
|
|
|
1178
1265
|
metaKey(sessionId),
|
|
1179
1266
|
pendingKey(sessionId),
|
|
1180
1267
|
readyKey(sessionId),
|
|
1268
|
+
snapshotEventPinsKey(sessionId, index),
|
|
1269
|
+
snapshotPinCountsKey(sessionId),
|
|
1181
1270
|
],
|
|
1182
1271
|
[index, attempt, error, maxFailures, clock.now().getTime()],
|
|
1183
1272
|
)) as string[]
|
|
@@ -1194,22 +1283,37 @@ export function createRedisStoreCore(
|
|
|
1194
1283
|
})
|
|
1195
1284
|
},
|
|
1196
1285
|
|
|
1197
|
-
async readState(sessionId, reducerName) {
|
|
1286
|
+
async readState(sessionId, reducerName, stateOptions) {
|
|
1198
1287
|
return wrap(async () => {
|
|
1199
1288
|
const reply = (await evalScript(
|
|
1200
1289
|
READ_STATE_LUA,
|
|
1201
|
-
[
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1290
|
+
[
|
|
1291
|
+
snapKey(sessionId, reducerName),
|
|
1292
|
+
snapHistoryKey(sessionId, reducerName),
|
|
1293
|
+
countKey(sessionId),
|
|
1294
|
+
],
|
|
1295
|
+
[
|
|
1296
|
+
stateOptions?.snapshotThroughIndex ??
|
|
1297
|
+
stateOptions?.throughIndex ??
|
|
1298
|
+
'',
|
|
1299
|
+
],
|
|
1300
|
+
)) as [string, string, string]
|
|
1301
|
+
const rawHead = reply[0]
|
|
1302
|
+
const raw = reply[1]
|
|
1303
|
+
const head =
|
|
1304
|
+
rawHead === ''
|
|
1305
|
+
? null
|
|
1306
|
+
: (JSON.parse(rawHead) as { index: number; state: unknown })
|
|
1205
1307
|
const parsed =
|
|
1206
1308
|
raw === ''
|
|
1207
1309
|
? null
|
|
1208
1310
|
: (JSON.parse(raw) as { index: number; state: unknown })
|
|
1209
|
-
const
|
|
1311
|
+
const currentFrontier = Number(reply[2])
|
|
1210
1312
|
if (
|
|
1211
|
-
!Number.isSafeInteger(
|
|
1212
|
-
|
|
1313
|
+
!Number.isSafeInteger(currentFrontier) ||
|
|
1314
|
+
currentFrontier < 0 ||
|
|
1315
|
+
(head !== null &&
|
|
1316
|
+
(!Number.isSafeInteger(head.index) || head.index < 0)) ||
|
|
1213
1317
|
(parsed !== null &&
|
|
1214
1318
|
(!Number.isSafeInteger(parsed.index) || parsed.index < 0))
|
|
1215
1319
|
) {
|
|
@@ -1218,26 +1322,78 @@ export function createRedisStoreCore(
|
|
|
1218
1322
|
`redis state frontier for session '${sessionId}' is invalid`,
|
|
1219
1323
|
)
|
|
1220
1324
|
}
|
|
1325
|
+
const frontier = Math.min(
|
|
1326
|
+
currentFrontier,
|
|
1327
|
+
stateOptions?.throughIndex ?? currentFrontier,
|
|
1328
|
+
)
|
|
1329
|
+
const snapshot = parsed
|
|
1221
1330
|
const events = await readEventRange(
|
|
1222
1331
|
sessionId,
|
|
1223
|
-
|
|
1332
|
+
snapshot?.index ?? 0,
|
|
1224
1333
|
frontier,
|
|
1225
1334
|
)
|
|
1226
1335
|
return {
|
|
1227
|
-
|
|
1228
|
-
|
|
1336
|
+
headIndex: head?.index ?? null,
|
|
1337
|
+
snapshot: snapshot
|
|
1338
|
+
? { index: snapshot.index, state: snapshot.state }
|
|
1229
1339
|
: null,
|
|
1230
1340
|
events,
|
|
1231
1341
|
}
|
|
1232
1342
|
})
|
|
1233
1343
|
},
|
|
1234
1344
|
|
|
1235
|
-
async
|
|
1345
|
+
async putSnapshots(sessionId, reducerName, writes) {
|
|
1346
|
+
const byIndex = new Map<
|
|
1347
|
+
number,
|
|
1348
|
+
{ index: number; state: unknown; pinEventIndexes: Set<number> }
|
|
1349
|
+
>()
|
|
1350
|
+
for (const write of writes) {
|
|
1351
|
+
const existing = byIndex.get(write.index)
|
|
1352
|
+
if (existing) {
|
|
1353
|
+
existing.state = write.state
|
|
1354
|
+
for (const eventIndex of write.pinEventIndexes ?? []) {
|
|
1355
|
+
existing.pinEventIndexes.add(eventIndex)
|
|
1356
|
+
}
|
|
1357
|
+
} else {
|
|
1358
|
+
byIndex.set(write.index, {
|
|
1359
|
+
index: write.index,
|
|
1360
|
+
state: write.state,
|
|
1361
|
+
pinEventIndexes: new Set(write.pinEventIndexes ?? []),
|
|
1362
|
+
})
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
const normalized = [...byIndex.values()].toSorted(
|
|
1366
|
+
(a, b) => a.index - b.index,
|
|
1367
|
+
)
|
|
1368
|
+
if (normalized.length === 0) return
|
|
1236
1369
|
await wrap(async () => {
|
|
1370
|
+
const args: Array<string | number> = [
|
|
1371
|
+
clock.now().getTime(),
|
|
1372
|
+
snapshotEventPinsPrefix(sessionId),
|
|
1373
|
+
normalized.length,
|
|
1374
|
+
]
|
|
1375
|
+
for (const write of normalized) {
|
|
1376
|
+
args.push(
|
|
1377
|
+
write.index,
|
|
1378
|
+
json({
|
|
1379
|
+
index: write.index,
|
|
1380
|
+
state: write.state,
|
|
1381
|
+
updatedAt: clock.now().getTime(),
|
|
1382
|
+
}),
|
|
1383
|
+
write.pinEventIndexes.size,
|
|
1384
|
+
...write.pinEventIndexes,
|
|
1385
|
+
)
|
|
1386
|
+
}
|
|
1237
1387
|
await evalScript(
|
|
1238
|
-
|
|
1239
|
-
[
|
|
1240
|
-
|
|
1388
|
+
PUT_SNAPSHOTS_LUA,
|
|
1389
|
+
[
|
|
1390
|
+
snapKey(sessionId, reducerName),
|
|
1391
|
+
snapHistoryKey(sessionId, reducerName),
|
|
1392
|
+
countKey(sessionId),
|
|
1393
|
+
metaKey(sessionId),
|
|
1394
|
+
snapshotPinCountsKey(sessionId),
|
|
1395
|
+
],
|
|
1396
|
+
args,
|
|
1241
1397
|
)
|
|
1242
1398
|
})
|
|
1243
1399
|
},
|
|
@@ -1325,152 +1481,6 @@ export function createRedisStoreCore(
|
|
|
1325
1481
|
},
|
|
1326
1482
|
},
|
|
1327
1483
|
|
|
1328
|
-
inspect: {
|
|
1329
|
-
async listSessions(inspectionOptions) {
|
|
1330
|
-
return wrap(async () => {
|
|
1331
|
-
const minimum =
|
|
1332
|
-
inspectionOptions.cursor === undefined
|
|
1333
|
-
? `[${inspectionOptions.prefix}`
|
|
1334
|
-
: `(${inspectionOptions.cursor}`
|
|
1335
|
-
const candidates = (await call(
|
|
1336
|
-
'ZRANGEBYLEX',
|
|
1337
|
-
sessionsKey,
|
|
1338
|
-
minimum,
|
|
1339
|
-
'+',
|
|
1340
|
-
'LIMIT',
|
|
1341
|
-
0,
|
|
1342
|
-
inspectionOptions.limit + 1,
|
|
1343
|
-
)) as string[]
|
|
1344
|
-
const sessionIds = candidates
|
|
1345
|
-
.filter((id) => id.startsWith(inspectionOptions.prefix))
|
|
1346
|
-
.slice(0, inspectionOptions.limit)
|
|
1347
|
-
|
|
1348
|
-
const sessions = await Promise.all(
|
|
1349
|
-
sessionIds.map(async (sessionId) => {
|
|
1350
|
-
const rows = await readRange(sessionId, 0)
|
|
1351
|
-
const first = rows[0]!
|
|
1352
|
-
let updatedAt = first.createdAt
|
|
1353
|
-
let pendingCount = 0
|
|
1354
|
-
let failedCount = 0
|
|
1355
|
-
let attemptCount = 0
|
|
1356
|
-
let failureCount = 0
|
|
1357
|
-
for (const row of rows) {
|
|
1358
|
-
attemptCount += row.attemptCount
|
|
1359
|
-
failureCount += row.failureCount
|
|
1360
|
-
if (row.processedAt === null && row.failedAt === null) {
|
|
1361
|
-
pendingCount += 1
|
|
1362
|
-
}
|
|
1363
|
-
if (row.failedAt !== null) failedCount += 1
|
|
1364
|
-
for (const timestamp of [
|
|
1365
|
-
row.createdAt,
|
|
1366
|
-
row.firstClaimedAt,
|
|
1367
|
-
row.lastClaimedAt,
|
|
1368
|
-
row.lastFailedAt,
|
|
1369
|
-
row.processedAt,
|
|
1370
|
-
row.failedAt,
|
|
1371
|
-
]) {
|
|
1372
|
-
if (timestamp && timestamp > updatedAt) updatedAt = timestamp
|
|
1373
|
-
}
|
|
1374
|
-
}
|
|
1375
|
-
return {
|
|
1376
|
-
sessionId,
|
|
1377
|
-
eventCount: rows.length,
|
|
1378
|
-
pendingCount,
|
|
1379
|
-
failedCount,
|
|
1380
|
-
attemptCount,
|
|
1381
|
-
failureCount,
|
|
1382
|
-
firstEventAt: first.createdAt,
|
|
1383
|
-
updatedAt,
|
|
1384
|
-
}
|
|
1385
|
-
}),
|
|
1386
|
-
)
|
|
1387
|
-
return {
|
|
1388
|
-
sessions: sessions.toSorted((a, b) =>
|
|
1389
|
-
a.sessionId.localeCompare(b.sessionId),
|
|
1390
|
-
),
|
|
1391
|
-
cursor:
|
|
1392
|
-
candidates.length > inspectionOptions.limit &&
|
|
1393
|
-
candidates[inspectionOptions.limit]?.startsWith(
|
|
1394
|
-
inspectionOptions.prefix,
|
|
1395
|
-
)
|
|
1396
|
-
? (sessionIds.at(-1) ?? null)
|
|
1397
|
-
: null,
|
|
1398
|
-
}
|
|
1399
|
-
})
|
|
1400
|
-
},
|
|
1401
|
-
async readEvents(sessionId, pageOptions) {
|
|
1402
|
-
return wrap(async () => {
|
|
1403
|
-
const currentFrontier = await readFrontier(sessionId)
|
|
1404
|
-
const throughIndex = Math.min(
|
|
1405
|
-
pageOptions.throughIndex ?? currentFrontier,
|
|
1406
|
-
currentFrontier,
|
|
1407
|
-
)
|
|
1408
|
-
const events = await readRangePage(
|
|
1409
|
-
sessionId,
|
|
1410
|
-
pageOptions.afterIndex,
|
|
1411
|
-
throughIndex,
|
|
1412
|
-
pageOptions.limit,
|
|
1413
|
-
)
|
|
1414
|
-
if (throughIndex > pageOptions.afterIndex && events.length === 0) {
|
|
1415
|
-
throw new A2Error(
|
|
1416
|
-
'STORE_UNAVAILABLE',
|
|
1417
|
-
`redis event range for session '${sessionId}' ended before index ${throughIndex}`,
|
|
1418
|
-
)
|
|
1419
|
-
}
|
|
1420
|
-
for (let position = 0; position < events.length; position += 1) {
|
|
1421
|
-
if (
|
|
1422
|
-
events[position]!.index !==
|
|
1423
|
-
pageOptions.afterIndex + position + 1
|
|
1424
|
-
) {
|
|
1425
|
-
throw new A2Error(
|
|
1426
|
-
'STORE_UNAVAILABLE',
|
|
1427
|
-
`redis event range for session '${sessionId}' is not contiguous`,
|
|
1428
|
-
)
|
|
1429
|
-
}
|
|
1430
|
-
}
|
|
1431
|
-
return { events, throughIndex }
|
|
1432
|
-
})
|
|
1433
|
-
},
|
|
1434
|
-
async listSnapshots(sessionId) {
|
|
1435
|
-
return wrap(async () => {
|
|
1436
|
-
const start = `${prefix}:${sessionId}:snap:`
|
|
1437
|
-
let cursor = '0'
|
|
1438
|
-
const keys: string[] = []
|
|
1439
|
-
do {
|
|
1440
|
-
// oxlint-disable-next-line no-await-in-loop -- each SCAN cursor comes from the prior reply
|
|
1441
|
-
const reply = (await call(
|
|
1442
|
-
'SCAN',
|
|
1443
|
-
cursor,
|
|
1444
|
-
'MATCH',
|
|
1445
|
-
`${escapeGlob(start)}*`,
|
|
1446
|
-
'COUNT',
|
|
1447
|
-
100,
|
|
1448
|
-
)) as [string, string[]]
|
|
1449
|
-
cursor = reply[0]
|
|
1450
|
-
keys.push(...reply[1].filter((key) => key.startsWith(start)))
|
|
1451
|
-
} while (cursor !== '0')
|
|
1452
|
-
const snapshots = await Promise.all(
|
|
1453
|
-
keys.map(async (key) => {
|
|
1454
|
-
const raw = (await call('GET', key)) as string | null
|
|
1455
|
-
if (raw === null) return null
|
|
1456
|
-
const parsed = JSON.parse(raw) as {
|
|
1457
|
-
index: number
|
|
1458
|
-
updatedAt?: number
|
|
1459
|
-
}
|
|
1460
|
-
return {
|
|
1461
|
-
reducerName: key.slice(start.length),
|
|
1462
|
-
index: parsed.index,
|
|
1463
|
-
updatedAt: new Date(parsed.updatedAt ?? 0),
|
|
1464
|
-
}
|
|
1465
|
-
}),
|
|
1466
|
-
)
|
|
1467
|
-
return snapshots
|
|
1468
|
-
.filter((snapshot) => snapshot !== null)
|
|
1469
|
-
.toSorted((a, b) => a.reducerName.localeCompare(b.reducerName))
|
|
1470
|
-
})
|
|
1471
|
-
},
|
|
1472
|
-
},
|
|
1473
|
-
|
|
1474
1484
|
async readRange(sessionId, afterIndex, throughIndex) {
|
|
1475
1485
|
return wrap(() => readRange(sessionId, afterIndex, throughIndex))
|
|
1476
1486
|
},
|