refactor: reduce complexity in display/selection and streaming functions (#1162)

Extract helper functions to reduce cyclomatic complexity:

- shared/common.sh: Split _display_and_select() (81 lines) into:
  - _prepare_fzf_input(): Format items for fzf
  - _fzf_select(): Handle fzf interactive selection
  - _numbered_list_select(): Fallback numbered list mode

- trigger-server.ts: Extract startStreamingRun() (133 lines) helpers:
  - createEnqueuer(): Manage client connection state safely
  - drainStreamOutput(): Generic stream draining with activity tracking

- render/lib/common.sh: Extract repeated error messages from
  _render_wait_for_service() (51 lines) into helper functions:
  - _render_print_deployment_failed_help()
  - _render_print_timeout_help()

Agent: complexity-hunter

Co-authored-by: spawn-refactor-bot <refactor@openrouter.ai>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
A 2026-02-14 19:12:58 -08:00 committed by GitHub
parent bf738bee69
commit df96db3499
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 166 additions and 100 deletions

View file

@ -190,6 +190,59 @@ function gracefulShutdown(signal: string) {
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
process.on("SIGINT", () => gracefulShutdown("SIGINT"));
/**
* Create a safe enqueue function that gracefully handles client disconnect.
*/
function createEnqueuer(encoder: TextEncoder): {
enqueue: (controller: ReadableStreamDefaultController, chunk: Uint8Array) => void;
isConnected: () => boolean;
setDisconnected: () => void;
} {
let clientConnected = true;
return {
enqueue(controller: ReadableStreamDefaultController, chunk: Uint8Array) {
if (!clientConnected) return;
try {
controller.enqueue(chunk);
} catch {
clientConnected = false;
}
},
isConnected() {
return clientConnected;
},
setDisconnected() {
clientConnected = false;
},
};
}
/**
* Drain a readable stream, logging to console and enqueuing to HTTP response.
*/
async function drainStreamOutput(
src: ReadableStream<Uint8Array> | null,
enqueue: (chunk: Uint8Array) => void,
onActivity: () => void
): Promise<void> {
if (!src) return;
const reader = src.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
// Always log locally
process.stdout.write(value);
// Stream to HTTP client if still connected
onActivity();
enqueue(value);
}
} finally {
reader.releaseLock();
}
}
/**
* Spawn the target script and return a streaming Response.
*
@ -237,49 +290,37 @@ function startStreamingRun(reason: string, issue: string): Response {
});
const encoder = new TextEncoder();
let clientConnected = true;
const enqueuer = createEnqueuer(encoder);
const stream = new ReadableStream({
async start(controller) {
// --- Header ---
const header = `[trigger] Run #${id} started (reason=${reason}${issue ? `, issue=#${issue}` : ""}, concurrent=${runs.size}/${MAX_CONCURRENT})\n`;
enqueue(controller, encoder.encode(header));
enqueuer.enqueue(controller, encoder.encode(header));
// --- Heartbeat: emit every 15s of silence to keep connection alive ---
// Must fire well before Bun's idleTimeout (255s) and any proxy timeouts.
let lastActivity = Date.now();
const heartbeat = setInterval(() => {
if (!clientConnected) return;
if (!enqueuer.isConnected()) return;
const silentMs = Date.now() - lastActivity;
if (silentMs >= 14_000) {
const elapsed = Math.round((Date.now() - startedAt) / 1000);
const msg = `[heartbeat] Run #${id} active (${elapsed}s elapsed)\n`;
enqueue(controller, encoder.encode(msg));
enqueuer.enqueue(controller, encoder.encode(msg));
lastActivity = Date.now();
}
}, 15_000);
// --- Drain stdout + stderr concurrently ---
async function drain(src: ReadableStream<Uint8Array> | null) {
if (!src) return;
const reader = src.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
// Always log locally
process.stdout.write(value);
// Stream to HTTP client if still connected
lastActivity = Date.now();
enqueue(controller, value);
}
} finally {
reader.releaseLock();
}
}
try {
await Promise.all([drain(proc.stdout), drain(proc.stderr)]);
await Promise.all([
drainStreamOutput(proc.stdout, (chunk) => enqueuer.enqueue(controller, chunk), () => {
lastActivity = Date.now();
}),
drainStreamOutput(proc.stderr, (chunk) => enqueuer.enqueue(controller, chunk), () => {
lastActivity = Date.now();
}),
]);
// --- Wait for exit ---
const exitCode = await proc.exited;
@ -287,12 +328,12 @@ function startStreamingRun(reason: string, issue: string): Response {
const elapsed = Math.round((Date.now() - startedAt) / 1000);
const footer = `\n[trigger] Run #${id} finished (exit=${exitCode}, duration=${elapsed}s, remaining=${runs.size}/${MAX_CONCURRENT})\n`;
console.log(footer.trim());
enqueue(controller, encoder.encode(footer));
enqueuer.enqueue(controller, encoder.encode(footer));
} catch (err) {
const elapsed = Math.round((Date.now() - startedAt) / 1000);
const errMsg = `\n[trigger] Run #${id} stream error after ${elapsed}s: ${err}\n`;
console.error(errMsg.trim());
enqueue(controller, encoder.encode(errMsg));
enqueuer.enqueue(controller, encoder.encode(errMsg));
} finally {
runs.delete(id);
clearInterval(heartbeat);
@ -304,26 +345,13 @@ function startStreamingRun(reason: string, issue: string): Response {
cancel() {
// Called when the HTTP client disconnects
clientConnected = false;
enqueuer.setDisconnected();
console.log(
`[trigger] Client disconnected from run #${id} stream (process continues running)`
);
},
});
/** Safely enqueue data — swallow errors from client disconnect */
function enqueue(
controller: ReadableStreamDefaultController,
chunk: Uint8Array
) {
if (!clientConnected) return;
try {
controller.enqueue(chunk);
} catch {
clientConnected = false;
}
}
return new Response(stream, {
headers: {
"Content-Type": "text/plain; charset=utf-8",

View file

@ -146,6 +146,30 @@ print(json.dumps(body))
# Wait for Render service to become live
# Usage: _render_wait_for_service SERVICE_ID [MAX_ATTEMPTS]
_render_print_deployment_failed_help() {
log_error "Common causes:"
log_error " - Build failure (check Docker image or build command configuration)"
log_error " - Insufficient resources for the selected instance type"
log_error " - Health check failure (service crashed during startup)"
log_error " - Application error in start command or missing runtime dependencies"
log_error " - Network/port configuration issues"
log_error ""
log_error "Debugging steps:"
log_error " 1. View deployment logs at: https://dashboard.render.com/"
log_error " 2. Check build and runtime logs for error messages"
log_error " 3. Verify service configuration (ports, env vars, start command)"
log_error " 4. Try a different region or instance type"
}
_render_print_timeout_help() {
log_error "The service may still be deploying. You can:"
log_error " 1. Check deployment status at: https://dashboard.render.com/"
log_error " 2. View real-time deployment logs in the dashboard"
log_error " 3. Re-run the spawn command to retry"
log_error ""
log_error "If the issue persists, the service may need manual intervention via the Render dashboard."
}
_render_wait_for_service() {
local service_id="$1"
local max_attempts=${2:-60}
@ -168,18 +192,7 @@ _render_wait_for_service() {
if [[ "$status" == "failed" ]]; then
log_error "Service deployment failed with status: $status"
log_error ""
log_error "Common causes:"
log_error " - Build failure (check Docker image or build command configuration)"
log_error " - Insufficient resources for the selected instance type"
log_error " - Health check failure (service crashed during startup)"
log_error " - Application error in start command or missing runtime dependencies"
log_error " - Network/port configuration issues"
log_error ""
log_error "Debugging steps:"
log_error " 1. View deployment logs at: https://dashboard.render.com/"
log_error " 2. Check build and runtime logs for error messages"
log_error " 3. Verify service configuration (ports, env vars, start command)"
log_error " 4. Try a different region or instance type"
_render_print_deployment_failed_help
return 1
fi
@ -190,12 +203,7 @@ _render_wait_for_service() {
log_error "Service did not become live after $max_attempts attempts"
log_error ""
log_error "The service may still be deploying. You can:"
log_error " 1. Check deployment status at: https://dashboard.render.com/"
log_error " 2. View real-time deployment logs in the dashboard"
log_error " 3. Re-run the spawn command to retry"
log_error ""
log_error "If the issue persists, the service may need manual intervention via the Render dashboard."
_render_print_timeout_help
return 1
}

View file

@ -2596,62 +2596,64 @@ EOF
# Display a numbered list and read user selection
# Pipe-delimited items: "id|label". Returns selected id via stdout.
# Usage: _display_and_select PROMPT_TEXT DEFAULT_VALUE DEFAULT_ID <<< "$items"
_display_and_select() {
_fzf_select() {
local prompt_text="${1}"
local default_value="${2}"
local default_id="${3:-}"
local default_id="${3}"
local fzf_input="${4}"
local default_line="${5}"
# Read all items into array
local items_array=()
while IFS= read -r line; do
items_array+=("${line}")
done
log_step "Select ${prompt_text%s} (type to filter):"
if [[ "${#items_array[@]}" -eq 0 ]]; then
log_warn "No ${prompt_text} available, using default: ${default_value}"
# Run fzf with default selection
local selected
if [[ -n "${default_line}" ]]; then
selected=$(printf '%s' "${fzf_input}" | fzf --height=~50% --reverse --prompt="Select > " --query="" --select-1 --exit-0 --header="Press ESC to use default (${default_id})" --print-query --query="${default_line%%$'\t'*}" | tail -1)
else
selected=$(printf '%s' "${fzf_input}" | fzf --height=~50% --reverse --prompt="Select > " --select-1 --exit-0)
fi
# If fzf was cancelled or returned nothing, use default
if [[ -z "${selected}" ]]; then
log_info "Using default: ${default_value}"
echo "${default_value}"
return
fi
# Try to use fzf for interactive filtering if available and stdin is a TTY
if command -v fzf >/dev/null 2>&1 && [[ -t 0 ]]; then
log_step "Select ${prompt_text%s} (type to filter):"
# Extract ID from selected line
local selected_id="${selected%%$'\t'*}"
echo "${selected_id}"
}
# Prepare fzf input with formatted display
local fzf_input=""
local default_line=""
for line in "${items_array[@]}"; do
local id="${line%%|*}"
local display
display=$(echo "${line}" | tr '|' '\t')
fzf_input+="${display}"$'\n'
if [[ -n "${default_id}" && "${id}" == "${default_id}" ]]; then
default_line="${display}"
fi
done
_prepare_fzf_input() {
local default_id="${1}"
shift
local items_array=("$@")
# Run fzf with default selection
local selected
if [[ -n "${default_line}" ]]; then
selected=$(printf '%s' "${fzf_input}" | fzf --height=~50% --reverse --prompt="Select > " --query="" --select-1 --exit-0 --header="Press ESC to use default (${default_id})" --print-query --query="${default_line%%$'\t'*}" | tail -1)
else
selected=$(printf '%s' "${fzf_input}" | fzf --height=~50% --reverse --prompt="Select > " --select-1 --exit-0)
local fzf_input=""
local default_line=""
for line in "${items_array[@]}"; do
local id="${line%%|*}"
local display
display=$(echo "${line}" | tr '|' '\t')
fzf_input+="${display}"$'\n'
if [[ -n "${default_id}" && "${id}" == "${default_id}" ]]; then
default_line="${display}"
fi
done
# If fzf was cancelled or returned nothing, use default
if [[ -z "${selected}" ]]; then
log_info "Using default: ${default_value}"
echo "${default_value}"
return
fi
# Return via globals (bash doesn't have good multi-value returns)
FZF_INPUT="${fzf_input}"
FZF_DEFAULT_LINE="${default_line}"
}
# Extract ID from selected line
local selected_id="${selected%%$'\t'*}"
echo "${selected_id}"
return
fi
_numbered_list_select() {
local prompt_text="${1}"
local default_value="${2}"
local default_id="${3}"
shift 3
local items_array=("$@")
# Fallback to numbered list when fzf is not available
log_step "Available ${prompt_text}:"
local i=1
local ids=()
@ -2679,6 +2681,34 @@ _display_and_select() {
fi
}
_display_and_select() {
local prompt_text="${1}"
local default_value="${2}"
local default_id="${3:-}"
# Read all items into array
local items_array=()
while IFS= read -r line; do
items_array+=("${line}")
done
if [[ "${#items_array[@]}" -eq 0 ]]; then
log_warn "No ${prompt_text} available, using default: ${default_value}"
echo "${default_value}"
return
fi
# Try to use fzf for interactive filtering if available and stdin is a TTY
if command -v fzf >/dev/null 2>&1 && [[ -t 0 ]]; then
_prepare_fzf_input "${default_id}" "${items_array[@]}"
_fzf_select "${prompt_text}" "${default_value}" "${default_id}" "${FZF_INPUT}" "${FZF_DEFAULT_LINE}"
return
fi
# Fallback to numbered list when fzf is not available
_numbered_list_select "${prompt_text}" "${default_value}" "${default_id}" "${items_array[@]}"
}
# Returns: selected ID via stdout
interactive_pick() {
local env_var_name="${1}"