Skip to content

Commit

Permalink
feat: ws package
Browse files Browse the repository at this point in the history
  • Loading branch information
izatop committed Jan 19, 2021
1 parent a5085f7 commit 0d39ce1
Show file tree
Hide file tree
Showing 85 changed files with 1,744 additions and 524 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ lerna-*.log
npm-*.log
.DS_Store
tsconfig.tsbuildinfo
test.js
11 changes: 6 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,16 @@
"@commitlint/cli": "^11.0.0",
"@commitlint/config-conventional": "^11.0.0",
"@types/jest": "^26.0.20",
"@types/node": "^14.14.20",
"@typescript-eslint/eslint-plugin": "^4.13.0",
"@typescript-eslint/parser": "^4.13.0",
"@types/node": "^14.14.21",
"@typescript-eslint/eslint-plugin": "^4.14.0",
"@typescript-eslint/parser": "^4.14.0",
"cross-env": "^7.0.3",
"eslint": "^7.17.0",
"husky": "^4.3.7",
"eslint": "^7.18.0",
"husky": "^4.3.8",
"jest": "^26.6.3",
"lerna": "^3.22.1",
"ts-jest": "^26.4.4",
"ts-node": "^9.1.1",
"typescript": "^4.1.3"
},
"resolutions": {
Expand Down
89 changes: 35 additions & 54 deletions packages/app/src/Application.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import {Context, ContextArg, unit, Unit} from "@bunt/unit";
import {assert, isDefined, isInstanceOf, logger, Logger} from "@bunt/util";
import {IRequest, MatchRoute, RouteResponse} from "./interfaces";
import {ApplyContext, Context, ContextArg, IContext, unit, Unit} from "@bunt/unit";
import {assert, isDefined, logger, Logger} from "@bunt/util";
import {IRequestMessage, MatchRoute, RouteResponse} from "./interfaces";
import {IRoute, RouteNotFound} from "./Route";

export class Application<U extends Unit<C>, C> {
export class Application<C extends IContext = any> {
@logger
public logger!: Logger;
protected logger!: Logger;

protected readonly unit: U;
protected readonly unit: Unit<C>;
protected readonly route: IRoute[] = [];

constructor(u: U) {
constructor(u: Unit<C>, routes: MatchRoute<C, IRoute>[] = []) {
this.unit = u;

if (routes.length > 0) {
routes.forEach((route) => this.add(route));
}
}

public get context(): C {
return this.unit.context;
}

public get size(): number {
Expand All @@ -20,13 +28,8 @@ export class Application<U extends Unit<C>, C> {

public static async factory<C extends Context>(
context: ContextArg<C>,
routes: MatchRoute<C, IRoute>[] = []): Promise<Application<Unit<C>, C>> {
const app = new this<Unit<C>, C>(await unit(context));
if (routes.length > 0) {
routes.forEach((route) => app.add(route));
}

return app;
routes: MatchRoute<C, IRoute>[] = []): Promise<Application<ApplyContext<C>>> {
return new this(await unit<C>(context), routes);
}

public add<R extends IRoute>(route: MatchRoute<C, R>): this {
Expand All @@ -48,49 +51,27 @@ export class Application<U extends Unit<C>, C> {
return this;
}

public async handle<R extends IRequest>(request: R): Promise<void> {
const finish = this.logger.perf("handle", request);

try {
assert(request.validate(), "Invalid Request");
await request.respond(await this.run(request));
} catch (error) {
if (!request.complete) {
await request.respond(error);
}

if (isInstanceOf(error, Error)) {
throw error;
}
} finally {
finish();
}
}
public async run<R extends IRequestMessage>(request: R): Promise<RouteResponse> {
const route = this.route.find((route) => route.test(request.route));
assert(route, () => new RouteNotFound(request.route));

this.logger.debug("match", route);

const unit = this.unit;
const state: Record<string, any> = {};
const matches = route.match(request.route);
const routeContext = {
request,
context: unit.context,
args: new Map<string, string>(Object.entries(matches)),
};

public async run<R extends IRequest>(request: R): Promise<RouteResponse> {
for (const route of this.route) {
if (route.test(request.route)) {
this.logger.debug("match", route);

const state: Record<string, any> = {};
const context = await this.unit.getContext();
const matches = route.match(request.route);
const routeContext = {
request,
context,
args: new Map<string, string>(Object.entries(matches)),
};

if (isDefined(route.payload)) {
const {payload} = route;
Object.assign(state, await payload.validate(routeContext));
}

return this.unit.run(route.action, state);
}
if (isDefined(route.payload)) {
const {payload} = route;
Object.assign(state, await payload.validate(routeContext));
}

throw new RouteNotFound(request.route);
return unit.run(route.action, state);
}

public getRoutes(): IRoute[] {
Expand Down
5 changes: 2 additions & 3 deletions packages/app/src/Route/RouteRule.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import {FieldSelectType} from "@bunt/input";
import {ActionState} from "@bunt/unit";
import {RouteAction} from "../interfaces";
import {Action, ActionState} from "@bunt/unit";
import {Payload, Resolver} from "../Payload";

export class RouteRule<A extends RouteAction> extends Payload<A> {
export class RouteRule<A extends Action<any, any>> extends Payload<A> {
public readonly route: string;

constructor(route: string, type: FieldSelectType<ActionState<A>>, resolver: Resolver<A>) {
Expand Down
6 changes: 3 additions & 3 deletions packages/app/src/Route/interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {Action, ActionCtor, IContext} from "@bunt/unit";
import {IRequest, RouteAction} from "../interfaces";
import {IRequestMessage, RouteAction} from "../interfaces";
import {Payload} from "../Payload";
import {Route} from "./Route";
import {RouteRule} from "./RouteRule";
Expand All @@ -18,15 +18,15 @@ export interface IRoute<A extends RouteAction = RouteAction> {

export type RouteMatcherFactory = IRouteMatcher | ((route: string) => IRouteMatcher);

export type RouteRuleArg<A extends RouteAction> = A extends Action<IContext, infer S>
export type RouteRuleArg<A extends Action<any, any>> = A extends Action<any, infer S>
? S extends null ? string : RouteRule<A>
: string;

export type RouteFactory = <A extends RouteAction>(action: ActionCtor<A>, rule: RouteRuleArg<A>) => Route<A>;

export interface IRouteContext<C extends IContext> {
context: C;
request: IRequest;
request: IRequestMessage;
args: Map<string, string>;
}

Expand Down
4 changes: 2 additions & 2 deletions packages/app/src/Transport/Request/JSONTransform.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {IRequest, IRequestTransform} from "../../interfaces";
import {IRequestMessage, IRequestTransform} from "../../interfaces";

export const JSONTransform = async <T>(request: IRequest): Promise<T> => {
export const JSONTransform = async <T>(request: IRequestMessage): Promise<T> => {
request.headers.assert("content-type", ["application/json"]);
const buffer = await request.getBuffer();
return JSON.parse(buffer.toString("utf-8"));
Expand Down
4 changes: 2 additions & 2 deletions packages/app/src/Transport/Request/URLEncodedFormTransform.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {parse, ParsedUrlQuery} from "querystring";
import {IRequest} from "../../interfaces";
import {IRequestMessage} from "../../interfaces";

export const URLEncodedFormTransform = async (request: IRequest): Promise<ParsedUrlQuery> => {
export const URLEncodedFormTransform = async (request: IRequestMessage): Promise<ParsedUrlQuery> => {
request.headers.assert("Content-Type", "application/x-www-form-urlencoded");
const buffer = await request.getBuffer();
return parse(buffer.toString("utf-8"));
Expand Down
63 changes: 5 additions & 58 deletions packages/app/src/Transport/RequestAbstract.ts
Original file line number Diff line number Diff line change
@@ -1,61 +1,8 @@
import {Promisify} from "@bunt/unit";
import {assert, ILogable, isFunction} from "@bunt/util";
import {IHeaders, IRequest, IRequestTransform, RequestTransformType, RouteResponse} from "../interfaces";
import {fromJsonRequest} from "./Request";
import {ResponderAbstract} from "./ResponderAbstract";

export abstract class RequestAbstract implements IRequest, ILogable<{ route: string }> {
public abstract readonly route: string;
public abstract readonly headers: IHeaders;
/**
* @deprecated see ResponderAbstract
*/
export abstract class RequestAbstract extends ResponderAbstract {

#complete = false;

public get complete(): boolean {
return this.#complete;
}

public async getBuffer(): Promise<Buffer> {
const chunks: Buffer[] = [];
const readableStream = await this.createReadableStream();
for await (const chunk of readableStream) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
}

return Buffer.concat(chunks);
}

public transform<T>(transformer: RequestTransformType<T>): Promise<T> {
if (isFunction(transformer)) {
return transformer(this);
}

return transformer.transform(this);
}

public async to<T>(transform: IRequestTransform<T>): Promise<T> {
this.headers.assert("content-type", [transform.type].flat(1));
return transform(await this.getBuffer());
}

public toObject<T = unknown>(): Promise<T> {
return this.to<unknown>(fromJsonRequest) as Promise<T>;
}

public async respond(response: RouteResponse): Promise<void> {
assert(!this.complete, `Response was already sent`);
try {
await this.write(response);
} finally {
this.#complete = true;
}
}

public abstract validate(): boolean;

public abstract createReadableStream(): Promisify<NodeJS.ReadableStream>;

public getLogValue(): { route: string } {
return {route: this.route};
}

protected abstract write(response: RouteResponse): Promise<void>;
}
44 changes: 44 additions & 0 deletions packages/app/src/Transport/RequestMessageAbstract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {ILogable, isFunction, Promisify} from "@bunt/util";
import {Application} from "../Application";
import {IHeaders, IRequestMessage, IRequestTransform, RequestTransformType} from "../interfaces";
import {fromJsonRequest} from "./Request";

export abstract class RequestMessageAbstract implements IRequestMessage, ILogable<{ route: string }> {
public abstract readonly route: string;
public abstract readonly headers: IHeaders;

public async getBuffer(): Promise<Buffer> {
const chunks: Buffer[] = [];
const readableStream = await this.createReadableStream();
for await (const chunk of readableStream) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
}

return Buffer.concat(chunks);
}

public transform<T>(transformer: RequestTransformType<T>): Promise<T> {
if (isFunction(transformer)) {
return transformer(this);
}

return transformer.transform(this);
}

public async to<T>(transform: IRequestTransform<T>): Promise<T> {
this.headers.assert("content-type", [transform.type].flat(1));
return transform(await this.getBuffer());
}

public toObject<T = unknown>(): Promise<T> {
return this.to<unknown>(fromJsonRequest) as Promise<T>;
}

public abstract validate(app: Application): boolean;

public abstract createReadableStream(): Promisify<NodeJS.ReadableStream>;

public getLogValue(): { route: string } {
return {route: this.route};
}
}
5 changes: 3 additions & 2 deletions packages/app/src/Transport/RequestValidatorAbstract.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {IRequest} from "../interfaces";
import {Application} from "../Application";
import {IRequestMessage} from "../interfaces";

export abstract class RequestValidatorAbstract<T extends Record<string, any>> {
protected readonly options: T;
Expand All @@ -7,5 +8,5 @@ export abstract class RequestValidatorAbstract<T extends Record<string, any>> {
this.options = options;
}

public abstract validate(request: IRequest): void;
public abstract validate(app: Application, request: IRequestMessage): void;
}
22 changes: 22 additions & 0 deletions packages/app/src/Transport/ResponderAbstract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {assert} from "@bunt/util";
import {IResponder, RouteResponse} from "../interfaces";
import {RequestMessageAbstract} from "./RequestMessageAbstract";

export abstract class ResponderAbstract extends RequestMessageAbstract implements IResponder {
#complete = false;

public get complete(): boolean {
return this.#complete;
}

public async respond(response: RouteResponse): Promise<void> {
assert(!this.complete, `Response was already sent`);
try {
await this.write(response);
} finally {
this.#complete = true;
}
}

protected abstract write(response: RouteResponse): Promise<void>;
}
1 change: 1 addition & 0 deletions packages/app/src/Transport/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export * from "./KeyValueMap";
export * from "./KeyValueReadonlyMap";
export * from "./HeadersAbstract";
export * from "./RequestAbstract";
export * from "./RequestMessageAbstract";
export * from "./RequestValidatorAbstract";
Loading

0 comments on commit 0d39ce1

Please sign in to comment.