2025-04-16 22:33:33 +02:00

5.9 KiB

System Patterns

This document outlines the system architecture, key technical decisions, and design patterns used in the G1 TypeScript Common Packages repository.

System Architecture

The repository follows a monorepo architecture using pnpm workspaces:

g1-ts-common-packages/
├── packages/                 # All packages are stored here
│   ├── sse-client/           # Server-Sent Events client package
│   └── [future-packages]/    # Additional packages will be added here
├── scripts/                  # Utility scripts for the repository
├── docs/                     # Documentation
└── .github/workflows/        # CI/CD workflows

Each package within the monorepo is designed to be:

  • Independent: Can be used without other packages from the monorepo
  • Focused: Solves a specific problem domain
  • Well-documented: Includes comprehensive documentation
  • Well-tested: Includes unit tests

Key Technical Decisions

1. Monorepo Structure

  • Decision: Use a monorepo structure with pnpm workspaces
  • Rationale:
    • Simplifies management of multiple related packages
    • Enables sharing of configuration and tooling
    • Facilitates coordinated changes across packages
    • Reduces duplication of dependencies

2. Package Scope

  • Decision: Use the @g1 scope for all packages
  • Rationale:
    • Provides namespace isolation
    • Makes it clear that packages belong to Generation One
    • Prevents name collisions with other packages

3. Package Versioning

  • Decision: Start packages at version 0.2.0
  • Rationale:
    • Follows Generation One's standard starting version
    • Allows for pre-1.0 breaking changes as packages mature

4. Publishing Strategy

  • Decision: Publish packages to Gitea registry
  • Rationale:
    • Keeps packages within Generation One's infrastructure
    • Provides access control for private packages (if needed in the future)
    • Simplifies CI/CD integration

5. TypeScript First

  • Decision: Use TypeScript for all packages
  • Rationale:
    • Provides type safety
    • Improves developer experience
    • Enables better tooling and IDE support
    • Reduces runtime errors

Design Patterns

SSE Client Package

The SSE Client package demonstrates several design patterns:

1. Singleton Pattern

  • Used in the SSEConnectionManager to ensure only one instance manages all connections
  • Provides a global point of access to the connection manager
export class SSEConnectionManager {
  private static instance: SSEConnectionManager;
  
  private constructor() {}
  
  public static getInstance(): SSEConnectionManager {
    if (!SSEConnectionManager.instance) {
      SSEConnectionManager.instance = new SSEConnectionManager();
    }
    return SSEConnectionManager.instance;
  }
  
  // ...
}

2. Factory Pattern

  • Helper functions like getSSEConnection act as factories to create or retrieve SSE client instances
  • Simplifies the creation and management of SSE connections
export function getSSEConnection(url: string, id: string, options: SSEClientOptions = {}): SSEClient {
  return SSEConnectionManager.getInstance().getConnection(url, id, options);
}

3. Observer Pattern

  • The SSE client implements an event-based system where clients can subscribe to events
  • Uses the browser's EventTarget or a custom implementation in Node.js
public on(eventName: string, listener: (event: any) => void): SSEClient {
  if (!this.eventListeners.has(eventName)) {
    this.eventListeners.set(eventName, new Set());
  }

  this.eventListeners.get(eventName)!.add(listener);
  
  // ...
  
  return this;
}

4. Options Pattern

  • Uses an options object with defaults for configuration
  • Allows for flexible and extensible configuration
export interface SSEClientOptions {
  headers?: Record<string, string>;
  withCredentials?: boolean;
  heartbeatTimeout?: number;
  // ...
}

const DEFAULT_OPTIONS: Required<Omit<SSEClientOptions, 'headers'>> = {
  withCredentials: true,
  heartbeatTimeout: 21600000, // 6 hours
  // ...
};

5. Exponential Backoff Pattern

  • Implements exponential backoff for reconnection attempts
  • Helps prevent overwhelming the server during outages
private async reconnect(): Promise<void> {
  this.retryCount++;

  // Calculate delay with exponential backoff
  const delayMs = Math.min(
    this.options.initialRetryDelay * Math.pow(2, this.retryCount - 1),
    this.options.maxRetryDelay
  );
  
  // ...
}

Critical Implementation Paths

SSE Client Connection Flow

  1. Initialization: Create an SSE client with URL and options
  2. Connection: Call connect() to establish the connection
  3. Event Handling: Register event listeners with on(eventName, listener)
  4. Error Handling: Automatic reconnection with exponential backoff
  5. Cleanup: Close connection with close() or use automatic cleanup

SSE Client Connection Management

  1. Get/Create Connection: Use getSSEConnection(url, id, options) to get or create a connection
  2. Connection Reuse: Connections are reused based on the provided ID
  3. Connection Cleanup: Close specific connections with closeSSEConnection(id) or all with closeAllSSEConnections()
  4. Automatic Cleanup: Set up with setupSSECleanup() to close connections on page unload

Component Relationships

  • SSEClient: Core class that handles the SSE connection and events
  • SSEConnectionManager: Singleton that manages multiple SSE connections
  • Helper Functions: Provide simplified access to the connection manager
    • getSSEConnection: Gets or creates a connection
    • closeSSEConnection: Closes a specific connection
    • closeAllSSEConnections: Closes all connections
    • setupSSECleanup: Sets up automatic cleanup
  • Debug Utilities: Provide debug logging capabilities
    • debugLog, debugInfo, debugWarn, debugError