feat!: v2.0.0-rc.0 — schema-inferred, middleware-composable, universal#1
Open
sshahriazz wants to merge 10 commits into
Open
feat!: v2.0.0-rc.0 — schema-inferred, middleware-composable, universal#1sshahriazz wants to merge 10 commits into
sshahriazz wants to merge 10 commits into
Conversation
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).
…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
|
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:
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' }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 acrosssrc/,tests/,docs/,examples/,bench/,scripts/, and CI workflows.Changes
Three wedges nobody else has
client.get(url, { schema: { response: User } })returnsz.infer<typeof User>with no<T>annotation.schema.bodyvalidates outgoing payloads pre-send.client.get('/users/:id/posts/:postId', { params: { id, postId } })enforces every placeholder at compile time. Segment-walker parser handles absolute URLs and avoids thehttps:colon trap. Recursion-capped at 24 segments.AmuError,AmuNetworkErrorw/ 8kindvalues,AmuUrlError,AmuValidationError,AmuUnknownError).client.safe.*returnsResult<T>for exhaustiveswitchnarrowing.Core + supporting feature surface
createClient(config)returns a frozenClient; defaultamusingleton kept for one-line absolute-URL calls.next()may be called multiple times so retry can re-enter.withHeader/withMeta/withSignalhelpers.client.stream()returnsReadableStream<Uint8Array>; tree-shakableparseSSEandparseNDJSON(with optional per-line schema).safe()Result API —client.safe.{get,post,...}returnsResult<T> = { ok: true; data } | { ok: false; error }.client.extend(overrides)— sub-clients with merged config + middleware.'flat'(default) |'qs'| custom function.RetryConfig.onAttempt({ attempt, error, delayMs })closes theky.beforeRetrygap.Built-in middleware library (per-file, tree-shakable)
amu-http/middleware/authbearerAuth,basicAuth,refreshOn401(concurrent-refresh dedupe)amu-http/middleware/requestIdrequestIdamu-http/middleware/loggerlogger(dev request/response, redacts Authorization in verbose)amu-http/middleware/otelotel(W3C traceparent + spans, peer dep on@opentelemetry/api)amu-http/middleware/cookiescookies,createCookieJar(zero-dep RFC 6265 subset)amu-http/middleware/cachecache,createMemoryCacheStore(RFC 9111 subset, ETag/Last-Modified revalidation)Helpers + test infrastructure
amu-http/forms—formData,urlEncodedtyped buildersamu-http/pagination—paginate({ schema })async iterator withcursor/pageTokenpresets andparseLinkHeaderhelperamu-http/test—createMockClient()typed mock with URL template matching, request recording,.assertCalled/.assertNotCalledTests + quality
bench/competitors.bench.ts) — amu vs ky 2.x / ofetch / redaxios / axios on the same in-process workloadTooling / build
AbortSignal.any){ createClient }) — 3.48 KB gzipparseSSE— 3.88 KBparseSSE+parseNDJSON— 4.15 KBCI workflows
ci.ymlci.yml(typecheck-matrix)tsc --noEmitagainst TypeScript 5.5 / 5.8 / 6.0runtime-smoke.ymlreal-network.yml(NEW)browser.yml(NEW)@vitest/browsercodeql.ymlpublish.ymlDocumentation
docs/: fromaxios, fromky, fromofetchdocs/api/(npm run docs:api)Checklist
npm run lint && npm run typecheck && npm testlocally and everything passes (241/241 tests, 95+ coverage)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 graduate2.0.0-alpha.0 → 2.0.0cleanly. Future minor/patch from 2.0.x onward will use changesets normallynpm run size(the gate also runs in CI). All 5 budgets greentests/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:new Amu(...)/class AmucreateClient(...)(no class)amu.get<T>(url)typing<T>still works orschema: { response: T }(inferred)params(query string)params(URL:idsubstitutions) andquery(?key=value){ raw: true }client.stream()for rawResponseAmuError+AmuNetworkError(4 kinds)AmuNetworkError.kindvaluessafe()Result APIstream/parseSSE/parseNDJSON)createMockClient>=18to>=20.3(nativeAbortSignal.any)No automated codemod ships. Pin to
1.xif you can't migrate immediately.Why RC, not GA
Per the audit before this PR, three things made
2.0.0GA premature:ky/ofetch/axiosis real)2.0.0-rc.0locks 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?