@cero-base/cero 2.0.0 → 2.1.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/package.json +2 -2
- package/src/build/index.js +45 -14
- package/src/build/internal.js +4 -2
- package/src/build/schemas.js +4 -3
- package/src/extensions/handle-sync.js +0 -1
- package/src/extensions/index.js +82 -2
- package/src/extensions/profile-sync.js +15 -11
- package/src/handle/index.js +52 -21
- package/src/index.js +12 -38
- package/src/lib/operators.js +19 -68
- package/src/lib/refs.js +8 -3
- package/src/rpc/client.js +34 -43
- package/src/rpc/server.js +17 -8
- package/types/build/index.d.ts +11 -1
- package/types/build/schemas.d.ts +4 -3
- package/types/extensions/handle-sync.d.ts +0 -1
- package/types/extensions/index.d.ts +83 -3
- package/types/extensions/profile-sync.d.ts +0 -1
- package/types/handle/index.d.ts +11 -11
- package/types/index.d.ts +19 -11
- package/types/lib/operators.d.ts +18 -43
- package/types/lib/refs.d.ts +4 -2
- package/types/rpc/client.d.ts +19 -8
- package/types/rpc/server.d.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cero-base/cero",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "The ideal p2p API — everything is a handle, handles contain refs, refs contain rows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
"test:node": "find test -name '*.test.js' | sort | xargs -P1 -n1 brittle-node"
|
|
99
99
|
},
|
|
100
100
|
"dependencies": {
|
|
101
|
-
"@cero-base/core": "^2.
|
|
101
|
+
"@cero-base/core": "^2.1.0",
|
|
102
102
|
"b4a": "^1.8.1",
|
|
103
103
|
"bare-abort-controller": "^1.1.2",
|
|
104
104
|
"bare-crypto": "^1.15.3",
|
package/src/build/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { join } from 'path'
|
|
1
|
+
import { join, resolve } from 'path'
|
|
2
2
|
import { promises as fs } from 'fs'
|
|
3
|
+
import { pathToFileURL } from 'url'
|
|
3
4
|
|
|
4
5
|
import Hyperschema from 'hyperschema'
|
|
5
6
|
import HyperdbBuilder from 'hyperdb/builder'
|
|
@@ -9,7 +10,7 @@ import HRPCBuilder from 'hrpc'
|
|
|
9
10
|
import { CeroError } from '@cero-base/core/errors'
|
|
10
11
|
|
|
11
12
|
import { NS } from '../lib/constants.js'
|
|
12
|
-
import {
|
|
13
|
+
import { extensionsOf } from '../extensions/index.js'
|
|
13
14
|
import * as internal from './internal.js'
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -19,6 +20,8 @@ import * as internal from './internal.js'
|
|
|
19
20
|
*
|
|
20
21
|
* @typedef {object} BuildOpts
|
|
21
22
|
* @property {string} [ns] Namespace prefix for emitted schema ids. Defaults to `'cero'`.
|
|
23
|
+
* @property {string | import('../extensions/index.js').Extension[]} [extensions] The extensions to fold in. A module specifier, relative to `specDir`, is imported here for its `extensions` export and written into the spec, so every process runs the same list. A list is folded in only. The bundled two by default, `[]` for none.
|
|
24
|
+
* @property {string} [operators] A module specifier, relative to `specDir`, written into the spec for its `operators` export, so every process binds the same map.
|
|
22
25
|
*/
|
|
23
26
|
|
|
24
27
|
/**
|
|
@@ -29,30 +32,34 @@ import * as internal from './internal.js'
|
|
|
29
32
|
* @param {BuildOpts} [opts]
|
|
30
33
|
* @returns {Promise<void>}
|
|
31
34
|
*/
|
|
32
|
-
export async function build(specDir, schema, { ns = NS, extensions
|
|
35
|
+
export async function build(specDir, schema, { ns = NS, extensions, operators } = {}) {
|
|
33
36
|
const raw = /** @type {SchemaDefs & { local?: SchemaDefs }} */ (schema?.defs || schema)
|
|
34
37
|
if (!raw || typeof raw !== 'object') throw CeroError.REQUIRED('schema')
|
|
38
|
+
const from = { extensions: str(extensions), operators: str(operators) }
|
|
39
|
+
const exts = extensionsOf(
|
|
40
|
+
null,
|
|
41
|
+
from.extensions ? await load(from.extensions, specDir) : extensions
|
|
42
|
+
)
|
|
35
43
|
|
|
36
|
-
// t.extend entries by internal type: app schema first, then each extension
|
|
44
|
+
// t.extend entries by internal type: the app schema first, then each extension at its scope
|
|
37
45
|
const extend = {}
|
|
38
46
|
const defs = {}
|
|
39
|
-
const
|
|
47
|
+
const fold = (entries, target, fromExt) => {
|
|
40
48
|
for (const [k, v] of Object.entries(entries)) {
|
|
41
49
|
if (v && v.kind === 'extend') {
|
|
42
50
|
const type = internal.defs.main[k]?.type
|
|
43
51
|
if (!type) throw CeroError.INVALID(`'${k}' is not an extendable builtin`)
|
|
44
52
|
// app schema wins on field conflicts, extensions only add new fields
|
|
45
53
|
extend[type] = fromExt ? { ...v.fields, ...extend[type] } : { ...extend[type], ...v.fields }
|
|
46
|
-
} else if (
|
|
47
|
-
|
|
54
|
+
} else if (fromExt && isPlainHandle(v)) {
|
|
55
|
+
fold(v, (target[k] = { ...target[k] }), true) // into the handle type, the app's refs kept
|
|
56
|
+
} else if (!fromExt || !(k in target)) {
|
|
57
|
+
target[k] = v // app schema wins over an extension's default
|
|
48
58
|
}
|
|
49
59
|
}
|
|
50
60
|
}
|
|
51
|
-
|
|
52
|
-
for (const ext of
|
|
53
|
-
if (ext.bundled && extensions === false) continue
|
|
54
|
-
if (ext.schema) collect(ext.schema, true)
|
|
55
|
-
}
|
|
61
|
+
fold(raw, defs, false)
|
|
62
|
+
for (const ext of exts) if (ext.schema) fold(ext.schema, defs, true)
|
|
56
63
|
|
|
57
64
|
const main = compile(splitMain(defs), ns, 'main')
|
|
58
65
|
const local = compile(defs.local || {}, ns, 'local')
|
|
@@ -81,7 +88,24 @@ export async function build(specDir, schema, { ns = NS, extensions = true } = {}
|
|
|
81
88
|
if (meta.refs[name]) meta.refs[name] = { kind: 'handle', type: name, schema: `@${ns}/handle` }
|
|
82
89
|
}
|
|
83
90
|
|
|
84
|
-
await fs.writeFile(
|
|
91
|
+
await fs.writeFile(
|
|
92
|
+
join(specDir, 'index.js'),
|
|
93
|
+
wireModule(meta, Object.keys(handles), from),
|
|
94
|
+
'utf-8'
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const str = (v) => (typeof v === 'string' ? v : null)
|
|
99
|
+
|
|
100
|
+
async function load(specifier, specDir) {
|
|
101
|
+
const url = specifier.startsWith('.')
|
|
102
|
+
? pathToFileURL(resolve(specDir, specifier)).href
|
|
103
|
+
: specifier
|
|
104
|
+
const { extensions } = await import(url)
|
|
105
|
+
if (!Array.isArray(extensions)) {
|
|
106
|
+
throw CeroError.INVALID(`'${specifier}' must export an extensions array`)
|
|
107
|
+
}
|
|
108
|
+
return extensions
|
|
85
109
|
}
|
|
86
110
|
|
|
87
111
|
function splitMain(defs) {
|
|
@@ -295,7 +319,10 @@ function handleEntries(names, kinds) {
|
|
|
295
319
|
.join(',\n')
|
|
296
320
|
}
|
|
297
321
|
|
|
298
|
-
|
|
322
|
+
// the spec imports what the build named, so every process finds the same lists
|
|
323
|
+
const named = (what, from) => (from ? `import { ${what} } from '${from}'` : `const ${what} = null`)
|
|
324
|
+
|
|
325
|
+
function wireModule(meta, names, from) {
|
|
299
326
|
const kinds = ['database', 'dispatch', 'schema']
|
|
300
327
|
return `// autogenerated by cero/build
|
|
301
328
|
import database from './main/db/index.js'
|
|
@@ -305,6 +332,8 @@ import rpc from './main/rpc/index.js'
|
|
|
305
332
|
import localDatabase from './local/db/index.js'
|
|
306
333
|
import * as localSchema from './local/schema/index.js'
|
|
307
334
|
${handleImports(names, kinds)}
|
|
335
|
+
${named('extensions', from.extensions)}
|
|
336
|
+
${named('operators', from.operators)}
|
|
308
337
|
|
|
309
338
|
export const meta = ${JSON.stringify(meta, null, 2)}
|
|
310
339
|
|
|
@@ -315,6 +344,8 @@ export const spec = {
|
|
|
315
344
|
rpc,
|
|
316
345
|
local: { database: localDatabase, schema: localSchema, meta: meta.local },
|
|
317
346
|
meta,
|
|
347
|
+
extensions,
|
|
348
|
+
operators,
|
|
318
349
|
handles: {
|
|
319
350
|
${handleEntries(names, kinds)}
|
|
320
351
|
}
|
package/src/build/internal.js
CHANGED
|
@@ -99,7 +99,6 @@ const COMMANDS = [
|
|
|
99
99
|
['get', 'req-query', 'res-rows'],
|
|
100
100
|
['get-one', 'req-id', 'res-data'],
|
|
101
101
|
['del', 'req-id', 'res-ok'],
|
|
102
|
-
['count', 'req-query', 'res-count'],
|
|
103
102
|
['watch', 'req-query', 'res-rows', true],
|
|
104
103
|
['call', 'req-call', 'res-data'],
|
|
105
104
|
['invite', 'req-invite', 'res-invite'],
|
|
@@ -109,7 +108,10 @@ const COMMANDS = [
|
|
|
109
108
|
['close-handle', 'req-handle', 'res-ok'],
|
|
110
109
|
['leave', 'req-handle', 'res-ok'],
|
|
111
110
|
['changes', 'req-query', 'res-changes', true],
|
|
112
|
-
['rotate', 'req-handle', 'res-epoch']
|
|
111
|
+
['rotate', 'req-handle', 'res-epoch'],
|
|
112
|
+
['set-active', 'req-set-active', 'res-ok'],
|
|
113
|
+
['suspend', 'req-empty', 'res-ok'],
|
|
114
|
+
['resume', 'req-empty', 'res-ok']
|
|
113
115
|
]
|
|
114
116
|
|
|
115
117
|
export function commands(ns) {
|
package/src/build/schemas.js
CHANGED
|
@@ -159,6 +159,10 @@ export const rpc = {
|
|
|
159
159
|
'req-handle': {
|
|
160
160
|
handle: required(string)
|
|
161
161
|
},
|
|
162
|
+
'req-set-active': {
|
|
163
|
+
handle: required(string),
|
|
164
|
+
active: bool
|
|
165
|
+
},
|
|
162
166
|
'res-data': {
|
|
163
167
|
data: bytes
|
|
164
168
|
},
|
|
@@ -171,9 +175,6 @@ export const rpc = {
|
|
|
171
175
|
changes: required(bytes),
|
|
172
176
|
reset: bool
|
|
173
177
|
},
|
|
174
|
-
'res-count': {
|
|
175
|
-
count: required(int)
|
|
176
|
-
},
|
|
177
178
|
'res-invite': {
|
|
178
179
|
invite: required(string)
|
|
179
180
|
},
|
|
@@ -10,7 +10,6 @@ import { get, set, watch } from '../lib/operators.js'
|
|
|
10
10
|
export function handleSync({ fields = { avatar: t.string } } = {}) {
|
|
11
11
|
return {
|
|
12
12
|
name: 'handle-sync',
|
|
13
|
-
bundled: true,
|
|
14
13
|
schema: { handles: t.extend(fields) },
|
|
15
14
|
setup(me) {
|
|
16
15
|
const keys = ['name', ...Object.keys(fields)]
|
package/src/extensions/index.js
CHANGED
|
@@ -1,8 +1,88 @@
|
|
|
1
1
|
import { profileSync } from './profile-sync.js'
|
|
2
2
|
import { handleSync } from './handle-sync.js'
|
|
3
|
+
import { t, schema } from '../lib/spec.js'
|
|
4
|
+
import * as operators from '../lib/operators.js'
|
|
3
5
|
|
|
4
6
|
export * from './profile-sync.js'
|
|
5
7
|
export * from './handle-sync.js'
|
|
6
8
|
|
|
7
|
-
//
|
|
8
|
-
export
|
|
9
|
+
// everything an extension module needs, light enough for the UI bundle the spec pulls it into
|
|
10
|
+
export { t, schema }
|
|
11
|
+
export const { put, set, get, del, watch, changes, call, open, rotate, before, after } = operators
|
|
12
|
+
export const cero = {
|
|
13
|
+
t,
|
|
14
|
+
schema,
|
|
15
|
+
put,
|
|
16
|
+
set,
|
|
17
|
+
get,
|
|
18
|
+
del,
|
|
19
|
+
watch,
|
|
20
|
+
changes,
|
|
21
|
+
call,
|
|
22
|
+
open,
|
|
23
|
+
rotate,
|
|
24
|
+
before,
|
|
25
|
+
after
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {object} Extension
|
|
30
|
+
* @property {Record<string, any>} [schema] Refs to add, or `t.extend` on a builtin, nested by handle type like the app schema.
|
|
31
|
+
* @property {(me: any) => any} [setup] Runs once the root is ready; a returned function runs on close.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/** The two every app gets unless its build names a list. */
|
|
35
|
+
export const bundled = [profileSync(), handleSync()]
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The extensions a spec carries, else the bundled two. A bare function is `{ setup }`.
|
|
39
|
+
*
|
|
40
|
+
* @param {any} spec
|
|
41
|
+
* @param {Array<any>} [override]
|
|
42
|
+
* @returns {Extension[]}
|
|
43
|
+
*/
|
|
44
|
+
export function extensionsOf(spec, override) {
|
|
45
|
+
const list = override || spec?.extensions || bundled
|
|
46
|
+
return list.map((e) => (typeof e === 'function' ? { setup: e } : e))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The operators a spec carries: functions taking the handle first, keyed by namespace, a
|
|
51
|
+
* key naming a handle type holding that type's namespaces.
|
|
52
|
+
*
|
|
53
|
+
* @param {any} spec
|
|
54
|
+
* @param {Record<string, any>} [override]
|
|
55
|
+
* @returns {Record<string, any>}
|
|
56
|
+
*/
|
|
57
|
+
export function operatorsOf(spec, override) {
|
|
58
|
+
return override || spec?.operators || {}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// handle.ns.fn(args) calls fn(handle, args)
|
|
62
|
+
function attach(handle, ns, fns) {
|
|
63
|
+
const bound = {}
|
|
64
|
+
for (const key of Object.keys(fns)) {
|
|
65
|
+
if (typeof fns[key] === 'function') bound[key] = (...args) => fns[key](handle, ...args)
|
|
66
|
+
}
|
|
67
|
+
handle[ns] = bound
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Put the operators for `handle` on it: the root when `type` is null, else a child of `type`.
|
|
72
|
+
*
|
|
73
|
+
* @param {any} handle
|
|
74
|
+
* @param {string | null} type
|
|
75
|
+
* @param {Record<string, any>} operators
|
|
76
|
+
* @returns {any} handle
|
|
77
|
+
*/
|
|
78
|
+
export function bind(handle, type, operators) {
|
|
79
|
+
const handles = handle.spec?.meta?.handles || {}
|
|
80
|
+
for (const [ns, fns] of Object.entries(operators)) {
|
|
81
|
+
if (type === null) {
|
|
82
|
+
if (!(ns in handles)) attach(handle, ns, fns)
|
|
83
|
+
} else if (ns === type) {
|
|
84
|
+
for (const [k, group] of Object.entries(fns)) attach(handle, k, group)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return handle
|
|
88
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t } from '../lib/spec.js'
|
|
2
|
-
import { get, set,
|
|
2
|
+
import { get, set, changes } from '../lib/operators.js'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Mirror your `profile` onto your `member` row in every handle you're in.
|
|
@@ -9,31 +9,35 @@ import { get, set, after } from '../lib/operators.js'
|
|
|
9
9
|
export function profileSync({ fields = { avatar: t.string } } = {}) {
|
|
10
10
|
return {
|
|
11
11
|
name: 'profile-sync',
|
|
12
|
-
bundled: true,
|
|
13
12
|
schema: {
|
|
14
13
|
profile: t.single({ name: t.string, ...fields }),
|
|
15
14
|
members: t.extend(fields)
|
|
16
15
|
},
|
|
17
16
|
setup(me) {
|
|
18
17
|
const keys = ['name', ...Object.keys(fields)]
|
|
19
|
-
const publish = (child, profile) =>
|
|
20
|
-
set(child.members, { id: me.identity.id, ...profile }, { upsert: false }).catch(me._onerror)
|
|
21
18
|
|
|
22
19
|
// an unconditional set on every open is a room-wide op forever
|
|
23
|
-
const
|
|
24
|
-
const { data: profile } = await get(me.profile)
|
|
25
|
-
if (!profile) return
|
|
20
|
+
const publish = async (child, profile) => {
|
|
26
21
|
const { data: member } = await get(child.members, me.identity.id)
|
|
27
22
|
if (member && keys.every((k) => member[k] === profile[k])) return
|
|
28
|
-
|
|
23
|
+
await set(child.members, { id: me.identity.id, ...profile }, { upsert: false })
|
|
29
24
|
}
|
|
30
25
|
|
|
31
|
-
const
|
|
32
|
-
|
|
26
|
+
const onHandle = async (child) => {
|
|
27
|
+
const { data: profile } = await get(me.profile)
|
|
28
|
+
if (profile) await publish(child, profile)
|
|
33
29
|
}
|
|
34
30
|
|
|
35
31
|
me.on('handle', (child) => onHandle(child).catch(me._onerror), { signal: me.signal })
|
|
36
|
-
|
|
32
|
+
|
|
33
|
+
// a local edit and one replicated from another device both land here
|
|
34
|
+
const stream = changes(me.profile, {}, { signal: me.signal })
|
|
35
|
+
stream.on('error', me._onerror)
|
|
36
|
+
stream.on('data', ({ changes: batch }) => {
|
|
37
|
+
for (const { next } of batch) {
|
|
38
|
+
if (next) me.children.forEach((child) => publish(child, next).catch(me._onerror))
|
|
39
|
+
}
|
|
40
|
+
})
|
|
37
41
|
}
|
|
38
42
|
}
|
|
39
43
|
}
|
package/src/handle/index.js
CHANGED
|
@@ -22,7 +22,8 @@ import { FileServer } from '@cero-base/core/blobs/server'
|
|
|
22
22
|
import { NS, TIMEOUT } from '../lib/constants.js'
|
|
23
23
|
|
|
24
24
|
import { Ref } from '../lib/refs.js'
|
|
25
|
-
import { bind } from '../
|
|
25
|
+
import { extensionsOf, operatorsOf, bind } from '../extensions/index.js'
|
|
26
|
+
import { before, after } from '../lib/operators.js'
|
|
26
27
|
|
|
27
28
|
export { Ref } from '../lib/refs.js'
|
|
28
29
|
|
|
@@ -48,7 +49,6 @@ export { Ref } from '../lib/refs.js'
|
|
|
48
49
|
* @property {Array<{ epoch: number, entropy: Uint8Array }>} [epochs] Rotation epochs delivered at join.
|
|
49
50
|
* @property {string} [namespace] Corestore namespace.
|
|
50
51
|
* @property {KeyPair} [keyPair] Writer keypair.
|
|
51
|
-
* @property {boolean} [passive] Join discovery server-only; flip later with `setActive`.
|
|
52
52
|
* @property {boolean} [pair] When `false`, skips creating a `Pairing` session.
|
|
53
53
|
*
|
|
54
54
|
* @typedef {object} CreateChildOpts
|
|
@@ -114,8 +114,11 @@ export class Handle extends ReadyResource {
|
|
|
114
114
|
this._discovery = opts.discovery || null
|
|
115
115
|
this._dir = opts.dir || null
|
|
116
116
|
this._opts = opts.opts || {}
|
|
117
|
+
this.extensions = parent?.extensions || extensionsOf(spec, this._opts.extensions)
|
|
118
|
+
this.operators = parent?.operators || operatorsOf(spec, this._opts.operators)
|
|
117
119
|
this._onerror = this._opts.onerror || safetyCatch
|
|
118
120
|
this.children = parent ? null : new Set()
|
|
121
|
+
this._typeHooks = parent ? null : new Set()
|
|
119
122
|
this._loading = parent ? null : new Map()
|
|
120
123
|
this._joining = parent ? null : new Map()
|
|
121
124
|
this._coreKeys = parent ? null : new Map()
|
|
@@ -139,7 +142,7 @@ export class Handle extends ReadyResource {
|
|
|
139
142
|
epochs: opts.epochs,
|
|
140
143
|
namespace: opts.namespace,
|
|
141
144
|
keyPair: opts.keyPair,
|
|
142
|
-
|
|
145
|
+
pinned: !parent,
|
|
143
146
|
onerror: this._onerror
|
|
144
147
|
})
|
|
145
148
|
this.pair = null
|
|
@@ -201,6 +204,11 @@ export class Handle extends ReadyResource {
|
|
|
201
204
|
return blobs
|
|
202
205
|
}
|
|
203
206
|
|
|
207
|
+
/** The handle type of a child, null on the root. */
|
|
208
|
+
get type() {
|
|
209
|
+
return this.spec.meta.type || null
|
|
210
|
+
}
|
|
211
|
+
|
|
204
212
|
/** Canonical id — identity id for the root handle, store key for children. */
|
|
205
213
|
get id() {
|
|
206
214
|
if (!this.parent) return this.identity.id
|
|
@@ -233,10 +241,12 @@ export class Handle extends ReadyResource {
|
|
|
233
241
|
await this.pair.ready()
|
|
234
242
|
// serve invites persisted by any member, in step with the rows
|
|
235
243
|
await this._syncInvites().catch(safetyCatch)
|
|
236
|
-
this._invitesSync = () =>
|
|
244
|
+
this._invitesSync = (touched) => {
|
|
245
|
+
if (touched.has('*') || touched.has('invites')) this._syncInvites().catch(safetyCatch)
|
|
246
|
+
}
|
|
237
247
|
this.store.on('update', this._invitesSync)
|
|
238
248
|
}
|
|
239
|
-
Ref.attach(this, this.store.refs)
|
|
249
|
+
Ref.attach(this, this.store.refs, this.spec.handles)
|
|
240
250
|
this.root._coreKeys.set(b4a.toString(this.store.key, 'hex'), this.store.encryptionKey)
|
|
241
251
|
if (!this.parent) await this.fileServer.listen()
|
|
242
252
|
}
|
|
@@ -349,15 +359,13 @@ export class Handle extends ReadyResource {
|
|
|
349
359
|
}
|
|
350
360
|
|
|
351
361
|
/**
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
* back on focus.
|
|
362
|
+
* `true` ranks this handle as just touched, `false` takes it out of the swarm until the
|
|
363
|
+
* next update lands in it.
|
|
355
364
|
*
|
|
356
365
|
* @param {boolean} active
|
|
357
|
-
* @returns {Promise<void>}
|
|
358
366
|
*/
|
|
359
367
|
setActive(active) {
|
|
360
|
-
|
|
368
|
+
this.store.setActive(active)
|
|
361
369
|
}
|
|
362
370
|
|
|
363
371
|
/**
|
|
@@ -467,16 +475,16 @@ export class Handle extends ReadyResource {
|
|
|
467
475
|
}
|
|
468
476
|
|
|
469
477
|
const sig = this.identity.sign(admission(this.store.key, writerKey, this.store.writerKey))
|
|
478
|
+
// one batch: the member row, then the writer that belongs to it; a refusal discards both
|
|
470
479
|
await this.store.tx(async (tx) => {
|
|
480
|
+
await tx.call('add-member', member)
|
|
471
481
|
await tx.call('add-writer', {
|
|
472
482
|
sig,
|
|
473
483
|
master: this.identity.publicKey,
|
|
474
484
|
writer: writerKey,
|
|
475
|
-
// add-member in the same transaction decides the rank, a refusal discards both
|
|
476
485
|
memberId: member.id,
|
|
477
486
|
ts: member.updatedAt || Date.now()
|
|
478
487
|
})
|
|
479
|
-
await tx.call('add-member', member)
|
|
480
488
|
})
|
|
481
489
|
}
|
|
482
490
|
|
|
@@ -675,9 +683,7 @@ export class Handle extends ReadyResource {
|
|
|
675
683
|
})
|
|
676
684
|
|
|
677
685
|
if (accept !== false) this._wireAccept(child, { role })
|
|
678
|
-
|
|
679
|
-
this.children.add(child)
|
|
680
|
-
this.emit('handle', child, { name, role })
|
|
686
|
+
this._adopt(child, { name, role })
|
|
681
687
|
publish(child)
|
|
682
688
|
this._loading?.delete(id)
|
|
683
689
|
return child
|
|
@@ -781,9 +787,7 @@ export class Handle extends ReadyResource {
|
|
|
781
787
|
updatedAt: ts
|
|
782
788
|
})
|
|
783
789
|
this._wireAccept(child)
|
|
784
|
-
|
|
785
|
-
this.children.add(child)
|
|
786
|
-
this.emit('handle', child, {})
|
|
790
|
+
this._adopt(child, {})
|
|
787
791
|
publish(child)
|
|
788
792
|
this._loading?.delete(id)
|
|
789
793
|
return child
|
|
@@ -852,9 +856,7 @@ export class Handle extends ReadyResource {
|
|
|
852
856
|
}
|
|
853
857
|
// `accept: false` is a host-approval gate, re-arming it silently is worse
|
|
854
858
|
if (opts?.accept !== false) this._wireAccept(child, { role: opts?.role })
|
|
855
|
-
|
|
856
|
-
this.children.add(child)
|
|
857
|
-
this.emit('handle', child, {})
|
|
859
|
+
this._adopt(child, {})
|
|
858
860
|
return child
|
|
859
861
|
}
|
|
860
862
|
|
|
@@ -882,6 +884,35 @@ export class Handle extends ReadyResource {
|
|
|
882
884
|
await Promise.all([...this.children].map((child) => child.pair?.resume()))
|
|
883
885
|
}
|
|
884
886
|
|
|
887
|
+
// a child is a child once it has its operators, the hooks declared for its type, and a slot
|
|
888
|
+
_adopt(child, info) {
|
|
889
|
+
bind(child, child.type, this.operators)
|
|
890
|
+
for (const hook of this._typeHooks) hook.apply(child)
|
|
891
|
+
this.children.add(child)
|
|
892
|
+
this.emit('handle', child, info)
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// before(me.room.notes, fn): on every room open now and every one opened later
|
|
896
|
+
_hookType(op, ref, fn, opts) {
|
|
897
|
+
const offs = new Map()
|
|
898
|
+
const hook = {
|
|
899
|
+
apply: (child) => {
|
|
900
|
+
if (child.type !== ref.type) return
|
|
901
|
+
offs.set(child, op(child[ref.name], fn))
|
|
902
|
+
child.once('close', () => offs.delete(child))
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
this._typeHooks.add(hook)
|
|
906
|
+
for (const child of this.children) hook.apply(child)
|
|
907
|
+
const off = () => {
|
|
908
|
+
this._typeHooks.delete(hook)
|
|
909
|
+
for (const o of offs.values()) o()
|
|
910
|
+
offs.clear()
|
|
911
|
+
}
|
|
912
|
+
onAbort(opts?.signal, off)
|
|
913
|
+
return off
|
|
914
|
+
}
|
|
915
|
+
|
|
885
916
|
/**
|
|
886
917
|
* @param {Handle} child
|
|
887
918
|
* @param {{ role?: string }} [opts]
|
package/src/index.js
CHANGED
|
@@ -17,21 +17,18 @@ import {
|
|
|
17
17
|
set,
|
|
18
18
|
get,
|
|
19
19
|
del,
|
|
20
|
-
count,
|
|
21
20
|
watch,
|
|
22
21
|
changes,
|
|
23
22
|
call,
|
|
24
23
|
open,
|
|
25
24
|
rotate,
|
|
26
25
|
before,
|
|
27
|
-
after
|
|
28
|
-
bind,
|
|
29
|
-
define
|
|
26
|
+
after
|
|
30
27
|
} from './lib/operators.js'
|
|
31
28
|
import { peek } from './lib/peek.js'
|
|
32
29
|
import { t, schema } from './lib/spec.js'
|
|
33
30
|
import { FLUSH, TIMEOUT } from './lib/constants.js'
|
|
34
|
-
import {
|
|
31
|
+
import { bind } from './extensions/index.js'
|
|
35
32
|
|
|
36
33
|
export { Handle, Ref, Local }
|
|
37
34
|
export {
|
|
@@ -39,16 +36,13 @@ export {
|
|
|
39
36
|
set,
|
|
40
37
|
get,
|
|
41
38
|
del,
|
|
42
|
-
count,
|
|
43
39
|
watch,
|
|
44
40
|
changes,
|
|
45
41
|
call,
|
|
46
42
|
open,
|
|
47
43
|
rotate,
|
|
48
44
|
before,
|
|
49
|
-
after
|
|
50
|
-
bind,
|
|
51
|
-
define
|
|
45
|
+
after
|
|
52
46
|
} from './lib/operators.js'
|
|
53
47
|
export { peek } from './lib/peek.js'
|
|
54
48
|
export { t, schema } from './lib/spec.js'
|
|
@@ -69,13 +63,15 @@ export { t, schema } from './lib/spec.js'
|
|
|
69
63
|
* @property {number[]} [backoffs] Swarm reconnect backoff tiers in ms (testing/tuning).
|
|
70
64
|
* @property {string} [channel] Optional network-isolation label; only same-channel peers connect.
|
|
71
65
|
* @property {Array<string | Uint8Array>} [mirrors] Blind-peer public keys. Rooms and files are mirrored through them so peers sync even when never online at the same time. Mirrors hold only encrypted blocks — they never read your data.
|
|
66
|
+
* @property {{ active?: number, announced?: number, idle?: number }} [presence] How many rooms search, how many only announce, and the idle ms before the rest leave the swarm.
|
|
72
67
|
* @property {Uint8Array} [key] Existing database key to recover into, skipping the pointer lookup.
|
|
73
68
|
* @property {Uint8Array} [encryptionKey] Pre-existing encryption key.
|
|
74
69
|
* @property {Record<string, Function>} [routes] Custom RPC routes for the database dispatcher.
|
|
75
70
|
* @property {(err: any) => void} [onerror] Background-task error handler.
|
|
76
71
|
* @property {number} [recoveryTimeout] Max wait to find another device and be admitted, in ms. Defaults to 30000.
|
|
77
72
|
* @property {Uint8Array} [storageKey] 32-byte key encrypting local key material (master seed, device keypairs) at rest. Source it from the OS keychain — cero never stores it.
|
|
78
|
-
* @property {
|
|
73
|
+
* @property {import('./extensions/index.js').Extension[]} [extensions] The extensions this instance runs, instead of the ones the spec carries. Build with the same list.
|
|
74
|
+
* @property {Record<string, any>} [operators] The operators to bind, instead of the ones the spec carries.
|
|
79
75
|
* @property {boolean | { autoStart?: boolean, backend?: any, maxOutbound?: number, maxInbound?: number, pipe?: 'l2cap' | 'gatt' }} [bluetooth] `true` enables nearby (Bluetooth) sync via `me.bluetooth` (auto-started). `{ autoStart: false }` creates the facade without starting the radio — the app calls `me.bluetooth.start()`/`stop()` (user toggle). `backend` injects a bare-bluetooth-shaped backend (tests). `maxOutbound`/`maxInbound` cap concurrent outbound links and inbound sessions. `pipe` picks the data pipe — `'l2cap'` (default, faster) or `'gatt'`; both peers must match. Absent backend on an unsupported host → `me.bluetooth.state === 'unsupported'`.
|
|
80
76
|
*/
|
|
81
77
|
|
|
@@ -129,7 +125,8 @@ export async function cero(dir, spec, opts = {}) {
|
|
|
129
125
|
backoffs: opts.backoffs,
|
|
130
126
|
channel: opts.channel,
|
|
131
127
|
store,
|
|
132
|
-
mirrors: opts.mirrors
|
|
128
|
+
mirrors: opts.mirrors,
|
|
129
|
+
presence: opts.presence
|
|
133
130
|
})
|
|
134
131
|
await network.ready()
|
|
135
132
|
discovery = network.join(identity.topic)
|
|
@@ -183,22 +180,12 @@ export async function cero(dir, spec, opts = {}) {
|
|
|
183
180
|
secretKey: result.writer.secretKey
|
|
184
181
|
})
|
|
185
182
|
}
|
|
186
|
-
if (!recovering) {
|
|
187
|
-
|
|
188
|
-
await me.store.call('add-member', {
|
|
189
|
-
id: identity.id,
|
|
190
|
-
key: me.store.writerKey,
|
|
191
|
-
role: 'owner',
|
|
192
|
-
name: opts.name || null,
|
|
193
|
-
createdAt: ts,
|
|
194
|
-
updatedAt: ts
|
|
195
|
-
})
|
|
196
|
-
if (pointer.length === 0) await pointer.append(c.encode(c.fixed32, me.store.key))
|
|
183
|
+
if (!recovering && pointer.length === 0) {
|
|
184
|
+
await pointer.append(c.encode(c.fixed32, me.store.key))
|
|
197
185
|
}
|
|
198
186
|
}
|
|
199
187
|
|
|
200
|
-
for (const ext of
|
|
201
|
-
if (ext.bundled && opts.extensions === false) continue
|
|
188
|
+
for (const ext of me.extensions) {
|
|
202
189
|
const off = await ext.setup?.(me)
|
|
203
190
|
if (typeof off === 'function') me.once('close', off)
|
|
204
191
|
}
|
|
@@ -217,7 +204,7 @@ export async function cero(dir, spec, opts = {}) {
|
|
|
217
204
|
await me.bluetooth.ready()
|
|
218
205
|
}
|
|
219
206
|
|
|
220
|
-
bind(me, null)
|
|
207
|
+
bind(me, null, me.operators)
|
|
221
208
|
return me
|
|
222
209
|
} catch (err) {
|
|
223
210
|
// before the root Handle exists, tear the raw resources down in reverse order
|
|
@@ -266,7 +253,6 @@ cero.put = put
|
|
|
266
253
|
cero.set = set
|
|
267
254
|
cero.get = get
|
|
268
255
|
cero.del = del
|
|
269
|
-
cero.count = count
|
|
270
256
|
cero.watch = watch
|
|
271
257
|
cero.changes = changes
|
|
272
258
|
cero.call = call
|
|
@@ -277,18 +263,6 @@ cero.after = after
|
|
|
277
263
|
cero.peek = peek
|
|
278
264
|
cero.restore = restore
|
|
279
265
|
cero.schema = schema
|
|
280
|
-
cero.bind = bind
|
|
281
|
-
cero.define = define
|
|
282
|
-
// test-only
|
|
283
|
-
cero._registry = registry
|
|
284
|
-
// a bare function is shorthand for { setup }; a named extension replaces one of the same name
|
|
285
|
-
cero.use = (...exts) => {
|
|
286
|
-
for (const e of exts.flat().map((e) => (typeof e === 'function' ? { setup: e } : e))) {
|
|
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)
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
266
|
|
|
293
267
|
function pointerManifest(store, identity) {
|
|
294
268
|
return { version: store.manifestVersion, signers: [{ publicKey: identity.publicKey }] }
|