@automerge/automerge-repo 2.0.0-collectionsync-alpha.1 → 2.0.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/README.md +8 -8
- package/dist/AutomergeUrl.d.ts +17 -5
- package/dist/AutomergeUrl.d.ts.map +1 -1
- package/dist/AutomergeUrl.js +71 -24
- package/dist/DocHandle.d.ts +33 -41
- package/dist/DocHandle.d.ts.map +1 -1
- package/dist/DocHandle.js +105 -66
- package/dist/FindProgress.d.ts +30 -0
- package/dist/FindProgress.d.ts.map +1 -0
- package/dist/FindProgress.js +1 -0
- package/dist/RemoteHeadsSubscriptions.d.ts +4 -5
- package/dist/RemoteHeadsSubscriptions.d.ts.map +1 -1
- package/dist/RemoteHeadsSubscriptions.js +4 -1
- package/dist/Repo.d.ts +24 -5
- package/dist/Repo.d.ts.map +1 -1
- package/dist/Repo.js +355 -169
- package/dist/helpers/abortable.d.ts +36 -0
- package/dist/helpers/abortable.d.ts.map +1 -0
- package/dist/helpers/abortable.js +47 -0
- package/dist/helpers/arraysAreEqual.d.ts.map +1 -1
- package/dist/helpers/bufferFromHex.d.ts +3 -0
- package/dist/helpers/bufferFromHex.d.ts.map +1 -0
- package/dist/helpers/bufferFromHex.js +13 -0
- package/dist/helpers/debounce.d.ts.map +1 -1
- package/dist/helpers/eventPromise.d.ts.map +1 -1
- package/dist/helpers/headsAreSame.d.ts +2 -2
- package/dist/helpers/headsAreSame.d.ts.map +1 -1
- package/dist/helpers/mergeArrays.d.ts +1 -1
- package/dist/helpers/mergeArrays.d.ts.map +1 -1
- package/dist/helpers/pause.d.ts.map +1 -1
- package/dist/helpers/tests/network-adapter-tests.d.ts.map +1 -1
- package/dist/helpers/tests/network-adapter-tests.js +13 -13
- package/dist/helpers/tests/storage-adapter-tests.d.ts.map +1 -1
- package/dist/helpers/tests/storage-adapter-tests.js +6 -9
- package/dist/helpers/throttle.d.ts.map +1 -1
- package/dist/helpers/withTimeout.d.ts.map +1 -1
- package/dist/index.d.ts +35 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +37 -6
- package/dist/network/NetworkSubsystem.d.ts +0 -1
- package/dist/network/NetworkSubsystem.d.ts.map +1 -1
- package/dist/network/NetworkSubsystem.js +0 -3
- package/dist/network/messages.d.ts +1 -7
- package/dist/network/messages.d.ts.map +1 -1
- package/dist/network/messages.js +1 -2
- package/dist/storage/StorageAdapter.d.ts +0 -9
- package/dist/storage/StorageAdapter.d.ts.map +1 -1
- package/dist/storage/StorageAdapter.js +0 -33
- package/dist/storage/StorageSubsystem.d.ts +6 -2
- package/dist/storage/StorageSubsystem.d.ts.map +1 -1
- package/dist/storage/StorageSubsystem.js +131 -37
- package/dist/storage/keyHash.d.ts +1 -1
- package/dist/storage/keyHash.d.ts.map +1 -1
- package/dist/synchronizer/CollectionSynchronizer.d.ts +3 -4
- package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/CollectionSynchronizer.js +32 -26
- package/dist/synchronizer/DocSynchronizer.d.ts +8 -8
- package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/DocSynchronizer.js +205 -79
- package/dist/types.d.ts +4 -1
- package/dist/types.d.ts.map +1 -1
- package/fuzz/fuzz.ts +3 -3
- package/package.json +4 -5
- package/src/AutomergeUrl.ts +101 -26
- package/src/DocHandle.ts +158 -77
- package/src/FindProgress.ts +48 -0
- package/src/RemoteHeadsSubscriptions.ts +11 -9
- package/src/Repo.ts +465 -180
- package/src/helpers/abortable.ts +62 -0
- package/src/helpers/bufferFromHex.ts +14 -0
- package/src/helpers/headsAreSame.ts +2 -2
- package/src/helpers/tests/network-adapter-tests.ts +14 -13
- package/src/helpers/tests/storage-adapter-tests.ts +13 -24
- package/src/index.ts +57 -38
- package/src/network/NetworkSubsystem.ts +0 -4
- package/src/network/messages.ts +2 -11
- package/src/storage/StorageAdapter.ts +0 -42
- package/src/storage/StorageSubsystem.ts +155 -45
- package/src/storage/keyHash.ts +1 -1
- package/src/synchronizer/CollectionSynchronizer.ts +42 -29
- package/src/synchronizer/DocSynchronizer.ts +263 -89
- package/src/types.ts +4 -1
- package/test/AutomergeUrl.test.ts +130 -0
- package/test/CollectionSynchronizer.test.ts +6 -8
- package/test/DocHandle.test.ts +161 -77
- package/test/DocSynchronizer.test.ts +11 -9
- package/test/RemoteHeadsSubscriptions.test.ts +1 -1
- package/test/Repo.test.ts +406 -341
- package/test/StorageSubsystem.test.ts +95 -20
- package/test/remoteHeads.test.ts +28 -13
- package/dist/CollectionHandle.d.ts +0 -14
- package/dist/CollectionHandle.d.ts.map +0 -1
- package/dist/CollectionHandle.js +0 -37
- package/dist/DocUrl.d.ts +0 -47
- package/dist/DocUrl.d.ts.map +0 -1
- package/dist/DocUrl.js +0 -72
- package/dist/EphemeralData.d.ts +0 -20
- package/dist/EphemeralData.d.ts.map +0 -1
- package/dist/EphemeralData.js +0 -1
- package/dist/ferigan.d.ts +0 -51
- package/dist/ferigan.d.ts.map +0 -1
- package/dist/ferigan.js +0 -98
- package/dist/src/DocHandle.d.ts +0 -182
- package/dist/src/DocHandle.d.ts.map +0 -1
- package/dist/src/DocHandle.js +0 -405
- package/dist/src/DocUrl.d.ts +0 -49
- package/dist/src/DocUrl.d.ts.map +0 -1
- package/dist/src/DocUrl.js +0 -72
- package/dist/src/EphemeralData.d.ts +0 -19
- package/dist/src/EphemeralData.d.ts.map +0 -1
- package/dist/src/EphemeralData.js +0 -1
- package/dist/src/Repo.d.ts +0 -74
- package/dist/src/Repo.d.ts.map +0 -1
- package/dist/src/Repo.js +0 -208
- package/dist/src/helpers/arraysAreEqual.d.ts +0 -2
- package/dist/src/helpers/arraysAreEqual.d.ts.map +0 -1
- package/dist/src/helpers/arraysAreEqual.js +0 -2
- package/dist/src/helpers/cbor.d.ts +0 -4
- package/dist/src/helpers/cbor.d.ts.map +0 -1
- package/dist/src/helpers/cbor.js +0 -8
- package/dist/src/helpers/eventPromise.d.ts +0 -11
- package/dist/src/helpers/eventPromise.d.ts.map +0 -1
- package/dist/src/helpers/eventPromise.js +0 -7
- package/dist/src/helpers/headsAreSame.d.ts +0 -2
- package/dist/src/helpers/headsAreSame.d.ts.map +0 -1
- package/dist/src/helpers/headsAreSame.js +0 -4
- package/dist/src/helpers/mergeArrays.d.ts +0 -2
- package/dist/src/helpers/mergeArrays.d.ts.map +0 -1
- package/dist/src/helpers/mergeArrays.js +0 -15
- package/dist/src/helpers/pause.d.ts +0 -6
- package/dist/src/helpers/pause.d.ts.map +0 -1
- package/dist/src/helpers/pause.js +0 -10
- package/dist/src/helpers/tests/network-adapter-tests.d.ts +0 -21
- package/dist/src/helpers/tests/network-adapter-tests.d.ts.map +0 -1
- package/dist/src/helpers/tests/network-adapter-tests.js +0 -122
- package/dist/src/helpers/withTimeout.d.ts +0 -12
- package/dist/src/helpers/withTimeout.d.ts.map +0 -1
- package/dist/src/helpers/withTimeout.js +0 -24
- package/dist/src/index.d.ts +0 -53
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js +0 -40
- package/dist/src/network/NetworkAdapter.d.ts +0 -26
- package/dist/src/network/NetworkAdapter.d.ts.map +0 -1
- package/dist/src/network/NetworkAdapter.js +0 -4
- package/dist/src/network/NetworkSubsystem.d.ts +0 -23
- package/dist/src/network/NetworkSubsystem.d.ts.map +0 -1
- package/dist/src/network/NetworkSubsystem.js +0 -120
- package/dist/src/network/messages.d.ts +0 -85
- package/dist/src/network/messages.d.ts.map +0 -1
- package/dist/src/network/messages.js +0 -23
- package/dist/src/storage/StorageAdapter.d.ts +0 -14
- package/dist/src/storage/StorageAdapter.d.ts.map +0 -1
- package/dist/src/storage/StorageAdapter.js +0 -1
- package/dist/src/storage/StorageSubsystem.d.ts +0 -12
- package/dist/src/storage/StorageSubsystem.d.ts.map +0 -1
- package/dist/src/storage/StorageSubsystem.js +0 -145
- package/dist/src/synchronizer/CollectionSynchronizer.d.ts +0 -25
- package/dist/src/synchronizer/CollectionSynchronizer.d.ts.map +0 -1
- package/dist/src/synchronizer/CollectionSynchronizer.js +0 -106
- package/dist/src/synchronizer/DocSynchronizer.d.ts +0 -29
- package/dist/src/synchronizer/DocSynchronizer.d.ts.map +0 -1
- package/dist/src/synchronizer/DocSynchronizer.js +0 -263
- package/dist/src/synchronizer/Synchronizer.d.ts +0 -9
- package/dist/src/synchronizer/Synchronizer.d.ts.map +0 -1
- package/dist/src/synchronizer/Synchronizer.js +0 -2
- package/dist/src/types.d.ts +0 -16
- package/dist/src/types.d.ts.map +0 -1
- package/dist/src/types.js +0 -1
- package/dist/test/CollectionSynchronizer.test.d.ts +0 -2
- package/dist/test/CollectionSynchronizer.test.d.ts.map +0 -1
- package/dist/test/CollectionSynchronizer.test.js +0 -57
- package/dist/test/DocHandle.test.d.ts +0 -2
- package/dist/test/DocHandle.test.d.ts.map +0 -1
- package/dist/test/DocHandle.test.js +0 -238
- package/dist/test/DocSynchronizer.test.d.ts +0 -2
- package/dist/test/DocSynchronizer.test.d.ts.map +0 -1
- package/dist/test/DocSynchronizer.test.js +0 -111
- package/dist/test/Network.test.d.ts +0 -2
- package/dist/test/Network.test.d.ts.map +0 -1
- package/dist/test/Network.test.js +0 -11
- package/dist/test/Repo.test.d.ts +0 -2
- package/dist/test/Repo.test.d.ts.map +0 -1
- package/dist/test/Repo.test.js +0 -568
- package/dist/test/StorageSubsystem.test.d.ts +0 -2
- package/dist/test/StorageSubsystem.test.d.ts.map +0 -1
- package/dist/test/StorageSubsystem.test.js +0 -56
- package/dist/test/helpers/DummyNetworkAdapter.d.ts +0 -9
- package/dist/test/helpers/DummyNetworkAdapter.d.ts.map +0 -1
- package/dist/test/helpers/DummyNetworkAdapter.js +0 -15
- package/dist/test/helpers/DummyStorageAdapter.d.ts +0 -16
- package/dist/test/helpers/DummyStorageAdapter.d.ts.map +0 -1
- package/dist/test/helpers/DummyStorageAdapter.js +0 -33
- package/dist/test/helpers/generate-large-object.d.ts +0 -5
- package/dist/test/helpers/generate-large-object.d.ts.map +0 -1
- package/dist/test/helpers/generate-large-object.js +0 -9
- package/dist/test/helpers/getRandomItem.d.ts +0 -2
- package/dist/test/helpers/getRandomItem.d.ts.map +0 -1
- package/dist/test/helpers/getRandomItem.js +0 -4
- package/dist/test/types.d.ts +0 -4
- package/dist/test/types.d.ts.map +0 -1
- package/dist/test/types.js +0 -1
- package/src/CollectionHandle.ts +0 -54
- package/src/ferigan.ts +0 -184
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { next as A } from "@automerge/automerge/slim"
|
|
2
2
|
import { decode } from "cbor-x"
|
|
3
3
|
import debug from "debug"
|
|
4
4
|
import {
|
|
@@ -17,10 +17,9 @@ import {
|
|
|
17
17
|
SyncMessage,
|
|
18
18
|
isRequestMessage,
|
|
19
19
|
} from "../network/messages.js"
|
|
20
|
-
import {
|
|
20
|
+
import { 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"
|
|
24
23
|
|
|
25
24
|
type PeerDocumentStatus = "unknown" | "has" | "unavailable" | "wants"
|
|
26
25
|
|
|
@@ -31,7 +30,8 @@ type PendingMessage = {
|
|
|
31
30
|
|
|
32
31
|
interface DocSynchronizerConfig {
|
|
33
32
|
handle: DocHandle<unknown>
|
|
34
|
-
|
|
33
|
+
peerId: PeerId
|
|
34
|
+
onLoadSyncState?: (peerId: PeerId) => Promise<A.SyncState | undefined>
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/**
|
|
@@ -45,56 +45,71 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
45
45
|
/** Active peers */
|
|
46
46
|
#peers: PeerId[] = []
|
|
47
47
|
|
|
48
|
+
#pendingSyncStateCallbacks: Record<
|
|
49
|
+
PeerId,
|
|
50
|
+
((syncState: A.SyncState) => void)[]
|
|
51
|
+
> = {}
|
|
52
|
+
|
|
48
53
|
#peerDocumentStatuses: Record<PeerId, PeerDocumentStatus> = {}
|
|
49
|
-
|
|
54
|
+
|
|
55
|
+
/** Sync state for each peer we've communicated with (including inactive peers) */
|
|
56
|
+
#syncStates: Record<PeerId, A.SyncState> = {}
|
|
57
|
+
|
|
58
|
+
#pendingSyncMessages: Array<PendingMessage> = []
|
|
59
|
+
|
|
60
|
+
// We keep this around at least in part for debugging.
|
|
61
|
+
// eslint-disable-next-line no-unused-private-class-members
|
|
62
|
+
#peerId: PeerId
|
|
50
63
|
#syncStarted = false
|
|
51
|
-
#beelay: A.beelay.Beelay
|
|
52
64
|
|
|
53
65
|
#handle: DocHandle<unknown>
|
|
54
|
-
#
|
|
66
|
+
#onLoadSyncState: (peerId: PeerId) => Promise<A.SyncState | undefined>
|
|
55
67
|
|
|
56
|
-
constructor({ handle,
|
|
68
|
+
constructor({ handle, peerId, onLoadSyncState }: DocSynchronizerConfig) {
|
|
57
69
|
super()
|
|
70
|
+
this.#peerId = peerId
|
|
58
71
|
this.#handle = handle
|
|
59
|
-
this.#
|
|
60
|
-
|
|
72
|
+
this.#onLoadSyncState =
|
|
73
|
+
onLoadSyncState ?? (() => Promise.resolve(undefined))
|
|
74
|
+
|
|
75
|
+
const docId = handle.documentId.slice(0, 5)
|
|
76
|
+
this.#log = debug(`automerge-repo:docsync:${docId}`)
|
|
61
77
|
|
|
62
|
-
|
|
78
|
+
handle.on(
|
|
79
|
+
"change",
|
|
80
|
+
throttle(() => this.#syncWithPeers(), this.syncDebounceRate)
|
|
81
|
+
)
|
|
63
82
|
|
|
64
83
|
handle.on("ephemeral-message-outbound", payload =>
|
|
65
84
|
this.#broadcastToPeers(payload)
|
|
66
85
|
)
|
|
67
86
|
|
|
68
|
-
handle
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
+
// Process pending sync messages immediately after the handle becomes ready.
|
|
88
|
+
void (async () => {
|
|
89
|
+
this.#processAllPendingSyncMessages()
|
|
90
|
+
})()
|
|
86
91
|
}
|
|
87
92
|
|
|
88
93
|
get peerStates() {
|
|
89
94
|
return this.#peerDocumentStatuses
|
|
90
95
|
}
|
|
91
96
|
|
|
92
|
-
get documentId()
|
|
93
|
-
return this.#
|
|
97
|
+
get documentId() {
|
|
98
|
+
return this.#handle.documentId
|
|
94
99
|
}
|
|
95
100
|
|
|
96
101
|
/// PRIVATE
|
|
97
102
|
|
|
103
|
+
async #syncWithPeers() {
|
|
104
|
+
try {
|
|
105
|
+
await this.#handle.whenReady()
|
|
106
|
+
const doc = this.#handle.doc() // XXX THIS ONE IS WEIRD
|
|
107
|
+
this.#peers.forEach(peerId => this.#sendSyncMessage(peerId, doc))
|
|
108
|
+
} catch (e) {
|
|
109
|
+
console.log("sync with peers threw an exception")
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
98
113
|
async #broadcastToPeers({
|
|
99
114
|
data,
|
|
100
115
|
}: DocHandleOutboundEphemeralMessagePayload<unknown>) {
|
|
@@ -108,85 +123,188 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
108
123
|
const message: MessageContents<EphemeralMessage> = {
|
|
109
124
|
type: "ephemeral",
|
|
110
125
|
targetId: peerId,
|
|
111
|
-
documentId: this.documentId,
|
|
126
|
+
documentId: this.#handle.documentId,
|
|
112
127
|
data,
|
|
113
128
|
}
|
|
114
129
|
this.emit("message", message)
|
|
115
130
|
}
|
|
116
131
|
|
|
132
|
+
#withSyncState(peerId: PeerId, callback: (syncState: A.SyncState) => void) {
|
|
133
|
+
this.#addPeer(peerId)
|
|
134
|
+
|
|
135
|
+
if (!(peerId in this.#peerDocumentStatuses)) {
|
|
136
|
+
this.#peerDocumentStatuses[peerId] = "unknown"
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const syncState = this.#syncStates[peerId]
|
|
140
|
+
if (syncState) {
|
|
141
|
+
callback(syncState)
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let pendingCallbacks = this.#pendingSyncStateCallbacks[peerId]
|
|
146
|
+
if (!pendingCallbacks) {
|
|
147
|
+
this.#onLoadSyncState(peerId)
|
|
148
|
+
.then(syncState => {
|
|
149
|
+
this.#initSyncState(peerId, syncState ?? A.initSyncState())
|
|
150
|
+
})
|
|
151
|
+
.catch(err => {
|
|
152
|
+
this.#log(`Error loading sync state for ${peerId}: ${err}`)
|
|
153
|
+
})
|
|
154
|
+
pendingCallbacks = this.#pendingSyncStateCallbacks[peerId] = []
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
pendingCallbacks.push(callback)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
#addPeer(peerId: PeerId) {
|
|
161
|
+
if (!this.#peers.includes(peerId)) {
|
|
162
|
+
this.#peers.push(peerId)
|
|
163
|
+
this.emit("open-doc", { documentId: this.documentId, peerId })
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#initSyncState(peerId: PeerId, syncState: A.SyncState) {
|
|
168
|
+
const pendingCallbacks = this.#pendingSyncStateCallbacks[peerId]
|
|
169
|
+
if (pendingCallbacks) {
|
|
170
|
+
for (const callback of pendingCallbacks) {
|
|
171
|
+
callback(syncState)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
delete this.#pendingSyncStateCallbacks[peerId]
|
|
176
|
+
|
|
177
|
+
this.#syncStates[peerId] = syncState
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
#setSyncState(peerId: PeerId, syncState: A.SyncState) {
|
|
181
|
+
this.#syncStates[peerId] = syncState
|
|
182
|
+
|
|
183
|
+
this.emit("sync-state", {
|
|
184
|
+
peerId,
|
|
185
|
+
syncState,
|
|
186
|
+
documentId: this.#handle.documentId,
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#sendSyncMessage(peerId: PeerId, doc: A.Doc<unknown>) {
|
|
191
|
+
this.#log(`sendSyncMessage ->${peerId}`)
|
|
192
|
+
|
|
193
|
+
this.#withSyncState(peerId, syncState => {
|
|
194
|
+
const [newSyncState, message] = A.generateSyncMessage(doc, syncState)
|
|
195
|
+
if (message) {
|
|
196
|
+
this.#setSyncState(peerId, newSyncState)
|
|
197
|
+
const isNew = A.getHeads(doc).length === 0
|
|
198
|
+
|
|
199
|
+
if (
|
|
200
|
+
!this.#handle.isReady() &&
|
|
201
|
+
isNew &&
|
|
202
|
+
newSyncState.sharedHeads.length === 0 &&
|
|
203
|
+
!Object.values(this.#peerDocumentStatuses).includes("has") &&
|
|
204
|
+
this.#peerDocumentStatuses[peerId] === "unknown"
|
|
205
|
+
) {
|
|
206
|
+
// we don't have the document (or access to it), so we request it
|
|
207
|
+
this.emit("message", {
|
|
208
|
+
type: "request",
|
|
209
|
+
targetId: peerId,
|
|
210
|
+
documentId: this.#handle.documentId,
|
|
211
|
+
data: message,
|
|
212
|
+
} as RequestMessage)
|
|
213
|
+
} else {
|
|
214
|
+
this.emit("message", {
|
|
215
|
+
type: "sync",
|
|
216
|
+
targetId: peerId,
|
|
217
|
+
data: message,
|
|
218
|
+
documentId: this.#handle.documentId,
|
|
219
|
+
} as SyncMessage)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// if we have sent heads, then the peer now has or will have the document
|
|
223
|
+
if (!isNew) {
|
|
224
|
+
this.#peerDocumentStatuses[peerId] = "has"
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
|
|
117
230
|
/// PUBLIC
|
|
118
231
|
|
|
119
232
|
hasPeer(peerId: PeerId) {
|
|
120
233
|
return this.#peers.includes(peerId)
|
|
121
234
|
}
|
|
122
235
|
|
|
123
|
-
beginSync(peerIds: PeerId[]) {
|
|
124
|
-
this.#
|
|
125
|
-
|
|
126
|
-
const docPromise = this.#handle
|
|
236
|
+
async beginSync(peerIds: PeerId[]) {
|
|
237
|
+
void this.#handle
|
|
127
238
|
.whenReady([READY, REQUESTING, UNAVAILABLE])
|
|
128
|
-
.then(
|
|
239
|
+
.then(() => {
|
|
240
|
+
this.#syncStarted = true
|
|
241
|
+
this.#checkDocUnavailable()
|
|
242
|
+
})
|
|
243
|
+
.catch(e => {
|
|
244
|
+
console.log("caught whenready", e)
|
|
129
245
|
this.#syncStarted = true
|
|
130
246
|
this.#checkDocUnavailable()
|
|
131
247
|
})
|
|
132
|
-
// TODO: handle this error
|
|
133
|
-
.catch(() => {})
|
|
134
248
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
} else {
|
|
139
|
-
return
|
|
140
|
-
}
|
|
141
|
-
this.#peerDocumentStatuses[peerId] = "unknown"
|
|
249
|
+
const peersWithDocument = this.#peers.some(peerId => {
|
|
250
|
+
return this.#peerDocumentStatuses[peerId] == "has"
|
|
251
|
+
})
|
|
142
252
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
253
|
+
if (peersWithDocument) {
|
|
254
|
+
await this.#handle.whenReady()
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
peerIds.forEach(peerId => {
|
|
258
|
+
this.#withSyncState(peerId, syncState => {
|
|
259
|
+
// HACK: if we have a sync state already, we round-trip it through the encoding system to make
|
|
260
|
+
// sure state is preserved. This prevents an infinite loop caused by failed attempts to send
|
|
261
|
+
// messages during disconnection.
|
|
262
|
+
// TODO: cover that case with a test and remove this hack
|
|
263
|
+
const reparsedSyncState = A.decodeSyncState(
|
|
264
|
+
A.encodeSyncState(syncState)
|
|
265
|
+
)
|
|
266
|
+
this.#setSyncState(peerId, reparsedSyncState)
|
|
267
|
+
|
|
268
|
+
// At this point if we don't have anything in our storage, we need to use an empty doc to sync
|
|
269
|
+
// with; but we don't want to surface that state to the front end
|
|
270
|
+
this.#handle
|
|
271
|
+
.whenReady([READY, REQUESTING, UNAVAILABLE])
|
|
272
|
+
.then(() => {
|
|
273
|
+
const doc = this.#handle.isReady()
|
|
274
|
+
? this.#handle.doc()
|
|
275
|
+
: A.init<unknown>()
|
|
276
|
+
|
|
277
|
+
const noPeersWithDocument = peerIds.every(
|
|
278
|
+
peerId =>
|
|
279
|
+
this.#peerDocumentStatuses[peerId] in ["unavailable", "wants"]
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
const wasUnavailable = doc === undefined
|
|
283
|
+
if (wasUnavailable && noPeersWithDocument) {
|
|
284
|
+
return
|
|
166
285
|
}
|
|
167
|
-
|
|
286
|
+
|
|
287
|
+
// If the doc is unavailable we still need a blank document to generate
|
|
288
|
+
// the sync message from
|
|
289
|
+
this.#sendSyncMessage(peerId, doc ?? A.init<unknown>())
|
|
290
|
+
})
|
|
291
|
+
.catch(err => {
|
|
292
|
+
this.#log(`Error loading doc for ${peerId}: ${err}`)
|
|
168
293
|
})
|
|
169
294
|
})
|
|
170
295
|
})
|
|
171
296
|
}
|
|
172
297
|
|
|
173
|
-
peerWantsDocument(peerId: PeerId) {
|
|
174
|
-
this.#peerDocumentStatuses[peerId] = "wants"
|
|
175
|
-
if (!this.#peers.includes(peerId)) {
|
|
176
|
-
this.beginSync([peerId as PeerId])
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
298
|
endSync(peerId: PeerId) {
|
|
181
299
|
this.#log(`removing peer ${peerId}`)
|
|
182
300
|
this.#peers = this.#peers.filter(p => p !== peerId)
|
|
183
|
-
this.#beelay.cancelListens(peerId)
|
|
184
301
|
}
|
|
185
302
|
|
|
186
303
|
receiveMessage(message: RepoMessage) {
|
|
187
304
|
switch (message.type) {
|
|
188
305
|
case "sync":
|
|
189
306
|
case "request":
|
|
307
|
+
this.receiveSyncMessage(message)
|
|
190
308
|
break
|
|
191
309
|
case "ephemeral":
|
|
192
310
|
this.receiveEphemeralMessage(message)
|
|
@@ -201,7 +319,7 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
201
319
|
}
|
|
202
320
|
|
|
203
321
|
receiveEphemeralMessage(message: EphemeralMessage) {
|
|
204
|
-
if (message.documentId !== this.documentId)
|
|
322
|
+
if (message.documentId !== this.#handle.documentId)
|
|
205
323
|
throw new Error(`channelId doesn't match documentId`)
|
|
206
324
|
|
|
207
325
|
const { senderId, data } = message
|
|
@@ -213,6 +331,7 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
213
331
|
senderId,
|
|
214
332
|
message: contents,
|
|
215
333
|
})
|
|
334
|
+
|
|
216
335
|
this.#peers.forEach(peerId => {
|
|
217
336
|
if (peerId === senderId) return
|
|
218
337
|
this.emit("message", {
|
|
@@ -222,7 +341,59 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
222
341
|
})
|
|
223
342
|
}
|
|
224
343
|
|
|
225
|
-
receiveSyncMessage(message: SyncMessage | RequestMessage) {
|
|
344
|
+
receiveSyncMessage(message: SyncMessage | RequestMessage) {
|
|
345
|
+
if (message.documentId !== this.#handle.documentId)
|
|
346
|
+
throw new Error(`channelId doesn't match documentId`)
|
|
347
|
+
|
|
348
|
+
// We need to block receiving the syncMessages until we've checked local storage
|
|
349
|
+
if (!this.#handle.inState([READY, REQUESTING, UNAVAILABLE])) {
|
|
350
|
+
this.#pendingSyncMessages.push({ message, received: new Date() })
|
|
351
|
+
return
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
this.#processAllPendingSyncMessages()
|
|
355
|
+
this.#processSyncMessage(message)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
#processSyncMessage(message: SyncMessage | RequestMessage) {
|
|
359
|
+
if (isRequestMessage(message)) {
|
|
360
|
+
this.#peerDocumentStatuses[message.senderId] = "wants"
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
this.#checkDocUnavailable()
|
|
364
|
+
|
|
365
|
+
// if the message has heads, then the peer has the document
|
|
366
|
+
if (A.decodeSyncMessage(message.data).heads.length > 0) {
|
|
367
|
+
this.#peerDocumentStatuses[message.senderId] = "has"
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
this.#withSyncState(message.senderId, syncState => {
|
|
371
|
+
this.#handle.update(doc => {
|
|
372
|
+
const start = performance.now()
|
|
373
|
+
|
|
374
|
+
const [newDoc, newSyncState] = A.receiveSyncMessage(
|
|
375
|
+
doc,
|
|
376
|
+
syncState,
|
|
377
|
+
message.data
|
|
378
|
+
)
|
|
379
|
+
const end = performance.now()
|
|
380
|
+
this.emit("metrics", {
|
|
381
|
+
type: "receive-sync-message",
|
|
382
|
+
documentId: this.#handle.documentId,
|
|
383
|
+
durationMillis: end - start,
|
|
384
|
+
...A.stats(doc),
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
this.#setSyncState(message.senderId, newSyncState)
|
|
388
|
+
|
|
389
|
+
// respond to just this peer (as required)
|
|
390
|
+
this.#sendSyncMessage(message.senderId, doc)
|
|
391
|
+
return newDoc
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
this.#checkDocUnavailable()
|
|
395
|
+
})
|
|
396
|
+
}
|
|
226
397
|
|
|
227
398
|
#checkDocUnavailable() {
|
|
228
399
|
// if we know none of the peers have the document, tell all our peers that we don't either
|
|
@@ -240,25 +411,28 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
240
411
|
.forEach(peerId => {
|
|
241
412
|
const message: MessageContents<DocumentUnavailableMessage> = {
|
|
242
413
|
type: "doc-unavailable",
|
|
243
|
-
documentId: this.documentId,
|
|
414
|
+
documentId: this.#handle.documentId,
|
|
244
415
|
targetId: peerId,
|
|
245
416
|
}
|
|
246
417
|
this.emit("message", message)
|
|
247
418
|
})
|
|
248
419
|
|
|
249
|
-
|
|
250
|
-
this.#handle.unavailable()
|
|
251
|
-
}
|
|
420
|
+
this.#handle.unavailable()
|
|
252
421
|
}
|
|
253
422
|
}
|
|
254
423
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
424
|
+
#processAllPendingSyncMessages() {
|
|
425
|
+
for (const message of this.#pendingSyncMessages) {
|
|
426
|
+
this.#processSyncMessage(message.message)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
this.#pendingSyncMessages = []
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
metrics(): { peers: PeerId[]; size: { numOps: number; numChanges: number } } {
|
|
259
433
|
return {
|
|
260
434
|
peers: this.#peers,
|
|
261
|
-
size: this.#handle
|
|
435
|
+
size: this.#handle.metrics(),
|
|
262
436
|
}
|
|
263
437
|
}
|
|
264
438
|
}
|
package/src/types.ts
CHANGED
|
@@ -27,8 +27,11 @@ export type AnyDocumentId =
|
|
|
27
27
|
| BinaryDocumentId
|
|
28
28
|
| LegacyDocumentId
|
|
29
29
|
|
|
30
|
+
// We need to define our own version of heads because the AutomergeHeads type is not bs58check encoded
|
|
31
|
+
export type UrlHeads = string[] & { __automergeUrlHeads: unknown }
|
|
32
|
+
|
|
30
33
|
/** A branded type for peer IDs */
|
|
31
34
|
export type PeerId = string & { __peerId: true }
|
|
32
35
|
|
|
33
36
|
/** A randomly generated string created when the {@link Repo} starts up */
|
|
34
|
-
export type SessionId = string & {
|
|
37
|
+
export type SessionId = string & { __sessionId: true }
|
|
@@ -3,9 +3,11 @@ import bs58check from "bs58check"
|
|
|
3
3
|
import { describe, it } from "vitest"
|
|
4
4
|
import {
|
|
5
5
|
generateAutomergeUrl,
|
|
6
|
+
getHeadsFromUrl,
|
|
6
7
|
isValidAutomergeUrl,
|
|
7
8
|
parseAutomergeUrl,
|
|
8
9
|
stringifyAutomergeUrl,
|
|
10
|
+
UrlHeads,
|
|
9
11
|
} from "../src/AutomergeUrl.js"
|
|
10
12
|
import type {
|
|
11
13
|
AutomergeUrl,
|
|
@@ -102,3 +104,131 @@ describe("AutomergeUrl", () => {
|
|
|
102
104
|
})
|
|
103
105
|
})
|
|
104
106
|
})
|
|
107
|
+
|
|
108
|
+
describe("AutomergeUrl with heads", () => {
|
|
109
|
+
// Create some sample encoded heads for testing
|
|
110
|
+
const head1 = bs58check.encode(new Uint8Array([1, 2, 3, 4])) as string
|
|
111
|
+
const head2 = bs58check.encode(new Uint8Array([5, 6, 7, 8])) as string
|
|
112
|
+
const goodHeads = [head1, head2] as UrlHeads
|
|
113
|
+
const urlWithHeads = `${goodUrl}#${head1}|${head2}` as AutomergeUrl
|
|
114
|
+
const invalidHead = "not-base58-encoded"
|
|
115
|
+
const invalidHeads = [invalidHead] as UrlHeads
|
|
116
|
+
|
|
117
|
+
describe("stringifyAutomergeUrl", () => {
|
|
118
|
+
it("should stringify a url with heads", () => {
|
|
119
|
+
const url = stringifyAutomergeUrl({
|
|
120
|
+
documentId: goodDocumentId,
|
|
121
|
+
heads: goodHeads,
|
|
122
|
+
})
|
|
123
|
+
assert.strictEqual(url, urlWithHeads)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it("should throw if heads are not valid base58check", () => {
|
|
127
|
+
assert.throws(() =>
|
|
128
|
+
stringifyAutomergeUrl({
|
|
129
|
+
documentId: goodDocumentId,
|
|
130
|
+
heads: invalidHeads,
|
|
131
|
+
})
|
|
132
|
+
)
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
describe("parseAutomergeUrl", () => {
|
|
137
|
+
it("should parse a url with heads", () => {
|
|
138
|
+
const { documentId, heads } = parseAutomergeUrl(urlWithHeads)
|
|
139
|
+
assert.equal(documentId, goodDocumentId)
|
|
140
|
+
assert.deepEqual(heads, [head1, head2])
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it("should parse a url without heads", () => {
|
|
144
|
+
const { documentId, heads } = parseAutomergeUrl(goodUrl)
|
|
145
|
+
assert.equal(documentId, goodDocumentId)
|
|
146
|
+
assert.equal(heads, undefined)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it("should throw on url with invalid heads encoding", () => {
|
|
150
|
+
const badUrl = `${goodUrl}#${invalidHead}` as AutomergeUrl
|
|
151
|
+
assert.throws(() => parseAutomergeUrl(badUrl))
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
describe("isValidAutomergeUrl", () => {
|
|
156
|
+
it("should return true for a valid url with heads", () => {
|
|
157
|
+
assert(isValidAutomergeUrl(urlWithHeads) === true)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it("should return false for a url with invalid heads", () => {
|
|
161
|
+
const badUrl = `${goodUrl}#${invalidHead}` as AutomergeUrl
|
|
162
|
+
assert(isValidAutomergeUrl(badUrl) === false)
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
describe("getHeadsFromUrl", () => {
|
|
167
|
+
it("should return heads from a valid url", () => {
|
|
168
|
+
const heads = getHeadsFromUrl(urlWithHeads)
|
|
169
|
+
assert.deepEqual(heads, [head1, head2])
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it("should return undefined for url without heads", () => {
|
|
173
|
+
const heads = getHeadsFromUrl(goodUrl)
|
|
174
|
+
assert.equal(heads, undefined)
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
it("should handle a single head correctly", () => {
|
|
178
|
+
const urlWithOneHead = `${goodUrl}#${head1}` as AutomergeUrl
|
|
179
|
+
const { heads } = parseAutomergeUrl(urlWithOneHead)
|
|
180
|
+
assert.deepEqual(heads, [head1])
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it("should round-trip urls with heads", () => {
|
|
184
|
+
const originalUrl = urlWithHeads
|
|
185
|
+
const parsed = parseAutomergeUrl(originalUrl)
|
|
186
|
+
const roundTripped = stringifyAutomergeUrl({
|
|
187
|
+
documentId: parsed.documentId,
|
|
188
|
+
heads: parsed.heads,
|
|
189
|
+
})
|
|
190
|
+
assert.equal(roundTripped, originalUrl)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
describe("should reject malformed urls", () => {
|
|
194
|
+
it("should reject urls with trailing delimiter", () => {
|
|
195
|
+
assert(!isValidAutomergeUrl(`${goodUrl}#${head1}:` as AutomergeUrl))
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it("should reject urls with empty head", () => {
|
|
199
|
+
assert(!isValidAutomergeUrl(`${goodUrl}#|${head1}` as AutomergeUrl))
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it("should reject urls with multiple hash characters", () => {
|
|
203
|
+
assert(
|
|
204
|
+
!isValidAutomergeUrl(`${goodUrl}#${head1}#${head2}` as AutomergeUrl)
|
|
205
|
+
)
|
|
206
|
+
})
|
|
207
|
+
})
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
describe("empty heads section", () => {
|
|
211
|
+
it("should treat bare # as empty heads array", () => {
|
|
212
|
+
const urlWithEmptyHeads = `${goodUrl}#` as AutomergeUrl
|
|
213
|
+
const { heads } = parseAutomergeUrl(urlWithEmptyHeads)
|
|
214
|
+
assert.deepEqual(heads, [])
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it("should round-trip empty heads array", () => {
|
|
218
|
+
const original = `${goodUrl}#` as AutomergeUrl
|
|
219
|
+
const parsed = parseAutomergeUrl(original)
|
|
220
|
+
const roundTripped = stringifyAutomergeUrl({
|
|
221
|
+
documentId: parsed.documentId,
|
|
222
|
+
heads: parsed.heads,
|
|
223
|
+
})
|
|
224
|
+
assert.equal(roundTripped, original)
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it("should distinguish between no heads and empty heads", () => {
|
|
228
|
+
const noHeads = parseAutomergeUrl(goodUrl)
|
|
229
|
+
const emptyHeads = parseAutomergeUrl(`${goodUrl}#` as AutomergeUrl)
|
|
230
|
+
|
|
231
|
+
assert.equal(noHeads.heads, undefined)
|
|
232
|
+
assert.deepEqual(emptyHeads.heads, [])
|
|
233
|
+
})
|
|
234
|
+
})
|
|
@@ -2,16 +2,14 @@ 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"
|
|
6
5
|
|
|
7
|
-
describe
|
|
6
|
+
describe("CollectionSynchronizer", () => {
|
|
8
7
|
let repo: Repo
|
|
9
8
|
let synchronizer: CollectionSynchronizer
|
|
10
|
-
let beelay: Automerge.beelay.Beelay
|
|
11
9
|
|
|
12
10
|
beforeEach(() => {
|
|
13
11
|
repo = new Repo()
|
|
14
|
-
synchronizer = new CollectionSynchronizer(
|
|
12
|
+
synchronizer = new CollectionSynchronizer(repo)
|
|
15
13
|
})
|
|
16
14
|
|
|
17
15
|
it("is not null", async () => {
|
|
@@ -30,13 +28,13 @@ describe.skip("CollectionSynchronizer", () => {
|
|
|
30
28
|
done()
|
|
31
29
|
})
|
|
32
30
|
|
|
33
|
-
synchronizer.addDocument(handle
|
|
31
|
+
synchronizer.addDocument(handle)
|
|
34
32
|
}))
|
|
35
33
|
|
|
36
34
|
it("starts synchronizing existing documents when a peer is added", () =>
|
|
37
35
|
new Promise<void>(done => {
|
|
38
36
|
const handle = repo.create()
|
|
39
|
-
synchronizer.addDocument(handle
|
|
37
|
+
synchronizer.addDocument(handle)
|
|
40
38
|
synchronizer.once("message", event => {
|
|
41
39
|
const { targetId, documentId } = event as SyncMessage
|
|
42
40
|
assert(targetId === "peer1")
|
|
@@ -52,7 +50,7 @@ describe.skip("CollectionSynchronizer", () => {
|
|
|
52
50
|
|
|
53
51
|
repo.sharePolicy = async (peerId: PeerId) => peerId !== "peer1"
|
|
54
52
|
|
|
55
|
-
synchronizer.addDocument(handle
|
|
53
|
+
synchronizer.addDocument(handle)
|
|
56
54
|
synchronizer.once("message", () => {
|
|
57
55
|
reject(new Error("Should not have sent a message"))
|
|
58
56
|
})
|
|
@@ -73,7 +71,7 @@ describe.skip("CollectionSynchronizer", () => {
|
|
|
73
71
|
reject(new Error("Should not have sent a message"))
|
|
74
72
|
})
|
|
75
73
|
|
|
76
|
-
synchronizer.addDocument(handle
|
|
74
|
+
synchronizer.addDocument(handle)
|
|
77
75
|
|
|
78
76
|
setTimeout(done)
|
|
79
77
|
}))
|