@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.
Files changed (68) hide show
  1. package/dist/DocCollection.d.ts +2 -1
  2. package/dist/DocCollection.d.ts.map +1 -1
  3. package/dist/DocCollection.js +17 -8
  4. package/dist/DocHandle.d.ts +27 -7
  5. package/dist/DocHandle.d.ts.map +1 -1
  6. package/dist/DocHandle.js +47 -23
  7. package/dist/DocUrl.d.ts +3 -3
  8. package/dist/DocUrl.js +9 -9
  9. package/dist/EphemeralData.d.ts +8 -16
  10. package/dist/EphemeralData.d.ts.map +1 -1
  11. package/dist/EphemeralData.js +1 -28
  12. package/dist/Repo.d.ts +0 -2
  13. package/dist/Repo.d.ts.map +1 -1
  14. package/dist/Repo.js +18 -36
  15. package/dist/helpers/headsAreSame.d.ts +2 -2
  16. package/dist/helpers/headsAreSame.d.ts.map +1 -1
  17. package/dist/helpers/headsAreSame.js +1 -4
  18. package/dist/helpers/tests/network-adapter-tests.d.ts.map +1 -1
  19. package/dist/helpers/tests/network-adapter-tests.js +15 -13
  20. package/dist/index.d.ts +2 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/network/NetworkAdapter.d.ts +4 -13
  23. package/dist/network/NetworkAdapter.d.ts.map +1 -1
  24. package/dist/network/NetworkSubsystem.d.ts +5 -4
  25. package/dist/network/NetworkSubsystem.d.ts.map +1 -1
  26. package/dist/network/NetworkSubsystem.js +39 -25
  27. package/dist/network/messages.d.ts +57 -0
  28. package/dist/network/messages.d.ts.map +1 -0
  29. package/dist/network/messages.js +21 -0
  30. package/dist/storage/StorageSubsystem.d.ts +2 -2
  31. package/dist/storage/StorageSubsystem.d.ts.map +1 -1
  32. package/dist/storage/StorageSubsystem.js +36 -6
  33. package/dist/synchronizer/CollectionSynchronizer.d.ts +3 -2
  34. package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
  35. package/dist/synchronizer/CollectionSynchronizer.js +19 -13
  36. package/dist/synchronizer/DocSynchronizer.d.ts +9 -3
  37. package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
  38. package/dist/synchronizer/DocSynchronizer.js +145 -29
  39. package/dist/synchronizer/Synchronizer.d.ts +3 -4
  40. package/dist/synchronizer/Synchronizer.d.ts.map +1 -1
  41. package/dist/types.d.ts +1 -3
  42. package/dist/types.d.ts.map +1 -1
  43. package/fuzz/fuzz.ts +4 -4
  44. package/package.json +3 -3
  45. package/src/DocCollection.ts +19 -9
  46. package/src/DocHandle.ts +82 -37
  47. package/src/DocUrl.ts +9 -9
  48. package/src/EphemeralData.ts +6 -36
  49. package/src/Repo.ts +20 -52
  50. package/src/helpers/headsAreSame.ts +3 -5
  51. package/src/helpers/tests/network-adapter-tests.ts +18 -14
  52. package/src/index.ts +12 -2
  53. package/src/network/NetworkAdapter.ts +4 -20
  54. package/src/network/NetworkSubsystem.ts +61 -38
  55. package/src/network/messages.ts +123 -0
  56. package/src/storage/StorageSubsystem.ts +42 -6
  57. package/src/synchronizer/CollectionSynchronizer.ts +38 -19
  58. package/src/synchronizer/DocSynchronizer.ts +196 -38
  59. package/src/synchronizer/Synchronizer.ts +3 -8
  60. package/src/types.ts +4 -1
  61. package/test/CollectionSynchronizer.test.ts +6 -7
  62. package/test/DocHandle.test.ts +36 -22
  63. package/test/DocSynchronizer.test.ts +85 -9
  64. package/test/Repo.test.ts +279 -59
  65. package/test/StorageSubsystem.test.ts +9 -9
  66. package/test/helpers/DummyNetworkAdapter.ts +1 -1
  67. package/tsconfig.json +2 -1
  68. package/test/EphemeralData.test.ts +0 -44
@@ -1,9 +1,27 @@
1
1
  import * as A from "@automerge/automerge"
2
- import { DocHandle, READY, REQUESTING } from "../DocHandle.js"
3
- import { ChannelId, PeerId } from "../types.js"
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<{ peerId: PeerId; message: Uint8Array }> = []
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 channelId = this.handle.documentId as string as ChannelId
81
-
82
- this.emit("message", {
83
- targetId: peerId,
84
- channelId,
85
- message,
86
- broadcast: false,
87
- })
88
- } else {
89
- this.#log(`sendSyncMessage ->${peerId} [no message generated]`)
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
- A.decodeChange(change).ops.map(op => JSON.stringify(op))
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(peerId: PeerId) {
120
- this.#log(`beginSync: ${peerId}`)
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
- const syncStateRaw = this.#getSyncState(peerId)
130
- const syncState = A.decodeSyncState(A.encodeSyncState(syncStateRaw))
131
- this.#setSyncState(peerId, syncState)
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
- this.#sendSyncMessage(peerId, doc)
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
- receiveSyncMessage(
143
- peerId: PeerId,
144
- channelId: ChannelId,
145
- message: Uint8Array
146
- ) {
147
- if ((channelId as string) !== (this.handle.documentId as string))
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({ peerId, message })
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(peerId, message)
277
+ this.#processSyncMessage(message)
158
278
  }
159
279
 
160
- #processSyncMessage(peerId: PeerId, message: Uint8Array) {
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(peerId),
165
- message
295
+ this.#getSyncState(message.senderId),
296
+ message.data
166
297
  )
167
298
 
168
- this.#setSyncState(peerId, newSyncState)
299
+ this.#setSyncState(message.senderId, newSyncState)
169
300
 
170
301
  // respond to just this peer (as required)
171
- this.#sendSyncMessage(peerId, doc)
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 { peerId, message } of this.#pendingSyncMessages) {
178
- this.#processSyncMessage(peerId, message)
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 { ChannelId, PeerId } from "../types.js"
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 receiveSyncMessage(
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: MessagePayload) => void
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
- export type ChannelId = string & { __channelId: false }
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 { MessagePayload } from "../src/network/NetworkAdapter.js"
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", (event: MessagePayload) => {
23
+ synchronizer.once("message", event => {
25
24
  assert(event.targetId === "peer1")
26
- assert(event.channelId === (handle.documentId as unknown as ChannelId))
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", (event: MessagePayload) => {
35
+ synchronizer.once("message", event => {
37
36
  assert(event.targetId === "peer1")
38
- assert(event.channelId === (handle.documentId as unknown as ChannelId))
37
+ assert(event.documentId === handle.documentId)
39
38
  done()
40
39
  })
41
40
  synchronizer.addPeer("peer1" as PeerId)
@@ -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, BinaryDocumentId } from "../src"
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()).encodedDocumentId
11
- const BOGUS_ID = parseAutomergeUrl(generateAutomergeUrl()).encodedDocumentId
12
+ const TEST_ID = parseAutomergeUrl(generateAutomergeUrl()).documentId
13
+ const BOGUS_ID = parseAutomergeUrl(generateAutomergeUrl()).documentId
12
14
 
13
- const binaryFromMockStorage = () => {
14
- const doc = A.change<{ foo: string }>(A.init(), d => (d.foo = "bar"))
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.load(binaryFromMockStorage())
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.foo, "bar")
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.load(binaryFromMockStorage())
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.load(binaryFromMockStorage())
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.foo, "bar")
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.load(binaryFromMockStorage())
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.foo, "pizza")
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(h => {}))
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.foo, "bar")
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.foo, "bar")
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.foo, "baz")
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.foo, "bar")
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.load(binaryFromMockStorage())
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.foo, "bar")
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.foo, "bar")
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
  })