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

@zakkster/lite-task

v1.0.0

Published

Generator-driven task runner for @zakkster/lite-signal. Synchronous-looking function* flows that yield Promises, signal conditions, time-slices, and races, with deterministic AbortController-based cancellation (iter.return runs your finally). Take-latest/

Readme

@zakkster/lite-task

npm version sponsor npm bundle size npm downloads npm total downloads lite-signal peer TypeScript Dependencies license

A generator-driven task runner for @zakkster/lite-signal. Write synchronous-looking function* flows that yield Promises, signal conditions, time-slices, or races; the runner drives them with deterministic, abortable cleanup. Cancelling a run aborts its AbortSignal and calls the generator's .return(), so your finally always runs.

import { task } from "@zakkster/lite-task";

const search = task(function* (signal, q) {          // signal is the run's AbortSignal
  const res = yield fetch(`/api/search?q=${q}`, { signal });
  return yield res.json();
}, { mode: "switch" });                              // "switch" = take-latest

search("re");                                         // typing fast...
search("rea");                                        // ...each call aborts the previous fetch
const run = search("react");
run.done.then(console.log);
search.running();                                     // reactive signal -> boolean

Why a generator runner (and an honest GC note)

async/await allocates a Promise and schedules microtasks at every await; in a hot loop that is visible GC pressure. A generator compiles to a single state machine, and -- unlike a Promise -- it is trivially cancellable: stop calling .next(), or call .return() to run its finally.

To be precise about the claim: lite-task is zero-GC for the synchronous orchestration and time-slicing. yield tick() resumes via a scheduler callback and yield waitFor(sig) resumes via lite-signal's when -- neither allocates a Promise per step. Real I/O (fetch) still returns one platform Promise per operation; you yield it and the runner awaits it. That is unavoidable and is not the hot path. So this is zero-GC control flow, not "zero-GC async."

Install

State is exposed as lite-signal signals, so @zakkster/lite-signal is a peer dependency:

npm install @zakkster/lite-task @zakkster/lite-signal

ESM only. Zero runtime dependencies of its own. Runs in browsers and Node (the time-slice scheduler uses MessageChannel, falling back to setTimeout; frame() uses requestAnimationFrame, falling back to a macrotask off-DOM).

flowchart TD
  CALL["task(gen)(...args)"] --> RUN["AbortController + iter = gen(signal, ...args)"]
  RUN --> NEXT["iter.next(value)"]
  NEXT --> Y{what was yielded?}
  Y -->|Promise| AWAIT["await -> resume with value, or throw on reject"]
  Y -->|waitFor pred| WHENC["when pred -> resume once truthy"]
  Y -->|tick / frame / sleep| SCHED["scheduler -> resume next macrotask / frame / timer"]
  Y -->|race / all| COMB["combine -> resume with winner / results"]
  Y -->|return| SETTLE["settle: status = done, resolve done"]
  AWAIT --> NEXT
  WHENC --> NEXT
  SCHED --> NEXT
  COMB --> NEXT
  RUN -. cancel .-> ABORT["abort + iter.return -> finally runs; status = cancelled"]

What you can yield

A task generator receives the run's AbortSignal first, then your call arguments, and may yield:

  • a Promise (e.g. fetch(url, { signal })) -- awaited; resumes with the value, or the rejection is thrown back in (catch it normally).
  • waitFor(predicate) -- resumes once predicate (a reactive read) first returns truthy, with that value. Built on lite-signal's when, so it is allocation-free and the subscription is disposed if the run is cancelled.
  • sleep(ms) -- resumes after a delay.
  • tick() -- yields control to the event loop (a macrotask) and resumes on the next turn. Use it to time-slice heavy loops so the frame can paint. (A microtask would not unblock rendering, which is why this is a macrotask.)
  • frame() -- resumes on the next animation frame.
  • race([...]) / all([...]) -- first-settles / all-settle over a mix of the above; losing branches are cancelled.
import { task, tick } from "@zakkster/lite-task";

// time-slice a heavy build so the UI stays responsive
const buildIndex = task(function* (signal, items) {
  for (let i = 0; i < items.length; i++) {
    indexOne(items[i]);
    if (i % 500 === 0) yield tick();   // let the browser paint
  }
});

Cancellation

run.cancel(reason?) aborts the run's AbortSignal and calls the generator's .return(), so a try { ... } finally { ... } cleanup block runs deterministically -- you do not need if (cancelled) return checks scattered through your logic. Pass the run's signal to fetch and the network request is dropped at the same moment. run.done rejects with TaskError (code cancelled) on abort, so a try/catch can tell cancel from error from success -- or just read the status signal.

Modes

task(gen, { mode }) controls how overlapping invocations behave:

  • switch (default) -- a new call aborts the previous run. Take-latest; ideal for typeahead and dependent fetches.
  • drop -- ignore a new call while one is running; returns the in-flight handle. Take-leading; good for submit buttons.
  • concurrent -- every call runs.
  • queue -- calls run one after another, in order.

Reactive state

The task object carries lite-signal signals reflecting the latest run, so a UI can bind them directly: task.status ('idle' | 'running' | 'done' | 'error' | 'cancelled'), task.running (boolean), task.error, task.result. task.cancelAll(reason?) cancels everything in flight (and any queued runs).

Combining cancellation sources

combineSignals(...signals) merges several AbortSignals into one (using native AbortSignal.any when available, a listener-merge fallback otherwise) and returns { signal, dispose }. timeout(ms) is an AbortSignal that aborts after a delay. Together they give cancel-or-timeout:

import { task, combineSignals, timeout } from "@zakkster/lite-task";

const load = task(function* (cancel, url) {
  const { signal, dispose } = combineSignals([cancel, timeout(5000)]); // cancel OR 5s
  try {
    const res = yield fetch(url, { signal });
    return yield res.json();
  } finally {
    dispose(); // detach the merge listeners
  }
});

Not a reactive resource

lite-task is an imperative runner: you call it to start work. If you want the reactive "subscribe to a signal, auto-fetch, expose data/loading/error" pattern, use @zakkster/lite-resource (resource(source, fetcher)), which is built for it. The two are deliberately separate.

Scope and limitations

v1 covers the runner, the yieldables, the four modes, reactive status, and the combineSignals/timeout utilities. Out of scope: saga-style channels and fork/join trees, retry/backoff, and debounce/throttle wrappers (composable from sleep/switch). For the reactive auto-fetch resource pattern, use lite-resource.

License

MIT (c) Zahary Shinikchiev