@cero-base/cero 1.19.0 → 2.0.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/README.md +38 -643
- package/package.json +9 -6
- package/src/build/index.js +21 -50
- package/src/build/internal.js +121 -0
- package/src/build/schemas.js +2 -8
- package/src/extensions/handle-sync.js +3 -10
- package/src/extensions/index.js +6 -0
- package/src/extensions/profile-sync.js +0 -5
- package/src/handle/index.js +241 -266
- package/src/index.js +20 -51
- package/src/lib/bluetooth.js +25 -56
- package/src/lib/constants.js +0 -15
- package/src/lib/operators.js +22 -72
- package/src/lib/peek.js +4 -8
- package/src/lib/refs.js +2 -5
- package/src/lib/spec.js +2 -3
- package/src/local/index.js +2 -3
- package/src/rpc/client.js +20 -34
- package/src/rpc/index.js +3 -3
- package/src/rpc/server.js +23 -35
- package/types/build/index.d.ts +1 -9
- package/types/build/{builtins.d.ts → internal.d.ts} +20 -38
- package/types/extensions/handle-sync.d.ts +2 -7
- package/types/extensions/index.d.ts +22 -0
- package/types/extensions/profile-sync.d.ts +0 -4
- package/types/handle/index.d.ts +83 -101
- package/types/index.d.ts +4 -8
- package/types/lib/bluetooth.d.ts +8 -32
- package/types/lib/constants.d.ts +0 -11
- package/types/lib/operators.d.ts +18 -46
- package/types/lib/peek.d.ts +2 -3
- package/types/lib/refs.d.ts +1 -3
- package/types/lib/spec.d.ts +2 -3
- package/types/local/index.d.ts +2 -3
- package/types/rpc/client.d.ts +10 -18
- package/types/rpc/index.d.ts +3 -3
- package/types/rpc/server.d.ts +6 -9
- package/src/build/builtins.js +0 -174
- package/src/lib/internal.js +0 -9
- package/types/lib/internal.d.ts +0 -24
package/src/index.js
CHANGED
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
import { peek } from './lib/peek.js'
|
|
32
32
|
import { t, schema } from './lib/spec.js'
|
|
33
33
|
import { FLUSH, TIMEOUT } from './lib/constants.js'
|
|
34
|
-
import {
|
|
34
|
+
import { registry } from './extensions/index.js'
|
|
35
35
|
|
|
36
36
|
export { Handle, Ref, Local }
|
|
37
37
|
export {
|
|
@@ -80,9 +80,7 @@ export { t, schema } from './lib/spec.js'
|
|
|
80
80
|
*/
|
|
81
81
|
|
|
82
82
|
/**
|
|
83
|
-
* Open (or create) a cero handle at `dir`.
|
|
84
|
-
* identity, then returns a ready root `Handle` with all schema refs
|
|
85
|
-
* attached as properties.
|
|
83
|
+
* Open (or create) a cero handle at `dir`.
|
|
86
84
|
*
|
|
87
85
|
* @param {string} dir Data directory.
|
|
88
86
|
* @param {any} spec Built spec — output of `cero/build`.
|
|
@@ -99,9 +97,7 @@ export async function cero(dir, spec, opts = {}) {
|
|
|
99
97
|
let network = null
|
|
100
98
|
let discovery = null
|
|
101
99
|
let me = null
|
|
102
|
-
//
|
|
103
|
-
// timeout, extension setup) must close whatever opened — else the corestore
|
|
104
|
-
// lock leaks and a retry in the same process hits locked storage.
|
|
100
|
+
// any failure during open must close what opened, or the storage lock leaks
|
|
105
101
|
try {
|
|
106
102
|
await storage.ready()
|
|
107
103
|
await fs.promises.chmod(`${dir}/main`, 0o700)
|
|
@@ -115,15 +111,11 @@ export async function cero(dir, spec, opts = {}) {
|
|
|
115
111
|
|
|
116
112
|
const { identity, fresh } = await resolveIdentity(opts, local)
|
|
117
113
|
const writer = local ? (await local.store.get('keypair')).data : null
|
|
118
|
-
// a supplied identity on a device with no writer
|
|
119
|
-
// authors the identity's pointer core or any core that exists elsewhere.
|
|
120
|
-
// Only an identity cero generated here creates a database
|
|
114
|
+
// a supplied identity on a device with no writer recovers; only a cero-minted identity creates
|
|
121
115
|
const recovering = !writer && (!!opts.key || !fresh)
|
|
122
116
|
const timeout = opts.recoveryTimeout || TIMEOUT
|
|
123
117
|
|
|
124
|
-
//
|
|
125
|
-
// including no channel at all, which would silently rejoin the global network — is a
|
|
126
|
-
// misconfiguration that could leak data across networks, so reject it.
|
|
118
|
+
// a storage remembers its channel; reopening under another would silently rejoin the global network
|
|
127
119
|
if (local) {
|
|
128
120
|
const stored = (await local.store.get('environment')).data?.channel ?? null
|
|
129
121
|
const wanted = opts.channel ?? null
|
|
@@ -141,13 +133,10 @@ export async function cero(dir, spec, opts = {}) {
|
|
|
141
133
|
})
|
|
142
134
|
await network.ready()
|
|
143
135
|
discovery = network.join(identity.topic)
|
|
144
|
-
// a
|
|
145
|
-
// before proceeding only delays first onboarding
|
|
136
|
+
// a fresh identity has no peers yet, flushing the announce would only delay onboarding
|
|
146
137
|
if (!fresh) await Promise.race([discovery.flush(), new Promise((r) => setTimeout(r, FLUSH))])
|
|
147
138
|
|
|
148
|
-
// the pointer core: signed
|
|
149
|
-
// created the identity, read by every device that recovers it. It holds the
|
|
150
|
-
// root database key, so a phrase alone finds the data
|
|
139
|
+
// the pointer core: identity-signed, written once by the creating device, holds the root key
|
|
151
140
|
const manifest = pointerManifest(store, identity)
|
|
152
141
|
const pointer = store.get(
|
|
153
142
|
!writer && !recovering
|
|
@@ -208,25 +197,16 @@ export async function cero(dir, spec, opts = {}) {
|
|
|
208
197
|
}
|
|
209
198
|
}
|
|
210
199
|
|
|
211
|
-
for (const ext of
|
|
200
|
+
for (const ext of registry) {
|
|
212
201
|
if (ext.bundled && opts.extensions === false) continue
|
|
213
202
|
const off = await ext.setup?.(me)
|
|
214
203
|
if (typeof off === 'function') me.once('close', off)
|
|
215
204
|
}
|
|
216
205
|
|
|
217
206
|
if (opts.bluetooth) {
|
|
218
|
-
|
|
219
|
-
// object (pre-1.3 shape, has Central/Server) is still accepted.
|
|
220
|
-
const bt =
|
|
221
|
-
opts.bluetooth === true
|
|
222
|
-
? {}
|
|
223
|
-
: opts.bluetooth.Central
|
|
224
|
-
? { backend: opts.bluetooth }
|
|
225
|
-
: opts.bluetooth
|
|
207
|
+
const bt = opts.bluetooth === true ? {} : opts.bluetooth
|
|
226
208
|
me.bluetooth = new Bluetooth(me, {
|
|
227
|
-
//
|
|
228
|
-
// backend (= lazy-load bare-bluetooth) into an explicit null
|
|
229
|
-
// (= disabled), leaving BLE 'unsupported' on every platform
|
|
209
|
+
// an omitted backend lazy-loads bare-bluetooth, null disables it
|
|
230
210
|
backend: bt.backend,
|
|
231
211
|
autoStart: bt.autoStart !== false,
|
|
232
212
|
maxOutbound: bt.maxOutbound,
|
|
@@ -240,8 +220,7 @@ export async function cero(dir, spec, opts = {}) {
|
|
|
240
220
|
bind(me, null)
|
|
241
221
|
return me
|
|
242
222
|
} catch (err) {
|
|
243
|
-
//
|
|
244
|
-
// tear the raw resources down in reverse order.
|
|
223
|
+
// before the root Handle exists, tear the raw resources down in reverse order
|
|
245
224
|
if (me) await me.close().catch(safetyCatch)
|
|
246
225
|
else {
|
|
247
226
|
await discovery?.destroy().catch(safetyCatch)
|
|
@@ -255,9 +234,7 @@ export async function cero(dir, spec, opts = {}) {
|
|
|
255
234
|
}
|
|
256
235
|
|
|
257
236
|
/**
|
|
258
|
-
* Restore a cero instance from a mnemonic phrase.
|
|
259
|
-
* instance, wipes the on-disk `main/` tree and re-opens with the phrase, which
|
|
260
|
-
* recovers so the writer slot is re-claimed.
|
|
237
|
+
* Restore a cero instance from a mnemonic phrase.
|
|
261
238
|
*
|
|
262
239
|
* @param {Handle} me Existing root handle to restore.
|
|
263
240
|
* @param {string} phrase BIP-39 mnemonic phrase.
|
|
@@ -267,12 +244,10 @@ export async function restore(me, phrase) {
|
|
|
267
244
|
if (!me?._dir) throw CeroError.INVALID('me must be a cero instance')
|
|
268
245
|
if (!phrase || typeof phrase !== 'string') throw CeroError.INVALID('phrase must be a string')
|
|
269
246
|
|
|
270
|
-
// Already this identity? Nothing to restore — return the running instance.
|
|
271
247
|
const current = await Identity.fromSeed(Identity.toSeed(phrase))
|
|
272
248
|
if (current.id === me.identity.id) return me
|
|
273
249
|
|
|
274
|
-
// everything carries over except the
|
|
275
|
-
// particular, or the recovered instance never meets its peers
|
|
250
|
+
// everything carries over except the identity, the channel above all
|
|
276
251
|
const {
|
|
277
252
|
_dir: dir,
|
|
278
253
|
spec,
|
|
@@ -285,9 +260,7 @@ export async function restore(me, phrase) {
|
|
|
285
260
|
return cero(dir, spec, { ...opts, phrase })
|
|
286
261
|
}
|
|
287
262
|
|
|
288
|
-
//
|
|
289
|
-
// `import { define }` and `cero.define(...)` work. Explicit assignments (not
|
|
290
|
-
// `Object.assign`) so tsc reflects them onto the `cero` namespace in the .d.ts.
|
|
263
|
+
// the facade: cero.put and import { put } are the same function
|
|
291
264
|
cero.t = t
|
|
292
265
|
cero.put = put
|
|
293
266
|
cero.set = set
|
|
@@ -306,18 +279,14 @@ cero.restore = restore
|
|
|
306
279
|
cero.schema = schema
|
|
307
280
|
cero.bind = bind
|
|
308
281
|
cero.define = define
|
|
309
|
-
// test-only
|
|
310
|
-
cero.
|
|
311
|
-
//
|
|
312
|
-
// Accepts both `use(a, b)` and `use([a, b])` (and a mix) for easier composition.
|
|
313
|
-
// A named extension replaces any registered one with the same name — so
|
|
314
|
-
// `use(profileSync({ fields }))` reconfigures the bundled default instead of
|
|
315
|
-
// doubling it.
|
|
282
|
+
// test-only
|
|
283
|
+
cero._registry = registry
|
|
284
|
+
// a bare function is shorthand for { setup }; a named extension replaces one of the same name
|
|
316
285
|
cero.use = (...exts) => {
|
|
317
286
|
for (const e of exts.flat().map((e) => (typeof e === 'function' ? { setup: e } : e))) {
|
|
318
|
-
const i = e.name ?
|
|
319
|
-
if (i >= 0)
|
|
320
|
-
else
|
|
287
|
+
const i = e.name ? registry.findIndex((x) => x.name === e.name) : -1
|
|
288
|
+
if (i >= 0) registry[i] = e
|
|
289
|
+
else registry.push(e)
|
|
321
290
|
}
|
|
322
291
|
}
|
|
323
292
|
|
package/src/lib/bluetooth.js
CHANGED
|
@@ -8,19 +8,8 @@ import { Pairing } from '@cero-base/core/pairing'
|
|
|
8
8
|
import { Invite } from '@cero-base/core/invite'
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* `me.bluetooth` — the app-facing surface for nearby (Bluetooth) sync, a thin
|
|
12
|
-
*
|
|
13
|
-
* bytes; capability-gated replication still decides what syncs. Discovery is
|
|
14
|
-
* one topic-derived service UUID at a time (tag `cero-ble`) — the data service
|
|
15
|
-
* sits on a fixed per-tag UUID, so switching topics only retunes the radio.
|
|
16
|
-
*
|
|
17
|
-
* ```js
|
|
18
|
-
* const me = await cero(dir, spec, { channel, bluetooth: true })
|
|
19
|
-
* me.bluetooth.state // 'unsupported' | 'unauthorized' | 'off' | 'waiting' | 'starting' | 'on'
|
|
20
|
-
* await me.bluetooth.start()
|
|
21
|
-
* me.bluetooth.peers // Map of live BLE links
|
|
22
|
-
* me.bluetooth.on('update', () => {})
|
|
23
|
-
* ```
|
|
11
|
+
* `me.bluetooth` — the app-facing surface for nearby (Bluetooth) sync, a thin facade over
|
|
12
|
+
* ble-swarm.
|
|
24
13
|
*
|
|
25
14
|
* @extends ReadyResource
|
|
26
15
|
*/
|
|
@@ -43,14 +32,11 @@ export class Bluetooth extends ReadyResource {
|
|
|
43
32
|
this._restorePending = false
|
|
44
33
|
|
|
45
34
|
const identity = handle.identity
|
|
46
|
-
//
|
|
47
|
-
// channelless — any nearby cero device links (global nearby; strangers
|
|
48
|
-
// still sync zero bytes, replication is capability-gated)
|
|
35
|
+
// one mesh topic per channel, or a global one when channelless; strangers still sync nothing
|
|
49
36
|
this._topic = crypto.hash(b4a.from(handle.network.channel || 'cero-ble'))
|
|
50
37
|
this.swarm = new BluetoothSwarm({
|
|
51
38
|
backend,
|
|
52
|
-
//
|
|
53
|
-
// swarm, so one person reached over Wi-Fi and BLE dedupes to one peer
|
|
39
|
+
// the swarm identity, so one person over Wi-Fi and BLE dedupes to one peer
|
|
54
40
|
keyPair: { publicKey: identity.publicKey, secretKey: identity.secretKey },
|
|
55
41
|
topic: this._topic,
|
|
56
42
|
tag: 'cero-ble',
|
|
@@ -59,9 +45,7 @@ export class Bluetooth extends ReadyResource {
|
|
|
59
45
|
maxInbound
|
|
60
46
|
})
|
|
61
47
|
this.swarm.on('update', () => {
|
|
62
|
-
//
|
|
63
|
-
// drain — retuning drops links, and the pairing link still carries
|
|
64
|
-
// the joiner's initial replication
|
|
48
|
+
// retune to the mesh only once the pairing link's initial replication drained
|
|
65
49
|
if (this._restorePending && this.swarm.peers.size === 0) {
|
|
66
50
|
this._restorePending = false
|
|
67
51
|
this.swarm.setTopic(this._topic).catch(safetyCatch)
|
|
@@ -69,7 +53,6 @@ export class Bluetooth extends ReadyResource {
|
|
|
69
53
|
this.emit('update')
|
|
70
54
|
})
|
|
71
55
|
this.swarm.on('connection', (conn) => {
|
|
72
|
-
// the same treatment as any transport: wakeup, replication, pairing
|
|
73
56
|
this._handle.network.inject(conn)
|
|
74
57
|
})
|
|
75
58
|
}
|
|
@@ -88,6 +71,11 @@ export class Bluetooth extends ReadyResource {
|
|
|
88
71
|
if (this._autoStart) await this.start()
|
|
89
72
|
}
|
|
90
73
|
|
|
74
|
+
async _close() {
|
|
75
|
+
this._clearAnnounce()
|
|
76
|
+
await this.swarm.destroy()
|
|
77
|
+
}
|
|
78
|
+
|
|
91
79
|
/**
|
|
92
80
|
* Begin advertising + scanning. Idempotent; no-op when unsupported.
|
|
93
81
|
*
|
|
@@ -98,34 +86,20 @@ export class Bluetooth extends ReadyResource {
|
|
|
98
86
|
}
|
|
99
87
|
|
|
100
88
|
/**
|
|
101
|
-
* Stop advertising/scanning and drop links; open invite rendezvous end with
|
|
102
|
-
* the radio. Idempotent. Local data and the rest of the network (DHT) are
|
|
103
|
-
* untouched.
|
|
89
|
+
* Stop advertising/scanning and drop links; open invite rendezvous end with the radio.
|
|
104
90
|
*
|
|
105
91
|
* @returns {Promise<void>}
|
|
106
92
|
*/
|
|
107
93
|
async stop() {
|
|
108
94
|
this._clearAnnounce()
|
|
109
95
|
await this.swarm.stop()
|
|
110
|
-
// while stopped setTopic
|
|
111
|
-
// mesh. A start() + announce() that landed during the await wins.
|
|
96
|
+
// while stopped setTopic sticks; a start() + announce() during the await wins
|
|
112
97
|
if (!this._announce) await this.swarm.setTopic(this._topic)
|
|
113
98
|
}
|
|
114
99
|
|
|
115
100
|
/**
|
|
116
|
-
* Offline join rendezvous: retune the radio to the invite-derived topic so
|
|
117
|
-
*
|
|
118
|
-
* announcing a new invite replaces the previous rendezvous. Returns a stop
|
|
119
|
-
* function — closing the QR must stop the rendezvous so a photographed
|
|
120
|
-
* invite doesn't stay an ambient discovery beacon (admission itself is
|
|
121
|
-
* always gated by blind-pairing verifying the invite). The retune back to
|
|
122
|
-
* the mesh topic waits for live links to drain: the link a join just
|
|
123
|
-
* established survives and carries the joiner's initial replication.
|
|
124
|
-
* Auto-stops at the invite's expiry, on `stop()`, and on close.
|
|
125
|
-
*
|
|
126
|
-
* Only active while nearby sync is on: before `start()` (and after `stop()`)
|
|
127
|
-
* this is a no-op — the user controls the radio, and a join must not touch
|
|
128
|
-
* Bluetooth (OS permissions, GATT server) they never enabled.
|
|
101
|
+
* Offline join rendezvous: retune the radio to the invite-derived topic so holder and
|
|
102
|
+
* joiner find each other with zero DHT.
|
|
129
103
|
*
|
|
130
104
|
* @param {string} invite Z32 invite string.
|
|
131
105
|
* @returns {() => void}
|
|
@@ -161,20 +135,6 @@ export class Bluetooth extends ReadyResource {
|
|
|
161
135
|
}
|
|
162
136
|
}
|
|
163
137
|
|
|
164
|
-
_stopAnnounce() {
|
|
165
|
-
this._clearAnnounce()
|
|
166
|
-
if (this.swarm.peers.size > 0) this._restorePending = true
|
|
167
|
-
else this.swarm.setTopic(this._topic).catch(safetyCatch)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
_clearAnnounce() {
|
|
171
|
-
const e = this._announce
|
|
172
|
-
if (!e) return
|
|
173
|
-
if (e.timer) clearTimeout(e.timer)
|
|
174
|
-
this._announce = null
|
|
175
|
-
this._restorePending = false
|
|
176
|
-
}
|
|
177
|
-
|
|
178
138
|
/**
|
|
179
139
|
* Host-lifecycle pause (app backgrounded): radio down, user intent kept.
|
|
180
140
|
*
|
|
@@ -193,8 +153,17 @@ export class Bluetooth extends ReadyResource {
|
|
|
193
153
|
await this.swarm.resume()
|
|
194
154
|
}
|
|
195
155
|
|
|
196
|
-
|
|
156
|
+
_stopAnnounce() {
|
|
197
157
|
this._clearAnnounce()
|
|
198
|
-
|
|
158
|
+
if (this.swarm.peers.size > 0) this._restorePending = true
|
|
159
|
+
else this.swarm.setTopic(this._topic).catch(safetyCatch)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
_clearAnnounce() {
|
|
163
|
+
const e = this._announce
|
|
164
|
+
if (!e) return
|
|
165
|
+
if (e.timer) clearTimeout(e.timer)
|
|
166
|
+
this._announce = null
|
|
167
|
+
this._restorePending = false
|
|
199
168
|
}
|
|
200
169
|
}
|
package/src/lib/constants.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// Shared cero constants.
|
|
2
|
-
|
|
3
1
|
export const NS = 'cero'
|
|
4
2
|
export const COUNTERS = 'counters'
|
|
5
3
|
export const EPOCHS = 'epochs'
|
|
@@ -7,16 +5,3 @@ export const EPOCHS = 'epochs'
|
|
|
7
5
|
// Writer-admission / pairing timeout (ms), and the initial discovery-flush wait (ms).
|
|
8
6
|
export const TIMEOUT = 30000
|
|
9
7
|
export const FLUSH = 500
|
|
10
|
-
|
|
11
|
-
// schema-DSL primitive → HyperDB type (used by the builder).
|
|
12
|
-
export const DB_TYPE = {
|
|
13
|
-
string: 'string',
|
|
14
|
-
uint: 'uint',
|
|
15
|
-
int: 'int',
|
|
16
|
-
bool: 'bool',
|
|
17
|
-
bytes: 'buffer',
|
|
18
|
-
json: 'json',
|
|
19
|
-
fixed32: 'fixed32',
|
|
20
|
-
fixed64: 'fixed64',
|
|
21
|
-
file: 'string'
|
|
22
|
-
}
|
package/src/lib/operators.js
CHANGED
|
@@ -30,9 +30,7 @@ export function resolveFile(handle, id, name) {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
|
-
* Insert (or overwrite by id) a row on `ref`.
|
|
34
|
-
* `put(handle.files, { data, type, name? })` uploads the bytes to this handle's
|
|
35
|
-
* blob store, records `{ id, name }`, and resolves the file.
|
|
33
|
+
* Insert (or overwrite by id) a row on `ref`.
|
|
36
34
|
*
|
|
37
35
|
* @param {Ref} ref
|
|
38
36
|
* @param {Record<string, any>} row
|
|
@@ -55,9 +53,7 @@ async function putFile(ref, row) {
|
|
|
55
53
|
}
|
|
56
54
|
|
|
57
55
|
/**
|
|
58
|
-
* Upsert a row on `ref` — merges with the existing row and preserves
|
|
59
|
-
* `createdAt`. Pass `{ upsert: false }` to update-only: a missing row is left
|
|
60
|
-
* untouched instead of created (atomic — never resurrects a deleted row).
|
|
56
|
+
* Upsert a row on `ref` — merges with the existing row and preserves `createdAt`.
|
|
61
57
|
*
|
|
62
58
|
* @param {Ref} ref
|
|
63
59
|
* @param {Record<string, any>} row
|
|
@@ -101,13 +97,10 @@ export function call(ref, d) {
|
|
|
101
97
|
return ref.handle.store.call(ref.name, d)
|
|
102
98
|
}
|
|
103
99
|
|
|
104
|
-
// Write ops per ref kind, for `before`/`after` subscriptions.
|
|
105
100
|
const WRITES = { single: ['set'], collection: ['put', 'set', 'del'] }
|
|
106
101
|
|
|
107
102
|
/**
|
|
108
|
-
* Intercept writes to `ref` before they commit — `fn(ctx)` runs in-path
|
|
109
|
-
* (awaited). Return `false` to cancel the write, or mutate `ctx.row`.
|
|
110
|
-
* Returns an unsubscribe fn; pass `{ signal }` to unsubscribe on abort.
|
|
103
|
+
* Intercept writes to `ref` before they commit — `fn(ctx)` runs in-path (awaited).
|
|
111
104
|
*
|
|
112
105
|
* @param {Ref} ref
|
|
113
106
|
* @param {(ctx: { op: string, name: string, row: any }) => any} fn
|
|
@@ -130,9 +123,8 @@ export function before(ref, fn, opts) {
|
|
|
130
123
|
}
|
|
131
124
|
|
|
132
125
|
/**
|
|
133
|
-
* Subscribe to writes on `ref` — fires after each committed write,
|
|
134
|
-
*
|
|
135
|
-
* to unsubscribe on abort.
|
|
126
|
+
* Subscribe to writes on `ref` — fires after each committed write, non-blocking (observe
|
|
127
|
+
* only).
|
|
136
128
|
*
|
|
137
129
|
* @param {Ref} ref
|
|
138
130
|
* @param {(ctx: { op: string, name: string, row: any }) => void} fn
|
|
@@ -153,10 +145,7 @@ export function after(ref, fn, opts) {
|
|
|
153
145
|
return off
|
|
154
146
|
}
|
|
155
147
|
|
|
156
|
-
//
|
|
157
|
-
// filtered by type (handle types are stored there with their { id, key,
|
|
158
|
-
// encryptionKey, name }). Data-kind refs go straight to the store.
|
|
159
|
-
// For handle-kind refs on the facade, the parent store lives on `root`.
|
|
148
|
+
// handle refs read their rows from the parent's `handles` collection, filtered by type
|
|
160
149
|
const parentStore = (ref) => (ref.handle.root ? ref.handle.root.store : ref.handle.store)
|
|
161
150
|
|
|
162
151
|
const normalize = (rows, name) => {
|
|
@@ -195,9 +184,7 @@ function resolveRow(ref, row) {
|
|
|
195
184
|
}
|
|
196
185
|
|
|
197
186
|
/**
|
|
198
|
-
* Read from `ref`. For data refs, dispatches to the underlying store.
|
|
199
|
-
* `handle`-kind refs, lists existing child handles of that type from the
|
|
200
|
-
* parent's `handles` collection.
|
|
187
|
+
* Read from `ref`. For data refs, dispatches to the underlying store.
|
|
201
188
|
*
|
|
202
189
|
* @param {Ref} ref
|
|
203
190
|
* @param {string | Record<string, any>} [q]
|
|
@@ -212,22 +199,17 @@ export async function get(ref, q) {
|
|
|
212
199
|
return resolveResult(ref, res)
|
|
213
200
|
}
|
|
214
201
|
|
|
215
|
-
//
|
|
216
|
-
// to an optional `{ signal }` (destroyed on abort). Local refs have no
|
|
217
|
-
// close-cascade, so they keep managing their own streams.
|
|
202
|
+
// a watch stream dies with its handle, or with the signal
|
|
218
203
|
const bindStream = (owner, stream, opts) => {
|
|
219
204
|
const stopAbort = onAbort(opts?.signal, () => stream.destroy())
|
|
220
|
-
// drop the abort listener once the stream ends
|
|
221
|
-
// long-lived signal after the stream is gone
|
|
205
|
+
// drop the abort listener once the stream ends
|
|
222
206
|
if (stopAbort) stream.once('close', stopAbort)
|
|
223
207
|
return owner.own ? owner.own(stream) : stream
|
|
224
208
|
}
|
|
225
209
|
|
|
226
210
|
/**
|
|
227
|
-
* Live snapshot stream on `ref` — re-emits the latest `get()` result on
|
|
228
|
-
*
|
|
229
|
-
* handle destroys it. Pass `{ signal }` to bind it to a finer scope, or
|
|
230
|
-
* destroy the stream directly to stop watching sooner.
|
|
211
|
+
* Live snapshot stream on `ref` — re-emits the latest `get()` result on every underlying
|
|
212
|
+
* mutation.
|
|
231
213
|
*
|
|
232
214
|
* @param {Ref} ref
|
|
233
215
|
* @param {Record<string, any>} [q]
|
|
@@ -247,10 +229,9 @@ export function watch(ref, q, opts) {
|
|
|
247
229
|
}
|
|
248
230
|
|
|
249
231
|
/**
|
|
250
|
-
* Delta subscription: batches of `{ prev, next }` row pairs instead of
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
* with `reset: true`). File-typed fields resolve on both sides.
|
|
232
|
+
* Delta subscription: batches of `{ prev, next }` row pairs instead of full snapshots —
|
|
233
|
+
* lossless under backpressure, self-contained (the first batch, and any batch after a view
|
|
234
|
+
* swap, replays current.
|
|
254
235
|
*/
|
|
255
236
|
export function changes(ref, q, opts) {
|
|
256
237
|
const owner = ref.handle
|
|
@@ -297,9 +278,7 @@ export function changes(ref, q, opts) {
|
|
|
297
278
|
return bindStream(owner, out, opts)
|
|
298
279
|
}
|
|
299
280
|
|
|
300
|
-
//
|
|
301
|
-
// instead of queueing every intermediate (a busy room + un-drained reader
|
|
302
|
-
// used to buffer full result sets without bound).
|
|
281
|
+
// snapshots are idempotent: a slow consumer gets only the newest
|
|
303
282
|
function snapshotStream(src, map) {
|
|
304
283
|
let pending
|
|
305
284
|
let wanted = false
|
|
@@ -329,19 +308,8 @@ function snapshotStream(src, map) {
|
|
|
329
308
|
return out
|
|
330
309
|
}
|
|
331
310
|
|
|
332
|
-
// Universal signal instantiator. Dispatch on the second arg:
|
|
333
|
-
// string → join via invite
|
|
334
|
-
// { invite: string } → join via invite (object form)
|
|
335
|
-
// { id: string } → load an existing handle by id
|
|
336
|
-
// object | undefined → create (opts)
|
|
337
311
|
/**
|
|
338
|
-
* Open (or create / join / load) a child handle through a `handle`-kind
|
|
339
|
-
* ref. Dispatches on the normalize of `arg`:
|
|
340
|
-
*
|
|
341
|
-
* - `string` → join via an invite string
|
|
342
|
-
* - `{ invite: string }` → join via invite (object form)
|
|
343
|
-
* - `{ id: string }` → load an existing handle by id
|
|
344
|
-
* - `object | undefined` → create a new handle with the given opts
|
|
312
|
+
* Open (or create / join / load) a child handle through a `handle`-kind ref.
|
|
345
313
|
*
|
|
346
314
|
* @param {Ref} ref
|
|
347
315
|
* @param {string | { invite?: string, id?: string, name?: string, routes?: any, role?: string, accept?: boolean } | undefined} [arg]
|
|
@@ -355,13 +323,9 @@ export function open(ref, arg) {
|
|
|
355
323
|
}
|
|
356
324
|
|
|
357
325
|
/**
|
|
358
|
-
* Rotate a handle's encryption epoch. A fresh secret is sealed to every
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
* permission (admin or owner). Compose with removal:
|
|
362
|
-
*
|
|
363
|
-
* await cero.del(room.members, memberId)
|
|
364
|
-
* await cero.rotate(room)
|
|
326
|
+
* Rotate a handle's encryption epoch. A fresh secret is sealed to every current member and
|
|
327
|
+
* announced through the log — members removed before the rotation cannot decrypt anything
|
|
328
|
+
* written after it.
|
|
365
329
|
*
|
|
366
330
|
* @param {any} handle
|
|
367
331
|
* @returns {Promise<{ epoch: number }>}
|
|
@@ -370,16 +334,8 @@ export function rotate(handle) {
|
|
|
370
334
|
return handle.store.rotate()
|
|
371
335
|
}
|
|
372
336
|
|
|
373
|
-
// ─── custom operators ──────────────────────────────────────────────────────
|
|
374
|
-
// App business logic lives as custom operators: pure functions whose first arg
|
|
375
|
-
// is the handle they act on, composed from the operators above. `define`
|
|
376
|
-
// registers them by scope; `bind` puts them on a handle — either an explicit
|
|
377
|
-
// `{ ns: module }` map, or (given a scope) the registered operators for that
|
|
378
|
-
// scope, which is how cero auto-binds the root and each child as it opens.
|
|
379
|
-
|
|
380
337
|
const registry = {}
|
|
381
338
|
|
|
382
|
-
// Curry `handle` as arg 0 of every function in `fns`, under `handle[ns]`.
|
|
383
339
|
function attach(handle, ns, fns) {
|
|
384
340
|
const bound = {}
|
|
385
341
|
for (const key of Object.keys(fns)) {
|
|
@@ -390,12 +346,7 @@ function attach(handle, ns, fns) {
|
|
|
390
346
|
|
|
391
347
|
/**
|
|
392
348
|
* Put custom operators on `handle`, currying it as their first argument so
|
|
393
|
-
* `handle.ns.fn(args)` calls `fn(handle, args)`.
|
|
394
|
-
* - a `{ ns: module }` map → bind exactly those, or
|
|
395
|
-
* - `null` → the registered root operators, or
|
|
396
|
-
* - a child-handle type → the registered operators for that type.
|
|
397
|
-
* The scope forms are how cero binds handles automatically; pass a map yourself
|
|
398
|
-
* for manual binding.
|
|
349
|
+
* `handle.ns.fn(args)` calls `fn(handle, args)`.
|
|
399
350
|
*
|
|
400
351
|
* @param {any} handle
|
|
401
352
|
* @param {Record<string, any> | string | null} arg
|
|
@@ -418,9 +369,8 @@ export function bind(handle, arg) {
|
|
|
418
369
|
}
|
|
419
370
|
|
|
420
371
|
/**
|
|
421
|
-
* Register custom operators by scope. A bare key binds on the root handle; a key
|
|
422
|
-
*
|
|
423
|
-
* at startup, before `cero()` / `connect()`, in both processes.
|
|
372
|
+
* Register custom operators by scope. A bare key binds on the root handle; a key that
|
|
373
|
+
* names a child-handle type binds on every handle of that type.
|
|
424
374
|
*
|
|
425
375
|
* @param {Record<string, any>} map
|
|
426
376
|
*/
|
package/src/lib/peek.js
CHANGED
|
@@ -6,14 +6,11 @@ import { CeroError } from '@cero-base/core/errors'
|
|
|
6
6
|
|
|
7
7
|
import { Local } from '../local/index.js'
|
|
8
8
|
|
|
9
|
-
//
|
|
10
|
-
// module graph — bundlers follow even dynamic imports, and a browser build
|
|
11
|
-
// must never reach hypercore/sodium.
|
|
9
|
+
// outside operators.js so storage deps stay off the client's module graph
|
|
12
10
|
|
|
13
11
|
/**
|
|
14
|
-
* Quickly check whether the on-disk directory at `dir` already holds an
|
|
15
|
-
*
|
|
16
|
-
* read-only and closes everything before returning.
|
|
12
|
+
* Quickly check whether the on-disk directory at `dir` already holds an initialised cero
|
|
13
|
+
* identity (i.e. a stored master seed).
|
|
17
14
|
*
|
|
18
15
|
* @param {string} dir Cero data directory.
|
|
19
16
|
* @param {any} spec Built spec — same value passed to `cero(dir, spec)`.
|
|
@@ -23,8 +20,7 @@ export async function peek(dir, spec) {
|
|
|
23
20
|
if (typeof dir !== 'string' || !dir) throw CeroError.INVALID('dir must be a non-empty string')
|
|
24
21
|
if (!spec) throw CeroError.REQUIRED('spec')
|
|
25
22
|
|
|
26
|
-
//
|
|
27
|
-
// any ready() must still close root + store + local, not leak the storage lock.
|
|
23
|
+
// construct first, ready inside the try: a corrupt dir must still close everything
|
|
28
24
|
const root = new HypercoreStorage(`${dir}/main`)
|
|
29
25
|
const store = new Corestore(root, { manifestVersion: 2 })
|
|
30
26
|
const local = new Local(null, spec, { store })
|
package/src/lib/refs.js
CHANGED
|
@@ -9,9 +9,7 @@ import { CeroError } from '@cero-base/core/errors'
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* Typed pointer to a single ref (table or handle slot) on a `Handle` or
|
|
13
|
-
* `Local`. Operators (`put`/`get`/`open`/...) take a `Ref` as their first
|
|
14
|
-
* argument and dispatch through the owning handle's store.
|
|
12
|
+
* Typed pointer to a single ref (table or handle slot) on a `Handle` or `Local`.
|
|
15
13
|
*/
|
|
16
14
|
export class Ref {
|
|
17
15
|
/**
|
|
@@ -36,8 +34,7 @@ export class Ref {
|
|
|
36
34
|
*/
|
|
37
35
|
static attach(target, refs) {
|
|
38
36
|
for (const [name, info] of Object.entries(refs || {})) {
|
|
39
|
-
//
|
|
40
|
-
// on, store) when a schema declares a ref named like a reserved member
|
|
37
|
+
// a ref named like a reserved member must fail loud, not overwrite it
|
|
41
38
|
if (name in target) {
|
|
42
39
|
throw CeroError.INVALID(
|
|
43
40
|
`schema ref '${name}' collides with a reserved ${target.constructor?.name || 'handle'} member — rename it`
|
package/src/lib/spec.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Re-exports of the schema DSL (`t`) and `schema()` wrapper from
|
|
3
|
-
*
|
|
4
|
-
* pulling in the core package directly.
|
|
2
|
+
* Re-exports of the schema DSL (`t`) and `schema()` wrapper from `@cero-base/core/schema`,
|
|
3
|
+
* so cero apps can describe their tables without pulling in the core package directly.
|
|
5
4
|
*/
|
|
6
5
|
export { t, schema } from '@cero-base/core/schema'
|
package/src/local/index.js
CHANGED
|
@@ -13,9 +13,8 @@ import { Ref } from '../lib/refs.js'
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* Per-device, single-writer storage for cero — holds the master seed,
|
|
17
|
-
*
|
|
18
|
-
* `Storage` and exposes each local ref as a property of the instance.
|
|
16
|
+
* Per-device, single-writer storage for cero — holds the master seed, device keypair and
|
|
17
|
+
* any per-handle keypairs.
|
|
19
18
|
*/
|
|
20
19
|
export class Local extends ReadyResource {
|
|
21
20
|
/**
|