@automerge/automerge-repo 1.1.5 → 1.1.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/README.md +3 -22
- package/dist/DocHandle.d.ts +124 -100
- package/dist/DocHandle.d.ts.map +1 -1
- package/dist/DocHandle.js +235 -229
- package/dist/Repo.d.ts +7 -8
- package/dist/Repo.d.ts.map +1 -1
- package/dist/Repo.js +9 -12
- package/dist/helpers/arraysAreEqual.d.ts.map +1 -1
- package/dist/helpers/debounce.d.ts.map +1 -1
- package/dist/helpers/throttle.d.ts.map +1 -1
- package/dist/helpers/withTimeout.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/storage/StorageSubsystem.d.ts +0 -5
- package/dist/storage/StorageSubsystem.d.ts.map +1 -1
- package/dist/storage/StorageSubsystem.js +0 -27
- package/package.json +4 -4
- package/src/DocHandle.ts +320 -372
- package/src/Repo.ts +12 -15
- package/src/index.ts +43 -0
- package/src/storage/StorageSubsystem.ts +0 -32
- package/test/CollectionSynchronizer.test.ts +1 -3
- package/test/DocHandle.test.ts +19 -1
- package/test/DocSynchronizer.test.ts +1 -4
- package/test/Repo.test.ts +82 -71
- package/test/remoteHeads.test.ts +1 -1
- package/tsconfig.json +1 -0
package/src/DocHandle.ts
CHANGED
|
@@ -1,19 +1,7 @@
|
|
|
1
1
|
import * as A from "@automerge/automerge/next"
|
|
2
2
|
import debug from "debug"
|
|
3
3
|
import { EventEmitter } from "eventemitter3"
|
|
4
|
-
import {
|
|
5
|
-
assign,
|
|
6
|
-
BaseActionObject,
|
|
7
|
-
createMachine,
|
|
8
|
-
interpret,
|
|
9
|
-
Interpreter,
|
|
10
|
-
ResolveTypegenMeta,
|
|
11
|
-
ServiceMap,
|
|
12
|
-
StateSchema,
|
|
13
|
-
StateValue,
|
|
14
|
-
TypegenDisabled,
|
|
15
|
-
} from "xstate"
|
|
16
|
-
import { waitFor } from "xstate/lib/waitFor.js"
|
|
4
|
+
import { assertEvent, assign, createActor, setup, waitFor } from "xstate"
|
|
17
5
|
import { stringifyAutomergeUrl } from "./AutomergeUrl.js"
|
|
18
6
|
import { encode } from "./helpers/cbor.js"
|
|
19
7
|
import { headsAreSame } from "./helpers/headsAreSame.js"
|
|
@@ -21,35 +9,34 @@ import { withTimeout } from "./helpers/withTimeout.js"
|
|
|
21
9
|
import type { AutomergeUrl, DocumentId, PeerId } from "./types.js"
|
|
22
10
|
import { StorageId } from "./storage/types.js"
|
|
23
11
|
|
|
24
|
-
/**
|
|
25
|
-
*
|
|
12
|
+
/**
|
|
13
|
+
* A DocHandle is a wrapper around a single Automerge document that lets us listen for changes and
|
|
14
|
+
* notify the network and storage of new changes.
|
|
26
15
|
*
|
|
27
16
|
* @remarks
|
|
28
|
-
* A `DocHandle` represents a document which is being managed by a {@link Repo}.
|
|
29
|
-
* To obtain `DocHandle` use {@link Repo.find} or {@link Repo.create}.
|
|
17
|
+
* A `DocHandle` represents a document which is being managed by a {@link Repo}. You shouldn't ever
|
|
18
|
+
* instantiate this yourself. To obtain `DocHandle` use {@link Repo.find} or {@link Repo.create}.
|
|
30
19
|
*
|
|
31
20
|
* To modify the underlying document use either {@link DocHandle.change} or
|
|
32
|
-
* {@link DocHandle.changeAt}. These methods will notify the `Repo` that some
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
|
|
36
|
-
export class DocHandle<T>
|
|
37
|
-
extends EventEmitter<DocHandleEvents<T>>
|
|
38
|
-
{
|
|
21
|
+
* {@link DocHandle.changeAt}. These methods will notify the `Repo` that some change has occured and
|
|
22
|
+
* the `Repo` will save any new changes to the attached {@link StorageAdapter} and send sync
|
|
23
|
+
* messages to connected peers.
|
|
24
|
+
*/
|
|
25
|
+
export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
39
26
|
#log: debug.Debugger
|
|
40
27
|
|
|
41
|
-
|
|
28
|
+
/** The XState actor running our state machine. */
|
|
29
|
+
#machine
|
|
30
|
+
|
|
31
|
+
/** The last known state of our document. */
|
|
32
|
+
#prevDocState: T | undefined
|
|
33
|
+
|
|
34
|
+
/** How long to wait before giving up on a document. (Note that a document will be marked
|
|
35
|
+
* unavailable much sooner if all known peers respond that they don't have it.) */
|
|
42
36
|
#timeoutDelay = 60_000
|
|
43
|
-
#remoteHeads: Record<StorageId, A.Heads> = {}
|
|
44
37
|
|
|
45
|
-
/**
|
|
46
|
-
|
|
47
|
-
* @remarks
|
|
48
|
-
* This can be used to request the document from an instance of {@link Repo}
|
|
49
|
-
*/
|
|
50
|
-
get url(): AutomergeUrl {
|
|
51
|
-
return stringifyAutomergeUrl({ documentId: this.documentId })
|
|
52
|
-
}
|
|
38
|
+
/** A dictionary mapping each peer to the last heads we know they have. */
|
|
39
|
+
#remoteHeads: Record<StorageId, A.Heads> = {}
|
|
53
40
|
|
|
54
41
|
/** @hidden */
|
|
55
42
|
constructor(
|
|
@@ -58,8 +45,6 @@ export class DocHandle<T> //
|
|
|
58
45
|
) {
|
|
59
46
|
super()
|
|
60
47
|
|
|
61
|
-
this.documentId = documentId
|
|
62
|
-
|
|
63
48
|
if ("timeoutDelay" in options && options.timeoutDelay) {
|
|
64
49
|
this.#timeoutDelay = options.timeoutDelay
|
|
65
50
|
}
|
|
@@ -78,156 +63,90 @@ export class DocHandle<T> //
|
|
|
78
63
|
|
|
79
64
|
this.#log = debug(`automerge-repo:dochandle:${this.documentId.slice(0, 5)}`)
|
|
80
65
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
idle: {
|
|
103
|
-
on: {
|
|
104
|
-
// If we're creating a new document, we don't need to load anything
|
|
105
|
-
CREATE: { target: READY },
|
|
106
|
-
// If we're accessing an existing document, we need to request it from storage
|
|
107
|
-
// and/or the network
|
|
108
|
-
FIND: { target: LOADING },
|
|
109
|
-
DELETE: { actions: "onDelete", target: DELETED },
|
|
110
|
-
},
|
|
111
|
-
},
|
|
112
|
-
loading: {
|
|
113
|
-
on: {
|
|
114
|
-
// UPDATE is called by the Repo if the document is found in storage
|
|
115
|
-
UPDATE: { actions: "onUpdate", target: READY },
|
|
116
|
-
// REQUEST is called by the Repo if the document is not found in storage
|
|
117
|
-
REQUEST: { target: REQUESTING },
|
|
118
|
-
// AWAIT_NETWORK is called by the repo if the document is not found in storage but the network is not yet ready
|
|
119
|
-
AWAIT_NETWORK: { target: AWAITING_NETWORK },
|
|
120
|
-
DELETE: { actions: "onDelete", target: DELETED },
|
|
121
|
-
},
|
|
122
|
-
after: [
|
|
123
|
-
{
|
|
124
|
-
delay: this.#timeoutDelay,
|
|
125
|
-
target: UNAVAILABLE,
|
|
126
|
-
},
|
|
127
|
-
],
|
|
128
|
-
},
|
|
129
|
-
awaitingNetwork: {
|
|
130
|
-
on: {
|
|
131
|
-
NETWORK_READY: { target: REQUESTING },
|
|
132
|
-
},
|
|
133
|
-
},
|
|
134
|
-
requesting: {
|
|
135
|
-
on: {
|
|
136
|
-
MARK_UNAVAILABLE: {
|
|
137
|
-
target: UNAVAILABLE,
|
|
138
|
-
actions: "onUnavailable",
|
|
139
|
-
},
|
|
140
|
-
// UPDATE is called by the Repo when we receive changes from the network
|
|
141
|
-
UPDATE: { actions: "onUpdate" },
|
|
142
|
-
// REQUEST_COMPLETE is called from `onUpdate` when the doc has been fully loaded from the network
|
|
143
|
-
REQUEST_COMPLETE: { target: READY },
|
|
144
|
-
DELETE: { actions: "onDelete", target: DELETED },
|
|
145
|
-
},
|
|
146
|
-
after: [
|
|
147
|
-
{
|
|
148
|
-
delay: this.#timeoutDelay,
|
|
149
|
-
target: UNAVAILABLE,
|
|
150
|
-
},
|
|
151
|
-
],
|
|
152
|
-
},
|
|
153
|
-
ready: {
|
|
154
|
-
on: {
|
|
155
|
-
// UPDATE is called by the Repo when we receive changes from the network
|
|
156
|
-
UPDATE: { actions: "onUpdate", target: READY },
|
|
157
|
-
DELETE: { actions: "onDelete", target: DELETED },
|
|
158
|
-
},
|
|
159
|
-
},
|
|
160
|
-
deleted: {
|
|
161
|
-
type: "final",
|
|
162
|
-
},
|
|
163
|
-
unavailable: {
|
|
164
|
-
on: {
|
|
165
|
-
UPDATE: { actions: "onUpdate" },
|
|
166
|
-
// REQUEST_COMPLETE is called from `onUpdate` when the doc has been fully loaded from the network
|
|
167
|
-
REQUEST_COMPLETE: { target: READY },
|
|
168
|
-
DELETE: { actions: "onDelete", target: DELETED },
|
|
169
|
-
},
|
|
170
|
-
},
|
|
171
|
-
},
|
|
66
|
+
const delay = this.#timeoutDelay
|
|
67
|
+
const machine = setup({
|
|
68
|
+
types: {
|
|
69
|
+
context: {} as DocHandleContext<T>,
|
|
70
|
+
events: {} as DocHandleEvent<T>,
|
|
71
|
+
},
|
|
72
|
+
actions: {
|
|
73
|
+
/** Update the doc using the given callback and put the modified doc in context */
|
|
74
|
+
onUpdate: assign(({ context, event }) => {
|
|
75
|
+
const oldDoc = context.doc
|
|
76
|
+
assertEvent(event, UPDATE)
|
|
77
|
+
const { callback } = event.payload
|
|
78
|
+
const doc = callback(oldDoc)
|
|
79
|
+
return { doc }
|
|
80
|
+
}),
|
|
81
|
+
onDelete: assign(() => {
|
|
82
|
+
this.emit("delete", { handle: this })
|
|
83
|
+
return { doc: undefined }
|
|
84
|
+
}),
|
|
85
|
+
onUnavailable: () => {
|
|
86
|
+
this.emit("unavailable", { handle: this })
|
|
172
87
|
},
|
|
88
|
+
},
|
|
89
|
+
}).createMachine({
|
|
90
|
+
/** @xstate-layout N4IgpgJg5mDOIC5QAoC2BDAxgCwJYDswBKAYgFUAFAEQEEAVAUQG0AGAXUVAAcB7WXAC64e+TiAAeiAOwAOAKwA6ACxSAzKqks1ATjlTdAGhABPRAFolAJksKN2y1KtKAbFLla5AX09G0WPISkVAwAMgyMrBxIILz8QiJikggAjCzOijKqLEqqybJyLizaRqYIFpbJtro5Uo7J2o5S3r4YOATECrgQADZgJADCAEoM9MzsYrGCwqLRSeoyCtra8pa5adquySXmDjY5ac7JljLJeepKzSB+bYGdPX0AYgCSAHJUkRN8UwmziM7HCgqyVcUnqcmScmcMm2ZV2yiyzkOx1OalUFx8V1aAQ63R46AgBCgJGGAEUyAwAMp0D7RSbxGagJKHFgKOSWJTJGRSCosCpKaEmRCqbQKU5yXINeTaer6LwY67YogKXH4wkkKgAeX6AH1hjQqABNGncL70xKIJQ5RY5BHOJag6wwpRyEWImQVeT1aWrVSXBXtJUqgn4Ik0ADqNCedG1L3CYY1gwA0saYqbpuaEG4pKLksKpFDgcsCjDhTnxTKpTLdH6sQGFOgAO7oKYhl5gAQNngAJwA1iRY3R40ndSNDSm6enfpm5BkWAVkvy7bpuTCKq7ndZnfVeSwuTX-HWu2AAI4AVzgQhD6q12rILxoADVIyEaAAhMLjtM-RmIE4LVSQi4nLLDIGzOCWwLKA0cgyLBoFWNy+43B0R5nheaqajqepjuMtJfgyEh-FoixqMCoKqOyhzgYKCDOq6UIeuCSxHOoSGKgop74OgABuzbdOgABGvTXlho5GrhJpxJOP4pLulT6KoMhpJY2hzsWNF0QobqMV6LG+pc+A8BAcBiP6gSfFJ36EQgKksksKxrHamwwmY7gLKB85QjBzoAWxdZdL0FnfARST8ooLC7qoTnWBU4pyC5ViVMKBQaHUDQuM4fm3EGhJBWaU7-CysEAUp3LpEpWw0WYRw2LmqzgqciIsCxWUdI2zaXlAbYdt2PZ5dJ1n5jY2iJY1ikOIcMJHCyUWHC62hRZkUVNPKta3Kh56wJ1-VWUyzhFc64JWJCtQNBBzhQW4cHwbsrVKpxPF8YJgV4ZZIWIKkiKiiNSkqZYWjzCWaQ5hFh0AcCuR3QoR74qUknBRmzholpv3OkpRQNNRpTzaKTWKbIWR5FDxm9AIkA7e9skUYCWayLILBZGoLkUSKbIyIdpxHPoyTeN4QA */
|
|
173
91
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}),
|
|
189
|
-
onUnavailable: assign(context => {
|
|
190
|
-
const { doc } = context
|
|
191
|
-
|
|
192
|
-
this.emit("unavailable", { handle: this })
|
|
193
|
-
return { doc }
|
|
194
|
-
}),
|
|
92
|
+
// You can use the XState extension for VS Code to visualize this machine.
|
|
93
|
+
// Or, you can see this static visualization (last updated April 2024): https://stately.ai/registry/editor/d7af9b58-c518-44f1-9c36-92a238b04a7a?machineId=91c387e7-0f01-42c9-a21d-293e9bf95bb7
|
|
94
|
+
|
|
95
|
+
initial: "idle",
|
|
96
|
+
context: { documentId, doc },
|
|
97
|
+
on: {
|
|
98
|
+
UPDATE: { actions: "onUpdate" },
|
|
99
|
+
DELETE: ".deleted",
|
|
100
|
+
},
|
|
101
|
+
states: {
|
|
102
|
+
idle: {
|
|
103
|
+
on: {
|
|
104
|
+
CREATE: "ready",
|
|
105
|
+
FIND: "loading",
|
|
195
106
|
},
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
107
|
+
},
|
|
108
|
+
loading: {
|
|
109
|
+
on: {
|
|
110
|
+
REQUEST: "requesting",
|
|
111
|
+
DOC_READY: "ready",
|
|
112
|
+
AWAIT_NETWORK: "awaitingNetwork",
|
|
113
|
+
},
|
|
114
|
+
after: { [delay]: "unavailable" },
|
|
115
|
+
},
|
|
116
|
+
awaitingNetwork: {
|
|
117
|
+
on: { NETWORK_READY: "requesting" },
|
|
118
|
+
},
|
|
119
|
+
requesting: {
|
|
120
|
+
on: {
|
|
121
|
+
DOC_UNAVAILABLE: "unavailable",
|
|
122
|
+
DOC_READY: "ready",
|
|
123
|
+
},
|
|
124
|
+
after: { [delay]: "unavailable" },
|
|
125
|
+
},
|
|
126
|
+
unavailable: {
|
|
127
|
+
entry: "onUnavailable",
|
|
128
|
+
on: { DOC_READY: "ready" },
|
|
129
|
+
},
|
|
130
|
+
ready: {},
|
|
131
|
+
deleted: { entry: "onDelete", type: "final" },
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
// Instantiate the state machine
|
|
136
|
+
this.#machine = createActor(machine)
|
|
137
|
+
|
|
138
|
+
// Listen for state transitions
|
|
139
|
+
this.#machine.subscribe(state => {
|
|
140
|
+
const before = this.#prevDocState
|
|
141
|
+
const after = state.context.doc
|
|
142
|
+
this.#log(`→ ${state.value} %o`, after)
|
|
143
|
+
// if the document has changed, emit a change event
|
|
144
|
+
this.#checkForChanges(before, after)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
// Start the machine, and send a create or find event to get things going
|
|
148
|
+
this.#machine.start()
|
|
149
|
+
this.#machine.send(isNew ? { type: CREATE } : { type: FIND })
|
|
231
150
|
}
|
|
232
151
|
|
|
233
152
|
// PRIVATE
|
|
@@ -255,55 +174,101 @@ export class DocHandle<T> //
|
|
|
255
174
|
)
|
|
256
175
|
}
|
|
257
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Called after state transitions. If the document has changed, emits a change event. If we just
|
|
179
|
+
* received the document for the first time, signal that our request has been completed.
|
|
180
|
+
*/
|
|
181
|
+
#checkForChanges(before: T | undefined, after: T) {
|
|
182
|
+
const docChanged =
|
|
183
|
+
after && before && !headsAreSame(A.getHeads(after), A.getHeads(before))
|
|
184
|
+
if (docChanged) {
|
|
185
|
+
this.emit("heads-changed", { handle: this, doc: after })
|
|
186
|
+
|
|
187
|
+
const patches = A.diff(after, A.getHeads(before), A.getHeads(after))
|
|
188
|
+
if (patches.length > 0) {
|
|
189
|
+
this.emit("change", {
|
|
190
|
+
handle: this,
|
|
191
|
+
doc: after,
|
|
192
|
+
patches,
|
|
193
|
+
// TODO: pass along the source (load/change/network)
|
|
194
|
+
patchInfo: { before, after, source: "change" },
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// If we didn't have the document yet, signal that we now do
|
|
199
|
+
if (!this.isReady()) this.#machine.send({ type: DOC_READY })
|
|
200
|
+
}
|
|
201
|
+
this.#prevDocState = after
|
|
202
|
+
}
|
|
203
|
+
|
|
258
204
|
// PUBLIC
|
|
259
205
|
|
|
206
|
+
/** Our documentId in Automerge URL form.
|
|
207
|
+
*/
|
|
208
|
+
get url(): AutomergeUrl {
|
|
209
|
+
return stringifyAutomergeUrl({ documentId: this.documentId })
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* @returns true if the document is ready for accessing or changes.
|
|
214
|
+
*
|
|
215
|
+
* Note that for documents already stored locally this occurs before synchronization with any
|
|
216
|
+
* peers. We do not currently have an equivalent `whenSynced()`.
|
|
217
|
+
*/
|
|
218
|
+
isReady = () => this.inState(["ready"])
|
|
219
|
+
|
|
260
220
|
/**
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
221
|
+
* @returns true if the document has been marked as deleted.
|
|
222
|
+
*
|
|
223
|
+
* Deleted documents are removed from local storage and the sync process. It's not currently
|
|
224
|
+
* possible at runtime to undelete a document.
|
|
264
225
|
*/
|
|
265
|
-
|
|
226
|
+
isDeleted = () => this.inState(["deleted"])
|
|
227
|
+
|
|
266
228
|
/**
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
|
|
229
|
+
* @returns true if the document is currently unavailable.
|
|
230
|
+
*
|
|
231
|
+
* This will be the case if the document is not found in storage and no peers have shared it with us.
|
|
232
|
+
*/
|
|
233
|
+
isUnavailable = () => this.inState(["unavailable"])
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* @returns true if the handle is in one of the given states.
|
|
271
237
|
*/
|
|
272
|
-
isDeleted = () => this.inState([HandleState.DELETED])
|
|
273
|
-
isUnavailable = () => this.inState([HandleState.UNAVAILABLE])
|
|
274
238
|
inState = (states: HandleState[]) =>
|
|
275
|
-
states.some(this.#machine
|
|
239
|
+
states.some(s => this.#machine.getSnapshot().matches(s))
|
|
276
240
|
|
|
277
241
|
/** @hidden */
|
|
278
242
|
get state() {
|
|
279
|
-
return this.#machine
|
|
243
|
+
return this.#machine.getSnapshot().value
|
|
280
244
|
}
|
|
281
245
|
|
|
282
246
|
/**
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
247
|
+
* @returns a promise that resolves when the document is in one of the given states (if no states
|
|
248
|
+
* are passed, when the document is ready)
|
|
249
|
+
*
|
|
250
|
+
* Use this to block until the document handle has finished loading. The async equivalent to
|
|
251
|
+
* checking `inState()`.
|
|
287
252
|
*/
|
|
288
|
-
async whenReady(awaitStates: HandleState[] = [
|
|
253
|
+
async whenReady(awaitStates: HandleState[] = ["ready"]) {
|
|
289
254
|
await withTimeout(this.#statePromise(awaitStates), this.#timeoutDelay)
|
|
290
255
|
}
|
|
291
256
|
|
|
292
257
|
/**
|
|
293
|
-
*
|
|
294
|
-
* Note that this waits for the handle to be ready if necessary, and currently, if
|
|
295
|
-
* loading (or synchronization) fails, will never resolve.
|
|
258
|
+
* @returns the current state of this handle's Automerge document.
|
|
296
259
|
*
|
|
297
|
-
*
|
|
260
|
+
* This is the recommended way to access a handle's document. Note that this waits for the handle
|
|
261
|
+
* to be ready if necessary. If loading (or synchronization) fails, this will never resolve.
|
|
298
262
|
*/
|
|
299
263
|
async doc(
|
|
300
|
-
|
|
301
|
-
|
|
264
|
+
/** states to wait for, such as "LOADING". mostly for internal use. */
|
|
265
|
+
awaitStates: HandleState[] = ["ready", "unavailable"]
|
|
266
|
+
) {
|
|
302
267
|
try {
|
|
303
268
|
// wait for the document to enter one of the desired states
|
|
304
269
|
await this.#statePromise(awaitStates)
|
|
305
270
|
} catch (error) {
|
|
306
|
-
// if we timed out
|
|
271
|
+
// if we timed out, return undefined
|
|
307
272
|
return undefined
|
|
308
273
|
}
|
|
309
274
|
// Return the document
|
|
@@ -311,33 +276,46 @@ export class DocHandle<T> //
|
|
|
311
276
|
}
|
|
312
277
|
|
|
313
278
|
/**
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
279
|
+
* Synchronously returns the current state of the Automerge document this handle manages, or
|
|
280
|
+
* undefined. Consider using `await handle.doc()` instead. Check `isReady()`, or use `whenReady()`
|
|
281
|
+
* if you want to make sure loading is complete first.
|
|
317
282
|
*
|
|
318
|
-
*
|
|
283
|
+
* Not to be confused with the SyncState of the document, which describes the state of the
|
|
284
|
+
* synchronization process.
|
|
319
285
|
*
|
|
320
|
-
* Note that `undefined` is not a valid Automerge document so the return from this function is
|
|
321
|
-
*
|
|
286
|
+
* Note that `undefined` is not a valid Automerge document, so the return from this function is
|
|
287
|
+
* unambigous.
|
|
288
|
+
*
|
|
289
|
+
* @returns the current document, or undefined if the document is not ready.
|
|
290
|
+
*/
|
|
291
|
+
docSync() {
|
|
292
|
+
if (!this.isReady()) return undefined
|
|
293
|
+
else return this.#doc
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Returns the current "heads" of the document, akin to a git commit.
|
|
298
|
+
* This precisely defines the state of a document.
|
|
299
|
+
* @returns the current document's heads, or undefined if the document is not ready
|
|
322
300
|
*/
|
|
323
|
-
|
|
301
|
+
heads(): A.Heads | undefined {
|
|
324
302
|
if (!this.isReady()) {
|
|
325
303
|
return undefined
|
|
326
304
|
}
|
|
327
|
-
|
|
328
|
-
return this.#doc
|
|
305
|
+
return A.getHeads(this.#doc)
|
|
329
306
|
}
|
|
330
307
|
|
|
331
|
-
/**
|
|
308
|
+
/**
|
|
309
|
+
* `update` is called by the repo when we receive changes from the network
|
|
310
|
+
* Called by the repo when we receive changes from the network.
|
|
332
311
|
* @hidden
|
|
333
|
-
|
|
312
|
+
*/
|
|
334
313
|
update(callback: (doc: A.Doc<T>) => A.Doc<T>) {
|
|
335
|
-
this.#machine.send(UPDATE, {
|
|
336
|
-
payload: { callback },
|
|
337
|
-
})
|
|
314
|
+
this.#machine.send({ type: UPDATE, payload: { callback } })
|
|
338
315
|
}
|
|
339
316
|
|
|
340
|
-
/**
|
|
317
|
+
/**
|
|
318
|
+
* Called by the repo either when a doc handle changes or we receive new remote heads.
|
|
341
319
|
* @hidden
|
|
342
320
|
*/
|
|
343
321
|
setRemoteHeads(storageId: StorageId, heads: A.Heads) {
|
|
@@ -345,28 +323,39 @@ export class DocHandle<T> //
|
|
|
345
323
|
this.emit("remote-heads", { storageId, heads })
|
|
346
324
|
}
|
|
347
325
|
|
|
348
|
-
/** Returns the heads of the storageId */
|
|
326
|
+
/** Returns the heads of the storageId. */
|
|
349
327
|
getRemoteHeads(storageId: StorageId): A.Heads | undefined {
|
|
350
328
|
return this.#remoteHeads[storageId]
|
|
351
329
|
}
|
|
352
330
|
|
|
353
|
-
/**
|
|
331
|
+
/**
|
|
332
|
+
* All changes to an Automerge document should be made through this method.
|
|
333
|
+
* Inside the callback, the document should be treated as mutable: all edits will be recorded
|
|
334
|
+
* using a Proxy and translated into operations as part of a single recorded "change".
|
|
335
|
+
*
|
|
336
|
+
* Note that assignment via ES6 spread operators will result in *replacing* the object
|
|
337
|
+
* instead of mutating it which will prevent clean merges. This may be what you want, but
|
|
338
|
+
* `doc.foo = { ...doc.foo, bar: "baz" }` is not equivalent to `doc.foo.bar = "baz"`.
|
|
339
|
+
*
|
|
340
|
+
* Local changes will be stored (by the StorageSubsystem) and synchronized (by the
|
|
341
|
+
* DocSynchronizer) to any peers you are sharing it with.
|
|
342
|
+
*
|
|
343
|
+
* @param callback - A function that takes the current document and mutates it.
|
|
344
|
+
*
|
|
345
|
+
*/
|
|
354
346
|
change(callback: A.ChangeFn<T>, options: A.ChangeOptions<T> = {}) {
|
|
355
347
|
if (!this.isReady()) {
|
|
356
348
|
throw new Error(
|
|
357
349
|
`DocHandle#${this.documentId} is not ready. Check \`handle.isReady()\` before accessing the document.`
|
|
358
350
|
)
|
|
359
351
|
}
|
|
360
|
-
this.#machine.send(
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
return A.change(doc, options, callback)
|
|
364
|
-
},
|
|
365
|
-
},
|
|
352
|
+
this.#machine.send({
|
|
353
|
+
type: UPDATE,
|
|
354
|
+
payload: { callback: doc => A.change(doc, options, callback) },
|
|
366
355
|
})
|
|
367
356
|
}
|
|
368
|
-
|
|
369
|
-
|
|
357
|
+
/**
|
|
358
|
+
* Makes a change as if the document were at `heads`.
|
|
370
359
|
*
|
|
371
360
|
* @returns A set of heads representing the concurrent change that was made.
|
|
372
361
|
*/
|
|
@@ -381,37 +370,39 @@ export class DocHandle<T> //
|
|
|
381
370
|
)
|
|
382
371
|
}
|
|
383
372
|
let resultHeads: string[] | undefined = undefined
|
|
384
|
-
this.#machine.send(
|
|
373
|
+
this.#machine.send({
|
|
374
|
+
type: UPDATE,
|
|
385
375
|
payload: {
|
|
386
|
-
callback:
|
|
376
|
+
callback: doc => {
|
|
387
377
|
const result = A.changeAt(doc, heads, options, callback)
|
|
388
378
|
resultHeads = result.newHeads || undefined
|
|
389
379
|
return result.newDoc
|
|
390
380
|
},
|
|
391
381
|
},
|
|
392
382
|
})
|
|
383
|
+
|
|
384
|
+
// the callback above will always run before we get here, so this should always contain the new heads
|
|
393
385
|
return resultHeads
|
|
394
386
|
}
|
|
395
387
|
|
|
396
|
-
/**
|
|
397
|
-
*
|
|
398
|
-
*
|
|
388
|
+
/**
|
|
389
|
+
* Merges another document into this document. Any peers we are sharing changes with will be
|
|
390
|
+
* notified of the changes resulting from the merge.
|
|
399
391
|
*
|
|
400
|
-
* @
|
|
401
|
-
* This is a convenience method for
|
|
402
|
-
* `handle.change(doc => A.merge(doc, otherHandle.docSync()))`. Any peers
|
|
403
|
-
* whom we are sharing changes with will be notified of the changes resulting
|
|
404
|
-
* from the merge.
|
|
392
|
+
* @returns the merged document.
|
|
405
393
|
*
|
|
406
|
-
* @throws if either document is not ready or if `otherHandle` is unavailable
|
|
394
|
+
* @throws if either document is not ready or if `otherHandle` is unavailable.
|
|
407
395
|
*/
|
|
408
|
-
merge(
|
|
396
|
+
merge(
|
|
397
|
+
/** the handle of the document to merge into this one */
|
|
398
|
+
otherHandle: DocHandle<T>
|
|
399
|
+
) {
|
|
409
400
|
if (!this.isReady() || !otherHandle.isReady()) {
|
|
410
401
|
throw new Error("Both handles must be ready to merge")
|
|
411
402
|
}
|
|
412
403
|
const mergingDoc = otherHandle.docSync()
|
|
413
404
|
if (!mergingDoc) {
|
|
414
|
-
throw new Error("The document to be merged in is
|
|
405
|
+
throw new Error("The document to be merged in is falsy, aborting.")
|
|
415
406
|
}
|
|
416
407
|
|
|
417
408
|
this.update(doc => {
|
|
@@ -419,37 +410,43 @@ export class DocHandle<T> //
|
|
|
419
410
|
})
|
|
420
411
|
}
|
|
421
412
|
|
|
413
|
+
/**
|
|
414
|
+
* Used in testing to mark this document as unavailable.
|
|
415
|
+
* @hidden
|
|
416
|
+
*/
|
|
422
417
|
unavailable() {
|
|
423
|
-
this.#machine.send(
|
|
418
|
+
this.#machine.send({ type: DOC_UNAVAILABLE })
|
|
424
419
|
}
|
|
425
420
|
|
|
426
|
-
/**
|
|
421
|
+
/** Called by the repo when the document is not found in storage.
|
|
427
422
|
* @hidden
|
|
428
423
|
* */
|
|
429
424
|
request() {
|
|
430
|
-
if (this.#state ===
|
|
425
|
+
if (this.#state === "loading") this.#machine.send({ type: REQUEST })
|
|
431
426
|
}
|
|
432
427
|
|
|
433
428
|
/** @hidden */
|
|
434
429
|
awaitNetwork() {
|
|
435
|
-
if (this.#state ===
|
|
430
|
+
if (this.#state === "loading") this.#machine.send({ type: AWAIT_NETWORK })
|
|
436
431
|
}
|
|
437
432
|
|
|
438
433
|
/** @hidden */
|
|
439
434
|
networkReady() {
|
|
440
|
-
if (this.#state ===
|
|
435
|
+
if (this.#state === "awaitingNetwork")
|
|
436
|
+
this.#machine.send({ type: NETWORK_READY })
|
|
441
437
|
}
|
|
442
438
|
|
|
443
|
-
/**
|
|
439
|
+
/** Called by the repo when the document is deleted. */
|
|
444
440
|
delete() {
|
|
445
|
-
this.#machine.send(DELETE)
|
|
441
|
+
this.#machine.send({ type: DELETE })
|
|
446
442
|
}
|
|
447
443
|
|
|
448
|
-
/**
|
|
449
|
-
*
|
|
450
|
-
*
|
|
451
|
-
*
|
|
452
|
-
*
|
|
444
|
+
/**
|
|
445
|
+
* Sends an arbitrary ephemeral message out to all reachable peers who would receive sync messages
|
|
446
|
+
* from you. It has no guarantee of delivery, and is not persisted to the underlying automerge doc
|
|
447
|
+
* in any way. Messages will have a sending PeerId but this is *not* a useful user identifier (a
|
|
448
|
+
* user could have multiple tabs open and would appear as multiple PeerIds). Every message source
|
|
449
|
+
* must have a unique PeerId.
|
|
453
450
|
*/
|
|
454
451
|
broadcast(message: unknown) {
|
|
455
452
|
this.emit("ephemeral-message-outbound", {
|
|
@@ -459,7 +456,7 @@ export class DocHandle<T> //
|
|
|
459
456
|
}
|
|
460
457
|
}
|
|
461
458
|
|
|
462
|
-
//
|
|
459
|
+
// TYPES
|
|
463
460
|
|
|
464
461
|
/** @hidden */
|
|
465
462
|
export type DocHandleOptions<T> =
|
|
@@ -479,24 +476,30 @@ export type DocHandleOptions<T> =
|
|
|
479
476
|
timeoutDelay?: number
|
|
480
477
|
}
|
|
481
478
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
479
|
+
// EXTERNAL EVENTS
|
|
480
|
+
|
|
481
|
+
/** These are the events that this DocHandle emits to external listeners */
|
|
482
|
+
export interface DocHandleEvents<T> {
|
|
483
|
+
"heads-changed": (payload: DocHandleEncodedChangePayload<T>) => void
|
|
484
|
+
change: (payload: DocHandleChangePayload<T>) => void
|
|
485
|
+
delete: (payload: DocHandleDeletePayload<T>) => void
|
|
486
|
+
unavailable: (payload: DocHandleUnavailablePayload<T>) => void
|
|
487
|
+
"ephemeral-message": (payload: DocHandleEphemeralMessagePayload<T>) => void
|
|
488
|
+
"ephemeral-message-outbound": (
|
|
489
|
+
payload: DocHandleOutboundEphemeralMessagePayload<T>
|
|
490
|
+
) => void
|
|
491
|
+
"remote-heads": (payload: DocHandleRemoteHeadsPayload) => void
|
|
486
492
|
}
|
|
487
493
|
|
|
494
|
+
/** Emitted when this document's heads have changed */
|
|
488
495
|
export interface DocHandleEncodedChangePayload<T> {
|
|
489
496
|
handle: DocHandle<T>
|
|
490
497
|
doc: A.Doc<T>
|
|
491
498
|
}
|
|
492
499
|
|
|
493
|
-
|
|
494
|
-
handle: DocHandle<T>
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
/** Emitted when a document has changed */
|
|
500
|
+
/** Emitted when this document has changed */
|
|
498
501
|
export interface DocHandleChangePayload<T> {
|
|
499
|
-
/** The
|
|
502
|
+
/** The handle that changed */
|
|
500
503
|
handle: DocHandle<T>
|
|
501
504
|
/** The value of the document after the change */
|
|
502
505
|
doc: A.Doc<T>
|
|
@@ -506,47 +509,41 @@ export interface DocHandleChangePayload<T> {
|
|
|
506
509
|
patchInfo: A.PatchInfo<T>
|
|
507
510
|
}
|
|
508
511
|
|
|
512
|
+
/** Emitted when this document is deleted */
|
|
513
|
+
export interface DocHandleDeletePayload<T> {
|
|
514
|
+
handle: DocHandle<T>
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/** Emitted when this document has been marked unavailable */
|
|
518
|
+
export interface DocHandleUnavailablePayload<T> {
|
|
519
|
+
handle: DocHandle<T>
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/** Emitted when an ephemeral message is received for the document */
|
|
509
523
|
export interface DocHandleEphemeralMessagePayload<T> {
|
|
510
524
|
handle: DocHandle<T>
|
|
511
525
|
senderId: PeerId
|
|
512
526
|
message: unknown
|
|
513
527
|
}
|
|
514
528
|
|
|
529
|
+
/** Emitted when an ephemeral message is sent for this document */
|
|
515
530
|
export interface DocHandleOutboundEphemeralMessagePayload<T> {
|
|
516
531
|
handle: DocHandle<T>
|
|
517
532
|
data: Uint8Array
|
|
518
533
|
}
|
|
519
534
|
|
|
535
|
+
/** Emitted when we have new remote heads for this document */
|
|
520
536
|
export interface DocHandleRemoteHeadsPayload {
|
|
521
537
|
storageId: StorageId
|
|
522
538
|
heads: A.Heads
|
|
523
539
|
}
|
|
524
540
|
|
|
525
|
-
|
|
526
|
-
peerId: PeerId
|
|
527
|
-
syncState: A.SyncState
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
export interface DocHandleEvents<T> {
|
|
531
|
-
"heads-changed": (payload: DocHandleEncodedChangePayload<T>) => void
|
|
532
|
-
change: (payload: DocHandleChangePayload<T>) => void
|
|
533
|
-
delete: (payload: DocHandleDeletePayload<T>) => void
|
|
534
|
-
unavailable: (payload: DocHandleDeletePayload<T>) => void
|
|
535
|
-
"ephemeral-message": (payload: DocHandleEphemeralMessagePayload<T>) => void
|
|
536
|
-
"ephemeral-message-outbound": (
|
|
537
|
-
payload: DocHandleOutboundEphemeralMessagePayload<T>
|
|
538
|
-
) => void
|
|
539
|
-
"remote-heads": (payload: DocHandleRemoteHeadsPayload) => void
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
// STATE MACHINE TYPES
|
|
541
|
+
// STATE MACHINE TYPES & CONSTANTS
|
|
543
542
|
|
|
544
543
|
// state
|
|
545
544
|
|
|
546
545
|
/**
|
|
547
|
-
*
|
|
548
|
-
* @enum
|
|
549
|
-
*
|
|
546
|
+
* Possible internal states for a DocHandle
|
|
550
547
|
*/
|
|
551
548
|
export const HandleState = {
|
|
552
549
|
/** The handle has been created but not yet loaded or requested */
|
|
@@ -566,12 +563,15 @@ export const HandleState = {
|
|
|
566
563
|
} as const
|
|
567
564
|
export type HandleState = (typeof HandleState)[keyof typeof HandleState]
|
|
568
565
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
566
|
+
export const {
|
|
567
|
+
IDLE,
|
|
568
|
+
LOADING,
|
|
569
|
+
AWAITING_NETWORK,
|
|
570
|
+
REQUESTING,
|
|
571
|
+
READY,
|
|
572
|
+
DELETED,
|
|
573
|
+
UNAVAILABLE,
|
|
574
|
+
} = HandleState
|
|
575
575
|
|
|
576
576
|
// context
|
|
577
577
|
|
|
@@ -582,81 +582,29 @@ interface DocHandleContext<T> {
|
|
|
582
582
|
|
|
583
583
|
// events
|
|
584
584
|
|
|
585
|
-
|
|
586
|
-
CREATE: "CREATE",
|
|
587
|
-
FIND: "FIND",
|
|
588
|
-
REQUEST: "REQUEST",
|
|
589
|
-
REQUEST_COMPLETE: "REQUEST_COMPLETE",
|
|
590
|
-
AWAIT_NETWORK: "AWAIT_NETWORK",
|
|
591
|
-
NETWORK_READY: "NETWORK_READY",
|
|
592
|
-
UPDATE: "UPDATE",
|
|
593
|
-
TIMEOUT: "TIMEOUT",
|
|
594
|
-
DELETE: "DELETE",
|
|
595
|
-
MARK_UNAVAILABLE: "MARK_UNAVAILABLE",
|
|
596
|
-
} as const
|
|
597
|
-
type Event = (typeof Event)[keyof typeof Event]
|
|
598
|
-
|
|
599
|
-
type CreateEvent = { type: typeof CREATE; payload: { documentId: string } }
|
|
600
|
-
type FindEvent = { type: typeof FIND; payload: { documentId: string } }
|
|
601
|
-
type RequestEvent = { type: typeof REQUEST }
|
|
602
|
-
type RequestCompleteEvent = { type: typeof REQUEST_COMPLETE }
|
|
603
|
-
type DeleteEvent = { type: typeof DELETE }
|
|
604
|
-
type UpdateEvent<T> = {
|
|
605
|
-
type: typeof UPDATE
|
|
606
|
-
payload: { callback: (doc: A.Doc<T>) => A.Doc<T> }
|
|
607
|
-
}
|
|
608
|
-
type TimeoutEvent = { type: typeof TIMEOUT }
|
|
609
|
-
type MarkUnavailableEvent = { type: typeof MARK_UNAVAILABLE }
|
|
610
|
-
type AwaitNetworkEvent = { type: typeof AWAIT_NETWORK }
|
|
611
|
-
type NetworkReadyEvent = { type: typeof NETWORK_READY }
|
|
612
|
-
|
|
585
|
+
/** These are the (internal) events that can be sent to the state machine */
|
|
613
586
|
type DocHandleEvent<T> =
|
|
614
|
-
|
|
|
615
|
-
|
|
|
616
|
-
|
|
|
617
|
-
|
|
|
618
|
-
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
|
623
|
-
|
|
|
624
|
-
|
|
625
|
-
type
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
>
|
|
639
|
-
>
|
|
640
|
-
|
|
641
|
-
// CONSTANTS
|
|
642
|
-
export const {
|
|
643
|
-
IDLE,
|
|
644
|
-
LOADING,
|
|
645
|
-
AWAITING_NETWORK,
|
|
646
|
-
REQUESTING,
|
|
647
|
-
READY,
|
|
648
|
-
DELETED,
|
|
649
|
-
UNAVAILABLE,
|
|
650
|
-
} = HandleState
|
|
651
|
-
const {
|
|
652
|
-
CREATE,
|
|
653
|
-
FIND,
|
|
654
|
-
REQUEST,
|
|
655
|
-
UPDATE,
|
|
656
|
-
TIMEOUT,
|
|
657
|
-
DELETE,
|
|
658
|
-
REQUEST_COMPLETE,
|
|
659
|
-
MARK_UNAVAILABLE,
|
|
660
|
-
AWAIT_NETWORK,
|
|
661
|
-
NETWORK_READY,
|
|
662
|
-
} = Event
|
|
587
|
+
| { type: typeof CREATE }
|
|
588
|
+
| { type: typeof FIND }
|
|
589
|
+
| { type: typeof REQUEST }
|
|
590
|
+
| { type: typeof DOC_READY }
|
|
591
|
+
| {
|
|
592
|
+
type: typeof UPDATE
|
|
593
|
+
payload: { callback: (doc: A.Doc<T>) => A.Doc<T> }
|
|
594
|
+
}
|
|
595
|
+
| { type: typeof TIMEOUT }
|
|
596
|
+
| { type: typeof DELETE }
|
|
597
|
+
| { type: typeof DOC_UNAVAILABLE }
|
|
598
|
+
| { type: typeof AWAIT_NETWORK }
|
|
599
|
+
| { type: typeof NETWORK_READY }
|
|
600
|
+
|
|
601
|
+
const CREATE = "CREATE"
|
|
602
|
+
const FIND = "FIND"
|
|
603
|
+
const REQUEST = "REQUEST"
|
|
604
|
+
const DOC_READY = "DOC_READY"
|
|
605
|
+
const AWAIT_NETWORK = "AWAIT_NETWORK"
|
|
606
|
+
const NETWORK_READY = "NETWORK_READY"
|
|
607
|
+
const UPDATE = "UPDATE"
|
|
608
|
+
const DELETE = "DELETE"
|
|
609
|
+
const TIMEOUT = "TIMEOUT"
|
|
610
|
+
const DOC_UNAVAILABLE = "DOC_UNAVAILABLE"
|