@rulvar/store-sqlite 1.45.0 → 1.47.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
@@ -1,4 +1,4 @@
1
- import { JournalEntry, LeasableStore, Lease, MetaLookupStore, RunFilter, RunMeta } from "@rulvar/core";
1
+ import { JournalEntry, LeasableStore, Lease, MetaLookupStore, RunFilter, RunMeta, TranscriptStore } from "@rulvar/core";
2
2
 
3
3
  //#region src/store.d.ts
4
4
  /** Appendix A interim reference for the sqlite store. */
@@ -18,6 +18,17 @@ interface SqliteStoreOptions {
18
18
  /** Injectable clock for lease-expiry tests. */
19
19
  now?: () => number;
20
20
  }
21
+ /**
22
+ * The fenced transcript twin over a SqliteStore database (the fenced
23
+ * run state RFC, F2): a TranscriptStore that declares `fencedWrites`
24
+ * because its blobs live in the SAME database as the lease rows, giving
25
+ * the fence check and the blob mutation one transactional domain.
26
+ * Obtain it from {@link SqliteStore.transcripts}; its lifetime is the
27
+ * owning store's (one shared connection, one `close()`).
28
+ */
29
+ interface SqliteTranscriptStore extends TranscriptStore {
30
+ readonly fencedWrites: true;
31
+ }
21
32
  declare class SqliteStore implements MetaLookupStore, LeasableStore {
22
33
  /**
23
34
  * The fenced writes promise (fenced run state RFC, phase 2): every
@@ -30,6 +41,7 @@ declare class SqliteStore implements MetaLookupStore, LeasableStore {
30
41
  private readonly db;
31
42
  private readonly ttlMs;
32
43
  private readonly now;
44
+ private transcriptTwin;
33
45
  constructor(options: SqliteStoreOptions);
34
46
  close(): void;
35
47
  private liveLease;
@@ -62,6 +74,25 @@ declare class SqliteStore implements MetaLookupStore, LeasableStore {
62
74
  private deleteRows;
63
75
  delete(runId: string, lease?: Lease): Promise<void>;
64
76
  /**
77
+ * The fenced transcript twin (fenced run state RFC, F2): a
78
+ * TranscriptStore whose blobs live in THIS store's database, beside
79
+ * the lease rows, so a lease-carrying put or delete verifies the
80
+ * current holder of the run the ref's leading path segment names
81
+ * atomically with the blob mutation, in the same one-immediate-
82
+ * transaction shape as the journal side. Sharing the connection is
83
+ * what makes the capability implementable at all (a blob write and a
84
+ * lease check in different domains cannot commit as one unit; with
85
+ * ':memory:' a separate connection would not even see the leases) and
86
+ * keeps one close() lifecycle. Wire it as the engine's transcript
87
+ * store next to this store as the journal: over the pair every
88
+ * durable run mutation is fenced, which is what
89
+ * `assertFencedWrites({ journal, transcripts })` verifies. The blob
90
+ * cascade of `deleteRun`/`pruneRun` stays ENGINE-side, exactly as the
91
+ * TranscriptStore contract says; the journal-side `delete(runId)`
92
+ * never touches blob rows.
93
+ */
94
+ transcripts(): SqliteTranscriptStore;
95
+ /**
65
96
  * TTL introspection (the LeasableStore optional capability): lets
66
97
  * createWorker verify at construction that its renew cadence matches
67
98
  * this store's expiry instead of trusting two config sources to agree.
@@ -72,4 +103,4 @@ declare class SqliteStore implements MetaLookupStore, LeasableStore {
72
103
  release(l: Lease): Promise<void>;
73
104
  }
74
105
  //#endregion
75
- export { DEFAULT_LEASE_TTL_MS, SqliteStore, type SqliteStoreOptions };
106
+ export { DEFAULT_LEASE_TTL_MS, SqliteStore, type SqliteStoreOptions, type SqliteTranscriptStore };
package/dist/index.js CHANGED
@@ -27,6 +27,11 @@ import { ConfigError, JournalOrderViolation, LeaseHeldError, metaMatchesFilter }
27
27
  * lease's runId to BE the mutated run, so a superseded worker can
28
28
  * neither strand a run through a stale terminal meta write (F1) nor
29
29
  * delete live run state (F4).
30
+ * - Fenced transcripts (the RFC's F2): transcripts() returns the
31
+ * TranscriptStore twin over this same database, so checkpoint,
32
+ * compaction, worktree patch, and workflow source blobs are fenced
33
+ * under the identical atomic rule, and a superseded segment's late
34
+ * checkpoint save cannot regress the blob a later boot decodes.
30
35
  * - acquire on a held, unexpired lease rejects with LeaseHeldError; the
31
36
  * holder MUST renew at an interval of at most ttl/3; an unrenewed
32
37
  * lease is reclaimable after ttl and reclaiming advances the epoch.
@@ -48,6 +53,7 @@ var SqliteStore = class {
48
53
  db;
49
54
  ttlMs;
50
55
  now;
56
+ transcriptTwin;
51
57
  constructor(options) {
52
58
  const ttlMs = options.ttlMs ?? 6e4;
53
59
  if (!Number.isInteger(ttlMs) || ttlMs < 1 || ttlMs > 2147483647) throw new ConfigError(`SqliteStoreOptions.ttlMs must be an integer between 1 and 2147483647 ms (workers renew on Node timers at ttl/3); got ${String(ttlMs)}`);
@@ -82,6 +88,12 @@ var SqliteStore = class {
82
88
  run_id TEXT PRIMARY KEY,
83
89
  epoch INTEGER NOT NULL
84
90
  );
91
+ CREATE TABLE IF NOT EXISTS blobs (
92
+ ref TEXT PRIMARY KEY,
93
+ run_id TEXT NOT NULL,
94
+ data BLOB NOT NULL
95
+ );
96
+ CREATE INDEX IF NOT EXISTS blobs_by_run ON blobs (run_id);
85
97
  `);
86
98
  }
87
99
  close() {
@@ -205,6 +217,65 @@ var SqliteStore = class {
205
217
  }
206
218
  }
207
219
  /**
220
+ * The fenced transcript twin (fenced run state RFC, F2): a
221
+ * TranscriptStore whose blobs live in THIS store's database, beside
222
+ * the lease rows, so a lease-carrying put or delete verifies the
223
+ * current holder of the run the ref's leading path segment names
224
+ * atomically with the blob mutation, in the same one-immediate-
225
+ * transaction shape as the journal side. Sharing the connection is
226
+ * what makes the capability implementable at all (a blob write and a
227
+ * lease check in different domains cannot commit as one unit; with
228
+ * ':memory:' a separate connection would not even see the leases) and
229
+ * keeps one close() lifecycle. Wire it as the engine's transcript
230
+ * store next to this store as the journal: over the pair every
231
+ * durable run mutation is fenced, which is what
232
+ * `assertFencedWrites({ journal, transcripts })` verifies. The blob
233
+ * cascade of `deleteRun`/`pruneRun` stays ENGINE-side, exactly as the
234
+ * TranscriptStore contract says; the journal-side `delete(runId)`
235
+ * never touches blob rows.
236
+ */
237
+ transcripts() {
238
+ if (this.transcriptTwin !== void 0) return this.transcriptTwin;
239
+ const runOf = (ref) => ref.split("/", 1)[0] ?? ref;
240
+ const upsertBlob = (ref, blob) => {
241
+ this.db.prepare("INSERT INTO blobs (ref, run_id, data) VALUES (?, ?, ?) ON CONFLICT(ref) DO UPDATE SET run_id = excluded.run_id, data = excluded.data").run(ref, runOf(ref), blob);
242
+ };
243
+ const deleteBlob = (ref) => {
244
+ this.db.prepare("DELETE FROM blobs WHERE ref = ?").run(ref);
245
+ };
246
+ this.transcriptTwin = {
247
+ fencedWrites: true,
248
+ put: async (ref, blob, lease) => {
249
+ if (lease !== void 0) {
250
+ this.requireRunMatch(lease, runOf(ref), "transcript write");
251
+ this.fenced(lease, () => {
252
+ upsertBlob(ref, blob);
253
+ });
254
+ return;
255
+ }
256
+ upsertBlob(ref, blob);
257
+ },
258
+ get: async (ref) => {
259
+ const row = this.db.prepare("SELECT data FROM blobs WHERE ref = ?").get(ref);
260
+ return row === void 0 ? null : new Uint8Array(row.data);
261
+ },
262
+ list: async (runId) => {
263
+ return this.db.prepare("SELECT ref FROM blobs WHERE run_id = ? AND ref <> run_id ORDER BY ref").all(runId).map((row) => row.ref);
264
+ },
265
+ delete: async (ref, lease) => {
266
+ if (lease !== void 0) {
267
+ this.requireRunMatch(lease, runOf(ref), "transcript deletion");
268
+ this.fenced(lease, () => {
269
+ deleteBlob(ref);
270
+ });
271
+ return;
272
+ }
273
+ deleteBlob(ref);
274
+ }
275
+ };
276
+ return this.transcriptTwin;
277
+ }
278
+ /**
208
279
  * TTL introspection (the LeasableStore optional capability): lets
209
280
  * createWorker verify at construction that its renew cadence matches
210
281
  * this store's expiry instead of trusting two config sources to agree.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulvar/store-sqlite",
3
- "version": "1.45.0",
3
+ "version": "1.47.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.45.0"
25
+ "@rulvar/core": "1.47.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.45.0"
31
+ "@rulvar/store-conformance": "1.47.0"
32
32
  },
33
33
  "repository": {
34
34
  "type": "git",