@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
@@ -0,0 +1,47 @@
1
+ import { pipeline } from 'node:stream/promises'
2
+ import { Transform } from 'node:stream'
3
+ import { createWebSocketStream } from 'ws'
4
+ /** @import { WebSocket } from 'ws' */
5
+ /** @import { ReplicationStream } from '../types.js' */
6
+
7
+ /**
8
+ * @param {WebSocket} ws
9
+ * @param {ReplicationStream} replicationStream
10
+ * @returns {Promise<void>}
11
+ */
12
+ export function wsCoreReplicator(ws, replicationStream) {
13
+ // This is purely to satisfy typescript at its worst. `pipeline` expects a
14
+ // NodeJS ReadWriteStream, but our replicationStream is a streamx Duplex
15
+ // stream. The difference is that streamx does not implement the
16
+ // `setEncoding`, `unpipe`, `wrap` or `isPaused` methods. The `pipeline`
17
+ // function does not depend on any of these methods (I have read through the
18
+ // NodeJS source code at cebf21d (v22.9.0) to confirm this), so we can safely
19
+ // cast the stream to a NodeJS ReadWriteStream.
20
+ const _replicationStream = /** @type {NodeJS.ReadWriteStream} */ (
21
+ /** @type {unknown} */ (replicationStream)
22
+ )
23
+ return pipeline(
24
+ _replicationStream,
25
+ wsSafetyTransform(ws),
26
+ createWebSocketStream(ws),
27
+ _replicationStream
28
+ )
29
+ }
30
+
31
+ /**
32
+ * Avoid writing data to a closing or closed websocket, which would result in an
33
+ * error. Instead we drop the data and wait for the stream close/end events to
34
+ * propagate and close the streams cleanly.
35
+ *
36
+ * @param {WebSocket} ws
37
+ */
38
+ function wsSafetyTransform(ws) {
39
+ return new Transform({
40
+ transform(chunk, encoding, callback) {
41
+ if (ws.readyState === ws.CLOSED || ws.readyState === ws.CLOSING) {
42
+ return callback()
43
+ }
44
+ callback(null, chunk)
45
+ },
46
+ })
47
+ }
@@ -16,10 +16,11 @@ import {
16
16
  kBlobStore,
17
17
  kClearDataIfLeft,
18
18
  kProjectLeave,
19
+ kSetIsArchiveDevice,
19
20
  kSetOwnDeviceInfo,
20
21
  } from './mapeo-project.js'
21
22
  import {
22
- localDeviceInfoTable,
23
+ deviceSettingsTable,
23
24
  projectKeysTable,
24
25
  projectSettingsTable,
25
26
  } from './schema/client.js'
@@ -44,14 +45,15 @@ import { LocalPeers } from './local-peers.js'
44
45
  import { InviteApi } from './invite-api.js'
45
46
  import { LocalDiscovery } from './discovery/local-discovery.js'
46
47
  import { Roles } from './roles.js'
47
- import NoiseSecretStream from '@hyperswarm/secret-stream'
48
48
  import { Logger } from './logger.js'
49
49
  import {
50
50
  kSyncState,
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' */
56
+ /** @import NoiseSecretStream from '@hyperswarm/secret-stream' */
55
57
  /** @import { SetNonNullable } from 'type-fest' */
56
58
  /** @import { CoreStorage, Namespace } from './types.js' */
57
59
  /** @import { DeviceInfoParam } from './schema/client.js' */
@@ -79,9 +81,6 @@ export const DEFAULT_FALLBACK_MAP_FILE_PATH = require.resolve(
79
81
  export const DEFAULT_ONLINE_STYLE_URL =
80
82
  'https://demotiles.maplibre.org/style.json'
81
83
 
82
- export const kRPC = Symbol('rpc')
83
- export const kManagerReplicate = Symbol('replicate manager')
84
-
85
84
  /**
86
85
  * @typedef {Omit<import('./local-peers.js').PeerInfo, 'protomux'>} PublicPeerInfo
87
86
  */
@@ -221,33 +220,10 @@ export class MapeoManager extends TypedEmitter {
221
220
  this.#localDiscovery.on('connection', this.#replicate.bind(this))
222
221
  }
223
222
 
224
- /**
225
- * MapeoRPC instance, used for tests
226
- */
227
- get [kRPC]() {
228
- return this.#localPeers
229
- }
230
-
231
223
  get deviceId() {
232
224
  return this.#deviceId
233
225
  }
234
226
 
235
- /**
236
- * Create a Mapeo replication stream. This replication connects the Mapeo RPC
237
- * channel and allows invites. All active projects will sync automatically to
238
- * this replication stream. Only use for local (trusted) connections, because
239
- * the RPC channel key is public. To sync a specific project without
240
- * connecting RPC, use project[kProjectReplication].
241
- *
242
- * @param {boolean} isInitiator
243
- */
244
- [kManagerReplicate](isInitiator) {
245
- const noiseStream = new NoiseSecretStream(isInitiator, undefined, {
246
- keyPair: this.#keyManager.getIdentityKeypair(),
247
- })
248
- return this.#replicate(noiseStream)
249
- }
250
-
251
227
  /**
252
228
  * @param {'blobs' | 'icons' | 'maps'} mediaType
253
229
  * @returns {Promise<string>}
@@ -481,7 +457,7 @@ export class MapeoManager extends TypedEmitter {
481
457
  .get()
482
458
 
483
459
  if (!projectKeysTableResult) {
484
- throw new Error(`NotFound: project ID ${projectPublicId} not found`)
460
+ throw new NotFoundError(`Project ID ${projectPublicId} not found`)
485
461
  }
486
462
 
487
463
  const { projectId } = projectKeysTableResult
@@ -507,6 +483,7 @@ export class MapeoManager extends TypedEmitter {
507
483
  async #createProjectInstance(projectKeys) {
508
484
  validateProjectKeys(projectKeys)
509
485
  const projectId = keyToId(projectKeys.projectKey)
486
+ const isArchiveDevice = this.getIsArchiveDevice()
510
487
  const project = new MapeoProject({
511
488
  ...this.#projectStorage(projectId),
512
489
  ...projectKeys,
@@ -517,6 +494,7 @@ export class MapeoManager extends TypedEmitter {
517
494
  localPeers: this.#localPeers,
518
495
  logger: this.#loggerBase,
519
496
  getMediaBaseUrl: this.#getMediaBaseUrl.bind(this),
497
+ isArchiveDevice,
520
498
  })
521
499
  await project[kClearDataIfLeft]()
522
500
  return project
@@ -579,7 +557,7 @@ export class MapeoManager extends TypedEmitter {
579
557
  * downloaded their proof of project membership and the project config.
580
558
  *
581
559
  * @param {Pick<import('./generated/rpc.js').ProjectJoinDetails, 'projectKey' | 'encryptionKeys'> & { projectName: string }} projectJoinDetails
582
- * @param {{ waitForSync?: boolean }} [opts] For internal use in tests, set opts.waitForSync = false to not wait for sync during addProject()
560
+ * @param {{ waitForSync?: boolean }} [opts] Set opts.waitForSync = false to not wait for sync during addProject()
583
561
  * @returns {Promise<string>}
584
562
  */
585
563
  addProject = async (
@@ -733,9 +711,7 @@ export class MapeoManager extends TypedEmitter {
733
711
  }
734
712
 
735
713
  /**
736
- * @typedef {Exclude<
737
- * import('./schema/client.js').DeviceInfoParam['deviceType'],
738
- * 'selfHostedServer'>} RPCDeviceType
714
+ * @typedef {import('./schema/client.js').DeviceInfoParam['deviceType']} RPCDeviceType
739
715
  */
740
716
 
741
717
  /**
@@ -746,10 +722,10 @@ export class MapeoManager extends TypedEmitter {
746
722
  async setDeviceInfo(deviceInfo) {
747
723
  const values = { deviceId: this.#deviceId, deviceInfo }
748
724
  this.#db
749
- .insert(localDeviceInfoTable)
725
+ .insert(deviceSettingsTable)
750
726
  .values(values)
751
727
  .onConflictDoUpdate({
752
- target: localDeviceInfoTable.deviceId,
728
+ target: deviceSettingsTable.deviceId,
753
729
  set: values,
754
730
  })
755
731
  .run()
@@ -762,13 +738,22 @@ export class MapeoManager extends TypedEmitter {
762
738
  })
763
739
  )
764
740
 
765
- await Promise.all(
766
- this.#localPeers.peers
767
- .filter(({ status }) => status === 'connected')
768
- .map((peer) =>
769
- this.#localPeers.sendDeviceInfo(peer.deviceId, deviceInfo)
770
- )
771
- )
741
+ if (deviceInfo.deviceType !== 'selfHostedServer') {
742
+ // We have to make a copy of this because TypeScript can't guarantee that
743
+ // `deviceInfo` won't be mutated by the time it gets to the
744
+ // `sendDeviceInfo` call below.
745
+ const deviceInfoToSend = {
746
+ ...deviceInfo,
747
+ deviceType: deviceInfo.deviceType,
748
+ }
749
+ await Promise.all(
750
+ this.#localPeers.peers
751
+ .filter(({ status }) => status === 'connected')
752
+ .map((peer) =>
753
+ this.#localPeers.sendDeviceInfo(peer.deviceId, deviceInfoToSend)
754
+ )
755
+ )
756
+ }
772
757
 
773
758
  this.#l.log('set device info %o', deviceInfo)
774
759
  }
@@ -784,8 +769,8 @@ export class MapeoManager extends TypedEmitter {
784
769
  getDeviceInfo() {
785
770
  const row = this.#db
786
771
  .select()
787
- .from(localDeviceInfoTable)
788
- .where(eq(localDeviceInfoTable.deviceId, this.#deviceId))
772
+ .from(deviceSettingsTable)
773
+ .where(eq(deviceSettingsTable.deviceId, this.#deviceId))
789
774
  .get()
790
775
  return {
791
776
  deviceId: this.#deviceId,
@@ -794,6 +779,50 @@ export class MapeoManager extends TypedEmitter {
794
779
  }
795
780
  }
796
781
 
782
+ /**
783
+ * Set whether this device is an archive device. Archive devices will download
784
+ * all media during sync, where-as non-archive devices will not download media
785
+ * original variants, and only download preview and thumbnail variants.
786
+ * @param {boolean} isArchiveDevice
787
+ * @returns {void}
788
+ */
789
+ setIsArchiveDevice(isArchiveDevice) {
790
+ const values = { deviceId: this.#deviceId, isArchiveDevice }
791
+ const result = this.#db
792
+ .insert(deviceSettingsTable)
793
+ .values(values)
794
+ .onConflictDoUpdate({
795
+ target: deviceSettingsTable.deviceId,
796
+ set: values,
797
+ })
798
+ .run()
799
+ if (!result || result.changes === 0) {
800
+ throw new Error('Failed to set isArchiveDevice')
801
+ }
802
+ for (const project of this.#activeProjects.values()) {
803
+ project[kSetIsArchiveDevice](isArchiveDevice)
804
+ }
805
+ }
806
+
807
+ /**
808
+ * Get whether this device is an archive device. Archive devices will download
809
+ * all media during sync, where-as non-archive devices will not download media
810
+ * original variants, and only download preview and thumbnail variants.
811
+ * @returns {boolean} isArchiveDevice
812
+ */
813
+ getIsArchiveDevice() {
814
+ const row = this.#db
815
+ .select()
816
+ .from(deviceSettingsTable)
817
+ .where(eq(deviceSettingsTable.deviceId, this.#deviceId))
818
+ .get()
819
+ if (typeof row?.isArchiveDevice === 'boolean') {
820
+ return row.isArchiveDevice
821
+ } else {
822
+ return true
823
+ }
824
+ }
825
+
797
826
  /**
798
827
  * @returns {InviteApi}
799
828
  */
@@ -868,7 +897,7 @@ export class MapeoManager extends TypedEmitter {
868
897
  .get()
869
898
 
870
899
  if (!row) {
871
- throw new Error(`NotFound: project ID ${projectPublicId} not found`)
900
+ throw new NotFoundError(`Project ID ${projectPublicId} not found`)
872
901
  }
873
902
 
874
903
  const { keysCipher, projectId, projectInfo } = row