life
Version:
Life.js is the first fullstack framework to build agentic web applications. It is minimal, extensible, and typesafe. Well, everything you love.
1 lines • 79.5 kB
Source Map (JSON)
{"version":3,"sources":["../shared/canon/serialize.ts","../shared/canon/stringify.ts","../shared/canon/equal.ts","../shared/canon/equal-schema.ts","../shared/canon/murmur.ts","../shared/canon/sha256.ts","../shared/canon/index.ts","../shared/nanoseconds.ts","../telemetry/helpers/otel-id.ts","../telemetry/helpers/register-consumer.ts","../telemetry/helpers/strip-ansi.ts","../telemetry/schemas.ts","../telemetry/clients/base.ts","../package.json"],"sourcesContent":["import { deserializeError, isErrorLike, serializeError } from \"serialize-error\";\nimport superjson, { type SuperJSONResult } from \"superjson\";\nimport { ZodError, z } from \"zod\";\nimport * as op from \"@/shared/operation\";\nimport { isLifeError, type LifeErrorUnion, lifeErrorFromObject, lifeErrorToObject } from \"../error\";\n\n// Register custom transformer for LifeError objects\n// biome-ignore lint/suspicious/noExplicitAny: Record<string, unknown> is serializable\nsuperjson.registerCustom<LifeErrorUnion, any>(\n {\n isApplicable: (v): v is LifeErrorUnion => isLifeError(v),\n // Using superjson.serialize ensures that 'err.cause' gets serialized properly\n serialize: (err) => superjson.serialize(lifeErrorToObject(err)),\n deserialize: (data) => lifeErrorFromObject(superjson.deserialize(data)),\n },\n \"LifeError\",\n);\n\n// Register custom transformer for ZodError to preserve all error information\n// We use unknown[] as the serialized type since ZodIssue has complex union types\n// that don't satisfy SuperJSON's JSONValue constraints\n// biome-ignore lint/suspicious/noExplicitAny: z.ZodIssue[] is not a valid JSONValue, but is serializable\nsuperjson.registerCustom<ZodError, any>(\n {\n isApplicable: (v): v is ZodError => v instanceof ZodError,\n serialize: (err) => err.issues,\n deserialize: (data) => new ZodError(data as z.core.$ZodIssue[]),\n },\n \"ZodError\",\n);\n\n// Register custom transformer for general Error objects using serialize-error\n// This runs after ZodError transformer, so ZodError takes precedence\n// biome-ignore lint/suspicious/noExplicitAny: serialize-error output is complex but serializable\nsuperjson.registerCustom<Error, any>(\n {\n isApplicable: (v): v is Error => isErrorLike(v),\n serialize: (err) => serializeError(err),\n deserialize: (data) => deserializeError(data),\n },\n \"Error\",\n);\n\n// Register custom transformer OperationResult tuples\n// biome-ignore lint/suspicious/noExplicitAny: serialize-error output is complex but serializable\nsuperjson.registerCustom<op.OperationResult<unknown>, any>(\n {\n isApplicable: (v): v is op.OperationResult<unknown> => op.isResult(v),\n serialize: (result) => op.serializeResult(result),\n deserialize: (data) => op.deserializeResult(data),\n },\n \"OperationResult\",\n);\n\n// - Primitive types\nconst serializablePrimitivesSchema = z.union([\n z.string(),\n z.number(),\n z.boolean(),\n z.null(),\n z.undefined(),\n z.bigint(),\n z.date(),\n z.instanceof(RegExp),\n z.instanceof(Error),\n z.instanceof(URL),\n z.instanceof(ArrayBuffer),\n z.instanceof(Int8Array),\n z.instanceof(Uint8Array),\n z.instanceof(Uint8ClampedArray),\n z.instanceof(Int16Array),\n z.instanceof(Uint16Array),\n z.instanceof(Int32Array),\n z.instanceof(Uint32Array),\n z.instanceof(Float32Array),\n z.instanceof(Float64Array),\n z.instanceof(BigInt64Array),\n z.instanceof(BigUint64Array),\n]);\ntype SerializablePrimitives = z.infer<typeof serializablePrimitivesSchema>;\n\n// Collections and recursive types\nexport const serializableValueSchema: z.ZodType<SerializableValue> = z.lazy(() =>\n z.union([\n serializablePrimitivesSchema,\n z.array(serializableValueSchema),\n z.set(serializableValueSchema),\n z.map(z.any(), serializableValueSchema),\n z.record(z.string(), serializableValueSchema),\n ]),\n);\nexport type SerializableValue =\n | SerializablePrimitives\n | SerializableValue[]\n | readonly SerializableValue[]\n | [SerializableValue, ...SerializableValue[]]\n | readonly [SerializableValue, ...SerializableValue[]]\n | Set<SerializableValue>\n | Map<SerializableValue, SerializableValue>\n | { [key: string]: SerializableValue };\n\nexport type SerializeResult = SuperJSONResult;\n\n/**\n * canon.serialize\n *\n * Converts any supported runtime value into the transport-friendly structure\n * that `canon` uses internally (built on top of SuperJSON). This step preserves\n * richer JavaScript types (Date, Map, Set, BigInt, RegExp, undefined, NaN,\n * Infinity, etc.) without yet enforcing key/element ordering—that canonical\n * normalization happens later in `canon.stringify`.\n *\n * Typical use cases:\n * - Store the result as JSON: `JSON.stringify(canon.serialize(v))`\n * - Send across the wire and rehydrate with `canon.deserialize`\n *\n * @param value - The value to encode into the canon/SuperJSON wire format.\n * @returns A plain JSON-safe object describing the value and its metadata.\n *\n * @example\n * ```ts\n * import { canon } from \"@shared/canon\";\n *\n * const encoded = canon.serialize(new Map([[\"a\", 1]]));\n * // → { json: {...}, meta: {...} }\n *\n * // Safe to stringify:\n * const payload = JSON.stringify(encoded);\n * ```\n */\nexport const serialize = (value: SerializableValue | unknown) =>\n op.attempt(() => superjson.serialize(value));\n\n/**\n * canon.deserialize\n *\n * Reconstructs a runtime value from a `SerializeResult` previously produced by\n * `canon.serialize`. All special types preserved during serialization are\n * restored (Date, Map, Set, BigInt, RegExp, etc.).\n *\n * @param value - The structured payload (usually parsed from JSON) to turn back into a live value.\n * @returns The fully rehydrated value.\n *\n * @example\n * ```ts\n * import { canon } from \"@shared/canon\";\n *\n * const encoded = canon.serialize(new Set([1, 2, 3]));\n * const roundTripped = canon.deserialize(encoded); // → Set {1, 2, 3}\n *\n * // When transmitting:\n * const wire = JSON.stringify(encoded);\n * const decoded = canon.deserialize(JSON.parse(wire));\n * ```\n */\nexport const deserialize = (\n value?: SerializeResult,\n): op.OperationResult<SerializableValue | undefined> => {\n if (!value) return op.success(value);\n return op.attempt(() => superjson.deserialize(value));\n};\n","/**\n * This function stringifies a given object into a JSON string, producing an\n * output with a stable keys order compared to JSON.stringify().\n *\n * Source: fast-json-stable-stringify (https://github.com/epoberezkin/fast-json-stable-stringify/blob/master/index.js)\n */\n\nimport * as op from \"@/shared/operation\";\nimport { deserialize, type SerializableValue, serialize } from \"./serialize\";\n// biome-ignore-start lint/style: reason\n// biome-ignore-start lint/suspicious: reason\n// biome-ignore-start lint/complexity: reason\n// biome-ignore-start lint/correctness: reason\n\nexport function stableDeepStringify(data: any, sortArrays: boolean, opts?: any): string {\n if (!opts) opts = {};\n if (typeof opts === \"function\") opts = { cmp: opts };\n var cycles = typeof opts.cycles === \"boolean\" ? opts.cycles : false;\n\n var cmp =\n opts.cmp &&\n ((f) => (node: any) => (a: any, b: any) => {\n var aobj = { key: a, value: node[a] };\n var bobj = { key: b, value: node[b] };\n return f(aobj, bobj);\n })(opts.cmp);\n\n var seen: any[] = [];\n return (function stringify_(node: any): string {\n if (node && node.toJSON && typeof node.toJSON === \"function\") {\n node = node.toJSON();\n }\n\n if (node === undefined) return \"\";\n if (typeof node == \"number\") return isFinite(node) ? \"\" + node : \"null\";\n if (typeof node !== \"object\") return JSON.stringify(node);\n\n var i, out;\n if (Array.isArray(node)) {\n const items = node.map((item) => stringify_(item) || \"null\");\n if (sortArrays) items.sort();\n return \"[\" + items.join(\",\") + \"]\";\n }\n\n if (node === null) return \"null\";\n\n if (seen.indexOf(node) !== -1) {\n if (cycles) return JSON.stringify(\"__cycle__\");\n throw new TypeError(\"Converting circular structure to JSON\");\n }\n\n var seenIndex = seen.push(node) - 1;\n var keys = Object.keys(node).sort(cmp && cmp(node));\n out = \"\";\n for (i = 0; i < keys.length; i++) {\n var key = keys[i];\n var value = stringify_(node[key!]);\n\n if (!value) continue;\n if (out) out += \",\";\n out += JSON.stringify(key) + \":\" + value;\n }\n seen.splice(seenIndex, 1);\n return \"{\" + out + \"}\";\n })(data);\n}\n\n// biome-ignore-end lint/style: reason\n// biome-ignore-end lint/suspicious: reason\n// biome-ignore-end lint/complexity: reason\n// biome-ignore-end lint/correctness: reason\n\n/**\n * canon.stringify\n *\n * Converts any value supported by `canon.serialize` into a **canonical, deterministic,\n * order‑insensitive string**. Objects have their keys sorted, collection types\n * (Arrays, Maps, Sets) are normalized, and special primitives are preserved during\n * serialization so that structurally equivalent values always stringify to the same\n * output.\n *\n * @param value - The value to canonicalize and stringify.\n * @returns A canonical JSON string representing the value.\n *\n * @example\n * ```ts\n * import { stringify } from \"@shared/canon\";\n *\n * // Key / element order does not change the result:\n * stringify({ b: 1, a: 2 }) === stringify({ a: 2, b: 1 }); // → true\n *\n * // Collections are normalized:\n * stringify(new Set([3, 1, 2])) === stringify(new Set([1, 2, 3])); // → true\n * ```\n */\nexport const stringify = (value: SerializableValue, sortArrays = false) => {\n const [err, res] = serialize(value);\n if (err) return op.failure(err);\n return op.attempt(() => stableDeepStringify(res, sortArrays));\n};\n\n/**\n * canon.parse\n *\n * Reconstructs a value previously produced by `canon.stringify` by first parsing\n * the JSON string and then running it through `canon.deserialize`, restoring\n * special types supported by the canon layer.\n *\n * @param value - A canonical string produced by `canon.stringify`.\n * @returns The deserialized value.\n *\n * @throws If `value` is not valid JSON.\n * @example\n * ```ts\n * import { stringify, parse } from \"@shared/canon\";\n *\n * const s = stringify(new Map([[\"a\", 1], [\"b\", 2]]));\n * const v = parse(s); // → Map { \"a\" => 1, \"b\" => 2 }\n * ```\n */\nexport const parse = (value: string): op.OperationResult<SerializableValue> => {\n const [err, res] = op.attempt(() => JSON.parse(value));\n if (err) return op.failure(err);\n return deserialize(res);\n};\n","import * as op from \"@/shared/operation\";\nimport type { SerializableValue } from \"./serialize\";\nimport { stringify } from \"./stringify\";\n\n/**\n * canon.equal\n *\n * Performs a deep, order‑independent equality check by first canonicalizing\n * each input into a deterministic representation—sorting object keys,\n * normalizing collection elements (arrays, Maps, Sets), then checking whether\n * the resulting serialized forms are identical.\n *\n * @template T - A value that can be serialized by `canon.stringify` (i.e. SuperJSON‑compatible).\n * @param a - The first value to compare.\n * @param b - The second value to compare.\n * @returns `true` if `a` and `b` are identical; otherwise returns `false`.\n *\n * @example\n * ```ts\n * import { canon } from \"@shared/canon\";\n *\n * // Order of keys and elements does not matter:\n * canon.equal({ b: 1, a: 2 }, { a: 2, b: 1 }); // → true\n * canon.equal(new Set([3, 1, 2]), new Set([1, 2, 3])); // → true\n *\n * // Dates, Regex, Maps, etc. are handled via SuperJSON:\n * canon.equal(new Date(\"2021-08-01\"), \"2021-08-01\"); // → true\n * ```\n */\nexport const equal = (a: SerializableValue, b: SerializableValue) => {\n const [err1, data1] = stringify(a, true);\n if (err1) return op.failure(err1);\n const [err2, data2] = stringify(b, true);\n if (err2) return op.failure(err2);\n return op.success(data1 === data2);\n};\n","import z from \"zod\";\nimport * as op from \"@/shared/operation\";\nimport { equal } from \"./equal\";\nimport type { SerializableValue } from \"./serialize\";\n\n/**\n * canon.equalSchema\n *\n * Performs a deep, order‑independent equality between two Zod schema, by first\n * canonicalizin each input into a deterministic representation—sorting JSON schema,\n * then checking whether the resulting serialized forms are identical.\n *\n * @template T - A Zod schema.\n * @param a - The first schema to compare.\n * @param b - The second schema to compare.\n * @returns `true` if `a` and `b` are identical; otherwise returns `false`.\n *\n * @example\n * ```ts\n * import { canon } from \"@shared/canon\";\n *\n * const schema1 = z.object({ b: z.number(), a: z.number() });\n * const schema2 = z.object({ a: z.number(), b: z.number() });\n *\n * // Order of keys and elements does not matter:\n * canon.equalSchema(schema1, schema2); // → true\n * ```\n */\nexport const equalSchema = (a: z.ZodType, b: z.ZodType) => {\n const [errJsonA, jsonA] = op.attempt(() => z.toJSONSchema(a, { unrepresentable: \"any\" }));\n if (errJsonA) return op.failure(errJsonA);\n const [errJsonB, jsonB] = op.attempt(() => z.toJSONSchema(b, { unrepresentable: \"any\" }));\n if (errJsonB) return op.failure(errJsonB);\n return equal(jsonA as SerializableValue, jsonB as SerializableValue);\n};\n","import MurmurHash3 from \"imurmurhash\";\nimport * as op from \"@/shared/operation\";\nimport type { SerializableValue } from \"./serialize\";\nimport { stringify } from \"./stringify\";\n\n/**\n * canon.murmur3\n *\n * MurmurHash3 is a non-cryptographic hash function designed to be fast and\n * have a low collision rate. It is a good choice for hash tables and other\n * data structures where collision resistance is not critical.\n *\n * For secure hashing, use `canon.sha256`.\n *\n * @param value - The value to hash.\n * @returns A 32-bit integer hash of the value.\n *\n * @example\n * ```ts\n * import { canon } from \"@shared/canon\";\n *\n * canon.murmur3({ a: 1, b: 2 }) === canon.murmur3({ b: 2, a: 1 }); // → true\n * ```\n */\n\nexport const murmur3 = (value: SerializableValue) => {\n try {\n const [err, data] = stringify(value);\n if (err) return op.failure(err);\n const [errHash, hashNumber] = op.attempt(() => MurmurHash3(data).result());\n if (errHash) return op.failure(errHash);\n const hash = hashNumber.toString(16);\n return op.success(hash);\n } catch (error) {\n return op.failure({ code: \"Unknown\", cause: error });\n }\n};\n","import * as op from \"@/shared/operation\";\nimport type { SerializableValue } from \"./serialize\";\nimport { stringify } from \"./stringify\";\n\n/**\n * canon.sha256\n *\n * Produces a deterministic SHA‑256 digest for any value supported by\n * `canon.serialize`. The value is first canonicalized (keys sorted, collection\n * elements normalized, special types preserved) via `canon.stringify`, and the\n * resulting canonical string is then hashed. Because the canonical form is\n * order‑insensitive, structurally equivalent inputs always yield the same hash.\n *\n * For fast and synchronous hashing, see `canon.murmur3`.\n *\n * @param value - The value to hash.\n * @returns A 64‑character, lowercase hex SHA‑256 digest of the value’s canonical form.\n *\n * @example\n * ```ts\n * import { canon } from \"@shared/canon\";\n *\n * // Key order does not affect the hash:\n * canon.sha256({ b: 1, a: 2 }) === canon.sha256({ a: 2, b: 1 }); // → true\n *\n * // Works with Sets, Maps, Dates, BigInts, etc. (as supported by canon.serialize):\n * canon.sha256(new Set([3, 1, 2]));\n * canon.sha256(new Map([[\"b\", 1], [\"a\", 2]]));\n * canon.sha256(new Date(\"2021-08-01\"));\n * ```\n */\n\nexport const sha256 = async (value: SerializableValue) => {\n try {\n const [err, data] = stringify(value);\n if (err) return op.failure(err);\n const hashedData = new TextEncoder().encode(data);\n const hashBuffer = await crypto.subtle.digest(\"SHA-256\", hashedData);\n const hashArray = Array.from(new Uint8Array(hashBuffer));\n const hash = hashArray.map((b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n return op.success(hash);\n } catch (error) {\n return op.failure({ code: \"Unknown\", cause: error });\n }\n};\n","import { equal } from \"./equal\";\nimport { equalSchema } from \"./equal-schema\";\nimport { murmur3 } from \"./murmur\";\nimport { deserialize, serialize } from \"./serialize\";\nimport { sha256 } from \"./sha256\";\nimport { parse, stringify } from \"./stringify\";\n\nexport const canon = {\n equal,\n equalSchema,\n serialize,\n deserialize,\n sha256,\n stringify,\n parse,\n murmur3,\n};\n\nexport type { SerializableValue, SerializeResult, serializableValueSchema } from \"./serialize\";\n","/**\n * Cross-runtime process.hrtime.bigint() equivalent.\n * - Uses Node's `process.hrtime.bigint()` when available.\n * - Else uses `process.hrtime()` tuple if present.\n * - Else uses browser `performance.now()` (monotonic) converted to ns.\n * - Else falls back to `Date.now()` converted to ns.\n */\nconst hrtimeBigint: () => bigint = (() => {\n // 1) Native Node: process.hrtime.bigint()\n if (typeof globalThis.process?.hrtime?.bigint === \"function\") {\n return () => globalThis.process.hrtime.bigint();\n }\n\n // 2) Node-like tuple: process.hrtime() -> [seconds, nanoseconds]\n if (typeof globalThis.process?.hrtime === \"function\") {\n return () => {\n const [s, n] = globalThis.process.hrtime() as [number, number];\n return BigInt(s) * 1_000_000_000n + BigInt(n);\n };\n }\n\n // 3) Browser: performance.now() in milliseconds (fractional), monotonic.\n if (typeof globalThis.performance?.now === \"function\") {\n let last = 0n;\n return () => {\n const ns = BigInt(Math.floor(globalThis.performance.now() * 1e6));\n if (ns > last) last = ns;\n return last;\n };\n }\n\n // 4) Final fallback: Date.now() in ms -> ns.\n return () => BigInt(Date.now()) * 1_000_000n;\n})();\n\nexport const ns = {\n /**\n * Returns the current Unix timestamp in nanoseconds\n * @returns BigInt (ns since epoch)\n */\n now() {\n return hrtimeBigint();\n },\n\n /**\n * Returns the duration between the current time and the given start time\n * @param start - BigInt (ns since epoch)\n * @returns BigInt (ns)\n */\n since(n?: bigint) {\n if (!n) return 0n;\n return ns.now() - n;\n },\n\n /**\n * Converts nanoseconds to milliseconds\n * @param ns - BigInt (ns)\n * @returns number (ms)\n */\n toMs(n?: bigint) {\n if (!n) return 0n;\n return n / 1_000_000n;\n },\n};\n","export const SPAN_ID_BYTES = 8;\nexport const TRACE_ID_BYTES = 16;\nexport const LOG_ID_BYTES = 16;\nexport const METRIC_ID_BYTES = 16;\n\nconst SHARED_CHAR_CODES_ARRAY = new Array(32);\n\n/**\n * Copyright The OpenTelemetry Authors - Apache License, Version 2.0\n * Taken from:\n * https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-sdk-trace-base/src/platform/browser/RandomIdGenerator.ts\n */\nfunction getOtelIdGenerator(bytes: number): () => string {\n return function generateId() {\n for (let i = 0; i < bytes * 2; i++) {\n SHARED_CHAR_CODES_ARRAY[i] = Math.floor(Math.random() * 16) + 48;\n // valid hex characters in the range 48-57 and 97-102\n if (SHARED_CHAR_CODES_ARRAY[i] >= 58) {\n SHARED_CHAR_CODES_ARRAY[i] += 39;\n }\n }\n return String.fromCharCode.apply(null, SHARED_CHAR_CODES_ARRAY.slice(0, bytes * 2));\n };\n}\n\nexport const generateSpanId = getOtelIdGenerator(SPAN_ID_BYTES);\nexport const generateTraceId = getOtelIdGenerator(TRACE_ID_BYTES);\nexport const generateLogId = getOtelIdGenerator(LOG_ID_BYTES);\nexport const generateMetricId = getOtelIdGenerator(METRIC_ID_BYTES);\n","import { AsyncQueue } from \"@/shared/async-queue\";\nimport type { TelemetryConsumer, TelemetryConsumerList, TelemetrySignal } from \"../types\";\n\nexport const registerConsumer = (consumer: TelemetryConsumer, list: TelemetryConsumerList) => {\n // Create a queue for this consumer\n const queue = new AsyncQueue<TelemetrySignal>();\n list.push({ instance: consumer, queue });\n\n // Start the consumer with the queue\n consumer.start(queue);\n\n // Return a function to unregister that consumer later\n let unregistered = false;\n return () => {\n if (unregistered) return;\n\n // Find and remove the consumer\n const index = list.findIndex((c) => c.instance === consumer);\n if (index !== -1) {\n list[index]?.queue.stop();\n list.splice(index, 1);\n unregistered = true;\n }\n };\n};\n","// Taken from https://github.com/chalk/strip-ansi/blob/main/index.js\nconst regex = new RegExp(\n [\n \"[\\\\u001B\\\\u009B][[\\\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\\\d\\\\/#&.:=?%@~_]+)*|[a-zA-Z\\\\d]+(?:;[-a-zA-Z\\\\d\\\\/#&.:=?%@~_]*)*)?(?:\\\\u0007|\\\\u001B\\\\u005C|\\\\u009C))\",\n \"(?:(?:\\\\d{1,4}(?:;\\\\d{0,4})*)?[\\\\dA-PR-TZcf-nq-uy=><~]))\",\n ].join(\"|\"),\n \"g\",\n);\nexport default function stripAnsi(string: string) {\n if (typeof string !== \"string\") {\n throw new TypeError(`Expected a \\`string\\`, got \\`${typeof string}\\``);\n }\n // Even though the regex is global, we don't need to reset the `.lastIndex`\n // because unlike `.exec()` and `.test()`, `.replace()` does it automatically\n // and doing it manually has a performance penalty.\n return string.replace(regex, \"\");\n}\n","import z from \"zod\";\nimport { LOG_ID_BYTES, METRIC_ID_BYTES, SPAN_ID_BYTES, TRACE_ID_BYTES } from \"./helpers/otel-id\";\n\n// Attributes\nexport const telemetryAttributeSchema = z.record(z.string(), z.unknown());\n\n// Resource\nexport const telemetryResourceSchema = z\n .object({\n environment: z.enum([\"development\", \"production\", \"staging\", \"test\"]),\n lifeVersion: z.string(),\n })\n .and(\n z.discriminatedUnion(\"platform\", [\n z.object({\n platform: z.literal(\"node\"),\n isCi: z.boolean(),\n nodeVersion: z.string(),\n osName: z.string(),\n osVersion: z.string(),\n cpuCount: z.number(),\n cpuArchitecture: z.string(),\n schemaVersion: z.string().prefault(\"1\"),\n }),\n z.object({\n platform: z.literal(\"browser\"),\n deviceType: z.enum([\n \"desktop\",\n \"mobile\",\n \"tablet\",\n \"wearable\",\n \"smarttv\",\n \"console\",\n \"xr\",\n \"embedded\",\n \"unknown\",\n ]),\n deviceBrand: z.string(),\n deviceModel: z.string(),\n osName: z.string(),\n osVersion: z.string(),\n cpuArchitecture: z\n .enum([\n \"ia32\",\n \"ia64\",\n \"amd64\",\n \"arm\",\n \"arm64\",\n \"armhf\",\n \"avr\",\n \"avr32\",\n \"irix\",\n \"irix64\",\n \"mips\",\n \"mips64\",\n \"68k\",\n \"pa-risc\",\n \"ppc\",\n \"sparc\",\n \"sparc64\",\n \"alpha\",\n \"unknown\",\n ])\n .optional(),\n browserUserAgent: z.string(),\n browserName: z.string(),\n browserVersion: z.string(),\n browserEngine: z.enum([\n \"Amaya\",\n \"ArkWeb\",\n \"Blink\",\n \"EdgeHTML\",\n \"Flow\",\n \"Gecko\",\n \"Goanna\",\n \"iCab\",\n \"KHTML\",\n \"LibWeb\",\n \"Links\",\n \"Lynx\",\n \"NetFront\",\n \"NetSurf\",\n \"Presto\",\n \"Servo\",\n \"Tasman\",\n \"Trident\",\n \"w3m\",\n \"WebKit\",\n \"unknown\",\n ]),\n isBot: z.boolean(),\n isAiBot: z.boolean(),\n schemaVersion: z.string().prefault(\"1\"),\n }),\n ]),\n );\n\n// IDs\nconst HEX_LOWER_RE = /^[0-9a-f]+$/;\nfunction createOtelHexIdSchema(bytes: number) {\n const len = bytes * 2;\n return z\n .string()\n .length(len, `expected ${len} lowercase hex chars`)\n .regex(HEX_LOWER_RE, \"must be lowercase hex [0-9a-f]\");\n}\nconst telemetryTraceIdSchema = createOtelHexIdSchema(TRACE_ID_BYTES);\nconst telemetryLogIdSchema = createOtelHexIdSchema(LOG_ID_BYTES);\nconst telemetrySpanIdSchema = createOtelHexIdSchema(SPAN_ID_BYTES);\nconst telemetryMetricIdSchema = createOtelHexIdSchema(METRIC_ID_BYTES);\n\n// Log\nexport const telemetryLogSchema = z.object({\n id: telemetryLogIdSchema,\n scope: z.string(),\n resource: telemetryResourceSchema,\n attributes: telemetryAttributeSchema.optional(),\n level: z.enum([\"debug\", \"info\", \"warn\", \"error\", \"fatal\"]),\n /**\n * The raw message with ANSI escape codes.\n * Useful for displaying in the terminal.\n * e.g., will preserve style of `chalk.bold.red(\"Hello\")`\n */\n message: z.string(),\n /**\n * The message without any ANSI escape codes.\n * Useful for using messages outside of the terminal.\n */\n messageUnstyled: z.string(),\n timestamp: z.bigint(),\n stack: z.string(),\n traceId: telemetryTraceIdSchema.optional(),\n spanId: telemetrySpanIdSchema.optional(),\n error: z.custom<Error>().optional(),\n});\n\n// Span\nexport const telemetrySpanSchema = z.object({\n id: telemetrySpanIdSchema,\n scope: z.string(),\n resource: telemetryResourceSchema,\n attributes: telemetryAttributeSchema.optional(),\n name: z.string(),\n /**\n * The timestamp when the span started in nanoseconds.\n */\n startTimestamp: z.bigint(),\n /**\n * The timestamp when the span ended in nanoseconds.\n * Is undefined if the span hasn't ended yet.\n */\n endTimestamp: z.bigint(),\n /**\n * The duration of the span in nanoseconds.\n * Is undefined if the span hasn't ended yet.\n */\n duration: z.bigint(),\n traceId: telemetryTraceIdSchema,\n parentSpanId: z.string().optional(),\n logs: z.array(\n telemetryLogSchema.omit({ resource: true, scope: true, traceId: true, spanId: true }),\n ),\n});\n\n// Metric\nexport const telemetryMetricSchema = z.object({\n id: telemetryMetricIdSchema,\n scope: z.string(),\n resource: telemetryResourceSchema,\n attributes: telemetryAttributeSchema.optional(),\n kind: z.enum([\"counter\", \"updown\", \"histogram\"]),\n name: z.string(),\n value: z.number().or(z.bigint()),\n});\n\n// Signal\nexport const telemetrySignalSchema = z.discriminatedUnion(\"type\", [\n telemetryLogSchema.extend({ type: z.literal(\"log\") }),\n telemetrySpanSchema.extend({ type: z.literal(\"span\") }),\n telemetryMetricSchema.extend({ type: z.literal(\"metric\") }),\n]);\n","import type z from \"zod\";\nimport { canon, type SerializableValue } from \"@/shared/canon\";\nimport { deepClone } from \"@/shared/deep-clone\";\nimport { lifeError } from \"@/shared/error\";\nimport { ns } from \"@/shared/nanoseconds\";\nimport {\n generateLogId,\n generateMetricId,\n generateSpanId,\n generateTraceId,\n} from \"../helpers/otel-id\";\nimport { registerConsumer } from \"../helpers/register-consumer\";\nimport stripAnsi from \"../helpers/strip-ansi\";\nimport { telemetrySignalSchema } from \"../schemas\";\nimport type {\n TelemetryAttributes,\n TelemetryConsumer,\n TelemetryConsumerList,\n TelemetryLog,\n TelemetryLogHandle,\n TelemetryLogInput,\n TelemetryMetric,\n TelemetryResource,\n TelemetryScopeDefinition,\n TelemetryScopesDefinition,\n TelemetrySignal,\n TelemetrySpan,\n TelemetrySpanHandle,\n} from \"../types\";\n\nexport const defineScopes = <const Schemas extends Record<string, z.ZodObject>>(\n scopes: {\n [K in keyof Schemas]: TelemetryScopeDefinition<Schemas[K]>;\n },\n) => scopes;\n\n/**\n * The telemetry client provides a unified interface for logging, tracing, and metrics\n * collection across the Life.js codebase. The collected data is almost OTEL-compliant\n * and can be piped to any provider via consumers registering.\n *\n * @dev The program shouldn't fail or throw an error because of telemetry, so the\n * telemetry clients are not using the 'operation' library, they swallow and log any error.\n *\n * @todo Support auto-capture OTEL telemetry data from nested libraries.\n * @todo Properly parse and clean stack traces. Right now, we're using the raw stack trace string from the Error object.\n */\nexport abstract class TelemetryClient {\n static #clients: InstanceType<typeof TelemetryClient>[] = [];\n\n readonly #scopesDefinition: TelemetryScopesDefinition = {};\n readonly scope: string;\n readonly resource: TelemetryResource;\n clientAttributes: TelemetryAttributes = {};\n\n constructor(scopesDefinition: TelemetryScopesDefinition, scope: string) {\n this.scope = scope;\n this.#scopesDefinition = scopesDefinition;\n this.resource = this.getResource();\n TelemetryClient.#clients.push(this);\n }\n\n // To be implemented by runtime-specific subclasses\n protected abstract getResource(): TelemetryResource;\n protected abstract getCurrentSpanData(): TelemetrySpan | undefined;\n protected abstract runWithSpanData(\n spanData: TelemetrySpan | undefined,\n fn: () => unknown,\n ): unknown;\n\n // Global consumers\n static readonly #globalConsumers: TelemetryConsumerList = [];\n /**\n * Registers a callback consumer to receive telemetry data from all the clients.\n * @param consumer - The consumer to register\n * @returns A function that unregisters the consumer when called\n * @example\n * ```typescript\n * const unregister = telemetry.registerGlobalConsumer(myConsumer);\n * unregister(); // Later, to stop receiving events\n * ```\n */\n static registerGlobalConsumer(consumer: TelemetryConsumer): () => void {\n return registerConsumer(consumer, TelemetryClient.#globalConsumers);\n }\n\n /**\n * Flushes any globally pending telemetry data, ensuring that all the consumers\n * of all the TelemetryClient instances have finished processing before returning\n * or until the timeout is reached.\n * @param timeoutMs - Maximum time to wait in milliseconds (default: 10000ms)\n * @returns A promise that resolves when flushing is complete or timeout is reached\n */\n static async flushAllConsumers(timeoutMs = 10_000): Promise<void> {\n await Promise.all(TelemetryClient.#clients.map((client) => client.flushConsumers(timeoutMs)));\n }\n\n // Local consumers\n readonly #consumers: TelemetryConsumerList = [];\n /**\n * Registers a callback consumer to receive telemetry data from this client.\n * @param consumer - The consumer to register\n * @returns A function that unregisters the consumer when called\n * @example\n * ```typescript\n * const unregister = telemetry.registerConsumer(myConsumer);\n * unregister(); // Later, to stop receiving events\n * ```\n */\n registerConsumer(consumer: TelemetryConsumer): () => void {\n try {\n return registerConsumer(consumer, this.#consumers);\n } catch (error) {\n this.log.error({\n message: \"Error registering telemetry consumer.\",\n error,\n attributes: { consumer },\n });\n return () => void 0;\n }\n }\n\n /**\n * Flushes any pending telemetry data, ensuring that all consumers have finished\n * processing before returning or until the timeout is reached.\n * @param timeoutMs - Maximum time to wait in milliseconds (default: 10000ms)\n * @returns A promise that resolves when flushing is complete or timeout is reached\n */\n async flushConsumers(timeoutMs = 10_000): Promise<void> {\n try {\n const startTime = Date.now();\n\n // Wait for all queues to be empty and consumers to finish processing\n while (Date.now() - startTime < timeoutMs) {\n let allDone = true;\n for (const { instance, queue } of this.#allConsumers) {\n if (queue.length() > 0 || instance.isProcessing?.()) {\n allDone = false;\n break;\n }\n }\n if (allDone) return;\n // Check again after 50ms\n // biome-ignore lint/performance/noAwaitInLoops: sequential required here\n await new Promise((resolve) => setTimeout(resolve, 50));\n }\n } catch (error) {\n // Swallow any error, but log it.\n this.log.error({\n message: \"Error flushing telemetry consumers.\",\n error,\n attributes: { timeoutMs },\n });\n }\n }\n\n /**\n * Sets a client-level attribute that will be included in all telemetry data from this client.\n * @param key - The attribute key\n * @param value - The attribute value (must be serializable, else will be ignored)\n * @example\n * ```typescript\n * telemetry.setAttribute(\"modelId\", \"abc\");\n * telemetry.setAttribute(\"region\", \"us-west-2\");\n * ```\n */\n setAttribute(key: string, value: unknown): void {\n this.clientAttributes[key] = value;\n }\n\n /**\n * Sets multiple client-level attributes that will be included in all telemetry data from this client.\n * @param attributes - The attributes to set\n * @example\n * ```typescript\n * telemetry.setAttributes({ modelId: \"abc\", region: \"us-west-2\" });\n * ```\n */\n setAttributes(attributes: TelemetryAttributes): void {\n this.clientAttributes = { ...this.clientAttributes, ...attributes };\n }\n\n /**\n * Measure the duration and capture logs about an operation.\n * Automatically handles both sync and async functions, preserving their return types.\n * trace() calls can be nested and will produce nested spans.\n * @param name - The name of the operation being traced\n * @param fn - The function to execute within the span context (sync or async)\n * @param options - Optional attributes and parent span\n * @returns The result of the function (direct value for sync, Promise for async)\n * @example\n * ```typescript\n * // Sync function - no await needed\n * const hash = telemetry.trace(\"compute-hash\", ({ log, setAttribute }) => {\n * log.info({ message: \"Computing hash\" });\n * const result = computeHash(data);\n * setAttribute(\"algorithm\", \"sha256\");\n * return result;\n * }, { attributes: { dataSize: data.length } });\n *\n * // Async function - await the result\n * const user = await telemetry.trace(\"fetch-user\", async ({ log }) => {\n * log.info({ message: \"Fetching user\" });\n * const response = await fetch(`/api/users/${id}`);\n * return response.json();\n * }, { attributes: { userId: id } });\n *\n * // Early end example:\n * telemetry.trace(\"process-item\", ({ end }) => {\n * if (shouldSkip) {\n * end();\n * return;\n * }\n * // ... process item\n * });\n * ```\n */\n trace<T>(\n name: string,\n fn: (params: TelemetrySpanHandle) => T,\n options: { attributes?: TelemetryAttributes; parent?: TelemetrySpanHandle } = {},\n ): T {\n // Use explicit parent if provided, otherwise get from context\n const parentSpan = options.parent ?? this.getCurrentSpan();\n const parentSpanData = parentSpan?.getData();\n\n // Build the span data\n const spanData: TelemetrySpan = {\n id: generateSpanId(),\n resource: this.resource,\n scope: this.scope,\n attributes: { ...options.attributes, ...this.clientAttributes },\n name,\n startTimestamp: ns.now(),\n endTimestamp: -1n,\n duration: -1n,\n traceId: parentSpanData?.traceId || generateTraceId(),\n parentSpanId: parentSpanData?.id,\n logs: [],\n };\n const span = this.#createSpanHandle(spanData);\n\n // Run the function in the span context\n return this.runWithSpanData(spanData, () => {\n try {\n const result = fn(span);\n\n // Async functions\n if (result instanceof Promise) return result.finally(() => span.end());\n\n // Sync functions\n span.end();\n return result;\n } catch (error) {\n // Ensure span is ended even on error\n span.end();\n throw error;\n }\n }) as T;\n }\n\n /**\n * Get the ambient tracing span handle.\n * @returns The current tracing span parent (if any)\n */\n getCurrentSpan() {\n const spanData = this.getCurrentSpanData();\n if (!spanData) return;\n return this.#createSpanHandle(spanData);\n }\n\n /**\n * Send a telemetry signal to all consumers.\n * This a raw method, prefer using log.*(), counter(), updown(), histogram(), etc.\n * @param signal - The telemetry signal to send\n */\n sendSignal(signal: TelemetrySignal, throwOnError = false): void {\n try {\n // Load signal with global attributes\n signal.attributes = { ...signal.attributes, ...this.clientAttributes };\n\n // Validate the signal shape\n const { error, data: parsedSignal } = telemetrySignalSchema.safeParse(signal);\n if (error)\n throw lifeError({\n code: \"Validation\",\n message: \"Invalid telemetry signal shape. It has been ignored.\",\n attributes: { signal: signal as SerializableValue },\n cause: error,\n });\n\n // Validate the scope\n const scopeDefinition = this.#scopesDefinition?.[parsedSignal?.scope ?? \"\"];\n if (!scopeDefinition) {\n throw lifeError({\n code: \"Validation\",\n message: `Invalid telemetry scope '${parsedSignal.scope}' in signal. It has been ignored.`,\n attributes: { parsedSignal: parsedSignal as SerializableValue },\n });\n }\n\n // Validate the scope's required attributes\n if (scopeDefinition.requiredAttributesSchema) {\n const { error: attributesError, data: parsedAttributes } =\n scopeDefinition.requiredAttributesSchema.safeParse(parsedSignal?.attributes ?? {});\n if (attributesError)\n throw lifeError({\n code: \"Validation\",\n message: `Signal contains invalid required attributes for scope '${parsedSignal.scope}'. It has been ignored.`,\n attributes: { parsedSignal: parsedSignal as SerializableValue },\n cause: attributesError,\n });\n parsedSignal.attributes = { ...parsedSignal.attributes, ...parsedAttributes };\n }\n\n // Validate the signal serializability\n const [errSerialize, serializedSignal] = canon.stringify(parsedSignal as SerializableValue);\n if (errSerialize) {\n throw lifeError({\n code: \"Validation\",\n message: \"Failed to serialize telemetry signal. It has been ignored.\",\n attributes: { parsedSignal: parsedSignal as SerializableValue },\n cause: errSerialize,\n });\n }\n\n // Validate the signal size\n const MAX_SIGNAL_SIZE = 1024 * 1024; // 1MB\n if (serializedSignal.length > MAX_SIGNAL_SIZE) {\n throw lifeError({\n code: \"Validation\",\n message: \"Telemetry signal is too large. It has been ignored.\",\n attributes: { parsedSignal: parsedSignal as SerializableValue },\n });\n }\n\n // Send to all consumers\n for (const consumer of this.#allConsumers) consumer.queue.push(parsedSignal);\n } catch (error) {\n if (throwOnError) throw error;\n else\n this.log.error({\n message: \"Unexpected error sending telemetry signal. It has been ignored.\",\n error,\n attributes: { signal },\n });\n }\n }\n\n /**\n * Unsafe version of sendSignal() bypassing all validation and checks.\n * Used internally to forward telemetry signals between processes.\n * @internal\n */\n _unsafeSendSignal(signal: TelemetrySignal): void {\n for (const consumer of this.#allConsumers) consumer.queue.push(signal);\n }\n\n /**\n * Creates a counter metric for tracking monotonically increasing values.\n * @param name - The name of the counter metric\n * @returns An object with methods to increment the counter\n * @example\n * ```typescript\n * const requestCounter = telemetry.counter(\"http_requests_total\");\n * requestCounter.increment({ method: \"GET\", status: \"200\" });\n * requestCounter.add(5, { batch: \"true\" });\n * ```\n */\n counter(name: string) {\n return {\n add: (n: number | bigint, attributes?: TelemetryAttributes) => {\n try {\n const fullMetric: TelemetryMetric = {\n id: generateMetricId(),\n resource: this.resource,\n scope: this.scope,\n attributes,\n name,\n kind: \"counter\",\n value: n,\n };\n this.sendSignal({ type: \"metric\", ...fullMetric });\n } catch (error) {\n // Swallow any error, but log it.\n this.log.error({\n message: \"Error adding to counter metric.\",\n error,\n attributes: { name, n, attributes },\n });\n }\n },\n increment: (attributes?: TelemetryAttributes) => {\n try {\n this.counter(name).add(1, attributes);\n } catch (error) {\n // Swallow any error, but log it.\n this.log.error({\n message: \"Error incrementing counter metric.\",\n error,\n attributes: { name, attributes },\n });\n }\n },\n };\n }\n\n /**\n * Creates an up/down counter metric for tracking values that can increase or decrease.\n * @param name - The name of the up/down counter metric\n * @returns An object with methods to modify the counter\n * @example\n * ```typescript\n * const connectionGauge = telemetry.updown(\"active_connections\");\n * connectionGauge.increment(); // New connection\n * connectionGauge.decrement(); // Connection closed\n * connectionGauge.add(10); // Bulk connections\n * ```\n */\n updown(name: string) {\n return {\n add: (n: number | bigint, attributes?: TelemetryAttributes) => {\n try {\n const fullMetric: TelemetryMetric = {\n id: generateMetricId(),\n resource: this.resource,\n scope: this.scope,\n attributes,\n name,\n kind: \"updown\",\n value: n,\n };\n this.sendSignal({ type: \"metric\", ...fullMetric });\n } catch (error) {\n // Swallow any error, but log it.\n this.log.error({\n message: \"Error adding to updown metric.\",\n error,\n attributes: { name, n, attributes },\n });\n }\n },\n remove: (n: number | bigint, attributes?: TelemetryAttributes) => {\n try {\n this.updown(name).add(-n, attributes);\n } catch (error) {\n // Swallow any error, but log it.\n this.log.error({\n message: \"Error removing from updown metric.\",\n error,\n attributes: { name, n, attributes },\n });\n }\n },\n increment: (attributes?: TelemetryAttributes) => {\n try {\n this.updown(name).add(1, attributes);\n } catch (error) {\n this.log.error({\n message: \"Error incrementing updown metric.\",\n error,\n attributes: { name, attributes },\n });\n }\n },\n decrement: (attributes?: TelemetryAttributes) => {\n try {\n this.updown(name).add(-1, attributes);\n } catch (error) {\n this.log.error({\n message: \"Error decrementing updown metric.\",\n error,\n attributes: { name, attributes },\n });\n }\n },\n };\n }\n\n /**\n * Creates a histogram metric for recording value distributions over time.\n * @param name - The name of the histogram metric\n * @returns An object with a method to record values\n * @example\n * ```typescript\n * const latencyHistogram = telemetry.histogram(\"request_duration_ms\");\n * latencyHistogram.record(responseTime, { endpoint: \"/api/users\" });\n * ```\n */\n histogram(name: string) {\n return {\n record: (value: number | bigint, attributes?: TelemetryAttributes) => {\n try {\n const fullMetric: TelemetryMetric = {\n id: generateMetricId(),\n resource: this.resource,\n scope: this.scope,\n attributes,\n name,\n kind: \"histogram\",\n value,\n };\n this.sendSignal({ type: \"metric\", ...fullMetric });\n } catch (error) {\n // Swallow any error, but log it.\n this.log.error({\n message: \"Error recording histogram metric.\",\n error,\n attributes: { name, value, attributes },\n });\n }\n },\n };\n }\n\n /**\n * Log writer for recording events at different severity levels.\n * Logs are automatically associated with the current span context if one exists.\n * @example\n * ```typescript\n * telemetry.log.info({ message: \"Server started\", attributes: { port: 3000 } });\n * telemetry.log.error({ error: new Error(\"Connection failed\"), attributes: { host: \"db.example.com\" } });\n * telemetry.log.warn({ message: \"Deprecated API used\", attributes: { endpoint: \"/v1/users\" } });\n * ```\n */\n log: TelemetryLogHandle = {\n debug: (input) => this.#emitLog(\"debug\", input),\n info: (input) => this.#emitLog(\"info\", input),\n warn: (input) => this.#emitLog(\"warn\", input),\n error: (input) => this.#emitLog(\"error\", input),\n fatal: (input) => this.#emitLog(\"fatal\", input),\n };\n\n // ========== Private Methods ==========\n\n /**\n * Get all consumers.\n * Including local and global consumers.\n */\n get #allConsumers(): TelemetryConsumerList {\n return this.#consumers.concat(TelemetryClient.#globalConsumers);\n }\n\n #endSpan(spanData: TelemetrySpan) {\n try {\n // Ignore if the span is already ended\n if (spanData.endTimestamp !== -1n) return;\n\n // End the span\n spanData.endTimestamp = ns.now();\n spanData.duration = ns.since(spanData.startTimestamp);\n\n // Send span to all consumer queues\n this.sendSignal({ type: \"span\", ...spanData });\n } catch (error) {\n this.log.error({\n message: \"Error ending span.\",\n error,\n attributes: { span: spanData },\n });\n }\n }\n\n /**\n * Private helper to create a span and its associated methods.\n * Used by both trace() and traceBlock() to avoid code duplication.\n */\n #createSpanHandle(spanData: TelemetrySpan): TelemetrySpanHandle {\n // Create getData() method\n const getData = () => deepClone(spanData);\n\n // Create _getWritableData() method\n const _getWritableData = () => spanData;\n\n // Create end() method\n const end = () => this.#endSpan(spanData);\n\n // Create setAttribute() method\n const setAttribute = (key: string, value: unknown) => {\n if (spanData.endTimestamp !== -1n) {\n this.log.error({\n message:\n \"Attempted to call 'setAttribute()' on already ended span. This is unexpected and must be fixed.\",\n attributes: { span: spanData, key, value },\n });\n return;\n }\n spanData.attributes = spanData.attributes || {};\n spanData.attributes[key] = value;\n };\n\n // Create setAttributes() method\n const setAttributes = (_attributes: TelemetryAttributes) => {\n if (spanData.endTimestamp !== -1n) {\n this.log.error({\n message:\n \"Attempted to call 'setAttributes()' on already ended span. This is unexpected and must be fixed.\",\n attributes: { span: spanData, attributes: _attributes },\n });\n return;\n }\n spanData.attributes = { ...spanData.attributes, ..._attributes };\n };\n\n // Build the span handle\n const span = { end, setAttribute, setAttributes, getData, _getWritableData };\n\n // Create log.{level}() methods\n const createSpanLogMethod =\n (level: TelemetryLog[\"level\"]) => (input: Omit<TelemetryLogInput, \"span\">) => {\n if (spanData.endTimestamp !== -1n) {\n this.log.error({\n message: `Attempted to call 'log.${level}()' on already ended span. This is unexpected and must be fixed.`,\n attributes: { span: spanData, input },\n });\n return;\n }\n this.#emitLog(level, { ...input, span } as TelemetryLogInput);\n };\n const log: TelemetryLogHandle = {\n debug: createSpanLogMethod(\"debug\"),\n info: createSpanLogMethod(\"info\"),\n warn: createSpanLogMethod(\"warn\"),\n error: createSpanLogMethod(\"error\"),\n fatal: createSpanLogMethod(\"fatal\"),\n };\n\n // Add log methods to span\n Object.assign(span, { log });\n\n // Return the span and handle\n return span as TelemetrySpanHandle;\n }\n\n /**\n * Internal log method that handles all log levels\n */\n #emitLog(level: TelemetryLog[\"level\"], input: TelemetryLogInput, fromEmitLog = false): void {\n try {\n if (fromEmitLog) {\n // Avoid recursive calls by just console.logging the error if coming from #emitLog()\n let consoleMethod: keyof Console = \"log\";\n if (level === \"error\") consoleMethod = \"error\";\n else if (level === \"warn\") consoleMethod = \"warn\";\n console[consoleMethod](`[${level}] ${JSON.stringify(input)}`);\n return; // Return early to prevent any further processing that could cause errors\n }\n\n const spanData = input.span?._