@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/index.js CHANGED
@@ -31,7 +31,7 @@ import {
31
31
  import { peek } from './lib/peek.js'
32
32
  import { t, schema } from './lib/spec.js'
33
33
  import { FLUSH, TIMEOUT } from './lib/constants.js'
34
- import { internal } from './lib/internal.js'
34
+ import { registry } from './extensions/index.js'
35
35
 
36
36
  export { Handle, Ref, Local }
37
37
  export {
@@ -80,9 +80,7 @@ export { t, schema } from './lib/spec.js'
80
80
  */
81
81
 
82
82
  /**
83
- * Open (or create) a cero handle at `dir`. Sets up storage, network and
84
- * identity, then returns a ready root `Handle` with all schema refs
85
- * attached as properties.
83
+ * Open (or create) a cero handle at `dir`.
86
84
  *
87
85
  * @param {string} dir Data directory.
88
86
  * @param {any} spec Built spec — output of `cero/build`.
@@ -99,9 +97,7 @@ export async function cero(dir, spec, opts = {}) {
99
97
  let network = null
100
98
  let discovery = null
101
99
  let me = null
102
- // Any failure during open (bad phrase, network/bootstrap error, recovery
103
- // timeout, extension setup) must close whatever opened — else the corestore
104
- // lock leaks and a retry in the same process hits locked storage.
100
+ // any failure during open must close what opened, or the storage lock leaks
105
101
  try {
106
102
  await storage.ready()
107
103
  await fs.promises.chmod(`${dir}/main`, 0o700)
@@ -115,15 +111,11 @@ export async function cero(dir, spec, opts = {}) {
115
111
 
116
112
  const { identity, fresh } = await resolveIdentity(opts, local)
117
113
  const writer = local ? (await local.store.get('keypair')).data : null
118
- // a supplied identity on a device with no writer is recovering: it never
119
- // authors the identity's pointer core or any core that exists elsewhere.
120
- // Only an identity cero generated here creates a database
114
+ // a supplied identity on a device with no writer recovers; only a cero-minted identity creates
121
115
  const recovering = !writer && (!!opts.key || !fresh)
122
116
  const timeout = opts.recoveryTimeout || TIMEOUT
123
117
 
124
- // Channel stamp: a storage remembers its channel; reopening it under a different channel
125
- // including no channel at all, which would silently rejoin the global network — is a
126
- // misconfiguration that could leak data across networks, so reject it.
118
+ // a storage remembers its channel; reopening under another would silently rejoin the global network
127
119
  if (local) {
128
120
  const stored = (await local.store.get('environment')).data?.channel ?? null
129
121
  const wanted = opts.channel ?? null
@@ -141,13 +133,10 @@ export async function cero(dir, spec, opts = {}) {
141
133
  })
142
134
  await network.ready()
143
135
  discovery = network.join(identity.topic)
144
- // a freshly minted identity has no peers yet flushing the announce
145
- // before proceeding only delays first onboarding
136
+ // a fresh identity has no peers yet, flushing the announce would only delay onboarding
146
137
  if (!fresh) await Promise.race([discovery.flush(), new Promise((r) => setTimeout(r, FLUSH))])
147
138
 
148
- // the pointer core: signed by the identity, written once by the device that
149
- // created the identity, read by every device that recovers it. It holds the
150
- // root database key, so a phrase alone finds the data
139
+ // the pointer core: identity-signed, written once by the creating device, holds the root key
151
140
  const manifest = pointerManifest(store, identity)
152
141
  const pointer = store.get(
153
142
  !writer && !recovering
@@ -208,25 +197,16 @@ export async function cero(dir, spec, opts = {}) {
208
197
  }
209
198
  }
210
199
 
211
- for (const ext of internal.extensions) {
200
+ for (const ext of registry) {
212
201
  if (ext.bundled && opts.extensions === false) continue
213
202
  const off = await ext.setup?.(me)
214
203
  if (typeof off === 'function') me.once('close', off)
215
204
  }
216
205
 
217
206
  if (opts.bluetooth) {
218
- // `true` defaults; `{ autoStart, backend }` options. A bare backend
219
- // object (pre-1.3 shape, has Central/Server) is still accepted.
220
- const bt =
221
- opts.bluetooth === true
222
- ? {}
223
- : opts.bluetooth.Central
224
- ? { backend: opts.bluetooth }
225
- : opts.bluetooth
207
+ const bt = opts.bluetooth === true ? {} : opts.bluetooth
226
208
  me.bluetooth = new Bluetooth(me, {
227
- // pass the backend through untouched: `|| null` turned an omitted
228
- // backend (= lazy-load bare-bluetooth) into an explicit null
229
- // (= disabled), leaving BLE 'unsupported' on every platform
209
+ // an omitted backend lazy-loads bare-bluetooth, null disables it
230
210
  backend: bt.backend,
231
211
  autoStart: bt.autoStart !== false,
232
212
  maxOutbound: bt.maxOutbound,
@@ -240,8 +220,7 @@ export async function cero(dir, spec, opts = {}) {
240
220
  bind(me, null)
241
221
  return me
242
222
  } catch (err) {
243
- // once the root Handle exists it owns (and closes) everything; before that,
244
- // tear the raw resources down in reverse order.
223
+ // before the root Handle exists, tear the raw resources down in reverse order
245
224
  if (me) await me.close().catch(safetyCatch)
246
225
  else {
247
226
  await discovery?.destroy().catch(safetyCatch)
@@ -255,9 +234,7 @@ export async function cero(dir, spec, opts = {}) {
255
234
  }
256
235
 
257
236
  /**
258
- * Restore a cero instance from a mnemonic phrase. Closes the running
259
- * instance, wipes the on-disk `main/` tree and re-opens with the phrase, which
260
- * recovers so the writer slot is re-claimed.
237
+ * Restore a cero instance from a mnemonic phrase.
261
238
  *
262
239
  * @param {Handle} me Existing root handle to restore.
263
240
  * @param {string} phrase BIP-39 mnemonic phrase.
@@ -267,12 +244,10 @@ export async function restore(me, phrase) {
267
244
  if (!me?._dir) throw CeroError.INVALID('me must be a cero instance')
268
245
  if (!phrase || typeof phrase !== 'string') throw CeroError.INVALID('phrase must be a string')
269
246
 
270
- // Already this identity? Nothing to restore — return the running instance.
271
247
  const current = await Identity.fromSeed(Identity.toSeed(phrase))
272
248
  if (current.id === me.identity.id) return me
273
249
 
274
- // everything carries over except the old identity the channel in
275
- // particular, or the recovered instance never meets its peers
250
+ // everything carries over except the identity, the channel above all
276
251
  const {
277
252
  _dir: dir,
278
253
  spec,
@@ -285,9 +260,7 @@ export async function restore(me, phrase) {
285
260
  return cero(dir, spec, { ...opts, phrase })
286
261
  }
287
262
 
288
- // Facade: expose the operators + helpers as properties on `cero` too, so both
289
- // `import { define }` and `cero.define(...)` work. Explicit assignments (not
290
- // `Object.assign`) so tsc reflects them onto the `cero` namespace in the .d.ts.
263
+ // the facade: cero.put and import { put } are the same function
291
264
  cero.t = t
292
265
  cero.put = put
293
266
  cero.set = set
@@ -306,18 +279,14 @@ cero.restore = restore
306
279
  cero.schema = schema
307
280
  cero.bind = bind
308
281
  cero.define = define
309
- // test-only escape hatch (cross-package tests reset/seed the registry)
310
- cero._internal = internal
311
- // A bare function is shorthand for a behavior-only extension: `{ setup: fn }`.
312
- // Accepts both `use(a, b)` and `use([a, b])` (and a mix) for easier composition.
313
- // A named extension replaces any registered one with the same name — so
314
- // `use(profileSync({ fields }))` reconfigures the bundled default instead of
315
- // doubling it.
282
+ // test-only
283
+ cero._registry = registry
284
+ // a bare function is shorthand for { setup }; a named extension replaces one of the same name
316
285
  cero.use = (...exts) => {
317
286
  for (const e of exts.flat().map((e) => (typeof e === 'function' ? { setup: e } : e))) {
318
- const i = e.name ? internal.extensions.findIndex((x) => x.name === e.name) : -1
319
- if (i >= 0) internal.extensions[i] = e
320
- else internal.extensions.push(e)
287
+ const i = e.name ? registry.findIndex((x) => x.name === e.name) : -1
288
+ if (i >= 0) registry[i] = e
289
+ else registry.push(e)
321
290
  }
322
291
  }
323
292
 
@@ -8,19 +8,8 @@ import { Pairing } from '@cero-base/core/pairing'
8
8
  import { Invite } from '@cero-base/core/invite'
9
9
 
10
10
  /**
11
- * `me.bluetooth` — the app-facing surface for nearby (Bluetooth) sync, a thin
12
- * facade over ble-swarm. Bluetooth only changes how peers meet and carry
13
- * bytes; capability-gated replication still decides what syncs. Discovery is
14
- * one topic-derived service UUID at a time (tag `cero-ble`) — the data service
15
- * sits on a fixed per-tag UUID, so switching topics only retunes the radio.
16
- *
17
- * ```js
18
- * const me = await cero(dir, spec, { channel, bluetooth: true })
19
- * me.bluetooth.state // 'unsupported' | 'unauthorized' | 'off' | 'waiting' | 'starting' | 'on'
20
- * await me.bluetooth.start()
21
- * me.bluetooth.peers // Map of live BLE links
22
- * me.bluetooth.on('update', () => {})
23
- * ```
11
+ * `me.bluetooth` — the app-facing surface for nearby (Bluetooth) sync, a thin facade over
12
+ * ble-swarm.
24
13
  *
25
14
  * @extends ReadyResource
26
15
  */
@@ -43,14 +32,11 @@ export class Bluetooth extends ReadyResource {
43
32
  this._restorePending = false
44
33
 
45
34
  const identity = handle.identity
46
- // the mesh topic: one per channel, or a fixed global topic when
47
- // channelless — any nearby cero device links (global nearby; strangers
48
- // still sync zero bytes, replication is capability-gated)
35
+ // one mesh topic per channel, or a global one when channelless; strangers still sync nothing
49
36
  this._topic = crypto.hash(b4a.from(handle.network.channel || 'cero-ble'))
50
37
  this.swarm = new BluetoothSwarm({
51
38
  backend,
52
- // injected links authenticate with the same long-lived identity as the
53
- // swarm, so one person reached over Wi-Fi and BLE dedupes to one peer
39
+ // the swarm identity, so one person over Wi-Fi and BLE dedupes to one peer
54
40
  keyPair: { publicKey: identity.publicKey, secretKey: identity.secretKey },
55
41
  topic: this._topic,
56
42
  tag: 'cero-ble',
@@ -59,9 +45,7 @@ export class Bluetooth extends ReadyResource {
59
45
  maxInbound
60
46
  })
61
47
  this.swarm.on('update', () => {
62
- // an ended rendezvous retunes back to the mesh only once its links
63
- // drain — retuning drops links, and the pairing link still carries
64
- // the joiner's initial replication
48
+ // retune to the mesh only once the pairing link's initial replication drained
65
49
  if (this._restorePending && this.swarm.peers.size === 0) {
66
50
  this._restorePending = false
67
51
  this.swarm.setTopic(this._topic).catch(safetyCatch)
@@ -69,7 +53,6 @@ export class Bluetooth extends ReadyResource {
69
53
  this.emit('update')
70
54
  })
71
55
  this.swarm.on('connection', (conn) => {
72
- // the same treatment as any transport: wakeup, replication, pairing
73
56
  this._handle.network.inject(conn)
74
57
  })
75
58
  }
@@ -88,6 +71,11 @@ export class Bluetooth extends ReadyResource {
88
71
  if (this._autoStart) await this.start()
89
72
  }
90
73
 
74
+ async _close() {
75
+ this._clearAnnounce()
76
+ await this.swarm.destroy()
77
+ }
78
+
91
79
  /**
92
80
  * Begin advertising + scanning. Idempotent; no-op when unsupported.
93
81
  *
@@ -98,34 +86,20 @@ export class Bluetooth extends ReadyResource {
98
86
  }
99
87
 
100
88
  /**
101
- * Stop advertising/scanning and drop links; open invite rendezvous end with
102
- * the radio. Idempotent. Local data and the rest of the network (DHT) are
103
- * untouched.
89
+ * Stop advertising/scanning and drop links; open invite rendezvous end with the radio.
104
90
  *
105
91
  * @returns {Promise<void>}
106
92
  */
107
93
  async stop() {
108
94
  this._clearAnnounce()
109
95
  await this.swarm.stop()
110
- // while stopped setTopic just sticks the next start() is back on the
111
- // mesh. A start() + announce() that landed during the await wins.
96
+ // while stopped setTopic sticks; a start() + announce() during the await wins
112
97
  if (!this._announce) await this.swarm.setTopic(this._topic)
113
98
  }
114
99
 
115
100
  /**
116
- * Offline join rendezvous: retune the radio to the invite-derived topic so
117
- * holder and joiner find each other with zero DHT. One topic at a time —
118
- * announcing a new invite replaces the previous rendezvous. Returns a stop
119
- * function — closing the QR must stop the rendezvous so a photographed
120
- * invite doesn't stay an ambient discovery beacon (admission itself is
121
- * always gated by blind-pairing verifying the invite). The retune back to
122
- * the mesh topic waits for live links to drain: the link a join just
123
- * established survives and carries the joiner's initial replication.
124
- * Auto-stops at the invite's expiry, on `stop()`, and on close.
125
- *
126
- * Only active while nearby sync is on: before `start()` (and after `stop()`)
127
- * this is a no-op — the user controls the radio, and a join must not touch
128
- * Bluetooth (OS permissions, GATT server) they never enabled.
101
+ * Offline join rendezvous: retune the radio to the invite-derived topic so holder and
102
+ * joiner find each other with zero DHT.
129
103
  *
130
104
  * @param {string} invite Z32 invite string.
131
105
  * @returns {() => void}
@@ -161,20 +135,6 @@ export class Bluetooth extends ReadyResource {
161
135
  }
162
136
  }
163
137
 
164
- _stopAnnounce() {
165
- this._clearAnnounce()
166
- if (this.swarm.peers.size > 0) this._restorePending = true
167
- else this.swarm.setTopic(this._topic).catch(safetyCatch)
168
- }
169
-
170
- _clearAnnounce() {
171
- const e = this._announce
172
- if (!e) return
173
- if (e.timer) clearTimeout(e.timer)
174
- this._announce = null
175
- this._restorePending = false
176
- }
177
-
178
138
  /**
179
139
  * Host-lifecycle pause (app backgrounded): radio down, user intent kept.
180
140
  *
@@ -193,8 +153,17 @@ export class Bluetooth extends ReadyResource {
193
153
  await this.swarm.resume()
194
154
  }
195
155
 
196
- async _close() {
156
+ _stopAnnounce() {
197
157
  this._clearAnnounce()
198
- await this.swarm.destroy()
158
+ if (this.swarm.peers.size > 0) this._restorePending = true
159
+ else this.swarm.setTopic(this._topic).catch(safetyCatch)
160
+ }
161
+
162
+ _clearAnnounce() {
163
+ const e = this._announce
164
+ if (!e) return
165
+ if (e.timer) clearTimeout(e.timer)
166
+ this._announce = null
167
+ this._restorePending = false
199
168
  }
200
169
  }
@@ -1,5 +1,3 @@
1
- // Shared cero constants.
2
-
3
1
  export const NS = 'cero'
4
2
  export const COUNTERS = 'counters'
5
3
  export const EPOCHS = 'epochs'
@@ -7,16 +5,3 @@ export const EPOCHS = 'epochs'
7
5
  // Writer-admission / pairing timeout (ms), and the initial discovery-flush wait (ms).
8
6
  export const TIMEOUT = 30000
9
7
  export const FLUSH = 500
10
-
11
- // schema-DSL primitive → HyperDB type (used by the builder).
12
- export const DB_TYPE = {
13
- string: 'string',
14
- uint: 'uint',
15
- int: 'int',
16
- bool: 'bool',
17
- bytes: 'buffer',
18
- json: 'json',
19
- fixed32: 'fixed32',
20
- fixed64: 'fixed64',
21
- file: 'string'
22
- }
@@ -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 })
package/src/lib/refs.js CHANGED
@@ -9,9 +9,7 @@ import { CeroError } from '@cero-base/core/errors'
9
9
  */
10
10
 
11
11
  /**
12
- * Typed pointer to a single ref (table or handle slot) on a `Handle` or
13
- * `Local`. Operators (`put`/`get`/`open`/...) take a `Ref` as their first
14
- * argument and dispatch through the owning handle's store.
12
+ * Typed pointer to a single ref (table or handle slot) on a `Handle` or `Local`.
15
13
  */
16
14
  export class Ref {
17
15
  /**
@@ -36,8 +34,7 @@ export class Ref {
36
34
  */
37
35
  static attach(target, refs) {
38
36
  for (const [name, info] of Object.entries(refs || {})) {
39
- // fail loud rather than silently overwrite a method or property (close,
40
- // on, store) when a schema declares a ref named like a reserved member
37
+ // a ref named like a reserved member must fail loud, not overwrite it
41
38
  if (name in target) {
42
39
  throw CeroError.INVALID(
43
40
  `schema ref '${name}' collides with a reserved ${target.constructor?.name || 'handle'} member — rename it`
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'
@@ -13,9 +13,8 @@ import { Ref } from '../lib/refs.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
  /**