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
Markdown
# š 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.
[](https://badge.fury.io/js/harmony-plugin-manager)
[](https://www.typescriptlang.org/)
[](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