@hiddentao/clockwork-engine
Version:
A TypeScript/PIXI.js game engine for deterministic, replayable games with built-in rendering
170 lines (169 loc) • 6.43 kB
JavaScript
export class Serializer {
constructor() {
this.typeRegistry = new Map();
}
/**
* Register a custom class for serialization and deserialization
* Classes must implement Serializable interface with serialize() method
* and provide static deserialize() method for reconstruction
* @param typeName Unique identifier for this type in serialized data
* @param classConstructor Class constructor implementing SerializableClass interface
*/
registerType(typeName, classConstructor) {
this.typeRegistry.set(typeName, classConstructor);
}
/**
* Serialize any value to a JSON-safe format with type information
* Handles primitives, arrays, objects, and registered custom classes
* @param value The value to serialize (primitive, array, object, or Serializable)
* @returns Serialized data that can be JSON.stringify'd and later deserialized
*/
serialize(value) {
if (value === null || value === undefined) {
return value;
}
// Handle primitives (string, number, boolean)
if (typeof value === "string" ||
typeof value === "number" ||
typeof value === "boolean") {
return value;
}
// Handle arrays
if (Array.isArray(value)) {
return {
__type: "Array",
__data: value.map((item) => this.serialize(item)),
};
}
// Handle objects with custom serialize method
if (value &&
typeof value === "object" &&
typeof value.serialize === "function") {
const typeName = this.getTypeName(value);
return {
__type: typeName,
__data: this.serialize(value.serialize()),
};
}
// Handle plain objects
if (value && typeof value === "object" && value.constructor === Object) {
const serializedObj = {};
for (const [key, val] of Object.entries(value)) {
serializedObj[key] = this.serialize(val);
}
return {
__type: "Object",
__data: serializedObj,
};
}
// Handle other objects as plain objects (fallback)
if (value && typeof value === "object") {
const serializedObj = {};
for (const [key, val] of Object.entries(value)) {
serializedObj[key] = this.serialize(val);
}
return {
__type: "Object",
__data: serializedObj,
};
}
return value;
}
/**
* Deserialize a value from serialized format with security validation
* Reconstructs primitives, arrays, objects, and registered custom classes
* Only allows deserialization of registered types for security
* @param value The serialized value created by serialize()
* @returns Reconstructed original value
* @throws Error if type name contains unsafe patterns or is not registered
*/
deserialize(value) {
// Handle primitives and null/undefined
if (value === null ||
value === undefined ||
typeof value === "string" ||
typeof value === "number" ||
typeof value === "boolean") {
return value;
}
// Handle wrapped objects
if (value &&
typeof value === "object" &&
"__type" in value &&
"__data" in value) {
const wrapper = value;
// Validate type name for security
if (typeof wrapper.__type !== "string") {
throw new Error("Invalid serialized data: __type must be a string");
}
// Prevent code injection by checking for dangerous patterns
if (wrapper.__type.includes("function") ||
wrapper.__type.includes("eval") ||
wrapper.__type.includes("constructor") ||
wrapper.__type.includes("__proto__") ||
wrapper.__type.includes("prototype")) {
throw new Error(`Unsafe type name: ${wrapper.__type}`);
}
switch (wrapper.__type) {
case "Array":
return wrapper.__data.map((item) => this.deserialize(item));
case "Object": {
const obj = {};
for (const [key, val] of Object.entries(wrapper.__data)) {
obj[key] = this.deserialize(val);
}
return obj;
}
default: {
// Handle registered custom types
const classConstructor = this.typeRegistry.get(wrapper.__type);
if (classConstructor) {
const deserializedData = this.deserialize(wrapper.__data);
return classConstructor.deserialize(deserializedData);
}
// Fallback to plain object for unknown types (backward compatibility)
return this.deserialize(wrapper.__data);
}
}
}
// Handle unwrapped objects
if (value && typeof value === "object") {
const obj = {};
for (const [key, val] of Object.entries(value)) {
obj[key] = this.deserialize(val);
}
return obj;
}
return value;
}
/**
* Get the type name for a serializable object
* Attempts to find the type name in the registry by constructor match
*/
getTypeName(obj) {
for (const [typeName, classConstructor] of this.typeRegistry) {
if (obj instanceof classConstructor) {
return typeName;
}
}
// Fallback to constructor name or 'Object'
return obj.constructor.name || "Object";
}
/**
* Get all registered type names
*/
getRegisteredTypes() {
return Array.from(this.typeRegistry.keys());
}
/**
* Clear all registered types
*/
clearRegistry() {
this.typeRegistry.clear();
}
}
/**
* Singleton instance for global type registration and serialization
* Use this for most common cases where you need a shared type registry
*/
export const serializer = new Serializer();