@wroud/flow-middleware
Version:
A lightweight middleware management library for JavaScript and TypeScript, facilitating middleware chains with re-runs, error handling, and disposability.
169 lines • 6 kB
JavaScript
export class MiddlewareRequest {
middlewares;
errorMiddlewares;
data;
logger;
middlewareStates;
isDisposed;
scheduledReRun;
isRunning;
constructor(middlewares, errorMiddlewares, data, logger) {
this.middlewares = middlewares;
this.errorMiddlewares = errorMiddlewares;
this.data = data;
this.logger = logger;
this.middlewares = middlewares;
this.middlewareStates = new Map();
this.isDisposed = false;
this.scheduledReRun = false;
this.isRunning = false;
}
use(...middleware) {
this.middlewares.push(...middleware);
return this;
}
error(...errorMiddleware) {
this.errorMiddlewares.push(...errorMiddleware);
return this;
}
async execute(error) {
if (this.isDisposed) {
throw new Error("Cannot execute a disposed request.");
}
const executedMiddlewares = new Set();
try {
this.isRunning = true;
if (error !== undefined) {
throw error;
}
await this.executeNext(executedMiddlewares, 0);
}
catch (error) {
await this.handleNextError(executedMiddlewares, 0, error);
}
finally {
await this.cleanupSubscriptions(executedMiddlewares);
this.isRunning = false;
if (this.scheduledReRun) {
this.scheduledReRun = false;
await this.execute();
}
}
}
async executeNext(executedMiddlewares, nextIndex) {
if (nextIndex >= this.middlewares.length) {
return;
}
const middleware = this.middlewares[nextIndex];
let currentState = this.middlewareStates.get(middleware);
if (!currentState) {
currentState = new Map();
this.middlewareStates.set(middleware, currentState);
}
this.markMiddlewareSubscriptionsInactive(currentState);
await middleware(this.data, () => this.executeNext(executedMiddlewares, nextIndex + 1), this.triggerReRun.bind(this), subscribe.bind(null, currentState));
executedMiddlewares.add(middleware);
}
async triggerReRun(error) {
this.logger?.info("Re-run triggered.");
if (this.isRunning) {
this.scheduledReRun = true;
}
else {
await this.execute(error);
}
}
async handleNextError(executedMiddlewares, nextIndex, error) {
if (nextIndex >= this.errorMiddlewares.length) {
this.logger?.error("Unhandled error:", error);
throw error;
}
try {
const errorMiddleware = this.errorMiddlewares[nextIndex];
let currentState = this.middlewareStates.get(errorMiddleware);
if (!currentState) {
currentState = new Map();
this.middlewareStates.set(errorMiddleware, currentState);
}
await errorMiddleware(error, this.data, () => this.handleNextError(executedMiddlewares, nextIndex + 1, error), this.triggerReRun.bind(this), subscribe.bind(null, currentState));
executedMiddlewares.add(errorMiddleware);
}
catch (error) {
await this.handleNextError(executedMiddlewares, nextIndex + 1, error);
}
}
markMiddlewareSubscriptionsInactive(state) {
for (const middlewareState of state.values()) {
middlewareState.active = false;
}
}
async cleanupSubscriptions(executedMiddlewares) {
for (const [middleware, state] of this.middlewareStates.entries()) {
if (executedMiddlewares.has(middleware)) {
for (const [key, subscription] of state) {
if (!subscription.active) {
await subscription.unsubscribe();
state.delete(key);
}
}
}
else {
this.disposeMiddlewareSubscriptions(middleware, state);
}
}
}
disposeMiddlewareSubscriptions(middleware, state) {
this.logger?.info(`Disposing subscriptions for middleware: ${middleware.name || "anonymous"}`);
for (const [key, { unsubscribe }] of state) {
try {
unsubscribe();
this.logger?.info(`Unsubscribed from ${key}.`);
}
catch (error) {
this.logger?.error(`Error unsubscribing from ${key}:`, error);
}
}
state.clear();
}
dispose() {
if (this.isDisposed) {
return;
}
this.isDisposed = true;
this.logger?.info("Disposing all subscriptions and request.");
for (const [middleware, state] of this.middlewareStates) {
this.disposeMiddlewareSubscriptions(middleware, state);
}
this.middlewareStates.clear();
}
}
function areDependenciesEqual(depsA, depsB) {
if (depsA === undefined && depsB === undefined)
return true;
if (depsA === undefined || depsB === undefined)
return false;
if (depsA.length === 0 && depsB.length === 0)
return true;
if (depsA.length !== depsB.length)
return false;
for (let i = 0; i < depsA.length; i++) {
if (depsA[i] !== depsB[i])
return false;
}
return true;
}
async function subscribe(state, key, subscribeFn, dependencies) {
const existingSubscription = state.get(key);
if (existingSubscription &&
areDependenciesEqual(existingSubscription.dependencies, dependencies)) {
existingSubscription.active = true;
return;
}
if (existingSubscription) {
await existingSubscription.unsubscribe();
state.delete(key);
}
const unsubscribe = await subscribeFn();
state.set(key, { dependencies, unsubscribe, active: true });
}
//# sourceMappingURL=MiddlewareRequest.js.map