@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 +98 -6
- package/package.json +9 -2
- package/src/build/index.js +14 -9
- package/src/extensions/handle-sync.js +32 -0
- package/src/extensions/index.js +2 -0
- package/src/extensions/profile-sync.js +30 -0
- package/src/handle/index.js +4 -36
- package/src/index.js +28 -3
- package/src/lib/internal.js +3 -0
- package/src/lib/operators.js +41 -2
- package/types/extensions/handle-sync.d.ts +22 -0
- package/types/extensions/index.d.ts +2 -0
- package/types/extensions/profile-sync.d.ts +21 -0
- package/types/handle/index.d.ts +0 -2
- package/types/index.d.ts +6 -1
- package/types/lib/internal.d.ts +3 -0
- package/types/lib/operators.d.ts +13 -1
- package/src/CLAUDE.md +0 -3
- package/src/handle/CLAUDE.md +0 -3
- package/src/lib/CLAUDE.md +0 -3
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
|
|
267
|
-
|
|
|
268
|
-
| `@cero-base/cero`
|
|
269
|
-
| `@cero-base/cero/client`
|
|
270
|
-
| `@cero-base/cero/server`
|
|
271
|
-
| `@cero-base/cero/build`
|
|
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.
|
|
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.
|
|
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",
|
package/src/build/index.js
CHANGED
|
@@ -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)
|
|
49
|
-
//
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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,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
|
+
}
|
package/src/handle/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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, {
|
|
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
|
package/src/lib/operators.js
CHANGED
|
@@ -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,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
|
+
};
|
package/types/handle/index.d.ts
CHANGED
|
@@ -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";
|
package/types/lib/operators.d.ts
CHANGED
|
@@ -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
|
|
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
package/src/handle/CLAUDE.md
DELETED
package/src/lib/CLAUDE.md
DELETED