@cero-base/cero 1.19.0 → 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 (40) hide show
  1. package/README.md +38 -643
  2. package/package.json +9 -6
  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 +241 -266
  10. package/src/index.js +20 -51
  11. package/src/lib/bluetooth.js +25 -56
  12. package/src/lib/constants.js +0 -15
  13. package/src/lib/operators.js +22 -72
  14. package/src/lib/peek.js +4 -8
  15. package/src/lib/refs.js +2 -5
  16. package/src/lib/spec.js +2 -3
  17. package/src/local/index.js +2 -3
  18. package/src/rpc/client.js +20 -34
  19. package/src/rpc/index.js +3 -3
  20. package/src/rpc/server.js +23 -35
  21. package/types/build/index.d.ts +1 -9
  22. package/types/build/{builtins.d.ts → internal.d.ts} +20 -38
  23. package/types/extensions/handle-sync.d.ts +2 -7
  24. package/types/extensions/index.d.ts +22 -0
  25. package/types/extensions/profile-sync.d.ts +0 -4
  26. package/types/handle/index.d.ts +83 -101
  27. package/types/index.d.ts +4 -8
  28. package/types/lib/bluetooth.d.ts +8 -32
  29. package/types/lib/constants.d.ts +0 -11
  30. package/types/lib/operators.d.ts +18 -46
  31. package/types/lib/peek.d.ts +2 -3
  32. package/types/lib/refs.d.ts +1 -3
  33. package/types/lib/spec.d.ts +2 -3
  34. package/types/local/index.d.ts +2 -3
  35. package/types/rpc/client.d.ts +10 -18
  36. package/types/rpc/index.d.ts +3 -3
  37. package/types/rpc/server.d.ts +6 -9
  38. package/src/build/builtins.js +0 -174
  39. package/src/lib/internal.js +0 -9
  40. package/types/lib/internal.d.ts +0 -24
package/src/rpc/client.js CHANGED
@@ -30,7 +30,7 @@ export { put, set, get, del, count, watch, changes, call, open, rotate, bind, de
30
30
  * @property {'single'|'collection'|'action'|'handle'} [kind]
31
31
  * @property {string} [schema]
32
32
  * @property {string} [type]
33
- * @property {boolean} [builtin]
33
+ * @property {boolean} [internal]
34
34
  *
35
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).
36
36
  *
@@ -48,7 +48,6 @@ export { put, set, get, del, count, watch, changes, call, open, rotate, bind, de
48
48
  * @property {string|null} name
49
49
  */
50
50
 
51
- // Compact-encoding blobId struct matching hypercore-blob-server's wire format.
52
51
  const blobIdEnc = {
53
52
  preencode(state, b) {
54
53
  c.uint.preencode(state, b.blockOffset)
@@ -72,9 +71,7 @@ const blobIdEnc = {
72
71
  }
73
72
  }
74
73
 
75
- // Mixin applied to Client, Handle and LocalRefs so they expose the same
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.
74
+ // the same row-ops surface as a local handle, routed over the wire
78
75
  const operators = {
79
76
  _local: false,
80
77
 
@@ -113,9 +110,8 @@ const operators = {
113
110
  },
114
111
 
115
112
  /**
116
- * Augment a decoded row (or array of rows) to resolve file-typed fields to
117
- * `{ id, type, size, url }` objects. The `files` builtin's own `id` is the
118
- * 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.
119
115
  *
120
116
  * @param {string} name
121
117
  * @param {any} data
@@ -130,7 +126,7 @@ const operators = {
130
126
 
131
127
  _resolveRow(name, info, row) {
132
128
  if (!row || typeof row !== 'object') return row
133
- if (info?.builtin && info?.verb === 'file') {
129
+ if (info?.internal && info?.verb === 'file') {
134
130
  if (!row.id) return row
135
131
  try {
136
132
  const { type, blobId } = decodeId(row.id)
@@ -318,15 +314,14 @@ const operators = {
318
314
  wire.on('end', end)
319
315
  wire.on('close', end)
320
316
  // the channel tears the stream down on client close — that is an end,
321
- // not a failure (bare-rpc ≥1.3.2 errors every in-flight op on teardown)
317
+ // not a failure
322
318
  wire.on('error', (err) => (err.code === 'CHANNEL_CLOSED' ? end() : out.destroy(err)))
323
319
  return out
324
320
  },
325
321
 
326
322
  /**
327
- * Delta subscription over the wire — same contract as the local operator:
328
- * batches of `{ prev, next }` with file fields resolved, `reset` marks
329
- * 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.
330
325
  */
331
326
  changes(name, query) {
332
327
  const refInfo = this._refInfo(name)
@@ -358,8 +353,7 @@ const operators = {
358
353
  }
359
354
  pump().catch((err) => {
360
355
  if (out.destroyed) return
361
- // the server tears the stream down on handle close, and the channel on
362
- // client close — both are ends, not failures (same contract as watch)
356
+ // both are ends, not failures
363
357
  if (err.code === 'PREMATURE_CLOSE' || err.code === 'CHANNEL_CLOSED') out.push(null)
364
358
  else out.destroy(err)
365
359
  })
@@ -435,10 +429,7 @@ export async function restore(me, phrase) {
435
429
  }
436
430
 
437
431
  /**
438
- * Per-device `local`-namespace surface on a Client. Exposes each app-defined
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.
432
+ * Per-device `local`-namespace surface on a Client.
442
433
  */
443
434
  class LocalRefs {
444
435
  /** @param {Client} client */
@@ -449,7 +440,7 @@ class LocalRefs {
449
440
  this.store = this
450
441
  this._local = true
451
442
  const refs = client.spec.meta.local?.refs || {}
452
- const exposed = Object.fromEntries(Object.entries(refs).filter(([, info]) => !info.builtin))
443
+ const exposed = Object.fromEntries(Object.entries(refs).filter(([, info]) => !info.internal))
453
444
  Ref.attach(this, exposed)
454
445
  }
455
446
 
@@ -465,9 +456,9 @@ class LocalRefs {
465
456
  }
466
457
 
467
458
  /**
468
- * IPC-side RPC client for cero. Wraps an `hrpc` channel and exposes the
469
- * same handle/ref/row API as a local cero instance, transparently
470
- * 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.
471
462
  */
472
463
  export class Client extends RPCClient {
473
464
  /**
@@ -505,7 +496,7 @@ export class Client extends RPCClient {
505
496
  * @returns {Promise<Handle>}
506
497
  */
507
498
  async _create(type, opts = {}) {
508
- // `routes` are functions and can't cross the wire; role/accept now do.
499
+ // routes are functions and cannot cross the wire
509
500
  const wire = { ...opts, noAccept: opts.accept === false || undefined }
510
501
  const stub = await this.rpc.addHandle({
511
502
  ref: type,
@@ -541,9 +532,9 @@ export class Client extends RPCClient {
541
532
  }
542
533
 
543
534
  /**
544
- * Client-side proxy for a remote handle. Exposes the same row-ops surface
545
- * as `Client` but scoped to a single child handle id, and routes every
546
- * 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.
547
538
  */
548
539
  class Handle {
549
540
  /**
@@ -595,13 +586,8 @@ export async function connect(ipc, spec) {
595
586
  }
596
587
 
597
588
  /**
598
- * Symmetric client entry. Mirrors the main `cero`, but `cero(ipc, spec)` connects
599
- * to a server (via `connect`) instead of opening a local store. The same operator
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.
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.
605
591
  *
606
592
  * @param {any} ipc Framed IPC duplex stream.
607
593
  * @param {any} spec Built cero spec.
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'
package/src/rpc/server.js CHANGED
@@ -27,10 +27,9 @@ import { put, set, get, del, count, watch, changes, call } from '../lib/operator
27
27
  */
28
28
 
29
29
  /**
30
- * IPC-side RPC server for cero. Bridges an `hrpc` channel to a live
31
- * `Handle` tree: lazy-initializes the root via `cero()` on the first
32
- * `init` call, then exposes data ops, pairing, and handle lifecycle
33
- * over the wire using the spec-bound codec.
30
+ * IPC-side RPC server for cero. Bridges an `hrpc` channel to a live `Handle` tree:
31
+ * lazy-initializes the root via `cero()` on the first `init` call, then exposes data ops,
32
+ * pairing, and handle lifecycle.
34
33
  */
35
34
  export class Server extends RPCServer {
36
35
  /**
@@ -52,14 +51,6 @@ export class Server extends RPCServer {
52
51
  this._wireInit()
53
52
  }
54
53
 
55
- /** End every watch stream bound to a handle (e.g. when it closes or leaves). */
56
- _endWatches(handle) {
57
- const set = this._watchStreams.get(handle)
58
- if (!set) return
59
- for (const s of set) s.destroy()
60
- this._watchStreams.delete(handle)
61
- }
62
-
63
54
  /**
64
55
  * Root cero id (null until `init` has run).
65
56
  *
@@ -87,6 +78,14 @@ export class Server extends RPCServer {
87
78
  await super._close()
88
79
  }
89
80
 
81
+ /** End every watch stream bound to a handle (e.g. when it closes or leaves). */
82
+ _endWatches(handle) {
83
+ const set = this._watchStreams.get(handle)
84
+ if (!set) return
85
+ for (const s of set) s.destroy()
86
+ this._watchStreams.delete(handle)
87
+ }
88
+
90
89
  /** Wire the `init` handler that lazily constructs the root cero handle. */
91
90
  _wireInit() {
92
91
  this.rpc.onInit(async () => {
@@ -147,7 +146,7 @@ export class Server extends RPCServer {
147
146
  r.kind === 'single'
148
147
  ? codec.encodeRow(r.schema, result.data)
149
148
  : codec.encodeRows(r.schema, result.data)
150
- // total is int on the wire: -1 encodes "skipped" (null), see T2.1 lazy total
149
+ // total is int on the wire: -1 encodes null
151
150
  return { data, total: result.total ?? -1, size: result.size ?? 0 }
152
151
  })
153
152
 
@@ -175,17 +174,13 @@ export class Server extends RPCServer {
175
174
  ;({ ref: r, codec } = this._refOf(handle, ref, local))
176
175
  live = watch(r, codec.decodeQuery(query))
177
176
  } catch (err) {
178
- // Forward the error to the client over the wire by destroying the
179
- // underlying response stream with it (emits a CLOSE|ERROR frame). The
180
- // local RPCStream's 'error' is swallowed so it doesn't crash the server.
177
+ // forward the error over the wire by destroying the response stream with it
181
178
  stream.once('error', () => {})
182
179
  stream.writeStream.destroy(err)
183
180
  stream.destroy()
184
181
  return
185
182
  }
186
- // keep-latest under IPC backpressure: snapshots are idempotent, so an
187
- // un-drained wire holds one pending snapshot (newest wins) instead of
188
- // queueing every intermediate result set — encode only what ships
183
+ // keep-latest under backpressure: snapshots are idempotent
189
184
  let pending = null
190
185
  let blocked = false
191
186
  const onData = (snap) => {
@@ -212,8 +207,7 @@ export class Server extends RPCServer {
212
207
  if (!set) this._watchStreams.set(handle, (set = new Set()))
213
208
  set.add(stream)
214
209
  live.on('data', onData)
215
- // channel teardown destroys the stream with CHANNEL_CLOSED 'close'
216
- // below does the cleanup; the error itself must not crash the server
210
+ // channel teardown destroys the stream with CHANNEL_CLOSED; 'close' cleans up
217
211
  stream.on('error', safetyCatch)
218
212
  stream.on('close', () => {
219
213
  live.off('data', onData)
@@ -237,8 +231,7 @@ export class Server extends RPCServer {
237
231
  let set = this._watchStreams.get(handle)
238
232
  if (!set) this._watchStreams.set(handle, (set = new Set()))
239
233
  set.add(stream)
240
- // no keep-latest: delta batches are not idempotent hold the iteration
241
- // on wire backpressure instead; the cursor folds everything missed
234
+ // deltas are not idempotent: hold the iteration on backpressure instead
242
235
  const pump = async () => {
243
236
  for await (const batch of live) {
244
237
  const ok = stream.write({
@@ -282,13 +275,10 @@ export class Server extends RPCServer {
282
275
  return { ok }
283
276
  })
284
277
 
285
- // specs built before rotation have no rotate command
286
- if (typeof this.rpc.onRotate === 'function') {
287
- this.rpc.onRotate(async ({ handle }) => {
288
- const { epoch } = await this._resolve(handle).store.rotate()
289
- return { epoch }
290
- })
291
- }
278
+ this.rpc.onRotate(async ({ handle }) => {
279
+ const { epoch } = await this._resolve(handle).store.rotate()
280
+ return { epoch }
281
+ })
292
282
 
293
283
  this.rpc.onJoin(async ({ parent, ref, invite }) => {
294
284
  if (this._resolve(parent) !== this.me) throw CeroError.UNSUPPORTED('nested handles')
@@ -306,7 +296,7 @@ export class Server extends RPCServer {
306
296
  const info = parent.spec.meta.refs?.[ref] || parent.spec.handles?.[ref]
307
297
  if (!info) throw CeroError.UNKNOWN('handle type', ref)
308
298
  const wire = parent.spec.codec.decodeCreate(data) || {}
309
- // routes can't cross the wire (functions) both sides register them via define()
299
+ // routes are functions and cannot cross the wire
310
300
  const opts = { name: wire.name, role: wire.role, accept: wire.noAccept ? false : undefined }
311
301
  const child = await this.me._create(ref, opts)
312
302
  const id = child.id
@@ -358,9 +348,7 @@ export class Server extends RPCServer {
358
348
  }
359
349
 
360
350
  /**
361
- * Resolve a `{ handle, ref }` pair to its `Ref` and codec. When `local` is
362
- * set, the ref is resolved against the root's per-device `local` store and
363
- * paired with the codec bound from the local schema.
351
+ * Resolve a `{ handle, ref }` pair to its `Ref` and codec.
364
352
  *
365
353
  * @param {string} id
366
354
  * @param {string} name
@@ -370,7 +358,7 @@ export class Server extends RPCServer {
370
358
  _refOf(id, name, local) {
371
359
  if (local) {
372
360
  const info = this.spec.meta.local?.refs?.[name]
373
- if (!info || info.builtin) throw CeroError.UNKNOWN('local ref', name)
361
+ if (!info || info.internal) throw CeroError.UNKNOWN('local ref', name)
374
362
  const r = this.me.local?.[name]
375
363
  if (!r) throw CeroError.UNKNOWN('local ref', name)
376
364
  return { ref: r, codec: this.spec.local.codec }
@@ -20,17 +20,9 @@ export type BuildOpts = {
20
20
  /**
21
21
  * Compile a cero schema into wire-level artifacts and write them to disk.
22
22
  *
23
- * Emits a `main/` tree (schema + hyperdb + dispatch + rpc), a `local/` tree
24
- * for per-device data, one `handles/<name>/` tree per child handle type, and
25
- * an `index.js` that re-exports a ready-to-use `spec` object.
26
- *
27
23
  * @param {string} specDir Output directory.
28
24
  * @param {SchemaInput} schema Either a `schema(...)` wrapper or its raw defs object.
29
25
  * @param {BuildOpts} [opts]
30
26
  * @returns {Promise<void>}
31
27
  */
32
- export { getHyperdbType } from './builtins.js';
33
- export declare function build(specDir: any, schema: any, { ns, extensions }?: {
34
- extensions?: boolean;
35
- ns?: string;
36
- }): Promise<void>;
28
+ export declare function build(specDir: string, schema: SchemaInput, { ns, extensions }?: BuildOpts): Promise<void>;
@@ -1,4 +1,4 @@
1
- export declare const refs: {
1
+ export declare const defs: {
2
2
  main: {
3
3
  members: {
4
4
  type: string;
@@ -34,26 +34,12 @@ export declare const refs: {
34
34
  };
35
35
  };
36
36
  };
37
- export declare function getHyperdbType(prim: any): any;
38
- export declare function builtinRefs(ns: any, scope: any): {
39
- [k: string]: {
40
- kind: any;
41
- path: string[];
42
- builtin: boolean;
43
- verb: any;
44
- schema: string;
45
- };
46
- };
47
- export declare function builtinTypes(scope: any, extend: any): {
37
+ export declare function fields(map: any): {
48
38
  name: string;
49
- compact: boolean;
50
- fields: {
51
- name: string;
52
- type: any;
53
- required: boolean;
54
- }[];
39
+ type: any;
40
+ required: boolean;
55
41
  }[];
56
- export declare function rpcTypes(): {
42
+ export declare function types(scope: any, extend?: {}): {
57
43
  name: string;
58
44
  compact: boolean;
59
45
  fields: {
@@ -62,35 +48,31 @@ export declare function rpcTypes(): {
62
48
  required: boolean;
63
49
  }[];
64
50
  }[];
65
- export declare function builtinCollections(ns: any, scope: any): {
51
+ export declare function refs(ns: any, scope: any): {
52
+ [k: string]: {
53
+ kind: any;
54
+ path: string[];
55
+ internal: boolean;
56
+ verb: any;
57
+ schema: string;
58
+ };
59
+ };
60
+ export declare function collections(ns: any, scope: any): {
66
61
  name: string;
67
62
  schema: string;
68
63
  key: string[];
69
64
  }[];
70
- export declare function builtinDispatches(ns: any): {
65
+ export declare function dispatches(ns: any): {
71
66
  name: string;
72
67
  requestType: string;
73
68
  }[];
74
- export declare function rotateDispatch(ns: any): {
75
- name: string;
76
- requestType: string;
77
- };
78
- export declare function rpcCommands(ns: any): ({
79
- name: string;
69
+ export declare function commands(ns: any): {
70
+ name: string | boolean;
80
71
  request: {
81
72
  name: string;
82
73
  };
83
74
  response: {
84
75
  name: string;
85
- stream?: undefined;
76
+ stream: string | true;
86
77
  };
87
- } | {
88
- name: string;
89
- request: {
90
- name: string;
91
- };
92
- response: {
93
- name: string;
94
- stream: boolean;
95
- };
96
- })[];
78
+ }[];
@@ -1,11 +1,6 @@
1
1
  /**
2
- * Mirror a child handle's `profile` (name + avatar) onto its row in the parent's
3
- * `handles` list — so a handle list shows names + avatars without opening each
4
- * one. Adds the synced `fields` to the `handle` builtin.
5
- *
6
- * On create it seeds the handle's `profile.name` from the open `{ name }`, then
7
- * reflects the (app-owned) `profile` onto the row. Requires your handle types to
8
- * declare a `profile` single — handles without one are left untouched.
2
+ * Mirror a child handle's `profile` (name + avatar) onto its row in the parent's `handles`
3
+ * list — so a handle list shows names + avatars without opening each one.
9
4
  *
10
5
  * @param {{ fields?: Record<string, any> }} [opts]
11
6
  */
@@ -1,2 +1,24 @@
1
1
  export * from './profile-sync.js';
2
2
  export * from './handle-sync.js';
3
+ export declare const registry: ({
4
+ name: string;
5
+ bundled: boolean;
6
+ schema: {
7
+ profile: import("@cero-base/core").TypeDef;
8
+ members: {
9
+ kind: 'extend';
10
+ fields: Record<string, import("@cero-base/core").Prim>;
11
+ };
12
+ };
13
+ setup(me: any): void;
14
+ } | {
15
+ name: string;
16
+ bundled: boolean;
17
+ schema: {
18
+ handles: {
19
+ kind: 'extend';
20
+ fields: Record<string, import("@cero-base/core").Prim>;
21
+ };
22
+ };
23
+ setup(me: any): void;
24
+ })[];
@@ -1,9 +1,5 @@
1
1
  /**
2
2
  * Mirror your `profile` onto your `member` row in every handle you're in.
3
- * Declares a `profile` single (`name`, `avatar`, plus any extra `fields`) and
4
- * mirrors them onto the `member` builtin, then publishes when you open/join a
5
- * handle and whenever your profile changes. An app may declare its own richer
6
- * `profile` instead — the app schema wins.
7
3
  *
8
4
  * @param {{ fields?: Record<string, any> }} [opts]
9
5
  */