@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.
- package/LICENSE +201 -0
- package/README.md +180 -136
- package/package.json +123 -28
- 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 +58 -440
- 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 -84
- package/src/lib/batch.js +0 -98
- package/src/lib/builder.js +0 -24
- package/src/lib/collection.js +0 -252
- package/src/lib/crypto.js +0 -6
- package/src/lib/index.js +0 -6
- package/src/lib/room.js +0 -145
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
import Autobee from 'autobee'
|
|
2
|
+
import HyperDB from 'hyperdb'
|
|
3
|
+
import Hypercore from 'hypercore'
|
|
4
|
+
import ReadyResource from 'ready-resource'
|
|
5
|
+
import safetyCatch from 'safety-catch'
|
|
6
|
+
import b4a from 'b4a'
|
|
7
|
+
|
|
8
|
+
import { NAMESPACE, SINGLE, COLLECTION } from '../lib/constants.js'
|
|
9
|
+
import { genId, subscribe } from '../lib/utils.js'
|
|
10
|
+
import { CeroError } from '../lib/errors.js'
|
|
11
|
+
import { bootstrap } from './bootstrap.js'
|
|
12
|
+
import { makeDispatcher } from './dispatch.js'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {object} DatabaseOpts
|
|
16
|
+
* @property {any} store Corestore (or compatible) used to materialize the autobee.
|
|
17
|
+
* @property {import('../identity/index.js').Identity} identity Long-lived member identity used to sign writer changes.
|
|
18
|
+
* @property {import('../network/index.js').Network} [network] Optional swarm; required for multi-writer replication.
|
|
19
|
+
* @property {{ database: any, dispatch: any, meta?: { ns?: string, refs?: Record<string, { kind?: string, verb?: string }> } }} spec Generated hyperdb + hyperdispatch spec.
|
|
20
|
+
* @property {Record<string, Function>} [routes] Custom action handlers keyed by route name.
|
|
21
|
+
* @property {string} [namespace] Corestore namespace; defaults to `cero`.
|
|
22
|
+
* @property {Uint8Array | null} [encryptionKey] Optional encryption key; falls back to identity's key.
|
|
23
|
+
* @property {(nodes: any, view: any, host: any) => Promise<void>} [apply] Override the default apply function.
|
|
24
|
+
* @property {Uint8Array | null} [key] Existing autobee key to reopen.
|
|
25
|
+
* @property {import('../identity/index.js').KeyPair} [keyPair] Device writer keypair; defaults to identity's keypair.
|
|
26
|
+
*
|
|
27
|
+
* @typedef {{ data: any | null }} SingleResult
|
|
28
|
+
* @typedef {{ data: any[], total: number, size: number }} ListResult
|
|
29
|
+
* @typedef {{ gt?: string, gte?: string, lt?: string, lte?: string, reverse?: boolean, limit?: number }} Query
|
|
30
|
+
* @typedef {{ kind: string, verb: string, name: string }} Ref
|
|
31
|
+
* @typedef {(ctx: any) => any | Promise<any>} HookFn
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Multi-writer database built on Autobee + HyperDB. Persists rows via a
|
|
36
|
+
* hyperdispatch-encoded log and exposes a `put/get/...` surface mirroring
|
|
37
|
+
* `Storage`, plus membership ops (claim/addWriter/removeWriter) for the
|
|
38
|
+
* shared writer set.
|
|
39
|
+
*/
|
|
40
|
+
export class Database extends ReadyResource {
|
|
41
|
+
/** @param {Partial<DatabaseOpts>} [opts] */
|
|
42
|
+
constructor(opts = {}) {
|
|
43
|
+
super()
|
|
44
|
+
|
|
45
|
+
if (!opts.store) throw CeroError.REQUIRED('store')
|
|
46
|
+
if (!opts.identity) throw CeroError.REQUIRED('identity')
|
|
47
|
+
if (!opts.spec || !opts.spec.database || !opts.spec.dispatch) {
|
|
48
|
+
throw CeroError.INVALID('spec must have database + dispatch')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.store = opts.store
|
|
52
|
+
this.identity = opts.identity
|
|
53
|
+
this.network = opts.network || null
|
|
54
|
+
this.spec = opts.spec
|
|
55
|
+
this.meta = opts.spec.meta || { ns: NAMESPACE, refs: {} }
|
|
56
|
+
this.ns = this.meta.ns || NAMESPACE
|
|
57
|
+
this.refs = this.meta.refs || {}
|
|
58
|
+
this.routes = opts.routes || {}
|
|
59
|
+
this.namespace = opts.namespace || NAMESPACE
|
|
60
|
+
this.encryptionKey = opts.encryptionKey || opts.identity.encryptionKey || null
|
|
61
|
+
this.applyOverride = opts.apply || null
|
|
62
|
+
this.key = opts.key || null
|
|
63
|
+
this.keyPair = opts.keyPair || {
|
|
64
|
+
publicKey: opts.identity.publicKey,
|
|
65
|
+
secretKey: opts.identity.secretKey
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this.bee = null
|
|
69
|
+
this.dispatcher = null
|
|
70
|
+
|
|
71
|
+
this.beforeHooks = new Map()
|
|
72
|
+
this.afterHooks = new Map()
|
|
73
|
+
this.updaters = new Set()
|
|
74
|
+
this.txQueue = null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async _open() {
|
|
78
|
+
await this.store.ready()
|
|
79
|
+
if (this.network) await this.network.ready()
|
|
80
|
+
|
|
81
|
+
await this.openBee()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async _close() {
|
|
85
|
+
if (this._discovery) {
|
|
86
|
+
await this._discovery.destroy()
|
|
87
|
+
this._discovery = null
|
|
88
|
+
}
|
|
89
|
+
if (this.bee) {
|
|
90
|
+
await this.bee.close()
|
|
91
|
+
this.bee = null
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Open the underlying autobee, wire dispatcher + apply, attach to network. */
|
|
96
|
+
async openBee() {
|
|
97
|
+
this.dispatcher = makeDispatcher(this.spec, this.ns, this.routes)
|
|
98
|
+
|
|
99
|
+
const bee = new Autobee(this.store.namespace(this.namespace), this.key, {
|
|
100
|
+
keyPair: this.keyPair,
|
|
101
|
+
encryptionKey: this.encryptionKey,
|
|
102
|
+
optimistic: true,
|
|
103
|
+
wakeup: this.network?.wakeup || undefined,
|
|
104
|
+
open: (b) => HyperDB.bee2(b, this.spec.database, { autoUpdate: true }),
|
|
105
|
+
apply: (nodes, view, host) =>
|
|
106
|
+
(this.applyOverride || this.dispatcher.apply)(nodes, view, host),
|
|
107
|
+
update: async (db) => {
|
|
108
|
+
await db.update()
|
|
109
|
+
for (const fn of this.updaters) fn()
|
|
110
|
+
this.emit('update')
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
await bee.ready()
|
|
115
|
+
await bee.update()
|
|
116
|
+
|
|
117
|
+
this.bee = bee
|
|
118
|
+
this.key = bee.key
|
|
119
|
+
|
|
120
|
+
bee.on('writable', () => this.emit('writable'))
|
|
121
|
+
|
|
122
|
+
if (this.network) {
|
|
123
|
+
this.network.attach(bee)
|
|
124
|
+
this._discovery = this.network.join(bee.discoveryKey)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
get discoveryKey() {
|
|
129
|
+
return this.bee?.discoveryKey || null
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
get writerKey() {
|
|
133
|
+
return this.bee?.local?.key || null
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
get writable() {
|
|
137
|
+
return this.bee?.writable === true
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
get length() {
|
|
141
|
+
return this.bee?.local?.length || 0
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
get view() {
|
|
145
|
+
return this.bee?.view || null
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Register a pre-op hook. Returning `false` from `fn` aborts the op.
|
|
150
|
+
*
|
|
151
|
+
* @param {string} op
|
|
152
|
+
* @param {HookFn} fn
|
|
153
|
+
* @returns {() => void} disposer
|
|
154
|
+
*/
|
|
155
|
+
before(op, fn) {
|
|
156
|
+
return this._addHook(this.beforeHooks, op, fn)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Register a post-op hook, fired after the write succeeds.
|
|
161
|
+
*
|
|
162
|
+
* @param {string} op
|
|
163
|
+
* @param {HookFn} fn
|
|
164
|
+
* @returns {() => void} disposer
|
|
165
|
+
*/
|
|
166
|
+
after(op, fn) {
|
|
167
|
+
return this._addHook(this.afterHooks, op, fn)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Subscribe to local apply notifications. Fires whenever the view updates.
|
|
172
|
+
*
|
|
173
|
+
* @param {() => void} fn
|
|
174
|
+
* @returns {() => void} disposer
|
|
175
|
+
*/
|
|
176
|
+
onUpdate(fn) {
|
|
177
|
+
this.updaters.add(fn)
|
|
178
|
+
return () => this.updaters.delete(fn)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Run all registered `before:op` hooks and emit the corresponding event.
|
|
183
|
+
*
|
|
184
|
+
* @param {string} op
|
|
185
|
+
* @param {any} ctx
|
|
186
|
+
* @returns {Promise<boolean>} `false` if any hook vetoed the op
|
|
187
|
+
*/
|
|
188
|
+
async runBefore(op, ctx) {
|
|
189
|
+
const hooks = this.beforeHooks.get(op)
|
|
190
|
+
if (hooks) {
|
|
191
|
+
for (const fn of hooks) {
|
|
192
|
+
const out = await fn(ctx)
|
|
193
|
+
if (out === false) return false
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
this.emit(`before:${op}`, ctx)
|
|
197
|
+
return true
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Run all registered `after:op` hooks and emit the corresponding event.
|
|
202
|
+
*
|
|
203
|
+
* @param {string} op
|
|
204
|
+
* @param {any} ctx
|
|
205
|
+
* @returns {Promise<void>}
|
|
206
|
+
*/
|
|
207
|
+
async runAfter(op, ctx) {
|
|
208
|
+
const hooks = this.afterHooks.get(op)
|
|
209
|
+
if (hooks) {
|
|
210
|
+
for (const fn of hooks) {
|
|
211
|
+
try {
|
|
212
|
+
await fn(ctx)
|
|
213
|
+
} catch (err) {
|
|
214
|
+
safetyCatch(err)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
this.emit(`after:${op}`, ctx)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Insert (or overwrite by id) a row, stamping `id`/`createdAt`/`updatedAt`.
|
|
223
|
+
*
|
|
224
|
+
* @param {string} name
|
|
225
|
+
* @param {Record<string, any>} row
|
|
226
|
+
* @returns {Promise<SingleResult | null>}
|
|
227
|
+
*/
|
|
228
|
+
async put(name, row) {
|
|
229
|
+
this.guard()
|
|
230
|
+
const ref = this.ref(name)
|
|
231
|
+
const id = row.id || genId()
|
|
232
|
+
const ts = Date.now()
|
|
233
|
+
const stored = { createdAt: ts, updatedAt: ts, ...row, id }
|
|
234
|
+
|
|
235
|
+
const ctx = { op: 'put', name, row: stored }
|
|
236
|
+
if ((await this.runBefore('put', ctx)) === false) return null
|
|
237
|
+
|
|
238
|
+
await this.write([[`add-${ref.verb}`, ctx.row]])
|
|
239
|
+
|
|
240
|
+
const result = { data: ctx.row }
|
|
241
|
+
await this.runAfter('put', { ...ctx, result })
|
|
242
|
+
return result
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Upsert by merging with the existing row, preserving `createdAt`.
|
|
247
|
+
*
|
|
248
|
+
* @param {string} name
|
|
249
|
+
* @param {Record<string, any>} row
|
|
250
|
+
* @returns {Promise<SingleResult | null>}
|
|
251
|
+
*/
|
|
252
|
+
async set(name, row) {
|
|
253
|
+
this.guard()
|
|
254
|
+
const ref = this.ref(name)
|
|
255
|
+
const col = this.col(ref)
|
|
256
|
+
const ts = Date.now()
|
|
257
|
+
const existing =
|
|
258
|
+
ref.kind === SINGLE
|
|
259
|
+
? await this.view.findOne(col, {})
|
|
260
|
+
: row?.id
|
|
261
|
+
? ((await this.view.get(col, { id: row.id })) ?? null)
|
|
262
|
+
: null
|
|
263
|
+
const stored = {
|
|
264
|
+
...existing,
|
|
265
|
+
...row,
|
|
266
|
+
createdAt: existing?.createdAt ?? ts,
|
|
267
|
+
updatedAt: ts
|
|
268
|
+
}
|
|
269
|
+
if (ref.kind === COLLECTION && !stored.id) stored.id = genId()
|
|
270
|
+
|
|
271
|
+
const ctx = { op: 'set', name, row: stored }
|
|
272
|
+
if ((await this.runBefore('set', ctx)) === false) return null
|
|
273
|
+
|
|
274
|
+
await this.write([[`set-${ref.verb}`, ctx.row]])
|
|
275
|
+
|
|
276
|
+
const result = { data: ctx.row }
|
|
277
|
+
await this.runAfter('set', { ...ctx, result })
|
|
278
|
+
return result
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Delete by id (collection) or wipe the whole single-row table.
|
|
283
|
+
*
|
|
284
|
+
* @param {string} name
|
|
285
|
+
* @param {string} [id]
|
|
286
|
+
* @returns {Promise<void | null>}
|
|
287
|
+
*/
|
|
288
|
+
async del(name, id) {
|
|
289
|
+
this.guard()
|
|
290
|
+
const ref = this.ref(name)
|
|
291
|
+
const ctx = { op: 'del', name, id }
|
|
292
|
+
if ((await this.runBefore('del', ctx)) === false) return null
|
|
293
|
+
|
|
294
|
+
await this.write([[`del-${ref.verb}`, { id }]])
|
|
295
|
+
|
|
296
|
+
await this.runAfter('del', ctx)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Dispatch a custom action route by name.
|
|
301
|
+
*
|
|
302
|
+
* @param {string} op
|
|
303
|
+
* @param {Record<string, any>} [data]
|
|
304
|
+
* @returns {Promise<void>}
|
|
305
|
+
*/
|
|
306
|
+
async call(op, data = {}) {
|
|
307
|
+
this.guard()
|
|
308
|
+
await this.write([[op, data]])
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Batch every write performed inside `fn` into a single autobee append.
|
|
313
|
+
* Nested calls reuse the outer queue.
|
|
314
|
+
*
|
|
315
|
+
* @template T
|
|
316
|
+
* @param {() => Promise<T> | T} fn
|
|
317
|
+
* @returns {Promise<T>}
|
|
318
|
+
*/
|
|
319
|
+
async tx(fn) {
|
|
320
|
+
if (this.txQueue) return fn()
|
|
321
|
+
const queue = []
|
|
322
|
+
this.txQueue = queue
|
|
323
|
+
let result
|
|
324
|
+
try {
|
|
325
|
+
result = await fn()
|
|
326
|
+
} catch (err) {
|
|
327
|
+
this.txQueue = null
|
|
328
|
+
throw err
|
|
329
|
+
}
|
|
330
|
+
this.txQueue = null
|
|
331
|
+
if (queue.length) await this.write(queue)
|
|
332
|
+
return result
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Encode and append dispatch ops. Buffers into the active `tx` queue if one
|
|
337
|
+
* is open.
|
|
338
|
+
*
|
|
339
|
+
* @param {Array<[string, any]>} ops
|
|
340
|
+
* @returns {Promise<void>}
|
|
341
|
+
*/
|
|
342
|
+
async write(ops) {
|
|
343
|
+
if (this.txQueue) {
|
|
344
|
+
for (const op of ops) this.txQueue.push(op)
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
if (!this.bee.writable) throw CeroError.NOT_WRITABLE('Database')
|
|
348
|
+
const encoded = ops.map(([op, payload]) =>
|
|
349
|
+
this.spec.dispatch.encode(`@${this.ns}/${op}`, payload)
|
|
350
|
+
)
|
|
351
|
+
await this.bee.append(encoded.length === 1 ? encoded[0] : encoded)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Read a row. With no `query`: list all (collection) or fetch the one
|
|
356
|
+
* record (single). With a string id: fetch that specific row.
|
|
357
|
+
*
|
|
358
|
+
* @param {string} name
|
|
359
|
+
* @param {string | Query} [query]
|
|
360
|
+
* @returns {Promise<SingleResult | ListResult>}
|
|
361
|
+
*/
|
|
362
|
+
async get(name, query) {
|
|
363
|
+
this.guard()
|
|
364
|
+
const ref = this.ref(name)
|
|
365
|
+
const col = this.col(ref)
|
|
366
|
+
|
|
367
|
+
if (ref.kind === SINGLE) {
|
|
368
|
+
return { data: await this.view.findOne(col, {}) }
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (typeof query === 'string') {
|
|
372
|
+
return { data: await this.view.get(col, { id: query }) }
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const all = await this.view.find(col, {}).toArray()
|
|
376
|
+
const total = all.length
|
|
377
|
+
const data = paginate(all, query)
|
|
378
|
+
return { data, total, size: data.length }
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Number of rows that match `query` (or total if omitted).
|
|
383
|
+
*
|
|
384
|
+
* @param {string} name
|
|
385
|
+
* @param {Query} [query]
|
|
386
|
+
* @returns {Promise<{ data: number }>}
|
|
387
|
+
*/
|
|
388
|
+
async count(name, query) {
|
|
389
|
+
this.guard()
|
|
390
|
+
const ref = this.ref(name)
|
|
391
|
+
const all = await this.view.find(this.col(ref), {}).toArray()
|
|
392
|
+
return { data: paginate(all, query).length }
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Live snapshot stream — re-emits the latest `get()` result on every
|
|
397
|
+
* underlying mutation. Destroy the stream to stop watching.
|
|
398
|
+
*
|
|
399
|
+
* @param {string} name
|
|
400
|
+
* @param {Query} [query]
|
|
401
|
+
* @returns {import('streamx').Readable}
|
|
402
|
+
*/
|
|
403
|
+
watch(name, query) {
|
|
404
|
+
this.guard()
|
|
405
|
+
this.ref(name)
|
|
406
|
+
return subscribe({
|
|
407
|
+
get: () => this.get(name, query),
|
|
408
|
+
watch: (fn) => this.onUpdate(fn)
|
|
409
|
+
})
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* First-run bootstrap: create the device writer, save it, and swap into it.
|
|
414
|
+
*
|
|
415
|
+
* @param {{ name?: string | null, isMobile?: boolean, recovering?: boolean }} [opts]
|
|
416
|
+
* @returns {Promise<{ id: Uint8Array, writer: import('../identity/index.js').KeyPair }>}
|
|
417
|
+
*/
|
|
418
|
+
bootstrap(opts) {
|
|
419
|
+
this.guard()
|
|
420
|
+
return bootstrap(this, opts)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Claim writership on an existing room by signing our writer key with the
|
|
425
|
+
* member identity and appending optimistically.
|
|
426
|
+
*
|
|
427
|
+
* @param {{ name?: string | null, isMobile?: boolean }} [opts]
|
|
428
|
+
* @returns {Promise<void>}
|
|
429
|
+
*/
|
|
430
|
+
async claim({ name = null, isMobile = false } = {}) {
|
|
431
|
+
this.guard()
|
|
432
|
+
if (this.bee.writable) return
|
|
433
|
+
|
|
434
|
+
const writerKey = this.writerKey
|
|
435
|
+
const sig = this.identity.sign(writerKey)
|
|
436
|
+
const encoded = this.spec.dispatch.encode(`@${this.ns}/claim-writer`, {
|
|
437
|
+
identity: this.identity.publicKey,
|
|
438
|
+
writer: writerKey,
|
|
439
|
+
sig,
|
|
440
|
+
name,
|
|
441
|
+
isMobile
|
|
442
|
+
})
|
|
443
|
+
await this.bee.append(encoded, { optimistic: true })
|
|
444
|
+
await this.bee.update()
|
|
445
|
+
if (!this.bee.writable) await this.whenWritable()
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Resolve once the bee becomes writable, or reject after `timeout` ms.
|
|
450
|
+
*
|
|
451
|
+
* @param {{ timeout?: number }} [opts]
|
|
452
|
+
* @returns {Promise<void>}
|
|
453
|
+
*/
|
|
454
|
+
async whenWritable({ timeout = 30000 } = {}) {
|
|
455
|
+
this.guard()
|
|
456
|
+
if (this.bee.writable) return
|
|
457
|
+
return new Promise((resolve, reject) => {
|
|
458
|
+
let timer = null
|
|
459
|
+
let pollTimer = null
|
|
460
|
+
let done = false
|
|
461
|
+
|
|
462
|
+
const finish = (err) => {
|
|
463
|
+
if (done) return
|
|
464
|
+
done = true
|
|
465
|
+
if (timer) clearTimeout(timer)
|
|
466
|
+
if (pollTimer) clearInterval(pollTimer)
|
|
467
|
+
if (this.bee) this.bee.off('writable', onWritable)
|
|
468
|
+
this.off('close', onClose)
|
|
469
|
+
if (err) reject(err)
|
|
470
|
+
else resolve()
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const onWritable = () => finish()
|
|
474
|
+
const onClose = () => finish(CeroError.CLOSED('Database'))
|
|
475
|
+
const poll = async () => {
|
|
476
|
+
if (done || !this.bee) return
|
|
477
|
+
try {
|
|
478
|
+
await this.bee.update()
|
|
479
|
+
if (!done && this.bee.writable) finish()
|
|
480
|
+
} catch {}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
this.bee.on('writable', onWritable)
|
|
484
|
+
this.on('close', onClose)
|
|
485
|
+
pollTimer = setInterval(poll, 1000)
|
|
486
|
+
if (timeout > 0) {
|
|
487
|
+
timer = setTimeout(() => finish(CeroError.TIMED_OUT('whenWritable')), timeout)
|
|
488
|
+
}
|
|
489
|
+
})
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Add a peer's writer key to the indexer set.
|
|
494
|
+
*
|
|
495
|
+
* @param {Uint8Array} publicKey
|
|
496
|
+
* @returns {Promise<void>}
|
|
497
|
+
*/
|
|
498
|
+
async addWriter(publicKey) {
|
|
499
|
+
return this._write('add-writer', 'addWriter', publicKey)
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Remove a peer's writer key from the indexer set.
|
|
504
|
+
*
|
|
505
|
+
* @param {Uint8Array} publicKey
|
|
506
|
+
* @returns {Promise<void>}
|
|
507
|
+
*/
|
|
508
|
+
async removeWriter(publicKey) {
|
|
509
|
+
return this._write('del-writer', 'removeWriter', publicKey)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
guard() {
|
|
513
|
+
if (this.closing || this.closed) throw CeroError.CLOSED('Database')
|
|
514
|
+
if (!this.bee) throw CeroError.NOT_READY('Database', 'db')
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
ref(name) {
|
|
518
|
+
if (typeof name !== 'string' || !name)
|
|
519
|
+
throw CeroError.INVALID('name must be a non-empty string')
|
|
520
|
+
const ref = this.refs[name]
|
|
521
|
+
if (!ref) throw CeroError.UNKNOWN('ref', name)
|
|
522
|
+
return { name, kind: ref.kind || COLLECTION, verb: ref.verb || name }
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
col(ref) {
|
|
526
|
+
return `@${this.ns}/${ref.name}`
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
_addHook(map, op, fn) {
|
|
530
|
+
if (typeof fn !== 'function') throw CeroError.INVALID('hook fn must be a function')
|
|
531
|
+
let arr = map.get(op)
|
|
532
|
+
if (!arr) map.set(op, (arr = []))
|
|
533
|
+
arr.push(fn)
|
|
534
|
+
return () => {
|
|
535
|
+
const i = arr.indexOf(fn)
|
|
536
|
+
if (i >= 0) arr.splice(i, 1)
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
async _write(verb, hook, publicKey) {
|
|
541
|
+
this.guard()
|
|
542
|
+
if (!b4a.isBuffer(publicKey) && !(publicKey instanceof Uint8Array)) {
|
|
543
|
+
throw CeroError.INVALID('publicKey must be a buffer')
|
|
544
|
+
}
|
|
545
|
+
const ctx = { op: hook, publicKey }
|
|
546
|
+
if ((await this.runBefore(hook, ctx)) === false) return
|
|
547
|
+
const writerKey = Hypercore.key({
|
|
548
|
+
version: this.store.manifestVersion,
|
|
549
|
+
signers: [{ publicKey }]
|
|
550
|
+
})
|
|
551
|
+
const sig = this.identity.sign(writerKey)
|
|
552
|
+
await this.write([
|
|
553
|
+
[
|
|
554
|
+
verb,
|
|
555
|
+
{ master: this.identity.publicKey, writer: writerKey, sig, name: null, isMobile: false }
|
|
556
|
+
]
|
|
557
|
+
])
|
|
558
|
+
await this.runAfter(hook, ctx)
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function paginate(rows, query) {
|
|
563
|
+
if (!query) return rows
|
|
564
|
+
let out = rows
|
|
565
|
+
if (query.gt !== undefined) out = out.filter((r) => r.id > query.gt)
|
|
566
|
+
if (query.gte !== undefined) out = out.filter((r) => r.id >= query.gte)
|
|
567
|
+
if (query.lt !== undefined) out = out.filter((r) => r.id < query.lt)
|
|
568
|
+
if (query.lte !== undefined) out = out.filter((r) => r.id <= query.lte)
|
|
569
|
+
if (query.reverse) out = [...out].reverse()
|
|
570
|
+
if (query.limit !== undefined) out = out.slice(0, query.limit)
|
|
571
|
+
return out
|
|
572
|
+
}
|