@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,415 @@
1
+ // Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
2
+ 'use strict'
3
+
4
+ // W3C Trace Context and Baggage (80 NIO-240..NIO-243, PYI-112/PYI-113, Part G.2): parsing,
5
+ // serialisation, the SpanContext value object, NEW_TRACE and the carriers of inject/extract.
6
+ // Pure functions: everything here works whether or not tracing is enabled.
7
+
8
+ const debug = require('../debug')('axm:tracing')
9
+
10
+ const BAGGAGE_MAX_ENTRIES = 64
11
+ const BAGGAGE_MAX_BYTES = 8192
12
+ const ZERO_TRACE = '0'.repeat(32)
13
+ const ZERO_SPAN = '0'.repeat(16)
14
+ const TRACEPARENT_RE = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})/
15
+ const HEX32 = /^[0-9a-f]{32}$/
16
+ const HEX16 = /^[0-9a-f]{16}$/
17
+ // RFC 7230 token (baggage keys)
18
+ const TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/
19
+ // tracestate is kept verbatim but never written when it holds a control character (G.2)
20
+ const CTL = /[\x00-\x1f\x7f]/ // eslint-disable-line no-control-regex
21
+
22
+ // ---------------------------------------------------------------------------------------------
23
+ // values
24
+
25
+ /** A header value as text: strings as is, Buffer/Uint8Array decoded as latin-1. */
26
+ function text (v) {
27
+ if (typeof v === 'string') return v
28
+ if (v instanceof Uint8Array) return Buffer.from(v.buffer, v.byteOffset, v.byteLength).toString('latin1')
29
+ return null
30
+ }
31
+
32
+ function trimWs (s) {
33
+ return s.replace(/^[ \t]+|[ \t]+$/g, '')
34
+ }
35
+
36
+ /**
37
+ * PYI-112: → { traceId, spanId, sampled } or null. Surrounding whitespace trimmed; lowercase hex
38
+ * only; version ff invalid; version 00 exact; higher versions lenient; all-zero ids invalid.
39
+ */
40
+ function parseTraceparent (value) {
41
+ let v = text(value)
42
+ if (v === null) return null
43
+ v = trimWs(v)
44
+ const m = TRACEPARENT_RE.exec(v)
45
+ if (!m) return null
46
+ const version = m[1]
47
+ if (version === 'ff') return null
48
+ const rest = v.slice(m[0].length)
49
+ if (version === '00') {
50
+ if (rest !== '') return null
51
+ } else if (rest !== '' && rest[0] !== '-') {
52
+ return null
53
+ }
54
+ if (m[2] === ZERO_TRACE || m[3] === ZERO_SPAN) return null
55
+ return { traceId: m[2], spanId: m[3], sampled: (parseInt(m[4], 16) & 1) === 1 }
56
+ }
57
+
58
+ // ---------------------------------------------------------------------------------------------
59
+ // baggage (PYI-113). Internally an ordered array of [key, value] pairs ("bag"), so insertion
60
+ // order survives keys that JavaScript objects would reorder (integer-like keys).
61
+
62
+ const EMPTY_BAG = Object.freeze([])
63
+
64
+ function utf8Len (s) {
65
+ return Buffer.byteLength(s, 'utf8')
66
+ }
67
+
68
+ /** Percent-decoding like Python's urllib.parse.unquote(errors='replace'). */
69
+ function unquote (s) {
70
+ if (s.indexOf('%') === -1) return s
71
+ const chunks = []
72
+ let run = ''
73
+ for (let i = 0; i < s.length; i++) {
74
+ if (s.charCodeAt(i) === 0x25 && i + 2 < s.length + 1 && /^[0-9a-fA-F]{2}$/.test(s.slice(i + 1, i + 3))) {
75
+ if (run) chunks.push(Buffer.from(run, 'utf8'))
76
+ run = ''
77
+ chunks.push(Buffer.from([parseInt(s.slice(i + 1, i + 3), 16)]))
78
+ i += 2
79
+ continue
80
+ }
81
+ run += s[i]
82
+ }
83
+ if (run) chunks.push(Buffer.from(run, 'utf8'))
84
+ return Buffer.concat(chunks).toString('utf8')
85
+ }
86
+
87
+ // baggage-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E ('%' is encoded too)
88
+ function safeOctet (c) {
89
+ return c >= 0x21 && c <= 0x7e && c !== 0x22 && c !== 0x2c && c !== 0x3b && c !== 0x5c && c !== 0x25
90
+ }
91
+
92
+ function quote (s) {
93
+ let plain = true
94
+ for (let i = 0; i < s.length; i++) {
95
+ if (!safeOctet(s.charCodeAt(i))) {
96
+ plain = false
97
+ break
98
+ }
99
+ }
100
+ if (plain) return s
101
+ let out = ''
102
+ for (const ch of s) {
103
+ const c = ch.charCodeAt(0)
104
+ if (ch.length === 1 && safeOctet(c)) {
105
+ out += ch
106
+ continue
107
+ }
108
+ for (const b of Buffer.from(ch, 'utf8')) out += '%' + (b < 16 ? '0' : '') + b.toString(16).toUpperCase()
109
+ }
110
+ return out
111
+ }
112
+
113
+ /** Parse one or several `baggage` header values → bag (ordered [k, v] pairs, frozen). */
114
+ function parseBaggage (values) {
115
+ if (values === undefined || values === null) return EMPTY_BAG
116
+ if (!Array.isArray(values)) values = [values]
117
+ const out = []
118
+ const index = new Map()
119
+ let size = 0
120
+ for (const raw of values) {
121
+ const t = text(raw)
122
+ if (!t) continue
123
+ for (const member of t.split(',')) {
124
+ const kv = member.split(';', 1)[0]
125
+ const eq = kv.indexOf('=')
126
+ if (eq === -1) continue
127
+ const k = trimWs(kv.slice(0, eq))
128
+ const v = trimWs(kv.slice(eq + 1))
129
+ if (!k || !TOKEN.test(k)) continue
130
+ const cost = k.length + 1 + utf8Len(v) + (out.length ? 1 : 0)
131
+ if (index.size >= BAGGAGE_MAX_ENTRIES || size + cost > BAGGAGE_MAX_BYTES) return Object.freeze(out)
132
+ size += cost
133
+ const val = unquote(v)
134
+ if (index.has(k)) {
135
+ out[index.get(k)] = [k, val]
136
+ } else {
137
+ index.set(k, out.length)
138
+ out.push([k, val])
139
+ }
140
+ }
141
+ }
142
+ return Object.freeze(out)
143
+ }
144
+
145
+ /** Serialise a bag in order, values percent-encoded (same limits as parsing). */
146
+ function formatBaggage (bag) {
147
+ const parts = []
148
+ let size = 0
149
+ for (const [k, v] of bag) {
150
+ if (typeof k !== 'string' || !TOKEN.test(k)) continue
151
+ const member = k + '=' + quote(typeof v === 'string' ? v : String(v))
152
+ const cost = member.length + (parts.length ? 1 : 0)
153
+ if (parts.length >= BAGGAGE_MAX_ENTRIES || size + cost > BAGGAGE_MAX_BYTES) break
154
+ size += cost
155
+ parts.push(member)
156
+ }
157
+ return parts.join(',')
158
+ }
159
+
160
+ /** bag + entries (existing keys replaced in place, new ones appended). */
161
+ function mergeBag (bag, entries) {
162
+ const out = bag.slice()
163
+ const pairs = entries instanceof Map ? Array.from(entries) : Array.isArray(entries) ? entries : Object.entries(entries || {})
164
+ for (const [k0, v0] of pairs) {
165
+ const k = String(k0)
166
+ const v = String(v0)
167
+ const i = out.findIndex((e) => e[0] === k)
168
+ if (i === -1) out.push([k, v])
169
+ else out[i] = [k, v]
170
+ }
171
+ return Object.freeze(out)
172
+ }
173
+
174
+ const objCache = new WeakMap()
175
+ /** The frozen plain-object view of a bag (NIO-243). */
176
+ function bagObject (bag) {
177
+ if (!bag || bag.length === 0) return EMPTY_OBJECT
178
+ let o = objCache.get(bag)
179
+ if (!o) {
180
+ o = {}
181
+ for (const [k, v] of bag) o[k] = v
182
+ Object.freeze(o)
183
+ objCache.set(bag, o)
184
+ }
185
+ return o
186
+ }
187
+ const EMPTY_OBJECT = Object.freeze({})
188
+
189
+ function toBag (baggage) {
190
+ if (!baggage) return EMPTY_BAG
191
+ if (Array.isArray(baggage) && Object.isFrozen(baggage) && baggage.every((e) => Array.isArray(e))) return baggage
192
+ return mergeBag(EMPTY_BAG, baggage)
193
+ }
194
+
195
+ // ---------------------------------------------------------------------------------------------
196
+ // SpanContext (NIO-240) and NEW_TRACE (NIO-247)
197
+
198
+ const BAG = Symbol('pmx.bag')
199
+
200
+ class SpanContext {
201
+ constructor (opts) {
202
+ const o = opts || {}
203
+ const { traceId, spanId } = o
204
+ if (typeof traceId !== 'string' || !HEX32.test(traceId) || traceId === ZERO_TRACE) {
205
+ throw new TypeError('traceId must be 32 lowercase hex digits, not all zero')
206
+ }
207
+ if (typeof spanId !== 'string' || !HEX16.test(spanId) || spanId === ZERO_SPAN) {
208
+ throw new TypeError('spanId must be 16 lowercase hex digits, not all zero')
209
+ }
210
+ const bag = o[BAG] || toBag(o.baggage)
211
+ Object.defineProperty(this, BAG, { value: bag, enumerable: false })
212
+ this.traceId = traceId
213
+ this.spanId = spanId
214
+ this.sampled = o.sampled === undefined ? true : Boolean(o.sampled)
215
+ this.traceState = typeof o.traceState === 'string' && o.traceState !== '' ? o.traceState : null
216
+ this.baggage = bagObject(bag)
217
+ this.isRemote = o.isRemote === undefined ? true : Boolean(o.isRemote)
218
+ Object.freeze(this)
219
+ }
220
+
221
+ traceparent () {
222
+ return `00-${this.traceId}-${this.spanId}-${this.sampled ? '01' : '00'}`
223
+ }
224
+
225
+ toJSON () {
226
+ return {
227
+ traceId: this.traceId,
228
+ spanId: this.spanId,
229
+ sampled: this.sampled,
230
+ traceState: this.traceState,
231
+ baggage: this.baggage,
232
+ isRemote: this.isRemote
233
+ }
234
+ }
235
+ }
236
+
237
+ /** Internal constructor that skips validation (ids generated by the tracer). */
238
+ function makeContext (traceId, spanId, sampled, traceState, bag, isRemote) {
239
+ return new SpanContext({ traceId, spanId, sampled, traceState, [BAG]: bag || EMPTY_BAG, isRemote })
240
+ }
241
+
242
+ function contextBag (ctx) {
243
+ return (ctx && ctx[BAG]) || EMPTY_BAG
244
+ }
245
+
246
+ function isSpanContext (v) {
247
+ return v instanceof SpanContext
248
+ }
249
+
250
+ const NEW_TRACE = Object.freeze({ toString: () => 'NEW_TRACE' })
251
+
252
+ // ---------------------------------------------------------------------------------------------
253
+ // carriers: extract (NIO-242)
254
+
255
+ function isPlainObject (v) {
256
+ if (v === null || typeof v !== 'object') return false
257
+ const p = Object.getPrototypeOf(v)
258
+ return p === Object.prototype || p === null
259
+ }
260
+
261
+ /** All values of header `name` (lowercase) in a carrier. */
262
+ function getAll (carrier, name) {
263
+ const out = []
264
+ const push = (v) => {
265
+ if (Array.isArray(v)) {
266
+ for (const x of v) out.push(x)
267
+ } else if (v !== undefined && v !== null) {
268
+ out.push(v)
269
+ }
270
+ }
271
+ if (carrier === null || carrier === undefined) return out
272
+ if (Array.isArray(carrier)) {
273
+ for (const item of carrier) {
274
+ if (!Array.isArray(item) || item.length < 2) continue
275
+ const k = text(item[0])
276
+ if (k !== null && k.toLowerCase() === name) push(item[1])
277
+ }
278
+ return out
279
+ }
280
+ if (carrier instanceof Map) {
281
+ for (const [k0, v] of carrier) {
282
+ const k = text(k0)
283
+ if (k !== null && k.toLowerCase() === name) push(v)
284
+ }
285
+ return out
286
+ }
287
+ if (isPlainObject(carrier)) {
288
+ for (const k of Object.keys(carrier)) {
289
+ if (k.toLowerCase() === name) push(carrier[k])
290
+ }
291
+ return out
292
+ }
293
+ if (typeof carrier.get === 'function') {
294
+ let v
295
+ try {
296
+ v = carrier.get(name)
297
+ } catch (_) {
298
+ v = undefined
299
+ }
300
+ push(v === null ? undefined : v)
301
+ return out
302
+ }
303
+ if (typeof carrier === 'object') {
304
+ for (const k of Object.keys(carrier)) {
305
+ if (k.toLowerCase() === name) push(carrier[k])
306
+ }
307
+ }
308
+ return out
309
+ }
310
+
311
+ function extract (carrier) {
312
+ try {
313
+ const tps = getAll(carrier, 'traceparent')
314
+ if (tps.length === 0) return null
315
+ const parsed = parseTraceparent(tps[0])
316
+ if (!parsed) return null
317
+ const states = getAll(carrier, 'tracestate').map(text).filter((s) => s)
318
+ const bag = parseBaggage(getAll(carrier, 'baggage'))
319
+ return makeContext(parsed.traceId, parsed.spanId, parsed.sampled, states.length ? states.join(',') : null, bag, true)
320
+ } catch (err) {
321
+ debug('extract failed: %s', err && err.message)
322
+ return null
323
+ }
324
+ }
325
+
326
+ // ---------------------------------------------------------------------------------------------
327
+ // carriers: inject (NIO-241)
328
+
329
+ /** The [name, value] pairs to write for a context and baggage (header hygiene, G.2). */
330
+ function headersFor (ctx, bag) {
331
+ const out = [['traceparent', ctx.traceparent()]]
332
+ if (ctx.traceState && !CTL.test(ctx.traceState)) out.push(['tracestate', ctx.traceState])
333
+ if (bag && bag.length) {
334
+ const b = formatBaggage(bag)
335
+ if (b) out.push(['baggage', b])
336
+ }
337
+ return out
338
+ }
339
+
340
+ function writeHeaders (carrier, headers, overwrite, asBytes) {
341
+ const val = (v) => (asBytes ? Buffer.from(v, 'utf8') : v)
342
+ if (Array.isArray(carrier)) {
343
+ for (const [name, value] of headers) {
344
+ const idx = []
345
+ for (let i = 0; i < carrier.length; i++) {
346
+ const item = carrier[i]
347
+ if (!Array.isArray(item)) continue
348
+ const k = text(item[0])
349
+ if (k !== null && k.toLowerCase() === name) idx.push(i)
350
+ }
351
+ if (idx.length && !overwrite) continue
352
+ if (idx.length) {
353
+ carrier[idx[0]] = [carrier[idx[0]][0], val(value)]
354
+ for (let j = idx.length - 1; j >= 1; j--) carrier.splice(idx[j], 1)
355
+ } else {
356
+ carrier.push([name, val(value)])
357
+ }
358
+ }
359
+ return carrier
360
+ }
361
+ if (carrier instanceof Map) {
362
+ for (const [name, value] of headers) {
363
+ const existing = []
364
+ for (const k of carrier.keys()) {
365
+ const t = text(k)
366
+ if (t !== null && t.toLowerCase() === name) existing.push(k)
367
+ }
368
+ if (existing.length && !overwrite) continue
369
+ for (const k of existing) if (k !== name) carrier.delete(k)
370
+ carrier.set(name, val(value))
371
+ }
372
+ return carrier
373
+ }
374
+ if (carrier && typeof carrier.set === 'function' && typeof carrier.has === 'function') {
375
+ for (const [name, value] of headers) {
376
+ if (carrier.has(name) && !overwrite) continue
377
+ carrier.set(name, val(value))
378
+ }
379
+ return carrier
380
+ }
381
+ if (carrier !== null && typeof carrier === 'object') {
382
+ for (const [name, value] of headers) {
383
+ const existing = Object.keys(carrier).filter((k) => k.toLowerCase() === name)
384
+ if (existing.length && !overwrite) continue
385
+ for (const k of existing) if (k !== name) delete carrier[k]
386
+ carrier[name] = val(value)
387
+ }
388
+ return carrier
389
+ }
390
+ throw new TypeError('unsupported carrier')
391
+ }
392
+
393
+ module.exports = {
394
+ BAGGAGE_MAX_ENTRIES,
395
+ BAGGAGE_MAX_BYTES,
396
+ EMPTY_BAG,
397
+ EMPTY_OBJECT,
398
+ NEW_TRACE,
399
+ SpanContext,
400
+ makeContext,
401
+ contextBag,
402
+ isSpanContext,
403
+ parseTraceparent,
404
+ parseBaggage,
405
+ formatBaggage,
406
+ mergeBag,
407
+ bagObject,
408
+ toBag,
409
+ extract,
410
+ headersFor,
411
+ writeHeaders,
412
+ getAll,
413
+ text,
414
+ CTL
415
+ }
@@ -0,0 +1,97 @@
1
+ // Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
2
+ 'use strict'
3
+
4
+ // Node IPC transport (80 A.9, 40 §6/§7): envelopes `{type, data}` over process.send.
5
+
6
+ const debug = require('../debug')('axm:transport:ipc')
7
+
8
+ const KEEPALIVE_RELEASE_MS = 3000
9
+ const noop = () => {}
10
+
11
+ function isClusterWorker () {
12
+ // Cheap check first: Node loads its internal cluster child module in every worker.
13
+ const list = process.moduleLoadList
14
+ if (Array.isArray(list) && list.length > 0) return list.includes('NativeModule internal/cluster/child')
15
+ try {
16
+ return require('cluster').isWorker === true
17
+ } catch (_) {
18
+ return false
19
+ }
20
+ }
21
+
22
+ class IPCTransport {
23
+ constructor () {
24
+ this._listeners = []
25
+ this._onMessage = null
26
+ this._releaseTimer = null
27
+ this.kind = 'ipc'
28
+ }
29
+
30
+ init () {
31
+ if (typeof process.send !== 'function') {
32
+ debug('no IPC channel, transport is inert')
33
+ return this
34
+ }
35
+ this._onMessage = (msg) => {
36
+ for (const fn of this._listeners.slice()) fn(msg)
37
+ }
38
+ process.on('message', this._onMessage)
39
+ // IO-093: our message listener must never keep an otherwise finished app alive.
40
+ // Node refs the channel when a 'message' listener is added; after a short window
41
+ // (so early daemon messages such as action triggers are still received) we unref it.
42
+ // If the app has other work, the process stays alive and messages keep flowing.
43
+ // Cluster workers are kept alive by Node's own cluster logic (40 §5.4): leave them.
44
+ if (!isClusterWorker()) {
45
+ this._releaseTimer = setTimeout(() => {
46
+ this._releaseTimer = null
47
+ try {
48
+ if (process.channel && typeof process.channel.unref === 'function') process.channel.unref()
49
+ } catch (_) {}
50
+ }, KEEPALIVE_RELEASE_MS)
51
+ if (this._releaseTimer.unref) this._releaseTimer.unref()
52
+ }
53
+ return this
54
+ }
55
+
56
+ isConnected () {
57
+ return typeof process.send === 'function' && process.connected !== false
58
+ }
59
+
60
+ /** Send a raw message (IO-090..IO-092). Returns -1 when there is no channel. */
61
+ sendRaw (msg, cb) {
62
+ if (typeof process.send !== 'function') {
63
+ if (cb) process.nextTick(cb)
64
+ return -1
65
+ }
66
+ if (process.connected === false) {
67
+ console.error('Process disconnected from parent! (not connected)')
68
+ process.exit(1)
69
+ return -1
70
+ }
71
+ try {
72
+ return process.send(msg, cb || noop)
73
+ } catch (err) {
74
+ console.error('Process disconnected from parent! (' + (err && err.message) + ')')
75
+ process.exit(1)
76
+ return -1
77
+ }
78
+ }
79
+
80
+ send (type, data, cb) {
81
+ return this.sendRaw({ type, data }, cb)
82
+ }
83
+
84
+ onMessage (fn) {
85
+ this._listeners.push(fn)
86
+ }
87
+
88
+ destroy () {
89
+ if (this._onMessage) process.removeListener('message', this._onMessage)
90
+ this._onMessage = null
91
+ this._listeners = []
92
+ if (this._releaseTimer) clearTimeout(this._releaseTimer)
93
+ this._releaseTimer = null
94
+ }
95
+ }
96
+
97
+ module.exports = { IPCTransport, isClusterWorker }