@cero-base/cero 1.18.2 → 2.0.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 -644
- package/package.json +16 -12
- package/src/build/index.js +21 -50
- package/src/build/internal.js +121 -0
- package/src/build/schemas.js +2 -8
- package/src/extensions/handle-sync.js +3 -10
- package/src/extensions/index.js +6 -0
- package/src/extensions/profile-sync.js +0 -5
- package/src/handle/index.js +255 -300
- package/src/index.js +65 -112
- package/src/lib/bluetooth.js +25 -56
- package/src/lib/constants.js +0 -15
- package/src/lib/operators.js +24 -74
- package/src/lib/peek.js +4 -8
- package/src/lib/refs.js +46 -0
- package/src/lib/spec.js +2 -3
- package/src/local/index.js +4 -5
- package/src/rpc/client.js +27 -39
- package/src/rpc/index.js +3 -3
- package/src/rpc/server.js +29 -40
- package/types/build/index.d.ts +19 -7
- package/types/build/internal.d.ts +78 -0
- package/types/build/schemas.d.ts +3 -3
- package/types/extensions/handle-sync.d.ts +4 -9
- package/types/extensions/index.d.ts +24 -2
- package/types/extensions/profile-sync.d.ts +2 -6
- package/types/handle/index.d.ts +218 -254
- package/types/index.d.ts +78 -102
- package/types/lib/bluetooth.d.ts +24 -46
- package/types/lib/constants.d.ts +5 -16
- package/types/lib/operators.d.ts +49 -77
- package/types/lib/peek.d.ts +3 -4
- package/types/lib/refs.d.ts +36 -0
- package/types/lib/spec.d.ts +5 -1
- package/types/local/index.d.ts +24 -23
- package/types/rpc/client.d.ts +107 -127
- package/types/rpc/index.d.ts +7 -2
- package/types/rpc/server.d.ts +62 -76
- package/src/build/builtins.js +0 -174
- package/src/lib/internal.js +0 -9
- package/src/lib/utils.js +0 -67
- package/types/build/builtins.d.ts +0 -100
- package/types/lib/internal.d.ts +0 -24
- package/types/lib/utils.d.ts +0 -55
package/src/lib/operators.js
CHANGED
|
@@ -3,10 +3,10 @@ import b4a from 'b4a'
|
|
|
3
3
|
|
|
4
4
|
import { encodeId, decodeId } from '@cero-base/core/blobs/codec'
|
|
5
5
|
import { CeroError } from '@cero-base/core/errors'
|
|
6
|
-
import { onAbort } from '
|
|
6
|
+
import { onAbort } from '@cero-base/core/utils'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* @typedef {import('./
|
|
9
|
+
* @typedef {import('./refs.js').Ref} Ref
|
|
10
10
|
* @typedef {import('../handle/index.js').CeroHandle} CeroHandle
|
|
11
11
|
* @typedef {{ data: any }} SingleResult
|
|
12
12
|
* @typedef {{ data: any[], total: number, size: number }} ListResult
|
|
@@ -30,9 +30,7 @@ export function resolveFile(handle, id, name) {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/**
|
|
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.
|
|
33
|
+
* Insert (or overwrite by id) a row on `ref`.
|
|
36
34
|
*
|
|
37
35
|
* @param {Ref} ref
|
|
38
36
|
* @param {Record<string, any>} row
|
|
@@ -55,9 +53,7 @@ async function putFile(ref, row) {
|
|
|
55
53
|
}
|
|
56
54
|
|
|
57
55
|
/**
|
|
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).
|
|
56
|
+
* Upsert a row on `ref` — merges with the existing row and preserves `createdAt`.
|
|
61
57
|
*
|
|
62
58
|
* @param {Ref} ref
|
|
63
59
|
* @param {Record<string, any>} row
|
|
@@ -101,13 +97,10 @@ export function call(ref, d) {
|
|
|
101
97
|
return ref.handle.store.call(ref.name, d)
|
|
102
98
|
}
|
|
103
99
|
|
|
104
|
-
// Write ops per ref kind, for `before`/`after` subscriptions.
|
|
105
100
|
const WRITES = { single: ['set'], collection: ['put', 'set', 'del'] }
|
|
106
101
|
|
|
107
102
|
/**
|
|
108
|
-
* Intercept writes to `ref` before they commit — `fn(ctx)` runs in-path
|
|
109
|
-
* (awaited). Return `false` to cancel the write, or mutate `ctx.row`.
|
|
110
|
-
* Returns an unsubscribe fn; pass `{ signal }` to unsubscribe on abort.
|
|
103
|
+
* Intercept writes to `ref` before they commit — `fn(ctx)` runs in-path (awaited).
|
|
111
104
|
*
|
|
112
105
|
* @param {Ref} ref
|
|
113
106
|
* @param {(ctx: { op: string, name: string, row: any }) => any} fn
|
|
@@ -130,9 +123,8 @@ export function before(ref, fn, opts) {
|
|
|
130
123
|
}
|
|
131
124
|
|
|
132
125
|
/**
|
|
133
|
-
* Subscribe to writes on `ref` — fires after each committed write,
|
|
134
|
-
*
|
|
135
|
-
* to unsubscribe on abort.
|
|
126
|
+
* Subscribe to writes on `ref` — fires after each committed write, non-blocking (observe
|
|
127
|
+
* only).
|
|
136
128
|
*
|
|
137
129
|
* @param {Ref} ref
|
|
138
130
|
* @param {(ctx: { op: string, name: string, row: any }) => void} fn
|
|
@@ -153,10 +145,7 @@ export function after(ref, fn, opts) {
|
|
|
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 }>}
|
|
@@ -370,16 +334,8 @@ export function rotate(handle) {
|
|
|
370
334
|
return handle.store.rotate()
|
|
371
335
|
}
|
|
372
336
|
|
|
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
337
|
const registry = {}
|
|
381
338
|
|
|
382
|
-
// Curry `handle` as arg 0 of every function in `fns`, under `handle[ns]`.
|
|
383
339
|
function attach(handle, ns, fns) {
|
|
384
340
|
const bound = {}
|
|
385
341
|
for (const key of Object.keys(fns)) {
|
|
@@ -390,12 +346,7 @@ function attach(handle, ns, fns) {
|
|
|
390
346
|
|
|
391
347
|
/**
|
|
392
348
|
* Put custom operators on `handle`, currying it as their first argument so
|
|
393
|
-
* `handle.ns.fn(args)` calls `fn(handle, args)`.
|
|
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.
|
|
349
|
+
* `handle.ns.fn(args)` calls `fn(handle, args)`.
|
|
399
350
|
*
|
|
400
351
|
* @param {any} handle
|
|
401
352
|
* @param {Record<string, any> | string | null} arg
|
|
@@ -418,9 +369,8 @@ export function bind(handle, arg) {
|
|
|
418
369
|
}
|
|
419
370
|
|
|
420
371
|
/**
|
|
421
|
-
* Register custom operators by scope. A bare key binds on the root handle; a key
|
|
422
|
-
*
|
|
423
|
-
* at startup, before `cero()` / `connect()`, in both processes.
|
|
372
|
+
* Register custom operators by scope. A bare key binds on the root handle; a key that
|
|
373
|
+
* names a child-handle type binds on every handle of that type.
|
|
424
374
|
*
|
|
425
375
|
* @param {Record<string, any>} map
|
|
426
376
|
*/
|
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
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { CeroError } from '@cero-base/core/errors'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {'collection' | 'single' | 'action' | 'handle'} RefKind
|
|
5
|
+
* @typedef {{ kind?: string, schema?: string }} RefInfo
|
|
6
|
+
* Shape of the entries in `meta.refs` — describes a single ref name.
|
|
7
|
+
* `kind` is one of {@link RefKind}, kept as `string` since it originates
|
|
8
|
+
* from a generated spec.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Typed pointer to a single ref (table or handle slot) on a `Handle` or `Local`.
|
|
13
|
+
*/
|
|
14
|
+
export class Ref {
|
|
15
|
+
/**
|
|
16
|
+
* @param {any} handle Owner — a `Handle` (or `Local`) the ref lives on.
|
|
17
|
+
* @param {string} name Ref name as declared in the schema.
|
|
18
|
+
* @param {string} kind Ref kind: `'collection'`, `'single'`, `'action'`, or `'handle'`.
|
|
19
|
+
* @param {string | null} [schema] Fully-qualified schema id, if any.
|
|
20
|
+
*/
|
|
21
|
+
constructor(handle, name, kind, schema = null) {
|
|
22
|
+
this.handle = handle
|
|
23
|
+
this.name = name
|
|
24
|
+
this.kind = kind
|
|
25
|
+
this.schema = schema
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Attach a `Ref` property to `target` for every entry in `refs`, so callers
|
|
30
|
+
* write `handle.someRef` instead of looking refs up by name.
|
|
31
|
+
*
|
|
32
|
+
* @param {any} target
|
|
33
|
+
* @param {Record<string, RefInfo>} refs
|
|
34
|
+
*/
|
|
35
|
+
static attach(target, refs) {
|
|
36
|
+
for (const [name, info] of Object.entries(refs || {})) {
|
|
37
|
+
// a ref named like a reserved member must fail loud, not overwrite it
|
|
38
|
+
if (name in target) {
|
|
39
|
+
throw CeroError.INVALID(
|
|
40
|
+
`schema ref '${name}' collides with a reserved ${target.constructor?.name || 'handle'} member — rename it`
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
target[name] = new Ref(target, name, info.kind, info.schema)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
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
|
@@ -3,7 +3,7 @@ import ReadyResource from 'ready-resource'
|
|
|
3
3
|
import { Storage } from '@cero-base/core/storage'
|
|
4
4
|
import { CeroError } from '@cero-base/core/errors'
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { Ref } from '../lib/refs.js'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* @typedef {object} LocalOpts
|
|
@@ -13,9 +13,8 @@ import { attachRefs } from '../lib/utils.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
|
/**
|
|
@@ -43,7 +42,7 @@ export class Local extends ReadyResource {
|
|
|
43
42
|
|
|
44
43
|
async _open() {
|
|
45
44
|
await this.store.ready()
|
|
46
|
-
|
|
45
|
+
Ref.attach(this, this.store.refs)
|
|
47
46
|
}
|
|
48
47
|
|
|
49
48
|
async _close() {
|
package/src/rpc/client.js
CHANGED
|
@@ -4,7 +4,7 @@ import c from 'compact-encoding'
|
|
|
4
4
|
import z32 from 'z32'
|
|
5
5
|
import { decodeId } from '@cero-base/core/blobs/codec'
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { Ref } from '../lib/refs.js'
|
|
8
8
|
import {
|
|
9
9
|
put,
|
|
10
10
|
set,
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
del,
|
|
13
13
|
count,
|
|
14
14
|
watch,
|
|
15
|
+
changes,
|
|
15
16
|
call,
|
|
16
17
|
open,
|
|
17
18
|
rotate,
|
|
@@ -20,7 +21,7 @@ import {
|
|
|
20
21
|
} from '../lib/operators.js'
|
|
21
22
|
import { t, schema } from '../lib/spec.js'
|
|
22
23
|
|
|
23
|
-
export { put, set, get, del, count, watch, call, open, rotate, bind, define, t, schema }
|
|
24
|
+
export { put, set, get, del, count, watch, changes, call, open, rotate, bind, define, t, schema }
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* @typedef {import('@cero-base/core/rpc').RPCClient} BaseRPCClient
|
|
@@ -29,7 +30,7 @@ export { put, set, get, del, count, watch, call, open, rotate, bind, define, t,
|
|
|
29
30
|
* @property {'single'|'collection'|'action'|'handle'} [kind]
|
|
30
31
|
* @property {string} [schema]
|
|
31
32
|
* @property {string} [type]
|
|
32
|
-
* @property {boolean} [
|
|
33
|
+
* @property {boolean} [internal]
|
|
33
34
|
*
|
|
34
35
|
* @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).
|
|
35
36
|
*
|
|
@@ -47,7 +48,6 @@ export { put, set, get, del, count, watch, call, open, rotate, bind, define, t,
|
|
|
47
48
|
* @property {string|null} name
|
|
48
49
|
*/
|
|
49
50
|
|
|
50
|
-
// Compact-encoding blobId struct matching hypercore-blob-server's wire format.
|
|
51
51
|
const blobIdEnc = {
|
|
52
52
|
preencode(state, b) {
|
|
53
53
|
c.uint.preencode(state, b.blockOffset)
|
|
@@ -71,9 +71,7 @@ const blobIdEnc = {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
//
|
|
75
|
-
// row-ops surface as a local cero handle but routed over the wire. `_local`
|
|
76
|
-
// selects the per-device store + JSON codec; main refs use the schema codec.
|
|
74
|
+
// the same row-ops surface as a local handle, routed over the wire
|
|
77
75
|
const operators = {
|
|
78
76
|
_local: false,
|
|
79
77
|
|
|
@@ -112,9 +110,8 @@ const operators = {
|
|
|
112
110
|
},
|
|
113
111
|
|
|
114
112
|
/**
|
|
115
|
-
* Augment a decoded row (or array of rows) to resolve file-typed fields to
|
|
116
|
-
*
|
|
117
|
-
* file id; other refs declare file fields in `meta.refs[name].files`.
|
|
113
|
+
* Augment a decoded row (or array of rows) to resolve file-typed fields to `{ id, type,
|
|
114
|
+
* size, url }` objects.
|
|
118
115
|
*
|
|
119
116
|
* @param {string} name
|
|
120
117
|
* @param {any} data
|
|
@@ -129,7 +126,7 @@ const operators = {
|
|
|
129
126
|
|
|
130
127
|
_resolveRow(name, info, row) {
|
|
131
128
|
if (!row || typeof row !== 'object') return row
|
|
132
|
-
if (info?.
|
|
129
|
+
if (info?.internal && info?.verb === 'file') {
|
|
133
130
|
if (!row.id) return row
|
|
134
131
|
try {
|
|
135
132
|
const { type, blobId } = decodeId(row.id)
|
|
@@ -317,15 +314,14 @@ const operators = {
|
|
|
317
314
|
wire.on('end', end)
|
|
318
315
|
wire.on('close', end)
|
|
319
316
|
// the channel tears the stream down on client close — that is an end,
|
|
320
|
-
// not a failure
|
|
317
|
+
// not a failure
|
|
321
318
|
wire.on('error', (err) => (err.code === 'CHANNEL_CLOSED' ? end() : out.destroy(err)))
|
|
322
319
|
return out
|
|
323
320
|
},
|
|
324
321
|
|
|
325
322
|
/**
|
|
326
|
-
* Delta subscription over the wire — same contract as the local operator:
|
|
327
|
-
*
|
|
328
|
-
* a full replay. Lossless: server-side the cursor folds under backpressure.
|
|
323
|
+
* Delta subscription over the wire — same contract as the local operator: batches of `{
|
|
324
|
+
* prev, next }` with file fields resolved, `reset` marks a full replay.
|
|
329
325
|
*/
|
|
330
326
|
changes(name, query) {
|
|
331
327
|
const refInfo = this._refInfo(name)
|
|
@@ -357,8 +353,7 @@ const operators = {
|
|
|
357
353
|
}
|
|
358
354
|
pump().catch((err) => {
|
|
359
355
|
if (out.destroyed) return
|
|
360
|
-
//
|
|
361
|
-
// client close — both are ends, not failures (same contract as watch)
|
|
356
|
+
// both are ends, not failures
|
|
362
357
|
if (err.code === 'PREMATURE_CLOSE' || err.code === 'CHANNEL_CLOSED') out.push(null)
|
|
363
358
|
else out.destroy(err)
|
|
364
359
|
})
|
|
@@ -434,10 +429,7 @@ export async function restore(me, phrase) {
|
|
|
434
429
|
}
|
|
435
430
|
|
|
436
431
|
/**
|
|
437
|
-
* Per-device `local`-namespace surface on a Client.
|
|
438
|
-
* local ref (e.g. `client.local.settings`) and routes ops over RPC with
|
|
439
|
-
* `local: true`, so they hit the server's per-device store and never
|
|
440
|
-
* replicate. Built-in local refs (identity master/keypair) are not exposed.
|
|
432
|
+
* Per-device `local`-namespace surface on a Client.
|
|
441
433
|
*/
|
|
442
434
|
class LocalRefs {
|
|
443
435
|
/** @param {Client} client */
|
|
@@ -448,8 +440,8 @@ class LocalRefs {
|
|
|
448
440
|
this.store = this
|
|
449
441
|
this._local = true
|
|
450
442
|
const refs = client.spec.meta.local?.refs || {}
|
|
451
|
-
const exposed = Object.fromEntries(Object.entries(refs).filter(([, info]) => !info.
|
|
452
|
-
|
|
443
|
+
const exposed = Object.fromEntries(Object.entries(refs).filter(([, info]) => !info.internal))
|
|
444
|
+
Ref.attach(this, exposed)
|
|
453
445
|
}
|
|
454
446
|
|
|
455
447
|
/** Underlying RPC channel borrowed from the parent. */
|
|
@@ -464,9 +456,9 @@ class LocalRefs {
|
|
|
464
456
|
}
|
|
465
457
|
|
|
466
458
|
/**
|
|
467
|
-
* IPC-side RPC client for cero. Wraps an `hrpc` channel and exposes the
|
|
468
|
-
*
|
|
469
|
-
*
|
|
459
|
+
* IPC-side RPC client for cero. Wraps an `hrpc` channel and exposes the same
|
|
460
|
+
* handle/ref/row API as a local cero instance, transparently routing every operation
|
|
461
|
+
* across the wire.
|
|
470
462
|
*/
|
|
471
463
|
export class Client extends RPCClient {
|
|
472
464
|
/**
|
|
@@ -491,7 +483,7 @@ export class Client extends RPCClient {
|
|
|
491
483
|
this._fileBase = fileBase || ''
|
|
492
484
|
this._fileToken = fileToken || ''
|
|
493
485
|
this.identity = { id, toPhrase: async () => (await this.rpc.seed({})).phrase || null }
|
|
494
|
-
|
|
486
|
+
Ref.attach(this, /** @type {Spec} */ (this.spec).meta.refs)
|
|
495
487
|
bind(this, null)
|
|
496
488
|
if (/** @type {Spec} */ (this.spec).meta.local?.refs) this.local = new LocalRefs(this)
|
|
497
489
|
}
|
|
@@ -504,7 +496,7 @@ export class Client extends RPCClient {
|
|
|
504
496
|
* @returns {Promise<Handle>}
|
|
505
497
|
*/
|
|
506
498
|
async _create(type, opts = {}) {
|
|
507
|
-
//
|
|
499
|
+
// routes are functions and cannot cross the wire
|
|
508
500
|
const wire = { ...opts, noAccept: opts.accept === false || undefined }
|
|
509
501
|
const stub = await this.rpc.addHandle({
|
|
510
502
|
ref: type,
|
|
@@ -540,9 +532,9 @@ export class Client extends RPCClient {
|
|
|
540
532
|
}
|
|
541
533
|
|
|
542
534
|
/**
|
|
543
|
-
* Client-side proxy for a remote handle. Exposes the same row-ops surface
|
|
544
|
-
*
|
|
545
|
-
*
|
|
535
|
+
* Client-side proxy for a remote handle. Exposes the same row-ops surface as `Client` but
|
|
536
|
+
* scoped to a single child handle id, and routes every call through the parent's RPC
|
|
537
|
+
* channel.
|
|
546
538
|
*/
|
|
547
539
|
class Handle {
|
|
548
540
|
/**
|
|
@@ -560,7 +552,7 @@ class Handle {
|
|
|
560
552
|
this.spec = /** @type {Spec} */ (parent.spec).handles[type]
|
|
561
553
|
if (!this.spec.codec) bindCodec(this.spec)
|
|
562
554
|
this.store = this
|
|
563
|
-
|
|
555
|
+
Ref.attach(this, this.spec.meta.refs)
|
|
564
556
|
bind(this, this.type)
|
|
565
557
|
}
|
|
566
558
|
|
|
@@ -594,13 +586,8 @@ export async function connect(ipc, spec) {
|
|
|
594
586
|
}
|
|
595
587
|
|
|
596
588
|
/**
|
|
597
|
-
* Symmetric client entry. Mirrors the main `cero`, but `cero(ipc, spec)` connects
|
|
598
|
-
*
|
|
599
|
-
* surface is attached, so `import { cero } from '@cero-base/cero/client'` works
|
|
600
|
-
* just like `import { cero } from '@cero-base/cero'`.
|
|
601
|
-
*
|
|
602
|
-
* Note: `before`/`after`/`peek` are intentionally not exposed here — they hook the
|
|
603
|
-
* local write path / probe a local store, which a client proxy has no notion of.
|
|
589
|
+
* Symmetric client entry. Mirrors the main `cero`, but `cero(ipc, spec)` connects to a
|
|
590
|
+
* server (via `connect`) instead of opening a local store.
|
|
604
591
|
*
|
|
605
592
|
* @param {any} ipc Framed IPC duplex stream.
|
|
606
593
|
* @param {any} spec Built cero spec.
|
|
@@ -618,6 +605,7 @@ cero.get = get
|
|
|
618
605
|
cero.del = del
|
|
619
606
|
cero.count = count
|
|
620
607
|
cero.watch = watch
|
|
608
|
+
cero.changes = changes
|
|
621
609
|
cero.call = call
|
|
622
610
|
cero.open = open
|
|
623
611
|
cero.rotate = rotate
|
package/src/rpc/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* RPC barrel — re-exports the `Server`/`Client` classes and their
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* RPC barrel — re-exports the `Server`/`Client` classes and their `serve()`/`connect()`
|
|
3
|
+
* helpers so consumers can spin up either side of the cero IPC bridge from a single
|
|
4
|
+
* import.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
export { Server, serve } from './server.js'
|