first commit
This commit is contained in:
105
docs/money/payment/payment-providers/src/http/http.md
Normal file
105
docs/money/payment/payment-providers/src/http/http.md
Normal 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.
|
||||
Reference in New Issue
Block a user