diff --git a/src/lambda/LambdaFunction.js b/src/lambda/LambdaFunction.js index 212d3c143..f729c3e25 100644 --- a/src/lambda/LambdaFunction.js +++ b/src/lambda/LambdaFunction.js @@ -57,8 +57,7 @@ export default class LambdaFunction { const _servicePath = resolve(servicePath, options.location || '') const { handler, name, package: functionPackage = {} } = functionDefinition - const [handlerPath, handlerName, handlerModuleNesting] = - splitHandlerPathAndName(handler) + const [handlerPath, handlerName] = splitHandlerPathAndName(handler) const memorySize = functionDefinition.memorySize || @@ -114,7 +113,6 @@ export default class LambdaFunction { functionKey, handler, handlerName, - handlerModuleNesting, codeDir: this.#codeDir, handlerPath: resolve(this.#codeDir, handlerPath), runtime, diff --git a/src/lambda/__tests__/fixtures/lambdaFunction.fixture.js b/src/lambda/__tests__/fixtures/lambdaFunction.fixture.js new file mode 100644 index 000000000..ade831d59 --- /dev/null +++ b/src/lambda/__tests__/fixtures/lambdaFunction.fixture.js @@ -0,0 +1,145 @@ +'use strict' + +exports.contextDoneHandler = function contextDoneHandler(event, context) { + context.done(null, 'foo') +} + +exports.contextDoneHandlerDeferred = function contextDoneHandlerDeferred( + event, + context, +) { + setTimeout(() => context.done(null, 'foo'), 100) +} + +exports.contextSucceedHandler = function contextSucceedHandler(event, context) { + context.succeed('foo') +} + +exports.contextSucceedHandlerDeferred = function contextSucceedHandlerDeferred( + event, + context, +) { + setTimeout(() => context.succeed('foo'), 100) +} + +exports.callbackHandler = function callbackHandler(event, context, callback) { + callback(null, 'foo') +} + +exports.callbackHandlerDeferred = function callbackHandlerDeferred( + event, + context, + callback, +) { + setTimeout(() => callback(null, 'foo'), 100) +} + +exports.promiseHandler = function promiseHandler() { + return Promise.resolve('foo') +} + +exports.promiseHandlerDeferred = function promiseDeferred() { + return new Promise((resolve) => { + setTimeout(() => resolve('foo'), 100) + }) +} + +exports.asyncFunctionHandler = async function asyncFunctionHandler() { + return 'foo' +} + +exports.asyncFunctionHandlerObject = async function asyncFunctionHandler() { + return { + foo: 'bar', + } +} + +// we deliberately test the case where a 'callback' is defined +// in the handler, but a promise is being returned to protect from a +// potential naive implementation, e.g. +// +// const { promisify } = 'utils' +// const promisifiedHandler = handler.length === 3 ? promisify(handler) : handler +// +// if someone would return a promise, but also defines callback, without using it +// the handler would not be returning anything +exports.promiseWithDefinedCallbackHandler = + function promiseWithDefinedCallbackHandler( + event, // eslint-disable-line no-unused-vars + context, // eslint-disable-line no-unused-vars + callback, // eslint-disable-line no-unused-vars + ) { + return Promise.resolve('Hello Promise!') + } + +exports.contextSucceedWithContextDoneHandler = + function contextSucceedWithContextDoneHandler(event, context) { + context.succeed('Hello Context.succeed!') + + context.done(null, 'Hello Context.done!') + } + +exports.callbackWithContextDoneHandler = + function callbackWithContextDoneHandler(event, context, callback) { + callback(null, 'Hello Callback!') + + context.done(null, 'Hello Context.done!') + } + +exports.callbackWithPromiseHandler = function callbackWithPromiseHandler( + event, + context, + callback, +) { + callback(null, 'Hello Callback!') + + return Promise.resolve('Hello Promise!') +} + +exports.callbackInsidePromiseHandler = function callbackInsidePromiseHandler( + event, + context, + callback, +) { + return new Promise((resolve) => { + callback(null, 'Hello Callback!') + + resolve('Hello Promise!') + }) +} + +exports.requestIdHandler = async function requestIdHandler(event, context) { + return context.awsRequestId +} + +exports.remainingExecutionTimeHandler = + async function remainingExecutionTimeHandler(event, context) { + const first = context.getRemainingTimeInMillis() + + await new Promise((resolve) => { + setTimeout(resolve, 100) + }) + + const second = context.getRemainingTimeInMillis() + + await new Promise((resolve) => { + setTimeout(resolve, 200) + }) + + const third = context.getRemainingTimeInMillis() + + return [first, second, third] + } + +exports.defaultTimeoutHandler = async function defaultTimeoutHandler( + event, + context, +) { + return context.getRemainingTimeInMillis() +} + +exports.executionTimeInMillisHandler = function executionTimeInMillisHandler() { + return new Promise((resolve) => { + setTimeout(resolve, 100) + }) +} diff --git a/src/lambda/__tests__/fixtures/lambdaFunction.js b/src/lambda/__tests__/fixtures/lambdaFunction.js deleted file mode 100644 index 8cb7ff11e..000000000 --- a/src/lambda/__tests__/fixtures/lambdaFunction.js +++ /dev/null @@ -1,123 +0,0 @@ -'use strict' - -exports.fixture = { - contextDoneHandler(event, context) { - context.done(null, 'foo') - }, - - contextDoneHandlerDeferred(event, context) { - setTimeout(() => context.done(null, 'foo'), 100) - }, - - contextSucceedHandler(event, context) { - context.succeed('foo') - }, - - contextSucceedHandlerDeferred(event, context) { - setTimeout(() => context.succeed('foo'), 100) - }, - - callbackHandler(event, context, callback) { - callback(null, 'foo') - }, - - callbackHandlerDeferred(event, context, callback) { - setTimeout(() => callback(null, 'foo'), 100) - }, - - promiseHandler() { - return Promise.resolve('foo') - }, - - promiseHandlerDeferred() { - return new Promise((resolve) => { - setTimeout(() => resolve('foo'), 100) - }) - }, - - async asyncFunctionHandler() { - return 'foo' - }, - - async asyncFunctionHandlerObject() { - return { - foo: 'bar', - } - }, - - // we deliberately test the case where a 'callback' is defined - // in the handler, but a promise is being returned to protect from a - // potential naive implementation, e.g. - // - // const { promisify } = 'utils' - // const promisifiedHandler = handler.length === 3 ? promisify(handler) : handler - // - // if someone would return a promise, but also defines callback, without using it - // the handler would not be returning anything - - promiseWithDefinedCallbackHandler( - event, // eslint-disable-line no-unused-vars - context, // eslint-disable-line no-unused-vars - callback, // eslint-disable-line no-unused-vars - ) { - return Promise.resolve('Hello Promise!') - }, - - contextSucceedWithContextDoneHandler(event, context) { - context.succeed('Hello Context.succeed!') - - context.done(null, 'Hello Context.done!') - }, - - callbackWithContextDoneHandler(event, context, callback) { - callback(null, 'Hello Callback!') - - context.done(null, 'Hello Context.done!') - }, - - callbackWithPromiseHandler(event, context, callback) { - callback(null, 'Hello Callback!') - - return Promise.resolve('Hello Promise!') - }, - - callbackInsidePromiseHandler(event, context, callback) { - return new Promise((resolve) => { - callback(null, 'Hello Callback!') - - resolve('Hello Promise!') - }) - }, - - async requestIdHandler(event, context) { - return context.awsRequestId - }, - - async remainingExecutionTimeHandler(event, context) { - const first = context.getRemainingTimeInMillis() - - await new Promise((resolve) => { - setTimeout(resolve, 100) - }) - - const second = context.getRemainingTimeInMillis() - - await new Promise((resolve) => { - setTimeout(resolve, 200) - }) - - const third = context.getRemainingTimeInMillis() - - return [first, second, third] - }, - - async defaultTimeoutHandler(event, context) { - return context.getRemainingTimeInMillis() - }, - - executionTimeInMillisHandler() { - return new Promise((resolve) => { - setTimeout(resolve, 100) - }) - }, -} diff --git a/src/lambda/handler-runner/HandlerRunner.js b/src/lambda/handler-runner/HandlerRunner.js index 99e8dbdb2..f9e9dd86d 100644 --- a/src/lambda/handler-runner/HandlerRunner.js +++ b/src/lambda/handler-runner/HandlerRunner.js @@ -30,14 +30,8 @@ export default class HandlerRunner { const { useDocker, useChildProcesses, useWorkerThreads, allowCache } = this.#options - const { - functionKey, - handlerName, - handlerPath, - handlerModuleNesting, - runtime, - timeout, - } = this.#funOptions + const { functionKey, handlerName, handlerPath, runtime, timeout } = + this.#funOptions if (this.log) { this.log.debug(`Loading handler... (${handlerPath})`) @@ -113,7 +107,6 @@ export default class HandlerRunner { functionKey, handlerPath, handlerName, - handlerModuleNesting, this.#env, timeout, allowCache, diff --git a/src/lambda/handler-runner/child-process-runner/ChildProcessRunner.js b/src/lambda/handler-runner/child-process-runner/ChildProcessRunner.js index 94c1d276b..b76ef2083 100644 --- a/src/lambda/handler-runner/child-process-runner/ChildProcessRunner.js +++ b/src/lambda/handler-runner/child-process-runner/ChildProcessRunner.js @@ -8,18 +8,11 @@ export default class ChildProcessRunner { #functionKey = null #handlerName = null #handlerPath = null - #handlerModuleNesting = null #timeout = null #allowCache = false constructor(funOptions, env, allowCache, v3Utils) { - const { - functionKey, - handlerName, - handlerPath, - handlerModuleNesting, - timeout, - } = funOptions + const { functionKey, handlerName, handlerPath, timeout } = funOptions if (v3Utils) { this.log = v3Utils.log @@ -32,7 +25,6 @@ export default class ChildProcessRunner { this.#functionKey = functionKey this.#handlerName = handlerName this.#handlerPath = handlerPath - this.#handlerModuleNesting = handlerModuleNesting this.#timeout = timeout this.#allowCache = allowCache } @@ -44,12 +36,7 @@ export default class ChildProcessRunner { async run(event, context) { const childProcess = node( childProcessHelperPath, - [ - this.#functionKey, - this.#handlerName, - this.#handlerPath, - this.#handlerModuleNesting, - ], + [this.#functionKey, this.#handlerName, this.#handlerPath], { env: this.#env, stdio: 'inherit', diff --git a/src/lambda/handler-runner/in-process-runner/InProcessRunner.js b/src/lambda/handler-runner/in-process-runner/InProcessRunner.js index a60535589..a2c59131e 100644 --- a/src/lambda/handler-runner/in-process-runner/InProcessRunner.js +++ b/src/lambda/handler-runner/in-process-runner/InProcessRunner.js @@ -74,24 +74,14 @@ export default class InProcessRunner { #functionKey = null #handlerName = null #handlerPath = null - #handlerModuleNesting = null #timeout = null #allowCache = false - constructor( - functionKey, - handlerPath, - handlerName, - handlerModuleNesting, - env, - timeout, - allowCache, - ) { + constructor(functionKey, handlerPath, handlerName, env, timeout, allowCache) { this.#env = env this.#functionKey = functionKey this.#handlerName = handlerName this.#handlerPath = handlerPath - this.#handlerModuleNesting = handlerModuleNesting this.#timeout = timeout this.#allowCache = allowCache } @@ -120,24 +110,7 @@ export default class InProcessRunner { if (!this.#allowCache) { clearModule(this.#handlerPath, { cleanup: true }) } - - let handler - try { - const handlerPathExport = await import(this.#handlerPath) - // this supports handling of nested handler paths like /.object1.object2.object3.handler - // a use case for this, is when the handler is further down the export tree or in nested objects - // NOTE: this feature is supported in AWS Lambda - handler = this.#handlerModuleNesting.reduce( - (obj, key) => obj[key], - handlerPathExport, - ) - } catch (error) { - throw new Error( - `offline: one of the module nesting ${ - this.#handlerModuleNesting - } for handler ${this.#handlerName} is undefined or not exported`, - ) - } + const { [this.#handlerName]: handler } = await import(this.#handlerPath) if (typeof handler !== 'function') { throw new Error( diff --git a/src/lambda/handler-runner/worker-thread-runner/WorkerThreadRunner.js b/src/lambda/handler-runner/worker-thread-runner/WorkerThreadRunner.js index 98129f4be..f012cd852 100644 --- a/src/lambda/handler-runner/worker-thread-runner/WorkerThreadRunner.js +++ b/src/lambda/handler-runner/worker-thread-runner/WorkerThreadRunner.js @@ -10,13 +10,7 @@ export default class WorkerThreadRunner { constructor(funOptions /* options */, env, allowCache) { // this._options = options - const { - functionKey, - handlerName, - handlerPath, - handlerModuleNesting, - timeout, - } = funOptions + const { functionKey, handlerName, handlerPath, timeout } = funOptions this.#allowCache = allowCache this.#workerThread = new Worker(workerThreadHelperPath, { @@ -26,7 +20,6 @@ export default class WorkerThreadRunner { functionKey, handlerName, handlerPath, - handlerModuleNesting, timeout, }, }) diff --git a/src/lambda/handler-runner/worker-thread-runner/workerThreadHelper.js b/src/lambda/handler-runner/worker-thread-runner/workerThreadHelper.js index d1bd851e8..dd7f55390 100644 --- a/src/lambda/handler-runner/worker-thread-runner/workerThreadHelper.js +++ b/src/lambda/handler-runner/worker-thread-runner/workerThreadHelper.js @@ -2,8 +2,7 @@ import { env } from 'process' import { parentPort, workerData } from 'worker_threads' // eslint-disable-line import/no-unresolved import InProcessRunner from '../in-process-runner/index.js' -const { functionKey, handlerName, handlerPath, handlerModuleNesting } = - workerData +const { functionKey, handlerName, handlerPath } = workerData parentPort.on('message', async (messageData) => { const { context, event, port, timeout, allowCache } = messageData @@ -13,7 +12,6 @@ parentPort.on('message', async (messageData) => { functionKey, handlerPath, handlerName, - handlerModuleNesting, env, timeout, allowCache, diff --git a/src/utils/__tests__/splitHandlerPathAndName.test.js b/src/utils/__tests__/splitHandlerPathAndName.test.js index 437682805..ace947991 100644 --- a/src/utils/__tests__/splitHandlerPathAndName.test.js +++ b/src/utils/__tests__/splitHandlerPathAndName.test.js @@ -28,33 +28,19 @@ const tests = [ }, { description: 'ruby handler from kernel', - expected: ['./src/somefolder/function', 'handler', ['handler']], + expected: ['./src/somefolder/function', 'handler'], handler: './src/somefolder/function.handler', }, { description: 'generic handler', - expected: ['./src/somefolder/.handlers/handler', 'run', ['run']], + expected: ['./src/somefolder/.handlers/handler', 'run'], handler: './src/somefolder/.handlers/handler.run', }, { description: 'generic handler, unnested', - expected: ['handler', 'run', ['run']], + expected: ['handler', 'run'], handler: 'handler.run', }, - { - description: 'generic handler with module nesting', - expected: [ - './src/somefolder/.handlers/handler', - 'run', - ['layer1', 'layer2', 'run'], - ], - handler: './src/somefolder/.handlers/handler.layer1.layer2.run', - }, - { - description: 'generic handler, unnested with module nesting', - expected: ['handler', 'run', ['layer1', 'layer2', 'run']], - handler: 'handler.layer1.layer2.run', - }, ] describe('splitHandlerPathAndName', () => { diff --git a/src/utils/splitHandlerPathAndName.js b/src/utils/splitHandlerPathAndName.js index e6d4b4a64..413a2df59 100644 --- a/src/utils/splitHandlerPathAndName.js +++ b/src/utils/splitHandlerPathAndName.js @@ -1,10 +1,6 @@ // some-folder/src.index => some-folder/src export default function splitHandlerPathAndName(handler) { // Split handler into method name and path i.e. handler.run - const prepathDelimiter = handler.lastIndexOf('/') - const prepath = handler.substr(0, prepathDelimiter + 1) // include '/' for path - const postpath = handler.substr(prepathDelimiter + 1) - // Support Ruby paths with namespace resolution operators e.g. // ./src/somefolder/source.LambdaFunctions::Handler.process // prepath: ./src/somefolder/ @@ -13,6 +9,9 @@ export default function splitHandlerPathAndName(handler) { // path: ./src/somefolder/source // name: LambdaFunctions::Handler.process if (handler.match(/::/)) { + const prepathDelimiter = handler.lastIndexOf('/') + const prepath = handler.substr(0, prepathDelimiter + 1) // include '/' for path + const postpath = handler.substr(prepathDelimiter + 1) const nameDelimiter = postpath.indexOf('.') const filename = postpath.substr(0, nameDelimiter) const path = prepath + filename @@ -24,13 +23,9 @@ export default function splitHandlerPathAndName(handler) { // Support nested paths i.e. ./src/somefolder/.handlers/handler.run // path: ./src/somefoler/.handlers/handler // name: run - const [filename, ...moduleNesting] = postpath.split('.') - const [name] = moduleNesting.slice(-1) - const path = prepath + filename + const delimiter = handler.lastIndexOf('.') + const path = handler.substr(0, delimiter) + const name = handler.substr(delimiter + 1) - // module nesting has been added to support when the - // handler function is buried deep inside of a module - // e.g /src/somefoler/handlers/index.layer1.layer2.handler - // AWS supports this feature - return [path, name, moduleNesting] + return [path, name] }