alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
655 lines (566 loc) • 20.1 kB
text/typescript
import { $hook } from "../primitives/$hook.ts";
import { SchemaCodec } from "./SchemaCodec.ts";
import type { TArray, TObject, TSchema, TUnion } from "./TypeProvider.ts";
import { type Static, z } from "./TypeProvider.ts";
// =============================================================================
// Keyless JSON Codec
// =============================================================================
// Schema-driven JSON encoding without keys:
// - Schema defines field order → no keys needed in output
// - Uses native JSON.stringify on arrays (fast!)
// - Uses native JSON.parse for decoding (blazing fast)
// - 50-56% smaller than JSON, decode 1.7-2x faster
//
// Example:
// JSON: {"name":"Alice","age":30,"active":true} (39 bytes)
// Keyless: ["Alice",30,true] (17 bytes)
// =============================================================================
export interface KeylessCodec<T = any> {
encode: (value: T) => string;
decode: (str: string) => T;
}
export interface KeylessCodecOptions {
/**
* Whether to use `new Function()` for code compilation.
* When false, uses an interpreter-based approach (safer but slower).
*
* @default Auto-detected: false in browser (CSP compatibility), true on server
*/
useFunctionCompilation?: boolean;
}
/**
* KeylessJsonSchemaCodec provides schema-driven JSON encoding without keys.
*
* It uses the schema to determine field order, allowing the encoded output
* to be a simple JSON array instead of an object with keys.
*/
export class KeylessJsonSchemaCodec extends SchemaCodec {
protected readonly cache = new Map<TSchema, KeylessCodec>();
protected readonly textEncoder = new TextEncoder();
protected readonly textDecoder = new TextDecoder();
protected varCounter = 0;
protected useFunctionCompilation = true;
/**
* Configure codec options.
*/
public configure(options: KeylessCodecOptions): this {
if (options.useFunctionCompilation !== undefined) {
this.useFunctionCompilation = options.useFunctionCompilation;
this.cache.clear(); // Clear cache when compilation mode changes
}
return this;
}
/**
* Hook to auto-detect safe mode on configure.
* Disables function compilation in browser by default.
*/
protected onConfigure = $hook({
on: "configure",
handler: () => {
// Auto-detect: disable function compilation in browser (CSP compatibility)
// and test if eval/Function is available
this.useFunctionCompilation = this.canUseFunction();
},
});
/**
* Encode value to a keyless JSON string.
*/
public encodeToString<T extends TSchema>(
schema: T,
value: Static<T>,
): string {
return this.getCodec(schema).encode(value);
}
/**
* Encode value to binary (UTF-8 encoded keyless JSON).
*/
public encodeToBinary<T extends TSchema>(
schema: T,
value: Static<T>,
): Uint8Array {
return this.textEncoder.encode(this.encodeToString(schema, value));
}
/**
* Decode keyless JSON string or binary to value.
*/
public decode<T>(schema: TSchema, value: unknown): T {
if (value instanceof Uint8Array) {
const text = this.textDecoder.decode(value);
return this.getCodec(schema).decode(text) as T;
}
if (typeof value === "string") {
return this.getCodec(schema).decode(value) as T;
}
// If already an array (parsed JSON), reconstruct object
if (Array.isArray(value)) {
return this.reconstructObject(schema, value) as T;
}
return value as T;
}
/**
* Test if `new Function()` is available (not blocked by CSP).
*/
protected canUseFunction(): boolean {
try {
const fn = new Function("return true");
return fn() === true;
} catch {
return false;
}
}
// ===========================================================================
// Codec Compilation
// ===========================================================================
/**
* Get a compiled codec for the given schema.
* Codecs are cached for reuse.
*/
protected getCodec<T>(schema: TSchema): KeylessCodec<T> {
let c = this.cache.get(schema);
if (!c) {
c = this.useFunctionCompilation
? this.compileWithFunction(schema)
: this.compileInterpreted(schema);
this.cache.set(schema, c);
}
return c as KeylessCodec<T>;
}
protected nextVar(): string {
return `_${this.varCounter++}`;
}
/**
* Compile codec using `new Function()` for maximum performance.
* Only used when CSP allows and useFunctionCompilation is true.
*/
protected compileWithFunction(schema: TSchema): KeylessCodec {
this.varCounter = 0;
const encBody = this.genEnc(schema, "v");
this.varCounter = 0;
const decBody = this.genDec(schema);
const encoder = new Function("v", `return JSON.stringify(${encBody});`) as (
value: any,
) => string;
const decoder = new Function(
"s",
`const a=JSON.parse(s);let i=0;${decBody.code}return ${decBody.result};`,
) as (str: string) => any;
return { encode: encoder, decode: decoder };
}
/**
* Compile codec using interpreter-based approach.
* Safer (no eval/Function) but slower. Used in browser by default.
*/
protected compileInterpreted(schema: TSchema): KeylessCodec {
const self = this;
return {
encode(value: any): string {
return JSON.stringify(self.interpretEncode(schema, value));
},
decode(str: string): any {
const ctx = { arr: JSON.parse(str), i: 0 };
return self.interpretDecode(schema, ctx);
},
};
}
// ===========================================================================
// Interpreter-based Encoding (Safe Mode)
// ===========================================================================
protected interpretEncode(schema: TSchema, value: any): any {
if (this.isLeaf(schema)) {
return value;
}
if (z.schema.isBigInt(schema)) {
return `${value}n`;
}
if (z.schema.isArray(schema)) {
const arrSchema = schema as TArray;
if (!Array.isArray(value)) return value;
if (z.schema.isScalar(arrSchema.items)) {
return value;
}
return value.map((e) => this.interpretEncode(arrSchema.items, e));
}
if (z.schema.isObject(schema)) {
const result: any[] = [];
for (const { key, isOpt, isNullable, inner } of this.getObjectFields(
schema as TObject,
)) {
const v = value[key];
if (isOpt) {
result.push(v !== undefined ? this.interpretEncode(inner, v) : null);
} else if (isNullable) {
result.push(v !== null ? this.interpretEncode(inner, v) : null);
} else {
result.push(this.interpretEncode(inner, v));
}
}
return result;
}
if (z.schema.isOptional(schema) || z.schema.isUnion(schema)) {
const inner = this.unwrap(schema);
if (this.isNullable(schema)) {
return value !== null ? this.interpretEncode(inner, value) : null;
}
return value !== undefined ? this.interpretEncode(inner, value) : null;
}
return value;
}
// ===========================================================================
// Interpreter-based Decoding (Safe Mode)
// ===========================================================================
protected interpretDecode(
schema: TSchema,
ctx: { arr: any[]; i: number },
): any {
if (this.isLeaf(schema)) {
return ctx.arr[ctx.i++];
}
if (z.schema.isBigInt(schema)) {
return BigInt(ctx.arr[ctx.i++].slice(0, -1));
}
if (z.schema.isArray(schema)) {
const arrSchema = schema as TArray;
const arr = ctx.arr[ctx.i++];
if (!Array.isArray(arr)) return arr;
if (z.schema.isObject(arrSchema.items)) {
return arr.map((e) =>
this.interpretDecodeFromValue(arrSchema.items, e),
);
}
return arr;
}
if (z.schema.isObject(schema)) {
const result: Record<string, any> = {};
for (const { key, isOpt, isNullable, inner } of this.getObjectFields(
schema as TObject,
)) {
const val = ctx.arr[ctx.i++];
if (isOpt) {
if (val !== null) {
result[key] = this.interpretDecodeFromValue(inner, val);
}
} else if (isNullable) {
result[key] =
val === null ? null : this.interpretDecodeFromValue(inner, val);
} else {
result[key] = this.interpretDecodeFromValue(inner, val);
}
}
return result;
}
if (z.schema.isOptional(schema) || z.schema.isUnion(schema)) {
const inner = this.unwrap(schema);
const val = ctx.arr[ctx.i++];
if (val === null) {
return this.isNullable(schema) ? null : undefined;
}
if (z.schema.isObject(inner) || z.schema.isArray(inner)) {
return this.interpretDecodeFromValue(inner, val);
}
return val;
}
return ctx.arr[ctx.i++];
}
protected interpretDecodeFromValue(schema: TSchema, value: any): any {
if (this.isLeaf(schema)) {
return value;
}
if (z.schema.isBigInt(schema)) {
return BigInt(value.slice(0, -1));
}
if (z.schema.isArray(schema)) {
if (!Array.isArray(value)) return value;
const arrSchema = schema as TArray;
if (z.schema.isObject(arrSchema.items)) {
return value.map((e) =>
this.interpretDecodeFromValue(arrSchema.items, e),
);
}
return value;
}
if (z.schema.isObject(schema)) {
const objSchema = schema as TObject;
const props = objSchema.properties as Record<string, TSchema>;
const keys = Object.keys(props);
const result: Record<string, any> = {};
for (let idx = 0; idx < keys.length; idx++) {
const k = keys[idx];
const prop = props[k];
const isOpt = z.schema.isOptional(prop);
const inner = this.unwrap(prop);
const v = value[idx];
if (isOpt && v === null) {
// Optional fields encoded as null should be decoded as undefined (omitted)
continue;
}
if (z.schema.isObject(inner)) {
result[k] = this.interpretDecodeFromValue(inner, v);
} else if (z.schema.isBigInt(inner)) {
result[k] = BigInt(v.slice(0, -1));
} else {
result[k] = v;
}
}
return result;
}
return value;
}
// ===========================================================================
// Encoder - generates code that returns an array representation
// ===========================================================================
protected genEnc(schema: TSchema, ve: string): string {
if (this.isLeaf(schema)) {
return ve;
}
if (z.schema.isBigInt(schema)) {
return `${ve}+'n'`;
}
if (z.schema.isArray(schema)) {
const arrSchema = schema as TArray;
const itemEnc = this.genEnc(arrSchema.items, "e");
if (z.schema.isScalar(arrSchema.items)) {
return ve;
}
return `${ve}.map(e=>${itemEnc})`;
}
if (z.schema.isObject(schema)) {
const parts: string[] = [];
for (const { key, isOpt, isNullable, inner } of this.getObjectFields(
schema as TObject,
)) {
const sk = JSON.stringify(key);
const access = `${ve}[${sk}]`;
const innerEnc = this.genEnc(inner, access);
if (isOpt) {
parts.push(`${access}!==undefined?${innerEnc}:null`);
} else if (isNullable) {
parts.push(`${access}!==null?${innerEnc}:null`);
} else {
parts.push(innerEnc);
}
}
return `[${parts.join(",")}]`;
}
if (z.schema.isOptional(schema) || z.schema.isUnion(schema)) {
const inner = this.unwrap(schema);
const innerEnc = this.genEnc(inner, ve);
if (this.isNullable(schema)) {
return `${ve}!==null?${innerEnc}:null`;
}
return `${ve}!==undefined?${innerEnc}:null`;
}
return ve;
}
// ===========================================================================
// Decoder - generates code to reconstruct object from parsed array
// ===========================================================================
protected genDec(schema: TSchema): { code: string; result: string } {
const v = this.nextVar();
if (this.isLeaf(schema)) {
return { code: "", result: "a[i++]" };
}
if (z.schema.isBigInt(schema)) {
return { code: "", result: "BigInt(a[i++].slice(0,-1))" };
}
if (z.schema.isArray(schema)) {
const arrSchema = schema as TArray;
// Check if array items need transformation (objects)
if (z.schema.isObject(arrSchema.items)) {
const itemTransform = this.genDecFromValue(arrSchema.items, "e");
return { code: "", result: `a[i++].map(e=>${itemTransform})` };
}
return { code: "", result: "a[i++]" };
}
if (z.schema.isObject(schema)) {
const fields = this.getObjectFields(schema as TObject);
// Check if simple (all required primitives)
const simple = fields.every(
({ isOpt, isNullable, inner }) =>
!isOpt &&
!isNullable &&
!z.schema.isObject(inner) &&
!z.schema.isArray(inner),
);
if (simple) {
const fieldExprs = fields.map(
({ key }) => `${JSON.stringify(key)}:a[i++]`,
);
return { code: "", result: `{${fieldExprs.join(",")}}` };
}
let code = `const ${v}={};`;
for (const { key, isOpt, isNullable, inner } of fields) {
const sk = JSON.stringify(key);
if (isOpt) {
const nested = this.genDecFromValue(inner, "t");
code += `{const t=a[i++];if(t!==null){${v}[${sk}]=${nested};}}`;
} else if (isNullable) {
const nested = this.genDecFromValue(inner, "t");
code += `{const t=a[i++];if(t===null){${v}[${sk}]=null;}else{${v}[${sk}]=${nested};}}`;
} else if (z.schema.isObject(inner)) {
const nested = this.genDecFromValue(inner, "a[i++]");
code += `${v}[${sk}]=${nested};`;
} else if (z.schema.isArray(inner)) {
// Handle arrays - check if items need transformation
const arrSchema = inner as TArray;
if (z.schema.isObject(arrSchema.items)) {
const itemTransform = this.genDecFromValue(arrSchema.items, "e");
code += `${v}[${sk}]=a[i++].map(e=>${itemTransform});`;
} else {
code += `${v}[${sk}]=a[i++];`;
}
} else {
code += `${v}[${sk}]=a[i++];`;
}
}
return { code, result: v };
}
if (z.schema.isOptional(schema) || z.schema.isUnion(schema)) {
const inner = this.unwrap(schema);
const innerDec = this.genDec(inner);
const nullVal = this.isNullable(schema) ? "null" : "undefined";
return {
code: `const ${v}t=a[i++];let ${v};if(${v}t===null){${v}=${nullVal};}else{${innerDec.code.replace(/a\[i\+\+\]/g, `${v}t`)}${v}=${innerDec.result.replace(/a\[i\+\+\]/g, `${v}t`)};}`,
result: v,
};
}
return { code: "", result: "a[i++]" };
}
protected genDecFromValue(schema: TSchema, expr: string): string {
if (this.isLeaf(schema)) {
return expr;
}
if (z.schema.isBigInt(schema)) {
return `BigInt(${expr}.slice(0,-1))`;
}
if (z.schema.isArray(schema)) {
const items = schema.items as TSchema;
if (z.schema.isObject(items) || z.schema.isBigInt(items)) {
const v = this.nextVar();
return `${expr}.map(${v}=>${this.genDecFromValue(items, v)})`;
}
return expr;
}
if (z.schema.isObject(schema)) {
const objSchema = schema as TObject;
const props = objSchema.properties as Record<string, TSchema>;
const keys = Object.keys(props);
const v = this.nextVar();
const fields = keys.map((k, idx) => {
const inner = this.unwrap(props[k]);
const innerExpr = `${v}[${idx}]`;
const sk = JSON.stringify(k);
if (z.schema.isObject(inner)) {
return `${sk}:${this.genDecFromValue(inner, innerExpr)}`;
}
if (z.schema.isBigInt(inner)) {
return `${sk}:BigInt(${innerExpr}.slice(0,-1))`;
}
return `${sk}:${innerExpr}`;
});
return `((${v}=${expr})=>({${fields.join(",")}}))()`;
}
return expr;
}
// ===========================================================================
// Helpers
// ===========================================================================
protected isLeaf(schema: TSchema): boolean {
return z.schema.isScalar(schema) || this.isEnum(schema);
}
protected getObjectFields(schema: TObject): Array<{
key: string;
isOpt: boolean;
isNullable: boolean;
inner: TSchema;
}> {
const props = schema.properties as Record<string, TSchema>;
const req = new Set(z.schema.requiredKeys(schema));
return Object.keys(props).map((key) => {
const ps = props[key];
return {
key,
isOpt: !req.has(key) || z.schema.isOptional(ps),
isNullable: this.isNullable(ps),
inner: this.unwrap(ps),
};
});
}
protected isEnum(schema: TSchema): boolean {
return (
"enum" in schema &&
Array.isArray((schema as { enum?: unknown[] }).enum) &&
((schema as { enum?: unknown[] }).enum?.length ?? 0) > 0
);
}
protected isNullable(schema: TSchema): boolean {
// zod represents `z.nullable(x)` as a `ZodNullable` wrapper (and
// `z.nullable(x).optional()` nests it inside a `ZodOptional`), so walk the
// optional/nullable wrapper chain rather than only inspecting a union.
let cur: any = schema;
while (cur) {
if (z.schema.isNullable(cur)) return true;
if (z.schema.isOptional(cur) && typeof cur.unwrap === "function") {
cur = cur.unwrap();
continue;
}
break;
}
// typebox-era nullable was a union `[schema, null]`.
if (z.schema.isUnion(schema)) {
const unionSchema = schema as TUnion;
return unionSchema.anyOf?.some((s: any) => z.schema.isNull(s)) ?? false;
}
return false;
}
protected unwrap(schema: TSchema): TSchema {
// Peel zod optional/nullable/default wrappers (`z.optional`, `z.nullable`).
// Without this the codec recurses forever on an optional field: the local
// unwrap only knew the typebox union representation and returned the same
// `ZodOptional` schema unchanged.
const peeled = z.schema.unwrap(schema);
// typebox-era nullable union `[schema, null]` — pick the first non-null.
if (z.schema.isUnion(peeled)) {
const unionSchema = peeled as TUnion;
if (Array.isArray(unionSchema.anyOf)) {
return (
(unionSchema.anyOf.find(
(s: any) => !z.schema.isNull(s),
) as TSchema) || peeled
);
}
}
return peeled;
}
/**
* Reconstruct an object from a parsed array (for when input is already parsed).
*/
protected reconstructObject(schema: TSchema, arr: any[]): any {
if (!z.schema.isObject(schema)) return arr;
const result: Record<string, any> = {};
let i = 0;
for (const { key, isOpt, isNullable, inner } of this.getObjectFields(
schema as TObject,
)) {
const val = arr[i++];
if (isOpt) {
if (val !== null) {
result[key] = z.schema.isObject(inner)
? this.reconstructObject(inner, val)
: val;
}
} else if (isNullable) {
result[key] =
val === null
? null
: z.schema.isObject(inner)
? this.reconstructObject(inner, val)
: val;
} else {
result[key] = z.schema.isObject(inner)
? this.reconstructObject(inner, val)
: val;
}
}
return result;
}
}