Skip to content

Commit 1b0ad70

Browse files
authored
Add readonly exporters (#2705)
* refactor * bunch of save-only persister added
1 parent 9f14db7 commit 1b0ad70

File tree

13 files changed

+1629
-238
lines changed

13 files changed

+1629
-238
lines changed
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { writeTextFile } from "@tauri-apps/plugin-fs";
2+
import { createCustomPersister } from "tinybase/persisters/with-schemas";
3+
import type { MergeableStore, OptionalSchemas } from "tinybase/with-schemas";
4+
5+
import type { ChatMessageStorage } from "@hypr/store";
6+
7+
import {
8+
ensureDirsExist,
9+
getChatDir,
10+
getDataDir,
11+
iterateTableRows,
12+
type TablesContent,
13+
} from "./utils";
14+
15+
type ChatGroupData = {
16+
id: string;
17+
user_id: string;
18+
created_at: string;
19+
title: string;
20+
};
21+
22+
type ChatMessageWithId = ChatMessageStorage & { id: string };
23+
24+
type ChatJson = {
25+
chat_group: ChatGroupData;
26+
messages: ChatMessageWithId[];
27+
};
28+
29+
function collectMessagesByChatGroup(
30+
tables: TablesContent | undefined,
31+
): Map<string, { chatGroup: ChatGroupData; messages: ChatMessageWithId[] }> {
32+
const messagesByChatGroup = new Map<
33+
string,
34+
{ chatGroup: ChatGroupData; messages: ChatMessageWithId[] }
35+
>();
36+
37+
const chatGroups = iterateTableRows(tables, "chat_groups");
38+
const chatMessages = iterateTableRows(tables, "chat_messages");
39+
40+
const chatGroupMap = new Map<string, ChatGroupData>();
41+
for (const group of chatGroups) {
42+
chatGroupMap.set(group.id, group);
43+
}
44+
45+
for (const message of chatMessages) {
46+
const chatGroupId = message.chat_group_id;
47+
if (!chatGroupId) continue;
48+
49+
const chatGroup = chatGroupMap.get(chatGroupId);
50+
if (!chatGroup) continue;
51+
52+
const existing = messagesByChatGroup.get(chatGroupId);
53+
if (existing) {
54+
existing.messages.push(message);
55+
} else {
56+
messagesByChatGroup.set(chatGroupId, {
57+
chatGroup,
58+
messages: [message],
59+
});
60+
}
61+
}
62+
63+
return messagesByChatGroup;
64+
}
65+
66+
export function createChatPersister<Schemas extends OptionalSchemas>(
67+
store: MergeableStore<Schemas>,
68+
) {
69+
return createCustomPersister(
70+
store,
71+
async () => {
72+
return undefined;
73+
},
74+
async (getContent) => {
75+
const [tables] = getContent() as [TablesContent | undefined, unknown];
76+
const dataDir = await getDataDir();
77+
78+
const messagesByChatGroup = collectMessagesByChatGroup(tables);
79+
if (messagesByChatGroup.size === 0) {
80+
return;
81+
}
82+
83+
const dirs = new Set<string>();
84+
const writeOperations: Array<{ path: string; content: string }> = [];
85+
86+
for (const [
87+
chatGroupId,
88+
{ chatGroup, messages },
89+
] of messagesByChatGroup) {
90+
const chatDir = getChatDir(dataDir, chatGroupId);
91+
dirs.add(chatDir);
92+
93+
const json: ChatJson = {
94+
chat_group: chatGroup,
95+
messages: messages.sort(
96+
(a, b) =>
97+
new Date(a.created_at || 0).getTime() -
98+
new Date(b.created_at || 0).getTime(),
99+
),
100+
};
101+
writeOperations.push({
102+
path: `${chatDir}/_messages.json`,
103+
content: JSON.stringify(json, null, 2),
104+
});
105+
}
106+
107+
try {
108+
await ensureDirsExist(dirs);
109+
} catch (e) {
110+
console.error("Failed to ensure dirs exist:", e);
111+
return;
112+
}
113+
114+
for (const op of writeOperations) {
115+
try {
116+
await writeTextFile(op.path, op.content);
117+
} catch (e) {
118+
console.error(`Failed to write ${op.path}:`, e);
119+
}
120+
}
121+
},
122+
(listener) => setInterval(listener, 1000),
123+
(interval) => clearInterval(interval),
124+
);
125+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { readTextFile, writeTextFile } from "@tauri-apps/plugin-fs";
2+
import { createCustomPersister } from "tinybase/persisters/with-schemas";
3+
import type {
4+
Content,
5+
MergeableStore,
6+
OptionalSchemas,
7+
} from "tinybase/with-schemas";
8+
9+
import { commands as path2Commands } from "@hypr/plugin-path2";
10+
import type { EventStorage } from "@hypr/store";
11+
12+
import { StoreOrMergeableStore } from "../store/shared";
13+
import type { PersisterMode } from "./utils";
14+
15+
const FILENAME = "events.json";
16+
17+
type EventsJson = Record<string, EventStorage>;
18+
19+
async function getFilePath(): Promise<string> {
20+
const base = await path2Commands.base();
21+
return `${base}/${FILENAME}`;
22+
}
23+
24+
export function jsonToContent<Schemas extends OptionalSchemas>(
25+
data: EventsJson,
26+
): Content<Schemas> {
27+
return [{ events: data }, {}] as unknown as Content<Schemas>;
28+
}
29+
30+
export function storeToJson<Schemas extends OptionalSchemas>(
31+
store: MergeableStore<Schemas>,
32+
): EventsJson {
33+
const table = store.getTable("events") ?? {};
34+
return table as unknown as EventsJson;
35+
}
36+
37+
export function createEventPersister<Schemas extends OptionalSchemas>(
38+
store: MergeableStore<Schemas>,
39+
config: { mode: PersisterMode } = { mode: "load-and-save" },
40+
) {
41+
const loadFn =
42+
config.mode === "save-only"
43+
? async (): Promise<Content<Schemas> | undefined> => undefined
44+
: async (): Promise<Content<Schemas> | undefined> => {
45+
try {
46+
const filePath = await getFilePath();
47+
const content = await readTextFile(filePath);
48+
const data = JSON.parse(content) as EventsJson;
49+
return jsonToContent<Schemas>(data);
50+
} catch (error) {
51+
const errorStr = String(error);
52+
if (
53+
errorStr.includes("No such file or directory") ||
54+
errorStr.includes("ENOENT") ||
55+
errorStr.includes("not found")
56+
) {
57+
return jsonToContent<Schemas>({});
58+
}
59+
console.error("[EventPersister] load error:", error);
60+
return undefined;
61+
}
62+
};
63+
64+
const saveFn =
65+
config.mode === "load-only"
66+
? async () => {}
67+
: async () => {
68+
try {
69+
const data = storeToJson(store);
70+
const filePath = await getFilePath();
71+
await writeTextFile(filePath, JSON.stringify(data, null, 2));
72+
} catch (error) {
73+
console.error("[EventPersister] save error:", error);
74+
}
75+
};
76+
77+
return createCustomPersister(
78+
store,
79+
loadFn,
80+
saveFn,
81+
(listener) => setInterval(listener, 1000),
82+
(handle) => clearInterval(handle),
83+
(error) => console.error("[EventPersister]:", error),
84+
StoreOrMergeableStore,
85+
);
86+
}

0 commit comments

Comments
 (0)