UNPKG

splitwise

Version:

A TypeScript SDK for the Splitwise API.

144 lines 5.61 kB
/** * 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