cojson 0.8.19 → 0.8.23

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 (37) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/native/CoValuesStore.js +31 -0
  3. package/dist/native/CoValuesStore.js.map +1 -0
  4. package/dist/native/PeerState.js +7 -0
  5. package/dist/native/PeerState.js.map +1 -1
  6. package/dist/native/SyncStateSubscriptionManager.js +2 -2
  7. package/dist/native/SyncStateSubscriptionManager.js.map +1 -1
  8. package/dist/native/coValueState.js +175 -27
  9. package/dist/native/coValueState.js.map +1 -1
  10. package/dist/native/localNode.js +20 -41
  11. package/dist/native/localNode.js.map +1 -1
  12. package/dist/native/sync.js +49 -93
  13. package/dist/native/sync.js.map +1 -1
  14. package/dist/web/CoValuesStore.js +31 -0
  15. package/dist/web/CoValuesStore.js.map +1 -0
  16. package/dist/web/PeerState.js +7 -0
  17. package/dist/web/PeerState.js.map +1 -1
  18. package/dist/web/SyncStateSubscriptionManager.js +2 -2
  19. package/dist/web/SyncStateSubscriptionManager.js.map +1 -1
  20. package/dist/web/coValueState.js +175 -27
  21. package/dist/web/coValueState.js.map +1 -1
  22. package/dist/web/localNode.js +20 -41
  23. package/dist/web/localNode.js.map +1 -1
  24. package/dist/web/sync.js +49 -93
  25. package/dist/web/sync.js.map +1 -1
  26. package/package.json +1 -1
  27. package/src/CoValuesStore.ts +38 -0
  28. package/src/PeerKnownStates.ts +1 -1
  29. package/src/PeerState.ts +10 -1
  30. package/src/SyncStateSubscriptionManager.ts +2 -2
  31. package/src/coValueState.ts +253 -42
  32. package/src/localNode.ts +28 -56
  33. package/src/sync.ts +64 -116
  34. package/src/tests/PeerState.test.ts +44 -1
  35. package/src/tests/coValueState.test.ts +362 -0
  36. package/src/tests/group.test.ts +1 -1
  37. package/src/tests/sync.test.ts +140 -19
package/src/sync.ts CHANGED
@@ -2,7 +2,6 @@ import { PeerState } from "./PeerState.js";
2
2
  import { SyncStateSubscriptionManager } from "./SyncStateSubscriptionManager.js";
3
3
  import { CoValueHeader, Transaction } from "./coValueCore.js";
4
4
  import { CoValueCore } from "./coValueCore.js";
5
- import { CoValueState } from "./coValueState.js";
6
5
  import { Signature } from "./crypto/crypto.js";
7
6
  import { RawCoID, SessionID } from "./ids.js";
8
7
  import { LocalNode } from "./localNode.js";
@@ -79,6 +78,7 @@ export interface Peer {
79
78
  role: "peer" | "server" | "client" | "storage";
80
79
  priority?: number;
81
80
  crashOnClose: boolean;
81
+ deletePeerStateOnClose?: boolean;
82
82
  }
83
83
 
84
84
  export function combinedKnownStates(
@@ -135,31 +135,10 @@ export class SyncManager {
135
135
  return Object.values(this.peers);
136
136
  }
137
137
 
138
- async loadFromPeers(id: RawCoID, forPeer?: PeerID) {
139
- const eligiblePeers = this.peersInPriorityOrder().filter(
140
- (peer) => peer.id !== forPeer && peer.isServerOrStoragePeer(),
138
+ getServerAndStoragePeers(excludePeerId?: PeerID): PeerState[] {
139
+ return this.peersInPriorityOrder().filter(
140
+ (peer) => peer.isServerOrStoragePeer() && peer.id !== excludePeerId,
141
141
  );
142
-
143
- const coValueEntry = this.local.coValues[id];
144
-
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
- }
152
- await peer.pushOutgoingMessage({
153
- action: "load",
154
- id: id,
155
- header: false,
156
- sessions: {},
157
- });
158
-
159
- if (coValueEntry?.state.type === "unknown") {
160
- await coValueEntry.state.waitForPeer(peer.id);
161
- }
162
- }
163
142
  }
164
143
 
165
144
  async handleSyncMessage(msg: SyncMessage, peer: PeerState) {
@@ -192,19 +171,10 @@ export class SyncManager {
192
171
  }
193
172
 
194
173
  async subscribeToIncludingDependencies(id: RawCoID, peer: PeerState) {
195
- const entry = this.local.coValues[id];
174
+ const entry = this.local.coValuesStore.get(id);
196
175
 
197
- if (!entry) {
198
- throw new Error("Expected coValue entry on subscribe");
199
- }
200
-
201
- if (entry.state.type === "unknown") {
202
- this.trySendToPeer(peer, {
203
- action: "load",
204
- id,
205
- header: false,
206
- sessions: {},
207
- }).catch((e: unknown) => {
176
+ if (entry.state.type !== "available") {
177
+ entry.loadFromPeers([peer]).catch((e: unknown) => {
208
178
  console.error("Error sending load", e);
209
179
  });
210
180
  return;
@@ -334,7 +304,7 @@ export class SyncManager {
334
304
 
335
305
  if (peerState.isServerOrStoragePeer()) {
336
306
  const initialSync = async () => {
337
- for (const id of Object.keys(this.local.coValues) as RawCoID[]) {
307
+ for (const id of this.local.coValuesStore.getKeys()) {
338
308
  // console.log("subscribing to after peer added", id, peer.id)
339
309
  await this.subscribeToIncludingDependencies(id, peerState);
340
310
 
@@ -392,6 +362,10 @@ export class SyncManager {
392
362
  const state = this.peers[peer.id];
393
363
  state?.gracefulShutdown();
394
364
  unsubscribeFromKnownStatesUpdates();
365
+
366
+ if (peer.deletePeerStateOnClose) {
367
+ delete this.peers[peer.id];
368
+ }
395
369
  });
396
370
  }
397
371
 
@@ -400,61 +374,39 @@ export class SyncManager {
400
374
  }
401
375
 
402
376
  async handleLoad(msg: LoadMessage, peer: PeerState) {
403
- peer.optimisticKnownStates.dispatch({
377
+ peer.dispatchToKnownStates({
404
378
  type: "SET",
405
379
  id: msg.id,
406
380
  value: knownStateIn(msg),
407
381
  });
408
- let entry = this.local.coValues[msg.id];
382
+ const entry = this.local.coValuesStore.get(msg.id);
409
383
 
410
- if (!entry) {
411
- // console.log(`Loading ${msg.id} from all peers except ${peer.id}`);
384
+ if (entry.state.type === "unknown" || entry.state.type === "unavailable") {
385
+ const eligiblePeers = this.getServerAndStoragePeers(peer.id);
412
386
 
413
- // special case: we should be able to solve this much more neatly
414
- // with an explicit state machine in the future
415
- const eligiblePeers = this.peersInPriorityOrder().filter(
416
- (other) => other.id !== peer.id && other.isServerOrStoragePeer(),
417
- );
418
387
  if (eligiblePeers.length === 0) {
388
+ // If the load request contains a header or any session data
389
+ // and we don't have any eligible peers to load the coValue from
390
+ // we try to load it from the sender because it is the only place
391
+ // where we can get informations about the coValue
419
392
  if (msg.header || Object.keys(msg.sessions).length > 0) {
420
- this.local.coValues[msg.id] = CoValueState.Unknown(
421
- new Set([peer.id]),
422
- );
423
- this.trySendToPeer(peer, {
424
- action: "known",
425
- id: msg.id,
426
- header: false,
427
- sessions: {},
428
- }).catch((e) => {
429
- console.error("Error sending known state", e);
393
+ entry.loadFromPeers([peer]).catch((e) => {
394
+ console.error("Error loading coValue in handleLoad", e);
430
395
  });
431
396
  }
432
397
  return;
433
398
  } else {
434
- this.local
435
- .loadCoValueCore(msg.id, {
436
- dontLoadFrom: peer.id,
437
- dontWaitFor: peer.id,
438
- })
439
- .catch((e) => {
440
- console.error("Error loading coValue in handleLoad", e);
441
- });
399
+ this.local.loadCoValueCore(msg.id, peer.id).catch((e) => {
400
+ console.error("Error loading coValue in handleLoad", e);
401
+ });
442
402
  }
443
-
444
- entry = this.local.coValues[msg.id]!;
445
403
  }
446
404
 
447
- if (entry.state.type === "unknown") {
448
- // console.debug(
449
- // "Waiting for loaded",
450
- // msg.id,
451
- // "after message from",
452
- // peer.id,
453
- // );
454
- const loaded = await entry.state.ready;
405
+ if (entry.state.type === "loading") {
406
+ const value = await entry.getCoValue();
455
407
 
456
- if (loaded === "unavailable") {
457
- peer.optimisticKnownStates.dispatch({
408
+ if (value === "unavailable") {
409
+ peer.dispatchToKnownStates({
458
410
  type: "SET",
459
411
  id: msg.id,
460
412
  value: knownStateIn(msg),
@@ -474,37 +426,37 @@ export class SyncManager {
474
426
  }
475
427
  }
476
428
 
477
- await this.tellUntoldKnownStateIncludingDependencies(msg.id, peer);
478
- await this.sendNewContentIncludingDependencies(msg.id, peer);
429
+ if (entry.state.type === "available") {
430
+ await this.tellUntoldKnownStateIncludingDependencies(msg.id, peer);
431
+ await this.sendNewContentIncludingDependencies(msg.id, peer);
432
+ }
479
433
  }
480
434
 
481
435
  async handleKnownState(msg: KnownStateMessage, peer: PeerState) {
482
- let entry = this.local.coValues[msg.id];
436
+ const entry = this.local.coValuesStore.get(msg.id);
483
437
 
484
- peer.optimisticKnownStates.dispatch({
438
+ peer.dispatchToKnownStates({
485
439
  type: "COMBINE_WITH",
486
440
  id: msg.id,
487
441
  value: knownStateIn(msg),
488
442
  });
489
443
 
490
- peer.knownStates.dispatch({
491
- type: "COMBINE_WITH",
492
- id: msg.id,
493
- value: knownStateIn(msg),
494
- });
495
-
496
- if (!entry) {
444
+ if (entry.state.type === "unknown" || entry.state.type === "unavailable") {
497
445
  if (msg.asDependencyOf) {
498
- if (this.local.coValues[msg.asDependencyOf]) {
499
- this.local
500
- .loadCoValueCore(msg.id, { dontLoadFrom: peer.id })
501
- .catch((e) => {
502
- console.error(
503
- `Error loading coValue ${msg.id} to create loading state, as dependency of ${msg.asDependencyOf}`,
504
- e,
505
- );
506
- });
507
- entry = this.local.coValues[msg.id]!; // must exist after loadCoValueCore
446
+ const dependencyEntry = this.local.coValuesStore.get(
447
+ msg.asDependencyOf,
448
+ );
449
+
450
+ if (
451
+ dependencyEntry.state.type === "available" ||
452
+ dependencyEntry.state.type === "loading"
453
+ ) {
454
+ this.local.loadCoValueCore(msg.id, peer.id).catch((e) => {
455
+ console.error(
456
+ `Error loading coValue ${msg.id} to create loading state, as dependency of ${msg.asDependencyOf}`,
457
+ e,
458
+ );
459
+ });
508
460
  } else {
509
461
  throw new Error(
510
462
  "Expected coValue dependency entry to be created, missing subscribe?",
@@ -517,12 +469,14 @@ export class SyncManager {
517
469
  }
518
470
  }
519
471
 
520
- if (entry.state.type === "unknown") {
472
+ // The header is a boolean value that tells us if the other peer do have information about the header.
473
+ // If it's false in this point it means that the coValue is unavailable on the other peer.
474
+ if (entry.state.type !== "available") {
521
475
  const availableOnPeer = peer.optimisticKnownStates.get(msg.id)?.header;
522
476
 
523
477
  if (!availableOnPeer) {
524
478
  entry.dispatch({
525
- type: "not-found",
479
+ type: "not-found-in-peer",
526
480
  peerId: peer.id,
527
481
  });
528
482
  }
@@ -530,29 +484,24 @@ export class SyncManager {
530
484
  return;
531
485
  }
532
486
 
533
- await this.tellUntoldKnownStateIncludingDependencies(msg.id, peer);
534
- await this.sendNewContentIncludingDependencies(msg.id, peer);
487
+ if (entry.state.type === "available") {
488
+ await this.tellUntoldKnownStateIncludingDependencies(msg.id, peer);
489
+ await this.sendNewContentIncludingDependencies(msg.id, peer);
490
+ }
535
491
  }
536
492
 
537
493
  async handleNewContent(msg: NewContentMessage, peer: PeerState) {
538
- const entry = this.local.coValues[msg.id];
539
-
540
- if (!entry) {
541
- console.error(
542
- `Expected coValue entry for ${msg.id} to be created on new content, missing subscribe?`,
543
- );
544
- return;
545
- }
494
+ const entry = this.local.coValuesStore.get(msg.id);
546
495
 
547
496
  let coValue: CoValueCore;
548
497
 
549
- if (entry.state.type === "unknown") {
498
+ if (entry.state.type !== "available") {
550
499
  if (!msg.header) {
551
500
  console.error("Expected header to be sent in first message");
552
501
  return;
553
502
  }
554
503
 
555
- peer.optimisticKnownStates.dispatch({
504
+ peer.dispatchToKnownStates({
556
505
  type: "UPDATE_HEADER",
557
506
  id: msg.id,
558
507
  header: true,
@@ -561,9 +510,8 @@ export class SyncManager {
561
510
  coValue = new CoValueCore(msg.header, this.local);
562
511
 
563
512
  entry.dispatch({
564
- type: "found",
513
+ type: "available",
565
514
  coValue,
566
- peerId: peer.id,
567
515
  });
568
516
  } else {
569
517
  coValue = entry.state.coValue;
@@ -645,7 +593,7 @@ export class SyncManager {
645
593
  continue;
646
594
  }
647
595
 
648
- peer.optimisticKnownStates.dispatch({
596
+ peer.dispatchToKnownStates({
649
597
  type: "UPDATE_SESSION_COUNTER",
650
598
  id: msg.id,
651
599
  sessionId: sessionID,
@@ -688,7 +636,7 @@ export class SyncManager {
688
636
  }
689
637
 
690
638
  async handleCorrection(msg: KnownStateMessage, peer: PeerState) {
691
- peer.optimisticKnownStates.dispatch({
639
+ peer.dispatchToKnownStates({
692
640
  type: "SET",
693
641
  id: msg.id,
694
642
  value: knownStateIn(msg),
@@ -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
  });