@m4ike1/ion-session-backend-sqlite-node 0.1.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 (64) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +62 -0
  3. package/dist/index.d.ts +6 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +103 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/sqlite/index.d.ts +5 -0
  8. package/dist/sqlite/index.d.ts.map +1 -0
  9. package/dist/sqlite/index.js +4 -0
  10. package/dist/sqlite/index.js.map +1 -0
  11. package/dist/sqlite/migrations/001_initial.sql +121 -0
  12. package/dist/sqlite/migrations.d.ts +3 -0
  13. package/dist/sqlite/migrations.d.ts.map +1 -0
  14. package/dist/sqlite/migrations.js +7 -0
  15. package/dist/sqlite/migrations.js.map +1 -0
  16. package/dist/sqlite/repo.d.ts +41 -0
  17. package/dist/sqlite/repo.d.ts.map +1 -0
  18. package/dist/sqlite/repo.js +408 -0
  19. package/dist/sqlite/repo.js.map +1 -0
  20. package/dist/sqlite/session/branch-entries.d.ts +6 -0
  21. package/dist/sqlite/session/branch-entries.d.ts.map +1 -0
  22. package/dist/sqlite/session/branch-entries.js +213 -0
  23. package/dist/sqlite/session/branch-entries.js.map +1 -0
  24. package/dist/sqlite/session/entries.d.ts +24 -0
  25. package/dist/sqlite/session/entries.d.ts.map +1 -0
  26. package/dist/sqlite/session/entries.js +126 -0
  27. package/dist/sqlite/session/entries.js.map +1 -0
  28. package/dist/sqlite/session/session-row.d.ts +23 -0
  29. package/dist/sqlite/session/session-row.d.ts.map +1 -0
  30. package/dist/sqlite/session/session-row.js +69 -0
  31. package/dist/sqlite/session/session-row.js.map +1 -0
  32. package/dist/sqlite/session/session-sequences.d.ts +4 -0
  33. package/dist/sqlite/session/session-sequences.d.ts.map +1 -0
  34. package/dist/sqlite/session/session-sequences.js +13 -0
  35. package/dist/sqlite/session/session-sequences.js.map +1 -0
  36. package/dist/sqlite/session/session-stats.d.ts +6 -0
  37. package/dist/sqlite/session/session-stats.d.ts.map +1 -0
  38. package/dist/sqlite/session/session-stats.js +39 -0
  39. package/dist/sqlite/session/session-stats.js.map +1 -0
  40. package/dist/sqlite/session/usage-ledger.d.ts +20 -0
  41. package/dist/sqlite/session/usage-ledger.d.ts.map +1 -0
  42. package/dist/sqlite/session/usage-ledger.js +50 -0
  43. package/dist/sqlite/session/usage-ledger.js.map +1 -0
  44. package/dist/sqlite/session/values.d.ts +23 -0
  45. package/dist/sqlite/session/values.d.ts.map +1 -0
  46. package/dist/sqlite/session/values.js +91 -0
  47. package/dist/sqlite/session/values.js.map +1 -0
  48. package/dist/sqlite/session.d.ts +42 -0
  49. package/dist/sqlite/session.d.ts.map +1 -0
  50. package/dist/sqlite/session.js +163 -0
  51. package/dist/sqlite/session.js.map +1 -0
  52. package/dist/sqlite/sql.d.ts +19 -0
  53. package/dist/sqlite/sql.d.ts.map +1 -0
  54. package/dist/sqlite/sql.js +58 -0
  55. package/dist/sqlite/sql.js.map +1 -0
  56. package/dist/sqlite/storage.d.ts +38 -0
  57. package/dist/sqlite/storage.d.ts.map +1 -0
  58. package/dist/sqlite/storage.js +161 -0
  59. package/dist/sqlite/storage.js.map +1 -0
  60. package/dist/sqlite/types.d.ts +29 -0
  61. package/dist/sqlite/types.d.ts.map +1 -0
  62. package/dist/sqlite/types.js +2 -0
  63. package/dist/sqlite/types.js.map +1 -0
  64. package/package.json +45 -0
@@ -0,0 +1,408 @@
1
+ import { mkdir, open as openFile, readdir, realpath, rm } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { branchTip, createForkSnapshot, StorageBackedSession } from "@m4ike1/ion-agent-core";
4
+ import { uuidv7 } from "@m4ike1/ion-ai";
5
+ import { applyInitialSchema } from "./migrations.js";
6
+ import { appendEntryToBranchIndex, scanBranchEntries } from "./session/branch-entries.js";
7
+ import { decodeEntryRow, EntryRowWriter } from "./session/entries.js";
8
+ import { deleteSessionRows, hasSessionRow, insertSessionRow, metadataFromSessionRow, readAllSessionRows, readSessionRow, } from "./session/session-row.js";
9
+ import { readAllScalarValueRows, setScalarValueRow } from "./session/values.js";
10
+ import { SqliteOpenSession } from "./session.js";
11
+ import { sql } from "./sql.js";
12
+ import { SqliteStorage } from "./storage.js";
13
+ export const SQLITE_STORAGE_VERSION = 1;
14
+ export const SQLITE_SESSION_EXTENSION = ".sqlite";
15
+ const FIRST_AVAILABLE_COMMIT_SEQ = 1;
16
+ const SAFE_SESSION_FILE_ID = /^[A-Za-z0-9_-]+$/;
17
+ function sessionFileName(id) {
18
+ if (SAFE_SESSION_FILE_ID.test(id))
19
+ return `${id}${SQLITE_SESSION_EXTENSION}`;
20
+ const encoded = Buffer.from(id, "utf16le").toString("base64url");
21
+ return `~${encoded}${SQLITE_SESSION_EXTENSION}`;
22
+ }
23
+ function sessionPath(directory, id) {
24
+ return join(directory, sessionFileName(id));
25
+ }
26
+ function storageIdentity(path, sessionId) {
27
+ return JSON.stringify([path, sessionId]);
28
+ }
29
+ function isErrorWithCode(error, code) {
30
+ return typeof error === "object" && error !== null && "code" in error && error.code === code;
31
+ }
32
+ async function removeSessionFiles(path, options) {
33
+ await rm(path, { force: options.force });
34
+ await rm(`${path}-wal`, { force: true });
35
+ await rm(`${path}-shm`, { force: true });
36
+ }
37
+ function configureWritableConnection(db) {
38
+ db.exec("PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 5000;");
39
+ }
40
+ function configureReadOnlyConnection(db) {
41
+ db.exec("PRAGMA busy_timeout = 5000;");
42
+ }
43
+ function readSourceEntries(db, sessionId) {
44
+ return sql `SELECT id, parent_id, seq, type, custom_type, timestamp, payload
45
+ FROM entries WHERE session_id = ${sessionId} ORDER BY seq ASC`
46
+ .all(db)
47
+ .map(decodeEntryRow);
48
+ }
49
+ function buildForkSnapshot(source, options) {
50
+ const snapshot = createForkSnapshot({
51
+ entries: source.entries,
52
+ scalarValues: source.scalarValues,
53
+ entriesComplete: source.entriesComplete,
54
+ }, options);
55
+ const entries = [...snapshot.entries.values()].sort((left, right) => left.seq - right.seq);
56
+ return {
57
+ entries,
58
+ scalarValues: snapshot.scalarValues,
59
+ messageCount: entries.filter((entry) => entry.type === "message").length,
60
+ nextSeq: snapshot.nextSeq,
61
+ };
62
+ }
63
+ // TODO(WP08): Remove this snapshot path when SQLite forks use streaming staging.
64
+ function readForkSourceEntries(db, sessionId, scalarValues, options) {
65
+ if (options.scope === "tree")
66
+ return readSourceEntries(db, sessionId);
67
+ const sourceAddress = branchTip(options.branch);
68
+ const sourceTip = scalarValues.find((stored) => stored.address.namespace === sourceAddress.namespace && stored.address.key === sourceAddress.key);
69
+ if (sourceTip === undefined)
70
+ throw new Error(`Unknown source branch: ${options.branch}`);
71
+ return sourceTip.value === null
72
+ ? []
73
+ : scanBranchEntries(db, sessionId, { start: sourceTip.value, order: "oldestFirst" });
74
+ }
75
+ function createSqliteForkSnapshot(sourceDb, source, options) {
76
+ sourceDb.exec("BEGIN");
77
+ let committed = false;
78
+ try {
79
+ metadataFromSessionRow(source.path, readSessionRow(sourceDb, source.id), SQLITE_STORAGE_VERSION);
80
+ const scalarValues = readAllScalarValueRows(sourceDb, source.id);
81
+ const snapshot = buildForkSnapshot({
82
+ entries: readForkSourceEntries(sourceDb, source.id, scalarValues, options),
83
+ scalarValues,
84
+ entriesComplete: options.scope === "tree",
85
+ }, options);
86
+ sourceDb.exec("COMMIT");
87
+ committed = true;
88
+ return snapshot;
89
+ }
90
+ catch (error) {
91
+ if (!committed)
92
+ sourceDb.exec("ROLLBACK");
93
+ throw error;
94
+ }
95
+ }
96
+ function insertForkValue(db, sessionId, stored) {
97
+ setScalarValueRow(db, sessionId, stored.address.namespace, stored.address.key, stored.seq, stored.value);
98
+ }
99
+ function updateForkSessionStats(db, sessionId, messageCount) {
100
+ sql `UPDATE sessions SET message_count = ${messageCount} WHERE id = ${sessionId}`.run(db);
101
+ }
102
+ export class SqliteSessionRepo {
103
+ directory;
104
+ databasePath;
105
+ databaseFactory;
106
+ now;
107
+ pendingIds = new Set();
108
+ openStorages = new Map();
109
+ openSessions = new Set();
110
+ closed = false;
111
+ closePromise;
112
+ constructor(options) {
113
+ this.directory = options.directory;
114
+ this.databasePath = options.databasePath;
115
+ this.databaseFactory = options.databaseFactory;
116
+ this.now = options.now ?? Date.now;
117
+ }
118
+ async create(options, _context) {
119
+ this.assertOpen();
120
+ options ??= {};
121
+ const createdAt = this.now();
122
+ const id = options.id ?? uuidv7(createdAt);
123
+ this.reserveId(id);
124
+ const path = this.pathForSession(id);
125
+ let db;
126
+ let reservedFile = false;
127
+ let initialized = false;
128
+ let session;
129
+ try {
130
+ await mkdir(dirname(path), { recursive: true });
131
+ if (!this.usesSharedDatabase()) {
132
+ const file = await openFile(path, "wx");
133
+ await file.close();
134
+ reservedFile = true;
135
+ }
136
+ const activeDb = await this.databaseFactory.open(path);
137
+ db = activeDb;
138
+ configureWritableConnection(activeDb);
139
+ await applyInitialSchema(activeDb);
140
+ const canonicalPath = await realpath(path);
141
+ const metadata = {
142
+ id,
143
+ createdAt,
144
+ storageVersion: SQLITE_STORAGE_VERSION,
145
+ ...(options.parentSessionId === undefined ? {} : { parentSessionId: options.parentSessionId }),
146
+ path: canonicalPath,
147
+ };
148
+ activeDb.transaction(() => {
149
+ if (hasSessionRow(activeDb, id))
150
+ throw new Error(`SQLite session already exists: ${id}`);
151
+ insertSessionRow(activeDb, metadata, SQLITE_STORAGE_VERSION, FIRST_AVAILABLE_COMMIT_SEQ);
152
+ });
153
+ initialized = true;
154
+ session = this.openStorageBackedSession(metadata, activeDb);
155
+ return session;
156
+ }
157
+ catch (error) {
158
+ if (reservedFile && !initialized)
159
+ await removeSessionFiles(path, { force: true });
160
+ throw error;
161
+ }
162
+ finally {
163
+ if (session === undefined) {
164
+ try {
165
+ db?.close();
166
+ }
167
+ finally {
168
+ this.pendingIds.delete(id);
169
+ }
170
+ }
171
+ }
172
+ }
173
+ async open(metadata, _context) {
174
+ this.assertOpen();
175
+ this.reserveId(metadata.id);
176
+ let db;
177
+ let session;
178
+ try {
179
+ const path = await this.repositoryPathForMetadata(metadata);
180
+ const activeDb = await this.databaseFactory.openExisting(path);
181
+ db = activeDb;
182
+ configureWritableConnection(activeDb);
183
+ const stored = metadataFromSessionRow(path, readSessionRow(activeDb, metadata.id), SQLITE_STORAGE_VERSION);
184
+ session = this.openStorageBackedSession(stored, activeDb);
185
+ return session;
186
+ }
187
+ finally {
188
+ if (session === undefined) {
189
+ try {
190
+ db?.close();
191
+ }
192
+ finally {
193
+ this.pendingIds.delete(metadata.id);
194
+ }
195
+ }
196
+ }
197
+ }
198
+ async list(_options, _context) {
199
+ this.assertOpen();
200
+ let paths;
201
+ if (this.usesSharedDatabase()) {
202
+ paths = [this.databasePath];
203
+ }
204
+ else {
205
+ let names;
206
+ try {
207
+ names = await readdir(this.directory);
208
+ }
209
+ catch (error) {
210
+ if (isErrorWithCode(error, "ENOENT"))
211
+ return [];
212
+ throw error;
213
+ }
214
+ paths = names
215
+ .filter((name) => name.endsWith(SQLITE_SESSION_EXTENSION))
216
+ .map((name) => join(this.directory, name));
217
+ }
218
+ const sessions = [];
219
+ for (const path of paths) {
220
+ let db;
221
+ try {
222
+ const canonicalPath = await realpath(path);
223
+ db = await this.databaseFactory.openReadOnly(canonicalPath);
224
+ configureReadOnlyConnection(db);
225
+ for (const row of readAllSessionRows(db)) {
226
+ sessions.push(metadataFromSessionRow(canonicalPath, row, SQLITE_STORAGE_VERSION));
227
+ }
228
+ }
229
+ catch {
230
+ // Discovery is best-effort: corrupt files, incompatible versions, and
231
+ // unrelated *.sqlite files are reported when explicitly opened.
232
+ }
233
+ finally {
234
+ db?.close();
235
+ }
236
+ }
237
+ return sessions.sort((left, right) => right.createdAt - left.createdAt);
238
+ }
239
+ async delete(metadata, _context) {
240
+ this.assertOpen();
241
+ this.reserveId(metadata.id);
242
+ try {
243
+ const path = await this.repositoryPathForMetadata(metadata);
244
+ const db = await this.databaseFactory.openExisting(path);
245
+ try {
246
+ configureWritableConnection(db);
247
+ if (this.usesSharedDatabase()) {
248
+ db.transaction(() => {
249
+ metadataFromSessionRow(path, readSessionRow(db, metadata.id), SQLITE_STORAGE_VERSION);
250
+ deleteSessionRows(db, metadata.id);
251
+ });
252
+ }
253
+ else {
254
+ metadataFromSessionRow(path, readSessionRow(db, metadata.id), SQLITE_STORAGE_VERSION);
255
+ }
256
+ }
257
+ finally {
258
+ db.close();
259
+ }
260
+ if (!this.usesSharedDatabase())
261
+ await removeSessionFiles(path, { force: false });
262
+ }
263
+ finally {
264
+ this.pendingIds.delete(metadata.id);
265
+ }
266
+ }
267
+ async fork(source, options, context) {
268
+ this.assertOpen();
269
+ const createdAt = this.now();
270
+ const id = options.id ?? uuidv7(createdAt);
271
+ this.reserveId(id);
272
+ const sourceStorage = this.openStorages.get(storageIdentity(source.path, source.id));
273
+ const activeSourceSnapshot = sourceStorage?.snapshot(options, context);
274
+ void activeSourceSnapshot?.catch(() => undefined);
275
+ const path = this.pathForSession(id);
276
+ let db;
277
+ let reservedFile = false;
278
+ let initialized = false;
279
+ let session;
280
+ try {
281
+ await mkdir(dirname(path), { recursive: true });
282
+ if (!this.usesSharedDatabase()) {
283
+ const file = await openFile(path, "wx");
284
+ await file.close();
285
+ reservedFile = true;
286
+ }
287
+ const snapshot = activeSourceSnapshot === undefined
288
+ ? await this.createForkSnapshotFromExternalSource(source, options)
289
+ : buildForkSnapshot(await activeSourceSnapshot, options);
290
+ const activeDb = await this.databaseFactory.open(path);
291
+ db = activeDb;
292
+ configureWritableConnection(activeDb);
293
+ await applyInitialSchema(activeDb);
294
+ const canonicalPath = await realpath(path);
295
+ const metadata = {
296
+ id,
297
+ createdAt,
298
+ storageVersion: SQLITE_STORAGE_VERSION,
299
+ parentSessionId: source.id,
300
+ path: canonicalPath,
301
+ };
302
+ activeDb.transaction(() => {
303
+ if (hasSessionRow(activeDb, id))
304
+ throw new Error(`SQLite session already exists: ${id}`);
305
+ insertSessionRow(activeDb, metadata, SQLITE_STORAGE_VERSION, snapshot.nextSeq);
306
+ const entryWriter = new EntryRowWriter(activeDb, id);
307
+ for (const entry of snapshot.entries) {
308
+ entryWriter.insert(entry);
309
+ appendEntryToBranchIndex(activeDb, id, entry);
310
+ }
311
+ for (const stored of snapshot.scalarValues)
312
+ insertForkValue(activeDb, id, stored);
313
+ updateForkSessionStats(activeDb, id, snapshot.messageCount);
314
+ });
315
+ initialized = true;
316
+ session = this.openStorageBackedSession(metadata, activeDb);
317
+ return session;
318
+ }
319
+ catch (error) {
320
+ if (reservedFile && !initialized)
321
+ await removeSessionFiles(path, { force: true });
322
+ throw error;
323
+ }
324
+ finally {
325
+ if (session === undefined) {
326
+ try {
327
+ db?.close();
328
+ }
329
+ finally {
330
+ this.pendingIds.delete(id);
331
+ }
332
+ }
333
+ }
334
+ }
335
+ close(context) {
336
+ if (this.closePromise !== undefined)
337
+ return this.closePromise;
338
+ this.closed = true;
339
+ this.closePromise = this.closeOpenSessions(context);
340
+ return this.closePromise;
341
+ }
342
+ async createForkSnapshotFromExternalSource(source, options) {
343
+ const path = await realpath(source.path);
344
+ const sourceDb = await this.databaseFactory.openReadOnly(path);
345
+ try {
346
+ configureReadOnlyConnection(sourceDb);
347
+ return createSqliteForkSnapshot(sourceDb, { ...source, path }, options);
348
+ }
349
+ finally {
350
+ sourceDb.close();
351
+ }
352
+ }
353
+ async closeOpenSessions(context) {
354
+ const results = await Promise.allSettled([...this.openSessions].map((session) => session.close(context)));
355
+ const errors = results.flatMap((result) => (result.status === "rejected" ? [result.reason] : []));
356
+ if (errors.length === 1)
357
+ throw errors[0];
358
+ if (errors.length > 1)
359
+ throw new AggregateError(errors, "Failed to close SQLite Sessions");
360
+ }
361
+ openStorageBackedSession(metadata, db) {
362
+ const key = storageIdentity(metadata.path, metadata.id);
363
+ const storage = new SqliteStorage(db, { sessionId: metadata.id, now: this.now });
364
+ this.openStorages.set(key, storage);
365
+ const session = new StorageBackedSession(metadata, storage);
366
+ const openSession = new SqliteOpenSession(session, {
367
+ onClose: () => {
368
+ try {
369
+ db.close();
370
+ }
371
+ finally {
372
+ if (this.openStorages.get(key) === storage)
373
+ this.openStorages.delete(key);
374
+ this.openSessions.delete(openSession);
375
+ this.pendingIds.delete(metadata.id);
376
+ }
377
+ },
378
+ });
379
+ this.openSessions.add(openSession);
380
+ return openSession;
381
+ }
382
+ async repositoryPathForMetadata(metadata) {
383
+ const [expected, actual] = await Promise.all([
384
+ realpath(this.pathForSession(metadata.id)),
385
+ realpath(metadata.path),
386
+ ]);
387
+ if (expected !== actual) {
388
+ throw new Error(`SQLite session metadata path is outside this repository: ${metadata.path}`);
389
+ }
390
+ return actual;
391
+ }
392
+ reserveId(id) {
393
+ if (this.pendingIds.has(id))
394
+ throw new Error(`Session is already open: ${id}`);
395
+ this.pendingIds.add(id);
396
+ }
397
+ pathForSession(id) {
398
+ return this.databasePath ?? sessionPath(this.directory, id);
399
+ }
400
+ usesSharedDatabase() {
401
+ return this.databasePath !== undefined;
402
+ }
403
+ assertOpen() {
404
+ if (this.closed)
405
+ throw new Error("SqliteSessionRepo is closed");
406
+ }
407
+ }
408
+ //# sourceMappingURL=repo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repo.js","sourceRoot":"","sources":["../../src/sqlite/repo.ts"],"names":[],"mappings":"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","sourcesContent":["import { mkdir, open as openFile, readdir, realpath, rm } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport type { Context, Entry, ForkOptions, SessionCreateOptions, StoredValue } from \"@m4ike1/ion-agent-core\";\nimport { branchTip, createForkSnapshot, StorageBackedSession } from \"@m4ike1/ion-agent-core\";\nimport { uuidv7 } from \"@m4ike1/ion-ai\";\nimport { applyInitialSchema } from \"./migrations.ts\";\nimport { appendEntryToBranchIndex, scanBranchEntries } from \"./session/branch-entries.ts\";\nimport { decodeEntryRow, type EntryRow, EntryRowWriter } from \"./session/entries.ts\";\nimport {\n\tdeleteSessionRows,\n\thasSessionRow,\n\tinsertSessionRow,\n\tmetadataFromSessionRow,\n\treadAllSessionRows,\n\treadSessionRow,\n\ttype SqliteSessionMetadata,\n} from \"./session/session-row.ts\";\nimport { readAllScalarValueRows, setScalarValueRow } from \"./session/values.ts\";\nimport { SqliteOpenSession } from \"./session.ts\";\nimport { sql } from \"./sql.ts\";\nimport { SqliteStorage, type SqliteStorageSnapshot } from \"./storage.ts\";\nimport type { SqliteDatabase, SqliteDatabaseFactory } from \"./types.ts\";\n\nexport const SQLITE_STORAGE_VERSION = 1;\nexport const SQLITE_SESSION_EXTENSION = \".sqlite\";\n\nconst FIRST_AVAILABLE_COMMIT_SEQ = 1;\nconst SAFE_SESSION_FILE_ID = /^[A-Za-z0-9_-]+$/;\n\nexport type SqliteSessionCreateOptions = SessionCreateOptions;\n\nexport interface SqliteSessionRepoOptions {\n\tdirectory: string;\n\t/** Optional single container path. Defaults to one encoded `${id}.sqlite` file per session under directory. */\n\tdatabasePath?: string;\n\tdatabaseFactory: SqliteDatabaseFactory;\n\tnow?: () => number;\n}\n\nfunction sessionFileName(id: string): string {\n\tif (SAFE_SESSION_FILE_ID.test(id)) return `${id}${SQLITE_SESSION_EXTENSION}`;\n\tconst encoded = Buffer.from(id, \"utf16le\").toString(\"base64url\");\n\treturn `~${encoded}${SQLITE_SESSION_EXTENSION}`;\n}\n\nfunction sessionPath(directory: string, id: string): string {\n\treturn join(directory, sessionFileName(id));\n}\n\nfunction storageIdentity(path: string, sessionId: string): string {\n\treturn JSON.stringify([path, sessionId]);\n}\n\nfunction isErrorWithCode(error: unknown, code: string): boolean {\n\treturn typeof error === \"object\" && error !== null && \"code\" in error && error.code === code;\n}\n\nasync function removeSessionFiles(path: string, options: { force: boolean }): Promise<void> {\n\tawait rm(path, { force: options.force });\n\tawait rm(`${path}-wal`, { force: true });\n\tawait rm(`${path}-shm`, { force: true });\n}\n\nfunction configureWritableConnection(db: SqliteDatabase): void {\n\tdb.exec(\"PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 5000;\");\n}\n\nfunction configureReadOnlyConnection(db: SqliteDatabase): void {\n\tdb.exec(\"PRAGMA busy_timeout = 5000;\");\n}\n\ninterface ForkSnapshot {\n\tentries: Entry[];\n\tscalarValues: StoredValue<unknown>[];\n\tmessageCount: number;\n\tnextSeq: number;\n}\n\nfunction readSourceEntries(db: SqliteDatabase, sessionId: string): Entry[] {\n\treturn sql`SELECT id, parent_id, seq, type, custom_type, timestamp, payload\n\t\tFROM entries WHERE session_id = ${sessionId} ORDER BY seq ASC`\n\t\t.all<EntryRow>(db)\n\t\t.map(decodeEntryRow);\n}\n\nfunction buildForkSnapshot(source: SqliteStorageSnapshot, options: ForkOptions): ForkSnapshot {\n\tconst snapshot = createForkSnapshot(\n\t\t{\n\t\t\tentries: source.entries,\n\t\t\tscalarValues: source.scalarValues,\n\t\t\tentriesComplete: source.entriesComplete,\n\t\t},\n\t\toptions,\n\t);\n\tconst entries = [...snapshot.entries.values()].sort((left, right) => left.seq - right.seq);\n\treturn {\n\t\tentries,\n\t\tscalarValues: snapshot.scalarValues,\n\t\tmessageCount: entries.filter((entry) => entry.type === \"message\").length,\n\t\tnextSeq: snapshot.nextSeq,\n\t};\n}\n\n// TODO(WP08): Remove this snapshot path when SQLite forks use streaming staging.\nfunction readForkSourceEntries(\n\tdb: SqliteDatabase,\n\tsessionId: string,\n\tscalarValues: readonly StoredValue<unknown>[],\n\toptions: ForkOptions,\n): Entry[] {\n\tif (options.scope === \"tree\") return readSourceEntries(db, sessionId);\n\tconst sourceAddress = branchTip(options.branch);\n\tconst sourceTip = scalarValues.find(\n\t\t(stored) => stored.address.namespace === sourceAddress.namespace && stored.address.key === sourceAddress.key,\n\t) as StoredValue<string | null> | undefined;\n\tif (sourceTip === undefined) throw new Error(`Unknown source branch: ${options.branch}`);\n\treturn sourceTip.value === null\n\t\t? []\n\t\t: scanBranchEntries(db, sessionId, { start: sourceTip.value, order: \"oldestFirst\" });\n}\n\nfunction createSqliteForkSnapshot(\n\tsourceDb: SqliteDatabase,\n\tsource: SqliteSessionMetadata,\n\toptions: ForkOptions,\n): ForkSnapshot {\n\tsourceDb.exec(\"BEGIN\");\n\tlet committed = false;\n\ttry {\n\t\tmetadataFromSessionRow(source.path, readSessionRow(sourceDb, source.id), SQLITE_STORAGE_VERSION);\n\t\tconst scalarValues = readAllScalarValueRows(sourceDb, source.id);\n\t\tconst snapshot = buildForkSnapshot(\n\t\t\t{\n\t\t\t\tentries: readForkSourceEntries(sourceDb, source.id, scalarValues, options),\n\t\t\t\tscalarValues,\n\t\t\t\tentriesComplete: options.scope === \"tree\",\n\t\t\t},\n\t\t\toptions,\n\t\t);\n\t\tsourceDb.exec(\"COMMIT\");\n\t\tcommitted = true;\n\t\treturn snapshot;\n\t} catch (error) {\n\t\tif (!committed) sourceDb.exec(\"ROLLBACK\");\n\t\tthrow error;\n\t}\n}\n\nfunction insertForkValue(db: SqliteDatabase, sessionId: string, stored: StoredValue<unknown>): void {\n\tsetScalarValueRow(db, sessionId, stored.address.namespace, stored.address.key, stored.seq, stored.value);\n}\n\nfunction updateForkSessionStats(db: SqliteDatabase, sessionId: string, messageCount: number): void {\n\tsql`UPDATE sessions SET message_count = ${messageCount} WHERE id = ${sessionId}`.run(db);\n}\n\nexport class SqliteSessionRepo {\n\tprivate readonly directory: string;\n\tprivate readonly databasePath: string | undefined;\n\tprivate readonly databaseFactory: SqliteDatabaseFactory;\n\tprivate readonly now: () => number;\n\tprivate readonly pendingIds = new Set<string>();\n\tprivate readonly openStorages = new Map<string, SqliteStorage>();\n\tprivate readonly openSessions = new Set<SqliteOpenSession>();\n\tprivate closed = false;\n\tprivate closePromise: Promise<void> | undefined;\n\n\tconstructor(options: SqliteSessionRepoOptions) {\n\t\tthis.directory = options.directory;\n\t\tthis.databasePath = options.databasePath;\n\t\tthis.databaseFactory = options.databaseFactory;\n\t\tthis.now = options.now ?? Date.now;\n\t}\n\n\tasync create(options: SqliteSessionCreateOptions | undefined, _context: Context): Promise<SqliteOpenSession> {\n\t\tthis.assertOpen();\n\t\toptions ??= {};\n\t\tconst createdAt = this.now();\n\t\tconst id = options.id ?? uuidv7(createdAt);\n\t\tthis.reserveId(id);\n\t\tconst path = this.pathForSession(id);\n\t\tlet db: SqliteDatabase | undefined;\n\t\tlet reservedFile = false;\n\t\tlet initialized = false;\n\t\tlet session: SqliteOpenSession | undefined;\n\t\ttry {\n\t\t\tawait mkdir(dirname(path), { recursive: true });\n\t\t\tif (!this.usesSharedDatabase()) {\n\t\t\t\tconst file = await openFile(path, \"wx\");\n\t\t\t\tawait file.close();\n\t\t\t\treservedFile = true;\n\t\t\t}\n\t\t\tconst activeDb = await this.databaseFactory.open(path);\n\t\t\tdb = activeDb;\n\t\t\tconfigureWritableConnection(activeDb);\n\t\t\tawait applyInitialSchema(activeDb);\n\t\t\tconst canonicalPath = await realpath(path);\n\t\t\tconst metadata: SqliteSessionMetadata = {\n\t\t\t\tid,\n\t\t\t\tcreatedAt,\n\t\t\t\tstorageVersion: SQLITE_STORAGE_VERSION,\n\t\t\t\t...(options.parentSessionId === undefined ? {} : { parentSessionId: options.parentSessionId }),\n\t\t\t\tpath: canonicalPath,\n\t\t\t};\n\t\t\tactiveDb.transaction(() => {\n\t\t\t\tif (hasSessionRow(activeDb, id)) throw new Error(`SQLite session already exists: ${id}`);\n\t\t\t\tinsertSessionRow(activeDb, metadata, SQLITE_STORAGE_VERSION, FIRST_AVAILABLE_COMMIT_SEQ);\n\t\t\t});\n\t\t\tinitialized = true;\n\t\t\tsession = this.openStorageBackedSession(metadata, activeDb);\n\t\t\treturn session;\n\t\t} catch (error) {\n\t\t\tif (reservedFile && !initialized) await removeSessionFiles(path, { force: true });\n\t\t\tthrow error;\n\t\t} finally {\n\t\t\tif (session === undefined) {\n\t\t\t\ttry {\n\t\t\t\t\tdb?.close();\n\t\t\t\t} finally {\n\t\t\t\t\tthis.pendingIds.delete(id);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tasync open(metadata: SqliteSessionMetadata, _context: Context): Promise<SqliteOpenSession> {\n\t\tthis.assertOpen();\n\t\tthis.reserveId(metadata.id);\n\t\tlet db: SqliteDatabase | undefined;\n\t\tlet session: SqliteOpenSession | undefined;\n\t\ttry {\n\t\t\tconst path = await this.repositoryPathForMetadata(metadata);\n\t\t\tconst activeDb = await this.databaseFactory.openExisting(path);\n\t\t\tdb = activeDb;\n\t\t\tconfigureWritableConnection(activeDb);\n\t\t\tconst stored = metadataFromSessionRow(path, readSessionRow(activeDb, metadata.id), SQLITE_STORAGE_VERSION);\n\t\t\tsession = this.openStorageBackedSession(stored, activeDb);\n\t\t\treturn session;\n\t\t} finally {\n\t\t\tif (session === undefined) {\n\t\t\t\ttry {\n\t\t\t\t\tdb?.close();\n\t\t\t\t} finally {\n\t\t\t\t\tthis.pendingIds.delete(metadata.id);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tasync list(_options: undefined, _context: Context): Promise<SqliteSessionMetadata[]> {\n\t\tthis.assertOpen();\n\t\tlet paths: string[];\n\t\tif (this.usesSharedDatabase()) {\n\t\t\tpaths = [this.databasePath!];\n\t\t} else {\n\t\t\tlet names: string[];\n\t\t\ttry {\n\t\t\t\tnames = await readdir(this.directory);\n\t\t\t} catch (error) {\n\t\t\t\tif (isErrorWithCode(error, \"ENOENT\")) return [];\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tpaths = names\n\t\t\t\t.filter((name) => name.endsWith(SQLITE_SESSION_EXTENSION))\n\t\t\t\t.map((name) => join(this.directory, name));\n\t\t}\n\t\tconst sessions: SqliteSessionMetadata[] = [];\n\t\tfor (const path of paths) {\n\t\t\tlet db: SqliteDatabase | undefined;\n\t\t\ttry {\n\t\t\t\tconst canonicalPath = await realpath(path);\n\t\t\t\tdb = await this.databaseFactory.openReadOnly(canonicalPath);\n\t\t\t\tconfigureReadOnlyConnection(db);\n\t\t\t\tfor (const row of readAllSessionRows(db)) {\n\t\t\t\t\tsessions.push(metadataFromSessionRow(canonicalPath, row, SQLITE_STORAGE_VERSION));\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Discovery is best-effort: corrupt files, incompatible versions, and\n\t\t\t\t// unrelated *.sqlite files are reported when explicitly opened.\n\t\t\t} finally {\n\t\t\t\tdb?.close();\n\t\t\t}\n\t\t}\n\t\treturn sessions.sort((left, right) => right.createdAt - left.createdAt);\n\t}\n\n\tasync delete(metadata: SqliteSessionMetadata, _context: Context): Promise<void> {\n\t\tthis.assertOpen();\n\t\tthis.reserveId(metadata.id);\n\t\ttry {\n\t\t\tconst path = await this.repositoryPathForMetadata(metadata);\n\t\t\tconst db = await this.databaseFactory.openExisting(path);\n\t\t\ttry {\n\t\t\t\tconfigureWritableConnection(db);\n\t\t\t\tif (this.usesSharedDatabase()) {\n\t\t\t\t\tdb.transaction(() => {\n\t\t\t\t\t\tmetadataFromSessionRow(path, readSessionRow(db, metadata.id), SQLITE_STORAGE_VERSION);\n\t\t\t\t\t\tdeleteSessionRows(db, metadata.id);\n\t\t\t\t\t});\n\t\t\t\t} else {\n\t\t\t\t\tmetadataFromSessionRow(path, readSessionRow(db, metadata.id), SQLITE_STORAGE_VERSION);\n\t\t\t\t}\n\t\t\t} finally {\n\t\t\t\tdb.close();\n\t\t\t}\n\t\t\tif (!this.usesSharedDatabase()) await removeSessionFiles(path, { force: false });\n\t\t} finally {\n\t\t\tthis.pendingIds.delete(metadata.id);\n\t\t}\n\t}\n\n\tasync fork(source: SqliteSessionMetadata, options: ForkOptions, context: Context): Promise<SqliteOpenSession> {\n\t\tthis.assertOpen();\n\t\tconst createdAt = this.now();\n\t\tconst id = options.id ?? uuidv7(createdAt);\n\t\tthis.reserveId(id);\n\t\tconst sourceStorage = this.openStorages.get(storageIdentity(source.path, source.id));\n\t\tconst activeSourceSnapshot = sourceStorage?.snapshot(options, context);\n\t\tvoid activeSourceSnapshot?.catch(() => undefined);\n\t\tconst path = this.pathForSession(id);\n\t\tlet db: SqliteDatabase | undefined;\n\t\tlet reservedFile = false;\n\t\tlet initialized = false;\n\t\tlet session: SqliteOpenSession | undefined;\n\t\ttry {\n\t\t\tawait mkdir(dirname(path), { recursive: true });\n\t\t\tif (!this.usesSharedDatabase()) {\n\t\t\t\tconst file = await openFile(path, \"wx\");\n\t\t\t\tawait file.close();\n\t\t\t\treservedFile = true;\n\t\t\t}\n\n\t\t\tconst snapshot =\n\t\t\t\tactiveSourceSnapshot === undefined\n\t\t\t\t\t? await this.createForkSnapshotFromExternalSource(source, options)\n\t\t\t\t\t: buildForkSnapshot(await activeSourceSnapshot, options);\n\n\t\t\tconst activeDb = await this.databaseFactory.open(path);\n\t\t\tdb = activeDb;\n\t\t\tconfigureWritableConnection(activeDb);\n\t\t\tawait applyInitialSchema(activeDb);\n\t\t\tconst canonicalPath = await realpath(path);\n\t\t\tconst metadata: SqliteSessionMetadata = {\n\t\t\t\tid,\n\t\t\t\tcreatedAt,\n\t\t\t\tstorageVersion: SQLITE_STORAGE_VERSION,\n\t\t\t\tparentSessionId: source.id,\n\t\t\t\tpath: canonicalPath,\n\t\t\t};\n\t\t\tactiveDb.transaction(() => {\n\t\t\t\tif (hasSessionRow(activeDb, id)) throw new Error(`SQLite session already exists: ${id}`);\n\t\t\t\tinsertSessionRow(activeDb, metadata, SQLITE_STORAGE_VERSION, snapshot.nextSeq);\n\t\t\t\tconst entryWriter = new EntryRowWriter(activeDb, id);\n\t\t\t\tfor (const entry of snapshot.entries) {\n\t\t\t\t\tentryWriter.insert(entry);\n\t\t\t\t\tappendEntryToBranchIndex(activeDb, id, entry);\n\t\t\t\t}\n\t\t\t\tfor (const stored of snapshot.scalarValues) insertForkValue(activeDb, id, stored);\n\t\t\t\tupdateForkSessionStats(activeDb, id, snapshot.messageCount);\n\t\t\t});\n\t\t\tinitialized = true;\n\t\t\tsession = this.openStorageBackedSession(metadata, activeDb);\n\t\t\treturn session;\n\t\t} catch (error) {\n\t\t\tif (reservedFile && !initialized) await removeSessionFiles(path, { force: true });\n\t\t\tthrow error;\n\t\t} finally {\n\t\t\tif (session === undefined) {\n\t\t\t\ttry {\n\t\t\t\t\tdb?.close();\n\t\t\t\t} finally {\n\t\t\t\t\tthis.pendingIds.delete(id);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tclose(context: Context): Promise<void> {\n\t\tif (this.closePromise !== undefined) return this.closePromise;\n\t\tthis.closed = true;\n\t\tthis.closePromise = this.closeOpenSessions(context);\n\t\treturn this.closePromise;\n\t}\n\n\tprivate async createForkSnapshotFromExternalSource(\n\t\tsource: SqliteSessionMetadata,\n\t\toptions: ForkOptions,\n\t): Promise<ForkSnapshot> {\n\t\tconst path = await realpath(source.path);\n\t\tconst sourceDb = await this.databaseFactory.openReadOnly(path);\n\t\ttry {\n\t\t\tconfigureReadOnlyConnection(sourceDb);\n\t\t\treturn createSqliteForkSnapshot(sourceDb, { ...source, path }, options);\n\t\t} finally {\n\t\t\tsourceDb.close();\n\t\t}\n\t}\n\n\tprivate async closeOpenSessions(context: Context): Promise<void> {\n\t\tconst results = await Promise.allSettled([...this.openSessions].map((session) => session.close(context)));\n\t\tconst errors = results.flatMap((result) => (result.status === \"rejected\" ? [result.reason] : []));\n\t\tif (errors.length === 1) throw errors[0];\n\t\tif (errors.length > 1) throw new AggregateError(errors, \"Failed to close SQLite Sessions\");\n\t}\n\n\tprivate openStorageBackedSession(metadata: SqliteSessionMetadata, db: SqliteDatabase): SqliteOpenSession {\n\t\tconst key = storageIdentity(metadata.path, metadata.id);\n\t\tconst storage = new SqliteStorage(db, { sessionId: metadata.id, now: this.now });\n\t\tthis.openStorages.set(key, storage);\n\t\tconst session = new StorageBackedSession(metadata, storage);\n\t\tconst openSession = new SqliteOpenSession(session, {\n\t\t\tonClose: () => {\n\t\t\t\ttry {\n\t\t\t\t\tdb.close();\n\t\t\t\t} finally {\n\t\t\t\t\tif (this.openStorages.get(key) === storage) this.openStorages.delete(key);\n\t\t\t\t\tthis.openSessions.delete(openSession);\n\t\t\t\t\tthis.pendingIds.delete(metadata.id);\n\t\t\t\t}\n\t\t\t},\n\t\t});\n\t\tthis.openSessions.add(openSession);\n\t\treturn openSession;\n\t}\n\n\tprivate async repositoryPathForMetadata(metadata: SqliteSessionMetadata): Promise<string> {\n\t\tconst [expected, actual] = await Promise.all([\n\t\t\trealpath(this.pathForSession(metadata.id)),\n\t\t\trealpath(metadata.path),\n\t\t]);\n\t\tif (expected !== actual) {\n\t\t\tthrow new Error(`SQLite session metadata path is outside this repository: ${metadata.path}`);\n\t\t}\n\t\treturn actual;\n\t}\n\n\tprivate reserveId(id: string): void {\n\t\tif (this.pendingIds.has(id)) throw new Error(`Session is already open: ${id}`);\n\t\tthis.pendingIds.add(id);\n\t}\n\n\tprivate pathForSession(id: string): string {\n\t\treturn this.databasePath ?? sessionPath(this.directory, id);\n\t}\n\n\tprivate usesSharedDatabase(): boolean {\n\t\treturn this.databasePath !== undefined;\n\t}\n\n\tprivate assertOpen(): void {\n\t\tif (this.closed) throw new Error(\"SqliteSessionRepo is closed\");\n\t}\n}\n"]}
@@ -0,0 +1,6 @@
1
+ import type { Entry, EntryStructure, StorageBranchScan } from "@m4ike1/ion-agent-core";
2
+ import type { SqliteDatabase } from "../types.ts";
3
+ export declare function appendEntryToBranchIndex(db: SqliteDatabase, sessionId: string, entry: Entry): void;
4
+ export declare function scanBranchEntries(db: SqliteDatabase, sessionId: string, query: StorageBranchScan): Entry[];
5
+ export declare function scanBranchEntryStructures(db: SqliteDatabase, sessionId: string, query: StorageBranchScan): EntryStructure[];
6
+ //# sourceMappingURL=branch-entries.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"branch-entries.d.ts","sourceRoot":"","sources":["../../../src/sqlite/session/branch-entries.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAEvF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAmKlD,wBAAgB,wBAAwB,CAAC,EAAE,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,GAAG,IAAI,CAYlG;AAwID,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,GAAG,KAAK,EAAE,CAE1G;AAED,wBAAgB,yBAAyB,CACxC,EAAE,EAAE,cAAc,EAClB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,iBAAiB,GACtB,cAAc,EAAE,CAElB","sourcesContent":["import type { Entry, EntryStructure, StorageBranchScan } from \"@m4ike1/ion-agent-core\";\nimport { joinSqlFragments, type SqlQuery, sql } from \"../sql.ts\";\nimport type { SqliteDatabase } from \"../types.ts\";\nimport { decodeEntryRow, type EntryRow } from \"./entries.ts\";\n\ninterface BranchMembershipRow {\n\tbranch_id: string;\n\tentry_seq: number;\n}\n\ninterface BranchMetaRow {\n\tbranch_id: string;\n\ttip_entry_id: string;\n\ttip_seq: number;\n\tbase_branch_id: string | null;\n\tbase_seq: number | null;\n}\n\ninterface BranchSegment {\n\tbranchId: string;\n\tlowerSeq: number;\n\tupperSeq: number;\n}\n\ninterface StopSeqRow {\n\tstop_seq: number | null;\n}\n\ninterface EntryStructureRow {\n\tid: string;\n\tparent_id: string | null;\n\tseq: number;\n\ttype: Entry[\"type\"];\n\tcustom_type: string | null;\n\ttimestamp: number;\n}\n\ninterface BranchTipRow {\n\tbranch_id: string;\n}\n\ninterface CompactionBoundary {\n\tbranchId: string;\n\tseq: number;\n}\n\ninterface CompactionBoundaryRow {\n\tentry_seq: number | null;\n}\n\nfunction readBranchMembership(db: SqliteDatabase, sessionId: string, entryId: string): BranchMembershipRow {\n\tconst row = sql`SELECT b.branch_id, b.entry_seq\n\t\tFROM branch_entries b\n\t\tJOIN branch_meta m ON m.session_id = b.session_id AND m.branch_id = b.branch_id\n\t\tWHERE b.session_id = ${sessionId}\n\t\t\tAND b.entry_id = ${entryId}\n\t\t\tAND ((m.base_seq IS NULL AND b.entry_seq > 0) OR (m.base_seq IS NOT NULL AND b.entry_seq > m.base_seq))\n\t\t\tAND b.entry_seq <= m.tip_seq\n\t\tORDER BY m.tip_seq DESC, b.branch_id\n\t\tLIMIT 1`.get<BranchMembershipRow>(db);\n\tif (row === undefined) throw new Error(`Branch cache missing entry ${entryId}`);\n\treturn row;\n}\n\nfunction readBranchMeta(db: SqliteDatabase, sessionId: string, branchId: string): BranchMetaRow {\n\tconst row = sql`SELECT branch_id, tip_entry_id, tip_seq, base_branch_id, base_seq\n\t\tFROM branch_meta\n\t\tWHERE session_id = ${sessionId} AND branch_id = ${branchId}`.get<BranchMetaRow>(db);\n\tif (row === undefined) throw new Error(`Branch metadata missing for branch ${branchId}`);\n\treturn row;\n}\n\nfunction insertBranchEntry(db: SqliteDatabase, sessionId: string, branchId: string, entry: Entry): void {\n\tsql`INSERT INTO branch_entries (session_id, branch_id, entry_id, entry_seq, entry_type)\n\t\tVALUES (${sessionId}, ${branchId}, ${entry.id}, ${entry.seq}, ${entry.type})`.run(db);\n}\n\nfunction readBranchTipForParent(db: SqliteDatabase, sessionId: string, parentId: string): BranchTipRow | undefined {\n\treturn sql`SELECT branch_id\n\t\tFROM branch_meta\n\t\tWHERE session_id = ${sessionId} AND tip_entry_id = ${parentId}`.get<BranchTipRow>(db);\n}\n\nfunction createRootBranchForEntry(db: SqliteDatabase, sessionId: string, entry: Entry): void {\n\tsql`INSERT INTO branch_meta (session_id, branch_id, tip_entry_id, tip_seq, base_branch_id, base_seq)\n\t\tVALUES (${sessionId}, ${entry.id}, ${entry.id}, ${entry.seq}, ${null}, ${null})`.run(db);\n\tinsertBranchEntry(db, sessionId, entry.id, entry);\n}\n\nfunction appendEntryToExistingBranch(db: SqliteDatabase, sessionId: string, branchId: string, entry: Entry): void {\n\tinsertBranchEntry(db, sessionId, branchId, entry);\n\tconst result = sql`UPDATE branch_meta\n\t\tSET tip_entry_id = ${entry.id}, tip_seq = ${entry.seq}\n\t\tWHERE session_id = ${sessionId} AND branch_id = ${branchId}`.run(db);\n\tif (result.changes !== 1) throw new Error(`Expected to update branch ${branchId}, updated ${result.changes}`);\n}\n\nfunction readBranchSegmentsNewestFirst(db: SqliteDatabase, sessionId: string, start: string): BranchSegment[] {\n\tlet { branch_id: branchId, entry_seq: upperSeq } = readBranchMembership(db, sessionId, start);\n\tconst segments: BranchSegment[] = [];\n\twhile (true) {\n\t\tconst meta = readBranchMeta(db, sessionId, branchId);\n\t\tconst lowerSeq = meta.base_seq ?? 0;\n\t\tsegments.push({ branchId, lowerSeq, upperSeq });\n\t\tif (meta.base_branch_id === null) break;\n\t\tif (meta.base_seq === null) throw new Error(`Branch ${branchId} has base branch without base_seq`);\n\t\tbranchId = meta.base_branch_id;\n\t\tupperSeq = meta.base_seq;\n\t}\n\treturn segments;\n}\n\nfunction readNewestCompactionBoundary(\n\tdb: SqliteDatabase,\n\tsessionId: string,\n\tsegmentsNewestFirst: readonly BranchSegment[],\n): CompactionBoundary | undefined {\n\tfor (const segment of segmentsNewestFirst) {\n\t\tconst row = sql`SELECT MAX(entry_seq) AS entry_seq\n\t\t\tFROM branch_entries\n\t\t\tWHERE session_id = ${sessionId}\n\t\t\t\tAND branch_id = ${segment.branchId}\n\t\t\t\tAND entry_seq > ${segment.lowerSeq}\n\t\t\t\tAND entry_seq <= ${segment.upperSeq}\n\t\t\t\tAND entry_type = ${\"compaction\"}`.get<CompactionBoundaryRow>(db);\n\t\tif (row?.entry_seq !== null && row?.entry_seq !== undefined)\n\t\t\treturn { branchId: segment.branchId, seq: row.entry_seq };\n\t}\n\treturn undefined;\n}\n\nfunction copyBranchEntriesAfterSeqThroughParent(\n\tdb: SqliteDatabase,\n\tsessionId: string,\n\ttargetBranchId: string,\n\tsegmentsNewestFirst: readonly BranchSegment[],\n\tafterSeq: number,\n): void {\n\tfor (const segment of [...segmentsNewestFirst].reverse()) {\n\t\tconst lowerSeq = Math.max(segment.lowerSeq, afterSeq);\n\t\tif (segment.upperSeq <= lowerSeq) continue;\n\t\tsql`INSERT INTO branch_entries (session_id, branch_id, entry_id, entry_seq, entry_type)\n\t\t\tSELECT ${sessionId}, ${targetBranchId}, entry_id, entry_seq, entry_type\n\t\t\tFROM branch_entries\n\t\t\tWHERE session_id = ${sessionId}\n\t\t\t\tAND branch_id = ${segment.branchId}\n\t\t\t\tAND entry_seq > ${lowerSeq}\n\t\t\t\tAND entry_seq <= ${segment.upperSeq}`.run(db);\n\t}\n}\n\nfunction createDivergentBranchForEntry(db: SqliteDatabase, sessionId: string, entry: Entry): void {\n\tif (entry.parentId === null) throw new Error(\"Root entries do not create divergent branches\");\n\tconst segmentsNewestFirst = readBranchSegmentsNewestFirst(db, sessionId, entry.parentId);\n\tconst compaction = readNewestCompactionBoundary(db, sessionId, segmentsNewestFirst);\n\tconst branchId = entry.id;\n\t// A null base means this segment stores its own root-through-parent prefix.\n\tsql`INSERT INTO branch_meta (session_id, branch_id, tip_entry_id, tip_seq, base_branch_id, base_seq)\n\t\tVALUES (${sessionId}, ${branchId}, ${entry.id}, ${entry.seq}, ${compaction?.branchId ?? null}, ${compaction?.seq ?? null})`.run(\n\t\tdb,\n\t);\n\tcopyBranchEntriesAfterSeqThroughParent(db, sessionId, branchId, segmentsNewestFirst, compaction?.seq ?? 0);\n\tinsertBranchEntry(db, sessionId, branchId, entry);\n}\n\nexport function appendEntryToBranchIndex(db: SqliteDatabase, sessionId: string, entry: Entry): void {\n\tif (entry.parentId === null) {\n\t\tcreateRootBranchForEntry(db, sessionId, entry);\n\t\treturn;\n\t}\n\n\tconst branch = readBranchTipForParent(db, sessionId, entry.parentId);\n\tif (branch === undefined) {\n\t\tcreateDivergentBranchForEntry(db, sessionId, entry);\n\t\treturn;\n\t}\n\tappendEntryToExistingBranch(db, sessionId, branch.branch_id, entry);\n}\n\nfunction stopPredicates(query: StorageBranchScan): SqlQuery[] {\n\tconst predicates: SqlQuery[] = [];\n\tif (query.stopAtType !== undefined) predicates.push(sql`b.entry_type = ${query.stopAtType}`);\n\tif (query.stopAtId !== undefined) predicates.push(sql`b.entry_id = ${query.stopAtId}`);\n\treturn predicates;\n}\n\nfunction readStopSeq(\n\tdb: SqliteDatabase,\n\tsessionId: string,\n\tsegment: BranchSegment,\n\tquery: StorageBranchScan,\n\toldestFirst: boolean,\n): number | undefined {\n\tconst stop = stopPredicates(query);\n\tif (stop.length === 0) return undefined;\n\tconst aggregate = oldestFirst ? sql`MIN(b.entry_seq)` : sql`MAX(b.entry_seq)`;\n\tconst row = sql`SELECT ${aggregate} AS stop_seq\n\t\tFROM branch_entries b\n\t\tWHERE b.session_id = ${sessionId}\n\t\t\tAND b.branch_id = ${segment.branchId}\n\t\t\tAND b.entry_seq > ${segment.lowerSeq}\n\t\t\tAND b.entry_seq <= ${segment.upperSeq}\n\t\t\tAND (${joinSqlFragments(stop, \" OR \")})`.get<StopSeqRow>(db);\n\treturn row?.stop_seq ?? undefined;\n}\n\nfunction branchScanPredicates(\n\tsessionId: string,\n\tsegment: BranchSegment,\n\tquery: StorageBranchScan,\n\toldestFirst: boolean,\n\tstopSeq: number | undefined,\n): SqlQuery[] {\n\tconst predicates: SqlQuery[] = [\n\t\tsql`b.session_id = ${sessionId}`,\n\t\tsql`b.branch_id = ${segment.branchId}`,\n\t\tsql`b.entry_seq > ${segment.lowerSeq}`,\n\t\tsql`b.entry_seq <= ${segment.upperSeq}`,\n\t\tsql`e.session_id = b.session_id`,\n\t];\n\tif (stopSeq !== undefined)\n\t\tpredicates.push(oldestFirst ? sql`b.entry_seq <= ${stopSeq}` : sql`b.entry_seq >= ${stopSeq}`);\n\tif (query.type !== undefined) predicates.push(sql`b.entry_type = ${query.type}`);\n\tif (query.customType !== undefined) predicates.push(sql`e.custom_type = ${query.customType}`);\n\tif (query.cursor !== undefined) {\n\t\tpredicates.push(oldestFirst ? sql`b.entry_seq > ${query.cursor.seq}` : sql`b.entry_seq < ${query.cursor.seq}`);\n\t}\n\treturn predicates;\n}\n\nfunction limitSql(limit: number | undefined): SqlQuery {\n\treturn limit === undefined ? sql`` : sql`LIMIT ${Math.max(0, limit)}`;\n}\n\nfunction scanEntrySegmentRows(\n\tdb: SqliteDatabase,\n\tsessionId: string,\n\tsegment: BranchSegment,\n\tquery: StorageBranchScan,\n\toldestFirst: boolean,\n\tstopSeq: number | undefined,\n\tlimit: number | undefined,\n): EntryRow[] {\n\tconst predicates = branchScanPredicates(sessionId, segment, query, oldestFirst, stopSeq);\n\tconst order = oldestFirst ? sql`ASC` : sql`DESC`;\n\treturn sql`SELECT e.id, e.parent_id, e.seq, e.type, e.custom_type, e.timestamp, e.payload\n\t\tFROM branch_entries b\n\t\tCROSS JOIN entries e ON e.session_id = b.session_id AND e.id = b.entry_id\n\t\tWHERE ${joinSqlFragments(predicates, \" AND \")}\n\t\tORDER BY b.entry_seq ${order} ${limitSql(limit)}`.all<EntryRow>(db);\n}\n\nfunction decodeEntryStructureRow(row: EntryStructureRow): EntryStructure {\n\treturn {\n\t\tid: row.id,\n\t\tparentId: row.parent_id,\n\t\tseq: row.seq,\n\t\ttimestamp: row.timestamp,\n\t\ttype: row.type,\n\t\t...(row.custom_type === null ? {} : { customType: row.custom_type }),\n\t};\n}\n\nfunction scanStructureSegmentRows(\n\tdb: SqliteDatabase,\n\tsessionId: string,\n\tsegment: BranchSegment,\n\tquery: StorageBranchScan,\n\toldestFirst: boolean,\n\tstopSeq: number | undefined,\n\tlimit: number | undefined,\n): EntryStructure[] {\n\tconst predicates = branchScanPredicates(sessionId, segment, query, oldestFirst, stopSeq);\n\tconst order = oldestFirst ? sql`ASC` : sql`DESC`;\n\tconst rows = sql`SELECT e.id, e.parent_id, e.seq, e.type, e.custom_type, e.timestamp\n\t\tFROM branch_entries b\n\t\tCROSS JOIN entries e ON e.session_id = b.session_id AND e.id = b.entry_id\n\t\tWHERE ${joinSqlFragments(predicates, \" AND \")}\n\t\tORDER BY b.entry_seq ${order} ${limitSql(limit)}`.all<EntryStructureRow>(db);\n\treturn rows.map(decodeEntryStructureRow);\n}\n\nfunction scanBranchSegments<T>(\n\tdb: SqliteDatabase,\n\tsessionId: string,\n\tquery: StorageBranchScan,\n\treadSegment: (\n\t\tdb: SqliteDatabase,\n\t\tsessionId: string,\n\t\tsegment: BranchSegment,\n\t\tquery: StorageBranchScan,\n\t\toldestFirst: boolean,\n\t\tstopSeq: number | undefined,\n\t\tlimit: number | undefined,\n\t) => T[],\n): T[] {\n\tconst oldestFirst = query.order === \"oldestFirst\";\n\tconst segmentsNewestFirst = readBranchSegmentsNewestFirst(db, sessionId, query.start);\n\tconst segments = oldestFirst ? [...segmentsNewestFirst].reverse() : segmentsNewestFirst;\n\tconst limit = query.limit === undefined ? undefined : Math.max(0, query.limit);\n\tif (limit === 0) return [];\n\n\tconst rows: T[] = [];\n\tfor (const segment of segments) {\n\t\tconst remaining = limit === undefined ? undefined : limit - rows.length;\n\t\tif (remaining !== undefined && remaining <= 0) break;\n\t\tconst stopSeq = readStopSeq(db, sessionId, segment, query, oldestFirst);\n\t\trows.push(...readSegment(db, sessionId, segment, query, oldestFirst, stopSeq, remaining));\n\t\tif (stopSeq !== undefined) break;\n\t}\n\treturn rows;\n}\n\nexport function scanBranchEntries(db: SqliteDatabase, sessionId: string, query: StorageBranchScan): Entry[] {\n\treturn scanBranchSegments(db, sessionId, query, scanEntrySegmentRows).map(decodeEntryRow);\n}\n\nexport function scanBranchEntryStructures(\n\tdb: SqliteDatabase,\n\tsessionId: string,\n\tquery: StorageBranchScan,\n): EntryStructure[] {\n\treturn scanBranchSegments(db, sessionId, query, scanStructureSegmentRows);\n}\n"]}