@cero-base/cero 0.6.0 → 0.7.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
@@ -173,6 +173,97 @@ await cero.open(me.room, { id: existingRoomId }) // load an existing room by id
173
173
 
174
174
  Returns the child handle, ready to operate on (`cero.put(joined.messages, …)` etc.).
175
175
 
176
+ ### `cero.before(ref, fn)` / `cero.after(ref, fn)`
177
+
178
+ Hook into writes to a ref. `before` runs **in-path** before the write commits — return `false` to cancel, or mutate `ctx.row`. `after` is a non-blocking **event** that fires once the write has committed. Both return an unsubscribe fn — call it to stop early, or ignore it for a subscription that should live as long as the handle (it's dropped when the handle closes).
179
+
180
+ ```js
181
+ const off = cero.before(room.messages, (ctx) => {
182
+ if (!ctx.row.text?.trim()) return false // reject empty messages
183
+ })
184
+
185
+ cero.after(me.profile, ({ row }) => {
186
+ /* react to your profile changing */
187
+ })
188
+ ```
189
+
190
+ `ctx` is `{ op, name, row }` (plus `result` in `after`).
191
+
192
+ ## Extensions
193
+
194
+ An extension bundles **schema** (build-time) with **behavior** (runtime). Register it with `cero.use()` — `build()` folds in its schema and `cero()` runs its `setup` once the handle is ready. cero core stays schema-agnostic; apps opt into the behaviors they want.
195
+
196
+ ```js
197
+ import { profileSync } from '@cero-base/cero/extensions'
198
+
199
+ cero.use(profileSync()) // before build() (for the schema) and before cero() (for the behavior)
200
+ await build('./spec', schema)
201
+ ```
202
+
203
+ An extension is two optional parts:
204
+
205
+ ```js
206
+ function myExtension() {
207
+ return {
208
+ schema: { members: cero.t.extend({ status: cero.t.string }) }, // merged into your schema
209
+ setup(me) {
210
+ me.on('handle', (room, opts) => {
211
+ /* opts carries the open args (e.g. opts.name on create); me.children is the set of open rooms */
212
+ })
213
+ }
214
+ }
215
+ }
216
+ ```
217
+
218
+ For a behavior-only extension (no schema), pass a function directly — it's shorthand for `{ setup }`:
219
+
220
+ ```js
221
+ cero.use((me) => {
222
+ me.on('handle', (room) => {
223
+ /* … */
224
+ })
225
+ })
226
+ ```
227
+
228
+ Build and the running app are separate processes, so `cero.use()` runs in both — your build script (for the schema) and once at app startup before `cero()` (for the behavior).
229
+
230
+ ### Cleanup
231
+
232
+ `setup(me)` may return a disposer; cero calls it on `me.close()`. You rarely need it — `before`/`after` and `me.on(...)` registered on `me` or its rooms are torn down automatically when the handle closes. Return a disposer **only** for things the handle's close won't clean up, like a timer or an external connection:
233
+
234
+ ```js
235
+ setup(me) {
236
+ const timer = setInterval(() => ping(me), 30_000)
237
+ return () => clearInterval(timer)
238
+ }
239
+ ```
240
+
241
+ ### `profileSync` (bundled)
242
+
243
+ Mirrors your `profile` onto your `member` row in every room, so others see your name and avatar. It **declares its own `profile` single** (`name` + the synced fields) and extends `member` with them — no app schema required. Defaults to syncing `avatar`:
244
+
245
+ ```js
246
+ cero.use(profileSync())
247
+ cero.use(profileSync({ fields: { status: cero.t.string } })) // sync extra fields
248
+ ```
249
+
250
+ Want a richer profile (e.g. a `bio` that doesn't sync)? Declare your own `profile` single — the app schema wins, and `profileSync` still adds the member side.
251
+
252
+ ### `handleSync` (bundled)
253
+
254
+ Mirrors a child handle's `profile` (name + avatar) onto its row in the parent's `handles` list — so a handle/room list renders names and photos without opening each handle. Adds `avatar` (or your `fields`) to the `handle` builtin and reflects the handle's own `profile` (which your app owns) onto the row.
255
+
256
+ ```js
257
+ cero.use(handleSync())
258
+
259
+ // your handle type declares a `profile`; your app sets it:
260
+ const room = await cero.open(me.room)
261
+ await cero.set(room.profile, { name: 'general', avatar: 'pic.png' })
262
+ // → the me.handles row now carries name + avatar — render the room list, no opens
263
+ ```
264
+
265
+ Requires your handle types to declare a `profile` single; handles without one are left untouched.
266
+
176
267
  ## RPC
177
268
 
178
269
  cero runs in one process; your UI runs in another. Connect them with any duplex stream (Bare IPC, Electron `contextBridge`, a worker port, even a TCP socket).
@@ -263,12 +354,13 @@ const remote = await connect(c, spec)
263
354
 
264
355
  ## Exports
265
356
 
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 |
357
+ | Path | What you get |
358
+ | ---------------------------- | ------------------------------------------------------------------------ |
359
+ | `@cero-base/cero` | factory + operators + schema DSL |
360
+ | `@cero-base/cero/client` | `connect(ipc, spec)` — talk to a cero running in another process |
361
+ | `@cero-base/cero/server` | `serve(ipc, { storage, spec })` — run + expose a cero over an IPC stream |
362
+ | `@cero-base/cero/build` | `build(specDir, schema)` — generate the on-disk spec |
363
+ | `@cero-base/cero/extensions` | bundled extensions (`profileSync`, …) |
272
364
 
273
365
  ## Tests
274
366
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cero-base/cero",
3
- "version": "0.6.0",
3
+ "version": "0.7.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",
@@ -30,6 +30,10 @@
30
30
  "./client": {
31
31
  "types": "./types/rpc/client.d.ts",
32
32
  "default": "./src/rpc/client.js"
33
+ },
34
+ "./extensions": {
35
+ "types": "./types/extensions/index.d.ts",
36
+ "default": "./src/extensions/index.js"
33
37
  }
34
38
  },
35
39
  "typesVersions": {
@@ -42,6 +46,9 @@
42
46
  ],
43
47
  "client": [
44
48
  "types/rpc/client.d.ts"
49
+ ],
50
+ "extensions": [
51
+ "types/extensions/index.d.ts"
45
52
  ]
46
53
  }
47
54
  },
@@ -67,7 +74,7 @@
67
74
  "test": "ls test/*.test.js | xargs -P2 -n1 brittle-node"
68
75
  },
69
76
  "dependencies": {
70
- "@cero-base/core": "^0.6.0",
77
+ "@cero-base/core": "^0.7.0",
71
78
  "b4a": "^1.8.1",
72
79
  "bare-crypto": "^1.13.7",
73
80
  "bare-fs": "^4.7.1",
@@ -9,6 +9,7 @@ import HRPCBuilder from 'hrpc'
9
9
  import { CeroError } from '@cero-base/core/errors'
10
10
 
11
11
  import { NS } from '../lib/constants.js'
12
+ import { internal } from '../lib/internal.js'
12
13
  import {
13
14
  refs,
14
15
  builtinRefs,
@@ -45,19 +46,23 @@ export async function build(specDir, schema, { ns = NS } = {}) {
45
46
  const raw = /** @type {SchemaDefs & { local?: SchemaDefs }} */ (schema?.defs || schema)
46
47
  if (!raw || typeof raw !== 'object') throw CeroError.REQUIRED('schema')
47
48
 
48
- // Pull out `t.extend(...)` entries (keyed by builtin ref name) and map them
49
- // to the builtin's type name, to merge into builtin types across every scope.
49
+ // Pull out `t.extend(...)` entries (keyed by builtin ref name) builtin type
50
+ // name, merged across scopes. App schema first, then each `cero.use()` extension.
50
51
  const extend = {}
51
52
  const defs = {}
52
- for (const [k, v] of Object.entries(raw)) {
53
- if (v && v.kind === 'extend') {
54
- const type = refs.main[k]?.type
55
- if (!type) throw CeroError.INVALID(`'${k}' is not an extendable builtin`)
56
- extend[type] = v.fields
57
- } else {
58
- defs[k] = v
53
+ const collect = (entries, fromExt = false) => {
54
+ for (const [k, v] of Object.entries(entries)) {
55
+ if (v && v.kind === 'extend') {
56
+ const type = refs.main[k]?.type
57
+ if (!type) throw CeroError.INVALID(`'${k}' is not an extendable builtin`)
58
+ extend[type] = { ...extend[type], ...v.fields }
59
+ } else if (!fromExt || !(k in defs)) {
60
+ defs[k] = v // app schema wins over an extension's default
61
+ }
59
62
  }
60
63
  }
64
+ collect(raw)
65
+ for (const ext of internal.extensions) if (ext.schema) collect(ext.schema, true)
61
66
 
62
67
  const main = compile(splitMain(defs), ns, 'main')
63
68
  const local = compile(defs.local || {}, ns, 'local')
@@ -0,0 +1,32 @@
1
+ import { t } from '../lib/spec.js'
2
+ import { set, watch } from '../lib/operators.js'
3
+
4
+ /**
5
+ * Mirror a child handle's `profile` (name + avatar) onto its row in the parent's
6
+ * `handles` list — so a handle list shows names + avatars without opening each
7
+ * one. Adds the synced `fields` to the `handle` builtin.
8
+ *
9
+ * On create it seeds the handle's `profile.name` from the open `{ name }`, then
10
+ * reflects the (app-owned) `profile` onto the row. Requires your handle types to
11
+ * declare a `profile` single — handles without one are left untouched.
12
+ *
13
+ * @param {{ fields?: Record<string, any> }} [opts]
14
+ */
15
+ export function handleSync({ fields = { avatar: t.string } } = {}) {
16
+ return {
17
+ schema: { handles: t.extend(fields) },
18
+ setup(me) {
19
+ me.on('handle', (child, opts) => {
20
+ if (!child.profile) return
21
+ if (opts.name) set(child.profile, { name: opts.name })
22
+ const sub = watch(child.profile)
23
+ sub.on('data', ({ data }) => {
24
+ if (!data?.name) return
25
+ child.name = data.name
26
+ set(me.handles, { id: child.id, ...data }, { upsert: false })
27
+ })
28
+ child.once('close', () => sub.destroy())
29
+ })
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,2 @@
1
+ export { profileSync } from './profile-sync.js'
2
+ export { handleSync } from './handle-sync.js'
@@ -0,0 +1,30 @@
1
+ import { t } from '../lib/spec.js'
2
+ import { get, set, after } from '../lib/operators.js'
3
+
4
+ /**
5
+ * Mirror your `profile` onto your `member` row in every room. Declares a
6
+ * `profile` single (`name`, `avatar`, plus any extra `fields`) and mirrors them
7
+ * onto the `member` builtin, then publishes when you open/join a room and whenever
8
+ * your profile changes. An app may declare its own richer `profile` instead —
9
+ * the app schema wins.
10
+ *
11
+ * @param {{ fields?: Record<string, any> }} [opts]
12
+ */
13
+ export function profileSync({ fields = { avatar: t.string } } = {}) {
14
+ return {
15
+ schema: {
16
+ profile: t.single({ name: t.string, ...fields }),
17
+ members: t.extend(fields)
18
+ },
19
+ setup(me) {
20
+ const publish = async (room) => {
21
+ const { data: member } = await get(room.members, me.identity.id)
22
+ if (!member) return
23
+ const { data: profile } = await get(me.profile)
24
+ if (profile) await set(room.members, { id: me.identity.id, ...profile })
25
+ }
26
+ me.on('handle', publish)
27
+ after(me.profile, () => me.children.forEach(publish))
28
+ }
29
+ }
30
+ }
@@ -137,16 +137,9 @@ export class Handle extends ReadyResource {
137
137
  await this.pair.ready()
138
138
  }
139
139
  attachRefs(this, this.store.refs)
140
- if (!this.children) return
141
-
142
- this._offUpdate = this.store.onUpdate(() => {
143
- if (this.closing || this.closed) return
144
- for (const c of this.children) this._syncMember(c).catch(this._onerror)
145
- })
146
140
  }
147
141
 
148
142
  async _close() {
149
- this._offUpdate?.()
150
143
  if (this.children) {
151
144
  for (const c of [...this.children]) await c.close()
152
145
  this.children.clear()
@@ -340,7 +333,6 @@ export class Handle extends ReadyResource {
340
333
  createdAt: ts,
341
334
  updatedAt: ts
342
335
  })
343
- if (name && child.profile) await child.store.set('profile', { name })
344
336
  await this.store.call('add-handle', {
345
337
  id,
346
338
  type,
@@ -353,7 +345,7 @@ export class Handle extends ReadyResource {
353
345
 
354
346
  if (accept !== false) this._wireAccept(child, { role })
355
347
  this.children.add(child)
356
- await this._syncMember(child)
348
+ this.emit('handle', child, { name, role })
357
349
  return child
358
350
  }
359
351
 
@@ -396,20 +388,19 @@ export class Handle extends ReadyResource {
396
388
  const id = toId(child.store.key)
397
389
  await this._saveKeyPair(id, child.store.keyPair)
398
390
 
399
- const name = child.profile ? await waitForProfileName(child, deadline) : null
400
391
  const ts = Date.now()
401
392
  await this.store.call('add-handle', {
402
393
  id,
403
394
  type,
404
395
  key: child.store.key,
405
396
  encryptionKey: child.store.encryptionKey,
406
- name,
397
+ name: null,
407
398
  createdAt: ts,
408
399
  updatedAt: ts
409
400
  })
410
401
  this._wireAccept(child)
411
402
  this.children.add(child)
412
- await this._syncMember(child)
403
+ this.emit('handle', child, {})
413
404
  return child
414
405
  }
415
406
 
@@ -452,6 +443,7 @@ export class Handle extends ReadyResource {
452
443
  }
453
444
  this._wireAccept(child)
454
445
  this.children.add(child)
446
+ this.emit('handle', child, {})
455
447
  return child
456
448
  }
457
449
 
@@ -534,20 +526,6 @@ export class Handle extends ReadyResource {
534
526
  })
535
527
  }
536
528
 
537
- async _syncMember(child) {
538
- if (!child.store.writable) return
539
- const { data: profile } = await this.store.get('profile')
540
- if (!profile) return
541
- const { data: existing } = await child.store.get('members', this.identity.id)
542
- if (!existing) return
543
- await child.store.call('set-member', {
544
- ...existing,
545
- ...profile,
546
- id: existing.id,
547
- updatedAt: Date.now()
548
- })
549
- }
550
-
551
529
  /**
552
530
  * @param {Handle} child
553
531
  * @param {{ role?: string }} [opts]
@@ -597,13 +575,3 @@ function pickHandle(spec, type) {
597
575
  function randomNs() {
598
576
  return z32.encode(Identity.randomBytes(8))
599
577
  }
600
-
601
- async function waitForProfileName(child, timeout) {
602
- const deadline = Date.now() + timeout
603
- while (Date.now() < deadline) {
604
- const { data } = await child.store.get('profile')
605
- if (data?.name) return data.name
606
- await new Promise((r) => setTimeout(r, 100))
607
- }
608
- return null
609
- }
package/src/index.js CHANGED
@@ -8,13 +8,14 @@ import { CeroError } from '@cero-base/core/errors'
8
8
 
9
9
  import { Handle, Ref } from './handle/index.js'
10
10
  import { Local } from './local/index.js'
11
- import { put, set, get, del, count, watch, call, open } from './lib/operators.js'
11
+ import { put, set, get, del, count, watch, call, open, before, after } from './lib/operators.js'
12
12
  import { peek } from './lib/peek.js'
13
13
  import { t, schema } from './lib/spec.js'
14
14
  import { FLUSH } from './lib/constants.js'
15
+ import { internal } from './lib/internal.js'
15
16
 
16
17
  export { Handle, Ref, Local }
17
- export { put, set, get, del, count, watch, call, open } from './lib/operators.js'
18
+ export { put, set, get, del, count, watch, call, open, before, after } from './lib/operators.js'
18
19
  export { peek } from './lib/peek.js'
19
20
  export { t, schema } from './lib/spec.js'
20
21
 
@@ -112,6 +113,11 @@ export async function cero(dir, spec, opts = {}) {
112
113
  }
113
114
  if (opts.recovery) await me.recover({ timeout: opts.recoveryTimeout })
114
115
 
116
+ for (const ext of internal.extensions) {
117
+ const off = await ext.setup?.(me)
118
+ if (typeof off === 'function') me.once('close', off)
119
+ }
120
+
115
121
  return me
116
122
  }
117
123
 
@@ -150,7 +156,26 @@ export async function restore(me, phrase) {
150
156
  })
151
157
  }
152
158
 
153
- Object.assign(cero, { put, set, get, del, count, watch, call, open, peek, restore, t, schema })
159
+ Object.assign(cero, {
160
+ t,
161
+ put,
162
+ set,
163
+ get,
164
+ del,
165
+ count,
166
+ watch,
167
+ call,
168
+ open,
169
+ before,
170
+ after,
171
+ peek,
172
+ restore,
173
+ schema
174
+ })
175
+ cero._internal = internal
176
+ // A bare function is shorthand for a behavior-only extension: `{ setup: fn }`.
177
+ cero.use = (...exts) =>
178
+ internal.extensions.push(...exts.map((e) => (typeof e === 'function' ? { setup: e } : e)))
154
179
 
155
180
  async function resolveIdentity(opts, local) {
156
181
  if (opts.identity) return opts.identity
@@ -0,0 +1,3 @@
1
+ // Process-wide registry for extensions registered via `cero.use()`.
2
+ // `build()` folds in each extension's schema; `cero()` runs each setup.
3
+ export const internal = { extensions: [] }
@@ -18,13 +18,15 @@ export const put = (ref, row) => ref.handle.store.put(ref.name, row)
18
18
 
19
19
  /**
20
20
  * Upsert a row on `ref` — merges with the existing row and preserves
21
- * `createdAt`.
21
+ * `createdAt`. Pass `{ upsert: false }` to update-only: a missing row is left
22
+ * untouched instead of created (atomic — never resurrects a deleted row).
22
23
  *
23
24
  * @param {Ref} ref
24
25
  * @param {Record<string, any>} row
26
+ * @param {{ upsert?: boolean }} [opts]
25
27
  * @returns {Promise<SingleResult>}
26
28
  */
27
- export const set = (ref, row) => ref.handle.store.set(ref.name, row)
29
+ export const set = (ref, row, opts) => ref.handle.store.set(ref.name, row, opts)
28
30
 
29
31
  /**
30
32
  * Delete a row by id (collection refs), or wipe the row (single refs).
@@ -53,6 +55,43 @@ export const count = (ref, q) => ref.handle.store.count(ref.name, q)
53
55
  */
54
56
  export const call = (ref, d) => ref.handle.store.call(ref.name, d)
55
57
 
58
+ // Write ops per ref kind, for `before`/`after` subscriptions.
59
+ const WRITES = { single: ['set'], collection: ['put', 'set', 'del'] }
60
+
61
+ /**
62
+ * Intercept writes to `ref` before they commit — `fn(ctx)` runs in-path
63
+ * (awaited). Return `false` to cancel the write, or mutate `ctx.row`.
64
+ * Returns an unsubscribe fn.
65
+ *
66
+ * @param {Ref} ref
67
+ * @param {(ctx: { op: string, name: string, row: any }) => any} fn
68
+ * @returns {() => void}
69
+ */
70
+ export const before = (ref, fn) => {
71
+ const db = ref.handle.store
72
+ const ops = WRITES[ref.kind] || ['set']
73
+ const offs = ops.map((op) =>
74
+ db.before(op, (ctx) => (ctx.name === ref.name ? fn(ctx) : undefined))
75
+ )
76
+ return () => offs.forEach((off) => off())
77
+ }
78
+
79
+ /**
80
+ * Subscribe to writes on `ref` — fires after each committed write,
81
+ * non-blocking (observe only). Returns an unsubscribe fn.
82
+ *
83
+ * @param {Ref} ref
84
+ * @param {(ctx: { op: string, name: string, row: any }) => void} fn
85
+ * @returns {() => void}
86
+ */
87
+ export const after = (ref, fn) => {
88
+ const db = ref.handle.store
89
+ const ops = WRITES[ref.kind] || ['set']
90
+ const handler = (ctx) => ctx.name === ref.name && fn(ctx)
91
+ for (const op of ops) db.on(`after:${op}`, handler)
92
+ return () => ops.forEach((op) => db.off(`after:${op}`, handler))
93
+ }
94
+
56
95
  // get/watch on a handle-kind ref list its rows from the `handles` collection
57
96
  // filtered by type (handle types are stored there with their { id, key,
58
97
  // encryptionKey, name }). Data-kind refs go straight to the store.
@@ -0,0 +1,22 @@
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.
9
+ *
10
+ * @param {{ fields?: Record<string, any> }} [opts]
11
+ */
12
+ export function handleSync({ fields }?: {
13
+ fields?: Record<string, any>;
14
+ }): {
15
+ schema: {
16
+ handles: {
17
+ kind: "extend";
18
+ fields: Record<string, import("@cero-base/core/schema").Prim>;
19
+ };
20
+ };
21
+ setup(me: any): void;
22
+ };
@@ -0,0 +1,2 @@
1
+ export { profileSync } from "./profile-sync.js";
2
+ export { handleSync } from "./handle-sync.js";
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Mirror your `profile` onto your `member` row in every room. Declares a
3
+ * `profile` single (`name`, `avatar`, plus any extra `fields`) and mirrors them
4
+ * onto the `member` builtin, then publishes when you open/join a room and whenever
5
+ * your profile changes. An app may declare its own richer `profile` instead —
6
+ * the app schema wins.
7
+ *
8
+ * @param {{ fields?: Record<string, any> }} [opts]
9
+ */
10
+ export function profileSync({ fields }?: {
11
+ fields?: Record<string, any>;
12
+ }): {
13
+ schema: {
14
+ profile: import("@cero-base/core/schema").TypeDef;
15
+ members: {
16
+ kind: "extend";
17
+ fields: Record<string, import("@cero-base/core/schema").Prim>;
18
+ };
19
+ };
20
+ setup(me: any): void;
21
+ };
@@ -89,7 +89,6 @@ export class Handle extends ReadyResource {
89
89
  store: Database;
90
90
  pair: Pairing;
91
91
  _wantsPair: boolean;
92
- _offUpdate: () => void;
93
92
  /** Canonical id — identity id for the root handle, store key for children. */
94
93
  get id(): any;
95
94
  /** This device's id + name. `null` on child handles. */
@@ -203,7 +202,6 @@ export class Handle extends ReadyResource {
203
202
  * @returns {Promise<void>}
204
203
  */
205
204
  resume(): Promise<void>;
206
- _syncMember(child: any): Promise<void>;
207
205
  /**
208
206
  * @param {Handle} child
209
207
  * @param {{ role?: string }} [opts]
package/types/index.d.ts CHANGED
@@ -25,6 +25,10 @@
25
25
  * @returns {Promise<Handle>}
26
26
  */
27
27
  export function cero(dir: string, spec: any, opts?: CeroOpts): Promise<Handle>;
28
+ export namespace cero {
29
+ export { internal as _internal };
30
+ export function use(...exts: any[]): number;
31
+ }
28
32
  /**
29
33
  * Restore a cero instance from a mnemonic phrase. Closes the running
30
34
  * instance, wipes the on-disk `main/` tree and re-opens with `recovery: true`
@@ -94,9 +98,10 @@ export type CeroOpts = {
94
98
  recoveryTimeout?: number;
95
99
  };
96
100
  import { Handle } from './handle/index.js';
101
+ import { internal } from './lib/internal.js';
97
102
  import { Ref } from './handle/index.js';
98
103
  import { Local } from './local/index.js';
99
104
  import { Identity } from '@cero-base/core/identity';
100
105
  export { Handle, Ref, Local };
101
- export { put, set, get, del, count, watch, call, open } from "./lib/operators.js";
106
+ export { put, set, get, del, count, watch, call, open, before, after } from "./lib/operators.js";
102
107
  export { t, schema } from "./lib/spec.js";
@@ -0,0 +1,3 @@
1
+ export namespace internal {
2
+ let extensions: any[];
3
+ }
@@ -1,10 +1,22 @@
1
1
  export function put(ref: Ref, row: Record<string, any>): Promise<SingleResult>;
2
- export function set(ref: Ref, row: Record<string, any>): Promise<SingleResult>;
2
+ export function set(ref: Ref, row: Record<string, any>, opts?: {
3
+ upsert?: boolean;
4
+ }): Promise<SingleResult>;
3
5
  export function del(ref: Ref, id?: string): Promise<void>;
4
6
  export function count(ref: Ref, q?: Record<string, any>): Promise<{
5
7
  data: number;
6
8
  }>;
7
9
  export function call(ref: Ref, d?: Record<string, any>): Promise<any>;
10
+ export function before(ref: Ref, fn: (ctx: {
11
+ op: string;
12
+ name: string;
13
+ row: any;
14
+ }) => any): () => void;
15
+ export function after(ref: Ref, fn: (ctx: {
16
+ op: string;
17
+ name: string;
18
+ row: any;
19
+ }) => void): () => void;
8
20
  export function get(ref: Ref, q?: string | Record<string, any>): Promise<SingleResult | ListResult | GetByIdResult>;
9
21
  export function watch(ref: Ref, q?: Record<string, any>): any;
10
22
  export function open(ref: Ref, arg?: string | {
package/src/CLAUDE.md DELETED
@@ -1,3 +0,0 @@
1
- <claude-mem-context>
2
-
3
- </claude-mem-context>
@@ -1,3 +0,0 @@
1
- <claude-mem-context>
2
-
3
- </claude-mem-context>
package/src/lib/CLAUDE.md DELETED
@@ -1,3 +0,0 @@
1
- <claude-mem-context>
2
-
3
- </claude-mem-context>