Skip to content

Commit 63e9c26

Browse files
committed
add missing load implementation
1 parent 475ba5c commit 63e9c26

File tree

7 files changed

+470
-19
lines changed

7 files changed

+470
-19
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { sep } from "@tauri-apps/api/path";
2+
import { readDir, readTextFile } from "@tauri-apps/plugin-fs";
3+
4+
import type { ChatGroup, ChatMessageStorage } from "@hypr/store";
5+
6+
import { isFileNotFoundError, isUUID } from "../utils";
7+
8+
type ChatGroupData = {
9+
id: string;
10+
user_id: string;
11+
created_at: string;
12+
title: string;
13+
};
14+
15+
type ChatMessageWithId = ChatMessageStorage & { id: string };
16+
17+
type ChatJson = {
18+
chat_group: ChatGroupData;
19+
messages: ChatMessageWithId[];
20+
};
21+
22+
export type LoadedChatData = {
23+
chat_groups: Record<string, ChatGroup>;
24+
chat_messages: Record<string, ChatMessageStorage>;
25+
};
26+
27+
const LABEL = "ChatPersister";
28+
29+
export async function loadAllChatData(
30+
dataDir: string,
31+
): Promise<LoadedChatData> {
32+
const result: LoadedChatData = {
33+
chat_groups: {},
34+
chat_messages: {},
35+
};
36+
37+
const chatsDir = [dataDir, "chats"].join(sep());
38+
39+
let entries: { name: string; isDirectory: boolean }[];
40+
try {
41+
entries = await readDir(chatsDir);
42+
} catch (error) {
43+
if (!isFileNotFoundError(error)) {
44+
console.error(`[${LABEL}] load error:`, error);
45+
}
46+
return result;
47+
}
48+
49+
for (const entry of entries) {
50+
if (!entry.isDirectory) continue;
51+
if (!isUUID(entry.name)) continue;
52+
53+
const chatGroupId = entry.name;
54+
const messagesPath = [chatsDir, chatGroupId, "_messages.json"].join(sep());
55+
56+
try {
57+
const content = await readTextFile(messagesPath);
58+
const data = JSON.parse(content) as ChatJson;
59+
60+
const { id: _groupId, ...chatGroupData } = data.chat_group;
61+
result.chat_groups[chatGroupId] = chatGroupData;
62+
63+
for (const message of data.messages) {
64+
const { id: messageId, ...messageData } = message;
65+
result.chat_messages[messageId] = messageData;
66+
}
67+
} catch (error) {
68+
if (!isFileNotFoundError(error)) {
69+
console.error(
70+
`[${LABEL}] Failed to load chat from ${messagesPath}:`,
71+
error,
72+
);
73+
}
74+
continue;
75+
}
76+
}
77+
78+
return result;
79+
}
Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,40 @@
1-
import type { MergeableStore, OptionalSchemas } from "tinybase/with-schemas";
1+
import type {
2+
Content,
3+
MergeableStore,
4+
OptionalSchemas,
5+
} from "tinybase/with-schemas";
26

3-
import { createSessionDirPersister } from "../utils";
7+
import { createSessionDirPersister, getDataDir } from "../utils";
48
import { collectChatWriteOps } from "./collect";
9+
import { loadAllChatData } from "./load";
510

611
export function createChatPersister<Schemas extends OptionalSchemas>(
712
store: MergeableStore<Schemas>,
813
) {
914
return createSessionDirPersister(store, {
1015
label: "ChatPersister",
1116
collect: (_store, tables, dataDir) => collectChatWriteOps(tables, dataDir),
17+
load: async (): Promise<Content<Schemas> | undefined> => {
18+
try {
19+
const dataDir = await getDataDir();
20+
const data = await loadAllChatData(dataDir);
21+
const hasData =
22+
Object.keys(data.chat_groups).length > 0 ||
23+
Object.keys(data.chat_messages).length > 0;
24+
if (!hasData) {
25+
return undefined;
26+
}
27+
return [
28+
{
29+
chat_groups: data.chat_groups,
30+
chat_messages: data.chat_messages,
31+
},
32+
{},
33+
] as unknown as Content<Schemas>;
34+
} catch (error) {
35+
console.error("[ChatPersister] load error:", error);
36+
return undefined;
37+
}
38+
},
1239
});
1340
}

apps/desktop/src/store/tinybase/persister/note/collect.ts

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,28 @@
11
import { sep } from "@tauri-apps/api/path";
22
import type { MergeableStore, OptionalSchemas } from "tinybase/with-schemas";
33

4+
import type { ParsedDocument } from "@hypr/plugin-frontmatter";
45
import type { EnhancedNoteStorage } from "@hypr/store";
5-
import { isValidTiptapContent } from "@hypr/tiptap/shared";
6+
import { isValidTiptapContent, json2md } from "@hypr/tiptap/shared";
67

78
import {
89
type CollectorResult,
910
getSessionDir,
1011
iterateTableRows,
11-
type JsonValue,
1212
sanitizeFilename,
1313
type TablesContent,
1414
} from "../utils";
1515

16-
function parseTiptapContent(content: string): JsonValue | undefined {
16+
export type NoteFrontmatter = {
17+
id: string;
18+
session_id: string;
19+
type: "enhanced_note" | "memo";
20+
template_id?: string;
21+
position?: number;
22+
title?: string;
23+
};
24+
25+
function tryParseAndConvertToMarkdown(content: string): string | undefined {
1726
let parsed: unknown;
1827
try {
1928
parsed = JSON.parse(content);
@@ -25,7 +34,7 @@ function parseTiptapContent(content: string): JsonValue | undefined {
2534
return undefined;
2635
}
2736

28-
return parsed as JsonValue;
37+
return json2md(parsed);
2938
}
3039

3140
function getEnhancedNoteFilename<Schemas extends OptionalSchemas>(
@@ -53,20 +62,29 @@ export function collectNoteWriteOps<Schemas extends OptionalSchemas>(
5362
dataDir: string,
5463
): CollectorResult {
5564
const dirs = new Set<string>();
56-
const mdBatchItems: Array<[JsonValue, string]> = [];
65+
const frontmatterBatchItems: Array<[ParsedDocument, string]> = [];
5766

5867
for (const enhancedNote of iterateTableRows(tables, "enhanced_notes")) {
5968
if (!enhancedNote.content || !enhancedNote.session_id) {
6069
continue;
6170
}
6271

63-
const parsed = parseTiptapContent(enhancedNote.content);
64-
if (!parsed) {
72+
const markdown = tryParseAndConvertToMarkdown(enhancedNote.content);
73+
if (!markdown) {
6574
continue;
6675
}
6776

6877
const filename = getEnhancedNoteFilename(store, enhancedNote);
6978

79+
const frontmatter: NoteFrontmatter = {
80+
id: enhancedNote.id,
81+
session_id: enhancedNote.session_id,
82+
type: "enhanced_note",
83+
template_id: enhancedNote.template_id || undefined,
84+
position: enhancedNote.position,
85+
title: enhancedNote.title || undefined,
86+
};
87+
7088
const session = tables.sessions?.[enhancedNote.session_id];
7189
const folderPath = session?.folder_id ?? "";
7290
const sessionDir = getSessionDir(
@@ -75,28 +93,43 @@ export function collectNoteWriteOps<Schemas extends OptionalSchemas>(
7593
folderPath,
7694
);
7795
dirs.add(sessionDir);
78-
mdBatchItems.push([parsed, [sessionDir, filename].join(sep())]);
96+
frontmatterBatchItems.push([
97+
{ frontmatter, content: markdown },
98+
[sessionDir, filename].join(sep()),
99+
]);
79100
}
80101

81102
for (const session of iterateTableRows(tables, "sessions")) {
82103
if (!session.raw_md) {
83104
continue;
84105
}
85106

86-
const parsed = parseTiptapContent(session.raw_md);
87-
if (!parsed) {
107+
const markdown = tryParseAndConvertToMarkdown(session.raw_md);
108+
if (!markdown) {
88109
continue;
89110
}
90111

112+
const frontmatter: NoteFrontmatter = {
113+
id: session.id,
114+
session_id: session.id,
115+
type: "memo",
116+
};
117+
91118
const folderPath = session.folder_id ?? "";
92119
const sessionDir = getSessionDir(dataDir, session.id, folderPath);
93120
dirs.add(sessionDir);
94-
mdBatchItems.push([parsed, [sessionDir, "_memo.md"].join(sep())]);
121+
frontmatterBatchItems.push([
122+
{ frontmatter, content: markdown },
123+
[sessionDir, "_memo.md"].join(sep()),
124+
]);
95125
}
96126

97127
const operations: CollectorResult["operations"] = [];
98-
if (mdBatchItems.length > 0) {
99-
operations.push({ type: "md-batch", items: mdBatchItems });
128+
if (frontmatterBatchItems.length > 0) {
129+
operations.push({
130+
type: "frontmatter-batch",
131+
items: frontmatterBatchItems,
132+
});
100133
}
101134

102135
return { dirs, operations };
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { sep } from "@tauri-apps/api/path";
2+
import { exists, readDir, readTextFile } from "@tauri-apps/plugin-fs";
3+
4+
import { commands as frontmatterCommands } from "@hypr/plugin-frontmatter";
5+
import type { EnhancedNoteStorage } from "@hypr/store";
6+
import { md2json } from "@hypr/tiptap/shared";
7+
8+
import { isFileNotFoundError } from "../utils";
9+
import type { NoteFrontmatter } from "./collect";
10+
11+
export type LoadedNoteData = {
12+
enhanced_notes: Record<string, EnhancedNoteStorage>;
13+
session_raw_md: Record<string, string>;
14+
};
15+
16+
const LABEL = "NotePersister";
17+
18+
async function loadNotesRecursively(
19+
sessionsDir: string,
20+
currentPath: string,
21+
result: LoadedNoteData,
22+
): Promise<void> {
23+
const s = sep();
24+
const fullPath = currentPath
25+
? [sessionsDir, currentPath].join(s)
26+
: sessionsDir;
27+
28+
let entries: { name: string; isDirectory: boolean }[];
29+
try {
30+
entries = await readDir(fullPath);
31+
} catch {
32+
return;
33+
}
34+
35+
for (const entry of entries) {
36+
if (entry.isDirectory) {
37+
const entryPath = currentPath
38+
? [currentPath, entry.name].join(s)
39+
: entry.name;
40+
const metaPath = [sessionsDir, entryPath, "_meta.json"].join(s);
41+
const hasMetaJson = await exists(metaPath);
42+
43+
if (hasMetaJson) {
44+
await loadNotesFromSessionDir(sessionsDir, entryPath, result);
45+
} else {
46+
await loadNotesRecursively(sessionsDir, entryPath, result);
47+
}
48+
}
49+
}
50+
}
51+
52+
async function loadNotesFromSessionDir(
53+
sessionsDir: string,
54+
sessionPath: string,
55+
result: LoadedNoteData,
56+
): Promise<void> {
57+
const s = sep();
58+
const sessionDir = [sessionsDir, sessionPath].join(s);
59+
60+
let entries: { name: string; isDirectory: boolean }[];
61+
try {
62+
entries = await readDir(sessionDir);
63+
} catch {
64+
return;
65+
}
66+
67+
for (const entry of entries) {
68+
if (entry.isDirectory) continue;
69+
if (!entry.name.endsWith(".md")) continue;
70+
71+
const filePath = [sessionDir, entry.name].join(s);
72+
73+
try {
74+
const content = await readTextFile(filePath);
75+
const parseResult = await frontmatterCommands.deserialize(content);
76+
77+
if (parseResult.status === "error") {
78+
console.error(
79+
`[${LABEL}] Failed to parse frontmatter from ${filePath}:`,
80+
parseResult.error,
81+
);
82+
continue;
83+
}
84+
85+
const { frontmatter, content: markdownBody } = parseResult.data;
86+
const fm = frontmatter as NoteFrontmatter;
87+
88+
if (!fm.id || !fm.session_id || !fm.type) {
89+
continue;
90+
}
91+
92+
const tiptapJson = md2json(markdownBody);
93+
const tiptapContent = JSON.stringify(tiptapJson);
94+
95+
if (fm.type === "memo") {
96+
result.session_raw_md[fm.session_id] = tiptapContent;
97+
} else if (fm.type === "enhanced_note") {
98+
result.enhanced_notes[fm.id] = {
99+
user_id: "",
100+
created_at: new Date().toISOString(),
101+
session_id: fm.session_id,
102+
content: tiptapContent,
103+
template_id: fm.template_id ?? "",
104+
position: fm.position ?? 0,
105+
title: fm.title ?? "",
106+
};
107+
}
108+
} catch (error) {
109+
console.error(`[${LABEL}] Failed to load note from ${filePath}:`, error);
110+
continue;
111+
}
112+
}
113+
}
114+
115+
export async function loadAllNoteData(
116+
dataDir: string,
117+
): Promise<LoadedNoteData> {
118+
const result: LoadedNoteData = {
119+
enhanced_notes: {},
120+
session_raw_md: {},
121+
};
122+
123+
const sessionsDir = [dataDir, "sessions"].join(sep());
124+
125+
try {
126+
await loadNotesRecursively(sessionsDir, "", result);
127+
} catch (error) {
128+
if (!isFileNotFoundError(error)) {
129+
console.error(`[${LABEL}] load error:`, error);
130+
}
131+
}
132+
133+
return result;
134+
}

0 commit comments

Comments
 (0)