@cero-base/cero 1.13.0 → 1.14.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/package.json +4 -3
- package/src/bluetooth.js +3 -2
- package/src/handle/index.js +60 -10
- package/src/index.js +1 -1
- package/types/handle/index.d.ts +3 -1
- package/types/index.d.ts +3 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cero-base/cero",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
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,
|
|
@@ -95,13 +95,13 @@
|
|
|
95
95
|
"test:node": "ls test/*.test.js | xargs -P1 -n1 brittle-node"
|
|
96
96
|
},
|
|
97
97
|
"dependencies": {
|
|
98
|
-
"@cero-base/core": "^1.
|
|
98
|
+
"@cero-base/core": "^1.14.0",
|
|
99
99
|
"b4a": "^1.8.1",
|
|
100
100
|
"bare-abort-controller": "^1.1.2",
|
|
101
101
|
"bare-crypto": "^1.15.3",
|
|
102
102
|
"bare-fs": "^4.8.0",
|
|
103
103
|
"bare-path": "^3.1.1",
|
|
104
|
-
"ble-swarm": "
|
|
104
|
+
"ble-swarm": "^2.2.0",
|
|
105
105
|
"compact-encoding": "^3.3.2",
|
|
106
106
|
"corestore": "^7.12.0",
|
|
107
107
|
"hrpc": "^4.3.1",
|
|
@@ -114,6 +114,7 @@
|
|
|
114
114
|
"ready-resource": "^1.2.0",
|
|
115
115
|
"safety-catch": "^1.0.3",
|
|
116
116
|
"streamx": "^2.28.0",
|
|
117
|
+
"suspendify": "^1.7.2",
|
|
117
118
|
"z32": "^1.1.0"
|
|
118
119
|
},
|
|
119
120
|
"devDependencies": {
|
package/src/bluetooth.js
CHANGED
|
@@ -107,8 +107,9 @@ export class Bluetooth extends ReadyResource {
|
|
|
107
107
|
async stop() {
|
|
108
108
|
this._clearAnnounce()
|
|
109
109
|
await this.swarm.stop()
|
|
110
|
-
// while stopped setTopic just sticks — the next start() is back on the
|
|
111
|
-
|
|
110
|
+
// while stopped setTopic just sticks — the next start() is back on the
|
|
111
|
+
// mesh. A start() + announce() that landed during the await wins.
|
|
112
|
+
if (!this._announce) await this.swarm.setTopic(this._topic)
|
|
112
113
|
}
|
|
113
114
|
|
|
114
115
|
/**
|
package/src/handle/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import Hypercore from 'hypercore'
|
|
|
5
5
|
import { discoveryKey } from 'hypercore-crypto'
|
|
6
6
|
import ReadyResource from 'ready-resource'
|
|
7
7
|
import safetyCatch from 'safety-catch'
|
|
8
|
+
import Suspendify from 'suspendify'
|
|
8
9
|
import z32 from 'z32'
|
|
9
10
|
|
|
10
11
|
import { Identity } from '@cero-base/core/identity'
|
|
@@ -127,6 +128,10 @@ export class Handle extends ReadyResource {
|
|
|
127
128
|
this._fileServer = null
|
|
128
129
|
this._blobs = null
|
|
129
130
|
this._epochBlobs = null
|
|
131
|
+
// root only — serializes suspend/resume, converges rapid bounces
|
|
132
|
+
this._sus = parent
|
|
133
|
+
? null
|
|
134
|
+
: new Suspendify({ suspend: () => this._suspend(), resume: () => this._resume() })
|
|
130
135
|
this._owned = new Set()
|
|
131
136
|
|
|
132
137
|
this.store = new Database({
|
|
@@ -348,7 +353,10 @@ export class Handle extends ReadyResource {
|
|
|
348
353
|
blobs
|
|
349
354
|
.ready()
|
|
350
355
|
.then(() => {
|
|
351
|
-
|
|
356
|
+
// close prunes _coreKeys first — a ready() resolving after that must
|
|
357
|
+
// not re-insert the entry (it would outlive the handle on the root)
|
|
358
|
+
if (this.closing || this.closed || !blobs.key) return
|
|
359
|
+
this.root._coreKeys.set(b4a.toString(blobs.key, 'hex'), encryptionKey)
|
|
352
360
|
})
|
|
353
361
|
.catch(this._onerror)
|
|
354
362
|
return blobs
|
|
@@ -374,7 +382,9 @@ export class Handle extends ReadyResource {
|
|
|
374
382
|
if (stamp === undefined) {
|
|
375
383
|
this.store
|
|
376
384
|
.get('files', id)
|
|
377
|
-
.then(({ data }) =>
|
|
385
|
+
.then(({ data }) => {
|
|
386
|
+
if (data && !this.closing && !this.closed) this._registerBlobCore(id, data.stamp || 0)
|
|
387
|
+
})
|
|
378
388
|
.catch(safetyCatch)
|
|
379
389
|
return
|
|
380
390
|
}
|
|
@@ -412,7 +422,7 @@ export class Handle extends ReadyResource {
|
|
|
412
422
|
}
|
|
413
423
|
|
|
414
424
|
get suspended() {
|
|
415
|
-
return this.
|
|
425
|
+
return this._sus?.suspended === true
|
|
416
426
|
}
|
|
417
427
|
|
|
418
428
|
/**
|
|
@@ -571,14 +581,14 @@ export class Handle extends ReadyResource {
|
|
|
571
581
|
const sig = this.identity.sign(
|
|
572
582
|
addWriterPayload(this.store.key, writerKey, this.store.writerKey)
|
|
573
583
|
)
|
|
574
|
-
await this.store.tx(async () => {
|
|
575
|
-
await
|
|
584
|
+
await this.store.tx(async (tx) => {
|
|
585
|
+
await tx.call('add-writer', {
|
|
576
586
|
sig,
|
|
577
587
|
master: this.identity.publicKey,
|
|
578
588
|
writer: writerKey,
|
|
579
589
|
ts: member.updatedAt || Date.now()
|
|
580
590
|
})
|
|
581
|
-
await
|
|
591
|
+
await tx.call('add-member', member)
|
|
582
592
|
})
|
|
583
593
|
}
|
|
584
594
|
|
|
@@ -619,11 +629,25 @@ export class Handle extends ReadyResource {
|
|
|
619
629
|
)
|
|
620
630
|
// a failure after the child opens must close it — else it leaks its Database,
|
|
621
631
|
// swarm session and attached bee, never reaped by the parent (not yet a child)
|
|
632
|
+
let publish = null
|
|
633
|
+
let abort = null
|
|
634
|
+
let inflightId = null
|
|
622
635
|
try {
|
|
623
636
|
await child.ready()
|
|
624
637
|
child.name = name
|
|
625
638
|
|
|
626
639
|
const id = toId(child.store.key)
|
|
640
|
+
// Close the create/open race: the `add-handle` write below makes the row
|
|
641
|
+
// visible before this create finishes, so a concurrent _load for the id
|
|
642
|
+
// must share this child — a duplicate Handle over the same core
|
|
643
|
+
// deadlocks in ready().
|
|
644
|
+
const inflight = new Promise((resolve, reject) => {
|
|
645
|
+
publish = resolve
|
|
646
|
+
abort = reject
|
|
647
|
+
})
|
|
648
|
+
inflight.catch(safetyCatch)
|
|
649
|
+
inflightId = id
|
|
650
|
+
this._loading?.set(id, inflight)
|
|
627
651
|
await this._saveKeyPair(id, writer)
|
|
628
652
|
|
|
629
653
|
const ts = Date.now()
|
|
@@ -658,8 +682,12 @@ export class Handle extends ReadyResource {
|
|
|
658
682
|
bind(child, type)
|
|
659
683
|
this.children.add(child)
|
|
660
684
|
this.emit('handle', child, { name, role })
|
|
685
|
+
publish(child)
|
|
686
|
+
this._loading?.delete(id)
|
|
661
687
|
return child
|
|
662
688
|
} catch (err) {
|
|
689
|
+
if (abort) abort(err)
|
|
690
|
+
if (inflightId) this._loading?.delete(inflightId)
|
|
663
691
|
await child.close().catch(safetyCatch)
|
|
664
692
|
throw err
|
|
665
693
|
}
|
|
@@ -738,11 +766,23 @@ export class Handle extends ReadyResource {
|
|
|
738
766
|
)
|
|
739
767
|
// whenWritable timing out (host offline) is a normal failure — close the
|
|
740
768
|
// fully-opened child rather than leak its Database/pairing/swarm session
|
|
769
|
+
let publish = null
|
|
770
|
+
let abort = null
|
|
771
|
+
let inflightId = null
|
|
741
772
|
try {
|
|
742
773
|
await child.ready()
|
|
743
774
|
if (!child.store.writable) await child.store.whenWritable({ timeout: deadline })
|
|
744
775
|
|
|
745
776
|
const id = toId(child.store.key)
|
|
777
|
+
// same create/open race as _create: the add-handle row is visible before
|
|
778
|
+
// this join finishes — a concurrent _load must share this child
|
|
779
|
+
const inflight = new Promise((resolve, reject) => {
|
|
780
|
+
publish = resolve
|
|
781
|
+
abort = reject
|
|
782
|
+
})
|
|
783
|
+
inflight.catch(safetyCatch)
|
|
784
|
+
inflightId = id
|
|
785
|
+
this._loading?.set(id, inflight)
|
|
746
786
|
await this._saveKeyPair(id, child.store.keyPair)
|
|
747
787
|
|
|
748
788
|
const ts = Date.now()
|
|
@@ -759,8 +799,12 @@ export class Handle extends ReadyResource {
|
|
|
759
799
|
bind(child, type)
|
|
760
800
|
this.children.add(child)
|
|
761
801
|
this.emit('handle', child, {})
|
|
802
|
+
publish(child)
|
|
803
|
+
this._loading?.delete(id)
|
|
762
804
|
return child
|
|
763
805
|
} catch (err) {
|
|
806
|
+
if (abort) abort(err)
|
|
807
|
+
if (inflightId) this._loading?.delete(inflightId)
|
|
764
808
|
await child.close().catch(safetyCatch)
|
|
765
809
|
throw err
|
|
766
810
|
}
|
|
@@ -834,8 +878,11 @@ export class Handle extends ReadyResource {
|
|
|
834
878
|
* @returns {Promise<void>}
|
|
835
879
|
*/
|
|
836
880
|
async suspend() {
|
|
837
|
-
if (this.
|
|
838
|
-
|
|
881
|
+
if (this._sus) await this._sus.suspend()
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
async _suspend() {
|
|
885
|
+
if (this.closing || this.closed) return
|
|
839
886
|
await Promise.all([...this.children].map((c) => c.pair?.suspend()))
|
|
840
887
|
await this.bluetooth?.suspend()
|
|
841
888
|
await this.network.suspend()
|
|
@@ -852,8 +899,11 @@ export class Handle extends ReadyResource {
|
|
|
852
899
|
* @returns {Promise<void>}
|
|
853
900
|
*/
|
|
854
901
|
async resume() {
|
|
855
|
-
if (this.
|
|
856
|
-
|
|
902
|
+
if (this._sus) await this._sus.resume()
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
async _resume() {
|
|
906
|
+
if (this.closing || this.closed) return
|
|
857
907
|
try {
|
|
858
908
|
await this.store.store.resume()
|
|
859
909
|
} catch (err) {
|
package/src/index.js
CHANGED
|
@@ -74,7 +74,7 @@ export { t, schema } from './lib/spec.js'
|
|
|
74
74
|
* @property {number} [recoveryTimeout] Max wait for peer data + writer capability during recovery.
|
|
75
75
|
* @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.
|
|
76
76
|
* @property {boolean} [extensions] `false` disables the bundled extensions (profileSync, handleSync) for this instance. Build with `{ extensions: false }` too so the spec matches.
|
|
77
|
-
* @property {boolean | { autoStart?: boolean, backend?: any, maxOutbound?: number, maxInbound?: number }} [bluetooth] `true` enables nearby (Bluetooth) sync via `me.bluetooth` (auto-started). `{ autoStart: false }` creates the facade without starting the radio — the app calls `me.bluetooth.start()`/`stop()` (user toggle). `backend` injects a bare-bluetooth-shaped backend (tests). `maxOutbound`/`maxInbound` cap concurrent outbound links and inbound sessions. Absent backend on an unsupported host → `me.bluetooth.state === 'unsupported'`.
|
|
77
|
+
* @property {boolean | { autoStart?: boolean, backend?: any, maxOutbound?: number, maxInbound?: number, pipe?: 'l2cap' | 'gatt' }} [bluetooth] `true` enables nearby (Bluetooth) sync via `me.bluetooth` (auto-started). `{ autoStart: false }` creates the facade without starting the radio — the app calls `me.bluetooth.start()`/`stop()` (user toggle). `backend` injects a bare-bluetooth-shaped backend (tests). `maxOutbound`/`maxInbound` cap concurrent outbound links and inbound sessions. `pipe` picks the data pipe — `'l2cap'` (default, faster) or `'gatt'`; both peers must match. Absent backend on an unsupported host → `me.bluetooth.state === 'unsupported'`.
|
|
78
78
|
*/
|
|
79
79
|
|
|
80
80
|
/**
|
package/types/handle/index.d.ts
CHANGED
|
@@ -96,6 +96,7 @@ export class Handle extends ReadyResource {
|
|
|
96
96
|
_fileServer: FileServer;
|
|
97
97
|
_blobs: Blobs;
|
|
98
98
|
_epochBlobs: any;
|
|
99
|
+
_sus: any;
|
|
99
100
|
_owned: Set<any>;
|
|
100
101
|
store: Database;
|
|
101
102
|
pair: Pairing;
|
|
@@ -314,13 +315,14 @@ export class Handle extends ReadyResource {
|
|
|
314
315
|
* @returns {Promise<void>}
|
|
315
316
|
*/
|
|
316
317
|
suspend(): Promise<void>;
|
|
317
|
-
|
|
318
|
+
_suspend(): Promise<void>;
|
|
318
319
|
/**
|
|
319
320
|
* Resume a suspended root handle. Idempotent; no-op on child handles.
|
|
320
321
|
*
|
|
321
322
|
* @returns {Promise<void>}
|
|
322
323
|
*/
|
|
323
324
|
resume(): Promise<void>;
|
|
325
|
+
_resume(): Promise<void>;
|
|
324
326
|
/**
|
|
325
327
|
* @param {Handle} child
|
|
326
328
|
* @param {{ role?: string }} [opts]
|
package/types/index.d.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* @property {number} [recoveryTimeout] Max wait for peer data + writer capability during recovery.
|
|
21
21
|
* @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.
|
|
22
22
|
* @property {boolean} [extensions] `false` disables the bundled extensions (profileSync, handleSync) for this instance. Build with `{ extensions: false }` too so the spec matches.
|
|
23
|
-
* @property {boolean | { autoStart?: boolean, backend?: any, maxOutbound?: number, maxInbound?: number }} [bluetooth] `true` enables nearby (Bluetooth) sync via `me.bluetooth` (auto-started). `{ autoStart: false }` creates the facade without starting the radio — the app calls `me.bluetooth.start()`/`stop()` (user toggle). `backend` injects a bare-bluetooth-shaped backend (tests). `maxOutbound`/`maxInbound` cap concurrent outbound links and inbound sessions. Absent backend on an unsupported host → `me.bluetooth.state === 'unsupported'`.
|
|
23
|
+
* @property {boolean | { autoStart?: boolean, backend?: any, maxOutbound?: number, maxInbound?: number, pipe?: 'l2cap' | 'gatt' }} [bluetooth] `true` enables nearby (Bluetooth) sync via `me.bluetooth` (auto-started). `{ autoStart: false }` creates the facade without starting the radio — the app calls `me.bluetooth.start()`/`stop()` (user toggle). `backend` injects a bare-bluetooth-shaped backend (tests). `maxOutbound`/`maxInbound` cap concurrent outbound links and inbound sessions. `pipe` picks the data pipe — `'l2cap'` (default, faster) or `'gatt'`; both peers must match. Absent backend on an unsupported host → `me.bluetooth.state === 'unsupported'`.
|
|
24
24
|
*/
|
|
25
25
|
/**
|
|
26
26
|
* Open (or create) a cero handle at `dir`. Sets up storage, network and
|
|
@@ -140,13 +140,14 @@ export type CeroOpts = {
|
|
|
140
140
|
*/
|
|
141
141
|
extensions?: boolean;
|
|
142
142
|
/**
|
|
143
|
-
* `true` enables nearby (Bluetooth) sync via `me.bluetooth` (auto-started). `{ autoStart: false }` creates the facade without starting the radio — the app calls `me.bluetooth.start()`/`stop()` (user toggle). `backend` injects a bare-bluetooth-shaped backend (tests). `maxOutbound`/`maxInbound` cap concurrent outbound links and inbound sessions. Absent backend on an unsupported host → `me.bluetooth.state === 'unsupported'`.
|
|
143
|
+
* `true` enables nearby (Bluetooth) sync via `me.bluetooth` (auto-started). `{ autoStart: false }` creates the facade without starting the radio — the app calls `me.bluetooth.start()`/`stop()` (user toggle). `backend` injects a bare-bluetooth-shaped backend (tests). `maxOutbound`/`maxInbound` cap concurrent outbound links and inbound sessions. `pipe` picks the data pipe — `'l2cap'` (default, faster) or `'gatt'`; both peers must match. Absent backend on an unsupported host → `me.bluetooth.state === 'unsupported'`.
|
|
144
144
|
*/
|
|
145
145
|
bluetooth?: boolean | {
|
|
146
146
|
autoStart?: boolean;
|
|
147
147
|
backend?: any;
|
|
148
148
|
maxOutbound?: number;
|
|
149
149
|
maxInbound?: number;
|
|
150
|
+
pipe?: "l2cap" | "gatt";
|
|
150
151
|
};
|
|
151
152
|
};
|
|
152
153
|
import { t } from './lib/spec.js';
|