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

@firekid/once

v1.0.2

Published

Deduplicate async function calls. The modern, memory-safe replacement for inflight.

Readme

@firekid/once

npm version npm downloads npm bundle size TypeScript License: MIT GitHub stars GitHub forks CI

The modern, memory-safe replacement for inflight.
Deduplicate async function calls. Zero dependencies. Full TypeScript support.


The Problem

inflight has a memory leak. It never cleans up its internal map after callbacks run, which causes Node.js heap crashes in long-running processes. It also uses callbacks instead of Promises, has no TypeScript support, and only runs in Node.

@firekid/once fixes all of it.

Installation

npm install @firekid/once
yarn add @firekid/once
pnpm add @firekid/once

Quick Start

import { once } from '@firekid/once'

const getUser = once(async (id: string) => {
  return await db.query('SELECT * FROM users WHERE id = ?', [id])
})

const [a, b, c] = await Promise.all([
  getUser('123'),
  getUser('123'),
  getUser('123'),
])

// Only 1 DB query fired. All 3 get the same result.

Core Behavior

When the same async function is called multiple times with the same arguments before the first call resolves, once deduplicates — only one invocation runs and all callers receive the same result.

After the promise settles (resolve or reject), the entry is automatically removed from the internal map. No memory leak. Ever.

Errors are propagated to all waiting callers. After a rejection, the key is cleared so the next call retries cleanly.

API

once(fn, options?)

Wraps an async function with deduplication.

once(fn, options?)
type OnceOptions<TArgs> = {
  key?: string | ((...args: TArgs) => string)
  ttl?: number
  maxKeys?: number
  onDeduplicated?: (key: string) => void
}

Returns an OnceInstance which is callable and also has .clear(), .size(), and .has() methods.

createOnce(defaults?)

Creates a factory with default options applied to every wrapped function.

const wrap = createOnce({ ttl: 5000 })

const getUser = wrap(async (id: string) => fetchUser(id))
const getPosts = wrap(async (userId: string) => fetchPosts(userId))

Options

key

By default the key is generated from the function arguments via JSON.stringify. You can override this with a static string or a function.

const fn = once(fetchConfig, { key: 'config' })

const fn = once(fetchUser, {
  key: (id, _role) => `user:${id}`
})

ttl

Keep the result in the dedup window for a number of milliseconds after the promise resolves. All calls within that window return the cached result without re-executing.

const fn = once(fetchConfig, { ttl: 5000 })

const first = await fn()
await sleep(3000)
const second = await fn() // returned from dedup window
await sleep(3000)
const third = await fn()  // ttl expired, executes again

maxKeys

Limit the number of in-flight keys tracked simultaneously. When the limit is reached, additional calls bypass deduplication and execute directly.

const fn = once(fetchUser, { maxKeys: 100 })

onDeduplicated

Called every time a call is deduplicated instead of executed. Useful for logging and metrics.

const fn = once(fetchUser, {
  onDeduplicated: (key) => {
    metrics.increment('dedup.hit', { key })
  }
})

Instance Methods

const fn = once(fetchUser)

fn.clear()         // clear all in-flight entries
fn.clear('123')    // clear a specific key
fn.size()          // number of currently in-flight keys
fn.has('123')      // whether a key is currently in-flight

Error Handling

Errors are shared across all concurrent callers and the key is immediately cleared after rejection, allowing clean retries.

const fn = once(async (id: string) => {
  return await riskyOperation(id)
})

const results = await Promise.allSettled([fn('x'), fn('x'), fn('x')])

const result = await fn('x') // retries cleanly after rejection

Real World Examples

API route — deduplicate DB calls

import { once } from '@firekid/once'

const getUser = once(async (id: string) => {
  return await db.users.findUnique({ where: { id } })
}, { ttl: 2000 })

app.get('/users/:id', async (req, res) => {
  const user = await getUser(req.params.id)
  res.json(user)
})

NestJS provider

import { Injectable } from '@nestjs/common'
import { once } from '@firekid/once'

@Injectable()
export class UserService {
  private getUser = once(async (id: string) => {
    return this.userRepo.findOne(id)
  }, { ttl: 3000 })

  find(id: string) {
    return this.getUser(id)
  }
}

Config loading — load once, reuse everywhere

import { once } from '@firekid/once'

const loadConfig = once(async () => {
  return await fetch('/api/config').then(r => r.json())
})

const config = await loadConfig()

Replace inflight directly

import { once } from '@firekid/once'

const fn = once(async () => {
  return await myAsyncOperation()
}, { key: 'my-key' })

await fn()

TypeScript

import { once, OnceOptions, OnceInstance } from '@firekid/once'

type User = { id: string; name: string }

const getUser: OnceInstance<[string], User> = once(
  async (id: string): Promise<User> => fetchUser(id),
  { ttl: 5000 }
)

const user = await getUser('123')
user.name

Response Shape

type OnceInstance<TArgs, TResult> = {
  (...args: TArgs): Promise<TResult>
  clear: (key?: string) => void
  size: () => number
  has: (key: string) => boolean
}

Environment Support

Works anywhere a JavaScript Promise is available.

  • Node.js 18 and above
  • Cloudflare Workers
  • Vercel Edge Functions
  • Deno
  • Bun
  • Browser

Exports both ESM (import) and CommonJS (require).

License

MIT


Built by Firekid♥️ — All rights reserved