export type ProjectCommandJsonValue = | null | boolean | number | string | ProjectCommandJsonValue[] | { [key: string]: ProjectCommandJsonValue }; export interface SerializedProjectCommand { schemaVersion: number; type: string; payload: TPayload; } export function serializeProjectCommand( command: SerializedProjectCommand ): string { assertSerializedProjectCommand(command); return JSON.stringify(command); } export function deserializeProjectCommand( serialized: string ): SerializedProjectCommand { const parsed: unknown = JSON.parse(serialized); assertSerializedProjectCommand(parsed); return parsed; } export function assertSerializedProjectCommand( value: unknown ): asserts value is SerializedProjectCommand { if (!isPlainObject(value)) { throw new Error("Project command must be an object."); } if ( !Number.isSafeInteger(value.schemaVersion) || (value.schemaVersion as number) < 1 ) { throw new Error("Project command schema version must be a positive integer."); } if (typeof value.type !== "string" || !value.type.trim()) { throw new Error("Project command type is required."); } assertJsonValue(value.payload, new WeakSet()); } function assertJsonValue(value: unknown, parents: WeakSet): void { if ( value === null || typeof value === "string" || typeof value === "boolean" ) { return; } if (typeof value === "number") { if (!Number.isFinite(value)) { throw new Error("Project command numbers must be finite."); } return; } if (typeof value !== "object") { throw new Error("Project command payload must contain JSON values only."); } if (parents.has(value)) { throw new Error("Project command payload must not contain cycles."); } parents.add(value); if (Array.isArray(value)) { for (const entry of value) { assertJsonValue(entry, parents); } } else { if (!isPlainObject(value)) { throw new Error("Project command payload must contain plain objects only."); } for (const entry of Object.values(value)) { assertJsonValue(entry, parents); } } parents.delete(value); } function isPlainObject(value: unknown): value is Record { if (value === null || typeof value !== "object") { return false; } const prototype = Object.getPrototypeOf(value); return prototype === Object.prototype || prototype === null; }