@xnetjs/sqlite 0.0.2 → 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 (37) hide show
  1. package/dist/{adapter-I7yAV6iu.d.ts → adapter-imtxkmXZ.d.ts} +48 -1
  2. package/dist/adapters/electron.d.ts +70 -6
  3. package/dist/adapters/electron.js +662 -36
  4. package/dist/adapters/expo.d.ts +6 -2
  5. package/dist/adapters/expo.js +34 -6
  6. package/dist/adapters/memory.d.ts +6 -2
  7. package/dist/adapters/memory.js +8 -1
  8. package/dist/adapters/reader-thread.d.ts +78 -0
  9. package/dist/adapters/reader-thread.js +57 -0
  10. package/dist/adapters/web-proxy.d.ts +71 -3
  11. package/dist/adapters/web-proxy.js +554 -39
  12. package/dist/adapters/web-router-worker.d.ts +76 -0
  13. package/dist/adapters/web-router-worker.js +91 -0
  14. package/dist/adapters/web-worker.d.ts +56 -1
  15. package/dist/adapters/web-worker.js +253 -14
  16. package/dist/adapters/web.d.ts +90 -3
  17. package/dist/adapters/web.js +10 -4
  18. package/dist/browser-support.d.ts +65 -1
  19. package/dist/browser-support.js +15 -3
  20. package/dist/chunk-5HC5V73T.js +191 -0
  21. package/dist/chunk-BBZDKLA3.js +727 -0
  22. package/dist/chunk-CBU263LI.js +30 -0
  23. package/dist/chunk-CGI2YBZY.js +16 -0
  24. package/dist/chunk-S6MT6KCI.js +279 -0
  25. package/dist/chunk-SV475UL5.js +44 -0
  26. package/dist/chunk-W3L4FXU5.js +415 -0
  27. package/dist/index.d.ts +111 -8
  28. package/dist/index.js +219 -130
  29. package/dist/schema-C4gufY3B.d.ts +35 -0
  30. package/dist/types-BTabr_VP.d.ts +225 -0
  31. package/dist/worker-scheduler-D04DqMmR.d.ts +56 -0
  32. package/package.json +5 -1
  33. package/dist/chunk-BXYZU3OL.js +0 -245
  34. package/dist/chunk-HIREU5S5.js +0 -193
  35. package/dist/chunk-ZRR5D2OD.js +0 -140
  36. package/dist/schema-CjkXTqxn.d.ts +0 -35
  37. package/dist/types-C_aHfRDF.d.ts +0 -42
@@ -0,0 +1,727 @@
1
+ import {
2
+ detectOpfsCapability
3
+ } from "./chunk-SV475UL5.js";
4
+ import {
5
+ isSQLiteCorruptionError
6
+ } from "./chunk-CBU263LI.js";
7
+ import {
8
+ SCHEMA_DDL,
9
+ SCHEMA_VERSION
10
+ } from "./chunk-W3L4FXU5.js";
11
+
12
+ // src/adapters/opfs-retry.ts
13
+ function isOpfsLockError(err) {
14
+ const e = err;
15
+ if (!e) return false;
16
+ if (e.name === "NoModificationAllowedError") return true;
17
+ return /Access Handles cannot be created|createSyncAccessHandle/i.test(e.message ?? "");
18
+ }
19
+ async function withOpfsLockRetry(fn, options = {}) {
20
+ const attempts = options.attempts ?? 5;
21
+ const baseDelayMs = options.baseDelayMs ?? 150;
22
+ const sleep = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
23
+ let lastErr;
24
+ for (let attempt = 1; attempt <= attempts; attempt++) {
25
+ try {
26
+ return await fn();
27
+ } catch (err) {
28
+ lastErr = err;
29
+ if (!isOpfsLockError(err) || attempt === attempts) throw err;
30
+ options.onRetry?.(attempt, err);
31
+ await sleep(baseDelayMs * attempt);
32
+ }
33
+ }
34
+ throw lastErr;
35
+ }
36
+
37
+ // src/adapters/stmt-cache.ts
38
+ var DEFAULT_STMT_CACHE_CAPACITY = 64;
39
+ function hasInteriorSemicolon(sql) {
40
+ const trimmed = sql.replace(/[\s;]+$/, "");
41
+ return trimmed.includes(";");
42
+ }
43
+ var StmtCache = class {
44
+ constructor(capacity = DEFAULT_STMT_CACHE_CAPACITY) {
45
+ this.capacity = capacity;
46
+ if (capacity < 1) {
47
+ throw new Error("StmtCache capacity must be >= 1");
48
+ }
49
+ }
50
+ entries = /* @__PURE__ */ new Map();
51
+ get size() {
52
+ return this.entries.size;
53
+ }
54
+ /** Look up a cached statement, refreshing its recency. */
55
+ get(sql) {
56
+ const stmt = this.entries.get(sql);
57
+ if (stmt !== void 0) {
58
+ this.entries.delete(sql);
59
+ this.entries.set(sql, stmt);
60
+ }
61
+ return stmt;
62
+ }
63
+ /** Insert a statement, finalizing the least-recently-used one if over capacity. */
64
+ set(sql, stmt) {
65
+ const existing = this.entries.get(sql);
66
+ if (existing !== void 0 && existing !== stmt) {
67
+ this.safeFinalize(existing);
68
+ }
69
+ this.entries.delete(sql);
70
+ this.entries.set(sql, stmt);
71
+ while (this.entries.size > this.capacity) {
72
+ const oldest = this.entries.keys().next().value;
73
+ const evicted = this.entries.get(oldest);
74
+ this.entries.delete(oldest);
75
+ if (evicted !== void 0) {
76
+ this.safeFinalize(evicted);
77
+ }
78
+ }
79
+ }
80
+ /**
81
+ * Finalize and drop every cached statement. Called on DDL (`exec`) — where a
82
+ * dropped table would otherwise leave poisoned handles — and on `close()`.
83
+ */
84
+ clear() {
85
+ for (const stmt of this.entries.values()) {
86
+ this.safeFinalize(stmt);
87
+ }
88
+ this.entries.clear();
89
+ }
90
+ /** finalize() must never throw into a caller that is already unwinding. */
91
+ safeFinalize(stmt) {
92
+ try {
93
+ stmt.finalize();
94
+ } catch {
95
+ }
96
+ }
97
+ };
98
+
99
+ // src/adapters/web.ts
100
+ var WEB_SQLITE_VFS_NAME = "opfs-sahpool";
101
+ var WEB_SQLITE_VFS_DIRECTORY = ".xnet-sqlite";
102
+ var WEB_SQLITE_INITIAL_CAPACITY = 10;
103
+ function isDebugEnabled() {
104
+ return typeof self !== "undefined" && typeof localStorage !== "undefined" && localStorage.getItem("xnet:sqlite:debug") === "true";
105
+ }
106
+ function log(...args) {
107
+ if (isDebugEnabled()) {
108
+ console.log(...args);
109
+ }
110
+ }
111
+ function nowMs() {
112
+ return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
113
+ }
114
+ function getDatabasePath(config) {
115
+ return config.path.startsWith("/") ? config.path : `/${config.path}`;
116
+ }
117
+ async function removeOPFSDirectory(directory) {
118
+ const storage = globalThis.navigator?.storage;
119
+ if (typeof storage?.getDirectory !== "function") {
120
+ return;
121
+ }
122
+ try {
123
+ const root = await storage.getDirectory();
124
+ await root.removeEntry(directory.replace(/^\/+/, ""), { recursive: true });
125
+ } catch (err) {
126
+ if (err instanceof DOMException && err.name === "NotFoundError") {
127
+ return;
128
+ }
129
+ throw err;
130
+ }
131
+ }
132
+ async function resetWebSQLiteOpfsStorage(config) {
133
+ const sqlite3InitModule = (await import("@sqlite.org/sqlite-wasm")).default;
134
+ const sqlite3 = await sqlite3InitModule();
135
+ const dbPath = getDatabasePath(config);
136
+ try {
137
+ const poolUtil = await sqlite3.installOpfsSAHPoolVfs({
138
+ name: WEB_SQLITE_VFS_NAME,
139
+ directory: WEB_SQLITE_VFS_DIRECTORY,
140
+ initialCapacity: WEB_SQLITE_INITIAL_CAPACITY,
141
+ clearOnInit: false
142
+ });
143
+ try {
144
+ poolUtil.unlink(dbPath);
145
+ } catch {
146
+ }
147
+ await poolUtil.wipeFiles();
148
+ await poolUtil.removeVfs();
149
+ } catch (err) {
150
+ console.warn("[WebSQLiteAdapter] SAH-pool reset failed, removing OPFS directory:", err);
151
+ await removeOPFSDirectory(WEB_SQLITE_VFS_DIRECTORY);
152
+ }
153
+ }
154
+ var WebSQLiteAdapter = class {
155
+ sqlite3 = null;
156
+ db = null;
157
+ poolUtil = null;
158
+ _config = null;
159
+ inTransaction = false;
160
+ /**
161
+ * Monotonic time of the open transaction's last operation. Lets the worker
162
+ * host distinguish a LIVE manual transaction from one abandoned by a client
163
+ * that died between BEGIN and COMMIT (PowerSync-style lease recovery, 0263).
164
+ */
165
+ lastTransactionActivityAt = 0;
166
+ storageMode = "memory";
167
+ /**
168
+ * Per-phase timing of the last {@link open} call, or null until it runs.
169
+ * Read by the worker host to emit `[xNet] sqlite open phases` (exploration
170
+ * 0253). Diagnostic only — never affects behaviour.
171
+ */
172
+ openPhaseTimings = null;
173
+ /** ms spent in `applySchema()` during {@link createWebSQLiteAdapter}, for the open-phases line. */
174
+ schemaApplyMs = 0;
175
+ /** How many times the OPFS lock-retry backoff fired during the last {@link open} (0 = first try). */
176
+ openRetryAttempts = 0;
177
+ /**
178
+ * Prepared-statement LRU for the hot query/run path (exploration 0263).
179
+ * `db.exec()` re-parses the SQL on every call; the hot path repeats a small
180
+ * statement set, so cached `oo1.Stmt` handles skip the parse+prepare cost.
181
+ * Cleared (finalizing all handles) on {@link exec} — the DDL/script path —
182
+ * and on {@link close}.
183
+ */
184
+ stmts = new StmtCache();
185
+ /** Per-phase timing of the last {@link open}, or null if it hasn't run. */
186
+ getOpenPhaseTimings() {
187
+ return this.openPhaseTimings ? { ...this.openPhaseTimings } : null;
188
+ }
189
+ async open(config) {
190
+ if (this.db !== null) {
191
+ throw new Error("Database already open. Call close() first.");
192
+ }
193
+ log("[WebSQLiteAdapter] Starting open()...");
194
+ const openStartedAt = nowMs();
195
+ let afterImport = openStartedAt;
196
+ let afterInit = openStartedAt;
197
+ let afterVfsInstall = openStartedAt;
198
+ let afterReserveCapacity = openStartedAt;
199
+ let afterDbOpen = openStartedAt;
200
+ log("[WebSQLiteAdapter] Importing sqlite-wasm...");
201
+ const sqlite3InitModule = (await import("@sqlite.org/sqlite-wasm")).default;
202
+ log("[WebSQLiteAdapter] sqlite-wasm imported");
203
+ afterImport = nowMs();
204
+ log("[WebSQLiteAdapter] Initializing sqlite3 module...");
205
+ this.sqlite3 = await sqlite3InitModule();
206
+ log("[WebSQLiteAdapter] sqlite3 module initialized");
207
+ afterInit = nowMs();
208
+ try {
209
+ await withOpfsLockRetry(
210
+ async () => {
211
+ log("[WebSQLiteAdapter] Installing OPFS-SAHPool VFS...");
212
+ this.poolUtil = await this.sqlite3.installOpfsSAHPoolVfs({
213
+ name: WEB_SQLITE_VFS_NAME,
214
+ directory: WEB_SQLITE_VFS_DIRECTORY,
215
+ initialCapacity: WEB_SQLITE_INITIAL_CAPACITY,
216
+ // Support ~3-4 databases with journals
217
+ clearOnInit: false
218
+ });
219
+ log("[WebSQLiteAdapter] OPFS-SAHPool VFS installed");
220
+ afterVfsInstall = nowMs();
221
+ log("[WebSQLiteAdapter] Reserving capacity...");
222
+ await this.poolUtil.reserveMinimumCapacity(10);
223
+ log("[WebSQLiteAdapter] Capacity reserved");
224
+ afterReserveCapacity = nowMs();
225
+ const dbPath = getDatabasePath(config);
226
+ log("[WebSQLiteAdapter] Opening database at", dbPath);
227
+ this.db = new this.poolUtil.OpfsSAHPoolDb(dbPath, "c");
228
+ this.storageMode = "opfs";
229
+ log("[WebSQLiteAdapter] Database opened with OPFS-SAHPool");
230
+ afterDbOpen = nowMs();
231
+ },
232
+ {
233
+ onRetry: (attempt, retryErr) => {
234
+ this.openRetryAttempts = attempt;
235
+ console.warn(
236
+ `[WebSQLiteAdapter] OPFS access handles are busy (attempt ${attempt}) \u2014 a previous tab/worker is likely still releasing them; retrying before any in-memory fallback.`,
237
+ retryErr
238
+ );
239
+ }
240
+ }
241
+ );
242
+ } catch (err) {
243
+ const capability = detectOpfsCapability();
244
+ if (capability.mode === "async-opfs") {
245
+ console.info("[WebSQLiteAdapter] Sync access handles unavailable \u2014 " + capability.reason);
246
+ } else {
247
+ console.warn("[WebSQLiteAdapter] OPFS-SAHPool not available, trying OPFS direct mode:", err);
248
+ }
249
+ const dbPath = getDatabasePath(config);
250
+ const opfsDbCtor = this.sqlite3?.oo1?.OpfsDb;
251
+ if (typeof opfsDbCtor === "function") {
252
+ try {
253
+ this.db = new opfsDbCtor(dbPath, "c");
254
+ this.storageMode = "opfs";
255
+ log("[WebSQLiteAdapter] Database opened with OPFS direct mode");
256
+ } catch (opfsErr) {
257
+ log("[WebSQLiteAdapter] OPFS direct mode not available:", opfsErr);
258
+ this.db = new this.sqlite3.oo1.DB(":memory:", "c");
259
+ this.storageMode = "memory";
260
+ }
261
+ } else {
262
+ this.db = new this.sqlite3.oo1.DB(":memory:", "c");
263
+ this.storageMode = "memory";
264
+ }
265
+ if (this.storageMode === "memory") {
266
+ const cause = isOpfsLockError(err) ? "another xNet tab/worker is holding the local database" : "OPFS is unavailable in this browser context";
267
+ console.error(
268
+ `[WebSQLiteAdapter] Using an IN-MEMORY database (${cause}). Local data will NOT persist across reloads and the workspace will appear empty until it re-syncs from the hub. Close other xNet tabs and reload to restore persistent storage.`
269
+ );
270
+ }
271
+ }
272
+ this._config = config;
273
+ const beforePragmas = nowMs();
274
+ if (config.foreignKeys !== false) {
275
+ this.execSync("PRAGMA foreign_keys = ON");
276
+ }
277
+ if (config.busyTimeout) {
278
+ this.execSync(`PRAGMA busy_timeout = ${config.busyTimeout}`);
279
+ } else {
280
+ this.execSync("PRAGMA busy_timeout = 5000");
281
+ }
282
+ try {
283
+ this.execSync("PRAGMA page_size = 8192");
284
+ } catch (err) {
285
+ log("[WebSQLiteAdapter] page_size pragma not applied:", err);
286
+ }
287
+ try {
288
+ this.execSync("PRAGMA auto_vacuum = INCREMENTAL");
289
+ } catch (err) {
290
+ log("[WebSQLiteAdapter] auto_vacuum pragma not applied:", err);
291
+ }
292
+ this.execSync("PRAGMA synchronous = NORMAL");
293
+ this.execSync("PRAGMA cache_size = -262144");
294
+ try {
295
+ this.execSync("PRAGMA mmap_size = 268435456");
296
+ } catch (err) {
297
+ log("[WebSQLiteAdapter] mmap_size pragma not applied:", err);
298
+ }
299
+ this.execSync("PRAGMA temp_store = MEMORY");
300
+ try {
301
+ this.execSync("PRAGMA journal_mode = TRUNCATE");
302
+ } catch (err) {
303
+ log("[WebSQLiteAdapter] journal_mode pragma not applied:", err);
304
+ }
305
+ try {
306
+ this.execSync("PRAGMA analysis_limit = 400");
307
+ this.execSync("PRAGMA optimize = 0x10002");
308
+ } catch (err) {
309
+ log("[WebSQLiteAdapter] open-time optimize not applied:", err);
310
+ }
311
+ const afterPragmas = nowMs();
312
+ const round = (a, b) => Math.round(b - a);
313
+ this.openPhaseTimings = {
314
+ wasmImportMs: round(openStartedAt, afterImport),
315
+ wasmInitMs: round(afterImport, afterInit),
316
+ vfsInstallMs: round(afterInit, afterVfsInstall),
317
+ reserveCapacityMs: round(afterVfsInstall, afterReserveCapacity),
318
+ dbOpenMs: round(afterReserveCapacity, afterDbOpen),
319
+ pragmasMs: round(beforePragmas, afterPragmas),
320
+ totalOpenMs: round(openStartedAt, afterPragmas)
321
+ };
322
+ }
323
+ async close() {
324
+ this.stmts.clear();
325
+ if (this.db) {
326
+ try {
327
+ this.execSync("PRAGMA optimize");
328
+ } catch (err) {
329
+ log("[WebSQLiteAdapter] optimize on close skipped:", err);
330
+ }
331
+ this.db.close();
332
+ this.db = null;
333
+ }
334
+ this.sqlite3 = null;
335
+ this.poolUtil = null;
336
+ this._config = null;
337
+ }
338
+ isOpen() {
339
+ return this.db !== null;
340
+ }
341
+ getStorageMode() {
342
+ return this.storageMode;
343
+ }
344
+ /** True while a manual BEGIN…COMMIT/ROLLBACK transaction is open. */
345
+ isInTransaction() {
346
+ return this.inTransaction;
347
+ }
348
+ /** ms since the open transaction last executed an operation (0 when none). */
349
+ transactionIdleMs() {
350
+ return this.inTransaction ? nowMs() - this.lastTransactionActivityAt : 0;
351
+ }
352
+ /** Stamp transaction liveness — called by ops that run inside one. */
353
+ touchTransaction() {
354
+ if (this.inTransaction) {
355
+ this.lastTransactionActivityAt = nowMs();
356
+ }
357
+ }
358
+ /**
359
+ * Fetch (or prepare and cache) the statement for `sql`, or null when the SQL
360
+ * must bypass the cache: `db.prepare()` compiles only the FIRST statement of
361
+ * a multi-statement string, whereas `db.exec()` runs them all — serving such
362
+ * SQL from the cache would silently drop statements.
363
+ */
364
+ getCachedStmt(sql) {
365
+ if (hasInteriorSemicolon(sql)) {
366
+ return null;
367
+ }
368
+ let stmt = this.stmts.get(sql);
369
+ if (stmt === void 0) {
370
+ stmt = this.db.prepare(sql);
371
+ this.stmts.set(sql, stmt);
372
+ }
373
+ return stmt;
374
+ }
375
+ async query(sql, params) {
376
+ this.ensureOpen();
377
+ this.touchTransaction();
378
+ const rows = [];
379
+ const stmt = this.getCachedStmt(sql);
380
+ if (stmt === null) {
381
+ this.db.exec({
382
+ sql,
383
+ bind: params,
384
+ rowMode: "object",
385
+ callback: (row) => {
386
+ rows.push(row);
387
+ }
388
+ });
389
+ return rows;
390
+ }
391
+ try {
392
+ if (params && params.length > 0) {
393
+ stmt.bind(params);
394
+ }
395
+ while (stmt.step()) {
396
+ rows.push(stmt.get({}));
397
+ }
398
+ } finally {
399
+ stmt.reset();
400
+ stmt.clearBindings();
401
+ }
402
+ return rows;
403
+ }
404
+ async queryOne(sql, params) {
405
+ const rows = await this.query(sql, params);
406
+ return rows[0] ?? null;
407
+ }
408
+ async queryBatch(reads) {
409
+ const results = [];
410
+ for (const read of reads) {
411
+ results.push(await this.query(read.sql, read.params));
412
+ }
413
+ return results;
414
+ }
415
+ async run(sql, params) {
416
+ this.ensureOpen();
417
+ this.touchTransaction();
418
+ const stmt = this.getCachedStmt(sql);
419
+ if (stmt === null) {
420
+ this.db.exec({
421
+ sql,
422
+ bind: params
423
+ });
424
+ } else {
425
+ try {
426
+ if (params && params.length > 0) {
427
+ stmt.bind(params);
428
+ }
429
+ while (stmt.step()) {
430
+ }
431
+ } finally {
432
+ stmt.reset();
433
+ stmt.clearBindings();
434
+ }
435
+ }
436
+ return {
437
+ changes: this.sqlite3.capi.sqlite3_changes(this.db.pointer),
438
+ lastInsertRowid: this.sqlite3.capi.sqlite3_last_insert_rowid(this.db.pointer)
439
+ };
440
+ }
441
+ async exec(sql) {
442
+ this.ensureOpen();
443
+ this.stmts.clear();
444
+ this.execSync(sql);
445
+ }
446
+ execSync(sql) {
447
+ this.db.exec({ sql });
448
+ }
449
+ async transaction(fn) {
450
+ await this.beginTransaction();
451
+ try {
452
+ const result = await fn();
453
+ await this.commit();
454
+ return result;
455
+ } catch (err) {
456
+ if (this.inTransaction) {
457
+ try {
458
+ await this.rollback();
459
+ } catch (rollbackErr) {
460
+ if (isSQLiteCorruptionError(rollbackErr) && !isSQLiteCorruptionError(err)) {
461
+ throw rollbackErr;
462
+ }
463
+ }
464
+ }
465
+ throw err;
466
+ }
467
+ }
468
+ async applyNodeBatch(input) {
469
+ await this.transaction(async () => {
470
+ for (const node of input.nodes) {
471
+ await this.run(
472
+ `INSERT INTO nodes (id, schema_id, created_at, updated_at, created_by, deleted_at)
473
+ VALUES (?, ?, ?, ?, ?, ?)
474
+ ON CONFLICT(id) DO UPDATE SET
475
+ schema_id = excluded.schema_id,
476
+ updated_at = excluded.updated_at,
477
+ deleted_at = excluded.deleted_at`,
478
+ [node.id, node.schemaId, node.createdAt, node.updatedAt, node.createdBy, node.deletedAt]
479
+ );
480
+ if (node.propertyKeys.length === 0) {
481
+ await this.run("DELETE FROM node_properties WHERE node_id = ?", [node.id]);
482
+ } else {
483
+ await this.run(
484
+ `DELETE FROM node_properties
485
+ WHERE node_id = ? AND property_key NOT IN (${node.propertyKeys.map(() => "?").join(", ")})`,
486
+ [node.id, ...node.propertyKeys]
487
+ );
488
+ }
489
+ }
490
+ for (const property of input.properties) {
491
+ await this.run(
492
+ `INSERT INTO node_properties
493
+ (node_id, property_key, value, lamport_time, updated_by, updated_at)
494
+ VALUES (?, ?, ?, ?, ?, ?)
495
+ ON CONFLICT(node_id, property_key) DO UPDATE SET
496
+ value = excluded.value,
497
+ lamport_time = excluded.lamport_time,
498
+ updated_by = excluded.updated_by,
499
+ updated_at = excluded.updated_at
500
+ WHERE excluded.lamport_time > node_properties.lamport_time`,
501
+ [
502
+ property.nodeId,
503
+ property.propertyKey,
504
+ property.value,
505
+ property.lamportTime,
506
+ property.updatedBy,
507
+ property.updatedAt
508
+ ]
509
+ );
510
+ }
511
+ if (input.indexMode !== "defer-schema") {
512
+ for (const node of input.nodes) {
513
+ await this.run("DELETE FROM node_property_scalars WHERE node_id = ?", [node.id]);
514
+ }
515
+ for (const row of input.scalarIndexRows) {
516
+ await this.run(
517
+ `INSERT INTO node_property_scalars
518
+ (
519
+ node_id,
520
+ schema_id,
521
+ property_key,
522
+ value_type,
523
+ value_text,
524
+ value_number,
525
+ value_boolean,
526
+ value_hash,
527
+ updated_at,
528
+ lamport_time
529
+ )
530
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
531
+ [
532
+ row.nodeId,
533
+ row.schemaId,
534
+ row.propertyKey,
535
+ row.valueType,
536
+ row.valueText,
537
+ row.valueNumber,
538
+ row.valueBoolean,
539
+ row.valueHash,
540
+ row.updatedAt,
541
+ row.lamportTime
542
+ ]
543
+ );
544
+ }
545
+ for (const nodeId of input.ftsNodeIds) {
546
+ await this.run("DELETE FROM nodes_fts WHERE node_id = ?", [nodeId]);
547
+ }
548
+ for (const row of input.ftsRows) {
549
+ await this.run("INSERT INTO nodes_fts (node_id, title, content) VALUES (?, ?, ?)", [
550
+ row.nodeId,
551
+ row.title,
552
+ row.content
553
+ ]);
554
+ }
555
+ }
556
+ for (const change of input.changes) {
557
+ await this.run(
558
+ `INSERT OR IGNORE INTO changes
559
+ (hash, node_id, payload, lamport_time, lamport_peer, wall_time, author, parent_hash, batch_id, signature)
560
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
561
+ [
562
+ change.hash,
563
+ change.nodeId,
564
+ change.payload,
565
+ change.lamportTime,
566
+ change.lamportPeer,
567
+ change.wallTime,
568
+ change.author,
569
+ change.parentHash,
570
+ change.batchId,
571
+ change.signature
572
+ ]
573
+ );
574
+ }
575
+ await this.run(
576
+ `INSERT INTO sync_state (key, value) VALUES ('lastLamportTime', ?)
577
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value`,
578
+ [String(input.lastLamportTime)]
579
+ );
580
+ if (input.indexMode !== "defer-schema") {
581
+ const invalidatedAt = Date.now();
582
+ for (const schemaId of input.affectedSchemaIds) {
583
+ await this.run(
584
+ `UPDATE node_query_materializations
585
+ SET invalidated_at = ?
586
+ WHERE schema_id = ? AND invalidated_at IS NULL`,
587
+ [invalidatedAt, schemaId]
588
+ );
589
+ }
590
+ }
591
+ });
592
+ return {
593
+ nodeRowsWritten: input.nodes.length,
594
+ propertyRowsWritten: input.properties.length,
595
+ changeRowsWritten: input.changes.length,
596
+ scalarRowsWritten: input.scalarIndexRows.length,
597
+ ftsRowsWritten: input.ftsRows.length
598
+ };
599
+ }
600
+ async beginTransaction() {
601
+ if (this.inTransaction) {
602
+ throw new Error("Transaction already in progress");
603
+ }
604
+ this.execSync("BEGIN IMMEDIATE");
605
+ this.inTransaction = true;
606
+ this.lastTransactionActivityAt = nowMs();
607
+ }
608
+ async commit() {
609
+ if (!this.inTransaction) {
610
+ throw new Error("No transaction in progress");
611
+ }
612
+ try {
613
+ this.execSync("COMMIT");
614
+ this.inTransaction = false;
615
+ } catch (err) {
616
+ if (isSQLiteCorruptionError(err)) {
617
+ this.inTransaction = false;
618
+ }
619
+ throw err;
620
+ }
621
+ }
622
+ async rollback() {
623
+ if (!this.inTransaction) {
624
+ return;
625
+ }
626
+ try {
627
+ this.execSync("ROLLBACK");
628
+ } finally {
629
+ this.inTransaction = false;
630
+ }
631
+ }
632
+ async prepare(sql) {
633
+ return {
634
+ query: async (params) => {
635
+ return this.query(sql, params);
636
+ },
637
+ queryOne: async (params) => {
638
+ return this.queryOne(sql, params);
639
+ },
640
+ run: async (params) => {
641
+ return this.run(sql, params);
642
+ },
643
+ finalize: async () => {
644
+ }
645
+ };
646
+ }
647
+ async getSchemaVersion() {
648
+ try {
649
+ const row = await this.queryOne(
650
+ "SELECT version FROM _schema_version ORDER BY version DESC LIMIT 1"
651
+ );
652
+ return row?.version ?? 0;
653
+ } catch {
654
+ return 0;
655
+ }
656
+ }
657
+ async setSchemaVersion(version) {
658
+ await this.run("INSERT INTO _schema_version (version, applied_at) VALUES (?, ?)", [
659
+ version,
660
+ Date.now()
661
+ ]);
662
+ }
663
+ async applySchema(version, sql) {
664
+ const currentVersion = await this.getSchemaVersion();
665
+ if (currentVersion >= version) {
666
+ return false;
667
+ }
668
+ await this.transaction(async () => {
669
+ await this.exec(sql);
670
+ await this.setSchemaVersion(version);
671
+ });
672
+ return true;
673
+ }
674
+ async getDatabaseSize() {
675
+ try {
676
+ const row = await this.queryOne(
677
+ "SELECT page_count * page_size as size FROM pragma_page_count(), pragma_page_size()"
678
+ );
679
+ return row?.size ?? 0;
680
+ } catch {
681
+ return 0;
682
+ }
683
+ }
684
+ async vacuum() {
685
+ await this.exec("VACUUM");
686
+ }
687
+ async incrementalVacuum(maxPages) {
688
+ this.ensureOpen();
689
+ return stepIncrementalVacuumToCompletion(this.db, maxPages);
690
+ }
691
+ async checkpoint() {
692
+ return 0;
693
+ }
694
+ ensureOpen() {
695
+ if (!this.db || !this.sqlite3) {
696
+ throw new Error("Database not open. Call open() first.");
697
+ }
698
+ }
699
+ };
700
+ function stepIncrementalVacuumToCompletion(db, maxPages) {
701
+ const limit = maxPages !== void 0 && maxPages > 0 ? Math.floor(maxPages) : Infinity;
702
+ const stmt = db.prepare("PRAGMA incremental_vacuum");
703
+ let freed = 0;
704
+ try {
705
+ while (freed < limit && stmt.step()) {
706
+ freed++;
707
+ }
708
+ } finally {
709
+ stmt.finalize();
710
+ }
711
+ return freed;
712
+ }
713
+ async function createWebSQLiteAdapter(config) {
714
+ const adapter = new WebSQLiteAdapter();
715
+ await adapter.open(config);
716
+ const schemaStartedAt = nowMs();
717
+ await adapter.applySchema(SCHEMA_VERSION, SCHEMA_DDL);
718
+ adapter.schemaApplyMs = Math.round(nowMs() - schemaStartedAt);
719
+ return adapter;
720
+ }
721
+
722
+ export {
723
+ resetWebSQLiteOpfsStorage,
724
+ WebSQLiteAdapter,
725
+ stepIncrementalVacuumToCompletion,
726
+ createWebSQLiteAdapter
727
+ };