164 lines
4.5 KiB
TypeScript
164 lines
4.5 KiB
TypeScript
"use client";
|
||
|
||
/**
|
||
* LocalChatStorage 完整实现(示范 2:IndexedDB 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);
|
||
}
|
||
}
|
||
}
|