Skip to content

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

Advanced 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

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