Skip to content

Commit a23cec9

Browse files
committed
add missing save only persisters (#2735)
1 parent 52010db commit a23cec9

File tree

17 files changed

+1277
-448
lines changed

17 files changed

+1277
-448
lines changed
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import { createMergeableStore } from "tinybase/with-schemas";
2+
import { beforeEach, describe, expect, test, vi } from "vitest";
3+
4+
import { SCHEMA, type Schemas } from "@hypr/store";
5+
6+
import { createCalendarPersister } from "./calendar";
7+
8+
vi.mock("@hypr/plugin-path2", () => ({
9+
commands: {
10+
base: vi.fn().mockResolvedValue("/mock/data/dir/hyprnote"),
11+
},
12+
}));
13+
14+
vi.mock("@tauri-apps/plugin-fs", () => ({
15+
mkdir: vi.fn().mockResolvedValue(undefined),
16+
readTextFile: vi.fn(),
17+
writeTextFile: vi.fn().mockResolvedValue(undefined),
18+
}));
19+
20+
function createTestStore() {
21+
return createMergeableStore()
22+
.setTablesSchema(SCHEMA.table)
23+
.setValuesSchema(SCHEMA.value);
24+
}
25+
26+
describe("createCalendarPersister", () => {
27+
let store: ReturnType<typeof createTestStore>;
28+
29+
beforeEach(() => {
30+
store = createTestStore();
31+
vi.clearAllMocks();
32+
});
33+
34+
test("returns a persister object with expected methods", () => {
35+
const persister = createCalendarPersister<Schemas>(store);
36+
37+
expect(persister).toBeDefined();
38+
expect(persister.save).toBeTypeOf("function");
39+
expect(persister.load).toBeTypeOf("function");
40+
expect(persister.destroy).toBeTypeOf("function");
41+
});
42+
43+
describe("load", () => {
44+
test("loads calendars from json file", async () => {
45+
const { readTextFile } = await import("@tauri-apps/plugin-fs");
46+
47+
const mockData = {
48+
"cal-1": {
49+
user_id: "user-1",
50+
created_at: "2024-01-01T00:00:00Z",
51+
tracking_id_calendar: "tracking-1",
52+
name: "Work Calendar",
53+
enabled: true,
54+
provider: "apple",
55+
source: "iCloud",
56+
color: "#FF0000",
57+
},
58+
};
59+
vi.mocked(readTextFile).mockResolvedValue(JSON.stringify(mockData));
60+
61+
const persister = createCalendarPersister<Schemas>(store);
62+
await persister.load();
63+
64+
expect(readTextFile).toHaveBeenCalledWith(
65+
"/mock/data/dir/hyprnote/calendars.json",
66+
);
67+
expect(store.getTable("calendars")).toEqual(mockData);
68+
});
69+
70+
test("returns empty calendars when file does not exist", async () => {
71+
const { readTextFile } = await import("@tauri-apps/plugin-fs");
72+
73+
vi.mocked(readTextFile).mockRejectedValue(
74+
new Error("No such file or directory"),
75+
);
76+
77+
const persister = createCalendarPersister<Schemas>(store);
78+
await persister.load();
79+
80+
expect(store.getTable("calendars")).toEqual({});
81+
});
82+
});
83+
84+
describe("save", () => {
85+
test("saves calendars to json file", async () => {
86+
const { writeTextFile } = await import("@tauri-apps/plugin-fs");
87+
88+
store.setRow("calendars", "cal-1", {
89+
user_id: "user-1",
90+
created_at: "2024-01-01T00:00:00Z",
91+
tracking_id_calendar: "tracking-1",
92+
name: "Work Calendar",
93+
enabled: true,
94+
provider: "apple",
95+
source: "iCloud",
96+
color: "#FF0000",
97+
});
98+
99+
const persister = createCalendarPersister<Schemas>(store);
100+
await persister.save();
101+
102+
expect(writeTextFile).toHaveBeenCalledWith(
103+
"/mock/data/dir/hyprnote/calendars.json",
104+
expect.any(String),
105+
);
106+
107+
const writtenContent = vi.mocked(writeTextFile).mock.calls[0][1];
108+
const parsed = JSON.parse(writtenContent);
109+
110+
expect(parsed).toEqual({
111+
"cal-1": {
112+
user_id: "user-1",
113+
created_at: "2024-01-01T00:00:00Z",
114+
tracking_id_calendar: "tracking-1",
115+
name: "Work Calendar",
116+
enabled: true,
117+
provider: "apple",
118+
source: "iCloud",
119+
color: "#FF0000",
120+
},
121+
});
122+
});
123+
124+
test("writes empty object when no calendars exist", async () => {
125+
const { writeTextFile } = await import("@tauri-apps/plugin-fs");
126+
127+
const persister = createCalendarPersister<Schemas>(store);
128+
await persister.save();
129+
130+
expect(writeTextFile).toHaveBeenCalledWith(
131+
"/mock/data/dir/hyprnote/calendars.json",
132+
"{}",
133+
);
134+
});
135+
136+
test("saves multiple calendars", async () => {
137+
const { writeTextFile } = await import("@tauri-apps/plugin-fs");
138+
139+
store.setRow("calendars", "cal-1", {
140+
user_id: "user-1",
141+
created_at: "2024-01-01T00:00:00Z",
142+
tracking_id_calendar: "tracking-1",
143+
name: "Work Calendar",
144+
enabled: true,
145+
provider: "apple",
146+
source: "iCloud",
147+
color: "#FF0000",
148+
});
149+
150+
store.setRow("calendars", "cal-2", {
151+
user_id: "user-1",
152+
created_at: "2024-01-02T00:00:00Z",
153+
tracking_id_calendar: "tracking-2",
154+
name: "Personal Calendar",
155+
enabled: false,
156+
provider: "google",
157+
source: "Gmail",
158+
color: "#00FF00",
159+
});
160+
161+
const persister = createCalendarPersister<Schemas>(store);
162+
await persister.save();
163+
164+
const writtenContent = vi.mocked(writeTextFile).mock.calls[0][1];
165+
const parsed = JSON.parse(writtenContent);
166+
167+
expect(Object.keys(parsed)).toHaveLength(2);
168+
expect(parsed["cal-1"].name).toBe("Work Calendar");
169+
expect(parsed["cal-2"].name).toBe("Personal Calendar");
170+
});
171+
});
172+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { MergeableStore, OptionalSchemas } from "tinybase/with-schemas";
2+
3+
import { createSimpleJsonPersister, type PersisterMode } from "./utils";
4+
5+
export function createCalendarPersister<Schemas extends OptionalSchemas>(
6+
store: MergeableStore<Schemas>,
7+
config: { mode: PersisterMode } = { mode: "save-only" },
8+
) {
9+
return createSimpleJsonPersister(store, {
10+
tableName: "calendars",
11+
filename: "calendars.json",
12+
label: "CalendarPersister",
13+
mode: config.mode,
14+
});
15+
}

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

Lines changed: 62 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import type { MergeableStore, OptionalSchemas } from "tinybase/with-schemas";
44

55
import type { ChatMessageStorage } from "@hypr/store";
66

7+
import { StoreOrMergeableStore } from "../store/shared";
78
import {
89
ensureDirsExist,
910
getChatDir,
1011
getDataDir,
1112
iterateTableRows,
13+
type PersisterMode,
1214
type TablesContent,
1315
} from "./utils";
1416

@@ -65,61 +67,70 @@ function collectMessagesByChatGroup(
6567

6668
export function createChatPersister<Schemas extends OptionalSchemas>(
6769
store: MergeableStore<Schemas>,
70+
config: { mode: PersisterMode } = { mode: "save-only" },
6871
) {
72+
const loadFn =
73+
config.mode === "save-only" ? async () => undefined : async () => undefined;
74+
75+
const saveFn =
76+
config.mode === "load-only"
77+
? async () => {}
78+
: async (getContent: () => unknown) => {
79+
const [tables] = getContent() as [TablesContent | undefined, unknown];
80+
const dataDir = await getDataDir();
81+
82+
const messagesByChatGroup = collectMessagesByChatGroup(tables);
83+
if (messagesByChatGroup.size === 0) {
84+
return;
85+
}
86+
87+
const dirs = new Set<string>();
88+
const writeOperations: Array<{ path: string; content: string }> = [];
89+
90+
for (const [
91+
chatGroupId,
92+
{ chatGroup, messages },
93+
] of messagesByChatGroup) {
94+
const chatDir = getChatDir(dataDir, chatGroupId);
95+
dirs.add(chatDir);
96+
97+
const json: ChatJson = {
98+
chat_group: chatGroup,
99+
messages: messages.sort(
100+
(a, b) =>
101+
new Date(a.created_at || 0).getTime() -
102+
new Date(b.created_at || 0).getTime(),
103+
),
104+
};
105+
writeOperations.push({
106+
path: `${chatDir}/_messages.json`,
107+
content: JSON.stringify(json, null, 2),
108+
});
109+
}
110+
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) {
119+
try {
120+
await writeTextFile(op.path, op.content);
121+
} catch (e) {
122+
console.error(`Failed to write ${op.path}:`, e);
123+
}
124+
}
125+
};
126+
69127
return createCustomPersister(
70128
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-
},
129+
loadFn,
130+
saveFn,
122131
(listener) => setInterval(listener, 1000),
123132
(interval) => clearInterval(interval),
133+
(error) => console.error("[ChatPersister]:", error),
134+
StoreOrMergeableStore,
124135
);
125136
}

0 commit comments

Comments
 (0)