diff --git a/eslint.config.js b/eslint.config.js index ed6028ea..022e68fb 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -16,6 +16,8 @@ export default [ clearTimeout: "readonly", URL: "readonly", Blob: "readonly", + AbortController: "readonly", + AbortSignal: "readonly", NodeJS: true, }, }, diff --git a/src/actions/auth/login.ts b/src/actions/auth/login.ts index f2e72601..17c59b01 100644 --- a/src/actions/auth/login.ts +++ b/src/actions/auth/login.ts @@ -8,6 +8,7 @@ import { LoginPrompts } from "../../prompts/auth/login.js"; import { CommandMetadata } from "../../types/common/command-metadata.js"; import { ActionResult } from "../action-result.js"; import { err, ok, Result } from "neverthrow"; +import { sleep } from "../../infrastructure/timer-extensions.js"; type LoginTimeout = "TIMEOUT"; @@ -58,7 +59,7 @@ export class LoginAction { if (result.isOk()) { return ok(result.value.apiKey); } - await new Promise((resolve) => setTimeout(resolve, delayMs)); + await sleep(delayMs); } } } diff --git a/src/infrastructure/file-service.ts b/src/infrastructure/file-service.ts index c06541c4..4be5eab7 100644 --- a/src/infrastructure/file-service.ts +++ b/src/infrastructure/file-service.ts @@ -7,6 +7,7 @@ import { FilePath } from "../types/file/filePath.js"; import { DirectoryPath } from "../types/file/directoryPath.js"; import { Directory } from "../types/file/directory.js"; import { FileName } from "../types/file/fileName.js"; +import { sleep } from "./timer-extensions.js"; export class FileService { @@ -169,7 +170,7 @@ export class FileService { onDeleteFailurePersists(); actionPerformed = true; } - await new Promise((resolve) => setTimeout(resolve, 500)); + await sleep(500); } } diff --git a/src/infrastructure/services/portal-service.ts b/src/infrastructure/services/portal-service.ts index cb2ddbb7..e9ed80a6 100644 --- a/src/infrastructure/services/portal-service.ts +++ b/src/infrastructure/services/portal-service.ts @@ -23,6 +23,7 @@ import { Sdl } from "../../types/sdl/sdl.js"; import { FilePath } from "../../types/file/filePath.js"; import { DirectoryPath } from "../../types/file/directoryPath.js"; import { FileService } from "../file-service.js"; +import { sleep } from "../timer-extensions.js"; import { apiClientFactory } from "./api-client-factory.js"; import { CommandMetadata } from "../../types/common/command-metadata.js"; import { err, ok, Result } from "neverthrow"; @@ -322,7 +323,7 @@ async function pollUntilCompleted( formatValidationError: ValidationErrorFormatter = formatValidationErrors ): Promise> { for (;;) { - await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)); + await sleep(pollIntervalMs); const statusResult = await fetchStatus(); if (statusResult.isErr()) { diff --git a/src/infrastructure/timer-extensions.ts b/src/infrastructure/timer-extensions.ts new file mode 100644 index 00000000..dbbbb6d5 --- /dev/null +++ b/src/infrastructure/timer-extensions.ts @@ -0,0 +1,11 @@ +import { setTimeout } from 'node:timers/promises'; + +export type SleepResult = 'ok' | 'failed' | 'cancelled'; + +export async function sleep(ms: number, signal?: AbortSignal): Promise { + try { + return await setTimeout(ms, 'ok', { signal }); + } catch { + return signal?.aborted ? 'cancelled' : 'failed'; + } +} diff --git a/src/prompts/sdk/publish.ts b/src/prompts/sdk/publish.ts index 99d0bd4f..7f7af042 100644 --- a/src/prompts/sdk/publish.ts +++ b/src/prompts/sdk/publish.ts @@ -1,6 +1,7 @@ import { log, spinner } from '@clack/prompts'; import { Result } from 'neverthrow'; import { ServiceError } from '../../infrastructure/service-error.js'; +import { sleep } from '../../infrastructure/timer-extensions.js'; import { PublishLogItem } from '../../types/publish-api/publish-log.js'; import { PublishingInfo } from '../../types/publish-api/publishing-info.js'; import { PublishType } from '../../types/publish-api/publishing-profile-item.js'; @@ -55,10 +56,10 @@ ${f.link(publishingLogUrl)}`; const TERMINAL_STATES = new Set(['Succeeded', 'Failed', 'Exception', 'InternalError']); const POLL_INTERVAL_MS = 10000; // poll after every 10 seconds. - let abortWait: (() => void) | undefined; + const waitAbort = new AbortController(); const spin = spinner({ onCancel: () => { - abortWait?.(); + waitAbort.abort(); }, cancelMessage: 'Publishing is still running on APIMatic and will continue without the CLI.' }); @@ -96,14 +97,11 @@ ${f.link(publishingLogUrl)}`; } spin.message(statusMessage); - await new Promise((resolve) => { - const timer = setTimeout(resolve, POLL_INTERVAL_MS); - abortWait = () => { - clearTimeout(timer); - resolve(); - }; - }); - abortWait = undefined; + + const sleepResult = await sleep(POLL_INTERVAL_MS, waitAbort.signal); + if (sleepResult !== 'ok') { + return sleepResult; + } } return 'cancelled';