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

windotwatchr

v0.2.0

Published

Zero-polling, event-driven detection of window.* global properties and nested sub-APIs.

Readme

windotwatchr

CI npm version npm bundle size license

Detect window.* properties the moment they appear. No polling.

  • ~2.4KB gzipped. React hook adds ~370B.
  • Zero dependencies. React is an optional peer dep.
  • Proxy-based. Intercepts property assignment via Object.defineProperty traps and ES Proxies. Fires your callback in the same microtask the value is set.
  • Typed. Written in TypeScript. Generics flow through to your callback or hook result.
npm install windotwatchr

AI Agent Skill

Give your AI coding agent deep knowledge of windotwatchr's API, internals, and best practices:

# Any agent (Claude Code, Cursor, Copilot, 38+ others)
npx skills add juanpprieto/windotwatchr -s windotwatchr-expert

# Claude Code plugin marketplace
/plugin marketplace add juanpprieto/windotwatchr
/plugin install windotwatchr-expert@windotwatchr

Live Demos

The Problem

Third-party scripts attach themselves to window at unpredictable times. Many provide no ready callback, making it hard to coordinate dependent UX flows, defer analytics events until an SDK is initialized, or render widgets that depend on deeply nested APIs. The standard workaround is a setTimeout loop:

const check = () => {
  if (window.AfterPay) {
    AfterPay.initialize({ countryCode: 'US' });
  } else {
    setTimeout(check, 100); // poll until it exists
  }
};
check();

This is wasteful, timing-dependent, and gets worse with deeply nested paths like window.acmePayments.ui.components.

| SDK | Window global | Has ready callback? | Notes | |-----|---------------|---------------------|-------| | Afterpay/Clearpay | window.AfterPay | No | Relies on script.onload. No event or promise. | | Affirm | window.affirm | Partial | affirm.ui.ready(fn) exists but requires the stub to already be on window. Race condition on SPAs. | | Attentive | window.__attentive | No | Docs recommend setTimeout(() => __attentive.trigger(), 500). | | Yotpo | window.yotpo | Unreliable | yotpo.onAllWidgetsReady(fn) throws if called before bootstrap. Devs resort to polling or MutationObserver. | | Klaviyo | window.klaviyo | Partial | Proxy-based queue buffers method calls, but no "ready" event for detecting initialization. | | Klarna | window.Klarna | Yes | window.klarnaAsyncCallback fires on load. | | Gorgias | window.GorgiasChat | Yes | gorgias-widget-loaded event + GorgiasChat.init() returns a Promise. |

The same problem applies beyond ecommerce: analytics coordination (firing events only after a tracker is fully initialized), A/B testing tools that gate rendering on experiment configs, and any script that builds its API surface incrementally across multiple async stages.

Usage

Callback

import { watchWindot } from 'windotwatchr';

const dispose = watchWindot<typeof AfterPay>('AfterPay', (ap) => {
  ap.initialize({ countryCode: 'US' });
});

// Stop watching:
dispose();

Promise

import { waitForWindot } from 'windotwatchr';

const ap = await waitForWindot<typeof AfterPay>('AfterPay', {
  timeout: 10_000,
});

React Hook

import { useWatchWindot } from 'windotwatchr/react';

function Checkout() {
  const { value: afterPay, status, error } = useWatchWindot<typeof AfterPay>('AfterPay');

  if (status === 'error')   return <p>Error: {error?.message}</p>;
  if (status === 'timeout') return <p>AfterPay took too long.</p>;
  if (!afterPay)             return <p>Loading...</p>;

  return <button onClick={() => afterPay.initialize({ countryCode: 'US' })}>Pay later</button>;
}

Nested Paths

Dot-notation works for arbitrarily deep properties. The engine installs Proxy traps at each level and resolves the moment the leaf value is assigned:

watchWindot('acmePayments.ui.components', (components) => {
  // Fires when window.acmePayments.ui.components is set,
  // even if acmePayments, ui, and components are assigned
  // at different times.
});

Custom Readiness

By default, a value is "ready" when it is not null or undefined. Override this when an SDK creates a stub object before it is fully initialized:

watchWindot('acmePx.loaded', (loaded) => {
  console.log('Pixel SDK ready');
}, {
  ready: (value) => value === true,
});

Abort

const controller = new AbortController();

waitForWindot('AfterPay', { signal: controller.signal })
  .catch((err) => console.log(err.message)); // "windotwatchr: aborted"

controller.abort();

API

watchWindot<T>(path, callback, options?): DisposeFunction

Watches a window property path. Calls callback with the resolved value when the readiness predicate passes. Returns a function that stops watching.

waitForWindot<T>(path, options?): Promise<T>

Promise wrapper around watchWindot. Resolves when the value is ready, rejects on timeout or abort.

useWatchWindot<T>(path, options?): WatchWindotResult<T>

React hook. Returns { value, status, error }. Status transitions: watchingready | timeout | error. Cleans up on unmount. StrictMode-safe.

Import from windotwatchr/react. React 16.8+ required.

Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | timeout | number | none | Time in ms before a ww:timeout event fires. | | pollInterval | number | 100 | Polling interval in ms for frozen/sealed objects. 0 disables. | | strategy | 'proxy' \| 'poll' \| 'auto' | 'auto' | Detection strategy. auto uses Proxy when possible, falls back to polling for frozen objects. | | ready | (value: unknown) => boolean | v != null | Predicate that determines when a value is considered ready. | | retries | number | 0 | Retry attempts after timeout before giving up. | | signal | AbortSignal | none | Abort signal to cancel the watcher. |

Types

// Result shape from useWatchWindot
interface WatchWindotResult<T> {
  value: T | null;
  status: WatcherState;
  error: Error | null;
}

type WatcherState = 'idle' | 'watching' | 'ready' | 'timeout' | 'error';
type DisposeFunction = () => void;
type SubscriberCallback<T> = (value: T) => void;

Events

The engine dispatches CustomEvents on window for lifecycle transitions:

| Event | Detail | When | |-------|--------|------| | ww:ready | { path } | Value passed the readiness predicate | | ww:timeout | { path } | Timeout elapsed without resolution | | ww:error | { path, error } | Watcher encountered an error |

How It Works

  1. watchWindot('a.b.c', cb) installs an Object.defineProperty trap on window.a.
  2. When window.a is assigned an object, the engine wraps it in a Proxy and watches for b.
  3. When b is assigned, another Proxy watches for c.
  4. When c is assigned and passes the readiness predicate, cb fires in the same microtask.

Multiple watchers on the same root key share a single trap (ref-counted). Cleanup is automatic when all subscribers dispose.

For frozen or sealed objects where Proxy cannot intercept assignments, the engine falls back to polling at pollInterval.

SSR

Both the core API and the React hook are SSR-safe. In non-browser environments:

  • watchWindot returns a no-op dispose function immediately.
  • waitForWindot rejects with an Error.
  • useWatchWindot stays in watching status (no-op until hydration).

Examples

See examples/nextjs and examples/vite-react for working demos that detect four mock SDKs with different loading patterns.

License

MIT