@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/src/builder.js ADDED
@@ -0,0 +1,276 @@
1
+ import { join } from 'path'
2
+ import { promises as fs } from 'fs'
3
+
4
+ import Hyperschema from 'hyperschema'
5
+ import HyperdbBuilder from 'hyperdb/builder'
6
+ import Hyperdispatch from 'hyperdispatch'
7
+ import HRPCBuilder from 'hrpc'
8
+
9
+ import { CeroError } from '@cero-base/core/errors'
10
+
11
+ import {
12
+ BUILTINS,
13
+ LOCAL_BUILTINS,
14
+ hyperdbType,
15
+ builtinTypes,
16
+ builtinCollections,
17
+ builtinDispatches,
18
+ localBuiltinTypes,
19
+ localBuiltinCollections,
20
+ rpcTypes,
21
+ rpcCommands
22
+ } from './lib/builtins.js'
23
+
24
+ const NS = 'cero'
25
+
26
+ /**
27
+ * @typedef {import('@cero-base/core/schema').Schema} Schema
28
+ * @typedef {import('@cero-base/core/schema').SchemaDefs} SchemaDefs
29
+ * @typedef {Schema | (SchemaDefs & { local?: SchemaDefs })} SchemaInput
30
+ *
31
+ * @typedef {object} BuildOpts
32
+ * @property {string} [ns] Namespace prefix for emitted schema ids. Defaults to `'cero'`.
33
+ */
34
+
35
+ /**
36
+ * Compile a cero schema into wire-level artifacts and write them to disk.
37
+ *
38
+ * Emits a `main/` tree (schema + hyperdb + dispatch + rpc), a `local/` tree
39
+ * for per-device data, one `handles/<name>/` tree per child handle type, and
40
+ * an `index.js` that re-exports a ready-to-use `spec` object.
41
+ *
42
+ * @param {string} specDir Output directory.
43
+ * @param {SchemaInput} schema Either a `schema(...)` wrapper or its raw defs object.
44
+ * @param {BuildOpts} [opts]
45
+ * @returns {Promise<void>}
46
+ */
47
+ export async function build(specDir, schema, { ns = NS } = {}) {
48
+ const defs = /** @type {SchemaDefs & { local?: SchemaDefs }} */ (schema?.defs || schema)
49
+ if (!defs || typeof defs !== 'object') throw CeroError.REQUIRED('schema')
50
+
51
+ const main = compile(splitMain(defs), ns, BUILTINS)
52
+ const local = defs.local
53
+ ? compile(defs.local, ns, LOCAL_BUILTINS)
54
+ : compile({}, ns, LOCAL_BUILTINS)
55
+ const handles = {}
56
+ for (const [name, child] of Object.entries(splitHandles(defs))) {
57
+ handles[name] = compile(child, ns)
58
+ }
59
+
60
+ emitMain(join(specDir, 'main'), ns, main, { rpc: true })
61
+ emitLocal(join(specDir, 'local'), ns, local)
62
+ for (const [name, handle] of Object.entries(handles)) {
63
+ emitMain(join(specDir, 'handles', name), ns, handle, { rpc: false })
64
+ }
65
+
66
+ const meta = {
67
+ ...main.meta,
68
+ local: local.meta,
69
+ handles: Object.fromEntries(
70
+ Object.entries(handles).map(([n, h]) => [n, { ...h.meta, type: n }])
71
+ )
72
+ }
73
+
74
+ // Hang a `type: <name>` marker on each handle ref in main so the runtime knows which type to open.
75
+ for (const [name] of Object.entries(handles)) {
76
+ if (meta.refs[name]) meta.refs[name] = { kind: 'handle', type: name, schema: `@${ns}/handle` }
77
+ }
78
+
79
+ await fs.writeFile(join(specDir, 'index.js'), wireModule(meta, Object.keys(handles)), 'utf-8')
80
+ }
81
+
82
+ function splitMain(defs) {
83
+ const out = {}
84
+ for (const [k, v] of Object.entries(defs)) {
85
+ if (k === 'local') continue
86
+ if (isPlainHandle(v)) {
87
+ out[k] = { kind: 'handle', type: k }
88
+ continue
89
+ }
90
+ out[k] = v
91
+ }
92
+ return out
93
+ }
94
+
95
+ function splitHandles(defs) {
96
+ const out = {}
97
+ for (const [k, v] of Object.entries(defs)) {
98
+ if (k === 'local') continue
99
+ if (isPlainHandle(v)) out[k] = v
100
+ }
101
+ return out
102
+ }
103
+
104
+ function isPlainHandle(v) {
105
+ return v && typeof v === 'object' && !v.kind && !v.prim
106
+ }
107
+
108
+ function compile(root, ns, builtins = BUILTINS) {
109
+ const ctx = { types: [], collections: [], dispatches: [], meta: { ns, refs: {} }, ns }
110
+
111
+ for (const b of builtins) {
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
+ }
120
+
121
+ for (const [name, node] of Object.entries(root)) {
122
+ if (node.kind === 'handle') {
123
+ ctx.meta.refs[name] = { kind: 'handle', type: node.type || name, schema: `@${ns}/handle` }
124
+ continue
125
+ }
126
+ register(name, node, ctx)
127
+ }
128
+
129
+ return {
130
+ types: ctx.types,
131
+ collections: ctx.collections,
132
+ dispatches: ctx.dispatches,
133
+ meta: ctx.meta
134
+ }
135
+ }
136
+
137
+ function register(name, node, ctx) {
138
+ const fqn = `@${ctx.ns}/${name}`
139
+ if (node.kind === 'action') {
140
+ ctx.types.push({ name, compact: false, fields: fieldsFor(node.fields) })
141
+ ctx.dispatches.push({ name, requestType: fqn })
142
+ ctx.meta.refs[name] = { kind: 'action', path: [name], schema: fqn }
143
+ return
144
+ }
145
+ if (node.kind === 'single') {
146
+ ctx.types.push({ name, compact: false, fields: fieldsFor(node.fields) })
147
+ ctx.collections.push({ name, schema: fqn, key: [] })
148
+ ctx.dispatches.push({ name: `set-${name}`, requestType: fqn })
149
+ ctx.meta.refs[name] = { kind: 'single', path: [name], schema: fqn }
150
+ return
151
+ }
152
+ if (node.kind === 'collection') {
153
+ ctx.types.push({
154
+ name,
155
+ compact: false,
156
+ fields: [
157
+ { name: 'id', type: 'string', required: true },
158
+ { name: 'memberId', type: 'string', required: false },
159
+ { name: 'index', type: 'uint', required: false },
160
+ { name: 'createdAt', type: 'int', required: false },
161
+ { name: 'updatedAt', type: 'int', required: false },
162
+ ...fieldsFor(node.fields)
163
+ ]
164
+ })
165
+ ctx.collections.push({ name, schema: fqn, key: ['id'] })
166
+ ctx.dispatches.push({ name: `add-${name}`, requestType: fqn })
167
+ ctx.dispatches.push({ name: `set-${name}`, requestType: fqn })
168
+ ctx.dispatches.push({ name: `del-${name}`, requestType: `@${ctx.ns}/del-by-id` })
169
+ ctx.meta.refs[name] = { kind: 'collection', path: [name], schema: fqn }
170
+ }
171
+ }
172
+
173
+ function fieldsFor(fields) {
174
+ return Object.entries(fields).map(([name, marker]) => ({
175
+ name,
176
+ type: hyperdbType(marker.prim),
177
+ required: false
178
+ }))
179
+ }
180
+
181
+ function emitMain(dir, ns, { types, collections, dispatches }, { rpc }) {
182
+ const schemaDir = join(dir, 'schema')
183
+ const dbDir = join(dir, 'db')
184
+ const dispatchDir = join(dir, 'dispatch')
185
+ const rpcDir = join(dir, 'rpc')
186
+
187
+ const s = Hyperschema.from(schemaDir)
188
+ const sns = s.namespace(ns)
189
+ for (const desc of builtinTypes()) sns.register(desc)
190
+ for (const desc of types) sns.register(desc)
191
+ if (rpc) for (const desc of rpcTypes()) sns.register(desc)
192
+ Hyperschema.toDisk(s, schemaDir, { esm: true })
193
+
194
+ const db = HyperdbBuilder.from(schemaDir, dbDir)
195
+ const dns = db.namespace(ns)
196
+ for (const desc of builtinCollections(ns)) dns.collections.register(desc)
197
+ for (const desc of collections) dns.collections.register(desc)
198
+ HyperdbBuilder.toDisk(db, dbDir, { esm: true })
199
+
200
+ const d = Hyperdispatch.from(schemaDir, dispatchDir)
201
+ const xns = d.namespace(ns)
202
+ for (const desc of builtinDispatches(ns)) xns.register(desc)
203
+ for (const desc of dispatches) xns.register(desc)
204
+ Hyperdispatch.toDisk(d, dispatchDir, { esm: true })
205
+
206
+ if (rpc) {
207
+ const r = HRPCBuilder.from(schemaDir, rpcDir)
208
+ const rns = r.namespace(ns)
209
+ for (const desc of rpcCommands(ns)) rns.register(desc)
210
+ HRPCBuilder.toDisk(r, rpcDir, { esm: true })
211
+ }
212
+ }
213
+
214
+ function emitLocal(dir, ns, { types, collections }) {
215
+ const schemaDir = join(dir, 'schema')
216
+ const dbDir = join(dir, 'db')
217
+
218
+ const s = Hyperschema.from(schemaDir)
219
+ const sns = s.namespace(ns)
220
+ for (const desc of localBuiltinTypes()) sns.register(desc)
221
+ for (const desc of types) sns.register(desc)
222
+ Hyperschema.toDisk(s, schemaDir, { esm: true })
223
+
224
+ const db = HyperdbBuilder.from(schemaDir, dbDir)
225
+ const dns = db.namespace(ns)
226
+ for (const desc of localBuiltinCollections(ns)) dns.collections.register(desc)
227
+ for (const desc of collections) dns.collections.register(desc)
228
+ HyperdbBuilder.toDisk(db, dbDir, { esm: true })
229
+ }
230
+
231
+ const IMPORTS = {
232
+ database: (n) => `import ${n}Database from './handles/${n}/db/index.js'`,
233
+ dispatch: (n) => `import * as ${n}Dispatch from './handles/${n}/dispatch/index.js'`,
234
+ schema: (n) => `import * as ${n}Schema from './handles/${n}/schema/index.js'`
235
+ }
236
+
237
+ const cap = (s) => s[0].toUpperCase() + s.slice(1)
238
+
239
+ function handleImports(names, kinds) {
240
+ return names.flatMap((n) => kinds.map((k) => IMPORTS[k](n))).join('\n')
241
+ }
242
+
243
+ function handleEntries(names, kinds) {
244
+ return names
245
+ .map((n) => {
246
+ const parts = kinds.map((k) => `${k}: ${n}${cap(k)}`).join(', ')
247
+ return ` ${n}: { ${parts}, meta: meta.handles.${n} }`
248
+ })
249
+ .join(',\n')
250
+ }
251
+
252
+ function wireModule(meta, names) {
253
+ const kinds = ['database', 'dispatch', 'schema']
254
+ return `// autogenerated by cero/build
255
+ import database from './main/db/index.js'
256
+ import * as dispatch from './main/dispatch/index.js'
257
+ import * as schema from './main/schema/index.js'
258
+ import rpc from './main/rpc/index.js'
259
+ import localDatabase from './local/db/index.js'
260
+ ${handleImports(names, kinds)}
261
+
262
+ export const meta = ${JSON.stringify(meta, null, 2)}
263
+
264
+ export const spec = {
265
+ database,
266
+ dispatch,
267
+ schema,
268
+ rpc,
269
+ local: { database: localDatabase },
270
+ meta,
271
+ handles: {
272
+ ${handleEntries(names, kinds)}
273
+ }
274
+ }
275
+ `
276
+ }
@@ -0,0 +1,3 @@
1
+ <claude-mem-context>
2
+
3
+ </claude-mem-context>