@automerge/automerge-repo 1.0.6 → 1.0.8
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/.eslintrc +1 -1
- package/dist/DocHandle.d.ts +8 -8
- package/dist/DocHandle.d.ts.map +1 -1
- package/dist/DocHandle.js +4 -8
- package/dist/DocUrl.d.ts.map +1 -1
- package/dist/Repo.d.ts +6 -3
- package/dist/Repo.d.ts.map +1 -1
- package/dist/Repo.js +20 -19
- package/dist/helpers/cbor.d.ts +2 -2
- package/dist/helpers/cbor.d.ts.map +1 -1
- package/dist/helpers/cbor.js +1 -1
- package/dist/helpers/debounce.d.ts +14 -0
- package/dist/helpers/debounce.d.ts.map +1 -0
- package/dist/helpers/debounce.js +21 -0
- package/dist/helpers/pause.d.ts.map +1 -1
- package/dist/helpers/pause.js +3 -1
- package/dist/helpers/tests/network-adapter-tests.d.ts.map +1 -1
- package/dist/helpers/tests/network-adapter-tests.js +2 -2
- package/dist/helpers/throttle.d.ts +28 -0
- package/dist/helpers/throttle.d.ts.map +1 -0
- package/dist/helpers/throttle.js +39 -0
- package/dist/index.d.ts +11 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/network/NetworkAdapter.d.ts +3 -3
- package/dist/network/NetworkAdapter.d.ts.map +1 -1
- package/dist/network/NetworkSubsystem.d.ts +2 -2
- package/dist/network/NetworkSubsystem.d.ts.map +1 -1
- package/dist/network/NetworkSubsystem.js +30 -18
- package/dist/network/messages.d.ts +38 -68
- package/dist/network/messages.d.ts.map +1 -1
- package/dist/network/messages.js +13 -21
- package/dist/storage/StorageSubsystem.d.ts +1 -1
- package/dist/storage/StorageSubsystem.d.ts.map +1 -1
- package/dist/storage/StorageSubsystem.js +9 -9
- package/dist/synchronizer/CollectionSynchronizer.d.ts +3 -3
- package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/CollectionSynchronizer.js +3 -3
- package/dist/synchronizer/DocSynchronizer.d.ts +4 -3
- package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/DocSynchronizer.js +25 -34
- package/dist/synchronizer/Synchronizer.d.ts +2 -2
- package/dist/synchronizer/Synchronizer.d.ts.map +1 -1
- package/dist/types.d.ts +5 -1
- package/dist/types.d.ts.map +1 -1
- package/fuzz/fuzz.ts +8 -6
- package/fuzz/tsconfig.json +8 -0
- package/package.json +5 -13
- package/src/DocHandle.ts +12 -15
- package/src/DocUrl.ts +3 -1
- package/src/Repo.ts +36 -29
- package/src/helpers/cbor.ts +4 -4
- package/src/helpers/debounce.ts +25 -0
- package/src/helpers/headsAreSame.ts +1 -1
- package/src/helpers/pause.ts +7 -2
- package/src/helpers/tests/network-adapter-tests.ts +3 -3
- package/src/helpers/throttle.ts +43 -0
- package/src/helpers/withTimeout.ts +2 -2
- package/src/index.ts +36 -29
- package/src/network/NetworkAdapter.ts +7 -3
- package/src/network/NetworkSubsystem.ts +31 -23
- package/src/network/messages.ts +88 -151
- package/src/storage/StorageSubsystem.ts +12 -12
- package/src/synchronizer/CollectionSynchronizer.ts +7 -16
- package/src/synchronizer/DocSynchronizer.ts +42 -53
- package/src/synchronizer/Synchronizer.ts +2 -2
- package/src/types.ts +8 -3
- package/test/CollectionSynchronizer.test.ts +58 -53
- package/test/DocHandle.test.ts +35 -36
- package/test/DocSynchronizer.test.ts +5 -8
- package/test/Network.test.ts +1 -0
- package/test/Repo.test.ts +229 -158
- package/test/StorageSubsystem.test.ts +6 -9
- package/test/helpers/DummyNetworkAdapter.ts +9 -4
- package/test/helpers/DummyStorageAdapter.ts +4 -2
- package/test/tsconfig.json +8 -0
- package/typedoc.json +3 -3
- package/.mocharc.json +0 -5
- package/dist/EphemeralData.d.ts +0 -20
- package/dist/EphemeralData.d.ts.map +0 -1
- package/dist/EphemeralData.js +0 -1
- package/src/EphemeralData.ts +0 -17
package/src/DocHandle.ts
CHANGED
|
@@ -14,12 +14,11 @@ import {
|
|
|
14
14
|
TypegenDisabled,
|
|
15
15
|
} from "xstate"
|
|
16
16
|
import { waitFor } from "xstate/lib/waitFor.js"
|
|
17
|
-
import { headsAreSame } from "./helpers/headsAreSame.js"
|
|
18
|
-
import { pause } from "./helpers/pause.js"
|
|
19
|
-
import { TimeoutError, withTimeout } from "./helpers/withTimeout.js"
|
|
20
|
-
import type { DocumentId, PeerId, AutomergeUrl } from "./types.js"
|
|
21
17
|
import { stringifyAutomergeUrl } from "./DocUrl.js"
|
|
22
18
|
import { encode } from "./helpers/cbor.js"
|
|
19
|
+
import { headsAreSame } from "./helpers/headsAreSame.js"
|
|
20
|
+
import { withTimeout } from "./helpers/withTimeout.js"
|
|
21
|
+
import type { AutomergeUrl, DocumentId, PeerId } from "./types.js"
|
|
23
22
|
|
|
24
23
|
/** DocHandle is a wrapper around a single Automerge document that lets us
|
|
25
24
|
* listen for changes and notify the network and storage of new changes.
|
|
@@ -290,14 +289,12 @@ export class DocHandle<T> //
|
|
|
290
289
|
async doc(
|
|
291
290
|
awaitStates: HandleState[] = [READY, UNAVAILABLE]
|
|
292
291
|
): Promise<A.Doc<T> | undefined> {
|
|
293
|
-
await pause() // yield one tick because reasons
|
|
294
292
|
try {
|
|
295
293
|
// wait for the document to enter one of the desired states
|
|
296
294
|
await this.#statePromise(awaitStates)
|
|
297
295
|
} catch (error) {
|
|
298
|
-
if (
|
|
299
|
-
|
|
300
|
-
else throw error
|
|
296
|
+
// if we timed out (or the load has already failed), return undefined
|
|
297
|
+
return undefined
|
|
301
298
|
}
|
|
302
299
|
// Return the document
|
|
303
300
|
return !this.isUnavailable() ? this.#doc : undefined
|
|
@@ -431,7 +428,7 @@ export class DocHandle<T> //
|
|
|
431
428
|
* a user could have multiple tabs open and would appear as multiple PeerIds.
|
|
432
429
|
* every message source must have a unique PeerId.
|
|
433
430
|
*/
|
|
434
|
-
broadcast(message:
|
|
431
|
+
broadcast(message: unknown) {
|
|
435
432
|
this.emit("ephemeral-message-outbound", {
|
|
436
433
|
handle: this,
|
|
437
434
|
data: encode(message),
|
|
@@ -474,14 +471,14 @@ export interface DocHandleChangePayload<T> {
|
|
|
474
471
|
patchInfo: A.PatchInfo<T>
|
|
475
472
|
}
|
|
476
473
|
|
|
477
|
-
export interface DocHandleEphemeralMessagePayload {
|
|
478
|
-
handle: DocHandle<
|
|
474
|
+
export interface DocHandleEphemeralMessagePayload<T> {
|
|
475
|
+
handle: DocHandle<T>
|
|
479
476
|
senderId: PeerId
|
|
480
477
|
message: unknown
|
|
481
478
|
}
|
|
482
479
|
|
|
483
|
-
export interface DocHandleOutboundEphemeralMessagePayload {
|
|
484
|
-
handle: DocHandle<
|
|
480
|
+
export interface DocHandleOutboundEphemeralMessagePayload<T> {
|
|
481
|
+
handle: DocHandle<T>
|
|
485
482
|
data: Uint8Array
|
|
486
483
|
}
|
|
487
484
|
|
|
@@ -490,9 +487,9 @@ export interface DocHandleEvents<T> {
|
|
|
490
487
|
change: (payload: DocHandleChangePayload<T>) => void
|
|
491
488
|
delete: (payload: DocHandleDeletePayload<T>) => void
|
|
492
489
|
unavailable: (payload: DocHandleDeletePayload<T>) => void
|
|
493
|
-
"ephemeral-message": (payload: DocHandleEphemeralMessagePayload) => void
|
|
490
|
+
"ephemeral-message": (payload: DocHandleEphemeralMessagePayload<T>) => void
|
|
494
491
|
"ephemeral-message-outbound": (
|
|
495
|
-
payload: DocHandleOutboundEphemeralMessagePayload
|
|
492
|
+
payload: DocHandleOutboundEphemeralMessagePayload<T>
|
|
496
493
|
) => void
|
|
497
494
|
}
|
|
498
495
|
|
package/src/DocUrl.ts
CHANGED
|
@@ -29,7 +29,9 @@ export const parseAutomergeUrl = (url: AutomergeUrl) => {
|
|
|
29
29
|
*/
|
|
30
30
|
export const stringifyAutomergeUrl = ({
|
|
31
31
|
documentId,
|
|
32
|
-
}: {
|
|
32
|
+
}: {
|
|
33
|
+
documentId: DocumentId | BinaryDocumentId
|
|
34
|
+
}): AutomergeUrl => {
|
|
33
35
|
if (documentId instanceof Uint8Array)
|
|
34
36
|
return (urlPrefix +
|
|
35
37
|
binaryToDocumentId(documentId as BinaryDocumentId)) as AutomergeUrl
|
package/src/Repo.ts
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
|
+
import { next as Automerge } from "@automerge/automerge"
|
|
1
2
|
import debug from "debug"
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { StorageAdapter } from "./storage/StorageAdapter.js"
|
|
5
|
-
import { StorageSubsystem } from "./storage/StorageSubsystem.js"
|
|
6
|
-
import { CollectionSynchronizer } from "./synchronizer/CollectionSynchronizer.js"
|
|
7
|
-
import { type AutomergeUrl, DocumentId, PeerId } from "./types.js"
|
|
8
|
-
import { v4 as uuid } from "uuid"
|
|
3
|
+
import { EventEmitter } from "eventemitter3"
|
|
4
|
+
import { DocHandle, DocHandleEncodedChangePayload } from "./DocHandle.js"
|
|
9
5
|
import {
|
|
10
|
-
parseAutomergeUrl,
|
|
11
6
|
generateAutomergeUrl,
|
|
12
7
|
isValidAutomergeUrl,
|
|
8
|
+
parseAutomergeUrl,
|
|
13
9
|
parseLegacyUUID,
|
|
14
10
|
} from "./DocUrl.js"
|
|
15
|
-
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
11
|
+
import { throttle } from "./helpers/throttle.js"
|
|
12
|
+
import { NetworkAdapter } from "./network/NetworkAdapter.js"
|
|
13
|
+
import { NetworkSubsystem } from "./network/NetworkSubsystem.js"
|
|
14
|
+
import { StorageAdapter } from "./storage/StorageAdapter.js"
|
|
15
|
+
import { StorageSubsystem } from "./storage/StorageSubsystem.js"
|
|
16
|
+
import { CollectionSynchronizer } from "./synchronizer/CollectionSynchronizer.js"
|
|
17
|
+
import { DocumentId, PeerId, type AutomergeUrl } from "./types.js"
|
|
19
18
|
|
|
20
19
|
/** A Repo is a collection of documents with networking, syncing, and storage capabilities. */
|
|
21
20
|
/** The `Repo` is the main entry point of this library
|
|
@@ -32,6 +31,11 @@ export class Repo extends EventEmitter<RepoEvents> {
|
|
|
32
31
|
networkSubsystem: NetworkSubsystem
|
|
33
32
|
/** @hidden */
|
|
34
33
|
storageSubsystem?: StorageSubsystem
|
|
34
|
+
|
|
35
|
+
/** The debounce rate is adjustable on the repo. */
|
|
36
|
+
/** @hidden */
|
|
37
|
+
saveDebounceRate = 100
|
|
38
|
+
|
|
35
39
|
#handleCache: Record<DocumentId, DocHandle<any>> = {}
|
|
36
40
|
|
|
37
41
|
/** By default, we share generously with all peers. */
|
|
@@ -49,10 +53,17 @@ export class Repo extends EventEmitter<RepoEvents> {
|
|
|
49
53
|
// up a document by ID. We listen for it in order to wire up storage and network synchronization.
|
|
50
54
|
this.on("document", async ({ handle, isNew }) => {
|
|
51
55
|
if (storageSubsystem) {
|
|
52
|
-
// Save when the document changes
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
// Save when the document changes, but no more often than saveDebounceRate.
|
|
57
|
+
const saveFn = ({
|
|
58
|
+
handle,
|
|
59
|
+
doc,
|
|
60
|
+
}: DocHandleEncodedChangePayload<any>) => {
|
|
61
|
+
void storageSubsystem.saveDoc(handle.documentId, doc)
|
|
62
|
+
}
|
|
63
|
+
const debouncedSaveFn = handle.on(
|
|
64
|
+
"heads-changed",
|
|
65
|
+
throttle(saveFn, this.saveDebounceRate)
|
|
66
|
+
)
|
|
56
67
|
|
|
57
68
|
if (isNew) {
|
|
58
69
|
// this is a new document, immediately save it
|
|
@@ -106,9 +117,9 @@ export class Repo extends EventEmitter<RepoEvents> {
|
|
|
106
117
|
// The synchronizer uses the network subsystem to keep documents in sync with peers.
|
|
107
118
|
const synchronizer = new CollectionSynchronizer(this)
|
|
108
119
|
|
|
109
|
-
// When the synchronizer emits
|
|
120
|
+
// When the synchronizer emits messages, send them to peers
|
|
110
121
|
synchronizer.on("message", message => {
|
|
111
|
-
this.#log(`sending
|
|
122
|
+
this.#log(`sending ${message.type} message to ${message.targetId}`)
|
|
112
123
|
networkSubsystem.send(message)
|
|
113
124
|
})
|
|
114
125
|
|
|
@@ -198,9 +209,9 @@ export class Repo extends EventEmitter<RepoEvents> {
|
|
|
198
209
|
* @param clonedHandle - The handle to clone
|
|
199
210
|
*
|
|
200
211
|
* @remarks This is a wrapper around the `clone` function in the Automerge library.
|
|
201
|
-
* The new `DocHandle` will have a new URL but will share history with the original,
|
|
202
|
-
* which means that changes made to the cloned handle can be sensibly merged back
|
|
203
|
-
* into the original.
|
|
212
|
+
* The new `DocHandle` will have a new URL but will share history with the original,
|
|
213
|
+
* which means that changes made to the cloned handle can be sensibly merged back
|
|
214
|
+
* into the original.
|
|
204
215
|
*
|
|
205
216
|
* Any peers this `Repo` is connected to for whom `sharePolicy` returns `true` will
|
|
206
217
|
* be notified of the newly created DocHandle.
|
|
@@ -223,7 +234,7 @@ export class Repo extends EventEmitter<RepoEvents> {
|
|
|
223
234
|
|
|
224
235
|
const handle = this.create<T>()
|
|
225
236
|
|
|
226
|
-
handle.update((
|
|
237
|
+
handle.update(() => {
|
|
227
238
|
// we replace the document with the new cloned one
|
|
228
239
|
return Automerge.clone(sourceDoc)
|
|
229
240
|
})
|
|
@@ -240,7 +251,7 @@ export class Repo extends EventEmitter<RepoEvents> {
|
|
|
240
251
|
automergeUrl: AutomergeUrl
|
|
241
252
|
): DocHandle<T> {
|
|
242
253
|
if (!isValidAutomergeUrl(automergeUrl)) {
|
|
243
|
-
|
|
254
|
+
const maybeAutomergeUrl = parseLegacyUUID(automergeUrl)
|
|
244
255
|
if (maybeAutomergeUrl) {
|
|
245
256
|
console.warn(
|
|
246
257
|
"Legacy UUID document ID detected, converting to AutomergeUrl. This will be removed in a future version."
|
|
@@ -274,17 +285,13 @@ export class Repo extends EventEmitter<RepoEvents> {
|
|
|
274
285
|
/** The documentId of the handle to delete */
|
|
275
286
|
id: DocumentId | AutomergeUrl
|
|
276
287
|
) {
|
|
277
|
-
if (isValidAutomergeUrl(id))
|
|
278
|
-
;({ documentId: id } = parseAutomergeUrl(id))
|
|
279
|
-
}
|
|
288
|
+
if (isValidAutomergeUrl(id)) id = parseAutomergeUrl(id).documentId
|
|
280
289
|
|
|
281
290
|
const handle = this.#getHandle(id, false)
|
|
282
291
|
handle.delete()
|
|
283
292
|
|
|
284
293
|
delete this.#handleCache[id]
|
|
285
|
-
this.emit("delete-document", {
|
|
286
|
-
documentId: id,
|
|
287
|
-
})
|
|
294
|
+
this.emit("delete-document", { documentId: id })
|
|
288
295
|
}
|
|
289
296
|
}
|
|
290
297
|
|
package/src/helpers/cbor.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { Encoder, decode as cborXdecode } from "cbor-x"
|
|
1
|
+
import { Encoder, decode as cborXdecode } from "cbor-x"
|
|
2
2
|
|
|
3
|
-
export function encode(obj:
|
|
4
|
-
|
|
3
|
+
export function encode(obj: unknown): Buffer {
|
|
4
|
+
const encoder = new Encoder({ tagUint8Array: false })
|
|
5
5
|
return encoder.encode(obj)
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
export function decode(buf: Buffer | Uint8Array):
|
|
8
|
+
export function decode<T = unknown>(buf: Buffer | Uint8Array): T {
|
|
9
9
|
return cborXdecode(buf)
|
|
10
10
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** throttle( callback, rate )
|
|
2
|
+
* Returns a throttle function with a build in debounce timer that runs after `wait` ms.
|
|
3
|
+
*
|
|
4
|
+
* Note that the args go inside the parameter and you should be careful not to
|
|
5
|
+
* recreate the function on each usage. (In React, see useMemo().)
|
|
6
|
+
*
|
|
7
|
+
*
|
|
8
|
+
* Example usage:
|
|
9
|
+
* const callback = throttle((ev) => { doSomethingExpensiveOrOccasional() }, 100)
|
|
10
|
+
* target.addEventListener('frequent-event', callback);
|
|
11
|
+
*
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const throttle = <F extends (...args: Parameters<F>) => ReturnType<F>>(
|
|
15
|
+
fn: F,
|
|
16
|
+
rate: number
|
|
17
|
+
) => {
|
|
18
|
+
let timeout: ReturnType<typeof setTimeout>
|
|
19
|
+
return function (...args: Parameters<F>) {
|
|
20
|
+
clearTimeout(timeout)
|
|
21
|
+
timeout = setTimeout(() => {
|
|
22
|
+
fn.apply(null, args)
|
|
23
|
+
}, rate)
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/helpers/pause.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
export const pause = (t = 0) =>
|
|
2
2
|
new Promise<void>(resolve => setTimeout(() => resolve(), t))
|
|
3
3
|
|
|
4
|
-
export function rejectOnTimeout<T>(
|
|
4
|
+
export function rejectOnTimeout<T>(
|
|
5
|
+
promise: Promise<T>,
|
|
6
|
+
millis: number
|
|
7
|
+
): Promise<T> {
|
|
5
8
|
return Promise.race([
|
|
6
9
|
promise,
|
|
7
|
-
pause(millis).then(() => {
|
|
10
|
+
pause(millis).then(() => {
|
|
11
|
+
throw new Error("timeout exceeded")
|
|
12
|
+
}),
|
|
8
13
|
])
|
|
9
14
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import assert from "assert"
|
|
2
|
+
import { describe, it } from "vitest"
|
|
3
|
+
import { PeerId, Repo, type NetworkAdapter } from "../../index.js"
|
|
2
4
|
import { eventPromise, eventPromises } from "../eventPromise.js"
|
|
3
|
-
import { assert } from "chai"
|
|
4
|
-
import { describe, it } from "mocha"
|
|
5
5
|
import { pause } from "../pause.js"
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Throttle
|
|
2
|
+
* Returns a function with a built in throttle timer that runs after `delay` ms.
|
|
3
|
+
*
|
|
4
|
+
* This function differs from a conventional `throttle` in that it ensures the final
|
|
5
|
+
* call will also execute and delays sending the first one until `delay` ms to allow
|
|
6
|
+
* additional work to accumulate.
|
|
7
|
+
*
|
|
8
|
+
* Here's a diagram:
|
|
9
|
+
*
|
|
10
|
+
* calls +----++++++-----++----
|
|
11
|
+
* dlay ^--v ^--v^--v ^--v
|
|
12
|
+
* execs ---+----+---+------+--
|
|
13
|
+
*
|
|
14
|
+
* The goal in this design is to create batches of changes without flooding
|
|
15
|
+
* communication or storage systems while still feeling responsive.
|
|
16
|
+
* (By default we communicate at 10hz / every 100ms.)
|
|
17
|
+
*
|
|
18
|
+
* Note that the args go inside the parameter and you should be careful not to
|
|
19
|
+
* recreate the function on each usage. (In React, see useMemo().)
|
|
20
|
+
*
|
|
21
|
+
*
|
|
22
|
+
* Example usage:
|
|
23
|
+
* const callback = debounce((ev) => { doSomethingExpensiveOrOccasional() }, 100)
|
|
24
|
+
* target.addEventListener('frequent-event', callback);
|
|
25
|
+
*
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export const throttle = <F extends (...args: Parameters<F>) => ReturnType<F>>(
|
|
29
|
+
fn: F,
|
|
30
|
+
delay: number
|
|
31
|
+
) => {
|
|
32
|
+
let lastCall = Date.now()
|
|
33
|
+
let wait
|
|
34
|
+
let timeout: ReturnType<typeof setTimeout>
|
|
35
|
+
return function (...args: Parameters<F>) {
|
|
36
|
+
wait = lastCall + delay - Date.now()
|
|
37
|
+
clearTimeout(timeout)
|
|
38
|
+
timeout = setTimeout(() => {
|
|
39
|
+
fn.apply(null, args)
|
|
40
|
+
lastCall = Date.now()
|
|
41
|
+
}, wait)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -6,7 +6,7 @@ export const withTimeout = async <T>(
|
|
|
6
6
|
promise: Promise<T>,
|
|
7
7
|
t: number
|
|
8
8
|
): Promise<T> => {
|
|
9
|
-
let timeoutId: ReturnType<typeof setTimeout>
|
|
9
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined
|
|
10
10
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
11
11
|
timeoutId = setTimeout(
|
|
12
12
|
() => reject(new TimeoutError(`withTimeout: timed out after ${t}ms`)),
|
|
@@ -16,7 +16,7 @@ export const withTimeout = async <T>(
|
|
|
16
16
|
try {
|
|
17
17
|
return await Promise.race([promise, timeoutPromise])
|
|
18
18
|
} finally {
|
|
19
|
-
clearTimeout(timeoutId
|
|
19
|
+
clearTimeout(timeoutId)
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
|
package/src/index.ts
CHANGED
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
*
|
|
17
17
|
* ```typescript
|
|
18
18
|
* import { Repo } from "@automerge/automerge-repo";
|
|
19
|
-
*
|
|
19
|
+
*
|
|
20
20
|
* const repo = new Repo({
|
|
21
|
-
* storage: <storage adapter>,
|
|
21
|
+
* storage: <storage adapter>,
|
|
22
22
|
* network: [<network adapter>, <network adapter>]
|
|
23
23
|
* })
|
|
24
24
|
*
|
|
@@ -26,47 +26,54 @@
|
|
|
26
26
|
* ```
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
-
export { DocHandle
|
|
30
|
-
export
|
|
29
|
+
export { DocHandle } from "./DocHandle.js"
|
|
30
|
+
export {
|
|
31
|
+
isValidAutomergeUrl,
|
|
32
|
+
parseAutomergeUrl,
|
|
33
|
+
stringifyAutomergeUrl,
|
|
34
|
+
} from "./DocUrl.js"
|
|
35
|
+
export { Repo } from "./Repo.js"
|
|
36
|
+
export { NetworkAdapter } from "./network/NetworkAdapter.js"
|
|
37
|
+
export { isValidRepoMessage } from "./network/messages.js"
|
|
38
|
+
export { StorageAdapter } from "./storage/StorageAdapter.js"
|
|
39
|
+
|
|
40
|
+
/** @hidden **/
|
|
41
|
+
export * as cbor from "./helpers/cbor.js"
|
|
42
|
+
|
|
43
|
+
// types
|
|
44
|
+
|
|
45
|
+
export type {
|
|
31
46
|
DocHandleChangePayload,
|
|
32
47
|
DocHandleDeletePayload,
|
|
48
|
+
DocHandleEncodedChangePayload,
|
|
33
49
|
DocHandleEphemeralMessagePayload,
|
|
50
|
+
DocHandleEvents,
|
|
51
|
+
DocHandleOptions,
|
|
34
52
|
DocHandleOutboundEphemeralMessagePayload,
|
|
35
|
-
|
|
53
|
+
HandleState,
|
|
36
54
|
} from "./DocHandle.js"
|
|
37
|
-
export { NetworkAdapter } from "./network/NetworkAdapter.js"
|
|
38
55
|
export type {
|
|
56
|
+
DeleteDocumentPayload,
|
|
57
|
+
DocumentPayload,
|
|
58
|
+
RepoConfig,
|
|
59
|
+
RepoEvents,
|
|
60
|
+
SharePolicy,
|
|
61
|
+
} from "./Repo.js"
|
|
62
|
+
export type {
|
|
63
|
+
NetworkAdapterEvents,
|
|
39
64
|
OpenPayload,
|
|
40
65
|
PeerCandidatePayload,
|
|
41
66
|
PeerDisconnectedPayload,
|
|
42
|
-
NetworkAdapterEvents,
|
|
43
67
|
} from "./network/NetworkAdapter.js"
|
|
44
|
-
|
|
45
|
-
// This is a bit confusing right now, but:
|
|
46
|
-
// Message is the type for messages used outside of the network adapters
|
|
47
|
-
// there are some extra internal network adapter-only messages on NetworkAdapterMessage
|
|
48
|
-
// and Message is (as of this writing) a union type for EphmeralMessage and SyncMessage
|
|
49
68
|
export type {
|
|
50
|
-
Message,
|
|
51
69
|
ArriveMessage,
|
|
52
|
-
|
|
53
|
-
NetworkAdapterMessage,
|
|
70
|
+
DocumentUnavailableMessage,
|
|
54
71
|
EphemeralMessage,
|
|
72
|
+
Message,
|
|
73
|
+
RepoMessage,
|
|
55
74
|
RequestMessage,
|
|
56
|
-
DocumentUnavailableMessage,
|
|
57
75
|
SyncMessage,
|
|
58
|
-
|
|
76
|
+
WelcomeMessage,
|
|
59
77
|
} from "./network/messages.js"
|
|
60
|
-
export {
|
|
61
|
-
|
|
62
|
-
export { Repo, type SharePolicy, type RepoConfig, type RepoEvents, type DeleteDocumentPayload, type DocumentPayload } from "./Repo.js"
|
|
63
|
-
export { StorageAdapter, type StorageKey } from "./storage/StorageAdapter.js"
|
|
64
|
-
export {
|
|
65
|
-
parseAutomergeUrl,
|
|
66
|
-
isValidAutomergeUrl,
|
|
67
|
-
stringifyAutomergeUrl as generateAutomergeUrl,
|
|
68
|
-
} from "./DocUrl.js"
|
|
78
|
+
export type { StorageKey } from "./storage/StorageAdapter.js"
|
|
69
79
|
export * from "./types.js"
|
|
70
|
-
|
|
71
|
-
/** @hidden **/
|
|
72
|
-
export * as cbor from "./helpers/cbor.js"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from "eventemitter3"
|
|
2
2
|
import { PeerId } from "../types.js"
|
|
3
|
-
import {
|
|
3
|
+
import { RepoMessage } from "./messages.js"
|
|
4
4
|
|
|
5
5
|
/** An interface representing some way to connect to other peers
|
|
6
6
|
*
|
|
@@ -22,7 +22,7 @@ export abstract class NetworkAdapter extends EventEmitter<NetworkAdapterEvents>
|
|
|
22
22
|
*
|
|
23
23
|
* @argument message - the message to send
|
|
24
24
|
*/
|
|
25
|
-
abstract send(message:
|
|
25
|
+
abstract send(message: RepoMessage): void
|
|
26
26
|
|
|
27
27
|
/** Called by the {@link Repo} to disconnect from the network */
|
|
28
28
|
abstract disconnect(): void
|
|
@@ -33,14 +33,18 @@ export abstract class NetworkAdapter extends EventEmitter<NetworkAdapterEvents>
|
|
|
33
33
|
export interface NetworkAdapterEvents {
|
|
34
34
|
/** Emitted when the network is ready to be used */
|
|
35
35
|
ready: (payload: OpenPayload) => void
|
|
36
|
+
|
|
36
37
|
/** Emitted when the network is closed */
|
|
37
38
|
close: () => void
|
|
39
|
+
|
|
38
40
|
/** Emitted when the network adapter learns about a new peer */
|
|
39
41
|
"peer-candidate": (payload: PeerCandidatePayload) => void
|
|
42
|
+
|
|
40
43
|
/** Emitted when the network adapter learns that a peer has disconnected */
|
|
41
44
|
"peer-disconnected": (payload: PeerDisconnectedPayload) => void
|
|
45
|
+
|
|
42
46
|
/** Emitted when the network adapter receives a message from a peer */
|
|
43
|
-
message: (payload:
|
|
47
|
+
message: (payload: RepoMessage) => void
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
export interface OpenPayload {
|
|
@@ -1,18 +1,15 @@
|
|
|
1
|
+
import debug from "debug"
|
|
1
2
|
import { EventEmitter } from "eventemitter3"
|
|
2
|
-
import { PeerId } from "../types.js"
|
|
3
|
+
import { PeerId, SessionId } from "../types.js"
|
|
3
4
|
import { NetworkAdapter, PeerDisconnectedPayload } from "./NetworkAdapter.js"
|
|
4
|
-
|
|
5
5
|
import {
|
|
6
6
|
EphemeralMessage,
|
|
7
|
-
isEphemeralMessage,
|
|
8
|
-
isValidMessage,
|
|
9
|
-
Message,
|
|
10
7
|
MessageContents,
|
|
8
|
+
RepoMessage,
|
|
9
|
+
isEphemeralMessage,
|
|
10
|
+
isValidRepoMessage,
|
|
11
11
|
} from "./messages.js"
|
|
12
12
|
|
|
13
|
-
import debug from "debug"
|
|
14
|
-
import { SessionId } from "../EphemeralData.js"
|
|
15
|
-
|
|
16
13
|
type EphemeralMessageSource = `${PeerId}:${SessionId}`
|
|
17
14
|
|
|
18
15
|
const getEphemeralMessageSource = (message: EphemeralMessage) =>
|
|
@@ -69,7 +66,7 @@ export class NetworkSubsystem extends EventEmitter<NetworkSubsystemEvents> {
|
|
|
69
66
|
})
|
|
70
67
|
|
|
71
68
|
networkAdapter.on("message", msg => {
|
|
72
|
-
if (!
|
|
69
|
+
if (!isValidRepoMessage(msg)) {
|
|
73
70
|
this.#log(`invalid message: ${JSON.stringify(msg)}`)
|
|
74
71
|
return
|
|
75
72
|
}
|
|
@@ -110,25 +107,36 @@ export class NetworkSubsystem extends EventEmitter<NetworkSubsystemEvents> {
|
|
|
110
107
|
this.#log(`Tried to send message but peer not found: ${message.targetId}`)
|
|
111
108
|
return
|
|
112
109
|
}
|
|
113
|
-
this.#log(`Sending message to ${message.targetId}`)
|
|
114
110
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
111
|
+
/** Messages come in without a senderId and other required information; this is where we make
|
|
112
|
+
* sure they have everything they need.
|
|
113
|
+
*/
|
|
114
|
+
const prepareMessage = (message: MessageContents): RepoMessage => {
|
|
115
|
+
if (message.type === "ephemeral") {
|
|
116
|
+
if ("count" in message) {
|
|
117
|
+
// existing ephemeral message from another peer; pass on without changes
|
|
118
|
+
return message as EphemeralMessage
|
|
119
|
+
} else {
|
|
120
|
+
// new ephemeral message from us; add our senderId as well as a counter and session id
|
|
121
|
+
return {
|
|
120
122
|
...message,
|
|
121
123
|
count: ++this.#count,
|
|
122
124
|
sessionId: this.#sessionId,
|
|
123
125
|
senderId: this.peerId,
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
126
|
+
} as EphemeralMessage
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
// other message type; just add our senderId
|
|
130
|
+
return {
|
|
131
|
+
...message,
|
|
132
|
+
senderId: this.peerId,
|
|
133
|
+
} as RepoMessage
|
|
134
|
+
}
|
|
131
135
|
}
|
|
136
|
+
|
|
137
|
+
const outbound = prepareMessage(message)
|
|
138
|
+
this.#log("sending message", outbound)
|
|
139
|
+
peer.send(outbound as RepoMessage)
|
|
132
140
|
}
|
|
133
141
|
|
|
134
142
|
isReady = () => {
|
|
@@ -157,7 +165,7 @@ function randomPeerId() {
|
|
|
157
165
|
export interface NetworkSubsystemEvents {
|
|
158
166
|
peer: (payload: PeerPayload) => void
|
|
159
167
|
"peer-disconnected": (payload: PeerDisconnectedPayload) => void
|
|
160
|
-
message: (payload:
|
|
168
|
+
message: (payload: RepoMessage) => void
|
|
161
169
|
ready: () => void
|
|
162
170
|
}
|
|
163
171
|
|