@rulvar/store-sqlite 1.47.0 → 1.48.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/dist/index.d.ts CHANGED
@@ -3,6 +3,16 @@ import { JournalEntry, LeasableStore, Lease, MetaLookupStore, RunFilter, RunMeta
3
3
  //#region src/store.d.ts
4
4
  /** Appendix A interim reference for the sqlite store. */
5
5
  declare const DEFAULT_LEASE_TTL_MS = 6e4;
6
+ /**
7
+ * Total time the constructor keeps retrying its schema bootstrap
8
+ * through SQLITE_BUSY before giving up, so concurrent multi-process
9
+ * construction over one fresh file serializes instead of dying raw.
10
+ * The bound applies ONLY to boot; every runtime contention path keeps
11
+ * the documented fail-fast semantics (busy surfaces immediately). A
12
+ * boot still busy past the bound throws the driver's error: something
13
+ * is wedged, not merely concurrent.
14
+ */
15
+ declare const BOOT_BUSY_TIMEOUT_MS = 5e3;
6
16
  interface SqliteStoreOptions {
7
17
  /** Database file path, or ':memory:' for an in-process store. */
8
18
  path: string;
@@ -103,4 +113,4 @@ declare class SqliteStore implements MetaLookupStore, LeasableStore {
103
113
  release(l: Lease): Promise<void>;
104
114
  }
105
115
  //#endregion
106
- export { DEFAULT_LEASE_TTL_MS, SqliteStore, type SqliteStoreOptions, type SqliteTranscriptStore };
116
+ export { BOOT_BUSY_TIMEOUT_MS, DEFAULT_LEASE_TTL_MS, SqliteStore, type SqliteStoreOptions, type SqliteTranscriptStore };
package/dist/index.js CHANGED
@@ -35,11 +35,43 @@ import { ConfigError, JournalOrderViolation, LeaseHeldError, metaMatchesFilter }
35
35
  * - acquire on a held, unexpired lease rejects with LeaseHeldError; the
36
36
  * holder MUST renew at an interval of at most ttl/3; an unrenewed
37
37
  * lease is reclaimable after ttl and reclaiming advances the epoch.
38
+ * - Concurrent boot: constructing over one shared file from several
39
+ * processes at once serializes on a boot-scoped busy timeout
40
+ * (BOOT_BUSY_TIMEOUT_MS) instead of dying raw in the schema
41
+ * bootstrap; runtime contention stays fail fast (busy 0).
38
42
  * - The lease ttl default is the Appendix A interim reference for this
39
43
  * store (60000 ms; the committed value is decided before M8).
40
44
  */
41
45
  /** Appendix A interim reference for the sqlite store. */
42
46
  const DEFAULT_LEASE_TTL_MS = 6e4;
47
+ /**
48
+ * Total time the constructor keeps retrying its schema bootstrap
49
+ * through SQLITE_BUSY before giving up, so concurrent multi-process
50
+ * construction over one fresh file serializes instead of dying raw.
51
+ * The bound applies ONLY to boot; every runtime contention path keeps
52
+ * the documented fail-fast semantics (busy surfaces immediately). A
53
+ * boot still busy past the bound throws the driver's error: something
54
+ * is wedged, not merely concurrent.
55
+ */
56
+ const BOOT_BUSY_TIMEOUT_MS = 5e3;
57
+ /**
58
+ * The SQLITE_BUSY family, the one boot error class worth a bounded
59
+ * retry. The driver reports EXTENDED result codes (a concurrent boot
60
+ * also yields SQLITE_BUSY_RECOVERY 261 while a sibling process is
61
+ * recovering the fresh WAL), so the primary code is the low byte.
62
+ */
63
+ function isSqliteBusy(thrown) {
64
+ const errcode = thrown?.errcode;
65
+ return errcode !== void 0 && (errcode & 255) === 5;
66
+ }
67
+ /**
68
+ * Synchronous bounded sleep for the boot retry loop (the constructor is
69
+ * synchronous by SPI shape): Atomics.wait blocks the thread for the few
70
+ * milliseconds another process needs to finish the shared bootstrap.
71
+ */
72
+ function sleepSync(ms) {
73
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
74
+ }
43
75
  const wallClock = Date.now.bind(globalThis);
44
76
  var SqliteStore = class {
45
77
  /**
@@ -60,7 +92,7 @@ var SqliteStore = class {
60
92
  this.db = new DatabaseSync(options.path);
61
93
  this.ttlMs = ttlMs;
62
94
  this.now = options.now ?? wallClock;
63
- this.db.exec(`
95
+ const schema = `
64
96
  PRAGMA journal_mode = WAL;
65
97
  CREATE TABLE IF NOT EXISTS entries (
66
98
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -94,7 +126,15 @@ var SqliteStore = class {
94
126
  data BLOB NOT NULL
95
127
  );
96
128
  CREATE INDEX IF NOT EXISTS blobs_by_run ON blobs (run_id);
97
- `);
129
+ `;
130
+ const bootDeadline = wallClock() + BOOT_BUSY_TIMEOUT_MS;
131
+ for (let attempt = 0;; attempt += 1) try {
132
+ this.db.exec(schema);
133
+ break;
134
+ } catch (thrown) {
135
+ if (!isSqliteBusy(thrown) || wallClock() > bootDeadline) throw thrown;
136
+ sleepSync(2 + attempt % 7);
137
+ }
98
138
  }
99
139
  close() {
100
140
  this.db.close();
@@ -314,4 +354,4 @@ var SqliteStore = class {
314
354
  }
315
355
  };
316
356
  //#endregion
317
- export { DEFAULT_LEASE_TTL_MS, SqliteStore };
357
+ export { BOOT_BUSY_TIMEOUT_MS, DEFAULT_LEASE_TTL_MS, SqliteStore };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulvar/store-sqlite",
3
- "version": "1.47.0",
3
+ "version": "1.48.0",
4
4
  "description": "Rulvar SQLite store implementing JournalStore and LeasableStore with a fencing epoch.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -22,13 +22,13 @@
22
22
  "access": "public"
23
23
  },
24
24
  "dependencies": {
25
- "@rulvar/core": "1.47.0"
25
+ "@rulvar/core": "1.48.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^22.20.0",
29
29
  "tsdown": "^0.22.3",
30
30
  "typescript": "~6.0.3",
31
- "@rulvar/store-conformance": "1.47.0"
31
+ "@rulvar/store-conformance": "1.48.0"
32
32
  },
33
33
  "repository": {
34
34
  "type": "git",