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

stalejs

v0.1.2

Published

Zero-dependency DOM content-freshness primitive. Keeps elements up to date automatically.

Readme

stalejs

Zero-dependency DOM content-freshness primitive. Keep any element automatically up to date — tab visibility, network reconnects, scroll intersection, and TTL expiry all handled out of the box.

npm size license

→ Live Demo


The problem

This is what keeping one element fresh actually looks like:

let intervalId, isVisible = true, isOnline = navigator.onLine, lastFetched = null

function fetchPrice() {
  if (!isVisible || !isOnline) return
  fetch('/api/price').then(r => r.json()).then(data => {
    document.querySelector('#price').textContent = data.price
    lastFetched = Date.now()
  }).catch(console.error)
}

intervalId = setInterval(fetchPrice, 30_000)
fetchPrice()

document.addEventListener('visibilitychange', () => {
  isVisible = !document.hidden
  if (!document.hidden && Date.now() - lastFetched > 30_000) fetchPrice()
})
window.addEventListener('online',  () => { isOnline = true;  fetchPrice() })
window.addEventListener('offline', () => { isOnline = false })

const io = new IntersectionObserver(([e]) => {
  isVisible = e.isIntersecting
  if (e.isIntersecting) fetchPrice()
})
io.observe(document.querySelector('#price'))

// cleanup you'll definitely forget
function destroy() {
  clearInterval(intervalId)
  io.disconnect()
  // ...removeEventListener × 3
}

40+ lines. Leaks if you forget cleanup. Multiply by every live widget in your app.

The solution

import { stale } from 'stalejs'

const unsub = stale('#price', {
  ttl: '30s',
  refetch: () => fetch('/api/price').then(r => r.json()),
  update:  (el, data) => { el.textContent = data.price },
})

unsub() // full cleanup — one call

Everything else is automatic.


Install

npm install stalejs
import { stale } from 'stalejs'        // ESM
const { stale } = require('stalejs')   // CJS

How it works

Every stale() call creates a binding that:

  1. Runs an initial fetch (unless eager: false)
  2. Starts a TTL interval — when it expires, refetches
  3. Pauses the clock when the tab is hidden
  4. Immediately refetches when the tab regains focus (if stale)
  5. Immediately refetches when the network comes back online
  6. Pauses when the element scrolls out of the viewport
  7. Resumes and refetches when it scrolls back in
  8. Auto-cleans up if the element is removed from the DOM

Call unsub() to manually tear everything down.


API

stale(target, options)

import { stale } from 'stalejs'

const unsub = stale(target, options)
unsub() // removes all listeners, observers, and intervals

target

string | HTMLElement | NodeList | NodeListOf<HTMLElement>

A CSS selector, a direct element reference, or a NodeList. When a selector matches multiple elements each gets an independent binding.

options

| Option | Type | Default | Description | |--------|------|---------|-------------| | ttl | string \| number | — | Time before data is considered stale. See TTL format. | | refetch | () => Promise<any> | — | Async function that returns fresh data. | | update | (el, data) => void | — | Applies the fetched data to the element. | | onError | (err: Error) => void | undefined | Called when refetch throws. Silent by default. | | eager | boolean | true | Fetch immediately on init. | | visibilityPause | boolean | true | Pause TTL when tab is hidden. | | focusRefetch | boolean | true | Refetch on tab focus if data is stale. | | intersectionPause | boolean | true | Pause TTL when element is out of viewport. | | reconnectRefetch | boolean | true | Refetch immediately when network comes back online. |

TTL format

| Value | Resolves to | |-------|-------------| | '500ms' | 500 ms | | '30s' | 30,000 ms | | '5m' | 300,000 ms | | '1h' | 3,600,000 ms | | 2000 (number) | 2,000 ms |


stale.invalidate(target)

Force an immediate refetch, regardless of TTL.

stale.invalidate('#price')
stale.invalidate(el)

stale.pause(target) / stale.resume(target)

Manually pause or resume a binding.

stale.pause('#price')    // stop polling
stale.resume('#price')   // resume — refetches immediately if stale

stale.getStatus(target)

Returns the current status of a binding. Useful for building loading states, debug overlays, or error indicators.

const status = stale.getStatus('#price')

// Returns null if no binding exists for the target
// Otherwise:
{
  paused:      boolean  // is the binding paused?
  fetching:    boolean  // is a refetch in flight?
  lastFetched: number   // timestamp of last successful fetch (0 = never)
  age:         number   // ms since last fetch (Infinity if never fetched)
  stale:       boolean  // is data currently stale?
  error:       Error | null  // last refetch error, if any
}

Example — show an error badge when refetch fails:

stale('#price', {
  ttl: '10s',
  refetch: () => fetch('/api/price').then(r => r.json()),
  update: (el, data) => { el.textContent = data.price },
  onError: () => {
    const status = stale.getStatus('#price')
    document.querySelector('#price-error').hidden = !status?.error
  },
})

stale.configure(defaults)

Set global defaults for all future stale() calls.

stale.configure({
  ttl: '60s',
  visibilityPause: true,
  reconnectRefetch: true,
})

Examples

Price ticker

import { stale } from 'stalejs'

stale('#btc-price', {
  ttl: '10s',
  refetch: () => fetch('/api/btc').then(r => r.json()),
  update:  (el, data) => { el.textContent = `$${data.usd.toLocaleString()}` },
  onError: (err) => console.warn('fetch failed:', err),
})

Notification badge

stale('#notif-count', {
  ttl: '1m',
  refetch: () => fetch('/api/notifications/unread').then(r => r.json()),
  update:  (el, { count }) => {
    el.textContent = count > 99 ? '99+' : String(count)
    el.hidden = count === 0
  },
})

Multiple elements via selector

// Each `.score-widget` gets its own independent binding
stale('.score-widget', {
  ttl: '5s',
  refetch: () => fetch('/api/score').then(r => r.json()),
  update:  (el, data) => { el.textContent = `${data.home} — ${data.away}` },
})

Framework usage

Vanilla JS

import { stale } from 'stalejs'

const unsub = stale('#price', {
  ttl: '30s',
  refetch: () => fetch('/api/price').then(r => r.json()),
  update:  (el, data) => { el.textContent = data.price },
})

window.addEventListener('unload', unsub)

Vue 3

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { stale } from 'stalejs'

const el = ref(null)
let unsub

onMounted(()  => { unsub = stale(el.value, { ttl: '15s', refetch, update }) })
onUnmounted(() => unsub?.())
</script>

<template><span ref="el">Loading…</span></template>

Svelte

<script>
  import { onMount } from 'svelte'
  import { stale } from 'stalejs'

  let el

  onMount(() => {
    return stale(el, { ttl: '15s', refetch, update }) // return = auto cleanup
  })
</script>

<span bind:this={el}>Loading…</span>

React (for non-React-state DOM needs)

import { useEffect, useRef } from 'react'
import { stale } from 'stalejs'

function PriceTicker() {
  const ref = useRef(null)

  useEffect(() => {
    return stale(ref.current, {
      ttl: '10s',
      refetch: () => fetch('/api/price').then(r => r.json()),
      update:  (el, data) => { el.textContent = data.price },
    })
  }, [])

  return <span ref={ref}>Loading…</span>
}

vs. SWR / React Query

| | stalejs | SWR / React Query | |--|---------|-------------------| | Framework | None — any DOM | React only | | Virtual DOM dependency | No | Yes | | Bundle size | 1.3 kb gz | ~13 kb+ | | Works with | Any HTML element | React component state | | SSR pages, HTMX, Web Components | ✅ | ❌ |

stalejs is not a replacement for SWR or React Query inside React apps. It's the answer for everything else — server-rendered pages, vanilla dashboards, HTMX partials, Web Components, and Vue/Svelte apps that need DOM-level freshness control.


License

MIT © RK