File size: 5,897 Bytes
dbb1bf9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | export const PANEL_SPANS_KEY = 'worldmonitor-panel-spans';
export const PANEL_COL_SPANS_KEY = 'worldmonitor-panel-col-spans';
export const PANEL_COLLAPSED_KEY = 'worldmonitor-panel-collapsed';
let panelSpansCache: Readonly<Record<string, number>> | null = null;
let panelColSpansCache: Readonly<Record<string, number>> | null = null;
let panelCollapsedCache: Readonly<Record<string, boolean>> | null = null;
let storageInvalidationListenerInstalled = false;
function ensurePanelStorageCacheInvalidationListener(): void {
if (storageInvalidationListenerInstalled) return;
if (typeof window === 'undefined' || typeof window.addEventListener !== 'function') return;
window.addEventListener('storage', (event: StorageEvent) => {
if (event.key === null) {
invalidatePanelStorageCacheForKeys([PANEL_SPANS_KEY, PANEL_COL_SPANS_KEY, PANEL_COLLAPSED_KEY]);
return;
}
invalidatePanelStorageCacheForKeys([event.key]);
});
storageInvalidationListenerInstalled = true;
}
function readStorageMap<T>(key: string): Record<string, T> {
try {
const stored = localStorage.getItem(key);
if (!stored) return {};
const parsed = JSON.parse(stored);
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
? parsed as Record<string, T>
: {};
} catch {
return {};
}
}
function freezeStorageMap<T>(map: Record<string, T>): Readonly<Record<string, T>> {
return Object.freeze(map);
}
function writeStorageMap<T>(key: string, map: Record<string, T>): void {
localStorage.setItem(key, JSON.stringify(map));
}
function removeStorageMap(key: string): void {
localStorage.removeItem(key);
}
export function loadPanelSpans(): Readonly<Record<string, number>> {
ensurePanelStorageCacheInvalidationListener();
panelSpansCache ??= freezeStorageMap(readStorageMap<number>(PANEL_SPANS_KEY));
return panelSpansCache;
}
export function savePanelSpan(panelId: string, span: number): void {
ensurePanelStorageCacheInvalidationListener();
const next = { ...loadPanelSpans(), [panelId]: span };
writeStorageMap(PANEL_SPANS_KEY, next);
panelSpansCache = freezeStorageMap(next);
}
// resetHeight historically persisted an empty aggregate map instead of removing
// the row-span key, so the default keeps that exact behavior for callers.
export function clearPanelSpan(panelId: string, options: { removeWhenEmpty?: boolean } = {}): void {
ensurePanelStorageCacheInvalidationListener();
const spans = loadPanelSpans();
if (!(panelId in spans)) return;
const next = { ...spans };
delete next[panelId];
if (options.removeWhenEmpty && Object.keys(next).length === 0) {
removeStorageMap(PANEL_SPANS_KEY);
panelSpansCache = freezeStorageMap(next);
return;
}
writeStorageMap(PANEL_SPANS_KEY, next);
panelSpansCache = freezeStorageMap(next);
}
export function clearPanelSpans(): void {
ensurePanelStorageCacheInvalidationListener();
removeStorageMap(PANEL_SPANS_KEY);
panelSpansCache = freezeStorageMap({});
}
export function loadPanelColSpans(): Readonly<Record<string, number>> {
ensurePanelStorageCacheInvalidationListener();
panelColSpansCache ??= freezeStorageMap(readStorageMap<number>(PANEL_COL_SPANS_KEY));
return panelColSpansCache;
}
export function savePanelColSpan(panelId: string, span: number): void {
ensurePanelStorageCacheInvalidationListener();
const next = { ...loadPanelColSpans(), [panelId]: span };
writeStorageMap(PANEL_COL_SPANS_KEY, next);
panelColSpansCache = freezeStorageMap(next);
}
// Column-span cleanup historically removed the aggregate key when the last
// entry disappeared, unlike row-span reset. Keep that legacy default explicit.
export function clearPanelColSpan(panelId: string, { removeWhenEmpty = true }: { removeWhenEmpty?: boolean } = {}): void {
ensurePanelStorageCacheInvalidationListener();
const spans = loadPanelColSpans();
if (!(panelId in spans)) return;
const next = { ...spans };
delete next[panelId];
if (removeWhenEmpty && Object.keys(next).length === 0) {
removeStorageMap(PANEL_COL_SPANS_KEY);
panelColSpansCache = freezeStorageMap(next);
return;
}
writeStorageMap(PANEL_COL_SPANS_KEY, next);
panelColSpansCache = freezeStorageMap(next);
}
export function clearPanelColSpans(): void {
ensurePanelStorageCacheInvalidationListener();
removeStorageMap(PANEL_COL_SPANS_KEY);
panelColSpansCache = freezeStorageMap({});
}
export function loadPanelCollapsed(): Readonly<Record<string, boolean>> {
ensurePanelStorageCacheInvalidationListener();
panelCollapsedCache ??= freezeStorageMap(readStorageMap<boolean>(PANEL_COLLAPSED_KEY));
return panelCollapsedCache;
}
export function savePanelCollapsed(panelId: string, collapsed: boolean): void {
ensurePanelStorageCacheInvalidationListener();
const next = { ...loadPanelCollapsed() };
if (collapsed) {
next[panelId] = true;
} else {
delete next[panelId];
}
if (Object.keys(next).length === 0) {
removeStorageMap(PANEL_COLLAPSED_KEY);
} else {
writeStorageMap(PANEL_COLLAPSED_KEY, next);
}
panelCollapsedCache = freezeStorageMap(next);
}
export function clearPanelSpanEntry(panelId: string): void {
try {
clearPanelSpan(panelId, { removeWhenEmpty: true });
} catch {
// Ignore corrupt or unavailable storage, matching the previous cleanup path.
}
}
export function clearPanelColSpanEntry(panelId: string): void {
try {
clearPanelColSpan(panelId, { removeWhenEmpty: true });
} catch {
// Ignore corrupt or unavailable storage, matching the previous cleanup path.
}
}
export function invalidatePanelStorageCacheForKeys(keys: Iterable<string>): void {
for (const key of keys) {
if (key === PANEL_SPANS_KEY) panelSpansCache = null;
else if (key === PANEL_COL_SPANS_KEY) panelColSpansCache = null;
else if (key === PANEL_COLLAPSED_KEY) panelCollapsedCache = null;
}
}
|