@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.
- 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
package/lib/relay/hub.js
ADDED
|
@@ -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 }
|