@automerge/automerge-repo 1.0.0-alpha.2 → 1.0.0-alpha.4

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 (80) hide show
  1. package/dist/DocCollection.d.ts +4 -2
  2. package/dist/DocCollection.d.ts.map +1 -1
  3. package/dist/DocCollection.js +20 -11
  4. package/dist/DocHandle.d.ts +34 -6
  5. package/dist/DocHandle.d.ts.map +1 -1
  6. package/dist/DocHandle.js +69 -9
  7. package/dist/DocUrl.d.ts +4 -4
  8. package/dist/DocUrl.d.ts.map +1 -1
  9. package/dist/DocUrl.js +9 -9
  10. package/dist/EphemeralData.d.ts +8 -16
  11. package/dist/EphemeralData.d.ts.map +1 -1
  12. package/dist/EphemeralData.js +1 -28
  13. package/dist/Repo.d.ts +0 -2
  14. package/dist/Repo.d.ts.map +1 -1
  15. package/dist/Repo.js +37 -39
  16. package/dist/helpers/cbor.d.ts +4 -0
  17. package/dist/helpers/cbor.d.ts.map +1 -0
  18. package/dist/helpers/cbor.js +8 -0
  19. package/dist/helpers/eventPromise.d.ts +1 -1
  20. package/dist/helpers/eventPromise.d.ts.map +1 -1
  21. package/dist/helpers/headsAreSame.d.ts +0 -1
  22. package/dist/helpers/headsAreSame.d.ts.map +1 -1
  23. package/dist/helpers/tests/network-adapter-tests.d.ts.map +1 -1
  24. package/dist/helpers/tests/network-adapter-tests.js +15 -13
  25. package/dist/index.d.ts +3 -1
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +1 -0
  28. package/dist/network/NetworkAdapter.d.ts +6 -15
  29. package/dist/network/NetworkAdapter.d.ts.map +1 -1
  30. package/dist/network/NetworkAdapter.js +1 -1
  31. package/dist/network/NetworkSubsystem.d.ts +9 -6
  32. package/dist/network/NetworkSubsystem.d.ts.map +1 -1
  33. package/dist/network/NetworkSubsystem.js +69 -32
  34. package/dist/network/messages.d.ts +57 -0
  35. package/dist/network/messages.d.ts.map +1 -0
  36. package/dist/network/messages.js +21 -0
  37. package/dist/storage/StorageSubsystem.d.ts +1 -1
  38. package/dist/storage/StorageSubsystem.d.ts.map +1 -1
  39. package/dist/storage/StorageSubsystem.js +2 -2
  40. package/dist/synchronizer/CollectionSynchronizer.d.ts +3 -2
  41. package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
  42. package/dist/synchronizer/CollectionSynchronizer.js +19 -13
  43. package/dist/synchronizer/DocSynchronizer.d.ts +9 -3
  44. package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
  45. package/dist/synchronizer/DocSynchronizer.js +149 -34
  46. package/dist/synchronizer/Synchronizer.d.ts +4 -5
  47. package/dist/synchronizer/Synchronizer.d.ts.map +1 -1
  48. package/dist/synchronizer/Synchronizer.js +1 -1
  49. package/dist/types.d.ts +1 -3
  50. package/dist/types.d.ts.map +1 -1
  51. package/fuzz/fuzz.ts +5 -5
  52. package/package.json +3 -3
  53. package/src/DocCollection.ts +23 -12
  54. package/src/DocHandle.ts +120 -13
  55. package/src/DocUrl.ts +10 -10
  56. package/src/EphemeralData.ts +6 -36
  57. package/src/Repo.ts +37 -55
  58. package/src/helpers/cbor.ts +10 -0
  59. package/src/helpers/eventPromise.ts +1 -1
  60. package/src/helpers/headsAreSame.ts +1 -1
  61. package/src/helpers/tests/network-adapter-tests.ts +18 -14
  62. package/src/index.ts +14 -2
  63. package/src/network/NetworkAdapter.ts +6 -22
  64. package/src/network/NetworkSubsystem.ts +94 -44
  65. package/src/network/messages.ts +123 -0
  66. package/src/storage/StorageSubsystem.ts +2 -2
  67. package/src/synchronizer/CollectionSynchronizer.ts +38 -19
  68. package/src/synchronizer/DocSynchronizer.ts +201 -43
  69. package/src/synchronizer/Synchronizer.ts +4 -9
  70. package/src/types.ts +4 -1
  71. package/test/CollectionSynchronizer.test.ts +6 -7
  72. package/test/DocCollection.test.ts +2 -2
  73. package/test/DocHandle.test.ts +32 -17
  74. package/test/DocSynchronizer.test.ts +85 -9
  75. package/test/Repo.test.ts +267 -63
  76. package/test/StorageSubsystem.test.ts +4 -5
  77. package/test/helpers/DummyNetworkAdapter.ts +12 -3
  78. package/test/helpers/DummyStorageAdapter.ts +1 -1
  79. package/tsconfig.json +4 -3
  80. package/test/EphemeralData.test.ts +0 -44
@@ -1,9 +1,28 @@
1
- import * as A from "@automerge/automerge"
2
- import { DocHandle, READY, REQUESTING } from "../DocHandle.js"
3
- import { ChannelId, PeerId } from "../types.js"
1
+ import * as A from "@automerge/automerge/next"
2
+ import {
3
+ AWAITING_NETWORK,
4
+ DocHandle,
5
+ DocHandleOutboundEphemeralMessagePayload,
6
+ READY,
7
+ REQUESTING,
8
+ UNAVAILABLE,
9
+ } from "../DocHandle.js"
10
+ import { PeerId } from "../types.js"
4
11
  import { Synchronizer } from "./Synchronizer.js"
5
12
 
6
13
  import debug from "debug"
14
+ import {
15
+ EphemeralMessage,
16
+ isDocumentUnavailableMessage,
17
+ isRequestMessage,
18
+ Message,
19
+ RequestMessage,
20
+ SynchronizerMessage,
21
+ SyncMessage,
22
+ } from "../network/messages.js"
23
+
24
+ type PeerDocumentStatus = "unknown" | "has" | "unavailable" | "wants"
25
+ import { decode } from "cbor-x"
7
26
 
8
27
  /**
9
28
  * DocSynchronizer takes a handle to an Automerge document, and receives & dispatches sync messages
@@ -17,10 +36,14 @@ export class DocSynchronizer extends Synchronizer {
17
36
  /** Active peers */
18
37
  #peers: PeerId[] = []
19
38
 
39
+ #peerDocumentStatuses: Record<PeerId, PeerDocumentStatus> = {}
40
+
20
41
  /** Sync state for each peer we've communicated with (including inactive peers) */
21
42
  #syncStates: Record<PeerId, A.SyncState> = {}
22
43
 
23
- #pendingSyncMessages: Array<{ peerId: PeerId; message: Uint8Array }> = []
44
+ #pendingSyncMessages: Array<SyncMessage | RequestMessage> = []
45
+
46
+ #syncStarted = false
24
47
 
25
48
  constructor(private handle: DocHandle<any>) {
26
49
  super()
@@ -31,6 +54,10 @@ export class DocSynchronizer extends Synchronizer {
31
54
 
32
55
  handle.on("change", () => this.#syncWithPeers())
33
56
 
57
+ handle.on("ephemeral-message-outbound", payload =>
58
+ this.#broadcastToPeers(payload)
59
+ )
60
+
34
61
  // Process pending sync messages immediately after the handle becomes ready.
35
62
  void (async () => {
36
63
  await handle.doc([READY, REQUESTING])
@@ -38,6 +65,10 @@ export class DocSynchronizer extends Synchronizer {
38
65
  })()
39
66
  }
40
67
 
68
+ get peerStates() {
69
+ return this.#peerDocumentStatuses
70
+ }
71
+
41
72
  get documentId() {
42
73
  return this.handle.documentId
43
74
  }
@@ -47,15 +78,37 @@ export class DocSynchronizer extends Synchronizer {
47
78
  async #syncWithPeers() {
48
79
  this.#log(`syncWithPeers`)
49
80
  const doc = await this.handle.doc()
81
+ if (doc === undefined) return
50
82
  this.#peers.forEach(peerId => this.#sendSyncMessage(peerId, doc))
51
83
  }
52
84
 
85
+ async #broadcastToPeers({ data }: DocHandleOutboundEphemeralMessagePayload) {
86
+ this.#log(`broadcastToPeers`, this.#peers)
87
+ this.#peers.forEach(peerId => this.#sendEphemeralMessage(peerId, data))
88
+ }
89
+
90
+ #sendEphemeralMessage(peerId: PeerId, data: Uint8Array) {
91
+ this.#log(`sendEphemeralMessage ->${peerId}`)
92
+
93
+ this.emit("message", {
94
+ type: "ephemeral",
95
+ targetId: peerId,
96
+ documentId: this.handle.documentId,
97
+ data,
98
+ })
99
+ }
100
+
53
101
  #getSyncState(peerId: PeerId) {
54
102
  if (!this.#peers.includes(peerId)) {
55
103
  this.#log("adding a new peer", peerId)
56
104
  this.#peers.push(peerId)
57
105
  }
58
106
 
107
+ // when a peer is added, we don't know if it has the document or not
108
+ if (!(peerId in this.#peerDocumentStatuses)) {
109
+ this.#peerDocumentStatuses[peerId] = "unknown"
110
+ }
111
+
59
112
  return this.#syncStates[peerId] ?? A.initSyncState()
60
113
  }
61
114
 
@@ -77,16 +130,35 @@ export class DocSynchronizer extends Synchronizer {
77
130
  if (message) {
78
131
  this.#logMessage(`sendSyncMessage 🡒 ${peerId}`, message)
79
132
 
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]`)
133
+ const decoded = A.decodeSyncMessage(message)
134
+
135
+ if (
136
+ !this.handle.isReady() &&
137
+ decoded.heads.length === 0 &&
138
+ newSyncState.sharedHeads.length === 0 &&
139
+ !Object.values(this.#peerDocumentStatuses).includes("has") &&
140
+ this.#peerDocumentStatuses[peerId] === "unknown"
141
+ ) {
142
+ // we don't have the document (or access to it), so we request it
143
+ this.emit("message", {
144
+ type: "request",
145
+ targetId: peerId,
146
+ documentId: this.handle.documentId,
147
+ data: message,
148
+ })
149
+ } else {
150
+ this.emit("message", {
151
+ type: "sync",
152
+ targetId: peerId,
153
+ data: message,
154
+ documentId: this.handle.documentId,
155
+ })
156
+ }
157
+
158
+ // if we have sent heads, then the peer now has or will have the document
159
+ if (decoded.heads.length > 0) {
160
+ this.#peerDocumentStatuses[peerId] = "has"
161
+ }
90
162
  }
91
163
  }
92
164
 
@@ -103,9 +175,9 @@ export class DocSynchronizer extends Synchronizer {
103
175
 
104
176
  // expanding is expensive, so only do it if we're logging at this level
105
177
  const expanded = this.#opsLog.enabled
106
- ? decoded.changes.flatMap(change =>
107
- A.decodeChange(change).ops.map(op => JSON.stringify(op))
108
- )
178
+ ? decoded.changes.flatMap((change: A.Change) =>
179
+ A.decodeChange(change).ops.map((op: any) => JSON.stringify(op))
180
+ )
109
181
  : null
110
182
  this.#opsLog(logText, expanded)
111
183
  }
@@ -116,21 +188,32 @@ export class DocSynchronizer extends Synchronizer {
116
188
  return this.#peers.includes(peerId)
117
189
  }
118
190
 
119
- beginSync(peerId: PeerId) {
120
- this.#log(`beginSync: ${peerId}`)
191
+ beginSync(peerIds: PeerId[]) {
192
+ this.#log(`beginSync: ${peerIds.join(", ")}`)
121
193
 
122
- // At this point if we don't have anything in our storage, we need to use an empty doc to sync
123
- // with; but we don't want to surface that state to the front end
124
- void this.handle.doc([READY, REQUESTING]).then(doc => {
125
- // HACK: if we have a sync state already, we round-trip it through the encoding system to make
126
- // sure state is preserved. This prevents an infinite loop caused by failed attempts to send
127
- // messages during disconnection.
128
- // TODO: cover that case with a test and remove this hack
194
+ // HACK: if we have a sync state already, we round-trip it through the encoding system to make
195
+ // sure state is preserved. This prevents an infinite loop caused by failed attempts to send
196
+ // messages during disconnection.
197
+ // TODO: cover that case with a test and remove this hack
198
+ peerIds.forEach(peerId => {
129
199
  const syncStateRaw = this.#getSyncState(peerId)
130
200
  const syncState = A.decodeSyncState(A.encodeSyncState(syncStateRaw))
131
201
  this.#setSyncState(peerId, syncState)
202
+ })
203
+
204
+ // At this point if we don't have anything in our storage, we need to use an empty doc to sync
205
+ // with; but we don't want to surface that state to the front end
206
+ void this.handle.doc([READY, REQUESTING, UNAVAILABLE]).then(doc => {
207
+
208
+ // we register out peers first, then say that sync has started
209
+ this.#syncStarted = true
210
+ this.#checkDocUnavailable()
211
+
212
+ if (doc === undefined) return
132
213
 
133
- this.#sendSyncMessage(peerId, doc)
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
- import EventEmitter from "eventemitter3"
2
- import { ChannelId, PeerId } from "../types.js"
3
- import { MessagePayload } from "../network/NetworkAdapter.js"
1
+ import { EventEmitter } from "eventemitter3"
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/index.js"
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,7 +1,7 @@
1
1
  import assert from "assert"
2
- import { DocCollection, BinaryDocumentId } from "../src"
2
+ import { DocCollection, BinaryDocumentId } from "../src/index.js"
3
3
  import { TestDoc } from "./types.js"
4
- import { generateAutomergeUrl, stringifyAutomergeUrl } from "../src/DocUrl"
4
+ import { generateAutomergeUrl, stringifyAutomergeUrl } from "../src/DocUrl.js"
5
5
 
6
6
  const MISSING_DOCID = generateAutomergeUrl()
7
7
 
@@ -1,14 +1,16 @@
1
- import * as A from "@automerge/automerge"
1
+ import * as A from "@automerge/automerge/next"
2
2
  import assert from "assert"
3
3
  import { it } from "mocha"
4
- import { DocHandle, DocHandleChangePayload } from "../src"
5
- import { pause } from "../src/helpers/pause"
4
+ import { DocHandle, DocHandleChangePayload } from "../src/index.js"
5
+ import { pause } from "../src/helpers/pause.js"
6
6
  import { TestDoc } from "./types.js"
7
- import { generateAutomergeUrl, parseAutomergeUrl } from "../src/DocUrl"
7
+ import { generateAutomergeUrl, parseAutomergeUrl } from "../src/DocUrl.js"
8
+ import { eventPromise } from "../src/helpers/eventPromise.js"
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
15
  const docFromMockStorage = (doc: A.Doc<{ foo: string }>) => {
14
16
  return A.change<{ foo: string }>(doc, d => (d.foo = "bar"))
@@ -28,8 +30,7 @@ describe("DocHandle", () => {
28
30
 
29
31
  assert.equal(handle.isReady(), true)
30
32
  const doc = await handle.doc()
31
- console.log("DOC", JSON.stringify(doc))
32
- assert.equal(doc.foo, "bar")
33
+ assert.equal(doc?.foo, "bar")
33
34
  })
34
35
 
35
36
  it("should allow sync access to the doc", async () => {
@@ -60,7 +61,7 @@ describe("DocHandle", () => {
60
61
  const doc = await handle.doc()
61
62
 
62
63
  assert.equal(handle.isReady(), true)
63
- assert.equal(doc.foo, "bar")
64
+ assert.equal(doc?.foo, "bar")
64
65
  })
65
66
 
66
67
  it("should block changes until ready()", async () => {
@@ -78,7 +79,7 @@ describe("DocHandle", () => {
78
79
  handle.change(d => (d.foo = "pizza"))
79
80
 
80
81
  const doc = await handle.doc()
81
- assert.equal(doc.foo, "pizza")
82
+ assert.equal(doc?.foo, "pizza")
82
83
  })
83
84
 
84
85
  it("should not be ready while requesting from the network", async () => {
@@ -89,7 +90,7 @@ describe("DocHandle", () => {
89
90
 
90
91
  assert.equal(handle.docSync(), undefined)
91
92
  assert.equal(handle.isReady(), false)
92
- assert.throws(() => handle.change(h => {}))
93
+ assert.throws(() => handle.change(_ => { }))
93
94
  })
94
95
 
95
96
  it("should become ready if the document is updated by the network", async () => {
@@ -105,7 +106,7 @@ describe("DocHandle", () => {
105
106
 
106
107
  const doc = await handle.doc()
107
108
  assert.equal(handle.isReady(), true)
108
- assert.equal(doc.foo, "bar")
109
+ assert.equal(doc?.foo, "bar")
109
110
  })
110
111
 
111
112
  it("should emit a change message when changes happen", async () => {
@@ -120,7 +121,7 @@ describe("DocHandle", () => {
120
121
  })
121
122
 
122
123
  const doc = await handle.doc()
123
- assert.equal(doc.foo, "bar")
124
+ assert.equal(doc?.foo, "bar")
124
125
 
125
126
  const changePayload = await p
126
127
  assert.deepStrictEqual(changePayload.doc, doc)
@@ -181,7 +182,7 @@ describe("DocHandle", () => {
181
182
  })
182
183
 
183
184
  const doc = await handle.doc()
184
- assert.equal(doc.foo, "baz")
185
+ assert.equal(doc?.foo, "baz")
185
186
 
186
187
  return p
187
188
  })
@@ -196,7 +197,7 @@ describe("DocHandle", () => {
196
197
 
197
198
  await p
198
199
  const doc = await handle.doc()
199
- assert.equal(doc.foo, "bar")
200
+ assert.equal(doc?.foo, "bar")
200
201
  })
201
202
 
202
203
  it("should not emit a patch message if no change happens", done => {
@@ -232,7 +233,7 @@ describe("DocHandle", () => {
232
233
 
233
234
  // now it should not time out
234
235
  const doc = await handle.doc()
235
- assert.equal(doc.foo, "bar")
236
+ assert.equal(doc?.foo, "bar")
236
237
  })
237
238
 
238
239
  it("should time out if the document is not updated from the network", async () => {
@@ -265,7 +266,7 @@ describe("DocHandle", () => {
265
266
  await pause(5)
266
267
 
267
268
  const doc = await handle.doc()
268
- assert.equal(doc.foo, "bar")
269
+ assert.equal(doc?.foo, "bar")
269
270
  })
270
271
 
271
272
  it("should emit a delete event when deleted", async () => {
@@ -301,4 +302,18 @@ describe("DocHandle", () => {
301
302
 
302
303
  assert(wasBar, "foo should have been bar as we changed at the old heads")
303
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
+ })
304
319
  })