@rulvar/store-sqlite 1.44.1 → 1.46.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 +52 -4
- package/dist/index.js +121 -6
- package/package.json +3 -3
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,13 +18,40 @@ 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 {
|
|
33
|
+
/**
|
|
34
|
+
* The fenced writes promise (fenced run state RFC, phase 2): every
|
|
35
|
+
* lease-carrying mutation of this store (append, putMeta, delete)
|
|
36
|
+
* verifies the lease is the current holder FOR THE MUTATED RUN,
|
|
37
|
+
* atomically with the mutation, and rejects stale or mismatched
|
|
38
|
+
* holders with the typed LeaseHeldError leaving nothing changed.
|
|
39
|
+
*/
|
|
40
|
+
readonly fencedWrites = true;
|
|
22
41
|
private readonly db;
|
|
23
42
|
private readonly ttlMs;
|
|
24
43
|
private readonly now;
|
|
44
|
+
private transcriptTwin;
|
|
25
45
|
constructor(options: SqliteStoreOptions);
|
|
26
46
|
close(): void;
|
|
27
47
|
private liveLease;
|
|
48
|
+
/**
|
|
49
|
+
* A lease fences exactly the run it names: guarding a mutation of a
|
|
50
|
+
* DIFFERENT run with it would pass the holder check while touching
|
|
51
|
+
* state the lease never protected, so the mismatch rejects typed
|
|
52
|
+
* before any check runs.
|
|
53
|
+
*/
|
|
54
|
+
private requireRunMatch;
|
|
28
55
|
/** Rejects unless `lease` is the CURRENT live lease for its run. */
|
|
29
56
|
private assertFencing;
|
|
30
57
|
/**
|
|
@@ -40,10 +67,31 @@ declare class SqliteStore implements MetaLookupStore, LeasableStore {
|
|
|
40
67
|
private insertEntry;
|
|
41
68
|
append(runId: string, e: JournalEntry, lease?: Lease): Promise<void>;
|
|
42
69
|
load(runId: string): Promise<JournalEntry[]>;
|
|
43
|
-
|
|
70
|
+
private upsertMeta;
|
|
71
|
+
putMeta(m: RunMeta, lease?: Lease): Promise<void>;
|
|
44
72
|
getMeta(runId: string): Promise<RunMeta | undefined>;
|
|
45
73
|
listRuns(f?: RunFilter): Promise<RunMeta[]>;
|
|
46
|
-
|
|
74
|
+
private deleteRows;
|
|
75
|
+
delete(runId: string, lease?: Lease): Promise<void>;
|
|
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;
|
|
47
95
|
/**
|
|
48
96
|
* TTL introspection (the LeasableStore optional capability): lets
|
|
49
97
|
* createWorker verify at construction that its renew cadence matches
|
|
@@ -55,4 +103,4 @@ declare class SqliteStore implements MetaLookupStore, LeasableStore {
|
|
|
55
103
|
release(l: Lease): Promise<void>;
|
|
56
104
|
}
|
|
57
105
|
//#endregion
|
|
58
|
-
export { DEFAULT_LEASE_TTL_MS, SqliteStore, type SqliteStoreOptions };
|
|
106
|
+
export { DEFAULT_LEASE_TTL_MS, SqliteStore, type SqliteStoreOptions, type SqliteTranscriptStore };
|
package/dist/index.js
CHANGED
|
@@ -21,6 +21,17 @@ import { ConfigError, JournalOrderViolation, LeaseHeldError, metaMatchesFilter }
|
|
|
21
21
|
* cross-process window where a takeover landing between them let the
|
|
22
22
|
* superseded holder append a visible entry, extend the successor's
|
|
23
23
|
* lease, or delete it outright (the fenced-run-state RFC, finding F3).
|
|
24
|
+
* - Fenced writes (`fencedWrites: true`, the RFC's phase 2): putMeta
|
|
25
|
+
* and delete accept the same optional lease under the same atomic
|
|
26
|
+
* rule, and every lease-guarded mutation additionally requires the
|
|
27
|
+
* lease's runId to BE the mutated run, so a superseded worker can
|
|
28
|
+
* neither strand a run through a stale terminal meta write (F1) nor
|
|
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.
|
|
24
35
|
* - acquire on a held, unexpired lease rejects with LeaseHeldError; the
|
|
25
36
|
* holder MUST renew at an interval of at most ttl/3; an unrenewed
|
|
26
37
|
* lease is reclaimable after ttl and reclaiming advances the epoch.
|
|
@@ -31,9 +42,18 @@ import { ConfigError, JournalOrderViolation, LeaseHeldError, metaMatchesFilter }
|
|
|
31
42
|
const DEFAULT_LEASE_TTL_MS = 6e4;
|
|
32
43
|
const wallClock = Date.now.bind(globalThis);
|
|
33
44
|
var SqliteStore = class {
|
|
45
|
+
/**
|
|
46
|
+
* The fenced writes promise (fenced run state RFC, phase 2): every
|
|
47
|
+
* lease-carrying mutation of this store (append, putMeta, delete)
|
|
48
|
+
* verifies the lease is the current holder FOR THE MUTATED RUN,
|
|
49
|
+
* atomically with the mutation, and rejects stale or mismatched
|
|
50
|
+
* holders with the typed LeaseHeldError leaving nothing changed.
|
|
51
|
+
*/
|
|
52
|
+
fencedWrites = true;
|
|
34
53
|
db;
|
|
35
54
|
ttlMs;
|
|
36
55
|
now;
|
|
56
|
+
transcriptTwin;
|
|
37
57
|
constructor(options) {
|
|
38
58
|
const ttlMs = options.ttlMs ?? 6e4;
|
|
39
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)}`);
|
|
@@ -68,6 +88,12 @@ var SqliteStore = class {
|
|
|
68
88
|
run_id TEXT PRIMARY KEY,
|
|
69
89
|
epoch INTEGER NOT NULL
|
|
70
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);
|
|
71
97
|
`);
|
|
72
98
|
}
|
|
73
99
|
close() {
|
|
@@ -78,6 +104,15 @@ var SqliteStore = class {
|
|
|
78
104
|
if (row === void 0 || row.expires_at <= this.now()) return;
|
|
79
105
|
return row;
|
|
80
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* A lease fences exactly the run it names: guarding a mutation of a
|
|
109
|
+
* DIFFERENT run with it would pass the holder check while touching
|
|
110
|
+
* state the lease never protected, so the mismatch rejects typed
|
|
111
|
+
* before any check runs.
|
|
112
|
+
*/
|
|
113
|
+
requireRunMatch(lease, runId, mutation) {
|
|
114
|
+
if (lease.runId !== runId) throw new LeaseHeldError(`lease for run '${lease.runId}' (owner ${lease.owner}, epoch ${lease.epoch}) cannot guard a ${mutation} of run '${runId}'; the mutation is rejected and nothing changed`);
|
|
115
|
+
}
|
|
81
116
|
/** Rejects unless `lease` is the CURRENT live lease for its run. */
|
|
82
117
|
assertFencing(lease) {
|
|
83
118
|
const live = this.liveLease(lease.runId);
|
|
@@ -112,6 +147,7 @@ var SqliteStore = class {
|
|
|
112
147
|
}
|
|
113
148
|
async append(runId, e, lease) {
|
|
114
149
|
if (lease !== void 0) {
|
|
150
|
+
this.requireRunMatch(lease, runId, "journal append");
|
|
115
151
|
this.fenced(lease, () => {
|
|
116
152
|
this.insertEntry(runId, e);
|
|
117
153
|
});
|
|
@@ -122,9 +158,19 @@ var SqliteStore = class {
|
|
|
122
158
|
async load(runId) {
|
|
123
159
|
return this.db.prepare("SELECT payload FROM entries WHERE run_id = ? ORDER BY id").all(runId).map((row) => JSON.parse(row.payload));
|
|
124
160
|
}
|
|
125
|
-
|
|
161
|
+
upsertMeta(m) {
|
|
126
162
|
this.db.prepare("INSERT INTO meta (run_id, payload) VALUES (?, ?) ON CONFLICT(run_id) DO UPDATE SET payload = excluded.payload").run(m.runId, JSON.stringify(m));
|
|
127
163
|
}
|
|
164
|
+
async putMeta(m, lease) {
|
|
165
|
+
if (lease !== void 0) {
|
|
166
|
+
this.requireRunMatch(lease, m.runId, "meta write");
|
|
167
|
+
this.fenced(lease, () => {
|
|
168
|
+
this.upsertMeta(m);
|
|
169
|
+
});
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
this.upsertMeta(m);
|
|
173
|
+
}
|
|
128
174
|
async getMeta(runId) {
|
|
129
175
|
const row = this.db.prepare("SELECT payload FROM meta WHERE run_id = ?").get(runId);
|
|
130
176
|
return row === void 0 ? void 0 : JSON.parse(row.payload);
|
|
@@ -147,13 +193,23 @@ var SqliteStore = class {
|
|
|
147
193
|
const sql = "SELECT payload FROM meta" + (where.length === 0 ? "" : ` WHERE ${where.join(" AND ")}`) + " ORDER BY run_id";
|
|
148
194
|
return this.db.prepare(sql).all(...params).map((row) => JSON.parse(row.payload)).filter((meta) => metaMatchesFilter(meta, f));
|
|
149
195
|
}
|
|
150
|
-
|
|
196
|
+
deleteRows(runId) {
|
|
197
|
+
this.db.prepare("DELETE FROM entries WHERE run_id = ?").run(runId);
|
|
198
|
+
this.db.prepare("DELETE FROM meta WHERE run_id = ?").run(runId);
|
|
199
|
+
this.db.prepare("DELETE FROM leases WHERE run_id = ?").run(runId);
|
|
200
|
+
this.db.prepare("DELETE FROM epochs WHERE run_id = ?").run(runId);
|
|
201
|
+
}
|
|
202
|
+
async delete(runId, lease) {
|
|
203
|
+
if (lease !== void 0) {
|
|
204
|
+
this.requireRunMatch(lease, runId, "run deletion");
|
|
205
|
+
this.fenced(lease, () => {
|
|
206
|
+
this.deleteRows(runId);
|
|
207
|
+
});
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
151
210
|
this.db.exec("BEGIN IMMEDIATE");
|
|
152
211
|
try {
|
|
153
|
-
this.
|
|
154
|
-
this.db.prepare("DELETE FROM meta WHERE run_id = ?").run(runId);
|
|
155
|
-
this.db.prepare("DELETE FROM leases WHERE run_id = ?").run(runId);
|
|
156
|
-
this.db.prepare("DELETE FROM epochs WHERE run_id = ?").run(runId);
|
|
212
|
+
this.deleteRows(runId);
|
|
157
213
|
this.db.exec("COMMIT");
|
|
158
214
|
} catch (thrown) {
|
|
159
215
|
this.db.exec("ROLLBACK");
|
|
@@ -161,6 +217,65 @@ var SqliteStore = class {
|
|
|
161
217
|
}
|
|
162
218
|
}
|
|
163
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
|
+
/**
|
|
164
279
|
* TTL introspection (the LeasableStore optional capability): lets
|
|
165
280
|
* createWorker verify at construction that its renew cadence matches
|
|
166
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.
|
|
3
|
+
"version": "1.46.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.
|
|
25
|
+
"@rulvar/core": "1.46.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.
|
|
31
|
+
"@rulvar/store-conformance": "1.46.0"
|
|
32
32
|
},
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|