From 9fedaa9696ff1ecf5d1e92b28b34d573583a7842 Mon Sep 17 00:00:00 2001 From: AllanFly120 Date: Tue, 28 Jul 2020 14:27:49 -0700 Subject: [PATCH] feat: refactor middleware stack (#1398) * feat(middleware-stack): refactor middleware stack implementation and interface * feat: update dedendents with updated middleware interface * feat(middleware-stack): make middleware stack a pluggable, address feedbacks BREAKING CHANGE: addRelativeTo() now doesn't require step in options. The middleware will be injected to the same step as the toMiddleware --- .../src/fromCognitoIdentity.spec.ts | 32 +- .../src/fromCognitoIdentityPool.spec.ts | 22 +- .../eventstream-handler-node/package.json | 2 +- .../src/bucketEndpointMiddleware.spec.ts | 4 +- .../src/bucketEndpointMiddleware.ts | 6 +- .../src/handling-middleware.ts | 5 +- .../src/prepend-account-id.ts | 6 +- .../src/middleware-endpoint.ts | 6 +- packages/middleware-signing/src/middleware.ts | 6 +- .../src/MiddlewareStack.spec.ts | 504 +++++++++------- .../middleware-stack/src/MiddlewareStack.ts | 560 +++++++----------- packages/middleware-stack/src/types.ts | 42 +- packages/smithy-client/src/client.ts | 4 +- packages/smithy-client/src/command.ts | 6 +- packages/types/src/middleware.ts | 86 +-- .../util-create-request/src/foo.fixture.ts | 8 +- packages/util-create-request/src/index.ts | 3 +- yarn.lock | 5 - 18 files changed, 574 insertions(+), 733 deletions(-) diff --git a/packages/credential-provider-cognito-identity/src/fromCognitoIdentity.spec.ts b/packages/credential-provider-cognito-identity/src/fromCognitoIdentity.spec.ts index 696c8afd6b84..8c4414b06a36 100644 --- a/packages/credential-provider-cognito-identity/src/fromCognitoIdentity.spec.ts +++ b/packages/credential-provider-cognito-identity/src/fromCognitoIdentity.spec.ts @@ -35,12 +35,12 @@ describe("fromCognitoIdentity", () => { expiration, }); - expect(send.mock.calls[0][0]).toEqual( - new GetCredentialsForIdentityCommand({ - IdentityId: identityId, - CustomRoleArn: "myArn", - }) - ); + const sendParam = send.mock.calls[0][0]; + expect(sendParam).toEqual(expect.any(GetCredentialsForIdentityCommand)); + expect(sendParam.input).toEqual({ + IdentityId: identityId, + CustomRoleArn: "myArn", + }); }); it("should resolve logins to string tokens and pass them to the service", async () => { @@ -54,16 +54,16 @@ describe("fromCognitoIdentity", () => { }, })(); - expect(send.mock.calls[0][0]).toEqual( - new GetCredentialsForIdentityCommand({ - IdentityId: identityId, - CustomRoleArn: "myArn", - Logins: { - myDomain: "token", - "www.amazon.com": "expiring nonce", - }, - }) - ); + const sendParam = send.mock.calls[0][0]; + expect(sendParam).toEqual(expect.any(GetCredentialsForIdentityCommand)); + expect(sendParam.input).toMatchObject({ + IdentityId: identityId, + CustomRoleArn: "myArn", + Logins: { + myDomain: "token", + "www.amazon.com": "expiring nonce", + }, + }); }); it("should convert a GetCredentialsForIdentity response without credentials to a provider error", async () => { diff --git a/packages/credential-provider-cognito-identity/src/fromCognitoIdentityPool.spec.ts b/packages/credential-provider-cognito-identity/src/fromCognitoIdentityPool.spec.ts index 0c27fb51b5f1..45f4953ba375 100644 --- a/packages/credential-provider-cognito-identity/src/fromCognitoIdentityPool.spec.ts +++ b/packages/credential-provider-cognito-identity/src/fromCognitoIdentityPool.spec.ts @@ -56,7 +56,10 @@ describe("fromCognitoIdentityPool", () => { }); expect(send.mock.calls.length).toBe(1); - expect(send.mock.calls[0][0]).toEqual(new GetIdCommand({ IdentityPoolId: identityPoolId })); + expect(send.mock.calls[0][0]).toEqual(expect.any(GetIdCommand)); + expect(send.mock.calls[0][0].input).toEqual({ + IdentityPoolId: identityPoolId, + }); expect((fromCognitoIdentity as any).mock.calls.length).toBe(1); expect((fromCognitoIdentity as any).mock.calls[0][0]).toEqual({ @@ -76,15 +79,14 @@ describe("fromCognitoIdentityPool", () => { }, })(); - expect(send.mock.calls[0][0]).toEqual( - new GetIdCommand({ - IdentityPoolId: identityPoolId, - Logins: { - myDomain: "token", - "www.amazon.com": "expiring nonce", - }, - }) - ); + expect(send.mock.calls[0][0]).toEqual(expect.any(GetIdCommand)); + expect(send.mock.calls[0][0].input).toEqual({ + IdentityPoolId: identityPoolId, + Logins: { + myDomain: "token", + "www.amazon.com": "expiring nonce", + }, + }); }); it("should not invoke GetId a second time once an identityID has been fetched", async () => { diff --git a/packages/eventstream-handler-node/package.json b/packages/eventstream-handler-node/package.json index 023681a82710..3d32046d3cc3 100644 --- a/packages/eventstream-handler-node/package.json +++ b/packages/eventstream-handler-node/package.json @@ -25,7 +25,7 @@ "@aws-sdk/util-utf8-node": "1.0.0-gamma.4", "@types/jest": "^26.0.4", "jest": "^26.1.0", - "typescript": "~3.4.0" + "typescript": "~3.9.3" }, "engines": { "node": ">= 10.0.0" diff --git a/packages/middleware-bucket-endpoint/src/bucketEndpointMiddleware.spec.ts b/packages/middleware-bucket-endpoint/src/bucketEndpointMiddleware.spec.ts index 918f22c00693..b044ec805071 100644 --- a/packages/middleware-bucket-endpoint/src/bucketEndpointMiddleware.spec.ts +++ b/packages/middleware-bucket-endpoint/src/bucketEndpointMiddleware.spec.ts @@ -1,4 +1,4 @@ -import { MiddlewareStack } from "@aws-sdk/middleware-stack"; +import { constructStack } from "@aws-sdk/middleware-stack"; import { HttpRequest } from "@aws-sdk/protocol-http"; import { bucketEndpointMiddleware, bucketEndpointMiddlewareOptions } from "./bucketEndpointMiddleware"; @@ -130,7 +130,7 @@ describe("bucketEndpointMiddleware", () => { }); it("should be inserted before 'hostheaderMiddleware' if exists", async () => { - const stack = new MiddlewareStack(); + const stack = constructStack(); const mockHostheaderMiddleware = (next: any) => (args: any) => { args.request.arr.push("two"); return next(args); diff --git a/packages/middleware-bucket-endpoint/src/bucketEndpointMiddleware.ts b/packages/middleware-bucket-endpoint/src/bucketEndpointMiddleware.ts index 7d7dab35feec..bc8a802cbcd4 100644 --- a/packages/middleware-bucket-endpoint/src/bucketEndpointMiddleware.ts +++ b/packages/middleware-bucket-endpoint/src/bucketEndpointMiddleware.ts @@ -2,12 +2,11 @@ import { HttpRequest } from "@aws-sdk/protocol-http"; import { BuildHandler, BuildHandlerArguments, - BuildHandlerOptions, BuildHandlerOutput, BuildMiddleware, MetadataBearer, Pluggable, - RelativeLocation, + RelativeMiddlewareOptions, } from "@aws-sdk/types"; import { bucketHostname } from "./bucketHostname"; @@ -49,8 +48,7 @@ export function bucketEndpointMiddleware(options: BucketEndpointResolvedConfig): }; } -export const bucketEndpointMiddlewareOptions: BuildHandlerOptions & RelativeLocation = { - step: "build", +export const bucketEndpointMiddlewareOptions: RelativeMiddlewareOptions = { tags: ["BUCKET_ENDPOINT"], name: "bucketEndpointMiddleware", relation: "before", diff --git a/packages/middleware-eventstream/src/handling-middleware.ts b/packages/middleware-eventstream/src/handling-middleware.ts index 194a4e3dcf57..ae9957950d7b 100644 --- a/packages/middleware-eventstream/src/handling-middleware.ts +++ b/packages/middleware-eventstream/src/handling-middleware.ts @@ -1,5 +1,5 @@ import { HttpRequest } from "@aws-sdk/protocol-http"; -import { FinalizeRequestHandlerOptions, FinalizeRequestMiddleware, RelativeLocation } from "@aws-sdk/types"; +import { FinalizeRequestMiddleware, RelativeMiddlewareOptions } from "@aws-sdk/types"; import { EventStreamResolvedConfig } from "./configuration"; @@ -11,8 +11,7 @@ export const eventStreamHandlingMiddleware = ( return options.eventStreamPayloadHandler.handle(next, args, context); }; -export const eventStreamHandlingMiddlewareOptions: FinalizeRequestHandlerOptions & RelativeLocation = { - step: "finalizeRequest", +export const eventStreamHandlingMiddlewareOptions: RelativeMiddlewareOptions = { tags: ["EVENT_STREAM", "SIGNATURE", "HANDLE"], name: "eventStreamHandlingMiddleware", relation: "after", diff --git a/packages/middleware-sdk-s3-control/src/prepend-account-id.ts b/packages/middleware-sdk-s3-control/src/prepend-account-id.ts index 1cf31e46d763..c4b44a173e9b 100644 --- a/packages/middleware-sdk-s3-control/src/prepend-account-id.ts +++ b/packages/middleware-sdk-s3-control/src/prepend-account-id.ts @@ -2,12 +2,11 @@ import { HttpRequest } from "@aws-sdk/protocol-http"; import { BuildHandler, BuildHandlerArguments, - BuildHandlerOptions, BuildHandlerOutput, BuildMiddleware, MetadataBearer, Pluggable, - RelativeLocation, + RelativeMiddlewareOptions, } from "@aws-sdk/types"; export function prependAccountIdMiddleware(): BuildMiddleware { @@ -40,8 +39,7 @@ export function prependAccountIdMiddleware(): BuildMiddleware { }; } -export const prependAccountIdMiddlewareOptions: BuildHandlerOptions & RelativeLocation = { - step: "build", +export const prependAccountIdMiddlewareOptions: RelativeMiddlewareOptions = { tags: ["PREPEND_ACCOUNT_ID_MIDDLEWARE"], name: "prependAccountIdMiddleware", relation: "before", diff --git a/packages/middleware-sdk-transcribe-streaming/src/middleware-endpoint.ts b/packages/middleware-sdk-transcribe-streaming/src/middleware-endpoint.ts index b99cc372a730..105dbb9a9bb1 100644 --- a/packages/middleware-sdk-transcribe-streaming/src/middleware-endpoint.ts +++ b/packages/middleware-sdk-transcribe-streaming/src/middleware-endpoint.ts @@ -2,9 +2,8 @@ import { HttpRequest } from "@aws-sdk/protocol-http"; import { BuildHandler, BuildHandlerArguments, - BuildHandlerOptions, BuildMiddleware, - RelativeLocation, + RelativeMiddlewareOptions, RequestHandler, } from "@aws-sdk/types"; @@ -54,8 +53,7 @@ export const websocketURLMiddleware = (options: { return next(args); }; -export const websocketURLMiddlewareOptions: BuildHandlerOptions & RelativeLocation = { - step: "build", +export const websocketURLMiddlewareOptions: RelativeMiddlewareOptions = { name: "websocketURLMiddleware", tags: ["WEBSOCKET", "EVENT_STREAM"], relation: "after", diff --git a/packages/middleware-signing/src/middleware.ts b/packages/middleware-signing/src/middleware.ts index f3db7ea3140b..d18d8b06f394 100644 --- a/packages/middleware-signing/src/middleware.ts +++ b/packages/middleware-signing/src/middleware.ts @@ -3,10 +3,9 @@ import { FinalizeHandler, FinalizeHandlerArguments, FinalizeHandlerOutput, - FinalizeRequestHandlerOptions, FinalizeRequestMiddleware, Pluggable, - RelativeLocation, + RelativeMiddlewareOptions, } from "@aws-sdk/types"; import { AwsAuthResolvedConfig } from "./configurations"; @@ -43,9 +42,8 @@ export function awsAuthMiddleware( }; } -export const awsAuthMiddlewareOptions: FinalizeRequestHandlerOptions & RelativeLocation = { +export const awsAuthMiddlewareOptions: RelativeMiddlewareOptions = { name: "awsAuthMiddleware", - step: "finalizeRequest", tags: ["SIGNATURE", "AWSAUTH"], relation: "after", toMiddleware: "retryMiddleware", diff --git a/packages/middleware-stack/src/MiddlewareStack.spec.ts b/packages/middleware-stack/src/MiddlewareStack.spec.ts index b6f9397e5611..f1c6f76d63a9 100644 --- a/packages/middleware-stack/src/MiddlewareStack.spec.ts +++ b/packages/middleware-stack/src/MiddlewareStack.spec.ts @@ -1,277 +1,325 @@ import { - BuildMiddleware, DeserializeHandlerArguments, DeserializeMiddleware, + FinalizeHandler, FinalizeHandlerArguments, - FinalizeRequestMiddleware, - Handler, - InitializeHandlerOutput, - InitializeMiddleware, - MiddlewareType, + InitializeHandler, Pluggable, } from "@aws-sdk/types"; -import { MiddlewareStack } from "./MiddlewareStack"; +import { constructStack } from "./MiddlewareStack"; type input = Array; type output = object; //return tagged union to make compiler happy -function getConcatMiddleware(message: string): MiddlewareType { - return (next: Handler): Handler => { - return (args: any): Promise> => - next({ - ...args, - input: args.input.concat(message), - }); - }; -} +const getConcatMiddleware = (message: string) => ( + next: FinalizeHandler +): InitializeHandler => (args: any) => + next({ + ...args, + input: args.input.concat(message), + request: undefined as any, + }); describe("MiddlewareStack", () => { - it("should resolve the stack into a composed handler", async () => { - const stack = new MiddlewareStack(); - const secondMW = getConcatMiddleware("second") as InitializeMiddleware; - stack.add(secondMW, { name: "second" }); - stack.addRelativeTo(getConcatMiddleware("first") as InitializeMiddleware, { - relation: "before", - toMiddleware: "second", - }); - stack.add(getConcatMiddleware("fourth") as BuildMiddleware, { - step: "build", - name: "fourth", - }); - stack.add(getConcatMiddleware("third") as BuildMiddleware, { - step: "build", - priority: "high", - }); - stack.add(getConcatMiddleware("fifth") as FinalizeRequestMiddleware, { - step: "finalizeRequest", - name: "fifth", - }); - stack.add(getConcatMiddleware("seventh") as FinalizeRequestMiddleware, { step: "finalizeRequest" }); - stack.addRelativeTo(getConcatMiddleware("sixth") as FinalizeRequestMiddleware, { - step: "finalizeRequest", - relation: "after", - toMiddleware: "fifth", - }); - stack.add(getConcatMiddleware("ninth") as DeserializeMiddleware, { - priority: "low", - step: "deserialize", - }); - stack.addRelativeTo(getConcatMiddleware("eighth") as DeserializeMiddleware, { - step: "deserialize", - relation: "before", - toMiddleware: "doesnt_exist", - }); - const inner = jest.fn(); + describe("add", () => { + it("should sort middleware based on step, priority, and ording of adding", async () => { + const stack = constructStack(); + const bMW = getConcatMiddleware("B"); + stack.add(bMW, { name: "B" }); + stack.add(getConcatMiddleware("A"), { + priority: "high", + }); + stack.add(getConcatMiddleware("D"), { + step: "build", + name: "D", + }); + stack.add(getConcatMiddleware("C"), { + step: "build", + priority: "high", + }); + stack.add(getConcatMiddleware("E"), { + step: "finalizeRequest", + }); + stack.add(getConcatMiddleware("F"), { step: "finalizeRequest" }); + stack.add(getConcatMiddleware("G") as DeserializeMiddleware, { + priority: "low", + step: "deserialize", + }); + const inner = jest.fn(); - const composed = stack.resolve(inner, {} as any); - await composed({ input: [] }); + const composed = stack.resolve(inner, {} as any); + await composed({ input: [] }); - expect(inner.mock.calls.length).toBe(1); - expect(inner).toBeCalledWith({ - input: ["first", "second", "third", "fourth", "fifth", "sixth", "seventh", "eighth", "ninth"], + expect(inner.mock.calls.length).toBe(1); + expect(inner).toBeCalledWith({ + input: ["A", "B", "C", "D", "E", "F", "G"], + }); }); }); - it("should allow adding middleware relatively", async () => { - const stack = new MiddlewareStack(); - type MW = InitializeMiddleware; - stack.addRelativeTo(getConcatMiddleware("H") as MW, { - name: "H", - relation: "after", - toMiddleware: "G", - }); - stack.addRelativeTo(getConcatMiddleware("A") as MW, { - name: "A", - relation: "after", - toMiddleware: "nonExist", - }); - stack.addRelativeTo(getConcatMiddleware("C") as MW, { - name: "C", - relation: "after", - toMiddleware: "A", - }); - stack.addRelativeTo(getConcatMiddleware("B") as MW, { - name: "B", - relation: "after", - toMiddleware: "A", - }); - stack.addRelativeTo(getConcatMiddleware("D") as MW, { - name: "D", - relation: "after", - toMiddleware: "C", - }); - stack.add(getConcatMiddleware("G") as MW, { - name: "G", - priority: "low", - }); - stack.addRelativeTo(getConcatMiddleware("E") as MW, { - name: "E", - relation: "before", - toMiddleware: "F", - }); - stack.addRelativeTo(getConcatMiddleware("F") as MW, { - name: "F", - relation: "before", - toMiddleware: "G", - }); - stack.addRelativeTo(getConcatMiddleware("I") as MW, { - name: "I", - relation: "before", - toMiddleware: "J", + describe("addRelativeTo", () => { + it("should allow adding middleware relatively based relation and order of adding", async () => { + const stack = constructStack(); + stack.addRelativeTo(getConcatMiddleware("H"), { + name: "H", + relation: "after", + toMiddleware: "G", + }); + stack.add(getConcatMiddleware("A"), { + name: "A", + }); + stack.addRelativeTo(getConcatMiddleware("C"), { + name: "C", + relation: "after", + toMiddleware: "A", + }); + stack.addRelativeTo(getConcatMiddleware("B"), { + name: "B", + relation: "after", + toMiddleware: "A", + }); + stack.addRelativeTo(getConcatMiddleware("D"), { + name: "D", + relation: "after", + toMiddleware: "C", + }); + stack.add(getConcatMiddleware("G"), { + name: "G", + priority: "low", + }); + stack.addRelativeTo(getConcatMiddleware("E"), { + name: "E", + relation: "before", + toMiddleware: "F", + }); + stack.addRelativeTo(getConcatMiddleware("F"), { + name: "F", + relation: "before", + toMiddleware: "G", + }); + const inner = jest.fn(); + const composed = stack.resolve(inner, {} as any); + await composed({ input: [] }); + expect(inner.mock.calls.length).toBe(1); + expect(inner).toBeCalledWith({ input: ["A", "B", "C", "D", "E", "F", "G", "H"] }); }); - stack.addRelativeTo(getConcatMiddleware("J") as MW, { - name: "J", - relation: "after", - toMiddleware: "I", + + it("should add relative middleware within the scope of adjacent absolute middleware", async () => { + const stack = constructStack(); + stack.addRelativeTo(getConcatMiddleware("B"), { + name: "B", + relation: "after", + toMiddleware: "A", + }); + stack.add(getConcatMiddleware("A"), { name: "A" }); + stack.addRelativeTo(getConcatMiddleware("C"), { + name: "C", + relation: "before", + toMiddleware: "D", + }); + stack.add(getConcatMiddleware("D"), { name: "D" }); + const inner = jest.fn(); + const composed = stack.resolve(inner, {} as any); + await composed({ input: [] }); + expect(inner.mock.calls.length).toBe(1); + expect(inner).toBeCalledWith({ input: ["A", "B", "C", "D"] }); }); - const inner = jest.fn(); - const composed = stack.resolve(inner, {} as any); - await composed({ input: [] }); + it("should not add self-referenced relative middleware", async () => { + const stack = constructStack(); + stack.addRelativeTo(getConcatMiddleware("A"), { + name: "A", + relation: "before", + toMiddleware: "B", + }); + stack.addRelativeTo(getConcatMiddleware("B"), { + name: "B", + relation: "before", + toMiddleware: "C", + }); + stack.addRelativeTo(getConcatMiddleware("C"), { + name: "C", + relation: "after", + toMiddleware: "A", + }); + const inner = jest.fn(); + const composed = stack.resolve(inner, {} as any); + await composed({ input: [] }); + expect(inner.mock.calls.length).toBe(1); + expect(inner).toBeCalledWith({ input: [] }); + }); - expect(inner.mock.calls.length).toBe(1); - const concatenated = inner.mock.calls[0][0].input; - expect(concatenated.indexOf("H")).toBeGreaterThan(concatenated.indexOf("G")); - expect(concatenated.indexOf("C")).toBeGreaterThan(concatenated.indexOf("A")); - expect(concatenated.indexOf("B")).toBeGreaterThan(concatenated.indexOf("A")); - expect(concatenated.indexOf("D")).toBeGreaterThan(concatenated.indexOf("C")); - expect(concatenated.indexOf("E")).toBeLessThan(concatenated.indexOf("F")); - expect(concatenated.indexOf("F")).toBeLessThan(concatenated.indexOf("G")); - expect(concatenated.indexOf("I")).toBeLessThan(concatenated.indexOf("J")); - expect(concatenated.indexOf("J")).toBeGreaterThan(concatenated.indexOf("I")); - expect(concatenated.indexOf("H")).toBeGreaterThan(concatenated.indexOf("G")); + it("should throw if add middleware relative to non-exist middleware", async () => { + expect.assertions(1); + const stack = constructStack(); + stack.addRelativeTo(getConcatMiddleware("foo"), { + name: "foo", + relation: "before", + toMiddleware: "non_exist", + }); + const inner = jest.fn(); + try { + stack.resolve(inner, {} as any); + } catch (e) { + expect(e.message).toBe("non_exist is not found when adding foo middleware before non_exist"); + } + }); }); - it("should allow cloning", async () => { - const stack = new MiddlewareStack(); - const secondMiddleware = getConcatMiddleware("second") as InitializeMiddleware; - stack.add(secondMiddleware); - stack.add(getConcatMiddleware("first") as InitializeMiddleware, { - name: "first", - priority: "high", + describe("clone", () => { + it("should allow cloning", async () => { + const stack = constructStack(); + const bMiddleware = getConcatMiddleware("B"); + stack.add(bMiddleware); + stack.add(getConcatMiddleware("A"), { + name: "A", + priority: "high", + }); + const secondStack = stack.clone(); + const inner = jest.fn(); + await secondStack.resolve(inner, {} as any)({ input: [] }); + expect(inner.mock.calls.length).toBe(1); + expect(inner).toBeCalledWith({ input: ["A", "B"] }); + // validate adding middleware to cloned stack won't affect the original stack. + inner.mockClear(); + secondStack.add(getConcatMiddleware("C")); + await secondStack.resolve(inner, {} as any)({ input: [] }); + expect(inner).toBeCalledWith({ input: ["A", "B", "C"] }); + inner.mockClear(); + await stack.resolve(inner, {} as any)({ input: [] }); + expect(inner).toBeCalledWith({ input: ["A", "B"] }); }); + }); - const secondStack = stack.clone(); + describe("concat", () => { + it("should allow combining stacks", async () => { + const stack = constructStack(); + stack.add(getConcatMiddleware("A")); + stack.add(getConcatMiddleware("B"), { + name: "B", + priority: "low", + }); - const inner = jest.fn(({ input }: DeserializeHandlerArguments) => { - expect(input).toEqual(["first", "second"]); - return Promise.resolve({ response: {} }); - }); - await secondStack.resolve(inner, {} as any)({ input: [] }); - expect(inner.mock.calls.length).toBe(1); - expect(() => secondStack.add(secondMiddleware, { name: "first" })).toThrowError( - "Duplicated middleware name 'first'" - ); - expect(() => - secondStack.addRelativeTo(getConcatMiddleware("first") as InitializeMiddleware, { - name: "first", - relation: "before", - toMiddleware: "first", - }) - ).toThrowError("Duplicated middleware name 'first'"); - }); + const secondStack = constructStack(); + secondStack.add(getConcatMiddleware("D"), { + step: "build", + priority: "low", + }); + secondStack.addRelativeTo(getConcatMiddleware("C"), { + relation: "after", + toMiddleware: "B", + }); - it("should allow combining stacks", async () => { - const stack = new MiddlewareStack(); - stack.add(getConcatMiddleware("first") as InitializeMiddleware); - stack.add(getConcatMiddleware("second") as InitializeMiddleware, { - name: "second", - priority: "low", + const inner = jest.fn(); + await stack.concat(secondStack).resolve(inner, {} as any)({ input: [] }); + expect(inner.mock.calls.length).toBe(1); + expect(inner).toBeCalledWith({ input: ["A", "B", "C", "D"] }); }); - const secondStack = new MiddlewareStack(); - secondStack.add(getConcatMiddleware("fourth") as FinalizeRequestMiddleware, { - step: "build", - priority: "low", - }); - secondStack.addRelativeTo(getConcatMiddleware("third") as FinalizeRequestMiddleware, { - step: "build", - relation: "after", - toMiddleware: "second", + it("should not touch the stack of the concat() caller or the parameter", async () => { + const stack = constructStack(); + stack.add(getConcatMiddleware("A")); + const secondStack = constructStack(); + secondStack.add(getConcatMiddleware("B")); + const inner = jest.fn(); + await stack.concat(secondStack).resolve(inner, {} as any)({ input: [] }); + expect(inner.mock.calls.length).toBe(1); + expect(inner).toBeCalledWith({ input: ["A", "B"] }); + inner.mockClear(); + await secondStack.resolve(inner, {} as any)({ input: [] }); + expect(inner).toBeCalledWith({ input: ["B"] }); + inner.mockClear(); + await stack.resolve(inner, {} as any)({ input: [] }); + expect(inner).toBeCalledWith({ input: ["A"] }); }); + }); - const inner = jest.fn(({ input }: DeserializeHandlerArguments) => { - expect(input).toEqual(["first", "second", "third", "fourth"]); - return Promise.resolve({ response: {} }); - }); - await stack.concat(secondStack).resolve(inner, {} as any)({ input: [] }); + describe("remove", () => { + it("should remove middleware by name", async () => { + const stack = constructStack(); + stack.add(getConcatMiddleware("don't remove me"), { name: "notRemove" }); + stack.addRelativeTo(getConcatMiddleware("remove me!"), { + relation: "after", + toMiddleware: "notRemove", + name: "toRemove", + }); - expect(inner.mock.calls.length).toBe(1); + await stack.resolve(({ input }: FinalizeHandlerArguments>) => { + expect(input.sort()).toEqual(["don't remove me", "remove me!"]); + return Promise.resolve({ response: {} }); + }, {} as any)({ input: [] }); - secondStack.add(getConcatMiddleware("second") as FinalizeRequestMiddleware, { - step: "build", - priority: "high", - name: "second", - }); - expect(() => stack.concat(secondStack)).toThrow("Duplicated middleware name 'second'"); - }); + stack.remove("toRemove"); - it("should allow the removal of middleware by constructor identity", async () => { - const stack = new MiddlewareStack(); - stack.add(getConcatMiddleware("don't remove me") as InitializeMiddleware, { name: "notRemove" }); - stack.addRelativeTo(getConcatMiddleware("remove me!") as InitializeMiddleware, { - relation: "after", - toMiddleware: "notRemove", - name: "toRemove", + const inner = jest.fn(); + await stack.resolve(inner, {} as any)({ input: [] }); + expect(inner.mock.calls.length).toBe(1); + expect(inner).toBeCalledWith({ input: ["don't remove me"] }); }); - await stack.resolve(({ input }: FinalizeHandlerArguments>) => { - expect(input.sort()).toEqual(["don't remove me", "remove me!"]); - return Promise.resolve({ response: {} }); - }, {} as any)({ input: [] }); + it("should remove middleware by reference", async () => { + const stack = constructStack(); + const mw = getConcatMiddleware("remove all references of me"); + stack.add(mw, { name: "toRemove1" }); + stack.add(getConcatMiddleware("don't remove me!")); + stack.add(mw, { name: "toRemove2" }); + stack.remove(mw); - stack.remove("toRemove"); - - await stack.resolve(({ input }: FinalizeHandlerArguments>) => { - expect(input).toEqual(["don't remove me"]); - return Promise.resolve({ response: {} }); - }, {} as any)({ input: [] }); + const inner = jest.fn(); + await stack.resolve(inner, {} as any)({ input: [] }); + expect(inner.mock.calls.length).toBe(1); + expect(inner).toBeCalledWith({ input: ["don't remove me!"] }); + }); }); - it("should allow the removal of middleware by tag", async () => { - const stack = new MiddlewareStack(); - stack.add(getConcatMiddleware("not removed") as InitializeMiddleware, { - name: "not removed", - tags: ["foo", "bar"], - }); - stack.addRelativeTo(getConcatMiddleware("remove me!") as InitializeMiddleware, { - relation: "after", - toMiddleware: "not removed", - tags: ["foo", "bar", "baz"], - }); + describe("removeByTag", () => { + it("should allow the removal of middleware by tag", async () => { + const stack = constructStack(); + stack.add(getConcatMiddleware("not removed"), { + name: "not removed", + tags: ["foo", "bar"], + }); + stack.addRelativeTo(getConcatMiddleware("remove me!"), { + relation: "after", + toMiddleware: "not removed", + tags: ["foo", "bar", "baz"], + }); - await stack.resolve(({ input }: FinalizeHandlerArguments>) => { - expect(input.sort()).toEqual(["not removed", "remove me!"]); - return Promise.resolve({ response: {} }); - }, {} as any)({ input: [] }); + await stack.resolve(({ input }: FinalizeHandlerArguments>) => { + expect(input.sort()).toEqual(["not removed", "remove me!"]); + return Promise.resolve({ response: {} }); + }, {} as any)({ input: [] }); - stack.removeByTag("baz"); + stack.removeByTag("baz"); - await stack.resolve(({ input }: DeserializeHandlerArguments>) => { - expect(input).toEqual(["not removed"]); - return Promise.resolve({ response: {} }); - }, {} as any)({ input: [] }); + await stack.resolve(({ input }: DeserializeHandlerArguments>) => { + expect(input).toEqual(["not removed"]); + return Promise.resolve({ response: {} }); + }, {} as any)({ input: [] }); + }); }); - it("should apply customizations from pluggables", async () => { - const stack = new MiddlewareStack(); - const plugin: Pluggable = { - applyToStack: (stack) => { - stack.addRelativeTo(getConcatMiddleware("second") as InitializeMiddleware, { - relation: "after", - toMiddleware: "first", - }); - stack.add(getConcatMiddleware("first") as InitializeMiddleware, { name: "first" }); - }, - }; - stack.use(plugin); - const inner = jest.fn(({ input }: DeserializeHandlerArguments) => { - expect(input).toEqual(["first", "second"]); - return Promise.resolve({ response: {} }); + describe("use", () => { + it("should apply customizations from pluggables", async () => { + const stack = constructStack(); + const plugin: Pluggable = { + applyToStack: (stack) => { + stack.addRelativeTo(getConcatMiddleware("second"), { + relation: "after", + toMiddleware: "first", + }); + stack.add(getConcatMiddleware("first"), { name: "first" }); + }, + }; + stack.use(plugin); + const inner = jest.fn(({ input }: DeserializeHandlerArguments) => { + expect(input).toEqual(["first", "second"]); + return Promise.resolve({ response: {} }); + }); + await stack.resolve(inner, {} as any)({ input: [] }); + expect(inner.mock.calls.length).toBe(1); }); - await stack.resolve(inner, {} as any)({ input: [] }); - expect(inner.mock.calls.length).toBe(1); }); }); diff --git a/packages/middleware-stack/src/MiddlewareStack.ts b/packages/middleware-stack/src/MiddlewareStack.ts index d27784f44ac5..e473dc5dc4e5 100644 --- a/packages/middleware-stack/src/MiddlewareStack.ts +++ b/packages/middleware-stack/src/MiddlewareStack.ts @@ -1,382 +1,238 @@ import { AbsoluteLocation, - BuildHandlerOptions, - BuildMiddleware, DeserializeHandler, - DeserializeHandlerOptions, - DeserializeMiddleware, - FinalizeRequestHandlerOptions, - FinalizeRequestMiddleware, Handler, HandlerExecutionContext, HandlerOptions, - InitializeHandlerOptions, - InitializeMiddleware, - MiddlewareStack as IMiddlewareStack, + MiddlewareStack, MiddlewareType, Pluggable, Priority, RelativeLocation, - SerializeHandlerOptions, - SerializeMiddleware, Step, } from "@aws-sdk/types"; -import { - MiddlewareEntry, - NamedMiddlewareEntriesMap, - NamedRelativeEntriesMap, - NormalizedRelativeEntry, - NormalizingEntryResult, - RelativeMiddlewareAnchor, - RelativeMiddlewareEntry, -} from "./types"; - -export interface MiddlewareStack extends IMiddlewareStack {} - -export class MiddlewareStack { - private readonly absoluteEntries: Array> = []; - private readonly relativeEntries: Array> = []; - private entriesNameMap: { - [middlewareName: string]: MiddlewareEntry | RelativeMiddlewareEntry; - } = {}; - - add(middleware: InitializeMiddleware, options?: InitializeHandlerOptions & AbsoluteLocation): void; - - add(middleware: SerializeMiddleware, options: SerializeHandlerOptions & AbsoluteLocation): void; - - add(middleware: BuildMiddleware, options: BuildHandlerOptions & AbsoluteLocation): void; - - add( - middleware: FinalizeRequestMiddleware, - options: FinalizeRequestHandlerOptions & AbsoluteLocation - ): void; - - add(middleware: DeserializeMiddleware, options: DeserializeHandlerOptions & AbsoluteLocation): void; - - add(middleware: MiddlewareType, options: HandlerOptions & AbsoluteLocation = {}): void { - const { name, step = "initialize", tags, priority = "normal" } = options; - const entry: MiddlewareEntry = { - name, - step, - tags, - priority, - middleware, - }; - if (name) { - if (Object.prototype.hasOwnProperty.call(this.entriesNameMap, name)) { - throw new Error(`Duplicated middleware name '${name}'`); - } - this.entriesNameMap[name] = entry; - } - this.absoluteEntries.push(entry); - } - - addRelativeTo( - middleware: InitializeMiddleware, - options: InitializeHandlerOptions & RelativeLocation - ): void; - - addRelativeTo( - middleware: SerializeMiddleware, - options: SerializeHandlerOptions & RelativeLocation - ): void; - - addRelativeTo( - middleware: BuildMiddleware, - options: BuildHandlerOptions & RelativeLocation - ): void; - - addRelativeTo( - middleware: FinalizeRequestMiddleware, - options: FinalizeRequestHandlerOptions & RelativeLocation - ): void; +import { AbsoluteMiddlewareEntry, MiddlewareEntry, Normalized, RelativeMiddlewareEntry } from "./types"; - addRelativeTo( - middleware: DeserializeMiddleware, - options: DeserializeHandlerOptions & RelativeLocation - ): void; +export const constructStack = (): MiddlewareStack => { + let absoluteEntries: AbsoluteMiddlewareEntry[] = []; + let relativeEntries: RelativeMiddlewareEntry[] = []; + const entriesNameSet: Set = new Set(); - addRelativeTo( - middleware: MiddlewareType, - options: HandlerOptions & RelativeLocation - ): void { - const { step = "initialize", name, tags, relation, toMiddleware } = options; - const entry: RelativeMiddlewareEntry = { - middleware, - step, - name, - tags, - next: relation === "before" ? toMiddleware : undefined, - prev: relation === "after" ? toMiddleware : undefined, - }; - if (name) { - if (Object.prototype.hasOwnProperty.call(this.entriesNameMap, name)) { - throw new Error(`Duplicated middleware name '${name}'`); - } - this.entriesNameMap[name] = entry; - } - this.relativeEntries.push(entry); - } - - private sort( - entries: Array | NormalizedRelativeEntry> - ): Array | NormalizedRelativeEntry> { - //reverse before sorting so that middleware of same step will execute in - //the order of being added - return entries.sort( + const sort = >(entries: T[]): T[] => + entries.sort( (a, b) => stepWeights[b.step] - stepWeights[a.step] || priorityWeights[b.priority || "normal"] - priorityWeights[a.priority || "normal"] ); - } - - clone(): IMiddlewareStack { - const clone = new MiddlewareStack(); - clone.absoluteEntries.push(...this.absoluteEntries); - clone.relativeEntries.push(...this.relativeEntries); - clone.entriesNameMap = { ...this.entriesNameMap }; - return clone; - } - - concat( - from: IMiddlewareStack - ): MiddlewareStack { - const clone = new MiddlewareStack(); - clone.entriesNameMap = { ...(this.entriesNameMap as any) }; - // IMiddlewareStack interface doesn't contain private members variables - // like `entriesNameMap`, but in fact the function expects `MiddlewareStack` - // class instance. So here we cast it. - const _from = from as MiddlewareStack; - for (const name in _from.entriesNameMap) { - if (clone.entriesNameMap[name]) { - throw new Error(`Duplicated middleware name '${name}'`); - } - clone.entriesNameMap[name] = _from.entriesNameMap[name]; - } - clone.absoluteEntries.push(...(this.absoluteEntries as any), ..._from.absoluteEntries); - clone.relativeEntries.push(...(this.relativeEntries as any), ..._from.relativeEntries); - return clone; - } - - remove(toRemove: MiddlewareType | string): boolean { - if (typeof toRemove === "string") return this.removeByName(toRemove); - else return this.removeByReference(toRemove); - } - - private removeByName(toRemove: string): boolean { - for (let i = this.absoluteEntries.length - 1; i >= 0; i--) { - if (this.absoluteEntries[i].name && this.absoluteEntries[i].name === toRemove) { - this.absoluteEntries.splice(i, 1); - delete this.entriesNameMap[toRemove]; - return true; - } - } - for (let i = this.relativeEntries.length - 1; i >= 0; i--) { - if (this.relativeEntries[i].name && this.relativeEntries[i].name === toRemove) { - this.relativeEntries.splice(i, 1); - delete this.entriesNameMap[toRemove]; - return true; - } - } - return false; - } - - private removeByReference(toRemove: MiddlewareType): boolean { - for (let i = this.absoluteEntries.length - 1; i >= 0; i--) { - if (this.absoluteEntries[i].middleware === toRemove) { - const { name } = this.absoluteEntries[i]; - if (name) delete this.entriesNameMap[name]; - this.absoluteEntries.splice(i, 1); - return true; - } - } - for (let i = this.relativeEntries.length - 1; i >= 0; i--) { - if (this.relativeEntries[i].middleware === toRemove) { - const { name } = this.relativeEntries[i]; - if (name) delete this.entriesNameMap[name]; - this.relativeEntries.splice(i, 1); - return true; - } - } - return false; - } - removeByTag(toRemove: string): boolean { - let removed = false; - for (let i = this.absoluteEntries.length - 1; i >= 0; i--) { - const { tags, name } = this.absoluteEntries[i]; - if (tags && tags.indexOf(toRemove) > -1) { - this.absoluteEntries.splice(i, 1); - if (name) delete this.entriesNameMap[name]; - removed = true; - } - } - for (let i = this.relativeEntries.length - 1; i >= 0; i--) { - const { tags, name } = this.relativeEntries[i]; - if (tags && tags.indexOf(toRemove) > -1) { - this.relativeEntries.splice(i, 1); - if (name) delete this.entriesNameMap[name]; - removed = true; + const removeByName = (toRemove: string): boolean => { + let isRemoved = false; + const filterCb = (entry: MiddlewareEntry): boolean => { + if (entry.name && entry.name === toRemove) { + isRemoved = true; + entriesNameSet.delete(toRemove); + return false; } - } - return removed; - } - - use(plugin: Pluggable) { - plugin.applyToStack(this); - } + return true; + }; + absoluteEntries = absoluteEntries.filter(filterCb); + relativeEntries = relativeEntries.filter(filterCb); + return isRemoved; + }; + + const removeByReference = (toRemove: MiddlewareType): boolean => { + let isRemoved = false; + const filterCb = (entry: MiddlewareEntry): boolean => { + if (entry.middleware === toRemove) { + isRemoved = true; + if (entry.name) entriesNameSet.delete(entry.name); + return false; + } + return true; + }; + absoluteEntries = absoluteEntries.filter(filterCb); + relativeEntries = relativeEntries.filter(filterCb); + return isRemoved; + }; + + const cloneTo = ( + toStack: MiddlewareStack + ): MiddlewareStack => { + absoluteEntries.forEach((entry) => { + //@ts-ignore + toStack.add(entry.middleware, { ...entry }); + }); + relativeEntries.forEach((entry) => { + //@ts-ignore + toStack.addRelativeTo(entry.middleware, { ...entry }); + }); + return toStack; + }; + + const expandRelativeMiddlewareList = ( + from: Normalized, Input, Output> + ): MiddlewareEntry[] => { + const expandedMiddlewareList: MiddlewareEntry[] = []; + from.before.forEach((entry) => { + if (entry.before.length === 0 && entry.after.length === 0) { + expandedMiddlewareList.push(entry); + } else { + expandedMiddlewareList.push(...expandRelativeMiddlewareList(entry)); + } + }); + expandedMiddlewareList.push(from); + from.after.reverse().forEach((entry) => { + if (entry.before.length === 0 && entry.after.length === 0) { + expandedMiddlewareList.push(entry); + } else { + expandedMiddlewareList.push(...expandRelativeMiddlewareList(entry)); + } + }); + return expandedMiddlewareList; + }; /** - * Resolve relative middleware entries to multiple double linked lists - * depicting the relative location of middleware. Only middleware that have - * direct or transitive relation will form a linked list. - * - * This function normalizes relative middleware into 2 categories of linked - * lists. (1) linked list that have absolute-located middleware on one end. - * These middleware will be resolved accordingly before or after the absolute- - * located middleware. (2) Linked list that have no absolute-located middleware - * on any end. They will be resolved to corresponding step with normal priority - * - * The 2 types of linked list will return as a tuple + * Get a final list of middleware in the order of being executed in the resolved handler. */ - private normalizeRelativeEntries(): NormalizingEntryResult { - const absoluteMiddlewareNamesMap = this.absoluteEntries - .filter((entry) => entry.name) - .reduce((accumulator, entry) => { - accumulator[entry.name!] = entry; - return accumulator; - }, {} as NamedMiddlewareEntriesMap); - const normalized = this.relativeEntries.map( - (entry) => - ({ - ...entry, - priority: null, - next: undefined, - prev: undefined, - } as NormalizedRelativeEntry) - ); - const relativeMiddlewareNamesMap = normalized - .filter((entry) => entry.name) - .reduce((accumulator, entry) => { - accumulator[entry.name!] = entry; - return accumulator; - }, {} as NamedRelativeEntriesMap); - - const anchors: RelativeMiddlewareAnchor = {}; - for (let i = 0; i < this.relativeEntries.length; i++) { - const { prev, next } = this.relativeEntries[i]; - const resolvedCurr = normalized[i]; - //either prev or next is set - if (prev) { - if (absoluteMiddlewareNamesMap[prev] && absoluteMiddlewareNamesMap[prev].step === resolvedCurr.step) { - if (!anchors[prev]) anchors[prev] = {}; - resolvedCurr.next = anchors[prev].next; - if (anchors[prev].next) anchors[prev].next!.prev = resolvedCurr; - anchors[prev].next = resolvedCurr; - } else if (relativeMiddlewareNamesMap[prev] && relativeMiddlewareNamesMap[prev].step === resolvedCurr.step) { - const resolvedPrev = relativeMiddlewareNamesMap[prev]; - if (resolvedPrev.next === resolvedCurr) continue; - resolvedCurr.next = resolvedPrev.next; - resolvedPrev.next = resolvedCurr; - if (resolvedCurr.next) resolvedCurr.next.prev = resolvedCurr; - resolvedCurr.prev = resolvedPrev; + const getMiddlewareList = (): Array> => { + const normalizedAbsoluteEntries: Normalized, Input, Output>[] = []; + const normalizedRelativeEntries: Normalized, Input, Output>[] = []; + const normalizedEntriesNameMap: { + [middlewareName: string]: Normalized, Input, Output>; + } = {}; + + absoluteEntries.forEach((entry) => { + const normalizedEntry = { + ...entry, + before: [], + after: [], + }; + if (normalizedEntry.name) normalizedEntriesNameMap[normalizedEntry.name] = normalizedEntry; + normalizedAbsoluteEntries.push(normalizedEntry); + }); + + relativeEntries.forEach((entry) => { + const normalizedEntry = { + ...entry, + before: [], + after: [], + }; + if (normalizedEntry.name) normalizedEntriesNameMap[normalizedEntry.name] = normalizedEntry; + normalizedRelativeEntries.push(normalizedEntry); + }); + + normalizedRelativeEntries.forEach((entry) => { + if (entry.toMiddleware) { + const toMiddleware = normalizedEntriesNameMap[entry.toMiddleware]; + if (toMiddleware === undefined) { + throw new Error( + `${entry.toMiddleware} is not found when adding ${entry.name || "anonymous"} middleware ${entry.relation} ${ + entry.toMiddleware + }` + ); } - } else if (next) { - if (absoluteMiddlewareNamesMap[next] && absoluteMiddlewareNamesMap[next].step === resolvedCurr.step) { - if (!anchors[next]) anchors[next] = {}; - resolvedCurr.prev = anchors[next].prev; - if (anchors[next].prev) anchors[next].prev!.next = resolvedCurr; - anchors[next].prev = resolvedCurr; - } else if (relativeMiddlewareNamesMap[next] && relativeMiddlewareNamesMap[next].step === resolvedCurr.step) { - const resolvedNext = relativeMiddlewareNamesMap[next]; - if (resolvedNext.prev === resolvedCurr) continue; - resolvedCurr.prev = resolvedNext.prev; - resolvedNext.prev = resolvedCurr; - if (resolvedCurr.prev) resolvedCurr.prev.next = resolvedCurr; - resolvedCurr.next = resolvedNext; + if (entry.relation === "after") { + toMiddleware.after.push(entry); + } + if (entry.relation === "before") { + toMiddleware.before.push(entry); } } - } - // get the head of the relative middleware linked list that have - // no transitive relation to absolute middleware. - const orphanedRelativeEntries: Array> = []; - const visited: WeakSet> = new WeakSet(); - for (const anchorName of Object.keys(anchors)) { - let { prev, next } = anchors[anchorName]; - while (prev) { - visited.add(prev); - prev = prev.prev; - } - while (next) { - visited.add(next); - next = next.next; - } - } - for (let i = 0; i < normalized.length; i++) { - let entry: NormalizedRelativeEntry | undefined = normalized[i]; - if (visited.has(entry)) continue; - if (!entry.prev) orphanedRelativeEntries.push(entry); - while (entry && !visited.has(entry)) { - visited.add(entry); - entry = entry.next; - } - } - return [orphanedRelativeEntries, anchors]; - } - - /** - * Get a final list of middleware in the order of being executed in the resolved handler. - * If relative entries list is not empty, those entries will be added to final middleware - * list with rules below: - * 1. if `toMiddleware` exists in the specific `step`, the middleware will be inserted before - * or after the specified `toMiddleware` - * 2. if `toMiddleware` doesn't exist in the specific `step`, the middleware will be appended - * to specific `step` with priority of `normal` - */ - private getMiddlewareList(): Array> { - const middlewareList: Array> = []; - const [orphanedRelativeEntries, anchors] = this.normalizeRelativeEntries(); - let entryList = [...this.absoluteEntries, ...orphanedRelativeEntries]; - entryList = this.sort(entryList); - for (const entry of entryList) { - const defaultAnchorValue = { prev: undefined, next: undefined }; - const { prev, next } = entry.name ? anchors[entry.name] || defaultAnchorValue : defaultAnchorValue; - let relativeEntry = prev; - //reverse relative entry linked list and add to ordered handler list - while (relativeEntry?.prev) { - relativeEntry = relativeEntry.prev; - } - while (relativeEntry) { - middlewareList.push(relativeEntry.middleware); - relativeEntry = relativeEntry.next; - } - middlewareList.push(entry.middleware); - let orphanedEntry = entry as any; - while ((orphanedEntry as any).next) { - middlewareList.push((orphanedEntry as any).next.middleware); - orphanedEntry = (orphanedEntry as any).next; - } - relativeEntry = next; - while (relativeEntry) { - middlewareList.push(relativeEntry.middleware); - relativeEntry = relativeEntry.next; - } - } - return middlewareList.reverse(); - } - - resolve( - handler: DeserializeHandler, - context: HandlerExecutionContext - ): Handler { - for (const middleware of this.getMiddlewareList()) { - handler = middleware(handler as Handler, context) as any; - } - - return handler as Handler; - } -} + }); + + const mainChain = sort(normalizedAbsoluteEntries) + .map(expandRelativeMiddlewareList) + .reduce((wholeList, expendedMiddlewareList) => { + // TODO: Replace it with Array.flat(); + wholeList.push(...expendedMiddlewareList); + return wholeList; + }, [] as MiddlewareEntry[]); + return mainChain.map((entry) => entry.middleware); + }; + + const stack = { + add: (middleware: MiddlewareType, options: HandlerOptions & AbsoluteLocation = {}) => { + const { name } = options; + const entry: AbsoluteMiddlewareEntry = { + step: "initialize", + priority: "normal", + middleware, + ...options, + }; + if (name) { + if (entriesNameSet.has(name)) { + throw new Error(`Duplicate middleware name '${name}'`); + } + entriesNameSet.add(name); + } + absoluteEntries.push(entry); + }, + + addRelativeTo: (middleware: MiddlewareType, options: HandlerOptions & RelativeLocation) => { + const { name } = options; + const entry: RelativeMiddlewareEntry = { + middleware, + ...options, + }; + if (name) { + if (entriesNameSet.has(name)) { + throw new Error(`Duplicated middleware name '${name}'`); + } + entriesNameSet.add(name); + } + relativeEntries.push(entry); + }, + + clone: () => cloneTo(constructStack()), + + use: (plugin: Pluggable) => { + plugin.applyToStack(stack); + }, + + remove: (toRemove: MiddlewareType | string): boolean => { + if (typeof toRemove === "string") return removeByName(toRemove); + else return removeByReference(toRemove); + }, + + removeByTag: (toRemove: string): boolean => { + let isRemoved = false; + const filterCb = (entry: MiddlewareEntry): boolean => { + const { tags, name } = entry; + if (tags && tags.includes(toRemove)) { + if (name) entriesNameSet.delete(name); + isRemoved = true; + return false; + } + return true; + }; + absoluteEntries = absoluteEntries.filter(filterCb); + relativeEntries = relativeEntries.filter(filterCb); + return isRemoved; + }, + + concat: ( + from: MiddlewareStack + ): MiddlewareStack => { + const cloned = cloneTo(constructStack()); + cloned.use(from); + return cloned; + }, + + applyToStack: cloneTo, + + resolve: ( + handler: DeserializeHandler, + context: HandlerExecutionContext + ): Handler => { + for (const middleware of getMiddlewareList().reverse()) { + handler = middleware(handler as Handler, context) as any; + } + return handler as Handler; + }, + }; + return stack; +}; const stepWeights: { [key in Step]: number } = { initialize: 5, diff --git a/packages/middleware-stack/src/types.ts b/packages/middleware-stack/src/types.ts index c8990a648a1a..5713cddc5bc6 100644 --- a/packages/middleware-stack/src/types.ts +++ b/packages/middleware-stack/src/types.ts @@ -1,17 +1,29 @@ -import { HandlerOptions, MiddlewareType, Priority, Step } from "@aws-sdk/types"; +import { AbsoluteLocation, HandlerOptions, MiddlewareType, Priority, RelativeLocation, Step } from "@aws-sdk/types"; + export interface MiddlewareEntry extends HandlerOptions { - step: Step; middleware: MiddlewareType; - priority: Priority; } -export interface RelativeMiddlewareEntry extends HandlerOptions { +export interface AbsoluteMiddlewareEntry + extends MiddlewareEntry, + AbsoluteLocation { step: Step; - middleware: MiddlewareType; - next?: string; - prev?: string; + priority: Priority; } +export interface RelativeMiddlewareEntry + extends MiddlewareEntry, + RelativeLocation {} + +export type Normalized< + T extends MiddlewareEntry, + Input extends object = {}, + Output extends object = {} +> = T & { + after: Normalized, Input, Output>[]; + before: Normalized, Input, Output>[]; +}; + export interface NormalizedRelativeEntry extends HandlerOptions { step: Step; middleware: MiddlewareType; @@ -23,19 +35,3 @@ export interface NormalizedRelativeEntry = { [key: string]: MiddlewareEntry; }; - -export type NamedRelativeEntriesMap = { - [key: string]: NormalizedRelativeEntry; -}; - -export type RelativeMiddlewareAnchor = { - [name: string]: { - prev?: NormalizedRelativeEntry; - next?: NormalizedRelativeEntry; - }; -}; - -export type NormalizingEntryResult = [ - Array>, - RelativeMiddlewareAnchor -]; diff --git a/packages/smithy-client/src/client.ts b/packages/smithy-client/src/client.ts index b2ff43dc83b2..5112a91b3290 100644 --- a/packages/smithy-client/src/client.ts +++ b/packages/smithy-client/src/client.ts @@ -1,4 +1,4 @@ -import { MiddlewareStack } from "@aws-sdk/middleware-stack"; +import { constructStack } from "@aws-sdk/middleware-stack"; import { Client as IClient, Command, MetadataBearer, RequestHandler } from "@aws-sdk/types"; export interface SmithyConfiguration { @@ -14,7 +14,7 @@ export class Client< ClientOutput extends MetadataBearer, ResolvedClientConfiguration extends SmithyResolvedConfiguration > implements IClient { - public middlewareStack = new MiddlewareStack(); + public middlewareStack = constructStack(); readonly config: ResolvedClientConfiguration; constructor(config: ResolvedClientConfiguration) { this.config = config; diff --git a/packages/smithy-client/src/command.ts b/packages/smithy-client/src/command.ts index 9295f6192887..f7065e4f2485 100644 --- a/packages/smithy-client/src/command.ts +++ b/packages/smithy-client/src/command.ts @@ -1,4 +1,4 @@ -import { MiddlewareStack } from "@aws-sdk/middleware-stack"; +import { constructStack } from "@aws-sdk/middleware-stack"; import { Command as ICommand, Handler, MetadataBearer, MiddlewareStack as IMiddlewareStack } from "@aws-sdk/types"; export abstract class Command< @@ -9,9 +9,9 @@ export abstract class Command< ClientOutput extends MetadataBearer = any > implements ICommand { abstract input: Input; - readonly middlewareStack: IMiddlewareStack = new MiddlewareStack(); + readonly middlewareStack: IMiddlewareStack = constructStack(); abstract resolveMiddleware( - stack: MiddlewareStack, + stack: IMiddlewareStack, configuration: ResolvedClientConfiguration, options: any ): Handler; diff --git a/packages/types/src/middleware.ts b/packages/types/src/middleware.ts index b93520513110..25979d343e91 100644 --- a/packages/types/src/middleware.ts +++ b/packages/types/src/middleware.ts @@ -218,7 +218,7 @@ export interface AbsoluteLocation { export type Relation = "before" | "after"; -export interface RelativeLocation { +export interface RelativeLocation { /** * Specify the relation to be before or after a know middleware. */ @@ -230,6 +230,8 @@ export interface RelativeLocation { toMiddleware: string; } +export type RelativeMiddlewareOptions = RelativeLocation & Omit; + export interface InitializeHandlerOptions extends HandlerOptions { step?: "initialize"; } @@ -259,32 +261,33 @@ export interface DeserializeHandlerOptions extends HandlerOptions { * `priority` options to `high` or `low`. * 2. Adding middleware to location relative to known middleware with `addRelativeTo()`. * This is useful when given middleware must be executed before or after specific - * middleware(`toMiddleware`). If specified `toMiddleware` is not found, given - * middleware will be added to given step with normal priority. - * Note `toMiddleware` can only be added to middleware stack staticly using - * `add()` API. + * middleware(`toMiddleware`). You can add a middleware relatively to another + * middleware which also added relatively. But eventually, this relative middleware + * chain **must** be 'anchored' by a middleware that added using `add()` API + * with absolute `step` and `priority`. This mothod will throw if specified + * `toMiddleware` is not found. */ -export interface MiddlewareStack { +export interface MiddlewareStack extends Pluggable { /** - * Add middleware to the list to be executed during the "initialize" step, + * Add middleware to the stack to be executed during the "initialize" step, * optionally specifying a priority, tags and name */ add(middleware: InitializeMiddleware, options?: InitializeHandlerOptions & AbsoluteLocation): void; /** - * Add middleware to the list to be executed during the "serialize" step, + * Add middleware to the stack to be executed during the "serialize" step, * optionally specifying a priority, tags and name */ add(middleware: SerializeMiddleware, options: SerializeHandlerOptions & AbsoluteLocation): void; /** - * Add middleware to the list to be executed during the "build" step, + * Add middleware to the stack to be executed during the "build" step, * optionally specifying a priority, tags and name */ add(middleware: BuildMiddleware, options: BuildHandlerOptions & AbsoluteLocation): void; /** - * Add middleware to the list to be executed during the "finalizeRequest" step, + * Add middleware to the stack to be executed during the "finalizeRequest" step, * optionally specifying a priority, tags and name */ add( @@ -293,65 +296,16 @@ export interface MiddlewareStack { ): void; /** - * Add middleware to the list to be executed during the "deserialize" step, + * Add middleware to the stack to be executed during the "deserialize" step, * optionally specifying a priority, tags and name */ add(middleware: DeserializeMiddleware, options: DeserializeHandlerOptions & AbsoluteLocation): void; /** - * Add middleware to location relative to a known middleware in 'initialize' step. - * Require setting `relation` (to `before` or `after`) and `toMiddleware` to - * identify the location of inserted middleware - * optionally specifying tags and name - */ - addRelativeTo( - middleware: InitializeMiddleware, - options: InitializeHandlerOptions & RelativeLocation - ): void; - - /** - * Add middleware to location relative to a known middleware in 'serialize' step. - * Require setting `relation` (to `before` or `after`) and `toMiddleware` to - * identify the location of inserted middleware - * optionally specifying tags and name + * Add middleware to a stack position before or after a known middleware,optionally + * specifying name and tags. */ - addRelativeTo( - middleware: SerializeMiddleware, - options: SerializeHandlerOptions & RelativeLocation - ): void; - - /** - * Add middleware to location relative to a known middleware in 'build' step. - * Require setting `relation` (to `before` or `after`) and `toMiddleware` to - * identify the location of inserted middleware - * optionally specifying tags and name - */ - addRelativeTo( - middleware: BuildMiddleware, - options: BuildHandlerOptions & RelativeLocation - ): void; - - /** - * Add middleware to location relative to a known middleware in 'finalizeRequest' step. - * Require setting `relation` (to `before` or `after`) and `toMiddleware` to - * identify the location of inserted middleware - * optionally specifying tags and name - */ - addRelativeTo( - middleware: FinalizeRequestMiddleware, - options: FinalizeRequestHandlerOptions & RelativeLocation - ): void; - - /** - * Add middleware to location relative to a known middleware in 'deserialize' step. - * Require setting `relation` (to `before` or `after`) and `toMiddleware` to - * identify the location of inserted middleware - * optionally specifying tags and name - */ - addRelativeTo( - middleware: DeserializeMiddleware, - options: DeserializeHandlerOptions & RelativeLocation - ): void; + addRelativeTo(middleware: MiddlewareType, options: RelativeMiddlewareOptions): void; /** * Apply a customization function to mutate the middleware stack, often @@ -360,7 +314,7 @@ export interface MiddlewareStack { use(pluggable: Pluggable): void; /** - * Create a shallow clone of this list. Step bindings and handler priorities + * Create a shallow clone of this stack. Step bindings and handler priorities * and tags are preserved in the copy. */ clone(): MiddlewareStack; @@ -383,8 +337,8 @@ export interface MiddlewareStack { removeByTag(toRemove: string): boolean; /** - * Create a list containing the middlewares in this list as well as the - * middlewares in the `from` list. Neither source is modified, and step + * Create a stack containing the middlewares in this stack as well as the + * middlewares in the `from` stack. Neither source is modified, and step * bindings and handler priorities and tags are preserved in the copy. */ concat( diff --git a/packages/util-create-request/src/foo.fixture.ts b/packages/util-create-request/src/foo.fixture.ts index bc58cb448b48..8361f7b86ac9 100644 --- a/packages/util-create-request/src/foo.fixture.ts +++ b/packages/util-create-request/src/foo.fixture.ts @@ -1,7 +1,7 @@ -import { MiddlewareStack } from "@aws-sdk/middleware-stack"; +import { constructStack } from "@aws-sdk/middleware-stack"; import { HttpRequest } from "@aws-sdk/protocol-http"; import { Client, Command } from "@aws-sdk/smithy-client"; -import { MetadataBearer } from "@aws-sdk/types"; +import { MetadataBearer, MiddlewareStack } from "@aws-sdk/types"; export interface OperationInput { String: string; @@ -22,14 +22,14 @@ const input: OperationInput = { String: "input" }; export const fooClient: Client = { config: {}, - middlewareStack: new MiddlewareStack(), + middlewareStack: constructStack(), send: (command: Command) => command.resolveMiddleware(this.middlewareStack, this.config, undefined)({ input }), destroy: () => {}, }; export const operationCommand: Command = { - middlewareStack: new MiddlewareStack(), + middlewareStack: constructStack(), input: {} as any, resolveMiddleware: (stack: MiddlewareStack) => { const concatStack = stack.concat(operationCommand.middlewareStack); diff --git a/packages/util-create-request/src/index.ts b/packages/util-create-request/src/index.ts index 1a1f64d641c7..11263c03e605 100644 --- a/packages/util-create-request/src/index.ts +++ b/packages/util-create-request/src/index.ts @@ -1,4 +1,3 @@ -import { MiddlewareStack } from "@aws-sdk/middleware-stack"; import { Client, Command } from "@aws-sdk/smithy-client"; import { BuildMiddleware, HttpRequest, MetadataBearer } from "@aws-sdk/types"; @@ -22,7 +21,7 @@ export async function createRequest< priority: "low", }); - const handler = command.resolveMiddleware(clientStack as MiddlewareStack, client.config, undefined); + const handler = command.resolveMiddleware(clientStack, client.config, undefined); //@ts-ignore return await handler(command).then((output) => output.output.request); diff --git a/yarn.lock b/yarn.lock index 9b4f9ef430b3..8f7d6160babc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11754,11 +11754,6 @@ typescript@3.7.x: resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.5.tgz#0692e21f65fd4108b9330238aac11dd2e177a1ae" integrity sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw== -typescript@~3.4.0: - version "3.4.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.4.5.tgz#2d2618d10bb566572b8d7aad5180d84257d70a99" - integrity sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw== - typescript@~3.9.3: version "3.9.6" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.6.tgz#8f3e0198a34c3ae17091b35571d3afd31999365a"