@cero-base/cero 0.4.0 → 0.5.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 CHANGED
@@ -206,7 +206,7 @@ const inviteStr = await room.invite()
206
206
  cero.watch(room.messages).on('data', ({ data }) => render(data))
207
207
  ```
208
208
 
209
- The same `spec/index.js` is imported on both sides. `cero/build` emits one file; its imports (hyperdb/runtime, hyperdispatch/runtime, hyperschema/runtime) are all pure JS — no native deps — so bundlers like vite handle the renderer bundle fine.
209
+ The same `spec/index.js` is imported on both sides. `@cero-base/cero/build` emits one file; its imports (hyperdb/runtime, hyperdispatch/runtime, hyperschema/runtime) are all pure JS — no native deps — so bundlers like vite handle the renderer bundle fine.
210
210
 
211
211
  ### What you can do over RPC
212
212
 
@@ -263,12 +263,12 @@ const remote = await connect(c, spec)
263
263
 
264
264
  ## Exports
265
265
 
266
- | Path | What you get |
267
- | ------------- | ------------------------------------------------------------------------ |
268
- | `cero` | factory + operators + schema DSL |
269
- | `cero/client` | `connect(ipc, spec)` — talk to a cero running in another process |
270
- | `cero/server` | `serve(ipc, { storage, spec })` — run + expose a cero over an IPC stream |
271
- | `cero/build` | `build(specDir, schema)` — generate the on-disk spec |
266
+ | Path | What you get |
267
+ | ------------------------ | ------------------------------------------------------------------------ |
268
+ | `@cero-base/cero` | factory + operators + schema DSL |
269
+ | `@cero-base/cero/client` | `connect(ipc, spec)` — talk to a cero running in another process |
270
+ | `@cero-base/cero/server` | `serve(ipc, { storage, spec })` — run + expose a cero over an IPC stream |
271
+ | `@cero-base/cero/build` | `build(specDir, schema)` — generate the on-disk spec |
272
272
 
273
273
  ## Tests
274
274
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cero-base/cero",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "The ideal p2p API — everything is a handle, handles contain refs, refs contain rows.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -15,10 +15,22 @@
15
15
  "access": "public"
16
16
  },
17
17
  "exports": {
18
- ".": "./src/index.js",
19
- "./build": "./src/builder.js",
20
- "./server": "./src/rpc/server.js",
21
- "./client": "./src/rpc/client.js"
18
+ ".": {
19
+ "types": "./types/index.d.ts",
20
+ "default": "./src/index.js"
21
+ },
22
+ "./build": {
23
+ "types": "./types/builder.d.ts",
24
+ "default": "./src/builder.js"
25
+ },
26
+ "./server": {
27
+ "types": "./types/rpc/server.d.ts",
28
+ "default": "./src/rpc/server.js"
29
+ },
30
+ "./client": {
31
+ "types": "./types/rpc/client.d.ts",
32
+ "default": "./src/rpc/client.js"
33
+ }
22
34
  },
23
35
  "typesVersions": {
24
36
  "*": {
@@ -55,7 +67,7 @@
55
67
  "test": "ls test/*.test.js | xargs -P2 -n1 brittle-node"
56
68
  },
57
69
  "dependencies": {
58
- "@cero-base/core": "^0.4.0",
70
+ "@cero-base/core": "^0.4.1",
59
71
  "b4a": "^1.8.1",
60
72
  "bare-crypto": "^1.13.7",
61
73
  "bare-fs": "^4.7.1",
package/src/builder.js CHANGED
@@ -257,6 +257,7 @@ import * as dispatch from './main/dispatch/index.js'
257
257
  import * as schema from './main/schema/index.js'
258
258
  import rpc from './main/rpc/index.js'
259
259
  import localDatabase from './local/db/index.js'
260
+ import * as localSchema from './local/schema/index.js'
260
261
  ${handleImports(names, kinds)}
261
262
 
262
263
  export const meta = ${JSON.stringify(meta, null, 2)}
@@ -266,7 +267,7 @@ export const spec = {
266
267
  dispatch,
267
268
  schema,
268
269
  rpc,
269
- local: { database: localDatabase },
270
+ local: { database: localDatabase, schema: localSchema, meta: meta.local },
270
271
  meta,
271
272
  handles: {
272
273
  ${handleEntries(names, kinds)}
@@ -254,7 +254,8 @@ export function rpcTypes() {
254
254
  fields: [
255
255
  { name: 'handle', type: 'string', required: true },
256
256
  { name: 'ref', type: 'string', required: true },
257
- { name: 'data', type: 'buffer', required: true }
257
+ { name: 'data', type: 'buffer', required: true },
258
+ { name: 'local', type: 'bool', required: false }
258
259
  ]
259
260
  },
260
261
  {
@@ -263,7 +264,8 @@ export function rpcTypes() {
263
264
  fields: [
264
265
  { name: 'handle', type: 'string', required: true },
265
266
  { name: 'ref', type: 'string', required: true },
266
- { name: 'id', type: 'string', required: true }
267
+ { name: 'id', type: 'string', required: true },
268
+ { name: 'local', type: 'bool', required: false }
267
269
  ]
268
270
  },
269
271
  {
@@ -272,7 +274,8 @@ export function rpcTypes() {
272
274
  fields: [
273
275
  { name: 'handle', type: 'string', required: true },
274
276
  { name: 'ref', type: 'string', required: true },
275
- { name: 'query', type: 'buffer', required: false }
277
+ { name: 'query', type: 'buffer', required: false },
278
+ { name: 'local', type: 'bool', required: false }
276
279
  ]
277
280
  },
278
281
  {
package/src/rpc/client.js CHANGED
@@ -12,8 +12,9 @@ export { put, set, get, del, count, watch, call, open } from '../lib/operators.j
12
12
  * @property {'single'|'collection'|'action'|'handle'} [kind]
13
13
  * @property {string} [schema]
14
14
  * @property {string} [type]
15
+ * @property {boolean} [builtin]
15
16
  *
16
- * @typedef {import('@cero-base/core/rpc').Spec & { meta: { ns?: string, refs: Record<string, RefInfo>, handles?: Record<string, Spec> }, handles: Record<string, Spec> }} Spec Built cero spec (schema + rpc + per-handle child specs).
17
+ * @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).
17
18
  *
18
19
  * @typedef {{ data: any }} SingleResult
19
20
  * @typedef {{ data: any[], total: number, size: number }} ListResult
@@ -29,12 +30,26 @@ export { put, set, get, del, count, watch, call, open } from '../lib/operators.j
29
30
  * @property {string|null} name
30
31
  */
31
32
 
32
- // Mixin applied to both Client and Handle so they expose the same row-ops
33
- // surface as a local cero handle but routed over the wire.
33
+ // Mixin applied to Client, Handle and LocalRefs so they expose the same
34
+ // row-ops surface as a local cero handle but routed over the wire. `_local`
35
+ // selects the per-device store + JSON codec; main refs use the schema codec.
34
36
  const operators = {
37
+ _local: false,
38
+
39
+ /** @param {string} name @returns {RefInfo|undefined} */
40
+ _refInfo(name) {
41
+ const refs = this._local ? this.spec.meta.local?.refs : this.spec.meta.refs
42
+ return refs?.[name]
43
+ },
44
+
45
+ /** @returns {any} */
46
+ _codec() {
47
+ return this._local ? this.spec.local.codec : this.spec.codec
48
+ },
49
+
35
50
  /** @param {string} name @returns {string|undefined} */
36
51
  schemaOf(name) {
37
- return this.spec.meta.refs[name]?.schema
52
+ return this._refInfo(name)?.schema
38
53
  },
39
54
 
40
55
  /**
@@ -45,13 +60,14 @@ const operators = {
45
60
  * @returns {Promise<SingleResult>}
46
61
  */
47
62
  async put(name, row) {
63
+ const codec = this._codec()
48
64
  const schema = this.schemaOf(name)
49
- const codec = this.spec.codec
50
65
  const input = row?.id ? row : { id: '', ...row }
51
66
  const res = await this.rpc.addRow({
52
67
  handle: this.id,
53
68
  ref: name,
54
- data: codec.encodeRow(schema, input)
69
+ data: codec.encodeRow(schema, input),
70
+ local: this._local
55
71
  })
56
72
  return { data: codec.decodeRow(schema, res.data) }
57
73
  },
@@ -64,12 +80,13 @@ const operators = {
64
80
  * @returns {Promise<SingleResult>}
65
81
  */
66
82
  async set(name, row) {
83
+ const codec = this._codec()
67
84
  const schema = this.schemaOf(name)
68
- const codec = this.spec.codec
69
85
  const res = await this.rpc.set({
70
86
  handle: this.id,
71
87
  ref: name,
72
- data: codec.encodeRow(schema, row)
88
+ data: codec.encodeRow(schema, row),
89
+ local: this._local
73
90
  })
74
91
  return { data: codec.decodeRow(schema, res.data) }
75
92
  },
@@ -83,20 +100,27 @@ const operators = {
83
100
  * @returns {Promise<SingleResult | ListResult | GetByIdResult>}
84
101
  */
85
102
  async get(name, query) {
103
+ const codec = this._codec()
86
104
  const schema = this.schemaOf(name)
87
- const codec = this.spec.codec
88
105
  if (typeof query === 'string') {
89
- const res = await this.rpc.getOne({ handle: this.id, ref: name, id: query })
106
+ const res = await this.rpc.getOne({
107
+ handle: this.id,
108
+ ref: name,
109
+ id: query,
110
+ local: this._local
111
+ })
90
112
  return { data: res.data ? codec.decodeRow(schema, res.data) : null }
91
113
  }
92
114
  const res = await this.rpc.get({
93
115
  handle: this.id,
94
116
  ref: name,
95
- query: codec.encodeQuery(query)
117
+ query: codec.encodeQuery(query),
118
+ local: this._local
96
119
  })
97
- const kind = this.spec.meta.refs[name]?.kind
98
120
  const data =
99
- kind === 'single' ? codec.decodeRow(schema, res.data) : codec.decodeRows(schema, res.data)
121
+ this._refInfo(name)?.kind === 'single'
122
+ ? codec.decodeRow(schema, res.data)
123
+ : codec.decodeRows(schema, res.data)
100
124
  return { data, total: res.total, size: res.size }
101
125
  },
102
126
 
@@ -108,7 +132,7 @@ const operators = {
108
132
  * @returns {Promise<void>}
109
133
  */
110
134
  async del(name, id) {
111
- await this.rpc.del({ handle: this.id, ref: name, id })
135
+ await this.rpc.del({ handle: this.id, ref: name, id, local: this._local })
112
136
  },
113
137
 
114
138
  /**
@@ -119,11 +143,12 @@ const operators = {
119
143
  * @returns {Promise<{ data: number }>}
120
144
  */
121
145
  async count(name, query) {
122
- const codec = this.spec.codec
146
+ const codec = this._codec()
123
147
  const res = await this.rpc.count({
124
148
  handle: this.id,
125
149
  ref: name,
126
- query: codec.encodeQuery(query)
150
+ query: codec.encodeQuery(query),
151
+ local: this._local
127
152
  })
128
153
  return { data: res.count }
129
154
  },
@@ -137,13 +162,14 @@ const operators = {
137
162
  * @returns {import('streamx').Readable}
138
163
  */
139
164
  watch(name, query) {
140
- const refInfo = this.spec.meta.refs[name]
165
+ const refInfo = this._refInfo(name)
166
+ const codec = this._codec()
141
167
  const schema = refInfo?.schema
142
- const codec = this.spec.codec
143
168
  const wire = this.rpc.watch({
144
169
  handle: this.id,
145
170
  ref: name,
146
- query: codec.encodeQuery(query)
171
+ query: codec.encodeQuery(query),
172
+ local: this._local
147
173
  })
148
174
  const out = new Readable({
149
175
  predestroy() {
@@ -171,7 +197,7 @@ const operators = {
171
197
  */
172
198
  async call(op, data) {
173
199
  const schema = this.schemaOf(op)
174
- const codec = this.spec.codec
200
+ const codec = this._codec()
175
201
  const encoded = data ? codec.encodeAction({ [op]: { schema } }, op, data) : null
176
202
  await this.rpc.call({ handle: this.id, op, data: encoded })
177
203
  },
@@ -215,6 +241,36 @@ export async function restore(me, phrase) {
215
241
  return me
216
242
  }
217
243
 
244
+ /**
245
+ * Per-device `local`-namespace surface on a Client. Exposes each app-defined
246
+ * local ref (e.g. `client.local.settings`) and routes ops over RPC with
247
+ * `local: true`, so they hit the server's per-device store and never
248
+ * replicate. Built-in local refs (identity master/keypair) are not exposed.
249
+ */
250
+ class LocalRefs {
251
+ /** @param {Client} client */
252
+ constructor(client) {
253
+ Object.assign(this, operators)
254
+ this.parent = client
255
+ this.spec = client.spec
256
+ this.store = this
257
+ this._local = true
258
+ const refs = client.spec.meta.local?.refs || {}
259
+ const exposed = Object.fromEntries(Object.entries(refs).filter(([, info]) => !info.builtin))
260
+ attachRefs(this, exposed)
261
+ }
262
+
263
+ /** Underlying RPC channel borrowed from the parent. */
264
+ get rpc() {
265
+ return this.parent.rpc
266
+ }
267
+
268
+ /** Root handle id (local ops are resolved against the root's local store). */
269
+ get id() {
270
+ return this.parent.id
271
+ }
272
+ }
273
+
218
274
  /**
219
275
  * IPC-side RPC client for cero. Wraps an `hrpc` channel and exposes the
220
276
  * same handle/ref/row API as a local cero instance, transparently
@@ -227,10 +283,12 @@ export class Client extends RPCClient {
227
283
  */
228
284
  constructor(ipc, spec) {
229
285
  super(ipc, spec)
286
+ if (spec.local?.schema && !spec.local.codec) bindCodec(spec.local)
230
287
  Object.assign(this, operators)
231
288
  this.id = null
232
289
  this.deviceId = null
233
290
  this.store = this
291
+ this.local = null
234
292
  }
235
293
 
236
294
  async _open() {
@@ -240,6 +298,7 @@ export class Client extends RPCClient {
240
298
  this.deviceId = deviceId || null
241
299
  this.identity = { id, toPhrase: () => phrase || null }
242
300
  attachRefs(this, /** @type {Spec} */ (this.spec).meta.refs)
301
+ if (/** @type {Spec} */ (this.spec).meta.local?.refs) this.local = new LocalRefs(this)
243
302
  }
244
303
 
245
304
  /**
package/src/rpc/server.js CHANGED
@@ -40,6 +40,7 @@ export class Server extends RPCServer {
40
40
  if (!storage) throw CeroError.REQUIRED('storage')
41
41
  if (!spec) throw CeroError.REQUIRED('spec')
42
42
  super(ipc, spec)
43
+ if (spec.local?.schema && !spec.local.codec) bindCodec(spec.local)
43
44
  this.storage = storage
44
45
  this.opts = opts
45
46
  this.me = null
@@ -100,20 +101,20 @@ export class Server extends RPCServer {
100
101
 
101
102
  /** Register the row-level RPC handlers (put/set/get/del/count/watch/call). */
102
103
  _wireData() {
103
- this.rpc.onAddRow(async ({ handle, ref, data }) => {
104
- const { ref: r, codec } = this._refOf(handle, ref)
104
+ this.rpc.onAddRow(async ({ handle, ref, data, local }) => {
105
+ const { ref: r, codec } = this._refOf(handle, ref, local)
105
106
  const { data: row } = await put(r, codec.decodeRow(r.schema, data))
106
107
  return { data: codec.encodeRow(r.schema, row) }
107
108
  })
108
109
 
109
- this.rpc.onSet(async ({ handle, ref, data }) => {
110
- const { ref: r, codec } = this._refOf(handle, ref)
110
+ this.rpc.onSet(async ({ handle, ref, data, local }) => {
111
+ const { ref: r, codec } = this._refOf(handle, ref, local)
111
112
  const { data: row } = await set(r, codec.decodeRow(r.schema, data))
112
113
  return { data: codec.encodeRow(r.schema, row) }
113
114
  })
114
115
 
115
- this.rpc.onGet(async ({ handle, ref, query }) => {
116
- const { ref: r, codec } = this._refOf(handle, ref)
116
+ this.rpc.onGet(async ({ handle, ref, query, local }) => {
117
+ const { ref: r, codec } = this._refOf(handle, ref, local)
117
118
  const result = /** @type {GetResult} */ (await get(r, codec.decodeQuery(query)))
118
119
  const data =
119
120
  r.kind === 'single'
@@ -122,28 +123,28 @@ export class Server extends RPCServer {
122
123
  return { data, total: result.total ?? 0, size: result.size ?? 0 }
123
124
  })
124
125
 
125
- this.rpc.onGetOne(async ({ handle, ref, id }) => {
126
- const { ref: r, codec } = this._refOf(handle, ref)
126
+ this.rpc.onGetOne(async ({ handle, ref, id, local }) => {
127
+ const { ref: r, codec } = this._refOf(handle, ref, local)
127
128
  const { data } = await get(r, id)
128
129
  return { data: data ? codec.encodeRow(r.schema, data) : null }
129
130
  })
130
131
 
131
- this.rpc.onDel(async ({ handle, ref, id }) => {
132
- await del(this._refOf(handle, ref).ref, id)
132
+ this.rpc.onDel(async ({ handle, ref, id, local }) => {
133
+ await del(this._refOf(handle, ref, local).ref, id)
133
134
  return {}
134
135
  })
135
136
 
136
- this.rpc.onCount(async ({ handle, ref, query }) => {
137
- const { ref: r, codec } = this._refOf(handle, ref)
137
+ this.rpc.onCount(async ({ handle, ref, query, local }) => {
138
+ const { ref: r, codec } = this._refOf(handle, ref, local)
138
139
  const { data } = await count(r, codec.decodeQuery(query))
139
140
  return { count: data }
140
141
  })
141
142
 
142
143
  this.rpc.onWatch((stream) => {
143
- const { handle, ref, query } = stream.data
144
+ const { handle, ref, query, local } = stream.data
144
145
  let r, codec, live
145
146
  try {
146
- ;({ ref: r, codec } = this._refOf(handle, ref))
147
+ ;({ ref: r, codec } = this._refOf(handle, ref, local))
147
148
  live = watch(r, codec.decodeQuery(query))
148
149
  } catch (err) {
149
150
  stream.destroy(err)
@@ -249,13 +250,23 @@ export class Server extends RPCServer {
249
250
  }
250
251
 
251
252
  /**
252
- * Resolve a `{ handle, ref }` pair to its `Ref` and the handle's codec.
253
+ * Resolve a `{ handle, ref }` pair to its `Ref` and codec. When `local` is
254
+ * set, the ref is resolved against the root's per-device `local` store and
255
+ * paired with the codec bound from the local schema.
253
256
  *
254
257
  * @param {string} id
255
258
  * @param {string} name
259
+ * @param {boolean} [local]
256
260
  * @returns {RefAndCodec}
257
261
  */
258
- _refOf(id, name) {
262
+ _refOf(id, name, local) {
263
+ if (local) {
264
+ const info = this.spec.meta.local?.refs?.[name]
265
+ if (!info || info.builtin) throw CeroError.UNKNOWN('local ref', name)
266
+ const r = this.me.local?.[name]
267
+ if (!r) throw CeroError.UNKNOWN('local ref', name)
268
+ return { ref: r, codec: this.spec.local.codec }
269
+ }
259
270
  const h = this._resolve(id)
260
271
  const r = h[name]
261
272
  if (!r) throw CeroError.UNKNOWN('ref', name)
@@ -29,6 +29,7 @@ export class Client extends RPCClient {
29
29
  id: any;
30
30
  deviceId: any;
31
31
  store: this;
32
+ local: LocalRefs;
32
33
  identity: {
33
34
  id: any;
34
35
  toPhrase: () => any;
@@ -63,6 +64,7 @@ export type RefInfo = {
63
64
  kind?: "single" | "collection" | "action" | "handle";
64
65
  schema?: string;
65
66
  type?: string;
67
+ builtin?: boolean;
66
68
  };
67
69
  /**
68
70
  * Built cero spec (schema + rpc + per-handle child specs).
@@ -71,6 +73,9 @@ export type Spec = import("@cero-base/core/rpc").Spec & {
71
73
  meta: {
72
74
  ns?: string;
73
75
  refs: Record<string, RefInfo>;
76
+ local?: {
77
+ refs: Record<string, RefInfo>;
78
+ };
74
79
  handles?: Record<string, Spec>;
75
80
  };
76
81
  handles: Record<string, Spec>;
@@ -96,6 +101,24 @@ export type HandleStub = {
96
101
  name: string | null;
97
102
  };
98
103
  import { RPCClient } from '@cero-base/core/rpc';
104
+ /**
105
+ * Per-device `local`-namespace surface on a Client. Exposes each app-defined
106
+ * local ref (e.g. `client.local.settings`) and routes ops over RPC with
107
+ * `local: true`, so they hit the server's per-device store and never
108
+ * replicate. Built-in local refs (identity master/keypair) are not exposed.
109
+ */
110
+ declare class LocalRefs {
111
+ /** @param {Client} client */
112
+ constructor(client: Client);
113
+ parent: Client;
114
+ spec: import("@cero-base/core/rpc").Spec;
115
+ store: this;
116
+ _local: boolean;
117
+ /** Underlying RPC channel borrowed from the parent. */
118
+ get rpc(): any;
119
+ /** Root handle id (local ops are resolved against the root's local store). */
120
+ get id(): any;
121
+ }
99
122
  /**
100
123
  * Client-side proxy for a remote handle. Exposes the same row-ops surface
101
124
  * as `Client` but scoped to a single child handle id, and routes every
@@ -87,13 +87,16 @@ export class Server extends RPCServer {
87
87
  */
88
88
  _resolve(id: string): any;
89
89
  /**
90
- * Resolve a `{ handle, ref }` pair to its `Ref` and the handle's codec.
90
+ * Resolve a `{ handle, ref }` pair to its `Ref` and codec. When `local` is
91
+ * set, the ref is resolved against the root's per-device `local` store and
92
+ * paired with the codec bound from the local schema.
91
93
  *
92
94
  * @param {string} id
93
95
  * @param {string} name
96
+ * @param {boolean} [local]
94
97
  * @returns {RefAndCodec}
95
98
  */
96
- _refOf(id: string, name: string): RefAndCodec;
99
+ _refOf(id: string, name: string, local?: boolean): RefAndCodec;
97
100
  /**
98
101
  * Snapshot the current identity for return to the client.
99
102
  *