MAESTRO: add analysis cache hook

This commit is contained in:
Mariusz Banach
2026-02-18 03:41:42 +01:00
parent 07ee139a00
commit c039a8a432
5 changed files with 227 additions and 3 deletions

View File

@@ -0,0 +1,171 @@
import { useCallback, useState } from "react";
import type { AnalysisConfig, AnalysisReport } from "../types/analysis";
export interface CachedAnalysisPayload {
headers: string;
config: AnalysisConfig;
result: AnalysisReport;
}
export interface CachedAnalysisData extends CachedAnalysisPayload {
timestamp: number;
}
export interface UseAnalysisCacheState {
save: (payload: CachedAnalysisPayload) => void;
load: () => CachedAnalysisData | null;
clear: () => void;
hasCachedData: () => boolean;
isNearLimit: boolean;
}
const STORAGE_LIMIT_BYTES = 5 * 1024 * 1024;
const WARNING_THRESHOLD_RATIO = 0.9;
const HEADER_KEY = "wha:headers";
const CONFIG_KEY = "wha:config";
const RESULT_KEY = "wha:result";
const TIMESTAMP_KEY = "wha:timestamp";
const canUseStorage = (): boolean => {
if (typeof window === "undefined") {
return false;
}
if (!window.localStorage) {
return false;
}
try {
const probeKey = "wha:probe";
window.localStorage.setItem(probeKey, probeKey);
window.localStorage.removeItem(probeKey);
return true;
} catch {
return false;
}
};
const estimateBytes = (value: string): number => {
if (typeof TextEncoder !== "undefined") {
return new TextEncoder().encode(value).length;
}
return value.length;
};
const estimateStorageSize = (): number => {
if (!canUseStorage()) {
return 0;
}
let total = 0;
for (let index = 0; index < localStorage.length; index += 1) {
const key = localStorage.key(index);
if (!key) {
continue;
}
const value = localStorage.getItem(key) ?? "";
total += estimateBytes(key);
total += estimateBytes(value);
}
return total;
};
const parseJson = <T>(value: string | null): T | null => {
if (!value) {
return null;
}
try {
return JSON.parse(value) as T;
} catch {
return null;
}
};
const useAnalysisCache = (): UseAnalysisCacheState => {
const [isNearLimit, setIsNearLimit] = useState(false);
const save = useCallback((payload: CachedAnalysisPayload) => {
if (!canUseStorage()) {
setIsNearLimit(false);
return;
}
const timestamp = Date.now();
localStorage.setItem(HEADER_KEY, payload.headers);
localStorage.setItem(CONFIG_KEY, JSON.stringify(payload.config));
localStorage.setItem(RESULT_KEY, JSON.stringify(payload.result));
localStorage.setItem(TIMESTAMP_KEY, String(timestamp));
const usedBytes = estimateStorageSize();
const warningThreshold = STORAGE_LIMIT_BYTES * WARNING_THRESHOLD_RATIO;
setIsNearLimit(usedBytes >= warningThreshold);
}, []);
const load = useCallback((): CachedAnalysisData | null => {
if (!canUseStorage()) {
return null;
}
const headers = localStorage.getItem(HEADER_KEY);
const config = parseJson<AnalysisConfig>(localStorage.getItem(CONFIG_KEY));
const result = parseJson<AnalysisReport>(localStorage.getItem(RESULT_KEY));
const timestampValue = localStorage.getItem(TIMESTAMP_KEY);
if (!headers || !config || !result || !timestampValue) {
return null;
}
const timestamp = Number(timestampValue);
if (Number.isNaN(timestamp)) {
return null;
}
return {
headers,
config,
result,
timestamp,
};
}, []);
const clear = useCallback(() => {
if (!canUseStorage()) {
setIsNearLimit(false);
return;
}
localStorage.removeItem(HEADER_KEY);
localStorage.removeItem(CONFIG_KEY);
localStorage.removeItem(RESULT_KEY);
localStorage.removeItem(TIMESTAMP_KEY);
setIsNearLimit(false);
}, []);
const hasCachedData = useCallback((): boolean => {
if (!canUseStorage()) {
return false;
}
return Boolean(
localStorage.getItem(HEADER_KEY) &&
localStorage.getItem(CONFIG_KEY) &&
localStorage.getItem(RESULT_KEY) &&
localStorage.getItem(TIMESTAMP_KEY),
);
}, []);
return {
save,
load,
clear,
hasCachedData,
isNearLimit,
};
};
export default useAnalysisCache;