diff --git a/.changeset/tall-cameras-reply.md b/.changeset/tall-cameras-reply.md new file mode 100644 index 00000000000..f44eabadba9 --- /dev/null +++ b/.changeset/tall-cameras-reply.md @@ -0,0 +1,5 @@ +--- +"@smithy/smithy-client": minor +--- + +add client handler caching diff --git a/packages/smithy-client/src/client.spec.ts b/packages/smithy-client/src/client.spec.ts index 56ed3944e00..aca906032da 100644 --- a/packages/smithy-client/src/client.spec.ts +++ b/packages/smithy-client/src/client.spec.ts @@ -9,7 +9,7 @@ describe("SmithyClient", () => { const getCommandWithOutput = (output: string) => ({ resolveMiddleware: mockResolveMiddleware, }); - const client = new Client({} as any); + const client = new Client({ cacheMiddleware: true } as any); beforeEach(() => { jest.clearAllMocks(); @@ -50,4 +50,28 @@ describe("SmithyClient", () => { }; client.send(getCommandWithOutput("foo") as any, options, callback); }); + + describe("handler caching", () => { + beforeEach(() => { + delete (client as any).handlers; + }); + + const privateAccess = () => (client as any).handlers; + + it("should cache the resolved handler", async () => { + await expect(client.send(getCommandWithOutput("foo") as any)).resolves.toEqual("foo"); + expect(privateAccess().get({}.constructor)).toBeDefined(); + }); + + it("should not cache the resolved handler if called with request options", async () => { + await expect(client.send(getCommandWithOutput("foo") as any, {})).resolves.toEqual("foo"); + expect(privateAccess()).toBeUndefined(); + }); + + it("unsets the cache if client.destroy() is called.", async () => { + await expect(client.send(getCommandWithOutput("foo") as any)).resolves.toEqual("foo"); + client.destroy(); + expect(privateAccess()).toBeUndefined(); + }); + }); }); diff --git a/packages/smithy-client/src/client.ts b/packages/smithy-client/src/client.ts index c0fd856b53c..aa12da841ac 100644 --- a/packages/smithy-client/src/client.ts +++ b/packages/smithy-client/src/client.ts @@ -3,6 +3,7 @@ import { Client as IClient, Command, FetchHttpHandlerOptions, + Handler, MetadataBearer, MiddlewareStack, NodeHttpHandlerOptions, @@ -24,6 +25,22 @@ export interface SmithyConfiguration { * @internal */ readonly apiVersion: string; + /** + * @public + * + * Default false. + * + * When true, the client will only resolve the middleware stack once per + * Command class. This means modifying the middlewareStack of the + * command or client after requests have been made will not be + * recognized. + * + * Calling client.destroy() also clears this cache. + * + * Enable this only if needing the additional time saved (0-1ms per request) + * and not needing middleware modifications between requests. + */ + cacheMiddleware?: boolean; } /** @@ -32,6 +49,7 @@ export interface SmithyConfiguration { export type SmithyResolvedConfiguration = { requestHandler: RequestHandler; readonly apiVersion: string; + cacheMiddleware?: boolean; }; /** @@ -45,10 +63,13 @@ export class Client< > implements IClient { public middlewareStack: MiddlewareStack = constructStack(); - readonly config: ResolvedClientConfiguration; - constructor(config: ResolvedClientConfiguration) { - this.config = config; - } + /** + * May be used to cache the resolved handler function for a Command class. + */ + private handlers?: WeakMap> | undefined; + + constructor(public readonly config: ResolvedClientConfiguration) {} + send( command: Command>, options?: HandlerOptions @@ -69,7 +90,28 @@ export class Client< ): Promise | void { const options = typeof optionsOrCb !== "function" ? optionsOrCb : undefined; const callback = typeof optionsOrCb === "function" ? (optionsOrCb as (err: any, data?: OutputType) => void) : cb; - const handler = command.resolveMiddleware(this.middlewareStack as any, this.config, options); + + const useHandlerCache = options === undefined && this.config.cacheMiddleware === true; + + let handler: Handler; + + if (useHandlerCache) { + if (!this.handlers) { + this.handlers = new WeakMap(); + } + const handlers = this.handlers!; + + if (handlers.has(command.constructor)) { + handler = handlers.get(command.constructor)!; + } else { + handler = command.resolveMiddleware(this.middlewareStack as any, this.config, options); + handlers.set(command.constructor, handler); + } + } else { + delete this.handlers; + handler = command.resolveMiddleware(this.middlewareStack as any, this.config, options); + } + if (callback) { handler(command) .then( @@ -87,6 +129,7 @@ export class Client< } destroy() { - if (this.config.requestHandler.destroy) this.config.requestHandler.destroy(); + this.config?.requestHandler?.destroy?.(); + delete this.handlers; } }