cojson 0.9.0 → 0.9.10

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 (43) hide show
  1. package/.turbo/turbo-build.log +3 -3
  2. package/CHANGELOG.md +12 -0
  3. package/dist/native/coValue.js +33 -0
  4. package/dist/native/coValue.js.map +1 -1
  5. package/dist/native/coValueCore.js +26 -167
  6. package/dist/native/coValueCore.js.map +1 -1
  7. package/dist/native/coValues/coList.js +7 -18
  8. package/dist/native/coValues/coList.js.map +1 -1
  9. package/dist/native/coValues/coPlainText.js +86 -0
  10. package/dist/native/coValues/coPlainText.js.map +1 -0
  11. package/dist/native/coValues/group.js +23 -0
  12. package/dist/native/coValues/group.js.map +1 -1
  13. package/dist/native/coreToCoValue.js +7 -3
  14. package/dist/native/coreToCoValue.js.map +1 -1
  15. package/dist/native/exports.js +3 -3
  16. package/dist/native/exports.js.map +1 -1
  17. package/dist/web/coValue.js +33 -0
  18. package/dist/web/coValue.js.map +1 -1
  19. package/dist/web/coValueCore.js +26 -167
  20. package/dist/web/coValueCore.js.map +1 -1
  21. package/dist/web/coValues/coList.js +7 -18
  22. package/dist/web/coValues/coList.js.map +1 -1
  23. package/dist/web/coValues/coPlainText.js +86 -0
  24. package/dist/web/coValues/coPlainText.js.map +1 -0
  25. package/dist/web/coValues/group.js +23 -0
  26. package/dist/web/coValues/group.js.map +1 -1
  27. package/dist/web/coreToCoValue.js +7 -3
  28. package/dist/web/coreToCoValue.js.map +1 -1
  29. package/dist/web/exports.js +3 -3
  30. package/dist/web/exports.js.map +1 -1
  31. package/package.json +1 -1
  32. package/src/coValue.ts +47 -0
  33. package/src/coValueCore.ts +30 -170
  34. package/src/coValues/coList.ts +12 -25
  35. package/src/coValues/coPlainText.ts +128 -0
  36. package/src/coValues/group.ts +31 -0
  37. package/src/coreToCoValue.ts +6 -3
  38. package/src/exports.ts +7 -2
  39. package/src/tests/coList.test.ts +19 -0
  40. package/src/tests/coPlainText.test.ts +133 -0
  41. package/src/tests/coValueCore.test.ts +32 -2
  42. package/src/tests/sync.test.ts +43 -0
  43. package/src/tests/testUtils.ts +19 -0
@@ -7,9 +7,9 @@ import { isCoValue } from "../typeUtils/isCoValue.js";
7
7
  import { RawAccountID } from "./account.js";
8
8
  import { RawGroup } from "./group.js";
9
9
 
10
- type OpID = TransactionID & { changeIdx: number };
10
+ export type OpID = TransactionID & { changeIdx: number };
11
11
 
12
- type InsertionOpPayload<T extends JsonValue> =
12
+ export type InsertionOpPayload<T extends JsonValue> =
13
13
  | {
14
14
  op: "pre";
15
15
  value: T;
@@ -21,7 +21,7 @@ type InsertionOpPayload<T extends JsonValue> =
21
21
  after: OpID | "start";
22
22
  };
23
23
 
24
- type DeletionOpPayload = {
24
+ export type DeletionOpPayload = {
25
25
  op: "del";
26
26
  insertion: OpID;
27
27
  };
@@ -49,7 +49,7 @@ export class RawCoListView<
49
49
  /** @category 6. Meta */
50
50
  id: CoID<this>;
51
51
  /** @category 6. Meta */
52
- type = "colist" as const;
52
+ type: "colist" | "coplaintext" = "colist" as const;
53
53
  /** @category 6. Meta */
54
54
  core: CoValueCore;
55
55
  /** @internal */
@@ -457,13 +457,7 @@ export class RawCoList<
457
457
 
458
458
  this.core.makeTransaction(changes, privacy);
459
459
 
460
- const listAfter = new RawCoList(this.core) as this;
461
-
462
- this.afterStart = listAfter.afterStart;
463
- this.beforeEnd = listAfter.beforeEnd;
464
- this.insertions = listAfter.insertions;
465
- this.deletionsByInsertion = listAfter.deletionsByInsertion;
466
- this._cachedEntries = undefined;
460
+ this.rebuildFromCore();
467
461
  }
468
462
 
469
463
  /**
@@ -510,13 +504,7 @@ export class RawCoList<
510
504
  privacy,
511
505
  );
512
506
 
513
- const listAfter = new RawCoList(this.core) as this;
514
-
515
- this.afterStart = listAfter.afterStart;
516
- this.beforeEnd = listAfter.beforeEnd;
517
- this.insertions = listAfter.insertions;
518
- this.deletionsByInsertion = listAfter.deletionsByInsertion;
519
- this._cachedEntries = undefined;
507
+ this.rebuildFromCore();
520
508
  }
521
509
 
522
510
  /** Deletes the item at index `at`.
@@ -543,13 +531,7 @@ export class RawCoList<
543
531
  privacy,
544
532
  );
545
533
 
546
- const listAfter = new RawCoList(this.core) as this;
547
-
548
- this.afterStart = listAfter.afterStart;
549
- this.beforeEnd = listAfter.beforeEnd;
550
- this.insertions = listAfter.insertions;
551
- this.deletionsByInsertion = listAfter.deletionsByInsertion;
552
- this._cachedEntries = undefined;
534
+ this.rebuildFromCore();
553
535
  }
554
536
 
555
537
  replace(
@@ -577,6 +559,11 @@ export class RawCoList<
577
559
  ],
578
560
  privacy,
579
561
  );
562
+ this.rebuildFromCore();
563
+ }
564
+
565
+ /** @internal */
566
+ rebuildFromCore() {
580
567
  const listAfter = new RawCoList(this.core) as this;
581
568
 
582
569
  this.afterStart = listAfter.afterStart;
@@ -0,0 +1,128 @@
1
+ import { CoValueCore } from "../coValueCore.js";
2
+ import { JsonObject } from "../jsonValue.js";
3
+ import { DeletionOpPayload, OpID, RawCoList } from "./coList.js";
4
+
5
+ export type StringifiedOpID = string & { __stringifiedOpID: true };
6
+
7
+ export function stringifyOpID(opID: OpID): StringifiedOpID {
8
+ return `${opID.sessionID}:${opID.txIndex}:${opID.changeIdx}` as StringifiedOpID;
9
+ }
10
+
11
+ type PlaintextIdxMapping = {
12
+ opIDbeforeIdx: OpID[];
13
+ opIDafterIdx: OpID[];
14
+ idxAfterOpID: { [opID: StringifiedOpID]: number };
15
+ idxBeforeOpID: { [opID: StringifiedOpID]: number };
16
+ };
17
+
18
+ export class RawCoPlainText<
19
+ Meta extends JsonObject | null = JsonObject | null,
20
+ > extends RawCoList<string, Meta> {
21
+ /** @category 6. Meta */
22
+ type = "coplaintext" as const;
23
+
24
+ private _segmenter: Intl.Segmenter;
25
+
26
+ _cachedMapping: WeakMap<
27
+ NonNullable<typeof this._cachedEntries>,
28
+ PlaintextIdxMapping
29
+ >;
30
+
31
+ constructor(core: CoValueCore) {
32
+ super(core);
33
+ this._cachedMapping = new WeakMap();
34
+ if (!Intl.Segmenter) {
35
+ throw new Error(
36
+ "Intl.Segmenter is not supported. Use a polyfill to get coPlainText support in Jazz. (eg. https://formatjs.github.io/docs/polyfills/intl-segmenter/)",
37
+ );
38
+ }
39
+ this._segmenter = new Intl.Segmenter("en", {
40
+ granularity: "grapheme",
41
+ });
42
+ }
43
+
44
+ get mapping() {
45
+ const entries = this.entries();
46
+ let mapping = this._cachedMapping.get(entries);
47
+ if (mapping) {
48
+ return mapping;
49
+ }
50
+
51
+ mapping = {
52
+ opIDbeforeIdx: [],
53
+ opIDafterIdx: [],
54
+ idxAfterOpID: {},
55
+ idxBeforeOpID: {},
56
+ };
57
+
58
+ let idxBefore = 0;
59
+
60
+ for (const entry of entries) {
61
+ const idxAfter = idxBefore + entry.value.length;
62
+
63
+ mapping.opIDafterIdx[idxBefore] = entry.opID;
64
+ mapping.opIDbeforeIdx[idxAfter] = entry.opID;
65
+ mapping.idxAfterOpID[stringifyOpID(entry.opID)] = idxAfter;
66
+ mapping.idxBeforeOpID[stringifyOpID(entry.opID)] = idxBefore;
67
+
68
+ idxBefore = idxAfter;
69
+ }
70
+
71
+ this._cachedMapping.set(entries, mapping);
72
+ return mapping;
73
+ }
74
+
75
+ toString() {
76
+ return this.entries()
77
+ .map((entry) => entry.value)
78
+ .join("");
79
+ }
80
+
81
+ insertAfter(
82
+ idx: number,
83
+ text: string,
84
+ privacy: "private" | "trusting" = "private",
85
+ ) {
86
+ const graphemes = [...this._segmenter.segment(text)].map((g) => g.segment);
87
+
88
+ if (idx === 0) {
89
+ // For insertions at start, just prepend each character, in reverse
90
+ for (const grapheme of graphemes.reverse()) {
91
+ this.prepend(grapheme, 0, privacy);
92
+ }
93
+ } 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
+ }
101
+ }
102
+ }
103
+
104
+ deleteRange(
105
+ { from, to }: { from: number; to: number },
106
+ privacy: "private" | "trusting" = "private",
107
+ ) {
108
+ const ops: DeletionOpPayload[] = [];
109
+ for (let idx = from; idx < to; ) {
110
+ const insertion = this.mapping.opIDafterIdx[idx];
111
+ if (!insertion) {
112
+ throw new Error("Invalid idx to delete " + idx);
113
+ }
114
+ ops.push({
115
+ op: "del",
116
+ insertion,
117
+ });
118
+ let nextIdx = idx + 1;
119
+ while (!this.mapping.opIDbeforeIdx[nextIdx] && nextIdx < to) {
120
+ nextIdx++;
121
+ }
122
+ idx = nextIdx;
123
+ }
124
+ this.core.makeTransaction(ops, privacy);
125
+
126
+ this.rebuildFromCore();
127
+ }
128
+ }
@@ -22,6 +22,7 @@ import {
22
22
  } from "./account.js";
23
23
  import { RawCoList } from "./coList.js";
24
24
  import { RawCoMap } from "./coMap.js";
25
+ import { RawCoPlainText } from "./coPlainText.js";
25
26
  import { RawBinaryCoStream, RawCoStream } from "./coStream.js";
26
27
 
27
28
  export const EVERYONE = "everyone" as const;
@@ -699,6 +700,36 @@ export class RawGroup<
699
700
  return list;
700
701
  }
701
702
 
703
+ /**
704
+ * Creates a new `CoPlainText` within this group, with the specified specialized
705
+ * `CoPlainText` type `T` and optional static metadata.
706
+ *
707
+ * @category 3. Value creation
708
+ */
709
+ createPlainText<T extends RawCoPlainText>(
710
+ init?: string,
711
+ meta?: T["headerMeta"],
712
+ initPrivacy: "trusting" | "private" = "private",
713
+ ): T {
714
+ const text = this.core.node
715
+ .createCoValue({
716
+ type: "coplaintext",
717
+ ruleset: {
718
+ type: "ownedByGroup",
719
+ group: this.id,
720
+ },
721
+ meta: meta || null,
722
+ ...this.core.crypto.createdNowUnique(),
723
+ })
724
+ .getCurrentContent() as T;
725
+
726
+ if (init) {
727
+ text.insertAfter(0, init, initPrivacy);
728
+ }
729
+
730
+ return text;
731
+ }
732
+
702
733
  /** @category 3. Value creation */
703
734
  createStream<C extends RawCoStream>(
704
735
  meta?: C["headerMeta"],
@@ -1,9 +1,10 @@
1
+ import { RawUnknownCoValue } from "./coValue.js";
1
2
  import type { CoValueCore } from "./coValueCore.js";
2
3
  import { RawAccount, RawControlledAccount } from "./coValues/account.js";
3
4
  import { RawCoList } from "./coValues/coList.js";
4
5
  import { RawCoMap } from "./coValues/coMap.js";
5
- import { RawCoStream } from "./coValues/coStream.js";
6
- import { RawBinaryCoStream } from "./coValues/coStream.js";
6
+ import { RawCoPlainText } from "./coValues/coPlainText.js";
7
+ import { RawBinaryCoStream, RawCoStream } from "./coValues/coStream.js";
7
8
  import { RawGroup } from "./coValues/group.js";
8
9
 
9
10
  export function coreToCoValue(
@@ -27,6 +28,8 @@ export function coreToCoValue(
27
28
  } else {
28
29
  return new RawCoMap(core);
29
30
  }
31
+ } else if (core.header.type === "coplaintext") {
32
+ return new RawCoPlainText(core);
30
33
  } else if (core.header.type === "colist") {
31
34
  return new RawCoList(core);
32
35
  } else if (core.header.type === "costream") {
@@ -36,6 +39,6 @@ export function coreToCoValue(
36
39
  return new RawCoStream(core);
37
40
  }
38
41
  } else {
39
- throw new Error(`Unknown coValue type ${core.header.type}`);
42
+ return new RawUnknownCoValue(core);
40
43
  }
41
44
  }
package/src/exports.ts CHANGED
@@ -6,14 +6,16 @@ import {
6
6
  MAX_RECOMMENDED_TX_SIZE,
7
7
  idforHeader,
8
8
  } from "./coValueCore.js";
9
- import { ControlledAgent, RawControlledAccount } from "./coValues/account.js";
10
9
  import {
10
+ ControlledAgent,
11
11
  RawAccount,
12
+ RawControlledAccount,
12
13
  RawProfile,
13
14
  accountHeaderForInitialAgentSecret,
14
15
  } from "./coValues/account.js";
15
- import { RawCoList } from "./coValues/coList.js";
16
+ import { OpID, RawCoList } from "./coValues/coList.js";
16
17
  import { RawCoMap } from "./coValues/coMap.js";
18
+ import { RawCoPlainText, stringifyOpID } from "./coValues/coPlainText.js";
17
19
  import {
18
20
  CoStreamItem,
19
21
  RawBinaryCoStream,
@@ -137,6 +139,8 @@ export {
137
139
  isRawCoID,
138
140
  LSMStorage,
139
141
  emptyKnownState,
142
+ RawCoPlainText,
143
+ stringifyOpID,
140
144
  };
141
145
 
142
146
  export type {
@@ -151,6 +155,7 @@ export type {
151
155
  CoValueUniqueness,
152
156
  Stringified,
153
157
  CoStreamItem,
158
+ OpID,
154
159
  };
155
160
 
156
161
  // eslint-disable-next-line @typescript-eslint/no-namespace
@@ -132,3 +132,22 @@ test("init the list correctly", () => {
132
132
  "hello",
133
133
  ]);
134
134
  });
135
+
136
+ test("Items prepended to start appear with latest first", () => {
137
+ const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
138
+
139
+ const coValue = node.createCoValue({
140
+ type: "colist",
141
+ ruleset: { type: "unsafeAllowAll" },
142
+ meta: null,
143
+ ...Crypto.createdNowUnique(),
144
+ });
145
+
146
+ const content = expectList(coValue.getCurrentContent());
147
+
148
+ content.prepend("first", 0, "trusting");
149
+ content.prepend("second", 0, "trusting");
150
+ content.prepend("third", 0, "trusting");
151
+
152
+ expect(content.toJSON()).toEqual(["third", "second", "first"]);
153
+ });
@@ -0,0 +1,133 @@
1
+ import { afterEach, expect, test, vi } from "vitest";
2
+ import { expectPlainText } from "../coValue.js";
3
+ import { WasmCrypto } from "../crypto/WasmCrypto.js";
4
+ import { LocalNode } from "../localNode.js";
5
+ import { randomAnonymousAccountAndSessionID } from "./testUtils.js";
6
+
7
+ const Crypto = await WasmCrypto.create();
8
+
9
+ afterEach(() => void vi.unstubAllGlobals());
10
+
11
+ test("should throw on creation if Intl.Segmenter is not available", () => {
12
+ vi.stubGlobal("Intl", {
13
+ Segmenter: undefined,
14
+ });
15
+
16
+ const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
17
+ const group = node.createGroup();
18
+ expect(() => group.createPlainText()).toThrow(
19
+ "Intl.Segmenter is not supported. Use a polyfill to get coPlainText support in Jazz. (eg. https://formatjs.github.io/docs/polyfills/intl-segmenter/)",
20
+ );
21
+ });
22
+
23
+ test("Empty CoPlainText works", () => {
24
+ const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
25
+
26
+ const coValue = node.createCoValue({
27
+ type: "coplaintext",
28
+ ruleset: { type: "unsafeAllowAll" },
29
+ meta: null,
30
+ ...Crypto.createdNowUnique(),
31
+ });
32
+
33
+ const content = expectPlainText(coValue.getCurrentContent());
34
+
35
+ expect(content.type).toEqual("coplaintext");
36
+ expect(content.toString()).toEqual("");
37
+ });
38
+
39
+ test("Can insert into empty CoPlainText", () => {
40
+ const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
41
+
42
+ const coValue = node.createCoValue({
43
+ type: "coplaintext",
44
+ ruleset: { type: "unsafeAllowAll" },
45
+ meta: null,
46
+ ...Crypto.createdNowUnique(),
47
+ });
48
+
49
+ const content = expectPlainText(coValue.getCurrentContent());
50
+
51
+ expect(content.type).toEqual("coplaintext");
52
+
53
+ content.insertAfter(0, "a", "trusting");
54
+ expect(content.toString()).toEqual("a");
55
+ });
56
+
57
+ test("Can insert and delete in CoPlainText", () => {
58
+ const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
59
+
60
+ const coValue = node.createCoValue({
61
+ type: "coplaintext",
62
+ ruleset: { type: "unsafeAllowAll" },
63
+ meta: null,
64
+ ...Crypto.createdNowUnique(),
65
+ });
66
+
67
+ const content = expectPlainText(coValue.getCurrentContent());
68
+
69
+ expect(content.type).toEqual("coplaintext");
70
+
71
+ content.insertAfter(0, "hello", "trusting");
72
+ expect(content.toString()).toEqual("hello");
73
+
74
+ content.insertAfter(5, " world", "trusting");
75
+ expect(content.toString()).toEqual("hello world");
76
+
77
+ content.insertAfter(0, "Hello, ", "trusting");
78
+ expect(content.toString()).toEqual("Hello, hello world");
79
+
80
+ console.log("first delete");
81
+ content.deleteRange({ from: 6, to: 12 }, "trusting");
82
+ expect(content.toString()).toEqual("Hello, world");
83
+
84
+ content.insertAfter(2, "😍", "trusting");
85
+ expect(content.toString()).toEqual("He😍llo, world");
86
+
87
+ console.log("second delete");
88
+ content.deleteRange({ from: 2, to: 4 }, "trusting");
89
+ expect(content.toString()).toEqual("Hello, world");
90
+ });
91
+
92
+ test("Multiple items appended after start appear in correct order", () => {
93
+ const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
94
+
95
+ const coValue = node.createCoValue({
96
+ type: "coplaintext",
97
+ ruleset: { type: "unsafeAllowAll" },
98
+ meta: null,
99
+ ...Crypto.createdNowUnique(),
100
+ });
101
+
102
+ const content = expectPlainText(coValue.getCurrentContent());
103
+
104
+ // Add multiple items in a single transaction, all after start
105
+ content.insertAfter(0, "h", "trusting");
106
+ content.insertAfter(1, "e", "trusting");
107
+ content.insertAfter(2, "y", "trusting");
108
+
109
+ // They should appear in insertion order (hey), not reversed (yeh)
110
+ expect(content.toString()).toEqual("hey");
111
+ });
112
+
113
+ test("Items inserted at start appear with latest first", () => {
114
+ const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
115
+
116
+ const coValue = node.createCoValue({
117
+ type: "coplaintext",
118
+ ruleset: { type: "unsafeAllowAll" },
119
+ meta: null,
120
+ ...Crypto.createdNowUnique(),
121
+ });
122
+
123
+ const content = expectPlainText(coValue.getCurrentContent());
124
+
125
+ // Insert multiple items at the start
126
+ content.insertAfter(0, "first", "trusting");
127
+ content.insertAfter(0, "second", "trusting");
128
+ content.insertAfter(0, "third", "trusting");
129
+
130
+ // They should appear in reverse chronological order
131
+ // because newer items should appear before older items
132
+ expect(content.toString()).toEqual("thirdsecondfirst");
133
+ });
@@ -1,11 +1,14 @@
1
1
  import { expect, test, vi } from "vitest";
2
- import { Transaction } from "../coValueCore.js";
2
+ import { CoValueCore, Transaction } from "../coValueCore.js";
3
3
  import { MapOpPayload } from "../coValues/coMap.js";
4
4
  import { WasmCrypto } from "../crypto/WasmCrypto.js";
5
5
  import { stableStringify } from "../jsonStringify.js";
6
6
  import { LocalNode } from "../localNode.js";
7
7
  import { Role } from "../permissions.js";
8
- import { randomAnonymousAccountAndSessionID } from "./testUtils.js";
8
+ import {
9
+ createTestNode,
10
+ randomAnonymousAccountAndSessionID,
11
+ } from "./testUtils.js";
9
12
 
10
13
  const Crypto = await WasmCrypto.create();
11
14
 
@@ -191,3 +194,30 @@ test("New transactions in a group correctly update owned values, including subsc
191
194
 
192
195
  expect(map.core.getValidSortedTransactions().length).toBe(0);
193
196
  });
197
+
198
+ test("creating a coValue with a group should't trigger automatically a content creation (performance)", () => {
199
+ const node = createTestNode();
200
+
201
+ const group = node.createGroup();
202
+
203
+ const getCurrentContentSpy = vi.spyOn(
204
+ CoValueCore.prototype,
205
+ "getCurrentContent",
206
+ );
207
+ const groupSpy = vi.spyOn(group.core, "getCurrentContent");
208
+
209
+ getCurrentContentSpy.mockClear();
210
+
211
+ node.createCoValue({
212
+ type: "comap",
213
+ ruleset: { type: "ownedByGroup", group: group.id },
214
+ meta: null,
215
+ ...Crypto.createdNowUnique(),
216
+ });
217
+
218
+ // It's called once for the group and never for the coValue
219
+ expect(getCurrentContentSpy).toHaveBeenCalledTimes(1);
220
+ expect(groupSpy).toHaveBeenCalledTimes(1);
221
+
222
+ getCurrentContentSpy.mockRestore();
223
+ });
@@ -12,8 +12,10 @@ import { connectedPeers, newQueuePair } from "../streamUtils.js";
12
12
  import type { SyncMessage } from "../sync.js";
13
13
  import {
14
14
  blockMessageTypeOnOutgoingPeer,
15
+ connectTwoPeers,
15
16
  createTestMetricReader,
16
17
  createTestNode,
18
+ loadCoValueOrFail,
17
19
  randomAnonymousAccountAndSessionID,
18
20
  tearDownTestMetricReader,
19
21
  waitFor,
@@ -1645,6 +1647,28 @@ function createTwoConnectedNodes() {
1645
1647
  };
1646
1648
  }
1647
1649
 
1650
+ test("a value created on one node can be loaded on anotehr node even if not directly connected", async () => {
1651
+ const userA = createTestNode();
1652
+ const userB = createTestNode();
1653
+ const serverA = createTestNode();
1654
+ const serverB = createTestNode();
1655
+ const core = createTestNode();
1656
+
1657
+ connectTwoPeers(userA, serverA, "client", "server");
1658
+ connectTwoPeers(userB, serverB, "client", "server");
1659
+ connectTwoPeers(serverA, core, "client", "server");
1660
+ connectTwoPeers(serverB, core, "client", "server");
1661
+
1662
+ const group = userA.createGroup();
1663
+ const map = group.createMap();
1664
+ map.set("key1", "value1", "trusting");
1665
+
1666
+ await map.core.waitForSync();
1667
+
1668
+ const mapOnUserB = await loadCoValueOrFail(userB, map.id);
1669
+ expect(mapOnUserB.get("key1")).toBe("value1");
1670
+ });
1671
+
1648
1672
  describe("SyncManager - knownStates vs optimisticKnownStates", () => {
1649
1673
  test("knownStates and optimisticKnownStates are the same when the coValue is fully synced", async () => {
1650
1674
  const { client, jazzCloud } = createTwoConnectedNodes();
@@ -1961,6 +1985,25 @@ describe("waitForSyncWithPeer", () => {
1961
1985
  });
1962
1986
  });
1963
1987
 
1988
+ test("Should not crash when syncing an unknown coValue type", async () => {
1989
+ const { client, jazzCloud } = createTwoConnectedNodes();
1990
+
1991
+ const coValue = client.createCoValue({
1992
+ type: "ooops" as any,
1993
+ ruleset: { type: "unsafeAllowAll" },
1994
+ meta: null,
1995
+ ...Crypto.createdNowUnique(),
1996
+ });
1997
+
1998
+ await coValue.waitForSync();
1999
+
2000
+ const coValueOnTheOtherNode = await loadCoValueOrFail(
2001
+ jazzCloud,
2002
+ coValue.getCurrentContent().id,
2003
+ );
2004
+ expect(coValueOnTheOtherNode.id).toBe(coValue.id);
2005
+ });
2006
+
1964
2007
  describe("metrics", () => {
1965
2008
  afterEach(() => {
1966
2009
  tearDownTestMetricReader();
@@ -33,6 +33,25 @@ export function createTestNode() {
33
33
  return new LocalNode(admin, session, Crypto);
34
34
  }
35
35
 
36
+ export function connectTwoPeers(
37
+ a: LocalNode,
38
+ b: LocalNode,
39
+ aRole: "client" | "server",
40
+ bRole: "client" | "server",
41
+ ) {
42
+ const [aAsPeer, bAsPeer] = connectedPeers(
43
+ "peer:" + a.account.id,
44
+ "peer:" + b.account.id,
45
+ {
46
+ peer1role: aRole,
47
+ peer2role: bRole,
48
+ },
49
+ );
50
+
51
+ a.syncManager.addPeer(bAsPeer);
52
+ b.syncManager.addPeer(aAsPeer);
53
+ }
54
+
36
55
  export async function createTwoConnectedNodes(
37
56
  node1Role: Peer["role"],
38
57
  node2Role: Peer["role"],