Releases: productdevbook/misina
v0.5.0
🚀 Features
- typed)(#111: Per-status-code responses + safe mode on TypedMisina - by @productdevbook and Claude Opus 4.7 (1M context) in #112 (d7406)
- typed)(#113: Kind discriminator on safe error envelope - by @productdevbook in #115 (b18ea)
View changes on GitHub
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
v0.3.1
Post-audit Q2/Q3 2026 roadmap landed — 43 feature commits across 13 closed issues since v0.2.0, plus three CI / packaging fixes (v0.3.0 → v0.3.1).
Quick stats since v0.2.0:
- 820 tests across 115 files, all green on Node 22 + 24
- Zero new core dependencies; one optional peer (
undici) gated behindpeerDependenciesMeta - 20 new subpaths added (auth/oauth, auth/sigv4, auth/signed, beacon, digest, driver/http2, driver/undici, graphql, hedge, otel, ratelimit, runtime/{bun,cloudflare,deno,next}, sentry, tracing, transfer, plus existing breaker / cookie / cache / dedupe / paginate / poll / stream / test all extended)
- JSR publishing alongside npm (skipping
runtime/*which JSR rejects) - Reproducible mitata bench suite vs ofetch / ky / axios / native fetch under
bench/
🆕 New subpaths
Auth & signing
misina/auth/oauth—withJwtRefresh(proactive single-flight refresh based on JWTexp),createPkcePair()+exchangePkceCode()(RFC 7636 PKCE / RFC 6749 token exchange) (#101)misina/auth/sigv4—withSigV4()AWS Signature V4 signer, zero@aws-sdk/*peer dep, Web Crypto only (#78)misina/auth/signed—withMessageSignature()RFC 9421 HTTP Message Signatures (Ed25519, ECDSA P-256, RSA-PSS, HMAC-SHA256) (#77)
Body integrity & transfers
misina/digest— RFC 9530withDigest()+verifyDigest()(Content-Digest/Repr-Digest, sha-256 / sha-512) (#76)misina/transfer—downloadResumable()(Range-aware with per-chunk retries) +uploadResumable()(draft-ietf-httpbis-resumable-upload PATCH protocol) (#75)
Observability
misina/otel—withOtel()emits HTTP client spans with the standard semconv attributes;Traceris duck-typed so misina never imports@opentelemetry/*(#93)misina/sentry—withSentry()capturesHTTPError/NetworkError/TimeoutErrorwith the originating request as Sentry context, no@sentry/*peer dep (#83)misina/tracing—withTracing()injects W3Ctraceparent+tracestate+ optionalBaggage(#65)misina/ratelimit—parseRateLimitHeaders()for OpenAI / Anthropic / IETF draft styles,withRateLimit()token-bucket client-side limiter (#62, #92)
LLM / SSE / streaming
sseStreamReconnecting()— Last-Event-ID + serverretry:field + exponential backoff across disconnects (HTML §9.2.4) (#72)accumulateOpenAIToolCalls/accumulateAnthropicMessage— drain a streaming response into the final tool-call list / assembled message (#85)
Networking
misina/driver/undici— Node-only optional driver, takes anyundici.Agent/Pool/Clientso callers can tune pool size, keep-alive, pipelining, and HTTP/2 multiplexing (#73)misina/driver/http2— zero-peer-dep alternative usingnode:http2. OneClientHttp2Sessionper origin, multiplex streams, auto-reconnect onGOAWAY(#74)misina/hedge— hedged requests across multiple endpoints (Google "tail at scale"), aborts losers when the first one returns (#79)misina/graphql—withGraphql()withquery,mutate, Apollo APQ via SHA-256, GET fallback (#80)misina/beacon— fire-and-forget telemetry: fetchLater → fetch keepalive → sendBeacon three-tier fallback (#84)
Test tooling
record()/recordToJSON()/replayFromJSON()— VCR-lite cassette round-trip, no fs dep (#102)harToCassette()— import a Chrome / Playwright HAR file straight into a cassette (#81)coverage()onTestMisina— flags routes that were declared but never hit (#81)randomStatus/randomNetworkError— chaos route handlers (#81)misinaCallSerializer— Vitest snapshot serializer that redacts authorization, idempotency-key, traceparent, etc. (#81)
Runtime augmentations (npm only — see install note for JSR)
misina/runtime/cloudflare— typedcfRequestInit pass-through (#82)misina/runtime/next—onTagInvalidate(misina, revalidateTag)for App Router,tag()helper, augmentsMisinaMeta.invalidates(#100)misina/runtime/bun— typedtls/proxy/unix/verboseknobs (#90)misina/runtime/deno— typedclient: Deno.HttpClientpass-through (#90)
🛡 Security & spec hardening
- Phase-1 hardening (#51, #52, #53, #54): redirect strip headers +
Signature/Signature-Inputstrip on cross-origin, CR/LF/NUL guard in URL composition, scheme-relative URL gate (//host), path-traversal rejection in typed path params (..,/,\, control chars). - AbortSignal.any listener leak (#55): manual
composeSignals()with{ once: true }+ explicit cleanup avoids Node #57736 unbounded-listener growth on long-lived shared signals. - Cookie jar across redirects (#59):
Set-Cookiefrom intermediate 30x hops is persisted (login flows that issue the session on the redirect step). - NetworkError carries response (#56): when the body read fails mid-stream the response is still attached to the error so callers can inspect status / headers / requestId.
📦 Cache, content-types, retry
- RFC 5861 stale-while-revalidate + stale-if-error (#67) — serve stale + background-revalidate, or serve stale on 5xx within window.
- RFC 8246
immutable(#68) — skip ETag / If-None-Match revalidation when the server promised the body can't change. - RFC 9211
parseCacheStatus()(#69) — Structured Field Values parser-backedCache-Statusdecoder (hit,fwd,ttl,stored,collapsed,key,detail). - RFC 6839
+jsonsuffix (#58) —application/vnd.x.api+jsonand friends now route through the JSON parser. retry-after-ms+x-should-retry(#61) — sub-second precision and explicit server hints honored by default (LLM SDK parity).maxResponseSizebyte cap (#63) — Content-Length pre-check + mid-stream counter, throwsResponseTooLargeError.HTTPError.requestId+[req: …]in error message (#66) — auto-scanned fromx-request-id/request-id/x-correlation-id; configurable header list.bodyTimeout(#43) — independent cap on response-body read time for slow-streaming servers.- Opt-in response decompression (#48) —
decompress: true | string[]covers gzip / deflate / br / zstd viaDecompressionStream. - Opt-in request body compression (#64) — symmetrical
compressRequestBody: 'gzip' | 'deflate' | 'deflate-raw'.
🛠 Developer ergonomics
parseServerTiming()+MisinaResponse.serverTimings(#71) — every response carries the parsed W3C Server-Timing entries.path()helper +PathParamsOf<T>(#98) — one-off URL building with full type narrowing on params.toFile()(#94) — build aFilefrom any byte source; auto-wraps async iterables for streaming uploads.followAccepted()(#87) — covers the 202 + Location async-job pattern (HEAD/GET poll until terminal).- RFC 9651 Structured Field Values parser (#70) — internal, powers Cache-Status and future SF-based headers (Repr-Digest, Signature-Input).
shouldRetryparsed body viactx.error.data(#60) — pinned with regression tests so refactors can't quietly break the ky #776 parity.- Symbol.asyncDispose across runtimes (#86) — sseStream, ndjsonStream, linesOf, paginate now all play with
await usingon Node 22 (which lacks the AsyncGenerator dispose by default).
🧪 Tooling
pnpm bench— mitata suite vs ofetch / ky / axios / native fetch over a localnode:httpfixture; full results live inbench/README.md. (#88)- JSR publishing —
pnpm jsr:check(dry run) +pnpm jsr:publish(manual back-fill); the Release workflow publishes to npm + JSR in lock-step on each tag. (#89) - README Recipes section — TanStack Query, SWR, Next.js App Router, Remix, SvelteKit, MSW vs createTestMisina table, pino / winston / consola
onCompletesnippets. (#91)
🐞 Fixes (v0.3.0 → v0.3.1)
test/driver-http2.test.ts > afterAll10 s hook timeout on Node 22 — sockets weren't being torn down becauseHttp2Serverdoesn't exposecloseAllConnections(). The harness now tracks every socket viaserver.on('connection', …)and destroys them explicitly beforeserver.close().releaseworkflow added the🦕 Publish to JSR (OIDC)step (no JSR token needed; OIDC trust-on-name).jsr.jsonexcludessrc/runtime/**— JSR rejects ambientdeclare moduleaugmentation outright (--allow-slow-typesonly suppresses warnings, not errors). The four runtime/* subpaths stay on npm.pnpm releaseno longer callspnpm jsr:publishlocally — the CI workflow already publishes both registries on tag push.
📌 Install
pnpm add misina
# JSR (deno / bunx / npx)
deno add jsr:@productdevbook/misinaJSR caveat: the JSR build skips the four
misina/runtime/*subpaths (bun,cloudflare,deno,next) because JSR doesn't accept ambient module augmentation. npm callers get them as usual; JSR users who need typed runtime knobs can paste theinterface MisinaRuntimeOptions { … }block from the source into their own project.
🔗 Compare
v0.2.0
🚀 Features
- #36: Safe() no-throw mode — { ok, data, error, response } discriminated result - by @productdevbook and Claude Opus 4.7 (1M context) (e97c2)
- #37: Poll() helper — long-poll with until + interval + abort - by @productdevbook and Claude Opus 4.7 (1M context) (da7b2)
- #38: TrailingSlash policy — preserve / strip / forbid - by @productdevbook and Claude Opus 4.7 (1M context) (42642)
- #39: OnComplete lifecycle hook — uniform observability - by @productdevbook and Claude Opus 4.7 (1M context) (e792d)
- #40: MisinaError.toJSON() — log-library safe serialization - by @productdevbook and Claude Opus 4.7 (1M context) (38a0b)
- #41: Generic HTTPError via second type param - by @productdevbook and Claude Opus 4.7 (1M context) (5d67f)
- #42: Meta field — per-request user data flows through hooks - by @productdevbook and Claude Opus 4.7 (1M context) (20881)
- #44: HTTP QUERY method shorthand (.query) - by @productdevbook and Claude Opus 4.7 (1M context) (da713)
- #45: AllowedProtocols config — Capacitor / Tauri / custom schemes - by @productdevbook and Claude Opus 4.7 (1M context) (db104)
- #46: Cache shouldStore + beforeStore callbacks - by @productdevbook and Claude Opus 4.7 (1M context) (805c8)
- #49: ProgressIntervalMs — throttle progress callbacks - by @productdevbook and Claude Opus 4.7 (1M context) (db117)
- #50: State — session-scoped mutable shared state - by @productdevbook and Claude Opus 4.7 (1M context) (868f0)
View changes on GitHub
v0.1.0
🚀 Features
- Driver pattern, hooks lifecycle, error taxonomy - by @productdevbook and Claude Opus 4.7 (1M context) (fca69)
- Retry, timeout/abort, redirect policy, validate, extend, test utils - by @productdevbook and Claude Opus 4.7 (1M context) (0697d)
- Streaming, progress, paginate, dedupe, cache, auth, cookie, typed API - by @productdevbook and Claude Opus 4.7 (1M context) (73f95)
- Response timings, framework passthrough, header smuggling guard - by @productdevbook and Claude Opus 4.7 (1M context) (c94b3)
- Stream body retry contract, examples, CHANGELOG - by @productdevbook and Claude Opus 4.7 (1M context) (6e0f1)
- ParseJson(text, ctx) — request/response context for routing parsers - by @productdevbook and Claude Opus 4.7 (1M context) (e6b89)
- ValidateResponse can be async - by @productdevbook and Claude Opus 4.7 (1M context) (f9c9b)
- Widen MisinaOptions.headers to HeadersInit - by undefined> and Claude Opus 4.7 (1M context) [( Reco)](https://github.com/productdevbook/misina/commit/ Record<…)
- SchemaValidationError surfaces first issue + path in message - by @productdevbook and Claude Opus 4.7 (1M context) (19916)
- IdempotencyKey: 'auto' — auto Idempotency-Key for retried mutations - by @productdevbook and Claude Opus 4.7 (1M context) (ccd53)
- RFC 9457 problem+json parsing on HTTPError - by @productdevbook and Claude Opus 4.7 (1M context) (94307)
- BeforeRetry can return Response to short-circuit retries - by @productdevbook and Claude Opus 4.7 (1M context) (5ed7a)
- Priority — pass-through RequestInit.priority hint - by @productdevbook and Claude Opus 4.7 (1M context) (67579)
- breaker: Circuit-breaker subpath misina/breaker - by @productdevbook and Claude Opus 4.7 (1M context) (c8c08)
- openapi: Type-only adapter from openapi-typescript paths to EndpointsMap - by @productdevbook and Claude Opus 4.7 (1M context) in #35 (d2a04)
- typed: Init argument is optional when no required fields - by @productdevbook and Claude Opus 4.7 (1M context) (30710)
🐞 Bug Fixes
- Audit pass 1 — nine real bugs found by re-reading the codebase - by @productdevbook and Claude Opus 4.7 (1M context) (c3248)
- Audit pass 2 — request cloning, body validation, redirect spec - by @productdevbook and Claude Opus 4.7 (1M context) (bf9cf)
- Audit pass 3 — cache RFC 9111 + cookie RFC 6265 + dedupe correctness - by @productdevbook and Claude Opus 4.7 (1M context) (6191b)
- Audit pass 4 — refresh-on-401 recursion, paginate cycle detection - by @productdevbook and Claude Opus 4.7 (1M context) (f8b39)
- Audit pass 5 — SSE parser WHATWG HTML §9.2 compliance - by @productdevbook and Claude Opus 4.7 (1M context) (fcf36)
- Audit pass 7 — Link header parser + cache Vary key - by @productdevbook and Claude Opus 4.7 (1M context) (41a86)
- Pre-flight + retry-loop abort checks - by @productdevbook and Claude Opus 4.7 (1M context) (a707d)
- MergeHeaders accepts Headers/[k,v][]/undefined values - by @productdevbook and Claude Opus 4.7 (1M context) (cef6e)
- Stream cancel + HTTPError body parse tolerance - by @productdevbook and Claude Opus 4.7 (1M context) (af4cc)
- dedupe: Free slot the moment underlying request settles - by @productdevbook and Claude Opus 4.7 (1M context) (c3c1b)