ember-concurrency

Cancelable, restartable async task primitives for Ember.js that replace manual promise and flag juggling with declarative concurrency control.

Library
npm
v5.2.0
688stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
55/100Fair
Development Activity4
Maintenance44
Community84
Maturity60
Momentum28

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
91/100Excellent
Architecture88
Code Quality90
Innovation85
Learning Curve100

ember-concurrency is an Ember addon that gives components, routes, and services a first-class task() primitive for managing asynchronous work. Instead of manually tracking loading flags, canceling in-flight requests, and guarding against out-of-order responses, you wrap generator or async-arrow functions in task() and apply declarative modifiers like restartable, drop, enqueue, and keepLatest to control exactly how concurrent invocations behave.

Under the hood, tasks are built on a scheduler and buffer-policy system that tracks running, queued, and canceled TaskInstances, exposing derived state (isRunning, last, lastSuccessful, performCount, etc.) that templates and computed properties can consume directly. Since v4 it ships as a TypeScript-authored Embroider v2 addon with full type declarations, a Babel transform for async-arrow task syntax, and built-in yieldables (timeout, animationFrame, waitForEvent) for expressing cancelable async workflows without hand-rolled promise bookkeeping.

What You Get

  • task() primitive - wraps a generator or async-arrow function into a cancelable Task with automatic destructor-based cleanup tied to the host object’s lifecycle
  • Concurrency modifiers - restartable, drop, enqueue, keepLatest, and maxConcurrency control how overlapping perform() calls are scheduled without manual bookkeeping
  • Derived task state - isRunning, isIdle, last, lastSuccessful, lastErrored, performCount, and more, computed automatically and consumable in templates or JS
  • Built-in yieldables - timeout, animationFrame, waitForEvent, waitForQueue, and rawTimeout give cancelable pauses inside a task without leaking timers or listeners
  • TypeScript-first types - full .d.ts declarations and Glint template-type support for typed task definitions and helper invocations

Common Use Cases

  • Debounced search input - a component performs a restartable search task on every keystroke so only the latest request’s response is applied
  • Save button that ignores double-clicks - a drop-policy task on a form’s save action prevents duplicate submissions while a save is in flight
  • Polling a background job’s status - a task loops with await timeout(ms) between checks and is automatically canceled when the component is destroyed
  • Sequential queue of user actions - an enqueue-policy task processes actions like reordering list items one at a time in the order they were triggered

Under The Hood

Architecture The engine uses a layered, generator-based state-machine model. The public task() API (src/-private/task-public-api.js) creates a TaskFactory that applies declarative modifiers (restartable/drop/enqueue/keepLatest/maxConcurrency) through a MODIFIER_REGISTRY, then instantiates a Task bound to a host context, registering lifecycle cleanup via Ember’s destroyable API. Each perform() call produces a TaskInstance whose generator stepping is delegated to a TaskInstanceExecutor, which drives a GeneratorState wrapper one step at a time using an index/ticket mechanism to invalidate stale resumes, alongside a Yieldable protocol that lets built-in and user-defined async primitives register disposer callbacks for cancellation-safe cleanup. Concurrency scheduling is delegated to a separate Scheduler plus pluggable SchedulerPolicy classes, decoupling “what happens on concurrent perform()” from the execution engine itself, which makes the buffer-policy layer swappable without touching the generator engine that every Task funnels through.

Tech Stack The addon is TypeScript-authored and built as an Embroider v2 addon using @embroider/addon-dev and @embroider/addon-shim, bundled with Rollup rather than the legacy ember-cli broccoli pipeline, and ships a custom Babel plugin (async-arrow-task-transform.js) that rewrites task(async () => {…}) arrow syntax into generator functions at build time. Runtime dependencies are minimal and Babel-internal only; the actual task engine has no dependency on RSVP or jQuery and integrates directly with Ember’s runloop, destroyable, and debug modules. A separate test-app workspace is a full Ember application (ember-source, ember-cli) used purely as a test harness and interactive documentation site.

Code Quality Testing lives in the sibling test-app workspace and is extensive: QUnit unit tests cover every scheduler policy (drop, restartable, enqueue, keepLatest, unbounded), task/task-instance/task-state behavior, cancelable-promise helpers, wait-for utilities, error handling, and self-cancel-loop detection, plus .gts integration tests for the perform/task template helpers, acceptance tests, and a dedicated types-tests package exercising the TypeScript declarations. CI runs linting (ESLint, Prettier, ember-template-lint, Glint type checks) followed by cross-browser QUnit runs and a scheduled floating-dependencies job. Cancellation is modeled explicitly and distinctly from errors via a dedicated cancelation-name check and typed CancelRequest kinds (lifespan-end, parent-cancel, yieldable-cancel), and unhandled task rejections are reported rather than swallowed. Naming is consistent throughout and core files carry substantial inline documentation explaining non-obvious control flow.

API Design The task() pattern turns an async generator or async-arrow function into a first-class object with rich derived state (isRunning, isIdle, last, lastSuccessful, performCount) that eliminates manual flag bookkeeping, and buffer-policy modifiers are expressed as plain boolean options on task() itself, keeping call sites declarative and one-line rather than requiring separate cancellation-token plumbing. Getting started requires wiring a Babel transform into the consuming app’s build to unlock the ergonomic async-arrow syntax, a modest amount of setup friction that is offset by an extensive interactive documentation site covering installation, tutorials, cancelation, concurrency, TypeScript usage, testing/debugging, and version upgrades.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search