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

@schedule-kit/deadline

v0.1.3

Published

A tiny, framework-agnostic scheduler for running a callback shortly before a *dynamic* deadline.

Readme

@schedule-kit/deadline

A tiny, framework-agnostic scheduler for running a callback shortly before a dynamic deadline.

DeadlineScheduler watches an expiry, arms a one-shot timer, runs your callback when the deadline is close, then re-reads the expiry and schedules the next cycle. It has no idea what the deadline represents — a lease, a reservation hold, a cached value, anything with an expiry works.

  • Zero dependencies
  • One timer per scheduler, no polling
  • Works with sync or async callbacks
  • Pause/resume/stop/reset as distinct, well-defined operations
  • Clock, timer, and error handling are all injectable
  • Never enters an unbounded immediate-fire loop, whatever the deadline does

Install

npm install @schedule-kit/deadline

Quick start

import { DeadlineScheduler } from "@schedule-kit/deadline";

const reservation = { expiresAt: initialExpiry };

const scheduler = new DeadlineScheduler({ leadTimeMs: 60_000 });

scheduler.watch(
  () => reservation.expiresAt,
  async () => {
    reservation.expiresAt = await renewReservation(reservation.id);
  }
);

leadTimeMs before reservation.expiresAt, the callback runs. Once it resolves, the scheduler reads reservation.expiresAt again — since the callback just updated it, the next cycle is armed against the new value automatically.

Core concept

watch()
  │
  ▼
read the deadline
  │
  ▼
arm a timer for (deadline − leadTime)
  │
  ▼
timer fires → run onDue()
  │
  ▼
wait for onDue() to settle
  │
  ▼
read the deadline again ──► arm the next cycle

The scheduler never caches the deadline across cycles — it's re-read every time, so it always schedules against the latest value. The caller owns all state; the scheduler only tracks timing.

Lifecycle

scheduler.watch(getDeadline, onDue); // start, or replace the current watch
scheduler.pause();                   // temporarily stop the timer, keep the watch
scheduler.resume();                  // re-read the deadline and continue
scheduler.stop();                    // stop scheduling, keep the watch
scheduler.reset();                   // stop and forget the watch entirely

pause()/resume() and stop()/reset() are deliberately separate: pausing is a temporary hold you expect to resume, while stopping ends the current watch (reset() additionally forgets it, so a later watch() starts clean rather than looking like a resumed one).

API

new DeadlineScheduler(config?)

| Option | Type | Default | Description | | --- | --- | --- | --- | | leadTimeMs | number | 60_000 | How long before the deadline onDue should run. | | clock | DeadlineClock | createSystemClock(onError) | Supplies the current time and parses deadline values. | | setTimer | SetDeadlineTimer | setTimeout | Creates a one-shot timer. | | clearTimer | ClearDeadlineTimer | clearTimeout | Cancels a timer. | | onError | DeadlineErrorHandler | console.error | Called for a failed onDue or a halted busy-loop-risk cycle. |

watch(getDeadline, onDue)

Starts watching. getDeadline returns the current deadline as an ISO 8601 string, or undefined if there's nothing to watch. Calling watch() again replaces the current watch and starts a fresh cycle — any in-flight onDue from the previous watch is ignored when it settles.

pause() / resume()

pause() cancels the active timer but keeps the watch. resume() re-reads the deadline and arms a new timer. Safe to call repeatedly; calling either while not watching is a no-op.

stop() / reset()

stop() cancels the timer and stops scheduling; the watch (the last getDeadline/onDue pair) is retained. reset() does the same and additionally forgets the watch, so watch() is required to start again.

isRunning / isPaused

isRunning is true whenever a watch is active — including while paused, and while idle after a halted cycle (see below). isPaused is true only after pause().

leadTimeMs / clock

Read back the configured lead time and the DeadlineClock in use (the injected one, or the default createSystemClock() instance).

isExpired

true if there's no active watch, getDeadline() returns undefined, the deadline is unparseable, or the current time is at or past deadline − leadTimeMs. Read-only and side-effect free — checking it never arms, cancels, or reports anything, so it's safe to poll from application code that just wants to ask "is it due?" without waiting on the timer.

Public exports

export class DeadlineScheduler { ... }
export interface DeadlineSchedulerConfig { ... }
export interface DeadlineClock { now(): number; parse(value: string): number }
export type GetDeadline = () => string | undefined;
export type DeadlineCallback = () => void | Promise<void>;
export type DeadlineTimerHandle = unknown;
export type SetDeadlineTimer = (delayMs: number, onFire: () => void) => DeadlineTimerHandle;
export type ClearDeadlineTimer = (handle: DeadlineTimerHandle) => void;
export type DeadlineErrorHandler = (error: unknown) => void;
export function createSystemClock(onError: DeadlineErrorHandler): DeadlineClock;

Everything a custom clock, setTimer, clearTimer, or onError implementation needs is exported — nothing else is public.

Behavior and error semantics

  • No deadline. If getDeadline() returns undefined, the scheduler stays disarmed until the value changes and resume()/watch() is called.
  • Invalid deadline. If clock.parse() returns NaN, the scheduler disarms. Reporting is the clock's job, not the scheduler's — it has no way to describe why a value is invalid. createSystemClock reports unparseable values through onError; a custom clock that stays silent on NaN will leave the scheduler quietly disarmed.
  • onDue fails. A rejection or synchronous throw is reported via onError; the scheduler does not automatically retry. Call resume() once the underlying issue is addressed.
  • Busy-loop guard. After a cycle completes, the scheduler re-reads the deadline and re-arms automatically. If that re-arm computes a delay that's already due (≤ 0), it does not arm — arming would fire immediately, run onDue, and land back here to compute another already-due delay, over and over. This holds regardless of whether the deadline value itself changed between cycles: a getDeadline that returns a different but still-overdue value every time is caught just as well as one returning the same value. The scheduler reports it via onError and goes idle; resume() retries once you're ready. Only an explicit watch()/resume() call is allowed to fire immediately — that's a deliberate request, not a loop.
  • Stale async work. If onDue is still running when watch()/stop()/reset() is called, its eventual result is ignored — it won't re-arm a timer or report an error for a cycle you've already superseded.

Timer limits

The default setTimer/clearTimer use the platform's setTimeout/clearTimeout, whose maximum single delay is about 24.8 days. For longer deadlines, supply a custom setTimer that chains shorter delays internally.

License

MIT