@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
|
@@ -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
|
|
@@ -68,6 +69,15 @@ describe("DocHandle", () => {
|
|
|
68
69
|
assert.equal(doc?.foo, "bar")
|
|
69
70
|
})
|
|
70
71
|
|
|
72
|
+
/** HISTORY TRAVERSAL
|
|
73
|
+
* This API is relatively alpha-ish but we're already
|
|
74
|
+
* doing things in our own apps that are fairly ambitious
|
|
75
|
+
* by routing around to a lower-level API.
|
|
76
|
+
* This is an attempt to wrap up the existing practice
|
|
77
|
+
* in a slightly more supportable set of APIs but should be
|
|
78
|
+
* considered provisional: expect further improvements.
|
|
79
|
+
*/
|
|
80
|
+
|
|
71
81
|
it("should return the heads when requested", async () => {
|
|
72
82
|
const handle = setup()
|
|
73
83
|
handle.change(d => (d.foo = "bar"))
|
|
@@ -84,6 +94,94 @@ describe("DocHandle", () => {
|
|
|
84
94
|
assert.deepEqual(handle.heads(), undefined)
|
|
85
95
|
})
|
|
86
96
|
|
|
97
|
+
it("should return the history when requested", async () => {
|
|
98
|
+
const handle = setup()
|
|
99
|
+
handle.change(d => (d.foo = "bar"))
|
|
100
|
+
handle.change(d => (d.foo = "baz"))
|
|
101
|
+
assert.equal(handle.isReady(), true)
|
|
102
|
+
|
|
103
|
+
const history = handle.history()
|
|
104
|
+
assert.deepEqual(handle.history().length, 2)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it("should return a commit from the history", async () => {
|
|
108
|
+
const handle = setup()
|
|
109
|
+
handle.change(d => (d.foo = "zero"))
|
|
110
|
+
handle.change(d => (d.foo = "one"))
|
|
111
|
+
handle.change(d => (d.foo = "two"))
|
|
112
|
+
handle.change(d => (d.foo = "three"))
|
|
113
|
+
assert.equal(handle.isReady(), true)
|
|
114
|
+
|
|
115
|
+
const history = handle.history()
|
|
116
|
+
const view = handle.view(history[1])
|
|
117
|
+
assert.deepEqual(view, { foo: "one" })
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it("should return diffs", async () => {
|
|
121
|
+
const handle = setup()
|
|
122
|
+
handle.change(d => (d.foo = "zero"))
|
|
123
|
+
handle.change(d => (d.foo = "one"))
|
|
124
|
+
handle.change(d => (d.foo = "two"))
|
|
125
|
+
handle.change(d => (d.foo = "three"))
|
|
126
|
+
assert.equal(handle.isReady(), true)
|
|
127
|
+
|
|
128
|
+
const history = handle.history()
|
|
129
|
+
const patches = handle.diff(history[1])
|
|
130
|
+
assert.deepEqual(patches, [
|
|
131
|
+
{ action: "put", path: ["foo"], value: "" },
|
|
132
|
+
{ action: "splice", path: ["foo", 0], value: "one" },
|
|
133
|
+
])
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it("should support arbitrary diffs too", async () => {
|
|
137
|
+
const handle = setup()
|
|
138
|
+
handle.change(d => (d.foo = "zero"))
|
|
139
|
+
handle.change(d => (d.foo = "one"))
|
|
140
|
+
handle.change(d => (d.foo = "two"))
|
|
141
|
+
handle.change(d => (d.foo = "three"))
|
|
142
|
+
assert.equal(handle.isReady(), true)
|
|
143
|
+
|
|
144
|
+
const history = handle.history()
|
|
145
|
+
const patches = handle.diff(history[1], history[3])
|
|
146
|
+
assert.deepEqual(patches, [
|
|
147
|
+
{ action: "put", path: ["foo"], value: "" },
|
|
148
|
+
{ action: "splice", path: ["foo", 0], value: "three" },
|
|
149
|
+
])
|
|
150
|
+
const backPatches = handle.diff(history[3], history[1])
|
|
151
|
+
assert.deepEqual(backPatches, [
|
|
152
|
+
{ action: "put", path: ["foo"], value: "" },
|
|
153
|
+
{ action: "splice", path: ["foo", 0], value: "one" },
|
|
154
|
+
])
|
|
155
|
+
})
|
|
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
|
+
|
|
87
185
|
/**
|
|
88
186
|
* Once there's a Repo#stop API this case should be covered in accompanying
|
|
89
187
|
* tests and the following test removed.
|
|
@@ -325,6 +423,49 @@ describe("DocHandle", () => {
|
|
|
325
423
|
assert.equal(handle.isDeleted(), true)
|
|
326
424
|
})
|
|
327
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
|
+
|
|
328
469
|
it("should allow changing at old heads", async () => {
|
|
329
470
|
const handle = setup()
|
|
330
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
|
|