@vercel/board 0.16.0 → 0.17.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-Bck5x1Kh.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-C5I7Wfdw.mjs} +2 -2
- package/dist/{backend-http-BVRno-ca.mjs → backend-http-CuUIACCs.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-DOdA7KhF.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-n4qKN7EL.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-CNYvA4pT.mjs} +5 -5
- package/dist/server-DtFrWxi3-Boa6gdWO.mjs +3602 -0
- package/dist/server-qgez0Xny-D5mLfqFQ.d.mts +385 -0
- package/dist/{server-jwPY7_wi.mjs → server-snYFkITb.mjs} +4 -4
- 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-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-DojIuoFg.mjs} +233 -28
- package/docs/01-board/01-mounting.md +1 -1
- package/docs/02-sync/01-overview.md +69 -47
- package/docs/02-sync/02-functions.md +395 -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 +58 -26
- 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 +22 -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,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 };
|