@cero-base/tools 0.8.4 → 0.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cero-tools.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { dial } from '../src/transport.js'
3
3
  import { connect } from '../src/connect.js'
4
- import { formatHandles, formatState, formatEvent, formatStats } from '../src/format.js'
4
+ import { formatHandles, formatState, formatError, formatEvent, formatStats } from '../src/format.js'
5
5
 
6
6
  const arg = process.argv[2]
7
7
  if (!arg) {
@@ -14,12 +14,25 @@ const host = hostPart
14
14
  const port = Number(portPart)
15
15
 
16
16
  const socket = dial({ host, port })
17
+ socket.on('error', (err) => {
18
+ console.error(
19
+ err.code === 'ECONNREFUSED'
20
+ ? `no tap on ${host ?? '127.0.0.1'}:${port} — is the app running with devtools()?`
21
+ : `connection error: ${err.message}`
22
+ )
23
+ process.exit(1)
24
+ })
25
+
17
26
  const session = await connect(socket)
18
27
 
19
28
  console.log(formatHandles(await session.handles()))
20
29
 
21
30
  for (const ref of process.argv.slice(3)) {
22
- console.log(formatState(ref, await session.get(ref)))
31
+ try {
32
+ console.log(formatState(ref, await session.get(ref)))
33
+ } catch (err) {
34
+ console.log(formatError(ref, err)) // a bad ref shouldn't kill the session
35
+ }
23
36
  }
24
37
 
25
38
  const events = session.events()
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@cero-base/tools",
3
- "version": "0.8.4",
3
+ "version": "0.8.5",
4
4
  "description": "Passive devtools for cero — observe data, events, and p2p/replication stats over a local out-of-band pipe.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
+ "types": "./types/index.d.ts",
7
8
  "bin": {
8
9
  "cero-tools": "./bin/cero-tools.js"
9
10
  },
@@ -15,6 +16,7 @@
15
16
  },
16
17
  "files": [
17
18
  "src",
19
+ "types",
18
20
  "bin",
19
21
  "README.md",
20
22
  "LICENSE"
@@ -23,16 +25,27 @@
23
25
  "access": "public"
24
26
  },
25
27
  "exports": {
26
- ".": "./src/index.js",
27
- "./connect": "./src/connect.js",
28
- "./stats": "./src/stats.js"
28
+ ".": {
29
+ "types": "./types/index.d.ts",
30
+ "default": "./src/index.js"
31
+ },
32
+ "./connect": {
33
+ "types": "./types/connect.d.ts",
34
+ "default": "./src/connect.js"
35
+ },
36
+ "./stats": {
37
+ "types": "./types/stats.d.ts",
38
+ "default": "./src/stats.js"
39
+ }
29
40
  },
30
41
  "scripts": {
42
+ "build:types": "rm -rf types && tsc -p .",
43
+ "prepublishOnly": "npm run build:types",
31
44
  "test": "ls test/*.test.js | xargs -P1 -n1 brittle-node"
32
45
  },
33
46
  "dependencies": {
34
- "@cero-base/cero": "^0.8.4",
35
- "@cero-base/core": "^0.8.4",
47
+ "@cero-base/cero": "^0.8.5",
48
+ "@cero-base/core": "^0.8.5",
36
49
  "b4a": "^1.8.1",
37
50
  "bare-net": "^2.3.1",
38
51
  "compact-encoding": "^3.1.0",
package/src/format.js CHANGED
@@ -1,6 +1,19 @@
1
1
  import b4a from 'b4a'
2
2
 
3
3
  const ROW_MAX = 80
4
+ const E = '\x1b['
5
+ const COLOR = !process.env.NO_COLOR && !!(process.stdout && process.stdout.isTTY)
6
+ const paint = (code) => (s) => (COLOR ? `${E}${code}m${s}${E}0m` : String(s))
7
+ const c = {
8
+ bold: paint(1),
9
+ dim: paint(2),
10
+ red: paint(31),
11
+ green: paint(32),
12
+ yellow: paint(33),
13
+ cyan: paint(36),
14
+ magenta: paint(35),
15
+ gray: paint(90)
16
+ }
4
17
 
5
18
  function shortRow(row) {
6
19
  if (row == null) return ''
@@ -15,15 +28,15 @@ function shortKey(key) {
15
28
  }
16
29
 
17
30
  /**
18
- * Render a handle tree (`[{ id, type }]`) as a short list.
31
+ * Render a handle tree (`[{ id, type }]`) as a short colored list.
19
32
  *
20
33
  * @param {Array<{ id: string, type: string | null }>} tree
21
34
  * @returns {string}
22
35
  */
23
36
  export function formatHandles(tree) {
24
- if (!tree || !tree.length) return 'handles: (none)'
25
- const lines = tree.map((h) => ` ${h.type || 'root'} ${h.id}`)
26
- return `handles (${tree.length}):\n${lines.join('\n')}`
37
+ if (!tree || !tree.length) return c.dim('handles: (none)')
38
+ const lines = tree.map((h) => ` ${c.cyan(h.type || 'root')} ${c.gray(h.id)}`)
39
+ return `${c.bold(`handles (${tree.length})`)}\n${lines.join('\n')}`
27
40
  }
28
41
 
29
42
  /**
@@ -38,26 +51,40 @@ export function formatState(ref, result) {
38
51
  const data = result?.data
39
52
  if (Array.isArray(data)) {
40
53
  const total = result.total ?? data.length
41
- const head = data.slice(0, 3).map(shortRow)
42
- return `${ref} (${total}):\n${head.map((r) => ' ' + r).join('\n')}`
54
+ const head = data.slice(0, 3).map((r) => ' ' + c.dim(shortRow(r)))
55
+ return `${c.cyan(ref)} ${c.gray(`(${total})`)}\n${head.join('\n')}`
43
56
  }
44
- return `${ref}: ${shortRow(data)}`
57
+ return `${c.cyan(ref)} ${c.dim(shortRow(data))}`
45
58
  }
46
59
 
47
60
  /**
48
- * Render one applied-op event as a single line.
61
+ * Render a failed ref lookup as one line (so the CLI keeps going).
62
+ *
63
+ * @param {string} ref
64
+ * @param {{ message?: string }} err
65
+ * @returns {string}
66
+ */
67
+ export function formatError(ref, err) {
68
+ return `${c.red('✗')} ${c.cyan(ref)} ${c.dim(err.message || String(err))}`
69
+ }
70
+
71
+ const OP_COLOR = { add: c.green, set: c.yellow, del: c.red, claim: c.magenta }
72
+
73
+ /**
74
+ * Render one applied-op event as a single colored line.
49
75
  *
50
76
  * @param {{ op: string, name: string, row: any, writerKey?: any, seq?: number }} e
51
77
  * @returns {string}
52
78
  */
53
79
  export function formatEvent(e) {
54
80
  const writer = shortKey(e.writerKey)
55
- const tail = writer ? ` (${writer})` : ''
56
- return `#${e.seq ?? '?'} ${e.op} ${e.name} ${shortRow(e.row)}${tail}`
81
+ const tail = writer ? c.gray(` ${writer}`) : ''
82
+ const op = (OP_COLOR[e.op] || c.cyan)(e.op)
83
+ return `${c.gray('#' + (e.seq ?? '?'))} ${op} ${c.cyan(e.name)} ${c.dim(shortRow(e.row))}${tail}`
57
84
  }
58
85
 
59
86
  /**
60
- * Render a stats snapshot as a single line.
87
+ * Render a stats snapshot as a single colored line.
61
88
  *
62
89
  * @param {{ network?: { connections: number, peers: number }, bee?: { local: number }, cores?: any[] }} s
63
90
  * @returns {string}
@@ -67,5 +94,5 @@ export function formatStats(s) {
67
94
  const peers = s?.network?.peers ?? 0
68
95
  const local = s?.bee?.local ?? 0
69
96
  const cores = s?.cores?.length ?? 0
70
- return `net ${conns}c/${peers}p · bee ${local} · cores ${cores}`
97
+ return `${c.gray('net')} ${c.cyan(conns + 'c')}/${c.cyan(peers + 'p')} ${c.gray('bee')} ${c.cyan(local)} ${c.gray('cores')} ${c.cyan(cores)}`
71
98
  }
package/src/tap.js CHANGED
@@ -1,14 +1,16 @@
1
1
  import { stats } from './stats.js'
2
2
  import { serve } from './server.js'
3
3
  import { redact } from './redact.js'
4
+ import { loopback } from './transport.js'
4
5
 
5
6
  /**
6
7
  * `cero.use(devtools())` tap extension. Bound to the root handle's lifecycle,
7
8
  * it feeds every applied op into a bounded ring buffer, samples `stats(me)` on
8
- * an interval, and — if given a `transport` — serves a read-only inspection
9
- * surface to out-of-band consumers. Nothing here touches the app swarm.
9
+ * an interval, and serves a read-only inspection surface to out-of-band
10
+ * consumers. By default it serves a loopback TCP server on `port` (9111); pass
11
+ * a custom `transport` to serve it some other way. Nothing touches the app swarm.
10
12
  *
11
- * @param {{ bufferSize?: number, sampleInterval?: number, redact?: ((ref: string, row: any) => any) | { fields?: string[], match?: Function, deny?: RegExp | false }, transport?: { accept(handler: (stream: any) => void): void } }} [opts]
13
+ * @param {{ port?: number, host?: string, bufferSize?: number, sampleInterval?: number, redact?: ((ref: string, row: any) => any) | { fields?: string[], match?: Function, deny?: RegExp | false }, transport?: { accept(handler: (stream: any) => void): void } }} [opts]
12
14
  * @returns {{ setup(me: any): () => void }}
13
15
  */
14
16
  export function devtools(opts = {}) {
@@ -17,6 +19,7 @@ export function devtools(opts = {}) {
17
19
  const events = new Ring(opts.bufferSize ?? 1000)
18
20
  const sampler = new Sampler(me, opts.sampleInterval ?? 1000)
19
21
  const redactor = makeRedactor(opts.redact)
22
+ const transport = opts.transport || loopback({ port: opts.port ?? 9111, host: opts.host })
20
23
  const offs = []
21
24
  const servers = new Set()
22
25
 
@@ -32,19 +35,18 @@ export function devtools(opts = {}) {
32
35
  for (const child of me.children) follow(child)
33
36
  me.on('handle', (child) => follow(child), { signal: me.signal })
34
37
 
35
- if (opts.transport) {
36
- opts.transport.accept((stream) => {
37
- const server = serve(stream, me, { events, stats: sampler, redact: redactor })
38
- servers.add(server)
39
- stream.on('close', () => servers.delete(server))
40
- })
41
- }
38
+ transport.accept((stream) => {
39
+ const server = serve(stream, me, { events, stats: sampler, redact: redactor })
40
+ servers.add(server)
41
+ stream.on('close', () => servers.delete(server))
42
+ })
42
43
 
43
44
  return () => {
44
45
  sampler.stop()
45
46
  for (const off of offs) off()
46
47
  for (const server of servers) server.close()
47
48
  servers.clear()
49
+ if (!opts.transport && transport.close) transport.close() // close the loopback we opened
48
50
  }
49
51
  }
50
52
  }
package/src/transport.js CHANGED
@@ -10,18 +10,27 @@ import net from 'net'
10
10
  */
11
11
  export function loopback(opts = {}) {
12
12
  const host = opts.host ?? '127.0.0.1'
13
+ const base = opts.port ?? 0
13
14
  let server = null
14
15
  return {
15
16
  accept(handler) {
16
17
  server = net.createServer((socket) => handler(socket))
17
- server.listen(opts.port ?? 0, host)
18
+ let port = base
19
+ let tries = 0
20
+ // A taken port (e.g. a second app instance) bumps to the next one rather
21
+ // than crashing the app — devtools is never essential.
22
+ server.on('error', (err) => {
23
+ if (err.code === 'EADDRINUSE' && base && tries++ < 10) server.listen(++port, host)
24
+ else console.error(`[devtools] tap could not start: ${err.message}`)
25
+ })
26
+ server.on('listening', () => console.log(banner(host, server.address().port)))
27
+ server.listen(port, host)
18
28
  },
19
29
  ready() {
20
30
  return new Promise((resolve, reject) => {
21
31
  if (!server) return reject(new Error('accept() not called'))
22
32
  if (server.listening) return resolve({ port: server.address().port })
23
33
  server.once('listening', () => resolve({ port: server.address().port }))
24
- server.once('error', reject)
25
34
  })
26
35
  },
27
36
  get port() {
@@ -43,3 +52,18 @@ export function loopback(opts = {}) {
43
52
  export function dial(opts = {}) {
44
53
  return net.connect(opts.port, opts.host ?? '127.0.0.1')
45
54
  }
55
+
56
+ // VITE-style startup banner printed when the tap starts listening.
57
+ function banner(host, port) {
58
+ const e = '\x1b['
59
+ const r = `${e}0m`
60
+ const dim = `${e}2m`
61
+ const bold = `${e}1m`
62
+ const cyan = `${e}36m`
63
+ const green = `${e}32m`
64
+ return (
65
+ `\n ${bold}${cyan}cero devtools${r} ${dim}tap ready${r}\n\n` +
66
+ ` ${green}➜${r} ${bold}Listening${r}: ${cyan}${host}:${port}${r}\n` +
67
+ ` ${green}➜${r} ${bold}Connect${r}: ${dim}cero-tools ${port}${r}\n`
68
+ )
69
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Consumer SDK for a tap server reached over a local Duplex `stream`. Returns
3
+ * a session exposing read methods (`handles`/`get`/`count`) and stream methods
4
+ * (`watch`/`events`/`stats`). Stream methods return a streamx Readable that
5
+ * cancels its server-side source when destroyed.
6
+ *
7
+ * @param {any} stream A streamx Duplex connected to a tap server.
8
+ * @returns {Promise<{ handles(): Promise<any>, get(ref: string, query?: any, handleId?: string): Promise<any>, count(ref: string, query?: any, handleId?: string): Promise<any>, watch(ref: string, query?: any, handleId?: string): import('streamx').Readable, events(): import('streamx').Readable, stats(): import('streamx').Readable, close(): void }>}
9
+ */
10
+ export function connect(stream: any): Promise<{
11
+ handles(): Promise<any>;
12
+ get(ref: string, query?: any, handleId?: string): Promise<any>;
13
+ count(ref: string, query?: any, handleId?: string): Promise<any>;
14
+ watch(ref: string, query?: any, handleId?: string): any;
15
+ events(): any;
16
+ stats(): any;
17
+ close(): void;
18
+ }>;
19
+ export class Session {
20
+ constructor(stream: any);
21
+ stream: any;
22
+ pending: Map<any, any>;
23
+ streams: Map<any, any>;
24
+ seq: number;
25
+ wire: Framed;
26
+ _onMessage(msg: any): void;
27
+ _newId(): number;
28
+ _request(method: any, fields: any): Promise<any>;
29
+ _stream(method: any, fields: any): any;
30
+ handles(): Promise<any>;
31
+ get(ref: any, query: any, handleId: any): Promise<any>;
32
+ count(ref: any, query: any, handleId: any): Promise<any>;
33
+ watch(ref: any, query: any, handleId: any): any;
34
+ events(): any;
35
+ stats(): any;
36
+ close(): void;
37
+ }
38
+ import { Framed } from './protocol.js';
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Render a handle tree (`[{ id, type }]`) as a short colored list.
3
+ *
4
+ * @param {Array<{ id: string, type: string | null }>} tree
5
+ * @returns {string}
6
+ */
7
+ export function formatHandles(tree: Array<{
8
+ id: string;
9
+ type: string | null;
10
+ }>): string;
11
+ /**
12
+ * Render a `get()` reply — `{ data, total, size }` for collections or
13
+ * `{ data }` for single refs — as a compact summary.
14
+ *
15
+ * @param {string} ref
16
+ * @param {{ data: any, total?: number, size?: number }} result
17
+ * @returns {string}
18
+ */
19
+ export function formatState(ref: string, result: {
20
+ data: any;
21
+ total?: number;
22
+ size?: number;
23
+ }): string;
24
+ /**
25
+ * Render a failed ref lookup as one line (so the CLI keeps going).
26
+ *
27
+ * @param {string} ref
28
+ * @param {{ message?: string }} err
29
+ * @returns {string}
30
+ */
31
+ export function formatError(ref: string, err: {
32
+ message?: string;
33
+ }): string;
34
+ /**
35
+ * Render one applied-op event as a single colored line.
36
+ *
37
+ * @param {{ op: string, name: string, row: any, writerKey?: any, seq?: number }} e
38
+ * @returns {string}
39
+ */
40
+ export function formatEvent(e: {
41
+ op: string;
42
+ name: string;
43
+ row: any;
44
+ writerKey?: any;
45
+ seq?: number;
46
+ }): string;
47
+ /**
48
+ * Render a stats snapshot as a single colored line.
49
+ *
50
+ * @param {{ network?: { connections: number, peers: number }, bee?: { local: number }, cores?: any[] }} s
51
+ * @returns {string}
52
+ */
53
+ export function formatStats(s: {
54
+ network?: {
55
+ connections: number;
56
+ peers: number;
57
+ };
58
+ bee?: {
59
+ local: number;
60
+ };
61
+ cores?: any[];
62
+ }): string;
@@ -0,0 +1,6 @@
1
+ export { stats } from "./stats.js";
2
+ export { redact } from "./redact.js";
3
+ export { devtools } from "./tap.js";
4
+ export { serve } from "./server.js";
5
+ export { connect } from "./connect.js";
6
+ export { loopback, dial } from "./transport.js";
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Length-prefixed JSON framing over a Duplex `stream`. Buffers incoming chunks
3
+ * and delivers each decoded frame to `onMessage`; `send` writes a framed frame.
4
+ */
5
+ export class Framed {
6
+ constructor(stream: any, onMessage: any);
7
+ stream: any;
8
+ onMessage: any;
9
+ buf: any;
10
+ _onData(chunk: any): void;
11
+ send(obj: any): void;
12
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Build a row masker for devtools. A field is masked when its dotted path is in
3
+ * `fields`, its name matches `deny` (default denylist above), or `match(key,
4
+ * value, path)` returns true. Masked values become a typed placeholder —
5
+ * `‹redacted:bytes(32)›` / `‹redacted:string›` — so the shape stays visible but
6
+ * the content is hidden. Everything else passes through untouched.
7
+ *
8
+ * @param {{ fields?: string[], deny?: RegExp | false, match?: (key: string, value: any, path: string) => boolean }} [config]
9
+ * @returns {(ref: string, row: any) => any}
10
+ */
11
+ export function redact(config?: {
12
+ fields?: string[];
13
+ deny?: RegExp | false;
14
+ match?: (key: string, value: any, path: string) => boolean;
15
+ }): (ref: string, row: any) => any;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Read-only inspection server over a local Duplex `stream`. Serves the live
3
+ * handle tree rooted at `me`, exposing only MAIN refs declared in each
4
+ * handle's `spec.meta.refs` — local secret refs (master/keypair/
5
+ * handle-keypairs) are never reachable, since they live in `meta.local.refs`.
6
+ *
7
+ * @param {any} stream A streamx Duplex carrying length-prefixed JSON frames.
8
+ * @param {any} me The live root cero handle.
9
+ * @param {{ events?: { snapshot(): any, subscribe(fn: (e: any) => void): () => void }, stats?: { snapshot(): any, subscribe(fn: (s: any) => void): () => void }, redact?: (ref: string, row: any) => any }} [opts]
10
+ * @returns {{ close(): void }}
11
+ */
12
+ export function serve(stream: any, me: any, opts?: {
13
+ events?: {
14
+ snapshot(): any;
15
+ subscribe(fn: (e: any) => void): () => void;
16
+ };
17
+ stats?: {
18
+ snapshot(): any;
19
+ subscribe(fn: (s: any) => void): () => void;
20
+ };
21
+ redact?: (ref: string, row: any) => any;
22
+ }): {
23
+ close(): void;
24
+ };
25
+ export class TapServer {
26
+ constructor(stream: any, me: any, opts?: {});
27
+ stream: any;
28
+ me: any;
29
+ redact: any;
30
+ opts: {};
31
+ tracked: Map<any, any>;
32
+ wire: Framed;
33
+ _cleanup: () => void;
34
+ onRequest(req: any): Promise<void>;
35
+ handles(req: any): void;
36
+ get(req: any): Promise<void>;
37
+ count(req: any): Promise<void>;
38
+ watch(req: any): void;
39
+ events(req: any): void;
40
+ stats(req: any): void;
41
+ cancel(req: any): void;
42
+ _resolveRef(req: any): any;
43
+ _source(req: any, src: any): void;
44
+ _track(id: any, off: any): void;
45
+ _drop(id: any): void;
46
+ _send(obj: any): void;
47
+ _teardown(): void;
48
+ close(): void;
49
+ }
50
+ import { Framed } from './protocol.js';
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Read-only snapshot of a handle's existing p2p/storage counters. Pure: no
3
+ * listeners, no mutation, never reads a secret. `dht.stats` and the corestore
4
+ * `cores` map are undocumented internals, so they are guarded — a fresh handle
5
+ * (pre-ready) yields zeros rather than throwing.
6
+ *
7
+ * @param {any} handle A cero root or child handle.
8
+ * @param {number} [at] Sample timestamp, stamped by the caller.
9
+ * @returns {{ handleId: string, network: { connections: number, peers: number, dht: any }, bee: { local: number }, cores: Array<{ length: number, byteLength: number, peers: number }>, at: number }}
10
+ */
11
+ export function stats(handle: any, at?: number): {
12
+ handleId: string;
13
+ network: {
14
+ connections: number;
15
+ peers: number;
16
+ dht: any;
17
+ };
18
+ bee: {
19
+ local: number;
20
+ };
21
+ cores: Array<{
22
+ length: number;
23
+ byteLength: number;
24
+ peers: number;
25
+ }>;
26
+ at: number;
27
+ };
package/types/tap.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * `cero.use(devtools())` tap extension. Bound to the root handle's lifecycle,
3
+ * it feeds every applied op into a bounded ring buffer, samples `stats(me)` on
4
+ * an interval, and serves a read-only inspection surface to out-of-band
5
+ * consumers. By default it serves a loopback TCP server on `port` (9111); pass
6
+ * a custom `transport` to serve it some other way. Nothing touches the app swarm.
7
+ *
8
+ * @param {{ port?: number, host?: string, bufferSize?: number, sampleInterval?: number, redact?: ((ref: string, row: any) => any) | { fields?: string[], match?: Function, deny?: RegExp | false }, transport?: { accept(handler: (stream: any) => void): void } }} [opts]
9
+ * @returns {{ setup(me: any): () => void }}
10
+ */
11
+ export function devtools(opts?: {
12
+ port?: number;
13
+ host?: string;
14
+ bufferSize?: number;
15
+ sampleInterval?: number;
16
+ redact?: ((ref: string, row: any) => any) | {
17
+ fields?: string[];
18
+ match?: Function;
19
+ deny?: RegExp | false;
20
+ };
21
+ transport?: {
22
+ accept(handler: (stream: any) => void): void;
23
+ };
24
+ }): {
25
+ setup(me: any): () => void;
26
+ };
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Loopback TCP transport implementing the tap `{ accept(handler) }` contract
3
+ * plus lifecycle. Each inbound connection's socket is a streamx-compatible
4
+ * Duplex, ready to pass straight to `serve()`.
5
+ *
6
+ * @param {{ port?: number, host?: string }} [opts]
7
+ * @returns {{ accept(handler: (socket: any) => void): void, ready(): Promise<{ port: number }>, port: number | null, close(): void }}
8
+ */
9
+ export function loopback(opts?: {
10
+ port?: number;
11
+ host?: string;
12
+ }): {
13
+ accept(handler: (socket: any) => void): void;
14
+ ready(): Promise<{
15
+ port: number;
16
+ }>;
17
+ port: number | null;
18
+ close(): void;
19
+ };
20
+ /**
21
+ * Dial a loopback tap server and return the connected socket, ready for
22
+ * `connect()`.
23
+ *
24
+ * @param {{ port: number, host?: string }} [opts]
25
+ * @returns {any} A streamx-compatible Duplex socket.
26
+ */
27
+ export function dial(opts?: {
28
+ port: number;
29
+ host?: string;
30
+ }): any;