Advance M8-M11 parity workflows
This commit is contained in:
134
web/protocol/recent-projects.ts
Normal file
134
web/protocol/recent-projects.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user