first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,105 @@
# `httpRequest` helper
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Universal HTTP helper used by every provider to issue typed, cancellable
JSON requests. The helper is a thin wrapper over an injectable
`FetchLike` (defaulting to `globalThis.fetch`) that JSON-serialises
non-string bodies, applies a per-call timeout via `AbortSignal`,
composes a caller-supplied `signal` with the internal timeout signal,
and converts every failure path into a typed `PaymentProviderError` so
callers can branch on `error.code` instead of parsing strings.
## Features
- Injectable `FetchLike` (`fetch?: FetchLike`) — defaults to
`globalThis.fetch`
- JSON serialisation of object bodies (string bodies pass through
unchanged, enabling `application/x-www-form-urlencoded` flows)
- Per-call `timeoutMs` (default `30_000`) via internal `AbortController`
- External `signal?: AbortSignal` composed with the internal timeout
- Typed errors: `HTTP_ERROR`, `TIMEOUT`, `PARSE_ERROR`
- Empty-body handling: 204 / empty responses resolve to `data: {}`
- `provider?: string` discriminant surfaced on every raised error
- `composeAbortSignal(internal, external)` public helper — reusable by
sibling provider packages that issue out-of-band HTTP calls (OAuth
token fetches, webhook signature fetches) and want the same
caller-signal composition without re-implementing the wiring
## Usage: Basic
```ts
import { httpRequest } from "@suaveplan/payment-providers";
const { status, data } = await httpRequest<{ id: string }>({
method: "POST",
url: "https://api.example.com/charges",
headers: { Authorization: "Bearer secret" },
body: { amount: 1000 },
timeoutMs: 10_000,
provider: "example",
});
```
## Usage: Advanced
```ts
import { type FetchLike, httpRequest } from "@suaveplan/payment-providers";
// Inject a recording FetchLike for tests — no globalThis monkey-patching.
const calls: Array<{ url: string; init?: RequestInit }> = [];
const fetch: FetchLike = async (url, init) => {
calls.push({ url, init });
return new Response(JSON.stringify({ ok: true }), { status: 200 });
};
const external = new AbortController();
const promise = httpRequest({
method: "GET",
url: "https://api.example.com/intent/1",
fetch,
signal: external.signal,
timeoutMs: 5_000,
});
// Cancel externally — either signal firing aborts the request.
external.abort();
await promise.catch((err) => console.log(err.code));
```
## API Reference
| Symbol | Kind | Description |
|---|---|---|
| `FetchLike` | `type` | `(input: string, init?: RequestInit) => Promise<Response>` |
| `httpRequest<T>(options: HttpOptions): Promise<ProviderResponse<T>>` | `function` | issues the HTTP call, returns parsed JSON |
| `composeAbortSignal(internal, external?): AbortSignal` | `function` | returns `internal` when `external` is undefined; otherwise returns a fresh signal that aborts when either input aborts, with already-aborted external propagating synchronously |
### `HttpOptions`
| Field | Type | Default | Description |
|---|---|---|---|
| `method` | `"GET" \| "POST" \| "DELETE" \| "PATCH"` | — | HTTP verb |
| `url` | `string` | — | request URL |
| `headers` | `Record<string, string>` | `{}` | extra headers (override the JSON defaults if set) |
| `body` | `unknown` | — | JSON-serialised unless already a `string` |
| `timeoutMs` | `number` | `30_000` | per-call timeout |
| `fetch` | `FetchLike` | `globalThis.fetch` | injectable HTTP client |
| `signal` | `AbortSignal` | — | external cancellation |
| `provider` | `string` | `"unknown"` | discriminant placed on raised errors |
## Implementation Notes
The internal `AbortController` always wins on timeout — its `abort()` is
called from a `setTimeout` whose handle is cleared in `finally`, so no
listener leaks past the call. The composed `AbortSignal` is created
inline via a third `AbortController`; both source signals
(`timeoutController.signal` and the caller's `signal`) propagate to it
via `addEventListener("abort", …, { once: true })`, plus an upfront
check for the already-aborted case. Non-`Error` throws from the
injected `FetchLike` (e.g. raw strings) become `HTTP_ERROR` with a
stringified body excerpt — the helper never lets an untyped failure
escape.