diff --git a/.changeset/spicy-geese-arrive.md b/.changeset/spicy-geese-arrive.md new file mode 100644 index 00000000000..0c61cbab685 --- /dev/null +++ b/.changeset/spicy-geese-arrive.md @@ -0,0 +1,5 @@ +--- +"@smithy/util-waiter": minor +--- + +add type information to WaiterResult reason diff --git a/packages/util-waiter/src/createWaiter.ts b/packages/util-waiter/src/createWaiter.ts index 235c27e5470..1b482a29ff3 100644 --- a/packages/util-waiter/src/createWaiter.ts +++ b/packages/util-waiter/src/createWaiter.ts @@ -5,15 +5,15 @@ import { validateWaiterOptions } from "./utils"; import type { WaiterOptions, WaiterResult } from "./waiter"; import { waiterServiceDefaults, WaiterState } from "./waiter"; -const abortTimeout = ( +const abortTimeout = ( abortSignal: AbortSignal | DeprecatedAbortSignal ): { clearListener: () => void; - aborted: Promise; + aborted: Promise>; } => { let onAbort: () => void; - const promise = new Promise((resolve) => { + const promise = new Promise>((resolve) => { onAbort = () => resolve({ state: WaiterState.ABORTED }); if (typeof (abortSignal as AbortSignal).addEventListener === "function") { // preferred. @@ -43,33 +43,33 @@ const abortTimeout = ( * * @internal */ -export const createWaiter = async ( +export const createWaiter = async ( options: WaiterOptions, input: Input, - acceptorChecks: (client: Client, input: Input) => Promise -): Promise => { + acceptorChecks: (client: Client, input: Input) => Promise> +): Promise> => { const params = { ...waiterServiceDefaults, ...options, }; validateWaiterOptions(params); - const exitConditions = [runPolling(params, input, acceptorChecks)]; + const exitConditions = [runPolling(params, input, acceptorChecks)]; const finalize = [] as Array<() => void>; if (options.abortSignal) { - const { aborted, clearListener } = abortTimeout(options.abortSignal); + const { aborted, clearListener } = abortTimeout(options.abortSignal); finalize.push(clearListener); exitConditions.push(aborted); } if (options.abortController?.signal) { - const { aborted, clearListener } = abortTimeout(options.abortController.signal); + const { aborted, clearListener } = abortTimeout(options.abortController.signal); finalize.push(clearListener); exitConditions.push(aborted); } - return Promise.race(exitConditions).then((result) => { + return Promise.race>(exitConditions).then((result) => { for (const fn of finalize) { fn(); } diff --git a/packages/util-waiter/src/poller.ts b/packages/util-waiter/src/poller.ts index 08555119f02..55257b0f505 100644 --- a/packages/util-waiter/src/poller.ts +++ b/packages/util-waiter/src/poller.ts @@ -4,9 +4,8 @@ import type { WaiterOptions, WaiterResult } from "./waiter"; import { WaiterState } from "./waiter"; /** - * @internal - * * Reference: https://smithy.io/2.0/additional-specs/waiters.html#waiter-retries + * @internal */ const exponentialBackoffWithJitter = (minDelay: number, maxDelay: number, attemptCeiling: number, attempt: number) => { if (attempt > attemptCeiling) return maxDelay; @@ -24,11 +23,11 @@ const randomInRange = (min: number, max: number) => min + Math.random() * (max - * @param input - client input * @param acceptorChecks - function that checks the acceptor states on each poll. */ -export const runPolling = async ( +export const runPolling = async ( { minDelay, maxDelay, maxWaitTime, abortController, client, abortSignal }: WaiterOptions, input: Input, - acceptorChecks: (client: Client, input: Input) => Promise -): Promise => { + acceptorChecks: (client: Client, input: Input) => Promise> +): Promise> => { const observedResponses: Record = {}; const { state, reason } = await acceptorChecks(client, input); @@ -78,9 +77,10 @@ export const runPolling = async ( }; /** - * @internal - * convert the result of an SDK operation, either an error or response object, to a + * Convert the result of an SDK operation, either an error or response object, to a * readable string. + * + * @internal */ const createMessageFromResponse = (reason: any): string => { if (reason?.$responseBodyText) { diff --git a/packages/util-waiter/src/waiter.ts b/packages/util-waiter/src/waiter.ts index 92f63b4b84c..1cfec817ea2 100644 --- a/packages/util-waiter/src/waiter.ts +++ b/packages/util-waiter/src/waiter.ts @@ -1,11 +1,8 @@ -import type { WaiterConfiguration as WaiterConfiguration__ } from "@smithy/types"; +import type { WaiterConfiguration } from "@smithy/types"; import { getCircularReplacer } from "./circularReplacer"; -/** - * @internal - */ -export interface WaiterConfiguration extends WaiterConfiguration__ {} +export { WaiterConfiguration }; /** * @internal @@ -22,7 +19,7 @@ export type WaiterOptions = WaiterConfiguration & Required, "minDelay" | "maxDelay">>; /** - * @internal + * @public */ export enum WaiterState { ABORTED = "ABORTED", @@ -33,15 +30,15 @@ export enum WaiterState { } /** - * @internal + * @public */ -export type WaiterResult = { +export type WaiterResult = { state: WaiterState; /** * (optional) Indicates a reason for why a waiter has reached its state. */ - reason?: any; + reason?: R; /** * Responses observed by the waiter during its polling, where the value @@ -51,12 +48,11 @@ export type WaiterResult = { }; /** - * @internal - * * Handles and throws exceptions resulting from the waiterResult + * @internal * @param result - WaiterResult */ -export const checkExceptions = (result: WaiterResult): WaiterResult => { +export const checkExceptions = (result: WaiterResult): WaiterResult => { if (result.state === WaiterState.ABORTED) { const abortError = new Error( `${JSON.stringify( diff --git a/private/my-local-model-schema/package.json b/private/my-local-model-schema/package.json index 81429ebb964..389eb7463df 100644 --- a/private/my-local-model-schema/package.json +++ b/private/my-local-model-schema/package.json @@ -48,6 +48,7 @@ "@smithy/util-middleware": "workspace:^", "@smithy/util-retry": "workspace:^", "@smithy/util-utf8": "workspace:^", + "@smithy/util-waiter": "workspace:^", "tslib": "^2.6.2" }, "devDependencies": { diff --git a/private/my-local-model-schema/src/index.ts b/private/my-local-model-schema/src/index.ts index d1f3d937aad..d62d7a0e121 100644 --- a/private/my-local-model-schema/src/index.ts +++ b/private/my-local-model-schema/src/index.ts @@ -12,6 +12,7 @@ export type { RuntimeExtension } from "./runtimeExtensions"; export type { XYZServiceExtensionConfiguration } from "./extensionConfiguration"; export * from "./commands"; export * from "./schemas/schemas_0"; +export * from "./waiters"; export * from "./models/errors"; export * from "./models/models_0"; diff --git a/private/my-local-model-schema/src/waiters/index.ts b/private/my-local-model-schema/src/waiters/index.ts new file mode 100644 index 00000000000..f38d196af1f --- /dev/null +++ b/private/my-local-model-schema/src/waiters/index.ts @@ -0,0 +1,2 @@ +// smithy-typescript generated code +export * from "./waitForNumbersAligned"; diff --git a/private/my-local-model-schema/src/waiters/waitForNumbersAligned.ts b/private/my-local-model-schema/src/waiters/waitForNumbersAligned.ts new file mode 100644 index 00000000000..8acc212587e --- /dev/null +++ b/private/my-local-model-schema/src/waiters/waitForNumbersAligned.ts @@ -0,0 +1,51 @@ +// smithy-typescript generated code +import { checkExceptions, createWaiter, WaiterConfiguration, WaiterResult, WaiterState } from "@smithy/util-waiter"; + +import { + type GetNumbersCommandInput, + type GetNumbersCommandOutput, + GetNumbersCommand, +} from "../commands/GetNumbersCommand"; +import { XYZServiceClient } from "../XYZServiceClient"; + +const checkState = async (client: XYZServiceClient, input: GetNumbersCommandInput): Promise> => { + let reason; + try { + let result: GetNumbersCommandOutput & any = await client.send(new GetNumbersCommand(input)); + reason = result; + return { state: WaiterState.SUCCESS, reason }; + } catch (exception) { + reason = exception; + if (exception.name === "MysteryThrottlingError") { + return { state: WaiterState.RETRY, reason }; + } + if (exception.name === "HaltError") { + return { state: WaiterState.FAILURE, reason }; + } + } + return { state: WaiterState.RETRY, reason }; +}; +/** + * wait until the numbers align + * @deprecated Use waitUntilNumbersAligned instead. waitForNumbersAligned does not throw error in non-success cases. + */ +export const waitForNumbersAligned = async ( + params: WaiterConfiguration, + input: GetNumbersCommandInput +): Promise> => { + const serviceDefaults = { minDelay: 2, maxDelay: 120 }; + return createWaiter({ ...serviceDefaults, ...params }, input, checkState); +}; +/** + * wait until the numbers align + * @param params - Waiter configuration options. + * @param input - The input to GetNumbersCommand for polling. + */ +export const waitUntilNumbersAligned = async ( + params: WaiterConfiguration, + input: GetNumbersCommandInput +): Promise> => { + const serviceDefaults = { minDelay: 2, maxDelay: 120 }; + const result = await createWaiter({ ...serviceDefaults, ...params }, input, checkState); + return checkExceptions(result); +}; diff --git a/private/my-local-model-schema/test/index-objects.spec.mjs b/private/my-local-model-schema/test/index-objects.spec.mjs index f95ad6392cb..2c7ca0a4165 100644 --- a/private/my-local-model-schema/test/index-objects.spec.mjs +++ b/private/my-local-model-schema/test/index-objects.spec.mjs @@ -19,6 +19,8 @@ import { TradeEventStreamCommand, TradeEventStreamRequest$, TradeEventStreamResponse$, + waitForNumbersAligned, + waitUntilNumbersAligned, XYZService, XYZServiceClient, XYZServiceServiceException, @@ -55,4 +57,7 @@ assert(typeof RetryableError$ === "object"); assert(XYZServiceServiceException.prototype instanceof XYZServiceSyntheticServiceException); assert(typeof XYZServiceServiceException$ === "object"); assert(XYZServiceSyntheticServiceException.prototype instanceof Error); +// waiters +assert(typeof waitForNumbersAligned === "function"); +assert(typeof waitUntilNumbersAligned === "function"); console.log(`XYZService index test passed.`); diff --git a/private/my-local-model-schema/test/index-types.ts b/private/my-local-model-schema/test/index-types.ts index fb0196a7dcd..deddc4db33d 100644 --- a/private/my-local-model-schema/test/index-types.ts +++ b/private/my-local-model-schema/test/index-types.ts @@ -21,4 +21,6 @@ export type { RetryableError, XYZServiceServiceException, XYZServiceSyntheticServiceException, + waitForNumbersAligned, + waitUntilNumbersAligned, } from "../dist-types/index.d"; diff --git a/private/my-local-model/package.json b/private/my-local-model/package.json index a4cc68a4ca1..cb1cc983693 100644 --- a/private/my-local-model/package.json +++ b/private/my-local-model/package.json @@ -47,6 +47,7 @@ "@smithy/util-middleware": "workspace:^", "@smithy/util-retry": "workspace:^", "@smithy/util-utf8": "workspace:^", + "@smithy/util-waiter": "workspace:^", "tslib": "^2.6.2" }, "devDependencies": { diff --git a/private/my-local-model/src/index.ts b/private/my-local-model/src/index.ts index 4cd5e074e6b..6f95cbf8672 100644 --- a/private/my-local-model/src/index.ts +++ b/private/my-local-model/src/index.ts @@ -11,6 +11,7 @@ export { ClientInputEndpointParameters } from "./endpoint/EndpointParameters"; export type { RuntimeExtension } from "./runtimeExtensions"; export type { XYZServiceExtensionConfiguration } from "./extensionConfiguration"; export * from "./commands"; +export * from "./waiters"; export * from "./models/errors"; export * from "./models/models_0"; diff --git a/private/my-local-model/src/waiters/index.ts b/private/my-local-model/src/waiters/index.ts new file mode 100644 index 00000000000..f38d196af1f --- /dev/null +++ b/private/my-local-model/src/waiters/index.ts @@ -0,0 +1,2 @@ +// smithy-typescript generated code +export * from "./waitForNumbersAligned"; diff --git a/private/my-local-model/src/waiters/waitForNumbersAligned.ts b/private/my-local-model/src/waiters/waitForNumbersAligned.ts new file mode 100644 index 00000000000..f0b793f377a --- /dev/null +++ b/private/my-local-model/src/waiters/waitForNumbersAligned.ts @@ -0,0 +1,47 @@ +// smithy-typescript generated code +import { checkExceptions, createWaiter, WaiterConfiguration, WaiterResult, WaiterState } from "@smithy/util-waiter"; + +import { GetNumbersCommand, GetNumbersCommandInput, GetNumbersCommandOutput } from "../commands/GetNumbersCommand"; +import { XYZServiceClient } from "../XYZServiceClient"; + +const checkState = async (client: XYZServiceClient, input: GetNumbersCommandInput): Promise> => { + let reason; + try { + let result: GetNumbersCommandOutput & any = await client.send(new GetNumbersCommand(input)); + reason = result; + return { state: WaiterState.SUCCESS, reason }; + } catch (exception) { + reason = exception; + if (exception.name === "MysteryThrottlingError") { + return { state: WaiterState.RETRY, reason }; + } + if (exception.name === "HaltError") { + return { state: WaiterState.FAILURE, reason }; + } + } + return { state: WaiterState.RETRY, reason }; +}; +/** + * wait until the numbers align + * @deprecated Use waitUntilNumbersAligned instead. waitForNumbersAligned does not throw error in non-success cases. + */ +export const waitForNumbersAligned = async ( + params: WaiterConfiguration, + input: GetNumbersCommandInput +): Promise> => { + const serviceDefaults = { minDelay: 2, maxDelay: 120 }; + return createWaiter({ ...serviceDefaults, ...params }, input, checkState); +}; +/** + * wait until the numbers align + * @param params - Waiter configuration options. + * @param input - The input to GetNumbersCommand for polling. + */ +export const waitUntilNumbersAligned = async ( + params: WaiterConfiguration, + input: GetNumbersCommandInput +): Promise> => { + const serviceDefaults = { minDelay: 2, maxDelay: 120 }; + const result = await createWaiter({ ...serviceDefaults, ...params }, input, checkState); + return checkExceptions(result); +}; diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/WaiterGenerator.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/WaiterGenerator.java index 303d7434118..4a48afa896d 100644 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/WaiterGenerator.java +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/WaiterGenerator.java @@ -37,6 +37,8 @@ class WaiterGenerator implements Runnable { private final Symbol serviceSymbol; private final Symbol operationSymbol; private final Symbol inputSymbol; + private final Symbol outputSymbol; + private final String waiterResultType; WaiterGenerator( String waiterName, @@ -53,6 +55,8 @@ class WaiterGenerator implements Runnable { this.operationSymbol = symbolProvider.toSymbol(operation); this.serviceSymbol = symbolProvider.toSymbol(service); this.inputSymbol = operationSymbol.expectProperty("inputType", Symbol.class); + this.outputSymbol = operationSymbol.expectProperty("outputType", Symbol.class); + waiterResultType = outputSymbol.getName() + " | Error"; } @Override @@ -89,11 +93,12 @@ private void generateWaiter() { export const waitFor$L = async ( params: WaiterConfiguration<$T>, input: $T - ): Promise => {""", + ): Promise> => {""", "};", waiterName, serviceSymbol, inputSymbol, + waiterResultType, () -> { writer.write( "const serviceDefaults = { minDelay: $L, maxDelay: $L };", @@ -118,11 +123,12 @@ private void generateWaiter() { export const waitUntil$L = async ( params: WaiterConfiguration<$T>, input: $T - ): Promise => {""", + ): Promise> => {""", "};", waiterName, serviceSymbol, inputSymbol, + waiterResultType, () -> { writer.write( "const serviceDefaults = { minDelay: $L, maxDelay: $L };", @@ -139,16 +145,21 @@ private void generateWaiter() { private void generateAcceptors() { writer.openBlock( - "const checkState = async (client: $T, input: $T): Promise => {", + "const checkState = async (client: $T, input: $T): Promise> => {", "};", serviceSymbol, inputSymbol, + waiterResultType, () -> { writer.write("let reason;"); writer.write("try {").indent(); { - writer.write("let result: any = await client.send(new $T(input));", operationSymbol); + writer.write( + "let result: $T & any = await client.send(new $T(input));", + outputSymbol, + operationSymbol + ); writer.write("reason = result;"); writeAcceptors("result", false); } @@ -205,7 +216,7 @@ private void generateSuccessMatcher(Matcher.SuccessMember member, AcceptorState } private void generateErrorMatcher(String accessor, Matcher.ErrorTypeMember member, AcceptorState state) { - writer.openBlock("if ($L.name && $L.name == $S) {", "}", accessor, accessor, member.getValue(), () -> { + writer.openBlock("if ($L.name === $S) {", "}", accessor, member.getValue(), () -> { writer.write("return $L;", makeWaiterResult(state)); }); } diff --git a/smithy-typescript-protocol-test-codegen/model/my-local-model/main.smithy b/smithy-typescript-protocol-test-codegen/model/my-local-model/main.smithy index 59434fd81bd..118e8b9f8b0 100644 --- a/smithy-typescript-protocol-test-codegen/model/my-local-model/main.smithy +++ b/smithy-typescript-protocol-test-codegen/model/my-local-model/main.smithy @@ -5,6 +5,7 @@ namespace org.xyz.v1 use smithy.protocols#rpcv2Cbor use smithy.rules#clientContextParams use smithy.rules#endpointRuleSet +use smithy.waiters#waitable @rpcv2Cbor @documentation("xyz interfaces") @@ -77,6 +78,25 @@ service XYZService { @httpError(400) structure MainServiceLinkedError {} +@waitable( + NumbersAligned: { + documentation: "wait until the numbers align" + acceptors: [ + { + state: "success" + matcher: { success: true } + } + { + state: "retry" + matcher: { errorType: "MysteryThrottlingError" } + } + { + state: "failure" + matcher: { errorType: "HaltError" } + } + ] + } +) @readonly operation GetNumbers { input: GetNumbersRequest diff --git a/yarn.lock b/yarn.lock index 87d79a8dd41..30bc09846ae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3677,7 +3677,7 @@ __metadata: languageName: unknown linkType: soft -"@smithy/util-waiter@workspace:packages/util-waiter": +"@smithy/util-waiter@workspace:^, @smithy/util-waiter@workspace:packages/util-waiter": version: 0.0.0-use.local resolution: "@smithy/util-waiter@workspace:packages/util-waiter" dependencies: @@ -12234,6 +12234,7 @@ __metadata: "@smithy/util-middleware": "workspace:^" "@smithy/util-retry": "workspace:^" "@smithy/util-utf8": "workspace:^" + "@smithy/util-waiter": "workspace:^" "@tsconfig/node20": "npm:20.1.8" "@types/node": "npm:^20.14.8" concurrently: "npm:7.0.0" @@ -12279,6 +12280,7 @@ __metadata: "@smithy/util-middleware": "workspace:^" "@smithy/util-retry": "workspace:^" "@smithy/util-utf8": "workspace:^" + "@smithy/util-waiter": "workspace:^" "@tsconfig/node20": "npm:20.1.8" "@types/node": "npm:^20.14.8" concurrently: "npm:7.0.0"