@comapeo/core 2.0.1 → 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.
Files changed (114) hide show
  1. package/dist/blob-store/downloader.d.ts +43 -0
  2. package/dist/blob-store/downloader.d.ts.map +1 -0
  3. package/dist/blob-store/entries-stream.d.ts +13 -0
  4. package/dist/blob-store/entries-stream.d.ts.map +1 -0
  5. package/dist/blob-store/hyperdrive-index.d.ts +20 -0
  6. package/dist/blob-store/hyperdrive-index.d.ts.map +1 -0
  7. package/dist/blob-store/index.d.ts +34 -29
  8. package/dist/blob-store/index.d.ts.map +1 -1
  9. package/dist/blob-store/utils.d.ts +27 -0
  10. package/dist/blob-store/utils.d.ts.map +1 -0
  11. package/dist/constants.d.ts +2 -1
  12. package/dist/constants.d.ts.map +1 -1
  13. package/dist/core-manager/index.d.ts +11 -1
  14. package/dist/core-manager/index.d.ts.map +1 -1
  15. package/dist/core-ownership.d.ts.map +1 -1
  16. package/dist/datastore/index.d.ts +5 -4
  17. package/dist/datastore/index.d.ts.map +1 -1
  18. package/dist/datatype/index.d.ts +5 -1
  19. package/dist/discovery/local-discovery.d.ts.map +1 -1
  20. package/dist/errors.d.ts +6 -1
  21. package/dist/errors.d.ts.map +1 -1
  22. package/dist/fastify-plugins/blobs.d.ts.map +1 -1
  23. package/dist/fastify-plugins/maps.d.ts.map +1 -1
  24. package/dist/generated/extensions.d.ts +31 -0
  25. package/dist/generated/extensions.d.ts.map +1 -1
  26. package/dist/index.d.ts +2 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/lib/drizzle-helpers.d.ts +6 -0
  29. package/dist/lib/drizzle-helpers.d.ts.map +1 -0
  30. package/dist/lib/error.d.ts +51 -0
  31. package/dist/lib/error.d.ts.map +1 -0
  32. package/dist/lib/get-own.d.ts +9 -0
  33. package/dist/lib/get-own.d.ts.map +1 -0
  34. package/dist/lib/is-hostname-ip-address.d.ts +17 -0
  35. package/dist/lib/is-hostname-ip-address.d.ts.map +1 -0
  36. package/dist/lib/ws-core-replicator.d.ts +11 -0
  37. package/dist/lib/ws-core-replicator.d.ts.map +1 -0
  38. package/dist/mapeo-manager.d.ts +18 -22
  39. package/dist/mapeo-manager.d.ts.map +1 -1
  40. package/dist/mapeo-project.d.ts +459 -26
  41. package/dist/mapeo-project.d.ts.map +1 -1
  42. package/dist/member-api.d.ts +44 -1
  43. package/dist/member-api.d.ts.map +1 -1
  44. package/dist/roles.d.ts.map +1 -1
  45. package/dist/schema/client.d.ts +17 -5
  46. package/dist/schema/client.d.ts.map +1 -1
  47. package/dist/schema/project.d.ts +212 -2
  48. package/dist/schema/project.d.ts.map +1 -1
  49. package/dist/sync/core-sync-state.d.ts +20 -15
  50. package/dist/sync/core-sync-state.d.ts.map +1 -1
  51. package/dist/sync/namespace-sync-state.d.ts +13 -1
  52. package/dist/sync/namespace-sync-state.d.ts.map +1 -1
  53. package/dist/sync/peer-sync-controller.d.ts +1 -1
  54. package/dist/sync/peer-sync-controller.d.ts.map +1 -1
  55. package/dist/sync/sync-api.d.ts +47 -2
  56. package/dist/sync/sync-api.d.ts.map +1 -1
  57. package/dist/sync/sync-state.d.ts +12 -0
  58. package/dist/sync/sync-state.d.ts.map +1 -1
  59. package/dist/translation-api.d.ts +2 -2
  60. package/dist/translation-api.d.ts.map +1 -1
  61. package/dist/types.d.ts +10 -2
  62. package/dist/types.d.ts.map +1 -1
  63. package/drizzle/client/0001_chubby_cargill.sql +12 -0
  64. package/drizzle/client/meta/0001_snapshot.json +208 -0
  65. package/drizzle/client/meta/_journal.json +7 -0
  66. package/drizzle/project/0001_medical_wendell_rand.sql +22 -0
  67. package/drizzle/project/meta/0001_snapshot.json +1267 -0
  68. package/drizzle/project/meta/_journal.json +7 -0
  69. package/package.json +14 -5
  70. package/src/blob-store/downloader.js +130 -0
  71. package/src/blob-store/entries-stream.js +81 -0
  72. package/src/blob-store/hyperdrive-index.js +122 -0
  73. package/src/blob-store/index.js +59 -117
  74. package/src/blob-store/utils.js +54 -0
  75. package/src/constants.js +4 -1
  76. package/src/core-manager/index.js +60 -3
  77. package/src/core-ownership.js +2 -4
  78. package/src/datastore/README.md +1 -2
  79. package/src/datastore/index.js +8 -8
  80. package/src/datatype/index.d.ts +5 -1
  81. package/src/datatype/index.js +22 -9
  82. package/src/discovery/local-discovery.js +2 -1
  83. package/src/errors.js +11 -2
  84. package/src/fastify-plugins/blobs.js +17 -1
  85. package/src/fastify-plugins/maps.js +2 -1
  86. package/src/generated/extensions.d.ts +31 -0
  87. package/src/generated/extensions.js +150 -0
  88. package/src/generated/extensions.ts +181 -0
  89. package/src/index.js +10 -0
  90. package/src/invite-api.js +1 -1
  91. package/src/lib/drizzle-helpers.js +79 -0
  92. package/src/lib/error.js +71 -0
  93. package/src/lib/get-own.js +10 -0
  94. package/src/lib/is-hostname-ip-address.js +26 -0
  95. package/src/lib/ws-core-replicator.js +47 -0
  96. package/src/mapeo-manager.js +74 -45
  97. package/src/mapeo-project.js +238 -58
  98. package/src/member-api.js +295 -2
  99. package/src/roles.js +38 -32
  100. package/src/schema/client.js +4 -3
  101. package/src/schema/project.js +7 -0
  102. package/src/sync/core-sync-state.js +39 -23
  103. package/src/sync/namespace-sync-state.js +22 -0
  104. package/src/sync/peer-sync-controller.js +1 -0
  105. package/src/sync/sync-api.js +197 -3
  106. package/src/sync/sync-state.js +18 -0
  107. package/src/translation-api.js +5 -9
  108. package/src/types.ts +12 -3
  109. package/dist/blob-store/live-download.d.ts +0 -107
  110. package/dist/blob-store/live-download.d.ts.map +0 -1
  111. package/dist/lib/timing-safe-equal.d.ts +0 -15
  112. package/dist/lib/timing-safe-equal.d.ts.map +0 -1
  113. package/src/blob-store/live-download.js +0 -373
  114. package/src/lib/timing-safe-equal.js +0 -34
@@ -2,7 +2,6 @@ import path from 'path'
2
2
  import Database from 'better-sqlite3'
3
3
  import { decodeBlockPrefix, decode, parseVersionId } from '@comapeo/schema'
4
4
  import { drizzle } from 'drizzle-orm/better-sqlite3'
5
- import { migrate } from 'drizzle-orm/better-sqlite3/migrator'
6
5
  import { discoveryKey } from 'hypercore-crypto'
7
6
  import { TypedEmitter } from 'tiny-typed-emitter'
8
7
 
@@ -24,6 +23,7 @@ import {
24
23
  roleTable,
25
24
  iconTable,
26
25
  translationTable,
26
+ remoteDetectionAlertTable,
27
27
  } from './schema/project.js'
28
28
  import {
29
29
  CoreOwnership,
@@ -38,20 +38,31 @@ import {
38
38
  } from './roles.js'
39
39
  import {
40
40
  assert,
41
+ ExhaustivenessError,
41
42
  getDeviceId,
42
43
  projectKeyToId,
43
44
  projectKeyToPublicId,
44
45
  valueOf,
45
46
  } from './utils.js'
47
+ import { migrate } from './lib/drizzle-helpers.js'
46
48
  import { omit } from './lib/omit.js'
47
49
  import { MemberApi } from './member-api.js'
48
- import { SyncApi, kHandleDiscoveryKey } from './sync/sync-api.js'
50
+ import {
51
+ SyncApi,
52
+ kAddBlobWantRange,
53
+ kClearBlobWantRanges,
54
+ kHandleDiscoveryKey,
55
+ kSetBlobDownloadFilter,
56
+ kWaitForInitialSyncWithPeer,
57
+ } from './sync/sync-api.js'
49
58
  import { Logger } from './logger.js'
50
59
  import { IconApi } from './icon-api.js'
51
60
  import { readConfig } from './config-import.js'
52
61
  import TranslationApi from './translation-api.js'
62
+ import { NotFoundError, nullIfNotFound } from './errors.js'
63
+ import { getErrorCode, getErrorMessage } from './lib/error.js'
53
64
  /** @import { ProjectSettingsValue } from '@comapeo/schema' */
54
- /** @import { CoreStorage, KeyPair, Namespace } from './types.js' */
65
+ /** @import { CoreStorage, BlobFilter, BlobStoreEntriesStream, KeyPair, Namespace, ReplicationStream } from './types.js' */
55
66
 
56
67
  /** @typedef {Omit<ProjectSettingsValue, 'schemaName'>} EditableProjectSettings */
57
68
  /** @typedef {ProjectSettingsValue['configMetadata']} ConfigMetadata */
@@ -66,15 +77,25 @@ export const kProjectReplicate = Symbol('replicate project')
66
77
  export const kDataTypes = Symbol('dataTypes')
67
78
  export const kProjectLeave = Symbol('leave project')
68
79
  export const kClearDataIfLeft = Symbol('clear data if left project')
80
+ export const kSetIsArchiveDevice = Symbol('set isArchiveDevice')
81
+ export const kIsArchiveDevice = Symbol('isArchiveDevice (temp - test only)')
69
82
 
70
83
  const EMPTY_PROJECT_SETTINGS = Object.freeze({})
71
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
+
72
92
  /**
73
93
  * @extends {TypedEmitter<{ close: () => void }>}
74
94
  */
75
95
  export class MapeoProject extends TypedEmitter {
76
- #projectId
96
+ #projectKey
77
97
  #deviceId
98
+ #identityKeypair
78
99
  #coreManager
79
100
  #indexWriter
80
101
  #dataStores
@@ -91,6 +112,7 @@ export class MapeoProject extends TypedEmitter {
91
112
  #l
92
113
  /** @type {Boolean} this avoids loading multiple configs in parallel */
93
114
  #loadingConfig
115
+ #isArchiveDevice
94
116
 
95
117
  static EMPTY_PROJECT_SETTINGS = EMPTY_PROJECT_SETTINGS
96
118
 
@@ -107,6 +129,7 @@ export class MapeoProject extends TypedEmitter {
107
129
  * @param {CoreStorage} opts.coreStorage Folder to store all hypercore data
108
130
  * @param {(mediaType: 'blobs' | 'icons') => Promise<string>} opts.getMediaBaseUrl
109
131
  * @param {import('./local-peers.js').LocalPeers} opts.localPeers
132
+ * @param {boolean} opts.isArchiveDevice Whether this device is an archive device
110
133
  * @param {Logger} [opts.logger]
111
134
  *
112
135
  */
@@ -123,20 +146,60 @@ export class MapeoProject extends TypedEmitter {
123
146
  getMediaBaseUrl,
124
147
  localPeers,
125
148
  logger,
149
+ isArchiveDevice,
126
150
  }) {
127
151
  super()
128
152
 
129
153
  this.#l = Logger.create('project', logger)
130
154
  this.#deviceId = getDeviceId(keyManager)
131
- this.#projectId = projectKeyToId(projectKey)
155
+ this.#projectKey = projectKey
132
156
  this.#loadingConfig = false
157
+ this.#isArchiveDevice = isArchiveDevice
158
+
159
+ const getReplicationStream = this[kProjectReplicate].bind(this, true)
160
+
161
+ const blobDownloadFilter = getBlobDownloadFilter(isArchiveDevice)
133
162
 
134
163
  ///////// 1. Setup database
164
+
135
165
  this.#sqlite = new Database(dbPath)
136
166
  const db = drizzle(this.#sqlite)
137
- migrate(db, { migrationsFolder: projectMigrationsFolder })
167
+ const migrationResult = migrate(db, {
168
+ migrationsFolder: projectMigrationsFolder,
169
+ })
170
+ let reindex
171
+ switch (migrationResult) {
172
+ case 'initialized database':
173
+ case 'no migration':
174
+ reindex = false
175
+ break
176
+ case 'migrated':
177
+ reindex = true
178
+ break
179
+ default:
180
+ throw new ExhaustivenessError(migrationResult)
181
+ }
138
182
 
139
- ///////// 2. Setup random-access-storage functions
183
+ const indexedTables = [
184
+ observationTable,
185
+ trackTable,
186
+ presetTable,
187
+ fieldTable,
188
+ coreOwnershipTable,
189
+ roleTable,
190
+ deviceInfoTable,
191
+ iconTable,
192
+ translationTable,
193
+ remoteDetectionAlertTable,
194
+ ]
195
+
196
+ ///////// 2. Wipe data if we need to re-index
197
+
198
+ if (reindex) {
199
+ for (const table of indexedTables) db.delete(table).run()
200
+ }
201
+
202
+ ///////// 3. Setup random-access-storage functions
140
203
 
141
204
  /** @type {ConstructorParameters<typeof CoreManager>[0]['storage']} */
142
205
  const coreManagerStorage = (name) =>
@@ -146,7 +209,7 @@ export class MapeoProject extends TypedEmitter {
146
209
  const indexerStorage = (name) =>
147
210
  coreStorage(path.join(INDEXER_STORAGE_FOLDER_NAME, name))
148
211
 
149
- ///////// 3. Create instances
212
+ ///////// 4. Create instances
150
213
 
151
214
  this.#coreManager = new CoreManager({
152
215
  projectSecretKey,
@@ -159,17 +222,7 @@ export class MapeoProject extends TypedEmitter {
159
222
  })
160
223
 
161
224
  this.#indexWriter = new IndexWriter({
162
- tables: [
163
- observationTable,
164
- trackTable,
165
- presetTable,
166
- fieldTable,
167
- coreOwnershipTable,
168
- roleTable,
169
- deviceInfoTable,
170
- iconTable,
171
- translationTable,
172
- ],
225
+ tables: indexedTables,
173
226
  sqlite: this.#sqlite,
174
227
  getWinner,
175
228
  mapDoc: (doc, version) => {
@@ -191,6 +244,7 @@ export class MapeoProject extends TypedEmitter {
191
244
  namespace: 'auth',
192
245
  batch: (entries) => this.#indexWriter.batch(entries),
193
246
  storage: indexerStorage,
247
+ reindex,
194
248
  }),
195
249
  config: new DataStore({
196
250
  coreManager: this.#coreManager,
@@ -201,12 +255,14 @@ export class MapeoProject extends TypedEmitter {
201
255
  sharedIndexWriter,
202
256
  }),
203
257
  storage: indexerStorage,
258
+ reindex,
204
259
  }),
205
260
  data: new DataStore({
206
261
  coreManager: this.#coreManager,
207
262
  namespace: 'data',
208
263
  batch: (entries) => this.#indexWriter.batch(entries),
209
264
  storage: indexerStorage,
265
+ reindex,
210
266
  }),
211
267
  }
212
268
 
@@ -225,6 +281,12 @@ export class MapeoProject extends TypedEmitter {
225
281
  db,
226
282
  getTranslations,
227
283
  }),
284
+ remoteDetectionAlert: new DataType({
285
+ dataStore: this.#dataStores.data,
286
+ table: remoteDetectionAlertTable,
287
+ db,
288
+ getTranslations,
289
+ }),
228
290
  preset: new DataType({
229
291
  dataStore: this.#dataStores.config,
230
292
  table: presetTable,
@@ -276,7 +338,7 @@ export class MapeoProject extends TypedEmitter {
276
338
  },
277
339
  }),
278
340
  }
279
- const identityKeypair = keyManager.getIdentityKeypair()
341
+ this.#identityKeypair = keyManager.getIdentityKeypair()
280
342
  const coreKeypairs = getCoreKeypairs({
281
343
  projectKey,
282
344
  projectSecretKey,
@@ -285,14 +347,14 @@ export class MapeoProject extends TypedEmitter {
285
347
  this.#coreOwnership = new CoreOwnership({
286
348
  dataType: this.#dataTypes.coreOwnership,
287
349
  coreKeypairs,
288
- identityKeypair,
350
+ identityKeypair: this.#identityKeypair,
289
351
  })
290
352
  this.#roles = new Roles({
291
353
  dataType: this.#dataTypes.role,
292
354
  coreOwnership: this.#coreOwnership,
293
355
  coreManager: this.#coreManager,
294
356
  projectKey: projectKey,
295
- deviceKey: keyManager.getIdentityKeypair().publicKey,
357
+ deviceKey: this.#identityKeypair.publicKey,
296
358
  })
297
359
 
298
360
  this.#memberApi = new MemberApi({
@@ -300,18 +362,27 @@ export class MapeoProject extends TypedEmitter {
300
362
  roles: this.#roles,
301
363
  coreOwnership: this.#coreOwnership,
302
364
  encryptionKeys,
365
+ getProjectName: this.#getProjectName.bind(this),
303
366
  projectKey,
304
367
  rpc: localPeers,
368
+ getReplicationStream,
369
+ waitForInitialSyncWithPeer: (deviceId, abortSignal) =>
370
+ this.$sync[kWaitForInitialSyncWithPeer](deviceId, abortSignal),
305
371
  dataTypes: {
306
372
  deviceInfo: this.#dataTypes.deviceInfo,
307
373
  project: this.#dataTypes.projectSettings,
308
374
  },
309
375
  })
310
376
 
311
- const projectPublicId = projectKeyToPublicId(projectKey)
312
-
313
377
  this.#blobStore = new BlobStore({
314
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)
315
386
  })
316
387
 
317
388
  this.$blobs = new BlobApi({
@@ -321,7 +392,7 @@ export class MapeoProject extends TypedEmitter {
321
392
  if (!base.endsWith('/')) {
322
393
  base += '/'
323
394
  }
324
- return base + projectPublicId
395
+ return base + this.#projectPublicId
325
396
  },
326
397
  })
327
398
 
@@ -333,7 +404,7 @@ export class MapeoProject extends TypedEmitter {
333
404
  if (!base.endsWith('/')) {
334
405
  base += '/'
335
406
  }
336
- return base + projectPublicId
407
+ return base + this.#projectPublicId
337
408
  },
338
409
  })
339
410
 
@@ -341,14 +412,75 @@ export class MapeoProject extends TypedEmitter {
341
412
  coreManager: this.#coreManager,
342
413
  coreOwnership: this.#coreOwnership,
343
414
  roles: this.#roles,
415
+ blobDownloadFilter,
344
416
  logger: this.#l,
417
+ getServerWebsocketUrls: async () => {
418
+ const members = await this.#memberApi.getMany()
419
+ /** @type {string[]} */
420
+ const serverWebsocketUrls = []
421
+ for (const member of members) {
422
+ if (
423
+ member.deviceType === 'selfHostedServer' &&
424
+ member.selfHostedServerDetails
425
+ ) {
426
+ const { baseUrl } = member.selfHostedServerDetails
427
+ const wsUrl = new URL(`/sync/${this.#projectPublicId}`, baseUrl)
428
+ wsUrl.protocol = wsUrl.protocol === 'http:' ? 'ws:' : 'wss:'
429
+ serverWebsocketUrls.push(wsUrl.href)
430
+ }
431
+ }
432
+ return serverWebsocketUrls
433
+ },
434
+ getReplicationStream,
435
+ })
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)
345
477
  })
346
478
 
347
479
  this.#translationApi = new TranslationApi({
348
480
  dataType: this.#dataTypes.translation,
349
481
  })
350
482
 
351
- ///////// 4. Replicate local peers automatically
483
+ ///////// 5. Replicate local peers automatically
352
484
 
353
485
  // Replicate already connected local peers
354
486
  for (const peer of localPeers.peers) {
@@ -416,6 +548,14 @@ export class MapeoProject extends TypedEmitter {
416
548
  return this.#deviceId
417
549
  }
418
550
 
551
+ get #projectId() {
552
+ return projectKeyToId(this.#projectKey)
553
+ }
554
+
555
+ get #projectPublicId() {
556
+ return projectKeyToPublicId(this.#projectKey)
557
+ }
558
+
419
559
  /**
420
560
  * Resolves when hypercores have all loaded
421
561
  *
@@ -429,6 +569,7 @@ export class MapeoProject extends TypedEmitter {
429
569
  */
430
570
  async close() {
431
571
  this.#l.log('closing project %h', this.#projectId)
572
+ this.#blobStore.close()
432
573
  const dataStorePromises = []
433
574
  for (const dataStore of Object.values(this.#dataStores)) {
434
575
  dataStorePromises.push(dataStore.close())
@@ -499,6 +640,10 @@ export class MapeoProject extends TypedEmitter {
499
640
  return this.#dataTypes.field
500
641
  }
501
642
 
643
+ get remoteDetectionAlert() {
644
+ return this.#dataTypes.remoteDetectionAlert
645
+ }
646
+
502
647
  get $member() {
503
648
  return this.#memberApi
504
649
  }
@@ -518,14 +663,9 @@ export class MapeoProject extends TypedEmitter {
518
663
  async $setProjectSettings(settings) {
519
664
  const { projectSettings } = this.#dataTypes
520
665
 
521
- // We only want to catch the error to the getByDocId call
522
- // Using try/catch for this is a little verbose when dealing with TS types
523
666
  const existing = await projectSettings
524
667
  .getByDocId(this.#projectId)
525
- .catch(() => {
526
- // project does not exist so return null
527
- return null
528
- })
668
+ .catch(nullIfNotFound)
529
669
 
530
670
  if (existing) {
531
671
  return extractEditableProjectSettings(
@@ -557,6 +697,13 @@ export class MapeoProject extends TypedEmitter {
557
697
  }
558
698
  }
559
699
 
700
+ /**
701
+ * @returns {Promise<undefined | string>}
702
+ */
703
+ async #getProjectName() {
704
+ return (await this.$getProjectSettings()).name
705
+ }
706
+
560
707
  async $getOwnRole() {
561
708
  return this.#roles.getRole(this.#deviceId)
562
709
  }
@@ -571,35 +718,45 @@ export class MapeoProject extends TypedEmitter {
571
718
  const coreId = this.#coreManager
572
719
  .getCoreByDiscoveryKey(coreDiscoveryKey)
573
720
  ?.key.toString('hex')
574
- if (!coreId) throw new Error('NotFound')
721
+ if (!coreId) throw new NotFoundError()
575
722
  return this.#coreOwnership.getOwner(coreId)
576
723
  }
577
724
 
578
725
  /**
579
726
  * Replicate a project to a @hyperswarm/secret-stream. Invites will not
580
727
  * function because the RPC channel is not connected for project replication,
581
- * and only this project will replicate (to replicate multiple projects you
582
- * need to replicate the manager instance via manager[kManagerReplicate])
728
+ * and only this project will replicate.
583
729
  *
584
- * @param {Parameters<import('hypercore')['replicate']>[0]} stream A duplex stream, a @hyperswarm/secret-stream, or a Protomux instance
730
+ * @param {(
731
+ * boolean |
732
+ * import('stream').Duplex |
733
+ * import('streamx').Duplex
734
+ * )} isInitiatorOrStream
735
+ * @returns {ReplicationStream}
585
736
  */
586
- [kProjectReplicate](stream) {
587
- // @ts-expect-error - hypercore types need updating
588
- const replicationStream = this.#coreManager.creatorCore.replicate(stream, {
589
- // @ts-ignore - hypercore types do not currently include this option
590
- ondiscoverykey: async (discoveryKey) => {
591
- const protomux =
592
- /** @type {import('protomux')<import('@hyperswarm/secret-stream')>} */ (
593
- replicationStream.noiseStream.userData
594
- )
595
- this.#syncApi[kHandleDiscoveryKey](discoveryKey, protomux)
596
- },
597
- })
737
+ [kProjectReplicate](isInitiatorOrStream) {
738
+ const replicationStream = this.#coreManager.creatorCore.replicate(
739
+ isInitiatorOrStream,
740
+ /**
741
+ * Hypercore types need updating.
742
+ * @type {any}
743
+ */ ({
744
+ keyPair: this.#identityKeypair,
745
+ /** @param {Buffer} discoveryKey */
746
+ ondiscoverykey: async (discoveryKey) => {
747
+ const protomux =
748
+ /** @type {import('protomux')<import('@hyperswarm/secret-stream')>} */ (
749
+ replicationStream.noiseStream.userData
750
+ )
751
+ this.#syncApi[kHandleDiscoveryKey](discoveryKey, protomux)
752
+ },
753
+ })
754
+ )
598
755
  return replicationStream
599
756
  }
600
757
 
601
758
  /**
602
- * @param {Pick<import('@comapeo/schema').DeviceInfoValue, 'name' | 'deviceType'>} value
759
+ * @param {Pick<import('@comapeo/schema').DeviceInfoValue, 'name' | 'deviceType' | 'selfHostedServerDetails'>} value
603
760
  * @returns {Promise<import('@comapeo/schema').DeviceInfo>}
604
761
  */
605
762
  async [kSetOwnDeviceInfo](value) {
@@ -612,17 +769,32 @@ export class MapeoProject extends TypedEmitter {
612
769
  const doc = {
613
770
  name: value.name,
614
771
  deviceType: value.deviceType,
772
+ selfHostedServerDetails: value.selfHostedServerDetails,
615
773
  schemaName: /** @type {const} */ ('deviceInfo'),
616
774
  }
617
775
 
618
- let existingDoc
619
- try {
620
- existingDoc = await deviceInfo.getByDocId(configCoreId)
621
- } catch (err) {
776
+ const existingDoc = await deviceInfo
777
+ .getByDocId(configCoreId)
778
+ .catch(nullIfNotFound)
779
+ if (existingDoc) {
780
+ return await deviceInfo.update(existingDoc.versionId, doc)
781
+ } else {
622
782
  return await deviceInfo[kCreateWithDocId](configCoreId, doc)
623
783
  }
784
+ }
785
+
786
+ /** @param {boolean} isArchiveDevice */
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)
792
+ this.#isArchiveDevice = isArchiveDevice
793
+ }
624
794
 
625
- return deviceInfo.update(existingDoc.versionId, doc)
795
+ /** @returns {boolean} */
796
+ get [kIsArchiveDevice]() {
797
+ return this.#isArchiveDevice
626
798
  }
627
799
 
628
800
  /**
@@ -769,7 +941,7 @@ export class MapeoProject extends TypedEmitter {
769
941
  const fieldRefs = fieldNames.map((fieldName) => {
770
942
  const fieldRef = fieldNameToRef.get(fieldName)
771
943
  if (!fieldRef) {
772
- throw new Error(
944
+ throw new NotFoundError(
773
945
  `field ${fieldName} not found (referenced by preset ${value.name})})`
774
946
  )
775
947
  }
@@ -781,7 +953,7 @@ export class MapeoProject extends TypedEmitter {
781
953
  }
782
954
  const iconRef = iconNameToRef.get(iconName)
783
955
  if (!iconRef) {
784
- throw new Error(
956
+ throw new NotFoundError(
785
957
  `icon ${iconName} not found (referenced by preset ${value.name})`
786
958
  )
787
959
  }
@@ -828,7 +1000,7 @@ export class MapeoProject extends TypedEmitter {
828
1000
  })
829
1001
  )
830
1002
  } else {
831
- throw new Error(
1003
+ throw new NotFoundError(
832
1004
  `docRef for ${value.docRefType} with name ${name} not found`
833
1005
  )
834
1006
  }
@@ -883,6 +1055,14 @@ export class MapeoProject extends TypedEmitter {
883
1055
  }
884
1056
  }
885
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
+
886
1066
  /**
887
1067
  * @param {import("@comapeo/schema").ProjectSettings & { forks: string[] }} projectDoc
888
1068
  * @returns {EditableProjectSettings}