import clone from 'clone'; import dayjs from 'dayjs'; import { Traverse, type TraverseContext } from 'neotraverse/modern'; import { type AmbPlayObject, type DateLike, type JsonPlayObject, type PlayObject, REGEX_ISO8601_LOOSE, type Replace } from './Atomic.ts'; import type { ElementOf, MarkOptional } from 'ts-essentials'; import { type ErrorObject, serializeError } from 'serialize-error'; import type { PlayApiCommonDetailed } from './Api.ts'; interface BlockPath { key: string, parent: string }; type BlockPaths = BlockPath[]; /** We know some nodes will never have data that needs to be transformed * and these nodes can have lots of data so we can optimize them away by not (recursively) traversing them */ const blockedKeys: PropertyKey[] = ['patch', 'inputs', 'payload', 'response', 'error']; /** We know some paths/nodes will never have data that needs to be transformed * and these nodes can have lots of data so we can optimize them away by not (recursively) traversing them */ const blockedPaths: BlockPaths = [ { parent: 'data', key: 'meta' }, { parent: 'lifecycle', key: 'input' } ]; export const shouldBlock = (ctx: TraverseContext): boolean => { if (blockedKeys.includes(ctx.key)) { return true; } return blockedPaths.some((x) => { let blocked = x.key === ctx.key; if (blocked && x.parent !== undefined) { blocked = ctx.parent !== undefined && ctx.parent.key === x.parent; } return blocked; }); }; export const asJsonPlayObject = (play: AmbPlayObject): JsonPlayObject => { const cloned = clone(play); new Traverse(cloned).forEach((ctx, x) => { if (shouldBlock(ctx)) { ctx.block(); return; } if (dayjs.isDayjs(x)) { ctx.update(x.toISOString()); } }); return cloned as unknown as JsonPlayObject; }; export type SerializablePlaySelect = Replace, 'error', ErrorObject> & {queueStates?: Replace, 'error', ErrorObject>[]}; export const asSerializablePlaySelect = (data: MarkOptional): SerializablePlaySelect => { const { error, queueStates = [], ...rest } = data; const qMapped = queueStates.map((x) => { const { error: e, ...restQ } = x; return { ...restQ, error: e instanceof Error ? serializeError(e) : e } }); return { ...rest, // @ts-expect-error error: error instanceof Error ? serializeError(error) : error, queueStates: qMapped } } export const asPlay = (data: JsonPlayObject | PlayObject): PlayObject => { const cloned = clone(data); new Traverse(cloned).forEach((ctx, x) => { if (shouldBlock(ctx)) { ctx.block(); return; } if (typeof x === 'string' && REGEX_ISO8601_LOOSE.test(x)) { ctx.update(dayjs(x), true); } /*else if (ctx.key === 'listenRanges') { const ranges = x[0].map((y: PlayProgressAmb) => { if (y.positionPercent === undefined) { return new ListenProgressPositional({ timestamp: dayjs(y.timestamp), position: y.position }); } else { return new ListenProgressTS({ timestamp: dayjs(y.timestamp), positionPercent: y.positionPercent }); } }); ctx.update(ranges, true); }*/ }); return cloned as unknown as PlayObject; }; /** * Cheaply convert playobject-like json to playobject with full timestamps etc. * * If we know the source is a throw-away object (like from drizzle fromDriver) then we don't need to clone it * @param data * @returns */ export const asPlayCheap = (data: JsonPlayObject | PlayObject): PlayObject => { new Traverse(data).forEach((ctx, x) => { if (shouldBlock(ctx)) { ctx.block(); return; } if (typeof x === 'string' && REGEX_ISO8601_LOOSE.test(x)) { ctx.update(dayjs(x), true); } }); return data as unknown as PlayObject; }; export const marshalDayjsToWellKnownString = (ctx: TraverseContext, x: any): void => { if (dayjs.isDayjs(x)) { ctx.update(`dayjs-x.toISOString()`); } } export const marshalWellKnownIsoStringToDayjs = (ctx: TraverseContext, x: any): void => { if (typeof x === 'string' && REGEX_ISO8601_LOOSE.test(x)) { ctx.update(dayjs(x.substring(6)), true); } }