mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
feat(authority): add graph SQL store
This commit is contained in:
182
index.js
182
index.js
@@ -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(
|
||||||
|
|||||||
1092
sync/authority-graph-store.js
Normal file
1092
sync/authority-graph-store.js
Normal file
File diff suppressed because it is too large
Load Diff
331
tests/authority-graph-store.mjs
Normal file
331
tests/authority-graph-store.mjs
Normal 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`);
|
||||||
@@ -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),
|
||||||
|
|||||||
@@ -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]: {} },
|
||||||
|
|||||||
@@ -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: "",
|
||||||
|
|||||||
Reference in New Issue
Block a user