@cero-base/core 0.2.0 → 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 +180 -136
  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 +58 -440
  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 -84
  47. package/src/lib/batch.js +0 -98
  48. package/src/lib/builder.js +0 -24
  49. package/src/lib/collection.js +0 -252
  50. package/src/lib/crypto.js +0 -6
  51. package/src/lib/index.js +0 -6
  52. package/src/lib/room.js +0 -145
@@ -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
@@ -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'
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
- }