@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
package/lib/actions.js ADDED
@@ -0,0 +1,120 @@
1
+ // Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
2
+ 'use strict'
3
+
4
+ // Custom, internal and scoped actions (80 A.8.1, A.8.2, NIO-040..042; 40 §7.4, §7.5, §7.8, §8.3).
5
+
6
+ const debug = require('./debug')('axm:services:actions')
7
+
8
+ function actionType (name) {
9
+ return name.startsWith('km:') || name.startsWith('internal:') ? 'internal' : 'custom'
10
+ }
11
+
12
+ class ActionService {
13
+ /**
14
+ * @param {object} transport
15
+ * @param {(err: any) => void} notifyError used for rejected async handlers (NIO-042)
16
+ */
17
+ constructor (transport, notifyError) {
18
+ this._transport = transport
19
+ this._notifyError = notifyError
20
+ this._actions = new Map()
21
+ transport.onMessage((msg) => this.dispatch(msg))
22
+ }
23
+
24
+ register (name, opts, handler) {
25
+ if (typeof name !== 'string') {
26
+ console.error('You must define an name when registering an action')
27
+ return false
28
+ }
29
+ if (typeof handler !== 'function') {
30
+ console.error('You must define an callback when registering an action')
31
+ return false
32
+ }
33
+ const arity = handler.length
34
+ this._actions.set(name, { name, handler, arity, scoped: false })
35
+ const data = { action_name: name, action_type: actionType(name), arity }
36
+ if (opts !== undefined) data.opts = opts
37
+ this._transport.send('axm:action', data)
38
+ return true
39
+ }
40
+
41
+ registerScoped (name, handler) {
42
+ if (typeof name !== 'string') {
43
+ console.error('You must define an name when registering an action')
44
+ return false
45
+ }
46
+ if (typeof handler !== 'function') {
47
+ console.error('You must define an callback when registering an action')
48
+ return false
49
+ }
50
+ this._actions.set(name, { name, handler, arity: handler.length, scoped: true })
51
+ this._transport.send('axm:action', { action_name: name, action_type: 'scoped', arity: handler.length, opts: null })
52
+ return true
53
+ }
54
+
55
+ has (name) {
56
+ return this._actions.has(name)
57
+ }
58
+
59
+ list () {
60
+ return Array.from(this._actions.values()).map((a) => ({
61
+ action_name: a.name,
62
+ action_type: a.scoped ? 'scoped' : actionType(a.name),
63
+ arity: a.arity
64
+ }))
65
+ }
66
+
67
+ /** Handle a daemon → app message (40 §8.3). Unknown names are ignored silently. */
68
+ dispatch (msg) {
69
+ let name
70
+ let opts
71
+ let uuid
72
+ if (typeof msg === 'string') {
73
+ name = msg
74
+ } else if (msg && typeof msg === 'object') {
75
+ name = msg.msg || msg.action_name
76
+ opts = msg.opts
77
+ uuid = msg.uuid
78
+ }
79
+ if (typeof name !== 'string') return
80
+ const action = this._actions.get(name)
81
+ if (!action) return
82
+ debug('invoking action %s', name)
83
+
84
+ if (action.scoped) {
85
+ if (!uuid) return
86
+ const send = (type) => (data) => this._transport.send(type, { data, uuid, action_name: name })
87
+ const stream = {
88
+ send: send('axm:scoped_action:stream'),
89
+ error: send('axm:scoped_action:error'),
90
+ end: send('axm:scoped_action:end')
91
+ }
92
+ action.handler(opts || {}, stream)
93
+ return
94
+ }
95
+
96
+ let replied = false
97
+ const reply = (data) => {
98
+ replied = true
99
+ this._transport.send('axm:reply', { at: Date.now(), action_name: name, return: data })
100
+ }
101
+ const params = opts === undefined || opts === null ? {} : opts // NIO-041
102
+ const ret = action.arity === 2 ? action.handler(params, reply) : action.handler(reply)
103
+ if (ret && typeof ret.then === 'function') {
104
+ ret.then((value) => {
105
+ if (!replied) reply(value)
106
+ }, (err) => {
107
+ if (!replied) reply({ success: false, err: err && err.message !== undefined ? err.message : String(err) })
108
+ try {
109
+ this._notifyError(err)
110
+ } catch (_) {}
111
+ })
112
+ }
113
+ }
114
+
115
+ destroy () {
116
+ this._actions.clear()
117
+ }
118
+ }
119
+
120
+ module.exports = { ActionService, actionType }
@@ -0,0 +1,402 @@
1
+ // Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
2
+ 'use strict'
3
+
4
+ // Preload activation (80 B.3 NIO-020..NIO-025, B.16 NIO-290..NIO-296) and coexistence with an
5
+ // app's own @pm2/io (B.4 NIO-030..NIO-036, 02 D-05).
6
+ //
7
+ // A process that loads the preload becomes one of:
8
+ // * the root (env PMX_IO=1 from the daemon, IPC channel, main thread): reports over IPC and,
9
+ // with `children` (default true), opens the local hub for its Node descendants (NIO-291);
10
+ // * a daemon-mode relay (PMX_IO unset, PMX_IO_HUB set, PMX_IO_STANDALONE unset): reports
11
+ // through the root's hub;
12
+ // * a thread relay (a worker thread of an instrumented process): reports through the main
13
+ // thread (NIO-293).
14
+
15
+ const path = require('path')
16
+ const fs = require('fs')
17
+ const { fileURLToPath } = require('url')
18
+ const { GLOBAL_SYMBOL } = require('./constants')
19
+ const { PKG_DIR } = require('./util')
20
+ const debug = require('./debug')('axm:main')
21
+
22
+ const STATE = Symbol.for('@pmx/io:preload-state')
23
+
24
+ function state () {
25
+ let s = globalThis[STATE]
26
+ if (!s) {
27
+ s = { active: false, instance: null, joined: false, conflict: false, esmHooked: false, role: null, hub: null, caps: [], closeThreads: null }
28
+ Object.defineProperty(globalThis, STATE, { value: s, enumerable: false, configurable: true })
29
+ }
30
+ return s
31
+ }
32
+
33
+ const OWN_PRELOADS = ['preload.cjs', 'preload.mjs', 'standalone.cjs', 'standalone.mjs'].map((f) => path.join(PKG_DIR, f))
34
+
35
+ function realpath (p) {
36
+ try {
37
+ return fs.realpathSync(p)
38
+ } catch (_) {
39
+ return p
40
+ }
41
+ }
42
+
43
+ function isOurPreload (spec) {
44
+ if (typeof spec !== 'string' || spec === '') return false
45
+ let file = spec
46
+ if (file.startsWith('file:')) {
47
+ try {
48
+ file = fileURLToPath(file)
49
+ } catch (_) {
50
+ return false
51
+ }
52
+ }
53
+ const abs = path.resolve(file)
54
+ if (OWN_PRELOADS.includes(abs)) return true
55
+ const real = realpath(abs)
56
+ return OWN_PRELOADS.some((own) => realpath(own) === real)
57
+ }
58
+
59
+ function isMainThread () {
60
+ // A worker thread's bootstrap always loads worker_threads: when it is not loaded yet this is
61
+ // the main thread, without paying for loading the module (start-up cost, NIO-024).
62
+ const list = process.moduleLoadList
63
+ if (Array.isArray(list) && !list.includes('NativeModule worker_threads')) return true
64
+ try {
65
+ return require('worker_threads').isMainThread !== false
66
+ } catch (_) {
67
+ return true
68
+ }
69
+ }
70
+
71
+ function optedOut (env) {
72
+ return env.pmx === 'false' || env.automation === 'false'
73
+ }
74
+
75
+ /** NIO-020 activation conditions (the root under the daemon). */
76
+ function shouldActivate (env) {
77
+ env = env || process.env
78
+ return env.PMX_IO === '1' &&
79
+ !optedOut(env) &&
80
+ isMainThread() &&
81
+ typeof process.send === 'function'
82
+ }
83
+
84
+ /** NIO-291: a descendant of an instrumented root, under the daemon. */
85
+ function isDaemonRelay (env) {
86
+ env = env || process.env
87
+ return !env.PMX_IO && Boolean(env.PMX_IO_HUB) && env.PMX_IO_STANDALONE !== '1' && !optedOut(env)
88
+ }
89
+
90
+ /** INJ-024: the capabilities the daemon advertised (read once, then removed). */
91
+ function readCaps (env) {
92
+ const raw = env.PMX_IO_CAPS
93
+ if (typeof raw !== 'string') return []
94
+ return raw.split(',').map((s) => s.trim()).filter((s) => s)
95
+ }
96
+
97
+ /**
98
+ * NIO-021 (amended by NIO-291): PMX_IO and PMX_IO_CAPS are never inherited; the preload tokens
99
+ * leave NODE_OPTIONS only when descendants are not instrumented (`children: false`).
100
+ */
101
+ function scrubEnvironment (keepPreloads) {
102
+ delete process.env.PMX_IO
103
+ delete process.env.PMX_IO_CAPS
104
+ if (keepPreloads) return
105
+ const opts = process.env.NODE_OPTIONS
106
+ if (typeof opts === 'string') {
107
+ const stripped = require('./node-options').stripPreloads(opts, isOurPreload)
108
+ if (stripped.trim() === '') delete process.env.NODE_OPTIONS
109
+ else process.env.NODE_OPTIONS = stripped
110
+ }
111
+ }
112
+
113
+ function sendOption (instance, data) {
114
+ try {
115
+ if (instance._transport && instance._initialized) instance._transport.send('axm:option:configuration', data)
116
+ else if (typeof process.send === 'function' && process.connected !== false && instance._mode === 'managed') {
117
+ process.send({ type: 'axm:option:configuration', data }, () => {})
118
+ }
119
+ } catch (_) {}
120
+ }
121
+
122
+ /** NIO-030/031: define the global symbol as an accessor so foreign reads/writes are observable. */
123
+ function claimSingleton (instance) {
124
+ const s = state()
125
+ Object.defineProperty(globalThis, GLOBAL_SYMBOL, {
126
+ configurable: true,
127
+ enumerable: false,
128
+ get () {
129
+ if (!s.joined) {
130
+ s.joined = true
131
+ debug('an app copy of @pm2/io joined the injected instance')
132
+ sendOption(instance, { pm2_io_joined: true })
133
+ }
134
+ return instance
135
+ },
136
+ set (value) {
137
+ if (value === instance) return
138
+ if (!s.conflict) {
139
+ s.conflict = true
140
+ sendOption(instance, { pm2_io_conflict: true })
141
+ try {
142
+ process.stderr.write('[pmx] another @pm2/io instance tried to replace the injected one; it was ignored\n')
143
+ } catch (_) {}
144
+ }
145
+ }
146
+ })
147
+ }
148
+
149
+ /** NIO-034: warn once when the legacy `pmx` module is required. Never alters it. */
150
+ function watchLegacyPmx () {
151
+ let Module
152
+ try {
153
+ Module = require('./util').cjsModule()
154
+ } catch (_) {
155
+ return
156
+ }
157
+ const proto = Module.prototype
158
+ if (!proto || typeof proto.require !== 'function') return
159
+ const orig = proto.require
160
+ let warned = false
161
+ proto.require = function require (id) {
162
+ if (!warned && id === 'pmx') {
163
+ warned = true
164
+ try {
165
+ process.stderr.write("[pmx] legacy 'pmx' module detected: start the app with --no-pmx to avoid duplicate metrics\n")
166
+ } catch (_) {}
167
+ }
168
+ return orig.apply(this, arguments)
169
+ }
170
+ }
171
+
172
+ /**
173
+ * A.2: the second init from env `io`. (Env `trace` and the sampling variables are applied by
174
+ * every init, NIO-201, so `trace=true` alone needs no second init.)
175
+ */
176
+ function secondInitConfig (env) {
177
+ const { envIoConf } = require('./tracing/options')
178
+ const conf = envIoConf(env)
179
+ return conf ? Object.assign({}, conf) : null
180
+ }
181
+
182
+ /** NIO-291: open the hub and export its variables before the app can start children. */
183
+ function openHub (s, instanceRef) {
184
+ const { Hub } = require('./relay/hub')
185
+ const hub = new Hub((msg) => {
186
+ const inst = instanceRef()
187
+ if (inst) inst._onRelayMessage(msg)
188
+ })
189
+ process.env.PMX_IO_HUB = hub.start()
190
+ process.env.PMX_IO_HUB_TOKEN = hub.token
191
+ process.env.PMX_IO_ROOT_PID = String(process.pid)
192
+ s.hub = hub
193
+ // worker threads start after the app's first line: listen for them off the start-up path
194
+ const t = setImmediate(() => {
195
+ s.closeThreads = require('./relay/transport').listenThreads((msg) => {
196
+ const inst = instanceRef()
197
+ if (inst) inst._onRelayMessage(msg)
198
+ })
199
+ })
200
+ if (t.unref) t.unref()
201
+ return hub
202
+ }
203
+
204
+ function activateRoot (s) {
205
+ const { childrenEnabled } = require('./tracing/options')
206
+ s.caps = readCaps(process.env)
207
+ const children = childrenEnabled(null, process.env)
208
+ scrubEnvironment(children)
209
+ if (Object.prototype.hasOwnProperty.call(globalThis, GLOBAL_SYMBOL)) {
210
+ debug('global @pm2/io instance already present; not injecting')
211
+ return null
212
+ }
213
+ // (a) node_version (40 §7.1)
214
+ try {
215
+ process.send({ node_version: process.versions.node }, () => {})
216
+ } catch (_) {}
217
+ // (b) claim the singleton
218
+ const { PMX } = require('./pmx')
219
+ const instance = new PMX({ managed: true, caps: s.caps })
220
+ s.instance = instance
221
+ s.active = true
222
+ s.role = 'root'
223
+ claimSingleton(instance)
224
+ watchLegacyPmx()
225
+ if (children) {
226
+ try {
227
+ openHub(s, () => s.instance)
228
+ } catch (err) {
229
+ debug('hub not started: %s', err && err.message)
230
+ }
231
+ } else {
232
+ delete process.env.PMX_IO_HUB
233
+ delete process.env.PMX_IO_HUB_TOKEN
234
+ }
235
+ // (c) default init, (d) second init from env
236
+ instance.init()
237
+ const conf = secondInitConfig(process.env)
238
+ if (conf) instance.init(conf)
239
+ return s
240
+ }
241
+
242
+ function activateRelay (s, kind) {
243
+ if (Object.prototype.hasOwnProperty.call(globalThis, GLOBAL_SYMBOL)) return null
244
+ const { PMX } = require('./pmx')
245
+ let instance
246
+ if (kind === 'thread') {
247
+ instance = new PMX({ mode: 'thread' })
248
+ } else {
249
+ const env = process.env
250
+ const hub = env.PMX_IO_HUB
251
+ const token = env.PMX_IO_HUB_TOKEN || ''
252
+ const { HubRelayTransport } = require('./relay/transport')
253
+ instance = new PMX({ mode: 'relay', relayFactory: () => new HubRelayTransport(hub, token) })
254
+ }
255
+ s.instance = instance
256
+ s.active = true
257
+ s.role = kind
258
+ claimSingleton(instance)
259
+ instance.init()
260
+ const conf = secondInitConfig(process.env)
261
+ if (conf) instance.init(conf)
262
+ return s
263
+ }
264
+
265
+ /**
266
+ * Activate instrumentation from a preload. Idempotent. Returns the state, or null when the
267
+ * activation conditions do not hold (then nothing at all is done).
268
+ */
269
+ function activate () {
270
+ const s = state()
271
+ if (s.active) return s
272
+ const env = process.env
273
+ if (!isMainThread()) {
274
+ // NIO-293: a worker thread of an instrumented process (daemon root with children, or a
275
+ // standalone tree) becomes a thread relay
276
+ if (optedOut(env) || !(env.PMX_IO_HUB || env.PMX_IO_STANDALONE === '1')) return null
277
+ if (require('./tracing/options').childrenEnabled(null, env) === false) return null
278
+ return activateRelay(s, 'thread')
279
+ }
280
+ if (shouldActivate(env)) return activateRoot(s)
281
+ if (isDaemonRelay(env)) {
282
+ if (require('./tracing/options').childrenEnabled(null, env) === false) return null
283
+ return activateRelay(s, 'relay')
284
+ }
285
+ return null
286
+ }
287
+
288
+ const ESM_SYNTAX = /^[ \t]*(?:import[ \t]*[\w*{'"]|export[ \t]+(?:\{|\*|default\b|const\b|let\b|var\b|function\b|class\b|async\b))/m
289
+
290
+ /** The `type` of the package.json nearest to `dir`, or undefined. */
291
+ function packageType (dir) {
292
+ for (let i = 0; i < 64; i++) {
293
+ let text = null
294
+ try {
295
+ text = fs.readFileSync(path.join(dir, 'package.json'), 'utf8')
296
+ } catch (_) {}
297
+ if (text !== null) {
298
+ try {
299
+ const t = JSON.parse(text).type
300
+ return typeof t === 'string' ? t : undefined
301
+ } catch (_) {
302
+ return undefined
303
+ }
304
+ }
305
+ const up = path.dirname(dir)
306
+ if (up === dir) return undefined
307
+ dir = up
308
+ }
309
+ return undefined
310
+ }
311
+
312
+ /**
313
+ * Whether the app's entry point is (or may be) an ES module: `.mjs`/`.mts`; `.js`/`.ts`/no
314
+ * extension under a package.json with `"type": "module"`, or containing import/export syntax
315
+ * (Node's syntax detection); `--input-type=module` / `--experimental-default-type=module`. Errs
316
+ * towards true: a wrong "true" only costs the hooks thread, a wrong "false" leaves ES-module
317
+ * builds of hono/h3 unwrapped (their CommonJS builds are always covered).
318
+ */
319
+ function entryIsEsm (argv, execArgv) {
320
+ argv = argv || process.argv
321
+ const flags = (execArgv || process.execArgv || []).concat(String(process.env.NODE_OPTIONS || '').split(/\s+/))
322
+ if (flags.some((f) => f === '--input-type=module' || f === '--experimental-default-type=module')) return true
323
+ const entry = argv[1]
324
+ if (typeof entry !== 'string' || entry === '') return false
325
+ let file = path.resolve(entry)
326
+ const ext = path.extname(file).toLowerCase()
327
+ if (ext === '.mjs' || ext === '.mts') return true
328
+ if (ext === '.cjs' || ext === '.cts') return false
329
+ if (ext === '' && !fs.existsSync(file) && fs.existsSync(file + '.js')) file += '.js'
330
+ try {
331
+ file = fs.realpathSync(file)
332
+ } catch (_) {}
333
+ if (packageType(path.dirname(file)) === 'module') return true
334
+ try {
335
+ const fd = fs.openSync(file, 'r')
336
+ try {
337
+ const buf = Buffer.alloc(64 * 1024)
338
+ const n = fs.readSync(fd, buf, 0, buf.length, 0)
339
+ return ESM_SYNTAX.test(buf.toString('utf8', 0, n))
340
+ } finally {
341
+ fs.closeSync(fd)
342
+ }
343
+ } catch (_) {
344
+ return false
345
+ }
346
+ }
347
+
348
+ /**
349
+ * NIO-085 / NIO-210: register the module customization hooks that wrap ES-module libraries,
350
+ * only when tracing is enabled (called from preload.mjs / standalone.mjs).
351
+ */
352
+ function registerEsmHooks () {
353
+ const s = state()
354
+ if (!s.active || s.esmHooked || !s.instance || !s.instance._tracingEnabled()) return false
355
+ let mod
356
+ try {
357
+ mod = require('module')
358
+ } catch (_) {
359
+ return false
360
+ }
361
+ const file = path.join(__dirname, 'esm-hooks.mjs')
362
+ try {
363
+ if (typeof mod.registerHooks === 'function') {
364
+ const hooks = require(file)
365
+ mod.registerHooks({ load: hooks.load })
366
+ } else if (typeof mod.register === 'function') {
367
+ // off-thread hooks (Node < 22.15 / < 23.5) start a worker thread, ~100 ms of start-up on
368
+ // Node 18/20: only when the entry point is an ES module (a CommonJS app loads the
369
+ // CommonJS builds, which the require interception covers)
370
+ if (!entryIsEsm()) {
371
+ debug('CommonJS entry point: module hooks not registered (ES-module libraries imported later are not instrumented)')
372
+ return false
373
+ }
374
+ mod.register(require('url').pathToFileURL(file).href)
375
+ } else {
376
+ debug('module hooks unavailable on this Node: ES-module libraries are not instrumented')
377
+ return false
378
+ }
379
+ } catch (err) {
380
+ debug('module hooks failed: %s', err && err.message)
381
+ return false
382
+ }
383
+ s.esmHooked = true
384
+ return true
385
+ }
386
+
387
+ module.exports = {
388
+ activate,
389
+ registerEsmHooks,
390
+ shouldActivate,
391
+ isDaemonRelay,
392
+ scrubEnvironment,
393
+ isOurPreload,
394
+ secondInitConfig,
395
+ state,
396
+ readCaps,
397
+ openHub,
398
+ claimSingleton,
399
+ isMainThread,
400
+ entryIsEsm,
401
+ OWN_PRELOADS
402
+ }
@@ -0,0 +1,106 @@
1
+ // Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
2
+ 'use strict'
3
+
4
+ // Applications (pmx 97 PAP-001/002/050/051, prodman 02 §6.20): the application this process
5
+ // belongs to. Under a pmx daemon the daemon decides (`PMX_APPLICATION`, PAP-022) and an init
6
+ // value that differs is ignored with one warning; standalone, init({ application }) >
7
+ // PMX_APPLICATION > `default`, an invalid value giving `default` and one warning.
8
+
9
+ const DEFAULT = 'default'
10
+ const ENV = 'PMX_APPLICATION'
11
+
12
+ const NAME = /^[a-z0-9](?:[a-z0-9._-]{0,61}[a-z0-9])?$/
13
+
14
+ /**
15
+ * PAP-001/002: `{ value }` with the normalised name (`null` = unset: empty after trimming), or
16
+ * `{ error }` with the failing rule: 'type', 'charset', 'length' or 'reserved'.
17
+ */
18
+ function normalize (raw) {
19
+ if (raw === undefined || raw === null) return { value: null }
20
+ if (typeof raw !== 'string') return { error: 'type' }
21
+ const n = raw.replace(/^[\t\n\f\r ]+|[\t\n\f\r ]+$/g, '').replace(/[A-Z]/g, (c) => c.toLowerCase())
22
+ if (n === '') return { value: null }
23
+ if (!/^[a-z0-9._-]+$/.test(n) || !/^[a-z0-9]/.test(n) || !/[a-z0-9]$/.test(n)) return { error: 'charset' }
24
+ if (n.length > 63) return { error: 'length' }
25
+ if (!NAME.test(n)) return { error: 'charset' }
26
+ if (n === 'all' || n === 'none' || n.startsWith('pmx')) return { error: 'reserved' }
27
+ return { value: n }
28
+ }
29
+
30
+ /** PAP-004 wording. */
31
+ function message (raw, reason) {
32
+ const shown = typeof raw === 'string' ? raw : JSON.stringify(raw)
33
+ if (reason === 'reserved') return `Application name "${shown}" is reserved`
34
+ return `Invalid application name "${shown}": use 1-63 lower-case letters, digits, '.', '_' or '-', starting and ending with a letter or digit`
35
+ }
36
+
37
+ function warnDefault (line) {
38
+ try {
39
+ process.stderr.write(line + '\n')
40
+ } catch (_) {}
41
+ }
42
+
43
+ let resolved = null
44
+ const warned = new Set()
45
+
46
+ function warnOnce (warn, line) {
47
+ if (warned.has(line)) return
48
+ warned.add(line)
49
+ ;(warn || warnDefault)(line)
50
+ }
51
+
52
+ /**
53
+ * PAP-050: resolve and remember the application. `opts`: { managed (under a pmx daemon), env,
54
+ * warn (line printer) }. Returns the name (`default` included).
55
+ */
56
+ function resolve (initValue, opts) {
57
+ opts = opts || {}
58
+ const env = opts.env || process.env
59
+ const warn = opts.warn
60
+ const fromEnv = normalize(env[ENV])
61
+ if (opts.managed) {
62
+ const a = fromEnv.value || DEFAULT
63
+ if (initValue !== undefined && initValue !== null) {
64
+ const n = normalize(initValue)
65
+ if (n.error || (n.value || DEFAULT) !== a) {
66
+ warnOnce(warn, `[pmx-io] application is set by pmx (${a}); ignoring init application ${typeof initValue === 'string' ? initValue : JSON.stringify(initValue)}`)
67
+ }
68
+ }
69
+ resolved = a
70
+ return a
71
+ }
72
+ resolved = DEFAULT
73
+ for (const raw of [initValue, env[ENV]]) {
74
+ const n = normalize(raw)
75
+ if (n.error) {
76
+ warnOnce(warn, `[pmx-io] ${message(raw, n.error)}; using application ${DEFAULT}`)
77
+ break
78
+ }
79
+ if (n.value) {
80
+ resolved = n.value
81
+ break
82
+ }
83
+ }
84
+ return resolved
85
+ }
86
+
87
+ /** The application resolved by the last init, else the environment's (`default` when unset). */
88
+ function current (env) {
89
+ if (resolved !== null) return resolved
90
+ return normalize((env || process.env)[ENV]).value || DEFAULT
91
+ }
92
+
93
+ /** PAP-051: the `service.namespace` value, or null: the application, else a non-default pm2 namespace. */
94
+ function serviceNamespace (application, namespace) {
95
+ if (application && application !== DEFAULT) return application
96
+ if (typeof namespace === 'string' && namespace !== '' && namespace !== DEFAULT) return namespace
97
+ return null
98
+ }
99
+
100
+ /** Tests only. */
101
+ function _reset () {
102
+ resolved = null
103
+ warned.clear()
104
+ }
105
+
106
+ module.exports = { DEFAULT, ENV, normalize, message, resolve, current, serviceNamespace, _reset }