export const INVALIDATION_TYPES = [ "directory.updated", "page.updated", "stygian.updated", "admin.updated", "catalog.updated", "glossary.updated", ] as const; export type InvalidationType = (typeof INVALIDATION_TYPES)[number]; export type AdminEditorScope = | "overview" | "weapon" | "artifact" | "constellations" | "team" | "extras:list" | `extra:${string}`; export interface InvalidationEvent { type: InvalidationType; id: string; version: number; scope?: AdminEditorScope; operation?: "overwrite"; actorName?: string; } export interface OutboxEventLike { aggregateId: string; eventType: string; payload: Record; topic: string; } function isInvalidationType(value: string): value is InvalidationType { return INVALIDATION_TYPES.some((type) => type === value); } function isAdminEditorScope(value: unknown): value is AdminEditorScope { return typeof value === "string" && ( ["overview", "weapon", "artifact", "constellations", "team", "extras:list"].includes(value) || /^extra:[0-9a-f-]{36}$/iu.test(value) ); } function overwriteDetails(payload: Record) { if (payload.operation === undefined && payload.actorName === undefined) return {}; if ( payload.operation !== "overwrite" || typeof payload.actorName !== "string" || !payload.actorName.trim() || payload.actorName.length > 100 ) return null; return { operation: "overwrite" as const, actorName: payload.actorName }; } export function expectedTopic(event: InvalidationEvent): string { switch (event.type) { case "directory.updated": return "directory"; case "page.updated": return `page:${event.id}`; case "stygian.updated": return "stygian"; case "admin.updated": case "catalog.updated": case "glossary.updated": return "admin"; } } export function toInvalidationEvent( record: OutboxEventLike, ): InvalidationEvent | null { if (!isInvalidationType(record.eventType)) return null; if ( record.eventType === "admin.updated" && record.payload.scope !== undefined && !isAdminEditorScope(record.payload.scope) ) return null; const details = record.eventType === "admin.updated" ? overwriteDetails(record.payload) : {}; if (details === null) return null; const id = record.payload.id; const version = record.payload.version; if ( typeof id !== "string" || id !== record.aggregateId || typeof version !== "number" || !Number.isSafeInteger(version) || version < 0 ) { return null; } const event: InvalidationEvent = { type: record.eventType, id, version, ...(record.eventType === "admin.updated" && isAdminEditorScope(record.payload.scope) ? { scope: record.payload.scope } : {}), ...details, }; return expectedTopic(event) === record.topic ? event : null; } export function parseInvalidationEvent(value: string): InvalidationEvent | null { try { const parsed = JSON.parse(value) as Record; const type = parsed.type; const id = parsed.id; const version = parsed.version; const scope = parsed.scope; const details = type === "admin.updated" ? overwriteDetails(parsed) : {}; if ( typeof type !== "string" || !isInvalidationType(type) || typeof id !== "string" || typeof version !== "number" || !Number.isSafeInteger(version) || version < 0 ) { return null; } if (scope !== undefined && (type !== "admin.updated" || !isAdminEditorScope(scope))) { return null; } if (details === null) return null; return { type, id, version, ...(scope === undefined ? {} : { scope }), ...details }; } catch { return null; } }