Skip to content

Commit

Permalink
minor perf improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
Inqnuam committed May 19, 2023
1 parent b95edd0 commit 0583edf
Show file tree
Hide file tree
Showing 18 changed files with 200 additions and 162 deletions.
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
[![NPM](https://nodei.co/npm/serverless-aws-lambda.png?compact=true)](https://nodei.co/npm/serverless-aws-lambda/)

## Description

> AWS Lambda dev tool for Serverless. Allows Express synthax in handlers. Supports packaging, local invoking and offline Application Load Balancer and API Gateway lambda server mocking.
Expand Down Expand Up @@ -48,7 +50,7 @@ This will overwrite serverless.yml custom > serverless-aws-lambda values if they

### Invoke

Offline server supports Application Load Balancer and API Gateway endponts.
Offline server supports Application Load Balancer and API Gateway and Function URL endpoints (see [plugins](#plugins) for more triggers).
Appropriate `event` object is sent to the handler based on your lambda declaration.

```yaml
Expand All @@ -71,19 +73,18 @@ However if your declare both `alb` and `http` or `httpApi` inside a single lambd
- header with `X-Mock-Type`.
- or in query string with `x_mock_type`.

Please note that invoking a lambda from sls CLI (`sls invoke local -f myFunction`) will not trigger the offline server. But you are still able to inject any event with `-d 'someData'` sls CLI option.
Please note that invoking a lambda from sls CLI (`sls invoke local -f myFunction`) will not trigger the offline server. But will still make your handler ready to be invoked.

You can also invoke your Lambdas with a custom `event` object by making a POST request to:
http://localhost:3000/@invoke/myAwsomeLambda
for `aws-sdk` Lambda client compatibility it is also possible to request to:
http://localhost:3000/2015-03-31/functions/myAwsomeLambda/invocations
To invoke your Lambda like with AWS Console's `Test` button prefix your Lambda name by `@invoke/`.
Example:
http://localhost:3000/@invoke/myAwsomeLambda

Example with with `aws-sdk` Lambda Client:

```js
const { LambdaClient, InvokeCommand } = require("@aws-sdk/client-lambda");
const client = new LambdaClient({ region: "PARADISE", endpoint: "http://localhost:3000" });
const client = new LambdaClient({ region: "us-east-1", endpoint: "http://localhost:3000" });
const DryRun = "DryRun";
const Event = "Event";
const RequestResponse = "RequestResponse";
Expand Down
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "serverless-aws-lambda",
"version": "4.5.4",
"version": "4.5.5",
"description": "AWS Application Load Balancer and API Gateway - Lambda dev tool for Serverless. Allows Express synthax in handlers. Supports packaging, local invoking and offline ALB, APG, S3, SNS, SQS, DynamoDB Stream server mocking.",
"author": "Inqnuam",
"license": "MIT",
Expand All @@ -9,6 +9,9 @@
"type": "git",
"url": "git+https://github.com/inqnuam/serverless-aws-lambda.git"
},
"bugs": {
"url": "https://github.com/inqnuam/serverless-aws-lambda/issues"
},
"main": "./dist/index.js",
"typings": "./dist/config.d.ts",
"exports": {
Expand Down Expand Up @@ -83,7 +86,9 @@
"bundle",
"esbuild",
"nodejs",
"node"
"node",
"python",
"ruby"
],
"scripts": {
"dev": "DEV=true node ./build.mjs",
Expand Down
16 changes: 15 additions & 1 deletion src/defineConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,17 @@ export type ILambda = {
};
/**
* Be notified when this lambda is invoked.
*
* Can be used to edit (local) input payload before invokation.
*/
onInvoke: (callback: (event: any, info?: any) => void) => void | { [key: string]: any };
/**
* Called when handler throws an error.
*/
onInvoke: (callback: (event: any, info?: any) => void) => void;
onInvokeError: (callback: (input: any, error: any, info?: any) => void) => void;
/**
* Called when handler returns successfully.
*/
onInvokeSuccess: (callback: (input: any, output: any, info?: any) => void) => void;
} & Omit<ILambdaMock, "invokeSub" | "invokeSuccessSub" | "invokeErrorSub" | "runner">;

Expand Down Expand Up @@ -50,12 +58,18 @@ export interface OfflineRequest {
* @default "ANY"
*/
method?: HttpMethod | HttpMethod[];
/**
* Filter for request path.
*/
filter: string | RegExp;
callback: (this: ClientConfigParams, req: IncomingMessage, res: ServerResponse) => Promise<any | void> | any | void;
}

export interface SlsAwsLambdaPlugin {
name: string;
/**
* Share any data with other plugins
*/
pluginData?: any;
buildCallback?: (this: ClientConfigParams, result: BuildResult, isRebuild: boolean) => Promise<void> | void;
onInit?: (this: ClientConfigParams) => Promise<void> | void;
Expand Down
58 changes: 31 additions & 27 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ class ServerlessAwsLambda extends Daemon {
properties: {
virtualEnvs: { type: "object" },
online: { anyOf: [{ type: "array", items: { type: "string" } }, { type: "boolean" }, { type: "string" }] },
files: { type: "array" },
files: { type: "array", items: { type: "string" } },
},
});

Expand All @@ -98,7 +98,7 @@ class ServerlessAwsLambda extends Daemon {
lifecycleEvents: ["run"],
options: {
port: {
usage: "Specify the server port (default: 3000)",
usage: "Specify the server port (default: random free port)",
shortcut: "p",
required: false,
type: "number",
Expand All @@ -120,13 +120,15 @@ class ServerlessAwsLambda extends Daemon {
"before:deploy:function:packageFunction": this.init.bind(this, true),
"before:invoke:local:invoke": this.invokeLocal.bind(this),
"after:aws:deploy:finalize:cleanup": this.afterDeploy.bind(this),
"after:invoke:local:invoke": process.exit,
};

this.resources = getResources(this.serverless);
}

async invokeLocal() {
this.invokeName = this.options.function;
this.watch = false;
await this.init(false);
}

Expand Down Expand Up @@ -227,7 +229,7 @@ class ServerlessAwsLambda extends Daemon {
const slsDeclaration = this.serverless.service.getFunction(this.invokeName);
const foundLambda = this.#lambdas.find((x) => x.name == this.invokeName);

if (foundLambda) {
if (foundLambda && foundLambda.runtime.startsWith("n")) {
(slsDeclaration as Serverless.FunctionDefinitionHandler).handler = foundLambda.esOutputPath.replace(`${cwd}/`, "").replace(".js", `.${foundLambda.handlerName}`);
}
} else if (this.isDeploying || this.isPackaging) {
Expand All @@ -242,29 +244,31 @@ class ServerlessAwsLambda extends Daemon {
}

await Promise.all(
packageLambdas.map(async (l) => {
const slsDeclaration = this.serverless.service.getFunction(l.name) as Serverless.FunctionDefinitionHandler;

if (typeof slsDeclaration.package?.artifact == "string") {
return;
}

// @ts-ignore
const filesToInclude = slsDeclaration.files;
const zipableBundledFilePath = l.esOutputPath.slice(0, -3);
const zipOptions: IZipOptions = {
filePath: zipableBundledFilePath,
zipName: l.outName,
include: filesToInclude,
sourcemap: this.esBuildConfig.sourcemap,
format,
};
const zipOutputPath = await zip(zipOptions);

// @ts-ignore
slsDeclaration.package = { ...slsDeclaration.package, disable: true, artifact: zipOutputPath };
slsDeclaration.handler = path.basename(l.handlerPath);
})
packageLambdas
.filter((x) => x.runtime.startsWith("n"))
.map(async (l) => {
const slsDeclaration = this.serverless.service.getFunction(l.name) as Serverless.FunctionDefinitionHandler;

if (typeof slsDeclaration.package?.artifact == "string") {
return;
}

// @ts-ignore
const filesToInclude = slsDeclaration.files;
const zipableBundledFilePath = l.esOutputPath.slice(0, -3);
const zipOptions: IZipOptions = {
filePath: zipableBundledFilePath,
zipName: l.outName,
include: filesToInclude,
sourcemap: this.esBuildConfig.sourcemap,
format,
};
const zipOutputPath = await zip(zipOptions);

// @ts-ignore
slsDeclaration.package = { ...slsDeclaration.package, disable: true, artifact: zipOutputPath };
slsDeclaration.handler = path.basename(l.handlerPath);
})
);
} else {
this.listen(ServerlessAwsLambda.PORT, async (port: number, localIp: string) => {
Expand Down Expand Up @@ -492,7 +496,7 @@ class ServerlessAwsLambda extends Daemon {
}
async #setCustomEsBuildConfig() {
const customConfigArgs = {
stop: async (cb: (err?: any) => void) => {
stop: async (cb?: (err?: any) => void) => {
if (this.buildContext.stop) {
await this.buildContext.stop();
}
Expand Down
7 changes: 3 additions & 4 deletions src/lib/runtime/bufferedStreamResponse.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CommonEventGenerator } from "../../plugins/lambda/events/common";
import type { LambdaEndpoint } from "../parseEvents/endpoints";
import { log } from "../utils/colorize";

Expand All @@ -11,8 +12,6 @@ export class BufferedStreamResponse {
_ct?: any;
_isStreamResponse = true;
#mockEvent?: LambdaEndpoint;
static httpErrMsg = '{"message":"Internal Server Error"}';
static amzMsgNull = '{"message":null}';
static codec = new TextDecoder();
static splitMessage = (buffer?: Uint8Array, metaDelimiter?: number) => {
if (!buffer) {
Expand Down Expand Up @@ -101,9 +100,9 @@ export class BufferedStreamResponse {
return {
statusCode: 500,
headers: {
"Content-Type": "application/json",
"Content-Type": CommonEventGenerator.contentType.json,
},
body: BufferedStreamResponse.httpErrMsg,
body: CommonEventGenerator.httpErrMsg,
};
} else if (this.buffer) {
const responseData = this.#parseResponseData(this.buffer);
Expand Down
14 changes: 10 additions & 4 deletions src/lib/runtime/rapidApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type { IKinesisEvent } from "../parseEvents/kinesis";
import type { IDocumentDbEvent } from "../parseEvents/documentDb";
import type { Runner } from "./runners/index";

type InvokeSub = (event: any, info?: any) => void;
type InvokeSub = (event: any, info?: any) => void | { [key: string]: any };
type InvokeSuccessSub = (input: any, output: any, info?: any) => void;
type InvokeErrorSub = InvokeSuccessSub;
export interface ILambdaMock {
Expand All @@ -28,10 +28,10 @@ export interface ILambdaMock {
* Deploy Lambda or not to AWS.
*/
online: boolean | string | string[];
runtime: string;
/**
* API Gateway and Application Load balancer events.
*/
runtime: string;
endpoints: LambdaEndpoint[];
s3: IS3Event[];
sns: ISnsEvent[];
Expand Down Expand Up @@ -235,13 +235,19 @@ export class LambdaMock implements ILambdaMock {
}

const awsRequestId = randomUUID();
const hrTimeStart = this.#printStart(awsRequestId, event, info);

// forEach is used to not slow down invokation with async subsribers
this.invokeSub.forEach((x) => {
try {
x(event, info);
const editedEvent = x(event, info);
if (editedEvent && typeof editedEvent.then != "function") {
event = editedEvent;
}
} catch (error) {}
});

const hrTimeStart = this.#printStart(awsRequestId, event, info);

try {
const eventResponse = await new Promise(async (resolve, reject) => {
let tm: NodeJS.Timeout | undefined;
Expand Down
8 changes: 5 additions & 3 deletions src/lib/runtime/runners/node/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ interface LambdaErrorResponse {
trace?: string[];
}

const root = process.cwd().split(path.sep).filter(Boolean)[0];
const cwd = process.cwd();
const router = `/dist/lambda/router.`;
const root = cwd.split(path.sep).filter(Boolean)[0];
const stackRegex = new RegExp(`\\((\/${root}|${root}).*(\\d+):(\\d+)`);
const orgCT = clearTimeout.bind(clearTimeout);
const { AWS_LAMBDA_FUNCTION_NAME } = process.env;
Expand Down Expand Up @@ -92,7 +94,7 @@ export const formatStack = (_stack: string[]) => {
_stack
.slice()
.reverse()
.filter((s: string) => s && !s.includes(__dirname))
.filter((s: string) => s && !s.includes(__dirname) && !s.includes(router))
.forEach((x) => {
const line = stackRegex.exec(x)?.[0];
if (line) {
Expand Down Expand Up @@ -136,7 +138,7 @@ export class EventQueue extends Map {
return fPath.slice(1);
}
})
.filter((s: string) => s && !s.includes(__dirname));
.filter((s: string) => s && !s.includes(__dirname) && !s.includes(router));

if (x.stack.length) {
delete x.start;
Expand Down
4 changes: 4 additions & 0 deletions src/lib/runtime/runners/ruby/index.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ def log(message)
handler_method, handler_class = handler_name.split('.').reverse
handler_class ||= "Kernel"

cwd = Dir.pwd
watchFiles = JSON.generate($LOADED_FEATURES.select { |s| s.start_with?(cwd) })
puts "__|watch|__#{watchFiles}"
$stdout.flush

input = ""
while (line = STDIN.gets)
Expand Down
Loading

0 comments on commit 0583edf

Please sign in to comment.