Files
paseo/packages/app/src/utils/terminal-attach.ts
T

93 lines
2.5 KiB
TypeScript

const TERMINAL_ATTACH_RETRYABLE_ERROR_PATTERNS = [
"terminal not found",
"timed out",
"timeout",
"connection",
"network",
"disconnected",
"stream ended",
] as const;
export type TerminalResumeOffsetStore = {
get: (input: { terminalId: string }) => number | undefined;
set: (input: { terminalId: string; offset: number }) => void;
};
export function getTerminalResumeOffset(input: {
terminalId: string;
resumeOffsetStore: TerminalResumeOffsetStore;
}): number | undefined {
const offset = input.resumeOffsetStore.get({ terminalId: input.terminalId });
if (typeof offset !== "number" || !Number.isFinite(offset)) {
return undefined;
}
const normalizedOffset = Math.max(0, Math.floor(offset));
return normalizedOffset;
}
export function updateTerminalResumeOffset(input: {
terminalId: string;
offset: number;
resumeOffsetStore: TerminalResumeOffsetStore;
}): void {
if (!Number.isFinite(input.offset)) {
return;
}
const normalizedOffset = Math.max(0, Math.floor(input.offset));
const previousOffset =
getTerminalResumeOffset({
terminalId: input.terminalId,
resumeOffsetStore: input.resumeOffsetStore,
}) ?? -1;
if (normalizedOffset <= previousOffset) {
return;
}
input.resumeOffsetStore.set({
terminalId: input.terminalId,
offset: normalizedOffset,
});
}
export function getTerminalAttachRetryDelayMs(input: { attempt: number }): number {
const clampedAttempt = Math.max(0, input.attempt);
const exponentialDelay = 250 * 2 ** clampedAttempt;
return Math.min(2_000, exponentialDelay);
}
export function isTerminalAttachRetryableError(input: { message: string }): boolean {
const normalized = input.message.toLowerCase();
return TERMINAL_ATTACH_RETRYABLE_ERROR_PATTERNS.some((pattern) => normalized.includes(pattern));
}
export async function waitForDuration(input: { durationMs: number }): Promise<void> {
await new Promise<void>((resolve) => {
setTimeout(resolve, Math.max(0, input.durationMs));
});
}
export async function withPromiseTimeout<T>(input: {
promise: Promise<T>;
timeoutMs: number;
timeoutMessage: string;
}): Promise<T> {
let timeoutHandle: ReturnType<typeof setTimeout> | null = null;
const timeoutPromise = new Promise<never>((_, reject) => {
timeoutHandle = setTimeout(
() => {
reject(new Error(input.timeoutMessage));
},
Math.max(0, input.timeoutMs),
);
});
try {
return await Promise.race([input.promise, timeoutPromise]);
} finally {
if (timeoutHandle) {
clearTimeout(timeoutHandle);
}
}
}