diff --git a/packages/middleware-content-length/src/index.ts b/packages/middleware-content-length/src/index.ts index 868833bbf427d..3826b46fdcbe8 100644 --- a/packages/middleware-content-length/src/index.ts +++ b/packages/middleware-content-length/src/index.ts @@ -5,7 +5,8 @@ import { BodyLengthCalculator, MetadataBearer, BuildHandlerOutput, - Pluggable + Pluggable, + BuildHandlerOptions } from "@aws-sdk/types"; import { HttpRequest } from "@aws-sdk/protocol-http"; @@ -44,13 +45,19 @@ export function contentLengthMiddleware( }; } +export const contentLengthMiddlewareOptions: BuildHandlerOptions = { + step: "build", + tags: ["SET_CONTENT_LENGTH", "CONTENT_LENGTH"], + name: "contentLengthMiddleware" +}; + export const getContentLengthPlugin = (options: { bodyLengthChecker: BodyLengthCalculator; }): Pluggable => ({ applyToStack: clientStack => { - clientStack.add(contentLengthMiddleware(options.bodyLengthChecker), { - step: "build", - tags: { SET_CONTENT_LENGTH: true } - }); + clientStack.add( + contentLengthMiddleware(options.bodyLengthChecker), + contentLengthMiddlewareOptions + ); } }); diff --git a/packages/middleware-retry/src/retryMiddleware.ts b/packages/middleware-retry/src/retryMiddleware.ts index 0baf079bbc511..33ded6cd0d66a 100644 --- a/packages/middleware-retry/src/retryMiddleware.ts +++ b/packages/middleware-retry/src/retryMiddleware.ts @@ -3,7 +3,9 @@ import { FinalizeHandlerArguments, MetadataBearer, FinalizeHandlerOutput, - Pluggable + Pluggable, + FinalizeRequestHandlerOptions, + AbsoluteLocation } from "@aws-sdk/types"; import { RetryResolvedConfig } from "./configurations"; @@ -17,15 +19,20 @@ export function retryMiddleware(options: RetryResolvedConfig) { }; } +export const retryMiddlewareOptions: FinalizeRequestHandlerOptions & + AbsoluteLocation = { + name: "retryMiddleware", + tags: ["RETRY"], + step: "finalizeRequest", + priority: "high" +}; + export const getRetryPlugin = ( options: RetryResolvedConfig ): Pluggable => ({ applyToStack: clientStack => { if (options.maxRetries > 0) { - clientStack.add(retryMiddleware(options), { - step: "finalizeRequest", - tags: { RETRY: true } - }); + clientStack.add(retryMiddleware(options), retryMiddlewareOptions); } } }); diff --git a/packages/middleware-serde/src/serdePlugin.ts b/packages/middleware-serde/src/serdePlugin.ts index 5a7f32f2deb09..07aa6d7e8a755 100644 --- a/packages/middleware-serde/src/serdePlugin.ts +++ b/packages/middleware-serde/src/serdePlugin.ts @@ -5,11 +5,25 @@ import { MetadataBearer, MiddlewareStack, EndpointBearer, - RequestHandler + RequestHandler, + DeserializeHandlerOptions, + SerializeHandlerOptions } from "@aws-sdk/types"; import { deserializerMiddleware } from "./deserializerMiddleware"; import { serializerMiddleware } from "./serializerMiddleware"; +export const deserializerMiddlewareOption: DeserializeHandlerOptions = { + name: "deserializerMiddleware", + step: "deserialize", + tags: ["DESERIALIZER"] +}; + +export const serializerMiddlewareOption: SerializeHandlerOptions = { + name: "serializerMiddleware", + step: "serialize", + tags: ["SERIALIZER"] +}; + export function getSerdePlugin< InputType extends object, SerDeContext extends EndpointBearer, @@ -24,14 +38,14 @@ export function getSerdePlugin< ): Pluggable { return { applyToStack: (commandStack: MiddlewareStack) => { - commandStack.add(deserializerMiddleware(config, deserializer), { - step: "deserialize", - tags: { DESERIALIZER: true } - }); - commandStack.add(serializerMiddleware(config, serializer), { - step: "serialize", - tags: { SERIALIZER: true } - }); + commandStack.add( + deserializerMiddleware(config, deserializer), + deserializerMiddlewareOption + ); + commandStack.add( + serializerMiddleware(config, serializer), + serializerMiddlewareOption + ); } }; } diff --git a/packages/middleware-signing/src/middleware.spec.ts b/packages/middleware-signing/src/middleware.spec.ts index 6f7fce67cd8b9..5d41fda1fb96f 100644 --- a/packages/middleware-signing/src/middleware.spec.ts +++ b/packages/middleware-signing/src/middleware.spec.ts @@ -1,6 +1,6 @@ -import { signingMiddleware } from "./middleware"; +import { awsAuthMiddleware } from "./middleware"; import { RequestSigner } from "@aws-sdk/types"; -import { HttpRequest } from '@aws-sdk/protocol-http'; +import { HttpRequest } from "@aws-sdk/protocol-http"; describe("SigningHandler", () => { const noOpSigner: RequestSigner = { @@ -20,7 +20,10 @@ describe("SigningHandler", () => { }); it("should sign the request and pass it to the next handler", async () => { - const signingHandler = signingMiddleware({ signer: noOpSigner } as any)(noOpNext, {} as any); + const signingHandler = awsAuthMiddleware({ signer: noOpSigner } as any)( + noOpNext, + {} as any + ); await signingHandler({ input: {}, request: new HttpRequest({ diff --git a/packages/middleware-signing/src/middleware.ts b/packages/middleware-signing/src/middleware.ts index d5e4f7f3b0c1b..24d1ca2f2d602 100644 --- a/packages/middleware-signing/src/middleware.ts +++ b/packages/middleware-signing/src/middleware.ts @@ -3,12 +3,14 @@ import { FinalizeHandlerArguments, FinalizeRequestMiddleware, FinalizeHandlerOutput, - Pluggable + Pluggable, + RelativeLocation, + FinalizeRequestHandlerOptions } from "@aws-sdk/types"; import { AwsAuthResolvedConfig } from "./configurations"; import { HttpRequest } from "@aws-sdk/protocol-http"; -export function signingMiddleware( +export function awsAuthMiddleware( options: AwsAuthResolvedConfig ): FinalizeRequestMiddleware { return ( @@ -25,13 +27,22 @@ export function signingMiddleware( }; } +export const awsAuthMiddlewareOptions: FinalizeRequestHandlerOptions & + RelativeLocation = { + name: "awsAuthMiddleware", + step: "finalizeRequest", + tags: ["SIGNATURE", "AWSAUTH"], + relation: "after", + toMiddleware: "retryMiddleware" +}; + export const getAwsAuthPlugin = ( options: AwsAuthResolvedConfig ): Pluggable => ({ applyToStack: clientStack => { - clientStack.add(signingMiddleware(options), { - step: "finalizeRequest", - tags: { SIGNATURE: true } - }); + clientStack.addRelativeTo( + awsAuthMiddleware(options), + awsAuthMiddlewareOptions + ); } }); diff --git a/packages/middleware-stack/README.md b/packages/middleware-stack/README.md index bd33ef0c8956b..65bf9dfea1b7a 100644 --- a/packages/middleware-stack/README.md +++ b/packages/middleware-stack/README.md @@ -2,3 +2,97 @@ [![NPM version](https://img.shields.io/npm/v/@aws-sdk/middleware-stack/preview.svg)](https://www.npmjs.com/package/@aws-sdk/middleware-stack) [![NPM downloads](https://img.shields.io/npm/dm/@aws-sdk/middleware-stack.svg)](https://www.npmjs.com/package/@aws-sdk/middleware-stack) + +The package contains an implementation of middleware stack interface. Middleware +stack is a structure storing middleware in specified order and resolve these +middleware into a single handler. + +A middleware stack has five `Step`s, each of them represents a specific request life cycle: + +- **initialize**: The input is being prepared. Examples of typical initialization tasks include injecting default options computing derived parameters. + +- **serialize**: The input is complete and ready to be serialized. Examples of typical serialization tasks include input validation and building an HTTP request from user input. + +- **build**: The input has been serialized into an HTTP request, but that request may require further modification. Any request alterations will be applied to all retries. Examples of typical build tasks include injecting HTTP headers that describe a stable aspect of the request, such as `Content-Length` or a body checksum. + +- **finalizeRequest**: The request is being prepared to be sent over the wire. The request in this stage should already be semantically complete and should therefore only be altered to match the recipient's expectations. Examples of typical finalization tasks include request signing and injecting hop-by-hop headers. + +- **deserialize**: The response has arrived, the middleware here will deserialize the raw response object to structured response + +## Adding Middleware + +There are two ways to add middleware to a middleware stack. They both add middleware to specified `Step` but they provide fine-grained location control differently. + +### Absolute Location + +You can add middleware to specified step with: + +```javascript +stack.add(middleware, { + step: "finalizeRequest" +}); +``` + +This approach works for most cases. Sometimes you want your middleware to be executed in the front of the `Step`, you can set the `Priority` to `high`. Set the `Priority` to `low` then this middleware will be executed at the end of `Step`: + +```javascript +stack.add(middleware, { + step: "finalizeRequest", + priority: "high" +}); +``` + +If multiple middleware is added to same `step` with same `priority`, the order of them is determined by the order of adding them. + +### Relative Location + +In some cases, you might want to execute your middleware before some other known middleware, then you can use `addRelativeTo()`: + +```javascript +stack.add(middleware, { + step: "finalizeRequest", + name: "myMiddleware" +}); +stack.addRelativeTo(anotherMiddleware, { + step: "finalizeRequest", + relation: "before", //or 'after' + toMiddleware: "myMiddleware" +}); +``` + +You need to specify the `step` in `addRelativeTo()`. This is because the middleware function signature of each step is different, middleware for different step should not be mixed. The previous middleware **must** have a unique name, this is the only way to refer a known middleware when adding middleware relatively. Note that if specified `step` doesn't have a middleware named as the value in `toMiddleware`, this middleware will fallback to be added with absolute location. + +You can do this: + +```javascript +stack.addRelativeTo(middleware1, { + step: "finalizeRequest", + name: "Middleware1", + relation: 'before', + toMiddleware: 'middleware2' +}); //this will fall back to `add()` +stack.addRelativeTo(middleware2, { + step: "finalizeRequest", + name: 'Middleware2' + relation: "after", + toMiddleware: "Middleware1" +}); //this will be added after middleware1 +``` + +## Removing Middleware + +You can remove middleware by name one at a time: + +```javascript +stack.remove("Middleware1"); +``` + +If you specify tags for middleware, you can remove multiple middleware at a time according to tag: + +```javascript +stack.add(middleware, { + step: "finalizeRequest", + tags: ["final"] +}); +stack.removeByTag("final"); +``` diff --git a/packages/middleware-stack/src/MiddlewareStack.spec.ts b/packages/middleware-stack/src/MiddlewareStack.spec.ts new file mode 100644 index 0000000000000..7ec35a80551ea --- /dev/null +++ b/packages/middleware-stack/src/MiddlewareStack.spec.ts @@ -0,0 +1,325 @@ +import { MiddlewareStack } from "./MiddlewareStack"; +import { + FinalizeHandlerArguments, + InitializeMiddleware, + FinalizeRequestMiddleware, + BuildMiddleware, + InitializeHandlerOutput, + DeserializeHandlerArguments, + MiddlewareType, + Handler, + DeserializeMiddleware, + Pluggable +} from "@aws-sdk/types"; + +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) + }); + }; +} + +describe("MiddlewareStack", () => { + it("should resolve the stack into a composed handler", async () => { + const stack = new MiddlewareStack(); + const secondMW = getConcatMiddleware("second") as InitializeMiddleware< + input, + output + >; + 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< + input, + output + >, + { 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(); + + 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" + ] + }); + }); + + it("should allow adding middleware relatively", async () => { + const stack = new MiddlewareStack(); + type MW = InitializeMiddleware; + stack.addRelativeTo(getConcatMiddleware("G") as MW, { + name: "G", + relation: "after", + toMiddleware: "F" + }); + stack.addRelativeTo(getConcatMiddleware("A") as MW, { + name: "A", + relation: "after", + toMiddleware: "nonExist" + }); + stack.addRelativeTo(getConcatMiddleware("B") as MW, { + name: "B", + relation: "after", + toMiddleware: "A" + }); + stack.addRelativeTo(getConcatMiddleware("C") as MW, { + name: "C", + relation: "after", + toMiddleware: "A" + }); + stack.add(getConcatMiddleware("F") as MW, { + name: "F", + priority: "low" + }); + stack.addRelativeTo(getConcatMiddleware("D") as MW, { + name: "D", + relation: "before", + toMiddleware: "F" + }); + stack.addRelativeTo(getConcatMiddleware("E") as MW, { + name: "E", + relation: "before", + toMiddleware: "F" + }); + stack.addRelativeTo(getConcatMiddleware("H") as MW, { + name: "H", + relation: "before", + toMiddleware: "I" + }); + stack.addRelativeTo(getConcatMiddleware("I") as MW, { + name: "I", + relation: "after", + toMiddleware: "H" + }); + 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", "H", "I", "D", "E", "F", "G"] + }); + }); + + 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" + } + ); + + const secondStack = stack.clone(); + + let 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'"); + }); + + it("should allow combining stacks", async () => { + const stack = new MiddlewareStack(); + stack.add(getConcatMiddleware("second") as InitializeMiddleware< + input, + output + >); + stack.add( + getConcatMiddleware("first") as InitializeMiddleware, + { + name: "first", + priority: "high" + } + ); + + const secondStack = new MiddlewareStack(); + secondStack.add( + getConcatMiddleware("fourth") as FinalizeRequestMiddleware, + { step: "build" } + ); + secondStack.add( + getConcatMiddleware("third") as FinalizeRequestMiddleware, + { step: "build", priority: "high" } + ); + + let 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: [] }); + + expect(inner.mock.calls.length).toBe(1); + + secondStack.add( + getConcatMiddleware("first") as FinalizeRequestMiddleware, + { step: "build", priority: "high", name: "first" } + ); + expect(() => stack.concat(secondStack)).toThrow( + "Duplicated middleware name 'first'" + ); + }); + + it("should allow the removal of middleware by constructor identity", async () => { + const stack = new MiddlewareStack(); + stack.add( + getConcatMiddleware("don't remove me") as InitializeMiddleware< + input, + output + >, + { name: "notRemove" } + ); + stack.addRelativeTo( + getConcatMiddleware("remove me!") as InitializeMiddleware, + { + relation: "after", + toMiddleware: "notRemove", + name: "toRemove" + } + ); + + await stack.resolve( + ({ input }: FinalizeHandlerArguments>) => { + expect(input.sort()).toEqual(["don't remove me", "remove me!"]); + return Promise.resolve({ response: {} }); + }, + {} as any + )({ input: [] }); + + stack.remove("toRemove"); + + await stack.resolve( + ({ input }: FinalizeHandlerArguments>) => { + expect(input).toEqual(["don't remove me"]); + return Promise.resolve({ response: {} }); + }, + {} as any + )({ input: [] }); + }); + + 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"] + } + ); + + await stack.resolve( + ({ input }: FinalizeHandlerArguments>) => { + expect(input.sort()).toEqual(["not removed", "remove me!"]); + return Promise.resolve({ response: {} }); + }, + {} as any + )({ input: [] }); + + stack.removeByTag("baz"); + + 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: {} }); + }); + 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 new file mode 100644 index 0000000000000..077fa8d30d84f --- /dev/null +++ b/packages/middleware-stack/src/MiddlewareStack.ts @@ -0,0 +1,455 @@ +import { + InitializeHandlerOptions, + BuildHandlerOptions, + FinalizeRequestHandlerOptions, + MiddlewareType, + SerializeMiddleware, + FinalizeRequestMiddleware, + HandlerExecutionContext, + HandlerOptions, + InitializeMiddleware, + MiddlewareStack as IMiddlewareStack, + SerializeHandlerOptions, + Step, + DeserializeMiddleware, + DeserializeHandlerOptions, + DeserializeHandler, + Pluggable, + BuildMiddleware, + Handler, + Priority, + AbsoluteLocation, + RelativeLocation +} from "@aws-sdk/types"; +import { + MiddlewareEntry, + RelativeMiddlewareEntry, + NormalizedRelativeEntry, + NamedMiddlewareEntriesMap, + NamedRelativeEntriesMap, + RelativeMiddlewareAnchor, + NormalizingEntryResult +} from "./types"; + +export interface MiddlewareStack + extends IMiddlewareStack {} + +export class MiddlewareStack { + private readonly absoluteEntries: Array> = []; + private readonly relativeEntries: Array< + RelativeMiddlewareEntry + > = []; + 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; + + addRelativeTo( + middleware: DeserializeMiddleware, + options: DeserializeHandlerOptions & RelativeLocation + ): void; + + 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< + MiddlewareEntry | NormalizedRelativeEntry + > + ): Array< + MiddlewareEntry | NormalizedRelativeEntry + > { + //reverse before sorting so that middleware of same step will execute in + //the order of being added + return entries + .reverse() + .sort( + (a, b) => + stepWeights[a.step] - stepWeights[b.step] || + priorityWeights[a.priority || "normal"] - + priorityWeights[b.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; + } + } + return removed; + } + + use(plugin: Pluggable) { + plugin.applyToStack(this); + } + + /** + * 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 + */ + 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; + } + } 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; + } + } + } + // get the head of the relative middleware linked list that have + // no transitive relation to absolute middleware. + const orphanedRelativeEntries: Array< + NormalizedRelativeEntry + > = []; + const visited: WeakSet< + NormalizedRelativeEntry + > = 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 geMiddlewareList(): Array> { + let 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 = next; + while (relativeEntry) { + middlewareList.push(relativeEntry.middleware); + relativeEntry = relativeEntry.next; + } + let orphanedEntry = entry as any; + while ((orphanedEntry as any).next) { + middlewareList.push((orphanedEntry as any).next.middleware); + orphanedEntry = (orphanedEntry as any).next; + } + middlewareList.push(entry.middleware); + relativeEntry = prev; + while (relativeEntry) { + middlewareList.push(relativeEntry.middleware); + relativeEntry = relativeEntry.prev; + } + } + return middlewareList; + } + + resolve( + handler: DeserializeHandler, + context: HandlerExecutionContext + ): Handler { + for (const middleware of this.geMiddlewareList()) { + handler = middleware( + handler as Handler, + context + ) as any; + } + + return handler as Handler; + } +} + +const stepWeights: { [key in Step]: number } = { + initialize: 5, + serialize: 4, + build: 3, + finalizeRequest: 2, + deserialize: 1 +}; + +const priorityWeights: { [key in Priority]: number } = { + high: 3, + normal: 2, + low: 1 +}; diff --git a/packages/middleware-stack/src/index.spec.ts b/packages/middleware-stack/src/index.spec.ts deleted file mode 100644 index 8501226ee9641..0000000000000 --- a/packages/middleware-stack/src/index.spec.ts +++ /dev/null @@ -1,235 +0,0 @@ -import { MiddlewareStack } from "./"; -import { - Handler, - HandlerArguments, - FinalizeHandlerArguments, - Middleware, - HandlerExecutionContext, - FinalizeRequestMiddleware, - FinalizeHandler, - BuildMiddleware, - HandlerOutput, - DeserializeHandlerArguments -} from "@aws-sdk/types"; -import { request } from "https"; - -type input = Array; -type output = object; - -//return tagged union to make compiler happy -function getConcatMiddleware( - message: string -): Middleware | FinalizeRequestMiddleware { - return (next: Handler): Handler => { - return (args: HandlerArguments): Promise> => - next({ - ...args, - input: args.input.concat(message) - }); - }; -} -function shuffle(arr: Array): Array { - arr = [...arr]; - for (let i = arr.length; i > 0; i--) { - const rand = Math.floor(Math.random() * i); - const curr = i - 1; - [arr[curr], arr[rand]] = [arr[rand], arr[curr]]; - } - return arr; -} - -describe("MiddlewareStack", () => { - it("should resolve the stack into a composed handler", async () => { - const stack = new MiddlewareStack(); - - const middleware = shuffle([ - [getConcatMiddleware("second"), {}], - [getConcatMiddleware("first"), { priority: 10 }], - [getConcatMiddleware("fourth"), { step: "build" }], - [getConcatMiddleware("third"), { step: "build", priority: 1 }], - [getConcatMiddleware("fifth"), { step: "finalizeRequest" }], - [getConcatMiddleware("sixth"), { step: "finalizeRequest", priority: -1 }], - [getConcatMiddleware("seven"), { step: "deserialize" }] - ]); - - for (const [mw, options] of middleware) { - stack.add(mw as any, options); - } - - 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: ["first", "second", "third", "fourth", "fifth", "sixth", "seven"] - }); - }); - - it("should allow cloning", async () => { - const stack = new MiddlewareStack(); - stack.add(getConcatMiddleware("second") as Middleware); - stack.add(getConcatMiddleware("first") as Middleware, { - priority: 100 - }); - - const secondStack = stack.clone(); - - let 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); - }); - - it("should allow combining stacks", async () => { - const stack = new MiddlewareStack(); - stack.add(getConcatMiddleware("second") as Middleware); - stack.add(getConcatMiddleware("first") as Middleware, { - priority: 100 - }); - - const secondStack = new MiddlewareStack(); - secondStack.add( - getConcatMiddleware("fourth") as FinalizeRequestMiddleware, - { step: "build" } - ); - secondStack.add( - getConcatMiddleware("third") as FinalizeRequestMiddleware, - { step: "build", priority: 100 } - ); - - let 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: [] }); - - expect(inner.mock.calls.length).toBe(1); - }); - - it("should allow the removal of middleware by constructor identity", async () => { - const MyMiddleware = getConcatMiddleware("remove me!") as Middleware< - input, - output - >; - const stack = new MiddlewareStack(); - stack.add(MyMiddleware); - stack.add( - getConcatMiddleware("don't remove me") as Middleware - ); - - await stack.resolve( - ({ input }: FinalizeHandlerArguments>) => { - expect(input.sort()).toEqual(["don't remove me", "remove me!"]); - return Promise.resolve({ response: {} }); - }, - {} as any - )({ input: [] }); - - stack.remove(MyMiddleware); - - await stack.resolve( - ({ input }: FinalizeHandlerArguments>) => { - expect(input).toEqual(["don't remove me"]); - return Promise.resolve({ response: {} }); - }, - {} as any - )({ input: [] }); - }); - - it("should allow the removal of middleware by tag", async () => { - const stack = new MiddlewareStack(); - stack.add(getConcatMiddleware("not removed") as Middleware, { - tags: { foo: true, bar: true } - }); - stack.add(getConcatMiddleware("remove me!") as Middleware, { - tags: { foo: true, bar: true, baz: true } - }); - - await stack.resolve( - ({ input }: FinalizeHandlerArguments>) => { - expect(input.sort()).toEqual(["not removed", "remove me!"]); - return Promise.resolve({ response: {} }); - }, - {} as any - )({ input: [] }); - - stack.remove("baz"); - - await stack.resolve( - ({ input }: DeserializeHandlerArguments>) => { - expect(input).toEqual(["not removed"]); - return Promise.resolve({ response: {} }); - }, - {} as any - )({ input: [] }); - }); - - it("should allow filtering of middlewares by middleware options", async () => { - const stack = new MiddlewareStack(); - stack.add(getConcatMiddleware("first") as Middleware, { - priority: 1 - }); - stack.add(getConcatMiddleware("second") as Middleware, { - tags: { foo: true, bar: true } - }); - stack.add(getConcatMiddleware("third") as Middleware, { - step: "initialize" - }); - stack.add(getConcatMiddleware("fourth") as Middleware, { - step: "serialize" - }); - stack.add(getConcatMiddleware("fifth") as BuildMiddleware, { - step: "build" - }); - stack.add( - getConcatMiddleware("sixth") as FinalizeRequestMiddleware, - { - step: "finalizeRequest" - } - ); - const filteredStack = stack.filter(middlewareStats => { - return ( - middlewareStats.priority === 1 || - (middlewareStats.tags && middlewareStats.tags.foo === true) || - middlewareStats.step === "initialize" - ); - }); - const handler = jest.fn(({ input }: DeserializeHandlerArguments) => { - expect(input).toEqual(["first", "third", "second"]); - return Promise.resolve({ response: {} }); - }); - - const composed = filteredStack.resolve(handler, {} as any); - await composed({ input: [] }); - - expect(handler.mock.calls.length).toBe(1); - }); - - it("should not allow altering stack that to be filtered", async () => { - const stack = new MiddlewareStack(); - stack.add(getConcatMiddleware("first") as Middleware, { - priority: 1 - }); - stack.add(getConcatMiddleware("second") as Middleware, { - tags: { foo: true, bar: true, baz: true } - }); - const filteredStack = stack.filter(middlewareStats => { - if (middlewareStats.tags!.baz) { - //try make "second" middleware prior to "first" middleware - middlewareStats.priority = 100; - } - return true; - }); - let inner = jest.fn(({ input }: DeserializeHandlerArguments) => { - expect(input).toEqual(["first", "second"]); - return Promise.resolve({ response: {} }); - }); - await filteredStack.resolve(inner, {} as any)({ input: [] }); - - expect(inner.mock.calls.length).toBe(1); - }); -}); diff --git a/packages/middleware-stack/src/index.ts b/packages/middleware-stack/src/index.ts index aead414822f82..16f56ce96abb1 100644 --- a/packages/middleware-stack/src/index.ts +++ b/packages/middleware-stack/src/index.ts @@ -1,173 +1 @@ -import { - BuildHandlerOptions, - FinalizeRequestHandlerOptions, - SerializeMiddleware, - FinalizeRequestMiddleware, - Handler, - HandlerExecutionContext, - HandlerOptions, - Middleware, - MiddlewareStack as IMiddlewareStack, - SerializeHandlerOptions, - Step, - DeserializeMiddleware, - DeserializeHandlerOptions, - DeserializeHandler, - Pluggable -} from "@aws-sdk/types"; - -interface HandlerListEntry { - step: Step; - priority: number; - middleware: Middleware; - tags?: { [tag: string]: any }; -} - -export interface MiddlewareStack - extends IMiddlewareStack {} - -export class MiddlewareStack { - private readonly entries: Array> = []; - private sorted: boolean = true; - - add( - middleware: Middleware, - options?: HandlerOptions & { step?: "initialize" } - ): void; - - add( - middleware: SerializeMiddleware, - options: SerializeHandlerOptions - ): void; - - add( - middleware: FinalizeRequestMiddleware, - options: BuildHandlerOptions - ): void; - - add( - middleware: FinalizeRequestMiddleware, - options: FinalizeRequestHandlerOptions - ): void; - - add( - middleware: DeserializeMiddleware, - options: DeserializeHandlerOptions - ): void; - - add( - middleware: Middleware, - options: HandlerOptions = {} - ): void { - const { step = "initialize", priority = 0, tags } = options; - this.sorted = false; - this.entries.push({ - middleware, - priority, - step, - tags - }); - } - - use(pluggable: Pluggable) { - pluggable.applyToStack(this); - } - - clone(): IMiddlewareStack { - const clone = new MiddlewareStack(); - clone.entries.push(...this.entries); - clone.sorted = this.sorted; - return clone; - } - - concat( - from: MiddlewareStack - ): MiddlewareStack { - const clone = new MiddlewareStack(); - clone.entries.push(...(this.entries as any), ...from.entries); - clone.sorted = false; - return clone; - } - - remove(toRemove: Middleware | string): boolean { - const { length } = this.entries; - if (typeof toRemove === "string") { - this.removeByTag(toRemove); - } else { - this.removeByIdentity(toRemove); - } - - return this.entries.length < length; - } - - filter( - callbackfn: (handlerOptions: HandlerOptions) => boolean - ): MiddlewareStack { - const filtered = new MiddlewareStack(); - for (const entry of this.entries) { - const options: HandlerOptions = { - step: entry.step, - priority: entry.priority, - tags: { - ...entry.tags - } - }; - if (callbackfn(options)) { - filtered.entries.push(entry); - } - } - filtered.sorted = this.sorted; - return filtered; - } - - resolve( - handler: DeserializeHandler, - context: HandlerExecutionContext - ): Handler { - if (!this.sorted) { - this.sort(); - } - - for (const { middleware } of this.entries) { - handler = middleware( - handler as Handler, - context - ) as any; - } - - return handler as Handler; - } - - private removeByIdentity(toRemove: Middleware) { - for (let i = this.entries.length - 1; i >= 0; i--) { - if (this.entries[i].middleware === toRemove) { - this.entries.splice(i, 1); - } - } - } - - private removeByTag(toRemove: string) { - for (let i = this.entries.length - 1; i >= 0; i--) { - const { tags } = this.entries[i]; - if (tags && toRemove in tags) { - this.entries.splice(i, 1); - } - } - } - - private sort(): void { - this.entries.sort((a, b) => { - const stepWeight = stepWeights[a.step] - stepWeights[b.step]; - return stepWeight || a.priority - b.priority; - }); - this.sorted = true; - } -} - -const stepWeights = { - initialize: 5, - serialize: 4, - build: 3, - finalizeRequest: 2, - deserialize: 1 -}; +export * from "./MiddlewareStack"; diff --git a/packages/middleware-stack/src/types.ts b/packages/middleware-stack/src/types.ts new file mode 100644 index 0000000000000..36788a05393b5 --- /dev/null +++ b/packages/middleware-stack/src/types.ts @@ -0,0 +1,60 @@ +import { HandlerOptions, Step, MiddlewareType, Priority } from "@aws-sdk/types"; +export interface MiddlewareEntry + extends HandlerOptions { + step: Step; + middleware: MiddlewareType; + priority: Priority; +} + +export interface RelativeMiddlewareEntry< + Input extends object, + Output extends object +> extends HandlerOptions { + step: Step; + middleware: MiddlewareType; + next?: string; + prev?: string; +} + +export interface NormalizedRelativeEntry< + Input extends object, + Output extends object +> extends HandlerOptions { + step: Step; + middleware: MiddlewareType; + next?: NormalizedRelativeEntry; + prev?: NormalizedRelativeEntry; + priority: null; +} + +export type NamedMiddlewareEntriesMap< + Input extends object, + Output extends object +> = { + [key: string]: MiddlewareEntry; +}; + +export type NamedRelativeEntriesMap< + Input extends object, + Output extends object +> = { + [key: string]: NormalizedRelativeEntry; +}; + +export type RelativeMiddlewareAnchor< + Input extends object, + Output extends object +> = { + [name: string]: { + prev?: NormalizedRelativeEntry; + next?: NormalizedRelativeEntry; + }; +}; + +export type NormalizingEntryResult< + Input extends object, + Output extends object +> = [ + Array>, + RelativeMiddlewareAnchor +]; diff --git a/packages/middleware-user-agent/src/middleware.ts b/packages/middleware-user-agent/src/middleware.ts index 82cc4b60e77a1..95a03dd190468 100644 --- a/packages/middleware-user-agent/src/middleware.ts +++ b/packages/middleware-user-agent/src/middleware.ts @@ -3,7 +3,8 @@ import { BuildHandler, MetadataBearer, BuildHandlerOutput, - Pluggable + Pluggable, + BuildHandlerOptions } from "@aws-sdk/types"; import { HttpRequest } from "@aws-sdk/protocol-http"; import { UserAgentResolvedConfig } from "./configurations"; @@ -34,13 +35,16 @@ export function userAgentMiddleware(options: UserAgentResolvedConfig) { }; } +export const getUserAgentMiddlewareOptions: BuildHandlerOptions = { + name: "getUserAgentMiddleware", + step: "build", + tags: ["SET_USER_AGENT", "USER_AGENT"] +}; + export const getUserAgentPlugin = ( config: UserAgentResolvedConfig ): Pluggable => ({ applyToStack: clientStack => { - clientStack.add(userAgentMiddleware(config), { - step: "build", - tags: { SET_USER_AGENT: true } - }); + clientStack.add(userAgentMiddleware(config), getUserAgentMiddlewareOptions); } }); diff --git a/packages/types/src/middleware.ts b/packages/types/src/middleware.ts index d0025cc91dc47..a3e974a1655b3 100644 --- a/packages/types/src/middleware.ts +++ b/packages/types/src/middleware.ts @@ -1,6 +1,6 @@ import { Logger } from "./logger"; -export interface HandlerArguments { +export interface InitializeHandlerArguments { /** * User input to a command. Reflects the userland representation of the * union of data types the command can effectively handle. @@ -8,13 +8,13 @@ export interface HandlerArguments { input: Input; } -export interface HandlerOutput +export interface InitializeHandlerOutput extends DeserializeHandlerOutput { output: Output; } export interface SerializeHandlerArguments - extends HandlerArguments { + extends InitializeHandlerArguments { /** * The user input serialized as a request object. The request object is unknown, * so you cannot modify it directly. When work with request, you need to guard its @@ -27,10 +27,16 @@ export interface SerializeHandlerArguments } export interface SerializeHandlerOutput - extends HandlerOutput {} + extends InitializeHandlerOutput {} + +export interface BuildHandlerArguments + extends FinalizeHandlerArguments {} + +export interface BuildHandlerOutput + extends InitializeHandlerOutput {} export interface FinalizeHandlerArguments - extends HandlerArguments { + extends SerializeHandlerArguments { /** * The user input serialized as a request. */ @@ -38,13 +44,7 @@ export interface FinalizeHandlerArguments } export interface FinalizeHandlerOutput - extends HandlerOutput {} - -export interface BuildHandlerArguments - extends FinalizeHandlerArguments {} - -export interface BuildHandlerOutput - extends HandlerOutput {} + extends InitializeHandlerOutput {} export interface DeserializeHandlerArguments extends FinalizeHandlerArguments {} @@ -62,16 +62,26 @@ export interface DeserializeHandlerOutput { output?: Output; } -export interface Handler { +export interface InitializeHandler< + Input extends object, + Output extends object +> { /** * Asynchronously converts an input object into an output object. * * @param args An object containing a input to the command as well as any * associated or previously generated execution artifacts. */ - (args: HandlerArguments): Promise>; + (args: InitializeHandlerArguments): Promise< + InitializeHandlerOutput + >; } +export type Handler< + Input extends object, + Output extends object +> = InitializeHandler; + export interface SerializeHandler { /** * Asynchronously converts an input object into an output object. @@ -96,8 +106,9 @@ export interface FinalizeHandler { >; } -export interface BuildHandler - extends FinalizeHandler {} +export interface BuildHandler { + (args: BuildHandlerArguments): Promise>; +} export interface DeserializeHandler< Input extends object, @@ -112,17 +123,20 @@ export interface DeserializeHandler< * A factory function that creates functions implementing the {Handler} * interface. */ -export interface Middleware { +export interface InitializeMiddleware< + Input extends object, + Output extends object +> { /** * @param next The handler to invoke after this middleware has operated on * the user input and before this middleware operates on the output. * * @param context Invariant data and functions for use by the handler. */ - (next: Handler, context: HandlerExecutionContext): Handler< - Input, - Output - >; + ( + next: InitializeHandler, + context: HandlerExecutionContext + ): InitializeHandler; } /** @@ -165,16 +179,30 @@ export interface FinalizeRequestMiddleware< ): FinalizeHandler; } -export interface BuildMiddleware - extends FinalizeRequestMiddleware {} +export interface BuildMiddleware { + ( + next: BuildHandler, + context: HandlerExecutionContext + ): BuildHandler; +} export interface DeserializeMiddleware< Input extends object, Output extends object > { - (next: DeserializeHandler): DeserializeHandler; + ( + next: DeserializeHandler, + context: HandlerExecutionContext + ): DeserializeHandler; } +export type MiddlewareType = + | InitializeMiddleware + | SerializeMiddleware + | BuildMiddleware + | FinalizeRequestMiddleware + | DeserializeMiddleware; + /** * A factory function that creates the terminal handler atop which a middleware * stack sits. @@ -192,6 +220,8 @@ export type Step = | "finalizeRequest" | "deserialize"; +export type Priority = "high" | "normal" | "low"; + export interface HandlerOptions { /** * Handlers are ordered using a "step" that describes the stage of command @@ -225,22 +255,42 @@ export interface HandlerOptions { step?: Step; /** - * A number that specifies how early in a given step of the middleware stack - * a handler should be executed. Higher numeric priorities will be executed - * earlier. - * - * Middleware registered at the same step and with the same priority may be - * executed in any order. + * A list of strings to any that identify the general purpose or important + * characteristics of a given handler. + */ + tags?: Array; + + /** + * A unique name to refer to a middleware + */ + name?: string; +} +export interface AbsoluteLocation { + /** + * By default middleware will be added to individual step in un-guaranteed order. + * In the case that * - * @default 0 + * @default 'normal' */ - priority?: number; + priority?: Priority; +} +export type Relation = "before" | "after"; + +export interface RelativeLocation { /** - * A map of strings to any that identify the general purpose or important - * characteristics of a given handler. + * Specify the relation to be before or after a know middleware. + */ + relation: Relation; + + /** + * A known middleware name to indicate inserting middleware's location. */ - tags?: { [tag: string]: any }; + toMiddleware: string; +} + +export interface InitializeHandlerOptions extends HandlerOptions { + step?: "initialize"; } export interface SerializeHandlerOptions extends HandlerOptions { @@ -259,51 +309,125 @@ export interface DeserializeHandlerOptions extends HandlerOptions { step: "deserialize"; } +/** + * A stack storing middleware. It can be resolved into a handler. It supports 2 + * approaches for adding middleware: + * 1. Adding middleware to specific step with `add()`. The order of middleware + * added into same step is determined by order of adding them. If one middleware + * needs to be executed at the front of the step or at the end of step, set + * `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. + */ export interface MiddlewareStack { /** - * Add middleware to the list, optionally specifying a priority and tags. + * Add middleware to the list to be executed during the "initialize" step, + * optionally specifying a priority, tags and name */ add( - middleware: Middleware, - options?: HandlerOptions & { step?: "initialize" } + middleware: InitializeMiddleware, + options?: InitializeHandlerOptions & AbsoluteLocation ): void; /** - * Add middleware to the list to be executed during the "serialize" phase, - * optionally specifying a priority and tags. + * Add middleware to the list to be executed during the "serialize" step, + * optionally specifying a priority, tags and name */ add( middleware: SerializeMiddleware, - options: SerializeHandlerOptions + options: SerializeHandlerOptions & AbsoluteLocation ): void; /** - * Add middleware to the list to be executed during the "build" phase, - * optionally specifying a priority and tags. + * Add middleware to the list to be executed during the "build" step, + * optionally specifying a priority, tags and name */ add( - middleware: FinalizeRequestMiddleware, - options: BuildHandlerOptions + middleware: BuildMiddleware, + options: BuildHandlerOptions & AbsoluteLocation ): void; /** - * Add middleware to the list to be executed during the "finalizeRequest" phase, - * optionally specifying a priority and tags. + * Add middleware to the list to be executed during the "finalizeRequest" step, + * optionally specifying a priority, tags and name */ add( middleware: FinalizeRequestMiddleware, - options: FinalizeRequestHandlerOptions + options: FinalizeRequestHandlerOptions & AbsoluteLocation ): void; /** - * Add middleware to the list to be executed during the "deserialize" phase, - * optionally specifying a priority and tags. + * Add middleware to the list to be executed during the "deserialize" step, + * optionally specifying a priority, tags and name */ add( middleware: DeserializeMiddleware, - options: DeserializeHandlerOptions + 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 + */ + 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; + + /** + * Apply a customization function to mutate the middleware stack, often + * used for customizations that requires mutating multiple middleware. + */ use(pluggable: Pluggable): void; /** @@ -313,12 +437,21 @@ export interface MiddlewareStack { clone(): MiddlewareStack; /** - * same to clone, but only filter in middlewares when evaluation callback - * function returns true. + * Removes middleware from the stack. + * + * If a string is provided, it will be treated as middleware name. If a middleware + * is inserted with the given name, it will be removed. + * + * If a middleware class is provided, all usages thereof will be removed. + */ + remove(toRemove: MiddlewareType | string): boolean; + + /** + * Removes middleware that contains given tag + * + * Multiple middleware will potentially be removed */ - filter( - callbackfn: (middlewareStats: HandlerOptions) => boolean - ): MiddlewareStack; + removeByTag(toRemove: string): boolean; /** * Create a list containing the middlewares in this list as well as the @@ -329,16 +462,6 @@ export interface MiddlewareStack { from: MiddlewareStack ): MiddlewareStack; - /** - * Removes middleware from the stack. - * - * If a string is provided, any entry in the stack whose tags contain that - * string will be removed from the stack. - * - * If a middleware class is provided, all usages thereof will be removed. - */ - remove(toRemove: Middleware | string): boolean; - /** * Builds a single handler function from zero or more middleware classes and * a core handler. The core handler is meant to send command objects to AWS @@ -352,7 +475,7 @@ export interface MiddlewareStack { resolve( handler: DeserializeHandler, context: HandlerExecutionContext - ): Handler; + ): InitializeHandler; } /**