@ esinx/ aync- context
一个用于Async上下文管理的TypeScript库,使用由Node.js Async LocalStorage提供动力的React-like上下文API.
安装
npm install @esinx/async-context
# or
yarn add @esinx/async-context
# or
pnpm add @esinx/async-context
什么是同步背景?
Async 上下文允许您在整个Async调用链条中保存上下文信息,而不明确通过每个函数参数. 这对于:
- 请求追查: 在整个自动同步操作中维护请求ID
- 用户背景: 在整个请求处理过程中提供用户信息
- 日志: 在日志中自动包含背景信息
- 数据库交易: 维护全自动同步操作的交易环境
- 地物标志: 在整个应用程序中访问特性标记状态
问题
在传统的Async JavaScript/TypeScript代码中,通过多层的上下文可能繁琐:
// Without async context - tedious parameter passing
async function handleRequest(userId: string, requestId: string) {
const user = await fetchUser(userId, requestId)
const data = await processData(user, requestId)
await saveResult(data, userId, requestId)
}
async function fetchUser(userId: string, requestId: string) {
console.log(`[${requestId}] Fetching user ${userId}`)
// ... fetch logic
}
async function processData(user: User, requestId: string) {
console.log(`[${requestId}] Processing data for ${user.id}`)
// ... processing logic
}
解决办法
在同步语境下,您可以隐含地保持此信息:
// With async context - clean and maintainable
const userContext = createContext<User | null>(null)
const requestContext = createContext<string>('')
async function handleRequest(userId: string, requestId: string) {
await requestContext.run(requestId, async () => {
const user = await fetchUser(userId)
await userContext.run(user, async () => {
const data = await processData()
await saveResult(data)
})
})
}
async function fetchUser(userId: string) {
const requestId = use(requestContext)
console.log(`[${requestId}] Fetching user ${userId}`)
// ... fetch logic
}
async function processData() {
const user = use(userContext)
const requestId = use(requestContext)
console.log(`[${requestId}] Processing data for ${user.id}`)
// ... processing logic
}
安装
npm install async-context
# or
yarn add async-context
# or
pnpm add async-context
快速启动
import { createContext, use } from 'async-context'
// Create a context with a default value
const userContext = createContext<string>('anonymous')
// Run code with a specific context value
await userContext.run('john-doe', async () => {
// All async operations in this scope have access to the context
const currentUser = use(userContext) // 'john-doe'
await someAsyncOperation()
// Still available after async operations
const stillCurrentUser = use(userContext) // 'john-doe'
})
// Outside the run scope, back to default
const defaultUser = use(userContext) // 'anonymous'
API 参考
createContext<T>(initialValue: T): ContextHandle<T>
创建一个带有初始/默认值的新上下文处理.
const stringContext = createContext<string>('default')
const numberContext = createContext<number>(0)
const userContext = createContext<User | null>(null)
ContextHandle<T>
该天体由 createContext 采用下列方法:
handle.run<Result>(value: T, fn: () => Promise<Result>): Promise<Result>
运行带有特定上下文值的async函数.
const result = await handle.run('new-value', async () => {
// Context is 'new-value' within this scope
return await someAsyncOperation()
})
handle.use(): T
获得当前上下文值( 与独立值相同) use 函数).
const currentValue = handle.use()
use<T>(handle: ContextHandle<T>): T
从上下文控件获取当前值.
const currentUser = use(userContext)
runWithContext<T>(handle: ContextHandle<T>, value: T)
创建可重用功能,运行具有特定上下文值的操作.
const runAsAdmin = runWithContext(userContext, adminUser)
const result = await runAsAdmin(async () => {
// This runs with adminUser as the context
return await performAdminOperation()
})
常见模式
请求追查
import { createContext, use } from 'async-context'
const requestIdContext = createContext<string>('')
// Express middleware
app.use((req, res, next) => {
const requestId = req.headers['x-request-id'] || generateId()
requestIdContext.run(requestId, () => {
next()
})
})
// In any handler or service
async function processOrder() {
const requestId = use(requestIdContext)
logger.info(`Processing order`, { requestId })
// ... processing logic
}
用户背景
interface User {
id: string
role: string
permissions: string[]
}
const userContext = createContext<User | null>(null)
// Authentication middleware
async function authenticate(token: string) {
const user = await verifyToken(token)
return userContext.run(user, async () => {
// All subsequent operations have access to user context
return await handleRequest()
})
}
// In any service
async function createPost(data: PostData) {
const currentUser = use(userContext)
if (!currentUser) {
throw new Error('Not authenticated')
}
if (!currentUser.permissions.includes('create:post')) {
throw new Error('Insufficient permissions')
}
// ... create post logic
}
数据库交易
const transactionContext = createContext<Transaction | null>(null)
async function withTransaction<T>(fn: () => Promise<T>): Promise<T> {
const transaction = await db.beginTransaction()
try {
const result = await transactionContext.run(transaction, fn)
await transaction.commit()
return result
} catch (error) {
await transaction.rollback()
throw error
}
}
// Usage
await withTransaction(async () => {
await createUser(userData)
await createProfile(profileData)
// Both operations use the same transaction automatically
})
嵌入上下文
const tenantContext = createContext<string>('default')
const userContext = createContext<User | null>(null)
await tenantContext.run('tenant-123', async () => {
await userContext.run(user, async () => {
// Both contexts are available
const tenant = use(tenantContext) // 'tenant-123'
const currentUser = use(userContext) // user object
await performOperation()
})
})
高级使用
构成
interface AppContext {
user: User | null
requestId: string
tenant: string
}
const appContext = createContext<AppContext>({
user: null,
requestId: '',
tenant: 'default',
})
// Helper function to create context
function createAppContext(user: User, requestId: string, tenant: string) {
return { user, requestId, tenant }
}
await appContext.run(createAppContext(user, requestId, tenant), async () => {
const { user, requestId, tenant } = use(appContext)
// ... application logic
})
上下文提供者(类似反应模式)
class ContextProvider {
constructor(
private userContext = createContext<User | null>(null),
private requestContext = createContext<string>('')
) {}
async provideContext<T>(
user: User,
requestId: string,
fn: () => Promise<T>
): Promise<T> {
return this.requestContext.run(requestId, async () => {
return this.userContext.run(user, fn)
})
}
getCurrentUser() {
return use(this.userContext)
}
getCurrentRequestId() {
return use(this.requestContext)
}
}
最佳做法
- 使用有意义的默认值: 选择适合您的应用程序的默认值
- 保持上下文不变: 不要修改上下文对象; 改为创建新对象
- 避免经常变化的数据的背景: 环境最适合相对稳定的信息
- 使用类型脚本: 为上下文值利用类型安全
- 记录您的背景: 明确每个上下文代表什么,何时可用
- 上下文测试: 在测试中包含上下文情景
处理错误
当错误发生时, 上下文会自动清理 :
try {
await userContext.run(user, async () => {
// Context is available here
throw new Error('Something went wrong')
})
} catch (error) {
// Context is back to default/initial value
const currentUser = use(userContext) // back to initial value
}
业绩考量
- Async LocalStorage 大多数应用程序的间接费用都很少
- 上下文搜索非常快( O(1))
- 网格环境的额外费用极少
- 考虑对复杂状态使用上下文构成,而不是使用许多单个背景
所需资源
- 节点.js 16+(同步本地集成支持)
- TypeScript 4.5+(用于最佳类型支持)
许可证
麻省理工学院