102 lines
3.3 KiB
TypeScript
102 lines
3.3 KiB
TypeScript
import { AppError } from '@/lib/error-model';
|
|
|
|
export type GameAssetCandidate = {
|
|
id: string;
|
|
name: string;
|
|
category: string;
|
|
status: string;
|
|
purpose: string;
|
|
source: string;
|
|
license: string;
|
|
localPath?: string;
|
|
mediaKind: 'image' | 'audio' | 'bundle' | 'unavailable';
|
|
previewDataUrl?: string;
|
|
audioDataUrl?: string;
|
|
manifest: string[];
|
|
unavailableReason?: string;
|
|
};
|
|
|
|
export type GameAssetReviewSnapshot = {
|
|
invocationId: string;
|
|
candidateIds: string[];
|
|
decisions: Record<string, 'approved' | 'discarded' | 'replace-requested'>;
|
|
status: 'pending' | 'resolved';
|
|
pendingAssetIds: string[];
|
|
approvedAssetIds: string[];
|
|
discardedAssetIds: string[];
|
|
};
|
|
|
|
export type GameAssetReviewResponse = {
|
|
review: GameAssetReviewSnapshot;
|
|
assets: GameAssetCandidate[];
|
|
};
|
|
|
|
export type GameAssetReviewLoadFailure = 'stale' | 'retryable';
|
|
|
|
const STALE_HTTP_STATUSES = new Set([400, 403, 404, 409, 410, 422]);
|
|
const MEDIA_KINDS = new Set(['image', 'audio', 'bundle', 'unavailable']);
|
|
const REVIEW_DECISIONS = new Set(['approved', 'discarded', 'replace-requested']);
|
|
|
|
function record(value: unknown): Record<string, unknown> | null {
|
|
return value !== null && typeof value === 'object'
|
|
? value as Record<string, unknown>
|
|
: null;
|
|
}
|
|
|
|
function stringArray(value: unknown): value is string[] {
|
|
return Array.isArray(value) && value.every((item) => typeof item === 'string');
|
|
}
|
|
|
|
function optionalString(value: unknown): boolean {
|
|
return value === undefined || typeof value === 'string';
|
|
}
|
|
|
|
function validAsset(value: unknown): value is GameAssetCandidate {
|
|
const item = record(value);
|
|
return Boolean(
|
|
item
|
|
&& typeof item.id === 'string'
|
|
&& typeof item.name === 'string'
|
|
&& typeof item.category === 'string'
|
|
&& typeof item.status === 'string'
|
|
&& typeof item.purpose === 'string'
|
|
&& typeof item.source === 'string'
|
|
&& typeof item.license === 'string'
|
|
&& typeof item.mediaKind === 'string'
|
|
&& MEDIA_KINDS.has(item.mediaKind)
|
|
&& stringArray(item.manifest)
|
|
&& optionalString(item.localPath)
|
|
&& optionalString(item.previewDataUrl)
|
|
&& optionalString(item.audioDataUrl)
|
|
&& optionalString(item.unavailableReason),
|
|
);
|
|
}
|
|
|
|
export function isGameAssetReviewResponse(value: unknown): value is GameAssetReviewResponse {
|
|
const response = record(value);
|
|
const review = record(response?.review);
|
|
const decisions = record(review?.decisions);
|
|
return Boolean(
|
|
response
|
|
&& review
|
|
&& typeof review.invocationId === 'string'
|
|
&& stringArray(review.candidateIds)
|
|
&& decisions
|
|
&& !Array.isArray(review.decisions)
|
|
&& Object.values(decisions).every((decision) => typeof decision === 'string' && REVIEW_DECISIONS.has(decision))
|
|
&& (review.status === 'pending' || review.status === 'resolved')
|
|
&& stringArray(review.pendingAssetIds)
|
|
&& stringArray(review.approvedAssetIds)
|
|
&& stringArray(review.discardedAssetIds)
|
|
&& Array.isArray(response.assets)
|
|
&& response.assets.every(validAsset),
|
|
);
|
|
}
|
|
|
|
export function classifyGameAssetReviewLoadFailure(error: unknown): GameAssetReviewLoadFailure {
|
|
const status = error instanceof AppError && typeof error.details?.status === 'number'
|
|
? error.details.status
|
|
: null;
|
|
return status !== null && STALE_HTTP_STATUSES.has(status) ? 'stale' : 'retryable';
|
|
}
|