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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +15 -0
- package/dist/PeerKnownStates.d.ts +5 -25
- package/dist/PeerKnownStates.d.ts.map +1 -1
- package/dist/PeerKnownStates.js +7 -20
- package/dist/PeerKnownStates.js.map +1 -1
- package/dist/PeerState.d.ts +14 -7
- package/dist/PeerState.d.ts.map +1 -1
- package/dist/PeerState.js +51 -7
- package/dist/PeerState.js.map +1 -1
- package/dist/coValueCore.d.ts +3 -1
- package/dist/coValueCore.d.ts.map +1 -1
- package/dist/coValueCore.js +25 -8
- package/dist/coValueCore.js.map +1 -1
- package/dist/coValues/coList.d.ts +13 -2
- package/dist/coValues/coList.d.ts.map +1 -1
- package/dist/coValues/coList.js +60 -34
- package/dist/coValues/coList.js.map +1 -1
- package/dist/coValues/coPlainText.d.ts +45 -0
- package/dist/coValues/coPlainText.d.ts.map +1 -1
- package/dist/coValues/coPlainText.js +61 -11
- package/dist/coValues/coPlainText.js.map +1 -1
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +8 -51
- package/dist/sync.js.map +1 -1
- package/dist/tests/PeerKnownStates.test.js +9 -14
- package/dist/tests/PeerKnownStates.test.js.map +1 -1
- package/dist/tests/PeerState.test.js +22 -34
- package/dist/tests/PeerState.test.js.map +1 -1
- package/dist/tests/coList.test.js +63 -0
- package/dist/tests/coList.test.js.map +1 -1
- package/dist/tests/coPlainText.test.js +66 -11
- package/dist/tests/coPlainText.test.js.map +1 -1
- package/dist/tests/coValueCore.test.js +15 -2
- package/dist/tests/coValueCore.test.js.map +1 -1
- package/dist/tests/sync.mesh.test.js +65 -3
- package/dist/tests/sync.mesh.test.js.map +1 -1
- package/dist/tests/sync.peerReconciliation.test.js +36 -0
- package/dist/tests/sync.peerReconciliation.test.js.map +1 -1
- package/dist/tests/testUtils.d.ts.map +1 -1
- package/dist/tests/testUtils.js +3 -2
- package/dist/tests/testUtils.js.map +1 -1
- package/package.json +1 -1
- package/src/PeerKnownStates.ts +19 -56
- package/src/PeerState.ts +70 -12
- package/src/coValueCore.ts +36 -7
- package/src/coValues/coList.ts +84 -44
- package/src/coValues/coPlainText.ts +75 -11
- package/src/sync.ts +11 -52
- package/src/tests/PeerKnownStates.test.ts +9 -14
- package/src/tests/PeerState.test.ts +27 -40
- package/src/tests/coList.test.ts +83 -0
- package/src/tests/coPlainText.test.ts +81 -11
- package/src/tests/coValueCore.test.ts +20 -2
- package/src/tests/sync.mesh.test.ts +81 -3
- package/src/tests/sync.peerReconciliation.test.ts +50 -0
- package/src/tests/testUtils.ts +4 -2
package/src/coValues/coList.ts
CHANGED
|
@@ -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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
283
|
-
|
|
302
|
+
const todo = [opID]; // a stack with the next item to do at the end
|
|
303
|
+
const predecessorsVisited = new Set<OpID>();
|
|
284
304
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
95
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
59
|
-
peerKnownStates.
|
|
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.
|
|
75
|
-
peerKnownStates.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
150
|
-
type: "SET",
|
|
148
|
+
peerState.setKnownState("co_z1", {
|
|
151
149
|
id: "co_z1",
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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.
|
|
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.
|
|
171
|
-
"
|
|
168
|
+
peerState._optimisticKnownStates,
|
|
169
|
+
"set",
|
|
172
170
|
);
|
|
173
171
|
|
|
174
|
-
const
|
|
175
|
-
type: "SET",
|
|
172
|
+
const state: CoValueKnownState = {
|
|
176
173
|
id: "co_z1",
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
header: false,
|
|
180
|
-
sessions: {},
|
|
181
|
-
},
|
|
174
|
+
header: false,
|
|
175
|
+
sessions: {},
|
|
182
176
|
};
|
|
183
|
-
peerState.dispatchToKnownStates(action);
|
|
184
177
|
|
|
185
|
-
|
|
186
|
-
|
|
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.
|
|
208
|
-
|
|
209
|
-
peerState.optimisticKnownStates,
|
|
210
|
-
"dispatch",
|
|
211
|
-
);
|
|
202
|
+
const knownStatesSpy = vi.spyOn(peerState._knownStates, "set");
|
|
203
|
+
expect(peerState._optimisticKnownStates).toBe("assumeInfallible");
|
|
212
204
|
|
|
213
|
-
const
|
|
214
|
-
type: "SET",
|
|
205
|
+
const state: CoValueKnownState = {
|
|
215
206
|
id: "co_z1",
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
header: false,
|
|
219
|
-
sessions: {},
|
|
220
|
-
},
|
|
207
|
+
header: false,
|
|
208
|
+
sessions: {},
|
|
221
209
|
};
|
|
222
|
-
|
|
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(
|
|
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", () => {
|