@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
package/src/sync/sync-api.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { TypedEmitter } from 'tiny-typed-emitter'
|
|
2
|
+
import WebSocket from 'ws'
|
|
2
3
|
import { SyncState } from './sync-state.js'
|
|
3
4
|
import { PeerSyncController } from './peer-sync-controller.js'
|
|
4
5
|
import { Logger } from '../logger.js'
|
|
@@ -8,15 +9,25 @@ import {
|
|
|
8
9
|
PRESYNC_NAMESPACES,
|
|
9
10
|
} from '../constants.js'
|
|
10
11
|
import { ExhaustivenessError, assert, keyToId, noop } from '../utils.js'
|
|
12
|
+
import { getOwn } from '../lib/get-own.js'
|
|
13
|
+
import { wsCoreReplicator } from '../lib/ws-core-replicator.js'
|
|
11
14
|
import { NO_ROLE_ID } from '../roles.js'
|
|
12
15
|
/** @import { CoreOwnership as CoreOwnershipDoc } from '@comapeo/schema' */
|
|
16
|
+
/** @import * as http from 'node:http' */
|
|
13
17
|
/** @import { CoreOwnership } from '../core-ownership.js' */
|
|
14
18
|
/** @import { OpenedNoiseStream } from '../lib/noise-secret-stream-helpers.js' */
|
|
19
|
+
/** @import { BlobFilter, ReplicationStream } from '../types.js' */
|
|
15
20
|
|
|
16
21
|
export const kHandleDiscoveryKey = Symbol('handle discovery key')
|
|
17
22
|
export const kSyncState = Symbol('sync state')
|
|
18
23
|
export const kRequestFullStop = Symbol('background')
|
|
19
24
|
export const kRescindFullStopRequest = Symbol('foreground')
|
|
25
|
+
export const kWaitForInitialSyncWithPeer = Symbol(
|
|
26
|
+
'wait for initial sync with peer'
|
|
27
|
+
)
|
|
28
|
+
export const kSetBlobDownloadFilter = Symbol('set isArchiveDevice')
|
|
29
|
+
export const kAddBlobWantRange = Symbol('add blob want range')
|
|
30
|
+
export const kClearBlobWantRanges = Symbol('clear blob want ranges')
|
|
20
31
|
|
|
21
32
|
/**
|
|
22
33
|
* @typedef {'initial' | 'full'} SyncType
|
|
@@ -65,6 +76,7 @@ export class SyncApi extends TypedEmitter {
|
|
|
65
76
|
/** @type {Map<string, PeerSyncController>} */
|
|
66
77
|
#pscByPeerId = new Map()
|
|
67
78
|
#wantsToSyncData = false
|
|
79
|
+
#wantsToConnectToServers = false
|
|
68
80
|
#hasRequestedFullStop = false
|
|
69
81
|
/** @type {SyncEnabledState} */
|
|
70
82
|
#previousSyncEnabledState = 'none'
|
|
@@ -77,22 +89,41 @@ export class SyncApi extends TypedEmitter {
|
|
|
77
89
|
/** @type {Map<import('protomux'), Set<Buffer>>} */
|
|
78
90
|
#pendingDiscoveryKeys = new Map()
|
|
79
91
|
#l
|
|
92
|
+
#getServerWebsocketUrls
|
|
93
|
+
#getReplicationStream
|
|
94
|
+
/** @type {Map<string, WebSocket>} */
|
|
95
|
+
#serverWebsockets = new Map()
|
|
96
|
+
/** @type {null | BlobFilter} */
|
|
97
|
+
#blobDownloadFilter = null
|
|
80
98
|
|
|
81
99
|
/**
|
|
82
|
-
*
|
|
83
100
|
* @param {object} opts
|
|
84
101
|
* @param {import('../core-manager/index.js').CoreManager} opts.coreManager
|
|
85
102
|
* @param {CoreOwnership} opts.coreOwnership
|
|
86
103
|
* @param {import('../roles.js').Roles} opts.roles
|
|
104
|
+
* @param {() => Promise<Iterable<string>>} opts.getServerWebsocketUrls
|
|
105
|
+
* @param {() => ReplicationStream} opts.getReplicationStream
|
|
106
|
+
* @param {null | BlobFilter} opts.blobDownloadFilter
|
|
87
107
|
* @param {number} [opts.throttleMs]
|
|
88
108
|
* @param {Logger} [opts.logger]
|
|
89
109
|
*/
|
|
90
|
-
constructor({
|
|
110
|
+
constructor({
|
|
111
|
+
coreManager,
|
|
112
|
+
throttleMs = 200,
|
|
113
|
+
roles,
|
|
114
|
+
getServerWebsocketUrls,
|
|
115
|
+
getReplicationStream,
|
|
116
|
+
logger,
|
|
117
|
+
coreOwnership,
|
|
118
|
+
blobDownloadFilter,
|
|
119
|
+
}) {
|
|
91
120
|
super()
|
|
92
121
|
this.#l = Logger.create('syncApi', logger)
|
|
93
122
|
this.#coreManager = coreManager
|
|
94
123
|
this.#coreOwnership = coreOwnership
|
|
95
124
|
this.#roles = roles
|
|
125
|
+
this.#getServerWebsocketUrls = getServerWebsocketUrls
|
|
126
|
+
this.#getReplicationStream = getReplicationStream
|
|
96
127
|
this[kSyncState] = new SyncState({
|
|
97
128
|
coreManager,
|
|
98
129
|
throttleMs,
|
|
@@ -104,6 +135,8 @@ export class SyncApi extends TypedEmitter {
|
|
|
104
135
|
this.#updateState(namespaceSyncState)
|
|
105
136
|
})
|
|
106
137
|
|
|
138
|
+
this[kSetBlobDownloadFilter](blobDownloadFilter)
|
|
139
|
+
|
|
107
140
|
this.#coreManager.creatorCore.on('peer-add', this.#handlePeerAdd)
|
|
108
141
|
this.#coreManager.creatorCore.on('peer-remove', this.#handlePeerDisconnect)
|
|
109
142
|
|
|
@@ -123,6 +156,37 @@ export class SyncApi extends TypedEmitter {
|
|
|
123
156
|
.catch(noop)
|
|
124
157
|
}
|
|
125
158
|
|
|
159
|
+
/** @param {import('../types.js').BlobFilter | null} blobDownloadFilter */
|
|
160
|
+
[kSetBlobDownloadFilter](blobDownloadFilter) {
|
|
161
|
+
this.#blobDownloadFilter = blobDownloadFilter
|
|
162
|
+
if (!blobDownloadFilter) return // No download intents = intend to download everything
|
|
163
|
+
for (const peer of this.#coreManager.creatorCore.peers) {
|
|
164
|
+
this.#coreManager.sendDownloadIntents(blobDownloadFilter, peer)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Add some blob blocks this peer wants.
|
|
170
|
+
*
|
|
171
|
+
* @param {string} peerId
|
|
172
|
+
* @param {number} start
|
|
173
|
+
* @param {number} length
|
|
174
|
+
* @returns {void}
|
|
175
|
+
*/
|
|
176
|
+
[kAddBlobWantRange](peerId, start, length) {
|
|
177
|
+
this[kSyncState].addBlobWantRange(peerId, start, length)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Clear the blob blocks this peer wants.
|
|
182
|
+
*
|
|
183
|
+
* @param {string} peerId
|
|
184
|
+
* @returns {void}
|
|
185
|
+
*/
|
|
186
|
+
[kClearBlobWantRanges](peerId) {
|
|
187
|
+
this[kSyncState].clearBlobWantRanges(peerId)
|
|
188
|
+
}
|
|
189
|
+
|
|
126
190
|
/** @type {import('../local-peers.js').LocalPeersEvents['discovery-key']} */
|
|
127
191
|
[kHandleDiscoveryKey](discoveryKey, protomux) {
|
|
128
192
|
const peerSyncController = this.#peerSyncControllers.get(protomux)
|
|
@@ -274,6 +338,74 @@ export class SyncApi extends TypedEmitter {
|
|
|
274
338
|
this.emit('sync-state', this.#getState(namespaceSyncState))
|
|
275
339
|
}
|
|
276
340
|
|
|
341
|
+
/**
|
|
342
|
+
* @returns {void}
|
|
343
|
+
*/
|
|
344
|
+
connectServers() {
|
|
345
|
+
this.#wantsToConnectToServers = true
|
|
346
|
+
|
|
347
|
+
this.#getServerWebsocketUrls()
|
|
348
|
+
.then((urls) => {
|
|
349
|
+
const hasDisconnectedSinceWebsocketUrlsRequestFinished =
|
|
350
|
+
!this.#wantsToConnectToServers
|
|
351
|
+
if (hasDisconnectedSinceWebsocketUrlsRequestFinished) return
|
|
352
|
+
|
|
353
|
+
for (const url of urls) {
|
|
354
|
+
const existingWebsocket = this.#serverWebsockets.get(url)
|
|
355
|
+
if (
|
|
356
|
+
existingWebsocket &&
|
|
357
|
+
(existingWebsocket.readyState === WebSocket.OPEN ||
|
|
358
|
+
existingWebsocket.readyState === WebSocket.CONNECTING)
|
|
359
|
+
) {
|
|
360
|
+
continue
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const websocket = new WebSocket(url)
|
|
364
|
+
|
|
365
|
+
/** @param {Error} err */
|
|
366
|
+
const onWebsocketError = (err) => {
|
|
367
|
+
this.#l.log('Ignoring WebSocket error to %s: %o', url, err)
|
|
368
|
+
}
|
|
369
|
+
websocket.on('error', onWebsocketError)
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* @param {unknown} _req
|
|
373
|
+
* @param {http.IncomingMessage} res
|
|
374
|
+
*/
|
|
375
|
+
const onWebsocketUnexpectedResponse = (_req, res) => {
|
|
376
|
+
this.#l.log(
|
|
377
|
+
'Ignoring unexpected %d WebSocket response to %s',
|
|
378
|
+
res.statusCode,
|
|
379
|
+
url
|
|
380
|
+
)
|
|
381
|
+
}
|
|
382
|
+
websocket.on('unexpected-response', onWebsocketUnexpectedResponse)
|
|
383
|
+
|
|
384
|
+
const replicationStream = this.#getReplicationStream()
|
|
385
|
+
wsCoreReplicator(websocket, replicationStream)
|
|
386
|
+
|
|
387
|
+
this.#serverWebsockets.set(url, websocket)
|
|
388
|
+
websocket.once('close', () => {
|
|
389
|
+
websocket.off('error', onWebsocketError)
|
|
390
|
+
websocket.off('unexpected-response', onWebsocketUnexpectedResponse)
|
|
391
|
+
this.#serverWebsockets.delete(url)
|
|
392
|
+
})
|
|
393
|
+
}
|
|
394
|
+
})
|
|
395
|
+
.catch(noop)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* @returns {void}
|
|
400
|
+
*/
|
|
401
|
+
disconnectServers() {
|
|
402
|
+
for (const websocket of this.#serverWebsockets.values()) {
|
|
403
|
+
websocket.close()
|
|
404
|
+
}
|
|
405
|
+
this.#serverWebsockets.clear()
|
|
406
|
+
this.#wantsToConnectToServers = false
|
|
407
|
+
}
|
|
408
|
+
|
|
277
409
|
/**
|
|
278
410
|
* Start syncing data cores.
|
|
279
411
|
*
|
|
@@ -348,6 +480,40 @@ export class SyncApi extends TypedEmitter {
|
|
|
348
480
|
})
|
|
349
481
|
}
|
|
350
482
|
|
|
483
|
+
/**
|
|
484
|
+
* @param {string} deviceId
|
|
485
|
+
* @param {AbortSignal} abortSignal
|
|
486
|
+
* @returns {Promise<void>}
|
|
487
|
+
*/
|
|
488
|
+
async [kWaitForInitialSyncWithPeer](deviceId, abortSignal) {
|
|
489
|
+
abortSignal.throwIfAborted()
|
|
490
|
+
|
|
491
|
+
const state = this[kSyncState].getState()
|
|
492
|
+
if (isInitiallySyncedWithPeer(state, deviceId)) return
|
|
493
|
+
|
|
494
|
+
return new Promise((resolve, reject) => {
|
|
495
|
+
/** @param {import('./sync-state.js').State} state */
|
|
496
|
+
const onState = (state) => {
|
|
497
|
+
if (isInitiallySyncedWithPeer(state, deviceId)) {
|
|
498
|
+
cleanup()
|
|
499
|
+
resolve()
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
const onAbort = () => {
|
|
503
|
+
cleanup()
|
|
504
|
+
reject(abortSignal.reason)
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const cleanup = () => {
|
|
508
|
+
this[kSyncState].off('state', onState)
|
|
509
|
+
abortSignal.removeEventListener('abort', onAbort)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
this[kSyncState].on('state', onState)
|
|
513
|
+
abortSignal.addEventListener('abort', onAbort)
|
|
514
|
+
})
|
|
515
|
+
}
|
|
516
|
+
|
|
351
517
|
#clearAutostopDataSyncTimeoutIfExists() {
|
|
352
518
|
if (this.#autostopDataSyncTimeout) {
|
|
353
519
|
clearTimeout(this.#autostopDataSyncTimeout)
|
|
@@ -363,7 +529,7 @@ export class SyncApi extends TypedEmitter {
|
|
|
363
529
|
* will then handle validation of role records to ensure that the peer is
|
|
364
530
|
* actually still part of the project.
|
|
365
531
|
*
|
|
366
|
-
* @param {{ protomux: import('protomux')<OpenedNoiseStream> }} peer
|
|
532
|
+
* @param {import('../types.js').HypercorePeer & { protomux: import('protomux')<OpenedNoiseStream> }} peer
|
|
367
533
|
*/
|
|
368
534
|
#handlePeerAdd = (peer) => {
|
|
369
535
|
const { protomux } = peer
|
|
@@ -374,6 +540,9 @@ export class SyncApi extends TypedEmitter {
|
|
|
374
540
|
)
|
|
375
541
|
return
|
|
376
542
|
}
|
|
543
|
+
if (this.#blobDownloadFilter) {
|
|
544
|
+
this.#coreManager.sendDownloadIntents(this.#blobDownloadFilter, peer)
|
|
545
|
+
}
|
|
377
546
|
const peerSyncController = new PeerSyncController({
|
|
378
547
|
protomux,
|
|
379
548
|
coreManager: this.#coreManager,
|
|
@@ -524,6 +693,31 @@ function isSynced(state, type, peerSyncControllers) {
|
|
|
524
693
|
return true
|
|
525
694
|
}
|
|
526
695
|
|
|
696
|
+
/**
|
|
697
|
+
* @param {import('./sync-state.js').State} state
|
|
698
|
+
* @param {string} peerId
|
|
699
|
+
*/
|
|
700
|
+
function isInitiallySyncedWithPeer(state, peerId) {
|
|
701
|
+
for (const ns of PRESYNC_NAMESPACES) {
|
|
702
|
+
const remoteDeviceSyncState = getOwn(state[ns].remoteStates, peerId)
|
|
703
|
+
if (!remoteDeviceSyncState) return false
|
|
704
|
+
|
|
705
|
+
switch (remoteDeviceSyncState.status) {
|
|
706
|
+
case 'starting':
|
|
707
|
+
return false
|
|
708
|
+
case 'started':
|
|
709
|
+
case 'stopped': {
|
|
710
|
+
const { want, wanted } = remoteDeviceSyncState
|
|
711
|
+
if (want || wanted) return false
|
|
712
|
+
break
|
|
713
|
+
}
|
|
714
|
+
default:
|
|
715
|
+
throw new ExhaustivenessError(remoteDeviceSyncState.status)
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return true
|
|
719
|
+
}
|
|
720
|
+
|
|
527
721
|
/**
|
|
528
722
|
* @param {import('./sync-state.js').State} namespaceSyncState
|
|
529
723
|
* @param {Iterable<PeerSyncController>} peerSyncControllers
|
package/src/sync/sync-state.js
CHANGED
|
@@ -68,6 +68,24 @@ export class SyncState extends TypedEmitter {
|
|
|
68
68
|
])
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* @param {string} peerId
|
|
73
|
+
* @param {number} start
|
|
74
|
+
* @param {number} length
|
|
75
|
+
* @returns {void}
|
|
76
|
+
*/
|
|
77
|
+
addBlobWantRange(peerId, start, length) {
|
|
78
|
+
this.#syncStates.blob.addWantRange(peerId, start, length)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {string} peerId
|
|
83
|
+
* @returns {void}
|
|
84
|
+
*/
|
|
85
|
+
clearBlobWantRanges(peerId) {
|
|
86
|
+
this.#syncStates.blob.clearWantRanges(peerId)
|
|
87
|
+
}
|
|
88
|
+
|
|
71
89
|
#handleUpdate = () => {
|
|
72
90
|
this.emit('state', this.getState())
|
|
73
91
|
}
|
package/src/translation-api.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { and, sql } from 'drizzle-orm'
|
|
2
2
|
import { kCreateWithDocId, kSelect } from './datatype/index.js'
|
|
3
3
|
import { hashObject } from './utils.js'
|
|
4
|
-
import {
|
|
4
|
+
import { nullIfNotFound } from './errors.js'
|
|
5
5
|
import { omit } from './lib/omit.js'
|
|
6
6
|
/** @import { Translation, TranslationValue } from '@comapeo/schema' */
|
|
7
7
|
/** @import { SetOptional } from 'type-fest' */
|
|
@@ -50,15 +50,11 @@ export default class TranslationApi {
|
|
|
50
50
|
async put(value) {
|
|
51
51
|
const identifiers = omit(value, ['message'])
|
|
52
52
|
const docId = hashObject(identifiers)
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
const doc = await this.#dataType.getByDocId(docId).catch(nullIfNotFound)
|
|
54
|
+
if (doc) {
|
|
55
55
|
return await this.#dataType.update(doc.versionId, value)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return await this.#dataType[kCreateWithDocId](docId, value)
|
|
59
|
-
} else {
|
|
60
|
-
throw new Error(`Error on translation ${e}`)
|
|
61
|
-
}
|
|
56
|
+
} else {
|
|
57
|
+
return await this.#dataType[kCreateWithDocId](docId, value)
|
|
62
58
|
}
|
|
63
59
|
}
|
|
64
60
|
|
package/src/types.ts
CHANGED
|
@@ -14,6 +14,8 @@ import { Duplex } from 'streamx'
|
|
|
14
14
|
import RandomAccessStorage from 'random-access-storage'
|
|
15
15
|
import { DefaultListener, ListenerSignature } from 'tiny-typed-emitter'
|
|
16
16
|
import type { NAMESPACES } from './constants.js'
|
|
17
|
+
import type { Readable } from 'stream'
|
|
18
|
+
import type { HyperdriveEntry } from 'hyperdrive'
|
|
17
19
|
|
|
18
20
|
export type Namespace = (typeof NAMESPACES)[number]
|
|
19
21
|
|
|
@@ -41,12 +43,13 @@ export type BlobId = Simplify<
|
|
|
41
43
|
}>
|
|
42
44
|
>
|
|
43
45
|
|
|
44
|
-
type ArrayAtLeastOne<T> = [T, ...T[]]
|
|
45
|
-
|
|
46
46
|
export type BlobFilter = RequireAtLeastOne<{
|
|
47
|
-
[KeyType in BlobType]:
|
|
47
|
+
[KeyType in BlobType]: Array<BlobVariant<KeyType>>
|
|
48
48
|
}>
|
|
49
49
|
|
|
50
|
+
/** Map of blob types to array of blob variants */
|
|
51
|
+
export type GenericBlobFilter = Record<string, string[]>
|
|
52
|
+
|
|
50
53
|
export type MapeoDocMap = {
|
|
51
54
|
[K in MapeoDoc['schemaName']]: Extract<MapeoDoc, { schemaName: K }>
|
|
52
55
|
}
|
|
@@ -146,3 +149,9 @@ export type DefaultEmitterEvents<
|
|
|
146
149
|
newListener: (event: keyof L, listener: L[keyof L]) => void
|
|
147
150
|
removeListener: (event: keyof L, listener: L[keyof L]) => void
|
|
148
151
|
}
|
|
152
|
+
|
|
153
|
+
export type BlobStoreEntriesStream = Readable & {
|
|
154
|
+
[Symbol.asyncIterator](): AsyncIterableIterator<
|
|
155
|
+
HyperdriveEntry & { driveId: string }
|
|
156
|
+
>
|
|
157
|
+
}
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reduce multiple states into one. Factored out for unit testing because I
|
|
3
|
-
* don't trust my coding. Probably a smarter way to do this, but this works.
|
|
4
|
-
*
|
|
5
|
-
* @param {Iterable<{ state: BlobDownloadState | BlobDownloadStateError }>} liveDownloads
|
|
6
|
-
* @param {{ signal?: AbortSignal }} options
|
|
7
|
-
* @returns {BlobDownloadState | BlobDownloadStateError}
|
|
8
|
-
*/
|
|
9
|
-
export function combineStates(liveDownloads: Iterable<{
|
|
10
|
-
state: BlobDownloadState | BlobDownloadStateError;
|
|
11
|
-
}>, { signal }?: {
|
|
12
|
-
signal?: AbortSignal;
|
|
13
|
-
}): BlobDownloadState | BlobDownloadStateError;
|
|
14
|
-
/**
|
|
15
|
-
* @typedef {object} BlobDownloadState
|
|
16
|
-
* @property {number} haveCount The number of files already downloaded
|
|
17
|
-
* @property {number} haveBytes The bytes already downloaded
|
|
18
|
-
* @property {number} wantCount The number of files pending download
|
|
19
|
-
* @property {number} wantBytes The bytes pending download
|
|
20
|
-
* @property {null} error If status = 'error' then this will be an Error object
|
|
21
|
-
* @property {'checking' | 'downloading' | 'downloaded' | 'aborted'} status
|
|
22
|
-
*/
|
|
23
|
-
/** @typedef {Omit<BlobDownloadState, 'error' | 'status'> & { status: 'error', error: Error }} BlobDownloadStateError */
|
|
24
|
-
/**
|
|
25
|
-
* @typedef {object} BlobDownloadEvents
|
|
26
|
-
* @property {(state: BlobDownloadState | BlobDownloadStateError ) => void} state Emitted with the current download state whenever it changes (not emitted during initial 'checking' status)
|
|
27
|
-
*/
|
|
28
|
-
/**
|
|
29
|
-
* LiveDownload class
|
|
30
|
-
* @extends {TypedEmitter<BlobDownloadEvents>}
|
|
31
|
-
*/
|
|
32
|
-
export class LiveDownload extends TypedEmitter<BlobDownloadEvents> {
|
|
33
|
-
/**
|
|
34
|
-
* Like drive.download() but 'live', and for multiple drives
|
|
35
|
-
* @param {Iterable<import('hyperdrive')>} drives
|
|
36
|
-
* @param {import('./index.js').InternalDriveEmitter} emitter
|
|
37
|
-
* @param {object} options
|
|
38
|
-
* @param {import('../types.js').BlobFilter} [options.filter] Filter blobs of specific types and/or sizes to download
|
|
39
|
-
* @param {AbortSignal} [options.signal]
|
|
40
|
-
*/
|
|
41
|
-
constructor(drives: Iterable<import("hyperdrive")>, emitter: import("./index.js").InternalDriveEmitter, { filter, signal }: {
|
|
42
|
-
filter?: import("../types.js").BlobFilter | undefined;
|
|
43
|
-
signal?: AbortSignal | undefined;
|
|
44
|
-
});
|
|
45
|
-
/**
|
|
46
|
-
* @returns {BlobDownloadState | BlobDownloadStateError}
|
|
47
|
-
*/
|
|
48
|
-
get state(): BlobDownloadState | BlobDownloadStateError;
|
|
49
|
-
#private;
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* LiveDownload class
|
|
53
|
-
* @extends {TypedEmitter<BlobDownloadEvents>}
|
|
54
|
-
*/
|
|
55
|
-
export class DriveLiveDownload extends TypedEmitter<BlobDownloadEvents> {
|
|
56
|
-
/**
|
|
57
|
-
* Like drive.download() but 'live',
|
|
58
|
-
* @param {import('hyperdrive')} drive
|
|
59
|
-
* @param {object} options
|
|
60
|
-
* @param {import('../types.js').BlobFilter} [options.filter] Filter blobs of specific types and/or sizes to download
|
|
61
|
-
* @param {AbortSignal} [options.signal]
|
|
62
|
-
*/
|
|
63
|
-
constructor(drive: import("hyperdrive"), { filter, signal }?: {
|
|
64
|
-
filter?: import("../types.js").BlobFilter | undefined;
|
|
65
|
-
signal?: AbortSignal | undefined;
|
|
66
|
-
});
|
|
67
|
-
/**
|
|
68
|
-
* @returns {BlobDownloadState | BlobDownloadStateError}
|
|
69
|
-
*/
|
|
70
|
-
get state(): BlobDownloadState | BlobDownloadStateError;
|
|
71
|
-
#private;
|
|
72
|
-
}
|
|
73
|
-
export type BlobDownloadState = {
|
|
74
|
-
/**
|
|
75
|
-
* The number of files already downloaded
|
|
76
|
-
*/
|
|
77
|
-
haveCount: number;
|
|
78
|
-
/**
|
|
79
|
-
* The bytes already downloaded
|
|
80
|
-
*/
|
|
81
|
-
haveBytes: number;
|
|
82
|
-
/**
|
|
83
|
-
* The number of files pending download
|
|
84
|
-
*/
|
|
85
|
-
wantCount: number;
|
|
86
|
-
/**
|
|
87
|
-
* The bytes pending download
|
|
88
|
-
*/
|
|
89
|
-
wantBytes: number;
|
|
90
|
-
/**
|
|
91
|
-
* If status = 'error' then this will be an Error object
|
|
92
|
-
*/
|
|
93
|
-
error: null;
|
|
94
|
-
status: "checking" | "downloading" | "downloaded" | "aborted";
|
|
95
|
-
};
|
|
96
|
-
export type BlobDownloadStateError = Omit<BlobDownloadState, "error" | "status"> & {
|
|
97
|
-
status: "error";
|
|
98
|
-
error: Error;
|
|
99
|
-
};
|
|
100
|
-
export type BlobDownloadEvents = {
|
|
101
|
-
/**
|
|
102
|
-
* Emitted with the current download state whenever it changes (not emitted during initial 'checking' status)
|
|
103
|
-
*/
|
|
104
|
-
state: (state: BlobDownloadState | BlobDownloadStateError) => void;
|
|
105
|
-
};
|
|
106
|
-
import { TypedEmitter } from 'tiny-typed-emitter';
|
|
107
|
-
//# sourceMappingURL=live-download.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"live-download.d.ts","sourceRoot":"","sources":["../../src/blob-store/live-download.js"],"names":[],"mappings":"AA+RA;;;;;;;GAOG;AACH,6CAJW,QAAQ,CAAC;IAAE,KAAK,EAAE,iBAAiB,GAAG,sBAAsB,CAAA;CAAE,CAAC,eAC/D;IAAE,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,GACtB,iBAAiB,GAAG,sBAAsB,CAqCtD;AApUD;;;;;;;;GAQG;AAEH,wHAAwH;AAExH;;;GAGG;AAEH;;;GAGG;AACH;IAKE;;;;;;;OAOG;IACH,oBANW,QAAQ,CAAC,OAAO,YAAY,CAAC,CAAC,WAC9B,OAAO,YAAY,EAAE,oBAAoB,sBAEjD;QAAmD,MAAM;QAC3B,MAAM;KAAC,EAiCvC;IAED;;OAEG;IACH,wDAEC;;CACF;AAED;;;GAGG;AACH;IAaE;;;;;;OAMG;IACH,mBALW,OAAO,YAAY,CAAC,uBAE5B;QAAmD,MAAM;QAC3B,MAAM;KAAC,EAmBvC;IAED;;OAEG;IACH,wDAyBC;;CAqIF;;;;;eArRa,MAAM;;;;eACN,MAAM;;;;eACN,MAAM;;;;eACN,MAAM;;;;WACN,IAAI;YACJ,UAAU,GAAG,aAAa,GAAG,YAAY,GAAG,SAAS;;qCAGrD,IAAI,CAAC,iBAAiB,EAAE,OAAO,GAAG,QAAQ,CAAC,GAAG;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE;;;;;WAI/E,CAAC,KAAK,EAAE,iBAAiB,GAAG,sBAAsB,KAAM,IAAI;;6BApB7C,oBAAoB"}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Compare two values in constant time.
|
|
3
|
-
*
|
|
4
|
-
* Useful when you want to avoid leaking data.
|
|
5
|
-
*
|
|
6
|
-
* Like `crypto.timingSafeEqual`, but works with strings and doesn't throw if
|
|
7
|
-
* lengths differ.
|
|
8
|
-
*
|
|
9
|
-
* @template {string | NodeJS.ArrayBufferView} T
|
|
10
|
-
* @param {T} a
|
|
11
|
-
* @param {T} b
|
|
12
|
-
* @returns {boolean}
|
|
13
|
-
*/
|
|
14
|
-
export default function timingSafeEqual<T extends string | NodeJS.ArrayBufferView>(a: T, b: T): boolean;
|
|
15
|
-
//# sourceMappingURL=timing-safe-equal.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"timing-safe-equal.d.ts","sourceRoot":"","sources":["../../src/lib/timing-safe-equal.js"],"names":[],"mappings":"AAaA;;;;;;;;;;;;GAYG;AACH,wCAL+C,CAAC,SAAlC,MAAM,GAAG,MAAM,CAAC,eAAgB,KACnC,CAAC,KACD,CAAC,GACC,OAAO,CASnB"}
|