@paykit-sdk/core
Version:
The Payment Toolkit for Typescript
180 lines (173 loc) • 4.93 kB
JavaScript
'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;