client-base/IMPLEMENTATION_SUMMARY.md

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: AbstractHttpClient interface 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: AbstractWebSocketClient interface

Error Handling (src/errors/)

  • client-error.ts: Error hierarchy
    • ClientError (base)
    • HttpError (statusCode, response)
    • WebSocketError (code)
    • TimeoutError (timeoutMs)
    • AbortError
    • MiddlewareError (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 auth
    • createQueryAuthMiddleware() - 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() - Registry
    • createHttpClient() - Factory function
    • HttpClientBuilder - Fluent builder
  • websocket-client-factory.ts: WebSocket client factory and builder
    • registerWebSocketClientImplementation() - Registry
    • createWebSocketClient() - Factory function
    • WebSocketClientBuilder - Fluent builder
  • client-composition.ts: Composed clients
    • ComposedClient - HTTP + WebSocket
    • createClientFactory() - Generic factory
    • createHttpOnlyClientFactory()
    • 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)

  1. Create axios implementation: @lilith/http-client-axios

    • Implement AbstractHttpClient using axios
    • Wire up interceptor managers
    • Handle errors properly
    • Register implementation
  2. Create fetch implementation: @lilith/http-client-fetch

    • Implement AbstractHttpClient using native fetch
    • Handle response types (json, blob, etc.)
    • Implement timeout using AbortController
    • Register implementation
  3. Update existing http-client: Deprecate or migrate to new architecture

Future Enhancements

  1. Socket.IO implementation: @lilith/websocket-client-socketio

    • Already exists, needs to implement AbstractWebSocketClient
  2. Native WebSocket implementation: @lilith/websocket-client-native

    • Implement AbstractWebSocketClient using native WebSocket
    • Handle reconnection manually
    • Message framing for structured events
  3. Additional Middleware

    • Circuit breaker
    • Request deduplication
    • Response caching
    • Rate limiting
  4. GraphQL Support

    • Extend AbstractHttpClient for GraphQL operations
    • Query/mutation helpers
    • Type generation integration

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

  1. DIP Compliance: Application code depends on abstractions, not implementations
  2. Testability: Easy to inject mock implementations
  3. Flexibility: Swap implementations without code changes
  4. Composability: Middleware pattern for cross-cutting concerns
  5. Type Safety: Full TypeScript support with generics
  6. Bundle Size: Choose minimal implementation (fetch vs axios)
  7. Migration Path: Gradual migration from legacy code
  8. 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.