export const MESH_GEOMETRY_STREAM_SCHEMA = 1 as const; export const MESH_GEOMETRY_STREAM_MAX_TRIANGLES = 20_000_000; export const MESH_GEOMETRY_STREAM_MAX_CHUNK_TRIANGLES = 500_000; export const MESH_GEOMETRY_STREAM_MAX_LOD_TRIANGLES = 100_000; export interface MeshGeometryStreamRequestIR { schemaVersion: typeof MESH_GEOMETRY_STREAM_SCHEMA; streamId: string; meshId: string; triangleCount: number; chunkTriangleCount: number; lodTriangleCount: number; } export interface MeshGeometryStreamChunkIR { schemaVersion: typeof MESH_GEOMETRY_STREAM_SCHEMA; streamId: string; meshId: string; chunkIndex: number; chunkCount: number; triangleOffset: number; triangleCount: number; vertexCount: number; byteLength: number; positions: ArrayBuffer; indices: ArrayBuffer; sha256: string; } export interface MeshGeometryStreamChunkRecordIR { chunkIndex: number; triangleOffset: number; triangleCount: number; vertexCount: number; byteLength: number; sha256: string; } export interface MeshGeometryStreamReportIR { streamId: string; meshId: string; status: "COMPLETED" | "CANCELLED"; triangleCount: number; chunkCount: number; transferredBytes: number; peakWorkingSetBytes: number; manifestSha256?: string; } export type MeshGeometryStreamWorkerRequest = | { type: "start"; request: MeshGeometryStreamRequestIR } | { type: "ack"; streamId: string; chunkIndex: number } | { type: "cancel"; streamId: string }; export type MeshGeometryStreamWorkerResponse = | { type: "lod"; streamId: string; geometry: MeshGeometryStreamChunkIR } | { type: "chunk"; streamId: string; chunk: MeshGeometryStreamChunkIR } | { type: "detached"; streamId: string; chunkIndex: number; positionsByteLength: number; indicesByteLength: number } | { type: "complete"; report: MeshGeometryStreamReportIR; chunks: MeshGeometryStreamChunkRecordIR[] } | { type: "cancelled"; report: MeshGeometryStreamReportIR } | { type: "error"; streamId?: string; code: string; message: string }; export class MeshGeometryStreamError extends Error { constructor(readonly code: "GEOMETRY_STREAM_INVALID" | "GEOMETRY_STREAM_RANGE_INVALID" | "GEOMETRY_STREAM_BUDGET_EXCEEDED", message: string) { super(`${code}: ${message}`); this.name = "MeshGeometryStreamError"; } } function record(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function boundedInteger(value: unknown, name: string, minimum: number, maximum: number): number { if (!Number.isSafeInteger(value) || (value as number) < minimum || (value as number) > maximum) { throw new MeshGeometryStreamError("GEOMETRY_STREAM_INVALID", `${name} must be an integer in [${minimum}, ${maximum}]`); } return value as number; } function identifier(value: unknown, name: string): string { if (typeof value !== "string" || !/^[A-Za-z0-9:._-]{1,128}$/.test(value)) { throw new MeshGeometryStreamError("GEOMETRY_STREAM_INVALID", `${name} is invalid`); } return value; } export function parseMeshGeometryStreamRequest(value: unknown): MeshGeometryStreamRequestIR { if (!record(value) || value.schemaVersion !== MESH_GEOMETRY_STREAM_SCHEMA) { throw new MeshGeometryStreamError("GEOMETRY_STREAM_INVALID", "unsupported stream request schema"); } const request: MeshGeometryStreamRequestIR = { schemaVersion: MESH_GEOMETRY_STREAM_SCHEMA, streamId: identifier(value.streamId, "streamId"), meshId: identifier(value.meshId, "meshId"), triangleCount: boundedInteger(value.triangleCount, "triangleCount", 1, MESH_GEOMETRY_STREAM_MAX_TRIANGLES), chunkTriangleCount: boundedInteger(value.chunkTriangleCount, "chunkTriangleCount", 1, MESH_GEOMETRY_STREAM_MAX_CHUNK_TRIANGLES), lodTriangleCount: boundedInteger(value.lodTriangleCount, "lodTriangleCount", 1, MESH_GEOMETRY_STREAM_MAX_LOD_TRIANGLES), }; if (request.lodTriangleCount > request.triangleCount) { throw new MeshGeometryStreamError("GEOMETRY_STREAM_INVALID", "lodTriangleCount exceeds triangleCount"); } return request; } function hex(bytes: ArrayBuffer): string { return Array.from(new Uint8Array(bytes), (value) => value.toString(16).padStart(2, "0")).join(""); } async function sha256(value: BufferSource): Promise { return hex(await crypto.subtle.digest("SHA-256", value)); } export async function meshGeometryStreamChunkSha256(chunk: Omit): Promise { const metadata = JSON.stringify([ chunk.schemaVersion, chunk.streamId, chunk.meshId, chunk.chunkIndex, chunk.chunkCount, chunk.triangleOffset, chunk.triangleCount, chunk.vertexCount, chunk.byteLength, await sha256(chunk.positions), await sha256(chunk.indices), ]); return sha256(new TextEncoder().encode(metadata)); } export async function meshGeometryStreamManifestSha256( request: MeshGeometryStreamRequestIR, chunks: readonly MeshGeometryStreamChunkRecordIR[], ): Promise { const canonical = JSON.stringify({ schemaVersion: request.schemaVersion, streamId: request.streamId, meshId: request.meshId, triangleCount: request.triangleCount, chunkTriangleCount: request.chunkTriangleCount, lodTriangleCount: request.lodTriangleCount, chunks, }); return sha256(new TextEncoder().encode(canonical)); } export class MeshGeometryStreamValidator { private readonly request: MeshGeometryStreamRequestIR; private readonly expectedChunkCount: number; private nextChunkIndex = 0; private nextTriangleOffset = 0; private transferredBytes = 0; private peakWorkingSetBytes = 0; private readonly chunks: MeshGeometryStreamChunkRecordIR[] = []; constructor(value: MeshGeometryStreamRequestIR) { this.request = parseMeshGeometryStreamRequest(value); this.expectedChunkCount = Math.ceil(this.request.triangleCount / this.request.chunkTriangleCount); } async accept(value: MeshGeometryStreamChunkIR): Promise { if (!record(value) || value.schemaVersion !== MESH_GEOMETRY_STREAM_SCHEMA || value.streamId !== this.request.streamId || value.meshId !== this.request.meshId) { throw new MeshGeometryStreamError("GEOMETRY_STREAM_RANGE_INVALID", "chunk envelope does not match the active stream"); } const expectedTriangles = Math.min(this.request.chunkTriangleCount, this.request.triangleCount - this.nextTriangleOffset); if (value.chunkIndex !== this.nextChunkIndex || value.chunkCount !== this.expectedChunkCount || value.triangleOffset !== this.nextTriangleOffset || value.triangleCount !== expectedTriangles) { throw new MeshGeometryStreamError("GEOMETRY_STREAM_RANGE_INVALID", "chunk ranges are not contiguous and complete"); } if (!(value.positions instanceof ArrayBuffer) || !(value.indices instanceof ArrayBuffer) || !Number.isSafeInteger(value.vertexCount) || value.vertexCount < 3 || value.positions.byteLength !== value.vertexCount * 3 * Float32Array.BYTES_PER_ELEMENT || value.indices.byteLength !== value.triangleCount * 3 * Uint32Array.BYTES_PER_ELEMENT || value.byteLength !== value.positions.byteLength + value.indices.byteLength) { throw new MeshGeometryStreamError("GEOMETRY_STREAM_RANGE_INVALID", "chunk binary lengths are invalid"); } const indices = new Uint32Array(value.indices); for (const index of indices) { if (index >= value.vertexCount) throw new MeshGeometryStreamError("GEOMETRY_STREAM_RANGE_INVALID", "chunk index exceeds its local vertex range"); } const { sha256: declaredSha256, ...unsigned } = value; const digest = await meshGeometryStreamChunkSha256(unsigned); if (!/^[a-f0-9]{64}$/.test(declaredSha256) || digest !== declaredSha256) { throw new MeshGeometryStreamError("GEOMETRY_STREAM_RANGE_INVALID", "chunk SHA-256 mismatch"); } this.chunks.push({ chunkIndex: value.chunkIndex, triangleOffset: value.triangleOffset, triangleCount: value.triangleCount, vertexCount: value.vertexCount, byteLength: value.byteLength, sha256: declaredSha256, }); this.nextChunkIndex += 1; this.nextTriangleOffset += value.triangleCount; this.transferredBytes += value.byteLength; this.peakWorkingSetBytes = Math.max(this.peakWorkingSetBytes, value.byteLength); } async finish(): Promise<{ chunks: MeshGeometryStreamChunkRecordIR[]; transferredBytes: number; peakWorkingSetBytes: number; manifestSha256: string }> { if (this.nextChunkIndex !== this.expectedChunkCount || this.nextTriangleOffset !== this.request.triangleCount) { throw new MeshGeometryStreamError("GEOMETRY_STREAM_RANGE_INVALID", "stream ended before all triangle ranges arrived"); } return { chunks: [...this.chunks], transferredBytes: this.transferredBytes, peakWorkingSetBytes: this.peakWorkingSetBytes, manifestSha256: await meshGeometryStreamManifestSha256(this.request, this.chunks), }; } }