@automerge/automerge-repo 2.0.0-alpha.7 → 2.0.0-collectionsync-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/dist/CollectionHandle.d.ts +14 -0
  2. package/dist/CollectionHandle.d.ts.map +1 -0
  3. package/dist/CollectionHandle.js +37 -0
  4. package/dist/DocHandle.d.ts +37 -6
  5. package/dist/DocHandle.d.ts.map +1 -1
  6. package/dist/DocHandle.js +64 -6
  7. package/dist/DocUrl.d.ts +47 -0
  8. package/dist/DocUrl.d.ts.map +1 -0
  9. package/dist/DocUrl.js +72 -0
  10. package/dist/EphemeralData.d.ts +20 -0
  11. package/dist/EphemeralData.d.ts.map +1 -0
  12. package/dist/EphemeralData.js +1 -0
  13. package/dist/Repo.d.ts +28 -7
  14. package/dist/Repo.d.ts.map +1 -1
  15. package/dist/Repo.js +142 -143
  16. package/dist/ferigan.d.ts +51 -0
  17. package/dist/ferigan.d.ts.map +1 -0
  18. package/dist/ferigan.js +98 -0
  19. package/dist/helpers/tests/storage-adapter-tests.d.ts +2 -2
  20. package/dist/helpers/tests/storage-adapter-tests.d.ts.map +1 -1
  21. package/dist/helpers/tests/storage-adapter-tests.js +19 -39
  22. package/dist/index.d.ts +2 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +1 -0
  25. package/dist/network/NetworkSubsystem.d.ts +1 -0
  26. package/dist/network/NetworkSubsystem.d.ts.map +1 -1
  27. package/dist/network/NetworkSubsystem.js +3 -0
  28. package/dist/network/messages.d.ts +7 -1
  29. package/dist/network/messages.d.ts.map +1 -1
  30. package/dist/network/messages.js +2 -1
  31. package/dist/src/DocHandle.d.ts +182 -0
  32. package/dist/src/DocHandle.d.ts.map +1 -0
  33. package/dist/src/DocHandle.js +405 -0
  34. package/dist/src/DocUrl.d.ts +49 -0
  35. package/dist/src/DocUrl.d.ts.map +1 -0
  36. package/dist/src/DocUrl.js +72 -0
  37. package/dist/src/EphemeralData.d.ts +19 -0
  38. package/dist/src/EphemeralData.d.ts.map +1 -0
  39. package/dist/src/EphemeralData.js +1 -0
  40. package/dist/src/Repo.d.ts +74 -0
  41. package/dist/src/Repo.d.ts.map +1 -0
  42. package/dist/src/Repo.js +208 -0
  43. package/dist/src/helpers/arraysAreEqual.d.ts +2 -0
  44. package/dist/src/helpers/arraysAreEqual.d.ts.map +1 -0
  45. package/dist/src/helpers/arraysAreEqual.js +2 -0
  46. package/dist/src/helpers/cbor.d.ts +4 -0
  47. package/dist/src/helpers/cbor.d.ts.map +1 -0
  48. package/dist/src/helpers/cbor.js +8 -0
  49. package/dist/src/helpers/eventPromise.d.ts +11 -0
  50. package/dist/src/helpers/eventPromise.d.ts.map +1 -0
  51. package/dist/src/helpers/eventPromise.js +7 -0
  52. package/dist/src/helpers/headsAreSame.d.ts +2 -0
  53. package/dist/src/helpers/headsAreSame.d.ts.map +1 -0
  54. package/dist/src/helpers/headsAreSame.js +4 -0
  55. package/dist/src/helpers/mergeArrays.d.ts +2 -0
  56. package/dist/src/helpers/mergeArrays.d.ts.map +1 -0
  57. package/dist/src/helpers/mergeArrays.js +15 -0
  58. package/dist/src/helpers/pause.d.ts +6 -0
  59. package/dist/src/helpers/pause.d.ts.map +1 -0
  60. package/dist/src/helpers/pause.js +10 -0
  61. package/dist/src/helpers/tests/network-adapter-tests.d.ts +21 -0
  62. package/dist/src/helpers/tests/network-adapter-tests.d.ts.map +1 -0
  63. package/dist/src/helpers/tests/network-adapter-tests.js +122 -0
  64. package/dist/src/helpers/withTimeout.d.ts +12 -0
  65. package/dist/src/helpers/withTimeout.d.ts.map +1 -0
  66. package/dist/src/helpers/withTimeout.js +24 -0
  67. package/dist/src/index.d.ts +53 -0
  68. package/dist/src/index.d.ts.map +1 -0
  69. package/dist/src/index.js +40 -0
  70. package/dist/src/network/NetworkAdapter.d.ts +26 -0
  71. package/dist/src/network/NetworkAdapter.d.ts.map +1 -0
  72. package/dist/src/network/NetworkAdapter.js +4 -0
  73. package/dist/src/network/NetworkSubsystem.d.ts +23 -0
  74. package/dist/src/network/NetworkSubsystem.d.ts.map +1 -0
  75. package/dist/src/network/NetworkSubsystem.js +120 -0
  76. package/dist/src/network/messages.d.ts +85 -0
  77. package/dist/src/network/messages.d.ts.map +1 -0
  78. package/dist/src/network/messages.js +23 -0
  79. package/dist/src/storage/StorageAdapter.d.ts +14 -0
  80. package/dist/src/storage/StorageAdapter.d.ts.map +1 -0
  81. package/dist/src/storage/StorageAdapter.js +1 -0
  82. package/dist/src/storage/StorageSubsystem.d.ts +12 -0
  83. package/dist/src/storage/StorageSubsystem.d.ts.map +1 -0
  84. package/dist/src/storage/StorageSubsystem.js +145 -0
  85. package/dist/src/synchronizer/CollectionSynchronizer.d.ts +25 -0
  86. package/dist/src/synchronizer/CollectionSynchronizer.d.ts.map +1 -0
  87. package/dist/src/synchronizer/CollectionSynchronizer.js +106 -0
  88. package/dist/src/synchronizer/DocSynchronizer.d.ts +29 -0
  89. package/dist/src/synchronizer/DocSynchronizer.d.ts.map +1 -0
  90. package/dist/src/synchronizer/DocSynchronizer.js +263 -0
  91. package/dist/src/synchronizer/Synchronizer.d.ts +9 -0
  92. package/dist/src/synchronizer/Synchronizer.d.ts.map +1 -0
  93. package/dist/src/synchronizer/Synchronizer.js +2 -0
  94. package/dist/src/types.d.ts +16 -0
  95. package/dist/src/types.d.ts.map +1 -0
  96. package/dist/src/types.js +1 -0
  97. package/dist/storage/StorageAdapter.d.ts +9 -0
  98. package/dist/storage/StorageAdapter.d.ts.map +1 -1
  99. package/dist/storage/StorageAdapter.js +33 -0
  100. package/dist/storage/StorageSubsystem.d.ts +12 -2
  101. package/dist/storage/StorageSubsystem.d.ts.map +1 -1
  102. package/dist/storage/StorageSubsystem.js +42 -100
  103. package/dist/synchronizer/CollectionSynchronizer.d.ts +4 -2
  104. package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
  105. package/dist/synchronizer/CollectionSynchronizer.js +28 -15
  106. package/dist/synchronizer/DocSynchronizer.d.ts +6 -5
  107. package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
  108. package/dist/synchronizer/DocSynchronizer.js +76 -178
  109. package/dist/synchronizer/Synchronizer.d.ts +11 -0
  110. package/dist/synchronizer/Synchronizer.d.ts.map +1 -1
  111. package/dist/test/CollectionSynchronizer.test.d.ts +2 -0
  112. package/dist/test/CollectionSynchronizer.test.d.ts.map +1 -0
  113. package/dist/test/CollectionSynchronizer.test.js +57 -0
  114. package/dist/test/DocHandle.test.d.ts +2 -0
  115. package/dist/test/DocHandle.test.d.ts.map +1 -0
  116. package/dist/test/DocHandle.test.js +238 -0
  117. package/dist/test/DocSynchronizer.test.d.ts +2 -0
  118. package/dist/test/DocSynchronizer.test.d.ts.map +1 -0
  119. package/dist/test/DocSynchronizer.test.js +111 -0
  120. package/dist/test/Network.test.d.ts +2 -0
  121. package/dist/test/Network.test.d.ts.map +1 -0
  122. package/dist/test/Network.test.js +11 -0
  123. package/dist/test/Repo.test.d.ts +2 -0
  124. package/dist/test/Repo.test.d.ts.map +1 -0
  125. package/dist/test/Repo.test.js +568 -0
  126. package/dist/test/StorageSubsystem.test.d.ts +2 -0
  127. package/dist/test/StorageSubsystem.test.d.ts.map +1 -0
  128. package/dist/test/StorageSubsystem.test.js +56 -0
  129. package/dist/test/helpers/DummyNetworkAdapter.d.ts +9 -0
  130. package/dist/test/helpers/DummyNetworkAdapter.d.ts.map +1 -0
  131. package/dist/test/helpers/DummyNetworkAdapter.js +15 -0
  132. package/dist/test/helpers/DummyStorageAdapter.d.ts +16 -0
  133. package/dist/test/helpers/DummyStorageAdapter.d.ts.map +1 -0
  134. package/dist/test/helpers/DummyStorageAdapter.js +33 -0
  135. package/dist/test/helpers/generate-large-object.d.ts +5 -0
  136. package/dist/test/helpers/generate-large-object.d.ts.map +1 -0
  137. package/dist/test/helpers/generate-large-object.js +9 -0
  138. package/dist/test/helpers/getRandomItem.d.ts +2 -0
  139. package/dist/test/helpers/getRandomItem.d.ts.map +1 -0
  140. package/dist/test/helpers/getRandomItem.js +4 -0
  141. package/dist/test/types.d.ts +4 -0
  142. package/dist/test/types.d.ts.map +1 -0
  143. package/dist/test/types.js +1 -0
  144. package/package.json +3 -3
  145. package/src/CollectionHandle.ts +54 -0
  146. package/src/DocHandle.ts +80 -8
  147. package/src/Repo.ts +192 -183
  148. package/src/ferigan.ts +184 -0
  149. package/src/helpers/tests/storage-adapter-tests.ts +31 -62
  150. package/src/index.ts +2 -0
  151. package/src/network/NetworkSubsystem.ts +4 -0
  152. package/src/network/messages.ts +11 -2
  153. package/src/storage/StorageAdapter.ts +42 -0
  154. package/src/storage/StorageSubsystem.ts +59 -119
  155. package/src/synchronizer/CollectionSynchronizer.ts +34 -26
  156. package/src/synchronizer/DocSynchronizer.ts +84 -231
  157. package/src/synchronizer/Synchronizer.ts +14 -0
  158. package/test/CollectionSynchronizer.test.ts +4 -2
  159. package/test/DocHandle.test.ts +72 -13
  160. package/test/DocSynchronizer.test.ts +6 -1
  161. package/test/RemoteHeadsSubscriptions.test.ts +1 -1
  162. package/test/Repo.test.ts +225 -117
  163. package/test/StorageSubsystem.test.ts +20 -16
  164. package/test/remoteHeads.test.ts +1 -1
package/dist/Repo.js CHANGED
@@ -2,13 +2,12 @@ import { next as Automerge } from "@automerge/automerge/slim";
2
2
  import debug from "debug";
3
3
  import { EventEmitter } from "eventemitter3";
4
4
  import { generateAutomergeUrl, interpretAsDocumentId, parseAutomergeUrl, } from "./AutomergeUrl.js";
5
- import { DocHandle } from "./DocHandle.js";
6
- import { RemoteHeadsSubscriptions } from "./RemoteHeadsSubscriptions.js";
7
- import { headsAreSame } from "./helpers/headsAreSame.js";
8
- import { throttle } from "./helpers/throttle.js";
5
+ import { DELETED, DocHandle, READY, UNAVAILABLE, UNLOADED, } from "./DocHandle.js";
9
6
  import { NetworkSubsystem } from "./network/NetworkSubsystem.js";
10
7
  import { StorageSubsystem } from "./storage/StorageSubsystem.js";
11
8
  import { CollectionSynchronizer } from "./synchronizer/CollectionSynchronizer.js";
9
+ import { next as A } from "@automerge/automerge/slim";
10
+ import { InMemoryStorageAdapter } from "./storage/StorageAdapter.js";
12
11
  function randomPeerId() {
13
12
  return ("peer-" + Math.random().toString(36).slice(4));
14
13
  }
@@ -24,11 +23,7 @@ export class Repo extends EventEmitter {
24
23
  #log;
25
24
  /** @hidden */
26
25
  networkSubsystem;
27
- /** @hidden */
28
26
  storageSubsystem;
29
- /** The debounce rate is adjustable on the repo. */
30
- /** @hidden */
31
- saveDebounceRate = 100;
32
27
  #handleCache = {};
33
28
  /** @hidden */
34
29
  synchronizer;
@@ -38,43 +33,73 @@ export class Repo extends EventEmitter {
38
33
  /** maps peer id to to persistence information (storageId, isEphemeral), access by collection synchronizer */
39
34
  /** @hidden */
40
35
  peerMetadataByPeerId = {};
41
- #remoteHeadsSubscriptions = new RemoteHeadsSubscriptions();
42
- #remoteHeadsGossipingEnabled = false;
43
- constructor({ storage, network = [], peerId = randomPeerId(), sharePolicy, isEphemeral = storage === undefined, enableRemoteHeadsGossiping = false, } = {}) {
36
+ #beelay;
37
+ constructor({ storage, network = [], peerId = randomPeerId(), sharePolicy, isEphemeral = storage === undefined, enableRemoteHeadsGossiping = false, denylist = [], } = {}) {
44
38
  super();
45
- this.#remoteHeadsGossipingEnabled = enableRemoteHeadsGossiping;
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);
46
58
  this.#log = debug(`automerge-repo:repo`);
47
59
  this.sharePolicy = sharePolicy ?? this.sharePolicy;
48
- this.on("delete-document", ({ documentId }) => {
49
- // TODO Pass the delete on to the network
50
- // synchronizer.removeDocument(documentId)
51
- if (storageSubsystem) {
52
- storageSubsystem.removeDoc(documentId).catch(err => {
53
- this.#log("error deleting document", { documentId, err });
54
- });
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));
55
73
  }
56
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,
90
+ });
91
+ })();
92
+ });
57
93
  // SYNCHRONIZER
58
- // The synchronizer uses the network subsystem to keep documents in sync with peers.
59
- this.synchronizer = new CollectionSynchronizer(this);
60
- // When the synchronizer emits messages, send them to peers
94
+ this.synchronizer = new CollectionSynchronizer(this.#beelay, this, []);
61
95
  this.synchronizer.on("message", message => {
62
96
  this.#log(`sending ${message.type} message to ${message.targetId}`);
63
97
  networkSubsystem.send(message);
64
98
  });
65
- if (this.#remoteHeadsGossipingEnabled) {
66
- this.synchronizer.on("open-doc", ({ peerId, documentId }) => {
67
- this.#remoteHeadsSubscriptions.subscribePeerToDoc(peerId, documentId);
68
- });
69
- }
70
- // STORAGE
71
- // The storage subsystem has access to some form of persistence, and deals with save and loading documents.
72
- const storageSubsystem = storage ? new StorageSubsystem(storage) : undefined;
73
- this.storageSubsystem = storageSubsystem;
74
99
  // NETWORK
75
100
  // The network subsystem deals with sending and receiving messages to and from peers.
76
101
  const myPeerMetadata = (async () => ({
77
- storageId: await storageSubsystem?.id(),
102
+ // storageId: await this.storageSubsystem.id(),
78
103
  isEphemeral,
79
104
  }))();
80
105
  const networkSubsystem = new NetworkSubsystem(network, peerId, myPeerMetadata);
@@ -85,92 +110,50 @@ export class Repo extends EventEmitter {
85
110
  if (peerMetadata) {
86
111
  this.peerMetadataByPeerId[peerId] = { ...peerMetadata };
87
112
  }
88
- this.sharePolicy(peerId)
89
- .then(shouldShare => {
90
- if (shouldShare && this.#remoteHeadsGossipingEnabled) {
91
- this.#remoteHeadsSubscriptions.addGenerousPeer(peerId);
92
- }
93
- })
94
- .catch(err => {
95
- console.log("error in share policy", { err });
96
- });
97
113
  this.synchronizer.addPeer(peerId);
98
114
  });
99
- // When a peer disconnects, remove it from the synchronizer
100
- networkSubsystem.on("peer-disconnected", ({ peerId }) => {
101
- this.synchronizer.removePeer(peerId);
102
- this.#remoteHeadsSubscriptions.removePeer(peerId);
103
- });
104
115
  // Handle incoming messages
105
116
  networkSubsystem.on("message", async (msg) => {
106
- this.#receiveMessage(msg);
107
- });
108
- this.synchronizer.on("sync-state", message => {
109
- this.#saveSyncState(message);
110
- const handle = this.#handleCache[message.documentId];
111
- const { storageId } = this.peerMetadataByPeerId[message.peerId] || {};
112
- if (!storageId) {
113
- return;
114
- }
115
- const heads = handle.getRemoteHeads(storageId);
116
- const haveHeadsChanged = message.syncState.theirHeads &&
117
- (!heads || !headsAreSame(heads, message.syncState.theirHeads));
118
- if (haveHeadsChanged && message.syncState.theirHeads) {
119
- handle.setRemoteHeads(storageId, message.syncState.theirHeads);
120
- if (storageId && this.#remoteHeadsGossipingEnabled) {
121
- this.#remoteHeadsSubscriptions.handleImmediateRemoteHeadsChanged(message.documentId, storageId, message.syncState.theirHeads);
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);
122
128
  }
123
- }
124
- });
125
- if (this.#remoteHeadsGossipingEnabled) {
126
- this.#remoteHeadsSubscriptions.on("notify-remote-heads", message => {
127
- this.networkSubsystem.send({
128
- type: "remote-heads-changed",
129
- targetId: message.targetId,
130
- documentId: message.documentId,
131
- newHeads: {
132
- [message.storageId]: {
133
- heads: message.heads,
134
- timestamp: message.timestamp,
135
- },
129
+ this.#beelay.receiveMessage({
130
+ message: {
131
+ sender: msg.senderId,
132
+ recipient: msg.targetId,
133
+ message: msg.message,
136
134
  },
137
135
  });
138
- });
139
- this.#remoteHeadsSubscriptions.on("change-remote-subs", message => {
140
- this.#log("change-remote-subs", message);
141
- for (const peer of message.peers) {
142
- this.networkSubsystem.send({
143
- type: "remote-subscription-change",
144
- targetId: peer,
145
- add: message.add,
146
- remove: message.remove,
147
- });
148
- }
149
- });
150
- this.#remoteHeadsSubscriptions.on("remote-heads-changed", message => {
151
- const handle = this.#handleCache[message.documentId];
152
- handle.setRemoteHeads(message.storageId, message.remoteHeads);
153
- });
154
- }
136
+ }
137
+ else {
138
+ this.#receiveMessage(msg);
139
+ }
140
+ });
155
141
  }
156
142
  // The `document` event is fired by the DocCollection any time we create a new document or look
157
143
  // up a document by ID. We listen for it in order to wire up storage and network synchronization.
158
144
  #registerHandleWithSubsystems(handle) {
159
- const { storageSubsystem } = this;
160
- if (storageSubsystem) {
161
- // Save when the document changes, but no more often than saveDebounceRate.
162
- const saveFn = ({ handle, doc }) => {
163
- void storageSubsystem.saveDoc(handle.documentId, doc);
164
- };
165
- handle.on("heads-changed", throttle(saveFn, this.saveDebounceRate));
166
- }
145
+ handle.on("heads-changed", () => {
146
+ const doc = handle.docSync();
147
+ if (doc != null) {
148
+ this.storageSubsystem.saveDoc(handle.documentId, doc);
149
+ }
150
+ });
167
151
  handle.on("unavailable", () => {
168
152
  this.#log("document unavailable", { documentId: handle.documentId });
169
153
  this.emit("unavailable-document", {
170
154
  documentId: handle.documentId,
171
155
  });
172
156
  });
173
- // Register the document with the synchronizer. This advertises our interest in the document.
174
157
  this.synchronizer.addDocument(handle.documentId);
175
158
  // Preserve the old event in case anyone was using it.
176
159
  this.emit("document", { handle });
@@ -178,42 +161,18 @@ export class Repo extends EventEmitter {
178
161
  #receiveMessage(message) {
179
162
  switch (message.type) {
180
163
  case "remote-subscription-change":
181
- if (this.#remoteHeadsGossipingEnabled) {
182
- this.#remoteHeadsSubscriptions.handleControlMessage(message);
183
- }
184
- break;
185
164
  case "remote-heads-changed":
186
- if (this.#remoteHeadsGossipingEnabled) {
187
- this.#remoteHeadsSubscriptions.handleRemoteHeads(message);
188
- }
189
165
  break;
190
166
  case "sync":
191
167
  case "request":
192
168
  case "ephemeral":
193
169
  case "doc-unavailable":
194
170
  this.synchronizer.receiveMessage(message).catch(err => {
195
- console.log("error receiving message", { err });
171
+ console.error("error receiving message", { err });
196
172
  });
173
+ break;
197
174
  }
198
175
  }
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
- }
217
176
  /** Returns an existing handle if we have it; creates one otherwise. */
218
177
  #getHandle({ documentId, }) {
219
178
  // If we have the handle cached, return it
@@ -232,7 +191,7 @@ export class Repo extends EventEmitter {
232
191
  }
233
192
  /** Returns a list of all connected peer ids */
234
193
  get peers() {
235
- return this.synchronizer.peers;
194
+ return this.networkSubsystem.peers;
236
195
  }
237
196
  getStorageIdOfPeer(peerId) {
238
197
  return this.peerMetadataByPeerId[peerId]?.storageId;
@@ -248,7 +207,7 @@ export class Repo extends EventEmitter {
248
207
  const handle = this.#getHandle({
249
208
  documentId,
250
209
  });
251
- this.#registerHandleWithSubsystems(handle);
210
+ let initialLinks = [];
252
211
  handle.update(() => {
253
212
  let nextDoc;
254
213
  if (initialValue) {
@@ -257,8 +216,27 @@ export class Repo extends EventEmitter {
257
216
  else {
258
217
  nextDoc = Automerge.emptyChange(Automerge.init());
259
218
  }
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
+ }
260
232
  return nextDoc;
261
233
  });
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);
262
240
  handle.doneLoading();
263
241
  return handle;
264
242
  }
@@ -280,7 +258,7 @@ export class Repo extends EventEmitter {
280
258
  clone(clonedHandle) {
281
259
  if (!clonedHandle.isReady()) {
282
260
  throw new Error(`Cloned handle is not yet in ready state.
283
- (Try await handle.waitForReady() first.)`);
261
+ (Try await handle.whenReady() first.)`);
284
262
  }
285
263
  const sourceDoc = clonedHandle.docSync();
286
264
  if (!sourceDoc) {
@@ -300,6 +278,7 @@ export class Repo extends EventEmitter {
300
278
  find(
301
279
  /** The url or documentId of the handle to retrieve */
302
280
  id) {
281
+ this.#log("find", { id });
303
282
  const documentId = interpretAsDocumentId(id);
304
283
  // If we have the handle cached, return it
305
284
  if (this.#handleCache[documentId]) {
@@ -319,9 +298,7 @@ export class Repo extends EventEmitter {
319
298
  });
320
299
  // Loading & network is going to be asynchronous no matter what,
321
300
  // but we want to return the handle immediately.
322
- const attemptLoad = this.storageSubsystem
323
- ? this.storageSubsystem.loadDoc(handle.documentId)
324
- : Promise.resolve(null);
301
+ const attemptLoad = this.storageSubsystem.loadDoc(handle.documentId);
325
302
  attemptLoad
326
303
  .then(async (loadedDoc) => {
327
304
  if (loadedDoc) {
@@ -333,6 +310,7 @@ export class Repo extends EventEmitter {
333
310
  // we want to wait for the network subsystem to be ready before
334
311
  // we request the document. this prevents entering unavailable during initialization.
335
312
  await this.networkSubsystem.whenReady();
313
+ console.log("we didn't find it so we're requesting");
336
314
  handle.request();
337
315
  }
338
316
  this.#registerHandleWithSubsystems(handle);
@@ -378,15 +356,7 @@ export class Repo extends EventEmitter {
378
356
  });
379
357
  return handle;
380
358
  }
381
- subscribeToRemotes = (remotes) => {
382
- if (this.#remoteHeadsGossipingEnabled) {
383
- this.#log("subscribeToRemotes", { remotes });
384
- this.#remoteHeadsSubscriptions.subscribeToRemotes(remotes);
385
- }
386
- else {
387
- this.#log("WARN: subscribeToRemotes called but remote heads gossiping is not enabled");
388
- }
389
- };
359
+ subscribeToRemotes = (remotes) => { };
390
360
  storageId = async () => {
391
361
  if (!this.storageSubsystem) {
392
362
  return undefined;
@@ -416,6 +386,34 @@ export class Repo extends EventEmitter {
416
386
  return this.storageSubsystem.saveDoc(handle.documentId, doc);
417
387
  }));
418
388
  }
389
+ /**
390
+ * Removes a DocHandle from the handleCache.
391
+ * @hidden this API is experimental and may change.
392
+ * @param documentId - documentId of the DocHandle to remove from handleCache, if present in cache.
393
+ * @returns Promise<void>
394
+ */
395
+ async removeFromCache(documentId) {
396
+ if (!this.#handleCache[documentId]) {
397
+ this.#log(`WARN: removeFromCache called but handle not found in handleCache for documentId: ${documentId}`);
398
+ return;
399
+ }
400
+ const handle = this.#getHandle({ documentId });
401
+ const doc = await handle.doc([READY, UNLOADED, DELETED, UNAVAILABLE]);
402
+ if (doc) {
403
+ if (handle.isReady()) {
404
+ handle.unload();
405
+ }
406
+ else {
407
+ this.#log(`WARN: removeFromCache called but handle for documentId: ${documentId} in unexpected state: ${handle.state}`);
408
+ }
409
+ delete this.#handleCache[documentId];
410
+ // TODO: remove document from synchronizer when removeDocument is implemented
411
+ // this.synchronizer.removeDocument(documentId)
412
+ }
413
+ else {
414
+ this.#log(`WARN: removeFromCache called but doc undefined for documentId: ${documentId}`);
415
+ }
416
+ }
419
417
  shutdown() {
420
418
  this.networkSubsystem.adapters.forEach(adapter => {
421
419
  adapter.disconnect();
@@ -423,6 +421,7 @@ export class Repo extends EventEmitter {
423
421
  return this.flush();
424
422
  }
425
423
  metrics() {
426
- return { documents: this.synchronizer.metrics() };
424
+ //return { documents: this.synchronizer.metrics() }
425
+ return { documents: {} };
427
426
  }
428
427
  }
@@ -0,0 +1,51 @@
1
+ import { EventEmitter } from "eventemitter3";
2
+ import { MessageContents, RepoMessage } from "./network/messages.js";
3
+ import { AutomergeUrl } from "./types.js";
4
+ import { Repo } from "./Repo.js";
5
+ export interface Ferigan extends EventEmitter<FeriganEvents> {
6
+ receiveMessage(message: RepoMessage): Promise<void>;
7
+ load(doc: ChangeLogId, since: ChangeHash[]): AsyncIterableIterator<Progress<ChangeLog | undefined>>;
8
+ loadCollection(doc: ChangeLogId): AsyncIterableIterator<Progress<Index | undefined>>;
9
+ append(doc: ChangeLogId, parents: ChangeHash[], changes: Uint8Array): Promise<void>;
10
+ replace(doc: ChangeLogId, start: ChangeHash, end: ChangeHash, changes: Uint8Array): Promise<void>;
11
+ }
12
+ export declare function makeFerigan(repo: Repo): Ferigan;
13
+ interface FeriganEvents {
14
+ message: (event: {
15
+ message: MessageContents;
16
+ }) => void;
17
+ changed: (event: {
18
+ changedLog: ChangeLogId;
19
+ }) => void;
20
+ indexChanged: (event: {
21
+ indexUrl: ChangeLogId;
22
+ change: IndexChange;
23
+ }) => void;
24
+ }
25
+ type IndexChange = {
26
+ type: "add";
27
+ url: AutomergeUrl;
28
+ };
29
+ type ChangeLogId = string;
30
+ type ChangeHash = string;
31
+ type ChangeLog = {
32
+ start: ChangeHash;
33
+ end: ChangeHash;
34
+ changes: Uint8Array;
35
+ };
36
+ export type Progress<T> = {
37
+ type: "synchronizing_index";
38
+ } | {
39
+ type: "synchronizing_docs";
40
+ progress: number;
41
+ total: number;
42
+ } | {
43
+ type: "done";
44
+ value: T;
45
+ };
46
+ export type Index = {
47
+ rootUrl: AutomergeUrl;
48
+ entries: AutomergeUrl[];
49
+ };
50
+ export {};
51
+ //# sourceMappingURL=ferigan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ferigan.d.ts","sourceRoot":"","sources":["../src/ferigan.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC5C,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AACpE,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AACzC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAIhC,MAAM,WAAW,OAAQ,SAAQ,YAAY,CAAC,aAAa,CAAC;IAC1D,cAAc,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACnD,IAAI,CACF,GAAG,EAAE,WAAW,EAChB,KAAK,EAAE,UAAU,EAAE,GAClB,qBAAqB,CAAC,QAAQ,CAAC,SAAS,GAAG,SAAS,CAAC,CAAC,CAAA;IACzD,cAAc,CACZ,GAAG,EAAE,WAAW,GACf,qBAAqB,CAAC,QAAQ,CAAC,KAAK,GAAG,SAAS,CAAC,CAAC,CAAA;IACrD,MAAM,CACJ,GAAG,EAAE,WAAW,EAChB,OAAO,EAAE,UAAU,EAAE,EACrB,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC,IAAI,CAAC,CAAA;IAChB,OAAO,CACL,GAAG,EAAE,WAAW,EAChB,KAAK,EAAE,UAAU,EACjB,GAAG,EAAE,UAAU,EACf,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC,IAAI,CAAC,CAAA;CACjB;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CA+G/C;AAED,UAAU,aAAa;IACrB,OAAO,EAAE,CAAC,KAAK,EAAE;QAAC,OAAO,EAAE,eAAe,CAAA;KAAC,KAAK,IAAI,CAAA;IACpD,OAAO,EAAE,CAAC,KAAK,EAAE;QAAC,UAAU,EAAE,WAAW,CAAA;KAAC,KAAK,IAAI,CAAA;IACnD,YAAY,EAAE,CAAC,KAAK,EAAE;QAAC,QAAQ,EAAE,WAAW,CAAC;QAAC,MAAM,EAAE,WAAW,CAAA;KAAC,KAAK,IAAI,CAAA;CAC5E;AAED,KAAK,WAAW,GAAG;IACjB,IAAI,EAAE,KAAK,CAAA;IACX,GAAG,EAAE,YAAY,CAAA;CAClB,CAAA;AAED,KAAK,WAAW,GAAG,MAAM,CAAA;AAEzB,KAAK,UAAU,GAAG,MAAM,CAAA;AAExB,KAAK,SAAS,GAAG;IAAE,KAAK,EAAE,UAAU,CAAC;IAAC,GAAG,EAAE,UAAU,CAAC;IAAC,OAAO,EAAE,UAAU,CAAA;CAAE,CAAA;AAE5E,MAAM,MAAM,QAAQ,CAAC,CAAC,IAClB;IAAE,IAAI,EAAE,qBAAqB,CAAA;CAAE,GAC/B;IACA,IAAI,EAAE,oBAAoB,CAAA;IAC1B,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;CACd,GACC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,CAAA;AAE9B,MAAM,MAAM,KAAK,GAAG;IAClB,OAAO,EAAE,YAAY,CAAC;IACtB,OAAO,EAAE,YAAY,EAAE,CAAC;CACzB,CAAA"}
@@ -0,0 +1,98 @@
1
+ import { EventEmitter } from "eventemitter3";
2
+ const URL_RE = /automerge:([\w\d+/=]+)(?:\?([\w\d+/=&]*))*/;
3
+ export function makeFerigan(repo) {
4
+ function fakeProgress() {
5
+ return (async function* () {
6
+ yield { type: "synchronizing_index" };
7
+ })();
8
+ }
9
+ class FakeFerigan extends EventEmitter {
10
+ #repo;
11
+ constructor(repo) {
12
+ super();
13
+ this.#repo = repo;
14
+ }
15
+ async receiveMessage(message) { }
16
+ load(doc) {
17
+ return fakeProgress();
18
+ }
19
+ async *loadCollection(doc) {
20
+ yield { type: "synchronizing_index" };
21
+ const index = { rootUrl: doc, entries: [] };
22
+ function findLinks(obj) {
23
+ const links = [];
24
+ const traverse = (value) => {
25
+ if (typeof value === "string") {
26
+ const url = parseUrl(value);
27
+ if (url != null) {
28
+ links.push(url);
29
+ }
30
+ }
31
+ else if (Array.isArray(value)) {
32
+ value.forEach(traverse);
33
+ }
34
+ else if (typeof value === "object" && value !== null) {
35
+ Object.values(value).forEach(traverse);
36
+ }
37
+ };
38
+ traverse(obj);
39
+ return links;
40
+ }
41
+ const indexDoc = this.#repo.find(doc);
42
+ const handlesToProcess = [indexDoc];
43
+ while (handlesToProcess.length > 0) {
44
+ const handle = handlesToProcess.pop();
45
+ if (!handle) {
46
+ continue;
47
+ }
48
+ const doc = await handle.doc();
49
+ if (!doc) {
50
+ continue;
51
+ }
52
+ handle.on("change", change => {
53
+ for (const patch of change.patches) {
54
+ if (patch.action === "splice") {
55
+ const possibleUrl = parseUrl(patch.value);
56
+ if (possibleUrl != null) {
57
+ this.emit("indexChanged", {
58
+ indexUrl: indexDoc.url,
59
+ change: {
60
+ type: "add",
61
+ url: possibleUrl.url,
62
+ }
63
+ });
64
+ }
65
+ }
66
+ }
67
+ });
68
+ const links = findLinks(doc);
69
+ for (const { url: urlStr } of links) {
70
+ const url = urlStr;
71
+ if (index.entries.some(entry => entry === url)) {
72
+ continue;
73
+ }
74
+ index.entries.push(url);
75
+ const childHandle = this.#repo.find(url);
76
+ handlesToProcess.push(childHandle);
77
+ }
78
+ }
79
+ yield { type: "done", value: index };
80
+ }
81
+ append(doc, parents, changes) {
82
+ return Promise.resolve();
83
+ }
84
+ replace(doc, start, end, changes) {
85
+ return Promise.resolve();
86
+ }
87
+ }
88
+ return new FakeFerigan(repo);
89
+ }
90
+ function parseUrl(urlStr) {
91
+ const match = urlStr.match(URL_RE);
92
+ if (!match) {
93
+ return undefined;
94
+ }
95
+ const url = new URL(match[0]);
96
+ const normalisedUrl = `automerge:${url.pathname}`;
97
+ return { url: normalisedUrl };
98
+ }
@@ -1,7 +1,7 @@
1
1
  import type { StorageAdapterInterface } from "../../storage/StorageAdapterInterface.js";
2
- export declare function runStorageAdapterTests(_setup: SetupFn, title?: string): void;
2
+ export declare function runStorageAdapterTests(setup: SetupFn, title?: string): void;
3
3
  export type SetupFn = () => Promise<{
4
4
  adapter: StorageAdapterInterface;
5
- teardown?: () => void;
5
+ teardown?: () => void | Promise<void>;
6
6
  }>;
7
7
  //# sourceMappingURL=storage-adapter-tests.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"storage-adapter-tests.d.ts","sourceRoot":"","sources":["../../../src/helpers/tests/storage-adapter-tests.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,0CAA0C,CAAA;AAQvF,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CA+K5E;AAID,MAAM,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC;IAClC,OAAO,EAAE,uBAAuB,CAAA;IAChC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB,CAAC,CAAA"}
1
+ {"version":3,"file":"storage-adapter-tests.d.ts","sourceRoot":"","sources":["../../../src/helpers/tests/storage-adapter-tests.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,0CAA0C,CAAA;AAcvF,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CA0I3E;AAID,MAAM,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC;IAClC,OAAO,EAAE,uBAAuB,CAAA;IAChC,QAAQ,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACtC,CAAC,CAAA"}