context.js 1.73 KB
import { randomUUID } from 'node:crypto'

/**
 * @typedef {Object} RequestContext
 * @property {string} traceId  贯穿一次请求的 trace id
 * @property {Object} fields    累积的结构化字段(如 userId/route)
 * @property {(extraFields?: Object) => RequestContext} child
 *   派生子上下文:继承 `traceId`,merge 字段,返回**新** ctx(不可变派生,不污染父)
 */

/**
 * 创建请求上下文。入口处一次,或从入站 trace header 续传。
 * 不传 `traceId` 时自动生成(`crypto.randomUUID`);传则续用(跨服务/入站续传)。
 *
 * @param {Object} [opts]
 * @param {string} [opts.traceId]  续传已有 trace_id;省略则自动生成
 * @param {Object} [opts.fields]   初始附加字段(如 route)
 * @returns {RequestContext}
 */
export function createRequestContext(opts) {
  const traceId = opts && opts.traceId ? opts.traceId : randomUUID()
  const fields = opts && opts.fields ? { ...opts.fields } : {}
  return makeContext(traceId, fields)
}

/**
 * 内部工厂:把 traceId + 已固化的 fields 包成不可变 RequestContext。
 * @param {string} traceId
 * @param {Object} fields  调用方已复制好的字段对象,本函数不再外泄其可变引用
 * @returns {RequestContext}
 */
function makeContext(traceId, fields) {
  /** @type {RequestContext} */
  const ctx = {
    traceId,
    fields,
    child(extraFields) {
      // 不可变派生:合并出全新字段对象,父 ctx.fields 不被触碰。
      const merged = { ...fields, ...(extraFields || {}) }
      return makeContext(traceId, merged)
    },
  }
  // 冻结对外可见形态,防止下游意外篡改 traceId/child 造成 trace 漂移。
  Object.freeze(ctx.fields)
  return Object.freeze(ctx)
}