@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
@@ -1,9 +1,13 @@
1
1
  import { sql } from 'drizzle-orm'
2
2
  import fs from 'node:fs'
3
3
  import path from 'node:path'
4
- import { assert } from '../utils.js'
5
4
  import { migrate as drizzleMigrate } from 'drizzle-orm/better-sqlite3/migrator'
6
5
  import { DRIZZLE_MIGRATIONS_TABLE } from '../constants.js'
6
+ import {
7
+ InvalidDrizzleJournalError,
8
+ InvalidDrizzleQueryResultError,
9
+ MigrationError,
10
+ } from '../errors.js'
7
11
  /** @import { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3' */
8
12
 
9
13
  /**
@@ -11,13 +15,16 @@ import { DRIZZLE_MIGRATIONS_TABLE } from '../constants.js'
11
15
  * @returns {number}
12
16
  */
13
17
  const getNumberResult = (queryResult) => {
14
- assert(
15
- queryResult &&
18
+ if (
19
+ !(
20
+ queryResult &&
16
21
  typeof queryResult === 'object' &&
17
22
  'result' in queryResult &&
18
- typeof queryResult.result === 'number',
19
- 'expected query to return proper result'
20
- )
23
+ typeof queryResult.result === 'number'
24
+ )
25
+ ) {
26
+ throw new InvalidDrizzleQueryResultError()
27
+ }
21
28
  return queryResult.result
22
29
  }
23
30
 
@@ -68,37 +75,40 @@ const safeGetLatestMigrationMillis = (db) =>
68
75
  * @returns {MigrationResult}
69
76
  */
70
77
  export function migrate(db, { migrationsFolder, migrationFns = {} }) {
71
- const journal = /** @type {unknown} */ (
72
- JSON.parse(
73
- fs.readFileSync(
74
- path.join(migrationsFolder, 'meta/_journal.json'),
75
- 'utf-8'
78
+ try {
79
+ const journal = /** @type {unknown} */ (
80
+ JSON.parse(
81
+ fs.readFileSync(
82
+ path.join(migrationsFolder, 'meta/_journal.json'),
83
+ 'utf-8'
84
+ )
76
85
  )
77
86
  )
78
- )
79
- // Drizzle _could_ decide to change the journal format in the future, but this
80
- // assertion will ensure that tests fail if they do.
81
- assertValidJournal(journal)
82
-
83
- const prevMigrationMillis = safeGetLatestMigrationMillis(db)
87
+ // Drizzle _could_ decide to change the journal format in the future, but this
88
+ // assertion will ensure that tests fail if they do.
89
+ assertValidJournal(journal)
84
90
 
85
- drizzleMigrate(db, {
86
- migrationsFolder,
87
- migrationsTable: DRIZZLE_MIGRATIONS_TABLE,
88
- })
91
+ const prevMigrationMillis = safeGetLatestMigrationMillis(db)
89
92
 
90
- for (const entry of journal.entries) {
91
- if (entry.when <= prevMigrationMillis) continue
92
- const fn = migrationFns[entry.tag]
93
- if (fn) fn(db)
94
- }
93
+ drizzleMigrate(db, {
94
+ migrationsFolder,
95
+ migrationsTable: DRIZZLE_MIGRATIONS_TABLE,
96
+ })
95
97
 
96
- const lastMigrationMillis = safeGetLatestMigrationMillis(db)
98
+ for (const entry of journal.entries) {
99
+ if (entry.when <= prevMigrationMillis) continue
100
+ const fn = migrationFns[entry.tag]
101
+ if (fn) fn(db)
102
+ }
97
103
 
98
- if (lastMigrationMillis === prevMigrationMillis) return 'no migration'
104
+ const lastMigrationMillis = safeGetLatestMigrationMillis(db)
99
105
 
100
- if (prevMigrationMillis === 0) return 'initialized database'
106
+ if (lastMigrationMillis === prevMigrationMillis) return 'no migration'
101
107
 
108
+ if (prevMigrationMillis === 0) return 'initialized database'
109
+ } catch (e) {
110
+ throw new MigrationError({ cause: e })
111
+ }
102
112
  return 'migrated'
103
113
  }
104
114
 
@@ -108,13 +118,16 @@ export function migrate(db, { migrationsFolder, migrationFns = {} }) {
108
118
  * @returns {asserts journal is { version: '5', entries: { tag: string, when: number }[] }}
109
119
  */
110
120
  function assertValidJournal(journal) {
111
- assert(journal && typeof journal === 'object', 'invalid journal')
112
- assert(
113
- 'version' in journal && journal.version === '5',
114
- 'unexpected journal version'
115
- )
116
- assert(
117
- 'entries' in journal &&
121
+ if (!(journal && typeof journal === 'object')) {
122
+ throw new InvalidDrizzleJournalError()
123
+ }
124
+ if (!('version' in journal && journal.version === '5')) {
125
+ throw new InvalidDrizzleJournalError('unexpected journal version')
126
+ }
127
+
128
+ if (
129
+ !(
130
+ 'entries' in journal &&
118
131
  Array.isArray(journal.entries) &&
119
132
  journal.entries.every(
120
133
  /** @param {unknown} m */
@@ -125,7 +138,9 @@ function assertValidJournal(journal) {
125
138
  typeof m.tag === 'string' &&
126
139
  'when' in m &&
127
140
  typeof m.when === 'number'
128
- ),
129
- 'invalid entries in journal'
130
- )
141
+ )
142
+ )
143
+ ) {
144
+ throw new InvalidDrizzleJournalError('invalid entries in journal')
145
+ }
131
146
  }
@@ -1,11 +1,13 @@
1
- import { assert } from '../utils.js'
1
+ import { MissingDiscoveryKeyError } from '../errors.js'
2
2
 
3
3
  /**
4
4
  * @param {import('hypercore')<'binary', any>} core Core to unreplicate. Must be ready.
5
5
  * @param {import('protomux')} protomux
6
6
  */
7
7
  export function unreplicate(core, protomux) {
8
- assert(core.discoveryKey, 'Core should have a discovery key')
8
+ if (!core.discoveryKey) {
9
+ throw new MissingDiscoveryKeyError()
10
+ }
9
11
  protomux.unpair({
10
12
  protocol: 'hypercore/alpha',
11
13
  id: core.discoveryKey,
package/src/lib/key-by.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { DuplicateKeyError } from '../errors.js'
2
+
1
3
  /**
2
4
  * Like [`Map.groupBy`][0], but the result's values aren't arrays.
3
5
  *
@@ -16,7 +18,7 @@ export function keyBy(items, callbackFn) {
16
18
  for (const item of items) {
17
19
  const key = callbackFn(item)
18
20
  if (result.has(key)) {
19
- throw new Error(`keyBy found duplicate key ${JSON.stringify(key)}`)
21
+ throw new DuplicateKeyError({ key: `${key}` })
20
22
  }
21
23
  result.set(key, item)
22
24
  }
@@ -1,7 +1,7 @@
1
1
  import { TypedEmitter } from 'tiny-typed-emitter'
2
2
  import Protomux from 'protomux'
3
3
  import timingSafeEqual from 'string-timing-safe-equal'
4
- import { assert, ExhaustivenessError, keyToId, noop } from './utils.js'
4
+ import { keyToId, noop, timeoutPromise } from './utils.js'
5
5
  import { isBlank } from './lib/string.js'
6
6
  import cenc from 'compact-encoding'
7
7
  import {
@@ -18,10 +18,15 @@ import {
18
18
  } from './generated/rpc.js'
19
19
  import pDefer from 'p-defer'
20
20
  import { Logger } from './logger.js'
21
- import pTimeout, { TimeoutError } from 'p-timeout'
22
21
  import {
22
+ PeerDisconnectedError,
23
+ PeerFailedConnectionError,
23
24
  RPCDisconnectBeforeAckError,
24
25
  RPCDisconnectBeforeSendingError,
26
+ UnknownPeerError,
27
+ ExhaustivenessError,
28
+ InvalidInviteError,
29
+ InvalidProjectJoinDetailsError,
25
30
  } from './errors.js'
26
31
  /** @import NoiseStream from '@hyperswarm/secret-stream' */
27
32
  /** @import { OpenedNoiseStream } from './lib/noise-secret-stream-helpers.js' */
@@ -157,7 +162,7 @@ class Peer {
157
162
  }
158
163
  /* c8 ignore next 2 */
159
164
  default:
160
- throw new ExhaustivenessError(this.#state)
165
+ throw new ExhaustivenessError({ value: this.#state })
161
166
  }
162
167
  }
163
168
  /**
@@ -280,7 +285,7 @@ class Peer {
280
285
  * @returns {Promise<void>}
281
286
  */
282
287
  async [kTestOnlySendRawInvite](buf) {
283
- this.#assertConnected()
288
+ this.#assertConnected('Peer not connected for test')
284
289
  const messageType = MESSAGE_TYPES.Invite
285
290
  await this.#waitForDrain(this.#channel.messages[messageType].send(buf))
286
291
  }
@@ -408,7 +413,7 @@ class Peer {
408
413
  this.#features = deviceInfo.features
409
414
  this.#log('received deviceInfo %o', deviceInfo)
410
415
  }
411
- /** @param {string} [message] */
416
+ /** @param {string} message */
412
417
  #assertConnected(message) {
413
418
  if (this.#state === 'connected' && !this.#channel.closed) return
414
419
  /* c8 ignore next */
@@ -536,7 +541,6 @@ export class LocalPeers extends TypedEmitter {
536
541
  */
537
542
  connect(stream) {
538
543
  const noiseStream = stream.noiseStream
539
- if (!noiseStream) throw new Error('Invalid stream')
540
544
  const outerStream = noiseStream.rawStream
541
545
  const protomux =
542
546
  noiseStream.userData && Protomux.isProtomux(noiseStream.userData)
@@ -617,8 +621,8 @@ export class LocalPeers extends TypedEmitter {
617
621
  /** @type {keyof typeof MESSAGE_TYPES} */ (type),
618
622
  message
619
623
  )
620
- } catch (err) {
621
- const errorMessage = String(err)
624
+ } catch (e) {
625
+ const errorMessage = String(e)
622
626
  this.emit('failed-to-handle-message', type, errorMessage)
623
627
  this.#l.log(`Error handling ${type} message: ${errorMessage}`)
624
628
  }
@@ -817,7 +821,9 @@ export class LocalPeers extends TypedEmitter {
817
821
  * Wait for any connections that are currently opening
818
822
  */
819
823
  #waitForPendingConnections() {
820
- return pTimeout(Promise.all(this.#opening), { milliseconds: SEND_TIMEOUT })
824
+ return timeoutPromise(Promise.all(this.#opening), {
825
+ milliseconds: SEND_TIMEOUT,
826
+ })
821
827
  }
822
828
 
823
829
  /**
@@ -830,14 +836,14 @@ export class LocalPeers extends TypedEmitter {
830
836
  async #getPeerByDeviceId(deviceId) {
831
837
  const devicePeers = this.#peers.get(deviceId)
832
838
  if (!devicePeers || devicePeers.size === 0) {
833
- throw new UnknownPeerError('Unknown peer ' + deviceId.slice(0, 7))
839
+ throw new UnknownPeerError({ deviceId: deviceId.slice(0, 7) })
834
840
  }
835
841
  const peer = chooseDevicePeer(devicePeers)
836
842
  if (peer) return peer
837
843
  return new Promise((resolve, reject) => {
838
844
  const timeoutId = setTimeout(() => {
839
845
  this.off('peers', onPeers)
840
- reject(new UnknownPeerError('Unknown peer ' + deviceId.slice(0, 7)))
846
+ reject(new UnknownPeerError({ deviceId: deviceId.slice(0, 7) }))
841
847
  }, DEDUPE_TIMEOUT)
842
848
 
843
849
  const onPeers = () => {
@@ -854,38 +860,14 @@ export class LocalPeers extends TypedEmitter {
854
860
  }
855
861
  }
856
862
 
857
- export { TimeoutError }
858
-
859
- export class UnknownPeerError extends Error {
860
- /** @param {string} [message] */
861
- constructor(message = 'UnknownPeerError') {
862
- super(message)
863
- this.name = 'UnknownPeerError'
864
- }
865
- }
866
-
867
- export class PeerDisconnectedError extends Error {
868
- /** @param {string} [message] */
869
- constructor(message = 'Peer disconnected') {
870
- super(message)
871
- this.name = 'PeerDisconnectedError'
872
- }
873
- }
874
-
875
- export class PeerFailedConnectionError extends Error {
876
- /** @param {string} [message] */
877
- constructor(message = 'PeerFailedConnectionError') {
878
- super(message)
879
- this.name = 'PeerFailedConnectionError'
880
- }
881
- }
882
-
883
863
  /**
884
864
  * @param {Readonly<Uint8Array>} id
885
865
  * @throws if the invite ID is too short
886
866
  */
887
867
  function assertInviteIdIsValid(id) {
888
- assert(id.byteLength >= 32, 'Invite ID must be >= 32 bytes')
868
+ if (id.byteLength < 32) {
869
+ throw new InvalidInviteError('Invite ID must be >= 32 bytes')
870
+ }
889
871
  }
890
872
 
891
873
  /**
@@ -896,9 +878,15 @@ function assertInviteIdIsValid(id) {
896
878
  function parseInvite(data) {
897
879
  const result = Invite.decode(data)
898
880
  assertInviteIdIsValid(result.inviteId)
899
- assert(result.projectInviteId.length, 'Invite must have project invite ID')
900
- assert(!isBlank(result.projectName), 'Invite project name cannot be blank')
901
- assert(!isBlank(result.invitorName), 'Invite invitor name cannot be blank')
881
+ if (!result.projectInviteId.length) {
882
+ throw new InvalidInviteError('Invite must have project invite ID')
883
+ }
884
+ if (isBlank(result.projectName)) {
885
+ throw new InvalidInviteError('Invite project name cannot be blank')
886
+ }
887
+ if (isBlank(result.invitorName)) {
888
+ throw new InvalidInviteError('Invite invitor name cannot be blank')
889
+ }
902
890
  return result
903
891
  }
904
892
 
@@ -932,11 +920,16 @@ function parseInviteResponse(data) {
932
920
  function parseProjectJoinDetails(data) {
933
921
  const result = ProjectJoinDetails.decode(data)
934
922
  assertInviteIdIsValid(result.inviteId)
935
- assert(result.projectKey.length, 'Project join details must have project key')
936
- assert(
937
- result.encryptionKeys?.auth?.byteLength,
938
- 'Project join details must have auth encryption keys'
939
- )
923
+ if (!result.projectKey.length) {
924
+ throw new InvalidProjectJoinDetailsError(
925
+ 'Project join details must have project key'
926
+ )
927
+ }
928
+ if (!result.encryptionKeys?.auth?.byteLength) {
929
+ throw new InvalidProjectJoinDetailsError(
930
+ 'Project join details must have auth encryption keys'
931
+ )
932
+ }
940
933
  return result
941
934
  }
942
935
 
package/src/logger.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import createDebug from 'debug'
2
+ import ensureError from 'ensure-error'
2
3
  import { discoveryKey } from 'hypercore-crypto'
3
4
  import mapObject from 'map-obj'
4
5
  import util from 'util'
@@ -69,7 +70,7 @@ createDebug.formatters.X = function (v) {
69
70
  breakLength: 90,
70
71
  })
71
72
  } catch (e) {
72
- return `[ERROR: $(e.message)]`
73
+ return `[ERROR: ${ensureError(e).message}]`
73
74
  }
74
75
  }
75
76
 
@@ -6,7 +6,6 @@ import { eq, and } from 'drizzle-orm'
6
6
  import { drizzle } from 'drizzle-orm/better-sqlite3'
7
7
  import Hypercore from 'hypercore'
8
8
  import { TypedEmitter } from 'tiny-typed-emitter'
9
- import pTimeout from 'p-timeout'
10
9
  import { createRequire } from 'module'
11
10
 
12
11
  import { IndexWriter } from './index-writer/index.js'
@@ -36,6 +35,7 @@ import {
36
35
  projectKeyToId,
37
36
  projectKeyToProjectInviteId,
38
37
  projectKeyToPublicId,
38
+ timeoutPromise,
39
39
  } from './utils.js'
40
40
  import { openedNoiseSecretStream } from './lib/noise-secret-stream-helpers.js'
41
41
  import { omit } from './lib/omit.js'
@@ -49,7 +49,14 @@ import { InviteApi } from './invite/invite-api.js'
49
49
  import { LocalDiscovery } from './discovery/local-discovery.js'
50
50
  import { Logger } from './logger.js'
51
51
  import { kRequestFullStop, kRescindFullStopRequest } from './sync/sync-api.js'
52
- import { NotFoundError } from './errors.js'
52
+ import {
53
+ EncryptionKeysNotFoundError,
54
+ ensureKnownError,
55
+ ExhaustivenessError,
56
+ FailedToSetIsArchiveDeviceError,
57
+ NotFoundError,
58
+ ProjectExistsError,
59
+ } from './errors.js'
53
60
  import { WebSocket } from 'ws'
54
61
  import { excludeKeys } from 'filter-obj'
55
62
  import { migrate } from './lib/drizzle-helpers.js'
@@ -63,7 +70,7 @@ import { migrate } from './lib/drizzle-helpers.js'
63
70
  /** @import { ProjectSettings, ProjectSettingsValue } from '@comapeo/schema' */
64
71
 
65
72
  /** @typedef {SetNonNullable<ProjectKeys, 'encryptionKeys'>} ValidatedProjectKeys */
66
- /** @typedef {Pick<ProjectJoinDetails, 'projectKey' | 'encryptionKeys'> & { projectName: string, projectColor?: string, projectDescription?: string, sendStats?: boolean }} ProjectToAddDetails */
73
+ /** @typedef {Pick<ProjectJoinDetails, 'projectKey' | 'encryptionKeys'> & { projectName: string, projectColor?: string, projectDescription?: string, sendStats?: boolean, invitorWroteDeviceInfo? : boolean }} ProjectToAddDetails */
67
74
  /** @typedef {Pick<ProjectSettings, 'createdAt' | 'updatedAt' | 'name' | 'projectColor' | 'projectDescription' | 'sendStats'>} ListedProjectSettings */
68
75
  /** @typedef {ListedProjectSettings & { status: 'joined', projectId: string } | ProjectInfo & { status: 'joining' | 'left', projectId: string }} ListedProject */
69
76
 
@@ -277,7 +284,7 @@ export class MapeoManager extends TypedEmitter {
277
284
  break
278
285
  }
279
286
  default: {
280
- throw new Error(`Unsupported media type ${mediaType}`)
287
+ throw new ExhaustivenessError({ value: mediaType })
281
288
  }
282
289
  }
283
290
 
@@ -587,11 +594,11 @@ export class MapeoManager extends TypedEmitter {
587
594
 
588
595
  /**
589
596
  *
590
- * @param {Error} err
597
+ * @param {Error} e
591
598
  * @param {MapShareExtension} mapShareExtension
592
599
  */
593
- const onMapShareError = (err, mapShareExtension) => {
594
- this.emit('map-share-error', err, mapShareExtension)
600
+ const onMapShareError = (e, mapShareExtension) => {
601
+ this.emit('map-share-error', e, mapShareExtension)
595
602
  }
596
603
 
597
604
  project.once('close', () => {
@@ -696,6 +703,7 @@ export class MapeoManager extends TypedEmitter {
696
703
  projectColor,
697
704
  projectDescription,
698
705
  sendStats = false,
706
+ invitorWroteDeviceInfo = false,
699
707
  },
700
708
  { waitForSync = true } = {}
701
709
  ) => {
@@ -715,7 +723,7 @@ export class MapeoManager extends TypedEmitter {
715
723
  .get()
716
724
 
717
725
  if (existingProject && existingProject.hasLeftProject !== true) {
718
- throw new Error(`Project with ID ${projectPublicId} already exists`)
726
+ throw new ProjectExistsError({ projectPublicId })
719
727
  }
720
728
 
721
729
  // 2. Check for an active project
@@ -770,7 +778,7 @@ export class MapeoManager extends TypedEmitter {
770
778
  .delete(projectKeysTable)
771
779
  .where(eq(projectKeysTable.projectId, projectId))
772
780
  .run()
773
- throw e
781
+ throw ensureKnownError(e)
774
782
  }
775
783
 
776
784
  // Make sure to clean up when closed
@@ -778,18 +786,21 @@ export class MapeoManager extends TypedEmitter {
778
786
  this.#activeProjects.delete(projectPublicId)
779
787
  })
780
788
 
781
- try {
782
- const deviceInfo = this.getDeviceInfo()
783
- if (hasSavedDeviceInfo(deviceInfo)) {
784
- await project[kSetOwnDeviceInfo](deviceInfo)
789
+ // Only write info on invite if configured
790
+ if (!invitorWroteDeviceInfo) {
791
+ try {
792
+ const deviceInfo = this.getDeviceInfo()
793
+ if (hasSavedDeviceInfo(deviceInfo)) {
794
+ await project[kSetOwnDeviceInfo](deviceInfo)
795
+ }
796
+ } catch (e) {
797
+ // Can ignore an error trying to write device info
798
+ this.#l.log(
799
+ 'ERROR: failed to write project %h deviceInfo %o',
800
+ projectKey,
801
+ e
802
+ )
785
803
  }
786
- } catch (e) {
787
- // Can ignore an error trying to write device info
788
- this.#l.log(
789
- 'ERROR: failed to write project %h deviceInfo %o',
790
- projectKey,
791
- e
792
- )
793
804
  }
794
805
 
795
806
  // 5. Wait for initial project sync
@@ -899,7 +910,7 @@ export class MapeoManager extends TypedEmitter {
899
910
  })
900
911
  .run()
901
912
  if (!result || result.changes === 0) {
902
- throw new Error('Failed to set isArchiveDevice')
913
+ throw new FailedToSetIsArchiveDeviceError()
903
914
  }
904
915
  for (const project of this.#activeProjects.values()) {
905
916
  project[kSetIsArchiveDevice](isArchiveDevice)
@@ -1036,7 +1047,9 @@ export class MapeoManager extends TypedEmitter {
1036
1047
  }
1037
1048
 
1038
1049
  async getMapStyleJsonUrl() {
1039
- await pTimeout(this.#fastify.ready(), { milliseconds: 1000 })
1050
+ await timeoutPromise(Promise.resolve(this.#fastify.ready()), {
1051
+ milliseconds: 1000,
1052
+ })
1040
1053
  return (await this.#getMediaBaseUrl('maps')) + '/style.json'
1041
1054
  }
1042
1055
 
@@ -1078,7 +1091,7 @@ function omitPeerProtomux(peers) {
1078
1091
  */
1079
1092
  function validateProjectKeys(projectKeys) {
1080
1093
  if (!projectKeys.encryptionKeys) {
1081
- throw new Error('encryptionKeys should not be undefined')
1094
+ throw new EncryptionKeysNotFoundError()
1082
1095
  }
1083
1096
  }
1084
1097