@cero-base/cero 1.7.0 → 1.8.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 +8 -8
- package/package.json +7 -7
- package/src/build/schemas.js +1 -1
- package/src/handle/index.js +76 -3
- package/src/lib/operators.js +1 -37
- package/src/rpc/client.js +2 -2
- package/src/rpc/server.js +2 -2
- package/types/build/schemas.d.ts +1 -1
- package/types/handle/index.d.ts +21 -1
package/README.md
CHANGED
|
@@ -521,14 +521,14 @@ The same `spec/index.js` is imported on both sides. `@cero-base/cero/build` emit
|
|
|
521
521
|
|
|
522
522
|
Everything the local API offers, plus lifecycle methods on returned handles:
|
|
523
523
|
|
|
524
|
-
| Operator / method
|
|
525
|
-
|
|
|
526
|
-
| `cero.put` / `set` / `get` / `del` / `count` / `watch` / `call`
|
|
527
|
-
| `cero.open(ref, …)` — create, join, load by id
|
|
528
|
-
| `room.invite({ role, expiresIn,
|
|
529
|
-
| `room.revoke(invite)` — invalidate an outstanding invite; `true` if it existed
|
|
530
|
-
| `room.close()` — release the handle on the server
|
|
531
|
-
| `room.leave()` — remove yourself from the room and from your handles list
|
|
524
|
+
| Operator / method | Works over RPC |
|
|
525
|
+
| -------------------------------------------------------------------------------------------------------------- | -------------- |
|
|
526
|
+
| `cero.put` / `set` / `get` / `del` / `count` / `watch` / `call` | ✓ |
|
|
527
|
+
| `cero.open(ref, …)` — create, join, load by id | ✓ |
|
|
528
|
+
| `room.invite({ role, expiresIn, reuse })` — mint an invite (`reuse: true` keeps it alive after the first join) | ✓ |
|
|
529
|
+
| `room.revoke(invite)` — invalidate an outstanding invite; `true` if it existed | ✓ |
|
|
530
|
+
| `room.close()` — release the handle on the server | ✓ |
|
|
531
|
+
| `room.leave()` — remove yourself from the room and from your handles list | ✓ |
|
|
532
532
|
|
|
533
533
|
`cero.watch(ref)` returns a Readable on both sides; snapshots flow as a server-streamed RPC.
|
|
534
534
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cero-base/cero",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.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,
|
|
@@ -89,27 +89,27 @@
|
|
|
89
89
|
"build:types": "rm -rf types && tsc -p .",
|
|
90
90
|
"pretest": "npm run build:test",
|
|
91
91
|
"prepublishOnly": "npm run build:types",
|
|
92
|
-
"test": "npm run test:
|
|
92
|
+
"test": "npm run test:bare",
|
|
93
93
|
"pretest:bare": "npm run build:test",
|
|
94
94
|
"test:bare": "ls test/*.test.js | xargs -P1 -n1 brittle-bare test/bare.js",
|
|
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.8.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.7.4",
|
|
103
103
|
"bare-path": "^3.1.1",
|
|
104
104
|
"compact-encoding": "^3.3.0",
|
|
105
|
-
"corestore": "^7.
|
|
105
|
+
"corestore": "^7.12.0",
|
|
106
106
|
"hrpc": "^4.3.0",
|
|
107
|
-
"hypercore": "^11.
|
|
107
|
+
"hypercore": "^11.35.1",
|
|
108
108
|
"hypercore-crypto": "^3.7.0",
|
|
109
109
|
"hypercore-storage": "^3.2.0",
|
|
110
|
-
"hyperdb": "^6.
|
|
110
|
+
"hyperdb": "^6.8.0",
|
|
111
111
|
"hyperdispatch": "^1.6.0",
|
|
112
|
-
"hyperschema": "^1.
|
|
112
|
+
"hyperschema": "^1.22.0",
|
|
113
113
|
"ready-resource": "^1.2.0",
|
|
114
114
|
"safety-catch": "^1.0.3",
|
|
115
115
|
"streamx": "^2.28.0",
|
package/src/build/schemas.js
CHANGED
package/src/handle/index.js
CHANGED
|
@@ -14,6 +14,7 @@ import { Pairing } from '@cero-base/core/pairing'
|
|
|
14
14
|
import { toId, grants, addWriterPayload } from '@cero-base/core/utils'
|
|
15
15
|
import { CeroError } from '@cero-base/core/errors'
|
|
16
16
|
import { Blobs } from '@cero-base/core/blobs'
|
|
17
|
+
import { decodeId } from '@cero-base/core/blobs/codec'
|
|
17
18
|
import { FileServer } from '@cero-base/core/blobs/server'
|
|
18
19
|
|
|
19
20
|
import { NS, TIMEOUT } from '../lib/constants.js'
|
|
@@ -121,6 +122,7 @@ export class Handle extends ReadyResource {
|
|
|
121
122
|
this._onerror = this._opts.onerror || safetyCatch
|
|
122
123
|
this.children = parent ? null : new Set()
|
|
123
124
|
this._loading = parent ? null : new Map()
|
|
125
|
+
this._joining = parent ? null : new Map()
|
|
124
126
|
this._coreKeys = parent ? null : new Map()
|
|
125
127
|
this._fileServer = null
|
|
126
128
|
this._blobs = null
|
|
@@ -339,6 +341,50 @@ export class Handle extends ReadyResource {
|
|
|
339
341
|
return blobs
|
|
340
342
|
}
|
|
341
343
|
|
|
344
|
+
/**
|
|
345
|
+
* Remember the blob-core key a file id points at so the file server can
|
|
346
|
+
* open the core. Lives on the Handle (not the shared operators) because the
|
|
347
|
+
* epoch-key derivation pulls native crypto — the RPC client must stay
|
|
348
|
+
* bundleable without it.
|
|
349
|
+
*
|
|
350
|
+
* @param {string} id
|
|
351
|
+
* @param {number} [stamp]
|
|
352
|
+
*/
|
|
353
|
+
_registerBlobCore(id, stamp) {
|
|
354
|
+
if (!id || !this.root?._coreKeys) return
|
|
355
|
+
try {
|
|
356
|
+
const { coreKey } = decodeId(id)
|
|
357
|
+
const hex = b4a.toString(coreKey, 'hex')
|
|
358
|
+
if (!this.root._coreKeys.has(hex)) {
|
|
359
|
+
// a file-field value carries no stamp — look it up from the files row
|
|
360
|
+
// (fire-and-forget: idempotent, resolution happens again per request)
|
|
361
|
+
if (stamp === undefined) {
|
|
362
|
+
this.store
|
|
363
|
+
.get('files', id)
|
|
364
|
+
.then(({ data }) => data && this._registerBlobCore(id, data.stamp || 0))
|
|
365
|
+
.catch(safetyCatch)
|
|
366
|
+
return
|
|
367
|
+
}
|
|
368
|
+
const key = this._blobCoreKey(stamp)
|
|
369
|
+
if (!key) return // unknown epoch — this device is not entitled to the core
|
|
370
|
+
this.root._coreKeys.set(hex, key)
|
|
371
|
+
}
|
|
372
|
+
// remember which handle read it, so close prunes the entry (re-registered
|
|
373
|
+
// on the next read if another handle still serves the same core)
|
|
374
|
+
if (this !== this.root) (this._blobKeys ??= new Set()).add(hex)
|
|
375
|
+
} catch {
|
|
376
|
+
// ignore invalid ids
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Base-era blob cores use the OWNING handle's key (not the root's — rooms
|
|
381
|
+
// have their own keys); rotated-era cores derive from the epoch entropy.
|
|
382
|
+
_blobCoreKey(stamp) {
|
|
383
|
+
if (!stamp) return this.store.encryptionKey
|
|
384
|
+
const entropy = this.store.keyring.entropy(stamp)
|
|
385
|
+
return entropy ? blobEpochKey(entropy) : null
|
|
386
|
+
}
|
|
387
|
+
|
|
342
388
|
/** Canonical id — identity id for the root handle, store key for children. */
|
|
343
389
|
get id() {
|
|
344
390
|
if (!this.parent) return this.identity.id
|
|
@@ -585,7 +631,33 @@ export class Handle extends ReadyResource {
|
|
|
585
631
|
* @param {JoinChildOpts} [opts]
|
|
586
632
|
* @returns {Promise<Handle>}
|
|
587
633
|
*/
|
|
588
|
-
async _join(invite, type,
|
|
634
|
+
async _join(invite, type, opts = {}) {
|
|
635
|
+
// Coalesce concurrent joins of the same room (a retry racing a pending
|
|
636
|
+
// join, a double tap): the pairing layer allows one candidate per invite,
|
|
637
|
+
// so the second attempt must attach to the in-flight join, not start
|
|
638
|
+
// another handshake.
|
|
639
|
+
const target = Pairing.inviteTopic(invite)
|
|
640
|
+
const key = target && `${type}/${b4a.toString(target, 'hex')}`
|
|
641
|
+
if (!key) return this._pair(invite, type, opts, target)
|
|
642
|
+
const pending = this._joining.get(key)
|
|
643
|
+
if (pending) return pending
|
|
644
|
+
const joining = this._pair(invite, type, opts, target)
|
|
645
|
+
this._joining.set(key, joining)
|
|
646
|
+
try {
|
|
647
|
+
return await joining
|
|
648
|
+
} finally {
|
|
649
|
+
this._joining.delete(key)
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* @param {string} invite
|
|
655
|
+
* @param {string} type
|
|
656
|
+
* @param {JoinChildOpts} [opts]
|
|
657
|
+
* @param {Uint8Array | null} [target]
|
|
658
|
+
* @returns {Promise<Handle>}
|
|
659
|
+
*/
|
|
660
|
+
async _pair(invite, type, { routes, timeout } = {}, target = null) {
|
|
589
661
|
const deadline = timeout || TIMEOUT
|
|
590
662
|
|
|
591
663
|
// Idempotent: if the invite targets a handle we already have AND we are
|
|
@@ -594,7 +666,6 @@ export class Handle extends ReadyResource {
|
|
|
594
666
|
// removed), the stored writer is revoked and the old session can never
|
|
595
667
|
// become writable again — holding a fresh invite is exactly the
|
|
596
668
|
// re-admission path, so fall through to a real pairing instead.
|
|
597
|
-
const target = Pairing.inviteTopic(invite)
|
|
598
669
|
if (target) {
|
|
599
670
|
const { data: joined } = await this.store.get('handles')
|
|
600
671
|
const existing = joined.find(
|
|
@@ -767,7 +838,9 @@ export class Handle extends ReadyResource {
|
|
|
767
838
|
if (!spec) throw CeroError.REQUIRED('spec')
|
|
768
839
|
|
|
769
840
|
const writer = Identity.randomKeyPair()
|
|
770
|
-
|
|
841
|
+
// join-only: no member listener, so concurrent joins never collide on the
|
|
842
|
+
// identity-derived default topic ('Active member already exist')
|
|
843
|
+
const pair = new Pairing({ network: net, identity: id, host: false })
|
|
771
844
|
await pair.ready()
|
|
772
845
|
|
|
773
846
|
let key, encryptionKey, additional
|
package/src/lib/operators.js
CHANGED
|
@@ -2,7 +2,6 @@ import { Readable } from 'streamx'
|
|
|
2
2
|
import b4a from 'b4a'
|
|
3
3
|
|
|
4
4
|
import { encodeId, decodeId } from '@cero-base/core/blobs/codec'
|
|
5
|
-
import { blobEpochKey } from '@cero-base/core/database/encryption'
|
|
6
5
|
import { CeroError } from '@cero-base/core/errors'
|
|
7
6
|
import { onAbort } from './utils.js'
|
|
8
7
|
|
|
@@ -169,7 +168,7 @@ function resolveRow(ref, row) {
|
|
|
169
168
|
if (ref.handle.rpc) return ref.handle._resolveRow(ref.name, ref.handle._refInfo(ref.name), row)
|
|
170
169
|
const handle = ref.handle
|
|
171
170
|
const resolve = (id, name, stamp) => {
|
|
172
|
-
|
|
171
|
+
handle._registerBlobCore(id, stamp)
|
|
173
172
|
return resolveFile(handle, id, name)
|
|
174
173
|
}
|
|
175
174
|
if (ref.name === 'files') {
|
|
@@ -186,41 +185,6 @@ function resolveRow(ref, row) {
|
|
|
186
185
|
return out
|
|
187
186
|
}
|
|
188
187
|
|
|
189
|
-
function registerBlobCore(handle, id, stamp) {
|
|
190
|
-
if (!id || !handle.root?._coreKeys) return
|
|
191
|
-
try {
|
|
192
|
-
const { coreKey } = decodeId(id)
|
|
193
|
-
const hex = b4a.toString(coreKey, 'hex')
|
|
194
|
-
if (!handle.root._coreKeys.has(hex)) {
|
|
195
|
-
// a file-field value carries no stamp — look it up from the files row
|
|
196
|
-
// (fire-and-forget: idempotent, resolution happens again per request)
|
|
197
|
-
if (stamp === undefined) {
|
|
198
|
-
handle.store
|
|
199
|
-
.get('files', id)
|
|
200
|
-
.then(({ data }) => data && registerBlobCore(handle, id, data.stamp || 0))
|
|
201
|
-
.catch(() => {})
|
|
202
|
-
return
|
|
203
|
-
}
|
|
204
|
-
const key = blobCoreKey(handle, stamp)
|
|
205
|
-
if (!key) return // unknown epoch — this device is not entitled to the core
|
|
206
|
-
handle.root._coreKeys.set(hex, key)
|
|
207
|
-
}
|
|
208
|
-
// remember which handle read it, so close prunes the entry (re-registered
|
|
209
|
-
// on the next read if another handle still serves the same core)
|
|
210
|
-
if (handle !== handle.root) (handle._blobKeys ??= new Set()).add(hex)
|
|
211
|
-
} catch {
|
|
212
|
-
// ignore invalid ids
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Base-era blob cores use the OWNING handle's key (not the root's — rooms have
|
|
217
|
-
// their own keys); rotated-era cores derive from the epoch entropy.
|
|
218
|
-
function blobCoreKey(handle, stamp) {
|
|
219
|
-
if (!stamp) return handle.store.encryptionKey
|
|
220
|
-
const entropy = handle.store.keyring.entropy(stamp)
|
|
221
|
-
return entropy ? blobEpochKey(entropy) : null
|
|
222
|
-
}
|
|
223
|
-
|
|
224
188
|
/**
|
|
225
189
|
* Read from `ref`. For data refs, dispatches to the underlying store. For
|
|
226
190
|
* `handle`-kind refs, lists existing child handles of that type from the
|
package/src/rpc/client.js
CHANGED
|
@@ -385,12 +385,12 @@ const operators = {
|
|
|
385
385
|
* @param {{ role?: string }} [opts]
|
|
386
386
|
* @returns {Promise<string>}
|
|
387
387
|
*/
|
|
388
|
-
async invite({ role, expiresIn,
|
|
388
|
+
async invite({ role, expiresIn, reuse } = {}) {
|
|
389
389
|
const { invite } = await this.rpc.invite({
|
|
390
390
|
handle: this.id,
|
|
391
391
|
role: role || '',
|
|
392
392
|
expiresIn: expiresIn || 0,
|
|
393
|
-
|
|
393
|
+
reuse: reuse === true
|
|
394
394
|
})
|
|
395
395
|
return invite
|
|
396
396
|
},
|
package/src/rpc/server.js
CHANGED
|
@@ -267,12 +267,12 @@ export class Server extends RPCServer {
|
|
|
267
267
|
|
|
268
268
|
/** Register invite/revoke/join RPC handlers. */
|
|
269
269
|
_wirePairing() {
|
|
270
|
-
this.rpc.onInvite(async ({ handle, role, expiresIn,
|
|
270
|
+
this.rpc.onInvite(async ({ handle, role, expiresIn, reuse }) => {
|
|
271
271
|
const h = this._resolve(handle)
|
|
272
272
|
const invite = await h.invite({
|
|
273
273
|
role: role || undefined,
|
|
274
274
|
expiresIn: expiresIn || undefined,
|
|
275
|
-
|
|
275
|
+
reuse: reuse === true
|
|
276
276
|
})
|
|
277
277
|
return { invite }
|
|
278
278
|
})
|
package/types/build/schemas.d.ts
CHANGED
|
@@ -129,7 +129,7 @@ export const rpc: {
|
|
|
129
129
|
handle: import("@cero-base/core").Prim;
|
|
130
130
|
role: import("@cero-base/core").Prim;
|
|
131
131
|
expiresIn: import("@cero-base/core").Prim;
|
|
132
|
-
|
|
132
|
+
reuse: import("@cero-base/core").Prim;
|
|
133
133
|
};
|
|
134
134
|
'req-revoke': {
|
|
135
135
|
handle: import("@cero-base/core").Prim;
|
package/types/handle/index.d.ts
CHANGED
|
@@ -91,6 +91,7 @@ export class Handle extends ReadyResource {
|
|
|
91
91
|
_onerror: any;
|
|
92
92
|
children: Set<any>;
|
|
93
93
|
_loading: Map<any, any>;
|
|
94
|
+
_joining: Map<any, any>;
|
|
94
95
|
_coreKeys: Map<any, any>;
|
|
95
96
|
_fileServer: FileServer;
|
|
96
97
|
_blobs: Blobs;
|
|
@@ -172,6 +173,17 @@ export class Handle extends ReadyResource {
|
|
|
172
173
|
get blobs(): Blobs;
|
|
173
174
|
_baseBlobs(): Blobs;
|
|
174
175
|
_makeBlobs(name: any, encryptionKey: any, stamp: any): Blobs;
|
|
176
|
+
/**
|
|
177
|
+
* Remember the blob-core key a file id points at so the file server can
|
|
178
|
+
* open the core. Lives on the Handle (not the shared operators) because the
|
|
179
|
+
* epoch-key derivation pulls native crypto — the RPC client must stay
|
|
180
|
+
* bundleable without it.
|
|
181
|
+
*
|
|
182
|
+
* @param {string} id
|
|
183
|
+
* @param {number} [stamp]
|
|
184
|
+
*/
|
|
185
|
+
_registerBlobCore(id: string, stamp?: number): void;
|
|
186
|
+
_blobCoreKey(stamp: any): Uint8Array<ArrayBufferLike>;
|
|
175
187
|
/** Canonical id — identity id for the root handle, store key for children. */
|
|
176
188
|
get id(): any;
|
|
177
189
|
/** This device's id + name. `null` on child handles. */
|
|
@@ -266,7 +278,15 @@ export class Handle extends ReadyResource {
|
|
|
266
278
|
* @param {JoinChildOpts} [opts]
|
|
267
279
|
* @returns {Promise<Handle>}
|
|
268
280
|
*/
|
|
269
|
-
_join(invite: string, type: string,
|
|
281
|
+
_join(invite: string, type: string, opts?: JoinChildOpts): Promise<Handle>;
|
|
282
|
+
/**
|
|
283
|
+
* @param {string} invite
|
|
284
|
+
* @param {string} type
|
|
285
|
+
* @param {JoinChildOpts} [opts]
|
|
286
|
+
* @param {Uint8Array | null} [target]
|
|
287
|
+
* @returns {Promise<Handle>}
|
|
288
|
+
*/
|
|
289
|
+
_pair(invite: string, type: string, { routes, timeout }?: JoinChildOpts, target?: Uint8Array | null): Promise<Handle>;
|
|
270
290
|
/**
|
|
271
291
|
* Get an open child by id, or re-open it. Concurrent calls for the same id
|
|
272
292
|
* share one in-flight load, so the child is built — and `handle` emitted —
|