diff --git a/packages/http/src/+internal/testing.util.ts b/packages/http/src/+internal/testing.util.ts index b3850c2d..025ba302 100644 --- a/packages/http/src/+internal/testing.util.ts +++ b/packages/http/src/+internal/testing.util.ts @@ -84,19 +84,22 @@ export const createMockEffectContext = () => { return createEffectContext({ ask: lookup(context), client }); }; -export const createTestRoute = (opts?: { throwError?: boolean; delay?: number; method?: HttpMethod }) => { +export const createTestRoute = (opts?: { throwError?: boolean; delay?: number; method?: HttpMethod; effectSpy?: jest.Mock }) => { const method = opts?.method ?? 'GET'; const routeDelay = opts?.delay ?? 0; const req = createHttpRequest(({ url: `/delay_${routeDelay}`, method })); const path = factorizeRegExpWithParams(`/delay_${routeDelay}`); - const effect: HttpEffect = req$ => - req$.pipe( + const effect: HttpEffect = req$ => { + opts?.effectSpy?.(); + + return req$.pipe( delay(routeDelay), tap(() => { if (opts?.throwError) throw new Error(); }), mapTo({ body: `delay_${routeDelay}` }), ); + }; const item: RoutingItem = { regExp: path.regExp, diff --git a/packages/http/src/effects/http.effects.interface.ts b/packages/http/src/effects/http.effects.interface.ts index 88a30156..351e31f4 100644 --- a/packages/http/src/effects/http.effects.interface.ts +++ b/packages/http/src/effects/http.effects.interface.ts @@ -1,5 +1,5 @@ import { Event, Effect } from '@marblejs/core'; -import { HttpRequest, HttpStatus, HttpHeaders, HttpServer } from '../http.interface'; +import { HttpRequest, HttpStatus, HttpHeaders, HttpServer, WithHttpRequest } from '../http.interface'; export interface HttpEffectResponse { request?: HttpRequest; @@ -15,7 +15,11 @@ export interface HttpMiddlewareEffect< export interface HttpErrorEffect< Err extends Error = Error, -> extends HttpEffect<{ req: HttpRequest; error: Err }, HttpEffectResponse> {} + Req extends HttpRequest = HttpRequest, +> extends HttpEffect< + WithHttpRequest<{ error: Err }, Req>, + WithHttpRequest +> {} export interface HttpServerEffect< Ev extends Event = Event @@ -23,8 +27,10 @@ export interface HttpServerEffect< export interface HttpOutputEffect< Req extends HttpRequest = HttpRequest, - Res extends HttpEffectResponse = HttpEffectResponse -> extends HttpEffect<{ req: Req; res: Res }, HttpEffectResponse> {} +> extends HttpEffect< + WithHttpRequest, + WithHttpRequest +> {} export interface HttpEffect< I = HttpRequest, diff --git a/packages/http/src/effects/http.requestMetadata.effect.ts b/packages/http/src/effects/http.requestMetadata.effect.ts index c7baff83..5ee998c0 100644 --- a/packages/http/src/effects/http.requestMetadata.effect.ts +++ b/packages/http/src/effects/http.requestMetadata.effect.ts @@ -6,15 +6,15 @@ import { HttpRequestMetadataStorageToken } from '../server/internal-dependencies import { getHttpRequestMetadataIdHeader } from '../+internal/metadata.util'; import { HttpOutputEffect } from './http.effects.interface'; -export const requestMetadata$: HttpOutputEffect = (out$, ctx) => { +export const requestMetadata$: HttpOutputEffect = (output$, ctx) => { const httpRequestMetadataStorage = useContext(HttpRequestMetadataStorageToken)(ctx.ask); - return out$.pipe( - map(({ req, res }) => pipe( - getHttpRequestMetadataIdHeader(req.headers), - O.fold(constant(res), requestId => { - httpRequestMetadataStorage.set(requestId, req.meta); - return res; + return output$.pipe( + map(response => pipe( + getHttpRequestMetadataIdHeader(response.request.headers), + O.fold(constant(response), requestId => { + httpRequestMetadataStorage.set(requestId, response.request.meta); + return response; }), )), ); diff --git a/packages/http/src/error/http.error.effect.spec.ts b/packages/http/src/error/http.error.effect.spec.ts index 4343a8e1..f7448e48 100644 --- a/packages/http/src/error/http.error.effect.spec.ts +++ b/packages/http/src/error/http.error.effect.spec.ts @@ -4,13 +4,12 @@ import { defaultError$ } from './http.error.effect'; import { HttpError } from './http.error.model'; describe('defaultError$', () => { - const req = createHttpRequest(); + const request = createHttpRequest(); const client = createHttpResponse(); const ctx = { client }; 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: { request, error } }], + ['-a-', { a: { request, ...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: { request, error } }], + ['-a-', { a: { request, ...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: { request, error } }], + ['-a-', { a: { request, ...outgoingResponse } }], ], { ctx }); }); }); diff --git a/packages/http/src/error/http.error.effect.ts b/packages/http/src/error/http.error.effect.ts index c27b4b26..66d30132 100644 --- a/packages/http/src/error/http.error.effect.ts +++ b/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(({ request, error = defaultHttpError }) => { const status = getStatusCode(error); const body = errorFactory(status)(error); - return { status, body }; + return ({ status, body, request }); }), ); diff --git a/packages/http/src/error/http.error.model.ts b/packages/http/src/error/http.error.model.ts index cd42d2cd..788c3beb 100644 --- a/packages/http/src/error/http.error.model.ts +++ b/packages/http/src/error/http.error.model.ts @@ -36,13 +36,8 @@ export const isHttpError = (error: Error | undefined): error is HttpError => export const isHttpRequestError = (error: Error | undefined): error is HttpRequestError => error?.name === HttpErrorType.HTTP_REQUEST_ERROR; -export const unexpectedErrorWhileSendingErrorFactory = (error: Error): CoreError => { - const message = `An unexpected error ${chalk.red(`"${error.message}"`)} occured while sending an error response. Please check your error effect.`; - return coreErrorFactory(message, { printStacktrace: false }); -}; - -export const unexpectedErrorWhileSendingOutputFactory = (error: Error): CoreError => { - const message = `An unexpected error ${chalk.red(`"${error.message}"`)} occured while sending a response. Please check your output effect.`; +export const unexpectedErrorWhileSendingResponseFactory = (error: Error): CoreError => { + const message = `An unexpected error ${chalk.red(`"${error.message}"`)} occured while sending a response. Please check your output/error effect.`; return coreErrorFactory(message, { printStacktrace: false }); }; diff --git a/packages/http/src/http.interface.ts b/packages/http/src/http.interface.ts index 222cc765..daa60fc1 100644 --- a/packages/http/src/http.interface.ts +++ b/packages/http/src/http.interface.ts @@ -20,6 +20,9 @@ export interface HttpRequest< [key: string]: any; } +export type WithHttpRequest = + { request: Req } & T; + export interface RouteParameters { [key: string]: any; } @@ -29,6 +32,13 @@ export interface QueryParameters { } export interface HttpResponse extends http.ServerResponse { + /** + * Send HTTP response + * + * @param response `HttpEffectResponse` + * @returns `Observable` (indicates whether the response was sent or not) + * @since 1.0.0 + */ send: (response: HttpEffectResponse) => Observable; } diff --git a/packages/http/src/router/http.router.helpers.ts b/packages/http/src/router/http.router.helpers.ts index 08eee378..92f646bd 100644 --- a/packages/http/src/router/http.router.helpers.ts +++ b/packages/http/src/router/http.router.helpers.ts @@ -18,7 +18,7 @@ export const decorateEffect = (stream: Observable, errorSubject: Er ...operations, map((res: HttpEffectResponse) => ({ ...res, request })), catchError((error: Error) => { - errorSubject.next({ error, req: request }); + errorSubject.next({ error, request }); return EMPTY; }), ])(of(request)) as Observable), @@ -32,7 +32,7 @@ export const decorateMiddleware = (stream: Observable, errorSubject mergeMap((request: HttpRequest) => pipeFromArray([ ...operations, catchError((error: Error) => { - errorSubject.next({ error, req: request }); + errorSubject.next({ error, request }); return EMPTY; }), ])(of(request)) as Observable), diff --git a/packages/http/src/router/http.router.interface.ts b/packages/http/src/router/http.router.interface.ts index b2046a59..c3ecf020 100644 --- a/packages/http/src/router/http.router.interface.ts +++ b/packages/http/src/router/http.router.interface.ts @@ -1,11 +1,8 @@ import { Subject } from 'rxjs'; -import { HttpMethod, HttpRequest } from '../http.interface'; +import { HttpMethod, HttpRequest, WithHttpRequest } from '../http.interface'; import { HttpEffect, HttpMiddlewareEffect, HttpEffectResponse } from '../effects/http.effects.interface'; -export type ErrorSubject = Subject<{ - error: Error; - req: HttpRequest; -}>; +export type ErrorSubject = Subject>; // Route export interface RouteMeta extends Record { diff --git a/packages/http/src/router/http.router.resolver.v2.ts b/packages/http/src/router/http.router.resolver.ts similarity index 50% rename from packages/http/src/router/http.router.resolver.v2.ts rename to packages/http/src/router/http.router.resolver.ts index 1c899ab3..c44bb991 100644 --- a/packages/http/src/router/http.router.resolver.v2.ts +++ b/packages/http/src/router/http.router.resolver.ts @@ -1,13 +1,13 @@ -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 { flow, pipe } from 'fp-ts/lib/function'; import { EffectContext, useContext, LoggerToken, LoggerTag, LoggerLevel } from '@marblejs/core'; -import { HttpServer, HttpRequest, HttpStatus } from '../http.interface'; +import { throwException } from '@marblejs/core/dist/+internal/utils'; +import { HttpServer, HttpRequest, HttpStatus, WithHttpRequest } from '../http.interface'; import { defaultError$ } from '../error/http.error.effect'; import { HttpEffectResponse, HttpErrorEffect, HttpOutputEffect } from '../effects/http.effects.interface'; import { - unexpectedErrorWhileSendingErrorFactory, - unexpectedErrorWhileSendingOutputFactory, + unexpectedErrorWhileSendingResponseFactory, errorNotBoundToRequestErrorFactory, responseNotBoundToRequestErrorFactory, isHttpRequestError, @@ -23,70 +23,66 @@ import { ROUTE_NOT_FOUND_ERROR } from './http.router.effects'; import { decorateEffect } from './http.router.helpers'; import { combineRouteMiddlewares } from './http.router.combiner'; -export const resolveRouting = ( +type ResolveRoutingConfig = Readonly<{ routing: Routing, ctx: EffectContext, -) => ( output$?: HttpOutputEffect, error$?: HttpErrorEffect, -) => { +}> + +export const resolveRouting = (config: ResolveRoutingConfig) => { 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>(); + const errorSubject = new Subject>(); - const close$ = fromEvent(ctx.client, 'close').pipe(take(1), share()); - const outputSubject = new Subject<{ res: HttpEffectResponse; req: HttpRequest}>(); - const errorSubject = new Subject<{ error: Error; req: HttpRequest }>(); + /** + * Server close stream (closes all active streams) + */ + const close$ = pipe( + fromEvent(config.ctx.client, 'close'), + take(1), + share()); /** - * @TODO investigate HttpOutputEffect lazy loading + * Outgoing response stream (the result triggers HTTP response call) */ - const outputFlow$ = outputSubject.asObservable().pipe( - mergeMap(data => { - const stream = environmentConfig.useHttpRequestMetadata() ? requestMetadata$(of(data), ctx) : of(data.res); - return stream.pipe(map(res => ({ res, req: data.req }))); - }), - mergeMap(data => { - const stream = output$ ? output$(of(data), ctx) : of(data.res); - return stream.pipe( - map(res => ([res, data.req] as [HttpEffectResponse, HttpRequest])), - ); - }), + const response$ = pipe( + outputSubject.asObservable(), + o$ => environmentConfig.useHttpRequestMetadata() ? requestMetadata$(o$, config.ctx) : o$, + o$ => config.output$ ? config.output$(o$, config.ctx) : o$, takeUntil(close$), ); /** - * @TODO investigate HttpErrorEffect lazy loading + * Outgoing error response stream (the result triggers HTTP response call) */ - 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(({ request, error }) => isHttpRequestError(error) ? { request, error: error.error } : ({ request, error })), + e$ => config.error$ ? config.error$(e$, config.ctx) : defaultError$(e$, config.ctx), takeUntil(close$), ); - const subscribeOutput = (stream$: Observable<[HttpEffectResponse, HttpRequest]>) => - stream$ - .pipe(mergeMap(([res, req]) => req.response.send(res))) - .subscribe({ - error: err => { throw unexpectedErrorWhileSendingOutputFactory(err); }, - }); - - const subscribeError = (stream$: Observable<[HttpEffectResponse, HttpRequest]>) => + /** + * Subscribe to all outgoing HTTP responses and trigger side effect + * @param stream$ incoming `HttpEffectResponse` + * @returns `Subscription` + */ + const subscribeResponse = (stream$: Observable>) => stream$ - .pipe(mergeMap(([res, req]) => req.response.send(res))) + .pipe(mergeMap(({ request, ...res }) => request.response.send(res))) .subscribe({ - error: err => { throw unexpectedErrorWhileSendingErrorFactory(err); }, + error: flow( + unexpectedErrorWhileSendingResponseFactory, + throwException), }); - subscribeOutput(outputFlow$); - subscribeError(errorFlow$); + subscribeResponse(response$); + subscribeResponse(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 }; @@ -102,26 +98,26 @@ export const resolveRouting = ( message: `Effect mapped: ${item.path || '/'} ${method}`, })(); + const processError = (error: any, originStream$: Observable): Observable => { + if (!error.request) throw errorNotBoundToRequestErrorFactory(error); + errorSubject.next({ error, request: error.request }); + return originStream$; + }; + const output$ = pipe( subject.asObservable(), - e$ => middleware(e$, ctx), + e$ => middleware(e$, config.ctx), e$ => decorate ? decorateEffect(e$, errorSubject) : e$, - e$ => effect(e$, ctx), - catchError((error, stream) => processError(stream)(error)), + e$ => effect(e$, config.ctx), + catchError(processError), takeUntil(close$), ); - const processError = (originStream$: Observable) => (error: any) => { - if (!error.request) throw errorNotBoundToRequestErrorFactory(error); - errorSubject.next({ error, req: error.request }); - return originStream$; - }; - const subscribe = (stream$: Observable) => stream$.subscribe({ next: res => { if (!res.request) throw responseNotBoundToRequestErrorFactory(res); - outputSubject.next({ res, req: res.request }); + outputSubject.next({ ...res, request: res.request }); }, error: err => { const type = 'RouterResolver'; @@ -146,29 +142,34 @@ export const resolveRouting = ( const find = matchRoute(bootstrappedRrouting); - const resolve = (req: HttpRequest) => { - const [urlPath, urlQuery] = req.url.split('?'); + /** + * Resolve incoming request + * @param request `HttpRequest` + * @returns `void` + */ + const resolve = (request: HttpRequest) => { + const [urlPath, urlQuery] = request.url.split('?'); try { - const resolvedRoute = find(urlPath, req.method); + const resolvedRoute = find(urlPath, request.method); if (!resolvedRoute) { - return errorSubject.next({ req, error: ROUTE_NOT_FOUND_ERROR }); + return errorSubject.next({ request, error: ROUTE_NOT_FOUND_ERROR }); } - req.query = queryParamsFactory(urlQuery); - req.params = resolvedRoute.params; - req.meta = {}; - req.meta.path = resolvedRoute.path; + request.query = queryParamsFactory(urlQuery); + request.params = resolvedRoute.params; + request.meta = {}; + request.meta.path = resolvedRoute.path; - resolvedRoute.subject.next(req); - requestBus.next(req); + resolvedRoute.subject.next(request); + requestBus.next(request); } catch (error) { if (error.name === 'URIError') { - return errorSubject.next({ req, error: new HttpError(error.message, HttpStatus.BAD_REQUEST) }); + return errorSubject.next({ request, error: new HttpError(error.message, HttpStatus.BAD_REQUEST) }); } - return errorSubject.next({ req, error: new HttpError(`Internal server error: ${error.message}`, HttpStatus.INTERNAL_SERVER_ERROR) }); + return errorSubject.next({ request, error: new HttpError(`Internal server error: ${error.message}`, HttpStatus.INTERNAL_SERVER_ERROR) }); } }; @@ -176,5 +177,6 @@ export const resolveRouting = ( resolve, errorSubject, outputSubject, + response$: merge(response$, error$), }; }; diff --git a/packages/http/src/router/specs/http.router.resolver.v2.spec.ts b/packages/http/src/router/specs/http.router.resolver.spec.ts similarity index 60% rename from packages/http/src/router/specs/http.router.resolver.v2.spec.ts rename to packages/http/src/router/specs/http.router.resolver.spec.ts index 13fb18d5..eb48a1e3 100644 --- a/packages/http/src/router/specs/http.router.resolver.v2.spec.ts +++ b/packages/http/src/router/specs/http.router.resolver.spec.ts @@ -1,15 +1,17 @@ -import { of, merge, firstValueFrom } from 'rxjs'; -import { mapTo, take, toArray, delay, mergeMap, map, first } from 'rxjs/operators'; +import { Task } from 'fp-ts/lib/Task'; +import { pipe } from 'fp-ts/lib/function'; +import { of, merge, firstValueFrom, lastValueFrom } from 'rxjs'; +import { mapTo, take, toArray, delay, mergeMap, map } from 'rxjs/operators'; import { createMockEffectContext, createHttpResponse, createHttpRequest, createTestRoute } from '../../+internal/testing.util'; -import { HttpEffect } 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 { resolveRouting } from '../http.router.resolver'; import { factorizeRegExpWithParams } from '../http.router.params.factory'; import { HttpError } from '../../error/http.error.model'; import { HttpStatus } from '../../http.interface'; describe('#resolveRouting', () => { - test('resolves routes inside collection', async (done) => { + test('resolves routes inside collection', async done => { // given const ctx = createMockEffectContext(); const response = createHttpResponse(); @@ -48,15 +50,15 @@ describe('#resolveRouting', () => { ]; // when - const { resolve, outputSubject } = resolveRouting(routing, ctx)(); + const { resolve, outputSubject } = resolveRouting({ routing, ctx }); // then outputSubject.pipe(take(4), toArray()).subscribe( result => { - expect(result[0].res).toEqual({ body: 'test_1', request: req1 }); - expect(result[1].res).toEqual({ body: 'test_2', request: req2 }); - expect(result[2].res).toEqual({ body: 'test_3', request: req3 }); - expect(result[3].res).toEqual({ body: 'test_4', request: req4 }); + expect(result[0]).toEqual({ body: 'test_1', request: req1 }); + expect(result[1]).toEqual({ body: 'test_2', request: req2 }); + expect(result[2]).toEqual({ body: 'test_3', request: req3 }); + expect(result[3]).toEqual({ body: 'test_4', request: req4 }); done(); }, ); @@ -68,11 +70,89 @@ describe('#resolveRouting', () => { resolve(req5); }); + test('resolves `HttpEffect` only once', async () => { + // given + const effectSpy = jest.fn(); + const ctx = createMockEffectContext(); + const routes = [createTestRoute({ effectSpy })]; + const routing: Routing = routes.map(route => route.item); + + // when + const { resolve, response$ } = resolveRouting({ routing, ctx }); + + const run: Task = () => { + 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('resolves `HttpOutputEffect` only once', async () => { + // given + const effectSpy = jest.fn(); + const ctx = createMockEffectContext(); + const routes = [createTestRoute()]; + const routing: Routing = routes.map(route => route.item); + + // given - output effect + const output$: HttpOutputEffect = out$ => (effectSpy(), out$); + + // when + const { resolve, response$ } = resolveRouting({ routing, ctx, output$ }); + + const run: Task = () => { + 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('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(({ request }) => ({ request, body: 'ERROR' }))); + }; + + // when + const { resolve, response$ } = resolveRouting({ routing, ctx, error$ }); + + const run: Task = () => { + 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 const ctx = createMockEffectContext(); const response = createHttpResponse(); - const req = createHttpRequest(({ url: '/unknown', method: 'GET', response })); + const request = createHttpRequest(({ url: '/unknown', method: 'GET', response })); const path = factorizeRegExpWithParams('/'); const effect$: HttpEffect = req$ => req$.pipe(mapTo({ body: 'test' })); @@ -84,14 +164,14 @@ describe('#resolveRouting', () => { }]; // when - const { resolve, errorSubject } = resolveRouting(routing, ctx)(); - const errorPromise = firstValueFrom(errorSubject.pipe(first())); + const { resolve, errorSubject } = resolveRouting({ routing, ctx }); + const errorPromise = firstValueFrom(errorSubject); - resolve(req); + resolve(request); // then await expect(errorPromise).resolves.toEqual({ - req, + request, error: new HttpError('Route not found', HttpStatus.NOT_FOUND), }); }); @@ -100,7 +180,7 @@ describe('#resolveRouting', () => { // given const ctx = createMockEffectContext(); const response = createHttpResponse(); - const req = createHttpRequest(({ url: '/group/%test', method: 'GET', response })); + const request = createHttpRequest(({ url: '/group/%test', method: 'GET', response })); const path = factorizeRegExpWithParams('/group/:id'); const effect$: HttpEffect = req$ => req$.pipe(mapTo({ body: 'test' })); @@ -112,14 +192,14 @@ describe('#resolveRouting', () => { }]; // when - const { resolve, errorSubject } = resolveRouting(routing, ctx)(); - const errorPromise = firstValueFrom(errorSubject.pipe(first())); + const { resolve, errorSubject } = resolveRouting({ routing, ctx }); + const errorPromise = firstValueFrom(errorSubject); - resolve(req); + resolve(request); // then await expect(errorPromise).resolves.toEqual({ - req, + request, error: new HttpError('URI malformed', HttpStatus.BAD_REQUEST), }); }); @@ -147,7 +227,7 @@ describe('#resolveRouting', () => { }]; // when - const { resolve, outputSubject } = resolveRouting(routing, ctx)(); + const { resolve, outputSubject } = resolveRouting({ routing, ctx }); const run = () => { resolve(testData[0]); // 10 delay resolve(testData[3]); // 40 delay @@ -158,10 +238,10 @@ describe('#resolveRouting', () => { // then outputSubject.pipe(take(4), toArray()).subscribe( result => { - expect(result[0].res).toEqual({ body: 'delay_10', request: testData[0] }); - expect(result[1].res).toEqual({ body: 'delay_20', request: testData[1] }); - expect(result[2].res).toEqual({ body: 'delay_30', request: testData[2] }); - expect(result[3].res).toEqual({ body: 'delay_40', request: testData[3] }); + expect(result[0]).toEqual({ body: 'delay_10', request: testData[0] }); + expect(result[1]).toEqual({ body: 'delay_20', request: testData[1] }); + expect(result[2]).toEqual({ body: 'delay_30', request: testData[2] }); + expect(result[3]).toEqual({ body: 'delay_40', request: testData[3] }); done(); }, ); @@ -183,7 +263,7 @@ describe('#resolveRouting', () => { const routing: Routing = testData.map(route => route.item); // when - const { resolve, outputSubject } = resolveRouting(routing, ctx)(); + const { resolve, outputSubject } = resolveRouting({ routing, ctx }); const run = () => { resolve(testData[0].req); // 10 delay resolve(testData[3].req); // 40 delay @@ -194,10 +274,10 @@ describe('#resolveRouting', () => { // then outputSubject.pipe(take(4), toArray()).subscribe( result => { - expect(result[0].res).toEqual({ body: 'delay_10', request: testData[0].req }); - expect(result[1].res).toEqual({ body: 'delay_20', request: testData[1].req }); - expect(result[2].res).toEqual({ body: 'delay_30', request: testData[2].req }); - expect(result[3].res).toEqual({ body: 'delay_40', request: testData[3].req }); + expect(result[0]).toEqual({ body: 'delay_10', request: testData[0].req }); + expect(result[1]).toEqual({ body: 'delay_20', request: testData[1].req }); + expect(result[2]).toEqual({ body: 'delay_30', request: testData[2].req }); + expect(result[3]).toEqual({ body: 'delay_40', request: testData[3].req }); done(); }, ); @@ -219,7 +299,7 @@ describe('#resolveRouting', () => { const routing: Routing = testData.map(route => route.item); // when - const { resolve, outputSubject, errorSubject } = resolveRouting(routing, ctx)(); + const { resolve, outputSubject, errorSubject } = resolveRouting({ routing, ctx }); const run = () => { resolve(testData[0].req); // 10 delay resolve(testData[1].req); // 20 delay @@ -229,7 +309,7 @@ describe('#resolveRouting', () => { // then merge( - outputSubject.pipe(take(3), map(res => res.res)), + outputSubject.pipe(take(3)), errorSubject.pipe(take(1), map(res => res.error)), ).pipe( toArray(), diff --git a/packages/http/src/server/http.server.listener.ts b/packages/http/src/server/http.server.listener.ts index b3dc0a8f..5ce3e8fb 100644 --- a/packages/http/src/server/http.server.listener.ts +++ b/packages/http/src/server/http.server.listener.ts @@ -4,7 +4,7 @@ import { HttpMiddlewareEffect, HttpErrorEffect, HttpOutputEffect } from '../effe import { HttpRequest, HttpResponse } from '../http.interface'; import { handleResponse } from '../response/http.responseHandler'; import { RouteEffect, RouteEffectGroup, Routing } from '../router/http.router.interface'; -import { resolveRouting } from '../router/http.router.resolver.v2'; +import { resolveRouting } from '../router/http.router.resolver'; import { factorizeRoutingWithDefaults } from '../router/http.router.factory'; import { HttpServerClientToken } from './internal-dependencies/httpServerClient.reader'; @@ -29,10 +29,10 @@ export const httpListener = createListener(con } = config ?? {}; const client = useContext(HttpServerClientToken)(ask); - const effectContext = createEffectContext({ ask, client }); + const ctx = createEffectContext({ ask, client }); const routing = factorizeRoutingWithDefaults(effects, middlewares ?? []); const sendResponse = handleResponse(ask); - const { resolve } = resolveRouting(routing, effectContext)(output$, error$); + const { resolve } = resolveRouting({ routing, ctx, output$, error$ }); const handle = (req: IncomingMessage, res: OutgoingMessage) => { const marbleReq = req as HttpRequest;