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

@mainframework/timer

v1.1.1

Published

Browser countdown and count-up timers backed by a shared Web Worker

Readme

@mainframework/timer

Countdown and count-up timers for browsers, backed by one shared Web Worker per loaded package instance.

Runtime contract

The root @mainframework/timer entry is framework-free browser JavaScript. The optional @mainframework/timer/react entry adapts the same worker for React 19 or later.

The root @mainframework/timer entry requires window and Worker; calling createWorker, getDefaultRouteKey, or setActiveRoute during server execution throws an explicit error.

On the server, pass an explicit routeKey: if it is omitted, render throws while resolving the pathname default via getDefaultRouteKey(). When render completes, countdown mode returns the raw durationSeconds and count-up mode returns 0, the same values as the first browser paint. Whole seconds still come from worker ticks after mount. When routeKey is omitted on the client, the pathname default applies from the first client render. The React bundle includes a "use client" directive for tools that understand React Server Components.

Installation

npm install @mainframework/timer

React is an optional peer dependency. Install it only when using the React entry.

React

import { useTimer } from "@mainframework/timer/react";

export const Countdown = () => {
  const secondsLeft = useTimer(60);
  return <span>{secondsLeft}s</span>;
};

export const Stopwatch = () => {
  const secondsElapsed = useTimer(0, "stopwatch", "up");
  return <span>{secondsElapsed}s</span>;
};

useTimer(durationSeconds, routeKey?, mode?) returns remaining whole seconds in "down" mode and elapsed whole seconds in "up" mode.

  • In the worker, countdown durations are floored to whole seconds; zero, negative, NaN, and infinite values register as zero. Until the first tick, the hook may still display the raw durationSeconds you passed (including fractions).
  • Count-up mode ignores the duration and later duration changes.
  • routeKey is an arbitrary grouping key. When omitted, it defaults to window.location.pathname. The key used for register is captured on the first render and does not change for that mount.
  • useTimer does not post worker unregister on unmount. On navigation, call setActiveRoute with the active group key (see Group cleanup). Global or dev keys (for example "stopwatch") stay in the worker until setActiveRoute purges them or you post explicit unregister like the plain JavaScript example.

Plain JavaScript

The root entry exposes the shared worker and its typed message protocol. Timer IDs must be unique across that worker.

import { createWorker, getDefaultRouteKey } from "@mainframework/timer";

const worker = createWorker();
const id = crypto.randomUUID();
const routeKey = getDefaultRouteKey();

/** @param {MessageEvent<import("@mainframework/timer").TimerWorkerMessage>} event */
const handleMessage = (event) => {
  const message = event.data;
  if (!message || message.id !== id) return;
  if (message.type === "tick" && message.mode === "down") {
    console.log(message.secondsLeft);
  }
  if (message.type === "expired") console.log("done");
};

worker.addEventListener("message", handleMessage);

worker.postMessage({
  type: "register",
  routeKey,
  id,
  mode: "down",
  durationSeconds: 60,
});

// Remove both owners when the consumer is done.
worker.removeEventListener("message", handleMessage);
worker.postMessage({ type: "unregister", routeKey, id });

Count-up registration omits the duration:

import { createWorker } from "@mainframework/timer";

const worker = createWorker();
worker.postMessage({
  type: "register",
  routeKey: "stopwatch",
  id: crypto.randomUUID(),
  mode: "up",
});

Use addEventListener rather than assigning worker.onmessage; the singleton may serve multiple consumers. The React entry sets onmessage once to dispatch ticks to hook instances—do not replace it if you mix the root and React entries in the same app.

Group cleanup

setActiveRoute(key) is optional application-controlled cleanup despite its historical name. It keeps the named group and permanently removes timers in every other group. The package does not observe navigation or depend on a routing library.

After the worker processes the route change, the named group is authoritative: later registrations for other groups are silently discarded. Calling setActiveRoute again replaces that authority with the new key.

import { setActiveRoute } from "@mainframework/timer";

setActiveRoute("current-workspace");

Public API

@mainframework/timer

| Export | Contract | | ---------------------------- | ----------------------------------------------------------------- | | createWorker() | Returns the singleton browser Worker. | | getDefaultRouteKey() | Returns window.location.pathname as a convenience grouping key. | | setActiveRoute(key) | Keeps one timer group and purges the others. | | TimerMode | "down" \| "up" | | TimerWorkerIncomingMessage | Main thread to worker protocol. | | TimerWorkerMessage | Worker to main thread protocol. |

@mainframework/timer/react

| Export | Contract | | --------------------------------------------- | ------------------------------------------- | | useTimer(durationSeconds, routeKey?, mode?) | Returns remaining or elapsed whole seconds. |

The exported protocol types are the source of truth for message payloads. Countdown registration requires durationSeconds; count-up registration does not accept it. Countdown completion emits expired once.

License

MIT — see package.json.