@cero-base/cero 0.8.10 → 1.0.1
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 +64 -14
- package/package.json +20 -10
- package/src/build/builtins.js +10 -3
- package/src/build/index.js +24 -4
- package/src/build/schemas.js +20 -1
- package/src/handle/index.js +178 -69
- package/src/index.js +85 -64
- package/src/lib/constants.js +2 -1
- package/src/lib/operators.js +114 -53
- package/src/lib/peek.js +42 -0
- package/src/lib/utils.js +16 -3
- package/src/rpc/client.js +128 -14
- package/src/rpc/server.js +54 -7
- package/types/build/builtins.d.ts +5 -1
- package/types/build/index.d.ts +4 -21
- package/types/build/schemas.d.ts +19 -0
- package/types/handle/index.d.ts +49 -8
- package/types/index.d.ts +10 -5
- package/types/lib/constants.d.ts +1 -0
- package/types/lib/operators.d.ts +30 -10
- package/types/lib/peek.d.ts +10 -0
- package/types/lib/utils.d.ts +6 -3
- package/types/rpc/client.d.ts +3 -1
- package/types/rpc/server.d.ts +6 -5
package/README.md
CHANGED
|
@@ -56,6 +56,7 @@ Ships with TypeScript declarations (`.d.ts`) generated from JSDoc.
|
|
|
56
56
|
| `recoveryTimeout` | Bound on the recovery wait. |
|
|
57
57
|
| `routes` | Custom action handlers keyed by route name. |
|
|
58
58
|
| `encryptionKey` | Override the per-identity encryption key. |
|
|
59
|
+
| `onerror` | Called with background/async failures that would otherwise be swallowed (failed after-hooks, `onApply` callbacks, pairing candidate errors); the app decides how to log or report them. |
|
|
59
60
|
|
|
60
61
|
Reads `me.id`, `me.device`, `me.identity` for canonical metadata. `me.identity.toPhrase()` renders the seed phrase.
|
|
61
62
|
|
|
@@ -104,8 +105,9 @@ Append a row to a collection. Generates an `id`, `createdAt`, and `updatedAt` if
|
|
|
104
105
|
|
|
105
106
|
```js
|
|
106
107
|
const { data } = await cero.put(room.messages, { text: 'hi' })
|
|
107
|
-
data.id // → 'abc…'
|
|
108
|
-
|
|
108
|
+
data.id // → 'abc…' (plus createdAt / updatedAt)
|
|
109
|
+
// `memberId` (the writer→member backlink) is stamped in the apply layer, so it
|
|
110
|
+
// surfaces on a later `cero.get`, not on the row this call returns.
|
|
109
111
|
```
|
|
110
112
|
|
|
111
113
|
### `cero.set(ref, row)`
|
|
@@ -166,9 +168,9 @@ A Readable stream that re-emits the latest snapshot on every change. Always emit
|
|
|
166
168
|
|
|
167
169
|
```js
|
|
168
170
|
const stream = cero.watch(room.messages, { limit: 50, reverse: true })
|
|
169
|
-
|
|
171
|
+
for await (const { data, total, size } of stream) {
|
|
170
172
|
/* render */
|
|
171
|
-
}
|
|
173
|
+
}
|
|
172
174
|
// stops on room.close(), on stream.destroy(), or on `signal` abort:
|
|
173
175
|
const ac = new AbortController()
|
|
174
176
|
cero.watch(room.messages, null, { signal: ac.signal }) // ac.abort() ⇒ destroyed
|
|
@@ -211,6 +213,53 @@ cero.after(me.profile, ({ row }) => {
|
|
|
211
213
|
|
|
212
214
|
`ctx` is `{ op, name, row }` (plus `result` in `after`).
|
|
213
215
|
|
|
216
|
+
## Files
|
|
217
|
+
|
|
218
|
+
Store a file (avatar, image, attachment) and get a ready-to-render URL. Bytes live in a per-handle blob core — replicated to members on demand, never inlined into the log — so rows carry only a small self-describing id.
|
|
219
|
+
|
|
220
|
+
Every handle has a builtin `files` collection. `cero.put(handle.files, …)` uploads bytes (a `Buffer` or a `Readable`) and returns a file with a `.url`:
|
|
221
|
+
|
|
222
|
+
```js
|
|
223
|
+
const { data: file } = await cero.put(me.files, {
|
|
224
|
+
data: bytes, // Buffer | Readable
|
|
225
|
+
name: 'cat.jpg',
|
|
226
|
+
type: 'image/jpeg'
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
img.src = file.url
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Read them back — every row carries a fresh `.url`:
|
|
233
|
+
|
|
234
|
+
```js
|
|
235
|
+
const { data: files } = await cero.get(me.files) // list all
|
|
236
|
+
const { data: one } = await cero.get(me.files, file.id) // one, by id
|
|
237
|
+
cero.watch(me.files).on('data', ({ data }) => render(data)) // live
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### The `file()` column type
|
|
241
|
+
|
|
242
|
+
For a file referenced from a row — an avatar, a room icon, a message attachment — declare the field `cero.t.file()`. Store the file's `id`; read it back already resolved to `{ id, name, type, size, url }`, with **no extra lookup**:
|
|
243
|
+
|
|
244
|
+
```js
|
|
245
|
+
// schema.js
|
|
246
|
+
profile: cero.t.single({ name: cero.t.string, avatar: cero.t.file() })
|
|
247
|
+
|
|
248
|
+
// save: upload, then store the id on the row
|
|
249
|
+
const { data: pic } = await cero.put(me.files, { data: bytes, type: 'image/png' })
|
|
250
|
+
await cero.set(me.profile, { avatar: pic.id })
|
|
251
|
+
|
|
252
|
+
// read: the avatar comes back url-ready
|
|
253
|
+
const { data: profile } = await cero.get(me.profile)
|
|
254
|
+
img.src = profile.avatar.url
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
`cero.t.file({ embed: true })` also stores the file's `name` inline — handy for a list of named attachments, so rendering needs no per-item lookup.
|
|
258
|
+
|
|
259
|
+
### URLs are local and ephemeral
|
|
260
|
+
|
|
261
|
+
A `.url` points at a localhost server this device runs, with a per-session token — it changes across restarts and is **not** shareable to other peers. Never persist a `.url`: store the **id** (cero does), and re-read to get a current one. Each member derives their own url from the same id, and the bytes download on demand when the url is first fetched.
|
|
262
|
+
|
|
214
263
|
## Custom operators
|
|
215
264
|
|
|
216
265
|
Your app's business logic lives as **custom operators** — plain functions whose first arg is the handle they act on, composing the built-ins. Because the built-ins resolve through the handle (a real DB on the core, an RPC proxy on the client), custom operators are symmetric over RPC for free.
|
|
@@ -360,8 +409,9 @@ The client side gets the **same operator API** as a local cero — `cero.put`, `
|
|
|
360
409
|
import { serve } from '@cero-base/cero/server'
|
|
361
410
|
import { spec } from './spec/index.js'
|
|
362
411
|
|
|
363
|
-
// serve builds and owns the root cero — pass storage + the built spec, not a handle
|
|
364
|
-
|
|
412
|
+
// serve builds and owns the root cero — pass storage + the built spec, not a handle.
|
|
413
|
+
// Returns a Server instance (call server.close() to shut down).
|
|
414
|
+
const server = await serve(Bare.IPC, { storage: './data', spec, seed: '…' })
|
|
365
415
|
```
|
|
366
416
|
|
|
367
417
|
### Client (the UI process)
|
|
@@ -386,14 +436,14 @@ The same `spec/index.js` is imported on both sides. `@cero-base/cero/build` emit
|
|
|
386
436
|
|
|
387
437
|
Everything the local API offers, plus lifecycle methods on returned handles:
|
|
388
438
|
|
|
389
|
-
| Operator / method
|
|
390
|
-
|
|
|
391
|
-
| `cero.put` / `set` / `get` / `del` / `count` / `watch` / `call`
|
|
392
|
-
| `cero.open(ref, …)` — create, join, load by id
|
|
393
|
-
| `room.invite({ role })` — mint an invite
|
|
394
|
-
| `room.revoke(invite)` — invalidate an outstanding invite; `true` if it existed
|
|
395
|
-
| `room.close()` — release the handle on the server
|
|
396
|
-
| `room.leave()` — remove yourself from the room and from your handles list
|
|
439
|
+
| Operator / method | Works over RPC |
|
|
440
|
+
| -------------------------------------------------------------------------------------------------------------------- | -------------- |
|
|
441
|
+
| `cero.put` / `set` / `get` / `del` / `count` / `watch` / `call` | ✓ |
|
|
442
|
+
| `cero.open(ref, …)` — create, join, load by id | ✓ |
|
|
443
|
+
| `room.invite({ role, expiresIn, multiUse })` — mint an invite (`multiUse: true` keeps it alive after the first join) | ✓ |
|
|
444
|
+
| `room.revoke(invite)` — invalidate an outstanding invite; `true` if it existed | ✓ |
|
|
445
|
+
| `room.close()` — release the handle on the server | ✓ |
|
|
446
|
+
| `room.leave()` — remove yourself from the room and from your handles list | ✓ |
|
|
397
447
|
|
|
398
448
|
`cero.watch(ref)` returns a Readable on both sides; snapshots flow as a server-streamed RPC.
|
|
399
449
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cero-base/cero",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "The ideal p2p API — everything is a handle, handles contain refs, refs contain rows.",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=20"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/lekinox/cero-base.git",
|
|
13
|
+
"directory": "packages/cero"
|
|
14
|
+
},
|
|
6
15
|
"main": "./src/index.js",
|
|
7
16
|
"types": "./types/index.d.ts",
|
|
8
17
|
"files": [
|
|
@@ -39,7 +48,7 @@
|
|
|
39
48
|
"typesVersions": {
|
|
40
49
|
"*": {
|
|
41
50
|
"build": [
|
|
42
|
-
"types/
|
|
51
|
+
"types/build/index.d.ts"
|
|
43
52
|
],
|
|
44
53
|
"server": [
|
|
45
54
|
"types/rpc/server.d.ts"
|
|
@@ -64,10 +73,6 @@
|
|
|
64
73
|
"crypto": {
|
|
65
74
|
"bare": "bare-crypto",
|
|
66
75
|
"default": "crypto"
|
|
67
|
-
},
|
|
68
|
-
"abort-controller": {
|
|
69
|
-
"bare": "bare-abort-controller",
|
|
70
|
-
"default": "bare-abort-controller"
|
|
71
76
|
}
|
|
72
77
|
},
|
|
73
78
|
"scripts": {
|
|
@@ -78,25 +83,30 @@
|
|
|
78
83
|
"test": "ls test/*.test.js | xargs -P1 -n1 brittle-node"
|
|
79
84
|
},
|
|
80
85
|
"dependencies": {
|
|
81
|
-
"@cero-base/core": "^0.
|
|
86
|
+
"@cero-base/core": "^1.0.1",
|
|
82
87
|
"b4a": "^1.8.1",
|
|
83
88
|
"bare-abort-controller": "^1.1.2",
|
|
84
|
-
"bare-crypto": "^1.
|
|
89
|
+
"bare-crypto": "^1.15.3",
|
|
85
90
|
"bare-fs": "^4.7.2",
|
|
86
91
|
"bare-path": "^3.0.1",
|
|
87
92
|
"blind-pairing": "^2.3.1",
|
|
88
|
-
"compact-encoding": "^3.
|
|
93
|
+
"compact-encoding": "^3.2.0",
|
|
94
|
+
"corestore": "^7.10.1",
|
|
89
95
|
"hrpc": "^4.3.0",
|
|
96
|
+
"hypercore": "^11.33.1",
|
|
97
|
+
"hypercore-crypto": "^3.7.0",
|
|
98
|
+
"hypercore-storage": "^3.1.1",
|
|
90
99
|
"hyperdb": "^6.7.0",
|
|
91
100
|
"hyperdispatch": "^1.6.0",
|
|
92
101
|
"hyperschema": "^1.21.0",
|
|
93
102
|
"ready-resource": "^1.2.0",
|
|
94
103
|
"safety-catch": "^1.0.3",
|
|
104
|
+
"streamx": "^2.28.0",
|
|
95
105
|
"z32": "^1.1.0"
|
|
96
106
|
},
|
|
97
107
|
"devDependencies": {
|
|
98
108
|
"@hyperswarm/testnet": "^3.1.4",
|
|
99
|
-
"brittle": "^4.0.
|
|
109
|
+
"brittle": "^4.0.2",
|
|
100
110
|
"typescript": "^6.0.3"
|
|
101
111
|
},
|
|
102
112
|
"license": "Apache-2.0"
|
package/src/build/builtins.js
CHANGED
|
@@ -13,7 +13,8 @@ export const refs = {
|
|
|
13
13
|
members: { type: 'member' },
|
|
14
14
|
devices: { type: 'device' },
|
|
15
15
|
invites: { type: 'invite' },
|
|
16
|
-
handles: { type: 'handle' }
|
|
16
|
+
handles: { type: 'handle' },
|
|
17
|
+
files: { type: 'file' }
|
|
17
18
|
},
|
|
18
19
|
local: {
|
|
19
20
|
master: { type: 'master', kind: 'single' },
|
|
@@ -22,7 +23,7 @@ export const refs = {
|
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
export const
|
|
26
|
+
export const getHyperdbType = (prim) => DB_TYPE[prim] || 'string'
|
|
26
27
|
|
|
27
28
|
const at = (ns, n) => `@${ns}/${n}`
|
|
28
29
|
const keyOf = (def) => (def.kind === 'single' ? [] : ['id'])
|
|
@@ -30,7 +31,7 @@ const keyOf = (def) => (def.kind === 'single' ? [] : ['id'])
|
|
|
30
31
|
const fields = (map) =>
|
|
31
32
|
Object.entries(map).map(([name, m]) => ({
|
|
32
33
|
name,
|
|
33
|
-
type:
|
|
34
|
+
type: getHyperdbType(m.prim),
|
|
34
35
|
required: m.required === true
|
|
35
36
|
}))
|
|
36
37
|
|
|
@@ -102,7 +103,13 @@ export const rpcCommands = (ns) => {
|
|
|
102
103
|
request: { name: ref('req-restore') },
|
|
103
104
|
response: { name: ref('res-identity') }
|
|
104
105
|
},
|
|
106
|
+
{ name: 'seed', request: { name: ref('req-empty') }, response: { name: ref('res-seed') } },
|
|
105
107
|
{ name: 'add-row', request: { name: ref('req-row') }, response: { name: ref('res-data') } },
|
|
108
|
+
{
|
|
109
|
+
name: 'add-file',
|
|
110
|
+
request: { name: ref('req-add-file') },
|
|
111
|
+
response: { name: ref('res-data') }
|
|
112
|
+
},
|
|
106
113
|
{
|
|
107
114
|
name: 'add-handle',
|
|
108
115
|
request: { name: ref('req-row') },
|
package/src/build/index.js
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
builtinDispatches,
|
|
19
19
|
rpcTypes,
|
|
20
20
|
rpcCommands,
|
|
21
|
-
|
|
21
|
+
getHyperdbType
|
|
22
22
|
} from './builtins.js'
|
|
23
23
|
|
|
24
24
|
/**
|
|
@@ -42,6 +42,8 @@ import {
|
|
|
42
42
|
* @param {BuildOpts} [opts]
|
|
43
43
|
* @returns {Promise<void>}
|
|
44
44
|
*/
|
|
45
|
+
export { getHyperdbType } from './builtins.js'
|
|
46
|
+
|
|
45
47
|
export async function build(specDir, schema, { ns = NS } = {}) {
|
|
46
48
|
const raw = /** @type {SchemaDefs & { local?: SchemaDefs }} */ (schema?.defs || schema)
|
|
47
49
|
if (!raw || typeof raw !== 'object') throw CeroError.REQUIRED('schema')
|
|
@@ -148,6 +150,12 @@ function compile(root, ns, scope = 'main') {
|
|
|
148
150
|
}
|
|
149
151
|
}
|
|
150
152
|
|
|
153
|
+
function fileFieldNames(fields) {
|
|
154
|
+
return Object.entries(fields)
|
|
155
|
+
.filter(([, m]) => m.prim === 'file')
|
|
156
|
+
.map(([name]) => name)
|
|
157
|
+
}
|
|
158
|
+
|
|
151
159
|
function register(name, node, ctx) {
|
|
152
160
|
const fqn = `@${ctx.ns}/${name}`
|
|
153
161
|
if (node.kind === 'action') {
|
|
@@ -160,7 +168,11 @@ function register(name, node, ctx) {
|
|
|
160
168
|
ctx.types.push({ name, compact: false, fields: fieldsFor(node.fields) })
|
|
161
169
|
ctx.collections.push({ name, schema: fqn, key: [] })
|
|
162
170
|
ctx.dispatches.push({ name: `set-${name}`, requestType: fqn })
|
|
163
|
-
ctx.
|
|
171
|
+
ctx.dispatches.push({ name: `del-${name}`, requestType: `@${ctx.ns}/del-by-id` })
|
|
172
|
+
const refEntry = { kind: 'single', path: [name], schema: fqn, fields: Object.keys(node.fields) }
|
|
173
|
+
const fileFields = fileFieldNames(node.fields)
|
|
174
|
+
if (fileFields.length) refEntry.files = fileFields
|
|
175
|
+
ctx.meta.refs[name] = refEntry
|
|
164
176
|
return
|
|
165
177
|
}
|
|
166
178
|
if (node.kind === 'collection') {
|
|
@@ -180,7 +192,15 @@ function register(name, node, ctx) {
|
|
|
180
192
|
ctx.dispatches.push({ name: `add-${name}`, requestType: fqn })
|
|
181
193
|
ctx.dispatches.push({ name: `set-${name}`, requestType: fqn })
|
|
182
194
|
ctx.dispatches.push({ name: `del-${name}`, requestType: `@${ctx.ns}/del-by-id` })
|
|
183
|
-
|
|
195
|
+
const refEntry = {
|
|
196
|
+
kind: 'collection',
|
|
197
|
+
path: [name],
|
|
198
|
+
schema: fqn,
|
|
199
|
+
fields: Object.keys(node.fields)
|
|
200
|
+
}
|
|
201
|
+
const fileFields = fileFieldNames(node.fields)
|
|
202
|
+
if (fileFields.length) refEntry.files = fileFields
|
|
203
|
+
ctx.meta.refs[name] = refEntry
|
|
184
204
|
if (node.indexes) {
|
|
185
205
|
for (const [idx, fields] of Object.entries(node.indexes)) {
|
|
186
206
|
ctx.indexes.push({ name: `${name}-${idx}`, collection: fqn, key: fields })
|
|
@@ -193,7 +213,7 @@ function register(name, node, ctx) {
|
|
|
193
213
|
function fieldsFor(fields) {
|
|
194
214
|
return Object.entries(fields).map(([name, marker]) => ({
|
|
195
215
|
name,
|
|
196
|
-
type:
|
|
216
|
+
type: getHyperdbType(marker.prim),
|
|
197
217
|
required: marker.required === true
|
|
198
218
|
}))
|
|
199
219
|
}
|
package/src/build/schemas.js
CHANGED
|
@@ -59,6 +59,14 @@ export const main = {
|
|
|
59
59
|
updatedAt: int,
|
|
60
60
|
index: uint
|
|
61
61
|
},
|
|
62
|
+
file: {
|
|
63
|
+
id: required(string),
|
|
64
|
+
memberId: string,
|
|
65
|
+
name: string,
|
|
66
|
+
createdAt: int,
|
|
67
|
+
updatedAt: int,
|
|
68
|
+
index: uint
|
|
69
|
+
},
|
|
62
70
|
claim: {
|
|
63
71
|
identity: required(bytes),
|
|
64
72
|
writer: required(bytes),
|
|
@@ -93,7 +101,8 @@ export const rpc = {
|
|
|
93
101
|
handle: required(string),
|
|
94
102
|
ref: required(string),
|
|
95
103
|
data: required(bytes),
|
|
96
|
-
local: bool
|
|
104
|
+
local: bool,
|
|
105
|
+
noUpsert: bool
|
|
97
106
|
},
|
|
98
107
|
'req-id': {
|
|
99
108
|
handle: required(string),
|
|
@@ -151,9 +160,19 @@ export const rpc = {
|
|
|
151
160
|
type: required(string),
|
|
152
161
|
name: string
|
|
153
162
|
},
|
|
163
|
+
'req-add-file': {
|
|
164
|
+
handle: required(string),
|
|
165
|
+
data: required(bytes),
|
|
166
|
+
name: string,
|
|
167
|
+
type: string
|
|
168
|
+
},
|
|
154
169
|
'res-identity': {
|
|
155
170
|
id: required(string),
|
|
156
171
|
deviceId: string,
|
|
172
|
+
fileBase: string,
|
|
173
|
+
fileToken: string
|
|
174
|
+
},
|
|
175
|
+
'res-seed': {
|
|
157
176
|
phrase: string
|
|
158
177
|
},
|
|
159
178
|
'res-ok': {
|