Skip to content

Commit e200ebb

Browse files
committed
persister fixes
1 parent 69b4729 commit e200ebb

File tree

11 files changed

+301
-115
lines changed

11 files changed

+301
-115
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/desktop/src/store/tinybase/persister/chat.ts

Lines changed: 57 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { writeTextFile } from "@tauri-apps/plugin-fs";
1+
import { sep } from "@tauri-apps/api/path";
2+
import { exists, readDir, remove, writeTextFile } from "@tauri-apps/plugin-fs";
23
import { createCustomPersister } from "tinybase/persisters/with-schemas";
34
import type { MergeableStore, OptionalSchemas } from "tinybase/with-schemas";
45

@@ -9,6 +10,7 @@ import {
910
ensureDirsExist,
1011
getChatDir,
1112
getDataDir,
13+
isUUID,
1214
iterateTableRows,
1315
type PersisterMode,
1416
type TablesContent,
@@ -65,6 +67,42 @@ function collectMessagesByChatGroup(
6567
return messagesByChatGroup;
6668
}
6769

70+
async function cleanupOrphanChatDirs(
71+
dataDir: string,
72+
validChatGroupIds: Set<string>,
73+
): Promise<void> {
74+
const chatsDir = [dataDir, "chats"].join(sep());
75+
76+
let entries: { name: string; isDirectory: boolean }[];
77+
try {
78+
entries = await readDir(chatsDir);
79+
} catch {
80+
return;
81+
}
82+
83+
for (const entry of entries) {
84+
if (!entry.isDirectory) continue;
85+
86+
const messagesPath = [chatsDir, entry.name, "_messages.json"].join(sep());
87+
const hasMessagesJson = await exists(messagesPath);
88+
89+
if (
90+
hasMessagesJson &&
91+
isUUID(entry.name) &&
92+
!validChatGroupIds.has(entry.name)
93+
) {
94+
try {
95+
await remove([chatsDir, entry.name].join(sep()), { recursive: true });
96+
} catch (e) {
97+
console.error(
98+
`[ChatPersister] Failed to remove orphan dir ${entry.name}:`,
99+
e,
100+
);
101+
}
102+
}
103+
}
104+
}
105+
68106
export function createChatPersister<Schemas extends OptionalSchemas>(
69107
store: MergeableStore<Schemas>,
70108
config: { mode: PersisterMode } = { mode: "save-only" },
@@ -80,9 +118,7 @@ export function createChatPersister<Schemas extends OptionalSchemas>(
80118
const dataDir = await getDataDir();
81119

82120
const messagesByChatGroup = collectMessagesByChatGroup(tables);
83-
if (messagesByChatGroup.size === 0) {
84-
return;
85-
}
121+
const validChatGroupIds = new Set(messagesByChatGroup.keys());
86122

87123
const dirs = new Set<string>();
88124
const writeOperations: Array<{ path: string; content: string }> = [];
@@ -103,33 +139,37 @@ export function createChatPersister<Schemas extends OptionalSchemas>(
103139
),
104140
};
105141
writeOperations.push({
106-
path: `${chatDir}/_messages.json`,
142+
path: [chatDir, "_messages.json"].join(sep()),
107143
content: JSON.stringify(json, null, 2),
108144
});
109145
}
110146

111-
try {
112-
await ensureDirsExist(dirs);
113-
} catch (e) {
114-
console.error("Failed to ensure dirs exist:", e);
115-
return;
116-
}
117-
118-
for (const op of writeOperations) {
147+
if (writeOperations.length > 0) {
119148
try {
120-
await writeTextFile(op.path, op.content);
149+
await ensureDirsExist(dirs);
121150
} catch (e) {
122-
console.error(`Failed to write ${op.path}:`, e);
151+
console.error("Failed to ensure dirs exist:", e);
152+
return;
153+
}
154+
155+
for (const op of writeOperations) {
156+
try {
157+
await writeTextFile(op.path, op.content);
158+
} catch (e) {
159+
console.error(`Failed to write ${op.path}:`, e);
160+
}
123161
}
124162
}
163+
164+
await cleanupOrphanChatDirs(dataDir, validChatGroupIds);
125165
};
126166

127167
return createCustomPersister(
128168
store,
129169
loadFn,
130170
saveFn,
131-
(listener) => setInterval(listener, 1000),
132-
(interval) => clearInterval(interval),
171+
() => null,
172+
() => {},
133173
(error) => console.error("[ChatPersister]:", error),
134174
StoreOrMergeableStore,
135175
);

apps/desktop/src/store/tinybase/persister/folder.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ export function createFolderPersister<Schemas extends OptionalSchemas>(
6666
store,
6767
loadFn,
6868
saveFn,
69-
(listener) => setInterval(listener, 5000),
70-
(handle) => clearInterval(handle),
69+
() => null,
70+
() => {},
7171
(error) => console.error("[FolderPersister]:", error),
7272
StoreOrMergeableStore,
7373
);

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { sep } from "@tauri-apps/api/path";
12
import { createCustomPersister } from "tinybase/persisters/with-schemas";
23
import type { MergeableStore, OptionalSchemas } from "tinybase/with-schemas";
34

@@ -66,8 +67,8 @@ export function createNotePersister<Schemas extends OptionalSchemas>(
6667
store,
6768
loadFn,
6869
saveFn,
69-
(listener) => setInterval(listener, 1000),
70-
(interval) => clearInterval(interval),
70+
() => null,
71+
() => {},
7172
(error) => console.error("[NotePersister]:", error),
7273
StoreOrMergeableStore,
7374
);
@@ -139,7 +140,7 @@ function collectEnhancedNoteBatchItems<Schemas extends OptionalSchemas>(
139140
folderPath,
140141
);
141142
dirs.add(sessionDir);
142-
items.push([parsed, `${sessionDir}/${filename}`]);
143+
items.push([parsed, [sessionDir, filename].join(sep())]);
143144
}
144145

145146
return { items, dirs };
@@ -165,7 +166,7 @@ function collectSessionBatchItems(
165166
const folderPath = session.folder_id ?? "";
166167
const sessionDir = getSessionDir(dataDir, session.id, folderPath);
167168
dirs.add(sessionDir);
168-
items.push([parsed, `${sessionDir}/_memo.md`]);
169+
items.push([parsed, [sessionDir, "_memo.md"].join(sep())]);
169170
}
170171

171172
return { items, dirs };

apps/desktop/src/store/tinybase/persister/session.ts

Lines changed: 86 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import { sep } from "@tauri-apps/api/path";
12
import {
23
exists,
34
readDir,
45
readTextFile,
6+
remove,
57
writeTextFile,
68
} from "@tauri-apps/plugin-fs";
79
import { createCustomPersister } from "tinybase/persisters/with-schemas";
@@ -23,6 +25,7 @@ import {
2325
ensureDirsExist,
2426
getDataDir,
2527
getSessionDir,
28+
isUUID,
2629
type PersisterMode,
2730
} from "./utils";
2831

@@ -52,7 +55,10 @@ async function loadSessionMetaRecursively(
5255
result: LoadedData,
5356
now: string,
5457
): Promise<void> {
55-
const fullPath = currentPath ? `${sessionsDir}/${currentPath}` : sessionsDir;
58+
const s = sep();
59+
const fullPath = currentPath
60+
? [sessionsDir, currentPath].join(s)
61+
: sessionsDir;
5662

5763
let entries: { name: string; isDirectory: boolean }[];
5864
try {
@@ -64,8 +70,10 @@ async function loadSessionMetaRecursively(
6470
for (const entry of entries) {
6571
if (!entry.isDirectory) continue;
6672

67-
const entryPath = currentPath ? `${currentPath}/${entry.name}` : entry.name;
68-
const metaPath = `${sessionsDir}/${entryPath}/_meta.json`;
73+
const entryPath = currentPath
74+
? [currentPath, entry.name].join(s)
75+
: entry.name;
76+
const metaPath = [sessionsDir, entryPath, "_meta.json"].join(s);
6977
const hasMetaJson = await exists(metaPath);
7078

7179
if (hasMetaJson) {
@@ -130,14 +138,75 @@ async function loadAllSessionMeta(dataDir: string): Promise<LoadedData> {
130138
mapping_tag_session: {},
131139
};
132140

133-
const sessionsDir = `${dataDir}/sessions`;
141+
const sessionsDir = [dataDir, "sessions"].join(sep());
134142
const now = new Date().toISOString();
135143

136144
await loadSessionMetaRecursively(sessionsDir, "", result, now);
137145

138146
return result;
139147
}
140148

149+
async function collectSessionDirsRecursively(
150+
sessionsDir: string,
151+
currentPath: string,
152+
result: Array<{ path: string; name: string }>,
153+
): Promise<void> {
154+
const s = sep();
155+
const fullPath = currentPath
156+
? [sessionsDir, currentPath].join(s)
157+
: sessionsDir;
158+
159+
let entries: { name: string; isDirectory: boolean }[];
160+
try {
161+
entries = await readDir(fullPath);
162+
} catch {
163+
return;
164+
}
165+
166+
for (const entry of entries) {
167+
if (!entry.isDirectory) continue;
168+
169+
const entryPath = currentPath
170+
? [currentPath, entry.name].join(s)
171+
: entry.name;
172+
const metaPath = [sessionsDir, entryPath, "_meta.json"].join(s);
173+
const hasMetaJson = await exists(metaPath);
174+
175+
if (hasMetaJson) {
176+
result.push({ path: [sessionsDir, entryPath].join(s), name: entry.name });
177+
} else {
178+
await collectSessionDirsRecursively(sessionsDir, entryPath, result);
179+
}
180+
}
181+
}
182+
183+
async function cleanupOrphanSessionDirs(
184+
dataDir: string,
185+
validSessionIds: Set<string>,
186+
): Promise<void> {
187+
const sessionsDir = [dataDir, "sessions"].join(sep());
188+
const existingDirs: Array<{ path: string; name: string }> = [];
189+
190+
try {
191+
await collectSessionDirsRecursively(sessionsDir, "", existingDirs);
192+
} catch {
193+
return;
194+
}
195+
196+
for (const dir of existingDirs) {
197+
if (isUUID(dir.name) && !validSessionIds.has(dir.name)) {
198+
try {
199+
await remove(dir.path, { recursive: true });
200+
} catch (e) {
201+
console.error(
202+
`[SessionPersister] Failed to remove orphan dir ${dir.path}:`,
203+
e,
204+
);
205+
}
206+
}
207+
}
208+
}
209+
141210
export function collectSessionMeta<Schemas extends OptionalSchemas>(
142211
store: MergeableStore<Schemas>,
143212
): Map<string, SessionMetaJson> {
@@ -230,10 +299,6 @@ export function createSessionPersister<Schemas extends OptionalSchemas>(
230299
const dataDir = await getDataDir();
231300
const sessionMetas = collectSessionMeta(store);
232301

233-
if (sessionMetas.size === 0) {
234-
return;
235-
}
236-
237302
const dirs = new Set<string>();
238303
const writeOperations: Array<{ path: string; content: string }> =
239304
[];
@@ -247,16 +312,23 @@ export function createSessionPersister<Schemas extends OptionalSchemas>(
247312
dirs.add(sessionDir);
248313

249314
writeOperations.push({
250-
path: `${sessionDir}/_meta.json`,
315+
path: [sessionDir, "_meta.json"].join(sep()),
251316
content: JSON.stringify(meta, null, 2),
252317
});
253318
}
254319

255-
await ensureDirsExist(dirs);
320+
if (writeOperations.length > 0) {
321+
await ensureDirsExist(dirs);
256322

257-
for (const op of writeOperations) {
258-
await writeTextFile(op.path, op.content);
323+
for (const op of writeOperations) {
324+
await writeTextFile(op.path, op.content);
325+
}
259326
}
327+
328+
await cleanupOrphanSessionDirs(
329+
dataDir,
330+
new Set(sessionMetas.keys()),
331+
);
260332
} catch (error) {
261333
console.error("[SessionPersister] save error:", error);
262334
}
@@ -266,8 +338,8 @@ export function createSessionPersister<Schemas extends OptionalSchemas>(
266338
store,
267339
loadFn,
268340
saveFn,
269-
(listener) => setInterval(listener, 1000),
270-
(handle) => clearInterval(handle),
341+
() => null,
342+
() => {},
271343
(error) => console.error("[SessionPersister]:", error),
272344
StoreOrMergeableStore,
273345
);

0 commit comments

Comments
 (0)