@automerge/automerge-repo 1.1.4 → 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 +239 -231
- package/dist/Repo.d.ts +10 -3
- package/dist/Repo.d.ts.map +1 -1
- package/dist/Repo.js +22 -1
- package/dist/helpers/arraysAreEqual.d.ts.map +1 -1
- package/dist/helpers/debounce.d.ts.map +1 -1
- package/dist/helpers/tests/network-adapter-tests.d.ts +1 -1
- package/dist/helpers/tests/network-adapter-tests.d.ts.map +1 -1
- package/dist/helpers/tests/network-adapter-tests.js +2 -2
- package/dist/helpers/tests/storage-adapter-tests.d.ts +7 -0
- package/dist/helpers/tests/storage-adapter-tests.d.ts.map +1 -0
- package/dist/helpers/tests/storage-adapter-tests.js +128 -0
- 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/synchronizer/DocSynchronizer.js +1 -1
- package/package.json +4 -4
- package/src/DocHandle.ts +325 -375
- package/src/Repo.ts +35 -8
- package/src/helpers/tests/network-adapter-tests.ts +4 -2
- package/src/helpers/tests/storage-adapter-tests.ts +193 -0
- package/src/index.ts +43 -0
- package/src/synchronizer/DocSynchronizer.ts +1 -1
- package/test/CollectionSynchronizer.test.ts +1 -3
- package/test/DocHandle.test.ts +19 -1
- package/test/DocSynchronizer.test.ts +1 -4
- package/test/DummyStorageAdapter.test.ts +11 -0
- package/test/Repo.test.ts +179 -53
- package/test/helpers/DummyNetworkAdapter.ts +20 -18
- package/test/helpers/DummyStorageAdapter.ts +5 -1
- 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
|
|
@@ -244,64 +163,112 @@ export class DocHandle<T> //
|
|
|
244
163
|
|
|
245
164
|
/** Returns a promise that resolves when the docHandle is in one of the given states */
|
|
246
165
|
#statePromise(awaitStates: HandleState | HandleState[]) {
|
|
247
|
-
const awaitStatesArray = Array.isArray(awaitStates)
|
|
166
|
+
const awaitStatesArray = Array.isArray(awaitStates)
|
|
167
|
+
? awaitStates
|
|
168
|
+
: [awaitStates]
|
|
248
169
|
return waitFor(
|
|
249
170
|
this.#machine,
|
|
250
|
-
s => awaitStatesArray.some(
|
|
171
|
+
s => awaitStatesArray.some(state => s.matches(state)),
|
|
251
172
|
// use a longer delay here so as not to race with other delays
|
|
252
|
-
{timeout: this.#timeoutDelay * 2}
|
|
173
|
+
{ timeout: this.#timeoutDelay * 2 }
|
|
253
174
|
)
|
|
254
175
|
}
|
|
255
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
|
+
|
|
256
204
|
// PUBLIC
|
|
257
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
|
+
|
|
258
220
|
/**
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
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.
|
|
262
225
|
*/
|
|
263
|
-
|
|
226
|
+
isDeleted = () => this.inState(["deleted"])
|
|
227
|
+
|
|
264
228
|
/**
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
|
|
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.
|
|
269
237
|
*/
|
|
270
|
-
isDeleted = () => this.inState([HandleState.DELETED])
|
|
271
|
-
isUnavailable = () => this.inState([HandleState.UNAVAILABLE])
|
|
272
238
|
inState = (states: HandleState[]) =>
|
|
273
|
-
states.some(this.#machine
|
|
239
|
+
states.some(s => this.#machine.getSnapshot().matches(s))
|
|
274
240
|
|
|
275
241
|
/** @hidden */
|
|
276
242
|
get state() {
|
|
277
|
-
return this.#machine
|
|
243
|
+
return this.#machine.getSnapshot().value
|
|
278
244
|
}
|
|
279
245
|
|
|
280
246
|
/**
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
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()`.
|
|
285
252
|
*/
|
|
286
|
-
async whenReady(awaitStates: HandleState[] = [
|
|
253
|
+
async whenReady(awaitStates: HandleState[] = ["ready"]) {
|
|
287
254
|
await withTimeout(this.#statePromise(awaitStates), this.#timeoutDelay)
|
|
288
255
|
}
|
|
289
256
|
|
|
290
257
|
/**
|
|
291
|
-
*
|
|
292
|
-
* Note that this waits for the handle to be ready if necessary, and currently, if
|
|
293
|
-
* loading (or synchronization) fails, will never resolve.
|
|
258
|
+
* @returns the current state of this handle's Automerge document.
|
|
294
259
|
*
|
|
295
|
-
*
|
|
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.
|
|
296
262
|
*/
|
|
297
263
|
async doc(
|
|
298
|
-
|
|
299
|
-
|
|
264
|
+
/** states to wait for, such as "LOADING". mostly for internal use. */
|
|
265
|
+
awaitStates: HandleState[] = ["ready", "unavailable"]
|
|
266
|
+
) {
|
|
300
267
|
try {
|
|
301
268
|
// wait for the document to enter one of the desired states
|
|
302
269
|
await this.#statePromise(awaitStates)
|
|
303
270
|
} catch (error) {
|
|
304
|
-
// if we timed out
|
|
271
|
+
// if we timed out, return undefined
|
|
305
272
|
return undefined
|
|
306
273
|
}
|
|
307
274
|
// Return the document
|
|
@@ -309,33 +276,46 @@ export class DocHandle<T> //
|
|
|
309
276
|
}
|
|
310
277
|
|
|
311
278
|
/**
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
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.
|
|
315
282
|
*
|
|
316
|
-
*
|
|
283
|
+
* Not to be confused with the SyncState of the document, which describes the state of the
|
|
284
|
+
* synchronization process.
|
|
317
285
|
*
|
|
318
|
-
* Note that `undefined` is not a valid Automerge document so the return from this function is
|
|
319
|
-
*
|
|
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
|
|
320
300
|
*/
|
|
321
|
-
|
|
301
|
+
heads(): A.Heads | undefined {
|
|
322
302
|
if (!this.isReady()) {
|
|
323
303
|
return undefined
|
|
324
304
|
}
|
|
325
|
-
|
|
326
|
-
return this.#doc
|
|
305
|
+
return A.getHeads(this.#doc)
|
|
327
306
|
}
|
|
328
307
|
|
|
329
|
-
/**
|
|
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.
|
|
330
311
|
* @hidden
|
|
331
|
-
|
|
312
|
+
*/
|
|
332
313
|
update(callback: (doc: A.Doc<T>) => A.Doc<T>) {
|
|
333
|
-
this.#machine.send(UPDATE, {
|
|
334
|
-
payload: { callback },
|
|
335
|
-
})
|
|
314
|
+
this.#machine.send({ type: UPDATE, payload: { callback } })
|
|
336
315
|
}
|
|
337
316
|
|
|
338
|
-
/**
|
|
317
|
+
/**
|
|
318
|
+
* Called by the repo either when a doc handle changes or we receive new remote heads.
|
|
339
319
|
* @hidden
|
|
340
320
|
*/
|
|
341
321
|
setRemoteHeads(storageId: StorageId, heads: A.Heads) {
|
|
@@ -343,28 +323,39 @@ export class DocHandle<T> //
|
|
|
343
323
|
this.emit("remote-heads", { storageId, heads })
|
|
344
324
|
}
|
|
345
325
|
|
|
346
|
-
/** Returns the heads of the storageId */
|
|
326
|
+
/** Returns the heads of the storageId. */
|
|
347
327
|
getRemoteHeads(storageId: StorageId): A.Heads | undefined {
|
|
348
328
|
return this.#remoteHeads[storageId]
|
|
349
329
|
}
|
|
350
330
|
|
|
351
|
-
/**
|
|
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
|
+
*/
|
|
352
346
|
change(callback: A.ChangeFn<T>, options: A.ChangeOptions<T> = {}) {
|
|
353
347
|
if (!this.isReady()) {
|
|
354
348
|
throw new Error(
|
|
355
349
|
`DocHandle#${this.documentId} is not ready. Check \`handle.isReady()\` before accessing the document.`
|
|
356
350
|
)
|
|
357
351
|
}
|
|
358
|
-
this.#machine.send(
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
return A.change(doc, options, callback)
|
|
362
|
-
},
|
|
363
|
-
},
|
|
352
|
+
this.#machine.send({
|
|
353
|
+
type: UPDATE,
|
|
354
|
+
payload: { callback: doc => A.change(doc, options, callback) },
|
|
364
355
|
})
|
|
365
356
|
}
|
|
366
|
-
|
|
367
|
-
|
|
357
|
+
/**
|
|
358
|
+
* Makes a change as if the document were at `heads`.
|
|
368
359
|
*
|
|
369
360
|
* @returns A set of heads representing the concurrent change that was made.
|
|
370
361
|
*/
|
|
@@ -379,37 +370,39 @@ export class DocHandle<T> //
|
|
|
379
370
|
)
|
|
380
371
|
}
|
|
381
372
|
let resultHeads: string[] | undefined = undefined
|
|
382
|
-
this.#machine.send(
|
|
373
|
+
this.#machine.send({
|
|
374
|
+
type: UPDATE,
|
|
383
375
|
payload: {
|
|
384
|
-
callback:
|
|
376
|
+
callback: doc => {
|
|
385
377
|
const result = A.changeAt(doc, heads, options, callback)
|
|
386
378
|
resultHeads = result.newHeads || undefined
|
|
387
379
|
return result.newDoc
|
|
388
380
|
},
|
|
389
381
|
},
|
|
390
382
|
})
|
|
383
|
+
|
|
384
|
+
// the callback above will always run before we get here, so this should always contain the new heads
|
|
391
385
|
return resultHeads
|
|
392
386
|
}
|
|
393
387
|
|
|
394
|
-
/**
|
|
395
|
-
*
|
|
396
|
-
*
|
|
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.
|
|
397
391
|
*
|
|
398
|
-
* @
|
|
399
|
-
* This is a convenience method for
|
|
400
|
-
* `handle.change(doc => A.merge(doc, otherHandle.docSync()))`. Any peers
|
|
401
|
-
* whom we are sharing changes with will be notified of the changes resulting
|
|
402
|
-
* from the merge.
|
|
392
|
+
* @returns the merged document.
|
|
403
393
|
*
|
|
404
|
-
* @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.
|
|
405
395
|
*/
|
|
406
|
-
merge(
|
|
396
|
+
merge(
|
|
397
|
+
/** the handle of the document to merge into this one */
|
|
398
|
+
otherHandle: DocHandle<T>
|
|
399
|
+
) {
|
|
407
400
|
if (!this.isReady() || !otherHandle.isReady()) {
|
|
408
401
|
throw new Error("Both handles must be ready to merge")
|
|
409
402
|
}
|
|
410
403
|
const mergingDoc = otherHandle.docSync()
|
|
411
404
|
if (!mergingDoc) {
|
|
412
|
-
throw new Error("The document to be merged in is
|
|
405
|
+
throw new Error("The document to be merged in is falsy, aborting.")
|
|
413
406
|
}
|
|
414
407
|
|
|
415
408
|
this.update(doc => {
|
|
@@ -417,37 +410,43 @@ export class DocHandle<T> //
|
|
|
417
410
|
})
|
|
418
411
|
}
|
|
419
412
|
|
|
413
|
+
/**
|
|
414
|
+
* Used in testing to mark this document as unavailable.
|
|
415
|
+
* @hidden
|
|
416
|
+
*/
|
|
420
417
|
unavailable() {
|
|
421
|
-
this.#machine.send(
|
|
418
|
+
this.#machine.send({ type: DOC_UNAVAILABLE })
|
|
422
419
|
}
|
|
423
420
|
|
|
424
|
-
/**
|
|
421
|
+
/** Called by the repo when the document is not found in storage.
|
|
425
422
|
* @hidden
|
|
426
423
|
* */
|
|
427
424
|
request() {
|
|
428
|
-
if (this.#state ===
|
|
425
|
+
if (this.#state === "loading") this.#machine.send({ type: REQUEST })
|
|
429
426
|
}
|
|
430
427
|
|
|
431
428
|
/** @hidden */
|
|
432
429
|
awaitNetwork() {
|
|
433
|
-
if (this.#state ===
|
|
430
|
+
if (this.#state === "loading") this.#machine.send({ type: AWAIT_NETWORK })
|
|
434
431
|
}
|
|
435
432
|
|
|
436
433
|
/** @hidden */
|
|
437
434
|
networkReady() {
|
|
438
|
-
if (this.#state ===
|
|
435
|
+
if (this.#state === "awaitingNetwork")
|
|
436
|
+
this.#machine.send({ type: NETWORK_READY })
|
|
439
437
|
}
|
|
440
438
|
|
|
441
|
-
/**
|
|
439
|
+
/** Called by the repo when the document is deleted. */
|
|
442
440
|
delete() {
|
|
443
|
-
this.#machine.send(DELETE)
|
|
441
|
+
this.#machine.send({ type: DELETE })
|
|
444
442
|
}
|
|
445
443
|
|
|
446
|
-
/**
|
|
447
|
-
*
|
|
448
|
-
*
|
|
449
|
-
*
|
|
450
|
-
*
|
|
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.
|
|
451
450
|
*/
|
|
452
451
|
broadcast(message: unknown) {
|
|
453
452
|
this.emit("ephemeral-message-outbound", {
|
|
@@ -457,7 +456,7 @@ export class DocHandle<T> //
|
|
|
457
456
|
}
|
|
458
457
|
}
|
|
459
458
|
|
|
460
|
-
//
|
|
459
|
+
// TYPES
|
|
461
460
|
|
|
462
461
|
/** @hidden */
|
|
463
462
|
export type DocHandleOptions<T> =
|
|
@@ -477,24 +476,30 @@ export type DocHandleOptions<T> =
|
|
|
477
476
|
timeoutDelay?: number
|
|
478
477
|
}
|
|
479
478
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
|
484
492
|
}
|
|
485
493
|
|
|
494
|
+
/** Emitted when this document's heads have changed */
|
|
486
495
|
export interface DocHandleEncodedChangePayload<T> {
|
|
487
496
|
handle: DocHandle<T>
|
|
488
497
|
doc: A.Doc<T>
|
|
489
498
|
}
|
|
490
499
|
|
|
491
|
-
|
|
492
|
-
handle: DocHandle<T>
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
/** Emitted when a document has changed */
|
|
500
|
+
/** Emitted when this document has changed */
|
|
496
501
|
export interface DocHandleChangePayload<T> {
|
|
497
|
-
/** The
|
|
502
|
+
/** The handle that changed */
|
|
498
503
|
handle: DocHandle<T>
|
|
499
504
|
/** The value of the document after the change */
|
|
500
505
|
doc: A.Doc<T>
|
|
@@ -504,47 +509,41 @@ export interface DocHandleChangePayload<T> {
|
|
|
504
509
|
patchInfo: A.PatchInfo<T>
|
|
505
510
|
}
|
|
506
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 */
|
|
507
523
|
export interface DocHandleEphemeralMessagePayload<T> {
|
|
508
524
|
handle: DocHandle<T>
|
|
509
525
|
senderId: PeerId
|
|
510
526
|
message: unknown
|
|
511
527
|
}
|
|
512
528
|
|
|
529
|
+
/** Emitted when an ephemeral message is sent for this document */
|
|
513
530
|
export interface DocHandleOutboundEphemeralMessagePayload<T> {
|
|
514
531
|
handle: DocHandle<T>
|
|
515
532
|
data: Uint8Array
|
|
516
533
|
}
|
|
517
534
|
|
|
535
|
+
/** Emitted when we have new remote heads for this document */
|
|
518
536
|
export interface DocHandleRemoteHeadsPayload {
|
|
519
537
|
storageId: StorageId
|
|
520
538
|
heads: A.Heads
|
|
521
539
|
}
|
|
522
540
|
|
|
523
|
-
|
|
524
|
-
peerId: PeerId
|
|
525
|
-
syncState: A.SyncState
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
export interface DocHandleEvents<T> {
|
|
529
|
-
"heads-changed": (payload: DocHandleEncodedChangePayload<T>) => void
|
|
530
|
-
change: (payload: DocHandleChangePayload<T>) => void
|
|
531
|
-
delete: (payload: DocHandleDeletePayload<T>) => void
|
|
532
|
-
unavailable: (payload: DocHandleDeletePayload<T>) => void
|
|
533
|
-
"ephemeral-message": (payload: DocHandleEphemeralMessagePayload<T>) => void
|
|
534
|
-
"ephemeral-message-outbound": (
|
|
535
|
-
payload: DocHandleOutboundEphemeralMessagePayload<T>
|
|
536
|
-
) => void
|
|
537
|
-
"remote-heads": (payload: DocHandleRemoteHeadsPayload) => void
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
// STATE MACHINE TYPES
|
|
541
|
+
// STATE MACHINE TYPES & CONSTANTS
|
|
541
542
|
|
|
542
543
|
// state
|
|
543
544
|
|
|
544
545
|
/**
|
|
545
|
-
*
|
|
546
|
-
* @enum
|
|
547
|
-
*
|
|
546
|
+
* Possible internal states for a DocHandle
|
|
548
547
|
*/
|
|
549
548
|
export const HandleState = {
|
|
550
549
|
/** The handle has been created but not yet loaded or requested */
|
|
@@ -564,12 +563,15 @@ export const HandleState = {
|
|
|
564
563
|
} as const
|
|
565
564
|
export type HandleState = (typeof HandleState)[keyof typeof HandleState]
|
|
566
565
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
566
|
+
export const {
|
|
567
|
+
IDLE,
|
|
568
|
+
LOADING,
|
|
569
|
+
AWAITING_NETWORK,
|
|
570
|
+
REQUESTING,
|
|
571
|
+
READY,
|
|
572
|
+
DELETED,
|
|
573
|
+
UNAVAILABLE,
|
|
574
|
+
} = HandleState
|
|
573
575
|
|
|
574
576
|
// context
|
|
575
577
|
|
|
@@ -580,81 +582,29 @@ interface DocHandleContext<T> {
|
|
|
580
582
|
|
|
581
583
|
// events
|
|
582
584
|
|
|
583
|
-
|
|
584
|
-
CREATE: "CREATE",
|
|
585
|
-
FIND: "FIND",
|
|
586
|
-
REQUEST: "REQUEST",
|
|
587
|
-
REQUEST_COMPLETE: "REQUEST_COMPLETE",
|
|
588
|
-
AWAIT_NETWORK: "AWAIT_NETWORK",
|
|
589
|
-
NETWORK_READY: "NETWORK_READY",
|
|
590
|
-
UPDATE: "UPDATE",
|
|
591
|
-
TIMEOUT: "TIMEOUT",
|
|
592
|
-
DELETE: "DELETE",
|
|
593
|
-
MARK_UNAVAILABLE: "MARK_UNAVAILABLE",
|
|
594
|
-
} as const
|
|
595
|
-
type Event = (typeof Event)[keyof typeof Event]
|
|
596
|
-
|
|
597
|
-
type CreateEvent = { type: typeof CREATE; payload: { documentId: string } }
|
|
598
|
-
type FindEvent = { type: typeof FIND; payload: { documentId: string } }
|
|
599
|
-
type RequestEvent = { type: typeof REQUEST }
|
|
600
|
-
type RequestCompleteEvent = { type: typeof REQUEST_COMPLETE }
|
|
601
|
-
type DeleteEvent = { type: typeof DELETE }
|
|
602
|
-
type UpdateEvent<T> = {
|
|
603
|
-
type: typeof UPDATE
|
|
604
|
-
payload: { callback: (doc: A.Doc<T>) => A.Doc<T> }
|
|
605
|
-
}
|
|
606
|
-
type TimeoutEvent = { type: typeof TIMEOUT }
|
|
607
|
-
type MarkUnavailableEvent = { type: typeof MARK_UNAVAILABLE }
|
|
608
|
-
type AwaitNetworkEvent = { type: typeof AWAIT_NETWORK }
|
|
609
|
-
type NetworkReadyEvent = { type: typeof NETWORK_READY }
|
|
610
|
-
|
|
585
|
+
/** These are the (internal) events that can be sent to the state machine */
|
|
611
586
|
type DocHandleEvent<T> =
|
|
612
|
-
|
|
|
613
|
-
|
|
|
614
|
-
|
|
|
615
|
-
|
|
|
616
|
-
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
|
621
|
-
|
|
|
622
|
-
|
|
623
|
-
type
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
>
|
|
637
|
-
>
|
|
638
|
-
|
|
639
|
-
// CONSTANTS
|
|
640
|
-
export const {
|
|
641
|
-
IDLE,
|
|
642
|
-
LOADING,
|
|
643
|
-
AWAITING_NETWORK,
|
|
644
|
-
REQUESTING,
|
|
645
|
-
READY,
|
|
646
|
-
DELETED,
|
|
647
|
-
UNAVAILABLE,
|
|
648
|
-
} = HandleState
|
|
649
|
-
const {
|
|
650
|
-
CREATE,
|
|
651
|
-
FIND,
|
|
652
|
-
REQUEST,
|
|
653
|
-
UPDATE,
|
|
654
|
-
TIMEOUT,
|
|
655
|
-
DELETE,
|
|
656
|
-
REQUEST_COMPLETE,
|
|
657
|
-
MARK_UNAVAILABLE,
|
|
658
|
-
AWAIT_NETWORK,
|
|
659
|
-
NETWORK_READY,
|
|
660
|
-
} = 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"
|