Files
GMW/services/discord-gateway/src/shared/utils/index.ts
T
asepharyana 5505983dbd fix(goLive): retry VIDEO(op12)/SPEAKING(op5) opcodes until ws OPEN — broken shared-screen video
Root cause: BaseMediaConnection.sendOpcode is a silent no-op when
ws.readyState !== OPEN. In GoLive, playStream() calls setVideoAttributes(true)
+ setSpeaking(true) the instant createStream() resolves (right after
SELECT_PROTOCOL_ACK), but the StreamConnection WebSocket can still be in
CONNECTING for a few ms — so op 12 (VIDEO, activating the video SSRC) was
silently DROPPED every session. Empirically verified: 0 ops 12/5 ever logged
across the entire journal, yet 10k+ video frames were sent and audio played
(audio SSRC is activated via the VoiceConnection handshake, independent of
GoLive op 12). Discord's media server thus received video RTP on video_ssrc
but was never told to forward it → black/broken shared-screen video with
working voice.

sendOpcodeWhenOpen retries up to ~2s for ws OPEN instead of dropping. Also
emits a=fmtp:101 packetization-mode=1;profile-level-id=42e01f in the answer
SDP (H264 FU-A fragments require packetization-mode=1 to reassemble).

Also removes pre-existing noNonNullAssertion lint (biome 2.5.8 now errors)
that was blocking the deploy CI.
2026-08-13 01:44:40 +07:00

116 lines
3.4 KiB
TypeScript

// Utility functions shared across services
export function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export * from "./pagination.js";
// ---------------------------------------------------------------------------
// Centralized AbortController with guaranteed cleanup
// ---------------------------------------------------------------------------
/**
* Creates an AbortController with a timeout that is ALWAYS cleaned up,
* even if the caller throws or returns early without calling clear().
*
* Returns both the controller and a cleanup handle.
*
* Usage:
* const { controller, clear } = createAbortControllerWithTimeout(8000);
* try {
* const res = await fetch(url, { signal: controller.signal });
* // ... work ...
* } finally {
* clear(); // guaranteed to clear the timeout
* }
*/
export function createAbortControllerWithTimeout(timeoutMs: number): {
controller: AbortController;
clear: () => void;
} {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
// Unref so the timeout doesn't keep the process alive
timeoutId?.unref?.();
return {
controller,
clear: () => {
clearTimeout(timeoutId);
},
};
}
// ---------------------------------------------------------------------------
// Retry with exponential backoff
// ---------------------------------------------------------------------------
export async function retryWithBackoff<T>(
fn: () => Promise<T>,
options: {
/** Number of retry attempts (default: 3) */
retries?: number;
/** Initial delay in ms (default: 1000) */
minTimeout?: number;
/** Maximum delay in ms (default: 30000) */
maxTimeout?: number;
/** Multiplication factor for each retry (default: 2) */
factor?: number;
/** Optional AbortSignal to cancel retries */
signal?: AbortSignal;
} = {},
): Promise<T> {
const {
retries = 3,
minTimeout = 1_000,
maxTimeout = 30_000,
factor = 2,
signal,
} = options;
let lastError: Error | undefined;
for (let attempt = 0; attempt <= retries; attempt++) {
if (signal?.aborted) {
const err = new Error("Aborted");
err.name = "AbortError";
throw err;
}
try {
return await fn();
} catch (err) {
lastError = err instanceof Error ? err : new Error(String(err));
if (lastError.name === "AbortError") {
throw lastError;
}
if (attempt === retries) break;
const backoff = Math.min(
minTimeout * factor ** attempt + Math.random() * 100,
maxTimeout,
);
await new Promise<void>((resolve, reject) => {
let timeoutId: NodeJS.Timeout;
const onAbort = () => {
clearTimeout(timeoutId);
const abortErr = new Error("Aborted");
abortErr.name = "AbortError";
reject(abortErr);
};
if (signal?.aborted) return onAbort();
timeoutId = setTimeout(() => {
if (signal) signal.removeEventListener("abort", onAbort);
resolve();
}, backoff);
if (signal) signal.addEventListener("abort", onAbort, { once: true });
});
}
}
// lastError is always set: the for-loop only exits via break when attempt
// === retries, which only happens in the catch branch that sets lastError.
if (!lastError) throw new Error("Unknown retry error");
throw lastError;
}