diff --git a/src/cli/commands/cron.ts b/src/cli/commands/cron.ts index 976e810..72912eb 100644 --- a/src/cli/commands/cron.ts +++ b/src/cli/commands/cron.ts @@ -1,6 +1,6 @@ import type { LogLine } from '@types' import color from 'kleur' -import { activeSignal, get, handleError, makeUrl, post } from '../http' +import { get, getSignal, handleError, makeUrl, post } from '../http' import { resolveAppName } from '../name' interface CronJobSummary { @@ -195,7 +195,7 @@ const printCronLog = (line: LogLine) => async function tailCronLogs(app: string, grep?: string) { try { const url = makeUrl(`/api/apps/${app}/logs/stream`) - const res = await fetch(url, { signal: activeSignal }) + const res = await fetch(url, { signal: getSignal() }) if (!res.ok) { console.error(`App not found: ${app}`) return diff --git a/src/cli/commands/logs.ts b/src/cli/commands/logs.ts index f9f09fb..81f0655 100644 --- a/src/cli/commands/logs.ts +++ b/src/cli/commands/logs.ts @@ -1,5 +1,5 @@ import type { LogLine } from '@types' -import { activeSignal, get, handleError, makeUrl } from '../http' +import { get, getSignal, handleError, makeUrl } from '../http' import { resolveAppName } from '../name' interface LogOptions { @@ -120,7 +120,7 @@ export async function logApp(arg: string | undefined, options: LogOptions) { export async function tailLogs(name: string, grep?: string) { try { const url = makeUrl(`/api/apps/${name}/logs/stream`) - const res = await fetch(url, { signal: activeSignal }) + const res = await fetch(url, { signal: getSignal() }) if (!res.ok) { console.error(`App not found: ${name}`) return diff --git a/src/cli/commands/sync.ts b/src/cli/commands/sync.ts index 4cb7761..178e13d 100644 --- a/src/cli/commands/sync.ts +++ b/src/cli/commands/sync.ts @@ -5,7 +5,7 @@ import color from 'kleur' import { diffLines } from 'diff' import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, unlinkSync, watch, writeFileSync } from 'fs' import { dirname, join } from 'path' -import { activeSignal, del, download, get, getManifest, handleError, makeUrl, post, put } from '../http' +import { del, download, get, getManifest, getSignal, handleError, makeUrl, post, put } from '../http' import { confirm, prompt } from '../prompts' import { getAppName, getAppPackage, isApp, resolveAppName } from '../name' @@ -592,7 +592,7 @@ export async function syncApp() { const url = makeUrl(`/api/sync/apps/${appName}/watch`) let res: Response try { - res = await fetch(url, { signal: activeSignal }) + res = await fetch(url, { signal: getSignal() }) if (!res.ok) { console.error(`Failed to connect to server: ${res.status} ${res.statusText}`) watcher.close() @@ -967,7 +967,7 @@ interface VersionsResponse { async function getVersions(appName: string): Promise { try { - const res = await fetch(makeUrl(`/api/sync/apps/${appName}/versions`), { signal: activeSignal }) + const res = await fetch(makeUrl(`/api/sync/apps/${appName}/versions`), { signal: getSignal() }) if (res.status === 404) { console.error(`App not found: ${appName}`) return null diff --git a/src/cli/http.ts b/src/cli/http.ts index 7907c27..a955b03 100644 --- a/src/cli/http.ts +++ b/src/cli/http.ts @@ -1,7 +1,8 @@ import type { Manifest } from '@types' -const DEFAULT_HOST = process.env.DEV ? 'http://localhost:3000' : 'http://toes.local' +import { AsyncLocalStorage } from 'node:async_hooks' -export let activeSignal: AbortSignal | undefined +const DEFAULT_HOST = process.env.DEV ? 'http://localhost:3000' : 'http://toes.local' +const signalStore = new AsyncLocalStorage() const normalizeUrl = (url: string) => url.startsWith('http://') || url.startsWith('https://') ? url : `http://${url}` @@ -19,9 +20,11 @@ export const HOST = process.env.TOES_URL ? normalizeUrl(process.env.TOES_URL) : DEFAULT_HOST -export const clearSignal = () => { activeSignal = undefined } +export const getSignal = () => signalStore.getStore() -export const setSignal = (signal: AbortSignal) => { activeSignal = signal } +export function withSignal(signal: AbortSignal, fn: () => T): T { + return signalStore.run(signal, fn) +} export function makeUrl(path: string): string { return `${HOST}${path}` @@ -42,7 +45,7 @@ export function handleError(error: unknown): void { export async function get(url: string): Promise { try { - const res = await fetch(makeUrl(url), { signal: activeSignal }) + const res = await fetch(makeUrl(url), { signal: getSignal() }) if (!res.ok) { const text = await res.text() const msg = tryParseError(text) ?? `${res.status} ${res.statusText}` @@ -56,7 +59,7 @@ export async function get(url: string): Promise { export async function getManifest(appName: string): Promise<{ exists: boolean, manifest?: Manifest, version?: string } | null> { try { - const res = await fetch(makeUrl(`/api/sync/apps/${appName}/manifest`), { signal: activeSignal }) + const res = await fetch(makeUrl(`/api/sync/apps/${appName}/manifest`), { signal: getSignal() }) if (res.status === 404) return { exists: false } if (!res.ok) throw new Error(`${res.status} ${res.statusText}`) const data = await res.json() @@ -74,7 +77,7 @@ export async function post(url: string, body?: B): Promise { try { const fullUrl = makeUrl(url) - const res = await fetch(fullUrl, { signal: activeSignal }) + const res = await fetch(fullUrl, { signal: getSignal() }) if (!res.ok) { const text = await res.text() const msg = tryParseError(text) ?? `${res.status} ${res.statusText}` @@ -125,7 +128,7 @@ export async function del(url: string): Promise { try { const res = await fetch(makeUrl(url), { method: 'DELETE', - signal: activeSignal, + signal: getSignal(), }) if (!res.ok) { const text = await res.text() diff --git a/src/cli/shell.ts b/src/cli/shell.ts index 8f83619..904325d 100644 --- a/src/cli/shell.ts +++ b/src/cli/shell.ts @@ -4,7 +4,7 @@ import * as readline from 'readline' import color from 'kleur' -import { clearSignal, get, handleError, HOST, setSignal } from './http' +import { get, handleError, HOST, withSignal } from './http' import { program } from './setup' import { STATE_ICONS } from './commands/manage' @@ -188,13 +188,12 @@ export async function shell(): Promise { // Set up AbortController for this command activeAbort = new AbortController() const signal = activeAbort.signal - setSignal(signal) // Pause readline so commands can use their own prompts rl.pause() try { - await program.parseAsync(['node', 'toes', ...tokens]) + await withSignal(signal, () => program.parseAsync(['node', 'toes', ...tokens])) } catch (err: unknown) { // Commander throws on exitOverride — suppress help/version exits if (err && typeof err === 'object' && 'code' in err) { @@ -213,7 +212,6 @@ export async function shell(): Promise { handleError(err) } } finally { - clearSignal() activeAbort = null }