@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.
- package/dist/blob-store/downloader.d.ts +43 -0
- package/dist/blob-store/downloader.d.ts.map +1 -0
- package/dist/blob-store/entries-stream.d.ts +13 -0
- package/dist/blob-store/entries-stream.d.ts.map +1 -0
- package/dist/blob-store/hyperdrive-index.d.ts +20 -0
- package/dist/blob-store/hyperdrive-index.d.ts.map +1 -0
- package/dist/blob-store/index.d.ts +34 -29
- package/dist/blob-store/index.d.ts.map +1 -1
- package/dist/blob-store/utils.d.ts +27 -0
- package/dist/blob-store/utils.d.ts.map +1 -0
- package/dist/constants.d.ts +2 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/core-manager/index.d.ts +11 -1
- package/dist/core-manager/index.d.ts.map +1 -1
- package/dist/core-ownership.d.ts.map +1 -1
- package/dist/datastore/index.d.ts +5 -4
- package/dist/datastore/index.d.ts.map +1 -1
- package/dist/datatype/index.d.ts +5 -1
- package/dist/discovery/local-discovery.d.ts.map +1 -1
- package/dist/errors.d.ts +6 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/fastify-plugins/blobs.d.ts.map +1 -1
- package/dist/fastify-plugins/maps.d.ts.map +1 -1
- package/dist/generated/extensions.d.ts +31 -0
- package/dist/generated/extensions.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/drizzle-helpers.d.ts +6 -0
- package/dist/lib/drizzle-helpers.d.ts.map +1 -0
- package/dist/lib/error.d.ts +51 -0
- package/dist/lib/error.d.ts.map +1 -0
- package/dist/lib/get-own.d.ts +9 -0
- package/dist/lib/get-own.d.ts.map +1 -0
- package/dist/lib/is-hostname-ip-address.d.ts +17 -0
- package/dist/lib/is-hostname-ip-address.d.ts.map +1 -0
- package/dist/lib/ws-core-replicator.d.ts +11 -0
- package/dist/lib/ws-core-replicator.d.ts.map +1 -0
- package/dist/mapeo-manager.d.ts +18 -22
- package/dist/mapeo-manager.d.ts.map +1 -1
- package/dist/mapeo-project.d.ts +459 -26
- package/dist/mapeo-project.d.ts.map +1 -1
- package/dist/member-api.d.ts +44 -1
- package/dist/member-api.d.ts.map +1 -1
- package/dist/roles.d.ts.map +1 -1
- package/dist/schema/client.d.ts +17 -5
- package/dist/schema/client.d.ts.map +1 -1
- package/dist/schema/project.d.ts +212 -2
- package/dist/schema/project.d.ts.map +1 -1
- package/dist/sync/core-sync-state.d.ts +20 -15
- package/dist/sync/core-sync-state.d.ts.map +1 -1
- package/dist/sync/namespace-sync-state.d.ts +13 -1
- package/dist/sync/namespace-sync-state.d.ts.map +1 -1
- package/dist/sync/peer-sync-controller.d.ts +1 -1
- package/dist/sync/peer-sync-controller.d.ts.map +1 -1
- package/dist/sync/sync-api.d.ts +47 -2
- package/dist/sync/sync-api.d.ts.map +1 -1
- package/dist/sync/sync-state.d.ts +12 -0
- package/dist/sync/sync-state.d.ts.map +1 -1
- package/dist/translation-api.d.ts +2 -2
- package/dist/translation-api.d.ts.map +1 -1
- package/dist/types.d.ts +10 -2
- package/dist/types.d.ts.map +1 -1
- package/drizzle/client/0001_chubby_cargill.sql +12 -0
- package/drizzle/client/meta/0001_snapshot.json +208 -0
- package/drizzle/client/meta/_journal.json +7 -0
- package/drizzle/project/0001_medical_wendell_rand.sql +22 -0
- package/drizzle/project/meta/0001_snapshot.json +1267 -0
- package/drizzle/project/meta/_journal.json +7 -0
- package/package.json +14 -5
- package/src/blob-store/downloader.js +130 -0
- package/src/blob-store/entries-stream.js +81 -0
- package/src/blob-store/hyperdrive-index.js +122 -0
- package/src/blob-store/index.js +59 -117
- package/src/blob-store/utils.js +54 -0
- package/src/constants.js +4 -1
- package/src/core-manager/index.js +60 -3
- package/src/core-ownership.js +2 -4
- package/src/datastore/README.md +1 -2
- package/src/datastore/index.js +8 -8
- package/src/datatype/index.d.ts +5 -1
- package/src/datatype/index.js +22 -9
- package/src/discovery/local-discovery.js +2 -1
- package/src/errors.js +11 -2
- package/src/fastify-plugins/blobs.js +17 -1
- package/src/fastify-plugins/maps.js +2 -1
- package/src/generated/extensions.d.ts +31 -0
- package/src/generated/extensions.js +150 -0
- package/src/generated/extensions.ts +181 -0
- package/src/index.js +10 -0
- package/src/invite-api.js +1 -1
- package/src/lib/drizzle-helpers.js +79 -0
- package/src/lib/error.js +71 -0
- package/src/lib/get-own.js +10 -0
- package/src/lib/is-hostname-ip-address.js +26 -0
- package/src/lib/ws-core-replicator.js +47 -0
- package/src/mapeo-manager.js +74 -45
- package/src/mapeo-project.js +238 -58
- package/src/member-api.js +295 -2
- package/src/roles.js +38 -32
- package/src/schema/client.js +4 -3
- package/src/schema/project.js +7 -0
- package/src/sync/core-sync-state.js +39 -23
- package/src/sync/namespace-sync-state.js +22 -0
- package/src/sync/peer-sync-controller.js +1 -0
- package/src/sync/sync-api.js +197 -3
- package/src/sync/sync-state.js +18 -0
- package/src/translation-api.js +5 -9
- package/src/types.ts +12 -3
- package/dist/blob-store/live-download.d.ts +0 -107
- package/dist/blob-store/live-download.d.ts.map +0 -1
- package/dist/lib/timing-safe-equal.d.ts +0 -15
- package/dist/lib/timing-safe-equal.d.ts.map +0 -1
- package/src/blob-store/live-download.js +0 -373
- 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
|
+
}
|
package/src/mapeo-manager.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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]
|
|
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 {
|
|
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(
|
|
725
|
+
.insert(deviceSettingsTable)
|
|
750
726
|
.values(values)
|
|
751
727
|
.onConflictDoUpdate({
|
|
752
|
-
target:
|
|
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
|
-
|
|
766
|
-
this
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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(
|
|
788
|
-
.where(eq(
|
|
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
|
|
900
|
+
throw new NotFoundError(`Project ID ${projectPublicId} not found`)
|
|
872
901
|
}
|
|
873
902
|
|
|
874
903
|
const { keysCipher, projectId, projectInfo } = row
|