@cero-base/cero 1.17.0 → 1.18.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 +7 -7
- package/src/build/builtins.js +33 -20
- package/src/handle/index.js +44 -74
- package/src/index.js +28 -12
- package/src/lib/operators.js +24 -13
- package/src/local/index.js +2 -1
- package/src/rpc/server.js +1 -1
- package/types/build/builtins.d.ts +40 -40
- package/types/handle/index.d.ts +2 -2
- package/types/lib/operators.d.ts +130 -19
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cero-base/cero",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.18.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,
|
|
@@ -95,12 +95,12 @@
|
|
|
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.18.1",
|
|
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.1",
|
|
103
|
-
"bare-path": "^3.1.
|
|
103
|
+
"bare-path": "^3.1.2",
|
|
104
104
|
"ble-swarm": "^2.3.0",
|
|
105
105
|
"compact-encoding": "^3.3.2",
|
|
106
106
|
"corestore": "^7.12.2",
|
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
"hypercore-storage": "^3.2.1",
|
|
111
111
|
"hyperdb": "^6.9.0",
|
|
112
112
|
"hyperdispatch": "^1.6.0",
|
|
113
|
-
"hyperschema": "^1.
|
|
113
|
+
"hyperschema": "^1.23.0",
|
|
114
114
|
"ready-resource": "^1.2.0",
|
|
115
115
|
"safety-catch": "^1.0.3",
|
|
116
116
|
"streamx": "^2.28.1",
|
|
@@ -120,10 +120,10 @@
|
|
|
120
120
|
"devDependencies": {
|
|
121
121
|
"@hyperswarm/testnet": "^3.1.4",
|
|
122
122
|
"bare-events": "^2.9.2",
|
|
123
|
-
"bare-fetch": "^3.2.
|
|
123
|
+
"bare-fetch": "^3.2.1",
|
|
124
124
|
"bare-process": "^4.5.1",
|
|
125
|
-
"bare-url": "^2.5.
|
|
126
|
-
"brittle": "^4.1.
|
|
125
|
+
"bare-url": "^2.5.4",
|
|
126
|
+
"brittle": "^4.1.1",
|
|
127
127
|
"typescript": "^5.9.3"
|
|
128
128
|
},
|
|
129
129
|
"license": "Apache-2.0"
|
package/src/build/builtins.js
CHANGED
|
@@ -24,7 +24,9 @@ export const refs = {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
export
|
|
27
|
+
export function getHyperdbType(prim) {
|
|
28
|
+
return DB_TYPE[prim] || 'string'
|
|
29
|
+
}
|
|
28
30
|
|
|
29
31
|
const at = (ns, n) => `@${ns}/${n}`
|
|
30
32
|
const keyOf = (def) => (def.kind === 'single' ? [] : ['id'])
|
|
@@ -39,9 +41,11 @@ const fields = (map) =>
|
|
|
39
41
|
// Merge app `t.extend` fields into a builtin's base fields — base can't be redeclared.
|
|
40
42
|
const merge = (type, base, extra) => {
|
|
41
43
|
if (!extra) return base
|
|
42
|
-
for (const k in extra)
|
|
43
|
-
if (k in base)
|
|
44
|
+
for (const k in extra) {
|
|
45
|
+
if (k in base) {
|
|
44
46
|
throw CeroError.INVALID(`'${k}' is a base field of '${type}' and cannot be redeclared`)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
45
49
|
return { ...base, ...extra }
|
|
46
50
|
}
|
|
47
51
|
|
|
@@ -53,8 +57,8 @@ const descriptors = (group, extend = {}) =>
|
|
|
53
57
|
}))
|
|
54
58
|
|
|
55
59
|
// meta.refs entries for a scope's builtins.
|
|
56
|
-
export
|
|
57
|
-
Object.fromEntries(
|
|
60
|
+
export function builtinRefs(ns, scope) {
|
|
61
|
+
return Object.fromEntries(
|
|
58
62
|
Object.entries(refs[scope]).map(([name, def]) => [
|
|
59
63
|
name,
|
|
60
64
|
{
|
|
@@ -66,14 +70,19 @@ export const builtinRefs = (ns, scope) =>
|
|
|
66
70
|
}
|
|
67
71
|
])
|
|
68
72
|
)
|
|
73
|
+
}
|
|
69
74
|
|
|
70
75
|
// hyperschema type descriptors. scope is 'main' | 'local' | 'rpc'; `extend`
|
|
71
76
|
// merges app `t.extend` fields into the matching type.
|
|
72
|
-
export
|
|
73
|
-
|
|
77
|
+
export function builtinTypes(scope, extend) {
|
|
78
|
+
return descriptors(schemas[scope], extend)
|
|
79
|
+
}
|
|
80
|
+
export function rpcTypes() {
|
|
81
|
+
return descriptors(schemas.rpc)
|
|
82
|
+
}
|
|
74
83
|
|
|
75
84
|
// hyperdb collection descriptors for a scope.
|
|
76
|
-
export
|
|
85
|
+
export function builtinCollections(ns, scope) {
|
|
77
86
|
const out = Object.entries(refs[scope]).map(([name, def]) => ({
|
|
78
87
|
name,
|
|
79
88
|
schema: at(ns, def.type),
|
|
@@ -87,25 +96,29 @@ export const builtinCollections = (ns, scope) => {
|
|
|
87
96
|
}
|
|
88
97
|
|
|
89
98
|
// hyperdispatch descriptors (main scope only).
|
|
90
|
-
export
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
{
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
]
|
|
99
|
+
export function builtinDispatches(ns) {
|
|
100
|
+
return [
|
|
101
|
+
{ name: 'add-writer', requestType: at(ns, 'writer') },
|
|
102
|
+
{ name: 'del-writer', requestType: at(ns, 'writer') },
|
|
103
|
+
{ name: 'claim-writer', requestType: at(ns, 'claim') },
|
|
104
|
+
...Object.values(refs.main).flatMap(({ type }) => [
|
|
105
|
+
{ name: `add-${type}`, requestType: at(ns, type) },
|
|
106
|
+
{ name: `set-${type}`, requestType: at(ns, type) },
|
|
107
|
+
{ name: `del-${type}`, requestType: at(ns, 'del-by-id') }
|
|
108
|
+
])
|
|
109
|
+
]
|
|
110
|
+
}
|
|
100
111
|
|
|
101
112
|
// Registered AFTER the app's own dispatches (see build/index.js): hyperdispatch
|
|
102
113
|
// numbers routes positionally and persists them, so a spec built before
|
|
103
114
|
// rotation must see rotate-key appended at the end — inserting it into the
|
|
104
115
|
// builtin group would collide with the app's persisted route ids on an
|
|
105
116
|
// incremental rebuild.
|
|
106
|
-
export
|
|
117
|
+
export function rotateDispatch(ns) {
|
|
118
|
+
return { name: 'rotate-key', requestType: at(ns, 'epoch') }
|
|
119
|
+
}
|
|
107
120
|
|
|
108
|
-
export
|
|
121
|
+
export function rpcCommands(ns) {
|
|
109
122
|
const ref = (n) => at(ns, n)
|
|
110
123
|
return [
|
|
111
124
|
{ name: 'init', request: { name: ref('req-empty') }, response: { name: ref('res-identity') } },
|
package/src/handle/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import { Identity } from '@cero-base/core/identity'
|
|
|
12
12
|
import { Database } from '@cero-base/core/database'
|
|
13
13
|
import { epochEntries, blobEpochKey } from '@cero-base/core/database/encryption'
|
|
14
14
|
import { Pairing } from '@cero-base/core/pairing'
|
|
15
|
-
import { toId, grants, isRank, addWriterPayload } from '@cero-base/core/utils'
|
|
15
|
+
import { toId, grants, can, isRank, addWriterPayload, REMOVE } from '@cero-base/core/utils'
|
|
16
16
|
import { CeroError } from '@cero-base/core/errors'
|
|
17
17
|
import { Blobs } from '@cero-base/core/blobs'
|
|
18
18
|
import { decodeId } from '@cero-base/core/blobs/codec'
|
|
@@ -128,7 +128,7 @@ export class Handle extends ReadyResource {
|
|
|
128
128
|
this._fileServer = null
|
|
129
129
|
this._blobs = null
|
|
130
130
|
this._epochBlobs = null
|
|
131
|
-
// root only
|
|
131
|
+
// root only, serializes suspend/resume and converges rapid bounces
|
|
132
132
|
this._sus = parent
|
|
133
133
|
? null
|
|
134
134
|
: new Suspendify({ suspend: () => this._suspend(), resume: () => this._resume() })
|
|
@@ -165,9 +165,8 @@ export class Handle extends ReadyResource {
|
|
|
165
165
|
}
|
|
166
166
|
})
|
|
167
167
|
await this.pair.ready()
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
// or consumed elsewhere).
|
|
168
|
+
// serve invites persisted by any member, and keep the set in step as rows
|
|
169
|
+
// replicate in or disappear (minted, revoked or consumed elsewhere)
|
|
171
170
|
await this._syncInvites().catch(safetyCatch)
|
|
172
171
|
this._invitesSync = () => this._syncInvites().catch(safetyCatch)
|
|
173
172
|
this.store.on('update', this._invitesSync)
|
|
@@ -353,8 +352,7 @@ export class Handle extends ReadyResource {
|
|
|
353
352
|
blobs
|
|
354
353
|
.ready()
|
|
355
354
|
.then(() => {
|
|
356
|
-
// close prunes _coreKeys first
|
|
357
|
-
// not re-insert the entry (it would outlive the handle on the root)
|
|
355
|
+
// close prunes _coreKeys first, a late ready() must not re-insert the entry
|
|
358
356
|
if (this.closing || this.closed || !blobs.key) return
|
|
359
357
|
this.root._coreKeys.set(b4a.toString(blobs.key, 'hex'), encryptionKey)
|
|
360
358
|
})
|
|
@@ -377,8 +375,7 @@ export class Handle extends ReadyResource {
|
|
|
377
375
|
const { coreKey } = decodeId(id)
|
|
378
376
|
const hex = b4a.toString(coreKey, 'hex')
|
|
379
377
|
if (!this.root._coreKeys.has(hex)) {
|
|
380
|
-
// a file-field value
|
|
381
|
-
// (fire-and-forget: idempotent, resolution happens again per request)
|
|
378
|
+
// no stamp on a file-field value, look it up (fire-and-forget, idempotent)
|
|
382
379
|
if (stamp === undefined) {
|
|
383
380
|
this.store
|
|
384
381
|
.get('files', id)
|
|
@@ -392,16 +389,14 @@ export class Handle extends ReadyResource {
|
|
|
392
389
|
if (!key) return // unknown epoch — this device is not entitled to the core
|
|
393
390
|
this.root._coreKeys.set(hex, key)
|
|
394
391
|
}
|
|
395
|
-
// remember which handle read it, so close prunes the entry
|
|
396
|
-
// on the next read if another handle still serves the same core)
|
|
392
|
+
// remember which handle read it, so close prunes the entry
|
|
397
393
|
if (this !== this.root) (this._blobKeys ??= new Set()).add(hex)
|
|
398
394
|
} catch {
|
|
399
395
|
// ignore invalid ids
|
|
400
396
|
}
|
|
401
397
|
}
|
|
402
398
|
|
|
403
|
-
//
|
|
404
|
-
// have their own keys); rotated-era cores derive from the epoch entropy.
|
|
399
|
+
// base-era cores use the OWNING handle's key, rooms have their own
|
|
405
400
|
_blobCoreKey(stamp) {
|
|
406
401
|
if (!stamp) return this.store.encryptionKey
|
|
407
402
|
const entropy = this.store.keyring.entropy(stamp)
|
|
@@ -479,20 +474,17 @@ export class Handle extends ReadyResource {
|
|
|
479
474
|
* @returns {Promise<string>} Z32-encoded invite string.
|
|
480
475
|
*/
|
|
481
476
|
async invite(opts) {
|
|
482
|
-
if (!this.pair)
|
|
477
|
+
if (!this.pair) {
|
|
483
478
|
throw CeroError.INVALID(
|
|
484
479
|
'invite() is not available on the root handle — open a child handle first'
|
|
485
480
|
)
|
|
486
|
-
|
|
487
|
-
//
|
|
488
|
-
// at apply — as a silent drop that still leaves the joiner admitted.
|
|
481
|
+
}
|
|
482
|
+
// an invite is a capability, cap the rank or apply drops the mismatch silently
|
|
489
483
|
if (opts?.role !== undefined && opts.role !== '') await this._checkGrant(opts.role)
|
|
490
484
|
const str = await this.pair.createInvite(opts)
|
|
491
485
|
const record = this.pair.recordOf(str)
|
|
492
486
|
if (record) {
|
|
493
|
-
//
|
|
494
|
-
// the in-memory record keeps working this session even if the write
|
|
495
|
-
// fails.
|
|
487
|
+
// persist so every member replica serves it across restarts
|
|
496
488
|
await this.store
|
|
497
489
|
.call('add-invite', {
|
|
498
490
|
id: b4a.toString(record.id, 'hex'),
|
|
@@ -519,15 +511,20 @@ export class Handle extends ReadyResource {
|
|
|
519
511
|
* Revoke a previously-minted invite by its string form.
|
|
520
512
|
*
|
|
521
513
|
* @param {string} invite
|
|
522
|
-
* @returns {boolean} `true` if the invite was found and removed.
|
|
514
|
+
* @returns {Promise<boolean>} `true` if the invite was found and removed.
|
|
523
515
|
*/
|
|
524
|
-
revoke(invite) {
|
|
516
|
+
async revoke(invite) {
|
|
525
517
|
if (!this.pair) throw CeroError.INVALID('revoke() is not available on the root handle')
|
|
518
|
+
// apply refuses revoke below REMOVE, and other members would keep serving it
|
|
519
|
+
const { data: me } = await this.store.get('members', this.identity.id)
|
|
520
|
+
if (me && !can(me.role, REMOVE)) {
|
|
521
|
+
throw CeroError.DENIED(null, 'revoking an invite needs the remove permission')
|
|
522
|
+
}
|
|
526
523
|
const record = this.pair.recordOf(invite)
|
|
527
524
|
const revoked = this.pair.revoke(invite)
|
|
528
525
|
if (revoked && record) {
|
|
529
526
|
// drop the persisted row so every other member stops serving it too
|
|
530
|
-
this.store.call('del-invite', { id: b4a.toString(record.id, 'hex') })
|
|
527
|
+
await this.store.call('del-invite', { id: b4a.toString(record.id, 'hex') })
|
|
531
528
|
}
|
|
532
529
|
return revoked
|
|
533
530
|
}
|
|
@@ -552,20 +549,14 @@ export class Handle extends ReadyResource {
|
|
|
552
549
|
if (candidate.invite.role && !grants(candidate.invite.role, role)) {
|
|
553
550
|
throw CeroError.INVALID(`role '${role}' exceeds the invite role '${candidate.invite.role}'`)
|
|
554
551
|
}
|
|
555
|
-
//
|
|
556
|
-
// inside the pairing request's lifetime, and any read here drops it. Rank
|
|
557
|
-
// validation is synchronous and catches the dangerous case (an app role
|
|
558
|
-
// name silently grants nothing); the cap against our own rank is enforced
|
|
559
|
-
// at apply, where add-writer refuses to admit what it cannot grant.
|
|
552
|
+
// synchronous on purpose, the cap against our own rank is enforced at apply
|
|
560
553
|
if (!isRank(role)) {
|
|
561
554
|
throw CeroError.INVALID(`role '${role}' is not a rank (owner, admin, member, reader)`)
|
|
562
555
|
}
|
|
563
556
|
|
|
564
|
-
// confirm must answer within the
|
|
565
|
-
//
|
|
566
|
-
//
|
|
567
|
-
// land; if they fail the joiner holds the key un-admitted, which is
|
|
568
|
-
// recoverable (re-pair) and surfaced via onerror in _wireAccept.
|
|
557
|
+
// confirm must answer within the pairing request's lifetime, any await before
|
|
558
|
+
// it (even ~100ms) drops the response. So the key is revealed before the
|
|
559
|
+
// membership writes land, and a failure there is recoverable by re-pairing.
|
|
569
560
|
// Epoch secrets ride along so a post-rotation joiner reads full history.
|
|
570
561
|
const epochs = this.store.keyring.all()
|
|
571
562
|
await candidate.confirm({
|
|
@@ -598,8 +589,7 @@ export class Handle extends ReadyResource {
|
|
|
598
589
|
sig,
|
|
599
590
|
master: this.identity.publicKey,
|
|
600
591
|
writer: writerKey,
|
|
601
|
-
// the
|
|
602
|
-
// decides the rank, and a refusal discards both
|
|
592
|
+
// add-member in the same transaction decides the rank, a refusal discards both
|
|
603
593
|
memberId: member.id,
|
|
604
594
|
ts: member.updatedAt || Date.now()
|
|
605
595
|
})
|
|
@@ -607,15 +597,13 @@ export class Handle extends ReadyResource {
|
|
|
607
597
|
})
|
|
608
598
|
}
|
|
609
599
|
|
|
610
|
-
//
|
|
611
|
-
// never above our own. `grants` treats an unknown role as "no" everywhere,
|
|
612
|
-
// so an app role name must fail loudly here rather than silently downstream.
|
|
600
|
+
// `grants` treats an unknown role as "no", so an app role name must fail loudly
|
|
613
601
|
async _checkGrant(role) {
|
|
614
602
|
if (!isRank(role)) {
|
|
615
603
|
throw CeroError.INVALID(`role '${role}' is not a rank (owner, admin, member, reader)`)
|
|
616
604
|
}
|
|
617
605
|
const { data: me } = await this.store.get('members', this.identity.id)
|
|
618
|
-
// no member row yet = genesis
|
|
606
|
+
// no member row yet = genesis, nothing to cap
|
|
619
607
|
if (!me) return
|
|
620
608
|
if (!grants(me.role, role)) {
|
|
621
609
|
throw CeroError.DENIED(null, `role '${role}' exceeds your own role '${me.role}'`)
|
|
@@ -652,13 +640,11 @@ export class Handle extends ReadyResource {
|
|
|
652
640
|
namespace: `${NS}/handle/${type}/${writer.id}`,
|
|
653
641
|
routes,
|
|
654
642
|
keyPair: writer,
|
|
655
|
-
//
|
|
656
|
-
// any member decrypt every room this identity ever created, plus its root db
|
|
643
|
+
// inheriting identity.encryptionKey would let any member decrypt every room
|
|
657
644
|
encryptionKey: Identity.randomBytes(32)
|
|
658
645
|
})
|
|
659
646
|
)
|
|
660
|
-
// a failure after the child opens
|
|
661
|
-
// swarm session and attached bee, never reaped by the parent (not yet a child)
|
|
647
|
+
// a failure after the child opens leaks its Database, swarm session and bee
|
|
662
648
|
let publish = null
|
|
663
649
|
let abort = null
|
|
664
650
|
let inflightId = null
|
|
@@ -667,10 +653,8 @@ export class Handle extends ReadyResource {
|
|
|
667
653
|
child.name = name
|
|
668
654
|
|
|
669
655
|
const id = toId(child.store.key)
|
|
670
|
-
//
|
|
671
|
-
//
|
|
672
|
-
// must share this child — a duplicate Handle over the same core
|
|
673
|
-
// deadlocks in ready().
|
|
656
|
+
// add-handle makes the row visible before this create finishes, and a
|
|
657
|
+
// duplicate Handle over the same core deadlocks in ready()
|
|
674
658
|
const inflight = new Promise((resolve, reject) => {
|
|
675
659
|
publish = resolve
|
|
676
660
|
abort = reject
|
|
@@ -682,8 +666,7 @@ export class Handle extends ReadyResource {
|
|
|
682
666
|
|
|
683
667
|
const ts = Date.now()
|
|
684
668
|
const writerKey = child.store.writerKey
|
|
685
|
-
//
|
|
686
|
-
// accept(), so a room's first two ops land together like every later pair
|
|
669
|
+
// same shape as accept(), so a room's first two ops land together
|
|
687
670
|
await child.store.tx(async (tx) => {
|
|
688
671
|
await tx.call('add-writer', {
|
|
689
672
|
master: this.identity.publicKey,
|
|
@@ -738,10 +721,7 @@ export class Handle extends ReadyResource {
|
|
|
738
721
|
* @returns {Promise<Handle>}
|
|
739
722
|
*/
|
|
740
723
|
async _join(invite, type, opts = {}) {
|
|
741
|
-
//
|
|
742
|
-
// join, a double tap): the pairing layer allows one candidate per invite,
|
|
743
|
-
// so the second attempt must attach to the in-flight join, not start
|
|
744
|
-
// another handshake.
|
|
724
|
+
// the pairing layer allows one candidate per invite, attach to the in-flight join
|
|
745
725
|
const target = Pairing.inviteTopic(invite)
|
|
746
726
|
const key = target && `${type}/${b4a.toString(target, 'hex')}`
|
|
747
727
|
if (!key) return this._pair(invite, type, opts, target)
|
|
@@ -766,14 +746,10 @@ export class Handle extends ReadyResource {
|
|
|
766
746
|
async _pair(invite, type, { routes, timeout } = {}, target = null) {
|
|
767
747
|
const deadline = timeout || TIMEOUT
|
|
768
748
|
|
|
769
|
-
//
|
|
770
|
-
//
|
|
771
|
-
//
|
|
772
|
-
//
|
|
773
|
-
// its keypair would only get it re-removed. Holding a fresh invite is
|
|
774
|
-
// exactly the re-admission path, so fall through to a real pairing —
|
|
775
|
-
// which mints a fresh keypair the host admits. Our member row can outlive
|
|
776
|
-
// our writer (one revoked device), so the device row is the real test.
|
|
749
|
+
// idempotent while our stored writer is still admitted, so no waiting on a
|
|
750
|
+
// peer to confirm. A removed writer's core is frozen and reusing its keypair
|
|
751
|
+
// only gets it re-removed, so fall through to a real pairing, which mints a
|
|
752
|
+
// fresh keypair. A member row outlives a revoked device, so test the device.
|
|
777
753
|
if (target) {
|
|
778
754
|
const { data: joined } = await this.store.get('handles')
|
|
779
755
|
const existing = joined.find(
|
|
@@ -788,8 +764,7 @@ export class Handle extends ReadyResource {
|
|
|
788
764
|
}
|
|
789
765
|
}
|
|
790
766
|
|
|
791
|
-
// offline join:
|
|
792
|
-
// BLE UUID for the duration of the join — pairing rides the injected link
|
|
767
|
+
// offline join: rendezvous on the invite-derived BLE UUID for the join
|
|
793
768
|
const stopNearby = this.root.bluetooth ? this.root.bluetooth.announce(invite) : null
|
|
794
769
|
|
|
795
770
|
const child = /** @type {Child} */ (
|
|
@@ -801,8 +776,7 @@ export class Handle extends ReadyResource {
|
|
|
801
776
|
timeout
|
|
802
777
|
}).finally(() => stopNearby?.())
|
|
803
778
|
)
|
|
804
|
-
// whenWritable timing out (host offline) is
|
|
805
|
-
// fully-opened child rather than leak its Database/pairing/swarm session
|
|
779
|
+
// whenWritable timing out (host offline) is normal, don't leak the opened child
|
|
806
780
|
let publish = null
|
|
807
781
|
let abort = null
|
|
808
782
|
let inflightId = null
|
|
@@ -811,8 +785,7 @@ export class Handle extends ReadyResource {
|
|
|
811
785
|
if (!child.store.writable) await child.store.whenWritable({ timeout: deadline })
|
|
812
786
|
|
|
813
787
|
const id = toId(child.store.key)
|
|
814
|
-
// same create/open race as _create
|
|
815
|
-
// this join finishes — a concurrent _load must share this child
|
|
788
|
+
// same create/open race as _create, a concurrent _load must share this child
|
|
816
789
|
const inflight = new Promise((resolve, reject) => {
|
|
817
790
|
publish = resolve
|
|
818
791
|
abort = reject
|
|
@@ -880,8 +853,9 @@ export class Handle extends ReadyResource {
|
|
|
880
853
|
async _reopen(type, id, opts) {
|
|
881
854
|
const { data } = await this.store.get('handles', id)
|
|
882
855
|
if (!data) throw CeroError.UNKNOWN('handle', id)
|
|
883
|
-
if (data.type !== type)
|
|
856
|
+
if (data.type !== type) {
|
|
884
857
|
throw CeroError.INVALID(`handle ${id} is type ${data.type}, not ${type}`)
|
|
858
|
+
}
|
|
885
859
|
let writer = await this._loadKeyPair(id)
|
|
886
860
|
const firstTime = !writer
|
|
887
861
|
if (firstTime) {
|
|
@@ -902,9 +876,7 @@ export class Handle extends ReadyResource {
|
|
|
902
876
|
if (firstTime) {
|
|
903
877
|
await this._saveKeyPair(id, writer)
|
|
904
878
|
}
|
|
905
|
-
//
|
|
906
|
-
// host-approval gate, and one that comes back on the next open is worse
|
|
907
|
-
// than none at all
|
|
879
|
+
// `accept: false` is a host-approval gate, re-arming it silently is worse
|
|
908
880
|
if (opts?.accept !== false) this._wireAccept(child, { role: opts?.role })
|
|
909
881
|
bind(child, type)
|
|
910
882
|
this.children.add(child)
|
|
@@ -989,9 +961,7 @@ export class Handle extends ReadyResource {
|
|
|
989
961
|
|
|
990
962
|
let epochs = null
|
|
991
963
|
if (additional?.byteLength) {
|
|
992
|
-
// the epoch set is load-bearing
|
|
993
|
-
// delivery must fail the join loudly, not produce a silently inert
|
|
994
|
-
// member (hosts predating rotation send no additional payload at all)
|
|
964
|
+
// the epoch set is load-bearing, a malformed delivery must fail the join
|
|
995
965
|
try {
|
|
996
966
|
epochs = c.decode(epochEntries, additional)
|
|
997
967
|
} catch {
|
package/src/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import Hypercore from 'hypercore'
|
|
1
2
|
import HypercoreStorage from 'hypercore-storage'
|
|
2
3
|
import Corestore from 'corestore'
|
|
3
4
|
import safetyCatch from 'safety-catch'
|
|
@@ -121,9 +122,9 @@ export async function cero(dir, spec, opts = {}) {
|
|
|
121
122
|
if (local) {
|
|
122
123
|
const stored = (await local.store.get('environment')).data?.channel ?? null
|
|
123
124
|
const wanted = opts.channel ?? null
|
|
124
|
-
if (stored == null && wanted != null)
|
|
125
|
+
if (stored == null && wanted != null) {
|
|
125
126
|
await local.store.set('environment', { channel: wanted })
|
|
126
|
-
else if (stored != null && stored !== wanted) throw CeroError.CHANNEL_MISMATCH()
|
|
127
|
+
} else if (stored != null && stored !== wanted) throw CeroError.CHANNEL_MISMATCH()
|
|
127
128
|
}
|
|
128
129
|
|
|
129
130
|
network = new Network({
|
|
@@ -159,20 +160,35 @@ export async function cero(dir, spec, opts = {}) {
|
|
|
159
160
|
|
|
160
161
|
// a supplied or stored identity may already have history elsewhere —
|
|
161
162
|
// authoring genesis twice forks the seed-derived writer core (a writable
|
|
162
|
-
// core has one author, ever).
|
|
163
|
-
//
|
|
164
|
-
// undetectable — this catches the reachable-peer case
|
|
163
|
+
// core has one author, ever). Ask the peers directly with a plain session
|
|
164
|
+
// on the genesis core: the base never replicates an empty local core.
|
|
165
|
+
// Offline reuse is undetectable — this catches the reachable-peer case.
|
|
165
166
|
if (!writer && !opts.key && !opts.recovery && !fresh) {
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
167
|
+
const genesis = store.get({
|
|
168
|
+
key: Hypercore.key({
|
|
169
|
+
version: store.manifestVersion,
|
|
170
|
+
signers: [{ publicKey: identity.publicKey }]
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
await genesis.ready()
|
|
174
|
+
network.attach(genesis)
|
|
175
|
+
try {
|
|
176
|
+
const until = Date.now() + 1500
|
|
177
|
+
while (Date.now() < until) {
|
|
178
|
+
if (genesis.length > 0 || genesis.peers.some((p) => p.remoteLength > 0)) {
|
|
179
|
+
throw CeroError.CONFLICT('identity already has history — open with { recovery: true }')
|
|
180
|
+
}
|
|
181
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
171
182
|
}
|
|
172
|
-
|
|
183
|
+
} finally {
|
|
184
|
+
network.detach(genesis)
|
|
185
|
+
await genesis.close()
|
|
173
186
|
}
|
|
174
187
|
}
|
|
175
|
-
|
|
188
|
+
// decided by the stored writer and the caller's intent, never by the local
|
|
189
|
+
// core's length: a same-identity device opens on the genesis core, which
|
|
190
|
+
// fills with the first device's blocks as soon as a peer connects
|
|
191
|
+
if (!writer && (!opts.key || opts.recovery)) {
|
|
176
192
|
const result = await me.bootstrap({
|
|
177
193
|
name: opts.name || null,
|
|
178
194
|
isMobile: opts.isMobile === true,
|
package/src/lib/operators.js
CHANGED
|
@@ -38,8 +38,9 @@ export function resolveFile(handle, id, name) {
|
|
|
38
38
|
* @param {Record<string, any>} row
|
|
39
39
|
* @returns {Promise<SingleResult>}
|
|
40
40
|
*/
|
|
41
|
-
export
|
|
42
|
-
ref.name === 'files' ? putFile(ref, row) : ref.handle.store.put(ref.name, row)
|
|
41
|
+
export function put(ref, row) {
|
|
42
|
+
return ref.name === 'files' ? putFile(ref, row) : ref.handle.store.put(ref.name, row)
|
|
43
|
+
}
|
|
43
44
|
|
|
44
45
|
async function putFile(ref, row) {
|
|
45
46
|
const handle = ref.handle
|
|
@@ -63,7 +64,9 @@ async function putFile(ref, row) {
|
|
|
63
64
|
* @param {{ upsert?: boolean }} [opts]
|
|
64
65
|
* @returns {Promise<SingleResult>}
|
|
65
66
|
*/
|
|
66
|
-
export
|
|
67
|
+
export function set(ref, row, opts) {
|
|
68
|
+
return ref.handle.store.set(ref.name, row, opts)
|
|
69
|
+
}
|
|
67
70
|
|
|
68
71
|
/**
|
|
69
72
|
* Delete a row by id (collection refs), or wipe the row (single refs).
|
|
@@ -72,7 +75,9 @@ export const set = (ref, row, opts) => ref.handle.store.set(ref.name, row, opts)
|
|
|
72
75
|
* @param {string} [id]
|
|
73
76
|
* @returns {Promise<void>}
|
|
74
77
|
*/
|
|
75
|
-
export
|
|
78
|
+
export function del(ref, id) {
|
|
79
|
+
return ref.handle.store.del(ref.name, id)
|
|
80
|
+
}
|
|
76
81
|
|
|
77
82
|
/**
|
|
78
83
|
* Count rows on `ref`, optionally filtered.
|
|
@@ -81,7 +86,9 @@ export const del = (ref, id) => ref.handle.store.del(ref.name, id)
|
|
|
81
86
|
* @param {Record<string, any>} [q]
|
|
82
87
|
* @returns {Promise<{ data: number }>}
|
|
83
88
|
*/
|
|
84
|
-
export
|
|
89
|
+
export function count(ref, q) {
|
|
90
|
+
return ref.handle.store.count(ref.name, q)
|
|
91
|
+
}
|
|
85
92
|
|
|
86
93
|
/**
|
|
87
94
|
* Invoke an `action`-kind ref (a custom mutation declared in the schema).
|
|
@@ -90,7 +97,9 @@ export const count = (ref, q) => ref.handle.store.count(ref.name, q)
|
|
|
90
97
|
* @param {Record<string, any>} [d]
|
|
91
98
|
* @returns {Promise<any>}
|
|
92
99
|
*/
|
|
93
|
-
export
|
|
100
|
+
export function call(ref, d) {
|
|
101
|
+
return ref.handle.store.call(ref.name, d)
|
|
102
|
+
}
|
|
94
103
|
|
|
95
104
|
// Write ops per ref kind, for `before`/`after` subscriptions.
|
|
96
105
|
const WRITES = { single: ['set'], collection: ['put', 'set', 'del'] }
|
|
@@ -105,7 +114,7 @@ const WRITES = { single: ['set'], collection: ['put', 'set', 'del'] }
|
|
|
105
114
|
* @param {{ signal?: AbortSignal }} [opts]
|
|
106
115
|
* @returns {() => void}
|
|
107
116
|
*/
|
|
108
|
-
export
|
|
117
|
+
export function before(ref, fn, opts) {
|
|
109
118
|
const db = ref.handle.store
|
|
110
119
|
const ops = WRITES[ref.kind] || ['set']
|
|
111
120
|
const offs = ops.map((op) =>
|
|
@@ -130,7 +139,7 @@ export const before = (ref, fn, opts) => {
|
|
|
130
139
|
* @param {{ signal?: AbortSignal }} [opts]
|
|
131
140
|
* @returns {() => void}
|
|
132
141
|
*/
|
|
133
|
-
export
|
|
142
|
+
export function after(ref, fn, opts) {
|
|
134
143
|
const db = ref.handle.store
|
|
135
144
|
const ops = WRITES[ref.kind] || ['set']
|
|
136
145
|
const handler = (ctx) => ctx.name === ref.name && fn(ctx)
|
|
@@ -194,7 +203,7 @@ function resolveRow(ref, row) {
|
|
|
194
203
|
* @param {string | Record<string, any>} [q]
|
|
195
204
|
* @returns {Promise<SingleResult | ListResult | GetByIdResult>}
|
|
196
205
|
*/
|
|
197
|
-
export
|
|
206
|
+
export async function get(ref, q) {
|
|
198
207
|
if (ref.kind === 'handle') {
|
|
199
208
|
const { data } = await parentStore(ref).get('handles', q)
|
|
200
209
|
return normalize(data, ref.name)
|
|
@@ -225,7 +234,7 @@ const bindStream = (owner, stream, opts) => {
|
|
|
225
234
|
* @param {{ signal?: AbortSignal }} [opts]
|
|
226
235
|
* @returns {import('streamx').Readable}
|
|
227
236
|
*/
|
|
228
|
-
export
|
|
237
|
+
export function watch(ref, q, opts) {
|
|
229
238
|
const owner = ref.handle
|
|
230
239
|
if (ref.kind !== 'handle') {
|
|
231
240
|
const src = owner.store.watch(ref.name, q)
|
|
@@ -243,7 +252,7 @@ export const watch = (ref, q, opts) => {
|
|
|
243
252
|
* batch, and any batch after a view swap, replays current state as inserts
|
|
244
253
|
* with `reset: true`). File-typed fields resolve on both sides.
|
|
245
254
|
*/
|
|
246
|
-
export
|
|
255
|
+
export function changes(ref, q, opts) {
|
|
247
256
|
const owner = ref.handle
|
|
248
257
|
if (ref.kind === 'handle') throw CeroError.INVALID('changes does not support handle refs')
|
|
249
258
|
const src = owner.store.changes(ref.name, q)
|
|
@@ -338,7 +347,7 @@ function snapshotStream(src, map) {
|
|
|
338
347
|
* @param {string | { invite?: string, id?: string, name?: string, routes?: any, role?: string, accept?: boolean } | undefined} [arg]
|
|
339
348
|
* @returns {Promise<CeroHandle>} The resolved child handle.
|
|
340
349
|
*/
|
|
341
|
-
export
|
|
350
|
+
export function open(ref, arg) {
|
|
342
351
|
if (typeof arg === 'string') return ref.handle._join(arg, ref.name)
|
|
343
352
|
if (arg && typeof arg.invite === 'string') return ref.handle._join(arg.invite, ref.name)
|
|
344
353
|
if (arg && typeof arg.id === 'string') return ref.handle._load(ref.name, arg.id, arg)
|
|
@@ -357,7 +366,9 @@ export const open = (ref, arg) => {
|
|
|
357
366
|
* @param {any} handle
|
|
358
367
|
* @returns {Promise<{ epoch: number }>}
|
|
359
368
|
*/
|
|
360
|
-
export
|
|
369
|
+
export function rotate(handle) {
|
|
370
|
+
return handle.store.rotate()
|
|
371
|
+
}
|
|
361
372
|
|
|
362
373
|
// ─── custom operators ──────────────────────────────────────────────────────
|
|
363
374
|
// App business logic lives as custom operators: pure functions whose first arg
|
package/src/local/index.js
CHANGED
|
@@ -25,8 +25,9 @@ export class Local extends ReadyResource {
|
|
|
25
25
|
*/
|
|
26
26
|
constructor(dir, spec, { root, store, storageKey } = {}) {
|
|
27
27
|
super()
|
|
28
|
-
if (!root && !store && (typeof dir !== 'string' || !dir))
|
|
28
|
+
if (!root && !store && (typeof dir !== 'string' || !dir)) {
|
|
29
29
|
throw CeroError.REQUIRED('dir, root, or store')
|
|
30
|
+
}
|
|
30
31
|
if (!spec?.local?.database) throw CeroError.REQUIRED('spec.local.database')
|
|
31
32
|
if (!spec?.meta?.local) throw CeroError.REQUIRED('spec.meta.local')
|
|
32
33
|
|
package/src/rpc/server.js
CHANGED
|
@@ -1,43 +1,3 @@
|
|
|
1
|
-
export namespace refs {
|
|
2
|
-
namespace main {
|
|
3
|
-
namespace members {
|
|
4
|
-
let type: string;
|
|
5
|
-
}
|
|
6
|
-
namespace devices {
|
|
7
|
-
let type_1: string;
|
|
8
|
-
export { type_1 as type };
|
|
9
|
-
}
|
|
10
|
-
namespace invites {
|
|
11
|
-
let type_2: string;
|
|
12
|
-
export { type_2 as type };
|
|
13
|
-
}
|
|
14
|
-
namespace handles {
|
|
15
|
-
let type_3: string;
|
|
16
|
-
export { type_3 as type };
|
|
17
|
-
}
|
|
18
|
-
namespace files {
|
|
19
|
-
let type_4: string;
|
|
20
|
-
export { type_4 as type };
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
let local: {
|
|
24
|
-
master: {
|
|
25
|
-
type: string;
|
|
26
|
-
kind: string;
|
|
27
|
-
};
|
|
28
|
-
keypair: {
|
|
29
|
-
type: string;
|
|
30
|
-
kind: string;
|
|
31
|
-
};
|
|
32
|
-
'handle-keypairs': {
|
|
33
|
-
type: string;
|
|
34
|
-
};
|
|
35
|
-
environment: {
|
|
36
|
-
type: string;
|
|
37
|
-
kind: string;
|
|
38
|
-
};
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
1
|
export function getHyperdbType(prim: any): any;
|
|
42
2
|
export function builtinRefs(ns: any, scope: any): {
|
|
43
3
|
[k: string]: {
|
|
@@ -98,3 +58,43 @@ export function rpcCommands(ns: any): ({
|
|
|
98
58
|
stream: boolean;
|
|
99
59
|
};
|
|
100
60
|
})[];
|
|
61
|
+
export namespace refs {
|
|
62
|
+
namespace main {
|
|
63
|
+
namespace members {
|
|
64
|
+
let type: string;
|
|
65
|
+
}
|
|
66
|
+
namespace devices {
|
|
67
|
+
let type_1: string;
|
|
68
|
+
export { type_1 as type };
|
|
69
|
+
}
|
|
70
|
+
namespace invites {
|
|
71
|
+
let type_2: string;
|
|
72
|
+
export { type_2 as type };
|
|
73
|
+
}
|
|
74
|
+
namespace handles {
|
|
75
|
+
let type_3: string;
|
|
76
|
+
export { type_3 as type };
|
|
77
|
+
}
|
|
78
|
+
namespace files {
|
|
79
|
+
let type_4: string;
|
|
80
|
+
export { type_4 as type };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
let local: {
|
|
84
|
+
master: {
|
|
85
|
+
type: string;
|
|
86
|
+
kind: string;
|
|
87
|
+
};
|
|
88
|
+
keypair: {
|
|
89
|
+
type: string;
|
|
90
|
+
kind: string;
|
|
91
|
+
};
|
|
92
|
+
'handle-keypairs': {
|
|
93
|
+
type: string;
|
|
94
|
+
};
|
|
95
|
+
environment: {
|
|
96
|
+
type: string;
|
|
97
|
+
kind: string;
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
}
|
package/types/handle/index.d.ts
CHANGED
|
@@ -242,9 +242,9 @@ export class Handle extends ReadyResource {
|
|
|
242
242
|
* Revoke a previously-minted invite by its string form.
|
|
243
243
|
*
|
|
244
244
|
* @param {string} invite
|
|
245
|
-
* @returns {boolean} `true` if the invite was found and removed.
|
|
245
|
+
* @returns {Promise<boolean>} `true` if the invite was found and removed.
|
|
246
246
|
*/
|
|
247
|
-
revoke(invite: string): boolean
|
|
247
|
+
revoke(invite: string): Promise<boolean>;
|
|
248
248
|
/**
|
|
249
249
|
* Accept a paired candidate — adds them as a writer (or read-only member)
|
|
250
250
|
* and confirms the pairing so they receive this handle's keys.
|
package/types/lib/operators.d.ts
CHANGED
|
@@ -22,38 +22,64 @@ export function resolveFile(handle: object, id: string, name?: string): {
|
|
|
22
22
|
name?: string;
|
|
23
23
|
};
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
26
|
-
* `handle.
|
|
27
|
-
*
|
|
28
|
-
* - `null` → the registered root operators, or
|
|
29
|
-
* - a child-handle type → the registered operators for that type.
|
|
30
|
-
* The scope forms are how cero binds handles automatically; pass a map yourself
|
|
31
|
-
* for manual binding.
|
|
25
|
+
* Insert (or overwrite by id) a row on `ref`. The `files` builtin is special:
|
|
26
|
+
* `put(handle.files, { data, type, name? })` uploads the bytes to this handle's
|
|
27
|
+
* blob store, records `{ id, name }`, and resolves the file.
|
|
32
28
|
*
|
|
33
|
-
* @param {
|
|
34
|
-
* @param {Record<string, any>
|
|
35
|
-
* @returns {
|
|
29
|
+
* @param {Ref} ref
|
|
30
|
+
* @param {Record<string, any>} row
|
|
31
|
+
* @returns {Promise<SingleResult>}
|
|
36
32
|
*/
|
|
37
|
-
export function
|
|
33
|
+
export function put(ref: Ref, row: Record<string, any>): Promise<SingleResult>;
|
|
38
34
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
35
|
+
* Upsert a row on `ref` — merges with the existing row and preserves
|
|
36
|
+
* `createdAt`. Pass `{ upsert: false }` to update-only: a missing row is left
|
|
37
|
+
* untouched instead of created (atomic — never resurrects a deleted row).
|
|
42
38
|
*
|
|
43
|
-
* @param {
|
|
39
|
+
* @param {Ref} ref
|
|
40
|
+
* @param {Record<string, any>} row
|
|
41
|
+
* @param {{ upsert?: boolean }} [opts]
|
|
42
|
+
* @returns {Promise<SingleResult>}
|
|
44
43
|
*/
|
|
45
|
-
export function define(map: Record<string, any>): void;
|
|
46
|
-
/** Test seam: clear all registered operators. */
|
|
47
|
-
export function _clearDefined(): void;
|
|
48
|
-
export function put(ref: Ref, row: Record<string, any>): Promise<SingleResult>;
|
|
49
44
|
export function set(ref: Ref, row: Record<string, any>, opts?: {
|
|
50
45
|
upsert?: boolean;
|
|
51
46
|
}): Promise<SingleResult>;
|
|
47
|
+
/**
|
|
48
|
+
* Delete a row by id (collection refs), or wipe the row (single refs).
|
|
49
|
+
*
|
|
50
|
+
* @param {Ref} ref
|
|
51
|
+
* @param {string} [id]
|
|
52
|
+
* @returns {Promise<void>}
|
|
53
|
+
*/
|
|
52
54
|
export function del(ref: Ref, id?: string): Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* Count rows on `ref`, optionally filtered.
|
|
57
|
+
*
|
|
58
|
+
* @param {Ref} ref
|
|
59
|
+
* @param {Record<string, any>} [q]
|
|
60
|
+
* @returns {Promise<{ data: number }>}
|
|
61
|
+
*/
|
|
53
62
|
export function count(ref: Ref, q?: Record<string, any>): Promise<{
|
|
54
63
|
data: number;
|
|
55
64
|
}>;
|
|
65
|
+
/**
|
|
66
|
+
* Invoke an `action`-kind ref (a custom mutation declared in the schema).
|
|
67
|
+
*
|
|
68
|
+
* @param {Ref} ref
|
|
69
|
+
* @param {Record<string, any>} [d]
|
|
70
|
+
* @returns {Promise<any>}
|
|
71
|
+
*/
|
|
56
72
|
export function call(ref: Ref, d?: Record<string, any>): Promise<any>;
|
|
73
|
+
/**
|
|
74
|
+
* Intercept writes to `ref` before they commit — `fn(ctx)` runs in-path
|
|
75
|
+
* (awaited). Return `false` to cancel the write, or mutate `ctx.row`.
|
|
76
|
+
* Returns an unsubscribe fn; pass `{ signal }` to unsubscribe on abort.
|
|
77
|
+
*
|
|
78
|
+
* @param {Ref} ref
|
|
79
|
+
* @param {(ctx: { op: string, name: string, row: any }) => any} fn
|
|
80
|
+
* @param {{ signal?: AbortSignal }} [opts]
|
|
81
|
+
* @returns {() => void}
|
|
82
|
+
*/
|
|
57
83
|
export function before(ref: Ref, fn: (ctx: {
|
|
58
84
|
op: string;
|
|
59
85
|
name: string;
|
|
@@ -61,6 +87,16 @@ export function before(ref: Ref, fn: (ctx: {
|
|
|
61
87
|
}) => any, opts?: {
|
|
62
88
|
signal?: AbortSignal;
|
|
63
89
|
}): () => void;
|
|
90
|
+
/**
|
|
91
|
+
* Subscribe to writes on `ref` — fires after each committed write,
|
|
92
|
+
* non-blocking (observe only). Returns an unsubscribe fn; pass `{ signal }`
|
|
93
|
+
* to unsubscribe on abort.
|
|
94
|
+
*
|
|
95
|
+
* @param {Ref} ref
|
|
96
|
+
* @param {(ctx: { op: string, name: string, row: any }) => void} fn
|
|
97
|
+
* @param {{ signal?: AbortSignal }} [opts]
|
|
98
|
+
* @returns {() => void}
|
|
99
|
+
*/
|
|
64
100
|
export function after(ref: Ref, fn: (ctx: {
|
|
65
101
|
op: string;
|
|
66
102
|
name: string;
|
|
@@ -68,11 +104,50 @@ export function after(ref: Ref, fn: (ctx: {
|
|
|
68
104
|
}) => void, opts?: {
|
|
69
105
|
signal?: AbortSignal;
|
|
70
106
|
}): () => void;
|
|
107
|
+
/**
|
|
108
|
+
* Read from `ref`. For data refs, dispatches to the underlying store. For
|
|
109
|
+
* `handle`-kind refs, lists existing child handles of that type from the
|
|
110
|
+
* parent's `handles` collection.
|
|
111
|
+
*
|
|
112
|
+
* @param {Ref} ref
|
|
113
|
+
* @param {string | Record<string, any>} [q]
|
|
114
|
+
* @returns {Promise<SingleResult | ListResult | GetByIdResult>}
|
|
115
|
+
*/
|
|
71
116
|
export function get(ref: Ref, q?: string | Record<string, any>): Promise<SingleResult | ListResult | GetByIdResult>;
|
|
117
|
+
/**
|
|
118
|
+
* Live snapshot stream on `ref` — re-emits the latest `get()` result on
|
|
119
|
+
* every underlying mutation. Tied to `ref.handle`'s lifecycle: closing the
|
|
120
|
+
* handle destroys it. Pass `{ signal }` to bind it to a finer scope, or
|
|
121
|
+
* destroy the stream directly to stop watching sooner.
|
|
122
|
+
*
|
|
123
|
+
* @param {Ref} ref
|
|
124
|
+
* @param {Record<string, any>} [q]
|
|
125
|
+
* @param {{ signal?: AbortSignal }} [opts]
|
|
126
|
+
* @returns {import('streamx').Readable}
|
|
127
|
+
*/
|
|
72
128
|
export function watch(ref: Ref, q?: Record<string, any>, opts?: {
|
|
73
129
|
signal?: AbortSignal;
|
|
74
130
|
}): import("streamx").Readable;
|
|
131
|
+
/**
|
|
132
|
+
* Delta subscription: batches of `{ prev, next }` row pairs instead of
|
|
133
|
+
* full snapshots — lossless under backpressure, self-contained (the first
|
|
134
|
+
* batch, and any batch after a view swap, replays current state as inserts
|
|
135
|
+
* with `reset: true`). File-typed fields resolve on both sides.
|
|
136
|
+
*/
|
|
75
137
|
export function changes(ref: any, q: any, opts: any): any;
|
|
138
|
+
/**
|
|
139
|
+
* Open (or create / join / load) a child handle through a `handle`-kind
|
|
140
|
+
* ref. Dispatches on the normalize of `arg`:
|
|
141
|
+
*
|
|
142
|
+
* - `string` → join via an invite string
|
|
143
|
+
* - `{ invite: string }` → join via invite (object form)
|
|
144
|
+
* - `{ id: string }` → load an existing handle by id
|
|
145
|
+
* - `object | undefined` → create a new handle with the given opts
|
|
146
|
+
*
|
|
147
|
+
* @param {Ref} ref
|
|
148
|
+
* @param {string | { invite?: string, id?: string, name?: string, routes?: any, role?: string, accept?: boolean } | undefined} [arg]
|
|
149
|
+
* @returns {Promise<CeroHandle>} The resolved child handle.
|
|
150
|
+
*/
|
|
76
151
|
export function open(ref: Ref, arg?: string | {
|
|
77
152
|
invite?: string;
|
|
78
153
|
id?: string;
|
|
@@ -81,9 +156,45 @@ export function open(ref: Ref, arg?: string | {
|
|
|
81
156
|
role?: string;
|
|
82
157
|
accept?: boolean;
|
|
83
158
|
} | undefined): Promise<CeroHandle>;
|
|
159
|
+
/**
|
|
160
|
+
* Rotate a handle's encryption epoch. A fresh secret is sealed to every
|
|
161
|
+
* current member and announced through the log — members removed before the
|
|
162
|
+
* rotation cannot decrypt anything written after it. Requires the remove
|
|
163
|
+
* permission (admin or owner). Compose with removal:
|
|
164
|
+
*
|
|
165
|
+
* await cero.del(room.members, memberId)
|
|
166
|
+
* await cero.rotate(room)
|
|
167
|
+
*
|
|
168
|
+
* @param {any} handle
|
|
169
|
+
* @returns {Promise<{ epoch: number }>}
|
|
170
|
+
*/
|
|
84
171
|
export function rotate(handle: any): Promise<{
|
|
85
172
|
epoch: number;
|
|
86
173
|
}>;
|
|
174
|
+
/**
|
|
175
|
+
* Put custom operators on `handle`, currying it as their first argument so
|
|
176
|
+
* `handle.ns.fn(args)` calls `fn(handle, args)`. `arg` is either:
|
|
177
|
+
* - a `{ ns: module }` map → bind exactly those, or
|
|
178
|
+
* - `null` → the registered root operators, or
|
|
179
|
+
* - a child-handle type → the registered operators for that type.
|
|
180
|
+
* The scope forms are how cero binds handles automatically; pass a map yourself
|
|
181
|
+
* for manual binding.
|
|
182
|
+
*
|
|
183
|
+
* @param {any} handle
|
|
184
|
+
* @param {Record<string, any> | string | null} arg
|
|
185
|
+
* @returns {any} handle
|
|
186
|
+
*/
|
|
187
|
+
export function bind(handle: any, arg: Record<string, any> | string | null): any;
|
|
188
|
+
/**
|
|
189
|
+
* Register custom operators by scope. A bare key binds on the root handle; a key
|
|
190
|
+
* that names a child-handle type binds on every handle of that type. Call once
|
|
191
|
+
* at startup, before `cero()` / `connect()`, in both processes.
|
|
192
|
+
*
|
|
193
|
+
* @param {Record<string, any>} map
|
|
194
|
+
*/
|
|
195
|
+
export function define(map: Record<string, any>): void;
|
|
196
|
+
/** Test seam: clear all registered operators. */
|
|
197
|
+
export function _clearDefined(): void;
|
|
87
198
|
export type Ref = import("./utils.js").Ref;
|
|
88
199
|
export type CeroHandle = import("../handle/index.js").CeroHandle;
|
|
89
200
|
export type SingleResult = {
|