@comapeo/core 2.1.0 → 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.
- package/dist/blob-store/downloader.d.ts +43 -0
- package/dist/blob-store/downloader.d.ts.map +1 -0
- package/dist/blob-store/entries-stream.d.ts +13 -0
- package/dist/blob-store/entries-stream.d.ts.map +1 -0
- package/dist/blob-store/hyperdrive-index.d.ts +20 -0
- package/dist/blob-store/hyperdrive-index.d.ts.map +1 -0
- package/dist/blob-store/index.d.ts +29 -21
- package/dist/blob-store/index.d.ts.map +1 -1
- package/dist/blob-store/utils.d.ts +27 -0
- package/dist/blob-store/utils.d.ts.map +1 -0
- package/dist/core-manager/index.d.ts +1 -1
- package/dist/core-manager/index.d.ts.map +1 -1
- package/dist/core-ownership.d.ts.map +1 -1
- package/dist/datastore/index.d.ts +1 -1
- package/dist/datastore/index.d.ts.map +1 -1
- package/dist/datatype/index.d.ts +5 -1
- package/dist/discovery/local-discovery.d.ts.map +1 -1
- package/dist/errors.d.ts +6 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/fastify-plugins/blobs.d.ts.map +1 -1
- package/dist/fastify-plugins/maps.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/error.d.ts +14 -0
- package/dist/lib/error.d.ts.map +1 -1
- package/dist/mapeo-manager.d.ts.map +1 -1
- package/dist/mapeo-project.d.ts +17 -17
- package/dist/mapeo-project.d.ts.map +1 -1
- package/dist/member-api.d.ts +4 -0
- package/dist/member-api.d.ts.map +1 -1
- package/dist/roles.d.ts.map +1 -1
- package/dist/schema/project.d.ts +2 -2
- package/dist/sync/core-sync-state.d.ts +20 -15
- package/dist/sync/core-sync-state.d.ts.map +1 -1
- package/dist/sync/namespace-sync-state.d.ts +13 -1
- package/dist/sync/namespace-sync-state.d.ts.map +1 -1
- package/dist/sync/peer-sync-controller.d.ts +1 -1
- package/dist/sync/sync-api.d.ts +22 -3
- package/dist/sync/sync-api.d.ts.map +1 -1
- package/dist/sync/sync-state.d.ts +12 -0
- 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 +7 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -1
- package/src/blob-store/downloader.js +130 -0
- package/src/blob-store/entries-stream.js +81 -0
- package/src/blob-store/hyperdrive-index.js +122 -0
- package/src/blob-store/index.js +56 -115
- package/src/blob-store/utils.js +54 -0
- package/src/core-manager/index.js +2 -1
- package/src/core-ownership.js +2 -4
- package/src/datastore/index.js +4 -3
- package/src/datatype/index.d.ts +5 -1
- package/src/datatype/index.js +22 -9
- package/src/discovery/local-discovery.js +2 -1
- package/src/errors.js +11 -2
- package/src/fastify-plugins/blobs.js +16 -1
- package/src/fastify-plugins/maps.js +2 -1
- package/src/lib/error.js +24 -0
- package/src/mapeo-manager.js +3 -2
- package/src/mapeo-project.js +89 -19
- package/src/member-api.js +68 -26
- package/src/roles.js +38 -32
- package/src/sync/core-sync-state.js +39 -23
- package/src/sync/namespace-sync-state.js +22 -0
- package/src/sync/sync-api.js +30 -4
- package/src/sync/sync-state.js +18 -0
- package/src/translation-api.js +5 -9
- package/src/types.ts +8 -0
- package/dist/blob-store/live-download.d.ts +0 -107
- package/dist/blob-store/live-download.d.ts.map +0 -1
- package/src/blob-store/live-download.js +0 -373
package/src/lib/error.js
CHANGED
|
@@ -21,6 +21,30 @@ export class ErrorWithCode extends Error {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* If the argument is an `Error` instance, return its `code` property if it is a string.
|
|
26
|
+
* Otherwise, returns `undefined`.
|
|
27
|
+
*
|
|
28
|
+
* @param {unknown} maybeError
|
|
29
|
+
* @returns {undefined | string}
|
|
30
|
+
* @example
|
|
31
|
+
* try {
|
|
32
|
+
* // do something
|
|
33
|
+
* } catch (err) {
|
|
34
|
+
* console.error(getErrorCode(err))
|
|
35
|
+
* }
|
|
36
|
+
*/
|
|
37
|
+
export function getErrorCode(maybeError) {
|
|
38
|
+
if (
|
|
39
|
+
maybeError instanceof Error &&
|
|
40
|
+
'code' in maybeError &&
|
|
41
|
+
typeof maybeError.code === 'string'
|
|
42
|
+
) {
|
|
43
|
+
return maybeError.code
|
|
44
|
+
}
|
|
45
|
+
return undefined
|
|
46
|
+
}
|
|
47
|
+
|
|
24
48
|
/**
|
|
25
49
|
* Get the error message from an object if possible.
|
|
26
50
|
* Otherwise, stringify the argument.
|
package/src/mapeo-manager.js
CHANGED
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
kRequestFullStop,
|
|
52
52
|
kRescindFullStopRequest,
|
|
53
53
|
} from './sync/sync-api.js'
|
|
54
|
+
import { NotFoundError } from './errors.js'
|
|
54
55
|
/** @import { ProjectSettingsValue as ProjectValue } from '@comapeo/schema' */
|
|
55
56
|
/** @import NoiseSecretStream from '@hyperswarm/secret-stream' */
|
|
56
57
|
/** @import { SetNonNullable } from 'type-fest' */
|
|
@@ -456,7 +457,7 @@ export class MapeoManager extends TypedEmitter {
|
|
|
456
457
|
.get()
|
|
457
458
|
|
|
458
459
|
if (!projectKeysTableResult) {
|
|
459
|
-
throw new
|
|
460
|
+
throw new NotFoundError(`Project ID ${projectPublicId} not found`)
|
|
460
461
|
}
|
|
461
462
|
|
|
462
463
|
const { projectId } = projectKeysTableResult
|
|
@@ -896,7 +897,7 @@ export class MapeoManager extends TypedEmitter {
|
|
|
896
897
|
.get()
|
|
897
898
|
|
|
898
899
|
if (!row) {
|
|
899
|
-
throw new
|
|
900
|
+
throw new NotFoundError(`Project ID ${projectPublicId} not found`)
|
|
900
901
|
}
|
|
901
902
|
|
|
902
903
|
const { keysCipher, projectId, projectInfo } = row
|
package/src/mapeo-project.js
CHANGED
|
@@ -49,15 +49,20 @@ import { omit } from './lib/omit.js'
|
|
|
49
49
|
import { MemberApi } from './member-api.js'
|
|
50
50
|
import {
|
|
51
51
|
SyncApi,
|
|
52
|
+
kAddBlobWantRange,
|
|
53
|
+
kClearBlobWantRanges,
|
|
52
54
|
kHandleDiscoveryKey,
|
|
55
|
+
kSetBlobDownloadFilter,
|
|
53
56
|
kWaitForInitialSyncWithPeer,
|
|
54
57
|
} from './sync/sync-api.js'
|
|
55
58
|
import { Logger } from './logger.js'
|
|
56
59
|
import { IconApi } from './icon-api.js'
|
|
57
60
|
import { readConfig } from './config-import.js'
|
|
58
61
|
import TranslationApi from './translation-api.js'
|
|
62
|
+
import { NotFoundError, nullIfNotFound } from './errors.js'
|
|
63
|
+
import { getErrorCode, getErrorMessage } from './lib/error.js'
|
|
59
64
|
/** @import { ProjectSettingsValue } from '@comapeo/schema' */
|
|
60
|
-
/** @import { CoreStorage, KeyPair, Namespace, ReplicationStream } from './types.js' */
|
|
65
|
+
/** @import { CoreStorage, BlobFilter, BlobStoreEntriesStream, KeyPair, Namespace, ReplicationStream } from './types.js' */
|
|
61
66
|
|
|
62
67
|
/** @typedef {Omit<ProjectSettingsValue, 'schemaName'>} EditableProjectSettings */
|
|
63
68
|
/** @typedef {ProjectSettingsValue['configMetadata']} ConfigMetadata */
|
|
@@ -77,6 +82,13 @@ export const kIsArchiveDevice = Symbol('isArchiveDevice (temp - test only)')
|
|
|
77
82
|
|
|
78
83
|
const EMPTY_PROJECT_SETTINGS = Object.freeze({})
|
|
79
84
|
|
|
85
|
+
/** @type {import('./types.js').BlobFilter} */
|
|
86
|
+
const NON_ARCHIVE_DEVICE_DOWNLOAD_FILTER = {
|
|
87
|
+
photo: ['preview', 'thumbnail'],
|
|
88
|
+
// Don't download any audio of video files, since previews and
|
|
89
|
+
// thumbnails aren't supported yet.
|
|
90
|
+
}
|
|
91
|
+
|
|
80
92
|
/**
|
|
81
93
|
* @extends {TypedEmitter<{ close: () => void }>}
|
|
82
94
|
*/
|
|
@@ -146,6 +158,8 @@ export class MapeoProject extends TypedEmitter {
|
|
|
146
158
|
|
|
147
159
|
const getReplicationStream = this[kProjectReplicate].bind(this, true)
|
|
148
160
|
|
|
161
|
+
const blobDownloadFilter = getBlobDownloadFilter(isArchiveDevice)
|
|
162
|
+
|
|
149
163
|
///////// 1. Setup database
|
|
150
164
|
|
|
151
165
|
this.#sqlite = new Database(dbPath)
|
|
@@ -362,6 +376,13 @@ export class MapeoProject extends TypedEmitter {
|
|
|
362
376
|
|
|
363
377
|
this.#blobStore = new BlobStore({
|
|
364
378
|
coreManager: this.#coreManager,
|
|
379
|
+
downloadFilter: blobDownloadFilter,
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
this.#blobStore.on('error', (err) => {
|
|
383
|
+
// TODO: Handle this error in some way - this error will come from an
|
|
384
|
+
// unexpected error with background blob downloads
|
|
385
|
+
console.error('BlobStore error', err)
|
|
365
386
|
})
|
|
366
387
|
|
|
367
388
|
this.$blobs = new BlobApi({
|
|
@@ -391,7 +412,7 @@ export class MapeoProject extends TypedEmitter {
|
|
|
391
412
|
coreManager: this.#coreManager,
|
|
392
413
|
coreOwnership: this.#coreOwnership,
|
|
393
414
|
roles: this.#roles,
|
|
394
|
-
blobDownloadFilter
|
|
415
|
+
blobDownloadFilter,
|
|
395
416
|
logger: this.#l,
|
|
396
417
|
getServerWebsocketUrls: async () => {
|
|
397
418
|
const members = await this.#memberApi.getMany()
|
|
@@ -413,6 +434,48 @@ export class MapeoProject extends TypedEmitter {
|
|
|
413
434
|
getReplicationStream,
|
|
414
435
|
})
|
|
415
436
|
|
|
437
|
+
/** @type {Map<string, BlobStoreEntriesStream>} */
|
|
438
|
+
const entriesReadStreams = new Map()
|
|
439
|
+
|
|
440
|
+
this.#coreManager.on('peer-download-intent', async (filter, peerId) => {
|
|
441
|
+
entriesReadStreams.get(peerId)?.destroy()
|
|
442
|
+
|
|
443
|
+
const entriesReadStream = this.#blobStore.createEntriesReadStream({
|
|
444
|
+
live: true,
|
|
445
|
+
filter,
|
|
446
|
+
})
|
|
447
|
+
entriesReadStreams.set(peerId, entriesReadStream)
|
|
448
|
+
|
|
449
|
+
entriesReadStream.once('close', () => {
|
|
450
|
+
if (entriesReadStreams.get(peerId) === entriesReadStream) {
|
|
451
|
+
entriesReadStreams.delete(peerId)
|
|
452
|
+
}
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
this.#syncApi[kClearBlobWantRanges](peerId)
|
|
456
|
+
|
|
457
|
+
try {
|
|
458
|
+
for await (const entry of entriesReadStream) {
|
|
459
|
+
const { blockOffset, blockLength } = entry.value.blob
|
|
460
|
+
this.#syncApi[kAddBlobWantRange](peerId, blockOffset, blockLength)
|
|
461
|
+
}
|
|
462
|
+
} catch (err) {
|
|
463
|
+
if (getErrorCode(err) === 'ERR_STREAM_PREMATURE_CLOSE') return
|
|
464
|
+
this.#l.log(
|
|
465
|
+
'Error getting blob entries stream for peer %h: %s',
|
|
466
|
+
peerId,
|
|
467
|
+
getErrorMessage(err)
|
|
468
|
+
)
|
|
469
|
+
}
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
this.#coreManager.creatorCore.on('peer-remove', (peer) => {
|
|
473
|
+
const peerKey = peer.protomux.stream.remotePublicKey
|
|
474
|
+
const peerId = peerKey.toString('hex')
|
|
475
|
+
entriesReadStreams.get(peerId)?.destroy()
|
|
476
|
+
entriesReadStreams.delete(peerId)
|
|
477
|
+
})
|
|
478
|
+
|
|
416
479
|
this.#translationApi = new TranslationApi({
|
|
417
480
|
dataType: this.#dataTypes.translation,
|
|
418
481
|
})
|
|
@@ -506,6 +569,7 @@ export class MapeoProject extends TypedEmitter {
|
|
|
506
569
|
*/
|
|
507
570
|
async close() {
|
|
508
571
|
this.#l.log('closing project %h', this.#projectId)
|
|
572
|
+
this.#blobStore.close()
|
|
509
573
|
const dataStorePromises = []
|
|
510
574
|
for (const dataStore of Object.values(this.#dataStores)) {
|
|
511
575
|
dataStorePromises.push(dataStore.close())
|
|
@@ -599,14 +663,9 @@ export class MapeoProject extends TypedEmitter {
|
|
|
599
663
|
async $setProjectSettings(settings) {
|
|
600
664
|
const { projectSettings } = this.#dataTypes
|
|
601
665
|
|
|
602
|
-
// We only want to catch the error to the getByDocId call
|
|
603
|
-
// Using try/catch for this is a little verbose when dealing with TS types
|
|
604
666
|
const existing = await projectSettings
|
|
605
667
|
.getByDocId(this.#projectId)
|
|
606
|
-
.catch(
|
|
607
|
-
// project does not exist so return null
|
|
608
|
-
return null
|
|
609
|
-
})
|
|
668
|
+
.catch(nullIfNotFound)
|
|
610
669
|
|
|
611
670
|
if (existing) {
|
|
612
671
|
return extractEditableProjectSettings(
|
|
@@ -659,7 +718,7 @@ export class MapeoProject extends TypedEmitter {
|
|
|
659
718
|
const coreId = this.#coreManager
|
|
660
719
|
.getCoreByDiscoveryKey(coreDiscoveryKey)
|
|
661
720
|
?.key.toString('hex')
|
|
662
|
-
if (!coreId) throw new
|
|
721
|
+
if (!coreId) throw new NotFoundError()
|
|
663
722
|
return this.#coreOwnership.getOwner(coreId)
|
|
664
723
|
}
|
|
665
724
|
|
|
@@ -714,20 +773,23 @@ export class MapeoProject extends TypedEmitter {
|
|
|
714
773
|
schemaName: /** @type {const} */ ('deviceInfo'),
|
|
715
774
|
}
|
|
716
775
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
776
|
+
const existingDoc = await deviceInfo
|
|
777
|
+
.getByDocId(configCoreId)
|
|
778
|
+
.catch(nullIfNotFound)
|
|
779
|
+
if (existingDoc) {
|
|
780
|
+
return await deviceInfo.update(existingDoc.versionId, doc)
|
|
781
|
+
} else {
|
|
721
782
|
return await deviceInfo[kCreateWithDocId](configCoreId, doc)
|
|
722
783
|
}
|
|
723
|
-
|
|
724
|
-
return deviceInfo.update(existingDoc.versionId, doc)
|
|
725
784
|
}
|
|
726
785
|
|
|
727
786
|
/** @param {boolean} isArchiveDevice */
|
|
728
787
|
async [kSetIsArchiveDevice](isArchiveDevice) {
|
|
788
|
+
if (this.#isArchiveDevice === isArchiveDevice) return
|
|
789
|
+
const blobDownloadFilter = getBlobDownloadFilter(isArchiveDevice)
|
|
790
|
+
this.#blobStore.setDownloadFilter(blobDownloadFilter)
|
|
791
|
+
this.#syncApi[kSetBlobDownloadFilter](blobDownloadFilter)
|
|
729
792
|
this.#isArchiveDevice = isArchiveDevice
|
|
730
|
-
// TODO: call this.#syncApi[kSetBlobDownloadFilter]()
|
|
731
793
|
}
|
|
732
794
|
|
|
733
795
|
/** @returns {boolean} */
|
|
@@ -879,7 +941,7 @@ export class MapeoProject extends TypedEmitter {
|
|
|
879
941
|
const fieldRefs = fieldNames.map((fieldName) => {
|
|
880
942
|
const fieldRef = fieldNameToRef.get(fieldName)
|
|
881
943
|
if (!fieldRef) {
|
|
882
|
-
throw new
|
|
944
|
+
throw new NotFoundError(
|
|
883
945
|
`field ${fieldName} not found (referenced by preset ${value.name})})`
|
|
884
946
|
)
|
|
885
947
|
}
|
|
@@ -891,7 +953,7 @@ export class MapeoProject extends TypedEmitter {
|
|
|
891
953
|
}
|
|
892
954
|
const iconRef = iconNameToRef.get(iconName)
|
|
893
955
|
if (!iconRef) {
|
|
894
|
-
throw new
|
|
956
|
+
throw new NotFoundError(
|
|
895
957
|
`icon ${iconName} not found (referenced by preset ${value.name})`
|
|
896
958
|
)
|
|
897
959
|
}
|
|
@@ -938,7 +1000,7 @@ export class MapeoProject extends TypedEmitter {
|
|
|
938
1000
|
})
|
|
939
1001
|
)
|
|
940
1002
|
} else {
|
|
941
|
-
throw new
|
|
1003
|
+
throw new NotFoundError(
|
|
942
1004
|
`docRef for ${value.docRefType} with name ${name} not found`
|
|
943
1005
|
)
|
|
944
1006
|
}
|
|
@@ -993,6 +1055,14 @@ export class MapeoProject extends TypedEmitter {
|
|
|
993
1055
|
}
|
|
994
1056
|
}
|
|
995
1057
|
|
|
1058
|
+
/**
|
|
1059
|
+
* @param {boolean} isArchiveDevice
|
|
1060
|
+
* @returns {null | BlobFilter}
|
|
1061
|
+
*/
|
|
1062
|
+
function getBlobDownloadFilter(isArchiveDevice) {
|
|
1063
|
+
return isArchiveDevice ? null : NON_ARCHIVE_DEVICE_DOWNLOAD_FILTER
|
|
1064
|
+
}
|
|
1065
|
+
|
|
996
1066
|
/**
|
|
997
1067
|
* @param {import("@comapeo/schema").ProjectSettings & { forks: string[] }} projectDoc
|
|
998
1068
|
* @returns {EditableProjectSettings}
|
package/src/member-api.js
CHANGED
|
@@ -277,6 +277,10 @@ export class MemberApi extends TypedEmitter {
|
|
|
277
277
|
* peer. For example, the project must have a name.
|
|
278
278
|
* - `NETWORK_ERROR`: there was an issue connecting to the server. Is the
|
|
279
279
|
* device online? Is the server online?
|
|
280
|
+
* - `SERVER_HAS_TOO_MANY_PROJECTS`: the server limits the number of projects
|
|
281
|
+
* it can have, and it's at the limit.
|
|
282
|
+
* - `PROJECT_NOT_IN_SERVER_ALLOWLIST`: the server only allows specific
|
|
283
|
+
* projects to be added and ours wasn't one of them.
|
|
280
284
|
* - `INVALID_SERVER_RESPONSE`: we connected to the server but it returned
|
|
281
285
|
* an unexpected response. Is the server running a compatible version of
|
|
282
286
|
* CoMapeo Cloud?
|
|
@@ -351,32 +355,7 @@ export class MemberApi extends TypedEmitter {
|
|
|
351
355
|
)
|
|
352
356
|
}
|
|
353
357
|
|
|
354
|
-
|
|
355
|
-
throw new ErrorWithCode(
|
|
356
|
-
'INVALID_SERVER_RESPONSE',
|
|
357
|
-
`Failed to add server peer due to HTTP status code ${response.status}`
|
|
358
|
-
)
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
try {
|
|
362
|
-
const responseBody = await response.json()
|
|
363
|
-
assert(
|
|
364
|
-
responseBody &&
|
|
365
|
-
typeof responseBody === 'object' &&
|
|
366
|
-
'data' in responseBody &&
|
|
367
|
-
responseBody.data &&
|
|
368
|
-
typeof responseBody.data === 'object' &&
|
|
369
|
-
'deviceId' in responseBody.data &&
|
|
370
|
-
typeof responseBody.data.deviceId === 'string',
|
|
371
|
-
'Response body is valid'
|
|
372
|
-
)
|
|
373
|
-
return { serverDeviceId: responseBody.data.deviceId }
|
|
374
|
-
} catch (err) {
|
|
375
|
-
throw new ErrorWithCode(
|
|
376
|
-
'INVALID_SERVER_RESPONSE',
|
|
377
|
-
"Failed to add server peer because we couldn't parse the response"
|
|
378
|
-
)
|
|
379
|
-
}
|
|
358
|
+
return await parseAddServerResponse(response)
|
|
380
359
|
}
|
|
381
360
|
|
|
382
361
|
/**
|
|
@@ -575,3 +554,66 @@ function isValidServerBaseUrl(
|
|
|
575
554
|
function encodeBufferForServer(buffer) {
|
|
576
555
|
return buffer ? b4a.toString(buffer, 'hex') : undefined
|
|
577
556
|
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* @param {Response} response
|
|
560
|
+
* @returns {Promise<{ serverDeviceId: string }>}
|
|
561
|
+
*/
|
|
562
|
+
async function parseAddServerResponse(response) {
|
|
563
|
+
if (response.status === 200) {
|
|
564
|
+
try {
|
|
565
|
+
const responseBody = await response.json()
|
|
566
|
+
assert(
|
|
567
|
+
responseBody &&
|
|
568
|
+
typeof responseBody === 'object' &&
|
|
569
|
+
'data' in responseBody &&
|
|
570
|
+
responseBody.data &&
|
|
571
|
+
typeof responseBody.data === 'object' &&
|
|
572
|
+
'deviceId' in responseBody.data &&
|
|
573
|
+
typeof responseBody.data.deviceId === 'string',
|
|
574
|
+
'Response body is valid'
|
|
575
|
+
)
|
|
576
|
+
return { serverDeviceId: responseBody.data.deviceId }
|
|
577
|
+
} catch (err) {
|
|
578
|
+
throw new ErrorWithCode(
|
|
579
|
+
'INVALID_SERVER_RESPONSE',
|
|
580
|
+
"Failed to add server peer because we couldn't parse the response"
|
|
581
|
+
)
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
let responseBody
|
|
586
|
+
try {
|
|
587
|
+
responseBody = await response.json()
|
|
588
|
+
} catch (_) {
|
|
589
|
+
responseBody = null
|
|
590
|
+
}
|
|
591
|
+
if (
|
|
592
|
+
responseBody &&
|
|
593
|
+
typeof responseBody === 'object' &&
|
|
594
|
+
'error' in responseBody &&
|
|
595
|
+
responseBody.error &&
|
|
596
|
+
typeof responseBody.error === 'object' &&
|
|
597
|
+
'code' in responseBody.error
|
|
598
|
+
) {
|
|
599
|
+
switch (responseBody.error.code) {
|
|
600
|
+
case 'PROJECT_NOT_IN_ALLOWLIST':
|
|
601
|
+
throw new ErrorWithCode(
|
|
602
|
+
'PROJECT_NOT_IN_SERVER_ALLOWLIST',
|
|
603
|
+
"The server only allows specific projects to be added, and this isn't one of them"
|
|
604
|
+
)
|
|
605
|
+
case 'TOO_MANY_PROJECTS':
|
|
606
|
+
throw new ErrorWithCode(
|
|
607
|
+
'SERVER_HAS_TOO_MANY_PROJECTS',
|
|
608
|
+
"The server limits the number of projects it can have and it's at the limit"
|
|
609
|
+
)
|
|
610
|
+
default:
|
|
611
|
+
break
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
throw new ErrorWithCode(
|
|
616
|
+
'INVALID_SERVER_RESPONSE',
|
|
617
|
+
`Failed to add server peer due to HTTP status code ${response.status}`
|
|
618
|
+
)
|
|
619
|
+
}
|
package/src/roles.js
CHANGED
|
@@ -2,6 +2,7 @@ import { currentSchemaVersions } from '@comapeo/schema'
|
|
|
2
2
|
import mapObject from 'map-obj'
|
|
3
3
|
import { kCreateWithDocId, kDataStore } from './datatype/index.js'
|
|
4
4
|
import { assert, setHas } from './utils.js'
|
|
5
|
+
import { nullIfNotFound } from './errors.js'
|
|
5
6
|
import { TypedEmitter } from 'tiny-typed-emitter'
|
|
6
7
|
/** @import { Namespace } from './types.js' */
|
|
7
8
|
|
|
@@ -98,6 +99,33 @@ export const CREATOR_ROLE = {
|
|
|
98
99
|
},
|
|
99
100
|
}
|
|
100
101
|
|
|
102
|
+
/**
|
|
103
|
+
* @type {Role<typeof BLOCKED_ROLE_ID>}
|
|
104
|
+
*/
|
|
105
|
+
const BLOCKED_ROLE = {
|
|
106
|
+
roleId: BLOCKED_ROLE_ID,
|
|
107
|
+
name: 'Blocked',
|
|
108
|
+
docs: mapObject(currentSchemaVersions, (key) => {
|
|
109
|
+
return [
|
|
110
|
+
key,
|
|
111
|
+
{
|
|
112
|
+
readOwn: false,
|
|
113
|
+
writeOwn: false,
|
|
114
|
+
readOthers: false,
|
|
115
|
+
writeOthers: false,
|
|
116
|
+
},
|
|
117
|
+
]
|
|
118
|
+
}),
|
|
119
|
+
roleAssignment: [],
|
|
120
|
+
sync: {
|
|
121
|
+
auth: 'blocked',
|
|
122
|
+
config: 'blocked',
|
|
123
|
+
data: 'blocked',
|
|
124
|
+
blobIndex: 'blocked',
|
|
125
|
+
blob: 'blocked',
|
|
126
|
+
},
|
|
127
|
+
}
|
|
128
|
+
|
|
101
129
|
/**
|
|
102
130
|
* This is the role assumed for a device when no role record can be found. This
|
|
103
131
|
* can happen when an invited device did not manage to sync with the device that
|
|
@@ -166,29 +194,7 @@ export const ROLES = {
|
|
|
166
194
|
blob: 'allowed',
|
|
167
195
|
},
|
|
168
196
|
},
|
|
169
|
-
[BLOCKED_ROLE_ID]:
|
|
170
|
-
roleId: BLOCKED_ROLE_ID,
|
|
171
|
-
name: 'Blocked',
|
|
172
|
-
docs: mapObject(currentSchemaVersions, (key) => {
|
|
173
|
-
return [
|
|
174
|
-
key,
|
|
175
|
-
{
|
|
176
|
-
readOwn: false,
|
|
177
|
-
writeOwn: false,
|
|
178
|
-
readOthers: false,
|
|
179
|
-
writeOthers: false,
|
|
180
|
-
},
|
|
181
|
-
]
|
|
182
|
-
}),
|
|
183
|
-
roleAssignment: [],
|
|
184
|
-
sync: {
|
|
185
|
-
auth: 'blocked',
|
|
186
|
-
config: 'blocked',
|
|
187
|
-
data: 'blocked',
|
|
188
|
-
blobIndex: 'blocked',
|
|
189
|
-
blob: 'blocked',
|
|
190
|
-
},
|
|
191
|
-
},
|
|
197
|
+
[BLOCKED_ROLE_ID]: BLOCKED_ROLE,
|
|
192
198
|
[LEFT_ROLE_ID]: {
|
|
193
199
|
roleId: LEFT_ROLE_ID,
|
|
194
200
|
name: 'Left',
|
|
@@ -264,12 +270,10 @@ export class Roles extends TypedEmitter {
|
|
|
264
270
|
* @returns {Promise<Role>}
|
|
265
271
|
*/
|
|
266
272
|
async getRole(deviceId) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
roleId = roleAssignment.roleId
|
|
272
|
-
} catch (e) {
|
|
273
|
+
const roleRecord = await this.#dataType
|
|
274
|
+
.getByDocId(deviceId)
|
|
275
|
+
.catch(nullIfNotFound)
|
|
276
|
+
if (!roleRecord) {
|
|
273
277
|
// The project creator will have the creator role
|
|
274
278
|
const authCoreId = await this.#coreOwnership.getCoreId(deviceId, 'auth')
|
|
275
279
|
if (authCoreId === this.#projectCreatorAuthCoreId) {
|
|
@@ -280,8 +284,10 @@ export class Roles extends TypedEmitter {
|
|
|
280
284
|
return NO_ROLE
|
|
281
285
|
}
|
|
282
286
|
}
|
|
287
|
+
|
|
288
|
+
const { roleId } = roleRecord
|
|
283
289
|
if (!isRoleId(roleId)) {
|
|
284
|
-
return
|
|
290
|
+
return BLOCKED_ROLE
|
|
285
291
|
}
|
|
286
292
|
return ROLES[roleId]
|
|
287
293
|
}
|
|
@@ -383,7 +389,7 @@ export class Roles extends TypedEmitter {
|
|
|
383
389
|
|
|
384
390
|
const existingRoleDoc = await this.#dataType
|
|
385
391
|
.getByDocId(deviceId)
|
|
386
|
-
.catch(
|
|
392
|
+
.catch(nullIfNotFound)
|
|
387
393
|
|
|
388
394
|
if (existingRoleDoc) {
|
|
389
395
|
await this.#dataType.update(
|
|
@@ -403,7 +409,7 @@ export class Roles extends TypedEmitter {
|
|
|
403
409
|
}
|
|
404
410
|
}
|
|
405
411
|
|
|
406
|
-
|
|
412
|
+
#isProjectCreator() {
|
|
407
413
|
const ownAuthCoreId = this.#coreManager
|
|
408
414
|
.getWriterCore('auth')
|
|
409
415
|
.key.toString('hex')
|
|
@@ -182,13 +182,23 @@ export class CoreSyncState {
|
|
|
182
182
|
* blocks/ranges that are added here
|
|
183
183
|
*
|
|
184
184
|
* @param {PeerId} peerId
|
|
185
|
-
* @param {
|
|
185
|
+
* @param {number} start
|
|
186
|
+
* @param {number} length
|
|
187
|
+
* @returns {void}
|
|
186
188
|
*/
|
|
187
|
-
|
|
189
|
+
addWantRange(peerId, start, length) {
|
|
188
190
|
const peerState = this.#getOrCreatePeerState(peerId)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
191
|
+
peerState.addWantRange(start, length)
|
|
192
|
+
this.#update()
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* @param {PeerId} peerId
|
|
197
|
+
* @returns {void}
|
|
198
|
+
*/
|
|
199
|
+
clearWantRanges(peerId) {
|
|
200
|
+
const peerState = this.#getOrCreatePeerState(peerId)
|
|
201
|
+
peerState.clearWantRanges()
|
|
192
202
|
this.#update()
|
|
193
203
|
}
|
|
194
204
|
|
|
@@ -291,14 +301,13 @@ export class PeerState {
|
|
|
291
301
|
#preHaves = new RemoteBitfield()
|
|
292
302
|
/** @type {HypercoreRemoteBitfield | undefined} */
|
|
293
303
|
#haves
|
|
294
|
-
/**
|
|
295
|
-
|
|
304
|
+
/**
|
|
305
|
+
* What blocks do we want? If `null`, we want everything.
|
|
306
|
+
* @type {null | Bitfield}
|
|
307
|
+
*/
|
|
308
|
+
#wants = null
|
|
296
309
|
/** @type {PeerNamespaceState['status']} */
|
|
297
310
|
status = 'stopped'
|
|
298
|
-
#wantAll
|
|
299
|
-
constructor({ wantAll = true } = {}) {
|
|
300
|
-
this.#wantAll = wantAll
|
|
301
|
-
}
|
|
302
311
|
get preHavesBitfield() {
|
|
303
312
|
return this.#preHaves
|
|
304
313
|
}
|
|
@@ -316,18 +325,27 @@ export class PeerState {
|
|
|
316
325
|
this.#haves = bitfield
|
|
317
326
|
}
|
|
318
327
|
/**
|
|
319
|
-
*
|
|
328
|
+
* Add a range of blocks that a peer wants. This is not part of the Hypercore
|
|
320
329
|
* protocol, so we need our own extension messages that a peer can use to
|
|
321
330
|
* inform us which blocks they are interested in. For most cores peers always
|
|
322
|
-
* want all blocks, but for blob cores
|
|
331
|
+
* want all blocks, but for blob cores peers may only want preview or
|
|
323
332
|
* thumbnail versions of media
|
|
324
333
|
*
|
|
325
|
-
* @param {
|
|
334
|
+
* @param {number} start
|
|
335
|
+
* @param {number} length
|
|
336
|
+
* @returns {void}
|
|
326
337
|
*/
|
|
327
|
-
|
|
328
|
-
this.#
|
|
338
|
+
addWantRange(start, length) {
|
|
339
|
+
this.#wants ??= new RemoteBitfield()
|
|
329
340
|
this.#wants.setRange(start, length, true)
|
|
330
341
|
}
|
|
342
|
+
/**
|
|
343
|
+
* Set the range of blocks that this peer wants to the empty set. In other
|
|
344
|
+
* words, this peer wants nothing from this core.
|
|
345
|
+
*/
|
|
346
|
+
clearWantRanges() {
|
|
347
|
+
this.#wants = new RemoteBitfield()
|
|
348
|
+
}
|
|
331
349
|
/**
|
|
332
350
|
* Returns whether the peer has the block at `index`. If a pre-have bitfield
|
|
333
351
|
* has been passed, this is used if no connected peer bitfield is available.
|
|
@@ -355,8 +373,7 @@ export class PeerState {
|
|
|
355
373
|
* @param {number} index
|
|
356
374
|
*/
|
|
357
375
|
want(index) {
|
|
358
|
-
|
|
359
|
-
return this.#wants.get(index)
|
|
376
|
+
return this.#wants ? this.#wants.get(index) : true
|
|
360
377
|
}
|
|
361
378
|
/**
|
|
362
379
|
* Return the "wants" for the 32 blocks from `index`, as a 32-bit integer
|
|
@@ -366,11 +383,10 @@ export class PeerState {
|
|
|
366
383
|
* the 32 blocks from `index`
|
|
367
384
|
*/
|
|
368
385
|
wantWord(index) {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
return getBitfieldWord(this.#wants, index)
|
|
386
|
+
return this.#wants
|
|
387
|
+
? getBitfieldWord(this.#wants, index)
|
|
388
|
+
: // This is a 32-bit number with all bits set
|
|
389
|
+
2 ** 32 - 1
|
|
374
390
|
}
|
|
375
391
|
}
|
|
376
392
|
|
|
@@ -136,6 +136,28 @@ export class NamespaceSyncState {
|
|
|
136
136
|
this.#getCoreState(coreDiscoveryId).insertPreHaves(peerId, start, bitfield)
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
/**
|
|
140
|
+
* @param {string} peerId
|
|
141
|
+
* @param {number} start
|
|
142
|
+
* @param {number} length
|
|
143
|
+
* @returns {void}
|
|
144
|
+
*/
|
|
145
|
+
addWantRange(peerId, start, length) {
|
|
146
|
+
for (const coreState of this.#coreStates.values()) {
|
|
147
|
+
coreState.addWantRange(peerId, start, length)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @param {string} peerId
|
|
153
|
+
* @returns {void}
|
|
154
|
+
*/
|
|
155
|
+
clearWantRanges(peerId) {
|
|
156
|
+
for (const coreState of this.#coreStates.values()) {
|
|
157
|
+
coreState.clearWantRanges(peerId)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
139
161
|
/**
|
|
140
162
|
* @param {string} discoveryId
|
|
141
163
|
*/
|