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

@cancjs/decorators

v1.0.0

Published

Class method decorators for cancelable coroutines.

Readme


Introduction

@AsyncMethod and @BindMethod wrap a class member with cancAsync from @cancjs/coroutine, so calling the member returns a CancelablePromise instead of a generator.

The same two decorators ship in three dialects, one per decorator syntax a toolchain can speak: the standard decorators, the TypeScript legacy ones, and the Babel legacy ones. The behavior is identical, only the wiring underneath differs.

Features

  • declarative cancelable methods, no manual wrapping in the constructor
  • standard, TypeScript legacy and Babel legacy dialects, from separate entry points
  • prototype-level or per-instance placement, chosen with one option
  • methods, arrow-function class fields and getters are all supported
  • per-instance placement that does not leak instances

Getting Started

Installation

npm install @cancjs/decorators @cancjs/coroutine @cancjs/promise

@cancjs/coroutine and @cancjs/promise are peer dependencies. This package is ecosystem tier: a minor release can carry a breaking change, so pin it with a tilde, ~1.4 (pin the minor, not ~1.x, which npm expands to the same range as ^1), rather than the default caret. See Versioning for the full policy.

Usage

The recommended form in TypeScript is a getter that returns cancAsync(...). A getter's return type is inferred from its body, so the call site sees CancelablePromise<T> with no cast:

import { AsyncMethod } from '@cancjs/decorators';
import { cancAsync, cancAwait } from '@cancjs/coroutine';

class IssueClient {
  @AsyncMethod()
  get loadIssue() {
    return cancAsync(function* (this: IssueClient, issueId: string) {
      const issue = yield* cancAwait(this.api.issue(issueId));
      const comments = yield* cancAwait(this.api.comments(issueId));

      return { issue, comments };
    }, this);
  }
}

const client = new IssueClient();
const pending = client.loadIssue('bug-118');

pending.cancel();

In plain JavaScript the shorter method style works too. It decorates a generator method directly, which is a more compact annotation. In TypeScript, however, a method decorator cannot change the declared return type of the method it decorates, so the call site sees a Generator instead of a CancelablePromise and every caller needs a cast. Use getter style in TypeScript.

class IssueClient {
  @AsyncMethod()
  *loadIssue(issueId) {
    return yield* cancAwait(this.api.issue(issueId));
  }
}

How It Works

The decorator replaces the member with the result of cancAsync(fn, ctx). Where that replacement lands is the one decision to make, and the bind option makes it:

| Placement | bind | Mechanism | this binding | Memory | | --------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------ | -------------------------------------------- | | Prototype level | false, the default for @AsyncMethod | the decorator returns the wrapped function, replacing the method on the prototype once | late bound, flows from the call site | one wrapped function shared by all instances | | Per instance | true, the default for @BindMethod | an initializer (standard) or a self-replacing own-property accessor (legacy dialects) installs a bound own property on each instance | early bound, fixed to the instance | one wrapped function per instance |

Default to prototype level. Pay for per-instance binding only when the method is detached from its instance and passed around as a bare reference, as in setTimeout(client.loadIssue) or a callback prop.

Getters and arrow-function class fields are always memoized per instance, whatever bind says. A getter's produced function must not be cached on the prototype, that was the original cross-instance corruption bug, and a class field's initial value is per-instance to begin with. The bind option only decides whether the produced function is bound to its context.

super interaction

Prototype-level methods sit on the prototype like any other method, so super.method() from a subclass calls the wrapped parent implementation through normal prototype lookup.

Per-instance methods are own properties on the instance and do not participate in super lookup. super.method() in a subclass still resolves to the parent prototype's method, unbound unless the parent's own initializer has already run for that instance. When a hierarchy relies on super reaching the cancelable behavior, keep those levels at prototype placement and reserve bind: true for leaf classes.

A subclass that overrides a decorated member needs its own decorator. Decoration does not propagate to overrides, standard method-resolution rules apply.

Memory

Prototype placement creates one wrapped function per class, at class definition time. Per-instance placement creates one per instance, each closing over that instance.

A discarded instance stays collectable even while another instance's bound method is still alive. That holds because placement is a self-replacing own property, never a cache keyed by property name on the prototype. The old cached form pinned the first instance forever and handed its bound method to every other instance, so do not reintroduce a shared cache for bound placement.

Description

Three dialects

| Entry point | Toolchain | Exports | | --------------------------------- | ------------------------------------------------------------------- | --------------------------- | | @cancjs/decorators | standard decorators (TypeScript 5, or Babel with the modern plugin) | AsyncMethod, BindMethod | | @cancjs/decorators/legacy | TypeScript with experimentalDecorators | AsyncMethod, BindMethod | | @cancjs/decorators/babel-legacy | Babel with @babel/plugin-proposal-decorators in legacy mode | AsyncMethod, BindMethod |

The main entry point also exports every dialect under an explicit name (LegacyAsyncMethod, LegacyBindMethod, BabelLegacyAsyncMethod, BabelLegacyBindMethod) for code that has to mix them, for example while migrating.

Both decorators can be applied bare or called with options:

@AsyncMethod          // bare
@AsyncMethod()        // called, same thing
@AsyncMethod({ bind: true })

Method style and getter style

There are two ways to attach a coroutine to a class, and in TypeScript they are not interchangeable.

Method style decorates a generator method directly. It is the shorter annotation, but a method decorator cannot change the declared type of the method it decorates. TypeScript keeps seeing the generator return type while at runtime the member returns a CancelablePromise, so every call site needs a cast. This is a TypeScript limitation with no decorator-side fix. Use method style in JavaScript, where there is no static type to be wrong, and avoid it in TypeScript.

Getter style sidesteps it. A getter's return type is inferred from its body, so the type of cancAsync(...) flows through to the property and the call site gets CancelablePromise<T> with no cast.

One boundary can still need a cast. A generator body's inferred value type does not always match a separate interface that declares plain Promise-returning methods. When a decorated class has to satisfy such an interface, the assignment needs a cast at class level, even though every call runs the real coroutine and returns the right value at runtime. Calling cancAsync by hand with no decorator hits exactly the same boundary.

Getter semantics

A decorated getter is expected to return an already built coroutine, the result of cancAsync(fn, ctx?), not a bare generator function. On a getter the decorator never calls cancAsync itself. It memoizes the getter's result on the instance, and optionally binds it.

@AsyncMethod() on a getter memoizes only, so the function keeps whatever binding cancAsync gave it. @BindMethod() memoizes and calls .bind(this) on the result, which makes it detach-safe whatever cancAsync was given. In both cases the result is cached per instance on first access.

The , this idiom

Passing the instance as the second argument of cancAsync binds the coroutine's this at creation time, regardless of how the result is later called. That is the default to reach for: the same body works under either decorator, called as a method or detached first.

It is not strictly required for a member that is always called as client.method(), since call-site this resolves correctly there. It removes one failure mode: a coroutine created without it and paired with @AsyncMethod throws once detached, because nothing supplies this at call time. @BindMethod is a harmless no-op on a coroutine that already has its own context.

Typing this inside the body needs a function (this: T) parameter, which is a TypeScript-only annotation and is erased at runtime.

Without decorators

The coroutine package exports asyncMethod and bindMethod, which provide the same getter memoization without decorator syntax. Both read the getter, bind the result to the instance, and install it as an own property. asyncMethod is the semantic name for cancelable coroutine methods, bindMethod for general binding. See class methods in the coroutine documentation.

A class field is the simplest manual form, at the cost of one wrapped function per instance:

class IssueClient {
  loadIssue = cancAsync(function* (this: IssueClient, issueId: string) {
    return yield* cancAwait(this.api.issue(issueId));
  }, this);
}

Inheritance

| Case | Behavior | | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- | | Subclass does not override the member | Inherits the prototype-level wrap. A bound member gets its own per-instance wrap, because the initializer runs again through the constructor chain | | Subclass overrides without a decorator | The override is plain, neither wrapped nor bound | | Subclass overrides with its own decorator | An independent wrap, see the super notes above | | Same member decorated at several levels through mixins | Last applied wins, standard prototype semantics |

API

AsyncMethod and BindMethod, applicable to a method, an arrow-function field or a getter. Both accept { bind?: boolean }. bind defaults to false for AsyncMethod and true for BindMethod.

The same pair is exported from @cancjs/decorators/legacy and @cancjs/decorators/babel-legacy, and under the Legacy and BabelLegacy prefixes from the main entry point.

Compatibility

Node.js 18 and later, current browsers. The standard entry point needs TypeScript 5.0 or later, or Babel with the modern decorators plugin. The legacy entry point needs experimentalDecorators, and the Babel legacy entry point needs @babel/plugin-proposal-decorators in legacy mode. reflect-metadata is not required.

Everything else follows @cancjs/promise.

Documentation

  • Coroutines for the coroutine side of class methods
  • @cancjs/promise for the cancellation model
  • Examples: demo-decorators builds one client class in all three dialects plus the manual form, app-angular uses them in a service

Contributing

You are welcome to participate through issues and pull requests!

License

MIT