Files
SenMeshNet/infrastructure/persistence/localStorageAdapter.ts
zhaolei 4a02370006
Some checks failed
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
feat: add monitoring settings tab, workbench view, and inspection alert improvements
2026-10-30 12:12:04 +08:00

120 lines
3.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const safeParse = <T>(value: string | null): T | null => {
if (!value) return null;
try {
return JSON.parse(value) as T;
} catch {
return null;
}
};
export const LOCAL_STORAGE_ADAPTER_CHANGED_EVENT = 'netcatty:local-storage-adapter-changed';
// 全局钩子:写后调用。AppSideEffects 注册后用来同步权威源。
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).__NETCATTY_SETTINGS_SYNC__ = null as null | ((key: string, value: string | null) => void);
const pendingChangedKeys = new Set<string>();
let emitChangedKeysTimer: ReturnType<typeof setTimeout> | null = null;
function dispatchLocalStorageAdapterChanged(key: string): void {
try {
const target = globalThis as typeof globalThis & {
dispatchEvent?: (event: Event) => boolean;
CustomEvent?: typeof CustomEvent;
};
if (typeof target.dispatchEvent !== 'function' || typeof target.CustomEvent !== 'function') return;
target.dispatchEvent(new target.CustomEvent<{ key: string }>(
LOCAL_STORAGE_ADAPTER_CHANGED_EVENT,
{ detail: { key } },
));
} catch {
// ignore
}
}
function emitLocalStorageAdapterChanged(key: string): void {
pendingChangedKeys.add(key);
if (emitChangedKeysTimer) return;
// Defer same-window storage notifications so React render-phase writes do
// not synchronously trigger state updates in unrelated components.
emitChangedKeysTimer = setTimeout(() => {
emitChangedKeysTimer = null;
const keys = Array.from(pendingChangedKeys);
pendingChangedKeys.clear();
for (const changedKey of keys) {
dispatchLocalStorageAdapterChanged(changedKey);
}
}, 0);
}
/**
* Safely write to localStorage, catching QuotaExceededError.
* Returns true if the write succeeded, false if storage quota was exceeded.
*/
function safeSetItem(key: string, value: string): boolean {
try {
if (localStorage.getItem(key) === value) {
return true;
}
localStorage.setItem(key, value);
emitLocalStorageAdapterChanged(key);
// 同步到权威源(如果钩子已注册)
try { (globalThis as any).__NETCATTY_SETTINGS_SYNC__?.(key, value); } catch {}
return true;
} catch (err) {
if (
err instanceof DOMException &&
(err.name === 'QuotaExceededError' || err.code === 22)
) {
console.warn(
`[localStorageAdapter] QuotaExceededError writing key "${key}" (${value.length} chars). Data was not persisted.`,
);
return false;
}
throw err; // Re-throw unexpected errors
}
}
export const localStorageAdapter = {
read<T>(key: string): T | null {
return safeParse<T>(localStorage.getItem(key));
},
write<T>(key: string, value: T): boolean {
const json = JSON.stringify(value);
return safeSetItem(key, json);
},
readString(key: string): string | null {
return localStorage.getItem(key);
},
writeString(key: string, value: string): boolean {
return safeSetItem(key, value);
},
readBoolean(key: string): boolean | null {
const value = localStorage.getItem(key);
if (value === null) return null;
if (value === "true") return true;
if (value === "false") return false;
return null;
},
writeBoolean(key: string, value: boolean): boolean {
return safeSetItem(key, value ? "true" : "false");
},
readNumber(key: string): number | null {
const value = localStorage.getItem(key);
if (!value) return null;
const num = parseInt(value, 10);
return isNaN(num) ? null : num;
},
writeNumber(key: string, value: number): boolean {
return safeSetItem(key, String(value));
},
remove(key: string) {
if (localStorage.getItem(key) === null) return;
localStorage.removeItem(key);
emitLocalStorageAdapterChanged(key);
// 同步到权威源(删除)
try { (globalThis as any).__NETCATTY_SETTINGS_SYNC__?.(key, null); } catch {}
},
};