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

@timekeeper-countdown/core

v0.2.0

Published

Lightweight countdown engine that powers the Timekeeper packages. Pure TypeScript with no runtime dependencies.

Readme

@timekeeper-countdown/core

Lightweight countdown engine used by Timekeeper Countdown. This package exposes the finite-state machine, snapshot APIs, formatting helpers, and testing utilities that power the React hook and future framework adapters.

  • Written in TypeScript with zero runtime dependencies.
  • Ships modern ESM bundles and type definitions.
  • Designed to run in browsers, Node.js, or custom runtimes.
  • Tested with deterministic fake timers for reliable behavior.

Looking for the React hook? Install @timekeeper-countdown/react for an idiomatic React API that wraps this engine.


Installation

npm install @timekeeper-countdown/core

The published bundle is pure ESM. When targeting CommonJS environments use a bundler that understands type: "module" packages.

Supported runtimes:

  • Node.js 18+
  • Modern browsers (ES2022 modules)

Quick Start

High-level helper (Countdown)

import { Countdown, TimerState } from '@timekeeper-countdown/core';
import { formatTime } from '@timekeeper-countdown/core/format';

const countdown = Countdown(300, {
  onSnapshot: snapshot => {
    const { minutes, seconds } = formatTime(snapshot);
    timerElement.textContent = `${minutes}:${seconds}`;
  },
  onStateChange: state => {
    if (state === TimerState.STOPPED) {
      console.log('Finished!');
    }
  },
  onError: error => {
    console.error('Countdown error:', error);
  },
});

countdown.start(); // Begin the countdown

Countdown wraps the lower-level engine and returns convenient methods:

countdown.start();                   // boolean (false when invalid transition)
countdown.pause();
countdown.resume();
countdown.reset(nextInitialSeconds?);
countdown.stop();
countdown.destroy();                 // dispose timers and listeners

countdown.getSnapshot();             // CountdownSnapshot
countdown.getCurrentState();         // TimerState
countdown.getMinutes();              // string e.g. "05"

Low-level engine (CountdownEngine)

import { CountdownEngine, TimerState } from '@timekeeper-countdown/core';

const engine = CountdownEngine(90, {
  tickIntervalMs: 50,
  onSnapshot: snapshot => {
    console.log(snapshot.totalSeconds);
  },
  onStateChange: (state, snapshot) => {
    if (state === TimerState.STOPPED && snapshot.isCompleted) {
      console.log('Done!');
    }
  },
  onError: error => {
    console.error('Timer failure', error);
  },
});

engine.start();

CountdownEngine exposes fine-grained control:

  • start, pause, resume, reset(nextInitialSeconds?), stop, setSeconds(value), destroy
  • getSnapshot() returns the latest snapshot.
  • subscribe(listener) emits the current snapshot immediately and on every tick.

Snapshot structure:

interface CountdownSnapshot {
  initialSeconds: number; // starting value
  totalSeconds: number; // remaining seconds, floor-clamped
  parts: {
    years: number;
    weeks: number;
    days: number;
    hours: number;
    minutes: number;
    seconds: number;
    totalDays: number;
    totalHours: number;
    totalMinutes: number;
  };
  state: TimerState; // IDLE | RUNNING | PAUSED | STOPPED
  isRunning: boolean;
  isCompleted: boolean;
}

State Transitions

The engine enforces a strict state machine. Invalid transitions are silently ignored and return false.

           start()
  IDLE ──────────────► RUNNING
   ▲                    │    │
   │                    │    │
   │   reset()    pause()   stop() / complete()
   │                    │    │
   │                    ▼    │
   │                 PAUSED  │
   │                    │    │
   │   reset()          │    │
   │◄───────────────────┘    │
   │                         │
   │   reset()               ▼
   │◄──────────────────── STOPPED

| From | Action | To | | --------- | ---------- | --------- | | IDLE | start() | RUNNING | | RUNNING | pause() | PAUSED | | RUNNING | reset() | IDLE | | RUNNING | stop() | STOPPED | | PAUSED | resume() | RUNNING | | PAUSED | reset() | IDLE | | PAUSED | stop() | STOPPED | | STOPPED | reset() | IDLE |


Formatting Helpers

Use the helpers exported at @timekeeper-countdown/core/format to avoid reimplementing padStart logic.

import { formatTime, formatMinutes, formatSeconds, Formatter } from '@timekeeper-countdown/core/format';

const snapshot = engine.getSnapshot();

formatTime(snapshot); // { minutes: "01", seconds: "30" }
formatMinutes(snapshot); // "01"

const formatter = Formatter();
formatter.formatHours(snapshot); // memoised string helpers

All helpers accept either a snapshot or any object that exposes totalSeconds.


Testing Utilities

The package includes utilities under @timekeeper-countdown/core/testing-utils to make unit tests deterministic.

import {
  createFakeTimeProvider,
  toTimeProvider,
  buildSnapshot,
  buildSnapshotSequence,
  assertSnapshotState,
  assertSnapshotCompleted,
  assertRemainingSeconds,
  TimerState,
} from '@timekeeper-countdown/core/testing-utils';

createFakeTimeProvider(options?)

Provides a controllable clock for deterministic testing.

interface FakeTimeOptions {
  startMs?: number;         // default: 0
  tickMs?: number;          // default: 1000 — default step for advance()
  highResolution?: boolean; // default: true
}

interface FakeTimeProvider extends TimeProvider {
  advance(ms?: number): number; // advances by ms (or tickMs if omitted), returns current time
  set(ms: number): number;      // sets clock to absolute value, returns current time
  reset(): number;              // resets to startMs, returns current time
  getTime(): number;            // returns current time without advancing
  now(): number;                // same as getTime (inherited from TimeProvider)
  isHighResolution: boolean;
  type: 'fake';
}

function createFakeTimeProvider(options?: FakeTimeOptions): FakeTimeProvider;
  • Negative or non-finite values are clamped to 0.
  • Values above Number.MAX_SAFE_INTEGER are clamped to Number.MAX_SAFE_INTEGER.
  • advance() without a parameter uses tickMs as the default step.

toTimeProvider(fake)

function toTimeProvider(fake: FakeTimeProvider): TimeProvider;

Converts a FakeTimeProvider to a read-only TimeProvider, used to pass to CountdownEngine or useCountdown.

buildSnapshot(options?)

interface SnapshotOptions {
  initialSeconds?: number; // fallback: totalSeconds, then 0
  totalSeconds?: number;   // fallback: initialSeconds, then 0
  state?: TimerState;      // fallback: IDLE if totalSeconds > 0, else STOPPED
}

function buildSnapshot(options?: SnapshotOptions): CountdownSnapshot;
const snapshot = buildSnapshot({ totalSeconds: 90, state: TimerState.RUNNING });
// snapshot.parts.minutes === 1
// snapshot.parts.seconds === 30
// snapshot.isRunning === true

buildSnapshotSequence(options?)

interface SequenceOptions extends SnapshotOptions {
  step?: number;  // default: 1 — decrement per snapshot
  count?: number; // default: 1 — number of snapshots
}

function buildSnapshotSequence(options?: SequenceOptions): CountdownSnapshot[];

Generates count snapshots, decrementing totalSeconds by step each iteration. The last snapshot with remaining === 0 gets state: STOPPED; all others get state: RUNNING.

const sequence = buildSnapshotSequence({ totalSeconds: 4, step: 2, count: 3 });
// sequence[0].totalSeconds === 4  (RUNNING)
// sequence[1].totalSeconds === 2  (RUNNING)
// sequence[2].totalSeconds === 0  (STOPPED)

assertSnapshotState(snapshot, expected, message?)

function assertSnapshotState(
  snapshot: CountdownSnapshot,
  expected: TimerState,
  message?: string // default: "Unexpected countdown state"
): void;

assertSnapshotCompleted(snapshot, message?)

function assertSnapshotCompleted(
  snapshot: CountdownSnapshot,
  message?: string // default: "Countdown should be completed"
): void;

Throws if snapshot.isCompleted === false OR snapshot.totalSeconds !== 0.

assertRemainingSeconds(snapshot, expected, tolerance?, message?)

function assertRemainingSeconds(
  snapshot: CountdownSnapshot,
  expected: number,
  tolerance?: number, // default: 0
  message?: string    // default: "Unexpected remaining seconds"
): void;

Throws if Math.abs(snapshot.totalSeconds - Math.floor(expected)) > tolerance or if expected is not a finite number.

assertRemainingSeconds(snapshot, 5);       // exact
assertRemainingSeconds(snapshot, 5, 0.5); // accepts 4.5–5.5

TimerState re-export

TimerState is re-exported via testing-utils, avoiding a double import:

// Instead of two separate imports:
import { TimerState } from '@timekeeper-countdown/core';
import { buildSnapshot } from '@timekeeper-countdown/core/testing-utils';

// You can import everything from one place:
import { buildSnapshot, TimerState } from '@timekeeper-countdown/core/testing-utils';

Custom Time Providers

CountdownEngine accepts either:

  • A function: () => number returning milliseconds.
  • An object implementing the TimeProvider interface: { now(): number; isHighResolution: boolean; type: string }.

This allows plugging in custom schedulers or synchronizing multiple engines.

const provider = {
  now: () => performance.now(),
  isHighResolution: true,
  type: 'custom',
};
CountdownEngine(60, { timeProvider: provider });

TypeScript Support

All exports are fully typed. Useful entry points:

import type {
  CountdownSnapshot,
  CountdownEngineOptions,
  CountdownEngineInstance,
  TimerState,
} from '@timekeeper-countdown/core';

Documentation & Examples


Contributing

Issues and pull requests are welcome. Please review the repository guidelines for more details and run:

npm run lint --workspaces
npm run test --workspaces
npm run typecheck --workspaces

before submitting changes.


License

MIT © Eduardo Kohn