refactor(http): eagerly loaded `HttpErrorEffect`
MODIFY
packages/http/src/effects/http.effects.interface.ts
packages/http/src/effects/http.effects.interface.ts
@@ -15,7 +15,9 @@ export interface HttpMiddlewareEffect<
export interface HttpErrorEffect<
Err extends Error = Error,
-> extends HttpEffect<{ req: HttpRequest; error: Err }, HttpEffectResponse> {}
+ Req extends HttpRequest = HttpRequest,
+ Res extends HttpEffectResponse = HttpEffectResponse
+> extends HttpEffect<{ req: Req; error: Err }, { req: Req, res: Res }> {}
export interface HttpServerEffect<
Ev extends Event = Event

MODIFY
packages/http/src/error/http.error.effect.spec.ts
packages/http/src/error/http.error.effect.spec.ts
@@ -10,7 +10,6 @@ describe('defaultError$', () => {
test('maps HttpError', () => {
const error = new HttpError('test-message', 400);
- const incomingRequest = { req, error };
const outgoingResponse = {
status: 400,
body: { error: {
@@ -20,14 +19,13 @@ describe('defaultError$', () => {
};
Marbles.assertEffect(defaultError$, [
- ['-a-', { a: incomingRequest }],
- ['-a-', { a: outgoingResponse }],
+ ['-a-', { a: { req, error } }],
+ ['-a-', { a: { req, res: outgoingResponse } }],
], { ctx });
});
test('maps other errors', () => {
const error = new Error('test-message');
- const incomingRequest = { req, error };
const outgoingResponse = {
status: 500,
body: { error: {
@@ -37,14 +35,13 @@ describe('defaultError$', () => {
};
Marbles.assertEffect(defaultError$, [
- ['-a-', { a: incomingRequest }],
- ['-a-', { a: outgoingResponse }],
+ ['-a-', { a: { req, error } }],
+ ['-a-', { a: { req, res: outgoingResponse } }],
], { ctx });
});
test('maps to "Internal server error" if "error" is not provided', () => {
const error = undefined;
- const incomingRequest = { req, error };
const outgoingResponse = {
status: 500,
body: { error: {
@@ -54,8 +51,8 @@ describe('defaultError$', () => {
};
Marbles.assertEffect(defaultError$, [
- ['-a-', { a: incomingRequest }],
- ['-a-', { a: outgoingResponse }],
+ ['-a-', { a: { req, error } }],
+ ['-a-', { a: { req, res: outgoingResponse } }],
], { ctx });
});
});

MODIFY
packages/http/src/error/http.error.effect.ts
packages/http/src/error/http.error.effect.ts
@@ -30,9 +30,9 @@ const errorFactory = (status: HttpStatus) => (error: Error): HttpErrorResponse =
export const defaultError$: HttpErrorEffect = req$ =>
req$.pipe(
- map(({ error = defaultHttpError }) => {
+ map(({ req, error = defaultHttpError }) => {
const status = getStatusCode(error);
const body = errorFactory(status)(error);
- return { status, body };
+ return ({ req, res: { status, body } });
}),
);

MODIFY
packages/http/src/router/http.router.resolver.v2.ts
packages/http/src/router/http.router.resolver.v2.ts
-import { Subject, of, fromEvent, Observable } from 'rxjs';
+import { Observable, Subject, fromEvent, merge } from 'rxjs';
import { takeUntil, share, take, mergeMap, map, catchError } from 'rxjs/operators';
import { pipe } from 'fp-ts/lib/function';
import { EffectContext, useContext, LoggerToken, LoggerTag, LoggerLevel } from '@marblejs/core';
@@ -23,21 +23,21 @@ import { ROUTE_NOT_FOUND_ERROR } from './http.router.effects';
import { decorateEffect } from './http.router.helpers';
import { combineRouteMiddlewares } from './http.router.combiner';
-export const resolveRouting = ({ routing, ctx, output$, error$ } : {
+export const resolveRouting = (config: {
routing: Routing,
ctx: EffectContext<HttpServer>,
output$?: HttpOutputEffect,
error$?: HttpErrorEffect,
}) => {
const environmentConfig = provideConfig();
- const requestBus = useContext(HttpRequestBusToken)(ctx.ask);
- const logger = useContext(LoggerToken)(ctx.ask);
+ const requestBus = useContext(HttpRequestBusToken)(config.ctx.ask);
+ const logger = useContext(LoggerToken)(config.ctx.ask);
const outputSubject = new Subject<{ res: HttpEffectResponse; req: HttpRequest}>();
const errorSubject = new Subject<{ error: Error; req: HttpRequest }>();
const close$ = pipe(
- fromEvent(ctx.client, 'close'),
+ fromEvent(config.ctx.client, 'close'),
take(1),
share());
@@ -46,22 +46,18 @@ export const resolveRouting = ({ routing, ctx, output$, error$ } : {
*/
const response$ = pipe(
outputSubject.asObservable(),
- out$ => environmentConfig.useHttpRequestMetadata() ? requestMetadata$(out$, ctx) : out$,
- out$ => output$ ? output$(out$, ctx) : out$,
- takeUntil(close$));
+ o$ => environmentConfig.useHttpRequestMetadata() ? requestMetadata$(o$, config.ctx) : o$,
+ o$ => config.output$ ? config.output$(o$, config.ctx) : o$,
+ takeUntil(close$),
+ );
/**
* Outgoing error response stream (the result triggers HTTP response call)
- * @TODO investigate HttpErrorEffect lazy loading
*/
- const errorFlow$ = errorSubject.asObservable().pipe(
- map(data => isHttpRequestError(data.error) ? { ...data, error: data.error.error } : data),
- mergeMap(data => {
- const stream = error$ ? error$(of(data), ctx) : defaultError$(of(data), ctx);
- return stream.pipe(
- map(res => ([res, data.req] as [HttpEffectResponse, HttpRequest])),
- );
- }),
+ const error$ = pipe(
+ errorSubject.asObservable(),
+ map(({ error , req }) => isHttpRequestError(error) ? { req, error: error.error } : ({ error, req })),
+ e$ => config.error$ ? config.error$(e$, config.ctx) : defaultError$(e$, config.ctx),
takeUntil(close$),
);
@@ -72,17 +68,17 @@ export const resolveRouting = ({ routing, ctx, output$, error$ } : {
error: err => { throw unexpectedErrorWhileSendingOutputFactory(err); },
});
- const subscribeError = (stream$: Observable<[HttpEffectResponse, HttpRequest]>) =>
+ const subscribeError = (stream$: Observable<{ res: HttpEffectResponse, req: HttpRequest }>) =>
stream$
- .pipe(mergeMap(([res, req]) => req.response.send(res)))
+ .pipe(mergeMap(({ res, req }) => req.response.send(res)))
.subscribe({
error: err => { throw unexpectedErrorWhileSendingErrorFactory(err); },
});
subscribeResponse(response$);
- subscribeError(errorFlow$);
+ subscribeError(error$);
- const bootstrappedRrouting: BootstrappedRoutingItem[] = routing.map(item => ({
+ const bootstrappedRrouting: BootstrappedRoutingItem[] = config.routing.map(item => ({
...item,
methods: Object.entries(item.methods).reduce((acc, [method, methodItem]) => {
if (!methodItem) return { [method]: undefined };
@@ -100,9 +96,9 @@ export const resolveRouting = ({ routing, ctx, output$, error$ } : {
const output$ = pipe(
subject.asObservable(),
- e$ => middleware(e$, ctx),
+ e$ => middleware(e$, config.ctx),
e$ => decorate ? decorateEffect(e$, errorSubject) : e$,
- e$ => effect(e$, ctx),
+ e$ => effect(e$, config.ctx),
catchError((error, stream) => processError(stream)(error)),
takeUntil(close$),
);
@@ -172,6 +168,6 @@ export const resolveRouting = ({ routing, ctx, output$, error$ } : {
resolve,
errorSubject,
outputSubject,
- response$,
+ response$: merge(response$, error$),
};
};

MODIFY
packages/http/src/router/specs/http.router.resolver.v2.spec.ts
packages/http/src/router/specs/http.router.resolver.v2.spec.ts
@@ -3,7 +3,7 @@ import { pipe } from 'fp-ts/lib/function';
import { of, merge, firstValueFrom, lastValueFrom } from 'rxjs';
import { mapTo, take, toArray, delay, mergeMap, map, first } from 'rxjs/operators';
import { createMockEffectContext, createHttpResponse, createHttpRequest, createTestRoute } from '../../+internal/testing.util';
-import { HttpEffect, HttpOutputEffect } from '../../effects/http.effects.interface';
+import { HttpEffect, HttpErrorEffect, HttpOutputEffect } from '../../effects/http.effects.interface';
import { Routing } from '../http.router.interface';
import { resolveRouting } from '../http.router.resolver.v2';
import { factorizeRegExpWithParams } from '../http.router.params.factory';
@@ -74,19 +74,15 @@ describe('#resolveRouting', () => {
// given
const effectSpy = jest.fn();
const ctx = createMockEffectContext();
-
- const testData = [
- createTestRoute({ effectSpy }),
- ];
-
- const routing: Routing = testData.map(route => route.item);
+ const routes = [createTestRoute({ effectSpy })];
+ const routing: Routing = routes.map(route => route.item);
// when
const { resolve, response$ } = resolveRouting({ routing, ctx });
const run: Task<any> = () => {
- resolve(testData[0].req); // first call
- resolve(testData[0].req); // second call
+ resolve(routes[0].req); // first call
+ resolve(routes[0].req); // second call
return pipe(response$, take(2), toArray(), lastValueFrom);
};
@@ -101,21 +97,18 @@ describe('#resolveRouting', () => {
// given
const effectSpy = jest.fn();
const ctx = createMockEffectContext();
+ const routes = [createTestRoute()];
+ const routing: Routing = routes.map(route => route.item);
- const testData = [
- createTestRoute(),
- ];
-
- const routing: Routing = testData.map(route => route.item);
-
+ // given - output effect
const output$: HttpOutputEffect = out$ => (effectSpy(), out$);
// when
const { resolve, response$ } = resolveRouting({ routing, ctx, output$ });
const run: Task<any> = () => {
- resolve(testData[0].req); // first call
- resolve(testData[0].req); // second call
+ resolve(routes[0].req); // first call
+ resolve(routes[0].req); // second call
return pipe(response$, take(2), toArray(), lastValueFrom);
};
@@ -126,7 +119,34 @@ describe('#resolveRouting', () => {
expect(effectSpy).toHaveBeenCalledTimes(1);
});
- test.todo('resolves `HttpErrorEffect` only once');
+ test('resolves `HttpErrorEffect` only once', async () => {
+ // given
+ const effectSpy = jest.fn();
+ const ctx = createMockEffectContext();
+ const routes = [createTestRoute({ throwError: true })];
+ const routing: Routing = routes.map(route => route.item);
+
+ // given - error effect
+ const error$: HttpErrorEffect = error$ => {
+ effectSpy();
+ return error$.pipe(map(({ req }) => ({ req, res: { body: 'ERROR' } })));
+ };
+
+ // when
+ const { resolve, response$ } = resolveRouting({ routing, ctx, error$ });
+
+ const run: Task<any> = () => {
+ resolve(routes[0].req); // first call
+ resolve(routes[0].req); // second call
+
+ return pipe(response$, take(2), toArray(), lastValueFrom);
+ };
+
+ await run();
+
+ // then
+ expect(effectSpy).toHaveBeenCalledTimes(1);
+ });
test(`returns ${HttpStatus.NOT_FOUND} (Not Found) response if route cannot be resolved`, async () => {
// given

refactor(http): eagerly loaded `HttpErrorEffect`