Skip to content

Releases: productdevbook/misina

v0.5.0

03 May 11:28

Choose a tag to compare

   🚀 Features

    View changes on GitHub

v0.4.0

26 Apr 19:42

Choose a tag to compare

💥 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

v0.3.1

26 Apr 17:58

Choose a tag to compare

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 behind peerDependenciesMeta
  • 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/oauthwithJwtRefresh (proactive single-flight refresh based on JWT exp), createPkcePair() + exchangePkceCode() (RFC 7636 PKCE / RFC 6749 token exchange) (#101)
  • misina/auth/sigv4withSigV4() AWS Signature V4 signer, zero @aws-sdk/* peer dep, Web Crypto only (#78)
  • misina/auth/signedwithMessageSignature() RFC 9421 HTTP Message Signatures (Ed25519, ECDSA P-256, RSA-PSS, HMAC-SHA256) (#77)

Body integrity & transfers

  • misina/digest — RFC 9530 withDigest() + verifyDigest() (Content-Digest / Repr-Digest, sha-256 / sha-512) (#76)
  • misina/transferdownloadResumable() (Range-aware with per-chunk retries) + uploadResumable() (draft-ietf-httpbis-resumable-upload PATCH protocol) (#75)

Observability

  • misina/otelwithOtel() emits HTTP client spans with the standard semconv attributes; Tracer is duck-typed so misina never imports @opentelemetry/* (#93)
  • misina/sentrywithSentry() captures HTTPError / NetworkError / TimeoutError with the originating request as Sentry context, no @sentry/* peer dep (#83)
  • misina/tracingwithTracing() injects W3C traceparent + tracestate + optional Baggage (#65)
  • misina/ratelimitparseRateLimitHeaders() for OpenAI / Anthropic / IETF draft styles, withRateLimit() token-bucket client-side limiter (#62, #92)

LLM / SSE / streaming

  • sseStreamReconnecting() — Last-Event-ID + server retry: 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 any undici.Agent / Pool / Client so callers can tune pool size, keep-alive, pipelining, and HTTP/2 multiplexing (#73)
  • misina/driver/http2 — zero-peer-dep alternative using node:http2. One ClientHttp2Session per origin, multiplex streams, auto-reconnect on GOAWAY (#74)
  • misina/hedge — hedged requests across multiple endpoints (Google "tail at scale"), aborts losers when the first one returns (#79)
  • misina/graphqlwithGraphql() with query, 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() on TestMisina — 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 — typed cf RequestInit pass-through (#82)
  • misina/runtime/nextonTagInvalidate(misina, revalidateTag) for App Router, tag() helper, augments MisinaMeta.invalidates (#100)
  • misina/runtime/bun — typed tls / proxy / unix / verbose knobs (#90)
  • misina/runtime/deno — typed client: Deno.HttpClient pass-through (#90)

🛡 Security & spec hardening

  • Phase-1 hardening (#51, #52, #53, #54): redirect strip headers + Signature / Signature-Input strip 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-Cookie from 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-backed Cache-Status decoder (hit, fwd, ttl, stored, collapsed, key, detail).
  • RFC 6839 +json suffix (#58) — application/vnd.x.api+json and 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).
  • maxResponseSize byte cap (#63) — Content-Length pre-check + mid-stream counter, throws ResponseTooLargeError.
  • HTTPError.requestId + [req: …] in error message (#66) — auto-scanned from x-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 via DecompressionStream.
  • 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 a File from 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).
  • shouldRetry parsed body via ctx.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 using on Node 22 (which lacks the AsyncGenerator dispose by default).

🧪 Tooling

  • pnpm bench — mitata suite vs ofetch / ky / axios / native fetch over a local node:http fixture; full results live in bench/README.md. (#88)
  • JSR publishingpnpm 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 onComplete snippets. (#91)

🐞 Fixes (v0.3.0 → v0.3.1)

  • test/driver-http2.test.ts > afterAll 10 s hook timeout on Node 22 — sockets weren't being torn down because Http2Server doesn't expose closeAllConnections(). The harness now tracks every socket via server.on('connection', …) and destroys them explicitly before server.close().
  • release workflow added the 🦕 Publish to JSR (OIDC) step (no JSR token needed; OIDC trust-on-name).
  • jsr.json excludes src/runtime/** — JSR rejects ambient declare module augmentation outright (--allow-slow-types only suppresses warnings, not errors). The four runtime/* subpaths stay on npm.
  • pnpm release no longer calls pnpm jsr:publish locally — the CI workflow already publishes both registries on tag push.

📌 Install

pnpm add misina
# JSR (deno / bunx / npx)
deno add jsr:@productdevbook/misina

JSR 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 the interface MisinaRuntimeOptions { … } block from the source into their own project.

🔗 Compare

Full diff: v0.2.0...v0.3.1

v0.2.0

26 Apr 07:04

Choose a tag to compare

   🚀 Features

    View changes on GitHub

v0.1.0

25 Apr 23:17

Choose a tag to compare

   🚀 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)
    View changes on GitHub