refactor(chat): tighten media cache boundaries

This commit is contained in:
2026-06-30 16:12:02 +08:00
parent ccd2d6bd48
commit 9cff16e2d9
17 changed files with 388 additions and 146 deletions
+19 -92
View File
@@ -22,10 +22,18 @@ import {
LocalChatMediaStorage,
LocalChatStorage,
LocalMessage,
type LocalChatMediaKind,
type LocalChatMediaRow,
} from "@/data/storage/chat";
import {
buildChatMediaCacheKey,
fallbackChatMediaMimeType,
getMessageMediaTargets,
getSendResponseMediaTargets,
isCacheableRemoteChatMediaUrl,
uniqueMediaTargets,
} from "./chat_media_cache_helpers";
import { createLazySingleton } from "./lazy_singleton";
import { requestBrowserPersistentStorageOnce } from "@/lib/browser/persistent_storage";
const log = new Logger("DataRepositoriesChatRepository");
@@ -180,7 +188,7 @@ export class ChatRepository implements IChatRepository {
input: CacheRemoteChatMediaInput,
): Promise<Result<LocalChatMediaRow>> {
return Result.wrap(async () => {
if (!isCacheableRemoteUrl(input.remoteUrl)) {
if (!isCacheableRemoteChatMediaUrl(input.remoteUrl)) {
throw new Error(`Unsupported media url: ${input.remoteUrl}`);
}
@@ -217,10 +225,12 @@ export class ChatRepository implements IChatRepository {
remoteUrl: input.remoteUrl,
blob,
mimeType:
blob.type || response.headers.get("content-type") || fallbackMimeType(input.kind),
blob.type ||
response.headers.get("content-type") ||
fallbackChatMediaMimeType(input.kind),
});
if (Result.isErr(saveResult)) throw saveResult.error;
void requestPersistentStorage();
void requestBrowserPersistentStorageOnce();
const savedResult = await this.mediaStorage.getMedia(cacheKey);
if (Result.isErr(savedResult)) throw savedResult.error;
@@ -333,7 +343,11 @@ export class ChatRepository implements IChatRepository {
ownerKey: string,
input: ChatMediaLookupInput,
): string {
return `${ownerKey}:${input.messageId}:${input.kind}`;
return buildChatMediaCacheKey({
ownerKey,
messageId: input.messageId,
kind: input.kind,
});
}
}
@@ -346,90 +360,3 @@ export const getChatRepository = createLazySingleton<IChatRepository>(
LocalChatMediaStorage.getInstance(),
),
);
function getMessageMediaTargets(
message: ChatMessage,
): CacheRemoteChatMediaInput[] {
const targets: CacheRemoteChatMediaInput[] = [];
pushMediaTarget(targets, {
messageId: message.id,
kind: "image",
remoteUrl: message.image.url,
});
pushMediaTarget(targets, {
messageId: message.id,
kind: "audio",
remoteUrl: message.audioUrl,
});
return targets;
}
function getSendResponseMediaTargets(
response: ChatSendResponse,
): CacheRemoteChatMediaInput[] {
const targets: CacheRemoteChatMediaInput[] = [];
pushMediaTarget(targets, {
messageId: response.messageId,
kind: "image",
remoteUrl: response.image.url,
});
pushMediaTarget(targets, {
messageId: response.messageId,
kind: "audio",
remoteUrl: response.audioUrl,
});
return targets;
}
function pushMediaTarget(
targets: CacheRemoteChatMediaInput[],
input: {
messageId: string;
kind: LocalChatMediaKind;
remoteUrl: string | null;
},
): void {
if (input.messageId.length === 0) return;
if (!input.remoteUrl || !isCacheableRemoteUrl(input.remoteUrl)) return;
targets.push({
messageId: input.messageId,
kind: input.kind,
remoteUrl: input.remoteUrl,
});
}
function isCacheableRemoteUrl(url: string): boolean {
return url.startsWith("http://") || url.startsWith("https://");
}
function fallbackMimeType(kind: LocalChatMediaKind): string {
return kind === "image" ? "image/*" : "audio/mpeg";
}
function uniqueMediaTargets(
targets: readonly CacheRemoteChatMediaInput[],
): CacheRemoteChatMediaInput[] {
const seen = new Set<string>();
return targets.filter((target) => {
const key = `${target.messageId}:${target.kind}:${target.remoteUrl}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}
let storagePersistenceRequested = false;
async function requestPersistentStorage(): Promise<void> {
if (storagePersistenceRequested) return;
storagePersistenceRequested = true;
if (typeof navigator === "undefined") return;
if (!navigator.storage?.persist) return;
try {
const persisted = await navigator.storage.persist();
log.debug("[chat-media] persistent storage requested", { persisted });
} catch (error) {
log.debug("[chat-media] persistent storage request skipped", { error });
}
}