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