New:Introducing Socket Scanning for VS Code Marketplace Extensions.Learn more →
Get Started

@3pm/api

Package Overview
Dependencies
Maintainers
1
Versions
2
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@3pm/api

Node.js client for the 3pm process manager: pm2-style API, event bus subscription, application metrics and events (io)

latest
Source
npmnpm
Version
0.1.1
Version published
Weekly downloads
15
7.14%
Maintainers
1
Weekly downloads
 
Created
Source

@3pm/api

Node.js client for the 3pm process manager, plus io, the application-metrics SDK (see below). Same shape as pm2's programmatic API — connect, start, list, stop, restart, delete, describe, launchBus — but promise-based, with zero dependencies. It talks straight to the daemon's Unix sockets (rpc.sock for commands, pub.sock for events) and auto-spawns the daemon when none is running.

npm install @3pm/api

Requires Node ≥ 18 and a 3pm binary on PATH (or THREEPM_BIN) for auto-spawn — not needed when a daemon is already up.

Quickstart

const { ThreePM } = require('@3pm/api');

const pm = await ThreePM.connect();          // spawns the daemon if needed

await pm.start({ script: 'app.js', name: 'api', instances: 2, env: { PORT: 3000 } });
console.table((await pm.list()).map(({ id, name, status, pid, restarts }) => ({ id, name, status, pid, restarts })));

const bus = await pm.launchBus({ process: 'api' });
bus.on('log:out',        (e) => process.stdout.write(`${e.name}[${e.instance}] ${e.line}\n`));
bus.on('log:err',        (e) => process.stderr.write(`${e.name}[${e.instance}] ${e.line}\n`));
bus.on('process:exit',   (e) => console.log('exit', e.exit_code, e.exit_signal, e.stderr_tail));
bus.on('process:online', (e) => console.log('online', e.name));
bus.on('daemon:shutdown',(e) => console.log('daemon going down:', e.reason));

await pm.restart('api');
await pm.stop('api');
await pm.delete('api');
pm.disconnect();                              // closes the rpc socket and every bus

API

new ThreePM({ home?, bin?, autoSpawn? = true, timeout? = 30000 }) — or await ThreePM.connect(opts).

home resolves like the CLI (resolvePaths): the option, else $THREEPM_HOME, else the home a root daemon hosts for this account (/var/lib/3pm/users/<user>, marked by a hosted file — hostedHome() returns it or null), else ~/.3pm. On a hosted home the client never spawns a daemon: when the root daemon is down it throws daemon_unreachable with err.hosted === true. start() accepts user (run the app as a declared account, root daemon) and persist: false (kept out of dump.json, never relaunched at boot); a hosted account's daemon-level calls (save, killDaemon, resurrect, internals, hostMetrics) fail with forbidden.

MethodWire requestReturns
connect()ping (checks protocol === 1)this
ping()ping{pid, version, protocol}
start(opts | opts[], {ifChanged?})startProcessRecord[]
scale(name, n)scaleProcessRecord[]
stop(target)stopProcessRecord[] (returns once dead)
restart(target, {toggleWatch?})restartProcessRecord[]
delete(target, {flush?}) / deldeleteProcessRecord[]
reset(target)resetProcessRecord[]
sendSignal(sig, target, {group?})send_signalProcessRecord[]
list() / ls()listProcessRecord[]
describe(target) / show()describeProcessRecord[] (every instance)
save() / resurrect()save / resurrect{path, count} / ProcessRecord[]
killDaemon()kill_daemon— (buses stay open to see daemon:shutdown)
internals() / hostMetrics()internals / host_metricsgrowable snapshots
flushLogs(t?) / rotateLogs(t?) / reloadLogs()*_logstouched file paths
request(cmd, fields)anythingraw response — escape hatch for new requests
launchBus(filter?)—EventBus

target is an id (3 / "3"), a name ("api"), "all", {id} or {name}.

Start options

pm2 names, pm2 units: script (required; resolved against cwd), name, cwd, args (string or array), interpreter (inferred from the extension — .js/.ts→node, .py→python3, .rb→ruby, .sh→bash, .pl→perl — then resolved to an absolute path; 'none' = direct exec), env, instances ('max' = one per CPU), autorestart / restart_policy, max_memory_restart ("200M"), min_uptime, kill_timeout, restart_delay, listen_timeout, wait_ready, watch (true, path, list), ignore_watch, out_file/error_file/log_file ('NULL' disables), merge_logs, log_date_format, stop_exit_codes, cron_restart, depends_on, max_restarts, backoff_*, log_rotate, … — the full list is in index.d.ts (StartOptions). Unknown keys throw invalid_spec with a did-you-mean.

Events

launchBus({ subscribe?, process?, ids?, reconnect? }) sends the daemon an event_filter so unwanted events never cross the socket:

  • subscribe: globs over event names — ['process:*'], ['log_err'], ['process:exit', 'log_*']
  • process: one app name; ids: process ids
  • reconnect: true | {delayMs, maxAttempts}: re-subscribe after an EOF (e.g. across 3pm update)
Emitted asAlso asPayload
log:out, log:errlog_out, log_err{id, name, instance, line, ts?}
process:event + process:<kind> (start, online, exit, restart, stop, delete, errored)process{id, name, kind, exit_code?, exit_signal?, stderr_tail?}
log:rotatedlog_rotated{id, name, instance, path}
lagged—{missed} — the subscriber fell behind
daemon:shutdowndaemon_shutdown{reason: 'kill' | 'signal' | 'home_removed'}
event—every decoded event, unknown kinds included
connect, reconnect, close({reason}), error—bus lifecycle

A close with reason: 'eof' (no daemon_shutdown first) is an unannounced daemon death.

Errors

Every failure is a ThreePMError with .code: the daemon's codes (not_found, invalid_spec, spawn_failed, unknown_request, shutting_down, internal) or client-side daemon_unreachable, protocol_skew (run 3pm update), timeout, disconnected, protocol. Codes are exported as CODES.

Application metrics and events: io

The other half of the package: what a process started by 3pm reports about itself (pm2's io.metric/io.counter/io.notifyError, for 3pm). Zero dependencies, never blocking, never throwing; outside 3pm (no THREEPM_CTL_SOCK in the environment) every call is a no-op.

const { io } = require('@3pm/api');
io.init({ source: 'worker', builtins: true });      // one connection per process of the app

const requests = io.counter('http.requests', { labels: { method: 'GET' } });
const latency  = io.histogram('http.latency', { unit: 'ms' });
const temp     = io.gauge('room.temp', { unit: 'Cel', description: 'studio temperature' });
io.metric('queue.depth', () => queue.length);        // read at every flush

requests.labels({ status: 200 }).inc();
latency.observe(12.5);
temp.set(21.5);
io.event('deploy', { level: 'warn', attrs: { version: '1.2' } });
io.notifyError(err, { attrs: { where: 'worker' } });  // an `error` event with the stack

Then 3pm show <app> (section "Custom metrics") and 3pm events -f app. Full reference, bounds and the three languages: docs/app-metrics.md in the 3pm repository.

CallNotes
io.init({ source?, flush_ms? = 1000, builtins?, profile?, on_shutdown? })Connects in the background; idempotent. source defaults to a name unique per connection: main for the process 3pm spawned, worker-<pid>-<threadId> in a worker thread, pid-<pid> in a forked child (child_process.fork, cluster) or a child of the process holding main; two processes that both look spawned (sh -c 'node a & node b') both claim main, and the one refused falls back to pid-<pid> — two connections of one instance under one source replace each other. The built-in 3pm:profile action is declared on main only (a bare 3pm profile <app> is never ambiguous); profile: true adds it on any source, profile: false withholds it. builtins: true adds the event loop (nodejs.eventloop.delay histogram, .p99/.max, .utilization), GC pauses (nodejs.gc.duration{kind}), memory (nodejs.heap.used/.committed/.limit, nodejs.memory.external/.array_buffers), nodejs.handles.active, process.cpu.utilization, and the counters process.page_faults.major, process.context_switches.involuntary; builtins: { heap_spaces: true } adds nodejs.heap.space.used{space}
io.electron(require('electron'))Electron main process: electron.processes/.process.cpu/.process.memory by type, electron.power.* (battery, thermal state, speed limit), counters and events for crashed processes and hung windows, sleep/wake. Returns stop()
createRendererIo(ipcRenderer, { source?, builtins? }) (@3pm/api/renderer)Renderer/preload side of io.ipcBridge(ipcMain). builtins: true adds renderer.eventloop.delay, renderer.longtask.duration, renderer.heap.*, renderer.blink.*, renderer.cpu.utilization, renderer.dom.nodes (what the runtime offers)
gauge / counter / meter / histogram(name, { unit?, description?, labels?, scale? })Same name + labels = same object. Counters are cumulative; histograms are base-2 exponential (scale 0..4, default 1) and lower their scale by one (buckets merge in pairs) whenever a 129th bucket would open, so an entry always fits the daemon's cap. .labels({...}) derives a child — cache it rather than calling it per request; past 50 label sets a metric folds into overflow="true", and the fold's .labels() returns the fold
metric(name, fn, opts?)Gauge by callback, evaluated on the event loop at flush. Callbacks and metrics share the 200-name budget: a name is one or the other
event(name, { level?, attrs?, exc?, fp? })Sent at once (queued while the connection comes up)
notifyError(err, { attrs?, name? })error-level event carrying exc.{type,message,stacktrace}
action(name, { description?, remote? = true, timeout_ms? }, handler)Expose handler(params, ctx) to 3pm trigger <app> <name> [params] and, unless remote: false, to the fleet backend. What it returns (JSON, ≤ 512 KiB, ≤ 64 levels deep) is the result; past either bound the call fails with code too_large — a bigger one is a file: write ctx.file + '.tmp' then await ctx.commitFile(ext) (past the call's timeout it deletes the .tmp and throws). Whatever the handler throws is the failure, even a value with no readable message. Returns unregister. A 3pm: name throws (the SDK's prefix). At most 32 actions per source, 3pm:profile included. 3pm:profile (CPU/heap sampling profile, {kind: cpu|heap, duration_ms: 1000..60000, sampling}) is declared on main by default; invalid params, or a timeout leaving less than 15 s after the sampling, fail the call before anything is sampled
flush({ wait_ms? }), shutdown()The whole registry is sent every flush_ms anyway. flush resolves true when every batch was written (and, with wait_ms, left the process); shutdown is safe to call twice

Strings are bounded in UTF-8 bytes, as the daemon measures them: label values ≤ 64 bytes (else the series is refused), unit/description cut to 16/128 bytes, event attributes to 256, exception fields to 128/1024/8192. Cuts land on character boundaries and lone surrogates become U+FFFD, so nothing sent is ever refused. Everything else that goes wrong (bad name, reserved label, wrong kind for a known name) returns a no-op metric (enabled: false) and logs under THREEPM_IO_DEBUG=1.

init also accepts transport knobs, advanced and mainly for tests: reconnect_min_ms, reconnect_max_ms, max_buffer_bytes (outbound budget past which metrics and events are dropped, 64 KiB; the actions declaration and replies have their own and are never crowded out), max_refusals, refusal_window_ms. io.stats is a copy of { sent, dropped, connects, closes, refusals } (metrics and events lines).

Then 3pm trigger <app> lists the actions, 3pm trigger <app> <name> '{"json": true}' runs one on every instance, 3pm profile <app> takes a profile and prints the hot frames; every call is a proc_action event (3pm events -f proc_action).

Electron: the renderer has no Unix socket. In the main process, io.init({ source: 'electron-main' }); io.ipcBridge(ipcMain, { flush_ms?, max_sources? = 16 }); in a renderer, const { createRendererIo } = require('@3pm/api/renderer'); const rio = createRendererIo(ipcRenderer, { source: 'renderer' }) — same surface, one control connection per renderer source. The renderer entry loads no Node builtin; the main process re-validates every relayed entry and bounds what it keeps per source; a renderer cannot take an automatic name (main, pid-*, worker-*) or the main process's own source. A renderer's rio.action() needs an ipcRenderer with on (calls come back on 3pm:action, acknowledgements on 3pm:actions_ack: the declaration is sent again until the bridge has it, so one made before ipcBridge() still arrives; shutdown() withdraws the actions and removes the listeners). Several windows may share a source: each keeps its own actions — a call goes to the window that declared it (the latest, when several declared one name) and only its reply is taken — until it declares again, commits a navigation (a reload too; the next page is asked for its own), crashes or is destroyed; a navigation that never commits (prevented, a download, an HTTP 204) keeps them. The bridge profiles a renderer over webContents.debugger under 3pm:profile when exactly one live window holds the source (give each window its own source to profile it), and fails at once when the debugger is already attached (DevTools open).

Compatibility

The wire is growable: unknown events are still surfaced on event, unknown response tags become a protocol error rather than a hang, and the frozen fixtures of threepm-protocol are part of this package's test suite. PROTOCOL_VERSION is checked on every connect().

Development

npm test                       # unit + wire fixtures + e2e (uses ../../target/{debug,release}/3pm or THREEPM_BIN)

Keywords

3pm

FAQs

Package last updated on 14 Sep 2026

Related posts