@cero-base/cero 2.1.8 → 2.2.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 +6 -6
- package/src/build/internal.js +2 -1
- package/src/build/schemas.js +5 -0
- package/src/handle/index.js +48 -39
- package/src/index.js +14 -3
- package/src/rpc/client.js +15 -0
- package/src/rpc/server.js +19 -1
- package/types/build/schemas.d.ts +5 -0
- package/types/index.d.ts +2 -2
- package/types/rpc/client.d.ts +1 -0
- package/types/rpc/server.d.ts +5 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cero-base/cero",
|
|
3
|
-
"version": "2.1
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"description": "The ideal p2p API — everything is a handle, handles contain refs, refs contain rows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -99,8 +99,8 @@
|
|
|
99
99
|
"test:node": "find test -name '*.test.js' | sort | xargs -P4 -n1 brittle-node"
|
|
100
100
|
},
|
|
101
101
|
"dependencies": {
|
|
102
|
-
"@cero-base/core": "^2.1
|
|
103
|
-
"b4a": "^1.
|
|
102
|
+
"@cero-base/core": "^2.2.1",
|
|
103
|
+
"b4a": "^1.9.0",
|
|
104
104
|
"bare-abort-controller": "^1.1.2",
|
|
105
105
|
"bare-crypto": "^1.15.3",
|
|
106
106
|
"bare-fs": "^4.8.1",
|
|
@@ -112,10 +112,10 @@
|
|
|
112
112
|
"hypercore": "^11.36.1",
|
|
113
113
|
"hypercore-crypto": "^3.7.0",
|
|
114
114
|
"hypercore-id-encoding": "^1.3.0",
|
|
115
|
-
"hypercore-storage": "^3.
|
|
115
|
+
"hypercore-storage": "^3.3.1",
|
|
116
116
|
"hyperdb": "^6.9.0",
|
|
117
117
|
"hyperdispatch": "^1.6.0",
|
|
118
|
-
"hyperschema": "^1.26.
|
|
118
|
+
"hyperschema": "^1.26.2",
|
|
119
119
|
"ready-resource": "^1.2.0",
|
|
120
120
|
"safety-catch": "^1.0.3",
|
|
121
121
|
"streamx": "^2.28.1",
|
|
@@ -125,7 +125,7 @@
|
|
|
125
125
|
"devDependencies": {
|
|
126
126
|
"@hyperswarm/testnet": "^3.1.4",
|
|
127
127
|
"bare-events": "^2.9.2",
|
|
128
|
-
"bare-fetch": "^3.
|
|
128
|
+
"bare-fetch": "^3.4.0",
|
|
129
129
|
"bare-process": "^4.5.1",
|
|
130
130
|
"bare-url": "^2.5.4",
|
|
131
131
|
"brittle": "^4.2.0",
|
package/src/build/internal.js
CHANGED
|
@@ -111,7 +111,8 @@ const COMMANDS = [
|
|
|
111
111
|
['rotate', 'req-handle', 'res-epoch'],
|
|
112
112
|
['set-active', 'req-set-active', 'res-ok'],
|
|
113
113
|
['suspend', 'req-empty', 'res-ok'],
|
|
114
|
-
['resume', 'req-empty', 'res-ok']
|
|
114
|
+
['resume', 'req-empty', 'res-ok'],
|
|
115
|
+
['errors', 'req-empty', 'res-error', true]
|
|
115
116
|
]
|
|
116
117
|
|
|
117
118
|
export function commands(ns) {
|
package/src/build/schemas.js
CHANGED
package/src/handle/index.js
CHANGED
|
@@ -116,7 +116,7 @@ export class Handle extends ReadyResource {
|
|
|
116
116
|
this._opts = opts.opts || {}
|
|
117
117
|
this.extensions = parent?.extensions || extensionsOf(spec, this._opts.extensions)
|
|
118
118
|
this.operators = parent?.operators || operatorsOf(spec, this._opts.operators)
|
|
119
|
-
this._onerror = this._opts.onerror ||
|
|
119
|
+
this._onerror = this._opts.onerror || ((err) => console.error(err))
|
|
120
120
|
this.children = parent ? null : new Set()
|
|
121
121
|
this._typeHooks = parent ? null : new Set()
|
|
122
122
|
this._loading = parent ? null : new Map()
|
|
@@ -260,36 +260,40 @@ export class Handle extends ReadyResource {
|
|
|
260
260
|
for (const hex of this._blobKeys || []) this.root._coreKeys.delete(hex)
|
|
261
261
|
for (const r of [...this._owned]) r.destroy?.()
|
|
262
262
|
this._owned.clear()
|
|
263
|
-
if (this._blobs) await this._blobs.close()
|
|
264
|
-
for (const b of this._epochBlobs?.values() || []) await b.close().catch(safetyCatch)
|
|
265
|
-
this._epochBlobs = null
|
|
266
|
-
if (this.children) {
|
|
267
|
-
for (const c of [...this.children]) await c.close()
|
|
268
|
-
this.children.clear()
|
|
269
|
-
}
|
|
270
263
|
if (this._invitesSync) {
|
|
271
264
|
this.store.off('update', this._invitesSync)
|
|
272
265
|
this._invitesSync = null
|
|
273
266
|
}
|
|
274
|
-
if (this.
|
|
267
|
+
if (this._discovery) this._discovery.destroy().catch(safetyCatch)
|
|
268
|
+
|
|
269
|
+
const steps = [
|
|
270
|
+
() => this._blobs?.close(),
|
|
271
|
+
...[...(this._epochBlobs?.values() || [])].map((b) => () => b.close()),
|
|
272
|
+
...[...(this.children || [])].map((c) => () => c.close()),
|
|
273
|
+
() => this.pair?.close()
|
|
274
|
+
]
|
|
275
|
+
this._epochBlobs = null
|
|
276
|
+
this.children?.clear()
|
|
275
277
|
|
|
276
278
|
if (this.parent) {
|
|
277
279
|
this.parent.children?.delete(this)
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
280
|
+
steps.push(() => this.store.close())
|
|
281
|
+
} else {
|
|
282
|
+
const store = this.store.store
|
|
283
|
+
steps.push(
|
|
284
|
+
() => this.bluetooth?.close(),
|
|
285
|
+
() => this.local?.close(),
|
|
286
|
+
async () => {
|
|
287
|
+
await this._fileServer?.close()
|
|
288
|
+
this._fileServer = null
|
|
289
|
+
},
|
|
290
|
+
() => this.store.close(),
|
|
291
|
+
() => this.network.close(),
|
|
292
|
+
() => store.close(),
|
|
293
|
+
() => this._storage?.close()
|
|
294
|
+
)
|
|
286
295
|
}
|
|
287
|
-
|
|
288
|
-
await this.store.close()
|
|
289
|
-
if (this._discovery) this._discovery.destroy().catch(safetyCatch)
|
|
290
|
-
await this.network.close()
|
|
291
|
-
await store.close()
|
|
292
|
-
if (this._storage) await this._storage.close()
|
|
296
|
+
await settle(steps)
|
|
293
297
|
}
|
|
294
298
|
|
|
295
299
|
/**
|
|
@@ -862,26 +866,18 @@ export class Handle extends ReadyResource {
|
|
|
862
866
|
|
|
863
867
|
async _suspend() {
|
|
864
868
|
if (this.closing || this.closed) return
|
|
865
|
-
await Promise.all([...this.children].map((c) => c.pair?.suspend()))
|
|
866
|
-
await this.bluetooth?.suspend()
|
|
867
|
-
await this.network.suspend()
|
|
868
|
-
|
|
869
|
-
await this.store.store.suspend()
|
|
870
|
-
} catch (err) {
|
|
871
|
-
this._onerror(err)
|
|
872
|
-
}
|
|
869
|
+
await Promise.all([...this.children].map((c) => c.pair?.suspend().catch(this._onerror)))
|
|
870
|
+
await this.bluetooth?.suspend().catch(this._onerror)
|
|
871
|
+
await this.network.suspend().catch(this._onerror)
|
|
872
|
+
await this.store.store.suspend().catch(this._onerror)
|
|
873
873
|
}
|
|
874
874
|
|
|
875
875
|
async _resume() {
|
|
876
876
|
if (this.closing || this.closed) return
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
}
|
|
882
|
-
await this.network.resume()
|
|
883
|
-
await this.bluetooth?.resume()
|
|
884
|
-
await Promise.all([...this.children].map((child) => child.pair?.resume()))
|
|
877
|
+
await this.store.store.resume().catch(this._onerror)
|
|
878
|
+
await this.network.resume().catch(this._onerror)
|
|
879
|
+
await this.bluetooth?.resume().catch(this._onerror)
|
|
880
|
+
await Promise.all([...this.children].map((c) => c.pair?.resume().catch(this._onerror)))
|
|
885
881
|
}
|
|
886
882
|
|
|
887
883
|
// a child is a child once it has its operators, the hooks declared for its type, and a slot
|
|
@@ -1019,3 +1015,16 @@ function pickHandle(spec, type) {
|
|
|
1019
1015
|
function randomNs() {
|
|
1020
1016
|
return z32.encode(Identity.randomBytes(8))
|
|
1021
1017
|
}
|
|
1018
|
+
|
|
1019
|
+
// run every teardown step, then surface the first failure: a bug must not leak the storage lock
|
|
1020
|
+
async function settle(steps) {
|
|
1021
|
+
let failed = null
|
|
1022
|
+
for (const step of steps) {
|
|
1023
|
+
try {
|
|
1024
|
+
await step()
|
|
1025
|
+
} catch (err) {
|
|
1026
|
+
failed ??= err
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
if (failed) throw failed
|
|
1030
|
+
}
|
package/src/index.js
CHANGED
|
@@ -67,7 +67,7 @@ export { t, schema } from './lib/spec.js'
|
|
|
67
67
|
* @property {Uint8Array} [key] Existing database key to recover into, skipping the pointer lookup.
|
|
68
68
|
* @property {Uint8Array} [encryptionKey] Pre-existing encryption key.
|
|
69
69
|
* @property {Record<string, Function>} [routes] Custom RPC routes for the database dispatcher.
|
|
70
|
-
* @property {(err: any) => void} [onerror] Background-task error handler.
|
|
70
|
+
* @property {(err: any) => void} [onerror] Background-task error handler; without one, errors emit 'error' on the root handle.
|
|
71
71
|
* @property {number} [recoveryTimeout] Max wait to find another device and be admitted, in ms. Defaults to 30000.
|
|
72
72
|
* @property {Uint8Array} [storageKey] 32-byte key encrypting local key material (master seed, device keypairs) at rest. Source it from the OS keychain — cero never stores it.
|
|
73
73
|
* @property {import('./extensions/index.js').Extension[]} [extensions] The extensions this instance runs, instead of the ones the spec carries. Build with the same list.
|
|
@@ -93,12 +93,23 @@ export async function cero(dir, spec, opts = {}) {
|
|
|
93
93
|
let network = null
|
|
94
94
|
let discovery = null
|
|
95
95
|
let me = null
|
|
96
|
+
// background failures reach the app through opts.onerror or the root's 'error' event, never silence
|
|
97
|
+
const onerror =
|
|
98
|
+
opts.onerror ||
|
|
99
|
+
((err) => (me?.listenerCount('error') ? me.emit('error', err) : console.error(err)))
|
|
100
|
+
opts = { ...opts, onerror }
|
|
96
101
|
// any failure during open must close what opened, or the storage lock leaks
|
|
97
102
|
try {
|
|
98
103
|
await storage.ready()
|
|
99
104
|
await fs.promises.chmod(`${dir}/main`, 0o700)
|
|
100
105
|
store = new Corestore(storage, { manifestVersion: 2 })
|
|
101
106
|
await store.ready()
|
|
107
|
+
store.watch((core) => {
|
|
108
|
+
const session = new Hypercore({ core, weak: true })
|
|
109
|
+
session.on('verification-error', onerror)
|
|
110
|
+
session.on('invalid-request', onerror)
|
|
111
|
+
session.on('conflict', () => onerror(CeroError.CONFLICT(`fork on core ${session.id}`)))
|
|
112
|
+
})
|
|
102
113
|
|
|
103
114
|
if (spec.local && spec.meta?.local) {
|
|
104
115
|
local = new Local(null, spec, { store, storageKey: opts.storageKey })
|
|
@@ -126,7 +137,8 @@ export async function cero(dir, spec, opts = {}) {
|
|
|
126
137
|
channel: opts.channel,
|
|
127
138
|
store,
|
|
128
139
|
mirrors: opts.mirrors,
|
|
129
|
-
presence: opts.presence
|
|
140
|
+
presence: opts.presence,
|
|
141
|
+
onerror
|
|
130
142
|
})
|
|
131
143
|
await network.ready()
|
|
132
144
|
discovery = network.join(identity.topic)
|
|
@@ -200,7 +212,6 @@ export async function cero(dir, spec, opts = {}) {
|
|
|
200
212
|
maxInbound: bt.maxInbound,
|
|
201
213
|
pipe: bt.pipe
|
|
202
214
|
})
|
|
203
|
-
me.once('close', () => me.bluetooth.close().catch(safetyCatch))
|
|
204
215
|
await me.bluetooth.ready()
|
|
205
216
|
}
|
|
206
217
|
|
package/src/rpc/client.js
CHANGED
|
@@ -457,6 +457,20 @@ export class Client extends RPCClient {
|
|
|
457
457
|
this.local = null
|
|
458
458
|
}
|
|
459
459
|
|
|
460
|
+
// the worker's background errors, delivered the way a local root delivers them
|
|
461
|
+
_pumpErrors() {
|
|
462
|
+
const report = (err) =>
|
|
463
|
+
this.listenerCount('error') ? this.emit('error', err) : console.error(err)
|
|
464
|
+
const pump = async () => {
|
|
465
|
+
for await (const { message, code, stack } of this.rpc.errors({})) {
|
|
466
|
+
report(Object.assign(new Error(message), code && { code }, stack && { stack }))
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
pump().catch((err) => {
|
|
470
|
+
if (err.code !== 'PREMATURE_CLOSE' && err.code !== 'CHANNEL_CLOSED') report(err)
|
|
471
|
+
})
|
|
472
|
+
}
|
|
473
|
+
|
|
460
474
|
async _open() {
|
|
461
475
|
await super._open()
|
|
462
476
|
const { id, deviceId, fileBase, fileToken } = await this.rpc.init({})
|
|
@@ -468,6 +482,7 @@ export class Client extends RPCClient {
|
|
|
468
482
|
Ref.attach(this, /** @type {Spec} */ (this.spec).meta.refs)
|
|
469
483
|
bind(this, null, this.operators)
|
|
470
484
|
if (/** @type {Spec} */ (this.spec).meta.local?.refs) this.local = new LocalRefs(this)
|
|
485
|
+
this._pumpErrors()
|
|
471
486
|
}
|
|
472
487
|
|
|
473
488
|
/** Pause networking and storage on the server. Idempotent. */
|
package/src/rpc/server.js
CHANGED
|
@@ -43,7 +43,10 @@ export class Server extends RPCServer {
|
|
|
43
43
|
super(ipc, spec)
|
|
44
44
|
if (spec.local?.schema && !spec.local.codec) bindCodec(spec.local)
|
|
45
45
|
this.storage = storage
|
|
46
|
-
this.
|
|
46
|
+
this._report = opts.onerror || ((err) => console.error(err))
|
|
47
|
+
this.opts = { ...opts, onerror: (err) => this._onerror(err) }
|
|
48
|
+
/** @type {Set<object>} open error streams, one per connected client */
|
|
49
|
+
this._errors = new Set()
|
|
47
50
|
this.me = null
|
|
48
51
|
this.handles = new Map()
|
|
49
52
|
/** @type {Map<string, Set<object>>} handle id → its open watch streams */
|
|
@@ -87,7 +90,22 @@ export class Server extends RPCServer {
|
|
|
87
90
|
}
|
|
88
91
|
|
|
89
92
|
/** Wire the `init` handler that lazily constructs the root cero handle. */
|
|
93
|
+
_onerror(err) {
|
|
94
|
+
if (!this._errors.size) return this._report(err)
|
|
95
|
+
const frame = {
|
|
96
|
+
message: err?.message || String(err),
|
|
97
|
+
code: err?.code || '',
|
|
98
|
+
stack: err?.stack || ''
|
|
99
|
+
}
|
|
100
|
+
for (const stream of this._errors) stream.write(frame)
|
|
101
|
+
}
|
|
102
|
+
|
|
90
103
|
_wireInit() {
|
|
104
|
+
this.rpc.onErrors((stream) => {
|
|
105
|
+
this._errors.add(stream)
|
|
106
|
+
stream.on('error', safetyCatch)
|
|
107
|
+
stream.on('close', () => this._errors.delete(stream))
|
|
108
|
+
})
|
|
91
109
|
this.rpc.onInit(async () => {
|
|
92
110
|
// a reloaded UI is a new client on the same worker: it re-attaches, its old streams end
|
|
93
111
|
if (this.me) {
|
package/types/build/schemas.d.ts
CHANGED
|
@@ -196,4 +196,9 @@ export declare const rpc: {
|
|
|
196
196
|
'res-epoch': {
|
|
197
197
|
epoch: import("@cero-base/core").Prim;
|
|
198
198
|
};
|
|
199
|
+
'res-error': {
|
|
200
|
+
message: import("@cero-base/core").Prim;
|
|
201
|
+
code: import("@cero-base/core").Prim;
|
|
202
|
+
stack: import("@cero-base/core").Prim;
|
|
203
|
+
};
|
|
199
204
|
};
|
package/types/index.d.ts
CHANGED
|
@@ -74,7 +74,7 @@ export type CeroOpts = {
|
|
|
74
74
|
*/
|
|
75
75
|
routes?: Record<string, Function>;
|
|
76
76
|
/**
|
|
77
|
-
* Background-task error handler.
|
|
77
|
+
* Background-task error handler; without one, errors emit 'error' on the root handle.
|
|
78
78
|
*/
|
|
79
79
|
onerror?: (err: any) => void;
|
|
80
80
|
/**
|
|
@@ -123,7 +123,7 @@ export type CeroOpts = {
|
|
|
123
123
|
* @property {Uint8Array} [key] Existing database key to recover into, skipping the pointer lookup.
|
|
124
124
|
* @property {Uint8Array} [encryptionKey] Pre-existing encryption key.
|
|
125
125
|
* @property {Record<string, Function>} [routes] Custom RPC routes for the database dispatcher.
|
|
126
|
-
* @property {(err: any) => void} [onerror] Background-task error handler.
|
|
126
|
+
* @property {(err: any) => void} [onerror] Background-task error handler; without one, errors emit 'error' on the root handle.
|
|
127
127
|
* @property {number} [recoveryTimeout] Max wait to find another device and be admitted, in ms. Defaults to 30000.
|
|
128
128
|
* @property {Uint8Array} [storageKey] 32-byte key encrypting local key material (master seed, device keypairs) at rest. Source it from the OS keychain — cero never stores it.
|
|
129
129
|
* @property {import('./extensions/index.js').Extension[]} [extensions] The extensions this instance runs, instead of the ones the spec carries. Build with the same list.
|
package/types/rpc/client.d.ts
CHANGED
|
@@ -89,6 +89,7 @@ export declare class Client extends RPCClient {
|
|
|
89
89
|
constructor(ipc: any, spec: Spec, opts?: {
|
|
90
90
|
operators?: Record<string, any>;
|
|
91
91
|
});
|
|
92
|
+
_pumpErrors(): void;
|
|
92
93
|
_open(): Promise<void>;
|
|
93
94
|
/** Pause networking and storage on the server. Idempotent. */
|
|
94
95
|
suspend(): Promise<void>;
|
package/types/rpc/server.d.ts
CHANGED
|
@@ -63,6 +63,7 @@ export type GetResult = {
|
|
|
63
63
|
*/
|
|
64
64
|
export declare class Server extends RPCServer {
|
|
65
65
|
storage: string;
|
|
66
|
+
_report: (err: Error) => void;
|
|
66
67
|
opts: {
|
|
67
68
|
name?: string;
|
|
68
69
|
bootstrap?: Array<{
|
|
@@ -70,8 +71,10 @@ export declare class Server extends RPCServer {
|
|
|
70
71
|
port: number;
|
|
71
72
|
}>;
|
|
72
73
|
isMobile?: boolean;
|
|
73
|
-
onerror
|
|
74
|
+
onerror: (err: any) => void;
|
|
74
75
|
};
|
|
76
|
+
/** @type {Set<object>} open error streams, one per connected client */
|
|
77
|
+
_errors: Set<object>;
|
|
75
78
|
me: import("../handle/index.js").CeroHandle;
|
|
76
79
|
handles: Map<any, any>;
|
|
77
80
|
/** @type {Map<string, Set<object>>} handle id → its open watch streams */
|
|
@@ -98,6 +101,7 @@ export declare class Server extends RPCServer {
|
|
|
98
101
|
/** End every watch stream bound to a handle (e.g. when it closes or leaves). */
|
|
99
102
|
_endWatches(handle: any): void;
|
|
100
103
|
/** Wire the `init` handler that lazily constructs the root cero handle. */
|
|
104
|
+
_onerror(err: any): void;
|
|
101
105
|
_wireInit(): void;
|
|
102
106
|
/** Wire the `restore` handler that rebuilds the local store from a phrase. */
|
|
103
107
|
_wireRestore(): void;
|