Files
cozsweet-frontend-nextjs/src/data/storage/chat/local_chat_storage.ts
T

164 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
/**
* LocalChatStorage 完整实现(示范 2IndexedDB via Dexie
*
* 对齐 Dart 端 `LocalChatStorage`lib/data/services/storage/chat/local_chat_storage.dart):
* - init / saveMessage / saveMessages / getAllMessages / clearAll / deleteMessage / close
* - 额外提供 `getMessageCount`(异步)替代 Dart 的同步 `messageCount` getter
* - 额外提供 `getAllMessagesBySession(sessionId)`filter 模拟,等下一轮加索引后再用 where)
*
* 单例挂在 class 静态字段。
* Dexie 实例可注入,便于测试用 fake-indexeddb 跑真 Dexie。
*
* 注意:当前表无次级索引,所有过滤(sessionId)都是 `toArray().then(filter)`O(n)。
* 数据量大时考虑升级 schema 加索引。
*/
import { Result, type Result as ResultT } from "@/utils";
import { LocalChatDB } from "./local_chat_db";
import { LocalMessage } from "./local_message";
export class LocalChatStorage {
private readonly db: LocalChatDB;
private static _instance: LocalChatStorage | null = null;
constructor(db?: LocalChatDB) {
this.db = db ?? new LocalChatDB();
}
static getInstance(): LocalChatStorage {
if (!LocalChatStorage._instance) {
LocalChatStorage._instance = new LocalChatStorage();
}
return LocalChatStorage._instance;
}
/** @internal */
static _resetForTests(): void {
LocalChatStorage._instance = null;
}
/**
* 触发 Dexie 打开数据库。
* 幂等:Dexie.open() 多次调用无副作用。
* 在 bootstrap 中 await 此方法可保证后续操作不会因连接未开而延迟。
*/
async init(): Promise<ResultT<void>> {
try {
await this.db.open();
return Result.ok(undefined);
} catch (e) {
return Result.err(e);
}
}
// ---- create / append ----
async saveMessage(message: LocalMessage): Promise<ResultT<void>> {
try {
await this.db.messages.add(message.toRow());
return Result.ok(undefined);
} catch (e) {
return Result.err(e);
}
}
async saveMessages(messages: readonly LocalMessage[]): Promise<ResultT<void>> {
try {
await this.db.transaction(
"rw",
this.db.messages,
async () => {
for (const m of messages) {
await this.db.messages.add(m.toRow());
}
},
);
return Result.ok(undefined);
} catch (e) {
return Result.err(e);
}
}
// ---- read ----
async getAllMessages(): Promise<ResultT<LocalMessage[]>> {
try {
const rows = await this.db.messages.toArray();
// 按 dbId 升序(与 Dart `box.values.toList()` 插入序语义一致)
rows.sort((a, b) => (a.dbId ?? 0) - (b.dbId ?? 0));
return Result.ok(rows.map((r) => LocalMessage.fromRow(r)));
} catch (e) {
return Result.err(e);
}
}
async getMessageCount(): Promise<ResultT<number>> {
try {
return Result.ok(await this.db.messages.count());
} catch (e) {
return Result.err(e);
}
}
async getAllMessagesBySession(
sessionId: string,
): Promise<ResultT<LocalMessage[]>> {
try {
const rows = await this.db.messages
.filter((r) => r.sessionId === sessionId)
.toArray();
rows.sort((a, b) => (a.dbId ?? 0) - (b.dbId ?? 0));
return Result.ok(rows.map((r) => LocalMessage.fromRow(r)));
} catch (e) {
return Result.err(e);
}
}
// ---- delete ----
async clearAll(): Promise<ResultT<void>> {
try {
await this.db.messages.clear();
return Result.ok(undefined);
} catch (e) {
return Result.err(e);
}
}
/**
* 按数组下标删除(保持 Dart 端语义:`box.values.toList()[index].delete()`)。
* O(n) 取下标后单条 delete,与 Dart 行为一致。
*/
async deleteMessage(index: number): Promise<ResultT<void>> {
try {
if (!Number.isInteger(index) || index < 0) {
return Result.err(
new RangeError(`deleteMessage: index ${index} out of range`),
);
}
const rows = await this.db.messages.toArray();
if (index >= rows.length) {
return Result.err(
new RangeError(`deleteMessage: index ${index} out of range`),
);
}
const target = rows[index]!;
await this.db.messages.delete(target.dbId!);
return Result.ok(undefined);
} catch (e) {
return Result.err(e);
}
}
async close(): Promise<ResultT<void>> {
try {
this.db.close();
return Result.ok(undefined);
} catch (e) {
return Result.err(e);
}
}
}