Skip to main content

Built and signed on GitHub Actions

A high-performance PartitionedBuffer implementation backed by Uint32Array for efficient memory usage and fast bitwise operations.

This package works with Cloudflare Workers, Node.js, Deno, Bun, Browsers
This package works with Cloudflare Workers
This package works with Node.js
This package works with Deno
This package works with Bun
This package works with Browsers
JSR Score
100%
Published
3 months ago (0.2.8)
Package root>src>Schema.ts
/** * @module Schema * @description A schema is a component storage definition. * @copyright 2024 the PartitionedBuffer authors. All rights reserved. * @license MIT */ import { isObject, isTypedArrayConstructor, isValidName, isValidTypedArrayValue, type TypedArray, type TypedArrayConstructor, } from "./utils.ts"; /** Maximum alignment in bytes for TypedArrays */ const MAX_ALIGNMENT = 8; /** * Storage convenience object */ export type SchemaStorage<T> = { byteOffset: number; byteLength: number; partitions: Record<keyof T, TypedArray>; }; /** * Schema property definition * * A schema property is either a TypedArrayConstructor or * an array where the 0th index is a TypedArrayConstructor and the 1st index is * a default value to initialise the array to. * * @example { x: Float32Array, y: Float32Array } * @example { x: [Float32Array, 100], y: [Float32Array, 100] } * @see Schema */ export type SchemaProperty = TypedArrayConstructor | [TypedArrayConstructor, number]; /** * A schema definition is a plain object where each property is a number * * It defines what input data is valid for each schema property. * * @example ``` * type Vec2 = { x: number, y: number }; // the schema definition * const positionSchema: Schema<Vec2> = { x: Float32Array, y: Float32Array }; // works * ``` */ export type SchemaSpec<T> = { [K in keyof T]: T[K]; }; /** * Schemas are component storage definitions. * * Schemas use TypedArray objects and so can only store a single number per property per entity. * * Values in TypedArrays are initialised to 0 by default. * * To set an initial value: `{ property: [Int8Array, defaultValue] }`. * * @example ``` * type Vec2 = { x: number, y: number }; * const positionSchema: Schema<Vec2> = { x: Float32Array, y: Float32Array }; * ``` * * @example ``` * type Vec2 = { x: number, y: number }; * const positionSchema: Schema<Vec2> = { x: [Float32Array, 100], y: [Float32Array, 100] }; * ``` */ export type Schema<T extends SchemaSpec<T>> = { [K in keyof T]: SchemaProperty; }; /** * @internal * Validates the names and values of a schema's entries */ const isValidSchemaEntry = (prop: [string, SchemaProperty]): boolean => { const [name, value] = prop; if (!isValidName(name)) { return false; } if (!Array.isArray(value)) { return isTypedArrayConstructor(value); } // if this is an array, the user wants to set an initial value const [arrayConstructor, n] = value as [TypedArrayConstructor, number]; return isTypedArrayConstructor(arrayConstructor) && isValidTypedArrayValue(arrayConstructor, n); }; /** * @public * Schema type guard * @param schema the object to test */ export const isSchema = (schema: unknown): schema is Schema<SchemaSpec<unknown> | null> => { if (schema === null) return true; // Explicitly handle null schemas try { if (!isObject(schema)) return false; const entries = Object.entries(schema) as [string, SchemaProperty][]; if (!entries.length) return false; return entries.every(isValidSchemaEntry); } catch (_) { return false; } }; /** * Calculate the aligned size of a schema in bytes * @param schema the schema to calculate the size of * @returns the size in bytes */ export function getSchemaSize<T extends SchemaSpec<T>>(schema: Schema<T>): number { if (!schema || !isSchema(schema)) return Number.NaN; let size = 0; let maxAlignment = 1; const schemaEntries = Object.entries(schema); if (schemaEntries.length === 0) return 0; // First pass: find maximum alignment requirement for (const [name, value] of schemaEntries) { const Ctr = Array.isArray(value) ? value[0] : value; const alignment = Ctr.BYTES_PER_ELEMENT; // Validate alignment is power of 2 if ((alignment & (alignment - 1)) !== 0) { throw new Error(`Invalid alignment ${alignment} for property "${name}"`); } maxAlignment = Math.max(maxAlignment, Math.min(alignment, MAX_ALIGNMENT)); } // Second pass: calculate aligned size for (const [name, value] of schemaEntries) { const Ctr = Array.isArray(value) ? value[0] : value; const bytes = Ctr.BYTES_PER_ELEMENT; // Align current offset const alignedOffset = (size + bytes - 1) & ~(bytes - 1); // Check for overflow if (alignedOffset < size || alignedOffset > Number.MAX_SAFE_INTEGER - bytes) { throw new Error(`Size calculation overflow at property "${name}"`); } size = alignedOffset + bytes; } // Align final size const finalSize = (size + maxAlignment - 1) & ~(maxAlignment - 1); if (finalSize < size) { throw new Error("Final size alignment overflow"); } return finalSize; }