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