Skip to content

Commit

Permalink
feat: rework registries, instantiate constructors every time
Browse files Browse the repository at this point in the history
  • Loading branch information
Zamralik committed May 17, 2024
1 parent 837bf93 commit 3b9ddc2
Show file tree
Hide file tree
Showing 19 changed files with 433 additions and 315 deletions.
2 changes: 1 addition & 1 deletion packages/architectura/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@vitruvius-labs/architectura",
"version": "0.9.0",
"version": "0.10.0",
"description": "A light weight strongly typed Node.JS framework providing isolated context for each request.",
"author": {
"name": "VitruviusLabs"
Expand Down
1 change: 1 addition & 0 deletions packages/architectura/src/core/endpoint/_index.mts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./definition/_index.mjs";
export * from "./base.endpoint.mjs";
export * from "./endpoint.registry.mjs";
48 changes: 1 addition & 47 deletions packages/architectura/src/core/endpoint/base.endpoint.mts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ import type { BasePreHook } from "../hook/base.pre-hook.mjs";
import type { BasePostHook } from "../hook/base.post-hook.mjs";
import type { BaseErrorHook } from "../hook/base.error-hook.mjs";
import type { ExecutionContext } from "../execution-context/execution-context.mjs";
import { Singleton } from "../../utility/singleton.mjs";

abstract class BaseEndpoint extends Singleton
abstract class BaseEndpoint
{
protected abstract readonly method: HTTPMethodEnum;
protected abstract readonly route: RegExp | string;
Expand All @@ -15,54 +14,9 @@ abstract class BaseEndpoint extends Singleton
protected readonly excludedGlobalPostHooks: Array<typeof BasePostHook> = [];
protected readonly errorHooks: Array<BaseErrorHook> = [];
protected readonly excludedGlobalErrorHooks: Array<typeof BaseErrorHook> = [];
private normalizedRoute: RegExp | undefined = undefined;

private static NormalizeRoute(route: RegExp | string): RegExp
{
if (route instanceof RegExp)
{
return new RegExp(BaseEndpoint.MakeRouteWhole(route.source), route.flags);
}

return new RegExp(BaseEndpoint.MakeRouteWhole(route));
}

private static MakeRouteWhole(route: string): string
{
let pattern: string = route;

if (!route.startsWith("^"))
{
pattern = `^${pattern}`;
}

if (!route.endsWith("$"))
{
pattern = `${pattern}$`;
}

return pattern;
}

public abstract execute(context: ExecutionContext): Promise<void> | void;

/** @sealed */
public getMethod(): HTTPMethodEnum
{
return this.method;
}

/** @sealed */
public getRoute(): RegExp
{
if (this.normalizedRoute === undefined)
{
this.normalizedRoute = BaseEndpoint.NormalizeRoute(this.route);
}

return this.normalizedRoute;
}

public getPreHooks(): Array<BasePreHook>
{
return this.preHooks;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./interface/endpoint-entry.interface.mjs";
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { ConstructorOf } from "@vitruvius-labs/ts-predicate";
import type { HTTPMethodEnum } from "../../../definition/enum/http-method.enum.mjs";
import type { BaseEndpoint } from "../../base.endpoint.mjs";

/** @internal */
interface EndpointEntryInterface
{
method: HTTPMethodEnum;
route: RegExp;
endpoint: BaseEndpoint | ConstructorOf<BaseEndpoint>;
}

export type { EndpointEntryInterface };
142 changes: 89 additions & 53 deletions packages/architectura/src/core/endpoint/endpoint.registry.mts
Original file line number Diff line number Diff line change
@@ -1,55 +1,98 @@
import type { Dirent } from "node:fs";
import type { HTTPMethodEnum } from "../definition/enum/http-method.enum.mjs";
import type { EndpointEntryInterface } from "./definition/interface/endpoint-entry.interface.mjs";
import { isFunction, isRecord, isString } from "@vitruvius-labs/ts-predicate/type-guard";
import { type ConstructorOf, getConstructorOf } from "@vitruvius-labs/ts-predicate/helper";
import { HelloWorldEndpoint } from "../../endpoint/hello-world.endpoint.mjs";
import { FileSystemService } from "../../service/file-system/file-system.service.mjs";
import { LoggerProxy } from "../../service/logger/logger.proxy.mjs";
import { BaseEndpoint } from "./base.endpoint.mjs";
import { isHTTPMethodEnum } from "../predicate/is-http-method-enum.mjs";
import { Singleton } from "../../utility/singleton.mjs";

class EndpointRegistry
{
private static readonly ENDPOINTS: Map<string, BaseEndpoint> = new Map();
private static readonly ENDPOINTS: Map<string, EndpointEntryInterface> = new Map();

public static GetEndpoints(): ReadonlyMap<string, BaseEndpoint>
public static FindEndpoint(request_method: HTTPMethodEnum, request_path: string): BaseEndpoint | undefined
{
if (this.ENDPOINTS.size === 0)
{
LoggerProxy.Warning("No endpoints have been added. Default endpoint.");
LoggerProxy.Warning("No endpoint have been added. Default endpoint.");

const MAP: Map<string, BaseEndpoint> = new Map();

let endpoint: HelloWorldEndpoint | undefined = Singleton.FindInstance(HelloWorldEndpoint);
return new HelloWorldEndpoint();
}

if (endpoint === undefined)
for (const [, ENDPOINT_ENTRY] of this.ENDPOINTS)
{
if (ENDPOINT_ENTRY.method === request_method && ENDPOINT_ENTRY.route.test(request_path))
{
endpoint = new HelloWorldEndpoint();
}

MAP.set("GET::/^.*$/", endpoint);
if (ENDPOINT_ENTRY.endpoint instanceof BaseEndpoint)
{
return ENDPOINT_ENTRY.endpoint;
}

return MAP;
return new ENDPOINT_ENTRY.endpoint();
}
}

return this.ENDPOINTS;
return undefined;
}

public static AddEndpoint(endpoint: BaseEndpoint): void
public static AddEndpoint(endpoint: BaseEndpoint | ConstructorOf<BaseEndpoint>): void
{
const METHOD: string = endpoint.getMethod();
const ROUTE: string = endpoint.getRoute().toString();
let constructor_class: ConstructorOf<BaseEndpoint> | undefined = undefined;
let instance: BaseEndpoint | undefined = undefined;

if (endpoint instanceof BaseEndpoint)
{
constructor_class = getConstructorOf(endpoint);
instance = endpoint;
}
else
{
constructor_class = endpoint;
instance = new endpoint();
}

const METHOD: unknown = Reflect.get(instance, "method");
const ROUTE: unknown = Reflect.get(instance, "route");

if (!isHTTPMethodEnum(METHOD))
{
throw new Error(`${constructor_class.name} method must be an HTTPMethodEnum.`);
}

if (!isString(ROUTE) && !(ROUTE instanceof RegExp))
{
throw new Error(`${constructor_class.name} route must be a string or RegExp.`);
}

const IDENTIFIER: string = `${METHOD}::${ROUTE}`;
const NORMALIZED_ROUTE: RegExp = this.NormalizeRoute(ROUTE);

if (this.ENDPOINTS.has(IDENTIFIER))
const IDENTIFIER: string = `${METHOD}::${ROUTE.toString()}`;

const ENTRY: EndpointEntryInterface | undefined = this.ENDPOINTS.get(IDENTIFIER);

if (ENTRY !== undefined)
{
throw new Error(`An endpoint is already added for method ${METHOD} and route "${ROUTE}".`);
if (ENTRY.endpoint === endpoint)
{
throw new Error(`Endpoint ${constructor_class.name} already added.`);
}

throw new Error(`An endpoint is already added for method ${METHOD} and route "${NORMALIZED_ROUTE.toString()}".`);
}

LoggerProxy.Debug(`Endpoint added ${METHOD} ${ROUTE}.`);
LoggerProxy.Debug(`Endpoint added ${METHOD} ${NORMALIZED_ROUTE.toString()}.`);

this.ENDPOINTS.set(IDENTIFIER, endpoint);
this.ENDPOINTS.set(
IDENTIFIER,
{
method: METHOD,
route: NORMALIZED_ROUTE,
endpoint: endpoint,
}
);
}

public static async AddEndpointsDirectory(directory: string): Promise<void>
Expand All @@ -65,18 +108,18 @@ class EndpointRegistry

for (const ENTITY of ENTITIES)
{
const FILE_PATH: string = `${directory}/${ENTITY.name}`;
const ENTITY_PATH: string = `${directory}/${ENTITY.name}`;

if (ENTITY.isDirectory())
{
await this.ParseDirectoryForEndpoints(FILE_PATH);
await this.ParseDirectoryForEndpoints(ENTITY_PATH);

continue;
}

if (ENTITY.isFile() && ENTITY.name.includes(".endpoint."))
{
await this.ExtractEndpoint(FILE_PATH);
await this.ExtractEndpoint(ENTITY_PATH);
}
}
}
Expand All @@ -87,55 +130,48 @@ class EndpointRegistry

if (isRecord(EXPORTS))
{
for (let [, endpoint] of Object.entries(EXPORTS))
for (const [, EXPORT] of Object.entries(EXPORTS))
{
if (this.IsEndpointConstructor(endpoint))
{
endpoint = new endpoint();
}

if (this.IsConcreteEndpoint(endpoint))
if (this.IsEndpoint(EXPORT))
{
this.AddEndpoint(endpoint);
this.AddEndpoint(EXPORT);

return;
}
}
}
}

private static IsEndpointConstructor(value: unknown): value is ConstructorOf<BaseEndpoint>
private static NormalizeRoute(route: RegExp | string): RegExp
{
return isFunction(value) && value.prototype instanceof BaseEndpoint;
}

private static IsConcreteEndpoint(value: unknown): value is BaseEndpoint
{
if (!(value instanceof BaseEndpoint))
if (route instanceof RegExp)
{
return false;
return new RegExp(this.MakeRouteWhole(route.source), route.flags);
}

const method: unknown = Reflect.get(value, "method");
const route: unknown = Reflect.get(value, "route");
return new RegExp(this.MakeRouteWhole(route));
}

if (method === undefined && route === undefined)
{
// Abstract class
return false;
}
private static MakeRouteWhole(route: string): string
{
let pattern: string = route;

if (!isHTTPMethodEnum(method))
if (!route.startsWith("^"))
{
throw new Error(`${getConstructorOf(value).name} method must be an HTTPMethodEnum.`);
pattern = `^${pattern}`;
}

if (!isString(route) && !(route instanceof RegExp))
if (!route.endsWith("$"))
{
throw new Error(`${getConstructorOf(value).name} route must be a string or RegExp.`);
pattern = `${pattern}$`;
}

return true;
return pattern;
}

private static IsEndpoint(value: unknown): value is BaseEndpoint | ConstructorOf<BaseEndpoint>
{
return value instanceof BaseEndpoint || isFunction(value) && value.prototype instanceof BaseEndpoint;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,9 @@ abstract class ExecutionContextRegistry
{
private static readonly ContextAccessor: AsyncLocalStorage<ExecutionContext> = new AsyncLocalStorage();

public static GetContextAccessor(): AsyncLocalStorage<ExecutionContext>
{
return this.ContextAccessor;
}

public static GetUnsafeExecutionContext(): ExecutionContext | undefined
{
return this.GetContextAccessor().getStore();
return this.ContextAccessor.getStore();
}

public static GetExecutionContext(): ExecutionContext
Expand All @@ -29,7 +24,7 @@ abstract class ExecutionContextRegistry

public static SetExecutionContext(execution_context: ExecutionContext): void
{
this.GetContextAccessor().enterWith(execution_context);
this.ContextAccessor.enterWith(execution_context);
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/architectura/src/core/hook/_index.mts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./base.pre-hook.mjs";
export * from "./base.post-hook.mjs";
export * from "./base.error-hook.mjs";
export * from "./hook.registry.mjs";
Loading

0 comments on commit 3b9ddc2

Please sign in to comment.