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
@@ -1,54 +1,44 @@
1
+ import { PeerState } from "./PeerState.js";
1
2
  import { CoValueCore } from "./coValueCore.js";
3
+ import { RawCoID } from "./ids.js";
2
4
  import { PeerID } from "./sync.js";
3
5
 
4
- function createResolvablePromise<T>() {
5
- let resolve!: (value: T) => void;
6
+ export const CO_VALUE_LOADING_MAX_RETRIES = 5;
6
7
 
7
- const promise = new Promise<T>((res) => {
8
- resolve = res;
9
- });
10
-
11
- return { promise, resolve };
8
+ export class CoValueUnknownState {
9
+ type = "unknown" as const;
12
10
  }
13
11
 
14
- class CoValueUnknownState {
15
- type = "unknown" as const;
16
- private peers: Map<
12
+ export class CoValueLoadingState {
13
+ type = "loading" as const;
14
+ private peers = new Map<
17
15
  PeerID,
18
- ReturnType<typeof createResolvablePromise<"available" | "unavailable">>
19
- >;
20
- private resolve: (value: "available" | "unavailable") => void;
16
+ ReturnType<typeof createResolvablePromise<void>>
17
+ >();
18
+ private resolveResult: (value: CoValueCore | "unavailable") => void;
21
19
 
22
- ready: Promise<"available" | "unavailable">;
20
+ result: Promise<CoValueCore | "unavailable">;
23
21
 
24
22
  constructor(peersIds: Iterable<PeerID>) {
25
23
  this.peers = new Map();
26
24
 
27
25
  for (const peerId of peersIds) {
28
- this.peers.set(
29
- peerId,
30
- createResolvablePromise<"available" | "unavailable">(),
31
- );
26
+ this.peers.set(peerId, createResolvablePromise<void>());
32
27
  }
33
28
 
34
29
  const { resolve, promise } = createResolvablePromise<
35
- "available" | "unavailable"
30
+ CoValueCore | "unavailable"
36
31
  >();
37
32
 
38
- this.ready = promise;
39
- this.resolve = resolve;
33
+ this.result = promise;
34
+ this.resolveResult = resolve;
40
35
  }
41
36
 
42
- update(peerId: PeerID, value: "available" | "unavailable") {
37
+ markAsUnavailable(peerId: PeerID) {
43
38
  const entry = this.peers.get(peerId);
44
39
 
45
40
  if (entry) {
46
- entry.resolve(value);
47
- }
48
-
49
- if (value === "available") {
50
- this.resolve("available");
51
- return;
41
+ entry.resolve();
52
42
  }
53
43
 
54
44
  this.peers.delete(peerId);
@@ -59,6 +49,14 @@ class CoValueUnknownState {
59
49
  }
60
50
  }
61
51
 
52
+ resolve(value: CoValueCore | "unavailable") {
53
+ this.resolveResult(value);
54
+ for (const entry of this.peers.values()) {
55
+ entry.resolve();
56
+ }
57
+ this.peers.clear();
58
+ }
59
+
62
60
  // Wait for a specific peer to have a known state
63
61
  waitForPeer(peerId: PeerID) {
64
62
  const entry = this.peers.get(peerId);
@@ -71,47 +69,260 @@ class CoValueUnknownState {
71
69
  }
72
70
  }
73
71
 
74
- class CoValueAvailableState {
72
+ export class CoValueAvailableState {
75
73
  type = "available" as const;
76
74
 
77
75
  constructor(public coValue: CoValueCore) {}
78
76
  }
79
77
 
78
+ export class CoValueUnavailableState {
79
+ type = "unavailable" as const;
80
+ }
81
+
80
82
  type CoValueStateAction =
81
83
  | {
82
- type: "not-found";
83
- peerId: PeerID;
84
+ type: "load-requested";
85
+ peersIds: PeerID[];
84
86
  }
85
87
  | {
86
- type: "found";
88
+ type: "not-found-in-peer";
87
89
  peerId: PeerID;
90
+ }
91
+ | {
92
+ type: "available";
88
93
  coValue: CoValueCore;
89
94
  };
90
95
 
96
+ type CoValueStateType =
97
+ | CoValueUnknownState
98
+ | CoValueLoadingState
99
+ | CoValueAvailableState
100
+ | CoValueUnavailableState;
101
+
91
102
  export class CoValueState {
92
- constructor(public state: CoValueUnknownState | CoValueAvailableState) {}
103
+ promise?: Promise<CoValueCore | "unavailable">;
104
+ private resolve?: (value: CoValueCore | "unavailable") => void;
105
+
106
+ constructor(
107
+ public id: RawCoID,
108
+ public state: CoValueStateType,
109
+ ) {}
93
110
 
94
- static Unknown(peersToWaitFor: Set<PeerID>) {
95
- return new CoValueState(new CoValueUnknownState(peersToWaitFor));
111
+ static Unknown(id: RawCoID) {
112
+ return new CoValueState(id, new CoValueUnknownState());
113
+ }
114
+
115
+ static Loading(id: RawCoID, peersIds: Iterable<PeerID>) {
116
+ return new CoValueState(id, new CoValueLoadingState(peersIds));
96
117
  }
97
118
 
98
119
  static Available(coValue: CoValueCore) {
99
- return new CoValueState(new CoValueAvailableState(coValue));
120
+ return new CoValueState(coValue.id, new CoValueAvailableState(coValue));
100
121
  }
101
122
 
102
- dispatch(action: CoValueStateAction) {
123
+ static Unavailable(id: RawCoID) {
124
+ return new CoValueState(id, new CoValueUnavailableState());
125
+ }
126
+
127
+ async getCoValue() {
103
128
  if (this.state.type === "available") {
129
+ return this.state.coValue;
130
+ }
131
+ if (this.state.type === "unavailable") {
132
+ return "unavailable";
133
+ }
134
+
135
+ // If we don't have a resolved state we return a new promise
136
+ // that will be resolved when the state will move to available or unavailable
137
+ if (!this.promise) {
138
+ const { promise, resolve } = createResolvablePromise<
139
+ CoValueCore | "unavailable"
140
+ >();
141
+
142
+ this.promise = promise;
143
+ this.resolve = resolve;
144
+ }
145
+
146
+ return this.promise;
147
+ }
148
+
149
+ private moveToState(value: CoValueStateType) {
150
+ this.state = value;
151
+
152
+ if (!this.resolve) {
104
153
  return;
105
154
  }
106
155
 
156
+ // If the state is available we resolve the promise
157
+ // and clear it to handle the possible transition from unavailable to available
158
+ if (value.type === "available") {
159
+ this.resolve(value.coValue);
160
+ this.clearPromise();
161
+ } else if (value.type === "unavailable") {
162
+ this.resolve("unavailable");
163
+ this.clearPromise();
164
+ }
165
+ }
166
+
167
+ private clearPromise() {
168
+ this.promise = undefined;
169
+ this.resolve = undefined;
170
+ }
171
+
172
+ async loadFromPeers(peers: PeerState[]) {
173
+ const state = this.state;
174
+
175
+ if (state.type !== "unknown" && state.type !== "unavailable") {
176
+ return;
177
+ }
178
+
179
+ if (peers.length === 0) {
180
+ return;
181
+ }
182
+
183
+ const doLoad = async (peersToLoadFrom: PeerState[]) => {
184
+ const peersWithoutErrors = getPeersWithoutErrors(
185
+ peersToLoadFrom,
186
+ this.id,
187
+ );
188
+
189
+ // If we are in the loading state we move to a new loading state
190
+ // to reset all the loading promises
191
+ if (this.state.type === "loading" || this.state.type === "unknown") {
192
+ this.moveToState(
193
+ new CoValueLoadingState(peersWithoutErrors.map((p) => p.id)),
194
+ );
195
+ }
196
+
197
+ // Assign the current state to a variable to not depend on the state changes
198
+ // that may happen while we wait for loadCoValueFromPeers to complete
199
+ const currentState = this.state;
200
+
201
+ // If we entered successfully the loading state, we load the coValue from the peers
202
+ //
203
+ // We may not enter the loading state if the coValue has become available in between
204
+ // of the retries
205
+ if (currentState.type === "loading") {
206
+ await loadCoValueFromPeers(this, peersWithoutErrors);
207
+
208
+ const result = await currentState.result;
209
+ return result !== "unavailable";
210
+ }
211
+
212
+ return currentState.type === "available";
213
+ };
214
+
215
+ await doLoad(peers);
216
+
217
+ // Retry loading from peers that have the retry flag enabled
218
+ const peersWithRetry = peers.filter((p) =>
219
+ p.shouldRetryUnavailableCoValues(),
220
+ );
221
+
222
+ if (peersWithRetry.length > 0) {
223
+ // We want to exit early if the coValue becomes available in between the retries
224
+ await Promise.race([
225
+ this.getCoValue(),
226
+ runWithRetry(
227
+ () => doLoad(peersWithRetry),
228
+ CO_VALUE_LOADING_MAX_RETRIES,
229
+ ),
230
+ ]);
231
+ }
232
+
233
+ // If after the retries the coValue is still loading, we consider the load failed
234
+ if (this.state.type === "loading") {
235
+ this.moveToState(new CoValueUnavailableState());
236
+ }
237
+ }
238
+
239
+ dispatch(action: CoValueStateAction) {
240
+ const currentState = this.state;
241
+
107
242
  switch (action.type) {
108
- case "not-found":
109
- this.state.update(action.peerId, "unavailable");
243
+ case "available":
244
+ if (currentState.type === "loading") {
245
+ currentState.resolve(action.coValue);
246
+ }
247
+
248
+ // It should be always possible to move to the available state
249
+ this.moveToState(new CoValueAvailableState(action.coValue));
250
+
110
251
  break;
111
- case "found":
112
- this.state.update(action.peerId, "available");
113
- this.state = new CoValueAvailableState(action.coValue);
252
+ case "not-found-in-peer":
253
+ if (currentState.type === "loading") {
254
+ currentState.markAsUnavailable(action.peerId);
255
+ }
256
+
114
257
  break;
115
258
  }
116
259
  }
117
260
  }
261
+
262
+ async function loadCoValueFromPeers(
263
+ coValueEntry: CoValueState,
264
+ peers: PeerState[],
265
+ ) {
266
+ for (const peer of peers) {
267
+ await peer.pushOutgoingMessage({
268
+ action: "load",
269
+ id: coValueEntry.id,
270
+ header: false,
271
+ sessions: {},
272
+ });
273
+
274
+ if (coValueEntry.state.type === "loading") {
275
+ await coValueEntry.state.waitForPeer(peer.id);
276
+ }
277
+ }
278
+ }
279
+
280
+ async function runWithRetry<T>(fn: () => Promise<T>, maxRetries: number) {
281
+ let retries = 1;
282
+
283
+ while (retries < maxRetries) {
284
+ /**
285
+ * With maxRetries of 5 we should wait:
286
+ * 300ms
287
+ * 900ms
288
+ * 2700ms
289
+ * 8100ms
290
+ */
291
+ await sleep(3 ** retries * 100);
292
+
293
+ const result = await fn();
294
+
295
+ if (result === true) {
296
+ return;
297
+ }
298
+
299
+ retries++;
300
+ }
301
+ }
302
+
303
+ function createResolvablePromise<T>() {
304
+ let resolve!: (value: T) => void;
305
+
306
+ const promise = new Promise<T>((res) => {
307
+ resolve = res;
308
+ });
309
+
310
+ return { promise, resolve };
311
+ }
312
+
313
+ function sleep(ms: number) {
314
+ return new Promise((resolve) => setTimeout(resolve, ms));
315
+ }
316
+
317
+ function getPeersWithoutErrors(peers: PeerState[], coValueId: RawCoID) {
318
+ return peers.filter((p) => {
319
+ if (p.erroredCoValues.has(coValueId)) {
320
+ console.error(
321
+ `Skipping load on errored coValue ${coValueId} from peer ${p.id}`,
322
+ );
323
+ return false;
324
+ }
325
+
326
+ return true;
327
+ });
328
+ }
package/src/localNode.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Result, ResultAsync, err, ok, okAsync } from "neverthrow";
2
+ import { CoValuesStore } from "./CoValuesStore.js";
2
3
  import { CoID } from "./coValue.js";
3
4
  import { RawCoValue } from "./coValue.js";
4
5
  import {
@@ -6,7 +7,6 @@ import {
6
7
  CoValueHeader,
7
8
  CoValueUniqueness,
8
9
  } from "./coValueCore.js";
9
- import { CoValueState } from "./coValueState.js";
10
10
  import {
11
11
  AccountMeta,
12
12
  ControlledAccountOrAgent,
@@ -45,7 +45,7 @@ export class LocalNode {
45
45
  /** @internal */
46
46
  crypto: CryptoProvider;
47
47
  /** @internal */
48
- coValues: { [key: RawCoID]: CoValueState } = {};
48
+ coValuesStore = new CoValuesStore();
49
49
  /** @category 3. Low-level */
50
50
  account: ControlledAccountOrAgent;
51
51
  /** @category 3. Low-level */
@@ -125,7 +125,8 @@ export class LocalNode {
125
125
  );
126
126
 
127
127
  nodeWithAccount.account = controlledAccount;
128
- nodeWithAccount.coValues[controlledAccount.id] = CoValueState.Available(
128
+ nodeWithAccount.coValuesStore.setAsAvailable(
129
+ controlledAccount.id,
129
130
  controlledAccount.core,
130
131
  );
131
132
  controlledAccount.core._cachedContent = undefined;
@@ -136,7 +137,7 @@ export class LocalNode {
136
137
 
137
138
  // we shouldn't need this, but it fixes account data not syncing for new accounts
138
139
  function syncAllCoValuesAfterCreateAccount() {
139
- for (const coValueEntry of Object.values(nodeWithAccount.coValues)) {
140
+ for (const coValueEntry of nodeWithAccount.coValuesStore.getValues()) {
140
141
  if (coValueEntry.state.type === "available") {
141
142
  void nodeWithAccount.syncManager.syncCoValue(
142
143
  coValueEntry.state.coValue,
@@ -206,7 +207,7 @@ export class LocalNode {
206
207
  node.syncManager.local = node;
207
208
 
208
209
  controlledAccount.core.node = node;
209
- node.coValues[accountID] = CoValueState.Available(controlledAccount.core);
210
+ node.coValuesStore.setAsAvailable(accountID, controlledAccount.core);
210
211
  controlledAccount.core._cachedContent = undefined;
211
212
 
212
213
  const profileID = account.get("profile");
@@ -243,7 +244,7 @@ export class LocalNode {
243
244
  }
244
245
 
245
246
  const coValue = new CoValueCore(header, this);
246
- this.coValues[coValue.id] = CoValueState.Available(coValue);
247
+ this.coValuesStore.setAsAvailable(coValue.id, coValue);
247
248
 
248
249
  void this.syncManager.syncCoValue(coValue);
249
250
 
@@ -253,10 +254,7 @@ export class LocalNode {
253
254
  /** @internal */
254
255
  async loadCoValueCore(
255
256
  id: RawCoID,
256
- options: {
257
- dontLoadFrom?: PeerID;
258
- dontWaitFor?: PeerID;
259
- } = {},
257
+ skipLoadingFromPeer?: PeerID,
260
258
  ): Promise<CoValueCore | "unavailable"> {
261
259
  if (this.crashed) {
262
260
  throw new Error("Trying to load CoValue after node has crashed", {
@@ -264,40 +262,18 @@ export class LocalNode {
264
262
  });
265
263
  }
266
264
 
267
- let entry = this.coValues[id];
268
- if (!entry) {
269
- const peersToWaitFor = new Set(
270
- Object.values(this.syncManager.peers)
271
- .filter((peer) => peer.isServerOrStoragePeer())
272
- .map((peer) => peer.id),
273
- );
274
- if (options.dontWaitFor) peersToWaitFor.delete(options.dontWaitFor);
275
- entry = CoValueState.Unknown(peersToWaitFor);
276
-
277
- this.coValues[id] = entry;
265
+ const entry = this.coValuesStore.get(id);
278
266
 
279
- this.syncManager.loadFromPeers(id, options.dontLoadFrom).catch((e) => {
280
- console.error(
281
- "Error loading from peers",
282
- id,
267
+ if (entry.state.type === "unknown" || entry.state.type === "unavailable") {
268
+ const peers =
269
+ this.syncManager.getServerAndStoragePeers(skipLoadingFromPeer);
283
270
 
284
- e,
285
- );
271
+ await entry.loadFromPeers(peers).catch((e) => {
272
+ console.error("Error loading from peers", id, e);
286
273
  });
287
274
  }
288
- if (entry.state.type === "available") {
289
- return Promise.resolve(entry.state.coValue);
290
- }
291
-
292
- await entry.state.ready;
293
-
294
- const updatedEntry = this.coValues[id];
295
-
296
- if (updatedEntry?.state.type === "available") {
297
- return Promise.resolve(updatedEntry.state.coValue);
298
- }
299
275
 
300
- return "unavailable";
276
+ return entry.getCoValue();
301
277
  }
302
278
 
303
279
  /**
@@ -318,13 +294,12 @@ export class LocalNode {
318
294
  }
319
295
 
320
296
  getLoaded<T extends RawCoValue>(id: CoID<T>): T | undefined {
321
- const entry = this.coValues[id];
322
- if (!entry) {
323
- return undefined;
324
- }
297
+ const entry = this.coValuesStore.get(id);
298
+
325
299
  if (entry.state.type === "available") {
326
300
  return entry.state.coValue.getCurrentContent() as T;
327
301
  }
302
+
328
303
  return undefined;
329
304
  }
330
305
 
@@ -448,15 +423,11 @@ export class LocalNode {
448
423
 
449
424
  /** @internal */
450
425
  expectCoValueLoaded(id: RawCoID, expectation?: string): CoValueCore {
451
- const entry = this.coValues[id];
452
- if (!entry) {
453
- throw new Error(
454
- `${expectation ? expectation + ": " : ""}Unknown CoValue ${id}`,
455
- );
456
- }
457
- if (entry.state.type === "unknown") {
426
+ const entry = this.coValuesStore.get(id);
427
+
428
+ if (entry.state.type !== "available") {
458
429
  throw new Error(
459
- `${expectation ? expectation + ": " : ""}CoValue ${id} not yet loaded`,
430
+ `${expectation ? expectation + ": " : ""}CoValue ${id} not yet loaded. Current state: ${entry.state.type}`,
460
431
  );
461
432
  }
462
433
  return entry.state.coValue;
@@ -650,18 +621,20 @@ export class LocalNode {
650
621
  ): LocalNode {
651
622
  const newNode = new LocalNode(account, currentSessionID, this.crypto);
652
623
 
653
- const coValuesToCopy = Object.entries(this.coValues);
624
+ const coValuesToCopy = Array.from(this.coValuesStore.getEntries());
654
625
 
655
626
  while (coValuesToCopy.length > 0) {
656
627
  const [coValueID, entry] = coValuesToCopy[coValuesToCopy.length - 1]!;
657
628
 
658
- if (entry.state.type === "unknown") {
629
+ if (entry.state.type !== "available") {
659
630
  coValuesToCopy.pop();
660
631
  continue;
661
632
  } else {
662
633
  const allDepsCopied = entry.state.coValue
663
634
  .getDependedOnCoValues()
664
- .every((dep) => newNode.coValues[dep]?.state.type === "available");
635
+ .every(
636
+ (dep) => newNode.coValuesStore.get(dep).state.type === "available",
637
+ );
665
638
 
666
639
  if (!allDepsCopied) {
667
640
  // move to end of queue
@@ -675,8 +648,7 @@ export class LocalNode {
675
648
  new Map(entry.state.coValue.sessionLogs),
676
649
  );
677
650
 
678
- newNode.coValues[coValueID as RawCoID] =
679
- CoValueState.Available(newCoValue);
651
+ newNode.coValuesStore.setAsAvailable(coValueID, newCoValue);
680
652
 
681
653
  coValuesToCopy.pop();
682
654
  }