Makelore 2.0 initial clean snapshot
This commit is contained in:
101
src/pages/Chat/game-asset-review-recovery.ts
Normal file
101
src/pages/Chat/game-asset-review-recovery.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
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';
|
||||
}
|
||||
Reference in New Issue
Block a user