import { Buffer } from "node:buffer"; import type { TraceEnvelope } from "./types.ts"; export function serializeTraceEvent(event: TraceEnvelope): string { const activeAncestors = new WeakSet(); const ancestors: object[] = []; function alignAncestors(holder: object): void { while (ancestors.length > 0 && ancestors[ancestors.length - 1] !== holder) { const ancestor = ancestors.pop(); if (ancestor !== undefined) { activeAncestors.delete(ancestor); } } } function enterObject(holder: object, candidate: object): boolean { alignAncestors(holder); if (activeAncestors.has(candidate)) { return false; } ancestors.push(candidate); activeAncestors.add(candidate); return true; } function enterReplacement(holder: object, source: object, replacement: object): boolean { alignAncestors(holder); if (activeAncestors.has(source)) { return false; } ancestors.push(source, replacement); activeAncestors.add(source); activeAncestors.add(replacement); return true; } function getDataProperty(value: object, key: string): unknown { let current: object | null = value; while (current !== null) { const descriptor = Object.getOwnPropertyDescriptor(current, key); if (descriptor !== undefined) { return "value" in descriptor ? descriptor.value : undefined; } current = Object.getPrototypeOf(current); } return undefined; } const serialized = JSON.stringify(event, function (this: Record, key, value: unknown): unknown { const descriptor = Object.getOwnPropertyDescriptor(this, key); const hasDataDescriptor = descriptor !== undefined && "value" in descriptor; const originalValue: unknown = hasDataDescriptor ? descriptor.value : value; if (typeof originalValue === "bigint") { const tagged = { type: "BigInt", value: originalValue.toString() }; enterObject(this, tagged); return tagged; } if (originalValue === undefined) { const tagged = { type: "Undefined" }; enterObject(this, tagged); return tagged; } if (originalValue instanceof Error) { const name = getDataProperty(originalValue, "name"); const message = getDataProperty(originalValue, "message"); const tagged: Record = { type: "Error", name: typeof name === "string" ? name : originalValue instanceof AggregateError ? "AggregateError" : "Error", message: typeof message === "string" ? message : "", }; const properties: Record = {}; const descriptors = Object.getOwnPropertyDescriptors(originalValue); for (const field of Object.keys(descriptors)) { const fieldDescriptor = descriptors[field]; if (fieldDescriptor === undefined) { continue; } if (!("value" in fieldDescriptor)) { properties[field] = { type: "Accessor", get: fieldDescriptor.get !== undefined, set: fieldDescriptor.set !== undefined, }; continue; } const fieldValue: unknown = fieldDescriptor.value; switch (field) { case "stack": if (fieldValue !== undefined) { tagged.stack = fieldValue; } break; case "cause": tagged.cause = fieldValue; break; case "errors": tagged.errors = fieldValue; break; case "name": case "message": break; default: properties[field] = fieldValue; break; } } if (Object.keys(properties).length > 0) { tagged.properties = properties; } if (!enterReplacement(this, originalValue, tagged)) { return { type: "Circular" }; } return tagged; } if (Buffer.isBuffer(originalValue)) { const tagged = { type: "Buffer", data: originalValue.toString("base64") }; enterObject(this, tagged); return tagged; } if (ArrayBuffer.isView(originalValue) && !(originalValue instanceof DataView)) { const tagged = { type: originalValue.constructor.name, data: Array.from( new Uint8Array(originalValue.buffer, originalValue.byteOffset, originalValue.byteLength), ), }; enterObject(this, tagged); return tagged; } if (typeof value === "object" && value !== null) { if (!enterObject(this, value)) { return { type: "Circular" }; } } return value; }); if (serialized === undefined) { throw new TypeError("Trace event did not serialize to JSON"); } return `${serialized}\n`; }