@automerge/automerge-repo 1.0.0-alpha.0 → 1.0.0-alpha.3
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/DocCollection.d.ts +2 -1
- package/dist/DocCollection.d.ts.map +1 -1
- package/dist/DocCollection.js +17 -8
- package/dist/DocHandle.d.ts +27 -7
- package/dist/DocHandle.d.ts.map +1 -1
- package/dist/DocHandle.js +47 -23
- package/dist/DocUrl.d.ts +3 -3
- package/dist/DocUrl.js +9 -9
- package/dist/EphemeralData.d.ts +8 -16
- package/dist/EphemeralData.d.ts.map +1 -1
- package/dist/EphemeralData.js +1 -28
- package/dist/Repo.d.ts +0 -2
- package/dist/Repo.d.ts.map +1 -1
- package/dist/Repo.js +18 -36
- package/dist/helpers/headsAreSame.d.ts +2 -2
- package/dist/helpers/headsAreSame.d.ts.map +1 -1
- package/dist/helpers/headsAreSame.js +1 -4
- package/dist/helpers/tests/network-adapter-tests.d.ts.map +1 -1
- package/dist/helpers/tests/network-adapter-tests.js +15 -13
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/network/NetworkAdapter.d.ts +4 -13
- package/dist/network/NetworkAdapter.d.ts.map +1 -1
- package/dist/network/NetworkSubsystem.d.ts +5 -4
- package/dist/network/NetworkSubsystem.d.ts.map +1 -1
- package/dist/network/NetworkSubsystem.js +39 -25
- package/dist/network/messages.d.ts +57 -0
- package/dist/network/messages.d.ts.map +1 -0
- package/dist/network/messages.js +21 -0
- package/dist/storage/StorageSubsystem.d.ts +2 -2
- package/dist/storage/StorageSubsystem.d.ts.map +1 -1
- package/dist/storage/StorageSubsystem.js +36 -6
- package/dist/synchronizer/CollectionSynchronizer.d.ts +3 -2
- package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/CollectionSynchronizer.js +19 -13
- package/dist/synchronizer/DocSynchronizer.d.ts +9 -3
- package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/DocSynchronizer.js +145 -29
- package/dist/synchronizer/Synchronizer.d.ts +3 -4
- package/dist/synchronizer/Synchronizer.d.ts.map +1 -1
- package/dist/types.d.ts +1 -3
- package/dist/types.d.ts.map +1 -1
- package/fuzz/fuzz.ts +4 -4
- package/package.json +3 -3
- package/src/DocCollection.ts +19 -9
- package/src/DocHandle.ts +82 -37
- package/src/DocUrl.ts +9 -9
- package/src/EphemeralData.ts +6 -36
- package/src/Repo.ts +20 -52
- package/src/helpers/headsAreSame.ts +3 -5
- package/src/helpers/tests/network-adapter-tests.ts +18 -14
- package/src/index.ts +12 -2
- package/src/network/NetworkAdapter.ts +4 -20
- package/src/network/NetworkSubsystem.ts +61 -38
- package/src/network/messages.ts +123 -0
- package/src/storage/StorageSubsystem.ts +42 -6
- package/src/synchronizer/CollectionSynchronizer.ts +38 -19
- package/src/synchronizer/DocSynchronizer.ts +196 -38
- package/src/synchronizer/Synchronizer.ts +3 -8
- package/src/types.ts +4 -1
- package/test/CollectionSynchronizer.test.ts +6 -7
- package/test/DocHandle.test.ts +36 -22
- package/test/DocSynchronizer.test.ts +85 -9
- package/test/Repo.test.ts +279 -59
- package/test/StorageSubsystem.test.ts +9 -9
- package/test/helpers/DummyNetworkAdapter.ts +1 -1
- package/tsconfig.json +2 -1
- package/test/EphemeralData.test.ts +0 -44
|
@@ -1,9 +1,27 @@
|
|
|
1
1
|
import * as A from "@automerge/automerge"
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
DocHandle,
|
|
4
|
+
DocHandleOutboundEphemeralMessagePayload,
|
|
5
|
+
READY,
|
|
6
|
+
REQUESTING,
|
|
7
|
+
UNAVAILABLE,
|
|
8
|
+
} from "../DocHandle.js"
|
|
9
|
+
import { PeerId } from "../types.js"
|
|
4
10
|
import { Synchronizer } from "./Synchronizer.js"
|
|
5
11
|
|
|
6
12
|
import debug from "debug"
|
|
13
|
+
import {
|
|
14
|
+
EphemeralMessage,
|
|
15
|
+
isDocumentUnavailableMessage,
|
|
16
|
+
isRequestMessage,
|
|
17
|
+
Message,
|
|
18
|
+
RequestMessage,
|
|
19
|
+
SynchronizerMessage,
|
|
20
|
+
SyncMessage,
|
|
21
|
+
} from "../network/messages.js"
|
|
22
|
+
|
|
23
|
+
type PeerDocumentStatus = "unknown" | "has" | "unavailable" | "wants"
|
|
24
|
+
import { decode } from "cbor-x"
|
|
7
25
|
|
|
8
26
|
/**
|
|
9
27
|
* DocSynchronizer takes a handle to an Automerge document, and receives & dispatches sync messages
|
|
@@ -17,10 +35,14 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
17
35
|
/** Active peers */
|
|
18
36
|
#peers: PeerId[] = []
|
|
19
37
|
|
|
38
|
+
#peerDocumentStatuses: Record<PeerId, PeerDocumentStatus> = {}
|
|
39
|
+
|
|
20
40
|
/** Sync state for each peer we've communicated with (including inactive peers) */
|
|
21
41
|
#syncStates: Record<PeerId, A.SyncState> = {}
|
|
22
42
|
|
|
23
|
-
#pendingSyncMessages: Array<
|
|
43
|
+
#pendingSyncMessages: Array<SyncMessage | RequestMessage> = []
|
|
44
|
+
|
|
45
|
+
#syncStarted = false
|
|
24
46
|
|
|
25
47
|
constructor(private handle: DocHandle<any>) {
|
|
26
48
|
super()
|
|
@@ -31,6 +53,10 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
31
53
|
|
|
32
54
|
handle.on("change", () => this.#syncWithPeers())
|
|
33
55
|
|
|
56
|
+
handle.on("ephemeral-message-outbound", payload =>
|
|
57
|
+
this.#broadcastToPeers(payload)
|
|
58
|
+
)
|
|
59
|
+
|
|
34
60
|
// Process pending sync messages immediately after the handle becomes ready.
|
|
35
61
|
void (async () => {
|
|
36
62
|
await handle.doc([READY, REQUESTING])
|
|
@@ -38,6 +64,10 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
38
64
|
})()
|
|
39
65
|
}
|
|
40
66
|
|
|
67
|
+
get peerStates() {
|
|
68
|
+
return this.#peerDocumentStatuses
|
|
69
|
+
}
|
|
70
|
+
|
|
41
71
|
get documentId() {
|
|
42
72
|
return this.handle.documentId
|
|
43
73
|
}
|
|
@@ -47,15 +77,37 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
47
77
|
async #syncWithPeers() {
|
|
48
78
|
this.#log(`syncWithPeers`)
|
|
49
79
|
const doc = await this.handle.doc()
|
|
80
|
+
if (doc === undefined) return
|
|
50
81
|
this.#peers.forEach(peerId => this.#sendSyncMessage(peerId, doc))
|
|
51
82
|
}
|
|
52
83
|
|
|
84
|
+
async #broadcastToPeers({ data }: DocHandleOutboundEphemeralMessagePayload) {
|
|
85
|
+
this.#log(`broadcastToPeers`, this.#peers)
|
|
86
|
+
this.#peers.forEach(peerId => this.#sendEphemeralMessage(peerId, data))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#sendEphemeralMessage(peerId: PeerId, data: Uint8Array) {
|
|
90
|
+
this.#log(`sendEphemeralMessage ->${peerId}`)
|
|
91
|
+
|
|
92
|
+
this.emit("message", {
|
|
93
|
+
type: "ephemeral",
|
|
94
|
+
targetId: peerId,
|
|
95
|
+
documentId: this.handle.documentId,
|
|
96
|
+
data,
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
53
100
|
#getSyncState(peerId: PeerId) {
|
|
54
101
|
if (!this.#peers.includes(peerId)) {
|
|
55
102
|
this.#log("adding a new peer", peerId)
|
|
56
103
|
this.#peers.push(peerId)
|
|
57
104
|
}
|
|
58
105
|
|
|
106
|
+
// when a peer is added, we don't know if it has the document or not
|
|
107
|
+
if (!(peerId in this.#peerDocumentStatuses)) {
|
|
108
|
+
this.#peerDocumentStatuses[peerId] = "unknown"
|
|
109
|
+
}
|
|
110
|
+
|
|
59
111
|
return this.#syncStates[peerId] ?? A.initSyncState()
|
|
60
112
|
}
|
|
61
113
|
|
|
@@ -77,16 +129,35 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
77
129
|
if (message) {
|
|
78
130
|
this.#logMessage(`sendSyncMessage 🡒 ${peerId}`, message)
|
|
79
131
|
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
132
|
+
const decoded = A.decodeSyncMessage(message)
|
|
133
|
+
|
|
134
|
+
if (
|
|
135
|
+
!this.handle.isReady() &&
|
|
136
|
+
decoded.heads.length === 0 &&
|
|
137
|
+
newSyncState.sharedHeads.length === 0 &&
|
|
138
|
+
!Object.values(this.#peerDocumentStatuses).includes("has") &&
|
|
139
|
+
this.#peerDocumentStatuses[peerId] === "unknown"
|
|
140
|
+
) {
|
|
141
|
+
// we don't have the document (or access to it), so we request it
|
|
142
|
+
this.emit("message", {
|
|
143
|
+
type: "request",
|
|
144
|
+
targetId: peerId,
|
|
145
|
+
documentId: this.handle.documentId,
|
|
146
|
+
data: message,
|
|
147
|
+
})
|
|
148
|
+
} else {
|
|
149
|
+
this.emit("message", {
|
|
150
|
+
type: "sync",
|
|
151
|
+
targetId: peerId,
|
|
152
|
+
data: message,
|
|
153
|
+
documentId: this.handle.documentId,
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// if we have sent heads, then the peer now has or will have the document
|
|
158
|
+
if (decoded.heads.length > 0) {
|
|
159
|
+
this.#peerDocumentStatuses[peerId] = "has"
|
|
160
|
+
}
|
|
90
161
|
}
|
|
91
162
|
}
|
|
92
163
|
|
|
@@ -104,8 +175,8 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
104
175
|
// expanding is expensive, so only do it if we're logging at this level
|
|
105
176
|
const expanded = this.#opsLog.enabled
|
|
106
177
|
? decoded.changes.flatMap(change =>
|
|
107
|
-
|
|
108
|
-
|
|
178
|
+
A.decodeChange(change).ops.map(op => JSON.stringify(op))
|
|
179
|
+
)
|
|
109
180
|
: null
|
|
110
181
|
this.#opsLog(logText, expanded)
|
|
111
182
|
}
|
|
@@ -116,21 +187,33 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
116
187
|
return this.#peers.includes(peerId)
|
|
117
188
|
}
|
|
118
189
|
|
|
119
|
-
beginSync(
|
|
120
|
-
this.#log(`beginSync: ${
|
|
190
|
+
beginSync(peerIds: PeerId[]) {
|
|
191
|
+
this.#log(`beginSync: ${peerIds.join(", ")}`)
|
|
121
192
|
|
|
122
193
|
// At this point if we don't have anything in our storage, we need to use an empty doc to sync
|
|
123
194
|
// with; but we don't want to surface that state to the front end
|
|
124
|
-
void this.handle.doc([READY, REQUESTING]).then(doc => {
|
|
195
|
+
void this.handle.doc([READY, REQUESTING, UNAVAILABLE]).then(doc => {
|
|
196
|
+
// if we don't have any peers, then we can say the document is unavailable
|
|
197
|
+
|
|
125
198
|
// HACK: if we have a sync state already, we round-trip it through the encoding system to make
|
|
126
199
|
// sure state is preserved. This prevents an infinite loop caused by failed attempts to send
|
|
127
200
|
// messages during disconnection.
|
|
128
201
|
// TODO: cover that case with a test and remove this hack
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
202
|
+
peerIds.forEach(peerId => {
|
|
203
|
+
const syncStateRaw = this.#getSyncState(peerId)
|
|
204
|
+
const syncState = A.decodeSyncState(A.encodeSyncState(syncStateRaw))
|
|
205
|
+
this.#setSyncState(peerId, syncState)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
// we register out peers first, then say that sync has started
|
|
209
|
+
this.#syncStarted = true
|
|
210
|
+
this.#checkDocUnavailable()
|
|
132
211
|
|
|
133
|
-
|
|
212
|
+
if (doc === undefined) return
|
|
213
|
+
|
|
214
|
+
peerIds.forEach(peerId => {
|
|
215
|
+
this.#sendSyncMessage(peerId, doc)
|
|
216
|
+
})
|
|
134
217
|
})
|
|
135
218
|
}
|
|
136
219
|
|
|
@@ -139,43 +222,118 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
139
222
|
this.#peers = this.#peers.filter(p => p !== peerId)
|
|
140
223
|
}
|
|
141
224
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
225
|
+
receiveMessage(message: SynchronizerMessage) {
|
|
226
|
+
switch (message.type) {
|
|
227
|
+
case "sync":
|
|
228
|
+
case "request":
|
|
229
|
+
this.receiveSyncMessage(message)
|
|
230
|
+
break
|
|
231
|
+
case "ephemeral":
|
|
232
|
+
this.receiveEphemeralMessage(message)
|
|
233
|
+
break
|
|
234
|
+
case "doc-unavailable":
|
|
235
|
+
this.#peerDocumentStatuses[message.senderId] = "unavailable"
|
|
236
|
+
this.#checkDocUnavailable()
|
|
237
|
+
break
|
|
238
|
+
default:
|
|
239
|
+
throw new Error(`unknown message type: ${message}`)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
receiveEphemeralMessage(message: EphemeralMessage) {
|
|
244
|
+
if (message.documentId !== this.handle.documentId)
|
|
245
|
+
throw new Error(`channelId doesn't match documentId`)
|
|
246
|
+
|
|
247
|
+
const { senderId, data } = message
|
|
248
|
+
|
|
249
|
+
const contents = decode(data)
|
|
250
|
+
|
|
251
|
+
this.handle.emit("ephemeral-message", {
|
|
252
|
+
handle: this.handle,
|
|
253
|
+
senderId,
|
|
254
|
+
message: contents,
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
this.#peers.forEach(peerId => {
|
|
258
|
+
if (peerId === senderId) return
|
|
259
|
+
this.emit("message", {
|
|
260
|
+
...message,
|
|
261
|
+
targetId: peerId,
|
|
262
|
+
})
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
receiveSyncMessage(message: SyncMessage | RequestMessage) {
|
|
267
|
+
if (message.documentId !== this.handle.documentId)
|
|
148
268
|
throw new Error(`channelId doesn't match documentId`)
|
|
149
269
|
|
|
150
270
|
// We need to block receiving the syncMessages until we've checked local storage
|
|
151
|
-
if (!this.handle.inState([READY, REQUESTING])) {
|
|
152
|
-
this.#pendingSyncMessages.push(
|
|
271
|
+
if (!this.handle.inState([READY, REQUESTING, UNAVAILABLE])) {
|
|
272
|
+
this.#pendingSyncMessages.push(message)
|
|
153
273
|
return
|
|
154
274
|
}
|
|
155
275
|
|
|
156
276
|
this.#processAllPendingSyncMessages()
|
|
157
|
-
this.#processSyncMessage(
|
|
277
|
+
this.#processSyncMessage(message)
|
|
158
278
|
}
|
|
159
279
|
|
|
160
|
-
#processSyncMessage(
|
|
280
|
+
#processSyncMessage(message: SyncMessage | RequestMessage) {
|
|
281
|
+
if (isRequestMessage(message)) {
|
|
282
|
+
this.#peerDocumentStatuses[message.senderId] = "wants"
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
this.#checkDocUnavailable()
|
|
286
|
+
|
|
287
|
+
// if the message has heads, then the peer has the document
|
|
288
|
+
if (A.decodeSyncMessage(message.data).heads.length > 0) {
|
|
289
|
+
this.#peerDocumentStatuses[message.senderId] = "has"
|
|
290
|
+
}
|
|
291
|
+
|
|
161
292
|
this.handle.update(doc => {
|
|
162
293
|
const [newDoc, newSyncState] = A.receiveSyncMessage(
|
|
163
294
|
doc,
|
|
164
|
-
this.#getSyncState(
|
|
165
|
-
message
|
|
295
|
+
this.#getSyncState(message.senderId),
|
|
296
|
+
message.data
|
|
166
297
|
)
|
|
167
298
|
|
|
168
|
-
this.#setSyncState(
|
|
299
|
+
this.#setSyncState(message.senderId, newSyncState)
|
|
169
300
|
|
|
170
301
|
// respond to just this peer (as required)
|
|
171
|
-
this.#sendSyncMessage(
|
|
302
|
+
this.#sendSyncMessage(message.senderId, doc)
|
|
172
303
|
return newDoc
|
|
173
304
|
})
|
|
305
|
+
|
|
306
|
+
this.#checkDocUnavailable()
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
#checkDocUnavailable() {
|
|
310
|
+
// if we know none of the peers have the document, tell all our peers that we don't either
|
|
311
|
+
if (
|
|
312
|
+
this.#syncStarted &&
|
|
313
|
+
this.handle.inState([REQUESTING]) &&
|
|
314
|
+
this.#peers.every(
|
|
315
|
+
peerId =>
|
|
316
|
+
this.#peerDocumentStatuses[peerId] === "unavailable" ||
|
|
317
|
+
this.#peerDocumentStatuses[peerId] === "wants"
|
|
318
|
+
)
|
|
319
|
+
) {
|
|
320
|
+
this.#peers
|
|
321
|
+
.filter(peerId => this.#peerDocumentStatuses[peerId] === "wants")
|
|
322
|
+
.forEach(peerId => {
|
|
323
|
+
this.emit("message", {
|
|
324
|
+
type: "doc-unavailable",
|
|
325
|
+
documentId: this.handle.documentId,
|
|
326
|
+
targetId: peerId,
|
|
327
|
+
})
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
this.handle.unavailable()
|
|
331
|
+
}
|
|
174
332
|
}
|
|
175
333
|
|
|
176
334
|
#processAllPendingSyncMessages() {
|
|
177
|
-
for (const
|
|
178
|
-
this.#processSyncMessage(
|
|
335
|
+
for (const message of this.#pendingSyncMessages) {
|
|
336
|
+
this.#processSyncMessage(message)
|
|
179
337
|
}
|
|
180
338
|
|
|
181
339
|
this.#pendingSyncMessages = []
|
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
import EventEmitter from "eventemitter3"
|
|
2
|
-
import {
|
|
3
|
-
import { MessagePayload } from "../network/NetworkAdapter.js"
|
|
2
|
+
import { Message, MessageContents } from "../network/messages.js"
|
|
4
3
|
|
|
5
4
|
export abstract class Synchronizer extends EventEmitter<SynchronizerEvents> {
|
|
6
|
-
abstract
|
|
7
|
-
peerId: PeerId,
|
|
8
|
-
channelId: ChannelId,
|
|
9
|
-
message: Uint8Array
|
|
10
|
-
): void
|
|
5
|
+
abstract receiveMessage(message: Message): void
|
|
11
6
|
}
|
|
12
7
|
|
|
13
8
|
export interface SynchronizerEvents {
|
|
14
|
-
message: (arg:
|
|
9
|
+
message: (arg: MessageContents) => void
|
|
15
10
|
}
|
package/src/types.ts
CHANGED
|
@@ -3,4 +3,7 @@ export type AutomergeUrl = string & { __documentUrl: true } // for opening / lin
|
|
|
3
3
|
export type BinaryDocumentId = Uint8Array & { __binaryDocumentId: true } // for storing / syncing
|
|
4
4
|
|
|
5
5
|
export type PeerId = string & { __peerId: false }
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
export type DistributiveOmit<T, K extends keyof any> = T extends any
|
|
8
|
+
? Omit<T, K>
|
|
9
|
+
: never
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { CollectionSynchronizer } from "../src/synchronizer/CollectionSynchronizer.js"
|
|
2
|
-
import { ChannelId, DocCollection, BinaryDocumentId, PeerId } from "../src"
|
|
3
1
|
import assert from "assert"
|
|
4
2
|
import { beforeEach } from "mocha"
|
|
5
|
-
import {
|
|
3
|
+
import { DocCollection, PeerId } from "../src"
|
|
4
|
+
import { CollectionSynchronizer } from "../src/synchronizer/CollectionSynchronizer.js"
|
|
6
5
|
|
|
7
6
|
describe("CollectionSynchronizer", () => {
|
|
8
7
|
let collection: DocCollection
|
|
@@ -21,9 +20,9 @@ describe("CollectionSynchronizer", () => {
|
|
|
21
20
|
const handle = collection.create()
|
|
22
21
|
synchronizer.addPeer("peer1" as PeerId)
|
|
23
22
|
|
|
24
|
-
synchronizer.once("message",
|
|
23
|
+
synchronizer.once("message", event => {
|
|
25
24
|
assert(event.targetId === "peer1")
|
|
26
|
-
assert(event.
|
|
25
|
+
assert(event.documentId === handle.documentId)
|
|
27
26
|
done()
|
|
28
27
|
})
|
|
29
28
|
|
|
@@ -33,9 +32,9 @@ describe("CollectionSynchronizer", () => {
|
|
|
33
32
|
it("starts synchronizing existing documents when a peer is added", done => {
|
|
34
33
|
const handle = collection.create()
|
|
35
34
|
synchronizer.addDocument(handle.documentId)
|
|
36
|
-
synchronizer.once("message",
|
|
35
|
+
synchronizer.once("message", event => {
|
|
37
36
|
assert(event.targetId === "peer1")
|
|
38
|
-
assert(event.
|
|
37
|
+
assert(event.documentId === handle.documentId)
|
|
39
38
|
done()
|
|
40
39
|
})
|
|
41
40
|
synchronizer.addPeer("peer1" as PeerId)
|
package/test/DocHandle.test.ts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import * as A from "@automerge/automerge"
|
|
2
2
|
import assert from "assert"
|
|
3
3
|
import { it } from "mocha"
|
|
4
|
-
import { DocHandle, DocHandleChangePayload
|
|
4
|
+
import { DocHandle, DocHandleChangePayload } from "../src"
|
|
5
5
|
import { pause } from "../src/helpers/pause"
|
|
6
6
|
import { TestDoc } from "./types.js"
|
|
7
7
|
import { generateAutomergeUrl, parseAutomergeUrl } from "../src/DocUrl"
|
|
8
|
+
import { eventPromise } from "../src/helpers/eventPromise"
|
|
9
|
+
import { decode } from "cbor-x"
|
|
8
10
|
|
|
9
11
|
describe("DocHandle", () => {
|
|
10
|
-
const TEST_ID = parseAutomergeUrl(generateAutomergeUrl()).
|
|
11
|
-
const BOGUS_ID = parseAutomergeUrl(generateAutomergeUrl()).
|
|
12
|
+
const TEST_ID = parseAutomergeUrl(generateAutomergeUrl()).documentId
|
|
13
|
+
const BOGUS_ID = parseAutomergeUrl(generateAutomergeUrl()).documentId
|
|
12
14
|
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
const binary = A.save(doc)
|
|
16
|
-
return binary
|
|
15
|
+
const docFromMockStorage = (doc: A.Doc<{ foo: string }>) => {
|
|
16
|
+
return A.change<{ foo: string }>(doc, d => (d.foo = "bar"))
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
it("should take the UUID passed into it", () => {
|
|
@@ -26,11 +26,11 @@ describe("DocHandle", () => {
|
|
|
26
26
|
assert.equal(handle.isReady(), false)
|
|
27
27
|
|
|
28
28
|
// simulate loading from storage
|
|
29
|
-
handle.
|
|
29
|
+
handle.update(doc => docFromMockStorage(doc))
|
|
30
30
|
|
|
31
31
|
assert.equal(handle.isReady(), true)
|
|
32
32
|
const doc = await handle.doc()
|
|
33
|
-
assert.equal(doc
|
|
33
|
+
assert.equal(doc?.foo, "bar")
|
|
34
34
|
})
|
|
35
35
|
|
|
36
36
|
it("should allow sync access to the doc", async () => {
|
|
@@ -38,7 +38,7 @@ describe("DocHandle", () => {
|
|
|
38
38
|
assert.equal(handle.isReady(), false)
|
|
39
39
|
|
|
40
40
|
// simulate loading from storage
|
|
41
|
-
handle.
|
|
41
|
+
handle.update(doc => docFromMockStorage(doc))
|
|
42
42
|
|
|
43
43
|
assert.equal(handle.isReady(), true)
|
|
44
44
|
const doc = await handle.doc()
|
|
@@ -56,12 +56,12 @@ describe("DocHandle", () => {
|
|
|
56
56
|
assert.equal(handle.isReady(), false)
|
|
57
57
|
|
|
58
58
|
// simulate loading from storage
|
|
59
|
-
handle.
|
|
59
|
+
handle.update(doc => docFromMockStorage(doc))
|
|
60
60
|
|
|
61
61
|
const doc = await handle.doc()
|
|
62
62
|
|
|
63
63
|
assert.equal(handle.isReady(), true)
|
|
64
|
-
assert.equal(doc
|
|
64
|
+
assert.equal(doc?.foo, "bar")
|
|
65
65
|
})
|
|
66
66
|
|
|
67
67
|
it("should block changes until ready()", async () => {
|
|
@@ -72,14 +72,14 @@ describe("DocHandle", () => {
|
|
|
72
72
|
assert.throws(() => handle.change(d => (d.foo = "baz")))
|
|
73
73
|
|
|
74
74
|
// simulate loading from storage
|
|
75
|
-
handle.
|
|
75
|
+
handle.update(doc => docFromMockStorage(doc))
|
|
76
76
|
|
|
77
77
|
// now we're in READY state so we can make changes
|
|
78
78
|
assert.equal(handle.isReady(), true)
|
|
79
79
|
handle.change(d => (d.foo = "pizza"))
|
|
80
80
|
|
|
81
81
|
const doc = await handle.doc()
|
|
82
|
-
assert.equal(doc
|
|
82
|
+
assert.equal(doc?.foo, "pizza")
|
|
83
83
|
})
|
|
84
84
|
|
|
85
85
|
it("should not be ready while requesting from the network", async () => {
|
|
@@ -90,7 +90,7 @@ describe("DocHandle", () => {
|
|
|
90
90
|
|
|
91
91
|
assert.equal(handle.docSync(), undefined)
|
|
92
92
|
assert.equal(handle.isReady(), false)
|
|
93
|
-
assert.throws(() => handle.change(
|
|
93
|
+
assert.throws(() => handle.change(_ => { }))
|
|
94
94
|
})
|
|
95
95
|
|
|
96
96
|
it("should become ready if the document is updated by the network", async () => {
|
|
@@ -106,7 +106,7 @@ describe("DocHandle", () => {
|
|
|
106
106
|
|
|
107
107
|
const doc = await handle.doc()
|
|
108
108
|
assert.equal(handle.isReady(), true)
|
|
109
|
-
assert.equal(doc
|
|
109
|
+
assert.equal(doc?.foo, "bar")
|
|
110
110
|
})
|
|
111
111
|
|
|
112
112
|
it("should emit a change message when changes happen", async () => {
|
|
@@ -121,7 +121,7 @@ describe("DocHandle", () => {
|
|
|
121
121
|
})
|
|
122
122
|
|
|
123
123
|
const doc = await handle.doc()
|
|
124
|
-
assert.equal(doc
|
|
124
|
+
assert.equal(doc?.foo, "bar")
|
|
125
125
|
|
|
126
126
|
const changePayload = await p
|
|
127
127
|
assert.deepStrictEqual(changePayload.doc, doc)
|
|
@@ -182,7 +182,7 @@ describe("DocHandle", () => {
|
|
|
182
182
|
})
|
|
183
183
|
|
|
184
184
|
const doc = await handle.doc()
|
|
185
|
-
assert.equal(doc
|
|
185
|
+
assert.equal(doc?.foo, "baz")
|
|
186
186
|
|
|
187
187
|
return p
|
|
188
188
|
})
|
|
@@ -197,7 +197,7 @@ describe("DocHandle", () => {
|
|
|
197
197
|
|
|
198
198
|
await p
|
|
199
199
|
const doc = await handle.doc()
|
|
200
|
-
assert.equal(doc
|
|
200
|
+
assert.equal(doc?.foo, "bar")
|
|
201
201
|
})
|
|
202
202
|
|
|
203
203
|
it("should not emit a patch message if no change happens", done => {
|
|
@@ -229,11 +229,11 @@ describe("DocHandle", () => {
|
|
|
229
229
|
const handle = new DocHandle<TestDoc>(TEST_ID, { timeoutDelay: 5 })
|
|
230
230
|
|
|
231
231
|
// simulate loading from storage before the timeout expires
|
|
232
|
-
handle.
|
|
232
|
+
handle.update(doc => docFromMockStorage(doc))
|
|
233
233
|
|
|
234
234
|
// now it should not time out
|
|
235
235
|
const doc = await handle.doc()
|
|
236
|
-
assert.equal(doc
|
|
236
|
+
assert.equal(doc?.foo, "bar")
|
|
237
237
|
})
|
|
238
238
|
|
|
239
239
|
it("should time out if the document is not updated from the network", async () => {
|
|
@@ -266,7 +266,7 @@ describe("DocHandle", () => {
|
|
|
266
266
|
await pause(5)
|
|
267
267
|
|
|
268
268
|
const doc = await handle.doc()
|
|
269
|
-
assert.equal(doc
|
|
269
|
+
assert.equal(doc?.foo, "bar")
|
|
270
270
|
})
|
|
271
271
|
|
|
272
272
|
it("should emit a delete event when deleted", async () => {
|
|
@@ -302,4 +302,18 @@ describe("DocHandle", () => {
|
|
|
302
302
|
|
|
303
303
|
assert(wasBar, "foo should have been bar as we changed at the old heads")
|
|
304
304
|
})
|
|
305
|
+
|
|
306
|
+
describe("ephemeral messaging", () => {
|
|
307
|
+
it("can broadcast a message for the network to send out", async () => {
|
|
308
|
+
const handle = new DocHandle<TestDoc>(TEST_ID, { isNew: true })
|
|
309
|
+
const message = { foo: "bar" }
|
|
310
|
+
|
|
311
|
+
const promise = eventPromise(handle, "ephemeral-message-outbound")
|
|
312
|
+
|
|
313
|
+
handle.broadcast(message)
|
|
314
|
+
|
|
315
|
+
const { data } = await promise
|
|
316
|
+
assert.deepStrictEqual(decode(data), message)
|
|
317
|
+
})
|
|
318
|
+
})
|
|
305
319
|
})
|