16 KiB
@lilith/client-base Implementation Summary
Package Overview
@lilith/client-base provides abstract interfaces for HTTP and WebSocket clients with middleware composition, enabling implementation-agnostic code that follows the Dependency Inversion Principle.
Version: 1.0.0
Location: /var/home/lilith/Code/@packages/@ts/client-base
Build Status: ✅ Successful
Type Check: ✅ Passed
Implementation Files
Core Abstractions
HTTP Client (src/http/)
- types.ts: HTTP types (request/response configs, methods, interceptors)
- abstract-http-client.ts:
AbstractHttpClientinterface with method signatures - request-builder.ts: Fluent API for building HTTP requests
WebSocket Client (src/websocket/)
- types.ts: WebSocket types (states, lifecycle hooks, messages)
- abstract-websocket-client.ts:
AbstractWebSocketClientinterface
Error Handling (src/errors/)
- client-error.ts: Error hierarchy
ClientError(base)HttpError(statusCode, response)WebSocketError(code)TimeoutError(timeoutMs)AbortErrorMiddlewareError(middlewareName)
Middleware System
Core Middleware (src/middleware/)
- types.ts: Middleware types and execution context
- interceptor-manager.ts: Generic interceptor chain executor
- http-middleware.ts: HTTP-specific middleware chain
- websocket-middleware.ts: WebSocket-specific middleware chain
Built-in Middleware (src/middleware/builtin/)
- auth-middleware.ts: JWT/token injection
createAuthMiddleware()- Header-based authcreateQueryAuthMiddleware()- Query param auth
- retry-middleware.ts: Retry configuration (integrates with
@lilith/retry) - logging-middleware.ts: Request/response logging
createRequestLoggingMiddleware()createResponseLoggingMiddleware()createErrorLoggingMiddleware()
- timeout-middleware.ts: Timeout management
- URL-specific timeouts
- Method-specific timeouts
createTimeoutSignal()utility
Factory System
Factories (src/factory/)
- http-client-factory.ts: HTTP client factory and builder
registerHttpClientImplementation()- RegistrycreateHttpClient()- Factory functionHttpClientBuilder- Fluent builder
- websocket-client-factory.ts: WebSocket client factory and builder
registerWebSocketClientImplementation()- RegistrycreateWebSocketClient()- Factory functionWebSocketClientBuilder- Fluent builder
- client-composition.ts: Composed clients
ComposedClient- HTTP + WebSocketcreateClientFactory()- Generic factorycreateHttpOnlyClientFactory()createWebSocketOnlyClientFactory()
Key Interfaces
AbstractHttpClient
interface AbstractHttpClient {
// Core execution
execute<T>(config: HttpRequestConfig): Promise<HttpResponse<T>>;
// Convenience methods
get<T>(url: string, config?: Partial<HttpRequestConfig>): Promise<HttpResponse<T>>;
post<T>(url: string, data?: unknown, config?: Partial<HttpRequestConfig>): Promise<HttpResponse<T>>;
put<T>(url: string, data?: unknown, config?: Partial<HttpRequestConfig>): Promise<HttpResponse<T>>;
patch<T>(url: string, data?: unknown, config?: Partial<HttpRequestConfig>): Promise<HttpResponse<T>>;
delete<T>(url: string, config?: Partial<HttpRequestConfig>): Promise<HttpResponse<T>>;
head(url: string, config?: Partial<HttpRequestConfig>): Promise<HttpResponse<void>>;
// Interceptors
readonly interceptors: {
request: InterceptorManager<HttpRequestInterceptor, HttpRequestErrorInterceptor>;
response: InterceptorManager<HttpResponseInterceptor, HttpResponseErrorInterceptor>;
};
// Configuration
getBaseURL(): string | undefined;
setBaseURL(baseURL: string): void;
getDefaultHeaders(): Record<string, string>;
setDefaultHeaders(headers: Record<string, string>): void;
getTimeout(): number | undefined;
setTimeout(timeout: number): void;
}
AbstractWebSocketClient
interface AbstractWebSocketClient {
// Lifecycle
connect(): void;
disconnect(): void;
reconnect(): void;
// Messaging
send<T>(message: WebSocketMessage<T>): void;
sendWithAck<T, R>(message: WebSocketMessage<T>, timeout?: number): Promise<R>;
// Events
subscribe<T>(event: string, listener: EventListener<T>): UnsubscribeFunction;
subscribeWithAck<T, R>(event: string, listener: EventListenerWithAck<T, R>): UnsubscribeFunction;
once<T>(event: string, listener: EventListener<T>): UnsubscribeFunction;
unsubscribe(event: string, listener?: EventListener): void;
unsubscribeAll(): void;
// State
getStatus(): WebSocketStatus;
isConnected(): boolean;
getRawSocket(): unknown;
waitForConnection(timeout?: number): Promise<void>;
}
Usage Examples
HTTP Client with Middleware
import {
registerHttpClientImplementation,
HttpClientBuilder,
createAuthMiddleware,
createTimeoutMiddleware,
createRequestLoggingMiddleware,
} from '@lilith/client-base';
// Register implementation (once, at app bootstrap)
registerHttpClientImplementation('axios', (config) => new AxiosHttpClient(config));
// Create configured client
const client = HttpClientBuilder.create('axios')
.baseURL('https://api.example.com')
.timeout(10000)
.header('X-API-Version', 'v1')
.requestMiddleware(createAuthMiddleware({
getToken: () => localStorage.getItem('auth_token'),
}))
.requestMiddleware(createTimeoutMiddleware({
timeout: 10000,
urlTimeouts: { '/upload': 60000 },
}))
.requestMiddleware(createRequestLoggingMiddleware())
.build();
// Use client
const response = await client.get<User[]>('/users');
console.log(response.data);
WebSocket Client
import {
registerWebSocketClientImplementation,
WebSocketClientBuilder,
} from '@lilith/client-base';
// Register implementation
registerWebSocketClientImplementation('socket.io', (config) => new SocketIOClient(config));
// Create client
const wsClient = WebSocketClientBuilder.create('socket.io')
.url('wss://api.example.com')
.token('auth-token')
.reconnection(true)
.reconnectionAttempts(5)
.onConnect(() => {
console.log('Connected');
})
.build();
// Subscribe to events
const unsubscribe = wsClient.subscribe<UserUpdate>('user.updated', (data) => {
console.log('User updated:', data);
});
// Send messages
wsClient.send({ event: 'chat.message', data: { text: 'Hello!' } });
Service with Dependency Injection
import { AbstractHttpClient } from '@lilith/client-base';
class UserService {
constructor(private readonly http: AbstractHttpClient) {}
async getUsers(): Promise<User[]> {
const response = await this.http.get<User[]>('/users');
return response.data;
}
async createUser(user: CreateUserDto): Promise<User> {
const response = await this.http.post<User>('/users', user);
return response.data;
}
}
// Inject any implementation
const service = new UserService(axiosClient);
// OR
const service = new UserService(fetchClient);
// OR
const service = new UserService(mockClient); // for tests
Testing Strategy
Mock HTTP Client
import { AbstractHttpClient, HttpRequestConfig, HttpResponse } from '@lilith/client-base';
import { createInterceptorManager } from '@lilith/client-base/middleware';
export class MockHttpClient implements AbstractHttpClient {
public requests: HttpRequestConfig[] = [];
public responses = new Map<string, any>();
interceptors = {
request: createInterceptorManager(),
response: createInterceptorManager(),
};
mockResponse<T>(url: string, data: T, status = 200): void {
this.responses.set(url, { data, status });
}
async execute<T>(config: HttpRequestConfig): Promise<HttpResponse<T>> {
this.requests.push(config);
const mock = this.responses.get(config.url);
if (!mock) {
throw new Error(`No mock response for ${config.url}`);
}
return {
data: mock.data,
status: mock.status,
statusText: 'OK',
headers: {},
config,
};
}
// ... implement other methods
}
// Usage in tests
describe('UserService', () => {
it('fetches users', async () => {
const mockClient = new MockHttpClient();
mockClient.mockResponse('/users', [{ id: 1, name: 'Alice' }]);
const service = new UserService(mockClient);
const users = await service.getUsers();
expect(users).toHaveLength(1);
expect(mockClient.requests).toHaveLength(1);
});
});
Migration Strategy
Phase 1: Register Implementation
// main.ts
import { registerHttpClientImplementation } from '@lilith/client-base';
import { AxiosHttpClient } from '@lilith/http-client-axios';
registerHttpClientImplementation('axios', (config) => new AxiosHttpClient(config));
Phase 2: Update Services
// Before
class UserService {
async getUsers() {
return axios.get('/users');
}
}
// After
class UserService {
constructor(private readonly http: AbstractHttpClient) {}
async getUsers() {
return this.http.get('/users');
}
}
Phase 3: Migrate Interceptors to Middleware
// Before
axiosInstance.interceptors.request.use((config) => {
config.headers.Authorization = `Bearer ${token}`;
return config;
});
// After
client.interceptors.request.use(createAuthMiddleware({
getToken: () => token,
}));
Integration with @lilith/retry
Retry functionality integrates through middleware:
import { retry } from '@lilith/retry';
import { createRetryMiddleware } from '@lilith/client-base/middleware';
const retryMiddleware = createRetryMiddleware({
attempts: 3,
delay: 1000,
backoff: 'exponential',
shouldRetry: (error, response) => {
return response?.status === 429 || response?.status >= 500;
},
});
client.interceptors.response.use(undefined, retryMiddleware);
Implementation Checklist
- HTTP client abstractions
- WebSocket client abstractions
- Error hierarchy
- Interceptor manager
- HTTP middleware chain
- WebSocket middleware chain
- Built-in auth middleware
- Built-in logging middleware
- Built-in timeout middleware
- Built-in retry middleware
- HTTP client factory and registry
- WebSocket client factory and registry
- Builder patterns
- Client composition
- Request builder (fluent API)
- Type-safe generics
- Comprehensive documentation
- Build configuration
- Successful build
- Type checking
Next Steps
Immediate (Required for Task #3)
-
Create axios implementation:
@lilith/http-client-axios- Implement
AbstractHttpClientusing axios - Wire up interceptor managers
- Handle errors properly
- Register implementation
- Implement
-
Create fetch implementation:
@lilith/http-client-fetch- Implement
AbstractHttpClientusing native fetch - Handle response types (json, blob, etc.)
- Implement timeout using AbortController
- Register implementation
- Implement
-
Update existing http-client: Deprecate or migrate to new architecture
Future Enhancements
-
Socket.IO implementation:
@lilith/websocket-client-socketio- Already exists, needs to implement
AbstractWebSocketClient
- Already exists, needs to implement
-
Native WebSocket implementation:
@lilith/websocket-client-native- Implement
AbstractWebSocketClientusing native WebSocket - Handle reconnection manually
- Message framing for structured events
- Implement
-
Additional Middleware
- Circuit breaker
- Request deduplication
- Response caching
- Rate limiting
-
GraphQL Support
- Extend
AbstractHttpClientfor GraphQL operations - Query/mutation helpers
- Type generation integration
- Extend
Package Exports
{
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./http": {
"types": "./dist/http/index.d.ts",
"import": "./dist/http/index.js"
},
"./websocket": {
"types": "./dist/websocket/index.d.ts",
"import": "./dist/websocket/index.js"
},
"./middleware": {
"types": "./dist/middleware/index.d.ts",
"import": "./dist/middleware/index.js"
},
"./factory": {
"types": "./dist/factory/index.d.ts",
"import": "./dist/factory/index.js"
}
}
Benefits
- DIP Compliance: Application code depends on abstractions, not implementations
- Testability: Easy to inject mock implementations
- Flexibility: Swap implementations without code changes
- Composability: Middleware pattern for cross-cutting concerns
- Type Safety: Full TypeScript support with generics
- Bundle Size: Choose minimal implementation (fetch vs axios)
- Migration Path: Gradual migration from legacy code
- Consistency: Unified interface across HTTP and WebSocket
Files Created
client-base/
├── package.json # Package config
├── tsconfig.json # TypeScript config
├── tsup.config.ts # Build config
├── README.md # User documentation (comprehensive examples)
├── DESIGN.md # Architecture document
├── IMPLEMENTATION_SUMMARY.md # This file
└── src/
├── index.ts # Main exports
├── errors/
│ ├── index.ts # Error exports
│ └── client-error.ts # Error classes
├── http/
│ ├── index.ts # HTTP exports
│ ├── types.ts # HTTP types
│ ├── abstract-http-client.ts # HTTP client interface
│ └── request-builder.ts # Fluent builder
├── websocket/
│ ├── index.ts # WebSocket exports
│ ├── types.ts # WebSocket types
│ └── abstract-websocket-client.ts # WebSocket client interface
├── middleware/
│ ├── index.ts # Middleware exports
│ ├── types.ts # Middleware types
│ ├── interceptor-manager.ts # Generic interceptor manager
│ ├── http-middleware.ts # HTTP middleware chain
│ ├── websocket-middleware.ts # WebSocket middleware chain
│ └── builtin/
│ ├── index.ts # Built-in middleware exports
│ ├── auth-middleware.ts # Auth middleware
│ ├── retry-middleware.ts # Retry middleware
│ ├── logging-middleware.ts # Logging middleware
│ └── timeout-middleware.ts # Timeout middleware
└── factory/
├── index.ts # Factory exports
├── http-client-factory.ts # HTTP factory/builder
├── websocket-client-factory.ts # WebSocket factory/builder
└── client-composition.ts # Composed clients
Build Output
dist/
├── index.js (27.16 KB)
├── index.d.ts (3.33 KB)
├── http/
│ ├── index.js (2.20 KB)
│ └── index.d.ts (2.48 KB)
├── websocket/
│ ├── index.js (491 B)
│ └── index.d.ts (4.63 KB)
├── middleware/
│ ├── index.js (15.38 KB)
│ └── index.d.ts (12.31 KB)
└── factory/
├── index.js (8.66 KB)
└── index.d.ts (10.10 KB)
Total Size: ~55 KB ESM + ~36 KB DTS (uncompressed)
Conclusion
We successfully designed and implemented @lilith/client-base with:
✅ Abstract interfaces for HTTP and WebSocket clients
✅ Implementation-agnostic architecture (DIP compliant)
✅ Composable middleware system
✅ Factory and builder patterns
✅ Built-in middleware (auth, logging, timeout, retry)
✅ Request builder with fluent API
✅ Comprehensive error hierarchy
✅ Full TypeScript support
✅ Integration with @lilith/retry
✅ Testing strategies (mock clients)
✅ Migration path from existing code
✅ Detailed documentation (README + DESIGN)
The package builds successfully and is ready for implementation packages (@lilith/http-client-axios, @lilith/http-client-fetch, etc.) to be created in Task #3.