mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
Harden local store cache invalidation for storage mode switching
This commit is contained in:
99
index.js
99
index.js
@@ -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, {
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user