@automerge/automerge-repo 1.0.5 → 1.0.7

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 (65) hide show
  1. package/.eslintrc +1 -1
  2. package/dist/DocHandle.d.ts +20 -7
  3. package/dist/DocHandle.d.ts.map +1 -1
  4. package/dist/DocHandle.js +27 -7
  5. package/dist/EphemeralData.d.ts +2 -2
  6. package/dist/EphemeralData.d.ts.map +1 -1
  7. package/dist/Repo.d.ts +16 -0
  8. package/dist/Repo.d.ts.map +1 -1
  9. package/dist/Repo.js +38 -10
  10. package/dist/helpers/cbor.d.ts +2 -2
  11. package/dist/helpers/cbor.d.ts.map +1 -1
  12. package/dist/helpers/cbor.js +1 -1
  13. package/dist/helpers/pause.d.ts.map +1 -1
  14. package/dist/helpers/pause.js +3 -1
  15. package/dist/helpers/tests/network-adapter-tests.d.ts.map +1 -1
  16. package/dist/helpers/tests/network-adapter-tests.js +2 -2
  17. package/dist/index.d.ts +11 -9
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +4 -4
  20. package/dist/network/NetworkAdapter.d.ts +3 -3
  21. package/dist/network/NetworkAdapter.d.ts.map +1 -1
  22. package/dist/network/NetworkSubsystem.d.ts +2 -2
  23. package/dist/network/NetworkSubsystem.d.ts.map +1 -1
  24. package/dist/network/NetworkSubsystem.js +30 -18
  25. package/dist/network/messages.d.ts +38 -68
  26. package/dist/network/messages.d.ts.map +1 -1
  27. package/dist/network/messages.js +13 -21
  28. package/dist/storage/StorageSubsystem.js +7 -7
  29. package/dist/synchronizer/CollectionSynchronizer.d.ts +3 -3
  30. package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
  31. package/dist/synchronizer/CollectionSynchronizer.js +2 -2
  32. package/dist/synchronizer/DocSynchronizer.d.ts +3 -3
  33. package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
  34. package/dist/synchronizer/DocSynchronizer.js +22 -29
  35. package/dist/synchronizer/Synchronizer.d.ts +2 -2
  36. package/dist/synchronizer/Synchronizer.d.ts.map +1 -1
  37. package/dist/types.d.ts +5 -1
  38. package/dist/types.d.ts.map +1 -1
  39. package/package.json +5 -13
  40. package/src/DocHandle.ts +38 -14
  41. package/src/EphemeralData.ts +2 -2
  42. package/src/Repo.ts +46 -12
  43. package/src/helpers/cbor.ts +4 -4
  44. package/src/helpers/pause.ts +7 -2
  45. package/src/helpers/tests/network-adapter-tests.ts +3 -3
  46. package/src/helpers/withTimeout.ts +2 -2
  47. package/src/index.ts +36 -29
  48. package/src/network/NetworkAdapter.ts +7 -3
  49. package/src/network/NetworkSubsystem.ts +31 -23
  50. package/src/network/messages.ts +88 -151
  51. package/src/storage/StorageSubsystem.ts +8 -8
  52. package/src/synchronizer/CollectionSynchronizer.ts +6 -15
  53. package/src/synchronizer/DocSynchronizer.ts +34 -48
  54. package/src/synchronizer/Synchronizer.ts +2 -2
  55. package/src/types.ts +8 -3
  56. package/test/CollectionSynchronizer.test.ts +58 -53
  57. package/test/DocHandle.test.ts +35 -36
  58. package/test/DocSynchronizer.test.ts +9 -8
  59. package/test/Network.test.ts +1 -0
  60. package/test/Repo.test.ts +273 -88
  61. package/test/StorageSubsystem.test.ts +6 -9
  62. package/test/tsconfig.json +8 -0
  63. package/test/types.ts +2 -0
  64. package/typedoc.json +3 -3
  65. package/.mocharc.json +0 -5
@@ -1,5 +1,5 @@
1
1
  import { DocumentId, PeerId } from "./index.js"
2
- import { EphemeralMessageContents } from "./network/messages.js"
2
+ import { EphemeralMessage, MessageContents } from "./network/messages.js"
3
3
 
4
4
  // types
5
5
  /** A randomly generated string created when the {@link Repo} starts up */
@@ -12,6 +12,6 @@ export interface EphemeralDataPayload {
12
12
  }
13
13
 
14
14
  export type EphemeralDataMessageEvents = {
15
- message: (event: EphemeralMessageContents) => void
15
+ message: (event: MessageContents<EphemeralMessage>) => void
16
16
  data: (event: EphemeralDataPayload) => void
17
17
  }
package/src/Repo.ts CHANGED
@@ -5,7 +5,7 @@ import { StorageAdapter } from "./storage/StorageAdapter.js"
5
5
  import { StorageSubsystem } from "./storage/StorageSubsystem.js"
6
6
  import { CollectionSynchronizer } from "./synchronizer/CollectionSynchronizer.js"
7
7
  import { type AutomergeUrl, DocumentId, PeerId } from "./types.js"
8
- import { v4 as uuid } from "uuid"
8
+
9
9
  import {
10
10
  parseAutomergeUrl,
11
11
  generateAutomergeUrl,
@@ -15,7 +15,7 @@ import {
15
15
 
16
16
  import { DocHandle } from "./DocHandle.js"
17
17
  import { EventEmitter } from "eventemitter3"
18
- import bs58check from "bs58check"
18
+ import { next as Automerge } from "@automerge/automerge"
19
19
 
20
20
  /** A Repo is a collection of documents with networking, syncing, and storage capabilities. */
21
21
  /** The `Repo` is the main entry point of this library
@@ -106,9 +106,9 @@ export class Repo extends EventEmitter<RepoEvents> {
106
106
  // The synchronizer uses the network subsystem to keep documents in sync with peers.
107
107
  const synchronizer = new CollectionSynchronizer(this)
108
108
 
109
- // When the synchronizer emits sync messages, send them to peers
109
+ // When the synchronizer emits messages, send them to peers
110
110
  synchronizer.on("message", message => {
111
- this.#log(`sending sync message to ${message.targetId}`)
111
+ this.#log(`sending ${message.type} message to ${message.targetId}`)
112
112
  networkSubsystem.send(message)
113
113
  })
114
114
 
@@ -193,6 +193,44 @@ export class Repo extends EventEmitter<RepoEvents> {
193
193
  return handle
194
194
  }
195
195
 
196
+ /** Create a new DocHandle by cloning the history of an existing DocHandle.
197
+ *
198
+ * @param clonedHandle - The handle to clone
199
+ *
200
+ * @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.
204
+ *
205
+ * Any peers this `Repo` is connected to for whom `sharePolicy` returns `true` will
206
+ * be notified of the newly created DocHandle.
207
+ *
208
+ * @throws if the cloned handle is not yet ready or if
209
+ * `clonedHandle.docSync()` returns `undefined` (i.e. the handle is unavailable).
210
+ */
211
+ clone<T>(clonedHandle: DocHandle<T>) {
212
+ if (!clonedHandle.isReady()) {
213
+ throw new Error(
214
+ `Cloned handle is not yet in ready state.
215
+ (Try await handle.waitForReady() first.)`
216
+ )
217
+ }
218
+
219
+ const sourceDoc = clonedHandle.docSync()
220
+ if (!sourceDoc) {
221
+ throw new Error("Cloned handle doesn't have a document.")
222
+ }
223
+
224
+ const handle = this.create<T>()
225
+
226
+ handle.update(() => {
227
+ // we replace the document with the new cloned one
228
+ return Automerge.clone(sourceDoc)
229
+ })
230
+
231
+ return handle
232
+ }
233
+
196
234
  /**
197
235
  * Retrieves a document by id. It gets data from the local system, but also emits a `document`
198
236
  * event to advertise interest in the document.
@@ -202,7 +240,7 @@ export class Repo extends EventEmitter<RepoEvents> {
202
240
  automergeUrl: AutomergeUrl
203
241
  ): DocHandle<T> {
204
242
  if (!isValidAutomergeUrl(automergeUrl)) {
205
- let maybeAutomergeUrl = parseLegacyUUID(automergeUrl)
243
+ const maybeAutomergeUrl = parseLegacyUUID(automergeUrl)
206
244
  if (maybeAutomergeUrl) {
207
245
  console.warn(
208
246
  "Legacy UUID document ID detected, converting to AutomergeUrl. This will be removed in a future version."
@@ -236,17 +274,13 @@ export class Repo extends EventEmitter<RepoEvents> {
236
274
  /** The documentId of the handle to delete */
237
275
  id: DocumentId | AutomergeUrl
238
276
  ) {
239
- if (isValidAutomergeUrl(id)) {
240
- ;({ documentId: id } = parseAutomergeUrl(id))
241
- }
277
+ if (isValidAutomergeUrl(id)) id = parseAutomergeUrl(id).documentId
242
278
 
243
279
  const handle = this.#getHandle(id, false)
244
280
  handle.delete()
245
281
 
246
282
  delete this.#handleCache[id]
247
- this.emit("delete-document", {
248
- documentId: id,
249
- })
283
+ this.emit("delete-document", { documentId: id })
250
284
  }
251
285
  }
252
286
 
@@ -267,7 +301,7 @@ export interface RepoConfig {
267
301
  sharePolicy?: SharePolicy
268
302
  }
269
303
 
270
- /** A function that determines whether we should share a document with a peer
304
+ /** A function that determines whether we should share a document with a peer
271
305
  *
272
306
  * @remarks
273
307
  * This function is called by the {@link Repo} every time a new document is created
@@ -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: any): Buffer {
4
- let encoder = new Encoder({tagUint8Array: false})
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): any {
8
+ export function decode<T = unknown>(buf: Buffer | Uint8Array): T {
9
9
  return cborXdecode(buf)
10
10
  }
@@ -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>(promise: Promise<T>, millis: number): Promise<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(() => { throw new Error("timeout exceeded") }),
10
+ pause(millis).then(() => {
11
+ throw new Error("timeout exceeded")
12
+ }),
8
13
  ])
9
14
  }
@@ -1,7 +1,7 @@
1
- import { PeerId, Repo, type NetworkAdapter, DocumentId } from "../../index.js"
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
  /**
@@ -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, type HandleState, type DocHandleOptions, type DocHandleEvents } from "./DocHandle.js"
30
- export type {
29
+ export { Repo } from "./Repo.js"
30
+ export { DocHandle } from "./DocHandle.js"
31
+ export { NetworkAdapter } from "./network/NetworkAdapter.js"
32
+ export { StorageAdapter } from "./storage/StorageAdapter.js"
33
+ export {
34
+ isValidAutomergeUrl,
35
+ parseAutomergeUrl,
36
+ stringifyAutomergeUrl,
37
+ } from "./DocUrl.js"
38
+ export { isValidRepoMessage } from "./network/messages.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
- DocHandleEncodedChangePayload,
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
- WelcomeMessage,
53
- NetworkAdapterMessage,
70
+ DocumentUnavailableMessage,
54
71
  EphemeralMessage,
72
+ Message,
73
+ RepoMessage,
55
74
  RequestMessage,
56
- DocumentUnavailableMessage,
57
75
  SyncMessage,
58
- SessionId,
76
+ WelcomeMessage,
59
77
  } from "./network/messages.js"
60
- export { isValidMessage } from "./network/messages.js"
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 { Message } from "./messages.js"
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: Message): void
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: Message) => void
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,
7
+ RepoMessage,
10
8
  MessageContents,
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 (!isValidMessage(msg)) {
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
- if (isEphemeralMessage(message)) {
116
- const outbound =
117
- "count" in message
118
- ? message
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
- this.#log("Ephemeral message", outbound)
126
- peer.send(outbound)
127
- } else {
128
- const outbound = { ...message, senderId: this.peerId }
129
- this.#log("Sync message", outbound)
130
- peer.send(outbound)
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: Message) => void
168
+ message: (payload: RepoMessage) => void
161
169
  ready: () => void
162
170
  }
163
171