OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-svc commits / 50084d7

Initial commit — hyper-svc
Reusable per-app daemon library. Provides UDS + HTTP adapters for local/browser RPC, plus a SwarmTransport that replicates a shared corestore to peers on a topic. Generic svc.handle(name, fn) registration dispatches from any transport into a uniform (ctx, req) handler signature. Mount domain modules with svc.mount('name', instance); their .base (if present) is replicate()'d on every swarm connection, and any instance exposing attachProtocol(mux) gets wired onto a per- connection Protomux so mux-based protocols (hyper-overlay, etc.) coexist with corestore replication. Length-prefixed JSON frames on UDS, POST /rpc/<method> over HTTP. No RPC over swarm in v1 — swarm is replication-only; future coordination would ride a new protomux channel. Local-user trust over UDS (ctx.identity.type = 'local', userId = env.USER); HTTP ctxFactory lets consumers populate ctx.identity from cookies/bearer tokens without hyper-svc knowing the auth scheme. ctxFactory was the only bug fixed during step 7 — the setImmediate 404 fallback also had to change to listenerCount('request') to avoid racing async user listeners. 20 tests / 37 asserts passing. Phase 6 of the pear-git-v3 bottom-up rebuild. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
50084d7f5aa6e465c2018a06d5972a174501e308
by plankatron <fleeky@gmail.com> · 2026-04-15T03:41:39.000Z

Changed files (13) +3445 -0

.gitignore +18 -0
……@@ -0,0 +1,18 @@
1# Dependencies
2node_modules/
3
4# Storage directories created by example CLIs
5.hyper-svc-*/
6
7# UDS sockets left behind by crashes
8*.sock
9
10# Editor and OS cruft
11.DS_Store
12*.log
13*.swp
14.vscode/
15.idea/
16
17# Test artifacts
18coverage/
examples/cli.js +161 -0
……@@ -0,0 +1,161 @@
1#!/usr/bin/env node
2/**
3 * Mini "echo service" demo for hyper-svc.
4 *
5 * Demonstrates the daemon + client lifecycle in two modes:
6 *
7 * node examples/cli.js daemon Start the echo daemon (UDS+HTTP)
8 * node examples/cli.js call <method> [json] Connect to the daemon and call a method
9 * node examples/cli.js demo Run an in-process round-trip end-to-end
10 *
11 * The daemon registers two methods:
12 * - "echo" → returns whatever you send
13 * - "uppercase" → returns the upper-cased string
14 * - "info" → returns service metadata (name, handlers, transports)
15 *
16 * Storage: HYPER_SVC_STORE env var (default: ./.hyper-svc-cli)
17 * Socket: HYPER_SVC_SOCK env var (default: ./.hyper-svc-cli/svc.sock)
18 *
19 * NOTE: storage path is provided for parity with other modules' examples; the
20 * echo demo doesn't actually use it. Real consumers (pear-git-v3) would pass
21 * a corestore for the swarm transport to replicate.
22 */
23
24import { Service, Client } from '../index.js'
25import { mkdir } from 'fs/promises'
26import { join, dirname } from 'path'
27
28const storagePath = process.env.HYPER_SVC_STORE || './.hyper-svc-cli'
29const sockPath = process.env.HYPER_SVC_SOCK || join(storagePath, 'svc.sock')
30const httpPort = parseInt(process.env.HYPER_SVC_HTTP || '4090')
31
32const args = process.argv.slice(2)
33const cmd = args[0]
34
35function buildService () {
36 const svc = new Service({
37 name: 'echo',
38 uds: sockPath,
39 http: { port: httpPort }
40 })
41 svc.handle('echo', async (ctx, params) => {
42 return { from: ctx.transport, params }
43 })
44 svc.handle('uppercase', async (ctx, params) => {
45 return String(params.text || '').toUpperCase()
46 })
47 svc.handle('info', async (ctx) => {
48 return {
49 name: svc.name,
50 transport: ctx.transport,
51 identity: ctx.identity,
52 handlers: svc.listHandlers(),
53 mounts: svc.listMounts()
54 }
55 })
56 return svc
57}
58
59async function main () {
60 if (cmd === 'daemon') {
61 await mkdir(dirname(sockPath), { recursive: true })
62 const svc = buildService()
63 await svc.listen()
64 console.log('hyper-svc echo daemon')
65 console.log(' uds: %s', sockPath)
66 console.log(' http: http://127.0.0.1:%d', svc.http.port)
67 console.log(' handlers: %s', svc.listHandlers().join(', '))
68 console.log(' (Ctrl-C to stop)')
69
70 process.on('SIGINT', async () => {
71 console.log('\nshutting down...')
72 try { await svc.close() } catch {}
73 process.exit(0)
74 })
75 return
76 }
77
78 if (cmd === 'call') {
79 const method = args[1]
80 if (!method) { console.error('Usage: cli.js call <method> [json-params]'); process.exit(1) }
81 let params = {}
82 if (args[2]) {
83 try { params = JSON.parse(args[2]) } catch (e) {
84 console.error('bad JSON params:', e.message)
85 process.exit(1)
86 }
87 }
88 const client = await Client.discover({
89 uds: sockPath,
90 http: `http://127.0.0.1:${httpPort}`
91 })
92 console.log('connected via', client.kind)
93 try {
94 const result = await client.call(method, params)
95 console.log(JSON.stringify(result, null, 2))
96 } finally {
97 await client.close()
98 }
99 return
100 }
101
102 if (cmd === 'demo') {
103 // In-process: spin up a daemon, connect to it, make calls, shut it down
104 await mkdir(dirname(sockPath), { recursive: true })
105 const svc = buildService()
106 await svc.listen()
107 console.log('--- hyper-svc demo ---\n')
108 console.log('Started service "echo"')
109 console.log(' uds: %s', sockPath)
110 console.log(' http: http://127.0.0.1:%d\n', svc.http.port)
111
112 console.log('1. Discover (prefers UDS)')
113 const client = await Client.discover({
114 uds: sockPath,
115 http: `http://127.0.0.1:${svc.http.port}`
116 })
117 console.log(' → connected via', client.kind, '\n')
118
119 console.log('2. Call echo({ hello: "world" })')
120 const a = await client.call('echo', { hello: 'world' })
121 console.log(' →', JSON.stringify(a), '\n')
122
123 console.log('3. Call uppercase({ text: "hyper-svc" })')
124 const b = await client.call('uppercase', { text: 'hyper-svc' })
125 console.log(' →', JSON.stringify(b), '\n')
126
127 console.log('4. Call info()')
128 const c = await client.call('info', {})
129 console.log(' →', JSON.stringify(c, null, 2), '\n')
130
131 console.log('5. Force HTTP transport')
132 await client.close()
133 const httpClient = await Client.discover({
134 http: `http://127.0.0.1:${svc.http.port}`,
135 prefer: 'http'
136 })
137 console.log(' → connected via', httpClient.kind)
138 const d = await httpClient.call('echo', { from: 'http' })
139 console.log(' →', JSON.stringify(d), '\n')
140
141 await httpClient.close()
142 await svc.close()
143 console.log('shutdown complete.')
144 return
145 }
146
147 console.log('hyper-svc echo service example')
148 console.log(' daemon Start the daemon (UDS + HTTP)')
149 console.log(' call <method> [json-params] Call a method on a running daemon')
150 console.log(' demo Run an in-process round-trip')
151 console.log('')
152 console.log('Storage: HYPER_SVC_STORE env var (default: ./.hyper-svc-cli)')
153 console.log('Socket: HYPER_SVC_SOCK env var (default: <storage>/svc.sock)')
154 console.log('HTTP: HYPER_SVC_HTTP env var (default: 4090)')
155 console.log('')
156 console.log('Note: this CLI does NOT share state with examples/web.js — they use')
157 console.log('different storage paths because the corestore single-writer lock applies')
158 console.log('once you start mounting real modules.')
159}
160
161main().catch((err) => { console.error(err.message); process.exit(1) })
examples/web.js +218 -0
……@@ -0,0 +1,218 @@
1#!/usr/bin/env node
2/**
3 * Mini SSR dashboard for hyper-svc.
4 *
5 * Demonstrates the "app routes coexist with /rpc" pattern: hyper-svc owns
6 * the http.Server and reserves POST /rpc/<method>; the app registers its own
7 * 'request' listener for SSR pages.
8 *
9 * Pages:
10 * GET / → status overview (handlers, mounts, transport state)
11 * GET /handlers → list of registered handlers
12 * POST /call → form-driven call to any handler (echoes result)
13 *
14 * Usage: node examples/web.js [--port 9098] [--storage <path>]
15 *
16 * NOTE: Uses its own storage path by default (./.hyper-svc-web). Does NOT
17 * share state with examples/cli.js — daemons can't share storage anyway.
18 */
19
20import { Service } from '../index.js'
21import b4a from 'b4a'
22import { mkdir } from 'fs/promises'
23import { join, dirname } from 'path'
24
25const args = process.argv.slice(2)
26function getFlag (name) {
27 const i = args.indexOf('--' + name)
28 return i !== -1 && args[i + 1] ? args[i + 1] : null
29}
30
31const port = parseInt(getFlag('port')) || 9098
32const storagePath = getFlag('storage') || './.hyper-svc-web'
33const sockPath = join(storagePath, 'svc.sock')
34
35await mkdir(storagePath, { recursive: true })
36
37const svc = new Service({
38 name: 'demo-web',
39 uds: sockPath,
40 http: { port }
41})
42
43svc.handle('echo', async (ctx, params) => ({ from: ctx.transport, params }))
44svc.handle('uppercase', async (ctx, params) => String(params.text || '').toUpperCase())
45svc.handle('reverse', async (ctx, params) => String(params.text || '').split('').reverse().join(''))
46svc.handle('time', async () => new Date().toISOString())
47
48// App-side routes — registered on the http.Server BEFORE listen()
49svc.http.server.on('request', async (req, res) => {
50 // Skip /rpc/* — hyper-svc owns those
51 if (req.url.startsWith('/rpc/')) return
52
53 try {
54 if (req.method === 'GET' && req.url === '/') {
55 return sendHtml(res, renderIndex())
56 }
57 if (req.method === 'GET' && req.url === '/handlers') {
58 return sendHtml(res, renderHandlers())
59 }
60 if (req.method === 'POST' && req.url === '/call') {
61 const body = await collectBody(req)
62 const params = new URLSearchParams(b4a.toString(body, 'utf8'))
63 const method = params.get('method')
64 const args = params.get('args') || '{}'
65 let parsed = {}
66 let result, error
67 try {
68 parsed = JSON.parse(args)
69 } catch (e) {
70 error = 'bad JSON args: ' + e.message
71 }
72 if (!error) {
73 try {
74 result = await svc.dispatch(method, parsed, { transport: 'http', identity: null })
75 } catch (e) {
76 error = e.message
77 }
78 }
79 return sendHtml(res, renderResult(method, args, result, error))
80 }
81 } catch (err) {
82 res.writeHead(500, { 'Content-Type': 'text/plain' })
83 res.end(err.message)
84 }
85})
86
87await svc.listen()
88
89console.log('hyper-svc dashboard:')
90console.log(' http://localhost:%d', port)
91console.log(' uds: %s', sockPath)
92console.log(' storage: %s', storagePath)
93
94// ── Helpers ──────────────────────────────────────────────────
95
96function sendHtml (res, html) {
97 res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
98 res.end(html)
99}
100
101function collectBody (req) {
102 return new Promise((resolve, reject) => {
103 const parts = []
104 req.on('data', (c) => parts.push(c))
105 req.on('end', () => resolve(b4a.concat(parts)))
106 req.on('error', reject)
107 })
108}
109
110function escapeHtml (s) {
111 return String(s == null ? '' : s)
112 .replace(/&/g, '&amp;').replace(/</g, '&lt;')
113 .replace(/>/g, '&gt;').replace(/"/g, '&quot;')
114}
115
116function styles () {
117 return `
118 body { font-family: monospace; background: #0d1117; color: #c9d1d9; padding: 2rem; max-width: 800px; margin: 0 auto; }
119 h1 { color: #58a6ff; }
120 h2 { font-size: 1rem; color: #c9d1d9; margin-top: 0; }
121 a { color: #58a6ff; text-decoration: none; }
122 a:hover { text-decoration: underline; }
123 .card { background: #161b22; border: 1px solid #30363d; border-radius: 6px; padding: 1rem; margin: 1rem 0; }
124 .card h2 { margin: 0 0 0.5rem; font-size: 0.75rem; text-transform: uppercase; color: #8b949e; }
125 .row { display: flex; justify-content: space-between; padding: 0.3rem 0; border-bottom: 1px solid #21262d; }
126 .row:last-child { border-bottom: none; }
127 .label { color: #8b949e; }
128 pre { background: #0d1117; border: 1px solid #30363d; padding: 0.8rem; border-radius: 4px; overflow-x: auto; color: #c9d1d9; }
129 pre.error { color: #f85149; border-color: #f8514944; }
130 input, textarea, button, select { background: #0d1117; color: #c9d1d9; border: 1px solid #30363d; padding: 0.4rem 0.6rem; border-radius: 4px; font-family: monospace; font-size: 0.85rem; }
131 textarea { width: 100%; min-height: 4em; }
132 input, select { width: 100%; box-sizing: border-box; }
133 button { background: #1f6feb; border-color: #1f6feb; color: white; cursor: pointer; }
134 button:hover { background: #1158c7; }
135 .field { margin-bottom: 0.5rem; }
136 .field label { display: block; color: #8b949e; font-size: 0.75rem; margin-bottom: 0.2rem; }
137 .badge-ok { background: #2ea04322; color: #3fb950; border: 1px solid #2ea04344; padding: 0.05rem 0.4rem; border-radius: 4px; font-size: 0.7rem; }
138 .badge-off { background: #30363d; color: #8b949e; border: 1px solid #484f58; padding: 0.05rem 0.4rem; border-radius: 4px; font-size: 0.7rem; }
139 `
140}
141
142function renderLayout (title, body) {
143 return `<!DOCTYPE html>
144<html><head><title>${escapeHtml(title)}</title><style>${styles()}</style></head><body>
145<h1><a href="/">hyper-svc</a> · ${escapeHtml(title)}</h1>
146${body}
147</body></html>`
148}
149
150function renderIndex () {
151 const transports = [
152 { name: 'uds', enabled: !!svc.uds, info: svc.uds ? svc.uds.path : '—' },
153 { name: 'http', enabled: !!svc.http, info: svc.http ? '127.0.0.1:' + (svc.http.port || '?') : '—' },
154 { name: 'swarm', enabled: !!svc.swarm, info: svc.swarm ? 'topic active' : '—' }
155 ]
156 const handlers = svc.listHandlers()
157 const mounts = svc.listMounts()
158
159 const body = `
160<div class="card">
161 <h2>Service</h2>
162 <div class="row"><div class="label">name</div><div>${escapeHtml(svc.name)}</div></div>
163 <div class="row"><div class="label">handlers</div><div>${handlers.length}</div></div>
164 <div class="row"><div class="label">mounts</div><div>${mounts.length}</div></div>
165</div>
166
167<div class="card">
168 <h2>Transports</h2>
169 ${transports.map(tr => `
170 <div class="row">
171 <div>${escapeHtml(tr.name)} ${tr.enabled ? '<span class="badge-ok">on</span>' : '<span class="badge-off">off</span>'}</div>
172 <div class="label">${escapeHtml(tr.info)}</div>
173 </div>`).join('')}
174</div>
175
176<div class="card">
177 <h2>Handlers</h2>
178 ${handlers.length === 0 ? '<div class="label">(none)</div>' : handlers.map(h => `
179 <div class="row"><div>${escapeHtml(h)}</div></div>`).join('')}
180 <div style="margin-top:0.8rem"><a href="/handlers">→ try a handler</a></div>
181</div>
182`
183 return renderLayout('overview', body)
184}
185
186function renderHandlers () {
187 const handlers = svc.listHandlers()
188 const body = `
189<div class="card">
190 <h2>Call a handler</h2>
191 <form action="/call" method="POST">
192 <div class="field"><label>Method</label>
193 <select name="method">
194 ${handlers.map(h => `<option value="${escapeHtml(h)}">${escapeHtml(h)}</option>`).join('')}
195 </select>
196 </div>
197 <div class="field"><label>JSON params</label><textarea name="args">{}</textarea></div>
198 <button type="submit">Call</button>
199 </form>
200</div>
201`
202 return renderLayout('handlers', body)
203}
204
205function renderResult (method, args, result, error) {
206 const body = `
207<div class="card">
208 <h2>Call: ${escapeHtml(method)}</h2>
209 <div class="label">params</div>
210 <pre>${escapeHtml(args)}</pre>
211 ${error
212 ? `<div class="label">error</div><pre class="error">${escapeHtml(error)}</pre>`
213 : `<div class="label">result</div><pre>${escapeHtml(JSON.stringify(result, null, 2))}</pre>`}
214 <div style="margin-top:0.8rem"><a href="/handlers">← back</a></div>
215</div>
216`
217 return renderLayout(`call ${method}`, body)
218}
index.js +10 -0
……@@ -0,0 +1,10 @@
1/**
2 * hyper-svc — reusable per-app daemon library
3 *
4 * Local-only RPC (UDS + HTTP) plus Hyperswarm replication for peer mirroring.
5 * No remote RPC: peers exchange data, not commands.
6 */
7
8export { Service } from './lib/service.js'
9export { Client } from './lib/client.js'
10export { encodeFrame, FrameDecoder } from './lib/frame.js'
lib/client.js +228 -0
……@@ -0,0 +1,228 @@
1/**
2 * Client — RPC client for hyper-svc daemons.
3 *
4 * Local-only. Connects via UDS (preferred) or HTTP. Both transports speak the
5 * same JSON-RPC-ish protocol; the wire format differs (length-prefixed frames
6 * for UDS, POST /rpc/<method> for HTTP) but the call signature is identical.
7 *
8 * Discovery: try UDS first (if a path is provided and the socket is reachable),
9 * fall back to HTTP. Explicit transport selection is also supported.
10 *
11 * Usage:
12 *
13 * const client = await Client.discover({
14 * uds: '/tmp/pear-git.sock',
15 * http: 'http://127.0.0.1:4040'
16 * })
17 *
18 * const orgs = await client.call('list-orgs', { limit: 10 })
19 *
20 * await client.close()
21 */
22
23import net from 'net'
24import http from 'http'
25import { URL } from 'url'
26import b4a from 'b4a'
27import { stat } from 'fs/promises'
28import { encodeFrame, FrameDecoder } from './frame.js'
29
30export class Client {
31 /**
32 * Discover and connect to a running hyper-svc daemon.
33 *
34 * @param {object} opts
35 * @param {string} [opts.uds] - UDS socket path. Tried first.
36 * @param {string} [opts.http] - HTTP base URL (e.g. 'http://127.0.0.1:4040'). Fallback.
37 * @param {'uds'|'http'} [opts.prefer] - explicit override
38 * @returns {Promise<Client>}
39 */
40 static async discover (opts = {}) {
41 const prefer = opts.prefer
42
43 if (prefer === 'uds') {
44 if (!opts.uds) throw new Error('discover({ prefer: "uds" }) requires opts.uds')
45 return Client.connectUds(opts.uds)
46 }
47 if (prefer === 'http') {
48 if (!opts.http) throw new Error('discover({ prefer: "http" }) requires opts.http')
49 return Client.connectHttp(opts.http)
50 }
51
52 // Auto: UDS first if reachable, then HTTP
53 if (opts.uds) {
54 const reachable = await udsReachable(opts.uds)
55 if (reachable) return Client.connectUds(opts.uds)
56 }
57 if (opts.http) {
58 return Client.connectHttp(opts.http)
59 }
60
61 const tried = []
62 if (opts.uds) tried.push(`uds:${opts.uds}`)
63 if (opts.http) tried.push(`http:${opts.http}`)
64 throw new Error(`no daemon reachable (tried: ${tried.join(', ') || 'nothing — pass uds and/or http'})`)
65 }
66
67 static async connectUds (path) {
68 const sock = net.connect(path)
69 await new Promise((resolve, reject) => {
70 sock.once('connect', () => {
71 sock.removeListener('error', reject)
72 resolve()
73 })
74 sock.once('error', reject)
75 })
76 return new Client({ kind: 'uds', sock })
77 }
78
79 static connectHttp (base) {
80 return new Client({ kind: 'http', base: base.replace(/\/+$/, '') })
81 }
82
83 constructor (opts) {
84 this._kind = opts.kind
85 this._sock = opts.sock || null
86 this._base = opts.base || null
87 this._closed = false
88
89 // UDS pending request map (id → {resolve, reject})
90 this._pending = new Map()
91 this._nextId = 1
92
93 if (this._kind === 'uds') {
94 this._decoder = new FrameDecoder((frame) => this._onFrame(frame))
95 this._sock.on('data', (chunk) => {
96 try {
97 this._decoder.push(chunk)
98 } catch (err) {
99 this._failAll(err)
100 this._sock.destroy()
101 }
102 })
103 this._sock.on('close', () => {
104 this._failAll(new Error('connection closed'))
105 })
106 this._sock.on('error', () => { /* close handler runs */ })
107 }
108 }
109
110 get kind () { return this._kind }
111 get closed () { return this._closed }
112
113 /**
114 * Make an RPC call. Throws on transport errors or remote handler errors.
115 * @param {string} method
116 * @param {any} [params]
117 */
118 async call (method, params = {}) {
119 if (this._closed) throw new Error('client closed')
120
121 if (this._kind === 'uds') return this._callUds(method, params)
122 if (this._kind === 'http') return this._callHttp(method, params)
123 throw new Error('unknown client kind: ' + this._kind)
124 }
125
126 _callUds (method, params) {
127 const id = this._nextId++
128 return new Promise((resolve, reject) => {
129 this._pending.set(id, { resolve, reject })
130 try {
131 this._sock.write(encodeFrame({ id, method, params }))
132 } catch (err) {
133 this._pending.delete(id)
134 reject(err)
135 }
136 })
137 }
138
139 _onFrame (frame) {
140 if (frame == null || (typeof frame.id !== 'number' && typeof frame.id !== 'string')) {
141 // Malformed response, can't match it to a caller
142 return
143 }
144 const pending = this._pending.get(frame.id)
145 if (!pending) return
146 this._pending.delete(frame.id)
147
148 if (frame.error) {
149 pending.reject(new Error(frame.error))
150 } else {
151 pending.resolve(frame.result)
152 }
153 }
154
155 _failAll (err) {
156 for (const [, pending] of this._pending) pending.reject(err)
157 this._pending.clear()
158 }
159
160 async _callHttp (method, params) {
161 const url = new URL(`${this._base}/rpc/${encodeURIComponent(method)}`)
162 return new Promise((resolve, reject) => {
163 const body = JSON.stringify(params)
164 const req = http.request({
165 method: 'POST',
166 hostname: url.hostname,
167 port: url.port || 80,
168 path: url.pathname + url.search,
169 headers: {
170 'Content-Type': 'application/json',
171 'Content-Length': b4a.byteLength(body)
172 }
173 }, (res) => {
174 const parts = []
175 res.on('data', (c) => parts.push(c))
176 res.on('end', () => {
177 const text = b4a.concat(parts).toString('utf8')
178 let parsed
179 try {
180 parsed = JSON.parse(text)
181 } catch {
182 return reject(new Error(`bad JSON response (status ${res.statusCode}): ${text.slice(0, 200)}`))
183 }
184 if (res.statusCode >= 200 && res.statusCode < 300) {
185 return resolve(parsed.result)
186 }
187 return reject(new Error(parsed.error || `HTTP ${res.statusCode}`))
188 })
189 res.on('error', reject)
190 })
191 req.on('error', reject)
192 req.write(body)
193 req.end()
194 })
195 }
196
197 async close () {
198 if (this._closed) return
199 this._closed = true
200 if (this._sock) {
201 this._failAll(new Error('client closed'))
202 try { this._sock.destroy() } catch {}
203 this._sock = null
204 }
205 }
206}
207
208async function udsReachable (path) {
209 // Check the socket file exists and is actually a socket
210 try {
211 const s = await stat(path)
212 if (!s.isSocket()) return false
213 } catch {
214 return false
215 }
216 // Try a real connect — covers stale sockets where the daemon crashed
217 return new Promise((resolve) => {
218 const sock = net.connect(path)
219 sock.once('connect', () => {
220 sock.destroy()
221 resolve(true)
222 })
223 sock.once('error', () => {
224 sock.destroy()
225 resolve(false)
226 })
227 })
228}
lib/frame.js +62 -0
……@@ -0,0 +1,62 @@
1/**
2 * Length-prefixed JSON framing.
3 *
4 * Wire format: [4 bytes BE length][JSON payload].
5 *
6 * One frame = one JSON object. Multiple frames can be pipelined on a single
7 * stream — FrameDecoder handles incremental reads where a frame may arrive
8 * across multiple data chunks.
9 */
10
11import b4a from 'b4a'
12
13const MAX_FRAME = 16 * 1024 * 1024 // 16 MiB hard cap to avoid runaway buffers
14
15export function encodeFrame (obj) {
16 const json = JSON.stringify(obj)
17 const payload = b4a.from(json, 'utf8')
18 if (payload.length > MAX_FRAME) {
19 throw new Error(`Frame too large: ${payload.length} > ${MAX_FRAME}`)
20 }
21 const frame = b4a.alloc(4 + payload.length)
22 frame.writeUInt32BE(payload.length, 0)
23 payload.copy(frame, 4)
24 return frame
25}
26
27export class FrameDecoder {
28 /**
29 * @param {(obj: any) => void} onFrame - called once per complete frame
30 */
31 constructor (onFrame) {
32 if (typeof onFrame !== 'function') throw new Error('onFrame is required')
33 this._onFrame = onFrame
34 this._buf = b4a.alloc(0)
35 }
36
37 /**
38 * Feed a chunk into the decoder. Calls onFrame for each complete frame.
39 * Throws if a frame exceeds MAX_FRAME or contains invalid JSON.
40 */
41 push (chunk) {
42 this._buf = b4a.concat([this._buf, chunk])
43
44 while (this._buf.length >= 4) {
45 const len = this._buf.readUInt32BE(0)
46 if (len > MAX_FRAME) {
47 throw new Error(`Frame too large: ${len} > ${MAX_FRAME}`)
48 }
49 if (this._buf.length < 4 + len) break // wait for more bytes
50
51 const payload = this._buf.subarray(4, 4 + len)
52 this._buf = this._buf.subarray(4 + len)
53
54 const json = b4a.toString(payload, 'utf8')
55 const obj = JSON.parse(json) // throws on bad JSON
56 this._onFrame(obj)
57 }
58 }
59
60 /** Number of bytes currently buffered (waiting for the rest of a frame). */
61 get pending () { return this._buf.length }
62}
lib/service.js +235 -0
……@@ -0,0 +1,235 @@
1/**
2 * Service — reusable per-app daemon library.
3 *
4 * One instance per app process. Owns:
5 * - Generic RPC handler registry (svc.handle)
6 * - Module mounts (svc.mount) with optional swarm replication
7 * - UDS, HTTP, and Hyperswarm transport instances (whichever the consumer enables)
8 *
9 * The consumer's corestore is passed in (or one is implied via swarm.corestore).
10 * The Service does NOT create or own storage — that's the app's job.
11 *
12 * Local-only RPC. Peers exchange data via swarm replication, never commands.
13 *
14 * Usage:
15 *
16 * const svc = new Service({
17 * name: 'pear-git',
18 * corestore: store,
19 * uds: '/tmp/pear-git.sock',
20 * http: { port: 4040 },
21 * swarm: { topic: '...32 bytes hex...' }
22 * })
23 *
24 * svc.mount('orgs', new HyperOrgs(store, { namespace: 'orgs' }))
25 *
26 * svc.handle('list-orgs', async (ctx, params) => {
27 * return svc.modules.orgs.list()
28 * })
29 *
30 * await svc.listen()
31 */
32
33import ReadyResource from 'ready-resource'
34import { UdsServer } from './transports/uds.js'
35import { HttpServer } from './transports/http.js'
36import { SwarmTransport } from './transports/swarm.js'
37
38class NoSuchMethodError extends Error {
39 constructor (method) {
40 super(`unknown method: ${method}`)
41 this.code = 'ENOMETHOD'
42 }
43}
44
45export class Service extends ReadyResource {
46 /**
47 * @param {object} opts
48 * @param {string} opts.name - service name (used for default UDS path, logging)
49 * @param {object} [opts.corestore] - required if swarm transport is enabled
50 * @param {string} [opts.uds] - UDS socket path. Pass null/undefined to disable.
51 * @param {object} [opts.http] - HTTP transport options { port, host }. Disable by omitting.
52 * @param {object} [opts.swarm] - Swarm options { topic, swarm? }. Disable by omitting.
53 */
54 constructor (opts = {}) {
55 super()
56 if (!opts.name) throw new Error('Service requires opts.name')
57
58 this._name = opts.name
59 this._corestore = opts.corestore || null
60 this._handlers = new Map()
61 this._mounts = new Map()
62
63 this._udsOpt = opts.uds || null
64 this._httpOpt = opts.http || null
65 this._swarmOpt = opts.swarm || null
66
67 this._uds = null
68 this._http = null
69 this._swarm = null
70
71 if (this._swarmOpt && !this._corestore) {
72 throw new Error('Service: swarm transport requires opts.corestore')
73 }
74
75 // Instantiate transports eagerly so apps can register HTTP routes,
76 // grab the UDS path, etc., BEFORE calling listen().
77 const dispatch = (method, params, ctx) => this.dispatch(method, params, ctx)
78
79 if (this._udsOpt) {
80 this._uds = new UdsServer({ path: this._udsOpt, dispatch })
81 }
82 if (this._httpOpt) {
83 this._http = new HttpServer({
84 port: this._httpOpt.port,
85 host: this._httpOpt.host,
86 ctxFactory: this._httpOpt.ctxFactory,
87 dispatch
88 })
89 }
90 if (this._swarmOpt) {
91 this._swarm = new SwarmTransport({
92 topic: this._swarmOpt.topic,
93 corestore: this._corestore,
94 mounts: () => this._listMounts(),
95 swarm: this._swarmOpt.swarm,
96 server: this._swarmOpt.server,
97 client: this._swarmOpt.client
98 })
99 }
100 }
101
102 // ── Properties ──────────────────────────────────────────────
103
104 get name () { return this._name }
105 get corestore () { return this._corestore }
106
107 /** Read-only handle to the UDS transport (or null if disabled). */
108 get uds () { return this._uds }
109
110 /** Read-only handle to the HTTP transport (or null if disabled). */
111 get http () { return this._http }
112
113 /** Read-only handle to the swarm transport (or null if disabled). */
114 get swarm () { return this._swarm }
115
116 /**
117 * Mounted modules, accessible by name. Use `svc.modules.orgs` after
118 * `svc.mount('orgs', orgsInstance)`.
119 */
120 get modules () {
121 // Lazily build a property-style accessor object on top of the map
122 if (!this._modulesProxy) {
123 const map = this._mounts
124 this._modulesProxy = new Proxy({}, {
125 get (_target, prop) {
126 const m = map.get(prop)
127 return m ? m.instance : undefined
128 },
129 has (_target, prop) {
130 return map.has(prop)
131 },
132 ownKeys () {
133 return Array.from(map.keys())
134 },
135 getOwnPropertyDescriptor (_target, prop) {
136 if (map.has(prop)) {
137 return { enumerable: true, configurable: true, value: map.get(prop).instance }
138 }
139 }
140 })
141 }
142 return this._modulesProxy
143 }
144
145 /** Map iteration: list mount entries as { name, instance }. */
146 _listMounts () {
147 const out = []
148 for (const [name, m] of this._mounts) out.push({ name, instance: m.instance })
149 return out
150 }
151
152 // ── Registration ────────────────────────────────────────────
153
154 /**
155 * Register an RPC handler. The handler is invoked from any RPC transport
156 * with `(ctx, params)` and should return a JSON-serializable result.
157 *
158 * @param {string} name - method name (e.g. "list-orgs")
159 * @param {(ctx: object, params: any) => Promise<any>|any} fn
160 */
161 handle (name, fn) {
162 if (typeof name !== 'string' || !name) throw new Error('handle requires a name')
163 if (typeof fn !== 'function') throw new Error('handle requires a function')
164 if (this._handlers.has(name)) throw new Error(`handler "${name}" already registered`)
165 this._handlers.set(name, fn)
166 }
167
168 /** Whether a handler is registered. */
169 hasHandler (name) { return this._handlers.has(name) }
170
171 /** List registered handler names. */
172 listHandlers () { return Array.from(this._handlers.keys()).sort() }
173
174 /**
175 * Mount a module instance. Modules are accessible via `svc.modules.<name>`.
176 * If the module exposes a `.base` (Autobase consumer) and the swarm
177 * transport is enabled, the base is replicated on every swarm connection.
178 *
179 * @param {string} name
180 * @param {object} instance
181 */
182 mount (name, instance) {
183 if (typeof name !== 'string' || !name) throw new Error('mount requires a name')
184 if (!instance) throw new Error('mount requires an instance')
185 if (this._mounts.has(name)) throw new Error(`mount "${name}" already registered`)
186 this._mounts.set(name, { instance })
187 }
188
189 /** Whether a module is mounted. */
190 hasMount (name) { return this._mounts.has(name) }
191
192 /** List mounted module names. */
193 listMounts () { return Array.from(this._mounts.keys()).sort() }
194
195 // ── Dispatch ────────────────────────────────────────────────
196
197 /**
198 * Dispatch a method call to its handler. Used by all RPC transports.
199 * Throws NoSuchMethodError (code: 'ENOMETHOD') if the method isn't registered.
200 */
201 async dispatch (method, params, ctx) {
202 const handler = this._handlers.get(method)
203 if (!handler) throw new NoSuchMethodError(method)
204 return handler(ctx || {}, params)
205 }
206
207 // ── Lifecycle ───────────────────────────────────────────────
208
209 async _open () {
210 if (this._uds) await this._uds.listen()
211 if (this._http) await this._http.listen()
212 if (this._swarm) await this._swarm.listen()
213 }
214
215 /**
216 * Alias for `ready()` — feels more natural for a daemon ("start listening").
217 */
218 async listen () {
219 await this.ready()
220 }
221
222 async _close () {
223 const errors = []
224 if (this._swarm) try { await this._swarm.close() } catch (e) { errors.push(e) }
225 if (this._http) try { await this._http.close() } catch (e) { errors.push(e) }
226 if (this._uds) try { await this._uds.close() } catch (e) { errors.push(e) }
227 this._swarm = null
228 this._http = null
229 this._uds = null
230 if (errors.length) {
231 const msg = errors.map(e => e.message).join('; ')
232 throw new Error('Service close errors: ' + msg)
233 }
234 }
235}
lib/transports/http.js +181 -0
……@@ -0,0 +1,181 @@
1/**
2 * HTTP transport for hyper-svc.
3 *
4 * Local-only RPC. Browsers (or curl, or any HTTP client) on the same machine
5 * POST a JSON body to /rpc/<method> and receive a JSON response.
6 *
7 * Wire protocol:
8 * POST /rpc/<method>
9 * Content-Type: application/json
10 * { ...params... }
11 *
12 * → 200 application/json { result: ... }
13 * 400 application/json { error: ... } (bad request / handler threw)
14 * 404 application/json { error: 'unknown method' }
15 *
16 * Other routes are NOT handled by hyper-svc — apps wire their own SSR pages
17 * via the underlying http.Server (exposed as `.server` on this class). Any
18 * request not matching POST /rpc/<method> falls through to user-registered
19 * 'request' listeners or, if none, returns 404.
20 */
21
22import http from 'http'
23import b4a from 'b4a'
24
25export class HttpServer {
26 /**
27 * @param {object} opts
28 * @param {number} [opts.port=0] - 0 = random port
29 * @param {string} [opts.host='127.0.0.1'] - bind address (local-only by default)
30 * @param {(method: string, params: any, ctx: object) => Promise<any>} opts.dispatch - request handler
31 */
32 constructor (opts = {}) {
33 if (typeof opts.dispatch !== 'function') throw new Error('HttpServer requires opts.dispatch')
34
35 this._port = opts.port || 0
36 this._host = opts.host || '127.0.0.1'
37 this._dispatch = opts.dispatch
38 // Optional consumer-provided ctx builder. Called per /rpc/* request to
39 // populate fields like identity from request headers/cookies. Receives
40 // (req) and may return either an object or a Promise<object>. The
41 // returned fields are merged into the default ctx ({ transport: 'http',
42 // identity: null, req, res }).
43 this._ctxFactory = typeof opts.ctxFactory === 'function' ? opts.ctxFactory : null
44 this._listening = false
45
46 // Create the server eagerly so apps can register routes BEFORE listen().
47 // Our /rpc/* listener is installed first so it has first dibs on requests.
48 this._server = http.createServer()
49 this._server.on('request', (req, res) => this._onRequest(req, res))
50 }
51
52 /**
53 * Update or set the ctxFactory after construction. Useful when the
54 * factory needs references that don't exist at constructor time
55 * (e.g. session stores created by the consumer in a later step).
56 */
57 setCtxFactory (fn) {
58 if (fn != null && typeof fn !== 'function') {
59 throw new Error('ctxFactory must be a function or null')
60 }
61 this._ctxFactory = fn
62 }
63
64 /** Underlying http.Server. Apps register their own routes here. */
65 get server () { return this._server }
66 get listening () { return this._listening }
67 get port () {
68 if (!this._listening || !this._server) return null
69 const addr = this._server.address()
70 return addr ? addr.port : null
71 }
72 get host () { return this._host }
73
74 async listen () {
75 if (this._listening) return
76
77 await new Promise((resolve, reject) => {
78 this._server.once('error', reject)
79 this._server.listen(this._port, this._host, () => {
80 this._server.removeListener('error', reject)
81 resolve()
82 })
83 })
84
85 this._listening = true
86 }
87
88 async _onRequest (req, res) {
89 if (req.method === 'POST' && req.url.startsWith('/rpc/')) {
90 const method = req.url.slice('/rpc/'.length).split('?')[0]
91 let body
92 try {
93 body = await collectBody(req)
94 } catch (err) {
95 return jsonError(res, 400, 'failed to read body: ' + err.message)
96 }
97
98 let params = {}
99 if (body.length > 0) {
100 try {
101 params = JSON.parse(b4a.toString(body, 'utf8'))
102 } catch {
103 return jsonError(res, 400, 'request body must be JSON')
104 }
105 }
106
107 let ctx = {
108 transport: 'http',
109 identity: null, // anonymous by default — apps populate via ctxFactory
110 req,
111 res
112 }
113
114 if (this._ctxFactory) {
115 try {
116 const extra = await this._ctxFactory(req)
117 if (extra && typeof extra === 'object') {
118 ctx = { ...ctx, ...extra }
119 }
120 } catch (err) {
121 return jsonError(res, 500, 'ctxFactory failed: ' + (err.message || String(err)))
122 }
123 }
124
125 try {
126 const result = await this._dispatch(method, params, ctx)
127 if (result === undefined) {
128 return jsonOk(res, { result: null })
129 }
130 return jsonOk(res, { result })
131 } catch (err) {
132 if (err && err.code === 'ENOMETHOD') {
133 return jsonError(res, 404, err.message)
134 }
135 return jsonError(res, 400, err.message || String(err))
136 }
137 }
138
139 // Not an /rpc/ request. App-owned routes live in OTHER 'request'
140 // listeners (registered on this._server by the consumer). If at least
141 // one other listener is present, leave the response to them — they're
142 // responsible for handling and 404-ing as needed.
143 //
144 // If we're the ONLY 'request' listener, nobody else will handle this,
145 // so we 404 it synchronously. Doing this synchronously avoids a race
146 // where setImmediate fires before an async user handler can respond.
147 if (this._server.listenerCount('request') <= 1) {
148 res.writeHead(404, { 'Content-Type': 'text/plain' })
149 res.end('Not found')
150 }
151 // Otherwise: leave the response to the user's listener.
152 }
153
154 async close () {
155 if (!this._server || !this._listening) {
156 this._listening = false
157 return
158 }
159 await new Promise((resolve) => this._server.close(() => resolve()))
160 this._listening = false
161 }
162}
163
164function collectBody (req) {
165 return new Promise((resolve, reject) => {
166 const parts = []
167 req.on('data', (c) => parts.push(c))
168 req.on('end', () => resolve(b4a.concat(parts)))
169 req.on('error', reject)
170 })
171}
172
173function jsonOk (res, body) {
174 res.writeHead(200, { 'Content-Type': 'application/json' })
175 res.end(JSON.stringify(body))
176}
177
178function jsonError (res, status, message) {
179 res.writeHead(status, { 'Content-Type': 'application/json' })
180 res.end(JSON.stringify({ error: message }))
181}
lib/transports/swarm.js +142 -0
……@@ -0,0 +1,142 @@
1/**
2 * Hyperswarm transport for hyper-svc.
3 *
4 * Joins a topic, and on every connection:
5 * 1. corestore.replicate(conn) — replicates all hypercores in the corestore
6 * 2. for each mounted module exposing a `.base` (Autobase consumer), also
7 * base.replicate(conn) — belt-and-suspenders against the autobase
8 * replication gotcha (corestore.replicate alone is unreliable in tests
9 * and we don't want to chase that bug in production)
10 * 3. for each mounted module exposing `attachProtocol(mux)` (mux-based
11 * protocols like hyper-overlay), lazily build `Protomux.from(conn)`
12 * once per connection and call attachProtocol on each consumer.
13 * Protomux.from reuses any protomux already attached by corestore/autobase
14 * replication, so channels multiplex cleanly over the same transport.
15 *
16 * No RPC over swarm today — modules communicate either via replication or
17 * via their own mux channels. Adding a new mux-based protocol only requires
18 * exposing `attachProtocol(mux)` on the mounted module.
19 */
20
21import Hyperswarm from 'hyperswarm'
22import Protomux from 'protomux'
23import b4a from 'b4a'
24
25export class SwarmTransport {
26 /**
27 * @param {object} opts
28 * @param {Buffer|string} opts.topic - 32-byte topic to join (hex string or buffer)
29 * @param {object} opts.corestore - corestore to replicate
30 * @param {() => Iterable<{ name: string, instance: object }>} opts.mounts - returns the current mount list (called on each connection so late mounts work)
31 * @param {object} [opts.swarm] - hyperswarm options (e.g., bootstrap nodes for testnet)
32 * @param {boolean} [opts.server=true]
33 * @param {boolean} [opts.client=true]
34 */
35 constructor (opts = {}) {
36 if (!opts.topic) throw new Error('SwarmTransport requires opts.topic')
37 if (!opts.corestore) throw new Error('SwarmTransport requires opts.corestore')
38 if (typeof opts.mounts !== 'function') throw new Error('SwarmTransport requires opts.mounts (function)')
39
40 this._topic = typeof opts.topic === 'string' ? b4a.from(opts.topic, 'hex') : opts.topic
41 if (this._topic.length !== 32) throw new Error('topic must be 32 bytes')
42
43 this._corestore = opts.corestore
44 this._getMounts = opts.mounts
45 this._swarmOpts = opts.swarm || {}
46 this._joinAsServer = opts.server !== false
47 this._joinAsClient = opts.client !== false
48
49 this._swarm = null
50 this._discovery = null
51 this._listening = false
52 this._connections = new Set()
53 }
54
55 get topic () { return this._topic }
56 get listening () { return this._listening }
57 get swarm () { return this._swarm }
58 get connections () { return this._connections.size }
59
60 async listen () {
61 if (this._listening) return
62
63 this._swarm = new Hyperswarm(this._swarmOpts)
64 this._swarm.on('connection', (conn, info) => this._onConnection(conn, info))
65
66 this._discovery = this._swarm.join(this._topic, {
67 server: this._joinAsServer,
68 client: this._joinAsClient
69 })
70 await this._discovery.flushed()
71 this._listening = true
72 }
73
74 _onConnection (conn, info) {
75 this._connections.add(conn)
76 conn.on('close', () => this._connections.delete(conn))
77 conn.on('error', () => { /* connection errors are routine — ignore */ })
78
79 // Always replicate the corestore. Hypercore handles muxing internally.
80 try {
81 this._corestore.replicate(conn)
82 } catch (err) {
83 // Don't crash on replication setup errors — log via stderr
84 // eslint-disable-next-line no-console
85 console.error('[hyper-svc/swarm] corestore.replicate failed:', err.message)
86 }
87
88 // Belt and suspenders: also call base.replicate on every mounted Autobase
89 // consumer. This is the documented workaround for the test-time gotcha
90 // where corestore.replicate alone doesn't propagate writer cores.
91 //
92 // In the same pass, wire up mux-based protocols: any mount exposing
93 // `attachProtocol(mux)` (hyper-overlay and future consumers) gets attached
94 // to a single Protomux per connection. Protomux.from reuses any protomux
95 // corestore/autobase replication already set up.
96 let mux = null
97 for (const { name, instance } of this._getMounts()) {
98 if (!instance) continue
99
100 const base = instance.base
101 if (base && typeof base.replicate === 'function') {
102 try {
103 base.replicate(conn)
104 } catch (err) {
105 // eslint-disable-next-line no-console
106 console.error(`[hyper-svc/swarm] ${name}.base.replicate failed:`, err.message)
107 }
108 }
109
110 if (typeof instance.attachProtocol === 'function') {
111 if (!mux) {
112 try {
113 mux = Protomux.from(conn)
114 } catch (err) {
115 // eslint-disable-next-line no-console
116 console.error('[hyper-svc/swarm] Protomux.from failed:', err.message)
117 }
118 }
119 if (mux) {
120 try {
121 instance.attachProtocol(mux)
122 } catch (err) {
123 // eslint-disable-next-line no-console
124 console.error(`[hyper-svc/swarm] ${name}.attachProtocol failed:`, err.message)
125 }
126 }
127 }
128 }
129 }
130
131 async close () {
132 if (!this._swarm) return
133 for (const conn of this._connections) {
134 try { conn.destroy() } catch {}
135 }
136 this._connections.clear()
137 try { await this._swarm.destroy() } catch {}
138 this._swarm = null
139 this._discovery = null
140 this._listening = false
141 }
142}
lib/transports/uds.js +145 -0
……@@ -0,0 +1,145 @@
1/**
2 * UDS (Unix Domain Socket) transport for hyper-svc.
3 *
4 * Local-only RPC. Trusts the local user. CLIs on the same machine connect
5 * to ~/.local/share/<name>/svc.sock (or wherever the daemon was told to bind)
6 * and exchange length-prefixed JSON frames.
7 *
8 * Wire protocol per connection:
9 * client → server: { id, method, params }
10 * server → client: { id, result } OR { id, error }
11 *
12 * Multiple in-flight requests are matched by id. Server is fully async —
13 * a slow handler doesn't block other requests on the same connection.
14 */
15
16import net from 'net'
17import { unlink, stat } from 'fs/promises'
18import { encodeFrame, FrameDecoder } from '../frame.js'
19
20export class UdsServer {
21 /**
22 * @param {object} opts
23 * @param {string} opts.path - filesystem path for the UNIX socket
24 * @param {(method: string, params: any, ctx: object) => Promise<any>} opts.dispatch - request handler
25 */
26 constructor (opts = {}) {
27 if (!opts.path) throw new Error('UdsServer requires opts.path')
28 if (typeof opts.dispatch !== 'function') throw new Error('UdsServer requires opts.dispatch')
29
30 this._path = opts.path
31 this._dispatch = opts.dispatch
32 this._server = null
33 this._connections = new Set()
34 this._listening = false
35 }
36
37 get path () { return this._path }
38 get listening () { return this._listening }
39
40 async listen () {
41 if (this._listening) return
42
43 // Best-effort cleanup of a stale socket file (server crashed without unlinking).
44 // ONLY remove if it's actually a socket — never touch regular files / dirs.
45 try {
46 const s = await stat(this._path)
47 if (s.isSocket()) {
48 // Try to connect first — if a daemon is already running, refuse to clobber.
49 const inUse = await this._probe()
50 if (inUse) throw new Error(`socket ${this._path} is already in use`)
51 await unlink(this._path)
52 }
53 } catch (err) {
54 if (err.code !== 'ENOENT' && err.message.indexOf('already in use') === -1) {
55 // stat failed for some reason other than missing — ignore, let listen() fail naturally
56 }
57 if (err.message && err.message.indexOf('already in use') !== -1) throw err
58 }
59
60 this._server = net.createServer((sock) => this._onConnection(sock))
61
62 await new Promise((resolve, reject) => {
63 this._server.once('error', reject)
64 this._server.listen(this._path, () => {
65 this._server.removeListener('error', reject)
66 resolve()
67 })
68 })
69
70 this._listening = true
71 }
72
73 _probe () {
74 return new Promise((resolve) => {
75 const sock = net.connect(this._path)
76 sock.once('connect', () => {
77 sock.destroy()
78 resolve(true)
79 })
80 sock.once('error', () => {
81 sock.destroy()
82 resolve(false)
83 })
84 })
85 }
86
87 _onConnection (sock) {
88 this._connections.add(sock)
89 sock.on('close', () => this._connections.delete(sock))
90
91 const decoder = new FrameDecoder((req) => this._handleRequest(sock, req))
92
93 sock.on('data', (chunk) => {
94 try {
95 decoder.push(chunk)
96 } catch (err) {
97 // Bad frame — close the connection rather than try to recover
98 try { sock.destroy(err) } catch {}
99 }
100 })
101
102 sock.on('error', () => { /* swallow — close handler runs anyway */ })
103 }
104
105 async _handleRequest (sock, req) {
106 const id = req && req.id
107 if (typeof id !== 'number' && typeof id !== 'string') {
108 // Malformed — best effort error response, but the client probably can't match it
109 try {
110 sock.write(encodeFrame({ id: null, error: 'request missing id' }))
111 } catch {}
112 return
113 }
114
115 const ctx = {
116 transport: 'uds',
117 identity: { type: 'local', userId: process.env.USER || null, trust: 'local' }
118 }
119
120 try {
121 const result = await this._dispatch(req.method, req.params, ctx)
122 try {
123 sock.write(encodeFrame({ id, result }))
124 } catch {}
125 } catch (err) {
126 try {
127 sock.write(encodeFrame({ id, error: err.message || String(err) }))
128 } catch {}
129 }
130 }
131
132 async close () {
133 if (!this._server) return
134 for (const sock of this._connections) {
135 try { sock.destroy() } catch {}
136 }
137 this._connections.clear()
138 await new Promise((resolve) => this._server.close(() => resolve()))
139 this._server = null
140 this._listening = false
141
142 // Clean up the socket file (best-effort)
143 try { await unlink(this._path) } catch {}
144 }
145}
package-lock.json +1592 -0
……@@ -0,0 +1,1592 @@
1{
2 "name": "hyper-svc",
3 "version": "0.1.0",
4 "lockfileVersion": 3,
5 "requires": true,
6 "packages": {
7 "": {
8 "name": "hyper-svc",
9 "version": "0.1.0",
10 "dependencies": {
11 "b4a": "^1.6.0",
12 "hyperswarm": "^4.0.0",
13 "ready-resource": "^1.0.0"
14 },
15 "devDependencies": {
16 "@hyperswarm/testnet": "^3.0.0",
17 "brittle": "^3.0.0",
18 "corestore": "^7.0.0",
19 "hyper-rooms": "file:../hyper-rooms"
20 }
21 },
22 "../hyper-rooms": {
23 "version": "0.1.0",
24 "dev": true,
25 "dependencies": {
26 "autobase": "^7.20.0",
27 "b4a": "^1.6.0",
28 "hyperbee": "^2.20.0",
29 "hypercore-crypto": "^3.4.0",
30 "ready-resource": "^1.1.0"
31 },
32 "devDependencies": {
33 "autobase-test-helpers": "^2.0.0",
34 "brittle": "^3.0.0",
35 "corestore": "^7.0.0"
36 }
37 },
38 "node_modules/@hyperswarm/secret-stream": {
39 "version": "6.9.1",
40 "resolved": "https://registry.npmjs.org/@hyperswarm/secret-stream/-/secret-stream-6.9.1.tgz",
41 "integrity": "sha512-xb0S5y3YJwBakD77JOGBHlBxdp63mHClZoXBYoLv+9wH8e054ESKlmQptWqjJK5dv5VMUIVYOJB4MaOpB0JdGw==",
42 "license": "Apache-2.0",
43 "dependencies": {
44 "b4a": "^1.1.0",
45 "hypercore-crypto": "^3.3.1",
46 "noise-curve-ed": "^2.0.1",
47 "noise-handshake": "^4.0.0",
48 "sodium-secretstream": "^1.1.0",
49 "sodium-universal": "^5.0.0",
50 "streamx": "^2.14.0",
51 "timeout-refresh": "^2.0.0",
52 "unslab": "^1.3.0"
53 }
54 },
55 "node_modules/@hyperswarm/testnet": {
56 "version": "3.1.4",
57 "resolved": "https://registry.npmjs.org/@hyperswarm/testnet/-/testnet-3.1.4.tgz",
58 "integrity": "sha512-Jw8JIZ9kdZ4R9jwJHsqctj5YeRqecw3aeCg2NS12HBXCqMioQIYzVqHs/8BWdOQCf34+BEXkwMQjba9CzaLwkg==",
59 "dev": true,
60 "license": "Apache-2.0",
61 "dependencies": {
62 "hyperdht": "^6.6.0"
63 },
64 "bin": {
65 "hyperswarm-testnet": "bin.js"
66 }
67 },
68 "node_modules/@jridgewell/resolve-uri": {
69 "version": "3.1.2",
70 "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
71 "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
72 "dev": true,
73 "license": "MIT",
74 "engines": {
75 "node": ">=6.0.0"
76 }
77 },
78 "node_modules/@jridgewell/sourcemap-codec": {
79 "version": "1.5.5",
80 "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
81 "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
82 "dev": true,
83 "license": "MIT"
84 },
85 "node_modules/@jridgewell/trace-mapping": {
86 "version": "0.3.31",
87 "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
88 "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
89 "dev": true,
90 "license": "MIT",
91 "dependencies": {
92 "@jridgewell/resolve-uri": "^3.1.0",
93 "@jridgewell/sourcemap-codec": "^1.4.14"
94 }
95 },
96 "node_modules/@types/istanbul-lib-coverage": {
97 "version": "2.0.6",
98 "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
99 "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
100 "dev": true,
101 "license": "MIT"
102 },
103 "node_modules/adaptive-timeout": {
104 "version": "1.0.1",
105 "resolved": "https://registry.npmjs.org/adaptive-timeout/-/adaptive-timeout-1.0.1.tgz",
106 "integrity": "sha512-+seGiBtHqbnyZ0Quq/ZGxxwP66153WBABawa07/QeyuPFzbduPBjiGQAyCiriiLDzt3dkbMBskSlfqtRwblK8Q==",
107 "license": "Apache-2.0",
108 "dependencies": {
109 "xache": "^1.2.1"
110 }
111 },
112 "node_modules/b4a": {
113 "version": "1.8.0",
114 "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz",
115 "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==",
116 "license": "Apache-2.0",
117 "peerDependencies": {
118 "react-native-b4a": "*"
119 },
120 "peerDependenciesMeta": {
121 "react-native-b4a": {
122 "optional": true
123 }
124 }
125 },
126 "node_modules/bare-abort": {
127 "version": "2.0.13",
128 "resolved": "https://registry.npmjs.org/bare-abort/-/bare-abort-2.0.13.tgz",
129 "integrity": "sha512-zdc8l88eB11Jsz5rDd6sCAgv2kUFXgdrZWoMlgU6JMkfAi1/uuGFC3IEHswKbIRQTk5H3T5CMuechsXYxiaHlQ==",
130 "dev": true,
131 "license": "Apache-2.0"
132 },
133 "node_modules/bare-addon-resolve": {
134 "version": "1.10.0",
135 "resolved": "https://registry.npmjs.org/bare-addon-resolve/-/bare-addon-resolve-1.10.0.tgz",
136 "integrity": "sha512-sSd0jieRJlDaODOzj0oe0RjFVC1QI0ZIjGIdPkbrTXsdVVtENg14c+lHHAhHwmWCZ2nQlMhy8jA3Y5LYPc/isA==",
137 "license": "Apache-2.0",
138 "dependencies": {
139 "bare-module-resolve": "^1.10.0",
140 "bare-semver": "^1.0.0"
141 },
142 "peerDependencies": {
143 "bare-url": "*"
144 },
145 "peerDependenciesMeta": {
146 "bare-url": {
147 "optional": true
148 }
149 }
150 },
151 "node_modules/bare-ansi-escapes": {
152 "version": "2.2.3",
153 "resolved": "https://registry.npmjs.org/bare-ansi-escapes/-/bare-ansi-escapes-2.2.3.tgz",
154 "integrity": "sha512-02ES4/E2RbrtZSnHJ9LntBhYkLA6lPpSEeP8iqS3MccBIVhVBlEmruF1I7HZqx5Q8aiTeYfQVeqmrU9YO2yYoQ==",
155 "license": "Apache-2.0",
156 "dependencies": {
157 "bare-stream": "^2.6.5"
158 },
159 "peerDependencies": {
160 "bare-buffer": "*"
161 },
162 "peerDependenciesMeta": {
163 "bare-buffer": {
164 "optional": true
165 }
166 }
167 },
168 "node_modules/bare-assert": {
169 "version": "1.2.0",
170 "resolved": "https://registry.npmjs.org/bare-assert/-/bare-assert-1.2.0.tgz",
171 "integrity": "sha512-c6uvgvTJBspTDxtVnPgrBKmLgcpW3Fp72NVKDLg6oT4QjQbhGtvrkHMhGYMK1sh4vjBHOBmuUalyt9hSzV37fQ==",
172 "license": "Apache-2.0",
173 "dependencies": {
174 "bare-inspect": "^3.1.2"
175 }
176 },
177 "node_modules/bare-bundle": {
178 "version": "1.10.0",
179 "resolved": "https://registry.npmjs.org/bare-bundle/-/bare-bundle-1.10.0.tgz",
180 "integrity": "sha512-4LVlnJAHr00Hh6Vu6ZUJS38rcEtJT3b3vChXSsBsJ2mk1TN0lQ+gzd+Dw5L0aV7uqDZv84smuwW+O02X7PfDlw==",
181 "dev": true,
182 "license": "Apache-2.0",
183 "peerDependencies": {
184 "bare-buffer": "*",
185 "bare-url": "*"
186 },
187 "peerDependenciesMeta": {
188 "bare-buffer": {
189 "optional": true
190 },
191 "bare-url": {
192 "optional": true
193 }
194 }
195 },
196 "node_modules/bare-cov": {
197 "version": "1.2.1",
198 "resolved": "https://registry.npmjs.org/bare-cov/-/bare-cov-1.2.1.tgz",
199 "integrity": "sha512-SHNqb/WG9udUEj8m5qGSehoUQ9o0Xzv8nrf4HIdmG+KNU/XdhPLhTP5VPrHnSAMAXhAzEB4Q+2IYZ0x9qPzN1g==",
200 "dev": true,
201 "license": "Apache-2.0",
202 "dependencies": {
203 "bare-fs": "^4.1.2",
204 "bare-inspector": "^4.0.1",
205 "bare-path": "^3.0.0",
206 "bare-process": "^4.2.1",
207 "bare-url": "^2.1.5",
208 "bare-v8-to-istanbul": "^1.0.2",
209 "picomatch": "^4.0.2",
210 "which-runtime": "^1.2.1"
211 }
212 },
213 "node_modules/bare-crypto": {
214 "version": "1.13.4",
215 "resolved": "https://registry.npmjs.org/bare-crypto/-/bare-crypto-1.13.4.tgz",
216 "integrity": "sha512-JiCZ5l2YOG1y8J7yy1BCAKTCZrPnHLb7pDRIdurBTOn5oIwBQDIv8iH5Pl2V85vzjl1NZXRfNY4HZLsE942jJA==",
217 "dev": true,
218 "license": "Apache-2.0",
219 "dependencies": {
220 "bare-assert": "^1.2.0",
221 "bare-stream": "^2.6.3"
222 },
223 "peerDependencies": {
224 "bare-buffer": "*"
225 },
226 "peerDependenciesMeta": {
227 "bare-buffer": {
228 "optional": true
229 }
230 }
231 },
232 "node_modules/bare-debug-log": {
233 "version": "2.0.0",
234 "resolved": "https://registry.npmjs.org/bare-debug-log/-/bare-debug-log-2.0.0.tgz",
235 "integrity": "sha512-Vi42PkMQsNV9PUpx2Gl1hikshx5O9FzMJ6o9Nnopseg7qLBBK7Nl31d0RHcfwLEAfmcPApytpc0ZFfq68u22FQ==",
236 "dev": true,
237 "license": "Apache-2.0",
238 "dependencies": {
239 "bare-os": "^3.0.1"
240 }
241 },
242 "node_modules/bare-dns": {
243 "version": "2.1.4",
244 "resolved": "https://registry.npmjs.org/bare-dns/-/bare-dns-2.1.4.tgz",
245 "integrity": "sha512-abwjHmpWqSRNB7V5615QxPH92L71AVzFm/kKTs8VYiNTAi2xVdonpv0BjJ0hwXLwomoW+xsSOPjW6PZPO14asg==",
246 "dev": true,
247 "license": "Apache-2.0",
248 "engines": {
249 "bare": ">=1.7.0"
250 }
251 },
252 "node_modules/bare-encoding": {
253 "version": "1.0.3",
254 "resolved": "https://registry.npmjs.org/bare-encoding/-/bare-encoding-1.0.3.tgz",
255 "integrity": "sha512-Kqf+t/azs13lUeyK4Tb7ha4wdLRXKWCXQ8w1rVmt7KtoPCPdHD/Xwt7LBIsCSwwGglrcmblo5VOLa5avkJqULA==",
256 "dev": true,
257 "license": "Apache-2.0",
258 "peerDependencies": {
259 "bare-buffer": "*"
260 },
261 "peerDependenciesMeta": {
262 "bare-buffer": {
263 "optional": true
264 }
265 }
266 },
267 "node_modules/bare-env": {
268 "version": "3.0.0",
269 "resolved": "https://registry.npmjs.org/bare-env/-/bare-env-3.0.0.tgz",
270 "integrity": "sha512-0u964P5ZLAxTi+lW4Kjp7YRJQ5gZr9ycYOtjLxsSrupgMz3sn5Z9n4SH/JIifHwvadsf1brA2JAjP+9IOWwTiw==",
271 "dev": true,
272 "license": "Apache-2.0",
273 "dependencies": {
274 "bare-os": "^3.0.1"
275 }
276 },
277 "node_modules/bare-events": {
278 "version": "2.8.2",
279 "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
280 "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
281 "license": "Apache-2.0",
282 "peerDependencies": {
283 "bare-abort-controller": "*"
284 },
285 "peerDependenciesMeta": {
286 "bare-abort-controller": {
287 "optional": true
288 }
289 }
290 },
291 "node_modules/bare-format": {
292 "version": "1.0.2",
293 "resolved": "https://registry.npmjs.org/bare-format/-/bare-format-1.0.2.tgz",
294 "integrity": "sha512-GswdhnOnP9QtwRbrf4wLApw5widkaLMsLe2XOs35fQD2YfEN1ApoGka+cZ7PfvzxMgfYXmMhj/2OGlVn5/Dxgw==",
295 "dev": true,
296 "license": "Apache-2.0",
297 "dependencies": {
298 "bare-inspect": "^3.0.0"
299 }
300 },
301 "node_modules/bare-fs": {
302 "version": "4.7.0",
303 "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.0.tgz",
304 "integrity": "sha512-xzqKsCFxAek9aezYhjJuJRXBIaYlg/0OGDTZp+T8eYmYMlm66cs6cYko02drIyjN2CBbi+I6L7YfXyqpqtKRXA==",
305 "dev": true,
306 "license": "Apache-2.0",
307 "dependencies": {
308 "bare-events": "^2.5.4",
309 "bare-path": "^3.0.0",
310 "bare-stream": "^2.6.4",
311 "bare-url": "^2.2.2",
312 "fast-fifo": "^1.3.2"
313 },
314 "engines": {
315 "bare": ">=1.16.0"
316 },
317 "peerDependencies": {
318 "bare-buffer": "*"
319 },
320 "peerDependenciesMeta": {
321 "bare-buffer": {
322 "optional": true
323 }
324 }
325 },
326 "node_modules/bare-hrtime": {
327 "version": "2.1.1",
328 "resolved": "https://registry.npmjs.org/bare-hrtime/-/bare-hrtime-2.1.1.tgz",
329 "integrity": "sha512-VMb3tHo05gsnbu3OXTmkDiwTjMlOsbQmKoysKqKEyR09m77TuDrYFbj3Q5GGk10dAKsUHrnXmwCaeJqzVpB5ZA==",
330 "dev": true,
331 "license": "Apache-2.0"
332 },
333 "node_modules/bare-http-parser": {
334 "version": "1.1.3",
335 "resolved": "https://registry.npmjs.org/bare-http-parser/-/bare-http-parser-1.1.3.tgz",
336 "integrity": "sha512-+dhVvQi6brHq14L/XHNRQ+TLuVE76VjRmMt61wVEtS+Od8xUslfMHWJN/ZjIIt3RtTG6vPuA+x9cOh7KrkBJsA==",
337 "dev": true,
338 "license": "Apache-2.0"
339 },
340 "node_modules/bare-http1": {
341 "version": "4.5.6",
342 "resolved": "https://registry.npmjs.org/bare-http1/-/bare-http1-4.5.6.tgz",
343 "integrity": "sha512-31OAwMkSU+z1VuUOCk65hx3aWQgzCfH/zQ6LGxbJtmiy2Czsw0+uvOBM9YkqaL6zUSTSYG2pLbL0v/TjME3Buw==",
344 "dev": true,
345 "license": "Apache-2.0",
346 "dependencies": {
347 "bare-events": "^2.6.0",
348 "bare-http-parser": "^1.1.1",
349 "bare-stream": "^2.10.0",
350 "bare-tcp": "^2.2.0"
351 },
352 "peerDependencies": {
353 "bare-buffer": "*",
354 "bare-url": "*"
355 },
356 "peerDependenciesMeta": {
357 "bare-buffer": {
358 "optional": true
359 },
360 "bare-url": {
361 "optional": true
362 }
363 }
364 },
365 "node_modules/bare-https": {
366 "version": "2.1.3",
367 "resolved": "https://registry.npmjs.org/bare-https/-/bare-https-2.1.3.tgz",
368 "integrity": "sha512-0TI/mJXQGYXmG7UUyWEG+KCJusayIAQLywUjFAskDoKuxfqVnGF+M/mTMrEV8J64DaIdU+5x761FvgmT7M68tA==",
369 "dev": true,
370 "license": "Apache-2.0",
371 "dependencies": {
372 "bare-http1": "^4.4.0",
373 "bare-tcp": "^2.2.0",
374 "bare-tls": "^2.0.0"
375 }
376 },
377 "node_modules/bare-inspect": {
378 "version": "3.1.4",
379 "resolved": "https://registry.npmjs.org/bare-inspect/-/bare-inspect-3.1.4.tgz",
380 "integrity": "sha512-jfW5KRA84o3REpI6Vr4nbvMn+hqVAw8GU1mMdRwUsY5yJovQamxYeKGVKGqdzs+8ZbG4jRzGUXP/3Ji/DnqfPg==",
381 "license": "Apache-2.0",
382 "dependencies": {
383 "bare-ansi-escapes": "^2.1.0",
384 "bare-type": "^1.0.0"
385 },
386 "engines": {
387 "bare": ">=1.18.0"
388 }
389 },
390 "node_modules/bare-inspector": {
391 "version": "4.0.3",
392 "resolved": "https://registry.npmjs.org/bare-inspector/-/bare-inspector-4.0.3.tgz",
393 "integrity": "sha512-yYk6+7tTVKJKHOja5+QBn989ETv8oVsmwoH0nCM+QYC6w5MC03GC4aQNK+lipAZSz1EaAj42WZ8Wewjpy0L9TA==",
394 "dev": true,
395 "license": "Apache-2.0",
396 "dependencies": {
397 "bare-events": "^2.1.0",
398 "bare-http1": "^4.0.0",
399 "bare-stream": "^2.0.0",
400 "bare-url": "^2.0.0",
401 "bare-ws": "^2.0.0"
402 },
403 "engines": {
404 "bare": ">=1.2.0"
405 },
406 "peerDependencies": {
407 "bare-tcp": "*"
408 },
409 "peerDependenciesMeta": {
410 "bare-tcp": {
411 "optional": true
412 }
413 }
414 },
415 "node_modules/bare-module": {
416 "version": "6.1.3",
417 "resolved": "https://registry.npmjs.org/bare-module/-/bare-module-6.1.3.tgz",
418 "integrity": "sha512-5XWsVHsvtWMH4tK4DQWgpNTV0t/sg3ZrAaQLIxrwjrS5+u8Q9vEgc/zQ4QaDPWDse/y/5h+d+YG1Q0JfSMt0zA==",
419 "dev": true,
420 "license": "Apache-2.0",
421 "dependencies": {
422 "bare-bundle": "^1.3.0",
423 "bare-module-lexer": "^1.0.0",
424 "bare-module-resolve": "^1.8.0",
425 "bare-path": "^3.0.0",
426 "bare-url": "^2.0.1"
427 },
428 "engines": {
429 "bare": ">=1.23.0"
430 },
431 "peerDependencies": {
432 "bare-buffer": "*"
433 },
434 "peerDependenciesMeta": {
435 "bare-buffer": {
436 "optional": true
437 }
438 }
439 },
440 "node_modules/bare-module-lexer": {
441 "version": "1.4.7",
442 "resolved": "https://registry.npmjs.org/bare-module-lexer/-/bare-module-lexer-1.4.7.tgz",
443 "integrity": "sha512-0klU4eMsjh/wcxi8FdHmNom2j2F4kmkXOhyJFL9qTaSFp2lE3m6BtbKgMHY8R5miqC9r8/IfA8wzXnC5Os14WA==",
444 "dev": true,
445 "license": "Apache-2.0",
446 "dependencies": {
447 "require-addon": "^1.0.2"
448 },
449 "peerDependencies": {
450 "bare-buffer": "*"
451 },
452 "peerDependenciesMeta": {
453 "bare-buffer": {
454 "optional": true
455 }
456 }
457 },
458 "node_modules/bare-module-resolve": {
459 "version": "1.12.1",
460 "resolved": "https://registry.npmjs.org/bare-module-resolve/-/bare-module-resolve-1.12.1.tgz",
461 "integrity": "sha512-hbmAPyFpEq8FoZMd5sFO3u6MC5feluWoGE8YKlA8fCrl6mNtx68Wjg4DTiDJcqRJaovTvOYKfYngoBUnbaT7eg==",
462 "license": "Apache-2.0",
463 "dependencies": {
464 "bare-semver": "^1.0.0"
465 },
466 "peerDependencies": {
467 "bare-url": "*"
468 },
469 "peerDependenciesMeta": {
470 "bare-url": {
471 "optional": true
472 }
473 }
474 },
475 "node_modules/bare-net": {
476 "version": "2.3.1",
477 "resolved": "https://registry.npmjs.org/bare-net/-/bare-net-2.3.1.tgz",
478 "integrity": "sha512-MypSqDKpDU2Xt7FIfazn5yGvRnV09gFcIPHGWstW0gxuzA4tucTcwJSZeos97C4F89vtU5oGwXDN/HrGN6Y4Jw==",
479 "dev": true,
480 "license": "Apache-2.0",
481 "dependencies": {
482 "bare-events": "^2.2.2",
483 "bare-pipe": "^4.0.0",
484 "bare-stream": "^2.0.0",
485 "bare-tcp": "^2.0.0"
486 }
487 },
488 "node_modules/bare-os": {
489 "version": "3.8.7",
490 "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.8.7.tgz",
491 "integrity": "sha512-G4Gr1UsGeEy2qtDTZwL7JFLo2wapUarz7iTMcYcMFdS89AIQuBoyjgXZz0Utv7uHs3xA9LckhVbeBi8lEQrC+w==",
492 "devOptional": true,
493 "license": "Apache-2.0",
494 "engines": {
495 "bare": ">=1.14.0"
496 }
497 },
498 "node_modules/bare-path": {
499 "version": "3.0.0",
500 "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
501 "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
502 "devOptional": true,
503 "license": "Apache-2.0",
504 "dependencies": {
505 "bare-os": "^3.0.1"
506 }
507 },
508 "node_modules/bare-pipe": {
509 "version": "4.1.5",
510 "resolved": "https://registry.npmjs.org/bare-pipe/-/bare-pipe-4.1.5.tgz",
511 "integrity": "sha512-6OfxaG8JSkRh3Gc4hzHRsxNt+yu2PpN7lrv1V+T78GdknWQkVGwiEvu4m+1nbfk8cMVQ0TGxRvQ90XA4rhnTuw==",
512 "dev": true,
513 "license": "Apache-2.0",
514 "dependencies": {
515 "bare-events": "^2.0.0",
516 "bare-stream": "^2.0.0"
517 },
518 "engines": {
519 "bare": ">=1.16.0"
520 }
521 },
522 "node_modules/bare-process": {
523 "version": "4.4.1",
524 "resolved": "https://registry.npmjs.org/bare-process/-/bare-process-4.4.1.tgz",
525 "integrity": "sha512-JfcTtymq6akqM2bdyTsP4A4GYJceBzb91k/ECmFps75uFf6uO33SM1bOZsRAqyRXExabsQJLn5S41NBl8HTM4A==",
526 "dev": true,
527 "license": "Apache-2.0",
528 "dependencies": {
529 "bare-abort": "^2.0.13",
530 "bare-env": "^3.0.0",
531 "bare-events": "^2.3.1",
532 "bare-hrtime": "^2.0.0",
533 "bare-os": "^3.7.1",
534 "bare-signals": "^4.0.0",
535 "bare-stdio": "^1.0.1"
536 }
537 },
538 "node_modules/bare-semver": {
539 "version": "1.0.3",
540 "resolved": "https://registry.npmjs.org/bare-semver/-/bare-semver-1.0.3.tgz",
541 "integrity": "sha512-HS/A30bi2+PiRJfU6R4+Kp+6KeLSCSByjYM2iiobOKzLAvtu1CT+S8xWfiU7wz0erknjkUoC+yXy108tzIuP5Q==",
542 "license": "Apache-2.0"
543 },
544 "node_modules/bare-signals": {
545 "version": "4.2.0",
546 "resolved": "https://registry.npmjs.org/bare-signals/-/bare-signals-4.2.0.tgz",
547 "integrity": "sha512-fNHMOdQIlYuTvMB3Oh9Apk99hLKn351+Ir8vz+khiPTcOqIyGG4uWWjdLTzxWdYGsA0eT+We3y0K74hjj2nq7A==",
548 "dev": true,
549 "license": "Apache-2.0",
550 "dependencies": {
551 "bare-events": "^2.5.3",
552 "bare-os": "^3.3.1"
553 },
554 "engines": {
555 "bare": ">=1.7.0"
556 }
557 },
558 "node_modules/bare-stdio": {
559 "version": "1.0.2",
560 "resolved": "https://registry.npmjs.org/bare-stdio/-/bare-stdio-1.0.2.tgz",
561 "integrity": "sha512-3WJDqtvVGP4f+j68kyEC05umOYNwKJ1xG+YAXL8yZ605WgNqiRhVaFq+mVIhBt2eKNp7pa5vCQdhOt1pNh79SA==",
562 "dev": true,
563 "license": "Apache-2.0",
564 "dependencies": {
565 "bare-fs": "^4.5.2",
566 "bare-pipe": "^4.1.5",
567 "bare-tty": "^5.0.3"
568 }
569 },
570 "node_modules/bare-stream": {
571 "version": "2.13.0",
572 "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.0.tgz",
573 "integrity": "sha512-3zAJRZMDFGjdn+RVnNpF9kuELw+0Fl3lpndM4NcEOhb9zwtSo/deETfuIwMSE5BXanA0FrN1qVjffGwAg2Y7EA==",
574 "license": "Apache-2.0",
575 "dependencies": {
576 "streamx": "^2.25.0",
577 "teex": "^1.0.1"
578 },
579 "peerDependencies": {
580 "bare-abort-controller": "*",
581 "bare-buffer": "*",
582 "bare-events": "*"
583 },
584 "peerDependenciesMeta": {
585 "bare-abort-controller": {
586 "optional": true
587 },
588 "bare-buffer": {
589 "optional": true
590 },
591 "bare-events": {
592 "optional": true
593 }
594 }
595 },
596 "node_modules/bare-stylize": {
597 "version": "0.0.1",
598 "resolved": "https://registry.npmjs.org/bare-stylize/-/bare-stylize-0.0.1.tgz",
599 "integrity": "sha512-l3MjmIl476bWijYWf3RbE+osl4iuXSOMudzp0vAqzIK7gPgn/+G3oAxp8Oin9CFF911KBP0LO9kts8Ci8mGZaQ==",
600 "dev": true,
601 "license": "Apache-2.0",
602 "dependencies": {
603 "bare-ansi-escapes": "^2.2.3",
604 "bare-process": "^4.2.1"
605 }
606 },
607 "node_modules/bare-subprocess": {
608 "version": "5.2.3",
609 "resolved": "https://registry.npmjs.org/bare-subprocess/-/bare-subprocess-5.2.3.tgz",
610 "integrity": "sha512-07wwswlV7M3sC9IykbZRZ/jHAkrXFWVLqdBWGv1y0ojCimtRD9hGwxdHmR5FUFmDUZLNsBmTYJNQqgio5+A85Q==",
611 "dev": true,
612 "license": "Apache-2.0",
613 "dependencies": {
614 "bare-env": "^3.0.0",
615 "bare-events": "^2.5.4",
616 "bare-os": "^3.0.1",
617 "bare-pipe": "^4.0.0",
618 "bare-url": "^2.2.2"
619 },
620 "engines": {
621 "bare": ">=1.7.0"
622 },
623 "peerDependencies": {
624 "bare-buffer": "*"
625 },
626 "peerDependenciesMeta": {
627 "bare-buffer": {
628 "optional": true
629 }
630 }
631 },
632 "node_modules/bare-tcp": {
633 "version": "2.2.7",
634 "resolved": "https://registry.npmjs.org/bare-tcp/-/bare-tcp-2.2.7.tgz",
635 "integrity": "sha512-rjpqNQ2cOCkNo3NeYA/W4GTK3DRkl8sDHO3uos+AEswUjLC8XXMQF8WrJCSjlIowCbS6NUVxKE92X5RGXjyefg==",
636 "dev": true,
637 "license": "Apache-2.0",
638 "dependencies": {
639 "bare-dns": "^2.0.4",
640 "bare-events": "^2.5.4",
641 "bare-stream": "^2.6.4"
642 },
643 "engines": {
644 "bare": ">=1.16.0"
645 }
646 },
647 "node_modules/bare-tls": {
648 "version": "2.2.3",
649 "resolved": "https://registry.npmjs.org/bare-tls/-/bare-tls-2.2.3.tgz",
650 "integrity": "sha512-dPYBGEXtgLceRFMfGaF2/rqR86/xMxMyrM9ootO/gaRKL/z2uNHJs7aP7IOBtnJF8eUCt5qwMzbKfrKgDIxPLg==",
651 "dev": true,
652 "license": "Apache-2.0",
653 "dependencies": {
654 "bare-net": "^2.0.1",
655 "bare-stream": "^2.6.4"
656 },
657 "engines": {
658 "bare": ">=1.7.0"
659 }
660 },
661 "node_modules/bare-tty": {
662 "version": "5.1.0",
663 "resolved": "https://registry.npmjs.org/bare-tty/-/bare-tty-5.1.0.tgz",
664 "integrity": "sha512-EZLvW4A+XiJgI3TW+e1pMME9PIJsfEXe/DA/WSKzIkq/v7Yarpv/rvG6Z5pGnpo4V/Bd+qopwnCLSR71hMMBYA==",
665 "dev": true,
666 "license": "Apache-2.0",
667 "dependencies": {
668 "bare-events": "^2.2.0",
669 "bare-signals": "^4.0.0",
670 "bare-stream": "^2.0.0"
671 },
672 "engines": {
673 "bare": ">=1.16.0"
674 }
675 },
676 "node_modules/bare-type": {
677 "version": "1.1.0",
678 "resolved": "https://registry.npmjs.org/bare-type/-/bare-type-1.1.0.tgz",
679 "integrity": "sha512-LdtnnEEYldOc87Dr4GpsKnStStZk3zfgoEMXy8yvEZkXrcCv9RtYDrUYWFsBQHtaB0s1EUWmcvS6XmEZYIj3Bw==",
680 "license": "Apache-2.0",
681 "engines": {
682 "bare": ">=1.2.0"
683 }
684 },
685 "node_modules/bare-url": {
686 "version": "2.4.0",
687 "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.0.tgz",
688 "integrity": "sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA==",
689 "devOptional": true,
690 "license": "Apache-2.0",
691 "peer": true,
692 "dependencies": {
693 "bare-path": "^3.0.0"
694 }
695 },
696 "node_modules/bare-utils": {
697 "version": "1.6.0",
698 "resolved": "https://registry.npmjs.org/bare-utils/-/bare-utils-1.6.0.tgz",
699 "integrity": "sha512-WhQEIkkAxkSnW7u1QgrI0AfNm5JpMruETXeYsb5qnkBJ0TTfNKygZmsh6rkoHBANaV+C/7Jed7bJP9OmEHG7rQ==",
700 "dev": true,
701 "license": "Apache-2.0",
702 "dependencies": {
703 "bare-debug-log": "^2.0.0",
704 "bare-encoding": "^1.0.0",
705 "bare-format": "^1.0.0",
706 "bare-inspect": "^3.0.0",
707 "bare-stylize": "^0.0.1",
708 "bare-type": "^1.0.6"
709 }
710 },
711 "node_modules/bare-v8-to-istanbul": {
712 "version": "1.0.2",
713 "resolved": "https://registry.npmjs.org/bare-v8-to-istanbul/-/bare-v8-to-istanbul-1.0.2.tgz",
714 "integrity": "sha512-CM0wPgyQtXTQ7h4q8NjbTZ/FH5he0Jkiwgpq0lFZZA5tkVZzmQ/9ul4R7E+cNlGZJIZlQZ9v4CwbgVaFG3zOAA==",
715 "dev": true,
716 "license": "Apache-2.0",
717 "dependencies": {
718 "bare-assert": "^1.0.2",
719 "bare-fs": "^4.1.2",
720 "bare-module": "^6.1.2",
721 "bare-path": "^3.0.0",
722 "bare-process": "^4.2.0",
723 "bare-url": "^2.1.5",
724 "bare-utils": "^1.2.0",
725 "v8-to-istanbul": "^9.3.0",
726 "which-runtime": "^1.2.1"
727 }
728 },
729 "node_modules/bare-ws": {
730 "version": "2.1.0",
731 "resolved": "https://registry.npmjs.org/bare-ws/-/bare-ws-2.1.0.tgz",
732 "integrity": "sha512-2gEWlPK9iyBchACdIY6oQXgmDz3KLrChdwrPgmU3IVXOFTnxTqSUT27WE/+izd4QojHj/SsqDkQiD2HDvuTdAA==",
733 "dev": true,
734 "license": "Apache-2.0",
735 "dependencies": {
736 "bare-crypto": "^1.2.0",
737 "bare-events": "^2.3.1",
738 "bare-http1": "^4.0.0",
739 "bare-https": "^2.0.0",
740 "bare-stream": "^2.1.2"
741 },
742 "peerDependencies": {
743 "bare-buffer": "*",
744 "bare-url": "*"
745 },
746 "peerDependenciesMeta": {
747 "bare-buffer": {
748 "optional": true
749 },
750 "bare-url": {
751 "optional": true
752 }
753 }
754 },
755 "node_modules/big-sparse-array": {
756 "version": "1.0.3",
757 "resolved": "https://registry.npmjs.org/big-sparse-array/-/big-sparse-array-1.0.3.tgz",
758 "integrity": "sha512-6RjV/3mSZORlMdpUaQ6rUSpG637cZm0//E54YYGtQg1c1O+AbZP8UTdJ/TchsDZcTVLmyWZcseBfp2HBeXUXOQ==",
759 "dev": true,
760 "license": "MIT"
761 },
762 "node_modules/bits-to-bytes": {
763 "version": "1.3.0",
764 "resolved": "https://registry.npmjs.org/bits-to-bytes/-/bits-to-bytes-1.3.0.tgz",
765 "integrity": "sha512-OJoHTpFXS9bXHBCekGTByf3MqM8CGblBDIduKQeeVVeiU9dDWywSSirXIBYGgg3d1zbVuvnMa1vD4r6PA0kOKg==",
766 "license": "ISC",
767 "dependencies": {
768 "b4a": "^1.5.0"
769 }
770 },
771 "node_modules/blind-relay": {
772 "version": "1.4.0",
773 "resolved": "https://registry.npmjs.org/blind-relay/-/blind-relay-1.4.0.tgz",
774 "integrity": "sha512-6xt7fDfCs6eGmNNym6I9N42jmjcMQn2qwwOVnkP9ZnrkXFk6c4/tdO1xqRmDEzKzV8gigd+DVdCUG/RUYnen7Q==",
775 "license": "Apache-2.0",
776 "dependencies": {
777 "b4a": "^1.6.4",
778 "bare-events": "^2.2.0",
779 "bits-to-bytes": "^1.3.0",
780 "compact-encoding": "^2.12.0",
781 "compact-encoding-bitfield": "^1.0.0",
782 "protomux": "^3.5.1",
783 "sodium-universal": "^5.0.0",
784 "streamx": "^2.15.1"
785 }
786 },
787 "node_modules/bogon": {
788 "version": "1.2.0",
789 "resolved": "https://registry.npmjs.org/bogon/-/bogon-1.2.0.tgz",
790 "integrity": "sha512-FqOBr/1VMzCOsoJd+fzNUMarUYki2+TKt07A2+xaulsNx4r53iJ7MV5k0jbqg7W2U0CsLqxCZOrFibdG6h6HSg==",
791 "license": "MIT",
792 "dependencies": {
793 "compact-encoding": "^2.11.0",
794 "compact-encoding-net": "^1.2.0"
795 }
796 },
797 "node_modules/brittle": {
798 "version": "3.19.1",
799 "resolved": "https://registry.npmjs.org/brittle/-/brittle-3.19.1.tgz",
800 "integrity": "sha512-4Ted1Mt9o9B6oIA6ImJJCtB/Fv++ZO3IDNQgRcHOXWSYGRUidgxchaGrUBaRn+4mlneXrgInPkCPzi0jO8qKbA==",
801 "dev": true,
802 "license": "Apache-2.0",
803 "dependencies": {
804 "b4a": "^1.6.0",
805 "bare-assert": "^1.0.2",
806 "bare-cov": "^1.1.0",
807 "bare-fs": "^4.1.6",
808 "bare-os": "^3.6.1",
809 "bare-path": "^3.0.0",
810 "bare-process": "^4.2.1",
811 "bare-subprocess": "^5.0.0",
812 "bare-url": "^2.1.6",
813 "error-stack-parser": "^2.1.4",
814 "globbie": "^1.0.2",
815 "paparam": "^1.6.2",
816 "same-object": "^1.0.2",
817 "test-tmp": "^1.4.0",
818 "tmatch": "^5.0.0"
819 },
820 "bin": {
821 "brittle": "bin/node.js",
822 "brittle-bare": "bin/bare.js",
823 "brittle-node": "bin/node.js",
824 "brittle-pear": "bin/pear.js"
825 }
826 },
827 "node_modules/compact-encoding": {
828 "version": "2.19.2",
829 "resolved": "https://registry.npmjs.org/compact-encoding/-/compact-encoding-2.19.2.tgz",
830 "integrity": "sha512-/YjhHQE/5L4F7l5Bht69dRbP9RV6zoJPeowi8bMKQxNKe3Nh6hOY8pBGoVE9fz5GaWfEd8fWJ2aU9sB4KZuMYg==",
831 "license": "Apache-2.0",
832 "dependencies": {
833 "b4a": "^1.3.0"
834 }
835 },
836 "node_modules/compact-encoding-bitfield": {
837 "version": "1.0.0",
838 "resolved": "https://registry.npmjs.org/compact-encoding-bitfield/-/compact-encoding-bitfield-1.0.0.tgz",
839 "integrity": "sha512-3nMVKUg+PF72UHfainmCL8uKvyWfxsjqOtUY+HiMPGLPCTjnwzoKfFAMo1Ad7nwTPdjBqtGK5b3BOFTFW4EBTg==",
840 "license": "ISC",
841 "dependencies": {
842 "compact-encoding": "^2.4.1"
843 }
844 },
845 "node_modules/compact-encoding-net": {
846 "version": "1.2.0",
847 "resolved": "https://registry.npmjs.org/compact-encoding-net/-/compact-encoding-net-1.2.0.tgz",
848 "integrity": "sha512-LVXpNpF7PGQeHRVVLGgYWzuVoYAaDZvKUsUxRioGfkotzvOh4AzoQF1HBH3zMNaSnx7gJXuUr3hkjnijaH/Eng==",
849 "license": "ISC",
850 "dependencies": {
851 "compact-encoding": "^2.4.1"
852 }
853 },
854 "node_modules/convert-source-map": {
855 "version": "2.0.0",
856 "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
857 "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
858 "dev": true,
859 "license": "MIT"
860 },
861 "node_modules/corestore": {
862 "version": "7.9.2",
863 "resolved": "https://registry.npmjs.org/corestore/-/corestore-7.9.2.tgz",
864 "integrity": "sha512-dyIktVSVLu0skZ1UM4yODbNbZ46dBpTHypzwlwET1btVuS3YIf66Zz8Tx8h3Xa8nbLO2USGqrYtvPYzSRj2wCA==",
865 "dev": true,
866 "license": "MIT",
867 "dependencies": {
868 "b4a": "^1.6.7",
869 "hypercore": "^11.19.0",
870 "hypercore-crypto": "^3.4.2",
871 "hypercore-errors": "^1.4.0",
872 "hypercore-id-encoding": "^1.3.0",
873 "ready-resource": "^1.1.1",
874 "sodium-universal": "^5.0.1",
875 "which-runtime": "^1.2.1"
876 }
877 },
878 "node_modules/device-file": {
879 "version": "2.3.1",
880 "resolved": "https://registry.npmjs.org/device-file/-/device-file-2.3.1.tgz",
881 "integrity": "sha512-bmON44lwxJPle9N2OcH4tqM44pMGZKT8G6OkzXkz0urvqQ9LKkoQdTS6w0ztYfmLdUE27R4UUmx0+y2gEz4Jug==",
882 "dev": true,
883 "license": "Apache-2.0",
884 "dependencies": {
885 "b4a": "^1.6.7",
886 "bare-fs": "^4.0.1",
887 "bare-path": "^3.0.0",
888 "fd-lock": "^2.1.0",
889 "fs-native-extensions": "^1.4.0",
890 "ready-resource": "^1.2.0"
891 }
892 },
893 "node_modules/dht-rpc": {
894 "version": "6.26.3",
895 "resolved": "https://registry.npmjs.org/dht-rpc/-/dht-rpc-6.26.3.tgz",
896 "integrity": "sha512-KuLfRv/hecUHipQcTXHpVv4/N4Jhpww5sLdsrn3Edm5oHwzK9SgNV34hNt7a2aVZCOcG5SfP4AvcQ7pI+y9YNg==",
897 "license": "MIT",
898 "dependencies": {
899 "adaptive-timeout": "^1.0.1",
900 "b4a": "^1.6.1",
901 "bare-events": "^2.2.0",
902 "compact-encoding": "^2.11.0",
903 "compact-encoding-net": "^1.2.0",
904 "fast-fifo": "^1.1.0",
905 "kademlia-routing-table": "^1.0.1",
906 "nat-sampler": "^1.0.1",
907 "sodium-universal": "^5.0.0",
908 "streamx": "^2.13.2",
909 "time-ordered-set": "^2.0.0",
910 "udx-native": "^1.5.3"
911 }
912 },
913 "node_modules/error-stack-parser": {
914 "version": "2.1.4",
915 "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
916 "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==",
917 "dev": true,
918 "license": "MIT",
919 "dependencies": {
920 "stackframe": "^1.3.4"
921 }
922 },
923 "node_modules/events-universal": {
924 "version": "1.0.1",
925 "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
926 "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
927 "license": "Apache-2.0",
928 "dependencies": {
929 "bare-events": "^2.7.0"
930 }
931 },
932 "node_modules/fast-fifo": {
933 "version": "1.3.2",
934 "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
935 "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
936 "license": "MIT"
937 },
938 "node_modules/fd-lock": {
939 "version": "2.1.1",
940 "resolved": "https://registry.npmjs.org/fd-lock/-/fd-lock-2.1.1.tgz",
941 "integrity": "sha512-H3VkcWFl39Rk0xBokDcUyBcVs6VrYTUo/DhDWMzJOF99wXWDAvmvq/GlLTS2NTehsznZhp3fXVyrtB2ldDXhwg==",
942 "dev": true,
943 "license": "Apache-2.0",
944 "dependencies": {
945 "bare-fs": "^4.5.0",
946 "fs-native-extensions": "^1.4.4",
947 "ready-resource": "^1.2.0",
948 "resource-on-exit": "^1.0.0"
949 }
950 },
951 "node_modules/flat-tree": {
952 "version": "1.13.0",
953 "resolved": "https://registry.npmjs.org/flat-tree/-/flat-tree-1.13.0.tgz",
954 "integrity": "sha512-fT3HIuCPwHhFgJ20QYzDHgUG0zMmFg5cHvFiFo5h+QMSJ28TihsEVY0f8HGliuO+pOzmvjMx1odToeaEWkTnyQ==",
955 "dev": true,
956 "license": "MIT"
957 },
958 "node_modules/fs-native-extensions": {
959 "version": "1.4.5",
960 "resolved": "https://registry.npmjs.org/fs-native-extensions/-/fs-native-extensions-1.4.5.tgz",
961 "integrity": "sha512-ekV0T//iDm4AvhOcuPaHpxub4DI7HvY5ucLJVDvi7T2J+NZkQ9S6MuvgP0yeQvoqNUaAGyLjVYb1905BF9bpmg==",
962 "dev": true,
963 "license": "Apache-2.0",
964 "dependencies": {
965 "require-addon": "^1.1.0",
966 "which-runtime": "^1.2.0"
967 }
968 },
969 "node_modules/generate-object-property": {
970 "version": "2.0.0",
971 "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-2.0.0.tgz",
972 "integrity": "sha512-KwuURPyqn2Mz8DdV29pJwQu0Y7tcsbkULr82eeOcY/ZllFK6I9Wm8dsRByIu7CKWlFi9BdW1b3mcXMp/kQBQsw==",
973 "dev": true,
974 "license": "MIT",
975 "dependencies": {
976 "is-property": "^1.0.0"
977 }
978 },
979 "node_modules/generate-string": {
980 "version": "1.0.1",
981 "resolved": "https://registry.npmjs.org/generate-string/-/generate-string-1.0.1.tgz",
982 "integrity": "sha512-IfTY0dKZM43ACyGvXkbG7De7WY7MxTS5VO6Juhe8oJKpCmrYYXoqp/cJMskkpi0k9H8wuXq0H+eI898/BCqvXg==",
983 "dev": true,
984 "license": "MIT"
985 },
986 "node_modules/globbie": {
987 "version": "1.0.3",
988 "resolved": "https://registry.npmjs.org/globbie/-/globbie-1.0.3.tgz",
989 "integrity": "sha512-hcryJmKcftf82xfBWTWxuUITWIIoYO3ec104V17SMHGFl6VLbm1d1Ju9LX9L+jyJTwICpemX/dmPI/HGYoKr1A==",
990 "dev": true,
991 "license": "Apache-2.0",
992 "dependencies": {
993 "bare-fs": "^4.1.2",
994 "bare-path": "^3.0.0",
995 "bare-process": "^4.2.1",
996 "picomatch": "^4.0.2"
997 }
998 },
999 "node_modules/hyper-rooms": {
1000 "resolved": "../hyper-rooms",
1001 "link": true
1002 },
1003 "node_modules/hypercore": {
1004 "version": "11.28.1",
1005 "resolved": "https://registry.npmjs.org/hypercore/-/hypercore-11.28.1.tgz",
1006 "integrity": "sha512-d8jqUilG7Gri8wT+sRdkFTYRPMHGyvBz/NCYHf5rX7+LoivRNRxQm5nWChT7caiUKbcsDak6h7IiEQ5wbD1wCg==",
1007 "dev": true,
1008 "license": "MIT",
1009 "dependencies": {
1010 "@hyperswarm/secret-stream": "^6.0.0",
1011 "b4a": "^1.1.0",
1012 "bare-events": "^2.2.0",
1013 "big-sparse-array": "^1.0.3",
1014 "compact-encoding": "^2.11.0",
1015 "fast-fifo": "^1.3.0",
1016 "flat-tree": "^1.9.0",
1017 "hypercore-crypto": "^3.2.1",
1018 "hypercore-errors": "^1.5.0",
1019 "hypercore-id-encoding": "^1.2.0",
1020 "hypercore-storage": "^2.8.0",
1021 "is-options": "^1.0.1",
1022 "nanoassert": "^2.0.0",
1023 "protomux": "^3.5.0",
1024 "quickbit-universal": "^2.2.0",
1025 "random-array-iterator": "^1.0.0",
1026 "safety-catch": "^1.0.1",
1027 "sodium-universal": "^5.0.1",
1028 "streamx": "^2.12.4",
1029 "unslab": "^1.3.0",
1030 "z32": "^1.0.0"
1031 }
1032 },
1033 "node_modules/hypercore-crypto": {
1034 "version": "3.6.1",
1035 "resolved": "https://registry.npmjs.org/hypercore-crypto/-/hypercore-crypto-3.6.1.tgz",
1036 "integrity": "sha512-ltIz2uDwy9pO/ZGTvqcjzyBkvt6O4cVm4r/nNxh0GFs/RbQtqP/i4wCvLEdmU7ptgtnw7fI67WYD1aHPuv4OVA==",
1037 "license": "MIT",
1038 "dependencies": {
1039 "b4a": "^1.6.6",
1040 "compact-encoding": "^2.15.0",
1041 "sodium-universal": "^5.0.0"
1042 }
1043 },
1044 "node_modules/hypercore-errors": {
1045 "version": "1.5.0",
1046 "resolved": "https://registry.npmjs.org/hypercore-errors/-/hypercore-errors-1.5.0.tgz",
1047 "integrity": "sha512-5KQ/SuDxsvet+7qWA35Ay6zdD9WyAHQoyWHGcPUTbmJBd300gvNIJoi3oma7kp4TTCSzii6qYumNZe/s0j/saQ==",
1048 "dev": true,
1049 "license": "Apache-2.0",
1050 "dependencies": {
1051 "hypercore-id-encoding": "^1.3.0"
1052 }
1053 },
1054 "node_modules/hypercore-id-encoding": {
1055 "version": "1.3.0",
1056 "resolved": "https://registry.npmjs.org/hypercore-id-encoding/-/hypercore-id-encoding-1.3.0.tgz",
1057 "integrity": "sha512-W6sHdGo5h7LXEsoWfKf/KfuROZmZRQDlGqJF2EPHW+noCK66Vvr0+zE6cL0vqQi18s0kQPeN7Sq3QyR0Ytc2VQ==",
1058 "license": "Apache-2.0",
1059 "dependencies": {
1060 "b4a": "^1.5.3",
1061 "z32": "^1.0.0"
1062 }
1063 },
1064 "node_modules/hypercore-storage": {
1065 "version": "2.8.0",
1066 "resolved": "https://registry.npmjs.org/hypercore-storage/-/hypercore-storage-2.8.0.tgz",
1067 "integrity": "sha512-s35HllFMYvrkSHvkIIklVFmCI/bRVNO7H3pi4mKvWl0XLyhGGGM+NtJ7fpsh82oPRCmL9XtMRO+pT6LiMAO8MA==",
1068 "dev": true,
1069 "license": "Apache-2.0",
1070 "dependencies": {
1071 "b4a": "^1.6.7",
1072 "bare-fs": "^4.0.1",
1073 "bare-path": "^3.0.0",
1074 "compact-encoding": "^2.16.0",
1075 "device-file": "^2.1.2",
1076 "flat-tree": "^1.12.1",
1077 "hypercore-crypto": "^3.4.2",
1078 "hyperschema": "^1.7.0",
1079 "index-encoder": "^3.3.2",
1080 "resolve-reject-promise": "^1.0.0",
1081 "rocksdb-native": "^3.11.0",
1082 "scope-lock": "^1.2.4",
1083 "streamx": "^2.21.1"
1084 }
1085 },
1086 "node_modules/hyperdht": {
1087 "version": "6.29.6",
1088 "resolved": "https://registry.npmjs.org/hyperdht/-/hyperdht-6.29.6.tgz",
1089 "integrity": "sha512-bMMtw02fhiALdsTVLNZ/VFmMZuEY8kxb0/Rprl6oYF4/TCS653i3FDjtJeRbqtnAzwAj6dVZ/EwhoNOSCFGaYQ==",
1090 "license": "MIT",
1091 "dependencies": {
1092 "@hyperswarm/secret-stream": "^6.6.2",
1093 "b4a": "^1.3.1",
1094 "bare-events": "^2.2.0",
1095 "blind-relay": "^1.3.0",
1096 "bogon": "^1.0.0",
1097 "compact-encoding": "^2.4.1",
1098 "dht-rpc": "^6.15.1",
1099 "hypercore-crypto": "^3.3.0",
1100 "hypercore-id-encoding": "^1.2.0",
1101 "noise-curve-ed": "^2.0.0",
1102 "noise-handshake": "^4.0.0",
1103 "record-cache": "^1.1.1",
1104 "safety-catch": "^1.0.1",
1105 "signal-promise": "^1.0.3",
1106 "sodium-universal": "^5.0.1",
1107 "streamx": "^2.16.1",
1108 "unslab": "^1.3.0",
1109 "xache": "^1.1.0"
1110 },
1111 "bin": {
1112 "hyperdht": "bin.js"
1113 }
1114 },
1115 "node_modules/hyperschema": {
1116 "version": "1.20.1",
1117 "resolved": "https://registry.npmjs.org/hyperschema/-/hyperschema-1.20.1.tgz",
1118 "integrity": "sha512-7gnvaTNUs8FqdrU3NNE6nzhOneGyDlUlpROu9YFlHA61fE8ppKKf2gLZjZjsx/enNt+VFN4ITxy8ijypfyWBLw==",
1119 "dev": true,
1120 "license": "Apache-2.0",
1121 "dependencies": {
1122 "bare-fs": "^4.0.1",
1123 "compact-encoding": "^2.19.0",
1124 "generate-object-property": "^2.0.0",
1125 "generate-string": "^1.0.1"
1126 }
1127 },
1128 "node_modules/hyperswarm": {
1129 "version": "4.17.0",
1130 "resolved": "https://registry.npmjs.org/hyperswarm/-/hyperswarm-4.17.0.tgz",
1131 "integrity": "sha512-oe86sK961Ueg7rvDN/veFwG8xH+Iv6vObPhGDkPJcDVxk/NduW41ZhAcVDnHzRbm7S0eLU7WaDUvehOYoKSpRQ==",
1132 "license": "MIT",
1133 "dependencies": {
1134 "b4a": "^1.3.1",
1135 "bare-events": "^2.2.0",
1136 "hyperdht": "^6.21.0",
1137 "safety-catch": "^1.0.2",
1138 "shuffled-priority-queue": "^2.1.0",
1139 "streamx": "^2.22.1",
1140 "unslab": "^1.3.0"
1141 }
1142 },
1143 "node_modules/index-encoder": {
1144 "version": "3.5.0",
1145 "resolved": "https://registry.npmjs.org/index-encoder/-/index-encoder-3.5.0.tgz",
1146 "integrity": "sha512-idZ1cxtZz2dRV6rUiaP9Xo99UjXbSzjcMacoQmxUMu/A7fEQcNPngvwDJYeWelQUS5XFlY71/or70lKn6XnwbQ==",
1147 "dev": true,
1148 "license": "Apache-2.0",
1149 "dependencies": {
1150 "b4a": "^1.6.4"
1151 }
1152 },
1153 "node_modules/is-options": {
1154 "version": "1.0.2",
1155 "resolved": "https://registry.npmjs.org/is-options/-/is-options-1.0.2.tgz",
1156 "integrity": "sha512-u+Ai74c8Q74aS8BuHwPdI1jptGOT1FQXgCq8/zv0xRuE+wRgSMEJLj8lVO8Zp9BeGb29BXY6AsNPinfqjkr7Fg==",
1157 "dev": true,
1158 "license": "MIT",
1159 "dependencies": {
1160 "b4a": "^1.1.1"
1161 }
1162 },
1163 "node_modules/is-property": {
1164 "version": "1.0.2",
1165 "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
1166 "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
1167 "dev": true,
1168 "license": "MIT"
1169 },
1170 "node_modules/kademlia-routing-table": {
1171 "version": "1.0.6",
1172 "resolved": "https://registry.npmjs.org/kademlia-routing-table/-/kademlia-routing-table-1.0.6.tgz",
1173 "integrity": "sha512-Ve6jwIlUCYvUzBnXnzVRHDZCFgXURW9gmF3r7n05kZs/2rNbLHXwGdcq0qIaSwdmJCvtosgR4JensnVU65hzNQ==",
1174 "license": "MIT",
1175 "dependencies": {
1176 "bare-events": "^2.2.0"
1177 }
1178 },
1179 "node_modules/nanoassert": {
1180 "version": "2.0.0",
1181 "resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-2.0.0.tgz",
1182 "integrity": "sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA==",
1183 "license": "ISC"
1184 },
1185 "node_modules/nat-sampler": {
1186 "version": "1.0.1",
1187 "resolved": "https://registry.npmjs.org/nat-sampler/-/nat-sampler-1.0.1.tgz",
1188 "integrity": "sha512-yQvyNN7xbqR8crTKk3U8gRgpcV1Az+vfCEijiHu9oHHsnIl8n3x+yXNHl42M6L3czGynAVoOT9TqBfS87gDdcw==",
1189 "license": "MIT"
1190 },
1191 "node_modules/noise-curve-ed": {
1192 "version": "2.1.0",
1193 "resolved": "https://registry.npmjs.org/noise-curve-ed/-/noise-curve-ed-2.1.0.tgz",
1194 "integrity": "sha512-zAzJx+VwZM3w6EA1hTmDhJfvAnCeBQn/1FAeZ0LtGxCcCtlAK/uJXQVF/eDVUOaAZ286lHlx77WJ+qj9SmsRRg==",
1195 "license": "ISC",
1196 "dependencies": {
1197 "b4a": "^1.1.0",
1198 "nanoassert": "^2.0.0",
1199 "sodium-universal": "^5.0.0"
1200 }
1201 },
1202 "node_modules/noise-handshake": {
1203 "version": "4.2.0",
1204 "resolved": "https://registry.npmjs.org/noise-handshake/-/noise-handshake-4.2.0.tgz",
1205 "integrity": "sha512-9O/VTNX/E2/AToyMTTDU0J/4WhaXMTdqc2DHs9vf+snoZ0cenSBq0dNYTVV1snYYEkmo6QeRrYMxtqtoYnY+LA==",
1206 "license": "Apache-2.0",
1207 "dependencies": {
1208 "b4a": "^1.1.0",
1209 "nanoassert": "^2.0.0",
1210 "sodium-universal": "^5.0.0"
1211 }
1212 },
1213 "node_modules/paparam": {
1214 "version": "1.10.1",
1215 "resolved": "https://registry.npmjs.org/paparam/-/paparam-1.10.1.tgz",
1216 "integrity": "sha512-viyQI64VIja0Za3njIzhoEP8ZVkgPowhZPuG0E96NwBfYJ6ZIyrrlhWGtFPkdN7eYLl2L8CTWL+wla0evm1KKQ==",
1217 "dev": true,
1218 "license": "Apache-2.0"
1219 },
1220 "node_modules/picomatch": {
1221 "version": "4.0.4",
1222 "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
1223 "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
1224 "dev": true,
1225 "license": "MIT",
1226 "engines": {
1227 "node": ">=12"
1228 },
1229 "funding": {
1230 "url": "https://github.com/sponsors/jonschlinkert"
1231 }
1232 },
1233 "node_modules/protomux": {
1234 "version": "3.10.1",
1235 "resolved": "https://registry.npmjs.org/protomux/-/protomux-3.10.1.tgz",
1236 "integrity": "sha512-jgBqx8ZyaBWea/DFG4eOu1scOaeBwcnagiRC1XFVrjeGt7oAb0Pk5udPpBUpJ4DJBRjra50jD6YcZiQQTRqaaA==",
1237 "license": "MIT",
1238 "dependencies": {
1239 "b4a": "^1.3.1",
1240 "compact-encoding": "^2.5.1",
1241 "queue-tick": "^1.0.0",
1242 "safety-catch": "^1.0.1",
1243 "unslab": "^1.3.0"
1244 }
1245 },
1246 "node_modules/queue-tick": {
1247 "version": "1.0.1",
1248 "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
1249 "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==",
1250 "license": "MIT"
1251 },
1252 "node_modules/quickbit-native": {
1253 "version": "2.4.8",
1254 "resolved": "https://registry.npmjs.org/quickbit-native/-/quickbit-native-2.4.8.tgz",
1255 "integrity": "sha512-FcCcqI+nIAWGknqhtrYT5TSD7t/N+Xd8ctM+2PrIIBuwOi5hx0SxAvuPtzLIEMfT/2h9+fhBakUe2uALOHX6yw==",
1256 "dev": true,
1257 "license": "Apache-2.0",
1258 "optional": true,
1259 "dependencies": {
1260 "require-addon": "^1.1.0"
1261 }
1262 },
1263 "node_modules/quickbit-universal": {
1264 "version": "2.2.0",
1265 "resolved": "https://registry.npmjs.org/quickbit-universal/-/quickbit-universal-2.2.0.tgz",
1266 "integrity": "sha512-w02i1R8n7+6pEKTud8DfF8zbFY9o7RtPlUc3jWbtCkDKvhbx/AvV7oNnz4/TcmsPGpSJS+fq5Ud6RH6+YPvSGg==",
1267 "dev": true,
1268 "license": "ISC",
1269 "dependencies": {
1270 "b4a": "^1.6.0",
1271 "simdle-universal": "^1.1.0"
1272 },
1273 "optionalDependencies": {
1274 "quickbit-native": "^2.2.0"
1275 }
1276 },
1277 "node_modules/random-array-iterator": {
1278 "version": "1.0.0",
1279 "resolved": "https://registry.npmjs.org/random-array-iterator/-/random-array-iterator-1.0.0.tgz",
1280 "integrity": "sha512-u7xCM93XqKEvPTP6xZp2ehttcAemKnh73oKNf1FvzuVCfpt6dILDt1Kxl1LeBjm2iNIeR49VGFhy4Iz3yOun+Q==",
1281 "dev": true,
1282 "license": "MIT"
1283 },
1284 "node_modules/ready-resource": {
1285 "version": "1.2.0",
1286 "resolved": "https://registry.npmjs.org/ready-resource/-/ready-resource-1.2.0.tgz",
1287 "integrity": "sha512-nfcco/8iAFV0M+2PYnmIc+/xY0iRb35d42HFHQ7AfjulbGEAFa+XWpByfwSyeVeiBoMLLFVMv1HixxNCqzSQ1g==",
1288 "license": "MIT",
1289 "dependencies": {
1290 "bare-events": "^2.2.0"
1291 }
1292 },
1293 "node_modules/record-cache": {
1294 "version": "1.2.0",
1295 "resolved": "https://registry.npmjs.org/record-cache/-/record-cache-1.2.0.tgz",
1296 "integrity": "sha512-kyy3HWCez2WrotaL3O4fTn0rsIdfRKOdQQcEJ9KpvmKmbffKVvwsloX063EgRUlpJIXHiDQFhJcTbZequ2uTZw==",
1297 "license": "MIT",
1298 "dependencies": {
1299 "b4a": "^1.3.1"
1300 }
1301 },
1302 "node_modules/refcounter": {
1303 "version": "1.0.0",
1304 "resolved": "https://registry.npmjs.org/refcounter/-/refcounter-1.0.0.tgz",
1305 "integrity": "sha512-1WosVzUy0kPUaPMEtlNDwm99UsteALIhXXR8rerELoa63WkYIXAl0hxgwPFrIYBRWZPGUyekQ04FRtPJ7dHk9w==",
1306 "dev": true,
1307 "license": "Apache-2.0"
1308 },
1309 "node_modules/require-addon": {
1310 "version": "1.2.0",
1311 "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.2.0.tgz",
1312 "integrity": "sha512-VNPDZlYgIYQwWp9jMTzljx+k0ZtatKlcvOhktZ/anNPI3dQ9NXk7cq2U4iJ1wd9IrytRnYhyEocFWbkdPb+MYA==",
1313 "license": "Apache-2.0",
1314 "dependencies": {
1315 "bare-addon-resolve": "^1.3.0"
1316 },
1317 "engines": {
1318 "bare": ">=1.10.0"
1319 }
1320 },
1321 "node_modules/resolve-reject-promise": {
1322 "version": "1.1.0",
1323 "resolved": "https://registry.npmjs.org/resolve-reject-promise/-/resolve-reject-promise-1.1.0.tgz",
1324 "integrity": "sha512-LWsTOA91AqzBTjSGgX79Tc130pwcBK6xjpJEO+qRT5IKZ6bGnHKcc8QL3upUBcWuU8OTIDzKK2VNSwmmlqvAVg==",
1325 "dev": true,
1326 "license": "MIT"
1327 },
1328 "node_modules/resource-on-exit": {
1329 "version": "1.0.0",
1330 "resolved": "https://registry.npmjs.org/resource-on-exit/-/resource-on-exit-1.0.0.tgz",
1331 "integrity": "sha512-ViJwJAknCkLRJRPR+9SISQQ7R5eRgtdIHLJsM2hHx1MweAJbJxJ5XnMjjq0Lc7ZGv44ufzAqds1nKxiVkdy4ag==",
1332 "dev": true,
1333 "license": "Apache-2.0"
1334 },
1335 "node_modules/rocksdb-native": {
1336 "version": "3.15.0",
1337 "resolved": "https://registry.npmjs.org/rocksdb-native/-/rocksdb-native-3.15.0.tgz",
1338 "integrity": "sha512-czYx0hOzKAy+HYmJROZm7thvJ1tHFWHNkGI3nomumXWzrJghJdrWo4MIhehGX4OTff0lnOCoB2zu6ekzIKY0rQ==",
1339 "dev": true,
1340 "license": "Apache-2.0",
1341 "dependencies": {
1342 "compact-encoding": "^2.15.0",
1343 "ready-resource": "^1.0.0",
1344 "refcounter": "^1.0.0",
1345 "require-addon": "^1.0.2",
1346 "resolve-reject-promise": "^1.1.0",
1347 "signal-promise": "^1.0.3",
1348 "streamx": "^2.16.1"
1349 },
1350 "engines": {
1351 "bare": ">=1.16.0"
1352 }
1353 },
1354 "node_modules/safety-catch": {
1355 "version": "1.0.3",
1356 "resolved": "https://registry.npmjs.org/safety-catch/-/safety-catch-1.0.3.tgz",
1357 "integrity": "sha512-Zq+J1TefpoEq/HTUabo0YXX5MNvttjWYODGohgPBO2jfko8Wqx3JYMgE823szDFVamdH5PlpByvfiWScTdSYDA==",
1358 "license": "MIT"
1359 },
1360 "node_modules/same-object": {
1361 "version": "1.0.2",
1362 "resolved": "https://registry.npmjs.org/same-object/-/same-object-1.0.2.tgz",
1363 "integrity": "sha512-csHWhvUsLbIOHDM/nP+KHWM+BLPsIzWkFa8HbzaI0G7BqKXgx+7FJpKTGgLXyz5amfdY2OVBcmXTqYOMEk04og==",
1364 "dev": true,
1365 "license": "MIT"
1366 },
1367 "node_modules/scope-lock": {
1368 "version": "1.2.4",
1369 "resolved": "https://registry.npmjs.org/scope-lock/-/scope-lock-1.2.4.tgz",
1370 "integrity": "sha512-BpSd8VCuCxW9ZitcdIC/vjs3gMaP9bRBL5nkHcyfX2VrS52n13/rHuBA2xJ/S/4DPuRdAO/Bk8pWd8eD/gHCIA==",
1371 "dev": true,
1372 "license": "Apache-2.0"
1373 },
1374 "node_modules/shuffled-priority-queue": {
1375 "version": "2.1.0",
1376 "resolved": "https://registry.npmjs.org/shuffled-priority-queue/-/shuffled-priority-queue-2.1.0.tgz",
1377 "integrity": "sha512-xhdh7fHyMsr0m/w2kDfRJuBFRS96b9l8ZPNWGaQ+PMvnUnZ/Eh+gJJ9NsHBd7P9k0399WYlCLzsy18EaMfyadA==",
1378 "license": "MIT",
1379 "dependencies": {
1380 "unordered-set": "^2.0.1"
1381 }
1382 },
1383 "node_modules/signal-promise": {
1384 "version": "1.0.3",
1385 "resolved": "https://registry.npmjs.org/signal-promise/-/signal-promise-1.0.3.tgz",
1386 "integrity": "sha512-WBgv0UnIq2C+Aeh0/n+IRpP6967eIx9WpynTUoiW3isPpfe1zu2LJzyfXdo9Tgef8yR/sGjcMvoUXD7EYdiz+g==",
1387 "license": "MIT"
1388 },
1389 "node_modules/simdle-native": {
1390 "version": "1.3.9",
1391 "resolved": "https://registry.npmjs.org/simdle-native/-/simdle-native-1.3.9.tgz",
1392 "integrity": "sha512-Isc8sP4OiiIU0mpslD4GHEnR0VQWvR/54WN7YtwEDkNdTJVWtpmvsSvsgRlw5BNGxdYXlVRegdnrSu10H/PhvA==",
1393 "dev": true,
1394 "license": "Apache-2.0",
1395 "optional": true,
1396 "dependencies": {
1397 "b4a": "^1.6.0",
1398 "require-addon": "^1.1.0"
1399 }
1400 },
1401 "node_modules/simdle-universal": {
1402 "version": "1.1.2",
1403 "resolved": "https://registry.npmjs.org/simdle-universal/-/simdle-universal-1.1.2.tgz",
1404 "integrity": "sha512-3n3w1bs+uwgHKQjt6arez83EywNlhZzYvNOhvAASTl/8KqNIcqr6aHyGt3JRlfuUC7iB0tomJRPlJ2cRGIpBzA==",
1405 "dev": true,
1406 "license": "ISC",
1407 "dependencies": {
1408 "b4a": "^1.6.0"
1409 },
1410 "optionalDependencies": {
1411 "simdle-native": "^1.1.1"
1412 }
1413 },
1414 "node_modules/sodium-native": {
1415 "version": "5.1.0",
1416 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.1.0.tgz",
1417 "integrity": "sha512-3RxgyWyJlhTsABPnJVpCI5CoTDANZTqqFrEPqr+kjfnRaBihpVtMUE3yTF40ukdoB1APXeoBNKF3MzZAIHg39g==",
1418 "license": "MIT",
1419 "dependencies": {
1420 "bare-assert": "^1.2.0",
1421 "require-addon": "^1.1.0",
1422 "which-runtime": "^1.2.1"
1423 },
1424 "engines": {
1425 "bare": ">=1.16.0"
1426 }
1427 },
1428 "node_modules/sodium-secretstream": {
1429 "version": "1.2.0",
1430 "resolved": "https://registry.npmjs.org/sodium-secretstream/-/sodium-secretstream-1.2.0.tgz",
1431 "integrity": "sha512-q/DbraNFXm1KfCiiZvapmz5UC3OlpirYFIvBK2MhGaOFSb3gRyk8OXTi17UI9SGfshQNCpsVvlopogbzZNyW6Q==",
1432 "license": "MIT",
1433 "dependencies": {
1434 "b4a": "^1.1.1",
1435 "sodium-universal": "^5.0.0"
1436 }
1437 },
1438 "node_modules/sodium-universal": {
1439 "version": "5.0.1",
1440 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz",
1441 "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==",
1442 "license": "MIT",
1443 "dependencies": {
1444 "sodium-native": "^5.0.1"
1445 },
1446 "peerDependencies": {
1447 "sodium-javascript": "~0.8.0"
1448 },
1449 "peerDependenciesMeta": {
1450 "sodium-javascript": {
1451 "optional": true
1452 }
1453 }
1454 },
1455 "node_modules/stackframe": {
1456 "version": "1.3.4",
1457 "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
1458 "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==",
1459 "dev": true,
1460 "license": "MIT"
1461 },
1462 "node_modules/streamx": {
1463 "version": "2.25.0",
1464 "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz",
1465 "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==",
1466 "license": "MIT",
1467 "dependencies": {
1468 "events-universal": "^1.0.0",
1469 "fast-fifo": "^1.3.2",
1470 "text-decoder": "^1.1.0"
1471 }
1472 },
1473 "node_modules/teex": {
1474 "version": "1.0.1",
1475 "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz",
1476 "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==",
1477 "license": "MIT",
1478 "dependencies": {
1479 "streamx": "^2.12.5"
1480 }
1481 },
1482 "node_modules/test-tmp": {
1483 "version": "1.4.0",
1484 "resolved": "https://registry.npmjs.org/test-tmp/-/test-tmp-1.4.0.tgz",
1485 "integrity": "sha512-GVggxGg+jXqP2Wbju50JVLo+9E+nIOPPyWqgr63EbOnNItIKu1cEbJpTWAJeflnyGqXOtcMI7ijHRp88GUkfDA==",
1486 "dev": true,
1487 "license": "MIT",
1488 "dependencies": {
1489 "bare-fs": "^4.0.1",
1490 "bare-os": "^3.3.0",
1491 "bare-path": "^3.0.0"
1492 }
1493 },
1494 "node_modules/text-decoder": {
1495 "version": "1.2.7",
1496 "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz",
1497 "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==",
1498 "license": "Apache-2.0",
1499 "dependencies": {
1500 "b4a": "^1.6.4"
1501 }
1502 },
1503 "node_modules/time-ordered-set": {
1504 "version": "2.0.1",
1505 "resolved": "https://registry.npmjs.org/time-ordered-set/-/time-ordered-set-2.0.1.tgz",
1506 "integrity": "sha512-VJEKmgSN2UiOLB8BpN8Sh2b9LGMHTP5OPrQRpnKjvOheOyzk0mufbjzjKTIG2gO4A+Y+vDJ+0TcLbpUmMLsg8A==",
1507 "license": "MIT"
1508 },
1509 "node_modules/timeout-refresh": {
1510 "version": "2.0.1",
1511 "resolved": "https://registry.npmjs.org/timeout-refresh/-/timeout-refresh-2.0.1.tgz",
1512 "integrity": "sha512-SVqEcMZBsZF9mA78rjzCrYrUs37LMJk3ShZ851ygZYW1cMeIjs9mL57KO6Iv5mmjSQnOe/29/VAfGXo+oRCiVw==",
1513 "license": "MIT"
1514 },
1515 "node_modules/tmatch": {
1516 "version": "5.0.0",
1517 "resolved": "https://registry.npmjs.org/tmatch/-/tmatch-5.0.0.tgz",
1518 "integrity": "sha512-Ib9OtBkpHn07tXP04SlN1SYRxFgTk6wSM2EBmjjxug4u5RXPRVLkdFJSS1PmrQidaSB8Lru9nRtViQBsbxzE5Q==",
1519 "dev": true,
1520 "license": "ISC",
1521 "engines": {
1522 "node": ">=8"
1523 }
1524 },
1525 "node_modules/udx-native": {
1526 "version": "1.19.2",
1527 "resolved": "https://registry.npmjs.org/udx-native/-/udx-native-1.19.2.tgz",
1528 "integrity": "sha512-RNYh+UhfryCsF5hE2ZOuIqcZ+qdipXK3UsarwxWJwsUQZFE3ybwz0mPjwb5ev1PMBcjFahWiepS/q0wwL51c2g==",
1529 "license": "Apache-2.0",
1530 "dependencies": {
1531 "b4a": "^1.5.0",
1532 "bare-events": "^2.2.0",
1533 "require-addon": "^1.1.0",
1534 "streamx": "^2.22.0"
1535 },
1536 "engines": {
1537 "bare": ">=1.17.4"
1538 }
1539 },
1540 "node_modules/unordered-set": {
1541 "version": "2.0.1",
1542 "resolved": "https://registry.npmjs.org/unordered-set/-/unordered-set-2.0.1.tgz",
1543 "integrity": "sha512-eUmNTPzdx+q/WvOHW0bgGYLWvWHNT3PTKEQLg0MAQhc0AHASHVHoP/9YytYd4RBVariqno/mEUhVZN98CmD7bg==",
1544 "license": "MIT"
1545 },
1546 "node_modules/unslab": {
1547 "version": "1.3.0",
1548 "resolved": "https://registry.npmjs.org/unslab/-/unslab-1.3.0.tgz",
1549 "integrity": "sha512-YATkfKAFj47kTzmiQrWXMyRvaVrHsW6MEALa4bm+FhiA2YG4oira+Z3DXN6LrYOYn2Y8eO94Lwl9DOHjs1FpoQ==",
1550 "license": "Apache-2.0",
1551 "dependencies": {
1552 "b4a": "^1.6.6"
1553 }
1554 },
1555 "node_modules/v8-to-istanbul": {
1556 "version": "9.3.0",
1557 "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
1558 "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==",
1559 "dev": true,
1560 "license": "ISC",
1561 "dependencies": {
1562 "@jridgewell/trace-mapping": "^0.3.12",
1563 "@types/istanbul-lib-coverage": "^2.0.1",
1564 "convert-source-map": "^2.0.0"
1565 },
1566 "engines": {
1567 "node": ">=10.12.0"
1568 }
1569 },
1570 "node_modules/which-runtime": {
1571 "version": "1.3.2",
1572 "resolved": "https://registry.npmjs.org/which-runtime/-/which-runtime-1.3.2.tgz",
1573 "integrity": "sha512-5kwCfWml7+b2NO7KrLMhYihjRx0teKkd3yGp1Xk5Vaf2JGdSh+rgVhEALAD9c/59dP+YwJHXoEO7e8QPy7gOkw==",
1574 "license": "Apache-2.0"
1575 },
1576 "node_modules/xache": {
1577 "version": "1.2.1",
1578 "resolved": "https://registry.npmjs.org/xache/-/xache-1.2.1.tgz",
1579 "integrity": "sha512-igRS6jPreJ54ABdzhh4mCDXcz+XMaWO2q1ABRV2yWYuk29jlp8VT7UBdCqNkX7rpYBbXsebVVKkwIuYZjyZNqA==",
1580 "license": "MIT"
1581 },
1582 "node_modules/z32": {
1583 "version": "1.1.0",
1584 "resolved": "https://registry.npmjs.org/z32/-/z32-1.1.0.tgz",
1585 "integrity": "sha512-1WUHy+VS6d0HPNspDxvLssBbeQjXMjSnpv0vH82vRAUfg847NmX3OXozp/hRP5jPhxBbrVzrgvAt+UsGNzRFQQ==",
1586 "license": "MIT",
1587 "dependencies": {
1588 "b4a": "^1.5.3"
1589 }
1590 }
1591 }
1592}
package.json +22 -0
……@@ -0,0 +1,22 @@
1{
2 "name": "hyper-svc",
3 "version": "0.1.0",
4 "description": "Reusable per-app daemon library — UDS + HTTP for local RPC, Hyperswarm for peer mirroring",
5 "type": "module",
6 "main": "index.js",
7 "scripts": {
8 "test": "brittle test/*.test.js"
9 },
10 "dependencies": {
11 "b4a": "^1.6.0",
12 "hyperswarm": "^4.0.0",
13 "protomux": "^3.0.0",
14 "ready-resource": "^1.0.0"
15 },
16 "devDependencies": {
17 "brittle": "^3.0.0",
18 "corestore": "^7.0.0",
19 "hyper-rooms": "file:../hyper-rooms",
20 "@hyperswarm/testnet": "^3.0.0"
21 }
22}
test/basic.test.js +431 -0
……@@ -0,0 +1,431 @@
1import test from 'brittle'
2import Corestore from 'corestore'
3import { Room, jsonSchema } from 'hyper-rooms'
4import createTestnet from '@hyperswarm/testnet'
5import { mkdtempSync } from 'fs'
6import { join } from 'path'
7import { tmpdir } from 'os'
8import b4a from 'b4a'
9
10import { Service, Client, encodeFrame, FrameDecoder } from '../index.js'
11
12// ── Helpers ────────────────────────────────────────────────────
13
14function tmpStore () {
15 return new Corestore(mkdtempSync(join(tmpdir(), 'hyper-svc-store-')))
16}
17
18function tmpSockPath () {
19 return join(mkdtempSync(join(tmpdir(), 'hyper-svc-sock-')), 'svc.sock')
20}
21
22// A trivial Room schema for swarm replication tests.
23const counterSchema = jsonSchema({
24 validate (op) {
25 if (op.type !== 'inc') throw new Error('only inc supported')
26 },
27 async apply (batch, op) {
28 const cur = await batch.get('counter')
29 const next = (cur ? cur.value.n : 0) + (op.delta || 1)
30 await batch.put('counter', { n: next })
31 }
32})
33
34class CounterRoom {
35 constructor (corestore, opts = {}) {
36 this._room = new Room(corestore, {
37 schema: counterSchema,
38 namespace: 'counter',
39 bootstrapKey: opts.bootstrapKey || null
40 })
41 }
42 async ready () { await this._room.ready() }
43 async close () { await this._room.close() }
44 get keyHex () { return this._room.keyHex }
45 get base () { return this._room.base }
46 async inc (delta = 1) {
47 await this._room.append({ type: 'inc', delta })
48 }
49 async value () {
50 await this._room.base.update()
51 const e = await this._room.view.get('counter')
52 return e ? e.value.n : 0
53 }
54}
55
56// ── Framing ───────────────────────────────────────────────────
57
58test('frame round-trip', (t) => {
59 const original = { id: 1, method: 'list', params: { limit: 10 } }
60 const frame = encodeFrame(original)
61
62 let decoded
63 const dec = new FrameDecoder((obj) => { decoded = obj })
64 dec.push(frame)
65
66 t.alike(decoded, original)
67})
68
69test('frame decoder handles multiple frames in one chunk', (t) => {
70 const a = encodeFrame({ id: 1, method: 'one' })
71 const b = encodeFrame({ id: 2, method: 'two' })
72
73 const seen = []
74 const dec = new FrameDecoder((obj) => seen.push(obj))
75 dec.push(b4a.concat([a, b]))
76
77 t.is(seen.length, 2)
78 t.is(seen[0].id, 1)
79 t.is(seen[1].id, 2)
80})
81
82test('frame decoder handles a frame split across chunks', (t) => {
83 const frame = encodeFrame({ id: 1, method: 'split', params: { x: 'hello world' } })
84 const seen = []
85 const dec = new FrameDecoder((obj) => seen.push(obj))
86
87 // Split arbitrarily
88 dec.push(frame.subarray(0, 3)) // partial header
89 t.is(seen.length, 0)
90 dec.push(frame.subarray(3, 6)) // rest of header + tiny payload
91 t.is(seen.length, 0)
92 dec.push(frame.subarray(6)) // rest of payload
93 t.is(seen.length, 1)
94 t.is(seen[0].method, 'split')
95})
96
97test('frame decoder rejects malformed JSON', (t) => {
98 const dec = new FrameDecoder(() => {})
99 // Length 5, payload "{bad}" — invalid JSON
100 const bad = b4a.alloc(9)
101 bad.writeUInt32BE(5, 0)
102 bad.write('{bad}', 4, 5, 'utf8')
103 // SyntaxError is a "native" error so we use exception.all
104 t.exception.all(() => dec.push(bad))
105})
106
107// ── In-process dispatch ──────────────────────────────────────
108
109test('in-process dispatch (no transports)', async (t) => {
110 const svc = new Service({ name: 'test' })
111 svc.handle('echo', async (ctx, params) => params)
112 svc.handle('hello', async (ctx, params) => `hi ${params.name}`)
113
114 await svc.ready()
115 t.teardown(async () => { try { await svc.close() } catch {} })
116
117 t.alike(await svc.dispatch('echo', { foo: 1 }), { foo: 1 })
118 t.is(await svc.dispatch('hello', { name: 'alice' }), 'hi alice')
119})
120
121test('dispatch unknown method throws ENOMETHOD', async (t) => {
122 const svc = new Service({ name: 'test' })
123 await svc.ready()
124 t.teardown(async () => { try { await svc.close() } catch {} })
125
126 try {
127 await svc.dispatch('nope', {})
128 t.fail('should have thrown')
129 } catch (err) {
130 t.is(err.code, 'ENOMETHOD')
131 t.ok(err.message.includes('nope'))
132 }
133})
134
135test('handle rejects duplicate names', async (t) => {
136 const svc = new Service({ name: 'test' })
137 svc.handle('foo', () => 1)
138 t.exception(() => svc.handle('foo', () => 2))
139 await svc.close()
140})
141
142test('listHandlers + listMounts', async (t) => {
143 const svc = new Service({ name: 'test' })
144 svc.handle('a', () => 1)
145 svc.handle('b', () => 2)
146 svc.mount('m1', { hello: 'world' })
147
148 t.alike(svc.listHandlers(), ['a', 'b'])
149 t.alike(svc.listMounts(), ['m1'])
150 t.is(svc.modules.m1.hello, 'world')
151 await svc.close()
152})
153
154// ── UDS transport ────────────────────────────────────────────
155
156test('UDS round-trip', async (t) => {
157 const sock = tmpSockPath()
158 const svc = new Service({ name: 'test', uds: sock })
159 svc.handle('echo', async (ctx, params) => {
160 return { from: ctx.transport, params }
161 })
162 await svc.listen()
163 t.teardown(async () => { try { await svc.close() } catch {} })
164
165 const client = await Client.discover({ uds: sock })
166 t.is(client.kind, 'uds')
167 t.teardown(async () => { try { await client.close() } catch {} })
168
169 const result = await client.call('echo', { x: 42 })
170 t.is(result.from, 'uds')
171 t.alike(result.params, { x: 42 })
172})
173
174test('UDS handler error propagates', async (t) => {
175 const sock = tmpSockPath()
176 const svc = new Service({ name: 'test', uds: sock })
177 svc.handle('boom', async () => { throw new Error('kaboom') })
178 await svc.listen()
179 t.teardown(async () => { try { await svc.close() } catch {} })
180
181 const client = await Client.discover({ uds: sock })
182 t.teardown(async () => { try { await client.close() } catch {} })
183
184 try {
185 await client.call('boom', {})
186 t.fail('should have thrown')
187 } catch (err) {
188 t.ok(err.message.includes('kaboom'))
189 }
190})
191
192test('UDS unknown method propagates', async (t) => {
193 const sock = tmpSockPath()
194 const svc = new Service({ name: 'test', uds: sock })
195 await svc.listen()
196 t.teardown(async () => { try { await svc.close() } catch {} })
197
198 const client = await Client.discover({ uds: sock })
199 t.teardown(async () => { try { await client.close() } catch {} })
200
201 try {
202 await client.call('missing', {})
203 t.fail('should have thrown')
204 } catch (err) {
205 t.ok(err.message.includes('missing') || err.message.includes('unknown'))
206 }
207})
208
209test('UDS pipelined requests', async (t) => {
210 const sock = tmpSockPath()
211 const svc = new Service({ name: 'test', uds: sock })
212 svc.handle('slow', async (ctx, params) => {
213 await new Promise(r => setTimeout(r, 30))
214 return params.n * 2
215 })
216 await svc.listen()
217 t.teardown(async () => { try { await svc.close() } catch {} })
218
219 const client = await Client.discover({ uds: sock })
220 t.teardown(async () => { try { await client.close() } catch {} })
221
222 // Fire 5 calls in parallel — they should pipeline and all return correct values
223 const results = await Promise.all([
224 client.call('slow', { n: 1 }),
225 client.call('slow', { n: 2 }),
226 client.call('slow', { n: 3 }),
227 client.call('slow', { n: 4 }),
228 client.call('slow', { n: 5 })
229 ])
230 t.alike(results, [2, 4, 6, 8, 10])
231})
232
233// ── HTTP transport ───────────────────────────────────────────
234
235test('HTTP round-trip', async (t) => {
236 const svc = new Service({ name: 'test', http: { port: 0 } })
237 svc.handle('echo', async (ctx, params) => {
238 return { from: ctx.transport, params }
239 })
240 await svc.listen()
241 t.teardown(async () => { try { await svc.close() } catch {} })
242
243 const port = svc.http.port
244 const client = await Client.discover({ http: `http://127.0.0.1:${port}` })
245 t.is(client.kind, 'http')
246 t.teardown(async () => { try { await client.close() } catch {} })
247
248 const result = await client.call('echo', { y: 'hi' })
249 t.is(result.from, 'http')
250 t.alike(result.params, { y: 'hi' })
251})
252
253test('HTTP unknown method returns 404', async (t) => {
254 const svc = new Service({ name: 'test', http: { port: 0 } })
255 await svc.listen()
256 t.teardown(async () => { try { await svc.close() } catch {} })
257
258 const client = await Client.discover({ http: `http://127.0.0.1:${svc.http.port}` })
259 t.teardown(async () => { try { await client.close() } catch {} })
260
261 try {
262 await client.call('nope', {})
263 t.fail('should have thrown')
264 } catch (err) {
265 t.ok(err.message.toLowerCase().includes('unknown') || err.message.includes('nope'))
266 }
267})
268
269test('HTTP app routes coexist with /rpc', async (t) => {
270 const svc = new Service({ name: 'test', http: { port: 0 } })
271 svc.handle('ping', async () => 'pong')
272
273 // App registers its own SSR route
274 svc.http.server.on('request', (req, res) => {
275 if (req.url === '/hello') {
276 res.writeHead(200, { 'Content-Type': 'text/plain' })
277 res.end('hello world')
278 }
279 })
280
281 await svc.listen()
282 t.teardown(async () => { try { await svc.close() } catch {} })
283
284 // RPC still works
285 const client = await Client.discover({ http: `http://127.0.0.1:${svc.http.port}` })
286 t.teardown(async () => { try { await client.close() } catch {} })
287 t.is(await client.call('ping', {}), 'pong')
288
289 // App route also works
290 const text = await fetchText(`http://127.0.0.1:${svc.http.port}/hello`)
291 t.is(text, 'hello world')
292})
293
294// ── Discovery ────────────────────────────────────────────────
295
296test('discover prefers UDS when both are reachable', async (t) => {
297 const sock = tmpSockPath()
298 const svc = new Service({ name: 'test', uds: sock, http: { port: 0 } })
299 svc.handle('echo', async (ctx) => ctx.transport)
300 await svc.listen()
301 t.teardown(async () => { try { await svc.close() } catch {} })
302
303 const client = await Client.discover({
304 uds: sock,
305 http: `http://127.0.0.1:${svc.http.port}`
306 })
307 t.is(client.kind, 'uds')
308 t.is(await client.call('echo', {}), 'uds')
309 await client.close()
310})
311
312test('discover falls back to HTTP when UDS unreachable', async (t) => {
313 // Daemon only listens on HTTP
314 const svc = new Service({ name: 'test', http: { port: 0 } })
315 svc.handle('echo', async (ctx) => ctx.transport)
316 await svc.listen()
317 t.teardown(async () => { try { await svc.close() } catch {} })
318
319 const client = await Client.discover({
320 uds: '/tmp/this-socket-does-not-exist-' + Date.now() + '.sock',
321 http: `http://127.0.0.1:${svc.http.port}`
322 })
323 t.is(client.kind, 'http')
324 t.is(await client.call('echo', {}), 'http')
325 await client.close()
326})
327
328test('discover throws when nothing is reachable', async (t) => {
329 try {
330 await Client.discover({})
331 t.fail('should have thrown')
332 } catch (err) {
333 t.ok(err.message.includes('no daemon') || err.message.includes('reachable'))
334 }
335})
336
337// ── Mounts ───────────────────────────────────────────────────
338
339test('mounted modules accessible via svc.modules', async (t) => {
340 const svc = new Service({ name: 'test' })
341 const orgs = { greet: () => 'hi from orgs' }
342 svc.mount('orgs', orgs)
343
344 svc.handle('greet', async () => svc.modules.orgs.greet())
345 await svc.ready()
346 t.teardown(async () => { try { await svc.close() } catch {} })
347
348 t.is(await svc.dispatch('greet', {}), 'hi from orgs')
349})
350
351// ── Swarm replication ────────────────────────────────────────
352
353test('swarm replicates a mounted Room across two services', async (t) => {
354 const testnet = await createTestnet(3, t.teardown)
355 const topic = b4a.alloc(32, 'topic-svc-test-rep')
356
357 // Service A — owns the bootstrap room
358 const storeA = tmpStore()
359 const counterA = new CounterRoom(storeA)
360 await counterA.ready()
361 await counterA.inc(1)
362
363 const svcA = new Service({
364 name: 'a',
365 corestore: storeA,
366 swarm: {
367 topic,
368 swarm: { bootstrap: testnet.bootstrap }
369 }
370 })
371 svcA.mount('counter', counterA)
372 await svcA.listen()
373
374 // Service B — joins with the same key
375 const storeB = tmpStore()
376 const counterB = new CounterRoom(storeB, { bootstrapKey: counterA.keyHex })
377 await counterB.ready()
378
379 const svcB = new Service({
380 name: 'b',
381 corestore: storeB,
382 swarm: {
383 topic,
384 swarm: { bootstrap: testnet.bootstrap }
385 }
386 })
387 svcB.mount('counter', counterB)
388 await svcB.listen()
389
390 t.teardown(async () => {
391 try { await svcA.close() } catch {}
392 try { await svcB.close() } catch {}
393 try { await counterA.close() } catch {}
394 try { await counterB.close() } catch {}
395 try { await storeA.close() } catch {}
396 try { await storeB.close() } catch {}
397 })
398
399 // Wait for B to receive A's writes (replication is async)
400 const ok = await waitFor(async () => (await counterB.value()) === 1, 5000)
401 t.ok(ok, 'B received A\'s data via swarm replication')
402
403 // Now A increments again, B should also see it
404 await counterA.inc(2)
405 const ok2 = await waitFor(async () => (await counterB.value()) === 3, 5000)
406 t.ok(ok2, 'B received A\'s second write')
407})
408
409// ── Helpers ───────────────────────────────────────────────────
410
411async function waitFor (predicate, timeoutMs) {
412 const start = Date.now()
413 while (Date.now() - start < timeoutMs) {
414 if (await predicate()) return true
415 await new Promise(r => setTimeout(r, 50))
416 }
417 return false
418}
419
420function fetchText (url) {
421 return new Promise((resolve, reject) => {
422 import('http').then(({ default: http }) => {
423 http.get(url, (res) => {
424 const parts = []
425 res.on('data', (c) => parts.push(c))
426 res.on('end', () => resolve(b4a.concat(parts).toString('utf8')))
427 res.on('error', reject)
428 }).on('error', reject)
429 })
430 })
431}