@automerge/automerge-repo 2.0.0-collectionsync-alpha.1 → 2.0.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 (203) hide show
  1. package/README.md +8 -8
  2. package/dist/AutomergeUrl.d.ts +17 -5
  3. package/dist/AutomergeUrl.d.ts.map +1 -1
  4. package/dist/AutomergeUrl.js +71 -24
  5. package/dist/DocHandle.d.ts +33 -41
  6. package/dist/DocHandle.d.ts.map +1 -1
  7. package/dist/DocHandle.js +105 -66
  8. package/dist/FindProgress.d.ts +30 -0
  9. package/dist/FindProgress.d.ts.map +1 -0
  10. package/dist/FindProgress.js +1 -0
  11. package/dist/RemoteHeadsSubscriptions.d.ts +4 -5
  12. package/dist/RemoteHeadsSubscriptions.d.ts.map +1 -1
  13. package/dist/RemoteHeadsSubscriptions.js +4 -1
  14. package/dist/Repo.d.ts +24 -5
  15. package/dist/Repo.d.ts.map +1 -1
  16. package/dist/Repo.js +355 -169
  17. package/dist/helpers/abortable.d.ts +36 -0
  18. package/dist/helpers/abortable.d.ts.map +1 -0
  19. package/dist/helpers/abortable.js +47 -0
  20. package/dist/helpers/arraysAreEqual.d.ts.map +1 -1
  21. package/dist/helpers/bufferFromHex.d.ts +3 -0
  22. package/dist/helpers/bufferFromHex.d.ts.map +1 -0
  23. package/dist/helpers/bufferFromHex.js +13 -0
  24. package/dist/helpers/debounce.d.ts.map +1 -1
  25. package/dist/helpers/eventPromise.d.ts.map +1 -1
  26. package/dist/helpers/headsAreSame.d.ts +2 -2
  27. package/dist/helpers/headsAreSame.d.ts.map +1 -1
  28. package/dist/helpers/mergeArrays.d.ts +1 -1
  29. package/dist/helpers/mergeArrays.d.ts.map +1 -1
  30. package/dist/helpers/pause.d.ts.map +1 -1
  31. package/dist/helpers/tests/network-adapter-tests.d.ts.map +1 -1
  32. package/dist/helpers/tests/network-adapter-tests.js +13 -13
  33. package/dist/helpers/tests/storage-adapter-tests.d.ts.map +1 -1
  34. package/dist/helpers/tests/storage-adapter-tests.js +6 -9
  35. package/dist/helpers/throttle.d.ts.map +1 -1
  36. package/dist/helpers/withTimeout.d.ts.map +1 -1
  37. package/dist/index.d.ts +35 -7
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +37 -6
  40. package/dist/network/NetworkSubsystem.d.ts +0 -1
  41. package/dist/network/NetworkSubsystem.d.ts.map +1 -1
  42. package/dist/network/NetworkSubsystem.js +0 -3
  43. package/dist/network/messages.d.ts +1 -7
  44. package/dist/network/messages.d.ts.map +1 -1
  45. package/dist/network/messages.js +1 -2
  46. package/dist/storage/StorageAdapter.d.ts +0 -9
  47. package/dist/storage/StorageAdapter.d.ts.map +1 -1
  48. package/dist/storage/StorageAdapter.js +0 -33
  49. package/dist/storage/StorageSubsystem.d.ts +6 -2
  50. package/dist/storage/StorageSubsystem.d.ts.map +1 -1
  51. package/dist/storage/StorageSubsystem.js +131 -37
  52. package/dist/storage/keyHash.d.ts +1 -1
  53. package/dist/storage/keyHash.d.ts.map +1 -1
  54. package/dist/synchronizer/CollectionSynchronizer.d.ts +3 -4
  55. package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
  56. package/dist/synchronizer/CollectionSynchronizer.js +32 -26
  57. package/dist/synchronizer/DocSynchronizer.d.ts +8 -8
  58. package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
  59. package/dist/synchronizer/DocSynchronizer.js +205 -79
  60. package/dist/types.d.ts +4 -1
  61. package/dist/types.d.ts.map +1 -1
  62. package/fuzz/fuzz.ts +3 -3
  63. package/package.json +4 -5
  64. package/src/AutomergeUrl.ts +101 -26
  65. package/src/DocHandle.ts +158 -77
  66. package/src/FindProgress.ts +48 -0
  67. package/src/RemoteHeadsSubscriptions.ts +11 -9
  68. package/src/Repo.ts +465 -180
  69. package/src/helpers/abortable.ts +62 -0
  70. package/src/helpers/bufferFromHex.ts +14 -0
  71. package/src/helpers/headsAreSame.ts +2 -2
  72. package/src/helpers/tests/network-adapter-tests.ts +14 -13
  73. package/src/helpers/tests/storage-adapter-tests.ts +13 -24
  74. package/src/index.ts +57 -38
  75. package/src/network/NetworkSubsystem.ts +0 -4
  76. package/src/network/messages.ts +2 -11
  77. package/src/storage/StorageAdapter.ts +0 -42
  78. package/src/storage/StorageSubsystem.ts +155 -45
  79. package/src/storage/keyHash.ts +1 -1
  80. package/src/synchronizer/CollectionSynchronizer.ts +42 -29
  81. package/src/synchronizer/DocSynchronizer.ts +263 -89
  82. package/src/types.ts +4 -1
  83. package/test/AutomergeUrl.test.ts +130 -0
  84. package/test/CollectionSynchronizer.test.ts +6 -8
  85. package/test/DocHandle.test.ts +161 -77
  86. package/test/DocSynchronizer.test.ts +11 -9
  87. package/test/RemoteHeadsSubscriptions.test.ts +1 -1
  88. package/test/Repo.test.ts +406 -341
  89. package/test/StorageSubsystem.test.ts +95 -20
  90. package/test/remoteHeads.test.ts +28 -13
  91. package/dist/CollectionHandle.d.ts +0 -14
  92. package/dist/CollectionHandle.d.ts.map +0 -1
  93. package/dist/CollectionHandle.js +0 -37
  94. package/dist/DocUrl.d.ts +0 -47
  95. package/dist/DocUrl.d.ts.map +0 -1
  96. package/dist/DocUrl.js +0 -72
  97. package/dist/EphemeralData.d.ts +0 -20
  98. package/dist/EphemeralData.d.ts.map +0 -1
  99. package/dist/EphemeralData.js +0 -1
  100. package/dist/ferigan.d.ts +0 -51
  101. package/dist/ferigan.d.ts.map +0 -1
  102. package/dist/ferigan.js +0 -98
  103. package/dist/src/DocHandle.d.ts +0 -182
  104. package/dist/src/DocHandle.d.ts.map +0 -1
  105. package/dist/src/DocHandle.js +0 -405
  106. package/dist/src/DocUrl.d.ts +0 -49
  107. package/dist/src/DocUrl.d.ts.map +0 -1
  108. package/dist/src/DocUrl.js +0 -72
  109. package/dist/src/EphemeralData.d.ts +0 -19
  110. package/dist/src/EphemeralData.d.ts.map +0 -1
  111. package/dist/src/EphemeralData.js +0 -1
  112. package/dist/src/Repo.d.ts +0 -74
  113. package/dist/src/Repo.d.ts.map +0 -1
  114. package/dist/src/Repo.js +0 -208
  115. package/dist/src/helpers/arraysAreEqual.d.ts +0 -2
  116. package/dist/src/helpers/arraysAreEqual.d.ts.map +0 -1
  117. package/dist/src/helpers/arraysAreEqual.js +0 -2
  118. package/dist/src/helpers/cbor.d.ts +0 -4
  119. package/dist/src/helpers/cbor.d.ts.map +0 -1
  120. package/dist/src/helpers/cbor.js +0 -8
  121. package/dist/src/helpers/eventPromise.d.ts +0 -11
  122. package/dist/src/helpers/eventPromise.d.ts.map +0 -1
  123. package/dist/src/helpers/eventPromise.js +0 -7
  124. package/dist/src/helpers/headsAreSame.d.ts +0 -2
  125. package/dist/src/helpers/headsAreSame.d.ts.map +0 -1
  126. package/dist/src/helpers/headsAreSame.js +0 -4
  127. package/dist/src/helpers/mergeArrays.d.ts +0 -2
  128. package/dist/src/helpers/mergeArrays.d.ts.map +0 -1
  129. package/dist/src/helpers/mergeArrays.js +0 -15
  130. package/dist/src/helpers/pause.d.ts +0 -6
  131. package/dist/src/helpers/pause.d.ts.map +0 -1
  132. package/dist/src/helpers/pause.js +0 -10
  133. package/dist/src/helpers/tests/network-adapter-tests.d.ts +0 -21
  134. package/dist/src/helpers/tests/network-adapter-tests.d.ts.map +0 -1
  135. package/dist/src/helpers/tests/network-adapter-tests.js +0 -122
  136. package/dist/src/helpers/withTimeout.d.ts +0 -12
  137. package/dist/src/helpers/withTimeout.d.ts.map +0 -1
  138. package/dist/src/helpers/withTimeout.js +0 -24
  139. package/dist/src/index.d.ts +0 -53
  140. package/dist/src/index.d.ts.map +0 -1
  141. package/dist/src/index.js +0 -40
  142. package/dist/src/network/NetworkAdapter.d.ts +0 -26
  143. package/dist/src/network/NetworkAdapter.d.ts.map +0 -1
  144. package/dist/src/network/NetworkAdapter.js +0 -4
  145. package/dist/src/network/NetworkSubsystem.d.ts +0 -23
  146. package/dist/src/network/NetworkSubsystem.d.ts.map +0 -1
  147. package/dist/src/network/NetworkSubsystem.js +0 -120
  148. package/dist/src/network/messages.d.ts +0 -85
  149. package/dist/src/network/messages.d.ts.map +0 -1
  150. package/dist/src/network/messages.js +0 -23
  151. package/dist/src/storage/StorageAdapter.d.ts +0 -14
  152. package/dist/src/storage/StorageAdapter.d.ts.map +0 -1
  153. package/dist/src/storage/StorageAdapter.js +0 -1
  154. package/dist/src/storage/StorageSubsystem.d.ts +0 -12
  155. package/dist/src/storage/StorageSubsystem.d.ts.map +0 -1
  156. package/dist/src/storage/StorageSubsystem.js +0 -145
  157. package/dist/src/synchronizer/CollectionSynchronizer.d.ts +0 -25
  158. package/dist/src/synchronizer/CollectionSynchronizer.d.ts.map +0 -1
  159. package/dist/src/synchronizer/CollectionSynchronizer.js +0 -106
  160. package/dist/src/synchronizer/DocSynchronizer.d.ts +0 -29
  161. package/dist/src/synchronizer/DocSynchronizer.d.ts.map +0 -1
  162. package/dist/src/synchronizer/DocSynchronizer.js +0 -263
  163. package/dist/src/synchronizer/Synchronizer.d.ts +0 -9
  164. package/dist/src/synchronizer/Synchronizer.d.ts.map +0 -1
  165. package/dist/src/synchronizer/Synchronizer.js +0 -2
  166. package/dist/src/types.d.ts +0 -16
  167. package/dist/src/types.d.ts.map +0 -1
  168. package/dist/src/types.js +0 -1
  169. package/dist/test/CollectionSynchronizer.test.d.ts +0 -2
  170. package/dist/test/CollectionSynchronizer.test.d.ts.map +0 -1
  171. package/dist/test/CollectionSynchronizer.test.js +0 -57
  172. package/dist/test/DocHandle.test.d.ts +0 -2
  173. package/dist/test/DocHandle.test.d.ts.map +0 -1
  174. package/dist/test/DocHandle.test.js +0 -238
  175. package/dist/test/DocSynchronizer.test.d.ts +0 -2
  176. package/dist/test/DocSynchronizer.test.d.ts.map +0 -1
  177. package/dist/test/DocSynchronizer.test.js +0 -111
  178. package/dist/test/Network.test.d.ts +0 -2
  179. package/dist/test/Network.test.d.ts.map +0 -1
  180. package/dist/test/Network.test.js +0 -11
  181. package/dist/test/Repo.test.d.ts +0 -2
  182. package/dist/test/Repo.test.d.ts.map +0 -1
  183. package/dist/test/Repo.test.js +0 -568
  184. package/dist/test/StorageSubsystem.test.d.ts +0 -2
  185. package/dist/test/StorageSubsystem.test.d.ts.map +0 -1
  186. package/dist/test/StorageSubsystem.test.js +0 -56
  187. package/dist/test/helpers/DummyNetworkAdapter.d.ts +0 -9
  188. package/dist/test/helpers/DummyNetworkAdapter.d.ts.map +0 -1
  189. package/dist/test/helpers/DummyNetworkAdapter.js +0 -15
  190. package/dist/test/helpers/DummyStorageAdapter.d.ts +0 -16
  191. package/dist/test/helpers/DummyStorageAdapter.d.ts.map +0 -1
  192. package/dist/test/helpers/DummyStorageAdapter.js +0 -33
  193. package/dist/test/helpers/generate-large-object.d.ts +0 -5
  194. package/dist/test/helpers/generate-large-object.d.ts.map +0 -1
  195. package/dist/test/helpers/generate-large-object.js +0 -9
  196. package/dist/test/helpers/getRandomItem.d.ts +0 -2
  197. package/dist/test/helpers/getRandomItem.d.ts.map +0 -1
  198. package/dist/test/helpers/getRandomItem.js +0 -4
  199. package/dist/test/types.d.ts +0 -4
  200. package/dist/test/types.d.ts.map +0 -1
  201. package/dist/test/types.js +0 -1
  202. package/src/CollectionHandle.ts +0 -54
  203. package/src/ferigan.ts +0 -184
package/dist/Repo.js CHANGED
@@ -1,13 +1,15 @@
1
1
  import { next as Automerge } from "@automerge/automerge/slim";
2
2
  import debug from "debug";
3
3
  import { EventEmitter } from "eventemitter3";
4
- import { generateAutomergeUrl, interpretAsDocumentId, parseAutomergeUrl, } from "./AutomergeUrl.js";
4
+ import { encodeHeads, generateAutomergeUrl, interpretAsDocumentId, isValidAutomergeUrl, parseAutomergeUrl, } from "./AutomergeUrl.js";
5
5
  import { DELETED, DocHandle, READY, UNAVAILABLE, UNLOADED, } from "./DocHandle.js";
6
+ import { RemoteHeadsSubscriptions } from "./RemoteHeadsSubscriptions.js";
7
+ import { headsAreSame } from "./helpers/headsAreSame.js";
8
+ import { throttle } from "./helpers/throttle.js";
6
9
  import { NetworkSubsystem } from "./network/NetworkSubsystem.js";
7
10
  import { StorageSubsystem } from "./storage/StorageSubsystem.js";
8
11
  import { CollectionSynchronizer } from "./synchronizer/CollectionSynchronizer.js";
9
- import { next as A } from "@automerge/automerge/slim";
10
- import { InMemoryStorageAdapter } from "./storage/StorageAdapter.js";
12
+ import { abortable } from "./helpers/abortable.js";
11
13
  function randomPeerId() {
12
14
  return ("peer-" + Math.random().toString(36).slice(4));
13
15
  }
@@ -23,7 +25,11 @@ export class Repo extends EventEmitter {
23
25
  #log;
24
26
  /** @hidden */
25
27
  networkSubsystem;
28
+ /** @hidden */
26
29
  storageSubsystem;
30
+ /** The debounce rate is adjustable on the repo. */
31
+ /** @hidden */
32
+ saveDebounceRate = 100;
27
33
  #handleCache = {};
28
34
  /** @hidden */
29
35
  synchronizer;
@@ -33,73 +39,49 @@ export class Repo extends EventEmitter {
33
39
  /** maps peer id to to persistence information (storageId, isEphemeral), access by collection synchronizer */
34
40
  /** @hidden */
35
41
  peerMetadataByPeerId = {};
36
- #beelay;
42
+ #remoteHeadsSubscriptions = new RemoteHeadsSubscriptions();
43
+ #remoteHeadsGossipingEnabled = false;
44
+ #progressCache = {};
37
45
  constructor({ storage, network = [], peerId = randomPeerId(), sharePolicy, isEphemeral = storage === undefined, enableRemoteHeadsGossiping = false, denylist = [], } = {}) {
38
46
  super();
39
- if (storage == null) {
40
- // beelayStorage = new InMemoryStorageAdapter()
41
- storage = new InMemoryStorageAdapter();
42
- }
43
- this.#beelay = new A.beelay.Beelay({
44
- storage,
45
- peerId,
46
- requestPolicy: async ({ docId }) => {
47
- const peers = Array.from(this.networkSubsystem.peers);
48
- const generousPeers = [];
49
- for (const peerId of peers) {
50
- const okToShare = await this.sharePolicy(peerId);
51
- if (okToShare)
52
- generousPeers.push(peerId);
53
- }
54
- return generousPeers;
55
- },
56
- });
57
- this.storageSubsystem = new StorageSubsystem(this.#beelay, storage);
47
+ this.#remoteHeadsGossipingEnabled = enableRemoteHeadsGossiping;
58
48
  this.#log = debug(`automerge-repo:repo`);
59
49
  this.sharePolicy = sharePolicy ?? this.sharePolicy;
60
- this.#beelay.on("message", ({ message }) => {
61
- this.#log(`sending ${message} message to ${message.recipient}`);
62
- networkSubsystem.send({
63
- targetId: message.recipient,
64
- type: "beelay",
65
- ...message,
66
- });
67
- });
68
- this.#beelay.on("docEvent", event => {
69
- this.#log(`received ${event.data.type} event for ${event.docId}`);
70
- const handle = this.#handleCache[event.docId];
71
- if (handle != null) {
72
- handle.update(d => Automerge.loadIncremental(d, event.data.contents));
73
- }
74
- });
75
- this.#beelay.on("bundleRequired", ({ start, end, checkpoints, docId }) => {
76
- ;
77
- (async () => {
78
- const doc = await this.storageSubsystem.loadDoc(docId);
79
- if (doc == null) {
80
- console.warn("document not found when creating bundle");
81
- return;
82
- }
83
- const bundle = A.saveBundle(doc, start, end);
84
- this.#beelay.addBundle({
85
- docId,
86
- checkpoints,
87
- start,
88
- end,
89
- data: bundle,
50
+ this.on("delete-document", ({ documentId }) => {
51
+ // TODO Pass the delete on to the network
52
+ // synchronizer.removeDocument(documentId)
53
+ if (storageSubsystem) {
54
+ storageSubsystem.removeDoc(documentId).catch(err => {
55
+ this.#log("error deleting document", { documentId, err });
90
56
  });
91
- })();
57
+ }
92
58
  });
93
59
  // SYNCHRONIZER
94
- this.synchronizer = new CollectionSynchronizer(this.#beelay, this, []);
60
+ // The synchronizer uses the network subsystem to keep documents in sync with peers.
61
+ this.synchronizer = new CollectionSynchronizer(this, denylist);
62
+ // When the synchronizer emits messages, send them to peers
95
63
  this.synchronizer.on("message", message => {
96
64
  this.#log(`sending ${message.type} message to ${message.targetId}`);
97
65
  networkSubsystem.send(message);
98
66
  });
67
+ // Forward metrics from doc synchronizers
68
+ this.synchronizer.on("metrics", event => this.emit("doc-metrics", event));
69
+ if (this.#remoteHeadsGossipingEnabled) {
70
+ this.synchronizer.on("open-doc", ({ peerId, documentId }) => {
71
+ this.#remoteHeadsSubscriptions.subscribePeerToDoc(peerId, documentId);
72
+ });
73
+ }
74
+ // STORAGE
75
+ // The storage subsystem has access to some form of persistence, and deals with save and loading documents.
76
+ const storageSubsystem = storage ? new StorageSubsystem(storage) : undefined;
77
+ if (storageSubsystem) {
78
+ storageSubsystem.on("document-loaded", event => this.emit("doc-metrics", { type: "doc-loaded", ...event }));
79
+ }
80
+ this.storageSubsystem = storageSubsystem;
99
81
  // NETWORK
100
82
  // The network subsystem deals with sending and receiving messages to and from peers.
101
83
  const myPeerMetadata = (async () => ({
102
- // storageId: await this.storageSubsystem.id(),
84
+ storageId: await storageSubsystem?.id(),
103
85
  isEphemeral,
104
86
  }))();
105
87
  const networkSubsystem = new NetworkSubsystem(network, peerId, myPeerMetadata);
@@ -110,69 +92,128 @@ export class Repo extends EventEmitter {
110
92
  if (peerMetadata) {
111
93
  this.peerMetadataByPeerId[peerId] = { ...peerMetadata };
112
94
  }
95
+ this.sharePolicy(peerId)
96
+ .then(shouldShare => {
97
+ if (shouldShare && this.#remoteHeadsGossipingEnabled) {
98
+ this.#remoteHeadsSubscriptions.addGenerousPeer(peerId);
99
+ }
100
+ })
101
+ .catch(err => {
102
+ console.log("error in share policy", { err });
103
+ });
113
104
  this.synchronizer.addPeer(peerId);
114
105
  });
106
+ // When a peer disconnects, remove it from the synchronizer
107
+ networkSubsystem.on("peer-disconnected", ({ peerId }) => {
108
+ this.synchronizer.removePeer(peerId);
109
+ this.#remoteHeadsSubscriptions.removePeer(peerId);
110
+ });
115
111
  // Handle incoming messages
116
112
  networkSubsystem.on("message", async (msg) => {
117
- //@ts-ignore
118
- // const inspected = A.beelay.inspectMessage(msg.message)
119
- // this.#log(`received msg: ${JSON.stringify(inspected)}`)
120
- //@ts-ignore
121
- if (msg.type === "beelay") {
122
- if (!(msg.message instanceof Uint8Array)) {
123
- // The Uint8Array instance in the vitest VM is _different_ from the
124
- // Uint8Array instance which is available in this file for some reason.
125
- // So, even though `msg.message` _is_ a `Uint8Array`, we have to do this
126
- // absurd thing to get the tests to pass
127
- msg.message = Uint8Array.from(msg.message);
128
- }
129
- this.#beelay.receiveMessage({
130
- message: {
131
- sender: msg.senderId,
132
- recipient: msg.targetId,
133
- message: msg.message,
134
- },
135
- });
113
+ this.#receiveMessage(msg);
114
+ });
115
+ this.synchronizer.on("sync-state", message => {
116
+ this.#saveSyncState(message);
117
+ const handle = this.#handleCache[message.documentId];
118
+ const { storageId } = this.peerMetadataByPeerId[message.peerId] || {};
119
+ if (!storageId) {
120
+ return;
136
121
  }
137
- else {
138
- this.#receiveMessage(msg);
122
+ const heads = handle.getRemoteHeads(storageId);
123
+ const haveHeadsChanged = message.syncState.theirHeads &&
124
+ (!heads ||
125
+ !headsAreSame(heads, encodeHeads(message.syncState.theirHeads)));
126
+ if (haveHeadsChanged && message.syncState.theirHeads) {
127
+ handle.setRemoteHeads(storageId, encodeHeads(message.syncState.theirHeads));
128
+ if (storageId && this.#remoteHeadsGossipingEnabled) {
129
+ this.#remoteHeadsSubscriptions.handleImmediateRemoteHeadsChanged(message.documentId, storageId, encodeHeads(message.syncState.theirHeads));
130
+ }
139
131
  }
140
132
  });
133
+ if (this.#remoteHeadsGossipingEnabled) {
134
+ this.#remoteHeadsSubscriptions.on("notify-remote-heads", message => {
135
+ this.networkSubsystem.send({
136
+ type: "remote-heads-changed",
137
+ targetId: message.targetId,
138
+ documentId: message.documentId,
139
+ newHeads: {
140
+ [message.storageId]: {
141
+ heads: message.heads,
142
+ timestamp: message.timestamp,
143
+ },
144
+ },
145
+ });
146
+ });
147
+ this.#remoteHeadsSubscriptions.on("change-remote-subs", message => {
148
+ this.#log("change-remote-subs", message);
149
+ for (const peer of message.peers) {
150
+ this.networkSubsystem.send({
151
+ type: "remote-subscription-change",
152
+ targetId: peer,
153
+ add: message.add,
154
+ remove: message.remove,
155
+ });
156
+ }
157
+ });
158
+ this.#remoteHeadsSubscriptions.on("remote-heads-changed", message => {
159
+ const handle = this.#handleCache[message.documentId];
160
+ handle.setRemoteHeads(message.storageId, message.remoteHeads);
161
+ });
162
+ }
141
163
  }
142
164
  // The `document` event is fired by the DocCollection any time we create a new document or look
143
165
  // up a document by ID. We listen for it in order to wire up storage and network synchronization.
144
166
  #registerHandleWithSubsystems(handle) {
145
- handle.on("heads-changed", () => {
146
- const doc = handle.docSync();
147
- if (doc != null) {
148
- this.storageSubsystem.saveDoc(handle.documentId, doc);
149
- }
150
- });
151
- handle.on("unavailable", () => {
152
- this.#log("document unavailable", { documentId: handle.documentId });
153
- this.emit("unavailable-document", {
154
- documentId: handle.documentId,
155
- });
156
- });
157
- this.synchronizer.addDocument(handle.documentId);
158
- // Preserve the old event in case anyone was using it.
159
- this.emit("document", { handle });
167
+ const { storageSubsystem } = this;
168
+ if (storageSubsystem) {
169
+ // Save when the document changes, but no more often than saveDebounceRate.
170
+ const saveFn = ({ handle, doc }) => {
171
+ void storageSubsystem.saveDoc(handle.documentId, doc);
172
+ };
173
+ handle.on("heads-changed", throttle(saveFn, this.saveDebounceRate));
174
+ }
175
+ // Register the document with the synchronizer. This advertises our interest in the document.
176
+ this.synchronizer.addDocument(handle);
160
177
  }
161
178
  #receiveMessage(message) {
162
179
  switch (message.type) {
163
180
  case "remote-subscription-change":
181
+ if (this.#remoteHeadsGossipingEnabled) {
182
+ this.#remoteHeadsSubscriptions.handleControlMessage(message);
183
+ }
184
+ break;
164
185
  case "remote-heads-changed":
186
+ if (this.#remoteHeadsGossipingEnabled) {
187
+ this.#remoteHeadsSubscriptions.handleRemoteHeads(message);
188
+ }
165
189
  break;
166
190
  case "sync":
167
191
  case "request":
168
192
  case "ephemeral":
169
193
  case "doc-unavailable":
170
194
  this.synchronizer.receiveMessage(message).catch(err => {
171
- console.error("error receiving message", { err });
195
+ console.log("error receiving message", { err });
172
196
  });
173
- break;
174
197
  }
175
198
  }
199
+ #throttledSaveSyncStateHandlers = {};
200
+ /** saves sync state throttled per storage id, if a peer doesn't have a storage id it's sync state is not persisted */
201
+ #saveSyncState(payload) {
202
+ if (!this.storageSubsystem) {
203
+ return;
204
+ }
205
+ const { storageId, isEphemeral } = this.peerMetadataByPeerId[payload.peerId] || {};
206
+ if (!storageId || isEphemeral) {
207
+ return;
208
+ }
209
+ let handler = this.#throttledSaveSyncStateHandlers[storageId];
210
+ if (!handler) {
211
+ handler = this.#throttledSaveSyncStateHandlers[storageId] = throttle(({ documentId, syncState }) => {
212
+ void this.storageSubsystem.saveSyncState(documentId, storageId, syncState);
213
+ }, this.saveDebounceRate);
214
+ }
215
+ handler(payload);
216
+ }
176
217
  /** Returns an existing handle if we have it; creates one otherwise. */
177
218
  #getHandle({ documentId, }) {
178
219
  // If we have the handle cached, return it
@@ -191,7 +232,7 @@ export class Repo extends EventEmitter {
191
232
  }
192
233
  /** Returns a list of all connected peer ids */
193
234
  get peers() {
194
- return this.networkSubsystem.peers;
235
+ return this.synchronizer.peers;
195
236
  }
196
237
  getStorageIdOfPeer(peerId) {
197
238
  return this.peerMetadataByPeerId[peerId]?.storageId;
@@ -207,7 +248,7 @@ export class Repo extends EventEmitter {
207
248
  const handle = this.#getHandle({
208
249
  documentId,
209
250
  });
210
- let initialLinks = [];
251
+ this.#registerHandleWithSubsystems(handle);
211
252
  handle.update(() => {
212
253
  let nextDoc;
213
254
  if (initialValue) {
@@ -216,27 +257,8 @@ export class Repo extends EventEmitter {
216
257
  else {
217
258
  nextDoc = Automerge.emptyChange(Automerge.init());
218
259
  }
219
- const patches = A.diff(nextDoc, [], A.getHeads(nextDoc));
220
- for (const patch of patches) {
221
- initialLinks = patches
222
- .map(patch => {
223
- if (patch.action === "put") {
224
- if (patch.value instanceof A.Link) {
225
- return patch.value;
226
- }
227
- }
228
- return null;
229
- })
230
- .filter(v => v != null);
231
- }
232
260
  return nextDoc;
233
261
  });
234
- for (const link of initialLinks) {
235
- const { documentId: target } = parseAutomergeUrl(link.target);
236
- this.#beelay.addLink({ from: documentId, to: target });
237
- }
238
- this.storageSubsystem.saveDoc(handle.documentId, handle.docSync());
239
- this.#registerHandleWithSubsystems(handle);
240
262
  handle.doneLoading();
241
263
  return handle;
242
264
  }
@@ -252,18 +274,13 @@ export class Repo extends EventEmitter {
252
274
  * Any peers this `Repo` is connected to for whom `sharePolicy` returns `true` will
253
275
  * be notified of the newly created DocHandle.
254
276
  *
255
- * @throws if the cloned handle is not yet ready or if
256
- * `clonedHandle.docSync()` returns `undefined` (i.e. the handle is unavailable).
257
277
  */
258
278
  clone(clonedHandle) {
259
279
  if (!clonedHandle.isReady()) {
260
280
  throw new Error(`Cloned handle is not yet in ready state.
261
281
  (Try await handle.whenReady() first.)`);
262
282
  }
263
- const sourceDoc = clonedHandle.docSync();
264
- if (!sourceDoc) {
265
- throw new Error("Cloned handle doesn't have a document.");
266
- }
283
+ const sourceDoc = clonedHandle.doc();
267
284
  const handle = this.create();
268
285
  handle.update(() => {
269
286
  // we replace the document with the new cloned one
@@ -271,55 +288,220 @@ export class Repo extends EventEmitter {
271
288
  });
272
289
  return handle;
273
290
  }
274
- /**
275
- * Retrieves a document by id. It gets data from the local system, but also emits a `document`
276
- * event to advertise interest in the document.
277
- */
278
- find(
279
- /** The url or documentId of the handle to retrieve */
280
- id) {
281
- this.#log("find", { id });
282
- const documentId = interpretAsDocumentId(id);
283
- // If we have the handle cached, return it
291
+ findWithProgress(id, options = {}) {
292
+ const { signal } = options;
293
+ const { documentId, heads } = isValidAutomergeUrl(id)
294
+ ? parseAutomergeUrl(id)
295
+ : { documentId: interpretAsDocumentId(id), heads: undefined };
296
+ // Check handle cache first - return plain FindStep for terminal states
284
297
  if (this.#handleCache[documentId]) {
285
- if (this.#handleCache[documentId].isUnavailable()) {
286
- // this ensures that the event fires after the handle has been returned
287
- setTimeout(() => {
288
- this.#handleCache[documentId].emit("unavailable", {
289
- handle: this.#handleCache[documentId],
290
- });
291
- });
298
+ const handle = this.#handleCache[documentId];
299
+ if (handle.state === UNAVAILABLE) {
300
+ const result = {
301
+ state: "unavailable",
302
+ error: new Error(`Document ${id} is unavailable`),
303
+ handle,
304
+ };
305
+ return result;
306
+ }
307
+ if (handle.state === DELETED) {
308
+ const result = {
309
+ state: "failed",
310
+ error: new Error(`Document ${id} was deleted`),
311
+ handle,
312
+ };
313
+ return result;
314
+ }
315
+ if (handle.state === READY) {
316
+ const result = {
317
+ state: "ready",
318
+ handle: heads ? handle.view(heads) : handle,
319
+ };
320
+ return result;
292
321
  }
293
- return this.#handleCache[documentId];
294
322
  }
295
- // If we don't already have the handle, make an empty one and try loading it
296
- const handle = this.#getHandle({
297
- documentId,
298
- });
299
- // Loading & network is going to be asynchronous no matter what,
300
- // but we want to return the handle immediately.
301
- const attemptLoad = this.storageSubsystem.loadDoc(handle.documentId);
302
- attemptLoad
303
- .then(async (loadedDoc) => {
323
+ // Check progress cache for any existing signal
324
+ const cachedProgress = this.#progressCache[documentId];
325
+ if (cachedProgress) {
326
+ const handle = this.#handleCache[documentId];
327
+ // Return cached progress if we have a handle and it's either in a terminal state or loading
328
+ if (handle &&
329
+ (handle.state === READY ||
330
+ handle.state === UNAVAILABLE ||
331
+ handle.state === DELETED ||
332
+ handle.state === "loading")) {
333
+ return cachedProgress;
334
+ }
335
+ }
336
+ const handle = this.#getHandle({ documentId });
337
+ const initial = {
338
+ state: "loading",
339
+ progress: 0,
340
+ handle,
341
+ };
342
+ // Create a new progress signal
343
+ const progressSignal = {
344
+ subscribers: new Set(),
345
+ currentProgress: undefined,
346
+ notify: (progress) => {
347
+ progressSignal.currentProgress = progress;
348
+ progressSignal.subscribers.forEach(callback => callback(progress));
349
+ // Cache all states, not just terminal ones
350
+ this.#progressCache[documentId] = progress;
351
+ },
352
+ peek: () => progressSignal.currentProgress || initial,
353
+ subscribe: (callback) => {
354
+ progressSignal.subscribers.add(callback);
355
+ return () => progressSignal.subscribers.delete(callback);
356
+ },
357
+ };
358
+ progressSignal.notify(initial);
359
+ // Start the loading process
360
+ void this.#loadDocumentWithProgress(id, documentId, handle, progressSignal, signal ? abortable(new Promise(() => { }), signal) : new Promise(() => { }));
361
+ const result = {
362
+ ...initial,
363
+ peek: progressSignal.peek,
364
+ subscribe: progressSignal.subscribe,
365
+ };
366
+ this.#progressCache[documentId] = result;
367
+ return result;
368
+ }
369
+ async #loadDocumentWithProgress(id, documentId, handle, progressSignal, abortPromise) {
370
+ try {
371
+ progressSignal.notify({
372
+ state: "loading",
373
+ progress: 25,
374
+ handle,
375
+ });
376
+ const loadingPromise = await (this.storageSubsystem
377
+ ? this.storageSubsystem.loadDoc(handle.documentId)
378
+ : Promise.resolve(null));
379
+ const loadedDoc = await Promise.race([loadingPromise, abortPromise]);
304
380
  if (loadedDoc) {
305
- // uhhhh, sorry if you're reading this because we were lying to the type system
306
381
  handle.update(() => loadedDoc);
307
382
  handle.doneLoading();
383
+ progressSignal.notify({
384
+ state: "loading",
385
+ progress: 50,
386
+ handle,
387
+ });
308
388
  }
309
389
  else {
310
- // we want to wait for the network subsystem to be ready before
311
- // we request the document. this prevents entering unavailable during initialization.
312
- await this.networkSubsystem.whenReady();
313
- console.log("we didn't find it so we're requesting");
390
+ await Promise.race([this.networkSubsystem.whenReady(), abortPromise]);
314
391
  handle.request();
392
+ progressSignal.notify({
393
+ state: "loading",
394
+ progress: 75,
395
+ handle,
396
+ });
315
397
  }
316
398
  this.#registerHandleWithSubsystems(handle);
317
- })
318
- .catch(err => {
319
- this.#log("error waiting for network", { err });
320
- });
399
+ await Promise.race([handle.whenReady([READY, UNAVAILABLE]), abortPromise]);
400
+ if (handle.state === UNAVAILABLE) {
401
+ const unavailableProgress = {
402
+ state: "unavailable",
403
+ handle,
404
+ };
405
+ progressSignal.notify(unavailableProgress);
406
+ return;
407
+ }
408
+ if (handle.state === DELETED) {
409
+ throw new Error(`Document ${id} was deleted`);
410
+ }
411
+ progressSignal.notify({ state: "ready", handle });
412
+ }
413
+ catch (error) {
414
+ progressSignal.notify({
415
+ state: "failed",
416
+ error: error instanceof Error ? error : new Error(String(error)),
417
+ handle: this.#getHandle({ documentId }),
418
+ });
419
+ }
420
+ }
421
+ async find(id, options = {}) {
422
+ const { allowableStates = ["ready"], signal } = options;
423
+ // Check if already aborted
424
+ if (signal?.aborted) {
425
+ throw new Error("Operation aborted");
426
+ }
427
+ const progress = this.findWithProgress(id, { signal });
428
+ if ("subscribe" in progress) {
429
+ this.#registerHandleWithSubsystems(progress.handle);
430
+ return new Promise((resolve, reject) => {
431
+ const unsubscribe = progress.subscribe(state => {
432
+ if (allowableStates.includes(state.handle.state)) {
433
+ unsubscribe();
434
+ resolve(state.handle);
435
+ }
436
+ else if (state.state === "unavailable") {
437
+ unsubscribe();
438
+ reject(new Error(`Document ${id} is unavailable`));
439
+ }
440
+ else if (state.state === "failed") {
441
+ unsubscribe();
442
+ reject(state.error);
443
+ }
444
+ });
445
+ });
446
+ }
447
+ else {
448
+ if (progress.handle.state === READY) {
449
+ return progress.handle;
450
+ }
451
+ // If the handle isn't ready, wait for it and then return it
452
+ await progress.handle.whenReady([READY, UNAVAILABLE]);
453
+ return progress.handle;
454
+ }
455
+ }
456
+ /**
457
+ * Loads a document without waiting for ready state
458
+ */
459
+ async #loadDocument(documentId) {
460
+ // If we have the handle cached, return it
461
+ if (this.#handleCache[documentId]) {
462
+ return this.#handleCache[documentId];
463
+ }
464
+ // If we don't already have the handle, make an empty one and try loading it
465
+ const handle = this.#getHandle({ documentId });
466
+ const loadedDoc = await (this.storageSubsystem
467
+ ? this.storageSubsystem.loadDoc(handle.documentId)
468
+ : Promise.resolve(null));
469
+ if (loadedDoc) {
470
+ // We need to cast this to <T> because loadDoc operates in <unknowns>.
471
+ // This is really where we ought to be validating the input matches <T>.
472
+ handle.update(() => loadedDoc);
473
+ handle.doneLoading();
474
+ }
475
+ else {
476
+ // Because the network subsystem might still be booting up, we wait
477
+ // here so that we don't immediately give up loading because we're still
478
+ // making our initial connection to a sync server.
479
+ await this.networkSubsystem.whenReady();
480
+ handle.request();
481
+ }
482
+ this.#registerHandleWithSubsystems(handle);
321
483
  return handle;
322
484
  }
485
+ /**
486
+ * Retrieves a document by id. It gets data from the local system, but also emits a `document`
487
+ * event to advertise interest in the document.
488
+ */
489
+ async findClassic(
490
+ /** The url or documentId of the handle to retrieve */
491
+ id, options = {}) {
492
+ const documentId = interpretAsDocumentId(id);
493
+ const { allowableStates, signal } = options;
494
+ return abortable((async () => {
495
+ const handle = await this.#loadDocument(documentId);
496
+ if (!allowableStates) {
497
+ await handle.whenReady([READY, UNAVAILABLE]);
498
+ if (handle.state === UNAVAILABLE && !signal?.aborted) {
499
+ throw new Error(`Document ${id} is unavailable`);
500
+ }
501
+ }
502
+ return handle;
503
+ })(), signal);
504
+ }
323
505
  delete(
324
506
  /** The url or documentId of the handle to delete */
325
507
  id) {
@@ -327,6 +509,7 @@ export class Repo extends EventEmitter {
327
509
  const handle = this.#getHandle({ documentId });
328
510
  handle.delete();
329
511
  delete this.#handleCache[documentId];
512
+ delete this.#progressCache[documentId];
330
513
  this.emit("delete-document", { documentId });
331
514
  }
332
515
  /**
@@ -339,9 +522,7 @@ export class Repo extends EventEmitter {
339
522
  async export(id) {
340
523
  const documentId = interpretAsDocumentId(id);
341
524
  const handle = this.#getHandle({ documentId });
342
- const doc = await handle.doc();
343
- if (!doc)
344
- return undefined;
525
+ const doc = handle.doc();
345
526
  return Automerge.save(doc);
346
527
  }
347
528
  /**
@@ -356,7 +537,15 @@ export class Repo extends EventEmitter {
356
537
  });
357
538
  return handle;
358
539
  }
359
- subscribeToRemotes = (remotes) => { };
540
+ subscribeToRemotes = (remotes) => {
541
+ if (this.#remoteHeadsGossipingEnabled) {
542
+ this.#log("subscribeToRemotes", { remotes });
543
+ this.#remoteHeadsSubscriptions.subscribeToRemotes(remotes);
544
+ }
545
+ else {
546
+ this.#log("WARN: subscribeToRemotes called but remote heads gossiping is not enabled");
547
+ }
548
+ };
360
549
  storageId = async () => {
361
550
  if (!this.storageSubsystem) {
362
551
  return undefined;
@@ -379,11 +568,7 @@ export class Repo extends EventEmitter {
379
568
  ? documents.map(id => this.#handleCache[id])
380
569
  : Object.values(this.#handleCache);
381
570
  await Promise.all(handles.map(async (handle) => {
382
- const doc = handle.docSync();
383
- if (!doc) {
384
- return;
385
- }
386
- return this.storageSubsystem.saveDoc(handle.documentId, doc);
571
+ return this.storageSubsystem.saveDoc(handle.documentId, handle.doc());
387
572
  }));
388
573
  }
389
574
  /**
@@ -398,7 +583,9 @@ export class Repo extends EventEmitter {
398
583
  return;
399
584
  }
400
585
  const handle = this.#getHandle({ documentId });
401
- const doc = await handle.doc([READY, UNLOADED, DELETED, UNAVAILABLE]);
586
+ await handle.whenReady([READY, UNLOADED, DELETED, UNAVAILABLE]);
587
+ const doc = handle.doc();
588
+ // because this is an internal-ish function, we'll be extra careful about undefined docs here
402
589
  if (doc) {
403
590
  if (handle.isReady()) {
404
591
  handle.unload();
@@ -421,7 +608,6 @@ export class Repo extends EventEmitter {
421
608
  return this.flush();
422
609
  }
423
610
  metrics() {
424
- //return { documents: this.synchronizer.metrics() }
425
- return { documents: {} };
611
+ return { documents: this.synchronizer.metrics() };
426
612
  }
427
613
  }