Skip to content

Commit

Permalink
feat(frameworks): added lazy framework to enable users to create inst…
Browse files Browse the repository at this point in the history
…ance of the app asynchronously
  • Loading branch information
H4ad committed May 26, 2022
1 parent faa808d commit be61c22
Show file tree
Hide file tree
Showing 4 changed files with 213 additions and 0 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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:

Expand Down
Empty file added src/frameworks/lazy/index.ts
Empty file.
105 changes: 105 additions & 0 deletions src/frameworks/lazy/lazy.framework.ts
Original file line number Diff line number Diff line change
@@ -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<TApp> implements FrameworkContract<null> {
//#region Constructor

/**
* Default Constructor
*/
constructor(
protected readonly framework: FrameworkContract<TApp>,
protected readonly factory: () => Promise<TApp>,
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<void>;

//#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
}
101 changes: 101 additions & 0 deletions test/frameworks/lazy.framework.spec.ts
Original file line number Diff line number Diff line change
@@ -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,');
});
});

0 comments on commit be61c22

Please sign in to comment.