@context-action/core 0.9.2 → 1.0.0-rc.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/dist/index.cjs CHANGED
@@ -1,294 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
2
 
3
- //#region src/execution-modes.ts
4
- function isPromiseLike(value) {
5
- return (typeof value === "object" || typeof value === "function") && value !== null && typeof value.then === "function";
6
- }
7
- /**
8
- * Create standardized error handling for handlers
9
- *
10
- * @param error - The error that occurred
11
- * @param registration - The handler registration that failed
12
- * @returns Standardized HandlerError object
13
- *
14
- * @internal
15
- */
16
- function handleExecutionError(error, registration) {
17
- const errorObj = error instanceof Error ? error : new Error(String(error));
18
- return {
19
- handlerId: registration.id,
20
- error: errorObj,
21
- timestamp: Date.now(),
22
- severity: registration.config.blocking ? "blocking" : "non-blocking"
23
- };
24
- }
25
- /**
26
- * Execute handlers in sequential mode (one after another)
27
- *
28
- * Executes action handlers one at a time in priority order (highest first).
29
- * Supports both blocking and non-blocking handlers, with proper abort and
30
- * termination handling. Handlers can modify payload for subsequent handlers
31
- * and jump to different priority levels.
32
- *
33
- * @template T - The payload type for the action
34
- * @template R - The result type for handlers
35
- *
36
- * @param context - Pipeline execution context containing handlers and state
37
- * @param createController - Factory function for creating pipeline controllers
38
- *
39
- * @throws {Error} When a blocking handler fails
40
- *
41
- * @see https://mineclover.github.io/context-action/en/guide/patterns/action/dispatch-patterns
42
- *
43
- * @public
44
- */
45
- async function executeSequential(context, createController) {
46
- let i = 0;
47
- const nonBlockingPromises = [];
48
- const errors = [];
49
- while (i < context.handlers.length) {
50
- if (context.aborted || context.terminated) break;
51
- const registration = context.handlers[i];
52
- if (!registration) continue;
53
- context.currentIndex = i;
54
- const controller = createController(registration, i);
55
- try {
56
- if (context.aborted) break;
57
- if (registration.config.condition) try {
58
- if (!registration.config.condition(context.payload)) {
59
- i++;
60
- continue;
61
- }
62
- } catch {
63
- i++;
64
- continue;
65
- }
66
- (context.executedHandlers ?? (context.executedHandlers = [])).push(registration);
67
- const result = registration.handler(context.payload, controller);
68
- const asyncResult = isPromiseLike(result) ? Promise.resolve(result) : void 0;
69
- const trackedResult = asyncResult && context.trackHandlerPromise ? context.trackHandlerPromise(asyncResult) : asyncResult;
70
- if (registration.config.blocking) {
71
- const handlerResult = trackedResult ? await trackedResult : result;
72
- if (handlerResult !== void 0 && !context.terminated) context.results.push(handlerResult);
73
- } else if (trackedResult) {
74
- const promiseWithErrorHandling = trackedResult.then((asyncResult) => {
75
- if (asyncResult !== void 0 && !context.terminated) context.results.push(asyncResult);
76
- return asyncResult;
77
- }).catch((error) => {
78
- const handlerError = handleExecutionError(error, registration);
79
- errors.push({
80
- handlerId: handlerError.handlerId,
81
- error: handlerError.error,
82
- timestamp: handlerError.timestamp,
83
- severity: "non-blocking"
84
- });
85
- });
86
- nonBlockingPromises.push(promiseWithErrorHandling);
87
- } else if (result !== void 0 && !context.terminated) context.results.push(result);
88
- /** Check if pipeline was terminated by controller.return() */
89
- if (context.terminated) break;
90
- /** Handle jump to priority AFTER handler execution */
91
- if (context.jumpToPriority !== void 0) {
92
- context.jumpCount = (context.jumpCount || 0) + 1;
93
- if (context.jumpCount > (context.maxJumps || 10)) {
94
- console.error(`[ActionRegister] ERROR: Maximum jump limit (${context.maxJumps || 10}) exceeded. Aborting to prevent infinite loop. Check your jumpToPriority logic and conditions.`);
95
- context.aborted = true;
96
- context.abortReason = `Maximum jump limit exceeded (${context.jumpCount} jumps)`;
97
- context.jumpToPriority = void 0;
98
- break;
99
- }
100
- const jumpIndex = context.handlers.findIndex((handler) => (handler.config.priority || 0) <= context.jumpToPriority);
101
- if (jumpIndex !== -1 && jumpIndex !== i) {
102
- if (jumpIndex < i) {
103
- const targetHandler = context.handlers[jumpIndex];
104
- if (targetHandler && !targetHandler.config.condition) console.warn(`[ActionRegister] WARNING: Backward jumpToPriority to handler '${targetHandler.config.id || "unnamed"}' without condition. This may cause infinite loops! Consider adding a condition to prevent re-execution. Jump count: ${context.jumpCount}/${context.maxJumps || 10}`);
105
- }
106
- i = jumpIndex;
107
- context.jumpToPriority = void 0;
108
- } else {
109
- context.jumpToPriority = void 0;
110
- i++;
111
- }
112
- } else i++;
113
- } catch (error) {
114
- const handlerError = handleExecutionError(error, registration);
115
- errors.push(handlerError);
116
- if (registration.config.blocking) throw handlerError.error;
117
- i++;
118
- }
119
- }
120
- if (nonBlockingPromises.length > 0) await Promise.allSettled(nonBlockingPromises);
121
- if (errors.length > 0) context.collectedErrors = errors.map((err) => ({
122
- handlerId: err.handlerId,
123
- error: err.error,
124
- timestamp: err.timestamp,
125
- severity: "non-blocking"
126
- }));
127
- }
128
- /**
129
- * Execute handlers in parallel mode (all at once)
130
- *
131
- * Executes all qualifying action handlers simultaneously using Promise.allSettled.
132
- * Supports both blocking and non-blocking handlers. Blocking handlers can still
133
- * fail the entire pipeline if they throw errors.
134
- *
135
- * @template T - The payload type for the action
136
- * @template R - The result type for handlers
137
- *
138
- * @param context - Pipeline execution context containing handlers and state
139
- * @param createController - Factory function for creating pipeline controllers
140
- *
141
- * @throws {Error} When any blocking handler fails
142
- *
143
- * @see https://mineclover.github.io/context-action/en/guide/patterns/action/dispatch-patterns#parallel-execution
144
- *
145
- * @public
146
- */
147
- async function executeParallel(context, createController) {
148
- /** All handlers are runnable */
149
- const runnableHandlers = context.handlers;
150
- /** Create promises for all handlers */
151
- const handlerPromises = runnableHandlers.map(async (registration, _index) => {
152
- const controller = createController(registration, _index);
153
- try {
154
- if (registration.config.condition) try {
155
- if (!registration.config.condition(context.payload)) return {
156
- success: true,
157
- handlerId: registration.id,
158
- result: void 0,
159
- terminated: false,
160
- skipped: true
161
- };
162
- } catch {
163
- return {
164
- success: true,
165
- handlerId: registration.id,
166
- result: void 0,
167
- terminated: false,
168
- skipped: true
169
- };
170
- }
171
- (context.executedHandlers ?? (context.executedHandlers = [])).push(registration);
172
- const result = registration.handler(context.payload, controller);
173
- const handlerResult = isPromiseLike(result) ? await Promise.resolve(result) : result;
174
- /** Collect result if handler returned something and pipeline wasn't terminated */
175
- if (handlerResult !== void 0 && !context.terminated) context.results.push(handlerResult);
176
- return {
177
- success: true,
178
- handlerId: registration.id,
179
- result: handlerResult,
180
- terminated: context.terminated
181
- };
182
- } catch (error) {
183
- const handlerError = handleExecutionError(error, registration);
184
- if (handlerError.severity === "blocking") throw handlerError.error;
185
- return {
186
- success: false,
187
- handlerId: registration.id,
188
- error: handlerError.error
189
- };
190
- }
191
- });
192
- const trackedHandlerPromises = context.trackHandlerPromise ? handlerPromises.map((promise) => context.trackHandlerPromise(promise)) : handlerPromises;
193
- /** Wait for all handlers to complete */
194
- const results = await Promise.allSettled(trackedHandlerPromises);
195
- /** Check for any rejected blocking handlers */
196
- const failures = results.filter((result, index) => {
197
- if (result.status === "rejected") return runnableHandlers[index]?.config.blocking ?? false;
198
- return false;
199
- });
200
- if (failures.length > 0) throw failures[0].reason;
201
- /** Check if any handler terminated the pipeline */
202
- const terminatedResults = results.filter((result) => result.status === "fulfilled" && result.value.terminated);
203
- if (terminatedResults.length > 0) {
204
- context.terminated = true;
205
- context.terminationResult = terminatedResults[0].value.result;
206
- }
207
- }
208
- /**
209
- * Execute handlers in race mode (first to complete wins)
210
- *
211
- * Executes all qualifying handlers simultaneously using Promise.race, where
212
- * the first handler to complete determines the pipeline result. Other handlers
213
- * continue in the background and remain tracked for lifecycle cleanup; handlers
214
- * must observe the controller signal for cooperative external cancellation.
215
- * Useful for scenarios where you want the fastest response from multiple
216
- * equivalent handlers.
217
- *
218
- * @template T - The payload type for the action
219
- * @template R - The result type for handlers
220
- *
221
- * @param context - Pipeline execution context containing handlers and state
222
- * @param createController - Factory function for creating pipeline controllers
223
- *
224
- * @throws {Error} When the winning handler fails and is blocking
225
- *
226
- * @see https://mineclover.github.io/context-action/en/guide/patterns/action/dispatch-patterns#race-execution
227
- *
228
- * @public
229
- */
230
- async function executeRace(context, createController) {
231
- /** All handlers are runnable */
232
- const runnableHandlers = context.handlers;
233
- if (runnableHandlers.length === 0) return;
234
- /** Create promises for all handlers */
235
- const handlerPromises = runnableHandlers.map(async (registration, _index) => {
236
- const controller = createController(registration, _index);
237
- try {
238
- if (registration.config.condition) try {
239
- if (!registration.config.condition(context.payload)) return {
240
- success: true,
241
- handlerId: registration.id,
242
- registration,
243
- result: void 0,
244
- terminated: false,
245
- skipped: true
246
- };
247
- } catch {
248
- return {
249
- success: true,
250
- handlerId: registration.id,
251
- registration,
252
- result: void 0,
253
- terminated: false,
254
- skipped: true
255
- };
256
- }
257
- (context.executedHandlers ?? (context.executedHandlers = [])).push(registration);
258
- const result = registration.handler(context.payload, controller);
259
- const handlerResult = isPromiseLike(result) ? await Promise.resolve(result) : result;
260
- return {
261
- success: true,
262
- handlerId: registration.id,
263
- registration,
264
- result: handlerResult,
265
- terminated: context.terminated
266
- };
267
- } catch (error) {
268
- const handlerError = handleExecutionError(error, registration);
269
- return {
270
- success: false,
271
- handlerId: registration.id,
272
- error: handlerError.error,
273
- registration
274
- };
275
- }
276
- });
277
- const trackedHandlerPromises = context.trackHandlerPromise ? handlerPromises.map((promise) => context.trackHandlerPromise(promise)) : handlerPromises;
278
- /** Race all handlers while retaining every loser for lifecycle draining. */
279
- const winner = await Promise.race(trackedHandlerPromises);
280
- /** If the winner failed and was blocking, throw the error */
281
- if (!winner.success && winner.registration?.config.blocking) throw winner.error;
282
- /** Collect result from the winning handler */
283
- if (winner.success && winner.result !== void 0) context.results.push(winner.result);
284
- /** Check if the winning handler terminated the pipeline */
285
- if (winner.success && winner.terminated) {
286
- context.terminated = true;
287
- context.terminationResult = winner.result;
288
- }
289
- }
290
-
291
- //#endregion
292
3
  //#region src/action-guard.ts
293
4
  /**
294
5
  * Action Guard system for managing action execution timing
@@ -323,8 +34,6 @@ var ActionGuard = class {
323
34
  this.guards = /* @__PURE__ */ new Map();
324
35
  this.maxIdleTime = 6e4;
325
36
  this.cleanupIntervalMs = 3e4;
326
- this.maxGuards = 1e3;
327
- this.accessOrder = [];
328
37
  this.autoCleanupEnabled = autoCleanup;
329
38
  }
330
39
  /** Start cleanup only after the first guard is used. */
@@ -355,75 +64,17 @@ var ActionGuard = class {
355
64
  * @internal
356
65
  */
357
66
  performCleanup() {
358
- const guardCount = this.guards.size;
359
- if (guardCount === 0) {
67
+ if (this.guards.size === 0) {
360
68
  this.stopAutoCleanup();
361
69
  return;
362
70
  }
363
71
  const now = Date.now();
364
- const keysToDelete = [];
365
- if (guardCount <= 10) this.guards.forEach((state, key) => {
366
- const isIdle = now - state.lastExecuted > this.maxIdleTime;
72
+ for (const [key, state] of this.guards) {
73
+ const isIdle = now - Math.max(state.lastThrottleExecutedAt, state.lastDebounceSettledAt) > this.maxIdleTime;
367
74
  const hasActiveTimers = state.debounceTimer || state.throttleTimer;
368
- if (isIdle && !hasActiveTimers) keysToDelete.push(key);
369
- });
370
- else {
371
- const entriesToCheck = Math.min(this.accessOrder.length, Math.ceil(guardCount / 4));
372
- for (let i = 0; i < entriesToCheck; i++) {
373
- const key = this.accessOrder[i];
374
- if (!key) continue;
375
- const state = this.guards.get(key);
376
- if (!state) {
377
- keysToDelete.push(key);
378
- continue;
379
- }
380
- const isIdle = now - state.lastExecuted > this.maxIdleTime;
381
- const hasActiveTimers = state.debounceTimer || state.throttleTimer;
382
- if (isIdle && !hasActiveTimers) keysToDelete.push(key);
383
- }
384
- }
385
- if (keysToDelete.length > 0) {
386
- keysToDelete.forEach((key) => {
387
- this.guards.delete(key);
388
- const accessIndex = this.accessOrder.indexOf(key);
389
- if (accessIndex !== -1) this.accessOrder.splice(accessIndex, 1);
390
- });
391
- if (typeof process !== "undefined" && process.env?.DEBUG_CONTEXT_ACTION) console.debug(`[ActionGuard] Cleaned up ${keysToDelete.length} idle guards`);
392
- if (this.guards.size === 0) this.stopAutoCleanup();
393
- }
394
- }
395
- /**
396
- * 🔧 Update access order for LRU tracking
397
- *
398
- * @internal
399
- */
400
- updateAccessOrder(key) {
401
- const existingIndex = this.accessOrder.indexOf(key);
402
- if (existingIndex !== -1) this.accessOrder.splice(existingIndex, 1);
403
- this.accessOrder.push(key);
404
- }
405
- /**
406
- * 🔧 Evict oldest guards if max limit exceeded
407
- *
408
- * @internal
409
- */
410
- evictIfNeeded() {
411
- if (this.guards.size >= this.maxGuards) {
412
- const evictCount = Math.ceil(this.maxGuards * .1);
413
- this.accessOrder.slice(0, evictCount).forEach((key) => {
414
- const state = this.guards.get(key);
415
- if (state) {
416
- if (state.debounceTimer) {
417
- clearTimeout(state.debounceTimer);
418
- if (state.debounceResolve) state.debounceResolve(false);
419
- }
420
- if (state.throttleTimer) clearTimeout(state.throttleTimer);
421
- }
422
- this.guards.delete(key);
423
- });
424
- this.accessOrder = this.accessOrder.slice(evictCount);
425
- if (typeof process !== "undefined" && process.env?.DEBUG_CONTEXT_ACTION) console.debug(`[ActionGuard] Evicted ${evictCount} oldest guards due to limit`);
75
+ if (isIdle && !hasActiveTimers) this.guards.delete(key);
426
76
  }
77
+ if (this.guards.size === 0) this.stopAutoCleanup();
427
78
  }
428
79
  /**
429
80
  * Apply debouncing to an action
@@ -447,24 +98,25 @@ var ActionGuard = class {
447
98
  *
448
99
  * @internal
449
100
  */
450
- async debounce(actionKey, debounceMs) {
101
+ async debounce(actionKey, debounceMs, signal) {
451
102
  this.ensureAutoCleanup();
452
- this.evictIfNeeded();
103
+ if (signal?.aborted) return false;
453
104
  /** Get or create guard state for this action */
454
105
  let state = this.guards.get(actionKey);
455
106
  if (!state) {
456
107
  /** Initialize new guard state with default values */
457
108
  state = {
458
- lastExecuted: 0,
109
+ lastThrottleExecutedAt: 0,
110
+ lastDebounceSettledAt: 0,
459
111
  isThrottled: false,
460
112
  debounceTimer: void 0,
461
113
  throttleTimer: void 0,
462
- debouncePromise: void 0,
463
- debounceResolve: void 0
114
+ debounceResolve: void 0,
115
+ debounceAbortCleanup: void 0,
116
+ debounceRequestId: 0
464
117
  };
465
118
  this.guards.set(actionKey, state);
466
119
  }
467
- this.updateAccessOrder(actionKey);
468
120
  /** Clear any existing debounce timer to restart the delay period */
469
121
  if (state.debounceTimer) {
470
122
  clearTimeout(state.debounceTimer);
@@ -472,18 +124,35 @@ var ActionGuard = class {
472
124
  state.debounceResolve(false);
473
125
  state.debounceResolve = void 0;
474
126
  }
127
+ state.debounceAbortCleanup?.();
128
+ state.debounceAbortCleanup = void 0;
475
129
  }
476
- /** Create new debounce promise */
130
+ const requestId = ++state.debounceRequestId;
131
+ /** Create a new abort-aware debounce promise. */
477
132
  return new Promise((resolve) => {
478
- state.debounceResolve = resolve;
479
- state.debounceTimer = setTimeout(() => {
480
- /** Clean up timer and resolver references */
481
- state.debounceTimer = void 0;
482
- state.debounceResolve = void 0;
483
- /** Update last execution timestamp */
484
- state.lastExecuted = Date.now();
485
- resolve(true);
486
- }, debounceMs);
133
+ let settled = false;
134
+ let abortCleanup;
135
+ const finish = (allowed) => {
136
+ if (settled) return;
137
+ settled = true;
138
+ if (state.debounceRequestId === requestId) {
139
+ if (state.debounceTimer) clearTimeout(state.debounceTimer);
140
+ state.debounceTimer = void 0;
141
+ state.debounceResolve = void 0;
142
+ state.debounceAbortCleanup = void 0;
143
+ if (allowed) state.lastDebounceSettledAt = Date.now();
144
+ }
145
+ abortCleanup?.();
146
+ resolve(allowed);
147
+ };
148
+ state.debounceResolve = finish;
149
+ state.debounceTimer = setTimeout(() => finish(true), debounceMs);
150
+ if (signal) {
151
+ const abort = () => finish(false);
152
+ signal.addEventListener("abort", abort, { once: true });
153
+ abortCleanup = () => signal.removeEventListener("abort", abort);
154
+ state.debounceAbortCleanup = abortCleanup;
155
+ }
487
156
  });
488
157
  }
489
158
  /**
@@ -508,31 +177,32 @@ var ActionGuard = class {
508
177
  *
509
178
  * @internal
510
179
  */
511
- throttle(actionKey, throttleMs) {
180
+ throttle(actionKey, throttleMs, signal) {
512
181
  this.ensureAutoCleanup();
513
- this.evictIfNeeded();
182
+ if (signal?.aborted) return false;
514
183
  /** Get or create guard state for this action */
515
184
  let state = this.guards.get(actionKey);
516
185
  if (!state) {
517
186
  /** Initialize new guard state with default values */
518
187
  state = {
519
- lastExecuted: 0,
188
+ lastThrottleExecutedAt: 0,
189
+ lastDebounceSettledAt: 0,
520
190
  isThrottled: false,
521
191
  debounceTimer: void 0,
522
192
  throttleTimer: void 0,
523
- debouncePromise: void 0,
524
- debounceResolve: void 0
193
+ debounceResolve: void 0,
194
+ debounceAbortCleanup: void 0,
195
+ debounceRequestId: 0
525
196
  };
526
197
  this.guards.set(actionKey, state);
527
198
  }
528
- this.updateAccessOrder(actionKey);
529
199
  const now = Date.now();
530
- const timeSinceLastExecution = now - state.lastExecuted;
200
+ const timeSinceLastExecution = now - state.lastThrottleExecutedAt;
531
201
  /** Check if enough time has passed since last execution */
532
202
  /** If throttle period has elapsed, allow immediate execution */
533
203
  if (timeSinceLastExecution >= throttleMs) {
534
204
  /** Update execution timestamp and clear throttled state */
535
- state.lastExecuted = now;
205
+ state.lastThrottleExecutedAt = now;
536
206
  state.isThrottled = false;
537
207
  return true;
538
208
  }
@@ -572,13 +242,13 @@ var ActionGuard = class {
572
242
  }
573
243
  state.debounceTimer = void 0;
574
244
  }
245
+ state.debounceAbortCleanup?.();
246
+ state.debounceAbortCleanup = void 0;
575
247
  if (state.throttleTimer) {
576
248
  clearTimeout(state.throttleTimer);
577
249
  state.throttleTimer = void 0;
578
250
  }
579
251
  this.guards.delete(actionKey);
580
- const accessIndex = this.accessOrder.indexOf(actionKey);
581
- if (accessIndex !== -1) this.accessOrder.splice(accessIndex, 1);
582
252
  if (this.guards.size === 0) this.stopAutoCleanup();
583
253
  }
584
254
  }
@@ -599,12 +269,12 @@ var ActionGuard = class {
599
269
  clearTimeout(state.debounceTimer);
600
270
  if (state.debounceResolve) state.debounceResolve(false);
601
271
  }
272
+ state.debounceAbortCleanup?.();
602
273
  /** Clear any active throttle timers */
603
274
  if (state.throttleTimer) clearTimeout(state.throttleTimer);
604
275
  });
605
276
  /** Remove all guard states from memory */
606
277
  this.guards.clear();
607
- this.accessOrder = [];
608
278
  this.stopAutoCleanup();
609
279
  }
610
280
  /**
@@ -863,11 +533,31 @@ var OperationQueue = class {
863
533
 
864
534
  //#endregion
865
535
  //#region src/errors.ts
866
- /**
867
- * Action payload 검증 실패 에러
868
- *
869
- * dispatch 시 Zod 스키마 검증이 실패하면 발생합니다.
870
- * (validationMode가 'strict'일 때만 throw)
536
+ /** Raised when dispatch result aggregation options cannot be processed. */
537
+ var ActionResultProcessingError = class ActionResultProcessingError extends Error {
538
+ constructor(message) {
539
+ super(message);
540
+ this.name = "ActionResultProcessingError";
541
+ Object.setPrototypeOf(this, ActionResultProcessingError.prototype);
542
+ }
543
+ };
544
+ /** Signals work from a completed race attempt to stop before the next retry. */
545
+ var ActionAttemptSupersededError = class ActionAttemptSupersededError extends Error {
546
+ constructor(attempt) {
547
+ super(`Action attempt ${attempt} was superseded by a retry.`);
548
+ this.attempt = attempt;
549
+ this.name = "ActionAttemptSupersededError";
550
+ Object.setPrototypeOf(this, ActionAttemptSupersededError.prototype);
551
+ }
552
+ };
553
+ function isActionResultProcessingError(error) {
554
+ return error instanceof ActionResultProcessingError;
555
+ }
556
+ /**
557
+ * Action payload 검증 실패 에러
558
+ *
559
+ * dispatch 시 Zod 스키마 검증이 실패하면 발생합니다.
560
+ * (validationMode가 'strict'일 때만 throw)
871
561
  *
872
562
  * @example
873
563
  * ```typescript
@@ -983,8 +673,443 @@ function isActionRegisterDestroyedError(error) {
983
673
  return error instanceof ActionRegisterDestroyedError;
984
674
  }
985
675
 
676
+ //#endregion
677
+ //#region src/execution-modes.ts
678
+ function isPromiseLike(value) {
679
+ return (typeof value === "object" || typeof value === "function") && value !== null && typeof value.then === "function";
680
+ }
681
+ function beginOutcome(registration) {
682
+ return {
683
+ id: registration.id,
684
+ status: "running",
685
+ executed: true,
686
+ duration: void 0,
687
+ result: void 0,
688
+ error: void 0,
689
+ metadata: registration.config.metadata ? { ...registration.config.metadata } : void 0
690
+ };
691
+ }
692
+ function createSkippedOutcome(registration) {
693
+ return {
694
+ id: registration.id,
695
+ status: "skipped",
696
+ executed: false,
697
+ duration: 0,
698
+ result: void 0,
699
+ error: void 0,
700
+ metadata: registration.config.metadata ? { ...registration.config.metadata } : void 0
701
+ };
702
+ }
703
+ function finishOutcome(outcome, startedAt, status, result, error) {
704
+ outcome.status = status;
705
+ outcome.duration = Date.now() - startedAt;
706
+ outcome.result = result;
707
+ outcome.error = error;
708
+ }
709
+ function appendLocalResults(context, state, returnedResult, registration, target = context.results) {
710
+ if (registration.role === "guard") return;
711
+ if (state.results.length > 0) target.push(...state.results);
712
+ if (returnedResult !== void 0 && !state.terminated) target.push(returnedResult);
713
+ }
714
+ /**
715
+ * Create standardized error handling for handlers
716
+ *
717
+ * @param error - The error that occurred
718
+ * @param registration - The handler registration that failed
719
+ * @returns Standardized HandlerError object
720
+ *
721
+ * @internal
722
+ */
723
+ function handleExecutionError(error, registration) {
724
+ const errorObj = error instanceof Error ? error : new Error(String(error));
725
+ return {
726
+ handlerId: registration.id,
727
+ error: errorObj,
728
+ timestamp: Date.now(),
729
+ severity: registration.config.errorPolicy === "fatal" ? "blocking" : "non-blocking"
730
+ };
731
+ }
732
+ /**
733
+ * Execute handlers in sequential mode (one after another)
734
+ *
735
+ * Executes action handlers one at a time in priority order (highest first).
736
+ * Supports both blocking and non-blocking handlers, with proper abort and
737
+ * termination handling. Handlers can modify payload for subsequent handlers
738
+ * and jump to different priority levels.
739
+ *
740
+ * @template T - The payload type for the action
741
+ * @template R - The result type for handlers
742
+ *
743
+ * @param context - Pipeline execution context containing handlers and state
744
+ * @param createController - Factory function for creating pipeline controllers
745
+ *
746
+ * @throws {Error} When a blocking handler fails
747
+ *
748
+ * @see https://mineclover.github.io/context-action/en/guide/patterns/action/dispatch-patterns
749
+ *
750
+ * @public
751
+ */
752
+ async function executeSequential(context, createController) {
753
+ let i = 0;
754
+ const nonBlockingPromises = [];
755
+ const errors = [];
756
+ while (i < context.handlers.length) {
757
+ if (context.aborted || context.terminated) break;
758
+ const registration = context.handlers[i];
759
+ if (!registration) continue;
760
+ context.currentIndex = i;
761
+ const controller = createController(registration, i);
762
+ if (registration.config.condition) {
763
+ if (!registration.config.condition(context.payload)) {
764
+ (context.handlerOutcomes ?? (context.handlerOutcomes = [])).push(createSkippedOutcome(registration));
765
+ i++;
766
+ continue;
767
+ }
768
+ }
769
+ if (context.claimOnce && !context.claimOnce(registration)) {
770
+ (context.handlerOutcomes ?? (context.handlerOutcomes = [])).push(createSkippedOutcome(registration));
771
+ i++;
772
+ continue;
773
+ }
774
+ const outcome = beginOutcome(registration);
775
+ const startedAt = Date.now();
776
+ (context.handlerOutcomes ?? (context.handlerOutcomes = [])).push(outcome);
777
+ try {
778
+ if (context.aborted) {
779
+ outcome.status = "cancelled";
780
+ outcome.executed = false;
781
+ outcome.duration = 0;
782
+ break;
783
+ }
784
+ (context.executedHandlers ?? (context.executedHandlers = [])).push(registration);
785
+ const result = registration.handler(context.payload, controller);
786
+ const asyncResult = isPromiseLike(result) ? Promise.resolve(result) : void 0;
787
+ const trackedResult = asyncResult && context.trackHandlerPromise ? context.trackHandlerPromise(asyncResult) : asyncResult;
788
+ if (registration.config.scheduling === "await-before-next") {
789
+ const handlerResult = trackedResult ? await trackedResult : result;
790
+ finishOutcome(outcome, startedAt, "succeeded", registration.role === "guard" ? void 0 : handlerResult);
791
+ if (registration.role !== "guard" && handlerResult !== void 0 && !context.terminated) context.results.push(handlerResult);
792
+ } else if (trackedResult) {
793
+ const promiseWithErrorHandling = trackedResult.then((asyncResult) => {
794
+ finishOutcome(outcome, startedAt, "succeeded", registration.role === "guard" ? void 0 : asyncResult);
795
+ if (registration.role !== "guard" && asyncResult !== void 0 && !context.terminated) context.results.push(asyncResult);
796
+ return asyncResult;
797
+ }).catch((error) => {
798
+ const handlerError = handleExecutionError(error, registration);
799
+ errors.push(handlerError);
800
+ finishOutcome(outcome, startedAt, "failed", void 0, handlerError.error);
801
+ });
802
+ nonBlockingPromises.push(promiseWithErrorHandling);
803
+ } else if (registration.role !== "guard" && result !== void 0 && !context.terminated) {
804
+ finishOutcome(outcome, startedAt, "succeeded", result);
805
+ context.results.push(result);
806
+ } else finishOutcome(outcome, startedAt, "succeeded", registration.role === "guard" ? void 0 : result);
807
+ outcome.terminationRequested = context.terminated;
808
+ if (context.terminated) outcome.terminationResult = context.terminationResult;
809
+ /** Check if pipeline was terminated by controller.return() */
810
+ if (context.terminated) break;
811
+ /** Handle jump to priority AFTER handler execution */
812
+ if (context.jumpToPriority !== void 0) {
813
+ context.jumpCount = (context.jumpCount || 0) + 1;
814
+ if (context.jumpCount > (context.maxJumps || 10)) {
815
+ context.aborted = true;
816
+ context.abortReason = `Maximum jump limit exceeded (${context.jumpCount} jumps)`;
817
+ context.jumpToPriority = void 0;
818
+ break;
819
+ }
820
+ const jumpIndex = context.handlers.findIndex((handler) => (handler.config.priority || 0) <= context.jumpToPriority);
821
+ if (jumpIndex !== -1 && jumpIndex !== i) {
822
+ i = jumpIndex;
823
+ context.jumpToPriority = void 0;
824
+ } else {
825
+ context.jumpToPriority = void 0;
826
+ i++;
827
+ }
828
+ } else i++;
829
+ } catch (error) {
830
+ const handlerError = handleExecutionError(error, registration);
831
+ finishOutcome(outcome, startedAt, "failed", void 0, handlerError.error);
832
+ errors.push(handlerError);
833
+ (context.collectedErrors ?? (context.collectedErrors = [])).push(handlerError);
834
+ if (registration.config.errorPolicy === "fatal") throw handlerError.error;
835
+ i++;
836
+ }
837
+ }
838
+ if (nonBlockingPromises.length > 0) await Promise.allSettled(nonBlockingPromises);
839
+ if (errors.length > 0) context.collectedErrors = errors;
840
+ const fatalError = errors.find((error) => error.severity === "blocking");
841
+ if (fatalError) throw fatalError.error;
842
+ }
843
+ /**
844
+ * Execute handlers in parallel mode (all at once)
845
+ *
846
+ * Executes all qualifying action handlers simultaneously using Promise.allSettled.
847
+ * Supports both blocking and non-blocking handlers. Blocking handlers can still
848
+ * fail the entire pipeline if they throw errors.
849
+ *
850
+ * @template T - The payload type for the action
851
+ * @template R - The result type for handlers
852
+ *
853
+ * @param context - Pipeline execution context containing handlers and state
854
+ * @param createController - Factory function for creating pipeline controllers
855
+ *
856
+ * @throws {Error} When any blocking handler fails
857
+ *
858
+ * @see https://mineclover.github.io/context-action/en/guide/patterns/action/dispatch-patterns#parallel-execution
859
+ *
860
+ * @public
861
+ */
862
+ async function executeParallel(context, createController) {
863
+ /**
864
+ * Conditions are dispatch preconditions in concurrent modes. Evaluate them
865
+ * before any handler starts so a predicate error rejects the dispatch rather
866
+ * than being mistaken for a non-blocking handler failure.
867
+ */
868
+ const runnableHandlers = [];
869
+ for (const registration of context.handlers) {
870
+ if (registration.config.condition && !registration.config.condition(context.payload)) {
871
+ (context.handlerOutcomes ?? (context.handlerOutcomes = [])).push(createSkippedOutcome(registration));
872
+ continue;
873
+ }
874
+ if (context.claimOnce && !context.claimOnce(registration)) {
875
+ (context.handlerOutcomes ?? (context.handlerOutcomes = [])).push(createSkippedOutcome(registration));
876
+ continue;
877
+ }
878
+ runnableHandlers.push(registration);
879
+ }
880
+ const terminationSlots = runnableHandlers.map(() => ({
881
+ requested: false,
882
+ result: void 0
883
+ }));
884
+ const resultSlots = runnableHandlers.map(() => []);
885
+ /** Create promises for all handlers */
886
+ const handlerPromises = runnableHandlers.map(async (registration, _index) => {
887
+ const state = {
888
+ payload: context.payload,
889
+ aborted: false,
890
+ abortReason: void 0,
891
+ jumpToPriority: void 0,
892
+ terminated: false,
893
+ terminationResult: void 0,
894
+ results: []
895
+ };
896
+ const controller = createController(registration, _index, state);
897
+ const outcome = beginOutcome(registration);
898
+ const startedAt = Date.now();
899
+ (context.handlerOutcomes ?? (context.handlerOutcomes = [])).push(outcome);
900
+ try {
901
+ (context.executedHandlers ?? (context.executedHandlers = [])).push(registration);
902
+ const result = registration.handler(state.payload, controller);
903
+ const handlerResult = isPromiseLike(result) ? await Promise.resolve(result) : result;
904
+ finishOutcome(outcome, startedAt, "succeeded", registration.role === "guard" ? void 0 : handlerResult);
905
+ outcome.terminationRequested = state.terminated;
906
+ if (state.terminated && registration.role !== "guard") {
907
+ outcome.terminationResult = state.terminationResult;
908
+ terminationSlots[_index] = {
909
+ requested: true,
910
+ result: state.terminationResult
911
+ };
912
+ }
913
+ appendLocalResults(context, state, handlerResult, registration, resultSlots[_index]);
914
+ return {
915
+ success: true,
916
+ handlerId: registration.id,
917
+ result: handlerResult,
918
+ terminated: state.terminated,
919
+ state,
920
+ outcome
921
+ };
922
+ } catch (error) {
923
+ const handlerError = handleExecutionError(error, registration);
924
+ finishOutcome(outcome, startedAt, "failed", void 0, handlerError.error);
925
+ (context.collectedErrors ?? (context.collectedErrors = [])).push(handlerError);
926
+ if (handlerError.severity === "blocking") throw handlerError.error;
927
+ return {
928
+ success: false,
929
+ handlerId: registration.id,
930
+ error: handlerError.error,
931
+ state,
932
+ outcome,
933
+ registration
934
+ };
935
+ }
936
+ });
937
+ const trackedHandlerPromises = context.trackHandlerPromise ? handlerPromises.map((promise) => context.trackHandlerPromise(promise)) : handlerPromises;
938
+ /** Wait for all handlers to complete */
939
+ const results = await Promise.allSettled(trackedHandlerPromises);
940
+ context.results.push(...resultSlots.flat());
941
+ /** Check for any rejected blocking handlers */
942
+ const failures = results.filter((result, index) => {
943
+ if (result.status === "rejected") return runnableHandlers[index]?.config.errorPolicy === "fatal";
944
+ return false;
945
+ });
946
+ if (failures.length > 0) throw failures[0].reason;
947
+ /** Check if any handler terminated the pipeline */
948
+ const firstTerminated = terminationSlots.find((slot) => slot.requested);
949
+ if (firstTerminated) {
950
+ context.terminated = true;
951
+ context.terminationResult = firstTerminated.result;
952
+ }
953
+ }
954
+ /**
955
+ * Execute handlers in race mode (first to complete wins)
956
+ *
957
+ * Executes all qualifying handlers simultaneously using Promise.race, where
958
+ * the first handler to complete determines the pipeline result. Other handlers
959
+ * continue in the background and remain tracked for lifecycle cleanup; handlers
960
+ * must observe the controller signal for cooperative external cancellation.
961
+ * Useful for scenarios where you want the fastest response from multiple
962
+ * equivalent handlers.
963
+ *
964
+ * @template T - The payload type for the action
965
+ * @template R - The result type for handlers
966
+ *
967
+ * @param context - Pipeline execution context containing handlers and state
968
+ * @param createController - Factory function for creating pipeline controllers
969
+ *
970
+ * @throws {Error} When the winning handler fails and is blocking
971
+ *
972
+ * @see https://mineclover.github.io/context-action/en/guide/patterns/action/dispatch-patterns#race-execution
973
+ *
974
+ * @public
975
+ */
976
+ async function executeRace(context, createController) {
977
+ /** See executeParallel: condition errors are dispatch errors in concurrent modes. */
978
+ const runnableHandlers = [];
979
+ for (const registration of context.handlers) {
980
+ if (registration.config.condition && !registration.config.condition(context.payload)) {
981
+ (context.handlerOutcomes ?? (context.handlerOutcomes = [])).push(createSkippedOutcome(registration));
982
+ continue;
983
+ }
984
+ if (context.claimOnce && !context.claimOnce(registration)) {
985
+ (context.handlerOutcomes ?? (context.handlerOutcomes = [])).push(createSkippedOutcome(registration));
986
+ continue;
987
+ }
988
+ runnableHandlers.push(registration);
989
+ }
990
+ if (runnableHandlers.length === 0) return;
991
+ /** Create promises for all handlers */
992
+ const handlerPromises = runnableHandlers.map(async (registration, _index) => {
993
+ const state = {
994
+ payload: context.payload,
995
+ aborted: false,
996
+ abortReason: void 0,
997
+ jumpToPriority: void 0,
998
+ terminated: false,
999
+ terminationResult: void 0,
1000
+ results: []
1001
+ };
1002
+ const controller = createController(registration, _index, state);
1003
+ const outcome = beginOutcome(registration);
1004
+ const startedAt = Date.now();
1005
+ (context.handlerOutcomes ?? (context.handlerOutcomes = [])).push(outcome);
1006
+ try {
1007
+ (context.executedHandlers ?? (context.executedHandlers = [])).push(registration);
1008
+ const result = registration.handler(state.payload, controller);
1009
+ const handlerResult = isPromiseLike(result) ? await Promise.resolve(result) : result;
1010
+ finishOutcome(outcome, startedAt, "succeeded", registration.role === "guard" ? void 0 : handlerResult);
1011
+ outcome.terminationRequested = state.terminated;
1012
+ if (state.terminated) outcome.terminationResult = state.terminationResult;
1013
+ return {
1014
+ success: true,
1015
+ handlerId: registration.id,
1016
+ registration,
1017
+ result: handlerResult,
1018
+ terminated: state.terminated,
1019
+ state,
1020
+ outcome
1021
+ };
1022
+ } catch (error) {
1023
+ const handlerError = handleExecutionError(error, registration);
1024
+ finishOutcome(outcome, startedAt, "failed", void 0, handlerError.error);
1025
+ return {
1026
+ success: false,
1027
+ handlerId: registration.id,
1028
+ error: handlerError.error,
1029
+ registration,
1030
+ state,
1031
+ outcome
1032
+ };
1033
+ }
1034
+ });
1035
+ const trackedHandlerPromises = context.trackHandlerPromise ? handlerPromises.map((promise) => context.trackHandlerPromise(promise)) : handlerPromises;
1036
+ const winnerCandidates = runnableHandlers.some((handler) => handler.role !== "guard") ? trackedHandlerPromises.filter((_, index) => runnableHandlers[index]?.role !== "guard") : trackedHandlerPromises;
1037
+ /** Race all handlers while retaining every loser for lifecycle draining. */
1038
+ const winner = await Promise.race(winnerCandidates);
1039
+ context.raceWinnerId = winner.handlerId;
1040
+ context.raceLoserOutcomes = (context.handlerOutcomes ?? []).filter((outcome) => outcome.id !== winner.handlerId).map((outcome) => ({
1041
+ ...outcome,
1042
+ metadata: outcome.metadata ? { ...outcome.metadata } : void 0
1043
+ }));
1044
+ /** If the winner failed and was blocking, throw the error */
1045
+ if (!winner.success && winner.registration?.config.errorPolicy === "fatal") {
1046
+ (context.collectedErrors ?? (context.collectedErrors = [])).push(handleExecutionError(winner.error, winner.registration));
1047
+ throw winner.error;
1048
+ }
1049
+ if (!winner.success) (context.collectedErrors ?? (context.collectedErrors = [])).push(handleExecutionError(winner.error, winner.registration));
1050
+ /** Only the winner contributes results to the race snapshot. */
1051
+ if (winner.success) {
1052
+ appendLocalResults(context, winner.state, winner.result, winner.registration);
1053
+ if (winner.state.aborted) {
1054
+ context.aborted = true;
1055
+ context.abortReason = winner.state.abortReason;
1056
+ }
1057
+ }
1058
+ /** Check if the winning handler terminated the pipeline */
1059
+ if (winner.success && winner.terminated) {
1060
+ context.terminated = true;
1061
+ context.terminationResult = winner.state.terminationResult;
1062
+ }
1063
+ }
1064
+
1065
+ //#endregion
1066
+ //#region src/types.ts
1067
+ /**
1068
+ * Resolve the supported `blocking` shorthand and all registration
1069
+ * defaults in one place. Adapters should pass their original config to the
1070
+ * registry and use this helper only when they need to expose resolved values.
1071
+ */
1072
+ function resolveHandlerConfig(config, handlerId) {
1073
+ return {
1074
+ priority: config?.priority ?? 0,
1075
+ id: handlerId,
1076
+ blocking: config?.errorPolicy === "fatal" || config?.blocking === true,
1077
+ scheduling: config?.scheduling ?? (config?.blocking === false ? "start-and-continue" : "await-before-next"),
1078
+ errorPolicy: config?.errorPolicy ?? (config?.blocking === true ? "fatal" : "collect"),
1079
+ once: config?.once ?? false,
1080
+ debounce: config?.debounce,
1081
+ throttle: config?.throttle,
1082
+ replaceExisting: config?.replaceExisting ?? true,
1083
+ cleanup: config?.cleanup,
1084
+ condition: config?.condition,
1085
+ metadata: config?.metadata
1086
+ };
1087
+ }
1088
+
986
1089
  //#endregion
987
1090
  //#region src/ActionRegister.ts
1091
+ const RESERVED_PROXY_KEYS = /* @__PURE__ */ new Set([
1092
+ "then",
1093
+ "catch",
1094
+ "finally",
1095
+ "toJSON",
1096
+ "constructor",
1097
+ "__proto__",
1098
+ "prototype"
1099
+ ]);
1100
+ const ATTEMPT_SIGNAL_CLEANUP = Symbol("attemptSignalCleanup");
1101
+ function snapshotHandlerOutcome(outcome) {
1102
+ return {
1103
+ ...outcome,
1104
+ metadata: outcome.metadata ? { ...outcome.metadata } : void 0
1105
+ };
1106
+ }
1107
+ function normalizePositiveLimit(value, fallback, label) {
1108
+ const limit = value ?? fallback;
1109
+ if (limit === Infinity) return limit;
1110
+ if (!Number.isSafeInteger(limit) || limit <= 0) throw new RangeError(`${label} must be a positive safe integer or Infinity.`);
1111
+ return limit;
1112
+ }
988
1113
  /**
989
1114
  * Action Register for managing action handlers with priority-based execution
990
1115
  *
@@ -1003,23 +1128,27 @@ function isActionRegisterDestroyedError(error) {
1003
1128
  var ActionRegister = class {
1004
1129
  constructor(config = {}) {
1005
1130
  this.pipelines = /* @__PURE__ */ new Map();
1131
+ this.observerHandlers = /* @__PURE__ */ new Map();
1132
+ this.claimedOnceHandlers = /* @__PURE__ */ new WeakSet();
1006
1133
  this.executionMode = "sequential";
1007
1134
  this.actionExecutionModes = /* @__PURE__ */ new Map();
1008
1135
  this.unregisterFunctions = /* @__PURE__ */ new Map();
1009
1136
  this.lastRegisteredTimestamps = /* @__PURE__ */ new Map();
1010
1137
  this.handlerIdCounter = 0;
1011
- this.controllerPool = [];
1012
1138
  this.lifecycleState = "active";
1013
1139
  this.lifecycleController = new AbortController();
1014
1140
  this.activeDispatches = /* @__PURE__ */ new Set();
1015
1141
  this.activeHandlerPromises = /* @__PURE__ */ new Set();
1016
1142
  this.dispatchConstructionDepth = 0;
1143
+ this.actionDispatchers = /* @__PURE__ */ new Map();
1144
+ this.actionResultDispatchers = /* @__PURE__ */ new Map();
1017
1145
  this.name = config.name || "ActionRegister";
1018
1146
  this.registryConfig = config.registry;
1019
- this.maxHandlersPerAction = config.registry?.maxHandlersPerAction ?? 1e3;
1020
- this.isDebugMode = Boolean(this.registryConfig?.debug && process.env.NODE_ENV === "development");
1147
+ this.maxHandlersPerAction = normalizePositiveLimit(config.registry?.maxHandlersPerAction, Infinity, "maxHandlersPerAction");
1148
+ this.maxJumps = normalizePositiveLimit(config.registry?.maxJumps, 10, "maxJumps");
1149
+ this.isDebugMode = this.registryConfig?.debug === true;
1021
1150
  this.actionGuard = new ActionGuard(this.registryConfig?.autoCleanup !== false);
1022
- if (config.registry?.useConcurrencyQueue !== false) this.dispatchQueue = new OperationQueue(`${this.name}-Dispatch`);
1151
+ if (config.registry?.useConcurrencyQueue === true) this.dispatchQueue = new OperationQueue(`${this.name}-Dispatch`);
1023
1152
  if (this.registryConfig?.defaultExecutionMode) this.executionMode = this.registryConfig.defaultExecutionMode;
1024
1153
  this.log("ActionRegister initialized", {
1025
1154
  defaultExecutionMode: this.executionMode,
@@ -1053,10 +1182,15 @@ var ActionRegister = class {
1053
1182
  */
1054
1183
  get actions() {
1055
1184
  if (!this._actionsProxy) this._actionsProxy = new Proxy({}, { get: (_target, prop) => {
1185
+ if (typeof prop !== "string") return void 0;
1186
+ if (RESERVED_PROXY_KEYS.has(prop)) return void 0;
1056
1187
  const actionKey = prop;
1057
- if (typeof prop === "string" && this.pipelines.has(actionKey)) return (payload, options) => {
1058
- return this.dispatch(actionKey, payload, options);
1059
- };
1188
+ let dispatcher = this.actionDispatchers.get(prop);
1189
+ if (!dispatcher) {
1190
+ dispatcher = (payload, options) => this.dispatch(actionKey, ...[payload, options]);
1191
+ this.actionDispatchers.set(prop, dispatcher);
1192
+ }
1193
+ return dispatcher;
1060
1194
  } });
1061
1195
  return this._actionsProxy;
1062
1196
  }
@@ -1089,32 +1223,76 @@ var ActionRegister = class {
1089
1223
  */
1090
1224
  get actionsWithResult() {
1091
1225
  if (!this._actionsWithResultProxy) this._actionsWithResultProxy = new Proxy({}, { get: (_target, prop) => {
1226
+ if (typeof prop !== "string") return void 0;
1227
+ if (RESERVED_PROXY_KEYS.has(prop)) return void 0;
1092
1228
  const actionKey = prop;
1093
- if (typeof prop === "string" && this.pipelines.has(actionKey)) return (payload, options) => {
1094
- return this.dispatchWithResult(actionKey, payload, options);
1095
- };
1229
+ let dispatcher = this.actionResultDispatchers.get(prop);
1230
+ if (!dispatcher) {
1231
+ const dispatchAction = this.dispatchWithResult.bind(this);
1232
+ dispatcher = (payload, options) => dispatchAction(actionKey, ...[payload, options]);
1233
+ this.actionResultDispatchers.set(prop, dispatcher);
1234
+ }
1235
+ return dispatcher;
1096
1236
  } });
1097
1237
  return this._actionsWithResultProxy;
1098
1238
  }
1239
+ register(action, handler, config = {}) {
1240
+ return this.registerWithRole(action, handler, config, "legacy");
1241
+ }
1242
+ registerEffect(action, handler, config) {
1243
+ if (config.effectKind === "guard") return this.registerGuard(action, handler, config);
1244
+ return this.registerObserver(action, (event) => handler(event.payload, {
1245
+ signal: event.signal,
1246
+ getPayload: () => event.payload
1247
+ }), config);
1248
+ }
1249
+ /** Register an authorization or validation guard that always runs before
1250
+ * concurrent result arbitration. */
1251
+ registerGuard(action, handler, config = {}) {
1252
+ return this.registerWithRole(action, handler, {
1253
+ ...config,
1254
+ scheduling: "await-before-next",
1255
+ errorPolicy: "fatal"
1256
+ }, "guard");
1257
+ }
1258
+ /** Register a terminal observer. It runs after result aggregation and has
1259
+ * no controller, result, payload, or winner-selection capabilities. */
1260
+ registerObserver(action, handler, config = {}) {
1261
+ const handlerId = config.id ?? this.generateHandlerId(action);
1262
+ const existing = this.pipelines.get(action)?.find((item) => item.id === handlerId);
1263
+ if (existing && (existing.role ?? "legacy") !== "observer") throw new Error(`Action handler role conflict for "${String(action)}" and id "${handlerId}": cannot replace ${existing.role ?? "legacy"} with observer.`);
1264
+ if (existing && config.replaceExisting === false) return () => {};
1265
+ const unregister = this.registerWithRole(action, (() => void 0), {
1266
+ ...config,
1267
+ id: handlerId
1268
+ }, "observer");
1269
+ const registration = this.pipelines.get(action)?.find((item) => item.id === handlerId);
1270
+ if (!registration) {
1271
+ unregister();
1272
+ throw new Error(`Observer registration "${handlerId}" was not retained.`);
1273
+ }
1274
+ this.observerHandlers.set(registration, {
1275
+ handler,
1276
+ when: config.when ?? "always"
1277
+ });
1278
+ return () => {
1279
+ this.observerHandlers.delete(registration);
1280
+ unregister();
1281
+ };
1282
+ }
1099
1283
  /**
1100
- * Register an action handler with optional configuration
1101
- *
1102
- * @param action - The action type to register handler for
1103
- * @param handler - The handler function to execute
1104
- * @param config - Optional handler configuration including priority, tags, etc.
1105
- *
1106
- * @returns Unregister function to remove this handler
1107
- *
1108
- * @throws {Error} When maximum handlers limit is reached
1109
- *
1110
- * @see https://mineclover.github.io/context-action/en/guide/patterns/action/basic-usage
1111
- *
1284
+ * Register a handler that contributes the result declared for an action.
1285
+ *
1112
1286
  * @public
1113
1287
  */
1114
- register(action, handler, config = {}) {
1288
+ registerResult(action, handler, config) {
1289
+ return this.registerWithRole(action, handler, config ?? {}, "result");
1290
+ }
1291
+ registerWithRole(action, handler, config, role) {
1292
+ this.assertStringActionKey(action);
1115
1293
  this.assertAcceptingWork();
1116
1294
  const handlerId = config.id || this.generateHandlerId(action);
1117
- return this._performRegistrationSync(action, handler, config, handlerId);
1295
+ return this._performRegistrationSync(action, handler, config, handlerId, role);
1118
1296
  }
1119
1297
  /**
1120
1298
  * 🆕 Unified logging method with cached debug mode check
@@ -1128,6 +1306,9 @@ var ActionRegister = class {
1128
1306
  assertAcceptingWork() {
1129
1307
  if (this.lifecycleState !== "active") throw new ActionRegisterDestroyedError(this.name, this.lifecycleState);
1130
1308
  }
1309
+ assertStringActionKey(action) {
1310
+ if (typeof action !== "string") throw new TypeError("Action keys must be strings.");
1311
+ }
1131
1312
  rejectedLifecyclePromise() {
1132
1313
  const error = new ActionRegisterDestroyedError(this.name, this.lifecycleState === "active" ? "destroyed" : this.lifecycleState);
1133
1314
  const rejected = Promise.reject(error);
@@ -1197,44 +1378,35 @@ var ActionRegister = class {
1197
1378
  /**
1198
1379
  * 🆕 Perform synchronous handler registration
1199
1380
  */
1200
- _performRegistrationSync(action, handler, config, handlerId) {
1381
+ _performRegistrationSync(action, handler, config, handlerId, role = "legacy") {
1201
1382
  const registration = {
1202
1383
  handler,
1203
- config: {
1204
- priority: config.priority ?? 0,
1205
- id: handlerId,
1206
- blocking: config.blocking ?? false,
1207
- once: config.once ?? false,
1208
- debounce: config.debounce ?? void 0,
1209
- throttle: config.throttle ?? void 0,
1210
- replaceExisting: config.replaceExisting ?? true,
1211
- cleanup: config.cleanup,
1212
- condition: config.condition
1213
- },
1214
- id: handlerId
1384
+ config: resolveHandlerConfig(config, handlerId),
1385
+ id: handlerId,
1386
+ role
1215
1387
  };
1216
1388
  if (!this.pipelines.has(action)) this.pipelines.set(action, []);
1217
1389
  const pipeline = this.pipelines.get(action);
1218
- if (pipeline.length >= this.maxHandlersPerAction) {
1219
- console.warn(`Handler limit (${this.maxHandlersPerAction}) reached for action "${String(action)}". Registration ignored.`);
1220
- return () => {};
1221
- }
1390
+ const actionUnregisterFunctions = this.getUnregisterFunctions(action);
1222
1391
  const existingIndex = pipeline.findIndex((reg) => reg.id === handlerId);
1392
+ if (existingIndex === -1 && pipeline.length >= this.maxHandlersPerAction) throw new RangeError(`Handler limit (${this.maxHandlersPerAction}) reached for action "${String(action)}".`);
1223
1393
  if (existingIndex !== -1) {
1224
1394
  const existing = pipeline[existingIndex];
1225
- const existingUnregister = this.unregisterFunctions.get(handlerId);
1395
+ const existingUnregister = actionUnregisterFunctions.get(handlerId);
1396
+ if (existing && (existing.role ?? "legacy") !== role) throw new Error(`Action handler role conflict for "${String(action)}" and id "${handlerId}": cannot replace ${existing.role ?? "legacy"} with ${role}.`);
1226
1397
  if (registration.config.replaceExisting) {
1227
1398
  if (existing?.config.cleanup && typeof existing.config.cleanup === "function") try {
1228
1399
  existing.config.cleanup();
1229
1400
  } catch (cleanupError) {
1230
1401
  this.log(`Cleanup error for replaced handler: ${String(action)}`, cleanupError, "warn");
1231
1402
  }
1232
- if (existingUnregister) this.unregisterFunctions.delete(handlerId);
1403
+ if (existing) this.observerHandlers.delete(existing);
1404
+ if (existingUnregister) actionUnregisterFunctions.delete(handlerId);
1233
1405
  pipeline[existingIndex] = registration;
1234
1406
  pipeline.sort((a, b) => b.config.priority - a.config.priority);
1235
1407
  this.lastRegisteredTimestamps.set(action, /* @__PURE__ */ new Date());
1236
1408
  const newUnregister = this.createUnregisterFunction(action, handlerId, registration);
1237
- this.unregisterFunctions.set(handlerId, newUnregister);
1409
+ actionUnregisterFunctions.set(handlerId, newUnregister);
1238
1410
  this.log(`Handler replaced: ${String(action)}`, {
1239
1411
  handlerId,
1240
1412
  priority: config.priority,
@@ -1255,7 +1427,7 @@ var ActionRegister = class {
1255
1427
  if (existingUnregister) return existingUnregister;
1256
1428
  else {
1257
1429
  const newUnregister = this.createUnregisterFunction(action, handlerId, existing);
1258
- this.unregisterFunctions.set(handlerId, newUnregister);
1430
+ actionUnregisterFunctions.set(handlerId, newUnregister);
1259
1431
  return newUnregister;
1260
1432
  }
1261
1433
  }
@@ -1264,7 +1436,7 @@ var ActionRegister = class {
1264
1436
  pipeline.sort((a, b) => b.config.priority - a.config.priority);
1265
1437
  this.lastRegisteredTimestamps.set(action, /* @__PURE__ */ new Date());
1266
1438
  const unregister = this.createUnregisterFunction(action, handlerId, registration);
1267
- this.unregisterFunctions.set(handlerId, unregister);
1439
+ actionUnregisterFunctions.set(handlerId, unregister);
1268
1440
  this.log(`Handler registered: ${String(action)}`, {
1269
1441
  handlerId,
1270
1442
  priority: config.priority,
@@ -1272,59 +1444,233 @@ var ActionRegister = class {
1272
1444
  });
1273
1445
  return unregister;
1274
1446
  }
1275
- dispatch(action, payload, options) {
1447
+ dispatch(action, ...args) {
1448
+ this.assertStringActionKey(action);
1449
+ const [payload, options] = args;
1276
1450
  if (this.lifecycleState !== "active") return this.rejectedLifecyclePromise();
1277
1451
  const timeoutScope = this.createTimeoutScope(action, options);
1278
1452
  const dispatchHandlerPromises = /* @__PURE__ */ new Set();
1279
1453
  const attemptState = { count: 0 };
1280
- const operation = async () => {
1281
- if (!timeoutScope.options?.signal?.aborted) this.validatePayload(action, payload);
1282
- return this.executeWithRetry(async () => {
1454
+ const plan = this.resolveDispatchPlan(action, options);
1455
+ const hasTimingGuard = plan.debounceMs !== void 0 || plan.throttleMs !== void 0;
1456
+ let observerNotified = false;
1457
+ let terminalErrorReported = false;
1458
+ const reportTerminalError = (error) => {
1459
+ if (terminalErrorReported) return;
1460
+ terminalErrorReported = true;
1461
+ this.invokeErrorHandler(error, action, payload, options, attemptState.count);
1462
+ };
1463
+ const notifyObservers = async (event) => {
1464
+ if (observerNotified) return;
1465
+ observerNotified = true;
1466
+ await this.executeObservers(action, plan, event);
1467
+ };
1468
+ const pipelineOperation = async () => {
1469
+ const guard = plan.guards.length > 0 ? await this.executeGuardPhase(action, payload, timeoutScope.options, plan, dispatchHandlerPromises) : {
1470
+ allowed: true,
1471
+ payload,
1472
+ aborted: false,
1473
+ abortReason: void 0,
1474
+ error: void 0,
1475
+ errors: [],
1476
+ outcomes: [],
1477
+ executedHandlers: [],
1478
+ duration: 0
1479
+ };
1480
+ this.cleanupOneTimeHandlers(action, guard.executedHandlers, dispatchHandlerPromises);
1481
+ if (!guard.allowed) {
1482
+ await notifyObservers({
1483
+ action: String(action),
1484
+ payload: guard.payload,
1485
+ outcome: guard.aborted ? "cancelled" : "failed",
1486
+ result: void 0,
1487
+ errors: guard.errors,
1488
+ signal: timeoutScope.options?.signal
1489
+ });
1490
+ if (!guard.aborted && guard.errors.length > 0) throw guard.error ?? guard.errors[0]?.error ?? /* @__PURE__ */ new Error(`Guard phase failed for "${String(action)}"`);
1491
+ return;
1492
+ }
1493
+ const execution = await this.executeWithRetry(async (attemptSignal) => {
1283
1494
  const executedHandlers = [];
1284
1495
  try {
1285
- return await this._performDispatch(action, payload, timeoutScope.options, attemptState.count > 1, executedHandlers, dispatchHandlerPromises);
1496
+ return await this._performDispatchWithResult(action, guard.payload, this.withAttemptSignal(timeoutScope.options, attemptSignal), void 0, plan, executedHandlers, dispatchHandlerPromises);
1286
1497
  } finally {
1287
1498
  this.cleanupOneTimeHandlers(action, executedHandlers, dispatchHandlerPromises);
1288
1499
  }
1289
- }, timeoutScope.options, attemptState, void 0, () => this.getHandlerCount(action) > 0);
1500
+ }, timeoutScope.options, attemptState, (result) => result.outcome === "failed", () => this.getAttemptHandlers(action, plan).length > 0, void 0, this.shouldDrainBeforeRetry(plan, timeoutScope.options) ? () => this.drainAttemptHandlers(dispatchHandlerPromises) : void 0);
1501
+ if (execution.outcome === "failed") throw execution.errors[execution.errors.length - 1]?.error ?? /* @__PURE__ */ new Error(`Action "${String(action)}" failed`);
1502
+ const result = this.processResults(execution.results, execution.terminated, execution.terminated ? execution.result : void 0, options?.result);
1503
+ await notifyObservers({
1504
+ action: String(action),
1505
+ payload: guard.payload,
1506
+ outcome: execution.outcome,
1507
+ result,
1508
+ errors: execution.errors,
1509
+ signal: timeoutScope.options?.signal
1510
+ });
1511
+ };
1512
+ const operation = async () => {
1513
+ if (timeoutScope.options?.signal?.aborted) {
1514
+ await notifyObservers({
1515
+ action: String(action),
1516
+ payload,
1517
+ outcome: "cancelled",
1518
+ result: void 0,
1519
+ errors: [],
1520
+ signal: timeoutScope.options?.signal
1521
+ });
1522
+ return;
1523
+ }
1524
+ this.validatePayload(action, payload);
1525
+ this.validateResultOptions(options?.result);
1526
+ if (timeoutScope.options?.signal?.aborted) {
1527
+ await notifyObservers({
1528
+ action: String(action),
1529
+ payload,
1530
+ outcome: "cancelled",
1531
+ result: void 0,
1532
+ errors: [],
1533
+ signal: timeoutScope.options?.signal
1534
+ });
1535
+ return;
1536
+ }
1537
+ if (hasTimingGuard) {
1538
+ const admission = await this.evaluateTimingGuards(String(action), plan, timeoutScope.options?.signal);
1539
+ if (admission.aborted || timeoutScope.options?.signal?.aborted || admission.reason) {
1540
+ await notifyObservers({
1541
+ action: String(action),
1542
+ payload,
1543
+ outcome: admission.aborted || timeoutScope.options?.signal?.aborted ? "cancelled" : admission.reason === "Debounced execution" ? "debounced" : "throttled",
1544
+ result: void 0,
1545
+ errors: admission.reason ? [{
1546
+ handlerId: "admission",
1547
+ error: new Error(admission.reason),
1548
+ timestamp: Date.now(),
1549
+ severity: "blocking"
1550
+ }] : [],
1551
+ signal: timeoutScope.options?.signal
1552
+ });
1553
+ return;
1554
+ }
1555
+ }
1556
+ if (timeoutScope.options?.signal?.aborted) {
1557
+ await notifyObservers({
1558
+ action: String(action),
1559
+ payload,
1560
+ outcome: "cancelled",
1561
+ result: void 0,
1562
+ errors: [],
1563
+ signal: timeoutScope.options?.signal
1564
+ });
1565
+ return;
1566
+ }
1567
+ if (timeoutScope.options?.immediate || !this.dispatchQueue) return pipelineOperation();
1568
+ const queued = this.dispatchQueue.enqueueWithHandle(pipelineOperation, timeoutScope.options?.queuePriority ?? 0);
1569
+ timeoutScope.onTimeout((error) => queued.cancel(error));
1570
+ return queued.promise;
1290
1571
  };
1291
- const hasTimingGuard = options?.debounce !== void 0 || options?.throttle !== void 0 || this.pipelines.get(action)?.some((handler) => handler.config.debounce !== void 0 || handler.config.throttle !== void 0) === true;
1292
1572
  let dispatchPromise;
1573
+ let observedDispatchPromise;
1293
1574
  this.dispatchConstructionDepth += 1;
1294
1575
  try {
1295
- if (timeoutScope.options?.immediate || hasTimingGuard || !this.dispatchQueue) dispatchPromise = operation();
1296
- else {
1297
- const queued = this.dispatchQueue.enqueueWithHandle(operation, timeoutScope.options?.queuePriority ?? 0);
1298
- timeoutScope.onTimeout((error) => queued.cancel(error));
1299
- dispatchPromise = queued.promise;
1300
- }
1301
- this.trackDispatchPromise(dispatchPromise);
1576
+ dispatchPromise = operation();
1577
+ observedDispatchPromise = dispatchPromise.catch(async (error) => {
1578
+ reportTerminalError(error);
1579
+ await notifyObservers({
1580
+ action: String(action),
1581
+ payload,
1582
+ outcome: "failed",
1583
+ result: void 0,
1584
+ errors: [{
1585
+ handlerId: "dispatch",
1586
+ error: error instanceof Error ? error : new Error(String(error)),
1587
+ timestamp: Date.now(),
1588
+ severity: "blocking"
1589
+ }],
1590
+ signal: timeoutScope.options?.signal
1591
+ });
1592
+ throw error;
1593
+ });
1594
+ this.trackDispatchPromise(observedDispatchPromise);
1302
1595
  } finally {
1303
1596
  this.dispatchConstructionDepth -= 1;
1304
1597
  }
1305
- const observedPromise = this.raceWithTimeout(dispatchPromise, timeoutScope, dispatchHandlerPromises).catch((error) => {
1306
- this.invokeErrorHandler(error, action, payload, options, attemptState.count);
1598
+ const observedPromise = this.raceWithTimeout(observedDispatchPromise, timeoutScope, dispatchHandlerPromises).catch(async (error) => {
1599
+ reportTerminalError(error);
1600
+ if (!observerNotified) await this.trackGlobalHandlerPromise(notifyObservers({
1601
+ action: String(action),
1602
+ payload,
1603
+ outcome: "failed",
1604
+ result: void 0,
1605
+ errors: [{
1606
+ handlerId: "dispatch",
1607
+ error: error instanceof Error ? error : new Error(String(error)),
1608
+ timestamp: Date.now(),
1609
+ severity: "blocking"
1610
+ }],
1611
+ signal: timeoutScope.options?.signal
1612
+ }));
1307
1613
  throw error;
1308
1614
  });
1309
1615
  observedPromise.catch(() => {});
1310
1616
  return observedPromise;
1311
1617
  }
1312
1618
  /** Execute a dispatch operation with an optional whole-action retry policy. */
1313
- async executeWithRetry(operation, options, attemptState, shouldRetryResult, canRetry = () => true) {
1619
+ async executeWithRetry(operation, options, attemptState, shouldRetryResult, canRetry = () => true, telemetry, beforeRetry) {
1314
1620
  const configuredAttempts = options?.retryOnError?.maxAttempts ?? 1;
1315
1621
  const maxAttempts = Number.isFinite(configuredAttempts) ? Math.max(1, Math.floor(configuredAttempts)) : 1;
1316
1622
  const retryDelay = Math.max(0, options?.retryOnError?.delay ?? 0);
1317
1623
  while (attemptState.count < maxAttempts) {
1318
1624
  attemptState.count += 1;
1625
+ const attemptStartedAt = Date.now();
1626
+ const attemptController = new AbortController();
1319
1627
  try {
1320
- const result = await operation();
1321
- if (!(shouldRetryResult?.(result) ?? false) || attemptState.count >= maxAttempts || options?.signal?.aborted || !canRetry()) return result;
1628
+ const result = await operation(attemptController.signal);
1629
+ const shouldRetry = shouldRetryResult?.(result) ?? false;
1630
+ const canRetryAttempt = shouldRetry && attemptState.count < maxAttempts && !options?.signal?.aborted && canRetry();
1631
+ const attemptEndedAt = Date.now();
1632
+ telemetry?.attempts.push({
1633
+ startTime: attemptStartedAt,
1634
+ endTime: attemptEndedAt,
1635
+ duration: attemptEndedAt - attemptStartedAt,
1636
+ outcome: shouldRetry ? canRetryAttempt ? "retried" : "failed" : "succeeded"
1637
+ });
1638
+ if (telemetry) telemetry.pipelineDuration += attemptEndedAt - attemptStartedAt;
1639
+ if (!canRetryAttempt) return result;
1640
+ attemptController.abort(new ActionAttemptSupersededError(attemptState.count));
1641
+ await beforeRetry?.();
1642
+ const retryStartedAt = Date.now();
1643
+ const shouldContinue = await this.waitForRetry(retryDelay, options?.signal);
1644
+ if (telemetry) telemetry.retryDelayDuration += Date.now() - retryStartedAt;
1645
+ if (!shouldContinue) {
1646
+ telemetry?.attempts.push({
1647
+ startTime: Date.now(),
1648
+ endTime: Date.now(),
1649
+ duration: 0,
1650
+ outcome: "cancelled"
1651
+ });
1652
+ return result;
1653
+ }
1322
1654
  } catch (error) {
1323
- if (error instanceof ActionValidationError || attemptState.count >= maxAttempts || options?.signal?.aborted || !canRetry()) throw error;
1655
+ const attemptEndedAt = Date.now();
1656
+ const canRetryAttempt = !(error instanceof ActionValidationError || attemptState.count >= maxAttempts || options?.signal?.aborted || !canRetry());
1657
+ telemetry?.attempts.push({
1658
+ startTime: attemptStartedAt,
1659
+ endTime: attemptEndedAt,
1660
+ duration: attemptEndedAt - attemptStartedAt,
1661
+ outcome: canRetryAttempt ? "retried" : "failed"
1662
+ });
1663
+ if (telemetry) telemetry.pipelineDuration += attemptEndedAt - attemptStartedAt;
1664
+ if (!canRetryAttempt) throw error;
1665
+ attemptController.abort(new ActionAttemptSupersededError(attemptState.count));
1666
+ await beforeRetry?.();
1667
+ const retryStartedAt = Date.now();
1668
+ const shouldContinue = await this.waitForRetry(retryDelay, options?.signal);
1669
+ if (telemetry) telemetry.retryDelayDuration += Date.now() - retryStartedAt;
1670
+ if (!shouldContinue) throw error;
1324
1671
  }
1325
- await this.waitForRetry(retryDelay, options?.signal);
1326
1672
  }
1327
- return operation();
1673
+ return operation(new AbortController().signal);
1328
1674
  }
1329
1675
  trackDispatchPromise(promise) {
1330
1676
  this.activeDispatches.add(promise);
@@ -1342,6 +1688,43 @@ var ActionRegister = class {
1342
1688
  promise.then(remove, remove);
1343
1689
  return promise;
1344
1690
  }
1691
+ withAttemptSignal(options, attemptSignal) {
1692
+ const outerSignal = options?.signal;
1693
+ if (!outerSignal) return {
1694
+ ...options,
1695
+ signal: attemptSignal
1696
+ };
1697
+ if (typeof AbortSignal.any === "function") return {
1698
+ ...options,
1699
+ signal: AbortSignal.any([outerSignal, attemptSignal])
1700
+ };
1701
+ const controller = new AbortController();
1702
+ const forwardOuter = () => controller.abort(outerSignal.reason);
1703
+ const forwardAttempt = () => controller.abort(attemptSignal.reason);
1704
+ if (outerSignal.aborted) forwardOuter();
1705
+ else outerSignal.addEventListener("abort", forwardOuter, { once: true });
1706
+ if (attemptSignal.aborted) forwardAttempt();
1707
+ else attemptSignal.addEventListener("abort", forwardAttempt, { once: true });
1708
+ const cleanup = () => {
1709
+ outerSignal.removeEventListener("abort", forwardOuter);
1710
+ attemptSignal.removeEventListener("abort", forwardAttempt);
1711
+ };
1712
+ controller.signal.addEventListener("abort", cleanup, { once: true });
1713
+ return {
1714
+ ...options,
1715
+ signal: controller.signal,
1716
+ [ATTEMPT_SIGNAL_CLEANUP]: cleanup
1717
+ };
1718
+ }
1719
+ shouldDrainBeforeRetry(plan, options) {
1720
+ return (options?.retryOnError?.attemptBarrier ?? (plan.executionMode === "race" ? "abort-and-drain" : "abort-and-overlap")) === "abort-and-drain";
1721
+ }
1722
+ /** Do not begin a whole-action retry while a previous race loser is still
1723
+ * running. Handlers should still observe their signal for cancellation. */
1724
+ async drainAttemptHandlers(dispatchHandlerPromises) {
1725
+ const pending = [...dispatchHandlerPromises];
1726
+ if (pending.length > 0) await Promise.allSettled(pending);
1727
+ }
1345
1728
  trackGlobalHandlerPromise(promise) {
1346
1729
  this.activeHandlerPromises.add(promise);
1347
1730
  const remove = () => this.activeHandlerPromises.delete(promise);
@@ -1350,22 +1733,25 @@ var ActionRegister = class {
1350
1733
  }
1351
1734
  /** Abort-aware retry delay so cancellation does not wait for the full backoff. */
1352
1735
  waitForRetry(delay, signal) {
1353
- if (delay <= 0 || signal?.aborted) return Promise.resolve();
1736
+ if (signal?.aborted) return Promise.resolve(false);
1737
+ if (delay <= 0) return Promise.resolve(true);
1354
1738
  return new Promise((resolve) => {
1355
1739
  const timer = setTimeout(finish, delay);
1356
- const abort = () => finish();
1357
- function finish() {
1740
+ const abort = () => finish(false);
1741
+ function finish(shouldContinue = true) {
1358
1742
  clearTimeout(timer);
1359
1743
  signal?.removeEventListener("abort", abort);
1360
- resolve();
1744
+ resolve(shouldContinue);
1361
1745
  }
1362
1746
  signal?.addEventListener("abort", abort, { once: true });
1363
1747
  });
1364
1748
  }
1365
1749
  /** Build a wall-clock timeout that also participates in pipeline cancellation. */
1366
1750
  createTimeoutScope(action, options) {
1367
- const hasTimeout = options?.timeout !== void 0 && Number.isFinite(options.timeout);
1368
- const timeout = hasTimeout ? Math.max(0, options.timeout) : void 0;
1751
+ const configuredTimeout = options?.timeout;
1752
+ if (configuredTimeout !== void 0 && (!Number.isFinite(configuredTimeout) || configuredTimeout < 0)) throw new RangeError("timeout must be a non-negative finite number.");
1753
+ const hasTimeout = configuredTimeout !== void 0;
1754
+ const timeout = configuredTimeout;
1369
1755
  const timeoutController = hasTimeout ? new AbortController() : void 0;
1370
1756
  const signalCleanups = [];
1371
1757
  const timeoutCallbacks = /* @__PURE__ */ new Set();
@@ -1483,13 +1869,23 @@ var ActionRegister = class {
1483
1869
  errors: result.error.issues.map((issue) => issue.message)
1484
1870
  };
1485
1871
  }
1486
- createAbortedExecutionResult(startTime, handlersSkipped = 0, validation) {
1872
+ createAbortedExecutionResult(startTime, skippedRegistrations = [], validation) {
1487
1873
  const endTime = Date.now();
1874
+ const handlers = skippedRegistrations.map((registration) => ({
1875
+ id: registration.id,
1876
+ status: "skipped",
1877
+ executed: false,
1878
+ duration: 0,
1879
+ result: void 0,
1880
+ error: void 0,
1881
+ metadata: registration.config.metadata ? { ...registration.config.metadata } : void 0
1882
+ }));
1488
1883
  return {
1489
1884
  success: false,
1490
1885
  aborted: true,
1491
1886
  abortReason: "Action dispatch aborted by signal",
1492
1887
  terminated: false,
1888
+ outcome: "cancelled",
1493
1889
  validation,
1494
1890
  result: void 0,
1495
1891
  successResults: [],
@@ -1497,202 +1893,524 @@ var ActionRegister = class {
1497
1893
  failedResults: [],
1498
1894
  execution: {
1499
1895
  duration: endTime - startTime,
1896
+ admissionDuration: endTime - startTime,
1897
+ queueWaitDuration: 0,
1898
+ pipelineDuration: 0,
1500
1899
  handlersExecuted: 0,
1501
- handlersSkipped,
1900
+ handlersSkipped: handlers.length,
1502
1901
  handlersFailed: 0,
1503
1902
  startTime,
1504
1903
  endTime
1505
1904
  },
1506
- handlers: [],
1905
+ handlers,
1507
1906
  errors: []
1508
1907
  };
1509
1908
  }
1909
+ resolveDispatchPlan(action, options) {
1910
+ const pipelineSnapshot = [...this.pipelines.get(action) ?? []];
1911
+ const guards = pipelineSnapshot.filter((handler) => handler.role === "guard");
1912
+ const filterableHandlers = pipelineSnapshot.filter((handler) => handler.role !== "guard");
1913
+ const filteredHandlers = options?.filter ? this.filterHandlers(filterableHandlers, options.filter) : filterableHandlers;
1914
+ const eligibleHandlers = [...guards, ...filteredHandlers];
1915
+ const admissionHandlers = eligibleHandlers.filter((handler) => handler.role !== "observer");
1916
+ const debounceMs = options?.debounce ?? admissionHandlers.find((handler) => handler.config.debounce !== void 0)?.config.debounce;
1917
+ const throttleMs = options?.throttle ?? admissionHandlers.find((handler) => handler.config.throttle !== void 0)?.config.throttle;
1918
+ return {
1919
+ pipelineSnapshot,
1920
+ eligibleHandlers,
1921
+ guards,
1922
+ results: filteredHandlers.filter((handler) => handler.role !== "observer"),
1923
+ observers: filteredHandlers.filter((handler) => handler.role === "observer"),
1924
+ debounceMs,
1925
+ throttleMs,
1926
+ executionMode: options?.executionMode ?? this.actionExecutionModes.get(action) ?? this.executionMode
1927
+ };
1928
+ }
1929
+ async evaluateTimingGuards(actionKey, plan, signal) {
1930
+ const { debounceMs, throttleMs } = plan;
1931
+ if (signal?.aborted) return { aborted: true };
1932
+ if (debounceMs !== void 0 && !await this.actionGuard.debounce(actionKey, debounceMs, signal)) return signal?.aborted ? { aborted: true } : {
1933
+ aborted: false,
1934
+ reason: "Debounced execution"
1935
+ };
1936
+ if (throttleMs !== void 0 && !this.actionGuard.throttle(actionKey, throttleMs, signal)) return signal?.aborted ? { aborted: true } : {
1937
+ aborted: false,
1938
+ reason: "Throttled execution"
1939
+ };
1940
+ return { aborted: false };
1941
+ }
1510
1942
  /**
1511
- * 🆕 실제 디스패치 작업 수행 (큐에서 호출됨)
1943
+ * Keep the dispatch plan stable across retries while honoring handlers that
1944
+ * were consumed by the `once` lifecycle after an earlier attempt.
1512
1945
  */
1513
- async _performDispatch(action, payload, options, skipGuards, executedHandlers, dispatchHandlerPromises) {
1514
- this.log(`Starting dispatch for action '${String(action)}'`, {
1515
- hasPayload: payload !== void 0,
1516
- payloadType: payload?.constructor?.name || typeof payload,
1517
- options: options ? Object.keys(options) : "none",
1518
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1519
- });
1520
- if (payload instanceof Event && process.env.NODE_ENV === "development") console.warn(`Event object passed to action "${String(action)}"`, payload.type);
1521
- const [effectiveSignal, autoAbortController, cleanup] = this.createAbortSignal(options);
1522
- if (options?.autoAbort?.onControllerCreated && autoAbortController) options.autoAbort.onControllerCreated(autoAbortController);
1523
- if (effectiveSignal?.aborted) {
1524
- this.log(`Dispatch aborted before execution for '${String(action)}'`);
1525
- cleanup();
1526
- return;
1527
- }
1528
- const pipeline = this.pipelines.get(action);
1529
- this.log(`Pipeline lookup for '${String(action)}'`, {
1530
- pipelineExists: Boolean(pipeline),
1531
- handlersCount: pipeline?.length || 0,
1532
- allRegisteredActions: Array.from(this.pipelines.keys()),
1533
- pipelineMap: Object.fromEntries(Array.from(this.pipelines.entries()).map(([k, v]) => [k, v.length]))
1946
+ getAttemptHandlers(action, plan) {
1947
+ const activePipeline = this.pipelines.get(action) ?? [];
1948
+ return plan.results.filter((handler) => !handler.config.once || activePipeline.includes(handler));
1949
+ }
1950
+ getObservers(action, plan) {
1951
+ const activePipeline = this.pipelines.get(action) ?? [];
1952
+ return plan.observers.flatMap((registration) => {
1953
+ const observer = this.observerHandlers.get(registration);
1954
+ return registration.role === "observer" && activePipeline.includes(registration) && observer ? [[registration, observer]] : [];
1534
1955
  });
1535
- if (!pipeline || pipeline.length === 0) {
1536
- const warningMessage = `⚠️ Action '${String(action)}' has no registered handlers. This action will be ignored.`;
1537
- if (process.env.NODE_ENV === "development") {
1538
- console.warn(warningMessage);
1539
- console.warn("💡 Tip: Register a handler using registry.register() before dispatching this action.");
1540
- console.warn("📋 Available actions:", Array.from(this.pipelines.keys()));
1541
- }
1542
- this.log(`No handlers found for action '${String(action)}', dispatch cancelled`, {}, "warn");
1543
- cleanup();
1544
- return;
1545
- }
1546
- const filteredHandlers = options?.filter ? this.filterHandlers(pipeline, options.filter) : pipeline;
1547
- const actionKey = String(action);
1548
- let throttleMs;
1549
- let debounceMs;
1550
- if (options?.throttle !== void 0) throttleMs = options.throttle;
1551
- else if (filteredHandlers.length > 0) {
1552
- for (const handler of filteredHandlers) if (handler.config.throttle !== void 0) {
1553
- throttleMs = handler.config.throttle;
1554
- break;
1555
- }
1556
- }
1557
- if (options?.debounce !== void 0) debounceMs = options.debounce;
1558
- else if (filteredHandlers.length > 0) {
1559
- for (const handler of filteredHandlers) if (handler.config.debounce !== void 0) {
1560
- debounceMs = handler.config.debounce;
1561
- break;
1562
- }
1563
- }
1564
- if (!skipGuards && debounceMs !== void 0) {
1565
- if (!await this.actionGuard.debounce(actionKey, debounceMs)) {
1566
- cleanup();
1567
- return;
1956
+ }
1957
+ /** Observers run after the canonical result has been constructed. Their
1958
+ * failures are isolated from that immutable result; detached observers are
1959
+ * still tracked for registry shutdown. */
1960
+ async executeObservers(action, plan, event) {
1961
+ const observerEvent = this.safeSnapshotObserverEvent(event);
1962
+ for (const [registration, observerEntry] of this.getObservers(action, plan)) {
1963
+ const successful = observerEvent.outcome === "completed" || observerEvent.outcome === "completed_with_errors";
1964
+ if (observerEntry.when === "success" && !successful) continue;
1965
+ if (observerEntry.when === "failure" && successful) continue;
1966
+ let shouldRun = true;
1967
+ try {
1968
+ shouldRun = registration.config.condition?.(observerEvent.payload) ?? true;
1969
+ } catch (error) {
1970
+ this.log(`Observer condition failed for ${String(action)}`, error, "warn");
1971
+ continue;
1568
1972
  }
1569
- }
1570
- if (!skipGuards && throttleMs !== void 0) {
1571
- if (!this.actionGuard.throttle(actionKey, throttleMs)) {
1572
- cleanup();
1573
- return;
1973
+ if (!shouldRun) continue;
1974
+ const detachedOnce = registration.config.once && this.removeRegistration(action, registration, false);
1975
+ const cleanupOnce = () => {
1976
+ if (detachedOnce) this.runRegistrationCleanup(action, registration);
1977
+ };
1978
+ const invocation = Promise.resolve().then(() => observerEntry.handler(observerEvent));
1979
+ if (registration.config.scheduling === "start-and-continue") this.trackGlobalHandlerPromise(invocation).then(cleanupOnce, (error) => {
1980
+ this.log(`Observer failed for ${String(action)}`, error, "warn");
1981
+ cleanupOnce();
1982
+ });
1983
+ else try {
1984
+ await invocation;
1985
+ } catch (error) {
1986
+ this.log(`Observer failed for ${String(action)}`, error, "warn");
1987
+ } finally {
1988
+ cleanupOnce();
1574
1989
  }
1575
1990
  }
1576
- if (effectiveSignal?.aborted) {
1577
- this.log(`Dispatch aborted during guard processing for '${String(action)}'`);
1578
- cleanup();
1579
- return;
1991
+ }
1992
+ /** Give JavaScript observers an isolated, shallowly immutable terminal view.
1993
+ * Result payloads are intentionally not deep-cloned: arbitrary result values
1994
+ * may be class instances, streams, or identity-bearing domain objects. */
1995
+ snapshotObserverEvent(event) {
1996
+ const freezeValue = (value) => {
1997
+ if (Array.isArray(value)) return Object.freeze([...value]);
1998
+ if (value !== null && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype) return Object.freeze({ ...value });
1999
+ return value;
2000
+ };
2001
+ return Object.freeze({
2002
+ ...event,
2003
+ payload: freezeValue(event.payload),
2004
+ result: freezeValue(event.result),
2005
+ errors: Object.freeze(event.errors.map((error) => Object.freeze({ ...error })))
2006
+ });
2007
+ }
2008
+ /** A diagnostic observer must never make an already constructed canonical
2009
+ * result reject, including when shallow-copying a Proxy/getter throws. */
2010
+ safeSnapshotObserverEvent(event) {
2011
+ try {
2012
+ return this.snapshotObserverEvent(event);
2013
+ } catch (error) {
2014
+ this.log("Observer snapshot failed", error, "warn");
2015
+ return Object.freeze({
2016
+ action: event.action,
2017
+ payload: event.payload,
2018
+ outcome: event.outcome,
2019
+ result: void 0,
2020
+ errors: Object.freeze([]),
2021
+ ...event.signal === void 0 ? {} : { signal: event.signal }
2022
+ });
1580
2023
  }
1581
- const currentExecutionMode = options?.executionMode || this.actionExecutionModes.get(action) || this.executionMode;
2024
+ }
2025
+ /** Execute the selected guard snapshot once for the whole dispatch. Guards
2026
+ * are deliberately outside the retry loop: authorization and normalization
2027
+ * belong to admission, not to each provider attempt. */
2028
+ async executeGuardPhase(action, payload, options, plan, dispatchHandlerPromises) {
2029
+ if (plan.guards.length === 0) return {
2030
+ allowed: true,
2031
+ payload,
2032
+ aborted: false,
2033
+ abortReason: void 0,
2034
+ error: void 0,
2035
+ errors: [],
2036
+ outcomes: [],
2037
+ executedHandlers: [],
2038
+ duration: 0
2039
+ };
2040
+ const [signal, autoAbortController, cleanup] = this.createAbortSignal(options);
1582
2041
  const context = {
1583
2042
  action: String(action),
1584
2043
  payload,
1585
- handlers: [...filteredHandlers],
2044
+ handlers: [...plan.guards],
1586
2045
  executedHandlers: [],
1587
- deferOnceCleanup: true,
1588
- signal: effectiveSignal ?? this.lifecycleController.signal,
2046
+ handlerOutcomes: [],
2047
+ claimOnce: (registration) => this.claimOnceRegistration(action, registration),
2048
+ signal: signal ?? this.lifecycleController.signal,
1589
2049
  trackHandlerPromise: (promise) => this.trackHandlerPromise(promise, dispatchHandlerPromises),
1590
2050
  aborted: false,
1591
2051
  abortReason: void 0,
1592
2052
  currentIndex: 0,
1593
2053
  jumpToPriority: void 0,
1594
2054
  jumpCount: 0,
1595
- maxJumps: 10,
1596
- executionMode: currentExecutionMode,
2055
+ maxJumps: this.maxJumps,
2056
+ executionMode: "sequential",
1597
2057
  results: [],
1598
2058
  terminated: false,
1599
2059
  terminationResult: void 0
1600
2060
  };
1601
- const abortHandler = effectiveSignal ? () => {
2061
+ const abortHandler = signal ? () => {
1602
2062
  context.aborted = true;
1603
- context.abortReason = typeof effectiveSignal.reason === "string" ? effectiveSignal.reason : "Action dispatch aborted by signal";
2063
+ context.abortReason = typeof signal.reason === "string" ? signal.reason : "Action dispatch aborted by signal";
1604
2064
  } : void 0;
1605
- if (effectiveSignal && abortHandler) effectiveSignal.addEventListener("abort", abortHandler, { once: true });
2065
+ signal?.addEventListener("abort", abortHandler, { once: true });
2066
+ let error;
1606
2067
  try {
1607
- await this.executePipeline(context, dispatchHandlerPromises, autoAbortController, options?.autoAbort);
1608
- this.log(`Pipeline execution succeeded for ${String(action)}`);
1609
- } catch (error) {
1610
- this.log(`Pipeline execution failed for ${String(action)}`, error, "error");
1611
- throw error;
2068
+ await executeSequential(context, (registration, _index) => {
2069
+ const controller = this.createController(context, autoAbortController, options?.autoAbort, void 0, false);
2070
+ return {
2071
+ signal: controller.signal,
2072
+ getPayload: controller.getPayload,
2073
+ modifyPayload: controller.modifyPayload,
2074
+ abort: controller.abort
2075
+ };
2076
+ });
2077
+ } catch (caught) {
2078
+ error = caught instanceof Error ? caught : new Error(String(caught));
1612
2079
  } finally {
1613
- executedHandlers?.push(...context.executedHandlers ?? []);
1614
- if (effectiveSignal && abortHandler) effectiveSignal.removeEventListener("abort", abortHandler);
1615
- this.cleanupSignalsAfterStartedHandlers(cleanup, dispatchHandlerPromises);
2080
+ if (signal && abortHandler) signal.removeEventListener("abort", abortHandler);
2081
+ this.cleanupSignalsAfterStartedHandlers(() => {
2082
+ cleanup();
2083
+ options?.[ATTEMPT_SIGNAL_CLEANUP]?.();
2084
+ }, dispatchHandlerPromises);
1616
2085
  }
2086
+ const errors = [...context.collectedErrors ?? []];
2087
+ if (error && !errors.some((entry) => entry.error === error)) errors.push({
2088
+ handlerId: "guard",
2089
+ error,
2090
+ timestamp: Date.now(),
2091
+ severity: "blocking"
2092
+ });
2093
+ return {
2094
+ allowed: !context.aborted && error === void 0 && errors.length === 0,
2095
+ payload: context.payload,
2096
+ aborted: context.aborted,
2097
+ abortReason: context.abortReason,
2098
+ error,
2099
+ errors,
2100
+ outcomes: (context.handlerOutcomes ?? []).map(snapshotHandlerOutcome),
2101
+ executedHandlers: context.executedHandlers ?? [],
2102
+ duration: (context.handlerOutcomes ?? []).reduce((total, outcome) => total + (outcome.duration ?? 0), 0)
2103
+ };
1617
2104
  }
1618
- /**
1619
- * Dispatch an action and return detailed execution results
1620
- *
1621
- * @param action - The action type to dispatch
1622
- * @param payload - The action payload data
1623
- * @param options - Optional dispatch options including result collection strategy
1624
- *
1625
- * @returns Promise resolving to comprehensive execution results
1626
- *
1627
- * @see https://mineclover.github.io/context-action/en/guide/patterns/action/basic-usage
1628
- *
1629
- * @public
1630
- */
1631
- dispatchWithResult(action, payload, options) {
2105
+ createGuardRejectedResult(startTime, validation, guard, selectedHandlers) {
2106
+ const endTime = Date.now();
2107
+ const outcomeById = new Map(guard.outcomes.map((outcome) => [outcome.id, outcome]));
2108
+ const handlers = selectedHandlers.map((registration) => {
2109
+ const outcome = outcomeById.get(registration.id);
2110
+ return outcome ? {
2111
+ ...snapshotHandlerOutcome(outcome),
2112
+ result: void 0
2113
+ } : {
2114
+ id: registration.id,
2115
+ status: "skipped",
2116
+ executed: false,
2117
+ duration: 0,
2118
+ result: void 0,
2119
+ error: void 0,
2120
+ metadata: registration.config.metadata ? { ...registration.config.metadata } : void 0
2121
+ };
2122
+ });
2123
+ return {
2124
+ success: false,
2125
+ aborted: guard.aborted,
2126
+ abortReason: guard.abortReason ?? guard.error?.message,
2127
+ terminated: false,
2128
+ outcome: guard.aborted ? "cancelled" : "failed",
2129
+ validation,
2130
+ result: void 0,
2131
+ successResults: [],
2132
+ results: [],
2133
+ failedResults: guard.errors.map((error) => ({
2134
+ handlerId: error.handlerId,
2135
+ error: error.error,
2136
+ expectedType: "unknown"
2137
+ })),
2138
+ execution: {
2139
+ duration: endTime - startTime,
2140
+ admissionDuration: 0,
2141
+ queueWaitDuration: 0,
2142
+ pipelineDuration: endTime - startTime,
2143
+ handlersExecuted: handlers.filter((handler) => handler.executed).length,
2144
+ handlersSkipped: handlers.filter((handler) => !handler.executed).length,
2145
+ handlersFailed: handlers.filter((handler) => handler.status === "failed").length,
2146
+ startTime,
2147
+ endTime
2148
+ },
2149
+ handlers,
2150
+ errors: guard.errors
2151
+ };
2152
+ }
2153
+ createTimingGuardResult(reason, startTime, handlers, validation) {
2154
+ const endTime = Date.now();
2155
+ return {
2156
+ success: false,
2157
+ aborted: false,
2158
+ abortReason: reason,
2159
+ terminated: false,
2160
+ outcome: reason === "Debounced execution" ? "debounced" : "throttled",
2161
+ validation,
2162
+ result: void 0,
2163
+ successResults: [],
2164
+ results: [],
2165
+ failedResults: [],
2166
+ execution: {
2167
+ duration: endTime - startTime,
2168
+ admissionDuration: endTime - startTime,
2169
+ queueWaitDuration: 0,
2170
+ pipelineDuration: 0,
2171
+ handlersExecuted: 0,
2172
+ handlersSkipped: handlers.length,
2173
+ handlersFailed: 0,
2174
+ startTime,
2175
+ endTime
2176
+ },
2177
+ handlers: handlers.map((handler) => ({
2178
+ id: handler.id,
2179
+ status: "skipped",
2180
+ executed: false,
2181
+ duration: 0,
2182
+ result: void 0,
2183
+ error: void 0,
2184
+ metadata: handler.config.metadata ? { ...handler.config.metadata } : void 0
2185
+ })),
2186
+ errors: []
2187
+ };
2188
+ }
2189
+ dispatchWithResult(action, ...args) {
2190
+ this.assertStringActionKey(action);
2191
+ const [payload, options] = args;
1632
2192
  if (this.lifecycleState !== "active") return this.rejectedLifecyclePromise();
1633
2193
  const timeoutScope = this.createTimeoutScope(action, options);
2194
+ const dispatchStartTime = Date.now();
1634
2195
  const dispatchHandlerPromises = /* @__PURE__ */ new Set();
1635
2196
  const attemptState = { count: 0 };
1636
2197
  let validation;
1637
- const operation = async () => {
1638
- if (!timeoutScope.options?.signal?.aborted) validation = this.validatePayload(action, payload);
1639
- return this.executeWithRetry(async () => {
2198
+ let observerPayload = payload;
2199
+ let admissionEndedAt = dispatchStartTime;
2200
+ let pipelineStartedAt;
2201
+ const retryTelemetry = {
2202
+ pipelineDuration: 0,
2203
+ retryDelayDuration: 0,
2204
+ attempts: []
2205
+ };
2206
+ const plan = this.resolveDispatchPlan(action, options);
2207
+ const hasTimingGuard = plan.debounceMs !== void 0 || plan.throttleMs !== void 0;
2208
+ const pipelineOperation = async () => {
2209
+ pipelineStartedAt = Date.now();
2210
+ const guard = plan.guards.length > 0 ? await this.executeGuardPhase(action, payload, timeoutScope.options, plan, dispatchHandlerPromises) : {
2211
+ allowed: true,
2212
+ payload,
2213
+ aborted: false,
2214
+ abortReason: void 0,
2215
+ error: void 0,
2216
+ errors: [],
2217
+ outcomes: [],
2218
+ executedHandlers: [],
2219
+ duration: 0
2220
+ };
2221
+ this.cleanupOneTimeHandlers(action, guard.executedHandlers, dispatchHandlerPromises);
2222
+ observerPayload = guard.payload;
2223
+ if (!guard.allowed) return this.createGuardRejectedResult(pipelineStartedAt, validation, guard, [...plan.guards, ...plan.results]);
2224
+ const rawExecution = await this.executeWithRetry(async (attemptSignal) => {
1640
2225
  const executedHandlers = [];
1641
2226
  try {
1642
- return await this._performDispatchWithResult(action, payload, timeoutScope.options, validation, attemptState.count > 1, executedHandlers, dispatchHandlerPromises);
2227
+ return await this._performDispatchWithResult(action, guard.payload, this.withAttemptSignal(timeoutScope.options, attemptSignal), validation, plan, executedHandlers, dispatchHandlerPromises);
1643
2228
  } finally {
1644
2229
  this.cleanupOneTimeHandlers(action, executedHandlers, dispatchHandlerPromises);
1645
2230
  }
1646
- }, timeoutScope.options, attemptState, (result) => !result.success && !result.aborted && this.getHandlerCount(action) > 0, () => this.getHandlerCount(action) > 0);
2231
+ }, timeoutScope.options, attemptState, (result) => result.outcome === "failed", () => this.getAttemptHandlers(action, plan).length > 0, retryTelemetry, this.shouldDrainBeforeRetry(plan, timeoutScope.options) ? () => this.drainAttemptHandlers(dispatchHandlerPromises) : void 0);
2232
+ if (timeoutScope.options?.signal?.aborted) return {
2233
+ ...rawExecution,
2234
+ success: false,
2235
+ aborted: true,
2236
+ abortReason: typeof timeoutScope.options.signal.reason === "string" ? timeoutScope.options.signal.reason : "Action dispatch aborted by signal",
2237
+ outcome: "cancelled"
2238
+ };
2239
+ const executionWithGuards = {
2240
+ ...rawExecution,
2241
+ handlers: [...guard.outcomes.map((outcome) => ({
2242
+ ...outcome,
2243
+ result: void 0
2244
+ })), ...rawExecution.handlers],
2245
+ execution: {
2246
+ ...rawExecution.execution,
2247
+ handlersExecuted: guard.outcomes.filter((outcome) => outcome.executed).length + rawExecution.execution.handlersExecuted,
2248
+ handlersSkipped: guard.outcomes.filter((outcome) => !outcome.executed).length + rawExecution.execution.handlersSkipped,
2249
+ handlersFailed: guard.outcomes.filter((outcome) => outcome.status === "failed").length + rawExecution.execution.handlersFailed
2250
+ }
2251
+ };
2252
+ const resultProcessingStartedAt = Date.now();
2253
+ const result = this.processResults(executionWithGuards.results, executionWithGuards.terminated, executionWithGuards.terminated ? executionWithGuards.result : void 0, options?.result);
2254
+ const resultProcessingDuration = Date.now() - resultProcessingStartedAt;
2255
+ return {
2256
+ ...executionWithGuards,
2257
+ result,
2258
+ execution: {
2259
+ ...executionWithGuards.execution,
2260
+ pipelineDuration: guard.duration + retryTelemetry.pipelineDuration,
2261
+ retryDelayDuration: retryTelemetry.retryDelayDuration,
2262
+ resultProcessingDuration,
2263
+ attempts: retryTelemetry.attempts
2264
+ }
2265
+ };
2266
+ };
2267
+ const operation = async () => {
2268
+ if (timeoutScope.options?.signal?.aborted) {
2269
+ admissionEndedAt = Date.now();
2270
+ return this.createAbortedExecutionResult(dispatchStartTime, [...plan.guards, ...plan.results]);
2271
+ }
2272
+ validation = this.validatePayload(action, payload);
2273
+ if (timeoutScope.options?.signal?.aborted) {
2274
+ admissionEndedAt = Date.now();
2275
+ return this.createAbortedExecutionResult(dispatchStartTime, [...plan.guards, ...plan.results], validation);
2276
+ }
2277
+ this.validateResultOptions(options?.result);
2278
+ if (hasTimingGuard) {
2279
+ const admission = await this.evaluateTimingGuards(String(action), plan, timeoutScope.options?.signal);
2280
+ if (admission.aborted || timeoutScope.options?.signal?.aborted) {
2281
+ admissionEndedAt = Date.now();
2282
+ return this.createAbortedExecutionResult(dispatchStartTime, [...plan.guards, ...plan.results], validation);
2283
+ }
2284
+ if (admission.reason) {
2285
+ admissionEndedAt = Date.now();
2286
+ return this.createTimingGuardResult(admission.reason, dispatchStartTime, [...plan.guards, ...plan.results], validation);
2287
+ }
2288
+ }
2289
+ if (timeoutScope.options?.signal?.aborted) {
2290
+ admissionEndedAt = Date.now();
2291
+ return this.createAbortedExecutionResult(dispatchStartTime, [...plan.guards, ...plan.results], validation);
2292
+ }
2293
+ admissionEndedAt = Date.now();
2294
+ if (timeoutScope.options?.immediate || !this.dispatchQueue) return pipelineOperation();
2295
+ const queued = this.dispatchQueue.enqueueWithHandle(pipelineOperation, timeoutScope.options?.queuePriority ?? 0);
2296
+ timeoutScope.onTimeout((error) => queued.cancel(error));
2297
+ return queued.promise;
2298
+ };
2299
+ let observerNotified = false;
2300
+ let terminalErrorReported = false;
2301
+ const reportTerminalError = (error) => {
2302
+ if (terminalErrorReported) return;
2303
+ terminalErrorReported = true;
2304
+ this.invokeErrorHandler(error, action, payload, options, attemptState.count);
2305
+ };
2306
+ const notifyObservers = async (event) => {
2307
+ if (observerNotified) return;
2308
+ observerNotified = true;
2309
+ await this.executeObservers(action, plan, event);
1647
2310
  };
1648
- const shouldQueue = !timeoutScope.options?.immediate && Boolean(this.dispatchQueue) && timeoutScope.options?.queuePriority !== void 0;
1649
2311
  let dispatchPromise;
2312
+ let observedDispatchPromise;
1650
2313
  this.dispatchConstructionDepth += 1;
1651
2314
  try {
1652
- if (shouldQueue) {
1653
- const queued = this.dispatchQueue.enqueueWithHandle(operation, timeoutScope.options.queuePriority);
1654
- timeoutScope.onTimeout((error) => queued.cancel(error));
1655
- dispatchPromise = queued.promise;
1656
- } else dispatchPromise = operation();
1657
- this.trackDispatchPromise(dispatchPromise);
2315
+ dispatchPromise = operation();
2316
+ observedDispatchPromise = dispatchPromise.then(async (result) => {
2317
+ const dispatchEndedAt = Date.now();
2318
+ const pipelineDuration = pipelineStartedAt === void 0 ? 0 : result.execution.pipelineDuration;
2319
+ const completedResult = {
2320
+ ...result,
2321
+ execution: {
2322
+ ...result.execution,
2323
+ duration: dispatchEndedAt - dispatchStartTime,
2324
+ admissionDuration: Math.max(0, admissionEndedAt - dispatchStartTime),
2325
+ queueWaitDuration: pipelineStartedAt === void 0 ? 0 : Math.max(0, pipelineStartedAt - admissionEndedAt),
2326
+ pipelineDuration,
2327
+ startTime: dispatchStartTime,
2328
+ endTime: dispatchEndedAt
2329
+ }
2330
+ };
2331
+ if (completedResult.outcome === "failed") {
2332
+ const terminalError = completedResult.errors[completedResult.errors.length - 1]?.error ?? /* @__PURE__ */ new Error(`Action "${String(action)}" failed`);
2333
+ reportTerminalError(terminalError);
2334
+ }
2335
+ await notifyObservers({
2336
+ action: String(action),
2337
+ payload: observerPayload,
2338
+ outcome: completedResult.outcome,
2339
+ result: completedResult.result,
2340
+ errors: completedResult.errors,
2341
+ signal: timeoutScope.options?.signal
2342
+ });
2343
+ return completedResult;
2344
+ }, async (error) => {
2345
+ reportTerminalError(error);
2346
+ await notifyObservers({
2347
+ action: String(action),
2348
+ payload: observerPayload,
2349
+ outcome: "failed",
2350
+ result: void 0,
2351
+ errors: [{
2352
+ handlerId: "dispatch",
2353
+ error: error instanceof Error ? error : new Error(String(error)),
2354
+ timestamp: Date.now(),
2355
+ severity: "blocking"
2356
+ }],
2357
+ signal: timeoutScope.options?.signal
2358
+ });
2359
+ throw error;
2360
+ });
2361
+ this.trackDispatchPromise(observedDispatchPromise);
1658
2362
  } finally {
1659
2363
  this.dispatchConstructionDepth -= 1;
1660
2364
  }
1661
- const observedPromise = this.raceWithTimeout(dispatchPromise, timeoutScope, dispatchHandlerPromises).then((result) => {
1662
- if (!result.success && !result.aborted) {
1663
- const terminalError = result.errors[result.errors.length - 1]?.error ?? /* @__PURE__ */ new Error(`Action "${String(action)}" failed`);
1664
- this.invokeErrorHandler(terminalError, action, payload, options, attemptState.count);
1665
- }
1666
- return result;
1667
- }, (error) => {
1668
- this.invokeErrorHandler(error, action, payload, options, attemptState.count);
2365
+ const observedPromise = this.raceWithTimeout(observedDispatchPromise, timeoutScope, dispatchHandlerPromises).catch(async (error) => {
2366
+ reportTerminalError(error);
2367
+ if (!observerNotified) await this.trackGlobalHandlerPromise(notifyObservers({
2368
+ action: String(action),
2369
+ payload: observerPayload,
2370
+ outcome: "failed",
2371
+ result: void 0,
2372
+ errors: [{
2373
+ handlerId: "dispatch",
2374
+ error: error instanceof Error ? error : new Error(String(error)),
2375
+ timestamp: Date.now(),
2376
+ severity: "blocking"
2377
+ }],
2378
+ signal: timeoutScope.options?.signal
2379
+ }));
1669
2380
  throw error;
1670
2381
  });
1671
2382
  observedPromise.catch(() => {});
1672
2383
  return observedPromise;
1673
2384
  }
1674
- async _performDispatchWithResult(action, payload, options, validation, skipGuards, executedHandlers, dispatchHandlerPromises) {
2385
+ async _performDispatchWithResult(action, payload, options, validation, plan, executedHandlers, dispatchHandlerPromises) {
1675
2386
  const _startTime = Date.now();
1676
2387
  const [effectiveSignal, autoAbortController, cleanup] = this.createAbortSignal(options);
1677
2388
  if (options?.autoAbort?.onControllerCreated && autoAbortController) options.autoAbort.onControllerCreated(autoAbortController);
1678
2389
  if (effectiveSignal?.aborted) {
1679
2390
  cleanup();
1680
- return this.createAbortedExecutionResult(_startTime, 0, validation);
2391
+ return this.createAbortedExecutionResult(_startTime, plan.results, validation);
1681
2392
  }
1682
- const pipeline = this.pipelines.get(action);
2393
+ const pipeline = plan.pipelineSnapshot;
1683
2394
  if (!pipeline || pipeline.length === 0) {
2395
+ this.log(`Pipeline lookup for '${String(action)}'`, {
2396
+ pipelineExists: false,
2397
+ handlersCount: 0,
2398
+ allRegisteredActions: Array.from(this.pipelines.keys())
2399
+ });
1684
2400
  const warningMessage = `⚠️ Action '${String(action)}' has no registered handlers. This action will be ignored.`;
1685
- if (process.env.NODE_ENV === "development") {
2401
+ if (this.isDebugMode) {
1686
2402
  console.warn(warningMessage);
1687
2403
  console.warn("💡 Tip: Register a handler using registry.register() before dispatching this action.");
1688
2404
  console.warn("📋 Available actions:", Array.from(this.pipelines.keys()));
1689
2405
  }
2406
+ this.log(`No handlers found for action '${String(action)}', dispatch cancelled`, {}, "warn");
1690
2407
  cleanup();
1691
2408
  return {
1692
2409
  success: true,
1693
2410
  aborted: false,
1694
2411
  abortReason: void 0,
1695
2412
  terminated: false,
2413
+ outcome: "completed",
1696
2414
  validation,
1697
2415
  result: void 0,
1698
2416
  successResults: [],
@@ -1700,6 +2418,9 @@ var ActionRegister = class {
1700
2418
  failedResults: [],
1701
2419
  execution: {
1702
2420
  duration: 0,
2421
+ admissionDuration: 0,
2422
+ queueWaitDuration: 0,
2423
+ pipelineDuration: 0,
1703
2424
  handlersExecuted: 0,
1704
2425
  handlersSkipped: 0,
1705
2426
  handlersFailed: 0,
@@ -1710,27 +2431,15 @@ var ActionRegister = class {
1710
2431
  errors: []
1711
2432
  };
1712
2433
  }
1713
- const filteredHandlers = options?.filter ? this.filterHandlers(pipeline, options.filter) : pipeline;
1714
- const actionKey = String(action);
1715
- const guardResult = skipGuards ? null : await this.applyActionGuardControlsWithResult(actionKey, filteredHandlers, options, _startTime, pipeline.length);
1716
- if (effectiveSignal?.aborted) {
1717
- cleanup();
1718
- return this.createAbortedExecutionResult(_startTime, pipeline.length, validation);
1719
- }
1720
- if (guardResult) {
1721
- cleanup();
1722
- return {
1723
- ...guardResult,
1724
- validation
1725
- };
1726
- }
1727
- const currentExecutionMode = options?.executionMode || this.actionExecutionModes.get(action) || this.executionMode;
2434
+ const filteredHandlers = this.getAttemptHandlers(action, plan);
1728
2435
  const context = {
1729
2436
  action: String(action),
1730
2437
  payload,
1731
2438
  handlers: [...filteredHandlers],
1732
2439
  executedHandlers: [],
2440
+ handlerOutcomes: [],
1733
2441
  deferOnceCleanup: true,
2442
+ claimOnce: (registration) => this.claimOnceRegistration(action, registration),
1734
2443
  signal: effectiveSignal ?? this.lifecycleController.signal,
1735
2444
  trackHandlerPromise: (promise) => this.trackHandlerPromise(promise, dispatchHandlerPromises),
1736
2445
  aborted: false,
@@ -1738,24 +2447,13 @@ var ActionRegister = class {
1738
2447
  currentIndex: 0,
1739
2448
  jumpToPriority: void 0,
1740
2449
  jumpCount: 0,
1741
- maxJumps: 10,
1742
- executionMode: currentExecutionMode,
2450
+ maxJumps: this.maxJumps,
2451
+ executionMode: plan.executionMode,
1743
2452
  results: [],
1744
2453
  terminated: false,
1745
2454
  terminationResult: void 0
1746
2455
  };
1747
2456
  let executionError;
1748
- const handlerResults = [];
1749
- filteredHandlers.forEach((handler) => {
1750
- handlerResults.push({
1751
- id: handler.config.id,
1752
- executed: false,
1753
- duration: void 0,
1754
- result: void 0,
1755
- error: void 0,
1756
- metadata: void 0
1757
- });
1758
- });
1759
2457
  const abortHandler = effectiveSignal ? () => {
1760
2458
  context.aborted = true;
1761
2459
  context.abortReason = typeof effectiveSignal.reason === "string" ? effectiveSignal.reason : "Action dispatch aborted by signal";
@@ -1765,13 +2463,6 @@ var ActionRegister = class {
1765
2463
  try {
1766
2464
  await this.executePipeline(context, dispatchHandlerPromises, autoAbortController, options?.autoAbort);
1767
2465
  errors = context.collectedErrors || [];
1768
- const executedCount = Math.min(context.currentIndex + (context.aborted ? 0 : 1), filteredHandlers.length);
1769
- for (let i = 0; i < executedCount; i++) {
1770
- const handler = filteredHandlers[i];
1771
- if (!handler) continue;
1772
- const handlerResult = handlerResults.find((hr) => hr.id === handler.config.id);
1773
- if (handlerResult) handlerResult.executed = true;
1774
- }
1775
2466
  } catch (error) {
1776
2467
  errors = context.collectedErrors || [];
1777
2468
  executionError = error instanceof Error ? error : new Error(String(error));
@@ -1781,156 +2472,118 @@ var ActionRegister = class {
1781
2472
  timestamp: Date.now(),
1782
2473
  severity: "blocking"
1783
2474
  });
1784
- const executedCount = Math.min(context.currentIndex + 1, filteredHandlers.length);
1785
- for (let i = 0; i < executedCount; i++) {
1786
- const handler = filteredHandlers[i];
1787
- if (!handler) continue;
1788
- const handlerResult = handlerResults.find((hr) => hr.id === handler.config.id);
1789
- if (handlerResult) handlerResult.executed = true;
1790
- }
1791
2475
  } finally {
1792
2476
  executedHandlers.push(...context.executedHandlers ?? []);
1793
2477
  if (effectiveSignal && abortHandler) effectiveSignal.removeEventListener("abort", abortHandler);
1794
- this.cleanupSignalsAfterStartedHandlers(cleanup, dispatchHandlerPromises);
2478
+ this.cleanupSignalsAfterStartedHandlers(() => {
2479
+ cleanup();
2480
+ options?.[ATTEMPT_SIGNAL_CLEANUP]?.();
2481
+ }, dispatchHandlerPromises);
1795
2482
  }
1796
2483
  const endTime = Date.now();
1797
- const processedResult = this.processResults(context, options?.result);
2484
+ const recordedOutcomes = context.handlerOutcomes ?? [];
2485
+ const outcomesById = new Map(recordedOutcomes.map((outcome) => [outcome.id, outcome]));
2486
+ const handlerResults = filteredHandlers.map((handler) => {
2487
+ const outcome = outcomesById.get(handler.id);
2488
+ return outcome ? snapshotHandlerOutcome(outcome) : {
2489
+ id: handler.id,
2490
+ status: "skipped",
2491
+ executed: false,
2492
+ duration: 0,
2493
+ result: void 0,
2494
+ error: void 0,
2495
+ metadata: handler.config.metadata ? { ...handler.config.metadata } : void 0
2496
+ };
2497
+ });
2498
+ const handlerErrors = errors.filter((error) => error.handlerId !== "pipeline");
2499
+ const reportedErrors = executionError ? errors.filter((error) => error.handlerId === "pipeline") : errors;
2500
+ const executionHandlersCount = handlerResults.filter((handler) => handler.executed).length;
1798
2501
  const successResults = context.results.filter((result) => result !== void 0);
1799
- const failedResults = errors.map((err) => ({
2502
+ const failedResults = handlerErrors.map((err) => ({
1800
2503
  handlerId: err.handlerId,
1801
2504
  error: err.error,
1802
- expectedType: typeof processedResult
2505
+ expectedType: "unknown"
1803
2506
  }));
1804
2507
  return {
1805
2508
  success: !executionError && !context.aborted,
1806
2509
  aborted: context.aborted,
1807
2510
  abortReason: context.abortReason,
1808
2511
  terminated: context.terminated,
2512
+ outcome: context.aborted ? "cancelled" : executionError ? "failed" : handlerErrors.length > 0 ? "completed_with_errors" : "completed",
1809
2513
  validation,
1810
- result: processedResult,
2514
+ result: context.terminated ? context.terminationResult : void 0,
1811
2515
  successResults,
1812
2516
  results: context.results,
1813
2517
  failedResults,
1814
2518
  execution: {
1815
2519
  duration: endTime - _startTime,
1816
- handlersExecuted: filteredHandlers.length === 0 ? 0 : context.currentIndex + (context.aborted ? 0 : 1),
1817
- handlersSkipped: Math.max(0, filteredHandlers.length - (context.currentIndex + 1)),
1818
- handlersFailed: errors.length,
2520
+ admissionDuration: 0,
2521
+ queueWaitDuration: 0,
2522
+ pipelineDuration: endTime - _startTime,
2523
+ handlersExecuted: executionHandlersCount,
2524
+ handlersSkipped: Math.max(0, filteredHandlers.length - executionHandlersCount),
2525
+ handlersFailed: context.executionMode === "race" ? context.raceWinnerId && outcomesById.get(context.raceWinnerId)?.status === "failed" ? 1 : 0 : handlerResults.filter((handler) => handler.status === "failed").length,
1819
2526
  startTime: _startTime,
1820
2527
  endTime
1821
2528
  },
1822
2529
  handlers: handlerResults,
1823
- errors: errors.map((err) => ({
2530
+ ...context.executionMode !== "race" ? {} : { raceDiagnostics: {
2531
+ ...context.raceWinnerId === void 0 ? {} : { winnerId: context.raceWinnerId },
2532
+ ...context.raceWinnerId === void 0 ? {} : { winner: snapshotHandlerOutcome(outcomesById.get(context.raceWinnerId)) },
2533
+ loserSnapshots: (context.raceLoserOutcomes ?? []).map(snapshotHandlerOutcome),
2534
+ pendingLosersAtReturn: (context.raceLoserOutcomes ?? []).filter((outcome) => outcome.status === "running").length,
2535
+ observedLoserFailures: (context.raceLoserOutcomes ?? []).filter((outcome) => outcome.status === "failed").length
2536
+ } },
2537
+ errors: reportedErrors.map((err) => ({
1824
2538
  handlerId: err.handlerId,
1825
2539
  error: err.error,
1826
2540
  timestamp: err.timestamp,
1827
- severity: "non-blocking"
2541
+ severity: err.severity
1828
2542
  }))
1829
2543
  };
1830
2544
  }
1831
- /**
1832
- * 🔧 Unified method for dispatchWithResult that returns ExecutionResult on guard rejection
1833
- */
1834
- async applyActionGuardControlsWithResult(actionKey, filteredHandlers, options, startTime, pipelineLength) {
1835
- let throttleMs;
1836
- let debounceMs;
1837
- if (options?.throttle !== void 0) throttleMs = options.throttle;
1838
- else if (filteredHandlers.length > 0) {
1839
- for (const handler of filteredHandlers) if (handler.config.throttle !== void 0) {
1840
- throttleMs = handler.config.throttle;
1841
- break;
1842
- }
1843
- }
1844
- if (options?.debounce !== void 0) debounceMs = options.debounce;
1845
- else if (filteredHandlers.length > 0) {
1846
- for (const handler of filteredHandlers) if (handler.config.debounce !== void 0) {
1847
- debounceMs = handler.config.debounce;
1848
- break;
1849
- }
1850
- }
1851
- if (debounceMs !== void 0) {
1852
- if (!await this.actionGuard.debounce(actionKey, debounceMs)) return {
1853
- success: false,
1854
- aborted: true,
1855
- abortReason: "Debounced execution",
1856
- terminated: false,
1857
- result: void 0,
1858
- successResults: [],
1859
- results: [],
1860
- failedResults: [],
1861
- execution: {
1862
- duration: Date.now() - startTime,
1863
- handlersExecuted: 0,
1864
- handlersSkipped: pipelineLength,
1865
- handlersFailed: 0,
1866
- startTime,
1867
- endTime: Date.now()
1868
- },
1869
- handlers: [],
1870
- errors: []
1871
- };
1872
- }
1873
- if (throttleMs !== void 0) {
1874
- if (!this.actionGuard.throttle(actionKey, throttleMs)) return {
1875
- success: false,
1876
- aborted: true,
1877
- abortReason: "Throttled execution",
1878
- terminated: false,
1879
- result: void 0,
1880
- successResults: [],
1881
- results: [],
1882
- failedResults: [],
1883
- execution: {
1884
- duration: Date.now() - startTime,
1885
- handlersExecuted: 0,
1886
- handlersSkipped: pipelineLength,
1887
- handlersFailed: 0,
1888
- startTime,
1889
- endTime: Date.now()
1890
- },
1891
- handlers: [],
1892
- errors: []
1893
- };
1894
- }
1895
- return null;
1896
- }
1897
- /**
1898
- * 🔧 Create or reuse PipelineController from pool for better performance
1899
- */
1900
- getControllerFromPool(context, autoAbortController, autoAbortOptions) {
1901
- let controller = this.controllerPool.pop();
1902
- if (!controller) controller = {};
2545
+ /** Create a pipeline controller for one handler execution. */
2546
+ createController(context, autoAbortController, autoAbortOptions, isolatedState, collectResults = true) {
2547
+ const controller = {};
1903
2548
  controller.signal = context.signal ?? this.lifecycleController.signal;
2549
+ const state = isolatedState ?? context;
1904
2550
  controller.abort = (reason) => {
1905
- context.aborted = true;
1906
- context.abortReason = reason;
1907
- if (autoAbortController && autoAbortOptions?.allowHandlerAbort) autoAbortController.abort(reason);
2551
+ state.aborted = true;
2552
+ state.abortReason = reason;
2553
+ const propagateAbort = !isolatedState || context.executionMode !== "race";
2554
+ if (propagateAbort) {
2555
+ context.aborted = true;
2556
+ context.abortReason = reason;
2557
+ }
2558
+ if (propagateAbort && autoAbortController && autoAbortOptions?.allowHandlerAbort) autoAbortController.abort(reason);
1908
2559
  };
1909
2560
  controller.modifyPayload = (modifier) => {
1910
- try {
1911
- context.payload = modifier(context.payload);
1912
- } catch (modificationError) {
1913
- this.log("Payload modification error", modificationError, "warn");
1914
- }
2561
+ state.payload = modifier(state.payload);
1915
2562
  };
1916
- controller.getPayload = () => context.payload;
2563
+ controller.getPayload = () => state.payload;
1917
2564
  controller.jumpToPriority = (priority) => {
1918
- context.jumpToPriority = priority;
2565
+ state.jumpToPriority = priority;
1919
2566
  };
1920
2567
  controller.return = (result) => {
1921
- context.terminated = true;
1922
- context.terminationResult = result;
2568
+ state.terminated = true;
2569
+ state.terminationResult = collectResults ? result : void 0;
2570
+ if (!isolatedState) {
2571
+ context.terminated = true;
2572
+ context.terminationResult = collectResults ? result : void 0;
2573
+ }
2574
+ return result;
1923
2575
  };
1924
2576
  controller.setResult = (result) => {
1925
- context.results.push(result);
2577
+ if (collectResults) state.results.push(result);
1926
2578
  };
1927
2579
  controller.getResults = () => {
1928
- return [...context.results];
2580
+ return [...state.results];
1929
2581
  };
1930
2582
  controller.mergeResult = (merger) => {
1931
- const currentResult = context.results[context.results.length - 1];
1932
- const mergedResult = merger(context.results.slice(0, -1), currentResult);
1933
- context.results[context.results.length - 1] = mergedResult;
2583
+ if (!collectResults) return;
2584
+ const currentResult = state.results[state.results.length - 1];
2585
+ const mergedResult = merger(state.results.slice(0, -1), currentResult);
2586
+ state.results[state.results.length - 1] = mergedResult;
1934
2587
  };
1935
2588
  return controller;
1936
2589
  }
@@ -1947,17 +2600,32 @@ var ActionRegister = class {
1947
2600
  if (filterOptions.priority.min !== void 0 && priority < filterOptions.priority.min) return false;
1948
2601
  if (filterOptions.priority.max !== void 0 && priority > filterOptions.priority.max) return false;
1949
2602
  }
1950
- if (filterOptions.custom && !filterOptions.custom(config)) return false;
2603
+ if (filterOptions.custom) {
2604
+ const configSnapshot = Object.freeze({
2605
+ ...config,
2606
+ metadata: config.metadata ? Object.freeze({ ...config.metadata }) : void 0
2607
+ });
2608
+ if (!filterOptions.custom(configSnapshot)) return false;
2609
+ }
1951
2610
  return true;
1952
2611
  });
1953
2612
  }
1954
- processResults(context, resultOptions) {
1955
- const results = context.results;
1956
- if (context.terminated && context.terminationResult !== void 0) return context.terminationResult;
2613
+ validateResultOptions(resultOptions) {
2614
+ if (!resultOptions) return;
2615
+ if (resultOptions.strategy === "custom" && typeof resultOptions.merger !== "function") throw new ActionResultProcessingError("Custom result strategy requires a merger function");
2616
+ if (resultOptions.maxResults !== void 0 && (!Number.isSafeInteger(resultOptions.maxResults) || resultOptions.maxResults < 0)) throw new RangeError("maxResults must be a non-negative safe integer.");
2617
+ }
2618
+ processResults(results, terminated, terminationResult, resultOptions) {
2619
+ if (terminated && terminationResult !== void 0) return terminationResult;
1957
2620
  if (!resultOptions) return results.length > 0 ? results[results.length - 1] : void 0;
1958
2621
  if (!resultOptions.collect && !resultOptions.strategy) return;
1959
- const limitedResults = resultOptions.maxResults ? results.slice(0, resultOptions.maxResults) : results;
1960
- if (limitedResults.length === 0) return;
2622
+ const collectedResults = results.filter((result) => result !== void 0);
2623
+ const limitedResults = resultOptions.maxResults !== void 0 ? collectedResults.slice(0, resultOptions.maxResults) : collectedResults;
2624
+ if (limitedResults.length === 0) {
2625
+ if (resultOptions.strategy === "all" || resultOptions.collect && !resultOptions.strategy) return [];
2626
+ if (resultOptions.strategy === "custom" || resultOptions.strategy === "merge" && resultOptions.merger) return resultOptions.merger(limitedResults);
2627
+ return;
2628
+ }
1961
2629
  switch (resultOptions.strategy) {
1962
2630
  case "first": return limitedResults[0];
1963
2631
  case "last": return limitedResults[limitedResults.length - 1];
@@ -1974,9 +2642,36 @@ var ActionRegister = class {
1974
2642
  }
1975
2643
  }
1976
2644
  async executePipeline(context, dispatchHandlerPromises, autoAbortController, autoAbortOptions) {
1977
- const createController = (_registration, _index) => {
1978
- return this.getControllerFromPool(context, autoAbortController, autoAbortOptions);
2645
+ const createController = (registration, _index, state) => {
2646
+ const controller = this.createController(context, autoAbortController, autoAbortOptions, state, registration.role !== "guard");
2647
+ if (registration.role === "guard") return {
2648
+ signal: controller.signal,
2649
+ getPayload: controller.getPayload,
2650
+ modifyPayload: controller.modifyPayload,
2651
+ abort: controller.abort
2652
+ };
2653
+ if (registration.role === "result") return {
2654
+ signal: controller.signal,
2655
+ getPayload: controller.getPayload,
2656
+ abort: controller.abort,
2657
+ return: controller.return,
2658
+ setResult: controller.setResult,
2659
+ getResults: controller.getResults,
2660
+ mergeResult: controller.mergeResult
2661
+ };
2662
+ return controller;
1979
2663
  };
2664
+ const originalHandlers = context.handlers;
2665
+ const preflightHandlers = originalHandlers.filter((handler) => handler.role === "guard");
2666
+ if (preflightHandlers.length > 0) {
2667
+ context.handlers = preflightHandlers;
2668
+ await executeSequential(context, createController);
2669
+ if (context.aborted || context.terminated) {
2670
+ context.handlers = originalHandlers;
2671
+ return;
2672
+ }
2673
+ }
2674
+ context.handlers = originalHandlers.filter((handler) => !preflightHandlers.includes(handler) && handler.role !== "observer");
1980
2675
  switch (context.executionMode) {
1981
2676
  case "sequential":
1982
2677
  await executeSequential(context, createController);
@@ -1989,6 +2684,7 @@ var ActionRegister = class {
1989
2684
  break;
1990
2685
  default: throw new Error(`Unknown execution mode: ${context.executionMode}`);
1991
2686
  }
2687
+ context.handlers = originalHandlers;
1992
2688
  if (!context.deferOnceCleanup) this.cleanupOneTimeHandlers(context.action, context.executedHandlers ?? [], dispatchHandlerPromises);
1993
2689
  }
1994
2690
  cleanupOneTimeHandlers(action, executedHandlers, dispatchHandlerPromises) {
@@ -1997,7 +2693,9 @@ var ActionRegister = class {
1997
2693
  const handlersStillRunning = [...dispatchHandlerPromises];
1998
2694
  const shouldDeferCleanup = handlersStillRunning.length > 0;
1999
2695
  oneTimeHandlers.forEach((registration) => {
2000
- if (this.removeRegistration(action, registration, !shouldDeferCleanup)) {
2696
+ const claimed = this.claimedOnceHandlers.delete(registration);
2697
+ if (claimed || this.removeRegistration(action, registration, !shouldDeferCleanup)) {
2698
+ if (claimed && !shouldDeferCleanup) this.runRegistrationCleanup(action, registration);
2001
2699
  if (shouldDeferCleanup && typeof registration.config.cleanup === "function") {
2002
2700
  const cleanupPromise = Promise.allSettled(handlersStillRunning).then(() => {
2003
2701
  this.runRegistrationCleanup(action, registration);
@@ -2006,11 +2704,20 @@ var ActionRegister = class {
2006
2704
  }
2007
2705
  this.log(`One-time handler removed: ${String(action)}`, {
2008
2706
  handlerId: registration.id,
2009
- remainingHandlers: this.getHandlerCount(action)
2707
+ remainingHandlers: this.pipelines.get(action)?.length ?? 0
2010
2708
  });
2011
2709
  }
2012
2710
  });
2013
2711
  }
2712
+ /** Reserve a once registration before user code starts. This is synchronous,
2713
+ * so independent dispatches cannot both invoke the same registration. */
2714
+ claimOnceRegistration(action, registration) {
2715
+ if (!registration.config.once) return true;
2716
+ if (this.claimedOnceHandlers.has(registration)) return false;
2717
+ if (!this.removeRegistration(action, registration, false)) return false;
2718
+ this.claimedOnceHandlers.add(registration);
2719
+ return true;
2720
+ }
2014
2721
  /**
2015
2722
  * Get the number of registered handlers for an action
2016
2723
  *
@@ -2083,8 +2790,10 @@ var ActionRegister = class {
2083
2790
  });
2084
2791
  this.pipelines.clear();
2085
2792
  this.lastRegisteredTimestamps.clear();
2793
+ this.unregisterFunctions.forEach((unregisters) => unregisters.clear());
2086
2794
  this.unregisterFunctions.clear();
2087
2795
  this.actionGuard.clearAll();
2796
+ this.observerHandlers.clear();
2088
2797
  }
2089
2798
  /**
2090
2799
  * Get the name of this action register
@@ -2156,7 +2865,7 @@ var ActionRegister = class {
2156
2865
  */
2157
2866
  setExecutionMode(mode) {
2158
2867
  this.executionMode = mode;
2159
- if (this.registryConfig?.debug && process.env.NODE_ENV === "development") console.log(`🎯 Global execution mode set to: ${mode}`);
2868
+ if (this.isDebugMode) console.log(`🎯 Global execution mode set to: ${mode}`);
2160
2869
  }
2161
2870
  /**
2162
2871
  * Set execution mode for a specific action
@@ -2166,7 +2875,7 @@ var ActionRegister = class {
2166
2875
  */
2167
2876
  setActionExecutionMode(action, mode) {
2168
2877
  this.actionExecutionModes.set(action, mode);
2169
- if (this.registryConfig?.debug && process.env.NODE_ENV === "development") console.log(`🎯 Execution mode set for action '${String(action)}': ${mode}`);
2878
+ if (this.isDebugMode) console.log(`🎯 Execution mode set for action '${String(action)}': ${mode}`);
2170
2879
  }
2171
2880
  /**
2172
2881
  * Get execution mode for a specific action
@@ -2184,7 +2893,7 @@ var ActionRegister = class {
2184
2893
  */
2185
2894
  removeActionExecutionMode(action) {
2186
2895
  this.actionExecutionModes.delete(action);
2187
- if (this.registryConfig?.debug && process.env.NODE_ENV === "development") console.log(`🎯 Execution mode reset for action '${String(action)}' to default: ${this.executionMode}`);
2896
+ if (this.isDebugMode) console.log(`🎯 Execution mode reset for action '${String(action)}' to default: ${this.executionMode}`);
2188
2897
  }
2189
2898
  /**
2190
2899
  * Get registry configuration (for debugging and inspection)
@@ -2215,11 +2924,19 @@ var ActionRegister = class {
2215
2924
  return () => {
2216
2925
  if (this.removeRegistration(action, registration)) this.log(`Handler unregistered: ${String(action)}`, {
2217
2926
  handlerId,
2218
- remainingHandlers: this.getHandlerCount(action),
2927
+ remainingHandlers: this.pipelines.get(action)?.length ?? 0,
2219
2928
  actionRemoved: !this.pipelines.has(action)
2220
2929
  });
2221
2930
  };
2222
2931
  }
2932
+ getUnregisterFunctions(action) {
2933
+ let unregisters = this.unregisterFunctions.get(action);
2934
+ if (!unregisters) {
2935
+ unregisters = /* @__PURE__ */ new Map();
2936
+ this.unregisterFunctions.set(action, unregisters);
2937
+ }
2938
+ return unregisters;
2939
+ }
2223
2940
  /** Remove a registration and release every resource owned by it exactly once. */
2224
2941
  removeRegistration(action, registration, runCleanup = true) {
2225
2942
  const pipeline = this.pipelines.get(action);
@@ -2227,11 +2944,13 @@ var ActionRegister = class {
2227
2944
  const index = pipeline.indexOf(registration);
2228
2945
  if (index === -1) return false;
2229
2946
  pipeline.splice(index, 1);
2230
- this.unregisterFunctions.delete(registration.id);
2947
+ this.observerHandlers.delete(registration);
2948
+ this.unregisterFunctions.get(action)?.delete(registration.id);
2231
2949
  if (runCleanup) this.runRegistrationCleanup(action, registration);
2232
2950
  if (pipeline.length === 0) {
2233
2951
  this.pipelines.delete(action);
2234
2952
  this.lastRegisteredTimestamps.delete(action);
2953
+ this.unregisterFunctions.delete(action);
2235
2954
  }
2236
2955
  return true;
2237
2956
  }
@@ -2250,7 +2969,11 @@ var ActionRegister = class {
2250
2969
  * @public
2251
2970
  */
2252
2971
  getUnregisterFunctionCount() {
2253
- return this.unregisterFunctions.size;
2972
+ let count = 0;
2973
+ this.unregisterFunctions.forEach((unregisters) => {
2974
+ count += unregisters.size;
2975
+ });
2976
+ return count;
2254
2977
  }
2255
2978
  /**
2256
2979
  * Checks if an unregister function exists for the given handler ID
@@ -2260,7 +2983,8 @@ var ActionRegister = class {
2260
2983
  * @public
2261
2984
  */
2262
2985
  hasUnregisterFunction(handlerId) {
2263
- return this.unregisterFunctions.has(handlerId);
2986
+ for (const unregisters of this.unregisterFunctions.values()) if (unregisters.has(handlerId)) return true;
2987
+ return false;
2264
2988
  }
2265
2989
  /** Reject queued dispatches without releasing registered handlers. */
2266
2990
  cancelPendingDispatches() {
@@ -2305,7 +3029,6 @@ var ActionRegister = class {
2305
3029
  if (this.lifecycleState === "destroyed") return;
2306
3030
  this.clearAll();
2307
3031
  this.actionExecutionModes.clear();
2308
- this.controllerPool.length = 0;
2309
3032
  this.lifecycleState = "destroyed";
2310
3033
  this.log("ActionRegister destroyed");
2311
3034
  }
@@ -2336,14 +3059,18 @@ var ActionRegister = class {
2336
3059
  };
2337
3060
 
2338
3061
  //#endregion
3062
+ exports.ActionAttemptSupersededError = ActionAttemptSupersededError;
2339
3063
  exports.ActionGuard = ActionGuard;
2340
3064
  exports.ActionRegister = ActionRegister;
2341
3065
  exports.ActionRegisterDestroyedError = ActionRegisterDestroyedError;
3066
+ exports.ActionResultProcessingError = ActionResultProcessingError;
2342
3067
  exports.ActionTimeoutError = ActionTimeoutError;
2343
3068
  exports.ActionValidationError = ActionValidationError;
2344
3069
  exports.executeParallel = executeParallel;
2345
3070
  exports.executeRace = executeRace;
2346
3071
  exports.executeSequential = executeSequential;
2347
3072
  exports.isActionRegisterDestroyedError = isActionRegisterDestroyedError;
3073
+ exports.isActionResultProcessingError = isActionResultProcessingError;
2348
3074
  exports.isActionTimeoutError = isActionTimeoutError;
2349
- exports.isActionValidationError = isActionValidationError;
3075
+ exports.isActionValidationError = isActionValidationError;
3076
+ exports.resolveHandlerConfig = resolveHandlerConfig;