diff --git a/.gitignore b/.gitignore index 5f373f3..28ff0e5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ dist .DS_Store packages/sdk/tests/e2e-output .env +package-lock.json diff --git a/examples/react-vite/package.json b/examples/react-vite/package.json index 77f4f52..273fbcc 100644 --- a/examples/react-vite/package.json +++ b/examples/react-vite/package.json @@ -9,6 +9,7 @@ "preview": "vite preview" }, "dependencies": { + "amazon-ivs-web-broadcast": "^1.14.0", "@decartai/sdk": "workspace:*", "react": "^19.0.0", "react-dom": "^19.0.0" diff --git a/examples/react-vite/src/App.tsx b/examples/react-vite/src/App.tsx index cc620a3..3083391 100644 --- a/examples/react-vite/src/App.tsx +++ b/examples/react-vite/src/App.tsx @@ -3,6 +3,7 @@ import { VideoStream } from "./components/VideoStream"; function App() { const [prompt, setPrompt] = useState("anime style, vibrant colors"); + const [transport, setTransport] = useState<"webrtc" | "ivs">("webrtc"); return (
@@ -20,7 +21,21 @@ function App() {
- +
+ +
+ + ); } diff --git a/examples/react-vite/src/components/VideoStream.tsx b/examples/react-vite/src/components/VideoStream.tsx index b9dbc75..7654d93 100644 --- a/examples/react-vite/src/components/VideoStream.tsx +++ b/examples/react-vite/src/components/VideoStream.tsx @@ -3,9 +3,10 @@ import { useEffect, useRef, useState } from "react"; interface VideoStreamProps { prompt: string; + transport?: "webrtc" | "ivs"; } -export function VideoStream({ prompt }: VideoStreamProps) { +export function VideoStream({ prompt, transport = "webrtc" }: VideoStreamProps) { const inputRef = useRef(null); const outputRef = useRef(null); const realtimeClientRef = useRef(null); @@ -33,8 +34,6 @@ export function VideoStream({ prompt }: VideoStreamProps) { inputRef.current.srcObject = stream; } - setStatus("connecting..."); - const apiKey = import.meta.env.VITE_DECART_API_KEY; if (!apiKey) { throw new Error("DECART_API_KEY is not set"); @@ -44,8 +43,11 @@ export function VideoStream({ prompt }: VideoStreamProps) { apiKey, }); + setStatus(`connecting via ${transport}...`); + const realtimeClient = await client.realtime.connect(stream, { model, + transport, onRemoteStream: (transformedStream: MediaStream) => { if (outputRef.current) { outputRef.current.srcObject = transformedStream; diff --git a/packages/sdk/index.html b/packages/sdk/index.html index dcc9b62..21d05c1 100644 --- a/packages/sdk/index.html +++ b/packages/sdk/index.html @@ -241,6 +241,13 @@

Configuration

+
+ + +
@@ -377,6 +384,7 @@

Console Logs

const elements = { apiKey: document.getElementById('api-key'), modelSelect: document.getElementById('model-select'), + transportSelect: document.getElementById('transport-select'), realtimeBaseUrl: document.getElementById('realtime-base-url'), initialPrompt: document.getElementById('initial-prompt'), startCamera: document.getElementById('start-camera'), @@ -443,6 +451,27 @@

Console Logs

elements.statusText.textContent = status.charAt(0).toUpperCase() + status.slice(1); } + // Transport selection handler — load IVS SDK from CDN when IVS is selected + let ivsScriptLoaded = false; + elements.transportSelect.addEventListener('change', (e) => { + const transport = e.target.value; + addLog(`Selected transport: ${transport}`, 'info'); + + if (transport === 'ivs' && !ivsScriptLoaded) { + addLog('Loading IVS Web Broadcast SDK from CDN...', 'info'); + const script = document.createElement('script'); + script.src = 'https://web-broadcast.live-video.net/1.14.0/amazon-ivs-web-broadcast.js'; + script.onload = () => { + ivsScriptLoaded = true; + addLog('IVS Web Broadcast SDK loaded', 'success'); + }; + script.onerror = () => { + addLog('Failed to load IVS SDK — IVS transport will not work', 'error'); + }; + document.head.appendChild(script); + } + }); + // Model selection handler elements.modelSelect.addEventListener('change', (e) => { const selectedModel = e.target.value; @@ -527,8 +556,12 @@

Console Logs

initialImage = await initialImageResponse.blob(); } + const selectedTransport = elements.transportSelect.value; + addLog(`Connecting with transport: ${selectedTransport}`, 'info'); + decartRealtime = await decartClient.realtime.connect(localStream, { model, + transport: selectedTransport, onRemoteStream: (stream) => { addLog('Received remote stream from Decart', 'success'); elements.remoteVideo.srcObject = stream; diff --git a/packages/sdk/package.json b/packages/sdk/package.json index dcdc03f..07f0181 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -59,5 +59,13 @@ "mitt": "^3.0.1", "p-retry": "^6.2.1", "zod": "^4.0.17" + }, + "peerDependencies": { + "amazon-ivs-web-broadcast": ">=1.14.0" + }, + "peerDependenciesMeta": { + "amazon-ivs-web-broadcast": { + "optional": true + } } } diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index caddfa2..918acc9 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -24,6 +24,7 @@ export type { RealTimeClientConnectOptions, RealTimeClientInitialState, } from "./realtime/client"; +export type { CompositeLatencyEstimate } from "./realtime/composite-latency"; export type { ConnectionPhase, DiagnosticEvent, @@ -39,6 +40,13 @@ export type { VideoStallEvent, } from "./realtime/diagnostics"; export type { SetInput } from "./realtime/methods"; +export type { + PixelLatencyEvent, + PixelLatencyMeasurement, + PixelLatencyReport, + PixelLatencyStats, + PixelLatencyStatus, +} from "./realtime/pixel-latency"; export type { RealTimeSubscribeClient, SubscribeEvents, diff --git a/packages/sdk/src/realtime/client.ts b/packages/sdk/src/realtime/client.ts index f1869b0..a4938e4 100644 --- a/packages/sdk/src/realtime/client.ts +++ b/packages/sdk/src/realtime/client.ts @@ -4,9 +4,14 @@ import { modelStateSchema } from "../shared/types"; import { classifyWebrtcError, type DecartSDKError } from "../utils/errors"; import type { Logger } from "../utils/logger"; import { AudioStreamManager } from "./audio-stream-manager"; -import type { DiagnosticEvent } from "./diagnostics"; +import type { CompositeLatencyEstimate } from "./composite-latency"; +import type { DiagnosticEmitter, DiagnosticEvent } from "./diagnostics"; import { createEventBuffer } from "./event-buffer"; +import { IVSManager } from "./ivs-manager"; +import { IVSStatsCollector } from "./ivs-stats-collector"; +import { LatencyDiagnostics } from "./latency-diagnostics"; import { realtimeMethods, type SetInput } from "./methods"; +import type { PixelLatencyEvent, PixelLatencyMeasurement, PixelLatencyReport } from "./pixel-latency"; import { decodeSubscribeToken, encodeSubscribeToken, @@ -15,6 +20,7 @@ import { type SubscribeOptions, } from "./subscribe-client"; import { type ITelemetryReporter, NullTelemetryReporter, TelemetryReporter } from "./telemetry-reporter"; +import type { RealtimeTransportManager } from "./transport-manager"; import type { ConnectionState, GenerationTickMessage, SessionIdMessage } from "./types"; import { WebRTCManager } from "./webrtc-manager"; import { type WebRTCStats, WebRTCStatsCollector } from "./webrtc-stats"; @@ -93,6 +99,15 @@ const realTimeClientConnectOptionsSchema = z.object({ }), initialState: realTimeClientInitialStateSchema.optional(), customizeOffer: createAsyncFunctionSchema(z.function()).optional(), + transport: z.enum(["webrtc", "ivs"]).optional().default("webrtc"), + latencyTracking: z + .object({ + composite: z.boolean().optional(), + pixelMarker: z.boolean().optional(), + videoElement: z.custom().optional(), + }) + .optional(), + extraQueryParams: z.record(z.string(), z.string()).optional(), }); export type RealTimeClientConnectOptions = Omit, "model"> & { model: ModelDefinition | CustomModelDefinition; @@ -104,6 +119,10 @@ export type Events = { generationTick: { seconds: number }; diagnostic: DiagnosticEvent; stats: WebRTCStats; + compositeLatency: CompositeLatencyEstimate; + pixelLatency: PixelLatencyMeasurement; + pixelLatencyEvent: PixelLatencyEvent; + pixelLatencyReport: PixelLatencyReport; }; export type RealTimeClient = { @@ -151,7 +170,8 @@ export const createRealTimeClient = (opts: RealTimeClientOptions) => { inputStream = stream ?? new MediaStream(); } - let webrtcManager: WebRTCManager | undefined; + const transport = parsedOptions.data.transport; + let transportManager: RealtimeTransportManager | undefined; let telemetryReporter: ITelemetryReporter = new NullTelemetryReporter(); let handleConnectionStateChange: ((state: ConnectionState) => void) | null = null; @@ -171,32 +191,49 @@ export const createRealTimeClient = (opts: RealTimeClientOptions) => { const { emitter: eventEmitter, emitOrBuffer, flush, stop } = createEventBuffer(); - webrtcManager = new WebRTCManager({ - webrtcUrl: `${url}?api_key=${encodeURIComponent(apiKey)}&model=${encodeURIComponent(options.model.name)}`, + const sharedCallbacks = { integration, logger, - onDiagnostic: (name, data) => { + onDiagnostic: ((name: DiagnosticEvent["name"], data: DiagnosticEvent["data"]) => { emitOrBuffer("diagnostic", { name, data } as Events["diagnostic"]); addTelemetryDiagnostic(name, data); - }, + }) as DiagnosticEmitter, onRemoteStream, - onConnectionStateChange: (state) => { + onConnectionStateChange: (state: ConnectionState) => { emitOrBuffer("connectionChange", state); handleConnectionStateChange?.(state); }, - onError: (error) => { - logger.error("WebRTC error", { error: error.message }); + onError: (error: Error) => { + logger.error(`${transport} error`, { error: error.message }); emitOrBuffer("error", classifyWebrtcError(error)); }, - customizeOffer: options.customizeOffer as ((offer: RTCSessionDescriptionInit) => Promise) | undefined, - vp8MinBitrate: 300, - vp8StartBitrate: 600, modelName: options.model.name, initialImage, initialPrompt, - }); + }; - const manager = webrtcManager; + const latencyQs = parsedOptions.data.latencyTracking ? "&latency_diagnostics=true" : ""; + const extraQs = parsedOptions.data.extraQueryParams + ? "&" + new URLSearchParams(parsedOptions.data.extraQueryParams).toString() + : ""; + + if (transport === "ivs") { + const ivsUrlPath = options.model.urlPath.replace(/\/?$/, "-ivs"); + transportManager = new IVSManager({ + ivsUrl: `${baseUrl}${ivsUrlPath}?api_key=${encodeURIComponent(apiKey)}&model=${encodeURIComponent(options.model.name)}${latencyQs}${extraQs}`, + ...sharedCallbacks, + }); + } else { + transportManager = new WebRTCManager({ + webrtcUrl: `${url}?api_key=${encodeURIComponent(apiKey)}&model=${encodeURIComponent(options.model.name)}${latencyQs}${extraQs}`, + ...sharedCallbacks, + customizeOffer: options.customizeOffer as ((offer: RTCSessionDescriptionInit) => Promise) | undefined, + vp8MinBitrate: 300, + vp8StartBitrate: 600, + }); + } + + const manager = transportManager; let sessionId: string | null = null; let subscribeToken: string | null = null; @@ -225,7 +262,7 @@ export const createRealTimeClient = (opts: RealTimeClientOptions) => { }; const sessionIdListener = (msg: SessionIdMessage) => { - subscribeToken = encodeSubscribeToken(msg.session_id, msg.server_ip, msg.server_port); + subscribeToken = encodeSubscribeToken(msg.session_id, msg.server_ip, msg.server_port, transport); sessionId = msg.session_id; // Start telemetry reporter now that we have a session ID @@ -239,6 +276,7 @@ export const createRealTimeClient = (opts: RealTimeClientOptions) => { sessionId: msg.session_id, model: options.model.name, integration, + transport, logger, }); reporter.start(); @@ -258,72 +296,137 @@ export const createRealTimeClient = (opts: RealTimeClientOptions) => { }; manager.getWebsocketMessageEmitter().on("generationTick", tickListener); + // Latency diagnostics (composite + pixel marker) — create before connect + // so the stamper can wrap inputStream before it's published. + let latencyStartTimer: ReturnType | undefined; + let latencyDiag: LatencyDiagnostics | null = null; + if (parsedOptions.data.latencyTracking) { + latencyDiag = new LatencyDiagnostics({ + ...parsedOptions.data.latencyTracking, + sendMessage: (msg) => manager.sendMessage(msg), + onCompositeLatency: (est) => emitOrBuffer("compositeLatency", est), + onPixelLatency: (m) => emitOrBuffer("pixelLatency", m), + onPixelLatencyEvent: (e) => emitOrBuffer("pixelLatencyEvent", e), + onPixelLatencyReport: (r) => emitOrBuffer("pixelLatencyReport", r), + }); + + // Wrap camera stream with canvas stamper for E2E pixel latency + if (parsedOptions.data.latencyTracking.pixelMarker && inputStream) { + inputStream = await latencyDiag.createStamper(inputStream); + } + } + await manager.connect(inputStream); const methods = realtimeMethods(manager, imageToBase64); - let statsCollector: WebRTCStatsCollector | null = null; - let statsCollectorPeerConnection: RTCPeerConnection | null = null; - // Video stall detection state (Twilio pattern: fps < 0.5 = stalled) const STALL_FPS_THRESHOLD = 0.5; let videoStalled = false; let stallStartMs = 0; - const startStatsCollection = (): (() => void) => { - statsCollector?.stop(); - videoStalled = false; - stallStartMs = 0; - statsCollector = new WebRTCStatsCollector(); - const pc = manager.getPeerConnection(); - statsCollectorPeerConnection = pc; - if (pc) { - statsCollector.start(pc, (stats) => { - emitOrBuffer("stats", stats); - telemetryReporter.addStats(stats); - - // Stall detection: check if video fps dropped below threshold - const fps = stats.video?.framesPerSecond ?? 0; - if (!videoStalled && stats.video && fps < STALL_FPS_THRESHOLD) { - videoStalled = true; - stallStartMs = Date.now(); - emitOrBuffer("diagnostic", { name: "videoStall", data: { stalled: true, durationMs: 0 } }); - addTelemetryDiagnostic("videoStall", { stalled: true, durationMs: 0 }, stallStartMs); - } else if (videoStalled && fps >= STALL_FPS_THRESHOLD) { - const durationMs = Date.now() - stallStartMs; - videoStalled = false; - emitOrBuffer("diagnostic", { name: "videoStall", data: { stalled: false, durationMs } }); - addTelemetryDiagnostic("videoStall", { stalled: false, durationMs }); - } - }); + const handleStats = (stats: WebRTCStats): void => { + emitOrBuffer("stats", stats); + telemetryReporter.addStats(stats); + + // Stall detection: check if video fps dropped below threshold + const fps = stats.video?.framesPerSecond ?? 0; + if (!videoStalled && stats.video && fps < STALL_FPS_THRESHOLD) { + videoStalled = true; + stallStartMs = Date.now(); + emitOrBuffer("diagnostic", { name: "videoStall", data: { stalled: true, durationMs: 0 } }); + addTelemetryDiagnostic("videoStall", { stalled: true, durationMs: 0 }, stallStartMs); + } else if (videoStalled && fps >= STALL_FPS_THRESHOLD) { + const durationMs = Date.now() - stallStartMs; + videoStalled = false; + emitOrBuffer("diagnostic", { name: "videoStall", data: { stalled: false, durationMs } }); + addTelemetryDiagnostic("videoStall", { stalled: false, durationMs }); } - return () => { + }; + + let statsCollector: WebRTCStatsCollector | IVSStatsCollector | null = null; + let statsCollectorPeerConnection: RTCPeerConnection | null = null; + + if (transport === "webrtc" && manager instanceof WebRTCManager) { + const webrtcManager = manager; + + const startStatsCollection = (): (() => void) => { statsCollector?.stop(); - statsCollector = null; - statsCollectorPeerConnection = null; + videoStalled = false; + stallStartMs = 0; + const collector = new WebRTCStatsCollector(); + statsCollector = collector; + const pc = webrtcManager.getPeerConnection(); + statsCollectorPeerConnection = pc; + if (pc) { + collector.start(pc, handleStats); + } + return () => { + collector.stop(); + statsCollector = null; + statsCollectorPeerConnection = null; + }; }; - }; - handleConnectionStateChange = (state) => { - if (!opts.telemetryEnabled) { - return; - } + handleConnectionStateChange = (state) => { + if (!opts.telemetryEnabled && !parsedOptions.data.latencyTracking) { + return; + } - if (state !== "connected" && state !== "generating") { - return; - } + if (state !== "connected" && state !== "generating") { + return; + } - const peerConnection = manager.getPeerConnection(); - if (!peerConnection || peerConnection === statsCollectorPeerConnection) { - return; + const peerConnection = webrtcManager.getPeerConnection(); + if (!peerConnection || peerConnection === statsCollectorPeerConnection) { + return; + } + + startStatsCollection(); + }; + + // Auto-start stats when telemetry or latency tracking is enabled + if (opts.telemetryEnabled || parsedOptions.data.latencyTracking) { + startStatsCollection(); } + } else if (transport === "ivs" && manager instanceof IVSManager) { + const ivsManager = manager; - startStatsCollection(); - }; + const startIVSStatsCollection = (): void => { + statsCollector?.stop(); + videoStalled = false; + stallStartMs = 0; + const collector = new IVSStatsCollector(); + statsCollector = collector; + collector.start(ivsManager, handleStats); + }; - // Auto-start stats when telemetry is enabled - if (opts.telemetryEnabled) { - startStatsCollection(); + handleConnectionStateChange = (state) => { + if (!opts.telemetryEnabled && !parsedOptions.data.latencyTracking) { + return; + } + + if (state !== "connected" && state !== "generating") { + return; + } + + // Only start once — IVS doesn't have PC reconnection like WebRTC + if (!statsCollector?.isRunning()) { + startIVSStatsCollection(); + } + }; + + // Auto-start stats when telemetry or latency tracking is enabled + if (opts.telemetryEnabled || parsedOptions.data.latencyTracking) { + startIVSStatsCollection(); + } + } + + // Wire latency diagnostics events and start delayed + if (latencyDiag) { + manager.getWebsocketMessageEmitter().on("latencyReport", (msg) => latencyDiag!.onServerReport(msg)); + eventEmitter.on("stats", (stats) => latencyDiag!.onStats(stats)); + latencyStartTimer = setTimeout(() => latencyDiag?.start(), 1000); } const client: RealTimeClient = { @@ -332,6 +435,8 @@ export const createRealTimeClient = (opts: RealTimeClientOptions) => { isConnected: () => manager.isConnected(), getConnectionState: () => manager.getConnectionState(), disconnect: () => { + clearTimeout(latencyStartTimer); + latencyDiag?.stop(); statsCollector?.stop(); telemetryReporter.stop(); stop(); @@ -368,14 +473,18 @@ export const createRealTimeClient = (opts: RealTimeClientOptions) => { return client; } catch (error) { telemetryReporter.stop(); - webrtcManager?.cleanup(); + transportManager?.cleanup(); audioStreamManager?.cleanup(); throw error; } }; - const subscribe = async (options: SubscribeOptions): Promise => { - const { sid, ip, port } = decodeSubscribeToken(options.token); + const subscribeWebRTC = async ( + options: SubscribeOptions, + sid: string, + ip: string, + port: number, + ): Promise => { const subscribeUrl = `${baseUrl}/subscribe/${encodeURIComponent(sid)}?IP=${encodeURIComponent(ip)}&port=${encodeURIComponent(port)}&api_key=${encodeURIComponent(apiKey)}`; const { emitter: eventEmitter, emitOrBuffer, flush, stop } = createEventBuffer(); @@ -422,6 +531,105 @@ export const createRealTimeClient = (opts: RealTimeClientOptions) => { } }; + const subscribeIVS = async (options: SubscribeOptions, sid: string): Promise => { + const { getIVSBroadcastClient } = await import("./ivs-connection"); + const ivs = await getIVSBroadcastClient(); + + const { emitter: eventEmitter, emitOrBuffer, flush, stop } = createEventBuffer(); + + // Fetch viewer token from bouncer (convert wss:// → https:// for HTTP call) + const httpBaseUrl = baseUrl.replace(/^wss:\/\//, "https://").replace(/^ws:\/\//, "http://"); + const resp = await fetch(`${httpBaseUrl}/v1/subscribe-ivs/${encodeURIComponent(sid)}`, { + headers: { "x-api-key": apiKey }, + }); + if (!resp.ok) { + throw new Error(`Failed to get IVS viewer token: ${resp.status}`); + } + const { subscribe_token, server_publish_participant_id } = (await resp.json()) as { + subscribe_token: string; + server_publish_participant_id: string; + }; + + let connected = false; + let connectionState: ConnectionState = "connecting"; + emitOrBuffer("connectionChange", connectionState); + + // Create subscribe-only IVS stage — filter to server's output stream only + const subscribeStrategy = { + stageStreamsToPublish: () => [] as never[], + shouldPublishParticipant: () => false, + shouldSubscribeToParticipant: (participant: { id: string }) => { + if (server_publish_participant_id && participant.id !== server_publish_participant_id) { + return ivs.SubscribeType.NONE; + } + return ivs.SubscribeType.AUDIO_VIDEO; + }, + }; + + const stage = new ivs.Stage(subscribe_token, subscribeStrategy); + + await new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error("IVS viewer subscribe timeout")), 30_000); + + stage.on(ivs.StageEvents.STAGE_PARTICIPANT_STREAMS_ADDED, (...args: unknown[]) => { + const participant = args[0] as { isLocal: boolean }; + const streams = args[1] as { mediaStreamTrack: MediaStreamTrack }[]; + if (participant.isLocal) return; + + clearTimeout(timer); + const remoteStream = new MediaStream(); + for (const s of streams) { + remoteStream.addTrack(s.mediaStreamTrack); + } + options.onRemoteStream(remoteStream); + connected = true; + connectionState = "connected"; + emitOrBuffer("connectionChange", connectionState); + resolve(); + }); + + stage.on(ivs.StageEvents.STAGE_CONNECTION_STATE_CHANGED, (...args: unknown[]) => { + const state = args[0] as string; + if (state === ivs.ConnectionState.DISCONNECTED.toString()) { + clearTimeout(timer); + connected = false; + connectionState = "disconnected"; + emitOrBuffer("connectionChange", connectionState); + } + }); + + stage.join().catch((err) => { + clearTimeout(timer); + reject(err); + }); + }); + + const client: RealTimeSubscribeClient = { + isConnected: () => connected, + getConnectionState: () => connectionState, + disconnect: () => { + stop(); + stage.leave(); + connected = false; + connectionState = "disconnected"; + }, + on: eventEmitter.on, + off: eventEmitter.off, + }; + + flush(); + return client; + }; + + const subscribe = async (options: SubscribeOptions): Promise => { + const { sid, ip, port, transport } = decodeSubscribeToken(options.token); + + if (transport === "ivs") { + return subscribeIVS(options, sid); + } + return subscribeWebRTC(options, sid, ip, port); + }; + return { connect, subscribe, diff --git a/packages/sdk/src/realtime/composite-latency.ts b/packages/sdk/src/realtime/composite-latency.ts new file mode 100644 index 0000000..213bda6 --- /dev/null +++ b/packages/sdk/src/realtime/composite-latency.ts @@ -0,0 +1,43 @@ +import type { LatencyReportMessage } from "./types"; + +export type CompositeLatencyEstimate = { + clientProxyRttMs: number; + serverProxyRttMs: number; + pipelineLatencyMs: number; + compositeE2eMs: number; +}; + +export class CompositeLatencyTracker { + private latestServerReport: { + serverProxyRttMs: number; + pipelineLatencyMs: number; + } | null = null; + + onServerReport(msg: LatencyReportMessage): void { + this.latestServerReport = { + serverProxyRttMs: msg.server_proxy_rtt_ms, + pipelineLatencyMs: msg.pipeline_latency_ms, + }; + } + + /** + * Compute composite E2E estimate. + * @param clientRttSeconds - client RTT in seconds from WebRTC stats, or null if unavailable (IVS) + */ + getEstimate(clientRttSeconds: number | null): CompositeLatencyEstimate | null { + if (!this.latestServerReport) return null; + + const { serverProxyRttMs, pipelineLatencyMs } = this.latestServerReport; + // Client RTT may be unavailable for IVS transport (no candidate-pair stats). + // In that case, report lower-bound estimate with clientProxyRttMs = 0. + const clientProxyRttMs = clientRttSeconds != null ? clientRttSeconds * 1000 : 0; + const compositeE2eMs = clientProxyRttMs + serverProxyRttMs + pipelineLatencyMs; + + return { + clientProxyRttMs, + serverProxyRttMs, + pipelineLatencyMs, + compositeE2eMs, + }; + } +} diff --git a/packages/sdk/src/realtime/diagnostics.ts b/packages/sdk/src/realtime/diagnostics.ts index 69059d9..60f9a23 100644 --- a/packages/sdk/src/realtime/diagnostics.ts +++ b/packages/sdk/src/realtime/diagnostics.ts @@ -1,5 +1,5 @@ /** Connection phase names for timing events. */ -export type ConnectionPhase = "websocket" | "avatar-image" | "initial-prompt" | "webrtc-handshake" | "total"; +export type ConnectionPhase = "websocket" | "avatar-image" | "initial-prompt" | "webrtc-handshake" | "ivs-stage-setup" | "total"; export type PhaseTimingEvent = { phase: ConnectionPhase; diff --git a/packages/sdk/src/realtime/insertable-streams.d.ts b/packages/sdk/src/realtime/insertable-streams.d.ts new file mode 100644 index 0000000..d4ab12e --- /dev/null +++ b/packages/sdk/src/realtime/insertable-streams.d.ts @@ -0,0 +1,22 @@ +/** + * Type declarations for the Insertable Streams API (Chrome 94+). + * https://developer.mozilla.org/en-US/docs/Web/API/Insertable_Streams_for_MediaStreamTrack_API + */ + +interface MediaStreamTrackProcessorInit { + track: MediaStreamTrack; +} + +declare class MediaStreamTrackProcessor { + constructor(init: MediaStreamTrackProcessorInit); + readonly readable: ReadableStream; +} + +interface MediaStreamTrackGeneratorInit { + kind: "audio" | "video"; +} + +declare class MediaStreamTrackGenerator extends MediaStreamTrack { + constructor(init: MediaStreamTrackGeneratorInit); + readonly writable: WritableStream; +} diff --git a/packages/sdk/src/realtime/ivs-connection.ts b/packages/sdk/src/realtime/ivs-connection.ts new file mode 100644 index 0000000..0506f94 --- /dev/null +++ b/packages/sdk/src/realtime/ivs-connection.ts @@ -0,0 +1,540 @@ +import mitt from "mitt"; + +import type { Logger } from "../utils/logger"; +import { buildUserAgent } from "../utils/user-agent"; +import type { DiagnosticEmitter } from "./diagnostics"; +import type { + ConnectionState, + IncomingIVSMessage, + OutgoingIVSMessage, + PromptAckMessage, + SetImageAckMessage, + WsMessageEvents, +} from "./types"; + +// ── IVS SDK type declarations ───────────────────────────────────────── +// Minimal type surface for amazon-ivs-web-broadcast so the SDK compiles +// even when the package is not installed. + +interface IVSStageStrategy { + stageStreamsToPublish(): IVSLocalStageStream[]; + shouldPublishParticipant(participant: IVSStageParticipant): boolean; + shouldSubscribeToParticipant(participant: IVSStageParticipant): IVSSubscribeType; +} + +interface IVSStage { + join(): Promise; + leave(): void; + on(event: string, handler: (...args: unknown[]) => void): void; +} + +interface IVSStageParticipant { + id: string; + isLocal: boolean; +} + +export interface IVSStageStream { + mediaStreamTrack: MediaStreamTrack; + requestRTCStats?(): Promise; +} + +export interface IVSLocalStageStream { + requestRTCStats?(): Promise; +} + +declare enum IVSSubscribeType { + NONE = "NONE", + AUDIO_VIDEO = "AUDIO_VIDEO", +} + +declare enum IVSStreamType { + VIDEO = "VIDEO", + AUDIO = "AUDIO", +} + +declare enum IVSStageEvents { + STAGE_CONNECTION_STATE_CHANGED = "STAGE_CONNECTION_STATE_CHANGED", + STAGE_PARTICIPANT_STREAMS_ADDED = "STAGE_PARTICIPANT_STREAMS_ADDED", +} + +declare enum IVSConnectionState { + CONNECTED = "CONNECTED", + DISCONNECTED = "DISCONNECTED", +} + +export interface IVSBroadcastModule { + Stage: new (token: string, strategy: IVSStageStrategy) => IVSStage; + LocalStageStream: new (track: MediaStreamTrack) => IVSLocalStageStream; + SubscribeType: typeof IVSSubscribeType; + StreamType: typeof IVSStreamType; + StageEvents: typeof IVSStageEvents; + ConnectionState: typeof IVSConnectionState; +} + +// ── Dynamic loader ──────────────────────────────────────────────────── + +export async function getIVSBroadcastClient(): Promise { + try { + const moduleName = "amazon-ivs-web-broadcast"; + // biome-ignore lint/suspicious/noExplicitAny: dynamic import of optional dependency + const mod = await (Function(`return import("${moduleName}")`)() as Promise); + return mod.default ?? mod; + } catch { + if (typeof globalThis !== "undefined" && "IVSBroadcastClient" in globalThis) { + // biome-ignore lint/suspicious/noExplicitAny: global fallback + return (globalThis as any).IVSBroadcastClient as IVSBroadcastModule; + } + throw new Error("amazon-ivs-web-broadcast not found. Install via npm or load via script tag."); + } +} + +// ── Types ───────────────────────────────────────────────────────────── + +const SETUP_TIMEOUT_MS = 30_000; + +interface IVSConnectionCallbacks { + onRemoteStream?: (stream: MediaStream) => void; + onStateChange?: (state: ConnectionState) => void; + onError?: (error: Error) => void; + modelName?: string; + initialImage?: string; + initialPrompt?: { text: string; enhance?: boolean }; + logger?: Logger; + onDiagnostic?: DiagnosticEmitter; +} + +const noopDiagnostic: DiagnosticEmitter = () => {}; + +// ── Connection ──────────────────────────────────────────────────────── + +export class IVSConnection { + private ws: WebSocket | null = null; + private publishStage: IVSStage | null = null; + private subscribeStage: IVSStage | null = null; + private connectionReject: ((error: Error) => void) | null = null; + private remoteStageStreams: IVSStageStream[] = []; + private localStageStreams: IVSLocalStageStream[] = []; + private logger: Logger; + private emitDiagnostic: DiagnosticEmitter; + state: ConnectionState = "disconnected"; + websocketMessagesEmitter = mitt(); + + constructor(private callbacks: IVSConnectionCallbacks = {}) { + this.logger = callbacks.logger ?? { debug() {}, info() {}, warn() {}, error() {} }; + this.emitDiagnostic = callbacks.onDiagnostic ?? noopDiagnostic; + } + + async connect(url: string, localStream: MediaStream | null, timeout: number, integration?: string): Promise { + // Phase 1: WebSocket + const userAgent = encodeURIComponent(buildUserAgent(integration)); + const separator = url.includes("?") ? "&" : "?"; + const wsUrl = `${url}${separator}user_agent=${userAgent}`; + + let rejectConnect!: (error: Error) => void; + const connectAbort = new Promise((_, reject) => { + rejectConnect = reject; + }); + connectAbort.catch(() => {}); + this.connectionReject = (error) => rejectConnect(error); + + const totalStart = performance.now(); + try { + const wsStart = performance.now(); + await Promise.race([ + new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error("WebSocket timeout")), timeout); + this.ws = new WebSocket(wsUrl); + + this.ws.onopen = () => { + clearTimeout(timer); + this.emitDiagnostic("phaseTiming", { + phase: "websocket", + durationMs: performance.now() - wsStart, + success: true, + }); + resolve(); + }; + this.ws.onmessage = (e) => { + try { + this.handleMessage(JSON.parse(e.data)); + } catch (err) { + this.logger.error("Message parse error", { error: String(err) }); + } + }; + this.ws.onerror = () => { + clearTimeout(timer); + const error = new Error("WebSocket error"); + this.emitDiagnostic("phaseTiming", { + phase: "websocket", + durationMs: performance.now() - wsStart, + success: false, + error: error.message, + }); + reject(error); + rejectConnect(error); + }; + this.ws.onclose = () => { + this.setState("disconnected"); + clearTimeout(timer); + reject(new Error("WebSocket closed before connection was established")); + rejectConnect(new Error("WebSocket closed")); + }; + }), + connectAbort, + ]); + + this.setState("connecting"); + + // Phase 2: IVS Stage setup — must complete before sending any messages. + // The bouncer creates the stage, sends ivs_stage_ready, then waits for + // ivs_joined before starting its message pump. Any set_image/prompt sent + // before ivs_joined would be consumed by the bouncer's join-wait loop + // and rejected as unexpected. + const stageStart = performance.now(); + await Promise.race([this.setupIVSStages(localStream, timeout), connectAbort]); + this.emitDiagnostic("phaseTiming", { + phase: "ivs-stage-setup", + durationMs: performance.now() - stageStart, + success: true, + }); + + // Phase 3: Post-handshake initial state (image/prompt) + // Now the bouncer's message pump is running and can handle these. + if (this.callbacks.initialImage) { + const imageStart = performance.now(); + await Promise.race([ + this.setImageBase64(this.callbacks.initialImage, { + prompt: this.callbacks.initialPrompt?.text, + enhance: this.callbacks.initialPrompt?.enhance, + }), + connectAbort, + ]); + this.emitDiagnostic("phaseTiming", { + phase: "avatar-image", + durationMs: performance.now() - imageStart, + success: true, + }); + } else if (this.callbacks.initialPrompt) { + const promptStart = performance.now(); + await Promise.race([this.sendInitialPrompt(this.callbacks.initialPrompt), connectAbort]); + this.emitDiagnostic("phaseTiming", { + phase: "initial-prompt", + durationMs: performance.now() - promptStart, + success: true, + }); + } else if (localStream) { + const nullStart = performance.now(); + await Promise.race([this.setImageBase64(null, { prompt: null }), connectAbort]); + this.emitDiagnostic("phaseTiming", { + phase: "initial-prompt", + durationMs: performance.now() - nullStart, + success: true, + }); + } + + this.emitDiagnostic("phaseTiming", { + phase: "total", + durationMs: performance.now() - totalStart, + success: true, + }); + } finally { + this.connectionReject = null; + } + } + + private async setupIVSStages(localStream: MediaStream | null, timeout: number): Promise { + const ivs = await getIVSBroadcastClient(); + + // Wait for bouncer to send ivs_stage_ready + const stageReady = await new Promise<{ + client_publish_token: string; + client_subscribe_token: string; + client_publish_participant_id: string; + }>((resolve, reject) => { + const timer = setTimeout(() => reject(new Error("IVS stage ready timeout")), timeout); + + const handler = (e: MessageEvent) => { + try { + const msg = JSON.parse(e.data); + if (msg.type === "ivs_stage_ready") { + clearTimeout(timer); + if (this.ws) { + this.ws.removeEventListener("message", handler); + } + resolve({ + client_publish_token: msg.client_publish_token, + client_subscribe_token: msg.client_subscribe_token, + client_publish_participant_id: msg.client_publish_participant_id ?? "", + }); + } + } catch { + // ignore parse errors, handled by main onmessage + } + }; + + this.ws?.addEventListener("message", handler); + }); + + // Subscribe stage — receive remote video/audio + const remoteStreamPromise = new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error("IVS subscribe stream timeout")), timeout); + + const clientPubId = stageReady.client_publish_participant_id; + const subscribeStrategy: IVSStageStrategy = { + stageStreamsToPublish: () => [], + shouldPublishParticipant: () => false, + shouldSubscribeToParticipant: (participant: IVSStageParticipant) => { + // Skip our own camera feed — only subscribe to server's processed output + if (clientPubId && participant.id === clientPubId) { + return ivs.SubscribeType.NONE; + } + return ivs.SubscribeType.AUDIO_VIDEO; + }, + }; + + this.subscribeStage = new ivs.Stage(stageReady.client_subscribe_token, subscribeStrategy); + + this.subscribeStage.on(ivs.StageEvents.STAGE_PARTICIPANT_STREAMS_ADDED, (...args: unknown[]) => { + const participant = args[0] as IVSStageParticipant; + const streams = args[1] as IVSStageStream[]; + if (participant.isLocal) return; + if (clientPubId && participant.id === clientPubId) return; + + clearTimeout(timer); + this.remoteStageStreams = streams; + const remoteStream = new MediaStream(); + for (const s of streams) { + remoteStream.addTrack(s.mediaStreamTrack); + } + this.callbacks.onRemoteStream?.(remoteStream); + resolve(); + }); + + this.subscribeStage.on(ivs.StageEvents.STAGE_CONNECTION_STATE_CHANGED, (...args: unknown[]) => { + const state = args[0] as string; + if (state === ivs.ConnectionState.DISCONNECTED.toString()) { + clearTimeout(timer); + reject(new Error("IVS subscribe stage disconnected during setup")); + this.setState("disconnected"); + } + }); + + this.subscribeStage.join().catch((err) => { + clearTimeout(timer); + reject(err); + }); + }); + + // Publish stage — send local camera + audio tracks + if (localStream) { + const localStageStreams: IVSLocalStageStream[] = []; + + const videoTrack = localStream.getVideoTracks()[0]; + if (videoTrack) { + localStageStreams.push(new ivs.LocalStageStream(videoTrack)); + } + const audioTrack = localStream.getAudioTracks()[0]; + if (audioTrack) { + localStageStreams.push(new ivs.LocalStageStream(audioTrack)); + } + this.localStageStreams = localStageStreams; + + const publishStrategy: IVSStageStrategy = { + stageStreamsToPublish: () => localStageStreams, + shouldPublishParticipant: () => true, + shouldSubscribeToParticipant: () => ivs.SubscribeType.NONE, + }; + + this.publishStage = new ivs.Stage(stageReady.client_publish_token, publishStrategy); + + this.publishStage.on(ivs.StageEvents.STAGE_CONNECTION_STATE_CHANGED, (...args: unknown[]) => { + const state = args[0] as string; + if (state === ivs.ConnectionState.CONNECTED.toString()) { + // Notify bouncer that we've joined the publish stage + this.send({ type: "ivs_joined" }); + this.setState("connected"); + } else if (state === ivs.ConnectionState.DISCONNECTED.toString()) { + this.setState("disconnected"); + } + }); + + await this.publishStage.join(); + } + + // Wait for remote stream from subscribe stage + await remoteStreamPromise; + } + + private handleMessage(msg: IncomingIVSMessage): void { + try { + if (msg.type === "error") { + const error = new Error(msg.error) as Error & { source?: string }; + error.source = "server"; + this.callbacks.onError?.(error); + if (this.connectionReject) { + this.connectionReject(error); + this.connectionReject = null; + } + return; + } + + if (msg.type === "set_image_ack") { + this.websocketMessagesEmitter.emit("setImageAck", msg); + return; + } + + if (msg.type === "prompt_ack") { + this.websocketMessagesEmitter.emit("promptAck", msg); + return; + } + + if (msg.type === "generation_started") { + this.setState("generating"); + return; + } + + if (msg.type === "generation_tick") { + this.websocketMessagesEmitter.emit("generationTick", msg); + return; + } + + if (msg.type === "generation_ended") { + return; + } + + if (msg.type === "session_id") { + this.websocketMessagesEmitter.emit("sessionId", msg); + return; + } + + if (msg.type === "latency_report") { + this.websocketMessagesEmitter.emit("latencyReport", msg); + return; + } + + // ivs_stage_ready is handled separately in setupIVSStages via addEventListener + } catch (error) { + this.logger.error("Message handler error", { error: String(error) }); + this.callbacks.onError?.(error as Error); + this.connectionReject?.(error as Error); + } + } + + send(message: OutgoingIVSMessage): boolean { + if (this.ws?.readyState === WebSocket.OPEN) { + this.ws.send(JSON.stringify(message)); + return true; + } + this.logger.warn("Message dropped: WebSocket is not open"); + return false; + } + + async setImageBase64( + imageBase64: string | null, + options?: { prompt?: string | null; enhance?: boolean; timeout?: number }, + ): Promise { + return new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => { + this.websocketMessagesEmitter.off("setImageAck", listener); + reject(new Error("Image send timed out")); + }, options?.timeout ?? SETUP_TIMEOUT_MS); + + const listener = (msg: SetImageAckMessage) => { + clearTimeout(timeoutId); + this.websocketMessagesEmitter.off("setImageAck", listener); + if (msg.success) { + resolve(); + } else { + reject(new Error(msg.error ?? "Failed to send image")); + } + }; + + this.websocketMessagesEmitter.on("setImageAck", listener); + + const message: { + type: "set_image"; + image_data: string | null; + prompt?: string | null; + enhance_prompt?: boolean; + } = { + type: "set_image", + image_data: imageBase64, + }; + + if (options?.prompt !== undefined) { + message.prompt = options.prompt; + } + if (options?.enhance !== undefined) { + message.enhance_prompt = options.enhance; + } + + if (!this.send(message)) { + clearTimeout(timeoutId); + this.websocketMessagesEmitter.off("setImageAck", listener); + reject(new Error("WebSocket is not open")); + } + }); + } + + private async sendInitialPrompt(prompt: { text: string; enhance?: boolean }): Promise { + return new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => { + this.websocketMessagesEmitter.off("promptAck", listener); + reject(new Error("Prompt send timed out")); + }, SETUP_TIMEOUT_MS); + + const listener = (msg: PromptAckMessage) => { + if (msg.prompt === prompt.text) { + clearTimeout(timeoutId); + this.websocketMessagesEmitter.off("promptAck", listener); + if (msg.success) { + resolve(); + } else { + reject(new Error(msg.error ?? "Failed to send prompt")); + } + } + }; + + this.websocketMessagesEmitter.on("promptAck", listener); + + if ( + !this.send({ + type: "prompt", + prompt: prompt.text, + enhance_prompt: prompt.enhance ?? true, + }) + ) { + clearTimeout(timeoutId); + this.websocketMessagesEmitter.off("promptAck", listener); + reject(new Error("WebSocket is not open")); + } + }); + } + + private setState(state: ConnectionState): void { + if (this.state !== state) { + this.state = state; + this.callbacks.onStateChange?.(state); + } + } + + getRemoteStreams(): IVSStageStream[] { + return this.remoteStageStreams; + } + + getLocalStreams(): IVSLocalStageStream[] { + return this.localStageStreams; + } + + cleanup(): void { + this.publishStage?.leave(); + this.publishStage = null; + this.subscribeStage?.leave(); + this.subscribeStage = null; + this.ws?.close(); + this.ws = null; + this.remoteStageStreams = []; + this.localStageStreams = []; + this.setState("disconnected"); + } +} diff --git a/packages/sdk/src/realtime/ivs-manager.ts b/packages/sdk/src/realtime/ivs-manager.ts new file mode 100644 index 0000000..fc325a5 --- /dev/null +++ b/packages/sdk/src/realtime/ivs-manager.ts @@ -0,0 +1,246 @@ +import pRetry, { AbortError } from "p-retry"; + +import type { Logger } from "../utils/logger"; +import type { DiagnosticEmitter } from "./diagnostics"; +import { IVSConnection } from "./ivs-connection"; +import type { RealtimeTransportManager } from "./transport-manager"; +import type { ConnectionState, OutgoingMessage } from "./types"; + +export interface IVSConfig { + ivsUrl: string; + integration?: string; + logger?: Logger; + onDiagnostic?: DiagnosticEmitter; + onRemoteStream: (stream: MediaStream) => void; + onConnectionStateChange?: (state: ConnectionState) => void; + onError?: (error: Error) => void; + modelName?: string; + initialImage?: string; + initialPrompt?: { text: string; enhance?: boolean }; +} + +const PERMANENT_ERRORS = [ + "permission denied", + "not allowed", + "invalid session", + "401", + "invalid api key", + "unauthorized", +]; + +const CONNECTION_TIMEOUT = 60_000 * 5; // 5 minutes + +const RETRY_OPTIONS = { + retries: 5, + factor: 2, + minTimeout: 1000, + maxTimeout: 10000, +} as const; + +export class IVSManager implements RealtimeTransportManager { + private connection: IVSConnection; + private config: IVSConfig; + private logger: Logger; + private localStream: MediaStream | null = null; + private managerState: ConnectionState = "disconnected"; + private hasConnected = false; + private isReconnecting = false; + private intentionalDisconnect = false; + private reconnectGeneration = 0; + + constructor(config: IVSConfig) { + this.config = config; + this.logger = config.logger ?? { debug() {}, info() {}, warn() {}, error() {} }; + this.connection = new IVSConnection({ + onRemoteStream: config.onRemoteStream, + onStateChange: (state) => this.handleConnectionStateChange(state), + onError: config.onError, + modelName: config.modelName, + initialImage: config.initialImage, + initialPrompt: config.initialPrompt, + logger: this.logger, + onDiagnostic: config.onDiagnostic, + }); + } + + private emitState(state: ConnectionState): void { + if (this.managerState !== state) { + this.managerState = state; + if (state === "connected" || state === "generating") this.hasConnected = true; + this.config.onConnectionStateChange?.(state); + } + } + + private handleConnectionStateChange(state: ConnectionState): void { + if (this.intentionalDisconnect) { + this.emitState("disconnected"); + return; + } + + if (this.isReconnecting) { + if (state === "connected" || state === "generating") { + this.isReconnecting = false; + this.emitState(state); + } + return; + } + + if (state === "disconnected" && !this.intentionalDisconnect && this.hasConnected) { + this.reconnect(); + return; + } + + this.emitState(state); + } + + private async reconnect(): Promise { + if (this.isReconnecting || this.intentionalDisconnect) return; + if (!this.localStream) return; + + const reconnectGeneration = ++this.reconnectGeneration; + this.isReconnecting = true; + this.emitState("reconnecting"); + const reconnectStart = performance.now(); + + try { + let attemptCount = 0; + + await pRetry( + async () => { + attemptCount++; + + if (this.intentionalDisconnect || reconnectGeneration !== this.reconnectGeneration) { + throw new AbortError("Reconnect cancelled"); + } + + if (!this.localStream) { + throw new AbortError("Reconnect cancelled: no local stream"); + } + + this.connection.cleanup(); + await this.connection.connect( + this.config.ivsUrl, + this.localStream, + CONNECTION_TIMEOUT, + this.config.integration, + ); + + if (this.intentionalDisconnect || reconnectGeneration !== this.reconnectGeneration) { + this.connection.cleanup(); + throw new AbortError("Reconnect cancelled"); + } + }, + { + ...RETRY_OPTIONS, + onFailedAttempt: (error) => { + if (this.intentionalDisconnect || reconnectGeneration !== this.reconnectGeneration) { + return; + } + this.logger.warn("IVS reconnect attempt failed", { error: error.message, attempt: error.attemptNumber }); + this.config.onDiagnostic?.("reconnect", { + attempt: error.attemptNumber, + maxAttempts: RETRY_OPTIONS.retries + 1, + durationMs: performance.now() - reconnectStart, + success: false, + error: error.message, + }); + this.connection.cleanup(); + }, + shouldRetry: (error) => { + if (this.intentionalDisconnect || reconnectGeneration !== this.reconnectGeneration) { + return false; + } + const msg = error.message.toLowerCase(); + return !PERMANENT_ERRORS.some((err) => msg.includes(err)); + }, + }, + ); + this.config.onDiagnostic?.("reconnect", { + attempt: attemptCount, + maxAttempts: RETRY_OPTIONS.retries + 1, + durationMs: performance.now() - reconnectStart, + success: true, + }); + } catch (error) { + this.isReconnecting = false; + if (this.intentionalDisconnect || reconnectGeneration !== this.reconnectGeneration) { + return; + } + this.emitState("disconnected"); + this.config.onError?.(error instanceof Error ? error : new Error(String(error))); + } + } + + async connect(localStream: MediaStream | null): Promise { + this.localStream = localStream; + this.intentionalDisconnect = false; + this.hasConnected = false; + this.isReconnecting = false; + this.reconnectGeneration += 1; + this.emitState("connecting"); + + return pRetry( + async () => { + if (this.intentionalDisconnect) { + throw new AbortError("Connect cancelled"); + } + await this.connection.connect(this.config.ivsUrl, localStream, CONNECTION_TIMEOUT, this.config.integration); + return true; + }, + { + ...RETRY_OPTIONS, + onFailedAttempt: (error) => { + this.logger.warn("IVS connection attempt failed", { error: error.message, attempt: error.attemptNumber }); + this.connection.cleanup(); + }, + shouldRetry: (error) => { + if (this.intentionalDisconnect) { + return false; + } + const msg = error.message.toLowerCase(); + return !PERMANENT_ERRORS.some((err) => msg.includes(err)); + }, + }, + ); + } + + sendMessage(message: OutgoingMessage): boolean { + return this.connection.send(message); + } + + cleanup(): void { + this.intentionalDisconnect = true; + this.isReconnecting = false; + this.reconnectGeneration += 1; + this.connection.cleanup(); + this.localStream = null; + this.emitState("disconnected"); + } + + isConnected(): boolean { + return this.managerState === "connected" || this.managerState === "generating"; + } + + getConnectionState(): ConnectionState { + return this.managerState; + } + + getWebsocketMessageEmitter() { + return this.connection.websocketMessagesEmitter; + } + + getRemoteStreams() { + return this.connection.getRemoteStreams(); + } + + getLocalStreams() { + return this.connection.getLocalStreams(); + } + + setImage( + imageBase64: string | null, + options?: { prompt?: string; enhance?: boolean; timeout?: number }, + ): Promise { + return this.connection.setImageBase64(imageBase64, options); + } +} diff --git a/packages/sdk/src/realtime/ivs-stats-collector.ts b/packages/sdk/src/realtime/ivs-stats-collector.ts new file mode 100644 index 0000000..2f218e1 --- /dev/null +++ b/packages/sdk/src/realtime/ivs-stats-collector.ts @@ -0,0 +1,93 @@ +import { type WebRTCStats, StatsParser, type StatsOptions } from "./webrtc-stats"; + +const DEFAULT_INTERVAL_MS = 1000; +const MIN_INTERVAL_MS = 500; + +// Minimal interface for IVS streams that support requestRTCStats +interface StatsCapableStream { + requestRTCStats?(): Promise; +} + +export interface IVSStatsSource { + getRemoteStreams(): StatsCapableStream[]; + getLocalStreams(): StatsCapableStream[]; +} + +export class IVSStatsCollector { + private parser = new StatsParser(); + private intervalId: ReturnType | null = null; + private source: IVSStatsSource | null = null; + private onStats: ((stats: WebRTCStats) => void) | null = null; + private intervalMs: number; + + constructor(options: StatsOptions = {}) { + this.intervalMs = Math.max(options.intervalMs ?? DEFAULT_INTERVAL_MS, MIN_INTERVAL_MS); + } + + start(source: IVSStatsSource, onStats: (stats: WebRTCStats) => void): void { + this.stop(); + this.source = source; + this.onStats = onStats; + this.parser.reset(); + this.intervalId = setInterval(() => this.collect(), this.intervalMs); + } + + stop(): void { + if (this.intervalId !== null) { + clearInterval(this.intervalId); + this.intervalId = null; + } + this.source = null; + this.onStats = null; + } + + isRunning(): boolean { + return this.intervalId !== null; + } + + private async collect(): Promise { + if (!this.source || !this.onStats) return; + + try { + // Get RTCStatsReport from remote streams (inbound video/audio) + const remoteStreams = this.source.getRemoteStreams(); + // Get from local streams (outbound video) if available + const localStreams = this.source.getLocalStreams(); + + // Collect all stats reports + const reports: RTCStatsReport[] = []; + + for (const stream of remoteStreams) { + if (stream.requestRTCStats) { + const report = await stream.requestRTCStats(); + if (report) reports.push(report); + } + } + for (const stream of localStreams) { + if (stream.requestRTCStats) { + const report = await stream.requestRTCStats(); + if (report) reports.push(report); + } + } + + if (reports.length === 0) return; + + // Merge all reports into a single Map-like structure that StatsParser can consume + // RTCStatsReport is a Map, so we can merge them + const merged = new Map(); + for (const report of reports) { + report.forEach((value, key) => { + merged.set(key, value); + }); + } + + // StatsParser.parse() expects RTCStatsReport which has a forEach method + // Our merged Map satisfies this interface + const stats = this.parser.parse(merged as unknown as RTCStatsReport); + this.onStats(stats); + } catch { + // Stream might be closed; stop silently + this.stop(); + } + } +} diff --git a/packages/sdk/src/realtime/latency-diagnostics.ts b/packages/sdk/src/realtime/latency-diagnostics.ts new file mode 100644 index 0000000..9d96c20 --- /dev/null +++ b/packages/sdk/src/realtime/latency-diagnostics.ts @@ -0,0 +1,113 @@ +/** + * Consolidated latency diagnostics for RT sessions. + * + * Bundles CompositeLatencyTracker and PixelLatencyProbe into one + * pluggable object, keeping client.ts clean. + */ + +import { type CompositeLatencyEstimate, CompositeLatencyTracker } from "./composite-latency"; +import { + type PixelLatencyEvent, + type PixelLatencyMeasurement, + PixelLatencyProbe, + type PixelLatencyReport, +} from "./pixel-latency"; +import { PixelLatencyStamper } from "./pixel-latency-stamper"; +import type { LatencyReportMessage, OutgoingMessage } from "./types"; +import type { WebRTCStats } from "./webrtc-stats"; + +export type LatencyDiagnosticsOptions = { + composite?: boolean; + pixelMarker?: boolean; + videoElement?: HTMLVideoElement; + sendMessage: (msg: OutgoingMessage) => void; + onCompositeLatency: (estimate: CompositeLatencyEstimate) => void; + onPixelLatency: (measurement: PixelLatencyMeasurement) => void; + onPixelLatencyEvent: (event: PixelLatencyEvent) => void; + onPixelLatencyReport: (report: PixelLatencyReport) => void; +}; + +export class LatencyDiagnostics { + private compositeTracker: CompositeLatencyTracker | null = null; + private pixelProbe: PixelLatencyProbe | null = null; + private stamper: PixelLatencyStamper | null = null; + private latestClientRtt: number | null = null; + private readonly options: LatencyDiagnosticsOptions; + private readonly onCompositeLatency: (estimate: CompositeLatencyEstimate) => void; + + constructor(options: LatencyDiagnosticsOptions) { + this.options = options; + this.onCompositeLatency = options.onCompositeLatency; + + if (options.composite) { + this.compositeTracker = new CompositeLatencyTracker(); + } + } + + /** + * Create a stamper wrapping the camera video track. + * Returns the processed MediaStream to use instead of the raw camera stream. + * Call this before manager.connect() to substitute the published stream. + * Starts the draw loop immediately so IVS gets frames from the start. + */ + async createStamper(localStream: MediaStream): Promise { + if (!this.options.pixelMarker) return localStream; + + const videoTrack = localStream.getVideoTracks()[0]; + if (!videoTrack) return localStream; + + this.stamper = new PixelLatencyStamper(videoTrack); + + // Start the draw loop now so the canvas track produces frames immediately + await this.stamper.start(); + + // Build a new stream: processed video + original audio + const processedStream = new MediaStream(); + for (const track of this.stamper.getProcessedStream().getVideoTracks()) { + processedStream.addTrack(track); + } + for (const track of localStream.getAudioTracks()) { + processedStream.addTrack(track); + } + + return processedStream; + } + + /** Handle incoming latency_report from server. */ + onServerReport(msg: LatencyReportMessage): void { + if (!this.compositeTracker) return; + this.compositeTracker.onServerReport(msg); + const estimate = this.compositeTracker.getEstimate(this.latestClientRtt); + if (estimate) { + this.onCompositeLatency(estimate); + } + } + + /** Update client RTT from WebRTC stats. */ + onStats(stats: WebRTCStats): void { + this.latestClientRtt = stats.connection?.currentRoundTripTime ?? null; + } + + /** Start pixel probing (stamper already started in createStamper). */ + async start(): Promise { + // Create and start pixel probe (deferred so stamper is available) + if (this.options.pixelMarker && this.options.videoElement) { + this.pixelProbe = new PixelLatencyProbe({ + sendMessage: this.options.sendMessage, + onMeasurement: this.options.onPixelLatency, + onEvent: this.options.onPixelLatencyEvent, + onReport: this.options.onPixelLatencyReport, + stamper: this.stamper ?? undefined, + }); + this.pixelProbe.start(this.options.videoElement); + } + } + + /** Tear down everything. */ + stop(): void { + this.pixelProbe?.stop(); + this.pixelProbe = null; + this.stamper?.stop(); + this.stamper = null; + } +} diff --git a/packages/sdk/src/realtime/methods.ts b/packages/sdk/src/realtime/methods.ts index 6755d41..22d0867 100644 --- a/packages/sdk/src/realtime/methods.ts +++ b/packages/sdk/src/realtime/methods.ts @@ -1,6 +1,6 @@ import { z } from "zod"; +import type { RealtimeTransportManager } from "./transport-manager"; import type { PromptAckMessage } from "./types"; -import type { WebRTCManager } from "./webrtc-manager"; const PROMPT_TIMEOUT_MS = 15 * 1000; // 15 seconds const UPDATE_TIMEOUT_MS = 30 * 1000; @@ -23,11 +23,11 @@ const setPromptInputSchema = z.object({ export type SetInput = z.input; export const realtimeMethods = ( - webrtcManager: WebRTCManager, + manager: RealtimeTransportManager, imageToBase64: (image: Blob | File | string) => Promise, ) => { const assertConnected = () => { - const state = webrtcManager.getConnectionState(); + const state = manager.getConnectionState(); if (state !== "connected" && state !== "generating") { throw new Error(`Cannot send message: connection is ${state}`); } @@ -48,7 +48,7 @@ export const realtimeMethods = ( imageBase64 = await imageToBase64(image); } - await webrtcManager.setImage(imageBase64, { prompt, enhance, timeout: UPDATE_TIMEOUT_MS }); + await manager.setImage(imageBase64, { prompt, enhance, timeout: UPDATE_TIMEOUT_MS }); }; const setPrompt = async (prompt: string, { enhance }: { enhance?: boolean } = {}): Promise => { @@ -63,7 +63,7 @@ export const realtimeMethods = ( throw parsedInput.error; } - const emitter = webrtcManager.getWebsocketMessageEmitter(); + const emitter = manager.getWebsocketMessageEmitter(); let promptAckListener: ((msg: PromptAckMessage) => void) | undefined; let timeoutId: ReturnType | undefined; @@ -83,7 +83,7 @@ export const realtimeMethods = ( }); // Send the message first - const sent = webrtcManager.sendMessage({ + const sent = manager.sendMessage({ type: "prompt", prompt: parsedInput.data.prompt, enhance_prompt: parsedInput.data.enhance, diff --git a/packages/sdk/src/realtime/pixel-latency-stamper.ts b/packages/sdk/src/realtime/pixel-latency-stamper.ts new file mode 100644 index 0000000..ca0e19a --- /dev/null +++ b/packages/sdk/src/realtime/pixel-latency-stamper.ts @@ -0,0 +1,234 @@ +/** + * Input frame stamper for E2E pixel latency. + * + * Wraps a camera MediaStreamTrack to optionally stamp a pixel marker (~every 2s). + * + * Primary path: Insertable Streams (MediaStreamTrackProcessor/Generator, Chrome 94+). + * - 1-in-1-out: output FPS naturally matches source (no rAF inflation). + * - 99% of frames pass through unchanged (zero copy, zero quality loss). + * - Only stamped frames go through OffscreenCanvas. + * + * Fallback: Canvas + rAF + captureStream (for environments without Insertable Streams). + */ + +const SYNC = [200, 50, 200, 50]; +const DATA_BITS = 16; +const CHECKSUM_BITS = 4; +const TOTAL_PIXELS = 24; // 4 sync + 16 data + 4 checksum + +export class PixelLatencyStamper { + private originalTrack: MediaStreamTrack; + private processedStream: MediaStream; + private running = false; + private pendingStamp: number | null = null; + + // Insertable Streams path + private abortController: AbortController | null = null; + + // Canvas fallback path + private canvas: HTMLCanvasElement | null = null; + private ctx: CanvasRenderingContext2D | null = null; + private sourceVideo: HTMLVideoElement | null = null; + + constructor(sourceVideoTrack: MediaStreamTrack) { + this.originalTrack = sourceVideoTrack; + + if (typeof MediaStreamTrackProcessor !== "undefined") { + this.processedStream = this.initInsertableStreams(sourceVideoTrack); + } else { + this.processedStream = this.initCanvasFallback(sourceVideoTrack); + } + } + + // ── Insertable Streams (primary) ───────────────────────────────────── + + private initInsertableStreams(track: MediaStreamTrack): MediaStream { + const processor = new MediaStreamTrackProcessor({ track }); + const generator = new MediaStreamTrackGenerator({ kind: "video" }); + + const stamper = this; + const transformer = new TransformStream({ + transform(frame, controller) { + if (stamper.pendingStamp !== null) { + const seq = stamper.pendingStamp; + stamper.pendingStamp = null; + + const w = frame.displayWidth; + const h = frame.displayHeight; + const canvas = new OffscreenCanvas(w, h); + const ctx = canvas.getContext("2d")!; + ctx.drawImage(frame, 0, 0); + stamper.stampMarker(ctx, h, seq); + + const stamped = new VideoFrame(canvas, { timestamp: frame.timestamp }); + frame.close(); + controller.enqueue(stamped); + } else { + // Pass through unchanged — zero copy, zero quality loss + controller.enqueue(frame); + } + }, + }); + + this.abortController = new AbortController(); + processor.readable + .pipeThrough(transformer, { signal: this.abortController.signal }) + .pipeTo(generator.writable, { signal: this.abortController.signal }) + .catch(() => { + // Expected on abort during stop() + }); + + return new MediaStream([generator]); + } + + // ── Canvas fallback ────────────────────────────────────────────────── + + private initCanvasFallback(sourceVideoTrack: MediaStreamTrack): MediaStream { + this.sourceVideo = document.createElement("video"); + this.sourceVideo.srcObject = new MediaStream([sourceVideoTrack]); + this.sourceVideo.muted = true; + this.sourceVideo.playsInline = true; + + this.canvas = document.createElement("canvas"); + + const settings = sourceVideoTrack.getSettings(); + if (settings.width) this.canvas.width = settings.width; + if (settings.height) this.canvas.height = settings.height; + + const ctx = this.canvas.getContext("2d"); + if (!ctx) throw new Error("Failed to create canvas 2d context for pixel stamper"); + this.ctx = ctx; + + return this.canvas.captureStream(); + } + + // ── Public API ─────────────────────────────────────────────────────── + + /** Get the processed MediaStream. */ + getProcessedStream(): MediaStream { + return this.processedStream; + } + + /** Get the original source track (for cleanup). */ + getOriginalTrack(): MediaStreamTrack { + return this.originalTrack; + } + + async start(): Promise { + if (this.running) return; + this.running = true; + + // Canvas fallback needs explicit play + draw loop + if (this.sourceVideo) { + await this.sourceVideo.play(); + this.drawLoop(); + } + // Insertable Streams path is already piping from the constructor + } + + stop(): void { + this.running = false; + + if (this.abortController) { + this.abortController.abort(); + this.abortController = null; + } + + if (this.sourceVideo) { + this.sourceVideo.pause(); + this.sourceVideo.srcObject = null; + } + + for (const track of this.processedStream.getTracks()) { + track.stop(); + } + } + + /** Queue a marker seq to be stamped on the next frame. */ + queueStamp(seq: number): void { + this.pendingStamp = seq; + } + + // ── Canvas fallback draw loop ──────────────────────────────────────── + + private drawLoop(): void { + if (!this.running) return; + + requestAnimationFrame(() => { + if ( + this.sourceVideo && + this.ctx && + this.canvas && + this.sourceVideo.videoWidth > 0 && + this.sourceVideo.videoHeight > 0 + ) { + if (this.canvas.width !== this.sourceVideo.videoWidth) { + this.canvas.width = this.sourceVideo.videoWidth; + } + if (this.canvas.height !== this.sourceVideo.videoHeight) { + this.canvas.height = this.sourceVideo.videoHeight; + } + + this.ctx.drawImage(this.sourceVideo, 0, 0); + + const seq = this.pendingStamp; + if (seq !== null) { + this.pendingStamp = null; + this.stampMarker(this.ctx, this.canvas.height, seq); + } + } + + this.drawLoop(); + }); + } + + // ── Shared stamp logic ─────────────────────────────────────────────── + + private stampMarker( + ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, + canvasHeight: number, + seq: number, + ): void { + const seqMasked = seq & 0xffff; + const imageData = ctx.createImageData(TOTAL_PIXELS, 1); + const data = imageData.data; + + // Sync pattern: R=G=B=200 or R=G=B=50 (maps to Y=200/Y=50 in YUV) + for (let i = 0; i < 4; i++) { + const val = SYNC[i]; + const offset = i * 4; + data[offset] = val; + data[offset + 1] = val; + data[offset + 2] = val; + data[offset + 3] = 255; + } + + // 16-bit seq (MSB first) + for (let i = 0; i < DATA_BITS; i++) { + const bit = (seqMasked >> (DATA_BITS - 1 - i)) & 1; + const val = bit ? 200 : 50; + const offset = (4 + i) * 4; + data[offset] = val; + data[offset + 1] = val; + data[offset + 2] = val; + data[offset + 3] = 255; + } + + // 4-bit XOR checksum + let checksum = 0; + for (let i = 0; i < DATA_BITS; i += 4) { + checksum ^= (seqMasked >> i) & 0xf; + } + for (let i = 0; i < CHECKSUM_BITS; i++) { + const bit = (checksum >> (CHECKSUM_BITS - 1 - i)) & 1; + const val = bit ? 200 : 50; + const offset = (20 + i) * 4; + data[offset] = val; + data[offset + 1] = val; + data[offset + 2] = val; + data[offset + 3] = 255; + } + + ctx.putImageData(imageData, 0, canvasHeight - 1); + } +} diff --git a/packages/sdk/src/realtime/pixel-latency.ts b/packages/sdk/src/realtime/pixel-latency.ts new file mode 100644 index 0000000..3187e15 --- /dev/null +++ b/packages/sdk/src/realtime/pixel-latency.ts @@ -0,0 +1,332 @@ +import type { PixelLatencyStamper } from "./pixel-latency-stamper"; + +export type PixelLatencyMeasurement = { + seq: number; + e2eLatencyMs: number; + timestamp: number; +}; + +export type PixelLatencyStats = { + sent: number; + received: number; + lost: number; + corrupted: number; + outOfOrder: number; + deliveryRate: number; +}; + +export type PixelLatencyStatus = "ok" | "ok_reordered" | "corrupted" | "lost"; + +export type PixelLatencyEvent = + | { status: "ok"; seq: number; e2eLatencyMs: number; timestamp: number } + | { status: "ok_reordered"; seq: number; e2eLatencyMs: number; timestamp: number } + | { status: "corrupted"; seq: null; e2eLatencyMs: null; timestamp: number } + | { status: "lost"; seq: number; e2eLatencyMs: null; timestamp: number; timeoutMs: number }; + +export type PixelLatencyReport = PixelLatencyStats & { + timestamp: number; + pending: number; +}; + +/** Message sent to server for legacy WS-probe mode. */ +type LatencyProbeMessage = { + type: "latency_probe"; + seq: number; + client_time: number; +}; + +/** Periodic E2E stats report sent to server. */ +type E2ELatencyReportMessage = { + type: "e2e_latency_report"; + avg_latency_ms: number | null; + delivery_rate: number; + lost: number; + corrupted: number; + out_of_order: number; +}; + +export class PixelLatencyProbe { + private static readonly SYNC = [200, 50, 200, 50]; + private static readonly DATA_BITS = 16; + private static readonly CHECKSUM_BITS = 4; + private static readonly TOTAL_PIXELS = 24; + private static readonly PROBE_INTERVAL_MS = 2000; + private static readonly PROBE_TTL_MS = 60000; + private static readonly REPORT_INTERVAL_MS = 5000; + + private seq = 0; + private pendingProbes = new Map(); // seq -> clientTime + private canvas: OffscreenCanvas; + private ctx: OffscreenCanvasRenderingContext2D; + private probeIntervalId: ReturnType | null = null; + private reportIntervalId: ReturnType | null = null; + private running = false; + + // E2E stamper (null = legacy WS-probe mode) + private stamper: PixelLatencyStamper | null; + + // Stats tracking + private lastReceivedSeq = 0; + private recentLatencies: number[] = []; + private stats = { sent: 0, received: 0, lost: 0, corrupted: 0, outOfOrder: 0 }; + // Previous snapshot for computing deltas sent to server + private prevReportStats = { lost: 0, corrupted: 0, outOfOrder: 0 }; + + private sendMessage: ((msg: LatencyProbeMessage | E2ELatencyReportMessage) => void) | null; + private onMeasurement: (m: PixelLatencyMeasurement) => void; + private onEvent: (e: PixelLatencyEvent) => void; + private onReport: (r: PixelLatencyReport) => void; + + constructor(options: { + sendMessage: ((msg: LatencyProbeMessage | E2ELatencyReportMessage) => void) | null; + onMeasurement: (m: PixelLatencyMeasurement) => void; + onEvent: (e: PixelLatencyEvent) => void; + onReport: (r: PixelLatencyReport) => void; + stamper?: PixelLatencyStamper; + }) { + this.sendMessage = options.sendMessage; + this.onMeasurement = options.onMeasurement; + this.onEvent = options.onEvent; + this.onReport = options.onReport; + this.stamper = options.stamper ?? null; + this.canvas = new OffscreenCanvas(PixelLatencyProbe.TOTAL_PIXELS, 1); + const ctx = this.canvas.getContext("2d"); + if (!ctx) throw new Error("Failed to create OffscreenCanvas 2d context"); + this.ctx = ctx; + } + + start(videoElement: HTMLVideoElement): void { + if (this.running) return; + this.running = true; + + if (this.stamper) { + // E2E mode: stamp input frames every ~2s + this.probeIntervalId = setInterval(() => this.stampInputFrame(), PixelLatencyProbe.PROBE_INTERVAL_MS); + // Periodic report (to server and/or local callback) + this.reportIntervalId = setInterval(() => this.sendE2EReport(), PixelLatencyProbe.REPORT_INTERVAL_MS); + } else if (this.sendMessage) { + // Legacy WS-probe mode + this.probeIntervalId = setInterval(() => this.sendProbe(), PixelLatencyProbe.PROBE_INTERVAL_MS); + } + + // Read output frames + this.readFrameLoop(videoElement); + } + + stop(): void { + this.running = false; + if (this.probeIntervalId != null) { + clearInterval(this.probeIntervalId); + this.probeIntervalId = null; + } + if (this.reportIntervalId != null) { + clearInterval(this.reportIntervalId); + this.reportIntervalId = null; + } + this.pendingProbes.clear(); + } + + getStats(): PixelLatencyStats { + const sent = this.stats.sent; + return { + ...this.stats, + deliveryRate: sent > 0 ? this.stats.received / sent : 0, + }; + } + + // ── E2E mode: stamp input frames ──────────────────────────────────── + + private stampInputFrame(): void { + if (!this.stamper) return; + this.seq = (this.seq + 1) & 0xffff; + const seq = this.seq; + this.pendingProbes.set(seq, performance.now()); + this.stats.sent++; + this.stamper.queueStamp(seq); + this.cleanUpOldProbes(); + } + + private sendE2EReport(): void { + this.cleanUpOldProbes(); + this.onReport({ ...this.getStats(), timestamp: Date.now(), pending: this.pendingProbes.size }); + if (!this.sendMessage) return; + const avgMs = + this.recentLatencies.length > 0 + ? this.recentLatencies.reduce((a, b) => a + b, 0) / this.recentLatencies.length + : null; + const sent = this.stats.sent; + const deltaLost = this.stats.lost - this.prevReportStats.lost; + const deltaCorrupted = this.stats.corrupted - this.prevReportStats.corrupted; + const deltaOutOfOrder = this.stats.outOfOrder - this.prevReportStats.outOfOrder; + this.sendMessage({ + type: "e2e_latency_report", + avg_latency_ms: avgMs !== null ? Math.round(avgMs * 100) / 100 : null, + delivery_rate: sent > 0 ? this.stats.received / sent : 0, + lost: deltaLost, + corrupted: deltaCorrupted, + out_of_order: deltaOutOfOrder, + }); + this.prevReportStats = { + lost: this.stats.lost, + corrupted: this.stats.corrupted, + outOfOrder: this.stats.outOfOrder, + }; + this.recentLatencies = []; + } + + // ── Legacy WS-probe mode ──────────────────────────────────────────── + + private sendProbe(): void { + if (!this.sendMessage) return; + this.seq = (this.seq + 1) & 0xffff; + const seq = this.seq; + const clientTime = performance.now(); + this.pendingProbes.set(seq, clientTime); + this.stats.sent++; + this.sendMessage({ type: "latency_probe", seq, client_time: clientTime }); + this.cleanUpOldProbes(); + } + + // ── Output frame reader (shared by both modes) ───────────────────── + + private readFrameLoop(video: HTMLVideoElement): void { + if (!this.running) return; + + // Use requestVideoFrameCallback if available (Chrome/Edge), else requestAnimationFrame + if ("requestVideoFrameCallback" in video) { + // biome-ignore lint/suspicious/noExplicitAny: requestVideoFrameCallback not in all TS libs + (video as any).requestVideoFrameCallback((_now: number, _metadata: unknown) => { + this.readFrame(video); + this.readFrameLoop(video); + }); + } else { + requestAnimationFrame(() => { + this.readFrame(video); + this.readFrameLoop(video); + }); + } + } + + private readFrame(video: HTMLVideoElement): void { + if (video.videoWidth === 0 || video.videoHeight === 0) return; + + try { + // Draw only the bottom-left 24x1 region + this.ctx.drawImage( + video, + 0, + video.videoHeight - 1, // source x, y (bottom-left) + PixelLatencyProbe.TOTAL_PIXELS, + 1, // source width, height + 0, + 0, // dest x, y + PixelLatencyProbe.TOTAL_PIXELS, + 1, // dest width, height + ); + + const imageData = this.ctx.getImageData(0, 0, PixelLatencyProbe.TOTAL_PIXELS, 1); + const pixels = imageData.data; // RGBA, 4 bytes per pixel + + const result = this.extractSeq(pixels); + if (result === "no_marker") return; + if (result === "corrupted") { + this.stats.corrupted++; + this.onEvent({ status: "corrupted", seq: null, e2eLatencyMs: null, timestamp: Date.now() }); + return; + } + + const seq = result; + const clientTime = this.pendingProbes.get(seq); + if (clientTime == null) return; + + this.pendingProbes.delete(seq); + this.stats.received++; + + const e2eLatencyMs = performance.now() - clientTime; + this.recentLatencies.push(e2eLatencyMs); + const timestamp = Date.now(); + + // Reorder detection + let reordered = false; + if (seq < this.lastReceivedSeq) { + const distance = this.lastReceivedSeq - seq; + if (distance < 0x8000) { + this.stats.outOfOrder++; + reordered = true; + } + } + this.lastReceivedSeq = seq; + + this.onMeasurement({ seq, e2eLatencyMs, timestamp }); + this.onEvent({ + status: reordered ? "ok_reordered" : "ok", + seq, + e2eLatencyMs, + timestamp, + }); + } catch { + // Ignore read errors (cross-origin, etc.) + } + } + + /** + * Extract seq from pixel data. + * Returns: number (valid seq), "no_marker" (sync doesn't match), "corrupted" (sync ok, checksum bad) + */ + private extractSeq(pixels: Uint8ClampedArray): number | "no_marker" | "corrupted" { + // Check sync pattern (R channel of RGBA) + for (let i = 0; i < PixelLatencyProbe.SYNC.length; i++) { + const r = pixels[i * 4]; // R channel + const expected = PixelLatencyProbe.SYNC[i]; + const isHigh = r >= 128; + const shouldBeHigh = expected >= 128; + if (isHigh !== shouldBeHigh) return "no_marker"; + } + + // Extract 16-bit seq + let seq = 0; + for (let i = 0; i < PixelLatencyProbe.DATA_BITS; i++) { + const r = pixels[(4 + i) * 4]; + if (r >= 128) { + seq |= 1 << (PixelLatencyProbe.DATA_BITS - 1 - i); + } + } + + // Verify 4-bit XOR checksum + let expectedChecksum = 0; + for (let i = 0; i < PixelLatencyProbe.DATA_BITS; i += 4) { + expectedChecksum ^= (seq >> i) & 0xf; + } + + let actualChecksum = 0; + for (let i = 0; i < PixelLatencyProbe.CHECKSUM_BITS; i++) { + const r = pixels[(20 + i) * 4]; + if (r >= 128) { + actualChecksum |= 1 << (PixelLatencyProbe.CHECKSUM_BITS - 1 - i); + } + } + + if (expectedChecksum !== actualChecksum) return "corrupted"; + + return seq; + } + + // ── Helpers ───────────────────────────────────────────────────────── + + private cleanUpOldProbes(): void { + const now = performance.now(); + for (const [s, t] of this.pendingProbes) { + if (now - t > PixelLatencyProbe.PROBE_TTL_MS) { + this.pendingProbes.delete(s); + this.stats.lost++; + this.onEvent({ + status: "lost", + seq: s, + e2eLatencyMs: null, + timestamp: Date.now(), + timeoutMs: PixelLatencyProbe.PROBE_TTL_MS, + }); + } + } + } +} diff --git a/packages/sdk/src/realtime/subscribe-client.ts b/packages/sdk/src/realtime/subscribe-client.ts index 6b1370f..c751018 100644 --- a/packages/sdk/src/realtime/subscribe-client.ts +++ b/packages/sdk/src/realtime/subscribe-client.ts @@ -6,10 +6,16 @@ type TokenPayload = { sid: string; ip: string; port: number; + transport?: "webrtc" | "ivs"; }; -export function encodeSubscribeToken(sessionId: string, serverIp: string, serverPort: number): string { - return btoa(JSON.stringify({ sid: sessionId, ip: serverIp, port: serverPort })); +export function encodeSubscribeToken( + sessionId: string, + serverIp: string, + serverPort: number, + transport?: "webrtc" | "ivs", +): string { + return btoa(JSON.stringify({ sid: sessionId, ip: serverIp, port: serverPort, transport })); } export function decodeSubscribeToken(token: string): TokenPayload { diff --git a/packages/sdk/src/realtime/telemetry-reporter.ts b/packages/sdk/src/realtime/telemetry-reporter.ts index a2a24bd..21c3b0b 100644 --- a/packages/sdk/src/realtime/telemetry-reporter.ts +++ b/packages/sdk/src/realtime/telemetry-reporter.ts @@ -34,6 +34,7 @@ export interface TelemetryReporterOptions { sessionId: string; model?: string; integration?: string; + transport?: "webrtc" | "ivs"; logger: Logger; reportIntervalMs?: number; } @@ -61,6 +62,7 @@ export class TelemetryReporter implements ITelemetryReporter { private sessionId: string; private model?: string; private integration?: string; + private transport?: "webrtc" | "ivs"; private logger: Logger; private reportIntervalMs: number; private intervalId: ReturnType | null = null; @@ -72,6 +74,7 @@ export class TelemetryReporter implements ITelemetryReporter { this.sessionId = options.sessionId; this.model = options.model; this.integration = options.integration; + this.transport = options.transport; this.logger = options.logger; this.reportIntervalMs = options.reportIntervalMs ?? DEFAULT_REPORT_INTERVAL_MS; } @@ -120,6 +123,7 @@ export class TelemetryReporter implements ITelemetryReporter { sdk_version: VERSION, ...(this.model ? { model: this.model } : {}), ...(this.integration ? { integration: this.integration } : {}), + ...(this.transport ? { transport: this.transport } : {}), }; return { diff --git a/packages/sdk/src/realtime/transport-manager.ts b/packages/sdk/src/realtime/transport-manager.ts new file mode 100644 index 0000000..1d6ce24 --- /dev/null +++ b/packages/sdk/src/realtime/transport-manager.ts @@ -0,0 +1,12 @@ +import type { Emitter } from "mitt"; +import type { ConnectionState, OutgoingMessage, WsMessageEvents } from "./types"; + +export interface RealtimeTransportManager { + connect(localStream: MediaStream | null): Promise; + sendMessage(message: OutgoingMessage): boolean; + setImage(imageBase64: string | null, options?: { prompt?: string; enhance?: boolean; timeout?: number }): Promise; + cleanup(): void; + isConnected(): boolean; + getConnectionState(): ConnectionState; + getWebsocketMessageEmitter(): Emitter; +} diff --git a/packages/sdk/src/realtime/types.ts b/packages/sdk/src/realtime/types.ts index e1618e8..3841178 100644 --- a/packages/sdk/src/realtime/types.ts +++ b/packages/sdk/src/realtime/types.ts @@ -71,6 +71,27 @@ export type SessionIdMessage = { server_port: number; }; +export type LatencyReportMessage = { + type: "latency_report"; + server_proxy_rtt_ms: number; + pipeline_latency_ms: number; +}; + +export type LatencyProbeMessage = { + type: "latency_probe"; + seq: number; + client_time: number; +}; + +export type E2ELatencyReportMessage = { + type: "e2e_latency_report"; + avg_latency_ms: number | null; + delivery_rate: number; + lost: number; + corrupted: number; + out_of_order: number; +}; + export type ConnectionState = "connecting" | "connected" | "generating" | "disconnected" | "reconnecting"; // Incoming message types (from server) @@ -85,7 +106,8 @@ export type IncomingWebRTCMessage = | GenerationStartedMessage | GenerationTickMessage | GenerationEndedMessage - | SessionIdMessage; + | SessionIdMessage + | LatencyReportMessage; // Outgoing message types (to server) export type OutgoingWebRTCMessage = @@ -93,6 +115,44 @@ export type OutgoingWebRTCMessage = | AnswerMessage | IceCandidateMessage | PromptMessage - | SetAvatarImageMessage; + | SetAvatarImageMessage + | LatencyProbeMessage + | E2ELatencyReportMessage; + +export type OutgoingMessage = PromptMessage | SetAvatarImageMessage | LatencyProbeMessage | E2ELatencyReportMessage; + +// IVS message types +export type IvsStageReadyMessage = { + type: "ivs_stage_ready"; + stage_arn: string; + client_publish_token: string; + client_subscribe_token: string; +}; + +export type IvsJoinedMessage = { + type: "ivs_joined"; +}; -export type OutgoingMessage = PromptMessage | SetAvatarImageMessage; +// IVS incoming messages (from bouncer) +export type IncomingIVSMessage = + | IvsStageReadyMessage + | PromptAckMessage + | ErrorMessage + | SetImageAckMessage + | GenerationStartedMessage + | GenerationTickMessage + | GenerationEndedMessage + | SessionIdMessage + | LatencyReportMessage; + +// IVS outgoing messages (to bouncer) +export type OutgoingIVSMessage = IvsJoinedMessage | PromptMessage | SetAvatarImageMessage | LatencyProbeMessage | E2ELatencyReportMessage; + +// Shared WebSocket message events (used by both WebRTC and IVS transports) +export type WsMessageEvents = { + promptAck: PromptAckMessage; + setImageAck: SetImageAckMessage; + sessionId: SessionIdMessage; + generationTick: GenerationTickMessage; + latencyReport: LatencyReportMessage; +}; diff --git a/packages/sdk/src/realtime/webrtc-connection.ts b/packages/sdk/src/realtime/webrtc-connection.ts index dc5802b..facbda1 100644 --- a/packages/sdk/src/realtime/webrtc-connection.ts +++ b/packages/sdk/src/realtime/webrtc-connection.ts @@ -5,12 +5,11 @@ import { buildUserAgent } from "../utils/user-agent"; import type { DiagnosticEmitter, IceCandidateEvent } from "./diagnostics"; import type { ConnectionState, - GenerationTickMessage, IncomingWebRTCMessage, OutgoingWebRTCMessage, PromptAckMessage, - SessionIdMessage, SetImageAckMessage, + WsMessageEvents, } from "./types"; const ICE_SERVERS: RTCIceServer[] = [{ urls: "stun:stun.l.google.com:19302" }]; @@ -30,13 +29,6 @@ interface ConnectionCallbacks { onDiagnostic?: DiagnosticEmitter; } -type WsMessageEvents = { - promptAck: PromptAckMessage; - setImageAck: SetImageAckMessage; - sessionId: SessionIdMessage; - generationTick: GenerationTickMessage; -}; - const noopDiagnostic: DiagnosticEmitter = () => {}; export class WebRTCConnection { @@ -254,6 +246,11 @@ export class WebRTCConnection { return; } + if (msg.type === "latency_report") { + this.websocketMessagesEmitter.emit("latencyReport", msg); + return; + } + // All other messages require peer connection if (!this.pc) return; diff --git a/packages/sdk/src/realtime/webrtc-manager.ts b/packages/sdk/src/realtime/webrtc-manager.ts index 71408fb..c986979 100644 --- a/packages/sdk/src/realtime/webrtc-manager.ts +++ b/packages/sdk/src/realtime/webrtc-manager.ts @@ -2,6 +2,7 @@ import pRetry, { AbortError } from "p-retry"; import type { Logger } from "../utils/logger"; import type { DiagnosticEmitter } from "./diagnostics"; +import type { RealtimeTransportManager } from "./transport-manager"; import type { ConnectionState, OutgoingMessage } from "./types"; import { WebRTCConnection } from "./webrtc-connection"; @@ -39,7 +40,7 @@ const RETRY_OPTIONS = { maxTimeout: 10000, } as const; -export class WebRTCManager { +export class WebRTCManager implements RealtimeTransportManager { private connection: WebRTCConnection; private config: WebRTCConfig; private logger: Logger; diff --git a/packages/sdk/src/realtime/webrtc-stats.ts b/packages/sdk/src/realtime/webrtc-stats.ts index 42319a4..ba18b96 100644 --- a/packages/sdk/src/realtime/webrtc-stats.ts +++ b/packages/sdk/src/realtime/webrtc-stats.ts @@ -22,6 +22,10 @@ export type WebRTCStats = { freezeCountDelta: number; /** Delta: freeze duration (seconds) since previous sample. */ freezeDurationDelta: number; + /** Cumulative NACK (retransmission request) count from inbound-rtp. */ + nackCount: number; + /** Delta: NACKs since previous sample (≈ NACK rate per polling interval). */ + nackCountDelta: number; } | null; audio: { bytesReceived: number; @@ -52,6 +56,11 @@ export type WebRTCStats = { currentRoundTripTime: number | null; /** Available outgoing bitrate estimate in bits/sec, or null if unavailable. */ availableOutgoingBitrate: number | null; + /** Selected candidate pairs from succeeded ICE negotiations (one per PeerConnection). */ + selectedCandidatePairs: Array<{ + local: { address: string; port: number; protocol: string; candidateType: string }; + remote: { address: string; port: number; protocol: string; candidateType: string }; + }>; }; }; @@ -63,9 +72,7 @@ export type StatsOptions = { const DEFAULT_INTERVAL_MS = 1000; const MIN_INTERVAL_MS = 500; -export class WebRTCStatsCollector { - private pc: RTCPeerConnection | null = null; - private intervalId: ReturnType | null = null; +export class StatsParser { private prevBytesVideo = 0; private prevBytesAudio = 0; private prevBytesSentVideo = 0; @@ -75,19 +82,11 @@ export class WebRTCStatsCollector { private prevFramesDropped = 0; private prevFreezeCount = 0; private prevFreezeDuration = 0; + private prevNackCount = 0; private prevPacketsLostAudio = 0; - private onStats: ((stats: WebRTCStats) => void) | null = null; - private intervalMs: number; - - constructor(options: StatsOptions = {}) { - this.intervalMs = Math.max(options.intervalMs ?? DEFAULT_INTERVAL_MS, MIN_INTERVAL_MS); - } - /** Attach to a peer connection and start polling. */ - start(pc: RTCPeerConnection, onStats: (stats: WebRTCStats) => void): void { - this.stop(); - this.pc = pc; - this.onStats = onStats; + /** Reset all delta-tracking state to zero. */ + reset(): void { this.prevBytesVideo = 0; this.prevBytesAudio = 0; this.prevBytesSentVideo = 0; @@ -96,47 +95,39 @@ export class WebRTCStatsCollector { this.prevFramesDropped = 0; this.prevFreezeCount = 0; this.prevFreezeDuration = 0; + this.prevNackCount = 0; this.prevPacketsLostAudio = 0; - this.intervalId = setInterval(() => this.collect(), this.intervalMs); - } - - /** Stop polling and release resources. */ - stop(): void { - if (this.intervalId !== null) { - clearInterval(this.intervalId); - this.intervalId = null; - } - this.pc = null; - this.onStats = null; - } - - isRunning(): boolean { - return this.intervalId !== null; } - private async collect(): Promise { - if (!this.pc || !this.onStats) return; - - try { - const rawStats = await this.pc.getStats(); - const stats = this.parse(rawStats); - this.onStats(stats); - } catch { - // PC might be closed; stop silently - this.stop(); - } - } - - private parse(rawStats: RTCStatsReport): WebRTCStats { + parse(rawStats: RTCStatsReport): WebRTCStats { const now = performance.now(); const elapsed = this.prevTimestamp > 0 ? (now - this.prevTimestamp) / 1000 : 0; let video: WebRTCStats["video"] = null; let audio: WebRTCStats["audio"] = null; let outboundVideo: WebRTCStats["outboundVideo"] = null; + // Pre-collect candidate entries so candidate-pair can reference them + type CandidateInfo = { address: string; port: number; protocol: string; candidateType: string }; + const candidateMap = new Map(); + rawStats.forEach((report) => { + if (report.type === "remote-candidate" || report.type === "local-candidate") { + const r = report as Record; + const addr = r.address as string | undefined; + if (addr) { + candidateMap.set(r.id as string, { + address: addr, + port: (r.port as number) ?? 0, + protocol: (r.protocol as string) ?? "udp", + candidateType: (r.candidateType as string) ?? "unknown", + }); + } + } + }); + const connection: WebRTCStats["connection"] = { currentRoundTripTime: null, availableOutgoingBitrate: null, + selectedCandidatePairs: [], }; rawStats.forEach((report) => { @@ -150,6 +141,7 @@ export class WebRTCStatsCollector { const framesDropped = (r.framesDropped as number) ?? 0; const freezeCount = (r.freezeCount as number) ?? 0; const freezeDuration = (r.totalFreezesDuration as number) ?? 0; + const nackCount = (r.nackCount as number) ?? 0; video = { framesDecoded: (r.framesDecoded as number) ?? 0, @@ -168,11 +160,14 @@ export class WebRTCStatsCollector { framesDroppedDelta: Math.max(0, framesDropped - this.prevFramesDropped), freezeCountDelta: Math.max(0, freezeCount - this.prevFreezeCount), freezeDurationDelta: Math.max(0, freezeDuration - this.prevFreezeDuration), + nackCount, + nackCountDelta: Math.max(0, nackCount - this.prevNackCount), }; this.prevPacketsLostVideo = packetsLost; this.prevFramesDropped = framesDropped; this.prevFreezeCount = freezeCount; this.prevFreezeDuration = freezeDuration; + this.prevNackCount = nackCount; } if (report.type === "outbound-rtp" && report.kind === "video") { @@ -216,6 +211,14 @@ export class WebRTCStatsCollector { if (r.state === "succeeded") { connection.currentRoundTripTime = (r.currentRoundTripTime as number) ?? null; connection.availableOutgoingBitrate = (r.availableOutgoingBitrate as number) ?? null; + // Resolve selected candidate pair + const localCandId = r.localCandidateId as string | undefined; + const remoteCandId = r.remoteCandidateId as string | undefined; + const local = localCandId ? candidateMap.get(localCandId) : undefined; + const remote = remoteCandId ? candidateMap.get(remoteCandId) : undefined; + if (local && remote) { + connection.selectedCandidatePairs.push({ local, remote }); + } } } }); @@ -231,3 +234,51 @@ export class WebRTCStatsCollector { }; } } + +export class WebRTCStatsCollector { + private pc: RTCPeerConnection | null = null; + private intervalId: ReturnType | null = null; + private parser = new StatsParser(); + private onStats: ((stats: WebRTCStats) => void) | null = null; + private intervalMs: number; + + constructor(options: StatsOptions = {}) { + this.intervalMs = Math.max(options.intervalMs ?? DEFAULT_INTERVAL_MS, MIN_INTERVAL_MS); + } + + /** Attach to a peer connection and start polling. */ + start(pc: RTCPeerConnection, onStats: (stats: WebRTCStats) => void): void { + this.stop(); + this.pc = pc; + this.onStats = onStats; + this.parser.reset(); + this.intervalId = setInterval(() => this.collect(), this.intervalMs); + } + + /** Stop polling and release resources. */ + stop(): void { + if (this.intervalId !== null) { + clearInterval(this.intervalId); + this.intervalId = null; + } + this.pc = null; + this.onStats = null; + } + + isRunning(): boolean { + return this.intervalId !== null; + } + + private async collect(): Promise { + if (!this.pc || !this.onStats) return; + + try { + const rawStats = await this.pc.getStats(); + const stats = this.parser.parse(rawStats); + this.onStats(stats); + } catch { + // PC might be closed; stop silently + this.stop(); + } + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a766cc3..55c5e0f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -149,6 +149,9 @@ importers: '@decartai/sdk': specifier: workspace:* version: link:../../packages/sdk + amazon-ivs-web-broadcast: + specifier: ^1.14.0 + version: 1.32.0 react: specifier: ^19.0.0 version: 19.2.1 @@ -313,10 +316,13 @@ importers: version: 5.9.2 vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@22.17.1)(@vitest/browser@3.2.4)(jiti@2.5.1)(msw@2.11.3(@types/node@22.17.1)(typescript@5.9.2))(tsx@4.21.0)(yaml@2.8.1) + version: 3.2.4(@types/node@22.17.1)(@vitest/browser@3.2.4)(jiti@2.5.1)(jsdom@26.1.0)(msw@2.11.3(@types/node@22.17.1)(typescript@5.9.2))(tsx@4.21.0)(yaml@2.8.1) packages/sdk: dependencies: + amazon-ivs-web-broadcast: + specifier: '>=1.14.0' + version: 1.32.0 mitt: specifier: ^3.0.1 version: 3.0.1 @@ -362,10 +368,13 @@ importers: version: 7.1.2(@types/node@22.17.1)(jiti@2.5.1)(tsx@4.21.0)(yaml@2.8.1) vitest: specifier: ^4.0.18 - version: 4.0.18(@types/node@22.17.1)(@vitest/browser-playwright@4.0.18)(jiti@2.5.1)(msw@2.11.3(@types/node@22.17.1)(typescript@5.9.2))(tsx@4.21.0)(yaml@2.8.1) + version: 4.0.18(@types/node@22.17.1)(@vitest/browser-playwright@4.0.18)(jiti@2.5.1)(jsdom@26.1.0)(msw@2.11.3(@types/node@22.17.1)(typescript@5.9.2))(tsx@4.21.0)(yaml@2.8.1) packages: + '@asamuzakjp/css-color@3.2.0': + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -578,6 +587,34 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + '@emnapi/core@1.7.1': resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} @@ -1322,6 +1359,22 @@ packages: '@types/node': optional: true + '@jest/environment@29.7.0': + resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/fake-timers@29.7.0': + resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/schemas@29.6.3': + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/types@29.6.3': + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -1535,8 +1588,8 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} - '@oxc-project/types@0.114.0': - resolution: {integrity: sha512-//nBfbzHQHvJs8oFIjv6coZ6uxQ4alLfiPe6D5vit6c4pmxATHHlVwgB1k+Hv4yoAMyncdxgRBF5K4BYWUCzvA==} + '@oxc-project/types@0.115.0': + resolution: {integrity: sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==} '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -1544,79 +1597,91 @@ packages: '@quansync/fs@0.1.4': resolution: {integrity: sha512-vy/41FCdnIalPTQCb2Wl0ic1caMdzGus4ktDp+gpZesQNydXcx8nhh8qB3qMPbGkictOTaXgXEUUfQEm8DQYoA==} - '@rolldown/binding-android-arm64@1.0.0-rc.5': - resolution: {integrity: sha512-zCEmUrt1bggwgBgeKLxNj217J1OrChrp3jJt24VK9jAharSTeVaHODNL+LpcQVhRz+FktYWfT9cjo5oZ99ZLpg==} + '@rolldown/binding-android-arm64@1.0.0-rc.9': + resolution: {integrity: sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.0-rc.5': - resolution: {integrity: sha512-ZP9xb9lPAex36pvkNWCjSEJW/Gfdm9I3ssiqOFLmpZ/vosPXgpoGxCmh+dX1Qs+/bWQE6toNFXWWL8vYoKoK9Q==} + '@rolldown/binding-darwin-arm64@1.0.0-rc.9': + resolution: {integrity: sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-rc.5': - resolution: {integrity: sha512-7IdrPunf6dp9mywMgTOKMMGDnMHQ6+h5gRl6LW8rhD8WK2kXX0IwzcM5Zc0B5J7xQs8QWOlKjv8BJsU/1CD3pg==} + '@rolldown/binding-darwin-x64@1.0.0-rc.9': + resolution: {integrity: sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.0-rc.5': - resolution: {integrity: sha512-o/JCk+dL0IN68EBhZ4DqfsfvxPfMeoM6cJtxORC1YYoxGHZyth2Kb2maXDb4oddw2wu8iIbnYXYPEzBtAF5CAg==} + '@rolldown/binding-freebsd-x64@1.0.0-rc.9': + resolution: {integrity: sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.5': - resolution: {integrity: sha512-IIBwTtA6VwxQLcEgq2mfrUgam7VvPZjhd/jxmeS1npM+edWsrrpRLHUdze+sk4rhb8/xpP3flemgcZXXUW6ukw==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.9': + resolution: {integrity: sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.5': - resolution: {integrity: sha512-KSol1De1spMZL+Xg7K5IBWXIvRWv7+pveaxFWXpezezAG7CS6ojzRjtCGCiLxQricutTAi/LkNWKMsd2wNhMKQ==} + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.9': + resolution: {integrity: sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.5': - resolution: {integrity: sha512-WFljyDkxtXRlWxMjxeegf7xMYXxUr8u7JdXlOEWKYgDqEgxUnSEsVDxBiNWQ1D5kQKwf8Wo4sVKEYPRhCdsjwA==} + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.9': + resolution: {integrity: sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.5': - resolution: {integrity: sha512-CUlplTujmbDWp2gamvrqVKi2Or8lmngXT1WxsizJfts7JrvfGhZObciaY/+CbdbS9qNnskvwMZNEhTPrn7b+WA==} + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.9': + resolution: {integrity: sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.9': + resolution: {integrity: sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.9': + resolution: {integrity: sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@rolldown/binding-linux-x64-musl@1.0.0-rc.5': - resolution: {integrity: sha512-wdf7g9NbVZCeAo2iGhsjJb7I8ZFfs6X8bumfrWg82VK+8P6AlLXwk48a1ASiJQDTS7Svq2xVzZg3sGO2aXpHRA==} + '@rolldown/binding-linux-x64-musl@1.0.0-rc.9': + resolution: {integrity: sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@rolldown/binding-openharmony-arm64@1.0.0-rc.5': - resolution: {integrity: sha512-0CWY7ubu12nhzz+tkpHjoG3IRSTlWYe0wrfJRf4qqjqQSGtAYgoL9kwzdvlhaFdZ5ffVeyYw9qLsChcjUMEloQ==} + '@rolldown/binding-openharmony-arm64@1.0.0-rc.9': + resolution: {integrity: sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.0-rc.5': - resolution: {integrity: sha512-LztXnGzv6t2u830mnZrFLRVqT/DPJ9DL4ZTz/y93rqUVkeHjMMYIYaFj+BUthiYxbVH9dH0SZYufETspKY/NhA==} + '@rolldown/binding-wasm32-wasi@1.0.0-rc.9': + resolution: {integrity: sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.5': - resolution: {integrity: sha512-jUct1XVeGtyjqJXEAfvdFa8xoigYZ2rge7nYEm70ppQxpfH9ze2fbIrpHmP2tNM2vL/F6Dd0CpXhpjPbC6bSxQ==} + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.9': + resolution: {integrity: sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.5': - resolution: {integrity: sha512-VQ8F9ld5gw29epjnVGdrx8ugiLTe8BMqmhDYy7nGbdeDo4HAt4bgdZvLbViEhg7DZyHLpiEUlO5/jPSUrIuxRQ==} + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.9': + resolution: {integrity: sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -1627,8 +1692,8 @@ packages: '@rolldown/pluginutils@1.0.0-beta.40': resolution: {integrity: sha512-s3GeJKSQOwBlzdUrj4ISjJj5SfSh+aqn0wjOar4Bx95iV1ETI7F6S/5hLcfAxZ9kXDcyrAkxPlqmd1ZITttf+w==} - '@rolldown/pluginutils@1.0.0-rc.5': - resolution: {integrity: sha512-RxlLX/DPoarZ9PtxVrQgZhPoor987YtKQqCo5zkjX+0S0yLJ7Vv515Wk6+xtTL67VONKJKxETWZwuZjss2idYw==} + '@rolldown/pluginutils@1.0.0-rc.9': + resolution: {integrity: sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==} '@rollup/rollup-android-arm-eabi@4.46.2': resolution: {integrity: sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==} @@ -1730,6 +1795,15 @@ packages: cpu: [x64] os: [win32] + '@sinclair/typebox@0.27.10': + resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==} + + '@sinonjs/commons@3.0.1': + resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + + '@sinonjs/fake-timers@10.3.0': + resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -1847,6 +1921,10 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' + '@tootallnate/once@2.0.0': + resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} + engines: {node: '>= 10'} + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -1898,6 +1976,18 @@ packages: '@types/http-errors@2.0.5': resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + '@types/istanbul-lib-coverage@2.0.6': + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + '@types/istanbul-lib-report@3.0.3': + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + + '@types/istanbul-reports@3.0.4': + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + + '@types/jsdom@20.0.1': + resolution: {integrity: sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==} + '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} @@ -1933,12 +2023,24 @@ packages: '@types/serve-static@1.15.10': resolution: {integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==} + '@types/stack-utils@2.0.3': + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + '@types/statuses@2.0.6': resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} + '@types/tough-cookie@4.0.5': + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + '@types/yargs@17.0.35': + resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} + '@vitejs/plugin-react@4.7.0': resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} engines: {node: ^14.18.0 || >=16.0.0} @@ -2029,10 +2131,17 @@ packages: '@vitest/utils@4.0.18': resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} + abab@2.0.6: + resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} + deprecated: Use your platform's native atob() and btoa() methods instead + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} + acorn-globals@7.0.1: + resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} + acorn-walk@8.3.2: resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} engines: {node: '>=0.4.0'} @@ -2047,6 +2156,17 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + amazon-ivs-web-broadcast@1.32.0: + resolution: {integrity: sha512-ajE7S50WJfkwYzlGsy4fiZ25rNHlNs+5XB1BwB6EJD6fdyDXItbaA2GUfvE/bM4TcVexw6O7xr0GeGlStKpFkg==} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -2094,6 +2214,9 @@ packages: resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} engines: {node: '>=4'} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + babel-dead-code-elimination@1.0.12: resolution: {integrity: sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==} @@ -2121,6 +2244,9 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -2182,6 +2308,10 @@ packages: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + check-error@2.1.1: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} @@ -2201,6 +2331,10 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} + ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + citty@0.1.6: resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} @@ -2229,6 +2363,10 @@ packages: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} engines: {node: '>=12.5.0'} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} @@ -2271,12 +2409,34 @@ packages: resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} engines: {node: '>= 6'} + cssom@0.3.8: + resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==} + + cssom@0.5.0: + resolution: {integrity: sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==} + + cssstyle@2.3.0: + resolution: {integrity: sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==} + engines: {node: '>=8'} + + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} + engines: {node: '>=18'} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} data-uri-to-buffer@2.0.2: resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==} + data-urls@3.0.2: + resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==} + engines: {node: '>=12'} + + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -2294,6 +2454,9 @@ packages: supports-color: optional: true + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + decode-uri-component@0.4.1: resolution: {integrity: sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==} engines: {node: '>=14.16'} @@ -2305,6 +2468,10 @@ packages: defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -2340,6 +2507,11 @@ packages: domelementtype@2.3.0: resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + domexception@4.0.0: + resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} + engines: {node: '>=12'} + deprecated: Use your platform's native DOMException instead + domhandler@5.0.3: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} @@ -2419,6 +2591,10 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + esbuild@0.17.19: resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==} engines: {node: '>=12'} @@ -2441,25 +2617,45 @@ packages: escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + estree-walker@0.6.1: resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==} estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + etag@1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + exit-hook@2.2.1: resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==} engines: {node: '>=6'} @@ -2496,6 +2692,10 @@ packages: resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} engines: {node: '>= 0.8'} + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -2557,6 +2757,9 @@ packages: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + graphql@16.11.0: resolution: {integrity: sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} @@ -2571,10 +2774,18 @@ packages: crossws: optional: true + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -2589,6 +2800,14 @@ packages: hookable@5.5.3: resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + html-encoding-sniffer@3.0.0: + resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} + engines: {node: '>=12'} + + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + htmlparser2@10.1.0: resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} @@ -2600,6 +2819,22 @@ packages: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} + http-proxy-agent@5.0.0: + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -2649,6 +2884,9 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + isbinaryfile@5.0.4: resolution: {integrity: sha512-YKBKVkKhty7s8rxddb40oOkuP0NbaeXrQvLin6QMHL7Ypiy2RW9LwOVrVgZRyOrhQlayMd9t+D8yDy8MKFTSDQ==} engines: {node: '>= 18.0.0'} @@ -2660,6 +2898,27 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + jest-environment-jsdom@29.7.0: + resolution: {integrity: sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + + jest-message-util@29.7.0: + resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-mock@29.7.0: + resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jiti@2.5.1: resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==} hasBin: true @@ -2674,6 +2933,24 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + jsdom@20.0.3: + resolution: {integrity: sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==} + engines: {node: '>=14'} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + + jsdom@26.1.0: + resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -2687,9 +2964,15 @@ packages: jsonc-parser@3.3.1: resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + lodash@4.17.23: + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + loupe@3.2.0: resolution: {integrity: sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw==} + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -2721,6 +3004,10 @@ packages: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -2843,6 +3130,9 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + nwsapi@2.2.23: + resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} + nypm@0.6.1: resolution: {integrity: sha512-hlacBiRiv1k9hZFiphPUkfSQ/ZfQzZDzC+8z0wL3lvDAOUu/2NnChkKuMoMjNur/9OpKuz2QsIeiPVN0xM5Q0w==} engines: {node: ^14.16.0 || >=16.10.0} @@ -2964,6 +3254,10 @@ packages: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + printable-characters@1.0.42: resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==} @@ -2971,6 +3265,13 @@ packages: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + qs@6.14.0: resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} engines: {node: '>=0.6'} @@ -2986,6 +3287,9 @@ packages: resolution: {integrity: sha512-pDSIZJ9sFuOp6VnD+5IkakSVf+rICAuuU88Hcsr6AKL0QtxSIfVuKiVP2oahFI7tk3CRSexwV+Ya6MOoTxzg9g==} engines: {node: '>=18'} + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + quick-lru@7.1.0: resolution: {integrity: sha512-Pzd/4IFnTb8E+I1P5rbLQoqpUHcXKg48qTYKi4EANg+sTPwGFEMOcYGiiZz6xuQcOMZP7MPsrdAPx+16Q8qahg==} engines: {node: '>=18'} @@ -3014,6 +3318,9 @@ packages: react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react-refresh@0.17.0: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} @@ -3038,10 +3345,16 @@ packages: resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} engines: {node: '>= 4'} + reflect-metadata@0.2.2: + resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -3068,8 +3381,8 @@ packages: vue-tsc: optional: true - rolldown@1.0.0-rc.5: - resolution: {integrity: sha512-0AdalTs6hNTioaCYIkAa7+xsmHBfU5hCNclZnM/lp7lGGDuUOb6N4BVNtwiomybbencDjq/waKjTImqiGCs5sw==} + rolldown@1.0.0-rc.9: + resolution: {integrity: sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -3091,15 +3404,29 @@ packages: rou3@0.7.12: resolution: {integrity: sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==} + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + sdp-transform@2.15.0: + resolution: {integrity: sha512-KrOH82c/W+GYQ0LHqtr3caRpM3ITglq3ljGUIb8LTki7ByacJZ9z+piSGiwZDsRyhQbYBOBJgr2k6X4BZXi3Kw==} + hasBin: true + + sdp@3.2.1: + resolution: {integrity: sha512-lwsAIzOPlH8/7IIjjz3K0zYBk7aBVVcvjMwt3M4fLxpjMYyy7i3I97SLHebgn4YBjirkzfp3RvRDWSKsh/+WFw==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -3180,6 +3507,10 @@ packages: resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} engines: {node: '>=18'} + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -3205,6 +3536,10 @@ packages: engines: {node: '>=20.16.0'} hasBin: true + stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -3260,6 +3595,13 @@ packages: babel-plugin-macros: optional: true + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} @@ -3303,9 +3645,16 @@ packages: resolution: {integrity: sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==} engines: {node: '>=14.0.0'} + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + tldts-core@7.0.16: resolution: {integrity: sha512-XHhPmHxphLi+LGbH0G/O7dmUH9V65OY20R7vH8gETHsp5AZCjBk9l8sqmRKLaGOxnETU7XNSDUPtewAy/K6jbA==} + tldts@6.1.86: + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + hasBin: true + tldts@7.0.16: resolution: {integrity: sha512-5bdPHSwbKTeHmXrgecID4Ljff8rQjv7g8zKQPkCozRo2HWWni+p310FSn5ImI+9kWw9kK4lzOB5q/a6iv0IJsw==} hasBin: true @@ -3322,10 +3671,26 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + + tough-cookie@5.1.2: + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} + tough-cookie@6.0.0: resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} engines: {node: '>=16'} + tr46@3.0.0: + resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} + engines: {node: '>=12'} + + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -3370,6 +3735,10 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + type-detect@4.1.0: resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} engines: {node: '>=4'} @@ -3422,6 +3791,10 @@ packages: universal-user-agent@6.0.1: resolution: {integrity: sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==} + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} @@ -3443,6 +3816,9 @@ packages: resolution: {integrity: sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + use-sync-external-store@1.6.0: resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} peerDependencies: @@ -3663,18 +4039,51 @@ packages: jsdom: optional: true + w3c-xmlserializer@4.0.0: + resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} + engines: {node: '>=14'} + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + webrtc-adapter@8.2.4: + resolution: {integrity: sha512-VwtwbYNKnVQW8koB9qb8YcxNwpSVHTvvKEZLzY6uQ3gFrA9E87VPbB5xE+m1AGwUjL1UgN35jRR9hQgteZI5bg==} + engines: {node: '>=6.0.0', npm: '>=3.10.0'} + + whatwg-encoding@2.0.0: + resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} + engines: {node: '>=12'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + whatwg-mimetype@4.0.0: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} engines: {node: '>=18'} + whatwg-url@11.0.0: + resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==} + engines: {node: '>=12'} + + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -3735,10 +4144,21 @@ packages: utf-8-validate: optional: true + xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + xmlbuilder2@4.0.3: resolution: {integrity: sha512-bx8Q1STctnNaaDymWnkfQLKofs0mGNN7rLLapJlGuV3VlvegD7Ls4ggMjE3aUSWItCCzU0PEv45lI87iSigiCA==} engines: {node: '>=20.0'} + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -3781,6 +4201,14 @@ packages: snapshots: + '@asamuzakjp/css-color@3.2.0': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 10.4.3 + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -3982,6 +4410,26 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 + '@csstools/color-helpers@5.1.0': {} + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-tokenizer@3.0.4': {} + '@emnapi/core@1.7.1': dependencies: '@emnapi/wasi-threads': 1.1.0 @@ -4432,6 +4880,35 @@ snapshots: optionalDependencies: '@types/node': 22.17.1 + '@jest/environment@29.7.0': + dependencies: + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.17.1 + jest-mock: 29.7.0 + + '@jest/fake-timers@29.7.0': + dependencies: + '@jest/types': 29.6.3 + '@sinonjs/fake-timers': 10.3.0 + '@types/node': 22.17.1 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + '@jest/schemas@29.6.3': + dependencies: + '@sinclair/typebox': 0.27.10 + + '@jest/types@29.6.3': + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 22.17.1 + '@types/yargs': 17.0.35 + chalk: 4.1.2 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -4629,7 +5106,7 @@ snapshots: '@open-draft/until@2.1.0': {} - '@oxc-project/types@0.114.0': {} + '@oxc-project/types@0.115.0': {} '@polka/url@1.0.0-next.29': {} @@ -4637,52 +5114,58 @@ snapshots: dependencies: quansync: 0.2.10 - '@rolldown/binding-android-arm64@1.0.0-rc.5': + '@rolldown/binding-android-arm64@1.0.0-rc.9': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.9': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-rc.5': + '@rolldown/binding-darwin-x64@1.0.0-rc.9': optional: true - '@rolldown/binding-darwin-x64@1.0.0-rc.5': + '@rolldown/binding-freebsd-x64@1.0.0-rc.9': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-rc.5': + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.9': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.5': + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.9': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.5': + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.9': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.5': + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.9': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.5': + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.9': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-rc.5': + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.9': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-rc.5': + '@rolldown/binding-linux-x64-musl@1.0.0-rc.9': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-rc.5': + '@rolldown/binding-openharmony-arm64@1.0.0-rc.9': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.9': dependencies: '@napi-rs/wasm-runtime': 1.1.1 optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.5': + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.9': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.5': + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.9': optional: true '@rolldown/pluginutils@1.0.0-beta.27': {} '@rolldown/pluginutils@1.0.0-beta.40': {} - '@rolldown/pluginutils@1.0.0-rc.5': {} + '@rolldown/pluginutils@1.0.0-rc.9': {} '@rollup/rollup-android-arm-eabi@4.46.2': optional: true @@ -4744,6 +5227,16 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.46.2': optional: true + '@sinclair/typebox@0.27.10': {} + + '@sinonjs/commons@3.0.1': + dependencies: + type-detect: 4.0.8 + + '@sinonjs/fake-timers@10.3.0': + dependencies: + '@sinonjs/commons': 3.0.1 + '@standard-schema/spec@1.1.0': {} '@swc/helpers@0.5.15': @@ -4948,6 +5441,8 @@ snapshots: '@testing-library/dom': 10.4.1 optional: true + '@tootallnate/once@2.0.0': {} + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 @@ -5020,6 +5515,22 @@ snapshots: '@types/http-errors@2.0.5': {} + '@types/istanbul-lib-coverage@2.0.6': {} + + '@types/istanbul-lib-report@3.0.3': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + + '@types/istanbul-reports@3.0.4': + dependencies: + '@types/istanbul-lib-report': 3.0.3 + + '@types/jsdom@20.0.1': + dependencies: + '@types/node': 22.17.1 + '@types/tough-cookie': 4.0.5 + parse5: 7.3.0 + '@types/mime@1.3.5': {} '@types/node@22.17.1': @@ -5059,12 +5570,22 @@ snapshots: '@types/node': 22.17.1 '@types/send': 0.17.6 + '@types/stack-utils@2.0.3': {} + '@types/statuses@2.0.6': {} + '@types/tough-cookie@4.0.5': {} + '@types/ws@8.18.1': dependencies: '@types/node': 22.17.1 + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@17.0.35': + dependencies: + '@types/yargs-parser': 21.0.3 + '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@25.0.3)(jiti@2.5.1)(tsx@4.21.0)(yaml@2.8.1))': dependencies: '@babel/core': 7.28.5 @@ -5095,7 +5616,7 @@ snapshots: '@vitest/mocker': 4.0.18(msw@2.11.3(@types/node@22.17.1)(typescript@5.9.2))(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(tsx@4.21.0)(yaml@2.8.1)) playwright: 1.58.2 tinyrainbow: 3.0.3 - vitest: 4.0.18(@types/node@22.17.1)(@vitest/browser-playwright@4.0.18)(jiti@2.5.1)(msw@2.11.3(@types/node@22.17.1)(typescript@5.9.2))(tsx@4.21.0)(yaml@2.8.1) + vitest: 4.0.18(@types/node@22.17.1)(@vitest/browser-playwright@4.0.18)(jiti@2.5.1)(jsdom@26.1.0)(msw@2.11.3(@types/node@22.17.1)(typescript@5.9.2))(tsx@4.21.0)(yaml@2.8.1) transitivePeerDependencies: - bufferutil - msw @@ -5111,7 +5632,7 @@ snapshots: magic-string: 0.30.21 sirv: 3.0.2 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@22.17.1)(@vitest/browser@3.2.4)(jiti@2.5.1)(msw@2.11.3(@types/node@22.17.1)(typescript@5.9.2))(tsx@4.21.0)(yaml@2.8.1) + vitest: 3.2.4(@types/node@22.17.1)(@vitest/browser@3.2.4)(jiti@2.5.1)(jsdom@26.1.0)(msw@2.11.3(@types/node@22.17.1)(typescript@5.9.2))(tsx@4.21.0)(yaml@2.8.1) ws: 8.19.0 optionalDependencies: playwright: 1.58.2 @@ -5131,7 +5652,7 @@ snapshots: pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.0.3 - vitest: 4.0.18(@types/node@22.17.1)(@vitest/browser-playwright@4.0.18)(jiti@2.5.1)(msw@2.11.3(@types/node@22.17.1)(typescript@5.9.2))(tsx@4.21.0)(yaml@2.8.1) + vitest: 4.0.18(@types/node@22.17.1)(@vitest/browser-playwright@4.0.18)(jiti@2.5.1)(jsdom@26.1.0)(msw@2.11.3(@types/node@22.17.1)(typescript@5.9.2))(tsx@4.21.0)(yaml@2.8.1) ws: 8.19.0 transitivePeerDependencies: - bufferutil @@ -5231,25 +5752,55 @@ snapshots: '@vitest/pretty-format': 4.0.18 tinyrainbow: 3.0.3 + abab@2.0.6: {} + accepts@1.3.8: dependencies: mime-types: 2.1.35 negotiator: 0.6.3 + acorn-globals@7.0.1: + dependencies: + acorn: 8.15.0 + acorn-walk: 8.3.2 + acorn-walk@8.3.2: {} acorn@8.14.0: {} acorn@8.15.0: {} + agent-base@6.0.2: + dependencies: + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + agent-base@7.1.4: {} + + amazon-ivs-web-broadcast@1.32.0: + dependencies: + bowser: 2.14.1 + eventemitter3: 4.0.7 + jest-environment-jsdom: 29.7.0 + jsdom: 26.1.0 + lodash: 4.17.23 + reflect-metadata: 0.2.2 + sdp-transform: 2.15.0 + webrtc-adapter: 8.2.4 + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - utf-8-validate + ansi-regex@5.0.1: {} ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 - ansi-styles@5.2.0: - optional: true + ansi-styles@5.2.0: {} ansis@4.1.0: {} @@ -5284,6 +5835,8 @@ snapshots: dependencies: tslib: 2.8.1 + asynckit@0.4.0: {} + babel-dead-code-elimination@1.0.12: dependencies: '@babel/core': 7.28.5 @@ -5322,6 +5875,8 @@ snapshots: boolbase@1.0.0: {} + bowser@2.14.1: {} + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -5403,6 +5958,11 @@ snapshots: chai@6.2.2: {} + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + check-error@2.1.1: {} cheerio-select@2.1.0: @@ -5444,6 +6004,8 @@ snapshots: dependencies: readdirp: 4.1.2 + ci-info@3.9.0: {} + citty@0.1.6: dependencies: consola: 3.4.2 @@ -5476,6 +6038,10 @@ snapshots: color-string: 1.9.1 optional: true + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + confbox@0.1.8: {} confbox@0.2.2: {} @@ -5512,10 +6078,34 @@ snapshots: css-what@6.2.2: {} + cssom@0.3.8: {} + + cssom@0.5.0: {} + + cssstyle@2.3.0: + dependencies: + cssom: 0.3.8 + + cssstyle@4.6.0: + dependencies: + '@asamuzakjp/css-color': 3.2.0 + rrweb-cssom: 0.8.0 + csstype@3.2.3: {} data-uri-to-buffer@2.0.2: {} + data-urls@3.0.2: + dependencies: + abab: 2.0.6 + whatwg-mimetype: 3.0.0 + whatwg-url: 11.0.0 + + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + debug@2.6.9: dependencies: ms: 2.0.0 @@ -5524,12 +6114,16 @@ snapshots: dependencies: ms: 2.1.3 + decimal.js@10.6.0: {} + decode-uri-component@0.4.1: {} deep-eql@5.0.2: {} defu@6.1.4: {} + delayed-stream@1.0.0: {} + depd@2.0.0: {} deprecation@2.3.1: {} @@ -5557,6 +6151,10 @@ snapshots: domelementtype@2.3.0: {} + domexception@4.0.0: + dependencies: + webidl-conversions: 7.0.0 + domhandler@5.0.3: dependencies: domelementtype: 2.3.0 @@ -5612,6 +6210,13 @@ snapshots: dependencies: es-errors: 1.3.0 + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + esbuild@0.17.19: optionalDependencies: '@esbuild/android-arm': 0.17.19 @@ -5699,18 +6304,34 @@ snapshots: escape-html@1.0.3: {} + escape-string-regexp@2.0.0: {} + escape-string-regexp@4.0.0: {} + escodegen@2.1.0: + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + esprima@4.0.1: {} + estraverse@5.3.0: {} + estree-walker@0.6.1: {} estree-walker@3.0.3: dependencies: '@types/estree': 1.0.8 + esutils@2.0.3: {} + etag@1.8.1: {} + eventemitter3@4.0.7: {} + exit-hook@2.2.1: {} expect-type@1.2.2: {} @@ -5775,6 +6396,14 @@ snapshots: transitivePeerDependencies: - supports-color + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + forwarded@0.2.0: {} fresh@0.5.2: {} @@ -5837,6 +6466,8 @@ snapshots: gopd@1.2.0: {} + graceful-fs@4.2.11: {} + graphql@16.11.0: {} h3@2.0.1-rc.14: @@ -5844,8 +6475,14 @@ snapshots: rou3: 0.7.12 srvx: 0.11.2 + has-flag@4.0.0: {} + has-symbols@1.1.0: {} + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -5856,6 +6493,14 @@ snapshots: hookable@5.5.3: {} + html-encoding-sniffer@3.0.0: + dependencies: + whatwg-encoding: 2.0.0 + + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + htmlparser2@10.1.0: dependencies: domelementtype: 2.3.0 @@ -5879,6 +6524,35 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 + http-proxy-agent@5.0.0: + dependencies: + '@tootallnate/once': 2.0.0 + agent-base: 6.0.2 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 @@ -5914,12 +6588,56 @@ snapshots: is-number@7.0.0: {} + is-potential-custom-element-name@1.0.1: {} + isbinaryfile@5.0.4: {} isbot@5.1.34: {} isexe@2.0.0: {} + jest-environment-jsdom@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/jsdom': 20.0.1 + '@types/node': 22.17.1 + jest-mock: 29.7.0 + jest-util: 29.7.0 + jsdom: 20.0.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jest-message-util@29.7.0: + dependencies: + '@babel/code-frame': 7.27.1 + '@jest/types': 29.6.3 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + stack-utils: 2.0.6 + + jest-mock@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 22.17.1 + jest-util: 29.7.0 + + jest-util@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 22.17.1 + chalk: 4.1.2 + ci-info: 3.9.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 + jiti@2.5.1: {} js-tokens@4.0.0: {} @@ -5930,14 +6648,78 @@ snapshots: dependencies: argparse: 2.0.1 + jsdom@20.0.3: + dependencies: + abab: 2.0.6 + acorn: 8.15.0 + acorn-globals: 7.0.1 + cssom: 0.5.0 + cssstyle: 2.3.0 + data-urls: 3.0.2 + decimal.js: 10.6.0 + domexception: 4.0.0 + escodegen: 2.1.0 + form-data: 4.0.5 + html-encoding-sniffer: 3.0.0 + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.23 + parse5: 7.3.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 4.1.4 + w3c-xmlserializer: 4.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 2.0.0 + whatwg-mimetype: 3.0.0 + whatwg-url: 11.0.0 + ws: 8.19.0 + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jsdom@26.1.0: + dependencies: + cssstyle: 4.6.0 + data-urls: 5.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.23 + parse5: 7.3.0 + rrweb-cssom: 0.8.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.1.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.19.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + jsesc@3.1.0: {} json5@2.2.3: {} jsonc-parser@3.3.1: {} + lodash@4.17.23: {} + loupe@3.2.0: {} + lru-cache@10.4.3: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -5965,6 +6747,11 @@ snapshots: methods@1.1.2: {} + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + mime-db@1.52.0: {} mime-types@2.1.35: @@ -6098,6 +6885,8 @@ snapshots: dependencies: boolbase: 1.0.0 + nwsapi@2.2.23: {} + nypm@0.6.1: dependencies: citty: 0.1.6 @@ -6220,6 +7009,12 @@ snapshots: react-is: 17.0.2 optional: true + pretty-format@29.7.0: + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.3.1 + printable-characters@1.0.42: {} proxy-addr@2.0.7: @@ -6227,6 +7022,12 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 + psl@1.15.0: + dependencies: + punycode: 2.3.1 + + punycode@2.3.1: {} + qs@6.14.0: dependencies: side-channel: 1.1.0 @@ -6248,6 +7049,8 @@ snapshots: filter-obj: 5.1.0 split-on-first: 3.0.0 + querystringify@2.2.0: {} + quick-lru@7.1.0: {} range-parser@1.2.1: {} @@ -6277,6 +7080,8 @@ snapshots: react-is@17.0.2: optional: true + react-is@18.3.1: {} + react-refresh@0.17.0: {} react@19.2.1: {} @@ -6297,15 +7102,19 @@ snapshots: tiny-invariant: 1.3.3 tslib: 2.8.1 + reflect-metadata@0.2.2: {} + require-directory@2.1.1: {} + requires-port@1.0.0: {} + resolve-pkg-maps@1.0.0: {} retry@0.13.1: {} rettime@0.7.0: {} - rolldown-plugin-dts@0.15.6(rolldown@1.0.0-rc.5)(typescript@5.9.2): + rolldown-plugin-dts@0.15.6(rolldown@1.0.0-rc.9)(typescript@5.9.2): dependencies: '@babel/generator': 7.28.5 '@babel/parser': 7.28.5 @@ -6315,31 +7124,33 @@ snapshots: debug: 4.4.1 dts-resolver: 2.1.1 get-tsconfig: 4.10.1 - rolldown: 1.0.0-rc.5 + rolldown: 1.0.0-rc.9 optionalDependencies: typescript: 5.9.2 transitivePeerDependencies: - oxc-resolver - supports-color - rolldown@1.0.0-rc.5: + rolldown@1.0.0-rc.9: dependencies: - '@oxc-project/types': 0.114.0 - '@rolldown/pluginutils': 1.0.0-rc.5 + '@oxc-project/types': 0.115.0 + '@rolldown/pluginutils': 1.0.0-rc.9 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-rc.5 - '@rolldown/binding-darwin-arm64': 1.0.0-rc.5 - '@rolldown/binding-darwin-x64': 1.0.0-rc.5 - '@rolldown/binding-freebsd-x64': 1.0.0-rc.5 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.5 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.5 - '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.5 - '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.5 - '@rolldown/binding-linux-x64-musl': 1.0.0-rc.5 - '@rolldown/binding-openharmony-arm64': 1.0.0-rc.5 - '@rolldown/binding-wasm32-wasi': 1.0.0-rc.5 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.5 - '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.5 + '@rolldown/binding-android-arm64': 1.0.0-rc.9 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.9 + '@rolldown/binding-darwin-x64': 1.0.0-rc.9 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.9 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.9 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.9 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.9 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.9 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.9 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.9 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.9 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.9 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.9 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.9 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.9 rollup-plugin-inject@3.0.2: dependencies: @@ -6383,12 +7194,22 @@ snapshots: rou3@0.7.12: {} + rrweb-cssom@0.8.0: {} + safe-buffer@5.2.1: {} safer-buffer@2.1.2: {} + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + scheduler@0.27.0: {} + sdp-transform@2.15.0: {} + + sdp@3.2.1: {} + semver@6.3.1: {} semver@7.7.3: {} @@ -6554,6 +7375,8 @@ snapshots: mrmime: 2.0.1 totalist: 3.0.1 + slash@3.0.0: {} + source-map-js@1.2.1: {} source-map@0.6.1: {} @@ -6566,6 +7389,10 @@ snapshots: srvx@0.11.2: {} + stack-utils@2.0.6: + dependencies: + escape-string-regexp: 2.0.0 + stackback@0.0.2: {} stacktracey@2.1.8: @@ -6611,6 +7438,12 @@ snapshots: client-only: 0.0.1 react: 19.2.3 + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + symbol-tree@3.2.4: {} + tiny-invariant@1.3.3: {} tiny-warning@1.0.3: {} @@ -6641,8 +7474,14 @@ snapshots: tinyspy@4.0.3: {} + tldts-core@6.1.86: {} + tldts-core@7.0.16: {} + tldts@6.1.86: + dependencies: + tldts-core: 6.1.86 + tldts@7.0.16: dependencies: tldts-core: 7.0.16 @@ -6655,10 +7494,29 @@ snapshots: totalist@3.0.1: {} + tough-cookie@4.1.4: + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + + tough-cookie@5.1.2: + dependencies: + tldts: 6.1.86 + tough-cookie@6.0.0: dependencies: tldts: 7.0.16 + tr46@3.0.0: + dependencies: + punycode: 2.3.1 + + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + tree-kill@1.2.2: {} tsconfck@3.1.6(typescript@5.9.3): @@ -6674,8 +7532,8 @@ snapshots: diff: 8.0.2 empathic: 2.0.0 hookable: 5.5.3 - rolldown: 1.0.0-rc.5 - rolldown-plugin-dts: 0.15.6(rolldown@1.0.0-rc.5)(typescript@5.9.2) + rolldown: 1.0.0-rc.9 + rolldown-plugin-dts: 0.15.6(rolldown@1.0.0-rc.9)(typescript@5.9.2) semver: 7.7.3 tinyexec: 1.0.1 tinyglobby: 0.2.14 @@ -6698,6 +7556,8 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + type-detect@4.0.8: {} + type-detect@4.1.0: {} type-fest@4.41.0: {} @@ -6742,6 +7602,8 @@ snapshots: universal-user-agent@6.0.1: {} + universalify@0.2.0: {} + unpipe@1.0.0: {} unplugin@2.3.11: @@ -6761,6 +7623,11 @@ snapshots: url-join@5.0.0: {} + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + use-sync-external-store@1.6.0(react@19.2.3): dependencies: react: 19.2.3 @@ -6852,7 +7719,7 @@ snapshots: optionalDependencies: vite: 7.3.1(@types/node@22.17.1)(jiti@2.5.1)(tsx@4.21.0)(yaml@2.8.1) - vitest@3.2.4(@types/node@22.17.1)(@vitest/browser@3.2.4)(jiti@2.5.1)(msw@2.11.3(@types/node@22.17.1)(typescript@5.9.2))(tsx@4.21.0)(yaml@2.8.1): + vitest@3.2.4(@types/node@22.17.1)(@vitest/browser@3.2.4)(jiti@2.5.1)(jsdom@26.1.0)(msw@2.11.3(@types/node@22.17.1)(typescript@5.9.2))(tsx@4.21.0)(yaml@2.8.1): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 @@ -6880,6 +7747,7 @@ snapshots: optionalDependencies: '@types/node': 22.17.1 '@vitest/browser': 3.2.4(msw@2.11.3(@types/node@22.17.1)(typescript@5.9.2))(playwright@1.58.2)(vite@7.3.1(@types/node@22.17.1)(jiti@2.5.1)(tsx@4.21.0)(yaml@2.8.1))(vitest@3.2.4) + jsdom: 26.1.0 transitivePeerDependencies: - jiti - less @@ -6894,7 +7762,7 @@ snapshots: - tsx - yaml - vitest@4.0.18(@types/node@22.17.1)(@vitest/browser-playwright@4.0.18)(jiti@2.5.1)(msw@2.11.3(@types/node@22.17.1)(typescript@5.9.2))(tsx@4.21.0)(yaml@2.8.1): + vitest@4.0.18(@types/node@22.17.1)(@vitest/browser-playwright@4.0.18)(jiti@2.5.1)(jsdom@26.1.0)(msw@2.11.3(@types/node@22.17.1)(typescript@5.9.2))(tsx@4.21.0)(yaml@2.8.1): dependencies: '@vitest/expect': 4.0.18 '@vitest/mocker': 4.0.18(msw@2.11.3(@types/node@22.17.1)(typescript@5.9.2))(vite@7.3.1(@types/node@22.17.1)(jiti@2.5.1)(tsx@4.21.0)(yaml@2.8.1)) @@ -6919,6 +7787,7 @@ snapshots: optionalDependencies: '@types/node': 22.17.1 '@vitest/browser-playwright': 4.0.18(msw@2.11.3(@types/node@22.17.1)(typescript@5.9.2))(playwright@1.58.2)(vite@7.1.2(@types/node@22.17.1)(jiti@2.5.1)(tsx@4.21.0)(yaml@2.8.1))(vitest@4.0.18) + jsdom: 26.1.0 transitivePeerDependencies: - jiti - less @@ -6932,14 +7801,44 @@ snapshots: - tsx - yaml + w3c-xmlserializer@4.0.0: + dependencies: + xml-name-validator: 4.0.0 + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@7.0.0: {} + webpack-virtual-modules@0.6.2: {} + webrtc-adapter@8.2.4: + dependencies: + sdp: 3.2.1 + + whatwg-encoding@2.0.0: + dependencies: + iconv-lite: 0.6.3 + whatwg-encoding@3.1.1: dependencies: iconv-lite: 0.6.3 + whatwg-mimetype@3.0.0: {} + whatwg-mimetype@4.0.0: {} + whatwg-url@11.0.0: + dependencies: + tr46: 3.0.0 + webidl-conversions: 7.0.0 + + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + which@2.0.2: dependencies: isexe: 2.0.0 @@ -6995,6 +7894,10 @@ snapshots: ws@8.19.0: {} + xml-name-validator@4.0.0: {} + + xml-name-validator@5.0.0: {} + xmlbuilder2@4.0.3: dependencies: '@oozcitak/dom': 2.0.2 @@ -7002,6 +7905,8 @@ snapshots: '@oozcitak/util': 10.0.0 js-yaml: 4.1.1 + xmlchars@2.2.0: {} + y18n@5.0.8: {} yallist@3.1.1: {}