Skip to content

Commit

Permalink
feat(smithy-client): add handler cache
Browse files Browse the repository at this point in the history
  • Loading branch information
kuhe committed Aug 30, 2024
1 parent a2bb933 commit 10ed6c1
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 10 deletions.
6 changes: 6 additions & 0 deletions .changeset/tall-cameras-reply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@smithy/middleware-stack": minor
"@smithy/smithy-client": minor
---

add client handler caching
22 changes: 19 additions & 3 deletions packages/middleware-stack/src/MiddlewareStack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,14 @@ const getMiddlewareNameWithAliases = (name: string | undefined, aliases: Array<s
return `${name || "anonymous"}${aliases && aliases.length > 0 ? ` (a.k.a. ${aliases.join(",")})` : ""}`;
};

export const constructStack = <Input extends object, Output extends object>(): MiddlewareStack<Input, Output> => {
export const constructStack = <Input extends object, Output extends object>(
stackOptions: {
/**
* Optional change listener, called with stack instance when middleware added/removed.
*/
onChange?: (middlewareStack: MiddlewareStack<any, any>) => void;
} = {}
): MiddlewareStack<Input, Output> => {
let absoluteEntries: AbsoluteMiddlewareEntry<Input, Output>[] = [];
let relativeEntries: RelativeMiddlewareEntry<Input, Output>[] = [];
let identifyOnResolve = false;
Expand Down Expand Up @@ -222,6 +229,7 @@ export const constructStack = <Input extends object, Output extends object>(): M
}
}
absoluteEntries.push(entry);
stackOptions.onChange?.(stack);
},

addRelativeTo: (middleware: MiddlewareType<Input, Output>, options: HandlerOptions & RelativeLocation) => {
Expand Down Expand Up @@ -258,6 +266,7 @@ export const constructStack = <Input extends object, Output extends object>(): M
}
}
relativeEntries.push(entry);
stackOptions.onChange?.(stack);
},

clone: () => cloneTo(constructStack<Input, Output>()),
Expand All @@ -267,8 +276,14 @@ export const constructStack = <Input extends object, Output extends object>(): M
},

remove: (toRemove: MiddlewareType<Input, Output> | string): boolean => {
if (typeof toRemove === "string") return removeByName(toRemove);
else return removeByReference(toRemove);
let isRemoved: boolean;
if (typeof toRemove === "string") {
isRemoved = removeByName(toRemove);
} else {
isRemoved = removeByReference(toRemove);
}
stackOptions.onChange?.(stack);
return isRemoved;
},

removeByTag: (toRemove: string): boolean => {
Expand All @@ -287,6 +302,7 @@ export const constructStack = <Input extends object, Output extends object>(): M
};
absoluteEntries = absoluteEntries.filter(filterCb);
relativeEntries = relativeEntries.filter(filterCb);
stackOptions?.onChange?.(stack);
return isRemoved;
},

Expand Down
16 changes: 16 additions & 0 deletions packages/smithy-client/src/client-handlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Handler } from "@smithy/types";

import type { Client } from "./client";

/**
* @internal
*
* Holds a private handler cache map for individual client instances.
* It is a separate map to avoid changing the shape interface of the Client base class.
*
* The organization is:
* ```
* Map<Client (instance), Map<Command (constructor), HandlerFn>>
* ```
*/
export const clientHandlers = new WeakMap<Client<any, any, any, any>, WeakMap<Function, Handler<any, any>>>();
23 changes: 23 additions & 0 deletions packages/smithy-client/src/client.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Client } from "./client";
import { clientHandlers } from "./client-handlers";

describe("SmithyClient", () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Expand Down Expand Up @@ -50,4 +51,26 @@ describe("SmithyClient", () => {
};
client.send(getCommandWithOutput("foo") as any, options, callback);
});

describe("handler caching", () => {
beforeEach(() => {
clientHandlers.delete(client);
});

it("should cache the resolved handler", async () => {
await expect(client.send(getCommandWithOutput("foo") as any)).resolves.toEqual("foo");
expect(clientHandlers.get(client)!.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(clientHandlers.get(client)).toBeUndefined();
});

it("should clear the handler cache when the middleareStack is modified", async () => {
await expect(client.send(getCommandWithOutput("foo") as any)).resolves.toEqual("foo");
client.middlewareStack.add((n) => (a) => n(a));
expect(clientHandlers.get(client)).toBeUndefined();
});
});
});
41 changes: 34 additions & 7 deletions packages/smithy-client/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ import {
Client as IClient,
Command,
FetchHttpHandlerOptions,
Handler,
MetadataBearer,
MiddlewareStack,
NodeHttpHandlerOptions,
RequestHandler,
} from "@smithy/types";

import { clientHandlers } from "./client-handlers";

/**
* @public
*/
Expand Down Expand Up @@ -44,11 +47,14 @@ export class Client<
ResolvedClientConfiguration extends SmithyResolvedConfiguration<HandlerOptions>,
> implements IClient<ClientInput, ClientOutput, ResolvedClientConfiguration>
{
public middlewareStack: MiddlewareStack<ClientInput, ClientOutput> = constructStack<ClientInput, ClientOutput>();
readonly config: ResolvedClientConfiguration;
constructor(config: ResolvedClientConfiguration) {
this.config = config;
}
public middlewareStack: MiddlewareStack<ClientInput, ClientOutput> = constructStack<ClientInput, ClientOutput>({
onChange: () => {
clientHandlers.delete(this);
},
});

constructor(public readonly config: ResolvedClientConfiguration) {}

send<InputType extends ClientInput, OutputType extends ClientOutput>(
command: Command<ClientInput, InputType, ClientOutput, OutputType, SmithyResolvedConfiguration<HandlerOptions>>,
options?: HandlerOptions
Expand All @@ -69,7 +75,27 @@ export class Client<
): Promise<OutputType> | 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;

let handler: Handler<any, any>;

if (useHandlerCache) {
if (!clientHandlers.has(this)) {
clientHandlers.set(this, new WeakMap());
}
const handlers = clientHandlers.get(this)!;

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 {
handler = command.resolveMiddleware(this.middlewareStack as any, this.config, options);
}

if (callback) {
handler(command)
.then(
Expand All @@ -87,6 +113,7 @@ export class Client<
}

destroy() {
if (this.config.requestHandler.destroy) this.config.requestHandler.destroy();
this.config.requestHandler.destroy?.();
clientHandlers.delete(this);
}
}

0 comments on commit 10ed6c1

Please sign in to comment.