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
@@ -2,6 +2,7 @@ import { CoID, RawCoValue } from "../coValue.js";
2
2
  import { CoValueCore } from "../coValueCore.js";
3
3
  import { AgentID, SessionID, TransactionID } from "../ids.js";
4
4
  import { JsonObject, JsonValue } from "../jsonValue.js";
5
+ import { CoValueKnownState } from "../sync.js";
5
6
  import { accountOrAgentIDfromSessionID } from "../typeUtils/accountOrAgentIDfromSessionID.js";
6
7
  import { isCoValue } from "../typeUtils/isCoValue.js";
7
8
  import { RawAccountID } from "./account.js";
@@ -81,6 +82,8 @@ export class RawCoListView<
81
82
  madeAt: number;
82
83
  opID: OpID;
83
84
  }[];
85
+ /** @internal */
86
+ knownTransactions: CoValueKnownState["sessions"];
84
87
 
85
88
  /** @internal */
86
89
  constructor(core: CoValueCore) {
@@ -95,12 +98,24 @@ export class RawCoListView<
95
98
  this.deletionsByInsertion = {};
96
99
  this.afterStart = [];
97
100
  this.beforeEnd = [];
101
+ this.knownTransactions = {};
102
+
103
+ this.processNewTransactions();
104
+ }
105
+
106
+ processNewTransactions() {
107
+ const newTransactions = this.core.getValidTransactions({
108
+ ignorePrivateTransactions: false,
109
+ knownTransactions: this.knownTransactions,
110
+ });
98
111
 
99
- for (const {
100
- txID,
101
- changes,
102
- madeAt,
103
- } of this.core.getValidSortedTransactions()) {
112
+ if (newTransactions.length === 0) {
113
+ return;
114
+ }
115
+
116
+ this._cachedEntries = undefined;
117
+
118
+ for (const { txID, changes, madeAt } of newTransactions) {
104
119
  for (const [changeIdx, changeUntyped] of changes.entries()) {
105
120
  const change = changeUntyped as ListOpPayload<Item>;
106
121
 
@@ -192,6 +207,11 @@ export class RawCoListView<
192
207
  );
193
208
  }
194
209
  }
210
+
211
+ this.knownTransactions[txID.sessionID] = Math.max(
212
+ this.knownTransactions[txID.sessionID] ?? 0,
213
+ txID.txIndex,
214
+ );
195
215
  }
196
216
  }
197
217
 
@@ -279,30 +299,52 @@ export class RawCoListView<
279
299
  opID: OpID;
280
300
  }[],
281
301
  ) {
282
- const entry =
283
- this.insertions[opID.sessionID]?.[opID.txIndex]?.[opID.changeIdx];
302
+ const todo = [opID]; // a stack with the next item to do at the end
303
+ const predecessorsVisited = new Set<OpID>();
284
304
 
285
- if (!entry) {
286
- throw new Error("Missing op " + opID);
287
- }
288
- for (const predecessor of entry.predecessors) {
289
- this.fillArrayFromOpID(predecessor, arr);
290
- }
291
- const deleted =
292
- (this.deletionsByInsertion[opID.sessionID]?.[opID.txIndex]?.[
293
- opID.changeIdx
294
- ]?.length || 0) > 0;
295
- if (!deleted) {
296
- arr.push({
297
- value: entry.value,
298
- madeAt: entry.madeAt,
299
- opID,
300
- });
301
- }
302
- // traverse successors in reverse for correct insertion behavior
303
- for (let i = entry.successors.length - 1; i >= 0; i--) {
304
- const successor = entry.successors[i]!;
305
- this.fillArrayFromOpID(successor, arr);
305
+ while (todo.length > 0) {
306
+ const currentOpID = todo[todo.length - 1]!;
307
+
308
+ const entry =
309
+ this.insertions[currentOpID.sessionID]?.[currentOpID.txIndex]?.[
310
+ currentOpID.changeIdx
311
+ ];
312
+
313
+ if (!entry) {
314
+ throw new Error("Missing op " + currentOpID);
315
+ }
316
+
317
+ const shouldTraversePredecessors =
318
+ entry.predecessors.length > 0 && !predecessorsVisited.has(currentOpID);
319
+
320
+ // We navigate the predecessors before processing the current opID in the list
321
+ if (shouldTraversePredecessors) {
322
+ for (let i = entry.predecessors.length - 1; i >= 0; i--) {
323
+ todo.push(entry.predecessors[i]!);
324
+ }
325
+ predecessorsVisited.add(currentOpID);
326
+ } else {
327
+ // Remove the current opID from the todo stack to consider it processed.
328
+ todo.pop();
329
+
330
+ const deleted =
331
+ (this.deletionsByInsertion[currentOpID.sessionID]?.[
332
+ currentOpID.txIndex
333
+ ]?.[currentOpID.changeIdx]?.length || 0) > 0;
334
+
335
+ if (!deleted) {
336
+ arr.push({
337
+ value: entry.value,
338
+ madeAt: entry.madeAt,
339
+ opID: currentOpID,
340
+ });
341
+ }
342
+
343
+ // traverse successors in reverse for correct insertion behavior
344
+ for (const successor of entry.successors) {
345
+ todo.push(successor);
346
+ }
347
+ }
306
348
  }
307
349
  }
308
350
 
@@ -410,6 +452,15 @@ export class RawCoList<
410
452
  this.appendItems([item], after, privacy);
411
453
  }
412
454
 
455
+ /**
456
+ * Appends `items` to the list at index `after`. If `after` is negative, it is treated as `0`.
457
+ *
458
+ * If `privacy` is `"private"` **(default)**, `items` are encrypted in the transaction, only readable by other members of the group this `CoList` belongs to. Not even sync servers can see the content in plaintext.
459
+ *
460
+ * If `privacy` is `"trusting"`, `items` are stored in plaintext in the transaction, visible to everyone who gets a hold of it, including sync servers.
461
+ *
462
+ * @category 2. Editing
463
+ */
413
464
  appendItems(
414
465
  items: Item[],
415
466
  after?: number,
@@ -421,7 +472,7 @@ export class RawCoList<
421
472
  ? entries.length > 0
422
473
  ? entries.length - 1
423
474
  : 0
424
- : after;
475
+ : Math.max(0, after);
425
476
  let opIDBefore: OpID | "start";
426
477
  if (entries.length > 0) {
427
478
  const entryBefore = entries[after];
@@ -450,7 +501,7 @@ export class RawCoList<
450
501
 
451
502
  this.core.makeTransaction(changes, privacy);
452
503
 
453
- this.rebuildFromCore();
504
+ this.processNewTransactions();
454
505
  }
455
506
 
456
507
  /**
@@ -497,7 +548,7 @@ export class RawCoList<
497
548
  privacy,
498
549
  );
499
550
 
500
- this.rebuildFromCore();
551
+ this.processNewTransactions();
501
552
  }
502
553
 
503
554
  /** Deletes the item at index `at`.
@@ -524,7 +575,7 @@ export class RawCoList<
524
575
  privacy,
525
576
  );
526
577
 
527
- this.rebuildFromCore();
578
+ this.processNewTransactions();
528
579
  }
529
580
 
530
581
  replace(
@@ -552,17 +603,6 @@ export class RawCoList<
552
603
  ],
553
604
  privacy,
554
605
  );
555
- this.rebuildFromCore();
556
- }
557
-
558
- /** @internal */
559
- rebuildFromCore() {
560
- const listAfter = new RawCoList(this.core) as this;
561
-
562
- this.afterStart = listAfter.afterStart;
563
- this.beforeEnd = listAfter.beforeEnd;
564
- this.insertions = listAfter.insertions;
565
- this.deletionsByInsertion = listAfter.deletionsByInsertion;
566
- this._cachedEntries = undefined;
606
+ this.processNewTransactions();
567
607
  }
568
608
  }
@@ -2,6 +2,12 @@ import { CoValueCore } from "../coValueCore.js";
2
2
  import { JsonObject } from "../jsonValue.js";
3
3
  import { DeletionOpPayload, OpID, RawCoList } from "./coList.js";
4
4
 
5
+ declare const navigator:
6
+ | {
7
+ language: string;
8
+ }
9
+ | undefined;
10
+
5
11
  export type StringifiedOpID = string & { __stringifiedOpID: true };
6
12
 
7
13
  export function stringifyOpID(opID: OpID): StringifiedOpID {
@@ -15,6 +21,33 @@ type PlaintextIdxMapping = {
15
21
  idxBeforeOpID: { [opID: StringifiedOpID]: number };
16
22
  };
17
23
 
24
+ /**
25
+ * A collaborative plain text implementation that supports grapheme-accurate editing.
26
+ *
27
+ * Locale support:
28
+ * - Locale can be specified in the meta field when creating the text: `{ meta: { locale: "ja-JP" } }`
29
+ * - If no locale is specified, falls back to browser's locale (`navigator.language`)
30
+ * - If browser locale is not available, defaults to 'en'
31
+ *
32
+ * @example
33
+ * ```typescript
34
+ * // With specific locale
35
+ * const textJa = node.createCoValue({
36
+ * type: "coplaintext",
37
+ * ruleset: { type: "unsafeAllowAll" },
38
+ * meta: { locale: "ja-JP" },
39
+ * ...Crypto.createdNowUnique(),
40
+ * });
41
+ *
42
+ * // Using browser locale
43
+ * const text = node.createCoValue({
44
+ * type: "coplaintext",
45
+ * ruleset: { type: "unsafeAllowAll" },
46
+ * meta: null,
47
+ * ...Crypto.createdNowUnique(),
48
+ * });
49
+ * ```
50
+ */
18
51
  export class RawCoPlainText<
19
52
  Meta extends JsonObject | null = JsonObject | null,
20
53
  > extends RawCoList<string, Meta> {
@@ -36,7 +69,17 @@ export class RawCoPlainText<
36
69
  "Intl.Segmenter is not supported. Use a polyfill to get coPlainText support in Jazz. (eg. https://formatjs.github.io/docs/polyfills/intl-segmenter/)",
37
70
  );
38
71
  }
39
- this._segmenter = new Intl.Segmenter("en", {
72
+
73
+ // Use locale from meta if provided, fallback to browser locale, or 'en' as last resort
74
+ const effectiveLocale =
75
+ (core.header.meta &&
76
+ typeof core.header.meta === "object" &&
77
+ "locale" in core.header.meta
78
+ ? (core.header.meta.locale as string)
79
+ : undefined) ||
80
+ (typeof navigator !== "undefined" ? navigator.language : "en");
81
+
82
+ this._segmenter = new Intl.Segmenter(effectiveLocale, {
40
83
  granularity: "grapheme",
41
84
  });
42
85
  }
@@ -78,7 +121,16 @@ export class RawCoPlainText<
78
121
  .join("");
79
122
  }
80
123
 
81
- insertAfter(
124
+ /**
125
+ * Inserts `text` before the character at index `idx`.
126
+ * If idx is 0, inserts at the start of the text.
127
+ *
128
+ * @param idx - The index of the character to insert before
129
+ * @param text - The text to insert
130
+ * @param privacy - Whether the operation should be private or trusting
131
+ * @category 2. Editing
132
+ */
133
+ insertBefore(
82
134
  idx: number,
83
135
  text: string,
84
136
  privacy: "private" | "trusting" = "private",
@@ -86,21 +138,33 @@ export class RawCoPlainText<
86
138
  const graphemes = [...this._segmenter.segment(text)].map((g) => g.segment);
87
139
 
88
140
  if (idx === 0) {
89
- // For insertions at start, just prepend each character, in reverse
141
+ // For insertions at start, prepend each character in reverse
90
142
  for (const grapheme of graphemes.reverse()) {
91
143
  this.prepend(grapheme, 0, privacy);
92
144
  }
93
145
  } else {
94
- // For other insertions, use append after the specified index
95
- // We append in forward order to maintain the text order
96
- let after = idx - 1;
97
- for (const grapheme of graphemes) {
98
- this.append(grapheme, after, privacy);
99
- after++; // Move the insertion point forward for each grapheme
100
- }
146
+ // For other insertions, append after the previous character
147
+ this.appendItems(graphemes, idx - 1, privacy);
101
148
  }
102
149
  }
103
150
 
151
+ /**
152
+ * Inserts `text` after the character at index `idx`.
153
+ *
154
+ * @param idx - The index of the character to insert after
155
+ * @param text - The text to insert
156
+ * @param privacy - Whether the operation should be private or trusting
157
+ * @category 2. Editing
158
+ */
159
+ insertAfter(
160
+ idx: number,
161
+ text: string,
162
+ privacy: "private" | "trusting" = "private",
163
+ ) {
164
+ const graphemes = [...this._segmenter.segment(text)].map((g) => g.segment);
165
+ this.appendItems(graphemes, idx, privacy);
166
+ }
167
+
104
168
  deleteRange(
105
169
  { from, to }: { from: number; to: number },
106
170
  privacy: "private" | "trusting" = "private",
@@ -123,6 +187,6 @@ export class RawCoPlainText<
123
187
  }
124
188
  this.core.makeTransaction(ops, privacy);
125
189
 
126
- this.rebuildFromCore();
190
+ this.processNewTransactions();
127
191
  }
128
192
  }
package/src/sync.ts CHANGED
@@ -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",
@@ -285,10 +281,7 @@ export class SyncManager {
285
281
 
286
282
  // Fill the missing known states with empty known states
287
283
  if (!peer.optimisticKnownStates.has(entry.id)) {
288
- peer.optimisticKnownStates.dispatch({
289
- type: "SET_AS_EMPTY",
290
- id: entry.id,
291
- });
284
+ peer.setOptimisticKnownState(entry.id, "empty");
292
285
  }
293
286
  }
294
287
 
@@ -403,11 +396,7 @@ export class SyncManager {
403
396
  * This way we can track part of the data loss that may occur when the other peer is restarted
404
397
  *
405
398
  */
406
- peer.dispatchToKnownStates({
407
- type: "SET",
408
- id: msg.id,
409
- value: knownStateIn(msg),
410
- });
399
+ peer.setKnownState(msg.id, knownStateIn(msg));
411
400
  const entry = this.local.coValuesStore.get(msg.id);
412
401
 
413
402
  if (entry.state.type === "unknown" || entry.state.type === "unavailable") {
@@ -482,11 +471,7 @@ 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.
@@ -526,22 +511,6 @@ export class SyncManager {
526
511
 
527
512
  let coValue: CoValueCore;
528
513
 
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
514
  if (entry.state.type !== "available") {
546
515
  if (!msg.header) {
547
516
  this.trySendToPeer(peer, {
@@ -560,11 +529,7 @@ export class SyncManager {
560
529
  return;
561
530
  }
562
531
 
563
- peer.dispatchToKnownStates({
564
- type: "UPDATE_HEADER",
565
- id: msg.id,
566
- header: true,
567
- });
532
+ peer.updateHeader(msg.id, true);
568
533
 
569
534
  coValue = new CoValueCore(msg.header, this.local);
570
535
 
@@ -622,14 +587,12 @@ export class SyncManager {
622
587
 
623
588
  this.recordTransactionsSize(newTransactions, peer.role);
624
589
 
625
- peer.dispatchToKnownStates({
626
- type: "UPDATE_SESSION_COUNTER",
627
- id: msg.id,
628
- sessionId: sessionID,
629
- value:
630
- newContentForSession.after +
590
+ peer.updateSessionCounter(
591
+ msg.id,
592
+ sessionID,
593
+ newContentForSession.after +
631
594
  newContentForSession.newTransactions.length,
632
- });
595
+ );
633
596
  }
634
597
 
635
598
  if (invalidStateAssumed) {
@@ -675,11 +638,7 @@ export class SyncManager {
675
638
  }
676
639
 
677
640
  async handleCorrection(msg: KnownStateMessage, peer: PeerState) {
678
- peer.dispatchToKnownStates({
679
- type: "SET",
680
- id: msg.id,
681
- value: knownStateIn(msg),
682
- });
641
+ peer.setKnownState(msg.id, knownStateIn(msg));
683
642
 
684
643
  return this.sendNewContentIncludingDependencies(msg.id, peer);
685
644
  }
@@ -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", () => {