@seanhogg/builderforce-memory 2026.9.34 → 2026.10.2

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 (97) hide show
  1. package/dist/errors/SSMError.d.ts +1 -1
  2. package/dist/errors/SSMError.d.ts.map +1 -1
  3. package/dist/errors/SSMError.js.map +1 -1
  4. package/dist/handoff/index.d.ts +5 -5
  5. package/dist/handoff/index.js +5 -5
  6. package/dist/handoff/note.d.ts +1 -1
  7. package/dist/handoff/note.js +1 -1
  8. package/dist/handoff/pack.d.ts +13 -4
  9. package/dist/handoff/pack.d.ts.map +1 -1
  10. package/dist/handoff/pack.js +31 -10
  11. package/dist/handoff/pack.js.map +1 -1
  12. package/dist/memory/MemoryStore.d.ts +120 -181
  13. package/dist/memory/MemoryStore.d.ts.map +1 -1
  14. package/dist/memory/MemoryStore.js +201 -389
  15. package/dist/memory/MemoryStore.js.map +1 -1
  16. package/dist/memory/SemanticRecall.d.ts +64 -0
  17. package/dist/memory/SemanticRecall.d.ts.map +1 -0
  18. package/dist/memory/SemanticRecall.js +188 -0
  19. package/dist/memory/SemanticRecall.js.map +1 -0
  20. package/dist/memory/records.d.ts +26 -0
  21. package/dist/memory/records.d.ts.map +1 -0
  22. package/dist/memory/records.js +71 -0
  23. package/dist/memory/records.js.map +1 -0
  24. package/dist/memory/ttlSweeper.d.ts +12 -0
  25. package/dist/memory/ttlSweeper.d.ts.map +1 -0
  26. package/dist/memory/ttlSweeper.js +28 -0
  27. package/dist/memory/ttlSweeper.js.map +1 -0
  28. package/dist/memory/types.d.ts +142 -0
  29. package/dist/memory/types.d.ts.map +1 -0
  30. package/dist/memory/types.js +6 -0
  31. package/dist/memory/types.js.map +1 -0
  32. package/dist/storage/ScopedFactRepository.d.ts +156 -0
  33. package/dist/storage/ScopedFactRepository.d.ts.map +1 -0
  34. package/dist/storage/ScopedFactRepository.js +278 -0
  35. package/dist/storage/ScopedFactRepository.js.map +1 -0
  36. package/dist/storage/factImport.d.ts +21 -0
  37. package/dist/storage/factImport.d.ts.map +1 -0
  38. package/dist/storage/factImport.js +61 -0
  39. package/dist/storage/factImport.js.map +1 -0
  40. package/dist/storage/factMutations.d.ts +46 -0
  41. package/dist/storage/factMutations.d.ts.map +1 -0
  42. package/dist/storage/factMutations.js +73 -0
  43. package/dist/storage/factMutations.js.map +1 -0
  44. package/dist/storage/factQueries.d.ts +16 -0
  45. package/dist/storage/factQueries.d.ts.map +1 -0
  46. package/dist/storage/factQueries.js +36 -0
  47. package/dist/storage/factQueries.js.map +1 -0
  48. package/dist/storage/factRetention.d.ts +30 -0
  49. package/dist/storage/factRetention.d.ts.map +1 -0
  50. package/dist/storage/factRetention.js +65 -0
  51. package/dist/storage/factRetention.js.map +1 -0
  52. package/dist/storage/fileLock.d.ts +56 -0
  53. package/dist/storage/fileLock.d.ts.map +1 -0
  54. package/dist/storage/fileLock.js +138 -0
  55. package/dist/storage/fileLock.js.map +1 -0
  56. package/dist/storage/fileTransaction.d.ts +33 -0
  57. package/dist/storage/fileTransaction.d.ts.map +1 -0
  58. package/dist/storage/fileTransaction.js +108 -0
  59. package/dist/storage/fileTransaction.js.map +1 -0
  60. package/dist/storage/idbAdapter.d.ts +95 -0
  61. package/dist/storage/idbAdapter.d.ts.map +1 -0
  62. package/dist/storage/idbAdapter.js +311 -0
  63. package/dist/storage/idbAdapter.js.map +1 -0
  64. package/dist/storage/nodeFileAdapter.d.ts +127 -0
  65. package/dist/storage/nodeFileAdapter.d.ts.map +1 -0
  66. package/dist/storage/nodeFileAdapter.js +275 -0
  67. package/dist/storage/nodeFileAdapter.js.map +1 -0
  68. package/dist/storage/syncJournal.d.ts +19 -0
  69. package/dist/storage/syncJournal.d.ts.map +1 -0
  70. package/dist/storage/syncJournal.js +46 -0
  71. package/dist/storage/syncJournal.js.map +1 -0
  72. package/dist/storage/types.d.ts +207 -0
  73. package/dist/storage/types.d.ts.map +1 -0
  74. package/dist/storage/types.js +90 -0
  75. package/dist/storage/types.js.map +1 -0
  76. package/package.json +3 -3
  77. package/src/errors/SSMError.ts +1 -0
  78. package/src/handoff/handoff.test.ts +52 -2
  79. package/src/handoff/index.ts +5 -5
  80. package/src/handoff/note.ts +1 -1
  81. package/src/handoff/pack.ts +32 -9
  82. package/src/memory/MemoryStore.ts +245 -525
  83. package/src/memory/SemanticRecall.ts +204 -0
  84. package/src/memory/records.ts +79 -0
  85. package/src/memory/ttlSweeper.ts +29 -0
  86. package/src/memory/types.ts +149 -0
  87. package/src/storage/ScopedFactRepository.ts +373 -0
  88. package/src/storage/factImport.ts +73 -0
  89. package/src/storage/factMutations.ts +106 -0
  90. package/src/storage/factQueries.ts +41 -0
  91. package/src/storage/factRetention.ts +82 -0
  92. package/src/storage/fileLock.ts +162 -0
  93. package/src/storage/fileTransaction.ts +116 -0
  94. package/src/storage/idbAdapter.ts +373 -0
  95. package/src/storage/nodeFileAdapter.ts +331 -0
  96. package/src/storage/syncJournal.ts +47 -0
  97. package/src/storage/types.ts +262 -0
@@ -0,0 +1,95 @@
1
+ /**
2
+ * storage/idbAdapter.ts – Durable browser/IndexedDB implementation of the
3
+ * storage port.
4
+ *
5
+ * Two properties the legacy MemoryStore write helpers did NOT have (M02):
6
+ * 1. `transact()` resolves only on `tx.oncomplete` — the durable commit point —
7
+ * and rejects on `tx.onabort`. Request-level `onsuccess` is used internally
8
+ * to chain operations, never to signal durability.
9
+ * 2. One `transact()` call is one unit of work spanning facts, tombstones,
10
+ * journal and meta, so a fact and its sync journal entry commit together.
11
+ *
12
+ * Schema (database version 3):
13
+ * - 'facts' keyPath 'id' — StoredFactRecord (scoped composite id)
14
+ * index 'byKey' on 'key' — key lookup without a scan
15
+ * index 'byTag' on 'tags' multiEntry — tag lookup without a scan
16
+ * - 'tombstones' keyPath 'id' — TombstoneRecord
17
+ * - 'journal' keyPath 'opId' — JournalEntry (the sync journal)
18
+ * - 'meta' keyPath 'key' — MetaCounters (durable seq / revision / opSeq)
19
+ * - 'weights' (out-of-line) — legacy weight blob store, retained
20
+ *
21
+ * Version 3 adds the key and tag indexes only. Existing fact records, including
22
+ * their timestamps and TTLs, are not rewritten.
23
+ *
24
+ * Version-1 databases (facts keyed by the raw key, no journal) are migrated in
25
+ * the upgrade transaction: records are copied to the new 'id'-keyed store with
26
+ * the unbound scope, their seq preserved, revision 1, and no pending sync state.
27
+ *
28
+ * Cross-context consistency (M05): when BroadcastChannel is available the
29
+ * adapter publishes a change note after every committed write and delivers
30
+ * foreign notes to subscribers, so sibling tabs/windows invalidate their caches
31
+ * instead of serving stale recalls. The channel opens lazily — on the first
32
+ * publish, or once the database is open for a subscriber — and is `unref()`ed
33
+ * where supported (Node), so a store that is never closed cannot keep the
34
+ * process alive.
35
+ */
36
+ import { type ConsistencyNote, type FactStoragePort, type MetaCounters, type StorageStoreName, type StorageTransaction } from './types.js';
37
+ declare const FACTS_STORE = "facts";
38
+ declare const TOMBSTONES_STORE = "tombstones";
39
+ declare const JOURNAL_STORE = "journal";
40
+ declare const META_STORE = "meta";
41
+ declare const WEIGHTS_STORE = "weights";
42
+ declare const DB_VERSION = 3;
43
+ declare const ALL_STORES: StorageStoreName[];
44
+ export interface IdbStorageAdapterOptions {
45
+ /** IDBFactory override (tests, non-standard hosts). NOT a durability shim. */
46
+ idbFactory?: IDBFactory;
47
+ /** Identity of this context for cross-context notes. Default: random. */
48
+ contextId?: string;
49
+ /** BroadcastChannel override (tests). Default: global, when present. */
50
+ channelFactory?: (name: string) => BroadcastChannelLike | undefined;
51
+ }
52
+ /** Minimal BroadcastChannel surface (Node's worker_threads channel satisfies it too). */
53
+ export interface BroadcastChannelLike {
54
+ postMessage(message: unknown): void;
55
+ close(): void;
56
+ onmessage: ((event: {
57
+ data: unknown;
58
+ }) => void) | null;
59
+ /** Node only: stop this channel from keeping the event loop alive. */
60
+ unref?(): void;
61
+ }
62
+ export declare class IdbStorageAdapter implements FactStoragePort {
63
+ private readonly _dbName;
64
+ readonly kind: "indexedDB";
65
+ private readonly _idb;
66
+ private readonly _contextId;
67
+ private readonly _channelFactory;
68
+ private _db;
69
+ private _channel;
70
+ private _channelFailed;
71
+ private readonly _listeners;
72
+ constructor(_dbName: string, opts?: IdbStorageAdapterOptions);
73
+ /** Identity of this context, stamped onto change notes and opIds. */
74
+ get contextId(): string;
75
+ private _open;
76
+ /**
77
+ * One unit of work: runs `fn` inside a single IndexedDB transaction and
78
+ * resolves ONLY on `tx.oncomplete`. If `fn` throws, the transaction is
79
+ * aborted so no partial writes survive; if the browser aborts the
80
+ * transaction (quota, corruption, crash mid-commit) the promise rejects.
81
+ */
82
+ transact<T>(stores: StorageStoreName[], mode: 'readonly' | 'readwrite', fn: (tx: StorageTransaction) => Promise<T> | T): Promise<T>;
83
+ publish(note: ConsistencyNote): void;
84
+ /**
85
+ * Registers `listener` for foreign change notes. The channel itself opens
86
+ * when the database does (or on the first publish), not here: subscribing
87
+ * from a constructor must not open a global channel.
88
+ */
89
+ subscribe(listener: (note: ConsistencyNote) => void): () => void;
90
+ private _ensureChannel;
91
+ close(): void;
92
+ }
93
+ export { ALL_STORES, FACTS_STORE, TOMBSTONES_STORE, JOURNAL_STORE, META_STORE, WEIGHTS_STORE, DB_VERSION };
94
+ export type { MetaCounters };
95
+ //# sourceMappingURL=idbAdapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idbAdapter.d.ts","sourceRoot":"","sources":["../../src/storage/idbAdapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,EAKH,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EAE1B,MAAM,YAAY,CAAC;AAEpB,QAAA,MAAM,WAAW,UAAe,CAAC;AACjC,QAAA,MAAM,gBAAgB,eAAe,CAAC;AACtC,QAAA,MAAM,aAAa,YAAe,CAAC;AACnC,QAAA,MAAM,UAAU,SAAe,CAAC;AAChC,QAAA,MAAM,aAAa,YAAe,CAAC;AACnC,QAAA,MAAM,UAAU,IAAU,CAAC;AAE3B,QAAA,MAAM,UAAU,EAAE,gBAAgB,EAA+D,CAAC;AAElG,MAAM,WAAW,wBAAwB;IACrC,8EAA8E;IAC9E,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,yEAAyE;IACzE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wEAAwE;IACxE,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,oBAAoB,GAAG,SAAS,CAAC;CACvE;AAED,yFAAyF;AACzF,MAAM,WAAW,oBAAoB;IACjC,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACpC,KAAK,IAAI,IAAI,CAAC;IACd,SAAS,EAAE,CAAC,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;IACvD,sEAAsE;IACtE,KAAK,CAAC,IAAI,IAAI,CAAC;CAClB;AA4CD,qBAAa,iBAAkB,YAAW,eAAe;IAYjD,OAAO,CAAC,QAAQ,CAAC,OAAO;IAX5B,QAAQ,CAAC,IAAI,EAAG,WAAW,CAAU;IAErC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAyB;IAC9C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAmE;IACnG,OAAO,CAAC,GAAG,CAA4B;IACvC,OAAO,CAAC,QAAQ,CAAqC;IACrD,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA8C;gBAGpD,OAAO,EAAE,MAAM,EAChC,IAAI,GAAE,wBAA6B;IAOvC,qEAAqE;IACrE,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED,OAAO,CAAC,KAAK;IAoCb;;;;;OAKG;IACG,QAAQ,CAAC,CAAC,EACZ,MAAM,EAAE,gBAAgB,EAAE,EAC1B,IAAI,EAAE,UAAU,GAAG,WAAW,EAC9B,EAAE,EAAE,CAAC,EAAE,EAAE,kBAAkB,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,GAC/C,OAAO,CAAC,CAAC,CAAC;IAmDb,OAAO,CAAC,IAAI,EAAE,eAAe,GAAG,IAAI;IAUpC;;;;OAIG;IACH,SAAS,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,GAAG,MAAM,IAAI;IAMhE,OAAO,CAAC,cAAc;IAqBtB,KAAK,IAAI,IAAI;CAMhB;AAmFD,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,gBAAgB,EAAE,aAAa,EAAE,UAAU,EAAE,aAAa,EAAE,UAAU,EAAE,CAAC;AAC3G,YAAY,EAAE,YAAY,EAAE,CAAC"}
@@ -0,0 +1,311 @@
1
+ /**
2
+ * storage/idbAdapter.ts – Durable browser/IndexedDB implementation of the
3
+ * storage port.
4
+ *
5
+ * Two properties the legacy MemoryStore write helpers did NOT have (M02):
6
+ * 1. `transact()` resolves only on `tx.oncomplete` — the durable commit point —
7
+ * and rejects on `tx.onabort`. Request-level `onsuccess` is used internally
8
+ * to chain operations, never to signal durability.
9
+ * 2. One `transact()` call is one unit of work spanning facts, tombstones,
10
+ * journal and meta, so a fact and its sync journal entry commit together.
11
+ *
12
+ * Schema (database version 3):
13
+ * - 'facts' keyPath 'id' — StoredFactRecord (scoped composite id)
14
+ * index 'byKey' on 'key' — key lookup without a scan
15
+ * index 'byTag' on 'tags' multiEntry — tag lookup without a scan
16
+ * - 'tombstones' keyPath 'id' — TombstoneRecord
17
+ * - 'journal' keyPath 'opId' — JournalEntry (the sync journal)
18
+ * - 'meta' keyPath 'key' — MetaCounters (durable seq / revision / opSeq)
19
+ * - 'weights' (out-of-line) — legacy weight blob store, retained
20
+ *
21
+ * Version 3 adds the key and tag indexes only. Existing fact records, including
22
+ * their timestamps and TTLs, are not rewritten.
23
+ *
24
+ * Version-1 databases (facts keyed by the raw key, no journal) are migrated in
25
+ * the upgrade transaction: records are copied to the new 'id'-keyed store with
26
+ * the unbound scope, their seq preserved, revision 1, and no pending sync state.
27
+ *
28
+ * Cross-context consistency (M05): when BroadcastChannel is available the
29
+ * adapter publishes a change note after every committed write and delivers
30
+ * foreign notes to subscribers, so sibling tabs/windows invalidate their caches
31
+ * instead of serving stale recalls. The channel opens lazily — on the first
32
+ * publish, or once the database is open for a subscriber — and is `unref()`ed
33
+ * where supported (Node), so a store that is never closed cannot keep the
34
+ * process alive.
35
+ */
36
+ import { FACT_KEY_INDEX, FACT_TAG_INDEX, MemoryStorageError, scopedRecordId, } from './types.js';
37
+ const FACTS_STORE = 'facts';
38
+ const TOMBSTONES_STORE = 'tombstones';
39
+ const JOURNAL_STORE = 'journal';
40
+ const META_STORE = 'meta';
41
+ const WEIGHTS_STORE = 'weights';
42
+ const DB_VERSION = 3;
43
+ const ALL_STORES = [FACTS_STORE, TOMBSTONES_STORE, JOURNAL_STORE, META_STORE];
44
+ function reqToPromise(req) {
45
+ return new Promise((resolve, reject) => {
46
+ req.onsuccess = () => resolve(req.result);
47
+ req.onerror = () => reject(req.error ?? new Error('IndexedDB request failed'));
48
+ });
49
+ }
50
+ class IdbStorageTransaction {
51
+ _tx;
52
+ constructor(_tx) {
53
+ this._tx = _tx;
54
+ }
55
+ async get(store, id) {
56
+ return reqToPromise(this._tx.objectStore(store).get(id));
57
+ }
58
+ async getAll(store) {
59
+ return reqToPromise(this._tx.objectStore(store).getAll());
60
+ }
61
+ async put(store, value) {
62
+ await reqToPromise(this._tx.objectStore(store).put(value));
63
+ }
64
+ async delete(store, id) {
65
+ await reqToPromise(this._tx.objectStore(store).delete(id));
66
+ }
67
+ async getByIndex(store, indexName, query) {
68
+ const objectStore = this._tx.objectStore(store);
69
+ // Legacy databases opened before the index existed fall through to a scan.
70
+ if (!objectStore.indexNames.contains(indexName))
71
+ return undefined;
72
+ return reqToPromise(objectStore.index(indexName).getAll(query));
73
+ }
74
+ }
75
+ export class IdbStorageAdapter {
76
+ _dbName;
77
+ kind = 'indexedDB';
78
+ _idb;
79
+ _contextId;
80
+ _channelFactory;
81
+ _db = null;
82
+ _channel = null;
83
+ _channelFailed = false;
84
+ _listeners = new Set();
85
+ constructor(_dbName, opts = {}) {
86
+ this._dbName = _dbName;
87
+ this._idb = opts.idbFactory;
88
+ this._contextId = opts.contextId ?? `ctx-${Math.random().toString(36).slice(2)}-${Date.now().toString(36)}`;
89
+ this._channelFactory = opts.channelFactory;
90
+ }
91
+ /** Identity of this context, stamped onto change notes and opIds. */
92
+ get contextId() {
93
+ return this._contextId;
94
+ }
95
+ _open() {
96
+ if (this._db)
97
+ return Promise.resolve(this._db);
98
+ return new Promise((resolve, reject) => {
99
+ const factory = this._idb ?? (typeof indexedDB !== 'undefined' ? indexedDB : undefined);
100
+ if (!factory) {
101
+ reject(new MemoryStorageError('STORAGE_UNAVAILABLE', 'IndexedDB is not available in this environment. For durable Node.js storage use NodeFileStorageAdapter; fake-indexeddb is a test double, not durable storage.'));
102
+ return;
103
+ }
104
+ const req = factory.open(this._dbName, DB_VERSION);
105
+ req.onupgradeneeded = (e) => {
106
+ const db = e.target.result;
107
+ const tx = e.target.transaction;
108
+ migrateSchema(db, tx, e.oldVersion);
109
+ };
110
+ req.onsuccess = () => {
111
+ this._db = req.result;
112
+ // Subscribers registered before the first open start listening now.
113
+ if (this._listeners.size > 0)
114
+ this._ensureChannel();
115
+ resolve(req.result);
116
+ };
117
+ /* istanbul ignore next -- IDB open onerror fires only on storage faults; not reproducible with fake-indexeddb */
118
+ req.onerror = () => reject(new MemoryStorageError('STORAGE_UNAVAILABLE', `Failed to open IndexedDB "${this._dbName}": ${req.error?.message ?? 'unknown'}`, req.error));
119
+ });
120
+ }
121
+ /**
122
+ * One unit of work: runs `fn` inside a single IndexedDB transaction and
123
+ * resolves ONLY on `tx.oncomplete`. If `fn` throws, the transaction is
124
+ * aborted so no partial writes survive; if the browser aborts the
125
+ * transaction (quota, corruption, crash mid-commit) the promise rejects.
126
+ */
127
+ async transact(stores, mode, fn) {
128
+ const db = await this._open();
129
+ return new Promise((resolve, reject) => {
130
+ let settled = false;
131
+ const fail = (err) => {
132
+ if (settled)
133
+ return;
134
+ settled = true;
135
+ reject(err instanceof MemoryStorageError ? err : new MemoryStorageError('STORAGE_ABORTED', `Transaction aborted on "${this._dbName}": ${err instanceof Error ? err.message : String(err)}`, err));
136
+ };
137
+ let tx;
138
+ try {
139
+ tx = db.transaction(stores, mode);
140
+ }
141
+ catch (err) {
142
+ fail(err);
143
+ return;
144
+ }
145
+ let result;
146
+ tx.oncomplete = () => {
147
+ if (settled)
148
+ return;
149
+ settled = true;
150
+ resolve(result);
151
+ };
152
+ /* istanbul ignore next -- abort fires only on storage faults or explicit abort; fault path not reproducible with fake-indexeddb */
153
+ tx.onabort = () => fail(tx.error ?? new Error('transaction aborted'));
154
+ /* istanbul ignore next -- request errors surface through the per-request rejections and the abort handler */
155
+ tx.onerror = () => { };
156
+ const stx = new IdbStorageTransaction(tx);
157
+ Promise.resolve()
158
+ .then(() => fn(stx))
159
+ .then((r) => { result = r; })
160
+ .catch((err) => {
161
+ // The unit of work failed — abort so facts/journal/tombstones
162
+ // never commit partially (M02), then surface the ORIGINAL error.
163
+ try {
164
+ tx.abort();
165
+ }
166
+ catch { /* already finished */ }
167
+ if (!settled) {
168
+ settled = true;
169
+ reject(err);
170
+ }
171
+ });
172
+ });
173
+ }
174
+ // ── Cross-context consistency seam (M05) ──────────────────────────────────
175
+ publish(note) {
176
+ const channel = this._ensureChannel();
177
+ if (!channel)
178
+ return;
179
+ try {
180
+ channel.postMessage(note);
181
+ }
182
+ catch {
183
+ // A dead channel must never fail a committed write.
184
+ }
185
+ }
186
+ /**
187
+ * Registers `listener` for foreign change notes. The channel itself opens
188
+ * when the database does (or on the first publish), not here: subscribing
189
+ * from a constructor must not open a global channel.
190
+ */
191
+ subscribe(listener) {
192
+ this._listeners.add(listener);
193
+ if (this._db)
194
+ this._ensureChannel();
195
+ return () => this._listeners.delete(listener);
196
+ }
197
+ _ensureChannel() {
198
+ if (this._channel || this._channelFailed)
199
+ return this._channel;
200
+ try {
201
+ const factory = this._channelFactory ?? defaultChannelFactory;
202
+ const channel = factory(`bf-memory:${this._dbName}`);
203
+ if (channel) {
204
+ channel.onmessage = (e) => {
205
+ const note = e.data;
206
+ if (!note || note.kind !== 'facts-changed')
207
+ return;
208
+ if (note.origin === this._contextId)
209
+ return; // our own write
210
+ for (const l of this._listeners)
211
+ l(note);
212
+ };
213
+ channel.unref?.();
214
+ this._channel = channel;
215
+ }
216
+ }
217
+ catch {
218
+ this._channelFailed = true;
219
+ }
220
+ return this._channel;
221
+ }
222
+ close() {
223
+ this._channel?.close();
224
+ this._channel = null;
225
+ this._db?.close();
226
+ this._db = null;
227
+ }
228
+ }
229
+ function defaultChannelFactory(name) {
230
+ if (typeof BroadcastChannel === 'undefined')
231
+ return undefined;
232
+ return new BroadcastChannel(name);
233
+ }
234
+ // ── Schema migration ──────────────────────────────────────────────────────────
235
+ function ensureFactIndexes(store) {
236
+ if (!store.indexNames.contains(FACT_KEY_INDEX)) {
237
+ store.createIndex(FACT_KEY_INDEX, 'key', { unique: false });
238
+ }
239
+ if (!store.indexNames.contains(FACT_TAG_INDEX)) {
240
+ // One index row per tag, so recallByTag is a lookup rather than a scan.
241
+ store.createIndex(FACT_TAG_INDEX, 'tags', { unique: false, multiEntry: true });
242
+ }
243
+ }
244
+ function migrateSchema(db, tx, oldVersion) {
245
+ if (oldVersion < 1) {
246
+ // Fresh database.
247
+ const facts = db.createObjectStore(FACTS_STORE, { keyPath: 'id' });
248
+ ensureFactIndexes(facts);
249
+ db.createObjectStore(WEIGHTS_STORE);
250
+ }
251
+ else if (oldVersion < 2 && db.objectStoreNames.contains(FACTS_STORE)) {
252
+ // v1 → v2: facts were keyed by the raw key; move them onto the scoped
253
+ // record shape in the unbound partition. Copy out, drop, recreate, refill —
254
+ // all inside the upgrade transaction, so a crash here rolls everything back.
255
+ const legacy = [];
256
+ const cursorReq = tx.objectStore(FACTS_STORE).openCursor();
257
+ cursorReq.onsuccess = () => {
258
+ const cursor = cursorReq.result;
259
+ if (cursor) {
260
+ legacy.push(cursor.value);
261
+ cursor.continue();
262
+ }
263
+ else {
264
+ db.deleteObjectStore(FACTS_STORE);
265
+ const store = db.createObjectStore(FACTS_STORE, { keyPath: 'id' });
266
+ ensureFactIndexes(store);
267
+ for (const e of legacy) {
268
+ const record = {
269
+ id: scopedRecordId(undefined, e.key),
270
+ key: e.key,
271
+ scope: {},
272
+ content: e.content,
273
+ timestamp: e.timestamp,
274
+ seq: e.seq ?? 0,
275
+ revision: 1,
276
+ opId: 'migrated:v1',
277
+ ttlMs: e.ttlMs,
278
+ type: e.type,
279
+ tags: e.tags,
280
+ importance: e.importance,
281
+ pendingSync: false,
282
+ };
283
+ store.put(record);
284
+ }
285
+ }
286
+ };
287
+ }
288
+ else if (!db.objectStoreNames.contains(FACTS_STORE)) {
289
+ const facts = db.createObjectStore(FACTS_STORE, { keyPath: 'id' });
290
+ ensureFactIndexes(facts);
291
+ }
292
+ else if (oldVersion < 3) {
293
+ // v2 → v3: add key/tag indexes. Do not rewrite records — timestamps,
294
+ // TTLs, tags and content already on disk must survive the upgrade.
295
+ ensureFactIndexes(tx.objectStore(FACTS_STORE));
296
+ }
297
+ if (!db.objectStoreNames.contains(WEIGHTS_STORE)) {
298
+ db.createObjectStore(WEIGHTS_STORE);
299
+ }
300
+ if (!db.objectStoreNames.contains(TOMBSTONES_STORE)) {
301
+ db.createObjectStore(TOMBSTONES_STORE, { keyPath: 'id' });
302
+ }
303
+ if (!db.objectStoreNames.contains(JOURNAL_STORE)) {
304
+ db.createObjectStore(JOURNAL_STORE, { keyPath: 'opId' });
305
+ }
306
+ if (!db.objectStoreNames.contains(META_STORE)) {
307
+ db.createObjectStore(META_STORE, { keyPath: 'key' });
308
+ }
309
+ }
310
+ export { ALL_STORES, FACTS_STORE, TOMBSTONES_STORE, JOURNAL_STORE, META_STORE, WEIGHTS_STORE, DB_VERSION };
311
+ //# sourceMappingURL=idbAdapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idbAdapter.js","sourceRoot":"","sources":["../../src/storage/idbAdapter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,127 @@
1
+ /**
2
+ * storage/nodeFileAdapter.ts – Durable Node.js implementation of the storage
3
+ * port, backed by a single atomically-replaced JSON document.
4
+ *
5
+ * This is the VSIX extension-host / MCP adapter the offline-memory scope (M01)
6
+ * requires: real durable storage on Node WITHOUT an in-memory IndexedDB shim.
7
+ * fake-indexeddb evaporates with the process; this adapter fsyncs a temp file
8
+ * and atomically renames it over the database file, so a crash mid-write leaves
9
+ * either the old or the new document — never a torn one.
10
+ *
11
+ * Transaction semantics (M02/M05):
12
+ * - All transactions are serialized through one promise queue, so concurrent
13
+ * writers in this process interleave at transaction granularity.
14
+ * - A read-write transaction holds an exclusive `<file>.lock` (see
15
+ * `fileLock.ts`: atomic create, stale-lock reclamation by PID liveness,
16
+ * bounded wait) across refresh + mutate + persist, so two processes can
17
+ * never both build on the same stale document and lose a write.
18
+ * - Under that lock, the file is re-read when another process changed it
19
+ * (mtime/size freshness), so cross-process writers build on each other's
20
+ * commits instead of blindly overwriting.
21
+ * - If the persist fails, the in-memory document is rolled back to the
22
+ * pre-transaction snapshot and the transaction rejects — no partial commits.
23
+ *
24
+ * Cross-process consistency: a polling watcher (default 1s, injectable)
25
+ * republishes foreign changes to subscribers, the Node analogue of the IDB
26
+ * adapter's BroadcastChannel.
27
+ *
28
+ * Node builtins are loaded indirectly (via a non-static import) so a browser
29
+ * bundle of this package never statically pulls in `node:fs`.
30
+ */
31
+ import { type FileLockOptions } from './fileLock.js';
32
+ import { type ConsistencyNote, type FactStoragePort, type StorageStoreName, type StorageTransaction } from './types.js';
33
+ export type { NodeFileDocument } from './fileTransaction.js';
34
+ export interface NodeFileStorageAdapterOptions {
35
+ /** Identity of this context for cross-context notes. Default: random. */
36
+ contextId?: string;
37
+ /**
38
+ * Poll interval (ms) for foreign-change notification. 0 disables the
39
+ * watcher. Default: 1000. Freshness is also checked synchronously before
40
+ * every write transaction regardless of this interval.
41
+ */
42
+ watchIntervalMs?: number;
43
+ /** Injectable fs (tests, sandboxed hosts). Default: node:fs. */
44
+ fs?: NodeFs;
45
+ /** Injectable path helpers. Default: node:path. */
46
+ path?: NodePath;
47
+ /** Cross-process write lock tuning (timeout, liveness probe). */
48
+ lock?: FileLockOptions;
49
+ }
50
+ /** The subset of node:fs this adapter uses. */
51
+ export interface NodeFs {
52
+ existsSync(path: string): boolean;
53
+ readFileSync(path: string, encoding: 'utf8'): string;
54
+ statSync(path: string): {
55
+ mtimeMs: number;
56
+ size: number;
57
+ };
58
+ mkdirSync(path: string, opts: {
59
+ recursive: boolean;
60
+ }): void;
61
+ openSync(path: string, flags: string): number;
62
+ writeFileSync(fd: number, data: string): void;
63
+ fsyncSync(fd: number): void;
64
+ closeSync(fd: number): void;
65
+ renameSync(from: string, to: string): void;
66
+ unlinkSync(path: string): void;
67
+ copyFileSync(from: string, to: string): void;
68
+ }
69
+ export interface NodePath {
70
+ dirname(p: string): string;
71
+ }
72
+ export declare class NodeFileStorageAdapter implements FactStoragePort {
73
+ private readonly _filePath;
74
+ readonly kind: "node-file";
75
+ private readonly _fs;
76
+ private readonly _path;
77
+ private readonly _contextId;
78
+ private _doc;
79
+ private _stamp;
80
+ /** Transaction queue tail — all transacts serialize through this. */
81
+ private _queue;
82
+ private _watchTimer;
83
+ private readonly _listeners;
84
+ private readonly _watchIntervalMs;
85
+ private readonly _lock;
86
+ private constructor();
87
+ /**
88
+ * Opens (creating if necessary) the durable file at `filePath`. Async only
89
+ * because Node builtins load indirectly; the document itself is read
90
+ * synchronously before this resolves.
91
+ */
92
+ static open(filePath: string, opts?: NodeFileStorageAdapterOptions): Promise<NodeFileStorageAdapter>;
93
+ /** Identity of this context, stamped onto change notes and opIds. */
94
+ get contextId(): string;
95
+ /** The durable file backing this adapter. */
96
+ get filePath(): string;
97
+ /**
98
+ * Reads the document from disk. Recovery order on a corrupt main file
99
+ * (killed mid-rename is impossible, but a torn fsync on exotic filesystems
100
+ * is not): try `<file>.tmp` (the last fully-fsynced candidate), then give up
101
+ * with the corrupt file preserved at `<file>.corrupt` for forensics rather
102
+ * than silently starting empty.
103
+ */
104
+ private _loadFromDisk;
105
+ private _readDocument;
106
+ /** Re-reads the document when another process rewrote the file. */
107
+ private _refreshIfChanged;
108
+ /**
109
+ * Atomic durable write: fsync the full document to `<file>.tmp`, then rename
110
+ * over the live file. A crash leaves the old file intact; the tmp file is
111
+ * the recovery source on next boot.
112
+ */
113
+ private _persist;
114
+ /**
115
+ * Serialized unit of work. Read-write transactions take the cross-process
116
+ * lock, refresh from disk (so we build on other processes' commits), run
117
+ * `fn` against the live document, then persist atomically before
118
+ * releasing. Any failure rolls the in-memory document back to the
119
+ * pre-transaction snapshot — no partial commits, in memory or on disk.
120
+ */
121
+ transact<T>(_stores: StorageStoreName[], mode: 'readonly' | 'readwrite', fn: (tx: StorageTransaction) => Promise<T> | T): Promise<T>;
122
+ private _readWrite;
123
+ publish(note: ConsistencyNote): void;
124
+ subscribe(listener: (note: ConsistencyNote) => void): () => void;
125
+ close(): void;
126
+ }
127
+ //# sourceMappingURL=nodeFileAdapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nodeFileAdapter.d.ts","sourceRoot":"","sources":["../../src/storage/nodeFileAdapter.ts"],"names":[],"mappings":"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"}