@axiomify/sdk-runtime
v7.1.0
Published
Runtime library for Axiomify-generated TypeScript SDKs — HTTP client, auth injection, retry engine, interceptors, caching, typed errors.
Maintainers
Readme
@axiomify/sdk-runtime
The @axiomify/sdk-runtime is the foundational HTTP networking library that powers all TypeScript clients generated by the axiomify sdk generate command.
Rather than bloating every single generated SDK file with complex logic for retries, interceptors, and authentication injection, the generated code outsources those responsibilities to this zero-dependency library.
Philosophy
- Zero Dependencies: Uses the native
fetchAPI. It does not wrap or bundle Axios, Got, or Node-Fetch. This keeps your client bundle size incredibly small. - Pluggable: Exposes an
InterceptorManagerto tap into the request lifecycle. - Resilient: Features an advanced
withRetryengine with exponential backoff and jitter designed specifically for server-to-server and high-latency edge environments.
Installation
Normally, you don't need to install this manually. If you used axiomify sdk generate -t typescript, this package is included as a dependency of the generated SDK.
However, if you generated only the types, you can install the runtime manually:
npm install @axiomify/sdk-runtimeBasic Usage
The runtime is initialized via the BaseClient. Your generated SDK will extend this class, but you will configure it using ClientConfig.
import { MyGeneratedSDK } from './generated-sdks';
const client = new MyGeneratedSDK({
baseUrl: 'https://api.example.com/v1',
timeoutMs: 10000, // optional; when omitted, no request timeout is applied
});
const users = await client.getUsers({ limit: 100 });Interceptors
Interceptors allow you to mutate requests before they are sent, or intercept responses before they are returned to the caller.
// 1. Add a Request Interceptor
client.interceptors.useRequest(async (req) => {
req.headers = { ...req.headers, 'X-Request-Start': Date.now().toString() };
return req;
});
// 2. Add a Response Interceptor
client.interceptors.useResponse(async (res) => {
if (res.status === 401) {
console.error('Unauthorized! Redirecting to login...');
}
return res;
});
// 3. Add an Error Interceptor
client.interceptors.useError(async (err) => {
console.error('Request failed:', err);
return err;
});A request interceptor receives a ClientRequest whose headers is a plain Record<string, string> (not a Headers instance), so mutate it as an object.
Retry Engine
Network requests fail. @axiomify/sdk-runtime handles retries automatically using exponential backoff and full jitter.
By default, the client retries 3 times. The default retryable status codes are [408, 409, 429, 500, 502, 503, 504], with a base delay of 500ms and a maximum delay of 5000ms.
You can override the retry config at initialization via retryConfig (all fields optional and merged over the defaults):
const client = new MyGeneratedSDK({
baseUrl: 'https://api.example.com/v1',
retryConfig: {
maxRetries: 5, // Retry up to 5 times
baseDelayMs: 200, // Start with a 200ms delay
maxDelayMs: 5000, // Cap the delay at 5000ms
retryableStatusCodes: [429, 500, 502, 503, 504],
},
});Authentication
Injecting tokens into every request is tedious. Use an AuthProvider to automatically manage tokens. An AuthProvider is any object with a getToken() method that returns the full Authorization header value (e.g. "Bearer eyJ..."), or null to skip auth. The client calls getToken() before every request and sets the returned value as the Authorization header.
Static Tokens
For a fixed token, use StaticTokenProvider:
import { StaticTokenProvider } from '@axiomify/sdk-runtime';
const client = new MyGeneratedSDK({
baseUrl: 'https://api.example.com/v1',
authProvider: new StaticTokenProvider('Bearer eyJ...'),
});Custom Providers
For tokens sourced from memory, local storage, or a credential store, implement AuthProvider directly:
const client = new MyGeneratedSDK({
baseUrl: 'https://api.example.com/v1',
authProvider: {
getToken: async () => {
const token = localStorage.getItem('access_token');
return token ? `Bearer ${token}` : null;
},
},
});OAuth2 Client Credentials
OAuth2BearerProvider performs the OAuth2 client_credentials flow for you, caching the token until it nears expiry and refreshing it automatically. It attaches the Authorization: Bearer <token> header before the request hits the network.
import { OAuth2BearerProvider } from '@axiomify/sdk-runtime';
const authProvider = new OAuth2BearerProvider(
'https://auth.example.com/oauth/token', // token URL
'my-client-id',
'my-client-secret',
'read write', // optional scope
);
const client = new MyGeneratedSDK({
baseUrl: 'https://api.example.com/v1',
authProvider,
});Advanced: Custom Fetch Implementation
Because the runtime uses the native fetch API, you can supply your own fetch implementation (for example, a mock in tests or a polyfill) via the fetch option:
const client = new MyGeneratedSDK({
baseUrl: 'https://api.example.com/v1',
fetch: myCustomFetch,
});Static headers applied to every request can be supplied via the headers option:
const client = new MyGeneratedSDK({
baseUrl: 'https://api.example.com/v1',
headers: { 'X-App-Version': '1.2.3' },
});Circuit Breaker
To prevent cascading failures in high-volume microservices or distributed systems, the SDK client wraps all outgoing requests in a circuit breaker. By default, it operates with standard thresholds, but you can configure the behavior to suit your system:
const client = new MyGeneratedSDK({
baseUrl: 'https://api.example.com/v1',
circuitBreakerConfig: {
failureThreshold: 5, // Number of failures before tripping the circuit
cooldownPeriodMs: 10000, // Time to wait (in ms) before attempting probe requests in HALF_OPEN state
halfOpenMaxProbeRequests: 3, // Max probe requests allowed in HALF_OPEN to check system health
},
});If the breaker trips, the client throws a CircuitBreakerError directly, shielding downstream dependencies until the cooldown period expires.
LRU TTL Caching
The runtime provides an in-memory Least Recently Used (LRU) cache with Time-To-Live (TTL) expiration. If enabled, it automatically caches incoming GET responses and returns them for matching paths and query configurations:
const client = new MyGeneratedSDK({
baseUrl: 'https://api.example.com/v1',
enableCache: true,
cacheTtlMs: 60000, // Cache GET responses for 60 seconds
});Additionally, the runtime performs Request Deduplication for identical in-flight GET requests, resolving multiple concurrent requests to the same endpoint with a single network round-trip.
Telemetry Hooks
Provide hooks to intercept and log request metadata, responses, or failures for auditing and APM integration:
const client = new MyGeneratedSDK({
baseUrl: 'https://api.example.com/v1',
telemetry: {
onBeforeRequest: (req) => {
console.log(`Sending ${req.method} request to ${req.path}`);
},
onAfterResponse: (res) => {
console.log(`Received status ${res.status} from ${res.request.path}`);
},
onError: (err) => {
console.error(`Request failed: ${err.message}`);
},
},
});Server-Sent Events (SSE) Client
The client runtime provides a dedicated SseClient to consume Server-Sent Events. It implements automatic reconnection logic with exponential backoff:
import { SseClient } from '@axiomify/sdk-runtime';
const sse = new SseClient('https://api.example.com/v1/live-feed', {
headers: {
Authorization: 'Bearer token123',
},
maxRetries: 5,
baseDelayMs: 1000,
onOpen: () => console.log('SSE connection opened'),
onMessage: (event, data) => console.log(`Received event: ${event}`, data),
onError: (err) => console.error('SSE Error:', err),
});
// Start listening
sse.connect();
// Stop listening
sse.disconnect();WebSocket Client
For full-duplex communication, use the WebSocketClient class. It manages standard browser/node WebSockets with customizable ping-pong heartbeats and auto-reconnection:
import { WebSocketClient } from '@axiomify/sdk-runtime';
const ws = new WebSocketClient('wss://api.example.com/v1/chat', {
heartbeatIntervalMs: 30000, // Send ping every 30 seconds
maxRetries: 10,
onOpen: () => {
console.log('WS connection active');
ws.send(JSON.stringify({ event: 'join', channel: 'general' }));
},
onMessage: (data) => console.log('WS Message:', data),
onClose: () => console.log('WS closed'),
});
ws.connect();Paginators
The Paginator handles cursor-based paginated endpoints seamlessly:
import { Paginator } from '@axiomify/sdk-runtime';
const paginator = new Paginator<User, { cursor?: string }>({
fetchPage: async (params) => {
const response = await client.listUsers(params.cursor);
return {
items: response.users,
nextCursor: response.nextCursor,
hasMore: !!response.nextCursor,
};
},
initialParams: {},
cursorParamName: 'cursor',
});
// Fetch pages sequentially
if (paginator.hasNext()) {
const users = await paginator.nextPage();
console.log('Fetched users:', users);
}Client Offline Queuing
For mobile or edge clients, the OfflineQueue caches requests during offline periods and flushes them automatically when the connection returns:
import { OfflineQueue } from '@axiomify/sdk-runtime';
const offlineQueue = new OfflineQueue();
// In offline state, queue operations
offlineQueue.enqueue({
path: '/users',
method: 'POST',
body: { name: 'Jane Doe', email: '[email protected]' },
});
// Flush automatically on navigator/window online event
// or call manually with a custom processor:
await offlineQueue.flush(async (queuedRequest) => {
await client.request({
path: queuedRequest.path,
method: queuedRequest.method as any,
body: queuedRequest.body,
});
});Environment Switcher
Manage target environments programmatically using the EnvironmentSwitcher:
import { EnvironmentSwitcher } from '@axiomify/sdk-runtime';
const environments = {
production: 'https://api.example.com/v1',
staging: 'https://api-staging.example.com/v1',
development: 'http://localhost:3000',
};
const switcher = new EnvironmentSwitcher(environments, 'development');
console.log(switcher.getUrl()); // http://localhost:3000
switcher.setEnvironment('production');
console.log(switcher.getUrl()); // https://api.example.com/v1