@cero-base/cero 0.5.2 → 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 +11 -4
- package/src/build/builtins.js +137 -0
- package/src/{builder.js → build/index.js} +40 -34
- package/src/build/schemas.js +162 -0
- 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 +15 -51
- package/src/index.js +31 -10
- package/src/lib/constants.js +20 -0
- package/src/lib/internal.js +3 -0
- package/src/lib/operators.js +41 -2
- package/types/build/builtins.d.ts +88 -0
- package/types/build/schemas.d.ts +153 -0
- 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 +1 -7
- package/types/index.d.ts +6 -1
- package/types/lib/constants.d.ts +14 -0
- 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/src/lib/builtins.js +0 -413
- package/types/lib/builtins.d.ts +0 -141
- /package/types/{builder.d.ts → build/index.d.ts} +0 -0
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",
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"default": "./src/index.js"
|
|
21
21
|
},
|
|
22
22
|
"./build": {
|
|
23
|
-
"types": "./types/
|
|
24
|
-
"default": "./src/
|
|
23
|
+
"types": "./types/build/index.d.ts",
|
|
24
|
+
"default": "./src/build/index.js"
|
|
25
25
|
},
|
|
26
26
|
"./server": {
|
|
27
27
|
"types": "./types/rpc/server.d.ts",
|
|
@@ -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",
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Build wiring for the builtin schemas (schemas.js): turns the grouped field
|
|
2
|
+
// maps into the type / collection / dispatch / command descriptors the builder
|
|
3
|
+
// registers. Every cero schema includes these; app refs are added on top.
|
|
4
|
+
import { CeroError } from '@cero-base/core/errors'
|
|
5
|
+
import { COUNTERS, DB_TYPE } from '../lib/constants.js'
|
|
6
|
+
import * as schemas from './schemas.js'
|
|
7
|
+
|
|
8
|
+
// Builtin collections by scope — ref name → { type, kind? }. kind defaults to
|
|
9
|
+
// 'collection' (id-keyed); 'single' has no key. Dispatches are derived for
|
|
10
|
+
// main; counters is internal (a collection with no add/set/del).
|
|
11
|
+
export const refs = {
|
|
12
|
+
main: {
|
|
13
|
+
members: { type: 'member' },
|
|
14
|
+
devices: { type: 'device' },
|
|
15
|
+
invites: { type: 'invite' },
|
|
16
|
+
handles: { type: 'handle' }
|
|
17
|
+
},
|
|
18
|
+
local: {
|
|
19
|
+
master: { type: 'master', kind: 'single' },
|
|
20
|
+
keypair: { type: 'keypair', kind: 'single' },
|
|
21
|
+
'handle-keypairs': { type: 'handle-keypair' }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const hyperdbType = (prim) => DB_TYPE[prim] || 'string'
|
|
26
|
+
|
|
27
|
+
const at = (ns, n) => `@${ns}/${n}`
|
|
28
|
+
const keyOf = (def) => (def.kind === 'single' ? [] : ['id'])
|
|
29
|
+
|
|
30
|
+
const fields = (map) =>
|
|
31
|
+
Object.entries(map).map(([name, m]) => ({
|
|
32
|
+
name,
|
|
33
|
+
type: hyperdbType(m.prim),
|
|
34
|
+
required: m.required === true
|
|
35
|
+
}))
|
|
36
|
+
|
|
37
|
+
// Merge app `t.extend` fields into a builtin's base fields — base can't be redeclared.
|
|
38
|
+
const merge = (type, base, extra) => {
|
|
39
|
+
if (!extra) return base
|
|
40
|
+
for (const k in extra)
|
|
41
|
+
if (k in base)
|
|
42
|
+
throw CeroError.INVALID(`'${k}' is a base field of '${type}' and cannot be redeclared`)
|
|
43
|
+
return { ...base, ...extra }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const descriptors = (group, extend = {}) =>
|
|
47
|
+
Object.entries(group).map(([name, base]) => ({
|
|
48
|
+
name,
|
|
49
|
+
compact: false,
|
|
50
|
+
fields: fields(merge(name, base, extend[name]))
|
|
51
|
+
}))
|
|
52
|
+
|
|
53
|
+
// meta.refs entries for a scope's builtins.
|
|
54
|
+
export const builtinRefs = (ns, scope) =>
|
|
55
|
+
Object.fromEntries(
|
|
56
|
+
Object.entries(refs[scope]).map(([name, def]) => [
|
|
57
|
+
name,
|
|
58
|
+
{
|
|
59
|
+
kind: def.kind || 'collection',
|
|
60
|
+
path: [name],
|
|
61
|
+
builtin: true,
|
|
62
|
+
...(scope === 'main' && { verb: def.type }),
|
|
63
|
+
schema: at(ns, def.type)
|
|
64
|
+
}
|
|
65
|
+
])
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
// hyperschema type descriptors. scope is 'main' | 'local' | 'rpc'; `extend`
|
|
69
|
+
// merges app `t.extend` fields into the matching type.
|
|
70
|
+
export const builtinTypes = (scope, extend) => descriptors(schemas[scope], extend)
|
|
71
|
+
export const rpcTypes = () => descriptors(schemas.rpc)
|
|
72
|
+
|
|
73
|
+
// hyperdb collection descriptors for a scope.
|
|
74
|
+
export const builtinCollections = (ns, scope) => {
|
|
75
|
+
const out = Object.entries(refs[scope]).map(([name, def]) => ({
|
|
76
|
+
name,
|
|
77
|
+
schema: at(ns, def.type),
|
|
78
|
+
key: keyOf(def)
|
|
79
|
+
}))
|
|
80
|
+
if (scope === 'main') out.push({ name: COUNTERS, schema: at(ns, 'counter'), key: ['name'] })
|
|
81
|
+
return out
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// hyperdispatch descriptors (main scope only).
|
|
85
|
+
export const builtinDispatches = (ns) => [
|
|
86
|
+
{ name: 'add-writer', requestType: at(ns, 'writer') },
|
|
87
|
+
{ name: 'del-writer', requestType: at(ns, 'writer') },
|
|
88
|
+
{ name: 'claim-writer', requestType: at(ns, 'claim') },
|
|
89
|
+
...Object.values(refs.main).flatMap(({ type }) => [
|
|
90
|
+
{ name: `add-${type}`, requestType: at(ns, type) },
|
|
91
|
+
{ name: `set-${type}`, requestType: at(ns, type) },
|
|
92
|
+
{ name: `del-${type}`, requestType: at(ns, 'del-by-id') }
|
|
93
|
+
])
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
export const rpcCommands = (ns) => {
|
|
97
|
+
const ref = (n) => at(ns, n)
|
|
98
|
+
return [
|
|
99
|
+
{ name: 'init', request: { name: ref('req-empty') }, response: { name: ref('res-identity') } },
|
|
100
|
+
{
|
|
101
|
+
name: 'restore',
|
|
102
|
+
request: { name: ref('req-restore') },
|
|
103
|
+
response: { name: ref('res-identity') }
|
|
104
|
+
},
|
|
105
|
+
{ name: 'add-row', request: { name: ref('req-row') }, response: { name: ref('res-data') } },
|
|
106
|
+
{
|
|
107
|
+
name: 'add-handle',
|
|
108
|
+
request: { name: ref('req-row') },
|
|
109
|
+
response: { name: ref('res-handle') }
|
|
110
|
+
},
|
|
111
|
+
{ name: 'set', request: { name: ref('req-row') }, response: { name: ref('res-data') } },
|
|
112
|
+
{ name: 'get', request: { name: ref('req-query') }, response: { name: ref('res-rows') } },
|
|
113
|
+
{ name: 'get-one', request: { name: ref('req-id') }, response: { name: ref('res-data') } },
|
|
114
|
+
{ name: 'del', request: { name: ref('req-id') }, response: { name: ref('res-ok') } },
|
|
115
|
+
{ name: 'count', request: { name: ref('req-query') }, response: { name: ref('res-count') } },
|
|
116
|
+
{
|
|
117
|
+
name: 'watch',
|
|
118
|
+
request: { name: ref('req-query') },
|
|
119
|
+
response: { name: ref('res-rows'), stream: true }
|
|
120
|
+
},
|
|
121
|
+
{ name: 'call', request: { name: ref('req-call') }, response: { name: ref('res-data') } },
|
|
122
|
+
{ name: 'invite', request: { name: ref('req-invite') }, response: { name: ref('res-invite') } },
|
|
123
|
+
{ name: 'revoke', request: { name: ref('req-revoke') }, response: { name: ref('res-ok') } },
|
|
124
|
+
{ name: 'join', request: { name: ref('req-join') }, response: { name: ref('res-handle') } },
|
|
125
|
+
{
|
|
126
|
+
name: 'open-handle',
|
|
127
|
+
request: { name: ref('req-open') },
|
|
128
|
+
response: { name: ref('res-handle') }
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: 'close-handle',
|
|
132
|
+
request: { name: ref('req-handle') },
|
|
133
|
+
response: { name: ref('res-ok') }
|
|
134
|
+
},
|
|
135
|
+
{ name: 'leave', request: { name: ref('req-handle') }, response: { name: ref('res-ok') } }
|
|
136
|
+
]
|
|
137
|
+
}
|
|
@@ -8,20 +8,18 @@ import HRPCBuilder from 'hrpc'
|
|
|
8
8
|
|
|
9
9
|
import { CeroError } from '@cero-base/core/errors'
|
|
10
10
|
|
|
11
|
+
import { NS } from '../lib/constants.js'
|
|
12
|
+
import { internal } from '../lib/internal.js'
|
|
11
13
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
hyperdbType,
|
|
14
|
+
refs,
|
|
15
|
+
builtinRefs,
|
|
15
16
|
builtinTypes,
|
|
16
17
|
builtinCollections,
|
|
17
18
|
builtinDispatches,
|
|
18
|
-
localBuiltinTypes,
|
|
19
|
-
localBuiltinCollections,
|
|
20
19
|
rpcTypes,
|
|
21
|
-
rpcCommands
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const NS = 'cero'
|
|
20
|
+
rpcCommands,
|
|
21
|
+
hyperdbType
|
|
22
|
+
} from './builtins.js'
|
|
25
23
|
|
|
26
24
|
/**
|
|
27
25
|
* @typedef {import('@cero-base/core/schema').Schema} Schema
|
|
@@ -45,22 +43,38 @@ const NS = 'cero'
|
|
|
45
43
|
* @returns {Promise<void>}
|
|
46
44
|
*/
|
|
47
45
|
export async function build(specDir, schema, { ns = NS } = {}) {
|
|
48
|
-
const
|
|
49
|
-
if (!
|
|
46
|
+
const raw = /** @type {SchemaDefs & { local?: SchemaDefs }} */ (schema?.defs || schema)
|
|
47
|
+
if (!raw || typeof raw !== 'object') throw CeroError.REQUIRED('schema')
|
|
48
|
+
|
|
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.
|
|
51
|
+
const extend = {}
|
|
52
|
+
const defs = {}
|
|
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
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
collect(raw)
|
|
65
|
+
for (const ext of internal.extensions) if (ext.schema) collect(ext.schema, true)
|
|
50
66
|
|
|
51
|
-
const main = compile(splitMain(defs), ns,
|
|
52
|
-
const local = defs.local
|
|
53
|
-
? compile(defs.local, ns, LOCAL_BUILTINS)
|
|
54
|
-
: compile({}, ns, LOCAL_BUILTINS)
|
|
67
|
+
const main = compile(splitMain(defs), ns, 'main')
|
|
68
|
+
const local = compile(defs.local || {}, ns, 'local')
|
|
55
69
|
const handles = {}
|
|
56
70
|
for (const [name, child] of Object.entries(splitHandles(defs))) {
|
|
57
|
-
handles[name] = compile(child, ns)
|
|
71
|
+
handles[name] = compile(child, ns, 'main')
|
|
58
72
|
}
|
|
59
73
|
|
|
60
|
-
emitMain(join(specDir, 'main'), ns, main, { rpc: true })
|
|
74
|
+
emitMain(join(specDir, 'main'), ns, main, { rpc: true, extend })
|
|
61
75
|
emitLocal(join(specDir, 'local'), ns, local)
|
|
62
76
|
for (const [name, handle] of Object.entries(handles)) {
|
|
63
|
-
emitMain(join(specDir, 'handles', name), ns, handle, { rpc: false })
|
|
77
|
+
emitMain(join(specDir, 'handles', name), ns, handle, { rpc: false, extend })
|
|
64
78
|
}
|
|
65
79
|
|
|
66
80
|
const meta = {
|
|
@@ -105,18 +119,10 @@ function isPlainHandle(v) {
|
|
|
105
119
|
return v && typeof v === 'object' && !v.kind && !v.prim
|
|
106
120
|
}
|
|
107
121
|
|
|
108
|
-
function compile(root, ns,
|
|
122
|
+
function compile(root, ns, scope = 'main') {
|
|
109
123
|
const ctx = { types: [], collections: [], dispatches: [], meta: { ns, refs: {} }, ns }
|
|
110
124
|
|
|
111
|
-
|
|
112
|
-
ctx.meta.refs[b.name] = {
|
|
113
|
-
kind: b.kind || 'collection',
|
|
114
|
-
path: [b.name],
|
|
115
|
-
builtin: true,
|
|
116
|
-
verb: b.verb,
|
|
117
|
-
schema: `@${ns}/${b.type}`
|
|
118
|
-
}
|
|
119
|
-
}
|
|
125
|
+
Object.assign(ctx.meta.refs, builtinRefs(ns, scope))
|
|
120
126
|
|
|
121
127
|
for (const [name, node] of Object.entries(root)) {
|
|
122
128
|
if (node.kind === 'handle') {
|
|
@@ -174,11 +180,11 @@ function fieldsFor(fields) {
|
|
|
174
180
|
return Object.entries(fields).map(([name, marker]) => ({
|
|
175
181
|
name,
|
|
176
182
|
type: hyperdbType(marker.prim),
|
|
177
|
-
required:
|
|
183
|
+
required: marker.required === true
|
|
178
184
|
}))
|
|
179
185
|
}
|
|
180
186
|
|
|
181
|
-
function emitMain(dir, ns, { types, collections, dispatches }, { rpc }) {
|
|
187
|
+
function emitMain(dir, ns, { types, collections, dispatches }, { rpc, extend = {} }) {
|
|
182
188
|
const schemaDir = join(dir, 'schema')
|
|
183
189
|
const dbDir = join(dir, 'db')
|
|
184
190
|
const dispatchDir = join(dir, 'dispatch')
|
|
@@ -186,14 +192,14 @@ function emitMain(dir, ns, { types, collections, dispatches }, { rpc }) {
|
|
|
186
192
|
|
|
187
193
|
const s = Hyperschema.from(schemaDir)
|
|
188
194
|
const sns = s.namespace(ns)
|
|
189
|
-
for (const desc of builtinTypes()) sns.register(desc)
|
|
195
|
+
for (const desc of builtinTypes('main', extend)) sns.register(desc)
|
|
190
196
|
for (const desc of types) sns.register(desc)
|
|
191
197
|
if (rpc) for (const desc of rpcTypes()) sns.register(desc)
|
|
192
198
|
Hyperschema.toDisk(s, schemaDir, { esm: true })
|
|
193
199
|
|
|
194
200
|
const db = HyperdbBuilder.from(schemaDir, dbDir)
|
|
195
201
|
const dns = db.namespace(ns)
|
|
196
|
-
for (const desc of builtinCollections(ns)) dns.collections.register(desc)
|
|
202
|
+
for (const desc of builtinCollections(ns, 'main')) dns.collections.register(desc)
|
|
197
203
|
for (const desc of collections) dns.collections.register(desc)
|
|
198
204
|
HyperdbBuilder.toDisk(db, dbDir, { esm: true })
|
|
199
205
|
|
|
@@ -217,13 +223,13 @@ function emitLocal(dir, ns, { types, collections }) {
|
|
|
217
223
|
|
|
218
224
|
const s = Hyperschema.from(schemaDir)
|
|
219
225
|
const sns = s.namespace(ns)
|
|
220
|
-
for (const desc of
|
|
226
|
+
for (const desc of builtinTypes('local')) sns.register(desc)
|
|
221
227
|
for (const desc of types) sns.register(desc)
|
|
222
228
|
Hyperschema.toDisk(s, schemaDir, { esm: true })
|
|
223
229
|
|
|
224
230
|
const db = HyperdbBuilder.from(schemaDir, dbDir)
|
|
225
231
|
const dns = db.namespace(ns)
|
|
226
|
-
for (const desc of
|
|
232
|
+
for (const desc of builtinCollections(ns, 'local')) dns.collections.register(desc)
|
|
227
233
|
for (const desc of collections) dns.collections.register(desc)
|
|
228
234
|
HyperdbBuilder.toDisk(db, dbDir, { esm: true })
|
|
229
235
|
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// Builtin type schemas grouped by scope, in the schema DSL — kept separate from
|
|
2
|
+
// the build wiring (builtins.js) so apps can extend them with `t.extend`.
|
|
3
|
+
// Field order is the wire order; do not reorder.
|
|
4
|
+
import { t } from '../lib/spec.js'
|
|
5
|
+
|
|
6
|
+
const { string, bytes, int, uint, bool, required } = t
|
|
7
|
+
|
|
8
|
+
export const main = {
|
|
9
|
+
'del-by-id': {
|
|
10
|
+
id: required(string)
|
|
11
|
+
},
|
|
12
|
+
writer: {
|
|
13
|
+
master: required(bytes),
|
|
14
|
+
writer: required(bytes),
|
|
15
|
+
sig: required(bytes),
|
|
16
|
+
isIndexer: bool
|
|
17
|
+
},
|
|
18
|
+
counter: {
|
|
19
|
+
name: required(string),
|
|
20
|
+
value: required(uint)
|
|
21
|
+
},
|
|
22
|
+
member: {
|
|
23
|
+
id: required(string),
|
|
24
|
+
key: required(bytes),
|
|
25
|
+
role: required(string),
|
|
26
|
+
name: string,
|
|
27
|
+
createdAt: int,
|
|
28
|
+
updatedAt: int,
|
|
29
|
+
sig: bytes,
|
|
30
|
+
index: uint
|
|
31
|
+
},
|
|
32
|
+
device: {
|
|
33
|
+
id: required(string),
|
|
34
|
+
memberId: string,
|
|
35
|
+
name: string,
|
|
36
|
+
isMobile: bool,
|
|
37
|
+
createdAt: int,
|
|
38
|
+
updatedAt: int,
|
|
39
|
+
index: uint
|
|
40
|
+
},
|
|
41
|
+
invite: {
|
|
42
|
+
id: required(string),
|
|
43
|
+
invite: required(bytes),
|
|
44
|
+
publicKey: required(bytes),
|
|
45
|
+
data: bytes,
|
|
46
|
+
sig: bytes,
|
|
47
|
+
role: required(string),
|
|
48
|
+
expires: int,
|
|
49
|
+
createdAt: int,
|
|
50
|
+
index: uint
|
|
51
|
+
},
|
|
52
|
+
handle: {
|
|
53
|
+
id: required(string),
|
|
54
|
+
type: required(string),
|
|
55
|
+
key: required(bytes),
|
|
56
|
+
encryptionKey: bytes,
|
|
57
|
+
name: string,
|
|
58
|
+
createdAt: int,
|
|
59
|
+
updatedAt: int,
|
|
60
|
+
index: uint
|
|
61
|
+
},
|
|
62
|
+
claim: {
|
|
63
|
+
identity: required(bytes),
|
|
64
|
+
writer: required(bytes),
|
|
65
|
+
sig: required(bytes)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const local = {
|
|
70
|
+
master: {
|
|
71
|
+
seed: required(bytes)
|
|
72
|
+
},
|
|
73
|
+
keypair: {
|
|
74
|
+
publicKey: required(bytes),
|
|
75
|
+
secretKey: required(bytes)
|
|
76
|
+
},
|
|
77
|
+
'handle-keypair': {
|
|
78
|
+
id: required(string),
|
|
79
|
+
publicKey: required(bytes),
|
|
80
|
+
secretKey: required(bytes),
|
|
81
|
+
encryptionKey: bytes
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const rpc = {
|
|
86
|
+
'req-empty': {
|
|
87
|
+
ok: bool
|
|
88
|
+
},
|
|
89
|
+
'req-restore': {
|
|
90
|
+
phrase: required(string)
|
|
91
|
+
},
|
|
92
|
+
'req-row': {
|
|
93
|
+
handle: required(string),
|
|
94
|
+
ref: required(string),
|
|
95
|
+
data: required(bytes),
|
|
96
|
+
local: bool
|
|
97
|
+
},
|
|
98
|
+
'req-id': {
|
|
99
|
+
handle: required(string),
|
|
100
|
+
ref: required(string),
|
|
101
|
+
id: required(string),
|
|
102
|
+
local: bool
|
|
103
|
+
},
|
|
104
|
+
'req-query': {
|
|
105
|
+
handle: required(string),
|
|
106
|
+
ref: required(string),
|
|
107
|
+
query: bytes,
|
|
108
|
+
local: bool
|
|
109
|
+
},
|
|
110
|
+
'req-call': {
|
|
111
|
+
handle: required(string),
|
|
112
|
+
op: required(string),
|
|
113
|
+
data: bytes
|
|
114
|
+
},
|
|
115
|
+
'req-invite': {
|
|
116
|
+
handle: required(string),
|
|
117
|
+
role: string
|
|
118
|
+
},
|
|
119
|
+
'req-revoke': {
|
|
120
|
+
handle: required(string),
|
|
121
|
+
invite: required(string)
|
|
122
|
+
},
|
|
123
|
+
'req-join': {
|
|
124
|
+
parent: required(string),
|
|
125
|
+
ref: required(string),
|
|
126
|
+
invite: required(string)
|
|
127
|
+
},
|
|
128
|
+
'req-open': {
|
|
129
|
+
parent: required(string),
|
|
130
|
+
row: required(string)
|
|
131
|
+
},
|
|
132
|
+
'req-handle': {
|
|
133
|
+
handle: required(string)
|
|
134
|
+
},
|
|
135
|
+
'res-data': {
|
|
136
|
+
data: bytes
|
|
137
|
+
},
|
|
138
|
+
'res-rows': {
|
|
139
|
+
data: required(bytes),
|
|
140
|
+
total: required(int),
|
|
141
|
+
size: required(int)
|
|
142
|
+
},
|
|
143
|
+
'res-count': {
|
|
144
|
+
count: required(int)
|
|
145
|
+
},
|
|
146
|
+
'res-invite': {
|
|
147
|
+
invite: required(string)
|
|
148
|
+
},
|
|
149
|
+
'res-handle': {
|
|
150
|
+
id: required(string),
|
|
151
|
+
type: required(string),
|
|
152
|
+
name: string
|
|
153
|
+
},
|
|
154
|
+
'res-identity': {
|
|
155
|
+
id: required(string),
|
|
156
|
+
deviceId: string,
|
|
157
|
+
phrase: string
|
|
158
|
+
},
|
|
159
|
+
'res-ok': {
|
|
160
|
+
ok: bool
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -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
|
+
}
|