From a2cb5dfbe0a312a71b29e1ee8c4365dee0ca371a Mon Sep 17 00:00:00 2001 From: Abhin Rustagi Date: Sun, 23 Aug 2026 12:43:16 +0530 Subject: [PATCH 1/2] fix: change the internals for observability listener management --- packages/observability/src/observability.ts | 79 +++++++++++++++------ packages/observability/tsconfig.json | 1 + 2 files changed, 59 insertions(+), 21 deletions(-) diff --git a/packages/observability/src/observability.ts b/packages/observability/src/observability.ts index 533ee599a..84bc936a5 100644 --- a/packages/observability/src/observability.ts +++ b/packages/observability/src/observability.ts @@ -7,16 +7,33 @@ import type { Remove, } from "./types"; -/** - * All listeners live in one map keyed by level plus a - * literal "all" key for `listenAll`. An event at level - * L is delivered to the L level and "all". - */ -const ALL_KEY = "all"; +/** Private — not part of the public API. */ +const EVENT_TYPE = "openui:observability"; + +function getEventTarget(): EventTarget { + if (typeof window !== "undefined") return window; + const key = Symbol.for("openui.observability.target"); + const store = globalThis as { [key]?: EventTarget }; + return (store[key] ??= new EventTarget()); +} + +function unwrap(raw: Event): ObservabilityEvent | undefined { + if (!("detail" in raw)) return undefined; + const event = (raw as CustomEvent).detail; + if (event == null || typeof event !== "object") return undefined; + return event; +} + +type Registration = { + listener: (raw: Event) => void; + levels: Set; + all: boolean; +}; /** Internal — the package exports a single shared instance, not this factory. */ function createObservability(): Observability { - const listeners = new Map>(); + const target = getEventTarget(); + const registrations = new Map(); // A throwing listener must not break the emitter or other listeners. const deliver = (listener: Handler, event: ObservabilityEvent) => { @@ -27,24 +44,45 @@ function createObservability(): Observability { } }; - const subscribe = (key: string, handler: Handler): Remove => { - let set = listeners.get(key); - if (!set) { - set = new Set(); - listeners.set(key, set); + const subscribe = (handler: Handler, keys: "all" | ObservabilityLevel[]): Remove => { + let registration = registrations.get(handler); + if (!registration) { + const listener = (raw: Event) => { + const current = registrations.get(handler); + if (!current) return; + const event = unwrap(raw); + if (!event) return; + if (!current.all && !current.levels.has(event.level)) return; + deliver(handler, event); + }; + target.addEventListener(EVENT_TYPE, listener); + registration = { listener, levels: new Set(), all: false }; + registrations.set(handler, registration); } - set.add(handler); + if (keys === "all") { + registration.all = true; + } else { + for (const level of keys) registration.levels.add(level); + } + return () => { - set.delete(handler); + const current = registrations.get(handler); + if (!current) return; + if (keys === "all") { + current.all = false; + } else { + for (const level of keys) current.levels.delete(level); + } + if (!current.all && current.levels.size === 0) { + target.removeEventListener(EVENT_TYPE, current.listener); + registrations.delete(handler); + } }; }; const emit = (level: ObservabilityLevel, detail: ObservabilityDetail): void => { const event: ObservabilityEvent = { level, timestamp: Date.now(), detail }; - const targets = new Set(); - listeners.get(level)?.forEach((listener) => targets.add(listener)); - listeners.get(ALL_KEY)?.forEach((listener) => targets.add(listener)); - targets.forEach((listener) => deliver(listener, event)); + target.dispatchEvent(new CustomEvent(EVENT_TYPE, { detail: event })); }; // The bus IS the emit function, with the rest of the API attached to it. @@ -52,10 +90,9 @@ function createObservability(): Observability { bus.listen = (level, handler) => { const levels = Array.isArray(level) ? level : [level]; - const removers = levels.map((l) => subscribe(l, handler as Handler)); - return () => removers.forEach((remove) => remove()); + return subscribe(handler as Handler, levels); }; - bus.listenAll = (handler) => subscribe(ALL_KEY, handler); + bus.listenAll = (handler) => subscribe(handler, "all"); bus.info = (detail) => emit("info", detail); bus.warn = (detail) => emit("warning", detail); bus.error = (detail) => emit("error", detail); diff --git a/packages/observability/tsconfig.json b/packages/observability/tsconfig.json index 050ddeb9a..cd6bc40f5 100644 --- a/packages/observability/tsconfig.json +++ b/packages/observability/tsconfig.json @@ -6,6 +6,7 @@ "compilerOptions": { "moduleResolution": "bundler", "module": "ESNext", + "lib": ["ES2022", "DOM"], "outDir": "./dist", "rootDir": "./src", "noEmit": true From fdd9046f48ff5062b0111120dc7585ce2bb523e4 Mon Sep 17 00:00:00 2001 From: Abhin Rustagi Date: Sun, 23 Aug 2026 14:11:37 +0530 Subject: [PATCH 2/2] fix: window only events --- packages/observability/package.json | 1 + .../observability/src/observability.test.ts | 1 + packages/observability/src/observability.ts | 18 +++++++----------- pnpm-lock.yaml | 3 +++ 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/observability/package.json b/packages/observability/package.json index 4c4eb0999..d7dc791bd 100644 --- a/packages/observability/package.json +++ b/packages/observability/package.json @@ -59,6 +59,7 @@ }, "author": "engineering@thesys.dev", "devDependencies": { + "jsdom": "^26.1.0", "vitest": "^4.1.0" } } diff --git a/packages/observability/src/observability.test.ts b/packages/observability/src/observability.test.ts index c260c24f3..fabbf28ac 100644 --- a/packages/observability/src/observability.test.ts +++ b/packages/observability/src/observability.test.ts @@ -1,3 +1,4 @@ +// @vitest-environment jsdom import { afterEach, describe, expect, it, vi } from "vitest"; import type { ObservabilityEvent } from "./index"; import { observability } from "./index"; diff --git a/packages/observability/src/observability.ts b/packages/observability/src/observability.ts index 84bc936a5..cfa621c63 100644 --- a/packages/observability/src/observability.ts +++ b/packages/observability/src/observability.ts @@ -10,13 +10,6 @@ import type { /** Private — not part of the public API. */ const EVENT_TYPE = "openui:observability"; -function getEventTarget(): EventTarget { - if (typeof window !== "undefined") return window; - const key = Symbol.for("openui.observability.target"); - const store = globalThis as { [key]?: EventTarget }; - return (store[key] ??= new EventTarget()); -} - function unwrap(raw: Event): ObservabilityEvent | undefined { if (!("detail" in raw)) return undefined; const event = (raw as CustomEvent).detail; @@ -32,7 +25,6 @@ type Registration = { /** Internal — the package exports a single shared instance, not this factory. */ function createObservability(): Observability { - const target = getEventTarget(); const registrations = new Map(); // A throwing listener must not break the emitter or other listeners. @@ -45,6 +37,9 @@ function createObservability(): Observability { }; const subscribe = (handler: Handler, keys: "all" | ObservabilityLevel[]): Remove => { + // Delivery is browser-only; no-op on the server. + if (typeof window === "undefined") return () => {}; + let registration = registrations.get(handler); if (!registration) { const listener = (raw: Event) => { @@ -55,7 +50,7 @@ function createObservability(): Observability { if (!current.all && !current.levels.has(event.level)) return; deliver(handler, event); }; - target.addEventListener(EVENT_TYPE, listener); + window.addEventListener(EVENT_TYPE, listener); registration = { listener, levels: new Set(), all: false }; registrations.set(handler, registration); } @@ -74,15 +69,16 @@ function createObservability(): Observability { for (const level of keys) current.levels.delete(level); } if (!current.all && current.levels.size === 0) { - target.removeEventListener(EVENT_TYPE, current.listener); + window.removeEventListener(EVENT_TYPE, current.listener); registrations.delete(handler); } }; }; const emit = (level: ObservabilityLevel, detail: ObservabilityDetail): void => { + if (typeof window === "undefined") return; const event: ObservabilityEvent = { level, timestamp: Date.now(), detail }; - target.dispatchEvent(new CustomEvent(EVENT_TYPE, { detail: event })); + window.dispatchEvent(new CustomEvent(EVENT_TYPE, { detail: event })); }; // The bus IS the emit function, with the rest of the API attached to it. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 19bf98fb4..403899edf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1817,6 +1817,9 @@ importers: packages/observability: devDependencies: + jsdom: + specifier: ^26.1.0 + version: 26.1.0 vitest: specifier: ^4.1.0 version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(jsdom@26.1.0)(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))