cojson 0.13.10 → 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 (42) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +9 -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/coValues/coList.d.ts +13 -2
  12. package/dist/coValues/coList.d.ts.map +1 -1
  13. package/dist/coValues/coList.js +60 -34
  14. package/dist/coValues/coList.js.map +1 -1
  15. package/dist/coValues/coPlainText.d.ts +45 -0
  16. package/dist/coValues/coPlainText.d.ts.map +1 -1
  17. package/dist/coValues/coPlainText.js +61 -11
  18. package/dist/coValues/coPlainText.js.map +1 -1
  19. package/dist/sync.d.ts.map +1 -1
  20. package/dist/sync.js +8 -51
  21. package/dist/sync.js.map +1 -1
  22. package/dist/tests/PeerKnownStates.test.js +9 -14
  23. package/dist/tests/PeerKnownStates.test.js.map +1 -1
  24. package/dist/tests/PeerState.test.js +22 -34
  25. package/dist/tests/PeerState.test.js.map +1 -1
  26. package/dist/tests/coList.test.js +63 -0
  27. package/dist/tests/coList.test.js.map +1 -1
  28. package/dist/tests/coPlainText.test.js +66 -11
  29. package/dist/tests/coPlainText.test.js.map +1 -1
  30. package/dist/tests/sync.mesh.test.js +65 -3
  31. package/dist/tests/sync.mesh.test.js.map +1 -1
  32. package/package.json +1 -1
  33. package/src/PeerKnownStates.ts +19 -56
  34. package/src/PeerState.ts +70 -12
  35. package/src/coValues/coList.ts +84 -44
  36. package/src/coValues/coPlainText.ts +75 -11
  37. package/src/sync.ts +11 -52
  38. package/src/tests/PeerKnownStates.test.ts +9 -14
  39. package/src/tests/PeerState.test.ts +27 -40
  40. package/src/tests/coList.test.ts +83 -0
  41. package/src/tests/coPlainText.test.ts +81 -11
  42. package/src/tests/sync.mesh.test.ts +81 -3
@@ -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
+ });
@@ -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();