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 returnsIn 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) => O5 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 completeInterrupt 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 stringType 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
