feat(sync): cn event coverage, bootstrap and outbox sender
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
// packages/contracts/src/events.ts
|
||||
// Outbox 事件契约(docs/03-技术实施方案.md §8.2):P1a 在业务事务内写 outbox,
|
||||
// P2 海外副本按 eventId 去重、按 entityVersion 单调递增消费。payload 由各事件
|
||||
// 类型自行约定,此处仅落 P2 展示一张照片所需的 photo.published 载荷(YAGNI)。
|
||||
// P2 海外副本按 eventId 去重、按 entityVersion 单调递增消费。P2 Task 3 起补全
|
||||
// album/category/asset 载荷(cn 事件补全);photo.* 载荷自 P1a 起落定。
|
||||
export interface OutboxEvent<T = unknown> {
|
||||
eventId: string; // cuid,持久唯一
|
||||
type:
|
||||
@@ -9,7 +9,10 @@ export interface OutboxEvent<T = unknown> {
|
||||
| 'photo.hidden'
|
||||
| 'photo.deleted'
|
||||
| 'photo.likes.updated'
|
||||
| 'album.updated';
|
||||
| 'album.updated'
|
||||
| 'category.updated'
|
||||
| 'category.deleted'
|
||||
| 'asset.updated';
|
||||
albumId: string;
|
||||
photoId?: string;
|
||||
entityVersion: number; // 单调递增,P2 inbox 消费依据
|
||||
@@ -30,6 +33,75 @@ export interface PhotoPublishedPayload {
|
||||
entityVersion: number;
|
||||
}
|
||||
|
||||
// album.updated 载荷:ov 副本(albums_ov)展示一张相册所需的标量 + 翻译全集。
|
||||
// categories 不在此列——分类是独立事件(category.*,各自携带 entityVersion);
|
||||
// banner 对象键经 asset.updated 事件同步(albums_ov.bannerAssetKey)。
|
||||
export interface AlbumUpdatedPayload {
|
||||
albumId: string;
|
||||
publicToken: string;
|
||||
status: string; // active|offline(cn 副本值)
|
||||
uploadEnabled: boolean;
|
||||
publicEnabled: boolean;
|
||||
downloadEnabled: boolean;
|
||||
originalDownloadEnabled: boolean;
|
||||
defaultLocale: string;
|
||||
timezone: string;
|
||||
startAt: string; // ISO UTC
|
||||
endAt: string | null; // ISO UTC;null 表示未定
|
||||
translations: AlbumTranslationPayload[]; // locale 升序(确定性输出)
|
||||
entityVersion: number;
|
||||
}
|
||||
|
||||
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 = [
|
||||
@@ -38,4 +110,43 @@ export const OUTBOX_EVENT_TYPES = [
|
||||
'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';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user