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 🙏

© 2025 – Pkg Stats / Ryan Hefner

decorio

v1.3.1

Published

First-class ECMAScript decorators for caching, binding, and concurrency patterns

Readme

💅 decorio — First-class ECMAScript decorators

A toolkit of decorators built on the Stage 3 ECMAScript Decorators proposal. These follow the TC39 spec (not the old typescript "legacy" decorators): https://github.com/tc39/proposal-decorators.

📦 Install

npm install decorio
# or
yarn add decorio

🗄️ Caching decorators

@once

Run a method only once for each unique set of arguments. Any further calls with the same inputs just return the cached result: no extra executions.

import { once } from 'decorio';

class Example {
  @once compute(x: number): number {
    console.log('Computing', x);

    return x * 2;
  }
}

const e = new Example();
e.compute(3); // logs 'Computing 3', returns 6
e.compute(3); // returns 6 from cache, no log

@cached

Cache all (args ➡️ result) pairs per instance. Use cached.invalidate(fn) to clear the cache for a specific method.

import { cached } from 'decorio';

class Example {
  @cached sum(a: number, b: number): number {
    return a + b;
  }
  
  // ⬇️ ttl
  @cached(60 * 1000) async fetchData(id: string): Promise<Data> { ... }
}

const e = new Example();
e.sum(1, 2); // computes 3
e.sum(1, 2); // returns 3 from cache

// flush the cache for this method
cached.invalidate(e.sum);

const p1 = e.fetchData('foo');
await wait(30 * 1000); // wait 30 sec
const p2 = e.fetchData('foo'); // p2 === p1
await wait(30 * 1000); // wait 30 sec
const p3 = e.fetchData('foo'); // new promise returned

@lazy

Evaluates the original getter once per instance on the first access and then keeps returning the same value on subsequent reads, without re-running the getter.

class User {
  #seed = Math.random();

  @lazy get checksum(): string {
    // expensive work...
    return hash(this.#seed);
  }
}

const u = new User();
u.checksum; // computed once
u.checksum; // served from cache

⚙️ Concurrency decorators

@singleflight

Prevent duplicate in-flight calls per argument list. If you call it again with the same args before it finishes, you get the same pending Promise.

import { singleflight } from 'decorio';

class Example {
  @singleflight async fetchData(id: string): Promise<Data> { ... }
}

const e = new Example();
const p1 = e.fetchData('foo');
const p2 = e.fetchData('foo'); // p2 === p1

@debounced(delayMs)

Debounce a method: wait for delayMs ms of "silence", then run only the last invocation. Every returned Promise resolves (or rejects) with that final run.

import { debounced } from 'decorio';

class Searcher {
  @debounced(300) async search(query: string): Promise<Array<string>> { ... }
}

const s = new Searcher();
s.search('a');
s.search('ab');
s.search('abc'); // only this one actually fires, after 300 ms

You can also cancel earlier runs by passing the built-in AbortSignal:

import { debounced } from 'decorio';

class Fetcher {
  @debounced(500) async fetchData(id: string): Promise<object> {
    const { signal } = debounced;

    // Pass the signal to fetch so that prior calls get aborted
    return fetch(`/api/data/${id}`, { signal }).then((r) => r.json());
  }
}

const f = new Fetcher();
f.fetchData('foo');
f.fetchData('bar');

@latest

Like a zero-delay debounce, but it fires instantly on each call and aborts any prior in-flight run. It always keeps the latest call and ignores arguments when deciding what to cancel.

import { latest } from 'decorio';

class Example {
  @latest async fetchData(id: string): Promise<Data> {
    const { signal } = latest;

    return fetch(`/api/data/${id}`, { signal }).then((r) => r.json());
  }
}

const e = new Example();
e.fetchData('1'); // starts immediately
e.fetchData('2'); // aborts '1' and starts '2' immediately

@mutex

Ensures that an async method never runs concurrently. If the method is called again while a previous call is still running, the new call will wait in a queue until all earlier calls have finished.

import { mutex } from 'decorio';

class Example {
  @mutex async save(data: string): Promise<void> { ... }
}

const e = new Example();
e.save('A'); // runs immediately
e.save('B'); // waits until A finishes
e.save('C'); // waits until B finishes
// start A → end A → start B → end B → start C → end C

🔗 Utility decorators

@bound

Ensure a method always calls with the right this. Even if you extract the function reference, it stays bound to its instance.

import { bound } from 'decorio';

class Example {
  message = 'Hello';

  @bound greet() {
    console.log(this.message);
  }
}

const e = new Example();
const greet = e.greet;
greet(); // always logs 'Hello'

@timeout(timeoutMs)

Enforce a maximum execution time on an async method. If the method does not complete within timeoutMs milliseconds, it will be aborted via an AbortSignal.

Decorator exposes a static property timeout.signal that the method can read at runtime.

import { timeout } from 'decorio';

class Example {
  @timeout(500) async fetchData(id: string): Promise<Data> {
    const { signal } = timeout;

    return fetch(`/api/data/${id}`, { signal }).then((r) => r.json());
  }
}

const e = new Example();
try {
  const data = await e.fetchData("123");
  console.log('Got data:', data);
} catch (e) {
  console.error(e.message); // If over 500 ms: "timeout 500ms exceeded"
}

🧶 Getting started with Stage 3 Decorators

To use these in typescript instead of "legacy" decorators, configure your toolchain:

Typescript (tsc)

Make sure "experimentalDecorators": false (the default) in your tsconfig.json.

Vite + esbuild

// vite.config.ts
import { defineConfig } from 'vite';

export default defineConfig({
  esbuild: {
    // disable esbuild's legacy-decorator transform so that Stage 3 decorator calls remain intact
    supported: {
      decorators: false,
    },
  },
});

SWC

Enable decorators in your .swcrc:

// .swcrc
// https://swc.rs/docs/configuration/compilation#jsctransformdecoratorversion
{
  "jsc": {
    "parser": {
      "syntax": "typescript",
      "decorators": true
    },
    "transform": {
      "decoratorVersion": "2022-03"
    }
  }
}

If you use @vitejs/plugin-react-swc, you can also mutate via:

// https://github.com/vitejs/vite-plugin-react-swc/releases/tag/v3.8.0
react({
  useAtYourOwnRisk_mutateSwcOptions(options) {
    options.jsc.parser.decorators = true;
    options.jsc.transform.decoratorVersion = '2022-03';
  },
});