UNPKG

harmony-plugin-manager

Version:

A comprehensive TypeScript library for generating harmonious color palettes with WCAG 2.1 accessibility compliance

1,655 lines (1,368 loc) • 107 kB
# šŸ”Œ Harmony Plugin Manager > **A type-safe, dependency-aware plugin system built on Harmony Pipeline** Transform your applications into extensible, modular powerhouses with a plugin architecture that just works. Built with TypeScript-first design, comprehensive lifecycle management, and zero-configuration dependency resolution. [![npm version](https://badge.fury.io/js/harmony-plugin-manager.svg)](https://badge.fury.io/js/harmony-plugin-manager) [![TypeScript](https://img.shields.io/badge/TypeScript-007ACC?style=flat&logo=typescript&logoColor=white)](https://www.typescriptlang.org/) [![MIT License](https://img.shields.io/badge/License-MIT-green.svg)](https://choosealicense.com/licenses/mit/) ## ✨ Why Choose Harmony Plugin Manager? šŸŽÆ **Type-Safe by Design** - Full TypeScript support with generic type constraints šŸ”— **Smart Dependency Resolution** - Automatic topological sorting with circular dependency detection ⚔ **Zero Configuration** - Works out of the box with sensible defaults šŸ—ļø **Flexible Architecture** - Builder patterns, factories, and middleware support šŸ›”ļø **Production Ready** - Comprehensive error handling and validation šŸ“Š **Rich Execution Context** - Shared state, logging, and metadata management ## šŸš€ Quick Start ```bash npm install harmony-plugin-manager harmony-pipeline ``` ### Basic Usage ```typescript import { createPluginManager, simplePlugin } from 'harmony-plugin-manager'; // Create a plugin manager const manager = createPluginManager<string, string>() .register(simplePlugin({ name: 'hello', version: '1.0.0' }, input => `Hello, ${input}!`)) .register(simplePlugin({ name: 'exclaim', version: '1.0.0' }, input => `${input}!!!`)) .build(); // Execute the pipeline const result = await manager.execute('World', {}); console.log(result.results[1].output); // "Hello, World!!!" ``` ## šŸŽØ Core Features ### šŸ”§ Plugin Creation Made Simple The library offers multiple plugin creation patterns to suit different development styles and requirements: #### **Factory Function Approach** - Perfect for Simple Plugins Quick and functional approach for straightforward transformations: ```typescript const validationPlugin = simplePlugin( { name: 'validate-email', version: '1.0.0' }, (email: string) => { if (!email.includes('@')) throw new Error('Invalid email'); return email; }, ); ``` **Benefits:** - āœ… Minimal boilerplate - āœ… Functional programming style - āœ… Type inference works automatically - āœ… Perfect for stateless operations #### **Class-Based Approach** - Full Control & Lifecycle Management For complex plugins that need state management and lifecycle hooks, inherit from `BasePlugin` to access the full plugin infrastructure: ##### **Understanding BasePlugin Architecture** The `BasePlugin` abstract class provides the foundation for all class-based plugins: ```typescript abstract class BasePlugin< TInput = unknown, TOutput = unknown, TContext extends PluginContext = PluginContext, > implements IPlugin<TInput, TOutput, TContext> { abstract readonly metadata: PluginMetadata; abstract execute(input: TInput, context: TContext): Promise<TOutput> | TOutput; // Lifecycle hooks with default implementations async initialize(context: TContext): Promise<void> { /* logging */ } async cleanup(context: TContext): Promise<void> { /* logging */ } async onError(error: Error, context: TContext): Promise<void> { /* error logging */ } // Utility methods for consistent logging protected logInfo(context: TContext, message: string, data?: unknown): void; protected logWarning(context: TContext, message: string, data?: unknown): void; protected logError(context: TContext, message: string, data?: unknown): void; } ``` ##### **Key Benefits of BasePlugin Inheritance** **1. Consistent Lifecycle Management** ```typescript // āœ… Automatic resource setup and teardown class DatabasePlugin extends BasePlugin<Query, Result> { private connection?: DatabaseConnection; async initialize(context: PluginContext): Promise<void> { await super.initialize(context); // Gets consistent logging this.connection = await createConnection(); // Resources automatically cleaned up in cleanup() } async cleanup(context: PluginContext): Promise<void> { await this.connection?.close(); await super.cleanup(context); // Consistent cleanup logging } } ``` **2. Built-in Error Handling Infrastructure** ```typescript // āœ… Structured error handling with context class PaymentPlugin extends BasePlugin<Payment, ProcessedPayment> { async onError(error: Error, context: PluginContext): Promise<void> { // Custom error handling if (error instanceof PaymentGatewayError) { await this.handlePaymentFailure(error, context); } // Parent handles standard error logging and context updates await super.onError(error, context); } } ``` **3. Consistent Logging with Plugin Context** ```typescript // āœ… Structured logging that includes plugin metadata class ValidationPlugin extends BasePlugin<Data, ValidatedData> { async execute(data: Data, context: PluginContext): Promise<ValidatedData> { // Logs automatically include plugin name and execution context this.logInfo(context, 'Starting validation', { dataSize: data.items.length }); if (someWarningCondition) { // Warnings are automatically aggregated in context this.logWarning(context, 'Validation warning', { issue: 'minor format issue' }); } return validatedData; } } ``` **4. State Management and Resource Sharing** ```typescript // āœ… Clean state management with proper encapsulation class CachingPlugin extends BasePlugin<CacheableData, CachedData> { private cache = new Map<string, CachedData>(); private readonly MAX_CACHE_SIZE = 1000; async execute(data: CacheableData, context: PluginContext): Promise<CachedData> { // Instance state is isolated per plugin const cached = this.cache.get(data.key); if (cached) return cached; const processed = await this.processData(data); // Manage cache size if (this.cache.size >= this.MAX_CACHE_SIZE) { this.evictOldestEntries(); } this.cache.set(data.key, processed); return processed; } async cleanup(context: PluginContext): Promise<void> { this.cache.clear(); // Automatic cleanup await super.cleanup(context); } } ``` **Benefits:** - āœ… Full lifecycle control (initialize, execute, cleanup, onError) - āœ… State management capabilities with proper encapsulation - āœ… Built-in logging methods with consistent formatting - āœ… Error handling customization with fallback to defaults - āœ… Resource sharing through context with automatic cleanup - āœ… Perfect for complex business logic requiring stateful operations - āœ… Type-safe inheritance with full TypeScript support ## šŸ”„ Data Flow: Immutability, Mutability, and Sequential Processing Understanding how data flows through the plugin pipeline is crucial for building reliable, predictable applications. The plugin system provides clear patterns for data transformation while maintaining state integrity. ### šŸ“‹ Core Principles #### **Immutable by Default** Input data should be treated as immutable. Plugins receive read-only input and produce new output rather than modifying existing data. ```typescript // āœ… GOOD: Immutable transformation class UserNormalizerPlugin extends BasePlugin<RawUser, NormalizedUser> { execute(input: RawUser): NormalizedUser { // Create new object instead of modifying input return { id: input.user_id, name: input.full_name.trim(), email: input.email_address.toLowerCase(), createdAt: new Date(input.created_timestamp), }; } } // āŒ BAD: Mutating input data class BadUserPlugin extends BasePlugin<RawUser, RawUser> { execute(input: RawUser): RawUser { input.email_address = input.email_address.toLowerCase(); // Mutates input! input.processed = true; // Side effect! return input; } } ``` #### **Controlled Mutability Through Context** Shared state mutations should only occur through the plugin context's controlled mechanisms. ```typescript class StatefulProcessorPlugin extends BasePlugin<Data, ProcessedData> { execute(input: Data, context: PluginContext): ProcessedData { // āœ… GOOD: Controlled shared state mutation const statistics = context.retrieve<ProcessingStats>('stats') || { totalProcessed: 0, errors: 0, }; statistics.totalProcessed++; context.share('stats', statistics); // āœ… GOOD: Immutable data transformation return { ...input, processedAt: Date.now(), status: 'processed', }; } } ``` ### šŸ”„ Sequential Data Processing Patterns #### **Linear Transformation Chain** Each plugin receives the output of the previous plugin as its input, creating a clear data transformation pipeline. ```typescript interface UserRegistrationFlow { // Stage 1: Raw form data formData: FormData; // Stage 2: After validation validatedData: ValidatedUserData; // Stage 3: After normalization normalizedData: NormalizedUserData; // Stage 4: After enrichment enrichedData: EnrichedUserData; // Stage 5: Final result savedUser: SavedUser; } const registrationPipeline = createPluginManager<FormData, SavedUser>() // Stage 1 → 2: Validation .register( simplePlugin( { name: 'input-validator', version: '1.0.0' }, (formData: FormData): ValidatedUserData => { if (!formData.email || !formData.password) { throw new Error('Missing required fields'); } return { email: formData.email, password: formData.password, name: formData.name || '', validatedAt: Date.now(), }; }, ), ) // Stage 2 → 3: Normalization .register( simplePlugin( { name: 'data-normalizer', version: '1.0.0', dependencies: ['input-validator'], }, (validated: ValidatedUserData): NormalizedUserData => ({ email: validated.email.toLowerCase().trim(), password: hashPassword(validated.password), name: validated.name.trim(), normalizedAt: Date.now(), }), ), ) // Stage 3 → 4: Enrichment .register( simplePlugin( { name: 'user-enricher', version: '1.0.0', dependencies: ['data-normalizer'], }, async (normalized: NormalizedUserData): Promise<EnrichedUserData> => { const profile = await fetchUserProfile(normalized.email); return { ...normalized, profile, enrichedAt: Date.now(), }; }, ), ) // Stage 4 → 5: Persistence .register( simplePlugin( { name: 'user-saver', version: '1.0.0', dependencies: ['user-enricher'], }, async (enriched: EnrichedUserData, context): Promise<SavedUser> => { const db = context.retrieve<Database>('database'); const savedUser = await db.users.create(enriched); context.share('user-registration-result', { userId: savedUser.id, registeredAt: Date.now(), }); return savedUser; }, ), ) .build(); ``` #### **Branching and Merging Patterns** Handle complex data flows where processing branches and merges. ```typescript // Parallel processing with result aggregation const dataAnalysisPipeline = createPluginManager<DataSet, AnalysisResult>() .register( simplePlugin( { name: 'data-validator', version: '1.0.0' }, (input: DataSet): ValidatedDataSet => validateAndCleanData(input), ), ) // Branch 1: Statistical Analysis .register( simplePlugin( { name: 'statistical-analyzer', version: '1.0.0', dependencies: ['data-validator'], }, async (data: ValidatedDataSet, context): Promise<ValidatedDataSet> => { const stats = await performStatisticalAnalysis(data); context.share('statistical-results', stats); return data; // Pass through unchanged }, ), ) // Branch 2: Trend Analysis .register( simplePlugin( { name: 'trend-analyzer', version: '1.0.0', dependencies: ['data-validator'], }, async (data: ValidatedDataSet, context): Promise<ValidatedDataSet> => { const trends = await performTrendAnalysis(data); context.share('trend-results', trends); return data; // Pass through unchanged }, ), ) // Merge: Combine Results .register( simplePlugin( { name: 'result-aggregator', version: '1.0.0', dependencies: ['statistical-analyzer', 'trend-analyzer'], }, (data: ValidatedDataSet, context): AnalysisResult => { const stats = context.retrieve<StatisticalResults>('statistical-results'); const trends = context.retrieve<TrendResults>('trend-results'); return { dataset: data, statistics: stats, trends: trends, aggregatedAt: Date.now(), }; }, ), ) .build(); ``` ### šŸŖ State Management Patterns #### **Plugin-Scoped State** State that belongs to a single plugin and doesn't need sharing. ```typescript class CachingPlugin extends BasePlugin<CacheableData, CacheableData> { private cache = new Map<string, any>(); private readonly maxSize = 1000; execute(input: CacheableData, context: PluginContext): CacheableData { const key = this.generateCacheKey(input); // Check plugin-local cache if (this.cache.has(key)) { context.logger.debug('Cache hit', { key }); return this.cache.get(key); } // Process and cache const result = this.processData(input); this.addToCache(key, result); return result; } private addToCache(key: string, value: any): void { if (this.cache.size >= this.maxSize) { const firstKey = this.cache.keys().next().value; this.cache.delete(firstKey); } this.cache.set(key, value); } } ``` #### **Shared Context State** State that needs to be shared between plugins in the same execution. ```typescript class DatabaseConnectionPlugin extends BasePlugin<any, any> { async initialize(context: PluginContext): Promise<void> { const settings = context.getSettings(this.metadata.name); const connectionString = settings?.config.connectionString as string; const db = await createDatabaseConnection(connectionString); // Share connection for other plugins context.share('database-connection', db); context.share('connection-stats', { connectedAt: Date.now(), queriesExecuted: 0, }); } async cleanup(context: PluginContext): Promise<void> { const db = context.retrieve<Database>('database-connection'); await db?.close(); } } class QueryExecutorPlugin extends BasePlugin<Query, QueryResult> { async execute(input: Query, context: PluginContext): Promise<QueryResult> { const db = context.retrieve<Database>('database-connection'); if (!db) { throw new Error('Database connection not available'); } const result = await db.execute(input); // Update shared statistics const stats = context.retrieve<ConnectionStats>('connection-stats'); if (stats) { stats.queriesExecuted++; context.share('connection-stats', stats); } return result; } } ``` #### **Accumulator Pattern** Building up state across multiple plugin executions. ```typescript class MetricsCollectorPlugin extends BasePlugin<ProcessableData, ProcessableData> { execute(input: ProcessableData, context: PluginContext): ProcessableData { // Get or initialize metrics accumulator const metrics = context.retrieve<ProcessingMetrics>('processing-metrics') || { totalItems: 0, successfulItems: 0, failedItems: 0, processingTimes: [], errors: [], }; const startTime = Date.now(); try { const result = this.processItem(input); // Update success metrics metrics.totalItems++; metrics.successfulItems++; metrics.processingTimes.push(Date.now() - startTime); context.share('processing-metrics', metrics); return result; } catch (error) { // Update failure metrics metrics.totalItems++; metrics.failedItems++; metrics.errors.push({ item: input.id, error: error.message, timestamp: Date.now(), }); context.share('processing-metrics', metrics); throw error; } } } class MetricsReporterPlugin extends BasePlugin<any, ProcessingReport> { execute(input: any, context: PluginContext): ProcessingReport { const metrics = context.retrieve<ProcessingMetrics>('processing-metrics'); if (!metrics) { throw new Error('No processing metrics available'); } const avgProcessingTime = metrics.processingTimes.length > 0 ? metrics.processingTimes.reduce((a, b) => a + b, 0) / metrics.processingTimes.length : 0; return { summary: { totalItems: metrics.totalItems, successRate: metrics.totalItems > 0 ? (metrics.successfulItems / metrics.totalItems) * 100 : 0, averageProcessingTime: avgProcessingTime, }, details: { successfulItems: metrics.successfulItems, failedItems: metrics.failedItems, errors: metrics.errors, processingTimes: metrics.processingTimes, }, generatedAt: Date.now(), }; } } ``` ### šŸ”„ Sequential Processing Best Practices #### **1. Clear Data Contracts** Define explicit input/output types for each transformation stage. ```typescript // āœ… GOOD: Clear type progression interface PipelineStages { raw: RawUserInput; // From form submission validated: ValidatedUser; // After input validation normalized: NormalizedUser; // After data normalization enriched: EnrichedUser; // After external data enrichment persisted: PersistedUser; // After database save } ``` #### **2. Functional Composition** Design plugins as pure functions when possible. ```typescript // āœ… GOOD: Pure transformation const emailNormalizer = simplePlugin( { name: 'email-normalizer', version: '1.0.0' }, (user: UserWithEmail): UserWithNormalizedEmail => ({ ...user, email: user.email.toLowerCase().trim(), }), ); // āœ… GOOD: Composable transformations const userProcessingPipeline = createPluginManager<RawUser, ProcessedUser>() .register(emailNormalizer) .register(nameNormalizer) .register(phoneNormalizer) .build(); ``` #### **3. Error Isolation** Ensure errors in one plugin don't corrupt shared state. ```typescript class ResilientProcessorPlugin extends BasePlugin<Data, ProcessedData> { async execute(input: Data, context: PluginContext): Promise<ProcessedData> { const processingId = `process-${Date.now()}`; try { // Create isolated processing scope context.share(`${processingId}-status`, 'processing'); const result = await this.processData(input); // Mark as successful context.share(`${processingId}-status`, 'completed'); return result; } catch (error) { // Clean up any partial state this.cleanupProcessingState(context, processingId); context.share(`${processingId}-status`, 'failed'); throw error; } } private cleanupProcessingState(context: PluginContext, processingId: string): void { // Remove any partial state created during processing context.deleteShared(`${processingId}-temp-data`); context.deleteShared(`${processingId}-partial-results`); } } ``` #### **4. State Debugging and Inspection** ```typescript class StateInspectorPlugin extends BasePlugin<any, any> { execute(input: any, context: PluginContext): any { if (process.env.NODE_ENV === 'development') { this.logContextState(context); } return input; // Pass-through plugin } private logContextState(context: PluginContext): void { const sharedKeys = context.getSharedKeys(); context.logger.debug('Current context state', { executionId: context.executionId, sharedKeys: sharedKeys, sharedValues: sharedKeys.reduce( (acc, key) => { acc[key] = this.sanitizeForLogging(context.retrieve(key)); return acc; }, {} as Record<string, any>, ), }); } private sanitizeForLogging(value: any): any { // Remove sensitive data before logging if (value && typeof value === 'object') { const sanitized = { ...value }; delete sanitized.password; delete sanitized.token; delete sanitized.secret; return sanitized; } return value; } } ``` ### ⚔ Performance Considerations #### **Memory Management** ```typescript class MemoryEfficientPlugin extends BasePlugin<LargeDataSet, ProcessedDataSet> { async execute(input: LargeDataSet, context: PluginContext): Promise<ProcessedDataSet> { // Process in chunks to avoid memory issues const chunkSize = 1000; const results: ProcessedItem[] = []; for (let i = 0; i < input.items.length; i += chunkSize) { const chunk = input.items.slice(i, i + chunkSize); const processedChunk = await this.processChunk(chunk); results.push(...processedChunk); // Allow garbage collection between chunks if (i % (chunkSize * 10) === 0) { await new Promise(resolve => setImmediate(resolve)); } } return { items: results, processedAt: Date.now() }; } } ``` #### **Specialized Factory Methods** - Domain-Specific Patterns Our factory methods solve specific architectural challenges by providing opinionated, battle-tested patterns: ### šŸ” **Validation Factory** - Input Integrity Assurance Traditional validation often scatters throughout codebases, making it hard to maintain and test. The validation factory centralizes validation logic with consistent error handling. ```typescript // āœ… GOOD: Dedicated validation plugin const emailValidator = PluginFactory.validation( 'email-check', (email: string, context) => { // Comprehensive validation with context awareness if (!email || typeof email !== 'string') { throw new Error('Email must be a non-empty string'); } if (!email.includes('@')) { throw new Error('Email must contain @ symbol'); } if (!email.includes('.')) { throw new Error('Email must contain a domain'); } if (email.length > 254) { throw new Error('Email too long (max 254 characters)'); } // Context-aware logging context.logger.debug('Email validation passed', { email: email.substring(0, 3) + '***' }); return email.toLowerCase().trim(); // Normalize on successful validation }, { stopOnError: true, // Halt pipeline on validation failure description: 'RFC-compliant email validation with normalization', version: '1.2.0', }, ); ``` **Lifecycle Execution:** 1. **Pre-execution**: Validation plugins typically run early (high priority) 2. **Execution**: Immediate failure on invalid input prevents downstream processing 3. **Post-execution**: Normalized/sanitized data flows to next plugins 4. **Error handling**: Validation errors contain specific field information **Best Practices:** ```typescript // āœ… DO: Use descriptive validation names const userAgeValidator = PluginFactory.validation('user-age-range', validateAge); const passwordStrengthValidator = PluginFactory.validation('password-strength', validatePassword); // āœ… DO: Return normalized data const phoneValidator = PluginFactory.validation('phone-format', phone => { if (!isValidPhone(phone)) throw new Error('Invalid phone format'); return normalizePhoneNumber(phone); // Remove spaces, add country code }); // āœ… DO: Use context for conditional validation const businessHourValidator = PluginFactory.validation('business-hours', (request, context) => { const userTimezone = context.metadata.timezone; const currentHour = getCurrentHour(userTimezone); if (currentHour < 9 || currentHour > 17) { throw new Error('Service only available during business hours (9 AM - 5 PM)'); } return request; }); ``` **Common Mistakes to Avoid:** ```typescript // āŒ AVOID: Generic validation names const validator1 = PluginFactory.validation('validate', someFunction); const checker = PluginFactory.validation('check', someOtherFunction); // āŒ AVOID: Side effects in validation const badValidator = PluginFactory.validation('user-check', user => { if (!user.email) throw new Error('No email'); // BAD: Don't modify external state in validation updateUserLastSeen(user.id); // This is a side effect! sendWelcomeEmail(user.email); // This is business logic! return user; }); // āŒ AVOID: Swallowing validation errors const silentValidator = PluginFactory.validation('silent-check', data => { try { validateData(data); return data; } catch (error) { // BAD: Don't hide validation failures console.log('Validation failed, but continuing anyway'); return data; // This defeats the purpose of validation } }); ``` **Naming Convention:** - Format: `{domain}-{validation-type}-validator` - Examples: `email-format-validator`, `user-age-range-validator`, `password-strength-validator` --- ### šŸ”„ **Transform Factory** - Data Shape Evolution Data transformation is one of the most common operations in pipelines. The transform factory provides a clean pattern for shape changes, normalization, and enrichment. ```typescript // āœ… GOOD: Comprehensive transformation with error handling const userTransformer = PluginFactory.transform( 'user-normalizer', (rawUser: RawUserData, context) => { // Log transformation start context.logger.debug('Transforming user data', { userId: rawUser.id }); // Get transformation settings const settings = context.getSettings('user-normalizer'); const includeMetadata = settings?.config?.includeMetadata ?? true; const transformed: NormalizedUser = { // Required fields with fallbacks id: rawUser.user_id || rawUser.id, name: (rawUser.full_name || rawUser.name || '').trim(), email: (rawUser.email_address || rawUser.email || '').toLowerCase(), // Conditional transformations phone: rawUser.phone_number ? normalizePhoneNumber(rawUser.phone_number) : undefined, // Date transformations with validation createdAt: rawUser.created_timestamp ? new Date(rawUser.created_timestamp) : new Date(), // Nested object transformation profile: { firstName: extractFirstName(rawUser.full_name), lastName: extractLastName(rawUser.full_name), avatar: rawUser.profile_picture_url, preferences: transformUserPreferences(rawUser.settings), }, }; // Add metadata if enabled if (includeMetadata) { transformed.metadata = { transformedAt: new Date().toISOString(), transformVersion: '2.1.0', sourceFormat: rawUser._format || 'unknown', }; } // Validate transformed data if (!transformed.id || !transformed.email) { throw new Error('Transformation failed: missing required fields'); } context.logger.debug('User transformation completed', { userId: transformed.id, fieldsTransformed: Object.keys(transformed).length, }); return transformed; }, { dependencies: ['user-validator'], // Only transform valid data description: 'Transforms raw user data to normalized application format', version: '2.1.0', }, ); ``` **Lifecycle Execution:** 1. **Pre-execution**: Typically runs after validation but before business logic 2. **Execution**: Pure transformation - input → output mapping 3. **Post-execution**: Transformed data becomes input for downstream plugins 4. **Error handling**: Transformation errors indicate data quality issues **Best Practices:** ```typescript // āœ… DO: Make transformations pure and predictable const priceCalculator = PluginFactory.transform('price-calculator', order => ({ ...order, subtotal: calculateSubtotal(order.items), tax: calculateTax(order.items, order.region), total: calculateTotal(order.items, order.region), })); // āœ… DO: Handle missing/invalid data gracefully const addressNormalizer = PluginFactory.transform('address-normalizer', address => ({ street: address.street || '', city: address.city || '', state: address.state || address.province || '', country: normalizeCountryCode(address.country || 'US'), postalCode: normalizePostalCode(address.zip || address.postal_code || ''), })); // āœ… DO: Use configuration for behavior customization const currencyConverter = PluginFactory.transform( 'currency-converter', (amount, context) => { const targetCurrency = context.getSettings('currency-converter')?.config?.targetCurrency || 'USD'; const rate = getExchangeRate(amount.currency, targetCurrency); return { ...amount, originalValue: amount.value, originalCurrency: amount.currency, value: amount.value * rate, currency: targetCurrency, convertedAt: new Date().toISOString(), }; }, { description: 'Converts monetary amounts between currencies' }, ); ``` **Common Mistakes to Avoid:** ```typescript // āŒ AVOID: Mutating input data const badTransformer = PluginFactory.transform('bad-transform', user => { user.email = user.email.toLowerCase(); // BAD: Modifying input user.processedAt = new Date(); // BAD: Side effects return user; // BAD: Returning mutated input }); // āŒ AVOID: Complex business logic in transformers const overloadedTransformer = PluginFactory.transform('overloaded', order => { // BAD: This should be separate plugins sendConfirmationEmail(order.customerEmail); // Business logic updateInventory(order.items); // Side effect chargePaymentMethod(order.payment); // Business logic return { ...order, status: 'processed' }; }); // āŒ AVOID: Ignoring transformation errors const unsafeTransformer = PluginFactory.transform('unsafe', data => { try { return transformComplexData(data); } catch (error) { // BAD: Returning partial/corrupted data return { error: 'transformation failed' }; } }); ``` **Naming Convention:** - Format: `{domain}-{transformation-type}` - Examples: `user-normalizer`, `price-calculator`, `address-formatter`, `data-enricher` --- ### šŸ”— **Middleware Factory** - Cross-Cutting Concerns Cross-cutting concerns like logging, caching, authentication, and monitoring shouldn't be mixed with business logic. Middleware provides a clean separation. ```typescript // āœ… GOOD: Comprehensive middleware with proper concern separation const performanceMiddleware = PluginFactory.middleware( 'performance-monitor', async (data, context, next) => { const startTime = process.hrtime.bigint(); const memoryBefore = process.memoryUsage(); // Pre-processing: Setup monitoring const requestId = context.executionId; const dataSize = JSON.stringify(data).length; context.logger.info('Processing started', { requestId, dataSize, timestamp: new Date().toISOString(), }); try { // Execute the next plugin in the chain const result = await next(); // Post-processing: Collect metrics const endTime = process.hrtime.bigint(); const memoryAfter = process.memoryUsage(); const duration = Number(endTime - startTime) / 1_000_000; // Convert to milliseconds const metrics = { requestId, duration, memoryDelta: memoryAfter.heapUsed - memoryBefore.heapUsed, inputSize: dataSize, outputSize: JSON.stringify(result).length, success: true, }; // Store metrics for other plugins const allMetrics = context.retrieve<any[]>('performance-metrics') || []; allMetrics.push(metrics); context.share('performance-metrics', allMetrics); // Add performance warning if slow if (duration > 1000) { context.addWarning('SLOW_PROCESSING', `Processing took ${duration}ms`, metrics); } context.logger.info('Processing completed', metrics); return result; } catch (error) { // Error handling with metrics const endTime = process.hrtime.bigint(); const duration = Number(endTime - startTime) / 1_000_000; context.logger.error('Processing failed', { requestId, duration, error: error.message, stack: error.stack, }); // Re-throw to maintain error flow throw error; } }, { description: 'Monitors performance metrics and memory usage', version: '1.3.0', }, ); ``` **Lifecycle Execution:** 1. **Pre-execution**: Setup (logging, auth, caching checks) 2. **Delegation**: Call `next()` to execute wrapped plugins 3. **Post-execution**: Cleanup (logging, cache storage, metric collection) 4. **Error handling**: Intercept and augment error information **Best Practices:** ```typescript // āœ… DO: Keep middleware focused on single concerns const authMiddleware = PluginFactory.middleware('auth-guard', async (request, context, next) => { // Only handle authentication const user = await authenticateRequest(request); context.share('authenticated-user', user); return next(); }); const cacheMiddleware = PluginFactory.middleware('cache-layer', async (input, context, next) => { // Only handle caching const cacheKey = generateCacheKey(input); const cached = getFromCache(cacheKey); if (cached) return cached; const result = await next(); setInCache(cacheKey, result); return result; }); // āœ… DO: Always call next() unless intentionally stopping the pipeline const rateLimitMiddleware = PluginFactory.middleware( 'rate-limiter', async (request, context, next) => { const userId = request.userId; const isAllowed = await checkRateLimit(userId); if (!isAllowed) { throw new Error('Rate limit exceeded'); } // Always continue to next plugin return next(); }, ); // āœ… DO: Use middleware for monitoring and observability const tracingMiddleware = PluginFactory.middleware( 'request-tracer', async (data, context, next) => { const span = startSpan('plugin-execution', { pluginName: context.metadata.currentPlugin, executionId: context.executionId, }); try { const result = await next(); span.setStatus({ code: SpanStatusCode.OK }); return result; } catch (error) { span.setStatus({ code: SpanStatusCode.ERROR, message: error.message }); throw error; } finally { span.end(); } }, ); ``` **Common Mistakes to Avoid:** ```typescript // āŒ AVOID: Forgetting to call next() const brokenMiddleware = PluginFactory.middleware('broken', async (data, context, next) => { console.log('Processing data'); // BAD: Missing next() call - pipeline stops here return data; // This bypasses all subsequent plugins }); // āŒ AVOID: Modifying data in middleware (use transforms instead) const mutatingMiddleware = PluginFactory.middleware('mutating', async (data, context, next) => { data.processedAt = new Date(); // BAD: Modifying input const result = await next(); result.completedAt = new Date(); // BAD: Modifying output return result; }); // āŒ AVOID: Heavy business logic in middleware const businessLogicMiddleware = PluginFactory.middleware( 'business', async (order, context, next) => { // BAD: This should be separate plugins await calculateTaxes(order); await validateInventory(order); await processPayment(order); return next(); // Middleware should wrap, not replace business logic }, ); // āŒ AVOID: Swallowing errors without re-throwing const silentMiddleware = PluginFactory.middleware('silent', async (data, context, next) => { try { return await next(); } catch (error) { console.log('Error occurred:', error.message); // BAD: Not re-throwing - hides errors from pipeline return { error: 'Something went wrong' }; } }); ``` **Naming Convention:** - Format: `{concern}-{type}` or `{domain}-{concern}` - Examples: `auth-guard`, `cache-layer`, `request-logger`, `performance-monitor`, `error-tracker` --- ### šŸ—ļø **Problems These Factories Solve** **1. Code Organization**: Prevents plugin logic from being scattered across the codebase **2. Consistency**: Ensures similar plugins follow the same patterns **3. Testability**: Each factory creates easily testable, isolated units **4. Reusability**: Common patterns become reusable building blocks **5. Maintainability**: Changes to patterns only require factory updates **6. Type Safety**: Factory methods provide better TypeScript inference **7. Best Practices**: Built-in patterns prevent common anti-patterns **Benefits:** - āœ… Pre-configured for specific patterns - āœ… Built-in best practices and error handling - āœ… Reduced configuration overhead - āœ… Consistent behavior across similar plugins - āœ… Clear separation of concerns - āœ… Enhanced debugging and monitoring capabilities ### šŸ—ļø Builder Pattern for Complex Scenarios ```typescript const manager = createPluginBuilder<UserData, ProcessedUser>() .plugin(validationPlugin, { priority: 100 }) .plugin(transformPlugin, { priority: 50 }) .when(process.env.NODE_ENV === 'development', debugPlugin) .group({ tag: 'formatters', items: [{ plugin: dateFormatterPlugin }, { plugin: currencyFormatterPlugin }], }) .withPriority(200, criticalPlugin) .validate() .build(); ``` ## šŸ­ Creating Custom Factory Plugins The plugin system is extensible through custom factory methods. You can create specialized plugin factories that encapsulate domain-specific patterns, provide pre-configured behaviors, or implement complex plugin compositions. ### šŸŽÆ Why Create Custom Factories? Custom factories solve common development challenges: - **Domain-Specific Patterns**: Encapsulate business logic patterns specific to your application - **Configuration Standardization**: Enforce consistent plugin configurations across teams - **Complex Plugin Composition**: Simplify the creation of multi-plugin workflows - **Best Practices Enforcement**: Bake in error handling, logging, and monitoring patterns - **Developer Productivity**: Reduce boilerplate and speed up plugin development ### šŸ—ļø Factory Architecture Patterns #### **1. Static Factory Methods** Create utility classes with static factory methods for related plugin types. ```typescript /** * Database plugin factory for standardized database operations */ export class DatabasePluginFactory { /** * Creates a database query plugin with connection management */ static query<TQuery, TResult>( name: string, queryHandler: (query: TQuery, db: Database) => Promise<TResult>, options?: { version?: string; timeout?: number; retryAttempts?: number; enableQueryLogging?: boolean; }, ): IPlugin<TQuery, TResult> { return PluginFactory.create( { name: `${name}-query`, version: options?.version || '1.0.0', description: `Database query plugin: ${name}`, tags: ['database', 'query'], }, async (query, context) => { const db = context.retrieve<Database>('database-connection'); if (!db) { throw new Error('Database connection not available'); } const timeout = options?.timeout || 30000; const enableLogging = options?.enableQueryLogging ?? false; if (enableLogging) { context.logger.debug(`Executing query: ${name}`, { query }); } const startTime = Date.now(); try { const result = await Promise.race([ queryHandler(query, db), new Promise<never>((_, reject) => setTimeout(() => reject(new Error('Query timeout')), timeout), ), ]); if (enableLogging) { const duration = Date.now() - startTime; context.logger.debug(`Query completed: ${name}`, { duration }); } return result; } catch (error) { const duration = Date.now() - startTime; context.logger.error(`Query failed: ${name}`, { error: error.message, duration, }); throw error; } }, ); } /** * Creates a database transaction plugin with automatic rollback */ static transaction<TInput, TOutput>( name: string, transactionHandler: (input: TInput, tx: DatabaseTransaction) => Promise<TOutput>, options?: { version?: string; isolationLevel?: 'READ_COMMITTED' | 'REPEATABLE_READ' | 'SERIALIZABLE'; timeout?: number; }, ): IPlugin<TInput, TOutput> { return PluginFactory.create( { name: `${name}-transaction`, version: options?.version || '1.0.0', description: `Database transaction plugin: ${name}`, tags: ['database', 'transaction'], }, async (input, context) => { const db = context.retrieve<Database>('database-connection'); if (!db) { throw new Error('Database connection not available'); } const transaction = await db.beginTransaction({ isolationLevel: options?.isolationLevel || 'READ_COMMITTED', timeout: options?.timeout || 60000, }); try { context.logger.debug(`Starting transaction: ${name}`); const result = await transactionHandler(input, transaction); await transaction.commit(); context.logger.debug(`Transaction committed: ${name}`); return result; } catch (error) { await transaction.rollback(); context.logger.error(`Transaction rolled back: ${name}`, { error: error.message, }); throw error; } }, ); } /** * Creates a database migration plugin */ static migration( name: string, migrations: Array<{ version: string; up: (db: Database) => Promise<void>; down: (db: Database) => Promise<void>; }>, options?: { version?: string; checkOnly?: boolean; }, ): IPlugin<void, MigrationResult> { return PluginFactory.create( { name: `${name}-migration`, version: options?.version || '1.0.0', description: `Database migration plugin: ${name}`, tags: ['database', 'migration', 'setup'], }, async (_, context) => { const db = context.retrieve<Database>('database-connection'); if (!db) { throw new Error('Database connection not available'); } const migrationResults: Array<{ version: string; status: 'applied' | 'skipped' | 'failed'; error?: string; }> = []; for (const migration of migrations) { try { const isApplied = await db.isMigrationApplied(migration.version); if (isApplied) { migrationResults.push({ version: migration.version, status: 'skipped', }); continue; } if (options?.checkOnly) { migrationResults.push({ version: migration.version, status: 'skipped', }); continue; } context.logger.info(`Applying migration: ${migration.version}`); await migration.up(db); await db.markMigrationApplied(migration.version); migrationResults.push({ version: migration.version, status: 'applied', }); } catch (error) { context.logger.error(`Migration failed: ${migration.version}`, { error: error.message, }); migrationResults.push({ version: migration.version, status: 'failed', error: error.message, }); if (!options?.checkOnly) { throw error; // Fail fast on migration errors } } } return { totalMigrations: migrations.length, applied: migrationResults.filter(r => r.status === 'applied').length, skipped: migrationResults.filter(r => r.status === 'skipped').length, failed: migrationResults.filter(r => r.status === 'failed').length, results: migrationResults, }; }, ); } } ``` #### **2. Domain-Specific Factory Classes** Create factories for specific business domains with rich functionality. ```typescript /** * API client factory for consistent external service integration */ export class ApiClientFactory { /** * Creates a RESTful API client plugin with built-in retry and error handling */ static restClient<TRequest, TResponse>( name: string, config: { baseUrl: string; defaultHeaders?: Record<string, string>; timeout?: number; retryAttempts?: number; retryDelay?: number; authProvider?: (context: PluginContext) => Promise<string>; }, ): IPlugin<TRequest & { endpoint: string; method: string }, TResponse> { return PluginFactory.withRetry( name, async (request, context) => { const headers = { ...config.defaultHeaders }; // Add authentication if provider exists if (config.authProvider) { const authToken = await config.authProvider(context); headers['Authorization'] = `Bearer ${authToken}`; } const url = `${config.baseUrl}${request.endpoint}`; const startTime = Date.now(); context.logger.debug(`API Request: ${request.method} ${url}`); const response = await fetch(url, { method: request.method, headers: { 'Content-Type': 'application/json', ...headers, }, body: request.method !== 'GET' ? JSON.stringify(request) : undefined, signal: AbortSignal.timeout(config.timeout || 30000), }); const duration = Date.now() - startTime; if (!response.ok) { const error = new Error(`API Error: ${response.status} ${response.statusText}`); context.logger.error(`API Request failed: ${request.method} ${url}`, { status: response.status, duration, }); throw error; } const result = await response.json(); context.logger.debug(`API Request completed: ${request.method} ${url}`, { status: response.status, duration, }); // Track API metrics const metrics = context.retrieve<ApiMetrics>('api-metrics') || { totalRequests: 0, successfulRequests: 0, failedRequests: 0, totalDuration: 0, }; metrics.totalRequests++; metrics.successfulRequests++; metrics.totalDuration += duration; context.share('api-metrics', metrics); return result; }, { maxAttempts: config.retryAttempts || 3, backoffMs: config.retryDelay || 1000, retryOn: error => { // Retry on network errors but not on client errors (4xx) return !error.message.includes('4'); }, }, ); } /** * Creates a GraphQL client plugin */ static graphqlClient<TVariables, TResponse>( name: string, config: { endpoint: string; defaultHeaders?: Record<string, string>; timeout?: number; authProvider?: (context: PluginContext) => Promise<string>; }, ): IPlugin<{ query: string; variables?: TVariables }, TResponse> { return PluginFactory.create( { name: `${name}-graphql`, version: '1.0.0', description: `GraphQL client plugin: ${name}`, tags: ['api', 'graphql', 'client'], }, async (request, context) => { const headers = { ...config.defaultHeaders }; if (config.authProvider) { const authToken = await config.authProvider(context); headers['Authorization'] = `Bearer ${authToken}`; } const response = await fetch(config.endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', ...headers, }, body: JSON.stringify({ query: request.query, variables: request.variables, }), signal: AbortSignal.timeout(config.timeout || 30000), }); const result = await response.json(); if (result.errors) { throw new Error(`GraphQL errors: ${JSON.stringify(result.errors)}`); } return result.data; }, ); } } ``` #### **3. Workflow Factory Patterns** Create factories that combine multiple plugins into complete workflows. ```typescript /** * Data processing workflow factory */ export class DataWorkflowFactory { /** * Creates a complete ETL (Extract, Transform, Load) workflow */ static etlWorkflow<TSource, TTransformed, TDestination>( name: string, config: { extractor: (source: TSource, context: PluginContext) => Promise<any[]>; transformer: (item: any, context: PluginContext) => Promise<TTransformed>; loader: (items: TTransformed[], context: PluginContext) => Promise<TDestination>; batchSize?: number; enableParallel?: boolean; errorHandling?: 'fail-fast' | 'continue' | 'collect-errors'; }, ): IPlugin<TSource, TDestination> { return PluginFactory.create( { name: `${name}-etl`, version: '1.0.0', description: `ETL workflow plugin: ${name}`, tags: ['etl', 'workflow', 'data-processing'], }, async (source, context) => { const batchSize = config.batchSize || 100; const enableParallel = config.enablePara