- Cache the model catalog once per user and each conversation's setting separately; clear both on logout and after model saves - Compress hashed assets asynchronously with a byte budget and set Vary - Memoize message parsing and throttle selection reads - Debounce draft writes to localStorage - Keep the composer editor on the page and focused, as before - Add a bundle budget check and run it in CI - Show a loading block while sessions load and a workspace avatar in the mobile conversation header
121 lines
3.0 KiB
TypeScript
121 lines
3.0 KiB
TypeScript
import { AppData } from "@/src/data/app-data";
|
|
import type {
|
|
BotHarnessSettingsPayload,
|
|
HarnessCatalogPayload,
|
|
} from "@/src/types";
|
|
|
|
/**
|
|
* Bumped when a cached model payload changes shape, so an older cache is
|
|
* fetched again instead of read.
|
|
*/
|
|
const CacheVersion = 1;
|
|
|
|
type CacheEnvelope<T> = {
|
|
v: number;
|
|
/**
|
|
* Unix ms it was fetched.
|
|
*/
|
|
at: number;
|
|
data: T;
|
|
};
|
|
|
|
type UserKey = {
|
|
user_id: number | string;
|
|
};
|
|
|
|
type SettingsKey = UserKey & {
|
|
bot_id: number | string;
|
|
workspace_id?: number | null;
|
|
};
|
|
|
|
function catalogKey({ user_id }: UserKey) {
|
|
return `${AppData.HarnessCacheStorageKey}:catalog:${user_id}`;
|
|
}
|
|
|
|
function settingsKey({ user_id, bot_id, workspace_id }: SettingsKey) {
|
|
return `${AppData.HarnessCacheStorageKey}:settings:${user_id}:${bot_id}:${workspace_id || 0}`;
|
|
}
|
|
|
|
function read<T>(key: string): CacheEnvelope<T> | null {
|
|
try {
|
|
const cached = JSON.parse(localStorage.getItem(key) || "null");
|
|
|
|
return cached?.v == CacheVersion &&
|
|
typeof cached.at == "number" &&
|
|
Array.isArray(cached.data?.harnesses)
|
|
? cached
|
|
: null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function write<T>(key: string, data: T) {
|
|
try {
|
|
localStorage.setItem(
|
|
key,
|
|
JSON.stringify({ v: CacheVersion, at: Date.now(), data } satisfies CacheEnvelope<T>),
|
|
);
|
|
} catch {}
|
|
}
|
|
|
|
/**
|
|
* The install-wide model catalog this user last fetched, from
|
|
* `localStorage`; null when there is none or it can't be read.
|
|
*/
|
|
export function readHarnessCatalogCache(key: UserKey) {
|
|
return read<HarnessCatalogPayload>(catalogKey(key));
|
|
}
|
|
|
|
export function writeHarnessCatalogCache({
|
|
user_id,
|
|
data,
|
|
}: UserKey & { data: HarnessCatalogPayload }) {
|
|
write(catalogKey({ user_id }), data);
|
|
}
|
|
|
|
/**
|
|
* One conversation's model setting this user last fetched, from
|
|
* `localStorage`; null when there is none or it can't be read.
|
|
*/
|
|
export function readHarnessSettingsCache(key: SettingsKey) {
|
|
return read<BotHarnessSettingsPayload>(settingsKey(key));
|
|
}
|
|
|
|
export function writeHarnessSettingsCache({
|
|
user_id,
|
|
bot_id,
|
|
workspace_id,
|
|
data,
|
|
}: SettingsKey & { data: BotHarnessSettingsPayload }) {
|
|
write(settingsKey({ user_id, bot_id, workspace_id }), data);
|
|
}
|
|
|
|
/**
|
|
* Drops every model cache, including the per-conversation keys an older
|
|
* version wrote. Called on logout and after model settings change
|
|
* elsewhere, so the modal can't show a stale pick.
|
|
*/
|
|
export function clearHarnessCache() {
|
|
const prefixes = [
|
|
AppData.HarnessCacheStorageKey,
|
|
AppData.LegacyHarnessOverviewStorageKey,
|
|
];
|
|
|
|
try {
|
|
const keys: string[] = [];
|
|
|
|
for (let index = 0; index < localStorage.length; index++) {
|
|
const key = localStorage.key(index);
|
|
|
|
if (key) keys.push(key);
|
|
}
|
|
|
|
for (const key of keys) {
|
|
if (prefixes.some((prefix) => key.startsWith(prefix))) {
|
|
localStorage.removeItem(key);
|
|
}
|
|
}
|
|
} catch {}
|
|
}
|