@aidra/pmx-io 0.0.0-stage → 9.13.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,307 @@
1
+ // Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
2
+ 'use strict'
3
+
4
+ // Standalone mode (80 B.15): activation (NIO-260), roots and relays (NIO-263), the root's setup
5
+ // (hub, capture, prodman connection, NIO-262..NIO-267), shutdown reasons (NIO-268) and isolation
6
+ // (NIO-269). Everything here is caught and logged under `axm:standalone`; nothing throws into the
7
+ // app and the app always runs.
8
+
9
+ const path = require('path')
10
+ const { PKG_DIR } = require('../util')
11
+ const debug = require('../debug')('axm:standalone')
12
+
13
+ const MISSING_LINE = '[@pm2/io] not reporting to prodman: %s not set (standalone mode needs PM2_PUBLIC_KEY, PM2_SECRET_KEY and PM2_APP_NAME)'
14
+ const UNREACHABLE_LINE = '[@pm2/io] not reporting: the standalone root (pid %s) is not reachable; start the server with exec'
15
+ const TREE_ENV = ['PMX_IO_STANDALONE', 'PMX_IO_ROOT_PID', 'PMX_IO_ROOT_PPID', 'PMX_IO_HUB', 'PMX_IO_HUB_TOKEN', 'PMX_IO_CAPTURE_LOGS']
16
+ const STANDALONE_CJS = path.join(PKG_DIR, 'standalone.cjs')
17
+ const GLOBAL = Symbol.for('@pmx/io:standalone')
18
+ const SIGNAL_MEMORY_MS = 10 * 60 * 1000
19
+ const SIGNAL_NUMBERS = { SIGHUP: 1, SIGINT: 2, SIGQUIT: 3, SIGTERM: 15 }
20
+
21
+ const printed = new Set()
22
+ function printOnce (line) {
23
+ if (printed.has(line)) return
24
+ printed.add(line)
25
+ try {
26
+ process.stderr.write(line + '\n')
27
+ } catch (_) {}
28
+ }
29
+
30
+ /** A process started by pmx/pm2 (or a descendant of one): the keys are the agent's (APM-001). */
31
+ function underPmx (env) {
32
+ return env.pm_id !== undefined && env.PMX_IO_STANDALONE !== '1'
33
+ }
34
+
35
+ function daemonChannel (env) {
36
+ return env.PMX_IO === '1' && typeof process.send === 'function'
37
+ }
38
+
39
+ function scrubTreeEnv (env) {
40
+ for (const k of TREE_ENV) delete env[k]
41
+ }
42
+
43
+ /** NIO-263: is this process the root of its tree? */
44
+ function isRoot (env) {
45
+ const pid = String(process.pid)
46
+ if (env.PMX_IO_ROOT_PID === pid) return true
47
+ if (env.PMX_IO_ROOT_PPID && env.PMX_IO_ROOT_PPID === String(process.ppid)) return true
48
+ return !env.PMX_IO_ROOT_PID && !env.PMX_IO_ROOT_PPID && !env.PMX_IO_HUB
49
+ }
50
+
51
+ /** Adds `--require <abs standalone.cjs>` to NODE_OPTIONS once (NIO-261). */
52
+ function ensureNodeOptions (env) {
53
+ const cur = env.NODE_OPTIONS || ''
54
+ if (cur.includes(STANDALONE_CJS)) return
55
+ const token = `--require "${STANDALONE_CJS.replace(/"/g, '\\"')}"`
56
+ env.NODE_OPTIONS = cur ? token + ' ' + cur : token
57
+ }
58
+
59
+ function childrenWanted (config) {
60
+ try {
61
+ return require('../tracing/options').childrenEnabled(config, process.env)
62
+ } catch (_) {
63
+ return true
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Decide what this process is (NIO-260, NIO-263). Returns
69
+ * { role: 'root', transport, handle } | { role: 'relay', factory } | null (inert).
70
+ */
71
+ function decide (config) {
72
+ const env = process.env
73
+ if (daemonChannel(env)) return null
74
+ if (underPmx(env)) {
75
+ debug('started by pmx (pm_id set): standalone mode not used')
76
+ return null
77
+ }
78
+ if (!isRoot(env)) {
79
+ const hub = env.PMX_IO_HUB
80
+ const rootPid = env.PMX_IO_ROOT_PID || env.PMX_IO_ROOT_PPID
81
+ if (!hub) {
82
+ if (rootPid) printOnce(UNREACHABLE_LINE.replace('%s', rootPid))
83
+ return null
84
+ }
85
+ const token = env.PMX_IO_HUB_TOKEN || ''
86
+ const { HubRelayTransport } = require('../relay/transport')
87
+ const factory = () => new HubRelayTransport(hub, token, {
88
+ onUnreachable: () => {
89
+ if (rootPid) printOnce(UNREACHABLE_LINE.replace('%s', rootPid))
90
+ }
91
+ })
92
+ return { role: 'relay', factory }
93
+ }
94
+ const container = require('./container')
95
+ const { resolveConfig } = require('./transport')
96
+ const info = container.globalInfo()
97
+ const cfg = resolveConfig(config && config.apmOptions, env, info)
98
+ if (cfg.missing.length) {
99
+ printOnce(MISSING_LINE.replace('%s', cfg.missing.join(', ')))
100
+ scrubTreeEnv(env)
101
+ return null
102
+ }
103
+ return { role: 'root', handle: setupRoot(cfg, info, config) }
104
+ }
105
+
106
+ // -- the root ---------------------------------------------------------------------------------------
107
+
108
+ let ROOT = null
109
+
110
+ function setupRoot (cfg, info, config) {
111
+ if (ROOT) return ROOT
112
+ const env = process.env
113
+ const { ProdmanTransport } = require('./transport')
114
+ env.PMX_IO_ROOT_PID = String(process.pid)
115
+ delete env.PMX_IO_ROOT_PPID
116
+ env.PMX_IO_STANDALONE = '1'
117
+ // NIO-262: descendants report through the hub and never need the bucket keys
118
+ delete env.PM2_SECRET_KEY
119
+ delete env.PM2_PUBLIC_KEY
120
+ const handle = {
121
+ transport: null,
122
+ hub: null,
123
+ capture: null,
124
+ closeThreads: null,
125
+ instance: null,
126
+ signal: null,
127
+ signalAt: 0,
128
+ crashed: false,
129
+ observers: new Map(),
130
+ destroyed: false
131
+ }
132
+ const children = childrenWanted(config)
133
+ if (children) {
134
+ ensureNodeOptions(env)
135
+ try {
136
+ const { Hub } = require('../relay/hub')
137
+ const hub = new Hub((msg) => onRelay(handle, msg))
138
+ env.PMX_IO_HUB = hub.start()
139
+ env.PMX_IO_HUB_TOKEN = hub.token
140
+ handle.hub = hub
141
+ } catch (err) {
142
+ debug('hub not started: %s', err && err.message)
143
+ }
144
+ try {
145
+ handle.closeThreads = require('../relay/transport').listenThreads((msg) => onRelay(handle, msg))
146
+ } catch (_) {}
147
+ } else {
148
+ delete env.PMX_IO_HUB
149
+ delete env.PMX_IO_HUB_TOKEN
150
+ }
151
+ env.PMX_IO_CAPTURE_LOGS = cfg.captureLogs
152
+ const transport = new ProdmanTransport(cfg, info, { relayPids: () => (handle.hub ? handle.hub.relayPids() : []) })
153
+ handle.transport = transport
154
+ if (cfg.captureLogs === 'stdio') {
155
+ try {
156
+ const { Capture } = require('./capture')
157
+ handle.capture = new Capture((stream, text) => transport.onLogChunk(stream, text))
158
+ handle.capture.start()
159
+ } catch (err) {
160
+ debug('log capture failed: %s', err && err.message)
161
+ }
162
+ }
163
+ installShutdown(handle)
164
+ handle.destroy = () => destroyRoot(handle)
165
+ Object.defineProperty(globalThis, GLOBAL, {
166
+ configurable: true,
167
+ enumerable: false,
168
+ value: { onUncaught: () => { handle.crashed = true }, handle }
169
+ })
170
+ transport.start()
171
+ ROOT = handle
172
+ return handle
173
+ }
174
+
175
+ function onRelay (handle, msg) {
176
+ const inst = handle.instance
177
+ if (inst && typeof inst._onRelayMessage === 'function') {
178
+ inst._onRelayMessage(msg)
179
+ } else if (msg && msg.__pmx_relay__ === 'log' && typeof msg.d === 'string') {
180
+ handle.transport.onLogChunk(msg.s === 'err' ? 'err' : 'out', msg.d)
181
+ }
182
+ }
183
+
184
+ // -- shutdown (NIO-268) -----------------------------------------------------------------------------
185
+
186
+ function exitReason (handle, code) {
187
+ if (handle.crashed) return { reason: 'crash', signal: null, planned: false }
188
+ if (handle.signal && Date.now() - handle.signalAt < SIGNAL_MEMORY_MS) return { reason: 'signal', signal: handle.signal, planned: true }
189
+ if (typeof code === 'number' && code !== 0) return { reason: 'crash', signal: null, planned: false }
190
+ return { reason: 'exit', signal: null, planned: true }
191
+ }
192
+
193
+ function installShutdown (handle) {
194
+ for (const sig of ['SIGTERM', 'SIGINT']) {
195
+ const observer = function pmxIoSignalObserver () {
196
+ if (handle.destroyed || process.pid !== Number(process.env.PMX_IO_ROOT_PID || process.pid)) return
197
+ handle.signal = sig
198
+ handle.signalAt = Date.now()
199
+ // the app's own exit hooks (onExit) run through exit.js's listener; ours only reports
200
+ const others = process.listeners(sig).filter((l) => l !== observer)
201
+ handle.transport.shutdown('signal', sig, true, 1000).then(() => {
202
+ if (others.length || process.listeners(sig).some((l) => l !== observer)) return
203
+ process.removeListener(sig, observer)
204
+ handle.observers.delete(sig)
205
+ if (process.pid === 1) process.exit(128 + SIGNAL_NUMBERS[sig])
206
+ else process.kill(process.pid, sig)
207
+ }, () => {})
208
+ }
209
+ try {
210
+ process.on(sig, observer)
211
+ handle.observers.set(sig, observer)
212
+ } catch (_) {}
213
+ }
214
+ handle.onExit = (code) => {
215
+ if (handle.destroyed) return
216
+ const r = exitReason(handle, code)
217
+ try {
218
+ handle.transport.shutdown(r.reason, r.signal, r.planned, 0)
219
+ } catch (_) {}
220
+ }
221
+ process.on('exit', handle.onExit)
222
+ }
223
+
224
+ function destroyRoot (handle) {
225
+ if (handle.destroyed) return
226
+ handle.destroyed = true
227
+ for (const [sig, fn] of handle.observers) process.removeListener(sig, fn)
228
+ handle.observers.clear()
229
+ if (handle.onExit) process.removeListener('exit', handle.onExit)
230
+ if (handle.capture) handle.capture.stop()
231
+ if (handle.hub) handle.hub.stop()
232
+ if (handle.closeThreads) handle.closeThreads()
233
+ try {
234
+ handle.transport.destroyAll()
235
+ } catch (_) {}
236
+ if (ROOT === handle) ROOT = null
237
+ }
238
+
239
+ // -- entry points -----------------------------------------------------------------------------------
240
+
241
+ /** Called from PMX.init when standalone mode is wanted (NIO-260 a/c). Returns a transport or null. */
242
+ function fromInit (instance, config) {
243
+ try {
244
+ const d = decide(config)
245
+ if (!d) return null
246
+ if (d.role === 'root') {
247
+ instance._mode = 'standalone'
248
+ instance._standalone = d.handle
249
+ d.handle.instance = instance
250
+ return d.handle.transport
251
+ }
252
+ instance._mode = 'relay'
253
+ instance._relayFactory = d.factory
254
+ startRelayCapture(instance)
255
+ return d.factory()
256
+ } catch (err) {
257
+ debug('activation failed: %s', err && err.message)
258
+ try {
259
+ process.stderr.write('[@pm2/io] standalone mode disabled: ' + (err && err.message) + '\n')
260
+ } catch (_) {}
261
+ return null
262
+ }
263
+ }
264
+
265
+ /** NIO-267: relays capture too (their chunks go to the root) when the root captures. */
266
+ function startRelayCapture (instance) {
267
+ if (process.env.PMX_IO_CAPTURE_LOGS !== 'stdio' || instance._relayCapture) return
268
+ const { Capture } = require('./capture')
269
+ instance._relayCapture = new Capture((stream, text) => {
270
+ const t = instance._transport
271
+ if (t && typeof t.sendLog === 'function') t.sendLog(stream, text)
272
+ })
273
+ instance._relayCapture.start()
274
+ }
275
+
276
+ /** The standalone preload entries (NIO-261 b): `@aidra/pmx-io/standalone` and `standalone.mjs`. */
277
+ function fromPreload () {
278
+ const env = process.env
279
+ const activate = require('../activate')
280
+ const s = activate.state()
281
+ if (s.active) return s
282
+ if (daemonChannel(env) || underPmx(env)) return null
283
+ if (env.pmx === 'false' || env.automation === 'false') return null
284
+ env.PMX_IO_STANDALONE = '1'
285
+ if (!env.PMX_IO_ROOT_PID && !env.PMX_IO_ROOT_PPID) env.PMX_IO_ROOT_PID = String(process.pid)
286
+ ensureNodeOptions(env)
287
+ const { GLOBAL_SYMBOL } = require('../constants')
288
+ if (Object.prototype.hasOwnProperty.call(globalThis, GLOBAL_SYMBOL)) return null
289
+ const { PMX } = require('../pmx')
290
+ let instance
291
+ if (!activate.isMainThread()) {
292
+ instance = new PMX({ mode: 'thread' })
293
+ s.role = 'thread'
294
+ } else {
295
+ instance = new PMX()
296
+ s.role = 'standalone'
297
+ }
298
+ s.instance = instance
299
+ s.active = true
300
+ activate.claimSingleton(instance)
301
+ instance.init({ standalone: true })
302
+ const conf = activate.secondInitConfig(env)
303
+ if (conf) instance.init(Object.assign({ standalone: true }, conf))
304
+ return s
305
+ }
306
+
307
+ module.exports = { decide, fromInit, fromPreload, isRoot, underPmx, ensureNodeOptions, exitReason, MISSING_LINE, UNREACHABLE_LINE, STANDALONE_CJS, TREE_ENV }
@@ -0,0 +1,55 @@
1
+ // Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
2
+ 'use strict'
3
+
4
+ // Log capture for standalone mode (80 NIO-267, `captureLogs: 'stdio'`): process.stdout.write and
5
+ // process.stderr.write are wrapped in the root and in relays. Output written by native addons or
6
+ // by child processes with inherited stdio directly to fds 1/2 is not captured (Node has no dup2,
7
+ // so Python's `fd` mode has no equivalent, Part G.1). Capture never throws, never blocks the
8
+ // writer and is re-entrancy guarded; the app's output is always written unchanged.
9
+
10
+ class Capture {
11
+ /** @param {(stream: 'out'|'err', text: string) => void} sink */
12
+ constructor (sink) {
13
+ this._sink = sink
14
+ this._restore = null
15
+ }
16
+
17
+ start () {
18
+ if (this._restore) return
19
+ const originals = {}
20
+ let busy = false
21
+ const sink = this._sink
22
+ for (const [type, stream] of [['out', process.stdout], ['err', process.stderr]]) {
23
+ if (!stream || typeof stream.write !== 'function') continue
24
+ const orig = stream.write
25
+ originals[type] = { stream, orig }
26
+ stream.write = function write (chunk, encoding) {
27
+ if (!busy) {
28
+ busy = true
29
+ try {
30
+ let text
31
+ if (typeof chunk === 'string') text = chunk
32
+ // the encoding argument applies to strings only: bytes are written as they are
33
+ else if (chunk instanceof Uint8Array) text = Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength).toString('utf8')
34
+ else text = String(chunk)
35
+ if (text) sink(type, text)
36
+ } catch (_) {
37
+ } finally {
38
+ busy = false
39
+ }
40
+ }
41
+ return orig.apply(this, arguments)
42
+ }
43
+ }
44
+ this._restore = () => {
45
+ for (const k of Object.keys(originals)) originals[k].stream.write = originals[k].orig
46
+ }
47
+ }
48
+
49
+ stop () {
50
+ if (this._restore) this._restore()
51
+ this._restore = null
52
+ }
53
+ }
54
+
55
+ module.exports = { Capture }