@aidra/pmx-io 0.0.0-stage → 9.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +69 -2
  2. package/bin/pmx-io-node.js +143 -0
  3. package/index.d.ts +381 -0
  4. package/index.js +27 -0
  5. package/index.mjs +47 -0
  6. package/lib/actions.js +120 -0
  7. package/lib/activate.js +402 -0
  8. package/lib/application.js +106 -0
  9. package/lib/bun.js +155 -0
  10. package/lib/config.js +67 -0
  11. package/lib/constants.js +17 -0
  12. package/lib/debug.js +48 -0
  13. package/lib/esm-hooks.mjs +111 -0
  14. package/lib/exit.js +79 -0
  15. package/lib/features/metrics.js +409 -0
  16. package/lib/features/notify.js +358 -0
  17. package/lib/features/profiling.js +208 -0
  18. package/lib/hooks.js +151 -0
  19. package/lib/inbound.js +174 -0
  20. package/lib/integrations.js +372 -0
  21. package/lib/itg/amqplib.js +169 -0
  22. package/lib/itg/bullmq.js +141 -0
  23. package/lib/itg/common.js +657 -0
  24. package/lib/itg/dbcomment.js +114 -0
  25. package/lib/itg/index.js +103 -0
  26. package/lib/itg/ioredis.js +212 -0
  27. package/lib/itg/kafkajs.js +205 -0
  28. package/lib/itg/mongodb.js +322 -0
  29. package/lib/itg/mysql2.js +205 -0
  30. package/lib/itg/nats.js +531 -0
  31. package/lib/itg/noderedis.js +236 -0
  32. package/lib/itg/pg.js +253 -0
  33. package/lib/itg/pgqueue.js +305 -0
  34. package/lib/itg/postgres.js +252 -0
  35. package/lib/itg/redis-common.js +265 -0
  36. package/lib/itg/sql.js +302 -0
  37. package/lib/metrics/service.js +286 -0
  38. package/lib/metrics/types.js +342 -0
  39. package/lib/modules.js +260 -0
  40. package/lib/node-options.js +62 -0
  41. package/lib/otel/bridge.js +797 -0
  42. package/lib/otel/env.js +114 -0
  43. package/lib/outbound.js +362 -0
  44. package/lib/pmx.js +757 -0
  45. package/lib/relay/hub.js +279 -0
  46. package/lib/relay/transport.js +253 -0
  47. package/lib/standalone/activate.js +307 -0
  48. package/lib/standalone/capture.js +55 -0
  49. package/lib/standalone/container.js +909 -0
  50. package/lib/standalone/transport.js +1023 -0
  51. package/lib/tracing/context.js +90 -0
  52. package/lib/tracing/control.js +124 -0
  53. package/lib/tracing/exporter.js +115 -0
  54. package/lib/tracing/index.js +242 -0
  55. package/lib/tracing/logs.js +176 -0
  56. package/lib/tracing/options.js +105 -0
  57. package/lib/tracing/paths.js +34 -0
  58. package/lib/tracing/tracer.js +850 -0
  59. package/lib/tracing/w3c.js +415 -0
  60. package/lib/transport/ipc.js +97 -0
  61. package/lib/transport/ws.js +511 -0
  62. package/lib/util.js +109 -0
  63. package/package.json +48 -4
  64. package/preload.cjs +26 -0
  65. package/preload.mjs +25 -0
  66. package/standalone.cjs +16 -0
  67. package/standalone.mjs +17 -0
@@ -0,0 +1,279 @@
1
+ // Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
2
+ 'use strict'
3
+
4
+ // The root's local hub (80 NIO-263, NIO-291; Part G.5 relay protocol version 1).
5
+ //
6
+ // Node descendants (cluster workers of an app that is its own primary, child_process.fork/spawn
7
+ // of node or bun) connect and speak the relay protocol: UTF-8 JSON objects, one per line. On
8
+ // Linux the hub is an abstract Unix socket, written `@pmx-io-<pid>-<16 hex>` in PMX_IO_HUB (the
9
+ // `@` stands for the NUL byte); elsewhere a socket in a new 0700 temporary directory. The first
10
+ // line of a connection must be the hello `{"__pmx_relay__":"hello","pid":…,"token":…,"rt":"node",
11
+ // "v":1}` within 2 s, with the token compared in constant time; a hello of another runtime closes
12
+ // the connection (NIO-296). At most 32 connections may await their hello. Node has no API for
13
+ // the peer's credentials, so the uid check of PYI-153 is not made (the token guards the socket).
14
+ // The listener and every connection are unref'd: the hub never keeps the app alive.
15
+
16
+ const net = require('net')
17
+ const fs = require('fs')
18
+ const path = require('path')
19
+ const debug = require('../debug')('axm:relay')
20
+
21
+ const RELAY_KEY = '__pmx_relay__'
22
+ const HELLO_TIMEOUT = 2000
23
+ const MAX_HELLO = 4096
24
+ const MAX_PENDING = 32
25
+ const MAX_LINE = 16 * 1024 * 1024
26
+
27
+ /** n random bytes as hex; /dev/urandom first (loading crypto costs ~5 ms at start-up). */
28
+ function randomHex (n) {
29
+ try {
30
+ const buf = Buffer.alloc(n)
31
+ const fd = fs.openSync('/dev/urandom', 'r')
32
+ try {
33
+ if (fs.readSync(fd, buf, 0, n, null) === n) return buf.toString('hex')
34
+ } finally {
35
+ fs.closeSync(fd)
36
+ }
37
+ } catch (_) {}
38
+ return require('crypto').randomBytes(n).toString('hex')
39
+ }
40
+
41
+ function newToken () {
42
+ return randomHex(16)
43
+ }
44
+
45
+ /** `@name` → the abstract socket path; anything else unchanged. */
46
+ function socketPath (addr) {
47
+ return typeof addr === 'string' && addr.startsWith('@') ? '\0' + addr.slice(1) : addr
48
+ }
49
+
50
+ function helloLine (pid, token) {
51
+ const o = { [RELAY_KEY]: 'hello', pid }
52
+ if (token !== undefined) o.token = token
53
+ o.rt = 'node'
54
+ o.v = 1
55
+ return JSON.stringify(o) + '\n'
56
+ }
57
+
58
+ function tokenOk (got, want) {
59
+ if (typeof got !== 'string' || typeof want !== 'string') return false
60
+ const a = Buffer.from(got, 'utf8')
61
+ const b = Buffer.from(want, 'utf8')
62
+ const crypto = require('crypto')
63
+ if (a.length !== b.length) {
64
+ crypto.timingSafeEqual(b, b)
65
+ return false
66
+ }
67
+ return crypto.timingSafeEqual(a, b)
68
+ }
69
+
70
+ /** The pid of a valid hello line for this hub, else null (and the reason). */
71
+ function checkHello (line, token) {
72
+ let msg
73
+ try {
74
+ msg = JSON.parse(line)
75
+ } catch (_) {
76
+ return { pid: null, why: 'not JSON' }
77
+ }
78
+ if (!msg || typeof msg !== 'object' || msg[RELAY_KEY] !== 'hello') return { pid: null, why: 'not a hello' }
79
+ if (!tokenOk(msg.token, token)) return { pid: null, why: 'bad token' }
80
+ const rt = msg.rt === undefined ? 'python' : msg.rt
81
+ if (rt !== 'node') return { pid: null, why: 'runtime ' + rt }
82
+ if (!Number.isInteger(msg.pid) || msg.pid <= 0) return { pid: null, why: 'bad pid' }
83
+ return { pid: msg.pid, why: null }
84
+ }
85
+
86
+ /** Split a stream into lines; calls onLine(text) for each complete line. */
87
+ function lineReader (onLine, onTooLong) {
88
+ let buf = ''
89
+ return (chunk) => {
90
+ buf += typeof chunk === 'string' ? chunk : chunk.toString('utf8')
91
+ let nl
92
+ while ((nl = buf.indexOf('\n')) !== -1) {
93
+ const line = buf.slice(0, nl)
94
+ buf = buf.slice(nl + 1)
95
+ if (line) onLine(line)
96
+ }
97
+ if (buf.length > MAX_LINE) {
98
+ buf = ''
99
+ if (onTooLong) onTooLong()
100
+ }
101
+ }
102
+ }
103
+
104
+ class Hub {
105
+ /**
106
+ * @param {(msg: object, pid: number) => void} onMessage relay message (not hellos)
107
+ * @param {{token?: string}} [opts]
108
+ */
109
+ constructor (onMessage, opts) {
110
+ this.onMessage = onMessage
111
+ this.token = (opts && opts.token) || newToken()
112
+ this.address = ''
113
+ this._server = null
114
+ this._tmpdir = null
115
+ this._pending = 0
116
+ this._relays = new Map() // socket → Set of pids announced on it
117
+ this._sockets = new Set()
118
+ }
119
+
120
+ /** Start listening (synchronously bound); returns the PMX_IO_HUB value. */
121
+ start () {
122
+ let listenPath
123
+ // Abstract sockets: Linux, on Node >= 20 and Bun (Node 18's listen fails with EADDRINUSE on
124
+ // them); elsewhere a socket in a new 0700 temporary directory
125
+ const major = Number(String(process.versions.node || '0').split('.')[0])
126
+ if (process.platform === 'linux' && (major >= 20 || typeof process.versions.bun === 'string')) {
127
+ const name = `pmx-io-${process.pid}-${randomHex(8)}`
128
+ this.address = '@' + name
129
+ listenPath = '\0' + name
130
+ } else {
131
+ this._tmpdir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'pmx-io-'))
132
+ fs.chmodSync(this._tmpdir, 0o700)
133
+ listenPath = path.join(this._tmpdir, 'hub.sock')
134
+ this.address = listenPath
135
+ // the directory and its socket file outlive the process unless removed at exit
136
+ this._onExit = () => this._removeTmpdir()
137
+ process.on('exit', this._onExit)
138
+ }
139
+ const server = net.createServer({ pauseOnConnect: false }, (sock) => this._accept(sock))
140
+ server.on('error', (err) => debug('hub error: %s', err && err.message))
141
+ this._server = server
142
+ // The address is chosen (and exported) now; binding waits until the app's first line has
143
+ // run (NIO-024): a descendant needs tens of milliseconds to start and load its preload.
144
+ const listen = () => {
145
+ if (this._server !== server) return
146
+ // exclusive: a cluster worker (pmx cluster mode) binds itself instead of asking its primary
147
+ server.listen({ path: listenPath, exclusive: true })
148
+ server.unref()
149
+ }
150
+ if (this._deferListen === false) listen()
151
+ else {
152
+ const t = setImmediate(listen)
153
+ if (t.unref) t.unref()
154
+ }
155
+ return this.address
156
+ }
157
+
158
+ _accept (sock) {
159
+ if (sock.unref) sock.unref()
160
+ if (this._pending >= MAX_PENDING) {
161
+ debug('hub: %d connections await a hello, closing a new one', this._pending)
162
+ sock.destroy()
163
+ return
164
+ }
165
+ this._pending++
166
+ let helloDone = false
167
+ let buf = ''
168
+ const timer = setTimeout(() => {
169
+ debug('hub: no hello within %d ms, closing', HELLO_TIMEOUT)
170
+ sock.destroy()
171
+ }, HELLO_TIMEOUT)
172
+ if (timer.unref) timer.unref()
173
+ const settle = () => {
174
+ if (!helloDone) {
175
+ helloDone = true
176
+ this._pending--
177
+ clearTimeout(timer)
178
+ }
179
+ }
180
+ // decode as a stream: a multi-byte UTF-8 character split across two chunks stays intact
181
+ sock.setEncoding('utf8')
182
+ sock.on('error', () => {})
183
+ sock.on('close', () => {
184
+ settle()
185
+ this._relays.delete(sock)
186
+ this._sockets.delete(sock)
187
+ })
188
+ const onData = (chunk) => {
189
+ buf += chunk
190
+ const nl = buf.indexOf('\n')
191
+ if (nl === -1) {
192
+ if (buf.length > MAX_HELLO) sock.destroy()
193
+ return
194
+ }
195
+ const res = checkHello(buf.slice(0, nl), this.token)
196
+ settle()
197
+ if (res.pid === null) {
198
+ debug('hub: invalid hello (%s), closing', res.why)
199
+ sock.destroy()
200
+ return
201
+ }
202
+ sock.removeListener('data', onData)
203
+ const pids = new Set([res.pid])
204
+ this._relays.set(sock, pids)
205
+ this._sockets.add(sock)
206
+ const reader = lineReader((line) => this._line(line, sock, pids), () => sock.destroy())
207
+ sock.on('data', reader)
208
+ const rest = buf.slice(nl + 1)
209
+ buf = ''
210
+ if (rest) reader(rest)
211
+ }
212
+ sock.on('data', onData)
213
+ }
214
+
215
+ _line (line, sock, pids) {
216
+ let msg
217
+ try {
218
+ msg = JSON.parse(line)
219
+ } catch (_) {
220
+ debug('hub: malformed line ignored')
221
+ return
222
+ }
223
+ if (!msg || typeof msg !== 'object') return
224
+ const kind = msg[RELAY_KEY]
225
+ if (kind === 'hello') {
226
+ // a relay forwards the hellos of the relays below it (no token)
227
+ if (Number.isInteger(msg.pid) && msg.pid > 0) pids.add(msg.pid)
228
+ return
229
+ }
230
+ if (kind === 'msg' || kind === 'metrics' || kind === 'log') {
231
+ try {
232
+ this.onMessage(msg, pids.values().next().value)
233
+ } catch (err) {
234
+ debug('hub: relay message failed: %s', err && err.message)
235
+ }
236
+ return
237
+ }
238
+ debug('hub: unknown relay message %s ignored', kind)
239
+ }
240
+
241
+ /** Pids of the live relays (footprint, NIO-263). */
242
+ relayPids () {
243
+ const out = []
244
+ for (const set of this._relays.values()) for (const p of set) if (!out.includes(p)) out.push(p)
245
+ return out
246
+ }
247
+
248
+ stop () {
249
+ if (this._server) {
250
+ try {
251
+ this._server.close()
252
+ } catch (_) {}
253
+ this._server = null
254
+ }
255
+ for (const s of this._sockets) {
256
+ try {
257
+ s.destroy()
258
+ } catch (_) {}
259
+ }
260
+ this._sockets.clear()
261
+ this._relays.clear()
262
+ this._removeTmpdir()
263
+ }
264
+
265
+ _removeTmpdir () {
266
+ if (this._onExit) {
267
+ process.removeListener('exit', this._onExit)
268
+ this._onExit = null
269
+ }
270
+ if (this._tmpdir) {
271
+ try {
272
+ fs.rmSync(this._tmpdir, { recursive: true, force: true })
273
+ } catch (_) {}
274
+ this._tmpdir = null
275
+ }
276
+ }
277
+ }
278
+
279
+ module.exports = { Hub, RELAY_KEY, socketPath, helloLine, checkHello, tokenOk, newToken, lineReader, HELLO_TIMEOUT, MAX_PENDING }
@@ -0,0 +1,253 @@
1
+ // Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
2
+ 'use strict'
3
+
4
+ // Relay transports (80 NIO-263, NIO-291, NIO-293; Part G.5): what an instance uses when it is not
5
+ // the root. It forwards exceptions (with metadata.pid, and metadata.thread_id from a worker
6
+ // thread), events and spans, metric contributions and captured log chunks, and nothing else:
7
+ // relays never run actions, never answer daemon messages and never open their own daemon or
8
+ // prodman connection.
9
+ // * HubRelayTransport: a Node descendant process → the root's hub socket.
10
+ // * ThreadRelayTransport: a worker thread → the main thread, over BroadcastChannel
11
+ // `pmx-io:<pid>` (relay message objects, no hello or token: the channel is process-local).
12
+
13
+ const net = require('net')
14
+ const { RELAY_KEY, socketPath, helloLine } = require('./hub')
15
+ const debug = require('../debug')('axm:relay')
16
+
17
+ const FORWARDED = ['process:exception', 'human:event', 'trace-span']
18
+ const MAX_QUEUE = 1000
19
+
20
+ function wrapMessage (type, data, extraMeta) {
21
+ if (!FORWARDED.includes(type)) return null
22
+ if (type === 'process:exception' && data && typeof data === 'object') {
23
+ const d = Object.assign({}, data)
24
+ const meta = Object.assign({}, d.metadata && typeof d.metadata === 'object' ? d.metadata : {})
25
+ if (meta.pid === undefined) meta.pid = process.pid
26
+ if (extraMeta) for (const k of Object.keys(extraMeta)) if (meta[k] === undefined) meta[k] = extraMeta[k]
27
+ d.metadata = meta
28
+ data = d
29
+ }
30
+ return { [RELAY_KEY]: 'msg', m: { type, data } }
31
+ }
32
+
33
+ class HubRelayTransport {
34
+ /**
35
+ * @param {string} hub PMX_IO_HUB
36
+ * @param {string} token PMX_IO_HUB_TOKEN
37
+ * @param {{rootPid?: string, onUnreachable?: Function}} [opts]
38
+ */
39
+ constructor (hub, token, opts) {
40
+ this.kind = 'relay'
41
+ this.hub = hub
42
+ this.token = token
43
+ this.opts = opts || {}
44
+ this._sock = null
45
+ this._queue = []
46
+ this._connected = false
47
+ this._failed = false
48
+ this._listeners = []
49
+ }
50
+
51
+ init () {
52
+ let sock
53
+ try {
54
+ sock = net.connect(socketPath(this.hub))
55
+ } catch (err) {
56
+ this._fail(err)
57
+ return this
58
+ }
59
+ this._sock = sock
60
+ if (sock.unref) sock.unref()
61
+ sock.on('connect', () => {
62
+ this._connected = true
63
+ sock.write(helloLine(process.pid, this.token))
64
+ const q = this._queue
65
+ this._queue = []
66
+ for (const [line, cb] of q) sock.write(line, cb)
67
+ })
68
+ sock.on('error', (err) => this._fail(err))
69
+ sock.on('close', () => {
70
+ this._connected = false
71
+ if (!this._failed) {
72
+ this._failed = true
73
+ debug('hub connection closed')
74
+ }
75
+ })
76
+ // the hub never sends anything to relays
77
+ sock.on('data', () => {})
78
+ return this
79
+ }
80
+
81
+ _fail (err) {
82
+ if (this._failed) return
83
+ this._failed = true
84
+ debug('hub %s not reachable: %s', this.hub, err && err.message)
85
+ const q = this._queue
86
+ this._queue = []
87
+ for (const [, cb] of q) if (cb) process.nextTick(cb, err)
88
+ if (this.opts.onUnreachable) {
89
+ try {
90
+ this.opts.onUnreachable(err)
91
+ } catch (_) {}
92
+ }
93
+ }
94
+
95
+ _write (obj, cb) {
96
+ if (this._failed) {
97
+ if (cb) process.nextTick(cb, new Error('hub not reachable'))
98
+ return false
99
+ }
100
+ let line
101
+ try {
102
+ line = JSON.stringify(obj) + '\n'
103
+ } catch (err) {
104
+ if (cb) process.nextTick(cb, err)
105
+ return false
106
+ }
107
+ if (!this._connected) {
108
+ if (this._queue.length >= MAX_QUEUE) this._queue.shift()
109
+ this._queue.push([line, cb])
110
+ return true
111
+ }
112
+ return this._sock.write(line, cb)
113
+ }
114
+
115
+ send (type, data, cb) {
116
+ const w = wrapMessage(type, data)
117
+ if (!w) {
118
+ if (cb) process.nextTick(cb)
119
+ return true
120
+ }
121
+ return this._write(w, cb)
122
+ }
123
+
124
+ sendRaw (msg, cb) {
125
+ if (cb) process.nextTick(cb)
126
+ return true
127
+ }
128
+
129
+ sendRelay (obj) {
130
+ return this._write(obj)
131
+ }
132
+
133
+ sendLog (stream, text) {
134
+ return this._write({ [RELAY_KEY]: 'log', s: stream === 'err' ? 'err' : 'out', d: text })
135
+ }
136
+
137
+ sendContributions (list) {
138
+ return this._write({ [RELAY_KEY]: 'metrics', c: list })
139
+ }
140
+
141
+ onMessage (fn) {
142
+ this._listeners.push(fn)
143
+ }
144
+
145
+ isConnected () {
146
+ return this._connected
147
+ }
148
+
149
+ destroy () {
150
+ if (this._sock) {
151
+ try {
152
+ this._sock.end()
153
+ } catch (_) {}
154
+ }
155
+ this._listeners = []
156
+ }
157
+ }
158
+
159
+ function channelName (pid) {
160
+ return 'pmx-io:' + (pid || process.pid)
161
+ }
162
+
163
+ class ThreadRelayTransport {
164
+ constructor () {
165
+ this.kind = 'thread-relay'
166
+ this._ch = null
167
+ this._threadId = undefined
168
+ this._listeners = []
169
+ }
170
+
171
+ init () {
172
+ const wt = require('worker_threads')
173
+ this._threadId = wt.threadId
174
+ this._ch = new wt.BroadcastChannel(channelName())
175
+ if (this._ch.unref) this._ch.unref()
176
+ return this
177
+ }
178
+
179
+ _post (obj) {
180
+ try {
181
+ this._ch.postMessage(obj)
182
+ return true
183
+ } catch (err) {
184
+ debug('thread relay post failed: %s', err && err.message)
185
+ return false
186
+ }
187
+ }
188
+
189
+ send (type, data, cb) {
190
+ const w = wrapMessage(type, data, { thread_id: this._threadId })
191
+ const ok = w ? this._post(w) : true
192
+ if (cb) process.nextTick(cb)
193
+ return ok
194
+ }
195
+
196
+ sendRaw (msg, cb) {
197
+ if (cb) process.nextTick(cb)
198
+ return true
199
+ }
200
+
201
+ sendLog (stream, text) {
202
+ return this._post({ [RELAY_KEY]: 'log', s: stream === 'err' ? 'err' : 'out', d: text })
203
+ }
204
+
205
+ sendContributions (list) {
206
+ return this._post({ [RELAY_KEY]: 'metrics', c: list })
207
+ }
208
+
209
+ onMessage (fn) {
210
+ this._listeners.push(fn)
211
+ }
212
+
213
+ isConnected () {
214
+ return true
215
+ }
216
+
217
+ destroy () {
218
+ if (this._ch) {
219
+ try {
220
+ this._ch.close()
221
+ } catch (_) {}
222
+ this._ch = null
223
+ }
224
+ }
225
+ }
226
+
227
+ /** Main-thread side: receive thread relays' messages. Returns a close function. */
228
+ function listenThreads (onMessage) {
229
+ let ch
230
+ try {
231
+ const wt = require('worker_threads')
232
+ ch = new wt.BroadcastChannel(channelName())
233
+ } catch (err) {
234
+ debug('thread relays unavailable: %s', err && err.message)
235
+ return () => {}
236
+ }
237
+ if (ch.unref) ch.unref()
238
+ ch.onmessage = (ev) => {
239
+ const msg = ev && ev.data
240
+ if (msg && typeof msg === 'object' && msg[RELAY_KEY]) {
241
+ try {
242
+ onMessage(msg, process.pid)
243
+ } catch (_) {}
244
+ }
245
+ }
246
+ return () => {
247
+ try {
248
+ ch.close()
249
+ } catch (_) {}
250
+ }
251
+ }
252
+
253
+ module.exports = { HubRelayTransport, ThreadRelayTransport, listenThreads, wrapMessage, channelName, FORWARDED }