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();
}
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) {
if (!forceRefresh && bmeLocalStoreCapabilitySnapshot.checked) {
return bmeLocalStoreCapabilitySnapshot;
@@ -3787,6 +3822,10 @@ function ensureBmeChatManager() {
bmeChatManager = new BmeChatManager({
databaseFactory: async (chatId) =>
await createPreferredGraphLocalStore(chatId),
selectorKeyResolver: async () =>
buildGraphLocalStoreSelectorKey(
await resolvePreferredGraphLocalStorePresentation(),
),
});
}
return bmeChatManager;
@@ -3916,19 +3955,33 @@ function isIndexedDbSnapshotMeaningful(snapshot = null) {
function cacheIndexedDbSnapshot(chatId, snapshot = null) {
const normalizedChatId = normalizeChatIdCandidate(chatId);
if (!normalizedChatId || !snapshot || typeof snapshot !== "object") return;
const snapshotStore = resolveSnapshotGraphStorePresentation(snapshot);
bmeIndexedDbSnapshotCacheByChatId.set(normalizedChatId, {
chatId: normalizedChatId,
revision: normalizeIndexedDbRevision(snapshot?.meta?.revision),
selectorKey: buildGraphLocalStoreSelectorKey(snapshotStore),
snapshot,
updatedAt: Date.now(),
});
}
function readCachedIndexedDbSnapshot(chatId) {
function readCachedIndexedDbSnapshot(chatId, expectedStore = null) {
const normalizedChatId = normalizeChatIdCandidate(chatId);
if (!normalizedChatId) return null;
const cacheEntry = bmeIndexedDbSnapshotCacheByChatId.get(normalizedChatId);
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;
}
@@ -7400,11 +7453,15 @@ function syncGraphLoadFromLiveContext(options = {}) {
});
}
const cachedSnapshot = readCachedIndexedDbSnapshot(chatId);
const cachedPreferredLocalStore = getPreferredGraphLocalStorePresentationSync();
const cachedSnapshot = readCachedIndexedDbSnapshot(
chatId,
cachedPreferredLocalStore,
);
if (isIndexedDbSnapshotMeaningful(cachedSnapshot)) {
const cachedStore = resolveSnapshotGraphStorePresentation(
cachedSnapshot,
getPreferredGraphLocalStorePresentationSync(),
cachedPreferredLocalStore,
);
const result = applyIndexedDbSnapshotToRuntime(chatId, cachedSnapshot, {
source: `${source}:indexeddb-cache`,
@@ -8232,6 +8289,9 @@ function updateModuleSettings(patch = {}) {
const previousCloudStorageMode = String(
settings.cloudStorageMode || "automatic",
);
const previousGraphLocalStorageMode = getRequestedGraphLocalStorageMode(
settings,
);
Object.assign(settings, patch);
extension_settings[MODULE_NAME] = settings;
globalThis.__stBmeDebugLoggingEnabled = Boolean(
@@ -8298,6 +8358,21 @@ function updateModuleSettings(patch = {}) {
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(
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)) {
const cachedStore = resolveSnapshotGraphStorePresentation(
cachedSnapshot,
preferredLocalStore,
);
const cachedResult = applyIndexedDbSnapshotToRuntime(
chatId,
cachedSnapshot,
{
source: `${source}:indexeddb-cache`,
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();
refreshPanelLiveState();
return {
@@ -8818,7 +8905,7 @@ async function saveGraphToIndexedDb(
localStore = resolveDbGraphStorePresentation(db);
const currentIdentity = resolveCurrentChatIdentity(getContext());
const baseSnapshot =
readCachedIndexedDbSnapshot(normalizedChatId) ||
readCachedIndexedDbSnapshot(normalizedChatId, localStore) ||
(await db.exportSnapshot());
const requestedRevision = resolvePersistRevisionFloor(revision, graph);
const snapshot = buildSnapshotFromGraph(graph, {

View File

@@ -19,6 +19,7 @@ const chatIdsForCleanup = new Set([
"chat-b",
"chat-manager-a",
"chat-manager-b",
"chat-manager-selector",
"chat-replace-reset",
]);
@@ -418,6 +419,54 @@ async function testChatIsolationAndManager() {
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() {
const graph = createEmptyGraph();
graph.historyState.chatId = "chat-a";
@@ -548,6 +597,7 @@ async function main() {
await testRevisionMonotonicity();
await testTombstonePrune();
await testChatIsolationAndManager();
await testManagerRecreatesDbWhenSelectorKeyChanges();
await testGraphSnapshotConverters();
await cleanupDatabases();