cojson-storage-indexeddb 0.8.32 → 0.8.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,179 @@
1
+ import { CojsonInternalTypes, SessionID, Stringified } from "cojson";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { getDependedOnCoValues } from "../syncUtils";
4
+
5
+ function getMockedSessionID(accountId?: `co_z${string}`) {
6
+ return `${accountId ?? getMockedCoValueId()}_session_z${Math.random().toString(36).substring(2, 15)}`;
7
+ }
8
+
9
+ function getMockedCoValueId() {
10
+ return `co_z${Math.random().toString(36).substring(2, 15)}` as const;
11
+ }
12
+
13
+ function generateNewContentMessage(
14
+ privacy: "trusting" | "private",
15
+ changes: any[],
16
+ accountId?: `co_z${string}`,
17
+ ) {
18
+ return {
19
+ action: "content",
20
+ id: getMockedCoValueId(),
21
+ new: {
22
+ [getMockedSessionID(accountId)]: {
23
+ after: 0,
24
+ lastSignature: "signature_z123",
25
+ newTransactions: [
26
+ {
27
+ privacy,
28
+ madeAt: 0,
29
+ changes: JSON.stringify(changes) as any,
30
+ },
31
+ ],
32
+ },
33
+ },
34
+ priority: 0,
35
+ } as CojsonInternalTypes.NewContentMessage;
36
+ }
37
+
38
+ describe("getDependedOnCoValues", () => {
39
+ it("should return dependencies for group ruleset", () => {
40
+ const coValueRow = {
41
+ id: "co_test",
42
+ header: {
43
+ ruleset: {
44
+ type: "group",
45
+ },
46
+ },
47
+ } as any;
48
+
49
+ const result = getDependedOnCoValues({
50
+ coValueRow,
51
+ newContentMessages: [
52
+ generateNewContentMessage("trusting", [
53
+ { op: "set", key: "co_zabc123", value: "test" },
54
+ { op: "set", key: "parent_co_zdef456", value: "test" },
55
+ { op: "set", key: "normal_key", value: "test" },
56
+ ]),
57
+ ],
58
+ });
59
+
60
+ expect(result).toEqual(["co_zabc123", "co_zdef456"]);
61
+ });
62
+
63
+ it("should not throw on malformed JSON", () => {
64
+ const coValueRow = {
65
+ id: "co_test",
66
+ header: {
67
+ ruleset: {
68
+ type: "group",
69
+ },
70
+ },
71
+ } as any;
72
+
73
+ const message = generateNewContentMessage("trusting", [
74
+ { op: "set", key: "co_zabc123", value: "test" },
75
+ ]);
76
+
77
+ message.new["invalid_session" as SessionID] = {
78
+ after: 0,
79
+ lastSignature: "signature_z123",
80
+ newTransactions: [
81
+ {
82
+ privacy: "trusting",
83
+ madeAt: 0,
84
+ changes: "}{-:)" as Stringified<CojsonInternalTypes.JsonObject[]>,
85
+ },
86
+ ],
87
+ };
88
+
89
+ const result = getDependedOnCoValues({
90
+ coValueRow,
91
+ newContentMessages: [message],
92
+ });
93
+
94
+ expect(result).toEqual(["co_zabc123"]);
95
+ });
96
+
97
+ it("should return dependencies for ownedByGroup ruleset", () => {
98
+ const groupId = getMockedCoValueId();
99
+ const coValueRow = {
100
+ id: "co_owner",
101
+ header: {
102
+ ruleset: {
103
+ type: "ownedByGroup",
104
+ group: groupId,
105
+ },
106
+ },
107
+ } as any;
108
+
109
+ const accountId = getMockedCoValueId();
110
+ const message = generateNewContentMessage(
111
+ "trusting",
112
+ [
113
+ { op: "set", key: "co_zabc123", value: "test" },
114
+ { op: "set", key: "parent_co_zdef456", value: "test" },
115
+ { op: "set", key: "normal_key", value: "test" },
116
+ ],
117
+ accountId,
118
+ );
119
+
120
+ message.new["invalid_session" as SessionID] = {
121
+ after: 0,
122
+ lastSignature: "signature_z123",
123
+ newTransactions: [],
124
+ };
125
+
126
+ const result = getDependedOnCoValues({
127
+ coValueRow,
128
+ newContentMessages: [message],
129
+ });
130
+
131
+ expect(result).toEqual([groupId, accountId]);
132
+ });
133
+
134
+ it("should return empty array for other ruleset types", () => {
135
+ const coValueRow = {
136
+ id: "co_test",
137
+ header: {
138
+ ruleset: {
139
+ type: "other",
140
+ },
141
+ },
142
+ } as any;
143
+
144
+ const result = getDependedOnCoValues({
145
+ coValueRow,
146
+ newContentMessages: [
147
+ generateNewContentMessage("trusting", [
148
+ { op: "set", key: "co_zabc123", value: "test" },
149
+ { op: "set", key: "parent_co_zdef456", value: "test" },
150
+ { op: "set", key: "normal_key", value: "test" },
151
+ ]),
152
+ ],
153
+ });
154
+
155
+ expect(result).toEqual([]);
156
+ });
157
+
158
+ it("should ignore non-trusting transactions in group ruleset", () => {
159
+ const coValueRow = {
160
+ id: "co_test",
161
+ header: {
162
+ ruleset: {
163
+ type: "group",
164
+ },
165
+ },
166
+ } as any;
167
+
168
+ const result = getDependedOnCoValues({
169
+ coValueRow,
170
+ newContentMessages: [
171
+ generateNewContentMessage("private", [
172
+ { op: "set", key: "co_zabc123", value: "test" },
173
+ ]),
174
+ ],
175
+ });
176
+
177
+ expect(result).toEqual([]);
178
+ });
179
+ });
@@ -6,7 +6,7 @@ import { IDBStorage } from "../index.js";
6
6
 
7
7
  const Crypto = await WasmCrypto.create();
8
8
 
9
- test.skip("Should be able to initialize and load from empty DB", async () => {
9
+ test("Should be able to initialize and load from empty DB", async () => {
10
10
  const agentSecret = Crypto.newRandomAgentSecret();
11
11
 
12
12
  const node = new LocalNode(
@@ -23,7 +23,7 @@ test.skip("Should be able to initialize and load from empty DB", async () => {
23
23
 
24
24
  await new Promise((resolve) => setTimeout(resolve, 200));
25
25
 
26
- expect(node.syncManager.peers["storage"]).toBeDefined();
26
+ expect(node.syncManager.peers["indexedDB"]).toBeDefined();
27
27
  });
28
28
 
29
29
  test("Should be able to sync data to database and then load that from a new node", async () => {
@@ -0,0 +1,337 @@
1
+ import {
2
+ Mocked,
3
+ afterEach,
4
+ beforeEach,
5
+ describe,
6
+ expect,
7
+ test,
8
+ vi,
9
+ } from "vitest";
10
+
11
+ import {
12
+ CojsonInternalTypes,
13
+ OutgoingSyncQueue,
14
+ SessionID,
15
+ SyncMessage,
16
+ } from "cojson";
17
+ import { IDBClient } from "../idbClient";
18
+ import { SyncManager } from "../syncManager";
19
+ import { getDependedOnCoValues } from "../syncUtils";
20
+ import { fixtures } from "./fixtureMessages";
21
+ import RawCoID = CojsonInternalTypes.RawCoID;
22
+ import NewContentMessage = CojsonInternalTypes.NewContentMessage;
23
+
24
+ vi.mock("../syncUtils");
25
+
26
+ const coValueIdToLoad = "co_zKwG8NyfZ8GXqcjDHY4NS3SbU2m";
27
+ const createEmptyLoadMsg = (id: string) =>
28
+ ({
29
+ action: "load",
30
+ id,
31
+ header: false,
32
+ sessions: {},
33
+ }) as SyncMessage;
34
+
35
+ const sessionsData = fixtures[coValueIdToLoad].sessionRecords;
36
+ const coValueHeader = fixtures[coValueIdToLoad].getContent({ after: 0 }).header;
37
+ const incomingContentMessage = fixtures[coValueIdToLoad].getContent({
38
+ after: 0,
39
+ }) as SyncMessage;
40
+
41
+ describe("IDB sync manager", () => {
42
+ let syncManager: SyncManager;
43
+ let queue: OutgoingSyncQueue = {} as unknown as OutgoingSyncQueue;
44
+
45
+ const IDBClient = vi.fn();
46
+ IDBClient.prototype.getCoValue = vi.fn();
47
+ IDBClient.prototype.getCoValueSessions = vi.fn();
48
+ IDBClient.prototype.addSessionUpdate = vi.fn();
49
+ IDBClient.prototype.addTransaction = vi.fn();
50
+
51
+ beforeEach(async () => {
52
+ const idbClient = new IDBClient() as unknown as Mocked<IDBClient>;
53
+ syncManager = new SyncManager(idbClient, queue);
54
+ syncManager.sendStateMessage = vi.fn();
55
+
56
+ // No dependencies found
57
+ vi.mocked(getDependedOnCoValues).mockReturnValue([]);
58
+ });
59
+
60
+ afterEach(() => {
61
+ vi.clearAllMocks();
62
+ });
63
+
64
+ test("Incoming known messages are not processed", async () => {
65
+ await syncManager.handleSyncMessage({ action: "known" } as SyncMessage);
66
+ expect(syncManager.sendStateMessage).not.toBeCalled();
67
+ });
68
+
69
+ describe("Handle load incoming message", () => {
70
+ test("sends empty known message for unknown coValue", async () => {
71
+ const loadMsg = createEmptyLoadMsg(coValueIdToLoad);
72
+
73
+ IDBClient.prototype.getCoValue.mockResolvedValueOnce(undefined);
74
+
75
+ await syncManager.handleSyncMessage(loadMsg);
76
+
77
+ expect(syncManager.sendStateMessage).toBeCalledWith({
78
+ action: "known",
79
+ header: false,
80
+ id: coValueIdToLoad,
81
+ sessions: {},
82
+ });
83
+ });
84
+
85
+ test("Sends known and content message for known coValue with no sessions", async () => {
86
+ const loadMsg = createEmptyLoadMsg(coValueIdToLoad);
87
+
88
+ IDBClient.prototype.getCoValue.mockResolvedValueOnce({
89
+ id: coValueIdToLoad,
90
+ header: coValueHeader,
91
+ rowID: 3,
92
+ });
93
+ IDBClient.prototype.getCoValueSessions.mockResolvedValueOnce([]);
94
+
95
+ await syncManager.handleSyncMessage(loadMsg);
96
+
97
+ expect(syncManager.sendStateMessage).toBeCalledTimes(2);
98
+ expect(syncManager.sendStateMessage).toBeCalledWith({
99
+ action: "known",
100
+ header: true,
101
+ id: coValueIdToLoad,
102
+ sessions: {},
103
+ });
104
+ expect(syncManager.sendStateMessage).toBeCalledWith({
105
+ action: "content",
106
+ header: expect.objectContaining({
107
+ type: expect.any(String),
108
+ ruleset: expect.any(Object),
109
+ }),
110
+ id: coValueIdToLoad,
111
+ new: {},
112
+ priority: 0,
113
+ });
114
+ });
115
+
116
+ test("Sends both known and content messages when we have new sessions info for the requested coValue ", async () => {
117
+ const loadMsg = createEmptyLoadMsg(coValueIdToLoad);
118
+
119
+ IDBClient.prototype.getCoValue.mockResolvedValueOnce({
120
+ id: coValueIdToLoad,
121
+ header: coValueHeader,
122
+ rowID: 3,
123
+ });
124
+ IDBClient.prototype.getCoValueSessions.mockResolvedValueOnce(
125
+ sessionsData,
126
+ );
127
+
128
+ const newTxData = {
129
+ newTransactions: [
130
+ {
131
+ privacy: "trusting",
132
+ madeAt: 1732368535089,
133
+ changes: "",
134
+ } as CojsonInternalTypes.Transaction,
135
+ ],
136
+ after: 0,
137
+ lastSignature: "signature_z111",
138
+ } satisfies CojsonInternalTypes.SessionNewContent;
139
+
140
+ // mock content data combined with session updates
141
+ syncManager.handleSessionUpdate = vi.fn(
142
+ async ({ sessionRow, newContentMessages }) => {
143
+ newContentMessages[0]!.new[sessionRow.sessionID] = newTxData;
144
+ },
145
+ );
146
+
147
+ await syncManager.handleSyncMessage(loadMsg);
148
+
149
+ expect(syncManager.sendStateMessage).toBeCalledTimes(2);
150
+
151
+ expect(syncManager.sendStateMessage).toHaveBeenNthCalledWith(1, {
152
+ action: "known",
153
+ header: true,
154
+ id: coValueIdToLoad,
155
+ sessions: sessionsData.reduce(
156
+ (acc, sessionRow) => ({
157
+ ...acc,
158
+ [sessionRow.sessionID]: sessionRow.lastIdx,
159
+ }),
160
+ {},
161
+ ),
162
+ });
163
+
164
+ expect(syncManager.sendStateMessage).toHaveBeenNthCalledWith(2, {
165
+ action: "content",
166
+ header: coValueHeader,
167
+ id: coValueIdToLoad,
168
+ new: sessionsData.reduce(
169
+ (acc, sessionRow) => ({
170
+ ...acc,
171
+ [sessionRow.sessionID]: {
172
+ after: expect.any(Number),
173
+ lastSignature: expect.any(String),
174
+ newTransactions: expect.any(Array),
175
+ },
176
+ }),
177
+ {},
178
+ ),
179
+ priority: 0,
180
+ });
181
+ });
182
+
183
+ test("Sends messages for unique coValue dependencies only, leaving out circular dependencies", async () => {
184
+ const loadMsg = createEmptyLoadMsg(coValueIdToLoad);
185
+ const dependency1 = "co_zMKhQJs5rAeGjta3JX2qEdBS6hS";
186
+ const dependency2 = "co_zP51HdyAVCuRY9ptq5iu8DhMyAb";
187
+ const dependency3 = "co_zGyBniuJmKkcirCKYrccWpjQEFY";
188
+ const dependenciesTreeWithLoop: Record<RawCoID, RawCoID[]> = {
189
+ [coValueIdToLoad]: [dependency1, dependency2],
190
+ [dependency1]: [],
191
+ [dependency2]: [coValueIdToLoad, dependency3],
192
+ [dependency3]: [dependency1],
193
+ };
194
+
195
+ IDBClient.prototype.getCoValue.mockImplementation(
196
+ (coValueId: RawCoID) => ({
197
+ id: coValueId,
198
+ header: coValueHeader,
199
+ rowID: 3,
200
+ }),
201
+ );
202
+
203
+ IDBClient.prototype.getCoValueSessions.mockResolvedValue([]);
204
+
205
+ // Fetch dependencies of the current dependency for the future recursion iterations
206
+ vi.mocked(getDependedOnCoValues).mockImplementation(
207
+ ({ coValueRow }) => dependenciesTreeWithLoop[coValueRow.id] || [],
208
+ );
209
+
210
+ await syncManager.handleSyncMessage(loadMsg);
211
+
212
+ // We send out pairs (known + content) messages only FOUR times - as many as the coValues number
213
+ // and less than amount of interconnected dependencies to loop through in dependenciesTreeWithLoop
214
+ expect(syncManager.sendStateMessage).toBeCalledTimes(4 * 2);
215
+
216
+ const knownExpected = {
217
+ action: "known",
218
+ header: true,
219
+ sessions: {},
220
+ };
221
+
222
+ const contentExpected = {
223
+ action: "content",
224
+ header: expect.any(Object),
225
+ new: {},
226
+ priority: 0,
227
+ };
228
+
229
+ expect(syncManager.sendStateMessage).toHaveBeenNthCalledWith(1, {
230
+ ...knownExpected,
231
+ id: dependency3,
232
+ asDependencyOf: coValueIdToLoad,
233
+ });
234
+ expect(syncManager.sendStateMessage).toHaveBeenNthCalledWith(2, {
235
+ ...contentExpected,
236
+ id: dependency3,
237
+ });
238
+ expect(syncManager.sendStateMessage).toHaveBeenNthCalledWith(3, {
239
+ ...knownExpected,
240
+ id: dependency2,
241
+ asDependencyOf: coValueIdToLoad,
242
+ });
243
+ expect(syncManager.sendStateMessage).toHaveBeenNthCalledWith(4, {
244
+ ...contentExpected,
245
+ id: dependency2,
246
+ });
247
+ expect(syncManager.sendStateMessage).toHaveBeenNthCalledWith(5, {
248
+ ...knownExpected,
249
+ id: dependency1,
250
+ asDependencyOf: coValueIdToLoad,
251
+ });
252
+ expect(syncManager.sendStateMessage).toHaveBeenNthCalledWith(6, {
253
+ ...contentExpected,
254
+ id: dependency1,
255
+ });
256
+ expect(syncManager.sendStateMessage).toHaveBeenNthCalledWith(7, {
257
+ ...knownExpected,
258
+ id: coValueIdToLoad,
259
+ });
260
+ expect(syncManager.sendStateMessage).toHaveBeenNthCalledWith(8, {
261
+ ...contentExpected,
262
+ id: coValueIdToLoad,
263
+ });
264
+ });
265
+ });
266
+
267
+ describe("Handle content incoming message", () => {
268
+ test("Sends correction message for unknown coValue", async () => {
269
+ IDBClient.prototype.getCoValue.mockResolvedValueOnce(undefined);
270
+
271
+ await syncManager.handleSyncMessage({
272
+ ...incomingContentMessage,
273
+ header: undefined,
274
+ } as SyncMessage);
275
+
276
+ expect(syncManager.sendStateMessage).toBeCalledWith({
277
+ action: "known",
278
+ header: false,
279
+ id: coValueIdToLoad,
280
+ isCorrection: true,
281
+ sessions: {},
282
+ });
283
+ });
284
+
285
+ test("Saves new transaction without sending message when IDB has fewer transactions", async () => {
286
+ IDBClient.prototype.getCoValue.mockResolvedValueOnce({
287
+ id: coValueIdToLoad,
288
+ header: coValueHeader,
289
+ rowID: 3,
290
+ });
291
+ IDBClient.prototype.getCoValueSessions.mockResolvedValueOnce([]);
292
+ const msg = {
293
+ ...incomingContentMessage,
294
+ header: undefined,
295
+ } as NewContentMessage;
296
+
297
+ await syncManager.handleSyncMessage(msg);
298
+
299
+ const incomingTxCount = Object.keys(msg.new).reduce(
300
+ (acc, sessionID) =>
301
+ acc + msg.new[sessionID as SessionID]!.newTransactions.length,
302
+ 0,
303
+ );
304
+ expect(IDBClient.prototype.addTransaction).toBeCalledTimes(
305
+ incomingTxCount,
306
+ );
307
+
308
+ expect(syncManager.sendStateMessage).not.toBeCalled();
309
+ });
310
+
311
+ test("Sends correction message when peer sends a message far ahead of our state due to invalid assumption", async () => {
312
+ IDBClient.prototype.getCoValue.mockResolvedValueOnce({
313
+ id: coValueIdToLoad,
314
+ header: coValueHeader,
315
+ rowID: 3,
316
+ });
317
+ IDBClient.prototype.getCoValueSessions.mockResolvedValueOnce(
318
+ sessionsData,
319
+ );
320
+
321
+ const farAheadContentMessage = fixtures[coValueIdToLoad].getContent({
322
+ after: 10000,
323
+ });
324
+ await syncManager.handleSyncMessage(
325
+ farAheadContentMessage as SyncMessage,
326
+ );
327
+
328
+ expect(syncManager.sendStateMessage).toBeCalledWith({
329
+ action: "known",
330
+ header: true,
331
+ id: coValueIdToLoad,
332
+ isCorrection: true,
333
+ sessions: expect.any(Object),
334
+ });
335
+ });
336
+ });
337
+ });
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.test.js","sourceRoot":"","sources":["../../src/tests/index.test.ts"],"names":[],"mappings":"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"}