Files
workinf_Blender_Wasm/web/protocol/recent-projects.ts
mes123456 0fe8d2bb56
Some checks are pending
M6 deployable RC / quick (push) Waiting to run
M6 deployable RC / chromium (push) Blocked by required conditions
M6 deployable RC / release (push) Blocked by required conditions
Advance M8-M11 parity workflows
2026-08-17 04:37:07 -04:00

135 lines
5.4 KiB
TypeScript

export const RECENT_PROJECTS_SCHEMA_VERSION = 1;
export const RECENT_PROJECTS_MAX_COUNT = 50;
export type RecentProjectBackend = "opfs" | "indexeddb" | "unknown";
export interface RecentProjectRecord {
schemaVersion: typeof RECENT_PROJECTS_SCHEMA_VERSION;
projectId: string;
displayName: string;
revision: number;
bytes: number;
sha256: string;
updatedAt: string;
lastOpenedAt: string;
backend: RecentProjectBackend;
}
export interface RecentProjectIndex {
schemaVersion: typeof RECENT_PROJECTS_SCHEMA_VERSION;
projects: RecentProjectRecord[];
}
export type RecentProjectIssueCode = "MISSING" | "HASH_MISMATCH" | "METADATA_MISMATCH";
export interface RecentProjectIdentity {
revision: number;
bytes: number;
sha256: string;
}
export interface RecentProjectIssue {
project: RecentProjectRecord;
code: RecentProjectIssueCode;
}
export interface RecentProjectParseResult {
index: RecentProjectIndex;
quarantined: number;
}
const SHA256_PATTERN = /^[a-f0-9]{64}$/;
const PROJECT_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
function canonicalTimestamp(value: unknown): string | undefined {
if (typeof value !== "string") return undefined;
const milliseconds = Date.parse(value);
return Number.isFinite(milliseconds) ? new Date(milliseconds).toISOString() : undefined;
}
export function createRecentProjectIndex(): RecentProjectIndex {
return { schemaVersion: RECENT_PROJECTS_SCHEMA_VERSION, projects: [] };
}
export function parseRecentProjectRecord(value: unknown): RecentProjectRecord | undefined {
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
const candidate = value as Record<string, unknown>;
const updatedAt = canonicalTimestamp(candidate.updatedAt);
const lastOpenedAt = canonicalTimestamp(candidate.lastOpenedAt);
if (candidate.schemaVersion !== RECENT_PROJECTS_SCHEMA_VERSION ||
typeof candidate.projectId !== "string" || !PROJECT_ID_PATTERN.test(candidate.projectId) ||
typeof candidate.displayName !== "string" || candidate.displayName.trim().length === 0 || candidate.displayName.length > 128 ||
!Number.isInteger(candidate.revision) || Number(candidate.revision) < 0 ||
!Number.isInteger(candidate.bytes) || Number(candidate.bytes) <= 0 ||
typeof candidate.sha256 !== "string" || !SHA256_PATTERN.test(candidate.sha256) ||
!updatedAt || !lastOpenedAt ||
!["opfs", "indexeddb", "unknown"].includes(candidate.backend as string)) {
return undefined;
}
return {
schemaVersion: RECENT_PROJECTS_SCHEMA_VERSION,
projectId: candidate.projectId,
displayName: candidate.displayName,
revision: Number(candidate.revision),
bytes: Number(candidate.bytes),
sha256: candidate.sha256,
updatedAt,
lastOpenedAt,
backend: candidate.backend as RecentProjectBackend,
};
}
function compareRecentProjects(left: RecentProjectRecord, right: RecentProjectRecord): number {
return right.lastOpenedAt.localeCompare(left.lastOpenedAt) ||
right.updatedAt.localeCompare(left.updatedAt) ||
right.revision - left.revision ||
left.projectId.localeCompare(right.projectId) ||
left.displayName.localeCompare(right.displayName) ||
left.backend.localeCompare(right.backend) ||
left.sha256.localeCompare(right.sha256) ||
left.bytes - right.bytes;
}
export function normalizeRecentProjects(records: readonly unknown[], limit = RECENT_PROJECTS_MAX_COUNT): RecentProjectParseResult {
const byProjectId = new Map<string, RecentProjectRecord>();
let quarantined = 0;
for (const value of records) {
const parsed = parseRecentProjectRecord(value);
if (!parsed) {
quarantined += 1;
continue;
}
const previous = byProjectId.get(parsed.projectId);
if (!previous || compareRecentProjects(parsed, previous) < 0) byProjectId.set(parsed.projectId, parsed);
}
const normalizedLimit = Number.isFinite(limit)
? Math.max(0, Math.min(RECENT_PROJECTS_MAX_COUNT, Math.floor(limit)))
: RECENT_PROJECTS_MAX_COUNT;
const projects = [...byProjectId.values()].sort(compareRecentProjects).slice(0, normalizedLimit);
return { index: { schemaVersion: RECENT_PROJECTS_SCHEMA_VERSION, projects }, quarantined };
}
export function parseRecentProjectIndex(value: unknown): RecentProjectParseResult {
if (!value || typeof value !== "object" || Array.isArray(value)) return { index: createRecentProjectIndex(), quarantined: 1 };
const candidate = value as Record<string, unknown>;
if (candidate.schemaVersion !== RECENT_PROJECTS_SCHEMA_VERSION || !Array.isArray(candidate.projects)) {
return { index: createRecentProjectIndex(), quarantined: 1 };
}
return normalizeRecentProjects(candidate.projects);
}
export function upsertRecentProject(index: RecentProjectIndex, record: RecentProjectRecord): RecentProjectIndex {
return normalizeRecentProjects([...index.projects, record]).index;
}
export function removeRecentProject(index: RecentProjectIndex, projectId: string): RecentProjectIndex {
return normalizeRecentProjects(index.projects.filter((project) => project.projectId !== projectId)).index;
}
export function classifyRecentProjectIdentity(expected: RecentProjectIdentity, actual: RecentProjectIdentity | undefined): RecentProjectIssueCode | undefined {
if (!actual) return "MISSING";
if (expected.sha256 !== actual.sha256) return "HASH_MISMATCH";
if (expected.revision !== actual.revision || expected.bytes !== actual.bytes) return "METADATA_MISMATCH";
return undefined;
}