cojson 0.13.7 → 0.13.11

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.
Files changed (57) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +15 -0
  3. package/dist/PeerKnownStates.d.ts +5 -25
  4. package/dist/PeerKnownStates.d.ts.map +1 -1
  5. package/dist/PeerKnownStates.js +7 -20
  6. package/dist/PeerKnownStates.js.map +1 -1
  7. package/dist/PeerState.d.ts +14 -7
  8. package/dist/PeerState.d.ts.map +1 -1
  9. package/dist/PeerState.js +51 -7
  10. package/dist/PeerState.js.map +1 -1
  11. package/dist/coValueCore.d.ts +3 -1
  12. package/dist/coValueCore.d.ts.map +1 -1
  13. package/dist/coValueCore.js +25 -8
  14. package/dist/coValueCore.js.map +1 -1
  15. package/dist/coValues/coList.d.ts +13 -2
  16. package/dist/coValues/coList.d.ts.map +1 -1
  17. package/dist/coValues/coList.js +60 -34
  18. package/dist/coValues/coList.js.map +1 -1
  19. package/dist/coValues/coPlainText.d.ts +45 -0
  20. package/dist/coValues/coPlainText.d.ts.map +1 -1
  21. package/dist/coValues/coPlainText.js +61 -11
  22. package/dist/coValues/coPlainText.js.map +1 -1
  23. package/dist/sync.d.ts.map +1 -1
  24. package/dist/sync.js +8 -51
  25. package/dist/sync.js.map +1 -1
  26. package/dist/tests/PeerKnownStates.test.js +9 -14
  27. package/dist/tests/PeerKnownStates.test.js.map +1 -1
  28. package/dist/tests/PeerState.test.js +22 -34
  29. package/dist/tests/PeerState.test.js.map +1 -1
  30. package/dist/tests/coList.test.js +63 -0
  31. package/dist/tests/coList.test.js.map +1 -1
  32. package/dist/tests/coPlainText.test.js +66 -11
  33. package/dist/tests/coPlainText.test.js.map +1 -1
  34. package/dist/tests/coValueCore.test.js +15 -2
  35. package/dist/tests/coValueCore.test.js.map +1 -1
  36. package/dist/tests/sync.mesh.test.js +65 -3
  37. package/dist/tests/sync.mesh.test.js.map +1 -1
  38. package/dist/tests/sync.peerReconciliation.test.js +36 -0
  39. package/dist/tests/sync.peerReconciliation.test.js.map +1 -1
  40. package/dist/tests/testUtils.d.ts.map +1 -1
  41. package/dist/tests/testUtils.js +3 -2
  42. package/dist/tests/testUtils.js.map +1 -1
  43. package/package.json +1 -1
  44. package/src/PeerKnownStates.ts +19 -56
  45. package/src/PeerState.ts +70 -12
  46. package/src/coValueCore.ts +36 -7
  47. package/src/coValues/coList.ts +84 -44
  48. package/src/coValues/coPlainText.ts +75 -11
  49. package/src/sync.ts +11 -52
  50. package/src/tests/PeerKnownStates.test.ts +9 -14
  51. package/src/tests/PeerState.test.ts +27 -40
  52. package/src/tests/coList.test.ts +83 -0
  53. package/src/tests/coPlainText.test.ts +81 -11
  54. package/src/tests/coValueCore.test.ts +20 -2
  55. package/src/tests/sync.mesh.test.ts +81 -3
  56. package/src/tests/sync.peerReconciliation.test.ts +50 -0
  57. package/src/tests/testUtils.ts +4 -2
@@ -95,6 +95,76 @@ test("appendItems add an array of items at the end of the list", () => {
95
95
  expect(content.toJSON()).toEqual(["hello", "world", "hooray", "universe"]);
96
96
  });
97
97
 
98
+ test("appendItems at index", () => {
99
+ const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
100
+
101
+ const coValue = node.createCoValue({
102
+ type: "colist",
103
+ ruleset: { type: "unsafeAllowAll" },
104
+ meta: null,
105
+ ...Crypto.createdNowUnique(),
106
+ });
107
+
108
+ const content = expectList(coValue.getCurrentContent());
109
+
110
+ content.append("first", 0, "trusting");
111
+ content.append("second", 0, "trusting");
112
+ expect(content.toJSON()).toEqual(["first", "second"]);
113
+
114
+ content.appendItems(["third", "fourth"], 1, "trusting");
115
+ expect(content.toJSON()).toEqual(["first", "second", "third", "fourth"]);
116
+
117
+ content.appendItems(["hello", "world"], 0, "trusting");
118
+ expect(content.toJSON()).toEqual([
119
+ "first",
120
+ "hello",
121
+ "world",
122
+ "second",
123
+ "third",
124
+ "fourth",
125
+ ]);
126
+ });
127
+
128
+ test("appendItems at index", () => {
129
+ const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
130
+
131
+ const coValue = node.createCoValue({
132
+ type: "colist",
133
+ ruleset: { type: "unsafeAllowAll" },
134
+ meta: null,
135
+ ...Crypto.createdNowUnique(),
136
+ });
137
+
138
+ const content = expectList(coValue.getCurrentContent());
139
+
140
+ content.append("first", 0, "trusting");
141
+ expect(content.toJSON()).toEqual(["first"]);
142
+
143
+ content.appendItems(["second"], 0, "trusting");
144
+ expect(content.toJSON()).toEqual(["first", "second"]);
145
+
146
+ content.appendItems(["third"], 1, "trusting");
147
+ expect(content.toJSON()).toEqual(["first", "second", "third"]);
148
+ });
149
+
150
+ test("appendItems with negative index", () => {
151
+ const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
152
+
153
+ const coValue = node.createCoValue({
154
+ type: "colist",
155
+ ruleset: { type: "unsafeAllowAll" },
156
+ meta: null,
157
+ ...Crypto.createdNowUnique(),
158
+ });
159
+
160
+ const content = expectList(coValue.getCurrentContent());
161
+
162
+ content.append("hello", 0, "trusting");
163
+ expect(content.toJSON()).toEqual(["hello"]);
164
+ content.appendItems(["world", "hooray", "universe"], -1, "trusting");
165
+ expect(content.toJSON()).toEqual(["hello", "world", "hooray", "universe"]);
166
+ });
167
+
98
168
  test("Can push into empty list", () => {
99
169
  const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
100
170
 
@@ -151,3 +221,16 @@ test("Items prepended to start appear with latest first", () => {
151
221
 
152
222
  expect(content.toJSON()).toEqual(["third", "second", "first"]);
153
223
  });
224
+
225
+ test("should handle large lists", () => {
226
+ const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
227
+
228
+ const group = node.createGroup();
229
+ const coValue = group.createList();
230
+
231
+ for (let i = 0; i < 8_000; i++) {
232
+ coValue.append(`item ${i}`, undefined, "trusting");
233
+ }
234
+
235
+ expect(coValue.toJSON().length).toEqual(8_000);
236
+ });
@@ -71,25 +71,23 @@ test("Can insert and delete in CoPlainText", () => {
71
71
  content.insertAfter(0, "hello", "trusting");
72
72
  expect(content.toString()).toEqual("hello");
73
73
 
74
- content.insertAfter(5, " world", "trusting");
74
+ content.insertAfter(4, " world", "trusting");
75
75
  expect(content.toString()).toEqual("hello world");
76
76
 
77
- content.insertAfter(0, "Hello, ", "trusting");
77
+ content.insertBefore(0, "Hello, ", "trusting");
78
78
  expect(content.toString()).toEqual("Hello, hello world");
79
79
 
80
- console.log("first delete");
81
80
  content.deleteRange({ from: 6, to: 12 }, "trusting");
82
81
  expect(content.toString()).toEqual("Hello, world");
83
82
 
84
- content.insertAfter(2, "😍", "trusting");
83
+ content.insertBefore(2, "😍", "trusting");
85
84
  expect(content.toString()).toEqual("He😍llo, world");
86
85
 
87
- console.log("second delete");
88
86
  content.deleteRange({ from: 2, to: 4 }, "trusting");
89
87
  expect(content.toString()).toEqual("Hello, world");
90
88
  });
91
89
 
92
- test("Multiple items appended after start appear in correct order", () => {
90
+ test("Multiple items inserted appear in correct order", () => {
93
91
  const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
94
92
 
95
93
  const coValue = node.createCoValue({
@@ -101,10 +99,10 @@ test("Multiple items appended after start appear in correct order", () => {
101
99
 
102
100
  const content = expectPlainText(coValue.getCurrentContent());
103
101
 
104
- // Add multiple items in a single transaction, all after start
102
+ // Add multiple items in sequence
105
103
  content.insertAfter(0, "h", "trusting");
106
- content.insertAfter(1, "e", "trusting");
107
- content.insertAfter(2, "y", "trusting");
104
+ content.insertAfter(0, "e", "trusting");
105
+ content.insertAfter(1, "y", "trusting");
108
106
 
109
107
  // They should appear in insertion order (hey), not reversed (yeh)
110
108
  expect(content.toString()).toEqual("hey");
@@ -124,10 +122,82 @@ test("Items inserted at start appear with latest first", () => {
124
122
 
125
123
  // Insert multiple items at the start
126
124
  content.insertAfter(0, "first", "trusting");
127
- content.insertAfter(0, "second", "trusting");
128
- content.insertAfter(0, "third", "trusting");
125
+ content.insertBefore(0, "second", "trusting");
126
+ content.insertBefore(0, "third", "trusting");
129
127
 
130
128
  // They should appear in reverse chronological order
131
129
  // because newer items should appear before older items
132
130
  expect(content.toString()).toEqual("thirdsecondfirst");
133
131
  });
132
+
133
+ test("Handles different locales correctly", () => {
134
+ const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
135
+
136
+ // Test with explicit locale in meta
137
+ const coValueJa = node.createCoValue({
138
+ type: "coplaintext",
139
+ ruleset: { type: "unsafeAllowAll" },
140
+ meta: { locale: "ja-JP" },
141
+ ...Crypto.createdNowUnique(),
142
+ });
143
+
144
+ const contentJa = expectPlainText(coValueJa.getCurrentContent());
145
+ contentJa.insertAfter(0, "こんにちは", "trusting");
146
+ expect(contentJa.toString()).toEqual("こんにちは");
147
+
148
+ // Test browser locale fallback
149
+ vi.stubGlobal("navigator", { language: "fr-FR" });
150
+
151
+ const coValueBrowser = node.createCoValue({
152
+ type: "coplaintext",
153
+ ruleset: { type: "unsafeAllowAll" },
154
+ meta: null,
155
+ ...Crypto.createdNowUnique(),
156
+ });
157
+
158
+ const contentBrowser = expectPlainText(coValueBrowser.getCurrentContent());
159
+ contentBrowser.insertAfter(0, "bonjour", "trusting");
160
+ expect(contentBrowser.toString()).toEqual("bonjour");
161
+
162
+ // Test fallback to 'en' when no navigator
163
+ vi.stubGlobal("navigator", undefined);
164
+
165
+ const coValueFallback = node.createCoValue({
166
+ type: "coplaintext",
167
+ ruleset: { type: "unsafeAllowAll" },
168
+ meta: null,
169
+ ...Crypto.createdNowUnique(),
170
+ });
171
+
172
+ const contentFallback = expectPlainText(coValueFallback.getCurrentContent());
173
+ contentFallback.insertAfter(0, "hello", "trusting");
174
+ expect(contentFallback.toString()).toEqual("hello");
175
+ });
176
+
177
+ test("insertBefore and insertAfter work as expected", () => {
178
+ const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
179
+ const coValue = node.createCoValue({
180
+ type: "coplaintext",
181
+ ruleset: { type: "unsafeAllowAll" },
182
+ meta: null,
183
+ ...Crypto.createdNowUnique(),
184
+ });
185
+
186
+ const content = expectPlainText(coValue.getCurrentContent());
187
+
188
+ // Insert 'h' at start
189
+ content.insertBefore(0, "h", "trusting"); // "h"
190
+ expect(content.toString()).toEqual("h");
191
+
192
+ // Insert 'e' after 'h'
193
+ content.insertAfter(0, "e", "trusting"); // "he"
194
+ expect(content.toString()).toEqual("he");
195
+
196
+ // Insert 'y' after 'e'
197
+ content.insertAfter(1, "y", "trusting"); // "hey"
198
+ expect(content.toString()).toEqual("hey");
199
+
200
+ // Insert '!' at start
201
+ content.insertBefore(0, "!", "trusting"); // "!hey"
202
+ expect(content.toString()).toEqual("!hey");
203
+ });
@@ -287,12 +287,30 @@ test("creating a coValue with a group should't trigger automatically a content c
287
287
  });
288
288
 
289
289
  // It's called once for the group and never for the coValue
290
- expect(getCurrentContentSpy).toHaveBeenCalledTimes(1);
291
- expect(groupSpy).toHaveBeenCalledTimes(1);
290
+ expect(getCurrentContentSpy).toHaveBeenCalledTimes(0);
291
+ expect(groupSpy).toHaveBeenCalledTimes(0);
292
292
 
293
293
  getCurrentContentSpy.mockRestore();
294
294
  });
295
295
 
296
+ test("loading a coValue core without having the owner group available doesn't crash", () => {
297
+ const [account, sessionID] = randomAnonymousAccountAndSessionID();
298
+ const node = new LocalNode(account, sessionID, Crypto);
299
+
300
+ const otherNode = createTestNode();
301
+
302
+ const group = otherNode.createGroup();
303
+
304
+ const coValue = node.createCoValue({
305
+ type: "costream",
306
+ ruleset: { type: "ownedByGroup", group: group.id },
307
+ meta: null,
308
+ ...Crypto.createdNowUnique(),
309
+ });
310
+
311
+ expect(coValue.id).toBeDefined();
312
+ });
313
+
296
314
  test("listeners are notified even if the previous listener threw an error", async () => {
297
315
  const { node1, node2 } = await createTwoConnectedNodes("server", "server");
298
316
 
@@ -1,5 +1,6 @@
1
1
  import { beforeEach, describe, expect, test, vi } from "vitest";
2
2
 
3
+ import { expectMap } from "../coValue";
3
4
  import {
4
5
  SyncMessagesLog,
5
6
  loadCoValueOrFail,
@@ -8,9 +9,7 @@ import {
8
9
  } from "./testUtils";
9
10
 
10
11
  function setupMesh() {
11
- const coreServer = setupTestNode({
12
- isSyncServer: true,
13
- });
12
+ const coreServer = setupTestNode();
14
13
 
15
14
  coreServer.addStoragePeer({
16
15
  ourName: "core",
@@ -20,12 +19,14 @@ function setupMesh() {
20
19
  edgeItaly.connectToSyncServer({
21
20
  ourName: "edge-italy",
22
21
  syncServerName: "core",
22
+ syncServer: coreServer.node,
23
23
  });
24
24
 
25
25
  const edgeFrance = setupTestNode();
26
26
  edgeFrance.connectToSyncServer({
27
27
  ourName: "edge-france",
28
28
  syncServerName: "core",
29
+ syncServer: coreServer.node,
29
30
  });
30
31
 
31
32
  return { coreServer, edgeItaly, edgeFrance };
@@ -161,6 +162,83 @@ describe("multiple clients syncing with the a cloud-like server mesh", () => {
161
162
  `);
162
163
  });
163
164
 
165
+ test("syncs corrections from multiple peers", async () => {
166
+ const client = setupTestNode();
167
+
168
+ client.connectToSyncServer({
169
+ syncServerName: "edge-italy",
170
+ syncServer: mesh.edgeItaly.node,
171
+ });
172
+
173
+ const group = mesh.edgeItaly.node.createGroup();
174
+ group.addMember("everyone", "writer");
175
+
176
+ const map = group.createMap({
177
+ fromServer: "initial",
178
+ fromClient: "initial",
179
+ });
180
+
181
+ // Load the coValue on the client
182
+ const mapOnClient = await loadCoValueOrFail(client.node, map.id);
183
+ const mapOnCoreServer = await loadCoValueOrFail(
184
+ mesh.coreServer.node,
185
+ map.id,
186
+ );
187
+
188
+ // Forcefully delete the coValue from the edge (simulating some data loss)
189
+ mesh.edgeItaly.node.coValuesStore.coValues.delete(map.id);
190
+
191
+ mapOnClient.set("fromClient", "updated", "trusting");
192
+ mapOnCoreServer.set("fromServer", "updated", "trusting");
193
+
194
+ await waitFor(() => {
195
+ const coValue = expectMap(
196
+ mesh.edgeItaly.node.expectCoValueLoaded(map.id).getCurrentContent(),
197
+ );
198
+ expect(coValue.get("fromServer")).toEqual("updated");
199
+ expect(coValue.get("fromClient")).toEqual("updated");
200
+ });
201
+
202
+ expect(
203
+ SyncMessagesLog.getMessages({
204
+ Group: group.core,
205
+ Map: map.core,
206
+ }),
207
+ ).toMatchInlineSnapshot(`
208
+ [
209
+ "client -> edge-italy | LOAD Map sessions: empty",
210
+ "edge-italy -> core | CONTENT Group header: true new: After: 0 New: 5",
211
+ "edge-italy -> client | CONTENT Group header: true new: After: 0 New: 5",
212
+ "core -> edge-italy | KNOWN Group sessions: header/5",
213
+ "client -> edge-italy | KNOWN Group sessions: header/5",
214
+ "core -> storage | CONTENT Group header: true new: After: 0 New: 5",
215
+ "edge-italy -> core | CONTENT Map header: true new: After: 0 New: 1",
216
+ "edge-italy -> client | CONTENT Map header: true new: After: 0 New: 1",
217
+ "storage -> core | KNOWN Group sessions: header/5",
218
+ "core -> edge-italy | KNOWN Map sessions: header/1",
219
+ "core -> storage | CONTENT Map header: true new: After: 0 New: 1",
220
+ "client -> edge-italy | KNOWN Map sessions: header/1",
221
+ "storage -> core | KNOWN Map sessions: header/1",
222
+ "client -> edge-italy | CONTENT Map header: false new: After: 0 New: 1",
223
+ "core -> storage | CONTENT Map header: false new: After: 0 New: 1",
224
+ "edge-italy -> client | KNOWN CORRECTION Map sessions: empty",
225
+ "storage -> core | KNOWN Map sessions: header/2",
226
+ "core -> edge-italy | CONTENT Map header: false new: After: 0 New: 1",
227
+ "client -> edge-italy | CONTENT Map header: true new: After: 0 New: 1 | After: 0 New: 1",
228
+ "edge-italy -> core | KNOWN CORRECTION Map sessions: empty",
229
+ "edge-italy -> client | KNOWN Map sessions: header/2",
230
+ "core -> edge-italy | CONTENT Map header: true new: After: 0 New: 1 | After: 0 New: 1",
231
+ "edge-italy -> core | CONTENT Map header: false new: After: 0 New: 1",
232
+ "edge-italy -> client | CONTENT Map header: false new: After: 0 New: 1",
233
+ "core -> edge-italy | KNOWN Map sessions: header/3",
234
+ "edge-italy -> core | KNOWN Map sessions: header/3",
235
+ "client -> edge-italy | KNOWN Map sessions: header/3",
236
+ "core -> storage | CONTENT Map header: false new: After: 0 New: 1",
237
+ "storage -> core | KNOWN Map sessions: header/3",
238
+ ]
239
+ `);
240
+ });
241
+
164
242
  test("sync of changes of a coValue with bad signatures should be blocked", async () => {
165
243
  const italianClient = setupTestNode();
166
244
  const frenchClient = setupTestNode();
@@ -1,6 +1,7 @@
1
1
  import { assert, beforeEach, describe, expect, test } from "vitest";
2
2
  import { expectMap } from "../coValue";
3
3
  import { WasmCrypto } from "../crypto/WasmCrypto";
4
+ import { CoValueCore, RawCoMap } from "../exports";
4
5
  import { LocalNode } from "../localNode";
5
6
  import { toSimplifiedMessages } from "./messagesTestUtils";
6
7
  import {
@@ -156,6 +157,55 @@ describe("peer reconciliation", () => {
156
157
  `);
157
158
  });
158
159
 
160
+ test("correctly handle server restarts in the middle of a sync", async () => {
161
+ const client = setupTestNode();
162
+
163
+ const group = client.node.createGroup();
164
+ const map = group.createMap();
165
+
166
+ map.set("hello", "world", "trusting");
167
+
168
+ await map.core.waitForSync();
169
+
170
+ jazzCloud.restart();
171
+ SyncMessagesLog.clear();
172
+ client.connectToSyncServer();
173
+
174
+ map.set("hello", "updated", "trusting");
175
+
176
+ await new Promise((resolve) => setTimeout(resolve, 0));
177
+
178
+ client.connectToSyncServer();
179
+
180
+ await waitFor(() => {
181
+ const mapOnSyncServer = jazzCloud.node.coValuesStore.get(map.id);
182
+
183
+ expect(mapOnSyncServer.state.type).toBe("available");
184
+ });
185
+
186
+ expect(
187
+ SyncMessagesLog.getMessages({
188
+ Group: group.core,
189
+ Map: map.core,
190
+ }),
191
+ ).toMatchInlineSnapshot(`
192
+ [
193
+ "client -> server | LOAD Group sessions: header/3",
194
+ "server -> client | KNOWN Group sessions: empty",
195
+ "client -> server | LOAD Map sessions: header/2",
196
+ "server -> client | KNOWN Map sessions: empty",
197
+ "client -> server | CONTENT Group header: true new: After: 0 New: 3",
198
+ "server -> client | KNOWN Group sessions: header/3",
199
+ "client -> server | CONTENT Map header: true new: After: 0 New: 2",
200
+ "server -> client | KNOWN Map sessions: header/2",
201
+ "client -> server | LOAD Group sessions: header/3",
202
+ "server -> client | KNOWN Group sessions: header/3",
203
+ "client -> server | LOAD Map sessions: header/2",
204
+ "server -> client | KNOWN Map sessions: header/2",
205
+ ]
206
+ `);
207
+ });
208
+
159
209
  test.skip("handle peer reconnections with data loss", async () => {
160
210
  const client = setupTestNode();
161
211
 
@@ -472,13 +472,13 @@ export function setupTestNode(
472
472
  connectToSyncServer();
473
473
  }
474
474
 
475
- return {
475
+ const ctx = {
476
476
  node,
477
477
  connectToSyncServer,
478
478
  addStoragePeer,
479
479
  restart: () => {
480
480
  node.gracefulShutdown();
481
- node = new LocalNode(admin, session, Crypto);
481
+ ctx.node = node = new LocalNode(admin, session, Crypto);
482
482
 
483
483
  if (opts.isSyncServer) {
484
484
  syncServer.current = node;
@@ -487,6 +487,8 @@ export function setupTestNode(
487
487
  return node;
488
488
  },
489
489
  };
490
+
491
+ return ctx;
490
492
  }
491
493
 
492
494
  export async function setupTestAccount(