← 中文资源

简体中文版本

事件上溯转换

事件上溯转换在读取时把旧版本事件变换为当前形态,让事件溯源系统演进事件模式而无需改写已存历史。

阅读英文原文

概念定义

事件存储按契约不可变,但业务对“事件该长什么样”会改主意。上溯转换调停两者:已存事实冻结不动,读取方把它翻译成今天的词汇。

工作机制

每个事件携带显式模式版本;注册表把版本对映射到上溯函数;读取或重建投影时,链式把 v1 变 v2 变当前。上溯函数必须纯、全、确定:同样的输入永远同样的输出,否则重放会分叉。

使用场景

破坏性变更:改名字段、拆分或合并概念、枚举语义变化、重构载荷。带默认值的增量变更可以完全跳过变换,不必为它们写上溯器。

常见误区

改写历史事件的迁移脚本会摧毁事件存储赖以存在的审计属性。有损上溯会永久重释历史——优先“旧字段保留加新字段合成”的加法式整形。未测试的上溯函数会在下一次投影重建时腐蚀一切投影——必须用合成历史事件做跨代重放测试。链式上溯的维护成本会复利累积,可定期整合为单个 v1-to-current 并退役整条链。

代码示例

type EventMap = {
  login: { userId: string };
  logout: { userId: string; reason: string };
  error: { message: string; retryable: boolean };
};

export class TypedEmitter<M extends Record<string, unknown>> {
  private handlers: { [K in keyof M]?: Array<(payload: M[K]) => void> } = {};

  on<K extends keyof M>(event: K, handler: (payload: M[K]) => void): void {
    (this.handlers[event] ??= []).push(handler);
  }

  emit<K extends keyof M>(event: K, payload: M[K]): void {
    for (const handler of this.handlers[event] ?? []) handler(payload);
  }
}

export const emitter = new TypedEmitter<EventMap>();
export class EventEmitter {
  constructor() {
    this.listeners = new Map();
  }

  on(event, handler) {
    const set = this.listeners.get(event) ?? new Set();
    set.add(handler);
    this.listeners.set(event, set);
    return () => this.off(event, handler);
  }

  once(event, handler) {
    const off = this.on(event, (payload) => {
      off();
      handler(payload);
    });
    return off;
  }

  off(event, handler) {
    const set = this.listeners.get(event);
    if (set) set.delete(handler);
  }

  emit(event, payload) {
    const set = this.listeners.get(event);
    if (!set) return 0;
    for (const handler of [...set]) handler(payload);
    return set.size;
  }
}