Skip to content

Commit d8df7bf

Browse files
authored
feat(smithy-client): add handler cache (smithy-lang#1383)
* feat(smithy-client): add handler cache * make handler cache configurable * Update client.spec.ts
1 parent 5510e83 commit d8df7bf

File tree

3 files changed

+79
-7
lines changed

3 files changed

+79
-7
lines changed

.changeset/tall-cameras-reply.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@smithy/smithy-client": minor
3+
---
4+
5+
add client handler caching

packages/smithy-client/src/client.spec.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ describe("SmithyClient", () => {
99
const getCommandWithOutput = (output: string) => ({
1010
resolveMiddleware: mockResolveMiddleware,
1111
});
12-
const client = new Client({} as any);
12+
const client = new Client({ cacheMiddleware: true } as any);
1313

1414
beforeEach(() => {
1515
jest.clearAllMocks();
@@ -50,4 +50,28 @@ describe("SmithyClient", () => {
5050
};
5151
client.send(getCommandWithOutput("foo") as any, options, callback);
5252
});
53+
54+
describe("handler caching", () => {
55+
beforeEach(() => {
56+
delete (client as any).handlers;
57+
});
58+
59+
const privateAccess = () => (client as any).handlers;
60+
61+
it("should cache the resolved handler", async () => {
62+
await expect(client.send(getCommandWithOutput("foo") as any)).resolves.toEqual("foo");
63+
expect(privateAccess().get({}.constructor)).toBeDefined();
64+
});
65+
66+
it("should not cache the resolved handler if called with request options", async () => {
67+
await expect(client.send(getCommandWithOutput("foo") as any, {})).resolves.toEqual("foo");
68+
expect(privateAccess()).toBeUndefined();
69+
});
70+
71+
it("unsets the cache if client.destroy() is called.", async () => {
72+
await expect(client.send(getCommandWithOutput("foo") as any)).resolves.toEqual("foo");
73+
client.destroy();
74+
expect(privateAccess()).toBeUndefined();
75+
});
76+
});
5377
});

packages/smithy-client/src/client.ts

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
Client as IClient,
44
Command,
55
FetchHttpHandlerOptions,
6+
Handler,
67
MetadataBearer,
78
MiddlewareStack,
89
NodeHttpHandlerOptions,
@@ -24,6 +25,22 @@ export interface SmithyConfiguration<HandlerOptions> {
2425
* @internal
2526
*/
2627
readonly apiVersion: string;
28+
/**
29+
* @public
30+
*
31+
* Default false.
32+
*
33+
* When true, the client will only resolve the middleware stack once per
34+
* Command class. This means modifying the middlewareStack of the
35+
* command or client after requests have been made will not be
36+
* recognized.
37+
*
38+
* Calling client.destroy() also clears this cache.
39+
*
40+
* Enable this only if needing the additional time saved (0-1ms per request)
41+
* and not needing middleware modifications between requests.
42+
*/
43+
cacheMiddleware?: boolean;
2744
}
2845

2946
/**
@@ -32,6 +49,7 @@ export interface SmithyConfiguration<HandlerOptions> {
3249
export type SmithyResolvedConfiguration<HandlerOptions> = {
3350
requestHandler: RequestHandler<any, any, HandlerOptions>;
3451
readonly apiVersion: string;
52+
cacheMiddleware?: boolean;
3553
};
3654

3755
/**
@@ -45,10 +63,13 @@ export class Client<
4563
> implements IClient<ClientInput, ClientOutput, ResolvedClientConfiguration>
4664
{
4765
public middlewareStack: MiddlewareStack<ClientInput, ClientOutput> = constructStack<ClientInput, ClientOutput>();
48-
readonly config: ResolvedClientConfiguration;
49-
constructor(config: ResolvedClientConfiguration) {
50-
this.config = config;
51-
}
66+
/**
67+
* May be used to cache the resolved handler function for a Command class.
68+
*/
69+
private handlers?: WeakMap<Function, Handler<any, any>> | undefined;
70+
71+
constructor(public readonly config: ResolvedClientConfiguration) {}
72+
5273
send<InputType extends ClientInput, OutputType extends ClientOutput>(
5374
command: Command<ClientInput, InputType, ClientOutput, OutputType, SmithyResolvedConfiguration<HandlerOptions>>,
5475
options?: HandlerOptions
@@ -69,7 +90,28 @@ export class Client<
6990
): Promise<OutputType> | void {
7091
const options = typeof optionsOrCb !== "function" ? optionsOrCb : undefined;
7192
const callback = typeof optionsOrCb === "function" ? (optionsOrCb as (err: any, data?: OutputType) => void) : cb;
72-
const handler = command.resolveMiddleware(this.middlewareStack as any, this.config, options);
93+
94+
const useHandlerCache = options === undefined && this.config.cacheMiddleware === true;
95+
96+
let handler: Handler<any, any>;
97+
98+
if (useHandlerCache) {
99+
if (!this.handlers) {
100+
this.handlers = new WeakMap();
101+
}
102+
const handlers = this.handlers!;
103+
104+
if (handlers.has(command.constructor)) {
105+
handler = handlers.get(command.constructor)!;
106+
} else {
107+
handler = command.resolveMiddleware(this.middlewareStack as any, this.config, options);
108+
handlers.set(command.constructor, handler);
109+
}
110+
} else {
111+
delete this.handlers;
112+
handler = command.resolveMiddleware(this.middlewareStack as any, this.config, options);
113+
}
114+
73115
if (callback) {
74116
handler(command)
75117
.then(
@@ -87,6 +129,7 @@ export class Client<
87129
}
88130

89131
destroy() {
90-
if (this.config.requestHandler.destroy) this.config.requestHandler.destroy();
132+
this.config?.requestHandler?.destroy?.();
133+
delete this.handlers;
91134
}
92135
}

0 commit comments

Comments
 (0)