@cero-base/cero 0.6.0 → 0.8.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 +118 -9
- package/package.json +9 -2
- package/src/build/index.js +14 -9
- package/src/extensions/handle-sync.js +31 -0
- package/src/extensions/index.js +2 -0
- package/src/extensions/profile-sync.js +36 -0
- package/src/handle/index.js +58 -37
- package/src/index.js +28 -3
- package/src/lib/internal.js +3 -0
- package/src/lib/operators.js +66 -6
- package/src/lib/utils.js +13 -0
- package/types/build/schemas.d.ts +89 -89
- 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 +35 -2
- package/types/index.d.ts +6 -1
- package/types/lib/internal.d.ts +3 -0
- package/types/lib/operators.d.ts +20 -2
- package/types/lib/utils.d.ts +8 -0
- package/types/rpc/client.d.ts +1 -1
- /package/src/{handle → extensions}/CLAUDE.md +0 -0
package/README.md
CHANGED
|
@@ -140,16 +140,18 @@ const { data: n } = await cero.count(room.messages)
|
|
|
140
140
|
const { data: n } = await cero.count(room.messages, { gt: 'm-2025' })
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
-
### `cero.watch(ref, q?)`
|
|
143
|
+
### `cero.watch(ref, q?, opts?)`
|
|
144
144
|
|
|
145
|
-
A Readable stream that re-emits the latest snapshot on every change. Always emits an initial snapshot.
|
|
145
|
+
A Readable stream that re-emits the latest snapshot on every change. Always emits an initial snapshot. The stream is **tied to the ref's handle** — closing the handle destroys it, so a watch never outlives its store and you don't track cleanup. Pass `{ signal }` to bind it to a finer scope instead.
|
|
146
146
|
|
|
147
147
|
```js
|
|
148
148
|
const stream = cero.watch(room.messages, { limit: 50, reverse: true })
|
|
149
149
|
stream.on('data', ({ data, total, size }) => {
|
|
150
150
|
/* render */
|
|
151
151
|
})
|
|
152
|
-
// stream.destroy()
|
|
152
|
+
// stops on room.close(), on stream.destroy(), or on `signal` abort:
|
|
153
|
+
const ac = new AbortController()
|
|
154
|
+
cero.watch(room.messages, null, { signal: ac.signal }) // ac.abort() ⇒ destroyed
|
|
153
155
|
```
|
|
154
156
|
|
|
155
157
|
### `cero.call(actionRef, data)`
|
|
@@ -173,6 +175,112 @@ await cero.open(me.room, { id: existingRoomId }) // load an existing room by id
|
|
|
173
175
|
|
|
174
176
|
Returns the child handle, ready to operate on (`cero.put(joined.messages, …)` etc.).
|
|
175
177
|
|
|
178
|
+
### `cero.before(ref, fn, opts?)` / `cero.after(ref, fn, opts?)`
|
|
179
|
+
|
|
180
|
+
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, ignore it for a subscription that lives as long as the handle, or pass `{ signal }` to unsubscribe when an `AbortSignal` fires (e.g. `{ signal: me.signal }` to stop on close).
|
|
181
|
+
|
|
182
|
+
```js
|
|
183
|
+
const off = cero.before(room.messages, (ctx) => {
|
|
184
|
+
if (!ctx.row.text?.trim()) return false // reject empty messages
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
cero.after(me.profile, ({ row }) => {
|
|
188
|
+
/* react to your profile changing */
|
|
189
|
+
})
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
`ctx` is `{ op, name, row }` (plus `result` in `after`).
|
|
193
|
+
|
|
194
|
+
## Extensions
|
|
195
|
+
|
|
196
|
+
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.
|
|
197
|
+
|
|
198
|
+
```js
|
|
199
|
+
import { profileSync } from '@cero-base/cero/extensions'
|
|
200
|
+
|
|
201
|
+
cero.use(profileSync()) // before build() (for the schema) and before cero() (for the behavior)
|
|
202
|
+
await build('./spec', schema)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
An extension is two optional parts:
|
|
206
|
+
|
|
207
|
+
```js
|
|
208
|
+
function myExtension() {
|
|
209
|
+
return {
|
|
210
|
+
schema: { members: cero.t.extend({ status: cero.t.string }) }, // merged into your schema
|
|
211
|
+
setup(me) {
|
|
212
|
+
const onHandle = (room, opts) => {
|
|
213
|
+
/* opts carries the open args (e.g. opts.name on create); me.children is the set of open rooms */
|
|
214
|
+
}
|
|
215
|
+
me.on('handle', onHandle, { signal: me.signal }) // dropped on me.close()
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
For a behavior-only extension (no schema), pass a function directly — it's shorthand for `{ setup }`:
|
|
222
|
+
|
|
223
|
+
```js
|
|
224
|
+
cero.use((me) => {
|
|
225
|
+
me.on('handle', (room) => {
|
|
226
|
+
/* … */
|
|
227
|
+
})
|
|
228
|
+
})
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
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).
|
|
232
|
+
|
|
233
|
+
### Cleanup
|
|
234
|
+
|
|
235
|
+
You rarely write teardown — a handle's subscriptions follow the same "dies with the node" model as DOM events:
|
|
236
|
+
|
|
237
|
+
- **`cero.watch(ref)` streams** are destroyed automatically when the ref's handle closes.
|
|
238
|
+
- **`me.on(...)`, `before`, `after`** take `{ signal }` to drop themselves when an `AbortSignal` fires.
|
|
239
|
+
- **`me.signal`** is an `AbortSignal` that aborts on `me.close()`. Pass it to tie a subscription to the handle's life, or use your own `AbortController` for a finer scope (drop it before the handle closes):
|
|
240
|
+
|
|
241
|
+
```js
|
|
242
|
+
setup(me) {
|
|
243
|
+
me.on('handle', onHandle, { signal: me.signal }) // dropped when me closes
|
|
244
|
+
cero.after(me.profile, onProfile, { signal: me.signal })
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
For anything the close cascade won't reach — a timer, an external connection, a stream you made yourself — hand it to **`me.own(resource)`** (destroyed on close) or return a disposer from `setup`:
|
|
249
|
+
|
|
250
|
+
```js
|
|
251
|
+
setup(me) {
|
|
252
|
+
const timer = setInterval(() => ping(me), 30_000)
|
|
253
|
+
me.own({ destroy: () => clearInterval(timer) }) // tied to me.close()
|
|
254
|
+
// …or: return () => clearInterval(timer)
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### `profileSync` (bundled)
|
|
259
|
+
|
|
260
|
+
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`:
|
|
261
|
+
|
|
262
|
+
```js
|
|
263
|
+
cero.use(profileSync())
|
|
264
|
+
cero.use(profileSync({ fields: { status: cero.t.string } })) // sync extra fields
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
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.
|
|
268
|
+
|
|
269
|
+
### `handleSync` (bundled)
|
|
270
|
+
|
|
271
|
+
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.
|
|
272
|
+
|
|
273
|
+
```js
|
|
274
|
+
cero.use(handleSync())
|
|
275
|
+
|
|
276
|
+
// your handle type declares a `profile`; your app sets it:
|
|
277
|
+
const room = await cero.open(me.room)
|
|
278
|
+
await cero.set(room.profile, { name: 'general', avatar: 'pic.png' })
|
|
279
|
+
// → the me.handles row now carries name + avatar — render the room list, no opens
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Requires your handle types to declare a `profile` single; handles without one are left untouched.
|
|
283
|
+
|
|
176
284
|
## RPC
|
|
177
285
|
|
|
178
286
|
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 +371,13 @@ const remote = await connect(c, spec)
|
|
|
263
371
|
|
|
264
372
|
## Exports
|
|
265
373
|
|
|
266
|
-
| Path
|
|
267
|
-
|
|
|
268
|
-
| `@cero-base/cero`
|
|
269
|
-
| `@cero-base/cero/client`
|
|
270
|
-
| `@cero-base/cero/server`
|
|
271
|
-
| `@cero-base/cero/build`
|
|
374
|
+
| Path | What you get |
|
|
375
|
+
| ---------------------------- | ------------------------------------------------------------------------ |
|
|
376
|
+
| `@cero-base/cero` | factory + operators + schema DSL |
|
|
377
|
+
| `@cero-base/cero/client` | `connect(ipc, spec)` — talk to a cero running in another process |
|
|
378
|
+
| `@cero-base/cero/server` | `serve(ipc, { storage, spec })` — run + expose a cero over an IPC stream |
|
|
379
|
+
| `@cero-base/cero/build` | `build(specDir, schema)` — generate the on-disk spec |
|
|
380
|
+
| `@cero-base/cero/extensions` | bundled extensions (`profileSync`, …) |
|
|
272
381
|
|
|
273
382
|
## Tests
|
|
274
383
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cero-base/cero",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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.8.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,31 @@
|
|
|
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
|
+
const onHandle = (child, opts) => {
|
|
20
|
+
if (!child.profile) return
|
|
21
|
+
if (opts.name) set(child.profile, { name: opts.name }).catch(me._onerror)
|
|
22
|
+
watch(child.profile).on('data', ({ data }) => {
|
|
23
|
+
if (!data?.name) return
|
|
24
|
+
child.name = data.name
|
|
25
|
+
set(me.handles, { id: child.id, ...data }, { upsert: false }).catch(me._onerror)
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
me.on('handle', onHandle, { signal: me.signal })
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
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 handle you're in.
|
|
6
|
+
* Declares a `profile` single (`name`, `avatar`, plus any extra `fields`) and
|
|
7
|
+
* mirrors them onto the `member` builtin, then publishes when you open/join a
|
|
8
|
+
* handle and whenever your profile changes. An app may declare its own richer
|
|
9
|
+
* `profile` instead — 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 = (child, profile) =>
|
|
21
|
+
set(child.members, { id: me.identity.id, ...profile }, { upsert: false }).catch(me._onerror)
|
|
22
|
+
|
|
23
|
+
const onHandle = async (child) => {
|
|
24
|
+
const { data: profile } = await get(me.profile)
|
|
25
|
+
if (profile) publish(child, profile)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const onProfile = (ctx) => {
|
|
29
|
+
if (ctx.row) me.children.forEach((child) => publish(child, ctx.row))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
me.on('handle', onHandle, { signal: me.signal })
|
|
33
|
+
after(me.profile, onProfile, { signal: me.signal })
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/handle/index.js
CHANGED
|
@@ -13,7 +13,7 @@ import { CeroError } from '@cero-base/core/errors'
|
|
|
13
13
|
|
|
14
14
|
import { NS, TIMEOUT } from '../lib/constants.js'
|
|
15
15
|
|
|
16
|
-
import { attachRefs } from '../lib/utils.js'
|
|
16
|
+
import { attachRefs, onAbort } from '../lib/utils.js'
|
|
17
17
|
|
|
18
18
|
export { Ref } from '../lib/utils.js'
|
|
19
19
|
|
|
@@ -110,6 +110,7 @@ export class Handle extends ReadyResource {
|
|
|
110
110
|
this._opts = opts.opts || {}
|
|
111
111
|
this._onerror = this._opts.onerror || safetyCatch
|
|
112
112
|
this.children = parent ? null : new Set()
|
|
113
|
+
this._owned = new Set()
|
|
113
114
|
|
|
114
115
|
this.store = new Database({
|
|
115
116
|
store: store,
|
|
@@ -137,16 +138,11 @@ export class Handle extends ReadyResource {
|
|
|
137
138
|
await this.pair.ready()
|
|
138
139
|
}
|
|
139
140
|
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
141
|
}
|
|
147
142
|
|
|
148
143
|
async _close() {
|
|
149
|
-
this.
|
|
144
|
+
for (const r of [...this._owned]) r.destroy?.()
|
|
145
|
+
this._owned.clear()
|
|
150
146
|
if (this.children) {
|
|
151
147
|
for (const c of [...this.children]) await c.close()
|
|
152
148
|
this.children.clear()
|
|
@@ -168,6 +164,56 @@ export class Handle extends ReadyResource {
|
|
|
168
164
|
if (this._storage) await this._storage.close()
|
|
169
165
|
}
|
|
170
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Tie a destroyable resource (a `watch` stream, a timer, any `{ destroy }`)
|
|
169
|
+
* to this handle's lifecycle — it's destroyed automatically on close, so
|
|
170
|
+
* callers don't track cleanup. De-registers itself if destroyed earlier.
|
|
171
|
+
*
|
|
172
|
+
* @template {{ destroy?: Function, once?: Function }} T
|
|
173
|
+
* @param {T} resource
|
|
174
|
+
* @returns {T}
|
|
175
|
+
*/
|
|
176
|
+
own(resource) {
|
|
177
|
+
if (this.closing || this.closed) {
|
|
178
|
+
resource.destroy?.()
|
|
179
|
+
return resource
|
|
180
|
+
}
|
|
181
|
+
this._owned.add(resource)
|
|
182
|
+
resource.once?.('close', () => this._owned.delete(resource))
|
|
183
|
+
return resource
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* An `AbortSignal` that fires when this handle closes. Pass it as
|
|
188
|
+
* `{ signal }` to `on`/`after`/`before`/`watch` to drop a subscription on
|
|
189
|
+
* close — or use your own `AbortController` for a finer scope.
|
|
190
|
+
*
|
|
191
|
+
* @returns {AbortSignal}
|
|
192
|
+
*/
|
|
193
|
+
get signal() {
|
|
194
|
+
if (!this._ac) {
|
|
195
|
+
this._ac = new AbortController()
|
|
196
|
+
if (this.closed) this._ac.abort()
|
|
197
|
+
else this.once('close', () => this._ac.abort())
|
|
198
|
+
}
|
|
199
|
+
return this._ac.signal
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* `EventEmitter.on` plus an optional `{ signal }` that removes the listener
|
|
204
|
+
* when the signal aborts — e.g. `me.on('handle', fn, { signal: me.signal })`.
|
|
205
|
+
*
|
|
206
|
+
* @param {string} event
|
|
207
|
+
* @param {(...args: any[]) => void} fn
|
|
208
|
+
* @param {{ signal?: AbortSignal }} [opts]
|
|
209
|
+
* @returns {this}
|
|
210
|
+
*/
|
|
211
|
+
on(event, fn, opts) {
|
|
212
|
+
super.on(event, fn)
|
|
213
|
+
onAbort(opts?.signal, () => super.off(event, fn))
|
|
214
|
+
return this
|
|
215
|
+
}
|
|
216
|
+
|
|
171
217
|
/** Canonical id — identity id for the root handle, store key for children. */
|
|
172
218
|
get id() {
|
|
173
219
|
if (!this.parent) return this.identity.id
|
|
@@ -340,7 +386,6 @@ export class Handle extends ReadyResource {
|
|
|
340
386
|
createdAt: ts,
|
|
341
387
|
updatedAt: ts
|
|
342
388
|
})
|
|
343
|
-
if (name && child.profile) await child.store.set('profile', { name })
|
|
344
389
|
await this.store.call('add-handle', {
|
|
345
390
|
id,
|
|
346
391
|
type,
|
|
@@ -353,7 +398,7 @@ export class Handle extends ReadyResource {
|
|
|
353
398
|
|
|
354
399
|
if (accept !== false) this._wireAccept(child, { role })
|
|
355
400
|
this.children.add(child)
|
|
356
|
-
|
|
401
|
+
this.emit('handle', child, { name, role })
|
|
357
402
|
return child
|
|
358
403
|
}
|
|
359
404
|
|
|
@@ -396,20 +441,19 @@ export class Handle extends ReadyResource {
|
|
|
396
441
|
const id = toId(child.store.key)
|
|
397
442
|
await this._saveKeyPair(id, child.store.keyPair)
|
|
398
443
|
|
|
399
|
-
const name = child.profile ? await waitForProfileName(child, deadline) : null
|
|
400
444
|
const ts = Date.now()
|
|
401
445
|
await this.store.call('add-handle', {
|
|
402
446
|
id,
|
|
403
447
|
type,
|
|
404
448
|
key: child.store.key,
|
|
405
449
|
encryptionKey: child.store.encryptionKey,
|
|
406
|
-
name,
|
|
450
|
+
name: null,
|
|
407
451
|
createdAt: ts,
|
|
408
452
|
updatedAt: ts
|
|
409
453
|
})
|
|
410
454
|
this._wireAccept(child)
|
|
411
455
|
this.children.add(child)
|
|
412
|
-
|
|
456
|
+
this.emit('handle', child, {})
|
|
413
457
|
return child
|
|
414
458
|
}
|
|
415
459
|
|
|
@@ -452,6 +496,7 @@ export class Handle extends ReadyResource {
|
|
|
452
496
|
}
|
|
453
497
|
this._wireAccept(child)
|
|
454
498
|
this.children.add(child)
|
|
499
|
+
this.emit('handle', child, {})
|
|
455
500
|
return child
|
|
456
501
|
}
|
|
457
502
|
|
|
@@ -534,20 +579,6 @@ export class Handle extends ReadyResource {
|
|
|
534
579
|
})
|
|
535
580
|
}
|
|
536
581
|
|
|
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
582
|
/**
|
|
552
583
|
* @param {Handle} child
|
|
553
584
|
* @param {{ role?: string }} [opts]
|
|
@@ -597,13 +628,3 @@ function pickHandle(spec, type) {
|
|
|
597
628
|
function randomNs() {
|
|
598
629
|
return z32.encode(Identity.randomBytes(8))
|
|
599
630
|
}
|
|
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
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Readable } from 'streamx'
|
|
2
2
|
|
|
3
|
+
import { onAbort } from './utils.js'
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* @typedef {import('./utils.js').Ref} Ref
|
|
5
7
|
* @typedef {{ data: any }} SingleResult
|
|
@@ -18,13 +20,15 @@ export const put = (ref, row) => ref.handle.store.put(ref.name, row)
|
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
22
|
* Upsert a row on `ref` — merges with the existing row and preserves
|
|
21
|
-
* `createdAt`.
|
|
23
|
+
* `createdAt`. Pass `{ upsert: false }` to update-only: a missing row is left
|
|
24
|
+
* untouched instead of created (atomic — never resurrects a deleted row).
|
|
22
25
|
*
|
|
23
26
|
* @param {Ref} ref
|
|
24
27
|
* @param {Record<string, any>} row
|
|
28
|
+
* @param {{ upsert?: boolean }} [opts]
|
|
25
29
|
* @returns {Promise<SingleResult>}
|
|
26
30
|
*/
|
|
27
|
-
export const set = (ref, row) => ref.handle.store.set(ref.name, row)
|
|
31
|
+
export const set = (ref, row, opts) => ref.handle.store.set(ref.name, row, opts)
|
|
28
32
|
|
|
29
33
|
/**
|
|
30
34
|
* Delete a row by id (collection refs), or wipe the row (single refs).
|
|
@@ -53,6 +57,50 @@ export const count = (ref, q) => ref.handle.store.count(ref.name, q)
|
|
|
53
57
|
*/
|
|
54
58
|
export const call = (ref, d) => ref.handle.store.call(ref.name, d)
|
|
55
59
|
|
|
60
|
+
// Write ops per ref kind, for `before`/`after` subscriptions.
|
|
61
|
+
const WRITES = { single: ['set'], collection: ['put', 'set', 'del'] }
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Intercept writes to `ref` before they commit — `fn(ctx)` runs in-path
|
|
65
|
+
* (awaited). Return `false` to cancel the write, or mutate `ctx.row`.
|
|
66
|
+
* Returns an unsubscribe fn; pass `{ signal }` to unsubscribe on abort.
|
|
67
|
+
*
|
|
68
|
+
* @param {Ref} ref
|
|
69
|
+
* @param {(ctx: { op: string, name: string, row: any }) => any} fn
|
|
70
|
+
* @param {{ signal?: AbortSignal }} [opts]
|
|
71
|
+
* @returns {() => void}
|
|
72
|
+
*/
|
|
73
|
+
export const before = (ref, fn, opts) => {
|
|
74
|
+
const db = ref.handle.store
|
|
75
|
+
const ops = WRITES[ref.kind] || ['set']
|
|
76
|
+
const offs = ops.map((op) =>
|
|
77
|
+
db.before(op, (ctx) => (ctx.name === ref.name ? fn(ctx) : undefined))
|
|
78
|
+
)
|
|
79
|
+
const off = () => offs.forEach((unsub) => unsub())
|
|
80
|
+
onAbort(opts?.signal, off)
|
|
81
|
+
return off
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Subscribe to writes on `ref` — fires after each committed write,
|
|
86
|
+
* non-blocking (observe only). Returns an unsubscribe fn; pass `{ signal }`
|
|
87
|
+
* to unsubscribe on abort.
|
|
88
|
+
*
|
|
89
|
+
* @param {Ref} ref
|
|
90
|
+
* @param {(ctx: { op: string, name: string, row: any }) => void} fn
|
|
91
|
+
* @param {{ signal?: AbortSignal }} [opts]
|
|
92
|
+
* @returns {() => void}
|
|
93
|
+
*/
|
|
94
|
+
export const after = (ref, fn, opts) => {
|
|
95
|
+
const db = ref.handle.store
|
|
96
|
+
const ops = WRITES[ref.kind] || ['set']
|
|
97
|
+
const handler = (ctx) => ctx.name === ref.name && fn(ctx)
|
|
98
|
+
for (const op of ops) db.on(`after:${op}`, handler)
|
|
99
|
+
const off = () => ops.forEach((op) => db.off(`after:${op}`, handler))
|
|
100
|
+
onAbort(opts?.signal, off)
|
|
101
|
+
return off
|
|
102
|
+
}
|
|
103
|
+
|
|
56
104
|
// get/watch on a handle-kind ref list its rows from the `handles` collection
|
|
57
105
|
// filtered by type (handle types are stored there with their { id, key,
|
|
58
106
|
// encryptionKey, name }). Data-kind refs go straight to the store.
|
|
@@ -79,16 +127,28 @@ export const get = async (ref, q) => {
|
|
|
79
127
|
return normalize(all, ref.name)
|
|
80
128
|
}
|
|
81
129
|
|
|
130
|
+
// Tie a fresh watch stream to its handle's lifecycle (destroyed on close) and
|
|
131
|
+
// to an optional `{ signal }` (destroyed on abort). Local refs have no
|
|
132
|
+
// close-cascade, so they keep managing their own streams.
|
|
133
|
+
const bindStream = (owner, stream, opts) => {
|
|
134
|
+
onAbort(opts?.signal, () => stream.destroy())
|
|
135
|
+
return owner.own ? owner.own(stream) : stream
|
|
136
|
+
}
|
|
137
|
+
|
|
82
138
|
/**
|
|
83
139
|
* Live snapshot stream on `ref` — re-emits the latest `get()` result on
|
|
84
|
-
* every underlying mutation.
|
|
140
|
+
* every underlying mutation. Tied to `ref.handle`'s lifecycle: closing the
|
|
141
|
+
* handle destroys it. Pass `{ signal }` to bind it to a finer scope, or
|
|
142
|
+
* destroy the stream directly to stop watching sooner.
|
|
85
143
|
*
|
|
86
144
|
* @param {Ref} ref
|
|
87
145
|
* @param {Record<string, any>} [q]
|
|
146
|
+
* @param {{ signal?: AbortSignal }} [opts]
|
|
88
147
|
* @returns {import('streamx').Readable}
|
|
89
148
|
*/
|
|
90
|
-
export const watch = (ref, q) => {
|
|
91
|
-
|
|
149
|
+
export const watch = (ref, q, opts) => {
|
|
150
|
+
const owner = ref.handle
|
|
151
|
+
if (ref.kind !== 'handle') return bindStream(owner, owner.store.watch(ref.name, q), opts)
|
|
92
152
|
const source = parentStore(ref).watch('handles', q)
|
|
93
153
|
const out = new Readable({
|
|
94
154
|
destroy(cb) {
|
|
@@ -99,7 +159,7 @@ export const watch = (ref, q) => {
|
|
|
99
159
|
source.on('data', (snap) => out.push(normalize(snap?.data, ref.name)))
|
|
100
160
|
source.on('end', () => out.push(null))
|
|
101
161
|
source.on('error', (err) => out.destroy(err))
|
|
102
|
-
return out
|
|
162
|
+
return bindStream(owner, out, opts)
|
|
103
163
|
}
|
|
104
164
|
|
|
105
165
|
// Universal signal instantiator. Dispatch on the second arg:
|
package/src/lib/utils.js
CHANGED
|
@@ -39,3 +39,16 @@ export function attachRefs(target, refs) {
|
|
|
39
39
|
target[name] = new Ref(target, name, info.kind, info.schema)
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Run `cb` when `signal` aborts — or immediately if it already has. No-op
|
|
45
|
+
* without a signal. The listener removes itself on fire.
|
|
46
|
+
*
|
|
47
|
+
* @param {AbortSignal} [signal]
|
|
48
|
+
* @param {() => void} cb
|
|
49
|
+
*/
|
|
50
|
+
export function onAbort(signal, cb) {
|
|
51
|
+
if (!signal) return
|
|
52
|
+
if (signal.aborted) return cb()
|
|
53
|
+
signal.addEventListener('abort', cb, { once: true })
|
|
54
|
+
}
|
package/types/build/schemas.d.ts
CHANGED
|
@@ -1,153 +1,153 @@
|
|
|
1
1
|
export const main: {
|
|
2
2
|
'del-by-id': {
|
|
3
|
-
id: import("@cero-base/core
|
|
3
|
+
id: import("@cero-base/core").Prim;
|
|
4
4
|
};
|
|
5
5
|
writer: {
|
|
6
|
-
master: import("@cero-base/core
|
|
7
|
-
writer: import("@cero-base/core
|
|
8
|
-
sig: import("@cero-base/core
|
|
9
|
-
isIndexer: import("@cero-base/core
|
|
6
|
+
master: import("@cero-base/core").Prim;
|
|
7
|
+
writer: import("@cero-base/core").Prim;
|
|
8
|
+
sig: import("@cero-base/core").Prim;
|
|
9
|
+
isIndexer: import("@cero-base/core").Prim;
|
|
10
10
|
};
|
|
11
11
|
counter: {
|
|
12
|
-
name: import("@cero-base/core
|
|
13
|
-
value: import("@cero-base/core
|
|
12
|
+
name: import("@cero-base/core").Prim;
|
|
13
|
+
value: import("@cero-base/core").Prim;
|
|
14
14
|
};
|
|
15
15
|
member: {
|
|
16
|
-
id: import("@cero-base/core
|
|
17
|
-
key: import("@cero-base/core
|
|
18
|
-
role: import("@cero-base/core
|
|
19
|
-
name: import("@cero-base/core
|
|
20
|
-
createdAt: import("@cero-base/core
|
|
21
|
-
updatedAt: import("@cero-base/core
|
|
22
|
-
sig: import("@cero-base/core
|
|
23
|
-
index: import("@cero-base/core
|
|
16
|
+
id: import("@cero-base/core").Prim;
|
|
17
|
+
key: import("@cero-base/core").Prim;
|
|
18
|
+
role: import("@cero-base/core").Prim;
|
|
19
|
+
name: import("@cero-base/core").Prim;
|
|
20
|
+
createdAt: import("@cero-base/core").Prim;
|
|
21
|
+
updatedAt: import("@cero-base/core").Prim;
|
|
22
|
+
sig: import("@cero-base/core").Prim;
|
|
23
|
+
index: import("@cero-base/core").Prim;
|
|
24
24
|
};
|
|
25
25
|
device: {
|
|
26
|
-
id: import("@cero-base/core
|
|
27
|
-
memberId: import("@cero-base/core
|
|
28
|
-
name: import("@cero-base/core
|
|
29
|
-
isMobile: import("@cero-base/core
|
|
30
|
-
createdAt: import("@cero-base/core
|
|
31
|
-
updatedAt: import("@cero-base/core
|
|
32
|
-
index: import("@cero-base/core
|
|
26
|
+
id: import("@cero-base/core").Prim;
|
|
27
|
+
memberId: import("@cero-base/core").Prim;
|
|
28
|
+
name: import("@cero-base/core").Prim;
|
|
29
|
+
isMobile: import("@cero-base/core").Prim;
|
|
30
|
+
createdAt: import("@cero-base/core").Prim;
|
|
31
|
+
updatedAt: import("@cero-base/core").Prim;
|
|
32
|
+
index: import("@cero-base/core").Prim;
|
|
33
33
|
};
|
|
34
34
|
invite: {
|
|
35
|
-
id: import("@cero-base/core
|
|
36
|
-
invite: import("@cero-base/core
|
|
37
|
-
publicKey: import("@cero-base/core
|
|
38
|
-
data: import("@cero-base/core
|
|
39
|
-
sig: import("@cero-base/core
|
|
40
|
-
role: import("@cero-base/core
|
|
41
|
-
expires: import("@cero-base/core
|
|
42
|
-
createdAt: import("@cero-base/core
|
|
43
|
-
index: import("@cero-base/core
|
|
35
|
+
id: import("@cero-base/core").Prim;
|
|
36
|
+
invite: import("@cero-base/core").Prim;
|
|
37
|
+
publicKey: import("@cero-base/core").Prim;
|
|
38
|
+
data: import("@cero-base/core").Prim;
|
|
39
|
+
sig: import("@cero-base/core").Prim;
|
|
40
|
+
role: import("@cero-base/core").Prim;
|
|
41
|
+
expires: import("@cero-base/core").Prim;
|
|
42
|
+
createdAt: import("@cero-base/core").Prim;
|
|
43
|
+
index: import("@cero-base/core").Prim;
|
|
44
44
|
};
|
|
45
45
|
handle: {
|
|
46
|
-
id: import("@cero-base/core
|
|
47
|
-
type: import("@cero-base/core
|
|
48
|
-
key: import("@cero-base/core
|
|
49
|
-
encryptionKey: import("@cero-base/core
|
|
50
|
-
name: import("@cero-base/core
|
|
51
|
-
createdAt: import("@cero-base/core
|
|
52
|
-
updatedAt: import("@cero-base/core
|
|
53
|
-
index: import("@cero-base/core
|
|
46
|
+
id: import("@cero-base/core").Prim;
|
|
47
|
+
type: import("@cero-base/core").Prim;
|
|
48
|
+
key: import("@cero-base/core").Prim;
|
|
49
|
+
encryptionKey: import("@cero-base/core").Prim;
|
|
50
|
+
name: import("@cero-base/core").Prim;
|
|
51
|
+
createdAt: import("@cero-base/core").Prim;
|
|
52
|
+
updatedAt: import("@cero-base/core").Prim;
|
|
53
|
+
index: import("@cero-base/core").Prim;
|
|
54
54
|
};
|
|
55
55
|
claim: {
|
|
56
|
-
identity: import("@cero-base/core
|
|
57
|
-
writer: import("@cero-base/core
|
|
58
|
-
sig: import("@cero-base/core
|
|
56
|
+
identity: import("@cero-base/core").Prim;
|
|
57
|
+
writer: import("@cero-base/core").Prim;
|
|
58
|
+
sig: import("@cero-base/core").Prim;
|
|
59
59
|
};
|
|
60
60
|
};
|
|
61
61
|
export const local: {
|
|
62
62
|
master: {
|
|
63
|
-
seed: import("@cero-base/core
|
|
63
|
+
seed: import("@cero-base/core").Prim;
|
|
64
64
|
};
|
|
65
65
|
keypair: {
|
|
66
|
-
publicKey: import("@cero-base/core
|
|
67
|
-
secretKey: import("@cero-base/core
|
|
66
|
+
publicKey: import("@cero-base/core").Prim;
|
|
67
|
+
secretKey: import("@cero-base/core").Prim;
|
|
68
68
|
};
|
|
69
69
|
'handle-keypair': {
|
|
70
|
-
id: import("@cero-base/core
|
|
71
|
-
publicKey: import("@cero-base/core
|
|
72
|
-
secretKey: import("@cero-base/core
|
|
73
|
-
encryptionKey: import("@cero-base/core
|
|
70
|
+
id: import("@cero-base/core").Prim;
|
|
71
|
+
publicKey: import("@cero-base/core").Prim;
|
|
72
|
+
secretKey: import("@cero-base/core").Prim;
|
|
73
|
+
encryptionKey: import("@cero-base/core").Prim;
|
|
74
74
|
};
|
|
75
75
|
};
|
|
76
76
|
export const rpc: {
|
|
77
77
|
'req-empty': {
|
|
78
|
-
ok: import("@cero-base/core
|
|
78
|
+
ok: import("@cero-base/core").Prim;
|
|
79
79
|
};
|
|
80
80
|
'req-restore': {
|
|
81
|
-
phrase: import("@cero-base/core
|
|
81
|
+
phrase: import("@cero-base/core").Prim;
|
|
82
82
|
};
|
|
83
83
|
'req-row': {
|
|
84
|
-
handle: import("@cero-base/core
|
|
85
|
-
ref: import("@cero-base/core
|
|
86
|
-
data: import("@cero-base/core
|
|
87
|
-
local: import("@cero-base/core
|
|
84
|
+
handle: import("@cero-base/core").Prim;
|
|
85
|
+
ref: import("@cero-base/core").Prim;
|
|
86
|
+
data: import("@cero-base/core").Prim;
|
|
87
|
+
local: import("@cero-base/core").Prim;
|
|
88
88
|
};
|
|
89
89
|
'req-id': {
|
|
90
|
-
handle: import("@cero-base/core
|
|
91
|
-
ref: import("@cero-base/core
|
|
92
|
-
id: import("@cero-base/core
|
|
93
|
-
local: import("@cero-base/core
|
|
90
|
+
handle: import("@cero-base/core").Prim;
|
|
91
|
+
ref: import("@cero-base/core").Prim;
|
|
92
|
+
id: import("@cero-base/core").Prim;
|
|
93
|
+
local: import("@cero-base/core").Prim;
|
|
94
94
|
};
|
|
95
95
|
'req-query': {
|
|
96
|
-
handle: import("@cero-base/core
|
|
97
|
-
ref: import("@cero-base/core
|
|
98
|
-
query: import("@cero-base/core
|
|
99
|
-
local: import("@cero-base/core
|
|
96
|
+
handle: import("@cero-base/core").Prim;
|
|
97
|
+
ref: import("@cero-base/core").Prim;
|
|
98
|
+
query: import("@cero-base/core").Prim;
|
|
99
|
+
local: import("@cero-base/core").Prim;
|
|
100
100
|
};
|
|
101
101
|
'req-call': {
|
|
102
|
-
handle: import("@cero-base/core
|
|
103
|
-
op: import("@cero-base/core
|
|
104
|
-
data: import("@cero-base/core
|
|
102
|
+
handle: import("@cero-base/core").Prim;
|
|
103
|
+
op: import("@cero-base/core").Prim;
|
|
104
|
+
data: import("@cero-base/core").Prim;
|
|
105
105
|
};
|
|
106
106
|
'req-invite': {
|
|
107
|
-
handle: import("@cero-base/core
|
|
108
|
-
role: import("@cero-base/core
|
|
107
|
+
handle: import("@cero-base/core").Prim;
|
|
108
|
+
role: import("@cero-base/core").Prim;
|
|
109
109
|
};
|
|
110
110
|
'req-revoke': {
|
|
111
|
-
handle: import("@cero-base/core
|
|
112
|
-
invite: import("@cero-base/core
|
|
111
|
+
handle: import("@cero-base/core").Prim;
|
|
112
|
+
invite: import("@cero-base/core").Prim;
|
|
113
113
|
};
|
|
114
114
|
'req-join': {
|
|
115
|
-
parent: import("@cero-base/core
|
|
116
|
-
ref: import("@cero-base/core
|
|
117
|
-
invite: import("@cero-base/core
|
|
115
|
+
parent: import("@cero-base/core").Prim;
|
|
116
|
+
ref: import("@cero-base/core").Prim;
|
|
117
|
+
invite: import("@cero-base/core").Prim;
|
|
118
118
|
};
|
|
119
119
|
'req-open': {
|
|
120
|
-
parent: import("@cero-base/core
|
|
121
|
-
row: import("@cero-base/core
|
|
120
|
+
parent: import("@cero-base/core").Prim;
|
|
121
|
+
row: import("@cero-base/core").Prim;
|
|
122
122
|
};
|
|
123
123
|
'req-handle': {
|
|
124
|
-
handle: import("@cero-base/core
|
|
124
|
+
handle: import("@cero-base/core").Prim;
|
|
125
125
|
};
|
|
126
126
|
'res-data': {
|
|
127
|
-
data: import("@cero-base/core
|
|
127
|
+
data: import("@cero-base/core").Prim;
|
|
128
128
|
};
|
|
129
129
|
'res-rows': {
|
|
130
|
-
data: import("@cero-base/core
|
|
131
|
-
total: import("@cero-base/core
|
|
132
|
-
size: import("@cero-base/core
|
|
130
|
+
data: import("@cero-base/core").Prim;
|
|
131
|
+
total: import("@cero-base/core").Prim;
|
|
132
|
+
size: import("@cero-base/core").Prim;
|
|
133
133
|
};
|
|
134
134
|
'res-count': {
|
|
135
|
-
count: import("@cero-base/core
|
|
135
|
+
count: import("@cero-base/core").Prim;
|
|
136
136
|
};
|
|
137
137
|
'res-invite': {
|
|
138
|
-
invite: import("@cero-base/core
|
|
138
|
+
invite: import("@cero-base/core").Prim;
|
|
139
139
|
};
|
|
140
140
|
'res-handle': {
|
|
141
|
-
id: import("@cero-base/core
|
|
142
|
-
type: import("@cero-base/core
|
|
143
|
-
name: import("@cero-base/core
|
|
141
|
+
id: import("@cero-base/core").Prim;
|
|
142
|
+
type: import("@cero-base/core").Prim;
|
|
143
|
+
name: import("@cero-base/core").Prim;
|
|
144
144
|
};
|
|
145
145
|
'res-identity': {
|
|
146
|
-
id: import("@cero-base/core
|
|
147
|
-
deviceId: import("@cero-base/core
|
|
148
|
-
phrase: import("@cero-base/core
|
|
146
|
+
id: import("@cero-base/core").Prim;
|
|
147
|
+
deviceId: import("@cero-base/core").Prim;
|
|
148
|
+
phrase: import("@cero-base/core").Prim;
|
|
149
149
|
};
|
|
150
150
|
'res-ok': {
|
|
151
|
-
ok: import("@cero-base/core
|
|
151
|
+
ok: import("@cero-base/core").Prim;
|
|
152
152
|
};
|
|
153
153
|
};
|
|
@@ -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").Prim>;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
setup(me: any): void;
|
|
22
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mirror your `profile` onto your `member` row in every handle you're in.
|
|
3
|
+
* Declares a `profile` single (`name`, `avatar`, plus any extra `fields`) and
|
|
4
|
+
* mirrors them onto the `member` builtin, then publishes when you open/join a
|
|
5
|
+
* handle and whenever your profile changes. An app may declare its own richer
|
|
6
|
+
* `profile` instead — 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").TypeDef;
|
|
15
|
+
members: {
|
|
16
|
+
kind: "extend";
|
|
17
|
+
fields: Record<string, import("@cero-base/core").Prim>;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
setup(me: any): void;
|
|
21
|
+
};
|
package/types/handle/index.d.ts
CHANGED
|
@@ -86,10 +86,44 @@ export class Handle extends ReadyResource {
|
|
|
86
86
|
_opts: any;
|
|
87
87
|
_onerror: any;
|
|
88
88
|
children: Set<any>;
|
|
89
|
+
_owned: Set<any>;
|
|
89
90
|
store: Database;
|
|
90
91
|
pair: Pairing;
|
|
91
92
|
_wantsPair: boolean;
|
|
92
|
-
|
|
93
|
+
/**
|
|
94
|
+
* Tie a destroyable resource (a `watch` stream, a timer, any `{ destroy }`)
|
|
95
|
+
* to this handle's lifecycle — it's destroyed automatically on close, so
|
|
96
|
+
* callers don't track cleanup. De-registers itself if destroyed earlier.
|
|
97
|
+
*
|
|
98
|
+
* @template {{ destroy?: Function, once?: Function }} T
|
|
99
|
+
* @param {T} resource
|
|
100
|
+
* @returns {T}
|
|
101
|
+
*/
|
|
102
|
+
own<T extends {
|
|
103
|
+
destroy?: Function;
|
|
104
|
+
once?: Function;
|
|
105
|
+
}>(resource: T): T;
|
|
106
|
+
/**
|
|
107
|
+
* An `AbortSignal` that fires when this handle closes. Pass it as
|
|
108
|
+
* `{ signal }` to `on`/`after`/`before`/`watch` to drop a subscription on
|
|
109
|
+
* close — or use your own `AbortController` for a finer scope.
|
|
110
|
+
*
|
|
111
|
+
* @returns {AbortSignal}
|
|
112
|
+
*/
|
|
113
|
+
get signal(): AbortSignal;
|
|
114
|
+
_ac: AbortController;
|
|
115
|
+
/**
|
|
116
|
+
* `EventEmitter.on` plus an optional `{ signal }` that removes the listener
|
|
117
|
+
* when the signal aborts — e.g. `me.on('handle', fn, { signal: me.signal })`.
|
|
118
|
+
*
|
|
119
|
+
* @param {string} event
|
|
120
|
+
* @param {(...args: any[]) => void} fn
|
|
121
|
+
* @param {{ signal?: AbortSignal }} [opts]
|
|
122
|
+
* @returns {this}
|
|
123
|
+
*/
|
|
124
|
+
on(event: string, fn: (...args: any[]) => void, opts?: {
|
|
125
|
+
signal?: AbortSignal;
|
|
126
|
+
}): this;
|
|
93
127
|
/** Canonical id — identity id for the root handle, store key for children. */
|
|
94
128
|
get id(): any;
|
|
95
129
|
/** This device's id + name. `null` on child handles. */
|
|
@@ -203,7 +237,6 @@ export class Handle extends ReadyResource {
|
|
|
203
237
|
* @returns {Promise<void>}
|
|
204
238
|
*/
|
|
205
239
|
resume(): Promise<void>;
|
|
206
|
-
_syncMember(child: any): Promise<void>;
|
|
207
240
|
/**
|
|
208
241
|
* @param {Handle} child
|
|
209
242
|
* @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,12 +1,30 @@
|
|
|
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, opts?: {
|
|
15
|
+
signal?: AbortSignal;
|
|
16
|
+
}): () => void;
|
|
17
|
+
export function after(ref: Ref, fn: (ctx: {
|
|
18
|
+
op: string;
|
|
19
|
+
name: string;
|
|
20
|
+
row: any;
|
|
21
|
+
}) => void, opts?: {
|
|
22
|
+
signal?: AbortSignal;
|
|
23
|
+
}): () => void;
|
|
8
24
|
export function get(ref: Ref, q?: string | Record<string, any>): Promise<SingleResult | ListResult | GetByIdResult>;
|
|
9
|
-
export function watch(ref: Ref, q?: Record<string, any
|
|
25
|
+
export function watch(ref: Ref, q?: Record<string, any>, opts?: {
|
|
26
|
+
signal?: AbortSignal;
|
|
27
|
+
}): any;
|
|
10
28
|
export function open(ref: Ref, arg?: string | {
|
|
11
29
|
invite?: string;
|
|
12
30
|
id?: string;
|
package/types/lib/utils.d.ts
CHANGED
|
@@ -7,6 +7,14 @@
|
|
|
7
7
|
* @param {Record<string, RefInfo>} refs
|
|
8
8
|
*/
|
|
9
9
|
export function attachRefs(target: any, refs: Record<string, RefInfo>): void;
|
|
10
|
+
/**
|
|
11
|
+
* Run `cb` when `signal` aborts — or immediately if it already has. No-op
|
|
12
|
+
* without a signal. The listener removes itself on fire.
|
|
13
|
+
*
|
|
14
|
+
* @param {AbortSignal} [signal]
|
|
15
|
+
* @param {() => void} cb
|
|
16
|
+
*/
|
|
17
|
+
export function onAbort(signal?: AbortSignal, cb: () => void): void;
|
|
10
18
|
/**
|
|
11
19
|
* @typedef {'collection' | 'single' | 'action' | 'handle'} RefKind
|
|
12
20
|
* @typedef {{ kind?: string, schema?: string }} RefInfo
|
package/types/rpc/client.d.ts
CHANGED
|
@@ -111,7 +111,7 @@ declare class LocalRefs {
|
|
|
111
111
|
/** @param {Client} client */
|
|
112
112
|
constructor(client: Client);
|
|
113
113
|
parent: Client;
|
|
114
|
-
spec: import("@cero-base/core
|
|
114
|
+
spec: import("@cero-base/core").Spec;
|
|
115
115
|
store: this;
|
|
116
116
|
_local: boolean;
|
|
117
117
|
/** Underlying RPC channel borrowed from the parent. */
|
|
File without changes
|