@automerge/automerge-repo 2.0.0-alpha.4 → 2.0.0-alpha.5

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/dist/Repo.d.ts CHANGED
@@ -5,6 +5,7 @@ import { NetworkSubsystem } from "./network/NetworkSubsystem.js";
5
5
  import { StorageAdapterInterface } from "./storage/StorageAdapterInterface.js";
6
6
  import { StorageSubsystem } from "./storage/StorageSubsystem.js";
7
7
  import { StorageId } from "./storage/types.js";
8
+ import { CollectionSynchronizer } from "./synchronizer/CollectionSynchronizer.js";
8
9
  import type { AnyDocumentId, DocumentId, PeerId } from "./types.js";
9
10
  /** A Repo is a collection of documents with networking, syncing, and storage capabilities. */
10
11
  /** The `Repo` is the main entry point of this library
@@ -23,6 +24,8 @@ export declare class Repo extends EventEmitter<RepoEvents> {
23
24
  /** The debounce rate is adjustable on the repo. */
24
25
  /** @hidden */
25
26
  saveDebounceRate: number;
27
+ /** @hidden */
28
+ synchronizer: CollectionSynchronizer;
26
29
  /** By default, we share generously with all peers. */
27
30
  /** @hidden */
28
31
  sharePolicy: SharePolicy;
@@ -1 +1 @@
1
- {"version":3,"file":"Repo.d.ts","sourceRoot":"","sources":["../src/Repo.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Repo.d.ts","sourceRoot":"","sources":["../src/Repo.ts"],"names":[],"mappings":"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"}
package/dist/Repo.js CHANGED
@@ -30,7 +30,8 @@ export class Repo extends EventEmitter {
30
30
  /** @hidden */
31
31
  saveDebounceRate = 100;
32
32
  #handleCache = {};
33
- #synchronizer;
33
+ /** @hidden */
34
+ synchronizer;
34
35
  /** By default, we share generously with all peers. */
35
36
  /** @hidden */
36
37
  sharePolicy = async () => true;
@@ -44,26 +45,6 @@ export class Repo extends EventEmitter {
44
45
  this.#remoteHeadsGossipingEnabled = enableRemoteHeadsGossiping;
45
46
  this.#log = debug(`automerge-repo:repo`);
46
47
  this.sharePolicy = sharePolicy ?? this.sharePolicy;
47
- // DOC COLLECTION
48
- // The `document` event is fired by the DocCollection any time we create a new document or look
49
- // up a document by ID. We listen for it in order to wire up storage and network synchronization.
50
- this.on("document", async ({ handle }) => {
51
- if (storageSubsystem) {
52
- // Save when the document changes, but no more often than saveDebounceRate.
53
- const saveFn = ({ handle, doc, }) => {
54
- void storageSubsystem.saveDoc(handle.documentId, doc);
55
- };
56
- handle.on("heads-changed", throttle(saveFn, this.saveDebounceRate));
57
- }
58
- handle.on("unavailable", () => {
59
- this.#log("document unavailable", { documentId: handle.documentId });
60
- this.emit("unavailable-document", {
61
- documentId: handle.documentId,
62
- });
63
- });
64
- // Register the document with the synchronizer. This advertises our interest in the document.
65
- this.#synchronizer.addDocument(handle.documentId);
66
- });
67
48
  this.on("delete-document", ({ documentId }) => {
68
49
  // TODO Pass the delete on to the network
69
50
  // synchronizer.removeDocument(documentId)
@@ -75,14 +56,14 @@ export class Repo extends EventEmitter {
75
56
  });
76
57
  // SYNCHRONIZER
77
58
  // The synchronizer uses the network subsystem to keep documents in sync with peers.
78
- this.#synchronizer = new CollectionSynchronizer(this);
59
+ this.synchronizer = new CollectionSynchronizer(this);
79
60
  // When the synchronizer emits messages, send them to peers
80
- this.#synchronizer.on("message", message => {
61
+ this.synchronizer.on("message", message => {
81
62
  this.#log(`sending ${message.type} message to ${message.targetId}`);
82
63
  networkSubsystem.send(message);
83
64
  });
84
65
  if (this.#remoteHeadsGossipingEnabled) {
85
- this.#synchronizer.on("open-doc", ({ peerId, documentId }) => {
66
+ this.synchronizer.on("open-doc", ({ peerId, documentId }) => {
86
67
  this.#remoteHeadsSubscriptions.subscribePeerToDoc(peerId, documentId);
87
68
  });
88
69
  }
@@ -113,18 +94,18 @@ export class Repo extends EventEmitter {
113
94
  .catch(err => {
114
95
  console.log("error in share policy", { err });
115
96
  });
116
- this.#synchronizer.addPeer(peerId);
97
+ this.synchronizer.addPeer(peerId);
117
98
  });
118
99
  // When a peer disconnects, remove it from the synchronizer
119
100
  networkSubsystem.on("peer-disconnected", ({ peerId }) => {
120
- this.#synchronizer.removePeer(peerId);
101
+ this.synchronizer.removePeer(peerId);
121
102
  this.#remoteHeadsSubscriptions.removePeer(peerId);
122
103
  });
123
104
  // Handle incoming messages
124
105
  networkSubsystem.on("message", async (msg) => {
125
106
  this.#receiveMessage(msg);
126
107
  });
127
- this.#synchronizer.on("sync-state", message => {
108
+ this.synchronizer.on("sync-state", message => {
128
109
  this.#saveSyncState(message);
129
110
  const handle = this.#handleCache[message.documentId];
130
111
  const { storageId } = this.peerMetadataByPeerId[message.peerId] || {};
@@ -172,6 +153,28 @@ export class Repo extends EventEmitter {
172
153
  });
173
154
  }
174
155
  }
156
+ // The `document` event is fired by the DocCollection any time we create a new document or look
157
+ // up a document by ID. We listen for it in order to wire up storage and network synchronization.
158
+ #registerHandleWithSubsystems(handle) {
159
+ const { storageSubsystem } = this;
160
+ if (storageSubsystem) {
161
+ // Save when the document changes, but no more often than saveDebounceRate.
162
+ const saveFn = ({ handle, doc }) => {
163
+ void storageSubsystem.saveDoc(handle.documentId, doc);
164
+ };
165
+ handle.on("heads-changed", throttle(saveFn, this.saveDebounceRate));
166
+ }
167
+ handle.on("unavailable", () => {
168
+ this.#log("document unavailable", { documentId: handle.documentId });
169
+ this.emit("unavailable-document", {
170
+ documentId: handle.documentId,
171
+ });
172
+ });
173
+ // Register the document with the synchronizer. This advertises our interest in the document.
174
+ this.synchronizer.addDocument(handle.documentId);
175
+ // Preserve the old event in case anyone was using it.
176
+ this.emit("document", { handle });
177
+ }
175
178
  #receiveMessage(message) {
176
179
  switch (message.type) {
177
180
  case "remote-subscription-change":
@@ -188,7 +191,7 @@ export class Repo extends EventEmitter {
188
191
  case "request":
189
192
  case "ephemeral":
190
193
  case "doc-unavailable":
191
- this.#synchronizer.receiveMessage(message).catch(err => {
194
+ this.synchronizer.receiveMessage(message).catch(err => {
192
195
  console.log("error receiving message", { err });
193
196
  });
194
197
  }
@@ -229,7 +232,7 @@ export class Repo extends EventEmitter {
229
232
  }
230
233
  /** Returns a list of all connected peer ids */
231
234
  get peers() {
232
- return this.#synchronizer.peers;
235
+ return this.synchronizer.peers;
233
236
  }
234
237
  getStorageIdOfPeer(peerId) {
235
238
  return this.peerMetadataByPeerId[peerId]?.storageId;
@@ -245,7 +248,7 @@ export class Repo extends EventEmitter {
245
248
  const handle = this.#getHandle({
246
249
  documentId,
247
250
  });
248
- this.emit("document", { handle });
251
+ this.#registerHandleWithSubsystems(handle);
249
252
  handle.update(() => {
250
253
  let nextDoc;
251
254
  if (initialValue) {
@@ -314,31 +317,29 @@ export class Repo extends EventEmitter {
314
317
  const handle = this.#getHandle({
315
318
  documentId,
316
319
  });
317
- // Try to load from disk before telling anyone else about it
318
- if (this.storageSubsystem) {
319
- void this.storageSubsystem.loadDoc(handle.documentId).then(loadedDoc => {
320
- if (loadedDoc) {
321
- // uhhhh, sorry if you're reading this because we were lying to the type system
322
- handle.update(() => loadedDoc);
323
- handle.doneLoading();
324
- }
325
- else {
326
- this.networkSubsystem
327
- .whenReady()
328
- .then(() => {
329
- handle.request();
330
- })
331
- .catch(err => {
332
- this.#log("error waiting for network", { err });
333
- });
334
- this.emit("document", { handle });
335
- }
336
- });
337
- }
338
- else {
339
- handle.request();
340
- this.emit("document", { handle });
341
- }
320
+ // Loading & network is going to be asynchronous no matter what,
321
+ // but we want to return the handle immediately.
322
+ const attemptLoad = this.storageSubsystem
323
+ ? this.storageSubsystem.loadDoc(handle.documentId)
324
+ : Promise.resolve(null);
325
+ attemptLoad
326
+ .then(async (loadedDoc) => {
327
+ if (loadedDoc) {
328
+ // uhhhh, sorry if you're reading this because we were lying to the type system
329
+ handle.update(() => loadedDoc);
330
+ handle.doneLoading();
331
+ }
332
+ else {
333
+ // we want to wait for the network subsystem to be ready before
334
+ // we request the document. this prevents entering unavailable during initialization.
335
+ await this.networkSubsystem.whenReady();
336
+ handle.request();
337
+ }
338
+ this.#registerHandleWithSubsystems(handle);
339
+ })
340
+ .catch(err => {
341
+ this.#log("error waiting for network", { err });
342
+ });
342
343
  return handle;
343
344
  }
344
345
  delete(
@@ -422,6 +423,6 @@ export class Repo extends EventEmitter {
422
423
  return this.flush();
423
424
  }
424
425
  metrics() {
425
- return { documents: this.#synchronizer.metrics() };
426
+ return { documents: this.synchronizer.metrics() };
426
427
  }
427
428
  }
@@ -1,11 +1,15 @@
1
1
  import { Repo } from "../Repo.js";
2
2
  import { DocMessage } from "../network/messages.js";
3
3
  import { DocumentId, PeerId } from "../types.js";
4
+ import { DocSynchronizer } from "./DocSynchronizer.js";
4
5
  import { Synchronizer } from "./Synchronizer.js";
5
6
  /** A CollectionSynchronizer is responsible for synchronizing a DocCollection with peers. */
6
7
  export declare class CollectionSynchronizer extends Synchronizer {
7
8
  #private;
8
9
  private repo;
10
+ /** A map of documentIds to their synchronizers */
11
+ /** @hidden */
12
+ docSynchronizers: Record<DocumentId, DocSynchronizer>;
9
13
  constructor(repo: Repo);
10
14
  /**
11
15
  * When we receive a sync message for a document we haven't got in memory, we
@@ -1 +1 @@
1
- {"version":3,"file":"CollectionSynchronizer.d.ts","sourceRoot":"","sources":["../../src/synchronizer/CollectionSynchronizer.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AACjC,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEhD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAIhD,4FAA4F;AAC5F,qBAAa,sBAAuB,SAAQ,YAAY;;IAU1C,OAAO,CAAC,IAAI;gBAAJ,IAAI,EAAE,IAAI;IAqD9B;;;OAGG;IACG,cAAc,CAAC,OAAO,EAAE,UAAU;IAyBxC;;OAEG;IACH,WAAW,CAAC,UAAU,EAAE,UAAU;IAalC,cAAc,CAAC,UAAU,EAAE,UAAU;IAIrC,2DAA2D;IAC3D,OAAO,CAAC,MAAM,EAAE,MAAM;IAgBtB,uDAAuD;IACvD,UAAU,CAAC,MAAM,EAAE,MAAM;IASzB,+CAA+C;IAC/C,IAAI,KAAK,IAAI,MAAM,EAAE,CAEpB;IAED,OAAO,IAAI;QACT,CAAC,GAAG,EAAE,MAAM,GAAG;YACb,KAAK,EAAE,MAAM,EAAE,CAAA;YACf,IAAI,EAAE;gBAAE,MAAM,EAAE,MAAM,CAAC;gBAAC,UAAU,EAAE,MAAM,CAAA;aAAE,CAAA;SAC7C,CAAA;KACF;CASF"}
1
+ {"version":3,"file":"CollectionSynchronizer.d.ts","sourceRoot":"","sources":["../../src/synchronizer/CollectionSynchronizer.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AACjC,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAIhD,4FAA4F;AAC5F,qBAAa,sBAAuB,SAAQ,YAAY;;IAW1C,OAAO,CAAC,IAAI;IAPxB,kDAAkD;IAClD,cAAc;IACd,gBAAgB,EAAE,MAAM,CAAC,UAAU,EAAE,eAAe,CAAC,CAAK;gBAKtC,IAAI,EAAE,IAAI;IAqD9B;;;OAGG;IACG,cAAc,CAAC,OAAO,EAAE,UAAU;IAyBxC;;OAEG;IACH,WAAW,CAAC,UAAU,EAAE,UAAU;IAalC,cAAc,CAAC,UAAU,EAAE,UAAU;IAIrC,2DAA2D;IAC3D,OAAO,CAAC,MAAM,EAAE,MAAM;IAgBtB,uDAAuD;IACvD,UAAU,CAAC,MAAM,EAAE,MAAM;IASzB,+CAA+C;IAC/C,IAAI,KAAK,IAAI,MAAM,EAAE,CAEpB;IAED,OAAO,IAAI;QACT,CAAC,GAAG,EAAE,MAAM,GAAG;YACb,KAAK,EAAE,MAAM,EAAE,CAAA;YACf,IAAI,EAAE;gBAAE,MAAM,EAAE,MAAM,CAAC;gBAAC,UAAU,EAAE,MAAM,CAAA;aAAE,CAAA;SAC7C,CAAA;KACF;CASF"}
@@ -9,7 +9,8 @@ export class CollectionSynchronizer extends Synchronizer {
9
9
  /** The set of peers we are connected with */
10
10
  #peers = new Set();
11
11
  /** A map of documentIds to their synchronizers */
12
- #docSynchronizers = {};
12
+ /** @hidden */
13
+ docSynchronizers = {};
13
14
  /** Used to determine if the document is know to the Collection and a synchronizer exists or is being set up */
14
15
  #docSetUp = {};
15
16
  constructor(repo) {
@@ -18,11 +19,11 @@ export class CollectionSynchronizer extends Synchronizer {
18
19
  }
19
20
  /** Returns a synchronizer for the given document, creating one if it doesn't already exist. */
20
21
  #fetchDocSynchronizer(documentId) {
21
- if (!this.#docSynchronizers[documentId]) {
22
+ if (!this.docSynchronizers[documentId]) {
22
23
  const handle = this.repo.find(stringifyAutomergeUrl({ documentId }));
23
- this.#docSynchronizers[documentId] = this.#initDocSynchronizer(handle);
24
+ this.docSynchronizers[documentId] = this.#initDocSynchronizer(handle);
24
25
  }
25
- return this.#docSynchronizers[documentId];
26
+ return this.docSynchronizers[documentId];
26
27
  }
27
28
  /** Creates a new docSynchronizer and sets it up to propagate messages */
28
29
  #initDocSynchronizer(handle) {
@@ -98,7 +99,7 @@ export class CollectionSynchronizer extends Synchronizer {
98
99
  return;
99
100
  }
100
101
  this.#peers.add(peerId);
101
- for (const docSynchronizer of Object.values(this.#docSynchronizers)) {
102
+ for (const docSynchronizer of Object.values(this.docSynchronizers)) {
102
103
  const { documentId } = docSynchronizer;
103
104
  void this.repo.sharePolicy(peerId, documentId).then(okToShare => {
104
105
  if (okToShare)
@@ -110,7 +111,7 @@ export class CollectionSynchronizer extends Synchronizer {
110
111
  removePeer(peerId) {
111
112
  log(`removing peer ${peerId}`);
112
113
  this.#peers.delete(peerId);
113
- for (const docSynchronizer of Object.values(this.#docSynchronizers)) {
114
+ for (const docSynchronizer of Object.values(this.docSynchronizers)) {
114
115
  docSynchronizer.endSync(peerId);
115
116
  }
116
117
  }
@@ -119,7 +120,7 @@ export class CollectionSynchronizer extends Synchronizer {
119
120
  return Array.from(this.#peers);
120
121
  }
121
122
  metrics() {
122
- return Object.fromEntries(Object.entries(this.#docSynchronizers).map(([documentId, synchronizer]) => {
123
+ return Object.fromEntries(Object.entries(this.docSynchronizers).map(([documentId, synchronizer]) => {
123
124
  return [documentId, synchronizer.metrics()];
124
125
  }));
125
126
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automerge/automerge-repo",
3
- "version": "2.0.0-alpha.4",
3
+ "version": "2.0.0-alpha.5",
4
4
  "description": "A repository object to manage a collection of automerge documents",
5
5
  "repository": "https://github.com/automerge/automerge-repo/tree/master/packages/automerge-repo",
6
6
  "author": "Peter van Hardenberg <pvh@pvh.ca>",
@@ -60,5 +60,5 @@
60
60
  "publishConfig": {
61
61
  "access": "public"
62
62
  },
63
- "gitHead": "40057e1c8c7833b65ee392a69d4d7b3820fe6060"
63
+ "gitHead": "9f10cdceb172f92cdf2dc1be361a940dfd3c5858"
64
64
  }
package/src/Repo.ts CHANGED
@@ -49,7 +49,8 @@ export class Repo extends EventEmitter<RepoEvents> {
49
49
 
50
50
  #handleCache: Record<DocumentId, DocHandle<any>> = {}
51
51
 
52
- #synchronizer: CollectionSynchronizer
52
+ /** @hidden */
53
+ synchronizer: CollectionSynchronizer
53
54
 
54
55
  /** By default, we share generously with all peers. */
55
56
  /** @hidden */
@@ -75,33 +76,6 @@ export class Repo extends EventEmitter<RepoEvents> {
75
76
  this.#log = debug(`automerge-repo:repo`)
76
77
  this.sharePolicy = sharePolicy ?? this.sharePolicy
77
78
 
78
- // DOC COLLECTION
79
-
80
- // The `document` event is fired by the DocCollection any time we create a new document or look
81
- // up a document by ID. We listen for it in order to wire up storage and network synchronization.
82
- this.on("document", async ({ handle }) => {
83
- if (storageSubsystem) {
84
- // Save when the document changes, but no more often than saveDebounceRate.
85
- const saveFn = ({
86
- handle,
87
- doc,
88
- }: DocHandleEncodedChangePayload<any>) => {
89
- void storageSubsystem.saveDoc(handle.documentId, doc)
90
- }
91
- handle.on("heads-changed", throttle(saveFn, this.saveDebounceRate))
92
- }
93
-
94
- handle.on("unavailable", () => {
95
- this.#log("document unavailable", { documentId: handle.documentId })
96
- this.emit("unavailable-document", {
97
- documentId: handle.documentId,
98
- })
99
- })
100
-
101
- // Register the document with the synchronizer. This advertises our interest in the document.
102
- this.#synchronizer.addDocument(handle.documentId)
103
- })
104
-
105
79
  this.on("delete-document", ({ documentId }) => {
106
80
  // TODO Pass the delete on to the network
107
81
  // synchronizer.removeDocument(documentId)
@@ -115,16 +89,16 @@ export class Repo extends EventEmitter<RepoEvents> {
115
89
 
116
90
  // SYNCHRONIZER
117
91
  // The synchronizer uses the network subsystem to keep documents in sync with peers.
118
- this.#synchronizer = new CollectionSynchronizer(this)
92
+ this.synchronizer = new CollectionSynchronizer(this)
119
93
 
120
94
  // When the synchronizer emits messages, send them to peers
121
- this.#synchronizer.on("message", message => {
95
+ this.synchronizer.on("message", message => {
122
96
  this.#log(`sending ${message.type} message to ${message.targetId}`)
123
97
  networkSubsystem.send(message)
124
98
  })
125
99
 
126
100
  if (this.#remoteHeadsGossipingEnabled) {
127
- this.#synchronizer.on("open-doc", ({ peerId, documentId }) => {
101
+ this.synchronizer.on("open-doc", ({ peerId, documentId }) => {
128
102
  this.#remoteHeadsSubscriptions.subscribePeerToDoc(peerId, documentId)
129
103
  })
130
104
  }
@@ -167,12 +141,12 @@ export class Repo extends EventEmitter<RepoEvents> {
167
141
  console.log("error in share policy", { err })
168
142
  })
169
143
 
170
- this.#synchronizer.addPeer(peerId)
144
+ this.synchronizer.addPeer(peerId)
171
145
  })
172
146
 
173
147
  // When a peer disconnects, remove it from the synchronizer
174
148
  networkSubsystem.on("peer-disconnected", ({ peerId }) => {
175
- this.#synchronizer.removePeer(peerId)
149
+ this.synchronizer.removePeer(peerId)
176
150
  this.#remoteHeadsSubscriptions.removePeer(peerId)
177
151
  })
178
152
 
@@ -181,7 +155,7 @@ export class Repo extends EventEmitter<RepoEvents> {
181
155
  this.#receiveMessage(msg)
182
156
  })
183
157
 
184
- this.#synchronizer.on("sync-state", message => {
158
+ this.synchronizer.on("sync-state", message => {
185
159
  this.#saveSyncState(message)
186
160
 
187
161
  const handle = this.#handleCache[message.documentId]
@@ -243,6 +217,32 @@ export class Repo extends EventEmitter<RepoEvents> {
243
217
  }
244
218
  }
245
219
 
220
+ // The `document` event is fired by the DocCollection any time we create a new document or look
221
+ // up a document by ID. We listen for it in order to wire up storage and network synchronization.
222
+ #registerHandleWithSubsystems(handle: DocHandle<any>) {
223
+ const { storageSubsystem } = this
224
+ if (storageSubsystem) {
225
+ // Save when the document changes, but no more often than saveDebounceRate.
226
+ const saveFn = ({ handle, doc }: DocHandleEncodedChangePayload<any>) => {
227
+ void storageSubsystem.saveDoc(handle.documentId, doc)
228
+ }
229
+ handle.on("heads-changed", throttle(saveFn, this.saveDebounceRate))
230
+ }
231
+
232
+ handle.on("unavailable", () => {
233
+ this.#log("document unavailable", { documentId: handle.documentId })
234
+ this.emit("unavailable-document", {
235
+ documentId: handle.documentId,
236
+ })
237
+ })
238
+
239
+ // Register the document with the synchronizer. This advertises our interest in the document.
240
+ this.synchronizer.addDocument(handle.documentId)
241
+
242
+ // Preserve the old event in case anyone was using it.
243
+ this.emit("document", { handle })
244
+ }
245
+
246
246
  #receiveMessage(message: RepoMessage) {
247
247
  switch (message.type) {
248
248
  case "remote-subscription-change":
@@ -259,7 +259,7 @@ export class Repo extends EventEmitter<RepoEvents> {
259
259
  case "request":
260
260
  case "ephemeral":
261
261
  case "doc-unavailable":
262
- this.#synchronizer.receiveMessage(message).catch(err => {
262
+ this.synchronizer.receiveMessage(message).catch(err => {
263
263
  console.log("error receiving message", { err })
264
264
  })
265
265
  }
@@ -324,7 +324,7 @@ export class Repo extends EventEmitter<RepoEvents> {
324
324
 
325
325
  /** Returns a list of all connected peer ids */
326
326
  get peers(): PeerId[] {
327
- return this.#synchronizer.peers
327
+ return this.synchronizer.peers
328
328
  }
329
329
 
330
330
  getStorageIdOfPeer(peerId: PeerId): StorageId | undefined {
@@ -343,7 +343,7 @@ export class Repo extends EventEmitter<RepoEvents> {
343
343
  documentId,
344
344
  }) as DocHandle<T>
345
345
 
346
- this.emit("document", { handle })
346
+ this.#registerHandleWithSubsystems(handle)
347
347
 
348
348
  handle.update(() => {
349
349
  let nextDoc: Automerge.Doc<T>
@@ -425,29 +425,29 @@ export class Repo extends EventEmitter<RepoEvents> {
425
425
  documentId,
426
426
  }) as DocHandle<T>
427
427
 
428
- // Try to load from disk before telling anyone else about it
429
- if (this.storageSubsystem) {
430
- void this.storageSubsystem.loadDoc(handle.documentId).then(loadedDoc => {
428
+ // Loading & network is going to be asynchronous no matter what,
429
+ // but we want to return the handle immediately.
430
+ const attemptLoad = this.storageSubsystem
431
+ ? this.storageSubsystem.loadDoc(handle.documentId)
432
+ : Promise.resolve(null)
433
+
434
+ attemptLoad
435
+ .then(async loadedDoc => {
431
436
  if (loadedDoc) {
432
437
  // uhhhh, sorry if you're reading this because we were lying to the type system
433
438
  handle.update(() => loadedDoc as Automerge.Doc<T>)
434
439
  handle.doneLoading()
435
440
  } else {
436
- this.networkSubsystem
437
- .whenReady()
438
- .then(() => {
439
- handle.request()
440
- })
441
- .catch(err => {
442
- this.#log("error waiting for network", { err })
443
- })
444
- this.emit("document", { handle })
441
+ // we want to wait for the network subsystem to be ready before
442
+ // we request the document. this prevents entering unavailable during initialization.
443
+ await this.networkSubsystem.whenReady()
444
+ handle.request()
445
445
  }
446
+ this.#registerHandleWithSubsystems(handle)
447
+ })
448
+ .catch(err => {
449
+ this.#log("error waiting for network", { err })
446
450
  })
447
- } else {
448
- handle.request()
449
- this.emit("document", { handle })
450
- }
451
451
  return handle
452
452
  }
453
453
 
@@ -547,7 +547,7 @@ export class Repo extends EventEmitter<RepoEvents> {
547
547
  }
548
548
 
549
549
  metrics(): { documents: { [key: string]: any } } {
550
- return { documents: this.#synchronizer.metrics() }
550
+ return { documents: this.synchronizer.metrics() }
551
551
  }
552
552
  }
553
553
 
@@ -15,7 +15,8 @@ export class CollectionSynchronizer extends Synchronizer {
15
15
  #peers: Set<PeerId> = new Set()
16
16
 
17
17
  /** A map of documentIds to their synchronizers */
18
- #docSynchronizers: Record<DocumentId, DocSynchronizer> = {}
18
+ /** @hidden */
19
+ docSynchronizers: Record<DocumentId, DocSynchronizer> = {}
19
20
 
20
21
  /** Used to determine if the document is know to the Collection and a synchronizer exists or is being set up */
21
22
  #docSetUp: Record<DocumentId, boolean> = {}
@@ -26,11 +27,11 @@ export class CollectionSynchronizer extends Synchronizer {
26
27
 
27
28
  /** Returns a synchronizer for the given document, creating one if it doesn't already exist. */
28
29
  #fetchDocSynchronizer(documentId: DocumentId) {
29
- if (!this.#docSynchronizers[documentId]) {
30
+ if (!this.docSynchronizers[documentId]) {
30
31
  const handle = this.repo.find(stringifyAutomergeUrl({ documentId }))
31
- this.#docSynchronizers[documentId] = this.#initDocSynchronizer(handle)
32
+ this.docSynchronizers[documentId] = this.#initDocSynchronizer(handle)
32
33
  }
33
- return this.#docSynchronizers[documentId]
34
+ return this.docSynchronizers[documentId]
34
35
  }
35
36
 
36
37
  /** Creates a new docSynchronizer and sets it up to propagate messages */
@@ -131,7 +132,7 @@ export class CollectionSynchronizer extends Synchronizer {
131
132
  }
132
133
 
133
134
  this.#peers.add(peerId)
134
- for (const docSynchronizer of Object.values(this.#docSynchronizers)) {
135
+ for (const docSynchronizer of Object.values(this.docSynchronizers)) {
135
136
  const { documentId } = docSynchronizer
136
137
  void this.repo.sharePolicy(peerId, documentId).then(okToShare => {
137
138
  if (okToShare) docSynchronizer.beginSync([peerId])
@@ -144,7 +145,7 @@ export class CollectionSynchronizer extends Synchronizer {
144
145
  log(`removing peer ${peerId}`)
145
146
  this.#peers.delete(peerId)
146
147
 
147
- for (const docSynchronizer of Object.values(this.#docSynchronizers)) {
148
+ for (const docSynchronizer of Object.values(this.docSynchronizers)) {
148
149
  docSynchronizer.endSync(peerId)
149
150
  }
150
151
  }
@@ -161,7 +162,7 @@ export class CollectionSynchronizer extends Synchronizer {
161
162
  }
162
163
  } {
163
164
  return Object.fromEntries(
164
- Object.entries(this.#docSynchronizers).map(
165
+ Object.entries(this.docSynchronizers).map(
165
166
  ([documentId, synchronizer]) => {
166
167
  return [documentId, synchronizer.metrics()]
167
168
  }
package/test/Repo.test.ts CHANGED
@@ -832,6 +832,46 @@ describe("Repo", () => {
832
832
  teardown()
833
833
  })
834
834
 
835
+ it("synchronizes changes from bobRepo to charlieRepo when loading from storage", async () => {
836
+ const { bobRepo, bobStorage, charlieRepo, aliceHandle, teardown } =
837
+ await setup()
838
+
839
+ // We create a repo that uses bobStorage to put a document into its imaginary disk
840
+ // without it knowing about it
841
+ const bobRepo2 = new Repo({
842
+ storage: bobStorage,
843
+ })
844
+ const inStorageHandle = bobRepo2.create<TestDoc>({
845
+ foo: "foundOnFakeDisk",
846
+ })
847
+ await bobRepo2.flush()
848
+
849
+ console.log("loading from disk", inStorageHandle.url)
850
+ // Now, let's load it on the original bob repo (which shares a "disk")
851
+ const bobFoundIt = bobRepo.find<TestDoc>(inStorageHandle.url)
852
+ await bobFoundIt.whenReady()
853
+
854
+ // Before checking if it syncs, make sure we have it!
855
+ // (This behaviour is mostly test-validation, we are already testing load/save elsewhere.)
856
+ assert.deepStrictEqual(await bobFoundIt.doc(), { foo: "foundOnFakeDisk" })
857
+
858
+ // We should have a docSynchronizer and its peers should be alice and charlie
859
+ assert.strictEqual(
860
+ bobRepo.synchronizer.docSynchronizers[bobFoundIt.documentId]?.hasPeer(
861
+ "alice" as PeerId
862
+ ),
863
+ true
864
+ )
865
+ assert.strictEqual(
866
+ bobRepo.synchronizer.docSynchronizers[bobFoundIt.documentId]?.hasPeer(
867
+ "charlie" as PeerId
868
+ ),
869
+ true
870
+ )
871
+
872
+ teardown()
873
+ })
874
+
835
875
  it("charlieRepo doesn't have a document it's not supposed to have", async () => {
836
876
  const { aliceRepo, bobRepo, charlieRepo, notForCharlie, teardown } =
837
877
  await setup()