@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
package/src/lib/collection.js
DELETED
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
import { Readable } from 'streamx'
|
|
2
|
-
import { randomId } from './crypto.js'
|
|
3
|
-
import { debounce, isLocal, isPrivate } from './utils.js'
|
|
4
|
-
import { SCOPE_LOCAL } from './constants.js'
|
|
5
|
-
|
|
6
|
-
/** Route a write operation based on scope */
|
|
7
|
-
function writeOp(owner, scope, path, ns, op, data, isDel) {
|
|
8
|
-
if (isLocal(scope)) {
|
|
9
|
-
const method = isDel ? 'delete' : 'insert'
|
|
10
|
-
return owner.local[method](path, data).then(() => owner.emit('update'))
|
|
11
|
-
}
|
|
12
|
-
const target = isPrivate(scope) ? owner.identity : owner.room
|
|
13
|
-
return target.dispatch(`${ns}/${op}`, data)
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Collection — 4 verbs: put, get, del, sub
|
|
18
|
-
*
|
|
19
|
-
* @example
|
|
20
|
-
* const tasks = room.collection('tasks')
|
|
21
|
-
*
|
|
22
|
-
* await tasks.put({ text: 'Buy milk', done: false })
|
|
23
|
-
* await tasks.get() // all docs
|
|
24
|
-
* await tasks.get({ done: false }) // filtered
|
|
25
|
-
* await tasks.del({ id: 'abc' })
|
|
26
|
-
* tasks.sub().on('data', cb)
|
|
27
|
-
*/
|
|
28
|
-
export class Collection {
|
|
29
|
-
constructor(owner, name, opts = {}) {
|
|
30
|
-
this.owner = owner
|
|
31
|
-
this.schema = owner.schema
|
|
32
|
-
this.name = name
|
|
33
|
-
|
|
34
|
-
if (opts.local) {
|
|
35
|
-
// Forced local resolution — used by db.local.collection(name) to reach
|
|
36
|
-
// cero's local-scope built-ins (rooms cache, files, etc.) and user-
|
|
37
|
-
// declared `scope: 'local'` collections via the same accessor.
|
|
38
|
-
const type = this.schema.localTypeOf(name)
|
|
39
|
-
if (!type) throw new Error(`'${name}' is not a local collection`)
|
|
40
|
-
this.scope = SCOPE_LOCAL
|
|
41
|
-
this.type = type
|
|
42
|
-
this.keys = this.schema.localKeysOf(name)
|
|
43
|
-
this.path = `@local/${name}`
|
|
44
|
-
this.ns = '@local'
|
|
45
|
-
this.hasAdd = true // local merges via direct insert; no dispatch involved
|
|
46
|
-
this.timestamps = false
|
|
47
|
-
this.counter = false
|
|
48
|
-
this.indexes = null
|
|
49
|
-
return
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const def = this.schema.collections[name]
|
|
53
|
-
this.scope = this.schema.getScope(name)
|
|
54
|
-
this.type = this.schema.types.get(name)
|
|
55
|
-
this.path = this.schema.getPath(name)
|
|
56
|
-
this.ns = this.path.split('/')[0]
|
|
57
|
-
this.keys = this.schema.keysOf(name)
|
|
58
|
-
this.hasAdd = this.schema.hasAdd(name)
|
|
59
|
-
this.timestamps = this.schema.hasTimestamps(name)
|
|
60
|
-
this.counter = !isLocal(this.scope) // all non-local collections have counter
|
|
61
|
-
this.indexes = def?.indexes || null
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
get db() {
|
|
65
|
-
if (isLocal(this.scope)) return null
|
|
66
|
-
return isPrivate(this.scope) ? this.owner.identity.db : this.owner.room.db
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Upsert — insert if new, update if exists */
|
|
70
|
-
async put(doc) {
|
|
71
|
-
const isNew = !doc.id
|
|
72
|
-
const data = isNew ? { ...doc, id: randomId() } : doc
|
|
73
|
-
|
|
74
|
-
// If cero doesn't have add-${type} for this built-in (profile, settings),
|
|
75
|
-
// there's no auto-incrementing index — set-${type} is the only path and
|
|
76
|
-
// is itself an idempotent upsert.
|
|
77
|
-
if (!this.hasAdd) {
|
|
78
|
-
return this._write(`set-${this.type}`, data)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Freshly randomized id can't possibly exist — skip the lookup.
|
|
82
|
-
const existing = isNew
|
|
83
|
-
? null
|
|
84
|
-
: await this._getOne(this.keys.reduce((acc, k) => ((acc[k] = data[k]), acc), {}))
|
|
85
|
-
|
|
86
|
-
if (existing) {
|
|
87
|
-
const updated = { ...existing, ...data }
|
|
88
|
-
if (this.timestamps) updated.updatedAt = Date.now()
|
|
89
|
-
return this._write(`set-${this.type}`, updated)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (this.timestamps) data.createdAt = data.createdAt ?? Date.now()
|
|
93
|
-
return this._write(`add-${this.type}`, data)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** Get docs — no query returns all, query filters, supports range queries on index */
|
|
97
|
-
async get(query = {}, opts = {}) {
|
|
98
|
-
if (isLocal(this.scope)) {
|
|
99
|
-
const all = await this.owner.local.find(this.path, opts)
|
|
100
|
-
return filter(all, query)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (
|
|
104
|
-
this.counter &&
|
|
105
|
-
(typeof query.gte === 'number' ||
|
|
106
|
-
typeof query.lte === 'number' ||
|
|
107
|
-
typeof query.gt === 'number' ||
|
|
108
|
-
typeof query.lt === 'number')
|
|
109
|
-
) {
|
|
110
|
-
return this._rangeByIndex(query, opts)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return findDocs(this.db, this.path, this.indexes, this.name, query, opts)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** Get count */
|
|
117
|
-
async count() {
|
|
118
|
-
if (!this.counter) return (await this.get()).length
|
|
119
|
-
|
|
120
|
-
const counterPath = `${this.ns}/counters`
|
|
121
|
-
const snapshot = this.db.snapshot()
|
|
122
|
-
try {
|
|
123
|
-
const counter = await snapshot.get(counterPath, { name: this.name })
|
|
124
|
-
return counter?.value || 0
|
|
125
|
-
} finally {
|
|
126
|
-
await snapshot.close()
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/** @private Range query on index field via the by-index index */
|
|
131
|
-
async _rangeByIndex(query, opts = {}) {
|
|
132
|
-
const indexPath = `${this.path}-by-index`
|
|
133
|
-
const snapshot = this.db.snapshot()
|
|
134
|
-
try {
|
|
135
|
-
const rangeQuery = {}
|
|
136
|
-
if (query.gte !== undefined) rangeQuery.gte = { index: query.gte }
|
|
137
|
-
if (query.lte !== undefined) rangeQuery.lte = { index: query.lte }
|
|
138
|
-
if (query.gt !== undefined) rangeQuery.gt = { index: query.gt }
|
|
139
|
-
if (query.lt !== undefined) rangeQuery.lt = { index: query.lt }
|
|
140
|
-
return snapshot.find(indexPath, { ...rangeQuery, ...opts }).toArray()
|
|
141
|
-
} finally {
|
|
142
|
-
await snapshot.close()
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/** Delete a document by query */
|
|
147
|
-
async del(query) {
|
|
148
|
-
const [existing] = await this.get(query)
|
|
149
|
-
if (!existing) return false
|
|
150
|
-
|
|
151
|
-
const keyData = this.keys.reduce((acc, k) => ((acc[k] = existing[k]), acc), {})
|
|
152
|
-
await this._write(`del-${this.type}`, keyData, true)
|
|
153
|
-
return true
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/** Dispatch a custom op (registered via the schema's handlers) on this collection */
|
|
157
|
-
async dispatch(op, data) {
|
|
158
|
-
if (isLocal(this.scope)) {
|
|
159
|
-
throw new Error(`Cannot dispatch on local collection '${this.name}'`)
|
|
160
|
-
}
|
|
161
|
-
const target = isPrivate(this.scope) ? this.owner.identity : this.owner.room
|
|
162
|
-
return target.dispatch(`${this.ns}/${op}`, data)
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/** Subscribe — returns Readable stream of query results */
|
|
166
|
-
sub(query = {}, opts = {}) {
|
|
167
|
-
const stream = new Readable()
|
|
168
|
-
const emitter = this.owner
|
|
169
|
-
|
|
170
|
-
const refresh = debounce(async () => {
|
|
171
|
-
try {
|
|
172
|
-
const results = await this.get(query, opts)
|
|
173
|
-
stream.push(results)
|
|
174
|
-
} catch (err) {
|
|
175
|
-
stream.destroy(err)
|
|
176
|
-
}
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
refresh()
|
|
180
|
-
emitter.on('update', refresh)
|
|
181
|
-
|
|
182
|
-
const destroy = stream.destroy.bind(stream)
|
|
183
|
-
stream.destroy = (err) => {
|
|
184
|
-
emitter.off('update', refresh)
|
|
185
|
-
return destroy(err)
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return stream
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/** @private Single-doc lookup by key (used internally by put/del) */
|
|
192
|
-
async _getOne(query) {
|
|
193
|
-
if (isLocal(this.scope)) {
|
|
194
|
-
return this.owner.local.get(this.path, query)
|
|
195
|
-
}
|
|
196
|
-
const snapshot = this.db.snapshot()
|
|
197
|
-
try {
|
|
198
|
-
return await snapshot.get(this.path, query)
|
|
199
|
-
} finally {
|
|
200
|
-
await snapshot.close()
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/** @private Route write to local/identity/room */
|
|
205
|
-
async _write(op, data, isDel = false) {
|
|
206
|
-
await writeOp(this.owner, this.scope, this.path, this.ns, op, data, isDel)
|
|
207
|
-
return isDel ? true : data
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// ==================== Private Helpers ====================
|
|
212
|
-
|
|
213
|
-
function filter(all, query) {
|
|
214
|
-
const entries = Object.entries(query)
|
|
215
|
-
if (entries.length === 0) return all
|
|
216
|
-
return all.filter((doc) => entries.every(([k, v]) => doc[k] === v))
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
async function findDocs(db, path, indexes, name, query, opts) {
|
|
220
|
-
const snapshot = db.snapshot()
|
|
221
|
-
|
|
222
|
-
try {
|
|
223
|
-
const rangeKey = query.gte || query.gt || query.lte || query.lt
|
|
224
|
-
if (rangeKey) {
|
|
225
|
-
const index = matchIndex(path, indexes, rangeKey)
|
|
226
|
-
if (!index) throw new Error(`No index found for range query on ${name}`)
|
|
227
|
-
return snapshot.find(index, { ...query, ...opts }).toArray()
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const index = matchIndex(path, indexes, query)
|
|
231
|
-
if (index) {
|
|
232
|
-
return snapshot.find(index, { gte: query, lte: query, ...opts }).toArray()
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const all = await snapshot.find(path, opts).toArray()
|
|
236
|
-
return filter(all, query)
|
|
237
|
-
} finally {
|
|
238
|
-
await snapshot.close()
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function matchIndex(path, indexes, query) {
|
|
243
|
-
if (!indexes) return null
|
|
244
|
-
|
|
245
|
-
const firstField = Object.keys(query)[0]
|
|
246
|
-
if (!firstField) return null
|
|
247
|
-
|
|
248
|
-
for (const [name, fields] of Object.entries(indexes)) {
|
|
249
|
-
if (fields[0] === firstField) return `${path}-${name}`
|
|
250
|
-
}
|
|
251
|
-
return null
|
|
252
|
-
}
|
package/src/lib/crypto.js
DELETED
package/src/lib/index.js
DELETED
package/src/lib/room.js
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import ReadyResource from 'ready-resource'
|
|
2
|
-
import safetyCatch from 'safety-catch'
|
|
3
|
-
import HypercoreId from 'hypercore-id-encoding'
|
|
4
|
-
|
|
5
|
-
import { Collection } from './collection.js'
|
|
6
|
-
import { Batch } from './batch.js'
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Room - Constructable ReadyResource for shared collections
|
|
10
|
-
*
|
|
11
|
-
* @example
|
|
12
|
-
* const room = new Room(db) // create new
|
|
13
|
-
* const room = new Room(db, roomId) // open existing
|
|
14
|
-
* const room = new Room(db, invite) // join via invite
|
|
15
|
-
* await room.ready()
|
|
16
|
-
*
|
|
17
|
-
* const tasks = room.collection('tasks')
|
|
18
|
-
* await tasks.insert({ text: 'Hello', done: false })
|
|
19
|
-
*
|
|
20
|
-
* @extends ReadyResource
|
|
21
|
-
*/
|
|
22
|
-
export class Room extends ReadyResource {
|
|
23
|
-
/**
|
|
24
|
-
* @param {Cero} db - Cero instance
|
|
25
|
-
* @param {string} [arg] - Room ID or invite string (omit to create new)
|
|
26
|
-
* @param {object} [opts] - Options
|
|
27
|
-
* @param {AbortSignal} [opts.signal] - AbortSignal for cancellation
|
|
28
|
-
*/
|
|
29
|
-
constructor(db, arg, opts = {}) {
|
|
30
|
-
super()
|
|
31
|
-
|
|
32
|
-
this.db = db
|
|
33
|
-
this.schema = db.schema
|
|
34
|
-
|
|
35
|
-
this._arg = arg
|
|
36
|
-
this._opts = opts
|
|
37
|
-
|
|
38
|
-
/** @type {import('cero').Room|null} - The underlying cero room */
|
|
39
|
-
this.room = null
|
|
40
|
-
|
|
41
|
-
this.ready().catch(safetyCatch)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** Room ID (z32) */
|
|
45
|
-
get id() {
|
|
46
|
-
return this.room?.id
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Room public key */
|
|
50
|
-
get publicKey() {
|
|
51
|
-
return this.room?.key
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async _open() {
|
|
55
|
-
if (!this.db.opened) await this.db.ready()
|
|
56
|
-
|
|
57
|
-
const type = detectType(this._arg)
|
|
58
|
-
|
|
59
|
-
if (type === 'create') {
|
|
60
|
-
this.room = await this.db.rooms.create(this._opts)
|
|
61
|
-
} else if (type === 'invite') {
|
|
62
|
-
this.room = await this.db.rooms.pair(this._arg, this._opts)
|
|
63
|
-
} else {
|
|
64
|
-
this.room = await this.db.rooms.open(this._arg)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
this.room.on('update', () => this.emit('update'))
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async _close() {
|
|
71
|
-
if (!this.room) return
|
|
72
|
-
this.db.rooms.release(this.room)
|
|
73
|
-
this.room = null
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Get a collection handle for shared data
|
|
78
|
-
* @param {string} name - Collection name
|
|
79
|
-
* @returns {Collection}
|
|
80
|
-
*/
|
|
81
|
-
collection(name) {
|
|
82
|
-
this.schema.require(name, 'room')
|
|
83
|
-
return new Collection(this, name)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Create a batch for multiple operations
|
|
88
|
-
* @returns {Batch}
|
|
89
|
-
*/
|
|
90
|
-
batch() {
|
|
91
|
-
return new Batch(this)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** Shorthand — create invite and return the string */
|
|
95
|
-
async invite(opts = {}) {
|
|
96
|
-
if (!this.opened) await this.ready()
|
|
97
|
-
const { invite } = await this.room.invites.create(opts)
|
|
98
|
-
return invite
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Dispatch an operation to the room
|
|
103
|
-
* @param {string} op - Operation path
|
|
104
|
-
* @param {object} data - Operation data
|
|
105
|
-
*/
|
|
106
|
-
async dispatch(op, data) {
|
|
107
|
-
if (!this.opened) await this.ready()
|
|
108
|
-
return this.room.dispatch(op, data)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Async factory — creates room + awaits ready
|
|
113
|
-
* @param {Cero} db - Cero instance
|
|
114
|
-
* @param {string} [arg] - Room ID, invite, or null to create
|
|
115
|
-
* @param {object} [opts] - Options
|
|
116
|
-
* @returns {Promise<Room>}
|
|
117
|
-
*/
|
|
118
|
-
static async from(db, arg, opts) {
|
|
119
|
-
const room = new this(db, arg, opts)
|
|
120
|
-
await room.ready()
|
|
121
|
-
return room
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// ==================== Private Helpers ====================
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Detect input type: nothing (create), id, or invite
|
|
129
|
-
* @private
|
|
130
|
-
*/
|
|
131
|
-
function detectType(input) {
|
|
132
|
-
if (!input) return 'create'
|
|
133
|
-
|
|
134
|
-
// z32 is ~52 chars, hex is 64 chars
|
|
135
|
-
if (input.length <= 64) {
|
|
136
|
-
try {
|
|
137
|
-
HypercoreId.decode(input)
|
|
138
|
-
return 'id'
|
|
139
|
-
} catch {
|
|
140
|
-
// Not a valid ID, assume invite
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return 'invite'
|
|
145
|
-
}
|