@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.
- package/dist/blob-store/entries-stream.d.ts.map +1 -1
- package/dist/blob-store/index.d.ts +27 -15
- package/dist/blob-store/index.d.ts.map +1 -1
- package/dist/core-manager/index.d.ts +4 -4
- package/dist/core-manager/index.d.ts.map +1 -1
- package/dist/datastore/index.d.ts +1 -1
- package/dist/datatype/index.d.ts +141 -111
- package/dist/datatype/index.d.ts.map +1 -0
- package/dist/fastify-plugins/utils.d.ts +0 -9
- package/dist/fastify-plugins/utils.d.ts.map +1 -1
- package/dist/generated/extensions.d.ts +7 -0
- package/dist/generated/extensions.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/mapeo-project.d.ts +17 -17
- package/dist/mapeo-project.d.ts.map +1 -1
- package/dist/schema/project.d.ts +1 -1
- package/dist/sync/core-sync-state.d.ts +14 -6
- package/dist/sync/core-sync-state.d.ts.map +1 -1
- package/dist/sync/namespace-sync-state.d.ts +3 -13
- package/dist/sync/namespace-sync-state.d.ts.map +1 -1
- package/dist/sync/sync-api.d.ts +17 -25
- package/dist/sync/sync-api.d.ts.map +1 -1
- package/dist/sync/sync-state.d.ts +3 -13
- package/dist/sync/sync-state.d.ts.map +1 -1
- package/dist/translation-api.d.ts +2 -2
- package/dist/translation-api.d.ts.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -5
- package/src/blob-store/entries-stream.js +43 -18
- package/src/blob-store/index.js +161 -19
- package/src/core-manager/index.js +14 -9
- package/src/datatype/index.js +57 -19
- package/src/fastify-plugins/maps.js +1 -0
- package/src/fastify-plugins/utils.js +0 -13
- package/src/generated/extensions.d.ts +7 -0
- package/src/generated/extensions.js +12 -2
- package/src/generated/extensions.ts +19 -1
- package/src/mapeo-project.js +7 -76
- package/src/sync/core-sync-state.js +41 -14
- package/src/sync/namespace-sync-state.js +25 -22
- package/src/sync/sync-api.js +12 -43
- package/src/sync/sync-state.js +9 -19
- package/src/translation-api.js +2 -1
- package/src/types.ts +1 -1
- 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
|
|
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
|
|
33
|
+
mergedEntriesStreams.add(getHistoryStream(drive, { live }))
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
/**
|
|
37
38
|
*
|
|
38
|
-
* @param {
|
|
39
|
+
* @param {Hyperdrive} drive
|
|
39
40
|
* @param {object} opts
|
|
40
41
|
* @param {boolean} opts.live
|
|
41
42
|
*/
|
|
42
|
-
function getHistoryStream(
|
|
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 =
|
|
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(
|
|
54
|
+
return pipeline(historyStream, new AddDriveIds(drive), noop)
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
class AddDriveIds extends Transform {
|
|
57
|
-
#
|
|
58
|
+
#drive
|
|
59
|
+
/** @type {string | undefined} */
|
|
58
60
|
#cachedDriveId
|
|
61
|
+
/** @type {string | undefined} */
|
|
62
|
+
#cachedBlobCoreId
|
|
59
63
|
|
|
60
|
-
/** @param {
|
|
61
|
-
constructor(
|
|
64
|
+
/** @param {Hyperdrive} drive */
|
|
65
|
+
constructor(drive) {
|
|
62
66
|
super({ objectMode: true })
|
|
63
|
-
this.#
|
|
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
|
-
|
|
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
|
-
|
|
76
|
+
return this.#cachedDriveId
|
|
75
77
|
} else {
|
|
76
|
-
|
|
77
|
-
this.#cachedDriveId
|
|
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
|
-
|
|
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
|
}
|
package/src/blob-store/index.js
CHANGED
|
@@ -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<
|
|
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 {
|
|
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,
|
|
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
|
-
|
|
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,
|
package/src/datatype/index.js
CHANGED
|
@@ -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 {
|
|
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 {
|
|
53
|
-
* @template {
|
|
54
|
-
* @template {
|
|
55
|
-
* @template {
|
|
56
|
-
* @extends {TypedEmitter<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 {
|
|
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
|
|
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 |
|
|
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
|
-
|
|
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
|
-
/**
|
|
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()
|
|
@@ -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[];
|