@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.
Files changed (130) hide show
  1. package/README.md +27 -27
  2. package/dist/{a2-C41hD8SF.mjs → a2-ChRLRupa.mjs} +969 -25
  3. package/dist/access.d.mts +1 -1
  4. package/dist/access.mjs +3 -3
  5. package/dist/{actors-DjMeZpnU.mjs → actors-BOamkEuz.mjs} +3 -73
  6. package/dist/attempts-CLG840Oj.mjs +20 -0
  7. package/dist/{auth-COzc09sn.mjs → auth-Drg9FiC2.mjs} +225 -2
  8. package/dist/backend-B-SJ7p6p.d.mts +680 -0
  9. package/dist/backend-a2/contract.d.mts +873 -2
  10. package/dist/backend-a2/contract.mjs +2 -2
  11. package/dist/backend-a2/scheduler-local.d.mts +1 -1
  12. package/dist/backend-a2/scheduler-local.mjs +1 -1
  13. package/dist/backend-a2/scheduler-qstash.d.mts +78 -1
  14. package/dist/backend-a2/scheduler-qstash.mjs +521 -1
  15. package/dist/backend-a2/scheduler-vercel.d.mts +37 -1
  16. package/dist/backend-a2/scheduler-vercel.mjs +203 -1
  17. package/dist/backend-a2/store-memory.d.mts +2 -2
  18. package/dist/backend-a2/store-memory.mjs +3 -3
  19. package/dist/backend-a2/store-postgres.d.mts +1 -1
  20. package/dist/backend-a2/store-postgres.mjs +2850 -2
  21. package/dist/backend-a2/store-redis-http.d.mts +1 -1
  22. package/dist/backend-a2/store-redis-http.mjs +490 -2
  23. package/dist/backend-a2/store-redis.d.mts +10 -2
  24. package/dist/backend-a2/store-redis.mjs +51 -2
  25. package/dist/backend-a2/store-sqlite.d.mts +1 -1
  26. package/dist/backend-a2/store-sqlite.mjs +3 -3
  27. package/dist/backend-a2.d.mts +2706 -3
  28. package/dist/backend-a2.mjs +1 -1
  29. package/dist/{backend-http-Bid7RIpC.d.mts → backend-http-7cNb8nJI.d.mts} +10 -3
  30. package/dist/{backend-http-0roEmjl0.mjs → backend-http-CMlL3yq3.mjs} +2 -2
  31. package/dist/{backend-http-BVRno-ca.mjs → backend-http-Ddzd5kTS.mjs} +32 -3
  32. package/dist/backend-http.d.mts +1 -1
  33. package/dist/backend-http.mjs +1 -1
  34. package/dist/backend-http.node.d.mts +1 -1
  35. package/dist/backend-http.node.mjs +1 -1
  36. package/dist/{chat-provider-XCnQlxq6.d.mts → chat-provider-DNJQyajS.d.mts} +16 -4
  37. package/dist/{chat-provider-jFSeyNmB.mjs → chat-provider-YHy-WbrB.mjs} +15 -9
  38. package/dist/cli.mjs +30 -10
  39. package/dist/{client-DGDPaN7U.mjs → client-BA-J7usH.mjs} +10 -2
  40. package/dist/{conformance-DRR23YNy.mjs → conformance-CT5EeHeC.mjs} +5 -2
  41. package/dist/conformance.d.mts +1 -1
  42. package/dist/conformance.mjs +1 -1
  43. package/dist/{engine-Bir5s8ts.mjs → engine-D0oBUBEK.mjs} +249 -163
  44. package/dist/events-CewD4kw7.mjs +75 -0
  45. package/dist/{exports-Dvc1o52I.mjs → exports-CgjfMrQ-.mjs} +35 -32
  46. package/dist/exports-ZsgjzEnM.d.mts +4 -0
  47. package/dist/external-BV_cUncs.mjs +151 -0
  48. package/dist/folds-CRYY14kc.d.mts +534 -0
  49. package/dist/{reducers-mUPFuFJV.mjs → folds-Dyjput8M.mjs} +6 -73
  50. package/dist/ids-C6AYo1bu.mjs +31 -0
  51. package/dist/index.d.mts +4 -4
  52. package/dist/{index.default-Bkg77Pxn.mjs → index.default-C5Cpz3sa.mjs} +1 -1
  53. package/dist/index.mjs +5 -5
  54. package/dist/index.node.d.mts +4 -4
  55. package/dist/index.node.mjs +3 -3
  56. package/dist/inspect/app.js +31 -31
  57. package/dist/inspect/engine.js +5 -5
  58. package/dist/internal-CvsrprnU-CRY89I-b.mjs +203 -0
  59. package/dist/{local-Cr_dSjvs.mjs → local-DhkrAqPP.mjs} +1 -1
  60. package/dist/mentions-CB6ShY4R.mjs +72 -0
  61. package/dist/{targets-yM5JBUgr.mjs → middleware-CqaCwucP.mjs} +1 -33
  62. package/dist/platform-B4TnJtWu-CwcTyKg2.mjs +32 -0
  63. package/dist/{project-CFiArraX.mjs → project-DMDST9kl.mjs} +1 -1
  64. package/dist/{provider-BGjz-mDa.d.mts → provider-B0b9Qxw4.d.mts} +170 -40
  65. package/dist/provider-B3DpYSDq.mjs +220 -0
  66. package/dist/redis-tables-DN9PfIDi.mjs +3897 -0
  67. package/dist/reducer-CYPHsSmd-BQIfozGi.d.mts +170 -0
  68. package/dist/reducer-CjmvZSAb-D_OSISmj.mjs +74 -0
  69. package/dist/{contract-CuJmlrhw.mjs → reducers-BFmIXilu.mjs} +260 -287
  70. package/dist/retryable-lazy-DZWmHpii-Bye-W8q7.mjs +19 -0
  71. package/dist/scheduler-task-X1SToDjP-BVoWuInI.mjs +69 -0
  72. package/dist/{serve-BsOOc-6_.mjs → serve-BGYw0uNL.mjs} +5 -5
  73. package/dist/{server-jwPY7_wi.mjs → server-DPxBSogx.mjs} +4 -4
  74. package/dist/server-DtFrWxi3-Boa6gdWO.mjs +3602 -0
  75. package/dist/server-qgez0Xny-D5mLfqFQ.d.mts +385 -0
  76. package/dist/{serving-BNbzc96L.mjs → serving-CNkgUPW1.mjs} +1 -1
  77. package/dist/session-clear-wHd5GCpZ-BnKSxReK.mjs +51 -0
  78. package/dist/{sql-DbiEnqM0.mjs → sql-U9WTxrwJ.mjs} +2 -2
  79. package/dist/{sqlite-db-Cbj03vmp.mjs → sqlite-db-Ds4majtT.mjs} +1 -1
  80. package/dist/store-D0MhBvg6-4Z2L0Kqd.d.mts +530 -0
  81. package/dist/store-N8PXxDAS-BWBgzBHp.mjs +5 -0
  82. package/dist/store-codec-DwrWlgQx-CYxGW0aD.mjs +61 -0
  83. package/dist/store-memory-Bas9Uq5C.mjs +872 -0
  84. package/dist/store-memory-ZLiZbH3R.mjs +2 -0
  85. package/dist/store-polling-BZt2Rf3D-BiEM6_HB.mjs +148 -0
  86. package/dist/store-presence-polling-JuAy2Ck2-CYpuK75X.mjs +109 -0
  87. package/dist/store-sqlite-BmwL3pUR.mjs +2 -0
  88. package/dist/store-sqlite-CxxPCcCT.mjs +1053 -0
  89. package/dist/stream-buffer-DrTyiIuo-BKy-vI14.mjs +53 -0
  90. package/dist/sync-github.d.mts +24 -17
  91. package/dist/sync-github.mjs +89 -22
  92. package/dist/sync-kapso.d.mts +15 -13
  93. package/dist/sync-kapso.mjs +17 -14
  94. package/dist/sync-linear.d.mts +219 -0
  95. package/dist/sync-linear.mjs +311 -0
  96. package/dist/sync-slack.d.mts +12 -17
  97. package/dist/sync-slack.mjs +54 -42
  98. package/dist/sync.chat.d.mts +1 -1
  99. package/dist/sync.chat.mjs +1 -1
  100. package/dist/sync.d.mts +3 -3
  101. package/dist/sync.mjs +5 -4
  102. package/dist/targets-C-wdR5wP.mjs +33 -0
  103. package/dist/{tokens-DKLn5P9B.mjs → tokens-DhuzKzAl.mjs} +1 -1
  104. package/dist/{types-BHCjTZdB.d.mts → types-8yq2VnKI.d.mts} +2 -2
  105. package/dist/{users-Dls7_ANV.mjs → users-yryHKFMo.mjs} +2 -2
  106. package/dist/{watch-spec-W0c7ypHJ.mjs → watchers-hash-RLtnSF-t.mjs} +233 -28
  107. package/docs/01-board/01-mounting.md +1 -1
  108. package/docs/02-sync/01-overview.md +72 -48
  109. package/docs/02-sync/02-functions.md +398 -0
  110. package/docs/02-sync/03-slack.md +30 -25
  111. package/docs/02-sync/04-github.md +62 -26
  112. package/docs/02-sync/05-whatsapp.md +23 -19
  113. package/docs/02-sync/06-providers.md +60 -27
  114. package/docs/02-sync/07-linear.md +129 -0
  115. package/docs/03-guides/04-triage.md +1 -1
  116. package/docs/04-reference/01-cli.md +26 -6
  117. package/docs/04-reference/02-board.md +18 -5
  118. package/docs/04-reference/03-http.md +5 -2
  119. package/docs/04-reference/05-conformance.md +1 -1
  120. package/docs/index.md +5 -10
  121. package/package.json +31 -6
  122. package/dist/a2-D24cvwpN.d.mts +0 -3359
  123. package/dist/access-CJU2-vaa.mjs +0 -226
  124. package/dist/attempts-Bxp9KgPf.mjs +0 -919
  125. package/dist/exports-DLiE7fLY.d.mts +0 -4
  126. package/dist/provider-CuewcDNZ.mjs +0 -205
  127. package/dist/redis-tables-B35vv4CZ.mjs +0 -842
  128. package/dist/reducers-B_vUGQRp.d.mts +0 -1172
  129. package/dist/watchers-hash-CLjsa6od.mjs +0 -225
  130. package/docs/02-sync/02-callbacks.md +0 -265
@@ -0,0 +1,1053 @@
1
+ import { n as CLEAR_EVENT_TYPE, o as sessionClearedError, s as supersededAttemptError, t as A2Error } from "./session-clear-wHd5GCpZ-BnKSxReK.mjs";
2
+ import { r as pollingStream } from "./store-polling-BZt2Rf3D-BiEM6_HB.mjs";
3
+ import { a as idleLogCursor, i as idempotentReplay, n as decodeCause, r as decodeReturnedEventIds, t as cursorKeyOf } from "./store-codec-DwrWlgQx-CYxGW0aD.mjs";
4
+ import { n as SYSTEM_CLOCK, t as RANDOM_IDS } from "./store-N8PXxDAS-BWBgzBHp.mjs";
5
+ import { mkdirSync } from "node:fs";
6
+ import { dirname, resolve } from "node:path";
7
+ import { DatabaseSync } from "node:sqlite";
8
+ //#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-sqlite.js
9
+ /**
10
+ * experimental-a2/store-sqlite — sqlite store backend (the dev default, `.a2/dev.db`).
11
+ *
12
+ * Built on `node:sqlite` (Node ≥ 22.13) so it ships with zero
13
+ * dependencies. Implements the A2Store interface; the conformance suite
14
+ * in test/conformance is the executable contract.
15
+ *
16
+ * The spec's `index` column is stored as `idx` (a2-implementation.md
17
+ * §11 — `index` collides with a reserved word) and mapped back at the
18
+ * API boundary. Timestamps are epoch milliseconds written from the
19
+ * injected clock — never SQL `now()` — so tests can time-travel against
20
+ * real storage.
21
+ */
22
+ /** The cursor layout; databases from before it are rejected, not migrated. */
23
+ const LAYOUT = "cursors-1";
24
+ const SCHEMA = `
25
+ create table if not exists a2_meta (
26
+ key text not null primary key,
27
+ value text not null
28
+ ) strict;
29
+
30
+ create table if not exists a2_events (
31
+ session_id text not null,
32
+ idx integer not null,
33
+ event_type text not null,
34
+ payload text not null,
35
+ event_id text not null,
36
+ created_at integer not null,
37
+ cause text,
38
+ primary key (session_id, idx)
39
+ ) strict;
40
+
41
+ create unique index if not exists a2_events_session_event_id
42
+ on a2_events (session_id, event_id);
43
+ drop index if exists a2_events_event_id;
44
+ create index if not exists a2_events_types on a2_events (session_id, event_type, idx);
45
+
46
+ create table if not exists a2_cursors (
47
+ session_id text not null,
48
+ consumer text not null,
49
+ cursor_key text not null,
50
+ lane text,
51
+ kind text not null,
52
+ position integer not null,
53
+ head integer,
54
+ types text,
55
+ attempt_count integer not null default 0,
56
+ first_claimed_at integer,
57
+ last_claimed_at integer,
58
+ claim_holder text,
59
+ claim_expires_at integer,
60
+ failure_count integer not null default 0,
61
+ last_failed_at integer,
62
+ last_failed_attempt integer,
63
+ last_error text,
64
+ failed_at integer,
65
+ completed_idx integer,
66
+ completed_attempt integer,
67
+ returned_event_ids text,
68
+ primary key (session_id, consumer, cursor_key)
69
+ ) strict;
70
+
71
+ create index if not exists a2_cursors_heads
72
+ on a2_cursors (session_id, head) where head is not null;
73
+
74
+ create table if not exists a2_lane_events (
75
+ session_id text not null,
76
+ consumer text not null,
77
+ lane text not null,
78
+ idx integer not null,
79
+ done integer not null default 0,
80
+ completed_attempt integer,
81
+ returned_event_ids text,
82
+ primary key (session_id, consumer, lane, idx)
83
+ ) strict;
84
+
85
+ create table if not exists a2_snapshots (
86
+ session_id text not null,
87
+ reducer_name text not null,
88
+ up_to_index integer not null,
89
+ state text not null,
90
+ updated_at integer not null,
91
+ primary key (session_id, reducer_name)
92
+ ) strict;
93
+
94
+ create table if not exists a2_snapshot_history (
95
+ session_id text not null,
96
+ reducer_name text not null,
97
+ up_to_index integer not null,
98
+ state text not null,
99
+ updated_at integer not null,
100
+ primary key (session_id, reducer_name, up_to_index)
101
+ ) strict;
102
+
103
+ create table if not exists a2_snapshot_pins (
104
+ session_id text not null,
105
+ consumer text not null,
106
+ cursor_key text not null,
107
+ event_index integer not null,
108
+ reducer_name text not null,
109
+ up_to_index integer not null,
110
+ primary key (session_id, consumer, cursor_key, event_index, reducer_name, up_to_index)
111
+ ) strict;
112
+
113
+ create index if not exists a2_snapshot_pins_checkpoint
114
+ on a2_snapshot_pins (session_id, reducer_name, up_to_index);
115
+
116
+ create table if not exists a2_clears (
117
+ session_id text not null primary key,
118
+ clear_index integer not null
119
+ ) strict;
120
+
121
+ create table if not exists a2_presence (
122
+ ns text not null,
123
+ participant text not null,
124
+ field text not null,
125
+ value text not null,
126
+ seen integer not null,
127
+ at integer not null,
128
+ expires_at integer not null,
129
+ primary key (ns, participant, field)
130
+ ) strict;
131
+ `;
132
+ /**
133
+ * Switching journal modes takes an exclusive lock, and — unlike normal
134
+ * statements — the switch can return SQLITE_BUSY without consulting the
135
+ * busy handler while several connections race it (instances cold-booting
136
+ * against one file). Bounded synchronous retry; the window is boot-only
137
+ * and tiny. Found by the multi-process torture test.
138
+ */
139
+ const setWalJournalMode = (db) => {
140
+ for (let attempt = 0;; attempt += 1) try {
141
+ db.exec("pragma journal_mode = wal");
142
+ return;
143
+ } catch (err) {
144
+ if (!(err.errcode === 5) || attempt >= 100) throw err;
145
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
146
+ }
147
+ };
148
+ const wrap = (fn) => {
149
+ try {
150
+ return fn();
151
+ } catch (err) {
152
+ if (err instanceof A2Error || err instanceof TypeError) throw err;
153
+ throw new A2Error("STORE_UNAVAILABLE", "sqlite store operation failed", { cause: err });
154
+ }
155
+ };
156
+ const toDate = (ms) => ms === null ? null : new Date(Number(ms));
157
+ const toNumber = (value) => value === null ? null : Number(value);
158
+ const toStored = (row) => ({
159
+ id: row.event_id,
160
+ type: row.event_type,
161
+ payload: JSON.parse(row.payload),
162
+ index: Number(row.idx),
163
+ sessionId: row.session_id,
164
+ createdAt: new Date(Number(row.created_at)),
165
+ cause: decodeCause(row.cause)
166
+ });
167
+ const toEvent = (row) => ({
168
+ id: row.event_id,
169
+ type: row.event_type,
170
+ payload: JSON.parse(row.payload),
171
+ index: Number(row.idx),
172
+ sessionId: row.session_id,
173
+ createdAt: new Date(Number(row.created_at))
174
+ });
175
+ const toCursor = (row) => ({
176
+ consumer: row.consumer,
177
+ lane: row.lane,
178
+ position: Number(row.position),
179
+ head: toNumber(row.head),
180
+ firstClaimedAt: toDate(row.first_claimed_at),
181
+ lastClaimedAt: toDate(row.last_claimed_at),
182
+ attemptCount: Number(row.attempt_count),
183
+ claimHolder: row.claim_holder,
184
+ claimExpiresAt: toDate(row.claim_expires_at),
185
+ failureCount: Number(row.failure_count),
186
+ lastFailedAt: toDate(row.last_failed_at),
187
+ lastFailedAttempt: toNumber(row.last_failed_attempt),
188
+ lastError: row.last_error,
189
+ failedAt: toDate(row.failed_at)
190
+ });
191
+ const toHead = (row) => {
192
+ const { position: _position, head: _head, ...dispatch } = toCursor(row);
193
+ return {
194
+ ...toStored({
195
+ session_id: row.session_id,
196
+ idx: row.e_idx,
197
+ event_type: row.e_event_type,
198
+ payload: row.e_payload,
199
+ event_id: row.e_event_id,
200
+ created_at: row.e_created_at,
201
+ cause: row.e_cause
202
+ }),
203
+ ...dispatch
204
+ };
205
+ };
206
+ const RECLAIM_BATCH = 256;
207
+ /** The head columns every claim, dead letter, and listing joins in. */
208
+ const HEAD_SELECT = `
209
+ select cursor.*,
210
+ event.idx as e_idx, event.event_type as e_event_type,
211
+ event.payload as e_payload, event.event_id as e_event_id,
212
+ event.created_at as e_created_at, event.cause as e_cause
213
+ from a2_cursors cursor
214
+ join a2_events event
215
+ on event.session_id = cursor.session_id and event.idx = cursor.head`;
216
+ /** A fresh head's dispatch columns, for `set` clauses. */
217
+ const FRESH_DISPATCH = `
218
+ attempt_count = 0, first_claimed_at = null, last_claimed_at = null,
219
+ claim_holder = null, claim_expires_at = null, failure_count = 0,
220
+ last_failed_at = null, last_failed_attempt = null, last_error = null,
221
+ failed_at = null`;
222
+ const placeholders = (count) => Array.from({ length: count }, () => "?").join(", ");
223
+ const compareText = (a, b) => a < b ? -1 : a > b ? 1 : 0;
224
+ function sqlite(options = {}) {
225
+ const path = options.path ?? ".a2/dev.db";
226
+ const clock = options.clock ?? SYSTEM_CLOCK;
227
+ const generateId = options.ids ?? RANDOM_IDS;
228
+ if (path !== ":memory:") mkdirSync(dirname(resolve(
229
+ /* turbopackIgnore: true */
230
+ path
231
+ )), { recursive: true });
232
+ const db = new DatabaseSync(path);
233
+ db.exec("pragma busy_timeout = 5000");
234
+ setWalJournalMode(db);
235
+ db.exec("pragma synchronous = normal");
236
+ if (db.prepare("select 1 from pragma_table_info('a2_events') where name = ?").get("processed_at")) {
237
+ db.close();
238
+ throw new A2Error("STORE_NOT_CONFIGURED", `the sqlite database at '${path}' has the A2 layout from before consumers; delete it, or point the store at a fresh database file`);
239
+ }
240
+ db.exec(SCHEMA);
241
+ db.prepare("insert into a2_meta (key, value) values ('layout', ?) on conflict (key) do nothing").run(LAYOUT);
242
+ const insertEvent = db.prepare(`insert into a2_events
243
+ (session_id, idx, event_type, payload, event_id, created_at, cause)
244
+ values (?, ?, ?, ?, ?, ?, ?)`);
245
+ const selectHead = db.prepare("select coalesce(max(idx), 0) as max from a2_events where session_id = ?");
246
+ const headOf = (sessionId) => Number(selectHead.get(sessionId).max);
247
+ const selectEvent = db.prepare("select 1 from a2_events where session_id = ? and idx = ?");
248
+ const selectCursor = db.prepare("select * from a2_cursors where session_id = ? and consumer = ? and cursor_key = ?");
249
+ const cursorOf = (sessionId, ref) => selectCursor.get(sessionId, ref.consumer, cursorKeyOf(ref.lane, ref.index));
250
+ const insertEventCursor = db.prepare(`insert into a2_cursors
251
+ (session_id, consumer, cursor_key, lane, kind, position, head)
252
+ values (?, ?, ?, null, 'event', ?, ?)`);
253
+ const insertLaneEvent = db.prepare(`insert into a2_lane_events (session_id, consumer, lane, idx)
254
+ values (?, ?, ?, ?)`);
255
+ const queueLaneHead = db.prepare(`insert into a2_cursors
256
+ (session_id, consumer, cursor_key, lane, kind, position, head)
257
+ values (?, ?, ?, ?, 'lane', ?, ?)
258
+ on conflict (session_id, consumer, cursor_key) do update set
259
+ head = excluded.head,
260
+ position = max(a2_cursors.position, excluded.position),
261
+ ${FRESH_DISPATCH}
262
+ where a2_cursors.head is null or a2_cursors.head <= excluded.position`);
263
+ const openLogCursor = db.prepare(`insert into a2_cursors
264
+ (session_id, consumer, cursor_key, lane, kind, position, head, types)
265
+ values (?, ?, ?, ?, 'log', ?, ?, ?)`);
266
+ const wakeLogCursor = db.prepare(`update a2_cursors set types = ?, head = ?
267
+ where session_id = ? and consumer = ? and cursor_key = ?`);
268
+ const refreshTypes = db.prepare(`update a2_cursors set types = ?
269
+ where session_id = ? and consumer = ? and kind = 'log'
270
+ and types is not ?`);
271
+ const nextLogHeadAll = db.prepare(`select min(idx) as next from a2_events
272
+ where session_id = ? and idx > ? and event_type != ?`);
273
+ const nextLogHeadOf = db.prepare(`select min(idx) as next from a2_events
274
+ where session_id = ? and idx > ? and event_type != ?
275
+ and event_type in (select value from json_each(?))`);
276
+ const nextLogHead = (sessionId, after, types) => toNumber((types === null ? nextLogHeadAll.get(sessionId, after, CLEAR_EVENT_TYPE) : nextLogHeadOf.get(sessionId, after, CLEAR_EVENT_TYPE, types)).next);
277
+ const nextLaneHead = db.prepare(`select min(idx) as next from a2_lane_events
278
+ where session_id = ? and consumer = ? and lane = ? and done = 0
279
+ and idx > ?`);
280
+ const selectLaneEvent = db.prepare(`select completed_attempt, returned_event_ids from a2_lane_events
281
+ where session_id = ? and consumer = ? and lane = ? and idx = ?`);
282
+ const finishLaneEvent = db.prepare(`update a2_lane_events
283
+ set done = 1, completed_attempt = ?, returned_event_ids = ?
284
+ where session_id = ? and consumer = ? and lane = ? and idx = ?`);
285
+ const moveCursor = db.prepare(`update a2_cursors set position = ?, head = ?, ${FRESH_DISPATCH}
286
+ where session_id = ? and consumer = ? and cursor_key = ?`);
287
+ const recordCompletion = db.prepare(`update a2_cursors
288
+ set completed_idx = ?, completed_attempt = ?, returned_event_ids = ?
289
+ where session_id = ? and consumer = ? and cursor_key = ?`);
290
+ const upsertPresence = db.prepare(`insert into a2_presence (ns, participant, field, value, seen, at, expires_at)
291
+ values (?, ?, ?, ?, ?, ?, ?)
292
+ on conflict (ns, participant, field) do update set
293
+ value = excluded.value,
294
+ seen = excluded.seen,
295
+ at = excluded.at,
296
+ expires_at = excluded.expires_at
297
+ where a2_presence.at <= excluded.at or a2_presence.expires_at <= ?`);
298
+ const sweepPresence = db.prepare("delete from a2_presence where ns = ? and expires_at <= ?");
299
+ const selectPresence = db.prepare(`select participant, field, value, seen, at, expires_at
300
+ from a2_presence
301
+ where ns = ? and expires_at > ? and value != 'null'
302
+ order by participant, field`);
303
+ const selectByIds = (count) => db.prepare(`select * from a2_events
304
+ where session_id = ? and event_id in (${placeholders(count)})
305
+ order by idx`);
306
+ const selectClearIndex = db.prepare("select clear_index from a2_clears where session_id = ?");
307
+ const clearIndexOf = (sessionId) => Number(selectClearIndex.get(sessionId)?.clear_index ?? 0);
308
+ const deleteEventById = db.prepare("delete from a2_events where session_id = ? and event_id = ?");
309
+ /** Served consumers as a SQL condition on `cursor.consumer`. */
310
+ const servedClause = (consumers) => consumers === void 0 ? {
311
+ sql: "",
312
+ params: []
313
+ } : consumers.length === 0 ? {
314
+ sql: "and 0",
315
+ params: []
316
+ } : {
317
+ sql: `and cursor.consumer in (${placeholders(consumers.length)})`,
318
+ params: consumers.map((consumer) => consumer.name)
319
+ };
320
+ const hasPending = (sessionId, clearIndex, consumers) => {
321
+ const served = servedClause(consumers);
322
+ return db.prepare(`select 1 from a2_cursors cursor
323
+ where cursor.session_id = ? and cursor.head > ?
324
+ and cursor.failed_at is null ${served.sql}
325
+ limit 1`).get(sessionId, clearIndex, ...served.params) !== void 0;
326
+ };
327
+ /**
328
+ * The session's rows owning `ids`. Rows below its clear marker do not
329
+ * own their ids: they are deleted here so the ids can be reused.
330
+ */
331
+ const liveOwnersOf = (sessionId, clearIndex, ids) => {
332
+ if (ids.length === 0) return [];
333
+ return selectByIds(ids.length).all(sessionId, ...ids).filter((row) => {
334
+ if (Number(row.idx) >= clearIndex) return true;
335
+ deleteEventById.run(sessionId, row.event_id);
336
+ return false;
337
+ });
338
+ };
339
+ /** The attempt-currency fence shared by consumer appends and clears. */
340
+ const assertCurrentCause = ({ sessionId, cause, clearIndex }) => {
341
+ if (cause.index <= clearIndex) throw supersededAttemptError(sessionId, cause);
342
+ const cursor = cursorOf(sessionId, cause);
343
+ if (!cursor || toNumber(cursor.head) !== cause.index || Number(cursor.attempt_count) !== cause.attempt || cursor.failed_at !== null) throw supersededAttemptError(sessionId, cause);
344
+ };
345
+ const tx = (fn) => {
346
+ db.exec("begin immediate");
347
+ try {
348
+ const result = fn();
349
+ db.exec("commit");
350
+ return result;
351
+ } catch (err) {
352
+ try {
353
+ db.exec("rollback");
354
+ } catch {}
355
+ throw err;
356
+ }
357
+ };
358
+ /** A finished or dead head's snapshot pins, and history no pin keeps. */
359
+ const releasePins = (sessionId, consumer, cursorKey, index) => {
360
+ db.prepare(`delete from a2_snapshot_pins
361
+ where session_id = ? and consumer = ? and cursor_key = ? and event_index = ?`).run(sessionId, consumer, cursorKey, index);
362
+ db.prepare(`delete from a2_snapshot_history
363
+ where session_id = ?
364
+ and not exists (
365
+ select 1 from a2_snapshot_pins pin
366
+ where pin.session_id = a2_snapshot_history.session_id
367
+ and pin.reducer_name = a2_snapshot_history.reducer_name
368
+ and pin.up_to_index = a2_snapshot_history.up_to_index
369
+ )`).run(sessionId);
370
+ };
371
+ /** Moves a cursor past its head; the next head starts fresh. */
372
+ const advance = (sessionId, cursor) => {
373
+ const done = Number(cursor.head);
374
+ releasePins(sessionId, cursor.consumer, cursor.cursor_key, done);
375
+ let next;
376
+ if (cursor.kind === "event") next = null;
377
+ else if (cursor.kind === "lane") next = toNumber(nextLaneHead.get(sessionId, cursor.consumer, cursor.lane, Math.max(done, clearIndexOf(sessionId))).next);
378
+ else {
379
+ const { types } = selectCursor.get(sessionId, cursor.consumer, cursor.cursor_key);
380
+ next = nextLogHead(sessionId, done, types);
381
+ }
382
+ moveCursor.run(done, next, sessionId, cursor.consumer, cursor.cursor_key);
383
+ };
384
+ /**
385
+ * Brings a log consumer's cursor up to date and records its types. A
386
+ * missing cursor opens at the clear marker with `backfill`, otherwise at
387
+ * `base`; an idle one takes the next event of its types after its
388
+ * position; one with a head keeps it, claim and all. Returns its key.
389
+ */
390
+ const wakeLogAt = ({ sessionId, consumer, log, base, clearIndex }) => {
391
+ const types = log.types === null ? null : JSON.stringify(log.types);
392
+ const key = cursorKeyOf(log.lane, 0);
393
+ const existing = selectCursor.get(sessionId, consumer, key);
394
+ if (existing) {
395
+ wakeLogCursor.run(types, existing.head === null ? nextLogHead(sessionId, Number(existing.position), types) : Number(existing.head), sessionId, consumer, key);
396
+ return key;
397
+ }
398
+ const position = log.backfill ? clearIndex : base;
399
+ openLogCursor.run(sessionId, consumer, key, log.lane, position, nextLogHead(sessionId, position, types), types);
400
+ return key;
401
+ };
402
+ /** Starts the cursor work an inserted batch carries. */
403
+ const enqueue = ({ sessionId, events, base, clearIndex, consumers }) => {
404
+ events.forEach((event, offset) => {
405
+ const index = base + 1 + offset;
406
+ for (const target of event.targets ?? []) {
407
+ if (target.lane === null) {
408
+ insertEventCursor.run(sessionId, target.consumer, cursorKeyOf(null, index), index - 1, index);
409
+ continue;
410
+ }
411
+ insertLaneEvent.run(sessionId, target.consumer, target.lane, index);
412
+ queueLaneHead.run(sessionId, target.consumer, cursorKeyOf(target.lane, index), target.lane, clearIndex, index);
413
+ }
414
+ });
415
+ for (const consumer of consumers ?? []) {
416
+ const log = consumer.log;
417
+ if (!log) continue;
418
+ if (!events.some((event) => event.type !== "a2.cleared" && (log.types === null || log.types.includes(event.type)))) continue;
419
+ wakeLogAt({
420
+ sessionId,
421
+ consumer: consumer.name,
422
+ log,
423
+ base,
424
+ clearIndex
425
+ });
426
+ }
427
+ };
428
+ const refreshLogTypes = (sessionId, consumers) => {
429
+ for (const consumer of consumers ?? []) {
430
+ if (!consumer.log) continue;
431
+ const types = consumer.log.types === null ? null : JSON.stringify(consumer.log.types);
432
+ refreshTypes.run(types, sessionId, consumer.name, types);
433
+ }
434
+ };
435
+ const insertBatch = ({ sessionId, events, clearIndex, consumers, cause }) => {
436
+ const base = headOf(sessionId);
437
+ const now = clock.now().getTime();
438
+ const inserted = events.map((event, offset) => {
439
+ const id = event.id ?? generateId();
440
+ const index = base + 1 + offset;
441
+ const eventCause = cause ?? event.cause ?? null;
442
+ insertEvent.run(sessionId, index, event.type, JSON.stringify(event.payload) ?? "null", id, now, eventCause ? JSON.stringify(eventCause) : null);
443
+ return {
444
+ id,
445
+ type: event.type,
446
+ payload: structuredClone(event.payload),
447
+ index,
448
+ sessionId,
449
+ createdAt: new Date(now),
450
+ cause: eventCause ? { ...eventCause } : null
451
+ };
452
+ });
453
+ enqueue({
454
+ sessionId,
455
+ events,
456
+ base,
457
+ clearIndex,
458
+ consumers
459
+ });
460
+ return inserted;
461
+ };
462
+ return {
463
+ async append(sessionId, events, appendOptions) {
464
+ const consumers = appendOptions?.consumers;
465
+ if (events.length === 0) return wrap(() => ({
466
+ events: [],
467
+ hasPending: hasPending(sessionId, clearIndexOf(sessionId), consumers)
468
+ }));
469
+ return wrap(() => tx(() => {
470
+ const clearIndex = clearIndexOf(sessionId);
471
+ const suppliedIds = events.filter((e) => e.id !== void 0).map((e) => e.id);
472
+ if (new Set(suppliedIds).size !== suppliedIds.length) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "batch contains the same event id more than once");
473
+ if (suppliedIds.length > 0) {
474
+ const existing = liveOwnersOf(sessionId, clearIndex, suppliedIds);
475
+ if (existing.length > 0) {
476
+ if (existing.length === events.length) return {
477
+ events: idempotentReplay(events, existing.map(toStored)),
478
+ hasPending: hasPending(sessionId, clearIndex, consumers)
479
+ };
480
+ throw new A2Error("PARTIAL_DUPLICATE_BATCH", `batch mixes ${existing.length} already-appended and ${events.length - existing.length} fresh events`);
481
+ }
482
+ }
483
+ const originIndex = appendOptions?.originIndex;
484
+ if (originIndex !== void 0 && originIndex <= clearIndex) throw sessionClearedError(`scheduled work from event ${originIndex} was cleared in session '${sessionId}'`);
485
+ for (const e of events) if (e.cause) assertCurrentCause({
486
+ sessionId,
487
+ cause: e.cause,
488
+ clearIndex
489
+ });
490
+ return {
491
+ events: insertBatch({
492
+ sessionId,
493
+ events,
494
+ clearIndex,
495
+ consumers
496
+ }),
497
+ hasPending: hasPending(sessionId, clearIndex, consumers)
498
+ };
499
+ }));
500
+ },
501
+ async wakeLog({ sessionId, consumer }) {
502
+ return wrap(() => {
503
+ const { lane } = consumer.log;
504
+ const marker = clearIndexOf(sessionId);
505
+ if (headOf(sessionId) === marker && !selectCursor.get(sessionId, consumer.name, cursorKeyOf(lane, 0))) return {
506
+ cursor: idleLogCursor(consumer.name, lane, marker),
507
+ head: marker
508
+ };
509
+ return tx(() => {
510
+ const head = headOf(sessionId);
511
+ const key = wakeLogAt({
512
+ sessionId,
513
+ consumer: consumer.name,
514
+ log: consumer.log,
515
+ base: head,
516
+ clearIndex: clearIndexOf(sessionId)
517
+ });
518
+ const cursor = selectCursor.get(sessionId, consumer.name, key);
519
+ return {
520
+ cursor: toCursor(cursor),
521
+ head
522
+ };
523
+ });
524
+ });
525
+ },
526
+ async cursors(sessionId) {
527
+ return wrap(() => {
528
+ const clearIndex = clearIndexOf(sessionId);
529
+ return db.prepare(`select * from a2_cursors
530
+ where session_id = ?
531
+ and (
532
+ kind = 'log'
533
+ or head > ?
534
+ or (kind = 'lane' and head is null and position > ?)
535
+ )
536
+ order by consumer, lane is null, lane, head`).all(sessionId, clearIndex, clearIndex).map(toCursor);
537
+ });
538
+ },
539
+ async read(sessionId, opts) {
540
+ return wrap(() => {
541
+ const conditions = ["session_id = ?", "idx >= coalesce((select clear_index from a2_clears where session_id = ?), 0)"];
542
+ const params = [sessionId, sessionId];
543
+ if (opts?.afterIndex !== void 0) {
544
+ conditions.push("idx > ?");
545
+ params.push(opts.afterIndex);
546
+ }
547
+ if (opts?.throughIndex !== void 0) {
548
+ conditions.push("idx <= ?");
549
+ params.push(opts.throughIndex);
550
+ }
551
+ return db.prepare(`select * from a2_events where ${conditions.join(" and ")} order by idx`).all(...params).map(toStored);
552
+ });
553
+ },
554
+ async claimAvailable({ sessionId, holder, ttlMs, expiresAtMs, exclude = [], consumers, maxFailures, through }) {
555
+ return wrap(() => tx(() => {
556
+ refreshLogTypes(sessionId, consumers);
557
+ const now = clock.now().getTime();
558
+ const expiresAt = expiresAtMs ?? now + ttlMs;
559
+ const clearIndex = clearIndexOf(sessionId);
560
+ const served = servedClause(consumers);
561
+ const excludedKeys = exclude.map((ref) => `${ref.consumer}\u0000${cursorKeyOf(ref.lane, ref.index)}`);
562
+ const exclusion = excludedKeys.length === 0 ? "" : `and cursor.consumer || char(0) || cursor.cursor_key
563
+ not in (${placeholders(excludedKeys.length)})`;
564
+ const filter = `
565
+ where cursor.session_id = ?
566
+ and cursor.head > ?
567
+ and cursor.failed_at is null
568
+ ${through === void 0 ? "" : "and cursor.head <= ?"}
569
+ ${served.sql}
570
+ ${exclusion}`;
571
+ const params = [
572
+ sessionId,
573
+ clearIndex,
574
+ ...through === void 0 ? [] : [through],
575
+ ...served.params,
576
+ ...excludedKeys,
577
+ now
578
+ ];
579
+ const eligible = db.prepare(`select cursor.consumer, cursor.cursor_key,
580
+ cursor.claim_holder, cursor.failure_count
581
+ from a2_cursors cursor
582
+ ${filter}
583
+ and (cursor.claim_expires_at is null
584
+ or cursor.claim_expires_at <= ?)
585
+ order by cursor.head, cursor.consumer`).all(...params);
586
+ if (eligible.length > 0) {
587
+ const claimHead = db.prepare(`update a2_cursors set
588
+ attempt_count = attempt_count + 1,
589
+ failure_count = failure_count
590
+ + case when claim_holder is null then 0 else 1 end,
591
+ first_claimed_at = coalesce(first_claimed_at, ?),
592
+ last_claimed_at = ?,
593
+ claim_holder = ?,
594
+ claim_expires_at = ?
595
+ where session_id = ? and consumer = ? and cursor_key = ?`);
596
+ const deadLetterHead = db.prepare(`update a2_cursors set
597
+ attempt_count = attempt_count + 1,
598
+ failure_count = failure_count + 1,
599
+ failed_at = ?,
600
+ claim_holder = null,
601
+ claim_expires_at = null
602
+ where session_id = ? and consumer = ? and cursor_key = ?`);
603
+ const selectHeadRow = db.prepare(`${HEAD_SELECT}
604
+ where cursor.session_id = ? and cursor.consumer = ?
605
+ and cursor.cursor_key = ?`);
606
+ return {
607
+ outcome: "claimed",
608
+ events: eligible.map((row) => {
609
+ const exhausted = row.claim_holder !== null && maxFailures !== void 0 && Number(row.failure_count) + 1 >= maxFailures;
610
+ if (exhausted) deadLetterHead.run(now, sessionId, row.consumer, row.cursor_key);
611
+ else claimHead.run(now, now, holder, expiresAt, sessionId, row.consumer, row.cursor_key);
612
+ const claimed = toHead(selectHeadRow.get(sessionId, row.consumer, row.cursor_key));
613
+ if (exhausted) releasePins(sessionId, row.consumer, row.cursor_key, claimed.index);
614
+ if (clearIndex > 0) claimed.clearIndex = clearIndex;
615
+ return claimed;
616
+ }),
617
+ head: headOf(sessionId)
618
+ };
619
+ }
620
+ const active = db.prepare(`select min(cursor.claim_expires_at) as retry_at
621
+ from a2_cursors cursor
622
+ ${filter}
623
+ and cursor.claim_expires_at > ?`).get(...params);
624
+ return active.retry_at === null ? { outcome: "settled" } : {
625
+ outcome: "busy",
626
+ retryAt: new Date(Number(active.retry_at))
627
+ };
628
+ }));
629
+ },
630
+ async renewClaims({ sessionId, holder, claims, ttlMs, expiresAtMs }) {
631
+ if (claims.length === 0) return {
632
+ renewed: [],
633
+ superseded: []
634
+ };
635
+ return wrap(() => tx(() => {
636
+ const now = clock.now().getTime();
637
+ const expiresAt = expiresAtMs ?? now + ttlMs;
638
+ const renewed = [];
639
+ const superseded = [];
640
+ const extend = db.prepare(`update a2_cursors set claim_expires_at = ?
641
+ where session_id = ? and consumer = ? and cursor_key = ?
642
+ and head = ?
643
+ and attempt_count = ?
644
+ and failed_at is null
645
+ and claim_holder = ?
646
+ and claim_expires_at > ?`);
647
+ const clearIndex = clearIndexOf(sessionId);
648
+ const ordered = claims.toSorted((a, b) => a.index - b.index || compareText(a.consumer, b.consumer));
649
+ for (const { attempt, ...ref } of ordered) {
650
+ if (ref.index <= clearIndex) {
651
+ superseded.push(ref);
652
+ continue;
653
+ }
654
+ const cursor = cursorOf(sessionId, ref);
655
+ if (!cursor) continue;
656
+ const head = toNumber(cursor.head);
657
+ if (head !== ref.index && Number(cursor.position) < ref.index) continue;
658
+ if (head !== ref.index || Number(cursor.attempt_count) > attempt) {
659
+ superseded.push(ref);
660
+ continue;
661
+ }
662
+ const updated = extend.run(expiresAt, sessionId, ref.consumer, cursor.cursor_key, ref.index, attempt, holder, now);
663
+ if (Number(updated.changes) === 1) renewed.push(ref);
664
+ }
665
+ return {
666
+ renewed,
667
+ superseded
668
+ };
669
+ }));
670
+ },
671
+ async completeAttempt({ sessionId, consumer, lane, index, attempt, events, consumers }) {
672
+ return wrap(() => tx(() => {
673
+ const clearIndex = clearIndexOf(sessionId);
674
+ if (index <= clearIndex) return { outcome: "superseded" };
675
+ const cursor = cursorOf(sessionId, {
676
+ consumer,
677
+ lane,
678
+ index
679
+ });
680
+ if (!cursor) {
681
+ if (!selectEvent.get(sessionId, index)) throw new TypeError(`no event at index ${index} in session '${sessionId}'`);
682
+ return { outcome: "superseded" };
683
+ }
684
+ refreshLogTypes(sessionId, consumers);
685
+ const ids = events.map((event) => event.id);
686
+ if (new Set(ids).size !== ids.length) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "returned event batch contains the same event id more than once");
687
+ if (toNumber(cursor.head) !== index) {
688
+ let completion;
689
+ if (cursor.kind === "lane") {
690
+ const row = selectLaneEvent.get(sessionId, consumer, lane, index);
691
+ completion = row && {
692
+ attempt: toNumber(row.completed_attempt),
693
+ ids: row.returned_event_ids
694
+ };
695
+ } else if (toNumber(cursor.completed_idx) === index) completion = {
696
+ attempt: toNumber(cursor.completed_attempt),
697
+ ids: cursor.returned_event_ids
698
+ };
699
+ if (!completion || completion.attempt !== attempt) return { outcome: "superseded" };
700
+ const returnedIds = completion.ids === null ? null : decodeReturnedEventIds(completion.ids);
701
+ if (returnedIds === null || returnedIds.length !== ids.length || returnedIds.some((id, offset) => id !== ids[offset])) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "completed attempt does not match the returned event batch");
702
+ const existing = ids.length === 0 ? [] : selectByIds(ids.length).all(sessionId, ...ids);
703
+ const byId = new Map(existing.map((row) => [row.event_id, row]));
704
+ const ordered = ids.map((id) => byId.get(id));
705
+ if (ordered.some((row) => {
706
+ const cause = row ? decodeCause(row.cause) : null;
707
+ return !row || cause?.consumer !== consumer || cause.lane !== lane || cause.index !== index || cause.attempt !== attempt;
708
+ })) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "returned event retry does not match the committed batch");
709
+ return {
710
+ outcome: "completed",
711
+ events: events.length === 0 ? [] : idempotentReplay(events, ordered.map(toStored))
712
+ };
713
+ }
714
+ if (Number(cursor.attempt_count) !== attempt || cursor.claim_holder === null || cursor.failed_at !== null) return { outcome: "superseded" };
715
+ if (liveOwnersOf(sessionId, clearIndex, ids).length > 0) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "returned event batch contains an already-appended event id");
716
+ const inserted = insertBatch({
717
+ sessionId,
718
+ events,
719
+ clearIndex,
720
+ consumers,
721
+ cause: {
722
+ consumer,
723
+ lane,
724
+ index,
725
+ attempt
726
+ }
727
+ });
728
+ const returned = JSON.stringify(ids);
729
+ if (cursor.kind === "lane") finishLaneEvent.run(attempt, returned, sessionId, consumer, lane, index);
730
+ else recordCompletion.run(index, attempt, returned, sessionId, consumer, cursor.cursor_key);
731
+ advance(sessionId, cursor);
732
+ return {
733
+ outcome: "completed",
734
+ events: inserted
735
+ };
736
+ }));
737
+ },
738
+ async failAttempt({ sessionId, consumer, lane, index, attempt, error, maxFailures, retryAfterMs }) {
739
+ return wrap(() => tx(() => {
740
+ if (index <= clearIndexOf(sessionId)) return {
741
+ outcome: "superseded",
742
+ failureCount: 0
743
+ };
744
+ const current = cursorOf(sessionId, {
745
+ consumer,
746
+ lane,
747
+ index
748
+ });
749
+ if (!current) {
750
+ if (!selectEvent.get(sessionId, index)) throw new TypeError(`no event at index ${index} in session '${sessionId}'`);
751
+ return {
752
+ outcome: "superseded",
753
+ failureCount: 0
754
+ };
755
+ }
756
+ if (toNumber(current.head) !== index) return {
757
+ outcome: "superseded",
758
+ failureCount: 0
759
+ };
760
+ const failureCount = Number(current.failure_count);
761
+ if (Number(current.attempt_count) !== attempt) return {
762
+ outcome: "superseded",
763
+ failureCount
764
+ };
765
+ if (current.claim_holder === null && current.last_failed_attempt !== null && Number(current.last_failed_attempt) === attempt) return {
766
+ outcome: current.failed_at === null ? "failed" : "dead_lettered",
767
+ failureCount
768
+ };
769
+ if (current.failed_at !== null) return {
770
+ outcome: "dead_lettered",
771
+ failureCount
772
+ };
773
+ if (current.claim_holder === null) return {
774
+ outcome: "superseded",
775
+ failureCount
776
+ };
777
+ const now = clock.now().getTime();
778
+ const nextFailureCount = failureCount + 1;
779
+ const deadLettered = nextFailureCount >= maxFailures;
780
+ db.prepare(`update a2_cursors set
781
+ failure_count = ?,
782
+ last_error = ?,
783
+ last_failed_at = ?,
784
+ last_failed_attempt = ?,
785
+ failed_at = ?,
786
+ claim_holder = null,
787
+ claim_expires_at = ?
788
+ where session_id = ? and consumer = ? and cursor_key = ?`).run(nextFailureCount, error, now, attempt, deadLettered ? now : null, deadLettered || retryAfterMs === void 0 ? null : now + retryAfterMs, sessionId, consumer, current.cursor_key);
789
+ if (deadLettered) releasePins(sessionId, consumer, current.cursor_key, index);
790
+ return {
791
+ outcome: deadLettered ? "dead_lettered" : "failed",
792
+ failureCount: nextFailureCount
793
+ };
794
+ }));
795
+ },
796
+ async deadLetters(sessionId) {
797
+ return wrap(() => {
798
+ return db.prepare(`${HEAD_SELECT}
799
+ where cursor.session_id = ?
800
+ and cursor.failed_at is not null
801
+ and cursor.head > coalesce(
802
+ (select clear_index from a2_clears where session_id = ?), 0
803
+ )
804
+ order by cursor.head, cursor.consumer`).all(sessionId, sessionId).map(toHead);
805
+ });
806
+ },
807
+ async redrive({ sessionId, consumer, index, action, consumers }) {
808
+ return wrap(() => tx(() => {
809
+ const clearIndex = clearIndexOf(sessionId);
810
+ const cursor = index > clearIndex ? db.prepare(`select * from a2_cursors
811
+ where session_id = ? and consumer = ? and head = ?
812
+ and failed_at is not null`).get(sessionId, consumer, index) : void 0;
813
+ if (cursor) {
814
+ refreshLogTypes(sessionId, consumers);
815
+ if (action === "skip") {
816
+ if (cursor.kind === "lane") finishLaneEvent.run(null, null, sessionId, consumer, cursor.lane, index);
817
+ advance(sessionId, cursor);
818
+ } else db.prepare(`update a2_cursors set failed_at = null, failure_count = 0
819
+ where session_id = ? and consumer = ? and cursor_key = ?`).run(sessionId, consumer, cursor.cursor_key);
820
+ }
821
+ return {
822
+ redriven: cursor !== void 0,
823
+ hasPending: hasPending(sessionId, clearIndex, consumers)
824
+ };
825
+ }));
826
+ },
827
+ async readState(sessionId, reducerName, stateOptions) {
828
+ return wrap(() => {
829
+ const throughIndex = stateOptions?.throughIndex ?? null;
830
+ const snapshotThroughIndex = stateOptions?.snapshotThroughIndex ?? throughIndex;
831
+ const rows = db.prepare(`with boundary as materialized (
832
+ select coalesce(
833
+ (select clear_index from a2_clears where session_id = ?), 0
834
+ ) as clear_index
835
+ ), head as materialized (
836
+ select up_to_index, state, updated_at
837
+ from a2_snapshots
838
+ where session_id = ? and reducer_name = ?
839
+ and up_to_index >= (select clear_index from boundary)
840
+ ), snapshot as materialized (
841
+ select up_to_index, state from (
842
+ select up_to_index, state from head
843
+ where (? is null or up_to_index <= ?)
844
+ union all
845
+ select up_to_index, state
846
+ from a2_snapshot_history
847
+ where session_id = ? and reducer_name = ?
848
+ and up_to_index >= (select clear_index from boundary)
849
+ and (? is null or up_to_index <= ?)
850
+ ) order by up_to_index desc limit 1
851
+ )
852
+ select 0 as row_order, 'snapshot' as row_kind,
853
+ (select clear_index from boundary) as clear_index,
854
+ (select up_to_index from head) as head_index,
855
+ (select up_to_index from snapshot) as snapshot_index,
856
+ (select state from snapshot) as snapshot_state,
857
+ null as session_id, null as idx, null as event_type,
858
+ null as payload, null as event_id, null as created_at,
859
+ null as cause
860
+ union all
861
+ select 1 as row_order, 'event' as row_kind,
862
+ null as clear_index,
863
+ (select up_to_index from head) as head_index,
864
+ null as snapshot_index, null as snapshot_state,
865
+ event.session_id, event.idx, event.event_type,
866
+ event.payload, event.event_id, event.created_at,
867
+ event.cause
868
+ from a2_events as event
869
+ where event.session_id = ?
870
+ and event.idx > max(
871
+ coalesce((select up_to_index from snapshot), 0),
872
+ (select clear_index from boundary) - 1
873
+ )
874
+ and (? is null or event.idx <= ?)
875
+ order by row_order, idx`).all(sessionId, sessionId, reducerName, snapshotThroughIndex, snapshotThroughIndex, sessionId, reducerName, snapshotThroughIndex, snapshotThroughIndex, sessionId, throughIndex, throughIndex);
876
+ const snapshot = rows.find((row) => row.row_kind === "snapshot");
877
+ const clearIndex = Number(snapshot?.clear_index ?? 0);
878
+ return {
879
+ ...clearIndex > 0 ? { clearIndex } : {},
880
+ headIndex: snapshot?.head_index === null || snapshot?.head_index === void 0 ? null : Number(snapshot.head_index),
881
+ snapshot: snapshot?.snapshot_index !== null && snapshot?.snapshot_index !== void 0 ? {
882
+ index: Number(snapshot.snapshot_index),
883
+ state: JSON.parse(snapshot.snapshot_state)
884
+ } : null,
885
+ events: rows.filter((row) => row.row_kind === "event").map(toEvent)
886
+ };
887
+ });
888
+ },
889
+ async putSnapshots(sessionId, reducerName, writes) {
890
+ await wrap(() => tx(() => {
891
+ const selectSnapshotHead = db.prepare(`select up_to_index, state, updated_at from a2_snapshots
892
+ where session_id = ? and reducer_name = ?`);
893
+ const unfinished = db.prepare(`select 1 from a2_cursors
894
+ where session_id = ? and consumer = ? and cursor_key = ?
895
+ and head = ? and head > ? and failed_at is null`);
896
+ const insertPin = db.prepare(`insert or ignore into a2_snapshot_pins
897
+ (session_id, consumer, cursor_key, event_index, reducer_name, up_to_index)
898
+ values (?, ?, ?, ?, ?, ?)`);
899
+ const hasPin = db.prepare(`select 1 from a2_snapshot_pins
900
+ where session_id = ? and reducer_name = ? and up_to_index = ?
901
+ limit 1`);
902
+ const putHistory = db.prepare(`insert into a2_snapshot_history
903
+ (session_id, reducer_name, up_to_index, state, updated_at)
904
+ values (?, ?, ?, ?, ?)
905
+ on conflict (session_id, reducer_name, up_to_index) do update set
906
+ state = excluded.state, updated_at = excluded.updated_at`);
907
+ const putHead = db.prepare(`insert into a2_snapshots
908
+ (session_id, reducer_name, up_to_index, state, updated_at)
909
+ values (?, ?, ?, ?, ?)
910
+ on conflict (session_id, reducer_name) do update set
911
+ up_to_index = excluded.up_to_index,
912
+ state = excluded.state,
913
+ updated_at = excluded.updated_at
914
+ where excluded.up_to_index >= a2_snapshots.up_to_index`);
915
+ const deleteHistory = db.prepare(`delete from a2_snapshot_history
916
+ where session_id = ? and reducer_name = ? and up_to_index = ?`);
917
+ const clearIndex = clearIndexOf(sessionId);
918
+ for (const write of writes.toSorted((a, b) => a.index - b.index)) {
919
+ if (write.index < clearIndex) continue;
920
+ for (const pin of write.pins ?? []) {
921
+ const cursorKey = cursorKeyOf(pin.lane, pin.index);
922
+ if (unfinished.get(sessionId, pin.consumer, cursorKey, pin.index, clearIndex)) insertPin.run(sessionId, pin.consumer, cursorKey, pin.index, reducerName, write.index);
923
+ }
924
+ const head = selectSnapshotHead.get(sessionId, reducerName);
925
+ const now = clock.now().getTime();
926
+ if (!head || write.index >= Number(head.up_to_index)) {
927
+ if (head && write.index > Number(head.up_to_index) && Number(head.up_to_index) >= clearIndex && hasPin.get(sessionId, reducerName, Number(head.up_to_index))) putHistory.run(sessionId, reducerName, Number(head.up_to_index), head.state, Number(head.updated_at));
928
+ putHead.run(sessionId, reducerName, write.index, JSON.stringify(write.state) ?? "null", now);
929
+ deleteHistory.run(sessionId, reducerName, write.index);
930
+ } else if (write.index < Number(head.up_to_index) && hasPin.get(sessionId, reducerName, write.index)) putHistory.run(sessionId, reducerName, write.index, JSON.stringify(write.state) ?? "null", now);
931
+ }
932
+ }));
933
+ },
934
+ presence: {
935
+ async set(ns, participant, values, meta) {
936
+ const entries = Object.entries(values);
937
+ if (entries.length === 0) return;
938
+ wrap(() => tx(() => {
939
+ const atMs = meta.at.getTime();
940
+ const nowMs = clock.now().getTime();
941
+ const expiresAtMs = nowMs + meta.ttlMs;
942
+ for (const [field, value] of entries) upsertPresence.run(ns, participant, field, JSON.stringify(value) ?? "null", meta.seen, atMs, expiresAtMs, nowMs);
943
+ }));
944
+ },
945
+ async read(ns) {
946
+ return wrap(() => {
947
+ const now = clock.now().getTime();
948
+ sweepPresence.run(ns, now);
949
+ return selectPresence.all(ns, now).map((row) => ({
950
+ participant: row.participant,
951
+ field: row.field,
952
+ value: JSON.parse(row.value),
953
+ seen: Number(row.seen),
954
+ at: new Date(Number(row.at)),
955
+ expiresAt: new Date(Number(row.expires_at))
956
+ }));
957
+ });
958
+ }
959
+ },
960
+ stream(sessionId, opts) {
961
+ const readAfter = (afterIndex) => wrap(() => {
962
+ return db.prepare(`select * from a2_events
963
+ where session_id = ? and idx > ?
964
+ and idx >= coalesce((select clear_index from a2_clears where session_id = ?), 0)
965
+ order by idx`).all(sessionId, afterIndex, sessionId).map(toStored);
966
+ });
967
+ return pollingStream(readAfter, opts?.startAfter !== void 0 ? { startAfter: opts.startAfter } : {});
968
+ },
969
+ async clear({ sessionId, ifLastIndex, cause }) {
970
+ return wrap(() => tx(() => {
971
+ const clearIndex = clearIndexOf(sessionId);
972
+ if (cause) assertCurrentCause({
973
+ sessionId,
974
+ cause,
975
+ clearIndex
976
+ });
977
+ const head = headOf(sessionId);
978
+ if (ifLastIndex !== void 0 && clearIndex === ifLastIndex + 1) return {
979
+ cleared: true,
980
+ index: head,
981
+ clearIndex
982
+ };
983
+ if (ifLastIndex !== void 0 && ifLastIndex !== head) return {
984
+ cleared: false,
985
+ index: head,
986
+ clearIndex
987
+ };
988
+ if (head === 0 || clearIndex === head) return {
989
+ cleared: true,
990
+ index: head,
991
+ clearIndex
992
+ };
993
+ const marker = head + 1;
994
+ insertEvent.run(sessionId, marker, CLEAR_EVENT_TYPE, "null", generateId(), clock.now().getTime(), null);
995
+ db.prepare(`insert into a2_clears (session_id, clear_index) values (?, ?)
996
+ on conflict (session_id) do update set clear_index = excluded.clear_index`).run(sessionId, marker);
997
+ db.prepare(`update a2_cursors
998
+ set position = ?, head = null, ${FRESH_DISPATCH},
999
+ completed_idx = null, completed_attempt = null,
1000
+ returned_event_ids = null
1001
+ where session_id = ? and kind = 'log'`).run(marker, sessionId);
1002
+ db.prepare("delete from a2_snapshot_pins where session_id = ? and event_index < ?").run(sessionId, marker);
1003
+ return {
1004
+ cleared: true,
1005
+ index: marker,
1006
+ clearIndex: marker
1007
+ };
1008
+ }));
1009
+ },
1010
+ async reclaim(sessionId) {
1011
+ return wrap(() => tx(() => {
1012
+ const clearIndex = clearIndexOf(sessionId);
1013
+ if (clearIndex === 0) return {
1014
+ deleted: 0,
1015
+ done: true
1016
+ };
1017
+ const last = db.prepare(`select max(idx) as last from (
1018
+ select idx from a2_events
1019
+ where session_id = ? and idx < ?
1020
+ order by idx limit ?
1021
+ )`).get(sessionId, clearIndex, RECLAIM_BATCH);
1022
+ let deleted = 0;
1023
+ if (last.last !== null) {
1024
+ const through = Number(last.last);
1025
+ deleted = Number(db.prepare("delete from a2_events where session_id = ? and idx <= ?").run(sessionId, through).changes);
1026
+ db.prepare("delete from a2_lane_events where session_id = ? and idx <= ?").run(sessionId, through);
1027
+ db.prepare(`delete from a2_cursors
1028
+ where session_id = ? and kind = 'event' and position < ?`).run(sessionId, through);
1029
+ }
1030
+ const done = db.prepare("select 1 from a2_events where session_id = ? and idx < ? limit 1").get(sessionId, clearIndex) === void 0;
1031
+ if (done) {
1032
+ db.prepare(`delete from a2_cursors
1033
+ where session_id = ? and kind != 'log'
1034
+ and (head is null or head <= ?) and position <= ?`).run(sessionId, clearIndex, clearIndex);
1035
+ for (const table of [
1036
+ "a2_snapshot_pins",
1037
+ "a2_snapshot_history",
1038
+ "a2_snapshots"
1039
+ ]) db.prepare(`delete from ${table} where session_id = ? and up_to_index < ?`).run(sessionId, clearIndex);
1040
+ }
1041
+ return {
1042
+ deleted,
1043
+ done
1044
+ };
1045
+ }));
1046
+ },
1047
+ close() {
1048
+ db.close();
1049
+ }
1050
+ };
1051
+ }
1052
+ //#endregion
1053
+ export { sqlite as t };