@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.
Files changed (52) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +200 -61
  3. package/package.json +85 -26
  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 +59 -194
  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 -98
  47. package/src/lib/batch.js +0 -95
  48. package/src/lib/builder.js +0 -24
  49. package/src/lib/collection.js +0 -208
  50. package/src/lib/crypto.js +0 -6
  51. package/src/lib/index.js +0 -6
  52. 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
- }
@@ -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
- }
@@ -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
@@ -1,6 +0,0 @@
1
- import crypto from 'hypercore-crypto'
2
- import z32 from 'z32'
3
-
4
- export function randomId() {
5
- return z32.encode(crypto.randomBytes(16))
6
- }
package/src/lib/index.js DELETED
@@ -1,6 +0,0 @@
1
- export * from './base.js'
2
- export * from './room.js'
3
- export * from './batch.js'
4
- export * from './collection.js'
5
- export * from './schema.js'
6
- export * from './constants.js'