Context System
Context provides request-level global state management, allowing you to elegantly share data between middleware.
🤔 Why Do We Need Context?
Imagine this scenario: you have 3 middleware, the 1st gets user information, the 3rd needs this information. Traditional approach?
typescript
// ❌ Approach 1: Global variables (dangerous! multiple requests will contaminate each other)
let currentUser = null
middleware1(() => {
currentUser = getUser()
})
middleware3(() => {
console.log(currentUser) // Might be someone else's user!
})
// ❌ Approach 2: Pass through layers (disgusting!)
middleware1((input, next) => {
const user = getUser()
return next({ ...input, user }) // Pass down
})Context's solution:
typescript
import { createContext } from 'farrow-pipeline'
// Create a user context
const UserContext = createContext<User | null>(null)
// 1st middleware: Set user
pipeline.use((req, next) => {
const user = authenticate(req)
UserContext.set(user) // 🎯 Set!
return next(req)
})
// 3rd middleware: Get user
pipeline.use((req) => {
const user = UserContext.get() // 🎯 Get!
return { user, data: processRequest(req) }
})✨ Magic principle: Each
pipeline.run()creates an independent container, different requests don't interfere with each other. Concurrency safe!
Context Basic Operations
Creating Context
typescript
import { createContext } from 'farrow-pipeline'
// Create Context with default value
const UserContext = createContext<User | null>(null)
const CounterContext = createContext<number>(0)
const ConfigContext = createContext({ debug: false })
// Generic parameter specifies type
const LoggerContext = createContext<{
log: (msg: string) => void
error: (msg: string) => void
}>({
log: console.log,
error: console.error
})Getting Value
typescript
// Get current value (might be default value)
const user = UserContext.get()
// Get and use
pipeline.use((input) => {
const user = UserContext.get()
if (user) {
console.log(`Current user: ${user.name}`)
}
return input
})Setting Value
typescript
// Set new value
UserContext.set(newUser)
// Set in middleware
pipeline.use((input, next) => {
const user = authenticate(input)
UserContext.set(user)
return next(input)
})Assert Non-null
typescript
// Assert value exists (throws error if null/undefined)
const user = UserContext.assert()
// Usage scenario
pipeline.use((input) => {
// Assume previous middleware has set user
const user = UserContext.assert() // Throws exception if null
return processForUser(user, input)
})🎯 Practical Examples
Example 1: Multi-context Collaboration
typescript
import { createContext, createPipeline } from 'farrow-pipeline'
type User = { id: string; name: string }
type Logger = { log: (msg: string) => void }
// Create multiple contexts
const UserContext = createContext<User | null>(null)
const RequestIdContext = createContext<string>('')
const LoggerContext = createContext<Logger>({ log: console.log })
const pipeline = createPipeline<Request, Response>()
// Middleware 1: Initialize contexts
pipeline.use((req, next) => {
UserContext.set(authenticate(req))
RequestIdContext.set(generateId())
LoggerContext.set(createLogger())
return next(req)
})
// Middleware 2: Use contexts
pipeline.use((req) => {
const user = UserContext.get()
const requestId = RequestIdContext.get()
const logger = LoggerContext.get()
logger.log(`[${requestId}] User ${user?.name} made a request`)
return { user, requestId, data: '...' }
})Example 2: Request Tracking
typescript
const TraceContext = createContext<{
traceId: string
startTime: number
steps: string[]
}>({
traceId: '',
startTime: 0,
steps: []
})
const tracePipeline = createPipeline<Request, Response>()
// Initialize tracking
tracePipeline.use((req, next) => {
TraceContext.set({
traceId: generateTraceId(),
startTime: Date.now(),
steps: []
})
return next(req)
})
// Record steps
function addStep(name: string) {
const trace = TraceContext.get()
trace.steps.push(`${name}: ${Date.now() - trace.startTime}ms`)
}
// Business middleware
tracePipeline.use((req, next) => {
addStep('Auth start')
const user = authenticate(req)
addStep('Auth complete')
return next(req)
})
tracePipeline.use((req) => {
addStep('Process start')
const result = processRequest(req)
addStep('Process complete')
const trace = TraceContext.get()
return {
result,
trace: {
id: trace.traceId,
duration: Date.now() - trace.startTime,
steps: trace.steps
}
}
})Example 3: Database Connection Management
typescript
interface Database {
query: (sql: string) => Promise<any>
close: () => Promise<void>
}
const DBContext = createContext<Database | null>(null)
const dbPipeline = createAsyncPipeline<Request, Response>()
// Open database connection
dbPipeline.use(async (req, next) => {
const db = await openDatabase()
DBContext.set(db)
try {
return await next(req)
} finally {
await db.close() // Ensure connection is closed
}
})
// Use database
dbPipeline.use(async (req) => {
const db = DBContext.assert() // Assert database is connected
const users = await db.query('SELECT * FROM users')
return { users }
})🔒 Context Isolation
Important feature: Each pipeline.run() has an independent container!
typescript
const CounterContext = createContext(0)
const pipeline = createPipeline<string, string>()
.use((input, next) => {
const count = CounterContext.get()
CounterContext.set(count + 1)
return next(`${input}:${CounterContext.get()}`)
})
// Concurrent execution, each starts counting from 0
await Promise.all([
pipeline.run('A'), // "A:1"
pipeline.run('B'), // "B:1" (not "B:2"!)
pipeline.run('C') // "C:1" (not "C:3"!)
])Isolation Demo
typescript
const UserContext = createContext<string>('Anonymous')
const pipeline = createPipeline<string, string>()
pipeline.use((input, next) => {
UserContext.set(input)
return next(input)
})
pipeline.use((input, next) => {
// Simulate async operation
setTimeout(() => {
console.log(`User: ${UserContext.get()}`)
}, 100)
return next(input)
})
pipeline.use((input) => {
return `Processing complete: ${input}`
})
// Quick successive calls
pipeline.run('Alice')
pipeline.run('Bob')
pipeline.run('Charlie')
// Output (order might vary, but values are isolated):
// User: Alice
// User: Bob
// User: CharlieAdvanced Usage
Custom Context Hook
typescript
// Create custom hook
function useUser() {
const user = UserContext.get()
if (!user) {
throw new Error('User not logged in')
}
return user
}
// Usage
pipeline.use((req) => {
const user = useUser() // Automatically assert user exists
return processForUser(user, req)
})Context Composition
typescript
interface AppContext {
user: User | null
db: Database
logger: Logger
}
// Create composed contexts
const AppContexts = {
user: createContext<User | null>(null),
db: createContext<Database | null>(null),
logger: createContext<Logger>(console)
}
// Initialize all contexts
function initializeContext(user: User, db: Database, logger: Logger) {
AppContexts.user.set(user)
AppContexts.db.set(db)
AppContexts.logger.set(logger)
}
// Usage
pipeline.use(async (req, next) => {
const user = await authenticate(req)
const db = await getDatabase()
const logger = createLogger()
initializeContext(user, db, logger)
return next(req)
})Nested Context
typescript
const OuterContext = createContext<string>('Outer')
const InnerContext = createContext<string>('Inner')
const outerPipeline = createPipeline<string, string>()
.use((input, next) => {
OuterContext.set('Outer value')
return next(input)
})
const innerPipeline = createPipeline<string, string>()
.use((input, next) => {
InnerContext.set('Inner value')
console.log('Outer:', OuterContext.get()) // Can access outer
console.log('Inner:', InnerContext.get())
return next(input)
})
// Nested calls
outerPipeline.use((input, next) => {
const runInner = usePipeline(innerPipeline)
return runInner(input)
})Best Practices
✅ Good for Context
- 🟢 Request-level data shared across middleware (user, request ID)
- 🟢 State that needs isolation
- 🟢 Resources like database connections, loggers
typescript
// ✅ Good: Suitable for Context
const UserContext = createContext<User | null>(null)
const RequestIdContext = createContext<string>('')
const DBContext = createContext<Database | null>(null)❌ Not Suitable for Context
- 🔴 Global configuration (use module variables)
- 🔴 Data that can be passed through parameters
- 🔴 State that doesn't need isolation
typescript
// ❌ Unnecessary: module variables are fine
const ConfigContext = createContext({ apiUrl: 'https://api.example.com' })
// Change to this:
const API_URL = 'https://api.example.com'Naming Conventions
typescript
// ✅ Good: ends with Context
const UserContext = createContext<User | null>(null)
const LoggerContext = createContext<Logger>(console)
// ❌ Bad: unclear naming
const User = createContext<User | null>(null)
const log = createContext<Logger>(console)Type Safety
typescript
// ✅ Good: explicitly specify types
const UserContext = createContext<User | null>(null)
// ❌ Bad: rely on type inference
const UserContext = createContext(null) // Type is null