experimental-a2 0.21.1 → 0.23.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 +32 -0
- package/dist/{actor-HnGmrOZZ.d.ts → actor-CqibLgjQ.d.ts} +13 -5
- package/dist/actor-CqibLgjQ.d.ts.map +1 -0
- package/dist/actor-client.d.ts +1 -1
- package/dist/actor-react.d.ts +1 -1
- package/dist/actor-react.js +1 -1
- package/dist/actor-shared-DFET5q16.js.map +1 -1
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +3 -3
- package/dist/{ai-CvEnqVfo.d.ts → ai-DAu77CDr.d.ts} +2 -2
- package/dist/{ai-CvEnqVfo.d.ts.map → ai-DAu77CDr.d.ts.map} +1 -1
- package/dist/{ai-CzE672Q3.js → ai-EEfKyc-z.js} +2 -2
- package/dist/{ai-CzE672Q3.js.map → ai-EEfKyc-z.js.map} +1 -1
- package/dist/ai-client.d.ts +1 -1
- package/dist/ai-client.js +2 -2
- package/dist/ai-fx.d.ts +4 -2
- package/dist/ai-fx.d.ts.map +1 -1
- package/dist/ai-fx.js +234 -62
- package/dist/ai-fx.js.map +1 -1
- package/dist/{ai-harness-Bfk-Lycn.d.ts → ai-harness-DTjkAr2T.d.ts} +2 -2
- package/dist/{ai-harness-Bfk-Lycn.d.ts.map → ai-harness-DTjkAr2T.d.ts.map} +1 -1
- package/dist/ai-harness-dQeJaR1t.js.map +1 -1
- package/dist/ai-pi.d.ts +1 -1
- package/dist/ai-pi.js +1 -1
- package/dist/ai-react.d.ts +1 -1
- package/dist/ai-react.js +2 -2
- package/dist/ai-server.d.ts +3 -3
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +10 -4
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +1 -1
- package/dist/ai.js +1 -1
- package/dist/{client-9IeTsiAs.js → client-BQS4i_V9.js} +27 -4
- package/dist/client-BQS4i_V9.js.map +1 -0
- package/dist/client-CUMbS_38.d.ts.map +1 -1
- package/dist/client.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/{internal-Bc4PvfHk.js → internal-CvsrprnU.js} +32 -15
- package/dist/{internal-Bc4PvfHk.js.map → internal-CvsrprnU.js.map} +1 -1
- package/dist/react.js +1 -1
- package/dist/{runtime-consumer-BWmht60z.js → runtime-consumer-CIeL33wj.js} +2 -2
- package/dist/{runtime-consumer-BWmht60z.js.map → runtime-consumer-CIeL33wj.js.map} +1 -1
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.js +2 -2
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.js +2 -2
- package/dist/{server-DvIB8kj5.js → server-DtFrWxi3.js} +30 -8
- package/dist/server-DtFrWxi3.js.map +1 -0
- package/dist/{server-BshFZ0eL.d.ts → server-qgez0Xny.d.ts} +3 -3
- package/dist/{server-BshFZ0eL.d.ts.map → server-qgez0Xny.d.ts.map} +1 -1
- package/dist/server.d.ts +2 -2
- package/dist/server.js +1 -1
- package/dist/{session-clear-Bk_29eEw.d.ts → session-clear-D1PFe7xl.d.ts} +2 -2
- package/dist/session-clear-D1PFe7xl.d.ts.map +1 -0
- package/dist/{store-Z2qjw50n.d.ts → store-D0MhBvg6.d.ts} +15 -6
- package/dist/store-D0MhBvg6.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 +15 -17
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-DYNyxCev.js → store-polling-BZt2Rf3D.js} +2 -2
- package/dist/{store-polling-DYNyxCev.js.map → store-polling-BZt2Rf3D.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 +31 -40
- package/dist/store-postgres.js.map +1 -1
- package/dist/store-presence-polling-JuAy2Ck2.js +111 -0
- package/dist/store-presence-polling-JuAy2Ck2.js.map +1 -0
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +3 -3
- package/dist/{store-redis-notify-IiVdlYHc.js → store-redis-notify-CBDVJxA_.js} +545 -399
- package/dist/store-redis-notify-CBDVJxA_.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 +3 -4
- 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 +16 -18
- package/dist/store-sqlite.js.map +1 -1
- package/dist/store-valkey.d.ts +33 -0
- package/dist/store-valkey.d.ts.map +1 -0
- package/dist/store-valkey.js +218 -0
- package/dist/store-valkey.js.map +1 -0
- package/dist/stream-buffer-DrTyiIuo.js +57 -0
- package/dist/stream-buffer-DrTyiIuo.js.map +1 -0
- package/dist/{wire-CZGqNZRx.js → wire-CV-Z7wz-.js} +3 -3
- package/dist/{wire-CZGqNZRx.js.map → wire-CV-Z7wz-.js.map} +1 -1
- package/docs/actors/01-introduction.mdx +5 -2
- package/docs/agents/03-conversations.mdx +9 -8
- package/docs/agents/04-harnesses.mdx +7 -4
- package/docs/client/03-presence.mdx +1 -0
- package/docs/reference/01-core.mdx +4 -2
- package/docs/reference/02-server.mdx +5 -1
- package/docs/reference/04-ai.mdx +13 -4
- package/docs/reference/06-errors.mdx +1 -1
- package/docs/server/01-consumers.mdx +4 -1
- package/docs/server/06-guarantees.mdx +7 -4
- package/docs/stores/01-overview.mdx +1 -0
- package/docs/stores/02-redis.mdx +18 -10
- package/docs/stores/03-redis-http.mdx +6 -4
- package/docs/stores/04-postgres.mdx +2 -2
- package/docs/stores/07-valkey.mdx +97 -0
- package/docs/upgrades/01-overview.mdx +1 -0
- package/docs/upgrades/78-0-22.mdx +115 -0
- package/examples/playground/app/a0/sandbox.ts +58 -13
- package/examples/playground/app/a2slack/db/a2.ts +7 -4
- package/examples/playground/app/a2slack/db/migrations/0004_a2_session_event_ids.sql +2 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0004_snapshot.json +1165 -0
- package/examples/playground/app/a2slack/db/migrations/meta/_journal.json +8 -1
- package/examples/playground/app/chat/server.ts +2 -2
- package/examples/playground/lib/store.ts +1 -1
- package/examples/playground/package.json +1 -1
- package/package.json +8 -2
- package/sql/postgres/drizzle-schema.ts +7 -4
- package/sql/postgres/schema.sql +24 -3
- package/src/actor-shared.ts +10 -2
- package/src/ai-agent.ts +13 -0
- package/src/ai-fx.ts +365 -78
- package/src/ai-harness.ts +7 -0
- package/src/client.ts +19 -2
- package/src/deterministic-id.ts +1 -1
- package/src/internal.ts +42 -11
- package/src/libfx.d.ts +2 -0
- package/src/presence-recovery.ts +18 -11
- package/src/presence-tombstones.ts +52 -0
- package/src/push-envelope.ts +2 -2
- package/src/server.ts +49 -15
- package/src/store-memory.ts +24 -32
- package/src/store-postgres.ts +43 -53
- package/src/store-presence-polling.ts +59 -34
- package/src/store-redis-core.ts +595 -439
- package/src/store-redis-notify.ts +82 -28
- package/src/store-redis.ts +2 -3
- package/src/store-sqlite.ts +34 -40
- package/src/store-valkey.ts +376 -0
- package/src/store.ts +14 -5
- package/dist/actor-HnGmrOZZ.d.ts.map +0 -1
- package/dist/client-9IeTsiAs.js.map +0 -1
- package/dist/server-DvIB8kj5.js.map +0 -1
- package/dist/session-clear-Bk_29eEw.d.ts.map +0 -1
- package/dist/store-Z2qjw50n.d.ts.map +0 -1
- package/dist/store-presence-polling-CzQ0wdtM.js +0 -94
- package/dist/store-presence-polling-CzQ0wdtM.js.map +0 -1
- package/dist/store-redis-notify-IiVdlYHc.js.map +0 -1
- package/dist/stream-buffer-Cx7AAPGq.js +0 -23
- package/dist/stream-buffer-Cx7AAPGq.js.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"session-clear-Bk_29eEw.d.ts","names":[],"sources":["../src/session-clear.ts"],"mappings":";;;cAOa;;KAGD,iBAAiB;EAC3B,aAAa;EACb"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"store-Z2qjw50n.d.ts","names":[],"sources":["../src/store.ts"],"mappings":";;;KAaY;EACV;EACA;EACA;;EAEA;EACA;EACA,WAAW;;;KAID;EACV;;EAEA;EACA;EACA;;EAEA;;;KAIU,cAAc;;EAExB,OAAO;;;;;;;;;KAUG;EACV;EACA;;EAEA,gBAAgB;;EAEhB,eAAe;;EAEf;;EAEA;;;;;EAKA,gBAAgB;;;;;EAKhB;;EAEA,cAAc;;EAEd;;EAEA;;EAEA,UAAU;;;KAIA,eAAe;;EAEzB;;EAEA;;;KAIU,YAAY,cACtB;;EAEE;;;KAIQ;EACV;EACA;EACA;;;;;;;KAQU;EACV;;;;;;;;EAQA;IACE;IACA;IACA;;;;;;;;;;KAWQ;EACV;EACA;EACA;EACA;EACA,IAAI;EACJ,WAAW;;;KAID;;EAEV;;EAEA;IAAY;IAAe;;;EAE3B,QAAQ;;EAER;;KAGU;EACV;EACA;;EAEA,gBAAgB;;KAGN;EACV;EACA;EACA;;;KAIU;EAEN;EACA,QAAQ;;EAER;;EAEA;EAAiB,SAAS;;EAC1B;;;KAGM;EACR;EAAsB,QAAQ;;EAAoB;;;KAG1C;;EAEV,SAAS;;EAET,YAAY;;;KAIF;EACV;EACA;;;KAIU;EACV;;EAEA;;;KAIU;EACV;EACA;;EAEA;;EAEA,QAAQ;;;;;;EAMR,mBAAmB;;;KAIT,gBAAgB;EAAgB;;;KAGhC;EACV,QAAQ;;;;;;EAMR;;KAGU;EACV;EACA;;EAEA,QAAQ;;KAGE;EACV;;EAEA;;EAEA;;KAGU;EAAuB;EAAiB;;KAExC;EACV;EACA;EACA;;EAEA;EACA,qBAAqB;;KAGX;;EAEV;;EAEA;;;;;;;KAQU;EACV,OAAO;;;KAIG;;;;;;;;;;;;;;;UAsBK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAkCf,OACE,mBACA,QAAQ,eACR;IACE;IACA,qBAAqB;MAEtB,QAAQ;;;;;;;;EASX,OAAO,SAAS,oBAAoB,QAAQ;;;;;;EAO5C,SAAS,oBAAoB,QAAQ;;;;;EAMrC,aAAa,oBAAoB,QAAQ;;;;;;;;;;;EAYzC,SAAS,SAAS,sBAAsB,QAAQ;;;;;;;;;;;;;;;;EAiBhD,QAAQ;IACN;IACA,UAAU;MAAkB,KAAK,YAAY;;MAC3C;IAAU,QAAQ;IAAc;;;;;;;;EAQpC,QAAQ,oBAAoB,QAAQ;;EAGpC,KACE,mBACA;IAAS;IAAqB;MAC7B,QAAQ;;EAGX,aACE,mBACA;IAAS;IAAqB;MAC7B,QAAQ;;;;;;;;EASX,eAAe;IACb;IACA;IACA;IACA;IACA,mBAAmB;IACnB,qBAAqB;;;;;;;;;IASrB;;;;;IAKA;MACE,QAAQ;;;;;;;;;;EAWZ,YAAY;IACV;IACA;IACA,kBAAkB;MAAY;;IAC9B;IACA;MACE,QAAQ;;;;;;;;EASZ,gBAAgB;IACd;IACA;IACA;IACA;IACA;IACA,QAAQ;IACR,qBAAqB;MACnB,QAAQ;;;;;;;;;EAUZ,YAAY;IACV;IACA;IACA;IACA;IACA;IACA;IACA;IACA;MACE,QAAQ;;;;;;;;EASZ,UACE,mBACA,qBACA;IAAY;IAAuB;MAClC,QAAQ;;;;;;;;EASX,YACE,mBAAmB,yBACnB,sBACC,QAAQ;;;;;;;;EASX,aACE,mBACA,qBACA,oBAAoB,uBACnB;;;;;;;EAQH;;;;;;;;;IASE,IACE,YACA,qBACA,QAAQ,gCACR;MAAQ;MAAc,IAAI;MAAM;QAC/B;;IAGH,KAAK,aAAa,QAAQ;;;;;;IAO1B,WAAW,YAAY,UAAU,OAAO;;;;;;;;;EAU1C,OACE,mBACA;IAAS;MACR,cAAc"}
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { i as POLL_TIMINGS } from "./internal-Bc4PvfHk.js";
|
|
2
|
-
import { t as defaultSleep } from "./store-polling-DYNyxCev.js";
|
|
3
|
-
//#region src/store-presence-polling.ts
|
|
4
|
-
const key = ({ participant, field }) => JSON.stringify([participant, field]);
|
|
5
|
-
function pollPresence({ read, deliver, sleep = defaultSleep }) {
|
|
6
|
-
let stopped = false;
|
|
7
|
-
let pending;
|
|
8
|
-
let interval = Math.min(POLL_TIMINGS.activeFloorMs, POLL_TIMINGS.idleCeilingMs);
|
|
9
|
-
let version = 0;
|
|
10
|
-
const seen = /* @__PURE__ */ new Map();
|
|
11
|
-
const activate = () => {
|
|
12
|
-
version += 1;
|
|
13
|
-
interval = Math.min(POLL_TIMINGS.activeFloorMs, POLL_TIMINGS.idleCeilingMs);
|
|
14
|
-
pending?.cancel();
|
|
15
|
-
};
|
|
16
|
-
const ready = read().then((rows) => {
|
|
17
|
-
if (stopped) return;
|
|
18
|
-
for (const row of rows) seen.set(key(row), row);
|
|
19
|
-
}).catch(() => void 0);
|
|
20
|
-
const run = async () => {
|
|
21
|
-
await ready;
|
|
22
|
-
for (;;) {
|
|
23
|
-
if (stopped) return;
|
|
24
|
-
const before = version;
|
|
25
|
-
pending = sleep(interval);
|
|
26
|
-
await pending.promise;
|
|
27
|
-
if (stopped) return;
|
|
28
|
-
try {
|
|
29
|
-
const reading = version;
|
|
30
|
-
const rows = await read();
|
|
31
|
-
if (stopped) return;
|
|
32
|
-
if (version !== reading) continue;
|
|
33
|
-
const live = /* @__PURE__ */ new Set();
|
|
34
|
-
let changed = false;
|
|
35
|
-
for (const row of rows) {
|
|
36
|
-
const id = key(row);
|
|
37
|
-
live.add(id);
|
|
38
|
-
const prior = seen.get(id);
|
|
39
|
-
seen.set(id, row);
|
|
40
|
-
if (prior?.seen === row.seen && prior.at.getTime() === row.at.getTime() && JSON.stringify(prior.value) === JSON.stringify(row.value)) continue;
|
|
41
|
-
changed = true;
|
|
42
|
-
deliver({
|
|
43
|
-
participant: row.participant,
|
|
44
|
-
values: { [row.field]: row.value },
|
|
45
|
-
seen: row.seen,
|
|
46
|
-
at: row.at
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
for (const [id, row] of seen) {
|
|
50
|
-
if (live.has(id)) continue;
|
|
51
|
-
seen.delete(id);
|
|
52
|
-
changed = true;
|
|
53
|
-
deliver({
|
|
54
|
-
participant: row.participant,
|
|
55
|
-
values: { [row.field]: null },
|
|
56
|
-
seen: row.seen,
|
|
57
|
-
at: /* @__PURE__ */ new Date()
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
if (changed) activate();
|
|
61
|
-
else if (version === before) interval = Math.min(interval * 2, POLL_TIMINGS.idleCeilingMs);
|
|
62
|
-
} catch {
|
|
63
|
-
interval = POLL_TIMINGS.idleCeilingMs;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
run();
|
|
68
|
-
return {
|
|
69
|
-
ready,
|
|
70
|
-
wake: activate,
|
|
71
|
-
update(patch) {
|
|
72
|
-
for (const [field, value] of Object.entries(patch.values)) seen.set(key({
|
|
73
|
-
participant: patch.participant,
|
|
74
|
-
field
|
|
75
|
-
}), {
|
|
76
|
-
participant: patch.participant,
|
|
77
|
-
field,
|
|
78
|
-
value,
|
|
79
|
-
seen: patch.seen,
|
|
80
|
-
at: patch.at
|
|
81
|
-
});
|
|
82
|
-
activate();
|
|
83
|
-
},
|
|
84
|
-
stop() {
|
|
85
|
-
stopped = true;
|
|
86
|
-
pending?.cancel();
|
|
87
|
-
seen.clear();
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
//#endregion
|
|
92
|
-
export { pollPresence as t };
|
|
93
|
-
|
|
94
|
-
//# sourceMappingURL=store-presence-polling-CzQ0wdtM.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"store-presence-polling-CzQ0wdtM.js","names":[],"sources":["../src/store-presence-polling.ts"],"sourcesContent":["import type { PresencePatch } from './contract.ts'\nimport { POLL_TIMINGS } from './internal.ts'\nimport { defaultSleep, type Sleeper } from './store-polling.ts'\nimport type { PresenceRow } from './store.ts'\n\nconst key = ({\n participant,\n field,\n}: Pick<PresenceRow, 'participant' | 'field'>): string =>\n JSON.stringify([participant, field])\n\nexport function pollPresence({\n read,\n deliver,\n sleep = defaultSleep,\n}: {\n read: () => Promise<PresenceRow[]>\n deliver: (patch: PresencePatch) => void\n sleep?: (ms: number) => Sleeper\n}): {\n ready: Promise<void>\n update(patch: PresencePatch): void\n wake(): void\n stop(): void\n} {\n let stopped = false\n let pending: Sleeper | undefined\n let interval = Math.min(\n POLL_TIMINGS.activeFloorMs,\n POLL_TIMINGS.idleCeilingMs,\n )\n let version = 0\n const seen = new Map<string, Omit<PresenceRow, 'expiresAt'>>()\n const activate = (): void => {\n version += 1\n interval = Math.min(POLL_TIMINGS.activeFloorMs, POLL_TIMINGS.idleCeilingMs)\n pending?.cancel()\n }\n const ready = read()\n .then((rows) => {\n if (stopped) return\n for (const row of rows) seen.set(key(row), row)\n return undefined\n })\n // Seeding is best effort; foreground snapshots still report read errors.\n .catch(() => undefined)\n\n const run = async (): Promise<void> => {\n await ready\n for (;;) {\n if (stopped) return\n const before = version\n pending = sleep(interval)\n // oxlint-disable-next-line no-await-in-loop -- adaptive presence polling\n await pending.promise\n if (stopped) return\n try {\n const reading = version\n // oxlint-disable-next-line no-await-in-loop -- read each new presence snapshot\n const rows = await read()\n if (stopped) return\n if (version !== reading) continue\n const live = new Set<string>()\n let changed = false\n for (const row of rows) {\n const id = key(row)\n live.add(id)\n const prior = seen.get(id)\n seen.set(id, row)\n if (\n prior?.seen === row.seen &&\n prior.at.getTime() === row.at.getTime() &&\n JSON.stringify(prior.value) === JSON.stringify(row.value)\n )\n continue\n changed = true\n deliver({\n participant: row.participant,\n values: { [row.field]: row.value },\n seen: row.seen,\n at: row.at,\n })\n }\n for (const [id, row] of seen) {\n if (live.has(id)) continue\n seen.delete(id)\n changed = true\n deliver({\n participant: row.participant,\n values: { [row.field]: null },\n seen: row.seen,\n at: new Date(),\n })\n }\n if (changed) activate()\n else if (version === before)\n interval = Math.min(interval * 2, POLL_TIMINGS.idleCeilingMs)\n } catch {\n interval = POLL_TIMINGS.idleCeilingMs\n }\n }\n }\n void run()\n return {\n ready,\n wake: activate,\n update(patch) {\n for (const [field, value] of Object.entries(patch.values)) {\n seen.set(key({ participant: patch.participant, field }), {\n participant: patch.participant,\n field,\n value,\n seen: patch.seen,\n at: patch.at,\n })\n }\n activate()\n },\n stop() {\n stopped = true\n pending?.cancel()\n seen.clear()\n },\n }\n}\n"],"mappings":";;;AAKA,MAAM,OAAO,EACX,aACA,YAEA,KAAK,UAAU,CAAC,aAAa,KAAK,CAAC;AAErC,SAAgB,aAAa,EAC3B,MACA,SACA,QAAQ,gBAUR;CACA,IAAI,UAAU;CACd,IAAI;CACJ,IAAI,WAAW,KAAK,IAClB,aAAa,eACb,aAAa,aACf;CACA,IAAI,UAAU;CACd,MAAM,uBAAO,IAAI,IAA4C;CAC7D,MAAM,iBAAuB;EAC3B,WAAW;EACX,WAAW,KAAK,IAAI,aAAa,eAAe,aAAa,aAAa;EAC1E,SAAS,OAAO;CAClB;CACA,MAAM,QAAQ,KAAK,CAAC,CACjB,MAAM,SAAS;EACd,IAAI,SAAS;EACb,KAAK,MAAM,OAAO,MAAM,KAAK,IAAI,IAAI,GAAG,GAAG,GAAG;CAEhD,CAAC,CAAC,CAED,YAAY,KAAA,CAAS;CAExB,MAAM,MAAM,YAA2B;EACrC,MAAM;EACN,SAAS;GACP,IAAI,SAAS;GACb,MAAM,SAAS;GACf,UAAU,MAAM,QAAQ;GAExB,MAAM,QAAQ;GACd,IAAI,SAAS;GACb,IAAI;IACF,MAAM,UAAU;IAEhB,MAAM,OAAO,MAAM,KAAK;IACxB,IAAI,SAAS;IACb,IAAI,YAAY,SAAS;IACzB,MAAM,uBAAO,IAAI,IAAY;IAC7B,IAAI,UAAU;IACd,KAAK,MAAM,OAAO,MAAM;KACtB,MAAM,KAAK,IAAI,GAAG;KAClB,KAAK,IAAI,EAAE;KACX,MAAM,QAAQ,KAAK,IAAI,EAAE;KACzB,KAAK,IAAI,IAAI,GAAG;KAChB,IACE,OAAO,SAAS,IAAI,QACpB,MAAM,GAAG,QAAQ,MAAM,IAAI,GAAG,QAAQ,KACtC,KAAK,UAAU,MAAM,KAAK,MAAM,KAAK,UAAU,IAAI,KAAK,GAExD;KACF,UAAU;KACV,QAAQ;MACN,aAAa,IAAI;MACjB,QAAQ,GAAG,IAAI,QAAQ,IAAI,MAAM;MACjC,MAAM,IAAI;MACV,IAAI,IAAI;KACV,CAAC;IACH;IACA,KAAK,MAAM,CAAC,IAAI,QAAQ,MAAM;KAC5B,IAAI,KAAK,IAAI,EAAE,GAAG;KAClB,KAAK,OAAO,EAAE;KACd,UAAU;KACV,QAAQ;MACN,aAAa,IAAI;MACjB,QAAQ,GAAG,IAAI,QAAQ,KAAK;MAC5B,MAAM,IAAI;MACV,oBAAI,IAAI,KAAK;KACf,CAAC;IACH;IACA,IAAI,SAAS,SAAS;SACjB,IAAI,YAAY,QACnB,WAAW,KAAK,IAAI,WAAW,GAAG,aAAa,aAAa;GAChE,QAAQ;IACN,WAAW,aAAa;GAC1B;EACF;CACF;CACA,IAAS;CACT,OAAO;EACL;EACA,MAAM;EACN,OAAO,OAAO;GACZ,KAAK,MAAM,CAAC,OAAO,UAAU,OAAO,QAAQ,MAAM,MAAM,GACtD,KAAK,IAAI,IAAI;IAAE,aAAa,MAAM;IAAa;GAAM,CAAC,GAAG;IACvD,aAAa,MAAM;IACnB;IACA;IACA,MAAM,MAAM;IACZ,IAAI,MAAM;GACZ,CAAC;GAEH,SAAS;EACX;EACA,OAAO;GACL,UAAU;GACV,SAAS,OAAO;GAChB,KAAK,MAAM;EACb;CACF;AACF"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"store-redis-notify-IiVdlYHc.js","names":[],"sources":["../src/presence-recovery.ts","../src/store-redis-core.ts","../src/store-redis-notify.ts"],"sourcesContent":["import type { PresencePatch } from './contract.ts'\nimport type { PresenceRow } from './store.ts'\n\ntype Field = Pick<PresenceRow, 'participant' | 'field' | 'seen' | 'at'>\nconst key = ({\n participant,\n field,\n}: Pick<Field, 'participant' | 'field'>): string =>\n JSON.stringify([participant, field])\n\nexport function presenceRecovery({\n read,\n deliver,\n isActive,\n}: {\n read: () => Promise<PresenceRow[]>\n deliver: (patch: PresencePatch) => void\n isActive: () => boolean\n}): {\n read(): Promise<PresenceRow[]>\n reconnect(): Promise<void>\n update(patch: PresencePatch): void\n} {\n const fields = new Map<string, Field>()\n const reading = new Set<Set<string>>()\n let generation = 0\n let latest: Promise<PresenceRow[]> | undefined\n\n const remember = ({ participant, field, seen, at }: Field): void => {\n const id = key({ participant, field })\n const previous = fields.get(id)\n if (\n !previous ||\n previous.at.getTime() < at.getTime() ||\n (previous.at.getTime() === at.getTime() && previous.seen !== seen)\n )\n fields.set(id, { participant, field, seen, at })\n }\n const snapshot = async ({\n reconcile,\n }: {\n reconcile: boolean\n }): Promise<PresenceRow[]> => {\n const run = generation\n const before = new Map(fields)\n const changed = new Set<string>()\n reading.add(changed)\n try {\n const rows = await read()\n if (!isActive()) return rows\n if (run !== generation) return latest!\n const live = new Set<string>()\n for (const row of rows) {\n const id = key(row)\n live.add(id)\n if (changed.has(id)) continue\n remember(row)\n if (reconcile)\n deliver({\n participant: row.participant,\n values: { [row.field]: row.value },\n seen: row.seen,\n at: row.at,\n })\n }\n if (reconcile) {\n for (const [id, prior] of before) {\n if (live.has(id) || changed.has(id) || fields.get(id) !== prior)\n continue\n fields.delete(id)\n deliver({\n participant: prior.participant,\n values: { [prior.field]: null },\n seen: prior.seen,\n at: prior.at,\n })\n }\n }\n return rows\n } finally {\n reading.delete(changed)\n if (reading.size === 0) latest = undefined\n }\n }\n\n return {\n async read() {\n let pending = latest\n while (pending) {\n // oxlint-disable-next-line no-await-in-loop -- follow replacement reconnects before starting a fresh read\n await pending\n if (latest === pending) break\n pending = latest\n }\n return snapshot({ reconcile: false })\n },\n async reconnect() {\n generation += 1\n latest = snapshot({ reconcile: true })\n await latest\n },\n update(patch) {\n for (const [field, value] of Object.entries(patch.values)) {\n if (value === undefined) continue\n const id = key({ participant: patch.participant, field })\n const previous = fields.get(id)\n if (previous && previous.at.getTime() > patch.at.getTime()) continue\n for (const changed of reading) changed.add(id)\n if (value === null) fields.delete(id)\n else\n remember({\n participant: patch.participant,\n field,\n seen: patch.seen,\n at: patch.at,\n })\n }\n },\n }\n}\n","/**\n * The shared Redis store core — every operation except `stream` and\n * `close`, expressed over one injected `call` function so both\n * transports reuse it: `store-redis` (Redis protocol via `ioredis`) and\n * `store-redis-http` (provider REST APIs).\n *\n * The stream IS the store: each event is a stream entry whose ID is its\n * dense a2 index (`${index}-0`), so `XRANGE` is the catch-up read. Stream\n * entries are immutable; dispatch bookkeeping lives on consumer cursors.\n * Every write path is one atomic Lua script.\n *\n * The cursor layout, per session `{p}:{s}:`:\n *\n * - `heads:{chunk}` holds one row per (head index, consumer) in hashes of\n * META_CHUNK_SIZE consecutive indexes: a head's attempt and failure\n * fields, its kind (`l`), its lane successor (`next`), its snapshot pins,\n * and, once the cursor moves past it, its completion record. A new head\n * is a new row, so moving a cursor resets its attempt fields for free,\n * and an event cursor costs one row, as a handled event did.\n * - `cursors` is one hash of every lane and log cursor: its head (`h`),\n * position (`p`), lane tail (`t`), or log types (`ty`). A lane's queue is\n * linked through the rows' `next` fields.\n * - `runnable` is a zset of every cursor with a head, scored by the head's\n * index, negated while the head is dead-lettered.\n * - `pins` counts, per snapshot, the heads that pin it.\n *\n * A cursor id names a cursor within its session: the consumer's byte\n * length, `:`, the consumer, then `#{index}` for an event cursor, `L{lane}`\n * for a lane cursor, or `G{lane}` for a log cursor. A clear supersedes every\n * head at or below its marker at once (the `runnable` set is dropped); lane and\n * log cursor records are reset lazily against the marker and swept by\n * reclaim.\n *\n * `{p}:schema` records the layout, and a prefix holding another one is\n * rejected. A session from before cursors (0.17 to 0.20) opens as is: its\n * log, counter, clear marker, snapshots, and ids read the same, and its\n * dispatch keys (`meta`, `meta:{chunk}`, `meta-split`, `pending`, `ready`,\n * `snapshot-pins:{index}`, `snapshot-pin-counts`) share no name with the\n * cursor layout's, so they're never read. Its unfinished work is dropped.\n *\n * Global event-id ownership is one key per id, so no key grows with the\n * number of sessions. The conformance suite in test/conformance is the\n * executable contract, run against a real spawned `redis-server`.\n *\n * Timestamps and claim expiry come from the injected clock so tests can\n * time-travel against a real server. Cluster mode is out: the atomic\n * scripts span keys.\n */\n\nimport type { PresencePatch } from './contract.ts'\nimport { A2Error, supersededAttemptError } from './errors.ts'\nimport { CLEAR_EVENT_TYPE, sessionClearedError } from './session-clear.ts'\nimport { idempotentReplay } from './idempotent-replay.ts'\nimport { nullProtoRecord } from './internal.ts'\nimport { decodeCause } from './store-codec.ts'\nimport type { SerializedEvent } from './stream-buffer.ts'\nimport type {\n A2Store,\n Clock,\n CompleteAttemptResult,\n Event,\n EventCause,\n EventTarget,\n HeadDispatch,\n HeadEvent,\n HeadRef,\n IdSource,\n StoreClaimAvailableResult,\n StoreConsumer,\n StoredCursor,\n PresenceRow,\n StoredEvent,\n} from './store.ts'\n\n/** Issues any Redis command; the seam both transports implement. */\nexport type RedisCall = (\n command: string,\n ...args: Array<string | number>\n) => Promise<unknown>\n\nexport type RedisStoreCoreOptions = {\n call: RedisCall\n /**\n * Issues a command whose bulk replies arrive as raw bytes. Live feed\n * catch-up pages use it to decode only the fields a row needs.\n */\n callBytes?: RedisCall\n clock: Clock\n ids: IdSource\n keyPrefix: string\n presenceSource: string\n readStream?: RedisStreamRead\n}\n\n/** Stream entries from `from` onward, in nonempty ordered batches. */\nexport type RedisStreamRead = (options: {\n key: string\n from: string\n signal: AbortSignal\n}) => AsyncGenerator<StreamEntry[], void>\n\nexport type RedisStoreCore = Omit<A2Store, 'stream' | 'presence'> & {\n /**\n * The storage half of the presence capability, shared by both\n * transports. `set` returns the applied patch for local loopback,\n * or null when every field lost the LWW race. The script publishes\n * the applied fields for remote subscribers.\n */\n presence: {\n set(\n ns: string,\n participant: string,\n values: Record<string, unknown | null>,\n meta: { seen: number; at: Date; ttlMs: number },\n ): Promise<PresencePatch | null>\n read(ns: string): Promise<PresenceRow[]>\n }\n /** XRANGE pages — the full-row catch-up read. */\n readRange(\n sessionId: string,\n afterIndex: number,\n throughIndex?: number,\n ): Promise<StoredEvent[]>\n /** One bounded XRANGE page of public events for live feeds. */\n readEvents(sessionId: string, afterIndex: number): Promise<Event[]>\n /**\n * One bounded XRANGE page of catch-up rows for a live feed without a\n * streaming reader, payloads left as stored JSON. The reply is held whole,\n * so a page retains up to `FEED_PAGE_SIZE` entries.\n */\n readFeedPage(\n sessionId: string,\n afterIndex: number,\n ): Promise<SerializedEvent[]>\n /** Catch-up events after `afterIndex`, in nonempty ordered batches. */\n streamEvents?: (options: {\n sessionId: string\n afterIndex: number\n signal: AbortSignal\n }) => AsyncGenerator<SerializedEvent[], void>\n /**\n * Events from `fromIndex` through `throughIndex` or the newest entry in\n * one newest-first page, or null when the range doesn't fit in one.\n */\n readTail(\n sessionId: string,\n fromIndex: number,\n throughIndex?: number,\n ): Promise<Event[] | null>\n}\n\nexport const READ_PAGE_SIZE = 256\nexport const FEED_PAGE_SIZE = 1000\nconst READ_CONCURRENCY = 16\n\n/** The key layout this core writes; any other is rejected. */\nexport const REDIS_LAYOUT = 'cursors-2'\nconst LAYOUT_ERROR = 'A2_LAYOUT'\n\nconst META_CHUNK_SIZE = 1000\n\n/** A head row's dispatch columns, in the order scripts return them. */\nconst HEAD_COLUMNS = `{ 'd', 'fc', 'lc', 'ch', 'ce', 'a', 'lf', 'lfa', 'e', 'f' }`\n\n// ── Lua scripts (each runs atomically) ───────────────────────────────\n\n/**\n * Upstash bills every call a script makes, so scripts read the counter,\n * clear marker, layout, typed-type index, and id keys with one MGET\n * (`read_bounds`), read rows with one HMGET per chunk (`read_rows`), and\n * buffer hash writes (`put`, `drop`) into one HSET and one HDEL per hash\n * (`flush`). Buffered writes are invisible to reads until flushed, so a\n * script must not read a field it has buffered. Every script binds KEYS[1]\n * to the session's counter and KEYS[2] to the prefix's layout key, and\n * derives the session's other keys.\n */\nconst BASE_LUA = `\nlocal function sliced(command, key, args)\n local results = {}\n for first = 1, #args, 1000 do\n local last = math.min(first + 999, #args)\n local reply\n if key then\n reply = redis.call(command, key, unpack(args, first, last))\n else\n reply = redis.call(command, unpack(args, first, last))\n end\n if type(reply) == 'table' then\n for _, value in ipairs(reply) do results[#results + 1] = value end\n end\n end\n return results\nend\nlocal COUNTER, SCHEMA = KEYS[1], KEYS[2]\nlocal SESSION = string.sub(COUNTER, 1, -6)\nlocal LOG, CLEAR = SESSION .. 'log', SESSION .. 'clear'\nlocal META, READY, CURSORS = SESSION .. 'heads', SESSION .. 'runnable', SESSION .. 'cursors'\nlocal PIN_COUNTS, TYPED = SESSION .. 'pins', SESSION .. 'typed'\nlocal CLEAR_TYPE = '${CLEAR_EVENT_TYPE}'\nlocal total, boundary, schema_missing = 0, 0, false\n-- The event types typed log cursors read: type → { from, last }. The type's\n-- index holds every event of it above \\`from\\`, and no event of it lies above\n-- \\`last\\`.\nlocal typed, typed_dirty = {}, false\n-- Reads the counter, the latest clear marker, the layout, the typed types,\n-- and the extra keys in one MGET. Returns the extra keys' values, or nil when\n-- the prefix holds another layout.\nlocal function read_bounds(extra)\n local keys = { COUNTER, CLEAR, SCHEMA, TYPED }\n for _, key in ipairs(extra or {}) do keys[#keys + 1] = key end\n local values = sliced('MGET', nil, keys)\n if values[3] and values[3] ~= '${REDIS_LAYOUT}' then return nil end\n total = tonumber(values[1] or '0')\n boundary = tonumber(values[2] or '0')\n schema_missing = not values[3]\n if values[4] then typed = cjson.decode(values[4]) end\n local rest = {}\n for i = 5, #keys do rest[i - 4] = values[i] end\n return rest\nend\nlocal function entry_index(id) return tonumber(string.match(id, '^(%d+)')) end\nlocal function cid_of(consumer, tag)\n return tostring(string.len(consumer)) .. ':' .. consumer .. tag\nend\nlocal function parse_cid(cid)\n local colon = string.find(cid, ':', 1, true)\n local n = tonumber(string.sub(cid, 1, colon - 1))\n return string.sub(cid, colon + 1, colon + n), string.sub(cid, colon + n + 1)\nend\n-- A cursor's rows: its consumer, then '#' for an event cursor or 'L' .. lane\n-- for a lane or log cursor, as requests name it.\nlocal function row_of(consumer, tag)\n local kind = string.sub(tag, 1, 1)\n if kind == '#' then return cid_of(consumer, '#') end\n if kind == 'G' then return cid_of(consumer, 'L' .. string.sub(tag, 2)) end\n return cid_of(consumer, tag)\nend\nlocal function chunk_key(idx)\n return META .. ':' .. tostring(math.floor((idx - 1) / ${META_CHUNK_SIZE}))\nend\nlocal function row_name(idx, row, suffix)\n return tostring(idx) .. ':' .. row .. ':' .. suffix\nend\n-- Rows of (index, row) refs, aligned with refs; one HMGET per chunk.\nlocal function read_rows(refs, suffixes)\n local groups, order = {}, {}\n for position, ref in ipairs(refs) do\n local key = chunk_key(ref[1])\n local group = groups[key]\n if not group then\n group = { positions = {}, names = {} }\n groups[key] = group\n order[#order + 1] = key\n end\n group.positions[#group.positions + 1] = position\n for _, suffix in ipairs(suffixes) do\n group.names[#group.names + 1] = row_name(ref[1], ref[2], suffix)\n end\n end\n local rows = {}\n for _, key in ipairs(order) do\n local group = groups[key]\n local values = sliced('HMGET', key, group.names)\n for n, position in ipairs(group.positions) do\n local row = {}\n for column = 1, #suffixes do row[column] = values[(n - 1) * #suffixes + column] end\n rows[position] = row\n end\n end\n return rows\nend\nlocal writes, write_order, deletes, delete_order = {}, {}, {}, {}\nlocal function put(key, field, value)\n local fields = writes[key]\n if not fields then\n fields = {}\n writes[key] = fields\n write_order[#write_order + 1] = key\n end\n fields[#fields + 1] = field\n fields[#fields + 1] = value\nend\nlocal function drop(key, field)\n local fields = deletes[key]\n if not fields then\n fields = {}\n deletes[key] = fields\n delete_order[#delete_order + 1] = key\n end\n fields[#fields + 1] = field\nend\nlocal function flush()\n for _, key in ipairs(write_order) do sliced('HSET', key, writes[key]) end\n for _, key in ipairs(delete_order) do sliced('HDEL', key, deletes[key]) end\n writes, write_order, deletes, delete_order = {}, {}, {}, {}\nend\nlocal function put_row(idx, row, suffix, value)\n put(chunk_key(idx), row_name(idx, row, suffix), value)\nend\nlocal function drop_row(idx, row, suffix)\n drop(chunk_key(idx), row_name(idx, row, suffix))\nend\n-- A request names a cursor by tag: '#' for an event cursor, 'L' .. lane for a\n-- lane or log cursor. A claimed row records its kind: '#', or the cursor's tag.\nlocal function owns(kind, tag)\n if not kind then return false end\n if tag == '#' then return kind == '#' end\n return kind == tag or kind == 'G' .. string.sub(tag, 2)\nend\nlocal function cid_for(consumer, kind, idx)\n if kind == '#' then return cid_of(consumer, '#' .. tostring(idx)) end\n return cid_of(consumer, kind)\nend\n-- One type's index, named by the type's bytes in hex so no type can spell\n-- another key of the session.\nlocal function type_key(name)\n return SESSION .. 'type:' .. (string.gsub(name, '.', function(c)\n return string.format('%02x', string.byte(c))\n end))\nend\nlocal ready_changes, ready_order = {}, {}\nlocal function ready_set(cid, score)\n if ready_changes[cid] == nil then ready_order[#ready_order + 1] = cid end\n ready_changes[cid] = score\nend\nlocal function flush_ready()\n local adds, removes = {}, {}\n for _, cid in ipairs(ready_order) do\n local score = ready_changes[cid]\n if score then\n adds[#adds + 1] = score\n adds[#adds + 1] = cid\n else\n removes[#removes + 1] = cid\n end\n end\n sliced('ZADD', READY, adds)\n sliced('ZREM', READY, removes)\n ready_changes, ready_order = {}, {}\nend\nlocal function release_pins(idx, row, pins)\n if not pins or pins == '' then return end\n for _, ref in ipairs(cjson.decode(pins)) do\n local count = redis.call('HINCRBY', PIN_COUNTS, ref, -1)\n if count <= 0 then\n redis.call('HDEL', PIN_COUNTS, ref)\n local checkpoint = cjson.decode(ref)\n redis.call('ZREMRANGEBYSCORE', checkpoint[1], checkpoint[2], checkpoint[2])\n end\n end\n put_row(idx, row, 'pins', '')\nend\n`\n\n/**\n * Cursor movement for scripts that add events or move cursors. Lane and log\n * cursor records are read once per script (`load_states`) and written back\n * once (`flush_states`). A record whose head lies at or below the latest clear\n * marker is idle, and a log cursor counts from the marker.\n *\n * A typed log cursor finds its next head in its types' indexes, one bounded\n * lookup per type, skipped when `typed` shows no newer event of it. When an\n * index does not reach back far enough yet (a typed backfill, or a type a\n * cursor newly reads), the lookup returns the type, and the script returns\n * `{ 'index', type, after }` before writing anything: the caller indexes\n * that type down to `after` (INDEX_TYPE_LUA, one page per call) and runs the\n * operation again.\n */\nconst CURSOR_LUA = `\nlocal served_all, served_set, served_logs = true, {}, {}\nlocal pending_fast = false\nlocal function types_of(raw)\n if not raw or raw == '*' then return nil end\n local set = {}\n for _, name in ipairs(cjson.decode(raw)) do set[name] = true end\n return set\nend\nlocal function takes(types, kind)\n return kind ~= CLEAR_TYPE and (types == nil or types[kind] == true)\nend\n-- The served consumers: '' for every consumer, else [[name] or\n-- [name, lane, typesJson or '*', backfill]].\nlocal function use_served(raw)\n if raw == '' then return end\n served_all = false\n for _, entry in ipairs(cjson.decode(raw)) do\n served_set[entry[1]] = true\n if entry[2] then\n served_logs[#served_logs + 1] = {\n name = entry[1], lane = entry[2], ty = entry[3],\n types = types_of(entry[3]), backfill = entry[4] == 1,\n }\n end\n end\nend\nlocal function served_has(name) return served_all or served_set[name] == true end\n-- Whether a served consumer has a live head. Newest heads come first: a\n-- removed consumer's backlog is older than the work a deployment serves.\nlocal function has_pending()\n if pending_fast then return '1' end\n if served_all then return redis.call('ZCOUNT', READY, 1, '+inf') > 0 and '1' or '0' end\n if next(served_set) == nil then return '0' end\n local page = redis.call('ZREVRANGEBYSCORE', READY, '+inf', 1, 'LIMIT', 0, 32)\n for _, cid in ipairs(page) do\n if served_set[(parse_cid(cid))] then return '1' end\n end\n if #page < 32 then return '0' end\n for _, cid in ipairs(redis.call('ZREVRANGEBYSCORE', READY, '+inf', 1, 'LIMIT', 32, -1)) do\n if served_set[(parse_cid(cid))] then return '1' end\n end\n return '0'\nend\nlocal function targets_of(raw)\n if raw == '' then return {} end\n return cjson.decode(raw)\nend\n-- Starts indexing a type: every later event of it.\nlocal function register(name, from)\n typed[name] = { from, from }\n typed_dirty = true\nend\n-- The first stored event in (after, total] of a type in \\`types\\` (nil: any\n-- type), or false. Nil and a type when that type's index can't answer yet.\nlocal function stored_head(after, types)\n if types == nil then\n if after + 1 <= total then return after + 1 end\n return false\n end\n local best = false\n for name in pairs(types) do\n local entry = typed[name]\n if entry == nil then\n if after < total then return nil, name end\n register(name, total)\n elseif entry[2] > after then\n if entry[1] > after then return nil, name end\n local found = redis.call('ZRANGEBYSCORE', type_key(name),\n '(' .. tostring(after), tostring(total), 'LIMIT', 0, 1)\n if #found > 0 then\n local idx = tonumber(found[1])\n if not best or idx < best then best = idx end\n end\n end\n end\n return best\nend\n-- The first event after \\`after\\` a log cursor takes: stored, then in the\n-- batch being appended.\nlocal function next_log_head(after, types, batch)\n local head, missing = stored_head(after, types)\n if head == nil then return nil, missing end\n if head then return head end\n for _, event in ipairs(batch) do\n if event.idx > after and takes(types, event.type) then return event.idx end\n end\n return false\nend\n-- Indexes a batch's events of the types typed log cursors read.\nlocal function index_batch(batch)\n local adds, order = {}, {}\n for _, event in ipairs(batch) do\n local entry = typed[event.type]\n if entry then\n local list = adds[event.type]\n if not list then\n list = {}\n adds[event.type] = list\n order[#order + 1] = event.type\n end\n list[#list + 1] = event.idx\n list[#list + 1] = tostring(event.idx)\n entry[2] = event.idx\n typed_dirty = true\n end\n end\n for _, name in ipairs(order) do sliced('ZADD', type_key(name), adds[name]) end\nend\nlocal states, state_order = {}, {}\nlocal function load_states(cids)\n local need, names = {}, {}\n for _, cid in ipairs(cids) do\n local state = states[cid]\n if (state == nil or state.partial) and not need[cid] then\n need[cid] = true\n need[#need + 1] = cid\n names[#names + 1] = cid .. ':h'\n names[#names + 1] = cid .. ':p'\n names[#names + 1] = cid .. ':t'\n names[#names + 1] = cid .. ':ty'\n end\n end\n if #need == 0 then return end\n local values = sliced('HMGET', CURSORS, names)\n for n, cid in ipairs(need) do\n local h, p, t, ty = values[n * 4 - 3], values[n * 4 - 2], values[n * 4 - 1], values[n * 4]\n local tail = (t and t ~= '') and tonumber(t) or false\n local state = states[cid]\n if state then\n state.t = tail\n state.partial = nil\n else\n local _, tag = parse_cid(cid)\n state = {\n kind = string.sub(tag, 1, 1), exists = p ~= false,\n h = (h and h ~= '') and tonumber(h) or false,\n p = tonumber(p or '0'), t = tail, ty = ty or false,\n }\n if state.h and state.h <= boundary then state.h = false end\n if state.kind == 'G' and state.exists and state.p < boundary then state.p = boundary end\n states[cid] = state\n state_order[#state_order + 1] = cid\n end\n end\nend\nlocal function flush_states()\n for _, cid in ipairs(state_order) do\n local state = states[cid]\n if state.dirty then\n put(CURSORS, cid .. ':h', state.h and tostring(state.h) or '')\n put(CURSORS, cid .. ':p', tostring(state.p))\n if state.kind == 'L' then\n if not state.partial then put(CURSORS, cid .. ':t', state.t and tostring(state.t) or '') end\n elseif state.ty then\n put(CURSORS, cid .. ':ty', state.ty)\n end\n end\n end\nend\n-- Moves a cursor past its head \\`idx\\`. \\`kind\\` is the head row's kind;\n-- \\`batch\\` is the returned events, appended after the stored log. Writes\n-- nothing; returns nil and a type when that type's index must reach back\n-- to \\`idx\\` first.\nlocal function advance(consumer, kind, idx, row_next, batch)\n if kind == '#' then\n ready_set(cid_of(consumer, '#' .. tostring(idx)), false)\n return true\n end\n local cid = cid_of(consumer, kind)\n local state = states[cid]\n local next_head\n if string.sub(kind, 1, 1) == 'L' then\n next_head = row_next and tonumber(row_next) or false\n if not state then\n state = { kind = 'L', exists = true, p = idx, h = false, t = false, ty = false, partial = true }\n states[cid] = state\n state_order[#state_order + 1] = cid\n end\n if not next_head then\n state.t = false\n state.partial = nil\n end\n else\n local lane, raw = string.sub(kind, 2), nil\n for _, spec in ipairs(served_logs) do\n if spec.name == consumer and spec.lane == lane then raw = spec.ty end\n end\n if not raw then\n load_states({ cid })\n raw = states[cid].ty or '*'\n end\n local head, missing = next_log_head(idx, types_of(raw), batch)\n if head == nil then return nil, missing end\n next_head = head\n state = states[cid]\n if not state then\n state = { kind = 'G', exists = true, p = idx, h = false, t = false, ty = raw }\n states[cid] = state\n state_order[#state_order + 1] = cid\n end\n state.ty = raw\n end\n state.h = next_head\n state.p = idx\n state.exists = true\n state.dirty = true\n ready_set(cid, next_head)\n return true\nend\n-- Reads what an appended batch's cursor work needs: its lane targets'\n-- records, and the head of each served log consumer whose types it carries.\n-- Writes nothing; returns nil, a type, and an index when that type's index\n-- must reach back to the index first.\nlocal log_plan = {}\nlocal function plan(batch, base)\n local need = {}\n for _, event in ipairs(batch) do\n for _, target in ipairs(event.targets) do\n if target[2] ~= '#' then need[#need + 1] = cid_of(target[1], target[2]) end\n end\n end\n for _, spec in ipairs(served_logs) do\n for _, event in ipairs(batch) do\n if takes(spec.types, event.type) then\n local cid = cid_of(spec.name, 'G' .. spec.lane)\n log_plan[#log_plan + 1] = { spec = spec, first = event.idx, cid = cid }\n need[#need + 1] = cid\n break\n end\n end\n end\n load_states(need)\n for _, log in ipairs(log_plan) do\n local state, spec = states[log.cid], log.spec\n if not state.exists then\n if spec.backfill then\n local head, missing = stored_head(boundary, spec.types)\n if head == nil then return nil, missing, boundary end\n log.p, log.h = boundary, head or log.first\n else\n -- Nothing it takes is stored after its start: index its types from there.\n for name in pairs(spec.types or {}) do\n if typed[name] == nil then register(name, base) end\n end\n log.p, log.h = base, log.first\n end\n elseif not state.h then\n -- Not the batch's first: a deployment that doesn't serve this consumer\n -- may have appended events it takes since it went idle.\n local head, missing = stored_head(state.p, spec.types)\n if head == nil then return nil, missing, state.p end\n log.h = head or log.first\n end\n end\n return true\nend\n-- Starts the cursor work \\`plan\\` read: each event's targets, and each\n-- planned log cursor.\nlocal function enqueue(batch)\n for _, event in ipairs(batch) do\n for _, target in ipairs(event.targets) do\n local consumer, tag = target[1], target[2]\n if served_has(consumer) then pending_fast = true end\n if tag == '#' then\n ready_set(cid_of(consumer, '#' .. tostring(event.idx)), event.idx)\n else\n local cid = cid_of(consumer, tag)\n local state = states[cid]\n if state.h and state.t then\n put_row(state.t, row_of(consumer, tag), 'next', tostring(event.idx))\n else\n state.h = event.idx\n if state.p < boundary then state.p = boundary end\n ready_set(cid, event.idx)\n end\n state.t = event.idx\n state.exists = true\n state.dirty = true\n end\n end\n end\n for _, log in ipairs(log_plan) do\n pending_fast = true\n local state = states[log.cid]\n if not state.exists then\n state.exists, state.p, state.h = true, log.p, log.h\n ready_set(log.cid, log.h)\n elseif not state.h then\n state.h = log.h\n ready_set(log.cid, log.h)\n end\n state.ty = log.spec.ty\n state.dirty = true\n end\nend\n-- Event-id owners for the extra keys read_bounds returned. A record this\n-- session made below its latest clear is free.\nlocal function owners_of(values, session)\n local owners = {}\n for i = 1, #values do\n local owner = values[i]\n if owner then\n local rec = cjson.decode(owner)\n if rec[1] == session and tonumber(rec[2]) < boundary then owner = false end\n end\n owners[i] = owner\n end\n return owners\nend\n`\n\nconst WAKE_PAYLOAD_LIMIT = 32_768\n\n/**\n * Wake-up messages carry the events they announce, so a subscribed reader\n * that is caught up applies them without reading the stream:\n * `{\"f\":frontier,\"t\":createdAt,\"e\":[[index,id,type,payload],...]}`. Payloads\n * are the stored JSON text, inlined verbatim. A batch over\n * WAKE_PAYLOAD_LIMIT bytes publishes `{\"f\":frontier}` and readers catch up\n * with XRANGE.\n */\nconst WAKE_LUA = `\nlocal wake_entries, wake_bytes = {}, 0\nlocal function wake_add(field, id, typ, payload)\n if wake_bytes + #payload > ${WAKE_PAYLOAD_LIMIT} then\n wake_bytes = ${WAKE_PAYLOAD_LIMIT} + 1\n return\n end\n local entry = '[' .. field .. ',' .. cjson.encode(id) .. ',' ..\n cjson.encode(typ) .. ',' .. payload .. ']'\n wake_bytes = wake_bytes + #entry + 1\n if wake_bytes <= ${WAKE_PAYLOAD_LIMIT} then\n wake_entries[#wake_entries + 1] = entry\n end\nend\nlocal function wake(channel, frontier, created_at)\n local message = '{\"f\":' .. frontier .. '}'\n if wake_bytes <= ${WAKE_PAYLOAD_LIMIT} then\n message = '{\"f\":' .. frontier .. ',\"t\":' .. created_at .. ',\"e\":[' ..\n table.concat(wake_entries, ',') .. ']}'\n end\n redis.pcall('PUBLISH', channel, message)\nend\n`\n\n/**\n * KEYS: counter, layout, then one id ownership key per event.\n * ARGV: sessionId, nowMs, n, served consumers, notify channel, origin index\n * or '', then per event: id, type, payloadJson, causeJson or '', targets.\n * Returns ['empty', hasPending] | ['ok', firstIndex, hasPending] |\n * ['dup', hasPending, ...indexes] | ['partial', count] |\n * ['foreign', eventId] | ['superseded', position] | ['cleared', originIndex] |\n * ['index', type, after].\n */\nconst APPEND_LUA = `${BASE_LUA}${CURSOR_LUA}${WAKE_LUA}\nlocal session_id, now, n = ARGV[1], ARGV[2], tonumber(ARGV[3])\nuse_served(ARGV[4])\nif n == 0 then return { 'empty', has_pending() } end\nlocal id_keys = {}\nfor i = 1, n do id_keys[i] = KEYS[2 + i] end\nlocal values = read_bounds(id_keys)\nif not values then return redis.error_reply('${LAYOUT_ERROR}') end\nlocal owners = owners_of(values, session_id)\nlocal base = total\nlocal dups = {}\nfor i = 1, n do\n if owners[i] then\n local rec = cjson.decode(owners[i])\n if rec[1] ~= session_id then return { 'foreign', ARGV[6 + (i - 1) * 5 + 1] } end\n dups[#dups + 1] = tostring(rec[2])\n end\nend\nif #dups == n then\n local res = { 'dup', has_pending() }\n for i = 1, #dups do res[#res + 1] = dups[i] end\n return res\nend\nif #dups > 0 then return { 'partial', tostring(#dups) } end\nlocal origin = ARGV[6]\nif origin ~= '' and tonumber(origin) <= boundary then return { 'cleared', origin } end\n-- The attempt-currency fence: a cause's head row must still be its cursor's\n-- head, at that attempt, not dead-lettered.\nlocal refs, fenced = {}, {}\nfor i = 1, n do\n local raw = ARGV[6 + (i - 1) * 5 + 4]\n if raw ~= '' then\n local cause = cjson.decode(raw)\n local index = tonumber(cause.index)\n if index <= boundary then return { 'superseded', tostring(i) } end\n local tag = type(cause.lane) == 'string' and ('L' .. cause.lane) or '#'\n refs[#refs + 1] = { index, row_of(cause.consumer, tag) }\n fenced[#fenced + 1] = {\n position = i, consumer = cause.consumer, attempt = tonumber(cause.attempt), tag = tag,\n }\n end\nend\nlocal rows = read_rows(refs, { 'd', 'f', 'p', 'l' })\nfor k, cause in ipairs(fenced) do\n local row = rows[k]\n if tonumber(row[1] or '0') ~= cause.attempt or row[2] or row[3] or not owns(row[4], cause.tag) then\n return { 'superseded', tostring(cause.position) }\n end\n if served_has(cause.consumer) then pending_fast = true end\nend\nlocal batch = {}\nfor i = 1, n do\n local o = 6 + (i - 1) * 5\n batch[i] = { idx = base + i, type = ARGV[o + 2], targets = targets_of(ARGV[o + 5]) }\nend\nlocal planned, missing, after = plan(batch, base)\nif not planned then return { 'index', missing, tostring(after) } end\nlocal owner_writes = {}\nfor i = 1, n do\n local o = 6 + (i - 1) * 5\n local field = tostring(base + i)\n redis.call('XADD', LOG, field .. '-0',\n 'id', ARGV[o + 1], 'type', ARGV[o + 2], 'payload', ARGV[o + 3],\n 'created_at', now, 'cause', ARGV[o + 4])\n owner_writes[#owner_writes + 1] = KEYS[2 + i]\n owner_writes[#owner_writes + 1] = cjson.encode({ session_id, base + i })\n wake_add(field, ARGV[o + 1], ARGV[o + 2], ARGV[o + 3])\nend\nindex_batch(batch)\nenqueue(batch)\nowner_writes[#owner_writes + 1] = COUNTER\nowner_writes[#owner_writes + 1] = tostring(base + n)\nif schema_missing then\n owner_writes[#owner_writes + 1] = SCHEMA\n owner_writes[#owner_writes + 1] = '${REDIS_LAYOUT}'\nend\nif typed_dirty then\n owner_writes[#owner_writes + 1] = TYPED\n owner_writes[#owner_writes + 1] = cjson.encode(typed)\nend\nsliced('MSET', nil, owner_writes)\nflush_states()\nflush()\nflush_ready()\nlocal pending = has_pending()\nwake(ARGV[5], tostring(base + n), now)\nreturn { 'ok', tostring(base + 1), pending }\n`\n\n/**\n * KEYS: counter, layout.\n * ARGV: type, after. Indexes one page of the stored log below the type's\n * index, toward `after`, registering the type first if no cursor read it\n * yet. Returns ['1'] once the index reaches `after`, else ['0'].\n */\nconst INDEX_TYPE_LUA = `${BASE_LUA}\nif not read_bounds() then return redis.error_reply('${LAYOUT_ERROR}') end\nlocal name, after = ARGV[1], tonumber(ARGV[2])\nlocal entry = typed[name]\nif not entry then\n entry = { total, total }\n typed[name] = entry\nend\nif entry[1] > after then\n local entries = redis.call('XREVRANGE', LOG, tostring(entry[1]) .. '-0',\n tostring(after + 1) .. '-0', 'COUNT', ${READ_PAGE_SIZE})\n local adds = {}\n for _, stored in ipairs(entries) do\n local fields = stored[2]\n for j = 1, #fields, 2 do\n if fields[j] == 'type' then\n if fields[j + 1] == name then\n local idx = entry_index(stored[1])\n adds[#adds + 1] = idx\n adds[#adds + 1] = tostring(idx)\n end\n break\n end\n end\n end\n sliced('ZADD', type_key(name), adds)\n if #entries < ${READ_PAGE_SIZE} then\n entry[1] = after\n else\n entry[1] = entry_index(entries[#entries][1]) - 1\n end\nend\nredis.call('SET', TYPED, cjson.encode(typed))\nreturn { entry[1] <= after and '1' or '0' }\n`\n\n/**\n * KEYS: counter, layout.\n * ARGV: holder, nowMs, expiresAtMs, maxFailures or '', served consumers,\n * the highest head to claim or '', excluded count, then per excluded head:\n * consumer, tag.\n * Live heads are the ready set's positive scores, so an empty one is enough\n * to report 'settled'. A claimed result ends with the clear marker.\n */\nconst CLAIM_AVAILABLE_LUA = `${BASE_LUA}${CURSOR_LUA}\nlocal ready = redis.call('ZRANGEBYSCORE', READY, 1, ARGV[6] == '' and '+inf' or ARGV[6], 'WITHSCORES')\nif #ready == 0 then return { 'settled' } end\nif not read_bounds() then return redis.error_reply('${LAYOUT_ERROR}') end\nlocal holder, now, expires_at = ARGV[1], tonumber(ARGV[2]), ARGV[3]\nlocal max_failures = tonumber(ARGV[4])\nuse_served(ARGV[5])\nlocal excluded = {}\nfor i = 1, tonumber(ARGV[7]) do\n excluded[ARGV[7 + (i - 1) * 2 + 1] .. '\\\\0' .. ARGV[7 + (i - 1) * 2 + 2]] = true\nend\nlocal candidates = {}\nfor i = 1, #ready, 2 do\n local cid, idx = ready[i], tonumber(ready[i + 1])\n local consumer, tag = parse_cid(cid)\n local kind = string.sub(tag, 1, 1)\n local key = consumer .. '\\\\0' .. (kind == 'G' and ('L' .. string.sub(tag, 2)) or tag)\n if served_has(consumer) and not excluded[key] then\n candidates[#candidates + 1] = {\n idx, row_of(consumer, tag), consumer = consumer, cid = cid,\n kind = kind == '#' and '#' or tag,\n }\n end\nend\nlocal rows = read_rows(candidates, { 'd', 'fc', 'lc', 'ch', 'ce', 'a', 'lf', 'lfa', 'e', 'f', 'pins' })\nlocal takes_over, retry_at, wanted = {}, nil, {}\nfor position, candidate in ipairs(candidates) do\n local row = rows[position]\n local expires = tonumber(row[5] or '0')\n if expires > now then\n if not retry_at or expires < retry_at then retry_at = expires end\n else\n takes_over[#takes_over + 1] = { candidate = candidate, row = row }\n wanted[candidate[1]] = true\n end\nend\nif #takes_over == 0 then\n if retry_at then return { 'busy', tostring(retry_at) } end\n return { 'settled' }\nend\n-- One XRANGE per run of consecutive claimed indexes.\nlocal indexes, entries = {}, {}\nfor idx in pairs(wanted) do indexes[#indexes + 1] = idx end\ntable.sort(indexes)\nlocal first = 1\nwhile first <= #indexes do\n local last = first\n while last < #indexes and indexes[last + 1] == indexes[last] + 1 do last = last + 1 end\n local range = redis.call('XRANGE', LOG, tostring(indexes[first]) .. '-0', tostring(indexes[last]) .. '-0')\n for _, entry in ipairs(range) do entries[entry_index(entry[1])] = entry end\n first = last + 1\nend\nfor _, idx in ipairs(indexes) do\n if not entries[idx] then return { 'missing', tostring(idx) } end\nend\nlocal result = { 'claimed', tostring(#takes_over) }\nfor _, take in ipairs(takes_over) do\n local candidate, row = take.candidate, take.row\n local idx, at = candidate[1], candidate[2]\n local attempt = tostring(tonumber(row[1] or '0') + 1)\n -- A claim still held here lapsed: its attempt never finished.\n local lapsed = row[4] ~= false\n local failures = tonumber(row[6] or '0')\n if lapsed then\n failures = failures + 1\n put_row(idx, at, 'a', tostring(failures))\n end\n put_row(idx, at, 'd', attempt)\n local columns\n if lapsed and max_failures and failures >= max_failures then\n -- That takeover spent the last failed attempt: dead-letter, unclaimed.\n -- Advancing the attempt fences the lapsed one; nothing runs.\n put_row(idx, at, 'f', ARGV[2])\n drop_row(idx, at, 'ch')\n drop_row(idx, at, 'ce')\n ready_set(candidate.cid, -idx)\n release_pins(idx, at, row[11])\n columns = { attempt, row[2], row[3], false, false, tostring(failures), row[7], row[8], row[9], ARGV[2] }\n else\n local first_claimed = row[2] or ARGV[2]\n put_row(idx, at, 'fc', first_claimed)\n put_row(idx, at, 'lc', ARGV[2])\n put_row(idx, at, 'ch', holder)\n put_row(idx, at, 'ce', expires_at)\n put_row(idx, at, 'l', candidate.kind)\n columns = { attempt, first_claimed, ARGV[2], holder, expires_at, tostring(failures), row[7], row[8], row[9], false }\n end\n result[#result + 1] = { candidate.consumer, candidate.kind, tostring(idx), columns, entries[idx] }\nend\nflush()\nflush_ready()\nresult[#result + 1] = tostring(boundary)\nresult[#result + 1] = tostring(total)\nreturn result\n`\n\n/**\n * KEYS: counter, layout.\n * ARGV: holder, nowMs, expiresAtMs, n, then per claim: consumer, tag, index,\n * attempt. Returns [renewedCount, ...renewed, ...superseded] as 1-based\n * positions in the request.\n */\nconst RENEW_CLAIMS_LUA = `${BASE_LUA}\nif not read_bounds() then return redis.error_reply('${LAYOUT_ERROR}') end\nlocal holder, now, expires_at = ARGV[1], tonumber(ARGV[2]), ARGV[3]\nlocal refs, items, superseded = {}, {}, {}\nfor i = 1, tonumber(ARGV[4]) do\n local o = 4 + (i - 1) * 4\n local idx = tonumber(ARGV[o + 3])\n if idx <= boundary then\n superseded[#superseded + 1] = tostring(i)\n else\n local row = row_of(ARGV[o + 1], ARGV[o + 2])\n refs[#refs + 1] = { idx, row }\n items[#items + 1] = { position = i, idx = idx, row = row, tag = ARGV[o + 2], attempt = tonumber(ARGV[o + 4]) }\n end\nend\nlocal rows = read_rows(refs, { 'd', 'ce', 'p', 'f', 'ch', 'l' })\nlocal renewed = {}\nfor k, item in ipairs(items) do\n local row = rows[k]\n local attempt = tonumber(row[1] or '0')\n if row[6] and not owns(row[6], item.tag) then\n -- Another cursor's head: this one never claimed it.\n elseif row[3] or attempt > item.attempt then\n superseded[#superseded + 1] = tostring(item.position)\n elseif attempt == item.attempt and tonumber(expires_at) > now and\n not row[4] and row[5] == holder and tonumber(row[2] or '0') > now then\n put_row(item.idx, item.row, 'ce', expires_at)\n renewed[#renewed + 1] = tostring(item.position)\n end\nend\nflush()\nlocal result = { tostring(#renewed) }\nfor i = 1, #renewed do result[#result + 1] = renewed[i] end\nfor i = 1, #superseded do result[#result + 1] = superseded[i] end\nreturn result\n`\n\n/**\n * KEYS: counter, layout, then one id ownership key per child.\n * ARGV: sessionId, nowMs, consumer, tag, index, attempt, child causeJson,\n * served consumers, notify channel, returned idsJson, n, then per child: id,\n * type, payloadJson, targets.\n */\nconst COMPLETE_ATTEMPT_LUA = `${BASE_LUA}${CURSOR_LUA}${WAKE_LUA}\nlocal session_id, now = ARGV[1], ARGV[2]\nlocal consumer, tag, idx, attempt = ARGV[3], ARGV[4], tonumber(ARGV[5]), tonumber(ARGV[6])\nlocal at = row_of(consumer, tag)\nlocal n = tonumber(ARGV[11])\nlocal id_keys = {}\nfor i = 1, n do id_keys[i] = KEYS[2 + i] end\nlocal values = read_bounds(id_keys)\nif not values then return redis.error_reply('${LAYOUT_ERROR}') end\nlocal owners = owners_of(values, session_id)\nif idx <= boundary then return { 'superseded' } end\nif idx < 1 or idx > total then return { 'missing' } end\nlocal row = read_rows({ { idx, at } }, { 'd', 'p', 'pa', 'ri', 'ch', 'f', 'l', 'next', 'pins' })[1]\nif row[2] then\n -- The cursor moved past this head: a retry of its completion replays.\n if not owns(row[7], tag) then return { 'superseded' } end\n if not row[3] or tonumber(row[3]) ~= attempt then return { 'superseded' } end\n local recorded = cjson.decode(row[4] or '[]')\n if #recorded ~= n then return { 'partial' } end\n for i = 1, n do\n if recorded[i] ~= ARGV[11 + (i - 1) * 4 + 1] then return { 'partial' } end\n end\n local result = { 'duplicate' }\n for i = 1, n do\n if not owners[i] then return { 'partial' } end\n local rec = cjson.decode(owners[i])\n if rec[1] ~= session_id then return { 'partial' } end\n result[#result + 1] = tostring(rec[2])\n end\n return result\nend\nif tonumber(row[1] or '0') ~= attempt or not row[5] or row[6] or not owns(row[7], tag) then\n return { 'superseded' }\nend\nfor i = 1, n do\n if owners[i] then return { 'partial' } end\nend\nuse_served(ARGV[8])\nlocal batch = {}\nfor i = 1, n do\n local o = 11 + (i - 1) * 4\n batch[i] = { idx = total + i, type = ARGV[o + 2], targets = targets_of(ARGV[o + 4]) }\nend\nlocal moved, missing = advance(consumer, row[7], idx, row[8], batch)\nif not moved then return { 'index', missing, tostring(idx) } end\nlocal planned, unindexed, after = plan(batch, total)\nif not planned then return { 'index', unindexed, tostring(after) } end\nput_row(idx, at, 'p', now)\nput_row(idx, at, 'pa', ARGV[6])\nput_row(idx, at, 'ri', ARGV[10])\nrelease_pins(idx, at, row[9])\nlocal owner_writes = {}\nfor i = 1, n do\n local o = 11 + (i - 1) * 4\n local field = tostring(total + i)\n redis.call('XADD', LOG, field .. '-0',\n 'id', ARGV[o + 1], 'type', ARGV[o + 2], 'payload', ARGV[o + 3],\n 'created_at', now, 'cause', ARGV[7])\n owner_writes[#owner_writes + 1] = KEYS[2 + i]\n owner_writes[#owner_writes + 1] = cjson.encode({ session_id, total + i })\n wake_add(field, ARGV[o + 1], ARGV[o + 2], ARGV[o + 3])\nend\nindex_batch(batch)\nenqueue(batch)\nif n > 0 then\n owner_writes[#owner_writes + 1] = COUNTER\n owner_writes[#owner_writes + 1] = tostring(total + n)\n if schema_missing then\n owner_writes[#owner_writes + 1] = SCHEMA\n owner_writes[#owner_writes + 1] = '${REDIS_LAYOUT}'\n end\nend\nif typed_dirty then\n owner_writes[#owner_writes + 1] = TYPED\n owner_writes[#owner_writes + 1] = cjson.encode(typed)\nend\nsliced('MSET', nil, owner_writes)\nflush_states()\nflush()\nflush_ready()\nif n > 0 then wake(ARGV[9], tostring(total + n), now) end\nreturn { 'ok', tostring(total + 1) }\n`\n\n/**\n * KEYS: counter, layout.\n * ARGV: consumer, tag, index, attempt, error, maxFailures, nowMs, retryAtMs\n * or ''.\n */\nconst FAIL_ATTEMPT_LUA = `${BASE_LUA}\nif not read_bounds() then return redis.error_reply('${LAYOUT_ERROR}') end\nlocal consumer, tag, idx, attempt = ARGV[1], ARGV[2], tonumber(ARGV[3]), tonumber(ARGV[4])\nlocal at = row_of(consumer, tag)\nif idx <= boundary then return { 'superseded', '0' } end\nif idx < 1 or idx > total then return { 'missing', '0' } end\nlocal row = read_rows({ { idx, at } }, { 'a', 'd', 'p', 'f', 'lfa', 'ch', 'l', 'pins' })[1]\nif row[3] or (row[7] and not owns(row[7], tag)) then return { 'superseded', '0' } end\nlocal failures = tonumber(row[1] or '0')\nif tonumber(row[2] or '0') ~= attempt then return { 'superseded', tostring(failures) } end\nif not row[6] and row[5] and tonumber(row[5]) == attempt then\n return { row[4] and 'dead_lettered' or 'failed', tostring(failures) }\nend\nif row[4] then return { 'dead_lettered', tostring(failures) } end\nif not row[6] then return { 'superseded', tostring(failures) } end\nfailures = failures + 1\nlocal dead = failures >= tonumber(ARGV[6])\nput_row(idx, at, 'a', tostring(failures))\nput_row(idx, at, 'e', ARGV[5])\nput_row(idx, at, 'lf', ARGV[7])\nput_row(idx, at, 'lfa', ARGV[4])\ndrop_row(idx, at, 'ch')\nif dead then\n put_row(idx, at, 'f', ARGV[7])\n drop_row(idx, at, 'ce')\n ready_set(cid_for(consumer, row[7], idx), -idx)\n release_pins(idx, at, row[8])\nelseif ARGV[8] ~= '' then\n put_row(idx, at, 'ce', ARGV[8])\nelse\n drop_row(idx, at, 'ce')\nend\nflush()\nflush_ready()\nreturn { dead and 'dead_lettered' or 'failed', tostring(failures) }\n`\n\n/**\n * KEYS: counter, layout. ARGV: consumer, index, action, nowMs, served\n * consumers. Returns [redriven, hasPending] | ['index', type, after]. Acts\n * only on the consumer's dead-lettered head at the index, above the clear\n * marker.\n */\nconst REDRIVE_LUA = `${BASE_LUA}${CURSOR_LUA}\nif not read_bounds() then return redis.error_reply('${LAYOUT_ERROR}') end\nuse_served(ARGV[5])\nlocal consumer, idx = ARGV[1], tonumber(ARGV[2])\nlocal function result(redriven) return { redriven, has_pending() } end\nif idx <= boundary or idx < 1 or idx > total then return result('0') end\nlocal cid, tag = nil, nil\nfor _, member in ipairs(redis.call('ZRANGEBYSCORE', READY, -idx, -idx)) do\n local owner, member_tag = parse_cid(member)\n if owner == consumer then\n cid, tag = member, member_tag\n break\n end\nend\nif not cid then return result('0') end\nlocal kind = string.sub(tag, 1, 1) == '#' and '#' or tag\nlocal at = row_of(consumer, tag)\nif ARGV[3] == 'skip' then\n local row = read_rows({ { idx, at } }, { 'next' })[1]\n local moved, missing = advance(consumer, kind, idx, row[1], {})\n if not moved then return { 'index', missing, tostring(idx) } end\n put_row(idx, at, 'p', ARGV[4])\n flush_states()\n if typed_dirty then redis.call('SET', TYPED, cjson.encode(typed)) end\nelse\n drop_row(idx, at, 'f')\n drop_row(idx, at, 'a')\n ready_set(cid, idx)\nend\nflush()\nflush_ready()\nreturn result('1')\n`\n\n/**\n * KEYS: counter, layout. ARGV: consumer, lane, types JSON or '*', backfill\n * ('1' or '0'). Brings one log cursor up to date without an append, as\n * `plan` and `enqueue` would for an append carrying its types: a missing\n * cursor starts at the clear marker with backfill, else at the head with\n * nothing to run; an idle one takes the next stored event of its types; one\n * with a head, claimed or not, keeps it. Records the types, and writes\n * nothing when nothing changed. Returns ['ok', position, head or nil, head\n * columns, session head] | ['index', type, after].\n */\nconst WAKE_LOG_LUA = `${BASE_LUA}${CURSOR_LUA}\nif not read_bounds() then return redis.error_reply('${LAYOUT_ERROR}') end\nlocal consumer, lane, ty = ARGV[1], ARGV[2], ARGV[3]\nlocal types = types_of(ty)\nlocal cid = cid_of(consumer, 'G' .. lane)\nload_states({ cid })\nlocal state = states[cid]\nlocal had_head = state.h\n-- Nothing after the latest clear: a missing cursor stays missing.\nif not state.exists and total == boundary then\n return { 'ok', tostring(boundary), false, {}, tostring(total) }\nend\nif not state.exists then\n state.exists, state.p = true, total\n if ARGV[4] == '1' then\n local head, missing = stored_head(boundary, types)\n if head == nil then return { 'index', missing, tostring(boundary) } end\n state.p, state.h = boundary, head\n else\n -- Nothing it takes is stored after its start: index its types from there.\n for name in pairs(types or {}) do\n if typed[name] == nil then register(name, total) end\n end\n end\n state.dirty = true\nelseif not state.h then\n -- A deployment that doesn't serve this consumer may have appended events\n -- it takes since it went idle.\n local head, missing = stored_head(state.p, types)\n if head == nil then return { 'index', missing, tostring(state.p) } end\n if head then state.h, state.dirty = head, true end\nend\nif state.ty ~= ty then state.ty, state.dirty = ty, true end\nlocal columns = {}\nif had_head then\n columns = read_rows({ { state.h, row_of(consumer, 'G' .. lane) } }, ${HEAD_COLUMNS})[1]\nelseif state.h then\n ready_set(cid, state.h)\nend\nflush_states()\nif typed_dirty then redis.call('SET', TYPED, cjson.encode(typed)) end\nflush()\nflush_ready()\nreturn { 'ok', tostring(state.p), state.h and tostring(state.h) or false, columns, tostring(total) }\n`\n\n/**\n * KEYS: counter, layout. Returns the dead-lettered heads above the clear\n * marker as [consumer, tag, index, columns, entry] tuples.\n */\nconst DEAD_LETTERS_LUA = `${BASE_LUA}\nlocal dead = redis.call('ZRANGEBYSCORE', READY, '-inf', '(0', 'WITHSCORES')\nif #dead == 0 then return {} end\nif not read_bounds() then return redis.error_reply('${LAYOUT_ERROR}') end\nlocal refs, items = {}, {}\nfor i = 1, #dead, 2 do\n local consumer, tag = parse_cid(dead[i])\n local idx = -tonumber(dead[i + 1])\n if idx > boundary then\n refs[#refs + 1] = { idx, row_of(consumer, tag) }\n items[#items + 1] = { consumer, tag, idx }\n end\nend\nlocal rows = read_rows(refs, ${HEAD_COLUMNS})\nlocal result = {}\nfor k, item in ipairs(items) do\n local entries = redis.call('XRANGE', LOG, tostring(item[3]) .. '-0', tostring(item[3]) .. '-0')\n if #entries > 0 then\n result[#result + 1] = { item[1], item[2], tostring(item[3]), rows[k], entries[1] }\n end\nend\nreturn result\n`\n\n/**\n * KEYS: counter, layout. Returns every listed cursor as [consumer, tag,\n * position, head or nil, columns]: lane and log cursors from the cursor\n * hash, read against the clear marker, then the event cursors with a head.\n */\nconst CURSORS_LUA = `${BASE_LUA}\nif not read_bounds() then return redis.error_reply('${LAYOUT_ERROR}') end\nlocal listed = {}\nlocal fields = redis.call('HGETALL', CURSORS)\nlocal records, order = {}, {}\nfor i = 1, #fields, 2 do\n local at = string.find(fields[i], ':[^:]*$')\n local cid, suffix = string.sub(fields[i], 1, at - 1), string.sub(fields[i], at + 1)\n if not records[cid] then\n records[cid] = {}\n order[#order + 1] = cid\n end\n records[cid][suffix] = fields[i + 1]\nend\nfor _, cid in ipairs(order) do\n local record = records[cid]\n local consumer, tag = parse_cid(cid)\n local head = (record.h and record.h ~= '') and tonumber(record.h) or false\n local position = tonumber(record.p or '0')\n if head and head <= boundary then head = false end\n local kind = string.sub(tag, 1, 1)\n if kind == 'G' and position < boundary then position = boundary end\n if kind == 'G' or head or position > boundary then\n listed[#listed + 1] = { consumer, tag, position, head }\n end\nend\nlocal ready = redis.call('ZRANGE', READY, 0, -1, 'WITHSCORES')\nfor i = 1, #ready, 2 do\n local consumer, tag = parse_cid(ready[i])\n if string.sub(tag, 1, 1) == '#' then\n local head = math.abs(tonumber(ready[i + 1]))\n listed[#listed + 1] = { consumer, '#', head - 1, head }\n end\nend\nlocal refs, positions = {}, {}\nfor position, cursor in ipairs(listed) do\n if cursor[4] then\n refs[#refs + 1] = { cursor[4], row_of(cursor[1], cursor[2]) }\n positions[#refs] = position\n end\nend\nlocal rows = read_rows(refs, ${HEAD_COLUMNS})\nlocal columns = {}\nfor k, position in ipairs(positions) do columns[position] = rows[k] end\nlocal result = {}\nfor position, cursor in ipairs(listed) do\n result[#result + 1] = {\n cursor[1], cursor[2], tostring(cursor[3]),\n cursor[4] and tostring(cursor[4]) or false, columns[position] or {},\n }\nend\nreturn result\n`\n\n/**\n * KEYS: counter, layout, head, history.\n * ARGV: write count, then per write: index, snapshot JSON, pin count, then\n * per pin: consumer, tag, index; finally the reducer name. Writes from\n * before the latest clear are dropped, and a head from before it is\n * replaced as if absent. A pin holds only on an unfinished, live head: a\n * claimed row says so itself, an unclaimed head by its cursor's ready score.\n */\nconst PUT_SNAPSHOTS_LUA = `${BASE_LUA}\nif not read_bounds() then return redis.error_reply('${LAYOUT_ERROR}') end\nlocal HEAD, HISTORY = KEYS[3], KEYS[4]\nlocal entries, refs, pins = {}, {}, {}\nlocal offset = 2\nfor w = 1, tonumber(ARGV[1]) do\n local index = tonumber(ARGV[offset])\n local pin_count = tonumber(ARGV[offset + 2])\n local entry = { index = index, snapshot = ARGV[offset + 1], pins = {} }\n for p = 1, pin_count do\n local o = offset + 2 + (p - 1) * 3\n local pin = { consumer = ARGV[o + 1], tag = ARGV[o + 2], idx = tonumber(ARGV[o + 3]) }\n if index >= boundary and pin.idx > boundary and pin.idx <= total then\n pin.row = row_of(pin.consumer, pin.tag)\n refs[#refs + 1] = { pin.idx, pin.row }\n pins[#pins + 1] = pin\n entry.pins[#entry.pins + 1] = pin\n end\n end\n entries[w] = entry\n offset = offset + 3 + pin_count * 3\nend\nlocal function live(consumer, tag, idx)\n return tonumber(redis.call('ZSCORE', READY, cid_of(consumer, tag)) or '0') == idx\nend\nlocal rows = read_rows(refs, { 'pins', 'p', 'f', 'l' })\nlocal held, held_order = {}, {}\nfor k, pin in ipairs(pins) do\n local row = rows[k]\n local valid\n if row[2] or row[3] then\n valid = false\n elseif row[4] then\n valid = owns(row[4], pin.tag)\n elseif pin.tag == '#' then\n valid = live(pin.consumer, '#' .. tostring(pin.idx), pin.idx)\n else\n valid = live(pin.consumer, pin.tag, pin.idx) or\n live(pin.consumer, 'G' .. string.sub(pin.tag, 2), pin.idx)\n end\n if valid then\n local key = tostring(pin.idx) .. ':' .. pin.row\n if not held[key] then\n local list = (row[1] and row[1] ~= '') and cjson.decode(row[1]) or {}\n local set = {}\n for _, ref in ipairs(list) do set[ref] = true end\n held[key] = { list = list, set = set, idx = pin.idx, row = pin.row }\n held_order[#held_order + 1] = key\n end\n pin.held = held[key]\n end\nend\nlocal snapshots = {}\nfor _, entry in ipairs(entries) do\n local ref = cjson.encode({ HISTORY, tostring(entry.index) })\n for _, pin in ipairs(entry.pins) do\n if pin.held and not pin.held.set[ref] then\n pin.held.set[ref] = true\n pin.held.list[#pin.held.list + 1] = ref\n pin.held.dirty = true\n redis.call('HINCRBY', PIN_COUNTS, ref, 1)\n end\n end\n if entry.index >= boundary then\n snapshots[#snapshots + 1] = { index = entry.index, snapshot = entry.snapshot, ref = ref }\n end\nend\nfor _, key in ipairs(held_order) do\n local row = held[key]\n if row.dirty then put_row(row.idx, row.row, 'pins', cjson.encode(row.list)) end\nend\nflush()\nif #snapshots == 0 then return 1 end\n\nlocal head = redis.call('GET', HEAD)\nlocal head_index = nil\nlocal registered = false\nif head then\n local record = cjson.decode(head)\n head_index = tonumber(record['index'])\n registered = record['r'] == 1\nend\nif head_index and head_index < boundary then\n head, head_index, registered = nil, nil, false\n redis.call('ZREMRANGEBYSCORE', HISTORY, '-inf', '(' .. boundary)\nend\n-- Deletion finds each reducer's keys through this set instead of a scan.\n-- Heads written since carry the mark, so a reducer registers once.\nif not registered then\n redis.call('SADD', SESSION .. 'snap-reducers', ARGV[#ARGV])\nend\nlocal latest = snapshots[#snapshots]\nlocal writes_head = not head_index or latest.index >= head_index\nlocal advances = not head_index or latest.index > head_index\nlocal final_head_index = advances and latest.index or head_index\n\nif advances and head and tonumber(redis.call('HGET', PIN_COUNTS, cjson.encode({ HISTORY, tostring(head_index) })) or '0') > 0 then\n redis.call('ZREMRANGEBYSCORE', HISTORY, head_index, head_index)\n redis.call('ZADD', HISTORY, head_index, head)\nend\n\nfor _, write in ipairs(snapshots) do\n if write.index < final_head_index then\n redis.call('ZREMRANGEBYSCORE', HISTORY, write.index, write.index)\n if tonumber(redis.call('HGET', PIN_COUNTS, write.ref) or '0') > 0 then\n redis.call('ZADD', HISTORY, write.index, write.snapshot)\n end\n end\nend\n\nif writes_head then\n redis.call('SET', HEAD, latest.snapshot)\n redis.call('ZREMRANGEBYSCORE', HISTORY, latest.index, latest.index)\nend\nreturn 1\n`\n\n/**\n * KEYS: presence hash. ARGV: atMs, ttlMs, n, then per entry:\n * hashField, valueJson ('' deletes), patchFieldJson; finally channel and\n * message prefix. Wire fragments stay JSON text to preserve arrays\n * and numeric precision through Lua. Field-wise LWW by the sender's\n * `at`: a stored entry with a strictly newer stamp survives; ties go\n * to the incoming write (memory-tier parity). Returns the applied\n * 1-based entry positions. The whole-key PEXPIRE is the coarse bound,\n * sound because core sends one uniform ttlMs; the per-entry `e` stamp\n * is the fine bound (the injected storage clock at the applied set\n * plus ttlMs — never the sender stamp), pruned on read against the\n * same clock.\n */\nconst PRESENCE_SET_LUA = `\nlocal at = tonumber(ARGV[1])\nlocal applied = {}\nlocal published = {}\nlocal n = tonumber(ARGV[3])\nfor i = 1, n do\n local field = ARGV[3 + (i - 1) * 3 + 1]\n local value = ARGV[3 + (i - 1) * 3 + 2]\n local existing = redis.call('HGET', KEYS[1], field)\n if not existing or tonumber(cjson.decode(existing)['a']) <= at then\n if value == '' then\n redis.call('HDEL', KEYS[1], field)\n else\n redis.call('HSET', KEYS[1], field, value)\n end\n applied[#applied + 1] = tostring(i)\n published[#published + 1] = ARGV[3 + (i - 1) * 3 + 3]\n end\nend\nif #applied > 0 then\n redis.call('PEXPIRE', KEYS[1], ARGV[2])\n redis.pcall('PUBLISH', ARGV[4 + n * 3], ARGV[5 + n * 3] .. table.concat(published, ',') .. '}}')\nend\nreturn applied\n`\n\n/**\n * KEYS: presence hash. ARGV: nowMs (the injected clock, so tests can\n * time-travel expiry against a real server). Returns the live\n * [field, value, ...] pairs; entries at or past their fine-bound\n * expiry are HDELed in the same atomic step, so a stale reader can\n * never sweep a concurrently refreshed field.\n */\nconst PRESENCE_READ_LUA = `\nlocal now = tonumber(ARGV[1])\nlocal rows = redis.call('HGETALL', KEYS[1])\nlocal live = {}\nfor i = 1, #rows, 2 do\n if tonumber(cjson.decode(rows[i + 1])['e']) <= now then\n redis.call('HDEL', KEYS[1], rows[i])\n else\n live[#live + 1] = rows[i]\n live[#live + 1] = rows[i + 1]\n end\nend\nreturn live\n`\n\n/**\n * KEYS: counter, layout, head, history. ARGV: snapshot frontier or ''.\n * Returns one atomic multiversion-cache-plus-log-frontier read, with the\n * clear boundary; checkpoints from before it are ignored.\n */\nconst READ_STATE_LUA = `\nlocal session = string.sub(KEYS[1], 1, -6)\nlocal counts = redis.call('MGET', KEYS[1], session .. 'clear', KEYS[2])\nif counts[3] and counts[3] ~= '${REDIS_LAYOUT}' then\n return redis.error_reply('${LAYOUT_ERROR}')\nend\nlocal boundary = tonumber(counts[2] or '0')\nlocal head = redis.call('GET', KEYS[3])\nlocal head_index = head and cjson.decode(head)['index'] or nil\nif head and (type(head_index) ~= 'number' or head_index < 0 or head_index > 9007199254740991 or head_index ~= math.floor(head_index)) then\n return redis.error_reply('invalid snapshot head index')\nend\nif head_index and head_index < boundary then\n head = nil\n head_index = nil\nend\nlocal cap = ARGV[1]\nlocal snapshot = nil\nif head_index and (cap == '' or head_index <= tonumber(cap)) then\n snapshot = head\nelse\n local upper = cap == '' and '+inf' or cap\n local historical = redis.call('ZREVRANGEBYSCORE', KEYS[4], upper, boundary, 'LIMIT', 0, 1)\n if #historical > 0 then snapshot = historical[1] end\nend\nreturn { head_index or '', snapshot or '', counts[1] or '0', tostring(boundary) }\n`\n\n/**\n * What reclaim removes once a session has no moved log left: every\n * registered reducer's checkpoints below the latest clear and the pin counts\n * naming them (snapshot writes register each reducer in\n * {prefix}:{session}:snap-reducers, so no key scan is needed), every head\n * row below it (whole chunks, then the boundary chunk's older rows), the\n * type indexes' older entries, and the lane cursors it left idle.\n */\nconst SWEEP_LUA = `\nlocal function sweep(boundary)\n for _, reducer in ipairs(redis.call('SMEMBERS', SESSION .. 'snap-reducers')) do\n local head_key = SESSION .. 'snap:' .. reducer\n local head = redis.call('GET', head_key)\n if head and tonumber(cjson.decode(head)['index']) < boundary then\n redis.call('DEL', head_key)\n end\n redis.call('ZREMRANGEBYSCORE', SESSION .. 'snap-history:' .. reducer, '-inf', '(' .. boundary)\n end\n local counts = redis.call('HGETALL', PIN_COUNTS)\n local stale = {}\n for i = 1, #counts, 2 do\n if tonumber(cjson.decode(counts[i])[2]) < boundary then stale[#stale + 1] = counts[i] end\n end\n sliced('HDEL', PIN_COUNTS, stale)\n local raw = redis.call('GET', TYPED)\n if raw then\n for name in pairs(cjson.decode(raw)) do\n redis.call('ZREMRANGEBYSCORE', type_key(name), '-inf', '(' .. boundary)\n end\n end\n local last = math.floor((boundary - 1) / ${META_CHUNK_SIZE})\n local chunks = {}\n for chunk = 0, last - 1 do chunks[#chunks + 1] = META .. ':' .. tostring(chunk) end\n sliced('UNLINK', nil, chunks)\n local older = {}\n for _, field in ipairs(redis.call('HKEYS', META .. ':' .. tostring(last))) do\n local idx = tonumber(string.match(field, '^(%d+):'))\n if idx and idx < boundary then older[#older + 1] = field end\n end\n sliced('HDEL', META .. ':' .. tostring(last), older)\n local fields = redis.call('HGETALL', CURSORS)\n local records, order = {}, {}\n for i = 1, #fields, 2 do\n local at = string.find(fields[i], ':[^:]*$')\n local cid, suffix = string.sub(fields[i], 1, at - 1), string.sub(fields[i], at + 1)\n if not records[cid] then\n records[cid] = {}\n order[#order + 1] = cid\n end\n records[cid][suffix] = fields[i + 1]\n end\n local idle, moved = {}, {}\n for _, cid in ipairs(order) do\n local record = records[cid]\n local _, tag = parse_cid(cid)\n local head = (record.h and record.h ~= '') and tonumber(record.h) or false\n local position = tonumber(record.p or '0')\n if head and head <= boundary then head = false end\n if string.sub(tag, 1, 1) == 'G' then\n if position < boundary then position = boundary end\n moved[#moved + 1] = cid .. ':h'\n moved[#moved + 1] = head and tostring(head) or ''\n moved[#moved + 1] = cid .. ':p'\n moved[#moved + 1] = tostring(position)\n elseif not head and position <= boundary then\n for suffix in pairs(record) do idle[#idle + 1] = cid .. ':' .. suffix end\n elseif not head and record.h ~= '' then\n moved[#moved + 1] = cid .. ':h'\n moved[#moved + 1] = ''\n end\n end\n sliced('HDEL', CURSORS, idle)\n sliced('HSET', CURSORS, moved)\nend\n`\n\n/**\n * KEYS: counter, layout, marker id ownership key.\n * ARGV: sessionId, nowMs, ifLastIndex or '', cause consumer or '', cause tag,\n * cause index, cause attempt, marker id, notify channel.\n * Returns ['ok', cleared, head, boundary] | ['superseded'] | ['foreign'].\n * The old lifetime's log moves aside in one RENAME, so every read of the log\n * starts at the marker, and its ready set is unlinked, so claims and pending\n * checks see only the new lifetime; deletion drains the moved log. Type\n * indexes keep their older entries until then: lookups start above the\n * marker.\n */\nconst CLEAR_LUA = `${BASE_LUA}${WAKE_LUA}\nlocal values = read_bounds({ KEYS[3] })\nif not values then return redis.error_reply('${LAYOUT_ERROR}') end\nlocal head = total\nif ARGV[4] ~= '' then\n local idx = tonumber(ARGV[6])\n if idx <= boundary then return { 'superseded' } end\n local row = read_rows({ { idx, row_of(ARGV[4], ARGV[5]) } }, { 'd', 'f', 'p', 'l' })[1]\n if tonumber(row[1] or '0') ~= tonumber(ARGV[7]) or row[2] or row[3] or not owns(row[4], ARGV[5]) then\n return { 'superseded' }\n end\nend\nlocal expected = ARGV[3]\nif expected ~= '' and boundary == tonumber(expected) + 1 then\n return { 'ok', '1', tostring(head), tostring(boundary) }\nend\nif expected ~= '' and tonumber(expected) ~= head then\n return { 'ok', '0', tostring(head), tostring(boundary) }\nend\nif head == 0 or boundary == head then\n return { 'ok', '1', tostring(head), tostring(boundary) }\nend\nif values[1] then return { 'foreign' } end\nlocal field = tostring(head + 1)\nlocal moved = SESSION .. 'cleared:' .. field\nredis.call('RENAME', LOG, moved)\nredis.call('UNLINK', READY)\nredis.call('SADD', SESSION .. 'cleared', moved)\nredis.call('XADD', LOG, field .. '-0',\n 'id', ARGV[8], 'type', CLEAR_TYPE, 'payload', 'null',\n 'created_at', ARGV[2], 'cause', '')\nlocal marker = { KEYS[3], cjson.encode({ ARGV[1], head + 1 }), COUNTER, field, CLEAR, field }\nif schema_missing then\n marker[#marker + 1] = SCHEMA\n marker[#marker + 1] = '${REDIS_LAYOUT}'\nend\nredis.call('MSET', unpack(marker))\nwake_add(field, ARGV[8], CLEAR_TYPE, 'null')\nwake(ARGV[9], field, ARGV[2])\nreturn { 'ok', '1', field, field }\n`\n\n/**\n * KEYS: counter, layout. ARGV: page size.\n * The next deletion batch: a moved log and up to a page of its [index, id]\n * pairs, without payloads. Empty moved logs are dropped; with none left it\n * sweeps what the old lifetime left and returns nothing, so a retry\n * finishes an interrupted deletion.\n */\nconst RECLAIM_BATCH_LUA = `${BASE_LUA}${SWEEP_LUA}\nlocal boundary = tonumber(redis.call('GET', CLEAR) or '0')\nif boundary == 0 then return {} end\nfor _, log in ipairs(redis.call('SMEMBERS', SESSION .. 'cleared')) do\n local entries = redis.call('XRANGE', log, '-', '+', 'COUNT', ARGV[1])\n if #entries > 0 then\n local batch = { log }\n for _, entry in ipairs(entries) do\n local fields = entry[2]\n for i = 1, #fields, 2 do\n if fields[i] == 'id' then\n batch[#batch + 1] = string.match(entry[1], '^(%d+)')\n batch[#batch + 1] = fields[i + 1]\n break\n end\n end\n end\n return batch\n end\n redis.call('DEL', log)\n redis.call('SREM', SESSION .. 'cleared', log)\nend\nsweep(boundary)\nreturn {}\n`\n\n/**\n * KEYS: counter, layout, moved log, then one id ownership key per entry.\n * ARGV: sessionId, n, then per entry: index, id.\n * Deletes the listed entries below the latest clear and their id records.\n * An id record survives when it names another event, so an id reused in the\n * new lifetime keeps its owner. Returns [deleted, done].\n */\nconst RECLAIM_LUA = `${BASE_LUA}${SWEEP_LUA}\nlocal boundary = tonumber(redis.call('GET', CLEAR) or '0')\nlocal MOVED = KEYS[3]\nlocal id_keys, indexes, stream_ids = {}, {}, {}\n-- A concurrent reclaim may list the same batch; only XDEL says who removed it.\nlocal removed = 0\nfor i = 1, tonumber(ARGV[2]) do\n local field = ARGV[2 + (i - 1) * 2 + 1]\n if tonumber(field) < boundary then\n id_keys[#id_keys + 1] = KEYS[3 + i]\n indexes[#id_keys] = tonumber(field)\n stream_ids[#id_keys] = field .. '-0'\n end\nend\nif #id_keys > 0 then\n local owners, stale_ids = sliced('MGET', nil, id_keys), {}\n for i = 1, #id_keys do\n if owners[i] then\n local rec = cjson.decode(owners[i])\n if rec[1] == ARGV[1] and tonumber(rec[2]) == indexes[i] then\n stale_ids[#stale_ids + 1] = id_keys[i]\n end\n end\n end\n sliced('DEL', nil, stale_ids)\n removed = redis.call('XDEL', MOVED, unpack(stream_ids))\nend\nlocal done = 0\nif redis.call('XLEN', MOVED) == 0 then\n redis.call('DEL', MOVED)\n redis.call('SREM', SESSION .. 'cleared', MOVED)\n if redis.call('SCARD', SESSION .. 'cleared') == 0 then\n sweep(boundary)\n done = 1\n end\nend\nreturn { removed, done }\n`\n\n// ── wire helpers ─────────────────────────────────────────────────────\n\nexport type StreamEntry = [id: string, fields: string[]]\n\nconst fieldMap = (fields: string[]): Record<string, string> => {\n const map: Record<string, string> = {}\n for (let i = 0; i + 1 < fields.length; i += 2) {\n map[fields[i]!] = fields[i + 1]!\n }\n return map\n}\n\nconst entryIndex = (entryId: string): number =>\n Number(entryId.slice(0, entryId.indexOf('-')))\n\nconst toEvent = (sessionId: string, entry: StreamEntry): Event => {\n const fields = fieldMap(entry[1])\n return {\n id: fields['id']!,\n type: fields['type']!,\n payload: JSON.parse(fields['payload']!),\n index: entryIndex(entry[0]),\n sessionId,\n createdAt: new Date(Number(fields['created_at'])),\n }\n}\n\n/**\n * A stream entry's causal edge. Releases before consumers (0.17 to 0.20)\n * recorded only the parent's index and attempt: their edge reads as the\n * handlers table's, `a2.handlers`, as Postgres's upgrade names it, with no\n * lane, since nothing reads an earlier release's lane.\n */\nconst decodeStreamCause = (raw: string): EventCause | null => {\n const parsed = JSON.parse(raw) as { consumer?: unknown }\n return decodeCause(\n parsed.consumer === undefined\n ? { consumer: 'a2.handlers', lane: null, ...parsed }\n : parsed,\n )\n}\n\n/** A stream entry with its causal edge. */\nconst toStoredEvent = (sessionId: string, entry: StreamEntry): StoredEvent => {\n const cause = fieldMap(entry[1])['cause']\n return Object.assign(toEvent(sessionId, entry), {\n cause:\n cause === undefined || cause === '' ? null : decodeStreamCause(cause),\n })\n}\n\ntype ByteStreamEntry = [Uint8Array, Uint8Array[]]\n\nconst fieldBytes = (name: string): Uint8Array => new TextEncoder().encode(name)\nconst ID_FIELD = fieldBytes('id')\nconst TYPE_FIELD = fieldBytes('type')\nconst PAYLOAD_FIELD = fieldBytes('payload')\nconst CREATED_AT_FIELD = fieldBytes('created_at')\n// A leading U+FEFF is part of a stored id or type, never a byte-order mark.\nconst utf8 = new TextDecoder('utf-8', { ignoreBOM: true })\n\nconst isField = (name: Uint8Array, field: Uint8Array): boolean => {\n if (name.length !== field.length) return false\n for (let at = 0; at < field.length; at++)\n if (name[at] !== field[at]) return false\n return true\n}\n\nconst bytesToSerializedEvent = (\n sessionId: string,\n entry: ByteStreamEntry,\n): SerializedEvent => {\n const values = entry[1]\n let id: string | undefined\n let type: string | undefined\n let payloadJson: string | undefined\n let createdAt: string | undefined\n for (let at = 0; at + 1 < values.length; at += 2) {\n const name = values[at]!\n if (isField(name, ID_FIELD)) id = utf8.decode(values[at + 1])\n else if (isField(name, TYPE_FIELD)) type = utf8.decode(values[at + 1])\n else if (isField(name, PAYLOAD_FIELD))\n payloadJson = utf8.decode(values[at + 1])\n else if (isField(name, CREATED_AT_FIELD))\n createdAt = utf8.decode(values[at + 1])\n }\n return {\n id: id!,\n type: type!,\n payloadJson: payloadJson!,\n index: entryIndex(utf8.decode(entry[0])),\n sessionId,\n createdAtMs: Number(createdAt),\n }\n}\n\nconst toSerializedEvent = (\n sessionId: string,\n entry: StreamEntry,\n): SerializedEvent => {\n const fields = fieldMap(entry[1])\n return {\n id: fields['id']!,\n type: fields['type']!,\n payloadJson: fields['payload']!,\n index: entryIndex(entry[0]),\n sessionId,\n createdAtMs: Number(fields['created_at']),\n }\n}\n\nconst json = (value: unknown): string => JSON.stringify(value) ?? 'null'\n\n/** How scripts name a cursor within its consumer: `#`, or `L` and its lane. */\nconst tagOf = (lane: string | null): string =>\n lane === null ? '#' : `L${lane}`\n\n/** The lane a cursor tag names: null for an event cursor. */\nconst laneOf = (tag: string): string | null =>\n tag.startsWith('#') ? null : tag.slice(1)\n\nconst targetsArg = (targets: readonly EventTarget[] | undefined): string =>\n targets === undefined || targets.length === 0\n ? ''\n : JSON.stringify(\n targets.map((target) => [target.consumer, tagOf(target.lane)]),\n )\n\n/** A log consumer's types, as scripts read them; '*' takes every type. */\nconst logTypesArg = (types: readonly string[] | null): string =>\n types === null ? '*' : JSON.stringify(types)\n\n/** The served consumers, as scripts read them; '' serves every consumer. */\nconst servedArg = (consumers: readonly StoreConsumer[] | undefined): string =>\n consumers === undefined\n ? ''\n : JSON.stringify(\n consumers.map((consumer) =>\n consumer.log\n ? [\n consumer.name,\n consumer.log.lane,\n logTypesArg(consumer.log.types),\n consumer.log.backfill ? 1 : 0,\n ]\n : [consumer.name],\n ),\n )\n\ntype Column = string | number | null | undefined\n\n/** A head's dispatch state from a row's HEAD_COLUMNS. */\nconst dispatchOf = (\n consumer: string,\n lane: string | null,\n columns: readonly Column[] | null | undefined,\n): HeadDispatch => {\n const column = (at: number): string | null => {\n const value = columns?.[at]\n return value === null || value === undefined ? null : String(value)\n }\n const date = (at: number): Date | null => {\n const value = column(at)\n return value === null ? null : new Date(Number(value))\n }\n const count = (at: number): number | null => {\n const value = column(at)\n return value === null ? null : Number(value)\n }\n return {\n consumer,\n lane,\n firstClaimedAt: date(1),\n lastClaimedAt: date(2),\n attemptCount: count(0) ?? 0,\n claimHolder: column(3),\n claimExpiresAt: date(4),\n failureCount: count(5) ?? 0,\n lastFailedAt: date(6),\n lastFailedAttempt: count(7),\n lastError: column(8),\n failedAt: date(9),\n }\n}\n\ntype HeadTuple = [\n consumer: string,\n tag: string,\n index: string,\n columns: Column[],\n entry: StreamEntry,\n]\n\nconst toHeadEvent = (sessionId: string, head: HeadTuple): HeadEvent => ({\n ...toStoredEvent(sessionId, head[4]),\n ...dispatchOf(head[0], laneOf(head[1]), head[3]),\n})\n\nconst compareText = (a: string, b: string): number =>\n a < b ? -1 : a > b ? 1 : 0\n\nconst byHead = (a: HeadRef, b: HeadRef): number =>\n a.index - b.index || compareText(a.consumer, b.consumer)\n\n/** Consumer, then lane, with concurrent cursors last by head. */\nconst byCursor = (a: StoredCursor, b: StoredCursor): number =>\n compareText(a.consumer, b.consumer) ||\n (a.lane === null\n ? b.lane === null\n ? (a.head ?? 0) - (b.head ?? 0)\n : 1\n : b.lane === null\n ? -1\n : compareText(a.lane, b.lane))\n\nconst toClaimAvailableResult = (\n sessionId: string,\n reply: unknown[],\n): StoreClaimAvailableResult => {\n const outcome = String(reply[0])\n if (outcome === 'claimed') {\n const count = Number(reply[1])\n const events = (reply.slice(2, 2 + count) as HeadTuple[])\n .map((head) => toHeadEvent(sessionId, head))\n .toSorted(byHead)\n const clearIndex = Number(reply[2 + count] ?? 0)\n if (clearIndex > 0)\n for (const event of events) event.clearIndex = clearIndex\n return { outcome: 'claimed', events, head: Number(reply[3 + count]) }\n }\n if (outcome === 'busy') {\n return { outcome, retryAt: new Date(Number(reply[1])) }\n }\n if (outcome === 'settled') return { outcome }\n throw new TypeError(\n `claim referenced a missing event in session '${sessionId}'`,\n )\n}\n\nconst toIndex = ({\n raw,\n sessionId,\n name,\n}: {\n raw: unknown\n sessionId: string\n name: string\n}): number => {\n const index = Number(raw ?? 0)\n if (!Number.isSafeInteger(index) || index < 0) {\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n `redis ${name} for session '${sessionId}' is invalid`,\n )\n }\n return index\n}\n\n/**\n * A range read that met a gap. A clear moves the old lifetime's log aside, so\n * a read whose bounds predate it finds nothing where it expected events;\n * anywhere else a gap is corruption.\n */\nconst rangeGap = (sessionId: string, message: string): A2Error =>\n Object.assign(\n new A2Error(\n 'STORE_UNAVAILABLE',\n `redis event range for session '${sessionId}' ${message}`,\n ),\n { rangeGap: true },\n )\n\nconst isRangeGap = (error: unknown): boolean =>\n error instanceof A2Error &&\n (error as { rangeGap?: boolean }).rangeGap === true\n\n/**\n * Run a whole read, bounds included. A gap means a clear raced it, so the\n * read runs once more from fresh bounds. A second gap, from another clear or\n * from corruption, surfaces as the retryable STORE_UNAVAILABLE.\n */\nconst retryAfterGap = async <T>(read: () => Promise<T>): Promise<T> => {\n try {\n return await read()\n } catch (error) {\n if (!isRangeGap(error)) throw error\n return read()\n }\n}\n\nconst wrap = async <T>(fn: () => Promise<T>): Promise<T> => {\n try {\n return await fn()\n } catch (err) {\n if (err instanceof A2Error || err instanceof TypeError) throw err\n throw new A2Error('STORE_UNAVAILABLE', 'redis store operation failed', {\n cause: err,\n })\n }\n}\n\nexport function createRedisStoreCore(\n options: RedisStoreCoreOptions,\n): RedisStoreCore {\n const { call, callBytes, clock } = options\n const generateId = options.ids\n const prefix = options.keyPrefix\n\n const storeKey = (s: string): string => `${prefix}:${s}:log`\n const countKey = (s: string): string => `${prefix}:${s}:count`\n // Scripts derive the session's other keys from the counter; keep them in step.\n const clearKey = (s: string): string => `${prefix}:${s}:clear`\n const schemaKey = `${prefix}:schema`\n const snapKey = (s: string, r: string): string => `${prefix}:${s}:snap:${r}`\n const snapHistoryKey = (s: string, r: string): string =>\n `${prefix}:${s}:snap-history:${r}`\n const presenceKey = (s: string): string => `${prefix}:${s}:presence`\n const notifyChannel = (s: string): string => `${prefix}:${s}:notify`\n // One ownership key per event id; reclaim deletes those naming cleared events.\n // Session ids are raw and may contain colons, so a raw id could name a\n // session key (`ids:count`). A JSON string with escaped colons starts with\n // a quote and has no colon, so it never matches a session key's suffix.\n const idKey = (id: string): string =>\n `${prefix}:ids:${JSON.stringify(id).replaceAll(':', '\\\\u003a')}`\n\n const layoutError = (): A2Error =>\n new A2Error(\n 'STORE_NOT_CONFIGURED',\n `the redis keys under prefix '${prefix}' hold an A2 layout other than '${REDIS_LAYOUT}', from another release; point the store at a fresh keyPrefix, or give the contract a new name so it starts new sessions`,\n )\n\n /** Runs a script over a session: KEYS start with its counter and the layout key. */\n const evalScript = async (\n script: string,\n sessionId: string,\n keys: string[],\n args: Array<string | number>,\n ): Promise<unknown> => {\n const allKeys = [countKey(sessionId), schemaKey, ...keys]\n try {\n return await call('EVAL', script, allKeys.length, ...allKeys, ...args)\n } catch (error) {\n if (String((error as Error)?.message).includes(LAYOUT_ERROR))\n throw layoutError()\n throw error\n }\n }\n\n /**\n * Runs a write that may need a type's index to reach further back first.\n * Such a write returns ['index', type, after] having written nothing, and\n * runs again once INDEX_TYPE_LUA has indexed that history, a page per call.\n */\n const indexing = async (\n sessionId: string,\n run: () => Promise<unknown>,\n ): Promise<unknown[]> => {\n for (;;) {\n // oxlint-disable-next-line no-await-in-loop -- each run follows the indexing it asked for\n const reply = (await run()) as unknown[]\n if (reply[0] !== 'index') return reply\n for (;;) {\n // oxlint-disable-next-line no-await-in-loop -- each page lowers the index's floor\n const [done] = (await evalScript(\n INDEX_TYPE_LUA,\n sessionId,\n [],\n [String(reply[1]), Number(reply[2])],\n )) as [string]\n if (done === '1') break\n }\n }\n }\n\n /** The frontier and clear marker, checking the layout in the same MGET. */\n const readBounds = async (\n sessionId: string,\n ): Promise<{ frontier: number; clearIndex: number }> => {\n const [count, cleared, schema] = (await call(\n 'MGET',\n countKey(sessionId),\n clearKey(sessionId),\n schemaKey,\n )) as [unknown, unknown, unknown]\n if (schema !== null && schema !== undefined && schema !== REDIS_LAYOUT) {\n throw layoutError()\n }\n return {\n frontier: toIndex({ raw: count, sessionId, name: 'event count' }),\n clearIndex: toIndex({ raw: cleared, sessionId, name: 'clear boundary' }),\n }\n }\n\n const readEventPages = async (\n sessionId: string,\n afterIndex: number,\n throughIndex: number,\n ): Promise<Event[]> => {\n if (throughIndex <= afterIndex) return []\n const events: Event[] = []\n let nextIndex = afterIndex + 1\n while (nextIndex <= throughIndex) {\n // oxlint-disable-next-line no-await-in-loop -- each page advances the dense frozen range\n const entries = (await call(\n 'XRANGE',\n storeKey(sessionId),\n `${nextIndex}-0`,\n `${throughIndex}-0`,\n 'COUNT',\n READ_PAGE_SIZE,\n )) as StreamEntry[]\n if (entries.length === 0) {\n throw rangeGap(sessionId, `ended before index ${throughIndex}`)\n }\n for (let position = 0; position < entries.length; position += 1) {\n const index = entryIndex(entries[position]![0])\n if (index !== nextIndex + position || index > throughIndex) {\n throw rangeGap(sessionId, 'is not contiguous')\n }\n }\n events.push(...entries.map((entry) => toEvent(sessionId, entry)))\n nextIndex = events.at(-1)!.index + 1\n }\n return events\n }\n\n const readEventRange = async (\n sessionId: string,\n afterIndex: number,\n throughIndex: number,\n ): Promise<Event[]> => {\n const events: Event[] = []\n for (\n let start = afterIndex;\n start < throughIndex;\n start += READ_PAGE_SIZE * READ_CONCURRENCY\n ) {\n const ranges = Array.from(\n {\n length: Math.min(\n READ_CONCURRENCY,\n Math.ceil((throughIndex - start) / READ_PAGE_SIZE),\n ),\n },\n (_, position) => {\n const after = start + position * READ_PAGE_SIZE\n return readEventPages(\n sessionId,\n after,\n Math.min(after + READ_PAGE_SIZE, throughIndex),\n )\n },\n )\n // oxlint-disable-next-line no-await-in-loop -- bound in-flight reads and settle every page before returning\n const results = await Promise.allSettled(ranges)\n for (const result of results) {\n if (result.status === 'rejected') throw result.reason\n events.push(...result.value)\n }\n }\n return events\n }\n\n /** Bounded XRANGE pages restore the full-read contract on capped providers. */\n const readRange = async (\n sessionId: string,\n afterIndex: number,\n throughIndex?: number,\n ): Promise<StoredEvent[]> =>\n (await readBoundedRange(sessionId, afterIndex, throughIndex)).rows\n\n /** `readRange`, with the clear boundary its rows start at. */\n const readBoundedRange = async (\n sessionId: string,\n afterIndex: number,\n throughIndex?: number,\n ): Promise<{ rows: StoredEvent[]; clearIndex: number }> => {\n if (throughIndex !== undefined && throughIndex <= afterIndex)\n return { rows: [], clearIndex: 0 }\n const { frontier: currentFrontier, clearIndex } =\n await readBounds(sessionId)\n const frontier = Math.min(throughIndex ?? currentFrontier, currentFrontier)\n // Reads start at the latest clear marker.\n const after = Math.max(afterIndex, clearIndex - 1)\n if (frontier <= after) return { rows: [], clearIndex }\n\n const rows: StoredEvent[] = []\n let nextIndex = after + 1\n while (nextIndex <= frontier) {\n // oxlint-disable-next-line no-await-in-loop -- each page advances the dense frozen range\n const entries = (await call(\n 'XRANGE',\n storeKey(sessionId),\n `${nextIndex}-0`,\n `${frontier}-0`,\n 'COUNT',\n READ_PAGE_SIZE,\n )) as StreamEntry[]\n if (entries.length === 0) {\n throw rangeGap(sessionId, `ended before index ${frontier}`)\n }\n const page = entries.map((entry) => toStoredEvent(sessionId, entry))\n for (let position = 0; position < page.length; position += 1) {\n if (page[position]!.index !== nextIndex + position) {\n throw rangeGap(sessionId, 'is not contiguous')\n }\n }\n const lastIndex = page.at(-1)!.index\n if (lastIndex < nextIndex || lastIndex > frontier) {\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n `redis event range for session '${sessionId}' did not advance`,\n )\n }\n rows.push(...page)\n nextIndex = lastIndex + 1\n }\n return { rows, clearIndex }\n }\n\n const readStream = options.readStream\n return {\n ...(readStream\n ? {\n streamEvents: async function* ({\n sessionId,\n afterIndex,\n signal,\n }: {\n sessionId: string\n afterIndex: number\n signal: AbortSignal\n }): AsyncGenerator<SerializedEvent[], void> {\n try {\n for await (const entries of readStream({\n key: storeKey(sessionId),\n from: `${afterIndex + 1}-0`,\n signal,\n })) {\n signal.throwIfAborted()\n yield entries.map((entry) =>\n toSerializedEvent(sessionId, entry),\n )\n }\n } catch (cause) {\n if (cause instanceof A2Error) throw cause\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n 'redis stream read failed',\n { cause },\n )\n }\n },\n }\n : {}),\n async append(sessionId, events, appendOptions) {\n return wrap(async () => {\n const supplied = events.filter((e) => e.id !== undefined)\n const suppliedIds = supplied.map((e) => e.id as string)\n if (new Set(suppliedIds).size !== suppliedIds.length) {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n 'batch contains the same event id more than once',\n )\n }\n const now = clock.now().getTime()\n const withIds = events.map((e) => ({\n id: e.id ?? generateId(),\n type: e.type,\n payload: e.payload,\n cause: e.cause ? { ...e.cause } : null,\n targets: e.targets,\n }))\n const reply = (await indexing(sessionId, () =>\n evalScript(\n APPEND_LUA,\n sessionId,\n withIds.map((e) => idKey(e.id)),\n [\n sessionId,\n now,\n withIds.length,\n servedArg(appendOptions?.consumers),\n notifyChannel(sessionId),\n appendOptions?.originIndex ?? '',\n ...withIds.flatMap((e) => [\n e.id,\n e.type,\n json(e.payload),\n e.cause ? json(e.cause) : '',\n targetsArg(e.targets),\n ]),\n ],\n ),\n )) as string[]\n\n const [verdict] = reply\n if (verdict === 'empty') {\n return { events: [], hasPending: reply[1] === '1' }\n }\n if (verdict === 'foreign') {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n `event id '${reply[1]}' already exists in another session`,\n )\n }\n if (verdict === 'cleared') {\n throw sessionClearedError(\n `scheduled work from event ${reply[1]} was cleared in session '${sessionId}'`,\n )\n }\n if (verdict === 'superseded') {\n throw supersededAttemptError(\n sessionId,\n withIds[Number(reply[1]) - 1]!.cause!,\n )\n }\n if (verdict === 'partial') {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n `batch mixes ${reply[1]} already-appended and ${events.length - Number(reply[1])} fresh events`,\n )\n }\n if (verdict === 'dup') {\n const hasPending = reply[1] === '1'\n const firstIndex = Math.min(...reply.slice(2).map(Number))\n // A valid retry occupies exactly events.length consecutive indexes.\n const { rows: existing, clearIndex } = await retryAfterGap(() =>\n readBoundedRange(\n sessionId,\n firstIndex - 1,\n firstIndex + events.length - 1,\n ),\n )\n if (firstIndex < clearIndex) {\n // Cleared after the script matched the retry: the ids are free.\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n `session '${sessionId}' was cleared while an append retry replayed; retry it`,\n )\n }\n return {\n events: idempotentReplay(events, existing),\n hasPending,\n }\n }\n const base = Number(reply[1]) - 1\n return {\n events: withIds.map((e, i) => ({\n id: e.id,\n type: e.type,\n payload: structuredClone(e.payload),\n index: base + 1 + i,\n sessionId,\n createdAt: new Date(now),\n cause: e.cause,\n })),\n hasPending: reply[2] === '1',\n }\n })\n },\n\n async cursors(sessionId) {\n return wrap(async () => {\n const reply = (await evalScript(\n CURSORS_LUA,\n sessionId,\n [],\n [],\n )) as Array<\n [\n consumer: string,\n tag: string,\n position: string,\n head: string | null,\n columns: Column[],\n ]\n >\n return reply\n .map(([consumer, tag, position, head, columns]): StoredCursor =>\n Object.assign(dispatchOf(consumer, laneOf(tag), columns), {\n position: Number(position),\n head: head === null ? null : Number(head),\n }),\n )\n .toSorted(byCursor)\n })\n },\n\n async read(sessionId, opts) {\n return wrap(() =>\n retryAfterGap(() =>\n readRange(sessionId, opts?.afterIndex ?? 0, opts?.throughIndex),\n ),\n )\n },\n\n async readHistory(sessionId, opts) {\n const after = opts?.afterIndex ?? 0\n if (opts?.throughIndex !== undefined && opts.throughIndex <= after)\n return []\n return wrap(() =>\n retryAfterGap(async () => {\n const { frontier, clearIndex } = await readBounds(sessionId)\n // Reads start at the latest clear marker.\n return readEventRange(\n sessionId,\n Math.max(after, clearIndex - 1),\n Math.min(opts?.throughIndex ?? frontier, frontier),\n )\n }),\n )\n },\n\n async claimAvailable({\n sessionId,\n holder,\n ttlMs,\n expiresAtMs,\n exclude,\n consumers,\n maxFailures,\n through,\n }) {\n return wrap(async () => {\n const now = clock.now().getTime()\n const expiresAt = expiresAtMs ?? now + ttlMs\n const excluded = exclude ?? []\n const reply = (await evalScript(\n CLAIM_AVAILABLE_LUA,\n sessionId,\n [],\n [\n holder,\n now,\n expiresAt,\n maxFailures ?? '',\n servedArg(consumers),\n through ?? '',\n excluded.length,\n ...excluded.flatMap((ref) => [\n ref.consumer,\n ref.lane === null ? `#${ref.index}` : `L${ref.lane}`,\n ]),\n ],\n )) as unknown[]\n return toClaimAvailableResult(sessionId, reply)\n })\n },\n\n async renewClaims({ sessionId, holder, claims, ttlMs, expiresAtMs }) {\n if (claims.length === 0) return { renewed: [], superseded: [] }\n return wrap(async () => {\n const now = clock.now().getTime()\n const expiresAt = expiresAtMs ?? now + ttlMs\n const reply = (await evalScript(\n RENEW_CLAIMS_LUA,\n sessionId,\n [],\n [\n holder,\n now,\n expiresAt,\n claims.length,\n ...claims.flatMap((claim) => [\n claim.consumer,\n tagOf(claim.lane),\n claim.index,\n claim.attempt,\n ]),\n ],\n )) as string[]\n const renewedCount = Number(reply[0])\n const refs = reply.slice(1).map((position): HeadRef => {\n const { consumer, lane, index } = claims[Number(position) - 1]!\n return { consumer, lane, index }\n })\n return {\n renewed: refs.slice(0, renewedCount).toSorted(byHead),\n superseded: refs.slice(renewedCount).toSorted(byHead),\n }\n })\n },\n\n async completeAttempt({\n sessionId,\n consumer,\n lane,\n index,\n attempt,\n events,\n consumers,\n }) {\n return wrap(async () => {\n const ids = events.map((event) => event.id)\n if (new Set(ids).size !== ids.length) {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n 'batch contains the same event id more than once',\n )\n }\n const now = clock.now().getTime()\n const cause: EventCause = { consumer, lane, index, attempt }\n const reply = (await indexing(sessionId, () =>\n evalScript(\n COMPLETE_ATTEMPT_LUA,\n sessionId,\n ids.map((id) => idKey(id)),\n [\n sessionId,\n now,\n consumer,\n tagOf(lane),\n index,\n attempt,\n json(cause),\n servedArg(consumers),\n notifyChannel(sessionId),\n json(ids),\n events.length,\n ...events.flatMap((event) => [\n event.id,\n event.type,\n json(event.payload),\n targetsArg(event.targets),\n ]),\n ],\n ),\n )) as string[]\n const outcome = reply[0]\n if (outcome === 'missing') {\n throw new TypeError(\n `no event at index ${index} in session '${sessionId}'`,\n )\n }\n if (outcome === 'partial') {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n 'returned event ids do not match the completed attempt',\n )\n }\n if (outcome === 'superseded') return { outcome }\n\n if (outcome === 'ok') {\n const base = Number(reply[1]) - 1\n return {\n outcome: 'completed',\n events: events.map((event, i) => ({\n id: event.id,\n type: event.type,\n payload: structuredClone(event.payload),\n index: base + 1 + i,\n sessionId,\n createdAt: new Date(now),\n cause: { ...cause },\n })),\n } satisfies CompleteAttemptResult\n }\n if (outcome === 'duplicate') {\n if (events.length === 0) {\n return { outcome: 'completed', events: [] }\n }\n const firstIndex = Math.min(...reply.slice(1).map(Number))\n const { rows: existing, clearIndex } = await retryAfterGap(() =>\n readBoundedRange(\n sessionId,\n firstIndex - 1,\n firstIndex + events.length - 1,\n ),\n )\n // Cleared after the script matched the retry.\n if (index <= clearIndex) return { outcome: 'superseded' }\n if (\n existing.some(\n (row) =>\n row.cause?.consumer !== consumer ||\n row.cause.lane !== lane ||\n row.cause.index !== index ||\n row.cause.attempt !== attempt,\n )\n ) {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n 'returned event retry does not match the committed batch',\n )\n }\n return {\n outcome: 'completed',\n events: idempotentReplay(events, existing),\n }\n }\n throw new TypeError(`unexpected completion outcome '${outcome}'`)\n })\n },\n\n async failAttempt({\n sessionId,\n consumer,\n lane,\n index,\n attempt,\n error,\n maxFailures,\n retryAfterMs,\n }) {\n return wrap(async () => {\n const now = clock.now().getTime()\n const reply = (await evalScript(\n FAIL_ATTEMPT_LUA,\n sessionId,\n [],\n [\n consumer,\n tagOf(lane),\n index,\n attempt,\n error,\n maxFailures,\n now,\n retryAfterMs === undefined ? '' : now + retryAfterMs,\n ],\n )) as string[]\n const outcome = reply[0]\n if (outcome === 'missing') {\n throw new TypeError(\n `no event at index ${index} in session '${sessionId}'`,\n )\n }\n return {\n outcome: outcome as 'failed' | 'dead_lettered' | 'superseded',\n failureCount: Number(reply[1]),\n }\n })\n },\n\n async deadLetters(sessionId) {\n return wrap(async () => {\n const reply = (await evalScript(\n DEAD_LETTERS_LUA,\n sessionId,\n [],\n [],\n )) as HeadTuple[]\n return reply\n .map((head) => toHeadEvent(sessionId, head))\n .toSorted(byHead)\n })\n },\n\n async redrive({ sessionId, consumer, index, action, consumers }) {\n return wrap(async () => {\n const [redriven, hasPending] = (await indexing(sessionId, () =>\n evalScript(\n REDRIVE_LUA,\n sessionId,\n [],\n [\n consumer,\n index,\n action,\n clock.now().getTime(),\n servedArg(consumers),\n ],\n ),\n )) as [string, string]\n return { redriven: redriven === '1', hasPending: hasPending === '1' }\n })\n },\n\n async wakeLog({ sessionId, consumer }) {\n return wrap(async () => {\n const { name, log } = consumer\n const [, position, head, columns, sessionHead] = (await indexing(\n sessionId,\n () =>\n evalScript(\n WAKE_LOG_LUA,\n sessionId,\n [],\n [name, log.lane, logTypesArg(log.types), log.backfill ? 1 : 0],\n ),\n )) as [\n ok: string,\n position: string,\n head: string | null,\n columns: Column[],\n sessionHead: string,\n ]\n return {\n cursor: Object.assign(dispatchOf(name, log.lane, columns), {\n position: Number(position),\n head: head === null ? null : Number(head),\n }),\n head: Number(sessionHead),\n }\n })\n },\n\n async readState(sessionId, reducerName, stateOptions) {\n return wrap(() =>\n retryAfterGap(async () => {\n const reply = (await evalScript(\n READ_STATE_LUA,\n sessionId,\n [\n snapKey(sessionId, reducerName),\n snapHistoryKey(sessionId, reducerName),\n ],\n [\n stateOptions?.snapshotThroughIndex ??\n stateOptions?.throughIndex ??\n '',\n ],\n )) as [number | string, string, string, string]\n const rawHead = reply[0]\n const raw = reply[1]\n const headIndex = rawHead === '' ? null : Number(rawHead)\n const parsed =\n raw === ''\n ? null\n : (JSON.parse(raw) as { index: number; state: unknown })\n const currentFrontier = Number(reply[2])\n if (\n !Number.isSafeInteger(currentFrontier) ||\n currentFrontier < 0 ||\n (headIndex !== null &&\n (!Number.isSafeInteger(headIndex) || headIndex < 0)) ||\n (parsed !== null &&\n (!Number.isSafeInteger(parsed.index) || parsed.index < 0))\n ) {\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n `redis state frontier for session '${sessionId}' is invalid`,\n )\n }\n const frontier = Math.min(\n currentFrontier,\n stateOptions?.throughIndex ?? currentFrontier,\n )\n const snapshot = parsed\n const clearIndex = Number(reply[3] ?? 0)\n // The fold starts at the latest clear marker when no checkpoint is newer.\n const events = await readEventRange(\n sessionId,\n Math.max(snapshot?.index ?? 0, clearIndex - 1),\n frontier,\n )\n return {\n ...(clearIndex > 0 ? { clearIndex } : {}),\n headIndex,\n snapshot: snapshot\n ? { index: snapshot.index, state: snapshot.state }\n : null,\n events,\n }\n }),\n )\n },\n\n async putSnapshots(sessionId, reducerName, writes) {\n const byIndex = new Map<\n number,\n { index: number; state: unknown; pins: Map<string, HeadRef> }\n >()\n for (const write of writes) {\n const existing = byIndex.get(write.index)\n const pins = existing?.pins ?? new Map<string, HeadRef>()\n for (const pin of write.pins ?? []) {\n pins.set(JSON.stringify([pin.consumer, pin.lane, pin.index]), pin)\n }\n if (existing) existing.state = write.state\n else\n byIndex.set(write.index, {\n index: write.index,\n state: write.state,\n pins,\n })\n }\n const normalized = [...byIndex.values()].toSorted(\n (a, b) => a.index - b.index,\n )\n if (normalized.length === 0) return\n await wrap(async () => {\n const args: Array<string | number> = [normalized.length]\n for (const write of normalized) {\n args.push(\n write.index,\n json({\n index: write.index,\n state: write.state,\n updatedAt: clock.now().getTime(),\n // This reducer is in the session's registry (PUT_SNAPSHOTS_LUA).\n r: 1,\n }),\n write.pins.size,\n ...[...write.pins.values()].flatMap((pin) => [\n pin.consumer,\n tagOf(pin.lane),\n pin.index,\n ]),\n )\n }\n await evalScript(\n PUT_SNAPSHOTS_LUA,\n sessionId,\n [\n snapKey(sessionId, reducerName),\n snapHistoryKey(sessionId, reducerName),\n ],\n [...args, reducerName],\n )\n })\n },\n\n presence: {\n async set(ns, participant, values, meta) {\n return wrap(async () => {\n const entries = Object.entries(values)\n if (entries.length === 0) return null\n const atMs = meta.at.getTime()\n // Expiry anchors on the injected storage clock — the sender's\n // `a` stamp orders writes, the `e` stamp bounds their\n // lifetime, and the two clocks never mix.\n const expiresAtMs = clock.now().getTime() + meta.ttlMs\n // The hash field is JSON `[participant, field]`: both are\n // caller strings, so no separator character is\n // collision-safe — the JSON array is.\n const applied = (await call(\n 'EVAL',\n PRESENCE_SET_LUA,\n 1,\n presenceKey(ns),\n atMs,\n meta.ttlMs,\n entries.length,\n ...entries.flatMap(([field, value]) => [\n JSON.stringify([participant, field]),\n value === null\n ? ''\n : json({\n v: value,\n s: meta.seen,\n a: atMs,\n e: expiresAtMs,\n }),\n `${json(field)}:${json(value)}`,\n ]),\n presenceKey(ns),\n `${json({ src: options.presenceSource, participant, seen: meta.seen, at: atMs }).slice(0, -1)},\"values\":{`,\n )) as string[]\n if (applied.length === 0) return null\n // Field-keyed and caller-named — null-prototype, like every\n // presence map (see `nullProtoRecord`).\n const appliedValues: Record<string, unknown> = nullProtoRecord()\n for (const position of applied) {\n const [field, value] = entries[Number(position) - 1]!\n appliedValues[field] = structuredClone(value)\n }\n return {\n participant,\n values: appliedValues,\n seen: meta.seen,\n at: new Date(atMs),\n }\n })\n },\n\n async read(ns) {\n return wrap(async () => {\n const reply = (await call(\n 'EVAL',\n PRESENCE_READ_LUA,\n 1,\n presenceKey(ns),\n clock.now().getTime(),\n )) as string[]\n const rows: PresenceRow[] = []\n for (let i = 0; i + 1 < reply.length; i += 2) {\n const [participant, field] = JSON.parse(reply[i]!) as [\n string,\n string,\n ]\n const entry = JSON.parse(reply[i + 1]!) as {\n v: unknown\n s: number\n a: number\n e: number\n }\n rows.push({\n participant,\n field,\n value: entry.v,\n seen: entry.s,\n at: new Date(entry.a),\n expiresAt: new Date(entry.e),\n })\n }\n return rows\n })\n },\n },\n\n async readRange(sessionId, afterIndex, throughIndex) {\n return wrap(() => readRange(sessionId, afterIndex, throughIndex))\n },\n\n async readTail(sessionId, fromIndex, throughIndex) {\n return wrap(async () => {\n const newest = (await call(\n 'XREVRANGE',\n storeKey(sessionId),\n throughIndex === undefined ? '+' : `${throughIndex}-0`,\n `${fromIndex}-0`,\n 'COUNT',\n READ_PAGE_SIZE,\n )) as StreamEntry[]\n const page = newest\n .toReversed()\n .map((entry) => toEvent(sessionId, entry))\n for (let position = 1; position < page.length; position += 1) {\n if (page[position]!.index !== page[position - 1]!.index + 1) {\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n `redis event range for session '${sessionId}' is not contiguous`,\n )\n }\n }\n const oldest = page[0]?.index\n return oldest === undefined || oldest <= fromIndex ? page : null\n })\n },\n\n async clear({ sessionId, ifLastIndex, cause }) {\n return wrap(async () => {\n const markerId = generateId()\n const reply = (await evalScript(\n CLEAR_LUA,\n sessionId,\n [idKey(markerId)],\n [\n sessionId,\n clock.now().getTime(),\n ifLastIndex ?? '',\n cause?.consumer ?? '',\n cause ? tagOf(cause.lane) : '',\n cause?.index ?? '',\n cause?.attempt ?? '',\n markerId,\n notifyChannel(sessionId),\n ],\n )) as string[]\n if (reply[0] === 'superseded') {\n throw supersededAttemptError(sessionId, cause!)\n }\n if (reply[0] === 'foreign') {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n `the clear marker id '${markerId}' already exists`,\n )\n }\n return {\n cleared: reply[1] === '1',\n index: Number(reply[2]),\n clearIndex: Number(reply[3]),\n }\n })\n },\n\n async reclaim(sessionId) {\n return wrap(async () => {\n const [log, ...pairs] = (await evalScript(\n RECLAIM_BATCH_LUA,\n sessionId,\n [],\n [READ_PAGE_SIZE],\n )) as string[]\n if (log === undefined) return { deleted: 0, done: true }\n // Id keys are built here, like append's: they use JSON.stringify,\n // whose escaping cjson does not reproduce.\n const ids: string[] = []\n for (let i = 1; i < pairs.length; i += 2) ids.push(pairs[i]!)\n const [deleted, done] = (await evalScript(\n RECLAIM_LUA,\n sessionId,\n [log, ...ids.map((id) => idKey(id))],\n [sessionId, ids.length, ...pairs],\n )) as [number, number]\n return { deleted: Number(deleted), done: Number(done) === 1 }\n })\n },\n\n async readEvents(sessionId, afterIndex) {\n return wrap(async () => {\n const entries = (await call(\n 'XRANGE',\n storeKey(sessionId),\n `${afterIndex + 1}-0`,\n '+',\n 'COUNT',\n READ_PAGE_SIZE,\n )) as StreamEntry[]\n return entries.map((entry) => toEvent(sessionId, entry))\n })\n },\n async readFeedPage(sessionId, afterIndex) {\n return wrap(async () => {\n const range = [\n storeKey(sessionId),\n `${afterIndex + 1}-0`,\n '+',\n 'COUNT',\n FEED_PAGE_SIZE,\n ] as const\n if (callBytes) {\n const entries = (await callBytes(\n 'XRANGE',\n ...range,\n )) as ByteStreamEntry[]\n return entries.map((entry) =>\n bytesToSerializedEvent(sessionId, entry),\n )\n }\n const entries = (await call('XRANGE', ...range)) as StreamEntry[]\n return entries.map((entry) => toSerializedEvent(sessionId, entry))\n })\n },\n }\n}\n","import type { PresencePatch } from './contract.ts'\nimport { A2Error } from './errors.ts'\nimport { NOTIFY_TIMINGS, stateTailReaders } from './internal.ts'\nimport { defaultSleep, pollingStream, type Sleeper } from './store-polling.ts'\nimport { pollPresence } from './store-presence-polling.ts'\nimport { presenceRecovery } from './presence-recovery.ts'\nimport { createStreamActivity, streamActivities } from './stream-activity.ts'\nimport {\n bufferedEvents,\n isSerializedEvent,\n parseSerializedEvent,\n type HeldEvent,\n type SerializedEvent,\n} from './stream-buffer.ts'\nimport {\n createRedisStoreCore,\n FEED_PAGE_SIZE,\n type RedisCall,\n type RedisStreamRead,\n} from './store-redis-core.ts'\nimport type { A2Store, Clock, Event, IdSource } from './store.ts'\n\nexport type RedisConnection = {\n call(command: string, ...args: Array<string | number>): Promise<unknown>\n duplicate(): RedisConnection\n on(\n event: 'message',\n listener: (channel: string, message: string) => void,\n ): unknown\n disconnect(): void\n}\n\ntype PresenceWireMessage = {\n src: string\n participant: string\n values: Record<string, unknown>\n seen: number\n at: number\n}\n\ntype PresenceSubscription = {\n listeners: Set<(patch: PresencePatch) => void>\n poll?: ReturnType<typeof pollPresence> | undefined\n recovery?: ReturnType<typeof presenceRecovery> | undefined\n}\n\n/**\n * One live event feed's view of its channel: wake-up messages received\n * since it subscribed, whether it must catch up with XRANGE before trusting\n * them, and the resolver of its park, if parked.\n */\ntype Feed = {\n inbox: string[]\n bytes: number\n dirty: boolean\n wake: (() => void) | null\n stopRead: () => Promise<void>\n}\n\n/** A feed that falls this far behind drops its inbox and catches up. */\nconst INBOX_BYTES = 512 * 1024\n\ntype WakeEntry = [index: number, id: string, type: string, payload: unknown]\n\n/**\n * Parses a wake-up message: `{\"f\":frontier}` or a bare frontier number\n * announces events without carrying them; `{\"f\",\"t\",\"e\"}` carries them.\n * Returns null for anything else, which callers treat as a hint to catch up.\n */\nconst parseWake = (\n message: string,\n): { frontier: number; createdAt: number; entries: WakeEntry[] } | null => {\n let parsed: unknown\n try {\n parsed = JSON.parse(message)\n } catch {\n return null\n }\n if (typeof parsed === 'number') {\n return Number.isSafeInteger(parsed)\n ? { frontier: parsed, createdAt: 0, entries: [] }\n : null\n }\n if (typeof parsed !== 'object' || parsed === null) return null\n const { f, t, e } = parsed as { f?: unknown; t?: unknown; e?: unknown }\n if (typeof f !== 'number' || !Number.isSafeInteger(f)) return null\n if (e === undefined) return { frontier: f, createdAt: 0, entries: [] }\n if (!Array.isArray(e) || typeof t !== 'number' || !Number.isFinite(t))\n return null\n for (const entry of e) {\n if (\n !Array.isArray(entry) ||\n entry.length !== 4 ||\n !Number.isSafeInteger(entry[0]) ||\n typeof entry[1] !== 'string' ||\n typeof entry[2] !== 'string'\n )\n return null\n }\n return { frontier: f, createdAt: t, entries: e as WakeEntry[] }\n}\n\nexport function createRedisNotifyStore(options: {\n client: () => Promise<RedisConnection>\n callBytes?: RedisCall\n closeClient: () => Promise<void>\n clock: Clock\n ids: IdSource\n keyPrefix: string\n onReconnect?: (listener: (channel: string) => void) => void\n polling?: () => boolean\n readStream?: RedisStreamRead\n}): A2Store & { close(): Promise<void> } {\n const { client, clock, ids, keyPrefix: prefix } = options\n const notifyChannel = (sessionId: string): string =>\n `${prefix}:${sessionId}:notify`\n const presenceChannel = (ns: string): string => `${prefix}:${ns}:presence`\n\n const instanceId = crypto.randomUUID()\n const core = createRedisStoreCore({\n call: async (command, ...args) => {\n const c = await client()\n return c.call(command, ...args)\n },\n clock,\n ids,\n keyPrefix: prefix,\n presenceSource: instanceId,\n ...(options.callBytes ? { callBytes: options.callBytes } : {}),\n ...(options.readStream ? { readStream: options.readStream } : {}),\n })\n\n let severed = false\n let subscriber: Promise<RedisConnection> | null = null\n /** channel → live event feeds, registered before they subscribe. */\n const feeds = new Map<string, Set<Feed>>()\n /** channel → refcounted subscription held for each live iterator. */\n const subscriptions = new Map<\n string,\n { refs: number; ready: Promise<unknown> }\n >()\n const presenceSubscriptions = new Map<string, PresenceSubscription>()\n const pollingStreams = new Set<AsyncIterator<Event>>()\n\n options.onReconnect?.((channel) => {\n for (const feed of feeds.get(channel) ?? []) {\n feed.dirty = true\n feed.wake?.()\n }\n void presenceSubscriptions\n .get(channel)\n ?.recovery?.reconnect()\n .catch(() => {})\n })\n\n const deliverRemotePresence = ({\n subscription,\n message,\n }: {\n subscription: PresenceSubscription\n message: string\n }): void => {\n let parsed: PresenceWireMessage\n try {\n parsed = JSON.parse(message) as PresenceWireMessage\n } catch {\n return\n }\n if (parsed.src === instanceId) return\n const patch: PresencePatch = {\n participant: parsed.participant,\n values: parsed.values,\n seen: parsed.seen,\n at: new Date(parsed.at),\n }\n subscription.recovery?.update(patch)\n for (const listener of subscription.listeners) listener(patch)\n }\n\n const getSubscriber = (): Promise<RedisConnection> => {\n subscriber ??= (async () => {\n const conn = (await client()).duplicate()\n conn.on('message', (channel, message) => {\n for (const feed of feeds.get(channel) ?? []) {\n if (feed.bytes + message.length <= INBOX_BYTES) {\n feed.inbox.push(message)\n feed.bytes += message.length\n } else {\n feed.inbox.length = 0\n feed.bytes = 0\n feed.dirty = true\n }\n feed.wake?.()\n }\n const subscription = presenceSubscriptions.get(channel)\n if (subscription) deliverRemotePresence({ subscription, message })\n })\n return conn\n })()\n return subscriber\n }\n\n /**\n * The subscriber, gated on connection readiness — for channel\n * commands only (`close` uses the ungated promise, so it can never\n * hang on an unreachable server). SUBSCRIBE carries redis's\n * ok-loading flag, so ioredis writes it mid-handshake, ahead of the\n * ready check's INFO — which then finds the connection already in\n * subscriber mode and fails the check (a spurious error event plus\n * a reconnect). ECHO has no such flag: it resolves only once the\n * connection is ready, so every channel command chained behind it\n * lands after the check.\n */\n let subscriberGate: Promise<RedisConnection> | null = null\n const gatedSubscriber = (): Promise<RedisConnection> => {\n subscriberGate ??= getSubscriber().then(async (conn) => {\n await conn.call('echo', 'ready')\n return conn\n })\n return subscriberGate\n }\n\n const acquireChannel = async (channel: string): Promise<void> => {\n const existing = subscriptions.get(channel)\n if (existing) {\n existing.refs += 1\n try {\n await existing.ready\n } catch (err) {\n existing.refs -= 1\n throw err\n }\n return\n }\n const lease: { refs: number; ready: Promise<void> } = {\n refs: 1,\n // Lowercase on purpose: ioredis keys its subscriber-mode\n // bookkeeping (and reconnect resubscription) on the exact\n // command name.\n ready: gatedSubscriber().then(async (conn) => {\n await conn.call('subscribe', channel)\n if (subscriptions.get(channel) !== lease || severed) return\n const presence = presenceSubscriptions.get(channel)\n if (options.polling?.() && presence) {\n presence.recovery = undefined\n const ns = channel.slice(prefix.length + 1, -':presence'.length)\n presence.poll = pollPresence({\n read: () => core.presence.read(ns),\n deliver: (patch) => {\n for (const listener of presence.listeners) listener(patch)\n },\n })\n await presence.poll.ready\n }\n return undefined\n }),\n }\n subscriptions.set(channel, lease)\n try {\n await lease.ready\n } catch (err) {\n if (subscriptions.get(channel) === lease) {\n subscriptions.delete(channel)\n const presence = presenceSubscriptions.get(channel)\n presence?.poll?.stop()\n if (presence) presence.poll = undefined\n }\n throw err\n }\n }\n\n const releaseChannel = (channel: string): void => {\n const lease = subscriptions.get(channel)\n if (!lease) return\n lease.refs -= 1\n if (lease.refs > 0) return\n subscriptions.delete(channel)\n void gatedSubscriber()\n .then((conn) => conn.call('unsubscribe', channel))\n .catch(() => {})\n }\n\n const store: A2Store & { close(): Promise<void> } = {\n append: core.append,\n cursors: core.cursors,\n read: core.read,\n readHistory: core.readHistory!,\n claimAvailable: core.claimAvailable,\n renewClaims: core.renewClaims,\n completeAttempt: core.completeAttempt,\n failAttempt: core.failAttempt,\n readState: core.readState,\n putSnapshots: core.putSnapshots,\n clear: core.clear!,\n reclaim: core.reclaim!,\n deadLetters: core.deadLetters!,\n redrive: core.redrive!,\n wakeLog: core.wakeLog,\n\n presence: {\n async set(ns, participant, values, meta) {\n const patch = await core.presence.set(ns, participant, values, meta)\n if (!patch) return\n const channel = presenceChannel(ns)\n const subscription = presenceSubscriptions.get(channel)\n if (subscription) {\n subscription.poll?.update(patch)\n subscription.recovery?.update(patch)\n for (const listener of subscription.listeners) listener(patch)\n }\n },\n\n async read(ns) {\n const channel = presenceChannel(ns)\n await subscriptions.get(channel)?.ready\n return (\n presenceSubscriptions.get(channel)?.recovery?.read() ??\n core.presence.read(ns)\n )\n },\n\n subscribe(ns, onPatch) {\n const channel = presenceChannel(ns)\n let subscription = presenceSubscriptions.get(channel)\n if (!subscription) {\n subscription = { listeners: new Set() }\n presenceSubscriptions.set(channel, subscription)\n if (options.onReconnect && !options.polling?.()) {\n const current = subscription\n subscription.recovery = presenceRecovery({\n read: () => core.presence.read(ns),\n deliver: (patch) => {\n for (const listener of current.listeners) listener(patch)\n },\n isActive: () =>\n !severed && presenceSubscriptions.get(channel) === current,\n })\n }\n }\n subscription.listeners.add(onPatch)\n // Fire-and-forget on the shared refcounted subscription: it\n // only carries remote patches, so settling late (or failing)\n // degrades cross-instance latency, never same-process\n // delivery.\n void acquireChannel(channel).catch(() => {})\n let stopped = false\n return () => {\n if (stopped) return\n stopped = true\n subscription.listeners.delete(onPatch)\n if (subscription.listeners.size === 0) {\n subscription.poll?.stop()\n presenceSubscriptions.delete(channel)\n }\n releaseChannel(channel)\n }\n },\n },\n\n stream(sessionId, opts) {\n const startAfter = opts?.startAfter ?? 0\n const channel = notifyChannel(sessionId)\n\n return {\n [Symbol.asyncIterator](): AsyncIterator<Event> {\n let last = startAfter\n let buffer: HeldEvent[] = []\n let closed = false\n let subscribed = false\n let pending: Sleeper | null = null\n let poller: AsyncIterator<Event> | undefined\n let stopActivity: (() => void) | undefined\n let catchup:\n | {\n controller: AbortController\n iterator: AsyncGenerator<SerializedEvent[], void>\n delivered: boolean\n }\n | undefined\n let stoppingRead: Promise<void> | undefined\n const stopRead = (): Promise<void> => {\n const reading = catchup\n catchup = undefined\n if (reading) {\n reading.controller.abort()\n stoppingRead = reading.iterator\n .return(undefined)\n .then(() => undefined)\n .catch(() => {})\n }\n return stoppingRead ?? Promise.resolve()\n }\n const nextRead = async (\n after: number,\n ): Promise<IteratorResult<SerializedEvent[], void>> => {\n if (!catchup) {\n const controller = new AbortController()\n catchup = {\n controller,\n delivered: false,\n iterator: core.streamEvents!({\n sessionId,\n afterIndex: after,\n signal: controller.signal,\n })[Symbol.asyncIterator](),\n }\n }\n const reading = catchup\n const result = await reading.iterator.next()\n if (result.done && catchup === reading) {\n catchup = undefined\n if (reading.delivered) feed.dirty = true\n } else if (!result.done) reading.delivered = true\n return result\n }\n const feed: Feed = {\n inbox: [],\n bytes: 0,\n dirty: true,\n wake: null,\n stopRead,\n }\n const activity = createStreamActivity(() => {\n if (poller) streamActivities.get(poller)?.activate()\n })\n\n const release = (): void => {\n void stopRead()\n stopActivity?.()\n if (poller) {\n void poller.return?.()\n pollingStreams.delete(poller)\n }\n if (!subscribed) return\n subscribed = false\n const set = feeds.get(channel)\n set?.delete(feed)\n if (set?.size === 0) feeds.delete(channel)\n releaseChannel(channel)\n }\n\n /**\n * Moves carried events that continue this feed's sequence into\n * the buffer. A gap, a message without events, or an unreadable\n * message marks the feed dirty so it catches up with XRANGE.\n */\n const applyInbox = (): void => {\n feed.bytes = 0\n for (const message of feed.inbox.splice(0)) {\n const wake = parseWake(message)\n if (!wake) {\n feed.dirty = true\n continue\n }\n for (const [index, id, type, payload] of wake.entries) {\n const expected = (buffer.at(-1)?.index ?? last) + 1\n if (index < expected) continue\n if (index > expected) {\n feed.dirty = true\n break\n }\n buffer.push({\n id,\n type,\n payload,\n index,\n sessionId,\n createdAt: new Date(wake.createdAt),\n })\n }\n if (wake.frontier > (buffer.at(-1)?.index ?? last))\n feed.dirty = true\n }\n }\n\n const iterator: AsyncIterator<Event> = {\n async next(): Promise<IteratorResult<Event>> {\n for (;;) {\n if (closed) {\n release()\n return { value: undefined, done: true }\n }\n const row = buffer.shift()\n if (row) {\n let event: Event\n try {\n event = isSerializedEvent(row)\n ? parseSerializedEvent(row)\n : row\n } catch (cause) {\n // A retry re-reads from the last delivered event.\n buffer = []\n feed.dirty = true\n release()\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n 'redis stream read failed',\n { cause },\n )\n }\n last = row.index\n return { value: event, done: false }\n }\n if (severed) {\n release()\n throw new A2Error('STORE_UNAVAILABLE', 'the store was closed')\n }\n try {\n if (!subscribed) {\n // Register and subscribe before the first read:\n // anything appended after it arrives in the inbox,\n // so nothing slips between catch-up and park.\n subscribed = true\n let set = feeds.get(channel)\n if (!set) {\n set = new Set()\n feeds.set(channel, set)\n }\n set.add(feed)\n // oxlint-disable-next-line no-await-in-loop -- one-time setup\n await acquireChannel(channel)\n if (closed || severed) continue\n }\n if (options.polling?.()) {\n if (!poller) {\n poller = pollingStream(\n (after) => core.readEvents(sessionId, after),\n { startAfter: last },\n )[Symbol.asyncIterator]()\n pollingStreams.add(poller)\n stopActivity = streamActivities\n .get(poller)\n ?.subscribe(activity.setIdle)\n }\n // oxlint-disable-next-line no-await-in-loop -- delegate the fallback to the shared poller\n const result = await poller.next()\n if (!result.done)\n presenceSubscriptions\n .get(presenceChannel(sessionId))\n ?.poll?.wake()\n if (severed)\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n 'the store was closed',\n )\n return result\n }\n if (catchup) {\n // oxlint-disable-next-line no-await-in-loop -- decode the next catch-up batch on demand\n const result = await nextRead(last)\n if (closed || severed) continue\n if (!result.done) buffer = result.value\n continue\n }\n applyInbox()\n if (buffer.length > 0) continue\n if (feed.dirty) {\n feed.dirty = false\n if (core.streamEvents) {\n // oxlint-disable-next-line no-await-in-loop -- begin an incremental catch-up\n const result = await nextRead(last)\n if (closed || severed) continue\n if (!result.done) buffer = result.value\n continue\n }\n // oxlint-disable-next-line no-await-in-loop -- live-feed loop\n buffer = await core.readFeedPage(sessionId, last)\n if (buffer.length >= FEED_PAGE_SIZE) feed.dirty = true\n continue\n }\n // Caught up: park until a message, a reconnect, or the\n // safety read that heals a lost message.\n const woken = new Promise<'wake'>((resolve) => {\n feed.wake = () => resolve('wake')\n })\n pending = defaultSleep(NOTIFY_TIMINGS.safetyReadMs)\n // oxlint-disable-next-line no-await-in-loop -- live-feed loop\n const outcome = await Promise.race([\n woken,\n pending.promise.then(() => 'timer' as const),\n ])\n pending.cancel()\n pending = null\n if (outcome === 'timer') feed.dirty = true\n } catch (err) {\n release()\n if (closed) return { value: undefined, done: true }\n if (err instanceof A2Error) throw err\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n 'redis store operation failed',\n { cause: err },\n )\n } finally {\n feed.wake = null\n }\n }\n },\n async return(): Promise<IteratorResult<Event>> {\n closed = true\n pending?.cancel()\n feed.wake?.() // wakes an in-flight next() immediately\n release()\n await stopRead()\n return { value: undefined, done: true }\n },\n }\n streamActivities.set(iterator, activity)\n bufferedEvents.set(iterator, () => {\n if (closed) return undefined\n const row = buffer.shift()\n if (row) last = row.index\n return row\n })\n return iterator\n },\n }\n },\n\n async close() {\n severed = true\n for (const subscription of presenceSubscriptions.values())\n subscription.poll?.stop()\n presenceSubscriptions.clear()\n for (const poller of pollingStreams) void poller.return?.()\n pollingStreams.clear()\n const reads: Promise<void>[] = []\n for (const set of feeds.values()) {\n for (const feed of set) {\n feed.wake?.()\n reads.push(feed.stopRead())\n }\n }\n await Promise.allSettled(reads)\n if (subscriber) {\n const sub = await subscriber.catch(() => null)\n sub?.disconnect()\n }\n await options.closeClient()\n },\n }\n stateTailReaders.set(store, core.readTail)\n return store\n}\n"],"mappings":"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"}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
//#region src/stream-buffer.ts
|
|
2
|
-
const isSerializedEvent = (event) => "payloadJson" in event;
|
|
3
|
-
const parseSerializedEvent = (event) => ({
|
|
4
|
-
id: event.id,
|
|
5
|
-
type: event.type,
|
|
6
|
-
payload: JSON.parse(event.payloadJson),
|
|
7
|
-
index: event.index,
|
|
8
|
-
sessionId: event.sessionId,
|
|
9
|
-
createdAt: new Date(event.createdAtMs)
|
|
10
|
-
});
|
|
11
|
-
/**
|
|
12
|
-
* The next event a stream iterator already holds, removed without
|
|
13
|
-
* awaiting its `next()`, which then continues after it; `undefined` when
|
|
14
|
-
* it holds none. Call only while no `next()` is pending. The SSE encoder
|
|
15
|
-
* writes these with the event `next()` just returned, so a buffered
|
|
16
|
-
* backlog leaves in few writes.
|
|
17
|
-
*/
|
|
18
|
-
const bufferedEvents = /* @__PURE__ */ new WeakMap();
|
|
19
|
-
const takeBuffered = (iterator) => bufferedEvents.get(iterator)?.();
|
|
20
|
-
//#endregion
|
|
21
|
-
export { takeBuffered as i, isSerializedEvent as n, parseSerializedEvent as r, bufferedEvents as t };
|
|
22
|
-
|
|
23
|
-
//# sourceMappingURL=stream-buffer-Cx7AAPGq.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"stream-buffer-Cx7AAPGq.js","names":[],"sources":["../src/stream-buffer.ts"],"sourcesContent":["import type { Event } from './store.ts'\n\n/**\n * An event as a store returned it, its payload still the JSON text A2's\n * store wrote with `JSON.stringify`. For payloads built from ordinary objects,\n * parsing and re-serializing that text yields the same text, so the SSE lane\n * can write it into a frame unchanged.\n */\nexport type SerializedEvent = {\n id: string\n type: string\n payloadJson: string\n index: number\n sessionId: string\n createdAtMs: number\n}\n\nexport type HeldEvent = Event | SerializedEvent\n\nexport const isSerializedEvent = (event: object): event is SerializedEvent =>\n 'payloadJson' in event\n\nexport const parseSerializedEvent = (event: SerializedEvent): Event => ({\n id: event.id,\n type: event.type,\n payload: JSON.parse(event.payloadJson),\n index: event.index,\n sessionId: event.sessionId,\n createdAt: new Date(event.createdAtMs),\n})\n\n/**\n * The next event a stream iterator already holds, removed without\n * awaiting its `next()`, which then continues after it; `undefined` when\n * it holds none. Call only while no `next()` is pending. The SSE encoder\n * writes these with the event `next()` just returned, so a buffered\n * backlog leaves in few writes.\n */\nexport const bufferedEvents: WeakMap<object, () => HeldEvent | undefined> =\n new WeakMap()\n\nexport const takeBuffered = (iterator: object): HeldEvent | undefined =>\n bufferedEvents.get(iterator)?.()\n"],"mappings":";AAmBA,MAAa,qBAAqB,UAChC,iBAAiB;AAEnB,MAAa,wBAAwB,WAAmC;CACtE,IAAI,MAAM;CACV,MAAM,MAAM;CACZ,SAAS,KAAK,MAAM,MAAM,WAAW;CACrC,OAAO,MAAM;CACb,WAAW,MAAM;CACjB,WAAW,IAAI,KAAK,MAAM,WAAW;AACvC;;;;;;;;AASA,MAAa,iCACX,IAAI,QAAQ;AAEd,MAAa,gBAAgB,aAC3B,eAAe,IAAI,QAAQ,CAAC,GAAG"}
|