v0.4.0
💥 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 TExtPlugins 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