UNPKG

@paykit-sdk/core

Version:

The Payment Toolkit for Typescript

180 lines (173 loc) 4.93 kB
'use strict'; var promises = require('timers/promises'); var zod = require('zod'); // src/tools/utils.ts zod.z.record(zod.z.string(), zod.z.string()); // src/tools/utils.ts var OK = (value) => ({ ok: true, value }); var ERR = (error) => ({ ok: false, error }); async function executeWithRetryWithHandler(apiCall, errorHandler, maxRetries = 3, baseDelay = 1e3, currentAttempt = 1) { try { return await apiCall(); } catch (error) { const handledError = errorHandler(error, currentAttempt); if (!handledError.retry) { if (handledError.data == null) throw error; return handledError.data; } if (handledError.retry && currentAttempt <= maxRetries) { const delay = baseDelay * Math.pow(2, currentAttempt - 1) * (0.5 + Math.random() * 0.5); await promises.setTimeout(delay); return executeWithRetryWithHandler( apiCall, errorHandler, maxRetries, baseDelay, currentAttempt + 1 ); } if (handledError.data == null) throw error; return handledError.data; } } function buildError(message, cause) { const error = new Error(message); if (cause) error.cause = cause; return error; } // src/tools/classify-error.ts var STATUS_MAP = { 401: "unauthorized", 403: "forbidden", 404: "not_found", 429: "rate_limit", 500: "internal_server_error", 502: "bad_gateway", 503: "service_unavailable", 504: "gateway_timeout" }; var CONNECTION_CODES = /ECONNREFUSED|ECONNRESET|ENOTFOUND|FETCH FAILED/; var TIMEOUT_CODES = /ETIMEDOUT|ECONNABORTED|TIMEOUT/; function classifyError(err, status) { if (status && STATUS_MAP[status]) return STATUS_MAP[status]; if (err instanceof Error) { const statusPrefix = /^(\d{3}):/.exec(err.message); if (statusPrefix) { const mapped = STATUS_MAP[Number(statusPrefix[1])]; if (mapped) return mapped; } const causeCode = String( err.cause?.code ?? "" ); const haystack = `${err.message} ${causeCode}`.toUpperCase(); if (CONNECTION_CODES.test(haystack)) return "connection"; if (TIMEOUT_CODES.test(haystack)) return "timeout"; } return "unknown"; } // src/http-client.ts var HTTPClient = class { constructor(config) { this.config = config; } errorHandler = (err) => { const errorType = classifyError( err, err?.status ); return ERR(buildError(errorType, err)); }; getFullUrl(endpoint) { const cleanEndpoint = endpoint.startsWith("/") ? endpoint.slice(1) : endpoint; return `${this.config.baseUrl}/${cleanEndpoint}`; } getRequestOptions(options) { return { ...options, headers: { "Content-Type": "application/json", ...this.config.headers, ...options?.headers } }; } retryErrorHandler = (error, attempt) => { const errorType = classifyError( error, error?.status ); const retryableTypes = [ "rate_limit", "connection", "timeout", "internal_server_error", "bad_gateway", "service_unavailable", "gateway_timeout" ]; const shouldRetry = retryableTypes.includes(errorType); if (this.config.retryOptions.debug) { console.info( `[HTTPClient] Attempt ${attempt} failed: ${errorType} - Retry: ${shouldRetry}` ); } return { retry: shouldRetry, data: null }; }; async withRetry(apiCall) { return executeWithRetryWithHandler( apiCall, this.retryErrorHandler, this.config.retryOptions.max, this.config.retryOptions.baseDelay ); } request = async (method, endpoint, options) => { return this.withRetry(async () => { const url = this.getFullUrl(endpoint); const requestOptions = this.getRequestOptions(options); const res = await fetch(url, { method, ...requestOptions }); const text = await res.text(); let data = null; if (text) { try { data = JSON.parse(text); } catch (parseError) { if (res.ok) { return ERR( buildError("Failed to parse response body", parseError) ); } data = text; } } if (!res.ok) { const error = buildError(`${res.status}: ${text}`, data); error.status = res.status; throw error; } return OK(data); }).catch((err) => this.errorHandler(err)); }; get = async (endpoint, options) => { return this.request("GET", endpoint, options); }; post = async (endpoint, options) => { return this.request("POST", endpoint, options); }; delete = async (endpoint, options) => { return this.request("DELETE", endpoint, options); }; put = async (endpoint, options) => { return this.request("PUT", endpoint, options); }; patch = async (endpoint, options) => { return this.request("PATCH", endpoint, options); }; }; exports.HTTPClient = HTTPClient;