splitwise
Version:
A TypeScript SDK for the Splitwise API.
144 lines • 5.61 kB
TypeScript
/**
* HTTP client for the Splitwise API.
*
* Wraps `fetch` with:
* - bearer-token auth (token is fetched per request via `getAccessToken`)
* - automatic snake_case <-> camelCase conversion at the boundary
* - form-urlencoded request bodies (the Splitwise API's default) with an
* opt-in JSON path for new endpoints
* - typed error responses via `createApiError`
* - transparent retries with exponential backoff
* - request timeouts via AbortController
*/
import type { LogLevel, Logger } from './types.js';
export interface HttpClientConfig {
baseUrl: string;
getAccessToken: () => Promise<string>;
fetch?: typeof fetch;
/** Per-request timeout in ms. Default 30000. */
timeout?: number;
/** Maximum retries for transient failures. Default 2. */
maxRetries?: number;
logger?: Logger;
/** Default 'none'. */
logLevel?: LogLevel;
/** Optional User-Agent string. */
userAgent?: string;
/** Lifecycle hooks; see `Hooks` interface for the contract. */
hooks?: Hooks;
}
/**
* Lifecycle hooks for observability and side-effects. Inspired by Stripe's
* event emitter (`stripe.on('request', cb)`), but expressed as a plain
* options bag so consumers don't need to import an event-emitter API.
*
* Hooks are called synchronously (not awaited). Returning a Promise has no
* effect on the request flow -- if a hook needs to do async work, it should
* fire-and-forget, and any thrown error is caught and ignored so that hook
* misbehavior doesn't break SDK calls.
*/
export interface Hooks {
/** Called before each HTTP request leaves the client. */
onRequest?: (event: RequestEvent) => void;
/**
* Called whenever the SDK receives an HTTP response, regardless of status.
* Fires for 2xx success, 4xx/5xx errors, and 200 responses with embedded
* errors -- i.e. any time the network actually returned something. Pair
* with `onError` if you want a separate signal for failures.
*/
onResponse?: (event: ResponseEvent) => void;
/**
* Called for every error that the SDK is about to throw. Includes both
* transport failures (connection, timeout, abort) and API errors. Fires
* once per attempt, so retried requests fire the hook multiple times.
*/
onError?: (event: ErrorEvent) => void;
}
export interface RequestEvent {
method: string;
url: string;
/** The Authorization header is replaced with "Bearer [REDACTED]" for safety. */
headers: Record<string, string>;
/** 1-indexed; >1 indicates a retry. */
attempt: number;
}
export interface ResponseEvent {
method: string;
url: string;
status: number;
/** Response headers (lowercased keys). */
headers: Record<string, string>;
/** Wall-clock ms from request dispatch to response received. */
durationMs: number;
attempt: number;
}
export interface ErrorEvent {
method: string;
url: string;
error: unknown;
durationMs: number;
attempt: number;
}
/**
* Per-request overrides exposed publicly to consumers of the SDK. The same
* options bag is accepted by every resource method (as the second argument)
* and by `sw.rawRequest()`. Internal-only options live on `RequestOptions`.
*/
export interface RequestOverrides {
/** Cancel the request via an AbortSignal. */
signal?: AbortSignal;
/** Override the client's per-request timeout (ms) for this call. */
timeout?: number;
/** Override the client's `maxRetries` for this call (0 disables retry). */
maxRetries?: number;
/** Override the base URL for this call (rare; useful for testing). */
baseUrl?: string;
}
export interface RequestOptions extends RequestOverrides {
/** Query string parameters. Always sent in the URL regardless of method. */
query?: Record<string, unknown>;
/** Body, used for POST/PUT/DELETE. */
body?: Record<string, unknown>;
/**
* If true (default), serialize body as form-urlencoded with flattenParams
* (the Splitwise convention). If false, send as JSON with snake_case keys.
*/
formEncoded?: boolean;
/**
* Property to extract from the parsed response, e.g. 'expenses' to get the
* value of `{ expenses: [...] }`. If undefined, returns the full response.
*/
unwrapKey?: string;
/**
* If true, skip the SplitwiseConstraintError throw for 200 responses with
* `success:false` or non-empty `errors`. Used by endpoints (like
* /parse_sentence) where these fields are normal response data rather
* than failure signals.
*/
bypassEmbeddedErrors?: boolean;
}
export declare class HttpClient {
private readonly baseUrl;
private readonly getAccessToken;
private readonly fetchImpl;
private readonly timeout;
private readonly maxRetries;
private readonly logger;
private readonly userAgent;
private readonly hooks;
constructor(config: HttpClientConfig);
get<T>(path: string, options?: Omit<RequestOptions, 'body' | 'formEncoded'>): Promise<T>;
post<T>(path: string, options?: RequestOptions): Promise<T>;
put<T>(path: string, options?: RequestOptions): Promise<T>;
delete<T>(path: string, options?: RequestOptions): Promise<T>;
private request;
private requestOnce;
/**
* Calls a hook if registered. Wraps the event-builder in a function so we
* skip the work entirely when no hook is registered. Catches synchronous
* throws so misbehaving user code doesn't break the request.
*/
private fireHook;
private handleResponse;
}
//# sourceMappingURL=http.d.ts.map