@automerge/automerge-repo 0.2.0 → 1.0.0-alpha.0

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 (57) hide show
  1. package/README.md +7 -24
  2. package/dist/DocCollection.d.ts +4 -4
  3. package/dist/DocCollection.d.ts.map +1 -1
  4. package/dist/DocCollection.js +25 -17
  5. package/dist/DocHandle.d.ts +46 -10
  6. package/dist/DocHandle.d.ts.map +1 -1
  7. package/dist/DocHandle.js +101 -36
  8. package/dist/DocUrl.d.ts +38 -18
  9. package/dist/DocUrl.d.ts.map +1 -1
  10. package/dist/DocUrl.js +63 -24
  11. package/dist/Repo.d.ts.map +1 -1
  12. package/dist/Repo.js +4 -6
  13. package/dist/helpers/headsAreSame.d.ts +1 -1
  14. package/dist/helpers/headsAreSame.d.ts.map +1 -1
  15. package/dist/helpers/tests/network-adapter-tests.js +10 -10
  16. package/dist/index.d.ts +3 -2
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +1 -0
  19. package/dist/network/NetworkAdapter.d.ts +2 -3
  20. package/dist/network/NetworkAdapter.d.ts.map +1 -1
  21. package/dist/network/NetworkSubsystem.d.ts +2 -3
  22. package/dist/network/NetworkSubsystem.d.ts.map +1 -1
  23. package/dist/network/NetworkSubsystem.js +9 -13
  24. package/dist/storage/StorageAdapter.d.ts +9 -5
  25. package/dist/storage/StorageAdapter.d.ts.map +1 -1
  26. package/dist/storage/StorageSubsystem.d.ts +2 -2
  27. package/dist/storage/StorageSubsystem.d.ts.map +1 -1
  28. package/dist/storage/StorageSubsystem.js +74 -26
  29. package/dist/synchronizer/CollectionSynchronizer.d.ts +1 -1
  30. package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
  31. package/dist/synchronizer/CollectionSynchronizer.js +5 -1
  32. package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
  33. package/dist/synchronizer/DocSynchronizer.js +6 -5
  34. package/dist/types.d.ts +6 -0
  35. package/dist/types.d.ts.map +1 -1
  36. package/package.json +8 -5
  37. package/src/DocCollection.ts +32 -22
  38. package/src/DocHandle.ts +121 -47
  39. package/src/DocUrl.ts +90 -0
  40. package/src/Repo.ts +5 -8
  41. package/src/helpers/tests/network-adapter-tests.ts +10 -10
  42. package/src/index.ts +7 -5
  43. package/src/network/NetworkAdapter.ts +2 -3
  44. package/src/network/NetworkSubsystem.ts +9 -14
  45. package/src/storage/StorageAdapter.ts +7 -5
  46. package/src/storage/StorageSubsystem.ts +96 -35
  47. package/src/synchronizer/CollectionSynchronizer.ts +10 -2
  48. package/src/synchronizer/DocSynchronizer.ts +7 -6
  49. package/src/types.ts +4 -1
  50. package/test/CollectionSynchronizer.test.ts +1 -1
  51. package/test/DocCollection.test.ts +3 -2
  52. package/test/DocHandle.test.ts +32 -26
  53. package/test/DocSynchronizer.test.ts +3 -2
  54. package/test/Repo.test.ts +76 -27
  55. package/test/StorageSubsystem.test.ts +10 -7
  56. package/test/helpers/DummyNetworkAdapter.ts +2 -2
  57. package/test/helpers/DummyStorageAdapter.ts +8 -4
package/dist/types.d.ts CHANGED
@@ -1,6 +1,12 @@
1
1
  export type DocumentId = string & {
2
2
  __documentId: true;
3
3
  };
4
+ export type AutomergeUrl = string & {
5
+ __documentUrl: true;
6
+ };
7
+ export type BinaryDocumentId = Uint8Array & {
8
+ __binaryDocumentId: true;
9
+ };
4
10
  export type PeerId = string & {
5
11
  __peerId: false;
6
12
  };
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG;IAAE,YAAY,EAAE,IAAI,CAAA;CAAE,CAAA;AACxD,MAAM,MAAM,MAAM,GAAG,MAAM,GAAG;IAAE,QAAQ,EAAE,KAAK,CAAA;CAAE,CAAA;AACjD,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG;IAAE,WAAW,EAAE,KAAK,CAAA;CAAE,CAAA"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG;IAAE,YAAY,EAAE,IAAI,CAAA;CAAE,CAAA;AACxD,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG;IAAE,aAAa,EAAE,IAAI,CAAA;CAAE,CAAA;AAC3D,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG;IAAE,kBAAkB,EAAE,IAAI,CAAA;CAAE,CAAA;AAExE,MAAM,MAAM,MAAM,GAAG,MAAM,GAAG;IAAE,QAAQ,EAAE,KAAK,CAAA;CAAE,CAAA;AACjD,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG;IAAE,WAAW,EAAE,KAAK,CAAA;CAAE,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automerge/automerge-repo",
3
- "version": "0.2.0",
3
+ "version": "1.0.0-alpha.0",
4
4
  "description": "A repository object to manage a collection of automerge documents",
5
5
  "repository": "https://github.com/automerge/automerge-repo",
6
6
  "author": "Peter van Hardenberg <pvh@pvh.ca>",
@@ -22,19 +22,22 @@
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/debug": "^4.1.7",
25
+ "@types/node": "^20.4.8",
25
26
  "@types/uuid": "^8.3.4",
26
27
  "@types/ws": "^8.5.3",
27
28
  "@typescript-eslint/eslint-plugin": "^5.33.0",
28
29
  "@typescript-eslint/parser": "^5.33.0",
29
- "http-server": "^14.1.0"
30
+ "http-server": "^14.1.0",
31
+ "typescript": "^5.1.6"
30
32
  },
31
33
  "peerDependencies": {
32
- "@automerge/automerge": "^2.1.0-alpha.8"
34
+ "@automerge/automerge": "^2.1.0-alpha.9"
33
35
  },
34
36
  "dependencies": {
37
+ "bs58check": "^3.0.1",
35
38
  "cbor-x": "^1.3.0",
36
39
  "debug": "^4.3.4",
37
- "eventemitter3": "^4.0.7",
40
+ "eventemitter3": "^5.0.1",
38
41
  "fast-sha256": "^1.3.0",
39
42
  "tiny-typed-emitter": "^2.1.0",
40
43
  "ts-node": "^10.9.1",
@@ -62,5 +65,5 @@
62
65
  "publishConfig": {
63
66
  "access": "public"
64
67
  },
65
- "gitHead": "b17ea68dce605c06e57e4fcd6e6295ef968a8b6d"
68
+ "gitHead": "38c0c32796ddca5f86a2e55ab0f1202a2ce107c8"
66
69
  }
@@ -1,8 +1,14 @@
1
1
  import EventEmitter from "eventemitter3"
2
- import { v4 as uuid } from "uuid"
3
2
  import { DocHandle } from "./DocHandle.js"
4
- import { type DocumentId } from "./types.js"
3
+ import { DocumentId, type BinaryDocumentId, AutomergeUrl } from "./types.js"
5
4
  import { type SharePolicy } from "./Repo.js"
5
+ import {
6
+ documentIdToBinary,
7
+ binaryToDocumentId,
8
+ generateAutomergeUrl,
9
+ isValidAutomergeUrl,
10
+ parseAutomergeUrl,
11
+ } from "./DocUrl.js"
6
12
 
7
13
  /**
8
14
  * A DocCollection is a collection of DocHandles. It supports creating new documents and finding
@@ -30,6 +36,7 @@ export class DocCollection extends EventEmitter<DocCollectionEvents> {
30
36
  if (this.#handleCache[documentId]) return this.#handleCache[documentId]
31
37
 
32
38
  // If not, create a new handle, cache it, and return it
39
+ if (!documentId) throw new Error(`Invalid documentId ${documentId}`)
33
40
  const handle = new DocHandle<T>(documentId, { isNew })
34
41
  this.#handleCache[documentId] = handle
35
42
  return handle
@@ -64,8 +71,9 @@ export class DocCollection extends EventEmitter<DocCollectionEvents> {
64
71
  // or
65
72
  // - pass a "reify" function that takes a `<any>` and returns `<T>`
66
73
 
67
- const documentId = uuid() as DocumentId
68
- const handle = this.#getHandle<T>(documentId, true) as DocHandle<T>
74
+ // Generate a new UUID and store it in the buffer
75
+ const { encodedDocumentId } = parseAutomergeUrl(generateAutomergeUrl())
76
+ const handle = this.#getHandle<T>(encodedDocumentId, true) as DocHandle<T>
69
77
  this.emit("document", { handle })
70
78
  return handle
71
79
  }
@@ -76,35 +84,37 @@ export class DocCollection extends EventEmitter<DocCollectionEvents> {
76
84
  */
77
85
  find<T>(
78
86
  /** The documentId of the handle to retrieve */
79
- documentId: DocumentId
87
+ automergeUrl: AutomergeUrl
80
88
  ): DocHandle<T> {
81
- // TODO: we want a way to make sure we don't yield intermediate document states during initial synchronization
89
+ if (!isValidAutomergeUrl(automergeUrl)) {
90
+ throw new Error(`Invalid AutomergeUrl: '${automergeUrl}'`)
91
+ }
82
92
 
83
- // If we already have a handle, return it
84
- if (this.#handleCache[documentId])
85
- return this.#handleCache[documentId] as DocHandle<T>
86
-
87
- // Otherwise, create a new handle
88
- const handle = this.#getHandle<T>(documentId, false) as DocHandle<T>
89
-
90
- // we don't directly initialize a value here because the StorageSubsystem and Synchronizers go
91
- // and get the data asynchronously and block on read instead of on create
93
+ const { encodedDocumentId } = parseAutomergeUrl(automergeUrl)
94
+ // If we have the handle cached, return it
95
+ if (this.#handleCache[encodedDocumentId])
96
+ return this.#handleCache[encodedDocumentId]
92
97
 
93
- // emit a document event to advertise interest in this document
98
+ const handle = this.#getHandle<T>(encodedDocumentId, false) as DocHandle<T>
94
99
  this.emit("document", { handle })
95
-
96
100
  return handle
97
101
  }
98
102
 
99
103
  delete(
100
104
  /** The documentId of the handle to delete */
101
- documentId: DocumentId
105
+ id: DocumentId | AutomergeUrl
102
106
  ) {
103
- const handle = this.#getHandle(documentId, false)
107
+ if (isValidAutomergeUrl(id)) {
108
+ ;({ encodedDocumentId: id } = parseAutomergeUrl(id))
109
+ }
110
+
111
+ const handle = this.#getHandle(id, false)
104
112
  handle.delete()
105
113
 
106
- delete this.#handleCache[documentId]
107
- this.emit("delete-document", { documentId })
114
+ delete this.#handleCache[id]
115
+ this.emit("delete-document", {
116
+ encodedDocumentId: id,
117
+ })
108
118
  }
109
119
  }
110
120
 
@@ -119,5 +129,5 @@ interface DocumentPayload {
119
129
  }
120
130
 
121
131
  interface DeleteDocumentPayload {
122
- documentId: DocumentId
132
+ encodedDocumentId: DocumentId
123
133
  }
package/src/DocHandle.ts CHANGED
@@ -17,7 +17,14 @@ import { waitFor } from "xstate/lib/waitFor.js"
17
17
  import { headsAreSame } from "./helpers/headsAreSame.js"
18
18
  import { pause } from "./helpers/pause.js"
19
19
  import { TimeoutError, withTimeout } from "./helpers/withTimeout.js"
20
- import type { ChannelId, DocumentId, PeerId } from "./types.js"
20
+ import type {
21
+ BinaryDocumentId,
22
+ ChannelId,
23
+ DocumentId,
24
+ PeerId,
25
+ AutomergeUrl,
26
+ } from "./types.js"
27
+ import { binaryToDocumentId, stringifyAutomergeUrl } from "./DocUrl.js"
21
28
 
22
29
  /** DocHandle is a wrapper around a single Automerge document that lets us listen for changes. */
23
30
  export class DocHandle<T> //
@@ -28,30 +35,32 @@ export class DocHandle<T> //
28
35
  #machine: DocHandleXstateMachine<T>
29
36
  #timeoutDelay: number
30
37
 
38
+ get url(): AutomergeUrl {
39
+ return stringifyAutomergeUrl({ documentId: this.documentId })
40
+ }
41
+
31
42
  constructor(
32
43
  public documentId: DocumentId,
33
- { isNew = false, timeoutDelay = 700000 }: DocHandleOptions = {}
44
+ { isNew = false, timeoutDelay = 60_000 }: DocHandleOptions = {}
34
45
  ) {
35
46
  super()
36
47
  this.#timeoutDelay = timeoutDelay
37
- this.#log = debug(`automerge-repo:dochandle:${documentId.slice(0, 5)}`)
48
+ this.#log = debug(`automerge-repo:dochandle:${this.documentId.slice(0, 5)}`)
38
49
 
39
50
  // initial doc
40
- const doc = A.init<T>({
41
- patchCallback: (patches, patchInfo) =>
42
- this.emit("patch", { handle: this, patches, patchInfo }),
43
- })
51
+ const doc = A.init<T>()
44
52
 
45
53
  /**
46
54
  * Internally we use a state machine to orchestrate document loading and/or syncing, in order to
47
55
  * avoid requesting data we already have, or surfacing intermediate values to the consumer.
48
56
  *
49
- * ┌─────────┐ ┌────────────┐
50
- * ┌───────┐ ┌──FIND──┤ loading ├─REQUEST──►│ requesting ├─UPDATE──┐
57
+ * ┌─────────────────────┬─────────TIMEOUT────►┌────────┐
58
+ * ┌───┴─────┐ ┌───┴────────┐ │ failed
59
+ * ┌───────┐ ┌──FIND──┤ loading ├─REQUEST──►│ requesting ├─UPDATE──┐ └────────┘
51
60
  * │ idle ├──┤ └───┬─────┘ └────────────┘ │
52
- * └───────┘ │ │ └─►┌─────────┐
53
- * │ └───────LOAD───────────────────────────────►│ ready │
54
- * └──CREATE───────────────────────────────────────────────►└─────────┘
61
+ * └───────┘ │ │ └─►┌────────┐
62
+ * │ └───────LOAD───────────────────────────────►│ ready │
63
+ * └──CREATE───────────────────────────────────────────────►└────────┘
55
64
  */
56
65
  this.#machine = interpret(
57
66
  createMachine<DocHandleContext<T>, DocHandleEvent<T>>(
@@ -60,7 +69,7 @@ export class DocHandle<T> //
60
69
 
61
70
  id: "docHandle",
62
71
  initial: IDLE,
63
- context: { documentId, doc },
72
+ context: { documentId: this.documentId, doc },
64
73
  states: {
65
74
  idle: {
66
75
  on: {
@@ -80,6 +89,12 @@ export class DocHandle<T> //
80
89
  REQUEST: { target: REQUESTING },
81
90
  DELETE: { actions: "onDelete", target: DELETED },
82
91
  },
92
+ after: [
93
+ {
94
+ delay: this.#timeoutDelay,
95
+ target: FAILED,
96
+ },
97
+ ],
83
98
  },
84
99
  requesting: {
85
100
  on: {
@@ -89,6 +104,12 @@ export class DocHandle<T> //
89
104
  REQUEST_COMPLETE: { target: READY },
90
105
  DELETE: { actions: "onDelete", target: DELETED },
91
106
  },
107
+ after: [
108
+ {
109
+ delay: this.#timeoutDelay,
110
+ target: FAILED,
111
+ },
112
+ ],
92
113
  },
93
114
  ready: {
94
115
  on: {
@@ -97,8 +118,12 @@ export class DocHandle<T> //
97
118
  DELETE: { actions: "onDelete", target: DELETED },
98
119
  },
99
120
  },
100
- error: {},
101
- deleted: {},
121
+ failed: {
122
+ type: "final",
123
+ },
124
+ deleted: {
125
+ type: "final",
126
+ },
102
127
  },
103
128
  },
104
129
 
@@ -133,33 +158,36 @@ export class DocHandle<T> //
133
158
  const oldDoc = history?.context?.doc
134
159
  const newDoc = context.doc
135
160
 
161
+ this.#log(`${event} → ${state}`, newDoc)
162
+
136
163
  const docChanged = newDoc && oldDoc && !headsAreSame(newDoc, oldDoc)
137
164
  if (docChanged) {
138
- this.emit("change", { handle: this, doc: newDoc })
165
+ this.emit("heads-changed", { handle: this, doc: newDoc })
166
+
167
+ const patches = A.diff(newDoc, A.getHeads(oldDoc), A.getHeads(newDoc))
168
+ if (patches.length > 0) {
169
+ const source = "change" // TODO: pass along the source (load/change/network)
170
+ this.emit("change", {
171
+ handle: this,
172
+ doc: newDoc,
173
+ patches,
174
+ patchInfo: { before: oldDoc, after: newDoc, source },
175
+ })
176
+ }
177
+
139
178
  if (!this.isReady()) {
140
179
  this.#machine.send(REQUEST_COMPLETE)
141
180
  }
142
181
  }
143
- this.#log(`${event} → ${state}`, this.#doc)
144
182
  })
145
183
  .start()
146
184
 
147
185
  this.#machine.send(isNew ? CREATE : FIND)
148
186
  }
149
187
 
150
- get doc() {
151
- if (!this.isReady()) {
152
- throw new Error(
153
- `DocHandle#${this.documentId} is not ready. Check \`handle.isReady()\` before accessing the document.`
154
- )
155
- }
156
-
157
- return this.#doc
158
- }
159
-
160
188
  // PRIVATE
161
189
 
162
- /** Returns the current document */
190
+ /** Returns the current document, regardless of state */
163
191
  get #doc() {
164
192
  return this.#machine?.getSnapshot().context.doc
165
193
  }
@@ -175,7 +203,7 @@ export class DocHandle<T> //
175
203
  return Promise.any(
176
204
  awaitStates.map(state =>
177
205
  waitFor(this.#machine, s => s.matches(state), {
178
- timeout: this.#timeoutDelay, // match the delay above
206
+ timeout: this.#timeoutDelay * 2000, // longer than the delay above for testing
179
207
  })
180
208
  )
181
209
  )
@@ -183,19 +211,48 @@ export class DocHandle<T> //
183
211
 
184
212
  // PUBLIC
185
213
 
186
- isReady = () => this.#state === READY
187
- isReadyOrRequesting = () =>
188
- this.#state === READY || this.#state === REQUESTING
189
- isDeleted = () => this.#state === DELETED
214
+ /**
215
+ * Checks if the document is ready for accessing or changes.
216
+ * Note that for documents already stored locally this occurs before synchronization
217
+ * with any peers. We do not currently have an equivalent `whenSynced()`.
218
+ */
219
+ isReady = () => this.inState([HandleState.READY])
220
+ /**
221
+ * Checks if this document has been marked as deleted.
222
+ * Deleted documents are removed from local storage and the sync process.
223
+ * It's not currently possible at runtime to undelete a document.
224
+ * @returns true if the document has been marked as deleted
225
+ */
226
+ isDeleted = () => this.inState([HandleState.DELETED])
227
+ inState = (states: HandleState[]) =>
228
+ states.some(this.#machine?.getSnapshot().matches)
229
+
230
+ get state() {
231
+ return this.#machine?.getSnapshot().value
232
+ }
233
+
234
+ /**
235
+ * Use this to block until the document handle has finished loading.
236
+ * The async equivalent to checking `inState()`.
237
+ * @param awaitStates = [READY]
238
+ * @returns
239
+ */
240
+ async whenReady(awaitStates: HandleState[] = [READY]): Promise<void> {
241
+ await withTimeout(this.#statePromise(awaitStates), this.#timeoutDelay)
242
+ }
190
243
 
191
244
  /**
192
- * Returns the current document, waiting for the handle to be ready if necessary.
245
+ * Returns the current state of the Automerge document this handle manages.
246
+ * Note that this waits for the handle to be ready if necessary, and currently, if
247
+ * loading (or synchronization) fails, will never resolve.
248
+ *
249
+ * @param {awaitStates=[READY]} optional states to wait for, such as "LOADING". mostly for internal use.
193
250
  */
194
- async value(awaitStates: HandleState[] = [READY]) {
251
+ async doc(awaitStates: HandleState[] = [READY]): Promise<A.Doc<T>> {
195
252
  await pause() // yield one tick because reasons
196
253
  try {
197
254
  // wait for the document to enter one of the desired states
198
- await withTimeout(this.#statePromise(awaitStates), this.#timeoutDelay)
255
+ await this.#statePromise(awaitStates)
199
256
  } catch (error) {
200
257
  if (error instanceof TimeoutError)
201
258
  throw new Error(`DocHandle: timed out loading ${this.documentId}`)
@@ -205,20 +262,36 @@ export class DocHandle<T> //
205
262
  return this.#doc
206
263
  }
207
264
 
208
- async loadAttemptedValue() {
209
- return this.value([READY, REQUESTING])
265
+ /**
266
+ * Returns the current state of the Automerge document this handle manages, or undefined.
267
+ * Useful in a synchronous context. Consider using `await handle.doc()` instead, check `isReady()`,
268
+ * or use `whenReady()` if you want to make sure loading is complete first.
269
+ *
270
+ * Do not confuse this with the SyncState of the document, which describes the state of the synchronization process.
271
+ *
272
+ * Note that `undefined` is not a valid Automerge document so the return from this function is unambigous.
273
+ * @returns the current document, or undefined if the document is not ready
274
+ */
275
+ docSync(): A.Doc<T> | undefined {
276
+ if (!this.isReady()) {
277
+ return undefined
278
+ }
279
+
280
+ return this.#doc
210
281
  }
211
282
 
212
283
  /** `load` is called by the repo when the document is found in storage */
213
284
  load(binary: Uint8Array) {
214
- if (binary.length) {
285
+ if (binary.length && binary.length > 0) {
215
286
  this.#machine.send(LOAD, { payload: { binary } })
216
287
  }
217
288
  }
218
289
 
219
290
  /** `update` is called by the repo when we receive changes from the network */
220
291
  update(callback: (doc: A.Doc<T>) => A.Doc<T>) {
221
- this.#machine.send(UPDATE, { payload: { callback } })
292
+ this.#machine.send(UPDATE, {
293
+ payload: { callback },
294
+ })
222
295
  }
223
296
 
224
297
  /** `change` is called by the repo when the document is changed locally */
@@ -250,7 +323,7 @@ export class DocHandle<T> //
250
323
  this.#machine.send(UPDATE, {
251
324
  payload: {
252
325
  callback: (doc: A.Doc<T>) => {
253
- return A.changeAt(doc, heads, options, callback)
326
+ return A.changeAt(doc, heads, options, callback).newDoc
254
327
  },
255
328
  },
256
329
  })
@@ -280,7 +353,7 @@ export interface DocHandleMessagePayload {
280
353
  data: Uint8Array
281
354
  }
282
355
 
283
- export interface DocHandleChangePayload<T> {
356
+ export interface DocHandleEncodedChangePayload<T> {
284
357
  handle: DocHandle<T>
285
358
  doc: A.Doc<T>
286
359
  }
@@ -289,15 +362,16 @@ export interface DocHandleDeletePayload<T> {
289
362
  handle: DocHandle<T>
290
363
  }
291
364
 
292
- export interface DocHandlePatchPayload<T> {
365
+ export interface DocHandleChangePayload<T> {
293
366
  handle: DocHandle<T>
367
+ doc: A.Doc<T>
294
368
  patches: A.Patch[]
295
369
  patchInfo: A.PatchInfo<T>
296
370
  }
297
371
 
298
372
  export interface DocHandleEvents<T> {
373
+ "heads-changed": (payload: DocHandleEncodedChangePayload<T>) => void
299
374
  change: (payload: DocHandleChangePayload<T>) => void
300
- patch: (payload: DocHandlePatchPayload<T>) => void
301
375
  delete: (payload: DocHandleDeletePayload<T>) => void
302
376
  }
303
377
 
@@ -310,7 +384,7 @@ export const HandleState = {
310
384
  LOADING: "loading",
311
385
  REQUESTING: "requesting",
312
386
  READY: "ready",
313
- ERROR: "error",
387
+ FAILED: "failed",
314
388
  DELETED: "deleted",
315
389
  } as const
316
390
  export type HandleState = (typeof HandleState)[keyof typeof HandleState]
@@ -325,7 +399,7 @@ type DocHandleMachineState = {
325
399
  // context
326
400
 
327
401
  interface DocHandleContext<T> {
328
- documentId: string
402
+ documentId: DocumentId
329
403
  doc: A.Doc<T>
330
404
  }
331
405
 
@@ -383,7 +457,7 @@ type DocHandleXstateMachine<T> = Interpreter<
383
457
 
384
458
  // CONSTANTS
385
459
 
386
- const { IDLE, LOADING, REQUESTING, READY, ERROR, DELETED } = HandleState
460
+ export const { IDLE, LOADING, REQUESTING, READY, FAILED, DELETED } = HandleState
387
461
  const {
388
462
  CREATE,
389
463
  LOAD,
package/src/DocUrl.ts ADDED
@@ -0,0 +1,90 @@
1
+ import {
2
+ type AutomergeUrl,
3
+ type BinaryDocumentId,
4
+ type DocumentId,
5
+ } from "./types"
6
+ import { v4 as uuid } from "uuid"
7
+ import bs58check from "bs58check"
8
+
9
+ export const urlPrefix = "automerge:"
10
+
11
+ /**
12
+ * given an Automerge URL, return a decoded DocumentId (and the encoded DocumentId)
13
+ *
14
+ * @param url
15
+ * @returns { documentId: Uint8Array(16), encodedDocumentId: bs58check.encode(documentId) }
16
+ */
17
+ export const parseAutomergeUrl = (url: AutomergeUrl) => {
18
+ const { binaryDocumentId: binaryDocumentId, encodedDocumentId } = parts(url)
19
+ if (!binaryDocumentId) throw new Error("Invalid document URL: " + url)
20
+ return { binaryDocumentId, encodedDocumentId }
21
+ }
22
+
23
+ interface StringifyAutomergeUrlOptions {
24
+ documentId: DocumentId | BinaryDocumentId
25
+ }
26
+
27
+ /**
28
+ * Given a documentId in either canonical form, return an Automerge URL
29
+ * Throws on invalid input.
30
+ * Note: this is an object because we anticipate adding fields in the future.
31
+ * @param { documentId: EncodedDocumentId | DocumentId }
32
+ * @returns AutomergeUrl
33
+ */
34
+ export const stringifyAutomergeUrl = ({
35
+ documentId,
36
+ }: StringifyAutomergeUrlOptions): AutomergeUrl => {
37
+ if (documentId instanceof Uint8Array)
38
+ return (urlPrefix +
39
+ binaryToDocumentId(documentId as BinaryDocumentId)) as AutomergeUrl
40
+ else if (typeof documentId === "string") {
41
+ return (urlPrefix + documentId) as AutomergeUrl
42
+ }
43
+ throw new Error("Invalid documentId: " + documentId)
44
+ }
45
+
46
+ /**
47
+ * Given a string, return true if it is a valid Automerge URL
48
+ * also acts as a type discriminator in Typescript.
49
+ * @param str: URL candidate
50
+ * @returns boolean
51
+ */
52
+ export const isValidAutomergeUrl = (str: string): str is AutomergeUrl => {
53
+ if (!str.startsWith(urlPrefix)) return false
54
+
55
+ const { binaryDocumentId: documentId } = parts(str)
56
+ return documentId ? true : false
57
+ }
58
+
59
+ /**
60
+ * generateAutomergeUrl produces a new AutomergeUrl.
61
+ * generally only called by create(), but used in tests as well.
62
+ * @returns a new Automerge URL with a random UUID documentId
63
+ */
64
+ export const generateAutomergeUrl = (): AutomergeUrl =>
65
+ stringifyAutomergeUrl({
66
+ documentId: uuid(null, new Uint8Array(16)) as BinaryDocumentId,
67
+ })
68
+
69
+ export const documentIdToBinary = (
70
+ docId: DocumentId
71
+ ): BinaryDocumentId | undefined =>
72
+ bs58check.decodeUnsafe(docId) as BinaryDocumentId | undefined
73
+
74
+ export const binaryToDocumentId = (docId: BinaryDocumentId): DocumentId =>
75
+ bs58check.encode(docId) as DocumentId
76
+
77
+ /**
78
+ * parts breaks up the URL into constituent pieces,
79
+ * eventually this could include things like heads, so we use this structure
80
+ * we return both a binary & string-encoded version of the document ID
81
+ * @param str
82
+ * @returns { binaryDocumentId, encodedDocumentId }
83
+ */
84
+ const parts = (str: string) => {
85
+ const regex = new RegExp(`^${urlPrefix}(\\w+)$`)
86
+ const [m, docMatch] = str.match(regex) || []
87
+ const encodedDocumentId = docMatch as DocumentId
88
+ const binaryDocumentId = documentIdToBinary(encodedDocumentId)
89
+ return { binaryDocumentId, encodedDocumentId }
90
+ }
package/src/Repo.ts CHANGED
@@ -5,12 +5,10 @@ import { NetworkSubsystem } from "./network/NetworkSubsystem.js"
5
5
  import { StorageAdapter } from "./storage/StorageAdapter.js"
6
6
  import { StorageSubsystem } from "./storage/StorageSubsystem.js"
7
7
  import { CollectionSynchronizer } from "./synchronizer/CollectionSynchronizer.js"
8
- import { ChannelId, DocumentId, PeerId } from "./types.js"
8
+ import { DocumentId, PeerId } from "./types.js"
9
9
 
10
10
  import debug from "debug"
11
11
 
12
- const SYNC_CHANNEL = "sync_channel" as ChannelId
13
-
14
12
  /** A Repo is a DocCollection with networking, syncing, and storage capabilities. */
15
13
  export class Repo extends DocCollection {
16
14
  #log: debug.Debugger
@@ -31,8 +29,7 @@ export class Repo extends DocCollection {
31
29
  this.on("document", async ({ handle }) => {
32
30
  if (storageSubsystem) {
33
31
  // Save when the document changes
34
- handle.on("change", async ({ handle }) => {
35
- const doc = await handle.value()
32
+ handle.on("heads-changed", async ({ handle, doc }) => {
36
33
  await storageSubsystem.save(handle.documentId, doc)
37
34
  })
38
35
 
@@ -47,12 +44,12 @@ export class Repo extends DocCollection {
47
44
  synchronizer.addDocument(handle.documentId)
48
45
  })
49
46
 
50
- this.on("delete-document", ({ documentId }) => {
47
+ this.on("delete-document", ({ encodedDocumentId }) => {
51
48
  // TODO Pass the delete on to the network
52
49
  // synchronizer.removeDocument(documentId)
53
50
 
54
51
  if (storageSubsystem) {
55
- storageSubsystem.remove(documentId)
52
+ storageSubsystem.remove(encodedDocumentId)
56
53
  }
57
54
  })
58
55
 
@@ -112,7 +109,7 @@ export class Repo extends DocCollection {
112
109
  })
113
110
 
114
111
  // We establish a special channel for sync messages
115
- networkSubsystem.join(SYNC_CHANNEL)
112
+ networkSubsystem.join()
116
113
 
117
114
  // EPHEMERAL DATA
118
115
  // The ephemeral data subsystem uses the network to send and receive messages that are not