feat(authority): add graph SQL store

This commit is contained in:
Youzini-afk
2026-04-28 02:20:15 +08:00
parent ee9b0afa35
commit dc37d22dcf
6 changed files with 1622 additions and 28 deletions

182
index.js
View File

@@ -40,6 +40,11 @@ import {
isGraphLocalStorageModeOpfs, isGraphLocalStorageModeOpfs,
normalizeGraphLocalStorageMode, normalizeGraphLocalStorageMode,
} from "./sync/bme-opfs-store.js"; } from "./sync/bme-opfs-store.js";
import {
AUTHORITY_GRAPH_STORE_KIND,
AUTHORITY_GRAPH_STORE_MODE,
AuthorityGraphStore,
} from "./sync/authority-graph-store.js";
import { import {
autoSyncOnChatChange, autoSyncOnChatChange,
autoSyncOnVisibility, autoSyncOnVisibility,
@@ -251,6 +256,7 @@ import {
} from "./runtime/settings-defaults.js"; } from "./runtime/settings-defaults.js";
import { import {
createDefaultAuthorityCapabilityState, createDefaultAuthorityCapabilityState,
normalizeAuthoritySettings,
normalizeAuthorityCapabilityState, normalizeAuthorityCapabilityState,
probeAuthorityCapabilities, probeAuthorityCapabilities,
} from "./runtime/authority-capabilities.js"; } from "./runtime/authority-capabilities.js";
@@ -1382,6 +1388,7 @@ function normalizePersistenceStorageTier(value = "none") {
[ [
"indexeddb", "indexeddb",
"opfs", "opfs",
"authority-sql",
"chat-state", "chat-state",
"luker-chat-state", "luker-chat-state",
"shadow", "shadow",
@@ -1401,6 +1408,9 @@ function resolveLocalStoreTierFromPresentation(
presentation && typeof presentation === "object" presentation && typeof presentation === "object"
? presentation ? presentation
: getPreferredGraphLocalStorePresentationSync(); : getPreferredGraphLocalStorePresentationSync();
if (normalizedPresentation.storagePrimary === AUTHORITY_GRAPH_STORE_KIND) {
return "authority-sql";
}
return normalizedPresentation.storagePrimary === "opfs" ? "opfs" : "indexeddb"; return normalizedPresentation.storagePrimary === "opfs" ? "opfs" : "indexeddb";
} }
@@ -1437,12 +1447,20 @@ function buildPersistenceEnvironment(
) { ) {
const hostProfile = resolvePersistenceHostProfile(context); const hostProfile = resolvePersistenceHostProfile(context);
const localStoreTier = resolveLocalStoreTierFromPresentation(presentation); const localStoreTier = resolveLocalStoreTierFromPresentation(presentation);
const authorityPrimary = localStoreTier === "authority-sql";
return { return {
hostProfile, hostProfile,
localStoreTier, localStoreTier,
primaryStorageTier: primaryStorageTier: authorityPrimary
hostProfile === "luker" ? "luker-chat-state" : localStoreTier, ? "authority-sql"
cacheStorageTier: hostProfile === "luker" ? localStoreTier : "none", : hostProfile === "luker"
? "luker-chat-state"
: localStoreTier,
cacheStorageTier: authorityPrimary
? "none"
: hostProfile === "luker"
? localStoreTier
: "none",
}; };
} }
@@ -4650,6 +4668,15 @@ function buildOpfsStorePresentation(
}; };
} }
function buildAuthorityStorePresentation() {
return {
storagePrimary: AUTHORITY_GRAPH_STORE_KIND,
storageMode: AUTHORITY_GRAPH_STORE_MODE,
statusLabel: "Authority SQL",
reasonPrefix: "authority-sql",
};
}
function getRequestedGraphLocalStorageMode(settings = getSettings()) { function getRequestedGraphLocalStorageMode(settings = getSettings()) {
const sourceSettings = const sourceSettings =
settings && typeof settings === "object" && !Array.isArray(settings) settings && typeof settings === "object" && !Array.isArray(settings)
@@ -4661,7 +4688,66 @@ function getRequestedGraphLocalStorageMode(settings = getSettings()) {
); );
} }
function shouldUseAuthorityGraphStore(settings = getSettings(), capability = authorityCapabilityState) {
const normalizedSettings = normalizeAuthoritySettings(settings);
const normalizedCapability = normalizeAuthorityCapabilityState(capability, settings);
return (
normalizedSettings.enabled &&
normalizedSettings.primaryWhenAvailable &&
normalizedSettings.sqlPrimary &&
normalizedSettings.storageMode !== "local-primary" &&
normalizedSettings.storageMode !== "off" &&
normalizedCapability.serverPrimaryReady &&
normalizedCapability.storagePrimaryReady
);
}
function shouldProbeAuthorityForStoreSelection(settings = getSettings()) {
const normalizedSettings = normalizeAuthoritySettings(settings);
if (
!normalizedSettings.enabled ||
!normalizedSettings.primaryWhenAvailable ||
!normalizedSettings.sqlPrimary ||
normalizedSettings.storageMode === "local-primary" ||
normalizedSettings.storageMode === "off"
) {
return false;
}
if (authorityProbePromise) return true;
const lastProbeAt = Number(authorityCapabilityState?.lastProbeAt || 0);
if (!lastProbeAt) return true;
return Date.now() - lastProbeAt >= normalizedSettings.probeIntervalMs;
}
async function resolveAuthorityCapabilityForStoreSelection(settings = getSettings()) {
if (shouldProbeAuthorityForStoreSelection(settings)) {
return await refreshAuthorityRuntimeState({
source: "store-selection",
});
}
authorityCapabilityState = normalizeAuthorityCapabilityState(
authorityCapabilityState,
settings,
);
return authorityCapabilityState;
}
function buildAuthorityGraphStoreOptions(settings = getSettings()) {
const normalizedSettings = normalizeAuthoritySettings(settings);
return {
baseUrl: normalizedSettings.baseUrl,
headerProvider:
typeof getRequestHeaders === "function" ? () => getRequestHeaders() : null,
};
}
function resolveDbGraphStorePresentation(db = null) { function resolveDbGraphStorePresentation(db = null) {
if (
db?.storeKind === AUTHORITY_GRAPH_STORE_KIND ||
db?.storeMode === AUTHORITY_GRAPH_STORE_MODE
) {
return buildAuthorityStorePresentation();
}
if (db?.storeKind === "opfs" || isGraphLocalStorageModeOpfs(db?.storeMode)) { if (db?.storeKind === "opfs" || isGraphLocalStorageModeOpfs(db?.storeMode)) {
return buildOpfsStorePresentation(db?.storeMode); return buildOpfsStorePresentation(db?.storeMode);
} }
@@ -4707,6 +4793,15 @@ function resolveSnapshotGraphStorePresentation(
const snapshotPrimary = String(snapshot?.meta?.storagePrimary || "") const snapshotPrimary = String(snapshot?.meta?.storagePrimary || "")
.trim() .trim()
.toLowerCase(); .toLowerCase();
const snapshotStorageMode = String(snapshot?.meta?.storageMode || "")
.trim()
.toLowerCase();
if (
snapshotPrimary === AUTHORITY_GRAPH_STORE_KIND ||
snapshotStorageMode === AUTHORITY_GRAPH_STORE_MODE
) {
return buildAuthorityStorePresentation();
}
const snapshotMode = normalizeGraphLocalStorageMode( const snapshotMode = normalizeGraphLocalStorageMode(
snapshot?.meta?.storageMode, snapshot?.meta?.storageMode,
normalizedFallback.storageMode, normalizedFallback.storageMode,
@@ -4724,6 +4819,12 @@ function buildGraphLocalStoreSelectorKey(
presentation && typeof presentation === "object" presentation && typeof presentation === "object"
? presentation ? presentation
: buildIndexedDbStorePresentation(); : buildIndexedDbStorePresentation();
if (
normalizedPresentation.storagePrimary === AUTHORITY_GRAPH_STORE_KIND ||
normalizedPresentation.storageMode === AUTHORITY_GRAPH_STORE_MODE
) {
return `${AUTHORITY_GRAPH_STORE_KIND}:${AUTHORITY_GRAPH_STORE_MODE}`;
}
const storagePrimary = const storagePrimary =
normalizedPresentation.storagePrimary === "opfs" || normalizedPresentation.storagePrimary === "opfs" ||
isGraphLocalStorageModeOpfs(normalizedPresentation.storageMode) isGraphLocalStorageModeOpfs(normalizedPresentation.storageMode)
@@ -4829,6 +4930,9 @@ async function getGraphLocalStoreCapability(forceRefresh = false) {
} }
function getPreferredGraphLocalStorePresentationSync(settings = getSettings()) { function getPreferredGraphLocalStorePresentationSync(settings = getSettings()) {
if (shouldUseAuthorityGraphStore(settings, authorityCapabilityState)) {
return buildAuthorityStorePresentation();
}
const requestedMode = getRequestedGraphLocalStorageMode(settings); const requestedMode = getRequestedGraphLocalStorageMode(settings);
if ( if (
requestedMode === "auto" && requestedMode === "auto" &&
@@ -4845,20 +4949,25 @@ function getPreferredGraphLocalStorePresentationSync(settings = getSettings()) {
return buildIndexedDbStorePresentation(); return buildIndexedDbStorePresentation();
} }
async function resolvePreferredGraphLocalStorePresentation( async function resolvePreferredGraphLocalStorePresentation(
settings = getSettings(), settings = getSettings(),
) { ) {
const requestedMode = getRequestedGraphLocalStorageMode(settings); const authorityCapability =
if (requestedMode === "auto") { await resolveAuthorityCapabilityForStoreSelection(settings);
const capability = await getGraphLocalStoreCapability(false, { if (shouldUseAuthorityGraphStore(settings, authorityCapability)) {
settings, return buildAuthorityStorePresentation();
}); }
return capability.opfsAvailable const requestedMode = getRequestedGraphLocalStorageMode(settings);
? buildOpfsStorePresentation(BME_GRAPH_LOCAL_STORAGE_MODE_OPFS_PRIMARY) if (requestedMode === "auto") {
: buildIndexedDbStorePresentation(); const capability = await getGraphLocalStoreCapability(false, {
} settings,
if (!isGraphLocalStorageModeOpfs(requestedMode)) { });
return buildIndexedDbStorePresentation(); return capability.opfsAvailable
? buildOpfsStorePresentation(BME_GRAPH_LOCAL_STORAGE_MODE_OPFS_PRIMARY)
: buildIndexedDbStorePresentation();
}
if (!isGraphLocalStorageModeOpfs(requestedMode)) {
return buildIndexedDbStorePresentation();
} }
const capability = await getGraphLocalStoreCapability(false, { const capability = await getGraphLocalStoreCapability(false, {
@@ -4871,9 +4980,9 @@ function getPreferredGraphLocalStorePresentationSync(settings = getSettings()) {
if (!bmeLocalStoreCapabilityWarningShown) { if (!bmeLocalStoreCapabilityWarningShown) {
console.warn("[ST-BME] OPFS 不可用,已回退到 IndexedDB:", capability.reason); console.warn("[ST-BME] OPFS 不可用,已回退到 IndexedDB:", capability.reason);
bmeLocalStoreCapabilityWarningShown = true; bmeLocalStoreCapabilityWarningShown = true;
} }
return buildIndexedDbStorePresentation(); return buildIndexedDbStorePresentation();
} }
async function createPreferredGraphLocalStore( async function createPreferredGraphLocalStore(
chatId, chatId,
@@ -4881,6 +4990,12 @@ async function createPreferredGraphLocalStore(
) { ) {
const preferredLocalStore = const preferredLocalStore =
await resolvePreferredGraphLocalStorePresentation(settings); await resolvePreferredGraphLocalStorePresentation(settings);
if (
preferredLocalStore.storagePrimary === AUTHORITY_GRAPH_STORE_KIND &&
typeof AuthorityGraphStore === "function"
) {
return new AuthorityGraphStore(chatId, buildAuthorityGraphStoreOptions(settings));
}
if ( if (
preferredLocalStore.storagePrimary === "opfs" && preferredLocalStore.storagePrimary === "opfs" &&
typeof OpfsGraphStore === "function" typeof OpfsGraphStore === "function"
@@ -4903,11 +5018,18 @@ async function refreshCurrentChatLocalStoreBinding(
const normalizedChatId = normalizeChatIdCandidate(chatId); const normalizedChatId = normalizeChatIdCandidate(chatId);
const settings = getSettings(); const settings = getSettings();
const requestedMode = getRequestedGraphLocalStorageMode(settings); const requestedMode = getRequestedGraphLocalStorageMode(settings);
const authorityCapability =
await resolveAuthorityCapabilityForStoreSelection(settings);
const authorityPrimary = shouldUseAuthorityGraphStore(
settings,
authorityCapability,
);
const shouldProbeCapability = const shouldProbeCapability =
forceCapabilityRefresh === true || !authorityPrimary &&
!bmeLocalStoreCapabilitySnapshot.checked || (forceCapabilityRefresh === true ||
requestedMode === "auto" || !bmeLocalStoreCapabilitySnapshot.checked ||
isGraphLocalStorageModeOpfs(requestedMode); requestedMode === "auto" ||
isGraphLocalStorageModeOpfs(requestedMode));
if (shouldProbeCapability) { if (shouldProbeCapability) {
await getGraphLocalStoreCapability(forceCapabilityRefresh === true, { await getGraphLocalStoreCapability(forceCapabilityRefresh === true, {
@@ -14150,7 +14272,8 @@ async function saveGraphToIndexedDb(
const shouldScheduleCloudUpload = const shouldScheduleCloudUpload =
scheduleCloudUploadOption != null scheduleCloudUploadOption != null
? scheduleCloudUploadOption === true ? scheduleCloudUploadOption === true
: persistenceEnvironment.hostProfile !== "luker" && : persistenceEnvironment.primaryStorageTier !== "authority-sql" &&
persistenceEnvironment.hostProfile !== "luker" &&
persistRole !== "cache-mirror"; persistRole !== "cache-mirror";
const directPersistDelta = const directPersistDelta =
persistDelta && persistDelta &&
@@ -14521,7 +14644,9 @@ async function saveGraphToIndexedDb(
snapshot.meta.lastMutationReason = String(reason || "graph-save"); snapshot.meta.lastMutationReason = String(reason || "graph-save");
snapshot.meta.storagePrimary = localStore.storagePrimary; snapshot.meta.storagePrimary = localStore.storagePrimary;
snapshot.meta.storageMode = localStore.storageMode; snapshot.meta.storageMode = localStore.storageMode;
cacheIndexedDbSnapshot(normalizedChatId, snapshot); if (localStore.storagePrimary !== AUTHORITY_GRAPH_STORE_KIND) {
cacheIndexedDbSnapshot(normalizedChatId, snapshot);
}
} }
if (dirtyPersistDeltaVersion > 0) { if (dirtyPersistDeltaVersion > 0) {
@@ -15276,7 +15401,8 @@ function saveGraphToChat(options = {}) {
} }
const shouldQueueIndexedDbPersist = const shouldQueueIndexedDbPersist =
persistenceEnvironment.hostProfile !== "luker" && (persistenceEnvironment.hostProfile !== "luker" ||
persistenceEnvironment.primaryStorageTier === "authority-sql") &&
(markMutation || !isGraphEffectivelyEmpty(currentGraph)); (markMutation || !isGraphEffectivelyEmpty(currentGraph));
if (shouldQueueIndexedDbPersist) { if (shouldQueueIndexedDbPersist) {
queueGraphPersistToIndexedDb(chatId, currentGraph, { queueGraphPersistToIndexedDb(chatId, currentGraph, {
@@ -15305,7 +15431,7 @@ function saveGraphToChat(options = {}) {
} }
} }
if (persistenceEnvironment.hostProfile === "luker") { if (persistenceEnvironment.primaryStorageTier === "luker-chat-state") {
const persistGraph = cloneGraphForPersistence(currentGraph, chatId); const persistGraph = cloneGraphForPersistence(currentGraph, chatId);
const chatStateTarget = resolveCurrentChatStateTarget(context); const chatStateTarget = resolveCurrentChatStateTarget(context);
const lastProcessedAssistantFloor = Number.isFinite( const lastProcessedAssistantFloor = Number.isFinite(

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,331 @@
import assert from "node:assert/strict";
import {
AUTHORITY_GRAPH_STORE_KIND,
AUTHORITY_GRAPH_STORE_MODE,
AuthorityGraphStore,
AuthoritySqlHttpClient,
} from "../sync/authority-graph-store.js";
import {
BME_DB_SCHEMA_VERSION,
BME_TOMBSTONE_RETENTION_MS,
} from "../sync/bme-db.js";
const PREFIX = "[ST-BME][authority-graph-store]";
class MockAuthoritySqlClient {
constructor() {
this.meta = new Map();
this.nodes = new Map();
this.edges = new Map();
this.tombstones = new Map();
this.statements = [];
}
async transaction(statements = []) {
for (const statement of statements) {
await this.execute(statement.sql, statement.params || {});
}
return { executed: statements.length };
}
async execute(sql, params = {}) {
this.statements.push({ sql, params });
const normalizedSql = String(sql || "").toLowerCase();
if (normalizedSql.startsWith("create table")) {
return { ok: true };
}
if (normalizedSql.includes("insert into st_bme_graph_meta")) {
this.meta.set(this._key(params.chatId, params.key), {
chat_id: params.chatId,
meta_key: params.key,
value_json: params.valueJson,
updated_at: params.updatedAt,
});
return { ok: true };
}
if (normalizedSql.includes("insert into st_bme_graph_nodes")) {
this.nodes.set(this._key(params.chatId, params.id), {
chat_id: params.chatId,
record_id: params.id,
payload_json: params.payloadJson,
node_type: params.type,
source_floor: params.sourceFloor,
archived: params.archived,
updated_at: params.updatedAt,
deleted_at: params.deletedAt,
});
return { ok: true };
}
if (normalizedSql.includes("insert into st_bme_graph_edges")) {
this.edges.set(this._key(params.chatId, params.id), {
chat_id: params.chatId,
record_id: params.id,
payload_json: params.payloadJson,
from_id: params.fromId,
to_id: params.toId,
relation: params.relation,
source_floor: params.sourceFloor,
updated_at: params.updatedAt,
deleted_at: params.deletedAt,
});
return { ok: true };
}
if (normalizedSql.includes("insert into st_bme_graph_tombstones")) {
this.tombstones.set(this._key(params.chatId, params.id), {
chat_id: params.chatId,
record_id: params.id,
payload_json: params.payloadJson,
tombstone_kind: params.kind,
target_id: params.targetId,
deleted_at: params.deletedAt,
source_device_id: params.sourceDeviceId,
});
return { ok: true };
}
if (normalizedSql.startsWith("delete from st_bme_graph_nodes")) {
this._deleteRows(this.nodes, params);
return { ok: true };
}
if (normalizedSql.startsWith("delete from st_bme_graph_edges")) {
this._deleteRows(this.edges, params);
return { ok: true };
}
if (normalizedSql.startsWith("delete from st_bme_graph_tombstones")) {
this._deleteRows(this.tombstones, params);
return { ok: true };
}
if (normalizedSql.startsWith("delete from st_bme_graph_meta")) {
this._deleteRows(this.meta, params);
return { ok: true };
}
throw new Error(`Unhandled SQL execute: ${sql}`);
}
async query(sql, params = {}) {
const normalizedSql = String(sql || "").toLowerCase();
if (normalizedSql.includes("from st_bme_graph_meta")) {
return this._readRows(this.meta, params).map((row) => ({
key: row.meta_key,
valueJson: row.value_json,
}));
}
if (normalizedSql.includes("from st_bme_graph_nodes")) {
if (normalizedSql.includes("count(*)")) {
return [{ count: this._readRows(this.nodes, params).length }];
}
return this._readRows(this.nodes, params).map((row) => ({
payloadJson: row.payload_json,
}));
}
if (normalizedSql.includes("from st_bme_graph_edges")) {
if (normalizedSql.includes("count(*)")) {
return [{ count: this._readRows(this.edges, params).length }];
}
return this._readRows(this.edges, params).map((row) => ({
payloadJson: row.payload_json,
}));
}
if (normalizedSql.includes("from st_bme_graph_tombstones")) {
if (normalizedSql.includes("count(*)")) {
return [{ count: this._readRows(this.tombstones, params).length }];
}
if (normalizedSql.includes("deleted_at <")) {
return this._readRows(this.tombstones, params)
.filter((row) => Number(row.deleted_at) < Number(params.cutoffMs))
.map((row) => ({ id: row.record_id }));
}
return this._readRows(this.tombstones, params).map((row) => ({
payloadJson: row.payload_json,
}));
}
throw new Error(`Unhandled SQL query: ${sql}`);
}
_key(chatId, id) {
return `${String(chatId || "")}\u0000${String(id || "")}`;
}
_readRows(table, params = {}) {
const chatId = String(params.chatId || "");
const id = params.id ?? params.key;
return Array.from(table.values()).filter((row) => {
if (String(row.chat_id || "") !== chatId) return false;
if (id == null) return true;
return String(row.record_id ?? row.meta_key ?? "") === String(id);
});
}
_deleteRows(table, params = {}) {
const chatId = String(params.chatId || "");
const id = params.id ?? params.key;
for (const [key, row] of table.entries()) {
if (String(row.chat_id || "") !== chatId) continue;
if (id != null && String(row.record_id ?? row.meta_key ?? "") !== String(id)) continue;
table.delete(key);
}
}
}
async function testOpenSeedsAuthorityMeta() {
const sqlClient = new MockAuthoritySqlClient();
const store = new AuthorityGraphStore("authority-chat-a", { sqlClient });
await store.open();
assert.equal(store.storeKind, AUTHORITY_GRAPH_STORE_KIND);
assert.equal(store.storeMode, AUTHORITY_GRAPH_STORE_MODE);
assert.equal(await store.getMeta("schemaVersion"), BME_DB_SCHEMA_VERSION);
assert.equal(await store.getMeta("storagePrimary"), AUTHORITY_GRAPH_STORE_KIND);
assert.equal(await store.getRevision(), 0);
const diagnostics = store.getStorageDiagnosticsSync();
assert.equal(diagnostics.storageKind, AUTHORITY_GRAPH_STORE_KIND);
assert.equal(diagnostics.browserCacheMode, "minimal");
}
async function testImportCommitAndExportSnapshot() {
const sqlClient = new MockAuthoritySqlClient();
const store = new AuthorityGraphStore("authority-chat-b", { sqlClient });
await store.open();
const importResult = await store.importSnapshot(
{
meta: {
revision: 7,
lastProcessedFloor: 3,
extractionCount: 4,
},
nodes: [
{ id: "node-1", type: "event", sourceFloor: 1, updatedAt: 10 },
{ id: "node-2", type: "event", archived: true, updatedAt: 20 },
{ id: "node-3", type: "memory", deletedAt: 30, updatedAt: 30 },
],
edges: [
{
id: "edge-1",
fromId: "node-1",
toId: "node-3",
relation: "refers",
updatedAt: 40,
},
],
tombstones: [
{
id: "tombstone-1",
kind: "node",
targetId: "node-old",
deletedAt: 50,
},
],
},
{ preserveRevision: true },
);
assert.equal(importResult.revision, 7);
assert.deepEqual(importResult.imported, { nodes: 3, edges: 1, tombstones: 1 });
assert.equal((await store.listNodes()).length, 3);
assert.deepEqual(
(await store.listNodes({ includeArchived: false, includeDeleted: false })).map((node) => node.id),
["node-1"],
);
assert.deepEqual((await store.listEdges({ relation: "refers" })).map((edge) => edge.id), ["edge-1"]);
const commitResult = await store.commitDelta(
{
upsertNodes: [{ id: "node-4", type: "event", updatedAt: 60 }],
deleteNodeIds: ["node-2"],
countDelta: {
previous: { nodes: 3, edges: 1, tombstones: 1 },
delta: { nodes: 0, edges: 0, tombstones: 0 },
},
runtimeMetaPatch: {
lastProcessedFloor: 8,
revision: 999,
},
},
{
reason: "test-commit",
requestedRevision: 9,
},
);
assert.equal(commitResult.revision, 9);
assert.deepEqual(commitResult.imported, { nodes: 3, edges: 1, tombstones: 1 });
assert.equal(await store.getMeta("lastProcessedFloor"), 8);
assert.equal(await store.getRevision(), 9);
assert.equal(await store.getMeta("lastMutationReason"), "test-commit");
assert.equal(await store.getMeta("syncDirty"), true);
assert.deepEqual((await store.listNodes()).map((node) => node.id).sort(), ["node-1", "node-3", "node-4"]);
const snapshot = await store.exportSnapshot();
assert.equal(snapshot.meta.revision, 9);
assert.equal(snapshot.meta.storagePrimary, AUTHORITY_GRAPH_STORE_KIND);
assert.equal(snapshot.meta.storageMode, AUTHORITY_GRAPH_STORE_MODE);
assert.equal(snapshot.meta.nodeCount, 3);
assert.equal(snapshot.nodes.length, 3);
assert.equal(snapshot.edges.length, 1);
assert.equal(snapshot.tombstones.length, 1);
assert.equal(snapshot.state.lastProcessedFloor, 8);
}
async function testPruneAndClear() {
const sqlClient = new MockAuthoritySqlClient();
const store = new AuthorityGraphStore("authority-chat-c", { sqlClient });
await store.importSnapshot({
nodes: [{ id: "node-1", type: "event", updatedAt: 1 }],
tombstones: [
{ id: "old-tombstone", kind: "node", targetId: "old", deletedAt: 1 },
{
id: "new-tombstone",
kind: "node",
targetId: "new",
deletedAt: BME_TOMBSTONE_RETENTION_MS,
},
],
});
const pruneResult = await store.pruneExpiredTombstones(BME_TOMBSTONE_RETENTION_MS + 100);
assert.equal(pruneResult.pruned, 1);
assert.deepEqual((await store.listTombstones()).map((item) => item.id), ["new-tombstone"]);
const clearResult = await store.clearAll();
assert.equal(clearResult.cleared, true);
assert.equal((await store.isEmpty({ includeTombstones: true })).empty, true);
assert.equal(await store.getMeta("storagePrimary"), AUTHORITY_GRAPH_STORE_KIND);
}
async function testHttpSqlClientBoundary() {
const requests = [];
const client = new AuthoritySqlHttpClient({
baseUrl: "https://authority.example.test/root/",
headerProvider: () => ({ "X-Test": "1" }),
fetchImpl: async (url, init) => {
requests.push({ url, init });
return {
ok: true,
status: 200,
async json() {
return { rows: [{ value: 1 }] };
},
};
},
});
const result = await client.query("SELECT 1", { chatId: "chat" });
assert.deepEqual(result, { rows: [{ value: 1 }] });
assert.equal(requests[0].url, "https://authority.example.test/root/v1/sql");
assert.equal(requests[0].init.method, "POST");
assert.equal(requests[0].init.headers["X-Test"], "1");
assert.deepEqual(JSON.parse(requests[0].init.body), {
action: "query",
sql: "SELECT 1",
params: { chatId: "chat" },
});
}
await testOpenSeedsAuthorityMeta();
await testImportCommitAndExportSnapshot();
await testPruneAndClear();
await testHttpSqlClientBoundary();
console.log(`${PREFIX} all tests passed`);

View File

@@ -101,6 +101,22 @@ import {
getPersistedSettingsSnapshot, getPersistedSettingsSnapshot,
mergePersistedSettings, mergePersistedSettings,
} from "../runtime/settings-defaults.js"; } from "../runtime/settings-defaults.js";
import {
createDefaultAuthorityCapabilityState,
normalizeAuthoritySettings,
normalizeAuthorityCapabilityState,
probeAuthorityCapabilities,
} from "../runtime/authority-capabilities.js";
import {
createAuthorityBrowserState,
getAuthorityBrowserStateSnapshot,
normalizeAuthorityBrowserState,
} from "../sync/authority-browser-state.js";
import {
AUTHORITY_GRAPH_STORE_KIND,
AUTHORITY_GRAPH_STORE_MODE,
AuthorityGraphStore,
} from "../sync/authority-graph-store.js";
import { import {
clampFloat, clampFloat,
clampInt, clampInt,
@@ -434,6 +450,16 @@ async function createGraphPersistenceHarness({
defaultSettings, defaultSettings,
getPersistedSettingsSnapshot, getPersistedSettingsSnapshot,
mergePersistedSettings, mergePersistedSettings,
createDefaultAuthorityCapabilityState,
normalizeAuthoritySettings,
normalizeAuthorityCapabilityState,
probeAuthorityCapabilities,
createAuthorityBrowserState,
getAuthorityBrowserStateSnapshot,
normalizeAuthorityBrowserState,
AUTHORITY_GRAPH_STORE_KIND,
AUTHORITY_GRAPH_STORE_MODE,
AuthorityGraphStore,
migrateLegacyTaskProfiles(settings = {}) { migrateLegacyTaskProfiles(settings = {}) {
return { return {
taskProfilesVersion: Number(settings?.taskProfilesVersion || 0), taskProfilesVersion: Number(settings?.taskProfilesVersion || 0),

View File

@@ -46,6 +46,17 @@ import {
defaultSettings, defaultSettings,
mergePersistedSettings, mergePersistedSettings,
} from "../../runtime/settings-defaults.js"; } from "../../runtime/settings-defaults.js";
import {
createDefaultAuthorityCapabilityState,
normalizeAuthoritySettings,
normalizeAuthorityCapabilityState,
probeAuthorityCapabilities,
} from "../../runtime/authority-capabilities.js";
import {
createAuthorityBrowserState,
getAuthorityBrowserStateSnapshot,
normalizeAuthorityBrowserState,
} from "../../sync/authority-browser-state.js";
const moduleDir = path.dirname(fileURLToPath(import.meta.url)); const moduleDir = path.dirname(fileURLToPath(import.meta.url));
const indexPath = path.resolve(moduleDir, "../../index.js"); const indexPath = path.resolve(moduleDir, "../../index.js");
@@ -89,6 +100,13 @@ export function createGenerationRecallHarness(options = {}) {
_panelModule: null, _panelModule: null,
defaultSettings, defaultSettings,
mergePersistedSettings, mergePersistedSettings,
createDefaultAuthorityCapabilityState,
normalizeAuthoritySettings,
normalizeAuthorityCapabilityState,
probeAuthorityCapabilities,
createAuthorityBrowserState,
getAuthorityBrowserStateSnapshot,
normalizeAuthorityBrowserState,
settings: {}, settings: {},
graphPersistenceState: createGraphPersistenceState(), graphPersistenceState: createGraphPersistenceState(),
extension_settings: { [MODULE_NAME]: {} }, extension_settings: { [MODULE_NAME]: {} },

View File

@@ -30,6 +30,7 @@ await fs.writeFile(
tempModulePath, tempModulePath,
` `
const GRAPH_LOAD_STATES = { SHADOW_RESTORED: "shadow-restored", LOADED: "loaded" }; const GRAPH_LOAD_STATES = { SHADOW_RESTORED: "shadow-restored", LOADED: "loaded" };
const AUTHORITY_GRAPH_STORE_KIND = "authority";
let currentGraph = null; let currentGraph = null;
let graphPersistenceState = { let graphPersistenceState = {
metadataIntegrity: "", metadataIntegrity: "",