Use AsyncLocalStorage for abort signal propagation
This commit is contained in:
parent
460d625f60
commit
5f1de651eb
|
|
@ -1,6 +1,6 @@
|
||||||
import type { LogLine } from '@types'
|
import type { LogLine } from '@types'
|
||||||
import color from 'kleur'
|
import color from 'kleur'
|
||||||
import { activeSignal, get, handleError, makeUrl, post } from '../http'
|
import { get, getSignal, handleError, makeUrl, post } from '../http'
|
||||||
import { resolveAppName } from '../name'
|
import { resolveAppName } from '../name'
|
||||||
|
|
||||||
interface CronJobSummary {
|
interface CronJobSummary {
|
||||||
|
|
@ -195,7 +195,7 @@ const printCronLog = (line: LogLine) =>
|
||||||
async function tailCronLogs(app: string, grep?: string) {
|
async function tailCronLogs(app: string, grep?: string) {
|
||||||
try {
|
try {
|
||||||
const url = makeUrl(`/api/apps/${app}/logs/stream`)
|
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) {
|
if (!res.ok) {
|
||||||
console.error(`App not found: ${app}`)
|
console.error(`App not found: ${app}`)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { LogLine } from '@types'
|
import type { LogLine } from '@types'
|
||||||
import { activeSignal, get, handleError, makeUrl } from '../http'
|
import { get, getSignal, handleError, makeUrl } from '../http'
|
||||||
import { resolveAppName } from '../name'
|
import { resolveAppName } from '../name'
|
||||||
|
|
||||||
interface LogOptions {
|
interface LogOptions {
|
||||||
|
|
@ -120,7 +120,7 @@ export async function logApp(arg: string | undefined, options: LogOptions) {
|
||||||
export async function tailLogs(name: string, grep?: string) {
|
export async function tailLogs(name: string, grep?: string) {
|
||||||
try {
|
try {
|
||||||
const url = makeUrl(`/api/apps/${name}/logs/stream`)
|
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) {
|
if (!res.ok) {
|
||||||
console.error(`App not found: ${name}`)
|
console.error(`App not found: ${name}`)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import color from 'kleur'
|
||||||
import { diffLines } from 'diff'
|
import { diffLines } from 'diff'
|
||||||
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, unlinkSync, watch, writeFileSync } from 'fs'
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, unlinkSync, watch, writeFileSync } from 'fs'
|
||||||
import { dirname, join } from 'path'
|
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 { confirm, prompt } from '../prompts'
|
||||||
import { getAppName, getAppPackage, isApp, resolveAppName } from '../name'
|
import { getAppName, getAppPackage, isApp, resolveAppName } from '../name'
|
||||||
|
|
||||||
|
|
@ -592,7 +592,7 @@ export async function syncApp() {
|
||||||
const url = makeUrl(`/api/sync/apps/${appName}/watch`)
|
const url = makeUrl(`/api/sync/apps/${appName}/watch`)
|
||||||
let res: Response
|
let res: Response
|
||||||
try {
|
try {
|
||||||
res = await fetch(url, { signal: activeSignal })
|
res = await fetch(url, { signal: getSignal() })
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
console.error(`Failed to connect to server: ${res.status} ${res.statusText}`)
|
console.error(`Failed to connect to server: ${res.status} ${res.statusText}`)
|
||||||
watcher.close()
|
watcher.close()
|
||||||
|
|
@ -967,7 +967,7 @@ interface VersionsResponse {
|
||||||
|
|
||||||
async function getVersions(appName: string): Promise<VersionsResponse | null> {
|
async function getVersions(appName: string): Promise<VersionsResponse | null> {
|
||||||
try {
|
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) {
|
if (res.status === 404) {
|
||||||
console.error(`App not found: ${appName}`)
|
console.error(`App not found: ${appName}`)
|
||||||
return null
|
return null
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import type { Manifest } from '@types'
|
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<AbortSignal>()
|
||||||
|
|
||||||
const normalizeUrl = (url: string) =>
|
const normalizeUrl = (url: string) =>
|
||||||
url.startsWith('http://') || url.startsWith('https://') ? url : `http://${url}`
|
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)
|
? normalizeUrl(process.env.TOES_URL)
|
||||||
: DEFAULT_HOST
|
: DEFAULT_HOST
|
||||||
|
|
||||||
export const clearSignal = () => { activeSignal = undefined }
|
export const getSignal = () => signalStore.getStore()
|
||||||
|
|
||||||
export const setSignal = (signal: AbortSignal) => { activeSignal = signal }
|
export function withSignal<T>(signal: AbortSignal, fn: () => T): T {
|
||||||
|
return signalStore.run(signal, fn)
|
||||||
|
}
|
||||||
|
|
||||||
export function makeUrl(path: string): string {
|
export function makeUrl(path: string): string {
|
||||||
return `${HOST}${path}`
|
return `${HOST}${path}`
|
||||||
|
|
@ -42,7 +45,7 @@ export function handleError(error: unknown): void {
|
||||||
|
|
||||||
export async function get<T>(url: string): Promise<T | undefined> {
|
export async function get<T>(url: string): Promise<T | undefined> {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(makeUrl(url), { signal: activeSignal })
|
const res = await fetch(makeUrl(url), { signal: getSignal() })
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const text = await res.text()
|
const text = await res.text()
|
||||||
const msg = tryParseError(text) ?? `${res.status} ${res.statusText}`
|
const msg = tryParseError(text) ?? `${res.status} ${res.statusText}`
|
||||||
|
|
@ -56,7 +59,7 @@ export async function get<T>(url: string): Promise<T | undefined> {
|
||||||
|
|
||||||
export async function getManifest(appName: string): Promise<{ exists: boolean, manifest?: Manifest, version?: string } | null> {
|
export async function getManifest(appName: string): Promise<{ exists: boolean, manifest?: Manifest, version?: string } | null> {
|
||||||
try {
|
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.status === 404) return { exists: false }
|
||||||
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`)
|
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`)
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
|
|
@ -74,7 +77,7 @@ export async function post<T, B = unknown>(url: string, body?: B): Promise<T | u
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: body !== undefined ? { 'Content-Type': 'application/json' } : undefined,
|
headers: body !== undefined ? { 'Content-Type': 'application/json' } : undefined,
|
||||||
body: body !== undefined ? JSON.stringify(body) : undefined,
|
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||||||
signal: activeSignal,
|
signal: getSignal(),
|
||||||
})
|
})
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const text = await res.text()
|
const text = await res.text()
|
||||||
|
|
@ -92,7 +95,7 @@ export async function put(url: string, body: Buffer | Uint8Array): Promise<boole
|
||||||
const res = await fetch(makeUrl(url), {
|
const res = await fetch(makeUrl(url), {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: body as BodyInit,
|
body: body as BodyInit,
|
||||||
signal: activeSignal,
|
signal: getSignal(),
|
||||||
})
|
})
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const text = await res.text()
|
const text = await res.text()
|
||||||
|
|
@ -109,7 +112,7 @@ export async function put(url: string, body: Buffer | Uint8Array): Promise<boole
|
||||||
export async function download(url: string): Promise<Buffer | undefined> {
|
export async function download(url: string): Promise<Buffer | undefined> {
|
||||||
try {
|
try {
|
||||||
const fullUrl = makeUrl(url)
|
const fullUrl = makeUrl(url)
|
||||||
const res = await fetch(fullUrl, { signal: activeSignal })
|
const res = await fetch(fullUrl, { signal: getSignal() })
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const text = await res.text()
|
const text = await res.text()
|
||||||
const msg = tryParseError(text) ?? `${res.status} ${res.statusText}`
|
const msg = tryParseError(text) ?? `${res.status} ${res.statusText}`
|
||||||
|
|
@ -125,7 +128,7 @@ export async function del(url: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(makeUrl(url), {
|
const res = await fetch(makeUrl(url), {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
signal: activeSignal,
|
signal: getSignal(),
|
||||||
})
|
})
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const text = await res.text()
|
const text = await res.text()
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import * as readline from 'readline'
|
||||||
|
|
||||||
import color from 'kleur'
|
import color from 'kleur'
|
||||||
|
|
||||||
import { clearSignal, get, handleError, HOST, setSignal } from './http'
|
import { get, handleError, HOST, withSignal } from './http'
|
||||||
import { program } from './setup'
|
import { program } from './setup'
|
||||||
import { STATE_ICONS } from './commands/manage'
|
import { STATE_ICONS } from './commands/manage'
|
||||||
|
|
||||||
|
|
@ -188,13 +188,12 @@ export async function shell(): Promise<void> {
|
||||||
// Set up AbortController for this command
|
// Set up AbortController for this command
|
||||||
activeAbort = new AbortController()
|
activeAbort = new AbortController()
|
||||||
const signal = activeAbort.signal
|
const signal = activeAbort.signal
|
||||||
setSignal(signal)
|
|
||||||
|
|
||||||
// Pause readline so commands can use their own prompts
|
// Pause readline so commands can use their own prompts
|
||||||
rl.pause()
|
rl.pause()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await program.parseAsync(['node', 'toes', ...tokens])
|
await withSignal(signal, () => program.parseAsync(['node', 'toes', ...tokens]))
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
// Commander throws on exitOverride — suppress help/version exits
|
// Commander throws on exitOverride — suppress help/version exits
|
||||||
if (err && typeof err === 'object' && 'code' in err) {
|
if (err && typeof err === 'object' && 'code' in err) {
|
||||||
|
|
@ -213,7 +212,6 @@ export async function shell(): Promise<void> {
|
||||||
handleError(err)
|
handleError(err)
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
clearSignal()
|
|
||||||
activeAbort = null
|
activeAbort = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user