feat(serializer): add aot serializer

This commit is contained in:
2026-05-21 09:11:51 +00:00
parent 6f417ba514
commit f327e64a6a
30 changed files with 6720 additions and 0 deletions
+329
View File
@@ -0,0 +1,329 @@
/**
* Simplified façade over the schema + codec system.
*
* 90% of usage:
* const Order = type('Order', { id: u53, price: f64, side: enumOf(['buy','sell'] as const) });
* type Order = typeof Order.$infer;
* const bytes = Order.encode(order);
* const back = Order.decode(bytes);
*
* Hot path:
* const w = new Writer(1024);
* Order.encodeInto(order, w);
* socket.send(w.bytes());
*
* AOT (compile-only) build: the transformer detects `type(...)` calls and
* replaces them with precomputed codec literals; the function below never runs
* in production. See codegen-plugin/.
*/
import type {
AnySchema,
ArraySchema,
BitsetSchema,
EnumSchema,
ObjectSchema,
OptionalSchema,
TupleSchema,
UnionSchema,
} from './descriptors.ts';
import type { Reader, Writer } from './io.ts';
import { Writer as WriterImpl } from './io.ts';
import { Reader as ReaderImpl } from './io.ts';
import { defineSchema, s } from './schema.ts';
import { register as registerSchema } from './register.ts';
// ── Primitive markers (re-export of the descriptor singletons) ─────────────
export const u8 = s.u8;
export const u16 = s.u16;
export const u32 = s.u32;
export const i8 = s.i8;
export const i16 = s.i16;
export const i32 = s.i32;
export const u53 = s.u53;
export const i53 = s.i53;
export const u64 = s.u64;
export const i64 = s.i64;
export const f32 = s.f32;
export const f64 = s.f64;
export const bool = s.bool;
export const str = s.str;
export const bytes = s.bytes;
export const f32Array = s.f32Array;
export const f64Array = s.f64Array;
export const u8Array = s.u8Array;
export const u16Array = s.u16Array;
export const u32Array = s.u32Array;
export const i32Array = s.i32Array;
// ── Combinators ────────────────────────────────────────────────────────────
export function list<E extends AnySchema>(elem: E): ArraySchema<E> {
return s.array(elem);
}
export function opt<E extends AnySchema>(elem: E): OptionalSchema<E> {
return s.optional(elem);
}
export function enumOf<L extends readonly string[]>(values: L): EnumSchema<L> {
return s.enum(values);
}
export function flags<L extends readonly string[]>(names: L): BitsetSchema<L> {
return s.bitset(names);
}
export function tuple<E extends readonly AnySchema[]>(...elems: E): TupleSchema<E> {
return s.tuple(...elems);
}
// ── TypeCodec: schema + runtime API in one value ───────────────────────────
/**
* A `TypeCodec<T>` is BOTH:
* - an `ObjectSchema` (so it can be used as a field in another `type(...)`)
* - a runtime codec with `encode`/`decode` methods
*
* Plus a phantom `$infer` field for `typeof T.$infer` extraction.
*/
export interface TypeCodec<T> extends ObjectSchema {
readonly id: number;
/** One-shot encode. Allocates a Writer; for hot paths prefer `encodeInto`. */
encode(value: T): Uint8Array;
encode(value: T, into: Writer): Uint8Array;
/** One-shot decode from a complete byte buffer. */
decode(bytes: Uint8Array): T;
/** Hot path: writes directly into a pre-allocated, pooled Writer. */
encodeInto(value: T, w: Writer): void;
/** Hot path: reads from a positioned Reader (does not advance past end). */
decodeFrom(r: Reader): T;
/** Phantom: `typeof Codec.$infer` gives the inferred TS type. Never read at runtime. */
readonly $infer: T;
}
// ── Type-level inference ───────────────────────────────────────────────────
type InferPrim<K> =
K extends 'u8' | 'u16' | 'u32' | 'i8' | 'i16' | 'i32' | 'u53' | 'i53' | 'f32' | 'f64' ? number
: K extends 'u64' | 'i64' ? bigint
: K extends 'bool' ? boolean
: K extends 'str' ? string
: K extends 'bytes' ? Uint8Array
: K extends 'f32Array' ? Float32Array
: K extends 'f64Array' ? Float64Array
: K extends 'u8Array' ? Uint8Array
: K extends 'u16Array' ? Uint16Array
: K extends 'u32Array' ? Uint32Array
: K extends 'i32Array' ? Int32Array
: never;
export type InferType<S> =
S extends { $infer: infer T } ? T
: S extends ArraySchema<infer E> ? InferType<E>[]
: S extends OptionalSchema<infer E> ? InferType<E> | undefined
: S extends EnumSchema<infer L> ? L[number]
: S extends BitsetSchema<infer L> ? { [K in L[number]]: boolean }
: S extends TupleSchema<infer E> ? { -readonly [K in keyof E]: InferType<E[K]> }
: S extends UnionSchema<infer D, infer V>
? V extends Record<string, ObjectSchema>
? {
[K in keyof V & string]: V[K] extends ObjectSchema<infer F>
? { [P in D]: K } & { [Pk in keyof F]: InferType<F[Pk]> }
: never
}[keyof V & string]
: never
: S extends ObjectSchema<infer F> ? { [K in keyof F]: InferType<F[K]> }
: S extends { kind: infer K } ? InferPrim<K>
: unknown;
type Fields = Record<string, AnySchema>;
// ── Anonymous naming ───────────────────────────────────────────────────────
let __anonCounter = 0;
function anonName(prefix = 'Anon'): string {
return `__${prefix}_${++__anonCounter}`;
}
// ── type() ────────────────────────────────────────────────────────────────
/**
* Define a serializable type.
*
* The `name` is REQUIRED for wire-stable types (anything sent over a network
* or stored to disk) because the schema ID is a hash of the name and the ID
* appears in the wire frame. Without a name we generate one, which is fine
* for transient/in-process use only.
*/
export function type<F extends Fields>(fields: F): TypeCodec<{ [K in keyof F]: InferType<F[K]> }>;
export function type<F extends Fields>(name: string, fields: F): TypeCodec<{ [K in keyof F]: InferType<F[K]> }>;
export function type(nameOrFields: string | Fields, maybeFields?: Fields): TypeCodec<unknown> {
const isNamed = typeof nameOrFields === 'string';
const name = isNamed ? nameOrFields : anonName('Type');
const fields = isNamed ? maybeFields! : nameOrFields;
const schema = defineSchema(name, () => fields);
const codec = registerSchema(schema);
const enc = codec.encode;
const dec = codec.decode;
function encode(value: unknown, into?: Writer): Uint8Array {
if (into) {
enc(into, value);
return into.bytes();
}
const tmp = new WriterImpl();
enc(tmp, value);
return tmp.bytesCopy();
}
function decode(b: Uint8Array): unknown {
const r = new ReaderImpl(b);
return dec(r);
}
function encodeInto(value: unknown, w: Writer): void {
enc(w, value);
}
function decodeFrom(r: Reader): unknown {
return dec(r);
}
return {
kind: 'object',
name,
fields: schema.fields,
id: codec.id,
encode,
decode,
encodeInto,
decodeFrom,
$infer: undefined as unknown,
} as TypeCodec<unknown>;
}
// ── oneOf() — discriminated union ──────────────────────────────────────────
/**
* Discriminated union. Each variant is a field map; at runtime the variant is
* identified by a `discriminator` key on the value.
*
* const Event = oneOf('kind', {
* fill: { price: f64, qty: f64 },
* cancel: { reason: str },
* });
* // fill → { kind: 'fill', price, qty }
* // cancel → { kind: 'cancel', reason }
*/
// Less-precise but stable inference for unions; user can narrow via `as`.
export function oneOf<D extends string, V extends Record<string, Fields>>(
discriminator: D,
variants: V,
): TypeCodec<{ [P in D]: keyof V & string } & Record<string, unknown>>;
export function oneOf<D extends string, V extends Record<string, Fields>>(
name: string,
discriminator: D,
variants: V,
): TypeCodec<{ [P in D]: keyof V & string } & Record<string, unknown>>;
export function oneOf(
arg1: string,
arg2: string | Record<string, Fields>,
arg3?: Record<string, Fields>,
): TypeCodec<unknown> {
const isNamed = arg3 !== undefined;
const name = isNamed ? arg1 : anonName('Union');
const discriminator = (isNamed ? arg2 : arg1) as string;
const variants = (isNamed ? arg3! : arg2) as Record<string, Fields>;
const schema = s.union(name, discriminator, variants);
const codec = registerSchema(schema);
const enc = codec.encode;
const dec = codec.decode;
function encode(value: unknown, into?: Writer): Uint8Array {
if (into) {
enc(into, value);
return into.bytes();
}
const tmp = new WriterImpl();
enc(tmp, value);
return tmp.bytesCopy();
}
function decode(b: Uint8Array): unknown {
const r = new ReaderImpl(b);
return dec(r);
}
// Union doesn't satisfy ObjectSchema directly — it's UnionSchema. We expose
// the runtime API on the same value but the descriptor shape is union.
// For use as a field, accept it via AnySchema (TypeScript inference handles it).
return {
kind: 'union',
name,
discriminator,
variants: (schema as unknown as { variants: unknown }).variants,
fields: {}, // unused for unions
id: codec.id,
encode,
decode,
encodeInto(value: unknown, w: Writer) { enc(w, value); },
decodeFrom(r: Reader) { return dec(r); },
$infer: undefined as unknown,
} as unknown as TypeCodec<unknown>;
}
// ── Router: framed multi-type dispatch ─────────────────────────────────────
export interface Router {
/** Encode with the 2-byte schema-ID frame. */
encode<T>(value: T, codec: TypeCodec<T>): Uint8Array;
encode<T>(value: T, codec: TypeCodec<T>, into: Writer): Uint8Array;
/** Decode a framed message, dispatching by schema ID. */
decode(bytes: Uint8Array): unknown;
}
/**
* Build a router for framed multi-message protocols. The router prepends a
* 2-byte schema ID on encode and dispatches on it during decode.
*/
export function router(...codecs: TypeCodec<unknown>[]): Router {
const byId = new Map<number, TypeCodec<unknown>>();
for (const c of codecs) byId.set(c.id, c);
return {
encode<T>(value: T, codec: TypeCodec<T>, into?: Writer): Uint8Array {
if (into) {
into.u16(codec.id);
codec.encodeInto(value, into);
return into.bytes();
}
const w = new WriterImpl();
w.u16(codec.id);
codec.encodeInto(value, w);
return w.bytesCopy();
},
decode(bytes: Uint8Array): unknown {
const r = new ReaderImpl(bytes);
const id = r.u16();
const codec = byId.get(id);
if (!codec) throw new Error(`Router: unknown schema ID 0x${id.toString(16)}`);
return codec.decodeFrom(r);
},
};
}
// ── Writer/Reader re-exports for hot path users ────────────────────────────
export { Writer, Reader } from './io.ts';
+729
View File
@@ -0,0 +1,729 @@
import type {
AnySchema,
ObjectSchema,
TypedArrayKind,
UnionSchema,
} from './descriptors.ts';
export interface CodegenResult {
/** Inner-function body for encoder: takes (w, o), uses lifted pos/buf/view locals. */
encodeBody: string;
/** Inner-function body for decoder: takes (r), uses lifted pos/buf/view locals. */
decodeBody: string;
/** Cross-codec dependencies for ref/codec fields only (nested objects are inlined). */
deps: Map<string, { mode: 'enc' | 'dec'; targetName: string }>;
/** Closure-captured values (enum maps, codec functions). */
closure: Map<string, unknown>;
}
function sanitize(name: string): string {
return name.replace(/[^A-Za-z0-9_]/g, '_');
}
const TA_ELEM_SIZE: Record<TypedArrayKind, number> = {
f32Array: 4,
f64Array: 8,
u8Array: 1,
u16Array: 2,
u32Array: 4,
i32Array: 4,
};
const TA_CTOR: Record<TypedArrayKind, string> = {
f32Array: 'Float32Array',
f64Array: 'Float64Array',
u8Array: 'Uint8Array',
u16Array: 'Uint16Array',
u32Array: 'Uint32Array',
i32Array: 'Int32Array',
};
function isTypedArrayKind(k: string): k is TypedArrayKind {
return k in TA_ELEM_SIZE;
}
/**
* Upper bound on bytes the schema may write, or null when truly variable
* (str, bytes, array, typed-array, ref, codec, and any composite containing them).
*/
function maxBytes(schema: AnySchema): number | null {
switch (schema.kind) {
case 'u8':
case 'i8':
case 'bool':
case 'enum':
return 1;
case 'u16':
case 'i16':
return 2;
case 'u32':
case 'i32':
case 'f32':
return 4;
case 'f64':
return 8;
case 'u53':
case 'i53':
case 'u64':
case 'i64':
return 10;
case 'bitset': {
const n = schema.flags.length;
return n <= 8 ? 1 : n <= 16 ? 2 : n <= 32 ? 4 : 10;
}
case 'tuple': {
let sum = 0;
for (const e of schema.elems) {
const m = maxBytes(e);
if (m === null) return null;
sum += m;
}
return sum;
}
case 'object': {
let sum = 0;
for (const f of Object.values(schema.fields)) {
const m = maxBytes(f);
if (m === null) return null;
sum += m;
}
return sum;
}
case 'optional': {
const m = maxBytes(schema.elem);
return m === null ? null : 1 + m;
}
case 'union': {
let max = 0;
for (const v of Object.values(schema.variants)) {
const m = maxBytes(v);
if (m === null) return null;
if (m > max) max = m;
}
return 1 + max;
}
case 'array':
case 'str':
case 'bytes':
case 'ref':
case 'codec':
return null;
}
return null;
}
class Ctx {
private counter = 0;
mode: 'enc' | 'dec';
deps = new Map<string, { mode: 'enc' | 'dec'; targetName: string }>();
closure = new Map<string, unknown>();
constructor(mode: 'enc' | 'dec') {
this.mode = mode;
}
fresh(prefix: string): string {
return `_${prefix}${++this.counter}`;
}
closureVar(prefix: string, value: unknown): string {
const name = `__cv_${prefix}_${++this.counter}`;
this.closure.set(name, value);
return name;
}
}
/**
* Builds an encoder body. Bounded ops are buffered with a running budget;
* unbounded ops (str, array, ref, etc.) flush the buffer with a single
* `ensure(budget)` then emit themselves. Inside arrays with bounded elements,
* we pre-ensure the whole batch and run the element loop in "noEnsure" mode.
*/
class SegBuilder {
private buffered: string[] = [];
private budget = 0;
private output: string[] = [];
noEnsure = false;
addBounded(stmt: string, maxBytesSize: number): void {
if (this.noEnsure) {
this.output.push(stmt);
return;
}
this.buffered.push(stmt);
this.budget += maxBytesSize;
}
addPrelude(stmt: string): void {
if (this.noEnsure) {
this.output.push(stmt);
return;
}
this.buffered.push(stmt);
}
flush(): void {
if (this.buffered.length === 0) return;
if (this.budget > 0 && !this.noEnsure) {
this.output.push(
`if (pos + ${this.budget} > buf.byteLength) { w.pos = pos; w.grow(${this.budget}); buf = w.buf; view = w.view; }`,
);
}
this.output.push(...this.buffered);
this.buffered = [];
this.budget = 0;
}
addUnbounded(stmt: string): void {
this.flush();
this.output.push(stmt);
}
build(): string {
this.flush();
return this.output.join('\n');
}
}
function emitEnc(schema: AnySchema, ctx: Ctx, seg: SegBuilder, vx: string): void {
switch (schema.kind) {
case 'u8':
seg.addBounded(`buf[pos++] = (${vx}) & 0xff;`, 1);
return;
case 'i8':
seg.addBounded(`buf[pos++] = (${vx}) & 0xff;`, 1);
return;
case 'bool':
seg.addBounded(`buf[pos++] = ${vx} ? 1 : 0;`, 1);
return;
case 'u16':
seg.addBounded(`view.setUint16(pos, ${vx}, true); pos += 2;`, 2);
return;
case 'i16':
seg.addBounded(`view.setInt16(pos, ${vx}, true); pos += 2;`, 2);
return;
case 'u32':
seg.addBounded(`view.setUint32(pos, ${vx}, true); pos += 4;`, 4);
return;
case 'i32':
seg.addBounded(`view.setInt32(pos, ${vx}, true); pos += 4;`, 4);
return;
case 'f32':
seg.addBounded(`view.setFloat32(pos, ${vx}, true); pos += 4;`, 4);
return;
case 'f64':
seg.addBounded(`view.setFloat64(pos, ${vx}, true); pos += 8;`, 8);
return;
case 'u53': {
const v = ctx.fresh('v');
seg.addBounded(
`{ let ${v} = ${vx}; while (${v} >= 0x80) { buf[pos++] = (${v} & 0x7f) | 0x80; ${v} = Math.floor(${v} / 128); } buf[pos++] = ${v}; }`,
10,
);
return;
}
case 'i53': {
const v = ctx.fresh('v');
const src = ctx.fresh('s');
seg.addBounded(
`{ const ${src} = ${vx}; let ${v} = ${src} >= 0 ? BigInt(${src}) * 2n : -BigInt(${src}) * 2n - 1n; while (${v} >= 0x80n) { buf[pos++] = Number(${v} & 0x7fn) | 0x80; ${v} >>= 7n; } buf[pos++] = Number(${v}); }`,
10,
);
return;
}
case 'u64': {
const v = ctx.fresh('v');
seg.addBounded(
`{ let ${v} = ${vx}; while (${v} >= 0x80n) { buf[pos++] = Number(${v} & 0x7fn) | 0x80; ${v} >>= 7n; } buf[pos++] = Number(${v}); }`,
10,
);
return;
}
case 'i64': {
const v = ctx.fresh('v');
const src = ctx.fresh('s');
seg.addBounded(
`{ const ${src} = ${vx}; let ${v} = ${src} >= 0n ? ${src} << 1n : (-${src} << 1n) - 1n; while (${v} >= 0x80n) { buf[pos++] = Number(${v} & 0x7fn) | 0x80; ${v} >>= 7n; } buf[pos++] = Number(${v}); }`,
10,
);
return;
}
case 'enum': {
const vals = schema.values;
if (vals.length === 2) {
seg.addBounded(`buf[pos++] = ${vx} === ${JSON.stringify(vals[0])} ? 0 : 1;`, 1);
} else if (vals.length === 3) {
seg.addBounded(
`buf[pos++] = ${vx} === ${JSON.stringify(vals[0])} ? 0 : ${vx} === ${JSON.stringify(vals[1])} ? 1 : 2;`,
1,
);
} else {
const map: Record<string, number> = Object.create(null);
for (let i = 0; i < vals.length; i++) map[vals[i]!] = i;
const cv = ctx.closureVar('enum', Object.freeze(map));
seg.addBounded(`buf[pos++] = ${cv}[${vx}];`, 1);
}
return;
}
case 'bitset': {
const b = ctx.fresh('b');
const flags = schema.flags;
if (flags.length <= 32) {
const parts: string[] = ['0'];
for (let i = 0; i < flags.length; i++) {
parts.push(`((${b})[${JSON.stringify(flags[i])}] ? ${1 << i} : 0)`);
}
const expr = parts.join(' | ');
if (flags.length <= 8) {
seg.addBounded(`{ const ${b} = ${vx}; buf[pos++] = ${expr}; }`, 1);
} else if (flags.length <= 16) {
seg.addBounded(`{ const ${b} = ${vx}; view.setUint16(pos, ${expr}, true); pos += 2; }`, 2);
} else {
seg.addBounded(`{ const ${b} = ${vx}; view.setUint32(pos, ${expr}, true); pos += 4; }`, 4);
}
} else {
let big = '0n';
for (let i = 0; i < flags.length; i++) {
big = `(${big}) | ((${b})[${JSON.stringify(flags[i])}] ? ${1n << BigInt(i)}n : 0n)`;
}
const v = ctx.fresh('v');
seg.addBounded(
`{ const ${b} = ${vx}; let ${v} = ${big}; while (${v} >= 0x80n) { buf[pos++] = Number(${v} & 0x7fn) | 0x80; ${v} >>= 7n; } buf[pos++] = Number(${v}); }`,
10,
);
}
return;
}
case 'str': {
seg.addUnbounded(
`w.pos = pos; w.str(${vx}); pos = w.pos; buf = w.buf; view = w.view;`,
);
return;
}
case 'bytes': {
seg.addUnbounded(
`w.pos = pos; w.bytesPrefixed(${vx}); pos = w.pos; buf = w.buf; view = w.view;`,
);
return;
}
case 'tuple': {
const t = ctx.fresh('t');
seg.addPrelude(`const ${t} = ${vx};`);
for (let i = 0; i < schema.elems.length; i++) {
emitEnc(schema.elems[i]!, ctx, seg, `${t}[${i}]`);
}
return;
}
case 'optional': {
const o = ctx.fresh('o');
const innerMax = maxBytes(schema.elem);
if (innerMax !== null) {
const innerSeg = new SegBuilder();
innerSeg.noEnsure = true;
emitEnc(schema.elem, ctx, innerSeg, o);
const innerSrc = innerSeg.build();
seg.addBounded(
`{ const ${o} = ${vx}; if (${o} === undefined || ${o} === null) { buf[pos++] = 0; } else { buf[pos++] = 1; ${innerSrc} } }`,
1 + innerMax,
);
} else {
const innerSeg = new SegBuilder();
emitEnc(schema.elem, ctx, innerSeg, o);
const innerSrc = innerSeg.build();
seg.addUnbounded(
`{ const ${o} = ${vx}; if (pos + 1 > buf.byteLength) { w.pos = pos; w.grow(1); buf = w.buf; view = w.view; } if (${o} === undefined || ${o} === null) { buf[pos++] = 0; } else { buf[pos++] = 1; ${innerSrc} } }`,
);
}
return;
}
case 'object': {
const oo = ctx.fresh('oo');
seg.addPrelude(`const ${oo} = ${vx};`);
for (const fname of Object.keys(schema.fields)) {
emitEnc(schema.fields[fname]!, ctx, seg, `${oo}[${JSON.stringify(fname)}]`);
}
return;
}
case 'union': {
const u = ctx.fresh('u');
const disc = JSON.stringify(schema.discriminator);
const keys = Object.keys(schema.variants);
const cases: string[] = [];
for (let i = 0; i < keys.length; i++) {
const variant = schema.variants[keys[i]!]!;
const innerSeg = new SegBuilder();
innerSeg.addBounded(`buf[pos++] = ${i};`, 1);
for (const fname of Object.keys(variant.fields)) {
emitEnc(variant.fields[fname]!, ctx, innerSeg, `${u}[${JSON.stringify(fname)}]`);
}
cases.push(`case ${JSON.stringify(keys[i])}: { ${innerSeg.build()} break; }`);
}
seg.addUnbounded(
`{ const ${u} = ${vx}; switch (${u}[${disc}]) { ${cases.join('\n')} default: throw new Error('Bad union variant: ' + ${u}[${disc}]); } }`,
);
return;
}
case 'array': {
const arr = ctx.fresh('arr');
const L = ctx.fresh('L');
const i = ctx.fresh('i');
const elemMax = maxBytes(schema.elem);
seg.addPrelude(`const ${arr} = ${vx}; const ${L} = ${arr}.length;`);
const vL = ctx.fresh('vL');
seg.addBounded(
`{ let ${vL} = ${L}; while (${vL} >= 0x80) { buf[pos++] = (${vL} & 0x7f) | 0x80; ${vL} = Math.floor(${vL} / 128); } buf[pos++] = ${vL}; }`,
10,
);
if (elemMax !== null) {
seg.addUnbounded(
`if (pos + ${L} * ${elemMax} > buf.byteLength) { w.pos = pos; w.grow(${L} * ${elemMax}); buf = w.buf; view = w.view; }`,
);
const elemSeg = new SegBuilder();
elemSeg.noEnsure = true;
emitEnc(schema.elem, ctx, elemSeg, `${arr}[${i}]`);
const elemSrc = elemSeg.build();
seg.addUnbounded(`for (let ${i} = 0; ${i} < ${L}; ${i}++) { ${elemSrc} }`);
} else {
const elemSeg = new SegBuilder();
emitEnc(schema.elem, ctx, elemSeg, `${arr}[${i}]`);
const elemSrc = elemSeg.build();
seg.addUnbounded(`for (let ${i} = 0; ${i} < ${L}; ${i}++) { ${elemSrc} }`);
}
return;
}
case 'ref': {
const resolved = schema.thunk();
const dep = `__enc_${sanitize(resolved.name)}`;
ctx.deps.set(dep, { mode: 'enc', targetName: resolved.name });
seg.addUnbounded(
`w.pos = pos; ${dep}(w, ${vx}); pos = w.pos; buf = w.buf; view = w.view;`,
);
return;
}
case 'codec': {
const cv = ctx.closureVar('codec_enc', schema.encode);
seg.addUnbounded(
`w.pos = pos; ${cv}(w, ${vx}); pos = w.pos; buf = w.buf; view = w.view;`,
);
return;
}
}
if (isTypedArrayKind(schema.kind)) {
const ta = ctx.fresh('ta');
seg.addUnbounded(
`{ const ${ta} = ${vx}; w.pos = pos; w.varu53(${ta}.length); w.raw(new Uint8Array(${ta}.buffer, ${ta}.byteOffset, ${ta}.byteLength)); pos = w.pos; buf = w.buf; view = w.view; }`,
);
return;
}
throw new Error(`emitEnc: unknown kind ${(schema as { kind: string }).kind}`);
}
function emitDec(schema: AnySchema, ctx: Ctx): { pre: string; expr: string } {
switch (schema.kind) {
case 'u8':
return { pre: '', expr: 'buf[pos++]' };
case 'i8':
return { pre: '', expr: '(buf[pos++] << 24 >> 24)' };
case 'bool':
return { pre: '', expr: '(buf[pos++] !== 0)' };
case 'u16': {
const v = ctx.fresh('v');
return { pre: `const ${v} = view.getUint16(pos, true); pos += 2;`, expr: v };
}
case 'i16': {
const v = ctx.fresh('v');
return { pre: `const ${v} = view.getInt16(pos, true); pos += 2;`, expr: v };
}
case 'u32': {
const v = ctx.fresh('v');
return { pre: `const ${v} = view.getUint32(pos, true); pos += 4;`, expr: v };
}
case 'i32': {
const v = ctx.fresh('v');
return { pre: `const ${v} = view.getInt32(pos, true); pos += 4;`, expr: v };
}
case 'f32': {
const v = ctx.fresh('v');
return { pre: `const ${v} = view.getFloat32(pos, true); pos += 4;`, expr: v };
}
case 'f64': {
const v = ctx.fresh('v');
return { pre: `const ${v} = view.getFloat64(pos, true); pos += 8;`, expr: v };
}
case 'u53': {
const v = ctx.fresh('v');
const vv = ctx.fresh('vv');
const m = ctx.fresh('m');
const b = ctx.fresh('b');
return {
pre: `let ${v}; { let ${vv} = 0, ${m} = 1, ${b}; do { ${b} = buf[pos++]; ${vv} += (${b} & 0x7f) * ${m}; ${m} *= 128; } while (${b} & 0x80); ${v} = ${vv}; }`,
expr: v,
};
}
case 'i53': {
const v = ctx.fresh('v');
const vv = ctx.fresh('vv');
const sh = ctx.fresh('sh');
const b = ctx.fresh('b');
return {
pre: `let ${v}; { let ${vv} = 0n, ${sh} = 0n, ${b}; do { ${b} = buf[pos++]; ${vv} |= BigInt(${b} & 0x7f) << ${sh}; ${sh} += 7n; } while (${b} & 0x80); const _z = (${vv} & 1n) === 0n ? ${vv} >> 1n : -((${vv} >> 1n) + 1n); ${v} = Number(_z); }`,
expr: v,
};
}
case 'u64': {
const v = ctx.fresh('v');
const vv = ctx.fresh('vv');
const sh = ctx.fresh('sh');
const b = ctx.fresh('b');
return {
pre: `let ${v}; { let ${vv} = 0n, ${sh} = 0n, ${b}; do { ${b} = buf[pos++]; ${vv} |= BigInt(${b} & 0x7f) << ${sh}; ${sh} += 7n; } while (${b} & 0x80); ${v} = ${vv}; }`,
expr: v,
};
}
case 'i64': {
const v = ctx.fresh('v');
const vv = ctx.fresh('vv');
const sh = ctx.fresh('sh');
const b = ctx.fresh('b');
return {
pre: `let ${v}; { let ${vv} = 0n, ${sh} = 0n, ${b}; do { ${b} = buf[pos++]; ${vv} |= BigInt(${b} & 0x7f) << ${sh}; ${sh} += 7n; } while (${b} & 0x80); ${v} = (${vv} & 1n) === 0n ? ${vv} >> 1n : -((${vv} >> 1n) + 1n); }`,
expr: v,
};
}
case 'enum': {
const vals = schema.values;
if (vals.length <= 2) {
return {
pre: '',
expr: `(buf[pos++] === 0 ? ${JSON.stringify(vals[0])} : ${JSON.stringify(vals[1])})`,
};
} else if (vals.length === 3) {
const v = ctx.fresh('v');
const t = ctx.fresh('t');
return {
pre: `let ${v}; { const ${t} = buf[pos++]; ${v} = ${t} === 0 ? ${JSON.stringify(vals[0])} : ${t} === 1 ? ${JSON.stringify(vals[1])} : ${JSON.stringify(vals[2])}; }`,
expr: v,
};
} else {
const cv = ctx.closureVar('enum_dec', Object.freeze(vals.slice()));
return { pre: '', expr: `${cv}[buf[pos++]]` };
}
}
case 'bitset': {
const v = ctx.fresh('v');
const raw = ctx.fresh('raw');
const flags = schema.flags;
let rawDecl: string;
let isBig = false;
if (flags.length <= 8) {
rawDecl = `const ${raw} = buf[pos++];`;
} else if (flags.length <= 16) {
rawDecl = `const ${raw} = view.getUint16(pos, true); pos += 2;`;
} else if (flags.length <= 32) {
rawDecl = `const ${raw} = view.getUint32(pos, true); pos += 4;`;
} else {
isBig = true;
const vv = ctx.fresh('vv');
const sh = ctx.fresh('sh');
const b = ctx.fresh('b');
rawDecl = `let ${raw}; { let ${vv} = 0n, ${sh} = 0n, ${b}; do { ${b} = buf[pos++]; ${vv} |= BigInt(${b} & 0x7f) << ${sh}; ${sh} += 7n; } while (${b} & 0x80); ${raw} = ${vv}; }`;
}
const props: string[] = [];
for (let i = 0; i < flags.length; i++) {
if (isBig) {
props.push(`${JSON.stringify(flags[i])}: (${raw} & ${1n << BigInt(i)}n) !== 0n`);
} else {
props.push(`${JSON.stringify(flags[i])}: (${raw} & ${1 << i}) !== 0`);
}
}
return {
pre: `${rawDecl} const ${v} = { ${props.join(', ')} };`,
expr: v,
};
}
case 'str': {
const v = ctx.fresh('v');
return {
pre: `r.pos = pos; const ${v} = r.str(); pos = r.pos;`,
expr: v,
};
}
case 'bytes': {
const v = ctx.fresh('v');
return {
pre: `r.pos = pos; const ${v} = r.bytesPrefixed(); pos = r.pos;`,
expr: v,
};
}
case 'tuple': {
let pre = '';
const exprs: string[] = [];
for (const elem of schema.elems) {
const inner = emitDec(elem, ctx);
if (inner.pre === '') {
exprs.push(inner.expr);
} else {
const tmp = ctx.fresh('tupv');
pre += `${inner.pre} const ${tmp} = ${inner.expr};`;
exprs.push(tmp);
}
}
const tup = ctx.fresh('tup');
return { pre: `${pre} const ${tup} = [${exprs.join(', ')}];`, expr: tup };
}
case 'optional': {
const v = ctx.fresh('opt');
const inner = emitDec(schema.elem, ctx);
return {
pre: `let ${v} = undefined; if (buf[pos++] !== 0) { ${inner.pre} ${v} = ${inner.expr}; }`,
expr: v,
};
}
case 'object': {
let pre = '';
const props: string[] = [];
for (const fname of Object.keys(schema.fields)) {
const inner = emitDec(schema.fields[fname]!, ctx);
const tmp = ctx.fresh(`f_${sanitize(fname)}`);
pre += `${inner.pre} const ${tmp} = ${inner.expr};`;
props.push(`${JSON.stringify(fname)}: ${tmp}`);
}
const obj = ctx.fresh('obj');
return { pre: `${pre} const ${obj} = { ${props.join(', ')} };`, expr: obj };
}
case 'union': {
const v = ctx.fresh('un');
const disc = JSON.stringify(schema.discriminator);
const keys = Object.keys(schema.variants);
const cases: string[] = [];
for (let i = 0; i < keys.length; i++) {
const variant = schema.variants[keys[i]!]!;
let varPre = '';
const props: string[] = [`${disc}: ${JSON.stringify(keys[i])}`];
for (const fname of Object.keys(variant.fields)) {
const inner = emitDec(variant.fields[fname]!, ctx);
if (inner.pre === '') {
props.push(`${JSON.stringify(fname)}: ${inner.expr}`);
} else {
const tmp = ctx.fresh('uv');
varPre += `${inner.pre} const ${tmp} = ${inner.expr};`;
props.push(`${JSON.stringify(fname)}: ${tmp}`);
}
}
cases.push(`case ${i}: { ${varPre} ${v} = { ${props.join(', ')} }; break; }`);
}
return {
pre: `let ${v}; switch (buf[pos++]) { ${cases.join(' ')} default: throw new Error('Bad union tag'); }`,
expr: v,
};
}
case 'array': {
const a = ctx.fresh('arr');
const L = ctx.fresh('len');
const i = ctx.fresh('i');
const vv = ctx.fresh('vv');
const m = ctx.fresh('m');
const b = ctx.fresh('b');
const elem = emitDec(schema.elem, ctx);
const lenRead = `let ${L}; { let ${vv} = 0, ${m} = 1, ${b}; do { ${b} = buf[pos++]; ${vv} += (${b} & 0x7f) * ${m}; ${m} *= 128; } while (${b} & 0x80); ${L} = ${vv}; }`;
return {
pre: `${lenRead} const ${a} = new Array(${L}); for (let ${i} = 0; ${i} < ${L}; ${i}++) { ${elem.pre} ${a}[${i}] = ${elem.expr}; }`,
expr: a,
};
}
case 'ref': {
const resolved = schema.thunk();
const dep = `__dec_${sanitize(resolved.name)}`;
ctx.deps.set(dep, { mode: 'dec', targetName: resolved.name });
const v = ctx.fresh('v');
return {
pre: `r.pos = pos; const ${v} = ${dep}(r); pos = r.pos;`,
expr: v,
};
}
case 'codec': {
const cv = ctx.closureVar('codec_dec', schema.decode);
const v = ctx.fresh('v');
return {
pre: `r.pos = pos; const ${v} = ${cv}(r); pos = r.pos;`,
expr: v,
};
}
}
if (isTypedArrayKind(schema.kind)) {
const len = ctx.fresh('talen');
const arr = ctx.fresh('ta');
const size = TA_ELEM_SIZE[schema.kind];
const ctor = TA_CTOR[schema.kind];
const vv = ctx.fresh('vv');
const m = ctx.fresh('m');
const b = ctx.fresh('b');
return {
pre: `let ${len}; { let ${vv} = 0, ${m} = 1, ${b}; do { ${b} = buf[pos++]; ${vv} += (${b} & 0x7f) * ${m}; ${m} *= 128; } while (${b} & 0x80); ${len} = ${vv}; } const ${arr} = new ${ctor}(${len}); { const _bytes = buf.subarray(pos, pos + ${len} * ${size}); new Uint8Array(${arr}.buffer, ${arr}.byteOffset, ${len} * ${size}).set(_bytes); pos += ${len} * ${size}; }`,
expr: arr,
};
}
throw new Error(`emitDec: unknown kind ${(schema as { kind: string }).kind}`);
}
const BARE_IDENT = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
export function compileObject(schema: ObjectSchema): CodegenResult {
const encCtx = new Ctx('enc');
const decCtx = new Ctx('dec');
// Encoder body
const encSeg = new SegBuilder();
for (const fname of Object.keys(schema.fields)) {
emitEnc(schema.fields[fname]!, encCtx, encSeg, `o[${JSON.stringify(fname)}]`);
}
const encodeBody = encSeg.build();
// Decoder body: emit as inline object literal in return statement.
// For fields whose inner.expr is already a bare identifier (declared via inner.pre),
// skip the wrapping `const tmp = expr;` and use the identifier directly.
let pre = '';
const props: string[] = [];
for (const fname of Object.keys(schema.fields)) {
const inner = emitDec(schema.fields[fname]!, decCtx);
if (inner.pre !== '' && BARE_IDENT.test(inner.expr)) {
pre += inner.pre;
props.push(`${JSON.stringify(fname)}: ${inner.expr}`);
} else {
const tmp = decCtx.fresh(`f_${sanitize(fname)}`);
pre += `${inner.pre} const ${tmp} = ${inner.expr};`;
props.push(`${JSON.stringify(fname)}: ${tmp}`);
}
}
const decodeBody = `${pre} r.pos = pos; return { ${props.join(', ')} };`;
const deps = new Map<string, { mode: 'enc' | 'dec'; targetName: string }>();
for (const [k, v] of encCtx.deps) deps.set(k, v);
for (const [k, v] of decCtx.deps) deps.set(k, v);
const closure = new Map<string, unknown>();
for (const [k, v] of encCtx.closure) closure.set(k, v);
for (const [k, v] of decCtx.closure) closure.set(k, v);
return { encodeBody, decodeBody, deps, closure };
}
export function compileUnion(schema: UnionSchema): CodegenResult {
const encCtx = new Ctx('enc');
const decCtx = new Ctx('dec');
const encSeg = new SegBuilder();
emitEnc(schema, encCtx, encSeg, 'o');
const encodeBody = encSeg.build();
const decRes = emitDec(schema, decCtx);
const decodeBody = `${decRes.pre} r.pos = pos; return ${decRes.expr};`;
const deps = new Map<string, { mode: 'enc' | 'dec'; targetName: string }>();
for (const [k, v] of encCtx.deps) deps.set(k, v);
for (const [k, v] of decCtx.deps) deps.set(k, v);
const closure = new Map<string, unknown>();
for (const [k, v] of encCtx.closure) closure.set(k, v);
for (const [k, v] of decCtx.closure) closure.set(k, v);
return { encodeBody, decodeBody, deps, closure };
}
+2
View File
@@ -0,0 +1,2 @@
export { transform, type TransformOptions, type TransformResult } from './transformer.ts';
export { serializerCodegen, type SerializerPluginOptions, type VitePlugin } from './vite.ts';
+457
View File
@@ -0,0 +1,457 @@
/**
* Compile-only transformer for @perf/serializer.
*
* Detects `type(...)` and `oneOf(...)` calls in the source, statically
* evaluates their arguments to ObjectSchema/UnionSchema descriptors, runs
* the existing codegen, and replaces each call with a self-contained IIFE
* that constructs the codec inline — no runtime `new Function`, no codegen
* module needed at runtime.
*
* Scope (v1):
* - Same-file only (no cross-file schema references).
* - Top-level `const X = type(...)` declarations (including `export const`).
* - Field values may be:
* • imported primitive markers (u8 … f64, bool, str, bytes, *Array)
* • calls to imported combinators (list, opt, enumOf, flags, tuple)
* • identifier references to previously-defined codecs in the file
* • inline ObjectExpression literals
* - `enumOf` / `flags` array args may be plain arrays or `[...] as const`.
*/
import { parseSync } from 'oxc-parser';
import MagicString from 'magic-string';
import { compileObject, compileUnion } from '../codegen.ts';
import { s } from '../schema.ts';
import type {
AnySchema,
ObjectSchema,
UnionSchema,
} from '../descriptors.ts';
const PKG_NAMES = new Set([
'@perf/serializer',
'@perf/serializer/index',
]);
interface ImportInfo {
bindings: Map<string, string>;
}
interface CompiledCodec {
schemaName: string;
schemaKind: 'object' | 'union';
fieldsDescriptor: string;
encodeBody: string;
decodeBody: string;
closure: Map<string, unknown>;
deps: Map<string, { mode: 'enc' | 'dec'; targetName: string }>;
id: number;
}
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;
}
const PRIMITIVES = new Set([
'u8', 'u16', 'u32', 'i8', 'i16', 'i32',
'u53', 'i53', 'u64', 'i64', 'f32', 'f64',
'bool', 'str', 'bytes',
'f32Array', 'f64Array', 'u8Array', 'u16Array', 'u32Array', 'i32Array',
]);
// oxc AST nodes vary widely per `type`; we treat them as loosely-typed objects
// and check `.type` before reading per-shape fields.
type AnyNode = Record<string, any>;
function collectImportsFromSet(program: AnyNode, aliases: Set<string>): ImportInfo {
const bindings = new Map<string, string>();
for (const stmt of program.body as AnyNode[]) {
if (stmt.type !== 'ImportDeclaration') continue;
const source = stmt.source.value as string;
if (!aliases.has(source)) continue;
for (const spec of stmt.specifiers as AnyNode[]) {
if (spec.type === 'ImportSpecifier') {
const local = spec.local.name as string;
const importedName = (spec.imported.name ?? spec.imported.value) as string;
bindings.set(local, importedName);
}
}
}
return { bindings };
}
interface Scope {
imports: ImportInfo;
locals: Map<string, AnySchema>;
}
let anonCounter = 0;
function evalExpr(node: AnyNode, scope: Scope): AnySchema | null {
switch (node.type) {
case 'Identifier': {
const name = node.name as string;
const exported = scope.imports.bindings.get(name);
if (exported && PRIMITIVES.has(exported)) {
const prim = (s as unknown as Record<string, AnySchema>)[exported];
if (prim) return prim;
}
const local = scope.locals.get(name);
if (local) return local;
return null;
}
case 'CallExpression': {
const callee = node.callee as AnyNode;
if (callee.type !== 'Identifier') return null;
const exported = scope.imports.bindings.get(callee.name as string);
if (!exported) return null;
const args = node.arguments as AnyNode[];
switch (exported) {
case 'list': {
if (args.length !== 1) return null;
const elem = evalExpr(args[0]!, scope);
return elem ? s.array(elem) : null;
}
case 'opt': {
if (args.length !== 1) return null;
const elem = evalExpr(args[0]!, scope);
return elem ? s.optional(elem) : null;
}
case 'enumOf': {
if (args.length !== 1) return null;
const arr = unwrapAsConst(args[0]!);
if (!arr || arr.type !== 'ArrayExpression') return null;
const values: string[] = [];
for (const el of arr.elements as AnyNode[]) {
if (el && el.type === 'Literal' && typeof el.value === 'string') {
values.push(el.value as string);
} else return null;
}
return s.enum(values);
}
case 'flags': {
if (args.length !== 1) return null;
const arr = unwrapAsConst(args[0]!);
if (!arr || arr.type !== 'ArrayExpression') return null;
const names: string[] = [];
for (const el of arr.elements as AnyNode[]) {
if (el && el.type === 'Literal' && typeof el.value === 'string') {
names.push(el.value as string);
} else return null;
}
return s.bitset(names);
}
case 'tuple': {
const elems: AnySchema[] = [];
for (const a of args) {
const e = evalExpr(a, scope);
if (!e) return null;
elems.push(e);
}
return s.tuple(...elems);
}
default:
return null;
}
}
case 'ObjectExpression': {
const fields = collectFields(node, scope);
if (!fields) return null;
const inlineName = `__InlineObj_${anonCounter++}`;
return { kind: 'object' as const, name: inlineName, fields };
}
default:
return null;
}
}
function unwrapAsConst(node: AnyNode): AnyNode | null {
if (node.type === 'TSAsExpression' || node.type === 'TSSatisfiesExpression') {
return node.expression as AnyNode;
}
if (node.type === 'ArrayExpression') return node;
return null;
}
function collectFields(obj: AnyNode, scope: Scope): Record<string, AnySchema> | null {
const fields: Record<string, AnySchema> = {};
for (const prop of obj.properties as AnyNode[]) {
if (prop.type !== 'Property') return null;
const key = prop.key as AnyNode;
let fname: string;
if (key.type === 'Identifier') fname = key.name as string;
else if (key.type === 'Literal' && typeof key.value === 'string') fname = key.value as string;
else return null;
const sub = evalExpr(prop.value as AnyNode, scope);
if (!sub) return null;
fields[fname] = sub;
}
return fields;
}
interface TypeCallInfo {
call: AnyNode;
declName: string;
fn: 'type' | 'oneOf';
}
function findTypeCalls(program: AnyNode, imports: ImportInfo): TypeCallInfo[] {
const calls: TypeCallInfo[] = [];
for (const topStmt of program.body as AnyNode[]) {
const stmt: AnyNode =
topStmt.type === 'ExportNamedDeclaration' && topStmt.declaration
? (topStmt.declaration as AnyNode)
: topStmt;
if (stmt.type !== 'VariableDeclaration') continue;
for (const decl of stmt.declarations as AnyNode[]) {
const id = decl.id as AnyNode;
const init = decl.init as AnyNode | null;
if (!init || id.type !== 'Identifier' || init.type !== 'CallExpression') continue;
const callee = init.callee as AnyNode;
if (callee.type !== 'Identifier') continue;
const exported = imports.bindings.get(callee.name as string);
if (exported === 'type' || exported === 'oneOf') {
calls.push({ call: init, declName: id.name as string, fn: exported });
}
}
}
return calls;
}
function buildSchemaFromTypeCall(
info: TypeCallInfo,
scope: Scope,
): ObjectSchema | UnionSchema | null {
const args = info.call.arguments as AnyNode[];
if (info.fn === 'type') {
let name: string;
let fieldsExpr: AnyNode;
if (args.length >= 2 && args[0]!.type === 'Literal' && typeof args[0]!.value === 'string') {
name = args[0]!.value as string;
fieldsExpr = args[1]!;
} else {
name = info.declName;
fieldsExpr = args[0]!;
}
if (fieldsExpr.type !== 'ObjectExpression') return null;
const fields = collectFields(fieldsExpr, scope);
if (!fields) return null;
return { kind: 'object', name, fields };
} else {
let name: string;
let disc: string;
let variantsExpr: AnyNode;
if (
args.length >= 3 &&
args[0]!.type === 'Literal' &&
typeof args[0]!.value === 'string'
) {
name = args[0]!.value as string;
const discNode = args[1]!;
if (discNode.type !== 'Literal' || typeof discNode.value !== 'string') return null;
disc = discNode.value as string;
variantsExpr = args[2]!;
} else if (args.length >= 2) {
name = info.declName;
const discNode = args[0]!;
if (discNode.type !== 'Literal' || typeof discNode.value !== 'string') return null;
disc = discNode.value as string;
variantsExpr = args[1]!;
} else return null;
if (variantsExpr.type !== 'ObjectExpression') return null;
const variants: Record<string, Record<string, AnySchema>> = {};
for (const prop of variantsExpr.properties as AnyNode[]) {
if (prop.type !== 'Property') return null;
const key = prop.key as AnyNode;
const variantName =
key.type === 'Identifier' ? (key.name as string)
: key.type === 'Literal' && typeof key.value === 'string' ? (key.value as string)
: null;
if (!variantName) return null;
const variantFields = prop.value as AnyNode;
if (variantFields.type !== 'ObjectExpression') return null;
const fields = collectFields(variantFields, scope);
if (!fields) return null;
variants[variantName] = fields;
}
return s.union(name, disc, variants);
}
}
function emitDescriptorLiteral(schema: AnySchema): string {
return JSON.stringify(schema, (key, value) => {
if (key === 'thunk' || typeof value === 'function') return undefined;
return value;
});
}
function compileCall(
info: TypeCallInfo,
scope: Scope,
): { src: string; compiled: CompiledCodec } | null {
const schema = buildSchemaFromTypeCall(info, scope);
if (!schema) return null;
const cg = schema.kind === 'object' ? compileObject(schema) : compileUnion(schema);
const id = fnv1a16(schema.name);
const fname = sanitize(schema.name);
if (cg.deps.size > 0) return null; // ref/codec deps not supported yet
const closureLines: string[] = [];
for (const [k, v] of cg.closure) {
closureLines.push(`const ${k} = ${serializeClosureValue(v)};`);
}
const descriptorLit = emitDescriptorLiteral(schema);
const src = `(function () {
${closureLines.join('\n ')}
function encode_${fname}(w, o) {
let pos = w.pos;
let buf = w.buf;
let view = w.view;
${cg.encodeBody}
w.pos = pos;
}
function decode_${fname}(r) {
let pos = r.pos;
const buf = r.buf;
const view = r.view;
${cg.decodeBody}
}
const __desc = ${descriptorLit};
const __codec = {
...__desc,
id: ${id},
encode(v, into) {
if (into) { encode_${fname}(into, v); return into.bytes(); }
const w = new __SerWriter();
encode_${fname}(w, v);
return w.bytesCopy();
},
decode(b) {
const r = new __SerReader(b);
return decode_${fname}(r);
},
encodeInto(v, w) { encode_${fname}(w, v); },
decodeFrom: decode_${fname},
$infer: undefined,
};
Object.freeze(__codec);
__serRegisterPrecompiled(__codec, encode_${fname}, decode_${fname});
return __codec;
})()`;
return {
src,
compiled: {
schemaName: schema.name,
schemaKind: schema.kind,
fieldsDescriptor: descriptorLit,
encodeBody: cg.encodeBody,
decodeBody: cg.decodeBody,
closure: cg.closure,
deps: cg.deps,
id,
},
};
}
function sanitize(name: string): string {
return name.replace(/[^A-Za-z0-9_]/g, '_');
}
function serializeClosureValue(v: unknown): string {
if (v === null || v === undefined) return String(v);
if (typeof v === 'number' || typeof v === 'boolean') return String(v);
if (typeof v === 'string') return JSON.stringify(v);
if (typeof v === 'bigint') return `${v.toString()}n`;
if (Array.isArray(v)) return `Object.freeze([${v.map(serializeClosureValue).join(',')}])`;
if (typeof v === 'object') {
const entries = Object.entries(v as object).map(
([k, val]) => `${JSON.stringify(k)}: ${serializeClosureValue(val)}`,
);
return `Object.freeze({${entries.join(',')}})`;
}
throw new Error(`Cannot serialize closure value of type ${typeof v}`);
}
function makePrelude(importPath: string): string {
return `
import { Writer as __SerWriter, Reader as __SerReader } from ${JSON.stringify(importPath)};
const __serRegistry = (globalThis.__serRegistry ??= new Map());
function __serRegisterPrecompiled(codec, enc, dec) {
__serRegistry.set(codec.id, codec);
}
`;
}
export interface TransformOptions {
importPath?: string;
packageAliases?: string[];
}
export interface TransformResult {
code: string;
transformedCount: number;
}
export function transform(source: string, filename = 'input.ts', options: TransformOptions = {}): TransformResult {
const importPath = options.importPath ?? '@perf/serializer';
const aliases = new Set<string>(PKG_NAMES);
for (const a of options.packageAliases ?? []) aliases.add(a);
aliases.add(importPath);
const lang = filename.endsWith('.ts') || filename.endsWith('.tsx') ? 'ts' : 'js';
const parsed = parseSync(filename, source, { lang });
if (parsed.errors && parsed.errors.length > 0) {
const msgs = parsed.errors.map((e) => e.message ?? String(e)).join('\n');
throw new Error(`Parse errors in ${filename}:\n${msgs}`);
}
const program = parsed.program as unknown as AnyNode;
const imports = collectImportsFromSet(program, aliases);
let hasTypeImport = false;
for (const v of imports.bindings.values()) {
if (v === 'type' || v === 'oneOf') { hasTypeImport = true; break; }
}
if (!hasTypeImport) return { code: source, transformedCount: 0 };
const calls = findTypeCalls(program, imports);
if (calls.length === 0) return { code: source, transformedCount: 0 };
const scope: Scope = { imports, locals: new Map() };
const ms = new MagicString(source);
let transformedCount = 0;
for (const info of calls) {
const result = compileCall(info, scope);
if (!result) continue;
ms.overwrite(info.call.start as number, info.call.end as number, result.src);
const schema =
result.compiled.schemaKind === 'object'
? ({
kind: 'object' as const,
name: result.compiled.schemaName,
fields: JSON.parse(result.compiled.fieldsDescriptor).fields,
} as ObjectSchema)
: ({
kind: 'union' as const,
name: result.compiled.schemaName,
...JSON.parse(result.compiled.fieldsDescriptor),
} as UnionSchema);
scope.locals.set(info.declName, schema);
transformedCount++;
}
if (transformedCount === 0) return { code: source, transformedCount: 0 };
ms.prepend(makePrelude(importPath));
return { code: ms.toString(), transformedCount };
}
+69
View File
@@ -0,0 +1,69 @@
import { transform, type TransformOptions } from './transformer.ts';
export interface SerializerPluginOptions extends TransformOptions {
/**
* Glob patterns to include. Defaults to all `.ts` / `.tsx` / `.mts` / `.cts` files.
*/
include?: RegExp;
/**
* Glob patterns to exclude (in addition to node_modules which is always excluded).
*/
exclude?: RegExp;
}
/**
* Vite plugin for compile-time codec generation.
*
* Add to vite.config.ts:
*
* import { serializerCodegen } from '@perf/serializer/codegen/vite';
*
* export default {
* plugins: [serializerCodegen()],
* };
*
* In source code, write:
*
* import { type, u53, f64, str } from '@perf/serializer';
* const Ticker = type('Ticker', { symbol: str, last: f64 });
*
* The plugin replaces each `type(...)` / `oneOf(...)` call with an inline IIFE
* that produces the same codec, eliminating runtime `new Function` compilation.
*/
export interface VitePlugin {
name: string;
enforce?: 'pre' | 'post';
transform(code: string, id: string): { code: string; map: null } | null;
}
export function serializerCodegen(options: SerializerPluginOptions = {}): VitePlugin {
const include = options.include ?? /\.(?:ts|tsx|mts|cts)$/;
const exclude = options.exclude;
return {
name: 'perf-serializer-codegen',
enforce: 'pre',
transform(code: string, id: string) {
if (id.includes('node_modules')) return null;
if (!include.test(id)) return null;
if (exclude && exclude.test(id)) return null;
// Quick negative: file doesn't even contain the word `type` or `oneOf`
if (!code.includes('type(') && !code.includes('oneOf(')) return null;
try {
const result = transform(code, id, options);
if (result.transformedCount === 0) return null;
return { code: result.code, map: null };
} catch (err) {
// Don't break the build on parse errors — leave source as-is so the
// runtime fallback handles it.
const msg = (err as Error).message ?? String(err);
// eslint-disable-next-line no-console
console.warn(`[perf-serializer-codegen] skipped ${id}: ${msg}`);
return null;
}
},
};
}
export default serializerCodegen;
+89
View File
@@ -0,0 +1,89 @@
import type { Writer, Reader } from './io.ts';
export type PrimitiveKind =
| 'u8' | 'u16' | 'u32'
| 'i8' | 'i16' | 'i32'
| 'u53' | 'i53'
| 'u64' | 'i64'
| 'f32' | 'f64'
| 'bool'
| 'str'
| 'bytes';
export type TypedArrayKind = 'f32Array' | 'f64Array' | 'u8Array' | 'u16Array' | 'u32Array' | 'i32Array';
export interface PrimitiveSchema<K extends PrimitiveKind, T> {
readonly kind: K;
readonly __t?: T;
}
export interface TypedArraySchema<K extends TypedArrayKind, T> {
readonly kind: K;
readonly __t?: T;
}
export interface ArraySchema<E extends AnySchema = AnySchema> {
readonly kind: 'array';
readonly elem: E;
}
export interface OptionalSchema<E extends AnySchema = AnySchema> {
readonly kind: 'optional';
readonly elem: E;
}
export interface EnumSchema<L extends readonly string[] = readonly string[]> {
readonly kind: 'enum';
readonly values: L;
}
export interface BitsetSchema<L extends readonly string[] = readonly string[]> {
readonly kind: 'bitset';
readonly flags: L;
}
export interface TupleSchema<E extends readonly AnySchema[] = readonly AnySchema[]> {
readonly kind: 'tuple';
readonly elems: E;
}
export interface ObjectSchema<F extends Record<string, AnySchema> = Record<string, AnySchema>> {
readonly kind: 'object';
readonly name: string;
readonly fields: F;
}
export interface UnionSchema<
D extends string = string,
V extends Record<string, ObjectSchema> = Record<string, ObjectSchema>,
> {
readonly kind: 'union';
readonly name: string;
readonly discriminator: D;
readonly variants: V;
}
export interface RefSchema<S extends ObjectSchema = ObjectSchema> {
readonly kind: 'ref';
readonly thunk: () => S;
}
export interface CodecSchema<T = unknown> {
readonly kind: 'codec';
readonly encode: (w: Writer, v: T) => void;
readonly decode: (r: Reader) => T;
readonly __t?: T;
}
export type AnySchema =
| PrimitiveSchema<PrimitiveKind, unknown>
| TypedArraySchema<TypedArrayKind, unknown>
| ArraySchema
| OptionalSchema
| EnumSchema
| BitsetSchema
| TupleSchema
| ObjectSchema
| UnionSchema
| RefSchema
| CodecSchema;
+41
View File
@@ -0,0 +1,41 @@
// ── Simplified façade (recommended) ────────────────────────────────────────
export {
type,
oneOf,
router,
u8, u16, u32, i8, i16, i32, u53, i53, u64, i64, f32, f64,
bool, str, bytes,
f32Array, f64Array, u8Array, u16Array, u32Array, i32Array,
list, opt, enumOf, flags, tuple,
} from './api.ts';
export type { TypeCodec, InferType, Router } from './api.ts';
// ── Low-level API (advanced) ───────────────────────────────────────────────
export { Writer, Reader } from './io.ts';
export { s, defineSchema } from './schema.ts';
export type { SchemaBuilder } from './schema.ts';
export { Serializable } from './symbol.ts';
export {
register,
registerClass,
serialize,
deserialize,
clearRegistry,
} from './register.ts';
export type { Codec } from './register.ts';
export type {
AnySchema,
ObjectSchema,
UnionSchema,
ArraySchema,
OptionalSchema,
EnumSchema,
BitsetSchema,
TupleSchema,
RefSchema,
CodecSchema,
PrimitiveSchema,
TypedArraySchema,
PrimitiveKind,
TypedArrayKind,
} from './descriptors.ts';
+358
View File
@@ -0,0 +1,358 @@
const TE = new TextEncoder();
const TD = new TextDecoder('utf-8', { fatal: false, ignoreBOM: true });
export class Writer {
buf: Uint8Array;
view: DataView;
pos: number;
constructor(initial = 1024) {
this.buf = new Uint8Array(initial);
this.view = new DataView(this.buf.buffer);
this.pos = 0;
}
reset(): void {
this.pos = 0;
}
bytes(): Uint8Array {
return this.buf.subarray(0, this.pos);
}
bytesCopy(): Uint8Array {
return this.buf.slice(0, this.pos);
}
ensure(n: number): void {
if (this.pos + n > this.buf.byteLength) this.grow(n);
}
private grow(n: number): void {
let next = this.buf.byteLength * 2;
const need = this.pos + n;
while (next < need) next *= 2;
const nb = new Uint8Array(next);
nb.set(this.buf);
this.buf = nb;
this.view = new DataView(nb.buffer);
}
u8(v: number): void {
if (this.pos + 1 > this.buf.byteLength) this.grow(1);
this.buf[this.pos++] = v;
}
u16(v: number): void {
if (this.pos + 2 > this.buf.byteLength) this.grow(2);
this.view.setUint16(this.pos, v, true);
this.pos += 2;
}
i16(v: number): void {
if (this.pos + 2 > this.buf.byteLength) this.grow(2);
this.view.setInt16(this.pos, v, true);
this.pos += 2;
}
u32(v: number): void {
if (this.pos + 4 > this.buf.byteLength) this.grow(4);
this.view.setUint32(this.pos, v, true);
this.pos += 4;
}
i32(v: number): void {
if (this.pos + 4 > this.buf.byteLength) this.grow(4);
this.view.setInt32(this.pos, v, true);
this.pos += 4;
}
f32(v: number): void {
if (this.pos + 4 > this.buf.byteLength) this.grow(4);
this.view.setFloat32(this.pos, v, true);
this.pos += 4;
}
f64(v: number): void {
if (this.pos + 8 > this.buf.byteLength) this.grow(8);
this.view.setFloat64(this.pos, v, true);
this.pos += 8;
}
bool(v: boolean): void {
if (this.pos + 1 > this.buf.byteLength) this.grow(1);
this.buf[this.pos++] = v ? 1 : 0;
}
varu32(v: number): void {
if (this.pos + 5 > this.buf.byteLength) this.grow(5);
while (v >= 0x80) {
this.buf[this.pos++] = (v & 0x7f) | 0x80;
v >>>= 7;
}
this.buf[this.pos++] = v;
}
vari32(v: number): void {
const z = ((v << 1) ^ (v >> 31)) >>> 0;
this.varu32(z);
}
varu53(v: number): void {
if (this.pos + 10 > this.buf.byteLength) this.grow(10);
while (v >= 0x80) {
this.buf[this.pos++] = (v & 0x7f) | 0x80;
v = Math.floor(v / 128);
}
this.buf[this.pos++] = v;
}
vari53(v: number): void {
const z = v >= 0 ? BigInt(v) * 2n : -BigInt(v) * 2n - 1n;
this.varbu(z);
}
varbu(v: bigint): void {
if (this.pos + 10 > this.buf.byteLength) this.grow(10);
while (v >= 0x80n) {
this.buf[this.pos++] = Number(v & 0x7fn) | 0x80;
v >>= 7n;
}
this.buf[this.pos++] = Number(v);
}
varbi(v: bigint): void {
const z = v >= 0n ? v << 1n : (-v << 1n) - 1n;
this.varbu(z);
}
raw(src: Uint8Array): void {
if (this.pos + src.length > this.buf.byteLength) this.grow(src.length);
this.buf.set(src, this.pos);
this.pos += src.length;
}
bytesPrefixed(src: Uint8Array): void {
this.varu53(src.length);
this.raw(src);
}
str(s: string): void {
const len = s.length;
if (len === 0) {
this.u8(0);
return;
}
if (len < 64) {
if (this.pos + len + 5 > this.buf.byteLength) this.grow(len + 5);
let allAscii = true;
for (let i = 0; i < len; i++) {
if (s.charCodeAt(i) > 127) {
allAscii = false;
break;
}
}
if (allAscii) {
let p = this.pos;
let lv = len;
while (lv >= 0x80) {
this.buf[p++] = (lv & 0x7f) | 0x80;
lv >>>= 7;
}
this.buf[p++] = lv;
for (let i = 0; i < len; i++) this.buf[p++] = s.charCodeAt(i);
this.pos = p;
return;
}
}
const maxBytes = len * 3;
if (this.pos + maxBytes + 5 > this.buf.byteLength) this.grow(maxBytes + 5);
const dst = this.buf.subarray(this.pos + 5);
const { written } = TE.encodeInto(s, dst);
const w = written ?? 0;
let lenBytes = 1;
let lv = w;
while (lv >= 0x80) {
lenBytes++;
lv >>>= 7;
}
if (lenBytes < 5 && w > 0) {
this.buf.copyWithin(this.pos + lenBytes, this.pos + 5, this.pos + 5 + w);
}
let p = this.pos;
lv = w;
while (lv >= 0x80) {
this.buf[p++] = (lv & 0x7f) | 0x80;
lv >>>= 7;
}
this.buf[p++] = lv;
this.pos = p + w;
}
}
export class Reader {
buf: Uint8Array;
view: DataView;
pos: number;
end: number;
constructor(buf: Uint8Array) {
this.buf = buf;
this.view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
this.pos = 0;
this.end = buf.byteLength;
}
reset(buf: Uint8Array): void {
this.buf = buf;
this.view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
this.pos = 0;
this.end = buf.byteLength;
}
remaining(): number {
return this.end - this.pos;
}
u8(): number {
return this.buf[this.pos++]!;
}
u16(): number {
const v = this.view.getUint16(this.pos, true);
this.pos += 2;
return v;
}
i16(): number {
const v = this.view.getInt16(this.pos, true);
this.pos += 2;
return v;
}
u32(): number {
const v = this.view.getUint32(this.pos, true);
this.pos += 4;
return v;
}
i32(): number {
const v = this.view.getInt32(this.pos, true);
this.pos += 4;
return v;
}
f32(): number {
const v = this.view.getFloat32(this.pos, true);
this.pos += 4;
return v;
}
f64(): number {
const v = this.view.getFloat64(this.pos, true);
this.pos += 8;
return v;
}
bool(): boolean {
return this.buf[this.pos++] !== 0;
}
varu32(): number {
let v = 0;
let shift = 0;
let byte = 0;
do {
byte = this.buf[this.pos++]!;
v |= (byte & 0x7f) << shift;
shift += 7;
} while (byte & 0x80);
return v >>> 0;
}
vari32(): number {
const z = this.varu32();
return (z >>> 1) ^ -(z & 1);
}
varu53(): number {
let v = 0;
let mult = 1;
let byte = 0;
do {
byte = this.buf[this.pos++]!;
v += (byte & 0x7f) * mult;
mult *= 128;
} while (byte & 0x80);
return v;
}
vari53(): number {
const z = this.varbu();
const v = (z & 1n) === 0n ? z >> 1n : -((z >> 1n) + 1n);
return Number(v);
}
varbu(): bigint {
let v = 0n;
let shift = 0n;
let byte = 0;
do {
byte = this.buf[this.pos++]!;
v |= BigInt(byte & 0x7f) << shift;
shift += 7n;
} while (byte & 0x80);
return v;
}
varbi(): bigint {
const z = this.varbu();
return (z & 1n) === 0n ? z >> 1n : -((z >> 1n) + 1n);
}
bytes(n: number): Uint8Array {
const slice = this.buf.subarray(this.pos, this.pos + n);
this.pos += n;
return slice;
}
bytesPrefixed(): Uint8Array {
const n = this.varu53();
return this.bytes(n);
}
str(): string {
const len = this.varu53();
if (len === 0) return '';
const start = this.pos;
const end = start + len;
const buf = this.buf;
if (len < 32) {
let allAscii = true;
for (let i = start; i < end; i++) {
if (buf[i]! > 127) {
allAscii = false;
break;
}
}
if (allAscii) {
let s = '';
for (let i = start; i < end; i++) {
s += String.fromCharCode(buf[i]!);
}
this.pos = end;
return s;
}
}
const slice = buf.subarray(start, end);
this.pos = end;
return TD.decode(slice);
}
}
+197
View File
@@ -0,0 +1,197 @@
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<T = unknown> {
readonly id: number;
readonly name: string;
readonly encode: (w: Writer, v: T) => void;
readonly decode: (r: Reader) => T;
}
type AnyCodec = Codec<any>;
const byName = new Map<string, AnyCodec>();
const byId = new Map<number, AnyCodec>();
const byCtor = new WeakMap<object, AnyCodec>();
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<string, ObjectSchema>): 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<T = unknown>(schema: ObjectSchema | UnionSchema): Codec<T> {
const existing = byName.get(schema.name);
if (existing) return existing as Codec<T>;
// Only `ref` boundaries require a separately registered codec; nested
// objects/unions/arrays/etc. are inlined into the generated function.
const refTargets = new Map<string, ObjectSchema>();
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<string, Function> = {};
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<string, unknown> = {};
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<T> = Object.freeze({
id,
name: schema.name,
encode: encFn,
decode: decFn,
});
byName.set(schema.name, codec);
byId.set(id, codec);
return codec;
}
export function registerClass<T>(Ctor: new (...args: never[]) => T): Codec<T> {
const cached = byCtor.get(Ctor);
if (cached) return cached as Codec<T>;
const schema = (Ctor as unknown as Record<symbol, unknown>)[Serializable] as
| ObjectSchema
| UnionSchema
| undefined;
if (!schema) {
throw new Error(`${Ctor.name} has no [Serializable] schema`);
}
const codec = register<T>(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<T>(value: T, codec: Codec<T>, 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<T = unknown>(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();
}
+111
View File
@@ -0,0 +1,111 @@
import type {
AnySchema,
ArraySchema,
BitsetSchema,
CodecSchema,
EnumSchema,
ObjectSchema,
OptionalSchema,
PrimitiveSchema,
RefSchema,
TupleSchema,
TypedArraySchema,
UnionSchema,
} from './descriptors.ts';
import type { Reader, Writer } from './io.ts';
type Prim<K extends string, T> = { readonly kind: K; readonly __t?: T };
function p<K extends string, T>(kind: K): Prim<K, T> {
return Object.freeze({ kind }) as Prim<K, T>;
}
export const s = Object.freeze({
u8: p<'u8', number>('u8') as PrimitiveSchema<'u8', number>,
u16: p<'u16', number>('u16') as PrimitiveSchema<'u16', number>,
u32: p<'u32', number>('u32') as PrimitiveSchema<'u32', number>,
i8: p<'i8', number>('i8') as PrimitiveSchema<'i8', number>,
i16: p<'i16', number>('i16') as PrimitiveSchema<'i16', number>,
i32: p<'i32', number>('i32') as PrimitiveSchema<'i32', number>,
u53: p<'u53', number>('u53') as PrimitiveSchema<'u53', number>,
i53: p<'i53', number>('i53') as PrimitiveSchema<'i53', number>,
u64: p<'u64', bigint>('u64') as PrimitiveSchema<'u64', bigint>,
i64: p<'i64', bigint>('i64') as PrimitiveSchema<'i64', bigint>,
f32: p<'f32', number>('f32') as PrimitiveSchema<'f32', number>,
f64: p<'f64', number>('f64') as PrimitiveSchema<'f64', number>,
bool: p<'bool', boolean>('bool') as PrimitiveSchema<'bool', boolean>,
str: p<'str', string>('str') as PrimitiveSchema<'str', string>,
bytes: p<'bytes', Uint8Array>('bytes') as PrimitiveSchema<'bytes', Uint8Array>,
f32Array: p<'f32Array', Float32Array>('f32Array') as TypedArraySchema<'f32Array', Float32Array>,
f64Array: p<'f64Array', Float64Array>('f64Array') as TypedArraySchema<'f64Array', Float64Array>,
u8Array: p<'u8Array', Uint8Array>('u8Array') as TypedArraySchema<'u8Array', Uint8Array>,
u16Array: p<'u16Array', Uint16Array>('u16Array') as TypedArraySchema<'u16Array', Uint16Array>,
u32Array: p<'u32Array', Uint32Array>('u32Array') as TypedArraySchema<'u32Array', Uint32Array>,
i32Array: p<'i32Array', Int32Array>('i32Array') as TypedArraySchema<'i32Array', Int32Array>,
array<E extends AnySchema>(elem: E): ArraySchema<E> {
return { kind: 'array', elem };
},
optional<E extends AnySchema>(elem: E): OptionalSchema<E> {
return { kind: 'optional', elem };
},
enum<L extends readonly string[]>(values: L): EnumSchema<L> {
if (values.length === 0) throw new Error('enum requires at least one value');
if (values.length > 256) throw new Error('enum supports up to 256 values');
return { kind: 'enum', values };
},
bitset<L extends readonly string[]>(flags: L): BitsetSchema<L> {
if (flags.length === 0) throw new Error('bitset requires at least one flag');
if (flags.length > 64) throw new Error('bitset supports up to 64 flags');
return { kind: 'bitset', flags };
},
tuple<E extends readonly AnySchema[]>(...elems: E): TupleSchema<E> {
return { kind: 'tuple', elems };
},
union<D extends string, V extends Record<string, Record<string, AnySchema>>>(
name: string,
discriminator: D,
variants: V,
): UnionSchema<D, { [K in keyof V]: ObjectSchema<V[K]> }> {
const variantSchemas = {} as Record<string, ObjectSchema>;
let i = 0;
for (const k of Object.keys(variants)) {
variantSchemas[k] = {
kind: 'object',
name: `${name}::${k}`,
fields: variants[k]!,
};
i++;
if (i > 256) throw new Error('union supports up to 256 variants');
}
return {
kind: 'union',
name,
discriminator,
variants: variantSchemas as { [K in keyof V]: ObjectSchema<V[K]> },
};
},
ref<S extends ObjectSchema>(thunk: () => S): RefSchema<S> {
return { kind: 'ref', thunk };
},
codec<T>(impl: { encode: (w: Writer, v: T) => void; decode: (r: Reader) => T }): CodecSchema<T> {
return { kind: 'codec', encode: impl.encode, decode: impl.decode };
},
});
export type SchemaBuilder = typeof s;
export function defineSchema<F extends Record<string, AnySchema>>(
name: string,
build: (s: SchemaBuilder) => F,
): ObjectSchema<F> {
return { kind: 'object', name, fields: build(s) };
}
+10
View File
@@ -0,0 +1,10 @@
/**
* The well-known symbol carrying a schema descriptor on a constructor.
*
* Registered via `Symbol.for` so multiple module copies (workers, dual builds)
* share identity.
*/
declare const SerializableBrand: unique symbol;
export const Serializable = Symbol.for('@perf/serializable') as typeof SerializableBrand;
export type SerializableKey = typeof SerializableBrand;