Skip to content

v0.4.0

Choose a tag to compare

@github-actions github-actions released this 26 Apr 19:42
· 16 commits to main since this release

💥 Breaking change: with* helpers are gone

Every withX(misina, opts) wrapper has been removed. Plugins now compose
through a single use: [...] array on createMisina.

Before

import { createMisina } from "misina"
import { withBearer } from "misina/auth"
import { withCache } from "misina/cache"
import { withCircuitBreaker } from "misina/breaker"

const api = withCircuitBreaker(
  withCache(withBearer(createMisina({ baseURL }), () => store.token), { ttl: 60_000 }),
  { failureThreshold: 5 },
)

After

import { createMisina } from "misina"
import { bearer } from "misina/auth"
import { cache } from "misina/cache"
import { breaker } from "misina/breaker"

const api = createMisina({
  baseURL,
  use: [
    bearer(() => store.token),
    cache({ ttl: 60_000 }),
    breaker({ failureThreshold: 5 }),
  ],
})

api.breaker.state() // ✓ typed via the plugin's TExt

Plugins are applied left-to-right: the first is innermost, the last
is outermost. A wrapping plugin (e.g. breaker) placed after a
hook-only plugin observes that hook's effects on every call it admits.

Mapping

before after
withBearer(m, src) bearer(src)
withBasic(m, u, p) basic(u, p)
withCsrf(m, opts) csrf(opts)
withRefreshOn401(m, opts) refreshOn401(opts)
withSigV4(m, opts) sigv4(opts)
withJwtRefresh(m, opts) jwtRefresh(opts)
withMessageSignature(m, opts) messageSignature(opts)
withCache(m, opts) cache(opts)
withCookieJar(m, jar) cookieJar(jar)
withDigest(m, opts) digestAuth(opts)
withDedupe(m, opts) dedupe(opts)
withCircuitBreaker(m, opts) breaker(opts)
withRateLimit(m, opts) rateLimit(opts)
withTracing(m, opts) tracing(opts)
withOtel(m, opts) otel(opts)
withSentry(m, opts) sentry(opts)
withGraphql(m, opts) createGraphqlClient(m, opts) (carve-out)

createGraphqlClient is the only carve-out — it returns a
GraphqlClient, not a Misina, so it can't fit the plugin shape.

Why

Imperative withX(withY(withZ(misina, ...), ...), ...) zincirleri
okuması zor, sırası belli değil, yeni eklemek için her seferinde tüm
chain'i yeniden yazmak gerekiyordu. Tek bir konfig array'ine geçmek
config-as-data düşüncesini koruyor, plugin sırası okuma yönüyle aynı,
ekosistem yazarları tek bir MisinaPlugin sözleşmesini öğrenip her
yerde kullanabiliyor.

Idea credit: @aleclarson — thanks for
the nudge that the with* wrapping was hostile DX.

Writing your own plugin

import type { MisinaPlugin } from "misina"

export function timingHeader(name = "x-client-time"): MisinaPlugin {
  return {
    name: "timingHeader",
    hooks: {
      beforeRequest: (ctx) => {
        const headers = new Headers(ctx.request.headers)
        headers.set(name, String(Date.now()))
        return new Request(ctx.request, { headers })
      },
    },
  }
}

Need to add a method or a typed handle on the returned client (like
breaker's .breaker)? Use the extend slot and declare what you
contribute through MisinaPlugin<TExt>. TExt must be a plain object
literal — unions trigger TypeScript's intersection × union cross-product
expansion.

What didn't change

  • All hook semantics (beforeRequest, afterResponse, beforeError,
    onComplete, …) are identical.
  • All plugin behavior (cache RFC compliance, circuit-breaker state
    machine, dedupe in-flight collapsing, …) is byte-for-byte the same.
  • createMisina(...).extend(...) still produces a child instance with
    deep-merged options. Plugins are resolved at the root call only.

Full diff: v0.3.1...v0.4.0