概念定义
事件存储按契约不可变,但业务对“事件该长什么样”会改主意。上溯转换调停两者:已存事实冻结不动,读取方把它翻译成今天的词汇。
工作机制
每个事件携带显式模式版本;注册表把版本对映射到上溯函数;读取或重建投影时,链式把 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;
}
}