@comapeo/core 5.5.0 → 6.0.1
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-api.d.ts.map +1 -1
- package/dist/blob-store/downloader.d.ts.map +1 -1
- package/dist/blob-store/hyperdrive-index.d.ts.map +1 -1
- package/dist/blob-store/index.d.ts.map +1 -1
- package/dist/core-manager/bitfield-rle.d.ts.map +1 -1
- package/dist/core-manager/core-index.d.ts.map +1 -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.map +1 -1
- package/dist/datatype/index.d.ts +7 -0
- package/dist/datatype/index.d.ts.map +1 -1
- package/dist/discovery/local-discovery.d.ts.map +1 -1
- package/dist/errors.d.ts +437 -35
- package/dist/errors.d.ts.map +1 -1
- package/dist/fastify-plugins/blobs.d.ts.map +1 -1
- package/dist/fastify-plugins/icons.d.ts.map +1 -1
- package/dist/fastify-plugins/maps.d.ts.map +1 -1
- package/dist/generated/rpc.d.ts +1 -0
- package/dist/generated/rpc.d.ts.map +1 -1
- package/dist/icon-api.d.ts +0 -1
- package/dist/icon-api.d.ts.map +1 -1
- package/dist/import-categories.d.ts.map +1 -1
- package/dist/index-writer/index.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/intl/parse-bcp-47.d.ts.map +1 -1
- package/dist/invite/invite-api.d.ts.map +1 -1
- package/dist/lib/drizzle-helpers.d.ts.map +1 -1
- package/dist/lib/hypercore-helpers.d.ts.map +1 -1
- package/dist/lib/key-by.d.ts.map +1 -1
- package/dist/local-peers.d.ts +0 -14
- package/dist/local-peers.d.ts.map +1 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/mapeo-manager.d.ts +2 -1
- package/dist/mapeo-manager.d.ts.map +1 -1
- package/dist/mapeo-project.d.ts +1 -3
- package/dist/mapeo-project.d.ts.map +1 -1
- package/dist/member-api.d.ts +42 -7
- package/dist/member-api.d.ts.map +1 -1
- package/dist/roles.d.ts.map +1 -1
- package/dist/schema/json-schema-to-drizzle.d.ts.map +1 -1
- package/dist/schema.d.ts +2 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/sync/core-sync-state.d.ts.map +1 -1
- package/dist/sync/peer-sync-controller.d.ts.map +1 -1
- package/dist/sync/sync-api.d.ts.map +1 -1
- package/dist/utils.d.ts +8 -10
- package/dist/utils.d.ts.map +1 -1
- package/package.json +19 -3
- package/src/blob-api.js +24 -4
- package/src/blob-store/downloader.js +7 -6
- package/src/blob-store/entries-stream.js +1 -1
- package/src/blob-store/hyperdrive-index.js +3 -5
- package/src/blob-store/index.js +15 -20
- package/src/core-manager/bitfield-rle.js +2 -1
- package/src/core-manager/core-index.js +2 -1
- package/src/core-manager/index.js +9 -10
- package/src/core-ownership.js +7 -3
- package/src/datastore/index.js +13 -9
- package/src/datatype/index.js +28 -5
- package/src/discovery/local-discovery.js +8 -7
- package/src/errors.js +530 -62
- package/src/fastify-controller.js +3 -3
- package/src/fastify-plugins/blobs.js +21 -14
- package/src/fastify-plugins/icons.js +18 -9
- package/src/fastify-plugins/maps.js +6 -5
- package/src/generated/rpc.d.ts +1 -0
- package/src/generated/rpc.js +12 -1
- package/src/generated/rpc.ts +13 -0
- package/src/icon-api.js +15 -7
- package/src/import-categories.js +6 -7
- package/src/index-writer/index.js +3 -2
- package/src/index.js +1 -0
- package/src/intl/parse-bcp-47.js +2 -1
- package/src/invite/invite-api.js +26 -20
- package/src/lib/drizzle-helpers.js +54 -39
- package/src/lib/hypercore-helpers.js +4 -2
- package/src/lib/key-by.js +3 -1
- package/src/local-peers.js +39 -46
- package/src/logger.js +2 -1
- package/src/mapeo-manager.js +36 -23
- package/src/mapeo-project.js +68 -61
- package/src/member-api.js +177 -96
- package/src/roles.js +11 -10
- package/src/schema/json-schema-to-drizzle.js +13 -4
- package/src/schema.js +1 -0
- package/src/sync/core-sync-state.js +2 -1
- package/src/sync/peer-sync-controller.js +4 -3
- package/src/sync/sync-api.js +9 -9
- package/src/translation-api.js +2 -2
- package/src/utils.js +56 -41
- package/dist/lib/error.d.ts +0 -51
- package/dist/lib/error.d.ts.map +0 -1
- package/src/lib/error.js +0 -71
package/src/blob-api.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from 'node:fs'
|
|
|
2
2
|
// @ts-expect-error - pipelinePromise missing from streamx types
|
|
3
3
|
import { Transform, pipelinePromise as pipeline } from 'streamx'
|
|
4
4
|
import { createHash, randomBytes } from 'node:crypto'
|
|
5
|
+
import { BlobReadError, UnsupportedMimeTypeError } from './errors.js'
|
|
5
6
|
/** @import { BlobId, BlobType } from './types.js' */
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -57,8 +58,13 @@ export class BlobApi {
|
|
|
57
58
|
{ type, variant: 'original', name },
|
|
58
59
|
{ metadata }
|
|
59
60
|
)
|
|
61
|
+
|
|
60
62
|
const writePromises = [
|
|
61
|
-
pipeline(fs.createReadStream(original), hashTransform(hash), ws)
|
|
63
|
+
pipeline(fs.createReadStream(original), hashTransform(hash), ws).catch(
|
|
64
|
+
(/** @type {Error} */ e) => {
|
|
65
|
+
throw new BlobReadError(original, { cause: e })
|
|
66
|
+
}
|
|
67
|
+
),
|
|
62
68
|
]
|
|
63
69
|
|
|
64
70
|
if (preview) {
|
|
@@ -66,7 +72,14 @@ export class BlobApi {
|
|
|
66
72
|
{ type, variant: 'preview', name },
|
|
67
73
|
{ metadata }
|
|
68
74
|
)
|
|
69
|
-
|
|
75
|
+
|
|
76
|
+
writePromises.push(
|
|
77
|
+
pipeline(fs.createReadStream(preview), ws).catch(
|
|
78
|
+
(/** @type {Error} */ e) => {
|
|
79
|
+
throw new BlobReadError(preview, { cause: e })
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
)
|
|
70
83
|
}
|
|
71
84
|
|
|
72
85
|
if (thumbnail) {
|
|
@@ -74,7 +87,14 @@ export class BlobApi {
|
|
|
74
87
|
{ type, variant: 'thumbnail', name },
|
|
75
88
|
{ metadata }
|
|
76
89
|
)
|
|
77
|
-
|
|
90
|
+
|
|
91
|
+
writePromises.push(
|
|
92
|
+
pipeline(fs.createReadStream(thumbnail), ws).catch(
|
|
93
|
+
(/** @type {Error} */ e) => {
|
|
94
|
+
throw new BlobReadError(thumbnail, { cause: e })
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
)
|
|
78
98
|
}
|
|
79
99
|
|
|
80
100
|
await Promise.all(writePromises)
|
|
@@ -109,5 +129,5 @@ function getType(mimeType) {
|
|
|
109
129
|
if (mimeType.startsWith('video')) return 'video'
|
|
110
130
|
if (mimeType.startsWith('audio')) return 'audio'
|
|
111
131
|
|
|
112
|
-
throw new
|
|
132
|
+
throw new UnsupportedMimeTypeError({ mimeType })
|
|
113
133
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { TypedEmitter } from 'tiny-typed-emitter'
|
|
2
2
|
import { createEntriesStream } from './entries-stream.js'
|
|
3
3
|
import { filePathMatchesFilter } from './utils.js'
|
|
4
|
+
import { DriveNotFoundError, UnexpectedEndOfStreamError } from '../errors.js'
|
|
4
5
|
|
|
5
6
|
/** @import { BlobFilter } from '../types.js' */
|
|
6
7
|
/** @import { THyperdriveIndex } from './hyperdrive-index.js' */
|
|
@@ -74,13 +75,13 @@ export class Downloader extends TypedEmitter {
|
|
|
74
75
|
if (!this.#shouldDownloadFile(filePath)) continue
|
|
75
76
|
const drive = this.#driveIndex.get(driveId)
|
|
76
77
|
// ERROR HANDLING: this is unexpected and should not happen
|
|
77
|
-
if (!drive) throw new
|
|
78
|
+
if (!drive) throw new DriveNotFoundError({ driveId })
|
|
78
79
|
const blobs = await drive.getBlobs()
|
|
79
80
|
this.#ac.signal.throwIfAborted()
|
|
80
81
|
await this.#processEntry(blobs.core, blob)
|
|
81
82
|
this.#ac.signal.throwIfAborted()
|
|
82
83
|
}
|
|
83
|
-
throw new
|
|
84
|
+
throw new UnexpectedEndOfStreamError()
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
/**
|
|
@@ -112,11 +113,11 @@ export class Downloader extends TypedEmitter {
|
|
|
112
113
|
this.#ac.abort()
|
|
113
114
|
}
|
|
114
115
|
|
|
115
|
-
/** @param {Error}
|
|
116
|
-
#handleError = (
|
|
116
|
+
/** @param {Error} err */
|
|
117
|
+
#handleError = (err) => {
|
|
117
118
|
if (this.#ac.signal.aborted) return
|
|
118
|
-
this.emit('error',
|
|
119
|
-
this.#ac.abort(
|
|
119
|
+
this.emit('error', err)
|
|
120
|
+
this.#ac.abort(err)
|
|
120
121
|
}
|
|
121
122
|
|
|
122
123
|
#handleAbort = () => {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import b4a from 'b4a'
|
|
2
2
|
import { discoveryKey } from 'hypercore-crypto'
|
|
3
3
|
import Hyperdrive from 'hyperdrive'
|
|
4
|
-
import util from 'node:util'
|
|
5
4
|
import { TypedEmitter } from 'tiny-typed-emitter'
|
|
5
|
+
import { MissingWriterError, UnsupportedCorestoreOptsError } from '../errors.js'
|
|
6
6
|
|
|
7
7
|
/** @typedef {HyperdriveIndexImpl} THyperdriveIndex */
|
|
8
8
|
|
|
@@ -33,7 +33,7 @@ export class HyperdriveIndexImpl extends TypedEmitter {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
if (!writer) {
|
|
36
|
-
throw new
|
|
36
|
+
throw new MissingWriterError({ namespace: 'blobIndex' })
|
|
37
37
|
}
|
|
38
38
|
this.#writer = writer
|
|
39
39
|
|
|
@@ -103,9 +103,7 @@ class PretendCorestore {
|
|
|
103
103
|
} else if (opts.name.includes('blobs')) {
|
|
104
104
|
return this.#coreManager.getWriterCore('blob').core
|
|
105
105
|
} else {
|
|
106
|
-
throw new
|
|
107
|
-
'Unsupported corestore.get() with opts ' + util.inspect(opts)
|
|
108
|
-
)
|
|
106
|
+
throw new UnsupportedCorestoreOptsError({ opts })
|
|
109
107
|
}
|
|
110
108
|
}
|
|
111
109
|
|
package/src/blob-store/index.js
CHANGED
|
@@ -7,7 +7,13 @@ import { noop } from '../utils.js'
|
|
|
7
7
|
import { TypedEmitter } from 'tiny-typed-emitter'
|
|
8
8
|
import { HyperdriveIndexImpl as HyperdriveIndex } from './hyperdrive-index.js'
|
|
9
9
|
import { Logger } from '../logger.js'
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
BlobNotFoundError,
|
|
12
|
+
getErrorCode,
|
|
13
|
+
BlobsNotFoundError,
|
|
14
|
+
DriveNotFoundError,
|
|
15
|
+
} from '../errors.js'
|
|
16
|
+
import ensureError from 'ensure-error'
|
|
11
17
|
|
|
12
18
|
/** @import Hyperdrive from 'hyperdrive' */
|
|
13
19
|
/** @import { JsonObject } from 'type-fest' */
|
|
@@ -52,13 +58,6 @@ const NON_ARCHIVE_DEVICE_DOWNLOAD_FILTER = {
|
|
|
52
58
|
// thumbnails aren't supported yet.
|
|
53
59
|
}
|
|
54
60
|
|
|
55
|
-
class ErrNotFound extends Error {
|
|
56
|
-
constructor(message = 'NotFound') {
|
|
57
|
-
super(message)
|
|
58
|
-
this.code = 'ENOENT'
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
61
|
/** @extends {TypedEmitter<BlobStoreEvents>} */
|
|
63
62
|
export class BlobStore extends TypedEmitter {
|
|
64
63
|
#driveIndex
|
|
@@ -110,12 +109,12 @@ export class BlobStore extends TypedEmitter {
|
|
|
110
109
|
blobCoreId,
|
|
111
110
|
})
|
|
112
111
|
}
|
|
113
|
-
} catch (
|
|
114
|
-
if (getErrorCode(
|
|
112
|
+
} catch (e) {
|
|
113
|
+
if (getErrorCode(e) === 'ERR_STREAM_PREMATURE_CLOSE') return
|
|
115
114
|
this.#l.log(
|
|
116
115
|
'Error getting blob entries stream for peer %h: %s',
|
|
117
116
|
peerId,
|
|
118
|
-
|
|
117
|
+
ensureError(e).message
|
|
119
118
|
)
|
|
120
119
|
}
|
|
121
120
|
}
|
|
@@ -217,7 +216,7 @@ export class BlobStore extends TypedEmitter {
|
|
|
217
216
|
*/
|
|
218
217
|
#getDrive(driveId) {
|
|
219
218
|
const drive = this.#driveIndex.get(driveId)
|
|
220
|
-
if (!drive) throw new
|
|
219
|
+
if (!drive) throw new DriveNotFoundError({ driveId: driveId.slice(0, 7) })
|
|
221
220
|
return drive
|
|
222
221
|
}
|
|
223
222
|
|
|
@@ -232,7 +231,7 @@ export class BlobStore extends TypedEmitter {
|
|
|
232
231
|
const drive = this.#getDrive(driveId)
|
|
233
232
|
const path = makePath({ type, variant, name })
|
|
234
233
|
const blob = await drive.get(path, { wait, timeout })
|
|
235
|
-
if (!blob) throw new
|
|
234
|
+
if (!blob) throw new BlobNotFoundError()
|
|
236
235
|
return blob
|
|
237
236
|
}
|
|
238
237
|
|
|
@@ -285,9 +284,7 @@ export class BlobStore extends TypedEmitter {
|
|
|
285
284
|
const blobs = await drive.getBlobs()
|
|
286
285
|
|
|
287
286
|
if (!blobs) {
|
|
288
|
-
throw new
|
|
289
|
-
'Hyperblobs instance not found for drive ' + driveId.slice(0, 7)
|
|
290
|
-
)
|
|
287
|
+
throw new BlobsNotFoundError({ driveId: driveId.slice(0, 7) })
|
|
291
288
|
}
|
|
292
289
|
|
|
293
290
|
return blobs.createReadStream(entry.value.blob, options)
|
|
@@ -305,9 +302,7 @@ export class BlobStore extends TypedEmitter {
|
|
|
305
302
|
const blobs = await drive.getBlobs()
|
|
306
303
|
|
|
307
304
|
if (!blobs) {
|
|
308
|
-
throw new
|
|
309
|
-
'Hyperblobs instance not found for drive ' + driveId.slice(0, 7)
|
|
310
|
-
)
|
|
305
|
+
throw new BlobsNotFoundError({ driveId: driveId.slice(0, 7) })
|
|
311
306
|
}
|
|
312
307
|
|
|
313
308
|
return blobs.get(entry.value.blob, { wait: false, start: 0, length })
|
|
@@ -380,7 +375,7 @@ export class BlobStore extends TypedEmitter {
|
|
|
380
375
|
options = { follow: false, wait: false }
|
|
381
376
|
) {
|
|
382
377
|
const drive = this.#driveIndex.get(driveId)
|
|
383
|
-
if (!drive) throw new
|
|
378
|
+
if (!drive) throw new DriveNotFoundError({ driveId: driveId.slice(0, 7) })
|
|
384
379
|
const path = makePath({ type, variant, name })
|
|
385
380
|
const entry = await drive.entry(path, options)
|
|
386
381
|
return entry
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// Modified to encode and decode Uint32Arrays
|
|
4
4
|
|
|
5
5
|
import varint from 'varint'
|
|
6
|
+
import { InvalidBitfieldError } from '../errors.js'
|
|
6
7
|
|
|
7
8
|
const isLittleEndian =
|
|
8
9
|
new Uint8Array(new Uint16Array([0xff]).buffer)[0] === 0xff
|
|
@@ -123,7 +124,7 @@ export function decodingLength(buffer, offset) {
|
|
|
123
124
|
if (!repeat) offset += slice
|
|
124
125
|
}
|
|
125
126
|
|
|
126
|
-
if (offset > buffer.length) throw new
|
|
127
|
+
if (offset > buffer.length) throw new InvalidBitfieldError()
|
|
127
128
|
|
|
128
129
|
if (len & (n - 1)) return len + (n - (len & (n - 1)))
|
|
129
130
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import crypto from 'hypercore-crypto'
|
|
2
|
+
import { MissingWriterError } from '../errors.js'
|
|
2
3
|
/** @import { Namespace } from '../types.js' */
|
|
3
4
|
/** @import { CoreRecord } from './index.js' */
|
|
4
5
|
|
|
@@ -59,7 +60,7 @@ export class CoreIndex {
|
|
|
59
60
|
const writerRecord = this.#writersByNamespace.get(namespace)
|
|
60
61
|
// Shouldn't happen, since we add all the writers in the contructor
|
|
61
62
|
if (!writerRecord) {
|
|
62
|
-
throw new
|
|
63
|
+
throw new MissingWriterError({ namespace })
|
|
63
64
|
}
|
|
64
65
|
return writerRecord
|
|
65
66
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { TypedEmitter } from 'tiny-typed-emitter'
|
|
2
2
|
import Corestore from 'corestore'
|
|
3
3
|
import { debounce } from 'throttle-debounce'
|
|
4
|
-
import assert from 'node:assert/strict'
|
|
5
4
|
import { sql, eq } from 'drizzle-orm'
|
|
6
5
|
|
|
7
6
|
import {
|
|
@@ -17,7 +16,11 @@ import { coresTable } from '../schema/project.js'
|
|
|
17
16
|
import * as rle from './bitfield-rle.js'
|
|
18
17
|
import { CoreIndex } from './core-index.js'
|
|
19
18
|
import mapObject from 'map-obj'
|
|
20
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
InvalidProjectKeyError,
|
|
21
|
+
InvalidProjectSecretKeyError,
|
|
22
|
+
PeerNotFoundError,
|
|
23
|
+
} from '../errors.js'
|
|
21
24
|
|
|
22
25
|
/** @import Hypercore from 'hypercore' */
|
|
23
26
|
/** @import { BlobFilter, GenericBlobFilter, HypercorePeer, Namespace } from '../types.js' */
|
|
@@ -83,14 +86,10 @@ export class CoreManager extends TypedEmitter {
|
|
|
83
86
|
logger,
|
|
84
87
|
}) {
|
|
85
88
|
super()
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
assert(
|
|
91
|
-
!projectSecretKey || projectSecretKey.length === 64,
|
|
92
|
-
'project owner core secret key must be 64-byte buffer'
|
|
93
|
-
)
|
|
89
|
+
if (projectKey.length !== 32) throw new InvalidProjectKeyError()
|
|
90
|
+
if (projectSecretKey && projectSecretKey.length !== 64) {
|
|
91
|
+
throw new InvalidProjectSecretKeyError()
|
|
92
|
+
}
|
|
94
93
|
// Each peer will attach a listener, so max listeners is max attached peers
|
|
95
94
|
this.setMaxListeners(0)
|
|
96
95
|
this.#l = Logger.create('coreManager', logger)
|
package/src/core-ownership.js
CHANGED
|
@@ -16,7 +16,7 @@ import pDefer from 'p-defer'
|
|
|
16
16
|
import { NAMESPACES } from './constants.js'
|
|
17
17
|
import { TypedEmitter } from 'tiny-typed-emitter'
|
|
18
18
|
import { omit } from './lib/omit.js'
|
|
19
|
-
import { NotFoundError } from './errors.js'
|
|
19
|
+
import { InvalidCoreOwnershipError, NotFoundError } from './errors.js'
|
|
20
20
|
/**
|
|
21
21
|
* @import {
|
|
22
22
|
* CoreOwnershipWithSignatures,
|
|
@@ -161,10 +161,14 @@ export function mapAndValidateCoreOwnership(doc, { coreDiscoveryKey }) {
|
|
|
161
161
|
if (
|
|
162
162
|
!coreDiscoveryKey.equals(discoveryKey(Buffer.from(doc.authCoreId, 'hex')))
|
|
163
163
|
) {
|
|
164
|
-
throw new
|
|
164
|
+
throw new InvalidCoreOwnershipError(
|
|
165
|
+
'Invalid coreOwnership record: mismatched authCoreId'
|
|
166
|
+
)
|
|
165
167
|
}
|
|
166
168
|
if (!verifyCoreOwnership(doc)) {
|
|
167
|
-
throw new
|
|
169
|
+
throw new InvalidCoreOwnershipError(
|
|
170
|
+
'Invalid coreOwnership record: signatures are invalid'
|
|
171
|
+
)
|
|
168
172
|
}
|
|
169
173
|
const docWithoutSignatures = omit(doc, [
|
|
170
174
|
'identitySignature',
|
package/src/datastore/index.js
CHANGED
|
@@ -5,7 +5,12 @@ import pDefer from 'p-defer'
|
|
|
5
5
|
import { discoveryKey } from 'hypercore-crypto'
|
|
6
6
|
import { NAMESPACE_SCHEMAS } from '../constants.js'
|
|
7
7
|
import { createMap } from '../utils.js'
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
InvalidDocSchemaError,
|
|
10
|
+
InvalidVersionIdError,
|
|
11
|
+
NotFoundError,
|
|
12
|
+
WriterCoreNotReadyError,
|
|
13
|
+
} from '../errors.js'
|
|
9
14
|
/** @import { MapeoDoc } from '@comapeo/schema' */
|
|
10
15
|
|
|
11
16
|
/**
|
|
@@ -139,11 +144,10 @@ export class DataStore extends TypedEmitter {
|
|
|
139
144
|
async write(doc) {
|
|
140
145
|
// @ts-ignore
|
|
141
146
|
if (!NAMESPACE_SCHEMAS[this.#namespace].includes(doc.schemaName)) {
|
|
142
|
-
throw new
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
)
|
|
147
|
+
throw new InvalidDocSchemaError({
|
|
148
|
+
schemaName: doc.schemaName,
|
|
149
|
+
namespace: this.#namespace,
|
|
150
|
+
})
|
|
147
151
|
}
|
|
148
152
|
const block = encode(doc)
|
|
149
153
|
// The indexer batch can sometimes complete before the append below
|
|
@@ -159,7 +163,7 @@ export class DataStore extends TypedEmitter {
|
|
|
159
163
|
const index = length - 1
|
|
160
164
|
const coreDiscoveryKey = this.#writerCore.discoveryKey
|
|
161
165
|
if (!coreDiscoveryKey) {
|
|
162
|
-
throw new
|
|
166
|
+
throw new WriterCoreNotReadyError()
|
|
163
167
|
}
|
|
164
168
|
const versionId = getVersionId({ coreDiscoveryKey, index })
|
|
165
169
|
/** @type {import('p-defer').DeferredPromise<void>} */
|
|
@@ -181,7 +185,7 @@ export class DataStore extends TypedEmitter {
|
|
|
181
185
|
async read(versionId) {
|
|
182
186
|
const { coreDiscoveryKey, index } = parseVersionId(versionId)
|
|
183
187
|
const coreRecord = this.#coreManager.getCoreByDiscoveryKey(coreDiscoveryKey)
|
|
184
|
-
if (!coreRecord) throw new
|
|
188
|
+
if (!coreRecord) throw new InvalidVersionIdError()
|
|
185
189
|
const block = await coreRecord.core.get(index, { wait: false })
|
|
186
190
|
if (!block) throw new NotFoundError('Not Found')
|
|
187
191
|
return decode(block, { coreDiscoveryKey, index })
|
|
@@ -193,7 +197,7 @@ export class DataStore extends TypedEmitter {
|
|
|
193
197
|
const index = length - 1
|
|
194
198
|
const coreDiscoveryKey = this.#writerCore.discoveryKey
|
|
195
199
|
if (!coreDiscoveryKey) {
|
|
196
|
-
throw new
|
|
200
|
+
throw new WriterCoreNotReadyError()
|
|
197
201
|
}
|
|
198
202
|
const versionId = getVersionId({ coreDiscoveryKey, index })
|
|
199
203
|
return versionId
|
package/src/datatype/index.js
CHANGED
|
@@ -3,10 +3,18 @@ import { getTableConfig } from 'drizzle-orm/sqlite-core'
|
|
|
3
3
|
import { eq, inArray, sql } from 'drizzle-orm'
|
|
4
4
|
import { randomBytes } from 'node:crypto'
|
|
5
5
|
import { noop, mutatingDeNullify } from '../utils.js'
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
DocAlreadyDeletedError,
|
|
8
|
+
DocAlreadyExistsError,
|
|
9
|
+
InvalidDocError,
|
|
10
|
+
InvalidDocFormatError,
|
|
11
|
+
NotFoundError,
|
|
12
|
+
nullIfNotFound,
|
|
13
|
+
} from '../errors.js'
|
|
7
14
|
import { TypedEmitter } from 'tiny-typed-emitter'
|
|
8
15
|
import { setProperty, getProperty } from 'dot-prop-extra'
|
|
9
16
|
import { parseBcp47 } from '../intl/parse-bcp-47.js'
|
|
17
|
+
|
|
10
18
|
/** @import { MapeoDoc, MapeoValue } from '@comapeo/schema' */
|
|
11
19
|
/** @import { RunResult } from 'better-sqlite3' */
|
|
12
20
|
/** @import { SQLiteSelectBase } from 'drizzle-orm/sqlite-core' */
|
|
@@ -78,6 +86,7 @@ function generateDate() {
|
|
|
78
86
|
return new Date().toISOString()
|
|
79
87
|
}
|
|
80
88
|
export const kCreateWithDocId = Symbol('kCreateWithDocId')
|
|
89
|
+
export const kCreateOrUpdateWithDocId = Symbol('kCreateWithDocId')
|
|
81
90
|
export const kSelect = Symbol('select')
|
|
82
91
|
export const kTable = Symbol('table')
|
|
83
92
|
export const kDataStore = Symbol('dataStore')
|
|
@@ -181,6 +190,20 @@ export class DataType extends TypedEmitter {
|
|
|
181
190
|
return this[kCreateWithDocId](docId, value, { checkExisting: false })
|
|
182
191
|
}
|
|
183
192
|
|
|
193
|
+
/**
|
|
194
|
+
* @param {string} docId
|
|
195
|
+
* @param {ExcludeSchema<TValue, 'coreOwnership'>} value
|
|
196
|
+
* @returns {Promise<TDoc & DerivedDocFields>}
|
|
197
|
+
*/
|
|
198
|
+
async [kCreateOrUpdateWithDocId](docId, value) {
|
|
199
|
+
const existing = await this.getByDocId(docId).catch(nullIfNotFound)
|
|
200
|
+
if (existing) {
|
|
201
|
+
return this.update(existing.versionId, value)
|
|
202
|
+
} else {
|
|
203
|
+
return this[kCreateWithDocId](docId, value, { checkExisting: false })
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
184
207
|
/**
|
|
185
208
|
* @param {string} docId
|
|
186
209
|
* @param {ExcludeSchema<TValue, 'coreOwnership'> | CoreOwnershipWithSignaturesValue} value
|
|
@@ -190,12 +213,12 @@ export class DataType extends TypedEmitter {
|
|
|
190
213
|
async [kCreateWithDocId](docId, value, { checkExisting = true } = {}) {
|
|
191
214
|
if (!validate(this.#schemaName, value)) {
|
|
192
215
|
// TODO: pass through errors from validate functions
|
|
193
|
-
throw new
|
|
216
|
+
throw new InvalidDocFormatError({ value })
|
|
194
217
|
}
|
|
195
218
|
if (checkExisting) {
|
|
196
219
|
const existing = await this.getByDocId(docId).catch(noop)
|
|
197
220
|
if (existing) {
|
|
198
|
-
throw new
|
|
221
|
+
throw new DocAlreadyExistsError({ docId })
|
|
199
222
|
}
|
|
200
223
|
}
|
|
201
224
|
const nowDateString = generateDate()
|
|
@@ -376,7 +399,7 @@ export class DataType extends TypedEmitter {
|
|
|
376
399
|
const existingDoc = await this.getByDocId(docId)
|
|
377
400
|
|
|
378
401
|
if ('deleted' in existingDoc && existingDoc.deleted) {
|
|
379
|
-
throw new
|
|
402
|
+
throw new DocAlreadyDeletedError()
|
|
380
403
|
}
|
|
381
404
|
|
|
382
405
|
/** @type {any} */
|
|
@@ -423,7 +446,7 @@ export class DataType extends TypedEmitter {
|
|
|
423
446
|
(doc) => doc.docId === docId && doc.schemaName === this.#schemaName
|
|
424
447
|
)
|
|
425
448
|
if (!areLinksValid) {
|
|
426
|
-
throw new
|
|
449
|
+
throw new InvalidDocError()
|
|
427
450
|
}
|
|
428
451
|
return { docId, createdAt, originalVersionId }
|
|
429
452
|
}
|
|
@@ -3,13 +3,14 @@ import net from 'node:net'
|
|
|
3
3
|
import { randomBytes } from 'node:crypto'
|
|
4
4
|
import NoiseSecretStream from '@hyperswarm/secret-stream'
|
|
5
5
|
import { once } from 'node:events'
|
|
6
|
-
import { noop } from '../utils.js'
|
|
6
|
+
import { noop, timeoutPromise } from '../utils.js'
|
|
7
7
|
import { isPrivate } from 'bogon'
|
|
8
8
|
import StartStopStateMachine from 'start-stop-state-machine'
|
|
9
|
-
import pTimeout from 'p-timeout'
|
|
10
9
|
import { keyToPublicId } from '@mapeo/crypto'
|
|
11
10
|
import { Logger } from '../logger.js'
|
|
12
|
-
import { getErrorCode } from '../
|
|
11
|
+
import { ensureKnownError, getErrorCode } from '../errors.js'
|
|
12
|
+
import { ServerNotListeningError } from '../errors.js'
|
|
13
|
+
|
|
13
14
|
/** @import { OpenedNoiseStream } from '../lib/noise-secret-stream-helpers.js' */
|
|
14
15
|
|
|
15
16
|
/** @typedef {{ publicKey: Buffer, secretKey: Buffer }} Keypair */
|
|
@@ -88,7 +89,7 @@ export class LocalDiscovery extends TypedEmitter {
|
|
|
88
89
|
this.#server.listen(this.#port, '0.0.0.0')
|
|
89
90
|
await onListening
|
|
90
91
|
} catch (e) {
|
|
91
|
-
if (this.#port === 0) throw e
|
|
92
|
+
if (this.#port === 0) throw ensureKnownError(e)
|
|
92
93
|
// Account for errors from re-binding the port failing
|
|
93
94
|
this.#port = 0
|
|
94
95
|
return this.#start()
|
|
@@ -297,7 +298,7 @@ export class LocalDiscovery extends TypedEmitter {
|
|
|
297
298
|
for (const socket of this.#noiseConnections.values()) {
|
|
298
299
|
socket.destroy()
|
|
299
300
|
}
|
|
300
|
-
return
|
|
301
|
+
return timeoutPromise(closePromise, { milliseconds: 500 })
|
|
301
302
|
}
|
|
302
303
|
|
|
303
304
|
if (!force) {
|
|
@@ -306,7 +307,7 @@ export class LocalDiscovery extends TypedEmitter {
|
|
|
306
307
|
// If timeout is 0, we force-close immediately
|
|
307
308
|
await forceClose()
|
|
308
309
|
} else {
|
|
309
|
-
await
|
|
310
|
+
await timeoutPromise(closePromise, {
|
|
310
311
|
milliseconds: timeout,
|
|
311
312
|
fallback: forceClose,
|
|
312
313
|
})
|
|
@@ -324,7 +325,7 @@ export class LocalDiscovery extends TypedEmitter {
|
|
|
324
325
|
function getAddress(server) {
|
|
325
326
|
const addr = server.address()
|
|
326
327
|
if (addr === null || typeof addr === 'string') {
|
|
327
|
-
throw new
|
|
328
|
+
throw new ServerNotListeningError()
|
|
328
329
|
}
|
|
329
330
|
return addr
|
|
330
331
|
}
|