Skip to content

Core Concepts

Understanding the core design philosophy of farrow-pipeline.

🧅 Onion Model - The Soul of Pipeline

Remember your experience peeling onions? (Hope you didn't cry 😢) Pipeline's execution model is like an onion, nested layer by layer:

Outer → Middle → Inner → Inner returns → Middle returns → Outer returns

In code, it looks like this:

typescript
const pipeline = createPipeline<number, number>()

pipeline.use((input, next) => {
  console.log('🧅 Outer: Enter')
  const result = next(input)
  console.log('🧅 Outer: Exit')
  return result
})

pipeline.use((input, next) => {
  console.log('  🧅 Middle: Enter')
  const result = next(input)
  console.log('  🧅 Middle: Exit')
  return result
})

pipeline.use((input) => {
  console.log('    🧅 Inner: Reached core')
  return input * 2
})

pipeline.run(5)

Output order:

🧅 Outer: Enter
  🧅 Middle: Enter
    🧅 Inner: Reached core
  🧅 Middle: Exit
🧅 Outer: Exit

💡 Why this design? The onion model lets you execute code before AND after processing. For example: log before request, calculate time after request. Perfect!

🧱 Middleware - The Building Blocks of Pipeline

Each middleware is a brick, receiving two parameters:

typescript
type Middleware<I, O> = (
  input: I,      // Current input
  next: Next<I, O>  // Call next middleware
) => O

type Next<I, O> = (input?: I) => O

5 Common Brick Patterns:

1. 🔄 Transformation - Change Input

typescript
pipeline.use((input, next) => {
  const transformed = transform(input)
  return next(transformed)  // Pass new value to next
})

2. ✨ Enhancement - Process Result

typescript
pipeline.use((input, next) => {
  const result = next(input)
  return addSomeSpice(result)  // Add flavor to result
})

3. 🚧 Interception - Conditional Execution

typescript
pipeline.use((input, next) => {
  if (!isValid(input)) {
    return 'Get out!'  // Return directly, don't call next
  }
  return next(input)  // Let through
})

4. 🎁 Wrapper - Before and After Processing

typescript
pipeline.use((input, next) => {
  console.log('Before')
  const result = next(input)
  console.log('After')
  return result
})

5. 🛑 Termination - Last Stop

typescript
pipeline.use((input) => {
  return 'This is the end!'  // Don't call next
})

Execution Flow Details

Complete Execution Example

typescript
const pipeline = createPipeline<number, string>()

// Middleware 1: Validation
pipeline.use((input, next) => {
  console.log('1️⃣ Validate input:', input)
  if (input < 0) {
    return 'Error: Negative number'  // Early return, don't call next
  }
  return next(input)
})

// Middleware 2: Transformation
pipeline.use((input, next) => {
  console.log('2️⃣ Transform:', input)
  const doubled = input * 2
  return next(doubled)
})

// Middleware 3: Formatting
pipeline.use((input, next) => {
  console.log('3️⃣ Format:', input)
  const result = next(`Number: ${input}`)
  console.log('3️⃣ Formatting complete')
  return result
})

// Middleware 4: Final processing
pipeline.use((input) => {
  console.log('4️⃣ Final processing:', input)
  return input.toUpperCase()
})

// Execute
pipeline.run(5)

Output:

1️⃣ Validate input: 5
2️⃣ Transform: 5
3️⃣ Format: 10
4️⃣ Final processing: Number: 10
3️⃣ Formatting complete

Interrupt Execution

typescript
const pipeline = createPipeline<number, string>()

pipeline.use((input, next) => {
  if (input > 100) {
    return 'Too big!'  // Return directly, subsequent middleware won't execute
  }
  return next(input)
})

pipeline.use((input) => {
  return `Value: ${input}`  // Only executes when input <= 100
})

console.log(pipeline.run(50))   // "Value: 50"
console.log(pipeline.run(200))  // "Too big!"

Type System

Benefits of Type Safety

typescript
// ✅ Correct: Types match
const pipeline = createPipeline<number, string>()
  .use((x: number, next) => next(x + 1))     // number → number
  .use((x: number) => `Result: ${x}`)          // number → string

// ❌ Error: Types don't match
const pipeline = createPipeline<number, string>()
  .use((x: number, next) => next('wrong'))    // Type error! next expects number
  .use((x: number) => x * 2)                 // Type error! Should return string

Type Inference

typescript
const pipeline = createPipeline<number, string>()

pipeline.use((input, next) => {
  // input automatically inferred as number
  // next parameter automatically inferred as number
  // Return value must be string or next's return value
  return next(input)
})

How the Next Function Works

What happens when you call next()?

typescript
pipeline.use((input, next) => {
  console.log('Before calling next')
  const result = next(input)  // Execute all subsequent middleware
  console.log('After calling next')
  return result
})

Modify value passed to next middleware

typescript
pipeline.use((input, next) => {
  // Pass modified value
  return next(input * 2)
})

pipeline.use((input, next) => {
  // Pass different type (if types allow)
  return next(input.toString())
})

Consequences of not calling next

typescript
pipeline.use((input, next) => {
  // If you don't call next, subsequent middleware won't execute
  if (shouldStop(input)) {
    return 'Stop'
  }
  return next(input)  // Only continue when condition is met
})

Practical Patterns

Pattern 1: Logging Wrapper

typescript
function withLogging<I, O>(
  middleware: Middleware<I, O>,
  name: string
): Middleware<I, O> {
  return (input, next) => {
    console.log(`[${name}] Input:`, input)
    const result = middleware(input, next)
    console.log(`[${name}] Output:`, result)
    return result
  }
}

// Usage
pipeline.use(
  withLogging((input, next) => next(input * 2), 'Double')
)

Pattern 2: Error Handling Wrapper

typescript
function withErrorHandling<I, O>(
  middleware: Middleware<I, O>,
  fallback: O
): Middleware<I, O> {
  return (input, next) => {
    try {
      return middleware(input, next)
    } catch (error) {
      console.error('Middleware error:', error)
      return fallback
    }
  }
}

// Usage
pipeline.use(
  withErrorHandling(
    (input, next) => {
      if (input < 0) throw new Error('Negative number')
      return next(input)
    },
    'Error occurred'
  )
)

Pattern 3: Conditional Middleware

typescript
function conditional<I, O>(
  condition: (input: I) => boolean,
  middleware: Middleware<I, O>
): Middleware<I, O> {
  return (input, next) => {
    if (condition(input)) {
      return middleware(input, next)
    }
    return next(input)
  }
}

// Usage
pipeline.use(
  conditional(
    (x) => x > 10,
    (x, next) => next(x * 2)
  )
)

Core Principles Summary

  • ✅ Middleware must return a value
  • ✅ Call next(input) to continue to next middleware
  • ✅ Not calling next() interrupts the execution chain
  • ✅ Onion model: Pre-processing → Call next → Post-processing
  • ✅ Type safety: Input/output types constrained by generics

This is a third-party Farrow documentation site | Built with ❤️ and TypeScript