@@ -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: {}