@automerge/automerge-repo 2.0.0-alpha.6 → 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 +67 -2
  5. package/dist/DocHandle.d.ts.map +1 -1
  6. package/dist/DocHandle.js +113 -2
  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 +133 -4
  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 +141 -0
  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/src/Repo.ts CHANGED
@@ -6,8 +6,14 @@ import {
6
6
  interpretAsDocumentId,
7
7
  parseAutomergeUrl,
8
8
  } from "./AutomergeUrl.js"
9
- import { DocHandle, DocHandleEncodedChangePayload } from "./DocHandle.js"
10
- import { RemoteHeadsSubscriptions } from "./RemoteHeadsSubscriptions.js"
9
+ import {
10
+ DELETED,
11
+ DocHandle,
12
+ DocHandleEncodedChangePayload,
13
+ READY,
14
+ UNAVAILABLE,
15
+ UNLOADED,
16
+ } from "./DocHandle.js"
11
17
  import { headsAreSame } from "./helpers/headsAreSame.js"
12
18
  import { throttle } from "./helpers/throttle.js"
13
19
  import {
@@ -15,13 +21,25 @@ import {
15
21
  type PeerMetadata,
16
22
  } from "./network/NetworkAdapterInterface.js"
17
23
  import { NetworkSubsystem } from "./network/NetworkSubsystem.js"
18
- import { RepoMessage } from "./network/messages.js"
24
+ import { MessageContents, RepoMessage } from "./network/messages.js"
19
25
  import { StorageAdapterInterface } from "./storage/StorageAdapterInterface.js"
20
26
  import { StorageSubsystem } from "./storage/StorageSubsystem.js"
21
27
  import { StorageId } from "./storage/types.js"
22
28
  import { CollectionSynchronizer } from "./synchronizer/CollectionSynchronizer.js"
23
- import { SyncStatePayload } from "./synchronizer/Synchronizer.js"
24
- import type { AnyDocumentId, DocumentId, PeerId } from "./types.js"
29
+ import {
30
+ DocSyncMetrics,
31
+ SyncStatePayload,
32
+ } from "./synchronizer/Synchronizer.js"
33
+ import type {
34
+ AnyDocumentId,
35
+ AutomergeUrl,
36
+ DocumentId,
37
+ PeerId,
38
+ } from "./types.js"
39
+ import { Progress } from "./ferigan.js"
40
+ import { CollectionHandle } from "./CollectionHandle.js"
41
+ import { next as A } from "@automerge/automerge/slim"
42
+ import { InMemoryStorageAdapter } from "./storage/StorageAdapter.js"
25
43
 
26
44
  function randomPeerId() {
27
45
  return ("peer-" + Math.random().toString(36).slice(4)) as PeerId
@@ -40,12 +58,7 @@ export class Repo extends EventEmitter<RepoEvents> {
40
58
 
41
59
  /** @hidden */
42
60
  networkSubsystem: NetworkSubsystem
43
- /** @hidden */
44
- storageSubsystem?: StorageSubsystem
45
-
46
- /** The debounce rate is adjustable on the repo. */
47
- /** @hidden */
48
- saveDebounceRate = 100
61
+ storageSubsystem: StorageSubsystem
49
62
 
50
63
  #handleCache: Record<DocumentId, DocHandle<any>> = {}
51
64
 
@@ -60,8 +73,7 @@ export class Repo extends EventEmitter<RepoEvents> {
60
73
  /** @hidden */
61
74
  peerMetadataByPeerId: Record<PeerId, PeerMetadata> = {}
62
75
 
63
- #remoteHeadsSubscriptions = new RemoteHeadsSubscriptions()
64
- #remoteHeadsGossipingEnabled = false
76
+ #beelay: A.beelay.Beelay
65
77
 
66
78
  constructor({
67
79
  storage,
@@ -70,49 +82,78 @@ export class Repo extends EventEmitter<RepoEvents> {
70
82
  sharePolicy,
71
83
  isEphemeral = storage === undefined,
72
84
  enableRemoteHeadsGossiping = false,
85
+ denylist = [],
73
86
  }: RepoConfig = {}) {
74
87
  super()
75
- this.#remoteHeadsGossipingEnabled = enableRemoteHeadsGossiping
88
+ if (storage == null) {
89
+ // beelayStorage = new InMemoryStorageAdapter()
90
+ storage = new InMemoryStorageAdapter()
91
+ }
92
+ this.#beelay = new A.beelay.Beelay({
93
+ storage,
94
+ peerId,
95
+ requestPolicy: async ({ docId }) => {
96
+ const peers = Array.from(this.networkSubsystem.peers)
97
+ const generousPeers: PeerId[] = []
98
+ for (const peerId of peers) {
99
+ const okToShare = await this.sharePolicy(peerId)
100
+ if (okToShare) generousPeers.push(peerId)
101
+ }
102
+ return generousPeers
103
+ },
104
+ })
105
+ this.storageSubsystem = new StorageSubsystem(this.#beelay, storage)
76
106
  this.#log = debug(`automerge-repo:repo`)
77
107
  this.sharePolicy = sharePolicy ?? this.sharePolicy
78
108
 
79
- this.on("delete-document", ({ documentId }) => {
80
- // TODO Pass the delete on to the network
81
- // synchronizer.removeDocument(documentId)
109
+ this.#beelay.on("message", ({ message }) => {
110
+ this.#log(`sending ${message} message to ${message.recipient}`)
111
+ networkSubsystem.send({
112
+ targetId: message.recipient as PeerId,
113
+ type: "beelay",
114
+ ...message,
115
+ } as MessageContents)
116
+ })
82
117
 
83
- if (storageSubsystem) {
84
- storageSubsystem.removeDoc(documentId).catch(err => {
85
- this.#log("error deleting document", { documentId, err })
86
- })
118
+ this.#beelay.on("docEvent", event => {
119
+ this.#log(`received ${event.data.type} event for ${event.docId}`)
120
+ const handle = this.#handleCache[event.docId as DocumentId]
121
+ if (handle != null) {
122
+ handle.update(d => Automerge.loadIncremental(d, event.data.contents))
87
123
  }
88
124
  })
89
125
 
126
+ this.#beelay.on("bundleRequired", ({ start, end, checkpoints, docId }) => {
127
+ ;(async () => {
128
+ const doc = await this.storageSubsystem.loadDoc(docId as DocumentId)
129
+ if (doc == null) {
130
+ console.warn("document not found when creating bundle")
131
+ return
132
+ }
133
+ const bundle = A.saveBundle(doc, start, end)
134
+ this.#beelay.addBundle({
135
+ docId,
136
+ checkpoints,
137
+ start,
138
+ end,
139
+ data: bundle,
140
+ })
141
+ })()
142
+ })
143
+
90
144
  // SYNCHRONIZER
91
- // The synchronizer uses the network subsystem to keep documents in sync with peers.
92
- this.synchronizer = new CollectionSynchronizer(this)
145
+ this.synchronizer = new CollectionSynchronizer(this.#beelay, this, [])
93
146
 
94
- // When the synchronizer emits messages, send them to peers
95
147
  this.synchronizer.on("message", message => {
96
148
  this.#log(`sending ${message.type} message to ${message.targetId}`)
97
149
  networkSubsystem.send(message)
98
150
  })
99
151
 
100
- if (this.#remoteHeadsGossipingEnabled) {
101
- this.synchronizer.on("open-doc", ({ peerId, documentId }) => {
102
- this.#remoteHeadsSubscriptions.subscribePeerToDoc(peerId, documentId)
103
- })
104
- }
105
-
106
- // STORAGE
107
- // The storage subsystem has access to some form of persistence, and deals with save and loading documents.
108
- const storageSubsystem = storage ? new StorageSubsystem(storage) : undefined
109
- this.storageSubsystem = storageSubsystem
110
-
111
152
  // NETWORK
112
153
  // The network subsystem deals with sending and receiving messages to and from peers.
113
154
 
114
155
  const myPeerMetadata: Promise<PeerMetadata> = (async () => ({
115
- storageId: await storageSubsystem?.id(),
156
+ // storageId: await this.storageSubsystem.id(),
116
157
  isEphemeral,
117
158
  }))()
118
159
 
@@ -126,109 +167,48 @@ export class Repo extends EventEmitter<RepoEvents> {
126
167
  // When we get a new peer, register it with the synchronizer
127
168
  networkSubsystem.on("peer", async ({ peerId, peerMetadata }) => {
128
169
  this.#log("peer connected", { peerId })
129
-
130
170
  if (peerMetadata) {
131
171
  this.peerMetadataByPeerId[peerId] = { ...peerMetadata }
132
172
  }
133
-
134
- this.sharePolicy(peerId)
135
- .then(shouldShare => {
136
- if (shouldShare && this.#remoteHeadsGossipingEnabled) {
137
- this.#remoteHeadsSubscriptions.addGenerousPeer(peerId)
138
- }
139
- })
140
- .catch(err => {
141
- console.log("error in share policy", { err })
142
- })
143
-
144
173
  this.synchronizer.addPeer(peerId)
145
174
  })
146
175
 
147
- // When a peer disconnects, remove it from the synchronizer
148
- networkSubsystem.on("peer-disconnected", ({ peerId }) => {
149
- this.synchronizer.removePeer(peerId)
150
- this.#remoteHeadsSubscriptions.removePeer(peerId)
151
- })
152
-
153
176
  // Handle incoming messages
154
177
  networkSubsystem.on("message", async msg => {
155
- this.#receiveMessage(msg)
156
- })
157
-
158
- this.synchronizer.on("sync-state", message => {
159
- this.#saveSyncState(message)
160
-
161
- const handle = this.#handleCache[message.documentId]
162
-
163
- const { storageId } = this.peerMetadataByPeerId[message.peerId] || {}
164
- if (!storageId) {
165
- return
166
- }
167
-
168
- const heads = handle.getRemoteHeads(storageId)
169
- const haveHeadsChanged =
170
- message.syncState.theirHeads &&
171
- (!heads || !headsAreSame(heads, message.syncState.theirHeads))
172
-
173
- if (haveHeadsChanged && message.syncState.theirHeads) {
174
- handle.setRemoteHeads(storageId, message.syncState.theirHeads)
175
-
176
- if (storageId && this.#remoteHeadsGossipingEnabled) {
177
- this.#remoteHeadsSubscriptions.handleImmediateRemoteHeadsChanged(
178
- message.documentId,
179
- storageId,
180
- message.syncState.theirHeads
181
- )
178
+ //@ts-ignore
179
+ // const inspected = A.beelay.inspectMessage(msg.message)
180
+ // this.#log(`received msg: ${JSON.stringify(inspected)}`)
181
+ //@ts-ignore
182
+ if (msg.type === "beelay") {
183
+ if (!(msg.message instanceof Uint8Array)) {
184
+ // The Uint8Array instance in the vitest VM is _different_ from the
185
+ // Uint8Array instance which is available in this file for some reason.
186
+ // So, even though `msg.message` _is_ a `Uint8Array`, we have to do this
187
+ // absurd thing to get the tests to pass
188
+ msg.message = Uint8Array.from(msg.message)
182
189
  }
183
- }
184
- })
185
-
186
- if (this.#remoteHeadsGossipingEnabled) {
187
- this.#remoteHeadsSubscriptions.on("notify-remote-heads", message => {
188
- this.networkSubsystem.send({
189
- type: "remote-heads-changed",
190
- targetId: message.targetId,
191
- documentId: message.documentId,
192
- newHeads: {
193
- [message.storageId]: {
194
- heads: message.heads,
195
- timestamp: message.timestamp,
196
- },
190
+ this.#beelay.receiveMessage({
191
+ message: {
192
+ sender: msg.senderId,
193
+ recipient: msg.targetId,
194
+ message: msg.message,
197
195
  },
198
196
  })
199
- })
200
-
201
- this.#remoteHeadsSubscriptions.on("change-remote-subs", message => {
202
- this.#log("change-remote-subs", message)
203
- for (const peer of message.peers) {
204
- this.networkSubsystem.send({
205
- type: "remote-subscription-change",
206
- targetId: peer,
207
- add: message.add,
208
- remove: message.remove,
209
- })
210
- }
211
- })
212
-
213
- this.#remoteHeadsSubscriptions.on("remote-heads-changed", message => {
214
- const handle = this.#handleCache[message.documentId]
215
- handle.setRemoteHeads(message.storageId, message.remoteHeads)
216
- })
217
- }
197
+ } else {
198
+ this.#receiveMessage(msg)
199
+ }
200
+ })
218
201
  }
219
202
 
220
203
  // The `document` event is fired by the DocCollection any time we create a new document or look
221
204
  // up a document by ID. We listen for it in order to wire up storage and network synchronization.
222
205
  #registerHandleWithSubsystems(handle: DocHandle<any>) {
223
- const { storageSubsystem } = this
224
- if (storageSubsystem) {
225
- // Save when the document changes, but no more often than saveDebounceRate.
226
- const saveFn = ({ handle, doc }: DocHandleEncodedChangePayload<any>) => {
227
- void storageSubsystem.saveDoc(handle.documentId, doc)
206
+ handle.on("heads-changed", () => {
207
+ const doc = handle.docSync()
208
+ if (doc != null) {
209
+ this.storageSubsystem.saveDoc(handle.documentId, doc)
228
210
  }
229
- handle.on("heads-changed", throttle(saveFn, this.saveDebounceRate))
230
- }
231
-
211
+ })
232
212
  handle.on("unavailable", () => {
233
213
  this.#log("document unavailable", { documentId: handle.documentId })
234
214
  this.emit("unavailable-document", {
@@ -236,7 +216,6 @@ export class Repo extends EventEmitter<RepoEvents> {
236
216
  })
237
217
  })
238
218
 
239
- // Register the document with the synchronizer. This advertises our interest in the document.
240
219
  this.synchronizer.addDocument(handle.documentId)
241
220
 
242
221
  // Preserve the old event in case anyone was using it.
@@ -246,60 +225,19 @@ export class Repo extends EventEmitter<RepoEvents> {
246
225
  #receiveMessage(message: RepoMessage) {
247
226
  switch (message.type) {
248
227
  case "remote-subscription-change":
249
- if (this.#remoteHeadsGossipingEnabled) {
250
- this.#remoteHeadsSubscriptions.handleControlMessage(message)
251
- }
252
- break
253
228
  case "remote-heads-changed":
254
- if (this.#remoteHeadsGossipingEnabled) {
255
- this.#remoteHeadsSubscriptions.handleRemoteHeads(message)
256
- }
257
229
  break
258
230
  case "sync":
259
231
  case "request":
260
232
  case "ephemeral":
261
233
  case "doc-unavailable":
262
234
  this.synchronizer.receiveMessage(message).catch(err => {
263
- console.log("error receiving message", { err })
235
+ console.error("error receiving message", { err })
264
236
  })
237
+ break
265
238
  }
266
239
  }
267
240
 
268
- #throttledSaveSyncStateHandlers: Record<
269
- StorageId,
270
- (payload: SyncStatePayload) => void
271
- > = {}
272
-
273
- /** saves sync state throttled per storage id, if a peer doesn't have a storage id it's sync state is not persisted */
274
- #saveSyncState(payload: SyncStatePayload) {
275
- if (!this.storageSubsystem) {
276
- return
277
- }
278
-
279
- const { storageId, isEphemeral } =
280
- this.peerMetadataByPeerId[payload.peerId] || {}
281
-
282
- if (!storageId || isEphemeral) {
283
- return
284
- }
285
-
286
- let handler = this.#throttledSaveSyncStateHandlers[storageId]
287
- if (!handler) {
288
- handler = this.#throttledSaveSyncStateHandlers[storageId] = throttle(
289
- ({ documentId, syncState }: SyncStatePayload) => {
290
- void this.storageSubsystem!.saveSyncState(
291
- documentId,
292
- storageId,
293
- syncState
294
- )
295
- },
296
- this.saveDebounceRate
297
- )
298
- }
299
-
300
- handler(payload)
301
- }
302
-
303
241
  /** Returns an existing handle if we have it; creates one otherwise. */
304
242
  #getHandle<T>({
305
243
  documentId,
@@ -324,7 +262,7 @@ export class Repo extends EventEmitter<RepoEvents> {
324
262
 
325
263
  /** Returns a list of all connected peer ids */
326
264
  get peers(): PeerId[] {
327
- return this.synchronizer.peers
265
+ return this.networkSubsystem.peers
328
266
  }
329
267
 
330
268
  getStorageIdOfPeer(peerId: PeerId): StorageId | undefined {
@@ -343,7 +281,7 @@ export class Repo extends EventEmitter<RepoEvents> {
343
281
  documentId,
344
282
  }) as DocHandle<T>
345
283
 
346
- this.#registerHandleWithSubsystems(handle)
284
+ let initialLinks: A.Link[] = []
347
285
 
348
286
  handle.update(() => {
349
287
  let nextDoc: Automerge.Doc<T>
@@ -352,9 +290,33 @@ export class Repo extends EventEmitter<RepoEvents> {
352
290
  } else {
353
291
  nextDoc = Automerge.emptyChange(Automerge.init())
354
292
  }
293
+ const patches = A.diff(nextDoc, [], A.getHeads(nextDoc))
294
+ for (const patch of patches) {
295
+ initialLinks = patches
296
+ .map(patch => {
297
+ if (patch.action === "put") {
298
+ if (patch.value instanceof A.Link) {
299
+ return patch.value
300
+ }
301
+ }
302
+ return null
303
+ })
304
+ .filter(v => v != null)
305
+ }
355
306
  return nextDoc
356
307
  })
357
308
 
309
+ for (const link of initialLinks) {
310
+ const { documentId: target } = parseAutomergeUrl(
311
+ link.target as AutomergeUrl
312
+ )
313
+ this.#beelay.addLink({ from: documentId, to: target })
314
+ }
315
+
316
+ this.storageSubsystem.saveDoc(handle.documentId, handle.docSync()!)
317
+
318
+ this.#registerHandleWithSubsystems(handle)
319
+
358
320
  handle.doneLoading()
359
321
  return handle
360
322
  }
@@ -378,7 +340,7 @@ export class Repo extends EventEmitter<RepoEvents> {
378
340
  if (!clonedHandle.isReady()) {
379
341
  throw new Error(
380
342
  `Cloned handle is not yet in ready state.
381
- (Try await handle.waitForReady() first.)`
343
+ (Try await handle.whenReady() first.)`
382
344
  )
383
345
  }
384
346
 
@@ -405,6 +367,7 @@ export class Repo extends EventEmitter<RepoEvents> {
405
367
  /** The url or documentId of the handle to retrieve */
406
368
  id: AnyDocumentId
407
369
  ): DocHandle<T> {
370
+ this.#log("find", { id })
408
371
  const documentId = interpretAsDocumentId(id)
409
372
 
410
373
  // If we have the handle cached, return it
@@ -427,9 +390,7 @@ export class Repo extends EventEmitter<RepoEvents> {
427
390
 
428
391
  // Loading & network is going to be asynchronous no matter what,
429
392
  // but we want to return the handle immediately.
430
- const attemptLoad = this.storageSubsystem
431
- ? this.storageSubsystem.loadDoc(handle.documentId)
432
- : Promise.resolve(null)
393
+ const attemptLoad = this.storageSubsystem.loadDoc(handle.documentId)
433
394
 
434
395
  attemptLoad
435
396
  .then(async loadedDoc => {
@@ -441,6 +402,7 @@ export class Repo extends EventEmitter<RepoEvents> {
441
402
  // we want to wait for the network subsystem to be ready before
442
403
  // we request the document. this prevents entering unavailable during initialization.
443
404
  await this.networkSubsystem.whenReady()
405
+ console.log("we didn't find it so we're requesting")
444
406
  handle.request()
445
407
  }
446
408
  this.#registerHandleWithSubsystems(handle)
@@ -496,16 +458,7 @@ export class Repo extends EventEmitter<RepoEvents> {
496
458
  return handle
497
459
  }
498
460
 
499
- subscribeToRemotes = (remotes: StorageId[]) => {
500
- if (this.#remoteHeadsGossipingEnabled) {
501
- this.#log("subscribeToRemotes", { remotes })
502
- this.#remoteHeadsSubscriptions.subscribeToRemotes(remotes)
503
- } else {
504
- this.#log(
505
- "WARN: subscribeToRemotes called but remote heads gossiping is not enabled"
506
- )
507
- }
508
- }
461
+ subscribeToRemotes = (remotes: StorageId[]) => {}
509
462
 
510
463
  storageId = async (): Promise<StorageId | undefined> => {
511
464
  if (!this.storageSubsystem) {
@@ -539,6 +492,39 @@ export class Repo extends EventEmitter<RepoEvents> {
539
492
  )
540
493
  }
541
494
 
495
+ /**
496
+ * Removes a DocHandle from the handleCache.
497
+ * @hidden this API is experimental and may change.
498
+ * @param documentId - documentId of the DocHandle to remove from handleCache, if present in cache.
499
+ * @returns Promise<void>
500
+ */
501
+ async removeFromCache(documentId: DocumentId) {
502
+ if (!this.#handleCache[documentId]) {
503
+ this.#log(
504
+ `WARN: removeFromCache called but handle not found in handleCache for documentId: ${documentId}`
505
+ )
506
+ return
507
+ }
508
+ const handle = this.#getHandle({ documentId })
509
+ const doc = await handle.doc([READY, UNLOADED, DELETED, UNAVAILABLE])
510
+ if (doc) {
511
+ if (handle.isReady()) {
512
+ handle.unload()
513
+ } else {
514
+ this.#log(
515
+ `WARN: removeFromCache called but handle for documentId: ${documentId} in unexpected state: ${handle.state}`
516
+ )
517
+ }
518
+ delete this.#handleCache[documentId]
519
+ // TODO: remove document from synchronizer when removeDocument is implemented
520
+ // this.synchronizer.removeDocument(documentId)
521
+ } else {
522
+ this.#log(
523
+ `WARN: removeFromCache called but doc undefined for documentId: ${documentId}`
524
+ )
525
+ }
526
+ }
527
+
542
528
  shutdown(): Promise<void> {
543
529
  this.networkSubsystem.adapters.forEach(adapter => {
544
530
  adapter.disconnect()
@@ -547,7 +533,8 @@ export class Repo extends EventEmitter<RepoEvents> {
547
533
  }
548
534
 
549
535
  metrics(): { documents: { [key: string]: any } } {
550
- return { documents: this.synchronizer.metrics() }
536
+ //return { documents: this.synchronizer.metrics() }
537
+ return { documents: {} }
551
538
  }
552
539
  }
553
540
 
@@ -575,6 +562,13 @@ export interface RepoConfig {
575
562
  * Whether to enable the experimental remote heads gossiping feature
576
563
  */
577
564
  enableRemoteHeadsGossiping?: boolean
565
+
566
+ /**
567
+ * A list of automerge URLs which should never be loaded regardless of what
568
+ * messages are received or what the share policy is. This is useful to avoid
569
+ * loading documents that are known to be too resource intensive.
570
+ */
571
+ denylist?: AutomergeUrl[]
578
572
  }
579
573
 
580
574
  /** A function that determines whether we should share a document with a peer
@@ -598,6 +592,7 @@ export interface RepoEvents {
598
592
  "delete-document": (arg: DeleteDocumentPayload) => void
599
593
  /** A document was marked as unavailable (we don't have it and none of our peers have it) */
600
594
  "unavailable-document": (arg: DeleteDocumentPayload) => void
595
+ "doc-metrics": (arg: DocMetrics) => void
601
596
  }
602
597
 
603
598
  export interface DocumentPayload {
@@ -607,3 +602,17 @@ export interface DocumentPayload {
607
602
  export interface DeleteDocumentPayload {
608
603
  documentId: DocumentId
609
604
  }
605
+
606
+ export type DocMetrics =
607
+ | DocSyncMetrics
608
+ | {
609
+ type: "doc-loaded"
610
+ documentId: DocumentId
611
+ durationMillis: number
612
+ numOps: number
613
+ numChanges: number
614
+ }
615
+ | {
616
+ type: "doc-denied"
617
+ documentId: DocumentId
618
+ }