Skip to content

Commit

Permalink
Optimize async operations (#11)
Browse files Browse the repository at this point in the history
* Reduce the promise awaits

* Avoid explicit Promise construction

* Split processing agent implementations
  • Loading branch information
surol authored Jun 24, 2020
1 parent b631f9d commit 8d48d05
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 67 deletions.
121 changes: 75 additions & 46 deletions src/core/request-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export function requestProcessor<TMeans>(
*/
abstract class RequestProcessorAgent<TBase, TMeans extends TBase> {

abstract readonly context: Promise<RequestContext<TMeans>>;
abstract readonly context: RequestContext<TMeans>;

protected constructor(readonly config: RequestProcessor.Config<TBase>) {
}
Expand All @@ -90,15 +90,15 @@ abstract class RequestProcessorAgent<TBase, TMeans extends TBase> {
modification?: RequestModification<TMeans, TExt> | RequestModifier<TMeans, TExt>,
): Promise<boolean> {

let context: Promise<RequestContext<TMeans & TExt>>;
let context: RequestContext<TMeans & TExt>;

if (modification) {
context = new ModifiedRequestProcessorAgent<TBase, TMeans, TExt>(this, modification).context;
context = (await nextRequestProcessorAgent(this, modification)).context;
} else {
context = this.context as Promise<RequestContext<TMeans & TExt>>;
context = this.context as RequestContext<TMeans & TExt>;
}

return this.config.next(handler, await context);
return this.config.next(handler, context);
}

abstract modify<TExt>(
Expand All @@ -118,15 +118,15 @@ abstract class RequestProcessorAgent<TBase, TMeans extends TBase> {
*/
class RootRequestProcessorAgent<TMeans> extends RequestProcessorAgent<TMeans, TMeans> {

readonly context: Promise<RequestContext<TMeans>>;
readonly context: RequestContext<TMeans>;

constructor(config: RequestProcessor.Config<TMeans>, means: TMeans) {
super(config);
this.context = Promise.resolve({
this.context = {
...means,
next: this.next.bind(this),
modifiedBy: this.modifiedBy,
});
};
}

modifiedBy(): undefined {
Expand All @@ -145,10 +145,27 @@ class RootRequestProcessorAgent<TMeans> extends RequestProcessorAgent<TMeans, TM
/**
* @internal
*/
class ModifiedRequestProcessorAgent<TBase, TMeans extends TBase, TExt>
async function nextRequestProcessorAgent<TBase, TMeans extends TBase, TExt>(
prev: RequestProcessorAgent<TBase, TMeans>,
modification: RequestModification<TMeans, TExt> | RequestModifier<TMeans, TExt>,
): Promise<RequestProcessorAgent<TBase, TMeans & TExt>> {
if (isRequestModifier(modification)) {
return new ModifierRequestProcessingAgent(
prev,
modification,
await prev.modify(await modification.modification(prev.context)),
);
}
return new ModificationRequestProcessorAgent(prev, await prev.modify(modification));
}

/**
* @internal
*/
class ModificationRequestProcessorAgent<TBase, TMeans extends TBase, TExt>
extends RequestProcessorAgent<TBase, TMeans & TExt> {

readonly context: Promise<RequestContext<TMeans & TExt>>;
readonly context: RequestContext<TMeans & TExt>;

readonly modify: <TNext>(
this: void,
Expand All @@ -165,51 +182,63 @@ class ModifiedRequestProcessorAgent<TBase, TMeans extends TBase, TExt>
modification: RequestModification<TMeans, TExt> | RequestModifier<TMeans, TExt>,
) {
super(prev.config);
this.modify = prev.modify as this['modify'];
this.modifiedBy = prev.modifiedBy as this['modifiedBy'];
this.context = {
...prev.context,
...modification,
next: this.next.bind(this),
modifiedBy: this.modifiedBy,
} as RequestContext<TMeans & TExt>;
}

let modify = prev.modify as <TNext>(
this: void,
modification: RequestModification<TMeans & TExt, TNext>,
) => Promise<RequestModification<TMeans & TExt, TNext>>;

let modPromise: Promise<RequestModification<TMeans, TExt>>;
}

if (isRequestModifier(modification)) {
/**
* @internal
*/
class ModifierRequestProcessingAgent<TBase, TMeans extends TBase, TExt>
extends RequestProcessorAgent<TBase, TMeans & TExt> {

const modifier = modification;
readonly context: RequestContext<TMeans & TExt>;

modPromise = prev.context
.then(ctx => modifier.modification(ctx))
.then(mod => prev.modify(mod));
readonly modify: <TNext>(
this: void,
modification: RequestModification<TMeans & TExt, TNext>,
) => Promise<RequestModification<TMeans & TExt, TNext>>;

if (modifier.modifyNext) {
modify = async <TNext>(mod: RequestModification<TMeans & TExt, TNext>) => prev.modify(
modifier.modifyNext!(await this.context, mod) as RequestModification<TMeans, TNext>,
) as Promise<RequestModification<TMeans & TExt, TNext>>;
}
readonly modifiedBy: <TModifierInput, TModifierExt>(
this: void,
ref: RequestModifierRef<TModifierInput, TModifierExt>,
) => RequestContext<TMeans & TExt & TModifierInput & TModifierExt> | undefined;

this.modifiedBy = <TModifierInput, TModifierExt>(
ref: RequestModifierRef<TModifierInput, TModifierExt>,
) => (modifier[RequestModifier__symbol] === ref[RequestModifier__symbol]
? this.context
: prev.modifiedBy(ref)
) as RequestContext<TMeans & TExt & TModifierInput & TModifierExt> | undefined;
constructor(
prev: RequestProcessorAgent<TBase, TMeans>,
modifier: RequestModifier<TMeans, TExt>,
modification: RequestModification<TMeans, TExt> | RequestModifier<TMeans, TExt>,
) {
super(prev.config);

if (modifier.modifyNext) {
this.modify = async <TNext>(mod: RequestModification<TMeans & TExt, TNext>) => prev.modify(
modifier.modifyNext!(this.context, mod) as RequestModification<TMeans, TNext>,
) as Promise<RequestModification<TMeans & TExt, TNext>>;
} else {
modPromise = prev.modify(modification);
this.modifiedBy = prev.modifiedBy as <TModifierInput, TModifierExt>(
this: void,
ref: RequestModifierRef<TModifierInput, TModifierExt>,
) => RequestContext<TMeans & TExt & TModifierInput & TModifierExt> | undefined;
this.modify = prev.modify as this['modify'];
}

this.modify = modify;
this.context = Promise.all([prev.context, modPromise])
.then(([prevContext, mod]) => ({
...prevContext,
...mod,
next: this.next.bind(this),
modifiedBy: this.modifiedBy,
} as RequestContext<TMeans & TExt>));
this.modifiedBy = <TModifierInput, TModifierExt>(
ref: RequestModifierRef<TModifierInput, TModifierExt>,
) => (modifier[RequestModifier__symbol] === ref[RequestModifier__symbol]
? this.context
: prev.modifiedBy(ref)
) as RequestContext<TMeans & TExt & TModifierInput & TModifierExt> | undefined;

this.context = {
...prev.context,
...modification,
next: this.next.bind(this),
modifiedBy: this.modifiedBy,
} as RequestContext<TMeans & TExt>;
}

}
51 changes: 30 additions & 21 deletions src/http/http-listener.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ describe('httpListener', () => {
server.listener.mockReset();
});

let logErrorSpy: jest.SpyInstance;

beforeEach(() => {
logErrorSpy = jest.spyOn(suppressedLog, 'error');
});
afterEach(() => {
logErrorSpy.mockRestore();
});

it('invokes handler', async () => {

const handler = jest.fn(({ response }: RequestContext<HttpMeans>) => {
Expand Down Expand Up @@ -146,13 +155,14 @@ describe('httpListener', () => {
it('logs error and invokes provided error handler', async () => {

const error = new Error('test');
const log = suppressedLog;
const logErrorSpy = jest.spyOn(log, 'error');
const errorHandler = jest.fn(({ response, error }: RequestContext<ErrorMeans & HttpMeans>) => {
response.end(`ERROR ${error.message}`);
});

server.listener.mockImplementation(httpListener({ log, errorHandler }, () => { throw error; }));
server.listener.mockImplementation(httpListener(
{ log:suppressedLog, errorHandler },
() => { throw error; },
));

const response = await server.get('/test');

Expand All @@ -166,13 +176,14 @@ describe('httpListener', () => {
it('logs HTTP error and invokes provided error handler', async () => {

const error = new HttpError(404, { details: 'Never Found' });
const log = suppressedLog;
const logErrorSpy = jest.spyOn(log, 'error');
const errorHandler = jest.fn(({ response, error }: RequestContext<ErrorMeans & HttpMeans>) => {
response.end(`ERROR ${error.message} ${error.details}`);
});

server.listener.mockImplementation(httpListener({ log, errorHandler }, () => { throw error; }));
server.listener.mockImplementation(httpListener(
{ log: suppressedLog, errorHandler },
() => { throw error; },
));

const response = await server.get('/test');
const body = await response.body();
Expand All @@ -187,10 +198,11 @@ describe('httpListener', () => {
it('logs ERROR when there is no error handler', async () => {

const error = new Error('test');
const log = suppressedLog;
const logErrorSpy = jest.spyOn(log, 'error');

server.listener.mockImplementation(httpListener({ log, errorHandler: false }, () => { throw error; }));
server.listener.mockImplementation(httpListener(
{ log: suppressedLog, errorHandler: false },
() => { throw error; },
));

const response = await server.get('/test');

Expand All @@ -200,18 +212,14 @@ describe('httpListener', () => {
it('logs ERROR when there is neither error, not default handler', async () => {

const error = new Error('test');
const log = suppressedLog;
const logErrorSpy = jest.spyOn(log, 'error');
const listener = httpListener(
{ log, defaultHandler: false, errorHandler: false },
{ log: suppressedLog, defaultHandler: false, errorHandler: false },
() => { throw error; },
);

server.listener.mockImplementation((request, response) => {
listener(request, response);
Promise.resolve().finally(() => {
response.end('NO RESPONSE');
});
response.end('NO RESPONSE');
});

await server.get('/test');
Expand All @@ -223,27 +231,28 @@ describe('httpListener', () => {
it('logs unhandled errors', async () => {

const error = new Error('test');
const log = suppressedLog;
const logErrorSpy = jest.spyOn(log, 'error');
const errorHandler = jest.fn(() => {
throw error;
});
const whenErrorLogged = new Promise(resolve => {
logErrorSpy.mockImplementation(resolve);
});

const listener = httpListener(
{ log, errorHandler },
{ log: suppressedLog, errorHandler },
() => { throw error; },
);

server.listener.mockImplementation((request, response) => {
listener(request, response);
Promise.resolve().finally(() => {
response.end('NO RESPONSE');
});
response.end('NO RESPONSE');
});

const response = await server.get('/test');

expect(await response.body()).toBe('NO RESPONSE');

await whenErrorLogged;
expect(logErrorSpy).toHaveBeenCalledWith('[GET /test]', 'Unhandled error', error);
});

Expand Down

0 comments on commit 8d48d05

Please sign in to comment.