zodsei
Version:
Contract-first type-safe HTTP client with Zod validation
1,172 lines (1,156 loc) • 34.1 kB
JavaScript
var __defProp = Object.defineProperty;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __esm = (fn, res) => function __init() {
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
// src/errors.ts
var ZodseiError, ValidationError, HttpError, NetworkError, ConfigError, TimeoutError;
var init_errors = __esm({
"src/errors.ts"() {
"use strict";
ZodseiError = class extends Error {
constructor(message, code) {
super(message);
this.code = code;
this.name = "ZodseiError";
}
};
ValidationError = class _ValidationError extends ZodseiError {
constructor(message, issues, type = "request") {
super(message, "VALIDATION_ERROR");
this.issues = issues;
this.type = type;
this.name = "ValidationError";
}
static fromZodError(error, type = "request") {
const message = `${type} validation failed: ${error.issues.map(
(issue) => `${issue.path.join(".")}: ${issue.message}`
).join(", ")}`;
return new _ValidationError(message, error.issues, type);
}
};
HttpError = class _HttpError extends ZodseiError {
constructor(message, status, statusText, response) {
super(message, "HTTP_ERROR");
this.status = status;
this.statusText = statusText;
this.response = response;
this.name = "HttpError";
}
static fromResponse(response, data) {
const message = `HTTP ${response.status}: ${response.statusText}`;
return new _HttpError(message, response.status, response.statusText, data);
}
};
NetworkError = class extends ZodseiError {
constructor(message, originalError) {
super(message, "NETWORK_ERROR");
this.originalError = originalError;
this.name = "NetworkError";
}
};
ConfigError = class extends ZodseiError {
constructor(message) {
super(message, "CONFIG_ERROR");
this.name = "ConfigError";
}
};
TimeoutError = class extends ZodseiError {
constructor(timeout) {
super(`Request timeout after ${timeout}ms`, "TIMEOUT_ERROR");
this.name = "TimeoutError";
}
};
}
});
// src/adapters/fetch.ts
var fetch_exports = {};
__export(fetch_exports, {
FetchAdapter: () => FetchAdapter
});
var FetchAdapter;
var init_fetch = __esm({
"src/adapters/fetch.ts"() {
"use strict";
init_errors();
FetchAdapter = class {
constructor(config = {}) {
this.name = "fetch";
this.config = {
timeout: 3e4,
...config
};
}
async request(context) {
try {
const init = this.createRequestInit(context);
const response = await fetch(context.url, init);
const data = await this.parseResponseData(response);
const responseHeaders = {};
response.headers.forEach((value, key) => {
responseHeaders[key] = value;
});
const responseContext = {
status: response.status,
statusText: response.statusText,
headers: responseHeaders,
data
};
if (!response.ok) {
throw HttpError.fromResponse(response, data);
}
return responseContext;
} catch (error) {
if (error instanceof HttpError) {
throw error;
}
if (error instanceof Error) {
if (error.name === "AbortError") {
throw new TimeoutError(this.config.timeout || 0);
}
throw new NetworkError(`Fetch request failed: ${error.message}`, error);
}
throw new NetworkError("Unknown fetch error", error);
}
}
createRequestInit(context) {
const init = {
method: context.method.toUpperCase(),
headers: {
"Content-Type": "application/json",
...context.headers
},
credentials: this.config.credentials,
mode: this.config.mode,
cache: this.config.cache,
redirect: this.config.redirect,
referrer: this.config.referrer,
referrerPolicy: this.config.referrerPolicy,
integrity: this.config.integrity
};
if (context.body !== void 0 && !["GET", "HEAD"].includes(context.method.toUpperCase())) {
if (typeof context.body === "object") {
init.body = JSON.stringify(context.body);
} else {
init.body = context.body;
}
}
if (this.config.timeout && this.config.timeout > 0) {
const controller = new AbortController();
setTimeout(() => controller.abort(), this.config.timeout);
init.signal = controller.signal;
}
return init;
}
async parseResponseData(response) {
const contentType = response.headers.get("content-type");
if (contentType?.includes("application/json")) {
return response.json();
} else if (contentType?.includes("text/")) {
return response.text();
} else if (contentType?.includes("application/octet-stream") || contentType?.includes("application/pdf")) {
return response.blob();
} else {
try {
return await response.json();
} catch {
return response.text();
}
}
}
};
}
});
// src/adapters/axios.ts
var axios_exports = {};
__export(axios_exports, {
AxiosAdapter: () => AxiosAdapter
});
var AxiosAdapter;
var init_axios = __esm({
"src/adapters/axios.ts"() {
"use strict";
init_errors();
AxiosAdapter = class {
constructor(config = {}) {
this.name = "axios";
this.config = {
timeout: 3e4,
validateStatus: () => true,
// We handle status validation ourselves
...config
};
}
async getAxios() {
if (!this.axios) {
try {
const axiosModule = await import("axios");
this.axios = axiosModule.default || axiosModule;
} catch (_error) {
throw new Error("axios is not installed. Please install it with: npm install axios");
}
}
return this.axios;
}
// Interceptors are not supported. Use middleware in the client instead.
async request(context) {
try {
const axios = await this.getAxios();
const axiosConfig = this.createAxiosConfig(context);
const response = await axios.request(axiosConfig);
const responseContext = {
status: response.status,
statusText: response.statusText,
headers: response.headers || {},
data: response.data
};
if (response.status >= 400) {
throw new HttpError(
`HTTP ${response.status}: ${response.statusText}`,
response.status,
response.statusText,
response.data
);
}
return responseContext;
} catch (error) {
if (error instanceof HttpError) {
throw error;
}
if (error.isAxiosError) {
if (error.code === "ECONNABORTED" || error.code === "ETIMEDOUT") {
throw new TimeoutError(this.config.timeout || 0);
}
if (error.response) {
throw new HttpError(
`HTTP ${error.response.status}: ${error.response.statusText}`,
error.response.status,
error.response.statusText,
error.response.data
);
} else if (error.request) {
throw new NetworkError(`Network request failed: ${error.message}`, error);
}
}
throw new NetworkError(`Axios request failed: ${error.message}`, error);
}
}
createAxiosConfig(context) {
const config = {
url: context.url,
method: context.method.toLowerCase(),
headers: {
"Content-Type": "application/json",
...context.headers
},
timeout: this.config.timeout,
maxRedirects: this.config.maxRedirects,
validateStatus: this.config.validateStatus,
maxContentLength: this.config.maxContentLength,
maxBodyLength: this.config.maxBodyLength,
withCredentials: this.config.withCredentials,
auth: this.config.auth,
proxy: this.config.proxy
};
if (context.body !== void 0 && !["GET", "HEAD"].includes(context.method.toUpperCase())) {
config.data = context.body;
}
if (context.query && Object.keys(context.query).length > 0) {
config.params = context.query;
}
return config;
}
};
}
});
// src/adapters/ky.ts
var ky_exports = {};
__export(ky_exports, {
KyAdapter: () => KyAdapter
});
var KyAdapter;
var init_ky = __esm({
"src/adapters/ky.ts"() {
"use strict";
init_errors();
KyAdapter = class {
constructor(config = {}) {
this.name = "ky";
this.config = {
timeout: 3e4,
throwHttpErrors: false,
// We handle errors ourselves
...config
};
}
async getKy() {
if (!this.ky) {
try {
const kyModule = await import("ky");
this.ky = kyModule.default || kyModule;
} catch (_error) {
throw new Error("ky is not installed. Please install it with: npm install ky");
}
}
return this.ky;
}
async request(context) {
try {
const ky = await this.getKy();
const kyOptions = this.createKyOptions(context);
const response = await ky(context.url, kyOptions);
const data = await this.parseResponseData(response);
const responseHeaders = {};
response.headers.forEach((value, key) => {
responseHeaders[key] = value;
});
const responseContext = {
status: response.status,
statusText: response.statusText,
headers: responseHeaders,
data
};
if (!response.ok) {
throw new HttpError(
`HTTP ${response.status}: ${response.statusText}`,
response.status,
response.statusText,
data
);
}
return responseContext;
} catch (error) {
if (error instanceof HttpError) {
throw error;
}
if (error.name === "HTTPError") {
const response = error.response;
const data = await this.parseResponseData(response).catch(() => null);
throw new HttpError(
`HTTP ${response.status}: ${response.statusText}`,
response.status,
response.statusText,
data
);
}
if (error.name === "TimeoutError") {
throw new TimeoutError(this.config.timeout || 0);
}
throw new NetworkError(`Ky request failed: ${error.message}`, error);
}
}
createKyOptions(context) {
const options = {
method: context.method.toLowerCase(),
headers: {
"Content-Type": "application/json",
...context.headers
},
timeout: this.config.timeout,
retry: this.config.retry,
throwHttpErrors: this.config.throwHttpErrors,
credentials: this.config.credentials,
mode: this.config.mode,
cache: this.config.cache,
redirect: this.config.redirect,
referrer: this.config.referrer,
referrerPolicy: this.config.referrerPolicy,
integrity: this.config.integrity
};
if (context.body !== void 0 && !["GET", "HEAD"].includes(context.method.toUpperCase())) {
if (typeof context.body === "object") {
options.json = context.body;
} else {
options.body = context.body;
}
}
if (context.query && Object.keys(context.query).length > 0) {
options.searchParams = context.query;
}
return options;
}
async parseResponseData(response) {
const contentType = response.headers.get("content-type");
if (contentType?.includes("application/json")) {
return response.json();
} else if (contentType?.includes("text/")) {
return response.text();
} else if (contentType?.includes("application/octet-stream") || contentType?.includes("application/pdf")) {
return response.blob();
} else {
try {
return await response.json();
} catch {
return response.text();
}
}
}
};
}
});
// src/validation.ts
init_errors();
import { z } from "zod";
function validateRequest(schema, data) {
if (!schema) {
return data;
}
try {
return schema.parse(data);
} catch (error) {
if (error instanceof z.ZodError) {
throw ValidationError.fromZodError(error, "request");
}
throw error;
}
}
function validateResponse(schema, data) {
if (!schema) {
return data;
}
try {
return schema.parse(data);
} catch (error) {
if (error instanceof z.ZodError) {
throw ValidationError.fromZodError(error, "response");
}
throw error;
}
}
function safeParseRequest(schema, data) {
if (!schema) {
return { success: true, data };
}
try {
const result = schema.parse(data);
return { success: true, data: result };
} catch (error) {
if (error instanceof z.ZodError) {
return { success: false, error: ValidationError.fromZodError(error, "request") };
}
return {
success: false,
error: new ValidationError("Unknown validation error", [], "request")
};
}
}
function safeParseResponse(schema, data) {
if (!schema) {
return { success: true, data };
}
try {
const result = schema.parse(data);
return { success: true, data: result };
} catch (error) {
if (error instanceof z.ZodError) {
return { success: false, error: ValidationError.fromZodError(error, "response") };
}
return {
success: false,
error: new ValidationError("Unknown validation error", [], "response")
};
}
}
function createValidator(schema, enabled) {
return {
validateRequest: enabled ? (data) => validateRequest(schema, data) : (data) => data,
validateResponse: enabled ? (data) => validateResponse(schema, data) : (data) => data,
safeParseRequest: (data) => safeParseRequest(schema, data),
safeParseResponse: (data) => safeParseResponse(schema, data)
};
}
// src/utils/path.ts
function extractPathParamNames(path) {
const matches = path.match(/:([^/]+)/g);
return matches ? matches.map((match) => match.slice(1)) : [];
}
function replacePath(path, params) {
let result = path;
for (const [key, value] of Object.entries(params)) {
result = result.replace(`:${key}`, encodeURIComponent(value));
}
return result;
}
function buildQueryString(params) {
const searchParams = new URLSearchParams();
for (const [key, value] of Object.entries(params)) {
if (value !== void 0 && value !== null) {
if (Array.isArray(value)) {
value.forEach((item) => searchParams.append(key, String(item)));
} else {
searchParams.append(key, String(value));
}
}
}
const queryString = searchParams.toString();
return queryString ? `?${queryString}` : "";
}
function buildUrl(baseUrl, path, query) {
const cleanBaseUrl = baseUrl.replace(/\/$/, "");
const cleanPath = path.startsWith("/") ? path : `/${path}`;
const queryString = query ? buildQueryString(query) : "";
return `${cleanBaseUrl}${cleanPath}${queryString}`;
}
function separateParams(path, data) {
const pathParamNames = extractPathParamNames(path);
const pathParams = {};
const queryParams = {};
if (!data) {
return { pathParams, queryParams };
}
for (const [key, value] of Object.entries(data)) {
if (pathParamNames.includes(key)) {
pathParams[key] = String(value);
} else {
queryParams[key] = value;
}
}
return { pathParams, queryParams };
}
function shouldHaveBody(method) {
return !["GET", "HEAD", "DELETE"].includes(method.toUpperCase());
}
// src/middleware/index.ts
var MiddlewareExecutor = class {
constructor(middleware = []) {
this.middleware = middleware;
}
// Execute middleware chain
async execute(request, finalHandler) {
if (this.middleware.length === 0) {
return finalHandler(request);
}
let index = 0;
const next = async (req) => {
if (index >= this.middleware.length) {
return finalHandler(req);
}
const middleware = this.middleware[index++];
return middleware(req, next);
};
return next(request);
}
// Add middleware
use(middleware) {
this.middleware.push(middleware);
}
// Get middleware list
getMiddleware() {
return [...this.middleware];
}
};
function createMiddlewareExecutor(middleware = []) {
return new MiddlewareExecutor(middleware);
}
function composeMiddleware(...middleware) {
return async (request, next) => {
const executor = new MiddlewareExecutor(middleware);
return executor.execute(request, next);
};
}
// src/adapters/index.ts
async function createAdapter(type, config) {
switch (type) {
case "fetch": {
const { FetchAdapter: FetchAdapter2 } = await Promise.resolve().then(() => (init_fetch(), fetch_exports));
return new FetchAdapter2(config);
}
case "axios": {
const { AxiosAdapter: AxiosAdapter2 } = await Promise.resolve().then(() => (init_axios(), axios_exports));
return new AxiosAdapter2(config);
}
case "ky": {
const { KyAdapter: KyAdapter2 } = await Promise.resolve().then(() => (init_ky(), ky_exports));
return new KyAdapter2(config);
}
default:
throw new Error(`Unsupported adapter type: ${type}`);
}
}
async function isAdapterAvailable(type) {
try {
switch (type) {
case "fetch":
return typeof fetch !== "undefined";
case "axios":
await import("axios");
return true;
case "ky":
await import("ky");
return true;
default:
return false;
}
} catch {
return false;
}
}
async function getDefaultAdapter(config) {
if (await isAdapterAvailable("fetch")) {
return createAdapter("fetch", config);
}
if (await isAdapterAvailable("axios")) {
return createAdapter("axios", config);
}
if (await isAdapterAvailable("ky")) {
return createAdapter("ky", config);
}
throw new Error(
"No HTTP adapter available. Please install axios or ky, or ensure fetch is available."
);
}
// src/schema.ts
import { z as z2 } from "zod";
var SchemaExtractor = class _SchemaExtractor {
constructor(contract) {
this.contract = contract;
}
/**
* Get endpoint definition by path
*/
getEndpoint(path) {
const endpoint = this.contract[path];
if (this.isEndpointDefinition(endpoint)) {
return endpoint;
}
throw new Error(`Endpoint "${String(path)}" not found or is not a valid endpoint`);
}
/**
* Get nested contract by path
*/
getNested(path) {
const nested = this.contract[path];
if (this.isNestedContract(nested)) {
return new _SchemaExtractor(nested);
}
throw new Error(`Nested contract "${String(path)}" not found or is not a valid contract`);
}
/**
* Get request schema for an endpoint
*/
getRequestSchema(path) {
const endpoint = this.getEndpoint(path);
return endpoint.request;
}
/**
* Get response schema for an endpoint
*/
getResponseSchema(path) {
const endpoint = this.getEndpoint(path);
return endpoint.response;
}
/**
* Get all schemas for an endpoint
*/
getEndpointSchemas(path) {
const endpoint = this.getEndpoint(path);
const result = {
request: endpoint.request,
response: endpoint.response,
endpoint
};
return result;
}
/**
* Get all endpoint paths in the contract
*/
getEndpointPaths() {
return Object.keys(this.contract).filter(
(key) => this.isEndpointDefinition(this.contract[key])
);
}
/**
* Get all nested contract paths
*/
getNestedPaths() {
return Object.keys(this.contract).filter(
(key) => this.isNestedContract(this.contract[key])
);
}
/**
* Generate OpenAPI-like schema description
*/
describeEndpoint(path) {
const endpoint = this.getEndpoint(path);
const result = {
path: endpoint.path,
method: endpoint.method,
requestSchema: endpoint.request,
responseSchema: endpoint.response,
requestType: endpoint.request ? this.getSchemaDescription(endpoint.request) : "void",
responseType: endpoint.response ? this.getSchemaDescription(endpoint.response) : "unknown"
};
return result;
}
/**
* Generate schema description for documentation
*/
getSchemaDescription(schema) {
if (!schema) {
return "undefined";
}
try {
if (schema instanceof z2.ZodObject) {
const shape = schema.shape;
const fields = Object.keys(shape).map((key) => {
const field = shape[key];
return `${key}: ${this.getZodTypeDescription(field)}`;
});
return `{ ${fields.join(", ")} }`;
}
return this.getZodTypeDescription(schema);
} catch {
return "unknown";
}
}
/**
* Get basic Zod type description
*/
getZodTypeDescription(schema) {
const def = schema._def;
if (def?.typeName) {
switch (def.typeName) {
case "ZodString":
return "string";
case "ZodNumber":
return "number";
case "ZodBoolean":
return "boolean";
case "ZodArray":
return def.type ? `${this.getZodTypeDescription(def.type)}[]` : "array";
case "ZodOptional":
return def.innerType ? `${this.getZodTypeDescription(def.innerType)}?` : "optional";
case "ZodNullable":
return def.innerType ? `${this.getZodTypeDescription(def.innerType)} | null` : "nullable";
case "ZodObject":
return "object";
case "ZodUnion":
return "union";
case "ZodLiteral":
return `literal(${JSON.stringify(def.value)})`;
case "ZodEnum":
return "enum";
default:
return def.typeName.replace("Zod", "").toLowerCase();
}
}
try {
if (schema instanceof z2.ZodString) return "string";
if (schema instanceof z2.ZodNumber) return "number";
if (schema instanceof z2.ZodBoolean) return "boolean";
if (schema instanceof z2.ZodArray) {
const element = schema.element;
return element ? `${this.getZodTypeDescription(element)}[]` : "array";
}
if (schema instanceof z2.ZodOptional) {
const inner = schema.unwrap();
return inner ? `${this.getZodTypeDescription(inner)}?` : "optional";
}
if (schema instanceof z2.ZodNullable) {
const inner = schema.unwrap();
return inner ? `${this.getZodTypeDescription(inner)} | null` : "nullable";
}
if (schema instanceof z2.ZodObject) return "object";
} catch {
}
return "unknown";
}
/**
* Check if a value is an endpoint definition
*/
isEndpointDefinition(value) {
return Boolean(value) && typeof value === "object" && value !== null && "path" in value && "method" in value;
}
/**
* Check if a value is a nested contract
*/
isNestedContract(value) {
return Boolean(value) && typeof value === "object" && value !== null && !this.isEndpointDefinition(value);
}
};
function createSchemaExtractor(contract) {
return new SchemaExtractor(contract);
}
function extractTypeInfo(endpoint) {
return {
requestSchema: endpoint.request,
responseSchema: endpoint.response,
method: endpoint.method,
path: endpoint.path,
hasRequestSchema: Boolean(endpoint.request),
hasResponseSchema: Boolean(endpoint.response)
};
}
// src/client.ts
var ZodseiClient = class {
constructor(contract, config) {
this.adapter = null;
this.contract = contract;
this.config = this.normalizeConfig(config);
this.middlewareExecutor = createMiddlewareExecutor(this.config.middleware);
this.$schema = createSchemaExtractor(contract);
return new Proxy(this, {
get: (target, prop) => {
if (typeof prop === "string") {
if (prop in this.contract && this.isEndpointDefinition(this.contract[prop])) {
return this.createEndpointMethod(prop);
}
if (prop in this.contract && this.isNestedContract(this.contract[prop])) {
return this.createNestedClient(this.contract[prop]);
}
}
return target[prop];
}
});
}
/**
* Normalize configuration
*/
normalizeConfig(config) {
return {
baseUrl: config.baseUrl.replace(/\/$/, ""),
validateRequest: config.validateRequest ?? true,
validateResponse: config.validateResponse ?? true,
headers: config.headers ?? {},
timeout: config.timeout ?? 3e4,
retries: config.retries ?? 0,
middleware: config.middleware ?? [],
adapter: config.adapter ?? "fetch",
adapterConfig: config.adapterConfig ?? {}
};
}
/**
* Check if a value is an endpoint definition
*/
isEndpointDefinition(value) {
return value && typeof value === "object" && "path" in value && "method" in value;
}
/**
* Check if a value is a nested contract
*/
isNestedContract(value) {
return value && typeof value === "object" && !this.isEndpointDefinition(value);
}
/**
* Create nested client for sub-contracts
*/
createNestedClient(nestedContract) {
return new Proxy(
{},
{
get: (_target, prop) => {
if (typeof prop === "string") {
if (prop in nestedContract && this.isEndpointDefinition(nestedContract[prop])) {
return this.createEndpointMethod(
`${prop}`,
nestedContract[prop]
);
}
if (prop in nestedContract && this.isNestedContract(nestedContract[prop])) {
return this.createNestedClient(nestedContract[prop]);
}
}
return void 0;
}
}
);
}
/**
* Create endpoint method with schema access
*/
createEndpointMethod(endpointName, endpoint) {
const targetEndpoint = endpoint || this.contract[endpointName];
const method = async (...args) => {
const data = targetEndpoint.request ? args[0] : void 0;
return this.executeEndpoint(targetEndpoint, data);
};
method.schema = {
request: targetEndpoint.request,
response: targetEndpoint.response,
endpoint: targetEndpoint
};
method.infer = {
request: targetEndpoint.request ? {} : void 0,
response: targetEndpoint.response ? {} : {}
};
return method;
}
/**
* Execute endpoint request
*/
async executeEndpoint(endpoint, data) {
const validatedData = this.config.validateRequest ? validateRequest(endpoint.request, data) : data;
const requestContext = this.buildRequestContext(endpoint, validatedData);
const response = await this.middlewareExecutor.execute(
requestContext,
(ctx) => this.executeHttpRequest(ctx)
);
const validatedResponse = this.config.validateResponse ? validateResponse(endpoint.response, response.data) : response.data;
return validatedResponse;
}
/**
* Build request context
*/
buildRequestContext(endpoint, data) {
const { path, method } = endpoint;
const { pathParams, queryParams } = separateParams(path, data);
const finalPath = replacePath(path, pathParams);
const url = method.toLowerCase() === "get" ? buildUrl(this.config.baseUrl, finalPath, queryParams) : buildUrl(this.config.baseUrl, finalPath);
const body = shouldHaveBody(method) ? method.toLowerCase() === "get" ? void 0 : data : void 0;
return {
url,
method,
headers: { ...this.config.headers },
body,
params: pathParams,
query: method.toLowerCase() === "get" ? queryParams : void 0
};
}
/**
* Get adapter
*/
async getAdapter() {
if (!this.adapter) {
const adapterConfig = {
timeout: this.config.timeout,
...this.config.adapterConfig
};
if (typeof this.config.adapter === "string") {
this.adapter = await createAdapter(this.config.adapter, adapterConfig);
} else {
this.adapter = await createAdapter("fetch", adapterConfig);
}
}
return this.adapter;
}
/**
* Execute HTTP request
*/
async executeHttpRequest(context) {
const adapter = await this.getAdapter();
return adapter.request(context);
}
/**
* Get configuration
*/
getConfig() {
return { ...this.config };
}
/**
* Get contract
*/
getContract() {
return { ...this.contract };
}
/**
* Add middleware
*/
use(middleware) {
this.middlewareExecutor.use(middleware);
}
};
function createClient(contract, config) {
return new ZodseiClient(contract, config);
}
// src/types.ts
function defineContract(contract) {
return contract;
}
// src/index.ts
init_errors();
// src/middleware/retry.ts
init_errors();
function defaultRetryCondition(error) {
if (error instanceof HttpError) {
return error.status >= 500 || error.status === 408 || error.status === 429;
}
return true;
}
function calculateDelay(attempt, baseDelay, backoff) {
switch (backoff) {
case "exponential":
return baseDelay * Math.pow(2, attempt);
case "linear":
default:
return baseDelay * (attempt + 1);
}
}
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function retryMiddleware(config) {
const {
retries,
delay: baseDelay,
backoff = "exponential",
retryCondition = defaultRetryCondition,
onRetry
} = config;
return async (request, next) => {
let lastError;
for (let attempt = 0; attempt <= retries; attempt++) {
try {
return await next(request);
} catch (error) {
lastError = error;
if (attempt === retries) {
throw lastError;
}
if (!retryCondition(lastError)) {
throw lastError;
}
if (onRetry) {
onRetry(attempt + 1, lastError);
}
const delayTime = calculateDelay(attempt, baseDelay, backoff);
await delay(delayTime);
}
}
throw lastError;
};
}
function simpleRetry(retries, delay2 = 1e3) {
return retryMiddleware({
retries,
delay: delay2,
backoff: "exponential"
});
}
// src/middleware/cache.ts
var MemoryCacheStorage = class {
constructor() {
this.cache = /* @__PURE__ */ new Map();
}
async get(key) {
const entry = this.cache.get(key);
if (!entry) {
return null;
}
if (Date.now() - entry.timestamp > entry.ttl) {
this.cache.delete(key);
return null;
}
return entry;
}
async set(key, entry) {
this.cache.set(key, entry);
}
async delete(key) {
this.cache.delete(key);
}
async clear() {
this.cache.clear();
}
// Get cache size
size() {
return this.cache.size;
}
// Clean expired cache
cleanup() {
const now = Date.now();
for (const [key, entry] of this.cache.entries()) {
if (now - entry.timestamp > entry.ttl) {
this.cache.delete(key);
}
}
}
};
function defaultKeyGenerator(request) {
const { url, method, body, query } = request;
const parts = [method.toUpperCase(), url];
if (query && Object.keys(query).length > 0) {
parts.push(JSON.stringify(query));
}
if (body) {
parts.push(JSON.stringify(body));
}
return parts.join("|");
}
function defaultShouldCache(request, response) {
return request.method.toLowerCase() === "get" && response.status >= 200 && response.status < 300;
}
function cacheMiddleware(config) {
const {
ttl,
keyGenerator = defaultKeyGenerator,
shouldCache = defaultShouldCache,
storage = new MemoryCacheStorage()
} = config;
return async (request, next) => {
const cacheKey = keyGenerator(request);
const cachedEntry = await storage.get(cacheKey);
if (cachedEntry) {
return cachedEntry.data;
}
const response = await next(request);
if (shouldCache(request, response)) {
const entry = {
data: response,
timestamp: Date.now(),
ttl
};
await storage.set(cacheKey, entry);
}
return response;
};
}
function simpleCache(ttl) {
return cacheMiddleware({ ttl });
}
// src/utils/request.ts
function mergeHeaders(defaultHeaders, requestHeaders) {
return {
...defaultHeaders,
...requestHeaders
};
}
// src/index.ts
init_fetch();
init_axios();
init_ky();
import { z as z3 } from "zod";
export {
AxiosAdapter,
ConfigError,
FetchAdapter,
HttpError,
KyAdapter,
MemoryCacheStorage,
NetworkError,
SchemaExtractor,
TimeoutError,
ValidationError,
ZodseiClient,
ZodseiError,
buildQueryString,
buildUrl,
cacheMiddleware,
composeMiddleware,
createAdapter,
createClient,
createMiddlewareExecutor,
createSchemaExtractor,
createValidator,
defineContract,
extractPathParamNames,
extractTypeInfo,
getDefaultAdapter,
isAdapterAvailable,
mergeHeaders,
replacePath,
retryMiddleware,
safeParseRequest,
safeParseResponse,
separateParams,
shouldHaveBody,
simpleCache,
simpleRetry,
validateRequest,
validateResponse,
z3 as z
};
//# sourceMappingURL=index.mjs.map