import { compileObject, compileUnion } from './codegen.ts'; import type { AnySchema, ObjectSchema, UnionSchema } from './descriptors.ts'; import type { Reader, Writer } from './io.ts'; export interface Codec { readonly id: number; readonly name: string; readonly encode: (w: Writer, v: T) => void; readonly decode: (r: Reader) => T; } type AnyCodec = Codec; const byName = new Map(); const byId = new Map(); function fnv1a16(s: string): number { let h = 0x811c9dc5; for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 0x01000193); } return ((h >>> 16) ^ h) & 0xffff; } function collectRefDeps(schema: AnySchema, acc: Map): void { switch (schema.kind) { case 'ref': { const target = schema.thunk(); if (!acc.has(target.name)) { acc.set(target.name, target); for (const f of Object.values(target.fields)) collectRefDeps(f, acc); } return; } case 'object': for (const f of Object.values(schema.fields)) collectRefDeps(f, acc); return; case 'array': case 'optional': collectRefDeps(schema.elem, acc); return; case 'tuple': for (const e of schema.elems) collectRefDeps(e, acc); return; case 'union': for (const v of Object.values(schema.variants)) collectRefDeps(v, acc); return; default: return; } } function sanIdent(name: string): string { return name.replace(/[^A-Za-z0-9_]/g, '_'); } /** * Compile a schema into a `Codec` and register it in the lookup tables. Used * internally by `type(...)` / `oneOf(...)`. Idempotent by schema name. */ export function register(schema: ObjectSchema | UnionSchema): Codec { const existing = byName.get(schema.name); if (existing) return existing as Codec; // Only `ref` boundaries require a separately registered codec; nested // objects/unions/arrays/etc. are inlined into the generated function. const refTargets = new Map(); if (schema.kind === 'object') { for (const f of Object.values(schema.fields)) collectRefDeps(f, refTargets); } else { for (const v of Object.values(schema.variants)) collectRefDeps(v, refTargets); } for (const dep of refTargets.values()) { if (!byName.has(dep.name)) register(dep); } const cg = schema.kind === 'object' ? compileObject(schema) : compileUnion(schema); const depsObj: Record = {}; for (const [local, info] of cg.deps) { const target = byName.get(info.targetName); if (!target) throw new Error(`Dep not registered: ${info.targetName}`); depsObj[local] = info.mode === 'enc' ? target.encode : target.decode; } const closureObj: Record = {}; for (const [k, v] of cg.closure) closureObj[k] = v; const encDeps: string[] = []; const decDeps: string[] = []; for (const [local, info] of cg.deps) { const line = `const ${local} = deps[${JSON.stringify(local)}];`; if (info.mode === 'enc') encDeps.push(line); else decDeps.push(line); } const closureDecls = [...cg.closure.keys()] .map((k) => `const ${k} = closure[${JSON.stringify(k)}];`) .join('\n'); const fname = sanIdent(schema.name); const encSrc = `${encDeps.join('\n')} ${closureDecls} return function encode_${fname}(w, o) { let pos = w.pos; let buf = w.buf; let view = w.view; ${cg.encodeBody} w.pos = pos; };`; const decSrc = `${decDeps.join('\n')} ${closureDecls} return function decode_${fname}(r) { let pos = r.pos; const buf = r.buf; const view = r.view; ${cg.decodeBody} };`; let encFn: (w: Writer, v: T) => void; let decFn: (r: Reader) => T; try { encFn = new Function('deps', 'closure', encSrc)(depsObj, closureObj); decFn = new Function('deps', 'closure', decSrc)(depsObj, closureObj); } catch (e) { throw new Error( `Codegen failed for "${schema.name}": ${(e as Error).message}\n--- encode source ---\n${encSrc}\n--- decode source ---\n${decSrc}`, ); } const id = fnv1a16(schema.name); const idExisting = byId.get(id); if (idExisting) { throw new Error( `Schema ID collision: "${schema.name}" and "${idExisting.name}" both hash to 0x${id.toString(16)}`, ); } const codec: Codec = Object.freeze({ id, name: schema.name, encode: encFn, decode: decFn, }); byName.set(schema.name, codec); byId.set(id, codec); return codec; } /** Reset the global codec registry. Test helper. */ export function clearRegistry(): void { byName.clear(); byId.clear(); } /** * Internal: AOT-generated codecs use this to inject themselves into the * runtime registry on module load. Not meant for user code. */ export function __registerPrecompiled(codec: Codec): Codec { const existing = byName.get(codec.name); if (existing) return existing as Codec; const idExisting = byId.get(codec.id); if (idExisting && idExisting.name !== codec.name) { throw new Error( `Schema ID collision: "${codec.name}" and "${idExisting.name}" both hash to 0x${codec.id.toString(16)}`, ); } byName.set(codec.name, codec as AnyCodec); byId.set(codec.id, codec as AnyCodec); return codec; }