@comapeo/core 5.5.0 → 6.0.1

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 (94) hide show
  1. package/dist/blob-api.d.ts.map +1 -1
  2. package/dist/blob-store/downloader.d.ts.map +1 -1
  3. package/dist/blob-store/hyperdrive-index.d.ts.map +1 -1
  4. package/dist/blob-store/index.d.ts.map +1 -1
  5. package/dist/core-manager/bitfield-rle.d.ts.map +1 -1
  6. package/dist/core-manager/core-index.d.ts.map +1 -1
  7. package/dist/core-manager/index.d.ts.map +1 -1
  8. package/dist/core-ownership.d.ts.map +1 -1
  9. package/dist/datastore/index.d.ts.map +1 -1
  10. package/dist/datatype/index.d.ts +7 -0
  11. package/dist/datatype/index.d.ts.map +1 -1
  12. package/dist/discovery/local-discovery.d.ts.map +1 -1
  13. package/dist/errors.d.ts +437 -35
  14. package/dist/errors.d.ts.map +1 -1
  15. package/dist/fastify-plugins/blobs.d.ts.map +1 -1
  16. package/dist/fastify-plugins/icons.d.ts.map +1 -1
  17. package/dist/fastify-plugins/maps.d.ts.map +1 -1
  18. package/dist/generated/rpc.d.ts +1 -0
  19. package/dist/generated/rpc.d.ts.map +1 -1
  20. package/dist/icon-api.d.ts +0 -1
  21. package/dist/icon-api.d.ts.map +1 -1
  22. package/dist/import-categories.d.ts.map +1 -1
  23. package/dist/index-writer/index.d.ts.map +1 -1
  24. package/dist/index.d.ts +1 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/intl/parse-bcp-47.d.ts.map +1 -1
  27. package/dist/invite/invite-api.d.ts.map +1 -1
  28. package/dist/lib/drizzle-helpers.d.ts.map +1 -1
  29. package/dist/lib/hypercore-helpers.d.ts.map +1 -1
  30. package/dist/lib/key-by.d.ts.map +1 -1
  31. package/dist/local-peers.d.ts +0 -14
  32. package/dist/local-peers.d.ts.map +1 -1
  33. package/dist/logger.d.ts.map +1 -1
  34. package/dist/mapeo-manager.d.ts +2 -1
  35. package/dist/mapeo-manager.d.ts.map +1 -1
  36. package/dist/mapeo-project.d.ts +1 -3
  37. package/dist/mapeo-project.d.ts.map +1 -1
  38. package/dist/member-api.d.ts +42 -7
  39. package/dist/member-api.d.ts.map +1 -1
  40. package/dist/roles.d.ts.map +1 -1
  41. package/dist/schema/json-schema-to-drizzle.d.ts.map +1 -1
  42. package/dist/schema.d.ts +2 -0
  43. package/dist/schema.d.ts.map +1 -0
  44. package/dist/sync/core-sync-state.d.ts.map +1 -1
  45. package/dist/sync/peer-sync-controller.d.ts.map +1 -1
  46. package/dist/sync/sync-api.d.ts.map +1 -1
  47. package/dist/utils.d.ts +8 -10
  48. package/dist/utils.d.ts.map +1 -1
  49. package/package.json +19 -3
  50. package/src/blob-api.js +24 -4
  51. package/src/blob-store/downloader.js +7 -6
  52. package/src/blob-store/entries-stream.js +1 -1
  53. package/src/blob-store/hyperdrive-index.js +3 -5
  54. package/src/blob-store/index.js +15 -20
  55. package/src/core-manager/bitfield-rle.js +2 -1
  56. package/src/core-manager/core-index.js +2 -1
  57. package/src/core-manager/index.js +9 -10
  58. package/src/core-ownership.js +7 -3
  59. package/src/datastore/index.js +13 -9
  60. package/src/datatype/index.js +28 -5
  61. package/src/discovery/local-discovery.js +8 -7
  62. package/src/errors.js +530 -62
  63. package/src/fastify-controller.js +3 -3
  64. package/src/fastify-plugins/blobs.js +21 -14
  65. package/src/fastify-plugins/icons.js +18 -9
  66. package/src/fastify-plugins/maps.js +6 -5
  67. package/src/generated/rpc.d.ts +1 -0
  68. package/src/generated/rpc.js +12 -1
  69. package/src/generated/rpc.ts +13 -0
  70. package/src/icon-api.js +15 -7
  71. package/src/import-categories.js +6 -7
  72. package/src/index-writer/index.js +3 -2
  73. package/src/index.js +1 -0
  74. package/src/intl/parse-bcp-47.js +2 -1
  75. package/src/invite/invite-api.js +26 -20
  76. package/src/lib/drizzle-helpers.js +54 -39
  77. package/src/lib/hypercore-helpers.js +4 -2
  78. package/src/lib/key-by.js +3 -1
  79. package/src/local-peers.js +39 -46
  80. package/src/logger.js +2 -1
  81. package/src/mapeo-manager.js +36 -23
  82. package/src/mapeo-project.js +68 -61
  83. package/src/member-api.js +177 -96
  84. package/src/roles.js +11 -10
  85. package/src/schema/json-schema-to-drizzle.js +13 -4
  86. package/src/schema.js +1 -0
  87. package/src/sync/core-sync-state.js +2 -1
  88. package/src/sync/peer-sync-controller.js +4 -3
  89. package/src/sync/sync-api.js +9 -9
  90. package/src/translation-api.js +2 -2
  91. package/src/utils.js +56 -41
  92. package/dist/lib/error.d.ts +0 -51
  93. package/dist/lib/error.d.ts.map +0 -1
  94. package/src/lib/error.js +0 -71
@@ -3,7 +3,6 @@ import Database from 'better-sqlite3'
3
3
  import { decodeBlockPrefix, decode, parseVersionId } from '@comapeo/schema'
4
4
  import { drizzle } from 'drizzle-orm/better-sqlite3'
5
5
  import { sql, count, eq } from 'drizzle-orm'
6
- import { discoveryKey } from 'hypercore-crypto'
7
6
  import { TypedEmitter } from 'tiny-typed-emitter'
8
7
  import ZipArchive from 'zip-stream-promise'
9
8
  import * as b4a from 'b4a'
@@ -14,7 +13,11 @@ import { Readable, pipelinePromise } from 'streamx'
14
13
  import { NAMESPACES, NAMESPACE_SCHEMAS } from './constants.js'
15
14
  import { CoreManager } from './core-manager/index.js'
16
15
  import { DataStore } from './datastore/index.js'
17
- import { DataType, kCreateWithDocId } from './datatype/index.js'
16
+ import {
17
+ DataType,
18
+ kCreateOrUpdateWithDocId,
19
+ kCreateWithDocId,
20
+ } from './datatype/index.js'
18
21
  import { BlobStore } from './blob-store/index.js'
19
22
  import { BlobApi } from './blob-api.js'
20
23
  import { IndexWriter } from './index-writer/index.js'
@@ -43,9 +46,7 @@ import {
43
46
  INACTIVE_MEMBER_ROLE_IDS,
44
47
  } from './roles.js'
45
48
  import {
46
- assert,
47
49
  buildBlobId,
48
- ExhaustivenessError,
49
50
  getDeviceId,
50
51
  projectKeyToId,
51
52
  projectKeyToPublicId,
@@ -64,9 +65,21 @@ import { Logger } from './logger.js'
64
65
  import { IconApi } from './icon-api.js'
65
66
  import { importCategories } from './import-categories.js'
66
67
  import TranslationApi from './translation-api.js'
67
- import { NotFoundError, nullIfNotFound } from './errors.js'
68
+ import {
69
+ CategoryFileNotFoundError,
70
+ ensureKnownError,
71
+ getErrorCode,
72
+ NotFoundError,
73
+ ExhaustivenessError,
74
+ nullIfNotFound,
75
+ GeoJSONExportError,
76
+ InvalidMapShareError,
77
+ MultipleCategoryImportsError,
78
+ UnexpectedDocSchemaError,
79
+ } from './errors.js'
68
80
  import { WebSocket } from 'ws'
69
- import { createWriteStream } from 'fs'
81
+ import fs from 'node:fs'
82
+
70
83
  import ensureError from 'ensure-error'
71
84
  /** @import { MapShareExtension } from './generated/extensions.js' */
72
85
  /** @import { ProjectSettingsValue, Observation, Track } from '@comapeo/schema' */
@@ -235,7 +248,7 @@ export class MapeoProject extends TypedEmitter {
235
248
  reindex = true
236
249
  break
237
250
  default:
238
- throw new ExhaustivenessError(migrationResult)
251
+ throw new ExhaustivenessError({ value: migrationResult })
239
252
  }
240
253
 
241
254
  const indexedTables = [
@@ -292,8 +305,6 @@ export class MapeoProject extends TypedEmitter {
292
305
  switch (doc.schemaName) {
293
306
  case 'coreOwnership':
294
307
  return mapAndValidateCoreOwnership(doc, version)
295
- case 'deviceInfo':
296
- return mapAndValidateDeviceInfo(doc, version)
297
308
  default:
298
309
  return doc
299
310
  }
@@ -449,12 +460,12 @@ export class MapeoProject extends TypedEmitter {
449
460
  logger: this.#l,
450
461
  })
451
462
 
452
- this.#blobStore.on('error', (err) => {
463
+ this.#blobStore.on('error', (e) => {
453
464
  // Ignore hypercore inflight request cancellation
454
- if (ensureError(err).message.includes('REQUEST_CANCELLED')) return
465
+ if (ensureError(e).message.includes('REQUEST_CANCELLED')) return
455
466
  // TODO: Handle this error in some way - this error will come from an
456
467
  // unexpected error with background blob downloads
457
- console.error('BlobStore error', err)
468
+ console.error('BlobStore error', e)
458
469
  })
459
470
 
460
471
  this.$blobs = new BlobApi({
@@ -658,7 +669,13 @@ export class MapeoProject extends TypedEmitter {
658
669
  index: entry.index,
659
670
  })
660
671
 
661
- assert(doc.schemaName === 'translation', 'expected a translation doc')
672
+ if (doc.schemaName !== 'translation') {
673
+ throw new UnexpectedDocSchemaError({
674
+ gotSchema: doc.schemaName,
675
+ expectedSchema: 'translation',
676
+ })
677
+ }
678
+
662
679
  this.#translationApi.index(doc)
663
680
  otherEntries.push(entry)
664
681
  } else {
@@ -744,7 +761,7 @@ export class MapeoProject extends TypedEmitter {
744
761
  return extractEditableProjectSettings(
745
762
  await this.#dataTypes.projectSettings.getByDocId(this.#projectId)
746
763
  )
747
- } catch (e) {
764
+ } catch {
748
765
  // if (e instanceof Error && e.name !== 'NotFoundError') throw e
749
766
  // If the project has not completed an initial sync, project settings will
750
767
  // not be available, so use fallback project info which is set from the
@@ -762,7 +779,7 @@ export class MapeoProject extends TypedEmitter {
762
779
  // Should error if we haven't synced before
763
780
  await this.#dataTypes.projectSettings.getByDocId(this.#projectId)
764
781
  return true
765
- } catch (e) {
782
+ } catch {
766
783
  return false
767
784
  }
768
785
  }
@@ -800,7 +817,7 @@ export class MapeoProject extends TypedEmitter {
800
817
  const sender = await this.$member.getById(senderDeviceId)
801
818
 
802
819
  if (INACTIVE_MEMBER_ROLE_IDS.includes(sender.role.roleId)) {
803
- throw new Error(
820
+ throw new InvalidMapShareError(
804
821
  `Map Share Sender is not an active member of the project (role: ${sender.role.name})`
805
822
  )
806
823
  }
@@ -875,15 +892,10 @@ export class MapeoProject extends TypedEmitter {
875
892
 
876
893
  /**
877
894
  * @param {Pick<import('@comapeo/schema').DeviceInfoValue, 'name' | 'deviceType' | 'selfHostedServerDetails'>} value
878
- * @returns {Promise<import('@comapeo/schema').DeviceInfo>}
879
895
  */
880
896
  async [kSetOwnDeviceInfo](value) {
881
897
  const { deviceInfo } = this.#dataTypes
882
898
 
883
- const configCoreId = this.#coreManager
884
- .getWriterCore('config')
885
- .key.toString('hex')
886
-
887
899
  const doc = {
888
900
  name: value.name,
889
901
  deviceType: value.deviceType,
@@ -891,13 +903,15 @@ export class MapeoProject extends TypedEmitter {
891
903
  schemaName: /** @type {const} */ ('deviceInfo'),
892
904
  }
893
905
 
894
- const existingDoc = await deviceInfo
895
- .getByDocId(configCoreId)
896
- .catch(nullIfNotFound)
897
- if (existingDoc) {
898
- return await deviceInfo.update(existingDoc.versionId, doc)
899
- } else {
900
- return await deviceInfo[kCreateWithDocId](configCoreId, doc)
906
+ // TODO: Remove configCore once we know everyone has deviceId
907
+ const configCoreId = this.#coreManager
908
+ .getWriterCore('config')
909
+ .key.toString('hex')
910
+
911
+ const docIds = [this.deviceId, configCoreId]
912
+
913
+ for (const docId of docIds) {
914
+ await deviceInfo[kCreateOrUpdateWithDocId](docId, doc)
901
915
  }
902
916
  }
903
917
 
@@ -1205,10 +1219,16 @@ export class MapeoProject extends TypedEmitter {
1205
1219
  const fileName = await this[kGeoJSONFileName](observations, tracks)
1206
1220
  const filePath = path.join(exportFolder, fileName)
1207
1221
  const source = this.#exportGeoJSONStream({ observations, tracks, lang })
1208
- const sink = createWriteStream(filePath)
1209
- await pipelinePromise(source, sink)
1210
1222
 
1211
- return filePath
1223
+ const sink = fs.createWriteStream(filePath)
1224
+
1225
+ try {
1226
+ await pipelinePromise(source, sink)
1227
+
1228
+ return filePath
1229
+ } catch (e) {
1230
+ throw new GeoJSONExportError({ cause: e })
1231
+ }
1212
1232
  }
1213
1233
 
1214
1234
  /**
@@ -1242,12 +1262,11 @@ export class MapeoProject extends TypedEmitter {
1242
1262
  }
1243
1263
  return { blobId, mimeType }
1244
1264
  } catch (e) {
1245
- if (!(e instanceof Error)) throw e
1246
1265
  this.#l.log(
1247
1266
  'Error loading blob id for attachment',
1248
1267
  attachment,
1249
1268
  variant,
1250
- e.message
1269
+ ensureError(e).message
1251
1270
  )
1252
1271
  continue
1253
1272
  }
@@ -1350,7 +1369,7 @@ export class MapeoProject extends TypedEmitter {
1350
1369
  tracks,
1351
1370
  attachments,
1352
1371
  lang,
1353
- }).catch((e) => archive.emit('error', e))
1372
+ }).catch((e) => archive.emit('error', ensureError(e)))
1354
1373
 
1355
1374
  // @ts-expect-error
1356
1375
  return archive
@@ -1378,10 +1397,14 @@ export class MapeoProject extends TypedEmitter {
1378
1397
  attachments,
1379
1398
  lang,
1380
1399
  })
1381
- const sink = createWriteStream(filePath)
1382
- await pipelinePromise(source, sink)
1400
+ const sink = fs.createWriteStream(filePath)
1401
+ try {
1402
+ await pipelinePromise(source, sink)
1383
1403
 
1384
- return filePath
1404
+ return filePath
1405
+ } catch (e) {
1406
+ throw new GeoJSONExportError({ cause: e })
1407
+ }
1385
1408
  }
1386
1409
 
1387
1410
  async [kProjectLeave]() {
@@ -1450,17 +1473,19 @@ export class MapeoProject extends TypedEmitter {
1450
1473
  * @returns {Promise<void>}
1451
1474
  */
1452
1475
  async $importCategories({ filePath }) {
1453
- assert(
1454
- !this.#importingCategories,
1455
- 'Cannot run multiple category imports at the same time'
1456
- )
1476
+ if (this.#importingCategories) {
1477
+ throw new MultipleCategoryImportsError()
1478
+ }
1457
1479
  this.#importingCategories = true
1458
1480
 
1459
1481
  try {
1460
1482
  await importCategories(this, { filePath, logger: this.#l })
1461
1483
  } catch (e) {
1462
- this.#l.log('error loading config', e)
1463
- throw e
1484
+ if (getErrorCode(e) === 'ENOENT') {
1485
+ throw new CategoryFileNotFoundError({ filePath })
1486
+ }
1487
+ this.#l.log('ERROR: could not load config', e)
1488
+ throw ensureKnownError(e)
1464
1489
  } finally {
1465
1490
  this.#importingCategories = false
1466
1491
  }
@@ -1525,24 +1550,6 @@ function getCoreKeypairs({ projectKey, projectSecretKey, keyManager }) {
1525
1550
  }
1526
1551
 
1527
1552
  /**
1528
- * Validate that a deviceInfo record is written by the device that is it about,
1529
- * e.g. version.coreKey should equal docId
1530
- *
1531
- * @param {import('@comapeo/schema').DeviceInfo} doc
1532
- * @param {import('@comapeo/schema').VersionIdObject} version
1533
- * @returns {import('@comapeo/schema').DeviceInfo}
1534
- */
1535
- function mapAndValidateDeviceInfo(doc, { coreDiscoveryKey }) {
1536
- if (!coreDiscoveryKey.equals(discoveryKey(Buffer.from(doc.docId, 'hex')))) {
1537
- throw new Error(
1538
- 'Invalid deviceInfo record, cannot write deviceInfo for another device'
1539
- )
1540
- }
1541
- return doc
1542
- }
1543
-
1544
- /**
1545
- *
1546
1553
  * @param {string} baseUrl
1547
1554
  * @param {string} projectPublicId
1548
1555
  * @returns {string}