92 lines
3.3 KiB
JavaScript
92 lines
3.3 KiB
JavaScript
import net from "node:net";
|
|
|
|
const GUARDED_SOCKET_TYPE_OF_SERVICE = Symbol.for("openwork.socket-type-of-service-guard");
|
|
|
|
export function isHarmlessSocketTypeOfServiceError(error) {
|
|
if (!(error instanceof Error)) return false;
|
|
const code = Reflect.get(error, "code");
|
|
const syscall = Reflect.get(error, "syscall");
|
|
const namesSocketOption = syscall === "setTypeOfService" || error.message.includes("setTypeOfService");
|
|
return namesSocketOption && code === "EINVAL";
|
|
}
|
|
|
|
/**
|
|
* Node 24.18's bundled Undici calls setTypeOfService(0) for ordinary HTTP/1.1
|
|
* requests. macOS can reject that best-effort QoS operation with EINVAL while
|
|
* a socket is being reused or torn down, and the synchronous throw escapes the
|
|
* fetch promise boundary. Guard the socket operation itself so request code can
|
|
* continue, while every other socket failure still follows Node's fatal path.
|
|
* @param {{
|
|
* SocketClass?: { prototype?: object },
|
|
* warn?: (message: string, error: unknown) => void,
|
|
* }} [options]
|
|
*/
|
|
export function installSocketTypeOfServiceGuard({
|
|
SocketClass = net.Socket,
|
|
warn = (message, error) => console.warn(message, error),
|
|
} = {}) {
|
|
const prototype = SocketClass?.prototype;
|
|
const original = prototype ? Reflect.get(prototype, "setTypeOfService") : undefined;
|
|
if (typeof original !== "function") return false;
|
|
if (Reflect.get(original, GUARDED_SOCKET_TYPE_OF_SERVICE) === true) return false;
|
|
|
|
function guardedSetTypeOfService(...args) {
|
|
try {
|
|
return Reflect.apply(original, this, args);
|
|
} catch (error) {
|
|
if (!isHarmlessSocketTypeOfServiceError(error)) throw error;
|
|
warn("[network] ignored unsupported socket traffic-class hint", error);
|
|
return this;
|
|
}
|
|
}
|
|
Reflect.set(guardedSetTypeOfService, GUARDED_SOCKET_TYPE_OF_SERVICE, true);
|
|
Object.defineProperty(prototype, "setTypeOfService", {
|
|
...Object.getOwnPropertyDescriptor(prototype, "setTypeOfService"),
|
|
value: guardedSetTypeOfService,
|
|
});
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param {string} label
|
|
* @param {() => unknown | Promise<unknown>} task
|
|
* @param {(message: string, error: unknown) => void} [report]
|
|
*/
|
|
export function runDetachedTask(label, task, report = (message, error) => console.error(message, error)) {
|
|
void Promise.resolve()
|
|
.then(task)
|
|
.catch((error) => report(`[desktop] ${label} failed`, error));
|
|
}
|
|
|
|
/**
|
|
* @param {{
|
|
* reload: () => unknown | Promise<unknown>,
|
|
* report?: (message: string, details: object | unknown) => void,
|
|
* onRepeatedCrash?: (details: object) => void,
|
|
* now?: () => number,
|
|
* retryWindowMs?: number,
|
|
* }} options
|
|
*/
|
|
export function createRendererCrashRecovery({
|
|
reload,
|
|
report = (message, details) => console.error(message, details),
|
|
onRepeatedCrash = () => undefined,
|
|
now = () => Date.now(),
|
|
retryWindowMs = 30_000,
|
|
}) {
|
|
let lastRecoveryAt = Number.NEGATIVE_INFINITY;
|
|
return (details = {}) => {
|
|
const reason = Reflect.get(details, "reason");
|
|
if (reason === "clean-exit") return false;
|
|
const recoveryAt = now();
|
|
report("[desktop] renderer process exited unexpectedly", details);
|
|
if (recoveryAt - lastRecoveryAt < retryWindowMs) {
|
|
onRepeatedCrash(details);
|
|
return false;
|
|
}
|
|
lastRecoveryAt = recoveryAt;
|
|
runDetachedTask("reload crashed renderer", reload, report);
|
|
return true;
|
|
};
|
|
}
|