Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(smithy-client): add handler cache #1383

Merged
merged 3 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tall-cameras-reply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@smithy/smithy-client": minor
---

add client handler caching
26 changes: 25 additions & 1 deletion packages/smithy-client/src/client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
});
});
});
55 changes: 49 additions & 6 deletions packages/smithy-client/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
Client as IClient,
Command,
FetchHttpHandlerOptions,
Handler,
MetadataBearer,
MiddlewareStack,
NodeHttpHandlerOptions,
Expand All @@ -24,6 +25,22 @@ export interface SmithyConfiguration<HandlerOptions> {
* @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;
}

/**
Expand All @@ -32,6 +49,7 @@ export interface SmithyConfiguration<HandlerOptions> {
export type SmithyResolvedConfiguration<HandlerOptions> = {
requestHandler: RequestHandler<any, any, HandlerOptions>;
readonly apiVersion: string;
cacheMiddleware?: boolean;
};

/**
Expand All @@ -45,10 +63,13 @@ export class Client<
> implements IClient<ClientInput, ClientOutput, ResolvedClientConfiguration>
{
public middlewareStack: MiddlewareStack<ClientInput, ClientOutput> = constructStack<ClientInput, ClientOutput>();
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<Function, Handler<any, any>> | undefined;

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 +90,28 @@ 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 && this.config.cacheMiddleware === true;

let handler: Handler<any, any>;

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(
Expand All @@ -87,6 +129,7 @@ export class Client<
}

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