@comapeo/core 2.0.1 → 2.1.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 (71) hide show
  1. package/dist/blob-store/index.d.ts +5 -8
  2. package/dist/blob-store/index.d.ts.map +1 -1
  3. package/dist/constants.d.ts +2 -1
  4. package/dist/constants.d.ts.map +1 -1
  5. package/dist/core-manager/index.d.ts +10 -0
  6. package/dist/core-manager/index.d.ts.map +1 -1
  7. package/dist/datastore/index.d.ts +5 -4
  8. package/dist/datastore/index.d.ts.map +1 -1
  9. package/dist/generated/extensions.d.ts +31 -0
  10. package/dist/generated/extensions.d.ts.map +1 -1
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/lib/drizzle-helpers.d.ts +6 -0
  14. package/dist/lib/drizzle-helpers.d.ts.map +1 -0
  15. package/dist/lib/error.d.ts +37 -0
  16. package/dist/lib/error.d.ts.map +1 -0
  17. package/dist/lib/get-own.d.ts +9 -0
  18. package/dist/lib/get-own.d.ts.map +1 -0
  19. package/dist/lib/is-hostname-ip-address.d.ts +17 -0
  20. package/dist/lib/is-hostname-ip-address.d.ts.map +1 -0
  21. package/dist/lib/ws-core-replicator.d.ts +11 -0
  22. package/dist/lib/ws-core-replicator.d.ts.map +1 -0
  23. package/dist/mapeo-manager.d.ts +18 -22
  24. package/dist/mapeo-manager.d.ts.map +1 -1
  25. package/dist/mapeo-project.d.ts +448 -15
  26. package/dist/mapeo-project.d.ts.map +1 -1
  27. package/dist/member-api.d.ts +40 -1
  28. package/dist/member-api.d.ts.map +1 -1
  29. package/dist/schema/client.d.ts +17 -5
  30. package/dist/schema/client.d.ts.map +1 -1
  31. package/dist/schema/project.d.ts +210 -0
  32. package/dist/schema/project.d.ts.map +1 -1
  33. package/dist/sync/peer-sync-controller.d.ts.map +1 -1
  34. package/dist/sync/sync-api.d.ts +28 -2
  35. package/dist/sync/sync-api.d.ts.map +1 -1
  36. package/dist/types.d.ts +3 -2
  37. package/dist/types.d.ts.map +1 -1
  38. package/drizzle/client/0001_chubby_cargill.sql +12 -0
  39. package/drizzle/client/meta/0001_snapshot.json +208 -0
  40. package/drizzle/client/meta/_journal.json +7 -0
  41. package/drizzle/project/0001_medical_wendell_rand.sql +22 -0
  42. package/drizzle/project/meta/0001_snapshot.json +1267 -0
  43. package/drizzle/project/meta/_journal.json +7 -0
  44. package/package.json +9 -5
  45. package/src/blob-store/index.js +3 -2
  46. package/src/constants.js +4 -1
  47. package/src/core-manager/index.js +58 -2
  48. package/src/datastore/README.md +1 -2
  49. package/src/datastore/index.js +4 -5
  50. package/src/fastify-plugins/blobs.js +1 -0
  51. package/src/generated/extensions.d.ts +31 -0
  52. package/src/generated/extensions.js +150 -0
  53. package/src/generated/extensions.ts +181 -0
  54. package/src/index.js +10 -0
  55. package/src/invite-api.js +1 -1
  56. package/src/lib/drizzle-helpers.js +79 -0
  57. package/src/lib/error.js +47 -0
  58. package/src/lib/get-own.js +10 -0
  59. package/src/lib/is-hostname-ip-address.js +26 -0
  60. package/src/lib/ws-core-replicator.js +47 -0
  61. package/src/mapeo-manager.js +71 -43
  62. package/src/mapeo-project.js +153 -43
  63. package/src/member-api.js +253 -2
  64. package/src/schema/client.js +4 -3
  65. package/src/schema/project.js +7 -0
  66. package/src/sync/peer-sync-controller.js +1 -0
  67. package/src/sync/sync-api.js +171 -3
  68. package/src/types.ts +4 -3
  69. package/dist/lib/timing-safe-equal.d.ts +0 -15
  70. package/dist/lib/timing-safe-equal.d.ts.map +0 -1
  71. 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,26 @@ 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
+ kHandleDiscoveryKey,
53
+ kWaitForInitialSyncWithPeer,
54
+ } from './sync/sync-api.js'
49
55
  import { Logger } from './logger.js'
50
56
  import { IconApi } from './icon-api.js'
51
57
  import { readConfig } from './config-import.js'
52
58
  import TranslationApi from './translation-api.js'
53
59
  /** @import { ProjectSettingsValue } from '@comapeo/schema' */
54
- /** @import { CoreStorage, KeyPair, Namespace } from './types.js' */
60
+ /** @import { CoreStorage, KeyPair, Namespace, ReplicationStream } from './types.js' */
55
61
 
56
62
  /** @typedef {Omit<ProjectSettingsValue, 'schemaName'>} EditableProjectSettings */
57
63
  /** @typedef {ProjectSettingsValue['configMetadata']} ConfigMetadata */
@@ -66,6 +72,8 @@ export const kProjectReplicate = Symbol('replicate project')
66
72
  export const kDataTypes = Symbol('dataTypes')
67
73
  export const kProjectLeave = Symbol('leave project')
68
74
  export const kClearDataIfLeft = Symbol('clear data if left project')
75
+ export const kSetIsArchiveDevice = Symbol('set isArchiveDevice')
76
+ export const kIsArchiveDevice = Symbol('isArchiveDevice (temp - test only)')
69
77
 
70
78
  const EMPTY_PROJECT_SETTINGS = Object.freeze({})
71
79
 
@@ -73,8 +81,9 @@ const EMPTY_PROJECT_SETTINGS = Object.freeze({})
73
81
  * @extends {TypedEmitter<{ close: () => void }>}
74
82
  */
75
83
  export class MapeoProject extends TypedEmitter {
76
- #projectId
84
+ #projectKey
77
85
  #deviceId
86
+ #identityKeypair
78
87
  #coreManager
79
88
  #indexWriter
80
89
  #dataStores
@@ -91,6 +100,7 @@ export class MapeoProject extends TypedEmitter {
91
100
  #l
92
101
  /** @type {Boolean} this avoids loading multiple configs in parallel */
93
102
  #loadingConfig
103
+ #isArchiveDevice
94
104
 
95
105
  static EMPTY_PROJECT_SETTINGS = EMPTY_PROJECT_SETTINGS
96
106
 
@@ -107,6 +117,7 @@ export class MapeoProject extends TypedEmitter {
107
117
  * @param {CoreStorage} opts.coreStorage Folder to store all hypercore data
108
118
  * @param {(mediaType: 'blobs' | 'icons') => Promise<string>} opts.getMediaBaseUrl
109
119
  * @param {import('./local-peers.js').LocalPeers} opts.localPeers
120
+ * @param {boolean} opts.isArchiveDevice Whether this device is an archive device
110
121
  * @param {Logger} [opts.logger]
111
122
  *
112
123
  */
@@ -123,20 +134,58 @@ export class MapeoProject extends TypedEmitter {
123
134
  getMediaBaseUrl,
124
135
  localPeers,
125
136
  logger,
137
+ isArchiveDevice,
126
138
  }) {
127
139
  super()
128
140
 
129
141
  this.#l = Logger.create('project', logger)
130
142
  this.#deviceId = getDeviceId(keyManager)
131
- this.#projectId = projectKeyToId(projectKey)
143
+ this.#projectKey = projectKey
132
144
  this.#loadingConfig = false
145
+ this.#isArchiveDevice = isArchiveDevice
146
+
147
+ const getReplicationStream = this[kProjectReplicate].bind(this, true)
133
148
 
134
149
  ///////// 1. Setup database
150
+
135
151
  this.#sqlite = new Database(dbPath)
136
152
  const db = drizzle(this.#sqlite)
137
- migrate(db, { migrationsFolder: projectMigrationsFolder })
153
+ const migrationResult = migrate(db, {
154
+ migrationsFolder: projectMigrationsFolder,
155
+ })
156
+ let reindex
157
+ switch (migrationResult) {
158
+ case 'initialized database':
159
+ case 'no migration':
160
+ reindex = false
161
+ break
162
+ case 'migrated':
163
+ reindex = true
164
+ break
165
+ default:
166
+ throw new ExhaustivenessError(migrationResult)
167
+ }
138
168
 
139
- ///////// 2. Setup random-access-storage functions
169
+ const indexedTables = [
170
+ observationTable,
171
+ trackTable,
172
+ presetTable,
173
+ fieldTable,
174
+ coreOwnershipTable,
175
+ roleTable,
176
+ deviceInfoTable,
177
+ iconTable,
178
+ translationTable,
179
+ remoteDetectionAlertTable,
180
+ ]
181
+
182
+ ///////// 2. Wipe data if we need to re-index
183
+
184
+ if (reindex) {
185
+ for (const table of indexedTables) db.delete(table).run()
186
+ }
187
+
188
+ ///////// 3. Setup random-access-storage functions
140
189
 
141
190
  /** @type {ConstructorParameters<typeof CoreManager>[0]['storage']} */
142
191
  const coreManagerStorage = (name) =>
@@ -146,7 +195,7 @@ export class MapeoProject extends TypedEmitter {
146
195
  const indexerStorage = (name) =>
147
196
  coreStorage(path.join(INDEXER_STORAGE_FOLDER_NAME, name))
148
197
 
149
- ///////// 3. Create instances
198
+ ///////// 4. Create instances
150
199
 
151
200
  this.#coreManager = new CoreManager({
152
201
  projectSecretKey,
@@ -159,17 +208,7 @@ export class MapeoProject extends TypedEmitter {
159
208
  })
160
209
 
161
210
  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
- ],
211
+ tables: indexedTables,
173
212
  sqlite: this.#sqlite,
174
213
  getWinner,
175
214
  mapDoc: (doc, version) => {
@@ -191,6 +230,7 @@ export class MapeoProject extends TypedEmitter {
191
230
  namespace: 'auth',
192
231
  batch: (entries) => this.#indexWriter.batch(entries),
193
232
  storage: indexerStorage,
233
+ reindex,
194
234
  }),
195
235
  config: new DataStore({
196
236
  coreManager: this.#coreManager,
@@ -201,12 +241,14 @@ export class MapeoProject extends TypedEmitter {
201
241
  sharedIndexWriter,
202
242
  }),
203
243
  storage: indexerStorage,
244
+ reindex,
204
245
  }),
205
246
  data: new DataStore({
206
247
  coreManager: this.#coreManager,
207
248
  namespace: 'data',
208
249
  batch: (entries) => this.#indexWriter.batch(entries),
209
250
  storage: indexerStorage,
251
+ reindex,
210
252
  }),
211
253
  }
212
254
 
@@ -225,6 +267,12 @@ export class MapeoProject extends TypedEmitter {
225
267
  db,
226
268
  getTranslations,
227
269
  }),
270
+ remoteDetectionAlert: new DataType({
271
+ dataStore: this.#dataStores.data,
272
+ table: remoteDetectionAlertTable,
273
+ db,
274
+ getTranslations,
275
+ }),
228
276
  preset: new DataType({
229
277
  dataStore: this.#dataStores.config,
230
278
  table: presetTable,
@@ -276,7 +324,7 @@ export class MapeoProject extends TypedEmitter {
276
324
  },
277
325
  }),
278
326
  }
279
- const identityKeypair = keyManager.getIdentityKeypair()
327
+ this.#identityKeypair = keyManager.getIdentityKeypair()
280
328
  const coreKeypairs = getCoreKeypairs({
281
329
  projectKey,
282
330
  projectSecretKey,
@@ -285,14 +333,14 @@ export class MapeoProject extends TypedEmitter {
285
333
  this.#coreOwnership = new CoreOwnership({
286
334
  dataType: this.#dataTypes.coreOwnership,
287
335
  coreKeypairs,
288
- identityKeypair,
336
+ identityKeypair: this.#identityKeypair,
289
337
  })
290
338
  this.#roles = new Roles({
291
339
  dataType: this.#dataTypes.role,
292
340
  coreOwnership: this.#coreOwnership,
293
341
  coreManager: this.#coreManager,
294
342
  projectKey: projectKey,
295
- deviceKey: keyManager.getIdentityKeypair().publicKey,
343
+ deviceKey: this.#identityKeypair.publicKey,
296
344
  })
297
345
 
298
346
  this.#memberApi = new MemberApi({
@@ -300,16 +348,18 @@ export class MapeoProject extends TypedEmitter {
300
348
  roles: this.#roles,
301
349
  coreOwnership: this.#coreOwnership,
302
350
  encryptionKeys,
351
+ getProjectName: this.#getProjectName.bind(this),
303
352
  projectKey,
304
353
  rpc: localPeers,
354
+ getReplicationStream,
355
+ waitForInitialSyncWithPeer: (deviceId, abortSignal) =>
356
+ this.$sync[kWaitForInitialSyncWithPeer](deviceId, abortSignal),
305
357
  dataTypes: {
306
358
  deviceInfo: this.#dataTypes.deviceInfo,
307
359
  project: this.#dataTypes.projectSettings,
308
360
  },
309
361
  })
310
362
 
311
- const projectPublicId = projectKeyToPublicId(projectKey)
312
-
313
363
  this.#blobStore = new BlobStore({
314
364
  coreManager: this.#coreManager,
315
365
  })
@@ -321,7 +371,7 @@ export class MapeoProject extends TypedEmitter {
321
371
  if (!base.endsWith('/')) {
322
372
  base += '/'
323
373
  }
324
- return base + projectPublicId
374
+ return base + this.#projectPublicId
325
375
  },
326
376
  })
327
377
 
@@ -333,7 +383,7 @@ export class MapeoProject extends TypedEmitter {
333
383
  if (!base.endsWith('/')) {
334
384
  base += '/'
335
385
  }
336
- return base + projectPublicId
386
+ return base + this.#projectPublicId
337
387
  },
338
388
  })
339
389
 
@@ -341,14 +391,33 @@ export class MapeoProject extends TypedEmitter {
341
391
  coreManager: this.#coreManager,
342
392
  coreOwnership: this.#coreOwnership,
343
393
  roles: this.#roles,
394
+ blobDownloadFilter: null,
344
395
  logger: this.#l,
396
+ getServerWebsocketUrls: async () => {
397
+ const members = await this.#memberApi.getMany()
398
+ /** @type {string[]} */
399
+ const serverWebsocketUrls = []
400
+ for (const member of members) {
401
+ if (
402
+ member.deviceType === 'selfHostedServer' &&
403
+ member.selfHostedServerDetails
404
+ ) {
405
+ const { baseUrl } = member.selfHostedServerDetails
406
+ const wsUrl = new URL(`/sync/${this.#projectPublicId}`, baseUrl)
407
+ wsUrl.protocol = wsUrl.protocol === 'http:' ? 'ws:' : 'wss:'
408
+ serverWebsocketUrls.push(wsUrl.href)
409
+ }
410
+ }
411
+ return serverWebsocketUrls
412
+ },
413
+ getReplicationStream,
345
414
  })
346
415
 
347
416
  this.#translationApi = new TranslationApi({
348
417
  dataType: this.#dataTypes.translation,
349
418
  })
350
419
 
351
- ///////// 4. Replicate local peers automatically
420
+ ///////// 5. Replicate local peers automatically
352
421
 
353
422
  // Replicate already connected local peers
354
423
  for (const peer of localPeers.peers) {
@@ -416,6 +485,14 @@ export class MapeoProject extends TypedEmitter {
416
485
  return this.#deviceId
417
486
  }
418
487
 
488
+ get #projectId() {
489
+ return projectKeyToId(this.#projectKey)
490
+ }
491
+
492
+ get #projectPublicId() {
493
+ return projectKeyToPublicId(this.#projectKey)
494
+ }
495
+
419
496
  /**
420
497
  * Resolves when hypercores have all loaded
421
498
  *
@@ -499,6 +576,10 @@ export class MapeoProject extends TypedEmitter {
499
576
  return this.#dataTypes.field
500
577
  }
501
578
 
579
+ get remoteDetectionAlert() {
580
+ return this.#dataTypes.remoteDetectionAlert
581
+ }
582
+
502
583
  get $member() {
503
584
  return this.#memberApi
504
585
  }
@@ -557,6 +638,13 @@ export class MapeoProject extends TypedEmitter {
557
638
  }
558
639
  }
559
640
 
641
+ /**
642
+ * @returns {Promise<undefined | string>}
643
+ */
644
+ async #getProjectName() {
645
+ return (await this.$getProjectSettings()).name
646
+ }
647
+
560
648
  async $getOwnRole() {
561
649
  return this.#roles.getRole(this.#deviceId)
562
650
  }
@@ -578,28 +666,38 @@ export class MapeoProject extends TypedEmitter {
578
666
  /**
579
667
  * Replicate a project to a @hyperswarm/secret-stream. Invites will not
580
668
  * 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])
669
+ * and only this project will replicate.
583
670
  *
584
- * @param {Parameters<import('hypercore')['replicate']>[0]} stream A duplex stream, a @hyperswarm/secret-stream, or a Protomux instance
671
+ * @param {(
672
+ * boolean |
673
+ * import('stream').Duplex |
674
+ * import('streamx').Duplex
675
+ * )} isInitiatorOrStream
676
+ * @returns {ReplicationStream}
585
677
  */
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
- })
678
+ [kProjectReplicate](isInitiatorOrStream) {
679
+ const replicationStream = this.#coreManager.creatorCore.replicate(
680
+ isInitiatorOrStream,
681
+ /**
682
+ * Hypercore types need updating.
683
+ * @type {any}
684
+ */ ({
685
+ keyPair: this.#identityKeypair,
686
+ /** @param {Buffer} discoveryKey */
687
+ ondiscoverykey: async (discoveryKey) => {
688
+ const protomux =
689
+ /** @type {import('protomux')<import('@hyperswarm/secret-stream')>} */ (
690
+ replicationStream.noiseStream.userData
691
+ )
692
+ this.#syncApi[kHandleDiscoveryKey](discoveryKey, protomux)
693
+ },
694
+ })
695
+ )
598
696
  return replicationStream
599
697
  }
600
698
 
601
699
  /**
602
- * @param {Pick<import('@comapeo/schema').DeviceInfoValue, 'name' | 'deviceType'>} value
700
+ * @param {Pick<import('@comapeo/schema').DeviceInfoValue, 'name' | 'deviceType' | 'selfHostedServerDetails'>} value
603
701
  * @returns {Promise<import('@comapeo/schema').DeviceInfo>}
604
702
  */
605
703
  async [kSetOwnDeviceInfo](value) {
@@ -612,6 +710,7 @@ export class MapeoProject extends TypedEmitter {
612
710
  const doc = {
613
711
  name: value.name,
614
712
  deviceType: value.deviceType,
713
+ selfHostedServerDetails: value.selfHostedServerDetails,
615
714
  schemaName: /** @type {const} */ ('deviceInfo'),
616
715
  }
617
716
 
@@ -625,6 +724,17 @@ export class MapeoProject extends TypedEmitter {
625
724
  return deviceInfo.update(existingDoc.versionId, doc)
626
725
  }
627
726
 
727
+ /** @param {boolean} isArchiveDevice */
728
+ async [kSetIsArchiveDevice](isArchiveDevice) {
729
+ this.#isArchiveDevice = isArchiveDevice
730
+ // TODO: call this.#syncApi[kSetBlobDownloadFilter]()
731
+ }
732
+
733
+ /** @returns {boolean} */
734
+ get [kIsArchiveDevice]() {
735
+ return this.#isArchiveDevice
736
+ }
737
+
628
738
  /**
629
739
  * @returns {import('./icon-api.js').IconApi}
630
740
  */