imp(corer): route resolving optimization
MODIFY
benchmarks/Makefile
benchmarks/Makefile
-all: middlewares routing
+all: routing middlewares
routing:
@./run 1 $@

MODIFY
packages/@integration/src/http/fakes/auth.fake.ts
packages/@integration/src/http/fakes/auth.fake.ts
import { HttpRequest } from '@marblejs/core';
export const isAuthorized = (request: HttpRequest) =>
- request.headers.authorization
- ? request.headers.authorization === 'Bearer FAKE'
+ request.headers.authorization || request.url.includes('?token')
+ ? request.headers.authorization === 'Bearer FAKE' || request.url.includes('?token=FAKE')
: false;

MODIFY
packages/@integration/src/http/http.listener.ts
packages/@integration/src/http/http.listener.ts
-import { httpListener } from '@marblejs/core';
+import { httpListener, HttpMiddlewareEffect } from '@marblejs/core';
import { bodyParser$ } from '@marblejs/middleware-body';
import { loggerDev$, loggerFile$ } from './middlewares/logger.middleware';
import { cors$ } from './middlewares/cors.middleware';
import { api$ } from './effects/api.effects';
+const middleware$: HttpMiddlewareEffect = req$ => {
+ console.log('test');
+ return req$;
+}
+
export default httpListener({
middlewares: [
loggerDev$,
loggerFile$,
bodyParser$(),
+ middleware$,
cors$,
],
effects: [api$]

MODIFY
packages/core/src/listener/http.listener.ts
packages/core/src/listener/http.listener.ts
import { IncomingMessage, OutgoingMessage } from 'http';
-import { flow } from 'fp-ts/lib/function';
import { pipe } from 'fp-ts/lib/pipeable';
import * as R from 'fp-ts/lib/Reader';
-import { of, Subject, zip, OperatorFunction } from 'rxjs';
-import { catchError, defaultIfEmpty, mergeMap, tap, map, publish } from 'rxjs/operators';
import { combineMiddlewares } from '../effects/effects.combiner';
import {
- HttpEffectResponse,
HttpMiddlewareEffect,
HttpErrorEffect,
HttpOutputEffect,
} from '../effects/http-effects.interface';
-import { HttpRequest, HttpResponse, HttpStatus } from '../http.interface';
+import { HttpRequest, HttpResponse } from '../http.interface';
import { handleResponse } from '../response/response.handler';
import { RouteEffect, RouteEffectGroup, RoutingItem } from '../router/router.interface';
-import { resolveRouting } from '../router/router.resolver';
+import { resolveRouting } from '../router/router.resolver.v2';
import { factorizeRouting } from '../router/router.factory';
-import { defaultError$ } from '../error/error.effect';
+// import { defaultError$ } from '../error/error.effect';
import { Context, lookup } from '../context/context.factory';
import { createEffectContext } from '../effects/effectsContext.factory';
import { useContext } from '../context/context.hook';
import { ServerClientToken } from '../server/server.tokens';
-import { EffectError } from '../error/error.model';
export interface HttpListenerConfig {
middlewares?: HttpMiddlewareEffect[];
@@ -38,61 +33,19 @@ export interface HttpListener {
export const httpListener = ({
middlewares = [],
effects,
- error$ = defaultError$,
- output$ = out$ => out$.pipe(map(o => o.res)),
+ // error$ = defaultError$,
+ // output$ = out$ => out$.pipe(map(o => o.res)),
}: HttpListenerConfig): R.Reader<Context, HttpListener> => pipe(
R.ask<Context>(),
R.map(ctx => {
- const requestSubject$ = new Subject<HttpRequest>();
- const request$ = requestSubject$.asObservable();
const ask = lookup(ctx);
const client = useContext(ServerClientToken)(ask);
const effectContext = createEffectContext({ ask, client });
const combinedMiddlewares = combineMiddlewares(...middlewares);
const routing = factorizeRouting(effects);
- const defaultResponse = { status: HttpStatus.NOT_FOUND } as HttpEffectResponse;
-
- const errorGuard: OperatorFunction<[any, HttpRequest], HttpRequest> = flow(
- map(result => {
- if (result[0] instanceof Error) throw new EffectError(result[0], result[1]);
- return result[0];
- }),
- );
-
- const catchEffectError: OperatorFunction<HttpRequest, Error> = flow(
- catchError(error => of(error)),
- );
-
- request$.pipe(
- publish(req$ => zip(
- combinedMiddlewares(req$, effectContext).pipe(catchEffectError),
- req$,
- )),
- errorGuard,
- mergeMap(req => {
- return of(req).pipe(
- mergeMap(resolveRouting(routing, effectContext)),
- defaultIfEmpty(defaultResponse),
- mergeMap(out => output$(of({ req, res: out }), effectContext)),
- tap(req.response.send),
- catchError(error =>
- error$(of({ req, error }), effectContext).pipe(
- mergeMap(out => output$(of({ req, res: out }), effectContext)),
- tap(req.response.send),
- ),
- ),
- );
- }),
- ).subscribe(
- undefined,
- ({ req, error }: EffectError) => {
- error$(of({ req, error }), effectContext).pipe(
- mergeMap(out => output$(of({ req, res: out }), effectContext)),
- tap(req.response.send),
- ).subscribe();
- },
- );
+ const provideRoute = resolveRouting(routing, effectContext)(combinedMiddlewares);
+ // const defaultResponse = { status: HttpStatus.NOT_FOUND } as HttpEffectResponse;
const httpServer = (req: IncomingMessage, res: OutgoingMessage) => {
const marbleReq = req as HttpRequest;
@@ -101,7 +54,8 @@ export const httpListener = ({
marbleRes.send = handleResponse(marbleRes)(marbleReq);
marbleReq.response = marbleRes;
- return requestSubject$.next(marbleReq);
+ const subject = provideRoute(marbleReq);
+ subject.next(marbleReq);
};
httpServer.config = { routing };

MODIFY
packages/core/src/router/router.factory.ts
packages/core/src/router/router.factory.ts
@@ -36,7 +36,6 @@ export const factorizeRouting = (
middleware: middlewares.length
? combineMiddlewares(...[...middlewares, route.middleware].filter(isNonNullable))
: route.middleware,
- parameters,
};
if (foundRoute) {
@@ -50,6 +49,7 @@ export const factorizeRouting = (
return routing.push({
path,
regExp,
+ parameters,
methods: { [route.method]: method },
} as RoutingItem);
});

MODIFY
packages/core/src/router/router.interface.ts
packages/core/src/router/router.interface.ts
@@ -30,7 +30,6 @@ export interface ParametricRegExp {
}
export interface RoutingMethod {
- parameters?: string[] | undefined;
middleware?: HttpMiddlewareEffect | undefined;
effect: HttpEffect;
}
@@ -39,6 +38,7 @@ export interface RoutingItem {
regExp: RegExp;
path: string;
methods: Partial<Record<HttpMethod, RoutingMethod>>;
+ parameters?: string[] | undefined;
}
export interface RouteMatched {

MODIFY
packages/core/src/router/router.resolver.ts
packages/core/src/router/router.resolver.ts
@@ -14,7 +14,7 @@ export const findRoute = (
method: HttpMethod
): RouteMatched | undefined => {
for (let i = 0; i < routing.length; ++i) {
- const { regExp, methods, path } = routing[i];
+ const { regExp, methods, path, parameters } = routing[i];
const match = url.match(regExp);
if (!match) { continue; }
@@ -23,7 +23,7 @@ export const findRoute = (
if (!routingMethod) { continue; }
- const { parameters, effect, middleware } = routingMethod;
+ const { effect, middleware } = routingMethod;
const params = {};
if (parameters) {

ADD
packages/core/src/router/router.resolver.v2.ts
+import { EMPTY, Subject, zip } from 'rxjs';
+import { publish} from 'rxjs/operators';
+import { Routing } from './router.interface';
+import { EffectContext } from '../effects/effects.interface';
+import { HttpServer, HttpRequest, HttpMethod } from '../http.interface';
+import { queryParamsFactory } from './router.query.factory';
+import { HttpMiddlewareEffect } from '../effects/http-effects.interface';
+
+interface RoutingResolver {
+ (routing: Routing, ctx: EffectContext<HttpServer>): (middleware$: HttpMiddlewareEffect) =>
+ (req: HttpRequest) => Subject<HttpRequest<unknown, unknown, unknown>>;
+}
+
+interface BootstrappedRoutingItem {
+ regExp: RegExp;
+ path: string;
+ methods: Partial<Record<HttpMethod, Subject<HttpRequest>>>;
+ parameters?: string[] | undefined;
+}
+
+type BootstrappedRouting = BootstrappedRoutingItem[];
+
+export interface RouteMatched {
+ subject: Subject<HttpRequest>;
+ params: Record<string, string>;
+ path: string;
+}
+
+export const findRoute = (routing: BootstrappedRouting) => (url: string, method: HttpMethod): RouteMatched | undefined => {
+ for (let i = 0; i < routing.length; ++i) {
+ const { regExp, methods, path, parameters } = routing[i];
+ const match = url.match(regExp);
+
+ if (!match) { continue; }
+
+ const subject = methods[method] || methods['*'];
+
+ if (!subject) { continue; }
+
+ const params = {};
+
+ if (parameters) {
+ for (let p = 0; p < parameters.length; p++) {
+ params[parameters[p]] = decodeURIComponent(match[p + 1]);
+ }
+ }
+
+ return { subject, params, path };
+ }
+
+ return undefined;
+};
+
+export const resolveRouting: RoutingResolver = (routing, ctx) => (globalMiddleware$) => {
+ const bootstrappedRrouting: BootstrappedRoutingItem[] = routing.map(item => ({
+ ...item,
+ methods: Object.entries(item.methods).reduce((acc, [method, methodItem]) => {
+ if (!methodItem) return { [method]: undefined };
+
+ const req$ = new Subject<HttpRequest>();
+
+ const subject$ = req$.pipe(
+ publish(req$ => globalMiddleware$(req$, ctx)),
+ publish(req$ => methodItem.middleware ? methodItem.middleware(req$, ctx) : req$),
+ publish(req$ => zip(
+ methodItem.effect(req$, ctx),
+ req$,
+ )),
+ );
+
+ subject$.subscribe(([res, req]) => req.response.send(res));
+
+ return { ...acc, [method]: subject$ };
+ }, {}),
+ }));
+
+ const find = findRoute(bootstrappedRrouting);
+
+ return req => {
+ const [urlPath, urlQuery] = req.url.split('?');
+ // const path = (urlPath + '/').replace(/\/\/+/g, '/'); // @TODO: think of removing it
+
+ const resolvedRoute = find(urlPath, req.method);
+
+ if (!resolvedRoute) { return EMPTY as any as Subject<HttpRequest>; }
+
+ req.query = queryParamsFactory(urlQuery);
+ req.params = resolvedRoute.params;
+ req.meta = {};
+ req.meta.path = resolvedRoute.path;
+
+ return resolvedRoute.subject;
+ };
+};

MODIFY
packages/core/src/router/specs/router.factory.spec.ts
packages/core/src/router/specs/router.factory.spec.ts
@@ -54,23 +54,26 @@ describe('#factorizeRouting', () => {
{
regExp: /^\/([^\/]+?)(?:\/)?$/i,
path: '/:id',
+ parameters: ['id'],
methods: {
- GET: { middleware: expect.any(Function), effect: e2$, parameters: ['id'] },
- POST: { middleware: expect.any(Function), effect: e3$, parameters: ['id'] },
+ GET: { middleware: expect.any(Function), effect: e2$ },
+ POST: { middleware: expect.any(Function), effect: e3$ },
},
},
{
regExp: /^\/([^\/]+?)\/nested(?:\/)?$/i,
path: '/:id/nested',
+ parameters: ['id'],
methods: {
- GET: { middleware: expect.any(Function), effect: e5$, parameters: ['id'] },
+ GET: { middleware: expect.any(Function), effect: e5$ },
},
},
{
regExp: /^\/([^\/]+?)\/(.*)(?:\/)?$/i,
path: '/:id/(.*)',
+ parameters: ['id'],
methods: {
- '*': { middleware: expect.any(Function), effect: e4$, parameters: ['id'] },
+ '*': { middleware: expect.any(Function), effect: e4$ },
},
},
] as Routing);

MODIFY
packages/core/src/router/specs/router.resolver.spec.ts
packages/core/src/router/specs/router.resolver.spec.ts
@@ -52,7 +52,8 @@ describe('#findRoute', () => {
const routing: Routing = [{
regExp: /^\/group\/([^\/]+)\/foo$/,
path: '/group/:param/foo',
- methods: { GET: { effect: e$, parameters: ['param'] } }
+ parameters: ['param'],
+ methods: { GET: { effect: e$ } }
}];
// when

ADD
packages/core/src/router/specs/router.resolver.v2.spec.ts
+// import { mapTo, take } from 'rxjs/operators';
+// import { HttpEffect } from '../../effects/http-effects.interface';
+// import { Routing } from '../router.interface';
+// import { resolveRouting } from '../router.resolver.v2';
+// import { createMockEffectContext, createHttpRequest } from '../../+internal';
+// import { of, forkJoin } from 'rxjs';
+// import { factorizeRegExpWithParams } from '../router.params.factory';
+
+// describe('#findRoute', () => {
+// test('finds route inside collection', async (done) => {
+// // given
+// const ctx = createMockEffectContext();
+
+// const path1 = factorizeRegExpWithParams('/');
+// const path2 = factorizeRegExpWithParams('/group');
+// const path3 = factorizeRegExpWithParams('/group/nested/foo');
+
+// const e1$: HttpEffect = req$ => req$.pipe(mapTo({ body: 'test_1' }));
+// const e2$: HttpEffect = req$ => req$.pipe(mapTo({ body: 'test_2' }));
+// const e3$: HttpEffect = req$ => req$.pipe(mapTo({ body: 'test_3' }));
+// const e4$: HttpEffect = req$ => req$.pipe(mapTo({ body: 'test_4' }));
+
+// const req1 = createHttpRequest(({ url: '/', method: 'GET' }));
+// const req2 = createHttpRequest(({ url: '/', method: 'POST' }));
+// const req3 = createHttpRequest(({ url: '/group', method: 'GET' }));
+// const req4 = createHttpRequest(({ url: '/group/nested/foo', method: 'POST' }));
+
+// const routing: Routing = [
+// {
+// regExp: path1.regExp,
+// path: path1.path,
+// methods: { GET: { effect: e1$ }, POST: { effect: e2$ } },
+// },
+// {
+// regExp: path2.regExp,
+// path: path2.path,
+// methods: { GET: { effect: e3$ } },
+// },
+// {
+// regExp: path3.regExp,
+// path: path3.path,
+// methods: { POST: { effect: e4$ } },
+// },
+// ];
+
+// // when
+// const routing$ = resolveRouting(routing, ctx);
+// const e1Result = await routing$(of(req1)).toPromise();
+// const e2Result = await routing$(of(req2)).toPromise();
+// const e3Result = await routing$(of(req3)).toPromise();
+// const e4Result = await routing$(of(req4)).toPromise();
+
+// // then
+// forkJoin([
+// e1Result.subject.pipe(take(1)),
+// e2Result.subject.pipe(take(1)),
+// e3Result.subject.pipe(take(1)),
+// e4Result.subject.pipe(take(1)),
+// ]).subscribe(
+// result => {
+// expect(result[0]).toEqual({ body: 'test_1' });
+// expect(result[1]).toEqual({ body: 'test_2' });
+// expect(result[2]).toEqual({ body: 'test_3' });
+// expect(result[3]).toEqual({ body: 'test_4' });
+// done();
+// },
+// )
+
+// e1Result.subject.next(e1Result.req);
+// e2Result.subject.next(e2Result.req);
+// e3Result.subject.next(e3Result.req);
+// e4Result.subject.next(e4Result.req);
+// });
+// });

imp(corer): route resolving optimization