Files
photo-live/packages/contracts/src/events.ts
T
2026-09-20 17:53:44 +08:00

173 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// packages/contracts/src/events.ts
// Outbox 事件契约(docs/03-技术实施方案.md §8.2):P1a 在业务事务内写 outbox,
// P2 海外副本按 eventId 去重、按 entityVersion 单调递增消费。P2 Task 3 起补全
// album/category/asset 载荷(cn 事件补全);photo.* 载荷自 P1a 起落定。
export interface OutboxEvent<T = unknown> {
eventId: string; // cuid,持久唯一
type:
| 'photo.published'
| 'photo.hidden'
| 'photo.deleted'
| 'photo.likes.updated'
| 'album.updated'
| 'category.updated'
| 'category.deleted'
| 'asset.updated';
albumId: string;
photoId?: string;
entityVersion: number; // 单调递增,P2 inbox 消费依据
payload: T;
occurredAt: string; // ISO UTC
}
// photo.published 载荷:P2 海外副本展示一张照片所需的全部字段。
export interface PhotoPublishedPayload {
photoId: string;
albumId: string;
categoryId: string | null; // 分类过滤(ov 公开列表 ?category=)与单图同形所需
objectKey: string; // 原图对象键
thumbKey: string; // 缩略图对象键
browseKey: string; // 浏览图对象键
width: number;
height: number;
publishedAt: string; // ISO UTC
entityVersion: number;
}
// album.updated 载荷:ov 副本(albums_ov)展示一张相册所需的标量 + 翻译全集。
// categories 默认不在此列——分类是独立事件(category.*,各自携带 entityVersion);
// banner 对象键经 asset.updated 事件同步(albums_ov.bannerAssetKey)。
// 例外:bootstrap/reconcile 的快照事件携带可选 categories 全量清单(Task 4
// digest 补漏的收敛原语,见 AlbumCategorySnapshot 注释)。
export interface AlbumUpdatedPayload {
albumId: string;
publicToken: string;
status: string; // active|offline(cn 副本值)
uploadEnabled: boolean;
publicEnabled: boolean;
downloadEnabled: boolean;
originalDownloadEnabled: boolean;
/** P4a A18:水印副本位(ov 模式键服务)。语义见 watermark.ts 注释——取「有效版本」而非最新创建版本。 */
watermarkEnabled: boolean;
/** ov 侧模式键 `variants/{photoId}/{watermarkConfigVersion}/watermark.jpg` 的版本段。 */
watermarkConfigVersion: number;
defaultLocale: string;
timezone: string;
startAt: string; // ISO UTC
endAt: string | null; // ISO UTC;null 表示未定
translations: AlbumTranslationPayload[]; // locale 升序(确定性输出)
/** 分类全量清单(可选):存在时 ov **整体替换** albums_ov.categories。 */
categories?: AlbumCategorySnapshot[];
entityVersion: number;
}
// albums_ov.categories JSON 元素的载荷形状。日常增量由 category.updated 逐个
// upsert / category.deleted 墓碑逐个移除;但**错过的墓碑**(cn 行已删,对账
// 无法为不存在的行重放事件)只有「全量清单整体替换」才能收敛——bootstrap/
// reconcile 的 album.updated 快照事件据此携带当前全集(Task 4 digest→bootstrap
// 补漏通道)。缺省(增量事件不携带)时 ov 不触碰 categories,语义不变。
export interface AlbumCategorySnapshot {
id: string;
sortOrder: number;
translations: CategoryTranslationPayload[]; // locale 升序
}
export interface AlbumTranslationPayload {
locale: string;
title: string;
description: string | null;
venue: string | null;
address: string | null;
}
// category.updated 载荷:相册分类(ov 落 albums_ov.categories JSON 数组元素)。
export interface CategoryUpdatedPayload {
categoryId: string;
albumId: string;
sortOrder: number;
translations: CategoryTranslationPayload[]; // locale 升序
entityVersion: number;
}
export interface CategoryTranslationPayload {
locale: string;
name: string;
}
// category.deleted 载荷:墓碑(ov 从 albums_ov.categories 移除该元素且不被旧
// 事件复活)。删除后 cn 行已不存在,entityVersion 为删除时刻的终值。
export interface CategoryDeletedPayload {
categoryId: string;
albumId: string;
entityVersion: number;
}
// photo.hidden / photo.deleted 载荷:可见性变更最小事实(ov 副本据此置
// photos_ov.visibility;无展示字段——撤下语义只需标识与版本)。
export interface PhotoVisibilityChangedPayload {
photoId: string;
albumId: string;
visibility: string; // hidden|deleted
entityVersion: number;
}
// asset.updated 载荷:相册资产生效(state pending→effective 的迁移才发事件)。
// ov 侧 kind=banner 投影到 albums_ov.bannerAssetKey;cover/logo 仅推进版本。
export interface AssetUpdatedPayload {
assetId: string;
albumId: string;
kind: string; // banner|cover|logo
locale: string | null;
objectKey: string;
entityVersion: number;
}
// 运行时校验用:全部合法事件类型字符串。satisfies 保证 数组 ⊆ 接口 union;
// __tests__/events.test.ts 的 Equal 断言反向锁定 union ⊆ 数组,两侧永远 1:1。
export const OUTBOX_EVENT_TYPES = [
'photo.published',
'photo.hidden',
'photo.deleted',
'photo.likes.updated',
'album.updated',
'category.updated',
'category.deleted',
'asset.updated',
] as const satisfies readonly OutboxEvent['type'][];
// ── outbox 类型 → 内部同步线格式(openapi SyncEvent.entityType)的映射 ──
// 发送器(services/sync)与 cn reconcile 端点共用,单一实现防两侧漂移;
// exhaustive switch:新增事件类型漏映射会在此编译报错。
/** 内部同步实体类型(对齐 openapi SyncEntityType;slot-state 为预留)。 */
export type SyncEntityType =
| 'album'
| 'category'
| 'photo'
| 'asset'
| 'slot-state';
export function syncEntityTypeOfOutboxType(
type: OutboxEvent['type'],
): SyncEntityType {
switch (type) {
case 'album.updated':
return 'album';
case 'category.updated':
case 'category.deleted':
return 'category';
case 'asset.updated':
return 'asset';
case 'photo.published':
case 'photo.hidden':
case 'photo.deleted':
case 'photo.likes.updated':
return 'photo';
}
}
/** 墓碑类事件(副本删除且不被旧事件复活,A26)。 */
export function isTombstoneOutboxType(type: OutboxEvent['type']): boolean {
return type === 'category.deleted' || type === 'photo.deleted';
}