Files
ChatApp/apps/desktop/src/lib/attachmentCache.ts
T
byGalax 44088b35d7 perf: bundle splitting, caches, thumbnails, batching, virtualization, release tuning
Route splitting
- React.lazy for AdminPage, SettingsPage, FriendsPage, DevicePage,
  AuthCallbackPage; ChatsPage + ConversationPage stay eager
- RouteSuspense wrapper with spinner fallback

Vendor chunking
- Vite manualChunks splits livekit-client, libsodium, @supabase, react
  into dedicated cacheable chunks

Image thumbnails
- createImageBitmap + OffscreenCanvas downscales inline preview to
  max 640px, emits webp; full blob reserved for the lightbox
- Passes through gif/apng/webp so animation is preserved
- decoding="async" on the inline img

Attachment cache
- lib/attachmentCache.ts backed by OPFS; 7-day TTL
- AttachmentImage/Audio/Video/PDF/Generic read cache first, decrypt on
  miss, write-through on success; graceful no-op when OPFS missing

Avatar cache
- lib/avatarCache.ts — session Map<url, blobUrl> + warmAvatarCache()
  helper for bulk preload

Message batching
- Realtime INSERT burst collapses to a single refresh() when >3 ids
  land within a 250ms window; solo inserts keep the per-id path for
  latency parity

Conversation-list virtualization
- VirtualConversationList with IntersectionObserver sentinel, initial
  40 rows + 40 per batch; no overhead under threshold

Rust release tuning
- Cargo [profile.release]: lto, codegen-units=1, strip=symbols,
  panic=abort, opt-level="s" — ~20-30% smaller binary, faster startup
2026-04-21 10:24:31 +02:00

103 lines
3.1 KiB
TypeScript

// Blob cache for decrypted attachments. Stores the Blob body in OPFS
// (Origin Private File System — always sandbox-scoped to the origin, no
// user prompt) keyed by attachment id. Avoids re-downloading + re-decrypting
// the same blob on every scroll-into-view.
//
// Tauri WebKit + WebView2 both support OPFS as of the versions targeted by
// tauri 2. On unsupported hosts the cache degrades to a no-op and callers
// fall back to the fetch path.
const DIR_NAME = 'attachments';
const DEFAULT_TTL_MS = 7 * 24 * 3600 * 1000;
interface OpfsRoot {
getDirectoryHandle: (
name: string,
opts?: { create?: boolean },
) => Promise<OpfsDir>;
}
interface OpfsDir {
getFileHandle: (
name: string,
opts?: { create?: boolean },
) => Promise<OpfsFile>;
removeEntry: (name: string, opts?: { recursive?: boolean }) => Promise<void>;
entries?: () => AsyncIterableIterator<[string, OpfsFile]>;
}
interface OpfsFile {
getFile: () => Promise<File>;
createWritable: () => Promise<{
write: (data: ArrayBuffer | Blob) => Promise<void>;
close: () => Promise<void>;
}>;
}
let dirPromise: Promise<OpfsDir | null> | null = null;
async function getDir(): Promise<OpfsDir | null> {
if (!dirPromise) {
dirPromise = (async () => {
const storage = (navigator as unknown as { storage?: { getDirectory?: () => Promise<OpfsRoot> } }).storage;
const getDirectory = storage?.getDirectory;
if (!storage || !getDirectory) return null;
try {
const root = await getDirectory.call(storage);
return await root.getDirectoryHandle(DIR_NAME, { create: true });
} catch (err: unknown) {
console.warn('opfs attachment cache init failed', err);
return null;
}
})();
}
return dirPromise;
}
function safeName(id: string): string {
// OPFS file names can't contain slashes; attachment ids are UUIDs so this
// is mostly defensive.
return id.replace(/[^A-Za-z0-9_.-]/g, '_') + '.bin';
}
export async function getCachedAttachment(id: string): Promise<Blob | null> {
const dir = await getDir();
if (!dir) return null;
try {
const handle = await dir.getFileHandle(safeName(id), { create: false });
const file = await handle.getFile();
// Evict stale entries lazily — if older than TTL, drop and miss.
if (Date.now() - file.lastModified > DEFAULT_TTL_MS) {
await dir.removeEntry(safeName(id)).catch(() => undefined);
return null;
}
return file;
} catch {
// File doesn't exist → cache miss.
return null;
}
}
export async function putCachedAttachment(id: string, blob: Blob): Promise<void> {
const dir = await getDir();
if (!dir) return;
try {
const handle = await dir.getFileHandle(safeName(id), { create: true });
const writable = await handle.createWritable();
await writable.write(blob);
await writable.close();
} catch (err: unknown) {
console.warn('putCachedAttachment failed', err);
}
}
export async function evictCachedAttachment(id: string): Promise<void> {
const dir = await getDir();
if (!dir) return;
try {
await dir.removeEntry(safeName(id));
} catch {
/* ignore — probably never existed */
}
}