@comapeo/core 2.0.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/dist/blob-store/downloader.d.ts +43 -0
  2. package/dist/blob-store/downloader.d.ts.map +1 -0
  3. package/dist/blob-store/entries-stream.d.ts +13 -0
  4. package/dist/blob-store/entries-stream.d.ts.map +1 -0
  5. package/dist/blob-store/hyperdrive-index.d.ts +20 -0
  6. package/dist/blob-store/hyperdrive-index.d.ts.map +1 -0
  7. package/dist/blob-store/index.d.ts +34 -29
  8. package/dist/blob-store/index.d.ts.map +1 -1
  9. package/dist/blob-store/utils.d.ts +27 -0
  10. package/dist/blob-store/utils.d.ts.map +1 -0
  11. package/dist/constants.d.ts +2 -1
  12. package/dist/constants.d.ts.map +1 -1
  13. package/dist/core-manager/index.d.ts +11 -1
  14. package/dist/core-manager/index.d.ts.map +1 -1
  15. package/dist/core-ownership.d.ts.map +1 -1
  16. package/dist/datastore/index.d.ts +5 -4
  17. package/dist/datastore/index.d.ts.map +1 -1
  18. package/dist/datatype/index.d.ts +5 -1
  19. package/dist/discovery/local-discovery.d.ts.map +1 -1
  20. package/dist/errors.d.ts +6 -1
  21. package/dist/errors.d.ts.map +1 -1
  22. package/dist/fastify-plugins/blobs.d.ts.map +1 -1
  23. package/dist/fastify-plugins/maps.d.ts.map +1 -1
  24. package/dist/generated/extensions.d.ts +31 -0
  25. package/dist/generated/extensions.d.ts.map +1 -1
  26. package/dist/index.d.ts +2 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/lib/drizzle-helpers.d.ts +6 -0
  29. package/dist/lib/drizzle-helpers.d.ts.map +1 -0
  30. package/dist/lib/error.d.ts +51 -0
  31. package/dist/lib/error.d.ts.map +1 -0
  32. package/dist/lib/get-own.d.ts +9 -0
  33. package/dist/lib/get-own.d.ts.map +1 -0
  34. package/dist/lib/is-hostname-ip-address.d.ts +17 -0
  35. package/dist/lib/is-hostname-ip-address.d.ts.map +1 -0
  36. package/dist/lib/ws-core-replicator.d.ts +11 -0
  37. package/dist/lib/ws-core-replicator.d.ts.map +1 -0
  38. package/dist/mapeo-manager.d.ts +18 -22
  39. package/dist/mapeo-manager.d.ts.map +1 -1
  40. package/dist/mapeo-project.d.ts +459 -26
  41. package/dist/mapeo-project.d.ts.map +1 -1
  42. package/dist/member-api.d.ts +44 -1
  43. package/dist/member-api.d.ts.map +1 -1
  44. package/dist/roles.d.ts.map +1 -1
  45. package/dist/schema/client.d.ts +17 -5
  46. package/dist/schema/client.d.ts.map +1 -1
  47. package/dist/schema/project.d.ts +212 -2
  48. package/dist/schema/project.d.ts.map +1 -1
  49. package/dist/sync/core-sync-state.d.ts +20 -15
  50. package/dist/sync/core-sync-state.d.ts.map +1 -1
  51. package/dist/sync/namespace-sync-state.d.ts +13 -1
  52. package/dist/sync/namespace-sync-state.d.ts.map +1 -1
  53. package/dist/sync/peer-sync-controller.d.ts +1 -1
  54. package/dist/sync/peer-sync-controller.d.ts.map +1 -1
  55. package/dist/sync/sync-api.d.ts +47 -2
  56. package/dist/sync/sync-api.d.ts.map +1 -1
  57. package/dist/sync/sync-state.d.ts +12 -0
  58. package/dist/sync/sync-state.d.ts.map +1 -1
  59. package/dist/translation-api.d.ts +2 -2
  60. package/dist/translation-api.d.ts.map +1 -1
  61. package/dist/types.d.ts +10 -2
  62. package/dist/types.d.ts.map +1 -1
  63. package/drizzle/client/0001_chubby_cargill.sql +12 -0
  64. package/drizzle/client/meta/0001_snapshot.json +208 -0
  65. package/drizzle/client/meta/_journal.json +7 -0
  66. package/drizzle/project/0001_medical_wendell_rand.sql +22 -0
  67. package/drizzle/project/meta/0001_snapshot.json +1267 -0
  68. package/drizzle/project/meta/_journal.json +7 -0
  69. package/package.json +14 -5
  70. package/src/blob-store/downloader.js +130 -0
  71. package/src/blob-store/entries-stream.js +81 -0
  72. package/src/blob-store/hyperdrive-index.js +122 -0
  73. package/src/blob-store/index.js +59 -117
  74. package/src/blob-store/utils.js +54 -0
  75. package/src/constants.js +4 -1
  76. package/src/core-manager/index.js +60 -3
  77. package/src/core-ownership.js +2 -4
  78. package/src/datastore/README.md +1 -2
  79. package/src/datastore/index.js +8 -8
  80. package/src/datatype/index.d.ts +5 -1
  81. package/src/datatype/index.js +22 -9
  82. package/src/discovery/local-discovery.js +2 -1
  83. package/src/errors.js +11 -2
  84. package/src/fastify-plugins/blobs.js +17 -1
  85. package/src/fastify-plugins/maps.js +2 -1
  86. package/src/generated/extensions.d.ts +31 -0
  87. package/src/generated/extensions.js +150 -0
  88. package/src/generated/extensions.ts +181 -0
  89. package/src/index.js +10 -0
  90. package/src/invite-api.js +1 -1
  91. package/src/lib/drizzle-helpers.js +79 -0
  92. package/src/lib/error.js +71 -0
  93. package/src/lib/get-own.js +10 -0
  94. package/src/lib/is-hostname-ip-address.js +26 -0
  95. package/src/lib/ws-core-replicator.js +47 -0
  96. package/src/mapeo-manager.js +74 -45
  97. package/src/mapeo-project.js +238 -58
  98. package/src/member-api.js +295 -2
  99. package/src/roles.js +38 -32
  100. package/src/schema/client.js +4 -3
  101. package/src/schema/project.js +7 -0
  102. package/src/sync/core-sync-state.js +39 -23
  103. package/src/sync/namespace-sync-state.js +22 -0
  104. package/src/sync/peer-sync-controller.js +1 -0
  105. package/src/sync/sync-api.js +197 -3
  106. package/src/sync/sync-state.js +18 -0
  107. package/src/translation-api.js +5 -9
  108. package/src/types.ts +12 -3
  109. package/dist/blob-store/live-download.d.ts +0 -107
  110. package/dist/blob-store/live-download.d.ts.map +0 -1
  111. package/dist/lib/timing-safe-equal.d.ts +0 -15
  112. package/dist/lib/timing-safe-equal.d.ts.map +0 -1
  113. package/src/blob-store/live-download.js +0 -373
  114. package/src/lib/timing-safe-equal.js +0 -34
@@ -1,373 +0,0 @@
1
- import { TypedEmitter } from 'tiny-typed-emitter'
2
- import { once } from 'node:events'
3
- import SubEncoder from 'sub-encoder'
4
-
5
- const keyEncoding = new SubEncoder('files', 'utf-8')
6
-
7
- /**
8
- * @typedef {object} BlobDownloadState
9
- * @property {number} haveCount The number of files already downloaded
10
- * @property {number} haveBytes The bytes already downloaded
11
- * @property {number} wantCount The number of files pending download
12
- * @property {number} wantBytes The bytes pending download
13
- * @property {null} error If status = 'error' then this will be an Error object
14
- * @property {'checking' | 'downloading' | 'downloaded' | 'aborted'} status
15
- */
16
-
17
- /** @typedef {Omit<BlobDownloadState, 'error' | 'status'> & { status: 'error', error: Error }} BlobDownloadStateError */
18
-
19
- /**
20
- * @typedef {object} BlobDownloadEvents
21
- * @property {(state: BlobDownloadState | BlobDownloadStateError ) => void} state Emitted with the current download state whenever it changes (not emitted during initial 'checking' status)
22
- */
23
-
24
- /**
25
- * LiveDownload class
26
- * @extends {TypedEmitter<BlobDownloadEvents>}
27
- */
28
- export class LiveDownload extends TypedEmitter {
29
- /** @type {Set<DriveLiveDownload>} */
30
- #driveLiveDownloads = new Set()
31
- #signal
32
-
33
- /**
34
- * Like drive.download() but 'live', and for multiple drives
35
- * @param {Iterable<import('hyperdrive')>} drives
36
- * @param {import('./index.js').InternalDriveEmitter} emitter
37
- * @param {object} options
38
- * @param {import('../types.js').BlobFilter} [options.filter] Filter blobs of specific types and/or sizes to download
39
- * @param {AbortSignal} [options.signal]
40
- */
41
- constructor(drives, emitter, { filter, signal }) {
42
- super()
43
- this.#signal = signal
44
-
45
- const emitState = () => {
46
- this.emit('state', this.state)
47
- }
48
-
49
- /** @param {import('hyperdrive')} drive */
50
- const addDrive = (drive) => {
51
- const download = new DriveLiveDownload(drive, {
52
- filter,
53
- signal,
54
- })
55
- this.#driveLiveDownloads.add(download)
56
- download.on('state', emitState)
57
- }
58
-
59
- for (const drive of drives) addDrive(drive)
60
- emitter.on('add-drive', addDrive)
61
-
62
- signal?.addEventListener(
63
- 'abort',
64
- () => {
65
- emitter.off('add-drive', addDrive)
66
- for (const download of this.#driveLiveDownloads) {
67
- download.off('state', emitState)
68
- }
69
- },
70
- { once: true }
71
- )
72
- }
73
-
74
- /**
75
- * @returns {BlobDownloadState | BlobDownloadStateError}
76
- */
77
- get state() {
78
- return combineStates(this.#driveLiveDownloads, { signal: this.#signal })
79
- }
80
- }
81
-
82
- /**
83
- * LiveDownload class
84
- * @extends {TypedEmitter<BlobDownloadEvents>}
85
- */
86
- export class DriveLiveDownload extends TypedEmitter {
87
- #haveCount = 0
88
- #haveBytes = 0
89
- #wantBytes = 0
90
- #initialCheck = true
91
- #drive
92
- #folders
93
- /** @type {Set<{ done(): Promise<void>, destroy(): void }>} */
94
- #downloads = new Set()
95
- /** @type {Error | null} */
96
- #error = null
97
- #signal
98
-
99
- /**
100
- * Like drive.download() but 'live',
101
- * @param {import('hyperdrive')} drive
102
- * @param {object} options
103
- * @param {import('../types.js').BlobFilter} [options.filter] Filter blobs of specific types and/or sizes to download
104
- * @param {AbortSignal} [options.signal]
105
- */
106
- constructor(drive, { filter, signal } = {}) {
107
- super()
108
- this.#drive = drive
109
- this.#folders = filterToFolders(filter)
110
- this.#signal = signal
111
- if (signal && !signal.aborted) {
112
- signal.addEventListener(
113
- 'abort',
114
- () => {
115
- for (const download of this.#downloads) download.destroy()
116
- this.#downloads.clear()
117
- this.emit('state', this.state)
118
- },
119
- { once: true }
120
- )
121
- }
122
- this.#start().catch(this.#handleError.bind(this))
123
- }
124
-
125
- /**
126
- * @returns {BlobDownloadState | BlobDownloadStateError}
127
- */
128
- get state() {
129
- if (this.#error) {
130
- return {
131
- haveCount: this.#haveCount,
132
- haveBytes: this.#haveBytes,
133
- wantCount: this.#downloads.size,
134
- wantBytes: this.#wantBytes,
135
- error: this.#error,
136
- status: 'error',
137
- }
138
- }
139
- return {
140
- haveCount: this.#haveCount,
141
- haveBytes: this.#haveBytes,
142
- wantCount: this.#downloads.size,
143
- wantBytes: this.#wantBytes,
144
- error: null,
145
- status: this.#signal?.aborted
146
- ? 'aborted'
147
- : this.#initialCheck
148
- ? 'checking'
149
- : this.#downloads.size > 0
150
- ? 'downloading'
151
- : 'downloaded',
152
- }
153
- }
154
-
155
- async #start() {
156
- const blobsCore = await this.#getBlobsCore()
157
- /* c8 ignore next */
158
- if (this.#signal?.aborted || !blobsCore) return // Can't get here in tests
159
- let seq = 0
160
-
161
- for (const folder of this.#folders) {
162
- // Don't emit state during initial iteration of existing data, since this is
163
- // likely fast and not useful UX feedback
164
- const entryStream = this.#drive.list(folder, { recursive: true })
165
- if (this.#signal) {
166
- this.#signal.addEventListener('abort', () => entryStream.destroy(), {
167
- once: true,
168
- })
169
- }
170
- for await (const entry of entryStream) {
171
- if (this.#signal?.aborted) return
172
- seq = Math.max(seq, entry.seq)
173
- const { blob } = entry.value
174
- if (!blob) continue
175
- await this.#processEntry(blobsCore, blob)
176
- }
177
- if (this.#signal?.aborted) return
178
- }
179
-
180
- this.#initialCheck = false
181
- this.emit('state', this.state)
182
-
183
- const bee = this.#drive.db
184
- // This will also download old versions of files, but it is the only way to
185
- // get a live stream from a Hyperbee, however we currently do not support
186
- // edits of blobs, so this should not be an issue. `keyEncoding` is
187
- // necessary because hyperdrive stores file index data under the `files`
188
- // sub-encoding key
189
- const historyStream = bee.createHistoryStream({
190
- live: true,
191
- gt: seq,
192
- keyEncoding,
193
- })
194
- if (this.#signal) {
195
- this.#signal.addEventListener('abort', () => historyStream.destroy(), {
196
- once: true,
197
- })
198
- }
199
- for await (const entry of historyStream) {
200
- if (this.#signal?.aborted) return
201
- const { blob } = entry.value
202
- if (!blob) continue
203
- if (!matchesFolder(entry.key, this.#folders)) continue
204
- // TODO: consider cancelling downloads when a delete entry is found?
205
- // Probably not worth the extra work.
206
- if (entry.type !== 'put') continue
207
- const wasDownloaded = this.state.status === 'downloaded'
208
- await this.#processEntry(blobsCore, blob)
209
- if (wasDownloaded && this.state.status === 'downloading') {
210
- // State has changed, so emit
211
- this.emit('state', this.state)
212
- }
213
- }
214
- /* c8 ignore next 2 */
215
- // Could possibly reach here if aborted after check in loop, hard to test
216
- this.emit('state', this.state)
217
- }
218
-
219
- /**
220
- * If a Hyperdrive has been added by its key and has never replicated, then
221
- * drive.getBlobs() will not resolve until replication starts. Since we do not
222
- * want the downloader to remain in the "checking" state forever, we catch
223
- * this case and update the state before waiting for the hyperdrive hyperblobs
224
- * instance. This also makes waiting for the blobs instance cancellable.
225
- *
226
- * @returns {Promise<import('hypercore') | undefined>}
227
- */
228
- async #getBlobsCore() {
229
- if (this.#drive.blobs) return this.#drive.blobs.core
230
- await this.#drive.ready()
231
- await this.#drive.core.update({ wait: true })
232
-
233
- // If no peers at this stage, we are not going to be able to get the blobs
234
- // until a peer appears, so consider this state "downloaded", because
235
- // otherwise this will just hang as "checking"
236
- if (!this.#drive.core.peers.length) {
237
- this.#initialCheck = false
238
- this.emit('state', this.state)
239
- }
240
- try {
241
- const [blobs] = await once(this.#drive, 'blobs', { signal: this.#signal })
242
- return blobs.core
243
- } catch (e) {
244
- if (e instanceof Error && e.name === 'AbortError') return
245
- throw e
246
- }
247
- }
248
-
249
- /** @param {Error} e */
250
- #handleError(e) {
251
- this.#error = e
252
- this.emit('state', this.state)
253
- }
254
-
255
- /**
256
- * Update state and queue missing entries for download
257
- *
258
- * @param {import('hypercore')} core
259
- * @param {{ blockOffset: number, blockLength: number, byteLength: number }} blob
260
- */
261
- async #processEntry(
262
- core,
263
- { blockOffset: start, blockLength: length, byteLength }
264
- ) {
265
- const end = start + length
266
- const have = await core.has(start, end)
267
- if (have) {
268
- this.#haveCount++
269
- this.#haveBytes += byteLength
270
- } else {
271
- this.#wantBytes += byteLength
272
- const download = core.download({ start, end })
273
- this.#downloads.add(download)
274
- download
275
- .done()
276
- .then(() => {
277
- this.#downloads.delete(download)
278
- this.#haveCount++
279
- this.#haveBytes += byteLength
280
- this.#wantBytes -= byteLength
281
- this.emit('state', this.state)
282
- })
283
- .catch(this.#handleError.bind(this))
284
- }
285
- }
286
- }
287
-
288
- /**
289
- * Reduce multiple states into one. Factored out for unit testing because I
290
- * don't trust my coding. Probably a smarter way to do this, but this works.
291
- *
292
- * @param {Iterable<{ state: BlobDownloadState | BlobDownloadStateError }>} liveDownloads
293
- * @param {{ signal?: AbortSignal }} options
294
- * @returns {BlobDownloadState | BlobDownloadStateError}
295
- */
296
- export function combineStates(liveDownloads, { signal } = {}) {
297
- /** @type {BlobDownloadState | BlobDownloadStateError} */
298
- let combinedState = {
299
- haveCount: 0,
300
- haveBytes: 0,
301
- wantCount: 0,
302
- wantBytes: 0,
303
- error: null,
304
- status: 'downloaded',
305
- }
306
- for (const { state } of liveDownloads) {
307
- combinedState.haveCount += state.haveCount
308
- combinedState.haveBytes += state.haveBytes
309
- combinedState.wantCount += state.wantCount
310
- combinedState.wantBytes += state.wantBytes
311
- if (state.status === combinedState.status) continue
312
- if (state.status === 'error') {
313
- combinedState = { ...combinedState, error: state.error, status: 'error' }
314
- } else if (
315
- state.status === 'downloading' &&
316
- combinedState.status === 'downloaded'
317
- ) {
318
- combinedState = { ...combinedState, status: 'downloading' }
319
- } else if (
320
- state.status === 'checking' &&
321
- (combinedState.status === 'downloaded' ||
322
- combinedState.status === 'downloading')
323
- ) {
324
- combinedState = { ...combinedState, status: 'checking' }
325
- }
326
- }
327
- if (signal?.aborted) {
328
- combinedState.status = 'aborted'
329
- }
330
- return combinedState
331
- }
332
-
333
- /**
334
- * Convert a filter to an array of folders that need to be downloaded
335
- *
336
- * @param {import('../types.js').BlobFilter} [filter]
337
- * @returns {string[]} array of folders that match the filter
338
- */
339
- function filterToFolders(filter) {
340
- if (!filter) return ['/']
341
- const folders = []
342
- for (const [
343
- type,
344
- variants,
345
- ] of /** @type {import('type-fest').Entries<typeof filter>} */ (
346
- Object.entries(filter)
347
- )) {
348
- // De-dupe variants array
349
- for (const variant of new Set(variants)) {
350
- folders.push(makePath({ type, variant }))
351
- }
352
- }
353
- return folders
354
- }
355
-
356
- /**
357
- * Returns true if the path is within one of the given folders
358
- *
359
- * @param {string} path
360
- * @param {string[]} folders
361
- * @returns {boolean}
362
- */
363
- function matchesFolder(path, folders) {
364
- for (const folder of folders) {
365
- if (path.startsWith(folder)) return true
366
- }
367
- return false
368
- }
369
-
370
- /** @param {Pick<import('../types.js').BlobId, 'type' | 'variant'>} opts */
371
- function makePath({ type, variant }) {
372
- return `/${type}/${variant}`
373
- }
@@ -1,34 +0,0 @@
1
- import * as crypto from 'node:crypto'
2
-
3
- /**
4
- * @param {string | NodeJS.ArrayBufferView} value
5
- * @returns {NodeJS.ArrayBufferView}
6
- */
7
- const bufferify = (value) =>
8
- // We use UTF-16 because it's the only supported encoding that doesn't
9
- // touch surrogate pairs. See [this post][0] for more details.
10
- //
11
- // [0]: https://evanhahn.com/crypto-timingsafeequal-with-strings/
12
- typeof value === 'string' ? Buffer.from(value, 'utf16le') : value
13
-
14
- /**
15
- * Compare two values in constant time.
16
- *
17
- * Useful when you want to avoid leaking data.
18
- *
19
- * Like `crypto.timingSafeEqual`, but works with strings and doesn't throw if
20
- * lengths differ.
21
- *
22
- * @template {string | NodeJS.ArrayBufferView} T
23
- * @param {T} a
24
- * @param {T} b
25
- * @returns {boolean}
26
- */
27
- export default function timingSafeEqual(a, b) {
28
- const bufferA = bufferify(a)
29
- const bufferB = bufferify(b)
30
- return (
31
- bufferA.byteLength === bufferB.byteLength &&
32
- crypto.timingSafeEqual(bufferA, bufferB)
33
- )
34
- }