@zudojs/cqrs 1.0.0 → 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
  Command Query Responsibility Segregation (CQRS) primitives for separating read and write operations.
4
4
 
5
+ <!-- zudo-docs:start -->
6
+
7
+ **Documentation:** [zudojs.oyinlola.site/docs/packages-cqrs](https://zudojs.oyinlola.site/docs/packages-cqrs) · **For AI agents:** [Markdown version](https://zudojs.oyinlola.site/docs/packages-cqrs.md), [llms.txt](https://zudojs.oyinlola.site/llms.txt)
8
+
9
+ <!-- zudo-docs:end -->
10
+
5
11
  ## Installation
6
12
 
7
13
  ```bash
@@ -103,6 +109,10 @@ Validation and handler resolution run at the end of the middleware pipeline,
103
109
  so middleware observes `InvalidCommandError` / `CommandHandlerNotFoundError`
104
110
  like any other failure. Each middleware may call `next()` at most once.
105
111
 
112
+ `onTiming` is an observer: if it throws, the command or query still
113
+ succeeds (or fails with its own error). Pass `onTimingError` to see those
114
+ observer failures.
115
+
106
116
  ## Features
107
117
 
108
118
  - Command bus for write operations
@@ -1,6 +1,6 @@
1
1
  import { executeCommandHandler } from "../command/commandHandler.core.js";
2
2
  import { CommandHandlerNotFoundError, DuplicateHandlerError, InvalidCommandError, } from "../cqrsErrors/cqrsError.base.js";
3
- import { composeMiddleware } from "../cqrsMiddleware/cqrsMiddleware.core.js";
3
+ import { composeMiddleware } from "../cqrsMiddleware/cqrsMiddleware.compose.js";
4
4
  import { assertExecutableHandler, assertHandlerType, assertMiddleware, } from "../cqrsValidation/cqrsValidation.core.js";
5
5
  /**
6
6
  * Command bus implementation.
@@ -1,10 +1,11 @@
1
- import { BaseError, ErrorCode, type BaseErrorOptions, type ErrorMetadata } from "@zudojs/errors";
1
+ import { CqrsError, ErrorCode, type ErrorMetadata } from "@zudojs/errors";
2
2
  /**
3
3
  * Base error for failures originating from the CQRS package.
4
+ *
5
+ * Owned by `@zudojs/errors` and re-exported here, so
6
+ * `instanceof CqrsError` matches across both packages.
4
7
  */
5
- export declare class CqrsError extends BaseError {
6
- constructor(message: string, options?: BaseErrorOptions);
7
- }
8
+ export { CqrsError };
8
9
  /**
9
10
  * Thrown when a CQRS request is invalid.
10
11
  *
@@ -1,20 +1,11 @@
1
- import { BaseError, ErrorCategory, ErrorCode, ErrorSeverity, } from "@zudojs/errors";
1
+ import { BaseError, CqrsError, ErrorCategory, ErrorCode, ErrorSeverity, } from "@zudojs/errors";
2
2
  /**
3
3
  * Base error for failures originating from the CQRS package.
4
+ *
5
+ * Owned by `@zudojs/errors` and re-exported here, so
6
+ * `instanceof CqrsError` matches across both packages.
4
7
  */
5
- export class CqrsError extends BaseError {
6
- constructor(message, options = {}) {
7
- super(message, {
8
- ...options,
9
- code: options.code ?? ErrorCode.INTERNAL_ERROR,
10
- category: options.category ?? ErrorCategory.SYSTEM,
11
- severity: options.severity ?? ErrorSeverity.ERROR,
12
- statusCode: options.statusCode ?? 500,
13
- expose: options.expose ?? false,
14
- isOperational: options.isOperational ?? true,
15
- });
16
- }
17
- }
8
+ export { CqrsError };
18
9
  /**
19
10
  * Thrown when a CQRS request is invalid.
20
11
  *
@@ -0,0 +1,14 @@
1
+ /**
2
+ * CQRS middleware composition, built on `compose` from `@zudojs/middleware`.
3
+ */
4
+ import type { CqrsMiddleware } from "../cqrsTypes/cqrsTypes.type.js";
5
+ /**
6
+ * Combines multiple middleware functions into a single middleware.
7
+ *
8
+ * Each middleware may call `next()` at most once per execution; a second
9
+ * call throws `MiddlewareExecutionError`. The command and query buses
10
+ * build their pipelines with this function, so the same rule applies
11
+ * there.
12
+ */
13
+ export declare function composeMiddleware(middleware: readonly CqrsMiddleware[]): CqrsMiddleware;
14
+ //# sourceMappingURL=cqrsMiddleware.compose.d.ts.map
@@ -0,0 +1,33 @@
1
+ /**
2
+ * CQRS middleware composition, built on `compose` from `@zudojs/middleware`.
3
+ */
4
+ import { compose } from "@zudojs/middleware";
5
+ import { MiddlewareExecutionError } from "../cqrsErrors/cqrsError.base.js";
6
+ /** Adapts one CQRS middleware to the shared `(context, next)` shape. */
7
+ function adapt(middleware) {
8
+ return async (state, next) => {
9
+ let called = false;
10
+ return middleware(state.request, state.context, (request, context) => {
11
+ if (called) {
12
+ return Promise.reject(new MiddlewareExecutionError());
13
+ }
14
+ called = true;
15
+ state.request = request;
16
+ state.context = context;
17
+ return next();
18
+ });
19
+ };
20
+ }
21
+ /**
22
+ * Combines multiple middleware functions into a single middleware.
23
+ *
24
+ * Each middleware may call `next()` at most once per execution; a second
25
+ * call throws `MiddlewareExecutionError`. The command and query buses
26
+ * build their pipelines with this function, so the same rule applies
27
+ * there.
28
+ */
29
+ export function composeMiddleware(middleware) {
30
+ const run = compose(middleware.map(adapt), async (state) => state.terminal(state.request, state.context), { maxDepth: Number.POSITIVE_INFINITY });
31
+ return async (request, context, terminal) => run({ request, context, terminal });
32
+ }
33
+ //# sourceMappingURL=cqrsMiddleware.compose.js.map
@@ -45,12 +45,19 @@ export interface CqrsTiming {
45
45
  export interface TimingMiddlewareOptions extends MiddlewareOptions {
46
46
  /**
47
47
  * Receives the measurement after each execution (success or failure).
48
- * Errors thrown by the callback propagate to the caller.
48
+ * The callback is an observer: if it throws or rejects, the error is
49
+ * passed to `onTimingError` (when given) and otherwise discarded. It
50
+ * never changes the outcome of the command or query.
49
51
  *
50
52
  * Optional: measurements are always exposed through
51
53
  * `TimingMiddleware.lastTiming` as well.
52
54
  */
53
55
  readonly onTiming?: (timing: CqrsTiming) => void | Promise<void>;
56
+ /**
57
+ * Receives any error thrown by `onTiming`. Errors thrown here are
58
+ * discarded as well.
59
+ */
60
+ readonly onTimingError?: (error: unknown, timing: CqrsTiming) => void;
54
61
  /**
55
62
  * Monotonic clock in milliseconds. Defaults to `performance.now()`.
56
63
  */
@@ -104,7 +111,12 @@ export declare function contextMiddleware(options?: MiddlewareOptions): CqrsMidd
104
111
  * `acquire` must resolve to a release function.
105
112
  */
106
113
  export interface CqrsLock {
107
- acquire(key: string): (() => void) | Promise<() => void>;
114
+ /**
115
+ * The release function may be synchronous or return a promise; the
116
+ * middleware awaits it. A rejected release surfaces as a `CqrsError`
117
+ * (when the handler succeeded) rather than an unhandled rejection.
118
+ */
119
+ acquire(key: string): (() => void | Promise<void>) | Promise<() => void | Promise<void>>;
108
120
  }
109
121
  /**
110
122
  * Options for `lockMiddleware`.
@@ -129,15 +141,6 @@ export declare function commandMiddleware(middleware: CommandMiddleware): CqrsMi
129
141
  * Adapts query-specific middleware to generic CQRS middleware.
130
142
  */
131
143
  export declare function queryMiddleware(middleware: QueryMiddleware): CqrsMiddleware;
132
- /**
133
- * Combines multiple middleware functions into a single middleware.
134
- *
135
- * Each middleware may call `next()` at most once per execution; a second
136
- * call throws `MiddlewareExecutionError`. The command and query buses
137
- * build their pipelines with this function, so the same rule applies
138
- * there.
139
- */
140
- export declare function composeMiddleware(middleware: readonly CqrsMiddleware[]): CqrsMiddleware;
141
144
  /**
142
145
  * Creates a middleware that runs a callback before execution.
143
146
  */
@@ -1,5 +1,5 @@
1
1
  import { BaseError } from "@zudojs/errors";
2
- import { CqrsError, CqrsValidationError, InvalidMiddlewareError, MiddlewareExecutionError, } from "../cqrsErrors/cqrsError.base.js";
2
+ import { CqrsError, CqrsValidationError, InvalidMiddlewareError, } from "../cqrsErrors/cqrsError.base.js";
3
3
  /**
4
4
  * Middleware that measures command or query execution time.
5
5
  *
@@ -10,6 +10,10 @@ export function timingMiddleware(options = {}) {
10
10
  if (options.onTiming !== undefined && typeof options.onTiming !== "function") {
11
11
  throw new InvalidMiddlewareError("timingMiddleware onTiming must be a function.");
12
12
  }
13
+ if (options.onTimingError !== undefined &&
14
+ typeof options.onTimingError !== "function") {
15
+ throw new InvalidMiddlewareError("timingMiddleware onTimingError must be a function.");
16
+ }
13
17
  if (options.now !== undefined && typeof options.now !== "function") {
14
18
  throw new InvalidMiddlewareError("timingMiddleware now must be a function.");
15
19
  }
@@ -21,9 +25,20 @@ export function timingMiddleware(options = {}) {
21
25
  const record = async (timing) => {
22
26
  lastTiming = timing;
23
27
  count += 1;
24
- if (onTiming) {
28
+ if (!onTiming) {
29
+ return;
30
+ }
31
+ try {
25
32
  await onTiming(timing);
26
33
  }
34
+ catch (error) {
35
+ try {
36
+ options.onTimingError?.(error, timing);
37
+ }
38
+ catch {
39
+ return;
40
+ }
41
+ }
27
42
  };
28
43
  const middleware = async (request, context, next) => {
29
44
  if (options.enabled === false) {
@@ -154,12 +169,35 @@ export function lockMiddleware(lock, options = {}) {
154
169
  lockKey: key,
155
170
  });
156
171
  }
172
+ // `release()` may be asynchronous (a Redis DEL, say). It used to be
173
+ // called and dropped, so a rejected release was an unhandled promise
174
+ // rejection — which terminates the process under Node's defaults.
175
+ let result;
157
176
  try {
158
- return await next(request, context);
177
+ result = await next(request, context);
178
+ }
179
+ catch (error) {
180
+ try {
181
+ await release();
182
+ }
183
+ catch {
184
+ // The handler's failure is the one the caller needs to see.
185
+ }
186
+ throw error;
187
+ }
188
+ try {
189
+ await release();
159
190
  }
160
- finally {
161
- release();
191
+ catch (error) {
192
+ throw new CqrsError(`CQRS lock release failed (key "${key}").`, {
193
+ cause: error,
194
+ metadata: {
195
+ lockKey: key,
196
+ requestType: getRequestType(request),
197
+ },
198
+ });
162
199
  }
200
+ return result;
163
201
  };
164
202
  }
165
203
  /**
@@ -178,32 +216,6 @@ export function queryMiddleware(middleware) {
178
216
  return middleware(request, context, async (query, nextContext) => next(query, nextContext));
179
217
  };
180
218
  }
181
- /**
182
- * Combines multiple middleware functions into a single middleware.
183
- *
184
- * Each middleware may call `next()` at most once per execution; a second
185
- * call throws `MiddlewareExecutionError`. The command and query buses
186
- * build their pipelines with this function, so the same rule applies
187
- * there.
188
- */
189
- export function composeMiddleware(middleware) {
190
- const stack = [...middleware];
191
- return async (request, context, terminal) => {
192
- let index = -1;
193
- const dispatch = async (currentIndex, currentRequest, currentContext) => {
194
- if (currentIndex <= index) {
195
- throw new MiddlewareExecutionError();
196
- }
197
- index = currentIndex;
198
- const current = stack[currentIndex];
199
- if (!current) {
200
- return terminal(currentRequest, currentContext);
201
- }
202
- return current(currentRequest, currentContext, (nextRequest, nextContext) => dispatch(currentIndex + 1, nextRequest, nextContext));
203
- };
204
- return dispatch(0, request, context);
205
- };
206
- }
207
219
  /**
208
220
  * Creates a middleware that runs a callback before execution.
209
221
  */
@@ -5,4 +5,5 @@
5
5
  * context enrichment, lifecycle callbacks, and composition.
6
6
  */
7
7
  export * from "./cqrsMiddleware.core.js";
8
+ export * from "./cqrsMiddleware.compose.js";
8
9
  //# sourceMappingURL=index.d.ts.map
@@ -5,4 +5,5 @@
5
5
  * context enrichment, lifecycle callbacks, and composition.
6
6
  */
7
7
  export * from "./cqrsMiddleware.core.js";
8
+ export * from "./cqrsMiddleware.compose.js";
8
9
  //# sourceMappingURL=index.js.map
@@ -1,6 +1,6 @@
1
1
  import { executeQueryHandler } from "../query/queryHandler.core.js";
2
2
  import { QueryHandlerNotFoundError, DuplicateHandlerError, InvalidQueryError, } from "../cqrsErrors/cqrsError.base.js";
3
- import { composeMiddleware } from "../cqrsMiddleware/cqrsMiddleware.core.js";
3
+ import { composeMiddleware } from "../cqrsMiddleware/cqrsMiddleware.compose.js";
4
4
  import { assertExecutableHandler, assertHandlerType, assertMiddleware, } from "../cqrsValidation/cqrsValidation.core.js";
5
5
  /**
6
6
  * Query bus implementation.
package/package.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "name": "@zudojs/cqrs",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Command Query Responsibility Segregation (CQRS) primitives for separating read and write operations.",
5
5
  "license": "MIT",
6
+ "author": {
7
+ "name": "Oluwayemi Oyinlola",
8
+ "url": "https://github.com/oyinlola-tech"
9
+ },
6
10
  "type": "module",
7
11
  "main": "./dist/index.js",
8
12
  "module": "./dist/index.js",
@@ -21,8 +25,9 @@
21
25
  "!dist/.tsbuildinfo"
22
26
  ],
23
27
  "dependencies": {
24
- "@zudojs/errors": "1.0.0",
25
- "@zudojs/events": "1.0.0"
28
+ "@zudojs/errors": "1.1.0",
29
+ "@zudojs/events": "1.1.0",
30
+ "@zudojs/middleware": "1.0.2"
26
31
  },
27
32
  "devDependencies": {
28
33
  "typescript": "7.0.2",