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

gering-flow

v0.2.0

Published

Composição de chamadas a APIs externas (fan-out, pipe, fallback, race, retry, cache, circuit breaker) — type-safe e agnóstica a transporte, com zero dependências.

Readme

English · Português

Gering

Composição de chamadas a APIs externas para TypeScript/Node.js — fan-out, cache, fallback, retry, timeout e circuit breaker com uma API fluente e segura.

Gering resolve, de forma consistente, o que todo projeto reescreve do zero: orquestrar várias chamadas a serviços externos com resiliência e tipos honestos.

import { task, parallel } from 'gering-flow'

const [previsao, qualidadeAr] = await parallel([
  task(() => openSky.getForecast(cidade)),
  task(() => airIndex.getAQI(cidade)),
]).unwrap()

Instalação

npm install gering-flow

Publicado como ESM, com tipos incluídos. Requer Node ≥ 20.

Princípios de design

  • Agnóstico a transporte. Tudo que importa é () => Promise<T>. Você traz o fetch/axios/SDK que quiser. Zero dependências.
  • Erros explícitos. Nada de throw silencioso: toda execução devolve um Result<T, E> que o consumidor é obrigado a tratar.
  • Cancelável por padrão. AbortSignal é propagado por todo o pipeline desde o v1.
  • Estado isolado. Cada task(fn) é uma instância independente — sem estado global escondido, o que torna tudo trivial de testar.
  • API fluente. task(fn).timeout(2000).retry(3).run().

Conceito central: Task

A abstração que sustenta tudo:

type Task<T, E = Error> = (signal?: AbortSignal) => Promise<Result<T, E>>

Uma função que, dado um AbortSignal opcional, devolve um Result. task(fn) adapta qualquer função que pode lançar para essa forma, capturando exceções como Err.

Result<T, E>

Modelo de erro type-safe, sem dependência externa. A discriminação é pelo campo ok, e a manipulação é feita por métodos no próprio objeto.

type Result<T, E = Error> = Ok<T, E> | Err<T, E>

| Método | Descrição | | --- | --- | | .isOk() / .isErr() | Type guards que estreitam para Ok / Err. | | .map(fn) | Transforma o valor de sucesso. No-op em Err. | | .mapErr(fn) | Transforma o erro. No-op em Ok. | | .andThen(fn) | Encadeia outro Result a partir do valor (flatMap). | | .orElse(fn) | Recupera de um erro produzindo outro Result. | | .unwrap() | Extrai o valor; lança se for Err. | | .unwrapOr(fallback) | Extrai o valor ou retorna o fallback. | | .unwrapErr() | Extrai o erro; lança se for Ok. | | .match({ ok, err }) | Pattern matching exaustivo. |

Construtores e helpers: ok(value), err(error), fromPromise(promise, onError?).

const r = ok(2).map((n) => n + 1)          // Ok(3)
r.match({ ok: (n) => n, err: () => -1 })   // 3

const e = err<string>('boom')
e.unwrapOr(99)                             // 99

Builder fluente

task(fn) devolve um TaskBuilder imutável — cada método retorna um novo builder.

const result = await task(() => fetch(url).then((r) => r.json()))
  .timeout(2000)
  .retry({ attempts: 3, backoff: 'exponential' })
  .map((json) => json.data)
  .run() // → Promise<Result<T, E>>

| Método | Descrição | | --- | --- | | .map(fn) / .mapErr(fn) | Transforma valor / erro do resultado. | | .andThen(fn) | Encadeia outro Task a partir do valor. | | .recover(fn) | Recupera de um erro encadeando outro Task. | | .timeout(ms) | Cancela via AbortSignal se não resolver a tempo. | | .retry(policy) | Repete enquanto retornar Err. Aceita number ou RetryPolicy. | | .cache(options) | Memoiza o resultado de sucesso por uma janela de TTL. | | .circuitBreaker(options?) | Protege um serviço instável (fail-fast). | | .use(wrap) | Ponto de extensão para middleware customizado. | | .run(signal?) | Executa e devolve o Result. | | .unwrap(signal?) | Executa e extrai o valor (lança em Err). |

RetryPolicy: { attempts, backoff?: 'fixed' | 'exponential', delay?, factor? }.

Combinadores

parallel — fan-out com tupla tipada

Dispara N tasks ao mesmo tempo e coleta os resultados em ordem, numa tupla tipada. Diferente de Promise.all, não faz short-circuit: uma falha não derruba as vizinhas — você inspeciona cada Result individualmente.

const [a, b, c] = await parallel([
  task(async () => 1),        // Task<number>
  task(async () => 'dois'),   // Task<string>
  task(async () => fetchX()), // Task<X>
]).unwrap()

// a: Result<number, Error>
// b: Result<string, Error>
// c: Result<X, Error>

Use concurrency para limitar quantas executam ao mesmo tempo (a ordem dos resultados é preservada):

await parallel(tasks, { concurrency: 3 }).unwrap()

A orquestração em si nunca falha (E = never); as falhas vivem dentro de cada Result da tupla.

pipe — sequência com passagem de valor

Composição sequencial onde o output de um passo vira o input do próximo, com os tipos encadeados (V0 → V1 → V2). O primeiro elemento é um Task; cada passo seguinte é uma função que recebe o valor anterior e devolve o próximo Task.

const nome = await pipe(
  task(() => api.getUserId()),                 // Task<string>
  (id) => task(() => api.getUser(id)),         // (string) => Task<User>
  (user) => task(() => api.getName(user.id)),  // (User) => Task<string>
).unwrap()

Short-circuit no primeiro Err: se um passo falha, os seguintes não executam e o pipe devolve esse Err. Os tipos são verificados passo a passo via overloads (até 5 passos; acima disso, encadeie dois pipe).

fallback — cadeia de degradação

Tenta cada task em ordem e devolve o primeiro Ok. Se um falha, passa para o próximo (short-circuit no primeiro sucesso — os seguintes nem executam). Se todos falharem, devolve o último Err.

const previsao = await fallback([
  task(() => openSky.getForecast(cidade)),  // provider primário
  task(() => meteoNow.getForecast(cidade)), // secundário
  task(() => localCache.get(cidade)),       // último recurso
]).unwrap()

Todas as alternativas compartilham o mesmo T e E (são intercambiáveis por definição). Se as fontes têm erros heterogêneos, normalize antes com .mapErr(...) em cada uma.

race — o primeiro a resolver vence

Dispara todos ao mesmo tempo e devolve o primeiro a terminar — vencedor por velocidade, não por sucesso (pode ser Ok ou Err). Os perdedores são cancelados via AbortSignal.

const cotacao = await race([
  task((s) => fetch(provedorA, { signal: s }).then((r) => r.json())),
  task((s) => fetch(provedorB, { signal: s }).then((r) => r.json())),
]).unwrap()

Útil para fontes redundantes onde você quer a resposta mais rápida. Lança em array vazio.

Exemplo end-to-end

examples/weather-dashboard.ts é um exemplo rodável (cenário fictício "Weather Dashboard") que combina os padrões num fluxo de agregação de previsão do tempo:

npm run example

Ele demonstra três situações, cada uma com um provedor caindo de um jeito diferente:

// Previsão resiliente: primário (retry + timeout + cache) → secundário → cache local
function resilientForecast(city: string) {
  return fallback<Forecast>([
    task((s) => openSkyGet(city, s))
      .retry({ attempts: 3, backoff: 'exponential', delay: 10 })
      .timeout(500)
      .cache({ ttl: 60_000, key: `weather:${city}` }),
    task((s) => meteoNowGet(city, s)).timeout(80),
    task(() => cacheGet(city)),
  ])
}

// Várias cidades em paralelo
const forecasts = await parallel(cities.map((c) => resilientForecast(c))).unwrap()

// Pipeline previsão → detecta alerta → push (short-circuit no primeiro erro)
const alerted = await pipe(
  resilientForecast(city),
  (f) => task(() => detectAlert(f)),
  (alert) => task((s) => sendPush(alert, s)).retry(3),
).run()

Saída esperada (o exemplo imprime em inglês) — cada linha mostra um padrão de resiliência salvando o dia (retry, fallback ao secundário, fallback ao cache):

━━━ 1. Resilient forecast per city ━━━
  ✔ Lisbon: 24°C via OpenSky          (retry: OpenSky falhou 2x, acertou na 3ª)
  ✔ Tokyo: 22°C via MeteoNow          (fallback: OpenSky fora → secundário)
  ✔ Nairobi: 29°C via cache (stale)   (fallback: ambos fora → cache local)

Middleware

Cada middleware é uma função (task: Task) => Task — composável via .use(...) ou pelos atalhos fluentes no builder. O estado (cache, contadores do breaker) vive no escopo do middleware: isolado por instância, sem nada global.

| Middleware | Atalho no builder | O que faz | | --- | --- | --- | | withTimeout(ms) | .timeout(ms) | Cancela via signal se estourar o tempo. | | withRetry(policy) | .retry(policy) | Repete enquanto retornar Err. | | withCache(opts) | .cache(opts) | Memoiza o Ok por uma janela de TTL (só sucesso). | | withCircuitBreaker(opts) | .circuitBreaker(opts) | Fail-fast após N falhas consecutivas. |

// Atalhos fluentes:
task(fn).timeout(2000).retry(3).cache({ ttl: 5000 }).circuitBreaker({ threshold: 5 })

// Ou composição explícita via .use, equivalente:
task(fn).use(withTimeout(2000)).use(withRetry(3))

Circuit breaker — máquina de estados closed → open → half-open. Abre após threshold falhas consecutivas (padrão 5), rejeitando na hora com CircuitOpenError por halfOpenAfter ms (padrão 10s); depois deixa uma tentativa passar (half-open) e fecha se ela vingar.

Cache — só Ok é cacheado; Err sempre repassa (não fixa falha transitória). Chaveado por key (padrão 'default').

Desenvolvimento

npm run typecheck   # tsc --noEmit
npm test            # suíte node:test (via tsx)
npm run build       # emite dist/ (js + d.ts)
npm run example     # roda o exemplo Weather Dashboard

Convenção de commits em COMMITS.md (Conventional Commits + template .gitmessage).

Licença

ISC