@zudojs/events 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  Event-driven architecture with event bus, emitter, middleware, and registry for decoupled communication.
4
4
 
5
+ <!-- zudo-docs:start -->
6
+
7
+ **Documentation:** [zudojs.oyinlola.site/docs/packages-events](https://zudojs.oyinlola.site/docs/packages-events) · **For AI agents:** [Markdown version](https://zudojs.oyinlola.site/docs/packages-events.md), [llms.txt](https://zudojs.oyinlola.site/llms.txt)
8
+
9
+ <!-- zudo-docs:end -->
10
+
5
11
  ## Installation
6
12
 
7
13
  ```bash
@@ -46,8 +52,9 @@ await bus.publishEvent({
46
52
  - Handler priorities, one-time handlers, per-handler timeouts
47
53
  - Wildcard subscriptions (`"user.*"`, `"*"`)
48
54
  - Event registry for typed definitions; handlers registered on the registry are dispatched by the bus
49
- - Deep-frozen events (`freezeEvents`, on by default) so handlers cannot alter what other handlers see
50
- - Listener-leak warnings (`maxListeners`) and an `onError` hook for fire-and-forget publishes
55
+ - Deep-frozen events (`freezeEvents`, on by default) so handlers cannot alter what other handlers see: handlers receive a frozen *copy* (`createFrozenEventSnapshot`), so the publisher's own objects are never frozen, and Map, Set and Date values (including `event.timestamp`) become read-only variants that throw on mutation. Class instances are passed by reference.
56
+ - A handler unsubscribed by an earlier handler in the same dispatch, or left over after the bus is disposed mid-dispatch, does not run
57
+ - Listener-leak warnings (`maxListeners`, reported through `onWarning` or, by default, `process.emitWarning` with type `ZudojsEventsWarning`) and an `onError` hook for fire-and-forget publishes
51
58
  - Typed error classes from `@zudojs/errors` (`EventHandlerError`, `EventMiddlewareError`, `EventDispatchAbortedError`, …)
52
59
 
53
60
  ## Lifecycle
@@ -52,7 +52,7 @@ export interface EventBusOptions {
52
52
  readonly requireRegistration?: boolean;
53
53
  readonly middleware?: readonly EventBusMiddlewareItem[];
54
54
  /**
55
- * Receives leak warnings. Defaults to console.warn.
55
+ * Receives leak warnings. Defaults to `process.emitWarning` (type `ZudojsEventsWarning`).
56
56
  */
57
57
  readonly onWarning?: (warning: EventRegistryWarning) => void;
58
58
  /**
@@ -6,7 +6,7 @@
6
6
  * bus and its registry share one set of handlers.
7
7
  */
8
8
  import { createEvent, isEvent } from "../eventTypes/eventDefinition.type.js";
9
- import { deepFreeze } from "../eventTypes/eventPayload.type.js";
9
+ import { createFrozenEventSnapshot } from "../eventTypes/eventSnapshot.freeze.js";
10
10
  import { createEventHandlerContext } from "../eventHandler/eventHandler.core.js";
11
11
  import { EventSubscriptionGroup } from "../eventSubscription/eventSubscription.core.js";
12
12
  import { EventRegistry } from "../eventRegistry/eventRegistry.store.js";
@@ -68,8 +68,12 @@ export class EventEmitter {
68
68
  }
69
69
  const mode = options.mode ?? this.options.mode;
70
70
  const errorMode = options.errorMode ?? this.options.errorMode;
71
- const dispatched = this.options.freezeEvents ? deepFreeze(event) : event;
72
- const handlers = this.store.getHandlersForEvent(dispatched);
71
+ const handlers = this.store.getHandlersForEvent(event);
72
+ // Handlers receive a frozen COPY: freezing the event in place froze
73
+ // the publisher's own objects, even when nobody was subscribed.
74
+ const dispatched = this.options.freezeEvents && handlers.length > 0
75
+ ? createFrozenEventSnapshot(event)
76
+ : event;
73
77
  const context = createEventHandlerContext(dispatched, {
74
78
  signal: options.signal,
75
79
  metadata: options.metadata,
@@ -87,7 +91,17 @@ export class EventEmitter {
87
91
  };
88
92
  }
89
93
  const hooks = {
90
- isRegistered: (handlerId) => this.store.hasHandler(handlerId),
94
+ // A bus disposed mid-dispatch has no registered handlers left.
95
+ isRegistered: (handlerId) => {
96
+ if (this.disposed)
97
+ return false;
98
+ try {
99
+ return this.store.hasHandler(handlerId);
100
+ }
101
+ catch {
102
+ return false;
103
+ }
104
+ },
91
105
  removeOnce: (handlerId) => {
92
106
  this.store.unregisterHandler(handlerId);
93
107
  },
@@ -22,12 +22,12 @@ export async function emitParallel(handlers, event, context, errorMode, results,
22
22
  * an overlapping dispatch cannot invoke them a second time.
23
23
  */
24
24
  const runnable = handlers.filter((handler) => {
25
- if (!handler.once) {
26
- return true;
27
- }
28
25
  if (!hooks.isRegistered(handler.id)) {
29
26
  return false;
30
27
  }
28
+ if (!handler.once) {
29
+ return true;
30
+ }
31
31
  hooks.removeOnce(handler.id);
32
32
  return true;
33
33
  });
@@ -13,11 +13,13 @@ export async function emitSequential(handlers, event, context, errorMode, result
13
13
  if (context.signal.aborted) {
14
14
  throw createAbortError(event, results, errors);
15
15
  }
16
+ // Checked for every handler, not only once-handlers: a handler
17
+ // unsubscribed (or a bus disposed) by an earlier handler in this
18
+ // same dispatch must not run.
19
+ if (!hooks.isRegistered(handler.id)) {
20
+ continue;
21
+ }
16
22
  if (handler.once) {
17
- if (!hooks.isRegistered(handler.id)) {
18
- // Already consumed by an overlapping dispatch.
19
- continue;
20
- }
21
23
  hooks.removeOnce(handler.id);
22
24
  }
23
25
  const started = performance.now();
@@ -44,7 +44,7 @@ export interface EventEmitterOptions {
44
44
  readonly maxListeners?: number;
45
45
  /**
46
46
  * Receives leak warnings. Only applies to the private store.
47
- * Defaults to console.warn.
47
+ * Defaults to `process.emitWarning` (type `ZudojsEventsWarning`).
48
48
  */
49
49
  readonly onWarning?: (warning: EventRegistryWarning) => void;
50
50
  }
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Event middleware pipeline execution for Zudojs.
2
+ * Event middleware pipeline execution for Zudojs, built on `compose` from
3
+ * `@zudojs/middleware`.
3
4
  */
4
5
  import type { Event } from "../eventTypes/eventDefinition.type.js";
5
6
  import type { EventMiddlewareContext, EventMiddlewareNext, RegisteredEventMiddleware, EventMiddlewarePipelineResult } from "./eventMiddleware.type.js";
@@ -1,8 +1,10 @@
1
1
  /**
2
- * Event middleware pipeline execution for Zudojs.
2
+ * Event middleware pipeline execution for Zudojs, built on `compose` from
3
+ * `@zudojs/middleware`.
3
4
  */
5
+ import { compose } from "@zudojs/middleware";
4
6
  import { EventDispatchAbortedError, EventMiddlewareError, toEventError, } from "../eventErrors/eventError.base.js";
5
- import { sortEventMiddleware, executeEventMiddleware, } from "./eventMiddleware.helper.js";
7
+ import { sortEventMiddleware, executeEventMiddleware } from "./eventMiddleware.helper.js";
6
8
  /**
7
9
  * Executes a middleware pipeline.
8
10
  *
@@ -21,47 +23,37 @@ import { sortEventMiddleware, executeEventMiddleware, } from "./eventMiddleware.
21
23
  */
22
24
  export async function executeEventMiddlewarePipeline(middleware, context, terminal) {
23
25
  const started = performance.now();
24
- const activeMiddleware = sortEventMiddleware(middleware.filter((item) => item.enabled));
25
26
  const executions = [];
26
- let index = -1;
27
- const dispatch = async (currentIndex) => {
28
- if (context.signal.aborted) {
29
- throw createAbortError(context);
30
- }
31
- if (currentIndex === activeMiddleware.length) {
32
- return terminal();
33
- }
34
- if (currentIndex <= index) {
35
- throw new EventMiddlewareError("Event middleware called next() more than once.", {
36
- eventType: context.event?.type,
37
- eventId: context.event?.id,
38
- });
39
- }
40
- index = currentIndex;
41
- const current = activeMiddleware[currentIndex];
42
- if (!current) {
43
- return terminal();
44
- }
27
+ const stages = sortEventMiddleware(middleware.filter((item) => item.enabled))
28
+ .map((current) => toStage(current, executions));
29
+ const run = compose(stages, async (ctx) => {
30
+ throwIfAborted(ctx);
31
+ return terminal();
32
+ }, { maxDepth: Number.POSITIVE_INFINITY });
33
+ const result = await run(context);
34
+ return { result, executions, duration: performance.now() - started };
35
+ }
36
+ /**
37
+ * Adapts one registered middleware to the shared composer: checks the
38
+ * abort signal, rejects a second next() with an EventMiddlewareError,
39
+ * records the execution, and wraps only the middleware's own errors.
40
+ */
41
+ function toStage(current, executions) {
42
+ return async (context, advance) => {
43
+ throwIfAborted(context);
45
44
  const middlewareStarted = performance.now();
45
+ const eventType = context.event?.type;
46
+ const eventId = context.event?.id;
46
47
  let nextCalled = false;
47
- /**
48
- * Errors that surfaced through next() belong to downstream
49
- * code, not to this middleware; they must pass through
50
- * unwrapped.
51
- */
52
48
  let downstreamThrew = false;
53
49
  let downstreamError;
54
50
  const next = async () => {
55
51
  if (nextCalled) {
56
- throw new EventMiddlewareError(`Middleware "${current.id}" called next() more than once.`, {
57
- middlewareId: current.id,
58
- eventType: context.event?.type,
59
- eventId: context.event?.id,
60
- });
52
+ throw new EventMiddlewareError(`Middleware "${current.id}" called next() more than once.`, { middlewareId: current.id, eventType, eventId });
61
53
  }
62
54
  nextCalled = true;
63
55
  try {
64
- return await dispatch(currentIndex + 1);
56
+ return await advance();
65
57
  }
66
58
  catch (error) {
67
59
  downstreamThrew = true;
@@ -79,39 +71,29 @@ export async function executeEventMiddlewarePipeline(middleware, context, termin
79
71
  return result;
80
72
  }
81
73
  catch (error) {
82
- if (downstreamThrew && error === downstreamError) {
83
- throw error;
84
- }
85
- if (error instanceof EventMiddlewareError ||
74
+ if ((downstreamThrew && error === downstreamError) ||
75
+ error instanceof EventMiddlewareError ||
86
76
  error instanceof EventDispatchAbortedError) {
87
77
  throw error;
88
78
  }
89
79
  throw new EventMiddlewareError(`Event middleware "${current.id}" failed.`, {
90
80
  middlewareId: current.id,
91
- eventType: context.event?.type,
92
- eventId: context.event?.id,
93
- cause: toEventError(error, {
94
- eventType: context.event?.type,
95
- eventId: context.event?.id,
96
- }),
81
+ eventType,
82
+ eventId,
83
+ cause: toEventError(error, { eventType, eventId }),
97
84
  });
98
85
  }
99
86
  };
100
- const result = await dispatch(0);
101
- return {
102
- result,
103
- executions,
104
- duration: performance.now() - started,
105
- };
106
87
  }
107
88
  /**
108
- * Creates the abort error thrown when the pipeline observes an
109
- * aborted signal.
89
+ * Throws the abort error when the pipeline observes an aborted signal.
110
90
  */
111
- function createAbortError(context) {
112
- return new EventDispatchAbortedError("Event dispatch was aborted.", {
113
- eventType: context.event?.type,
114
- eventId: context.event?.id,
115
- });
91
+ function throwIfAborted(context) {
92
+ if (context.signal.aborted) {
93
+ throw new EventDispatchAbortedError("Event dispatch was aborted.", {
94
+ eventType: context.event?.type,
95
+ eventId: context.event?.id,
96
+ });
97
+ }
116
98
  }
117
99
  //# sourceMappingURL=eventMiddleware.pipeline.js.map
@@ -12,8 +12,18 @@ import { normalizeRegistryEventType, registryRegister, registryRegisterHandler,
12
12
  import { getHandlersForEvent, getHandlersForType, getAllDefinitions, getAllHandlers, } from "./eventRegistry.queries.js";
13
13
  import { registryClear, registryDispose, registryNotify, } from "./eventRegistry.lifecycle.js";
14
14
  export { DuplicateEventDefinitionError, EventDefinitionNotFoundError, DuplicateEventHandlerError, EventHandlerNotFoundError, EventRegistryDisposedError, };
15
+ /**
16
+ * Default leak-warning sink: Node's process warning channel (the same one
17
+ * `EventEmitter` uses for `MaxListenersExceededWarning`), which honours
18
+ * `--no-warnings` and `process.on("warning")`, instead of writing to the
19
+ * console directly. A no-op where `process.emitWarning` is unavailable.
20
+ */
15
21
  function defaultWarning(warning) {
16
- console.warn(`[@zudojs/events] ${warning.message}`);
22
+ const emit = globalThis.process?.emitWarning;
23
+ emit?.(`[@zudojs/events] ${warning.message}`, {
24
+ type: "ZudojsEventsWarning",
25
+ code: "ZUDOJS_EVENTS_HANDLER_LIMIT",
26
+ });
17
27
  }
18
28
  /**
19
29
  * Main event registry.
@@ -55,12 +55,12 @@ export interface EventRegistryOptions {
55
55
  readonly onDuplicateHandlerId?: DuplicateHandlerIdPolicy;
56
56
  /**
57
57
  * Maximum handlers per event pattern before a leak warning is
58
- * emitted through `onWarning` (or console.warn). Use 0 to
58
+ * emitted through `onWarning` (or `process.emitWarning`). Use 0 to
59
59
  * disable. Defaults to 100.
60
60
  */
61
61
  readonly maxHandlersPerPattern?: number;
62
62
  /**
63
- * Receives limit warnings. Defaults to console.warn.
63
+ * Receives limit warnings. Defaults to `process.emitWarning` (type `ZudojsEventsWarning`).
64
64
  */
65
65
  readonly onWarning?: (warning: EventRegistryWarning) => void;
66
66
  /**
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Immutable event snapshots.
3
+ *
4
+ * The emitter used to deep-freeze the event IN PLACE, which froze the
5
+ * publisher's own objects (a later `cart.total = 20` threw) and left
6
+ * Map, Set and Date internals mutable, because `Object.freeze` does not
7
+ * reach them. A snapshot is a frozen COPY: the caller's graph is never
8
+ * touched, and Map, Set and Date are copied into read-only variants
9
+ * whose mutators throw.
10
+ */
11
+ /** A Date whose setters throw. */
12
+ export declare class FrozenEventDate extends Date {
13
+ }
14
+ /** A Map whose mutators throw once construction has finished. */
15
+ export declare class FrozenEventMap<K, V> extends Map<K, V> {
16
+ set(): this;
17
+ delete(): boolean;
18
+ clear(): void;
19
+ }
20
+ /** A Set whose mutators throw once construction has finished. */
21
+ export declare class FrozenEventSet<T> extends Set<T> {
22
+ add(): this;
23
+ delete(): boolean;
24
+ clear(): void;
25
+ }
26
+ /**
27
+ * Returns a deeply frozen copy of `value`, leaving `value` untouched.
28
+ *
29
+ * Plain objects, arrays and errors are copied (own properties, including
30
+ * a `__proto__` data key, are defined rather than assigned) and frozen.
31
+ * Map, Set and Date become {@link FrozenEventMap}, {@link FrozenEventSet}
32
+ * and {@link FrozenEventDate}, which still pass `instanceof Map` / `Set`
33
+ * / `Date` but throw on mutation. Class instances and other exotic
34
+ * objects are passed by reference. Cycles are preserved.
35
+ */
36
+ export declare function createFrozenEventSnapshot<T>(value: T): T;
37
+ //# sourceMappingURL=eventSnapshot.freeze.d.ts.map
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Immutable event snapshots.
3
+ *
4
+ * The emitter used to deep-freeze the event IN PLACE, which froze the
5
+ * publisher's own objects (a later `cart.total = 20` threw) and left
6
+ * Map, Set and Date internals mutable, because `Object.freeze` does not
7
+ * reach them. A snapshot is a frozen COPY: the caller's graph is never
8
+ * touched, and Map, Set and Date are copied into read-only variants
9
+ * whose mutators throw.
10
+ */
11
+ /** Throws the TypeError every read-only mutator raises. */
12
+ function readOnly(kind) {
13
+ throw new TypeError(`Cannot modify a frozen event ${kind}.`);
14
+ }
15
+ /** A Date whose setters throw. */
16
+ export class FrozenEventDate extends Date {
17
+ }
18
+ for (const key of Object.getOwnPropertyNames(Date.prototype)) {
19
+ if (key.startsWith("set")) {
20
+ Object.defineProperty(FrozenEventDate.prototype, key, {
21
+ value: () => readOnly("Date"),
22
+ configurable: true,
23
+ writable: true,
24
+ });
25
+ }
26
+ }
27
+ /** A Map whose mutators throw once construction has finished. */
28
+ export class FrozenEventMap extends Map {
29
+ set() {
30
+ return readOnly("Map");
31
+ }
32
+ delete() {
33
+ return readOnly("Map");
34
+ }
35
+ clear() {
36
+ readOnly("Map");
37
+ }
38
+ }
39
+ /** A Set whose mutators throw once construction has finished. */
40
+ export class FrozenEventSet extends Set {
41
+ add() {
42
+ return readOnly("Set");
43
+ }
44
+ delete() {
45
+ return readOnly("Set");
46
+ }
47
+ clear() {
48
+ readOnly("Set");
49
+ }
50
+ }
51
+ /** Whether a value is a plain object (literal or null-prototype). */
52
+ function isPlainRecord(value) {
53
+ const proto = Object.getPrototypeOf(value);
54
+ return proto === Object.prototype || proto === null;
55
+ }
56
+ function snapshot(value, seen) {
57
+ if (typeof value !== "object" || value === null)
58
+ return value;
59
+ if (seen.has(value))
60
+ return seen.get(value);
61
+ if (value instanceof Date) {
62
+ const copy = Object.freeze(new FrozenEventDate(value.getTime()));
63
+ seen.set(value, copy);
64
+ return copy;
65
+ }
66
+ if (value instanceof Map) {
67
+ const copy = new FrozenEventMap();
68
+ seen.set(value, copy);
69
+ for (const [key, item] of value) {
70
+ Map.prototype.set.call(copy, key, snapshot(item, seen));
71
+ }
72
+ return Object.freeze(copy);
73
+ }
74
+ if (value instanceof Set) {
75
+ const copy = new FrozenEventSet();
76
+ seen.set(value, copy);
77
+ for (const item of value) {
78
+ Set.prototype.add.call(copy, snapshot(item, seen));
79
+ }
80
+ return Object.freeze(copy);
81
+ }
82
+ const isArray = Array.isArray(value);
83
+ const isError = value instanceof Error;
84
+ // Instances of other classes (and exotic built-ins such as typed
85
+ // arrays, RegExp, URL) cannot be copied faithfully — private fields and
86
+ // internal slots do not survive — so they are passed by reference.
87
+ if (!isArray && !isError && !isPlainRecord(value))
88
+ return value;
89
+ const copy = isArray
90
+ ? []
91
+ : Object.create(Object.getPrototypeOf(value));
92
+ seen.set(value, copy);
93
+ for (const key of Reflect.ownKeys(value)) {
94
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
95
+ if (!descriptor)
96
+ continue;
97
+ if ("value" in descriptor) {
98
+ descriptor.value = snapshot(descriptor.value, seen);
99
+ }
100
+ Object.defineProperty(copy, key, descriptor);
101
+ }
102
+ return Object.freeze(copy);
103
+ }
104
+ /**
105
+ * Returns a deeply frozen copy of `value`, leaving `value` untouched.
106
+ *
107
+ * Plain objects, arrays and errors are copied (own properties, including
108
+ * a `__proto__` data key, are defined rather than assigned) and frozen.
109
+ * Map, Set and Date become {@link FrozenEventMap}, {@link FrozenEventSet}
110
+ * and {@link FrozenEventDate}, which still pass `instanceof Map` / `Set`
111
+ * / `Date` but throw on mutation. Class instances and other exotic
112
+ * objects are passed by reference. Cycles are preserved.
113
+ */
114
+ export function createFrozenEventSnapshot(value) {
115
+ return snapshot(value, new Map());
116
+ }
117
+ //# sourceMappingURL=eventSnapshot.freeze.js.map
@@ -6,4 +6,5 @@
6
6
  export { type EventId, type EventType, type EventTimestamp, type EventSource, type EventCorrelationId, type EventCausationId, type EventPayload, type Event, type EventInput, type EventDefinition, isEvent, createEventId, createEvent, defineEvent, withEventMetadata, createDerivedEvent, getEventType, getEventPayload, describeEvent, } from "./eventDefinition.type.js";
7
7
  export { type ObjectEventPayload, type PrimitiveEventPayload, type JsonEventPayload, type EventPayloadMap, type PayloadOf, type PayloadMap, type EventPayloadFactory, type EventPayloadOptions, isPrimitiveEventPayload, isObjectEventPayload, isJsonEventPayload, createEventPayload, createObjectEventPayload, createJsonEventPayload, cloneEventPayload, deepFreeze, stripUndefinedValues, mergeEventPayloads, staticPayload, definePayloadFactory, validateEventPayload, describeEventPayload, } from "./eventPayload.type.js";
8
8
  export { type EventTypeList, type EventTypePattern, type EventTypeOf, type EventUnion, isValidEventType, isValidEventTypePattern, normalizeEventType, normalizeEventTypePattern, tryNormalizeEventType, assertEventType, createEventType, createEventTypePattern, getEventNamespace, getEventAction, getEventTypeSegments, matchesEventType, isSameEventNamespace, isChildEventType, eventMatchesType, filterEventsByType, defineEventTypes, defineEventType, } from "./eventType.type.js";
9
+ export { createFrozenEventSnapshot, FrozenEventDate, FrozenEventMap, FrozenEventSet, } from "./eventSnapshot.freeze.js";
9
10
  //# sourceMappingURL=index.d.ts.map
@@ -6,4 +6,5 @@
6
6
  export { isEvent, createEventId, createEvent, defineEvent, withEventMetadata, createDerivedEvent, getEventType, getEventPayload, describeEvent, } from "./eventDefinition.type.js";
7
7
  export { isPrimitiveEventPayload, isObjectEventPayload, isJsonEventPayload, createEventPayload, createObjectEventPayload, createJsonEventPayload, cloneEventPayload, deepFreeze, stripUndefinedValues, mergeEventPayloads, staticPayload, definePayloadFactory, validateEventPayload, describeEventPayload, } from "./eventPayload.type.js";
8
8
  export { isValidEventType, isValidEventTypePattern, normalizeEventType, normalizeEventTypePattern, tryNormalizeEventType, assertEventType, createEventType, createEventTypePattern, getEventNamespace, getEventAction, getEventTypeSegments, matchesEventType, isSameEventNamespace, isChildEventType, eventMatchesType, filterEventsByType, defineEventTypes, defineEventType, } from "./eventType.type.js";
9
+ export { createFrozenEventSnapshot, FrozenEventDate, FrozenEventMap, FrozenEventSet, } from "./eventSnapshot.freeze.js";
9
10
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zudojs/events",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Event-driven architecture with event bus, emitter, middleware, and registry for decoupled communication.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -20,8 +20,9 @@
20
20
  ],
21
21
  "sideEffects": false,
22
22
  "dependencies": {
23
- "@zudojs/errors": "1.0.1",
24
- "@zudojs/constants": "1.0.1"
23
+ "@zudojs/constants": "1.1.0",
24
+ "@zudojs/errors": "1.1.0",
25
+ "@zudojs/middleware": "1.0.2"
25
26
  },
26
27
  "engines": {
27
28
  "node": ">=24.0.0"