@automerge/automerge-repo 2.0.0-collectionsync-alpha.1 → 2.0.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 (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
@@ -1,9 +1,10 @@
1
- import * as A from "@automerge/automerge/slim/next";
1
+ import { next as A } from "@automerge/automerge/slim";
2
2
  import { decode } from "cbor-x";
3
3
  import debug from "debug";
4
4
  import { READY, REQUESTING, UNAVAILABLE, } from "../DocHandle.js";
5
+ import { isRequestMessage, } from "../network/messages.js";
5
6
  import { Synchronizer } from "./Synchronizer.js";
6
- import { parseAutomergeUrl } from "../AutomergeUrl.js";
7
+ import { throttle } from "../helpers/throttle.js";
7
8
  /**
8
9
  * DocSynchronizer takes a handle to an Automerge document, and receives & dispatches sync messages
9
10
  * to bring it inline with all other peers' versions.
@@ -13,43 +14,49 @@ export class DocSynchronizer extends Synchronizer {
13
14
  syncDebounceRate = 100;
14
15
  /** Active peers */
15
16
  #peers = [];
17
+ #pendingSyncStateCallbacks = {};
16
18
  #peerDocumentStatuses = {};
17
- #lastSaveOffset = null;
19
+ /** Sync state for each peer we've communicated with (including inactive peers) */
20
+ #syncStates = {};
21
+ #pendingSyncMessages = [];
22
+ // We keep this around at least in part for debugging.
23
+ // eslint-disable-next-line no-unused-private-class-members
24
+ #peerId;
18
25
  #syncStarted = false;
19
- #beelay;
20
26
  #handle;
21
- #docId;
22
- constructor({ handle, beelay }) {
27
+ #onLoadSyncState;
28
+ constructor({ handle, peerId, onLoadSyncState }) {
23
29
  super();
30
+ this.#peerId = peerId;
24
31
  this.#handle = handle;
25
- this.#beelay = beelay;
26
- this.#docId = this.#handle.documentId;
27
- this.#log = debug(`automerge-repo:docsync:${this.#handle.documentId}`);
32
+ this.#onLoadSyncState =
33
+ onLoadSyncState ?? (() => Promise.resolve(undefined));
34
+ const docId = handle.documentId.slice(0, 5);
35
+ this.#log = debug(`automerge-repo:docsync:${docId}`);
36
+ handle.on("change", throttle(() => this.#syncWithPeers(), this.syncDebounceRate));
28
37
  handle.on("ephemeral-message-outbound", payload => this.#broadcastToPeers(payload));
29
- handle.on("change", changeInfo => {
30
- const newLinks = changeInfo.patches
31
- .map(patch => {
32
- if (patch.action === "put") {
33
- if (patch.value instanceof A.Link) {
34
- return patch.value;
35
- }
36
- }
37
- return null;
38
- })
39
- .filter(v => v != null);
40
- for (const link of newLinks) {
41
- const { documentId: target } = parseAutomergeUrl(link.target);
42
- this.#beelay.addLink({ from: this.#handle.documentId, to: target });
43
- }
44
- });
38
+ // Process pending sync messages immediately after the handle becomes ready.
39
+ void (async () => {
40
+ this.#processAllPendingSyncMessages();
41
+ })();
45
42
  }
46
43
  get peerStates() {
47
44
  return this.#peerDocumentStatuses;
48
45
  }
49
46
  get documentId() {
50
- return this.#docId;
47
+ return this.#handle.documentId;
51
48
  }
52
49
  /// PRIVATE
50
+ async #syncWithPeers() {
51
+ try {
52
+ await this.#handle.whenReady();
53
+ const doc = this.#handle.doc(); // XXX THIS ONE IS WEIRD
54
+ this.#peers.forEach(peerId => this.#sendSyncMessage(peerId, doc));
55
+ }
56
+ catch (e) {
57
+ console.log("sync with peers threw an exception");
58
+ }
59
+ }
53
60
  async #broadcastToPeers({ data, }) {
54
61
  this.#log(`broadcastToPeers`, this.#peers);
55
62
  this.#peers.forEach(peerId => this.#sendEphemeralMessage(peerId, data));
@@ -59,78 +66,155 @@ export class DocSynchronizer extends Synchronizer {
59
66
  const message = {
60
67
  type: "ephemeral",
61
68
  targetId: peerId,
62
- documentId: this.documentId,
69
+ documentId: this.#handle.documentId,
63
70
  data,
64
71
  };
65
72
  this.emit("message", message);
66
73
  }
74
+ #withSyncState(peerId, callback) {
75
+ this.#addPeer(peerId);
76
+ if (!(peerId in this.#peerDocumentStatuses)) {
77
+ this.#peerDocumentStatuses[peerId] = "unknown";
78
+ }
79
+ const syncState = this.#syncStates[peerId];
80
+ if (syncState) {
81
+ callback(syncState);
82
+ return;
83
+ }
84
+ let pendingCallbacks = this.#pendingSyncStateCallbacks[peerId];
85
+ if (!pendingCallbacks) {
86
+ this.#onLoadSyncState(peerId)
87
+ .then(syncState => {
88
+ this.#initSyncState(peerId, syncState ?? A.initSyncState());
89
+ })
90
+ .catch(err => {
91
+ this.#log(`Error loading sync state for ${peerId}: ${err}`);
92
+ });
93
+ pendingCallbacks = this.#pendingSyncStateCallbacks[peerId] = [];
94
+ }
95
+ pendingCallbacks.push(callback);
96
+ }
97
+ #addPeer(peerId) {
98
+ if (!this.#peers.includes(peerId)) {
99
+ this.#peers.push(peerId);
100
+ this.emit("open-doc", { documentId: this.documentId, peerId });
101
+ }
102
+ }
103
+ #initSyncState(peerId, syncState) {
104
+ const pendingCallbacks = this.#pendingSyncStateCallbacks[peerId];
105
+ if (pendingCallbacks) {
106
+ for (const callback of pendingCallbacks) {
107
+ callback(syncState);
108
+ }
109
+ }
110
+ delete this.#pendingSyncStateCallbacks[peerId];
111
+ this.#syncStates[peerId] = syncState;
112
+ }
113
+ #setSyncState(peerId, syncState) {
114
+ this.#syncStates[peerId] = syncState;
115
+ this.emit("sync-state", {
116
+ peerId,
117
+ syncState,
118
+ documentId: this.#handle.documentId,
119
+ });
120
+ }
121
+ #sendSyncMessage(peerId, doc) {
122
+ this.#log(`sendSyncMessage ->${peerId}`);
123
+ this.#withSyncState(peerId, syncState => {
124
+ const [newSyncState, message] = A.generateSyncMessage(doc, syncState);
125
+ if (message) {
126
+ this.#setSyncState(peerId, newSyncState);
127
+ const isNew = A.getHeads(doc).length === 0;
128
+ if (!this.#handle.isReady() &&
129
+ isNew &&
130
+ newSyncState.sharedHeads.length === 0 &&
131
+ !Object.values(this.#peerDocumentStatuses).includes("has") &&
132
+ this.#peerDocumentStatuses[peerId] === "unknown") {
133
+ // we don't have the document (or access to it), so we request it
134
+ this.emit("message", {
135
+ type: "request",
136
+ targetId: peerId,
137
+ documentId: this.#handle.documentId,
138
+ data: message,
139
+ });
140
+ }
141
+ else {
142
+ this.emit("message", {
143
+ type: "sync",
144
+ targetId: peerId,
145
+ data: message,
146
+ documentId: this.#handle.documentId,
147
+ });
148
+ }
149
+ // if we have sent heads, then the peer now has or will have the document
150
+ if (!isNew) {
151
+ this.#peerDocumentStatuses[peerId] = "has";
152
+ }
153
+ }
154
+ });
155
+ }
67
156
  /// PUBLIC
68
157
  hasPeer(peerId) {
69
158
  return this.#peers.includes(peerId);
70
159
  }
71
- beginSync(peerIds) {
72
- this.#log(`beginSync: ${peerIds.join(", ")}`);
73
- const docPromise = this.#handle
160
+ async beginSync(peerIds) {
161
+ void this.#handle
74
162
  .whenReady([READY, REQUESTING, UNAVAILABLE])
75
- .then(doc => {
163
+ .then(() => {
76
164
  this.#syncStarted = true;
77
165
  this.#checkDocUnavailable();
78
166
  })
79
- // TODO: handle this error
80
- .catch(() => { });
167
+ .catch(e => {
168
+ console.log("caught whenready", e);
169
+ this.#syncStarted = true;
170
+ this.#checkDocUnavailable();
171
+ });
172
+ const peersWithDocument = this.#peers.some(peerId => {
173
+ return this.#peerDocumentStatuses[peerId] == "has";
174
+ });
175
+ if (peersWithDocument) {
176
+ await this.#handle.whenReady();
177
+ }
81
178
  peerIds.forEach(peerId => {
82
- if (!this.#peers.includes(peerId)) {
83
- this.#peers.push(peerId);
84
- }
85
- else {
86
- return;
87
- }
88
- this.#peerDocumentStatuses[peerId] = "unknown";
89
- docPromise.then(() => {
90
- this.#syncStarted = true;
91
- this.#log(`beginning sync with ${peerId} for doc: ${this.documentId}`);
92
- this.#beelay
93
- .syncDoc(this.documentId, peerId)
94
- .then(({ snapshot, found }) => {
95
- this.#peerDocumentStatuses[peerId] = found ? "has" : "unavailable";
96
- // this.#log("synced snapshot: ", snapshot)
97
- if (found) {
98
- this.#beelay.loadDocument(this.#docId).then(commitOrBundles => {
99
- if (commitOrBundles != null) {
100
- this.#handle?.update(d => {
101
- let doc = d;
102
- for (const commitOrBundle of commitOrBundles) {
103
- doc = A.loadIncremental(doc, commitOrBundle.contents);
104
- }
105
- return doc;
106
- });
107
- this.#checkDocUnavailable();
108
- }
109
- });
110
- }
111
- else {
112
- this.#checkDocUnavailable();
179
+ this.#withSyncState(peerId, syncState => {
180
+ // HACK: if we have a sync state already, we round-trip it through the encoding system to make
181
+ // sure state is preserved. This prevents an infinite loop caused by failed attempts to send
182
+ // messages during disconnection.
183
+ // TODO: cover that case with a test and remove this hack
184
+ const reparsedSyncState = A.decodeSyncState(A.encodeSyncState(syncState));
185
+ this.#setSyncState(peerId, reparsedSyncState);
186
+ // At this point if we don't have anything in our storage, we need to use an empty doc to sync
187
+ // with; but we don't want to surface that state to the front end
188
+ this.#handle
189
+ .whenReady([READY, REQUESTING, UNAVAILABLE])
190
+ .then(() => {
191
+ const doc = this.#handle.isReady()
192
+ ? this.#handle.doc()
193
+ : A.init();
194
+ const noPeersWithDocument = peerIds.every(peerId => this.#peerDocumentStatuses[peerId] in ["unavailable", "wants"]);
195
+ const wasUnavailable = doc === undefined;
196
+ if (wasUnavailable && noPeersWithDocument) {
197
+ return;
113
198
  }
114
- this.#beelay.listen(peerId, snapshot);
199
+ // If the doc is unavailable we still need a blank document to generate
200
+ // the sync message from
201
+ this.#sendSyncMessage(peerId, doc ?? A.init());
202
+ })
203
+ .catch(err => {
204
+ this.#log(`Error loading doc for ${peerId}: ${err}`);
115
205
  });
116
206
  });
117
207
  });
118
208
  }
119
- peerWantsDocument(peerId) {
120
- this.#peerDocumentStatuses[peerId] = "wants";
121
- if (!this.#peers.includes(peerId)) {
122
- this.beginSync([peerId]);
123
- }
124
- }
125
209
  endSync(peerId) {
126
210
  this.#log(`removing peer ${peerId}`);
127
211
  this.#peers = this.#peers.filter(p => p !== peerId);
128
- this.#beelay.cancelListens(peerId);
129
212
  }
130
213
  receiveMessage(message) {
131
214
  switch (message.type) {
132
215
  case "sync":
133
216
  case "request":
217
+ this.receiveSyncMessage(message);
134
218
  break;
135
219
  case "ephemeral":
136
220
  this.receiveEphemeralMessage(message);
@@ -144,7 +228,7 @@ export class DocSynchronizer extends Synchronizer {
144
228
  }
145
229
  }
146
230
  receiveEphemeralMessage(message) {
147
- if (message.documentId !== this.documentId)
231
+ if (message.documentId !== this.#handle.documentId)
148
232
  throw new Error(`channelId doesn't match documentId`);
149
233
  const { senderId, data } = message;
150
234
  const contents = decode(new Uint8Array(data));
@@ -162,7 +246,45 @@ export class DocSynchronizer extends Synchronizer {
162
246
  });
163
247
  });
164
248
  }
165
- receiveSyncMessage(message) { }
249
+ receiveSyncMessage(message) {
250
+ if (message.documentId !== this.#handle.documentId)
251
+ throw new Error(`channelId doesn't match documentId`);
252
+ // We need to block receiving the syncMessages until we've checked local storage
253
+ if (!this.#handle.inState([READY, REQUESTING, UNAVAILABLE])) {
254
+ this.#pendingSyncMessages.push({ message, received: new Date() });
255
+ return;
256
+ }
257
+ this.#processAllPendingSyncMessages();
258
+ this.#processSyncMessage(message);
259
+ }
260
+ #processSyncMessage(message) {
261
+ if (isRequestMessage(message)) {
262
+ this.#peerDocumentStatuses[message.senderId] = "wants";
263
+ }
264
+ this.#checkDocUnavailable();
265
+ // if the message has heads, then the peer has the document
266
+ if (A.decodeSyncMessage(message.data).heads.length > 0) {
267
+ this.#peerDocumentStatuses[message.senderId] = "has";
268
+ }
269
+ this.#withSyncState(message.senderId, syncState => {
270
+ this.#handle.update(doc => {
271
+ const start = performance.now();
272
+ const [newDoc, newSyncState] = A.receiveSyncMessage(doc, syncState, message.data);
273
+ const end = performance.now();
274
+ this.emit("metrics", {
275
+ type: "receive-sync-message",
276
+ documentId: this.#handle.documentId,
277
+ durationMillis: end - start,
278
+ ...A.stats(doc),
279
+ });
280
+ this.#setSyncState(message.senderId, newSyncState);
281
+ // respond to just this peer (as required)
282
+ this.#sendSyncMessage(message.senderId, doc);
283
+ return newDoc;
284
+ });
285
+ this.#checkDocUnavailable();
286
+ });
287
+ }
166
288
  #checkDocUnavailable() {
167
289
  // if we know none of the peers have the document, tell all our peers that we don't either
168
290
  if (this.#syncStarted &&
@@ -174,20 +296,24 @@ export class DocSynchronizer extends Synchronizer {
174
296
  .forEach(peerId => {
175
297
  const message = {
176
298
  type: "doc-unavailable",
177
- documentId: this.documentId,
299
+ documentId: this.#handle.documentId,
178
300
  targetId: peerId,
179
301
  };
180
302
  this.emit("message", message);
181
303
  });
182
- if (this.#handle) {
183
- this.#handle.unavailable();
184
- }
304
+ this.#handle.unavailable();
305
+ }
306
+ }
307
+ #processAllPendingSyncMessages() {
308
+ for (const message of this.#pendingSyncMessages) {
309
+ this.#processSyncMessage(message.message);
185
310
  }
311
+ this.#pendingSyncMessages = [];
186
312
  }
187
313
  metrics() {
188
314
  return {
189
315
  peers: this.#peers,
190
- size: this.#handle?.metrics(),
316
+ size: this.#handle.metrics(),
191
317
  };
192
318
  }
193
319
  }
package/dist/types.d.ts CHANGED
@@ -26,12 +26,15 @@ export type LegacyDocumentId = string & {
26
26
  __legacyDocumentId: true;
27
27
  };
28
28
  export type AnyDocumentId = AutomergeUrl | DocumentId | BinaryDocumentId | LegacyDocumentId;
29
+ export type UrlHeads = string[] & {
30
+ __automergeUrlHeads: unknown;
31
+ };
29
32
  /** A branded type for peer IDs */
30
33
  export type PeerId = string & {
31
34
  __peerId: true;
32
35
  };
33
36
  /** A randomly generated string created when the {@link Repo} starts up */
34
37
  export type SessionId = string & {
35
- __SessionId: true;
38
+ __sessionId: true;
36
39
  };
37
40
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG;IAAE,aAAa,EAAE,IAAI,CAAA;CAAE,CAAA;AAE3D;;;;;GAKG;AACH,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG;IAAE,YAAY,EAAE,IAAI,CAAA;CAAE,CAAA;AAExD,iGAAiG;AACjG,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG;IAAE,kBAAkB,EAAE,IAAI,CAAA;CAAE,CAAA;AAExE;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG;IAAE,kBAAkB,EAAE,IAAI,CAAA;CAAE,CAAA;AAEpE,MAAM,MAAM,aAAa,GACrB,YAAY,GACZ,UAAU,GACV,gBAAgB,GAChB,gBAAgB,CAAA;AAEpB,kCAAkC;AAClC,MAAM,MAAM,MAAM,GAAG,MAAM,GAAG;IAAE,QAAQ,EAAE,IAAI,CAAA;CAAE,CAAA;AAEhD,0EAA0E;AAC1E,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG;IAAE,WAAW,EAAE,IAAI,CAAA;CAAE,CAAA"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG;IAAE,aAAa,EAAE,IAAI,CAAA;CAAE,CAAA;AAE3D;;;;;GAKG;AACH,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG;IAAE,YAAY,EAAE,IAAI,CAAA;CAAE,CAAA;AAExD,iGAAiG;AACjG,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG;IAAE,kBAAkB,EAAE,IAAI,CAAA;CAAE,CAAA;AAExE;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG;IAAE,kBAAkB,EAAE,IAAI,CAAA;CAAE,CAAA;AAEpE,MAAM,MAAM,aAAa,GACrB,YAAY,GACZ,UAAU,GACV,gBAAgB,GAChB,gBAAgB,CAAA;AAGpB,MAAM,MAAM,QAAQ,GAAG,MAAM,EAAE,GAAG;IAAE,mBAAmB,EAAE,OAAO,CAAA;CAAE,CAAA;AAElE,kCAAkC;AAClC,MAAM,MAAM,MAAM,GAAG,MAAM,GAAG;IAAE,QAAQ,EAAE,IAAI,CAAA;CAAE,CAAA;AAEhD,0EAA0E;AAC1E,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG;IAAE,WAAW,EAAE,IAAI,CAAA;CAAE,CAAA"}
package/fuzz/fuzz.ts CHANGED
@@ -107,9 +107,9 @@ for (let i = 0; i < 100000; i++) {
107
107
  })
108
108
 
109
109
  await pause(0)
110
- const a = await aliceRepo.find(doc.url).doc()
111
- const b = await bobRepo.find(doc.url).doc()
112
- const c = await charlieRepo.find(doc.url).doc()
110
+ const a = (await aliceRepo.find(doc.url)).doc()
111
+ const b = (await bobRepo.find(doc.url)).doc()
112
+ const c = (await charlieRepo.find(doc.url)).doc()
113
113
  assert.deepStrictEqual(a, b, "A and B should be equal")
114
114
  assert.deepStrictEqual(b, c, "B and C should be equal")
115
115
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automerge/automerge-repo",
3
- "version": "2.0.0-collectionsync-alpha.1",
3
+ "version": "2.0.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>",
@@ -20,17 +20,16 @@
20
20
  },
21
21
  "devDependencies": {
22
22
  "http-server": "^14.1.0",
23
+ "ts-node": "^10.9.2",
23
24
  "vite": "^5.0.8"
24
25
  },
25
26
  "dependencies": {
26
- "@automerge/automerge": "3.0.0-collectionsync-alpha.1",
27
+ "@automerge/automerge": "^2.2.8",
27
28
  "bs58check": "^3.0.1",
28
29
  "cbor-x": "^1.3.0",
29
30
  "debug": "^4.3.4",
30
31
  "eventemitter3": "^5.0.1",
31
32
  "fast-sha256": "^1.3.0",
32
- "tiny-typed-emitter": "^2.1.0",
33
- "ts-node": "^10.9.1",
34
33
  "uuid": "^9.0.0",
35
34
  "xstate": "^5.9.1"
36
35
  },
@@ -60,5 +59,5 @@
60
59
  "publishConfig": {
61
60
  "access": "public"
62
61
  },
63
- "gitHead": "0a639a7d57bcee4e0a87890e6592b9582c2a5d6f"
62
+ "gitHead": "6e8d13f3aebcbc31ff3dd478be85191eee1ad120"
64
63
  }
@@ -4,26 +4,54 @@ import type {
4
4
  BinaryDocumentId,
5
5
  DocumentId,
6
6
  AnyDocumentId,
7
+ UrlHeads,
7
8
  } from "./types.js"
9
+
8
10
  import * as Uuid from "uuid"
9
11
  import bs58check from "bs58check"
12
+ import {
13
+ uint8ArrayFromHexString,
14
+ uint8ArrayToHexString,
15
+ } from "./helpers/bufferFromHex.js"
16
+
17
+ import type { Heads as AutomergeHeads } from "@automerge/automerge/slim"
10
18
 
11
19
  export const urlPrefix = "automerge:"
12
20
 
21
+ interface ParsedAutomergeUrl {
22
+ /** unencoded DocumentId */
23
+ binaryDocumentId: BinaryDocumentId
24
+ /** bs58 encoded DocumentId */
25
+ documentId: DocumentId
26
+ /** Optional array of heads, if specified in URL */
27
+ heads?: UrlHeads
28
+ /** Optional hex array of heads, in Automerge core format */
29
+ hexHeads?: string[] // AKA: heads
30
+ }
31
+
13
32
  /** Given an Automerge URL, returns the DocumentId in both base58check-encoded form and binary form */
14
- export const parseAutomergeUrl = (url: AutomergeUrl) => {
33
+ export const parseAutomergeUrl = (url: AutomergeUrl): ParsedAutomergeUrl => {
34
+ const [baseUrl, headsSection, ...rest] = url.split("#")
35
+ if (rest.length > 0) {
36
+ throw new Error("Invalid URL: contains multiple heads sections")
37
+ }
15
38
  const regex = new RegExp(`^${urlPrefix}(\\w+)$`)
16
- const [, docMatch] = url.match(regex) || []
39
+ const [, docMatch] = baseUrl.match(regex) || []
17
40
  const documentId = docMatch as DocumentId
18
41
  const binaryDocumentId = documentIdToBinary(documentId)
19
42
 
20
43
  if (!binaryDocumentId) throw new Error("Invalid document URL: " + url)
21
- return {
22
- /** unencoded DocumentId */
23
- binaryDocumentId,
24
- /** encoded DocumentId */
25
- documentId,
26
- }
44
+ if (headsSection === undefined) return { binaryDocumentId, documentId }
45
+
46
+ const heads = (headsSection === "" ? [] : headsSection.split("|")) as UrlHeads
47
+ const hexHeads = heads.map(head => {
48
+ try {
49
+ return uint8ArrayToHexString(bs58check.decode(head))
50
+ } catch (e) {
51
+ throw new Error(`Invalid head in URL: ${head}`)
52
+ }
53
+ })
54
+ return { binaryDocumentId, hexHeads, documentId, heads }
27
55
  }
28
56
 
29
57
  /**
@@ -32,38 +60,78 @@ export const parseAutomergeUrl = (url: AutomergeUrl) => {
32
60
  */
33
61
  export const stringifyAutomergeUrl = (
34
62
  arg: UrlOptions | DocumentId | BinaryDocumentId
35
- ) => {
36
- const documentId =
37
- arg instanceof Uint8Array || typeof arg === "string"
38
- ? arg
39
- : "documentId" in arg
40
- ? arg.documentId
41
- : undefined
63
+ ): AutomergeUrl => {
64
+ if (arg instanceof Uint8Array || typeof arg === "string") {
65
+ return (urlPrefix +
66
+ (arg instanceof Uint8Array
67
+ ? binaryToDocumentId(arg)
68
+ : arg)) as AutomergeUrl
69
+ }
70
+
71
+ const { documentId, heads = undefined } = arg
72
+
73
+ if (documentId === undefined)
74
+ throw new Error("Invalid documentId: " + documentId)
42
75
 
43
76
  const encodedDocumentId =
44
77
  documentId instanceof Uint8Array
45
78
  ? binaryToDocumentId(documentId)
46
- : typeof documentId === "string"
47
- ? documentId
48
- : undefined
79
+ : documentId
80
+
81
+ let url = `${urlPrefix}${encodedDocumentId}`
82
+
83
+ if (heads !== undefined) {
84
+ heads.forEach(head => {
85
+ try {
86
+ bs58check.decode(head)
87
+ } catch (e) {
88
+ throw new Error(`Invalid head: ${head}`)
89
+ }
90
+ })
91
+ url += "#" + heads.join("|")
92
+ }
49
93
 
50
- if (encodedDocumentId === undefined)
51
- throw new Error("Invalid documentId: " + documentId)
94
+ return url as AutomergeUrl
95
+ }
52
96
 
53
- return (urlPrefix + encodedDocumentId) as AutomergeUrl
97
+ /** Helper to extract just the heads from a URL if they exist */
98
+ export const getHeadsFromUrl = (url: AutomergeUrl): string[] | undefined => {
99
+ const { heads } = parseAutomergeUrl(url)
100
+ return heads
54
101
  }
55
102
 
103
+ export const anyDocumentIdToAutomergeUrl = (id: AnyDocumentId) =>
104
+ isValidAutomergeUrl(id)
105
+ ? id
106
+ : isValidDocumentId(id)
107
+ ? stringifyAutomergeUrl({ documentId: id })
108
+ : isValidUuid(id)
109
+ ? parseLegacyUUID(id)
110
+ : undefined
111
+
56
112
  /**
57
113
  * Given a string, returns true if it is a valid Automerge URL. This function also acts as a type
58
114
  * discriminator in Typescript.
59
115
  */
60
116
  export const isValidAutomergeUrl = (str: unknown): str is AutomergeUrl => {
61
- if (typeof str !== "string") return false
62
- if (!str || !str.startsWith(urlPrefix)) return false
63
- const automergeUrl = str as AutomergeUrl
117
+ if (typeof str !== "string" || !str || !str.startsWith(urlPrefix))
118
+ return false
64
119
  try {
65
- const { documentId } = parseAutomergeUrl(automergeUrl)
66
- return isValidDocumentId(documentId)
120
+ const { documentId, heads } = parseAutomergeUrl(str as AutomergeUrl)
121
+ if (!isValidDocumentId(documentId)) return false
122
+ if (
123
+ heads &&
124
+ !heads.every(head => {
125
+ try {
126
+ bs58check.decode(head)
127
+ return true
128
+ } catch {
129
+ return false
130
+ }
131
+ })
132
+ )
133
+ return false
134
+ return true
67
135
  } catch {
68
136
  return false
69
137
  }
@@ -97,6 +165,12 @@ export const documentIdToBinary = (docId: DocumentId) =>
97
165
  export const binaryToDocumentId = (docId: BinaryDocumentId) =>
98
166
  bs58check.encode(docId) as DocumentId
99
167
 
168
+ export const encodeHeads = (heads: AutomergeHeads): UrlHeads =>
169
+ heads.map(h => bs58check.encode(uint8ArrayFromHexString(h))) as UrlHeads
170
+
171
+ export const decodeHeads = (heads: UrlHeads): AutomergeHeads =>
172
+ heads.map(h => uint8ArrayToHexString(bs58check.decode(h))) as AutomergeHeads
173
+
100
174
  export const parseLegacyUUID = (str: string) => {
101
175
  if (!Uuid.validate(str)) return undefined
102
176
  const documentId = Uuid.parse(str) as BinaryDocumentId
@@ -141,4 +215,5 @@ export const interpretAsDocumentId = (id: AnyDocumentId) => {
141
215
 
142
216
  type UrlOptions = {
143
217
  documentId: DocumentId | BinaryDocumentId
218
+ heads?: UrlHeads
144
219
  }