@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.
- package/dist/errors/SSMError.d.ts +1 -1
- package/dist/errors/SSMError.d.ts.map +1 -1
- package/dist/errors/SSMError.js.map +1 -1
- package/dist/handoff/index.d.ts +5 -5
- package/dist/handoff/index.js +5 -5
- package/dist/handoff/note.d.ts +1 -1
- package/dist/handoff/note.js +1 -1
- package/dist/handoff/pack.d.ts +13 -4
- package/dist/handoff/pack.d.ts.map +1 -1
- package/dist/handoff/pack.js +31 -10
- package/dist/handoff/pack.js.map +1 -1
- package/dist/memory/MemoryStore.d.ts +120 -181
- package/dist/memory/MemoryStore.d.ts.map +1 -1
- package/dist/memory/MemoryStore.js +201 -389
- package/dist/memory/MemoryStore.js.map +1 -1
- package/dist/memory/SemanticRecall.d.ts +64 -0
- package/dist/memory/SemanticRecall.d.ts.map +1 -0
- package/dist/memory/SemanticRecall.js +188 -0
- package/dist/memory/SemanticRecall.js.map +1 -0
- package/dist/memory/records.d.ts +26 -0
- package/dist/memory/records.d.ts.map +1 -0
- package/dist/memory/records.js +71 -0
- package/dist/memory/records.js.map +1 -0
- package/dist/memory/ttlSweeper.d.ts +12 -0
- package/dist/memory/ttlSweeper.d.ts.map +1 -0
- package/dist/memory/ttlSweeper.js +28 -0
- package/dist/memory/ttlSweeper.js.map +1 -0
- package/dist/memory/types.d.ts +142 -0
- package/dist/memory/types.d.ts.map +1 -0
- package/dist/memory/types.js +6 -0
- package/dist/memory/types.js.map +1 -0
- package/dist/storage/ScopedFactRepository.d.ts +156 -0
- package/dist/storage/ScopedFactRepository.d.ts.map +1 -0
- package/dist/storage/ScopedFactRepository.js +278 -0
- package/dist/storage/ScopedFactRepository.js.map +1 -0
- package/dist/storage/factImport.d.ts +21 -0
- package/dist/storage/factImport.d.ts.map +1 -0
- package/dist/storage/factImport.js +61 -0
- package/dist/storage/factImport.js.map +1 -0
- package/dist/storage/factMutations.d.ts +46 -0
- package/dist/storage/factMutations.d.ts.map +1 -0
- package/dist/storage/factMutations.js +73 -0
- package/dist/storage/factMutations.js.map +1 -0
- package/dist/storage/factQueries.d.ts +16 -0
- package/dist/storage/factQueries.d.ts.map +1 -0
- package/dist/storage/factQueries.js +36 -0
- package/dist/storage/factQueries.js.map +1 -0
- package/dist/storage/factRetention.d.ts +30 -0
- package/dist/storage/factRetention.d.ts.map +1 -0
- package/dist/storage/factRetention.js +65 -0
- package/dist/storage/factRetention.js.map +1 -0
- package/dist/storage/fileLock.d.ts +56 -0
- package/dist/storage/fileLock.d.ts.map +1 -0
- package/dist/storage/fileLock.js +138 -0
- package/dist/storage/fileLock.js.map +1 -0
- package/dist/storage/fileTransaction.d.ts +33 -0
- package/dist/storage/fileTransaction.d.ts.map +1 -0
- package/dist/storage/fileTransaction.js +108 -0
- package/dist/storage/fileTransaction.js.map +1 -0
- package/dist/storage/idbAdapter.d.ts +95 -0
- package/dist/storage/idbAdapter.d.ts.map +1 -0
- package/dist/storage/idbAdapter.js +311 -0
- package/dist/storage/idbAdapter.js.map +1 -0
- package/dist/storage/nodeFileAdapter.d.ts +127 -0
- package/dist/storage/nodeFileAdapter.d.ts.map +1 -0
- package/dist/storage/nodeFileAdapter.js +275 -0
- package/dist/storage/nodeFileAdapter.js.map +1 -0
- package/dist/storage/syncJournal.d.ts +19 -0
- package/dist/storage/syncJournal.d.ts.map +1 -0
- package/dist/storage/syncJournal.js +46 -0
- package/dist/storage/syncJournal.js.map +1 -0
- package/dist/storage/types.d.ts +207 -0
- package/dist/storage/types.d.ts.map +1 -0
- package/dist/storage/types.js +90 -0
- package/dist/storage/types.js.map +1 -0
- package/package.json +3 -3
- package/src/errors/SSMError.ts +1 -0
- package/src/handoff/handoff.test.ts +52 -2
- package/src/handoff/index.ts +5 -5
- package/src/handoff/note.ts +1 -1
- package/src/handoff/pack.ts +32 -9
- package/src/memory/MemoryStore.ts +245 -525
- package/src/memory/SemanticRecall.ts +204 -0
- package/src/memory/records.ts +79 -0
- package/src/memory/ttlSweeper.ts +29 -0
- package/src/memory/types.ts +149 -0
- package/src/storage/ScopedFactRepository.ts +373 -0
- package/src/storage/factImport.ts +73 -0
- package/src/storage/factMutations.ts +106 -0
- package/src/storage/factQueries.ts +41 -0
- package/src/storage/factRetention.ts +82 -0
- package/src/storage/fileLock.ts +162 -0
- package/src/storage/fileTransaction.ts +116 -0
- package/src/storage/idbAdapter.ts +373 -0
- package/src/storage/nodeFileAdapter.ts +331 -0
- package/src/storage/syncJournal.ts +47 -0
- 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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"}
|