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

@ghom/query

v1.0.0

Published

TypeScript query manager (caching, throttling, retry, debounce, dedup, circuit breaker, timeout, batching)

Readme

@ghom/query

npm version License: MIT

A lightweight, zero-dependency, technology-agnostic query manager for JavaScript/TypeScript. Optimize your API calls with caching, throttling, retry, debounce, deduplication, circuit breaker, timeout, and batching.

Features

  • Caching - TTL-based caching with stale-while-revalidate support
  • Throttling - Limit call frequency with leading/trailing edge options
  • Retry - Automatic retry with exponential backoff, linear, or custom delay
  • Debouncing - Wait for calls to settle before executing
  • Deduplication - Prevent concurrent duplicate calls
  • Circuit Breaker - Fail fast when a service is down
  • Timeout - Limit execution time with automatic cancellation
  • Batching - Group multiple calls into a single execution
  • Zero dependencies - No external runtime dependencies
  • Works everywhere - Node.js, Bun, Deno, browsers

Installation

# npm
npm install @ghom/query

# yarn
yarn add @ghom/query

# pnpm
pnpm add @ghom/query

# bun
bun add @ghom/query

Quick Start

import { Query } from "@ghom/query"

// Create a query with multiple features
const fetchUser = new Query(
  async (userId: string) => {
    const res = await fetch(`https://api.example.com/users/${userId}`)
    return res.json()
  },
  {
    cache: { ttl: 60_000 },           // Cache for 1 minute
    retry: { attempts: 3 },            // Retry up to 3 times
    timeout: 10_000,                   // 10 second timeout
    dedup: true,                       // Deduplicate concurrent calls
  }
)

// Execute the query
const user = await fetchUser.execute("123")

API Reference

Query Class

The main class that orchestrates all features.

import { Query } from "@ghom/query"

const query = new Query(fn, options)

Options

| Option | Type | Description | |--------|------|-------------| | cache | { ttl: number, staleWhileRevalidate?: boolean } | Enable caching with TTL in ms | | throttle | { interval: number, leading?: boolean, trailing?: boolean } | Throttle calls | | retry | { attempts: number, delay?: number \| Function, retryIf?: Function } | Retry on failure | | debounce | { wait: number, maxWait?: number } | Debounce calls | | dedup | boolean | Deduplicate concurrent calls | | circuitBreaker | { threshold: number, resetTimeout: number } | Circuit breaker | | timeout | number | Timeout in ms | | batch | { maxSize: number, maxWait: number, batchFn: Function } | Batch calls | | onSuccess | (result, params) => void | Success callback | | onError | (error, params) => void | Error callback | | keyFn | (...params) => string | Custom key generator |

Methods

// Execute the query
await query.execute(...params)

// Force fetch (bypass cache)
await query.fetch(...params)

// Invalidate cache
query.invalidateCache()        // All entries
query.invalidateCache("key")   // Specific key

// Get statistics
const stats = query.getStats()

// Reset everything
query.reset()

// Cancel pending calls
query.cancel()
query.cancel("key")

// Flush pending throttled/debounced calls
await query.flush()

Individual Handlers

Each feature is available as a standalone class for advanced usage.

CachedQuery

import { CachedQuery } from "@ghom/query"

const cache = new CachedQuery(
  async (id: string) => fetchData(id),
  60_000, // TTL
  { staleWhileRevalidate: true }
)

const data = await cache.get("key", "param1")
cache.invalidate("key")

ThrottledQuery

import { ThrottledQuery } from "@ghom/query"

const throttled = new ThrottledQuery(
  async (id: string) => fetchData(id),
  1000, // 1 second interval
  { leading: true, trailing: true }
)

await throttled.execute("key", "param1")

RetryHandler

import { RetryHandler, exponentialBackoff } from "@ghom/query"

const retry = new RetryHandler(
  async () => fetchData(),
  {
    attempts: 3,
    delay: exponentialBackoff, // or a number, or custom function
    retryIf: (error) => error.status === 503
  }
)

await retry.execute()

DebounceHandler

import { DebounceHandler } from "@ghom/query"

const debounced = new DebounceHandler(
  async (query: string) => search(query),
  { wait: 300, maxWait: 1000 }
)

await debounced.execute("key", "search term")

DedupHandler

import { DedupHandler } from "@ghom/query"

const dedup = new DedupHandler(
  async (id: string) => fetchData(id)
)

// These will share the same request
const [r1, r2, r3] = await Promise.all([
  dedup.execute("same-key", "id"),
  dedup.execute("same-key", "id"),
  dedup.execute("same-key", "id"),
])

CircuitBreaker

import { CircuitBreaker, CircuitState } from "@ghom/query"

const breaker = new CircuitBreaker({
  threshold: 5,      // Open after 5 failures
  resetTimeout: 30_000 // Try again after 30 seconds
})

try {
  breaker.allowRequest()
  const result = await fetchData()
  breaker.recordSuccess()
} catch (error) {
  breaker.recordFailure()
}

// Or use the wrapper
const result = await breaker.execute(() => fetchData())

TimeoutHandler

import { TimeoutHandler, withTimeout } from "@ghom/query"

// Using the class
const timeout = new TimeoutHandler(
  async () => fetchData(),
  5000
)
await timeout.execute()

// Using the utility function
const result = await withTimeout(fetchData(), 5000)

BatchHandler

import { BatchHandler, createDataLoader } from "@ghom/query"

// Low-level batching
const batcher = new BatchHandler({
  maxSize: 100,
  maxWait: 16, // ~1 frame
  batchFn: async (params) => {
    const ids = params.map(([id]) => id)
    return fetchMany(ids)
  }
})

// DataLoader pattern
const loadUser = createDataLoader(
  async (ids: string[]) => fetchUsers(ids),
  { maxSize: 100, maxWait: 16 }
)

const user = await loadUser("123")

Examples

Discord API

import { Query } from "@ghom/query"

const fetchGuildMember = new Query(
  async (guildId: string, userId: string) => {
    const res = await fetch(
      `https://discord.com/api/v10/guilds/${guildId}/members/${userId}`,
      { headers: { Authorization: `Bot ${token}` } }
    )
    if (!res.ok) throw new Error(`HTTP ${res.status}`)
    return res.json()
  },
  {
    cache: { ttl: 60_000, staleWhileRevalidate: true },
    retry: { 
      attempts: 3, 
      delay: (n) => Math.pow(2, n) * 1000,
      retryIf: (err) => err.message.includes("429") || err.message.includes("5")
    },
    throttle: { interval: 50 }, // Respect rate limits
    dedup: true,
    circuitBreaker: { threshold: 10, resetTimeout: 60_000 },
    timeout: 15_000,
    onError: (err, [guildId, userId]) => {
      console.error(`Failed to fetch member ${userId} from ${guildId}:`, err)
    }
  }
)

const member = await fetchGuildMember.execute("guild-id", "user-id")

Database Queries

import { Query } from "@ghom/query"

const findUserById = new Query(
  async (id: number) => {
    return db.query("SELECT * FROM users WHERE id = $1", [id])
  },
  {
    cache: { ttl: 30_000 },
    dedup: true,
    retry: { attempts: 2, delay: 100 },
    timeout: 5_000,
  }
)

// Batch loading with DataLoader pattern
import { createDataLoader } from "@ghom/query"

const loadUser = createDataLoader(
  async (ids: number[]) => {
    const result = await db.query(
      "SELECT * FROM users WHERE id = ANY($1)",
      [ids]
    )
    // Ensure order matches input
    return ids.map(id => result.find(u => u.id === id))
  }
)

const user = await loadUser(123)

Search with Debounce

import { Query } from "@ghom/query"

const searchProducts = new Query(
  async (query: string) => {
    const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`)
    return res.json()
  },
  {
    debounce: { wait: 300, maxWait: 1000 },
    cache: { ttl: 60_000 },
    timeout: 5_000,
  }
)

// In your UI
input.addEventListener("input", async (e) => {
  const results = await searchProducts.execute(e.target.value)
  renderResults(results)
})

Error Handling

import { 
  Query, 
  QueryTimeoutError, 
  CircuitOpenError, 
  MaxRetriesExceededError 
} from "@ghom/query"

try {
  await query.execute("param")
} catch (error) {
  if (error instanceof QueryTimeoutError) {
    console.error(`Request timed out after ${error.timeoutMs}ms`)
  } else if (error instanceof CircuitOpenError) {
    console.error(`Circuit is open, retry in ${error.resetTimeout}ms`)
  } else if (error instanceof MaxRetriesExceededError) {
    console.error(`Failed after ${error.attempts} attempts:`, error.lastError)
  }
}

Statistics

const query = new Query(fn, options)

// After some executions
const stats = query.getStats()

console.log(stats)
// {
//   totalCalls: 100,
//   cacheHits: 75,
//   cacheMisses: 25,
//   successes: 95,
//   failures: 5,
//   retries: 10,
//   deduped: 15,
//   throttled: 5,
//   debounced: 0,
//   timeouts: 2,
//   circuitBreaks: 1,
//   circuitState: "CLOSED"
// }

TypeScript

Full TypeScript support with generic types:

interface User {
  id: string
  name: string
  email: string
}

const fetchUser = new Query<[string], User>(
  async (id: string) => {
    const res = await fetch(`/api/users/${id}`)
    return res.json()
  },
  { cache: { ttl: 60_000 } }
)

// Type-safe execution
const user: User = await fetchUser.execute("123")

License

MIT