Run any async task on a heartbeat. Capture every result. Never miss a pulse.
From polling remote APIs to calculating live metrics, syncing edge state, to reading sensor data , StatePulse orchestrates it all with surgical precision.
StatePulse is a zero-dependency periodic execution and state management engine. Define a task. Give it an interval. StatePulse takes care of everything else, retrying on failure, caching the result, respecting overlap boundaries, persisting history, and tearing down cleanly when the process exits.
Run it in a Node.js server, a Deno edge function, a frontend, a browser worker, or an embedded runtime. StatePulse doesn't care where it runs, it just runs, reliably!.
There is no other library that combines background execution control, automatic state retention, and pluggable persistence into one coherent, dependency-free primitive.
- β‘ Absolute Execution Control: Stop wrestling with
setIntervaldrifts. Define exact polling intervals, robust retry limits, and smart overlap boundaries (skip vs. concurrent). - βοΈ Truly Task-Agnostic: From synchronous calculations and long-running I/O operations to fire-and-forget background jobs. If it can execute, StatePulse can orchestrate it, even if it doesn't return state.
- πͺΆ Featherweight & Zero-Dependency: No bloat, no conflicts. A minuscule footprint that's equally at home in a heavily constrained IoT device as it is in a massive cloud fleet.
- πΎ State That Survives: Instantly plug in Redis, Postgres, or any key-value store to persist state across process restarts or keep it strictly lightning-fast in-memory.
- π‘οΈ Bulletproof Memory Management: Built for services that never sleep. Bounded history buffers and automatic eviction mean zero memory leaks and no OOM crashes.
- β±οΈ Built-in Telemetry: Every state snapshot automatically includes an unintrusive performance breakdown (like execution
timeTaken) so you can monitor task health without heavy profilers. - π¦ Resilient Fault Tolerance: Exceptions won't crash the engine. Execution faults and thrown errors are seamlessly absorbed and surfaced as normal state updates, keeping your application totally bulletproof.
- π Graceful By Default: Native
AbortSignalpropagation ensures cleanly aborted in-flight tasks and safe teardowns the moment an exit signal is fired. - π Run It Anywhere: Drop it into NestJS, Fastify, Next.js, React, Express, or use it raw. Node.js, Deno, Bun, or the browser, it just works.
npm install statepulseManage periodic state execution and background data polling with absolute reliability.
import { StatePulse } from "statepulse";
// 1. Initialize StatePulse (Zero global config needed by default)
const pulse = new StatePulse();
// 2. Register a declarative state node
await pulse.register({
key: "bitcoin-price",
run: async (signal) => {
// AbortSignal is automatically provided for graceful cancellations!
const response = await fetch("https://api.coindesk.com/v1/bpi/currentprice.json", { signal });
const data = await response.json();
return parseFloat(data.bpi.USD.rate_float);
},
refreshPolicy: {
intervalMs: 10000, // Poll every 10 seconds
overlapAction: "skip", // Skip execution if the previous run is still active
},
retryPolicy: {
count: 3, // Automatically retry up to 3 times on failure
},
stateConfig: {
inMemory: true, // Cache the last successful run in-memory
history: {
maxHistoryLength: 50, // Bounded rolling history to prevent memory leaks
},
},
logErrors: (err) => console.error(`Failed to fetch BTC price: ${err}`),
});
// 3. Instantly retrieve the latest snapshot (zero blocking!)
const snapshot = await pulse.get<number>("bitcoin-price");
if (snapshot) {
console.log(`Latest USD Price: $${snapshot.value}`);
console.log(`Last Updated At: ${new Date(snapshot.updatedAt).toISOString()}`);
console.log(`Fetch Duration: ${snapshot.timeTaken}ms`);
}
// 4. Retrieve execution history safely
const history = pulse.getHistory<number>("bitcoin-price");
console.log(`History records available: ${history.length}`);Create one StatePulse instance and register all your polling tasks on it using .register(). Each registered node runs its own independent background loop with its own interval, retry policy, and history buffer.
Do not create a new StatePulse instance per task, the centralized orchestrator is highly optimized to efficiently handle hundreds of nodes simultaneously.
// β
Correct single instance, multiple nodes
const pulse = new StatePulse();
await pulse.register({ key: "exchange-rate", run: fetchExchangeRates, refreshPolicy: { intervalMs: 10000 } });
await pulse.register({ key: "db-health", run: pingDatabase, refreshPolicy: { intervalMs: 60000 } });
// β Wrong wasteful, no benefit
const pricePulse = new StatePulse();
const healthPulse = new StatePulse();StatePulse scales from a single Node.js process to a distributed microservice fleet. Easily plug in any key-value store (Redis, Keyv, etc.) by implementing the PersistenceAdapter interface.
import { StatePulse, PersistenceAdapter } from "statepulse";
import Redis from "ioredis";
const redis = new Redis();
const redisAdapter: PersistenceAdapter = {
// Retrieve a snapshot
get: async (key) => {
const data = await redis.get(key);
return data ? JSON.parse(data) : null;
},
// Save the latest state with uniform millisecond TTL
set: async (key, value, ttlMs) => {
if (ttlMs) {
await redis.set(key, JSON.stringify(value), "PX", ttlMs);
} else {
await redis.set(key, JSON.stringify(value));
}
},
// (Optional) Bulk insert history cycle batches
addHistory: async (key, entries) => {
await redis.lpush(`history:${key}`, ...entries.map(e => JSON.stringify(e)));
}
};
const pulse = new StatePulse({
persistence: redisAdapter, // Set as global fallback persistence adapter
});
await pulse.register({
key: "exchange-rate",
run: fetchRates,
stateConfig: {
history: {
historyCycle: 10, // Automatically flush history queue to redis every 10 cycles!
maxHistoryLength: 50, // Bound local queue to 50 snapshots
},
},
});What happens when your backend fetch takes longer than its polling interval? StatePulse provides intelligent overflow handling out of the box:
skip(Default): If a task is still running when the next cycle is scheduled to start, StatePulse safely skips the missed cycle. The next cycle will schedule precisely atnextExpectedTick + interval.overlap: The subsequent polling cycle starts immediately at its scheduled time, running concurrently with the in-flight task for maximum throughput.
StatePulse is unopinionated and works anywhere.
Use the official @statepulse/nestjs module for robust dependency injection and lifecycle management:
npm install statepulse @statepulse/nestjsimport { Module } from "@nestjs/common";
import { StatePulseModule } from "@statepulse/nestjs";
@Module({
imports: [StatePulseModule.forRoot()],
})
export class AppModule {}Decorate your instance and automatically hook into onClose for graceful zero-downtime shutdown:
import Fastify from "fastify";
import { StatePulse } from "statepulse";
const fastify = Fastify();
const pulse = new StatePulse();
fastify.decorate("pulse", pulse);
fastify.addHook("onClose", () => { pulse.terminate(); });
await pulse.register({
key: "db-health",
run: async () => { /* ... */ },
refreshPolicy: { intervalMs: 30000 },
});
fastify.get("/health", async () => {
return await pulse.get("db-health");
});Just instantiate and use directly:
import express from "express";
import { StatePulse } from "statepulse";
const app = express();
const pulse = new StatePulse();
await pulse.register({
key: "exchange-rates",
run: async () => { /* ... */ },
refreshPolicy: { intervalMs: 15000 },
});
app.get("/rates", async (_req, res) => {
const snapshot = await pulse.get("exchange-rates");
res.json(snapshot);
});
// Safe cleanup on exit
process.on("SIGTERM", () => {
pulse.terminate();
process.exit(0);
});The main orchestrator class.
persistence?: PersistenceAdapter | null- Optional global persistence adapter.enableSignalHandling?: boolean- Iftrue, automatically gracefully aborts tasks onSIGINT/SIGTERM. Defaults totrue.
Registers and instantly spins up a periodic task loop.
key: string- Unique identifier for the task.run: (signal: AbortSignal) => T | Promise<T>- Polling function.logErrors?: boolean | ((error: string) => void)- Defaults tofalse.refreshPolicyintervalMs- Interval duration between polls. Defaults to300000(5 minutes).overlapAction-"skip"(default) or"overlap".
retryPolicycount- Maximum retries on execution failure. Defaults to3.
stateConfiginMemory?: boolean- Cache locally? Defaults totrue.persistence?: NodePersistenceConfig- Enable/disable or override persistence for this specific node.history?: { historyCycle?: number, keepHistoryAfterSave?: boolean, maxHistoryLength?: number }- Tune exactly how history is buffered and flushed.
Instantly retrieves the latest state snapshot for the given key without blocking.
Returns an immutable clone of the rolling history of execution snapshots.
Halts execution for a specific node, and clears its memory storage and history.
Gracefully stops all active polling loops, cancels in-flight requests, and clears memory stores.
A getter returning the keys of all nodes currently executing.
MIT Β© Leo