@cero-base/cero 1.19.0 → 2.1.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 +38 -643
- package/package.json +9 -6
- package/src/build/index.js +63 -61
- package/src/build/internal.js +123 -0
- package/src/build/schemas.js +6 -11
- package/src/extensions/handle-sync.js +3 -11
- package/src/extensions/index.js +86 -0
- package/src/extensions/profile-sync.js +15 -16
- package/src/handle/index.js +290 -284
- package/src/index.js +24 -81
- package/src/lib/bluetooth.js +25 -56
- package/src/lib/constants.js +0 -15
- package/src/lib/operators.js +35 -134
- package/src/lib/peek.js +4 -8
- package/src/lib/refs.js +10 -8
- package/src/lib/spec.js +2 -3
- package/src/local/index.js +2 -3
- package/src/rpc/client.js +54 -77
- package/src/rpc/index.js +3 -3
- package/src/rpc/server.js +40 -43
- package/types/build/index.d.ts +11 -9
- package/types/build/{builtins.d.ts → internal.d.ts} +20 -38
- package/types/build/schemas.d.ts +4 -3
- package/types/extensions/handle-sync.d.ts +2 -8
- package/types/extensions/index.d.ts +102 -0
- package/types/extensions/profile-sync.d.ts +0 -5
- package/types/handle/index.d.ts +90 -108
- package/types/index.d.ts +21 -17
- package/types/lib/bluetooth.d.ts +8 -32
- package/types/lib/constants.d.ts +0 -11
- package/types/lib/operators.d.ts +30 -83
- package/types/lib/peek.d.ts +2 -3
- package/types/lib/refs.d.ts +5 -5
- package/types/lib/spec.d.ts +2 -3
- package/types/local/index.d.ts +2 -3
- package/types/rpc/client.d.ts +29 -26
- package/types/rpc/index.d.ts +3 -3
- package/types/rpc/server.d.ts +7 -10
- package/src/build/builtins.js +0 -174
- package/src/lib/internal.js +0 -9
- package/types/lib/internal.d.ts +0 -24
package/src/lib/operators.js
CHANGED
|
@@ -7,6 +7,7 @@ import { onAbort } from '@cero-base/core/utils'
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* @typedef {import('./refs.js').Ref} Ref
|
|
10
|
+
* @typedef {import('@cero-base/core/database').HookContext} HookContext
|
|
10
11
|
* @typedef {import('../handle/index.js').CeroHandle} CeroHandle
|
|
11
12
|
* @typedef {{ data: any }} SingleResult
|
|
12
13
|
* @typedef {{ data: any[], total: number, size: number }} ListResult
|
|
@@ -30,9 +31,7 @@ export function resolveFile(handle, id, name) {
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
|
-
* Insert (or overwrite by id) a row on `ref`.
|
|
34
|
-
* `put(handle.files, { data, type, name? })` uploads the bytes to this handle's
|
|
35
|
-
* blob store, records `{ id, name }`, and resolves the file.
|
|
34
|
+
* Insert (or overwrite by id) a row on `ref`.
|
|
36
35
|
*
|
|
37
36
|
* @param {Ref} ref
|
|
38
37
|
* @param {Record<string, any>} row
|
|
@@ -55,9 +54,7 @@ async function putFile(ref, row) {
|
|
|
55
54
|
}
|
|
56
55
|
|
|
57
56
|
/**
|
|
58
|
-
* Upsert a row on `ref` — merges with the existing row and preserves
|
|
59
|
-
* `createdAt`. Pass `{ upsert: false }` to update-only: a missing row is left
|
|
60
|
-
* untouched instead of created (atomic — never resurrects a deleted row).
|
|
57
|
+
* Upsert a row on `ref` — merges with the existing row and preserves `createdAt`.
|
|
61
58
|
*
|
|
62
59
|
* @param {Ref} ref
|
|
63
60
|
* @param {Record<string, any>} row
|
|
@@ -79,17 +76,6 @@ export function del(ref, id) {
|
|
|
79
76
|
return ref.handle.store.del(ref.name, id)
|
|
80
77
|
}
|
|
81
78
|
|
|
82
|
-
/**
|
|
83
|
-
* Count rows on `ref`, optionally filtered.
|
|
84
|
-
*
|
|
85
|
-
* @param {Ref} ref
|
|
86
|
-
* @param {Record<string, any>} [q]
|
|
87
|
-
* @returns {Promise<{ data: number }>}
|
|
88
|
-
*/
|
|
89
|
-
export function count(ref, q) {
|
|
90
|
-
return ref.handle.store.count(ref.name, q)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
79
|
/**
|
|
94
80
|
* Invoke an `action`-kind ref (a custom mutation declared in the schema).
|
|
95
81
|
*
|
|
@@ -101,20 +87,25 @@ export function call(ref, d) {
|
|
|
101
87
|
return ref.handle.store.call(ref.name, d)
|
|
102
88
|
}
|
|
103
89
|
|
|
104
|
-
// Write ops per ref kind, for `before`/`after` subscriptions.
|
|
105
90
|
const WRITES = { single: ['set'], collection: ['put', 'set', 'del'] }
|
|
106
91
|
|
|
107
92
|
/**
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
93
|
+
* Rule that runs before a write to `ref` lands — at apply, on every peer, inside the op's
|
|
94
|
+
* transaction. Return `false` to refuse it: the writer's own call rejects with `REFUSED`.
|
|
95
|
+
* `ctx` is `{ op, name, row, existing, id, memberId, role, get, put, set, del }`; mutate
|
|
96
|
+
* `ctx.row` to rewrite what is stored. `op` is the op as it applies, so an upsert on a
|
|
97
|
+
* collection is a `put`. The four operators on `ctx` read and write the room as it stands at
|
|
98
|
+
* this op, inside the transaction. Must be deterministic — read only `ctx`, never a clock or
|
|
99
|
+
* local state — and registered before any op applies, in the process that owns the data. The
|
|
100
|
+
* imported operators throw inside a hook; use the ones on `ctx`. Not available over RPC.
|
|
111
101
|
*
|
|
112
102
|
* @param {Ref} ref
|
|
113
|
-
* @param {(ctx:
|
|
103
|
+
* @param {(ctx: HookContext) => unknown} fn
|
|
114
104
|
* @param {{ signal?: AbortSignal }} [opts]
|
|
115
105
|
* @returns {() => void}
|
|
116
106
|
*/
|
|
117
107
|
export function before(ref, fn, opts) {
|
|
108
|
+
if (ref.type) return ref.handle._hookType(before, ref, fn, opts)
|
|
118
109
|
const db = ref.handle.store
|
|
119
110
|
const ops = WRITES[ref.kind] || ['set']
|
|
120
111
|
const offs = ops.map((op) =>
|
|
@@ -130,33 +121,31 @@ export function before(ref, fn, opts) {
|
|
|
130
121
|
}
|
|
131
122
|
|
|
132
123
|
/**
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
124
|
+
* Rule that runs after a write to `ref` lands — at apply, on every peer, inside the op's
|
|
125
|
+
* transaction. Write derived rows through `ctx.put` / `ctx.set` / `ctx.del`; a throw refuses
|
|
126
|
+
* the whole op. Same `ctx` and the same determinism and registration rules as `before`. Use
|
|
127
|
+
* `changes(ref)` instead to observe writes locally.
|
|
136
128
|
*
|
|
137
129
|
* @param {Ref} ref
|
|
138
|
-
* @param {(ctx:
|
|
130
|
+
* @param {(ctx: HookContext) => unknown} fn
|
|
139
131
|
* @param {{ signal?: AbortSignal }} [opts]
|
|
140
132
|
* @returns {() => void}
|
|
141
133
|
*/
|
|
142
134
|
export function after(ref, fn, opts) {
|
|
135
|
+
if (ref.type) return ref.handle._hookType(after, ref, fn, opts)
|
|
143
136
|
const db = ref.handle.store
|
|
144
137
|
const ops = WRITES[ref.kind] || ['set']
|
|
145
|
-
const
|
|
146
|
-
for (const op of ops) db.on(`after:${op}`, handler)
|
|
138
|
+
const offs = ops.map((op) => db.after(op, (ctx) => (ctx.name === ref.name ? fn(ctx) : undefined)))
|
|
147
139
|
let stopAbort
|
|
148
140
|
const off = () => {
|
|
149
|
-
|
|
141
|
+
offs.forEach((unsub) => unsub())
|
|
150
142
|
stopAbort?.()
|
|
151
143
|
}
|
|
152
144
|
stopAbort = onAbort(opts?.signal, off)
|
|
153
145
|
return off
|
|
154
146
|
}
|
|
155
147
|
|
|
156
|
-
//
|
|
157
|
-
// filtered by type (handle types are stored there with their { id, key,
|
|
158
|
-
// encryptionKey, name }). Data-kind refs go straight to the store.
|
|
159
|
-
// For handle-kind refs on the facade, the parent store lives on `root`.
|
|
148
|
+
// handle refs read their rows from the parent's `handles` collection, filtered by type
|
|
160
149
|
const parentStore = (ref) => (ref.handle.root ? ref.handle.root.store : ref.handle.store)
|
|
161
150
|
|
|
162
151
|
const normalize = (rows, name) => {
|
|
@@ -195,9 +184,7 @@ function resolveRow(ref, row) {
|
|
|
195
184
|
}
|
|
196
185
|
|
|
197
186
|
/**
|
|
198
|
-
* Read from `ref`. For data refs, dispatches to the underlying store.
|
|
199
|
-
* `handle`-kind refs, lists existing child handles of that type from the
|
|
200
|
-
* parent's `handles` collection.
|
|
187
|
+
* Read from `ref`. For data refs, dispatches to the underlying store.
|
|
201
188
|
*
|
|
202
189
|
* @param {Ref} ref
|
|
203
190
|
* @param {string | Record<string, any>} [q]
|
|
@@ -212,22 +199,17 @@ export async function get(ref, q) {
|
|
|
212
199
|
return resolveResult(ref, res)
|
|
213
200
|
}
|
|
214
201
|
|
|
215
|
-
//
|
|
216
|
-
// to an optional `{ signal }` (destroyed on abort). Local refs have no
|
|
217
|
-
// close-cascade, so they keep managing their own streams.
|
|
202
|
+
// a watch stream dies with its handle, or with the signal
|
|
218
203
|
const bindStream = (owner, stream, opts) => {
|
|
219
204
|
const stopAbort = onAbort(opts?.signal, () => stream.destroy())
|
|
220
|
-
// drop the abort listener once the stream ends
|
|
221
|
-
// long-lived signal after the stream is gone
|
|
205
|
+
// drop the abort listener once the stream ends
|
|
222
206
|
if (stopAbort) stream.once('close', stopAbort)
|
|
223
207
|
return owner.own ? owner.own(stream) : stream
|
|
224
208
|
}
|
|
225
209
|
|
|
226
210
|
/**
|
|
227
|
-
* Live snapshot stream on `ref` — re-emits the latest `get()` result on
|
|
228
|
-
*
|
|
229
|
-
* handle destroys it. Pass `{ signal }` to bind it to a finer scope, or
|
|
230
|
-
* destroy the stream directly to stop watching sooner.
|
|
211
|
+
* Live snapshot stream on `ref` — re-emits the latest `get()` result on every underlying
|
|
212
|
+
* mutation.
|
|
231
213
|
*
|
|
232
214
|
* @param {Ref} ref
|
|
233
215
|
* @param {Record<string, any>} [q]
|
|
@@ -247,10 +229,9 @@ export function watch(ref, q, opts) {
|
|
|
247
229
|
}
|
|
248
230
|
|
|
249
231
|
/**
|
|
250
|
-
* Delta subscription: batches of `{ prev, next }` row pairs instead of
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
* with `reset: true`). File-typed fields resolve on both sides.
|
|
232
|
+
* Delta subscription: batches of `{ prev, next }` row pairs instead of full snapshots —
|
|
233
|
+
* lossless under backpressure, self-contained (the first batch, and any batch after a view
|
|
234
|
+
* swap, replays current.
|
|
254
235
|
*/
|
|
255
236
|
export function changes(ref, q, opts) {
|
|
256
237
|
const owner = ref.handle
|
|
@@ -297,9 +278,7 @@ export function changes(ref, q, opts) {
|
|
|
297
278
|
return bindStream(owner, out, opts)
|
|
298
279
|
}
|
|
299
280
|
|
|
300
|
-
//
|
|
301
|
-
// instead of queueing every intermediate (a busy room + un-drained reader
|
|
302
|
-
// used to buffer full result sets without bound).
|
|
281
|
+
// snapshots are idempotent: a slow consumer gets only the newest
|
|
303
282
|
function snapshotStream(src, map) {
|
|
304
283
|
let pending
|
|
305
284
|
let wanted = false
|
|
@@ -329,19 +308,8 @@ function snapshotStream(src, map) {
|
|
|
329
308
|
return out
|
|
330
309
|
}
|
|
331
310
|
|
|
332
|
-
// Universal signal instantiator. Dispatch on the second arg:
|
|
333
|
-
// string → join via invite
|
|
334
|
-
// { invite: string } → join via invite (object form)
|
|
335
|
-
// { id: string } → load an existing handle by id
|
|
336
|
-
// object | undefined → create (opts)
|
|
337
311
|
/**
|
|
338
|
-
* Open (or create / join / load) a child handle through a `handle`-kind
|
|
339
|
-
* ref. Dispatches on the normalize of `arg`:
|
|
340
|
-
*
|
|
341
|
-
* - `string` → join via an invite string
|
|
342
|
-
* - `{ invite: string }` → join via invite (object form)
|
|
343
|
-
* - `{ id: string }` → load an existing handle by id
|
|
344
|
-
* - `object | undefined` → create a new handle with the given opts
|
|
312
|
+
* Open (or create / join / load) a child handle through a `handle`-kind ref.
|
|
345
313
|
*
|
|
346
314
|
* @param {Ref} ref
|
|
347
315
|
* @param {string | { invite?: string, id?: string, name?: string, routes?: any, role?: string, accept?: boolean } | undefined} [arg]
|
|
@@ -355,13 +323,9 @@ export function open(ref, arg) {
|
|
|
355
323
|
}
|
|
356
324
|
|
|
357
325
|
/**
|
|
358
|
-
* Rotate a handle's encryption epoch. A fresh secret is sealed to every
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
* permission (admin or owner). Compose with removal:
|
|
362
|
-
*
|
|
363
|
-
* await cero.del(room.members, memberId)
|
|
364
|
-
* await cero.rotate(room)
|
|
326
|
+
* Rotate a handle's encryption epoch. A fresh secret is sealed to every current member and
|
|
327
|
+
* announced through the log — members removed before the rotation cannot decrypt anything
|
|
328
|
+
* written after it.
|
|
365
329
|
*
|
|
366
330
|
* @param {any} handle
|
|
367
331
|
* @returns {Promise<{ epoch: number }>}
|
|
@@ -369,66 +333,3 @@ export function open(ref, arg) {
|
|
|
369
333
|
export function rotate(handle) {
|
|
370
334
|
return handle.store.rotate()
|
|
371
335
|
}
|
|
372
|
-
|
|
373
|
-
// ─── custom operators ──────────────────────────────────────────────────────
|
|
374
|
-
// App business logic lives as custom operators: pure functions whose first arg
|
|
375
|
-
// is the handle they act on, composed from the operators above. `define`
|
|
376
|
-
// registers them by scope; `bind` puts them on a handle — either an explicit
|
|
377
|
-
// `{ ns: module }` map, or (given a scope) the registered operators for that
|
|
378
|
-
// scope, which is how cero auto-binds the root and each child as it opens.
|
|
379
|
-
|
|
380
|
-
const registry = {}
|
|
381
|
-
|
|
382
|
-
// Curry `handle` as arg 0 of every function in `fns`, under `handle[ns]`.
|
|
383
|
-
function attach(handle, ns, fns) {
|
|
384
|
-
const bound = {}
|
|
385
|
-
for (const key of Object.keys(fns)) {
|
|
386
|
-
if (typeof fns[key] === 'function') bound[key] = (...args) => fns[key](handle, ...args)
|
|
387
|
-
}
|
|
388
|
-
handle[ns] = bound
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
/**
|
|
392
|
-
* Put custom operators on `handle`, currying it as their first argument so
|
|
393
|
-
* `handle.ns.fn(args)` calls `fn(handle, args)`. `arg` is either:
|
|
394
|
-
* - a `{ ns: module }` map → bind exactly those, or
|
|
395
|
-
* - `null` → the registered root operators, or
|
|
396
|
-
* - a child-handle type → the registered operators for that type.
|
|
397
|
-
* The scope forms are how cero binds handles automatically; pass a map yourself
|
|
398
|
-
* for manual binding.
|
|
399
|
-
*
|
|
400
|
-
* @param {any} handle
|
|
401
|
-
* @param {Record<string, any> | string | null} arg
|
|
402
|
-
* @returns {any} handle
|
|
403
|
-
*/
|
|
404
|
-
export function bind(handle, arg) {
|
|
405
|
-
if (arg !== null && typeof arg !== 'string') {
|
|
406
|
-
for (const ns of Object.keys(arg)) attach(handle, ns, arg[ns])
|
|
407
|
-
return handle
|
|
408
|
-
}
|
|
409
|
-
const handles = handle.spec?.meta?.handles || {}
|
|
410
|
-
for (const ns of Object.keys(registry)) {
|
|
411
|
-
if (arg === null) {
|
|
412
|
-
if (!(ns in handles)) attach(handle, ns, registry[ns]) // bare root namespace
|
|
413
|
-
} else if (ns === arg) {
|
|
414
|
-
for (const k of Object.keys(registry[ns])) attach(handle, k, registry[ns][k]) // child group
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
return handle
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
/**
|
|
421
|
-
* Register custom operators by scope. A bare key binds on the root handle; a key
|
|
422
|
-
* that names a child-handle type binds on every handle of that type. Call once
|
|
423
|
-
* at startup, before `cero()` / `connect()`, in both processes.
|
|
424
|
-
*
|
|
425
|
-
* @param {Record<string, any>} map
|
|
426
|
-
*/
|
|
427
|
-
export function define(map) {
|
|
428
|
-
Object.assign(registry, map)
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
/** Test seam: clear all registered operators. */
|
|
432
|
-
export function _clearDefined() {
|
|
433
|
-
for (const k of Object.keys(registry)) delete registry[k]
|
|
434
|
-
}
|
package/src/lib/peek.js
CHANGED
|
@@ -6,14 +6,11 @@ import { CeroError } from '@cero-base/core/errors'
|
|
|
6
6
|
|
|
7
7
|
import { Local } from '../local/index.js'
|
|
8
8
|
|
|
9
|
-
//
|
|
10
|
-
// module graph — bundlers follow even dynamic imports, and a browser build
|
|
11
|
-
// must never reach hypercore/sodium.
|
|
9
|
+
// outside operators.js so storage deps stay off the client's module graph
|
|
12
10
|
|
|
13
11
|
/**
|
|
14
|
-
* Quickly check whether the on-disk directory at `dir` already holds an
|
|
15
|
-
*
|
|
16
|
-
* read-only and closes everything before returning.
|
|
12
|
+
* Quickly check whether the on-disk directory at `dir` already holds an initialised cero
|
|
13
|
+
* identity (i.e. a stored master seed).
|
|
17
14
|
*
|
|
18
15
|
* @param {string} dir Cero data directory.
|
|
19
16
|
* @param {any} spec Built spec — same value passed to `cero(dir, spec)`.
|
|
@@ -23,8 +20,7 @@ export async function peek(dir, spec) {
|
|
|
23
20
|
if (typeof dir !== 'string' || !dir) throw CeroError.INVALID('dir must be a non-empty string')
|
|
24
21
|
if (!spec) throw CeroError.REQUIRED('spec')
|
|
25
22
|
|
|
26
|
-
//
|
|
27
|
-
// any ready() must still close root + store + local, not leak the storage lock.
|
|
23
|
+
// construct first, ready inside the try: a corrupt dir must still close everything
|
|
28
24
|
const root = new HypercoreStorage(`${dir}/main`)
|
|
29
25
|
const store = new Corestore(root, { manifestVersion: 2 })
|
|
30
26
|
const local = new Local(null, spec, { store })
|
package/src/lib/refs.js
CHANGED
|
@@ -9,9 +9,7 @@ import { CeroError } from '@cero-base/core/errors'
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* Typed pointer to a single ref (table or handle slot) on a `Handle` or
|
|
13
|
-
* `Local`. Operators (`put`/`get`/`open`/...) take a `Ref` as their first
|
|
14
|
-
* argument and dispatch through the owning handle's store.
|
|
12
|
+
* Typed pointer to a single ref (table or handle slot) on a `Handle` or `Local`.
|
|
15
13
|
*/
|
|
16
14
|
export class Ref {
|
|
17
15
|
/**
|
|
@@ -20,11 +18,12 @@ export class Ref {
|
|
|
20
18
|
* @param {string} kind Ref kind: `'collection'`, `'single'`, `'action'`, or `'handle'`.
|
|
21
19
|
* @param {string | null} [schema] Fully-qualified schema id, if any.
|
|
22
20
|
*/
|
|
23
|
-
constructor(handle, name, kind, schema = null) {
|
|
21
|
+
constructor(handle, name, kind, schema = null, type = null) {
|
|
24
22
|
this.handle = handle
|
|
25
23
|
this.name = name
|
|
26
24
|
this.kind = kind
|
|
27
25
|
this.schema = schema
|
|
26
|
+
this.type = type
|
|
28
27
|
}
|
|
29
28
|
|
|
30
29
|
/**
|
|
@@ -33,17 +32,20 @@ export class Ref {
|
|
|
33
32
|
*
|
|
34
33
|
* @param {any} target
|
|
35
34
|
* @param {Record<string, RefInfo>} refs
|
|
35
|
+
* @param {Record<string, any>} [handles] The handle types, so `target.room.notes` names every room's notes.
|
|
36
36
|
*/
|
|
37
|
-
static attach(target, refs) {
|
|
37
|
+
static attach(target, refs, handles) {
|
|
38
38
|
for (const [name, info] of Object.entries(refs || {})) {
|
|
39
|
-
//
|
|
40
|
-
// on, store) when a schema declares a ref named like a reserved member
|
|
39
|
+
// a ref named like a reserved member must fail loud, not overwrite it
|
|
41
40
|
if (name in target) {
|
|
42
41
|
throw CeroError.INVALID(
|
|
43
42
|
`schema ref '${name}' collides with a reserved ${target.constructor?.name || 'handle'} member — rename it`
|
|
44
43
|
)
|
|
45
44
|
}
|
|
46
|
-
target[name] = new Ref(target, name, info.kind, info.schema)
|
|
45
|
+
const ref = (target[name] = new Ref(target, name, info.kind, info.schema))
|
|
46
|
+
for (const [sub, i] of Object.entries(handles?.[name]?.meta?.refs || {})) {
|
|
47
|
+
ref[sub] = new Ref(target, sub, i.kind, i.schema, name)
|
|
48
|
+
}
|
|
47
49
|
}
|
|
48
50
|
}
|
|
49
51
|
}
|
package/src/lib/spec.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Re-exports of the schema DSL (`t`) and `schema()` wrapper from
|
|
3
|
-
*
|
|
4
|
-
* pulling in the core package directly.
|
|
2
|
+
* Re-exports of the schema DSL (`t`) and `schema()` wrapper from `@cero-base/core/schema`,
|
|
3
|
+
* so cero apps can describe their tables without pulling in the core package directly.
|
|
5
4
|
*/
|
|
6
5
|
export { t, schema } from '@cero-base/core/schema'
|
package/src/local/index.js
CHANGED
|
@@ -13,9 +13,8 @@ import { Ref } from '../lib/refs.js'
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* Per-device, single-writer storage for cero — holds the master seed,
|
|
17
|
-
*
|
|
18
|
-
* `Storage` and exposes each local ref as a property of the instance.
|
|
16
|
+
* Per-device, single-writer storage for cero — holds the master seed, device keypair and
|
|
17
|
+
* any per-handle keypairs.
|
|
19
18
|
*/
|
|
20
19
|
export class Local extends ReadyResource {
|
|
21
20
|
/**
|
package/src/rpc/client.js
CHANGED
|
@@ -5,23 +5,11 @@ import z32 from 'z32'
|
|
|
5
5
|
import { decodeId } from '@cero-base/core/blobs/codec'
|
|
6
6
|
|
|
7
7
|
import { Ref } from '../lib/refs.js'
|
|
8
|
-
import {
|
|
9
|
-
put,
|
|
10
|
-
set,
|
|
11
|
-
get,
|
|
12
|
-
del,
|
|
13
|
-
count,
|
|
14
|
-
watch,
|
|
15
|
-
changes,
|
|
16
|
-
call,
|
|
17
|
-
open,
|
|
18
|
-
rotate,
|
|
19
|
-
bind,
|
|
20
|
-
define
|
|
21
|
-
} from '../lib/operators.js'
|
|
8
|
+
import { put, set, get, del, watch, changes, call, open, rotate } from '../lib/operators.js'
|
|
22
9
|
import { t, schema } from '../lib/spec.js'
|
|
10
|
+
import { operatorsOf, bind } from '../extensions/index.js'
|
|
23
11
|
|
|
24
|
-
export { put, set, get, del,
|
|
12
|
+
export { put, set, get, del, watch, changes, call, open, rotate, t, schema }
|
|
25
13
|
|
|
26
14
|
/**
|
|
27
15
|
* @typedef {import('@cero-base/core/rpc').RPCClient} BaseRPCClient
|
|
@@ -30,7 +18,7 @@ export { put, set, get, del, count, watch, changes, call, open, rotate, bind, de
|
|
|
30
18
|
* @property {'single'|'collection'|'action'|'handle'} [kind]
|
|
31
19
|
* @property {string} [schema]
|
|
32
20
|
* @property {string} [type]
|
|
33
|
-
* @property {boolean} [
|
|
21
|
+
* @property {boolean} [internal]
|
|
34
22
|
*
|
|
35
23
|
* @typedef {import('@cero-base/core/rpc').Spec & { meta: { ns?: string, refs: Record<string, RefInfo>, local?: { refs: Record<string, RefInfo> }, handles?: Record<string, Spec> }, handles: Record<string, Spec> }} Spec Built cero spec (schema + rpc + per-handle child specs).
|
|
36
24
|
*
|
|
@@ -48,7 +36,6 @@ export { put, set, get, del, count, watch, changes, call, open, rotate, bind, de
|
|
|
48
36
|
* @property {string|null} name
|
|
49
37
|
*/
|
|
50
38
|
|
|
51
|
-
// Compact-encoding blobId struct matching hypercore-blob-server's wire format.
|
|
52
39
|
const blobIdEnc = {
|
|
53
40
|
preencode(state, b) {
|
|
54
41
|
c.uint.preencode(state, b.blockOffset)
|
|
@@ -72,9 +59,7 @@ const blobIdEnc = {
|
|
|
72
59
|
}
|
|
73
60
|
}
|
|
74
61
|
|
|
75
|
-
//
|
|
76
|
-
// row-ops surface as a local cero handle but routed over the wire. `_local`
|
|
77
|
-
// selects the per-device store + JSON codec; main refs use the schema codec.
|
|
62
|
+
// the same row-ops surface as a local handle, routed over the wire
|
|
78
63
|
const operators = {
|
|
79
64
|
_local: false,
|
|
80
65
|
|
|
@@ -113,9 +98,8 @@ const operators = {
|
|
|
113
98
|
},
|
|
114
99
|
|
|
115
100
|
/**
|
|
116
|
-
* Augment a decoded row (or array of rows) to resolve file-typed fields to
|
|
117
|
-
*
|
|
118
|
-
* file id; other refs declare file fields in `meta.refs[name].files`.
|
|
101
|
+
* Augment a decoded row (or array of rows) to resolve file-typed fields to `{ id, type,
|
|
102
|
+
* size, url }` objects.
|
|
119
103
|
*
|
|
120
104
|
* @param {string} name
|
|
121
105
|
* @param {any} data
|
|
@@ -130,7 +114,7 @@ const operators = {
|
|
|
130
114
|
|
|
131
115
|
_resolveRow(name, info, row) {
|
|
132
116
|
if (!row || typeof row !== 'object') return row
|
|
133
|
-
if (info?.
|
|
117
|
+
if (info?.internal && info?.verb === 'file') {
|
|
134
118
|
if (!row.id) return row
|
|
135
119
|
try {
|
|
136
120
|
const { type, blobId } = decodeId(row.id)
|
|
@@ -256,24 +240,6 @@ const operators = {
|
|
|
256
240
|
await this.rpc.del({ handle: this.id, ref: name, id, local: this._local })
|
|
257
241
|
},
|
|
258
242
|
|
|
259
|
-
/**
|
|
260
|
-
* Count matching rows.
|
|
261
|
-
*
|
|
262
|
-
* @param {string} name
|
|
263
|
-
* @param {Record<string, any>} [query]
|
|
264
|
-
* @returns {Promise<{ data: number }>}
|
|
265
|
-
*/
|
|
266
|
-
async count(name, query) {
|
|
267
|
-
const codec = this._codec()
|
|
268
|
-
const res = await this.rpc.count({
|
|
269
|
-
handle: this.id,
|
|
270
|
-
ref: name,
|
|
271
|
-
query: codec.encodeQuery(query),
|
|
272
|
-
local: this._local
|
|
273
|
-
})
|
|
274
|
-
return { data: res.count }
|
|
275
|
-
},
|
|
276
|
-
|
|
277
243
|
/**
|
|
278
244
|
* Live snapshot stream. Re-emits the latest `get()` shape on every
|
|
279
245
|
* underlying mutation. Destroy the stream to stop watching.
|
|
@@ -318,15 +284,14 @@ const operators = {
|
|
|
318
284
|
wire.on('end', end)
|
|
319
285
|
wire.on('close', end)
|
|
320
286
|
// the channel tears the stream down on client close — that is an end,
|
|
321
|
-
// not a failure
|
|
287
|
+
// not a failure
|
|
322
288
|
wire.on('error', (err) => (err.code === 'CHANNEL_CLOSED' ? end() : out.destroy(err)))
|
|
323
289
|
return out
|
|
324
290
|
},
|
|
325
291
|
|
|
326
292
|
/**
|
|
327
|
-
* Delta subscription over the wire — same contract as the local operator:
|
|
328
|
-
*
|
|
329
|
-
* a full replay. Lossless: server-side the cursor folds under backpressure.
|
|
293
|
+
* Delta subscription over the wire — same contract as the local operator: batches of `{
|
|
294
|
+
* prev, next }` with file fields resolved, `reset` marks a full replay.
|
|
330
295
|
*/
|
|
331
296
|
changes(name, query) {
|
|
332
297
|
const refInfo = this._refInfo(name)
|
|
@@ -358,8 +323,7 @@ const operators = {
|
|
|
358
323
|
}
|
|
359
324
|
pump().catch((err) => {
|
|
360
325
|
if (out.destroyed) return
|
|
361
|
-
//
|
|
362
|
-
// client close — both are ends, not failures (same contract as watch)
|
|
326
|
+
// both are ends, not failures
|
|
363
327
|
if (err.code === 'PREMATURE_CLOSE' || err.code === 'CHANNEL_CLOSED') out.push(null)
|
|
364
328
|
else out.destroy(err)
|
|
365
329
|
})
|
|
@@ -415,6 +379,16 @@ const operators = {
|
|
|
415
379
|
async rotate() {
|
|
416
380
|
const { epoch } = await this.rpc.rotate({ handle: this.id })
|
|
417
381
|
return { epoch }
|
|
382
|
+
},
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* `true` ranks this handle as just updated on the server, `false` takes it off the swarm.
|
|
386
|
+
*
|
|
387
|
+
* @param {boolean} active
|
|
388
|
+
* @returns {Promise<void>}
|
|
389
|
+
*/
|
|
390
|
+
async setActive(active) {
|
|
391
|
+
await this.rpc.setActive({ handle: this.id, active })
|
|
418
392
|
}
|
|
419
393
|
}
|
|
420
394
|
|
|
@@ -435,10 +409,7 @@ export async function restore(me, phrase) {
|
|
|
435
409
|
}
|
|
436
410
|
|
|
437
411
|
/**
|
|
438
|
-
* Per-device `local`-namespace surface on a Client.
|
|
439
|
-
* local ref (e.g. `client.local.settings`) and routes ops over RPC with
|
|
440
|
-
* `local: true`, so they hit the server's per-device store and never
|
|
441
|
-
* replicate. Built-in local refs (identity master/keypair) are not exposed.
|
|
412
|
+
* Per-device `local`-namespace surface on a Client.
|
|
442
413
|
*/
|
|
443
414
|
class LocalRefs {
|
|
444
415
|
/** @param {Client} client */
|
|
@@ -449,7 +420,7 @@ class LocalRefs {
|
|
|
449
420
|
this.store = this
|
|
450
421
|
this._local = true
|
|
451
422
|
const refs = client.spec.meta.local?.refs || {}
|
|
452
|
-
const exposed = Object.fromEntries(Object.entries(refs).filter(([, info]) => !info.
|
|
423
|
+
const exposed = Object.fromEntries(Object.entries(refs).filter(([, info]) => !info.internal))
|
|
453
424
|
Ref.attach(this, exposed)
|
|
454
425
|
}
|
|
455
426
|
|
|
@@ -465,19 +436,21 @@ class LocalRefs {
|
|
|
465
436
|
}
|
|
466
437
|
|
|
467
438
|
/**
|
|
468
|
-
* IPC-side RPC client for cero. Wraps an `hrpc` channel and exposes the
|
|
469
|
-
*
|
|
470
|
-
*
|
|
439
|
+
* IPC-side RPC client for cero. Wraps an `hrpc` channel and exposes the same
|
|
440
|
+
* handle/ref/row API as a local cero instance, transparently routing every operation
|
|
441
|
+
* across the wire.
|
|
471
442
|
*/
|
|
472
443
|
export class Client extends RPCClient {
|
|
473
444
|
/**
|
|
474
445
|
* @param {any} ipc Framed IPC stream (must be writable).
|
|
475
446
|
* @param {Spec} spec Compiled cero spec (schema + rpc + handles).
|
|
447
|
+
* @param {{ operators?: Record<string, any> }} [opts] The operators to bind, instead of the ones the spec carries.
|
|
476
448
|
*/
|
|
477
|
-
constructor(ipc, spec) {
|
|
449
|
+
constructor(ipc, spec, opts = {}) {
|
|
478
450
|
super(ipc, spec)
|
|
479
451
|
if (spec.local?.schema && !spec.local.codec) bindCodec(spec.local)
|
|
480
452
|
Object.assign(this, operators)
|
|
453
|
+
this.operators = operatorsOf(spec, opts.operators)
|
|
481
454
|
this.id = null
|
|
482
455
|
this.deviceId = null
|
|
483
456
|
this.store = this
|
|
@@ -493,10 +466,20 @@ export class Client extends RPCClient {
|
|
|
493
466
|
this._fileToken = fileToken || ''
|
|
494
467
|
this.identity = { id, toPhrase: async () => (await this.rpc.seed({})).phrase || null }
|
|
495
468
|
Ref.attach(this, /** @type {Spec} */ (this.spec).meta.refs)
|
|
496
|
-
bind(this, null)
|
|
469
|
+
bind(this, null, this.operators)
|
|
497
470
|
if (/** @type {Spec} */ (this.spec).meta.local?.refs) this.local = new LocalRefs(this)
|
|
498
471
|
}
|
|
499
472
|
|
|
473
|
+
/** Pause networking and storage on the server. Idempotent. */
|
|
474
|
+
async suspend() {
|
|
475
|
+
await this.rpc.suspend({})
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** Resume a suspended server. Idempotent. */
|
|
479
|
+
async resume() {
|
|
480
|
+
await this.rpc.resume({})
|
|
481
|
+
}
|
|
482
|
+
|
|
500
483
|
/**
|
|
501
484
|
* Create a new child handle of the given type.
|
|
502
485
|
*
|
|
@@ -505,7 +488,7 @@ export class Client extends RPCClient {
|
|
|
505
488
|
* @returns {Promise<Handle>}
|
|
506
489
|
*/
|
|
507
490
|
async _create(type, opts = {}) {
|
|
508
|
-
//
|
|
491
|
+
// routes are functions and cannot cross the wire
|
|
509
492
|
const wire = { ...opts, noAccept: opts.accept === false || undefined }
|
|
510
493
|
const stub = await this.rpc.addHandle({
|
|
511
494
|
ref: type,
|
|
@@ -541,9 +524,9 @@ export class Client extends RPCClient {
|
|
|
541
524
|
}
|
|
542
525
|
|
|
543
526
|
/**
|
|
544
|
-
* Client-side proxy for a remote handle. Exposes the same row-ops surface
|
|
545
|
-
*
|
|
546
|
-
*
|
|
527
|
+
* Client-side proxy for a remote handle. Exposes the same row-ops surface as `Client` but
|
|
528
|
+
* scoped to a single child handle id, and routes every call through the parent's RPC
|
|
529
|
+
* channel.
|
|
547
530
|
*/
|
|
548
531
|
class Handle {
|
|
549
532
|
/**
|
|
@@ -562,7 +545,7 @@ class Handle {
|
|
|
562
545
|
if (!this.spec.codec) bindCodec(this.spec)
|
|
563
546
|
this.store = this
|
|
564
547
|
Ref.attach(this, this.spec.meta.refs)
|
|
565
|
-
bind(this, this.type)
|
|
548
|
+
bind(this, this.type, parent.operators)
|
|
566
549
|
}
|
|
567
550
|
|
|
568
551
|
/** Underlying RPC channel borrowed from the parent. */
|
|
@@ -586,29 +569,26 @@ class Handle {
|
|
|
586
569
|
*
|
|
587
570
|
* @param {any} ipc
|
|
588
571
|
* @param {object} spec
|
|
572
|
+
* @param {{ operators?: Record<string, any> }} [opts]
|
|
589
573
|
* @returns {Promise<Client>}
|
|
590
574
|
*/
|
|
591
|
-
export async function connect(ipc, spec) {
|
|
592
|
-
const client = new Client(ipc, spec)
|
|
575
|
+
export async function connect(ipc, spec, opts) {
|
|
576
|
+
const client = new Client(ipc, spec, opts)
|
|
593
577
|
await client.ready()
|
|
594
578
|
return client
|
|
595
579
|
}
|
|
596
580
|
|
|
597
581
|
/**
|
|
598
|
-
* Symmetric client entry. Mirrors the main `cero`, but `cero(ipc, spec)` connects
|
|
599
|
-
*
|
|
600
|
-
* surface is attached, so `import { cero } from '@cero-base/cero/client'` works
|
|
601
|
-
* just like `import { cero } from '@cero-base/cero'`.
|
|
602
|
-
*
|
|
603
|
-
* Note: `before`/`after`/`peek` are intentionally not exposed here — they hook the
|
|
604
|
-
* local write path / probe a local store, which a client proxy has no notion of.
|
|
582
|
+
* Symmetric client entry. Mirrors the main `cero`, but `cero(ipc, spec)` connects to a
|
|
583
|
+
* server (via `connect`) instead of opening a local store.
|
|
605
584
|
*
|
|
606
585
|
* @param {any} ipc Framed IPC duplex stream.
|
|
607
586
|
* @param {any} spec Built cero spec.
|
|
587
|
+
* @param {{ operators?: Record<string, any> }} [opts]
|
|
608
588
|
* @returns {Promise<Client>}
|
|
609
589
|
*/
|
|
610
|
-
export function cero(ipc, spec) {
|
|
611
|
-
return connect(ipc, spec)
|
|
590
|
+
export function cero(ipc, spec, opts) {
|
|
591
|
+
return connect(ipc, spec, opts)
|
|
612
592
|
}
|
|
613
593
|
cero.connect = connect
|
|
614
594
|
cero.restore = restore
|
|
@@ -617,12 +597,9 @@ cero.put = put
|
|
|
617
597
|
cero.set = set
|
|
618
598
|
cero.get = get
|
|
619
599
|
cero.del = del
|
|
620
|
-
cero.count = count
|
|
621
600
|
cero.watch = watch
|
|
622
601
|
cero.changes = changes
|
|
623
602
|
cero.call = call
|
|
624
603
|
cero.open = open
|
|
625
604
|
cero.rotate = rotate
|
|
626
|
-
cero.bind = bind
|
|
627
|
-
cero.define = define
|
|
628
605
|
cero.schema = schema
|