Optimize guest metadata cache persistence
This commit is contained in:
parent
7241ef61d0
commit
535421b8ea
1 changed files with 200 additions and 20 deletions
|
|
@ -22,30 +22,172 @@ import { usePersistentSignal } from '@/hooks/usePersistentSignal';
|
||||||
|
|
||||||
const GUEST_METADATA_STORAGE_KEY = 'pulseGuestMetadata';
|
const GUEST_METADATA_STORAGE_KEY = 'pulseGuestMetadata';
|
||||||
|
|
||||||
const readGuestMetadataCache = (): Record<string, GuestMetadata> => {
|
type GuestMetadataRecord = Record<string, GuestMetadata>;
|
||||||
if (typeof window === 'undefined') return {};
|
type IdleCallbackHandle = number;
|
||||||
|
type IdleCallback = (deadline?: { didTimeout: boolean; timeRemaining: () => number }) => void;
|
||||||
|
type IdleCapableWindow = Window & {
|
||||||
|
requestIdleCallback?: (callback: IdleCallback, options?: { timeout?: number }) => IdleCallbackHandle;
|
||||||
|
cancelIdleCallback?: (handle: IdleCallbackHandle) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
let cachedGuestMetadata: GuestMetadataRecord | null = null;
|
||||||
|
let lastPersistedGuestMetadataJSON: string | null = null;
|
||||||
|
let pendingPersistMetadata: GuestMetadataRecord | null = null;
|
||||||
|
let persistHandle: number | null = null;
|
||||||
|
let persistHandleType: 'idle' | 'timeout' | null = null;
|
||||||
|
|
||||||
|
const instrumentationEnabled = import.meta.env.DEV && typeof performance !== 'undefined';
|
||||||
|
|
||||||
|
const readGuestMetadataCache = (): GuestMetadataRecord => {
|
||||||
|
if (cachedGuestMetadata) {
|
||||||
|
return cachedGuestMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
cachedGuestMetadata = {};
|
||||||
|
return cachedGuestMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const raw = window.localStorage.getItem(GUEST_METADATA_STORAGE_KEY);
|
const raw = window.localStorage.getItem(GUEST_METADATA_STORAGE_KEY);
|
||||||
if (!raw) return {};
|
if (!raw) {
|
||||||
|
cachedGuestMetadata = {};
|
||||||
|
lastPersistedGuestMetadataJSON = null;
|
||||||
|
return cachedGuestMetadata;
|
||||||
|
}
|
||||||
const parsed = JSON.parse(raw);
|
const parsed = JSON.parse(raw);
|
||||||
if (parsed && typeof parsed === 'object') {
|
if (parsed && typeof parsed === 'object') {
|
||||||
return parsed as Record<string, GuestMetadata>;
|
cachedGuestMetadata = parsed as GuestMetadataRecord;
|
||||||
|
lastPersistedGuestMetadataJSON = raw;
|
||||||
|
return cachedGuestMetadata;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('Failed to parse cached guest metadata:', err);
|
console.warn('Failed to parse cached guest metadata:', err);
|
||||||
}
|
}
|
||||||
return {};
|
|
||||||
|
cachedGuestMetadata = {};
|
||||||
|
lastPersistedGuestMetadataJSON = null;
|
||||||
|
return cachedGuestMetadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
const persistGuestMetadataCache = (metadata: Record<string, GuestMetadata>) => {
|
const clearPendingPersistHandle = (idleWindow: IdleCapableWindow) => {
|
||||||
if (typeof window === 'undefined') return;
|
if (persistHandle === null || persistHandleType === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (persistHandleType === 'idle' && idleWindow.cancelIdleCallback) {
|
||||||
|
idleWindow.cancelIdleCallback(persistHandle);
|
||||||
|
} else if (persistHandleType === 'timeout') {
|
||||||
|
window.clearTimeout(persistHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
persistHandle = null;
|
||||||
|
persistHandleType = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const runGuestMetadataPersist = () => {
|
||||||
|
if (typeof window === 'undefined' || !pendingPersistMetadata) {
|
||||||
|
pendingPersistMetadata = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadata = pendingPersistMetadata;
|
||||||
|
pendingPersistMetadata = null;
|
||||||
|
|
||||||
|
const markBase = instrumentationEnabled ? `guest-metadata:persist:${Date.now()}` : null;
|
||||||
|
if (markBase) {
|
||||||
|
performance.mark(`${markBase}:start`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let serialized: string;
|
||||||
try {
|
try {
|
||||||
window.localStorage.setItem(GUEST_METADATA_STORAGE_KEY, JSON.stringify(metadata));
|
serialized = JSON.stringify(metadata);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (markBase) {
|
||||||
|
performance.mark(`${markBase}:end`);
|
||||||
|
performance.measure(markBase, `${markBase}:start`, `${markBase}:end`);
|
||||||
|
performance.clearMarks(`${markBase}:start`);
|
||||||
|
performance.clearMarks(`${markBase}:end`);
|
||||||
|
performance.clearMeasures(markBase);
|
||||||
|
}
|
||||||
|
console.warn('Failed to serialize guest metadata cache:', err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serialized === lastPersistedGuestMetadataJSON) {
|
||||||
|
if (markBase) {
|
||||||
|
performance.mark(`${markBase}:end`);
|
||||||
|
performance.measure(markBase, `${markBase}:start`, `${markBase}:end`);
|
||||||
|
const entries = performance.getEntriesByName(markBase);
|
||||||
|
const entry = entries[entries.length - 1];
|
||||||
|
if (entry) {
|
||||||
|
console.debug(
|
||||||
|
`[guestMetadataCache] skipped persist (unchanged) in ${entry.duration.toFixed(2)}ms`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
performance.clearMarks(`${markBase}:start`);
|
||||||
|
performance.clearMarks(`${markBase}:end`);
|
||||||
|
performance.clearMeasures(markBase);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(GUEST_METADATA_STORAGE_KEY, serialized);
|
||||||
|
lastPersistedGuestMetadataJSON = serialized;
|
||||||
|
if (markBase) {
|
||||||
|
performance.mark(`${markBase}:end`);
|
||||||
|
performance.measure(markBase, `${markBase}:start`, `${markBase}:end`);
|
||||||
|
const entries = performance.getEntriesByName(markBase);
|
||||||
|
const entry = entries[entries.length - 1];
|
||||||
|
if (entry) {
|
||||||
|
console.debug(
|
||||||
|
`[guestMetadataCache] persisted ${Object.keys(metadata).length} entries in ${entry.duration.toFixed(2)}ms`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
performance.clearMarks(`${markBase}:start`);
|
||||||
|
performance.clearMarks(`${markBase}:end`);
|
||||||
|
performance.clearMeasures(markBase);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (markBase) {
|
||||||
|
performance.mark(`${markBase}:end`);
|
||||||
|
performance.measure(markBase, `${markBase}:start`, `${markBase}:end`);
|
||||||
|
performance.clearMarks(`${markBase}:start`);
|
||||||
|
performance.clearMarks(`${markBase}:end`);
|
||||||
|
performance.clearMeasures(markBase);
|
||||||
|
}
|
||||||
console.warn('Failed to persist guest metadata cache:', err);
|
console.warn('Failed to persist guest metadata cache:', err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const queueGuestMetadataPersist = (metadata: GuestMetadataRecord) => {
|
||||||
|
cachedGuestMetadata = metadata;
|
||||||
|
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingPersistMetadata = metadata;
|
||||||
|
const idleWindow = window as IdleCapableWindow;
|
||||||
|
|
||||||
|
clearPendingPersistHandle(idleWindow);
|
||||||
|
|
||||||
|
const schedule: IdleCallback = () => {
|
||||||
|
persistHandle = null;
|
||||||
|
persistHandleType = null;
|
||||||
|
runGuestMetadataPersist();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (idleWindow.requestIdleCallback) {
|
||||||
|
persistHandleType = 'idle';
|
||||||
|
persistHandle = idleWindow.requestIdleCallback(schedule, { timeout: 750 });
|
||||||
|
} else {
|
||||||
|
persistHandleType = 'timeout';
|
||||||
|
persistHandle = window.setTimeout(schedule, 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
interface DashboardProps {
|
interface DashboardProps {
|
||||||
vms: VM[];
|
vms: VM[];
|
||||||
containers: Container[];
|
containers: Container[];
|
||||||
|
|
@ -65,16 +207,17 @@ export function Dashboard(props: DashboardProps) {
|
||||||
const [search, setSearch] = createSignal('');
|
const [search, setSearch] = createSignal('');
|
||||||
const [isSearchLocked, setIsSearchLocked] = createSignal(false);
|
const [isSearchLocked, setIsSearchLocked] = createSignal(false);
|
||||||
const [selectedNode, setSelectedNode] = createSignal<string | null>(null);
|
const [selectedNode, setSelectedNode] = createSignal<string | null>(null);
|
||||||
const [guestMetadata, setGuestMetadata] = createSignal<Record<string, GuestMetadata>>(
|
const [guestMetadata, setGuestMetadata] = createSignal<GuestMetadataRecord>(
|
||||||
readGuestMetadataCache(),
|
readGuestMetadataCache(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateGuestMetadataState = (
|
const updateGuestMetadataState = (updater: (prev: GuestMetadataRecord) => GuestMetadataRecord) =>
|
||||||
updater: (prev: Record<string, GuestMetadata>) => Record<string, GuestMetadata>,
|
|
||||||
) =>
|
|
||||||
setGuestMetadata((prev) => {
|
setGuestMetadata((prev) => {
|
||||||
const next = updater(prev);
|
const next = updater(prev);
|
||||||
persistGuestMetadataCache(next);
|
if (next === prev) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
queueGuestMetadataPersist(next);
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -127,15 +270,52 @@ export function Dashboard(props: DashboardProps) {
|
||||||
|
|
||||||
// Callback to update a guest's custom URL in metadata
|
// Callback to update a guest's custom URL in metadata
|
||||||
const handleCustomUrlUpdate = (guestId: string, url: string) => {
|
const handleCustomUrlUpdate = (guestId: string, url: string) => {
|
||||||
|
const trimmedUrl = url.trim();
|
||||||
|
const nextUrl = trimmedUrl === '' ? undefined : trimmedUrl;
|
||||||
|
const currentUrl = guestMetadata()[guestId]?.customUrl;
|
||||||
|
if (currentUrl === nextUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
updateGuestMetadataState((prev) => {
|
updateGuestMetadataState((prev) => {
|
||||||
const next = {
|
const previousEntry = prev[guestId];
|
||||||
...prev,
|
|
||||||
[guestId]: {
|
if (nextUrl === undefined) {
|
||||||
...(prev[guestId] || { id: guestId }),
|
if (!previousEntry || typeof previousEntry.customUrl === 'undefined') {
|
||||||
customUrl: url || undefined,
|
return prev;
|
||||||
},
|
}
|
||||||
|
const { customUrl: _removed, ...restEntry } = previousEntry;
|
||||||
|
const hasAdditionalMetadata = Object.entries(restEntry).some(
|
||||||
|
([key, value]) => key !== 'id' && value !== undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasAdditionalMetadata) {
|
||||||
|
const { [guestId]: _omit, ...rest } = prev;
|
||||||
|
return rest;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
[guestId]: {
|
||||||
|
...restEntry,
|
||||||
|
id: restEntry.id ?? guestId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previousEntry && previousEntry.customUrl === nextUrl) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextEntry: GuestMetadata = {
|
||||||
|
...(previousEntry || { id: guestId }),
|
||||||
|
customUrl: nextUrl,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
[guestId]: nextEntry,
|
||||||
};
|
};
|
||||||
return next;
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue