Middleware Composition
This document provides a detailed reference for middleware composition utilities in the ReX content system. These utilities enable you to combine multiple middleware functions into cohesive pipelines.
Core Composition Functions
composeMiddleware
function composeMiddleware(...middleware: Middleware[]): Middleware
Combines multiple middleware functions into a single pipeline, executing them in the provided order.
Parameters:
...middleware
: A list of middleware functions to compose
Returns:
- A composed middleware function that represents the entire pipeline
Example:
import { composeMiddleware } from '@lib/content/middleware'
import {
loggingMiddleware,
cacheMiddleware,
validationMiddleware,
} from '@lib/content/middleware'
const pipeline = composeMiddleware(
loggingMiddleware({ level: 'info' }),
cacheMiddleware({ ttl: 3600 }),
validationMiddleware({ schema })
)
// Apply the composed middleware
const enhancedStore = createContentStore({
adapter,
middleware: pipeline,
})
Implementation Details:
- Processes middleware in order, but with bidirectional flow
- Each middleware can modify the context before and after the next middleware executes
- Provides a clean, functional composition approach
pipe
function pipe<T>(base: T, ...enhancers: Array<(input: T) => T>): T
Applies a series of enhancer functions to a base value, useful for creating enhanced content stores.
Parameters:
base
: The base value to enhance...enhancers
: Functions that transform the base value
Returns:
- The enhanced value after applying all enhancers
Example:
import { pipe } from '@lib/content/middleware'
const enhancedStore = pipe(
baseStore,
withLogging({ level: 'info' }),
withCaching({ ttl: 3600 }),
withValidation({ schema })
)
// The enhancedStore has all middleware applied
Implementation Details:
- Right-to-left function application (last enhancer is applied first)
- Each enhancer receives the result of the previous enhancer
- Pure functional approach with no side effects
Conditional Composition
conditionalMiddleware
function conditionalMiddleware(
condition: (context: ContentContext) => boolean,
middleware: Middleware
): Middleware
Creates middleware that only applies when a specific condition is met.
Parameters:
condition
: Function that determines if the middleware should be appliedmiddleware
: The middleware to conditionally apply
Returns:
- A middleware function that conditionally executes the provided middleware
Example:
import { conditionalMiddleware } from '@lib/content/middleware'
// Only apply markdown processing to markdown files
const markdownOnlyMiddleware = conditionalMiddleware(
context => context.uri.endsWith('.md'),
markdownProcessingMiddleware
)
// Only apply caching to read operations
const readCachingMiddleware = conditionalMiddleware(
context => context.operation === 'read',
cacheMiddleware({ ttl: 3600 })
)
Implementation Details:
- Evaluates condition before executing middleware
- Bypasses middleware when condition is false
- Passes context unmodified when bypassing
onlyForOperation
function onlyForOperation(
operations: Array<'read' | 'write' | 'delete' | 'list' | 'watch'> | string,
middleware: Middleware
): Middleware
Creates middleware that only applies to specific operation types.
Parameters:
operations
: Array of operation types or a single operation typemiddleware
: The middleware to conditionally apply
Returns:
- A middleware function that only executes for the specified operations
Example:
import { onlyForOperation } from '@lib/content/middleware'
// Apply caching only for read and list operations
const cachingMiddleware = onlyForOperation(
['read', 'list'],
cacheMiddleware({ ttl: 3600 })
)
// Apply validation only for write operations
const validationMiddleware = onlyForOperation(
'write',
validationMiddleware({ schema })
)
Implementation Details:
- Simple wrapper around conditionalMiddleware
- Checks operation type in context
- Supports arrays for multiple operation types
onlyForContentType
function onlyForContentType(
contentTypes: string[] | string,
middleware: Middleware
): Middleware
Creates middleware that only applies to specific content types.
Parameters:
contentTypes
: Array of content types or a single content typemiddleware
: The middleware to conditionally apply
Returns:
- A middleware function that only executes for the specified content types
Example:
import { onlyForContentType } from '@lib/content/middleware'
// Process only markdown content
const markdownMiddleware = onlyForContentType(
'text/markdown',
markdownProcessingMiddleware
)
// Apply specific transformation to certain content types
const transformMiddleware = onlyForContentType(
['text/markdown', 'text/mdx', 'text/html'],
htmlSanitizationMiddleware
)
Implementation Details:
- Checks content type in context
- Only applies for write operations or after content is loaded in read operations
- Supports arrays for multiple content types
onlyInEnvironment
function onlyInEnvironment(
environment: 'node' | 'browser' | 'serviceworker' | string,
middleware: Middleware
): Middleware
Creates middleware that only applies in specific JavaScript environments.
Parameters:
environment
: The target environmentmiddleware
: The middleware to conditionally apply
Returns:
- A middleware function that only executes in the specified environment
Example:
import { onlyInEnvironment } from '@lib/content/middleware'
// Apply filesystem caching only in Node.js
const nodeCachingMiddleware = onlyInEnvironment(
'node',
filesystemCacheMiddleware
)
// Apply IndexedDB storage only in browsers
const browserStorageMiddleware = onlyInEnvironment(
'browser',
indexedDBStorageMiddleware
)
Implementation Details:
- Uses environment detection from
@lib/utils/env
- Condition is evaluated at runtime
- Works in isomorphic code that runs in multiple environments
Utility Composition Functions
tapMiddleware
function tapMiddleware(
fn: (context: ContentContext) => void | Promise<void>
): Middleware
Creates middleware that performs a side effect without modifying the context.
Parameters:
fn
: Function that performs side effects with the context
Returns:
- A middleware function that performs the side effect and passes context unmodified
Example:
import { tapMiddleware } from '@lib/content/middleware'
// Log operation details without modifying the pipeline
const loggingTap = tapMiddleware(context => {
console.log(`Operation: ${context.operation}, URI: ${context.uri}`)
})
// Track operation metrics
const metricsTap = tapMiddleware(async context => {
await metrics.increment(`content.${context.operation}`)
})
Implementation Details:
- Executes side effect before calling next middleware
- Does not modify context before or after execution
- Awaits the side effect function if it returns a Promise
catchMiddleware
function catchMiddleware(
errorHandler: (
error: Error,
context: ContentContext
) => Promise<ContentContext> | ContentContext
): Middleware
Creates middleware that catches and handles errors from downstream middleware.
Parameters:
errorHandler
: Function that handles errors and can recover the operation
Returns:
- A middleware function that catches errors from later middleware
Example:
import { catchMiddleware } from '@lib/content/middleware'
// Recover from content not found errors
const notFoundHandler = catchMiddleware((error, context) => {
if (error instanceof ContentNotFoundError) {
console.warn(`Content not found: ${context.uri}, using fallback`)
// Provide fallback content
context.content = {
data: '# Not Found\n\nThe requested content was not found.',
contentType: 'text/markdown',
metadata: { title: 'Not Found' },
}
return context
}
// Rethrow other errors
throw error
})
Implementation Details:
- Catches errors from downstream middleware
- Can recover by returning a modified context
- Can rethrow the error or throw a different error
timingMiddleware
function timingMiddleware(options?: {
key?: string
threshold?: number
logger?: (message: string, data: any) => void
}): Middleware
Creates middleware that measures operation execution time.
Parameters:
options
: Configuration optionskey
: State key for storing timing information (default: ‘timing’)threshold
: Log warning if operation exceeds threshold in ms (default: 1000)logger
: Function for logging timing information (default: console.log)
Returns:
- A middleware function that records operation timing
Example:
import { timingMiddleware } from '@lib/content/middleware'
// Basic timing middleware
const timing = timingMiddleware()
// Custom threshold and logger
const performanceTiming = timingMiddleware({
threshold: 500,
logger: (message, data) => {
metrics.recordTiming('content_operation', data.duration)
if (data.duration > 500) {
console.warn(message, data)
}
},
})
Implementation Details:
- Records start time before downstream middleware
- Measures duration after downstream middleware completes
- Stores timing information in context state
Middleware Factory Pattern
createMiddlewareFactory
function createMiddlewareFactory<Options = any>(
factoryFn: (options?: Options) => Middleware
): (options?: Options) => Middleware
Creates a standardized middleware factory with consistent error handling and option defaults.
Parameters:
factoryFn
: Function that creates middleware based on options
Returns:
- A middleware factory function with enhanced error handling
Example:
import { createMiddlewareFactory } from '@lib/content/middleware'
// Create a validation middleware factory
const withValidation = createMiddlewareFactory((options = {}) => {
const { schema, failOnError = true } = options
if (!schema) {
throw new Error('Schema is required for validation middleware')
}
return async (context, next) => {
// Validation implementation
if (context.content) {
const validationResult = schema.validate(context.content)
if (!validationResult.valid && failOnError) {
throw new ContentValidationError(
`Content validation failed for ${context.uri}`,
{ details: validationResult.errors }
)
}
// Store validation results in context
context.state.validationResult = validationResult
}
return next()
}
})
// Usage
const validationMiddleware = withValidation({
schema: contentSchema,
})
Implementation Details:
- Standardizes option handling
- Provides error handling for middleware creation
- Enforces consistent middleware function signature
Advanced Composition Techniques
composeMiddlewareWithContext
function composeMiddlewareWithContext(
...middleware: Middleware[]
): (initialContext: ContentContext) => Promise<ContentContext>
Creates a function that executes a middleware pipeline with a provided initial context.
Parameters:
...middleware
: A list of middleware functions to compose
Returns:
- A function that takes an initial context and returns the final context
Example:
import { composeMiddlewareWithContext } from '@lib/content/middleware'
// Create a pipeline executor
const executePipeline = composeMiddlewareWithContext(
loggingMiddleware(),
validationMiddleware({ schema }),
transformationMiddleware()
)
// Execute with custom context
const result = await executePipeline({
operation: 'read',
uri: 'content/article.md',
content: {
data: '# Article',
contentType: 'text/markdown',
metadata: { title: 'Article' },
},
state: {},
})
Implementation Details:
- Executes middleware pipeline with provided context
- Does not require a base operation
- Useful for testing and composition without an adapter
branchMiddleware
function branchMiddleware(
condition: (context: ContentContext) => boolean,
trueBranch: Middleware,
falseBranch: Middleware
): Middleware
Creates middleware that branches between two different middleware paths based on a condition.
Parameters:
condition
: Function that determines which branch to followtrueBranch
: Middleware to execute when condition is truefalseBranch
: Middleware to execute when condition is false
Returns:
- A middleware function that selects between two execution paths
Example:
import { branchMiddleware } from '@lib/content/middleware'
// Use different caching strategies based on content size
const intelligentCachingMiddleware = branchMiddleware(
context => {
const size = context.content?.data?.length || 0
return size > 100000 // Large content
},
compressionMiddleware(), // For large content
inMemoryCacheMiddleware() // For small content
)
Implementation Details:
- Evaluates condition at runtime
- Executes only one branch
- Useful for implementing complex middleware logic
Performance Considerations
Optimizing Middleware Composition
Minimize Middleware Count: Each middleware adds overhead to operations
typescript// Prefer combining related concerns const loggingMiddleware = createMiddlewareFactory(options => { // Handle both logging and metrics in one middleware return async (context, next) => { // Log operation // Track metrics return next() } })
Early Short-Circuit: Position short-circuiting middleware first
typescript// Position cache early to avoid unnecessary processing const pipeline = composeMiddleware( cacheMiddleware({ ttl: 3600 }), // May return immediately loggingMiddleware(), validationMiddleware() )
Conditional Execution: Use conditional middleware to avoid unnecessary processing
typescript// Only validate write operations const optimalValidation = onlyForOperation( 'write', validationMiddleware({ schema }) )
Batch Context Modifications: Avoid multiple context modifications
typescript// Inefficient: multiple state modifications const inefficientMiddleware = async (context, next) => { context.state.timestamp = Date.now() context.state.requestId = generateId() context.state.env = getEnvironment() return next() } // Efficient: single state modification const efficientMiddleware = async (context, next) => { // Modify state once Object.assign(context.state, { timestamp: Date.now(), requestId: generateId(), env: getEnvironment(), }) return next() }
Best Practices
General Composition Guidelines
Consistent Ordering: Maintain consistent middleware ordering across stores
typescript// Define a standard middleware order function createStandardMiddleware(options = {}) { return composeMiddleware( // Observability (always first) loggingMiddleware(options.logging), metricsMiddleware(options.metrics), // Performance optimization cacheMiddleware(options.cache), batchingMiddleware(options.batching), // Validation and security validationMiddleware(options.validation), sanitizationMiddleware(options.sanitization), // Content transformation (closest to adapter) transformationMiddleware(options.transforms) ) }
Error Propagation: Handle errors consistently
typescript// Wrap critical middleware with error handling const robustMiddleware = composeMiddleware( catchMiddleware((error, context) => { // Log error logger.error(`Operation failed: ${context.operation}`, error) // Convert to standard error type if (!(error instanceof ContentError)) { throw new ContentError(`Unexpected error: ${error.message}`, { cause: error, }) } throw error }), criticalMiddleware() )
Documentation: Document middleware dependencies and side effects
typescript/** * Validates content against a schema * * @dependencies Requires transformationMiddleware to run AFTER this middleware * @sideEffects Sets context.state.validationResult */ function validationMiddleware(options = {}) { // Implementation }
Testing: Test middleware in isolation and in composition
typescript// Test middleware in isolation test('validation middleware rejects invalid content', async () => { const middleware = validationMiddleware({ schema }) const context = { content: invalidContent, state: {} } await expect(middleware(context, async () => context)).rejects.toThrow( ContentValidationError ) }) // Test middleware composition test('middleware pipeline processes content correctly', async () => { const pipeline = composeMiddlewareWithContext( validationMiddleware({ schema }), transformationMiddleware() ) const result = await pipeline({ content: validContent, state: {} }) expect(result.content).toMatchObject(expectedTransformedContent) })