mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
fix: harden opfs capability recovery
This commit is contained in:
108
index.js
108
index.js
@@ -1308,6 +1308,7 @@ let bmeLocalStoreCapabilitySnapshot = {
|
||||
reason: "unprobed",
|
||||
};
|
||||
let bmeLocalStoreCapabilityWarningShown = false;
|
||||
const BME_LOCAL_STORE_CAPABILITY_FAILURE_RETRY_MS = 4000;
|
||||
const bmeIndexedDbSnapshotCacheByChatId = new Map();
|
||||
const bmeIndexedDbLoadInFlightByChatId = new Map();
|
||||
const bmeIndexedDbWriteInFlightByChatId = new Map();
|
||||
@@ -4425,23 +4426,65 @@ function isCachedIndexedDbSnapshotCompatible(snapshot = null, expectedStore = nu
|
||||
}
|
||||
|
||||
async function getGraphLocalStoreCapability(forceRefresh = false) {
|
||||
if (!forceRefresh && bmeLocalStoreCapabilitySnapshot.checked) {
|
||||
const settings =
|
||||
arguments.length > 1 && arguments[1] && typeof arguments[1] === "object"
|
||||
? arguments[1].settings || getSettings()
|
||||
: getSettings();
|
||||
const eagerRetry =
|
||||
arguments.length > 1 &&
|
||||
arguments[1] &&
|
||||
typeof arguments[1] === "object" &&
|
||||
arguments[1].eagerRetry === true;
|
||||
const requestedMode = getRequestedGraphLocalStorageMode(settings);
|
||||
const usesOpfsPreference =
|
||||
requestedMode === "auto" || isGraphLocalStorageModeOpfs(requestedMode);
|
||||
const capabilityReason = String(
|
||||
bmeLocalStoreCapabilitySnapshot?.reason || "",
|
||||
).trim();
|
||||
const capabilityFailureStable =
|
||||
capabilityReason === "missing-directory-handle" ||
|
||||
capabilityReason === "OPFS 不可用" ||
|
||||
/not.?supported/i.test(capabilityReason) ||
|
||||
/missing.+getdirectory/i.test(capabilityReason);
|
||||
const capabilityFailureRetryable =
|
||||
usesOpfsPreference &&
|
||||
bmeLocalStoreCapabilitySnapshot.checked === true &&
|
||||
bmeLocalStoreCapabilitySnapshot.opfsAvailable !== true &&
|
||||
capabilityFailureStable !== true;
|
||||
const capabilityFailureAgeMs = Math.max(
|
||||
0,
|
||||
Date.now() - Number(bmeLocalStoreCapabilitySnapshot?.checkedAt || 0),
|
||||
);
|
||||
const shouldRetryFailedProbe =
|
||||
forceRefresh !== true &&
|
||||
capabilityFailureRetryable &&
|
||||
(eagerRetry === true ||
|
||||
capabilityFailureAgeMs >= BME_LOCAL_STORE_CAPABILITY_FAILURE_RETRY_MS);
|
||||
|
||||
if (
|
||||
!forceRefresh &&
|
||||
!shouldRetryFailedProbe &&
|
||||
bmeLocalStoreCapabilitySnapshot.checked
|
||||
) {
|
||||
return bmeLocalStoreCapabilitySnapshot;
|
||||
}
|
||||
if (!forceRefresh && bmeLocalStoreCapabilityPromise) {
|
||||
if (!forceRefresh && !shouldRetryFailedProbe && bmeLocalStoreCapabilityPromise) {
|
||||
return await bmeLocalStoreCapabilityPromise;
|
||||
}
|
||||
|
||||
bmeLocalStoreCapabilityPromise = detectOpfsSupport()
|
||||
.then((result) => {
|
||||
bmeLocalStoreCapabilitySnapshot = {
|
||||
checked: true,
|
||||
checkedAt: Date.now(),
|
||||
opfsAvailable: Boolean(result?.available),
|
||||
reason: String(result?.reason || (result?.available ? "ok" : "unavailable")),
|
||||
};
|
||||
return bmeLocalStoreCapabilitySnapshot;
|
||||
})
|
||||
.then((result) => {
|
||||
bmeLocalStoreCapabilitySnapshot = {
|
||||
checked: true,
|
||||
checkedAt: Date.now(),
|
||||
opfsAvailable: Boolean(result?.available),
|
||||
reason: String(result?.reason || (result?.available ? "ok" : "unavailable")),
|
||||
};
|
||||
if (bmeLocalStoreCapabilitySnapshot.opfsAvailable) {
|
||||
bmeLocalStoreCapabilityWarningShown = false;
|
||||
}
|
||||
return bmeLocalStoreCapabilitySnapshot;
|
||||
})
|
||||
.catch((error) => {
|
||||
bmeLocalStoreCapabilitySnapshot = {
|
||||
checked: true,
|
||||
@@ -4480,7 +4523,9 @@ function getPreferredGraphLocalStorePresentationSync(settings = getSettings()) {
|
||||
) {
|
||||
const requestedMode = getRequestedGraphLocalStorageMode(settings);
|
||||
if (requestedMode === "auto") {
|
||||
const capability = await getGraphLocalStoreCapability();
|
||||
const capability = await getGraphLocalStoreCapability(false, {
|
||||
settings,
|
||||
});
|
||||
return capability.opfsAvailable
|
||||
? buildOpfsStorePresentation(BME_GRAPH_LOCAL_STORAGE_MODE_OPFS_PRIMARY)
|
||||
: buildIndexedDbStorePresentation();
|
||||
@@ -4489,7 +4534,9 @@ function getPreferredGraphLocalStorePresentationSync(settings = getSettings()) {
|
||||
return buildIndexedDbStorePresentation();
|
||||
}
|
||||
|
||||
const capability = await getGraphLocalStoreCapability();
|
||||
const capability = await getGraphLocalStoreCapability(false, {
|
||||
settings,
|
||||
});
|
||||
if (capability.opfsAvailable) {
|
||||
return buildOpfsStorePresentation(requestedMode);
|
||||
}
|
||||
@@ -4536,7 +4583,10 @@ async function refreshCurrentChatLocalStoreBinding(
|
||||
isGraphLocalStorageModeOpfs(requestedMode);
|
||||
|
||||
if (shouldProbeCapability) {
|
||||
await getGraphLocalStoreCapability(forceCapabilityRefresh === true);
|
||||
await getGraphLocalStoreCapability(forceCapabilityRefresh === true, {
|
||||
settings,
|
||||
eagerRetry: forceCapabilityRefresh === true,
|
||||
});
|
||||
}
|
||||
|
||||
const preferredLocalStore =
|
||||
@@ -4643,6 +4693,21 @@ function buildPanelOpenLocalStoreRefreshPlan(
|
||||
const resolvedIsOpfs = resolvedLocalStoreKey.startsWith("opfs:");
|
||||
const preferredIsOpfs = preferredLocalStore.storagePrimary === "opfs";
|
||||
const capabilityUnchecked = bmeLocalStoreCapabilitySnapshot.checked !== true;
|
||||
const capabilityRetryRecommended =
|
||||
usesOpfsPreference &&
|
||||
bmeLocalStoreCapabilitySnapshot.checked === true &&
|
||||
bmeLocalStoreCapabilitySnapshot.opfsAvailable !== true &&
|
||||
!(
|
||||
String(bmeLocalStoreCapabilitySnapshot.reason || "") ===
|
||||
"missing-directory-handle" ||
|
||||
String(bmeLocalStoreCapabilitySnapshot.reason || "") === "OPFS 不可用" ||
|
||||
/not.?supported/i.test(
|
||||
String(bmeLocalStoreCapabilitySnapshot.reason || ""),
|
||||
) ||
|
||||
/missing.+getdirectory/i.test(
|
||||
String(bmeLocalStoreCapabilitySnapshot.reason || ""),
|
||||
)
|
||||
);
|
||||
const pendingPersist = graphPersistenceState.pendingPersist === true;
|
||||
const writesBlocked = graphPersistenceState.writesBlocked === true;
|
||||
const loadState = String(graphPersistenceState.loadState || "");
|
||||
@@ -4658,6 +4723,7 @@ function buildPanelOpenLocalStoreRefreshPlan(
|
||||
const shouldRefresh =
|
||||
usesOpfsPreference &&
|
||||
(capabilityUnchecked ||
|
||||
capabilityRetryRecommended ||
|
||||
pendingPersist ||
|
||||
writesBlocked ||
|
||||
blocked ||
|
||||
@@ -4666,6 +4732,7 @@ function buildPanelOpenLocalStoreRefreshPlan(
|
||||
localStoreMismatch);
|
||||
const forceCapabilityRefresh =
|
||||
capabilityUnchecked ||
|
||||
capabilityRetryRecommended ||
|
||||
pendingPersist ||
|
||||
blocked ||
|
||||
loadingWithoutDb ||
|
||||
@@ -4676,6 +4743,7 @@ function buildPanelOpenLocalStoreRefreshPlan(
|
||||
(pendingPersist || writesBlocked || blocked || Boolean(persistError) || localStoreMismatch);
|
||||
const reasons = [];
|
||||
if (capabilityUnchecked) reasons.push("capability-unchecked");
|
||||
if (capabilityRetryRecommended) reasons.push("capability-retryable-failure");
|
||||
if (pendingPersist) reasons.push("pending-persist");
|
||||
if (writesBlocked) reasons.push("writes-blocked");
|
||||
if (blocked) reasons.push("load-blocked");
|
||||
@@ -5715,6 +5783,18 @@ async function syncBmeChatManagerWithCurrentChat(
|
||||
source = "unknown",
|
||||
context = getContext(),
|
||||
) {
|
||||
const currentSettings = getSettings();
|
||||
const requestedMode = getRequestedGraphLocalStorageMode(currentSettings);
|
||||
if (
|
||||
requestedMode === "auto" ||
|
||||
isGraphLocalStorageModeOpfs(requestedMode)
|
||||
) {
|
||||
await getGraphLocalStoreCapability(false, {
|
||||
settings: currentSettings,
|
||||
eagerRetry: true,
|
||||
});
|
||||
}
|
||||
|
||||
const manager = ensureBmeChatManager();
|
||||
if (!manager) {
|
||||
return {
|
||||
|
||||
@@ -321,12 +321,52 @@ function isNotFoundError(error) {
|
||||
return name === "NotFoundError" || /not.?found/i.test(message);
|
||||
}
|
||||
|
||||
function isTypeMismatchError(error) {
|
||||
const name = String(error?.name || "");
|
||||
const message = String(error?.message || "");
|
||||
return (
|
||||
name === "TypeMismatchError" ||
|
||||
/type.?mismatch/i.test(message) ||
|
||||
/different file type/i.test(message)
|
||||
);
|
||||
}
|
||||
|
||||
async function ensureDirectoryHandle(parentHandle, name) {
|
||||
return await parentHandle.getDirectoryHandle(String(name || ""), {
|
||||
create: true,
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureOpfsRootDirectory(
|
||||
rootDirectory,
|
||||
{ repairFileConflict = false } = {},
|
||||
) {
|
||||
if (!rootDirectory || typeof rootDirectory.getDirectoryHandle !== "function") {
|
||||
throw new Error("OPFS 根目录不可用");
|
||||
}
|
||||
|
||||
try {
|
||||
return await ensureDirectoryHandle(rootDirectory, OPFS_ROOT_DIRECTORY_NAME);
|
||||
} catch (error) {
|
||||
if (!repairFileConflict || !isTypeMismatchError(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const conflictingFile = await maybeGetFileHandle(
|
||||
rootDirectory,
|
||||
OPFS_ROOT_DIRECTORY_NAME,
|
||||
).catch(() => null);
|
||||
if (!conflictingFile || typeof rootDirectory.removeEntry !== "function") {
|
||||
throw error;
|
||||
}
|
||||
|
||||
await rootDirectory.removeEntry(OPFS_ROOT_DIRECTORY_NAME, {
|
||||
recursive: false,
|
||||
});
|
||||
return await ensureDirectoryHandle(rootDirectory, OPFS_ROOT_DIRECTORY_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
async function maybeGetFileHandle(parentHandle, name) {
|
||||
try {
|
||||
return await parentHandle.getFileHandle(String(name || ""), {
|
||||
@@ -598,7 +638,9 @@ export async function detectOpfsSupport(options = {}) {
|
||||
reason: "missing-directory-handle",
|
||||
};
|
||||
}
|
||||
await ensureDirectoryHandle(rootDirectory, OPFS_ROOT_DIRECTORY_NAME);
|
||||
await ensureOpfsRootDirectory(rootDirectory, {
|
||||
repairFileConflict: true,
|
||||
});
|
||||
return {
|
||||
available: true,
|
||||
reason: "ok",
|
||||
@@ -1534,10 +1576,9 @@ class LegacyOpfsGraphStore {
|
||||
if (!rootDirectory || typeof rootDirectory.getDirectoryHandle !== "function") {
|
||||
throw new Error("OPFS 根目录不可用");
|
||||
}
|
||||
const opfsRoot = await ensureDirectoryHandle(
|
||||
rootDirectory,
|
||||
OPFS_ROOT_DIRECTORY_NAME,
|
||||
);
|
||||
const opfsRoot = await ensureOpfsRootDirectory(rootDirectory, {
|
||||
repairFileConflict: true,
|
||||
});
|
||||
const chatsDirectory = await ensureDirectoryHandle(
|
||||
opfsRoot,
|
||||
OPFS_CHATS_DIRECTORY_NAME,
|
||||
@@ -2810,10 +2851,9 @@ export class OpfsGraphStore {
|
||||
if (!rootDirectory || typeof rootDirectory.getDirectoryHandle !== "function") {
|
||||
throw new Error("OPFS 根目录不可用");
|
||||
}
|
||||
const opfsRoot = await ensureDirectoryHandle(
|
||||
rootDirectory,
|
||||
OPFS_ROOT_DIRECTORY_NAME,
|
||||
);
|
||||
const opfsRoot = await ensureOpfsRootDirectory(rootDirectory, {
|
||||
repairFileConflict: true,
|
||||
});
|
||||
const chatsDirectory = await ensureDirectoryHandle(
|
||||
opfsRoot,
|
||||
OPFS_CHATS_DIRECTORY_NAME,
|
||||
|
||||
@@ -2186,6 +2186,47 @@ result = {
|
||||
assert.equal(plan.reasons.includes("resolved-store-mismatch"), true);
|
||||
}
|
||||
|
||||
{
|
||||
const harness = await createGraphPersistenceHarness({
|
||||
chatId: "chat-panel-open-capability-retry",
|
||||
globalChatId: "chat-panel-open-capability-retry",
|
||||
chatMetadata: {
|
||||
integrity: "chat-panel-open-capability-retry-integrity",
|
||||
},
|
||||
});
|
||||
harness.runtimeContext.extension_settings[MODULE_NAME] = {
|
||||
graphLocalStorageMode: "auto",
|
||||
};
|
||||
harness.api.setLocalStoreCapabilitySnapshot({
|
||||
checked: true,
|
||||
checkedAt: Date.now(),
|
||||
opfsAvailable: false,
|
||||
reason: "UnknownError: transient-opfs-init-failure",
|
||||
});
|
||||
harness.api.setGraphPersistenceState({
|
||||
loadState: "loaded",
|
||||
chatId: "chat-panel-open-capability-retry",
|
||||
reason: "healthy",
|
||||
dbReady: true,
|
||||
writesBlocked: false,
|
||||
pendingPersist: false,
|
||||
indexedDbLastError: "",
|
||||
resolvedLocalStore: "indexeddb:indexeddb",
|
||||
storagePrimary: "indexeddb",
|
||||
storageMode: "indexeddb",
|
||||
});
|
||||
|
||||
const plan = harness.api.buildPanelOpenLocalStoreRefreshPlan();
|
||||
|
||||
assert.equal(plan.shouldRefresh, true);
|
||||
assert.equal(plan.forceCapabilityRefresh, true);
|
||||
assert.equal(
|
||||
plan.reasons.includes("capability-retryable-failure"),
|
||||
true,
|
||||
"可恢复的 OPFS 探测失败应在面板打开时触发重新探测",
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
const harness = await createGraphPersistenceHarness({
|
||||
chatId: "chat-luker-panel-open",
|
||||
|
||||
@@ -23,6 +23,12 @@ function createNotFoundError(message) {
|
||||
return error;
|
||||
}
|
||||
|
||||
function createTypeMismatchError(message) {
|
||||
const error = new Error(String(message || "Type mismatch"));
|
||||
error.name = "TypeMismatchError";
|
||||
return error;
|
||||
}
|
||||
|
||||
class MemoryOpfsFileHandle {
|
||||
constructor(parent, name) {
|
||||
this.parent = parent;
|
||||
@@ -71,6 +77,11 @@ class MemoryOpfsDirectoryHandle {
|
||||
|
||||
async getDirectoryHandle(name, options = {}) {
|
||||
const normalizedName = String(name || "");
|
||||
if (this.files.has(normalizedName)) {
|
||||
throw createTypeMismatchError(
|
||||
`A file already exists for directory: ${normalizedName}`,
|
||||
);
|
||||
}
|
||||
let directory = this.directories.get(normalizedName) || null;
|
||||
if (!directory) {
|
||||
if (!options.create) {
|
||||
@@ -84,6 +95,11 @@ class MemoryOpfsDirectoryHandle {
|
||||
|
||||
async getFileHandle(name, options = {}) {
|
||||
const normalizedName = String(name || "");
|
||||
if (this.directories.has(normalizedName)) {
|
||||
throw createTypeMismatchError(
|
||||
`A directory already exists for file: ${normalizedName}`,
|
||||
);
|
||||
}
|
||||
if (!this.files.has(normalizedName)) {
|
||||
if (!options.create) {
|
||||
throw createNotFoundError(`File not found: ${normalizedName}`);
|
||||
@@ -177,6 +193,16 @@ async function testDetectOpfsSupport() {
|
||||
assert.equal(supported.available, true);
|
||||
assert.equal(supported.reason, "ok");
|
||||
|
||||
const conflictedRootDirectory = createMemoryOpfsRoot();
|
||||
conflictedRootDirectory.files.set("st-bme", "legacy-conflict");
|
||||
const repairedConflict = await detectOpfsSupport({
|
||||
rootDirectoryFactory: async () => conflictedRootDirectory,
|
||||
});
|
||||
assert.equal(repairedConflict.available, true);
|
||||
assert.equal(repairedConflict.reason, "ok");
|
||||
assert.equal(conflictedRootDirectory.files.has("st-bme"), false);
|
||||
assert.ok(conflictedRootDirectory.directories.has("st-bme"));
|
||||
|
||||
const missingHandle = await detectOpfsSupport({
|
||||
rootDirectoryFactory: async () => ({}),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user