From 0094edfda8505471f79e18a195c0afb450333b97 Mon Sep 17 00:00:00 2001 From: Jakub Vacek Date: Tue, 11 Jul 2023 16:01:51 +0200 Subject: [PATCH 1/2] fix: polling errors --- src/commands/map.ts | 10 +++--- src/common/error.ts | 16 ++++----- src/common/polling.ts | 84 +++++++++++++++++++++++++------------------ src/common/ux.ts | 11 ++++-- src/logic/map.ts | 17 ++++----- src/logic/new.ts | 38 ++++++++++---------- src/logic/prepare.ts | 7 ++-- 7 files changed, 104 insertions(+), 79 deletions(-) diff --git a/src/commands/map.ts b/src/commands/map.ts index 19c7a5e6..94c3c4ee 100644 --- a/src/commands/map.ts +++ b/src/commands/map.ts @@ -128,8 +128,8 @@ export default class Map extends Command { boilerplate.saved ? `Boilerplate code prepared for ${resolvedLanguage} at ${boilerplate.path}` : `Boilerplate for ${getLanguageName( - resolvedLanguage - )} already exists at ${boilerplate.path}.` + resolvedLanguage + )} already exists at ${boilerplate.path}.` ); if (boilerplate.envVariables !== undefined) { @@ -154,10 +154,8 @@ export default class Map extends Command { ux.warn(project.installationGuide); ux.succeed( - `Local project set up. You can now install defined dependencies and run \`superface execute ${ - resolvedProviderJson.providerJson.name - } ${ - ProfileId.fromScopeName(profile.scope, profile.name).id + `Local project set up. You can now install defined dependencies and run \`superface execute ${resolvedProviderJson.providerJson.name + } ${ProfileId.fromScopeName(profile.scope, profile.name).id }\` to execute your integration.` ); } diff --git a/src/common/error.ts b/src/common/error.ts index 5f30749d..f4ba67a2 100644 --- a/src/common/error.ts +++ b/src/common/error.ts @@ -12,16 +12,16 @@ import { UX } from './ux'; */ export const createUserError = (emoji: boolean) => - (message: string, code: number): CLIError => { - // Make sure that UX is stoped before throwing an error. - UX.clear(); + (message: string, code: number): CLIError => { + // Make sure that UX is stoped before throwing an error. + UX.clear(); - if (code <= 0) { - throw developerError('expected positive error code', 1); - } + if (code <= 0) { + throw developerError('expected positive error code', 1); + } - return new CLIError(emoji ? '❌ ' + message : message, { exit: code }); - }; + return new CLIError(emoji ? '❌ ' + message : message, { exit: code }); + }; export type UserError = ReturnType; export type DeveloperError = typeof developerError; diff --git a/src/common/polling.ts b/src/common/polling.ts index 63ad9a44..fdc13f9d 100644 --- a/src/common/polling.ts +++ b/src/common/polling.ts @@ -4,7 +4,7 @@ import type { UserError } from './error'; import type { UX } from './ux'; export const DEFAULT_POLLING_TIMEOUT_SECONDS = 300; -export const DEFAULT_POLLING_INTERVAL_SECONDS = 1; +export const DEFAULT_POLLING_INTERVAL_SECONDS = 2; enum PollStatus { Success = 'Success', @@ -19,24 +19,24 @@ enum PollResultType { } type PollResponse = | { - result_url: string; - status: PollStatus.Success; - result_type: PollResultType; - } + result_url: string; + status: PollStatus.Success; + result_type: PollResultType; + } | { - status: PollStatus.Pending; - events: { - occuredAt: Date; - type: string; - description: string; - }[]; - result_type: PollResultType; - } + status: PollStatus.Pending; + events: { + occuredAt: Date; + type: string; + description: string; + }[]; + result_type: PollResultType; + } | { - status: PollStatus.Failed; - failure_reason: string; - result_type: PollResultType; - } + status: PollStatus.Failed; + failure_reason: string; + result_type: PollResultType; + } | { status: PollStatus.Cancelled; result_type: PollResultType }; function isPollResponse(input: unknown): input is PollResponse { @@ -94,12 +94,10 @@ export async function pollUrl( }; }, { - // logger, client, userError, ux, }: { - // logger: ILogger; client: ServiceClient; userError: UserError; ux: UX; @@ -111,6 +109,8 @@ export async function pollUrl( const pollingIntervalMilliseconds = (options?.pollingIntervalSeconds ?? DEFAULT_POLLING_INTERVAL_SECONDS) * 1000; + + let lastEvenetDescription = ''; while ( new Date().getTime() - startPollingTimeStamp.getTime() < timeoutMilliseconds @@ -118,11 +118,11 @@ export async function pollUrl( const result = await pollFetch(url, { client, userError }); if (result.status === PollStatus.Success) { + ux.succeed(`Successfully finished operation`); return result.result_url; } else if (result.status === PollStatus.Failed) { throw userError( - `Failed to ${getJobDescription(result.result_type)}: ${ - result.failure_reason + `Failed to ${getJobDescription(result.result_type)}: ${result.failure_reason }`, 1 ); @@ -133,19 +133,23 @@ export async function pollUrl( )}: Operation has been cancelled.`, 1 ); + } else if (result.status === PollStatus.Pending) { + // get events from response and present them to user + if (result.events.length > 0 && options?.quiet !== true) { + const currentEvent = result.events[result.events.length - 1]; + + if (currentEvent.description !== lastEvenetDescription) { + // console.log(`${currentEvent.type} - ${currentEvent.description}`); + ux.info(`${currentEvent.type} - ${currentEvent.description}`); + } + + lastEvenetDescription = currentEvent.description; + + await new Promise(resolve => + setTimeout(resolve, pollingIntervalMilliseconds) + ); + } } - - // get events from response and present them to user - if (result.events.length > 0 && options?.quiet !== true) { - const lastEvent = result.events[result.events.length - 1]; - - ux.info(`${lastEvent.type} - ${lastEvent.description}`); - // logger.info('pollingEvent', lastEvent.type, lastEvent.description); - } - - await new Promise(resolve => - setTimeout(resolve, pollingIntervalMilliseconds) - ); } throw userError( @@ -179,7 +183,19 @@ async function pollFetch( }, }); if (result.status === 200) { - const data = (await result.json()) as unknown; + let data: unknown; + try { + data = (await result.json()) as unknown; + } catch (error) { + throw userError( + `Unexpected response from server: ${JSON.stringify( + await result.text(), + null, + 2 + )}`, + 1 + ); + } if (isPollResponse(data)) { return data; diff --git a/src/common/ux.ts b/src/common/ux.ts index 661f13da..00eb23c5 100644 --- a/src/common/ux.ts +++ b/src/common/ux.ts @@ -10,7 +10,7 @@ export class UX { private lastText = ''; private constructor() { - this.spinner = createSpinner(undefined, { color: 'cyan', interval: 25 }); + this.spinner = createSpinner(undefined, { color: 'cyan', interval: 50 }); UX.instance = this; } @@ -28,7 +28,9 @@ export class UX { } public info(text: string): void { - if (text !== this.lastText) { + if (text.trim() !== this.lastText.trim()) { + + this.spinner.clear(); this.spinner.update({ text }); } @@ -39,6 +41,10 @@ export class UX { this.spinner.warn({ text: yellow(text), mark: yellow('⚠') }); } + public stop(): void { + this.spinner.stop(); + } + public static create(): UX { if (UX.instance === undefined) { UX.instance = new UX(); @@ -48,6 +54,7 @@ export class UX { } public static clear(): void { + UX.instance?.spinner.clear(); UX.instance?.spinner.stop(); } } diff --git a/src/logic/map.ts b/src/logic/map.ts index ce4de44b..6fd356b9 100644 --- a/src/logic/map.ts +++ b/src/logic/map.ts @@ -11,7 +11,8 @@ export type MapPreparationResponse = { }; function assertMapResponse( - input: unknown + input: unknown, + { userError }: { userError: UserError } ): asserts input is MapPreparationResponse { if (typeof input === 'object' && input !== null && 'source' in input) { const tmp = input as { source: string }; @@ -21,7 +22,7 @@ function assertMapResponse( } } - throw Error(`Unexpected response received`); + throw userError(`Unexpected response received`, 1); } export async function mapProviderToProfile( @@ -58,6 +59,7 @@ export async function mapProviderToProfile( return ( await finishMapPreparation(resultUrl, { client, + userError, }) ).source; } @@ -78,9 +80,8 @@ async function startMapPreparation( }, { client, userError }: { client: ServiceClient; userError: UserError } ): Promise { - const profileId = `${profile.scope !== undefined ? profile.scope + '.' : ''}${ - profile.name - }`; + const profileId = `${profile.scope !== undefined ? profile.scope + '.' : ''}${profile.name + }`; const jobUrlResponse = await client.fetch( `/authoring/profiles/${profileId}/maps`, { @@ -127,7 +128,7 @@ async function startMapPreparation( async function finishMapPreparation( resultUrl: string, - { client }: { client: ServiceClient } + { client, userError }: { client: ServiceClient, userError: UserError } ): Promise { const resultResponse = await client.fetch(resultUrl, { method: 'GET', @@ -139,12 +140,12 @@ async function finishMapPreparation( }); if (resultResponse.status !== 200) { - throw Error(`Unexpected status code ${resultResponse.status} received`); + throw userError(`Unexpected status code ${resultResponse.status} received`, 1); } const body = (await resultResponse.json()) as unknown; - assertMapResponse(body); + assertMapResponse(body, { userError }); return body; } diff --git a/src/logic/new.ts b/src/logic/new.ts index 6b312616..c26caa08 100644 --- a/src/logic/new.ts +++ b/src/logic/new.ts @@ -1,5 +1,5 @@ import type { ProviderJson } from '@superfaceai/ast'; -import { parseDocumentId, parseProfile, Source } from '@superfaceai/parser'; +import { parseDocumentId } from '@superfaceai/parser'; import type { ServiceClient } from '@superfaceai/service-client'; import type { UserError } from '../common/error'; @@ -17,7 +17,8 @@ export type ProfilePreparationResponse = { }; function assertProfileResponse( - input: unknown + input: unknown, + { userError }: { userError: UserError } ): asserts input is ProfilePreparationResponse { if ( typeof input === 'object' && @@ -28,26 +29,26 @@ function assertProfileResponse( const tmp = input as { id: string; profile: { source?: string } }; if (typeof tmp.profile.source !== 'string') { - throw Error( + throw userError( `Unexpected response received - missing profile source: ${JSON.stringify( tmp, null, 2 - )}` + )}`, 1 ); } - try { - parseProfile(new Source(tmp.profile.source)); - } catch (e) { - throw Error( - `Unexpected response received - unable to parse profile source: ${JSON.stringify( - e, - null, - 2 - )}` - ); - } + // try { + // parseProfile(new Source(tmp.profile.source)); + // } catch (e) { + // throw userError( + // `Unexpected response received - unable to parse profile source: ${JSON.stringify( + // e, + // null, + // 2 + // )}`, 1 + // ); + // } // TODO: validate id format? if (typeof tmp.id === 'string') { @@ -84,6 +85,7 @@ export async function newProfile( const profileResponse = await finishProfilePreparation(resultUrl, { client, + userError, }); // Supports both . and / in profile id @@ -146,7 +148,7 @@ async function startProfilePreparation( async function finishProfilePreparation( resultUrl: string, - { client }: { client: ServiceClient } + { client, userError }: { client: ServiceClient, userError: UserError } ): Promise { const resultResponse = await client.fetch(resultUrl, { method: 'GET', @@ -158,12 +160,12 @@ async function finishProfilePreparation( }); if (resultResponse.status !== 200) { - throw Error(`Unexpected status code ${resultResponse.status} received`); + throw userError(`Unexpected status code ${resultResponse.status} received`, 1); } const body = (await resultResponse.json()) as unknown; - assertProfileResponse(body); + assertProfileResponse(body, { userError }); return body; } diff --git a/src/logic/prepare.ts b/src/logic/prepare.ts index 7d135b97..2ad12edf 100644 --- a/src/logic/prepare.ts +++ b/src/logic/prepare.ts @@ -14,7 +14,8 @@ export type ProviderPreparationResponse = { }; function assertProviderResponse( - input: unknown + input: unknown, + { userError }: { userError: UserError } ): asserts input is ProviderPreparationResponse { if ( typeof input === 'object' && @@ -39,7 +40,7 @@ function assertProviderResponse( } } - throw Error(`Unexpected response received`); + throw userError(`Unexpected response received`, 1); } export async function prepareProviderJson( @@ -141,7 +142,7 @@ async function finishProviderPreparation( const body = (await resultResponse.json()) as unknown; - assertProviderResponse(body); + assertProviderResponse(body, { userError }); return body; } From 58b19cd756922ae610274144fd3b7d74f14c8f85 Mon Sep 17 00:00:00 2001 From: Jakub Vacek Date: Mon, 17 Jul 2023 08:48:32 +0200 Subject: [PATCH 2/2] chore: throw on compile error --- src/common/polling.ts | 4 ++-- src/logic/new.ts | 25 +++++++++++++------------ 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/common/polling.ts b/src/common/polling.ts index 05cf0090..d5b29984 100644 --- a/src/common/polling.ts +++ b/src/common/polling.ts @@ -119,8 +119,8 @@ export async function pollUrl( if (result.status === PollStatus.Success) { ux.succeed(`Successfully finished operation`); - -return result.result_url; + + return result.result_url; } else if (result.status === PollStatus.Failed) { throw userError( `Failed to ${getJobDescription(result.result_type)}: ${ diff --git a/src/logic/new.ts b/src/logic/new.ts index 87d35499..dc5fa250 100644 --- a/src/logic/new.ts +++ b/src/logic/new.ts @@ -1,5 +1,5 @@ import type { ProviderJson } from '@superfaceai/ast'; -import { parseDocumentId } from '@superfaceai/parser'; +import { parseDocumentId, parseProfile,Source } from '@superfaceai/parser'; import type { ServiceClient } from '@superfaceai/service-client'; import type { UserError } from '../common/error'; @@ -39,17 +39,18 @@ function assertProfileResponse( ); } - // try { - // parseProfile(new Source(tmp.profile.source)); - // } catch (e) { - // throw userError( - // `Unexpected response received - unable to parse profile source: ${JSON.stringify( - // e, - // null, - // 2 - // )}`, 1 - // ); - // } + try { + parseProfile(new Source(tmp.profile.source)); + } catch (e) { + throw userError( + `Unexpected response received - unable to parse profile source: ${JSON.stringify( + e, + null, + 2 + )}`, + 1 + ); + } // TODO: validate id format? if (typeof tmp.id === 'string') {