alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
178 lines (153 loc) • 5.37 kB
text/typescript
import type { Alepha } from "../Alepha.ts";
import { KIND } from "../constants/KIND.ts";
import { OPTIONS } from "../constants/OPTIONS.ts";
import { createPrimitive, Primitive } from "../helpers/primitive.ts";
import { $context } from "./$context.ts";
/**
* Creates a pipeline primitive that composes middleware with a handler.
*
* It makes a big function which runs all functions (middleware) inside `use` array before and after the `handler` function.
* The middleware functions are applied in the order they are defined in the `use` array.
*
* So PipelinePrimitive is also a base for primitives with `handler` which need some sort of plugins.
*
* Use `$pipeline` for standalone composition outside host primitives (`$action`, `$job`, `$page`).
* Host primitives extend `PipelinePrimitive` directly to get middleware support.
*
* ```ts
* class OrderService {
* processOrder = $pipeline({
* use: [$lock({ name: "process-order" }), $retry({ max: 3 })],
* handler: async (orderId: string) => {
* return await this.orders.updateById(orderId, { status: "paid" });
* },
* });
* }
* ```
*/
export function $pipeline<T extends (...args: any[]) => any>(
options: PipelineOptions<T>,
): PipelinePrimitiveFn<T> {
const instance = createPrimitive(PipelinePrimitive, options);
const fn = (...args: Parameters<T>): any => instance.run(...args);
return Object.setPrototypeOf(fn, instance) as PipelinePrimitiveFn<T>;
}
// -----------------------------------------------------------------------------------------------------------------
export class PipelinePrimitive<
TOptions extends PipelinePrimitiveOptions = PipelinePrimitiveOptions,
> extends Primitive<TOptions> {
public readonly handler = new PipelineHandler(
this.options.handler,
this.options.use,
);
public get middlewares(): MiddlewareMetadata[] {
return (this.options.use ?? []).map(
(it) => it[OPTIONS] as MiddlewareMetadata,
);
}
/**
* Execute the wrapped handler.
*/
public run(...args: any[]): any {
return this.handler.run(...args);
}
}
export interface MiddlewareMetadata {
name: string;
options?: Record<string, unknown>;
}
export type Middleware = (<T extends (...args: any[]) => any>(
handler: T,
) => T) & {
[OPTIONS]?: MiddlewareMetadata;
};
export interface PipelinePrimitiveOptions<
THandler extends (...args: any[]) => any = (...args: any[]) => any,
> {
use?: Middleware[];
handler: THandler;
}
export interface PipelineOptions<
T extends (...args: any[]) => any = (...args: any[]) => any,
> {
use: Middleware[];
handler: T;
}
export interface CreateMiddlewareOptions {
name: string;
options?: any;
handler: (context: {
alepha: Alepha;
next: (...args: any[]) => any;
}) => (...args: any[]) => any;
}
/**
* Internal class that composes middleware and the handler function.
* This is required when adding more middleware dynamically. See `$middleware()`.
*/
export class PipelineHandler {
protected fn: (...args: any[]) => any;
protected middlewares: Middleware[];
constructor(fn: (...args: any[]) => any, middlewares?: Middleware[]) {
this.fn = fn;
this.middlewares = middlewares ?? [];
}
protected wrapped?: (...args: any[]) => any;
public use(...middleware: Middleware[]): void {
this.middlewares.unshift(...middleware);
this.wrapped = undefined; // reset wrapped function to apply new middleware
}
public run(...args: any[]): any {
if (!this.wrapped) {
this.wrapped = this.fn;
for (let i = this.middlewares.length - 1; i >= 0; i--) {
this.wrapped = this.middlewares[i](this.wrapped);
}
}
return this.wrapped(...args);
}
}
// -----------------------------------------------------------------------------------------------------------------
export interface PipelinePrimitiveFn<
T extends (...args: any[]) => any = (...args: any[]) => any,
> extends PipelinePrimitive<PipelineOptions<T>> {
(...args: Parameters<T>): Promise<Awaited<ReturnType<T>>>;
}
$pipeline[KIND] = PipelinePrimitive;
// -----------------------------------------------------------------------------------------------------------------
/**
* Creates a middleware with automatic `$context()` resolution, type handling, and metadata.
*
* Eliminates the boilerplate of calling `$context()`, casting types, and attaching
* `MiddlewareMetadata`. The handler receives `{ alepha, next }` and returns the
* wrapped function directly.
*
* ```typescript
* export const $logElastic = (options?: LogOptions): Middleware => {
* return createMiddleware({
* name: "$logElastic",
* options,
* handler: ({ alepha, next }) => {
* const elastic = alepha.inject(ElasticProvider);
* return async (...args) => {
* const result = await next(...args);
* elastic.push({ ... }).catch(() => {});
* return result;
* };
* },
* });
* };
* ```
*/
export const createMiddleware = (
config: CreateMiddlewareOptions,
): Middleware => {
const { alepha } = $context();
const mw: Middleware = <T extends (...args: any[]) => any>(handler: T): T =>
config.handler({ alepha, next: handler }) as T;
mw[OPTIONS] =
config.options != null
? { name: config.name, options: config.options }
: { name: config.name };
return mw;
};