diff --git a/compositor_web/src/wasm.rs b/compositor_web/src/wasm.rs index 323b50c70..c94f965be 100644 --- a/compositor_web/src/wasm.rs +++ b/compositor_web/src/wasm.rs @@ -23,19 +23,23 @@ mod wgpu; pub fn start() -> Result<(), JsValue> { console_error_panic_hook::set_once(); tracing_wasm::set_as_global_default(); - wasm_log::init(wasm_log::Config::new(log::Level::Info)); Ok(()) } #[wasm_bindgen] pub async fn create_renderer(options: JsValue) -> Result { + let options = types::from_js_value::(options)?; + // This option will only be respected for the first renderer + let _ = wasm_log::try_init(wasm_log::Config::new(options.logger_level.into())); + let (device, queue) = create_wgpu_context().await?; - let mut options: compositor_render::RendererOptions = - types::from_js_value::(options)?.into(); - options.wgpu_ctx = Some((device, queue)); - let (renderer, _) = Renderer::new(options).map_err(to_js_error)?; + let (renderer, _) = Renderer::new(compositor_render::RendererOptions { + wgpu_ctx: Some((device, queue)), + ..options.into() + }) + .map_err(to_js_error)?; let input_uploader = InputUploader::default(); let output_downloader = OutputDownloader::default(); diff --git a/compositor_web/src/wasm/types.rs b/compositor_web/src/wasm/types.rs index ec07f600d..80a08e158 100644 --- a/compositor_web/src/wasm/types.rs +++ b/compositor_web/src/wasm/types.rs @@ -6,7 +6,30 @@ use wasm_bindgen::prelude::*; #[derive(Debug, Deserialize)] pub struct RendererOptions { - stream_fallback_timeout_ms: u64, + pub stream_fallback_timeout_ms: u64, + pub logger_level: LoggerLevel, +} + +#[derive(Debug, Deserialize, Clone, Copy)] +#[serde(rename_all = "snake_case")] +pub enum LoggerLevel { + Error, + Warn, + Info, + Debug, + Trace, +} + +impl From for log::Level { + fn from(value: LoggerLevel) -> Self { + match value { + LoggerLevel::Error => log::Level::Error, + LoggerLevel::Warn => log::Level::Warn, + LoggerLevel::Info => log::Level::Info, + LoggerLevel::Debug => log::Level::Debug, + LoggerLevel::Trace => log::Level::Trace, + } + } } #[wasm_bindgen] diff --git a/ts/@live-compositor/browser-render/src/renderer.ts b/ts/@live-compositor/browser-render/src/renderer.ts index 50ec76188..924de3a2b 100644 --- a/ts/@live-compositor/browser-render/src/renderer.ts +++ b/ts/@live-compositor/browser-render/src/renderer.ts @@ -6,6 +6,8 @@ export type RendererOptions = { * A timeout that defines when the compositor should switch to fallback on the input stream that stopped sending frames. */ streamFallbackTimeoutMs: number; + + logger_level?: 'error' | 'warn' | 'info' | 'debug' | 'trace'; }; export type FrameSet = { @@ -34,6 +36,7 @@ export class Renderer { public static async create(options: RendererOptions): Promise { const renderer = await wasm.create_renderer({ stream_fallback_timeout_ms: options.streamFallbackTimeoutMs, + logger_level: options.logger_level ?? 'warn', }); return new Renderer(renderer); } diff --git a/ts/@live-compositor/core/package.json b/ts/@live-compositor/core/package.json index a3dffef7f..c783ceaa6 100644 --- a/ts/@live-compositor/core/package.json +++ b/ts/@live-compositor/core/package.json @@ -32,10 +32,11 @@ "@types/react-reconciler": "0.28.8" }, "dependencies": { + "pino": "^9.5.0", "react-reconciler": "0.29.2" }, "peerDependencies": { - "react": "*", - "live-compositor": "workspace:^0.1.0" + "live-compositor": "workspace:^0.1.0", + "react": "*" } } diff --git a/ts/@live-compositor/core/src/compositorManager.ts b/ts/@live-compositor/core/src/compositorManager.ts index e45aa5a06..3292737d4 100644 --- a/ts/@live-compositor/core/src/compositorManager.ts +++ b/ts/@live-compositor/core/src/compositorManager.ts @@ -1,3 +1,4 @@ +import type { Logger } from 'pino'; import type { ApiRequest } from './api.js'; export interface SetupInstanceOptions { @@ -5,6 +6,8 @@ export interface SetupInstanceOptions { * sets LIVE_COMPOSITOR_AHEAD_OF_TIME_PROCESSING_ENABLE environment variable. */ aheadOfTimeProcessing: boolean; + + logger: Logger; } export interface CompositorManager { diff --git a/ts/@live-compositor/core/src/event.ts b/ts/@live-compositor/core/src/event.ts index 42bf97ea0..aee5c5789 100644 --- a/ts/@live-compositor/core/src/event.ts +++ b/ts/@live-compositor/core/src/event.ts @@ -1,12 +1,13 @@ import { _liveCompositorInternals } from 'live-compositor'; import { parseInputRef } from './api/input.js'; +import type { Logger } from 'pino'; export type CompositorEvent = _liveCompositorInternals.CompositorEvent; export const CompositorEventType = _liveCompositorInternals.CompositorEventType; -export function parseEvent(event: any): CompositorEvent | null { +export function parseEvent(event: any, logger: Logger): CompositorEvent | null { if (!event.type) { - console.error(`Malformed event: ${event}`); + logger.error(`Malformed event: ${event}`); return null; } else if ( [ @@ -22,7 +23,7 @@ export function parseEvent(event: any): CompositorEvent | null { } else if (CompositorEventType.OUTPUT_DONE === event.type) { return { type: event.type, outputId: event.output_id }; } else { - console.error(`Unknown event type: ${event.type}`); + logger.error(`Unknown event type: ${event.type}`); return null; } } diff --git a/ts/@live-compositor/core/src/index.ts b/ts/@live-compositor/core/src/index.ts index 6010d8052..8d44db3c8 100644 --- a/ts/@live-compositor/core/src/index.ts +++ b/ts/@live-compositor/core/src/index.ts @@ -4,3 +4,4 @@ export { OfflineCompositor } from './offline/compositor.js'; export { CompositorManager, SetupInstanceOptions } from './compositorManager.js'; export { RegisterInputRequest, RegisterInput } from './api/input.js'; export { RegisterOutputRequest, RegisterOutput } from './api/output.js'; +export { Logger, LoggerLevel } from './logger.js'; diff --git a/ts/@live-compositor/core/src/live/compositor.ts b/ts/@live-compositor/core/src/live/compositor.ts index 6667dc7e7..9969736d3 100644 --- a/ts/@live-compositor/core/src/live/compositor.ts +++ b/ts/@live-compositor/core/src/live/compositor.ts @@ -11,6 +11,7 @@ import { parseEvent } from '../event.js'; import { intoRegisterImage, intoRegisterWebRenderer } from '../api/renderer.js'; import { handleEvent } from './event.js'; import type { ReactElement } from 'react'; +import type { Logger } from 'pino'; export class LiveCompositor { private manager: CompositorManager; @@ -18,16 +19,21 @@ export class LiveCompositor { private store: _liveCompositorInternals.LiveInputStreamStore; private outputs: Record = {}; private startTime?: number; + private logger: Logger; - public constructor(manager: CompositorManager) { + public constructor(manager: CompositorManager, logger: Logger) { this.manager = manager; this.api = new ApiClient(this.manager); - this.store = new _liveCompositorInternals.LiveInputStreamStore(); + this.store = new _liveCompositorInternals.LiveInputStreamStore(logger); + this.logger = logger; } public async init(): Promise { this.manager.registerEventListener((event: unknown) => this.handleEvent(event)); - await this.manager.setupInstance({ aheadOfTimeProcessing: false }); + await this.manager.setupInstance({ + aheadOfTimeProcessing: false, + logger: this.logger.child({ element: 'connection-manager' }), + }); } public async registerOutput( @@ -35,7 +41,16 @@ export class LiveCompositor { root: ReactElement, request: RegisterOutput ): Promise { - const output = new Output(outputId, root, request, this.api, this.store, this.startTime); + this.logger.info({ outputId, type: request.type }, 'Register new output'); + const output = new Output( + outputId, + root, + request, + this.api, + this.store, + this.startTime, + this.logger + ); const apiRequest = intoRegisterOutput(request, output.scene()); const result = await this.api.registerOutput(outputId, apiRequest); @@ -45,6 +60,7 @@ export class LiveCompositor { } public async unregisterOutput(outputId: string): Promise { + this.logger.info({ outputId }, 'Unregister output'); this.outputs[outputId].close(); delete this.outputs[outputId]; // TODO: wait for event @@ -52,6 +68,7 @@ export class LiveCompositor { } public async registerInput(inputId: string, request: RegisterInput): Promise { + this.logger.info({ inputId, type: request.type }, 'Register new input'); return this.store.runBlocking(async updateStore => { const inputRef = { type: 'global', id: inputId } as const; const result = await this.api.registerInput(inputRef, intoRegisterInput(request)); @@ -68,6 +85,7 @@ export class LiveCompositor { } public async unregisterInput(inputId: string): Promise { + this.logger.info({ inputId }, 'Unregister input'); return this.store.runBlocking(async updateStore => { const inputRef = { type: 'global', id: inputId } as const; const result = this.api.unregisterInput(inputRef, {}); @@ -80,18 +98,22 @@ export class LiveCompositor { shaderId: string, request: Renderers.RegisterShader ): Promise { + this.logger.info({ shaderId }, 'Register shader'); return this.api.registerShader(shaderId, request); } public async unregisterShader(shaderId: string): Promise { + this.logger.info({ shaderId }, 'Unregister shader'); return this.api.unregisterShader(shaderId); } public async registerImage(imageId: string, request: Renderers.RegisterImage): Promise { + this.logger.info({ imageId }, 'Register image'); return this.api.registerImage(imageId, intoRegisterImage(request)); } public async unregisterImage(imageId: string): Promise { + this.logger.info({ imageId }, 'Unregister image'); return this.api.unregisterImage(imageId); } @@ -99,14 +121,17 @@ export class LiveCompositor { instanceId: string, request: Renderers.RegisterWebRenderer ): Promise { + this.logger.info({ instanceId }, 'Register web renderer'); return this.api.registerWebRenderer(instanceId, intoRegisterWebRenderer(request)); } public async unregisterWebRenderer(instanceId: string): Promise { + this.logger.info({ instanceId }, 'Unregister web renderer'); return this.api.unregisterWebRenderer(instanceId); } public async start(): Promise { + this.logger.info('Start compositor instance.'); const startTime = Date.now(); await this.api.start(); Object.values(this.outputs).forEach(output => { @@ -116,10 +141,11 @@ export class LiveCompositor { } private handleEvent(rawEvent: unknown) { - const event = parseEvent(rawEvent); + const event = parseEvent(rawEvent, this.logger); if (!event) { return; } + this.logger.debug({ event }, 'New event received'); handleEvent(this.store, this.outputs, event); } } diff --git a/ts/@live-compositor/core/src/live/output.ts b/ts/@live-compositor/core/src/live/output.ts index 432f3c16d..72c7081b1 100644 --- a/ts/@live-compositor/core/src/live/output.ts +++ b/ts/@live-compositor/core/src/live/output.ts @@ -8,6 +8,7 @@ import type { RegisterOutput } from '../api/output.js'; import { intoAudioInputsConfiguration } from '../api/output.js'; import { throttle } from '../utils.js'; import { OutputRootComponent, OutputShutdownStateStore } from '../rootComponent.js'; +import type { Logger } from 'pino'; type AudioContext = _liveCompositorInternals.AudioContext; type LiveTimeContext = _liveCompositorInternals.LiveTimeContext; @@ -21,6 +22,7 @@ class Output { timeContext: LiveTimeContext; internalInputStreamStore: LiveInputStreamStore; outputShutdownStateStore: OutputShutdownStateStore; + logger: Logger; shouldUpdateWhenReady: boolean = false; throttledUpdate: () => void; @@ -36,9 +38,11 @@ class Output { registerRequest: RegisterOutput, api: ApiClient, store: LiveInputStreamStore, - startTimestamp: number | undefined + startTimestamp: number | undefined, + logger: Logger ) { this.api = api; + this.logger = logger; this.outputId = outputId; this.outputShutdownStateStore = new OutputShutdownStateStore(); this.shouldUpdateWhenReady = false; @@ -52,7 +56,7 @@ class Output { const onUpdate = () => this.throttledUpdate(); this.audioContext = new _liveCompositorInternals.AudioContext(onUpdate); this.timeContext = new _liveCompositorInternals.LiveTimeContext(); - this.internalInputStreamStore = new _liveCompositorInternals.LiveInputStreamStore(); + this.internalInputStreamStore = new _liveCompositorInternals.LiveInputStreamStore(this.logger); if (startTimestamp !== undefined) { this.timeContext.initClock(startTimestamp); } @@ -68,6 +72,7 @@ class Output { rootElement, onUpdate, idPrefix: `${outputId}-`, + logger: logger.child({ element: 'react-renderer' }), }); } @@ -90,9 +95,15 @@ class Output { } public async ready() { - this.throttledUpdate = throttle(async () => { - await this.api.updateScene(this.outputId, this.scene()); - }, 30); + this.throttledUpdate = throttle( + async () => { + await this.api.updateScene(this.outputId, this.scene()); + }, + { + timeoutMs: 30, + logger: this.logger, + } + ); if (this.shouldUpdateWhenReady) { this.throttledUpdate(); } @@ -113,6 +124,7 @@ class OutputContext implements CompositorOutputContext { public readonly audioContext: _liveCompositorInternals.AudioContext; public readonly timeContext: _liveCompositorInternals.TimeContext; public readonly outputId: string; + public readonly logger: Logger; private output: Output; constructor( @@ -126,6 +138,7 @@ class OutputContext implements CompositorOutputContext { this.audioContext = output.audioContext; this.timeContext = output.timeContext; this.outputId = outputId; + this.logger = output.logger; } public async registerMp4Input( @@ -158,6 +171,7 @@ class OutputContext implements CompositorOutputContext { }; }); } + public async unregisterMp4Input(inputId: number): Promise { await this.output.api.unregisterInput( { diff --git a/ts/@live-compositor/core/src/logger.ts b/ts/@live-compositor/core/src/logger.ts new file mode 100644 index 000000000..bbf847d2c --- /dev/null +++ b/ts/@live-compositor/core/src/logger.ts @@ -0,0 +1,11 @@ +import type { _liveCompositorInternals } from 'live-compositor'; + +export type Logger = _liveCompositorInternals.Logger; + +export enum LoggerLevel { + ERROR = 'error', + WARN = 'warn', + INFO = 'info', + DEBUG = 'debug', + TRACE = 'trace', +} diff --git a/ts/@live-compositor/core/src/offline/compositor.ts b/ts/@live-compositor/core/src/offline/compositor.ts index c0c548266..c6385f8d3 100644 --- a/ts/@live-compositor/core/src/offline/compositor.ts +++ b/ts/@live-compositor/core/src/offline/compositor.ts @@ -6,10 +6,11 @@ import type { RegisterOutput } from '../api/output.js'; import { intoRegisterOutput } from '../api/output.js'; import type { RegisterInput } from '../api/input.js'; import { intoRegisterInput } from '../api/input.js'; -import { intoRegisterImage, intoRegisterWebRenderer } from '../api/renderer.js'; +import { intoRegisterImage } from '../api/renderer.js'; import OfflineOutput from './output.js'; import { CompositorEventType, parseEvent } from '../event.js'; import type { ReactElement } from 'react'; +import type { Logger } from 'pino'; /** * Offline rendering only supports one output, so we can just pick any value to use @@ -26,23 +27,28 @@ export class OfflineCompositor { * Start and end timestamp of an inputs (if known). */ private inputTimestamps: number[] = []; + private logger: Logger; - public constructor(manager: CompositorManager) { + public constructor(manager: CompositorManager, logger: Logger) { this.manager = manager; this.api = new ApiClient(this.manager); this.store = new _liveCompositorInternals.OfflineInputStreamStore(); + this.logger = logger; } public async init(): Promise { this.checkNotStarted(); - await this.manager.setupInstance({ aheadOfTimeProcessing: true }); + await this.manager.setupInstance({ + aheadOfTimeProcessing: true, + logger: this.logger.child({ element: 'connection-manager' }), + }); } public async render(root: ReactElement, request: RegisterOutput, durationMs?: number) { this.checkNotStarted(); this.renderStarted = true; - const output = new OfflineOutput(root, request, this.api, this.store, durationMs); + const output = new OfflineOutput(root, request, this.api, this.store, this.logger, durationMs); for (const inputTimestamp of this.inputTimestamps) { output.timeContext.addTimestamp({ timestamp: inputTimestamp }); } @@ -58,7 +64,7 @@ export class OfflineCompositor { const renderPromise = new Promise((res, _rej) => { this.manager.registerEventListener(rawEvent => { - const event = parseEvent(rawEvent); + const event = parseEvent(rawEvent, this.logger); if ( event && event.type === CompositorEventType.OUTPUT_DONE && @@ -76,6 +82,8 @@ export class OfflineCompositor { public async registerInput(inputId: string, request: RegisterInput): Promise { this.checkNotStarted(); + this.logger.info({ inputId, type: request.type }, 'Register new input'); + const inputRef = { type: 'global', id: inputId } as const; const result = await this.api.registerInput(inputRef, intoRegisterInput(request)); @@ -111,22 +119,16 @@ export class OfflineCompositor { request: Renderers.RegisterShader ): Promise { this.checkNotStarted(); + this.logger.info({ shaderId }, 'Register shader'); return this.api.registerShader(shaderId, request); } public async registerImage(imageId: string, request: Renderers.RegisterImage): Promise { this.checkNotStarted(); + this.logger.info({ imageId }, 'Register image'); return this.api.registerImage(imageId, intoRegisterImage(request)); } - public async registerWebRenderer( - instanceId: string, - request: Renderers.RegisterWebRenderer - ): Promise { - this.checkNotStarted(); - return this.api.registerWebRenderer(instanceId, intoRegisterWebRenderer(request)); - } - private checkNotStarted() { if (this.renderStarted) { throw new Error('Render was already started.'); diff --git a/ts/@live-compositor/core/src/offline/output.ts b/ts/@live-compositor/core/src/offline/output.ts index 46b30dde4..6c7e97cc1 100644 --- a/ts/@live-compositor/core/src/offline/output.ts +++ b/ts/@live-compositor/core/src/offline/output.ts @@ -9,6 +9,7 @@ import { intoAudioInputsConfiguration } from '../api/output.js'; import { sleep } from '../utils.js'; import { OFFLINE_OUTPUT_ID } from './compositor.js'; import { OutputRootComponent, OutputShutdownStateStore } from '../rootComponent.js'; +import type { Logger } from 'pino'; type AudioContext = _liveCompositorInternals.AudioContext; type OfflineTimeContext = _liveCompositorInternals.OfflineTimeContext; @@ -16,6 +17,8 @@ type OfflineInputStreamStore = _liveCompositorInternals.OfflineInputStreamSt type CompositorOutputContext = _liveCompositorInternals.CompositorOutputContext; type ChildrenLifetimeContext = _liveCompositorInternals.ChildrenLifetimeContext; +type Timeout = ReturnType; + class OfflineOutput { api: ApiClient; outputId: string; @@ -24,6 +27,8 @@ class OfflineOutput { childrenLifetimeContext: ChildrenLifetimeContext; internalInputStreamStore: OfflineInputStreamStore; outputShutdownStateStore: OutputShutdownStateStore; + logger: Logger; + durationMs?: number; updateTracker?: UpdateTracker; @@ -37,9 +42,11 @@ class OfflineOutput { registerRequest: RegisterOutput, api: ApiClient, store: OfflineInputStreamStore, + logger: Logger, durationMs?: number ) { this.api = api; + this.logger = logger; this.outputId = OFFLINE_OUTPUT_ID; this.outputShutdownStateStore = new OutputShutdownStateStore(); this.durationMs = durationMs; @@ -55,7 +62,8 @@ class OfflineOutput { (timestamp: number) => { store.setCurrentTimestamp(timestamp); this.internalInputStreamStore.setCurrentTimestamp(timestamp); - } + }, + this.logger ); this.childrenLifetimeContext = new _liveCompositorInternals.ChildrenLifetimeContext(() => {}); @@ -70,6 +78,7 @@ class OfflineOutput { rootElement, onUpdate, idPrefix: `${this.outputId}-`, + logger: logger.child({ element: 'react-renderer' }), }); } @@ -86,7 +95,7 @@ class OfflineOutput { } public async scheduleAllUpdates(): Promise { - this.updateTracker = new UpdateTracker(); + this.updateTracker = new UpdateTracker(this.logger); while (this.timeContext.timestampMs() <= (this.durationMs ?? Infinity)) { while (true) { @@ -118,6 +127,7 @@ class OutputContext implements CompositorOutputContext { public readonly audioContext: _liveCompositorInternals.AudioContext; public readonly timeContext: _liveCompositorInternals.TimeContext; public readonly outputId: string; + public readonly logger: Logger; private output: OfflineOutput; constructor( @@ -131,6 +141,7 @@ class OutputContext implements CompositorOutputContext { this.audioContext = output.audioContext; this.timeContext = output.timeContext; this.outputId = outputId; + this.logger = output.logger; } public async registerMp4Input( @@ -205,14 +216,16 @@ const MAX_RENDER_TIMEOUT_MS = 2000; class UpdateTracker { private promise: Promise = new Promise(() => {}); private promiseRes: () => void = () => {}; - private updateTimeout: number = -1; - private renderTimeout: number = -1; + private updateTimeout?: Timeout; + private renderTimeout?: Timeout; + private logger: Logger; - constructor() { + constructor(logger: Logger) { this.promise = new Promise((res, _rej) => { this.promiseRes = res; }); this.onUpdate(); + this.logger = logger; } public onUpdate() { @@ -228,7 +241,7 @@ class UpdateTracker { }); clearTimeout(this.renderTimeout); this.renderTimeout = setTimeout(() => { - console.warn( + this.logger.warn( "Render for a specific timestamp took too long, make sure you don't have infinite update loop." ); this.promiseRes(); diff --git a/ts/@live-compositor/core/src/renderer.ts b/ts/@live-compositor/core/src/renderer.ts index 53cc188ca..cc3027888 100644 --- a/ts/@live-compositor/core/src/renderer.ts +++ b/ts/@live-compositor/core/src/renderer.ts @@ -4,6 +4,7 @@ import { DefaultEventPriority, LegacyRoot } from 'react-reconciler/constants.js' import type { Api } from './api.js'; import type { _liveCompositorInternals } from 'live-compositor'; import type React from 'react'; +import type { Logger } from 'pino'; type SceneBuilder

= _liveCompositorInternals.SceneBuilder

; type SceneComponent = _liveCompositorInternals.SceneComponent; @@ -36,6 +37,7 @@ type HostContext = object; type Instance = LiveCompositorHostComponent; type TextInstance = string; type ChildSet = Array; +type Timeout = ReturnType; const HostConfig: Reconciler.HostConfig< Type, @@ -49,7 +51,7 @@ const HostConfig: Reconciler.HostConfig< HostContext, object, // UpdatePayload ChildSet, - number, // TimeoutHandle + Timeout, // TimeoutHandle -1 // NoTimeout > = { getPublicInstance(instance: Instance | TextInstance) { @@ -219,6 +221,7 @@ type RendererOptions = { rootElement: React.ReactElement; onUpdate: () => void; idPrefix: string; + logger: Logger; }; // docs @@ -232,8 +235,10 @@ interface FiberRootNode { class Renderer { public readonly root: FiberRootNode; public readonly onUpdate: () => void; + private logger: Logger; - constructor({ rootElement, onUpdate, idPrefix }: RendererOptions) { + constructor({ rootElement, onUpdate, idPrefix, logger }: RendererOptions) { + this.logger = logger; this.onUpdate = onUpdate; this.root = CompositorRenderer.createContainer( @@ -243,7 +248,7 @@ class Renderer { false, // isStrictMode null, // concurrentUpdatesByDefaultOverride idPrefix, // identifierPrefix - console.error, // onRecoverableError + logger.error, // onRecoverableError null // transitionCallbacks ); @@ -255,13 +260,14 @@ class Renderer { // on `pendingChildren`. I'm not sure it is always populated, so there is a fallback to // `root.current`. - const rootComponent = this.root.pendingChildren[0] ?? rootHostComponent(this.root.current); + const rootComponent = + this.root.pendingChildren[0] ?? rootHostComponent(this.root.current, this.logger); return rootComponent.scene(); } } -function rootHostComponent(root: any): LiveCompositorHostComponent { - console.error('No pendingChildren found, this might be an error.'); +function rootHostComponent(root: any, logger: Logger): LiveCompositorHostComponent { + logger.error('No pendingChildren found, this might be an error.'); let current = root; while (current) { if (current?.stateNode instanceof LiveCompositorHostComponent) { diff --git a/ts/@live-compositor/core/src/utils.ts b/ts/@live-compositor/core/src/utils.ts index e1fc83b9a..08916cd3b 100644 --- a/ts/@live-compositor/core/src/utils.ts +++ b/ts/@live-compositor/core/src/utils.ts @@ -1,4 +1,11 @@ -export function throttle(fn: () => Promise, timeoutMs: number): () => void { +import type { Logger } from 'pino'; + +type ThrottleOptions = { + logger: Logger; + timeoutMs: number; +}; + +export function throttle(fn: () => Promise, opts: ThrottleOptions): () => void { let shouldCall: boolean = false; let running: boolean = false; @@ -10,10 +17,10 @@ export function throttle(fn: () => Promise, timeoutMs: number): () => void try { await fn(); } catch (error) { - console.log(error); + opts.logger.error(error); } - const timeoutLeft = start + timeoutMs - Date.now(); + const timeoutLeft = start + opts.timeoutMs - Date.now(); if (timeoutLeft > 0) { await sleep(timeoutLeft); } diff --git a/ts/@live-compositor/node/package.json b/ts/@live-compositor/node/package.json index 5339dfbb8..0815b1b2f 100644 --- a/ts/@live-compositor/node/package.json +++ b/ts/@live-compositor/node/package.json @@ -27,6 +27,8 @@ "@live-compositor/core": "workspace:^0.1.0", "fs-extra": "^11.2.0", "node-fetch": "^2.6.7", + "pino": "^9.5.0", + "pino-pretty": "^13.0.0", "tar": "^7.4.3", "uuid": "^10.0.0", "ws": "^8.18.0" diff --git a/ts/@live-compositor/node/src/index.ts b/ts/@live-compositor/node/src/index.ts index 38eb2f064..0113d543b 100644 --- a/ts/@live-compositor/node/src/index.ts +++ b/ts/@live-compositor/node/src/index.ts @@ -5,17 +5,18 @@ import { } from '@live-compositor/core'; import LocallySpawnedInstance from './manager/locallySpawnedInstance'; import ExistingInstance from './manager/existingInstance'; +import { createLogger } from './logger'; export { LocallySpawnedInstance, ExistingInstance }; export default class LiveCompositor extends CoreLiveCompositor { constructor(manager?: CompositorManager) { - super(manager ?? LocallySpawnedInstance.defaultManager()); + super(manager ?? LocallySpawnedInstance.defaultManager(), createLogger()); } } export class OfflineCompositor extends CoreOfflineCompositor { constructor(manager?: CompositorManager) { - super(manager ?? LocallySpawnedInstance.defaultManager()); + super(manager ?? LocallySpawnedInstance.defaultManager(), createLogger()); } } diff --git a/ts/@live-compositor/node/src/logger.ts b/ts/@live-compositor/node/src/logger.ts new file mode 100644 index 000000000..4a8591aef --- /dev/null +++ b/ts/@live-compositor/node/src/logger.ts @@ -0,0 +1,71 @@ +import { LoggerLevel } from '@live-compositor/core'; +import { pino, type Logger } from 'pino'; + +const PRETTY_TRANSPORT = { + target: 'pino-pretty', + options: { + colorize: true, + }, +}; + +function getLoggerLevel(): LoggerLevel { + const logLevel = + process.env.LIVE_COMPOSITOR_LOGGER_LEVEL ?? (process.env.DEBUG ? 'debug' : undefined); + if (Object.values(LoggerLevel).includes(logLevel as LoggerLevel)) { + return logLevel as LoggerLevel; + } else { + return LoggerLevel.WARN; + } +} + +type LoggerFormat = 'json' | 'pretty' | 'compact'; + +function getLoggerFormat(): LoggerFormat { + const env = process.env.LIVE_COMPOSITOR_LOGGER_FORMAT; + return ['json', 'compact', 'pretty'].includes(process.env.LIVE_COMPOSITOR_LOGGER_FORMAT ?? '') + ? (env as LoggerFormat) + : 'json'; +} + +export function createLogger(): Logger { + return pino({ + level: getLoggerLevel(), + transport: ['pretty', 'compact'].includes(getLoggerFormat()) ? PRETTY_TRANSPORT : undefined, + }); +} + +export function compositorInstanceLoggerOptions(): { + format: string; + level: string; +} { + const loggerLevel = getLoggerLevel(); + const format = getLoggerFormat(); + + if ([LoggerLevel.WARN, LoggerLevel.ERROR].includes(loggerLevel)) { + return { + level: loggerLevel, + format, + }; + } else if (loggerLevel === LoggerLevel.INFO) { + return { + level: 'warn', + format, + }; + } else if (loggerLevel === LoggerLevel.DEBUG) { + return { + // hide scene update request with "compositor_pipeline::pipeline=warn" + level: 'info,wgpu_hal=warn,wgpu_core=warn,compositor_pipeline::pipeline=warn', + format, + }; + } else if (loggerLevel === LoggerLevel.TRACE) { + return { + level: 'debug,wgpu_hal=warn,wgpu_core=warn,naga=warn,live_compositor::log_request_body=trace', + format, + }; + } else { + return { + level: 'error', + format: 'json', + }; + } +} diff --git a/ts/@live-compositor/node/src/manager/existingInstance.ts b/ts/@live-compositor/node/src/manager/existingInstance.ts index 97aa3fb50..8efd4cdbc 100644 --- a/ts/@live-compositor/node/src/manager/existingInstance.ts +++ b/ts/@live-compositor/node/src/manager/existingInstance.ts @@ -23,12 +23,11 @@ class ExistingInstance implements CompositorManager { this.port = opts.port; this.ip = opts.ip; this.protocol = opts.protocol ?? 'http'; - const wsProtocol = this.protocol === 'https' ? 'wss' : 'ws'; this.wsConnection = new WebSocketConnection(`${wsProtocol}://${this.ip}:${this.port}/ws`); } - public async setupInstance(_opts: SetupInstanceOptions): Promise { + public async setupInstance(opts: SetupInstanceOptions): Promise { // TODO: verify if options match // https://github.com/software-mansion/live-compositor/issues/877 await retry(async () => { @@ -38,7 +37,7 @@ class ExistingInstance implements CompositorManager { route: '/status', }); }, 10); - await this.wsConnection.connect(); + await this.wsConnection.connect(opts.logger); } public async sendRequest(request: ApiRequest): Promise { @@ -46,7 +45,7 @@ class ExistingInstance implements CompositorManager { } public registerEventListener(cb: (event: object) => void): void { - this.wsConnection.registerEventListener(cb); + this.wsConnection?.registerEventListener(cb); } } diff --git a/ts/@live-compositor/node/src/manager/locallySpawnedInstance.ts b/ts/@live-compositor/node/src/manager/locallySpawnedInstance.ts index 0ce7d9ee6..22bf1485b 100644 --- a/ts/@live-compositor/node/src/manager/locallySpawnedInstance.ts +++ b/ts/@live-compositor/node/src/manager/locallySpawnedInstance.ts @@ -10,6 +10,7 @@ import { download, sendRequest } from '../fetch'; import { retry, sleep } from '../utils'; import { spawn } from '../spawn'; import { WebSocketConnection } from '../ws'; +import { compositorInstanceLoggerOptions } from '../logger'; const VERSION = `v0.3.0`; @@ -51,22 +52,28 @@ class LocallySpawnedInstance implements CompositorManager { public async setupInstance(opts: SetupInstanceOptions): Promise { const executablePath = this.executablePath ?? (await prepareExecutable(this.enableWebRenderer)); - spawn(executablePath, [], { - env: { - LIVE_COMPOSITOR_DOWNLOAD_DIR: path.join(this.workingdir, 'download'), - LIVE_COMPOSITOR_API_PORT: this.port.toString(), - LIVE_COMPOSITOR_WEB_RENDERER_ENABLE: this.enableWebRenderer ? 'true' : 'false', - // silence scene updates logging - LIVE_COMPOSITOR_LOGGER_FORMAT: 'compact', - LIVE_COMPOSITOR_LOGGER_LEVEL: - 'info,wgpu_hal=warn,wgpu_core=warn,compositor_pipeline::pipeline=warn,live_compositor::log_request_body=debug', - LIVE_COMPOSITOR_AHEAD_OF_TIME_PROCESSING_ENABLE: opts.aheadOfTimeProcessing - ? 'true' - : 'false', - ...process.env, - }, - }).catch(err => { - console.error('LiveCompositor instance failed', err); + const { level, format } = compositorInstanceLoggerOptions(); + + const env = { + LIVE_COMPOSITOR_DOWNLOAD_DIR: path.join(this.workingdir, 'download'), + LIVE_COMPOSITOR_API_PORT: this.port.toString(), + LIVE_COMPOSITOR_WEB_RENDERER_ENABLE: this.enableWebRenderer ? 'true' : 'false', + LIVE_COMPOSITOR_AHEAD_OF_TIME_PROCESSING_ENABLE: opts.aheadOfTimeProcessing + ? 'true' + : 'false', + ...process.env, + LIVE_COMPOSITOR_LOGGER_FORMAT: format, + LIVE_COMPOSITOR_LOGGER_LEVEL: level, + }; + spawn(executablePath, [], { env, stdio: 'inherit' }).catch(err => { + opts.logger.error({ err }, 'LiveCompositor instance failed'); + // TODO: parse structured logging from compositor and send them to this logger + if (err.stderr) { + console.error(err.stderr); + } + if (err.stdout) { + console.error(err.stdout); + } }); await retry(async () => { @@ -77,7 +84,7 @@ class LocallySpawnedInstance implements CompositorManager { }); }, 10); - await this.wsConnection.connect(); + await this.wsConnection.connect(opts.logger); } public async sendRequest(request: ApiRequest): Promise { @@ -85,7 +92,7 @@ class LocallySpawnedInstance implements CompositorManager { } public registerEventListener(cb: (event: object) => void): void { - this.wsConnection.registerEventListener(cb); + this.wsConnection?.registerEventListener(cb); } } diff --git a/ts/@live-compositor/node/src/spawn.ts b/ts/@live-compositor/node/src/spawn.ts index ecc138a2d..20b2f8182 100644 --- a/ts/@live-compositor/node/src/spawn.ts +++ b/ts/@live-compositor/node/src/spawn.ts @@ -10,6 +10,8 @@ export function spawn(command: string, args: string[], options: SpawnOptions): S stdio: 'inherit', ...options, }); + let stdout: string[] = []; + let stderr: string[] = []; const promise = new Promise((res, rej) => { child.on('error', err => { rej(err); @@ -18,9 +20,26 @@ export function spawn(command: string, args: string[], options: SpawnOptions): S if (code === 0) { res(); } else { - rej(new Error(`Command "${command} ${args.join(' ')}" failed with exit code ${code}.`)); + let err = new Error( + `Command "${command} ${args.join(' ')}" failed with exit code ${code}.` + ); + (err as any).stdout = stdout.length > 0 ? stdout.join('\n') : undefined; + (err as any).stderr = stderr.length > 0 ? stderr.join('\n') : undefined; + rej(err); } }); + child.stdout?.on('data', chunk => { + if (stdout.length >= 100) { + stdout.shift(); + } + stdout.push(chunk.toString()); + }); + child.stderr?.on('data', chunk => { + if (stderr.length >= 100) { + stderr.shift(); + } + stderr.push(chunk.toString()); + }); }) as SpawnPromise; promise.child = child; return promise; diff --git a/ts/@live-compositor/node/src/ws.ts b/ts/@live-compositor/node/src/ws.ts index 9a00c3ebc..1837c54ec 100644 --- a/ts/@live-compositor/node/src/ws.ts +++ b/ts/@live-compositor/node/src/ws.ts @@ -1,3 +1,4 @@ +import { type Logger } from 'pino'; import WebSocket from 'ws'; export class WebSocketConnection { @@ -11,14 +12,14 @@ export class WebSocketConnection { this.listeners = new Set(); } - public async connect(): Promise { + public async connect(logger: Logger): Promise { const ws = new WebSocket(this.url); let connected = false; await new Promise((res, rej) => { ws.on('error', (err: any) => { if (connected) { - console.log('error', err); + logger.error(err, 'WebSocket error'); } else { rej(err); } @@ -30,7 +31,7 @@ export class WebSocketConnection { }); ws.on('message', data => { - const event = parseEvent(data); + const event = parseEvent(data, logger); if (event) { for (const listener of this.listeners) { listener(event); @@ -50,11 +51,11 @@ export class WebSocketConnection { } } -function parseEvent(data: WebSocket.RawData): unknown { +function parseEvent(data: WebSocket.RawData, logger: Logger): unknown { try { return JSON.parse(data.toString()); - } catch { - console.error(`Invalid event received ${data}`); + } catch (err: any) { + logger.warn(err, `Invalid event received ${data}`); return null; } } diff --git a/ts/@live-compositor/web-wasm/package.json b/ts/@live-compositor/web-wasm/package.json index 04b2a5f0c..3cd0f3e32 100644 --- a/ts/@live-compositor/web-wasm/package.json +++ b/ts/@live-compositor/web-wasm/package.json @@ -22,7 +22,8 @@ "@live-compositor/core": "workspace:0.1.0", "live-compositor": "workspace:^0.1.0", "mp4box": "^0.5.2", - "path-parser": "^6.1.0" + "path-parser": "^6.1.0", + "pino": "^9.5.0" }, "devDependencies": { "@types/react": "^18.3.3" diff --git a/ts/@live-compositor/web-wasm/src/compositor.ts b/ts/@live-compositor/web-wasm/src/compositor.ts index 153baf2e1..b4faba7b2 100644 --- a/ts/@live-compositor/web-wasm/src/compositor.ts +++ b/ts/@live-compositor/web-wasm/src/compositor.ts @@ -7,6 +7,8 @@ import type { RegisterInput } from './input/registerInput'; import { intoRegisterInput } from './input/registerInput'; import type { RegisterImage } from './renderers'; import type { ReactElement } from 'react'; +import type { Logger } from 'pino'; +import { pino } from 'pino'; export type LiveCompositorOptions = { framerate?: Framerate; @@ -23,6 +25,7 @@ export default class LiveCompositor { private instance?: WasmInstance; private renderer?: Renderer; private options: LiveCompositorOptions; + private logger: Logger = pino({ level: 'warn' }); public constructor(options: LiveCompositorOptions) { this.options = options; @@ -40,7 +43,7 @@ export default class LiveCompositor { renderer: this.renderer!, framerate: this.options.framerate ?? { num: 30, den: 1 }, }); - this.coreCompositor = new CoreLiveCompositor(this.instance!); + this.coreCompositor = new CoreLiveCompositor(this.instance!, this.logger); await this.coreCompositor!.init(); } diff --git a/ts/live-compositor/src/context/index.ts b/ts/live-compositor/src/context/index.ts index 9f632c95a..e8d3a52c8 100644 --- a/ts/live-compositor/src/context/index.ts +++ b/ts/live-compositor/src/context/index.ts @@ -4,6 +4,7 @@ import type { TimeContext } from './timeContext.js'; import { LiveTimeContext } from './timeContext.js'; import { LiveInputStreamStore, type InputStreamStore } from './inputStreamStore.js'; import type { RegisterMp4Input } from '../types/registerInput.js'; +import type { Logger } from '../types/logger.js'; export type CompositorOutputContext = { // global store for input stream state @@ -17,6 +18,8 @@ export type CompositorOutputContext = { outputId: string; + logger: Logger; + // TODO: aggregate that into some context object when we add more methods like this. registerMp4Input: ( inputId: number, @@ -26,12 +29,21 @@ export type CompositorOutputContext = { unregisterMp4Input: (inputId: number) => Promise; }; +const noopLogger = { + error: () => null, + warn: () => null, + info: () => null, + debug: () => null, + trace: () => null, +} as const; + export const LiveCompositorContext = createContext({ - globalInputStreamStore: new LiveInputStreamStore(), - internalInputStreamStore: new LiveInputStreamStore(), + globalInputStreamStore: new LiveInputStreamStore(noopLogger), + internalInputStreamStore: new LiveInputStreamStore(noopLogger), audioContext: new AudioContext(() => {}), timeContext: new LiveTimeContext(), outputId: '', registerMp4Input: async () => ({}), unregisterMp4Input: async () => {}, + logger: noopLogger, }); diff --git a/ts/live-compositor/src/context/inputStreamStore.ts b/ts/live-compositor/src/context/inputStreamStore.ts index 8430d2055..a9535c6b8 100644 --- a/ts/live-compositor/src/context/inputStreamStore.ts +++ b/ts/live-compositor/src/context/inputStreamStore.ts @@ -1,5 +1,6 @@ import { useContext, useState } from 'react'; import { LiveCompositorContext } from './index.js'; +import type { Logger } from '../types/logger.js'; let nextStreamNumber = 1; @@ -43,6 +44,11 @@ export class LiveInputStreamStore { private context: Record> = {}; private onChangeCallbacks: Set<() => void> = new Set(); private eventQueue?: UpdateAction[]; + private logger: Logger; + + constructor(logger: Logger) { + this.logger = logger; + } /** * Apply update immediately if there are no `runBlocking` calls in progress. @@ -87,7 +93,7 @@ export class LiveInputStreamStore { private addInput(input: InputStreamInfo) { if (this.context[String(input.inputId)]) { - console.warn(`Adding input ${input.inputId}. Input already exists.`); + this.logger.warn(`Adding input ${input.inputId}. Input already exists.`); } this.context = { ...this.context, [String(input.inputId)]: input }; this.signalUpdate(); @@ -96,7 +102,7 @@ export class LiveInputStreamStore { private updateInput(update: InputStreamInfo) { const oldInput = this.context[String(update.inputId)]; if (!oldInput) { - console.warn(`Updating input ${update.inputId}. Input does not exist.`); + this.logger.warn(`Updating input ${update.inputId}. Input does not exist.`); return; } this.context = { diff --git a/ts/live-compositor/src/context/timeContext.ts b/ts/live-compositor/src/context/timeContext.ts index 1fb893a3a..cb7413b03 100644 --- a/ts/live-compositor/src/context/timeContext.ts +++ b/ts/live-compositor/src/context/timeContext.ts @@ -1,3 +1,5 @@ +import type { Logger } from '../internal.js'; + export interface BlockingTask { done(): void; } @@ -21,14 +23,16 @@ export class OfflineTimeContext { private onChange: () => void; private currentTimestamp: number = 0; private onChangeCallbacks: Set<() => void> = new Set(); + private logger: Logger; - constructor(onChange: () => void, onTimeChange: (timestam: number) => void) { + constructor(onChange: () => void, onTimeChange: (timestamp: number) => void, logger: Logger) { this.onChange = onChange; this.tasks = []; this.timestamps = []; this.onChangeCallbacks.add(() => { onTimeChange(this.currentTimestamp); }); + this.logger = logger; } public timestampMs(): number { @@ -40,9 +44,14 @@ export class OfflineTimeContext { } public newBlockingTask(): BlockingTask { + this.logger.trace('Start new blocking task'); const task: BlockingTask = {} as any; task.done = () => { + const originalLength = this.tasks.length; this.tasks = this.tasks.filter(t => t !== task); + if (this.tasks.length < originalLength) { + this.logger.trace('Blocking task finished'); + } if (this.tasks.length === 0) { this.onChange(); } @@ -52,10 +61,12 @@ export class OfflineTimeContext { } public addTimestamp(timestamp: TimestampObject) { + this.logger.trace({ timestampMs: timestamp.timestamp }, 'Add new timestamp to render.'); this.timestamps.push(timestamp); } public removeTimestamp(timestamp: TimestampObject) { + this.logger.trace({ timestampMs: timestamp.timestamp }, 'Remove timestamp to render.'); this.timestamps = this.timestamps.filter(t => timestamp !== t); } @@ -65,10 +76,12 @@ export class OfflineTimeContext { value.timestamp < acc.timestamp && value.timestamp > this.currentTimestamp ? value : acc, { timestamp: Infinity } ); + this.logger.debug({ timestampMs: next.timestamp }, 'Rendering new timestamp'); this.currentTimestamp = next.timestamp; for (const cb of this.onChangeCallbacks) { cb(); } + this.logger.trace({ timestampMs: next.timestamp }, 'Callbacks for timestamp finished'); } // callback for useSyncExternalStore diff --git a/ts/live-compositor/src/internal.ts b/ts/live-compositor/src/internal.ts index 54402154f..07beb03a0 100644 --- a/ts/live-compositor/src/internal.ts +++ b/ts/live-compositor/src/internal.ts @@ -16,3 +16,4 @@ export { ChildrenLifetimeContext, ChildrenLifetimeContextType, } from './context/childrenLifetimeContext.js'; +export { Logger } from './types/logger.js'; diff --git a/ts/live-compositor/src/types/logger.ts b/ts/live-compositor/src/types/logger.ts new file mode 100644 index 000000000..7c53e0972 --- /dev/null +++ b/ts/live-compositor/src/types/logger.ts @@ -0,0 +1,7 @@ +export interface Logger { + error(...params: any[]): void; + warn(...params: any[]): void; + info(...params: any[]): void; + debug(...params: any[]): void; + trace(...params: any[]): void; +} diff --git a/ts/pnpm-lock.yaml b/ts/pnpm-lock.yaml index 1d5803a89..d50df79f6 100644 --- a/ts/pnpm-lock.yaml +++ b/ts/pnpm-lock.yaml @@ -84,6 +84,9 @@ importers: live-compositor: specifier: workspace:^0.1.0 version: link:../../live-compositor + pino: + specifier: ^9.5.0 + version: 9.5.0 react: specifier: '*' version: 18.3.1 @@ -109,6 +112,12 @@ importers: node-fetch: specifier: ^2.6.7 version: 2.7.0 + pino: + specifier: ^9.5.0 + version: 9.5.0 + pino-pretty: + specifier: ^13.0.0 + version: 13.0.0 tar: specifier: ^7.4.3 version: 7.4.3 @@ -155,6 +164,9 @@ importers: path-parser: specifier: ^6.1.0 version: 6.1.0 + pino: + specifier: ^9.5.0 + version: 9.5.0 devDependencies: '@types/react': specifier: ^18.3.3 @@ -1027,6 +1039,10 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -1109,6 +1125,9 @@ packages: colorette@1.4.0: resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==} + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -1161,6 +1180,9 @@ packages: resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} engines: {node: '>= 0.4'} + dateformat@4.6.3: + resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} + debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -1244,6 +1266,9 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} + end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + enhanced-resolve@5.17.1: resolution: {integrity: sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==} engines: {node: '>=10.13.0'} @@ -1422,6 +1447,9 @@ packages: resolution: {integrity: sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==} engines: {node: '>= 0.10.0'} + fast-copy@3.0.2: + resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1438,6 +1466,13 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-redact@3.5.0: + resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} + engines: {node: '>=6'} + + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} @@ -1625,6 +1660,9 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + help-me@5.0.0: + resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} + http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} @@ -1758,6 +1796,10 @@ packages: resolution: {integrity: sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==} engines: {node: 20 || >=22} + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1977,6 +2019,10 @@ packages: resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==} engines: {node: '>= 0.4'} + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -2051,6 +2097,20 @@ packages: resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} engines: {node: '>=12'} + pino-abstract-transport@2.0.0: + resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} + + pino-pretty@13.0.0: + resolution: {integrity: sha512-cQBBIVG3YajgoUjo1FdKVRX6t9XPxwB9lcNJVD5GCnNM4Y6T12YYx8c6zEejxQsU0wrg9TwmDulcE9LR7qcJqA==} + hasBin: true + + pino-std-serializers@7.0.0: + resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==} + + pino@9.5.0: + resolution: {integrity: sha512-xSEmD4pLnV54t0NOUN16yCl7RIB1c5UUOse5HSyEXtBp+FgFQyPeDutc+Q2ZO7/22vImV7VfEjH/1zV2QuqvYw==} + hasBin: true + possible-typed-array-names@1.0.0: resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} engines: {node: '>= 0.4'} @@ -2072,6 +2132,9 @@ packages: engines: {node: '>=14'} hasBin: true + process-warning@4.0.0: + resolution: {integrity: sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw==} + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -2080,6 +2143,9 @@ packages: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} + pump@3.0.2: + resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -2091,6 +2157,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} @@ -2122,6 +2191,10 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + regexp.prototype.flags@1.5.3: resolution: {integrity: sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==} engines: {node: '>= 0.4'} @@ -2192,6 +2265,10 @@ packages: resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} engines: {node: '>= 0.4'} + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -2201,6 +2278,9 @@ packages: search-params@3.0.0: resolution: {integrity: sha512-8CYNl/bjkEhXWbDTU/K7c2jQtrnqEffIPyOLMqygW/7/b+ym8UtQumcAZjOfMLjZKR6AxK5tOr9fChbQZCzPqg==} + secure-json-parse@2.7.0: + resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -2255,10 +2335,17 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} + sonic-boom@4.2.0: + resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} @@ -2326,6 +2413,9 @@ packages: resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} engines: {node: '>=18'} + thread-stream@3.1.0: + resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} + tinyglobby@0.2.10: resolution: {integrity: sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==} engines: {node: '>=12.0.0'} @@ -3290,6 +3380,8 @@ snapshots: asynckit@0.4.0: {} + atomic-sleep@1.0.0: {} + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.0.0 @@ -3398,6 +3490,8 @@ snapshots: colorette@1.4.0: {} + colorette@2.0.20: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -3454,6 +3548,8 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.1 + dateformat@4.6.3: {} + debug@2.6.9: dependencies: ms: 2.0.0 @@ -3510,6 +3606,10 @@ snapshots: encodeurl@2.0.0: {} + end-of-stream@1.4.4: + dependencies: + once: 1.4.0 + enhanced-resolve@5.17.1: dependencies: graceful-fs: 4.2.11 @@ -3816,6 +3916,8 @@ snapshots: transitivePeerDependencies: - supports-color + fast-copy@3.0.2: {} + fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -3832,6 +3934,10 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-redact@3.5.0: {} + + fast-safe-stringify@2.1.1: {} + fastq@1.17.1: dependencies: reusify: 1.0.4 @@ -4032,6 +4138,8 @@ snapshots: dependencies: function-bind: 1.1.2 + help-me@5.0.0: {} + http-errors@2.0.0: dependencies: depd: 2.0.0 @@ -4161,6 +4269,8 @@ snapshots: dependencies: '@isaacs/cliui': 8.0.2 + joycon@3.1.1: {} + js-tokens@4.0.0: {} js-yaml@4.1.0: @@ -4347,6 +4457,8 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.0.0 + on-exit-leak-free@2.1.2: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -4413,6 +4525,42 @@ snapshots: picomatch@4.0.2: {} + pino-abstract-transport@2.0.0: + dependencies: + split2: 4.2.0 + + pino-pretty@13.0.0: + dependencies: + colorette: 2.0.20 + dateformat: 4.6.3 + fast-copy: 3.0.2 + fast-safe-stringify: 2.1.1 + help-me: 5.0.0 + joycon: 3.1.1 + minimist: 1.2.8 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 2.0.0 + pump: 3.0.2 + secure-json-parse: 2.7.0 + sonic-boom: 4.2.0 + strip-json-comments: 3.1.1 + + pino-std-serializers@7.0.0: {} + + pino@9.5.0: + dependencies: + atomic-sleep: 1.0.0 + fast-redact: 3.5.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 2.0.0 + pino-std-serializers: 7.0.0 + process-warning: 4.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.0 + thread-stream: 3.1.0 + possible-typed-array-names@1.0.0: {} postcss@8.4.49: @@ -4429,6 +4577,8 @@ snapshots: prettier@3.3.3: {} + process-warning@4.0.0: {} + prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -4439,6 +4589,11 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 + pump@3.0.2: + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + punycode@2.3.1: {} qs@6.13.0: @@ -4447,6 +4602,8 @@ snapshots: queue-microtask@1.2.3: {} + quick-format-unescaped@4.0.4: {} + range-parser@1.2.1: {} raw-body@2.5.2: @@ -4478,6 +4635,8 @@ snapshots: dependencies: picomatch: 2.3.1 + real-require@0.2.0: {} + regexp.prototype.flags@1.5.3: dependencies: call-bind: 1.0.7 @@ -4575,6 +4734,8 @@ snapshots: es-errors: 1.3.0 is-regex: 1.1.4 + safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} scheduler@0.23.2: @@ -4583,6 +4744,8 @@ snapshots: search-params@3.0.0: {} + secure-json-parse@2.7.0: {} + semver@6.3.1: {} semver@7.6.3: {} @@ -4653,8 +4816,14 @@ snapshots: slash@3.0.0: {} + sonic-boom@4.2.0: + dependencies: + atomic-sleep: 1.0.0 + source-map-js@1.2.1: {} + split2@4.2.0: {} + statuses@2.0.1: {} string-width@4.2.3: @@ -4735,6 +4904,10 @@ snapshots: mkdirp: 3.0.1 yallist: 5.0.0 + thread-stream@3.1.0: + dependencies: + real-require: 0.2.0 + tinyglobby@0.2.10: dependencies: fdir: 6.4.2(picomatch@4.0.2)