@cero-base/core 0.8.10 → 1.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/package.json +25 -9
- package/src/blobs/codec.js +52 -0
- package/src/blobs/index.js +42 -164
- package/src/blobs/server.js +48 -0
- package/src/database/bootstrap.js +24 -12
- package/src/database/dispatch.js +91 -17
- package/src/database/index.js +112 -26
- package/src/identity/index.js +1 -1
- package/src/index.js +25 -2
- package/src/lib/constants.js +21 -1
- package/src/lib/schema.js +2 -0
- package/src/lib/spec/index.js +106 -120
- package/src/lib/spec/schema.json +74 -80
- package/src/lib/utils.js +33 -2
- package/src/network/index.js +1 -1
- package/src/pairing/index.js +57 -56
- package/src/pairing/invite.js +19 -18
- package/src/rpc/client.js +4 -42
- package/src/rpc/index.js +8 -4
- package/src/rpc/peer.js +46 -0
- package/src/rpc/server.js +4 -42
- package/src/storage/index.js +16 -6
- package/types/blobs/codec.d.ts +26 -0
- package/types/blobs/index.d.ts +25 -90
- package/types/blobs/server.d.ts +35 -0
- package/types/database/bootstrap.d.ts +1 -1
- package/types/database/dispatch.d.ts +26 -3
- package/types/database/index.d.ts +53 -17
- package/types/index.d.ts +50 -2
- package/types/lib/constants.d.ts +24 -1
- package/types/lib/schema.d.ts +1 -0
- package/types/lib/spec/index.d.ts +9 -12
- package/types/lib/utils.d.ts +12 -2
- package/types/pairing/index.d.ts +15 -4
- package/types/pairing/invite.d.ts +11 -11
- package/types/rpc/client.d.ts +4 -15
- package/types/rpc/peer.d.ts +18 -0
- package/types/rpc/server.d.ts +4 -15
- package/types/storage/index.d.ts +9 -4
package/package.json
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cero-base/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "cero p2p primitives — identity, storage, network, database, blobs, rpc, pairing.",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=20"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/lekinox/cero-base.git",
|
|
13
|
+
"directory": "packages/core"
|
|
14
|
+
},
|
|
6
15
|
"main": "./src/index.js",
|
|
7
16
|
"types": "./types/index.d.ts",
|
|
8
17
|
"files": [
|
|
@@ -39,6 +48,12 @@
|
|
|
39
48
|
"types": "./types/blobs/index.d.ts",
|
|
40
49
|
"default": "./src/blobs/index.js"
|
|
41
50
|
},
|
|
51
|
+
"./blobs/codec": {
|
|
52
|
+
"default": "./src/blobs/codec.js"
|
|
53
|
+
},
|
|
54
|
+
"./blobs/server": {
|
|
55
|
+
"default": "./src/blobs/server.js"
|
|
56
|
+
},
|
|
42
57
|
"./rpc": {
|
|
43
58
|
"types": "./types/rpc/index.d.ts",
|
|
44
59
|
"default": "./src/rpc/index.js"
|
|
@@ -106,22 +121,23 @@
|
|
|
106
121
|
"test": "brittle-node test/*.test.js"
|
|
107
122
|
},
|
|
108
123
|
"dependencies": {
|
|
109
|
-
"autobee": "^1.0.
|
|
124
|
+
"autobee": "^1.0.9",
|
|
110
125
|
"b4a": "^1.8.1",
|
|
111
|
-
"bare-crypto": "^1.
|
|
126
|
+
"bare-crypto": "^1.15.3",
|
|
112
127
|
"bare-fs": "^4.7.2",
|
|
113
128
|
"bare-path": "^3.0.1",
|
|
114
129
|
"bip39-mnemonic": "^2.5.0",
|
|
115
130
|
"blind-pairing": "^2.3.1",
|
|
116
|
-
"compact-encoding": "^3.
|
|
117
|
-
"corestore": "^7.10.
|
|
131
|
+
"compact-encoding": "^3.2.0",
|
|
132
|
+
"corestore": "^7.10.1",
|
|
118
133
|
"framed-stream": "^1.0.1",
|
|
119
134
|
"hrpc": "^4.3.0",
|
|
120
|
-
"hyperblobs": "^2.12.
|
|
135
|
+
"hyperblobs": "^2.12.1",
|
|
136
|
+
"hypercore-blob-server": "^1.12.0",
|
|
121
137
|
"hypercore": "^11.33.1",
|
|
122
138
|
"hypercore-crypto": "^3.7.0",
|
|
123
139
|
"hypercore-id-encoding": "^1.3.0",
|
|
124
|
-
"hypercore-storage": "^3.
|
|
140
|
+
"hypercore-storage": "^3.1.1",
|
|
125
141
|
"hyperdb": "^6.7.0",
|
|
126
142
|
"hyperdispatch": "^1.6.0",
|
|
127
143
|
"hyperschema": "^1.21.0",
|
|
@@ -131,12 +147,12 @@
|
|
|
131
147
|
"ready-resource": "^1.2.0",
|
|
132
148
|
"safety-catch": "^1.0.3",
|
|
133
149
|
"sodium-universal": "^5.0.1",
|
|
134
|
-
"streamx": "^2.
|
|
150
|
+
"streamx": "^2.28.0",
|
|
135
151
|
"z32": "^1.1.0"
|
|
136
152
|
},
|
|
137
153
|
"devDependencies": {
|
|
138
154
|
"@hyperswarm/testnet": "^3.1.4",
|
|
139
|
-
"brittle": "^4.0.
|
|
155
|
+
"brittle": "^4.0.2",
|
|
140
156
|
"typescript": "^5.9.3"
|
|
141
157
|
},
|
|
142
158
|
"license": "Apache-2.0"
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import c from 'compact-encoding'
|
|
2
|
+
import z32 from 'z32'
|
|
3
|
+
|
|
4
|
+
import { getEncoding } from '../lib/spec/index.js'
|
|
5
|
+
import { CeroError } from '../lib/errors.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {object} RawBlobId
|
|
9
|
+
* @property {number} blockOffset
|
|
10
|
+
* @property {number} blockLength
|
|
11
|
+
* @property {number} byteOffset
|
|
12
|
+
* @property {number} byteLength
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const BlobId = getEncoding('@cero/blob-id')
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Encode a coreKey + raw hyperblobs blobId + type into a durable string id.
|
|
19
|
+
*
|
|
20
|
+
* @param {Uint8Array} coreKey
|
|
21
|
+
* @param {RawBlobId} blobId
|
|
22
|
+
* @param {string} type
|
|
23
|
+
* @returns {string}
|
|
24
|
+
*/
|
|
25
|
+
export function encodeId(coreKey, blobId, type) {
|
|
26
|
+
return z32.encode(c.encode(BlobId, { coreKey, ...blobId, type }))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Decode a string id back into its coreKey, raw blobId and type.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} id
|
|
33
|
+
* @returns {{ coreKey: Uint8Array, blobId: RawBlobId, type: string }}
|
|
34
|
+
*/
|
|
35
|
+
export function decodeId(id) {
|
|
36
|
+
if (typeof id !== 'string' || id.length === 0)
|
|
37
|
+
throw CeroError.INVALID('id must be a non-empty string')
|
|
38
|
+
let buf
|
|
39
|
+
try {
|
|
40
|
+
buf = z32.decode(id)
|
|
41
|
+
} catch {
|
|
42
|
+
throw CeroError.INVALID('id must be valid z32')
|
|
43
|
+
}
|
|
44
|
+
let v
|
|
45
|
+
try {
|
|
46
|
+
v = c.decode(BlobId, buf)
|
|
47
|
+
} catch {
|
|
48
|
+
throw CeroError.INVALID('id is malformed')
|
|
49
|
+
}
|
|
50
|
+
const { coreKey, blockOffset, blockLength, byteOffset, byteLength, type } = v
|
|
51
|
+
return { coreKey, blobId: { blockOffset, blockLength, byteOffset, byteLength }, type }
|
|
52
|
+
}
|
package/src/blobs/index.js
CHANGED
|
@@ -1,41 +1,26 @@
|
|
|
1
1
|
import Hyperblobs from 'hyperblobs'
|
|
2
2
|
import ReadyResource from 'ready-resource'
|
|
3
|
-
import sodium from 'sodium-universal'
|
|
4
3
|
import b4a from 'b4a'
|
|
5
|
-
import z32 from 'z32'
|
|
6
4
|
|
|
7
5
|
import { NAMESPACE } from '../lib/constants.js'
|
|
8
6
|
import { toId } from '../lib/utils.js'
|
|
9
7
|
import { CeroError } from '../lib/errors.js'
|
|
8
|
+
import { encodeId, decodeId } from './codec.js'
|
|
10
9
|
|
|
11
10
|
const NS = `${NAMESPACE}/blobs`
|
|
12
11
|
|
|
13
12
|
/**
|
|
14
13
|
* @typedef {object} BlobsOpts
|
|
15
|
-
* @property {
|
|
14
|
+
* @property {object} store Corestore (or compatible) used to host the blob core.
|
|
16
15
|
* @property {import('../identity/index.js').Identity} [identity] Identity supplying the default encryption key.
|
|
17
16
|
* @property {import('../network/index.js').Network} [network] Optional network used to announce + replicate the core.
|
|
18
17
|
* @property {Uint8Array} [key] Pre-existing blob core key — joins an existing blob feed.
|
|
19
18
|
* @property {Uint8Array} [encryptionKey] Explicit encryption key, overrides `identity.encryptionKey`.
|
|
20
19
|
*
|
|
21
|
-
* @typedef {
|
|
22
|
-
* @property {string} id Opaque z32 id used by `get`/`info`/`delete`.
|
|
23
|
-
* @property {string} hash Content hash (z32-encoded blake2b of the bytes).
|
|
24
|
-
* @property {number} size Logical byte length recorded at put-time.
|
|
25
|
-
* @property {any} meta Caller-provided metadata, stored verbatim.
|
|
26
|
-
* @property {number} createdAt Wall-clock timestamp of the put.
|
|
27
|
-
*
|
|
28
|
-
* @typedef {object} PutOpts
|
|
29
|
-
* @property {any} [meta] Arbitrary metadata persisted alongside the blob.
|
|
30
|
-
* @property {number} [size] Override the recorded size (defaults to byte length).
|
|
20
|
+
* @typedef {import('./codec.js').RawBlobId} RawBlobId
|
|
31
21
|
*/
|
|
32
22
|
|
|
33
|
-
/**
|
|
34
|
-
* Content-addressable blob store backed by a single Hyperblobs core.
|
|
35
|
-
* Writes dedupe locally by content hash, reads/streams are positional
|
|
36
|
-
* descriptors, and the underlying core is attached to the supplied
|
|
37
|
-
* network for replication.
|
|
38
|
-
*/
|
|
23
|
+
/** Thin wrapper over a single Hyperblobs core; deals only in raw blobIds. */
|
|
39
24
|
export class Blobs extends ReadyResource {
|
|
40
25
|
/** @param {BlobsOpts} [opts] */
|
|
41
26
|
constructor({ store, identity, network, key, encryptionKey } = {}) {
|
|
@@ -51,12 +36,7 @@ export class Blobs extends ReadyResource {
|
|
|
51
36
|
this._coreKey = key || null
|
|
52
37
|
this.core = null
|
|
53
38
|
this.hyperblobs = null
|
|
54
|
-
|
|
55
39
|
this._discovery = null
|
|
56
|
-
/** @type {Map<string, BlobEntry>} contentHash → entry */
|
|
57
|
-
this._index = new Map()
|
|
58
|
-
/** @type {Map<string, BlobEntry>} id → entry (so info/has/delete by id work) */
|
|
59
|
-
this._byId = new Map()
|
|
60
40
|
}
|
|
61
41
|
|
|
62
42
|
/**
|
|
@@ -94,7 +74,8 @@ export class Blobs extends ReadyResource {
|
|
|
94
74
|
if (this._coreKey) opts.key = this._coreKey
|
|
95
75
|
else opts.name = 'blobs'
|
|
96
76
|
|
|
97
|
-
|
|
77
|
+
const ns = this.store.namespace(NS)
|
|
78
|
+
this.core = ns.get(opts)
|
|
98
79
|
await this.core.ready()
|
|
99
80
|
|
|
100
81
|
this.hyperblobs = new Hyperblobs(this.core)
|
|
@@ -114,130 +95,67 @@ export class Blobs extends ReadyResource {
|
|
|
114
95
|
}
|
|
115
96
|
|
|
116
97
|
/**
|
|
117
|
-
* Store a buffer or readable stream, returning its
|
|
118
|
-
* content from the same writer is deduplicated locally.
|
|
98
|
+
* Store a buffer or readable stream, returning its raw hyperblobs blobId.
|
|
119
99
|
*
|
|
120
100
|
* @param {Uint8Array | import('streamx').Readable} input
|
|
121
|
-
* @
|
|
122
|
-
* @returns {Promise<string>}
|
|
101
|
+
* @returns {Promise<RawBlobId>}
|
|
123
102
|
*/
|
|
124
|
-
async put(input
|
|
103
|
+
async put(input) {
|
|
125
104
|
this._guard()
|
|
126
105
|
if (input == null) throw CeroError.REQUIRED('input')
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
bytes = b4a.from(input)
|
|
131
|
-
} else if (isReadable(input)) {
|
|
132
|
-
bytes = await readAll(input)
|
|
133
|
-
} else {
|
|
134
|
-
throw CeroError.INVALID('input must be a buffer or a Readable stream')
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const hashBuf = contentHash(bytes)
|
|
138
|
-
const hash = toId(hashBuf)
|
|
139
|
-
|
|
140
|
-
// Local dedup: same writer putting same bytes returns the existing id.
|
|
141
|
-
const existing = this._index.get(hash)
|
|
142
|
-
if (existing) return existing.id
|
|
143
|
-
|
|
144
|
-
const raw = await this.hyperblobs.put(bytes)
|
|
145
|
-
const id = encodeId(raw)
|
|
146
|
-
const entry = {
|
|
147
|
-
id,
|
|
148
|
-
hash,
|
|
149
|
-
size: size ?? bytes.byteLength,
|
|
150
|
-
meta,
|
|
151
|
-
createdAt: Date.now()
|
|
152
|
-
}
|
|
153
|
-
this._index.set(hash, entry)
|
|
154
|
-
this._byId.set(id, entry)
|
|
155
|
-
return id
|
|
106
|
+
if (b4a.isBuffer(input)) return this.hyperblobs.put(b4a.from(input))
|
|
107
|
+
if (isReadable(input)) return this._putStream(input)
|
|
108
|
+
throw CeroError.INVALID('input must be a buffer or a Readable stream')
|
|
156
109
|
}
|
|
157
110
|
|
|
158
111
|
/**
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
* @param {string} id
|
|
162
|
-
* @returns {Promise<void>}
|
|
112
|
+
* @param {import('streamx').Readable} input
|
|
113
|
+
* @returns {Promise<RawBlobId>}
|
|
163
114
|
*/
|
|
164
|
-
async
|
|
165
|
-
this.
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
115
|
+
async _putStream(input) {
|
|
116
|
+
const ws = this.hyperblobs.createWriteStream()
|
|
117
|
+
await new Promise((resolve, reject) => {
|
|
118
|
+
input.on('error', reject)
|
|
119
|
+
ws.on('error', reject)
|
|
120
|
+
ws.on('close', resolve)
|
|
121
|
+
input.pipe(ws)
|
|
122
|
+
})
|
|
123
|
+
return ws.id
|
|
173
124
|
}
|
|
174
125
|
|
|
175
126
|
/**
|
|
176
|
-
* Resolve the blob bytes for
|
|
127
|
+
* Resolve the blob bytes for a raw blobId. Fetches from peers if not local.
|
|
177
128
|
*
|
|
178
|
-
* @param {
|
|
179
|
-
* @param {
|
|
129
|
+
* @param {RawBlobId} blobId
|
|
130
|
+
* @param {object} [opts]
|
|
180
131
|
* @returns {Promise<Uint8Array>}
|
|
181
132
|
*/
|
|
182
|
-
async get(
|
|
133
|
+
async get(blobId, opts) {
|
|
183
134
|
this._guard()
|
|
184
|
-
|
|
185
|
-
return this.hyperblobs.get(raw, opts)
|
|
135
|
+
return this.hyperblobs.get(blobId, opts)
|
|
186
136
|
}
|
|
187
137
|
|
|
188
138
|
/**
|
|
189
|
-
* Streaming counterpart of `get` —
|
|
139
|
+
* Streaming counterpart of `get` — a Readable over the blob bytes.
|
|
190
140
|
*
|
|
191
|
-
* @param {
|
|
192
|
-
* @param {
|
|
141
|
+
* @param {RawBlobId} blobId
|
|
142
|
+
* @param {object} [opts]
|
|
193
143
|
* @returns {import('streamx').Readable}
|
|
194
144
|
*/
|
|
195
|
-
|
|
196
|
-
this._guard()
|
|
197
|
-
const raw = decodeId(id)
|
|
198
|
-
return this.hyperblobs.createReadStream(raw, opts)
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Look up the locally-cached metadata for a blob id, or `null` if absent.
|
|
203
|
-
*
|
|
204
|
-
* @param {string} id
|
|
205
|
-
* @returns {Promise<BlobEntry | null>}
|
|
206
|
-
*/
|
|
207
|
-
async info(id) {
|
|
145
|
+
createReadStream(blobId, opts) {
|
|
208
146
|
this._guard()
|
|
209
|
-
|
|
210
|
-
return this._byId.get(id) || null
|
|
147
|
+
return this.hyperblobs.createReadStream(blobId, opts)
|
|
211
148
|
}
|
|
212
149
|
|
|
213
150
|
/**
|
|
214
|
-
*
|
|
151
|
+
* Clear the blocks backing a raw blobId from the local core.
|
|
215
152
|
*
|
|
216
|
-
* @param {
|
|
217
|
-
* @returns {Promise<boolean>}
|
|
218
|
-
*/
|
|
219
|
-
async has(id) {
|
|
220
|
-
this._guard()
|
|
221
|
-
const raw = decodeId(id)
|
|
222
|
-
const len = raw.blockOffset + raw.blockLength
|
|
223
|
-
if (this.core.length < len) return false
|
|
224
|
-
return this.core.has(raw.blockOffset, len)
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Pre-fetch the blocks for `id` from peers without reading the bytes.
|
|
229
|
-
*
|
|
230
|
-
* @param {string} id
|
|
231
|
-
* @param {any} [opts]
|
|
153
|
+
* @param {RawBlobId} blobId
|
|
232
154
|
* @returns {Promise<void>}
|
|
233
155
|
*/
|
|
234
|
-
async
|
|
156
|
+
async clear(blobId) {
|
|
235
157
|
this._guard()
|
|
236
|
-
|
|
237
|
-
const start = raw.blockOffset
|
|
238
|
-
const end = raw.blockOffset + raw.blockLength
|
|
239
|
-
const range = this.core.download({ start, end, ...opts })
|
|
240
|
-
await range.done()
|
|
158
|
+
await this.hyperblobs.clear(blobId)
|
|
241
159
|
}
|
|
242
160
|
|
|
243
161
|
_guard() {
|
|
@@ -246,52 +164,12 @@ export class Blobs extends ReadyResource {
|
|
|
246
164
|
}
|
|
247
165
|
}
|
|
248
166
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function isBufferLike(x) {
|
|
256
|
-
return b4a.isBuffer(x) || x instanceof Uint8Array
|
|
257
|
-
}
|
|
258
|
-
|
|
167
|
+
/**
|
|
168
|
+
* @param {unknown} x
|
|
169
|
+
* @returns {x is import('streamx').Readable}
|
|
170
|
+
*/
|
|
259
171
|
function isReadable(x) {
|
|
260
172
|
return x && typeof x.pipe === 'function' && typeof x.on === 'function'
|
|
261
173
|
}
|
|
262
174
|
|
|
263
|
-
|
|
264
|
-
const chunks = []
|
|
265
|
-
for await (const chunk of stream) chunks.push(chunk)
|
|
266
|
-
return b4a.concat(chunks)
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// id <-> raw hyperblobs descriptor (positional inside the core)
|
|
270
|
-
// 16-byte packed: 4 uint32-BE = blockOffset, blockLength, byteOffset, byteLength
|
|
271
|
-
|
|
272
|
-
function encodeId(raw) {
|
|
273
|
-
const buf = b4a.alloc(16)
|
|
274
|
-
buf.writeUInt32BE(raw.blockOffset, 0)
|
|
275
|
-
buf.writeUInt32BE(raw.blockLength, 4)
|
|
276
|
-
buf.writeUInt32BE(raw.byteOffset, 8)
|
|
277
|
-
buf.writeUInt32BE(raw.byteLength, 12)
|
|
278
|
-
return z32.encode(buf)
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function decodeId(id) {
|
|
282
|
-
if (typeof id !== 'string') throw CeroError.INVALID('id must be a string')
|
|
283
|
-
if (id.length === 0) throw CeroError.INVALID('id must be a non-empty string')
|
|
284
|
-
let buf
|
|
285
|
-
try {
|
|
286
|
-
buf = z32.decode(id)
|
|
287
|
-
} catch {
|
|
288
|
-
throw CeroError.INVALID('id must be valid z32')
|
|
289
|
-
}
|
|
290
|
-
if (buf.length !== 16) throw CeroError.INVALID('id must be 16 bytes when decoded')
|
|
291
|
-
return {
|
|
292
|
-
blockOffset: buf.readUInt32BE(0),
|
|
293
|
-
blockLength: buf.readUInt32BE(4),
|
|
294
|
-
byteOffset: buf.readUInt32BE(8),
|
|
295
|
-
byteLength: buf.readUInt32BE(12)
|
|
296
|
-
}
|
|
297
|
-
}
|
|
175
|
+
export { encodeId, decodeId }
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import BlobServer from 'hypercore-blob-server'
|
|
2
|
+
import { decodeId } from './index.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {{ key: Buffer, encryptionKey: Buffer | null }} Resolved
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* HTTP server that turns a string file id into a renderable localhost URL.
|
|
10
|
+
* Wraps hypercore-blob-server; the underlying store session is owned by
|
|
11
|
+
* BlobServer and closed when `close()` is called.
|
|
12
|
+
*/
|
|
13
|
+
export class FileServer {
|
|
14
|
+
/**
|
|
15
|
+
* @param {object} opts
|
|
16
|
+
* @param {import('corestore')} opts.store
|
|
17
|
+
* @param {(coreKey: Buffer, info: object) => Resolved | null | Promise<Resolved | null>} opts.resolve
|
|
18
|
+
*/
|
|
19
|
+
constructor({ store, resolve }) {
|
|
20
|
+
this.resolve = resolve
|
|
21
|
+
this.server = new BlobServer(store.session(), {
|
|
22
|
+
resolve: (key, info) => this.resolve(key, info)
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get port() {
|
|
27
|
+
return this.server.port
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async listen() {
|
|
31
|
+
await this.server.listen()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Build a renderable localhost URL for a string file id.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} id
|
|
38
|
+
* @returns {string}
|
|
39
|
+
*/
|
|
40
|
+
getLink(id) {
|
|
41
|
+
const { coreKey, blobId, type } = decodeId(id)
|
|
42
|
+
return this.server.getLink(coreKey, { blob: blobId, type })
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async close() {
|
|
46
|
+
await this.server.close()
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import b4a from 'b4a'
|
|
1
2
|
import Hypercore from 'hypercore'
|
|
2
3
|
import { Identity } from '../identity/index.js'
|
|
4
|
+
import { CeroError } from '../lib/errors.js'
|
|
3
5
|
import { toId } from '../lib/utils.js'
|
|
4
6
|
|
|
5
7
|
/**
|
|
@@ -11,7 +13,7 @@ import { toId } from '../lib/utils.js'
|
|
|
11
13
|
* @param {{ name?: string | null, isMobile?: boolean, recovering?: boolean }} [opts]
|
|
12
14
|
* @returns {Promise<{ id: Uint8Array, writer: import('../identity/index.js').KeyPair }>}
|
|
13
15
|
*/
|
|
14
|
-
export async function bootstrap(db, { name, isMobile, recovering = false } = {}) {
|
|
16
|
+
export async function bootstrap(db, { name, isMobile, recovering = false, timeout = 30000 } = {}) {
|
|
15
17
|
const keyPair = Identity.randomKeyPair()
|
|
16
18
|
const manifest = {
|
|
17
19
|
version: db.store.manifestVersion,
|
|
@@ -19,22 +21,36 @@ export async function bootstrap(db, { name, isMobile, recovering = false } = {})
|
|
|
19
21
|
}
|
|
20
22
|
const writerKey = Hypercore.key(manifest)
|
|
21
23
|
|
|
22
|
-
if (recovering) await waitForFirstPeerAppend(db)
|
|
24
|
+
if (recovering) await waitForFirstPeerAppend(db, timeout)
|
|
23
25
|
await saveWriter(db, writerKey, { name, isMobile })
|
|
24
26
|
await swapWriter(db, keyPair, manifest)
|
|
25
27
|
|
|
26
28
|
return { id: writerKey, writer: keyPair }
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
async function waitForFirstPeerAppend(db) {
|
|
31
|
+
async function waitForFirstPeerAppend(db, timeout = 30000) {
|
|
30
32
|
if (db.bee.local.length > 0 || !db.network?.swarm) return
|
|
31
|
-
await new Promise((resolve) => {
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
await new Promise((resolve, reject) => {
|
|
34
|
+
let done = false
|
|
35
|
+
const finish = (err) => {
|
|
36
|
+
if (done) return
|
|
37
|
+
done = true
|
|
38
|
+
clearTimeout(timer)
|
|
34
39
|
db.bee.local.off('append', onAppend)
|
|
35
|
-
|
|
40
|
+
db.off('close', onClose)
|
|
41
|
+
if (err) reject(err)
|
|
42
|
+
else resolve()
|
|
36
43
|
}
|
|
44
|
+
// timeout + close handling so a peer that never replicates can't hang the
|
|
45
|
+
// recovery forever, and the append listener is always removed.
|
|
46
|
+
const onAppend = () => db.bee.local.length > 0 && finish()
|
|
47
|
+
const onClose = () => finish(CeroError.CLOSED('Database'))
|
|
48
|
+
const timer = setTimeout(
|
|
49
|
+
() => finish(CeroError.TIMED_OUT('recovery — no peer append')),
|
|
50
|
+
timeout
|
|
51
|
+
)
|
|
37
52
|
db.bee.local.on('append', onAppend)
|
|
53
|
+
db.on('close', onClose)
|
|
38
54
|
})
|
|
39
55
|
await db.bee.update()
|
|
40
56
|
}
|
|
@@ -46,7 +62,7 @@ async function saveWriter(db, writerKey, { name, isMobile }) {
|
|
|
46
62
|
{
|
|
47
63
|
master: db.identity.publicKey,
|
|
48
64
|
writer: writerKey,
|
|
49
|
-
sig: db.identity.sign(writerKey)
|
|
65
|
+
sig: db.identity.sign(b4a.concat([writerKey, db.writerKey]))
|
|
50
66
|
}
|
|
51
67
|
],
|
|
52
68
|
[
|
|
@@ -65,10 +81,6 @@ async function swapWriter(db, keyPair, manifest) {
|
|
|
65
81
|
const enc = await db.bee.local.getUserData('autobee/encryption')
|
|
66
82
|
|
|
67
83
|
if (db.network) db.network.detach(db.bee)
|
|
68
|
-
if (db.onLocalAppend && db.bee.local) {
|
|
69
|
-
db.bee.local.off('append', db.onLocalAppend)
|
|
70
|
-
db.onLocalAppend = null
|
|
71
|
-
}
|
|
72
84
|
await db.bee.close()
|
|
73
85
|
db.bee = null
|
|
74
86
|
|