Files
paseo/packages/server/src/server/websocket/physical-socket.ts
T
9e5accee3c fix: reject oversized checkout diffs (#2488)
* fix: reject oversized checkout diffs

* fix(diff): keep large previews from disconnecting clients

The physical socket limit is an OOM backstop, not a frame-size contract. Bound structured diff construction incrementally and report oversized previews without closing the client's socket.

* fix(app): preserve oversized diff state

* fix(diff): stay within relay frame limit

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-07-30 00:48:05 +08:00

115 lines
3.4 KiB
TypeScript

// OOM backstop for a socket whose client stopped draining. A daemon normally has
// 1-10 physical sockets (tens at the outside), so 64 MiB bounds abandoned queues
// without treating ordinary large frames as a protocol or frame-size violation.
export const MAX_PHYSICAL_SOCKET_BUFFERED_BYTES = 64 * 1024 * 1024;
// Current clients ping every 10 seconds. Four delayed cycles fit inside the
// lease without making an abandoned application socket linger for minutes.
export const APPLICATION_SOCKET_LEASE_MS = 45_000;
export const APPLICATION_SOCKET_LEASE_CHECK_INTERVAL_MS = 10_000;
type Clock = () => number;
export class ApplicationSocketLease<TSocket extends object> {
private readonly deadlines = new Map<TSocket, number>();
constructor(private readonly clock: Clock = Date.now) {}
claim(socket: TSocket): void {
this.deadlines.set(socket, this.clock() + APPLICATION_SOCKET_LEASE_MS);
}
renew(socket: TSocket): void {
if (this.deadlines.has(socket)) {
this.claim(socket);
}
}
release(socket: TSocket): void {
this.deadlines.delete(socket);
}
listExpired(): TSocket[] {
const now = this.clock();
const expired: TSocket[] = [];
for (const [socket, deadline] of this.deadlines) {
if (deadline > now) continue;
expired.push(socket);
}
return expired;
}
clear(): void {
this.deadlines.clear();
}
}
export function outboundFrameByteLength(data: string | Uint8Array | ArrayBuffer): number {
if (typeof data === "string") return Buffer.byteLength(data);
return data.byteLength;
}
interface BoundedPhysicalSocket {
readyState: number;
bufferedAmount?: number;
send: (
data: string | Uint8Array | ArrayBuffer,
callback?: (error?: Error) => void,
) => void | Promise<void>;
}
export async function sendBoundedPhysicalFrameAndWait(params: {
socket: BoundedPhysicalSocket;
frame: string | Uint8Array | ArrayBuffer;
frameBytes?: number;
onHighWater: () => void;
}): Promise<boolean> {
const { socket, frame, frameBytes = outboundFrameByteLength(frame), onHighWater } = params;
if (socket.readyState !== 1) return false;
if (!physicalSocketHasCapacity(socket, frameBytes)) {
onHighWater();
return false;
}
await new Promise<void>((resolve, reject) => {
let callbackUsed = false;
const result = socket.send(frame, (error) => {
callbackUsed = true;
if (error) reject(error);
else resolve();
});
if (result && typeof result.then === "function") {
result.then(resolve, reject);
} else if (socket.send.length < 2 && !callbackUsed) {
resolve();
}
});
return true;
}
export function physicalSocketHasCapacity(
socket: Pick<BoundedPhysicalSocket, "bufferedAmount">,
frameBytes: number,
): boolean {
if (typeof socket.bufferedAmount !== "number") return true;
return socket.bufferedAmount + frameBytes <= MAX_PHYSICAL_SOCKET_BUFFERED_BYTES;
}
export function sendBoundedPhysicalFrame(params: {
socket: BoundedPhysicalSocket;
frame: string | Uint8Array | ArrayBuffer;
frameBytes?: number;
onHighWater: () => void;
}): boolean {
const { socket, frame, frameBytes = outboundFrameByteLength(frame), onHighWater } = params;
if (socket.readyState !== 1) return false;
if (!physicalSocketHasCapacity(socket, frameBytes)) {
onHighWater();
return false;
}
const result = socket.send(frame);
if (result && typeof result.then === "function") {
void result.catch(() => undefined);
}
return true;
}