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 Sep 3, 2024
1 parent c8c53ae commit 659e4f7
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 9 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
24 changes: 24 additions & 0 deletions packages/smithy-client/src/client.spec.ts
Original file line number Diff line number Diff line change
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("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(privateAccess()).toBeUndefined();
});
});
});
46 changes: 40 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 Down Expand Up @@ -44,11 +45,21 @@ 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: () => {
delete this.handlers;
},
});
/**
* May be used to cache the resolved handler function for a Command class.
*/
private handlers?: WeakMap<Function, Handler<any, any>> | undefined;
private configRef?: ResolvedClientConfiguration | undefined;

constructor(public readonly config: ResolvedClientConfiguration) {
this.configRef = this.config;
}

send<InputType extends ClientInput, OutputType extends ClientOutput>(
command: Command<ClientInput, InputType, ClientOutput, OutputType, SmithyResolvedConfiguration<HandlerOptions>>,
options?: HandlerOptions
Expand All @@ -69,7 +80,29 @@ 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 === this.configRef;

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);
this.configRef = this.config;
}

if (callback) {
handler(command)
.then(
Expand All @@ -87,6 +120,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 659e4f7

Please sign in to comment.