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

@tejika/process

v0.5.1

Published

Local daemon spawn, lifecycle and Enkaku client reconnection

Readme

@tejika/process

Local daemon spawn / lifecycle / Enkaku client reconnect for CLIs built on the @tejika/* stack.

  • runDaemon — boot a daemon in the current process. Takes a short-lived boot mutex (@sozai/lock, at <pidPath>.lock) before classifying, cleaning up and binding its socket (no split-brain boot race), writes its presence record, and returns a DaemonHandle.
  • ensureDaemon — connect to a running daemon, spawning and waiting for one if none is reachable, under a single time budget.
  • stopDaemon — signal a running daemon, wait for it to exit (escalating to SIGKILL), and report what happened instead of throwing.
  • getDaemonStatus — classify a daemon's state file into a state union, purely and lock-free (never mutates the filesystem, never blocks behind a boot).
  • createDaemonClient / createDaemonTransport — connect an Enkaku client to a daemon socket, reconnecting automatically on drop.
  • spawnDaemon — spawn the detached daemon process and wait for its socket, surfacing a boot crash as a DaemonBootError instead of a bare timeout.

Examples

runDaemon

The daemon entry ensureDaemon/spawnDaemon spawns must parse --socket-path and --pid-path from argv and pass them through — see "always passed to the child" below for why.

import { parseArgs } from 'node:util'
import { runDaemon } from '@tejika/process'
import { serve } from '@enkaku/server'
import type { MyProtocol } from './protocol.js'

const { values } = parseArgs({
  options: { 'socket-path': { type: 'string' }, 'pid-path': { type: 'string' } },
  strict: false,
})

const handle = await runDaemon<MyProtocol>({
  app: 'my-app',
  socketPath: values['socket-path'] as string,
  pidPath: values['pid-path'] as string,
  serve: (transport) =>
    serve<MyProtocol>({ requireAuth: false, handlers: { ping: () => 'pong' }, transport }),
})

// handle.pid, handle.socketPath, handle.pidPath
await handle.close() // idempotent

ensureDaemon

import { ensureDaemon } from '@tejika/process'
import type { MyProtocol } from './protocol.js'

const client = await ensureDaemon<MyProtocol>({
  app: 'my-app',
  entry: new URL('./daemon-entry.js', import.meta.url).pathname,
})

await client.request('ping') // 'pong'
await client.dispose()

stopDaemon

import { stopDaemon } from '@tejika/process'

const result = await stopDaemon({ app: 'my-app' })
if (!result.stopped) {
  // 'not-running' | 'not-owned' | 'timeout' | 'aborted' | 'busy' | 'error'
  console.log(result.reason)
}

stopDaemon never throws — every outcome, including an unexpected errno from the kill itself (reason: 'error', with the failure on result.error), comes back as a StopResult.

Breaking changes

This package's public surface changed substantially on top of the last published 0.1.0. If you're upgrading, read this section first.

Stop any running daemon before upgrading. The pidfile format changed (see below); a daemon booted by the old code is invisible to the new getDaemonStatus/ensureDaemon, which leads to a confusing (if harmless) DaemonAlreadyRunningError on the next boot attempt instead of a clean takeover.

| Before | After | |---|---| | @tejika/env's getPidPath | getPIDPath — hard rename, no alias | | getDaemonStatus(): DaemonStatus, synchronous, reaped a stale pidfile as a side effect | getDaemonStatus(): Promise<DaemonStatus>, pure — never reaps | | DaemonStatus = { running: boolean; pid?: number; stale: boolean } | discriminated union on state: 'not-running' \| 'stale' \| 'booting' \| 'running' \| 'running-not-owned' — there is no .running boolean anymore | | stopDaemon(): Promise<void> — fire-and-forget SIGTERM, could throw | stopDaemon(): Promise<StopResult> ({ stopped, pid?, reason?, error? }) — waits for exit and escalates to SIGKILL by default, reporting failure rather than throwing. Never throws, not even on your own signal firing: an abort mid-stop resolves with reason: 'aborted' (reason is 'not-running' \| 'not-owned' \| 'timeout' \| 'aborted' \| 'error') rather than rejecting, because the daemon's fate is genuinely unknown at that point and reporting a timeout would be a lie. An already-aborted signal is refused up-front, so no SIGTERM is ever sent | | runDaemon(): Promise<void>, signal handlers always installed | runDaemon(): Promise<DaemonHandle> ({ pid, socketPath, pidPath, close() }); still await-compatible at the call site. Signal handlers are opt-in via handleSignals (default true) | | spawnDaemon's post-spawn wait just timed out on a boot crash | spawnDaemon races the child's exit against the socket wait and throws a DaemonBootError (carrying logPath) immediately on a crash | | ensureDaemon({ timeoutMs }) bounded only the post-spawn connect retries (default 5000ms) | timeoutMs bounds the whole call — connect, spawn, socket wait, retries (default 10000ms). It bounds only the call: neither the timeout nor your signal is wired into the returned client, whose reconnect loop keeps your unclamped connectTimeoutMs and outlives the budget | | spawnDaemon passed --pid-path only when you supplied pidPath | pidPath defaults from app (like socketPath) and is always passed to the child. An entry that parses and honors the flag can never resolve a different lockfile than its parent; an entry that ignores it (e.g. re-deriving paths from app alone) can still diverge under an env override — see the runDaemon example above |

New exports with no 0.1.0 equivalent: createDaemonTransport (the reconnecting-transport seam behind createDaemonClient, for a consumer with its own Client subtype), createDeadline/Deadline (a composable signal+timeout budget), probeSocket, and typed errors DaemonAlreadyRunningError / DaemonBootError.

probeSocket returns 'live' | 'dead' | 'forbidden' | 'unknown'. Only 'dead' is load-bearing, and only 'dead' is dangerous: it is the verdict that authorises unlinking a socket file. So it is stated positively — ECONNREFUSED, ENOENT, ENOTSOCK — and everything else fails safe. 'forbidden' (EACCES/EPERM) means another user's daemon is listening; 'unknown' means the connect failed for a reason that says nothing about the peer (EMFILE, ENOMEM, …, i.e. our problem, not the daemon's). isSocketLive is true for all three non-dead verdicts, so a machine under fd pressure can never unlink a healthy daemon's socket.

Locking moved out of the pidfile (0.3.0). The pidfile is still JSON at the same path, with the same fields — it is now a DaemonState ({ pid, socketPath, startedAt, ready }), a presence record and nothing more. Exclusion is a separate, short-lived mutex at <pidPath>.lock, provided by @sozai/lock and held only across the boot, stop and shutdown critical sections — never for the daemon's lifetime, so getDaemonStatus never blocks behind a live daemon.

| Before | After | |---|---| | LockRecord | DaemonState — same fields, exported as a type | | runDaemon({ bootGraceMs }), getDaemonStatus({ bootGraceMs }) | gone. An unready record is 'booting' to an observer, and provably abandoned to a mutex holder — no clock is consulted | | — | runDaemon({ lockPath, lockTimeoutMs }) and stopDaemon({ lockPath, lockTimeoutMs }). lockPath defaults to `${pidPath}.lock`, so nothing needs configuring, and no new CLI flag is passed to the child | | — | StopResult.reason: 'busy' — the mutex is held by a concurrent boot or stop, so nothing was attempted | | — | TimeoutInterruption (re-exported from @sozai/lock) can escape runDaemon when the boot mutex cannot be taken within lockTimeoutMs. Distinct from DaemonAlreadyRunningError: "someone is booting or stopping and will not let go" is not "someone is already serving" | | DaemonAlreadyRunningError.pid: number, -1 when the daemon holds the socket with no state record naming it | pid?: numberundefined there instead. -1 is not a pid: process.kill(-1, sig) signals every process you may signal, so an error object carrying it hands process.kill(err.pid, …) a weapon | | — | getLockPathFor(pidPath) is exported. Anything that passes an explicit pidPath (spawnDaemon does, by default) can now name the mutex guarding it; @tejika/env's getLockPath(app) only derives it from an app name |

Why: the old pidfile was a mutex and a presence record at once, so every boot and every stop was a check-then-act guarded by the lockfile's inode. That guard does not hold — the kernel recycles an inode number the moment a file is unlinked, so a reaper could unlink the very lock a fresh daemon had just claimed on the recycled inode. @sozai/lock guards on a per-claim nonce, plus an OS boot ID so a pid is only trusted when it comes from this boot.

'booting' is still a real, distinct DaemonStatus state (record written, socket not yet bound) and must not be treated as 'running'. A daemon booted by 0.2.0 is still readable by 0.3.0 — the record format did not change — but it holds no boot mutex, so stop it before upgrading rather than relying on the overlap.

stopDaemon never signals a 'booting' record's pid: it removes the record and reports reason: 'not-running'. A ready: false record is only ever written from inside the mutex, so one read from inside the mutex was written by a process that no longer holds it — it is abandoned by construction, and its pid is either dead or recycled onto an unrelated process. (The record outlives a reboot, so this is ordinary: SIGKILL a daemon mid-boot, reboot, run stop.) runDaemon reclaims such a record on exactly the same proof. A recycled pid on a ready: true record is caught instead by the socket probe, which demotes it to 'stale'.