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.
- package/.turbo/turbo-build.log +3 -3
- package/CHANGELOG.md +12 -0
- package/dist/native/coValue.js +33 -0
- package/dist/native/coValue.js.map +1 -1
- package/dist/native/coValueCore.js +26 -167
- package/dist/native/coValueCore.js.map +1 -1
- package/dist/native/coValues/coList.js +7 -18
- package/dist/native/coValues/coList.js.map +1 -1
- package/dist/native/coValues/coPlainText.js +86 -0
- package/dist/native/coValues/coPlainText.js.map +1 -0
- package/dist/native/coValues/group.js +23 -0
- package/dist/native/coValues/group.js.map +1 -1
- package/dist/native/coreToCoValue.js +7 -3
- package/dist/native/coreToCoValue.js.map +1 -1
- package/dist/native/exports.js +3 -3
- package/dist/native/exports.js.map +1 -1
- package/dist/web/coValue.js +33 -0
- package/dist/web/coValue.js.map +1 -1
- package/dist/web/coValueCore.js +26 -167
- package/dist/web/coValueCore.js.map +1 -1
- package/dist/web/coValues/coList.js +7 -18
- package/dist/web/coValues/coList.js.map +1 -1
- package/dist/web/coValues/coPlainText.js +86 -0
- package/dist/web/coValues/coPlainText.js.map +1 -0
- package/dist/web/coValues/group.js +23 -0
- package/dist/web/coValues/group.js.map +1 -1
- package/dist/web/coreToCoValue.js +7 -3
- package/dist/web/coreToCoValue.js.map +1 -1
- package/dist/web/exports.js +3 -3
- package/dist/web/exports.js.map +1 -1
- package/package.json +1 -1
- package/src/coValue.ts +47 -0
- package/src/coValueCore.ts +30 -170
- package/src/coValues/coList.ts +12 -25
- package/src/coValues/coPlainText.ts +128 -0
- package/src/coValues/group.ts +31 -0
- package/src/coreToCoValue.ts +6 -3
- package/src/exports.ts +7 -2
- package/src/tests/coList.test.ts +19 -0
- package/src/tests/coPlainText.test.ts +133 -0
- package/src/tests/coValueCore.test.ts +32 -2
- package/src/tests/sync.test.ts +43 -0
- package/src/tests/testUtils.ts +19 -0
package/src/coValues/coList.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/coValues/group.ts
CHANGED
|
@@ -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"],
|
package/src/coreToCoValue.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
package/src/tests/coList.test.ts
CHANGED
|
@@ -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 {
|
|
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
|
+
});
|
package/src/tests/sync.test.ts
CHANGED
|
@@ -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();
|
package/src/tests/testUtils.ts
CHANGED
|
@@ -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"],
|