From be61c222d24c0cc35382b6aebb553d5092a535f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Louren=C3=A7o?= Date: Thu, 26 May 2022 15:05:30 -0300 Subject: [PATCH] feat(frameworks): added lazy framework to enable users to create instance of the app asynchronously --- README.md | 7 ++ src/frameworks/lazy/index.ts | 0 src/frameworks/lazy/lazy.framework.ts | 105 +++++++++++++++++++++++++ test/frameworks/lazy.framework.spec.ts | 101 ++++++++++++++++++++++++ 4 files changed, 213 insertions(+) create mode 100644 src/frameworks/lazy/index.ts create mode 100644 src/frameworks/lazy/lazy.framework.ts create mode 100644 test/frameworks/lazy.framework.spec.ts diff --git a/README.md b/README.md index 76792b4e..a68beb1c 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,10 @@ import { DefaultHandler } from '@h4ad/serverless-adapter/lib/handlers/default'; import { PromiseResolver } from '@h4ad/serverless-adapter/lib/resolvers/promise'; import app from './app'; +// if you create your app asynchronously +// check the docs about the LazyFramework. +// ServerlessAdapter.new(null) +// .setFramework(new LazyFramework(new ExpressFramework(), async () => createAsyncApp())) export const handler = ServerlessAdapter.new(app) .setFramework(new ExpressFramework()) .setHandler(new DefaultHandler()) @@ -119,6 +123,9 @@ Currently, we support these frameworks: - [Fastify](https://www.fastify.io/) by using ([FastifyFramework](src/frameworks/fastify/fastify.framework.ts)) - [Hapi](https://hapi.dev/) by using ([HapiFramework](src/frameworks/hapi/hapi.framework.ts)) - [Koa](https://koajs.com/) by using ([KoaFramework](src/frameworks/koa/koa.framework.ts)) +- Async Initialization by using ([LazyFramework](src/frameworks/lazy/lazy.framework.ts)) + - Use this framework to provide a way to create the instance of your app asynchronously. + - With him, you can create an instance of Express or Fastify asynchronously, [see the docs](src/frameworks/lazy/lazy.framework.ts). We support these event sources: diff --git a/src/frameworks/lazy/index.ts b/src/frameworks/lazy/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/frameworks/lazy/lazy.framework.ts b/src/frameworks/lazy/lazy.framework.ts new file mode 100644 index 00000000..d14863a2 --- /dev/null +++ b/src/frameworks/lazy/lazy.framework.ts @@ -0,0 +1,105 @@ +//#region Imports + +import { IncomingMessage, ServerResponse } from 'http'; +import { FrameworkContract } from '../../contracts'; +import { ILogger, createDefaultLogger } from '../../core'; + +//#endregion + +/** + * The framework that asynchronously instantiates your application and forwards the request to the framework as quickly as possible. + * + * @example```typescript + * import express from 'express'; + * import { ServerlessAdapter } from '@h4ad/serverless-adapter'; + * import { ExpressFramework } from '@h4ad/serverless-adapter/lib/frameworks/express'; + * import { LazyFramework } from '@h4ad/serverless-adapter/lib/frameworks/lazy'; + * + * const expressFramework = new ExpressFramework(); + * const factory = async () => { + * const app = express(); + * + * // do some asynchronous stuffs like create the database; + * await new Promise(resolve => setTimeout(resolve, 100); + * + * return app; + * }; + * const framework = new LazyFramework(expressFramework, factory); + * + * export const handler = ServerlessAdapter.new(null) + * .setFramework(framework) + * // set other configurations and then build + * .build(); + * ``` + */ +export class LazyFramework implements FrameworkContract { + //#region Constructor + + /** + * Default Constructor + */ + constructor( + protected readonly framework: FrameworkContract, + protected readonly factory: () => Promise, + protected readonly logger: ILogger = createDefaultLogger(), + ) { + this.delayedFactory = Promise.resolve() + .then(() => factory()) + .then(app => { + this.cachedApp = app; + }) + .catch((error: Error) => { + // deal with the error only when receive some request + // to be able to return some message to user + this.logger.error( + 'SERVERLESS_ADAPTER:LAZY_FRAMEWORK: An error occours during the creation of your app.', + ); + this.logger.error(error); + }); + } + + //#endregion + + //#region Protected Properties + + /** + * The cached version of the app + */ + protected cachedApp?: TApp; + + /** + * The delayed factory to create an instance of the app + */ + protected readonly delayedFactory: Promise; + + //#endregion + + //#region Public Methods + + /** + * @inheritDoc + */ + public sendRequest( + app: null, + request: IncomingMessage, + response: ServerResponse, + ): void { + if (this.cachedApp) + return this.framework.sendRequest(this.cachedApp, request, response); + + this.delayedFactory.then(() => { + if (!this.cachedApp) { + return response.emit( + 'error', + new Error( + 'SERVERLESS_ADAPTER:LAZY_FRAMEWORK: The instance of the app returned by the factory is not valid, see the logs to learn more.', + ), + ); + } + + return this.framework.sendRequest(this.cachedApp, request, response); + }); + } + + //#endregion +} diff --git a/test/frameworks/lazy.framework.spec.ts b/test/frameworks/lazy.framework.spec.ts new file mode 100644 index 00000000..cdc1bd01 --- /dev/null +++ b/test/frameworks/lazy.framework.spec.ts @@ -0,0 +1,101 @@ +import { + ILogger, + ServerlessRequest, + ServerlessResponse, + waitForStreamComplete, +} from '../../src'; +import { LazyFramework } from '../../src/frameworks/lazy/lazy.framework'; +import { FrameworkMock } from '../mocks/framework.mock'; + +describe(LazyFramework.name, () => { + it('should can lazy create an instance of any app and return the cached version', async () => { + const appInstance = Symbol('Your app'); + + const mockFramework = new FrameworkMock(200, { + data: true, + }); + + // eslint-disable-next-line @typescript-eslint/unbound-method + mockFramework.sendRequest = jest.fn(mockFramework.sendRequest); + + const factory = jest.fn( + () => new Promise(resolve => setTimeout(() => resolve(appInstance), 100)), + ); + + const framework = new LazyFramework(mockFramework, factory); + + const firstRequest = new ServerlessRequest({ + method: 'POST', + headers: {}, + url: '/users', + }); + const firstResponse = new ServerlessResponse({ + method: 'POST', + }); + + framework.sendRequest(null, firstRequest, firstResponse); + + await waitForStreamComplete(firstResponse); + + expect(framework['factory']).toHaveBeenCalledTimes(1); + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(mockFramework.sendRequest).toHaveBeenLastCalledWith( + appInstance, + firstRequest, + firstResponse, + ); + + const secondRequest = new ServerlessRequest({ + method: 'GET', + headers: {}, + url: '/users', + }); + const secondResponse = new ServerlessResponse({ + method: 'GET', + }); + + framework.sendRequest(null, secondRequest, secondResponse); + + await waitForStreamComplete(secondResponse); + + expect(factory).toHaveBeenCalledTimes(1); + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(mockFramework.sendRequest).toHaveBeenCalledTimes(2); + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(mockFramework.sendRequest).toHaveBeenLastCalledWith( + appInstance, + secondRequest, + secondResponse, + ); + }); + + it('should throw error if error occours in factory function', async () => { + const mockFramework = new FrameworkMock(200, { + data: true, + }); + + const mockLogger = { error: jest.fn() } as unknown as ILogger; + const error = new Error('Something Wrong Occours'); + + const framework = new LazyFramework( + mockFramework, + () => Promise.reject(error), + mockLogger, + ); + + const request = new ServerlessRequest({ + method: 'GET', + headers: {}, + url: '/users', + }); + const response = new ServerlessResponse({ + method: 'GET', + }); + + framework.sendRequest(null, request, response); + + await expect( + async () => await waitForStreamComplete(response), + ).rejects.toThrowError('factory is not valid,'); + }); +});