Harden local store cache invalidation for storage mode switching

This commit is contained in:
Youzini-afk
2026-04-13 23:03:25 +08:00
parent a15259d73b
commit 74d661e433
2 changed files with 143 additions and 6 deletions

View File

@@ -3630,6 +3630,41 @@ function resolveSnapshotGraphStorePresentation(
return buildIndexedDbStorePresentation(); return buildIndexedDbStorePresentation();
} }
function buildGraphLocalStoreSelectorKey(
presentation = buildIndexedDbStorePresentation(),
) {
const normalizedPresentation =
presentation && typeof presentation === "object"
? presentation
: buildIndexedDbStorePresentation();
const storagePrimary =
normalizedPresentation.storagePrimary === "opfs" ||
isGraphLocalStorageModeOpfs(normalizedPresentation.storageMode)
? "opfs"
: "indexeddb";
const storageMode =
storagePrimary === "opfs"
? normalizeGraphLocalStorageMode(
normalizedPresentation.storageMode,
BME_GRAPH_LOCAL_STORAGE_MODE_OPFS_SHADOW,
)
: BME_GRAPH_LOCAL_STORAGE_MODE_INDEXEDDB;
return `${storagePrimary}:${storageMode}`;
}
function isGraphLocalStorePresentationCompatible(left, right) {
return (
buildGraphLocalStoreSelectorKey(left) ===
buildGraphLocalStoreSelectorKey(right)
);
}
function isCachedIndexedDbSnapshotCompatible(snapshot = null, expectedStore = null) {
if (!expectedStore || typeof expectedStore !== "object") return true;
const snapshotStore = resolveSnapshotGraphStorePresentation(snapshot, expectedStore);
return isGraphLocalStorePresentationCompatible(snapshotStore, expectedStore);
}
async function getGraphLocalStoreCapability(forceRefresh = false) { async function getGraphLocalStoreCapability(forceRefresh = false) {
if (!forceRefresh && bmeLocalStoreCapabilitySnapshot.checked) { if (!forceRefresh && bmeLocalStoreCapabilitySnapshot.checked) {
return bmeLocalStoreCapabilitySnapshot; return bmeLocalStoreCapabilitySnapshot;
@@ -3787,6 +3822,10 @@ function ensureBmeChatManager() {
bmeChatManager = new BmeChatManager({ bmeChatManager = new BmeChatManager({
databaseFactory: async (chatId) => databaseFactory: async (chatId) =>
await createPreferredGraphLocalStore(chatId), await createPreferredGraphLocalStore(chatId),
selectorKeyResolver: async () =>
buildGraphLocalStoreSelectorKey(
await resolvePreferredGraphLocalStorePresentation(),
),
}); });
} }
return bmeChatManager; return bmeChatManager;
@@ -3916,19 +3955,33 @@ function isIndexedDbSnapshotMeaningful(snapshot = null) {
function cacheIndexedDbSnapshot(chatId, snapshot = null) { function cacheIndexedDbSnapshot(chatId, snapshot = null) {
const normalizedChatId = normalizeChatIdCandidate(chatId); const normalizedChatId = normalizeChatIdCandidate(chatId);
if (!normalizedChatId || !snapshot || typeof snapshot !== "object") return; if (!normalizedChatId || !snapshot || typeof snapshot !== "object") return;
const snapshotStore = resolveSnapshotGraphStorePresentation(snapshot);
bmeIndexedDbSnapshotCacheByChatId.set(normalizedChatId, { bmeIndexedDbSnapshotCacheByChatId.set(normalizedChatId, {
chatId: normalizedChatId, chatId: normalizedChatId,
revision: normalizeIndexedDbRevision(snapshot?.meta?.revision), revision: normalizeIndexedDbRevision(snapshot?.meta?.revision),
selectorKey: buildGraphLocalStoreSelectorKey(snapshotStore),
snapshot, snapshot,
updatedAt: Date.now(), updatedAt: Date.now(),
}); });
} }
function readCachedIndexedDbSnapshot(chatId) { function readCachedIndexedDbSnapshot(chatId, expectedStore = null) {
const normalizedChatId = normalizeChatIdCandidate(chatId); const normalizedChatId = normalizeChatIdCandidate(chatId);
if (!normalizedChatId) return null; if (!normalizedChatId) return null;
const cacheEntry = bmeIndexedDbSnapshotCacheByChatId.get(normalizedChatId); const cacheEntry = bmeIndexedDbSnapshotCacheByChatId.get(normalizedChatId);
if (!cacheEntry?.snapshot) return null; if (!cacheEntry?.snapshot) return null;
if (expectedStore && typeof expectedStore === "object") {
const expectedSelectorKey = buildGraphLocalStoreSelectorKey(expectedStore);
if (cacheEntry.selectorKey && cacheEntry.selectorKey !== expectedSelectorKey) {
return null;
}
if (
!cacheEntry.selectorKey &&
!isCachedIndexedDbSnapshotCompatible(cacheEntry.snapshot, expectedStore)
) {
return null;
}
}
return cacheEntry.snapshot; return cacheEntry.snapshot;
} }
@@ -7400,11 +7453,15 @@ function syncGraphLoadFromLiveContext(options = {}) {
}); });
} }
const cachedSnapshot = readCachedIndexedDbSnapshot(chatId); const cachedPreferredLocalStore = getPreferredGraphLocalStorePresentationSync();
const cachedSnapshot = readCachedIndexedDbSnapshot(
chatId,
cachedPreferredLocalStore,
);
if (isIndexedDbSnapshotMeaningful(cachedSnapshot)) { if (isIndexedDbSnapshotMeaningful(cachedSnapshot)) {
const cachedStore = resolveSnapshotGraphStorePresentation( const cachedStore = resolveSnapshotGraphStorePresentation(
cachedSnapshot, cachedSnapshot,
getPreferredGraphLocalStorePresentationSync(), cachedPreferredLocalStore,
); );
const result = applyIndexedDbSnapshotToRuntime(chatId, cachedSnapshot, { const result = applyIndexedDbSnapshotToRuntime(chatId, cachedSnapshot, {
source: `${source}:indexeddb-cache`, source: `${source}:indexeddb-cache`,
@@ -8232,6 +8289,9 @@ function updateModuleSettings(patch = {}) {
const previousCloudStorageMode = String( const previousCloudStorageMode = String(
settings.cloudStorageMode || "automatic", settings.cloudStorageMode || "automatic",
); );
const previousGraphLocalStorageMode = getRequestedGraphLocalStorageMode(
settings,
);
Object.assign(settings, patch); Object.assign(settings, patch);
extension_settings[MODULE_NAME] = settings; extension_settings[MODULE_NAME] = settings;
globalThis.__stBmeDebugLoggingEnabled = Boolean( globalThis.__stBmeDebugLoggingEnabled = Boolean(
@@ -8298,6 +8358,21 @@ function updateModuleSettings(patch = {}) {
refreshVisibleStageNotices(); refreshVisibleStageNotices();
} }
const currentGraphLocalStorageMode = getRequestedGraphLocalStorageMode(
settings,
);
if (previousGraphLocalStorageMode !== currentGraphLocalStorageMode) {
clearAllCachedIndexedDbSnapshots();
scheduleBmeIndexedDbTask(async () => {
if (bmeChatManager && typeof bmeChatManager.closeAll === "function") {
await bmeChatManager.closeAll();
}
await syncBmeChatManagerWithCurrentChat(
"settings:graph-local-storage-mode-changed",
);
});
}
const currentCloudStorageMode = String( const currentCloudStorageMode = String(
settings.cloudStorageMode || "automatic", settings.cloudStorageMode || "automatic",
); );
@@ -8468,17 +8543,29 @@ function loadGraphFromChat(options = {}) {
}); });
} }
const cachedSnapshot = readCachedIndexedDbSnapshot(chatId); const preferredLocalStore = getPreferredGraphLocalStorePresentationSync();
const cachedSnapshot = readCachedIndexedDbSnapshot(
chatId,
preferredLocalStore,
);
if (isIndexedDbSnapshotMeaningful(cachedSnapshot)) { if (isIndexedDbSnapshotMeaningful(cachedSnapshot)) {
const cachedStore = resolveSnapshotGraphStorePresentation(
cachedSnapshot,
preferredLocalStore,
);
const cachedResult = applyIndexedDbSnapshotToRuntime( const cachedResult = applyIndexedDbSnapshotToRuntime(
chatId, chatId,
cachedSnapshot, cachedSnapshot,
{ {
source: `${source}:indexeddb-cache`, source: `${source}:indexeddb-cache`,
attemptIndex, attemptIndex,
storagePrimary: cachedStore.storagePrimary,
storageMode: cachedStore.storageMode,
statusLabel: cachedStore.statusLabel,
reasonPrefix: cachedStore.reasonPrefix,
}, },
); );
if (cachedResult?.reason === "indexeddb-stale-runtime") { if (cachedResult?.reason === `${cachedStore.reasonPrefix}-stale-runtime`) {
clearPendingGraphLoadRetry(); clearPendingGraphLoadRetry();
refreshPanelLiveState(); refreshPanelLiveState();
return { return {
@@ -8818,7 +8905,7 @@ async function saveGraphToIndexedDb(
localStore = resolveDbGraphStorePresentation(db); localStore = resolveDbGraphStorePresentation(db);
const currentIdentity = resolveCurrentChatIdentity(getContext()); const currentIdentity = resolveCurrentChatIdentity(getContext());
const baseSnapshot = const baseSnapshot =
readCachedIndexedDbSnapshot(normalizedChatId) || readCachedIndexedDbSnapshot(normalizedChatId, localStore) ||
(await db.exportSnapshot()); (await db.exportSnapshot());
const requestedRevision = resolvePersistRevisionFloor(revision, graph); const requestedRevision = resolvePersistRevisionFloor(revision, graph);
const snapshot = buildSnapshotFromGraph(graph, { const snapshot = buildSnapshotFromGraph(graph, {

View File

@@ -19,6 +19,7 @@ const chatIdsForCleanup = new Set([
"chat-b", "chat-b",
"chat-manager-a", "chat-manager-a",
"chat-manager-b", "chat-manager-b",
"chat-manager-selector",
"chat-replace-reset", "chat-replace-reset",
]); ]);
@@ -418,6 +419,54 @@ async function testChatIsolationAndManager() {
assert.equal(manager.getCurrentChatId(), ""); assert.equal(manager.getCurrentChatId(), "");
} }
async function testManagerRecreatesDbWhenSelectorKeyChanges() {
let selectorKey = "indexeddb:indexeddb";
let instanceCounter = 0;
const closeLog = [];
const manager = new BmeChatManager({
selectorKeyResolver: async () => selectorKey,
databaseFactory: async (chatId) => {
instanceCounter += 1;
const instanceId = instanceCounter;
return {
chatId,
instanceId,
openCount: 0,
closed: false,
async open() {
this.openCount += 1;
return this;
},
async close() {
this.closed = true;
closeLog.push(instanceId);
},
};
},
});
const dbA = await manager.getCurrentDb("chat-manager-selector");
assert.equal(dbA.instanceId, 1);
assert.equal(dbA.openCount, 1);
const reopenedSameSelector = await manager.getCurrentDb("chat-manager-selector");
assert.equal(reopenedSameSelector, dbA);
assert.equal(dbA.openCount, 2);
assert.deepEqual(closeLog, []);
selectorKey = "opfs:opfs-shadow";
const dbB = await manager.getCurrentDb("chat-manager-selector");
assert.notEqual(dbB, dbA);
assert.equal(dbB.instanceId, 2);
assert.equal(dbB.openCount, 1);
assert.equal(dbA.closed, true);
assert.deepEqual(closeLog, [1]);
await manager.closeAll();
assert.equal(dbB.closed, true);
assert.deepEqual(closeLog, [1, 2]);
}
async function testGraphSnapshotConverters() { async function testGraphSnapshotConverters() {
const graph = createEmptyGraph(); const graph = createEmptyGraph();
graph.historyState.chatId = "chat-a"; graph.historyState.chatId = "chat-a";
@@ -548,6 +597,7 @@ async function main() {
await testRevisionMonotonicity(); await testRevisionMonotonicity();
await testTombstonePrune(); await testTombstonePrune();
await testChatIsolationAndManager(); await testChatIsolationAndManager();
await testManagerRecreatesDbWhenSelectorKeyChanges();
await testGraphSnapshotConverters(); await testGraphSnapshotConverters();
await cleanupDatabases(); await cleanupDatabases();