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