@rulvar/store-sqlite 1.44.0 → 1.44.1

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
@@ -27,6 +27,17 @@ declare class SqliteStore implements MetaLookupStore, LeasableStore {
27
27
  private liveLease;
28
28
  /** Rejects unless `lease` is the CURRENT live lease for its run. */
29
29
  private assertFencing;
30
+ /**
31
+ * Runs the fence check and the guarded mutation as ONE immediate
32
+ * transaction, the same shape acquire already uses: BEGIN IMMEDIATE
33
+ * takes the write lock BEFORE the check reads the lease row, so a
34
+ * competing takeover cannot land between the check and the mutation
35
+ * (it serializes behind the commit and the loser sees the final rows).
36
+ * As two autocommit statements, a takeover in that window let a
37
+ * superseded holder mutate live state (fenced-run-state RFC, F3).
38
+ */
39
+ private fenced;
40
+ private insertEntry;
30
41
  append(runId: string, e: JournalEntry, lease?: Lease): Promise<void>;
31
42
  load(runId: string): Promise<JournalEntry[]>;
32
43
  putMeta(m: RunMeta): Promise<void>;
package/dist/index.js CHANGED
@@ -14,7 +14,13 @@ import { ConfigError, JournalOrderViolation, LeaseHeldError, metaMatchesFilter }
14
14
  * through untouched.
15
15
  * - Fencing: the epoch is monotonic per run for the store's lifetime;
16
16
  * an append carrying a stale or released lease rejects with the typed
17
- * LeaseHeldError and the entry never becomes visible.
17
+ * LeaseHeldError and the entry never becomes visible. The fence check
18
+ * and the guarded mutation (append's insert, renew's extension,
19
+ * release's deletion) commit as ONE immediate transaction: checking in
20
+ * one autocommit statement and mutating in the next left a
21
+ * cross-process window where a takeover landing between them let the
22
+ * superseded holder append a visible entry, extend the successor's
23
+ * lease, or delete it outright (the fenced-run-state RFC, finding F3).
18
24
  * - acquire on a held, unexpired lease rejects with LeaseHeldError; the
19
25
  * holder MUST renew at an interval of at most ttl/3; an unrenewed
20
26
  * lease is reclaimable after ttl and reclaiming advances the epoch.
@@ -77,14 +83,42 @@ var SqliteStore = class {
77
83
  const live = this.liveLease(lease.runId);
78
84
  if (live === void 0 || live.owner !== lease.owner || live.epoch !== lease.epoch) throw new LeaseHeldError(`stale fencing epoch for run '${lease.runId}': lease (owner ${lease.owner}, epoch ${lease.epoch}) is not the current holder; the append or renew is rejected and nothing became visible`);
79
85
  }
80
- async append(runId, e, lease) {
81
- if (lease !== void 0) this.assertFencing(lease);
86
+ /**
87
+ * Runs the fence check and the guarded mutation as ONE immediate
88
+ * transaction, the same shape acquire already uses: BEGIN IMMEDIATE
89
+ * takes the write lock BEFORE the check reads the lease row, so a
90
+ * competing takeover cannot land between the check and the mutation
91
+ * (it serializes behind the commit and the loser sees the final rows).
92
+ * As two autocommit statements, a takeover in that window let a
93
+ * superseded holder mutate live state (fenced-run-state RFC, F3).
94
+ */
95
+ fenced(lease, mutate) {
96
+ this.db.exec("BEGIN IMMEDIATE");
97
+ try {
98
+ this.assertFencing(lease);
99
+ mutate();
100
+ this.db.exec("COMMIT");
101
+ } catch (thrown) {
102
+ this.db.exec("ROLLBACK");
103
+ throw thrown;
104
+ }
105
+ }
106
+ insertEntry(runId, e) {
82
107
  if (!Number.isFinite(e.seq)) {
83
108
  this.db.prepare("INSERT INTO entries (run_id, payload) VALUES (?, ?)").run(runId, JSON.stringify(e));
84
109
  return;
85
110
  }
86
111
  if (this.db.prepare("INSERT INTO entries (run_id, payload) SELECT ?, ? WHERE NOT EXISTS (SELECT 1 FROM entries WHERE run_id = ? AND CAST(json_extract(payload, '$.seq') AS INTEGER) >= ?)").run(runId, JSON.stringify(e), runId, e.seq).changes === 0) throw new JournalOrderViolation(`SqliteStore: append of seq ${e.seq} to run '${runId}' is not after the stored tail seq; a concurrent writer raced this journal from a stale tail`);
87
112
  }
113
+ async append(runId, e, lease) {
114
+ if (lease !== void 0) {
115
+ this.fenced(lease, () => {
116
+ this.insertEntry(runId, e);
117
+ });
118
+ return;
119
+ }
120
+ this.insertEntry(runId, e);
121
+ }
88
122
  async load(runId) {
89
123
  return this.db.prepare("SELECT payload FROM entries WHERE run_id = ? ORDER BY id").all(runId).map((row) => JSON.parse(row.payload));
90
124
  }
@@ -154,12 +188,14 @@ var SqliteStore = class {
154
188
  }
155
189
  }
156
190
  async renew(l) {
157
- this.assertFencing(l);
158
- this.db.prepare("UPDATE leases SET expires_at = ? WHERE run_id = ?").run(this.now() + this.ttlMs, l.runId);
191
+ this.fenced(l, () => {
192
+ this.db.prepare("UPDATE leases SET expires_at = ? WHERE run_id = ? AND owner = ? AND epoch = ?").run(this.now() + this.ttlMs, l.runId, l.owner, l.epoch);
193
+ });
159
194
  }
160
195
  async release(l) {
161
- this.assertFencing(l);
162
- this.db.prepare("DELETE FROM leases WHERE run_id = ?").run(l.runId);
196
+ this.fenced(l, () => {
197
+ this.db.prepare("DELETE FROM leases WHERE run_id = ? AND owner = ? AND epoch = ?").run(l.runId, l.owner, l.epoch);
198
+ });
163
199
  }
164
200
  };
165
201
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulvar/store-sqlite",
3
- "version": "1.44.0",
3
+ "version": "1.44.1",
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.44.0"
25
+ "@rulvar/core": "1.44.1"
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.44.0"
31
+ "@rulvar/store-conformance": "1.44.1"
32
32
  },
33
33
  "repository": {
34
34
  "type": "git",