refactor(chat): tighten media cache boundaries
This commit is contained in:
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user