* 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>
115 lines
3.4 KiB
TypeScript
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;
|
|
}
|