Include content delivery runtime

This commit is contained in:
phenom committed 2026-08-15 21:11:43 -04:00
1 parent 4d0980a389
commit b5c9bce1aa
9 files changed
+809 -1

No files matched your search

+107
View File
@@ -0,0 +1,107 @@
export const CONTENT_MANIFEST_SCHEMA_VERSION = 1;
export interface ContentManifestFile {
readonly packId: string;
readonly url: string;
readonly sha256: string;
readonly bytes: number;
}
export interface ContentManifestPack {
readonly id: string;
readonly label: string;
readonly bytes: number;
readonly fileCount: number;
readonly bundledOnAndroid: boolean;
readonly dependencies: readonly string[];
}
export interface ContentManifest {
readonly schemaVersion: number;
readonly version: string;
readonly createdAt: string;
readonly minimumAppVersion: string;
readonly packs: readonly ContentManifestPack[];
readonly files: Readonly<Record<string, ContentManifestFile>>;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
export function parseContentManifest(value: unknown): ContentManifest {
if (!isRecord(value) || value.schemaVersion !== CONTENT_MANIFEST_SCHEMA_VERSION) {
throw new Error("This content catalog uses an unsupported schema.");
}
if (typeof value.version !== "string" || typeof value.createdAt !== "string" || typeof value.minimumAppVersion !== "string") {
throw new Error("The content catalog is missing version information.");
}
if (!Array.isArray(value.packs) || !isRecord(value.files)) {
throw new Error("The content catalog is malformed.");
}
const packs = value.packs.map((pack) => {
if (!isRecord(pack)
|| typeof pack.id !== "string"
|| typeof pack.label !== "string"
|| typeof pack.bytes !== "number"
|| typeof pack.fileCount !== "number"
|| typeof pack.bundledOnAndroid !== "boolean"
|| !Array.isArray(pack.dependencies)
|| !pack.dependencies.every((dependency) => typeof dependency === "string")) {
throw new Error("The content catalog contains a malformed pack.");
}
return pack as unknown as ContentManifestPack;
});
const files = Object.fromEntries(Object.entries(value.files).map(([logicalPath, entry]) => {
if (!logicalPath.startsWith("/")
|| !isRecord(entry)
|| typeof entry.packId !== "string"
|| typeof entry.url !== "string"
|| !/^[a-f0-9]{64}$/.test(String(entry.sha256))
|| typeof entry.bytes !== "number"
|| !Number.isSafeInteger(entry.bytes)
|| entry.bytes < 0) {
throw new Error(`The content catalog contains a malformed file: ${logicalPath}`);
}
return [logicalPath, entry as unknown as ContentManifestFile];
}));
return { ...value, packs, files } as unknown as ContentManifest;
}
function numericVersion(value: string): readonly number[] {
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(value.trim());
return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : [0, 0, 0];
}
export function appVersionMeetsMinimum(current: string, minimum: string): boolean {
const left = numericVersion(current);
const right = numericVersion(minimum);
for (let index = 0; index < 3; index += 1) {
if (left[index] > right[index]) return true;
if (left[index] < right[index]) return false;
}
return true;
}
export function filesForContentPack(
manifest: ContentManifest,
packId: string,
): readonly (readonly [string, ContentManifestFile])[] {
return Object.entries(manifest.files)
.filter(([, entry]) => entry.packId === packId)
.sort(([left], [right]) => left.localeCompare(right));
}
export interface ContentFileIdentity {
readonly sha256: string;
}
export function changedFilesForPack(
manifest: ContentManifest,
packId: string,
activeFiles: Readonly<Record<string, ContentFileIdentity>>,
): readonly (readonly [string, ContentManifestFile])[] {
return filesForContentPack(manifest, packId).filter(([logicalPath, entry]) => (
activeFiles[logicalPath]?.sha256 !== entry.sha256
));
}