@comapeo/core 2.3.0 → 2.3.2

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 (47) hide show
  1. package/dist/blob-store/entries-stream.d.ts.map +1 -1
  2. package/dist/blob-store/index.d.ts +27 -15
  3. package/dist/blob-store/index.d.ts.map +1 -1
  4. package/dist/core-manager/index.d.ts +4 -4
  5. package/dist/core-manager/index.d.ts.map +1 -1
  6. package/dist/datastore/index.d.ts +1 -1
  7. package/dist/datatype/index.d.ts +141 -111
  8. package/dist/datatype/index.d.ts.map +1 -0
  9. package/dist/fastify-plugins/utils.d.ts +0 -9
  10. package/dist/fastify-plugins/utils.d.ts.map +1 -1
  11. package/dist/generated/extensions.d.ts +7 -0
  12. package/dist/generated/extensions.d.ts.map +1 -1
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/mapeo-project.d.ts +17 -17
  16. package/dist/mapeo-project.d.ts.map +1 -1
  17. package/dist/schema/project.d.ts +1 -1
  18. package/dist/sync/core-sync-state.d.ts +14 -6
  19. package/dist/sync/core-sync-state.d.ts.map +1 -1
  20. package/dist/sync/namespace-sync-state.d.ts +3 -13
  21. package/dist/sync/namespace-sync-state.d.ts.map +1 -1
  22. package/dist/sync/sync-api.d.ts +17 -25
  23. package/dist/sync/sync-api.d.ts.map +1 -1
  24. package/dist/sync/sync-state.d.ts +3 -13
  25. package/dist/sync/sync-state.d.ts.map +1 -1
  26. package/dist/translation-api.d.ts +2 -2
  27. package/dist/translation-api.d.ts.map +1 -1
  28. package/dist/types.d.ts +1 -0
  29. package/dist/types.d.ts.map +1 -1
  30. package/package.json +7 -5
  31. package/src/blob-store/entries-stream.js +43 -18
  32. package/src/blob-store/index.js +161 -19
  33. package/src/core-manager/index.js +14 -9
  34. package/src/datatype/index.js +57 -19
  35. package/src/fastify-plugins/maps.js +1 -0
  36. package/src/fastify-plugins/utils.js +0 -13
  37. package/src/generated/extensions.d.ts +7 -0
  38. package/src/generated/extensions.js +12 -2
  39. package/src/generated/extensions.ts +19 -1
  40. package/src/mapeo-project.js +7 -76
  41. package/src/sync/core-sync-state.js +41 -14
  42. package/src/sync/namespace-sync-state.js +25 -22
  43. package/src/sync/sync-api.js +12 -43
  44. package/src/sync/sync-state.js +9 -19
  45. package/src/translation-api.js +2 -1
  46. package/src/types.ts +1 -1
  47. package/src/datatype/index.d.ts +0 -112
@@ -2,6 +2,7 @@ import SubEncoder from 'sub-encoder'
2
2
  import mergeStreams from '@sindresorhus/merge-streams'
3
3
  import { Transform, pipeline } from 'node:stream'
4
4
  import { noop } from '../utils.js'
5
+ import ensureError from 'ensure-error'
5
6
 
6
7
  /** @import Hyperdrive from 'hyperdrive' */
7
8
  /** @import { BlobStoreEntriesStream } from '../types.js' */
@@ -18,7 +19,7 @@ const keyEncoding = new SubEncoder('files', 'utf-8')
18
19
  */
19
20
  export function createEntriesStream(driveIndex, { live = false } = {}) {
20
21
  const mergedEntriesStreams = mergeStreams(
21
- [...driveIndex].map((drive) => getHistoryStream(drive.db, { live }))
22
+ [...driveIndex].map((drive) => getHistoryStream(drive, { live }))
22
23
  )
23
24
  driveIndex.on('add-drive', addDrive)
24
25
  // Close is always emitted, so we can use it to remove the listener
@@ -29,53 +30,77 @@ export function createEntriesStream(driveIndex, { live = false } = {}) {
29
30
 
30
31
  /** @param {Hyperdrive} drive */
31
32
  function addDrive(drive) {
32
- mergedEntriesStreams.add(getHistoryStream(drive.db, { live }))
33
+ mergedEntriesStreams.add(getHistoryStream(drive, { live }))
33
34
  }
34
35
  }
35
36
 
36
37
  /**
37
38
  *
38
- * @param {import('hyperbee')} bee
39
+ * @param {Hyperdrive} drive
39
40
  * @param {object} opts
40
41
  * @param {boolean} opts.live
41
42
  */
42
- function getHistoryStream(bee, { live }) {
43
+ function getHistoryStream(drive, { live }) {
43
44
  // This will also include old versions of files, but it is the only way to
44
45
  // get a live stream from a Hyperbee, however we currently do not support
45
46
  // edits of blobs, so this should not be an issue, and the consequence is
46
47
  // that old versions are downloaded too, which is acceptable.
47
- const historyStream = bee.createHistoryStream({
48
+ const historyStream = drive.db.createHistoryStream({
48
49
  live,
49
50
  // `keyEncoding` is necessary because hyperdrive stores file index data
50
51
  // under the `files` sub-encoding key
51
52
  keyEncoding,
52
53
  })
53
- return pipeline(historyStream, new AddDriveIds(bee.core), noop)
54
+ return pipeline(historyStream, new AddDriveIds(drive), noop)
54
55
  }
55
56
 
56
57
  class AddDriveIds extends Transform {
57
- #core
58
+ #drive
59
+ /** @type {string | undefined} */
58
60
  #cachedDriveId
61
+ /** @type {string | undefined} */
62
+ #cachedBlobCoreId
59
63
 
60
- /** @param {import('hypercore')} core */
61
- constructor(core) {
64
+ /** @param {Hyperdrive} drive */
65
+ constructor(drive) {
62
66
  super({ objectMode: true })
63
- this.#core = core
64
- this.#cachedDriveId = core.discoveryKey?.toString('hex')
67
+ this.#drive = drive
68
+ this.#cachedDriveId = drive.db.core.discoveryKey?.toString('hex')
65
69
  }
66
70
 
67
- /** @type {Transform['_transform']} */
68
- _transform(entry, _, callback) {
71
+ get #driveId() {
69
72
  // Minimal performance optimization to only call toString() once.
70
73
  // core.discoveryKey will always be defined by the time it starts
71
74
  // streaming, but could be null when the instance is first created.
72
- let driveId
73
75
  if (this.#cachedDriveId) {
74
- driveId = this.#cachedDriveId
76
+ return this.#cachedDriveId
75
77
  } else {
76
- driveId = this.#core.discoveryKey?.toString('hex')
77
- this.#cachedDriveId = driveId
78
+ this.#cachedDriveId = this.#drive.db.core.discoveryKey?.toString('hex')
79
+ return this.#cachedDriveId
80
+ }
81
+ }
82
+
83
+ async #getBlobCoreId() {
84
+ if (this.#cachedBlobCoreId) return this.#cachedBlobCoreId
85
+ const blobs = await this.#drive.getBlobs()
86
+ this.#cachedBlobCoreId = blobs.core.discoveryKey?.toString('hex')
87
+ return this.#cachedBlobCoreId
88
+ }
89
+
90
+ /** @type {Transform['_transform']} */
91
+ _transform(entry, _, callback) {
92
+ if (!this.#driveId) {
93
+ return callback(new Error('Drive discovery key unexpectedly missing'))
78
94
  }
79
- callback(null, { ...entry, driveId })
95
+ this.#getBlobCoreId()
96
+ .then((blobCoreId) => {
97
+ if (!blobCoreId) {
98
+ return callback(
99
+ new Error('Blob core discovery key unexpectedly missing')
100
+ )
101
+ }
102
+ callback(null, { ...entry, driveId: this.#driveId, blobCoreId })
103
+ })
104
+ .catch((reason) => callback(ensureError(reason)))
80
105
  }
81
106
  }
@@ -6,12 +6,26 @@ import { FilterEntriesStream } from './utils.js'
6
6
  import { noop } from '../utils.js'
7
7
  import { TypedEmitter } from 'tiny-typed-emitter'
8
8
  import { HyperdriveIndexImpl as HyperdriveIndex } from './hyperdrive-index.js'
9
+ import { Logger } from '../logger.js'
10
+ import { getErrorCode, getErrorMessage } from '../lib/error.js'
9
11
 
10
12
  /** @import Hyperdrive from 'hyperdrive' */
11
13
  /** @import { JsonObject } from 'type-fest' */
12
14
  /** @import { Readable as NodeReadable } from 'node:stream' */
13
15
  /** @import { Readable as StreamxReadable, Writable } from 'streamx' */
14
- /** @import { BlobFilter, BlobId, BlobStoreEntriesStream } from '../types.js' */
16
+ /** @import { GenericBlobFilter, BlobFilter, BlobId, BlobStoreEntriesStream } from '../types.js' */
17
+
18
+ /**
19
+ * @typedef {object} BlobStoreEvents
20
+ * @prop {(peerId: string, blobFilter: GenericBlobFilter | null) => void} blob-filter
21
+ * @prop {(opts: {
22
+ * peerId: string
23
+ * start: number
24
+ * length: number
25
+ * blobCoreId: string
26
+ * }) => void} want-blob-range
27
+ * @prop {(error: Error) => void} error
28
+ */
15
29
 
16
30
  /**
17
31
  * @internal
@@ -31,6 +45,13 @@ const SUPPORTED_BLOB_VARIANTS = /** @type {const} */ ({
31
45
  // the type with JSDoc
32
46
  export { SUPPORTED_BLOB_VARIANTS }
33
47
 
48
+ /** @type {import('../types.js').BlobFilter} */
49
+ const NON_ARCHIVE_DEVICE_DOWNLOAD_FILTER = {
50
+ photo: ['preview', 'thumbnail'],
51
+ // Don't download any audio of video files, since previews and
52
+ // thumbnails aren't supported yet.
53
+ }
54
+
34
55
  class ErrNotFound extends Error {
35
56
  constructor(message = 'NotFound') {
36
57
  super(message)
@@ -38,24 +59,117 @@ class ErrNotFound extends Error {
38
59
  }
39
60
  }
40
61
 
41
- /** @extends {TypedEmitter<{ error: (error: Error) => void }>} */
62
+ /** @extends {TypedEmitter<BlobStoreEvents>} */
42
63
  export class BlobStore extends TypedEmitter {
43
64
  #driveIndex
44
65
  /** @type {Downloader} */
45
66
  #downloader
67
+ /** @type {Map<string, GenericBlobFilter | null>} */
68
+ #blobFilters = new Map()
69
+ #l
70
+ /** @type {Map<string, BlobStoreEntriesStream>} */
71
+ #entriesStreams = new Map()
72
+ #isArchiveDevice
73
+ #deviceId
74
+
75
+ /**
76
+ * Bound function for handling download intents for both peers and self
77
+ * @param {GenericBlobFilter | null} filter
78
+ * @param {string} peerId
79
+ */
80
+ #handleDownloadIntent = async (filter, peerId) => {
81
+ this.#l.log('Download intent %o for peer %S', filter, peerId)
82
+ try {
83
+ this.#entriesStreams.get(peerId)?.destroy()
84
+ this.emit('blob-filter', peerId, filter)
85
+ this.#blobFilters.set(peerId, filter)
86
+
87
+ if (filter === null) return
88
+
89
+ const entriesReadStream = this.createEntriesReadStream({
90
+ live: true,
91
+ filter,
92
+ })
93
+ this.#entriesStreams.set(peerId, entriesReadStream)
94
+
95
+ entriesReadStream.once('close', () => {
96
+ if (this.#entriesStreams.get(peerId) === entriesReadStream) {
97
+ this.#entriesStreams.delete(peerId)
98
+ }
99
+ })
100
+
101
+ for await (const {
102
+ blobCoreId,
103
+ value: { blob },
104
+ } of entriesReadStream) {
105
+ const { blockOffset: start, blockLength: length } = blob
106
+ this.emit('want-blob-range', {
107
+ peerId,
108
+ start,
109
+ length,
110
+ blobCoreId,
111
+ })
112
+ }
113
+ } catch (err) {
114
+ if (getErrorCode(err) === 'ERR_STREAM_PREMATURE_CLOSE') return
115
+ this.#l.log(
116
+ 'Error getting blob entries stream for peer %h: %s',
117
+ peerId,
118
+ getErrorMessage(err)
119
+ )
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Bound to `this`
125
+ * This will be called whenever a peer is successfully added to the creatorcore
126
+ * @param {import('../types.js').HypercorePeer & { protomux: import('protomux')<import('../lib/noise-secret-stream-helpers.js').OpenedNoiseStream> }} peer
127
+ */
128
+ #handlePeerAdd = (peer) => {
129
+ const downloadFilter = getBlobDownloadFilter(this.#isArchiveDevice)
130
+ this.#coreManager.sendDownloadIntents(downloadFilter, peer)
131
+ }
132
+
133
+ /**
134
+ * Bound to `this`
135
+ * @param {import('../types.js').HypercorePeer & { protomux: import('protomux')<import('../lib/noise-secret-stream-helpers.js').OpenedNoiseStream> }} peer
136
+ */
137
+ #handlePeerRemove = (peer) => {
138
+ const peerKey = peer.protomux.stream.remotePublicKey
139
+ const peerId = peerKey.toString('hex')
140
+ this.#entriesStreams.get(peerId)?.destroy()
141
+ this.#entriesStreams.delete(peerId)
142
+ }
143
+
144
+ #coreManager
145
+ #logger
46
146
 
47
147
  /**
48
148
  * @param {object} options
49
149
  * @param {import('../core-manager/index.js').CoreManager} options.coreManager
50
- * @param {BlobFilter | null} options.downloadFilter - Filter blob types and/or variants to download. Set to `null` to download all blobs.
150
+ * @param {boolean} [options.isArchiveDevice] Set to `true` if this is an archive device which should download all blobs, or just a selection of blobs
151
+ * @param {import('../logger.js').Logger} [options.logger]
51
152
  */
52
- constructor({ coreManager, downloadFilter }) {
153
+ constructor({ coreManager, isArchiveDevice = true, logger }) {
53
154
  super()
155
+ this.#logger = logger
156
+ this.#l = Logger.create('blobStore', logger)
157
+ this.#isArchiveDevice = isArchiveDevice
54
158
  this.#driveIndex = new HyperdriveIndex(coreManager)
159
+ this.#coreManager = coreManager
160
+ this.#deviceId = coreManager.deviceId
161
+ const downloadFilter = getBlobDownloadFilter(isArchiveDevice)
162
+ if (downloadFilter) {
163
+ this.#handleDownloadIntent(downloadFilter, this.#deviceId)
164
+ }
55
165
  this.#downloader = new Downloader(this.#driveIndex, {
56
166
  filter: downloadFilter,
57
167
  })
58
168
  this.#downloader.on('error', (error) => this.emit('error', error))
169
+
170
+ coreManager.on('peer-download-intent', this.#handleDownloadIntent)
171
+ coreManager.creatorCore.on('peer-add', this.#handlePeerAdd)
172
+ coreManager.creatorCore.on('peer-remove', this.#handlePeerRemove)
59
173
  }
60
174
 
61
175
  /**
@@ -65,6 +179,38 @@ export class BlobStore extends TypedEmitter {
65
179
  return getDiscoveryId(this.#driveIndex.writerKey)
66
180
  }
67
181
 
182
+ get isArchiveDevice() {
183
+ return this.#isArchiveDevice
184
+ }
185
+
186
+ /**
187
+ * @param {string} peerId
188
+ * @returns {GenericBlobFilter | null}
189
+ */
190
+ getBlobFilter(peerId) {
191
+ return this.#blobFilters.get(peerId) ?? null
192
+ }
193
+
194
+ /** @param {boolean} isArchiveDevice */
195
+ async setIsArchiveDevice(isArchiveDevice) {
196
+ this.#l.log('Setting isArchiveDevice to %s', isArchiveDevice)
197
+ if (this.#isArchiveDevice === isArchiveDevice) return
198
+ this.#isArchiveDevice = isArchiveDevice
199
+ const blobDownloadFilter = getBlobDownloadFilter(isArchiveDevice)
200
+ this.#downloader.removeAllListeners()
201
+ this.#downloader.destroy()
202
+ this.#downloader = new Downloader(this.#driveIndex, {
203
+ filter: blobDownloadFilter,
204
+ })
205
+ this.#downloader.on('error', (error) => this.emit('error', error))
206
+ // Even if blobFilter is null, e.g. we plan to download everything, we still
207
+ // need to inform connected peers of the change.
208
+ for (const peer of this.#coreManager.creatorCore.peers) {
209
+ this.#coreManager.sendDownloadIntents(blobDownloadFilter, peer)
210
+ }
211
+ this.#handleDownloadIntent(blobDownloadFilter, this.#deviceId)
212
+ }
213
+
68
214
  /**
69
215
  * @param {string} driveId hex-encoded discovery key
70
216
  * @returns {Hyperdrive}
@@ -90,21 +236,6 @@ export class BlobStore extends TypedEmitter {
90
236
  return blob
91
237
  }
92
238
 
93
- /**
94
- * Set the filter for downloading blobs.
95
- *
96
- * @param {import('../types.js').BlobFilter | null} filter Filter blob types and/or variants to download. Filter is { [BlobType]: BlobVariants[] }. At least one blob variant must be specified for each blob type.
97
- * @returns {void}
98
- */
99
- setDownloadFilter(filter) {
100
- this.#downloader.removeAllListeners()
101
- this.#downloader.destroy()
102
- this.#downloader = new Downloader(this.#driveIndex, {
103
- filter,
104
- })
105
- this.#downloader.on('error', (error) => this.emit('error', error))
106
- }
107
-
108
239
  /**
109
240
  * @param {BlobId} blobId
110
241
  * @param {object} [options]
@@ -245,6 +376,9 @@ export class BlobStore extends TypedEmitter {
245
376
  close() {
246
377
  this.#downloader.removeAllListeners()
247
378
  this.#downloader.destroy()
379
+ this.#coreManager.off('peer-download-intent', this.#handleDownloadIntent)
380
+ this.#coreManager.creatorCore.off('peer-add', this.#handlePeerAdd)
381
+ this.#coreManager.creatorCore.off('peer-remove', this.#handlePeerRemove)
248
382
  }
249
383
  }
250
384
 
@@ -280,3 +414,11 @@ function makePath({ type, variant, name }) {
280
414
  function getDiscoveryId(key) {
281
415
  return discoveryKey(key).toString('hex')
282
416
  }
417
+
418
+ /**
419
+ * @param {boolean} isArchiveDevice
420
+ * @returns {null | BlobFilter}
421
+ */
422
+ function getBlobDownloadFilter(isArchiveDevice) {
423
+ return isArchiveDevice ? null : NON_ARCHIVE_DEVICE_DOWNLOAD_FILTER
424
+ }
@@ -30,7 +30,7 @@ export const kCoreManagerReplicate = Symbol('replicate core manager')
30
30
  * @typedef {Object} Events
31
31
  * @property {(coreRecord: CoreRecord) => void} add-core
32
32
  * @property {(namespace: Namespace, msg: { coreDiscoveryId: string, peerId: string, start: number, bitfield: Uint32Array }) => void} peer-have
33
- * @property {(blobFilter: GenericBlobFilter, peerId: string) => void} peer-download-intent
33
+ * @property {(blobFilter: GenericBlobFilter | null, peerId: string) => void} peer-download-intent
34
34
  */
35
35
 
36
36
  /**
@@ -41,7 +41,6 @@ export class CoreManager extends TypedEmitter {
41
41
  #coreIndex
42
42
  /** @type {CoreRecord} */
43
43
  #creatorCoreRecord
44
- #projectKey
45
44
  #queries
46
45
  #encryptionKeys
47
46
  #projectExtension
@@ -93,7 +92,6 @@ export class CoreManager extends TypedEmitter {
93
92
  this.#l = Logger.create('coreManager', logger)
94
93
  const primaryKey = keyManager.getDerivedKey('primaryKey', projectKey)
95
94
  this.#deviceId = keyManager.getIdentityKeypair().publicKey.toString('hex')
96
- this.#projectKey = projectKey
97
95
  this.#encryptionKeys = encryptionKeys
98
96
  this.#autoDownload = autoDownload
99
97
 
@@ -414,7 +412,7 @@ export class CoreManager extends TypedEmitter {
414
412
  }
415
413
 
416
414
  /**
417
- * @param {GenericBlobFilter} blobFilter
415
+ * @param {GenericBlobFilter | null} blobFilter
418
416
  * @param {HypercorePeer} peer
419
417
  */
420
418
  #handleDownloadIntentMessage(blobFilter, peer) {
@@ -423,7 +421,7 @@ export class CoreManager extends TypedEmitter {
423
421
  }
424
422
 
425
423
  /**
426
- * @param {BlobFilter} blobFilter
424
+ * @param {BlobFilter | null} blobFilter
427
425
  * @param {HypercorePeer} peer
428
426
  */
429
427
  sendDownloadIntents(blobFilter, peer) {
@@ -542,20 +540,27 @@ const HaveExtensionCodec = {
542
540
  }
543
541
 
544
542
  const DownloadIntentCodec = {
545
- /** @param {BlobFilter} filter */
543
+ /** @param {BlobFilter | null} filter */
546
544
  encode(filter) {
547
- const downloadIntents = mapObject(filter, (key, value) => [
545
+ const downloadIntents = mapObject(filter || {}, (key, value) => [
548
546
  key,
549
547
  { variants: value || [] },
550
548
  ])
551
- return DownloadIntentExtension.encode({ downloadIntents }).finish()
549
+ // If filter is null, we want to download everything
550
+ const everything = !filter
551
+ return DownloadIntentExtension.encode({
552
+ downloadIntents,
553
+ everything,
554
+ }).finish()
552
555
  },
553
556
  /**
554
557
  * @param {Buffer | Uint8Array} buf
555
- * @returns {GenericBlobFilter}
558
+ * @returns {GenericBlobFilter | null}
556
559
  */
557
560
  decode(buf) {
558
561
  const msg = DownloadIntentExtension.decode(buf)
562
+ // If everything is true, we ignore the downloadIntents and return null, which means download everything
563
+ if (msg.everything) return null
559
564
  return mapObject(msg.downloadIntents, (key, value) => [
560
565
  key + '', // keep TS happy
561
566
  value.variants,
@@ -8,29 +8,57 @@ import { TypedEmitter } from 'tiny-typed-emitter'
8
8
  import { parse as parseBCP47 } from 'bcp-47'
9
9
  import { setProperty, getProperty } from 'dot-prop'
10
10
  /** @import { MapeoDoc, MapeoValue } from '@comapeo/schema' */
11
- /** @import { MapeoDocMap, MapeoValueMap } from '../types.js' */
11
+ /** @import { RunResult } from 'better-sqlite3' */
12
+ /** @import { SQLiteSelectBase } from 'drizzle-orm/sqlite-core' */
13
+ /** @import { Exact } from 'type-fest' */
12
14
  /** @import { DataStore } from '../datastore/index.js' */
15
+ /**
16
+ * @import {
17
+ * CoreOwnershipWithSignaturesValue,
18
+ * DefaultEmitterEvents,
19
+ * MapeoDocMap,
20
+ * MapeoValueMap,
21
+ * } from '../types.js'
22
+ */
13
23
 
14
24
  /**
25
+ * @internal
15
26
  * @typedef {`${MapeoDoc['schemaName']}Table`} MapeoDocTableName
16
27
  */
28
+
17
29
  /**
30
+ * @internal
18
31
  * @template T
19
32
  * @typedef {T[(keyof T) & MapeoDocTableName]} GetMapeoDocTables
20
33
  */
34
+
21
35
  /**
22
36
  * Union of Drizzle schema tables that correspond to MapeoDoc types (e.g. excluding backlink tables and other utility tables)
37
+ * @internal
23
38
  * @typedef {GetMapeoDocTables<typeof import('../schema/project.js')> | GetMapeoDocTables<typeof import('../schema/client.js')>} MapeoDocTables
24
39
  */
40
+
25
41
  /**
42
+ * @internal
26
43
  * @typedef {{ [K in MapeoDocTables['_']['name']]: Extract<MapeoDocTables, { _: { name: K }}> }} MapeoDocTablesMap
27
44
  */
45
+
28
46
  /**
47
+ * @internal
29
48
  * @template T
30
49
  * @template {keyof any} K
31
50
  * @typedef {T extends any ? Omit<T, K> : never} OmitUnion
32
51
  */
52
+
53
+ /**
54
+ * @internal
55
+ * @template {MapeoValue} T
56
+ * @template {MapeoValue['schemaName']} S
57
+ * @typedef {Exclude<T, { schemaName: S }>} ExcludeSchema
58
+ */
59
+
33
60
  /**
61
+ * @internal
34
62
  * @template {MapeoDoc} TDoc
35
63
  * @typedef {object} DataTypeEvents
36
64
  * @property {(docs: TDoc[]) => void} updated-docs
@@ -49,11 +77,11 @@ export const kDataStore = Symbol('dataStore')
49
77
 
50
78
  /**
51
79
  * @template {DataStore} TDataStore
52
- * @template {TDataStore['schemas'][number]} TSchemaName
53
- * @template {MapeoDocTablesMap[TSchemaName]} TTable
54
- * @template {Exclude<MapeoDocMap[TSchemaName], { schemaName: 'coreOwnership' }>} TDoc
55
- * @template {Exclude<MapeoValueMap[TSchemaName], { schemaName: 'coreOwnership' }>} TValue
56
- * @extends {TypedEmitter<DataTypeEvents<TDoc> & import('../types.js').DefaultEmitterEvents<DataTypeEvents<TDoc>>>}
80
+ * @template {MapeoDocTables} TTable
81
+ * @template {TTable['_']['name']} TSchemaName
82
+ * @template {MapeoDocMap[TSchemaName]} TDoc
83
+ * @template {MapeoValueMap[TSchemaName]} TValue
84
+ * @extends {TypedEmitter<DataTypeEvents<TDoc> & DefaultEmitterEvents<DataTypeEvents<TDoc>>>}
57
85
  */
58
86
  export class DataType extends TypedEmitter {
59
87
  #dataStore
@@ -106,36 +134,42 @@ export class DataType extends TypedEmitter {
106
134
  })
107
135
  }
108
136
 
137
+ /** @returns {TTable} */
109
138
  get [kTable]() {
110
139
  return this.#table
111
140
  }
112
141
 
142
+ /** @returns {TSchemaName} */
113
143
  get schemaName() {
114
144
  return this.#schemaName
115
145
  }
116
146
 
147
+ /** @returns {TDataStore['namespace']} */
117
148
  get namespace() {
118
149
  return this.#dataStore.namespace
119
150
  }
120
151
 
152
+ /** @returns {TDataStore} */
121
153
  get [kDataStore]() {
122
154
  return this.#dataStore
123
155
  }
124
156
 
125
157
  /**
126
- * @template {import('type-fest').Exact<TValue, T>} T
158
+ * @template {Exact<ExcludeSchema<TValue, 'coreOwnership'>, T>} T
127
159
  * @param {T} value
160
+ * @returns {Promise<TDoc & { forks: string[] }>}
128
161
  */
129
162
  async create(value) {
130
163
  const docId = generateId()
131
- // @ts-expect-error - can't figure this one out, types in index.d.ts override this
164
+ // @ts-expect-error - can't figure this one out
132
165
  return this[kCreateWithDocId](docId, value, { checkExisting: false })
133
166
  }
134
167
 
135
168
  /**
136
169
  * @param {string} docId
137
- * @param {TValue | import('../types.js').CoreOwnershipWithSignaturesValue} value
170
+ * @param {ExcludeSchema<TValue, 'coreOwnership'> | CoreOwnershipWithSignaturesValue} value
138
171
  * @param {{ checkExisting?: boolean }} [opts] - only used internally to skip the checkExisting check when creating a document with a random ID (collisions should be too small probability to be worth checking for)
172
+ * @returns {Promise<TDoc & { forks: string[] }>}
139
173
  */
140
174
  async [kCreateWithDocId](docId, value, { checkExisting = true } = {}) {
141
175
  if (!validate(this.#schemaName, value)) {
@@ -193,15 +227,18 @@ export class DataType extends TypedEmitter {
193
227
  /**
194
228
  * @param {string} versionId
195
229
  * @param {{ lang?: string }} [opts]
230
+ * @returns {Promise<TDoc>}
196
231
  */
197
232
  async getByVersionId(versionId, { lang } = {}) {
198
233
  const result = await this.#dataStore.read(versionId)
199
- return this.#translate(result, { lang })
234
+ if (result.schemaName !== this.#schemaName) throw new NotFoundError()
235
+ return this.#translate(deNullify(result), { lang })
200
236
  }
201
237
 
202
238
  /**
203
239
  * @param {any} doc
204
240
  * @param {{ lang?: string }} [opts]
241
+ * @returns {Promise<TDoc & { forks: string[] }>}
205
242
  */
206
243
  async #translate(doc, { lang } = {}) {
207
244
  if (!lang) return doc
@@ -235,27 +272,27 @@ export class DataType extends TypedEmitter {
235
272
  return doc
236
273
  }
237
274
 
238
- /** @param {{ includeDeleted?: boolean, lang?: string }} [opts] */
275
+ /**
276
+ * @param {object} opts
277
+ * @param {boolean} [opts.includeDeleted]
278
+ * @param {string} [opts.lang]
279
+ * @returns {Promise<Array<TDoc & { forks: string[] }>>}
280
+ */
239
281
  async getMany({ includeDeleted = false, lang } = {}) {
240
282
  await this.#dataStore.indexer.idle()
241
283
  const rows = includeDeleted
242
284
  ? this.#sql.getManyWithDeleted.all()
243
285
  : this.#sql.getMany.all()
244
286
  return await Promise.all(
245
- rows.map(
246
- async (doc) =>
247
- await this.#translate(deNullify(/** @type {MapeoDoc} */ (doc)), {
248
- lang,
249
- })
250
- )
287
+ rows.map((doc) => this.#translate(deNullify(doc), { lang }))
251
288
  )
252
289
  }
253
290
 
254
291
  /**
255
- *
256
- * @template {import('type-fest').Exact<TValue, T>} T
292
+ * @template {Exact<ExcludeSchema<TValue, 'coreOwnership'>, T>} T
257
293
  * @param {string | string[]} versionId
258
294
  * @param {T} value
295
+ * @returns {Promise<TDoc & { forks: string[] }>}
259
296
  */
260
297
  async update(versionId, value) {
261
298
  await this.#dataStore.indexer.idle()
@@ -279,6 +316,7 @@ export class DataType extends TypedEmitter {
279
316
 
280
317
  /**
281
318
  * @param {string} docId
319
+ * @returns {Promise<TDoc & { forks: string[] }>}
282
320
  */
283
321
  async delete(docId) {
284
322
  await this.#dataStore.indexer.idle()
@@ -120,6 +120,7 @@ export async function plugin(fastify, opts) {
120
120
  if (resp && resp.status === 200) {
121
121
  return reply
122
122
  .headers({
123
+ 'access-control-allow-origin': '*',
123
124
  'cache-control': 'no-cache',
124
125
  })
125
126
  .redirect(url.toString())
@@ -43,16 +43,3 @@ export async function getFastifyServerAddress(server, { timeout } = {}) {
43
43
 
44
44
  return 'http://' + addr
45
45
  }
46
-
47
- /**
48
- * @param {Readonly<Date>} lastModified
49
- */
50
- export function createStyleJsonResponseHeaders(lastModified) {
51
- return {
52
- 'Cache-Control': 'max-age=' + 5 * 60, // 5 minutes
53
- 'Access-Control-Allow-Headers':
54
- 'Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since',
55
- 'Access-Control-Allow-Origin': '*',
56
- 'Last-Modified': lastModified.toUTCString(),
57
- }
58
- }
@@ -24,6 +24,13 @@ export interface DownloadIntentExtension {
24
24
  downloadIntents: {
25
25
  [key: string]: DownloadIntentExtension_DownloadIntent;
26
26
  };
27
+ /**
28
+ * If true, the peer intends to download all blobs - this is the default
29
+ * assumption when a peer has not sent a download intent, but if a peer
30
+ * changes their intent while connected, we need to send the new intent to
31
+ * download everything.
32
+ */
33
+ everything: boolean;
27
34
  }
28
35
  export interface DownloadIntentExtension_DownloadIntent {
29
36
  variants: string[];