@cero-base/cero 0.4.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.
@@ -0,0 +1,128 @@
1
+ import { Readable } from 'streamx'
2
+
3
+ /**
4
+ * @typedef {import('./utils.js').Ref} Ref
5
+ * @typedef {{ data: any }} SingleResult
6
+ * @typedef {{ data: any[], total: number, size: number }} ListResult
7
+ * @typedef {{ data: any | null }} GetByIdResult
8
+ */
9
+
10
+ /**
11
+ * Insert (or overwrite by id) a row on `ref`.
12
+ *
13
+ * @param {Ref} ref
14
+ * @param {Record<string, any>} row
15
+ * @returns {Promise<SingleResult>}
16
+ */
17
+ export const put = (ref, row) => ref.handle.store.put(ref.name, row)
18
+
19
+ /**
20
+ * Upsert a row on `ref` — merges with the existing row and preserves
21
+ * `createdAt`.
22
+ *
23
+ * @param {Ref} ref
24
+ * @param {Record<string, any>} row
25
+ * @returns {Promise<SingleResult>}
26
+ */
27
+ export const set = (ref, row) => ref.handle.store.set(ref.name, row)
28
+
29
+ /**
30
+ * Delete a row by id (collection refs), or wipe the row (single refs).
31
+ *
32
+ * @param {Ref} ref
33
+ * @param {string} [id]
34
+ * @returns {Promise<void>}
35
+ */
36
+ export const del = (ref, id) => ref.handle.store.del(ref.name, id)
37
+
38
+ /**
39
+ * Count rows on `ref`, optionally filtered.
40
+ *
41
+ * @param {Ref} ref
42
+ * @param {Record<string, any>} [q]
43
+ * @returns {Promise<{ data: number }>}
44
+ */
45
+ export const count = (ref, q) => ref.handle.store.count(ref.name, q)
46
+
47
+ /**
48
+ * Invoke an `action`-kind ref (a custom mutation declared in the schema).
49
+ *
50
+ * @param {Ref} ref
51
+ * @param {Record<string, any>} [d]
52
+ * @returns {Promise<any>}
53
+ */
54
+ export const call = (ref, d) => ref.handle.store.call(ref.name, d)
55
+
56
+ // get/watch on a handle-kind ref list its rows from the `handles` collection
57
+ // filtered by type (handle types are stored there with their { id, key,
58
+ // encryptionKey, name }). Data-kind refs go straight to the store.
59
+ // For handle-kind refs on the facade, the parent store lives on `root`.
60
+ const parentStore = (ref) => (ref.handle.root ? ref.handle.root.store : ref.handle.store)
61
+
62
+ const normalize = (rows, name) => {
63
+ const data = (rows || []).filter((r) => r.type === name)
64
+ return { data, total: data.length, size: data.length }
65
+ }
66
+
67
+ /**
68
+ * Read from `ref`. For data refs, dispatches to the underlying store. For
69
+ * `handle`-kind refs, lists existing child handles of that type from the
70
+ * parent's `handles` collection.
71
+ *
72
+ * @param {Ref} ref
73
+ * @param {string | Record<string, any>} [q]
74
+ * @returns {Promise<SingleResult | ListResult | GetByIdResult>}
75
+ */
76
+ export const get = async (ref, q) => {
77
+ if (ref.kind !== 'handle') return ref.handle.store.get(ref.name, q)
78
+ const { data: all } = await parentStore(ref).get('handles', q)
79
+ return normalize(all, ref.name)
80
+ }
81
+
82
+ /**
83
+ * Live snapshot stream on `ref` — re-emits the latest `get()` result on
84
+ * every underlying mutation. Destroy the stream to stop watching.
85
+ *
86
+ * @param {Ref} ref
87
+ * @param {Record<string, any>} [q]
88
+ * @returns {import('streamx').Readable}
89
+ */
90
+ export const watch = (ref, q) => {
91
+ if (ref.kind !== 'handle') return ref.handle.store.watch(ref.name, q)
92
+ const source = parentStore(ref).watch('handles', q)
93
+ const out = new Readable({
94
+ destroy(cb) {
95
+ source.destroy()
96
+ cb(null)
97
+ }
98
+ })
99
+ source.on('data', (snap) => out.push(normalize(snap?.data, ref.name)))
100
+ source.on('end', () => out.push(null))
101
+ source.on('error', (err) => out.destroy(err))
102
+ return out
103
+ }
104
+
105
+ // Universal signal instantiator. Dispatch on the second arg:
106
+ // string → join via invite
107
+ // { invite: string } → join via invite (object form)
108
+ // { id: string } → load an existing handle by id
109
+ // object | undefined → create (opts)
110
+ /**
111
+ * Open (or create / join / load) a child handle through a `handle`-kind
112
+ * ref. Dispatches on the normalize of `arg`:
113
+ *
114
+ * - `string` → join via an invite string
115
+ * - `{ invite: string }` → join via invite (object form)
116
+ * - `{ id: string }` → load an existing handle by id
117
+ * - `object | undefined` → create a new handle with the given opts
118
+ *
119
+ * @param {Ref} ref
120
+ * @param {string | { invite?: string, id?: string, name?: string, routes?: any, role?: string, accept?: boolean } | undefined} [arg]
121
+ * @returns {Promise<any>} The resolved child `Handle`.
122
+ */
123
+ export const open = (ref, arg) => {
124
+ if (typeof arg === 'string') return ref.handle._join(arg, ref.name)
125
+ if (arg && typeof arg.invite === 'string') return ref.handle._join(arg.invite, ref.name)
126
+ if (arg && typeof arg.id === 'string') return ref.handle._load(ref.name, arg.id)
127
+ return ref.handle._create(ref.name, arg)
128
+ }
@@ -0,0 +1,36 @@
1
+ import HypercoreStorage from 'hypercore-storage'
2
+ import Corestore from 'corestore'
3
+
4
+ import { CeroError } from '@cero-base/core/errors'
5
+
6
+ import { Local } from '../local/index.js'
7
+
8
+ /**
9
+ * Quickly check whether the on-disk directory at `dir` already holds an
10
+ * initialised cero identity (i.e. a stored master seed). Opens the local
11
+ * store read-only and closes everything before returning.
12
+ *
13
+ * @param {string} dir Cero data directory.
14
+ * @param {any} spec Built spec — same value passed to `cero(dir, spec)`.
15
+ * @returns {Promise<boolean>} `true` if a master seed exists on disk.
16
+ */
17
+ export async function peek(dir, spec) {
18
+ if (typeof dir !== 'string' || !dir) throw CeroError.INVALID('dir must be a non-empty string')
19
+ if (!spec) throw CeroError.REQUIRED('spec')
20
+
21
+ const root = new HypercoreStorage(`${dir}/main`)
22
+ await root.ready()
23
+ const store = new Corestore(root, { manifestVersion: 2 })
24
+ await store.ready()
25
+
26
+ const local = new Local(null, spec, { store })
27
+ await local.ready()
28
+ try {
29
+ const { data } = await local.store.get('master')
30
+ return !!data?.seed
31
+ } finally {
32
+ await local.close()
33
+ await store.close()
34
+ await root.close()
35
+ }
36
+ }
@@ -0,0 +1,6 @@
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.
5
+ */
6
+ export { t, schema } from '@cero-base/core/schema'
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @typedef {'collection' | 'single' | 'action' | 'handle'} RefKind
3
+ * @typedef {{ kind?: string, schema?: string }} RefInfo
4
+ * Shape of the entries in `meta.refs` — describes a single ref name.
5
+ * `kind` is one of {@link RefKind}, kept as `string` since it originates
6
+ * from a generated spec.
7
+ */
8
+
9
+ /**
10
+ * Typed pointer to a single ref (table or handle slot) on a `Handle` or
11
+ * `Local`. Operators (`put`/`get`/`open`/...) take a `Ref` as their first
12
+ * argument and dispatch through the owning handle's store.
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
+ /**
30
+ * Attach a `Ref` property to `target` for every entry in `refs`. Used by
31
+ * `Handle` and `Local` during `_open()` so callers can write
32
+ * `handle.someRef` instead of looking refs up by name.
33
+ *
34
+ * @param {any} target
35
+ * @param {Record<string, RefInfo>} refs
36
+ */
37
+ export function attachRefs(target, refs) {
38
+ for (const [name, info] of Object.entries(refs || {})) {
39
+ target[name] = new Ref(target, name, info.kind, info.schema)
40
+ }
41
+ }
@@ -0,0 +1,49 @@
1
+ import ReadyResource from 'ready-resource'
2
+
3
+ import { Storage } from '@cero-base/core/storage'
4
+ import { CeroError } from '@cero-base/core/errors'
5
+
6
+ import { attachRefs } from '../lib/utils.js'
7
+
8
+ /**
9
+ * @typedef {object} LocalOpts
10
+ * @property {any} [root] Pre-existing HypercoreStorage to reuse.
11
+ * @property {any} [store] Pre-existing Corestore to reuse.
12
+ */
13
+
14
+ /**
15
+ * Per-device, single-writer storage for cero — holds the master seed,
16
+ * device keypair and any per-handle keypairs. Wraps a hyperbee-backed
17
+ * `Storage` and exposes each local ref as a property of the instance.
18
+ */
19
+ export class Local extends ReadyResource {
20
+ /**
21
+ * @param {string | null} dir Directory for the local store, or `null` when reusing an external `store`.
22
+ * @param {any} spec Built cero spec — must include `spec.local.database` and `spec.meta.local`.
23
+ * @param {LocalOpts} [opts]
24
+ */
25
+ constructor(dir, spec, { root, store } = {}) {
26
+ super()
27
+ if (!root && !store && (typeof dir !== 'string' || !dir))
28
+ throw CeroError.REQUIRED('dir, root, or store')
29
+ if (!spec?.local?.database) throw CeroError.REQUIRED('spec.local.database')
30
+ if (!spec?.meta?.local) throw CeroError.REQUIRED('spec.meta.local')
31
+
32
+ this.dir = dir
33
+ this.spec = spec
34
+ this.store = Storage.bee(dir, {
35
+ spec: { database: spec.local.database, meta: spec.meta.local },
36
+ root,
37
+ store
38
+ })
39
+ }
40
+
41
+ async _open() {
42
+ await this.store.ready()
43
+ attachRefs(this, this.store.refs)
44
+ }
45
+
46
+ async _close() {
47
+ await this.store.close()
48
+ }
49
+ }
@@ -0,0 +1,3 @@
1
+ <claude-mem-context>
2
+
3
+ </claude-mem-context>
@@ -0,0 +1,337 @@
1
+ import { Readable } from 'streamx'
2
+ import { RPCClient, bindCodec } from '@cero-base/core/rpc'
3
+
4
+ import { attachRefs } from '../lib/utils.js'
5
+
6
+ export { put, set, get, del, count, watch, call, open } from '../lib/operators.js'
7
+
8
+ /**
9
+ * @typedef {import('@cero-base/core/rpc').RPCClient} BaseRPCClient
10
+ *
11
+ * @typedef {object} RefInfo
12
+ * @property {'single'|'collection'|'action'|'handle'} [kind]
13
+ * @property {string} [schema]
14
+ * @property {string} [type]
15
+ *
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
+ *
18
+ * @typedef {{ data: any }} SingleResult
19
+ * @typedef {{ data: any[], total: number, size: number }} ListResult
20
+ * @typedef {{ data: any | null }} GetByIdResult
21
+ *
22
+ * @typedef {object} ClientIdentity
23
+ * @property {string} id
24
+ * @property {() => string|null} toPhrase
25
+ *
26
+ * @typedef {object} HandleStub
27
+ * @property {string} id
28
+ * @property {string} type
29
+ * @property {string|null} name
30
+ */
31
+
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.
34
+ const operators = {
35
+ /** @param {string} name @returns {string|undefined} */
36
+ schemaOf(name) {
37
+ return this.spec.meta.refs[name]?.schema
38
+ },
39
+
40
+ /**
41
+ * Insert a row over the wire.
42
+ *
43
+ * @param {string} name
44
+ * @param {Record<string, any>} row
45
+ * @returns {Promise<SingleResult>}
46
+ */
47
+ async put(name, row) {
48
+ const schema = this.schemaOf(name)
49
+ const codec = this.spec.codec
50
+ const input = row?.id ? row : { id: '', ...row }
51
+ const res = await this.rpc.addRow({
52
+ handle: this.id,
53
+ ref: name,
54
+ data: codec.encodeRow(schema, input)
55
+ })
56
+ return { data: codec.decodeRow(schema, res.data) }
57
+ },
58
+
59
+ /**
60
+ * Upsert a row over the wire.
61
+ *
62
+ * @param {string} name
63
+ * @param {Record<string, any>} row
64
+ * @returns {Promise<SingleResult>}
65
+ */
66
+ async set(name, row) {
67
+ const schema = this.schemaOf(name)
68
+ const codec = this.spec.codec
69
+ const res = await this.rpc.set({
70
+ handle: this.id,
71
+ ref: name,
72
+ data: codec.encodeRow(schema, row)
73
+ })
74
+ return { data: codec.decodeRow(schema, res.data) }
75
+ },
76
+
77
+ /**
78
+ * Read a row (by id string) or a list (by query object). Mirrors the
79
+ * `Storage.get` contract.
80
+ *
81
+ * @param {string} name
82
+ * @param {string | Record<string, any>} [query]
83
+ * @returns {Promise<SingleResult | ListResult | GetByIdResult>}
84
+ */
85
+ async get(name, query) {
86
+ const schema = this.schemaOf(name)
87
+ const codec = this.spec.codec
88
+ if (typeof query === 'string') {
89
+ const res = await this.rpc.getOne({ handle: this.id, ref: name, id: query })
90
+ return { data: res.data ? codec.decodeRow(schema, res.data) : null }
91
+ }
92
+ const res = await this.rpc.get({
93
+ handle: this.id,
94
+ ref: name,
95
+ query: codec.encodeQuery(query)
96
+ })
97
+ const kind = this.spec.meta.refs[name]?.kind
98
+ const data =
99
+ kind === 'single' ? codec.decodeRow(schema, res.data) : codec.decodeRows(schema, res.data)
100
+ return { data, total: res.total, size: res.size }
101
+ },
102
+
103
+ /**
104
+ * Delete a row by id.
105
+ *
106
+ * @param {string} name
107
+ * @param {string} [id]
108
+ * @returns {Promise<void>}
109
+ */
110
+ async del(name, id) {
111
+ await this.rpc.del({ handle: this.id, ref: name, id })
112
+ },
113
+
114
+ /**
115
+ * Count matching rows.
116
+ *
117
+ * @param {string} name
118
+ * @param {Record<string, any>} [query]
119
+ * @returns {Promise<{ data: number }>}
120
+ */
121
+ async count(name, query) {
122
+ const codec = this.spec.codec
123
+ const res = await this.rpc.count({
124
+ handle: this.id,
125
+ ref: name,
126
+ query: codec.encodeQuery(query)
127
+ })
128
+ return { data: res.count }
129
+ },
130
+
131
+ /**
132
+ * Live snapshot stream. Re-emits the latest `get()` shape on every
133
+ * underlying mutation. Destroy the stream to stop watching.
134
+ *
135
+ * @param {string} name
136
+ * @param {Record<string, any>} [query]
137
+ * @returns {import('streamx').Readable}
138
+ */
139
+ watch(name, query) {
140
+ const refInfo = this.spec.meta.refs[name]
141
+ const schema = refInfo?.schema
142
+ const codec = this.spec.codec
143
+ const wire = this.rpc.watch({
144
+ handle: this.id,
145
+ ref: name,
146
+ query: codec.encodeQuery(query)
147
+ })
148
+ const out = new Readable({
149
+ predestroy() {
150
+ wire.destroy()
151
+ }
152
+ })
153
+ wire.on('data', (snap) => {
154
+ const data =
155
+ refInfo.kind === 'single'
156
+ ? (codec.decodeRow(schema, snap.data) ?? null)
157
+ : codec.decodeRows(schema, snap.data)
158
+ out.push({ data, total: snap.total, size: snap.size })
159
+ })
160
+ wire.on('end', () => out.push(null))
161
+ wire.on('error', (err) => out.destroy(err))
162
+ return out
163
+ },
164
+
165
+ /**
166
+ * Invoke a named action ref over the wire.
167
+ *
168
+ * @param {string} op
169
+ * @param {any} [data]
170
+ * @returns {Promise<void>}
171
+ */
172
+ async call(op, data) {
173
+ const schema = this.schemaOf(op)
174
+ const codec = this.spec.codec
175
+ const encoded = data ? codec.encodeAction({ [op]: { schema } }, op, data) : null
176
+ await this.rpc.call({ handle: this.id, op, data: encoded })
177
+ },
178
+
179
+ /**
180
+ * Mint a pairing invite for this handle.
181
+ *
182
+ * @param {{ role?: string }} [opts]
183
+ * @returns {Promise<string>}
184
+ */
185
+ async invite({ role } = {}) {
186
+ const { invite } = await this.rpc.invite({ handle: this.id, role: role || '' })
187
+ return invite
188
+ },
189
+
190
+ /**
191
+ * Revoke a previously issued invite.
192
+ *
193
+ * @param {string} invite
194
+ * @returns {Promise<boolean>}
195
+ */
196
+ async revoke(invite) {
197
+ const { ok } = await this.rpc.revoke({ handle: this.id, invite })
198
+ return ok
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Re-initialize a `Client` from a recovery phrase. Closes the existing
204
+ * local state and reseeds identity from the phrase.
205
+ *
206
+ * @param {Client} me
207
+ * @param {string} phrase
208
+ * @returns {Promise<Client>}
209
+ */
210
+ export async function restore(me, phrase) {
211
+ const res = await me.rpc.restore({ phrase })
212
+ me.id = res.id
213
+ me.deviceId = res.deviceId || null
214
+ me.identity = { id: res.id, toPhrase: () => res.phrase || null }
215
+ return me
216
+ }
217
+
218
+ /**
219
+ * IPC-side RPC client for cero. Wraps an `hrpc` channel and exposes the
220
+ * same handle/ref/row API as a local cero instance, transparently
221
+ * routing every operation across the wire.
222
+ */
223
+ export class Client extends RPCClient {
224
+ /**
225
+ * @param {any} ipc Framed IPC stream (must be writable).
226
+ * @param {Spec} spec Compiled cero spec (schema + rpc + handles).
227
+ */
228
+ constructor(ipc, spec) {
229
+ super(ipc, spec)
230
+ Object.assign(this, operators)
231
+ this.id = null
232
+ this.deviceId = null
233
+ this.store = this
234
+ }
235
+
236
+ async _open() {
237
+ await super._open()
238
+ const { id, deviceId, phrase } = await this.rpc.init({})
239
+ this.id = id
240
+ this.deviceId = deviceId || null
241
+ this.identity = { id, toPhrase: () => phrase || null }
242
+ attachRefs(this, /** @type {Spec} */ (this.spec).meta.refs)
243
+ }
244
+
245
+ /**
246
+ * Create a new child handle of the given type.
247
+ *
248
+ * @param {string} type
249
+ * @param {Record<string, any>} [opts]
250
+ * @returns {Promise<Handle>}
251
+ */
252
+ async _create(type, opts = {}) {
253
+ const stub = await this.rpc.addHandle({
254
+ ref: type,
255
+ handle: this.id,
256
+ data: this.spec.codec.encodeCreate(opts)
257
+ })
258
+ return new Handle(this, stub.id, stub.type, stub.name || null)
259
+ }
260
+
261
+ /**
262
+ * Load an existing child handle by id.
263
+ *
264
+ * @param {string} type
265
+ * @param {string} id
266
+ * @returns {Promise<Handle>}
267
+ */
268
+ async _load(type, id) {
269
+ const stub = await this.rpc.openHandle({ parent: this.id, row: id })
270
+ return new Handle(this, stub.id, stub.type, stub.name || null)
271
+ }
272
+
273
+ /**
274
+ * Join a child handle via invite.
275
+ *
276
+ * @param {string} invite
277
+ * @param {string} type
278
+ * @returns {Promise<Handle>}
279
+ */
280
+ async _join(invite, type) {
281
+ const stub = await this.rpc.join({ parent: this.id, ref: type, invite })
282
+ return new Handle(this, stub.id, stub.type, stub.name || null)
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Client-side proxy for a remote handle. Exposes the same row-ops surface
288
+ * as `Client` but scoped to a single child handle id, and routes every
289
+ * call through the parent's RPC channel.
290
+ */
291
+ class Handle {
292
+ /**
293
+ * @param {Client} parent
294
+ * @param {string} id
295
+ * @param {string} type
296
+ * @param {string|null} name
297
+ */
298
+ constructor(parent, id, type, name) {
299
+ Object.assign(this, operators)
300
+ this.parent = parent
301
+ this.id = id
302
+ this.type = type
303
+ this.name = name
304
+ this.spec = /** @type {Spec} */ (parent.spec).handles[type]
305
+ if (!this.spec.codec) bindCodec(this.spec)
306
+ this.store = this
307
+ attachRefs(this, this.spec.meta.refs)
308
+ }
309
+
310
+ /** Underlying RPC channel borrowed from the parent. */
311
+ get rpc() {
312
+ return this.parent.rpc
313
+ }
314
+
315
+ /** Tear down the remote handle without leaving the room. */
316
+ close() {
317
+ return this.parent.rpc.closeHandle({ handle: this.id })
318
+ }
319
+
320
+ /** Tear down the remote handle and drop membership. */
321
+ leave() {
322
+ return this.parent.rpc.leave({ handle: this.id })
323
+ }
324
+ }
325
+
326
+ /**
327
+ * Construct a `Client`, wait for `init` to complete, and return it.
328
+ *
329
+ * @param {any} ipc
330
+ * @param {object} spec
331
+ * @returns {Promise<Client>}
332
+ */
333
+ export async function connect(ipc, spec) {
334
+ const client = new Client(ipc, spec)
335
+ await client.ready()
336
+ return client
337
+ }
@@ -0,0 +1,8 @@
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.
5
+ */
6
+
7
+ export { Server, serve } from './server.js'
8
+ export { Client, connect } from './client.js'