@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.
- package/dist/CollectionHandle.d.ts +14 -0
- package/dist/CollectionHandle.d.ts.map +1 -0
- package/dist/CollectionHandle.js +37 -0
- package/dist/DocHandle.d.ts +67 -2
- package/dist/DocHandle.d.ts.map +1 -1
- package/dist/DocHandle.js +113 -2
- package/dist/DocUrl.d.ts +47 -0
- package/dist/DocUrl.d.ts.map +1 -0
- package/dist/DocUrl.js +72 -0
- package/dist/EphemeralData.d.ts +20 -0
- package/dist/EphemeralData.d.ts.map +1 -0
- package/dist/EphemeralData.js +1 -0
- package/dist/Repo.d.ts +28 -7
- package/dist/Repo.d.ts.map +1 -1
- package/dist/Repo.js +142 -143
- package/dist/ferigan.d.ts +51 -0
- package/dist/ferigan.d.ts.map +1 -0
- package/dist/ferigan.js +98 -0
- package/dist/helpers/tests/storage-adapter-tests.d.ts +2 -2
- package/dist/helpers/tests/storage-adapter-tests.d.ts.map +1 -1
- package/dist/helpers/tests/storage-adapter-tests.js +19 -39
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/network/NetworkSubsystem.d.ts +1 -0
- package/dist/network/NetworkSubsystem.d.ts.map +1 -1
- package/dist/network/NetworkSubsystem.js +3 -0
- package/dist/network/messages.d.ts +7 -1
- package/dist/network/messages.d.ts.map +1 -1
- package/dist/network/messages.js +2 -1
- package/dist/src/DocHandle.d.ts +182 -0
- package/dist/src/DocHandle.d.ts.map +1 -0
- package/dist/src/DocHandle.js +405 -0
- package/dist/src/DocUrl.d.ts +49 -0
- package/dist/src/DocUrl.d.ts.map +1 -0
- package/dist/src/DocUrl.js +72 -0
- package/dist/src/EphemeralData.d.ts +19 -0
- package/dist/src/EphemeralData.d.ts.map +1 -0
- package/dist/src/EphemeralData.js +1 -0
- package/dist/src/Repo.d.ts +74 -0
- package/dist/src/Repo.d.ts.map +1 -0
- package/dist/src/Repo.js +208 -0
- package/dist/src/helpers/arraysAreEqual.d.ts +2 -0
- package/dist/src/helpers/arraysAreEqual.d.ts.map +1 -0
- package/dist/src/helpers/arraysAreEqual.js +2 -0
- package/dist/src/helpers/cbor.d.ts +4 -0
- package/dist/src/helpers/cbor.d.ts.map +1 -0
- package/dist/src/helpers/cbor.js +8 -0
- package/dist/src/helpers/eventPromise.d.ts +11 -0
- package/dist/src/helpers/eventPromise.d.ts.map +1 -0
- package/dist/src/helpers/eventPromise.js +7 -0
- package/dist/src/helpers/headsAreSame.d.ts +2 -0
- package/dist/src/helpers/headsAreSame.d.ts.map +1 -0
- package/dist/src/helpers/headsAreSame.js +4 -0
- package/dist/src/helpers/mergeArrays.d.ts +2 -0
- package/dist/src/helpers/mergeArrays.d.ts.map +1 -0
- package/dist/src/helpers/mergeArrays.js +15 -0
- package/dist/src/helpers/pause.d.ts +6 -0
- package/dist/src/helpers/pause.d.ts.map +1 -0
- package/dist/src/helpers/pause.js +10 -0
- package/dist/src/helpers/tests/network-adapter-tests.d.ts +21 -0
- package/dist/src/helpers/tests/network-adapter-tests.d.ts.map +1 -0
- package/dist/src/helpers/tests/network-adapter-tests.js +122 -0
- package/dist/src/helpers/withTimeout.d.ts +12 -0
- package/dist/src/helpers/withTimeout.d.ts.map +1 -0
- package/dist/src/helpers/withTimeout.js +24 -0
- package/dist/src/index.d.ts +53 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +40 -0
- package/dist/src/network/NetworkAdapter.d.ts +26 -0
- package/dist/src/network/NetworkAdapter.d.ts.map +1 -0
- package/dist/src/network/NetworkAdapter.js +4 -0
- package/dist/src/network/NetworkSubsystem.d.ts +23 -0
- package/dist/src/network/NetworkSubsystem.d.ts.map +1 -0
- package/dist/src/network/NetworkSubsystem.js +120 -0
- package/dist/src/network/messages.d.ts +85 -0
- package/dist/src/network/messages.d.ts.map +1 -0
- package/dist/src/network/messages.js +23 -0
- package/dist/src/storage/StorageAdapter.d.ts +14 -0
- package/dist/src/storage/StorageAdapter.d.ts.map +1 -0
- package/dist/src/storage/StorageAdapter.js +1 -0
- package/dist/src/storage/StorageSubsystem.d.ts +12 -0
- package/dist/src/storage/StorageSubsystem.d.ts.map +1 -0
- package/dist/src/storage/StorageSubsystem.js +145 -0
- package/dist/src/synchronizer/CollectionSynchronizer.d.ts +25 -0
- package/dist/src/synchronizer/CollectionSynchronizer.d.ts.map +1 -0
- package/dist/src/synchronizer/CollectionSynchronizer.js +106 -0
- package/dist/src/synchronizer/DocSynchronizer.d.ts +29 -0
- package/dist/src/synchronizer/DocSynchronizer.d.ts.map +1 -0
- package/dist/src/synchronizer/DocSynchronizer.js +263 -0
- package/dist/src/synchronizer/Synchronizer.d.ts +9 -0
- package/dist/src/synchronizer/Synchronizer.d.ts.map +1 -0
- package/dist/src/synchronizer/Synchronizer.js +2 -0
- package/dist/src/types.d.ts +16 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +1 -0
- package/dist/storage/StorageAdapter.d.ts +9 -0
- package/dist/storage/StorageAdapter.d.ts.map +1 -1
- package/dist/storage/StorageAdapter.js +33 -0
- package/dist/storage/StorageSubsystem.d.ts +12 -2
- package/dist/storage/StorageSubsystem.d.ts.map +1 -1
- package/dist/storage/StorageSubsystem.js +42 -100
- package/dist/synchronizer/CollectionSynchronizer.d.ts +4 -2
- package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/CollectionSynchronizer.js +28 -15
- package/dist/synchronizer/DocSynchronizer.d.ts +6 -5
- package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/DocSynchronizer.js +76 -178
- package/dist/synchronizer/Synchronizer.d.ts +11 -0
- package/dist/synchronizer/Synchronizer.d.ts.map +1 -1
- package/dist/test/CollectionSynchronizer.test.d.ts +2 -0
- package/dist/test/CollectionSynchronizer.test.d.ts.map +1 -0
- package/dist/test/CollectionSynchronizer.test.js +57 -0
- package/dist/test/DocHandle.test.d.ts +2 -0
- package/dist/test/DocHandle.test.d.ts.map +1 -0
- package/dist/test/DocHandle.test.js +238 -0
- package/dist/test/DocSynchronizer.test.d.ts +2 -0
- package/dist/test/DocSynchronizer.test.d.ts.map +1 -0
- package/dist/test/DocSynchronizer.test.js +111 -0
- package/dist/test/Network.test.d.ts +2 -0
- package/dist/test/Network.test.d.ts.map +1 -0
- package/dist/test/Network.test.js +11 -0
- package/dist/test/Repo.test.d.ts +2 -0
- package/dist/test/Repo.test.d.ts.map +1 -0
- package/dist/test/Repo.test.js +568 -0
- package/dist/test/StorageSubsystem.test.d.ts +2 -0
- package/dist/test/StorageSubsystem.test.d.ts.map +1 -0
- package/dist/test/StorageSubsystem.test.js +56 -0
- package/dist/test/helpers/DummyNetworkAdapter.d.ts +9 -0
- package/dist/test/helpers/DummyNetworkAdapter.d.ts.map +1 -0
- package/dist/test/helpers/DummyNetworkAdapter.js +15 -0
- package/dist/test/helpers/DummyStorageAdapter.d.ts +16 -0
- package/dist/test/helpers/DummyStorageAdapter.d.ts.map +1 -0
- package/dist/test/helpers/DummyStorageAdapter.js +33 -0
- package/dist/test/helpers/generate-large-object.d.ts +5 -0
- package/dist/test/helpers/generate-large-object.d.ts.map +1 -0
- package/dist/test/helpers/generate-large-object.js +9 -0
- package/dist/test/helpers/getRandomItem.d.ts +2 -0
- package/dist/test/helpers/getRandomItem.d.ts.map +1 -0
- package/dist/test/helpers/getRandomItem.js +4 -0
- package/dist/test/types.d.ts +4 -0
- package/dist/test/types.d.ts.map +1 -0
- package/dist/test/types.js +1 -0
- package/package.json +3 -3
- package/src/CollectionHandle.ts +54 -0
- package/src/DocHandle.ts +133 -4
- package/src/Repo.ts +192 -183
- package/src/ferigan.ts +184 -0
- package/src/helpers/tests/storage-adapter-tests.ts +31 -62
- package/src/index.ts +2 -0
- package/src/network/NetworkSubsystem.ts +4 -0
- package/src/network/messages.ts +11 -2
- package/src/storage/StorageAdapter.ts +42 -0
- package/src/storage/StorageSubsystem.ts +59 -119
- package/src/synchronizer/CollectionSynchronizer.ts +34 -26
- package/src/synchronizer/DocSynchronizer.ts +84 -231
- package/src/synchronizer/Synchronizer.ts +14 -0
- package/test/CollectionSynchronizer.test.ts +4 -2
- package/test/DocHandle.test.ts +141 -0
- package/test/DocSynchronizer.test.ts +6 -1
- package/test/RemoteHeadsSubscriptions.test.ts +1 -1
- package/test/Repo.test.ts +225 -117
- package/test/StorageSubsystem.test.ts +20 -16
- 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 {
|
|
10
|
-
|
|
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 {
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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("
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
187
|
-
|
|
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
|
-
|
|
202
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
+
}
|