Merge branch 'Youzini-afk:main' into main

This commit is contained in:
Hao19911125
2026-04-06 22:40:28 +08:00
committed by GitHub
3 changed files with 1273 additions and 69 deletions

View File

@@ -21,6 +21,7 @@ export const GRAPH_LOAD_STATES = Object.freeze({
});
export const GRAPH_LOAD_PENDING_CHAT_ID = "__pending_chat__";
export const GRAPH_SHADOW_SNAPSHOT_STORAGE_PREFIX = `${MODULE_NAME}:graph-shadow:`;
export const GRAPH_IDENTITY_ALIAS_STORAGE_KEY = `${MODULE_NAME}:chat-identity-aliases`;
export const GRAPH_STARTUP_RECONCILE_DELAYS_MS = [150, 600, 1800, 4000];
// ═══════════════════════════════════════════════════════════
@@ -51,6 +52,247 @@ export function createLocalIntegritySlug() {
export const GRAPH_PERSISTENCE_SESSION_ID = createLocalIntegritySlug();
function normalizeIdentityValue(value) {
return String(value ?? "").trim();
}
function getLocalStorageSafe() {
const storage = globalThis.localStorage;
if (
!storage ||
typeof storage.getItem !== "function" ||
typeof storage.setItem !== "function"
) {
return null;
}
return storage;
}
function getSessionStorageSafe() {
const storage = globalThis.sessionStorage;
if (!storage || typeof storage.getItem !== "function") {
return null;
}
return storage;
}
function listStorageKeys(storage) {
if (!storage) return [];
if (typeof storage.length === "number" && typeof storage.key === "function") {
const keys = [];
for (let index = 0; index < storage.length; index += 1) {
const key = storage.key(index);
if (typeof key === "string" && key) {
keys.push(key);
}
}
return keys;
}
if (storage.__store instanceof Map) {
return Array.from(storage.__store.keys()).map((key) => String(key));
}
return [];
}
function readGraphIdentityAliasRegistryRaw() {
const storage = getLocalStorageSafe();
if (!storage) {
return {
byIntegrity: {},
};
}
try {
const raw = storage.getItem(GRAPH_IDENTITY_ALIAS_STORAGE_KEY);
if (!raw) {
return {
byIntegrity: {},
};
}
const parsed = JSON.parse(raw);
const byIntegrity =
parsed?.byIntegrity &&
typeof parsed.byIntegrity === "object" &&
!Array.isArray(parsed.byIntegrity)
? parsed.byIntegrity
: {};
return {
byIntegrity,
};
} catch {
return {
byIntegrity: {},
};
}
}
function writeGraphIdentityAliasRegistryRaw(registry = null) {
const storage = getLocalStorageSafe();
if (!storage) return false;
try {
storage.setItem(
GRAPH_IDENTITY_ALIAS_STORAGE_KEY,
JSON.stringify({
byIntegrity:
registry?.byIntegrity &&
typeof registry.byIntegrity === "object" &&
!Array.isArray(registry.byIntegrity)
? registry.byIntegrity
: {},
}),
);
return true;
} catch {
return false;
}
}
function normalizeGraphIdentityAliasEntry(entry = {}, integrity = "") {
const normalizedIntegrity = normalizeIdentityValue(integrity || entry.integrity);
const normalizedPersistenceChatId = normalizeIdentityValue(
entry.persistenceChatId || normalizedIntegrity,
);
const normalizedHostChatIds = Array.from(
new Set(
(Array.isArray(entry.hostChatIds) ? entry.hostChatIds : [])
.map((value) => normalizeIdentityValue(value))
.filter(Boolean),
),
).slice(-16);
return {
integrity: normalizedIntegrity,
persistenceChatId: normalizedPersistenceChatId || normalizedIntegrity,
hostChatIds: normalizedHostChatIds,
updatedAt: String(entry.updatedAt || ""),
};
}
export function rememberGraphIdentityAlias({
integrity = "",
hostChatId = "",
persistenceChatId = "",
} = {}) {
const normalizedIntegrity = normalizeIdentityValue(integrity);
if (!normalizedIntegrity) return null;
const normalizedHostChatId = normalizeIdentityValue(hostChatId);
const normalizedPersistenceChatId = normalizeIdentityValue(
persistenceChatId || normalizedIntegrity,
);
const registry = readGraphIdentityAliasRegistryRaw();
const existingEntry = normalizeGraphIdentityAliasEntry(
registry.byIntegrity?.[normalizedIntegrity] || {},
normalizedIntegrity,
);
const hostChatIds = Array.from(
new Set(
[normalizedHostChatId, ...existingEntry.hostChatIds].filter(Boolean),
),
).slice(-16);
const nextEntry = {
integrity: normalizedIntegrity,
persistenceChatId: normalizedPersistenceChatId || normalizedIntegrity,
hostChatIds,
updatedAt: new Date().toISOString(),
};
registry.byIntegrity[normalizedIntegrity] = nextEntry;
writeGraphIdentityAliasRegistryRaw(registry);
return nextEntry;
}
export function resolveGraphIdentityAliasByHostChatId(hostChatId = "") {
const normalizedHostChatId = normalizeIdentityValue(hostChatId);
if (!normalizedHostChatId) return "";
const registry = readGraphIdentityAliasRegistryRaw();
let bestEntry = null;
for (const [integrity, value] of Object.entries(registry.byIntegrity || {})) {
const entry = normalizeGraphIdentityAliasEntry(value, integrity);
if (!entry.hostChatIds.includes(normalizedHostChatId)) {
continue;
}
if (!bestEntry) {
bestEntry = entry;
continue;
}
if (String(entry.updatedAt || "") > String(bestEntry.updatedAt || "")) {
bestEntry = entry;
}
}
return normalizeIdentityValue(bestEntry?.persistenceChatId || "");
}
export function getGraphIdentityAliasCandidates({
integrity = "",
hostChatId = "",
persistenceChatId = "",
} = {}) {
const normalizedIntegrity = normalizeIdentityValue(integrity);
const normalizedHostChatId = normalizeIdentityValue(hostChatId);
const normalizedPersistenceChatId = normalizeIdentityValue(persistenceChatId);
const registry = readGraphIdentityAliasRegistryRaw();
const candidates = [];
const seen = new Set();
const pushCandidate = (value) => {
const normalized = normalizeIdentityValue(value);
if (!normalized || seen.has(normalized)) return;
seen.add(normalized);
candidates.push(normalized);
};
if (normalizedIntegrity) {
const entry = normalizeGraphIdentityAliasEntry(
registry.byIntegrity?.[normalizedIntegrity] || {},
normalizedIntegrity,
);
pushCandidate(entry.persistenceChatId);
for (const value of entry.hostChatIds) {
pushCandidate(value);
}
} else if (normalizedHostChatId) {
pushCandidate(resolveGraphIdentityAliasByHostChatId(normalizedHostChatId));
}
pushCandidate(normalizedHostChatId);
pushCandidate(normalizedPersistenceChatId);
return candidates;
}
function normalizeShadowSnapshotPayload(snapshot = null) {
if (!snapshot || typeof snapshot !== "object") {
return null;
}
const serializedGraph = String(snapshot.serializedGraph || "");
const chatId = normalizeIdentityValue(snapshot.chatId);
if (!chatId || !serializedGraph) {
return null;
}
return {
chatId,
revision: Number.isFinite(snapshot.revision) ? snapshot.revision : 0,
serializedGraph,
updatedAt: String(snapshot.updatedAt || ""),
reason: String(snapshot.reason || ""),
integrity: normalizeIdentityValue(snapshot.integrity),
persistedChatId: normalizeIdentityValue(snapshot.persistedChatId),
debugReason: String(snapshot.debugReason || snapshot.reason || ""),
};
}
// ═══════════════════════════════════════════════════════════
// 图谱持久化元数据
// ═══════════════════════════════════════════════════════════
@@ -146,33 +388,72 @@ export function readGraphShadowSnapshot(chatId = "") {
if (!storageKey) return null;
try {
const raw = globalThis.sessionStorage?.getItem(storageKey);
const raw = getSessionStorageSafe()?.getItem(storageKey);
if (!raw) return null;
const snapshot = JSON.parse(raw);
if (
!snapshot ||
typeof snapshot !== "object" ||
String(snapshot.chatId || "") !== String(chatId || "") ||
typeof snapshot.serializedGraph !== "string" ||
!snapshot.serializedGraph
) {
const snapshot = normalizeShadowSnapshotPayload(JSON.parse(raw));
if (!snapshot || snapshot.chatId !== String(chatId || "")) {
return null;
}
return {
chatId: String(snapshot.chatId || ""),
revision: Number.isFinite(snapshot.revision) ? snapshot.revision : 0,
serializedGraph: snapshot.serializedGraph,
updatedAt: String(snapshot.updatedAt || ""),
reason: String(snapshot.reason || ""),
integrity: String(snapshot.integrity || ""),
persistedChatId: String(snapshot.persistedChatId || ""),
debugReason: String(snapshot.debugReason || snapshot.reason || ""),
};
return snapshot;
} catch {
return null;
}
}
export function findGraphShadowSnapshotByIntegrity(
integrity = "",
{ excludeChatIds = [] } = {},
) {
const normalizedIntegrity = normalizeIdentityValue(integrity);
if (!normalizedIntegrity) return null;
const storage = getSessionStorageSafe();
if (!storage) return null;
const excludedChatIds = new Set(
(Array.isArray(excludeChatIds) ? excludeChatIds : [])
.map((value) => normalizeIdentityValue(value))
.filter(Boolean),
);
let bestSnapshot = null;
for (const key of listStorageKeys(storage)) {
if (!String(key || "").startsWith(GRAPH_SHADOW_SNAPSHOT_STORAGE_PREFIX)) {
continue;
}
try {
const snapshot = normalizeShadowSnapshotPayload(
JSON.parse(storage.getItem(key)),
);
if (!snapshot || snapshot.integrity !== normalizedIntegrity) {
continue;
}
if (excludedChatIds.has(snapshot.chatId)) {
continue;
}
const bestRevision = Number(bestSnapshot?.revision || 0);
const nextRevision = Number(snapshot.revision || 0);
if (!bestSnapshot || nextRevision > bestRevision) {
bestSnapshot = snapshot;
continue;
}
if (
nextRevision === bestRevision &&
String(snapshot.updatedAt || "") > String(bestSnapshot.updatedAt || "")
) {
bestSnapshot = snapshot;
}
} catch {
// ignore broken shadow snapshot payloads
}
}
return bestSnapshot;
}
/**
* @param {string} chatId
* @param {object} graph
@@ -191,7 +472,7 @@ export function writeGraphShadowSnapshot(
try {
const serializedGraph = serializeGraph(graph);
const persistedMeta = getGraphPersistenceMeta(graph) || {};
globalThis.sessionStorage?.setItem(
getSessionStorageSafe()?.setItem(
storageKey,
JSON.stringify({
chatId: String(chatId || ""),
@@ -216,7 +497,7 @@ export function removeGraphShadowSnapshot(chatId = "") {
if (!storageKey) return false;
try {
globalThis.sessionStorage?.removeItem(storageKey);
getSessionStorageSafe()?.removeItem(storageKey);
return true;
} catch {
return false;

620
index.js
View File

@@ -18,6 +18,8 @@ import {
import { BmeChatManager } from "./bme-chat-manager.js";
import {
BmeDatabase,
buildBmeDbName,
buildGraphFromSnapshot,
buildSnapshotFromGraph,
ensureDexieLoaded,
@@ -82,6 +84,7 @@ import {
generateSynopsis,
} from "./extractor.js";
import {
findGraphShadowSnapshotByIntegrity,
GRAPH_LOAD_PENDING_CHAT_ID,
GRAPH_LOAD_STATES,
GRAPH_METADATA_KEY,
@@ -90,7 +93,12 @@ import {
cloneGraphForPersistence,
cloneRuntimeDebugValue,
getGraphPersistedRevision,
getGraphPersistenceMeta,
getGraphIdentityAliasCandidates,
readGraphShadowSnapshot,
removeGraphShadowSnapshot,
rememberGraphIdentityAlias,
resolveGraphIdentityAliasByHostChatId,
stampGraphPersistenceMeta,
writeChatMetadataPatch,
writeGraphShadowSnapshot,
@@ -930,10 +938,19 @@ function throwIfAborted(signal, message = "操作已终止") {
function assertRecoveryChatStillActive(expectedChatId, label = "") {
if (!expectedChatId) return;
const currentId = getCurrentChatId();
if (currentId && currentId !== expectedChatId) {
const currentIdentity = resolveCurrentChatIdentity(getContext());
const currentId = normalizeChatIdCandidate(currentIdentity.chatId);
const normalizedExpectedChatId = normalizeChatIdCandidate(expectedChatId);
if (
currentId &&
normalizedExpectedChatId &&
!doesChatIdMatchResolvedGraphIdentity(
normalizedExpectedChatId,
currentIdentity,
)
) {
throw createAbortError(
`历史恢复已终止:聊天已从 ${expectedChatId} 切换到 ${currentId}${label ? ` (${label})` : ""}`,
`历史恢复已终止:聊天已从 ${normalizedExpectedChatId} 切换到 ${currentId}${label ? ` (${label})` : ""}`,
);
}
}
@@ -3309,7 +3326,7 @@ function isHostChatMetadataReady(context = getContext()) {
return false;
}
function resolveCurrentChatIdentity(context = getContext()) {
function resolveCurrentHostChatId(context = getContext()) {
const candidates = [
context?.chatId,
context?.getCurrentChatId?.(),
@@ -3320,13 +3337,43 @@ function resolveCurrentChatIdentity(context = getContext()) {
context?.chatMetadata?.sessionId,
];
const chatId =
return (
candidates
.map((candidate) => normalizeChatIdCandidate(candidate))
.find(Boolean) || "";
.find(Boolean) || ""
);
}
function resolveCurrentChatIdentity(context = getContext()) {
const hostChatId = resolveCurrentHostChatId(context);
const integrity =
typeof getChatMetadataIntegrity === "function"
? getChatMetadataIntegrity(context)
: normalizeChatIdCandidate(
context?.chatMetadata?.integrity ||
context?.chatMetadata?.chat_id ||
context?.chatMetadata?.chatId ||
"",
);
const aliasedChatId =
!integrity &&
hostChatId &&
typeof resolveGraphIdentityAliasByHostChatId === "function"
? resolveGraphIdentityAliasByHostChatId(hostChatId)
: "";
const chatId = integrity || aliasedChatId || hostChatId;
return {
chatId,
hostChatId,
integrity,
identitySource: integrity
? "integrity"
: aliasedChatId
? "alias"
: hostChatId
? "host-chat-id"
: "",
hasLikelySelectedChat: hasLikelySelectedChatContext(context),
};
}
@@ -3335,6 +3382,250 @@ function getCurrentChatId(context = getContext()) {
return resolveCurrentChatIdentity(context).chatId;
}
function rememberResolvedGraphIdentityAlias(
context = getContext(),
persistenceChatId = getCurrentChatId(context),
) {
const identity = resolveCurrentChatIdentity(context);
if (!identity.integrity || !persistenceChatId) {
return null;
}
return rememberGraphIdentityAlias({
integrity: identity.integrity,
hostChatId: identity.hostChatId,
persistenceChatId,
});
}
function buildLegacyGraphIdentityCandidates(
targetChatId,
context = getContext(),
{ shadowSnapshot = null } = {},
) {
const normalizedTargetChatId = normalizeChatIdCandidate(targetChatId);
const identity = resolveCurrentChatIdentity(context);
const candidates = new Set();
const addCandidate = (value) => {
const normalized = normalizeChatIdCandidate(value);
if (!normalized || normalized === normalizedTargetChatId) return;
candidates.add(normalized);
};
addCandidate(identity.hostChatId);
for (const aliasCandidate of getGraphIdentityAliasCandidates({
integrity: identity.integrity,
hostChatId: identity.hostChatId,
persistenceChatId: normalizedTargetChatId,
})) {
addCandidate(aliasCandidate);
}
const currentGraphMeta = getGraphPersistenceMeta(currentGraph) || {};
const runtimeGraphIntegrity = normalizeChatIdCandidate(
currentGraphMeta.integrity || graphPersistenceState.metadataIntegrity,
);
if (
identity.integrity &&
runtimeGraphIntegrity &&
runtimeGraphIntegrity === identity.integrity
) {
addCandidate(graphPersistenceState.chatId);
addCandidate(currentGraph?.historyState?.chatId);
addCandidate(currentGraphMeta.chatId);
}
addCandidate(shadowSnapshot?.chatId);
addCandidate(shadowSnapshot?.persistedChatId);
return Array.from(candidates);
}
async function doesIndexedDbChatStoreExist(chatId = "") {
const normalizedChatId = normalizeChatIdCandidate(chatId);
if (!normalizedChatId) return false;
const DexieCtor = globalThis.Dexie || (await ensureDexieLoaded());
if (typeof DexieCtor?.exists === "function") {
return await DexieCtor.exists(buildBmeDbName(normalizedChatId));
}
if (typeof DexieCtor?.getDatabaseNames === "function") {
const names = await DexieCtor.getDatabaseNames();
return Array.isArray(names)
? names.includes(buildBmeDbName(normalizedChatId))
: false;
}
return false;
}
async function exportIndexedDbSnapshotForChat(chatId = "") {
const normalizedChatId = normalizeChatIdCandidate(chatId);
if (!normalizedChatId) {
return null;
}
if (!(await doesIndexedDbChatStoreExist(normalizedChatId))) {
return null;
}
const DexieCtor = globalThis.Dexie || (await ensureDexieLoaded());
const db = new BmeDatabase(normalizedChatId, {
dexieClass: DexieCtor,
});
try {
await db.open();
return await db.exportSnapshot();
} finally {
await db.close();
}
}
function buildRecoveredSnapshotForChatIdentity(
graph,
targetChatId,
{
revision = 0,
integrity = "",
source = "identity-recovery",
legacyChatId = "",
} = {},
) {
const normalizedTargetChatId = normalizeChatIdCandidate(targetChatId);
const normalizedIntegrity = normalizeChatIdCandidate(integrity);
const normalizedLegacyChatId = normalizeChatIdCandidate(legacyChatId);
const normalizedGraph = cloneGraphForPersistence(graph, normalizedTargetChatId);
const effectiveRevision = Math.max(
1,
normalizeIndexedDbRevision(
revision || graphPersistenceState.revision || getGraphPersistedRevision(graph),
),
);
stampGraphPersistenceMeta(normalizedGraph, {
revision: effectiveRevision,
reason: source,
chatId: normalizedTargetChatId,
integrity: normalizedIntegrity,
});
return buildSnapshotFromGraph(normalizedGraph, {
chatId: normalizedTargetChatId,
revision: effectiveRevision,
lastModified: Date.now(),
meta: {
storagePrimary: "indexeddb",
lastMutationReason: String(source || "identity-recovery"),
integrity: normalizedIntegrity,
migratedFromChatId: normalizedLegacyChatId,
identityMigrationSource: String(source || "identity-recovery"),
},
});
}
async function importRecoveredSnapshotToIndexedDb(
targetDb,
targetChatId,
graph,
{ revision = 0, integrity = "", source = "identity-recovery", legacyChatId = "" } = {},
) {
const snapshot = buildRecoveredSnapshotForChatIdentity(graph, targetChatId, {
revision,
integrity,
source,
legacyChatId,
});
const importResult = await targetDb.importSnapshot(snapshot, {
mode: "replace",
preserveRevision: true,
revision: snapshot.meta.revision,
markSyncDirty: true,
});
snapshot.meta.revision = normalizeIndexedDbRevision(
importResult?.revision,
snapshot.meta.revision,
);
return snapshot;
}
function doesChatIdMatchResolvedGraphIdentity(
candidateChatId,
identity = resolveCurrentChatIdentity(getContext()),
) {
const normalizedCandidate = normalizeChatIdCandidate(candidateChatId);
if (!normalizedCandidate || !identity || typeof identity !== "object") {
return false;
}
const knownChatIds = new Set();
const addKnownChatId = (value) => {
const normalized = normalizeChatIdCandidate(value);
if (normalized) {
knownChatIds.add(normalized);
}
};
addKnownChatId(identity.chatId);
addKnownChatId(identity.hostChatId);
addKnownChatId(identity.integrity);
for (const aliasCandidate of getGraphIdentityAliasCandidates({
integrity: identity.integrity,
hostChatId: identity.hostChatId,
persistenceChatId: identity.chatId,
})) {
addKnownChatId(aliasCandidate);
}
return knownChatIds.has(normalizedCandidate);
}
function resolveCompatibleGraphShadowSnapshot(
identity = resolveCurrentChatIdentity(getContext()),
) {
if (!identity || typeof identity !== "object") {
return null;
}
const directSnapshot = readGraphShadowSnapshot(identity.chatId);
if (directSnapshot) {
return directSnapshot;
}
const seenChatIds = new Set(
[identity.chatId].map((value) => normalizeChatIdCandidate(value)).filter(Boolean),
);
const readByChatId = (value) => {
const normalized = normalizeChatIdCandidate(value);
if (!normalized || seenChatIds.has(normalized)) {
return null;
}
seenChatIds.add(normalized);
return readGraphShadowSnapshot(normalized);
};
const hostSnapshot = readByChatId(identity.hostChatId);
if (hostSnapshot) {
return hostSnapshot;
}
for (const aliasCandidate of getGraphIdentityAliasCandidates({
integrity: identity.integrity,
hostChatId: identity.hostChatId,
persistenceChatId: identity.chatId,
})) {
const aliasSnapshot = readByChatId(aliasCandidate);
if (aliasSnapshot) {
return aliasSnapshot;
}
}
return findGraphShadowSnapshotByIntegrity(identity.integrity, {
excludeChatIds: Array.from(seenChatIds),
});
}
async function refreshRuntimeGraphAfterSyncApplied(syncPayload = {}) {
const action = String(syncPayload?.action || "")
.trim()
@@ -3348,8 +3639,14 @@ async function refreshRuntimeGraphAfterSyncApplied(syncPayload = {}) {
}
const syncedChatId = normalizeChatIdCandidate(syncPayload?.chatId);
const activeChatId = normalizeChatIdCandidate(getCurrentChatId());
const targetChatId = syncedChatId || activeChatId;
const activeIdentity = resolveCurrentChatIdentity(getContext());
const activeChatId = normalizeChatIdCandidate(activeIdentity.chatId);
const targetChatId =
activeChatId &&
syncedChatId &&
doesChatIdMatchResolvedGraphIdentity(syncedChatId, activeIdentity)
? activeChatId
: syncedChatId || activeChatId;
if (!targetChatId) {
return {
@@ -3671,6 +3968,223 @@ function readLegacyGraphFromChatMetadata(chatId, context = getContext()) {
}
}
async function maybeRecoverIndexedDbGraphFromStableIdentity(
chatId,
context = getContext(),
{ source = "unknown", db = null } = {},
) {
const normalizedChatId = normalizeChatIdCandidate(chatId);
if (!normalizedChatId) {
return {
migrated: false,
reason: "identity-recovery-missing-chat-id",
chatId: "",
};
}
const identity = resolveCurrentChatIdentity(context);
if (!identity.integrity) {
return {
migrated: false,
reason: "identity-recovery-integrity-missing",
chatId: normalizedChatId,
};
}
const manager = ensureBmeChatManager();
if (!manager) {
return {
migrated: false,
reason: "identity-recovery-manager-unavailable",
chatId: normalizedChatId,
};
}
const targetDb = db || (await manager.getCurrentDb(normalizedChatId));
if (!targetDb) {
return {
migrated: false,
reason: "identity-recovery-db-unavailable",
chatId: normalizedChatId,
};
}
const emptyStatus = await targetDb.isEmpty();
if (!emptyStatus?.empty) {
return {
migrated: false,
reason: "identity-recovery-target-not-empty",
chatId: normalizedChatId,
emptyStatus,
};
}
const finalizeMigration = async (
graph,
{
revision = 0,
legacyChatId = "",
migrationSource = "identity-recovery",
shadowChatId = "",
} = {},
) => {
const snapshot = await importRecoveredSnapshotToIndexedDb(
targetDb,
normalizedChatId,
graph,
{
revision,
integrity: identity.integrity,
source: migrationSource,
legacyChatId,
},
);
cacheIndexedDbSnapshot(normalizedChatId, snapshot);
rememberResolvedGraphIdentityAlias(context, normalizedChatId);
if (shadowChatId && shadowChatId !== normalizedChatId) {
removeGraphShadowSnapshot(shadowChatId);
}
let syncResult = {
synced: false,
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);
syncResult = {
synced: false,
reason: "identity-recovery-sync-failed",
chatId: normalizedChatId,
error: syncError?.message || String(syncError),
};
}
return {
migrated: true,
reason: "identity-recovery-completed",
chatId: normalizedChatId,
legacyChatId: normalizeChatIdCandidate(legacyChatId),
source: migrationSource,
snapshot,
syncResult,
};
};
const currentGraphMeta = getGraphPersistenceMeta(currentGraph) || {};
const runtimeGraphIntegrity = normalizeChatIdCandidate(
currentGraphMeta.integrity || graphPersistenceState.metadataIntegrity,
);
const runtimeGraphChatId = normalizeChatIdCandidate(
currentGraph?.historyState?.chatId ||
currentGraphMeta.chatId ||
graphPersistenceState.chatId,
);
if (
currentGraph &&
!isGraphEffectivelyEmpty(currentGraph) &&
runtimeGraphIntegrity &&
runtimeGraphIntegrity === identity.integrity &&
runtimeGraphChatId &&
runtimeGraphChatId !== normalizedChatId
) {
return await finalizeMigration(currentGraph, {
revision: Math.max(
graphPersistenceState.revision || 0,
getGraphPersistedRevision(currentGraph),
1,
),
legacyChatId: runtimeGraphChatId,
migrationSource: "runtime-identity-promotion",
});
}
const aliasShadowSnapshot = findGraphShadowSnapshotByIntegrity(
identity.integrity,
{
excludeChatIds: [normalizedChatId],
},
);
if (aliasShadowSnapshot?.serializedGraph) {
try {
const shadowGraph = normalizeGraphRuntimeState(
deserializeGraph(aliasShadowSnapshot.serializedGraph),
normalizedChatId,
);
if (!isGraphEffectivelyEmpty(shadowGraph)) {
return await finalizeMigration(shadowGraph, {
revision: Math.max(
Number(aliasShadowSnapshot.revision || 0),
getGraphPersistedRevision(shadowGraph),
1,
),
legacyChatId:
aliasShadowSnapshot.persistedChatId || aliasShadowSnapshot.chatId,
migrationSource: "shadow-identity-recovery",
shadowChatId: aliasShadowSnapshot.chatId,
});
}
} catch (error) {
console.warn("[ST-BME] 通过影子快照恢复聊天身份失败:", error);
}
}
const legacyCandidates = buildLegacyGraphIdentityCandidates(
normalizedChatId,
context,
{
shadowSnapshot: aliasShadowSnapshot,
},
);
for (const legacyChatId of legacyCandidates) {
try {
const legacySnapshot = await exportIndexedDbSnapshotForChat(legacyChatId);
if (!isIndexedDbSnapshotMeaningful(legacySnapshot)) {
continue;
}
const legacyGraph = buildGraphFromSnapshot(legacySnapshot, {
chatId: legacyChatId,
});
if (isGraphEffectivelyEmpty(legacyGraph)) {
continue;
}
return await finalizeMigration(legacyGraph, {
revision: Math.max(
normalizeIndexedDbRevision(legacySnapshot?.meta?.revision),
getGraphPersistedRevision(legacyGraph),
1,
),
legacyChatId,
migrationSource: "indexeddb-identity-alias",
});
} catch (error) {
console.warn("[ST-BME] 读取旧身份 IndexedDB 图谱失败:", {
legacyChatId,
error,
});
}
}
return {
migrated: false,
reason: "identity-recovery-no-match",
chatId: normalizedChatId,
};
}
async function maybeMigrateLegacyGraphToIndexedDb(
chatId,
context = getContext(),
@@ -3980,6 +4494,14 @@ function applyIndexedDbSnapshotToRuntime(
normalizeGraphRuntimeState(graphFromSnapshot, normalizedChatId),
normalizedChatId,
);
stampGraphPersistenceMeta(currentGraph, {
revision,
reason: `indexeddb:${String(source || "indexeddb")}`,
chatId: normalizedChatId,
integrity:
normalizeChatIdCandidate(snapshot?.meta?.integrity) ||
getChatMetadataIntegrity(getContext()),
});
currentGraph.vectorIndexState.lastIntegrityIssue = null;
extractionCount = Number.isFinite(currentGraph?.historyState?.extractionCount)
@@ -4045,6 +4567,7 @@ function applyIndexedDbSnapshotToRuntime(
at: Date.now(),
},
});
rememberResolvedGraphIdentityAlias(getContext(), normalizedChatId);
removeGraphShadowSnapshot(normalizedChatId);
refreshPanelLiveState();
@@ -4101,14 +4624,59 @@ async function loadGraphFromIndexedDb(
}
const db = await manager.getCurrentDb(normalizedChatId);
const migrationResult = await maybeMigrateLegacyGraphToIndexedDb(
normalizedChatId,
getContext(),
{
source,
db,
},
);
const identityRecoveryResult =
await maybeRecoverIndexedDbGraphFromStableIdentity(
normalizedChatId,
getContext(),
{
source,
db,
},
);
if (identityRecoveryResult?.migrated) {
const recoveredRevision = normalizeIndexedDbRevision(
identityRecoveryResult?.snapshot?.meta?.revision,
);
updateGraphPersistenceState({
storagePrimary: "indexeddb",
storageMode: "indexeddb",
indexedDbRevision: recoveredRevision,
indexedDbLastError: "",
lastSyncError: "",
dualWriteLastResult: {
action: "identity-recovery",
source: String(identityRecoveryResult?.source || "indexeddb"),
success: true,
chatId: normalizedChatId,
legacyChatId: String(identityRecoveryResult?.legacyChatId || ""),
revision: recoveredRevision,
reason: String(
identityRecoveryResult?.reason || "identity-recovery",
),
at: Date.now(),
syncResult: cloneRuntimeDebugValue(
identityRecoveryResult?.syncResult,
null,
),
},
});
}
const migrationResult = identityRecoveryResult?.migrated
? {
migrated: false,
reason: "identity-recovery-already-applied",
chatId: normalizedChatId,
}
: await maybeMigrateLegacyGraphToIndexedDb(
normalizedChatId,
getContext(),
{
source,
db,
},
);
if (migrationResult?.migrated) {
const migratedRevision = normalizeIndexedDbRevision(
@@ -4146,8 +4714,11 @@ async function loadGraphFromIndexedDb(
},
});
}
const snapshot =
identityRecoveryResult?.snapshot ||
migrationResult?.snapshot ||
(await db.exportSnapshot());
const snapshot = migrationResult?.snapshot || (await db.exportSnapshot());
cacheIndexedDbSnapshot(normalizedChatId, snapshot);
if (!isIndexedDbSnapshotMeaningful(snapshot)) {
@@ -4713,6 +5284,7 @@ function persistGraphToChatMetadata(
queuedPersistRotateIntegrity: false,
queuedPersistReason: "",
});
rememberResolvedGraphIdentityAlias(context, chatId);
return buildGraphPersistResult({
saved: true,
@@ -5949,7 +6521,7 @@ function loadGraphFromChat(options = {}) {
normalizeGraphRuntimeState(deserializeGraph(savedData), chatId),
chatId,
);
const shadowSnapshot = readGraphShadowSnapshot(chatId);
const shadowSnapshot = resolveCompatibleGraphShadowSnapshot(chatIdentity);
const shadowDecision = shouldPreferShadowSnapshotOverOfficial(
officialGraph,
shadowSnapshot,
@@ -5976,6 +6548,12 @@ function loadGraphFromChat(options = {}) {
clearPendingGraphLoadRetry();
currentGraph = officialGraph;
stampGraphPersistenceMeta(currentGraph, {
revision: officialRevision,
reason: `${source}:metadata-compat-provisional`,
chatId,
integrity: getChatMetadataIntegrity(context),
});
extractionCount = Number.isFinite(
currentGraph?.historyState?.extractionCount,
)
@@ -6043,6 +6621,7 @@ function loadGraphFromChat(options = {}) {
at: Date.now(),
},
});
rememberResolvedGraphIdentityAlias(context, chatId);
scheduleIndexedDbGraphProbe(chatId, {
source: `${source}:indexeddb-probe`,
@@ -6126,6 +6705,7 @@ async function saveGraphToIndexedDb(
};
}
const db = await manager.getCurrentDb(normalizedChatId);
const currentIdentity = resolveCurrentChatIdentity(getContext());
const baseSnapshot =
readCachedIndexedDbSnapshot(normalizedChatId) ||
(await db.exportSnapshot());
@@ -6137,6 +6717,9 @@ async function saveGraphToIndexedDb(
meta: {
storagePrimary: "indexeddb",
lastMutationReason: String(reason || "graph-save"),
integrity:
currentIdentity.integrity || graphPersistenceState.metadataIntegrity,
hostChatId: currentIdentity.hostChatId || "",
},
});
const importResult = await db.importSnapshot(snapshot, {
@@ -6179,6 +6762,7 @@ async function saveGraphToIndexedDb(
at: Date.now(),
},
});
rememberResolvedGraphIdentityAlias(getContext(), normalizedChatId);
return {
saved: true,

View File

@@ -4,15 +4,22 @@ import path from "node:path";
import { fileURLToPath } from "node:url";
import vm from "node:vm";
import { buildGraphFromSnapshot, buildSnapshotFromGraph } from "../bme-db.js";
import {
buildBmeDbName,
buildGraphFromSnapshot,
buildSnapshotFromGraph,
} from "../bme-db.js";
import { onMessageReceivedController } from "../event-binding.js";
import {
cloneGraphForPersistence,
cloneRuntimeDebugValue,
findGraphShadowSnapshotByIntegrity,
getGraphPersistedRevision,
getGraphIdentityAliasCandidates,
getGraphPersistenceMeta,
getGraphShadowSnapshotStorageKey,
GRAPH_LOAD_PENDING_CHAT_ID,
GRAPH_IDENTITY_ALIAS_STORAGE_KEY,
GRAPH_LOAD_STATES,
GRAPH_METADATA_KEY,
GRAPH_PERSISTENCE_META_KEY,
@@ -21,7 +28,9 @@ import {
GRAPH_STARTUP_RECONCILE_DELAYS_MS,
MODULE_NAME,
readGraphShadowSnapshot,
rememberGraphIdentityAlias,
removeGraphShadowSnapshot,
resolveGraphIdentityAliasByHostChatId,
shouldPreferShadowSnapshotOverOfficial,
stampGraphPersistenceMeta,
writeChatMetadataPatch,
@@ -83,6 +92,28 @@ const messageSnippet = extractSnippet(
);
function createSessionStorage(seed = null) {
const store = seed instanceof Map ? seed : new Map();
return {
__store: store,
get length() {
return store.size;
},
key(index) {
return Array.from(store.keys())[Number(index)] ?? null;
},
getItem(key) {
return store.has(key) ? store.get(key) : null;
},
setItem(key, value) {
store.set(String(key), String(value));
},
removeItem(key) {
store.delete(String(key));
},
};
}
function createLocalStorage(seed = null) {
const store = seed instanceof Map ? seed : new Map();
return {
__store: store,
@@ -154,15 +185,75 @@ async function createGraphPersistenceHarness({
chatId = "chat-test",
chatMetadata = undefined,
sessionStore = null,
localStore = null,
globalChatId = "",
characterId = "",
groupId = null,
indexedDbSnapshot = null,
indexedDbSnapshots = null,
chat = [],
} = {}) {
const timers = new Map();
let nextTimerId = 1;
const storage = createSessionStorage(sessionStore);
const localStorage = createLocalStorage(localStore);
const indexedDbSnapshotMap =
indexedDbSnapshots instanceof Map
? new Map(indexedDbSnapshots)
: new Map(
Object.entries(
indexedDbSnapshots &&
typeof indexedDbSnapshots === "object" &&
!Array.isArray(indexedDbSnapshots)
? indexedDbSnapshots
: {},
),
);
if (indexedDbSnapshot) {
const primaryChatId = String(chatId || globalChatId || "");
if (primaryChatId) {
indexedDbSnapshotMap.set(primaryChatId, structuredClone(indexedDbSnapshot));
}
}
function buildEmptyIndexedDbSnapshot(targetChatId = "") {
return {
meta: { revision: 0, chatId: String(targetChatId || "") },
nodes: [],
edges: [],
tombstones: [],
state: { lastProcessedFloor: -1, extractionCount: 0 },
};
}
function getIndexedDbSnapshotForChat(targetChatId = "") {
const normalizedChatId = String(targetChatId || "");
if (normalizedChatId && indexedDbSnapshotMap.has(normalizedChatId)) {
return structuredClone(indexedDbSnapshotMap.get(normalizedChatId));
}
if (
normalizedChatId &&
indexedDbSnapshot &&
!indexedDbSnapshotMap.size &&
normalizedChatId === String(chatId || globalChatId || "")
) {
return structuredClone(indexedDbSnapshot);
}
return buildEmptyIndexedDbSnapshot(normalizedChatId);
}
function setIndexedDbSnapshotForChat(targetChatId = "", snapshot = null) {
const normalizedChatId = String(targetChatId || "");
if (!normalizedChatId) return;
if (!snapshot) {
indexedDbSnapshotMap.delete(normalizedChatId);
return;
}
indexedDbSnapshotMap.set(normalizedChatId, structuredClone(snapshot));
}
const runtimeContext = {
console,
@@ -176,8 +267,12 @@ async function createGraphPersistenceHarness({
Boolean,
structuredClone,
result: null,
__indexedDbSnapshot: indexedDbSnapshot,
__indexedDbSnapshot: getIndexedDbSnapshotForChat(
String(chatId || globalChatId || ""),
),
__indexedDbSnapshots: indexedDbSnapshotMap,
sessionStorage: storage,
localStorage,
setTimeout(fn, delay) {
const id = nextTimerId++;
timers.set(id, { fn, delay });
@@ -211,6 +306,21 @@ async function createGraphPersistenceHarness({
},
},
__globalChatId: String(globalChatId || ""),
Dexie: {
async exists(dbName = "") {
return Array.from(indexedDbSnapshotMap.keys()).some(
(candidateChatId) => buildBmeDbName(candidateChatId) === String(dbName),
);
},
async getDatabaseNames() {
return Array.from(indexedDbSnapshotMap.keys()).map((candidateChatId) =>
buildBmeDbName(candidateChatId),
);
},
},
async ensureDexieLoaded() {
return runtimeContext.Dexie;
},
refreshPanelLiveState() {
runtimeContext.__panelRefreshCount += 1;
},
@@ -241,7 +351,9 @@ async function createGraphPersistenceHarness({
onMessageReceivedController,
getGraphPersistenceMeta,
getGraphPersistedRevision,
getGraphIdentityAliasCandidates,
getGraphShadowSnapshotStorageKey,
GRAPH_IDENTITY_ALIAS_STORAGE_KEY,
GRAPH_LOAD_PENDING_CHAT_ID,
GRAPH_LOAD_STATES,
GRAPH_METADATA_KEY,
@@ -250,14 +362,141 @@ async function createGraphPersistenceHarness({
GRAPH_SHADOW_SNAPSHOT_STORAGE_PREFIX,
GRAPH_STARTUP_RECONCILE_DELAYS_MS,
MODULE_NAME,
findGraphShadowSnapshotByIntegrity,
readGraphShadowSnapshot,
rememberGraphIdentityAlias,
removeGraphShadowSnapshot,
resolveGraphIdentityAliasByHostChatId,
shouldPreferShadowSnapshotOverOfficial,
stampGraphPersistenceMeta,
writeChatMetadataPatch,
writeGraphShadowSnapshot,
// Shadow snapshot functions need VM-local sessionStorage overrides
// because imported versions use the outer globalThis (no sessionStorage)
rememberGraphIdentityAlias({
integrity = "",
hostChatId = "",
persistenceChatId = "",
} = {}) {
const normalizedIntegrity = String(integrity || "").trim();
if (!normalizedIntegrity) return null;
const normalizedHostChatId = String(hostChatId || "").trim();
const normalizedPersistenceChatId = String(
persistenceChatId || normalizedIntegrity,
).trim();
let registry = { byIntegrity: {} };
try {
const raw = localStorage.getItem(GRAPH_IDENTITY_ALIAS_STORAGE_KEY);
if (raw) {
const parsed = JSON.parse(raw);
if (
parsed?.byIntegrity &&
typeof parsed.byIntegrity === "object" &&
!Array.isArray(parsed.byIntegrity)
) {
registry = { byIntegrity: parsed.byIntegrity };
}
}
} catch {
registry = { byIntegrity: {} };
}
const current = registry.byIntegrity[normalizedIntegrity] || {};
const hostChatIds = Array.from(
new Set(
[
normalizedHostChatId,
...(Array.isArray(current.hostChatIds) ? current.hostChatIds : []),
].filter(Boolean),
),
);
const next = {
integrity: normalizedIntegrity,
persistenceChatId: normalizedPersistenceChatId,
hostChatIds,
updatedAt: new Date().toISOString(),
};
registry.byIntegrity[normalizedIntegrity] = next;
localStorage.setItem(
GRAPH_IDENTITY_ALIAS_STORAGE_KEY,
JSON.stringify(registry),
);
return next;
},
resolveGraphIdentityAliasByHostChatId(hostChatId = "") {
const normalizedHostChatId = String(hostChatId || "").trim();
if (!normalizedHostChatId) return "";
try {
const raw = localStorage.getItem(GRAPH_IDENTITY_ALIAS_STORAGE_KEY);
const parsed = raw ? JSON.parse(raw) : { byIntegrity: {} };
let best = "";
let bestUpdatedAt = "";
for (const value of Object.values(parsed.byIntegrity || {})) {
const hostChatIds = Array.isArray(value?.hostChatIds)
? value.hostChatIds.map((item) => String(item || "").trim())
: [];
if (!hostChatIds.includes(normalizedHostChatId)) continue;
const persistenceChatId = String(
value?.persistenceChatId || value?.integrity || "",
).trim();
if (!persistenceChatId) continue;
const updatedAt = String(value?.updatedAt || "");
if (!best || updatedAt > bestUpdatedAt) {
best = persistenceChatId;
bestUpdatedAt = updatedAt;
}
}
return best;
} catch {
return "";
}
},
getGraphIdentityAliasCandidates({
integrity = "",
hostChatId = "",
persistenceChatId = "",
} = {}) {
const normalizedIntegrity = String(integrity || "").trim();
const normalizedHostChatId = String(hostChatId || "").trim();
const normalizedPersistenceChatId = String(
persistenceChatId || "",
).trim();
const candidates = [];
const seen = new Set();
const addCandidate = (value) => {
const normalized = String(value || "").trim();
if (!normalized || seen.has(normalized)) return;
seen.add(normalized);
candidates.push(normalized);
};
try {
const raw = localStorage.getItem(GRAPH_IDENTITY_ALIAS_STORAGE_KEY);
const parsed = raw ? JSON.parse(raw) : { byIntegrity: {} };
if (normalizedIntegrity) {
const value = parsed.byIntegrity?.[normalizedIntegrity] || {};
addCandidate(value?.persistenceChatId || value?.integrity || "");
for (const candidate of Array.isArray(value?.hostChatIds)
? value.hostChatIds
: []) {
addCandidate(candidate);
}
} else if (normalizedHostChatId) {
addCandidate(
runtimeContext.resolveGraphIdentityAliasByHostChatId(
normalizedHostChatId,
),
);
}
} catch {
// ignore
}
addCandidate(normalizedHostChatId);
addCandidate(normalizedPersistenceChatId);
return candidates;
},
readGraphShadowSnapshot(chatId = "") {
const key = getGraphShadowSnapshotStorageKey(chatId);
if (!key) return null;
@@ -286,6 +525,56 @@ async function createGraphPersistenceHarness({
return null;
}
},
findGraphShadowSnapshotByIntegrity(integrity = "", { excludeChatIds = [] } = {}) {
const normalizedIntegrity = String(integrity || "").trim();
if (!normalizedIntegrity) return null;
const excluded = new Set(
(Array.isArray(excludeChatIds) ? excludeChatIds : [])
.map((value) => String(value || "").trim())
.filter(Boolean),
);
let best = null;
for (const key of storage.__store.keys()) {
if (!String(key || "").startsWith(GRAPH_SHADOW_SNAPSHOT_STORAGE_PREFIX)) {
continue;
}
try {
const snap = JSON.parse(storage.getItem(key));
if (
!snap ||
String(snap.integrity || "") !== normalizedIntegrity ||
typeof snap.serializedGraph !== "string" ||
!snap.serializedGraph
) {
continue;
}
const normalizedChatId = String(snap.chatId || "").trim();
if (!normalizedChatId || excluded.has(normalizedChatId)) {
continue;
}
if (
!best ||
Number(snap.revision || 0) > Number(best.revision || 0) ||
(Number(snap.revision || 0) === Number(best.revision || 0) &&
String(snap.updatedAt || "") > String(best.updatedAt || ""))
) {
best = {
chatId: normalizedChatId,
revision: Number.isFinite(snap.revision) ? snap.revision : 0,
serializedGraph: snap.serializedGraph,
updatedAt: String(snap.updatedAt || ""),
reason: String(snap.reason || ""),
integrity: String(snap.integrity || ""),
persistedChatId: String(snap.persistedChatId || ""),
debugReason: String(snap.debugReason || snap.reason || ""),
};
}
} catch {
// ignore
}
}
return best;
},
writeGraphShadowSnapshot(
chatId = "",
graph = null,
@@ -350,6 +639,8 @@ async function createGraphPersistenceHarness({
return true;
},
notifyExtractionIssue() {},
debugDebug() {},
debugLog() {},
async runExtraction() {},
getRequestHeaders() {
return {};
@@ -394,24 +685,37 @@ async function createGraphPersistenceHarness({
__contextImmediateSaveCalls: 0,
buildGraphFromSnapshot,
buildSnapshotFromGraph,
buildBmeDbName,
scheduleUpload() {},
BmeDatabase: class {
constructor(dbChatId = "") {
this.chatId = String(dbChatId || "");
}
async open() {}
async close() {}
async exportSnapshot() {
return getIndexedDbSnapshotForChat(this.chatId);
}
async importSnapshot(snapshot) {
setIndexedDbSnapshotForChat(this.chatId, snapshot);
return {
revision: Number(snapshot?.meta?.revision) || 0,
};
}
},
BmeChatManager: class {
constructor() {
this._db = {
this._currentChatId = "";
}
_createDb(dbChatId = "") {
return {
async exportSnapshot() {
if (runtimeContext.__indexedDbSnapshot) {
return structuredClone(runtimeContext.__indexedDbSnapshot);
}
return {
meta: { revision: 0, chatId: "" },
nodes: [],
edges: [],
tombstones: [],
state: { lastProcessedFloor: -1, extractionCount: 0 },
};
return getIndexedDbSnapshotForChat(dbChatId);
},
async importSnapshot(snapshot) {
runtimeContext.__indexedDbSnapshot = structuredClone(snapshot);
setIndexedDbSnapshotForChat(dbChatId, snapshot);
runtimeContext.__indexedDbSnapshot =
getIndexedDbSnapshotForChat(dbChatId);
return {
revision:
Number(snapshot?.meta?.revision) ||
@@ -420,19 +724,18 @@ async function createGraphPersistenceHarness({
};
},
async getMeta(key, fallbackValue = 0) {
const snapshot = runtimeContext.__indexedDbSnapshot || {};
const snapshot = getIndexedDbSnapshotForChat(dbChatId) || {};
if (!snapshot?.meta || !(key in snapshot.meta)) {
return fallbackValue;
}
return snapshot.meta[key];
},
async getRevision() {
return (
Number(runtimeContext.__indexedDbSnapshot?.meta?.revision) || 0
);
const snapshot = getIndexedDbSnapshotForChat(dbChatId) || {};
return Number(snapshot?.meta?.revision) || 0;
},
async isEmpty() {
const snapshot = runtimeContext.__indexedDbSnapshot || {};
const snapshot = getIndexedDbSnapshotForChat(dbChatId) || {};
const nodes = Array.isArray(snapshot?.nodes)
? snapshot.nodes.length
: 0;
@@ -451,33 +754,44 @@ async function createGraphPersistenceHarness({
},
async importLegacyGraph(graph, options = {}) {
const revision = Number(options?.revision) || 1;
runtimeContext.__indexedDbSnapshot = buildSnapshotFromGraph(graph, {
chatId: runtimeContext.__chatContext?.chatId || "",
const migratedSnapshot = buildSnapshotFromGraph(graph, {
chatId: dbChatId || runtimeContext.__chatContext?.chatId || "",
revision,
meta: {
migrationCompletedAt: Date.now(),
migrationSource: "chat_metadata",
},
});
setIndexedDbSnapshotForChat(dbChatId, migratedSnapshot);
runtimeContext.__indexedDbSnapshot =
getIndexedDbSnapshotForChat(dbChatId);
return {
migrated: true,
revision,
imported: {
nodes: runtimeContext.__indexedDbSnapshot.nodes.length,
edges: runtimeContext.__indexedDbSnapshot.edges.length,
nodes: runtimeContext.__indexedDbSnapshot?.nodes?.length || 0,
edges: runtimeContext.__indexedDbSnapshot?.edges?.length || 0,
tombstones:
runtimeContext.__indexedDbSnapshot.tombstones.length,
runtimeContext.__indexedDbSnapshot?.tombstones?.length || 0,
},
};
},
async markSyncDirty() {},
};
}
async getCurrentDb() {
return this._db;
async getCurrentDb(dbChatId = this._currentChatId) {
this._currentChatId = String(dbChatId || this._currentChatId || "");
runtimeContext.__indexedDbSnapshot = getIndexedDbSnapshotForChat(
this._currentChatId,
);
return this._createDb(this._currentChatId);
}
async switchChat() {
return this._db;
async switchChat(dbChatId = "") {
this._currentChatId = String(dbChatId || "");
runtimeContext.__indexedDbSnapshot = getIndexedDbSnapshotForChat(
this._currentChatId,
);
return this._createDb(this._currentChatId);
}
async closeCurrent() {}
},
@@ -544,11 +858,33 @@ result = {
return globalThis.__chatContext;
},
setIndexedDbSnapshot(snapshot) {
globalThis.__indexedDbSnapshot = snapshot;
const activeChatId =
String(globalThis.__chatContext?.chatId || globalThis.__globalChatId || "");
if (activeChatId) {
globalThis.__indexedDbSnapshots.set(
activeChatId,
structuredClone(snapshot),
);
}
globalThis.__indexedDbSnapshot = structuredClone(snapshot);
},
getIndexedDbSnapshot() {
return globalThis.__indexedDbSnapshot;
},
setIndexedDbSnapshotForChat(chatId, snapshot) {
const normalizedChatId = String(chatId || "");
if (!normalizedChatId) return;
globalThis.__indexedDbSnapshots.set(
normalizedChatId,
structuredClone(snapshot),
);
},
getIndexedDbSnapshotForChat(chatId) {
const normalizedChatId = String(chatId || "");
if (!normalizedChatId) return null;
const snapshot = globalThis.__indexedDbSnapshots.get(normalizedChatId);
return snapshot ? structuredClone(snapshot) : null;
},
};
`,
].join("\n"),
@@ -708,7 +1044,10 @@ result = {
assert.equal(result.synced, true);
assert.equal(result.loadState, "loading");
assert.equal(harness.api.getCurrentGraph().historyState.chatId, "chat-late");
assert.equal(
harness.api.getCurrentGraph().historyState.chatId,
"chat-late-ready",
);
assert.equal(harness.api.getGraphPersistenceState().dbReady, true);
assert.equal(
harness.api.getGraphPersistenceState().storagePrimary,