@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,45 @@
|
|
|
1
|
+
import ReadyResource from 'ready-resource'
|
|
2
|
+
import safetyCatch from 'safety-catch'
|
|
3
|
+
import FramedStream from 'framed-stream'
|
|
4
|
+
|
|
5
|
+
import { bindCodec } from './index.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Client-side RPC adapter. Mirror of `RPCServer` — wraps an IPC duplex
|
|
9
|
+
* in a length-framed stream and constructs the spec's hrpc binding ready
|
|
10
|
+
* to issue requests. Frames are paused until `_open` runs so handlers
|
|
11
|
+
* registered before `ready()` see a clean slate.
|
|
12
|
+
*/
|
|
13
|
+
export class RPCClient extends ReadyResource {
|
|
14
|
+
/**
|
|
15
|
+
* @param {any} ipc Duplex IPC stream (e.g. a socket or pipe).
|
|
16
|
+
* @param {import('./index.js').Spec} spec Spec object exposing `rpc` and `schema`.
|
|
17
|
+
*/
|
|
18
|
+
constructor(ipc, spec) {
|
|
19
|
+
super()
|
|
20
|
+
if (!ipc) throw new TypeError('ipc is required')
|
|
21
|
+
if (!spec) throw new TypeError('spec is required')
|
|
22
|
+
if (!spec.rpc) throw new TypeError('spec.rpc is required')
|
|
23
|
+
|
|
24
|
+
this.ipc = ipc
|
|
25
|
+
this.spec = spec
|
|
26
|
+
this.framed = new FramedStream(ipc)
|
|
27
|
+
this.framed.pause()
|
|
28
|
+
this.rpc = new spec.rpc(this.framed)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async _open() {
|
|
32
|
+
if (!this.spec.codec) bindCodec(this.spec)
|
|
33
|
+
this.framed.resume()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async _close() {
|
|
37
|
+
if (this.framed && !this.framed.destroyed) {
|
|
38
|
+
try {
|
|
39
|
+
this.framed.destroy()
|
|
40
|
+
} catch (err) {
|
|
41
|
+
safetyCatch(err)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/rpc/index.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import b4a from 'b4a'
|
|
2
|
+
import c from 'compact-encoding'
|
|
3
|
+
|
|
4
|
+
import { getEncoding } from '../lib/spec/index.js'
|
|
5
|
+
|
|
6
|
+
export { RPCServer } from './server.js'
|
|
7
|
+
export { RPCClient } from './client.js'
|
|
8
|
+
|
|
9
|
+
const EMPTY = b4a.alloc(0)
|
|
10
|
+
|
|
11
|
+
// Default envelope encodings for the @cero namespace, used when the supplied
|
|
12
|
+
// spec does not provide its own rows/query/create types in its schema.
|
|
13
|
+
const DEFAULT_ROWS = getEncoding('@cero/rows')
|
|
14
|
+
const DEFAULT_QUERY = getEncoding('@cero/query')
|
|
15
|
+
const DEFAULT_CREATE = getEncoding('@cero/create')
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {object} Spec
|
|
19
|
+
* @property {{ encode: (type: string, value: any) => Uint8Array, decode: (type: string, buf: Uint8Array) => any, getEncoding?: (name: string) => any }} schema hyperschema-shaped module.
|
|
20
|
+
* @property {{ ns?: string }} [meta] Optional metadata; `ns` controls envelope type fqns.
|
|
21
|
+
* @property {any} [rpc] hrpc constructor used by RPCServer/RPCClient.
|
|
22
|
+
* @property {Codec} [codec] Filled in by `bindCodec`.
|
|
23
|
+
*
|
|
24
|
+
* @typedef {object} Codec
|
|
25
|
+
* @property {(type: string, row: any) => Uint8Array} encodeRow
|
|
26
|
+
* @property {(type: string, buf: Uint8Array) => any} decodeRow
|
|
27
|
+
* @property {(type: string, rows: any[]) => Uint8Array} encodeRows
|
|
28
|
+
* @property {(type: string, buf: Uint8Array) => any[]} decodeRows
|
|
29
|
+
* @property {(q: any) => Uint8Array} encodeQuery
|
|
30
|
+
* @property {(buf: Uint8Array) => any} decodeQuery
|
|
31
|
+
* @property {(row: any) => Uint8Array} encodeCreate
|
|
32
|
+
* @property {(buf: Uint8Array) => any} decodeCreate
|
|
33
|
+
* @property {(handle: any, op: string, data: any) => Uint8Array} encodeAction
|
|
34
|
+
* @property {(handle: any, op: string, buf: Uint8Array) => any} decodeAction
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Attach a `codec` namespace to a hyperschema-shaped `spec`. Looks up
|
|
39
|
+
* envelope types using the spec's namespace (`@<ns>/rows` etc.) and falls
|
|
40
|
+
* back to the bundled `@cero/*` envelopes when the spec has not declared
|
|
41
|
+
* its own. Returns the spec for chaining.
|
|
42
|
+
*
|
|
43
|
+
* @param {Spec} spec
|
|
44
|
+
* @returns {Spec}
|
|
45
|
+
*/
|
|
46
|
+
export function bindCodec(spec) {
|
|
47
|
+
if (!spec) throw new TypeError('spec is required')
|
|
48
|
+
const schema = spec.schema
|
|
49
|
+
if (!schema) throw new TypeError('spec.schema is required')
|
|
50
|
+
|
|
51
|
+
const ns = spec.meta?.ns
|
|
52
|
+
const ROWS = ns ? `@${ns}/rows` : null
|
|
53
|
+
const QUERY = ns ? `@${ns}/query` : null
|
|
54
|
+
const CREATE = ns ? `@${ns}/create` : null
|
|
55
|
+
|
|
56
|
+
spec.codec = {
|
|
57
|
+
encodeRow(type, row) {
|
|
58
|
+
if (row == null) return EMPTY
|
|
59
|
+
return schema.encode(type, row)
|
|
60
|
+
},
|
|
61
|
+
decodeRow(type, buf) {
|
|
62
|
+
if (!buf || buf.length === 0) return undefined
|
|
63
|
+
return schema.decode(type, buf)
|
|
64
|
+
},
|
|
65
|
+
encodeRows(type, rows) {
|
|
66
|
+
const data = (rows ?? []).map((r) => schema.encode(type, r))
|
|
67
|
+
return encodeEnvelope(schema, ROWS, DEFAULT_ROWS, { data })
|
|
68
|
+
},
|
|
69
|
+
decodeRows(type, buf) {
|
|
70
|
+
if (!buf || buf.length === 0) return []
|
|
71
|
+
const env = decodeEnvelope(schema, ROWS, DEFAULT_ROWS, buf)
|
|
72
|
+
const data = env?.data || []
|
|
73
|
+
return data.map((b) => schema.decode(type, b))
|
|
74
|
+
},
|
|
75
|
+
encodeQuery(q) {
|
|
76
|
+
if (q == null) return encodeEnvelope(schema, QUERY, DEFAULT_QUERY, {})
|
|
77
|
+
const { gt, gte, lt, lte, limit, reverse, ...rest } = q
|
|
78
|
+
const env = { gt, gte, lt, lte, limit, reverse }
|
|
79
|
+
if (Object.keys(rest).length > 0) env.data = rest
|
|
80
|
+
return encodeEnvelope(schema, QUERY, DEFAULT_QUERY, env)
|
|
81
|
+
},
|
|
82
|
+
decodeQuery(buf) {
|
|
83
|
+
if (!buf || buf.length === 0) return undefined
|
|
84
|
+
const env = decodeEnvelope(schema, QUERY, DEFAULT_QUERY, buf)
|
|
85
|
+
const out = {}
|
|
86
|
+
// Hyperschema fills optional uint/bool with 0/false defaults; only
|
|
87
|
+
// include fields the encoder actually wrote (truthy).
|
|
88
|
+
if (env.gt) out.gt = env.gt
|
|
89
|
+
if (env.gte) out.gte = env.gte
|
|
90
|
+
if (env.lt) out.lt = env.lt
|
|
91
|
+
if (env.lte) out.lte = env.lte
|
|
92
|
+
if (env.limit) out.limit = env.limit
|
|
93
|
+
if (env.reverse) out.reverse = env.reverse
|
|
94
|
+
if (env.data) Object.assign(out, env.data)
|
|
95
|
+
return Object.keys(out).length === 0 ? undefined : out
|
|
96
|
+
},
|
|
97
|
+
encodeCreate(row) {
|
|
98
|
+
if (row == null) return EMPTY
|
|
99
|
+
return encodeEnvelope(schema, CREATE, DEFAULT_CREATE, row)
|
|
100
|
+
},
|
|
101
|
+
decodeCreate(buf) {
|
|
102
|
+
if (!buf || buf.length === 0) return {}
|
|
103
|
+
return decodeEnvelope(schema, CREATE, DEFAULT_CREATE, buf)
|
|
104
|
+
},
|
|
105
|
+
encodeAction(handle, op, data) {
|
|
106
|
+
const ref = handle?.[op]
|
|
107
|
+
if (!ref || !ref.schema) throw new Error(`unknown action: ${op}`)
|
|
108
|
+
if (data == null) return EMPTY
|
|
109
|
+
return schema.encode(ref.schema, data)
|
|
110
|
+
},
|
|
111
|
+
decodeAction(handle, op, buf) {
|
|
112
|
+
const ref = handle?.[op]
|
|
113
|
+
if (!ref || !ref.schema) throw new Error(`unknown action: ${op}`)
|
|
114
|
+
if (!buf || buf.length === 0) return undefined
|
|
115
|
+
return schema.decode(ref.schema, buf)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return spec
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Tries the spec's own envelope type first; falls back to the built-in @cero
|
|
122
|
+
// encoding bundled with this primitive. This lets callers point a foreign spec
|
|
123
|
+
// at the codec without re-declaring the rows/query/create envelopes.
|
|
124
|
+
function encodeEnvelope(schema, fqn, fallback, value) {
|
|
125
|
+
if (fqn && schemaHas(schema, fqn)) return schema.encode(fqn, value)
|
|
126
|
+
return c.encode(fallback, value)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function decodeEnvelope(schema, fqn, fallback, buf) {
|
|
130
|
+
if (fqn && schemaHas(schema, fqn)) return schema.decode(fqn, buf)
|
|
131
|
+
return c.decode(fallback, buf)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function schemaHas(schema, name) {
|
|
135
|
+
if (typeof schema.getEncoding !== 'function') return false
|
|
136
|
+
try {
|
|
137
|
+
return schema.getEncoding(name) != null
|
|
138
|
+
} catch {
|
|
139
|
+
return false
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import ReadyResource from 'ready-resource'
|
|
2
|
+
import safetyCatch from 'safety-catch'
|
|
3
|
+
import FramedStream from 'framed-stream'
|
|
4
|
+
|
|
5
|
+
import { bindCodec } from './index.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Server-side RPC adapter. Wraps an IPC duplex in a length-framed stream
|
|
9
|
+
* and instantiates the spec's hrpc binding once `bindCodec` has populated
|
|
10
|
+
* envelope encoders. Reads are paused until `_open` runs so the
|
|
11
|
+
* application has a chance to register handlers before frames flow.
|
|
12
|
+
*/
|
|
13
|
+
export class RPCServer extends ReadyResource {
|
|
14
|
+
/**
|
|
15
|
+
* @param {any} ipc Duplex IPC stream (e.g. a socket or pipe).
|
|
16
|
+
* @param {import('./index.js').Spec} spec Spec object exposing `rpc` and `schema`.
|
|
17
|
+
*/
|
|
18
|
+
constructor(ipc, spec) {
|
|
19
|
+
super()
|
|
20
|
+
if (!ipc) throw new TypeError('ipc is required')
|
|
21
|
+
if (!spec) throw new TypeError('spec is required')
|
|
22
|
+
if (!spec.rpc) throw new TypeError('spec.rpc is required')
|
|
23
|
+
|
|
24
|
+
this.ipc = ipc
|
|
25
|
+
this.spec = spec
|
|
26
|
+
this.framed = new FramedStream(ipc)
|
|
27
|
+
this.framed.pause()
|
|
28
|
+
this.rpc = new spec.rpc(this.framed)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async _open() {
|
|
32
|
+
if (!this.spec.codec) bindCodec(this.spec)
|
|
33
|
+
this.framed.resume()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async _close() {
|
|
37
|
+
if (this.framed && !this.framed.destroyed) {
|
|
38
|
+
try {
|
|
39
|
+
this.framed.destroy()
|
|
40
|
+
} catch (err) {
|
|
41
|
+
safetyCatch(err)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import HypercoreStorage from 'hypercore-storage'
|
|
2
|
+
import Corestore from 'corestore'
|
|
3
|
+
import HyperDB from 'hyperdb'
|
|
4
|
+
import ReadyResource from 'ready-resource'
|
|
5
|
+
|
|
6
|
+
import { ROCKS, BEE, SINGLE, COLLECTION } from '../lib/constants.js'
|
|
7
|
+
import { genId, subscribe } from '../lib/utils.js'
|
|
8
|
+
import { CeroError } from '../lib/errors.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {object} StorageOpts
|
|
12
|
+
* @property {{ database: any, meta?: { ns?: string, refs?: Record<string, { kind?: string }> } }} spec
|
|
13
|
+
* @property {'rocks' | 'bee'} backend
|
|
14
|
+
* @property {any} [root] Pre-existing HypercoreStorage to reuse.
|
|
15
|
+
* @property {any} [store] Pre-existing Corestore to reuse.
|
|
16
|
+
*
|
|
17
|
+
* @typedef {{ name: string, kind: string }} Ref
|
|
18
|
+
* @typedef {{ id?: string, createdAt: number, updatedAt: number, [k: string]: any }} StoredRow
|
|
19
|
+
* @typedef {{ data: any }} SingleResult
|
|
20
|
+
* @typedef {{ data: any[], total: number, size: number }} ListResult
|
|
21
|
+
* @typedef {{ data: any | null }} GetByIdResult
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Local, single-writer storage. Backed by either RocksDB (`rocks`) or a
|
|
26
|
+
* Hyperbee on top of corestore (`bee`). Exposes the same `put/get/...`
|
|
27
|
+
* surface as `Database` but without networking or multi-writer apply.
|
|
28
|
+
*/
|
|
29
|
+
export class Storage extends ReadyResource {
|
|
30
|
+
/**
|
|
31
|
+
* @param {string} dir
|
|
32
|
+
* @param {StorageOpts} [opts]
|
|
33
|
+
*/
|
|
34
|
+
constructor(dir, { spec, backend, root, store } = {}) {
|
|
35
|
+
super()
|
|
36
|
+
if (!root && !store && (typeof dir !== 'string' || !dir))
|
|
37
|
+
throw CeroError.REQUIRED('dir, root, or store')
|
|
38
|
+
if (!spec || !spec.database) throw CeroError.REQUIRED('spec.database')
|
|
39
|
+
if (backend !== ROCKS && backend !== BEE)
|
|
40
|
+
throw CeroError.INVALID('backend must be "rocks" or "bee"')
|
|
41
|
+
|
|
42
|
+
this.dir = dir
|
|
43
|
+
this.spec = spec
|
|
44
|
+
this.backend = backend
|
|
45
|
+
this.ns = spec.meta?.ns || 'cero'
|
|
46
|
+
this.refs = spec.meta?.refs || {}
|
|
47
|
+
|
|
48
|
+
this._cf = `${this.ns}/local`
|
|
49
|
+
this._ownsRoot = !root && !store
|
|
50
|
+
this._ownsStore = !store
|
|
51
|
+
this.root = root || null
|
|
52
|
+
this.store = store || null
|
|
53
|
+
this.db = null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Construct a RocksDB-backed Storage.
|
|
58
|
+
*
|
|
59
|
+
* @param {string} dir
|
|
60
|
+
* @param {Omit<StorageOpts, 'backend'>} opts
|
|
61
|
+
*/
|
|
62
|
+
static rocks(dir, opts) {
|
|
63
|
+
return new Storage(dir, { ...opts, backend: ROCKS })
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Construct a Hyperbee-backed Storage.
|
|
68
|
+
*
|
|
69
|
+
* @param {string} dir
|
|
70
|
+
* @param {Omit<StorageOpts, 'backend'>} opts
|
|
71
|
+
*/
|
|
72
|
+
static bee(dir, opts) {
|
|
73
|
+
return new Storage(dir, { ...opts, backend: BEE })
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async _open() {
|
|
77
|
+
if (this._ownsRoot) {
|
|
78
|
+
this.root = new HypercoreStorage(this.dir, { columnFamilies: [this._cf] })
|
|
79
|
+
await this.root.ready()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (this._ownsStore) {
|
|
83
|
+
this.store = new Corestore(this.root, { manifestVersion: 2 })
|
|
84
|
+
await this.store.ready()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (this.backend === ROCKS) {
|
|
88
|
+
const cf = this.root.rocks.columnFamily(this._cf)
|
|
89
|
+
this.db = HyperDB.rocks(cf, this.spec.database)
|
|
90
|
+
} else {
|
|
91
|
+
const core = this.store.get({ name: 'local' })
|
|
92
|
+
await core.ready()
|
|
93
|
+
this.db = HyperDB.bee(core, this.spec.database, { extension: false, autoUpdate: true })
|
|
94
|
+
}
|
|
95
|
+
await this.db.ready()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async _close() {
|
|
99
|
+
if (this.db) {
|
|
100
|
+
if (this.db.flush) await this.db.flush()
|
|
101
|
+
await this.db.close()
|
|
102
|
+
this.db = null
|
|
103
|
+
}
|
|
104
|
+
if (this._ownsStore && this.store) {
|
|
105
|
+
await this.store.close()
|
|
106
|
+
this.store = null
|
|
107
|
+
}
|
|
108
|
+
if (this._ownsRoot && this.root) {
|
|
109
|
+
await this.root.close()
|
|
110
|
+
this.root = null
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Insert (or overwrite by id) a row, stamping `id`/`createdAt`/`updatedAt`.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} name
|
|
118
|
+
* @param {Record<string, any>} row
|
|
119
|
+
* @returns {Promise<SingleResult>}
|
|
120
|
+
*/
|
|
121
|
+
async put(name, row) {
|
|
122
|
+
this._guard()
|
|
123
|
+
const ref = this._ref(name)
|
|
124
|
+
const id = row.id ?? genId()
|
|
125
|
+
const ts = Date.now()
|
|
126
|
+
const stored = { id, createdAt: ts, updatedAt: ts, ...row }
|
|
127
|
+
await this._write(ref, stored)
|
|
128
|
+
return { data: stored }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Upsert by merging with the existing row, preserving `createdAt`.
|
|
133
|
+
*
|
|
134
|
+
* @param {string} name
|
|
135
|
+
* @param {Record<string, any>} row
|
|
136
|
+
* @returns {Promise<SingleResult>}
|
|
137
|
+
*/
|
|
138
|
+
async set(name, row) {
|
|
139
|
+
this._guard()
|
|
140
|
+
const ref = this._ref(name)
|
|
141
|
+
const ts = Date.now()
|
|
142
|
+
const existing = await this._read(ref, row?.id)
|
|
143
|
+
/** @type {StoredRow} */
|
|
144
|
+
const stored = {
|
|
145
|
+
...row,
|
|
146
|
+
createdAt: existing?.createdAt ?? ts,
|
|
147
|
+
updatedAt: ts
|
|
148
|
+
}
|
|
149
|
+
if (ref.kind === COLLECTION && !stored.id) stored.id = genId()
|
|
150
|
+
await this._write(ref, stored)
|
|
151
|
+
return { data: stored }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Delete by id (collection) or wipe the whole single-row table.
|
|
156
|
+
*
|
|
157
|
+
* @param {string} name
|
|
158
|
+
* @param {string} [id]
|
|
159
|
+
* @returns {Promise<void>}
|
|
160
|
+
*/
|
|
161
|
+
async del(name, id) {
|
|
162
|
+
this._guard()
|
|
163
|
+
const ref = this._ref(name)
|
|
164
|
+
const col = this._col(ref)
|
|
165
|
+
await this.db.delete(col, id != null ? { id } : {})
|
|
166
|
+
await this.db.flush()
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Read a row. With no `query`: list all (collection) or fetch the one
|
|
171
|
+
* record (single). With a string id: fetch that specific row.
|
|
172
|
+
*
|
|
173
|
+
* @param {string} name
|
|
174
|
+
* @param {string | Record<string, any>} [query]
|
|
175
|
+
* @returns {Promise<SingleResult | ListResult | GetByIdResult>}
|
|
176
|
+
*/
|
|
177
|
+
async get(name, query) {
|
|
178
|
+
this._guard()
|
|
179
|
+
const ref = this._ref(name)
|
|
180
|
+
const col = this._col(ref)
|
|
181
|
+
|
|
182
|
+
if (ref.kind === SINGLE) return { data: await this.db.findOne(col, {}) }
|
|
183
|
+
|
|
184
|
+
if (typeof query === 'string') {
|
|
185
|
+
const rows = await this.db.find(col, {}).toArray()
|
|
186
|
+
return { data: rows.find((r) => r.id === query) ?? null }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const data = await this.db.find(col, query || {}).toArray()
|
|
190
|
+
const total = (await this.db.find(col, {}).toArray()).length
|
|
191
|
+
return { data, total, size: data.length }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Number of rows that match `query` (or total if omitted).
|
|
196
|
+
*
|
|
197
|
+
* @param {string} name
|
|
198
|
+
* @param {Record<string, any>} [query]
|
|
199
|
+
* @returns {Promise<{ data: number }>}
|
|
200
|
+
*/
|
|
201
|
+
async count(name, query) {
|
|
202
|
+
this._guard()
|
|
203
|
+
const ref = this._ref(name)
|
|
204
|
+
const col = this._col(ref)
|
|
205
|
+
const rows = await this.db.find(col, query || {}).toArray()
|
|
206
|
+
return { data: rows.length }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Live snapshot stream — re-emits the latest `get()` result on every
|
|
211
|
+
* underlying mutation. Destroy the stream to stop watching.
|
|
212
|
+
*
|
|
213
|
+
* @param {string} name
|
|
214
|
+
* @param {Record<string, any>} [query]
|
|
215
|
+
* @returns {import('streamx').Readable}
|
|
216
|
+
*/
|
|
217
|
+
watch(name, query) {
|
|
218
|
+
this._guard()
|
|
219
|
+
this._ref(name)
|
|
220
|
+
return subscribe({
|
|
221
|
+
get: () => this.get(name, query),
|
|
222
|
+
watch: (fn) => {
|
|
223
|
+
this.db.watch(fn)
|
|
224
|
+
return () => this.db.unwatch(fn)
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
_guard() {
|
|
230
|
+
if (this.closing || this.closed) throw CeroError.CLOSED('Storage')
|
|
231
|
+
if (!this.db) throw CeroError.NOT_READY('Storage', 'storage')
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** @param {string} name @returns {Ref} */
|
|
235
|
+
_ref(name) {
|
|
236
|
+
if (typeof name !== 'string' || !name)
|
|
237
|
+
throw CeroError.INVALID('name must be a non-empty string')
|
|
238
|
+
const ref = this.refs[name]
|
|
239
|
+
if (!ref) throw CeroError.UNKNOWN('ref', name)
|
|
240
|
+
return { name, kind: ref.kind || COLLECTION }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** @param {Ref} ref @returns {string} */
|
|
244
|
+
_col(ref) {
|
|
245
|
+
return `@${this.ns}/${ref.name}`
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** @param {Ref} ref @param {string} [id] @returns {Promise<any | null>} */
|
|
249
|
+
async _read(ref, id) {
|
|
250
|
+
if (ref.kind === SINGLE) return this.db.findOne(this._col(ref), {})
|
|
251
|
+
if (id == null) return null
|
|
252
|
+
const rows = await this.db.find(this._col(ref), {}).toArray()
|
|
253
|
+
return rows.find((r) => r.id === id) ?? null
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** @param {Ref} ref @param {Record<string, any>} row @returns {Promise<void>} */
|
|
257
|
+
async _write(ref, row) {
|
|
258
|
+
await this.db.insert(this._col(ref), row)
|
|
259
|
+
await this.db.flush()
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {object} BlobsOpts
|
|
3
|
+
* @property {any} store Corestore (or compatible) used to host the blob core.
|
|
4
|
+
* @property {import('../identity/index.js').Identity} [identity] Identity supplying the default encryption key.
|
|
5
|
+
* @property {import('../network/index.js').Network} [network] Optional network used to announce + replicate the core.
|
|
6
|
+
* @property {Uint8Array} [key] Pre-existing blob core key — joins an existing blob feed.
|
|
7
|
+
* @property {Uint8Array} [encryptionKey] Explicit encryption key, overrides `identity.encryptionKey`.
|
|
8
|
+
*
|
|
9
|
+
* @typedef {object} BlobEntry
|
|
10
|
+
* @property {string} id Opaque z32 id used by `get`/`info`/`delete`.
|
|
11
|
+
* @property {string} hash Content hash (z32-encoded blake2b of the bytes).
|
|
12
|
+
* @property {number} size Logical byte length recorded at put-time.
|
|
13
|
+
* @property {any} meta Caller-provided metadata, stored verbatim.
|
|
14
|
+
* @property {number} createdAt Wall-clock timestamp of the put.
|
|
15
|
+
*
|
|
16
|
+
* @typedef {object} PutOpts
|
|
17
|
+
* @property {any} [meta] Arbitrary metadata persisted alongside the blob.
|
|
18
|
+
* @property {number} [size] Override the recorded size (defaults to byte length).
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Content-addressable blob store backed by a single Hyperblobs core.
|
|
22
|
+
* Writes dedupe locally by content hash, reads/streams are positional
|
|
23
|
+
* descriptors, and the underlying core is attached to the supplied
|
|
24
|
+
* network for replication.
|
|
25
|
+
*/
|
|
26
|
+
export class Blobs extends ReadyResource {
|
|
27
|
+
/** @param {BlobsOpts} [opts] */
|
|
28
|
+
constructor({ store, identity, network, key, encryptionKey }?: BlobsOpts);
|
|
29
|
+
store: any;
|
|
30
|
+
identity: import("../index.js").Identity;
|
|
31
|
+
network: import("../index.js").Network;
|
|
32
|
+
encryptionKey: Uint8Array<ArrayBufferLike>;
|
|
33
|
+
_coreKey: Uint8Array<ArrayBufferLike>;
|
|
34
|
+
core: any;
|
|
35
|
+
hyperblobs: any;
|
|
36
|
+
_discovery: import("../network/discovery.js").Discovery;
|
|
37
|
+
/** @type {Map<string, BlobEntry>} contentHash → entry */
|
|
38
|
+
_index: Map<string, BlobEntry>;
|
|
39
|
+
/** @type {Map<string, BlobEntry>} id → entry (so info/has/delete by id work) */
|
|
40
|
+
_byId: Map<string, BlobEntry>;
|
|
41
|
+
/**
|
|
42
|
+
* Canonical z32 id of the underlying core (null until ready).
|
|
43
|
+
*
|
|
44
|
+
* @returns {string | null}
|
|
45
|
+
*/
|
|
46
|
+
get id(): string | null;
|
|
47
|
+
/**
|
|
48
|
+
* Public key of the underlying core.
|
|
49
|
+
*
|
|
50
|
+
* @returns {Uint8Array | null}
|
|
51
|
+
*/
|
|
52
|
+
get key(): Uint8Array | null;
|
|
53
|
+
/**
|
|
54
|
+
* Discovery key derived from the core key, used for swarm topics.
|
|
55
|
+
*
|
|
56
|
+
* @returns {Uint8Array | null}
|
|
57
|
+
*/
|
|
58
|
+
get discoveryKey(): Uint8Array | null;
|
|
59
|
+
/**
|
|
60
|
+
* Store a buffer or readable stream, returning its opaque id. Identical
|
|
61
|
+
* content from the same writer is deduplicated locally.
|
|
62
|
+
*
|
|
63
|
+
* @param {Uint8Array | import('streamx').Readable} input
|
|
64
|
+
* @param {PutOpts} [opts]
|
|
65
|
+
* @returns {Promise<string>}
|
|
66
|
+
*/
|
|
67
|
+
put(input: Uint8Array | any, { meta, size }?: PutOpts): Promise<string>;
|
|
68
|
+
/**
|
|
69
|
+
* Delete the blob with the given id and forget its local index entry.
|
|
70
|
+
*
|
|
71
|
+
* @param {string} id
|
|
72
|
+
* @returns {Promise<void>}
|
|
73
|
+
*/
|
|
74
|
+
delete(id: string): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Resolve the blob bytes for an id. Fetches from peers if not local.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} id
|
|
79
|
+
* @param {any} [opts]
|
|
80
|
+
* @returns {Promise<Uint8Array>}
|
|
81
|
+
*/
|
|
82
|
+
get(id: string, opts?: any): Promise<Uint8Array>;
|
|
83
|
+
/**
|
|
84
|
+
* Streaming counterpart of `get` — returns a Readable over the blob bytes.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} id
|
|
87
|
+
* @param {any} [opts]
|
|
88
|
+
* @returns {import('streamx').Readable}
|
|
89
|
+
*/
|
|
90
|
+
read(id: string, opts?: any): any;
|
|
91
|
+
/**
|
|
92
|
+
* Look up the locally-cached metadata for a blob id, or `null` if absent.
|
|
93
|
+
*
|
|
94
|
+
* @param {string} id
|
|
95
|
+
* @returns {Promise<BlobEntry | null>}
|
|
96
|
+
*/
|
|
97
|
+
info(id: string): Promise<BlobEntry | null>;
|
|
98
|
+
/**
|
|
99
|
+
* Whether the underlying blocks for the blob are present in the local core.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} id
|
|
102
|
+
* @returns {Promise<boolean>}
|
|
103
|
+
*/
|
|
104
|
+
has(id: string): Promise<boolean>;
|
|
105
|
+
/**
|
|
106
|
+
* Pre-fetch the blocks for `id` from peers without reading the bytes.
|
|
107
|
+
*
|
|
108
|
+
* @param {string} id
|
|
109
|
+
* @param {any} [opts]
|
|
110
|
+
* @returns {Promise<void>}
|
|
111
|
+
*/
|
|
112
|
+
fetch(id: string, opts?: any): Promise<void>;
|
|
113
|
+
_guard(): void;
|
|
114
|
+
}
|
|
115
|
+
export type BlobsOpts = {
|
|
116
|
+
/**
|
|
117
|
+
* Corestore (or compatible) used to host the blob core.
|
|
118
|
+
*/
|
|
119
|
+
store: any;
|
|
120
|
+
/**
|
|
121
|
+
* Identity supplying the default encryption key.
|
|
122
|
+
*/
|
|
123
|
+
identity?: import("../identity/index.js").Identity;
|
|
124
|
+
/**
|
|
125
|
+
* Optional network used to announce + replicate the core.
|
|
126
|
+
*/
|
|
127
|
+
network?: import("../network/index.js").Network;
|
|
128
|
+
/**
|
|
129
|
+
* Pre-existing blob core key — joins an existing blob feed.
|
|
130
|
+
*/
|
|
131
|
+
key?: Uint8Array;
|
|
132
|
+
/**
|
|
133
|
+
* Explicit encryption key, overrides `identity.encryptionKey`.
|
|
134
|
+
*/
|
|
135
|
+
encryptionKey?: Uint8Array;
|
|
136
|
+
};
|
|
137
|
+
export type BlobEntry = {
|
|
138
|
+
/**
|
|
139
|
+
* Opaque z32 id used by `get`/`info`/`delete`.
|
|
140
|
+
*/
|
|
141
|
+
id: string;
|
|
142
|
+
/**
|
|
143
|
+
* Content hash (z32-encoded blake2b of the bytes).
|
|
144
|
+
*/
|
|
145
|
+
hash: string;
|
|
146
|
+
/**
|
|
147
|
+
* Logical byte length recorded at put-time.
|
|
148
|
+
*/
|
|
149
|
+
size: number;
|
|
150
|
+
/**
|
|
151
|
+
* Caller-provided metadata, stored verbatim.
|
|
152
|
+
*/
|
|
153
|
+
meta: any;
|
|
154
|
+
/**
|
|
155
|
+
* Wall-clock timestamp of the put.
|
|
156
|
+
*/
|
|
157
|
+
createdAt: number;
|
|
158
|
+
};
|
|
159
|
+
export type PutOpts = {
|
|
160
|
+
/**
|
|
161
|
+
* Arbitrary metadata persisted alongside the blob.
|
|
162
|
+
*/
|
|
163
|
+
meta?: any;
|
|
164
|
+
/**
|
|
165
|
+
* Override the recorded size (defaults to byte length).
|
|
166
|
+
*/
|
|
167
|
+
size?: number;
|
|
168
|
+
};
|
|
169
|
+
import ReadyResource from 'ready-resource';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-run device provisioning: mint a device writer keypair, persist it as a
|
|
3
|
+
* writer, then swap the autobee's local core over to it. With `recovering`,
|
|
4
|
+
* waits for the first peer-replicated append before swapping.
|
|
5
|
+
*
|
|
6
|
+
* @param {import('./index.js').Database} db
|
|
7
|
+
* @param {{ name?: string | null, isMobile?: boolean, recovering?: boolean }} [opts]
|
|
8
|
+
* @returns {Promise<{ id: Uint8Array, writer: import('../identity/index.js').KeyPair }>}
|
|
9
|
+
*/
|
|
10
|
+
export function bootstrap(db: import("./index.js").Database, { name, isMobile, recovering }?: {
|
|
11
|
+
name?: string | null;
|
|
12
|
+
isMobile?: boolean;
|
|
13
|
+
recovering?: boolean;
|
|
14
|
+
}): Promise<{
|
|
15
|
+
id: Uint8Array;
|
|
16
|
+
writer: import("../identity/index.js").KeyPair;
|
|
17
|
+
}>;
|