@cero-base/core 0.0.5 → 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 +200 -61
- package/package.json +85 -26
- package/src/blobs/index.js +297 -0
- package/src/database/CLAUDE.md +3 -0
- package/src/database/bootstrap.js +76 -0
- package/src/database/dispatch.js +156 -0
- package/src/database/index.js +572 -0
- package/src/identity/CLAUDE.md +3 -0
- package/src/identity/index.js +232 -0
- package/src/index.js +20 -1
- package/src/lib/CLAUDE.md +3 -0
- package/src/lib/constants.js +24 -4
- package/src/lib/errors.js +150 -0
- package/src/lib/schema.js +59 -194
- package/src/lib/spec/index.js +353 -0
- package/src/lib/spec/schema.json +284 -0
- package/src/lib/utils.js +54 -49
- package/src/network/discovery.js +80 -0
- package/src/network/index.js +231 -0
- package/src/pairing/index.js +482 -0
- package/src/pairing/invite.js +199 -0
- package/src/rpc/client.js +45 -0
- package/src/rpc/index.js +141 -0
- package/src/rpc/server.js +45 -0
- package/src/storage/index.js +261 -0
- package/types/blobs/index.d.ts +169 -0
- package/types/database/bootstrap.d.ts +17 -0
- package/types/database/dispatch.d.ts +8 -0
- package/types/database/index.d.ts +329 -0
- package/types/identity/index.d.ts +160 -0
- package/types/index.d.ts +11 -0
- package/types/lib/constants.d.ts +13 -0
- package/types/lib/errors.d.ts +110 -0
- package/types/lib/schema.d.ts +53 -0
- package/types/lib/spec/index.d.ts +95 -0
- package/types/lib/utils.d.ts +39 -0
- package/types/network/discovery.d.ts +44 -0
- package/types/network/index.d.ts +115 -0
- package/types/pairing/index.d.ts +194 -0
- package/types/pairing/invite.d.ts +157 -0
- package/types/rpc/client.d.ts +18 -0
- package/types/rpc/index.d.ts +67 -0
- package/types/rpc/server.d.ts +18 -0
- package/types/storage/index.d.ts +163 -0
- package/src/lib/base.js +0 -98
- package/src/lib/batch.js +0 -95
- package/src/lib/builder.js +0 -24
- package/src/lib/collection.js +0 -208
- package/src/lib/crypto.js +0 -6
- package/src/lib/index.js +0 -6
- package/src/lib/room.js +0 -156
|
@@ -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,98 +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
|
-
this.once('ready', () => {
|
|
32
|
-
this._bindIdentityBase()
|
|
33
|
-
this._initFacades()
|
|
34
|
-
})
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
_bindIdentityBase() {
|
|
38
|
-
if (this._baseUpdateHandler && this._prevBase) {
|
|
39
|
-
this._prevBase.removeListener('update', this._baseUpdateHandler)
|
|
40
|
-
}
|
|
41
|
-
this._baseUpdateHandler = () => this.emit('update')
|
|
42
|
-
this._prevBase = this.identity?.base
|
|
43
|
-
if (this._prevBase) this._prevBase.on('update', this._baseUpdateHandler)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async _swapIdentity(handler, opts) {
|
|
47
|
-
const result = await super._swapIdentity(handler, opts)
|
|
48
|
-
this._bindIdentityBase()
|
|
49
|
-
return result
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
seed() {
|
|
53
|
-
return this.identity?.seed?.get() || null
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async join(input, opts = {}) {
|
|
57
|
-
const isSeed = typeof input === 'string' && input.trim().split(/\s+/).length >= SEED_WORD_COUNT
|
|
58
|
-
if (isSeed) return this.recover(input, opts)
|
|
59
|
-
return this.pair(input, opts)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
_initFacades() {
|
|
63
|
-
const p = this.identity?.profile
|
|
64
|
-
if (p) {
|
|
65
|
-
this._profile = {
|
|
66
|
-
get: () => p.get(),
|
|
67
|
-
set: (data) => p.set(data),
|
|
68
|
-
sub: () => p.subscribe()
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const d = this.identity?.devices
|
|
73
|
-
if (d) {
|
|
74
|
-
this._devices = {
|
|
75
|
-
get: (query) => d.list(query),
|
|
76
|
-
sub: (id) => d.subscribe(id),
|
|
77
|
-
invite: () => this.invites.create().then(({ invite }) => invite)
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
get profile() {
|
|
83
|
-
return this._profile || null
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
get devices() {
|
|
87
|
-
return this._devices || null
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
collection(name) {
|
|
91
|
-
this.schema.require(name, 'db')
|
|
92
|
-
return new Collection(this, name)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
batch() {
|
|
96
|
-
return new Batch(this)
|
|
97
|
-
}
|
|
98
|
-
}
|
package/src/lib/batch.js
DELETED
|
@@ -1,95 +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
|
-
this.operations.push({ action: 'add', name, data, scope })
|
|
28
|
-
return data
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
async del(name, query) {
|
|
32
|
-
const scope = this._validate(name)
|
|
33
|
-
|
|
34
|
-
const col = this.owner.collection(name)
|
|
35
|
-
const [existing] = await col.get(query)
|
|
36
|
-
if (!existing) return false
|
|
37
|
-
|
|
38
|
-
const keys = this.schema.collections[name]?.key || ['id']
|
|
39
|
-
const keyData = keys.reduce((acc, k) => ((acc[k] = existing[k]), acc), {})
|
|
40
|
-
|
|
41
|
-
this.operations.push({ action: 'del', name, data: keyData, scope })
|
|
42
|
-
return true
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async flush() {
|
|
46
|
-
if (this.operations.length === 0) return
|
|
47
|
-
|
|
48
|
-
const localOps = this.operations.filter((op) => isLocal(op.scope))
|
|
49
|
-
const privateOps = this.operations.filter((op) => isPrivate(op.scope))
|
|
50
|
-
const sharedOps = this.operations.filter((op) => isShared(op.scope))
|
|
51
|
-
|
|
52
|
-
for (const op of localOps) {
|
|
53
|
-
const path = this.schema.getPath(op.name)
|
|
54
|
-
if (op.action === 'del') {
|
|
55
|
-
await this.owner.local.delete(path, op.data)
|
|
56
|
-
} else {
|
|
57
|
-
await this.owner.local.insert(path, op.data)
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (privateOps.length > 0) {
|
|
62
|
-
const identity = this.owner.identity
|
|
63
|
-
const ns = identity.ns
|
|
64
|
-
await identity.dispatch(`@${ns}/batch`, {
|
|
65
|
-
operations: privateOps.map((op) => {
|
|
66
|
-
const route = `@${ns}/${op.action}-${this.schema.types.get(op.name)}`
|
|
67
|
-
return identity.spec.dispatch.encode(route, op.data)
|
|
68
|
-
})
|
|
69
|
-
})
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (sharedOps.length > 0) {
|
|
73
|
-
const room = this.owner.room
|
|
74
|
-
const ns = room.ns
|
|
75
|
-
await room.dispatch(`@${ns}/batch`, {
|
|
76
|
-
operations: sharedOps.map((op) => {
|
|
77
|
-
const route = `@${ns}/${op.action}-${this.schema.types.get(op.name)}`
|
|
78
|
-
return room.spec.dispatch.encode(route, op.data)
|
|
79
|
-
})
|
|
80
|
-
})
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
this.operations = []
|
|
84
|
-
|
|
85
|
-
if (localOps.length > 0) {
|
|
86
|
-
this.owner.emit('update')
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
_validate(name) {
|
|
91
|
-
const scope = this.schema.getScope(name)
|
|
92
|
-
if (!scope) throw new Error(`Unknown collection: '${name}'`)
|
|
93
|
-
return scope
|
|
94
|
-
}
|
|
95
|
-
}
|
package/src/lib/builder.js
DELETED
|
@@ -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
|
-
}
|
package/src/lib/collection.js
DELETED
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
import { Readable } from 'streamx'
|
|
2
|
-
import { randomId } from './crypto.js'
|
|
3
|
-
import { debounce, isLocal, isPrivate } from './utils.js'
|
|
4
|
-
|
|
5
|
-
/** Route a write operation based on scope */
|
|
6
|
-
function writeOp(owner, scope, path, ns, op, data, isDel) {
|
|
7
|
-
if (isLocal(scope)) {
|
|
8
|
-
const method = isDel ? 'delete' : 'insert'
|
|
9
|
-
return owner.local[method](path, data).then(() => owner.emit('update'))
|
|
10
|
-
}
|
|
11
|
-
const target = isPrivate(scope) ? owner.identity : owner.room
|
|
12
|
-
return target.dispatch(`${ns}/${op}`, data)
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Collection — 4 verbs: put, get, del, sub
|
|
17
|
-
*
|
|
18
|
-
* @example
|
|
19
|
-
* const tasks = room.collection('tasks')
|
|
20
|
-
*
|
|
21
|
-
* await tasks.put({ text: 'Buy milk', done: false })
|
|
22
|
-
* await tasks.get() // all docs
|
|
23
|
-
* await tasks.get({ done: false }) // filtered
|
|
24
|
-
* await tasks.del({ id: 'abc' })
|
|
25
|
-
* tasks.sub().on('data', cb)
|
|
26
|
-
*/
|
|
27
|
-
export class Collection {
|
|
28
|
-
constructor(owner, name) {
|
|
29
|
-
this.owner = owner
|
|
30
|
-
this.schema = owner.schema
|
|
31
|
-
|
|
32
|
-
const def = this.schema.collections[name]
|
|
33
|
-
this.scope = this.schema.getScope(name)
|
|
34
|
-
this.name = name
|
|
35
|
-
this.type = this.schema.types.get(name)
|
|
36
|
-
this.path = this.schema.getPath(name)
|
|
37
|
-
this.ns = this.path.split('/')[0]
|
|
38
|
-
this.keys = def.key || ['id']
|
|
39
|
-
this.timestamps = this.schema.hasTimestamps(name)
|
|
40
|
-
this.counter = !isLocal(this.scope) // all non-local collections have counter
|
|
41
|
-
this.indexes = def.indexes || null
|
|
42
|
-
this.db = isLocal(this.scope) ? null : isPrivate(this.scope) ? owner.identity.db : owner.room.db
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** Upsert — insert if new, update if exists */
|
|
46
|
-
async put(doc) {
|
|
47
|
-
const data = doc.id ? doc : { ...doc, id: randomId() }
|
|
48
|
-
const keyQuery = this.keys.reduce((acc, k) => ((acc[k] = data[k]), acc), {})
|
|
49
|
-
const existing = data.id ? await this._getOne(keyQuery) : null
|
|
50
|
-
|
|
51
|
-
if (existing) {
|
|
52
|
-
const updated = { ...existing, ...data }
|
|
53
|
-
if (this.timestamps) updated.updatedAt = Date.now()
|
|
54
|
-
return this._write(`set-${this.type}`, updated)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (this.timestamps) data.createdAt = data.createdAt ?? Date.now()
|
|
58
|
-
return this._write(`add-${this.type}`, data)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Get docs — no query returns all, query filters, supports range queries on index */
|
|
62
|
-
async get(query = {}, opts = {}) {
|
|
63
|
-
if (isLocal(this.scope)) {
|
|
64
|
-
const all = await this.owner.local.find(this.path, opts)
|
|
65
|
-
return filter(all, query)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (
|
|
69
|
-
this.counter &&
|
|
70
|
-
(typeof query.gte === 'number' ||
|
|
71
|
-
typeof query.lte === 'number' ||
|
|
72
|
-
typeof query.gt === 'number' ||
|
|
73
|
-
typeof query.lt === 'number')
|
|
74
|
-
) {
|
|
75
|
-
return this._rangeByIndex(query, opts)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return findDocs(this.db, this.path, this.indexes, this.name, query, opts)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** Get count */
|
|
82
|
-
async count() {
|
|
83
|
-
if (!this.counter) return (await this.get()).length
|
|
84
|
-
|
|
85
|
-
const counterPath = `${this.ns}/counters`
|
|
86
|
-
const snapshot = this.db.snapshot()
|
|
87
|
-
try {
|
|
88
|
-
const counter = await snapshot.get(counterPath, { name: this.name })
|
|
89
|
-
return counter?.value || 0
|
|
90
|
-
} finally {
|
|
91
|
-
await snapshot.close()
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** @private Range query on index field via the by-index index */
|
|
96
|
-
async _rangeByIndex(query, opts = {}) {
|
|
97
|
-
const indexPath = `${this.path}-by-index`
|
|
98
|
-
const snapshot = this.db.snapshot()
|
|
99
|
-
try {
|
|
100
|
-
const rangeQuery = {}
|
|
101
|
-
if (query.gte !== undefined) rangeQuery.gte = { index: query.gte }
|
|
102
|
-
if (query.lte !== undefined) rangeQuery.lte = { index: query.lte }
|
|
103
|
-
if (query.gt !== undefined) rangeQuery.gt = { index: query.gt }
|
|
104
|
-
if (query.lt !== undefined) rangeQuery.lt = { index: query.lt }
|
|
105
|
-
return snapshot.find(indexPath, { ...rangeQuery, ...opts }).toArray()
|
|
106
|
-
} finally {
|
|
107
|
-
await snapshot.close()
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** Delete a document by query */
|
|
112
|
-
async del(query) {
|
|
113
|
-
const [existing] = await this.get(query)
|
|
114
|
-
if (!existing) return false
|
|
115
|
-
|
|
116
|
-
const keyData = this.keys.reduce((acc, k) => ((acc[k] = existing[k]), acc), {})
|
|
117
|
-
await this._write(`del-${this.type}`, keyData, true)
|
|
118
|
-
return true
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** Subscribe — returns Readable stream of query results */
|
|
122
|
-
sub(query = {}, opts = {}) {
|
|
123
|
-
const stream = new Readable()
|
|
124
|
-
const emitter = this.owner
|
|
125
|
-
|
|
126
|
-
const refresh = debounce(async () => {
|
|
127
|
-
try {
|
|
128
|
-
const results = await this.get(query, opts)
|
|
129
|
-
stream.push(results)
|
|
130
|
-
} catch (err) {
|
|
131
|
-
stream.destroy(err)
|
|
132
|
-
}
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
refresh()
|
|
136
|
-
emitter.on('update', refresh)
|
|
137
|
-
|
|
138
|
-
const destroy = stream.destroy.bind(stream)
|
|
139
|
-
stream.destroy = (err) => {
|
|
140
|
-
emitter.off('update', refresh)
|
|
141
|
-
return destroy(err)
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return stream
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/** @private Single-doc lookup by key (used internally by put/del) */
|
|
148
|
-
async _getOne(query) {
|
|
149
|
-
if (isLocal(this.scope)) {
|
|
150
|
-
return this.owner.local.get(this.path, query)
|
|
151
|
-
}
|
|
152
|
-
const snapshot = this.db.snapshot()
|
|
153
|
-
try {
|
|
154
|
-
return await snapshot.get(this.path, query)
|
|
155
|
-
} finally {
|
|
156
|
-
await snapshot.close()
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/** @private Route write to local/identity/room */
|
|
161
|
-
async _write(op, data, isDel = false) {
|
|
162
|
-
await writeOp(this.owner, this.scope, this.path, this.ns, op, data, isDel)
|
|
163
|
-
return isDel ? true : data
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// ==================== Private Helpers ====================
|
|
168
|
-
|
|
169
|
-
function filter(all, query) {
|
|
170
|
-
const entries = Object.entries(query)
|
|
171
|
-
if (entries.length === 0) return all
|
|
172
|
-
return all.filter((doc) => entries.every(([k, v]) => doc[k] === v))
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
async function findDocs(db, path, indexes, name, query, opts) {
|
|
176
|
-
const snapshot = db.snapshot()
|
|
177
|
-
|
|
178
|
-
try {
|
|
179
|
-
const rangeKey = query.gte || query.gt || query.lte || query.lt
|
|
180
|
-
if (rangeKey) {
|
|
181
|
-
const index = matchIndex(path, indexes, rangeKey)
|
|
182
|
-
if (!index) throw new Error(`No index found for range query on ${name}`)
|
|
183
|
-
return snapshot.find(index, { ...query, ...opts }).toArray()
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const index = matchIndex(path, indexes, query)
|
|
187
|
-
if (index) {
|
|
188
|
-
return snapshot.find(index, { gte: query, lte: query, ...opts }).toArray()
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const all = await snapshot.find(path, opts).toArray()
|
|
192
|
-
return filter(all, query)
|
|
193
|
-
} finally {
|
|
194
|
-
await snapshot.close()
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function matchIndex(path, indexes, query) {
|
|
199
|
-
if (!indexes) return null
|
|
200
|
-
|
|
201
|
-
const firstField = Object.keys(query)[0]
|
|
202
|
-
if (!firstField) return null
|
|
203
|
-
|
|
204
|
-
for (const [name, fields] of Object.entries(indexes)) {
|
|
205
|
-
if (fields[0] === firstField) return `${path}-${name}`
|
|
206
|
-
}
|
|
207
|
-
return null
|
|
208
|
-
}
|
package/src/lib/crypto.js
DELETED