@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.
- package/README.md +69 -2
- package/bin/pmx-io-node.js +143 -0
- package/index.d.ts +381 -0
- package/index.js +27 -0
- package/index.mjs +47 -0
- package/lib/actions.js +120 -0
- package/lib/activate.js +402 -0
- package/lib/application.js +106 -0
- package/lib/bun.js +155 -0
- package/lib/config.js +67 -0
- package/lib/constants.js +17 -0
- package/lib/debug.js +48 -0
- package/lib/esm-hooks.mjs +111 -0
- package/lib/exit.js +79 -0
- package/lib/features/metrics.js +409 -0
- package/lib/features/notify.js +358 -0
- package/lib/features/profiling.js +208 -0
- package/lib/hooks.js +151 -0
- package/lib/inbound.js +174 -0
- package/lib/integrations.js +372 -0
- package/lib/itg/amqplib.js +169 -0
- package/lib/itg/bullmq.js +141 -0
- package/lib/itg/common.js +657 -0
- package/lib/itg/dbcomment.js +114 -0
- package/lib/itg/index.js +103 -0
- package/lib/itg/ioredis.js +212 -0
- package/lib/itg/kafkajs.js +205 -0
- package/lib/itg/mongodb.js +322 -0
- package/lib/itg/mysql2.js +205 -0
- package/lib/itg/nats.js +531 -0
- package/lib/itg/noderedis.js +236 -0
- package/lib/itg/pg.js +253 -0
- package/lib/itg/pgqueue.js +305 -0
- package/lib/itg/postgres.js +252 -0
- package/lib/itg/redis-common.js +265 -0
- package/lib/itg/sql.js +302 -0
- package/lib/metrics/service.js +286 -0
- package/lib/metrics/types.js +342 -0
- package/lib/modules.js +260 -0
- package/lib/node-options.js +62 -0
- package/lib/otel/bridge.js +797 -0
- package/lib/otel/env.js +114 -0
- package/lib/outbound.js +362 -0
- package/lib/pmx.js +757 -0
- package/lib/relay/hub.js +279 -0
- package/lib/relay/transport.js +253 -0
- package/lib/standalone/activate.js +307 -0
- package/lib/standalone/capture.js +55 -0
- package/lib/standalone/container.js +909 -0
- package/lib/standalone/transport.js +1023 -0
- package/lib/tracing/context.js +90 -0
- package/lib/tracing/control.js +124 -0
- package/lib/tracing/exporter.js +115 -0
- package/lib/tracing/index.js +242 -0
- package/lib/tracing/logs.js +176 -0
- package/lib/tracing/options.js +105 -0
- package/lib/tracing/paths.js +34 -0
- package/lib/tracing/tracer.js +850 -0
- package/lib/tracing/w3c.js +415 -0
- package/lib/transport/ipc.js +97 -0
- package/lib/transport/ws.js +511 -0
- package/lib/util.js +109 -0
- package/package.json +48 -4
- package/preload.cjs +26 -0
- package/preload.mjs +25 -0
- package/standalone.cjs +16 -0
- 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 }
|