Skip to content

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

typescript
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:

typescript
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

typescript
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:

typescript
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

typescript
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 applied
  • middleware: The middleware to conditionally apply

Returns:

  • A middleware function that conditionally executes the provided middleware

Example:

typescript
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

typescript
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 type
  • middleware: The middleware to conditionally apply

Returns:

  • A middleware function that only executes for the specified operations

Example:

typescript
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

typescript
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 type
  • middleware: The middleware to conditionally apply

Returns:

  • A middleware function that only executes for the specified content types

Example:

typescript
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

typescript
function onlyInEnvironment(
  environment: 'node' | 'browser' | 'serviceworker' | string,
  middleware: Middleware
): Middleware

Creates middleware that only applies in specific JavaScript environments.

Parameters:

  • environment: The target environment
  • middleware: The middleware to conditionally apply

Returns:

  • A middleware function that only executes in the specified environment

Example:

typescript
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

typescript
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:

typescript
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

typescript
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:

typescript
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

typescript
function timingMiddleware(options?: {
  key?: string
  threshold?: number
  logger?: (message: string, data: any) => void
}): Middleware

Creates middleware that measures operation execution time.

Parameters:

  • options: Configuration options
    • key: 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:

typescript
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

typescript
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:

typescript
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

typescript
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:

typescript
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

typescript
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 follow
  • trueBranch: Middleware to execute when condition is true
  • falseBranch: Middleware to execute when condition is false

Returns:

  • A middleware function that selects between two execution paths

Example:

typescript
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

  1. 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()
      }
    })
  2. 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()
    )
  3. Conditional Execution: Use conditional middleware to avoid unnecessary processing

    typescript
    // Only validate write operations
    const optimalValidation = onlyForOperation(
      'write',
      validationMiddleware({ schema })
    )
  4. 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

  1. 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)
      )
    }
  2. 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()
    )
  3. 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
    }
  4. 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)
    })

Released under the MIT License.