@automerge/automerge-repo 1.1.0-alpha.3 → 1.1.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.
@@ -36,6 +36,7 @@ export declare class RemoteHeadsSubscriptions extends EventEmitter<RemoteHeadsSu
36
36
  handleImmediateRemoteHeadsChanged(documentId: DocumentId, storageId: StorageId, heads: A.Heads): void;
37
37
  addGenerousPeer(peerId: PeerId): void;
38
38
  removePeer(peerId: PeerId): void;
39
+ subscribePeerToDoc(peerId: PeerId, documentId: DocumentId): void;
39
40
  }
40
41
  export {};
41
42
  //# sourceMappingURL=RemoteHeadsSubscriptions.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"RemoteHeadsSubscriptions.d.ts","sourceRoot":"","sources":["../src/RemoteHeadsSubscriptions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,IAAI,CAAC,EAAE,MAAM,sBAAsB,CAAA;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAC/C,OAAO,EACL,kBAAkB,EAClB,gCAAgC,EACjC,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAItC,MAAM,MAAM,mCAAmC,GAAG;IAChD,UAAU,EAAE,UAAU,CAAA;IACtB,SAAS,EAAE,SAAS,CAAA;IACpB,WAAW,EAAE,CAAC,CAAC,KAAK,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;CAClB,CAAA;AAGD,MAAM,MAAM,wBAAwB,GAAG;IACrC,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,UAAU,CAAA;IACtB,SAAS,EAAE,SAAS,CAAA;IACpB,KAAK,EAAE,CAAC,CAAC,KAAK,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,KAAK,6BAA6B,GAAG;IACnC,sBAAsB,EAAE,CAAC,OAAO,EAAE,mCAAmC,KAAK,IAAI,CAAA;IAC9E,oBAAoB,EAAE,CAAC,OAAO,EAAE;QAC9B,KAAK,EAAE,MAAM,EAAE,CAAA;QACf,GAAG,CAAC,EAAE,SAAS,EAAE,CAAA;QACjB,MAAM,CAAC,EAAE,SAAS,EAAE,CAAA;KACrB,KAAK,IAAI,CAAA;IACV,qBAAqB,EAAE,CAAC,OAAO,EAAE,wBAAwB,KAAK,IAAI,CAAA;CACnE,CAAA;AAED,qBAAa,wBAAyB,SAAQ,YAAY,CAAC,6BAA6B,CAAC;;IAWvF,kBAAkB,CAAC,OAAO,EAAE,SAAS,EAAE;IAkBvC,sBAAsB,CAAC,OAAO,EAAE,SAAS,EAAE;IAsB3C,oBAAoB,CAAC,OAAO,EAAE,gCAAgC;IA4C9D,sEAAsE;IACtE,iBAAiB,CAAC,GAAG,EAAE,kBAAkB;IA8CzC,kEAAkE;IAClE,iCAAiC,CAC/B,UAAU,EAAE,UAAU,EACtB,SAAS,EAAE,SAAS,EACpB,KAAK,EAAE,CAAC,CAAC,KAAK;IA8BhB,eAAe,CAAC,MAAM,EAAE,MAAM;IAwB9B,UAAU,CAAC,MAAM,EAAE,MAAM;CA+D1B"}
1
+ {"version":3,"file":"RemoteHeadsSubscriptions.d.ts","sourceRoot":"","sources":["../src/RemoteHeadsSubscriptions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,IAAI,CAAC,EAAE,MAAM,sBAAsB,CAAA;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAC/C,OAAO,EACL,kBAAkB,EAClB,gCAAgC,EACjC,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAItC,MAAM,MAAM,mCAAmC,GAAG;IAChD,UAAU,EAAE,UAAU,CAAA;IACtB,SAAS,EAAE,SAAS,CAAA;IACpB,WAAW,EAAE,CAAC,CAAC,KAAK,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;CAClB,CAAA;AAGD,MAAM,MAAM,wBAAwB,GAAG;IACrC,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,UAAU,CAAA;IACtB,SAAS,EAAE,SAAS,CAAA;IACpB,KAAK,EAAE,CAAC,CAAC,KAAK,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,KAAK,6BAA6B,GAAG;IACnC,sBAAsB,EAAE,CAAC,OAAO,EAAE,mCAAmC,KAAK,IAAI,CAAA;IAC9E,oBAAoB,EAAE,CAAC,OAAO,EAAE;QAC9B,KAAK,EAAE,MAAM,EAAE,CAAA;QACf,GAAG,CAAC,EAAE,SAAS,EAAE,CAAA;QACjB,MAAM,CAAC,EAAE,SAAS,EAAE,CAAA;KACrB,KAAK,IAAI,CAAA;IACV,qBAAqB,EAAE,CAAC,OAAO,EAAE,wBAAwB,KAAK,IAAI,CAAA;CACnE,CAAA;AAED,qBAAa,wBAAyB,SAAQ,YAAY,CAAC,6BAA6B,CAAC;;IAcvF,kBAAkB,CAAC,OAAO,EAAE,SAAS,EAAE;IAkBvC,sBAAsB,CAAC,OAAO,EAAE,SAAS,EAAE;IAsB3C,oBAAoB,CAAC,OAAO,EAAE,gCAAgC;IA0E9D,sEAAsE;IACtE,iBAAiB,CAAC,GAAG,EAAE,kBAAkB;IAgDzC,kEAAkE;IAClE,iCAAiC,CAC/B,UAAU,EAAE,UAAU,EACtB,SAAS,EAAE,SAAS,EACpB,KAAK,EAAE,CAAC,CAAC,KAAK;IAgChB,eAAe,CAAC,MAAM,EAAE,MAAM;IAwB9B,UAAU,CAAC,MAAM,EAAE,MAAM;IA2BzB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU;CAoE1D"}
@@ -9,6 +9,8 @@ export class RemoteHeadsSubscriptions extends EventEmitter {
9
9
  #theirSubscriptions = new Map();
10
10
  // Peers we will always share remote heads with even if they are not subscribed
11
11
  #generousPeers = new Set();
12
+ // Documents each peer has open, we need this information so we only send remote heads of documents that the peer knows
13
+ #subscribedDocsByPeer = new Map();
12
14
  #log = debug("automerge-repo:remote-heads-subscriptions");
13
15
  subscribeToRemotes(remotes) {
14
16
  this.#log("subscribeToRemotes", remotes);
@@ -47,10 +49,14 @@ export class RemoteHeadsSubscriptions extends EventEmitter {
47
49
  handleControlMessage(control) {
48
50
  const remotesToAdd = [];
49
51
  const remotesToRemove = [];
52
+ const addedRemotesWeKnow = [];
50
53
  this.#log("handleControlMessage", control);
51
54
  if (control.add) {
52
55
  for (const remote of control.add) {
53
56
  let theirSubs = this.#theirSubscriptions.get(remote);
57
+ if (this.#ourSubscriptions.has(remote) || theirSubs) {
58
+ addedRemotesWeKnow.push(remote);
59
+ }
54
60
  if (!theirSubs) {
55
61
  theirSubs = new Set();
56
62
  this.#theirSubscriptions.set(remote, theirSubs);
@@ -80,6 +86,28 @@ export class RemoteHeadsSubscriptions extends EventEmitter {
80
86
  remove: remotesToRemove,
81
87
  });
82
88
  }
89
+ // send all our stored heads of documents the peer knows for the remotes they've added
90
+ for (const remote of addedRemotesWeKnow) {
91
+ const subscribedDocs = this.#subscribedDocsByPeer.get(control.senderId);
92
+ if (subscribedDocs) {
93
+ for (const documentId of subscribedDocs) {
94
+ const knownHeads = this.#knownHeads.get(documentId);
95
+ if (!knownHeads) {
96
+ continue;
97
+ }
98
+ const lastHeads = knownHeads.get(remote);
99
+ if (lastHeads) {
100
+ this.emit("notify-remote-heads", {
101
+ targetId: control.senderId,
102
+ documentId,
103
+ heads: lastHeads.heads,
104
+ timestamp: lastHeads.timestamp,
105
+ storageId: remote,
106
+ });
107
+ }
108
+ }
109
+ }
110
+ }
83
111
  }
84
112
  /** A peer we are not directly connected to has changed their heads */
85
113
  handleRemoteHeads(msg) {
@@ -112,13 +140,15 @@ export class RemoteHeadsSubscriptions extends EventEmitter {
112
140
  const theirSubs = this.#theirSubscriptions.get(event.storageId);
113
141
  if (theirSubs) {
114
142
  for (const peerId of theirSubs) {
115
- this.emit("notify-remote-heads", {
116
- targetId: peerId,
117
- documentId: event.documentId,
118
- heads: event.remoteHeads,
119
- timestamp: event.timestamp,
120
- storageId: event.storageId,
121
- });
143
+ if (this.#isPeerSubscribedToDoc(peerId, event.documentId)) {
144
+ this.emit("notify-remote-heads", {
145
+ targetId: peerId,
146
+ documentId: event.documentId,
147
+ heads: event.remoteHeads,
148
+ timestamp: event.timestamp,
149
+ storageId: event.storageId,
150
+ });
151
+ }
122
152
  }
123
153
  }
124
154
  }
@@ -140,13 +170,15 @@ export class RemoteHeadsSubscriptions extends EventEmitter {
140
170
  const theirSubs = this.#theirSubscriptions.get(storageId);
141
171
  if (theirSubs) {
142
172
  for (const peerId of theirSubs) {
143
- this.emit("notify-remote-heads", {
144
- targetId: peerId,
145
- documentId: documentId,
146
- heads: heads,
147
- timestamp: timestamp,
148
- storageId: storageId,
149
- });
173
+ if (this.#isPeerSubscribedToDoc(peerId, documentId)) {
174
+ this.emit("notify-remote-heads", {
175
+ targetId: peerId,
176
+ documentId: documentId,
177
+ heads: heads,
178
+ timestamp: timestamp,
179
+ storageId: storageId,
180
+ });
181
+ }
150
182
  }
151
183
  }
152
184
  }
@@ -175,6 +207,7 @@ export class RemoteHeadsSubscriptions extends EventEmitter {
175
207
  this.#log("removePeer", peerId);
176
208
  const remotesToRemove = [];
177
209
  this.#generousPeers.delete(peerId);
210
+ this.#subscribedDocsByPeer.delete(peerId);
178
211
  for (const [storageId, peerIds] of this.#theirSubscriptions) {
179
212
  if (peerIds.has(peerId)) {
180
213
  peerIds.delete(peerId);
@@ -191,6 +224,33 @@ export class RemoteHeadsSubscriptions extends EventEmitter {
191
224
  });
192
225
  }
193
226
  }
227
+ subscribePeerToDoc(peerId, documentId) {
228
+ let subscribedDocs = this.#subscribedDocsByPeer.get(peerId);
229
+ if (!subscribedDocs) {
230
+ subscribedDocs = new Set();
231
+ this.#subscribedDocsByPeer.set(peerId, subscribedDocs);
232
+ }
233
+ subscribedDocs.add(documentId);
234
+ const remoteHeads = this.#knownHeads.get(documentId);
235
+ if (remoteHeads) {
236
+ for (const [storageId, lastHeads] of remoteHeads) {
237
+ const subscribedPeers = this.#theirSubscriptions.get(storageId);
238
+ if (subscribedPeers && subscribedPeers.has(peerId)) {
239
+ this.emit("notify-remote-heads", {
240
+ targetId: peerId,
241
+ documentId,
242
+ heads: lastHeads.heads,
243
+ timestamp: lastHeads.timestamp,
244
+ storageId,
245
+ });
246
+ }
247
+ }
248
+ }
249
+ }
250
+ #isPeerSubscribedToDoc(peerId, documentId) {
251
+ let subscribedDocs = this.#subscribedDocsByPeer.get(peerId);
252
+ return subscribedDocs && subscribedDocs.has(documentId);
253
+ }
194
254
  /** Returns the (document, storageId) pairs which have changed after processing msg */
195
255
  #changedHeads(msg) {
196
256
  const changedHeads = [];
package/dist/Repo.d.ts CHANGED
@@ -34,6 +34,7 @@ export declare class Repo extends EventEmitter<RepoEvents> {
34
34
  get handles(): Record<DocumentId, DocHandle<any>>;
35
35
  /** Returns a list of all connected peer ids */
36
36
  get peers(): PeerId[];
37
+ getStorageIdOfPeer(peerId: PeerId): StorageId | undefined;
37
38
  /**
38
39
  * Creates a new document and returns a handle to it. The initial value of the document is
39
40
  * an empty object `{}`. Its documentId is generated by the system. we emit a `document` event
@@ -1 +1 @@
1
- {"version":3,"file":"Repo.d.ts","sourceRoot":"","sources":["../src/Repo.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAM5C,OAAO,EAAE,SAAS,EAAiC,MAAM,gBAAgB,CAAA;AAEzE,OAAO,EAAE,cAAc,EAAE,KAAK,YAAY,EAAE,MAAM,6BAA6B,CAAA;AAC/E,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAA;AAChE,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AAC5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAA;AAEhE,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAEnE,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAI9C,8FAA8F;AAC9F;;;;;;GAMG;AACH,qBAAa,IAAK,SAAQ,YAAY,CAAC,UAAU,CAAC;;IAGhD,cAAc;IACd,gBAAgB,EAAE,gBAAgB,CAAA;IAClC,cAAc;IACd,gBAAgB,CAAC,EAAE,gBAAgB,CAAA;IAEnC,mDAAmD;IACnD,cAAc;IACd,gBAAgB,SAAM;IAMtB,sDAAsD;IACtD,cAAc;IACd,WAAW,EAAE,WAAW,CAAmB;IAE3C,8GAA8G;IAC9G,cAAc;IACd,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAK;gBAI3C,EACV,OAAO,EACP,OAAO,EACP,MAAM,EACN,WAAW,EACX,WAAmC,GACpC,EAAE,UAAU;IAuQb,8CAA8C;IAC9C,IAAI,OAAO,uCAEV;IAED,+CAA+C;IAC/C,IAAI,KAAK,IAAI,MAAM,EAAE,CAEpB;IAED;;;;OAIG;IACH,MAAM,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC;IA0BzB;;;;;;;;;;;;;;OAcG;IACH,KAAK,CAAC,CAAC,EAAE,YAAY,EAAE,SAAS,CAAC,CAAC,CAAC;IAuBnC;;;OAGG;IACH,IAAI,CAAC,CAAC;IACJ,sDAAsD;IACtD,EAAE,EAAE,aAAa,GAChB,SAAS,CAAC,CAAC,CAAC;IAqBf,MAAM;IACJ,oDAAoD;IACpD,EAAE,EAAE,aAAa;IAWnB,kBAAkB,YAAa,SAAS,EAAE,UAGzC;IAED,SAAS,QAAa,QAAQ,SAAS,GAAG,SAAS,CAAC,CAMnD;CACF;AAED,MAAM,WAAW,UAAU;IACzB,4BAA4B;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAA;IAEf;8DAC0D;IAC1D,WAAW,CAAC,EAAE,OAAO,CAAA;IAErB,gDAAgD;IAChD,OAAO,CAAC,EAAE,cAAc,CAAA;IAExB,oDAAoD;IACpD,OAAO,EAAE,cAAc,EAAE,CAAA;IAEzB;;;OAGG;IACH,WAAW,CAAC,EAAE,WAAW,CAAA;CAC1B;AAED;;;;;;;KAOK;AACL,MAAM,MAAM,WAAW,GAAG,CACxB,MAAM,EAAE,MAAM,EACd,UAAU,CAAC,EAAE,UAAU,KACpB,OAAO,CAAC,OAAO,CAAC,CAAA;AAGrB,MAAM,WAAW,UAAU;IACzB,+CAA+C;IAC/C,QAAQ,EAAE,CAAC,GAAG,EAAE,eAAe,KAAK,IAAI,CAAA;IACxC,6BAA6B;IAC7B,iBAAiB,EAAE,CAAC,GAAG,EAAE,qBAAqB,KAAK,IAAI,CAAA;IACvD,4FAA4F;IAC5F,sBAAsB,EAAE,CAAC,GAAG,EAAE,qBAAqB,KAAK,IAAI,CAAA;CAC7D;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,SAAS,CAAC,GAAG,CAAC,CAAA;IACtB,KAAK,EAAE,OAAO,CAAA;CACf;AAED,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,UAAU,CAAA;CACvB"}
1
+ {"version":3,"file":"Repo.d.ts","sourceRoot":"","sources":["../src/Repo.ts"],"names":[],"mappings":"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"}
package/dist/Repo.js CHANGED
@@ -101,6 +101,9 @@ export class Repo extends EventEmitter {
101
101
  this.#log(`sending ${message.type} message to ${message.targetId}`);
102
102
  networkSubsystem.send(message);
103
103
  });
104
+ this.#synchronizer.on("open-doc", ({ peerId, documentId }) => {
105
+ this.#remoteHeadsSubscriptions.subscribePeerToDoc(peerId, documentId);
106
+ });
104
107
  // STORAGE
105
108
  // The storage subsystem has access to some form of persistence, and deals with save and loading documents.
106
109
  const storageSubsystem = storage ? new StorageSubsystem(storage) : undefined;
@@ -244,6 +247,9 @@ export class Repo extends EventEmitter {
244
247
  get peers() {
245
248
  return this.#synchronizer.peers;
246
249
  }
250
+ getStorageIdOfPeer(peerId) {
251
+ return this.peerMetadataByPeerId[peerId]?.storageId;
252
+ }
247
253
  /**
248
254
  * Creates a new document and returns a handle to it. The initial value of the document is
249
255
  * an empty object `{}`. Its documentId is generated by the system. we emit a `document` event
@@ -111,6 +111,11 @@ export interface SyncStateMessage {
111
111
  documentId: DocumentId;
112
112
  syncState: SyncState;
113
113
  }
114
+ /** Notify the repo that a peer started syncing with a doc */
115
+ export interface OpenDocMessage {
116
+ peerId: PeerId;
117
+ documentId: DocumentId;
118
+ }
114
119
  export declare const isValidRepoMessage: (message: Message) => message is RepoMessage;
115
120
  export declare const isDocumentUnavailableMessage: (msg: Message) => msg is DocumentUnavailableMessage;
116
121
  export declare const isRequestMessage: (msg: Message) => msg is RequestMessage;
@@ -1 +1 @@
1
- {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/network/messages.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/network/messages.ts"],"names":[],"mappings":"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"}
@@ -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;AAChE,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;IAoD9B;;;OAGG;IACG,cAAc,CAAC,OAAO,EAAE,UAAU;IAyBxC;;OAEG;IACH,WAAW,CAAC,UAAU,EAAE,UAAU;IAYlC,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;CACF"}
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;AAChE,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;IAYlC,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;CACF"}
@@ -40,6 +40,7 @@ export class CollectionSynchronizer extends Synchronizer {
40
40
  },
41
41
  });
42
42
  docSynchronizer.on("message", event => this.emit("message", event));
43
+ docSynchronizer.on("open-doc", event => this.emit("open-doc", event));
43
44
  docSynchronizer.on("sync-state", event => this.emit("sync-state", event));
44
45
  return docSynchronizer;
45
46
  }
@@ -1 +1 @@
1
- {"version":3,"file":"DocSynchronizer.d.ts","sourceRoot":"","sources":["../../src/synchronizer/DocSynchronizer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,2BAA2B,CAAA;AAG9C,OAAO,EACL,SAAS,EAKV,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAEL,gBAAgB,EAEhB,WAAW,EACX,cAAc,EACd,WAAW,EAEZ,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAIhD,KAAK,kBAAkB,GAAG,SAAS,GAAG,KAAK,GAAG,aAAa,GAAG,OAAO,CAAA;AAOrE,UAAU,qBAAqB;IAC7B,MAAM,EAAE,SAAS,CAAC,OAAO,CAAC,CAAA;IAC1B,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,CAAC,CAAC,SAAS,GAAG,SAAS,CAAA;CAC9D;AAED;;;GAGG;AACH,qBAAa,eAAgB,SAAQ,YAAY;;IAE/C,gBAAgB,SAAM;gBAsBV,EAAE,MAAM,EAAE,eAAe,EAAE,EAAE,qBAAqB;IAyB9D,IAAI,UAAU,uCAEb;IAED,IAAI,UAAU,qCAEb;IAyHD,OAAO,CAAC,MAAM,EAAE,MAAM;IAItB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE;IA+C3B,OAAO,CAAC,MAAM,EAAE,MAAM;IAKtB,cAAc,CAAC,OAAO,EAAE,WAAW;IAkBnC,uBAAuB,CAAC,OAAO,EAAE,gBAAgB;IAuBjD,kBAAkB,CAAC,OAAO,EAAE,WAAW,GAAG,cAAc;CA8EzD"}
1
+ {"version":3,"file":"DocSynchronizer.d.ts","sourceRoot":"","sources":["../../src/synchronizer/DocSynchronizer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,2BAA2B,CAAA;AAG9C,OAAO,EACL,SAAS,EAKV,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAEL,gBAAgB,EAEhB,WAAW,EACX,cAAc,EACd,WAAW,EAEZ,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAGhD,KAAK,kBAAkB,GAAG,SAAS,GAAG,KAAK,GAAG,aAAa,GAAG,OAAO,CAAA;AAOrE,UAAU,qBAAqB;IAC7B,MAAM,EAAE,SAAS,CAAC,OAAO,CAAC,CAAA;IAC1B,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,CAAC,CAAC,SAAS,GAAG,SAAS,CAAA;CAC9D;AAED;;;GAGG;AACH,qBAAa,eAAgB,SAAQ,YAAY;;IAE/C,gBAAgB,SAAM;gBAsBV,EAAE,MAAM,EAAE,eAAe,EAAE,EAAE,qBAAqB;IAyB9D,IAAI,UAAU,uCAEb;IAED,IAAI,UAAU,qCAEb;IA8HD,OAAO,CAAC,MAAM,EAAE,MAAM;IAItB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE;IA+C3B,OAAO,CAAC,MAAM,EAAE,MAAM;IAKtB,cAAc,CAAC,OAAO,EAAE,WAAW;IAkBnC,uBAAuB,CAAC,OAAO,EAAE,gBAAgB;IAuBjD,kBAAkB,CAAC,OAAO,EAAE,WAAW,GAAG,cAAc;CA8EzD"}
@@ -66,9 +66,7 @@ export class DocSynchronizer extends Synchronizer {
66
66
  this.emit("message", message);
67
67
  }
68
68
  #withSyncState(peerId, callback) {
69
- if (!this.#peers.includes(peerId)) {
70
- this.#peers.push(peerId);
71
- }
69
+ this.#addPeer(peerId);
72
70
  if (!(peerId in this.#peerDocumentStatuses)) {
73
71
  this.#peerDocumentStatuses[peerId] = "unknown";
74
72
  }
@@ -86,6 +84,12 @@ export class DocSynchronizer extends Synchronizer {
86
84
  }
87
85
  pendingCallbacks.push(callback);
88
86
  }
87
+ #addPeer(peerId) {
88
+ if (!this.#peers.includes(peerId)) {
89
+ this.#peers.push(peerId);
90
+ this.emit("open-doc", { documentId: this.documentId, peerId });
91
+ }
92
+ }
89
93
  #initSyncState(peerId, syncState) {
90
94
  const pendingCallbacks = this.#pendingSyncStateCallbacks[peerId];
91
95
  if (pendingCallbacks) {
@@ -1,10 +1,11 @@
1
1
  import { EventEmitter } from "eventemitter3";
2
- import { MessageContents, RepoMessage, SyncStateMessage } from "../network/messages.js";
2
+ import { MessageContents, OpenDocMessage, RepoMessage, SyncStateMessage } from "../network/messages.js";
3
3
  export declare abstract class Synchronizer extends EventEmitter<SynchronizerEvents> {
4
4
  abstract receiveMessage(message: RepoMessage): void;
5
5
  }
6
6
  export interface SynchronizerEvents {
7
7
  message: (arg: MessageContents) => void;
8
8
  "sync-state": (arg: SyncStateMessage) => void;
9
+ "open-doc": (arg: OpenDocMessage) => void;
9
10
  }
10
11
  //# sourceMappingURL=Synchronizer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Synchronizer.d.ts","sourceRoot":"","sources":["../../src/synchronizer/Synchronizer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC5C,OAAO,EACL,eAAe,EACf,WAAW,EACX,gBAAgB,EACjB,MAAM,wBAAwB,CAAA;AAE/B,8BAAsB,YAAa,SAAQ,YAAY,CAAC,kBAAkB,CAAC;IACzE,QAAQ,CAAC,cAAc,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI;CACpD;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,CAAC,GAAG,EAAE,eAAe,KAAK,IAAI,CAAA;IACvC,YAAY,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,IAAI,CAAA;CAC9C"}
1
+ {"version":3,"file":"Synchronizer.d.ts","sourceRoot":"","sources":["../../src/synchronizer/Synchronizer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC5C,OAAO,EACL,eAAe,EACf,cAAc,EACd,WAAW,EACX,gBAAgB,EACjB,MAAM,wBAAwB,CAAA;AAE/B,8BAAsB,YAAa,SAAQ,YAAY,CAAC,kBAAkB,CAAC;IACzE,QAAQ,CAAC,cAAc,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI;CACpD;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,CAAC,GAAG,EAAE,eAAe,KAAK,IAAI,CAAA;IACvC,YAAY,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,IAAI,CAAA;IAC7C,UAAU,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,IAAI,CAAA;CAC1C"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automerge/automerge-repo",
3
- "version": "1.1.0-alpha.3",
3
+ "version": "1.1.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>",
@@ -19,10 +19,11 @@
19
19
  "crypto": false
20
20
  },
21
21
  "devDependencies": {
22
- "http-server": "^14.1.0"
22
+ "http-server": "^14.1.0",
23
+ "vite": "^4.4.11"
23
24
  },
24
25
  "dependencies": {
25
- "@automerge/automerge": "^2.1.8-alpha.2",
26
+ "@automerge/automerge": "^2.1.8",
26
27
  "bs58check": "^3.0.1",
27
28
  "cbor-x": "^1.3.0",
28
29
  "debug": "^4.3.4",
@@ -54,5 +55,5 @@
54
55
  "publishConfig": {
55
56
  "access": "public"
56
57
  },
57
- "gitHead": "0d76620579403005a01d4205ee15fd08a85d445c"
58
+ "gitHead": "78e8cd7e0a6dae17c1bb293afcceaa40fcf02639"
58
59
  }
@@ -44,6 +44,9 @@ export class RemoteHeadsSubscriptions extends EventEmitter<RemoteHeadsSubscripti
44
44
  #theirSubscriptions: Map<StorageId, Set<PeerId>> = new Map()
45
45
  // Peers we will always share remote heads with even if they are not subscribed
46
46
  #generousPeers: Set<PeerId> = new Set()
47
+ // Documents each peer has open, we need this information so we only send remote heads of documents that the peer knows
48
+ #subscribedDocsByPeer: Map<PeerId, Set<DocumentId>> = new Map()
49
+
47
50
  #log = debug("automerge-repo:remote-heads-subscriptions")
48
51
 
49
52
  subscribeToRemotes(remotes: StorageId[]) {
@@ -89,11 +92,17 @@ export class RemoteHeadsSubscriptions extends EventEmitter<RemoteHeadsSubscripti
89
92
  handleControlMessage(control: RemoteSubscriptionControlMessage) {
90
93
  const remotesToAdd: StorageId[] = []
91
94
  const remotesToRemove: StorageId[] = []
95
+ const addedRemotesWeKnow: StorageId[] = []
92
96
 
93
97
  this.#log("handleControlMessage", control)
94
98
  if (control.add) {
95
99
  for (const remote of control.add) {
96
100
  let theirSubs = this.#theirSubscriptions.get(remote)
101
+
102
+ if (this.#ourSubscriptions.has(remote) || theirSubs) {
103
+ addedRemotesWeKnow.push(remote)
104
+ }
105
+
97
106
  if (!theirSubs) {
98
107
  theirSubs = new Set()
99
108
  this.#theirSubscriptions.set(remote, theirSubs)
@@ -128,6 +137,30 @@ export class RemoteHeadsSubscriptions extends EventEmitter<RemoteHeadsSubscripti
128
137
  remove: remotesToRemove,
129
138
  })
130
139
  }
140
+
141
+ // send all our stored heads of documents the peer knows for the remotes they've added
142
+ for (const remote of addedRemotesWeKnow) {
143
+ const subscribedDocs = this.#subscribedDocsByPeer.get(control.senderId)
144
+ if (subscribedDocs) {
145
+ for (const documentId of subscribedDocs) {
146
+ const knownHeads = this.#knownHeads.get(documentId)
147
+ if (!knownHeads) {
148
+ continue
149
+ }
150
+
151
+ const lastHeads = knownHeads.get(remote)
152
+ if (lastHeads) {
153
+ this.emit("notify-remote-heads", {
154
+ targetId: control.senderId,
155
+ documentId,
156
+ heads: lastHeads.heads,
157
+ timestamp: lastHeads.timestamp,
158
+ storageId: remote,
159
+ })
160
+ }
161
+ }
162
+ }
163
+ }
131
164
  }
132
165
 
133
166
  /** A peer we are not directly connected to has changed their heads */
@@ -165,13 +198,15 @@ export class RemoteHeadsSubscriptions extends EventEmitter<RemoteHeadsSubscripti
165
198
  const theirSubs = this.#theirSubscriptions.get(event.storageId)
166
199
  if (theirSubs) {
167
200
  for (const peerId of theirSubs) {
168
- this.emit("notify-remote-heads", {
169
- targetId: peerId,
170
- documentId: event.documentId,
171
- heads: event.remoteHeads,
172
- timestamp: event.timestamp,
173
- storageId: event.storageId,
174
- })
201
+ if (this.#isPeerSubscribedToDoc(peerId, event.documentId)) {
202
+ this.emit("notify-remote-heads", {
203
+ targetId: peerId,
204
+ documentId: event.documentId,
205
+ heads: event.remoteHeads,
206
+ timestamp: event.timestamp,
207
+ storageId: event.storageId,
208
+ })
209
+ }
175
210
  }
176
211
  }
177
212
  }
@@ -200,13 +235,15 @@ export class RemoteHeadsSubscriptions extends EventEmitter<RemoteHeadsSubscripti
200
235
  const theirSubs = this.#theirSubscriptions.get(storageId)
201
236
  if (theirSubs) {
202
237
  for (const peerId of theirSubs) {
203
- this.emit("notify-remote-heads", {
204
- targetId: peerId,
205
- documentId: documentId,
206
- heads: heads,
207
- timestamp: timestamp,
208
- storageId: storageId,
209
- })
238
+ if (this.#isPeerSubscribedToDoc(peerId, documentId)) {
239
+ this.emit("notify-remote-heads", {
240
+ targetId: peerId,
241
+ documentId: documentId,
242
+ heads: heads,
243
+ timestamp: timestamp,
244
+ storageId: storageId,
245
+ })
246
+ }
210
247
  }
211
248
  }
212
249
  }
@@ -241,6 +278,7 @@ export class RemoteHeadsSubscriptions extends EventEmitter<RemoteHeadsSubscripti
241
278
  const remotesToRemove = []
242
279
 
243
280
  this.#generousPeers.delete(peerId)
281
+ this.#subscribedDocsByPeer.delete(peerId)
244
282
 
245
283
  for (const [storageId, peerIds] of this.#theirSubscriptions) {
246
284
  if (peerIds.has(peerId)) {
@@ -261,6 +299,37 @@ export class RemoteHeadsSubscriptions extends EventEmitter<RemoteHeadsSubscripti
261
299
  }
262
300
  }
263
301
 
302
+ subscribePeerToDoc(peerId: PeerId, documentId: DocumentId) {
303
+ let subscribedDocs = this.#subscribedDocsByPeer.get(peerId)
304
+ if (!subscribedDocs) {
305
+ subscribedDocs = new Set()
306
+ this.#subscribedDocsByPeer.set(peerId, subscribedDocs)
307
+ }
308
+
309
+ subscribedDocs.add(documentId)
310
+
311
+ const remoteHeads = this.#knownHeads.get(documentId)
312
+ if (remoteHeads) {
313
+ for (const [storageId, lastHeads] of remoteHeads) {
314
+ const subscribedPeers = this.#theirSubscriptions.get(storageId)
315
+ if (subscribedPeers && subscribedPeers.has(peerId)) {
316
+ this.emit("notify-remote-heads", {
317
+ targetId: peerId,
318
+ documentId,
319
+ heads: lastHeads.heads,
320
+ timestamp: lastHeads.timestamp,
321
+ storageId,
322
+ })
323
+ }
324
+ }
325
+ }
326
+ }
327
+
328
+ #isPeerSubscribedToDoc(peerId: PeerId, documentId: DocumentId) {
329
+ let subscribedDocs = this.#subscribedDocsByPeer.get(peerId)
330
+ return subscribedDocs && subscribedDocs.has(documentId)
331
+ }
332
+
264
333
  /** Returns the (document, storageId) pairs which have changed after processing msg */
265
334
  #changedHeads(msg: RemoteHeadsChanged): {
266
335
  documentId: DocumentId
package/src/Repo.ts CHANGED
@@ -140,6 +140,10 @@ export class Repo extends EventEmitter<RepoEvents> {
140
140
  networkSubsystem.send(message)
141
141
  })
142
142
 
143
+ this.#synchronizer.on("open-doc", ({ peerId, documentId }) => {
144
+ this.#remoteHeadsSubscriptions.subscribePeerToDoc(peerId, documentId)
145
+ })
146
+
143
147
  // STORAGE
144
148
  // The storage subsystem has access to some form of persistence, and deals with save and loading documents.
145
149
  const storageSubsystem = storage ? new StorageSubsystem(storage) : undefined
@@ -332,6 +336,10 @@ export class Repo extends EventEmitter<RepoEvents> {
332
336
  return this.#synchronizer.peers
333
337
  }
334
338
 
339
+ getStorageIdOfPeer(peerId: PeerId): StorageId | undefined {
340
+ return this.peerMetadataByPeerId[peerId]?.storageId
341
+ }
342
+
335
343
  /**
336
344
  * Creates a new document and returns a handle to it. The initial value of the document is
337
345
  * an empty object `{}`. Its documentId is generated by the system. we emit a `document` event
@@ -104,19 +104,19 @@ export type AuthMessage<TPayload = any> = {
104
104
  }
105
105
 
106
106
  export type RemoteSubscriptionControlMessage = {
107
- type: "remote-subscription-change",
108
- senderId: PeerId,
109
- targetId: PeerId,
110
- add?: StorageId[],
111
- remove?: StorageId[],
107
+ type: "remote-subscription-change"
108
+ senderId: PeerId
109
+ targetId: PeerId
110
+ add?: StorageId[]
111
+ remove?: StorageId[]
112
112
  }
113
113
 
114
114
  export type RemoteHeadsChanged = {
115
- type: "remote-heads-changed",
116
- senderId: PeerId,
117
- targetId: PeerId,
118
- documentId: DocumentId,
119
- newHeads: {[key: StorageId]: {heads: string[], timestamp: number}},
115
+ type: "remote-heads-changed"
116
+ senderId: PeerId
117
+ targetId: PeerId
118
+ documentId: DocumentId
119
+ newHeads: { [key: StorageId]: { heads: string[]; timestamp: number } }
120
120
  }
121
121
 
122
122
  /** These are message types that a {@link NetworkAdapter} surfaces to a {@link Repo}. */
@@ -128,7 +128,11 @@ export type RepoMessage =
128
128
  | RemoteSubscriptionControlMessage
129
129
  | RemoteHeadsChanged
130
130
 
131
- export type DocMessage = SyncMessage | EphemeralMessage | RequestMessage | DocumentUnavailableMessage
131
+ export type DocMessage =
132
+ | SyncMessage
133
+ | EphemeralMessage
134
+ | RequestMessage
135
+ | DocumentUnavailableMessage
132
136
 
133
137
  /** These are all the message types that a {@link NetworkAdapter} might see. */
134
138
  export type Message = RepoMessage | AuthMessage
@@ -148,6 +152,12 @@ export interface SyncStateMessage {
148
152
  syncState: SyncState
149
153
  }
150
154
 
155
+ /** Notify the repo that a peer started syncing with a doc */
156
+ export interface OpenDocMessage {
157
+ peerId: PeerId
158
+ documentId: DocumentId
159
+ }
160
+
151
161
  // TYPE GUARDS
152
162
 
153
163
  export const isValidRepoMessage = (message: Message): message is RepoMessage =>
@@ -174,7 +184,9 @@ export const isSyncMessage = (msg: Message): msg is SyncMessage =>
174
184
  export const isEphemeralMessage = (msg: Message): msg is EphemeralMessage =>
175
185
  msg.type === "ephemeral"
176
186
 
177
- export const isRemoteSubscriptionControlMessage = (msg: Message): msg is RemoteSubscriptionControlMessage =>
187
+ export const isRemoteSubscriptionControlMessage = (
188
+ msg: Message
189
+ ): msg is RemoteSubscriptionControlMessage =>
178
190
  msg.type === "remote-subscription-change"
179
191
 
180
192
  export const isRemoteHeadsChanged = (msg: Message): msg is RemoteHeadsChanged =>
@@ -55,6 +55,7 @@ export class CollectionSynchronizer extends Synchronizer {
55
55
  },
56
56
  })
57
57
  docSynchronizer.on("message", event => this.emit("message", event))
58
+ docSynchronizer.on("open-doc", event => this.emit("open-doc", event))
58
59
  docSynchronizer.on("sync-state", event => this.emit("sync-state", event))
59
60
  return docSynchronizer
60
61
  }
@@ -20,7 +20,6 @@ import {
20
20
  import { PeerId } from "../types.js"
21
21
  import { Synchronizer } from "./Synchronizer.js"
22
22
  import { throttle } from "../helpers/throttle.js"
23
- import { headsAreSame } from "../helpers/headsAreSame.js"
24
23
 
25
24
  type PeerDocumentStatus = "unknown" | "has" | "unavailable" | "wants"
26
25
 
@@ -124,9 +123,7 @@ export class DocSynchronizer extends Synchronizer {
124
123
  }
125
124
 
126
125
  #withSyncState(peerId: PeerId, callback: (syncState: A.SyncState) => void) {
127
- if (!this.#peers.includes(peerId)) {
128
- this.#peers.push(peerId)
129
- }
126
+ this.#addPeer(peerId)
130
127
 
131
128
  if (!(peerId in this.#peerDocumentStatuses)) {
132
129
  this.#peerDocumentStatuses[peerId] = "unknown"
@@ -149,6 +146,13 @@ export class DocSynchronizer extends Synchronizer {
149
146
  pendingCallbacks.push(callback)
150
147
  }
151
148
 
149
+ #addPeer(peerId: PeerId) {
150
+ if (!this.#peers.includes(peerId)) {
151
+ this.#peers.push(peerId)
152
+ this.emit("open-doc", { documentId: this.documentId, peerId })
153
+ }
154
+ }
155
+
152
156
  #initSyncState(peerId: PeerId, syncState: A.SyncState) {
153
157
  const pendingCallbacks = this.#pendingSyncStateCallbacks[peerId]
154
158
  if (pendingCallbacks) {
@@ -1,6 +1,7 @@
1
1
  import { EventEmitter } from "eventemitter3"
2
2
  import {
3
3
  MessageContents,
4
+ OpenDocMessage,
4
5
  RepoMessage,
5
6
  SyncStateMessage,
6
7
  } from "../network/messages.js"
@@ -12,4 +13,5 @@ export abstract class Synchronizer extends EventEmitter<SynchronizerEvents> {
12
13
  export interface SynchronizerEvents {
13
14
  message: (arg: MessageContents) => void
14
15
  "sync-state": (arg: SyncStateMessage) => void
16
+ "open-doc": (arg: OpenDocMessage) => void
15
17
  }
@@ -1,15 +1,14 @@
1
1
  import * as A from "@automerge/automerge"
2
2
  import assert from "assert"
3
3
  import { describe, it } from "vitest"
4
+ import { generateAutomergeUrl, parseAutomergeUrl } from "../src/AutomergeUrl.js"
4
5
  import { RemoteHeadsSubscriptions } from "../src/RemoteHeadsSubscriptions.js"
5
6
  import { PeerId, StorageId } from "../src/index.js"
6
- import { generateAutomergeUrl, parseAutomergeUrl } from "../src/AutomergeUrl.js"
7
- import { pause } from "../src/helpers/pause.js"
8
- import { EventEmitter } from "eventemitter3"
9
7
  import {
10
8
  RemoteHeadsChanged,
11
9
  RemoteSubscriptionControlMessage,
12
10
  } from "../src/network/messages.js"
11
+ import { waitForMessages } from "./helpers/waitForMessages.js"
13
12
 
14
13
  describe("RepoHeadsSubscriptions", () => {
15
14
  const storageA = "remote-a" as StorageId
@@ -224,6 +223,8 @@ describe("RepoHeadsSubscriptions", () => {
224
223
  remoteHeadsSubscriptions,
225
224
  "notify-remote-heads"
226
225
  )
226
+ remoteHeadsSubscriptions.subscribePeerToDoc(peerC, docA)
227
+ remoteHeadsSubscriptions.subscribePeerToDoc(peerC, docC)
227
228
 
228
229
  // change message for docA in storageB
229
230
  remoteHeadsSubscriptions.handleRemoteHeads(docAHeadsChangedForStorageB)
@@ -260,6 +261,32 @@ describe("RepoHeadsSubscriptions", () => {
260
261
  assert.strictEqual(messages.length, 0)
261
262
  })
262
263
 
264
+ it("should not send remote heads for docs that the peer is not subscribed to", async () => {
265
+ const remoteHeadsSubscriptions = new RemoteHeadsSubscriptions()
266
+ remoteHeadsSubscriptions.subscribeToRemotes([storageB])
267
+
268
+ // subscribe peer c to storage b
269
+ remoteHeadsSubscriptions.handleControlMessage(subscribePeerCToStorageB)
270
+ const messagesAfterSubscribePromise = waitForMessages(
271
+ remoteHeadsSubscriptions,
272
+ "notify-remote-heads"
273
+ )
274
+
275
+ // change message for docA in storageB
276
+ remoteHeadsSubscriptions.handleRemoteHeads(docAHeadsChangedForStorageB)
277
+
278
+ // change heads directly
279
+ remoteHeadsSubscriptions.handleImmediateRemoteHeadsChanged(
280
+ docC,
281
+ storageB,
282
+ []
283
+ )
284
+
285
+ // expect peer c to be notified both changes
286
+ let messages = await messagesAfterSubscribePromise
287
+ assert.strictEqual(messages.length, 0)
288
+ })
289
+
263
290
  it("should ignore sync states with an older timestamp", async () => {
264
291
  const remoteHeadsSubscription = new RemoteHeadsSubscriptions()
265
292
 
@@ -321,23 +348,3 @@ describe("RepoHeadsSubscriptions", () => {
321
348
  assert.deepStrictEqual(messages[2].peers, [])
322
349
  })
323
350
  })
324
-
325
- async function waitForMessages(
326
- emitter: EventEmitter,
327
- event: string,
328
- timeout: number = 100
329
- ): Promise<any[]> {
330
- const messages = []
331
-
332
- const onEvent = message => {
333
- messages.push(message)
334
- }
335
-
336
- emitter.on(event, onEvent)
337
-
338
- await pause(timeout)
339
-
340
- emitter.off(event)
341
-
342
- return messages
343
- }
@@ -0,0 +1,22 @@
1
+ import { EventEmitter } from "eventemitter3"
2
+ import { pause } from "../../src/helpers/pause.js"
3
+
4
+ export async function waitForMessages(
5
+ emitter: EventEmitter,
6
+ event: string,
7
+ timeout: number = 100
8
+ ): Promise<any[]> {
9
+ const messages = []
10
+
11
+ const onEvent = message => {
12
+ messages.push(message)
13
+ }
14
+
15
+ emitter.on(event, onEvent)
16
+
17
+ await pause(timeout)
18
+
19
+ emitter.off(event)
20
+
21
+ return messages
22
+ }
@@ -1,20 +1,19 @@
1
+ import { MessageChannelNetworkAdapter } from "@automerge/automerge-repo-network-messagechannel"
1
2
  import * as A from "@automerge/automerge/next"
2
3
  import assert from "assert"
3
- import { decode } from "cbor-x"
4
+ import { setTimeout } from "timers/promises"
4
5
  import { describe, it } from "vitest"
5
6
  import { generateAutomergeUrl, parseAutomergeUrl } from "../src/AutomergeUrl.js"
6
7
  import { eventPromise } from "../src/helpers/eventPromise.js"
7
- import { pause } from "../src/helpers/pause.js"
8
8
  import {
9
9
  DocHandle,
10
10
  DocHandleRemoteHeadsPayload,
11
11
  PeerId,
12
12
  Repo,
13
13
  } from "../src/index.js"
14
- import { TestDoc } from "./types.js"
15
- import { MessageChannelNetworkAdapter } from "@automerge/automerge-repo-network-messagechannel"
16
- import { setTimeout } from "timers/promises"
17
14
  import { DummyStorageAdapter } from "./helpers/DummyStorageAdapter.js"
15
+ import { waitForMessages } from "./helpers/waitForMessages.js"
16
+ import { TestDoc } from "./types.js"
18
17
 
19
18
  describe("DocHandle.remoteHeads", () => {
20
19
  const TEST_ID = parseAutomergeUrl(generateAutomergeUrl()).documentId
@@ -40,86 +39,207 @@ describe("DocHandle.remoteHeads", () => {
40
39
  assert.deepStrictEqual(handle.getRemoteHeads(bobStorageId), [])
41
40
  })
42
41
 
43
- it("should report remoteHeads for peers who are several hops away", async () => {
44
- // replicates a tab -> service worker -> sync server <- service worker <- tab scenario
45
- const leftTab = new Repo({
46
- peerId: "left-tab" as PeerId,
47
- network: [],
48
- sharePolicy: async () => true,
49
- })
50
- const leftServiceWorker = new Repo({
51
- peerId: "left-service-worker" as PeerId,
52
- network: [],
53
- sharePolicy: async peer => peer === "sync-server",
54
- storage: new DummyStorageAdapter(),
55
- isEphemeral: false,
56
- })
57
- const syncServer = new Repo({
58
- peerId: "sync-server" as PeerId,
59
- network: [],
60
- isEphemeral: false,
61
- sharePolicy: async () => false,
62
- storage: new DummyStorageAdapter(),
63
- })
64
- const rightServiceWorker = new Repo({
65
- peerId: "right-service-worker" as PeerId,
66
- network: [],
67
- sharePolicy: async peer => peer === "sync-server",
68
- isEphemeral: false,
69
- storage: new DummyStorageAdapter(),
42
+ describe("multi hop sync", () => {
43
+ async function setup() {
44
+ // setup topology: tab -> service worker -> sync server <- service worker <- tab
45
+ const leftTab1 = new Repo({
46
+ peerId: "left-tab-1" as PeerId,
47
+ network: [],
48
+ sharePolicy: async () => true,
49
+ })
50
+ const leftTab2 = new Repo({
51
+ peerId: "left-tab-2" as PeerId,
52
+ network: [],
53
+ sharePolicy: async () => true,
54
+ })
55
+ const leftServiceWorker = new Repo({
56
+ peerId: "left-service-worker" as PeerId,
57
+ network: [],
58
+ sharePolicy: async peer => peer === "sync-server",
59
+ storage: new DummyStorageAdapter(),
60
+ isEphemeral: false,
61
+ })
62
+ const syncServer = new Repo({
63
+ peerId: "sync-server" as PeerId,
64
+ network: [],
65
+ isEphemeral: false,
66
+ sharePolicy: async () => false,
67
+ storage: new DummyStorageAdapter(),
68
+ })
69
+ const rightServiceWorker = new Repo({
70
+ peerId: "right-service-worker" as PeerId,
71
+ network: [],
72
+ sharePolicy: async peer => peer === "sync-server",
73
+ isEphemeral: false,
74
+ storage: new DummyStorageAdapter(),
75
+ })
76
+ const rightTab = new Repo({
77
+ peerId: "right-tab" as PeerId,
78
+ network: [],
79
+ sharePolicy: async () => true,
80
+ })
81
+
82
+ // connect them all up
83
+ connectRepos(leftTab1, leftServiceWorker)
84
+ connectRepos(leftTab2, leftServiceWorker)
85
+ connectRepos(leftServiceWorker, syncServer)
86
+ connectRepos(syncServer, rightServiceWorker)
87
+ connectRepos(rightServiceWorker, rightTab)
88
+
89
+ await setTimeout(100)
90
+
91
+ return {
92
+ leftTab1,
93
+ leftTab2,
94
+ leftServiceWorker,
95
+ syncServer,
96
+ rightServiceWorker,
97
+ rightTab,
98
+ }
99
+ }
100
+
101
+ it("should report remoteHeads for peers", async () => {
102
+ const { rightTab, rightServiceWorker, leftServiceWorker, leftTab1 } =
103
+ await setup()
104
+
105
+ // subscribe to the left service worker storage ID on the right tab
106
+ rightTab.subscribeToRemotes([await leftServiceWorker.storageId()!])
107
+
108
+ await setTimeout(100)
109
+
110
+ // create a doc in the left tab
111
+ const leftTabDoc = leftTab1.create<TestDoc>()
112
+ leftTabDoc.change(d => (d.foo = "bar"))
113
+
114
+ // wait for the document to arrive on the right tab
115
+ const rightTabDoc = rightTab.find<TestDoc>(leftTabDoc.url)
116
+ await rightTabDoc.whenReady()
117
+
118
+ // wait for the document to arrive in the left service worker
119
+ const leftServiceWorkerDoc = leftServiceWorker.find(leftTabDoc.documentId)
120
+ await leftServiceWorkerDoc.whenReady()
121
+
122
+ const leftServiceWorkerStorageId = await leftServiceWorker.storageId()
123
+ let leftSeenByRightPromise = new Promise<DocHandleRemoteHeadsPayload>(
124
+ resolve => {
125
+ rightTabDoc.on("remote-heads", message => {
126
+ if (message.storageId === leftServiceWorkerStorageId) {
127
+ resolve(message)
128
+ }
129
+ })
130
+ }
131
+ )
132
+
133
+ // make a change on the right
134
+ rightTabDoc.change(d => (d.foo = "baz"))
135
+
136
+ // wait for the change to be acknolwedged by the left
137
+ const leftSeenByRight = await leftSeenByRightPromise
138
+
139
+ assert.deepStrictEqual(
140
+ leftSeenByRight.heads,
141
+ A.getHeads(leftServiceWorkerDoc.docSync())
142
+ )
70
143
  })
71
- const rightTab = new Repo({
72
- peerId: "right-tab" as PeerId,
73
- network: [],
74
- sharePolicy: async () => true,
144
+
145
+ it("should report remoteHeads only for documents the subscriber has open", async () => {
146
+ const { leftTab1, rightTab, rightServiceWorker } = await setup()
147
+
148
+ // subscribe leftTab to storageId of rightServiceWorker
149
+ leftTab1.subscribeToRemotes([await rightServiceWorker.storageId()!])
150
+
151
+ await setTimeout(100)
152
+
153
+ // create 2 docs in right tab
154
+ const rightTabDocA = rightTab.create<TestDoc>()
155
+ rightTabDocA.change(d => (d.foo = "A"))
156
+
157
+ const rightTabDocB = rightTab.create<TestDoc>()
158
+ rightTabDocB.change(d => (d.foo = "B"))
159
+
160
+ // open doc b in left tab 1
161
+ const leftTabDocA = leftTab1.find<TestDoc>(rightTabDocA.url)
162
+
163
+ const remoteHeadsChangedMessages = (
164
+ await waitForMessages(leftTab1.networkSubsystem, "message")
165
+ ).filter(({ type }) => type === "remote-heads-changed")
166
+
167
+ // we should only be notified of the head changes of doc A
168
+ assert.strictEqual(remoteHeadsChangedMessages.length, 1)
169
+ assert.strictEqual(
170
+ remoteHeadsChangedMessages[0].documentId,
171
+ leftTabDocA.documentId
172
+ )
75
173
  })
76
174
 
77
- // connect them all up
78
- connectRepos(leftTab, leftServiceWorker)
79
- connectRepos(leftServiceWorker, syncServer)
80
- connectRepos(syncServer, rightServiceWorker)
81
- connectRepos(rightServiceWorker, rightTab)
175
+ it("should report remote heads for doc on subscribe if peer already knows them", async () => {
176
+ const { leftTab1, leftTab2, rightTab, rightServiceWorker } = await setup()
82
177
 
83
- await setTimeout(100)
178
+ // create 2 docs in right tab
179
+ const rightTabDocA = rightTab.create<TestDoc>()
180
+ rightTabDocA.change(d => (d.foo = "A"))
84
181
 
85
- // subscribe to the left service worker storage ID on the right tab
86
- rightTab.subscribeToRemotes([await leftServiceWorker.storageId()!])
182
+ const rightTabDocB = rightTab.create<TestDoc>()
183
+ rightTabDocB.change(d => (d.foo = "B"))
87
184
 
88
- await setTimeout(100)
185
+ // open docs in left tab 1
186
+ const leftTab1DocA = leftTab1.find<TestDoc>(rightTabDocA.url)
187
+ const leftTab1DocB = leftTab1.find<TestDoc>(rightTabDocB.url)
89
188
 
90
- // create a doc in the left tab
91
- const leftTabDoc = leftTab.create<TestDoc>()
92
- leftTabDoc.change(d => (d.foo = "bar"))
189
+ // subscribe leftTab 1 to storageId of rightServiceWorker
190
+ leftTab1.subscribeToRemotes([await rightServiceWorker.storageId()!])
93
191
 
94
- // wait for the document to arrive on the right tab
95
- const rightTabDoc = rightTab.find<TestDoc>(leftTabDoc.url)
96
- await rightTabDoc.whenReady()
192
+ await setTimeout(200)
97
193
 
98
- // wait for the document to arrive in the left service worker
99
- const leftServiceWorkerDoc = leftServiceWorker.find(leftTabDoc.documentId)
100
- await leftServiceWorkerDoc.whenReady()
194
+ // now the left service worker has the remote heads of the right service worker for both doc A and doc B
195
+ // if we subscribe from left tab 1 the left service workers should send it's stored remote heads immediately
101
196
 
102
- const leftServiceWorkerStorageId = await leftServiceWorker.storageId()
103
- let leftSeenByRightPromise = new Promise<DocHandleRemoteHeadsPayload>(
104
- resolve => {
105
- rightTabDoc.on("remote-heads", message => {
106
- if (message.storageId === leftServiceWorkerStorageId) {
107
- resolve(message)
108
- }
109
- })
110
- }
111
- )
197
+ // open doc and subscribe leftTab 2 to storageId of rightServiceWorker
198
+ const leftTab2DocA = leftTab2.find<TestDoc>(rightTabDocA.url)
199
+ leftTab2.subscribeToRemotes([await rightServiceWorker.storageId()!])
200
+
201
+ const remoteHeadsChangedMessages = (
202
+ await waitForMessages(leftTab2.networkSubsystem, "message")
203
+ ).filter(({ type }) => type === "remote-heads-changed")
112
204
 
113
- // make a change on the right
114
- rightTabDoc.change(d => (d.foo = "baz"))
205
+ // we should only be notified of the head changes of doc A
206
+ assert.strictEqual(remoteHeadsChangedMessages.length, 1)
207
+ assert.strictEqual(
208
+ remoteHeadsChangedMessages[0].documentId,
209
+ leftTab1DocA.documentId
210
+ )
211
+ })
212
+
213
+ it("should report remote heads for subscribed storage id once we open a new doc", async () => {
214
+ const { leftTab1, leftTab2, rightTab, rightServiceWorker } = await setup()
215
+
216
+ // create 2 docs in right tab
217
+ const rightTabDocA = rightTab.create<TestDoc>()
218
+ rightTabDocA.change(d => (d.foo = "A"))
219
+
220
+ const rightTabDocB = rightTab.create<TestDoc>()
221
+ rightTabDocB.change(d => (d.foo = "B"))
222
+
223
+ await setTimeout(200)
115
224
 
116
- // wait for the change to be acknolwedged by the left
117
- const leftSeenByRight = await leftSeenByRightPromise
225
+ // subscribe leftTab 1 to storageId of rightServiceWorker
226
+ leftTab1.subscribeToRemotes([await rightServiceWorker.storageId()!])
118
227
 
119
- assert.deepStrictEqual(
120
- leftSeenByRight.heads,
121
- A.getHeads(leftServiceWorkerDoc.docSync())
122
- )
228
+ // in leftTab 1 open doc A
229
+ const leftTab1DocA = leftTab1.find<TestDoc>(rightTabDocA.url)
230
+
231
+ const remoteHeadsChangedMessages = (
232
+ await waitForMessages(leftTab1.networkSubsystem, "message")
233
+ ).filter(({ type }) => type === "remote-heads-changed")
234
+
235
+ console.log(JSON.stringify(remoteHeadsChangedMessages, null, 2))
236
+
237
+ assert.strictEqual(remoteHeadsChangedMessages.length, 1)
238
+ assert.strictEqual(
239
+ remoteHeadsChangedMessages[0].documentId,
240
+ leftTab1DocA.documentId
241
+ )
242
+ })
123
243
  })
124
244
  })
125
245