@cero-base/core 0.2.0 → 0.4.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.
Files changed (52) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +180 -136
  3. package/package.json +123 -28
  4. package/src/blobs/index.js +297 -0
  5. package/src/database/CLAUDE.md +3 -0
  6. package/src/database/bootstrap.js +76 -0
  7. package/src/database/dispatch.js +156 -0
  8. package/src/database/index.js +572 -0
  9. package/src/identity/CLAUDE.md +3 -0
  10. package/src/identity/index.js +232 -0
  11. package/src/index.js +20 -1
  12. package/src/lib/CLAUDE.md +3 -0
  13. package/src/lib/constants.js +24 -4
  14. package/src/lib/errors.js +150 -0
  15. package/src/lib/schema.js +58 -440
  16. package/src/lib/spec/index.js +353 -0
  17. package/src/lib/spec/schema.json +284 -0
  18. package/src/lib/utils.js +54 -49
  19. package/src/network/discovery.js +80 -0
  20. package/src/network/index.js +231 -0
  21. package/src/pairing/index.js +482 -0
  22. package/src/pairing/invite.js +199 -0
  23. package/src/rpc/client.js +45 -0
  24. package/src/rpc/index.js +141 -0
  25. package/src/rpc/server.js +45 -0
  26. package/src/storage/index.js +261 -0
  27. package/types/blobs/index.d.ts +169 -0
  28. package/types/database/bootstrap.d.ts +17 -0
  29. package/types/database/dispatch.d.ts +8 -0
  30. package/types/database/index.d.ts +329 -0
  31. package/types/identity/index.d.ts +160 -0
  32. package/types/index.d.ts +11 -0
  33. package/types/lib/constants.d.ts +13 -0
  34. package/types/lib/errors.d.ts +110 -0
  35. package/types/lib/schema.d.ts +53 -0
  36. package/types/lib/spec/index.d.ts +95 -0
  37. package/types/lib/utils.d.ts +39 -0
  38. package/types/network/discovery.d.ts +44 -0
  39. package/types/network/index.d.ts +115 -0
  40. package/types/pairing/index.d.ts +194 -0
  41. package/types/pairing/invite.d.ts +157 -0
  42. package/types/rpc/client.d.ts +18 -0
  43. package/types/rpc/index.d.ts +67 -0
  44. package/types/rpc/server.d.ts +18 -0
  45. package/types/storage/index.d.ts +163 -0
  46. package/src/lib/base.js +0 -84
  47. package/src/lib/batch.js +0 -98
  48. package/src/lib/builder.js +0 -24
  49. package/src/lib/collection.js +0 -252
  50. package/src/lib/crypto.js +0 -6
  51. package/src/lib/index.js +0 -6
  52. package/src/lib/room.js +0 -145
@@ -0,0 +1,67 @@
1
+ /**
2
+ * @typedef {object} Spec
3
+ * @property {{ encode: (type: string, value: any) => Uint8Array, decode: (type: string, buf: Uint8Array) => any, getEncoding?: (name: string) => any }} schema hyperschema-shaped module.
4
+ * @property {{ ns?: string }} [meta] Optional metadata; `ns` controls envelope type fqns.
5
+ * @property {any} [rpc] hrpc constructor used by RPCServer/RPCClient.
6
+ * @property {Codec} [codec] Filled in by `bindCodec`.
7
+ *
8
+ * @typedef {object} Codec
9
+ * @property {(type: string, row: any) => Uint8Array} encodeRow
10
+ * @property {(type: string, buf: Uint8Array) => any} decodeRow
11
+ * @property {(type: string, rows: any[]) => Uint8Array} encodeRows
12
+ * @property {(type: string, buf: Uint8Array) => any[]} decodeRows
13
+ * @property {(q: any) => Uint8Array} encodeQuery
14
+ * @property {(buf: Uint8Array) => any} decodeQuery
15
+ * @property {(row: any) => Uint8Array} encodeCreate
16
+ * @property {(buf: Uint8Array) => any} decodeCreate
17
+ * @property {(handle: any, op: string, data: any) => Uint8Array} encodeAction
18
+ * @property {(handle: any, op: string, buf: Uint8Array) => any} decodeAction
19
+ */
20
+ /**
21
+ * Attach a `codec` namespace to a hyperschema-shaped `spec`. Looks up
22
+ * envelope types using the spec's namespace (`@<ns>/rows` etc.) and falls
23
+ * back to the bundled `@cero/*` envelopes when the spec has not declared
24
+ * its own. Returns the spec for chaining.
25
+ *
26
+ * @param {Spec} spec
27
+ * @returns {Spec}
28
+ */
29
+ export function bindCodec(spec: Spec): Spec;
30
+ export { RPCServer } from "./server.js";
31
+ export { RPCClient } from "./client.js";
32
+ export type Spec = {
33
+ /**
34
+ * hyperschema-shaped module.
35
+ */
36
+ schema: {
37
+ encode: (type: string, value: any) => Uint8Array;
38
+ decode: (type: string, buf: Uint8Array) => any;
39
+ getEncoding?: (name: string) => any;
40
+ };
41
+ /**
42
+ * Optional metadata; `ns` controls envelope type fqns.
43
+ */
44
+ meta?: {
45
+ ns?: string;
46
+ };
47
+ /**
48
+ * hrpc constructor used by RPCServer/RPCClient.
49
+ */
50
+ rpc?: any;
51
+ /**
52
+ * Filled in by `bindCodec`.
53
+ */
54
+ codec?: Codec;
55
+ };
56
+ export type Codec = {
57
+ encodeRow: (type: string, row: any) => Uint8Array;
58
+ decodeRow: (type: string, buf: Uint8Array) => any;
59
+ encodeRows: (type: string, rows: any[]) => Uint8Array;
60
+ decodeRows: (type: string, buf: Uint8Array) => any[];
61
+ encodeQuery: (q: any) => Uint8Array;
62
+ decodeQuery: (buf: Uint8Array) => any;
63
+ encodeCreate: (row: any) => Uint8Array;
64
+ decodeCreate: (buf: Uint8Array) => any;
65
+ encodeAction: (handle: any, op: string, data: any) => Uint8Array;
66
+ decodeAction: (handle: any, op: string, buf: Uint8Array) => any;
67
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Server-side RPC adapter. Wraps an IPC duplex in a length-framed stream
3
+ * and instantiates the spec's hrpc binding once `bindCodec` has populated
4
+ * envelope encoders. Reads are paused until `_open` runs so the
5
+ * application has a chance to register handlers before frames flow.
6
+ */
7
+ export class RPCServer extends ReadyResource {
8
+ /**
9
+ * @param {any} ipc Duplex IPC stream (e.g. a socket or pipe).
10
+ * @param {import('./index.js').Spec} spec Spec object exposing `rpc` and `schema`.
11
+ */
12
+ constructor(ipc: any, spec: import("./index.js").Spec);
13
+ ipc: any;
14
+ spec: import("./index.js").Spec;
15
+ framed: any;
16
+ rpc: any;
17
+ }
18
+ import ReadyResource from 'ready-resource';
@@ -0,0 +1,163 @@
1
+ /**
2
+ * @typedef {object} StorageOpts
3
+ * @property {{ database: any, meta?: { ns?: string, refs?: Record<string, { kind?: string }> } }} spec
4
+ * @property {'rocks' | 'bee'} backend
5
+ * @property {any} [root] Pre-existing HypercoreStorage to reuse.
6
+ * @property {any} [store] Pre-existing Corestore to reuse.
7
+ *
8
+ * @typedef {{ name: string, kind: string }} Ref
9
+ * @typedef {{ id?: string, createdAt: number, updatedAt: number, [k: string]: any }} StoredRow
10
+ * @typedef {{ data: any }} SingleResult
11
+ * @typedef {{ data: any[], total: number, size: number }} ListResult
12
+ * @typedef {{ data: any | null }} GetByIdResult
13
+ */
14
+ /**
15
+ * Local, single-writer storage. Backed by either RocksDB (`rocks`) or a
16
+ * Hyperbee on top of corestore (`bee`). Exposes the same `put/get/...`
17
+ * surface as `Database` but without networking or multi-writer apply.
18
+ */
19
+ export class Storage extends ReadyResource {
20
+ /**
21
+ * Construct a RocksDB-backed Storage.
22
+ *
23
+ * @param {string} dir
24
+ * @param {Omit<StorageOpts, 'backend'>} opts
25
+ */
26
+ static rocks(dir: string, opts: Omit<StorageOpts, "backend">): Storage;
27
+ /**
28
+ * Construct a Hyperbee-backed Storage.
29
+ *
30
+ * @param {string} dir
31
+ * @param {Omit<StorageOpts, 'backend'>} opts
32
+ */
33
+ static bee(dir: string, opts: Omit<StorageOpts, "backend">): Storage;
34
+ /**
35
+ * @param {string} dir
36
+ * @param {StorageOpts} [opts]
37
+ */
38
+ constructor(dir: string, { spec, backend, root, store }?: StorageOpts);
39
+ dir: string;
40
+ spec: {
41
+ database: any;
42
+ meta?: {
43
+ ns?: string;
44
+ refs?: Record<string, {
45
+ kind?: string;
46
+ }>;
47
+ };
48
+ };
49
+ backend: "rocks" | "bee";
50
+ ns: string;
51
+ refs: Record<string, {
52
+ kind?: string;
53
+ }>;
54
+ _cf: string;
55
+ _ownsRoot: boolean;
56
+ _ownsStore: boolean;
57
+ root: any;
58
+ store: any;
59
+ db: any;
60
+ /**
61
+ * Insert (or overwrite by id) a row, stamping `id`/`createdAt`/`updatedAt`.
62
+ *
63
+ * @param {string} name
64
+ * @param {Record<string, any>} row
65
+ * @returns {Promise<SingleResult>}
66
+ */
67
+ put(name: string, row: Record<string, any>): Promise<SingleResult>;
68
+ /**
69
+ * Upsert by merging with the existing row, preserving `createdAt`.
70
+ *
71
+ * @param {string} name
72
+ * @param {Record<string, any>} row
73
+ * @returns {Promise<SingleResult>}
74
+ */
75
+ set(name: string, row: Record<string, any>): Promise<SingleResult>;
76
+ /**
77
+ * Delete by id (collection) or wipe the whole single-row table.
78
+ *
79
+ * @param {string} name
80
+ * @param {string} [id]
81
+ * @returns {Promise<void>}
82
+ */
83
+ del(name: string, id?: string): Promise<void>;
84
+ /**
85
+ * Read a row. With no `query`: list all (collection) or fetch the one
86
+ * record (single). With a string id: fetch that specific row.
87
+ *
88
+ * @param {string} name
89
+ * @param {string | Record<string, any>} [query]
90
+ * @returns {Promise<SingleResult | ListResult | GetByIdResult>}
91
+ */
92
+ get(name: string, query?: string | Record<string, any>): Promise<SingleResult | ListResult | GetByIdResult>;
93
+ /**
94
+ * Number of rows that match `query` (or total if omitted).
95
+ *
96
+ * @param {string} name
97
+ * @param {Record<string, any>} [query]
98
+ * @returns {Promise<{ data: number }>}
99
+ */
100
+ count(name: string, query?: Record<string, any>): Promise<{
101
+ data: number;
102
+ }>;
103
+ /**
104
+ * Live snapshot stream — re-emits the latest `get()` result on every
105
+ * underlying mutation. Destroy the stream to stop watching.
106
+ *
107
+ * @param {string} name
108
+ * @param {Record<string, any>} [query]
109
+ * @returns {import('streamx').Readable}
110
+ */
111
+ watch(name: string, query?: Record<string, any>): any;
112
+ _guard(): void;
113
+ /** @param {string} name @returns {Ref} */
114
+ _ref(name: string): Ref;
115
+ /** @param {Ref} ref @returns {string} */
116
+ _col(ref: Ref): string;
117
+ /** @param {Ref} ref @param {string} [id] @returns {Promise<any | null>} */
118
+ _read(ref: Ref, id?: string): Promise<any | null>;
119
+ /** @param {Ref} ref @param {Record<string, any>} row @returns {Promise<void>} */
120
+ _write(ref: Ref, row: Record<string, any>): Promise<void>;
121
+ }
122
+ export type StorageOpts = {
123
+ spec: {
124
+ database: any;
125
+ meta?: {
126
+ ns?: string;
127
+ refs?: Record<string, {
128
+ kind?: string;
129
+ }>;
130
+ };
131
+ };
132
+ backend: "rocks" | "bee";
133
+ /**
134
+ * Pre-existing HypercoreStorage to reuse.
135
+ */
136
+ root?: any;
137
+ /**
138
+ * Pre-existing Corestore to reuse.
139
+ */
140
+ store?: any;
141
+ };
142
+ export type Ref = {
143
+ name: string;
144
+ kind: string;
145
+ };
146
+ export type StoredRow = {
147
+ id?: string;
148
+ createdAt: number;
149
+ updatedAt: number;
150
+ [k: string]: any;
151
+ };
152
+ export type SingleResult = {
153
+ data: any;
154
+ };
155
+ export type ListResult = {
156
+ data: any[];
157
+ total: number;
158
+ size: number;
159
+ };
160
+ export type GetByIdResult = {
161
+ data: any | null;
162
+ };
163
+ import ReadyResource from 'ready-resource';
package/src/lib/base.js DELETED
@@ -1,84 +0,0 @@
1
- import { Cero } from '@lekinox/cero'
2
- import { SCOPE_PRIVATE, SCOPE_SHARED } from '../lib/constants.js'
3
- import { Collection } from '../lib/collection.js'
4
- import { Batch } from '../lib/batch.js'
5
-
6
- const SEED_WORD_COUNT = 12
7
-
8
- export class CeroBase extends Cero {
9
- constructor(dir, schema, spec, opts = {}) {
10
- super(
11
- dir,
12
- {
13
- ...spec,
14
- identity: {
15
- ...spec.identity,
16
- routes: schema.routes(SCOPE_PRIVATE),
17
- counters: schema.counterNames(SCOPE_PRIVATE)
18
- },
19
- room: {
20
- ...spec.room,
21
- ns: schema.namespace || undefined,
22
- routes: schema.routes(SCOPE_SHARED),
23
- counters: schema.counterNames(SCOPE_SHARED)
24
- }
25
- },
26
- opts
27
- )
28
-
29
- this.schema = schema
30
- this._baseUpdateHandler = null
31
-
32
- // Expose cero's local-scope tables (rooms cache, files, settings, …) and
33
- // user-declared `scope: 'local'` collections via the unified Collection API.
34
- // db.local already has cero's find/insert/delete; we just add .collection.
35
- if (this.local) {
36
- this.local.collection = (name) => new Collection(this, name, { local: true })
37
- }
38
-
39
- this.once('ready', () => {
40
- this._bindIdentityBase()
41
- })
42
- }
43
-
44
- _bindIdentityBase() {
45
- if (this._baseUpdateHandler && this._prevBase) {
46
- this._prevBase.removeListener('update', this._baseUpdateHandler)
47
- }
48
- this._baseUpdateHandler = () => this.emit('update')
49
- this._prevBase = this.identity?.base
50
- if (this._prevBase) this._prevBase.on('update', this._baseUpdateHandler)
51
- }
52
-
53
- async _swapIdentity(handler, opts) {
54
- const result = await super._swapIdentity(handler, opts)
55
- this._bindIdentityBase()
56
- return result
57
- }
58
-
59
- seed() {
60
- return this.identity?.seed?.get() || null
61
- }
62
-
63
- async join(input, opts = {}) {
64
- const isSeed = typeof input === 'string' && input.trim().split(/\s+/).length >= SEED_WORD_COUNT
65
- if (isSeed) return this.recover(input, opts)
66
- return this.pair(input, opts)
67
- }
68
-
69
- /** Create a device-pairing invite string for this identity. */
70
- async invite(opts = {}) {
71
- if (!this.opened) await this.ready()
72
- const { invite } = await this.invites.create(opts)
73
- return invite
74
- }
75
-
76
- collection(name) {
77
- this.schema.require(name, 'db')
78
- return new Collection(this, name)
79
- }
80
-
81
- batch() {
82
- return new Batch(this)
83
- }
84
- }
package/src/lib/batch.js DELETED
@@ -1,98 +0,0 @@
1
- import { randomId } from './crypto.js'
2
- import { isLocal, isPrivate, isShared } from './utils.js'
3
-
4
- /**
5
- * Batch — buffer multiple put/del operations and flush as single dispatch
6
- *
7
- * @example
8
- * const batch = db.batch()
9
- * batch.put('settings', { key: 'a', value: '1' })
10
- * await batch.flush()
11
- */
12
- export class Batch {
13
- constructor(owner) {
14
- this.owner = owner
15
- this.schema = owner.schema
16
- this.operations = []
17
- }
18
-
19
- put(name, doc) {
20
- const scope = this._validate(name)
21
-
22
- const data = doc.id ? doc : { ...doc, id: randomId() }
23
- if (this.schema.hasTimestamps(name)) {
24
- data.createdAt = data.createdAt ?? Date.now()
25
- }
26
-
27
- // If cero doesn't have add-${type}, fall back to set-${type} which is
28
- // itself an upsert.
29
- const action = this.schema.hasAdd(name) ? 'add' : 'set'
30
- this.operations.push({ action, name, data, scope })
31
- return data
32
- }
33
-
34
- async del(name, query) {
35
- const scope = this._validate(name)
36
-
37
- const col = this.owner.collection(name)
38
- const [existing] = await col.get(query)
39
- if (!existing) return false
40
-
41
- const keys = this.schema.keysOf(name)
42
- const keyData = keys.reduce((acc, k) => ((acc[k] = existing[k]), acc), {})
43
-
44
- this.operations.push({ action: 'del', name, data: keyData, scope })
45
- return true
46
- }
47
-
48
- async flush() {
49
- if (this.operations.length === 0) return
50
-
51
- const localOps = this.operations.filter((op) => isLocal(op.scope))
52
- const privateOps = this.operations.filter((op) => isPrivate(op.scope))
53
- const sharedOps = this.operations.filter((op) => isShared(op.scope))
54
-
55
- for (const op of localOps) {
56
- const path = this.schema.getPath(op.name)
57
- if (op.action === 'del') {
58
- await this.owner.local.delete(path, op.data)
59
- } else {
60
- await this.owner.local.insert(path, op.data)
61
- }
62
- }
63
-
64
- if (privateOps.length > 0) {
65
- const identity = this.owner.identity
66
- const ns = identity.ns
67
- await identity.dispatch(`@${ns}/batch`, {
68
- operations: privateOps.map((op) => {
69
- const route = `@${ns}/${op.action}-${this.schema.types.get(op.name)}`
70
- return identity.spec.dispatch.encode(route, op.data)
71
- })
72
- })
73
- }
74
-
75
- if (sharedOps.length > 0) {
76
- const room = this.owner.room
77
- const ns = room.ns
78
- await room.dispatch(`@${ns}/batch`, {
79
- operations: sharedOps.map((op) => {
80
- const route = `@${ns}/${op.action}-${this.schema.types.get(op.name)}`
81
- return room.spec.dispatch.encode(route, op.data)
82
- })
83
- })
84
- }
85
-
86
- this.operations = []
87
-
88
- if (localOps.length > 0) {
89
- this.owner.emit('update')
90
- }
91
- }
92
-
93
- _validate(name) {
94
- const scope = this.schema.getScope(name)
95
- if (!scope) throw new Error(`Unknown collection: '${name}'`)
96
- return scope
97
- }
98
- }
@@ -1,24 +0,0 @@
1
- import { build as ceroBuild } from '@lekinox/cero'
2
-
3
- /**
4
- * Build all specs in one output directory.
5
- *
6
- * @param {string} dir - Output directory
7
- * @param {object} [schema] - Schema from t.schema()
8
- * @param {object} [opts] - Options
9
- * @param {function} [opts.rpc] - RPC builder function ({ register, ns }) => void
10
- */
11
- export async function build(dir, schema, opts = {}) {
12
- const ns = schema?.namespace || null
13
- const builders = {
14
- local: ({ register, ns }) => schema?.build('local', register, ns),
15
- identity: ({ register, ns }) => schema?.build('private', register, ns),
16
- room: ({ register, ns }) => schema?.build('shared', register, ns),
17
- ...(opts.rpc ? { rpc: opts.rpc } : {})
18
- }
19
- if (ns) {
20
- await ceroBuild(dir, ns, builders)
21
- } else {
22
- await ceroBuild(dir, builders)
23
- }
24
- }