refactor(persistence): add v3 graph store adapters

This commit is contained in:
youzini
2026-05-30 14:22:00 +00:00
parent 7779e66e04
commit 1d09c53c0e
4 changed files with 365 additions and 0 deletions

View File

@@ -7,6 +7,7 @@ export const GRAPH_V3_NAMESPACE_VERSION = 3;
export const GRAPH_V3_MODULE_NAME = "st_bme_v3";
export const GRAPH_V3_METADATA_KEY = `${GRAPH_V3_MODULE_NAME}_graph`;
export const GRAPH_V3_HEAD_KEY = `${GRAPH_V3_MODULE_NAME}_graph_head`;
export const GRAPH_V3_COMMIT_MARKER_KEY = `${GRAPH_V3_MODULE_NAME}_commit_marker`;
export const GRAPH_V3_CHAT_STATE_NAMESPACE = `${GRAPH_V3_MODULE_NAME}_graph_state`;
export const GRAPH_V3_LUKER_MANIFEST_NAMESPACE = `${GRAPH_V3_MODULE_NAME}_graph_manifest`;
@@ -65,6 +66,7 @@ export function listGraphV3NamespaceValues() {
return Object.freeze([
GRAPH_V3_MODULE_NAME,
GRAPH_V3_METADATA_KEY,
GRAPH_V3_HEAD_KEY,
GRAPH_V3_COMMIT_MARKER_KEY,
GRAPH_V3_CHAT_STATE_NAMESPACE,
GRAPH_V3_LUKER_MANIFEST_NAMESPACE,

View File

@@ -11,6 +11,7 @@
"test:persistence-reducer": "node tests/persistence-reducer.mjs",
"test:graph-head": "node tests/graph-head.mjs",
"test:graph-store-contract": "node tests/graph-store-contract.mjs",
"test:graph-store-v3-adapter": "node tests/graph-store-v3-adapter.mjs",
"test:reroll-transaction-boundary": "node tests/reroll-transaction-boundary.mjs",
"test:vector-gate": "node tests/vector-gate.mjs",
"test:hide-engine": "node tests/hide-engine.mjs",

View File

@@ -0,0 +1,172 @@
// ST-BME v3 GraphStore adapter wrappers.
//
// These wrappers add v3 head/marker sidecar methods to existing stores without
// changing their legacy load/save behavior. Physical namespace cutover is handled
// by dedicated constructors/routes later.
import {
GRAPH_V3_COMMIT_MARKER_KEY,
GRAPH_V3_HEAD_KEY,
} from "../graph/graph-v3-namespace.js";
import {
normalizeCommitMarkerV3,
normalizeGraphHead,
} from "../graph/graph-head.js";
import {
readGraphChatStateNamespaces,
writeGraphChatStatePayload,
} from "../graph/graph-persistence.js";
import { assertGraphStoreContract } from "./graph-store-contract.js";
const GRAPH_STORE_V3_WRAPPED = Symbol.for("st-bme.graph-store-v3-wrapped");
function bindStoreMethod(store = null, method = "") {
const value = store?.[method];
return typeof value === "function" ? value.bind(store) : value;
}
export function isGraphStoreV3Wrapped(store = null) {
return Boolean(store?.[GRAPH_STORE_V3_WRAPPED]);
}
export function wrapDbLikeGraphStoreV3(store = null) {
assertGraphStoreContract(store);
if (isGraphStoreV3Wrapped(store)) return store;
const wrapper = Object.create(store);
Object.defineProperty(wrapper, GRAPH_STORE_V3_WRAPPED, {
value: true,
enumerable: false,
});
for (const key of Reflect.ownKeys(store)) {
if (key === GRAPH_STORE_V3_WRAPPED) continue;
const descriptor = Object.getOwnPropertyDescriptor(store, key);
if (descriptor) Object.defineProperty(wrapper, key, descriptor);
}
for (const method of [
"open",
"close",
"getMeta",
"setMeta",
"patchMeta",
"commitDelta",
"exportSnapshot",
"exportSnapshotProbe",
"importSnapshot",
"isEmpty",
"clearAll",
]) {
if (typeof store[method] === "function") {
wrapper[method] = bindStoreMethod(store, method);
}
}
wrapper.readHead = async ({ fallback = null } = {}) => {
const raw = await store.getMeta(GRAPH_V3_HEAD_KEY, null);
return raw == null ? fallback : normalizeGraphHead(raw, fallback || {});
};
wrapper.writeHead = async (head = null, { fallback = null } = {}) => {
const normalized = normalizeGraphHead(head, fallback || {});
await store.patchMeta({ [GRAPH_V3_HEAD_KEY]: normalized });
return normalized;
};
wrapper.readCommitMarker = async ({ fallback = null } = {}) => {
const raw = await store.getMeta(GRAPH_V3_COMMIT_MARKER_KEY, null);
return normalizeCommitMarkerV3(raw) || fallback;
};
wrapper.writeCommitMarker = async (marker = null) => {
const normalized = normalizeCommitMarkerV3(marker);
if (!normalized) {
const error = new Error("graph-store-v3-commit-marker-invalid");
error.code = "graph_store_v3_commit_marker_invalid";
throw error;
}
await store.patchMeta({ [GRAPH_V3_COMMIT_MARKER_KEY]: normalized });
return normalized;
};
wrapper.deleteAll = async (...args) => {
if (typeof store.clearAll !== "function") {
const error = new Error("graph-store-v3-delete-all-unavailable");
error.code = "graph_store_v3_delete_all_unavailable";
throw error;
}
return store.clearAll(...args);
};
return wrapper;
}
export function createLukerChatStateGraphStoreV3({
context = null,
chatStateTarget = null,
storeKind = "luker-chat-state",
storeMode = "luker-chat-state-v3",
} = {}) {
async function readNamespace(namespace = "", fallback = null) {
const payloads = await readGraphChatStateNamespaces(context, [namespace], {
target: chatStateTarget,
});
return payloads.get(namespace) ?? fallback;
}
async function writeNamespace(namespace = "", payload = null) {
const result = await writeGraphChatStatePayload(context, namespace, payload, {
target: chatStateTarget,
});
if (result?.ok !== true) {
const error = new Error(result?.reason || "luker-graph-store-v3-write-failed");
error.code = "luker_graph_store_v3_write_failed";
error.result = result;
throw error;
}
return payload;
}
return {
storeKind,
storeMode,
async open() {
return this;
},
async close() {},
async getMeta(key = "", fallbackValue = null) {
return readNamespace(String(key || ""), fallbackValue);
},
async patchMeta(record = {}) {
const entries = Object.entries(record && typeof record === "object" ? record : {});
for (const [key, value] of entries) {
await writeNamespace(key, value);
}
return record;
},
async readHead({ fallback = null } = {}) {
const raw = await readNamespace(GRAPH_V3_HEAD_KEY, null);
return raw == null ? fallback : normalizeGraphHead(raw, fallback || {});
},
async writeHead(head = null, { fallback = null } = {}) {
const normalized = normalizeGraphHead(head, fallback || {});
await writeNamespace(GRAPH_V3_HEAD_KEY, normalized);
return normalized;
},
async readCommitMarker({ fallback = null } = {}) {
const raw = await readNamespace(GRAPH_V3_COMMIT_MARKER_KEY, null);
return normalizeCommitMarkerV3(raw) || fallback;
},
async writeCommitMarker(marker = null) {
const normalized = normalizeCommitMarkerV3(marker);
if (!normalized) {
const error = new Error("luker-graph-store-v3-commit-marker-invalid");
error.code = "luker_graph_store_v3_commit_marker_invalid";
throw error;
}
await writeNamespace(GRAPH_V3_COMMIT_MARKER_KEY, normalized);
return normalized;
},
};
}

View File

@@ -0,0 +1,190 @@
// ST-BME restrained rebirth — Phase 7 v3 GraphStore adapter tests.
import assert from "node:assert/strict";
import {
GRAPH_V3_COMMIT_MARKER_KEY,
GRAPH_V3_HEAD_KEY,
GRAPH_V3_METADATA_KEY,
} from "../graph/graph-v3-namespace.js";
import {
buildCommitMarkerV3,
normalizeGraphHead,
normalizeReplicaPointer,
} from "../graph/graph-head.js";
import { GRAPH_STORE_REQUIRED_METHODS, inspectGraphStoreContract } from "../sync/graph-store-contract.js";
import {
createLukerChatStateGraphStoreV3,
isGraphStoreV3Wrapped,
wrapDbLikeGraphStoreV3,
} from "../sync/graph-store-v3-adapter.js";
function createMockDbLikeStore() {
const meta = new Map();
const calls = [];
return {
storeKind: "indexeddb",
storeMode: "indexeddb-v3-test",
meta,
calls,
async open() {
calls.push(["open"]);
return this;
},
async close() {
calls.push(["close"]);
},
async getMeta(key, fallbackValue = null) {
calls.push(["getMeta", key]);
return meta.has(key) ? meta.get(key) : fallbackValue;
},
async patchMeta(record = {}) {
calls.push(["patchMeta", Object.keys(record).sort()]);
for (const [key, value] of Object.entries(record)) {
meta.set(key, value);
}
return record;
},
async commitDelta() {},
async exportSnapshot() {},
async exportSnapshotProbe() {},
async importSnapshot() {},
async clearAll() {
calls.push(["clearAll"]);
meta.clear();
return { ok: true };
},
};
}
const rawStore = createMockDbLikeStore();
const wrapped = wrapDbLikeGraphStoreV3(rawStore);
assert.equal(isGraphStoreV3Wrapped(wrapped), true);
assert.equal(wrapDbLikeGraphStoreV3(wrapped), wrapped);
const wrappedContract = inspectGraphStoreContract(wrapped);
assert.equal(wrappedContract.valid, true);
assert.ok(wrappedContract.supportedOptionalMethods.includes("readHead"));
assert.ok(wrappedContract.supportedOptionalMethods.includes("writeCommitMarker"));
assert.ok(wrappedContract.supportedOptionalMethods.includes("deleteAll"));
const head = normalizeGraphHead({
graphId: "graph-a",
chatId: "chat-a",
integrity: "integrity-a",
revision: 9,
counts: { nodeCount: 2, edgeCount: 1 },
});
const writtenHead = await wrapped.writeHead(head);
assert.equal(writtenHead.graphId, "graph-a");
assert.deepEqual(await wrapped.readHead(), writtenHead);
assert.deepEqual(rawStore.meta.get(GRAPH_V3_HEAD_KEY), writtenHead);
assert.equal(rawStore.meta.has(GRAPH_V3_METADATA_KEY), false, "head must use dedicated v3 head key");
const marker = buildCommitMarkerV3({
head,
replica: normalizeReplicaPointer({
graphId: head.graphId,
revision: head.revision,
storageTier: "indexeddb",
accepted: true,
}),
});
const writtenMarker = await wrapped.writeCommitMarker(marker);
assert.equal(writtenMarker.accepted, true);
assert.deepEqual(await wrapped.readCommitMarker(), writtenMarker);
assert.deepEqual(rawStore.meta.get(GRAPH_V3_COMMIT_MARKER_KEY), writtenMarker);
assert.equal(rawStore.meta.has("st_bme_commit_marker"), false, "legacy marker key must stay untouched");
await wrapped.deleteAll();
assert.equal(rawStore.meta.size, 0);
console.log(" ✓ DB-like v3 wrapper adds head/marker methods without legacy key writes");
class ClassBackedStore {
constructor() {
this.storeKind = "opfs";
this.storeMode = "class-backed-test";
this.meta = new Map();
this.clearCount = 0;
}
async open() {
return this;
}
async close() {}
async getMeta(key, fallbackValue = null) {
assert.equal(this instanceof ClassBackedStore, true, "wrapped methods must keep class instance this");
return this.meta.has(key) ? this.meta.get(key) : fallbackValue;
}
async patchMeta(record = {}) {
assert.equal(this instanceof ClassBackedStore, true, "patchMeta must run on the original class instance");
for (const [key, value] of Object.entries(record)) {
this.meta.set(key, value);
}
return record;
}
async commitDelta() {}
async exportSnapshot() {}
async exportSnapshotProbe() {}
async importSnapshot() {}
async clearAll() {
assert.equal(this instanceof ClassBackedStore, true, "deleteAll must delegate to class-backed clearAll");
this.clearCount += 1;
}
}
const classBackedRaw = new ClassBackedStore();
const classBackedWrapped = wrapDbLikeGraphStoreV3(classBackedRaw);
await classBackedWrapped.writeHead(head);
assert.equal((await classBackedWrapped.readHead()).graphId, "graph-a");
await classBackedWrapped.deleteAll();
assert.equal(classBackedRaw.clearCount, 1);
console.log(" ✓ DB-like wrapper preserves class-instance method binding");
const chatState = new Map();
const updatedNamespaces = [];
const lukerContext = {
getChatState(namespace) {
return chatState.get(namespace) || null;
},
getChatStateBatch(namespaces) {
return new Map(namespaces.map((namespace) => [namespace, chatState.get(namespace) || null]));
},
updateChatState(namespace, updater) {
updatedNamespaces.push(namespace);
const next = updater(chatState.get(namespace) || null);
chatState.set(namespace, next);
return { ok: true, updated: true };
},
};
const lukerStore = createLukerChatStateGraphStoreV3({ context: lukerContext });
const lukerContract = inspectGraphStoreContract(lukerStore, {
requiredMethods: ["open", "close", "getMeta", "patchMeta", "readHead", "writeHead", "readCommitMarker", "writeCommitMarker"],
});
assert.equal(lukerContract.valid, true);
for (const requiredMethod of GRAPH_STORE_REQUIRED_METHODS) {
if (["commitDelta", "exportSnapshot", "exportSnapshotProbe", "importSnapshot"].includes(requiredMethod)) {
assert.equal(
typeof lukerStore[requiredMethod],
"undefined",
`Luker thin wrapper must not claim unsupported DB-like method ${requiredMethod}`,
);
}
}
await lukerStore.writeHead(head);
await lukerStore.writeCommitMarker(marker);
assert.deepEqual(await lukerStore.readHead(), head);
assert.deepEqual(await lukerStore.readCommitMarker(), marker);
assert.deepEqual(updatedNamespaces, [GRAPH_V3_HEAD_KEY, GRAPH_V3_COMMIT_MARKER_KEY]);
assert.equal(chatState.has("st_bme_graph_state"), false, "Luker wrapper must not write legacy chat-state namespace");
console.log(" ✓ Luker v3 wrapper writes only v3 head/marker namespaces");
console.log("graph-store-v3-adapter tests passed");