@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.
Files changed (44) hide show
  1. package/README.md +38 -644
  2. package/package.json +16 -12
  3. package/src/build/index.js +21 -50
  4. package/src/build/internal.js +121 -0
  5. package/src/build/schemas.js +2 -8
  6. package/src/extensions/handle-sync.js +3 -10
  7. package/src/extensions/index.js +6 -0
  8. package/src/extensions/profile-sync.js +0 -5
  9. package/src/handle/index.js +255 -300
  10. package/src/index.js +65 -112
  11. package/src/lib/bluetooth.js +25 -56
  12. package/src/lib/constants.js +0 -15
  13. package/src/lib/operators.js +24 -74
  14. package/src/lib/peek.js +4 -8
  15. package/src/lib/refs.js +46 -0
  16. package/src/lib/spec.js +2 -3
  17. package/src/local/index.js +4 -5
  18. package/src/rpc/client.js +27 -39
  19. package/src/rpc/index.js +3 -3
  20. package/src/rpc/server.js +29 -40
  21. package/types/build/index.d.ts +19 -7
  22. package/types/build/internal.d.ts +78 -0
  23. package/types/build/schemas.d.ts +3 -3
  24. package/types/extensions/handle-sync.d.ts +4 -9
  25. package/types/extensions/index.d.ts +24 -2
  26. package/types/extensions/profile-sync.d.ts +2 -6
  27. package/types/handle/index.d.ts +218 -254
  28. package/types/index.d.ts +78 -102
  29. package/types/lib/bluetooth.d.ts +24 -46
  30. package/types/lib/constants.d.ts +5 -16
  31. package/types/lib/operators.d.ts +49 -77
  32. package/types/lib/peek.d.ts +3 -4
  33. package/types/lib/refs.d.ts +36 -0
  34. package/types/lib/spec.d.ts +5 -1
  35. package/types/local/index.d.ts +24 -23
  36. package/types/rpc/client.d.ts +107 -127
  37. package/types/rpc/index.d.ts +7 -2
  38. package/types/rpc/server.d.ts +62 -76
  39. package/src/build/builtins.js +0 -174
  40. package/src/lib/internal.js +0 -9
  41. package/src/lib/utils.js +0 -67
  42. package/types/build/builtins.d.ts +0 -100
  43. package/types/lib/internal.d.ts +0 -24
  44. package/types/lib/utils.d.ts +0 -55
@@ -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 './utils.js'
6
+ import { onAbort } from '@cero-base/core/utils'
7
7
 
8
8
  /**
9
- * @typedef {import('./utils.js').Ref} Ref
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`. The `files` builtin is special:
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
- * non-blocking (observe only). Returns an unsubscribe fn; pass `{ signal }`
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
- // get/watch on a handle-kind ref list its rows from the `handles` collection
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. For
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
- // Tie a fresh watch stream to its handle's lifecycle (destroyed on close) and
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, so it doesn't linger on a
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
- * every underlying mutation. Tied to `ref.handle`'s lifecycle: closing the
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
- * full snapshots — lossless under backpressure, self-contained (the first
252
- * batch, and any batch after a view swap, replays current state as inserts
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
- // Snapshots are idempotent — under a slow consumer hold only the NEWEST one
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
- * current member and announced through the log — members removed before the
360
- * rotation cannot decrypt anything written after it. Requires the remove
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)`. `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.
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
- * that names a child-handle type binds on every handle of that type. Call once
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
- // Lives outside operators.js so the storage deps stay off the RPC client's
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
- * initialised cero identity (i.e. a stored master seed). Opens the local store
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
- // Construct first (cheap), ready inside the try a corrupt dir that throws in
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 })
@@ -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
- * `@cero-base/core/schema`, so cero apps can describe their tables without
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'
@@ -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 { attachRefs } from '../lib/utils.js'
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
- * device keypair and any per-handle keypairs. Wraps a hyperbee-backed
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
- attachRefs(this, this.store.refs)
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 { attachRefs } from '../lib/utils.js'
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} [builtin]
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
- // Mixin applied to Client, Handle and LocalRefs so they expose the same
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
- * `{ id, type, size, url }` objects. The `files` builtin's own `id` is the
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?.builtin && info?.verb === 'file') {
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 (bare-rpc ≥1.3.2 errors every in-flight op on teardown)
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
- * batches of `{ prev, next }` with file fields resolved, `reset` marks
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
- // the server tears the stream down on handle close, and the channel on
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. Exposes each app-defined
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.builtin))
452
- attachRefs(this, exposed)
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
- * same handle/ref/row API as a local cero instance, transparently
469
- * routing every operation across the wire.
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
- attachRefs(this, /** @type {Spec} */ (this.spec).meta.refs)
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
- // `routes` are functions and can't cross the wire; role/accept now do.
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
- * as `Client` but scoped to a single child handle id, and routes every
545
- * call through the parent's RPC channel.
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
- attachRefs(this, this.spec.meta.refs)
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
- * to a server (via `connect`) instead of opening a local store. The same operator
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
- * `serve()`/`connect()` helpers so consumers can spin up either side
4
- * of the cero IPC bridge from a single import.
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'