@automerge/automerge-repo 2.0.0-alpha.6 → 2.0.0-beta.1
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/README.md +8 -8
- package/dist/AutomergeUrl.d.ts +17 -5
- package/dist/AutomergeUrl.d.ts.map +1 -1
- package/dist/AutomergeUrl.js +71 -24
- package/dist/DocHandle.d.ts +87 -30
- package/dist/DocHandle.d.ts.map +1 -1
- package/dist/DocHandle.js +198 -48
- package/dist/FindProgress.d.ts +30 -0
- package/dist/FindProgress.d.ts.map +1 -0
- package/dist/FindProgress.js +1 -0
- package/dist/RemoteHeadsSubscriptions.d.ts +4 -5
- package/dist/RemoteHeadsSubscriptions.d.ts.map +1 -1
- package/dist/RemoteHeadsSubscriptions.js +4 -1
- package/dist/Repo.d.ts +46 -6
- package/dist/Repo.d.ts.map +1 -1
- package/dist/Repo.js +252 -67
- package/dist/helpers/abortable.d.ts +39 -0
- package/dist/helpers/abortable.d.ts.map +1 -0
- package/dist/helpers/abortable.js +45 -0
- package/dist/helpers/arraysAreEqual.d.ts.map +1 -1
- package/dist/helpers/bufferFromHex.d.ts +3 -0
- package/dist/helpers/bufferFromHex.d.ts.map +1 -0
- package/dist/helpers/bufferFromHex.js +13 -0
- package/dist/helpers/debounce.d.ts.map +1 -1
- package/dist/helpers/eventPromise.d.ts.map +1 -1
- package/dist/helpers/headsAreSame.d.ts +2 -2
- package/dist/helpers/headsAreSame.d.ts.map +1 -1
- package/dist/helpers/mergeArrays.d.ts +1 -1
- package/dist/helpers/mergeArrays.d.ts.map +1 -1
- package/dist/helpers/pause.d.ts.map +1 -1
- package/dist/helpers/tests/network-adapter-tests.d.ts.map +1 -1
- package/dist/helpers/tests/network-adapter-tests.js +13 -13
- package/dist/helpers/tests/storage-adapter-tests.d.ts +2 -2
- package/dist/helpers/tests/storage-adapter-tests.d.ts.map +1 -1
- package/dist/helpers/tests/storage-adapter-tests.js +25 -48
- package/dist/helpers/throttle.d.ts.map +1 -1
- package/dist/helpers/withTimeout.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/network/messages.d.ts.map +1 -1
- package/dist/storage/StorageSubsystem.d.ts +15 -1
- package/dist/storage/StorageSubsystem.d.ts.map +1 -1
- package/dist/storage/StorageSubsystem.js +50 -14
- package/dist/synchronizer/CollectionSynchronizer.d.ts +4 -3
- package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/CollectionSynchronizer.js +34 -15
- package/dist/synchronizer/DocSynchronizer.d.ts +3 -2
- package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/DocSynchronizer.js +51 -27
- package/dist/synchronizer/Synchronizer.d.ts +11 -0
- package/dist/synchronizer/Synchronizer.d.ts.map +1 -1
- package/dist/types.d.ts +4 -1
- package/dist/types.d.ts.map +1 -1
- package/fuzz/fuzz.ts +3 -3
- package/package.json +3 -4
- package/src/AutomergeUrl.ts +101 -26
- package/src/DocHandle.ts +268 -58
- package/src/FindProgress.ts +48 -0
- package/src/RemoteHeadsSubscriptions.ts +11 -9
- package/src/Repo.ts +364 -74
- package/src/helpers/abortable.ts +61 -0
- package/src/helpers/bufferFromHex.ts +14 -0
- package/src/helpers/headsAreSame.ts +2 -2
- package/src/helpers/tests/network-adapter-tests.ts +14 -13
- package/src/helpers/tests/storage-adapter-tests.ts +44 -86
- package/src/index.ts +7 -0
- package/src/storage/StorageSubsystem.ts +66 -16
- package/src/synchronizer/CollectionSynchronizer.ts +37 -16
- package/src/synchronizer/DocSynchronizer.ts +59 -32
- package/src/synchronizer/Synchronizer.ts +14 -0
- package/src/types.ts +4 -1
- package/test/AutomergeUrl.test.ts +130 -0
- package/test/CollectionSynchronizer.test.ts +4 -4
- package/test/DocHandle.test.ts +255 -30
- package/test/DocSynchronizer.test.ts +10 -3
- package/test/Repo.test.ts +376 -203
- package/test/StorageSubsystem.test.ts +80 -1
- package/test/remoteHeads.test.ts +27 -12
package/src/DocHandle.ts
CHANGED
|
@@ -2,11 +2,15 @@ import * as A from "@automerge/automerge/slim/next"
|
|
|
2
2
|
import debug from "debug"
|
|
3
3
|
import { EventEmitter } from "eventemitter3"
|
|
4
4
|
import { assertEvent, assign, createActor, setup, waitFor } from "xstate"
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
decodeHeads,
|
|
7
|
+
encodeHeads,
|
|
8
|
+
stringifyAutomergeUrl,
|
|
9
|
+
} from "./AutomergeUrl.js"
|
|
6
10
|
import { encode } from "./helpers/cbor.js"
|
|
7
11
|
import { headsAreSame } from "./helpers/headsAreSame.js"
|
|
8
12
|
import { withTimeout } from "./helpers/withTimeout.js"
|
|
9
|
-
import type { AutomergeUrl, DocumentId, PeerId } from "./types.js"
|
|
13
|
+
import type { AutomergeUrl, DocumentId, PeerId, UrlHeads } from "./types.js"
|
|
10
14
|
import { StorageId } from "./storage/types.js"
|
|
11
15
|
|
|
12
16
|
/**
|
|
@@ -28,6 +32,9 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
28
32
|
/** The XState actor running our state machine. */
|
|
29
33
|
#machine
|
|
30
34
|
|
|
35
|
+
/** If set, this handle will only show the document at these heads */
|
|
36
|
+
#fixedHeads?: UrlHeads
|
|
37
|
+
|
|
31
38
|
/** The last known state of our document. */
|
|
32
39
|
#prevDocState: T = A.init<T>()
|
|
33
40
|
|
|
@@ -36,7 +43,10 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
36
43
|
#timeoutDelay = 60_000
|
|
37
44
|
|
|
38
45
|
/** A dictionary mapping each peer to the last heads we know they have. */
|
|
39
|
-
#remoteHeads: Record<StorageId,
|
|
46
|
+
#remoteHeads: Record<StorageId, UrlHeads> = {}
|
|
47
|
+
|
|
48
|
+
/** Cache for view handles, keyed by the stringified heads */
|
|
49
|
+
#viewCache: Map<string, DocHandle<T>> = new Map()
|
|
40
50
|
|
|
41
51
|
/** @hidden */
|
|
42
52
|
constructor(
|
|
@@ -49,6 +59,10 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
49
59
|
this.#timeoutDelay = options.timeoutDelay
|
|
50
60
|
}
|
|
51
61
|
|
|
62
|
+
if ("heads" in options) {
|
|
63
|
+
this.#fixedHeads = options.heads
|
|
64
|
+
}
|
|
65
|
+
|
|
52
66
|
const doc = A.init<T>()
|
|
53
67
|
|
|
54
68
|
this.#log = debug(`automerge-repo:dochandle:${this.documentId.slice(0, 5)}`)
|
|
@@ -72,9 +86,12 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
72
86
|
this.emit("delete", { handle: this })
|
|
73
87
|
return { doc: A.init() }
|
|
74
88
|
}),
|
|
75
|
-
onUnavailable: () => {
|
|
76
|
-
|
|
77
|
-
},
|
|
89
|
+
onUnavailable: assign(() => {
|
|
90
|
+
return { doc: A.init() }
|
|
91
|
+
}),
|
|
92
|
+
onUnload: assign(() => {
|
|
93
|
+
return { doc: A.init() }
|
|
94
|
+
}),
|
|
78
95
|
},
|
|
79
96
|
}).createMachine({
|
|
80
97
|
/** @xstate-layout N4IgpgJg5mDOIC5QAoC2BDAxgCwJYDswBKAYgFUAFAEQEEAVAUQG0AGAXUVAAcB7WXAC64e+TiAAeiAOwAOAKwA6ACxSAzKqks1ATjlTdAGhABPRAFolAJksKN2y1KtKAbFLla5AX09G0WPISkVAwAMgyMrBxIILz8QiJikggAjCzOijKqLEqqybJyLizaRqYIFpbJtro5Uo7J2o5S3r4YOATECrgQADZgJADCAEoM9MzsYrGCwqLRSeoyCtra8pa5adquySXmDjY5ac7JljLJeepKzSB+bYGdPX0AYgCSAHJUkRN8UwmziM7HCgqyVcUnqcmScmcMm2ZV2yiyzkOx1OalUFx8V1aAQ63R46AgBCgJGGAEUyAwAMp0D7RSbxGagJKHFgKOSWJTJGRSCosCpKaEmRCqbQKU5yXINeTaer6LwY67YogKXH4wkkKgAeX6AH1hjQqABNGncL70xKIJQ5RY5BHOJag6wwpRyEWImQVeT1aWrVSXBXtJUqgn4Ik0ADqNCedG1L3CYY1gwA0saYqbpuaEG4pKLksKpFDgcsCjDhTnxTKpTLdH6sQGFOgAO7oKYhl5gAQNngAJwA1iRY3R40ndSNDSm6enfpm5BkWAVkvy7bpuTCKq7ndZnfVeSwuTX-HWu2AAI4AVzgQhD6q12rILxoADVIyEaAAhMLjtM-RmIE4LVSQi4nLLDIGzOCWwLKA0cgyLBoFWNy+43B0R5nheaqajqepjuMtJfgyEh-FoixqMCoKqOyhzgYKCDOq6UIeuCSxHOoSGKgop74OgABuzbdOgABGvTXlho5GrhJpxJOP4pLulT6KoMhpJY2hzsWNF0QobqMV6LG+pc+A8BAcBiP6gSfFJ36EQgKksksKxrHamwwmY7gLKB85QjBzoAWxdZdL0FnfARST8ooLC7qoTnWBU4pyC5ViVMKBQaHUDQuM4fm3EGhJBWaU7-CysEAUp3LpEpWw0WYRw2LmqzgqciIsCxWUdI2zaXlAbYdt2PZ5dJ1n5jY2iJY1ikOIcMJHCyUWHC62hRZkUVNPKta3Kh56wJ1-VWUyzhFc64JWJCtQNBBzhQW4cHwbsrVKpxPF8YJgV4ZZIWIKkiKiiNSkqZYWjzCWaQ5hFh0AcCuR3QoR74qUknBRmzholpv3OkpRQNNRpTzaKTWKbIWR5FDxm9AIkA7e9skUYCWayLILBZGoLkUSKbIyIdpxHPoyTeN4QA */
|
|
@@ -86,6 +103,7 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
86
103
|
context: { documentId, doc },
|
|
87
104
|
on: {
|
|
88
105
|
UPDATE: { actions: "onUpdate" },
|
|
106
|
+
UNLOAD: ".unloaded",
|
|
89
107
|
DELETE: ".deleted",
|
|
90
108
|
},
|
|
91
109
|
states: {
|
|
@@ -113,6 +131,12 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
113
131
|
on: { DOC_READY: "ready" },
|
|
114
132
|
},
|
|
115
133
|
ready: {},
|
|
134
|
+
unloaded: {
|
|
135
|
+
entry: "onUnload",
|
|
136
|
+
on: {
|
|
137
|
+
RELOAD: "loading",
|
|
138
|
+
},
|
|
139
|
+
},
|
|
116
140
|
deleted: { entry: "onDelete", type: "final" },
|
|
117
141
|
},
|
|
118
142
|
})
|
|
@@ -131,7 +155,7 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
131
155
|
|
|
132
156
|
// Start the machine, and send a create or find event to get things going
|
|
133
157
|
this.#machine.start()
|
|
134
|
-
this
|
|
158
|
+
this.begin()
|
|
135
159
|
}
|
|
136
160
|
|
|
137
161
|
// PRIVATE
|
|
@@ -166,7 +190,10 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
166
190
|
#checkForChanges(before: A.Doc<T>, after: A.Doc<T>) {
|
|
167
191
|
const beforeHeads = A.getHeads(before)
|
|
168
192
|
const afterHeads = A.getHeads(after)
|
|
169
|
-
const docChanged = !headsAreSame(
|
|
193
|
+
const docChanged = !headsAreSame(
|
|
194
|
+
encodeHeads(afterHeads),
|
|
195
|
+
encodeHeads(beforeHeads)
|
|
196
|
+
)
|
|
170
197
|
if (docChanged) {
|
|
171
198
|
this.emit("heads-changed", { handle: this, doc: after })
|
|
172
199
|
|
|
@@ -192,7 +219,10 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
192
219
|
/** Our documentId in Automerge URL form.
|
|
193
220
|
*/
|
|
194
221
|
get url(): AutomergeUrl {
|
|
195
|
-
return stringifyAutomergeUrl({
|
|
222
|
+
return stringifyAutomergeUrl({
|
|
223
|
+
documentId: this.documentId,
|
|
224
|
+
heads: this.#fixedHeads,
|
|
225
|
+
})
|
|
196
226
|
}
|
|
197
227
|
|
|
198
228
|
/**
|
|
@@ -203,6 +233,14 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
203
233
|
*/
|
|
204
234
|
isReady = () => this.inState(["ready"])
|
|
205
235
|
|
|
236
|
+
/**
|
|
237
|
+
* @returns true if the document has been unloaded.
|
|
238
|
+
*
|
|
239
|
+
* Unloaded documents are freed from memory but not removed from local storage. It's not currently
|
|
240
|
+
* possible at runtime to reload an unloaded document.
|
|
241
|
+
*/
|
|
242
|
+
isUnloaded = () => this.inState(["unloaded"])
|
|
243
|
+
|
|
206
244
|
/**
|
|
207
245
|
* @returns true if the document has been marked as deleted.
|
|
208
246
|
*
|
|
@@ -241,42 +279,28 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
241
279
|
}
|
|
242
280
|
|
|
243
281
|
/**
|
|
244
|
-
*
|
|
282
|
+
* Returns the current state of the Automerge document this handle manages.
|
|
283
|
+
*
|
|
284
|
+
* @returns the current document
|
|
285
|
+
* @throws on deleted and unavailable documents
|
|
245
286
|
*
|
|
246
|
-
* This is the recommended way to access a handle's document. Note that this waits for the handle
|
|
247
|
-
* to be ready if necessary. If loading (or synchronization) fails, this will never resolve.
|
|
248
287
|
*/
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
try {
|
|
254
|
-
// wait for the document to enter one of the desired states
|
|
255
|
-
await this.#statePromise(awaitStates)
|
|
256
|
-
} catch (error) {
|
|
257
|
-
// if we timed out, return undefined
|
|
258
|
-
return undefined
|
|
288
|
+
doc() {
|
|
289
|
+
if (!this.isReady()) throw new Error("DocHandle is not ready")
|
|
290
|
+
if (this.#fixedHeads) {
|
|
291
|
+
return A.view(this.#doc, decodeHeads(this.#fixedHeads))
|
|
259
292
|
}
|
|
260
|
-
|
|
261
|
-
return !this.isUnavailable() ? this.#doc : undefined
|
|
293
|
+
return this.#doc
|
|
262
294
|
}
|
|
263
295
|
|
|
264
296
|
/**
|
|
265
|
-
* Synchronously returns the current state of the Automerge document this handle manages, or
|
|
266
|
-
* undefined. Consider using `await handle.doc()` instead. Check `isReady()`, or use `whenReady()`
|
|
267
|
-
* if you want to make sure loading is complete first.
|
|
268
297
|
*
|
|
269
|
-
*
|
|
270
|
-
* synchronization process.
|
|
271
|
-
*
|
|
272
|
-
* Note that `undefined` is not a valid Automerge document, so the return from this function is
|
|
273
|
-
* unambigous.
|
|
274
|
-
*
|
|
275
|
-
* @returns the current document, or undefined if the document is not ready.
|
|
276
|
-
*/
|
|
298
|
+
* @deprecated */
|
|
277
299
|
docSync() {
|
|
278
|
-
|
|
279
|
-
|
|
300
|
+
console.warn(
|
|
301
|
+
"docSync is deprecated. Use doc() instead. This function will be removed as part of the 2.0 release."
|
|
302
|
+
)
|
|
303
|
+
return this.doc()
|
|
280
304
|
}
|
|
281
305
|
|
|
282
306
|
/**
|
|
@@ -284,11 +308,155 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
284
308
|
* This precisely defines the state of a document.
|
|
285
309
|
* @returns the current document's heads, or undefined if the document is not ready
|
|
286
310
|
*/
|
|
287
|
-
heads():
|
|
311
|
+
heads(): UrlHeads {
|
|
312
|
+
if (!this.isReady()) throw new Error("DocHandle is not ready")
|
|
313
|
+
if (this.#fixedHeads) {
|
|
314
|
+
return this.#fixedHeads
|
|
315
|
+
}
|
|
316
|
+
return encodeHeads(A.getHeads(this.#doc))
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
begin() {
|
|
320
|
+
this.#machine.send({ type: BEGIN })
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Returns an array of all past "heads" for the document in topological order.
|
|
325
|
+
*
|
|
326
|
+
* @remarks
|
|
327
|
+
* A point-in-time in an automerge document is an *array* of heads since there may be
|
|
328
|
+
* concurrent edits. This API just returns a topologically sorted history of all edits
|
|
329
|
+
* so every previous entry will be (in some sense) before later ones, but the set of all possible
|
|
330
|
+
* history views would be quite large under concurrency (every thing in each branch against each other).
|
|
331
|
+
* There might be a clever way to think about this, but we haven't found it yet, so for now at least
|
|
332
|
+
* we present a single traversable view which excludes concurrency.
|
|
333
|
+
* @returns UrlHeads[] - The individual heads for every change in the document. Each item is a tagged string[1].
|
|
334
|
+
*/
|
|
335
|
+
history(): UrlHeads[] | undefined {
|
|
336
|
+
if (!this.isReady()) {
|
|
337
|
+
return undefined
|
|
338
|
+
}
|
|
339
|
+
// This just returns all the heads as individual strings.
|
|
340
|
+
|
|
341
|
+
return A.topoHistoryTraversal(this.#doc).map(h =>
|
|
342
|
+
encodeHeads([h])
|
|
343
|
+
) as UrlHeads[]
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Creates a fixed "view" of an automerge document at the given point in time represented
|
|
348
|
+
* by the `heads` passed in. The return value is the same type as doc() and will return
|
|
349
|
+
* undefined if the object hasn't finished loading.
|
|
350
|
+
*
|
|
351
|
+
* @remarks
|
|
352
|
+
* Note that our Typescript types do not consider change over time and the current version
|
|
353
|
+
* of Automerge doesn't check types at runtime, so if you go back to an old set of heads
|
|
354
|
+
* that doesn't match the heads here, Typescript will not save you.
|
|
355
|
+
*
|
|
356
|
+
* @argument heads - The heads to view the document at. See history().
|
|
357
|
+
* @returns DocHandle<T> at the time of `heads`
|
|
358
|
+
*/
|
|
359
|
+
view(heads: UrlHeads): DocHandle<T> {
|
|
360
|
+
if (!this.isReady()) {
|
|
361
|
+
throw new Error(
|
|
362
|
+
`DocHandle#${this.documentId} is not ready. Check \`handle.isReady()\` before calling view().`
|
|
363
|
+
)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Create a cache key from the heads
|
|
367
|
+
const cacheKey = JSON.stringify(heads)
|
|
368
|
+
|
|
369
|
+
// Check if we have a cached handle for these heads
|
|
370
|
+
const cachedHandle = this.#viewCache.get(cacheKey)
|
|
371
|
+
if (cachedHandle) {
|
|
372
|
+
return cachedHandle
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Create a new handle with the same documentId but fixed heads
|
|
376
|
+
const handle = new DocHandle<T>(this.documentId, {
|
|
377
|
+
heads,
|
|
378
|
+
timeoutDelay: this.#timeoutDelay,
|
|
379
|
+
})
|
|
380
|
+
handle.update(() => A.clone(this.#doc))
|
|
381
|
+
handle.doneLoading()
|
|
382
|
+
|
|
383
|
+
// Store in cache
|
|
384
|
+
this.#viewCache.set(cacheKey, handle)
|
|
385
|
+
|
|
386
|
+
return handle
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Returns a set of Patch operations that will move a materialized document from one state to another
|
|
391
|
+
* if applied.
|
|
392
|
+
*
|
|
393
|
+
* @remarks
|
|
394
|
+
* We allow specifying either:
|
|
395
|
+
* - Two sets of heads to compare directly
|
|
396
|
+
* - A single set of heads to compare against our current heads
|
|
397
|
+
* - Another DocHandle to compare against (which must share history with this document)
|
|
398
|
+
*
|
|
399
|
+
* @throws Error if the documents don't share history or if either document is not ready
|
|
400
|
+
* @returns Automerge patches that go from one document state to the other
|
|
401
|
+
*/
|
|
402
|
+
diff(first: UrlHeads | DocHandle<T>, second?: UrlHeads): A.Patch[] {
|
|
403
|
+
if (!this.isReady()) {
|
|
404
|
+
throw new Error(
|
|
405
|
+
`DocHandle#${this.documentId} is not ready. Check \`handle.isReady()\` before calling diff().`
|
|
406
|
+
)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const doc = this.#doc
|
|
410
|
+
if (!doc) throw new Error("Document not available")
|
|
411
|
+
|
|
412
|
+
// If first argument is a DocHandle
|
|
413
|
+
if (first instanceof DocHandle) {
|
|
414
|
+
if (!first.isReady()) {
|
|
415
|
+
throw new Error("Cannot diff against a handle that isn't ready")
|
|
416
|
+
}
|
|
417
|
+
const otherHeads = first.heads()
|
|
418
|
+
if (!otherHeads) throw new Error("Other document's heads not available")
|
|
419
|
+
|
|
420
|
+
// Create a temporary merged doc to verify shared history and compute diff
|
|
421
|
+
const mergedDoc = A.merge(A.clone(doc), first.doc()!)
|
|
422
|
+
// Use the merged doc to compute the diff
|
|
423
|
+
return A.diff(
|
|
424
|
+
mergedDoc,
|
|
425
|
+
decodeHeads(this.heads()!),
|
|
426
|
+
decodeHeads(otherHeads)
|
|
427
|
+
)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Otherwise treat as heads
|
|
431
|
+
const from = second ? first : ((this.heads() || []) as UrlHeads)
|
|
432
|
+
const to = second ? second : first
|
|
433
|
+
return A.diff(doc, decodeHeads(from), decodeHeads(to))
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* `metadata(head?)` allows you to look at the metadata for a change
|
|
438
|
+
* this can be used to build history graphs to find commit messages and edit times.
|
|
439
|
+
* this interface.
|
|
440
|
+
*
|
|
441
|
+
* @remarks
|
|
442
|
+
* I'm really not convinced this is the right way to surface this information so
|
|
443
|
+
* I'm leaving this API "hidden".
|
|
444
|
+
*
|
|
445
|
+
* @hidden
|
|
446
|
+
*/
|
|
447
|
+
metadata(change?: string): A.DecodedChange | undefined {
|
|
288
448
|
if (!this.isReady()) {
|
|
289
449
|
return undefined
|
|
290
450
|
}
|
|
291
|
-
|
|
451
|
+
|
|
452
|
+
if (!change) {
|
|
453
|
+
change = this.heads()![0]
|
|
454
|
+
}
|
|
455
|
+
// we return undefined instead of null by convention in this API
|
|
456
|
+
return (
|
|
457
|
+
A.inspectChange(this.#doc, decodeHeads([change] as UrlHeads)[0]) ||
|
|
458
|
+
undefined
|
|
459
|
+
)
|
|
292
460
|
}
|
|
293
461
|
|
|
294
462
|
/**
|
|
@@ -311,16 +479,16 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
311
479
|
}
|
|
312
480
|
|
|
313
481
|
/**
|
|
314
|
-
* Called by the repo
|
|
482
|
+
* Called by the repo when a doc handle changes or we receive new remote heads.
|
|
315
483
|
* @hidden
|
|
316
484
|
*/
|
|
317
|
-
setRemoteHeads(storageId: StorageId, heads:
|
|
485
|
+
setRemoteHeads(storageId: StorageId, heads: UrlHeads) {
|
|
318
486
|
this.#remoteHeads[storageId] = heads
|
|
319
487
|
this.emit("remote-heads", { storageId, heads })
|
|
320
488
|
}
|
|
321
489
|
|
|
322
490
|
/** Returns the heads of the storageId. */
|
|
323
|
-
getRemoteHeads(storageId: StorageId):
|
|
491
|
+
getRemoteHeads(storageId: StorageId): UrlHeads | undefined {
|
|
324
492
|
return this.#remoteHeads[storageId]
|
|
325
493
|
}
|
|
326
494
|
|
|
@@ -345,6 +513,13 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
345
513
|
`DocHandle#${this.documentId} is in ${this.state} and not ready. Check \`handle.isReady()\` before accessing the document.`
|
|
346
514
|
)
|
|
347
515
|
}
|
|
516
|
+
|
|
517
|
+
if (this.#fixedHeads) {
|
|
518
|
+
throw new Error(
|
|
519
|
+
`DocHandle#${this.documentId} is in view-only mode at specific heads. Use clone() to create a new document from this state.`
|
|
520
|
+
)
|
|
521
|
+
}
|
|
522
|
+
|
|
348
523
|
this.#machine.send({
|
|
349
524
|
type: UPDATE,
|
|
350
525
|
payload: { callback: doc => A.change(doc, options, callback) },
|
|
@@ -356,22 +531,29 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
356
531
|
* @returns A set of heads representing the concurrent change that was made.
|
|
357
532
|
*/
|
|
358
533
|
changeAt(
|
|
359
|
-
heads:
|
|
534
|
+
heads: UrlHeads,
|
|
360
535
|
callback: A.ChangeFn<T>,
|
|
361
536
|
options: A.ChangeOptions<T> = {}
|
|
362
|
-
):
|
|
537
|
+
): UrlHeads[] | undefined {
|
|
363
538
|
if (!this.isReady()) {
|
|
364
539
|
throw new Error(
|
|
365
540
|
`DocHandle#${this.documentId} is not ready. Check \`handle.isReady()\` before accessing the document.`
|
|
366
541
|
)
|
|
367
542
|
}
|
|
368
|
-
|
|
543
|
+
if (this.#fixedHeads) {
|
|
544
|
+
throw new Error(
|
|
545
|
+
`DocHandle#${this.documentId} is in view-only mode at specific heads. Use clone() to create a new document from this state.`
|
|
546
|
+
)
|
|
547
|
+
}
|
|
548
|
+
let resultHeads: UrlHeads | undefined = undefined
|
|
369
549
|
this.#machine.send({
|
|
370
550
|
type: UPDATE,
|
|
371
551
|
payload: {
|
|
372
552
|
callback: doc => {
|
|
373
|
-
const result = A.changeAt(doc, heads, options, callback)
|
|
374
|
-
resultHeads = result.newHeads
|
|
553
|
+
const result = A.changeAt(doc, decodeHeads(heads), options, callback)
|
|
554
|
+
resultHeads = result.newHeads
|
|
555
|
+
? encodeHeads(result.newHeads)
|
|
556
|
+
: undefined
|
|
375
557
|
return result.newDoc
|
|
376
558
|
},
|
|
377
559
|
},
|
|
@@ -396,10 +578,12 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
396
578
|
if (!this.isReady() || !otherHandle.isReady()) {
|
|
397
579
|
throw new Error("Both handles must be ready to merge")
|
|
398
580
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
581
|
+
if (this.#fixedHeads) {
|
|
582
|
+
throw new Error(
|
|
583
|
+
`DocHandle#${this.documentId} is in view-only mode at specific heads. Use clone() to create a new document from this state.`
|
|
584
|
+
)
|
|
402
585
|
}
|
|
586
|
+
const mergingDoc = otherHandle.doc()
|
|
403
587
|
|
|
404
588
|
this.update(doc => {
|
|
405
589
|
return A.merge(doc, mergingDoc)
|
|
@@ -407,20 +591,31 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
407
591
|
}
|
|
408
592
|
|
|
409
593
|
/**
|
|
410
|
-
*
|
|
594
|
+
* Updates the internal state machine to mark the document unavailable.
|
|
411
595
|
* @hidden
|
|
412
596
|
*/
|
|
413
597
|
unavailable() {
|
|
414
598
|
this.#machine.send({ type: DOC_UNAVAILABLE })
|
|
415
599
|
}
|
|
416
600
|
|
|
417
|
-
/**
|
|
601
|
+
/**
|
|
602
|
+
* Called by the repo either when the document is not found in storage.
|
|
418
603
|
* @hidden
|
|
419
604
|
* */
|
|
420
605
|
request() {
|
|
421
606
|
if (this.#state === "loading") this.#machine.send({ type: REQUEST })
|
|
422
607
|
}
|
|
423
608
|
|
|
609
|
+
/** Called by the repo to free memory used by the document. */
|
|
610
|
+
unload() {
|
|
611
|
+
this.#machine.send({ type: UNLOAD })
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/** Called by the repo to reuse an unloaded handle. */
|
|
615
|
+
reload() {
|
|
616
|
+
this.#machine.send({ type: RELOAD })
|
|
617
|
+
}
|
|
618
|
+
|
|
424
619
|
/** Called by the repo when the document is deleted. */
|
|
425
620
|
delete() {
|
|
426
621
|
this.#machine.send({ type: DELETE })
|
|
@@ -436,7 +631,7 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
436
631
|
broadcast(message: unknown) {
|
|
437
632
|
this.emit("ephemeral-message-outbound", {
|
|
438
633
|
handle: this,
|
|
439
|
-
data: encode(message),
|
|
634
|
+
data: new Uint8Array(encode(message)),
|
|
440
635
|
})
|
|
441
636
|
}
|
|
442
637
|
|
|
@@ -461,6 +656,9 @@ export type DocHandleOptions<T> =
|
|
|
461
656
|
| {
|
|
462
657
|
isNew?: false
|
|
463
658
|
|
|
659
|
+
// An optional point in time to lock the document to.
|
|
660
|
+
heads?: UrlHeads
|
|
661
|
+
|
|
464
662
|
/** The number of milliseconds before we mark this document as unavailable if we don't have it and nobody shares it with us. */
|
|
465
663
|
timeoutDelay?: number
|
|
466
664
|
}
|
|
@@ -472,7 +670,6 @@ export interface DocHandleEvents<T> {
|
|
|
472
670
|
"heads-changed": (payload: DocHandleEncodedChangePayload<T>) => void
|
|
473
671
|
change: (payload: DocHandleChangePayload<T>) => void
|
|
474
672
|
delete: (payload: DocHandleDeletePayload<T>) => void
|
|
475
|
-
unavailable: (payload: DocHandleUnavailablePayload<T>) => void
|
|
476
673
|
"ephemeral-message": (payload: DocHandleEphemeralMessagePayload<T>) => void
|
|
477
674
|
"ephemeral-message-outbound": (
|
|
478
675
|
payload: DocHandleOutboundEphemeralMessagePayload<T>
|
|
@@ -524,7 +721,7 @@ export interface DocHandleOutboundEphemeralMessagePayload<T> {
|
|
|
524
721
|
/** Emitted when we have new remote heads for this document */
|
|
525
722
|
export interface DocHandleRemoteHeadsPayload {
|
|
526
723
|
storageId: StorageId
|
|
527
|
-
heads:
|
|
724
|
+
heads: UrlHeads
|
|
528
725
|
}
|
|
529
726
|
|
|
530
727
|
// STATE MACHINE TYPES & CONSTANTS
|
|
@@ -543,6 +740,8 @@ export const HandleState = {
|
|
|
543
740
|
REQUESTING: "requesting",
|
|
544
741
|
/** The document is available */
|
|
545
742
|
READY: "ready",
|
|
743
|
+
/** The document has been unloaded from the handle, to free memory usage */
|
|
744
|
+
UNLOADED: "unloaded",
|
|
546
745
|
/** The document has been deleted from the repo */
|
|
547
746
|
DELETED: "deleted",
|
|
548
747
|
/** The document was not available in storage or from any connected peers */
|
|
@@ -550,8 +749,15 @@ export const HandleState = {
|
|
|
550
749
|
} as const
|
|
551
750
|
export type HandleState = (typeof HandleState)[keyof typeof HandleState]
|
|
552
751
|
|
|
553
|
-
export const {
|
|
554
|
-
|
|
752
|
+
export const {
|
|
753
|
+
IDLE,
|
|
754
|
+
LOADING,
|
|
755
|
+
REQUESTING,
|
|
756
|
+
READY,
|
|
757
|
+
UNLOADED,
|
|
758
|
+
DELETED,
|
|
759
|
+
UNAVAILABLE,
|
|
760
|
+
} = HandleState
|
|
555
761
|
|
|
556
762
|
// context
|
|
557
763
|
|
|
@@ -571,14 +777,18 @@ type DocHandleEvent<T> =
|
|
|
571
777
|
type: typeof UPDATE
|
|
572
778
|
payload: { callback: (doc: A.Doc<T>) => A.Doc<T> }
|
|
573
779
|
}
|
|
574
|
-
| { type: typeof
|
|
780
|
+
| { type: typeof UNLOAD }
|
|
781
|
+
| { type: typeof RELOAD }
|
|
575
782
|
| { type: typeof DELETE }
|
|
783
|
+
| { type: typeof TIMEOUT }
|
|
576
784
|
| { type: typeof DOC_UNAVAILABLE }
|
|
577
785
|
|
|
578
786
|
const BEGIN = "BEGIN"
|
|
579
787
|
const REQUEST = "REQUEST"
|
|
580
788
|
const DOC_READY = "DOC_READY"
|
|
581
789
|
const UPDATE = "UPDATE"
|
|
790
|
+
const UNLOAD = "UNLOAD"
|
|
791
|
+
const RELOAD = "RELOAD"
|
|
582
792
|
const DELETE = "DELETE"
|
|
583
793
|
const TIMEOUT = "TIMEOUT"
|
|
584
794
|
const DOC_UNAVAILABLE = "DOC_UNAVAILABLE"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { DocHandle } from "./DocHandle.js"
|
|
2
|
+
|
|
3
|
+
export type FindProgressState =
|
|
4
|
+
| "loading"
|
|
5
|
+
| "ready"
|
|
6
|
+
| "failed"
|
|
7
|
+
| "aborted"
|
|
8
|
+
| "unavailable"
|
|
9
|
+
|
|
10
|
+
interface FindProgressBase<T> {
|
|
11
|
+
state: FindProgressState
|
|
12
|
+
handle: DocHandle<T>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface FindProgressLoading<T> extends FindProgressBase<T> {
|
|
16
|
+
state: "loading"
|
|
17
|
+
progress: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface FindProgressReady<T> extends FindProgressBase<T> {
|
|
21
|
+
state: "ready"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface FindProgressFailed<T> extends FindProgressBase<T> {
|
|
25
|
+
state: "failed"
|
|
26
|
+
error: Error
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface FindProgressUnavailable<T> extends FindProgressBase<T> {
|
|
30
|
+
state: "unavailable"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface FindProgressAborted<T> extends FindProgressBase<T> {
|
|
34
|
+
state: "aborted"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type FindProgress<T> =
|
|
38
|
+
| FindProgressLoading<T>
|
|
39
|
+
| FindProgressReady<T>
|
|
40
|
+
| FindProgressFailed<T>
|
|
41
|
+
| FindProgressUnavailable<T>
|
|
42
|
+
| FindProgressAborted<T>
|
|
43
|
+
|
|
44
|
+
export type FindProgressWithMethods<T> = FindProgress<T> & {
|
|
45
|
+
next: () => Promise<FindProgressWithMethods<T>>
|
|
46
|
+
// TODO: i don't like this allowableStates
|
|
47
|
+
untilReady: (allowableStates: string[]) => Promise<DocHandle<T>>
|
|
48
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { next as A } from "@automerge/automerge/slim"
|
|
2
1
|
import { EventEmitter } from "eventemitter3"
|
|
3
|
-
import { DocumentId, PeerId } from "./types.js"
|
|
2
|
+
import { DocumentId, PeerId, UrlHeads } from "./types.js"
|
|
4
3
|
import {
|
|
5
4
|
RemoteHeadsChanged,
|
|
6
5
|
RemoteSubscriptionControlMessage,
|
|
@@ -12,7 +11,7 @@ import debug from "debug"
|
|
|
12
11
|
export type RemoteHeadsSubscriptionEventPayload = {
|
|
13
12
|
documentId: DocumentId
|
|
14
13
|
storageId: StorageId
|
|
15
|
-
remoteHeads:
|
|
14
|
+
remoteHeads: UrlHeads
|
|
16
15
|
timestamp: number
|
|
17
16
|
}
|
|
18
17
|
|
|
@@ -21,7 +20,7 @@ export type NotifyRemoteHeadsPayload = {
|
|
|
21
20
|
targetId: PeerId
|
|
22
21
|
documentId: DocumentId
|
|
23
22
|
storageId: StorageId
|
|
24
|
-
heads:
|
|
23
|
+
heads: UrlHeads
|
|
25
24
|
timestamp: number
|
|
26
25
|
}
|
|
27
26
|
|
|
@@ -216,7 +215,7 @@ export class RemoteHeadsSubscriptions extends EventEmitter<RemoteHeadsSubscripti
|
|
|
216
215
|
handleImmediateRemoteHeadsChanged(
|
|
217
216
|
documentId: DocumentId,
|
|
218
217
|
storageId: StorageId,
|
|
219
|
-
heads:
|
|
218
|
+
heads: UrlHeads
|
|
220
219
|
) {
|
|
221
220
|
this.#log("handleLocalHeadsChanged", documentId, storageId, heads)
|
|
222
221
|
const remote = this.#knownHeads.get(documentId)
|
|
@@ -334,7 +333,7 @@ export class RemoteHeadsSubscriptions extends EventEmitter<RemoteHeadsSubscripti
|
|
|
334
333
|
#changedHeads(msg: RemoteHeadsChanged): {
|
|
335
334
|
documentId: DocumentId
|
|
336
335
|
storageId: StorageId
|
|
337
|
-
remoteHeads:
|
|
336
|
+
remoteHeads: UrlHeads
|
|
338
337
|
timestamp: number
|
|
339
338
|
}[] {
|
|
340
339
|
const changedHeads = []
|
|
@@ -356,11 +355,14 @@ export class RemoteHeadsSubscriptions extends EventEmitter<RemoteHeadsSubscripti
|
|
|
356
355
|
if (docRemote && docRemote.timestamp >= timestamp) {
|
|
357
356
|
continue
|
|
358
357
|
} else {
|
|
359
|
-
remote.set(storageId as StorageId, {
|
|
358
|
+
remote.set(storageId as StorageId, {
|
|
359
|
+
timestamp,
|
|
360
|
+
heads: heads as UrlHeads,
|
|
361
|
+
})
|
|
360
362
|
changedHeads.push({
|
|
361
363
|
documentId,
|
|
362
364
|
storageId: storageId as StorageId,
|
|
363
|
-
remoteHeads: heads,
|
|
365
|
+
remoteHeads: heads as UrlHeads,
|
|
364
366
|
timestamp,
|
|
365
367
|
})
|
|
366
368
|
}
|
|
@@ -371,5 +373,5 @@ export class RemoteHeadsSubscriptions extends EventEmitter<RemoteHeadsSubscripti
|
|
|
371
373
|
|
|
372
374
|
type LastHeads = {
|
|
373
375
|
timestamp: number
|
|
374
|
-
heads:
|
|
376
|
+
heads: UrlHeads
|
|
375
377
|
}
|