@cero-base/cero 0.4.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/LICENSE +201 -0
- package/README.md +287 -0
- package/package.json +79 -0
- package/src/CLAUDE.md +3 -0
- package/src/builder.js +276 -0
- package/src/handle/CLAUDE.md +3 -0
- package/src/handle/index.js +600 -0
- package/src/index.js +170 -0
- package/src/lib/CLAUDE.md +3 -0
- package/src/lib/builtins.js +410 -0
- package/src/lib/operators.js +128 -0
- package/src/lib/peek.js +36 -0
- package/src/lib/spec.js +6 -0
- package/src/lib/utils.js +41 -0
- package/src/local/index.js +49 -0
- package/src/rpc/CLAUDE.md +3 -0
- package/src/rpc/client.js +337 -0
- package/src/rpc/index.js +8 -0
- package/src/rpc/server.js +290 -0
- package/types/builder.d.ts +32 -0
- package/types/handle/index.d.ts +353 -0
- package/types/index.d.ts +102 -0
- package/types/lib/builtins.d.ts +141 -0
- package/types/lib/operators.d.ts +29 -0
- package/types/lib/peek.d.ts +10 -0
- package/types/lib/spec.d.ts +1 -0
- package/types/lib/utils.d.ts +44 -0
- package/types/local/index.d.ts +33 -0
- package/types/rpc/client.d.ts +125 -0
- package/types/rpc/index.d.ts +2 -0
- package/types/rpc/server.d.ts +154 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { Readable } from 'streamx'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {import('./utils.js').Ref} Ref
|
|
5
|
+
* @typedef {{ data: any }} SingleResult
|
|
6
|
+
* @typedef {{ data: any[], total: number, size: number }} ListResult
|
|
7
|
+
* @typedef {{ data: any | null }} GetByIdResult
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Insert (or overwrite by id) a row on `ref`.
|
|
12
|
+
*
|
|
13
|
+
* @param {Ref} ref
|
|
14
|
+
* @param {Record<string, any>} row
|
|
15
|
+
* @returns {Promise<SingleResult>}
|
|
16
|
+
*/
|
|
17
|
+
export const put = (ref, row) => ref.handle.store.put(ref.name, row)
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Upsert a row on `ref` — merges with the existing row and preserves
|
|
21
|
+
* `createdAt`.
|
|
22
|
+
*
|
|
23
|
+
* @param {Ref} ref
|
|
24
|
+
* @param {Record<string, any>} row
|
|
25
|
+
* @returns {Promise<SingleResult>}
|
|
26
|
+
*/
|
|
27
|
+
export const set = (ref, row) => ref.handle.store.set(ref.name, row)
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Delete a row by id (collection refs), or wipe the row (single refs).
|
|
31
|
+
*
|
|
32
|
+
* @param {Ref} ref
|
|
33
|
+
* @param {string} [id]
|
|
34
|
+
* @returns {Promise<void>}
|
|
35
|
+
*/
|
|
36
|
+
export const del = (ref, id) => ref.handle.store.del(ref.name, id)
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Count rows on `ref`, optionally filtered.
|
|
40
|
+
*
|
|
41
|
+
* @param {Ref} ref
|
|
42
|
+
* @param {Record<string, any>} [q]
|
|
43
|
+
* @returns {Promise<{ data: number }>}
|
|
44
|
+
*/
|
|
45
|
+
export const count = (ref, q) => ref.handle.store.count(ref.name, q)
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Invoke an `action`-kind ref (a custom mutation declared in the schema).
|
|
49
|
+
*
|
|
50
|
+
* @param {Ref} ref
|
|
51
|
+
* @param {Record<string, any>} [d]
|
|
52
|
+
* @returns {Promise<any>}
|
|
53
|
+
*/
|
|
54
|
+
export const call = (ref, d) => ref.handle.store.call(ref.name, d)
|
|
55
|
+
|
|
56
|
+
// get/watch on a handle-kind ref list its rows from the `handles` collection
|
|
57
|
+
// filtered by type (handle types are stored there with their { id, key,
|
|
58
|
+
// encryptionKey, name }). Data-kind refs go straight to the store.
|
|
59
|
+
// For handle-kind refs on the facade, the parent store lives on `root`.
|
|
60
|
+
const parentStore = (ref) => (ref.handle.root ? ref.handle.root.store : ref.handle.store)
|
|
61
|
+
|
|
62
|
+
const normalize = (rows, name) => {
|
|
63
|
+
const data = (rows || []).filter((r) => r.type === name)
|
|
64
|
+
return { data, total: data.length, size: data.length }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Read from `ref`. For data refs, dispatches to the underlying store. For
|
|
69
|
+
* `handle`-kind refs, lists existing child handles of that type from the
|
|
70
|
+
* parent's `handles` collection.
|
|
71
|
+
*
|
|
72
|
+
* @param {Ref} ref
|
|
73
|
+
* @param {string | Record<string, any>} [q]
|
|
74
|
+
* @returns {Promise<SingleResult | ListResult | GetByIdResult>}
|
|
75
|
+
*/
|
|
76
|
+
export const get = async (ref, q) => {
|
|
77
|
+
if (ref.kind !== 'handle') return ref.handle.store.get(ref.name, q)
|
|
78
|
+
const { data: all } = await parentStore(ref).get('handles', q)
|
|
79
|
+
return normalize(all, ref.name)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Live snapshot stream on `ref` — re-emits the latest `get()` result on
|
|
84
|
+
* every underlying mutation. Destroy the stream to stop watching.
|
|
85
|
+
*
|
|
86
|
+
* @param {Ref} ref
|
|
87
|
+
* @param {Record<string, any>} [q]
|
|
88
|
+
* @returns {import('streamx').Readable}
|
|
89
|
+
*/
|
|
90
|
+
export const watch = (ref, q) => {
|
|
91
|
+
if (ref.kind !== 'handle') return ref.handle.store.watch(ref.name, q)
|
|
92
|
+
const source = parentStore(ref).watch('handles', q)
|
|
93
|
+
const out = new Readable({
|
|
94
|
+
destroy(cb) {
|
|
95
|
+
source.destroy()
|
|
96
|
+
cb(null)
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
source.on('data', (snap) => out.push(normalize(snap?.data, ref.name)))
|
|
100
|
+
source.on('end', () => out.push(null))
|
|
101
|
+
source.on('error', (err) => out.destroy(err))
|
|
102
|
+
return out
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Universal signal instantiator. Dispatch on the second arg:
|
|
106
|
+
// string → join via invite
|
|
107
|
+
// { invite: string } → join via invite (object form)
|
|
108
|
+
// { id: string } → load an existing handle by id
|
|
109
|
+
// object | undefined → create (opts)
|
|
110
|
+
/**
|
|
111
|
+
* Open (or create / join / load) a child handle through a `handle`-kind
|
|
112
|
+
* ref. Dispatches on the normalize of `arg`:
|
|
113
|
+
*
|
|
114
|
+
* - `string` → join via an invite string
|
|
115
|
+
* - `{ invite: string }` → join via invite (object form)
|
|
116
|
+
* - `{ id: string }` → load an existing handle by id
|
|
117
|
+
* - `object | undefined` → create a new handle with the given opts
|
|
118
|
+
*
|
|
119
|
+
* @param {Ref} ref
|
|
120
|
+
* @param {string | { invite?: string, id?: string, name?: string, routes?: any, role?: string, accept?: boolean } | undefined} [arg]
|
|
121
|
+
* @returns {Promise<any>} The resolved child `Handle`.
|
|
122
|
+
*/
|
|
123
|
+
export const open = (ref, arg) => {
|
|
124
|
+
if (typeof arg === 'string') return ref.handle._join(arg, ref.name)
|
|
125
|
+
if (arg && typeof arg.invite === 'string') return ref.handle._join(arg.invite, ref.name)
|
|
126
|
+
if (arg && typeof arg.id === 'string') return ref.handle._load(ref.name, arg.id)
|
|
127
|
+
return ref.handle._create(ref.name, arg)
|
|
128
|
+
}
|
package/src/lib/peek.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import HypercoreStorage from 'hypercore-storage'
|
|
2
|
+
import Corestore from 'corestore'
|
|
3
|
+
|
|
4
|
+
import { CeroError } from '@cero-base/core/errors'
|
|
5
|
+
|
|
6
|
+
import { Local } from '../local/index.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Quickly check whether the on-disk directory at `dir` already holds an
|
|
10
|
+
* initialised cero identity (i.e. a stored master seed). Opens the local
|
|
11
|
+
* store read-only and closes everything before returning.
|
|
12
|
+
*
|
|
13
|
+
* @param {string} dir Cero data directory.
|
|
14
|
+
* @param {any} spec Built spec — same value passed to `cero(dir, spec)`.
|
|
15
|
+
* @returns {Promise<boolean>} `true` if a master seed exists on disk.
|
|
16
|
+
*/
|
|
17
|
+
export async function peek(dir, spec) {
|
|
18
|
+
if (typeof dir !== 'string' || !dir) throw CeroError.INVALID('dir must be a non-empty string')
|
|
19
|
+
if (!spec) throw CeroError.REQUIRED('spec')
|
|
20
|
+
|
|
21
|
+
const root = new HypercoreStorage(`${dir}/main`)
|
|
22
|
+
await root.ready()
|
|
23
|
+
const store = new Corestore(root, { manifestVersion: 2 })
|
|
24
|
+
await store.ready()
|
|
25
|
+
|
|
26
|
+
const local = new Local(null, spec, { store })
|
|
27
|
+
await local.ready()
|
|
28
|
+
try {
|
|
29
|
+
const { data } = await local.store.get('master')
|
|
30
|
+
return !!data?.seed
|
|
31
|
+
} finally {
|
|
32
|
+
await local.close()
|
|
33
|
+
await store.close()
|
|
34
|
+
await root.close()
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/lib/spec.js
ADDED
package/src/lib/utils.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {'collection' | 'single' | 'action' | 'handle'} RefKind
|
|
3
|
+
* @typedef {{ kind?: string, schema?: string }} RefInfo
|
|
4
|
+
* Shape of the entries in `meta.refs` — describes a single ref name.
|
|
5
|
+
* `kind` is one of {@link RefKind}, kept as `string` since it originates
|
|
6
|
+
* from a generated spec.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Typed pointer to a single ref (table or handle slot) on a `Handle` or
|
|
11
|
+
* `Local`. Operators (`put`/`get`/`open`/...) take a `Ref` as their first
|
|
12
|
+
* argument and dispatch through the owning handle's store.
|
|
13
|
+
*/
|
|
14
|
+
export class Ref {
|
|
15
|
+
/**
|
|
16
|
+
* @param {any} handle Owner — a `Handle` (or `Local`) the ref lives on.
|
|
17
|
+
* @param {string} name Ref name as declared in the schema.
|
|
18
|
+
* @param {string} kind Ref kind: `'collection'`, `'single'`, `'action'`, or `'handle'`.
|
|
19
|
+
* @param {string | null} [schema] Fully-qualified schema id, if any.
|
|
20
|
+
*/
|
|
21
|
+
constructor(handle, name, kind, schema = null) {
|
|
22
|
+
this.handle = handle
|
|
23
|
+
this.name = name
|
|
24
|
+
this.kind = kind
|
|
25
|
+
this.schema = schema
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Attach a `Ref` property to `target` for every entry in `refs`. Used by
|
|
31
|
+
* `Handle` and `Local` during `_open()` so callers can write
|
|
32
|
+
* `handle.someRef` instead of looking refs up by name.
|
|
33
|
+
*
|
|
34
|
+
* @param {any} target
|
|
35
|
+
* @param {Record<string, RefInfo>} refs
|
|
36
|
+
*/
|
|
37
|
+
export function attachRefs(target, refs) {
|
|
38
|
+
for (const [name, info] of Object.entries(refs || {})) {
|
|
39
|
+
target[name] = new Ref(target, name, info.kind, info.schema)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import ReadyResource from 'ready-resource'
|
|
2
|
+
|
|
3
|
+
import { Storage } from '@cero-base/core/storage'
|
|
4
|
+
import { CeroError } from '@cero-base/core/errors'
|
|
5
|
+
|
|
6
|
+
import { attachRefs } from '../lib/utils.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {object} LocalOpts
|
|
10
|
+
* @property {any} [root] Pre-existing HypercoreStorage to reuse.
|
|
11
|
+
* @property {any} [store] Pre-existing Corestore to reuse.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Per-device, single-writer storage for cero — holds the master seed,
|
|
16
|
+
* device keypair and any per-handle keypairs. Wraps a hyperbee-backed
|
|
17
|
+
* `Storage` and exposes each local ref as a property of the instance.
|
|
18
|
+
*/
|
|
19
|
+
export class Local extends ReadyResource {
|
|
20
|
+
/**
|
|
21
|
+
* @param {string | null} dir Directory for the local store, or `null` when reusing an external `store`.
|
|
22
|
+
* @param {any} spec Built cero spec — must include `spec.local.database` and `spec.meta.local`.
|
|
23
|
+
* @param {LocalOpts} [opts]
|
|
24
|
+
*/
|
|
25
|
+
constructor(dir, spec, { root, store } = {}) {
|
|
26
|
+
super()
|
|
27
|
+
if (!root && !store && (typeof dir !== 'string' || !dir))
|
|
28
|
+
throw CeroError.REQUIRED('dir, root, or store')
|
|
29
|
+
if (!spec?.local?.database) throw CeroError.REQUIRED('spec.local.database')
|
|
30
|
+
if (!spec?.meta?.local) throw CeroError.REQUIRED('spec.meta.local')
|
|
31
|
+
|
|
32
|
+
this.dir = dir
|
|
33
|
+
this.spec = spec
|
|
34
|
+
this.store = Storage.bee(dir, {
|
|
35
|
+
spec: { database: spec.local.database, meta: spec.meta.local },
|
|
36
|
+
root,
|
|
37
|
+
store
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async _open() {
|
|
42
|
+
await this.store.ready()
|
|
43
|
+
attachRefs(this, this.store.refs)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async _close() {
|
|
47
|
+
await this.store.close()
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { Readable } from 'streamx'
|
|
2
|
+
import { RPCClient, bindCodec } from '@cero-base/core/rpc'
|
|
3
|
+
|
|
4
|
+
import { attachRefs } from '../lib/utils.js'
|
|
5
|
+
|
|
6
|
+
export { put, set, get, del, count, watch, call, open } from '../lib/operators.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {import('@cero-base/core/rpc').RPCClient} BaseRPCClient
|
|
10
|
+
*
|
|
11
|
+
* @typedef {object} RefInfo
|
|
12
|
+
* @property {'single'|'collection'|'action'|'handle'} [kind]
|
|
13
|
+
* @property {string} [schema]
|
|
14
|
+
* @property {string} [type]
|
|
15
|
+
*
|
|
16
|
+
* @typedef {import('@cero-base/core/rpc').Spec & { meta: { ns?: string, refs: Record<string, RefInfo>, handles?: Record<string, Spec> }, handles: Record<string, Spec> }} Spec Built cero spec (schema + rpc + per-handle child specs).
|
|
17
|
+
*
|
|
18
|
+
* @typedef {{ data: any }} SingleResult
|
|
19
|
+
* @typedef {{ data: any[], total: number, size: number }} ListResult
|
|
20
|
+
* @typedef {{ data: any | null }} GetByIdResult
|
|
21
|
+
*
|
|
22
|
+
* @typedef {object} ClientIdentity
|
|
23
|
+
* @property {string} id
|
|
24
|
+
* @property {() => string|null} toPhrase
|
|
25
|
+
*
|
|
26
|
+
* @typedef {object} HandleStub
|
|
27
|
+
* @property {string} id
|
|
28
|
+
* @property {string} type
|
|
29
|
+
* @property {string|null} name
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
// Mixin applied to both Client and Handle so they expose the same row-ops
|
|
33
|
+
// surface as a local cero handle but routed over the wire.
|
|
34
|
+
const operators = {
|
|
35
|
+
/** @param {string} name @returns {string|undefined} */
|
|
36
|
+
schemaOf(name) {
|
|
37
|
+
return this.spec.meta.refs[name]?.schema
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Insert a row over the wire.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} name
|
|
44
|
+
* @param {Record<string, any>} row
|
|
45
|
+
* @returns {Promise<SingleResult>}
|
|
46
|
+
*/
|
|
47
|
+
async put(name, row) {
|
|
48
|
+
const schema = this.schemaOf(name)
|
|
49
|
+
const codec = this.spec.codec
|
|
50
|
+
const input = row?.id ? row : { id: '', ...row }
|
|
51
|
+
const res = await this.rpc.addRow({
|
|
52
|
+
handle: this.id,
|
|
53
|
+
ref: name,
|
|
54
|
+
data: codec.encodeRow(schema, input)
|
|
55
|
+
})
|
|
56
|
+
return { data: codec.decodeRow(schema, res.data) }
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Upsert a row over the wire.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} name
|
|
63
|
+
* @param {Record<string, any>} row
|
|
64
|
+
* @returns {Promise<SingleResult>}
|
|
65
|
+
*/
|
|
66
|
+
async set(name, row) {
|
|
67
|
+
const schema = this.schemaOf(name)
|
|
68
|
+
const codec = this.spec.codec
|
|
69
|
+
const res = await this.rpc.set({
|
|
70
|
+
handle: this.id,
|
|
71
|
+
ref: name,
|
|
72
|
+
data: codec.encodeRow(schema, row)
|
|
73
|
+
})
|
|
74
|
+
return { data: codec.decodeRow(schema, res.data) }
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Read a row (by id string) or a list (by query object). Mirrors the
|
|
79
|
+
* `Storage.get` contract.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} name
|
|
82
|
+
* @param {string | Record<string, any>} [query]
|
|
83
|
+
* @returns {Promise<SingleResult | ListResult | GetByIdResult>}
|
|
84
|
+
*/
|
|
85
|
+
async get(name, query) {
|
|
86
|
+
const schema = this.schemaOf(name)
|
|
87
|
+
const codec = this.spec.codec
|
|
88
|
+
if (typeof query === 'string') {
|
|
89
|
+
const res = await this.rpc.getOne({ handle: this.id, ref: name, id: query })
|
|
90
|
+
return { data: res.data ? codec.decodeRow(schema, res.data) : null }
|
|
91
|
+
}
|
|
92
|
+
const res = await this.rpc.get({
|
|
93
|
+
handle: this.id,
|
|
94
|
+
ref: name,
|
|
95
|
+
query: codec.encodeQuery(query)
|
|
96
|
+
})
|
|
97
|
+
const kind = this.spec.meta.refs[name]?.kind
|
|
98
|
+
const data =
|
|
99
|
+
kind === 'single' ? codec.decodeRow(schema, res.data) : codec.decodeRows(schema, res.data)
|
|
100
|
+
return { data, total: res.total, size: res.size }
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Delete a row by id.
|
|
105
|
+
*
|
|
106
|
+
* @param {string} name
|
|
107
|
+
* @param {string} [id]
|
|
108
|
+
* @returns {Promise<void>}
|
|
109
|
+
*/
|
|
110
|
+
async del(name, id) {
|
|
111
|
+
await this.rpc.del({ handle: this.id, ref: name, id })
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Count matching rows.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} name
|
|
118
|
+
* @param {Record<string, any>} [query]
|
|
119
|
+
* @returns {Promise<{ data: number }>}
|
|
120
|
+
*/
|
|
121
|
+
async count(name, query) {
|
|
122
|
+
const codec = this.spec.codec
|
|
123
|
+
const res = await this.rpc.count({
|
|
124
|
+
handle: this.id,
|
|
125
|
+
ref: name,
|
|
126
|
+
query: codec.encodeQuery(query)
|
|
127
|
+
})
|
|
128
|
+
return { data: res.count }
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Live snapshot stream. Re-emits the latest `get()` shape on every
|
|
133
|
+
* underlying mutation. Destroy the stream to stop watching.
|
|
134
|
+
*
|
|
135
|
+
* @param {string} name
|
|
136
|
+
* @param {Record<string, any>} [query]
|
|
137
|
+
* @returns {import('streamx').Readable}
|
|
138
|
+
*/
|
|
139
|
+
watch(name, query) {
|
|
140
|
+
const refInfo = this.spec.meta.refs[name]
|
|
141
|
+
const schema = refInfo?.schema
|
|
142
|
+
const codec = this.spec.codec
|
|
143
|
+
const wire = this.rpc.watch({
|
|
144
|
+
handle: this.id,
|
|
145
|
+
ref: name,
|
|
146
|
+
query: codec.encodeQuery(query)
|
|
147
|
+
})
|
|
148
|
+
const out = new Readable({
|
|
149
|
+
predestroy() {
|
|
150
|
+
wire.destroy()
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
wire.on('data', (snap) => {
|
|
154
|
+
const data =
|
|
155
|
+
refInfo.kind === 'single'
|
|
156
|
+
? (codec.decodeRow(schema, snap.data) ?? null)
|
|
157
|
+
: codec.decodeRows(schema, snap.data)
|
|
158
|
+
out.push({ data, total: snap.total, size: snap.size })
|
|
159
|
+
})
|
|
160
|
+
wire.on('end', () => out.push(null))
|
|
161
|
+
wire.on('error', (err) => out.destroy(err))
|
|
162
|
+
return out
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Invoke a named action ref over the wire.
|
|
167
|
+
*
|
|
168
|
+
* @param {string} op
|
|
169
|
+
* @param {any} [data]
|
|
170
|
+
* @returns {Promise<void>}
|
|
171
|
+
*/
|
|
172
|
+
async call(op, data) {
|
|
173
|
+
const schema = this.schemaOf(op)
|
|
174
|
+
const codec = this.spec.codec
|
|
175
|
+
const encoded = data ? codec.encodeAction({ [op]: { schema } }, op, data) : null
|
|
176
|
+
await this.rpc.call({ handle: this.id, op, data: encoded })
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Mint a pairing invite for this handle.
|
|
181
|
+
*
|
|
182
|
+
* @param {{ role?: string }} [opts]
|
|
183
|
+
* @returns {Promise<string>}
|
|
184
|
+
*/
|
|
185
|
+
async invite({ role } = {}) {
|
|
186
|
+
const { invite } = await this.rpc.invite({ handle: this.id, role: role || '' })
|
|
187
|
+
return invite
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Revoke a previously issued invite.
|
|
192
|
+
*
|
|
193
|
+
* @param {string} invite
|
|
194
|
+
* @returns {Promise<boolean>}
|
|
195
|
+
*/
|
|
196
|
+
async revoke(invite) {
|
|
197
|
+
const { ok } = await this.rpc.revoke({ handle: this.id, invite })
|
|
198
|
+
return ok
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Re-initialize a `Client` from a recovery phrase. Closes the existing
|
|
204
|
+
* local state and reseeds identity from the phrase.
|
|
205
|
+
*
|
|
206
|
+
* @param {Client} me
|
|
207
|
+
* @param {string} phrase
|
|
208
|
+
* @returns {Promise<Client>}
|
|
209
|
+
*/
|
|
210
|
+
export async function restore(me, phrase) {
|
|
211
|
+
const res = await me.rpc.restore({ phrase })
|
|
212
|
+
me.id = res.id
|
|
213
|
+
me.deviceId = res.deviceId || null
|
|
214
|
+
me.identity = { id: res.id, toPhrase: () => res.phrase || null }
|
|
215
|
+
return me
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* IPC-side RPC client for cero. Wraps an `hrpc` channel and exposes the
|
|
220
|
+
* same handle/ref/row API as a local cero instance, transparently
|
|
221
|
+
* routing every operation across the wire.
|
|
222
|
+
*/
|
|
223
|
+
export class Client extends RPCClient {
|
|
224
|
+
/**
|
|
225
|
+
* @param {any} ipc Framed IPC stream (must be writable).
|
|
226
|
+
* @param {Spec} spec Compiled cero spec (schema + rpc + handles).
|
|
227
|
+
*/
|
|
228
|
+
constructor(ipc, spec) {
|
|
229
|
+
super(ipc, spec)
|
|
230
|
+
Object.assign(this, operators)
|
|
231
|
+
this.id = null
|
|
232
|
+
this.deviceId = null
|
|
233
|
+
this.store = this
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async _open() {
|
|
237
|
+
await super._open()
|
|
238
|
+
const { id, deviceId, phrase } = await this.rpc.init({})
|
|
239
|
+
this.id = id
|
|
240
|
+
this.deviceId = deviceId || null
|
|
241
|
+
this.identity = { id, toPhrase: () => phrase || null }
|
|
242
|
+
attachRefs(this, /** @type {Spec} */ (this.spec).meta.refs)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Create a new child handle of the given type.
|
|
247
|
+
*
|
|
248
|
+
* @param {string} type
|
|
249
|
+
* @param {Record<string, any>} [opts]
|
|
250
|
+
* @returns {Promise<Handle>}
|
|
251
|
+
*/
|
|
252
|
+
async _create(type, opts = {}) {
|
|
253
|
+
const stub = await this.rpc.addHandle({
|
|
254
|
+
ref: type,
|
|
255
|
+
handle: this.id,
|
|
256
|
+
data: this.spec.codec.encodeCreate(opts)
|
|
257
|
+
})
|
|
258
|
+
return new Handle(this, stub.id, stub.type, stub.name || null)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Load an existing child handle by id.
|
|
263
|
+
*
|
|
264
|
+
* @param {string} type
|
|
265
|
+
* @param {string} id
|
|
266
|
+
* @returns {Promise<Handle>}
|
|
267
|
+
*/
|
|
268
|
+
async _load(type, id) {
|
|
269
|
+
const stub = await this.rpc.openHandle({ parent: this.id, row: id })
|
|
270
|
+
return new Handle(this, stub.id, stub.type, stub.name || null)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Join a child handle via invite.
|
|
275
|
+
*
|
|
276
|
+
* @param {string} invite
|
|
277
|
+
* @param {string} type
|
|
278
|
+
* @returns {Promise<Handle>}
|
|
279
|
+
*/
|
|
280
|
+
async _join(invite, type) {
|
|
281
|
+
const stub = await this.rpc.join({ parent: this.id, ref: type, invite })
|
|
282
|
+
return new Handle(this, stub.id, stub.type, stub.name || null)
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Client-side proxy for a remote handle. Exposes the same row-ops surface
|
|
288
|
+
* as `Client` but scoped to a single child handle id, and routes every
|
|
289
|
+
* call through the parent's RPC channel.
|
|
290
|
+
*/
|
|
291
|
+
class Handle {
|
|
292
|
+
/**
|
|
293
|
+
* @param {Client} parent
|
|
294
|
+
* @param {string} id
|
|
295
|
+
* @param {string} type
|
|
296
|
+
* @param {string|null} name
|
|
297
|
+
*/
|
|
298
|
+
constructor(parent, id, type, name) {
|
|
299
|
+
Object.assign(this, operators)
|
|
300
|
+
this.parent = parent
|
|
301
|
+
this.id = id
|
|
302
|
+
this.type = type
|
|
303
|
+
this.name = name
|
|
304
|
+
this.spec = /** @type {Spec} */ (parent.spec).handles[type]
|
|
305
|
+
if (!this.spec.codec) bindCodec(this.spec)
|
|
306
|
+
this.store = this
|
|
307
|
+
attachRefs(this, this.spec.meta.refs)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Underlying RPC channel borrowed from the parent. */
|
|
311
|
+
get rpc() {
|
|
312
|
+
return this.parent.rpc
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Tear down the remote handle without leaving the room. */
|
|
316
|
+
close() {
|
|
317
|
+
return this.parent.rpc.closeHandle({ handle: this.id })
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Tear down the remote handle and drop membership. */
|
|
321
|
+
leave() {
|
|
322
|
+
return this.parent.rpc.leave({ handle: this.id })
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Construct a `Client`, wait for `init` to complete, and return it.
|
|
328
|
+
*
|
|
329
|
+
* @param {any} ipc
|
|
330
|
+
* @param {object} spec
|
|
331
|
+
* @returns {Promise<Client>}
|
|
332
|
+
*/
|
|
333
|
+
export async function connect(ipc, spec) {
|
|
334
|
+
const client = new Client(ipc, spec)
|
|
335
|
+
await client.ready()
|
|
336
|
+
return client
|
|
337
|
+
}
|
package/src/rpc/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RPC barrel — re-exports the `Server`/`Client` classes and their
|
|
3
|
+
* `serve()`/`connect()` helpers so consumers can spin up either side
|
|
4
|
+
* of the cero IPC bridge from a single import.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { Server, serve } from './server.js'
|
|
8
|
+
export { Client, connect } from './client.js'
|