@cero-base/core 1.1.1 → 1.3.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/README.md +14 -12
- package/package.json +13 -4
- package/src/blobs/index.js +1 -1
- package/src/database/bootstrap.js +2 -2
- package/src/database/dispatch.js +41 -6
- package/src/database/index.js +177 -17
- package/src/identity/index.js +15 -1
- package/src/lib/errors.js +8 -0
- package/src/lib/utils.js +33 -12
- package/src/network/bluetooth.js +804 -0
- package/src/network/gatt-stream.js +59 -0
- package/src/network/index.js +121 -8
- package/src/pairing/index.js +6 -6
- package/src/rpc/index.js +2 -2
- package/src/storage/index.js +30 -8
- package/types/database/dispatch.d.ts +2 -1
- package/types/database/index.d.ts +34 -8
- package/types/identity/index.d.ts +9 -0
- package/types/lib/errors.d.ts +6 -0
- package/types/lib/utils.d.ts +4 -6
- package/types/network/bluetooth.d.ts +162 -0
- package/types/network/gatt-stream.d.ts +26 -0
- package/types/network/index.d.ts +36 -1
- package/types/storage/index.d.ts +7 -1
- package/src/CLAUDE.md +0 -3
- package/src/database/CLAUDE.md +0 -3
- package/src/identity/CLAUDE.md +0 -3
- package/src/lib/CLAUDE.md +0 -3
- package/src/network/CLAUDE.md +0 -3
- package/src/rpc/CLAUDE.md +0 -3
- package/src/storage/CLAUDE.md +0 -3
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Duplex } from 'streamx'
|
|
2
|
+
|
|
3
|
+
// iOS caps a single GATT write (and a notify payload) at ATT_MTU − 3 ≈ 182
|
|
4
|
+
// bytes. 150 stays safely under that without negotiating an MTU. Raise later
|
|
5
|
+
// via the peripheral's maximumWriteValueLength + write-without-response.
|
|
6
|
+
const PAYLOAD = 150
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A dumb byte-carrying duplex for the GATT transport. Framing and session logic
|
|
10
|
+
* live in BluetoothTransport; this only fragments outbound writes to fit a GATT
|
|
11
|
+
* write and pushes inbound payload bytes. NoiseSecretStream wraps it as a raw
|
|
12
|
+
* duplex, exactly like the old L2CAP channel.
|
|
13
|
+
*
|
|
14
|
+
* @extends Duplex
|
|
15
|
+
*/
|
|
16
|
+
export class GattStream extends Duplex {
|
|
17
|
+
/**
|
|
18
|
+
* @param {object} opts
|
|
19
|
+
* @param {(buffer: Uint8Array) => Promise<void>} opts.send Transmit one payload piece (transport frames it).
|
|
20
|
+
* @param {() => void} [opts.onclose] Called once on teardown (send a close frame, disconnect).
|
|
21
|
+
*/
|
|
22
|
+
constructor({ send, onclose } = {}) {
|
|
23
|
+
super()
|
|
24
|
+
this._send = send
|
|
25
|
+
this._onclose = onclose || null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async _write(chunk, cb) {
|
|
29
|
+
try {
|
|
30
|
+
for (let offset = 0; offset < chunk.byteLength; offset += PAYLOAD) {
|
|
31
|
+
await this._send(chunk.subarray(offset, offset + PAYLOAD))
|
|
32
|
+
}
|
|
33
|
+
cb(null)
|
|
34
|
+
} catch (err) {
|
|
35
|
+
cb(err)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
receive(buffer) {
|
|
40
|
+
this.push(buffer)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
remoteEnd() {
|
|
44
|
+
this.push(null)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
_destroy(cb) {
|
|
48
|
+
const onclose = this._onclose
|
|
49
|
+
this._onclose = null
|
|
50
|
+
if (onclose) {
|
|
51
|
+
try {
|
|
52
|
+
onclose()
|
|
53
|
+
} catch {
|
|
54
|
+
// teardown is best-effort
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
cb(null)
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/network/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import Hyperswarm from 'hyperswarm'
|
|
2
|
+
import NoiseSecretStream from '@hyperswarm/secret-stream'
|
|
3
|
+
import BlindPairing from 'blind-pairing'
|
|
2
4
|
import ProtomuxWakeup from 'protomux-wakeup'
|
|
3
5
|
import ReadyResource from 'ready-resource'
|
|
4
6
|
import safetyCatch from 'safety-catch'
|
|
@@ -39,6 +41,89 @@ export class Network extends ReadyResource {
|
|
|
39
41
|
|
|
40
42
|
this._replicateables = new Set()
|
|
41
43
|
this._discoveries = new Set()
|
|
44
|
+
this._injected = new Set()
|
|
45
|
+
this._blind = null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Feed an externally-established connection — a Bluetooth L2CAP channel, a
|
|
50
|
+
* serial link, an in-process pair, any duplex — into the network. A raw
|
|
51
|
+
* duplex is wrapped in NoiseSecretStream (pass `isInitiator`); a stream
|
|
52
|
+
* that already IS one is used as-is. From here it gets the exact same
|
|
53
|
+
* treatment as a swarm connection: wakeup, replication of every attached
|
|
54
|
+
* core, pairing, and the 'connection' event.
|
|
55
|
+
*
|
|
56
|
+
* @param {any} stream Duplex transport, or a ready NoiseSecretStream.
|
|
57
|
+
* @param {{ isInitiator?: boolean }} [opts] Which side initiates the noise handshake (raw duplexes only).
|
|
58
|
+
* @returns {any} The encrypted connection stream.
|
|
59
|
+
*/
|
|
60
|
+
inject(stream, { isInitiator } = {}) {
|
|
61
|
+
if (this.closing || this.closed) throw CeroError.CLOSED('Network')
|
|
62
|
+
if (!stream) throw CeroError.REQUIRED('stream')
|
|
63
|
+
// Injected links authenticate with the same long-lived identity as the
|
|
64
|
+
// swarm — an ephemeral key per link would defeat duplicate-peer detection
|
|
65
|
+
// (two radios between one pair would look like two different peers).
|
|
66
|
+
const keyPair = this.identity
|
|
67
|
+
? { publicKey: this.identity.publicKey, secretKey: this.identity.secretKey }
|
|
68
|
+
: (this.swarm?.keyPair ?? undefined)
|
|
69
|
+
const conn =
|
|
70
|
+
stream.noiseStream === stream
|
|
71
|
+
? stream
|
|
72
|
+
: new NoiseSecretStream(isInitiator === true, stream, keyPair ? { keyPair } : undefined)
|
|
73
|
+
|
|
74
|
+
// blind-pairing picks the lowest-`rtt` unvisited channel to send on; that
|
|
75
|
+
// field only exists on real udx sockets. A raw injected duplex has none,
|
|
76
|
+
// so `rtt < Infinity` is always false and pairing never sends a request.
|
|
77
|
+
// Zero is also the semantically correct RTT for a direct injected link.
|
|
78
|
+
if (conn.rawStream && conn.rawStream.rtt === undefined) conn.rawStream.rtt = 0
|
|
79
|
+
|
|
80
|
+
this._injected.add(conn)
|
|
81
|
+
conn.on('close', () => this._injected.delete(conn))
|
|
82
|
+
conn.on('error', safetyCatch) // a dropped radio link must not crash the host
|
|
83
|
+
|
|
84
|
+
this.wakeup.addStream(conn)
|
|
85
|
+
for (const r of this._replicateables) replicateInto(r, conn)
|
|
86
|
+
this.emit('connection', conn, { injected: true })
|
|
87
|
+
return conn
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Lazily create the network-shared BlindPairing. One instance serves every
|
|
92
|
+
* handle's pairing member — per-handle instances each added their own swarm
|
|
93
|
+
* and DHT listeners plus a protomux channel per connection.
|
|
94
|
+
*
|
|
95
|
+
* @returns {Promise<any>}
|
|
96
|
+
*/
|
|
97
|
+
async blind() {
|
|
98
|
+
if (!this._blind) {
|
|
99
|
+
const blind = new BlindPairing(this.swarm)
|
|
100
|
+
this._blind = blind.ready().then(() => {
|
|
101
|
+
// blind-pairing only watches the swarm — injected connections must
|
|
102
|
+
// reach it too, or offline (e.g. Bluetooth) pairing never completes.
|
|
103
|
+
// _onconnection is upstream-private; the offline-pairing test pins it.
|
|
104
|
+
this.on('connection', (conn, info) => {
|
|
105
|
+
if (info?.injected) blind._onconnection(conn)
|
|
106
|
+
})
|
|
107
|
+
for (const conn of this._injected) blind._onconnection(conn)
|
|
108
|
+
return blind
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
return this._blind
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Re-attach pairing channels on injected connections. blind-pairing only
|
|
116
|
+
* auto-attaches refs that existed when a connection arrived — swarm peers
|
|
117
|
+
* meet again over topic joins, injected links (Bluetooth, pipes) don't, so
|
|
118
|
+
* a member/candidate added later must re-run the attach. Idempotent:
|
|
119
|
+
* protomux refuses duplicate channels.
|
|
120
|
+
*
|
|
121
|
+
* @returns {Promise<void>}
|
|
122
|
+
*/
|
|
123
|
+
async refreshInjected() {
|
|
124
|
+
if (!this._blind || !this._injected.size) return
|
|
125
|
+
const blind = await this._blind
|
|
126
|
+
for (const conn of this._injected) blind._onconnection(conn)
|
|
42
127
|
}
|
|
43
128
|
|
|
44
129
|
/** @returns {Map<string, any>} Known peers keyed by public-key string. */
|
|
@@ -46,9 +131,10 @@ export class Network extends ReadyResource {
|
|
|
46
131
|
return this.swarm ? this.swarm.peers : new Map()
|
|
47
132
|
}
|
|
48
133
|
|
|
49
|
-
/** @returns {Set<any>} Live connection streams. */
|
|
134
|
+
/** @returns {Set<any>} Live connection streams — swarm and injected. */
|
|
50
135
|
get connections() {
|
|
51
|
-
return this.swarm ? this.swarm.connections : new Set()
|
|
136
|
+
if (!this._injected.size) return this.swarm ? this.swarm.connections : new Set()
|
|
137
|
+
return new Set([...(this.swarm ? this.swarm.connections : []), ...this._injected])
|
|
52
138
|
}
|
|
53
139
|
|
|
54
140
|
/** @returns {boolean} */
|
|
@@ -129,6 +215,15 @@ export class Network extends ReadyResource {
|
|
|
129
215
|
}
|
|
130
216
|
|
|
131
217
|
async _close() {
|
|
218
|
+
for (const conn of [...this._injected]) {
|
|
219
|
+
try {
|
|
220
|
+
conn.destroy()
|
|
221
|
+
} catch (err) {
|
|
222
|
+
safetyCatch(err)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
this._injected.clear()
|
|
226
|
+
|
|
132
227
|
for (const d of [...this._discoveries]) {
|
|
133
228
|
try {
|
|
134
229
|
await d.destroy()
|
|
@@ -137,6 +232,15 @@ export class Network extends ReadyResource {
|
|
|
137
232
|
}
|
|
138
233
|
}
|
|
139
234
|
|
|
235
|
+
if (this._blind) {
|
|
236
|
+
try {
|
|
237
|
+
await (await this._blind).close()
|
|
238
|
+
} catch (err) {
|
|
239
|
+
safetyCatch(err)
|
|
240
|
+
}
|
|
241
|
+
this._blind = null
|
|
242
|
+
}
|
|
243
|
+
|
|
140
244
|
if (this.swarm) {
|
|
141
245
|
try {
|
|
142
246
|
await this.flush()
|
|
@@ -195,9 +299,7 @@ export class Network extends ReadyResource {
|
|
|
195
299
|
attach(core) {
|
|
196
300
|
if (!core) throw CeroError.REQUIRED('core')
|
|
197
301
|
this._replicateables.add(core)
|
|
198
|
-
|
|
199
|
-
for (const stream of this.swarm.connections) replicateInto(core, stream)
|
|
200
|
-
}
|
|
302
|
+
for (const stream of this.connections) replicateInto(core, stream)
|
|
201
303
|
}
|
|
202
304
|
|
|
203
305
|
/**
|
|
@@ -223,13 +325,24 @@ export class Network extends ReadyResource {
|
|
|
223
325
|
if (!target || typeof target.replicate !== 'function') {
|
|
224
326
|
throw CeroError.INVALID('target must be an object with a replicate(stream) method')
|
|
225
327
|
}
|
|
226
|
-
|
|
227
|
-
for (const stream of this.swarm.connections) replicateInto(target, stream)
|
|
228
|
-
}
|
|
328
|
+
for (const stream of this.connections) replicateInto(target, stream)
|
|
229
329
|
}
|
|
230
330
|
}
|
|
231
331
|
|
|
332
|
+
// Corestore replication is store-wide: N attached bees on one store would
|
|
333
|
+
// re-attach every core AND add N duplicate StreamTracker records per
|
|
334
|
+
// connection. Replicate each root store once per stream; wakeup streams are
|
|
335
|
+
// added at the network level, so skipped bees lose nothing.
|
|
336
|
+
const replicatedRoots = new WeakMap()
|
|
337
|
+
|
|
232
338
|
function replicateInto(core, stream) {
|
|
339
|
+
const root = core.store ? core.store.root || core.store : null
|
|
340
|
+
if (root) {
|
|
341
|
+
let seen = replicatedRoots.get(stream)
|
|
342
|
+
if (!seen) replicatedRoots.set(stream, (seen = new WeakSet()))
|
|
343
|
+
if (seen.has(root)) return
|
|
344
|
+
seen.add(root)
|
|
345
|
+
}
|
|
233
346
|
try {
|
|
234
347
|
core.replicate(stream)
|
|
235
348
|
} catch (err) {
|
package/src/pairing/index.js
CHANGED
|
@@ -88,13 +88,14 @@ export class Pairing extends ReadyResource {
|
|
|
88
88
|
|
|
89
89
|
async _open() {
|
|
90
90
|
await this.network.ready()
|
|
91
|
-
|
|
92
|
-
await this.
|
|
91
|
+
// shared instance — one set of swarm/DHT listeners for every handle
|
|
92
|
+
this._blind = await this.network.blind()
|
|
93
93
|
|
|
94
94
|
this._member = this._blind.addMember({
|
|
95
95
|
discoveryKey: discoveryKey(this.topic),
|
|
96
96
|
onadd: (req) => this._onCandidate(req).catch(this._onerror)
|
|
97
97
|
})
|
|
98
|
+
await this.network.refreshInjected()
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
async _close() {
|
|
@@ -105,10 +106,8 @@ export class Pairing extends ReadyResource {
|
|
|
105
106
|
await this._member.close()
|
|
106
107
|
this._member = null
|
|
107
108
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
this._blind = null
|
|
111
|
-
}
|
|
109
|
+
// the BlindPairing is network-owned and shared — never close it here
|
|
110
|
+
this._blind = null
|
|
112
111
|
this._invites.clear()
|
|
113
112
|
}
|
|
114
113
|
|
|
@@ -415,6 +414,7 @@ class Candidate {
|
|
|
415
414
|
onadd: (result) => this._done(result)
|
|
416
415
|
})
|
|
417
416
|
this._candidate.request.on('rejected', (err) => this._fail(fromBlindError(err)))
|
|
417
|
+
this.pairing.network.refreshInjected().catch(this._onerror ?? (() => {}))
|
|
418
418
|
} catch (err) {
|
|
419
419
|
this._fail(err instanceof CeroError ? err : CeroError.NETWORK_ERROR(err.message))
|
|
420
420
|
}
|
package/src/rpc/index.js
CHANGED
|
@@ -108,13 +108,13 @@ export function bindCodec(spec) {
|
|
|
108
108
|
},
|
|
109
109
|
encodeAction(handle, op, data) {
|
|
110
110
|
const ref = handle?.[op]
|
|
111
|
-
if (!ref || !ref.schema) throw
|
|
111
|
+
if (!ref || !ref.schema) throw CeroError.UNKNOWN('action', op)
|
|
112
112
|
if (data == null) return EMPTY
|
|
113
113
|
return schema.encode(ref.schema, data)
|
|
114
114
|
},
|
|
115
115
|
decodeAction(handle, op, buf) {
|
|
116
116
|
const ref = handle?.[op]
|
|
117
|
-
if (!ref || !ref.schema) throw
|
|
117
|
+
if (!ref || !ref.schema) throw CeroError.UNKNOWN('action', op)
|
|
118
118
|
if (!buf || buf.length === 0) return undefined
|
|
119
119
|
return schema.decode(ref.schema, buf)
|
|
120
120
|
}
|
package/src/storage/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
|
|
1
3
|
import HypercoreStorage from 'hypercore-storage'
|
|
2
4
|
import Corestore from 'corestore'
|
|
3
5
|
import HyperDB from 'hyperdb'
|
|
@@ -7,12 +9,15 @@ import { ROCKS, BEE, SINGLE, COLLECTION } from '../lib/constants.js'
|
|
|
7
9
|
import { genId, subscribe } from '../lib/utils.js'
|
|
8
10
|
import { CeroError } from '../lib/errors.js'
|
|
9
11
|
|
|
12
|
+
const RANGE_OPS = new Set(['gt', 'gte', 'lt', 'lte', 'reverse', 'limit'])
|
|
13
|
+
|
|
10
14
|
/**
|
|
11
15
|
* @typedef {object} StorageOpts
|
|
12
16
|
* @property {{ database: any, meta?: { ns?: string, refs?: Record<string, { kind?: string }> } }} spec
|
|
13
17
|
* @property {'rocks' | 'bee'} backend
|
|
14
18
|
* @property {any} [root] Pre-existing HypercoreStorage to reuse.
|
|
15
19
|
* @property {any} [store] Pre-existing Corestore to reuse.
|
|
20
|
+
* @property {Uint8Array} [storageKey] 32-byte key encrypting the backing core at rest (bee backend only).
|
|
16
21
|
*
|
|
17
22
|
* @typedef {{ name: string, kind: string }} Ref
|
|
18
23
|
* @typedef {{ id?: string, createdAt: number, updatedAt: number, [k: string]: any }} StoredRow
|
|
@@ -31,17 +36,22 @@ export class Storage extends ReadyResource {
|
|
|
31
36
|
* @param {string} dir
|
|
32
37
|
* @param {StorageOpts} [opts]
|
|
33
38
|
*/
|
|
34
|
-
constructor(dir, { spec, backend, root, store } = {}) {
|
|
39
|
+
constructor(dir, { spec, backend, root, store, storageKey } = {}) {
|
|
35
40
|
super()
|
|
36
41
|
if (!root && !store && (typeof dir !== 'string' || !dir))
|
|
37
42
|
throw CeroError.REQUIRED('dir, root, or store')
|
|
38
43
|
if (!spec || !spec.database) throw CeroError.REQUIRED('spec.database')
|
|
39
44
|
if (backend !== ROCKS && backend !== BEE)
|
|
40
45
|
throw CeroError.INVALID('backend must be "rocks" or "bee"')
|
|
46
|
+
if (storageKey && backend === ROCKS)
|
|
47
|
+
throw CeroError.INVALID('storageKey requires the bee backend')
|
|
48
|
+
if (storageKey && storageKey.byteLength !== 32)
|
|
49
|
+
throw CeroError.INVALID('storageKey must be 32 bytes')
|
|
41
50
|
|
|
42
51
|
this.dir = dir
|
|
43
52
|
this.spec = spec
|
|
44
53
|
this.backend = backend
|
|
54
|
+
this.storageKey = storageKey || null
|
|
45
55
|
this.ns = spec.meta?.ns || 'cero'
|
|
46
56
|
this.refs = spec.meta?.refs || {}
|
|
47
57
|
|
|
@@ -77,6 +87,7 @@ export class Storage extends ReadyResource {
|
|
|
77
87
|
if (this._ownsRoot) {
|
|
78
88
|
this.root = new HypercoreStorage(this.dir, { columnFamilies: [this._cf] })
|
|
79
89
|
await this.root.ready()
|
|
90
|
+
await fs.promises.chmod(this.dir, 0o700)
|
|
80
91
|
}
|
|
81
92
|
|
|
82
93
|
if (this._ownsStore) {
|
|
@@ -90,7 +101,7 @@ export class Storage extends ReadyResource {
|
|
|
90
101
|
const cf = this.root.rocks.columnFamily(this._cf)
|
|
91
102
|
this.db = HyperDB.rocks(cf, this.spec.database)
|
|
92
103
|
} else {
|
|
93
|
-
const core = this.store.get({ name: 'local' })
|
|
104
|
+
const core = this.store.get({ name: 'local', encryptionKey: this.storageKey })
|
|
94
105
|
await core.ready()
|
|
95
106
|
this.db = HyperDB.bee(core, this.spec.database, { extension: false, autoUpdate: true })
|
|
96
107
|
}
|
|
@@ -193,9 +204,19 @@ export class Storage extends ReadyResource {
|
|
|
193
204
|
return { data: (await this.db.get(col, { id: query })) ?? null }
|
|
194
205
|
}
|
|
195
206
|
|
|
207
|
+
// hyperdb only honors range/limit operators — equality fields must be
|
|
208
|
+
// filtered here or they are silently ignored and every row comes back
|
|
209
|
+
const eq = Object.keys(query || {}).filter((k) => !RANGE_OPS.has(k))
|
|
210
|
+
if (eq.length) {
|
|
211
|
+
const rows = await this.db.find(col, {}).toArray()
|
|
212
|
+
const matched = rows.filter((r) => eq.every((k) => r[k] === query[k]))
|
|
213
|
+
const data = query.limit != null ? matched.slice(0, query.limit) : matched
|
|
214
|
+
return { data, total: matched.length, size: data.length }
|
|
215
|
+
}
|
|
216
|
+
|
|
196
217
|
const data = await this.db.find(col, query || {}).toArray()
|
|
197
|
-
// a
|
|
198
|
-
//
|
|
218
|
+
// a limit caps `data`, so `total` needs a full count; with no query
|
|
219
|
+
// `data` is already everything — skip the second scan.
|
|
199
220
|
const filtered = query && Object.keys(query).length > 0
|
|
200
221
|
const total = filtered ? (await this.db.find(col, {}).toArray()).length : data.length
|
|
201
222
|
return { data, total, size: data.length }
|
|
@@ -212,8 +233,10 @@ export class Storage extends ReadyResource {
|
|
|
212
233
|
this._guard()
|
|
213
234
|
const ref = this._ref(name)
|
|
214
235
|
const col = this._col(ref)
|
|
215
|
-
const
|
|
216
|
-
|
|
236
|
+
const eq = Object.keys(query || {}).filter((k) => !RANGE_OPS.has(k))
|
|
237
|
+
const rows = await this.db.find(col, eq.length ? {} : query || {}).toArray()
|
|
238
|
+
const matched = eq.length ? rows.filter((r) => eq.every((k) => r[k] === query[k])) : rows
|
|
239
|
+
return { data: matched.length }
|
|
217
240
|
}
|
|
218
241
|
|
|
219
242
|
/**
|
|
@@ -259,8 +282,7 @@ export class Storage extends ReadyResource {
|
|
|
259
282
|
async _read(ref, id) {
|
|
260
283
|
if (ref.kind === SINGLE) return this.db.findOne(this._col(ref), {})
|
|
261
284
|
if (id == null) return null
|
|
262
|
-
|
|
263
|
-
return rows.find((r) => r.id === id) ?? null
|
|
285
|
+
return (await this.db.get(this._col(ref), { id })) ?? null
|
|
264
286
|
}
|
|
265
287
|
|
|
266
288
|
/** @param {Ref} ref @param {Record<string, any>} row @returns {Promise<void>} */
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* @param {{ dispatch: { Router: Function }, meta?: { refs?: Record<string, { kind?: string, builtin?: boolean, verb?: string }> } }} spec Generated hyperdispatch spec.
|
|
6
6
|
* @param {string} ns Namespace prefix for collection and op names.
|
|
7
7
|
* @param {Record<string, Function>} routes Custom action handlers keyed by route name.
|
|
8
|
+
* @param {(err: Error) => void} [onerror] Called when a malformed node is skipped.
|
|
8
9
|
* @returns {{ dispatcher: object, apply: (nodes: Array<{ value: Buffer, key: Buffer }>, view: object, host: object) => Promise<void> }}
|
|
9
10
|
*/
|
|
10
11
|
export function makeDispatcher(spec: {
|
|
@@ -18,7 +19,7 @@ export function makeDispatcher(spec: {
|
|
|
18
19
|
verb?: string;
|
|
19
20
|
}>;
|
|
20
21
|
};
|
|
21
|
-
}, ns: string, routes: Record<string, Function
|
|
22
|
+
}, ns: string, routes: Record<string, Function>, onerror?: (err: Error) => void, getDbKey?: () => any): {
|
|
22
23
|
dispatcher: object;
|
|
23
24
|
apply: (nodes: Array<{
|
|
24
25
|
value: Buffer;
|
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
* @property {Uint8Array | null} [encryptionKey] Optional encryption key; falls back to identity's key.
|
|
10
10
|
* @property {(nodes: any, view: any, host: any) => Promise<void>} [apply] Override the default apply function.
|
|
11
11
|
* @property {Uint8Array | null} [key] Existing autobee key to reopen.
|
|
12
|
+
* @property {boolean} [passive] Join discovery server-only (reachable but not searching). Flip at runtime with `setActive`.
|
|
12
13
|
* @property {import('../identity/index.js').KeyPair} [keyPair] Device writer keypair; defaults to identity's keypair.
|
|
13
|
-
* @property {(err: Error) => void} [onerror] Called when a background after-hook or onApply callback fails.
|
|
14
|
+
* @property {(err: Error) => void} [onerror] Called when a background after-hook or onApply callback fails, a malformed node is skipped, or the bee errors.
|
|
14
15
|
*
|
|
15
16
|
* @typedef {{ data: any | null }} SingleResult
|
|
16
|
-
* @typedef {{ data: any[], total: number, size: number }} ListResult
|
|
17
|
-
* @typedef {{ gt?: string, gte?: string, lt?: string, lte?: string, reverse?: boolean, limit?: number, search?: string, fields?: string[] }} Query
|
|
17
|
+
* @typedef {{ data: any[], total: number | null, size: number }} ListResult `total` is null when a limited read skipped the full count — pass `{ total: true }` to force it.
|
|
18
|
+
* @typedef {{ gt?: string, gte?: string, lt?: string, lte?: string, reverse?: boolean, limit?: number, search?: string, fields?: string[], total?: boolean }} Query
|
|
18
19
|
* @typedef {{ kind: string, verb: string, name: string }} Ref
|
|
19
20
|
* @typedef {(ctx: any) => any | Promise<any>} HookFn
|
|
20
21
|
*/
|
|
@@ -58,6 +59,7 @@ export class Database extends ReadyResource {
|
|
|
58
59
|
encryptionKey: Uint8Array<ArrayBufferLike>;
|
|
59
60
|
applyOverride: (nodes: any, view: any, host: any) => Promise<void>;
|
|
60
61
|
key: Uint8Array<ArrayBufferLike>;
|
|
62
|
+
passive: boolean;
|
|
61
63
|
keyPair: import("../index.js").KeyPair | {
|
|
62
64
|
publicKey: Uint8Array<ArrayBufferLike>;
|
|
63
65
|
secretKey: Uint8Array<ArrayBufferLike>;
|
|
@@ -73,13 +75,22 @@ export class Database extends ReadyResource {
|
|
|
73
75
|
};
|
|
74
76
|
beforeHooks: Map<any, any>;
|
|
75
77
|
afterHooks: Map<any, any>;
|
|
76
|
-
updaters:
|
|
78
|
+
updaters: Map<any, any>;
|
|
77
79
|
onApplyHooks: Set<any>;
|
|
78
80
|
_applySeq: number;
|
|
81
|
+
_touched: Set<any>;
|
|
79
82
|
txQueue: any[];
|
|
80
83
|
_discovery: import("../network/discovery.js").Discovery;
|
|
81
84
|
/** Open the underlying autobee, wire dispatcher + apply, attach to network. */
|
|
82
85
|
openBee(): Promise<void>;
|
|
86
|
+
/**
|
|
87
|
+
* Flip announce mode at runtime — passive stays reachable (server) but
|
|
88
|
+
* stops actively looking (client). Cheap; use it to demote idle rooms.
|
|
89
|
+
*
|
|
90
|
+
* @param {boolean} active
|
|
91
|
+
* @returns {Promise<void>}
|
|
92
|
+
*/
|
|
93
|
+
setActive(active: boolean): Promise<void>;
|
|
83
94
|
/** @returns {Uint8Array | null} discovery key of the underlying bee */
|
|
84
95
|
get discoveryKey(): Uint8Array | null;
|
|
85
96
|
/** @returns {Uint8Array | null} this device's local writer key */
|
|
@@ -107,12 +118,16 @@ export class Database extends ReadyResource {
|
|
|
107
118
|
*/
|
|
108
119
|
after(op: string, fn: HookFn): () => void;
|
|
109
120
|
/**
|
|
110
|
-
* Subscribe to local apply notifications. Fires whenever the view updates
|
|
121
|
+
* Subscribe to local apply notifications. Fires whenever the view updates;
|
|
122
|
+
* pass `scope` (a ref name) to fire only when that ref was touched.
|
|
111
123
|
*
|
|
112
124
|
* @param {() => void} fn
|
|
125
|
+
* @param {string} [scope]
|
|
113
126
|
* @returns {() => void} disposer
|
|
114
127
|
*/
|
|
115
|
-
onUpdate(fn: () => void): () => void;
|
|
128
|
+
onUpdate(fn: () => void, scope?: string): () => void;
|
|
129
|
+
_touch(nodes: any): void;
|
|
130
|
+
_byVerb: Map<any, any>;
|
|
116
131
|
/**
|
|
117
132
|
* Observe every applied op — local AND replicated (apply processes the merged
|
|
118
133
|
* log). The callback receives `{ op, name, row, writerKey, seq }` and runs
|
|
@@ -210,6 +225,9 @@ export class Database extends ReadyResource {
|
|
|
210
225
|
* @returns {Promise<SingleResult | ListResult>}
|
|
211
226
|
*/
|
|
212
227
|
get(name: string, query?: string | Query): Promise<SingleResult | ListResult>;
|
|
228
|
+
_orderReady(name: any, col: any): Promise<any>;
|
|
229
|
+
_orderOk: Map<any, any>;
|
|
230
|
+
_total(col: any, query: any, rows: any): Promise<any>;
|
|
213
231
|
/**
|
|
214
232
|
* Number of rows that match `query` (or total if omitted).
|
|
215
233
|
*
|
|
@@ -359,21 +377,28 @@ export type DatabaseOpts = {
|
|
|
359
377
|
* Existing autobee key to reopen.
|
|
360
378
|
*/
|
|
361
379
|
key?: Uint8Array | null;
|
|
380
|
+
/**
|
|
381
|
+
* Join discovery server-only (reachable but not searching). Flip at runtime with `setActive`.
|
|
382
|
+
*/
|
|
383
|
+
passive?: boolean;
|
|
362
384
|
/**
|
|
363
385
|
* Device writer keypair; defaults to identity's keypair.
|
|
364
386
|
*/
|
|
365
387
|
keyPair?: import("../identity/index.js").KeyPair;
|
|
366
388
|
/**
|
|
367
|
-
* Called when a background after-hook or onApply callback fails.
|
|
389
|
+
* Called when a background after-hook or onApply callback fails, a malformed node is skipped, or the bee errors.
|
|
368
390
|
*/
|
|
369
391
|
onerror?: (err: Error) => void;
|
|
370
392
|
};
|
|
371
393
|
export type SingleResult = {
|
|
372
394
|
data: any | null;
|
|
373
395
|
};
|
|
396
|
+
/**
|
|
397
|
+
* `total` is null when a limited read skipped the full count — pass `{ total: true }` to force it.
|
|
398
|
+
*/
|
|
374
399
|
export type ListResult = {
|
|
375
400
|
data: any[];
|
|
376
|
-
total: number;
|
|
401
|
+
total: number | null;
|
|
377
402
|
size: number;
|
|
378
403
|
};
|
|
379
404
|
export type Query = {
|
|
@@ -385,6 +410,7 @@ export type Query = {
|
|
|
385
410
|
limit?: number;
|
|
386
411
|
search?: string;
|
|
387
412
|
fields?: string[];
|
|
413
|
+
total?: boolean;
|
|
388
414
|
};
|
|
389
415
|
export type Ref = {
|
|
390
416
|
kind: string;
|
|
@@ -125,6 +125,15 @@ export class Identity {
|
|
|
125
125
|
* @type {Uint8Array}
|
|
126
126
|
*/
|
|
127
127
|
seed: Uint8Array;
|
|
128
|
+
/**
|
|
129
|
+
* Redacted — secretKey/encryptionKey/seed must never reach JSON.stringify
|
|
130
|
+
* or a structured logger.
|
|
131
|
+
*
|
|
132
|
+
* @returns {{ id: string }}
|
|
133
|
+
*/
|
|
134
|
+
toJSON(): {
|
|
135
|
+
id: string;
|
|
136
|
+
};
|
|
128
137
|
/**
|
|
129
138
|
* Detached Ed25519 signature over `message`.
|
|
130
139
|
*
|
package/types/lib/errors.d.ts
CHANGED
|
@@ -42,6 +42,12 @@ export class CeroError extends Error {
|
|
|
42
42
|
* @param {string} name
|
|
43
43
|
*/
|
|
44
44
|
static NOT_READY(resource: string, name: string): CeroError;
|
|
45
|
+
/**
|
|
46
|
+
* Operation raced an existing state (e.g. double init).
|
|
47
|
+
*
|
|
48
|
+
* @param {string} msg
|
|
49
|
+
*/
|
|
50
|
+
static CONFLICT(msg: string): CeroError;
|
|
45
51
|
/**
|
|
46
52
|
* Resource has been destroyed.
|
|
47
53
|
*
|
package/types/lib/utils.d.ts
CHANGED
|
@@ -39,11 +39,9 @@ export const toId: (key: Uint8Array) => string;
|
|
|
39
39
|
* @type {(id: string) => Uint8Array}
|
|
40
40
|
*/
|
|
41
41
|
export const toKey: (id: string) => Uint8Array;
|
|
42
|
-
/**
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
*/
|
|
47
|
-
export const isKeyId: (id: string) => boolean;
|
|
42
|
+
/** @type {(dbKey: Uint8Array, writer: Uint8Array, appender: Uint8Array) => Uint8Array} */
|
|
43
|
+
export const addWriterPayload: (dbKey: Uint8Array, writer: Uint8Array, appender: Uint8Array) => Uint8Array;
|
|
44
|
+
/** @type {(dbKey: Uint8Array, writer: Uint8Array) => Uint8Array} */
|
|
45
|
+
export const claimWriterPayload: (dbKey: Uint8Array, writer: Uint8Array) => Uint8Array;
|
|
48
46
|
export function grants(a: any, b: any): boolean;
|
|
49
47
|
export function outranks(a: any, b: any): boolean;
|