mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
feat(authority): migrate local graphs to server primary
This commit is contained in:
355
index.js
355
index.js
@@ -50,6 +50,7 @@ import {
|
||||
autoSyncOnVisibility,
|
||||
backupToServer,
|
||||
buildRestoreSafetyChatId,
|
||||
createRestoreSafetySnapshot,
|
||||
deleteRemoteSyncFile,
|
||||
deleteServerBackup,
|
||||
getRestoreSafetySnapshotStatus,
|
||||
@@ -264,6 +265,7 @@ import {
|
||||
createAuthorityBrowserState,
|
||||
getAuthorityBrowserStateSnapshot,
|
||||
normalizeAuthorityBrowserState,
|
||||
recordAuthorityAcceptedRevision,
|
||||
} from "./sync/authority-browser-state.js";
|
||||
import { retrieve } from "./retrieval/retriever.js";
|
||||
import {
|
||||
@@ -1502,6 +1504,103 @@ function buildAuthorityPersistenceStatePatch(settings = getSettings()) {
|
||||
};
|
||||
}
|
||||
|
||||
function isAuthorityGraphStorePresentation(presentation = null) {
|
||||
if (!presentation || typeof presentation !== "object") return false;
|
||||
return (
|
||||
presentation.storagePrimary === AUTHORITY_GRAPH_STORE_KIND ||
|
||||
presentation.storageMode === AUTHORITY_GRAPH_STORE_MODE
|
||||
);
|
||||
}
|
||||
|
||||
function isAuthorityGraphStoreDb(db = null) {
|
||||
return (
|
||||
db?.storeKind === AUTHORITY_GRAPH_STORE_KIND ||
|
||||
db?.storeMode === AUTHORITY_GRAPH_STORE_MODE
|
||||
);
|
||||
}
|
||||
|
||||
function recordAuthorityAcceptedRevisionPointer({
|
||||
revision = 0,
|
||||
integrity = "",
|
||||
committedAt = Date.now(),
|
||||
} = {}) {
|
||||
const settings = getSettings();
|
||||
authorityBrowserState = recordAuthorityAcceptedRevision(
|
||||
authorityBrowserState,
|
||||
{
|
||||
revision: normalizeIndexedDbRevision(revision),
|
||||
integrity: String(integrity || ""),
|
||||
committedAt,
|
||||
},
|
||||
settings,
|
||||
committedAt,
|
||||
);
|
||||
updateGraphPersistenceState(buildAuthorityPersistenceStatePatch(settings));
|
||||
return authorityBrowserState;
|
||||
}
|
||||
|
||||
async function captureAuthorityMigrationSafetySnapshot(
|
||||
chatId,
|
||||
snapshot,
|
||||
{ source = "authority-migration", reason = "authority-migration-safety" } = {},
|
||||
) {
|
||||
const normalizedChatId = normalizeChatIdCandidate(chatId);
|
||||
if (!normalizedChatId || !isIndexedDbSnapshotMeaningful(snapshot)) {
|
||||
return {
|
||||
captured: false,
|
||||
reason: "authority-migration-safety-source-empty",
|
||||
chatId: normalizedChatId || "",
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
await createRestoreSafetySnapshot(
|
||||
normalizedChatId,
|
||||
snapshot,
|
||||
buildBmeSyncRuntimeOptions({
|
||||
reason,
|
||||
trigger: String(source || "authority-migration"),
|
||||
}),
|
||||
);
|
||||
const migrationGraph = buildGraphFromSnapshot(snapshot, {
|
||||
chatId: normalizedChatId,
|
||||
});
|
||||
const revision = normalizeIndexedDbRevision(snapshot?.meta?.revision);
|
||||
const identity = resolveCurrentChatIdentity(getContext());
|
||||
const integrity = String(
|
||||
snapshot?.meta?.integrity || identity?.integrity || "",
|
||||
).trim();
|
||||
let shadowCaptured = false;
|
||||
try {
|
||||
shadowCaptured = writeGraphShadowSnapshot(normalizedChatId, migrationGraph, {
|
||||
revision,
|
||||
reason,
|
||||
integrity,
|
||||
debugReason: String(source || "authority-migration"),
|
||||
});
|
||||
} catch (shadowError) {
|
||||
console.warn("[ST-BME] Authority 迁移影子安全快照创建失败:", shadowError);
|
||||
}
|
||||
return {
|
||||
captured: true,
|
||||
restoreSafetyCaptured: true,
|
||||
shadowCaptured,
|
||||
reason: "authority-migration-restore-safety-created",
|
||||
chatId: normalizedChatId,
|
||||
revision,
|
||||
integrity,
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn("[ST-BME] Authority 迁移安全快照创建失败:", error);
|
||||
return {
|
||||
captured: false,
|
||||
reason: "authority-migration-safety-failed",
|
||||
chatId: normalizedChatId,
|
||||
error: error?.message || String(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshAuthorityRuntimeState({
|
||||
force = false,
|
||||
source = "authority-refresh",
|
||||
@@ -1688,6 +1787,22 @@ function getGraphPersistenceLiveState() {
|
||||
authorityRuntime.capability.lastError ||
|
||||
"",
|
||||
),
|
||||
authorityMigrationState: String(
|
||||
graphPersistenceState.authorityMigrationState || "idle",
|
||||
),
|
||||
authorityMigrationSource: String(
|
||||
graphPersistenceState.authorityMigrationSource || "",
|
||||
),
|
||||
authorityMigrationRevision: Number(
|
||||
graphPersistenceState.authorityMigrationRevision || 0,
|
||||
),
|
||||
authorityMigrationLastError: String(
|
||||
graphPersistenceState.authorityMigrationLastError || "",
|
||||
),
|
||||
lastAuthorityMigrationResult: cloneRuntimeDebugValue(
|
||||
graphPersistenceState.lastAuthorityMigrationResult,
|
||||
null,
|
||||
),
|
||||
resolvedLocalStore: String(
|
||||
graphPersistenceState.resolvedLocalStore ||
|
||||
buildGraphLocalStoreSelectorKey(getPreferredGraphLocalStorePresentationSync()),
|
||||
@@ -5714,7 +5829,14 @@ async function importRecoveredSnapshotToIndexedDb(
|
||||
targetDb,
|
||||
targetChatId,
|
||||
graph,
|
||||
{ revision = 0, integrity = "", source = "identity-recovery", legacyChatId = "" } = {},
|
||||
{
|
||||
revision = 0,
|
||||
integrity = "",
|
||||
source = "identity-recovery",
|
||||
legacyChatId = "",
|
||||
markSyncDirty = true,
|
||||
beforeImport = null,
|
||||
} = {},
|
||||
) {
|
||||
const snapshot = buildRecoveredSnapshotForChatIdentity(graph, targetChatId, {
|
||||
revision,
|
||||
@@ -5722,11 +5844,14 @@ async function importRecoveredSnapshotToIndexedDb(
|
||||
source,
|
||||
legacyChatId,
|
||||
});
|
||||
if (typeof beforeImport === "function") {
|
||||
await beforeImport(snapshot);
|
||||
}
|
||||
const importResult = await targetDb.importSnapshot(snapshot, {
|
||||
mode: "replace",
|
||||
preserveRevision: true,
|
||||
revision: snapshot.meta.revision,
|
||||
markSyncDirty: true,
|
||||
markSyncDirty,
|
||||
});
|
||||
snapshot.meta.revision = normalizeIndexedDbRevision(
|
||||
importResult?.revision,
|
||||
@@ -6154,9 +6279,11 @@ function buildBmeSyncRuntimeOptions(extra = {}) {
|
||||
return await manager.getCurrentDb(chatId);
|
||||
},
|
||||
getSafetyDb: async (chatId) => {
|
||||
const safetyDb = await createPreferredGraphLocalStore(
|
||||
buildRestoreSafetyChatId(chatId),
|
||||
);
|
||||
const safetyChatId = buildRestoreSafetyChatId(chatId);
|
||||
const safetyStore = await resolvePreferredGraphLocalStorePresentation();
|
||||
const safetyDb = isAuthorityGraphStorePresentation(safetyStore)
|
||||
? new BmeDatabase(safetyChatId)
|
||||
: await createPreferredGraphLocalStore(safetyChatId);
|
||||
await safetyDb.open();
|
||||
return safetyDb;
|
||||
},
|
||||
@@ -6558,6 +6685,7 @@ function cacheIndexedDbSnapshot(chatId, snapshot = null) {
|
||||
if (!normalizedChatId || !snapshot || typeof snapshot !== "object") return;
|
||||
if (snapshot.__stBmeTombstonesOmitted === true) return;
|
||||
const snapshotStore = resolveSnapshotGraphStorePresentation(snapshot);
|
||||
if (snapshotStore.storagePrimary === AUTHORITY_GRAPH_STORE_KIND) return;
|
||||
bmeIndexedDbSnapshotCacheByChatId.set(normalizedChatId, {
|
||||
chatId: normalizedChatId,
|
||||
revision: normalizeIndexedDbRevision(snapshot?.meta?.revision),
|
||||
@@ -8797,6 +8925,8 @@ async function maybeRecoverIndexedDbGraphFromStableIdentity(
|
||||
chatId: normalizedChatId,
|
||||
};
|
||||
}
|
||||
const targetStore = resolveDbGraphStorePresentation(targetDb);
|
||||
const authorityTarget = isAuthorityGraphStorePresentation(targetStore);
|
||||
|
||||
const emptyStatus = await targetDb.isEmpty();
|
||||
if (!emptyStatus?.empty) {
|
||||
@@ -8817,6 +8947,7 @@ async function maybeRecoverIndexedDbGraphFromStableIdentity(
|
||||
shadowChatId = "",
|
||||
} = {},
|
||||
) => {
|
||||
let safetySnapshotResult = null;
|
||||
const snapshot = await importRecoveredSnapshotToIndexedDb(
|
||||
targetDb,
|
||||
normalizedChatId,
|
||||
@@ -8826,9 +8957,24 @@ async function maybeRecoverIndexedDbGraphFromStableIdentity(
|
||||
integrity: identity.integrity,
|
||||
source: migrationSource,
|
||||
legacyChatId,
|
||||
markSyncDirty: !authorityTarget,
|
||||
beforeImport: authorityTarget
|
||||
? async (candidateSnapshot) => {
|
||||
safetySnapshotResult = await captureAuthorityMigrationSafetySnapshot(
|
||||
normalizedChatId,
|
||||
candidateSnapshot,
|
||||
{
|
||||
source: migrationSource,
|
||||
reason: "authority-identity-recovery-safety",
|
||||
},
|
||||
);
|
||||
}
|
||||
: null,
|
||||
},
|
||||
);
|
||||
cacheIndexedDbSnapshot(normalizedChatId, snapshot);
|
||||
if (!authorityTarget) {
|
||||
cacheIndexedDbSnapshot(normalizedChatId, snapshot);
|
||||
}
|
||||
rememberResolvedGraphIdentityAlias(context, normalizedChatId);
|
||||
|
||||
if (shadowChatId && shadowChatId !== normalizedChatId) {
|
||||
@@ -8840,32 +8986,49 @@ async function maybeRecoverIndexedDbGraphFromStableIdentity(
|
||||
reason: "identity-recovery-sync-skipped",
|
||||
chatId: normalizedChatId,
|
||||
};
|
||||
try {
|
||||
syncResult = await syncNow(
|
||||
normalizedChatId,
|
||||
buildBmeSyncRuntimeOptions({
|
||||
reason: "identity-recovery",
|
||||
trigger: `${String(source || "identity-recovery")}:identity-recovery`,
|
||||
}),
|
||||
);
|
||||
} catch (syncError) {
|
||||
console.warn("[ST-BME] 身份恢复后的同步失败:", syncError);
|
||||
if (authorityTarget) {
|
||||
const acceptedRevision = normalizeIndexedDbRevision(snapshot?.meta?.revision);
|
||||
recordAuthorityAcceptedRevisionPointer({
|
||||
revision: acceptedRevision,
|
||||
integrity: snapshot?.meta?.integrity || identity.integrity,
|
||||
});
|
||||
syncResult = {
|
||||
synced: false,
|
||||
reason: "identity-recovery-sync-failed",
|
||||
reason: "authority-primary-legacy-sync-skipped",
|
||||
chatId: normalizedChatId,
|
||||
error: syncError?.message || String(syncError),
|
||||
};
|
||||
} else {
|
||||
try {
|
||||
syncResult = await syncNow(
|
||||
normalizedChatId,
|
||||
buildBmeSyncRuntimeOptions({
|
||||
reason: "identity-recovery",
|
||||
trigger: `${String(source || "identity-recovery")}:identity-recovery`,
|
||||
}),
|
||||
);
|
||||
} catch (syncError) {
|
||||
console.warn("[ST-BME] 身份恢复后的同步失败:", syncError);
|
||||
syncResult = {
|
||||
synced: false,
|
||||
reason: "identity-recovery-sync-failed",
|
||||
chatId: normalizedChatId,
|
||||
error: syncError?.message || String(syncError),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
migrated: true,
|
||||
reason: "identity-recovery-completed",
|
||||
reason: authorityTarget
|
||||
? "authority-identity-recovery-completed"
|
||||
: "identity-recovery-completed",
|
||||
chatId: normalizedChatId,
|
||||
legacyChatId: normalizeChatIdCandidate(legacyChatId),
|
||||
source: migrationSource,
|
||||
snapshot,
|
||||
syncResult,
|
||||
safetySnapshotResult,
|
||||
targetStore,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -9013,6 +9176,8 @@ async function maybeMigrateLegacyGraphToIndexedDb(
|
||||
chatId: normalizedChatId,
|
||||
};
|
||||
}
|
||||
const targetStore = resolveDbGraphStorePresentation(targetDb);
|
||||
const authorityTarget = isAuthorityGraphStorePresentation(targetStore);
|
||||
|
||||
const contextChatId = resolveCurrentChatIdentity(context).chatId;
|
||||
if (contextChatId && contextChatId !== normalizedChatId) {
|
||||
@@ -9062,9 +9227,26 @@ async function maybeMigrateLegacyGraphToIndexedDb(
|
||||
normalizeIndexedDbRevision(getGraphPersistedRevision(legacyGraph), 0),
|
||||
1,
|
||||
);
|
||||
const safetySnapshotResult = authorityTarget
|
||||
? await captureAuthorityMigrationSafetySnapshot(
|
||||
normalizedChatId,
|
||||
buildSnapshotFromGraph(legacyGraph, {
|
||||
chatId: normalizedChatId,
|
||||
revision: legacyRevision,
|
||||
meta: {
|
||||
migrationSource: "chat_metadata",
|
||||
},
|
||||
}),
|
||||
{
|
||||
source: "chat_metadata",
|
||||
reason: "authority-chat-metadata-migration-safety",
|
||||
},
|
||||
)
|
||||
: null;
|
||||
const migrationResult = await targetDb.importLegacyGraph(legacyGraph, {
|
||||
source: "chat_metadata",
|
||||
revision: legacyRevision,
|
||||
markSyncDirty: !authorityTarget,
|
||||
});
|
||||
if (!migrationResult?.migrated) {
|
||||
return {
|
||||
@@ -9076,7 +9258,18 @@ async function maybeMigrateLegacyGraphToIndexedDb(
|
||||
}
|
||||
|
||||
const postMigrationSnapshot = await targetDb.exportSnapshot();
|
||||
cacheIndexedDbSnapshot(normalizedChatId, postMigrationSnapshot);
|
||||
if (!authorityTarget) {
|
||||
cacheIndexedDbSnapshot(normalizedChatId, postMigrationSnapshot);
|
||||
}
|
||||
if (authorityTarget) {
|
||||
recordAuthorityAcceptedRevisionPointer({
|
||||
revision:
|
||||
postMigrationSnapshot?.meta?.revision ||
|
||||
migrationResult?.revision ||
|
||||
legacyRevision,
|
||||
integrity: postMigrationSnapshot?.meta?.integrity,
|
||||
});
|
||||
}
|
||||
debugDebug("[ST-BME] legacy chat_metadata 图谱迁移完成", {
|
||||
source,
|
||||
chatId: normalizedChatId,
|
||||
@@ -9092,31 +9285,43 @@ async function maybeMigrateLegacyGraphToIndexedDb(
|
||||
reason: "post-migration-sync-skipped",
|
||||
chatId: normalizedChatId,
|
||||
};
|
||||
try {
|
||||
syncResult = await syncNow(
|
||||
normalizedChatId,
|
||||
buildBmeSyncRuntimeOptions({
|
||||
reason: "post-migration",
|
||||
trigger: `${String(source || "migration")}:post-migration`,
|
||||
}),
|
||||
);
|
||||
} catch (syncError) {
|
||||
console.warn("[ST-BME] legacy 迁移后立即同步失败:", syncError);
|
||||
if (authorityTarget) {
|
||||
syncResult = {
|
||||
synced: false,
|
||||
reason: "post-migration-sync-failed",
|
||||
reason: "authority-primary-legacy-sync-skipped",
|
||||
chatId: normalizedChatId,
|
||||
error: syncError?.message || String(syncError),
|
||||
};
|
||||
} else {
|
||||
try {
|
||||
syncResult = await syncNow(
|
||||
normalizedChatId,
|
||||
buildBmeSyncRuntimeOptions({
|
||||
reason: "post-migration",
|
||||
trigger: `${String(source || "migration")}:post-migration`,
|
||||
}),
|
||||
);
|
||||
} catch (syncError) {
|
||||
console.warn("[ST-BME] legacy 迁移后立即同步失败:", syncError);
|
||||
syncResult = {
|
||||
synced: false,
|
||||
reason: "post-migration-sync-failed",
|
||||
chatId: normalizedChatId,
|
||||
error: syncError?.message || String(syncError),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
migrated: true,
|
||||
reason: "migration-completed",
|
||||
reason: authorityTarget
|
||||
? "authority-chat-metadata-migration-completed"
|
||||
: "migration-completed",
|
||||
chatId: normalizedChatId,
|
||||
migrationResult,
|
||||
snapshot: postMigrationSnapshot,
|
||||
syncResult,
|
||||
safetySnapshotResult,
|
||||
targetStore,
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn("[ST-BME] legacy chat_metadata 迁移失败:", error);
|
||||
@@ -9179,6 +9384,7 @@ async function maybeImportLegacyIndexedDbSnapshotToLocalStore(
|
||||
}
|
||||
|
||||
const targetStore = resolveDbGraphStorePresentation(targetDb);
|
||||
const authorityTarget = isAuthorityGraphStorePresentation(targetStore);
|
||||
if (targetStore.storagePrimary === "indexeddb") {
|
||||
return {
|
||||
migrated: false,
|
||||
@@ -9225,14 +9431,29 @@ async function maybeImportLegacyIndexedDbSnapshotToLocalStore(
|
||||
!Array.isArray(legacySnapshot.state)
|
||||
? legacySnapshot.state
|
||||
: {};
|
||||
const migrationSource = authorityTarget
|
||||
? "legacy_indexeddb_to_authority"
|
||||
: "legacy_indexeddb_snapshot";
|
||||
const safetySnapshotResult = authorityTarget
|
||||
? await captureAuthorityMigrationSafetySnapshot(normalizedChatId, legacySnapshot, {
|
||||
source: migrationSource,
|
||||
reason: "authority-indexeddb-migration-safety",
|
||||
})
|
||||
: null;
|
||||
const importSnapshot = {
|
||||
meta: {
|
||||
...legacyMeta,
|
||||
chatId: normalizedChatId,
|
||||
migrationCompletedAt: nowMs,
|
||||
migrationSource: "legacy_indexeddb_snapshot",
|
||||
migrationSource,
|
||||
migratedFromStoragePrimary: "indexeddb",
|
||||
migratedFromStorageMode: BME_GRAPH_LOCAL_STORAGE_MODE_INDEXEDDB,
|
||||
migratedToStoragePrimary: authorityTarget
|
||||
? AUTHORITY_GRAPH_STORE_KIND
|
||||
: targetStore.storagePrimary,
|
||||
migratedToStorageMode: authorityTarget
|
||||
? AUTHORITY_GRAPH_STORE_MODE
|
||||
: targetStore.storageMode,
|
||||
},
|
||||
state: {
|
||||
...legacyState,
|
||||
@@ -9258,9 +9479,15 @@ async function maybeImportLegacyIndexedDbSnapshotToLocalStore(
|
||||
mode: "replace",
|
||||
preserveRevision: true,
|
||||
revision: normalizedRevision,
|
||||
markSyncDirty: Boolean(legacyMeta.syncDirty),
|
||||
markSyncDirty: authorityTarget ? false : Boolean(legacyMeta.syncDirty),
|
||||
});
|
||||
const snapshot = await targetDb.exportSnapshot();
|
||||
if (authorityTarget) {
|
||||
recordAuthorityAcceptedRevisionPointer({
|
||||
revision: snapshot?.meta?.revision || migrationResult?.revision || normalizedRevision,
|
||||
integrity: snapshot?.meta?.integrity || legacyMeta.integrity,
|
||||
});
|
||||
}
|
||||
|
||||
debugDebug("[ST-BME] 已将 legacy IndexedDB 快照迁移到当前本地存储", {
|
||||
source,
|
||||
@@ -9272,12 +9499,15 @@ async function maybeImportLegacyIndexedDbSnapshotToLocalStore(
|
||||
|
||||
return {
|
||||
migrated: true,
|
||||
reason: "migration-local-store-completed",
|
||||
source: "legacy_indexeddb_snapshot",
|
||||
reason: authorityTarget
|
||||
? "authority-indexeddb-migration-completed"
|
||||
: "migration-local-store-completed",
|
||||
source: migrationSource,
|
||||
chatId: normalizedChatId,
|
||||
migrationResult,
|
||||
snapshot,
|
||||
targetStore,
|
||||
safetySnapshotResult,
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn("[ST-BME] 迁移 legacy IndexedDB 快照到当前本地存储失败:", {
|
||||
@@ -10148,6 +10378,8 @@ async function loadGraphFromIndexedDb(
|
||||
identityRecoveryResult?.snapshot,
|
||||
localStore,
|
||||
);
|
||||
const recoveredAuthorityStore =
|
||||
isAuthorityGraphStorePresentation(recoveredStore);
|
||||
const recoveredRevision = normalizeIndexedDbRevision(
|
||||
identityRecoveryResult?.snapshot?.meta?.revision,
|
||||
);
|
||||
@@ -10158,6 +10390,19 @@ async function loadGraphFromIndexedDb(
|
||||
localStoreFormatVersion:
|
||||
recoveredStore.storagePrimary === "opfs" ? 2 : 1,
|
||||
localStoreMigrationState: "completed",
|
||||
authorityMigrationState: recoveredAuthorityStore ? "completed" : graphPersistenceState.authorityMigrationState,
|
||||
authorityMigrationSource: recoveredAuthorityStore
|
||||
? String(identityRecoveryResult?.source || "identity-recovery")
|
||||
: graphPersistenceState.authorityMigrationSource,
|
||||
authorityMigrationRevision: recoveredAuthorityStore
|
||||
? recoveredRevision
|
||||
: graphPersistenceState.authorityMigrationRevision,
|
||||
authorityMigrationLastError: recoveredAuthorityStore
|
||||
? ""
|
||||
: graphPersistenceState.authorityMigrationLastError,
|
||||
lastAuthorityMigrationResult: recoveredAuthorityStore
|
||||
? cloneRuntimeDebugValue(identityRecoveryResult, null)
|
||||
: graphPersistenceState.lastAuthorityMigrationResult,
|
||||
indexedDbRevision: recoveredRevision,
|
||||
indexedDbLastError: "",
|
||||
lastSyncError: "",
|
||||
@@ -10195,7 +10440,9 @@ async function loadGraphFromIndexedDb(
|
||||
);
|
||||
|
||||
const migrationResult =
|
||||
identityRecoveryResult?.migrated || localStoreMigrationResult?.migrated
|
||||
identityRecoveryResult?.migrated ||
|
||||
localStoreMigrationResult?.migrated ||
|
||||
localStoreMigrationResult?.reason === "migration-local-store-failed"
|
||||
? localStoreMigrationResult
|
||||
: await maybeMigrateLegacyGraphToIndexedDb(
|
||||
normalizedChatId,
|
||||
@@ -10211,6 +10458,8 @@ async function loadGraphFromIndexedDb(
|
||||
migrationResult?.snapshot,
|
||||
localStore,
|
||||
);
|
||||
const migratedAuthorityStore =
|
||||
isAuthorityGraphStorePresentation(migratedStore);
|
||||
const migratedRevision = normalizeIndexedDbRevision(
|
||||
migrationResult?.snapshot?.meta?.revision ||
|
||||
migrationResult?.migrationResult?.revision,
|
||||
@@ -10222,6 +10471,19 @@ async function loadGraphFromIndexedDb(
|
||||
localStoreFormatVersion:
|
||||
migratedStore.storagePrimary === "opfs" ? 2 : 1,
|
||||
localStoreMigrationState: "completed",
|
||||
authorityMigrationState: migratedAuthorityStore ? "completed" : graphPersistenceState.authorityMigrationState,
|
||||
authorityMigrationSource: migratedAuthorityStore
|
||||
? String(migrationResult?.source || migrationResult?.reason || "")
|
||||
: graphPersistenceState.authorityMigrationSource,
|
||||
authorityMigrationRevision: migratedAuthorityStore
|
||||
? migratedRevision
|
||||
: graphPersistenceState.authorityMigrationRevision,
|
||||
authorityMigrationLastError: migratedAuthorityStore
|
||||
? ""
|
||||
: graphPersistenceState.authorityMigrationLastError,
|
||||
lastAuthorityMigrationResult: migratedAuthorityStore
|
||||
? cloneRuntimeDebugValue(migrationResult, null)
|
||||
: graphPersistenceState.lastAuthorityMigrationResult,
|
||||
indexedDbRevision: migratedRevision,
|
||||
indexedDbLastError: "",
|
||||
lastSyncError: "",
|
||||
@@ -10236,12 +10498,27 @@ async function loadGraphFromIndexedDb(
|
||||
syncResult: cloneRuntimeDebugValue(migrationResult?.syncResult, null),
|
||||
},
|
||||
});
|
||||
} else if (migrationResult?.reason === "migration-failed") {
|
||||
} else if (
|
||||
migrationResult?.reason === "migration-failed" ||
|
||||
migrationResult?.reason === "migration-local-store-failed"
|
||||
) {
|
||||
updateGraphPersistenceState({
|
||||
indexedDbLastError: String(
|
||||
migrationResult?.error || "migration-failed",
|
||||
),
|
||||
localStoreMigrationState: "failed",
|
||||
authorityMigrationState:
|
||||
localStore.storagePrimary === AUTHORITY_GRAPH_STORE_KIND
|
||||
? "failed"
|
||||
: graphPersistenceState.authorityMigrationState,
|
||||
authorityMigrationLastError:
|
||||
localStore.storagePrimary === AUTHORITY_GRAPH_STORE_KIND
|
||||
? String(migrationResult?.error || migrationResult?.reason || "migration-failed")
|
||||
: graphPersistenceState.authorityMigrationLastError,
|
||||
lastAuthorityMigrationResult:
|
||||
localStore.storagePrimary === AUTHORITY_GRAPH_STORE_KIND
|
||||
? cloneRuntimeDebugValue(migrationResult, null)
|
||||
: graphPersistenceState.lastAuthorityMigrationResult,
|
||||
dualWriteLastResult: {
|
||||
action: "migration",
|
||||
source: "chat_metadata",
|
||||
|
||||
@@ -662,7 +662,7 @@ async function writeBackupEnvelope(envelope, chatId, options = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
async function createRestoreSafetySnapshot(chatId, snapshot, options = {}) {
|
||||
export async function createRestoreSafetySnapshot(chatId, snapshot, options = {}) {
|
||||
const safetyDb = await getSafetyDb(chatId, options);
|
||||
const revision = normalizeRevision(snapshot?.meta?.revision);
|
||||
try {
|
||||
|
||||
@@ -111,6 +111,7 @@ import {
|
||||
createAuthorityBrowserState,
|
||||
getAuthorityBrowserStateSnapshot,
|
||||
normalizeAuthorityBrowserState,
|
||||
recordAuthorityAcceptedRevision,
|
||||
} from "../sync/authority-browser-state.js";
|
||||
import {
|
||||
AUTHORITY_GRAPH_STORE_KIND,
|
||||
@@ -323,6 +324,173 @@ async function createGraphPersistenceHarness({
|
||||
indexedDbSnapshotMap.set(normalizedChatId, structuredClone(snapshot));
|
||||
}
|
||||
|
||||
const authoritySnapshotMap = new Map();
|
||||
|
||||
function getAuthoritySnapshotForChat(targetChatId = "") {
|
||||
const normalizedChatId = String(targetChatId || "");
|
||||
if (normalizedChatId && authoritySnapshotMap.has(normalizedChatId)) {
|
||||
return structuredClone(authoritySnapshotMap.get(normalizedChatId));
|
||||
}
|
||||
return {
|
||||
meta: {
|
||||
revision: 0,
|
||||
chatId: normalizedChatId,
|
||||
storagePrimary: AUTHORITY_GRAPH_STORE_KIND,
|
||||
storageMode: AUTHORITY_GRAPH_STORE_MODE,
|
||||
},
|
||||
nodes: [],
|
||||
edges: [],
|
||||
tombstones: [],
|
||||
state: { lastProcessedFloor: -1, extractionCount: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
function setAuthoritySnapshotForChat(targetChatId = "", snapshot = null) {
|
||||
const normalizedChatId = String(targetChatId || "");
|
||||
if (!normalizedChatId) return;
|
||||
if (!snapshot) {
|
||||
authoritySnapshotMap.delete(normalizedChatId);
|
||||
return;
|
||||
}
|
||||
authoritySnapshotMap.set(normalizedChatId, structuredClone(snapshot));
|
||||
}
|
||||
|
||||
class HarnessAuthorityGraphStore {
|
||||
constructor(dbChatId = "") {
|
||||
this.chatId = String(dbChatId || "");
|
||||
this.storeKind = AUTHORITY_GRAPH_STORE_KIND;
|
||||
this.storeMode = AUTHORITY_GRAPH_STORE_MODE;
|
||||
}
|
||||
async open() {}
|
||||
async close() {}
|
||||
getStorageDiagnosticsSync() {
|
||||
return {
|
||||
formatVersion: 1,
|
||||
migrationState: "idle",
|
||||
resolvedStoreMode: AUTHORITY_GRAPH_STORE_MODE,
|
||||
storageKind: AUTHORITY_GRAPH_STORE_KIND,
|
||||
browserCacheMode: "minimal",
|
||||
};
|
||||
}
|
||||
async exportSnapshot() {
|
||||
return getAuthoritySnapshotForChat(this.chatId);
|
||||
}
|
||||
async exportSnapshotProbe() {
|
||||
return getAuthoritySnapshotForChat(this.chatId);
|
||||
}
|
||||
async importSnapshot(snapshot = {}, options = {}) {
|
||||
const current = getAuthoritySnapshotForChat(this.chatId);
|
||||
const currentRevision = Number(current?.meta?.revision || 0);
|
||||
const incomingRevision = Number(snapshot?.meta?.revision || 0);
|
||||
const requestedRevision = Number.isFinite(Number(options?.revision))
|
||||
? Math.max(0, Math.floor(Number(options.revision)))
|
||||
: options?.preserveRevision === true
|
||||
? Math.max(0, Math.floor(incomingRevision))
|
||||
: currentRevision + 1;
|
||||
const nextRevision = Math.max(currentRevision + 1, requestedRevision);
|
||||
const nowMs = Date.now();
|
||||
const nodes = Array.isArray(snapshot?.nodes)
|
||||
? snapshot.nodes.map((node) => structuredClone(node))
|
||||
: [];
|
||||
const edges = Array.isArray(snapshot?.edges)
|
||||
? snapshot.edges.map((edge) => structuredClone(edge))
|
||||
: [];
|
||||
const tombstones = Array.isArray(snapshot?.tombstones)
|
||||
? snapshot.tombstones.map((record) => structuredClone(record))
|
||||
: [];
|
||||
const nextSnapshot = {
|
||||
meta: {
|
||||
...(snapshot?.meta && typeof snapshot.meta === "object"
|
||||
? structuredClone(snapshot.meta)
|
||||
: {}),
|
||||
chatId: this.chatId,
|
||||
storagePrimary: AUTHORITY_GRAPH_STORE_KIND,
|
||||
storageMode: AUTHORITY_GRAPH_STORE_MODE,
|
||||
revision: nextRevision,
|
||||
lastModified: nowMs,
|
||||
lastMutationReason: "importSnapshot",
|
||||
syncDirty: options?.markSyncDirty !== false,
|
||||
syncDirtyReason: options?.markSyncDirty === false ? "" : "importSnapshot",
|
||||
nodeCount: nodes.length,
|
||||
edgeCount: edges.length,
|
||||
tombstoneCount: tombstones.length,
|
||||
},
|
||||
nodes,
|
||||
edges,
|
||||
tombstones,
|
||||
state:
|
||||
snapshot?.state && typeof snapshot.state === "object"
|
||||
? structuredClone(snapshot.state)
|
||||
: { lastProcessedFloor: -1, extractionCount: 0 },
|
||||
};
|
||||
setAuthoritySnapshotForChat(this.chatId, nextSnapshot);
|
||||
return {
|
||||
mode: String(options?.mode || "replace"),
|
||||
revision: nextRevision,
|
||||
imported: {
|
||||
nodes: nodes.length,
|
||||
edges: edges.length,
|
||||
tombstones: tombstones.length,
|
||||
},
|
||||
};
|
||||
}
|
||||
async importLegacyGraph(graph, options = {}) {
|
||||
const revision = Math.max(1, Math.floor(Number(options?.revision) || 1));
|
||||
const snapshot = buildSnapshotFromGraph(graph, {
|
||||
chatId: this.chatId,
|
||||
revision,
|
||||
meta: {
|
||||
migrationCompletedAt: Number(options?.nowMs || Date.now()),
|
||||
migrationSource: String(options?.source || "chat_metadata"),
|
||||
},
|
||||
});
|
||||
const importResult = await this.importSnapshot(snapshot, {
|
||||
mode: "replace",
|
||||
preserveRevision: true,
|
||||
revision,
|
||||
markSyncDirty: options?.markSyncDirty,
|
||||
});
|
||||
return {
|
||||
migrated: true,
|
||||
revision: importResult.revision,
|
||||
imported: importResult.imported,
|
||||
};
|
||||
}
|
||||
async isEmpty() {
|
||||
const snapshot = getAuthoritySnapshotForChat(this.chatId);
|
||||
const nodes = Array.isArray(snapshot?.nodes) ? snapshot.nodes.length : 0;
|
||||
const edges = Array.isArray(snapshot?.edges) ? snapshot.edges.length : 0;
|
||||
const tombstones = Array.isArray(snapshot?.tombstones)
|
||||
? snapshot.tombstones.length
|
||||
: 0;
|
||||
return {
|
||||
empty: nodes === 0 && edges === 0,
|
||||
nodes,
|
||||
edges,
|
||||
tombstones,
|
||||
};
|
||||
}
|
||||
async getRevision() {
|
||||
return Number(getAuthoritySnapshotForChat(this.chatId)?.meta?.revision || 0);
|
||||
}
|
||||
async getMeta(key, fallbackValue = 0) {
|
||||
const snapshot = getAuthoritySnapshotForChat(this.chatId);
|
||||
if (!snapshot?.meta || !(key in snapshot.meta)) {
|
||||
return fallbackValue;
|
||||
}
|
||||
return snapshot.meta[key];
|
||||
}
|
||||
async patchMeta(record = {}) {
|
||||
const snapshot = getAuthoritySnapshotForChat(this.chatId);
|
||||
snapshot.meta = {
|
||||
...(snapshot.meta || {}),
|
||||
...(record && typeof record === "object" ? structuredClone(record) : {}),
|
||||
};
|
||||
setAuthoritySnapshotForChat(this.chatId, snapshot);
|
||||
return record;
|
||||
}
|
||||
}
|
||||
|
||||
function commitIndexedDbDelta(targetChatId = "", delta = {}, options = {}) {
|
||||
const normalizedChatId = String(targetChatId || "");
|
||||
const currentSnapshot = getIndexedDbSnapshotForChat(normalizedChatId);
|
||||
@@ -442,6 +610,9 @@ async function createGraphPersistenceHarness({
|
||||
String(chatId || globalChatId || ""),
|
||||
),
|
||||
__indexedDbSnapshots: indexedDbSnapshotMap,
|
||||
__authoritySnapshots: authoritySnapshotMap,
|
||||
__getAuthoritySnapshotForChat: getAuthoritySnapshotForChat,
|
||||
__setAuthoritySnapshotForChat: setAuthoritySnapshotForChat,
|
||||
sessionStorage: storage,
|
||||
localStorage,
|
||||
extension_settings: {
|
||||
@@ -457,9 +628,10 @@ async function createGraphPersistenceHarness({
|
||||
createAuthorityBrowserState,
|
||||
getAuthorityBrowserStateSnapshot,
|
||||
normalizeAuthorityBrowserState,
|
||||
recordAuthorityAcceptedRevision,
|
||||
AUTHORITY_GRAPH_STORE_KIND,
|
||||
AUTHORITY_GRAPH_STORE_MODE,
|
||||
AuthorityGraphStore,
|
||||
AuthorityGraphStore: HarnessAuthorityGraphStore,
|
||||
migrateLegacyTaskProfiles(settings = {}) {
|
||||
return {
|
||||
taskProfilesVersion: Number(settings?.taskProfilesVersion || 0),
|
||||
@@ -1071,6 +1243,32 @@ async function createGraphPersistenceHarness({
|
||||
hasGraphPersistDirtyState,
|
||||
pruneGraphPersistDirtyState,
|
||||
buildBmeDbName,
|
||||
buildRestoreSafetyChatId(chatId = "") {
|
||||
return `__restore_safety__${String(chatId || "").trim()}`;
|
||||
},
|
||||
async createRestoreSafetySnapshot(chatId, snapshot, options = {}) {
|
||||
const safetyDb =
|
||||
typeof options?.getSafetyDb === "function"
|
||||
? await options.getSafetyDb(chatId)
|
||||
: new runtimeContext.BmeDatabase(
|
||||
runtimeContext.buildRestoreSafetyChatId(chatId),
|
||||
);
|
||||
await safetyDb.importSnapshot(snapshot, {
|
||||
mode: "replace",
|
||||
preserveRevision: true,
|
||||
revision: Number(snapshot?.meta?.revision || 0),
|
||||
markSyncDirty: false,
|
||||
});
|
||||
if (typeof safetyDb.patchMeta === "function") {
|
||||
await safetyDb.patchMeta({
|
||||
restoreSafetySnapshotExists: true,
|
||||
restoreSafetySnapshotChatId: String(chatId || "").trim(),
|
||||
});
|
||||
}
|
||||
if (typeof safetyDb.close === "function") {
|
||||
await safetyDb.close();
|
||||
}
|
||||
},
|
||||
BME_GRAPH_LOCAL_STORAGE_MODE_AUTO: "auto",
|
||||
BME_GRAPH_LOCAL_STORAGE_MODE_INDEXEDDB: "indexeddb",
|
||||
BME_GRAPH_LOCAL_STORAGE_MODE_OPFS_PRIMARY: "opfs-primary",
|
||||
@@ -1154,10 +1352,78 @@ async function createGraphPersistenceHarness({
|
||||
revision: Number(snapshot?.meta?.revision) || 0,
|
||||
};
|
||||
}
|
||||
async patchMeta(record = {}) {
|
||||
const snapshot = getIndexedDbSnapshotForChat(this.chatId);
|
||||
snapshot.meta = {
|
||||
...(snapshot.meta || {}),
|
||||
...(record && typeof record === "object" ? structuredClone(record) : {}),
|
||||
};
|
||||
setIndexedDbSnapshotForChat(this.chatId, snapshot);
|
||||
return record;
|
||||
}
|
||||
async getMeta(key, fallbackValue = 0) {
|
||||
const snapshot = getIndexedDbSnapshotForChat(this.chatId) || {};
|
||||
if (!snapshot?.meta || !(key in snapshot.meta)) {
|
||||
return fallbackValue;
|
||||
}
|
||||
return snapshot.meta[key];
|
||||
}
|
||||
async getRevision() {
|
||||
const snapshot = getIndexedDbSnapshotForChat(this.chatId) || {};
|
||||
return Number(snapshot?.meta?.revision) || 0;
|
||||
}
|
||||
async isEmpty() {
|
||||
const snapshot = getIndexedDbSnapshotForChat(this.chatId) || {};
|
||||
const nodes = Array.isArray(snapshot?.nodes) ? snapshot.nodes.length : 0;
|
||||
const edges = Array.isArray(snapshot?.edges) ? snapshot.edges.length : 0;
|
||||
const tombstones = Array.isArray(snapshot?.tombstones)
|
||||
? snapshot.tombstones.length
|
||||
: 0;
|
||||
return {
|
||||
empty: nodes === 0 && edges === 0,
|
||||
nodes,
|
||||
edges,
|
||||
tombstones,
|
||||
};
|
||||
}
|
||||
async importLegacyGraph(graph, options = {}) {
|
||||
const revision = Number(options?.revision) || 1;
|
||||
const migratedSnapshot = buildSnapshotFromGraph(graph, {
|
||||
chatId: this.chatId || runtimeContext.__chatContext?.chatId || "",
|
||||
revision,
|
||||
meta: {
|
||||
migrationCompletedAt: Date.now(),
|
||||
migrationSource: "chat_metadata",
|
||||
},
|
||||
});
|
||||
setIndexedDbSnapshotForChat(this.chatId, migratedSnapshot);
|
||||
return {
|
||||
migrated: true,
|
||||
revision,
|
||||
imported: {
|
||||
nodes: migratedSnapshot?.nodes?.length || 0,
|
||||
edges: migratedSnapshot?.edges?.length || 0,
|
||||
tombstones: migratedSnapshot?.tombstones?.length || 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
async markSyncDirty() {
|
||||
if (runtimeContext.__markSyncDirtyShouldThrow) {
|
||||
throw new Error("mark-sync-dirty-failed");
|
||||
}
|
||||
}
|
||||
},
|
||||
BmeChatManager: class {
|
||||
constructor() {
|
||||
constructor(options = {}) {
|
||||
this._currentChatId = "";
|
||||
this._databaseFactory =
|
||||
typeof options?.databaseFactory === "function"
|
||||
? options.databaseFactory
|
||||
: null;
|
||||
this._selectorKeyResolver =
|
||||
typeof options?.selectorKeyResolver === "function"
|
||||
? options.selectorKeyResolver
|
||||
: null;
|
||||
}
|
||||
_createDb(dbChatId = "") {
|
||||
return {
|
||||
@@ -1249,6 +1515,16 @@ async function createGraphPersistenceHarness({
|
||||
if (runtimeContext.__indexedDbGetCurrentDbShouldThrow) {
|
||||
throw new Error("indexeddb-get-current-db-failed");
|
||||
}
|
||||
const selectorKey = this._selectorKeyResolver
|
||||
? String(await this._selectorKeyResolver(this._currentChatId) || "")
|
||||
: "";
|
||||
if (this._databaseFactory && selectorKey.startsWith("authority:")) {
|
||||
const db = await this._databaseFactory(this._currentChatId);
|
||||
if (typeof db?.open === "function") {
|
||||
await db.open();
|
||||
}
|
||||
return db;
|
||||
}
|
||||
return this._createDb(this._currentChatId);
|
||||
}
|
||||
async switchChat(dbChatId = "") {
|
||||
@@ -1256,6 +1532,16 @@ async function createGraphPersistenceHarness({
|
||||
runtimeContext.__indexedDbSnapshot = getIndexedDbSnapshotForChat(
|
||||
this._currentChatId,
|
||||
);
|
||||
const selectorKey = this._selectorKeyResolver
|
||||
? String(await this._selectorKeyResolver(this._currentChatId) || "")
|
||||
: "";
|
||||
if (this._databaseFactory && selectorKey.startsWith("authority:")) {
|
||||
const db = await this._databaseFactory(this._currentChatId);
|
||||
if (typeof db?.open === "function") {
|
||||
await db.open();
|
||||
}
|
||||
return db;
|
||||
}
|
||||
return this._createDb(this._currentChatId);
|
||||
}
|
||||
async closeCurrent() {}
|
||||
@@ -1327,6 +1613,20 @@ result = {
|
||||
};
|
||||
return bmeLocalStoreCapabilitySnapshot;
|
||||
},
|
||||
setAuthorityCapabilityState(patch = {}) {
|
||||
authorityCapabilityState = normalizeAuthorityCapabilityState(
|
||||
{
|
||||
...authorityCapabilityState,
|
||||
...(patch || {}),
|
||||
},
|
||||
getSettings(),
|
||||
);
|
||||
authorityBrowserState = normalizeAuthorityBrowserState(
|
||||
authorityBrowserState,
|
||||
getSettings(),
|
||||
);
|
||||
return authorityCapabilityState;
|
||||
},
|
||||
setChatContext(nextContext) {
|
||||
globalThis.__chatContext = nextContext;
|
||||
return globalThis.__chatContext;
|
||||
@@ -1362,6 +1662,9 @@ result = {
|
||||
const snapshot = globalThis.__indexedDbSnapshots.get(normalizedChatId);
|
||||
return snapshot ? structuredClone(snapshot) : null;
|
||||
},
|
||||
getAuthoritySnapshotForChat(chatId) {
|
||||
return globalThis.__getAuthoritySnapshotForChat(chatId);
|
||||
},
|
||||
};
|
||||
`,
|
||||
].join("\n"),
|
||||
@@ -3642,6 +3945,92 @@ result = {
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
const chatId = "meta-authority-indexeddb-migration";
|
||||
const legacyGraph = stampPersistedGraph(
|
||||
createMeaningfulGraph(chatId, "authority-indexeddb-migration"),
|
||||
{
|
||||
revision: 4,
|
||||
integrity: "meta-authority-indexeddb-migration",
|
||||
chatId,
|
||||
reason: "legacy-indexeddb",
|
||||
},
|
||||
);
|
||||
const legacySnapshot = buildSnapshotFromGraph(legacyGraph, {
|
||||
chatId,
|
||||
revision: 4,
|
||||
meta: {
|
||||
integrity: "meta-authority-indexeddb-migration",
|
||||
storagePrimary: "indexeddb",
|
||||
storageMode: "indexeddb",
|
||||
syncDirty: true,
|
||||
},
|
||||
});
|
||||
const harness = await createGraphPersistenceHarness({
|
||||
chatId,
|
||||
globalChatId: chatId,
|
||||
chatMetadata: {
|
||||
integrity: "meta-authority-indexeddb-migration",
|
||||
},
|
||||
indexedDbSnapshot: legacySnapshot,
|
||||
});
|
||||
harness.runtimeContext.extension_settings[MODULE_NAME] = {
|
||||
authorityEnabled: "on",
|
||||
authorityPrimaryWhenAvailable: true,
|
||||
authorityStorageMode: "server-primary",
|
||||
authoritySqlPrimary: true,
|
||||
authorityBrowserCacheMode: "minimal",
|
||||
};
|
||||
harness.api.setAuthorityCapabilityState({
|
||||
installed: true,
|
||||
healthy: true,
|
||||
sessionReady: true,
|
||||
permissionReady: true,
|
||||
features: ["sql.query", "sql.mutation", "trivium.search", "jobs", "blob"],
|
||||
reason: "ok",
|
||||
lastProbeAt: Date.now(),
|
||||
});
|
||||
|
||||
const result = await harness.api.loadGraphFromIndexedDb(chatId, {
|
||||
source: "authority-indexeddb-migration",
|
||||
attemptIndex: 0,
|
||||
});
|
||||
|
||||
assert.equal(result.loaded, true);
|
||||
assert.equal(result.reason, "authority-sql:authority-indexeddb-migration");
|
||||
assert.equal(harness.runtimeContext.__syncNowCalls.length, 0);
|
||||
const authoritySnapshot = harness.api.getAuthoritySnapshotForChat(chatId);
|
||||
assert.equal(authoritySnapshot?.nodes?.length, 1);
|
||||
assert.equal(authoritySnapshot?.meta?.storagePrimary, AUTHORITY_GRAPH_STORE_KIND);
|
||||
assert.equal(authoritySnapshot?.meta?.storageMode, AUTHORITY_GRAPH_STORE_MODE);
|
||||
assert.equal(
|
||||
authoritySnapshot?.meta?.migrationSource,
|
||||
"legacy_indexeddb_to_authority",
|
||||
);
|
||||
assert.equal(authoritySnapshot?.meta?.syncDirty, false);
|
||||
const safetySnapshot = harness.api.getIndexedDbSnapshotForChat(
|
||||
harness.runtimeContext.buildRestoreSafetyChatId(chatId),
|
||||
);
|
||||
assert.equal(safetySnapshot?.nodes?.length, 1);
|
||||
assert.equal(safetySnapshot?.meta?.restoreSafetySnapshotExists, true);
|
||||
assert.equal(safetySnapshot?.meta?.restoreSafetySnapshotChatId, chatId);
|
||||
const live = harness.api.getGraphPersistenceLiveState();
|
||||
assert.equal(live.storagePrimary, AUTHORITY_GRAPH_STORE_KIND);
|
||||
assert.equal(live.storageMode, AUTHORITY_GRAPH_STORE_MODE);
|
||||
assert.equal(live.authorityMigrationState, "completed");
|
||||
assert.equal(live.authorityMigrationSource, "legacy_indexeddb_to_authority");
|
||||
assert.equal(Number(live.authorityMigrationRevision), 4);
|
||||
assert.equal(
|
||||
live.lastAuthorityMigrationResult?.safetySnapshotResult?.restoreSafetyCaptured,
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
harness.runtimeContext.__indexedDbSnapshots.has(chatId),
|
||||
true,
|
||||
"迁移成功后仍保留 legacy IndexedDB 源数据,不删除本地数据",
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
const harness = await createGraphPersistenceHarness({
|
||||
chatId: "chat-b",
|
||||
|
||||
@@ -56,6 +56,7 @@ import {
|
||||
createAuthorityBrowserState,
|
||||
getAuthorityBrowserStateSnapshot,
|
||||
normalizeAuthorityBrowserState,
|
||||
recordAuthorityAcceptedRevision,
|
||||
} from "../../sync/authority-browser-state.js";
|
||||
|
||||
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
@@ -107,6 +108,7 @@ export function createGenerationRecallHarness(options = {}) {
|
||||
createAuthorityBrowserState,
|
||||
getAuthorityBrowserStateSnapshot,
|
||||
normalizeAuthorityBrowserState,
|
||||
recordAuthorityAcceptedRevision,
|
||||
settings: {},
|
||||
graphPersistenceState: createGraphPersistenceState(),
|
||||
extension_settings: { [MODULE_NAME]: {} },
|
||||
|
||||
@@ -141,6 +141,11 @@ export function createGraphPersistenceState() {
|
||||
authorityOfflineQueueBytes: 0,
|
||||
authorityOfflineQueueItems: 0,
|
||||
authorityDegradedReason: "",
|
||||
authorityMigrationState: "idle",
|
||||
authorityMigrationSource: "",
|
||||
authorityMigrationRevision: 0,
|
||||
authorityMigrationLastError: "",
|
||||
lastAuthorityMigrationResult: null,
|
||||
localStoreFormatVersion: 1,
|
||||
localStoreMigrationState: "idle",
|
||||
opfsWriteLockState: {
|
||||
|
||||
Reference in New Issue
Block a user