experimental-a2 0.17.0 → 0.18.1
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 +32 -0
- package/README.md +1 -1
- package/dist/{actor-DBbe5xeE.d.ts → actor-C7b-1arn.d.ts} +5 -5
- package/dist/{actor-DBbe5xeE.d.ts.map → actor-C7b-1arn.d.ts.map} +1 -1
- package/dist/actor-client.d.ts +1 -1
- package/dist/actor-react.d.ts +3 -3
- package/dist/actor-react.js +1 -1
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +2 -2
- package/dist/{ai-vpGtcaCB.d.ts → ai-BMLlI2Ff.d.ts} +3 -3
- package/dist/{ai-vpGtcaCB.d.ts.map → ai-BMLlI2Ff.d.ts.map} +1 -1
- package/dist/{ai-UrWfKeuv.js → ai-D6mGlO8j.js} +2 -2
- package/dist/{ai-UrWfKeuv.js.map → ai-D6mGlO8j.js.map} +1 -1
- package/dist/ai-client.d.ts +2 -2
- package/dist/ai-client.js +2 -2
- 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 +692 -0
- package/dist/ai-fx.js.map +1 -0
- package/dist/ai-harness-B2suP2Qh.d.ts +26 -0
- package/dist/ai-harness-B2suP2Qh.d.ts.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-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 +950 -0
- package/dist/ai-pi.js.map +1 -0
- package/dist/ai-react.d.ts +5 -5
- package/dist/ai-react.js +2 -2
- package/dist/ai-server.d.ts +47 -25
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +113 -40
- package/dist/ai-server.js.map +1 -1
- package/dist/ai-tool-input-DmSqMUQi.js +40 -0
- package/dist/ai-tool-input-DmSqMUQi.js.map +1 -0
- package/dist/ai.d.ts +1 -1
- package/dist/ai.js +1 -1
- package/dist/bounded-cache-CPHWF-8h.js +41 -0
- package/dist/bounded-cache-CPHWF-8h.js.map +1 -0
- package/dist/{client-COaiaDub.js → client-BhHLoNKF.js} +3 -3
- package/dist/{client-COaiaDub.js.map → client-BhHLoNKF.js.map} +1 -1
- package/dist/{client-qRnRUzBS.d.ts → client-h_DKFAjj.d.ts} +3 -3
- package/dist/{client-qRnRUzBS.d.ts.map → client-h_DKFAjj.d.ts.map} +1 -1
- package/dist/client.d.ts +3 -3
- package/dist/client.js +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/{initial-snapshot-BrLso5x_.d.ts → initial-snapshot-Bx-c0pDP.d.ts} +2 -2
- package/dist/{initial-snapshot-BrLso5x_.d.ts.map → initial-snapshot-Bx-c0pDP.d.ts.map} +1 -1
- package/dist/{internal-CbVvEwad.js → internal-CscSY7K_.js} +11 -3
- package/dist/internal-CscSY7K_.js.map +1 -0
- package/dist/otel.d.ts +1 -1
- package/dist/{react-BbJ-g6Uu.d.ts → react-BBXgHWFc.d.ts} +4 -4
- package/dist/{react-BbJ-g6Uu.d.ts.map → react-BBXgHWFc.d.ts.map} +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/reducer-BAXc8JyF.js.map +1 -1
- package/dist/{reducer-BYu6DT7h.d.ts → reducer-CqA59Q6U.d.ts} +2 -2
- package/dist/{reducer-BYu6DT7h.d.ts.map → reducer-CqA59Q6U.d.ts.map} +1 -1
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.js +3 -3
- package/dist/scheduler-qstash.js.map +1 -1
- package/dist/scheduler-vercel.d.ts +7 -7
- package/dist/scheduler-vercel.d.ts.map +1 -1
- package/dist/scheduler-vercel.js +9 -10
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/{server-Rj64fNvX.js → server-D-Y4pqwu.js} +126 -67
- package/dist/server-D-Y4pqwu.js.map +1 -0
- package/dist/{server-CGM8WzRG.d.ts → server-DYAATFuT.d.ts} +42 -7
- package/dist/server-DYAATFuT.d.ts.map +1 -0
- package/dist/server.d.ts +4 -4
- package/dist/server.js +1 -1
- package/dist/{session-clear-Dshpuhff.d.ts → session-clear-C-QwTXV1.d.ts} +2 -2
- package/dist/session-clear-C-QwTXV1.d.ts.map +1 -0
- package/dist/{store-Gpkq7WBo.d.ts → store-CyLUy86v.d.ts} +53 -5
- package/dist/store-CyLUy86v.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.js +56 -11
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-4_y8lETm.js → store-polling-7WJV0YWP.js} +2 -2
- package/dist/{store-polling-4_y8lETm.js.map → store-polling-7WJV0YWP.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 +106 -25
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-presence-polling-Q1C-k2dd.js → store-presence-polling-B1ixeqIS.js} +3 -3
- package/dist/{store-presence-polling-Q1C-k2dd.js.map → store-presence-polling-B1ixeqIS.js.map} +1 -1
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +3 -3
- package/dist/{store-redis-notify-tqWNnMRu.js → store-redis-notify-CM0AfLY0.js} +271 -101
- package/dist/store-redis-notify-CM0AfLY0.js.map +1 -0
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.js +1 -1
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.js +66 -21
- package/dist/store-sqlite.js.map +1 -1
- package/dist/{telemetry-cZH-cibj.d.ts → telemetry-Cyfqo6Qt.d.ts} +2 -2
- package/dist/telemetry-Cyfqo6Qt.d.ts.map +1 -0
- package/dist/{wire-DjfHOvdW.js → wire-BKxH1vbP.js} +2 -2
- package/dist/{wire-DjfHOvdW.js.map → wire-BKxH1vbP.js.map} +1 -1
- package/docs/01-quickstart.mdx +49 -83
- package/docs/{guides/07-examples.mdx → 02-examples.mdx} +10 -20
- package/docs/actors/01-introduction.mdx +48 -86
- package/docs/actors/02-concurrency.mdx +43 -70
- package/docs/actors/03-timers.mdx +28 -42
- package/docs/actors/04-routes.mdx +110 -150
- 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 +109 -145
- package/docs/reference/01-core.mdx +139 -0
- package/docs/reference/02-server.mdx +860 -0
- package/docs/reference/03-client.mdx +301 -0
- package/docs/reference/04-ai.mdx +731 -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 +142 -0
- package/docs/server/04-routes.mdx +243 -0
- package/docs/server/05-production.mdx +323 -0
- package/docs/server/06-guarantees.mdx +132 -0
- package/examples/playground/DEPLOY.md +3 -5
- package/examples/playground/app/a0/[appId]/page.tsx +12 -13
- package/examples/playground/app/a0/app-list.tsx +9 -2
- package/examples/playground/app/a0/app-view.tsx +9 -1
- package/examples/playground/app/a0/apps/[appId]/route.ts +10 -5
- package/examples/playground/app/a0/apps.test.ts +12 -0
- package/examples/playground/app/a0/apps.ts +22 -7
- package/examples/playground/app/a0/chat.tsx +47 -1
- package/examples/playground/app/a0/devtools.tsx +2 -8
- 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/model.test.ts +18 -1
- package/examples/playground/app/a0/model.ts +47 -10
- 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/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/sandbox-log.ts +10 -4
- package/examples/playground/app/a0/sandbox-session.ts +152 -0
- package/examples/playground/app/a0/scheduler/route.ts +2 -2
- package/examples/playground/app/a0/server.ts +98 -54
- package/examples/playground/app/a0/session.ts +17 -5
- package/examples/playground/app/a0/shell.tsx +11 -3
- package/examples/playground/app/agent/ingress.test.ts +6 -3
- package/examples/playground/app/agent/server.ts +9 -6
- package/examples/playground/app/agent/telemetry.test.ts +71 -50
- package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +1 -1
- package/examples/playground/app/chat/server.test.ts +30 -26
- package/examples/playground/app/chat/server.ts +46 -46
- package/examples/playground/next.config.ts +17 -5
- package/examples/playground/package.json +4 -1
- package/examples/playground/vercel.json +18 -0
- package/package.json +27 -3
- package/src/ai-agent.ts +271 -90
- package/src/ai-fx.browser.ts +5 -0
- package/src/ai-fx.ts +912 -0
- package/src/ai-harness.ts +518 -0
- package/src/ai-model-metadata.ts +11 -6
- package/src/ai-pi.browser.ts +5 -0
- package/src/ai-pi.ts +1322 -0
- package/src/ai-sdk-step.ts +2 -2
- package/src/ai-server.ts +3 -1
- package/src/ai-tool-input.ts +45 -0
- package/src/internal.ts +13 -0
- package/src/libfx.d.ts +34 -0
- package/src/reducer.ts +1 -1
- package/src/scheduler-qstash.ts +5 -5
- package/src/scheduler-vercel.ts +14 -15
- package/src/server.ts +233 -29
- package/src/store-memory.ts +92 -16
- package/src/store-postgres.ts +156 -25
- package/src/store-redis-core.ts +322 -110
- package/src/store-redis-notify.ts +2 -0
- package/src/store-sqlite.ts +121 -28
- package/src/store.ts +55 -3
- package/src/telemetry.ts +1 -0
- package/dist/internal-CbVvEwad.js.map +0 -1
- package/dist/server-CGM8WzRG.d.ts.map +0 -1
- package/dist/server-Rj64fNvX.js.map +0 -1
- package/dist/session-clear-Dshpuhff.d.ts.map +0 -1
- package/dist/store-Gpkq7WBo.d.ts.map +0 -1
- package/dist/store-redis-notify-tqWNnMRu.js.map +0 -1
- package/dist/telemetry-cZH-cibj.d.ts.map +0 -1
- package/docs/concepts/01-contracts.mdx +0 -237
- 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 -193
- package/docs/guides/02-cancellation.mdx +0 -161
- package/docs/guides/03-react.mdx +0 -324
- package/docs/guides/04-local-first.mdx +0 -47
- package/docs/guides/05-production.mdx +0 -534
- package/docs/guides/06-ai-agents.mdx +0 -1408
- package/docs/guides/08-application-data.mdx +0 -113
- package/docs/guides/09-presence.mdx +0 -262
- package/docs/guides/10-transports.mdx +0 -221
- package/docs/reference/01-api.mdx +0 -2358
- package/docs/reference/02-errors.mdx +0 -150
- /package/docs/{concepts → contract}/meta.ts +0 -0
- /package/docs/{guides → server}/meta.ts +0 -0
package/src/store-redis-core.ts
CHANGED
|
@@ -123,6 +123,23 @@ const READ_CONCURRENCY = 16
|
|
|
123
123
|
|
|
124
124
|
const META_CHUNK_SIZE = 1000
|
|
125
125
|
|
|
126
|
+
/** The metadata columns of a full stored row. */
|
|
127
|
+
const STORED_EVENT_META_SUFFIXES = [
|
|
128
|
+
'p',
|
|
129
|
+
'pa',
|
|
130
|
+
'ri',
|
|
131
|
+
'ch',
|
|
132
|
+
'ce',
|
|
133
|
+
'f',
|
|
134
|
+
'fc',
|
|
135
|
+
'lc',
|
|
136
|
+
'lf',
|
|
137
|
+
'lfa',
|
|
138
|
+
'd',
|
|
139
|
+
'a',
|
|
140
|
+
'e',
|
|
141
|
+
] as const
|
|
142
|
+
|
|
126
143
|
/**
|
|
127
144
|
* Upstash bills every call a script makes, so scripts read the counter and
|
|
128
145
|
* split with one MGET, read metadata with one HMGET per hash (`read_fields`),
|
|
@@ -278,6 +295,33 @@ local function flush_queue()
|
|
|
278
295
|
end
|
|
279
296
|
`
|
|
280
297
|
|
|
298
|
+
/**
|
|
299
|
+
* Settles one unfinished event for scripts that include ENQUEUE_LUA: it
|
|
300
|
+
* leaves the pending and ready sets, and a lane head hands its lane to the
|
|
301
|
+
* next unfinished event. A dead-lettered successor stays out of the ready set.
|
|
302
|
+
*/
|
|
303
|
+
const SETTLE_LUA = `
|
|
304
|
+
local function settle(field, lane, next_field)
|
|
305
|
+
redis.call('ZREM', PENDING, field)
|
|
306
|
+
redis.call('ZREM', READY, field)
|
|
307
|
+
if lane == '' then return end
|
|
308
|
+
local head_key = lane_field(lane, 'head')
|
|
309
|
+
if redis.call('HGET', META, head_key) ~= field then return end
|
|
310
|
+
while next_field do
|
|
311
|
+
local row = read_fields({ next_field }, { 'p', 'f', 'next' })[next_field]
|
|
312
|
+
if not row[1] then
|
|
313
|
+
redis.call('HSET', META, head_key, next_field)
|
|
314
|
+
if not row[2] then
|
|
315
|
+
redis.call('ZADD', READY, tonumber(next_field), next_field)
|
|
316
|
+
end
|
|
317
|
+
return
|
|
318
|
+
end
|
|
319
|
+
next_field = row[3]
|
|
320
|
+
end
|
|
321
|
+
clear_lane(lane)
|
|
322
|
+
end
|
|
323
|
+
`
|
|
324
|
+
|
|
281
325
|
/**
|
|
282
326
|
* Event-id ownership for scripts that add events. Each script reads the
|
|
283
327
|
* counter, split, clear boundary, and per-id keys with one MGET and applies
|
|
@@ -450,7 +494,7 @@ return { 'ok', tostring(base + 1), pending_after }
|
|
|
450
494
|
/**
|
|
451
495
|
* KEYS: counter, meta, pending, ready, event snapshot pins, snapshot pin counts,
|
|
452
496
|
* metadata split.
|
|
453
|
-
* ARGV: idx, attempt, error, maxFailures, nowMs.
|
|
497
|
+
* ARGV: idx, attempt, error, maxFailures, nowMs, retryAtMs ('' for none).
|
|
454
498
|
*/
|
|
455
499
|
const FAIL_ATTEMPT_LUA = `
|
|
456
500
|
local COUNTER, META, SPLIT = KEYS[1], KEYS[2], KEYS[7]
|
|
@@ -482,8 +526,14 @@ put(meta_key, field .. ':e', ARGV[3])
|
|
|
482
526
|
put(meta_key, field .. ':lf', ARGV[5])
|
|
483
527
|
put(meta_key, field .. ':lfa', ARGV[2])
|
|
484
528
|
if dead then put(meta_key, field .. ':f', ARGV[5]) end
|
|
529
|
+
local hold = not dead and ARGV[6] ~= ''
|
|
530
|
+
if hold then put(meta_key, field .. ':ce', ARGV[6]) end
|
|
485
531
|
flush()
|
|
486
|
-
|
|
532
|
+
if hold then
|
|
533
|
+
redis.call('HDEL', meta_key, field .. ':ch')
|
|
534
|
+
else
|
|
535
|
+
redis.call('HDEL', meta_key, field .. ':ch', field .. ':ce')
|
|
536
|
+
end
|
|
487
537
|
if dead then
|
|
488
538
|
redis.call('ZREM', KEYS[4], field)
|
|
489
539
|
release_snapshot_pins(KEYS[5], KEYS[6])
|
|
@@ -493,23 +543,29 @@ return { 'failed', tostring(failures) }
|
|
|
493
543
|
`
|
|
494
544
|
|
|
495
545
|
/**
|
|
496
|
-
* KEYS: store, counter, meta, pending, ready, metadata split
|
|
497
|
-
*
|
|
546
|
+
* KEYS: store, counter, meta, pending, ready, metadata split, snapshot pin
|
|
547
|
+
* counts.
|
|
548
|
+
* ARGV: holder, nowMs, expiresAtMs, excludedCount, excluded indexes, then
|
|
549
|
+
* maxFailures ('' for none) and the event snapshot-pins key prefix.
|
|
498
550
|
* The ready set is a subset of the pending set, so an empty ready set is
|
|
499
551
|
* enough to report 'settled'. A claimed result ends with the clear boundary.
|
|
500
552
|
*/
|
|
501
553
|
const CLAIM_AVAILABLE_LUA = `
|
|
502
554
|
local COUNTER, META, SPLIT = KEYS[2], KEYS[3], KEYS[6]
|
|
503
555
|
${META_LUA}
|
|
556
|
+
${RELEASE_PINS_LUA}
|
|
504
557
|
local ready = redis.call('ZRANGE', KEYS[5], 0, -1)
|
|
505
558
|
local excluded = {}
|
|
559
|
+
local tail = 5 + tonumber(ARGV[4])
|
|
506
560
|
for i = 1, tonumber(ARGV[4]) do excluded[ARGV[4 + i]] = true end
|
|
561
|
+
local max_failures = tonumber(ARGV[tail])
|
|
507
562
|
local candidates = {}
|
|
508
563
|
for _, field in ipairs(ready) do
|
|
509
564
|
if not excluded[field] then candidates[#candidates + 1] = field end
|
|
510
565
|
end
|
|
511
566
|
local rows = read_fields(candidates, { 'ce', 'd', 'fc', 'a', 'e', 'lf', 'lfa' })
|
|
512
567
|
local claimed = {}
|
|
568
|
+
local dead = {}
|
|
513
569
|
local retry_at = nil
|
|
514
570
|
for _, field in ipairs(candidates) do
|
|
515
571
|
local row = rows[field]
|
|
@@ -519,29 +575,63 @@ for _, field in ipairs(candidates) do
|
|
|
519
575
|
else
|
|
520
576
|
local entries = redis.call('XRANGE', KEYS[1], field .. '-0', field .. '-0')
|
|
521
577
|
if #entries == 0 then return { 'missing', field } end
|
|
522
|
-
local attempt = tostring(tonumber(row[2] or '0') + 1)
|
|
523
|
-
local first_claimed = row[3] or ARGV[2]
|
|
524
578
|
local meta_key = mkey(field)
|
|
579
|
+
local attempt = tostring(tonumber(row[2] or '0') + 1)
|
|
580
|
+
-- An expiry that is not the current attempt's retry hold is a lapsed
|
|
581
|
+
-- claim: its attempt never finished.
|
|
582
|
+
local lapsed = row[1] and row[7] ~= row[2]
|
|
583
|
+
local failures = tonumber(row[4] or '0')
|
|
584
|
+
if lapsed then
|
|
585
|
+
failures = failures + 1
|
|
586
|
+
put(meta_key, field .. ':a', tostring(failures))
|
|
587
|
+
end
|
|
525
588
|
put(meta_key, field .. ':d', attempt)
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
589
|
+
if lapsed and max_failures and failures >= max_failures then
|
|
590
|
+
-- That takeover spent the last failed attempt: dead-letter, unclaimed.
|
|
591
|
+
-- Advancing the attempt fences the lapsed one; nothing runs.
|
|
592
|
+
put(meta_key, field .. ':f', ARGV[2])
|
|
593
|
+
dead[#dead + 1] = field
|
|
594
|
+
claimed[#claimed + 1] = {
|
|
595
|
+
attempt,
|
|
596
|
+
tostring(failures),
|
|
597
|
+
row[5] or false,
|
|
598
|
+
row[3] or false,
|
|
599
|
+
read_fields({ field }, { 'lc' })[field][1] or false,
|
|
600
|
+
row[6] or false,
|
|
601
|
+
row[7] or false,
|
|
602
|
+
false,
|
|
603
|
+
false,
|
|
604
|
+
entries[1],
|
|
605
|
+
ARGV[2]
|
|
606
|
+
}
|
|
607
|
+
else
|
|
608
|
+
local first_claimed = row[3] or ARGV[2]
|
|
609
|
+
put(meta_key, field .. ':fc', first_claimed)
|
|
610
|
+
put(meta_key, field .. ':lc', ARGV[2])
|
|
611
|
+
put(meta_key, field .. ':ch', ARGV[1])
|
|
612
|
+
put(meta_key, field .. ':ce', ARGV[3])
|
|
613
|
+
claimed[#claimed + 1] = {
|
|
614
|
+
attempt,
|
|
615
|
+
tostring(failures),
|
|
616
|
+
row[5] or false,
|
|
617
|
+
first_claimed,
|
|
618
|
+
ARGV[2],
|
|
619
|
+
row[6] or false,
|
|
620
|
+
row[7] or false,
|
|
621
|
+
ARGV[1],
|
|
622
|
+
ARGV[3],
|
|
623
|
+
entries[1],
|
|
624
|
+
false
|
|
625
|
+
}
|
|
626
|
+
end
|
|
542
627
|
end
|
|
543
628
|
end
|
|
544
629
|
flush()
|
|
630
|
+
for _, field in ipairs(dead) do
|
|
631
|
+
redis.call('HDEL', mkey(field), field .. ':ch', field .. ':ce')
|
|
632
|
+
redis.call('ZREM', KEYS[5], field)
|
|
633
|
+
release_snapshot_pins(ARGV[tail + 1] .. field, KEYS[7])
|
|
634
|
+
end
|
|
545
635
|
if #claimed > 0 then
|
|
546
636
|
local result = { 'claimed', tostring(#claimed) }
|
|
547
637
|
for _, claim in ipairs(claimed) do
|
|
@@ -611,25 +701,7 @@ ${ENQUEUE_LUA}
|
|
|
611
701
|
${ID_OWNERS_LUA}
|
|
612
702
|
${RELEASE_PINS_LUA}
|
|
613
703
|
${WAKE_LUA}
|
|
614
|
-
|
|
615
|
-
redis.call('ZREM', PENDING, field)
|
|
616
|
-
redis.call('ZREM', READY, field)
|
|
617
|
-
if lane == '' then return end
|
|
618
|
-
local head_key = lane_field(lane, 'head')
|
|
619
|
-
if redis.call('HGET', META, head_key) ~= field then return end
|
|
620
|
-
while next_field do
|
|
621
|
-
local row = read_fields({ next_field }, { 'p', 'f', 'next' })[next_field]
|
|
622
|
-
if not row[1] then
|
|
623
|
-
redis.call('HSET', META, head_key, next_field)
|
|
624
|
-
if not row[2] then
|
|
625
|
-
redis.call('ZADD', READY, tonumber(next_field), next_field)
|
|
626
|
-
end
|
|
627
|
-
return
|
|
628
|
-
end
|
|
629
|
-
next_field = row[3]
|
|
630
|
-
end
|
|
631
|
-
clear_lane(lane)
|
|
632
|
-
end
|
|
704
|
+
${SETTLE_LUA}
|
|
633
705
|
local parent = ARGV[3]
|
|
634
706
|
local attempt = tonumber(ARGV[4])
|
|
635
707
|
local n = tonumber(ARGV[5])
|
|
@@ -715,6 +787,78 @@ if n > 0 then wake(ARGV[6 + n * 5], tostring(total + n), ARGV[2]) end
|
|
|
715
787
|
return { 'ok', tostring(total + 1) }
|
|
716
788
|
`
|
|
717
789
|
|
|
790
|
+
/**
|
|
791
|
+
* KEYS: counter, meta, pending, ready, metadata split.
|
|
792
|
+
* ARGV: idx, action ('retry' | 'skip'), nowMs.
|
|
793
|
+
* Returns [redriven, hasPending]. Acts only on an unfinished, dead-lettered
|
|
794
|
+
* event above the clear boundary. A retry keeps the event pending and makes
|
|
795
|
+
* it ready again when it is unlaned or its lane's head.
|
|
796
|
+
*/
|
|
797
|
+
const REDRIVE_LUA = `
|
|
798
|
+
local COUNTER, META, SPLIT, PENDING, READY = KEYS[1], KEYS[2], KEYS[5], KEYS[3], KEYS[4]
|
|
799
|
+
${META_LUA}
|
|
800
|
+
${ENQUEUE_LUA}
|
|
801
|
+
${SETTLE_LUA}
|
|
802
|
+
local function result(redriven)
|
|
803
|
+
return { redriven, redis.call('ZCARD', PENDING) > 0 and '1' or '0' }
|
|
804
|
+
end
|
|
805
|
+
local counts = redis.call('MGET', COUNTER, SPLIT, CLEAR)
|
|
806
|
+
local total = tonumber(counts[1] or '0')
|
|
807
|
+
use_split(counts[2], counts[1], counts[3])
|
|
808
|
+
local idx = tonumber(ARGV[1])
|
|
809
|
+
if idx <= boundary or idx > total then return result('0') end
|
|
810
|
+
local field = tostring(idx)
|
|
811
|
+
local row = read_fields({ field }, { 'p', 'f', 'l', 'next' })[field]
|
|
812
|
+
if row[1] or not row[2] then return result('0') end
|
|
813
|
+
local meta_key = mkey(field)
|
|
814
|
+
local lane = row[3] or ''
|
|
815
|
+
if ARGV[2] == 'skip' then
|
|
816
|
+
put(meta_key, field .. ':p', ARGV[3])
|
|
817
|
+
settle(field, lane, row[4])
|
|
818
|
+
flush_queue()
|
|
819
|
+
flush()
|
|
820
|
+
else
|
|
821
|
+
redis.call('HDEL', meta_key, field .. ':f', field .. ':a')
|
|
822
|
+
if lane == '' or redis.call('HGET', META, lane_field(lane, 'head')) == field then
|
|
823
|
+
redis.call('ZADD', READY, idx, field)
|
|
824
|
+
end
|
|
825
|
+
end
|
|
826
|
+
return result('1')
|
|
827
|
+
`
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* KEYS: store, counter, meta, pending, metadata split.
|
|
831
|
+
* Returns the dead-lettered events above the clear boundary, oldest first,
|
|
832
|
+
* as pairs of a stream entry and its STORED_EVENT_META_SUFFIXES columns.
|
|
833
|
+
* Every unfinished event is in the pending set, so the scan is bounded by
|
|
834
|
+
* unfinished work, not history; only dead letters read their full columns.
|
|
835
|
+
*/
|
|
836
|
+
const DEAD_LETTERS_LUA = `
|
|
837
|
+
local COUNTER, META, SPLIT = KEYS[2], KEYS[3], KEYS[5]
|
|
838
|
+
${META_LUA}
|
|
839
|
+
local counts = redis.call('MGET', COUNTER, SPLIT, CLEAR)
|
|
840
|
+
use_split(counts[2], counts[1], counts[3])
|
|
841
|
+
local candidates = {}
|
|
842
|
+
for _, field in ipairs(redis.call('ZRANGE', KEYS[4], 0, -1)) do
|
|
843
|
+
if tonumber(field) > boundary then candidates[#candidates + 1] = field end
|
|
844
|
+
end
|
|
845
|
+
local status = read_fields(candidates, { 'p', 'f' })
|
|
846
|
+
local dead = {}
|
|
847
|
+
for _, field in ipairs(candidates) do
|
|
848
|
+
if status[field][2] and not status[field][1] then dead[#dead + 1] = field end
|
|
849
|
+
end
|
|
850
|
+
local rows = read_fields(dead, { ${STORED_EVENT_META_SUFFIXES.map((suffix) => `'${suffix}'`).join(', ')} })
|
|
851
|
+
local result = {}
|
|
852
|
+
for _, field in ipairs(dead) do
|
|
853
|
+
local entries = redis.call('XRANGE', KEYS[1], field .. '-0', field .. '-0')
|
|
854
|
+
if #entries > 0 then
|
|
855
|
+
result[#result + 1] = entries[1]
|
|
856
|
+
result[#result + 1] = rows[field]
|
|
857
|
+
end
|
|
858
|
+
end
|
|
859
|
+
return result
|
|
860
|
+
`
|
|
861
|
+
|
|
718
862
|
/**
|
|
719
863
|
* KEYS: head, history, event counter, event meta, pin counts, metadata split.
|
|
720
864
|
* ARGV: nowMs, event pin key prefix, write count, then per write:
|
|
@@ -1117,22 +1261,6 @@ const fieldMap = (fields: string[]): Record<string, string> => {
|
|
|
1117
1261
|
return map
|
|
1118
1262
|
}
|
|
1119
1263
|
|
|
1120
|
-
const STORED_EVENT_META_SUFFIXES = [
|
|
1121
|
-
'p',
|
|
1122
|
-
'pa',
|
|
1123
|
-
'ri',
|
|
1124
|
-
'ch',
|
|
1125
|
-
'ce',
|
|
1126
|
-
'f',
|
|
1127
|
-
'fc',
|
|
1128
|
-
'lc',
|
|
1129
|
-
'lf',
|
|
1130
|
-
'lfa',
|
|
1131
|
-
'd',
|
|
1132
|
-
'a',
|
|
1133
|
-
'e',
|
|
1134
|
-
] as const
|
|
1135
|
-
|
|
1136
1264
|
const entryIndex = (entryId: string): number =>
|
|
1137
1265
|
Number(entryId.slice(0, entryId.indexOf('-')))
|
|
1138
1266
|
|
|
@@ -1240,6 +1368,46 @@ const toCause = (raw: string | undefined): EventCause | null => {
|
|
|
1240
1368
|
}
|
|
1241
1369
|
}
|
|
1242
1370
|
|
|
1371
|
+
type MetaSuffix = (typeof STORED_EVENT_META_SUFFIXES)[number]
|
|
1372
|
+
|
|
1373
|
+
/** A stream entry joined with its metadata columns; absent columns are unset. */
|
|
1374
|
+
const toStoredEvent = (
|
|
1375
|
+
sessionId: string,
|
|
1376
|
+
entry: StreamEntry,
|
|
1377
|
+
column: (suffix: MetaSuffix) => string | undefined,
|
|
1378
|
+
): StoredEvent => {
|
|
1379
|
+
const fields = fieldMap(entry[1])
|
|
1380
|
+
const date = (suffix: MetaSuffix): Date | null => {
|
|
1381
|
+
const value = column(suffix)
|
|
1382
|
+
return value === undefined ? null : new Date(Number(value))
|
|
1383
|
+
}
|
|
1384
|
+
const count = (suffix: MetaSuffix): number | null => {
|
|
1385
|
+
const value = column(suffix)
|
|
1386
|
+
return value === undefined ? null : Number(value)
|
|
1387
|
+
}
|
|
1388
|
+
const returnedEventIds = column('ri')
|
|
1389
|
+
return Object.assign(toEvent(sessionId, entry), {
|
|
1390
|
+
cause: toCause(fields['cause']),
|
|
1391
|
+
lane: fields['lane'] === '' ? null : fields['lane']!,
|
|
1392
|
+
processedAt: date('p'),
|
|
1393
|
+
processedByAttempt: count('pa'),
|
|
1394
|
+
returnedEventIds:
|
|
1395
|
+
returnedEventIds === undefined
|
|
1396
|
+
? null
|
|
1397
|
+
: decodeReturnedEventIds(returnedEventIds),
|
|
1398
|
+
firstClaimedAt: date('fc'),
|
|
1399
|
+
lastClaimedAt: date('lc'),
|
|
1400
|
+
attemptCount: count('d') ?? 0,
|
|
1401
|
+
claimHolder: column('ch') ?? null,
|
|
1402
|
+
claimExpiresAt: date('ce'),
|
|
1403
|
+
failureCount: count('a') ?? 0,
|
|
1404
|
+
lastFailedAt: date('lf'),
|
|
1405
|
+
lastFailedAttempt: count('lfa'),
|
|
1406
|
+
lastError: column('e') ?? null,
|
|
1407
|
+
failedAt: date('f'),
|
|
1408
|
+
})
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1243
1411
|
const toClaimAvailableResult = (
|
|
1244
1412
|
sessionId: string,
|
|
1245
1413
|
reply: unknown[],
|
|
@@ -1249,7 +1417,7 @@ const toClaimAvailableResult = (
|
|
|
1249
1417
|
const count = Number(reply[1])
|
|
1250
1418
|
const events: StoredEvent[] = []
|
|
1251
1419
|
for (let i = 0; i < count; i++) {
|
|
1252
|
-
const offset = 2 + i *
|
|
1420
|
+
const offset = 2 + i * 11
|
|
1253
1421
|
const entry = reply[offset + 9] as StreamEntry
|
|
1254
1422
|
const event = toEvent(sessionId, entry)
|
|
1255
1423
|
const fields = fieldMap(entry[1])
|
|
@@ -1264,10 +1432,17 @@ const toClaimAvailableResult = (
|
|
|
1264
1432
|
reply[offset + 3] === null
|
|
1265
1433
|
? null
|
|
1266
1434
|
: new Date(Number(reply[offset + 3])),
|
|
1267
|
-
lastClaimedAt:
|
|
1435
|
+
lastClaimedAt:
|
|
1436
|
+
reply[offset + 4] === null
|
|
1437
|
+
? null
|
|
1438
|
+
: new Date(Number(reply[offset + 4])),
|
|
1268
1439
|
attemptCount: Number(reply[offset]),
|
|
1269
|
-
claimHolder:
|
|
1270
|
-
|
|
1440
|
+
claimHolder:
|
|
1441
|
+
reply[offset + 7] === null ? null : String(reply[offset + 7]),
|
|
1442
|
+
claimExpiresAt:
|
|
1443
|
+
reply[offset + 8] === null
|
|
1444
|
+
? null
|
|
1445
|
+
: new Date(Number(reply[offset + 8])),
|
|
1271
1446
|
failureCount: Number(reply[offset + 1]),
|
|
1272
1447
|
lastFailedAt:
|
|
1273
1448
|
reply[offset + 5] === null
|
|
@@ -1277,11 +1452,14 @@ const toClaimAvailableResult = (
|
|
|
1277
1452
|
reply[offset + 6] === null ? null : Number(reply[offset + 6]),
|
|
1278
1453
|
lastError:
|
|
1279
1454
|
reply[offset + 2] === null ? null : String(reply[offset + 2]),
|
|
1280
|
-
failedAt:
|
|
1455
|
+
failedAt:
|
|
1456
|
+
reply[offset + 10] === null
|
|
1457
|
+
? null
|
|
1458
|
+
: new Date(Number(reply[offset + 10])),
|
|
1281
1459
|
}),
|
|
1282
1460
|
)
|
|
1283
1461
|
}
|
|
1284
|
-
const clearIndex = Number(reply[2 + count *
|
|
1462
|
+
const clearIndex = Number(reply[2 + count * 11] ?? 0)
|
|
1285
1463
|
if (clearIndex > 0)
|
|
1286
1464
|
for (const event of events) event.clearIndex = clearIndex
|
|
1287
1465
|
return {
|
|
@@ -1471,50 +1649,12 @@ export function createRedisStoreCore(
|
|
|
1471
1649
|
}
|
|
1472
1650
|
}
|
|
1473
1651
|
return entries.map((entry) => {
|
|
1474
|
-
const
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
const claimHolder = meta[`${idx}:ch`]
|
|
1481
|
-
const claimExpiresAt = meta[`${idx}:ce`]
|
|
1482
|
-
const failedAt = meta[`${idx}:f`]
|
|
1483
|
-
const firstClaimedAt = meta[`${idx}:fc`]
|
|
1484
|
-
const lastClaimedAt = meta[`${idx}:lc`]
|
|
1485
|
-
const lastFailedAt = meta[`${idx}:lf`]
|
|
1486
|
-
const lastFailedAttempt = meta[`${idx}:lfa`]
|
|
1487
|
-
return Object.assign(event, {
|
|
1488
|
-
cause: toCause(fields['cause']),
|
|
1489
|
-
lane: fields['lane'] === '' ? null : fields['lane']!,
|
|
1490
|
-
processedAt:
|
|
1491
|
-
processedAt === undefined ? null : new Date(Number(processedAt)),
|
|
1492
|
-
processedByAttempt:
|
|
1493
|
-
processedByAttempt === undefined ? null : Number(processedByAttempt),
|
|
1494
|
-
returnedEventIds:
|
|
1495
|
-
returnedEventIds === undefined
|
|
1496
|
-
? null
|
|
1497
|
-
: decodeReturnedEventIds(returnedEventIds),
|
|
1498
|
-
firstClaimedAt:
|
|
1499
|
-
firstClaimedAt === undefined
|
|
1500
|
-
? null
|
|
1501
|
-
: new Date(Number(firstClaimedAt)),
|
|
1502
|
-
lastClaimedAt:
|
|
1503
|
-
lastClaimedAt === undefined ? null : new Date(Number(lastClaimedAt)),
|
|
1504
|
-
attemptCount: Number(meta[`${idx}:d`] ?? 0),
|
|
1505
|
-
claimHolder: claimHolder ?? null,
|
|
1506
|
-
claimExpiresAt:
|
|
1507
|
-
claimExpiresAt === undefined
|
|
1508
|
-
? null
|
|
1509
|
-
: new Date(Number(claimExpiresAt)),
|
|
1510
|
-
failureCount: Number(meta[`${idx}:a`] ?? 0),
|
|
1511
|
-
lastFailedAt:
|
|
1512
|
-
lastFailedAt === undefined ? null : new Date(Number(lastFailedAt)),
|
|
1513
|
-
lastFailedAttempt:
|
|
1514
|
-
lastFailedAttempt === undefined ? null : Number(lastFailedAttempt),
|
|
1515
|
-
lastError: meta[`${idx}:e`] ?? null,
|
|
1516
|
-
failedAt: failedAt === undefined ? null : new Date(Number(failedAt)),
|
|
1517
|
-
})
|
|
1652
|
+
const idx = entryIndex(entry[0])
|
|
1653
|
+
return toStoredEvent(
|
|
1654
|
+
sessionId,
|
|
1655
|
+
entry,
|
|
1656
|
+
(suffix) => meta[`${idx}:${suffix}`],
|
|
1657
|
+
)
|
|
1518
1658
|
})
|
|
1519
1659
|
}
|
|
1520
1660
|
|
|
@@ -1858,6 +1998,7 @@ export function createRedisStoreCore(
|
|
|
1858
1998
|
ttlMs,
|
|
1859
1999
|
expiresAtMs,
|
|
1860
2000
|
excludeIndexes,
|
|
2001
|
+
maxFailures,
|
|
1861
2002
|
}) {
|
|
1862
2003
|
return wrap(async () => {
|
|
1863
2004
|
const now = clock.now().getTime()
|
|
@@ -1872,8 +2013,17 @@ export function createRedisStoreCore(
|
|
|
1872
2013
|
pendingKey(sessionId),
|
|
1873
2014
|
readyKey(sessionId),
|
|
1874
2015
|
metaSplitKey(sessionId),
|
|
2016
|
+
snapshotPinCountsKey(sessionId),
|
|
2017
|
+
],
|
|
2018
|
+
[
|
|
2019
|
+
holder,
|
|
2020
|
+
now,
|
|
2021
|
+
expiresAt,
|
|
2022
|
+
excluded.length,
|
|
2023
|
+
...excluded,
|
|
2024
|
+
maxFailures ?? '',
|
|
2025
|
+
snapshotEventPinsPrefix(sessionId),
|
|
1875
2026
|
],
|
|
1876
|
-
[holder, now, expiresAt, excluded.length, ...excluded],
|
|
1877
2027
|
)) as unknown[]
|
|
1878
2028
|
return toClaimAvailableResult(sessionId, reply)
|
|
1879
2029
|
})
|
|
@@ -2009,8 +2159,16 @@ export function createRedisStoreCore(
|
|
|
2009
2159
|
})
|
|
2010
2160
|
},
|
|
2011
2161
|
|
|
2012
|
-
async failAttempt({
|
|
2162
|
+
async failAttempt({
|
|
2163
|
+
sessionId,
|
|
2164
|
+
index,
|
|
2165
|
+
attempt,
|
|
2166
|
+
error,
|
|
2167
|
+
maxFailures,
|
|
2168
|
+
retryAfterMs,
|
|
2169
|
+
}) {
|
|
2013
2170
|
return wrap(async () => {
|
|
2171
|
+
const now = clock.now().getTime()
|
|
2014
2172
|
const reply = (await evalScript(
|
|
2015
2173
|
FAIL_ATTEMPT_LUA,
|
|
2016
2174
|
[
|
|
@@ -2022,7 +2180,14 @@ export function createRedisStoreCore(
|
|
|
2022
2180
|
snapshotPinCountsKey(sessionId),
|
|
2023
2181
|
metaSplitKey(sessionId),
|
|
2024
2182
|
],
|
|
2025
|
-
[
|
|
2183
|
+
[
|
|
2184
|
+
index,
|
|
2185
|
+
attempt,
|
|
2186
|
+
error,
|
|
2187
|
+
maxFailures,
|
|
2188
|
+
now,
|
|
2189
|
+
retryAfterMs === undefined ? '' : now + retryAfterMs,
|
|
2190
|
+
],
|
|
2026
2191
|
)) as string[]
|
|
2027
2192
|
const outcome = reply[0]
|
|
2028
2193
|
if (outcome === 'cleared')
|
|
@@ -2039,6 +2204,53 @@ export function createRedisStoreCore(
|
|
|
2039
2204
|
})
|
|
2040
2205
|
},
|
|
2041
2206
|
|
|
2207
|
+
async deadLetters(sessionId) {
|
|
2208
|
+
return wrap(async () => {
|
|
2209
|
+
const reply = (await evalScript(
|
|
2210
|
+
DEAD_LETTERS_LUA,
|
|
2211
|
+
[
|
|
2212
|
+
storeKey(sessionId),
|
|
2213
|
+
countKey(sessionId),
|
|
2214
|
+
metaKey(sessionId),
|
|
2215
|
+
pendingKey(sessionId),
|
|
2216
|
+
metaSplitKey(sessionId),
|
|
2217
|
+
],
|
|
2218
|
+
[],
|
|
2219
|
+
)) as unknown[]
|
|
2220
|
+
const events: StoredEvent[] = []
|
|
2221
|
+
for (let i = 0; i + 1 < reply.length; i += 2) {
|
|
2222
|
+
const columns = reply[i + 1] as Array<string | null>
|
|
2223
|
+
events.push(
|
|
2224
|
+
toStoredEvent(
|
|
2225
|
+
sessionId,
|
|
2226
|
+
reply[i] as StreamEntry,
|
|
2227
|
+
(suffix) =>
|
|
2228
|
+
columns[STORED_EVENT_META_SUFFIXES.indexOf(suffix)] ??
|
|
2229
|
+
undefined,
|
|
2230
|
+
),
|
|
2231
|
+
)
|
|
2232
|
+
}
|
|
2233
|
+
return events
|
|
2234
|
+
})
|
|
2235
|
+
},
|
|
2236
|
+
|
|
2237
|
+
async redrive({ sessionId, index, action }) {
|
|
2238
|
+
return wrap(async () => {
|
|
2239
|
+
const [redriven, hasPending] = (await evalScript(
|
|
2240
|
+
REDRIVE_LUA,
|
|
2241
|
+
[
|
|
2242
|
+
countKey(sessionId),
|
|
2243
|
+
metaKey(sessionId),
|
|
2244
|
+
pendingKey(sessionId),
|
|
2245
|
+
readyKey(sessionId),
|
|
2246
|
+
metaSplitKey(sessionId),
|
|
2247
|
+
],
|
|
2248
|
+
[index, action, clock.now().getTime()],
|
|
2249
|
+
)) as [string, string]
|
|
2250
|
+
return { redriven: redriven === '1', hasPending: hasPending === '1' }
|
|
2251
|
+
})
|
|
2252
|
+
},
|
|
2253
|
+
|
|
2042
2254
|
async readState(sessionId, reducerName, stateOptions) {
|
|
2043
2255
|
return wrap(() =>
|
|
2044
2256
|
retryAfterGap(async () => {
|
|
@@ -292,6 +292,8 @@ export function createRedisNotifyStore(options: {
|
|
|
292
292
|
putSnapshots: core.putSnapshots,
|
|
293
293
|
clear: core.clear!,
|
|
294
294
|
reclaim: core.reclaim!,
|
|
295
|
+
deadLetters: core.deadLetters!,
|
|
296
|
+
redrive: core.redrive!,
|
|
295
297
|
|
|
296
298
|
presence: {
|
|
297
299
|
async set(ns, participant, values, meta) {
|