npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@3pm/api

v0.1.1

Published

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

Readme

@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.

| Method | Wire request | Returns | |---|---|---| | connect() | ping (checks protocol === 1) | this | | ping() | ping | {pid, version, protocol} | | start(opts \| opts[], {ifChanged?}) | start | ProcessRecord[] | | scale(name, n) | scale | ProcessRecord[] | | stop(target) | stop | ProcessRecord[] (returns once dead) | | restart(target, {toggleWatch?}) | restart | ProcessRecord[] | | delete(target, {flush?}) / del | delete | ProcessRecord[] | | reset(target) | reset | ProcessRecord[] | | sendSignal(sig, target, {group?}) | send_signal | ProcessRecord[] | | list() / ls() | list | ProcessRecord[] | | describe(target) / show() | describe | ProcessRecord[] (every instance) | | save() / resurrect() | save / resurrect | {path, count} / ProcessRecord[] | | killDaemon() | kill_daemon | — (buses stay open to see daemon:shutdown) | | internals() / hostMetrics() | internals / host_metrics | growable snapshots | | flushLogs(t?) / rotateLogs(t?) / reloadLogs() | *_logs | touched file paths | | request(cmd, fields) | anything | raw 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 as | Also as | Payload | |---|---|---| | log:out, log:err | log_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:rotated | log_rotated | {id, name, instance, path} | | lagged | — | {missed} — the subscriber fell behind | | daemon:shutdown | daemon_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.

| Call | Notes | |---|---| | 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)