lastcall
v0.1.1
Published
Graceful process lifecycle management for Node.js and Bun
Maintainers
Readme
lastcall
Graceful process lifecycle management for Node.js and Bun.
Published on npm as lastcall. Source repository: sakurablush/lastcall. Documentation: sakurablush.github.io/lastcall.
lastcall is a zero-dependency, TypeScript-first library for managing graceful shutdown in production applications. It handles SIGTERM/SIGINT from Docker, Kubernetes, and PM2 — with priorities, dependencies, phases, and HTTP server draining built in.
~22 KB ESM bundle · zero production dependencies · 100% line coverage · integration tests via child processes
npm install lastcallTry it locally (clone, install, run tests):
git clone https://github.com/sakurablush/lastcall.git && cd lastcall && npm ci && npm testDocumentation
Browse online: sakurablush.github.io/lastcall
📖 VitePress docs cover getting started, API reference, guides (Kubernetes, Docker, Bun), and examples.
Automation: CI and automation · Security: SECURITY.md · See Contributing.
Cursor users: project Agent Skills in .cursor/skills/ guide the AI through development, testing, and docs.
Community
| Document | Purpose | | ---------------------------------------- | ---------------------------------- | | CONTRIBUTING.md | Development setup and PR checklist | | CODE_OF_CONDUCT.md | Community standards | | CONTRIBUTORS.md | How contributors are recognized | | CHANGELOG.md | Release history | | SECURITY.md | Vulnerability reporting |
Why lastcall?
| Problem | lastcall solution |
| ------------------------------------- | ------------------------------------- |
| In-flight requests dropped on SIGTERM | withHttpServer() drains connections |
| Cleanup runs in wrong order | Priorities + dependency graph |
| Custom signal handlers are buggy | Test-covered, idempotent shutdown |
| Hard to test shutdown | simulateSignal(), autoExit: false |
| No observability | Metrics hooks + lifecycle events |
Quick start
import { createServer } from 'node:http';
import { createLastcall } from 'lastcall';
const server = createServer((req, res) => {
res.end('ok');
});
const lastcall = createLastcall();
lastcall.withHttpServer(server); // before listen()
lastcall.register(
'database',
async () => {
await db.disconnect();
},
{ critical: true },
);
server.listen(3000);
// SIGTERM / SIGINT trigger graceful shutdown automatically.API
createLastcall(options?)
| Option | Default | Description |
| --------------------------- | ---------------------------------------- | ------------------------------------ |
| signals | ['SIGTERM', 'SIGINT', 'SIGHUP'] | Signals to listen for |
| shutdownTimeoutMs | 30000 | Global shutdown timeout (> 0) |
| phases | ['pre', 'drain', 'cleanup', 'post'] | Shutdown phases (no duplicates) |
| onHandlerStart | — | Metrics hook when handler starts |
| onHandlerEnd | — | Metrics hook when handler ends |
| captureUncaughtException | false | Shutdown on uncaught exceptions |
| captureUnhandledRejection | false | Shutdown on unhandled rejections |
| autoExit | true | Call process.exit() after shutdown |
| logger | console.error with [lastcall] prefix | Custom logger |
lastcall.register(name, fn, options?)
| Option | Default | Description |
| ----------- | ----------- | --------------------------------------------- |
| priority | 100 | Lower runs earlier (within same phase) |
| timeoutMs | — | Per-handler timeout |
| critical | false | Failure affects exit code |
| deps | [] | Names that must finish first (same phase) |
| phase | 'cleanup' | Shutdown phase |
Shutdown phases
pre— Mark unhealthy, stop accepting new workdrain— Wait for in-flight requests/jobscleanup— Close DB, Redis, queues, filespost— Final logs, metrics flush
getDefaultLastcall(options?)
Returns a process-wide singleton. Options apply only on the first call.
Other methods
await lastcall.shutdown('reason', 'SIGTERM'); // Manual trigger (optional signal)
lastcall.isShuttingDown(); // True once shutdown starts (stays true after completion)
lastcall.isShutdownComplete(); // True after shutdown finishes
lastcall.getState(); // 'idle' | 'shutting_down' | 'done' | 'force_exit'
lastcall.simulateSignal('SIGTERM'); // Testing
lastcall.withHttpServer(server); // HTTP drain
lastcall.unregister('name'); // Remove handler (blocked if others depend on it)
lastcall.hasHandler('name'); // Check if a handler is registered
lastcall.listHandlers(); // Snapshot for logging / health dashboards
lastcall.on('beforeShutdown', fn); // EventsEvents
beforeShutdown—{ reason, signal? }afterShutdown—{ exitCode }handlerError—{ name, phase, durationMs, error }handlerCompleted—{ name, phase, durationMs, error? }
HTTP server integration
import http from 'node:http';
import { createLastcall } from 'lastcall';
const server = http.createServer(handler);
const lastcall = createLastcall();
lastcall.withHttpServer(server, {
drainTimeoutMs: 10_000,
priority: 10,
phase: 'drain',
});
server.listen(3000);Works with Node.js http/https/http2 servers. Framework examples: Express, Fastify, Hono.
Dependencies between handlers
lastcall.register(
'database',
async () => {
/* ... */
},
{ phase: 'cleanup' },
);
lastcall.register(
'cache',
async () => {
/* ... */
},
{
phase: 'cleanup',
deps: ['database'],
},
);Handlers with deps wait for their dependencies to complete within the same phase. Circular dependencies throw at shutdown time. Cross-phase dependencies are rejected at registration time.
Examples
| Example | Description | | ------------------------------------------------ | ---------------------- | | standalone-worker | Worker without HTTP | | express | Express + health check | | fastify | Fastify server | | hono | Hono server | | node-http | Raw Node.js http | | docker-k8s | K8s/Docker guide | | nextjs-standalone | Next.js custom server | | pm2-cluster | PM2 cluster mode |
See examples/README.md for how to run them.
Testing
npm test # silent
npm run test:log # verbose proof logging (LASTCALL_TEST_LOG=1)
npm run test:coverage # 100% line coverage (98% branch threshold)import { createLastcall } from 'lastcall';
const lastcall = createLastcall({
autoExit: false,
signals: [],
});
lastcall.register('cleanup', vi.fn());
await lastcall.shutdown('test');See docs/testing/running-tests.md for the full guide.
Kubernetes / Docker
See examples/docker-k8s/ for deployment best practices.
Key settings:
- Set
terminationGracePeriodSeconds>shutdownTimeoutMs - Use
preStophook for load balancer drain - Register health check as unhealthy in
prephase
// Readiness probe — fail fast once shutdown starts
app.get('/health', (_req, res) => {
if (lastcall.isShuttingDown()) {
res.status(503).send('shutting down');
return;
}
res.status(200).send('ok');
});Bun compatibility
lastcall works with Bun. Signal handling may differ on Windows — SIGHUP is not available on all platforms and is safely skipped.
Troubleshooting
Handler hangs forever
Set timeoutMs on the handler. lastcall will log a clear timeout message and continue shutdown.
Multiple SIGTERM signals
Shutdown is idempotent — subsequent signals are ignored while shutting down.
Exit code is 1
A handler marked critical: true failed or timed out. Check logs for Handler "name" failed.
Tests call process.exit
Use autoExit: false in test configuration. Use resetDefaultLastcall() when testing code that calls getDefaultLastcall().
Sustain development (optional)
lastcall is MIT-licensed and free on npm — no paywall, no paid tier, no feature lock.
Keeping it current still has a real cost: cross-platform signal testing, integration suites via child processes, docs, CI, and compatibility with Node.js and Bun releases. That work runs on a self-funded stack.
Contribute in code if that fits you: Issues or a pull request per CONTRIBUTING.md.
Financial support is optional — only if lastcall saved you time or you want to help cover the next development cycle. No tiers, no perks, no obligation:
| Coin | Address |
| -------- | --------------------------------------------- |
| Bitcoin | bc1qcrj9wreunffxm75fcz0a2gjkw87jshcwvmxv68 |
| Ethereum | 0xB85Bf389E5fC5E12636FB6F17b68Df7fC990a3dA |
| Litecoin | ltc1q0nsarncmnz8vk34dav7l0vgu9m5k48drr8z6js |
| Dogecoin | D6YGoYWpFZxW8JXvEfT5iB9uNXcs8AeY7L |
License
MIT
