@cero-base/core 0.0.5 → 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.
- package/LICENSE +201 -0
- package/README.md +200 -61
- package/package.json +85 -26
- 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 +59 -194
- 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 -98
- package/src/lib/batch.js +0 -95
- package/src/lib/builder.js +0 -24
- package/src/lib/collection.js +0 -208
- package/src/lib/crypto.js +0 -6
- package/src/lib/index.js +0 -6
- package/src/lib/room.js +0 -156
package/src/lib/utils.js
CHANGED
|
@@ -1,60 +1,65 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
[SCOPE_LOCAL]: 'local',
|
|
6
|
-
[SCOPE_PRIVATE]: 'identity',
|
|
7
|
-
[SCOPE_SHARED]: 'room'
|
|
8
|
-
}
|
|
1
|
+
import crypto from 'crypto'
|
|
2
|
+
import { Readable } from 'streamx'
|
|
3
|
+
import z32 from 'z32'
|
|
4
|
+
import { encode, decode, isValid } from 'hypercore-id-encoding'
|
|
9
5
|
|
|
10
6
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
7
|
+
* Generate a short opaque id (z32-encoded 16 random bytes).
|
|
8
|
+
*
|
|
9
|
+
* @returns {string}
|
|
13
10
|
*/
|
|
14
|
-
export function
|
|
15
|
-
|
|
16
|
-
if (name.endsWith('s')) return name.slice(0, -1)
|
|
17
|
-
return name
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** Get cero namespace for a scope */
|
|
21
|
-
export function scopeNs(scope) {
|
|
22
|
-
return SCOPE_NS[scope]
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** Get dispatch/collection path: @ns/name */
|
|
26
|
-
export function scopePath(scope, name) {
|
|
27
|
-
return `@${SCOPE_NS[scope]}/${name}`
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function isLocal(scope) {
|
|
31
|
-
return scope === SCOPE_LOCAL
|
|
11
|
+
export function genId() {
|
|
12
|
+
return z32.encode(crypto.randomBytes(16))
|
|
32
13
|
}
|
|
33
14
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
15
|
+
/**
|
|
16
|
+
* z32-encode a 32-byte key into a canonical string id.
|
|
17
|
+
*
|
|
18
|
+
* @type {(key: Uint8Array) => string}
|
|
19
|
+
*/
|
|
20
|
+
export const toId = encode
|
|
37
21
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Decode a canonical string id back into a 32-byte key.
|
|
24
|
+
*
|
|
25
|
+
* @type {(id: string) => Uint8Array}
|
|
26
|
+
*/
|
|
27
|
+
export const toKey = decode
|
|
41
28
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
29
|
+
/**
|
|
30
|
+
* Test whether a string is a well-formed canonical id.
|
|
31
|
+
*
|
|
32
|
+
* @type {(id: string) => boolean}
|
|
33
|
+
*/
|
|
34
|
+
export const isKeyId = isValid
|
|
49
35
|
|
|
50
36
|
/**
|
|
51
|
-
*
|
|
37
|
+
* Stream-of-snapshots primitive. Couples a `get` (returns the latest value)
|
|
38
|
+
* with a `watch` (re-fires on change) and emits the newest snapshot every
|
|
39
|
+
* time `watch` ticks.
|
|
40
|
+
*
|
|
41
|
+
* @template T
|
|
42
|
+
* @param {object} args
|
|
43
|
+
* @param {() => Promise<T> | T} args.get
|
|
44
|
+
* @param {(fn: () => void) => (() => void) | void} args.watch Returns an unsubscribe.
|
|
45
|
+
* @returns {import('streamx').Readable<T>}
|
|
52
46
|
*/
|
|
53
|
-
export function
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
})
|
|
47
|
+
export function subscribe({ get, watch }) {
|
|
48
|
+
let stop = null
|
|
49
|
+
const stream = new Readable({
|
|
50
|
+
destroy(cb) {
|
|
51
|
+
Promise.resolve(stop?.()).then(() => cb(null), cb)
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
const push = async () => {
|
|
55
|
+
if (stream.destroyed) return
|
|
56
|
+
try {
|
|
57
|
+
stream.push(await get())
|
|
58
|
+
} catch (e) {
|
|
59
|
+
stream.destroy(e)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
stop = watch(push)
|
|
63
|
+
push()
|
|
64
|
+
return stream
|
|
60
65
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import safetyCatch from 'safety-catch'
|
|
2
|
+
|
|
3
|
+
import { ACTIVE, PASSIVE } from '../lib/constants.js'
|
|
4
|
+
import { CeroError } from '../lib/errors.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Handle for a single topic membership on a {@link Network}. Wraps a hyperswarm
|
|
8
|
+
* PeerDiscovery session and lets it switch between active/passive announce/lookup.
|
|
9
|
+
*/
|
|
10
|
+
export class Discovery {
|
|
11
|
+
/**
|
|
12
|
+
* @param {import('./index.js').Network} network Owning network.
|
|
13
|
+
* @param {any} session Hyperswarm PeerDiscovery session from `swarm.join()`.
|
|
14
|
+
* @param {'active' | 'passive'} mode
|
|
15
|
+
*/
|
|
16
|
+
constructor(network, session, mode) {
|
|
17
|
+
this.network = network
|
|
18
|
+
this.session = session
|
|
19
|
+
this._mode = mode
|
|
20
|
+
this._destroyed = false
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** @returns {'active' | 'passive'} */
|
|
24
|
+
get mode() {
|
|
25
|
+
return this._mode
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** @returns {boolean} */
|
|
29
|
+
get destroyed() {
|
|
30
|
+
return this._destroyed
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Switch to active (client+server) announce/lookup.
|
|
35
|
+
*
|
|
36
|
+
* @returns {Promise<void>}
|
|
37
|
+
*/
|
|
38
|
+
async activate() {
|
|
39
|
+
if (this._destroyed) throw CeroError.DESTROYED('Discovery')
|
|
40
|
+
this._mode = ACTIVE
|
|
41
|
+
await this.session.refresh({ client: true, server: true })
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Switch to passive announce.
|
|
46
|
+
*
|
|
47
|
+
* @returns {Promise<void>}
|
|
48
|
+
*/
|
|
49
|
+
async deactivate() {
|
|
50
|
+
if (this._destroyed) throw CeroError.DESTROYED('Discovery')
|
|
51
|
+
this._mode = PASSIVE
|
|
52
|
+
await this.session.refresh({ client: false, server: true })
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Wait for the current announce/lookup to settle.
|
|
57
|
+
*
|
|
58
|
+
* @returns {Promise<void>}
|
|
59
|
+
*/
|
|
60
|
+
async flush() {
|
|
61
|
+
if (this._destroyed) return
|
|
62
|
+
await this.session.flushed()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Leave the topic and tear down the session. Idempotent.
|
|
67
|
+
*
|
|
68
|
+
* @returns {Promise<void>}
|
|
69
|
+
*/
|
|
70
|
+
async destroy() {
|
|
71
|
+
if (this._destroyed) return
|
|
72
|
+
this._destroyed = true
|
|
73
|
+
this.network._discoveries.delete(this)
|
|
74
|
+
try {
|
|
75
|
+
await this.session.destroy()
|
|
76
|
+
} catch (err) {
|
|
77
|
+
safetyCatch(err)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import Hyperswarm from 'hyperswarm'
|
|
2
|
+
import ProtomuxWakeup from 'protomux-wakeup'
|
|
3
|
+
import ReadyResource from 'ready-resource'
|
|
4
|
+
import safetyCatch from 'safety-catch'
|
|
5
|
+
import b4a from 'b4a'
|
|
6
|
+
|
|
7
|
+
import { ACTIVE, PASSIVE } from '../lib/constants.js'
|
|
8
|
+
import { CeroError } from '../lib/errors.js'
|
|
9
|
+
import { Discovery } from './discovery.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {object} NetworkOpts
|
|
13
|
+
* @property {import('../identity/index.js').Identity} [identity] Long-lived keypair used as the swarm identity.
|
|
14
|
+
* @property {Array<{ host: string, port: number }>} [bootstrap] Custom DHT bootstrap nodes.
|
|
15
|
+
* @property {(remotePublicKey: Uint8Array, payload: any) => boolean} [firewall] Incoming-connection filter.
|
|
16
|
+
* @property {Uint8Array[]} [relayThrough] Relay public keys to tunnel through.
|
|
17
|
+
*
|
|
18
|
+
* @typedef {{ replicate: (stream: any) => any }} Replicable
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Hyperswarm peer-discovery + replication multiplexer. Wraps a swarm and a
|
|
23
|
+
* shared wakeup channel, replicating any attached resource onto every peer.
|
|
24
|
+
*/
|
|
25
|
+
export class Network extends ReadyResource {
|
|
26
|
+
/** @param {NetworkOpts} [opts] */
|
|
27
|
+
constructor({ identity, bootstrap, firewall, relayThrough } = {}) {
|
|
28
|
+
super()
|
|
29
|
+
this.identity = identity || null
|
|
30
|
+
this.bootstrap = bootstrap || null
|
|
31
|
+
this.firewall = firewall || null
|
|
32
|
+
this.relayThrough = relayThrough || null
|
|
33
|
+
|
|
34
|
+
this.swarm = null
|
|
35
|
+
this.wakeup = new ProtomuxWakeup()
|
|
36
|
+
|
|
37
|
+
this._replicateables = new Set()
|
|
38
|
+
this._discoveries = new Set()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** @returns {Map<string, any>} Known peers keyed by public-key string. */
|
|
42
|
+
get peers() {
|
|
43
|
+
return this.swarm ? this.swarm.peers : new Map()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** @returns {Set<any>} Live connection streams. */
|
|
47
|
+
get connections() {
|
|
48
|
+
return this.swarm ? this.swarm.connections : new Set()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** @returns {boolean} */
|
|
52
|
+
get suspended() {
|
|
53
|
+
return this.swarm?.suspended === true
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async _open() {
|
|
57
|
+
const opts = {}
|
|
58
|
+
if (this.identity)
|
|
59
|
+
opts.keyPair = { publicKey: this.identity.publicKey, secretKey: this.identity.secretKey }
|
|
60
|
+
if (this.bootstrap) opts.bootstrap = this.bootstrap
|
|
61
|
+
if (this.firewall) opts.firewall = this.firewall
|
|
62
|
+
if (this.relayThrough) opts.relayThrough = this.relayThrough
|
|
63
|
+
|
|
64
|
+
this.swarm = new Hyperswarm(opts)
|
|
65
|
+
|
|
66
|
+
this.swarm.on('connection', (stream, info) => {
|
|
67
|
+
if (this.closing || this.closed) {
|
|
68
|
+
stream.destroy()
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
this.wakeup.addStream(stream)
|
|
72
|
+
for (const r of this._replicateables) replicateInto(r, stream)
|
|
73
|
+
this.emit('connection', stream, info)
|
|
74
|
+
})
|
|
75
|
+
this.swarm.on('peer-add', (peer) => this.emit('peer-add', peer))
|
|
76
|
+
this.swarm.on('peer-remove', (peer) => this.emit('peer-remove', peer))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Wait for pending DHT announces and lookups to settle, bounded by timeout.
|
|
81
|
+
*
|
|
82
|
+
* @param {{ timeout?: number }} [opts]
|
|
83
|
+
* @returns {Promise<void>}
|
|
84
|
+
*/
|
|
85
|
+
async flush({ timeout = 500 } = {}) {
|
|
86
|
+
if (!this.swarm) return
|
|
87
|
+
await Promise.race([this.swarm.flush(), new Promise((r) => setTimeout(r, timeout))])
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Pause the swarm — keeps state, drops sockets. Idempotent.
|
|
92
|
+
*
|
|
93
|
+
* @returns {Promise<void>}
|
|
94
|
+
*/
|
|
95
|
+
async suspend() {
|
|
96
|
+
if (!this.swarm || this.closing || this.closed) return
|
|
97
|
+
if (this.swarm.suspended) return
|
|
98
|
+
try {
|
|
99
|
+
await this.swarm.suspend()
|
|
100
|
+
} catch (err) {
|
|
101
|
+
safetyCatch(err)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Resume a suspended swarm. Idempotent.
|
|
107
|
+
*
|
|
108
|
+
* @returns {Promise<void>}
|
|
109
|
+
*/
|
|
110
|
+
async resume() {
|
|
111
|
+
if (!this.swarm || this.closing || this.closed) return
|
|
112
|
+
if (!this.swarm.suspended) return
|
|
113
|
+
try {
|
|
114
|
+
await this.swarm.resume()
|
|
115
|
+
} catch (err) {
|
|
116
|
+
safetyCatch(err)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async _close() {
|
|
121
|
+
for (const d of [...this._discoveries]) {
|
|
122
|
+
try {
|
|
123
|
+
await d.destroy()
|
|
124
|
+
} catch (err) {
|
|
125
|
+
safetyCatch(err)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (this.swarm) {
|
|
130
|
+
try {
|
|
131
|
+
await this.flush()
|
|
132
|
+
} catch (err) {
|
|
133
|
+
safetyCatch(err)
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
await this.swarm.destroy()
|
|
137
|
+
} catch (err) {
|
|
138
|
+
safetyCatch(err)
|
|
139
|
+
}
|
|
140
|
+
this.swarm = null
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (this.wakeup) {
|
|
144
|
+
try {
|
|
145
|
+
await this.wakeup.destroy()
|
|
146
|
+
} catch (err) {
|
|
147
|
+
safetyCatch(err)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Announce/lookup on a 32-byte topic and return a Discovery handle.
|
|
154
|
+
*
|
|
155
|
+
* @param {Uint8Array} topic
|
|
156
|
+
* @param {{ mode?: 'active' | 'passive' }} [opts] `active` = client+server, `passive` = server-only
|
|
157
|
+
* @returns {Discovery}
|
|
158
|
+
*/
|
|
159
|
+
join(topic, { mode = ACTIVE } = {}) {
|
|
160
|
+
if (mode !== ACTIVE && mode !== PASSIVE) {
|
|
161
|
+
throw CeroError.INVALID('mode must be "active" or "passive"')
|
|
162
|
+
}
|
|
163
|
+
if (this.closing || this.closed) throw CeroError.CLOSED('Network')
|
|
164
|
+
if (!this.swarm) throw CeroError.NOT_READY('Network', 'network')
|
|
165
|
+
if (!isTopic(topic)) throw CeroError.INVALID('topic must be a 32-byte buffer')
|
|
166
|
+
|
|
167
|
+
const session =
|
|
168
|
+
mode === ACTIVE
|
|
169
|
+
? this.swarm.join(topic, { client: true, server: true })
|
|
170
|
+
: this.swarm.join(topic, { client: false, server: true })
|
|
171
|
+
|
|
172
|
+
const discovery = new Discovery(this, session, mode)
|
|
173
|
+
this._discoveries.add(discovery)
|
|
174
|
+
return discovery
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Register a replicable resource (hypercore, autobee, hyperdb).
|
|
179
|
+
* It is replicated on every current and future swarm connection.
|
|
180
|
+
*
|
|
181
|
+
* @param {Replicable} core
|
|
182
|
+
* @returns {void}
|
|
183
|
+
*/
|
|
184
|
+
attach(core) {
|
|
185
|
+
if (!core) throw CeroError.REQUIRED('core')
|
|
186
|
+
this._replicateables.add(core)
|
|
187
|
+
if (this.swarm) {
|
|
188
|
+
for (const stream of this.swarm.connections) replicateInto(core, stream)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Unregister a previously attached resource. New connections will no
|
|
194
|
+
* longer replicate it (existing replication streams continue).
|
|
195
|
+
*
|
|
196
|
+
* @param {Replicable} core
|
|
197
|
+
* @returns {void}
|
|
198
|
+
*/
|
|
199
|
+
detach(core) {
|
|
200
|
+
if (!core) throw CeroError.REQUIRED('core')
|
|
201
|
+
this._replicateables.delete(core)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Replicate a one-off resource onto every current swarm connection
|
|
206
|
+
* without registering it as a long-lived attachment.
|
|
207
|
+
*
|
|
208
|
+
* @param {Replicable} target
|
|
209
|
+
* @returns {void}
|
|
210
|
+
*/
|
|
211
|
+
replicate(target) {
|
|
212
|
+
if (!target || typeof target.replicate !== 'function') {
|
|
213
|
+
throw CeroError.INVALID('target must be an object with a replicate(stream) method')
|
|
214
|
+
}
|
|
215
|
+
if (this.swarm) {
|
|
216
|
+
for (const stream of this.swarm.connections) replicateInto(target, stream)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function replicateInto(core, stream) {
|
|
222
|
+
try {
|
|
223
|
+
core.replicate(stream)
|
|
224
|
+
} catch (err) {
|
|
225
|
+
safetyCatch(err)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function isTopic(x) {
|
|
230
|
+
return (b4a.isBuffer(x) || x instanceof Uint8Array) && x.length === 32
|
|
231
|
+
}
|