'use strict'; /** * Creates a pipeline for middleware processing. * * @param middlewares - The middleware functions to be executed in order. * @returns A Pipeline instance with push and execute methods. * @link https://muniftanjim.dev/blog/basic-middleware-pattern-in-javascript/ * @link https://github.com/koajs/compose/blob/master/index.js#L31-L47 */ function createPipeline(...middlewares) { const stack = middlewares; const push = (...middlewares) => { stack.push(...middlewares); }; const execute = async (context) => { let prevIndex = -1; const runner = async (index) => { if (index === prevIndex) { throw new Error('next() called multiple times'); } prevIndex = index; const middleware = stack[index]; if (middleware) { await middleware(context, () => runner(index + 1)); } }; await runner(0); }; return { push, execute }; } /** * A type-safe event controller for a single Zod schema validated event. * * Supports adding, emitting, and removing the event listener, as well as middleware for event processing. */ class EventController { schema; eventName; element; pipeline; condition; conditionCallback; eventListener; onError; onDispatch; onSubscribe; onUnsubscribe; /** * Creates a new {@link EventController} instance. * * @param schema - The Zod schema for validating the event payload. * @param eventName - The name of the custom event. * @param options - Configuration options for the {@link EventController}. * * @example * ```ts * import { z } from "zod"; * import { EventController } from "zod-custom-events"; * * const schema = z.object({ * name: z.string(), * }); * * const controller = new EventController(schema, "myEvent", { * onError: ({ error }) => console.error("Validation error:", error), * onDispatch: ({ payload }) => console.log("Dispatching event with payload:", payload), * }); * ``` */ constructor(schema, eventName, options = {}) { this.schema = schema; this.eventName = eventName; this.pipeline = createPipeline(); this.element = options.element || window; this.onError = options.onError; this.onDispatch = options.onDispatch; this.onSubscribe = options.onSubscribe; this.onUnsubscribe = options.onUnsubscribe; } /** * Adds an event listener for the event. * * @param listener - The function to be called when the event is triggered. * @param options - Optional parameters for the event listener. * * @example * ```ts * controller.subscribe((event) => { * console.log("Received user event:", event.detail); * }); * ``` */ subscribe(listener, options) { const eventListener = (event) => listener(event); this.onSubscribe?.(); this.element.addEventListener(this.eventName, eventListener, options); this.eventListener = eventListener; } /** * Removes the previously registered event listener for the event. * * @param options - Optional parameters to match the event listener. * * @example * ```ts * controller.unsubscribe(); * ``` */ unsubscribe(options) { if (this.eventListener) { this.element.removeEventListener(this.eventName, this.eventListener, options); this.onUnsubscribe?.(); this.eventListener = undefined; } } /** * Dispatches the event, validating its payload using the Zod schema and applying middleware. * * @param payload - The data associated with the event. * @param eventInitDict - Optional parameters for initializing the event. * @throws {Error} If the payload is invalid and no onError callback is provided. * * @example * ```ts * controller.dispatch({ * id: 1, * name: "John Doe", * }); * ``` */ async dispatch(payload, eventInitDict = {}) { const validation = this.schema.safeParse(payload); if (!validation.success) { if (this.onError) { this.onError({ error: validation.error }); } else { throw new Error(validation.error.message); } return; } if (this.condition && !this.condition(payload)) { if (this.conditionCallback) { this.conditionCallback({ payload }); } return; } const ctx = { payload }; await this.pipeline.execute(ctx); this.onDispatch?.(ctx); const event = new CustomEvent(this.eventName, { detail: payload, bubbles: eventInitDict.bubbles ?? false, cancelable: eventInitDict.cancelable, }); this.element.dispatchEvent(event); } /** * Sets a condition for the event, allowing control over whether the event is dispatched. * * @param condition - A function that takes the event payload as input and returns a boolean. * @param callback - An optional callback function to be called when the condition is not met. * * @example * ```ts * controller.refine( * (payload) => payload.id > 0, * (ctx) => { * const { payload } = ctx; * console.log("Invalid user ID:", payload.id) * } * ); * ``` */ refine(condition, callback) { this.condition = condition; this.conditionCallback = callback; } /** * Updates the {@link EventController} options. * * @param options - New configuration options for the EventController. * * @example * ```ts * const controller = new EventController(schema, "myEvent"); * * // Later in the code * controller.update({ * onError: ({ error }) => console.warn("New error handler:", error), * onDispatch: ({ payload }) => console.log("New dispatch handler:", payload), * }); * ``` */ update(options) { Object.assign(this, options); } /** * Adds a middleware function to the event processing pipeline. * * Must call the `next()` function. * * @param middleware - A function that processes the event context and calls the next middleware. * * @example * ```ts * controller.use(async (ctx, next) => { * console.log("Processing user event:", ctx.payload); * await next(); * console.log("User event processed"); * }); * ``` */ use(middleware) { this.pipeline.push(middleware); } } exports.EventController = EventController;