@automerge/automerge-repo 1.2.1 → 2.0.0-alpha.1

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 (44) hide show
  1. package/dist/AutomergeUrl.d.ts +3 -3
  2. package/dist/AutomergeUrl.d.ts.map +1 -1
  3. package/dist/AutomergeUrl.js +5 -1
  4. package/dist/DocHandle.d.ts +11 -10
  5. package/dist/DocHandle.d.ts.map +1 -1
  6. package/dist/DocHandle.js +23 -43
  7. package/dist/Repo.d.ts +1 -1
  8. package/dist/Repo.d.ts.map +1 -1
  9. package/dist/Repo.js +53 -36
  10. package/dist/entrypoints/slim.d.ts +1 -0
  11. package/dist/entrypoints/slim.d.ts.map +1 -1
  12. package/dist/entrypoints/slim.js +1 -0
  13. package/dist/helpers/DummyNetworkAdapter.d.ts +3 -0
  14. package/dist/helpers/DummyNetworkAdapter.d.ts.map +1 -1
  15. package/dist/helpers/DummyNetworkAdapter.js +24 -5
  16. package/dist/helpers/tests/network-adapter-tests.d.ts.map +1 -1
  17. package/dist/helpers/tests/network-adapter-tests.js +88 -1
  18. package/dist/helpers/throttle.d.ts +1 -1
  19. package/dist/helpers/throttle.js +1 -1
  20. package/dist/network/NetworkAdapter.d.ts +2 -0
  21. package/dist/network/NetworkAdapter.d.ts.map +1 -1
  22. package/dist/network/NetworkAdapterInterface.d.ts +2 -2
  23. package/dist/network/NetworkAdapterInterface.d.ts.map +1 -1
  24. package/dist/network/NetworkSubsystem.d.ts +5 -2
  25. package/dist/network/NetworkSubsystem.d.ts.map +1 -1
  26. package/dist/network/NetworkSubsystem.js +21 -25
  27. package/package.json +3 -3
  28. package/src/AutomergeUrl.ts +6 -6
  29. package/src/DocHandle.ts +27 -57
  30. package/src/Repo.ts +55 -40
  31. package/src/entrypoints/slim.ts +1 -0
  32. package/src/helpers/DummyNetworkAdapter.ts +29 -5
  33. package/src/helpers/tests/network-adapter-tests.ts +121 -1
  34. package/src/helpers/throttle.ts +1 -1
  35. package/src/network/NetworkAdapter.ts +3 -0
  36. package/src/network/NetworkAdapterInterface.ts +4 -3
  37. package/src/network/NetworkSubsystem.ts +24 -31
  38. package/test/AutomergeUrl.test.ts +4 -0
  39. package/test/DocHandle.test.ts +20 -24
  40. package/test/DocSynchronizer.test.ts +5 -1
  41. package/test/NetworkSubsystem.test.ts +107 -0
  42. package/test/Repo.test.ts +37 -15
  43. package/test/remoteHeads.test.ts +3 -3
  44. package/test/Network.test.ts +0 -14
@@ -1,6 +1,6 @@
1
1
  import assert from "assert";
2
2
  import { describe, expect, it } from "vitest";
3
- import { Repo } from "../../index.js";
3
+ import { generateAutomergeUrl, parseAutomergeUrl, Repo, } from "../../index.js";
4
4
  import { eventPromise, eventPromises } from "../eventPromise.js";
5
5
  import { pause } from "../pause.js";
6
6
  /**
@@ -125,6 +125,93 @@ export function runNetworkAdapterTests(_setup, title) {
125
125
  });
126
126
  teardown();
127
127
  });
128
+ it("should emit disconnect events on disconnect", async () => {
129
+ const { adapters, teardown } = await setup();
130
+ const left = adapters[0][0];
131
+ const right = adapters[1][0];
132
+ const leftPeerId = "left";
133
+ const rightPeerId = "right";
134
+ const leftRepo = new Repo({
135
+ network: [left],
136
+ peerId: leftPeerId,
137
+ });
138
+ const rightRepo = new Repo({
139
+ network: [right],
140
+ peerId: rightPeerId,
141
+ });
142
+ await Promise.all([
143
+ eventPromise(leftRepo.networkSubsystem, "peer"),
144
+ eventPromise(rightRepo.networkSubsystem, "peer"),
145
+ ]);
146
+ const disconnectionPromises = Promise.all([
147
+ eventPromise(leftRepo.networkSubsystem, "peer-disconnected"),
148
+ eventPromise(rightRepo.networkSubsystem, "peer-disconnected"),
149
+ ]);
150
+ left.disconnect();
151
+ await disconnectionPromises;
152
+ teardown();
153
+ });
154
+ it("should not send messages after disconnect", async () => {
155
+ const { adapters, teardown } = await setup();
156
+ const left = adapters[0][0];
157
+ const right = adapters[1][0];
158
+ const leftPeerId = "left";
159
+ const rightPeerId = "right";
160
+ const leftRepo = new Repo({
161
+ network: [left],
162
+ peerId: leftPeerId,
163
+ });
164
+ const rightRepo = new Repo({
165
+ network: [right],
166
+ peerId: rightPeerId,
167
+ });
168
+ await Promise.all([
169
+ eventPromise(rightRepo.networkSubsystem, "peer"),
170
+ eventPromise(leftRepo.networkSubsystem, "peer"),
171
+ ]);
172
+ const disconnected = eventPromise(right, "peer-disconnected");
173
+ left.disconnect();
174
+ await disconnected;
175
+ const rightReceivedFromLeft = new Promise(resolve => {
176
+ right.on("message", msg => {
177
+ if (msg.senderId === leftPeerId) {
178
+ resolve(null);
179
+ }
180
+ });
181
+ });
182
+ const rightReceived = Promise.race([rightReceivedFromLeft, pause(10)]);
183
+ const documentId = parseAutomergeUrl(generateAutomergeUrl()).documentId;
184
+ left.send({
185
+ type: "foo",
186
+ data: new Uint8Array([1, 2, 3]),
187
+ documentId,
188
+ senderId: leftPeerId,
189
+ targetId: rightPeerId,
190
+ });
191
+ assert.equal(await rightReceived, null);
192
+ teardown();
193
+ });
194
+ it("should support reconnecting after disconnect", async () => {
195
+ const { adapters, teardown } = await setup();
196
+ const left = adapters[0][0];
197
+ const right = adapters[1][0];
198
+ const leftPeerId = "left";
199
+ const rightPeerId = "right";
200
+ const _leftRepo = new Repo({
201
+ network: [left],
202
+ peerId: leftPeerId,
203
+ });
204
+ const rightRepo = new Repo({
205
+ network: [right],
206
+ peerId: rightPeerId,
207
+ });
208
+ await eventPromise(rightRepo.networkSubsystem, "peer");
209
+ left.disconnect();
210
+ await pause(10);
211
+ left.connect(leftPeerId);
212
+ await eventPromise(left, "peer-candidate");
213
+ teardown();
214
+ });
128
215
  });
129
216
  }
130
217
  const NO_OP = () => { };
@@ -20,7 +20,7 @@
20
20
  *
21
21
  *
22
22
  * Example usage:
23
- * const callback = debounce((ev) => { doSomethingExpensiveOrOccasional() }, 100)
23
+ * const callback = throttle((ev) => { doSomethingExpensiveOrOccasional() }, 100)
24
24
  * target.addEventListener('frequent-event', callback);
25
25
  *
26
26
  */
@@ -20,7 +20,7 @@
20
20
  *
21
21
  *
22
22
  * Example usage:
23
- * const callback = debounce((ev) => { doSomethingExpensiveOrOccasional() }, 100)
23
+ * const callback = throttle((ev) => { doSomethingExpensiveOrOccasional() }, 100)
24
24
  * target.addEventListener('frequent-event', callback);
25
25
  *
26
26
  */
@@ -16,6 +16,8 @@ import { NetworkAdapterInterface } from "./NetworkAdapterInterface.js";
16
16
  export declare abstract class NetworkAdapter extends EventEmitter<NetworkAdapterEvents> implements NetworkAdapterInterface {
17
17
  peerId?: PeerId;
18
18
  peerMetadata?: PeerMetadata;
19
+ abstract isReady(): boolean;
20
+ abstract whenReady(): Promise<void>;
19
21
  /** Called by the {@link Repo} to start the connection process
20
22
  *
21
23
  * @argument peerId - the peerId of this repo
@@ -1 +1 @@
1
- {"version":3,"file":"NetworkAdapter.d.ts","sourceRoot":"","sources":["../../src/network/NetworkAdapter.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC5C,OAAO,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAChE,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AACvC,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAA;AAEtE;;;;;;;;;GASG;AACH,8BAAsB,cACpB,SAAQ,YAAY,CAAC,oBAAoB,CACzC,YAAW,uBAAuB;IAElC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,YAAY,CAAC,EAAE,YAAY,CAAA;IAE3B;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,YAAY,GAAG,IAAI;IAEnE;;;OAGG;IACH,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAErC,gEAAgE;IAChE,QAAQ,CAAC,UAAU,IAAI,IAAI;CAC5B"}
1
+ {"version":3,"file":"NetworkAdapter.d.ts","sourceRoot":"","sources":["../../src/network/NetworkAdapter.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC5C,OAAO,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAChE,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AACvC,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAA;AAEtE;;;;;;;;;GASG;AACH,8BAAsB,cACpB,SAAQ,YAAY,CAAC,oBAAoB,CACzC,YAAW,uBAAuB;IAElC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,YAAY,CAAC,EAAE,YAAY,CAAA;IAE3B,QAAQ,CAAC,OAAO,IAAI,OAAO;IAC3B,QAAQ,CAAC,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAEnC;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,YAAY,GAAG,IAAI;IAEnE;;;OAGG;IACH,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAErC,gEAAgE;IAChE,QAAQ,CAAC,UAAU,IAAI,IAAI;CAC5B"}
@@ -26,6 +26,8 @@ export interface PeerMetadata {
26
26
  export interface NetworkAdapterInterface extends EventEmitter<NetworkAdapterEvents> {
27
27
  peerId?: PeerId;
28
28
  peerMetadata?: PeerMetadata;
29
+ isReady(): boolean;
30
+ whenReady(): Promise<void>;
29
31
  /** Called by the {@link Repo} to start the connection process
30
32
  *
31
33
  * @argument peerId - the peerId of this repo
@@ -41,8 +43,6 @@ export interface NetworkAdapterInterface extends EventEmitter<NetworkAdapterEven
41
43
  disconnect(): void;
42
44
  }
43
45
  export interface NetworkAdapterEvents {
44
- /** Emitted when the network is ready to be used */
45
- ready: (payload: OpenPayload) => void;
46
46
  /** Emitted when the network is closed */
47
47
  close: () => void;
48
48
  /** Emitted when the network adapter learns about a new peer */
@@ -1 +1 @@
1
- {"version":3,"file":"NetworkAdapterInterface.d.ts","sourceRoot":"","sources":["../../src/network/NetworkAdapterInterface.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AACvC,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAE/C;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,uBACf,SAAQ,YAAY,CAAC,oBAAoB,CAAC;IAC1C,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,YAAY,CAAC,EAAE,YAAY,CAAA;IAE3B;;;;OAIG;IACH,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,YAAY,GAAG,IAAI,CAAA;IAE1D;;;OAGG;IACH,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;IAE5B,gEAAgE;IAChE,UAAU,IAAI,IAAI,CAAA;CACnB;AAID,MAAM,WAAW,oBAAoB;IACnC,mDAAmD;IACnD,KAAK,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,CAAA;IAErC,yCAAyC;IACzC,KAAK,EAAE,MAAM,IAAI,CAAA;IAEjB,+DAA+D;IAC/D,gBAAgB,EAAE,CAAC,OAAO,EAAE,oBAAoB,KAAK,IAAI,CAAA;IAEzD,2EAA2E;IAC3E,mBAAmB,EAAE,CAAC,OAAO,EAAE,uBAAuB,KAAK,IAAI,CAAA;IAE/D,sEAAsE;IACtE,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAA;CACpC;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,uBAAuB,CAAA;CACjC;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAA;IACd,YAAY,EAAE,YAAY,CAAA;CAC3B;AAED,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,MAAM,CAAA;CACf"}
1
+ {"version":3,"file":"NetworkAdapterInterface.d.ts","sourceRoot":"","sources":["../../src/network/NetworkAdapterInterface.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AACvC,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAE/C;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,uBACf,SAAQ,YAAY,CAAC,oBAAoB,CAAC;IAC1C,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,YAAY,CAAC,EAAE,YAAY,CAAA;IAE3B,OAAO,IAAI,OAAO,CAAA;IAClB,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IAE1B;;;;OAIG;IACH,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,YAAY,GAAG,IAAI,CAAA;IAG1D;;;OAGG;IACH,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;IAE5B,gEAAgE;IAChE,UAAU,IAAI,IAAI,CAAA;CACnB;AAID,MAAM,WAAW,oBAAoB;IACnC,yCAAyC;IACzC,KAAK,EAAE,MAAM,IAAI,CAAA;IAEjB,+DAA+D;IAC/D,gBAAgB,EAAE,CAAC,OAAO,EAAE,oBAAoB,KAAK,IAAI,CAAA;IAEzD,2EAA2E;IAC3E,mBAAmB,EAAE,CAAC,OAAO,EAAE,uBAAuB,KAAK,IAAI,CAAA;IAE/D,sEAAsE;IACtE,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAA;CACpC;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,uBAAuB,CAAA;CACjC;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAA;IACd,YAAY,EAAE,YAAY,CAAA;CAC3B;AAED,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,MAAM,CAAA;CACf"}
@@ -6,17 +6,20 @@ export declare class NetworkSubsystem extends EventEmitter<NetworkSubsystemEvent
6
6
  #private;
7
7
  peerId: PeerId;
8
8
  private peerMetadata;
9
+ adapters: NetworkAdapterInterface[];
9
10
  constructor(adapters: NetworkAdapterInterface[], peerId: PeerId, peerMetadata: Promise<PeerMetadata>);
11
+ disconnect(): void;
12
+ reconnect(): void;
10
13
  addNetworkAdapter(networkAdapter: NetworkAdapterInterface): void;
14
+ removeNetworkAdapter(networkAdapter: NetworkAdapterInterface): void;
11
15
  send(message: MessageContents): void;
12
16
  isReady: () => boolean;
13
- whenReady: () => Promise<void>;
17
+ whenReady: () => Promise<void[]>;
14
18
  }
15
19
  export interface NetworkSubsystemEvents {
16
20
  peer: (payload: PeerPayload) => void;
17
21
  "peer-disconnected": (payload: PeerDisconnectedPayload) => void;
18
22
  message: (payload: RepoMessage) => void;
19
- ready: () => void;
20
23
  }
21
24
  export interface PeerPayload {
22
25
  peerId: PeerId;
@@ -1 +1 @@
1
- {"version":3,"file":"NetworkSubsystem.d.ts","sourceRoot":"","sources":["../../src/network/NetworkSubsystem.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC5C,OAAO,EAAE,MAAM,EAAa,MAAM,aAAa,CAAA;AAC/C,OAAO,KAAK,EACV,uBAAuB,EACvB,uBAAuB,EACvB,YAAY,EACb,MAAM,8BAA8B,CAAA;AACrC,OAAO,EAEL,eAAe,EACf,WAAW,EAGZ,MAAM,eAAe,CAAA;AAOtB,qBAAa,gBAAiB,SAAQ,YAAY,CAAC,sBAAsB,CAAC;;IAY/D,MAAM;IACb,OAAO,CAAC,YAAY;gBAFpB,QAAQ,EAAE,uBAAuB,EAAE,EAC5B,MAAM,QAAiB,EACtB,YAAY,EAAE,OAAO,CAAC,YAAY,CAAC;IAO7C,iBAAiB,CAAC,cAAc,EAAE,uBAAuB;IA2EzD,IAAI,CAAC,OAAO,EAAE,eAAe;IAsC7B,OAAO,gBAEN;IAED,SAAS,sBAUR;CACF;AAQD,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,CAAA;IACpC,mBAAmB,EAAE,CAAC,OAAO,EAAE,uBAAuB,KAAK,IAAI,CAAA;IAC/D,OAAO,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,CAAA;IACvC,KAAK,EAAE,MAAM,IAAI,CAAA;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,YAAY,EAAE,YAAY,CAAA;CAC3B"}
1
+ {"version":3,"file":"NetworkSubsystem.d.ts","sourceRoot":"","sources":["../../src/network/NetworkSubsystem.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC5C,OAAO,EAAE,MAAM,EAAa,MAAM,aAAa,CAAA;AAC/C,OAAO,KAAK,EACV,uBAAuB,EACvB,uBAAuB,EACvB,YAAY,EACb,MAAM,8BAA8B,CAAA;AACrC,OAAO,EAEL,eAAe,EACf,WAAW,EAGZ,MAAM,eAAe,CAAA;AAOtB,qBAAa,gBAAiB,SAAQ,YAAY,CAAC,sBAAsB,CAAC;;IAW/D,MAAM,EAAE,MAAM;IACrB,OAAO,CAAC,YAAY;IALtB,QAAQ,EAAE,uBAAuB,EAAE,CAAK;gBAGtC,QAAQ,EAAE,uBAAuB,EAAE,EAC5B,MAAM,EAAE,MAAM,EACb,YAAY,EAAE,OAAO,CAAC,YAAY,CAAC;IAO7C,UAAU;IAIV,SAAS;IAIT,iBAAiB,CAAC,cAAc,EAAE,uBAAuB;IAqEzD,oBAAoB,CAAC,cAAc,EAAE,uBAAuB;IAK5D,IAAI,CAAC,OAAO,EAAE,eAAe;IAsC7B,OAAO,gBAEN;IAED,SAAS,wBAER;CACF;AAID,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,CAAA;IACpC,mBAAmB,EAAE,CAAC,OAAO,EAAE,uBAAuB,KAAK,IAAI,CAAA;IAC/D,OAAO,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,CAAA;CACxC;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,YAAY,EAAE,YAAY,CAAA;CAC3B"}
@@ -10,27 +10,29 @@ export class NetworkSubsystem extends EventEmitter {
10
10
  #count = 0;
11
11
  #sessionId = Math.random().toString(36).slice(2);
12
12
  #ephemeralSessionCounts = {};
13
- #readyAdapterCount = 0;
14
- #adapters = [];
15
- constructor(adapters, peerId = randomPeerId(), peerMetadata) {
13
+ adapters = [];
14
+ constructor(adapters, peerId, peerMetadata) {
16
15
  super();
17
16
  this.peerId = peerId;
18
17
  this.peerMetadata = peerMetadata;
19
18
  this.#log = debug(`automerge-repo:network:${this.peerId}`);
20
19
  adapters.forEach(a => this.addNetworkAdapter(a));
21
20
  }
21
+ disconnect() {
22
+ this.adapters.forEach(a => a.disconnect());
23
+ }
24
+ reconnect() {
25
+ this.adapters.forEach(a => a.connect(this.peerId));
26
+ }
22
27
  addNetworkAdapter(networkAdapter) {
23
- this.#adapters.push(networkAdapter);
24
- networkAdapter.once("ready", () => {
25
- this.#readyAdapterCount++;
26
- this.#log("Adapters ready: ", this.#readyAdapterCount, "/", this.#adapters.length);
27
- if (this.#readyAdapterCount === this.#adapters.length) {
28
- this.emit("ready");
29
- }
30
- });
28
+ this.adapters.push(networkAdapter);
31
29
  networkAdapter.on("peer-candidate", ({ peerId, peerMetadata }) => {
32
30
  this.#log(`peer candidate: ${peerId} `);
33
31
  // TODO: This is where authentication would happen
32
+ // TODO: on reconnection, this would create problems!
33
+ // the server would see a reconnection as a late-arriving channel
34
+ // for an existing peer and decide to ignore it until the connection
35
+ // times out: turns out my ICE/SIP emulation laziness did not pay off here
34
36
  if (!this.#adaptersByPeer[peerId]) {
35
37
  // TODO: handle losing a server here
36
38
  this.#adaptersByPeer[peerId] = networkAdapter;
@@ -75,6 +77,12 @@ export class NetworkSubsystem extends EventEmitter {
75
77
  this.#log("error connecting to network", err);
76
78
  });
77
79
  }
80
+ // TODO: this probably introduces a race condition for the ready event
81
+ // but I plan to refactor that as part of this branch in another patch
82
+ removeNetworkAdapter(networkAdapter) {
83
+ this.adapters = this.adapters.filter(a => a !== networkAdapter);
84
+ networkAdapter.disconnect();
85
+ }
78
86
  send(message) {
79
87
  const peer = this.#adaptersByPeer[message.targetId];
80
88
  if (!peer) {
@@ -113,21 +121,9 @@ export class NetworkSubsystem extends EventEmitter {
113
121
  peer.send(outbound);
114
122
  }
115
123
  isReady = () => {
116
- return this.#readyAdapterCount === this.#adapters.length;
124
+ return this.adapters.every(a => a.isReady());
117
125
  };
118
126
  whenReady = async () => {
119
- if (this.isReady()) {
120
- return;
121
- }
122
- else {
123
- return new Promise(resolve => {
124
- this.once("ready", () => {
125
- resolve();
126
- });
127
- });
128
- }
127
+ return Promise.all(this.adapters.map(a => a.whenReady()));
129
128
  };
130
129
  }
131
- function randomPeerId() {
132
- return `user-${Math.round(Math.random() * 100000)}`;
133
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automerge/automerge-repo",
3
- "version": "1.2.1",
3
+ "version": "2.0.0-alpha.1",
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>",
@@ -23,7 +23,7 @@
23
23
  "vite": "^5.0.8"
24
24
  },
25
25
  "dependencies": {
26
- "@automerge/automerge": "^2.2.5",
26
+ "@automerge/automerge": "^2.2.7",
27
27
  "bs58check": "^3.0.1",
28
28
  "cbor-x": "^1.3.0",
29
29
  "debug": "^4.3.4",
@@ -60,5 +60,5 @@
60
60
  "publishConfig": {
61
61
  "access": "public"
62
62
  },
63
- "gitHead": "2108823d685e855977defb75a938838a3734818b"
63
+ "gitHead": "86421aba21c8037b916ee93f962df1c121fef52b"
64
64
  }
@@ -57,9 +57,8 @@ export const stringifyAutomergeUrl = (
57
57
  * Given a string, returns true if it is a valid Automerge URL. This function also acts as a type
58
58
  * discriminator in Typescript.
59
59
  */
60
- export const isValidAutomergeUrl = (
61
- str: string | undefined | null
62
- ): str is AutomergeUrl => {
60
+ export const isValidAutomergeUrl = (str: unknown): str is AutomergeUrl => {
61
+ if (typeof str !== "string") return false
63
62
  if (!str || !str.startsWith(urlPrefix)) return false
64
63
  const automergeUrl = str as AutomergeUrl
65
64
  try {
@@ -70,7 +69,8 @@ export const isValidAutomergeUrl = (
70
69
  }
71
70
  }
72
71
 
73
- export const isValidDocumentId = (str: string): str is DocumentId => {
72
+ export const isValidDocumentId = (str: unknown): str is DocumentId => {
73
+ if (typeof str !== "string") return false
74
74
  // try to decode from base58
75
75
  const binaryDocumentID = documentIdToBinary(str as DocumentId)
76
76
  if (binaryDocumentID === undefined) return false // invalid base58check encoding
@@ -80,8 +80,8 @@ export const isValidDocumentId = (str: string): str is DocumentId => {
80
80
  return Uuid.validate(documentId)
81
81
  }
82
82
 
83
- export const isValidUuid = (str: string): str is LegacyDocumentId =>
84
- Uuid.validate(str)
83
+ export const isValidUuid = (str: unknown): str is LegacyDocumentId =>
84
+ typeof str === "string" && Uuid.validate(str)
85
85
 
86
86
  /**
87
87
  * Returns a new Automerge URL with a random UUID documentId. Called by Repo.create(), and also used by tests.
package/src/DocHandle.ts CHANGED
@@ -29,7 +29,7 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
29
29
  #machine
30
30
 
31
31
  /** The last known state of our document. */
32
- #prevDocState: T | undefined
32
+ #prevDocState: T = A.init<T>()
33
33
 
34
34
  /** How long to wait before giving up on a document. (Note that a document will be marked
35
35
  * unavailable much sooner if all known peers respond that they don't have it.) */
@@ -49,17 +49,7 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
49
49
  this.#timeoutDelay = options.timeoutDelay
50
50
  }
51
51
 
52
- let doc: T
53
- const isNew = "isNew" in options && options.isNew
54
- if (isNew) {
55
- // T should really be constrained to extend `Record<string, unknown>` (an automerge doc can't be
56
- // e.g. a primitive, an array, etc. - it must be an object). But adding that constraint creates
57
- // a bunch of other problems elsewhere so for now we'll just cast it here to make Automerge happy.
58
- doc = A.from(options.initialValue as Record<string, unknown>) as T
59
- doc = A.emptyChange<T>(doc)
60
- } else {
61
- doc = A.init<T>()
62
- }
52
+ const doc = A.init<T>()
63
53
 
64
54
  this.#log = debug(`automerge-repo:dochandle:${this.documentId.slice(0, 5)}`)
65
55
 
@@ -80,7 +70,7 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
80
70
  }),
81
71
  onDelete: assign(() => {
82
72
  this.emit("delete", { handle: this })
83
- return { doc: undefined }
73
+ return { doc: A.init() }
84
74
  }),
85
75
  onUnavailable: () => {
86
76
  this.emit("unavailable", { handle: this })
@@ -101,21 +91,16 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
101
91
  states: {
102
92
  idle: {
103
93
  on: {
104
- CREATE: "ready",
105
- FIND: "loading",
94
+ BEGIN: "loading",
106
95
  },
107
96
  },
108
97
  loading: {
109
98
  on: {
110
99
  REQUEST: "requesting",
111
100
  DOC_READY: "ready",
112
- AWAIT_NETWORK: "awaitingNetwork",
113
101
  },
114
102
  after: { [delay]: "unavailable" },
115
103
  },
116
- awaitingNetwork: {
117
- on: { NETWORK_READY: "requesting" },
118
- },
119
104
  requesting: {
120
105
  on: {
121
106
  DOC_UNAVAILABLE: "unavailable",
@@ -146,7 +131,7 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
146
131
 
147
132
  // Start the machine, and send a create or find event to get things going
148
133
  this.#machine.start()
149
- this.#machine.send(isNew ? { type: CREATE } : { type: FIND })
134
+ this.#machine.send({ type: BEGIN })
150
135
  }
151
136
 
152
137
  // PRIVATE
@@ -178,13 +163,14 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
178
163
  * Called after state transitions. If the document has changed, emits a change event. If we just
179
164
  * received the document for the first time, signal that our request has been completed.
180
165
  */
181
- #checkForChanges(before: T | undefined, after: T) {
182
- const docChanged =
183
- after && before && !headsAreSame(A.getHeads(after), A.getHeads(before))
166
+ #checkForChanges(before: A.Doc<T>, after: A.Doc<T>) {
167
+ const beforeHeads = A.getHeads(before)
168
+ const afterHeads = A.getHeads(after)
169
+ const docChanged = !headsAreSame(afterHeads, beforeHeads)
184
170
  if (docChanged) {
185
171
  this.emit("heads-changed", { handle: this, doc: after })
186
172
 
187
- const patches = A.diff(after, A.getHeads(before), A.getHeads(after))
173
+ const patches = A.diff(after, beforeHeads, afterHeads)
188
174
  if (patches.length > 0) {
189
175
  this.emit("change", {
190
176
  handle: this,
@@ -306,14 +292,24 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
306
292
  }
307
293
 
308
294
  /**
309
- * `update` is called by the repo when we receive changes from the network
310
- * Called by the repo when we receive changes from the network.
295
+ * `update` is called any time we have a new document state; could be
296
+ * from a local change, a remote change, or a new document from storage.
297
+ * Does not cause state changes.
311
298
  * @hidden
312
299
  */
313
300
  update(callback: (doc: A.Doc<T>) => A.Doc<T>) {
314
301
  this.#machine.send({ type: UPDATE, payload: { callback } })
315
302
  }
316
303
 
304
+ /**
305
+ * `doneLoading` is called by the repo after it decides it has all the changes
306
+ * it's going to get during setup. This might mean it was created locally,
307
+ * or that it was loaded from storage, or that it was received from a peer.
308
+ */
309
+ doneLoading() {
310
+ this.#machine.send({ type: DOC_READY })
311
+ }
312
+
317
313
  /**
318
314
  * Called by the repo either when a doc handle changes or we receive new remote heads.
319
315
  * @hidden
@@ -346,7 +342,7 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
346
342
  change(callback: A.ChangeFn<T>, options: A.ChangeOptions<T> = {}) {
347
343
  if (!this.isReady()) {
348
344
  throw new Error(
349
- `DocHandle#${this.documentId} is not ready. Check \`handle.isReady()\` before accessing the document.`
345
+ `DocHandle#${this.documentId} is in ${this.state} and not ready. Check \`handle.isReady()\` before accessing the document.`
350
346
  )
351
347
  }
352
348
  this.#machine.send({
@@ -425,17 +421,6 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
425
421
  if (this.#state === "loading") this.#machine.send({ type: REQUEST })
426
422
  }
427
423
 
428
- /** @hidden */
429
- awaitNetwork() {
430
- if (this.#state === "loading") this.#machine.send({ type: AWAIT_NETWORK })
431
- }
432
-
433
- /** @hidden */
434
- networkReady() {
435
- if (this.#state === "awaitingNetwork")
436
- this.#machine.send({ type: NETWORK_READY })
437
- }
438
-
439
424
  /** Called by the repo when the document is deleted. */
440
425
  delete() {
441
426
  this.#machine.send({ type: DELETE })
@@ -550,8 +535,6 @@ export const HandleState = {
550
535
  IDLE: "idle",
551
536
  /** We are waiting for storage to finish loading */
552
537
  LOADING: "loading",
553
- /** We are waiting for the network to be come ready */
554
- AWAITING_NETWORK: "awaitingNetwork",
555
538
  /** We are waiting for someone in the network to respond to a sync request */
556
539
  REQUESTING: "requesting",
557
540
  /** The document is available */
@@ -563,15 +546,8 @@ export const HandleState = {
563
546
  } as const
564
547
  export type HandleState = (typeof HandleState)[keyof typeof HandleState]
565
548
 
566
- export const {
567
- IDLE,
568
- LOADING,
569
- AWAITING_NETWORK,
570
- REQUESTING,
571
- READY,
572
- DELETED,
573
- UNAVAILABLE,
574
- } = HandleState
549
+ export const { IDLE, LOADING, REQUESTING, READY, DELETED, UNAVAILABLE } =
550
+ HandleState
575
551
 
576
552
  // context
577
553
 
@@ -584,8 +560,7 @@ interface DocHandleContext<T> {
584
560
 
585
561
  /** These are the (internal) events that can be sent to the state machine */
586
562
  type DocHandleEvent<T> =
587
- | { type: typeof CREATE }
588
- | { type: typeof FIND }
563
+ | { type: typeof BEGIN }
589
564
  | { type: typeof REQUEST }
590
565
  | { type: typeof DOC_READY }
591
566
  | {
@@ -595,15 +570,10 @@ type DocHandleEvent<T> =
595
570
  | { type: typeof TIMEOUT }
596
571
  | { type: typeof DELETE }
597
572
  | { type: typeof DOC_UNAVAILABLE }
598
- | { type: typeof AWAIT_NETWORK }
599
- | { type: typeof NETWORK_READY }
600
573
 
601
- const CREATE = "CREATE"
602
- const FIND = "FIND"
574
+ const BEGIN = "BEGIN"
603
575
  const REQUEST = "REQUEST"
604
576
  const DOC_READY = "DOC_READY"
605
- const AWAIT_NETWORK = "AWAIT_NETWORK"
606
- const NETWORK_READY = "NETWORK_READY"
607
577
  const UPDATE = "UPDATE"
608
578
  const DELETE = "DELETE"
609
579
  const TIMEOUT = "TIMEOUT"