@vercel/board 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -27
- package/dist/{a2-C41hD8SF.mjs → a2-ChRLRupa.mjs} +969 -25
- package/dist/access.d.mts +1 -1
- package/dist/access.mjs +3 -3
- package/dist/{actors-DjMeZpnU.mjs → actors-BOamkEuz.mjs} +3 -73
- package/dist/attempts-CLG840Oj.mjs +20 -0
- package/dist/{auth-COzc09sn.mjs → auth-Drg9FiC2.mjs} +225 -2
- package/dist/backend-B-SJ7p6p.d.mts +680 -0
- package/dist/backend-a2/contract.d.mts +873 -2
- package/dist/backend-a2/contract.mjs +2 -2
- package/dist/backend-a2/scheduler-local.d.mts +1 -1
- package/dist/backend-a2/scheduler-local.mjs +1 -1
- package/dist/backend-a2/scheduler-qstash.d.mts +78 -1
- package/dist/backend-a2/scheduler-qstash.mjs +521 -1
- package/dist/backend-a2/scheduler-vercel.d.mts +37 -1
- package/dist/backend-a2/scheduler-vercel.mjs +203 -1
- package/dist/backend-a2/store-memory.d.mts +2 -2
- package/dist/backend-a2/store-memory.mjs +3 -3
- package/dist/backend-a2/store-postgres.d.mts +1 -1
- package/dist/backend-a2/store-postgres.mjs +2850 -2
- package/dist/backend-a2/store-redis-http.d.mts +1 -1
- package/dist/backend-a2/store-redis-http.mjs +490 -2
- package/dist/backend-a2/store-redis.d.mts +10 -2
- package/dist/backend-a2/store-redis.mjs +51 -2
- package/dist/backend-a2/store-sqlite.d.mts +1 -1
- package/dist/backend-a2/store-sqlite.mjs +3 -3
- package/dist/backend-a2.d.mts +2706 -3
- package/dist/backend-a2.mjs +1 -1
- package/dist/{backend-http-Bid7RIpC.d.mts → backend-http-7cNb8nJI.d.mts} +10 -3
- package/dist/{backend-http-0roEmjl0.mjs → backend-http-CMlL3yq3.mjs} +2 -2
- package/dist/{backend-http-BVRno-ca.mjs → backend-http-Ddzd5kTS.mjs} +32 -3
- package/dist/backend-http.d.mts +1 -1
- package/dist/backend-http.mjs +1 -1
- package/dist/backend-http.node.d.mts +1 -1
- package/dist/backend-http.node.mjs +1 -1
- package/dist/{chat-provider-XCnQlxq6.d.mts → chat-provider-DNJQyajS.d.mts} +16 -4
- package/dist/{chat-provider-jFSeyNmB.mjs → chat-provider-YHy-WbrB.mjs} +15 -9
- package/dist/cli.mjs +30 -10
- package/dist/{client-DGDPaN7U.mjs → client-BA-J7usH.mjs} +10 -2
- package/dist/{conformance-DRR23YNy.mjs → conformance-CT5EeHeC.mjs} +5 -2
- package/dist/conformance.d.mts +1 -1
- package/dist/conformance.mjs +1 -1
- package/dist/{engine-Bir5s8ts.mjs → engine-D0oBUBEK.mjs} +249 -163
- package/dist/events-CewD4kw7.mjs +75 -0
- package/dist/{exports-Dvc1o52I.mjs → exports-CgjfMrQ-.mjs} +35 -32
- package/dist/exports-ZsgjzEnM.d.mts +4 -0
- package/dist/external-BV_cUncs.mjs +151 -0
- package/dist/folds-CRYY14kc.d.mts +534 -0
- package/dist/{reducers-mUPFuFJV.mjs → folds-Dyjput8M.mjs} +6 -73
- package/dist/ids-C6AYo1bu.mjs +31 -0
- package/dist/index.d.mts +4 -4
- package/dist/{index.default-Bkg77Pxn.mjs → index.default-C5Cpz3sa.mjs} +1 -1
- package/dist/index.mjs +5 -5
- package/dist/index.node.d.mts +4 -4
- package/dist/index.node.mjs +3 -3
- package/dist/inspect/app.js +31 -31
- package/dist/inspect/engine.js +5 -5
- package/dist/internal-CvsrprnU-CRY89I-b.mjs +203 -0
- package/dist/{local-Cr_dSjvs.mjs → local-DhkrAqPP.mjs} +1 -1
- package/dist/mentions-CB6ShY4R.mjs +72 -0
- package/dist/{targets-yM5JBUgr.mjs → middleware-CqaCwucP.mjs} +1 -33
- package/dist/platform-B4TnJtWu-CwcTyKg2.mjs +32 -0
- package/dist/{project-CFiArraX.mjs → project-DMDST9kl.mjs} +1 -1
- package/dist/{provider-BGjz-mDa.d.mts → provider-B0b9Qxw4.d.mts} +170 -40
- package/dist/provider-B3DpYSDq.mjs +220 -0
- package/dist/redis-tables-DN9PfIDi.mjs +3897 -0
- package/dist/reducer-CYPHsSmd-BQIfozGi.d.mts +170 -0
- package/dist/reducer-CjmvZSAb-D_OSISmj.mjs +74 -0
- package/dist/{contract-CuJmlrhw.mjs → reducers-BFmIXilu.mjs} +260 -287
- package/dist/retryable-lazy-DZWmHpii-Bye-W8q7.mjs +19 -0
- package/dist/scheduler-task-X1SToDjP-BVoWuInI.mjs +69 -0
- package/dist/{serve-BsOOc-6_.mjs → serve-BGYw0uNL.mjs} +5 -5
- package/dist/{server-jwPY7_wi.mjs → server-DPxBSogx.mjs} +4 -4
- package/dist/server-DtFrWxi3-Boa6gdWO.mjs +3602 -0
- package/dist/server-qgez0Xny-D5mLfqFQ.d.mts +385 -0
- package/dist/{serving-BNbzc96L.mjs → serving-CNkgUPW1.mjs} +1 -1
- package/dist/session-clear-wHd5GCpZ-BnKSxReK.mjs +51 -0
- package/dist/{sql-DbiEnqM0.mjs → sql-U9WTxrwJ.mjs} +2 -2
- package/dist/{sqlite-db-Cbj03vmp.mjs → sqlite-db-Ds4majtT.mjs} +1 -1
- package/dist/store-D0MhBvg6-4Z2L0Kqd.d.mts +530 -0
- package/dist/store-N8PXxDAS-BWBgzBHp.mjs +5 -0
- package/dist/store-codec-DwrWlgQx-CYxGW0aD.mjs +61 -0
- package/dist/store-memory-Bas9Uq5C.mjs +872 -0
- package/dist/store-memory-ZLiZbH3R.mjs +2 -0
- package/dist/store-polling-BZt2Rf3D-BiEM6_HB.mjs +148 -0
- package/dist/store-presence-polling-JuAy2Ck2-CYpuK75X.mjs +109 -0
- package/dist/store-sqlite-BmwL3pUR.mjs +2 -0
- package/dist/store-sqlite-CxxPCcCT.mjs +1053 -0
- package/dist/stream-buffer-DrTyiIuo-BKy-vI14.mjs +53 -0
- package/dist/sync-github.d.mts +24 -17
- package/dist/sync-github.mjs +89 -22
- package/dist/sync-kapso.d.mts +15 -13
- package/dist/sync-kapso.mjs +17 -14
- package/dist/sync-linear.d.mts +219 -0
- package/dist/sync-linear.mjs +311 -0
- package/dist/sync-slack.d.mts +12 -17
- package/dist/sync-slack.mjs +54 -42
- package/dist/sync.chat.d.mts +1 -1
- package/dist/sync.chat.mjs +1 -1
- package/dist/sync.d.mts +3 -3
- package/dist/sync.mjs +5 -4
- package/dist/targets-C-wdR5wP.mjs +33 -0
- package/dist/{tokens-DKLn5P9B.mjs → tokens-DhuzKzAl.mjs} +1 -1
- package/dist/{types-BHCjTZdB.d.mts → types-8yq2VnKI.d.mts} +2 -2
- package/dist/{users-Dls7_ANV.mjs → users-yryHKFMo.mjs} +2 -2
- package/dist/{watch-spec-W0c7ypHJ.mjs → watchers-hash-RLtnSF-t.mjs} +233 -28
- package/docs/01-board/01-mounting.md +1 -1
- package/docs/02-sync/01-overview.md +72 -48
- package/docs/02-sync/02-functions.md +398 -0
- package/docs/02-sync/03-slack.md +30 -25
- package/docs/02-sync/04-github.md +62 -26
- package/docs/02-sync/05-whatsapp.md +23 -19
- package/docs/02-sync/06-providers.md +60 -27
- package/docs/02-sync/07-linear.md +129 -0
- package/docs/03-guides/04-triage.md +1 -1
- package/docs/04-reference/01-cli.md +26 -6
- package/docs/04-reference/02-board.md +18 -5
- package/docs/04-reference/03-http.md +5 -2
- package/docs/04-reference/05-conformance.md +1 -1
- package/docs/index.md +5 -10
- package/package.json +31 -6
- package/dist/a2-D24cvwpN.d.mts +0 -3359
- package/dist/access-CJU2-vaa.mjs +0 -226
- package/dist/attempts-Bxp9KgPf.mjs +0 -919
- package/dist/exports-DLiE7fLY.d.mts +0 -4
- package/dist/provider-CuewcDNZ.mjs +0 -205
- package/dist/redis-tables-B35vv4CZ.mjs +0 -842
- package/dist/reducers-B_vUGQRp.d.mts +0 -1172
- package/dist/watchers-hash-CLjsa6od.mjs +0 -225
- package/docs/02-sync/02-callbacks.md +0 -265
|
@@ -0,0 +1,872 @@
|
|
|
1
|
+
import { n as CLEAR_EVENT_TYPE, o as sessionClearedError, s as supersededAttemptError, t as A2Error } from "./session-clear-wHd5GCpZ-BnKSxReK.mjs";
|
|
2
|
+
import { m as nullProtoRecord } from "./internal-CvsrprnU-CRY89I-b.mjs";
|
|
3
|
+
import { a as idleLogCursor, i as idempotentReplay } from "./store-codec-DwrWlgQx-CYxGW0aD.mjs";
|
|
4
|
+
import { n as SYSTEM_CLOCK, t as RANDOM_IDS } from "./store-N8PXxDAS-BWBgzBHp.mjs";
|
|
5
|
+
//#region ../../node_modules/.pnpm/experimental-a2@0.22.0_@upstash+qstash@2.12.0_@vercel+queue@0.7.0_ai@7.0.130_zod@4.6.5__52060f00b21aecfab049c27fa3abd2f9/node_modules/experimental-a2/dist/store-memory.js
|
|
6
|
+
const RECLAIM_BATCH = 256;
|
|
7
|
+
const headOf = (log) => log ? log.offset + log.rows.length : 0;
|
|
8
|
+
const freshDispatch = () => ({
|
|
9
|
+
firstClaimedAt: null,
|
|
10
|
+
lastClaimedAt: null,
|
|
11
|
+
attemptCount: 0,
|
|
12
|
+
claimHolder: null,
|
|
13
|
+
claimExpiresAt: null,
|
|
14
|
+
failureCount: 0,
|
|
15
|
+
lastFailedAt: null,
|
|
16
|
+
lastFailedAttempt: null,
|
|
17
|
+
lastError: null,
|
|
18
|
+
failedAt: null
|
|
19
|
+
});
|
|
20
|
+
const date = (value) => value ? new Date(value) : null;
|
|
21
|
+
const toStored = (row) => ({
|
|
22
|
+
id: row.id,
|
|
23
|
+
type: row.type,
|
|
24
|
+
payload: structuredClone(row.payload),
|
|
25
|
+
index: row.index,
|
|
26
|
+
sessionId: row.sessionId,
|
|
27
|
+
createdAt: new Date(row.createdAt),
|
|
28
|
+
cause: row.cause ? { ...row.cause } : null
|
|
29
|
+
});
|
|
30
|
+
const toEvent = (row) => ({
|
|
31
|
+
id: row.id,
|
|
32
|
+
type: row.type,
|
|
33
|
+
payload: structuredClone(row.payload),
|
|
34
|
+
index: row.index,
|
|
35
|
+
sessionId: row.sessionId,
|
|
36
|
+
createdAt: new Date(row.createdAt)
|
|
37
|
+
});
|
|
38
|
+
const toHead = (cursor, row) => ({
|
|
39
|
+
...toStored(row),
|
|
40
|
+
consumer: cursor.consumer,
|
|
41
|
+
lane: cursor.lane,
|
|
42
|
+
firstClaimedAt: date(cursor.dispatch.firstClaimedAt),
|
|
43
|
+
lastClaimedAt: date(cursor.dispatch.lastClaimedAt),
|
|
44
|
+
attemptCount: cursor.dispatch.attemptCount,
|
|
45
|
+
claimHolder: cursor.dispatch.claimHolder,
|
|
46
|
+
claimExpiresAt: date(cursor.dispatch.claimExpiresAt),
|
|
47
|
+
failureCount: cursor.dispatch.failureCount,
|
|
48
|
+
lastFailedAt: date(cursor.dispatch.lastFailedAt),
|
|
49
|
+
lastFailedAttempt: cursor.dispatch.lastFailedAttempt,
|
|
50
|
+
lastError: cursor.dispatch.lastError,
|
|
51
|
+
failedAt: date(cursor.dispatch.failedAt)
|
|
52
|
+
});
|
|
53
|
+
const toCursor = (cursor) => ({
|
|
54
|
+
consumer: cursor.consumer,
|
|
55
|
+
lane: cursor.lane,
|
|
56
|
+
position: cursor.position,
|
|
57
|
+
head: cursor.head,
|
|
58
|
+
firstClaimedAt: date(cursor.dispatch.firstClaimedAt),
|
|
59
|
+
lastClaimedAt: date(cursor.dispatch.lastClaimedAt),
|
|
60
|
+
attemptCount: cursor.dispatch.attemptCount,
|
|
61
|
+
claimHolder: cursor.dispatch.claimHolder,
|
|
62
|
+
claimExpiresAt: date(cursor.dispatch.claimExpiresAt),
|
|
63
|
+
failureCount: cursor.dispatch.failureCount,
|
|
64
|
+
lastFailedAt: date(cursor.dispatch.lastFailedAt),
|
|
65
|
+
lastFailedAttempt: cursor.dispatch.lastFailedAttempt,
|
|
66
|
+
lastError: cursor.dispatch.lastError,
|
|
67
|
+
failedAt: date(cursor.dispatch.failedAt)
|
|
68
|
+
});
|
|
69
|
+
const compareText = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
70
|
+
/** Consumer, then lane, with concurrent cursors last by head. */
|
|
71
|
+
const byCursor = (a, b) => compareText(a.consumer, b.consumer) || (a.lane === null ? b.lane === null ? (a.head ?? 0) - (b.head ?? 0) : 1 : b.lane === null ? -1 : compareText(a.lane, b.lane));
|
|
72
|
+
const cursorKey = (consumer, lane, index) => lane === null ? `${consumer}\u0000#${index}` : `${consumer}\u0000L${lane}`;
|
|
73
|
+
const takes = (types, type) => type !== "a2.cleared" && (types === null || types.includes(type));
|
|
74
|
+
const byHead = (a, b) => a.index - b.index || compareText(a.consumer, b.consumer);
|
|
75
|
+
const snapshotKey = (sessionId, reducerName) => `${sessionId}\u0000${reducerName}`;
|
|
76
|
+
const headPinKey = (sessionId, ref) => `${sessionId}\u0000${cursorKey(ref.consumer, ref.lane, ref.index)}\u0000${ref.index}`;
|
|
77
|
+
/** Whether a cursor belongs to a consumer the caller serves; omitted, every one. */
|
|
78
|
+
const servedBy = (consumers) => {
|
|
79
|
+
if (!consumers) return () => true;
|
|
80
|
+
const names = new Set(consumers.map((consumer) => consumer.name));
|
|
81
|
+
return (cursor) => names.has(cursor.consumer);
|
|
82
|
+
};
|
|
83
|
+
/** A cursor's current head. */
|
|
84
|
+
const refOf = (cursor) => ({
|
|
85
|
+
consumer: cursor.consumer,
|
|
86
|
+
lane: cursor.lane,
|
|
87
|
+
index: cursor.head
|
|
88
|
+
});
|
|
89
|
+
function memory(options = {}) {
|
|
90
|
+
const clock = options.clock ?? SYSTEM_CLOCK;
|
|
91
|
+
const generateId = options.ids ?? RANDOM_IDS;
|
|
92
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
93
|
+
/** Session id → cursor key → cursor. */
|
|
94
|
+
const cursors = /* @__PURE__ */ new Map();
|
|
95
|
+
/** Keyed by `${sessionId}\u0000${reducerName}` — a pure cache. */
|
|
96
|
+
const snapshots = /* @__PURE__ */ new Map();
|
|
97
|
+
const historicalSnapshots = /* @__PURE__ */ new Map();
|
|
98
|
+
const snapshotPins = /* @__PURE__ */ new Map();
|
|
99
|
+
const snapshotPinCounts = /* @__PURE__ */ new Map();
|
|
100
|
+
const streamSubscribers = /* @__PURE__ */ new Map();
|
|
101
|
+
/** ns → participant → field → latest surviving write. */
|
|
102
|
+
const presenceRows = /* @__PURE__ */ new Map();
|
|
103
|
+
const presenceSubscribers = /* @__PURE__ */ new Map();
|
|
104
|
+
const notifyStreams = (sessionId, rows) => {
|
|
105
|
+
const subs = streamSubscribers.get(sessionId);
|
|
106
|
+
if (!subs) return;
|
|
107
|
+
for (const listener of subs) for (const row of rows) listener(row);
|
|
108
|
+
};
|
|
109
|
+
const logOf = (sessionId) => {
|
|
110
|
+
let log = sessions.get(sessionId);
|
|
111
|
+
if (!log) {
|
|
112
|
+
log = {
|
|
113
|
+
rows: [],
|
|
114
|
+
offset: 0,
|
|
115
|
+
clearIndex: 0,
|
|
116
|
+
byEventId: /* @__PURE__ */ new Map()
|
|
117
|
+
};
|
|
118
|
+
sessions.set(sessionId, log);
|
|
119
|
+
}
|
|
120
|
+
return log;
|
|
121
|
+
};
|
|
122
|
+
const rowAt = (sessionId, index) => {
|
|
123
|
+
const log = sessions.get(sessionId);
|
|
124
|
+
return log && index > log.offset ? log.rows[index - log.offset - 1] : void 0;
|
|
125
|
+
};
|
|
126
|
+
const clearIndexOf = (sessionId) => sessions.get(sessionId)?.clearIndex ?? 0;
|
|
127
|
+
/** The session's row owning `id`, ignoring rows below its clear. */
|
|
128
|
+
const ownerOf = (sessionId, id) => {
|
|
129
|
+
const log = sessions.get(sessionId);
|
|
130
|
+
const row = log?.byEventId.get(id);
|
|
131
|
+
return log && row && row.index >= log.clearIndex ? row : void 0;
|
|
132
|
+
};
|
|
133
|
+
const cursorsOf = (sessionId) => {
|
|
134
|
+
let map = cursors.get(sessionId);
|
|
135
|
+
if (!map) {
|
|
136
|
+
map = /* @__PURE__ */ new Map();
|
|
137
|
+
cursors.set(sessionId, map);
|
|
138
|
+
}
|
|
139
|
+
return map;
|
|
140
|
+
};
|
|
141
|
+
const cursorAt = (sessionId, ref) => cursors.get(sessionId)?.get(cursorKey(ref.consumer, ref.lane, ref.index));
|
|
142
|
+
const checkpointKey = (sessionId, reducerName, index) => `${snapshotKey(sessionId, reducerName)}\u0000${index}`;
|
|
143
|
+
const releaseSnapshotPins = (sessionId, ref) => {
|
|
144
|
+
const pinKey = headPinKey(sessionId, ref);
|
|
145
|
+
const checkpoints = snapshotPins.get(pinKey);
|
|
146
|
+
if (!checkpoints) return;
|
|
147
|
+
for (const key of checkpoints) {
|
|
148
|
+
const next = (snapshotPinCounts.get(key) ?? 1) - 1;
|
|
149
|
+
if (next === 0) {
|
|
150
|
+
snapshotPinCounts.delete(key);
|
|
151
|
+
historicalSnapshots.delete(key);
|
|
152
|
+
} else snapshotPinCounts.set(key, next);
|
|
153
|
+
}
|
|
154
|
+
snapshotPins.delete(pinKey);
|
|
155
|
+
};
|
|
156
|
+
/** The next event after `after` that a log cursor takes, or null. */
|
|
157
|
+
const nextLogHead = (sessionId, after, types) => {
|
|
158
|
+
const log = sessions.get(sessionId);
|
|
159
|
+
if (!log) return null;
|
|
160
|
+
for (let offset = Math.max(0, after - log.offset); offset < log.rows.length; offset += 1) {
|
|
161
|
+
const row = log.rows[offset];
|
|
162
|
+
if (takes(types, row.type)) return row.index;
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
};
|
|
166
|
+
/** Moves a cursor past its head: the next head starts with fresh bookkeeping. */
|
|
167
|
+
const advance = (sessionId, cursor) => {
|
|
168
|
+
const done = cursor.head;
|
|
169
|
+
releaseSnapshotPins(sessionId, refOf(cursor));
|
|
170
|
+
cursor.position = done;
|
|
171
|
+
cursor.dispatch = freshDispatch();
|
|
172
|
+
if (cursor.kind === "event") cursor.head = null;
|
|
173
|
+
else if (cursor.kind === "lane") {
|
|
174
|
+
cursor.queued += 1;
|
|
175
|
+
cursor.head = cursor.queue[cursor.queued] ?? null;
|
|
176
|
+
} else cursor.head = nextLogHead(sessionId, done, cursor.types);
|
|
177
|
+
};
|
|
178
|
+
const hasPending = (sessionId, consumers) => {
|
|
179
|
+
const served = servedBy(consumers);
|
|
180
|
+
for (const cursor of cursors.get(sessionId)?.values() ?? []) if (cursor.head !== null && cursor.dispatch.failedAt === null && served(cursor)) return true;
|
|
181
|
+
return false;
|
|
182
|
+
};
|
|
183
|
+
/** The attempt-currency fence shared by consumer appends and clears. */
|
|
184
|
+
const assertCurrentCause = (sessionId, cause) => {
|
|
185
|
+
if (cause.index <= clearIndexOf(sessionId)) throw supersededAttemptError(sessionId, cause);
|
|
186
|
+
const cursor = cursorAt(sessionId, cause);
|
|
187
|
+
if (!cursor || cursor.head !== cause.index || cursor.dispatch.attemptCount !== cause.attempt || cursor.dispatch.failedAt !== null) throw supersededAttemptError(sessionId, cause);
|
|
188
|
+
};
|
|
189
|
+
const deadLetter = (sessionId, cursor, now) => {
|
|
190
|
+
cursor.dispatch.failedAt = new Date(now);
|
|
191
|
+
releaseSnapshotPins(sessionId, refOf(cursor));
|
|
192
|
+
};
|
|
193
|
+
const claim = ({ sessionId, cursor, holder, now, expiresAt, maxFailures }) => {
|
|
194
|
+
const dispatch = cursor.dispatch;
|
|
195
|
+
const row = rowAt(sessionId, cursor.head);
|
|
196
|
+
if (dispatch.claimHolder !== null) {
|
|
197
|
+
dispatch.failureCount += 1;
|
|
198
|
+
if (maxFailures !== void 0 && dispatch.failureCount >= maxFailures) {
|
|
199
|
+
dispatch.attemptCount += 1;
|
|
200
|
+
dispatch.claimHolder = null;
|
|
201
|
+
dispatch.claimExpiresAt = null;
|
|
202
|
+
deadLetter(sessionId, cursor, now);
|
|
203
|
+
return toHead(cursor, row);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
dispatch.attemptCount += 1;
|
|
207
|
+
dispatch.firstClaimedAt ??= new Date(now);
|
|
208
|
+
dispatch.lastClaimedAt = new Date(now);
|
|
209
|
+
dispatch.claimHolder = holder;
|
|
210
|
+
dispatch.claimExpiresAt = new Date(expiresAt);
|
|
211
|
+
return toHead(cursor, row);
|
|
212
|
+
};
|
|
213
|
+
const checkBatchIds = (sessionId, events) => {
|
|
214
|
+
const supplied = events.filter((candidate) => candidate.id !== void 0);
|
|
215
|
+
if (new Set(supplied.map((candidate) => candidate.id)).size !== supplied.length) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "batch contains the same event id more than once");
|
|
216
|
+
return supplied.map((candidate) => ownerOf(sessionId, candidate.id)).filter((row) => row !== void 0);
|
|
217
|
+
};
|
|
218
|
+
/** Records the types a declared log consumer takes on its cursor. */
|
|
219
|
+
const refreshLogTypes = (sessionId, consumers) => {
|
|
220
|
+
const map = cursors.get(sessionId);
|
|
221
|
+
if (!map) return;
|
|
222
|
+
for (const consumer of consumers ?? []) {
|
|
223
|
+
if (!consumer.log) continue;
|
|
224
|
+
const cursor = map.get(cursorKey(consumer.name, consumer.log.lane, 0));
|
|
225
|
+
if (cursor?.kind === "log") cursor.types = consumer.log.types;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
/** Starts the cursor work an inserted batch carries. */
|
|
229
|
+
const enqueue = (sessionId, rows, events, base, consumers) => {
|
|
230
|
+
const map = cursorsOf(sessionId);
|
|
231
|
+
rows.forEach((row, offset) => {
|
|
232
|
+
for (const target of events[offset].targets ?? []) {
|
|
233
|
+
const key = cursorKey(target.consumer, target.lane, row.index);
|
|
234
|
+
if (target.lane === null) {
|
|
235
|
+
map.set(key, {
|
|
236
|
+
consumer: target.consumer,
|
|
237
|
+
lane: null,
|
|
238
|
+
kind: "event",
|
|
239
|
+
position: row.index - 1,
|
|
240
|
+
head: row.index,
|
|
241
|
+
dispatch: freshDispatch(),
|
|
242
|
+
queue: [],
|
|
243
|
+
queued: 0,
|
|
244
|
+
completions: /* @__PURE__ */ new Map(),
|
|
245
|
+
types: null
|
|
246
|
+
});
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
let cursor = map.get(key);
|
|
250
|
+
if (!cursor) {
|
|
251
|
+
cursor = {
|
|
252
|
+
consumer: target.consumer,
|
|
253
|
+
lane: target.lane,
|
|
254
|
+
kind: "lane",
|
|
255
|
+
position: clearIndexOf(sessionId),
|
|
256
|
+
head: null,
|
|
257
|
+
dispatch: freshDispatch(),
|
|
258
|
+
queue: [],
|
|
259
|
+
queued: 0,
|
|
260
|
+
completions: /* @__PURE__ */ new Map(),
|
|
261
|
+
types: null
|
|
262
|
+
};
|
|
263
|
+
map.set(key, cursor);
|
|
264
|
+
}
|
|
265
|
+
cursor.queue.push(row.index);
|
|
266
|
+
cursor.head ??= row.index;
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
const clearIndex = clearIndexOf(sessionId);
|
|
270
|
+
for (const consumer of consumers ?? []) {
|
|
271
|
+
const log = consumer.log;
|
|
272
|
+
if (!log) continue;
|
|
273
|
+
const first = rows.find((row) => takes(log.types, row.type));
|
|
274
|
+
if (!first) continue;
|
|
275
|
+
const key = cursorKey(consumer.name, log.lane, 0);
|
|
276
|
+
const cursor = map.get(key);
|
|
277
|
+
if (!cursor) {
|
|
278
|
+
const position = log.backfill ? clearIndex : base;
|
|
279
|
+
map.set(key, {
|
|
280
|
+
consumer: consumer.name,
|
|
281
|
+
lane: log.lane,
|
|
282
|
+
kind: "log",
|
|
283
|
+
position,
|
|
284
|
+
head: log.backfill ? nextLogHead(sessionId, position, log.types) : first.index,
|
|
285
|
+
dispatch: freshDispatch(),
|
|
286
|
+
queue: [],
|
|
287
|
+
queued: 0,
|
|
288
|
+
completions: /* @__PURE__ */ new Map(),
|
|
289
|
+
types: log.types
|
|
290
|
+
});
|
|
291
|
+
} else {
|
|
292
|
+
cursor.types = log.types;
|
|
293
|
+
cursor.head ??= nextLogHead(sessionId, cursor.position, log.types);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
const insert = (sessionId, events, now, consumers, cause) => {
|
|
298
|
+
const log = logOf(sessionId);
|
|
299
|
+
const base = headOf(log);
|
|
300
|
+
const inserted = events.map((candidate, offset) => ({
|
|
301
|
+
id: candidate.id ?? generateId(),
|
|
302
|
+
type: candidate.type,
|
|
303
|
+
payload: structuredClone(candidate.payload),
|
|
304
|
+
index: base + 1 + offset,
|
|
305
|
+
sessionId,
|
|
306
|
+
createdAt: new Date(now),
|
|
307
|
+
cause: cause ? { ...cause } : candidate.cause ? { ...candidate.cause } : null
|
|
308
|
+
}));
|
|
309
|
+
for (const row of inserted) if (ownerOf(sessionId, row.id)) throw new A2Error("PARTIAL_DUPLICATE_BATCH", `event id '${row.id}' already exists`);
|
|
310
|
+
for (const row of inserted) {
|
|
311
|
+
log.rows.push(row);
|
|
312
|
+
log.byEventId.set(row.id, row);
|
|
313
|
+
}
|
|
314
|
+
enqueue(sessionId, inserted, events, base, consumers);
|
|
315
|
+
return inserted;
|
|
316
|
+
};
|
|
317
|
+
/** Every cached fold of this session below its clear, head or historical. */
|
|
318
|
+
const dropCheckpointsBelow = (sessionId, clearIndex) => {
|
|
319
|
+
const prefix = `${sessionId}\u0000`;
|
|
320
|
+
for (const [key, snapshot] of snapshots) {
|
|
321
|
+
if (!key.startsWith(prefix) || snapshot.index >= clearIndex) continue;
|
|
322
|
+
snapshots.delete(key);
|
|
323
|
+
snapshotPinCounts.delete(`${key}\u0000${snapshot.index}`);
|
|
324
|
+
}
|
|
325
|
+
for (const [key, snapshot] of historicalSnapshots) {
|
|
326
|
+
if (!key.startsWith(prefix) || snapshot.index >= clearIndex) continue;
|
|
327
|
+
historicalSnapshots.delete(key);
|
|
328
|
+
snapshotPinCounts.delete(key);
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
const readStateOf = (sessionId, reducerName, throughIndex, snapshotThroughIndex) => {
|
|
332
|
+
const key = snapshotKey(sessionId, reducerName);
|
|
333
|
+
const clearIndex = clearIndexOf(sessionId);
|
|
334
|
+
const snapshotFrontier = snapshotThroughIndex ?? throughIndex;
|
|
335
|
+
const stored = snapshots.get(key);
|
|
336
|
+
const head = stored && stored.index >= clearIndex ? stored : void 0;
|
|
337
|
+
let candidate = head && (snapshotFrontier === void 0 || head.index <= snapshotFrontier) ? head : null;
|
|
338
|
+
if (snapshotFrontier !== void 0) {
|
|
339
|
+
const prefix = `${key}\u0000`;
|
|
340
|
+
for (const [historicalKey, snapshot] of historicalSnapshots) if (historicalKey.startsWith(prefix) && snapshot.index >= clearIndex && snapshot.index <= snapshotFrontier && (!candidate || snapshot.index > candidate.index)) candidate = snapshot;
|
|
341
|
+
}
|
|
342
|
+
const snapshot = candidate ? {
|
|
343
|
+
index: candidate.index,
|
|
344
|
+
state: structuredClone(candidate.state)
|
|
345
|
+
} : null;
|
|
346
|
+
const afterIndex = Math.max(snapshot?.index ?? 0, clearIndex - 1);
|
|
347
|
+
return {
|
|
348
|
+
headIndex: head?.index ?? null,
|
|
349
|
+
snapshot,
|
|
350
|
+
events: (sessions.get(sessionId)?.rows ?? []).filter((row) => row.index > afterIndex && (throughIndex === void 0 || row.index <= throughIndex)).map(toEvent),
|
|
351
|
+
...clearIndex > 0 ? { clearIndex } : {}
|
|
352
|
+
};
|
|
353
|
+
};
|
|
354
|
+
/** The dead-lettered head a redrive names, or undefined. */
|
|
355
|
+
const deadHeadOf = (sessionId, consumer, index) => {
|
|
356
|
+
if (index <= clearIndexOf(sessionId)) return void 0;
|
|
357
|
+
for (const cursor of cursors.get(sessionId)?.values() ?? []) if (cursor.consumer === consumer && cursor.head === index && cursor.dispatch.failedAt !== null) return cursor;
|
|
358
|
+
};
|
|
359
|
+
return {
|
|
360
|
+
async append(sessionId, events, appendOptions) {
|
|
361
|
+
const consumers = appendOptions?.consumers;
|
|
362
|
+
if (events.length === 0) return {
|
|
363
|
+
events: [],
|
|
364
|
+
hasPending: hasPending(sessionId, consumers)
|
|
365
|
+
};
|
|
366
|
+
const existing = checkBatchIds(sessionId, events);
|
|
367
|
+
if (existing.length > 0) {
|
|
368
|
+
if (existing.length === events.length) return {
|
|
369
|
+
events: idempotentReplay(events, existing.map(toStored)),
|
|
370
|
+
hasPending: hasPending(sessionId, consumers)
|
|
371
|
+
};
|
|
372
|
+
throw new A2Error("PARTIAL_DUPLICATE_BATCH", `batch mixes ${existing.length} already-appended and ${events.length - existing.length} fresh events`);
|
|
373
|
+
}
|
|
374
|
+
const originIndex = appendOptions?.originIndex;
|
|
375
|
+
if (originIndex !== void 0 && originIndex <= clearIndexOf(sessionId)) throw sessionClearedError(`scheduled work from event ${originIndex} was cleared in session '${sessionId}'`);
|
|
376
|
+
for (const event of events) if (event.cause) assertCurrentCause(sessionId, event.cause);
|
|
377
|
+
const inserted = insert(sessionId, events, clock.now(), consumers);
|
|
378
|
+
notifyStreams(sessionId, inserted);
|
|
379
|
+
return {
|
|
380
|
+
events: inserted.map(toStored),
|
|
381
|
+
hasPending: hasPending(sessionId, consumers)
|
|
382
|
+
};
|
|
383
|
+
},
|
|
384
|
+
async clear({ sessionId, ifLastIndex, cause }) {
|
|
385
|
+
if (cause) assertCurrentCause(sessionId, cause);
|
|
386
|
+
const log = sessions.get(sessionId);
|
|
387
|
+
const head = headOf(log);
|
|
388
|
+
const clearIndex = log?.clearIndex ?? 0;
|
|
389
|
+
if (ifLastIndex !== void 0 && clearIndex === ifLastIndex + 1) return {
|
|
390
|
+
cleared: true,
|
|
391
|
+
index: head,
|
|
392
|
+
clearIndex
|
|
393
|
+
};
|
|
394
|
+
if (ifLastIndex !== void 0 && ifLastIndex !== head) return {
|
|
395
|
+
cleared: false,
|
|
396
|
+
index: head,
|
|
397
|
+
clearIndex
|
|
398
|
+
};
|
|
399
|
+
if (head === 0 || clearIndex === head) return {
|
|
400
|
+
cleared: true,
|
|
401
|
+
index: head,
|
|
402
|
+
clearIndex
|
|
403
|
+
};
|
|
404
|
+
const [marker] = insert(sessionId, [{
|
|
405
|
+
type: CLEAR_EVENT_TYPE,
|
|
406
|
+
payload: null
|
|
407
|
+
}], clock.now(), void 0);
|
|
408
|
+
logOf(sessionId).clearIndex = marker.index;
|
|
409
|
+
const map = cursors.get(sessionId);
|
|
410
|
+
for (const [key, cursor] of map ?? []) {
|
|
411
|
+
if (cursor.head !== null) releaseSnapshotPins(sessionId, refOf(cursor));
|
|
412
|
+
if (cursor.kind !== "log") {
|
|
413
|
+
map.delete(key);
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
cursor.position = marker.index;
|
|
417
|
+
cursor.head = null;
|
|
418
|
+
cursor.dispatch = freshDispatch();
|
|
419
|
+
cursor.completions.clear();
|
|
420
|
+
}
|
|
421
|
+
notifyStreams(sessionId, [marker]);
|
|
422
|
+
return {
|
|
423
|
+
cleared: true,
|
|
424
|
+
index: marker.index,
|
|
425
|
+
clearIndex: marker.index
|
|
426
|
+
};
|
|
427
|
+
},
|
|
428
|
+
async reclaim(sessionId) {
|
|
429
|
+
const log = sessions.get(sessionId);
|
|
430
|
+
if (!log || log.offset >= log.clearIndex - 1) return {
|
|
431
|
+
deleted: 0,
|
|
432
|
+
done: true
|
|
433
|
+
};
|
|
434
|
+
const removed = log.rows.splice(0, Math.min(RECLAIM_BATCH, log.clearIndex - 1 - log.offset));
|
|
435
|
+
log.offset += removed.length;
|
|
436
|
+
for (const row of removed) if (log.byEventId.get(row.id) === row) log.byEventId.delete(row.id);
|
|
437
|
+
const done = log.offset === log.clearIndex - 1;
|
|
438
|
+
if (done) dropCheckpointsBelow(sessionId, log.clearIndex);
|
|
439
|
+
return {
|
|
440
|
+
deleted: removed.length,
|
|
441
|
+
done
|
|
442
|
+
};
|
|
443
|
+
},
|
|
444
|
+
async wakeLog({ sessionId, consumer }) {
|
|
445
|
+
const { log } = consumer;
|
|
446
|
+
const key = cursorKey(consumer.name, log.lane, 0);
|
|
447
|
+
const head = headOf(sessions.get(sessionId));
|
|
448
|
+
let cursor = cursors.get(sessionId)?.get(key);
|
|
449
|
+
if (!cursor) {
|
|
450
|
+
const marker = clearIndexOf(sessionId);
|
|
451
|
+
if (head === marker) return {
|
|
452
|
+
cursor: idleLogCursor(consumer.name, log.lane, marker),
|
|
453
|
+
head
|
|
454
|
+
};
|
|
455
|
+
const position = log.backfill ? marker : head;
|
|
456
|
+
cursor = {
|
|
457
|
+
consumer: consumer.name,
|
|
458
|
+
lane: log.lane,
|
|
459
|
+
kind: "log",
|
|
460
|
+
position,
|
|
461
|
+
head: log.backfill ? nextLogHead(sessionId, position, log.types) : null,
|
|
462
|
+
dispatch: freshDispatch(),
|
|
463
|
+
queue: [],
|
|
464
|
+
queued: 0,
|
|
465
|
+
completions: /* @__PURE__ */ new Map(),
|
|
466
|
+
types: log.types
|
|
467
|
+
};
|
|
468
|
+
cursorsOf(sessionId).set(key, cursor);
|
|
469
|
+
} else {
|
|
470
|
+
cursor.types = log.types;
|
|
471
|
+
cursor.head ??= nextLogHead(sessionId, cursor.position, log.types);
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
cursor: toCursor(cursor),
|
|
475
|
+
head
|
|
476
|
+
};
|
|
477
|
+
},
|
|
478
|
+
async cursors(sessionId) {
|
|
479
|
+
return [...cursors.get(sessionId)?.values() ?? []].filter((cursor) => cursor.kind !== "event" || cursor.head !== null).map(toCursor).toSorted(byCursor);
|
|
480
|
+
},
|
|
481
|
+
async read(sessionId, opts) {
|
|
482
|
+
const log = sessions.get(sessionId);
|
|
483
|
+
if (!log) return [];
|
|
484
|
+
const head = headOf(log);
|
|
485
|
+
const after = Math.max(opts?.afterIndex ?? 0, log.clearIndex - 1, log.offset);
|
|
486
|
+
const through = Math.min(opts?.throughIndex ?? head, head);
|
|
487
|
+
if (through <= after) return [];
|
|
488
|
+
return log.rows.slice(after - log.offset, through - log.offset).map(toStored);
|
|
489
|
+
},
|
|
490
|
+
async claimAvailable({ sessionId, holder, ttlMs, expiresAtMs, exclude, consumers, maxFailures, through }) {
|
|
491
|
+
refreshLogTypes(sessionId, consumers);
|
|
492
|
+
const claimedAt = clock.now();
|
|
493
|
+
const now = claimedAt.getTime();
|
|
494
|
+
const expiresAt = new Date(expiresAtMs ?? now + ttlMs);
|
|
495
|
+
const excluded = new Set((exclude ?? []).map((ref) => cursorKey(ref.consumer, ref.lane, ref.index)));
|
|
496
|
+
const served = servedBy(consumers);
|
|
497
|
+
const eligible = [];
|
|
498
|
+
let retryAt = null;
|
|
499
|
+
for (const [key, cursor] of cursors.get(sessionId) ?? []) {
|
|
500
|
+
if (cursor.head === null || through !== void 0 && cursor.head > through || cursor.dispatch.failedAt !== null || !served(cursor) || excluded.has(key)) continue;
|
|
501
|
+
const expiry = cursor.dispatch.claimExpiresAt;
|
|
502
|
+
if (expiry && expiry.getTime() > now) {
|
|
503
|
+
if (!retryAt || expiry < retryAt) retryAt = new Date(expiry);
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
eligible.push(cursor);
|
|
507
|
+
}
|
|
508
|
+
if (eligible.length === 0) return retryAt ? {
|
|
509
|
+
outcome: "busy",
|
|
510
|
+
retryAt
|
|
511
|
+
} : { outcome: "settled" };
|
|
512
|
+
const clearIndex = clearIndexOf(sessionId);
|
|
513
|
+
return {
|
|
514
|
+
outcome: "claimed",
|
|
515
|
+
events: eligible.map((cursor) => {
|
|
516
|
+
const head = claim({
|
|
517
|
+
sessionId,
|
|
518
|
+
cursor,
|
|
519
|
+
holder,
|
|
520
|
+
now: claimedAt,
|
|
521
|
+
expiresAt,
|
|
522
|
+
maxFailures
|
|
523
|
+
});
|
|
524
|
+
if (clearIndex > 0) head.clearIndex = clearIndex;
|
|
525
|
+
return head;
|
|
526
|
+
}).toSorted(byHead),
|
|
527
|
+
head: headOf(sessions.get(sessionId))
|
|
528
|
+
};
|
|
529
|
+
},
|
|
530
|
+
async renewClaims({ sessionId, holder, claims, ttlMs, expiresAtMs }) {
|
|
531
|
+
const now = clock.now();
|
|
532
|
+
const expiresAt = new Date(expiresAtMs ?? now.getTime() + ttlMs);
|
|
533
|
+
const clearIndex = clearIndexOf(sessionId);
|
|
534
|
+
const renewed = [];
|
|
535
|
+
const superseded = [];
|
|
536
|
+
const ordered = claims.toSorted((a, b) => a.index - b.index || compareText(a.consumer, b.consumer));
|
|
537
|
+
for (const { attempt, ...ref } of ordered) {
|
|
538
|
+
if (ref.index <= clearIndex) {
|
|
539
|
+
superseded.push(ref);
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
const cursor = cursorAt(sessionId, ref);
|
|
543
|
+
if (!cursor) continue;
|
|
544
|
+
const dispatch = cursor.dispatch;
|
|
545
|
+
if (cursor.head !== ref.index && cursor.position < ref.index) continue;
|
|
546
|
+
if (cursor.head !== ref.index || dispatch.attemptCount > attempt) {
|
|
547
|
+
superseded.push(ref);
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
if (dispatch.failedAt === null && dispatch.claimHolder === holder && dispatch.claimExpiresAt !== null && dispatch.claimExpiresAt > now) {
|
|
551
|
+
dispatch.claimExpiresAt = new Date(expiresAt);
|
|
552
|
+
renewed.push(ref);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return {
|
|
556
|
+
renewed,
|
|
557
|
+
superseded
|
|
558
|
+
};
|
|
559
|
+
},
|
|
560
|
+
async completeAttempt({ sessionId, consumer, lane, index, attempt, events, consumers }) {
|
|
561
|
+
if (index <= clearIndexOf(sessionId)) return { outcome: "superseded" };
|
|
562
|
+
const cursor = cursorAt(sessionId, {
|
|
563
|
+
consumer,
|
|
564
|
+
lane,
|
|
565
|
+
index
|
|
566
|
+
});
|
|
567
|
+
if (!cursor) {
|
|
568
|
+
if (!rowAt(sessionId, index)) throw new TypeError(`no event at index ${index} in session '${sessionId}'`);
|
|
569
|
+
return { outcome: "superseded" };
|
|
570
|
+
}
|
|
571
|
+
refreshLogTypes(sessionId, consumers);
|
|
572
|
+
if (cursor.head !== index) {
|
|
573
|
+
const completion = cursor.completions.get(index);
|
|
574
|
+
if (!completion || completion.attempt !== attempt) return { outcome: "superseded" };
|
|
575
|
+
const requestedIds = events.map((event) => event.id);
|
|
576
|
+
if (completion.returnedEventIds.length !== requestedIds.length || completion.returnedEventIds.some((id, offset) => id !== requestedIds[offset])) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "completed attempt does not match the returned event batch");
|
|
577
|
+
const existing = checkBatchIds(sessionId, events);
|
|
578
|
+
if (existing.length !== events.length || existing.some((row) => row.cause?.consumer !== consumer || row.cause.lane !== lane || row.cause.index !== index || row.cause.attempt !== attempt)) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "completed attempt does not match the returned event batch");
|
|
579
|
+
return {
|
|
580
|
+
outcome: "completed",
|
|
581
|
+
events: events.length === 0 ? [] : idempotentReplay(events, existing.map(toStored))
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
const dispatch = cursor.dispatch;
|
|
585
|
+
if (dispatch.attemptCount !== attempt || dispatch.failedAt !== null || dispatch.claimHolder === null) return { outcome: "superseded" };
|
|
586
|
+
if (checkBatchIds(sessionId, events).length > 0) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "returned event batch contains already-appended ids");
|
|
587
|
+
const inserted = insert(sessionId, events, clock.now(), consumers, {
|
|
588
|
+
consumer,
|
|
589
|
+
lane,
|
|
590
|
+
index,
|
|
591
|
+
attempt
|
|
592
|
+
});
|
|
593
|
+
if (cursor.kind === "log") cursor.completions.clear();
|
|
594
|
+
cursor.completions.set(index, {
|
|
595
|
+
attempt,
|
|
596
|
+
returnedEventIds: events.map((event) => event.id)
|
|
597
|
+
});
|
|
598
|
+
advance(sessionId, cursor);
|
|
599
|
+
notifyStreams(sessionId, inserted);
|
|
600
|
+
return {
|
|
601
|
+
outcome: "completed",
|
|
602
|
+
events: inserted.map(toStored)
|
|
603
|
+
};
|
|
604
|
+
},
|
|
605
|
+
async failAttempt({ sessionId, consumer, lane, index, attempt, error, maxFailures, retryAfterMs }) {
|
|
606
|
+
if (index <= clearIndexOf(sessionId)) return {
|
|
607
|
+
outcome: "superseded",
|
|
608
|
+
failureCount: 0
|
|
609
|
+
};
|
|
610
|
+
const cursor = cursorAt(sessionId, {
|
|
611
|
+
consumer,
|
|
612
|
+
lane,
|
|
613
|
+
index
|
|
614
|
+
});
|
|
615
|
+
if (!cursor) {
|
|
616
|
+
if (!rowAt(sessionId, index)) throw new TypeError(`no event at index ${index} in session '${sessionId}'`);
|
|
617
|
+
return {
|
|
618
|
+
outcome: "superseded",
|
|
619
|
+
failureCount: 0
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
const dispatch = cursor.dispatch;
|
|
623
|
+
if (cursor.head !== index || dispatch.attemptCount !== attempt || dispatch.claimHolder === null && dispatch.lastFailedAttempt !== attempt) return {
|
|
624
|
+
outcome: "superseded",
|
|
625
|
+
failureCount: cursor.head === index ? dispatch.failureCount : 0
|
|
626
|
+
};
|
|
627
|
+
if (dispatch.lastFailedAttempt === attempt && dispatch.claimHolder === null) return {
|
|
628
|
+
outcome: dispatch.failedAt ? "dead_lettered" : "failed",
|
|
629
|
+
failureCount: dispatch.failureCount
|
|
630
|
+
};
|
|
631
|
+
if (dispatch.failedAt) return {
|
|
632
|
+
outcome: "dead_lettered",
|
|
633
|
+
failureCount: dispatch.failureCount
|
|
634
|
+
};
|
|
635
|
+
dispatch.failureCount += 1;
|
|
636
|
+
dispatch.lastError = error;
|
|
637
|
+
const failedAt = clock.now();
|
|
638
|
+
dispatch.lastFailedAt = new Date(failedAt);
|
|
639
|
+
dispatch.lastFailedAttempt = attempt;
|
|
640
|
+
dispatch.claimHolder = null;
|
|
641
|
+
dispatch.claimExpiresAt = retryAfterMs === void 0 || dispatch.failureCount >= maxFailures ? null : new Date(failedAt.getTime() + retryAfterMs);
|
|
642
|
+
if (dispatch.failureCount >= maxFailures) {
|
|
643
|
+
deadLetter(sessionId, cursor, failedAt);
|
|
644
|
+
return {
|
|
645
|
+
outcome: "dead_lettered",
|
|
646
|
+
failureCount: dispatch.failureCount
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
return {
|
|
650
|
+
outcome: "failed",
|
|
651
|
+
failureCount: dispatch.failureCount
|
|
652
|
+
};
|
|
653
|
+
},
|
|
654
|
+
async deadLetters(sessionId) {
|
|
655
|
+
const clearIndex = clearIndexOf(sessionId);
|
|
656
|
+
const dead = [];
|
|
657
|
+
for (const cursor of cursors.get(sessionId)?.values() ?? []) {
|
|
658
|
+
if (cursor.head === null || cursor.head <= clearIndex || cursor.dispatch.failedAt === null) continue;
|
|
659
|
+
dead.push(toHead(cursor, rowAt(sessionId, cursor.head)));
|
|
660
|
+
}
|
|
661
|
+
return dead.toSorted(byHead);
|
|
662
|
+
},
|
|
663
|
+
async redrive({ sessionId, consumer, index, action, consumers }) {
|
|
664
|
+
const cursor = deadHeadOf(sessionId, consumer, index);
|
|
665
|
+
if (!cursor) return {
|
|
666
|
+
redriven: false,
|
|
667
|
+
hasPending: hasPending(sessionId, consumers)
|
|
668
|
+
};
|
|
669
|
+
refreshLogTypes(sessionId, consumers);
|
|
670
|
+
if (action === "skip") advance(sessionId, cursor);
|
|
671
|
+
else {
|
|
672
|
+
cursor.dispatch.failedAt = null;
|
|
673
|
+
cursor.dispatch.failureCount = 0;
|
|
674
|
+
}
|
|
675
|
+
return {
|
|
676
|
+
redriven: true,
|
|
677
|
+
hasPending: hasPending(sessionId, consumers)
|
|
678
|
+
};
|
|
679
|
+
},
|
|
680
|
+
async readState(sessionId, reducerName, stateOptions) {
|
|
681
|
+
return readStateOf(sessionId, reducerName, stateOptions?.throughIndex, stateOptions?.snapshotThroughIndex);
|
|
682
|
+
},
|
|
683
|
+
async readStates(requests, reducerName) {
|
|
684
|
+
return requests.map((request) => readStateOf(request.sessionId, reducerName, request.throughIndex, request.snapshotThroughIndex));
|
|
685
|
+
},
|
|
686
|
+
async putSnapshots(sessionId, reducerName, writes) {
|
|
687
|
+
const key = snapshotKey(sessionId, reducerName);
|
|
688
|
+
const clearIndex = clearIndexOf(sessionId);
|
|
689
|
+
for (const write of writes.toSorted((a, b) => a.index - b.index)) {
|
|
690
|
+
if (write.index < clearIndex) continue;
|
|
691
|
+
const checkpoint = checkpointKey(sessionId, reducerName, write.index);
|
|
692
|
+
for (const pin of write.pins ?? []) {
|
|
693
|
+
const cursor = cursorAt(sessionId, pin);
|
|
694
|
+
if (!cursor || pin.index <= clearIndex || cursor.head !== pin.index || cursor.dispatch.failedAt !== null) continue;
|
|
695
|
+
const pinKey = headPinKey(sessionId, pin);
|
|
696
|
+
let pinned = snapshotPins.get(pinKey);
|
|
697
|
+
if (!pinned) {
|
|
698
|
+
pinned = /* @__PURE__ */ new Set();
|
|
699
|
+
snapshotPins.set(pinKey, pinned);
|
|
700
|
+
}
|
|
701
|
+
if (!pinned.has(checkpoint)) {
|
|
702
|
+
pinned.add(checkpoint);
|
|
703
|
+
snapshotPinCounts.set(checkpoint, (snapshotPinCounts.get(checkpoint) ?? 0) + 1);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
const current = snapshots.get(key);
|
|
707
|
+
if (!current || write.index >= current.index) {
|
|
708
|
+
if (current && write.index > current.index && current.index >= clearIndex) {
|
|
709
|
+
const currentCheckpoint = checkpointKey(sessionId, reducerName, current.index);
|
|
710
|
+
if ((snapshotPinCounts.get(currentCheckpoint) ?? 0) > 0) historicalSnapshots.set(currentCheckpoint, {
|
|
711
|
+
index: current.index,
|
|
712
|
+
state: structuredClone(current.state),
|
|
713
|
+
updatedAt: new Date(current.updatedAt)
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
snapshots.set(key, {
|
|
717
|
+
index: write.index,
|
|
718
|
+
state: structuredClone(write.state),
|
|
719
|
+
updatedAt: clock.now()
|
|
720
|
+
});
|
|
721
|
+
historicalSnapshots.delete(checkpoint);
|
|
722
|
+
} else if (write.index < current.index && (snapshotPinCounts.get(checkpoint) ?? 0) > 0) historicalSnapshots.set(checkpoint, {
|
|
723
|
+
index: write.index,
|
|
724
|
+
state: structuredClone(write.state),
|
|
725
|
+
updatedAt: clock.now()
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
},
|
|
729
|
+
presence: {
|
|
730
|
+
async set(ns, participant, values, meta) {
|
|
731
|
+
let participants = presenceRows.get(ns);
|
|
732
|
+
if (!participants) {
|
|
733
|
+
participants = /* @__PURE__ */ new Map();
|
|
734
|
+
presenceRows.set(ns, participants);
|
|
735
|
+
}
|
|
736
|
+
let fields = participants.get(participant);
|
|
737
|
+
if (!fields) {
|
|
738
|
+
fields = /* @__PURE__ */ new Map();
|
|
739
|
+
participants.set(participant, fields);
|
|
740
|
+
}
|
|
741
|
+
const now = clock.now();
|
|
742
|
+
const expiresAtMs = now.getTime() + meta.ttlMs;
|
|
743
|
+
const applied = nullProtoRecord();
|
|
744
|
+
let appliedCount = 0;
|
|
745
|
+
for (const [field, value] of Object.entries(values)) {
|
|
746
|
+
const existing = fields.get(field);
|
|
747
|
+
if (existing && existing.expiresAt > now && existing.at > meta.at) continue;
|
|
748
|
+
fields.set(field, {
|
|
749
|
+
value: structuredClone(value),
|
|
750
|
+
seen: meta.seen,
|
|
751
|
+
at: new Date(meta.at),
|
|
752
|
+
expiresAt: new Date(expiresAtMs)
|
|
753
|
+
});
|
|
754
|
+
applied[field] = structuredClone(value);
|
|
755
|
+
appliedCount += 1;
|
|
756
|
+
}
|
|
757
|
+
if (fields.size === 0) participants.delete(participant);
|
|
758
|
+
if (participants.size === 0) presenceRows.delete(ns);
|
|
759
|
+
if (appliedCount === 0) return;
|
|
760
|
+
const subs = presenceSubscribers.get(ns);
|
|
761
|
+
if (!subs) return;
|
|
762
|
+
const patch = {
|
|
763
|
+
participant,
|
|
764
|
+
values: applied,
|
|
765
|
+
seen: meta.seen,
|
|
766
|
+
at: new Date(meta.at)
|
|
767
|
+
};
|
|
768
|
+
for (const listener of subs) listener(patch);
|
|
769
|
+
},
|
|
770
|
+
async read(ns) {
|
|
771
|
+
const participants = presenceRows.get(ns);
|
|
772
|
+
if (!participants) return [];
|
|
773
|
+
const now = clock.now();
|
|
774
|
+
const rows = [];
|
|
775
|
+
for (const [participant, fields] of participants) {
|
|
776
|
+
for (const [field, entry] of fields) {
|
|
777
|
+
if (entry.expiresAt <= now) {
|
|
778
|
+
fields.delete(field);
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
if (entry.value === null) continue;
|
|
782
|
+
rows.push({
|
|
783
|
+
participant,
|
|
784
|
+
field,
|
|
785
|
+
value: structuredClone(entry.value),
|
|
786
|
+
seen: entry.seen,
|
|
787
|
+
at: new Date(entry.at),
|
|
788
|
+
expiresAt: new Date(entry.expiresAt)
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
if (fields.size === 0) participants.delete(participant);
|
|
792
|
+
}
|
|
793
|
+
if (participants.size === 0) presenceRows.delete(ns);
|
|
794
|
+
return rows;
|
|
795
|
+
},
|
|
796
|
+
subscribe(ns, onPatch) {
|
|
797
|
+
let subs = presenceSubscribers.get(ns);
|
|
798
|
+
if (!subs) {
|
|
799
|
+
subs = /* @__PURE__ */ new Set();
|
|
800
|
+
presenceSubscribers.set(ns, subs);
|
|
801
|
+
}
|
|
802
|
+
subs.add(onPatch);
|
|
803
|
+
return () => {
|
|
804
|
+
subs.delete(onPatch);
|
|
805
|
+
if (subs.size === 0) presenceSubscribers.delete(ns);
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
},
|
|
809
|
+
stream(sessionId, opts) {
|
|
810
|
+
const startAfter = opts?.startAfter ?? 0;
|
|
811
|
+
return { [Symbol.asyncIterator]() {
|
|
812
|
+
let last = Math.max(startAfter, clearIndexOf(sessionId) - 1);
|
|
813
|
+
let wake = null;
|
|
814
|
+
let closed = false;
|
|
815
|
+
const onRow = () => {
|
|
816
|
+
wake?.();
|
|
817
|
+
};
|
|
818
|
+
let subs = streamSubscribers.get(sessionId);
|
|
819
|
+
if (!subs) {
|
|
820
|
+
subs = /* @__PURE__ */ new Set();
|
|
821
|
+
streamSubscribers.set(sessionId, subs);
|
|
822
|
+
}
|
|
823
|
+
subs.add(onRow);
|
|
824
|
+
const stop = () => {
|
|
825
|
+
if (closed) return;
|
|
826
|
+
closed = true;
|
|
827
|
+
subs.delete(onRow);
|
|
828
|
+
if (subs.size === 0) streamSubscribers.delete(sessionId);
|
|
829
|
+
wake?.();
|
|
830
|
+
};
|
|
831
|
+
return {
|
|
832
|
+
async next() {
|
|
833
|
+
try {
|
|
834
|
+
for (;;) {
|
|
835
|
+
if (closed) return {
|
|
836
|
+
value: void 0,
|
|
837
|
+
done: true
|
|
838
|
+
};
|
|
839
|
+
const log = sessions.get(sessionId);
|
|
840
|
+
const row = log?.rows[Math.max(0, last - log.offset)];
|
|
841
|
+
if (row) {
|
|
842
|
+
const value = toStored(row);
|
|
843
|
+
last = row.index;
|
|
844
|
+
return {
|
|
845
|
+
value,
|
|
846
|
+
done: false
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
await new Promise((resolve) => {
|
|
850
|
+
wake = resolve;
|
|
851
|
+
});
|
|
852
|
+
wake = null;
|
|
853
|
+
}
|
|
854
|
+
} catch (error) {
|
|
855
|
+
stop();
|
|
856
|
+
throw error;
|
|
857
|
+
}
|
|
858
|
+
},
|
|
859
|
+
async return() {
|
|
860
|
+
stop();
|
|
861
|
+
return {
|
|
862
|
+
value: void 0,
|
|
863
|
+
done: true
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
} };
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
//#endregion
|
|
872
|
+
export { memory as t };
|