cojson 0.13.10 β†’ 0.13.12

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 (102) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +15 -0
  3. package/dist/CoValuesStore.d.ts +3 -1
  4. package/dist/CoValuesStore.d.ts.map +1 -1
  5. package/dist/CoValuesStore.js +7 -6
  6. package/dist/CoValuesStore.js.map +1 -1
  7. package/dist/PeerKnownStates.d.ts +5 -25
  8. package/dist/PeerKnownStates.d.ts.map +1 -1
  9. package/dist/PeerKnownStates.js +7 -20
  10. package/dist/PeerKnownStates.js.map +1 -1
  11. package/dist/PeerState.d.ts +14 -9
  12. package/dist/PeerState.d.ts.map +1 -1
  13. package/dist/PeerState.js +51 -8
  14. package/dist/PeerState.js.map +1 -1
  15. package/dist/SyncStateManager.js +2 -2
  16. package/dist/SyncStateManager.js.map +1 -1
  17. package/dist/coValueCore.js +2 -2
  18. package/dist/coValueCore.js.map +1 -1
  19. package/dist/coValueState.d.ts +21 -46
  20. package/dist/coValueState.d.ts.map +1 -1
  21. package/dist/coValueState.js +174 -246
  22. package/dist/coValueState.js.map +1 -1
  23. package/dist/coValues/coList.d.ts +13 -2
  24. package/dist/coValues/coList.d.ts.map +1 -1
  25. package/dist/coValues/coList.js +60 -34
  26. package/dist/coValues/coList.js.map +1 -1
  27. package/dist/coValues/coPlainText.d.ts +45 -0
  28. package/dist/coValues/coPlainText.d.ts.map +1 -1
  29. package/dist/coValues/coPlainText.js +61 -11
  30. package/dist/coValues/coPlainText.js.map +1 -1
  31. package/dist/coValues/group.js +2 -2
  32. package/dist/coValues/group.js.map +1 -1
  33. package/dist/exports.d.ts +2 -4
  34. package/dist/exports.d.ts.map +1 -1
  35. package/dist/exports.js +1 -2
  36. package/dist/exports.js.map +1 -1
  37. package/dist/localNode.d.ts.map +1 -1
  38. package/dist/localNode.js +20 -16
  39. package/dist/localNode.js.map +1 -1
  40. package/dist/sync.d.ts.map +1 -1
  41. package/dist/sync.js +40 -92
  42. package/dist/sync.js.map +1 -1
  43. package/dist/tests/PeerKnownStates.test.js +9 -14
  44. package/dist/tests/PeerKnownStates.test.js.map +1 -1
  45. package/dist/tests/PeerState.test.js +22 -34
  46. package/dist/tests/PeerState.test.js.map +1 -1
  47. package/dist/tests/coList.test.js +63 -0
  48. package/dist/tests/coList.test.js.map +1 -1
  49. package/dist/tests/coPlainText.test.js +66 -11
  50. package/dist/tests/coPlainText.test.js.map +1 -1
  51. package/dist/tests/coValueState.test.js +57 -104
  52. package/dist/tests/coValueState.test.js.map +1 -1
  53. package/dist/tests/group.test.js +1 -2
  54. package/dist/tests/group.test.js.map +1 -1
  55. package/dist/tests/messagesTestUtils.d.ts +4 -1
  56. package/dist/tests/messagesTestUtils.d.ts.map +1 -1
  57. package/dist/tests/messagesTestUtils.js +10 -0
  58. package/dist/tests/messagesTestUtils.js.map +1 -1
  59. package/dist/tests/sync.mesh.test.js +65 -3
  60. package/dist/tests/sync.mesh.test.js.map +1 -1
  61. package/dist/tests/sync.peerReconciliation.test.js +8 -8
  62. package/dist/tests/sync.peerReconciliation.test.js.map +1 -1
  63. package/dist/tests/sync.test.js +6 -4
  64. package/dist/tests/sync.test.js.map +1 -1
  65. package/package.json +1 -1
  66. package/src/CoValuesStore.ts +9 -6
  67. package/src/PeerKnownStates.ts +19 -56
  68. package/src/PeerState.ts +69 -13
  69. package/src/SyncStateManager.ts +2 -2
  70. package/src/coValueCore.ts +2 -2
  71. package/src/coValueState.ts +197 -317
  72. package/src/coValues/coList.ts +84 -44
  73. package/src/coValues/coPlainText.ts +75 -11
  74. package/src/coValues/group.ts +2 -2
  75. package/src/exports.ts +0 -6
  76. package/src/localNode.ts +30 -21
  77. package/src/sync.ts +46 -95
  78. package/src/tests/PeerKnownStates.test.ts +9 -14
  79. package/src/tests/PeerState.test.ts +27 -40
  80. package/src/tests/coList.test.ts +83 -0
  81. package/src/tests/coPlainText.test.ts +81 -11
  82. package/src/tests/coValueState.test.ts +55 -106
  83. package/src/tests/group.test.ts +2 -2
  84. package/src/tests/messagesTestUtils.ts +12 -1
  85. package/src/tests/sync.mesh.test.ts +81 -3
  86. package/src/tests/sync.peerReconciliation.test.ts +8 -8
  87. package/src/tests/sync.test.ts +8 -23
  88. package/dist/storage/FileSystem.d.ts +0 -37
  89. package/dist/storage/FileSystem.d.ts.map +0 -1
  90. package/dist/storage/FileSystem.js +0 -48
  91. package/dist/storage/FileSystem.js.map +0 -1
  92. package/dist/storage/chunksAndKnownStates.d.ts +0 -7
  93. package/dist/storage/chunksAndKnownStates.d.ts.map +0 -1
  94. package/dist/storage/chunksAndKnownStates.js +0 -98
  95. package/dist/storage/chunksAndKnownStates.js.map +0 -1
  96. package/dist/storage/index.d.ts +0 -52
  97. package/dist/storage/index.d.ts.map +0 -1
  98. package/dist/storage/index.js +0 -335
  99. package/dist/storage/index.js.map +0 -1
  100. package/src/storage/FileSystem.ts +0 -113
  101. package/src/storage/chunksAndKnownStates.ts +0 -137
  102. package/src/storage/index.ts +0 -531
package/src/sync.ts CHANGED
@@ -163,7 +163,7 @@ export class SyncManager {
163
163
  }
164
164
 
165
165
  async handleSyncMessage(msg: SyncMessage, peer: PeerState) {
166
- if (peer.erroredCoValues.has(msg.id)) {
166
+ if (this.local.coValuesStore.get(msg.id).isErroredInPeer(peer.id)) {
167
167
  logger.warn(
168
168
  `Skipping message ${msg.action} on errored coValue ${msg.id} from peer ${peer.id}`,
169
169
  );
@@ -223,11 +223,7 @@ export class SyncManager {
223
223
  }
224
224
 
225
225
  peer.toldKnownState.add(id);
226
- peer.optimisticKnownStates.dispatch({
227
- type: "COMBINE_WITH",
228
- id: id,
229
- value: coValue.knownState(),
230
- });
226
+ peer.combineOptimisticWith(id, coValue.knownState());
231
227
  } else if (!peer.toldKnownState.has(id)) {
232
228
  this.trySendToPeer(peer, {
233
229
  action: "known",
@@ -255,8 +251,8 @@ export class SyncManager {
255
251
  for (const id of coValue.getDependedOnCoValues()) {
256
252
  const entry = this.local.coValuesStore.get(id);
257
253
 
258
- if (entry.state.type === "available") {
259
- buildOrderedCoValueList(entry.state.coValue);
254
+ if (entry.isAvailable()) {
255
+ buildOrderedCoValueList(entry.core);
260
256
  }
261
257
  }
262
258
 
@@ -264,31 +260,25 @@ export class SyncManager {
264
260
  };
265
261
 
266
262
  for (const entry of this.local.coValuesStore.getValues()) {
267
- switch (entry.state.type) {
268
- case "unavailable":
269
- // If the coValue is unavailable and we never tried this peer
270
- // we try to load it from the peer
271
- if (!peer.toldKnownState.has(entry.id)) {
272
- await entry.loadFromPeers([peer]).catch((e: unknown) => {
273
- logger.error("Error sending load", { err: e });
274
- });
275
- }
276
- break;
277
- case "available":
278
- const coValue = entry.state.coValue;
279
-
280
- // Build the list of coValues ordered by dependency
281
- // so we can send the load message in the correct order
282
- buildOrderedCoValueList(coValue);
283
- break;
263
+ if (!entry.isAvailable()) {
264
+ // If the coValue is unavailable and we never tried this peer
265
+ // we try to load it from the peer
266
+ if (!peer.toldKnownState.has(entry.id)) {
267
+ await entry.loadFromPeers([peer]).catch((e: unknown) => {
268
+ logger.error("Error sending load", { err: e });
269
+ });
270
+ }
271
+ } else {
272
+ const coValue = entry.core;
273
+
274
+ // Build the list of coValues ordered by dependency
275
+ // so we can send the load message in the correct order
276
+ buildOrderedCoValueList(coValue);
284
277
  }
285
278
 
286
279
  // Fill the missing known states with empty known states
287
280
  if (!peer.optimisticKnownStates.has(entry.id)) {
288
- peer.optimisticKnownStates.dispatch({
289
- type: "SET_AS_EMPTY",
290
- id: entry.id,
291
- });
281
+ peer.setOptimisticKnownState(entry.id, "empty");
292
282
  }
293
283
  }
294
284
 
@@ -403,14 +393,13 @@ export class SyncManager {
403
393
  * This way we can track part of the data loss that may occur when the other peer is restarted
404
394
  *
405
395
  */
406
- peer.dispatchToKnownStates({
407
- type: "SET",
408
- id: msg.id,
409
- value: knownStateIn(msg),
410
- });
396
+ peer.setKnownState(msg.id, knownStateIn(msg));
411
397
  const entry = this.local.coValuesStore.get(msg.id);
412
398
 
413
- if (entry.state.type === "unknown" || entry.state.type === "unavailable") {
399
+ if (
400
+ entry.highLevelState === "unknown" ||
401
+ entry.highLevelState === "unavailable"
402
+ ) {
414
403
  const eligiblePeers = this.getServerAndStoragePeers(peer.id);
415
404
 
416
405
  if (eligiblePeers.length === 0) {
@@ -437,7 +426,7 @@ export class SyncManager {
437
426
  }
438
427
  }
439
428
 
440
- if (entry.state.type === "loading") {
429
+ if (entry.highLevelState === "loading") {
441
430
  // We need to return from handleLoad immediately and wait for the CoValue to be loaded
442
431
  // in a new task, otherwise we might block further incoming content messages that would
443
432
  // resolve the CoValue as available. This can happen when we receive fresh
@@ -467,7 +456,7 @@ export class SyncManager {
467
456
  err: e,
468
457
  });
469
458
  });
470
- } else if (entry.state.type === "available") {
459
+ } else if (entry.isAvailable()) {
471
460
  await this.sendNewContentIncludingDependencies(msg.id, peer);
472
461
  } else {
473
462
  this.trySendToPeer(peer, {
@@ -482,28 +471,17 @@ export class SyncManager {
482
471
  async handleKnownState(msg: KnownStateMessage, peer: PeerState) {
483
472
  const entry = this.local.coValuesStore.get(msg.id);
484
473
 
485
- peer.dispatchToKnownStates({
486
- type: "COMBINE_WITH",
487
- id: msg.id,
488
- value: knownStateIn(msg),
489
- });
474
+ peer.combineWith(msg.id, knownStateIn(msg));
490
475
 
491
476
  // The header is a boolean value that tells us if the other peer do have information about the header.
492
477
  // If it's false in this point it means that the coValue is unavailable on the other peer.
493
- if (entry.state.type !== "available") {
494
- const availableOnPeer = peer.optimisticKnownStates.get(msg.id)?.header;
495
-
496
- if (!availableOnPeer) {
497
- entry.dispatch({
498
- type: "not-found-in-peer",
499
- peerId: peer.id,
500
- });
501
- }
478
+ const availableOnPeer = peer.optimisticKnownStates.get(msg.id)?.header;
502
479
 
503
- return;
480
+ if (!availableOnPeer) {
481
+ entry.markNotFoundInPeer(peer.id);
504
482
  }
505
483
 
506
- if (entry.state.type === "available") {
484
+ if (entry.isAvailable()) {
507
485
  await this.sendNewContentIncludingDependencies(msg.id, peer);
508
486
  }
509
487
  }
@@ -526,23 +504,7 @@ export class SyncManager {
526
504
 
527
505
  let coValue: CoValueCore;
528
506
 
529
- /**
530
- * The new content might come while the coValue is loading or is not loaded yet.
531
- *
532
- * This might happen when we restart the server because:
533
- * - The client known state assumes that the coValue is available on the server
534
- * - The server might not have loaded the coValue yet because it was not requested
535
- *
536
- * In this case we need to load the coValue from the storage or other peers.
537
- *
538
- * If this load fails we send a correction request, because the client has the wrong assumption that
539
- * we have the coValue while we don't.
540
- */
541
- if (entry.state.type !== "available" && !msg.header) {
542
- await this.local.loadCoValueCore(msg.id, peer.id);
543
- }
544
-
545
- if (entry.state.type !== "available") {
507
+ if (!entry.isAvailable()) {
546
508
  if (!msg.header) {
547
509
  this.trySendToPeer(peer, {
548
510
  action: "known",
@@ -560,20 +522,13 @@ export class SyncManager {
560
522
  return;
561
523
  }
562
524
 
563
- peer.dispatchToKnownStates({
564
- type: "UPDATE_HEADER",
565
- id: msg.id,
566
- header: true,
567
- });
525
+ peer.updateHeader(msg.id, true);
568
526
 
569
527
  coValue = new CoValueCore(msg.header, this.local);
570
528
 
571
- entry.dispatch({
572
- type: "available",
573
- coValue,
574
- });
529
+ entry.markAvailable(coValue, peer.id);
575
530
  } else {
576
- coValue = entry.state.coValue;
531
+ coValue = entry.core;
577
532
  }
578
533
 
579
534
  let invalidStateAssumed = false;
@@ -616,20 +571,18 @@ export class SyncManager {
616
571
  id: msg.id,
617
572
  err: result.error,
618
573
  });
619
- peer.erroredCoValues.set(msg.id, result.error);
574
+ entry.markErrored(peer.id, result.error);
620
575
  continue;
621
576
  }
622
577
 
623
578
  this.recordTransactionsSize(newTransactions, peer.role);
624
579
 
625
- peer.dispatchToKnownStates({
626
- type: "UPDATE_SESSION_COUNTER",
627
- id: msg.id,
628
- sessionId: sessionID,
629
- value:
630
- newContentForSession.after +
580
+ peer.updateSessionCounter(
581
+ msg.id,
582
+ sessionID,
583
+ newContentForSession.after +
631
584
  newContentForSession.newTransactions.length,
632
- });
585
+ );
633
586
  }
634
587
 
635
588
  if (invalidStateAssumed) {
@@ -675,11 +628,7 @@ export class SyncManager {
675
628
  }
676
629
 
677
630
  async handleCorrection(msg: KnownStateMessage, peer: PeerState) {
678
- peer.dispatchToKnownStates({
679
- type: "SET",
680
- id: msg.id,
681
- value: knownStateIn(msg),
682
- });
631
+ peer.setKnownState(msg.id, knownStateIn(msg));
683
632
 
684
633
  return this.sendNewContentIncludingDependencies(msg.id, peer);
685
634
  }
@@ -712,7 +661,8 @@ export class SyncManager {
712
661
  async actuallySyncCoValue(coValue: CoValueCore) {
713
662
  for (const peer of this.peersInPriorityOrder()) {
714
663
  if (peer.closed) continue;
715
- if (peer.erroredCoValues.has(coValue.id)) continue;
664
+ if (this.local.coValuesStore.get(coValue.id).isErroredInPeer(peer.id))
665
+ continue;
716
666
 
717
667
  if (peer.optimisticKnownStates.has(coValue.id)) {
718
668
  await this.sendNewContentIncludingDependencies(coValue.id, peer);
@@ -767,7 +717,8 @@ export class SyncManager {
767
717
  const coValues = this.local.coValuesStore.getValues();
768
718
  const validCoValues = Array.from(coValues).filter(
769
719
  (coValue) =>
770
- coValue.state.type === "available" || coValue.state.type === "loading",
720
+ coValue.highLevelState === "available" ||
721
+ coValue.highLevelState === "loading",
771
722
  );
772
723
 
773
724
  return Promise.all(
@@ -9,7 +9,7 @@ describe("PeerKnownStates", () => {
9
9
  const id = "test-id" as RawCoID;
10
10
  const knownState: CoValueKnownState = emptyKnownState(id);
11
11
 
12
- peerKnownStates.dispatch({ type: "SET", id, value: knownState });
12
+ peerKnownStates.set(id, knownState);
13
13
 
14
14
  expect(peerKnownStates.get(id)).toEqual(knownState);
15
15
  expect(peerKnownStates.has(id)).toBe(true);
@@ -19,7 +19,7 @@ describe("PeerKnownStates", () => {
19
19
  const peerKnownStates = new PeerKnownStates();
20
20
  const id = "test-id" as RawCoID;
21
21
 
22
- peerKnownStates.dispatch({ type: "UPDATE_HEADER", id, header: true });
22
+ peerKnownStates.updateHeader(id, true);
23
23
 
24
24
  const result = peerKnownStates.get(id);
25
25
  expect(result?.header).toBe(true);
@@ -30,12 +30,7 @@ describe("PeerKnownStates", () => {
30
30
  const id = "test-id" as RawCoID;
31
31
  const sessionId = "session-1" as SessionID;
32
32
 
33
- peerKnownStates.dispatch({
34
- type: "UPDATE_SESSION_COUNTER",
35
- id,
36
- sessionId,
37
- value: 5,
38
- });
33
+ peerKnownStates.updateSessionCounter(id, sessionId, 5);
39
34
 
40
35
  const result = peerKnownStates.get(id);
41
36
  expect(result?.sessions[sessionId]).toBe(5);
@@ -55,8 +50,8 @@ describe("PeerKnownStates", () => {
55
50
  sessions: { [session2]: 10 },
56
51
  };
57
52
 
58
- peerKnownStates.dispatch({ type: "SET", id, value: initialState });
59
- peerKnownStates.dispatch({ type: "COMBINE_WITH", id, value: combineState });
53
+ peerKnownStates.set(id, initialState);
54
+ peerKnownStates.combineWith(id, combineState);
60
55
 
61
56
  const result = peerKnownStates.get(id);
62
57
  expect(result?.sessions).toEqual({ [session1]: 5, [session2]: 10 });
@@ -71,8 +66,8 @@ describe("PeerKnownStates", () => {
71
66
  sessions: { [sessionId]: 5 },
72
67
  };
73
68
 
74
- peerKnownStates.dispatch({ type: "SET", id, value: initialState });
75
- peerKnownStates.dispatch({ type: "SET_AS_EMPTY", id });
69
+ peerKnownStates.set(id, initialState);
70
+ peerKnownStates.set(id, "empty");
76
71
 
77
72
  const result = peerKnownStates.get(id);
78
73
  expect(result).toEqual(emptyKnownState(id));
@@ -84,7 +79,7 @@ describe("PeerKnownStates", () => {
84
79
  const listener = vi.fn();
85
80
 
86
81
  peerKnownStates.subscribe(listener);
87
- peerKnownStates.dispatch({ type: "SET_AS_EMPTY", id });
82
+ peerKnownStates.set(id, "empty");
88
83
 
89
84
  expect(listener).toHaveBeenCalledWith(id, emptyKnownState(id));
90
85
  });
@@ -97,7 +92,7 @@ describe("PeerKnownStates", () => {
97
92
  const unsubscribe = peerKnownStates.subscribe(listener);
98
93
  unsubscribe();
99
94
 
100
- peerKnownStates.dispatch({ type: "SET_AS_EMPTY", id });
95
+ peerKnownStates.set(id, "empty");
101
96
 
102
97
  expect(listener).not.toHaveBeenCalled();
103
98
  });
@@ -1,8 +1,7 @@
1
1
  import { describe, expect, test, vi } from "vitest";
2
- import { PeerKnownStateActions } from "../PeerKnownStates.js";
3
2
  import { PeerState } from "../PeerState.js";
4
3
  import { CO_VALUE_PRIORITY } from "../priority.js";
5
- import { Peer, SyncMessage } from "../sync.js";
4
+ import { CoValueKnownState, Peer, SyncMessage } from "../sync.js";
6
5
 
7
6
  function setup() {
8
7
  const mockPeer: Peer = {
@@ -146,16 +145,11 @@ describe("PeerState", () => {
146
145
 
147
146
  test("should clone the knownStates into optimisticKnownStates and knownStates when passed as argument", () => {
148
147
  const { peerState, mockPeer } = setup();
149
- const action: PeerKnownStateActions = {
150
- type: "SET",
148
+ peerState.setKnownState("co_z1", {
151
149
  id: "co_z1",
152
- value: {
153
- id: "co_z1",
154
- header: false,
155
- sessions: {},
156
- },
157
- };
158
- peerState.dispatchToKnownStates(action);
150
+ header: false,
151
+ sessions: {},
152
+ });
159
153
 
160
154
  const newPeerState = new PeerState(mockPeer, peerState.knownStates);
161
155
 
@@ -165,25 +159,26 @@ describe("PeerState", () => {
165
159
 
166
160
  test("should dispatch to both states", () => {
167
161
  const { peerState } = setup();
168
- const knownStatesSpy = vi.spyOn(peerState.knownStates, "dispatch");
162
+ const knownStatesSpy = vi.spyOn(peerState._knownStates, "set");
163
+ if (peerState._optimisticKnownStates === "assumeInfallible") {
164
+ throw new Error("Expected normal optimisticKnownStates");
165
+ }
166
+
169
167
  const optimisticKnownStatesSpy = vi.spyOn(
170
- peerState.optimisticKnownStates,
171
- "dispatch",
168
+ peerState._optimisticKnownStates,
169
+ "set",
172
170
  );
173
171
 
174
- const action: PeerKnownStateActions = {
175
- type: "SET",
172
+ const state: CoValueKnownState = {
176
173
  id: "co_z1",
177
- value: {
178
- id: "co_z1",
179
- header: false,
180
- sessions: {},
181
- },
174
+ header: false,
175
+ sessions: {},
182
176
  };
183
- peerState.dispatchToKnownStates(action);
184
177
 
185
- expect(knownStatesSpy).toHaveBeenCalledWith(action);
186
- expect(optimisticKnownStatesSpy).toHaveBeenCalledWith(action);
178
+ peerState.setKnownState("co_z1", state);
179
+
180
+ expect(knownStatesSpy).toHaveBeenCalledWith("co_z1", state);
181
+ expect(optimisticKnownStatesSpy).toHaveBeenCalledWith("co_z1", state);
187
182
  });
188
183
 
189
184
  test("should use same reference for knownStates and optimisticKnownStates for storage peers", () => {
@@ -204,28 +199,20 @@ describe("PeerState", () => {
204
199
  expect(peerState.knownStates).toBe(peerState.optimisticKnownStates);
205
200
 
206
201
  // Verify that dispatching only updates one state
207
- const knownStatesSpy = vi.spyOn(peerState.knownStates, "dispatch");
208
- const optimisticKnownStatesSpy = vi.spyOn(
209
- peerState.optimisticKnownStates,
210
- "dispatch",
211
- );
202
+ const knownStatesSpy = vi.spyOn(peerState._knownStates, "set");
203
+ expect(peerState._optimisticKnownStates).toBe("assumeInfallible");
212
204
 
213
- const action: PeerKnownStateActions = {
214
- type: "SET",
205
+ const state: CoValueKnownState = {
215
206
  id: "co_z1",
216
- value: {
217
- id: "co_z1",
218
- header: false,
219
- sessions: {},
220
- },
207
+ header: false,
208
+ sessions: {},
221
209
  };
222
- peerState.dispatchToKnownStates(action);
210
+
211
+ peerState.setKnownState("co_z1", state);
223
212
 
224
213
  // Only one dispatch should happen since they're the same reference
225
214
  expect(knownStatesSpy).toHaveBeenCalledTimes(1);
226
- expect(knownStatesSpy).toHaveBeenCalledWith(action);
227
- expect(optimisticKnownStatesSpy).toHaveBeenCalledTimes(1);
228
- expect(optimisticKnownStatesSpy).toHaveBeenCalledWith(action);
215
+ expect(knownStatesSpy).toHaveBeenCalledWith("co_z1", state);
229
216
  });
230
217
 
231
218
  test("should use separate references for knownStates and optimisticKnownStates for non-storage peers", () => {
@@ -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
+ });