@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.
- 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 +37 -6
- package/dist/DocHandle.d.ts.map +1 -1
- package/dist/DocHandle.js +64 -6
- 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 +80 -8
- 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 +72 -13
- 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
|
@@ -17,9 +17,10 @@ import {
|
|
|
17
17
|
SyncMessage,
|
|
18
18
|
isRequestMessage,
|
|
19
19
|
} from "../network/messages.js"
|
|
20
|
-
import { PeerId } from "../types.js"
|
|
20
|
+
import { AutomergeUrl, DocumentId, PeerId } from "../types.js"
|
|
21
21
|
import { Synchronizer } from "./Synchronizer.js"
|
|
22
22
|
import { throttle } from "../helpers/throttle.js"
|
|
23
|
+
import { parseAutomergeUrl } from "../AutomergeUrl.js"
|
|
23
24
|
|
|
24
25
|
type PeerDocumentStatus = "unknown" | "has" | "unavailable" | "wants"
|
|
25
26
|
|
|
@@ -30,7 +31,7 @@ type PendingMessage = {
|
|
|
30
31
|
|
|
31
32
|
interface DocSynchronizerConfig {
|
|
32
33
|
handle: DocHandle<unknown>
|
|
33
|
-
|
|
34
|
+
beelay: A.beelay.Beelay
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
/**
|
|
@@ -44,65 +45,56 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
44
45
|
/** Active peers */
|
|
45
46
|
#peers: PeerId[] = []
|
|
46
47
|
|
|
47
|
-
#pendingSyncStateCallbacks: Record<
|
|
48
|
-
PeerId,
|
|
49
|
-
((syncState: A.SyncState) => void)[]
|
|
50
|
-
> = {}
|
|
51
|
-
|
|
52
48
|
#peerDocumentStatuses: Record<PeerId, PeerDocumentStatus> = {}
|
|
53
|
-
|
|
54
|
-
/** Sync state for each peer we've communicated with (including inactive peers) */
|
|
55
|
-
#syncStates: Record<PeerId, A.SyncState> = {}
|
|
56
|
-
|
|
57
|
-
#pendingSyncMessages: Array<PendingMessage> = []
|
|
58
|
-
|
|
49
|
+
#lastSaveOffset: string | null = null
|
|
59
50
|
#syncStarted = false
|
|
51
|
+
#beelay: A.beelay.Beelay
|
|
60
52
|
|
|
61
53
|
#handle: DocHandle<unknown>
|
|
62
|
-
#
|
|
54
|
+
#docId: DocumentId
|
|
63
55
|
|
|
64
|
-
constructor({ handle,
|
|
56
|
+
constructor({ handle, beelay }: DocSynchronizerConfig) {
|
|
65
57
|
super()
|
|
66
58
|
this.#handle = handle
|
|
67
|
-
this.#
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const docId = handle.documentId.slice(0, 5)
|
|
71
|
-
this.#log = debug(`automerge-repo:docsync:${docId}`)
|
|
59
|
+
this.#beelay = beelay
|
|
60
|
+
this.#docId = this.#handle.documentId
|
|
72
61
|
|
|
73
|
-
handle.
|
|
74
|
-
"change",
|
|
75
|
-
throttle(() => this.#syncWithPeers(), this.syncDebounceRate)
|
|
76
|
-
)
|
|
62
|
+
this.#log = debug(`automerge-repo:docsync:${this.#handle.documentId}`)
|
|
77
63
|
|
|
78
64
|
handle.on("ephemeral-message-outbound", payload =>
|
|
79
65
|
this.#broadcastToPeers(payload)
|
|
80
66
|
)
|
|
81
67
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
68
|
+
handle.on("change", changeInfo => {
|
|
69
|
+
const newLinks = changeInfo.patches
|
|
70
|
+
.map(patch => {
|
|
71
|
+
if (patch.action === "put") {
|
|
72
|
+
if (patch.value instanceof A.Link) {
|
|
73
|
+
return patch.value
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return null
|
|
77
|
+
})
|
|
78
|
+
.filter(v => v != null)
|
|
79
|
+
for (const link of newLinks) {
|
|
80
|
+
const { documentId: target } = parseAutomergeUrl(
|
|
81
|
+
link.target as AutomergeUrl
|
|
82
|
+
)
|
|
83
|
+
this.#beelay.addLink({ from: this.#handle.documentId, to: target })
|
|
84
|
+
}
|
|
85
|
+
})
|
|
87
86
|
}
|
|
88
87
|
|
|
89
88
|
get peerStates() {
|
|
90
89
|
return this.#peerDocumentStatuses
|
|
91
90
|
}
|
|
92
91
|
|
|
93
|
-
get documentId() {
|
|
94
|
-
return this.#
|
|
92
|
+
get documentId(): DocumentId {
|
|
93
|
+
return this.#docId
|
|
95
94
|
}
|
|
96
95
|
|
|
97
96
|
/// PRIVATE
|
|
98
97
|
|
|
99
|
-
async #syncWithPeers() {
|
|
100
|
-
this.#log(`syncWithPeers`)
|
|
101
|
-
const doc = await this.#handle.doc()
|
|
102
|
-
if (doc === undefined) return
|
|
103
|
-
this.#peers.forEach(peerId => this.#sendSyncMessage(peerId, doc))
|
|
104
|
-
}
|
|
105
|
-
|
|
106
98
|
async #broadcastToPeers({
|
|
107
99
|
data,
|
|
108
100
|
}: DocHandleOutboundEphemeralMessagePayload<unknown>) {
|
|
@@ -116,110 +108,12 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
116
108
|
const message: MessageContents<EphemeralMessage> = {
|
|
117
109
|
type: "ephemeral",
|
|
118
110
|
targetId: peerId,
|
|
119
|
-
documentId: this
|
|
111
|
+
documentId: this.documentId,
|
|
120
112
|
data,
|
|
121
113
|
}
|
|
122
114
|
this.emit("message", message)
|
|
123
115
|
}
|
|
124
116
|
|
|
125
|
-
#withSyncState(peerId: PeerId, callback: (syncState: A.SyncState) => void) {
|
|
126
|
-
this.#addPeer(peerId)
|
|
127
|
-
|
|
128
|
-
if (!(peerId in this.#peerDocumentStatuses)) {
|
|
129
|
-
this.#peerDocumentStatuses[peerId] = "unknown"
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const syncState = this.#syncStates[peerId]
|
|
133
|
-
if (syncState) {
|
|
134
|
-
callback(syncState)
|
|
135
|
-
return
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
let pendingCallbacks = this.#pendingSyncStateCallbacks[peerId]
|
|
139
|
-
if (!pendingCallbacks) {
|
|
140
|
-
this.#onLoadSyncState(peerId)
|
|
141
|
-
.then(syncState => {
|
|
142
|
-
this.#initSyncState(peerId, syncState ?? A.initSyncState())
|
|
143
|
-
})
|
|
144
|
-
.catch(err => {
|
|
145
|
-
this.#log(`Error loading sync state for ${peerId}: ${err}`)
|
|
146
|
-
})
|
|
147
|
-
pendingCallbacks = this.#pendingSyncStateCallbacks[peerId] = []
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
pendingCallbacks.push(callback)
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
#addPeer(peerId: PeerId) {
|
|
154
|
-
if (!this.#peers.includes(peerId)) {
|
|
155
|
-
this.#peers.push(peerId)
|
|
156
|
-
this.emit("open-doc", { documentId: this.documentId, peerId })
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
#initSyncState(peerId: PeerId, syncState: A.SyncState) {
|
|
161
|
-
const pendingCallbacks = this.#pendingSyncStateCallbacks[peerId]
|
|
162
|
-
if (pendingCallbacks) {
|
|
163
|
-
for (const callback of pendingCallbacks) {
|
|
164
|
-
callback(syncState)
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
delete this.#pendingSyncStateCallbacks[peerId]
|
|
169
|
-
|
|
170
|
-
this.#syncStates[peerId] = syncState
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
#setSyncState(peerId: PeerId, syncState: A.SyncState) {
|
|
174
|
-
this.#syncStates[peerId] = syncState
|
|
175
|
-
|
|
176
|
-
this.emit("sync-state", {
|
|
177
|
-
peerId,
|
|
178
|
-
syncState,
|
|
179
|
-
documentId: this.#handle.documentId,
|
|
180
|
-
})
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
#sendSyncMessage(peerId: PeerId, doc: A.Doc<unknown>) {
|
|
184
|
-
this.#log(`sendSyncMessage ->${peerId}`)
|
|
185
|
-
|
|
186
|
-
this.#withSyncState(peerId, syncState => {
|
|
187
|
-
const [newSyncState, message] = A.generateSyncMessage(doc, syncState)
|
|
188
|
-
if (message) {
|
|
189
|
-
this.#setSyncState(peerId, newSyncState)
|
|
190
|
-
const isNew = A.getHeads(doc).length === 0
|
|
191
|
-
|
|
192
|
-
if (
|
|
193
|
-
!this.#handle.isReady() &&
|
|
194
|
-
isNew &&
|
|
195
|
-
newSyncState.sharedHeads.length === 0 &&
|
|
196
|
-
!Object.values(this.#peerDocumentStatuses).includes("has") &&
|
|
197
|
-
this.#peerDocumentStatuses[peerId] === "unknown"
|
|
198
|
-
) {
|
|
199
|
-
// we don't have the document (or access to it), so we request it
|
|
200
|
-
this.emit("message", {
|
|
201
|
-
type: "request",
|
|
202
|
-
targetId: peerId,
|
|
203
|
-
documentId: this.#handle.documentId,
|
|
204
|
-
data: message,
|
|
205
|
-
} as RequestMessage)
|
|
206
|
-
} else {
|
|
207
|
-
this.emit("message", {
|
|
208
|
-
type: "sync",
|
|
209
|
-
targetId: peerId,
|
|
210
|
-
data: message,
|
|
211
|
-
documentId: this.#handle.documentId,
|
|
212
|
-
} as SyncMessage)
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// if we have sent heads, then the peer now has or will have the document
|
|
216
|
-
if (!isNew) {
|
|
217
|
-
this.#peerDocumentStatuses[peerId] = "has"
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
})
|
|
221
|
-
}
|
|
222
|
-
|
|
223
117
|
/// PUBLIC
|
|
224
118
|
|
|
225
119
|
hasPeer(peerId: PeerId) {
|
|
@@ -227,66 +121,72 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
227
121
|
}
|
|
228
122
|
|
|
229
123
|
beginSync(peerIds: PeerId[]) {
|
|
230
|
-
|
|
231
|
-
peerId => this.#peerDocumentStatuses[peerId] in ["unavailable", "wants"]
|
|
232
|
-
)
|
|
233
|
-
|
|
234
|
-
// At this point if we don't have anything in our storage, we need to use an empty doc to sync
|
|
235
|
-
// with; but we don't want to surface that state to the front end
|
|
124
|
+
this.#log(`beginSync: ${peerIds.join(", ")}`)
|
|
236
125
|
|
|
237
126
|
const docPromise = this.#handle
|
|
238
|
-
.
|
|
127
|
+
.whenReady([READY, REQUESTING, UNAVAILABLE])
|
|
239
128
|
.then(doc => {
|
|
240
|
-
// we register out peers first, then say that sync has started
|
|
241
129
|
this.#syncStarted = true
|
|
242
130
|
this.#checkDocUnavailable()
|
|
243
|
-
|
|
244
|
-
const wasUnavailable = doc === undefined
|
|
245
|
-
if (wasUnavailable && noPeersWithDocument) {
|
|
246
|
-
return
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// If the doc is unavailable we still need a blank document to generate
|
|
250
|
-
// the sync message from
|
|
251
|
-
return doc ?? A.init<unknown>()
|
|
252
131
|
})
|
|
253
|
-
|
|
254
|
-
|
|
132
|
+
// TODO: handle this error
|
|
133
|
+
.catch(() => {})
|
|
255
134
|
|
|
256
135
|
peerIds.forEach(peerId => {
|
|
257
|
-
this.#
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
A.encodeSyncState(syncState)
|
|
264
|
-
)
|
|
265
|
-
this.#setSyncState(peerId, reparsedSyncState)
|
|
136
|
+
if (!this.#peers.includes(peerId)) {
|
|
137
|
+
this.#peers.push(peerId)
|
|
138
|
+
} else {
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
this.#peerDocumentStatuses[peerId] = "unknown"
|
|
266
142
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
143
|
+
docPromise.then(() => {
|
|
144
|
+
this.#syncStarted = true
|
|
145
|
+
this.#log(`beginning sync with ${peerId} for doc: ${this.documentId}`)
|
|
146
|
+
this.#beelay
|
|
147
|
+
.syncDoc(this.documentId, peerId)
|
|
148
|
+
.then(({ snapshot, found }) => {
|
|
149
|
+
this.#peerDocumentStatuses[peerId] = found ? "has" : "unavailable"
|
|
150
|
+
// this.#log("synced snapshot: ", snapshot)
|
|
151
|
+
if (found) {
|
|
152
|
+
this.#beelay.loadDocument(this.#docId).then(commitOrBundles => {
|
|
153
|
+
if (commitOrBundles != null) {
|
|
154
|
+
this.#handle?.update(d => {
|
|
155
|
+
let doc = d
|
|
156
|
+
for (const commitOrBundle of commitOrBundles) {
|
|
157
|
+
doc = A.loadIncremental(doc, commitOrBundle.contents)
|
|
158
|
+
}
|
|
159
|
+
return doc
|
|
160
|
+
})
|
|
161
|
+
this.#checkDocUnavailable()
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
} else {
|
|
165
|
+
this.#checkDocUnavailable()
|
|
271
166
|
}
|
|
272
|
-
|
|
273
|
-
.catch(err => {
|
|
274
|
-
this.#log(`Error loading doc for ${peerId}: ${err}`)
|
|
167
|
+
this.#beelay.listen(peerId, snapshot)
|
|
275
168
|
})
|
|
276
169
|
})
|
|
277
170
|
})
|
|
278
171
|
}
|
|
279
172
|
|
|
173
|
+
peerWantsDocument(peerId: PeerId) {
|
|
174
|
+
this.#peerDocumentStatuses[peerId] = "wants"
|
|
175
|
+
if (!this.#peers.includes(peerId)) {
|
|
176
|
+
this.beginSync([peerId as PeerId])
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
280
180
|
endSync(peerId: PeerId) {
|
|
281
181
|
this.#log(`removing peer ${peerId}`)
|
|
282
182
|
this.#peers = this.#peers.filter(p => p !== peerId)
|
|
183
|
+
this.#beelay.cancelListens(peerId)
|
|
283
184
|
}
|
|
284
185
|
|
|
285
186
|
receiveMessage(message: RepoMessage) {
|
|
286
187
|
switch (message.type) {
|
|
287
188
|
case "sync":
|
|
288
189
|
case "request":
|
|
289
|
-
this.receiveSyncMessage(message)
|
|
290
190
|
break
|
|
291
191
|
case "ephemeral":
|
|
292
192
|
this.receiveEphemeralMessage(message)
|
|
@@ -301,7 +201,7 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
301
201
|
}
|
|
302
202
|
|
|
303
203
|
receiveEphemeralMessage(message: EphemeralMessage) {
|
|
304
|
-
if (message.documentId !== this
|
|
204
|
+
if (message.documentId !== this.documentId)
|
|
305
205
|
throw new Error(`channelId doesn't match documentId`)
|
|
306
206
|
|
|
307
207
|
const { senderId, data } = message
|
|
@@ -313,7 +213,6 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
313
213
|
senderId,
|
|
314
214
|
message: contents,
|
|
315
215
|
})
|
|
316
|
-
|
|
317
216
|
this.#peers.forEach(peerId => {
|
|
318
217
|
if (peerId === senderId) return
|
|
319
218
|
this.emit("message", {
|
|
@@ -323,50 +222,7 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
323
222
|
})
|
|
324
223
|
}
|
|
325
224
|
|
|
326
|
-
receiveSyncMessage(message: SyncMessage | RequestMessage) {
|
|
327
|
-
if (message.documentId !== this.#handle.documentId)
|
|
328
|
-
throw new Error(`channelId doesn't match documentId`)
|
|
329
|
-
|
|
330
|
-
// We need to block receiving the syncMessages until we've checked local storage
|
|
331
|
-
if (!this.#handle.inState([READY, REQUESTING, UNAVAILABLE])) {
|
|
332
|
-
this.#pendingSyncMessages.push({ message, received: new Date() })
|
|
333
|
-
return
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
this.#processAllPendingSyncMessages()
|
|
337
|
-
this.#processSyncMessage(message)
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
#processSyncMessage(message: SyncMessage | RequestMessage) {
|
|
341
|
-
if (isRequestMessage(message)) {
|
|
342
|
-
this.#peerDocumentStatuses[message.senderId] = "wants"
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
this.#checkDocUnavailable()
|
|
346
|
-
|
|
347
|
-
// if the message has heads, then the peer has the document
|
|
348
|
-
if (A.decodeSyncMessage(message.data).heads.length > 0) {
|
|
349
|
-
this.#peerDocumentStatuses[message.senderId] = "has"
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
this.#withSyncState(message.senderId, syncState => {
|
|
353
|
-
this.#handle.update(doc => {
|
|
354
|
-
const [newDoc, newSyncState] = A.receiveSyncMessage(
|
|
355
|
-
doc,
|
|
356
|
-
syncState,
|
|
357
|
-
message.data
|
|
358
|
-
)
|
|
359
|
-
|
|
360
|
-
this.#setSyncState(message.senderId, newSyncState)
|
|
361
|
-
|
|
362
|
-
// respond to just this peer (as required)
|
|
363
|
-
this.#sendSyncMessage(message.senderId, doc)
|
|
364
|
-
return newDoc
|
|
365
|
-
})
|
|
366
|
-
|
|
367
|
-
this.#checkDocUnavailable()
|
|
368
|
-
})
|
|
369
|
-
}
|
|
225
|
+
receiveSyncMessage(message: SyncMessage | RequestMessage) {}
|
|
370
226
|
|
|
371
227
|
#checkDocUnavailable() {
|
|
372
228
|
// if we know none of the peers have the document, tell all our peers that we don't either
|
|
@@ -384,28 +240,25 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
384
240
|
.forEach(peerId => {
|
|
385
241
|
const message: MessageContents<DocumentUnavailableMessage> = {
|
|
386
242
|
type: "doc-unavailable",
|
|
387
|
-
documentId: this
|
|
243
|
+
documentId: this.documentId,
|
|
388
244
|
targetId: peerId,
|
|
389
245
|
}
|
|
390
246
|
this.emit("message", message)
|
|
391
247
|
})
|
|
392
248
|
|
|
393
|
-
this.#handle
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
#processAllPendingSyncMessages() {
|
|
398
|
-
for (const message of this.#pendingSyncMessages) {
|
|
399
|
-
this.#processSyncMessage(message.message)
|
|
249
|
+
if (this.#handle) {
|
|
250
|
+
this.#handle.unavailable()
|
|
251
|
+
}
|
|
400
252
|
}
|
|
401
|
-
|
|
402
|
-
this.#pendingSyncMessages = []
|
|
403
253
|
}
|
|
404
254
|
|
|
405
|
-
metrics(): {
|
|
255
|
+
metrics(): {
|
|
256
|
+
peers: PeerId[]
|
|
257
|
+
size: { numOps: number; numChanges: number } | undefined
|
|
258
|
+
} {
|
|
406
259
|
return {
|
|
407
260
|
peers: this.#peers,
|
|
408
|
-
size: this.#handle
|
|
261
|
+
size: this.#handle?.metrics(),
|
|
409
262
|
}
|
|
410
263
|
}
|
|
411
264
|
}
|
|
@@ -15,6 +15,7 @@ export interface SynchronizerEvents {
|
|
|
15
15
|
message: (payload: MessageContents) => void
|
|
16
16
|
"sync-state": (payload: SyncStatePayload) => void
|
|
17
17
|
"open-doc": (arg: OpenDocMessage) => void
|
|
18
|
+
metrics: (arg: DocSyncMetrics) => void
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
/** Notify the repo that the sync state has changed */
|
|
@@ -23,3 +24,16 @@ export interface SyncStatePayload {
|
|
|
23
24
|
documentId: DocumentId
|
|
24
25
|
syncState: SyncState
|
|
25
26
|
}
|
|
27
|
+
|
|
28
|
+
export type DocSyncMetrics =
|
|
29
|
+
| {
|
|
30
|
+
type: "receive-sync-message"
|
|
31
|
+
documentId: DocumentId
|
|
32
|
+
durationMillis: number
|
|
33
|
+
numOps: number
|
|
34
|
+
numChanges: number
|
|
35
|
+
}
|
|
36
|
+
| {
|
|
37
|
+
type: "doc-denied"
|
|
38
|
+
documentId: DocumentId
|
|
39
|
+
}
|
|
@@ -2,14 +2,16 @@ import assert from "assert"
|
|
|
2
2
|
import { beforeEach, describe, it } from "vitest"
|
|
3
3
|
import { PeerId, Repo, SyncMessage } from "../src/index.js"
|
|
4
4
|
import { CollectionSynchronizer } from "../src/synchronizer/CollectionSynchronizer.js"
|
|
5
|
+
import { next as Automerge } from "@automerge/automerge"
|
|
5
6
|
|
|
6
|
-
describe("CollectionSynchronizer", () => {
|
|
7
|
+
describe.skip("CollectionSynchronizer", () => {
|
|
7
8
|
let repo: Repo
|
|
8
9
|
let synchronizer: CollectionSynchronizer
|
|
10
|
+
let beelay: Automerge.beelay.Beelay
|
|
9
11
|
|
|
10
12
|
beforeEach(() => {
|
|
11
13
|
repo = new Repo()
|
|
12
|
-
synchronizer = new CollectionSynchronizer(repo)
|
|
14
|
+
synchronizer = new CollectionSynchronizer(beelay, repo)
|
|
13
15
|
})
|
|
14
16
|
|
|
15
17
|
it("is not null", async () => {
|
package/test/DocHandle.test.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { eventPromise } from "../src/helpers/eventPromise.js"
|
|
|
7
7
|
import { pause } from "../src/helpers/pause.js"
|
|
8
8
|
import { DocHandle, DocHandleChangePayload } from "../src/index.js"
|
|
9
9
|
import { TestDoc } from "./types.js"
|
|
10
|
+
import { UNLOADED } from "../src/DocHandle.js"
|
|
10
11
|
|
|
11
12
|
describe("DocHandle", () => {
|
|
12
13
|
const TEST_ID = parseAutomergeUrl(generateAutomergeUrl()).documentId
|
|
@@ -116,19 +117,6 @@ describe("DocHandle", () => {
|
|
|
116
117
|
assert.deepEqual(view, { foo: "one" })
|
|
117
118
|
})
|
|
118
119
|
|
|
119
|
-
it("should return a commit from the history", async () => {
|
|
120
|
-
const handle = setup()
|
|
121
|
-
handle.change(d => (d.foo = "zero"))
|
|
122
|
-
handle.change(d => (d.foo = "one"))
|
|
123
|
-
handle.change(d => (d.foo = "two"))
|
|
124
|
-
handle.change(d => (d.foo = "three"))
|
|
125
|
-
assert.equal(handle.isReady(), true)
|
|
126
|
-
|
|
127
|
-
const history = handle.history()
|
|
128
|
-
const view = handle.view(history[1])
|
|
129
|
-
assert.deepEqual(view, { foo: "one" })
|
|
130
|
-
})
|
|
131
|
-
|
|
132
120
|
it("should return diffs", async () => {
|
|
133
121
|
const handle = setup()
|
|
134
122
|
handle.change(d => (d.foo = "zero"))
|
|
@@ -166,6 +154,34 @@ describe("DocHandle", () => {
|
|
|
166
154
|
])
|
|
167
155
|
})
|
|
168
156
|
|
|
157
|
+
it("should allow direct access to decoded changes", async () => {
|
|
158
|
+
const handle = setup()
|
|
159
|
+
const time = Date.now()
|
|
160
|
+
handle.change(d => (d.foo = "foo"), { message: "commitMessage" })
|
|
161
|
+
assert.equal(handle.isReady(), true)
|
|
162
|
+
|
|
163
|
+
const metadata = handle.metadata()
|
|
164
|
+
assert.deepEqual(metadata.message, "commitMessage")
|
|
165
|
+
// NOTE: I'm not testing time because of https://github.com/automerge/automerge/issues/965
|
|
166
|
+
// but it does round-trip successfully!
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it("should allow direct access to a specific decoded change", async () => {
|
|
170
|
+
const handle = setup()
|
|
171
|
+
const time = Date.now()
|
|
172
|
+
handle.change(d => (d.foo = "foo"), { message: "commitMessage" })
|
|
173
|
+
handle.change(d => (d.foo = "foo"), { message: "commitMessage2" })
|
|
174
|
+
handle.change(d => (d.foo = "foo"), { message: "commitMessage3" })
|
|
175
|
+
handle.change(d => (d.foo = "foo"), { message: "commitMessage4" })
|
|
176
|
+
assert.equal(handle.isReady(), true)
|
|
177
|
+
|
|
178
|
+
const history = handle.history()
|
|
179
|
+
const metadata = handle.metadata(history[0][0])
|
|
180
|
+
assert.deepEqual(metadata.message, "commitMessage")
|
|
181
|
+
// NOTE: I'm not testing time because of https://github.com/automerge/automerge/issues/965
|
|
182
|
+
// but it does round-trip successfully!
|
|
183
|
+
})
|
|
184
|
+
|
|
169
185
|
/**
|
|
170
186
|
* Once there's a Repo#stop API this case should be covered in accompanying
|
|
171
187
|
* tests and the following test removed.
|
|
@@ -407,6 +423,49 @@ describe("DocHandle", () => {
|
|
|
407
423
|
assert.equal(handle.isDeleted(), true)
|
|
408
424
|
})
|
|
409
425
|
|
|
426
|
+
it("should clear document reference when unloaded", async () => {
|
|
427
|
+
const handle = setup()
|
|
428
|
+
|
|
429
|
+
handle.change(doc => {
|
|
430
|
+
doc.foo = "bar"
|
|
431
|
+
})
|
|
432
|
+
const doc = await handle.doc()
|
|
433
|
+
assert.equal(doc?.foo, "bar")
|
|
434
|
+
|
|
435
|
+
handle.unload()
|
|
436
|
+
assert.equal(handle.isUnloaded(), true)
|
|
437
|
+
|
|
438
|
+
const clearedDoc = await handle.doc([UNLOADED])
|
|
439
|
+
assert.notEqual(clearedDoc?.foo, "bar")
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
it("should allow reloading after unloading", async () => {
|
|
443
|
+
const handle = setup()
|
|
444
|
+
|
|
445
|
+
handle.change(doc => {
|
|
446
|
+
doc.foo = "bar"
|
|
447
|
+
})
|
|
448
|
+
const doc = await handle.doc()
|
|
449
|
+
assert.equal(doc?.foo, "bar")
|
|
450
|
+
|
|
451
|
+
handle.unload()
|
|
452
|
+
|
|
453
|
+
// reload to transition from unloaded to loading
|
|
454
|
+
handle.reload()
|
|
455
|
+
|
|
456
|
+
// simulate requesting from the network
|
|
457
|
+
handle.request()
|
|
458
|
+
|
|
459
|
+
// simulate updating from the network
|
|
460
|
+
handle.update(doc => {
|
|
461
|
+
return A.change(doc, d => (d.foo = "bar"))
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
const reloadedDoc = await handle.doc()
|
|
465
|
+
assert.equal(handle.isReady(), true)
|
|
466
|
+
assert.equal(reloadedDoc?.foo, "bar")
|
|
467
|
+
})
|
|
468
|
+
|
|
410
469
|
it("should allow changing at old heads", async () => {
|
|
411
470
|
const handle = setup()
|
|
412
471
|
|
|
@@ -16,9 +16,10 @@ const alice = "alice" as PeerId
|
|
|
16
16
|
const bob = "bob" as PeerId
|
|
17
17
|
const charlie = "charlie" as PeerId
|
|
18
18
|
|
|
19
|
-
describe("DocSynchronizer", () => {
|
|
19
|
+
describe.skip("DocSynchronizer", () => {
|
|
20
20
|
let handle: DocHandle<TestDoc>
|
|
21
21
|
let docSynchronizer: DocSynchronizer
|
|
22
|
+
let beelay: Automerge.beelay.Beelay
|
|
22
23
|
|
|
23
24
|
const setup = () => {
|
|
24
25
|
const docId = parseAutomergeUrl(generateAutomergeUrl()).documentId
|
|
@@ -26,6 +27,7 @@ describe("DocSynchronizer", () => {
|
|
|
26
27
|
handle.doneLoading()
|
|
27
28
|
|
|
28
29
|
docSynchronizer = new DocSynchronizer({
|
|
30
|
+
beelay,
|
|
29
31
|
handle: handle as DocHandle<unknown>,
|
|
30
32
|
})
|
|
31
33
|
|
|
@@ -106,6 +108,7 @@ describe("DocSynchronizer", () => {
|
|
|
106
108
|
|
|
107
109
|
const handle = new DocHandle<TestDoc>(docId, { isNew: false })
|
|
108
110
|
docSynchronizer = new DocSynchronizer({
|
|
111
|
+
beelay,
|
|
109
112
|
handle: handle as DocHandle<unknown>,
|
|
110
113
|
})
|
|
111
114
|
docSynchronizer.beginSync([alice])
|
|
@@ -120,6 +123,7 @@ describe("DocSynchronizer", () => {
|
|
|
120
123
|
|
|
121
124
|
const bobHandle = new DocHandle<TestDoc>(docId, { isNew: false })
|
|
122
125
|
const bobDocSynchronizer = new DocSynchronizer({
|
|
126
|
+
beelay,
|
|
123
127
|
handle: bobHandle as DocHandle<unknown>,
|
|
124
128
|
})
|
|
125
129
|
bobDocSynchronizer.beginSync([alice])
|
|
@@ -129,6 +133,7 @@ describe("DocSynchronizer", () => {
|
|
|
129
133
|
const aliceHandle = new DocHandle<TestDoc>(docId, { isNew: false })
|
|
130
134
|
const aliceDocSynchronizer = new DocSynchronizer({
|
|
131
135
|
handle: aliceHandle as DocHandle<unknown>,
|
|
136
|
+
beelay,
|
|
132
137
|
})
|
|
133
138
|
aliceHandle.request()
|
|
134
139
|
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
} from "../src/network/messages.js"
|
|
11
11
|
import { collectMessages } from "./helpers/collectMessages.js"
|
|
12
12
|
|
|
13
|
-
describe("RepoHeadsSubscriptions", () => {
|
|
13
|
+
describe.skip("RepoHeadsSubscriptions", () => {
|
|
14
14
|
const storageA = "remote-a" as StorageId
|
|
15
15
|
const storageB = "remote-b" as StorageId
|
|
16
16
|
const storageC = "remote-c" as StorageId
|