feat(media): implement caching for chat media and update components to use cached media

This commit is contained in:
2026-06-30 15:59:33 +08:00
parent 78d8aae22e
commit ccd2d6bd48
15 changed files with 591 additions and 42 deletions
+1
View File
@@ -5,4 +5,5 @@
export * from "./chat_storage";
export * from "./local_chat_db";
export * from "./local_chat_storage";
export * from "./local_chat_media_storage";
export * from "./local_message";
+21
View File
@@ -32,13 +32,34 @@ export interface LocalMessageRow {
sessionId: string;
}
export type LocalChatMediaKind = "image" | "audio";
export interface LocalChatMediaRow {
cacheKey: string;
ownerKey: string;
messageId: string;
kind: LocalChatMediaKind;
remoteUrl: string;
blob: Blob;
mimeType: string;
byteSize: number;
createdAt: number;
updatedAt: number;
lastAccessedAt: number;
}
export class LocalChatDB extends Dexie {
messages!: Table<LocalMessageRow, number>;
media!: Table<LocalChatMediaRow, string>;
constructor(dbName: string = "cozsweet-chat") {
super(dbName);
this.version(1).stores({
messages: "++dbId",
});
this.version(2).stores({
messages: "++dbId",
media: "cacheKey, ownerKey, messageId, kind, remoteUrl, updatedAt, lastAccessedAt",
});
}
}
@@ -0,0 +1,88 @@
"use client";
import { Result, type Result as ResultT } from "@/utils";
import {
LocalChatDB,
type LocalChatMediaKind,
type LocalChatMediaRow,
} from "./local_chat_db";
export type { LocalChatMediaKind, LocalChatMediaRow };
export interface SaveLocalChatMediaInput {
cacheKey: string;
ownerKey: string;
messageId: string;
kind: LocalChatMediaKind;
remoteUrl: string;
blob: Blob;
mimeType: string;
}
export class LocalChatMediaStorage {
private readonly db: LocalChatDB;
private static _instance: LocalChatMediaStorage | null = null;
constructor(db?: LocalChatDB) {
this.db = db ?? new LocalChatDB();
}
static getInstance(): LocalChatMediaStorage {
if (!LocalChatMediaStorage._instance) {
LocalChatMediaStorage._instance = new LocalChatMediaStorage();
}
return LocalChatMediaStorage._instance;
}
/** @internal */
static _resetForTests(): void {
LocalChatMediaStorage._instance = null;
}
async getMedia(cacheKey: string): Promise<ResultT<LocalChatMediaRow | null>> {
try {
const row = (await this.db.media.get(cacheKey)) ?? null;
if (row) {
await this.db.media.update(cacheKey, {
lastAccessedAt: Date.now(),
});
}
return Result.ok(row);
} catch (e) {
return Result.err(e);
}
}
async saveMedia(input: SaveLocalChatMediaInput): Promise<ResultT<void>> {
try {
const now = Date.now();
const existing = await this.db.media.get(input.cacheKey);
await this.db.media.put({
cacheKey: input.cacheKey,
ownerKey: input.ownerKey,
messageId: input.messageId,
kind: input.kind,
remoteUrl: input.remoteUrl,
blob: input.blob,
mimeType: input.mimeType,
byteSize: input.blob.size,
createdAt: existing?.createdAt ?? now,
updatedAt: now,
lastAccessedAt: now,
});
return Result.ok(undefined);
} catch (e) {
return Result.err(e);
}
}
async clearAll(): Promise<ResultT<void>> {
try {
await this.db.media.clear();
return Result.ok(undefined);
} catch (e) {
return Result.err(e);
}
}
}