cojson 0.8.19-group-inheritance.0 → 0.8.21

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 (39) hide show
  1. package/CHANGELOG.md +8 -2
  2. package/dist/native/PeerState.js +5 -0
  3. package/dist/native/PeerState.js.map +1 -1
  4. package/dist/native/coValueCore.js +3 -34
  5. package/dist/native/coValueCore.js.map +1 -1
  6. package/dist/native/coValues/group.js +3 -89
  7. package/dist/native/coValues/group.js.map +1 -1
  8. package/dist/native/permissions.js +145 -174
  9. package/dist/native/permissions.js.map +1 -1
  10. package/dist/native/storage/index.js +4 -8
  11. package/dist/native/storage/index.js.map +1 -1
  12. package/dist/native/sync.js +17 -11
  13. package/dist/native/sync.js.map +1 -1
  14. package/dist/web/PeerState.js +5 -0
  15. package/dist/web/PeerState.js.map +1 -1
  16. package/dist/web/coValueCore.js +3 -34
  17. package/dist/web/coValueCore.js.map +1 -1
  18. package/dist/web/coValues/group.js +3 -89
  19. package/dist/web/coValues/group.js.map +1 -1
  20. package/dist/web/permissions.js +145 -174
  21. package/dist/web/permissions.js.map +1 -1
  22. package/dist/web/storage/index.js +4 -8
  23. package/dist/web/storage/index.js.map +1 -1
  24. package/dist/web/sync.js +17 -11
  25. package/dist/web/sync.js.map +1 -1
  26. package/package.json +1 -1
  27. package/src/PeerKnownStates.ts +1 -1
  28. package/src/PeerState.ts +9 -1
  29. package/src/coValueCore.ts +4 -50
  30. package/src/coValues/group.ts +4 -159
  31. package/src/permissions.ts +203 -244
  32. package/src/storage/index.ts +4 -12
  33. package/src/sync.ts +20 -12
  34. package/src/tests/PeerState.test.ts +44 -1
  35. package/src/tests/permissions.test.ts +0 -748
  36. package/src/tests/sync.test.ts +17 -10
  37. package/.turbo/turbo-build.log +0 -12
  38. package/.turbo/turbo-lint.log +0 -4
  39. package/.turbo/turbo-test.log +0 -1001
@@ -2,7 +2,7 @@ import { CoID } from "./coValue.js";
2
2
  import { CoValueCore, Transaction } from "./coValueCore.js";
3
3
  import { RawAccount, RawAccountID, RawProfile } from "./coValues/account.js";
4
4
  import { MapOpPayload } from "./coValues/coMap.js";
5
- import { EVERYONE, Everyone, RawGroup } from "./coValues/group.js";
5
+ import { EVERYONE, Everyone } from "./coValues/group.js";
6
6
  import { KeyID } from "./crypto/crypto.js";
7
7
  import { AgentID, RawCoID, SessionID, TransactionID } from "./ids.js";
8
8
  import { parseJSON } from "./jsonStringify.js";
@@ -28,12 +28,198 @@ export function determineValidTransactions(
28
28
  coValue: CoValueCore,
29
29
  ): { txID: TransactionID; tx: Transaction }[] {
30
30
  if (coValue.header.ruleset.type === "group") {
31
+ const allTransactionsSorted = [...coValue.sessionLogs.entries()].flatMap(
32
+ ([sessionID, sessionLog]) => {
33
+ return sessionLog.transactions.map((tx, txIndex) => ({
34
+ sessionID,
35
+ txIndex,
36
+ tx,
37
+ })) as {
38
+ sessionID: SessionID;
39
+ txIndex: number;
40
+ tx: Transaction;
41
+ }[];
42
+ },
43
+ );
44
+
45
+ allTransactionsSorted.sort((a, b) => {
46
+ return a.tx.madeAt - b.tx.madeAt;
47
+ });
48
+
31
49
  const initialAdmin = coValue.header.ruleset.initialAdmin;
50
+
32
51
  if (!initialAdmin) {
33
52
  throw new Error("Group must have initialAdmin");
34
53
  }
35
54
 
36
- return determineValidTransactionsForGroup(coValue, initialAdmin);
55
+ const memberState: {
56
+ [agent: RawAccountID | AgentID]: Role;
57
+ [EVERYONE]?: Role;
58
+ } = {};
59
+
60
+ const validTransactions: { txID: TransactionID; tx: Transaction }[] = [];
61
+
62
+ for (const { sessionID, txIndex, tx } of allTransactionsSorted) {
63
+ // console.log("before", { memberState, validTransactions });
64
+ const transactor = accountOrAgentIDfromSessionID(sessionID);
65
+
66
+ if (tx.privacy === "private") {
67
+ if (memberState[transactor] === "admin") {
68
+ validTransactions.push({
69
+ txID: { sessionID, txIndex },
70
+ tx,
71
+ });
72
+ continue;
73
+ } else {
74
+ console.warn("Only admins can make private transactions in groups");
75
+ continue;
76
+ }
77
+ }
78
+
79
+ let changes;
80
+
81
+ try {
82
+ changes = parseJSON(tx.changes);
83
+ } catch (e) {
84
+ console.warn(
85
+ coValue.id,
86
+ "Invalid JSON in transaction",
87
+ e,
88
+ tx,
89
+ JSON.stringify(tx.changes, (k, v) =>
90
+ k === "changes" || k === "encryptedChanges"
91
+ ? v.slice(0, 20) + "..."
92
+ : v,
93
+ ),
94
+ );
95
+ continue;
96
+ }
97
+
98
+ const change = changes[0] as
99
+ | MapOpPayload<RawAccountID | AgentID | Everyone, Role>
100
+ | MapOpPayload<"readKey", JsonValue>
101
+ | MapOpPayload<"profile", CoID<RawProfile>>;
102
+ if (changes.length !== 1) {
103
+ console.warn("Group transaction must have exactly one change");
104
+ continue;
105
+ }
106
+
107
+ if (change.op !== "set") {
108
+ console.warn("Group transaction must set a role or readKey");
109
+ continue;
110
+ }
111
+
112
+ if (change.key === "readKey") {
113
+ if (memberState[transactor] !== "admin") {
114
+ console.warn("Only admins can set readKeys");
115
+ continue;
116
+ }
117
+
118
+ validTransactions.push({ txID: { sessionID, txIndex }, tx });
119
+ continue;
120
+ } else if (change.key === "profile") {
121
+ if (memberState[transactor] !== "admin") {
122
+ console.warn("Only admins can set profile");
123
+ continue;
124
+ }
125
+
126
+ validTransactions.push({ txID: { sessionID, txIndex }, tx });
127
+ continue;
128
+ } else if (
129
+ isKeyForKeyField(change.key) ||
130
+ isKeyForAccountField(change.key)
131
+ ) {
132
+ if (
133
+ memberState[transactor] !== "admin" &&
134
+ memberState[transactor] !== "adminInvite" &&
135
+ memberState[transactor] !== "writerInvite" &&
136
+ memberState[transactor] !== "readerInvite"
137
+ ) {
138
+ console.warn("Only admins can reveal keys");
139
+ continue;
140
+ }
141
+
142
+ // TODO: check validity of agents who the key is revealed to?
143
+
144
+ validTransactions.push({ txID: { sessionID, txIndex }, tx });
145
+ continue;
146
+ }
147
+
148
+ const affectedMember = change.key;
149
+ const assignedRole = change.value;
150
+
151
+ if (
152
+ change.value !== "admin" &&
153
+ change.value !== "writer" &&
154
+ change.value !== "reader" &&
155
+ change.value !== "revoked" &&
156
+ change.value !== "adminInvite" &&
157
+ change.value !== "writerInvite" &&
158
+ change.value !== "readerInvite"
159
+ ) {
160
+ console.warn("Group transaction must set a valid role");
161
+ continue;
162
+ }
163
+
164
+ if (
165
+ affectedMember === EVERYONE &&
166
+ !(
167
+ change.value === "reader" ||
168
+ change.value === "writer" ||
169
+ change.value === "revoked"
170
+ )
171
+ ) {
172
+ console.warn("Everyone can only be set to reader, writer or revoked");
173
+ continue;
174
+ }
175
+
176
+ const isFirstSelfAppointment =
177
+ !memberState[transactor] &&
178
+ transactor === initialAdmin &&
179
+ change.op === "set" &&
180
+ change.key === transactor &&
181
+ change.value === "admin";
182
+
183
+ if (!isFirstSelfAppointment) {
184
+ if (memberState[transactor] === "admin") {
185
+ if (
186
+ memberState[affectedMember] === "admin" &&
187
+ affectedMember !== transactor &&
188
+ assignedRole !== "admin"
189
+ ) {
190
+ console.warn("Admins can only demote themselves.");
191
+ continue;
192
+ }
193
+ } else if (memberState[transactor] === "adminInvite") {
194
+ if (change.value !== "admin") {
195
+ console.warn("AdminInvites can only create admins.");
196
+ continue;
197
+ }
198
+ } else if (memberState[transactor] === "writerInvite") {
199
+ if (change.value !== "writer") {
200
+ console.warn("WriterInvites can only create writers.");
201
+ continue;
202
+ }
203
+ } else if (memberState[transactor] === "readerInvite") {
204
+ if (change.value !== "reader") {
205
+ console.warn("ReaderInvites can only create reader.");
206
+ continue;
207
+ }
208
+ } else {
209
+ console.warn(
210
+ "Group transaction must be made by current admin or invite",
211
+ );
212
+ continue;
213
+ }
214
+ }
215
+
216
+ memberState[affectedMember] = change.value;
217
+ validTransactions.push({ txID: { sessionID, txIndex }, tx });
218
+
219
+ // console.log("after", { memberState, validTransactions });
220
+ }
221
+
222
+ return validTransactions;
37
223
  } else if (coValue.header.ruleset.type === "ownedByGroup") {
38
224
  const groupContent = expectGroup(
39
225
  coValue.node
@@ -55,18 +241,27 @@ export function determineValidTransactions(
55
241
  return sessionLog.transactions
56
242
  .filter((tx) => {
57
243
  const groupAtTime = groupContent.atTime(tx.madeAt);
58
- const effectiveTransactor = agentInAccountOrMemberInGroup(
59
- transactor,
60
- groupAtTime,
61
- );
244
+ const effectiveTransactor =
245
+ transactor === groupContent.id &&
246
+ groupAtTime instanceof RawAccount
247
+ ? groupAtTime.currentAgentID().match(
248
+ (agentID) => agentID,
249
+ (e) => {
250
+ console.error(
251
+ "Error while determining current agent ID in valid transactions",
252
+ e,
253
+ );
254
+ return undefined;
255
+ },
256
+ )
257
+ : transactor;
62
258
 
63
259
  if (!effectiveTransactor) {
64
260
  return false;
65
261
  }
66
262
 
67
263
  const transactorRoleAtTxTime =
68
- groupAtTime.roleOfInternal(effectiveTransactor)?.role ||
69
- groupAtTime.roleOfInternal(EVERYONE)?.role;
264
+ groupAtTime.get(effectiveTransactor) || groupAtTime.get(EVERYONE);
70
265
 
71
266
  return (
72
267
  transactorRoleAtTxTime === "admin" ||
@@ -96,234 +291,6 @@ export function determineValidTransactions(
96
291
  }
97
292
  }
98
293
 
99
- function determineValidTransactionsForGroup(
100
- coValue: CoValueCore,
101
- initialAdmin: RawAccountID | AgentID,
102
- ): { txID: TransactionID; tx: Transaction }[] {
103
- const allTransactionsSorted = [...coValue.sessionLogs.entries()].flatMap(
104
- ([sessionID, sessionLog]) => {
105
- return sessionLog.transactions.map((tx, txIndex) => ({
106
- sessionID,
107
- txIndex,
108
- tx,
109
- })) as {
110
- sessionID: SessionID;
111
- txIndex: number;
112
- tx: Transaction;
113
- }[];
114
- },
115
- );
116
-
117
- allTransactionsSorted.sort((a, b) => {
118
- return a.tx.madeAt - b.tx.madeAt;
119
- });
120
-
121
- const memberState: {
122
- [agent: RawAccountID | AgentID]: Role;
123
- [EVERYONE]?: Role;
124
- } = {};
125
-
126
- const validTransactions: { txID: TransactionID; tx: Transaction }[] = [];
127
-
128
- for (const { sessionID, txIndex, tx } of allTransactionsSorted) {
129
- // console.log("before", { memberState, validTransactions });
130
- const transactor = accountOrAgentIDfromSessionID(sessionID);
131
-
132
- if (tx.privacy === "private") {
133
- if (memberState[transactor] === "admin") {
134
- validTransactions.push({
135
- txID: { sessionID, txIndex },
136
- tx,
137
- });
138
- continue;
139
- } else {
140
- console.warn("Only admins can make private transactions in groups");
141
- continue;
142
- }
143
- }
144
-
145
- let changes;
146
-
147
- try {
148
- changes = parseJSON(tx.changes);
149
- } catch (e) {
150
- console.warn(
151
- coValue.id,
152
- "Invalid JSON in transaction",
153
- e,
154
- tx,
155
- JSON.stringify(tx.changes, (k, v) =>
156
- k === "changes" || k === "encryptedChanges"
157
- ? v.slice(0, 20) + "..."
158
- : v,
159
- ),
160
- );
161
- continue;
162
- }
163
-
164
- const change = changes[0] as
165
- | MapOpPayload<RawAccountID | AgentID | Everyone, Role>
166
- | MapOpPayload<"readKey", JsonValue>
167
- | MapOpPayload<"profile", CoID<RawProfile>>
168
- | MapOpPayload<`parent_${CoID<RawGroup>}`, CoID<RawGroup>>
169
- | MapOpPayload<`child_${CoID<RawGroup>}`, CoID<RawGroup>>;
170
-
171
- if (changes.length !== 1) {
172
- console.warn("Group transaction must have exactly one change");
173
- continue;
174
- }
175
-
176
- if (change.op !== "set") {
177
- console.warn("Group transaction must set a role or readKey");
178
- continue;
179
- }
180
-
181
- if (change.key === "readKey") {
182
- if (memberState[transactor] !== "admin") {
183
- console.warn("Only admins can set readKeys");
184
- continue;
185
- }
186
-
187
- validTransactions.push({ txID: { sessionID, txIndex }, tx });
188
- continue;
189
- } else if (change.key === "profile") {
190
- if (memberState[transactor] !== "admin") {
191
- console.warn("Only admins can set profile");
192
- continue;
193
- }
194
-
195
- validTransactions.push({ txID: { sessionID, txIndex }, tx });
196
- continue;
197
- } else if (
198
- isKeyForKeyField(change.key) ||
199
- isKeyForAccountField(change.key)
200
- ) {
201
- if (
202
- memberState[transactor] !== "admin" &&
203
- memberState[transactor] !== "adminInvite" &&
204
- memberState[transactor] !== "writerInvite" &&
205
- memberState[transactor] !== "readerInvite"
206
- ) {
207
- console.warn("Only admins can reveal keys");
208
- continue;
209
- }
210
-
211
- // TODO: check validity of agents who the key is revealed to?
212
-
213
- validTransactions.push({ txID: { sessionID, txIndex }, tx });
214
- continue;
215
- } else if (isParentExtension(change.key)) {
216
- if (memberState[transactor] !== "admin") {
217
- console.warn("Only admins can set parent extensions");
218
- continue;
219
- }
220
- validTransactions.push({ txID: { sessionID, txIndex }, tx });
221
- continue;
222
- } else if (isChildExtension(change.key)) {
223
- if (memberState[transactor] !== "admin") {
224
- console.warn("Only admins can set child extensions");
225
- continue;
226
- }
227
- validTransactions.push({ txID: { sessionID, txIndex }, tx });
228
- continue;
229
- }
230
-
231
- const affectedMember = change.key;
232
- const assignedRole = change.value;
233
-
234
- if (
235
- change.value !== "admin" &&
236
- change.value !== "writer" &&
237
- change.value !== "reader" &&
238
- change.value !== "revoked" &&
239
- change.value !== "adminInvite" &&
240
- change.value !== "writerInvite" &&
241
- change.value !== "readerInvite"
242
- ) {
243
- console.warn("Group transaction must set a valid role");
244
- continue;
245
- }
246
-
247
- if (
248
- affectedMember === EVERYONE &&
249
- !(
250
- change.value === "reader" ||
251
- change.value === "writer" ||
252
- change.value === "revoked"
253
- )
254
- ) {
255
- console.warn("Everyone can only be set to reader, writer or revoked");
256
- continue;
257
- }
258
-
259
- const isFirstSelfAppointment =
260
- !memberState[transactor] &&
261
- transactor === initialAdmin &&
262
- change.op === "set" &&
263
- change.key === transactor &&
264
- change.value === "admin";
265
-
266
- if (!isFirstSelfAppointment) {
267
- if (memberState[transactor] === "admin") {
268
- if (
269
- memberState[affectedMember] === "admin" &&
270
- affectedMember !== transactor &&
271
- assignedRole !== "admin"
272
- ) {
273
- console.warn("Admins can only demote themselves.");
274
- continue;
275
- }
276
- } else if (memberState[transactor] === "adminInvite") {
277
- if (change.value !== "admin") {
278
- console.warn("AdminInvites can only create admins.");
279
- continue;
280
- }
281
- } else if (memberState[transactor] === "writerInvite") {
282
- if (change.value !== "writer") {
283
- console.warn("WriterInvites can only create writers.");
284
- continue;
285
- }
286
- } else if (memberState[transactor] === "readerInvite") {
287
- if (change.value !== "reader") {
288
- console.warn("ReaderInvites can only create reader.");
289
- continue;
290
- }
291
- } else {
292
- console.warn(
293
- "Group transaction must be made by current admin or invite",
294
- );
295
- continue;
296
- }
297
- }
298
-
299
- memberState[affectedMember] = change.value;
300
- validTransactions.push({ txID: { sessionID, txIndex }, tx });
301
-
302
- // console.log("after", { memberState, validTransactions });
303
- }
304
-
305
- return validTransactions;
306
- }
307
-
308
- function agentInAccountOrMemberInGroup(
309
- transactor: RawAccountID | AgentID,
310
- groupAtTime: RawGroup,
311
- ): RawAccountID | AgentID | undefined {
312
- if (transactor === groupAtTime.id && groupAtTime instanceof RawAccount) {
313
- return groupAtTime.currentAgentID().match(
314
- (agentID) => agentID,
315
- (e) => {
316
- console.error(
317
- "Error while determining current agent ID in valid transactions",
318
- e,
319
- );
320
- return undefined;
321
- },
322
- );
323
- }
324
- return transactor;
325
- }
326
-
327
294
  export function isKeyForKeyField(co: string): co is `${KeyID}_for_${KeyID}` {
328
295
  return co.startsWith("key_") && co.includes("_for_key");
329
296
  }
@@ -337,11 +304,3 @@ export function isKeyForAccountField(
337
304
  co.includes("_for_everyone")
338
305
  );
339
306
  }
340
-
341
- function isParentExtension(key: string): key is `parent_${CoID<RawGroup>}` {
342
- return key.startsWith("parent_");
343
- }
344
-
345
- function isChildExtension(key: string): key is `child_${CoID<RawGroup>}` {
346
- return key.startsWith("child_");
347
- }
@@ -146,7 +146,7 @@ export class LSMStorage<WH, RH, FS extends FileSystem<WH, RH>> {
146
146
  asDependencyOf || id,
147
147
  );
148
148
  } else if (!known?.header && coValue.header?.ruleset.type === "group") {
149
- const dependedOnAccountsAndGroups = new Set();
149
+ const dependedOnAccounts = new Set();
150
150
  for (const session of Object.values(coValue.sessionEntries)) {
151
151
  for (const entry of session) {
152
152
  for (const tx of entry.transactions) {
@@ -154,24 +154,16 @@ export class LSMStorage<WH, RH, FS extends FileSystem<WH, RH>> {
154
154
  const parsedChanges = JSON.parse(tx.changes);
155
155
  for (const change of parsedChanges) {
156
156
  if (change.op === "set" && change.key.startsWith("co_")) {
157
- dependedOnAccountsAndGroups.add(change.key);
158
- }
159
- if (
160
- change.op === "set" &&
161
- change.key.startsWith("parent_co_")
162
- ) {
163
- dependedOnAccountsAndGroups.add(
164
- change.key.replace("parent_", ""),
165
- );
157
+ dependedOnAccounts.add(change.key);
166
158
  }
167
159
  }
168
160
  }
169
161
  }
170
162
  }
171
163
  }
172
- for (const accountOrGroup of dependedOnAccountsAndGroups) {
164
+ for (const account of dependedOnAccounts) {
173
165
  await this.sendNewContent(
174
- accountOrGroup as CoID<RawCoValue>,
166
+ account as CoID<RawCoValue>,
175
167
  undefined,
176
168
  asDependencyOf || id,
177
169
  );
package/src/sync.ts CHANGED
@@ -143,6 +143,12 @@ export class SyncManager {
143
143
  const coValueEntry = this.local.coValues[id];
144
144
 
145
145
  for (const peer of eligiblePeers) {
146
+ if (peer.erroredCoValues.has(id)) {
147
+ console.error(
148
+ `Skipping load on errored coValue ${id} from peer ${peer.id}`,
149
+ );
150
+ continue;
151
+ }
146
152
  await peer.pushOutgoingMessage({
147
153
  action: "load",
148
154
  id: id,
@@ -157,6 +163,12 @@ export class SyncManager {
157
163
  }
158
164
 
159
165
  async handleSyncMessage(msg: SyncMessage, peer: PeerState) {
166
+ if (peer.erroredCoValues.has(msg.id)) {
167
+ console.error(
168
+ `Skipping message ${msg.action} on errored coValue ${msg.id} from peer ${peer.id}`,
169
+ );
170
+ return;
171
+ }
160
172
  // TODO: validate
161
173
  switch (msg.action) {
162
174
  case "load":
@@ -388,7 +400,7 @@ export class SyncManager {
388
400
  }
389
401
 
390
402
  async handleLoad(msg: LoadMessage, peer: PeerState) {
391
- peer.optimisticKnownStates.dispatch({
403
+ peer.dispatchToKnownStates({
392
404
  type: "SET",
393
405
  id: msg.id,
394
406
  value: knownStateIn(msg),
@@ -442,7 +454,7 @@ export class SyncManager {
442
454
  const loaded = await entry.state.ready;
443
455
 
444
456
  if (loaded === "unavailable") {
445
- peer.optimisticKnownStates.dispatch({
457
+ peer.dispatchToKnownStates({
446
458
  type: "SET",
447
459
  id: msg.id,
448
460
  value: knownStateIn(msg),
@@ -469,13 +481,7 @@ export class SyncManager {
469
481
  async handleKnownState(msg: KnownStateMessage, peer: PeerState) {
470
482
  let entry = this.local.coValues[msg.id];
471
483
 
472
- peer.optimisticKnownStates.dispatch({
473
- type: "COMBINE_WITH",
474
- id: msg.id,
475
- value: knownStateIn(msg),
476
- });
477
-
478
- peer.knownStates.dispatch({
484
+ peer.dispatchToKnownStates({
479
485
  type: "COMBINE_WITH",
480
486
  id: msg.id,
481
487
  value: knownStateIn(msg),
@@ -540,7 +546,7 @@ export class SyncManager {
540
546
  return;
541
547
  }
542
548
 
543
- peer.optimisticKnownStates.dispatch({
549
+ peer.dispatchToKnownStates({
544
550
  type: "UPDATE_HEADER",
545
551
  id: msg.id,
546
552
  header: true,
@@ -629,10 +635,11 @@ export class SyncManager {
629
635
  "our last known tx idx now: " +
630
636
  coValue.sessionLogs.get(sessionID)?.transactions.length,
631
637
  );
638
+ peer.erroredCoValues.set(msg.id, result.error);
632
639
  continue;
633
640
  }
634
641
 
635
- peer.optimisticKnownStates.dispatch({
642
+ peer.dispatchToKnownStates({
636
643
  type: "UPDATE_SESSION_COUNTER",
637
644
  id: msg.id,
638
645
  sessionId: sessionID,
@@ -675,7 +682,7 @@ export class SyncManager {
675
682
  }
676
683
 
677
684
  async handleCorrection(msg: KnownStateMessage, peer: PeerState) {
678
- peer.optimisticKnownStates.dispatch({
685
+ peer.dispatchToKnownStates({
679
686
  type: "SET",
680
687
  id: msg.id,
681
688
  value: knownStateIn(msg),
@@ -716,6 +723,7 @@ export class SyncManager {
716
723
  // let blockingSince = performance.now();
717
724
  for (const peer of this.peersInPriorityOrder()) {
718
725
  if (peer.closed) continue;
726
+ if (peer.erroredCoValues.has(coValue.id)) continue;
719
727
  // if (performance.now() - blockingSince > 5) {
720
728
  // await new Promise<void>((resolve) => {
721
729
  // setTimeout(resolve, 0);
@@ -1,4 +1,5 @@
1
1
  import { describe, expect, test, vi } from "vitest";
2
+ import { PeerKnownStateActions } from "../PeerKnownStates.js";
2
3
  import { PeerState } from "../PeerState.js";
3
4
  import { CO_VALUE_PRIORITY } from "../priority.js";
4
5
  import { Peer, SyncMessage } from "../sync.js";
@@ -15,7 +16,7 @@ function setup() {
15
16
  close: vi.fn(),
16
17
  },
17
18
  };
18
- const peerState = new PeerState(mockPeer);
19
+ const peerState = new PeerState(mockPeer, undefined);
19
20
  return { mockPeer, peerState };
20
21
  }
21
22
 
@@ -115,4 +116,46 @@ describe("PeerState", () => {
115
116
  contentMessageMid,
116
117
  );
117
118
  });
119
+
120
+ test("should clone the knownStates into optimisticKnownStates and knownStates when passed as argument", () => {
121
+ const { peerState, mockPeer } = setup();
122
+ const action: PeerKnownStateActions = {
123
+ type: "SET",
124
+ id: "co_z1",
125
+ value: {
126
+ id: "co_z1",
127
+ header: false,
128
+ sessions: {},
129
+ },
130
+ };
131
+ peerState.dispatchToKnownStates(action);
132
+
133
+ const newPeerState = new PeerState(mockPeer, peerState.knownStates);
134
+
135
+ expect(newPeerState.knownStates).toEqual(peerState.knownStates);
136
+ expect(newPeerState.optimisticKnownStates).toEqual(peerState.knownStates);
137
+ });
138
+
139
+ test("should dispatch to both states", () => {
140
+ const { peerState } = setup();
141
+ const knownStatesSpy = vi.spyOn(peerState.knownStates, "dispatch");
142
+ const optimisticKnownStatesSpy = vi.spyOn(
143
+ peerState.optimisticKnownStates,
144
+ "dispatch",
145
+ );
146
+
147
+ const action: PeerKnownStateActions = {
148
+ type: "SET",
149
+ id: "co_z1",
150
+ value: {
151
+ id: "co_z1",
152
+ header: false,
153
+ sessions: {},
154
+ },
155
+ };
156
+ peerState.dispatchToKnownStates(action);
157
+
158
+ expect(knownStatesSpy).toHaveBeenCalledWith(action);
159
+ expect(optimisticKnownStatesSpy).toHaveBeenCalledWith(action);
160
+ });
118
161
  });