import { compileObject, compileUnion } from './codegen.ts'; import type { AnySchema, ObjectSchema, UnionSchema } from './descriptors.ts'; import { Reader, Writer } from './io.ts'; import { Serializable } from './symbol.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(); const byCtor = new WeakMap(); 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, '_'); } 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; } export function registerClass(Ctor: new (...args: never[]) => T): Codec { const cached = byCtor.get(Ctor); if (cached) return cached as Codec; const schema = (Ctor as unknown as Record)[Serializable] as | ObjectSchema | UnionSchema | undefined; if (!schema) { throw new Error(`${Ctor.name} has no [Serializable] schema`); } const codec = register(schema); byCtor.set(Ctor, codec); return codec; } /** * Encode a value into a framed Uint8Array (2-byte schema ID + body). * If a Writer is passed, returns a view; otherwise returns a fresh copy. */ export function serialize(value: T, codec: Codec, writer?: Writer): Uint8Array { if (writer) { writer.u16(codec.id); codec.encode(writer, value); return writer.bytes(); } const w = new Writer(); w.u16(codec.id); codec.encode(w, value); return w.bytesCopy(); } /** * Decode a framed Uint8Array by looking up its schema ID. */ export function deserialize(bytes: Uint8Array): T { const r = new Reader(bytes); const id = r.u16(); const codec = byId.get(id); if (!codec) throw new Error(`Unknown schema ID: 0x${id.toString(16)}`); return codec.decode(r) as T; } export function clearRegistry(): void { byName.clear(); byId.clear(); }