Skip to content

feat!: v2.0.0-rc.0 — schema-inferred, middleware-composable, universal#1

Open
sshahriazz wants to merge 10 commits into
mainfrom
polash
Open

feat!: v2.0.0-rc.0 — schema-inferred, middleware-composable, universal#1
sshahriazz wants to merge 10 commits into
mainfrom
polash

Conversation

@sshahriazz
Copy link
Copy Markdown
Collaborator

What & why

Ground-up rewrite of amu-http as the most type-safe HTTP client in JavaScript. v1's class-based API is replaced with a functional core, a single Koa-style middleware abstraction, and three wedge features no other client has shipped together: schema-inferred response and request body types, type-safe URL parameters, and a 5-class discriminated error union with a Result<T> escape hatch.

This is 2.0.0-rc.0 — feature-complete, locked API, pending real-user feedback before tagging stable. 8 commits, ~12 KLOC of changes across src/, tests/, docs/, examples/, bench/, scripts/, and CI workflows.

Changes

Three wedges nobody else has

  • Schema-inferred response + request body types via Standard Schema (Zod 3.24+, Valibot 0.31+, ArkType 2+). client.get(url, { schema: { response: User } }) returns z.infer<typeof User> with no <T> annotation. schema.body validates outgoing payloads pre-send.
  • Type-safe URL paramsclient.get('/users/:id/posts/:postId', { params: { id, postId } }) enforces every placeholder at compile time. Segment-walker parser handles absolute URLs and avoids the https: colon trap. Recursion-capped at 24 segments.
  • Discriminated errors + safe() Result API — five classes (AmuError, AmuNetworkError w/ 8 kind values, AmuUrlError, AmuValidationError, AmuUnknownError). client.safe.* returns Result<T> for exhaustive switch narrowing.

Core + supporting feature surface

  • Functional core, no class. createClient(config) returns a frozen Client; default amu singleton kept for one-line absolute-URL calls.
  • Single Koa-style middleware abstraction. Built-in features (retry, timeout, validate, parse) are themselves middleware. next() may be called multiple times so retry can re-enter.
  • Frozen request context with withHeader/withMeta/withSignal helpers.
  • Streaming first-classclient.stream() returns ReadableStream<Uint8Array>; tree-shakable parseSSE and parseNDJSON (with optional per-line schema).
  • safe() Result APIclient.safe.{get,post,...} returns Result<T> = { ok: true; data } | { ok: false; error }.
  • client.extend(overrides) — sub-clients with merged config + middleware.
  • Configurable query serializer'flat' (default) | 'qs' | custom function.
  • Per-attempt retry hookRetryConfig.onAttempt({ attempt, error, delayMs }) closes the ky.beforeRetry gap.
  • Body serializer — pass-through for FormData, URLSearchParams, Blob, ArrayBuffer, ReadableStream, strings; auto-falls-back to JSON.

Built-in middleware library (per-file, tree-shakable)

Module Exports
amu-http/middleware/auth bearerAuth, basicAuth, refreshOn401 (concurrent-refresh dedupe)
amu-http/middleware/requestId requestId
amu-http/middleware/logger logger (dev request/response, redacts Authorization in verbose)
amu-http/middleware/otel otel (W3C traceparent + spans, peer dep on @opentelemetry/api)
amu-http/middleware/cookies cookies, createCookieJar (zero-dep RFC 6265 subset)
amu-http/middleware/cache cache, createMemoryCacheStore (RFC 9111 subset, ETag/Last-Modified revalidation)

Helpers + test infrastructure

  • amu-http/formsformData, urlEncoded typed builders
  • amu-http/paginationpaginate({ schema }) async iterator with cursor/pageToken presets and parseLinkHeader helper
  • amu-http/testcreateMockClient() typed mock with URL template matching, request recording, .assertCalled / .assertNotCalled

Tests + quality

  • 241 tests across unit / browser-env (happy-dom + Playwright) / type-level / Standard Schema interop / classifier matrix / leak tests
  • Coverage thresholds raised to 90/85/95/90; current 95+ / 86+ / 98+ / 95+
  • Network-error classifier matrix — 24 tests covering undici, Bun, Deno, browser fetch error shapes; signal-precedence; retryability flag
  • Competitor benchmark (bench/competitors.bench.ts) — amu vs ky 2.x / ofetch / redaxios / axios on the same in-process workload
  • Performance: amu adds ~2.82× over raw fetch's microseconds (ofetch 1.32×, axios 2.79×, redaxios 2.86×, ky 4.25×); negligible vs network RTT

Tooling / build

  • ESM-only, target ES2022, Node ≥ 20.3 (native AbortSignal.any)
  • tsdown multi-entry — 11 entry points, shared chunks for tree-shaking
  • Biome 2 for lint + format (replaces ESLint + Prettier)
  • publint + arethetypeswrong — 11 entry points × 4 resolution modes = 44/44 green
  • size-limit — 5 budgets covering core + per-feature import sets:
    • core ({ createClient }) — 3.48 KB gzip
    • with parseSSE — 3.88 KB
    • with parseSSE + parseNDJSON — 4.15 KB
    • full barrel — 4.16 KB

CI workflows

Workflow Trigger Coverage
ci.yml every PR + push lint + typecheck + test + build + verify + size on Node 20.3 / 20 / 22
ci.yml (typecheck-matrix) every PR + push tsc --noEmit against TypeScript 5.5 / 5.8 / 6.0
runtime-smoke.yml every PR + push Bun + Deno smoke against built dist/
real-network.yml (NEW) manual + weekly cron real network smoke against jsonplaceholder.typicode.com on Node / Bun / Deno
browser.yml (NEW) manual + when src/ changes Playwright Chromium via @vitest/browser
codeql.yml every PR + weekly security-and-quality SAST
publish.yml push to main Changesets-driven release-PR / publish with npm provenance

Documentation

  • README — leads with the 3 wedges, full cookbook, competitive matrix vs ky/ofetch/wretch/got/fetch/axios, runtime/migration tables
  • CONTRIBUTING — v2 architecture, type-safety contract, all 11 entry points, full tooling table
  • 3 migration guides under docs/: from axios, from ky, from ofetch
  • 11 runnable numbered examples demonstrating every major feature
  • TypeDoc API docs generated to docs/api/ (npm run docs:api)
  • CHANGELOG — comprehensive 2.0.0 + 2.0.0-rc.0 entries

Checklist

  • I ran npm run lint && npm run typecheck && npm test locally and everything passes (241/241 tests, 95+ coverage)
  • I added/updated tests covering the new behavior (unit + type tests where applicable)
  • I added a changeset (npx changeset) — NOT NEEDED for this PR: this is the v2.0.0-rc.0 release itself; version is bumped manually since changesets can't graduate 2.0.0-alpha.0 → 2.0.0 cleanly. Future minor/patch from 2.0.x onward will use changesets normally
  • Bundle size impact reviewed via npm run size (the gate also runs in CI). All 5 budgets green
  • Public API changes are reflected in tests/types/*.test-d.ts (21 type-level test cases)

Breaking changes

This is the v2 GA. Everything in v1 is on the table. Migration is documented in docs/migration-from-axios.md, docs/migration-from-ky.md, docs/migration-from-ofetch.md. v1 → v2 specifically:

v1 v2
new Amu(...) / class Amu createClient(...) (no class)
amu.get<T>(url) typing <T> still works or schema: { response: T } (inferred)
params (query string) split into params (URL :id substitutions) and query (?key=value)
{ raw: true } dropped — use client.stream() for raw Response
AmuError + AmuNetworkError (4 kinds) 5 classes with 8 AmuNetworkError.kind values
safe() Result API new
Streaming (stream/parseSSE/parseNDJSON) new
createMockClient new
CJS dual-format dropped — ESM-only
Engines bumped from >=18 to >=20.3 (native AbortSignal.any)

No automated codemod ships. Pin to 1.x if you can't migrate immediately.

Why RC, not GA

Per the audit before this PR, three things made 2.0.0 GA premature:

  1. No production track record (zero adoption — the maturity gap vs ky/ofetch/axios is real)
  2. No real-user feedback on the API surface
  3. Migration story from competitors needed concrete recipes (now shipped)

2.0.0-rc.0 locks the API. Plan: ~1–2 weeks of RC, address feedback, tag stable.

Related issues

Refs the v2 plan discussions in this thread.


🚢 Ready to ship as 2.0.0-rc.0. Reviewers: focus on (1) API surface — anything you'd want changed before stable? (2) Migration guides — coverage of your real-world codebase patterns? (3) Bench claims — replicate locally?

Build & bundling
- Switch tsup → tsdown (rolldown). Build ~270ms; ESM 1.85 KB / CJS 2 KB gzip.
- Fix exports map: per-condition types, drop the require.cjs shim.
- Add sideEffects:false, engines.node>=18, module field.
- tsconfig: moduleResolution Bundler, add @/* → src/* path alias, drop .js
  extensions throughout src/ and tests/.
- Disable sourceMap/declarationMap in build tsconfig (no src/ ships, maps were
  dead weight).

Test & quality gates
- Bump Vitest 1.6 → 3, add @vitest/coverage-v8 (clears prior audit warnings).
- Add Biome 2 (single tool, replaces ESLint+Prettier); auto-fix run on the
  codebase (import sort, useImportType, formatting).
- Add tests/types/ — expectTypeOf assertions on the public API surface.
- Add tests/browser/ — happy-dom-env smoke tests for browser-like globals.
- Add bench/ — vitest bench microbenchmarks via tinybench.
- Add publint + arethetypeswrong + size-limit gates.
- Wire coverage thresholds (75/80/65/75) as a ratchet baseline.
- Replace standard-version with Changesets; release flow runs via
  changesets/action@v1 with NPM_CONFIG_PROVENANCE=true.
- Convert examples/*.js → *.ts (run via tsx).

CI workflows
- New ci.yml: lint, typecheck, test, build, verify, size on Node 18/20/22
  + a typecheck-matrix job covering TypeScript 5.5 / 5.8 / 6.0.
- New runtime-smoke.yml: Bun + Deno smoke against built dist/.
- New codeql.yml: security-and-quality SAST on PR + weekly schedule.
- Rewrite publish.yml: Changesets-action driven release-PR / publish flow with
  npm provenance.

Community & repo hygiene
- Add simple-git-hooks + lint-staged pre-commit (biome on staged files).
- Add SECURITY.md, .github/ISSUE_TEMPLATE/{bug,feature,config},
  PULL_REQUEST_TEMPLATE.md.
- Add .github/dependabot.yml (weekly grouped npm + monthly actions).
- Add jsr.json for optional dual-publish to jsr.io.
- Add .editorconfig and .npmrc (engine-strict=true).
- Update .gitignore (coverage/, *.log, *.tsbuildinfo, .vitest-cache/).

Documentation
- README: add badges (npm, downloads, gzip size, types, CI, license); expand
  Development section.
- CONTRIBUTING.md: full rewrite covering tooling, test categories, CI matrix,
  release flow, required repo settings, code conventions.
- examples/README.md: updated for tsx-based TypeScript runs.
…e, universal

BREAKING CHANGE: complete rewrite. v1 code is gone; new public API.

Architecture
- Functional core (no class). createClient(config) returns a frozen Client; the
  default amu singleton remains for absolute-URL one-liners.
- Single Koa-style middleware abstraction. Built-in features (retry, timeout,
  validate, parse, parseOnError) are themselves middleware. User middleware
  composes via the same chain.
- Per-request RequestContext is shape-frozen; Headers cloned on write. Pure
  helpers `withHeader`, `withMeta`, `withSignal`. Composer permits `next()` to
  be called multiple times so retry middleware can re-enter.
- ESM-only, engines >=20.3, target ES2022, native AbortSignal.any().

Schema inference — both directions (the wedge)
- Standard Schema v1 inlined; works with Zod 3.24+, Valibot 0.31+, ArkType 2+.
- `schema.response` types the resolved value; `schema.body` types and validates
  POST/PUT/PATCH payloads pre-send.
- Legacy `.parse` / function validator fallback still infers via `Awaited<...>`.

Type-safe URL params (the second wedge)
- Path templates like `/users/:id` extract required `params` keys at compile
  time. Segment-walker parser sidesteps the `https:` colon trap. Recursion
  capped at 24 segments.
- `query` is now its own option (separate from URL `params`).

Errors — discriminated, exhaustive
- Five classes: AmuError (HTTP non-2xx), AmuNetworkError (8 kinds:
  dns | connect | tls | timeout-idle | timeout-active | abort | reset | unknown),
  AmuUrlError, AmuValidationError, AmuUnknownError.
- AmuAnyError union enables exhaustive switch.

safe() Result API
- `client.safe.{get,post,...}` returns `Result<T> = { ok: true; data } | { ok: false; error }`.
- Implementation is a 5-line wrapper; non-amu errors normalized to AmuUnknownError.

Body serialization
- New body.ts handles FormData, URLSearchParams, Blob, ArrayBuffer/views,
  ReadableStream (streamed uploads), strings. Plain objects fall back to JSON.
  Sets sensible default Content-Type only when caller hasn't.

Streaming — first-class, tree-shakable
- `client.stream(path, options)` returns Promise<ReadableStream<Uint8Array>>.
- Standalone parsers exported as named functions (tree-shaken when unused):
  parseSSE — spec-compliant SSE parser, normalizes CR/CRLF, joins multi-line
  data fields. Auto-reconnect intentionally not included.
  parseNDJSON — newline-delimited JSON with optional per-line schema validation
  and `onError: 'throw' | 'skip' | 'yield'`.
- Both cancel the upstream on `break`/`throw` from `for await` (verified by
  leak tests asserting cancel() fires on the underlying ReadableStream).

Built-in middleware library — per-file entry points
- amu-http/middleware/auth — bearerAuth (static or () => token, sync/async),
  basicAuth (UTF-8-safe base64), refreshOn401 (concurrent-refresh dedupe).
- amu-http/middleware/requestId — x-request-id stamping with crypto.randomUUID
  fallback; preserves existing header by default.
- amu-http/middleware/logger — dev request/response logger with redacted
  Authorization in verbose mode.
- Composition: user middleware sits OUTSIDE built-ins so refreshOn401 catches
  AmuError thrown by parse, and logger sees the full request lifecycle.

Test infrastructure
- amu-http/test exports createMockClient(config?). Full type-safe URL template
  matching, request recording, assertCalled / assertNotCalled, reply()
  shorthand, per-handler delay, async handlers. Replaces vi.stubGlobal('fetch'),
  composes naturally with all amu features (schemas, middleware, retries).

Tooling / build
- tsdown multi-entry: dist/index.mjs + dist/middleware/* + dist/test/mock.mjs
  with shared chunks. Each middleware is independently tree-shakable.
- tsconfig: noUncheckedIndexedAccess, verbatimModuleSyntax, ES2022, strict,
  Bundler resolution, @/* alias.
- size-limit: 5 budgeted import-sets covering core, errors-only, +SSE, +NDJSON,
  full barrel. Core ~3.24 KB gzip; full barrel 3.92 KB.
- attw: 5 entry points × 4 resolution modes — 20/20 green.

Tests / coverage
- 150 tests across unit (url, middleware-pipeline, client, streaming, sse,
  ndjson, mock-client, middleware-auth/requestId/logger), browser smoke
  (happy-dom), Standard Schema interop (Zod), and 21 type-level *.test-d.ts
  assertions on the public API surface.
- Coverage thresholds raised to 90/85/95/90; current 95.08 / 85.68 / 97.64 / 95.08.

Removed
- class Amu, AmuHybrid, AmuPromise, the `(url, body, config)` v1 method shapes,
  CJS dual-format, the require.cjs shim, all legacy examples and smoke scripts.

Package
- amu-http@2.0.0-alpha.0
client.extend(overrides)
- New sub-client factory on Client. Inherits parent config + middleware;
  overrides win on baseURL / timeout / retries / fetch / querySerializer;
  headers are merged (extension wins on conflict); middleware is appended
  (extension sits inside parent in the onion).
- Enables clean composition patterns: a generic api client extended with
  per-feature middleware (e.g. authed scopes, alternate baseURLs).

Configurable query serializer
- New `querySerializer` field on ClientConfig: 'flat' (default) | 'qs' | function.
- 'flat': scalars + comma-joined arrays — the safe REST default.
- 'qs':  bracketed nested syntax — `{filter:{date:{gt:'2024'}}}` →
        `filter[date][gt]=2024`. Handles arbitrary nesting + arrays of objects.
- Custom function gets the raw query record, returns the final string.
- Per-request `query` widened from primitives-only to `Record<string, unknown>`
  so nested objects can flow through the configured serializer.

amu-http/forms — tree-shakable form builders
- formData(fields) — typed FormData builder; arrays append multiple entries;
  Blob/File pass through; null/undefined dropped.
- urlEncoded(fields) — URLSearchParams equivalent for form-urlencoded bodies.

Tooling
- Multi-entry build now emits `dist/forms/index.{mjs,d.mts}` chunk.
- New `./forms` package export.
- size-limit budgets bumped to honest reality after v2.4 additions:
  core 3.45 KB, with SSE 3.84 KB, with SSE+NDJSON 4.12 KB, full 4.13 KB.

Tests
- 175 tests across 20 files (was 150). New: extend (5), query (10),
  forms (7), query-serializer-client (3).
- Coverage: 95.47 / 86.57 / 97.84 / 95.47.
- attw: 6 entry points × 4 resolution modes — 24/24 green.
README
- Rewrite to lead with the three v2 wedges: schema inference (response + body),
  type-safe URL params, discriminated errors with safe() Result API.
- Drop v1's "5 differentiators" framing in favor of one-line pitch + 3 features
  no other JS HTTP client matches.
- Add competitive matrix vs ky / ofetch / wretch / got / fetch / axios.
- Cookbook covering: HTTP methods, default singleton, query serializer config,
  retries, timeouts, multi-validator schema interop, streaming (raw / SSE /
  NDJSON / streamed upload), middleware, sub-clients via extend, forms helpers,
  testing via createMockClient, full error taxonomy, per-import-set bundle
  table, runtime compat, migration notes from v1.

CONTRIBUTING
- Update for v2 architecture: functional core, single middleware abstraction,
  shape-frozen RequestContext, per-file entry points for tree-shaking.
- Document the 6 public entry points and the chunks tsdown emits.
- Update prerequisites (Node ≥20.3, native AbortSignal.any).
- Update test category table, coverage thresholds (90/85/95/90), size-limit
  budgets (5 import-sets), CI matrix (TS 5.5/5.8/6.0).
- Add explicit code conventions list including the "errors must be one of the
  five exported classes" rule and the "frozen contexts" middleware rule.

examples/
- 11 numbered runnable files demonstrating every major feature:
   01 — basic GET via default singleton
   02 — schema-inferred response types
   03 — type-safe URL parameters
   04 — schema-validated request bodies (caught locally)
   05 — discriminated errors via client.safe.*
   06 — retries (safe defaults + advanced policy)
   07 — auth middleware + refresh-on-401
   08 — Server-Sent Events via parseSSE
   09 — NDJSON with per-line schema validation
   10 — testing with createMockClient
   11 — client.extend() + form helpers
- examples/README.md indexes them with what-it-shows captions.
- All examples import from `'amu-http'` (the published name) and run via
  Node's package self-reference. First run requires `npm run build`; the
  README documents this.

package.json
- Add per-example npm scripts (`npm run example:get`, `example:schema`, ...)
  plus a `npm run examples` runner that executes the whole sequence.
Method signatures previously had `<Path, S>` as the only generics — calling
`api.get<User[]>('/users')` (the universal axios/ofetch pattern) failed with
TS2344 because `User[]` landed on the slot that wanted a string.

Restructure all four method types (BodylessMethod, BodyMethod, and their
safe variants) to take `<TResponse = unknown, Path, S>`. The return type
becomes:

  [S['response']] extends [Schema] ? InferResponse<S> : TResponse

The non-distributive `[X] extends [Y]` form is required so the conditional
doesn't incorrectly take the schema branch when `S['response']` is the base
`Schema | undefined` (i.e. no schema was passed).

Caveat: TypeScript doesn't perform partial generic inference. When a caller
supplies `<TResponse>` AND a `schema` option, the explicit generic wins and
the schema's inferred type is dropped (runtime validation still runs). This
is documented in the inference type tests. Pass either, not both.

examples/tsconfig.json
- New examples-only tsconfig with `paths` mapping `amu-http` and its subpaths
  to `../src/`. The IDE picks this up so example files don't show false
  errors when the user has the package self-referenced through dist/.
- `rootDir: ".."` + `noEmit: true` + `exclude: []` so the config's own
  examples are included rather than excluded by the inherited base.

Tests
- Add type-test for `api.get<User>(url)` returning User without a schema.
- Add type-test for `api.post<Out>(url, body)` mirror.
- Update existing URL-param type tests to use the new generic positions.
- 177 tests pass (was 175); examples typecheck clean via the new tsconfig.
amu-http/pagination
- New `paginate({ fetch, getItems, getNext })` async iterator over paged
  endpoints. Three preset shapes:
    cursor()         — server-returns-next-cursor (Stripe, Notion, etc.)
    pageToken()      — Google APIs / GCP convention
    parseLinkHeader  — RFC 5988 / GitHub-style header parser (helper, not preset)
- `paginate.pages()` variant yields whole pages (use when you need page-level
  metadata like totals or headers).
- Type tests confirm composition with cursor / pageToken presets.

amu-http/middleware/otel
- OpenTelemetry middleware. Creates a CLIENT span per request with HTTP
  semantic-convention attributes (http.request.method, url.full,
  server.address, url.scheme, url.path, http.response.status_code,
  error.type). Sets span status from response/error class.
- Injects W3C traceparent (and any other configured) headers via the
  registered global propagator. Skip via `propagate: false`.
- @opentelemetry/api is declared as an optional peer dep
  (peerDependenciesMeta) so consumers only install it if they import the
  middleware. tsdown externalizes it from the bundle.

amu-http/middleware/cookies
- Zero-dep cookie jar implementing the useful subset of RFC 6265:
  name/value, Domain, Path, Expires, Max-Age, Secure, HttpOnly, SameSite.
- Domain matching: exact OR proper subdomain (RFC 6265 §5.1.3).
- Path matching: default-path algorithm (§5.1.4) plus prefix matching.
- Secure cookies dropped on plain HTTP. Expired cookies purged on read.
- Reads via Headers.getSetCookie() where available; comma-split fallback for
  older runtimes. Merges with caller-provided Cookie header.
- Caveats vs tough-cookie documented (no PSL awareness, in-memory only).

Tooling
- 3 new entry points under amu-http/{pagination,middleware/otel,middleware/cookies}.
- tsdown config: added new entries + `external: ['@opentelemetry/api']`.
- package.json: peerDependencies + peerDependenciesMeta for @opentelemetry/api.

Tests / coverage
- 24 new tests across pagination (12), otel (4), cookies (10).
- 199 total tests pass; coverage 94.46 / 85.44 / 98.27 / 94.46.
- attw clean across 9 entry points × 4 resolution modes.
- size-limit budgets unchanged (the new entries are separate chunks; they
  add nothing to the core bundle).
…e scripts

- package.json: 2.0.0-alpha.0 → 2.0.0
- package-lock.json: re-synced

CHANGELOG.md
- Comprehensive v2.0 entry covering: breaking changes from v1, every added
  feature (functional core, middleware, schema inference, URL params,
  errors, safe() API, streaming, built-in middleware library, mock client,
  forms, pagination, query serializers, client.extend), tooling/quality
  baselines (199 tests, 94+/85+/98+/94+ coverage, 9 entry points × 4
  resolution modes = 36/36 attw green, 5 size budgets, perf bench).
- Notes that 2.x is managed by Changesets; future PRs add changesets.

scripts/ — restored after the v2 wipe so the runtime-smoke workflow runs
- smoke.ts — Node + Bun smoke. Uses Node package self-reference to import
  amu-http through the published `exports` map. Exercises createClient,
  middleware composition, type-safe URL params, safe() on a 404, and the
  throwing API on a 404.
- smoke-deno.ts — Deno smoke. Imports directly from dist/ (Deno doesn't
  honor Node's self-reference). Same coverage.

bench/request-perf.bench.ts
- Deterministic in-process bench measuring amu's pipeline overhead vs raw
  fetch + Response.json. Synthetic Response, no network. Scenarios:
  raw fetch, amu (no baseURL, no middleware), amu (baseURL only), amu
  (baseURL + 2 user middleware), amu + Zod schema validation.
- Current numbers on local hardware: amu adds ~2.5x over raw fetch's
  microseconds. Schema validation adds ~25% on top. Negligible vs network
  RTT (typical request: 10–1000ms wall clock).

CI: drop Node 18 from the matrix
- v2 requires Node ≥20.3 (native AbortSignal.any). Matrix is now 20.3 / 20 / 22.
- publish.yml and runtime-smoke.yml stay on Node 20 (already satisfies the floor).
…fier matrix

Per-attempt retry hook
- New `RetryConfig.onAttempt(info)` fires before each retry with
  `{ attempt, error, delayMs }`. Async hooks awaited. Closes the telemetry
  gap vs ky's `beforeRetry`.

paginate() + schema integration
- `PaginateOptions.schema?` runs each fetched page through Standard Schema.
  Validation failures throw `AmuValidationError(target='response')`. The
  validated value is what `getItems`/`getNext` see. `paginate.pages` carries
  the same option for parity.

amu-http/middleware/cache (NEW entry point)
- RFC 9111 subset: max-age, Expires, no-store, no-cache.
- RFC 7232 revalidation: ETag/If-None-Match + Last-Modified/If-Modified-Since.
- 304 transparently replays the cached entry; on 200 with new ETag, replaces.
- Pluggable `CacheStore` interface; ships with `createMemoryCacheStore()`
  (LRU eviction at maxEntries).
- Caveats: only GET/HEAD by default, only text/JSON bodies cached, request
  Cache-Control directives honored, Vary: * skipped per spec.
- Custom `keyFor` enables per-user / per-tenant scoping.

Network-error classifier matrix (24 new tests)
- Synthesizes runtime-specific error shapes:
  - undici (Node fetch) — TypeError with cause.code (12 codes covered)
  - Bun fetch — Error with code on the error itself
  - Deno fetch — TypeError without cause / no code → unknown
  - Browser fetch — generic TypeError → unknown
- AbortSignal-based classification asserts precedence over runtime codes.
- isRetryable flag verified per kind (abort + tls non-retryable; others retryable).

Competitor benchmark
- bench/competitors.bench.ts measures amu vs ky / ofetch / redaxios / axios
  on the same in-process workload (deterministic fakeFetch).
- Schema-validation comparison: amu+Zod vs ky+manualZodParse vs ofetch+manualZodParse.
- Devdeps: ky 2.x, ofetch, redaxios, axios — used for bench only.

Migration guides
- docs/migration-from-axios.md — interceptors → middleware, Result API,
  schema inference, FormData / mock client patterns.
- docs/migration-from-ky.md — hooks → middleware, .json() chain elimination,
  HTTPError → AmuError + AmuNetworkError discrimination.
- docs/migration-from-ofetch.md — onRequest/onResponse → single middleware,
  Nuxt/$fetch trade-offs noted.

Browser test environment
- vitest.browser.config.ts using @vitest/browser + Playwright (Chromium).
- npm run test:browser runs the existing tests/browser/ suite in a real
  browser. Off by default.
- .github/workflows/browser.yml — opt-in CI workflow.
- Devdeps installed via --legacy-peer-deps (vitest peer-dep mismatch).

Real-network smoke workflow
- .github/workflows/real-network.yml — manual + weekly schedule. Runs the
  smoke scripts against jsonplaceholder.typicode.com on Node 22, Bun,
  Deno 2.x. Off the default PR matrix to avoid network flake.

API documentation via TypeDoc
- typedoc.json: 10 entry points, expand strategy.
- npm run docs:api generates HTML to docs/api/ (gitignored).

Tooling / version
- @opentelemetry/api added to devdeps for OTel middleware tests.
- package.json version: 2.0.0 → 2.0.0-rc.0 (locks API for RC review).
- size-limit budgets unchanged: core 3.48 KB · with SSE 3.88 KB · full 4.16 KB.
- 241 tests pass (was 199; +42 covering retry/paginate/classifier/cache).
@sshahriazz sshahriazz requested a review from lahin31 April 29, 2026 05:49
sshahriazz and others added 2 commits April 29, 2026 15:59
…hreat model, plugin guide, quality bar)

Closes the gaps identified in the architecture audit. Folds in the
operational concerns the original architecture doc didn't cover.

docs/ARCHITECTURE.md (new) — canonical architecture doc
- Lifecycle model: Client.dispose(), state-sharing semantics, cold-start
  guarantee, time/random injection points
- Concurrency contract: re-entrancy, multi-call next(), cancellation
  propagation, backpressure
- Failure semantics: cleanup contract via try/finally, error normalization,
  partial-execution rules under throw
- Idempotency model: 3 categories (safe/idempotent/unsafe), Idempotency-Key,
  retry budgets
- Public-surface enforcement: TSDoc tags, lint rule, CI snapshot
- Module-graph integrity: dependency-cruiser in CI, layer rule enforcement
- Quality enforcement: type safety, coverage, type tests, performance gates
- Architectural patterns named: Closure-State, Pluggable-Backend,
  Sentinel-Cause, Double-Validation, Order-Aware Onion
- Standards/RFCs table expanded: 9110/9111/9112, 7232, 7233, 7807, 6265bis,
  8288 (replacing obsolete 5988), 8941, 8246, 9211, 6750, 7617, 6749, 9421,
  W3C Trace Context, OTel HTTP semconv, SSE, NDJSON, Streams, Standard Schema
- Compatibility matrix + cadence; explicit non-guarantees ("we don't promise")
- Governance evolution path (single maintainer → committee)
- Request-lifecycle diagram

docs/THREAT_MODEL.md (new) — STRIDE threat model
- Trust boundary diagram (your code | amu | runtime fetch | network)
- Per-category coverage: Spoofing, Tampering, Repudiation, Information
  disclosure, DoS, Elevation of privilege
- What amu defends + what's explicitly out of scope per category
- Vulnerability response process (acknowledgement, severity, embargo, CVE)
- Severity classification (CVSS v3.1 → patch timeline)
- Compromise response runbook
- Security commitments (no telemetry, no eval, no global registration, etc.)

docs/PLUGIN_AUTHORING.md (new) — third-party plugin author guide
- Naming conventions (amu-http-* / @scope/* / @amu-http/*)
- Seven middleware contracts (return next's value; don't mutate ctx; honor
  signal; multi-call safe; clean up in finally; don't retain ctx; throw amu
  errors)
- Six reusable patterns (stateless transform, closure-state, pluggable
  backend, single-flight, outer observer, inner transform)
- Recommended ordering by middleware role
- Configuration discipline; testing patterns (unit, type, concurrency,
  cancellation); compatibility expectations; documentation expectations
- Anti-patterns table (mutating ctx, top-level I/O, ignoring signal, etc.)

docs/QUALITY_BAR.md (new) — bar for @amu-http/* packages
- Required checklist (code, tests, build, docs, release, security, identity)
- Recommended checklist (property tests, fuzz, mutation testing,
  microbench, OTel)
- Application process for @amu-http/* namespace
- Loss-of-status conditions
- Quick checklist reference

docs/adrs/ (new) — 6 backfilled Architecture Decision Records
- 0001 Functional core, no class — context, alternatives (class kept,
  hybrid, inheritance, builder), consequences
- 0002 ESM-only — context, alternatives (dual format, optional CJS, runtime
  shim, parallel v1.x), consequences observed in rc
- 0003 Standard Schema interop — vs coupling to Zod / custom interface /
  per-validator adapters / no-schema
- 0004 User middleware outside built-ins — origin: real refreshOn401 bug
  during dev; alternatives (multi-insertion-points, sort-on-construct)
- 0005 next() may be called multiple times — diverges from Koa convention;
  enables retry/hedging/single-flight as plain middleware
- 0006 Frozen request context — vs mutable / deep-frozen / Immutable.js /
  Reader monad

SECURITY.md — strengthened policy
- Formal SLO table (acknowledgement, assessment, fix, advisory)
- CVSS v3.1 severity classification → patch timeline
- Embargo policy (standard 30d, extended 90d, reporter veto)
- CVE assignment via GitHub Security Advisory (CNA path)
- PGP key placeholder for v2.0 GA
- Hall of Fame section (empty for now)
- Compromise response runbook
- In-scope / out-of-scope explicit
- Bug-bounty stance (none today; recognition + acknowledgement)

CONTRIBUTING.md — links to new docs at the top
@github-advanced-security
Copy link
Copy Markdown

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.


describe('GET — bare (no schema, no extras)', () => {
bench('raw fetch + Response.json()', async () => {
const r = await fakeFetch('https://api.example.com/users/1');

describe('request pipeline overhead vs raw fetch', () => {
bench('raw fetch + Response.json()', async () => {
const res = await fakeFetch('https://api.example.com/users/1');
},
};

const api = createClient({
const api = createClient({ baseURL: 'https://api.example.com' });

// Sub-client: same baseURL, but adds auth.
const authed = api.extend({ middleware: [bearerAuth('static-token')] });
const authed = api.extend({ middleware: [bearerAuth('static-token')] });

// Sub-client: different baseURL, inherits everything else.
const v2 = api.extend({ baseURL: 'https://api.example.com/v2' });
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants