Skip to content

Commit

Permalink
feat(smithy-client): add handler cache (#1383)
Browse files Browse the repository at this point in the history
* feat(smithy-client): add handler cache

* make handler cache configurable

* Update client.spec.ts
  • Loading branch information
kuhe authored Sep 9, 2024
1 parent 5510e83 commit d8df7bf
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 7 deletions.
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;
}
}

0 comments on commit d8df7bf

Please sign in to comment.