import { SERIALIZE_CODES } from "../errors/codes.js"; import { SerializationError } from "../errors/serialize-error.js"; import type { TypeDefinition } from "./registry.types.js"; export type { TypeDefinition }; // --------------------------------------------------------------------------- // Global singleton registry // --------------------------------------------------------------------------- const _byTag = new Map(); // biome-ignore lint/complexity/noBannedTypes: intentional Function key for ctor lookup const _byCtor = new Map(); /** * Register a custom type for round-trip serialization. * * Throws `SERIALIZE.REGISTRY.DUPLICATE_TAG` if the tag is already registered * for a *different* constructor (re-registering the same tag+ctor is a no-op). */ export function registerType(def: TypeDefinition): void { const existing = _byTag.get(def.tag); if (existing !== undefined && existing.ctor !== def.ctor) { throw new SerializationError( SERIALIZE_CODES["REGISTRY.DUPLICATE_TAG"], "serialize.registry.duplicate_tag", { context: { tag: def.tag } } ); } _byTag.set(def.tag, def); _byCtor.set(def.ctor, def); } /** Remove a previously registered type by tag. Silent if not found. */ export function unregisterType(tag: string): void { const existing = _byTag.get(tag); if (existing !== undefined) { _byCtor.delete(existing.ctor); _byTag.delete(tag); } } /** Return all currently registered `TypeDefinition` objects. */ export function listTypes(): TypeDefinition[] { return Array.from(_byTag.values()); } /** Look up a `TypeDefinition` by its tag string, or `undefined` if absent. */ export function getType(tag: string): TypeDefinition | undefined { return _byTag.get(tag); } /** Look up a `TypeDefinition` by constructor reference, or `undefined`. */ // biome-ignore lint/complexity/noBannedTypes: intentional Function for ctor lookup export function getTypeByCtor(ctor: Function): TypeDefinition | undefined { return _byCtor.get(ctor); } // --------------------------------------------------------------------------- // Built-in type registrations // --------------------------------------------------------------------------- function registerBuiltin(def: TypeDefinition, skipCtorIndex = false): void { // Built-ins always win; force-set even if already present (idempotent). _byTag.set(def.tag, def); if (!skipCtorIndex) { _byCtor.set(def.ctor, def); } } registerBuiltin({ tag: "__Date", ctor: Date, encode: (v) => (v as Date).toISOString(), decode: (enc) => new Date(enc as string), }); registerBuiltin({ tag: "__Map", ctor: Map, encode: (v) => Array.from((v as Map).entries()), decode: (enc) => new Map(enc as [unknown, unknown][]), }); registerBuiltin({ tag: "__Set", ctor: Set, encode: (v) => Array.from((v as Set).values()), decode: (enc) => new Set(enc as unknown[]), }); registerBuiltin({ tag: "__BigInt", ctor: BigInt, encode: (v) => (v as bigint).toString(), decode: (enc) => BigInt(enc as string), }); registerBuiltin({ tag: "__Uint8Array", ctor: Uint8Array, encode: (v) => Array.from(v as Uint8Array) .map((b) => b.toString(16).padStart(2, "0")) .join(""), decode: (enc) => { const hex = enc as string; const bytes = new Uint8Array(hex.length / 2); for (let i = 0; i < bytes.length; i++) { bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16); } return bytes; }, }); registerBuiltin({ tag: "__Int8Array", ctor: Int8Array, encode: (v) => Array.from(v as Int8Array), decode: (enc) => new Int8Array(enc as number[]), }); registerBuiltin({ tag: "__Int16Array", ctor: Int16Array, encode: (v) => Array.from(v as Int16Array), decode: (enc) => new Int16Array(enc as number[]), }); registerBuiltin({ tag: "__Int32Array", ctor: Int32Array, encode: (v) => Array.from(v as Int32Array), decode: (enc) => new Int32Array(enc as number[]), }); registerBuiltin({ tag: "__Uint16Array", ctor: Uint16Array, encode: (v) => Array.from(v as Uint16Array), decode: (enc) => new Uint16Array(enc as number[]), }); registerBuiltin({ tag: "__Uint32Array", ctor: Uint32Array, encode: (v) => Array.from(v as Uint32Array), decode: (enc) => new Uint32Array(enc as number[]), }); registerBuiltin({ tag: "__Float32Array", ctor: Float32Array, encode: (v) => Array.from(v as Float32Array), decode: (enc) => new Float32Array(enc as number[]), }); registerBuiltin({ tag: "__Float64Array", ctor: Float64Array, encode: (v) => Array.from(v as Float64Array), decode: (enc) => new Float64Array(enc as number[]), }); registerBuiltin({ tag: "__URL", ctor: URL, encode: (v) => (v as URL).href, decode: (enc) => new URL(enc as string), }); registerBuiltin({ tag: "__RegExp", ctor: RegExp, encode: (v) => { const re = v as RegExp; return { source: re.source, flags: re.flags }; }, decode: (enc) => { const { source, flags } = enc as { source: string; flags: string }; return new RegExp(source, flags); }, }); // Special sentinel registrations — these are detected by value, not by ctor. // We register tag-only entries (skipCtorIndex=true) so getType() can return // a decode handler without polluting the ctor index. registerBuiltin( { tag: "__undefined", ctor: Object, // placeholder; never used for ctor lookup encode: (_v) => null, decode: (_enc) => undefined, }, true // skip ctor index ); registerBuiltin( { tag: "__NaN", ctor: Object, // placeholder; never used for ctor lookup encode: (_v) => null, decode: (_enc) => Number.NaN, }, true // skip ctor index ); registerBuiltin( { tag: "__Infinity", ctor: Object, // placeholder; never used for ctor lookup encode: (v) => (v === Number.POSITIVE_INFINITY ? "+" : "-"), decode: (enc) => enc === "+" ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY, }, true // skip ctor index );