Files
gunshiz 9580407a90
CI / Verify (push) Successful in 1m56s
CI / Build immutable images and deploy (push) Successful in 3m12s
perf : cache
2026-10-08 23:21:34 +07:00

135 lines
3.7 KiB
TypeScript

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<string, unknown>;
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<string, unknown>) {
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<string, unknown>;
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;
}
}