Skip to content

leodarkseid/statepulse

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

12 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

StatePulse ⚑

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

✨ Why StatePulse?

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 setInterval drifts. 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 AbortSignal propagation 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.

πŸš€ Installation

npm install statepulse

πŸ› οΈ Quick Start

Manage 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}`);

πŸ“Œ Usage Guidance: The Single Instance Pattern

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();

πŸ’Ύ Custom Persistence Adapters

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
    },
  },
});

⚑ Scheduling & Overlap Actions

What happens when your backend fetch takes longer than its polling interval? StatePulse provides intelligent overflow handling out of the box:

  1. 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 at nextExpectedTick + interval.
  2. overlap: The subsequent polling cycle starts immediately at its scheduled time, running concurrently with the in-flight task for maximum throughput.

πŸ”Œ Framework Integration

StatePulse is unopinionated and works anywhere.

NestJS

Use the official @statepulse/nestjs module for robust dependency injection and lifecycle management:

npm install statepulse @statepulse/nestjs
import { Module } from "@nestjs/common";
import { StatePulseModule } from "@statepulse/nestjs";

@Module({
  imports: [StatePulseModule.forRoot()],
})
export class AppModule {}

Fastify

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");
});

Express

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);
});

πŸ“‹ API Reference

StatePulse

The main orchestrator class.

new StatePulse(config?: Partial<StatePulseConfig>)

  • persistence?: PersistenceAdapter | null - Optional global persistence adapter.
  • enableSignalHandling?: boolean - If true, automatically gracefully aborts tasks on SIGINT / SIGTERM. Defaults to true.

register<T>(node: RegisterNodeConfig<T>): Promise<void>

Registers and instantly spins up a periodic task loop.

RegisterNodeConfig<T>
  • key: string - Unique identifier for the task.
  • run: (signal: AbortSignal) => T | Promise<T> - Polling function.
  • logErrors?: boolean | ((error: string) => void) - Defaults to false.
  • refreshPolicy
    • intervalMs - Interval duration between polls. Defaults to 300000 (5 minutes).
    • overlapAction - "skip" (default) or "overlap".
  • retryPolicy
    • count - Maximum retries on execution failure. Defaults to 3.
  • stateConfig
    • inMemory?: boolean - Cache locally? Defaults to true.
    • 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.

get<T>(key: string): Promise<StateSnapshot<T> | null>

Instantly retrieves the latest state snapshot for the given key without blocking.

getHistory<T>(key: string): StateSnapshot<T>[]

Returns an immutable clone of the rolling history of execution snapshots.

unregister(key: string): void

Halts execution for a specific node, and clears its memory storage and history.

terminate(): void

Gracefully stops all active polling loops, cancels in-flight requests, and clears memory stores.

activeRuns: string[]

A getter returning the keys of all nodes currently executing.


πŸ“„ License

MIT Β© Leo

About

StatePulse is a Industrial-grade background polling and state management engine for Node.js. Designed for mission-critical services, StatePulse provides periodic execution, automatic retry policies, bounded history buffers, and pluggable persistence adapters.

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors