58 lines
2.3 KiB
TypeScript
58 lines
2.3 KiB
TypeScript
/** Server 内部消息统一信封;字段同时满足幂等、版本演进和跨进程链路追踪。 */
|
|
export interface MessageEnvelope<TPayload> {
|
|
eventId: string;
|
|
eventType: string;
|
|
schemaVersion: 1;
|
|
idempotencyKey: string;
|
|
occurredAt: string;
|
|
tradeDate: string;
|
|
traceId: string;
|
|
payload: TPayload;
|
|
}
|
|
|
|
type MessageEnvelopeInput<TPayload> = Omit<MessageEnvelope<TPayload>, 'schemaVersion'> & {
|
|
schemaVersion: number;
|
|
};
|
|
|
|
function deepFreeze<TValue>(value: TValue): Readonly<TValue> {
|
|
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
for (const child of Object.values(value as Record<string, unknown>)) deepFreeze(child);
|
|
Object.freeze(value);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
/** 校验、规范化并深冻结消息信封,防止投递后字段被异步处理链路改写。 */
|
|
export function createMessageEnvelope<TPayload>(
|
|
input: MessageEnvelopeInput<TPayload>,
|
|
): Readonly<MessageEnvelope<TPayload>> {
|
|
if (!input.eventId.trim()) throw new Error('eventId 不能为空');
|
|
if (!input.eventType.trim()) throw new Error('eventType 不能为空');
|
|
if (input.schemaVersion !== 1) throw new Error('schemaVersion 只支持 1');
|
|
if (!input.idempotencyKey.trim()) throw new Error('idempotencyKey 不能为空');
|
|
if (!input.occurredAt.trim() || Number.isNaN(Date.parse(input.occurredAt))) {
|
|
throw new Error('occurredAt 必须为合法 ISO 时间');
|
|
}
|
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(input.tradeDate)) {
|
|
throw new Error('tradeDate 必须为合法 YYYY-MM-DD 日期');
|
|
}
|
|
const [year, month, day] = input.tradeDate.split('-').map(Number);
|
|
const parsedTradeDate = new Date(Date.UTC(year, month - 1, day));
|
|
if (parsedTradeDate.toISOString().slice(0, 10) !== input.tradeDate) {
|
|
throw new Error('tradeDate 必须为合法 YYYY-MM-DD 日期');
|
|
}
|
|
if (!input.traceId.trim()) throw new Error('traceId 不能为空');
|
|
if (input.payload === undefined) throw new Error('payload 不能为空');
|
|
|
|
return deepFreeze({
|
|
eventId: input.eventId.trim(),
|
|
eventType: input.eventType.trim(),
|
|
schemaVersion: 1,
|
|
idempotencyKey: input.idempotencyKey.trim(),
|
|
occurredAt: new Date(input.occurredAt).toISOString(),
|
|
tradeDate: input.tradeDate,
|
|
traceId: input.traceId.trim(),
|
|
payload: input.payload,
|
|
}) as Readonly<MessageEnvelope<TPayload>>;
|
|
}
|