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

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 (40) 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/helpers/DummyNetworkAdapter.d.ts +3 -0
  11. package/dist/helpers/DummyNetworkAdapter.d.ts.map +1 -1
  12. package/dist/helpers/DummyNetworkAdapter.js +24 -5
  13. package/dist/helpers/tests/network-adapter-tests.d.ts.map +1 -1
  14. package/dist/helpers/tests/network-adapter-tests.js +88 -1
  15. package/dist/helpers/throttle.d.ts +1 -1
  16. package/dist/helpers/throttle.js +1 -1
  17. package/dist/network/NetworkAdapter.d.ts +2 -0
  18. package/dist/network/NetworkAdapter.d.ts.map +1 -1
  19. package/dist/network/NetworkAdapterInterface.d.ts +2 -2
  20. package/dist/network/NetworkAdapterInterface.d.ts.map +1 -1
  21. package/dist/network/NetworkSubsystem.d.ts +5 -2
  22. package/dist/network/NetworkSubsystem.d.ts.map +1 -1
  23. package/dist/network/NetworkSubsystem.js +21 -25
  24. package/package.json +3 -3
  25. package/src/AutomergeUrl.ts +6 -6
  26. package/src/DocHandle.ts +27 -57
  27. package/src/Repo.ts +55 -40
  28. package/src/helpers/DummyNetworkAdapter.ts +29 -5
  29. package/src/helpers/tests/network-adapter-tests.ts +121 -1
  30. package/src/helpers/throttle.ts +1 -1
  31. package/src/network/NetworkAdapter.ts +3 -0
  32. package/src/network/NetworkAdapterInterface.ts +4 -3
  33. package/src/network/NetworkSubsystem.ts +24 -31
  34. package/test/AutomergeUrl.test.ts +4 -0
  35. package/test/DocHandle.test.ts +20 -24
  36. package/test/DocSynchronizer.test.ts +5 -1
  37. package/test/NetworkSubsystem.test.ts +107 -0
  38. package/test/Repo.test.ts +37 -15
  39. package/test/remoteHeads.test.ts +3 -3
  40. package/test/Network.test.ts +0 -14
@@ -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.0",
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": "16356392fb2ed9245565ae04f6e6b49e61195e65"
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"
package/src/Repo.ts CHANGED
@@ -23,6 +23,10 @@ import { CollectionSynchronizer } from "./synchronizer/CollectionSynchronizer.js
23
23
  import { SyncStatePayload } from "./synchronizer/Synchronizer.js"
24
24
  import type { AnyDocumentId, DocumentId, PeerId } from "./types.js"
25
25
 
26
+ function randomPeerId() {
27
+ return ("peer-" + Math.random().toString(36).slice(4)) as PeerId
28
+ }
29
+
26
30
  /** A Repo is a collection of documents with networking, syncing, and storage capabilities. */
27
31
  /** The `Repo` is the main entry point of this library
28
32
  *
@@ -61,7 +65,7 @@ export class Repo extends EventEmitter<RepoEvents> {
61
65
  constructor({
62
66
  storage,
63
67
  network = [],
64
- peerId,
68
+ peerId = randomPeerId(),
65
69
  sharePolicy,
66
70
  isEphemeral = storage === undefined,
67
71
  enableRemoteHeadsGossiping = false,
@@ -75,7 +79,7 @@ export class Repo extends EventEmitter<RepoEvents> {
75
79
 
76
80
  // The `document` event is fired by the DocCollection any time we create a new document or look
77
81
  // up a document by ID. We listen for it in order to wire up storage and network synchronization.
78
- this.on("document", async ({ handle, isNew }) => {
82
+ this.on("document", async ({ handle }) => {
79
83
  if (storageSubsystem) {
80
84
  // Save when the document changes, but no more often than saveDebounceRate.
81
85
  const saveFn = ({
@@ -85,17 +89,6 @@ export class Repo extends EventEmitter<RepoEvents> {
85
89
  void storageSubsystem.saveDoc(handle.documentId, doc)
86
90
  }
87
91
  handle.on("heads-changed", throttle(saveFn, this.saveDebounceRate))
88
-
89
- if (isNew) {
90
- // this is a new document, immediately save it
91
- await storageSubsystem.saveDoc(handle.documentId, handle.docSync()!)
92
- } else {
93
- // Try to load from disk
94
- const loadedDoc = await storageSubsystem.loadDoc(handle.documentId)
95
- if (loadedDoc) {
96
- handle.update(() => loadedDoc)
97
- }
98
- }
99
92
  }
100
93
 
101
94
  handle.on("unavailable", () => {
@@ -105,20 +98,6 @@ export class Repo extends EventEmitter<RepoEvents> {
105
98
  })
106
99
  })
107
100
 
108
- if (this.networkSubsystem.isReady()) {
109
- handle.request()
110
- } else {
111
- handle.awaitNetwork()
112
- this.networkSubsystem
113
- .whenReady()
114
- .then(() => {
115
- handle.networkReady()
116
- })
117
- .catch(err => {
118
- this.#log("error waiting for network", { err })
119
- })
120
- }
121
-
122
101
  // Register the document with the synchronizer. This advertises our interest in the document.
123
102
  this.#synchronizer.addDocument(handle.documentId)
124
103
  })
@@ -324,20 +303,16 @@ export class Repo extends EventEmitter<RepoEvents> {
324
303
  /** Returns an existing handle if we have it; creates one otherwise. */
325
304
  #getHandle<T>({
326
305
  documentId,
327
- isNew,
328
- initialValue,
329
306
  }: {
330
307
  /** The documentId of the handle to look up or create */
331
308
  documentId: DocumentId /** If we know we're creating a new document, specify this so we can have access to it immediately */
332
- isNew: boolean
333
- initialValue?: T
334
309
  }) {
335
310
  // If we have the handle cached, return it
336
311
  if (this.#handleCache[documentId]) return this.#handleCache[documentId]
337
312
 
338
313
  // If not, create a new handle, cache it, and return it
339
314
  if (!documentId) throw new Error(`Invalid documentId ${documentId}`)
340
- const handle = new DocHandle<T>(documentId, { isNew, initialValue })
315
+ const handle = new DocHandle<T>(documentId)
341
316
  this.#handleCache[documentId] = handle
342
317
  return handle
343
318
  }
@@ -366,10 +341,21 @@ export class Repo extends EventEmitter<RepoEvents> {
366
341
  const { documentId } = parseAutomergeUrl(generateAutomergeUrl())
367
342
  const handle = this.#getHandle<T>({
368
343
  documentId,
369
- isNew: true,
370
- initialValue,
371
344
  }) as DocHandle<T>
372
- this.emit("document", { handle, isNew: true })
345
+
346
+ this.emit("document", { handle })
347
+
348
+ handle.update(() => {
349
+ let nextDoc: Automerge.Doc<T>
350
+ if (initialValue) {
351
+ nextDoc = Automerge.from(initialValue)
352
+ } else {
353
+ nextDoc = Automerge.emptyChange(Automerge.init())
354
+ }
355
+ return nextDoc
356
+ })
357
+
358
+ handle.doneLoading()
373
359
  return handle
374
360
  }
375
361
 
@@ -434,11 +420,34 @@ export class Repo extends EventEmitter<RepoEvents> {
434
420
  return this.#handleCache[documentId]
435
421
  }
436
422
 
423
+ // If we don't already have the handle, make an empty one and try loading it
437
424
  const handle = this.#getHandle<T>({
438
425
  documentId,
439
- isNew: false,
440
426
  }) as DocHandle<T>
441
- this.emit("document", { handle, isNew: false })
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 => {
431
+ if (loadedDoc) {
432
+ // uhhhh, sorry if you're reading this because we were lying to the type system
433
+ handle.update(() => loadedDoc as Automerge.Doc<T>)
434
+ handle.doneLoading()
435
+ } 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 })
445
+ }
446
+ })
447
+ } else {
448
+ handle.request()
449
+ this.emit("document", { handle })
450
+ }
442
451
  return handle
443
452
  }
444
453
 
@@ -448,7 +457,7 @@ export class Repo extends EventEmitter<RepoEvents> {
448
457
  ) {
449
458
  const documentId = interpretAsDocumentId(id)
450
459
 
451
- const handle = this.#getHandle({ documentId, isNew: false })
460
+ const handle = this.#getHandle({ documentId })
452
461
  handle.delete()
453
462
 
454
463
  delete this.#handleCache[documentId]
@@ -465,7 +474,7 @@ export class Repo extends EventEmitter<RepoEvents> {
465
474
  async export(id: AnyDocumentId): Promise<Uint8Array | undefined> {
466
475
  const documentId = interpretAsDocumentId(id)
467
476
 
468
- const handle = this.#getHandle({ documentId, isNew: false })
477
+ const handle = this.#getHandle({ documentId })
469
478
  const doc = await handle.doc()
470
479
  if (!doc) return undefined
471
480
  return Automerge.save(doc)
@@ -529,6 +538,13 @@ export class Repo extends EventEmitter<RepoEvents> {
529
538
  })
530
539
  )
531
540
  }
541
+
542
+ shutdown(): Promise<void> {
543
+ this.networkSubsystem.adapters.forEach(adapter => {
544
+ adapter.disconnect()
545
+ })
546
+ return this.flush()
547
+ }
532
548
  }
533
549
 
534
550
  export interface RepoConfig {
@@ -582,7 +598,6 @@ export interface RepoEvents {
582
598
 
583
599
  export interface DocumentPayload {
584
600
  handle: DocHandle<any>
585
- isNew: boolean
586
601
  }
587
602
 
588
603
  export interface DeleteDocumentPayload {
@@ -2,20 +2,44 @@ import { pause } from "../../src/helpers/pause.js"
2
2
  import { Message, NetworkAdapter, PeerId } from "../../src/index.js"
3
3
 
4
4
  export class DummyNetworkAdapter extends NetworkAdapter {
5
- #startReady: boolean
6
5
  #sendMessage?: SendMessageFn
7
6
 
7
+ #ready = false
8
+ #readyResolver?: () => void
9
+ #readyPromise: Promise<void> = new Promise<void>(resolve => {
10
+ this.#readyResolver = resolve
11
+ })
12
+
13
+ isReady() {
14
+ return this.#ready
15
+ }
16
+
17
+ whenReady() {
18
+ return this.#readyPromise
19
+ }
20
+
21
+ #forceReady() {
22
+ if (!this.#ready) {
23
+ this.#ready = true
24
+ this.#readyResolver?.()
25
+ }
26
+ }
27
+
28
+ // A public wrapper for use in tests!
29
+ forceReady() {
30
+ this.#forceReady()
31
+ }
32
+
8
33
  constructor(opts: Options = { startReady: true }) {
9
34
  super()
10
- this.#startReady = opts.startReady || false
35
+ if (opts.startReady) {
36
+ this.#forceReady()
37
+ }
11
38
  this.#sendMessage = opts.sendMessage
12
39
  }
13
40
 
14
41
  connect(peerId: PeerId) {
15
42
  this.peerId = peerId
16
- if (this.#startReady) {
17
- this.emit("ready", { network: this })
18
- }
19
43
  }
20
44
 
21
45
  disconnect() {}