@context-action/core 0.8.6 → 0.8.8

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,4 +1,7 @@
1
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
+ }
2
5
  /**
3
6
  * Create standardized error handling for handlers
4
7
  *
@@ -58,12 +61,15 @@ async function executeSequential(context, createController) {
58
61
  i++;
59
62
  continue;
60
63
  }
64
+ (context.executedHandlers ?? (context.executedHandlers = [])).push(registration);
61
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;
62
68
  if (registration.config.blocking) {
63
- const handlerResult = result instanceof Promise ? await result : result;
69
+ const handlerResult = trackedResult ? await trackedResult : result;
64
70
  if (handlerResult !== void 0 && !context.terminated) context.results.push(handlerResult);
65
- } else if (result instanceof Promise) {
66
- const promiseWithErrorHandling = result.then((asyncResult) => {
71
+ } else if (trackedResult) {
72
+ const promiseWithErrorHandling = trackedResult.then((asyncResult) => {
67
73
  if (asyncResult !== void 0 && !context.terminated) context.results.push(asyncResult);
68
74
  return asyncResult;
69
75
  }).catch((error) => {
@@ -161,10 +167,9 @@ async function executeParallel(context, createController) {
161
167
  skipped: true
162
168
  };
163
169
  }
170
+ (context.executedHandlers ?? (context.executedHandlers = [])).push(registration);
164
171
  const result = registration.handler(context.payload, controller);
165
- let handlerResult;
166
- if (result instanceof Promise) handlerResult = await result;
167
- else handlerResult = result;
172
+ const handlerResult = isPromiseLike(result) ? await Promise.resolve(result) : result;
168
173
  /** Collect result if handler returned something and pipeline wasn't terminated */
169
174
  if (handlerResult !== void 0 && !context.terminated) context.results.push(handlerResult);
170
175
  return {
@@ -183,8 +188,9 @@ async function executeParallel(context, createController) {
183
188
  };
184
189
  }
185
190
  });
191
+ const trackedHandlerPromises = context.trackHandlerPromise ? handlerPromises.map((promise) => context.trackHandlerPromise(promise)) : handlerPromises;
186
192
  /** Wait for all handlers to complete */
187
- const results = await Promise.allSettled(handlerPromises);
193
+ const results = await Promise.allSettled(trackedHandlerPromises);
188
194
  /** Check for any rejected blocking handlers */
189
195
  const failures = results.filter((result, index) => {
190
196
  if (result.status === "rejected") return runnableHandlers[index]?.config.blocking ?? false;
@@ -203,8 +209,10 @@ async function executeParallel(context, createController) {
203
209
  *
204
210
  * Executes all qualifying handlers simultaneously using Promise.race, where
205
211
  * the first handler to complete determines the pipeline result. Other handlers
206
- * are effectively cancelled. Useful for scenarios where you want the fastest
207
- * response from multiple equivalent handlers.
212
+ * continue in the background and remain tracked for lifecycle cleanup; handlers
213
+ * must observe the controller signal for cooperative external cancellation.
214
+ * Useful for scenarios where you want the fastest response from multiple
215
+ * equivalent handlers.
208
216
  *
209
217
  * @template T - The payload type for the action
210
218
  * @template R - The result type for handlers
@@ -245,10 +253,9 @@ async function executeRace(context, createController) {
245
253
  skipped: true
246
254
  };
247
255
  }
256
+ (context.executedHandlers ?? (context.executedHandlers = [])).push(registration);
248
257
  const result = registration.handler(context.payload, controller);
249
- let handlerResult;
250
- if (result instanceof Promise) handlerResult = await result;
251
- else handlerResult = result;
258
+ const handlerResult = isPromiseLike(result) ? await Promise.resolve(result) : result;
252
259
  return {
253
260
  success: true,
254
261
  handlerId: registration.id,
@@ -266,8 +273,9 @@ async function executeRace(context, createController) {
266
273
  };
267
274
  }
268
275
  });
269
- /** Race all handlers */
270
- const winner = await Promise.race(handlerPromises);
276
+ const trackedHandlerPromises = context.trackHandlerPromise ? handlerPromises.map((promise) => context.trackHandlerPromise(promise)) : handlerPromises;
277
+ /** Race all handlers while retaining every loser for lifecycle draining. */
278
+ const winner = await Promise.race(trackedHandlerPromises);
271
279
  /** If the winner failed and was blocking, throw the error */
272
280
  if (!winner.success && winner.registration?.config.blocking) throw winner.error;
273
281
  /** Collect result from the winning handler */
@@ -316,7 +324,11 @@ var ActionGuard = class {
316
324
  this.cleanupIntervalMs = 3e4;
317
325
  this.maxGuards = 1e3;
318
326
  this.accessOrder = [];
319
- if (autoCleanup) this.startAutoCleanup();
327
+ this.autoCleanupEnabled = autoCleanup;
328
+ }
329
+ /** Start cleanup only after the first guard is used. */
330
+ ensureAutoCleanup() {
331
+ if (this.autoCleanupEnabled && !this.cleanupInterval) this.startAutoCleanup();
320
332
  }
321
333
  /**
322
334
  * Start automatic cleanup of idle guard states
@@ -324,9 +336,17 @@ var ActionGuard = class {
324
336
  * @internal
325
337
  */
326
338
  startAutoCleanup() {
339
+ if (this.cleanupInterval) return;
327
340
  this.cleanupInterval = setInterval(() => {
328
341
  this.performCleanup();
329
342
  }, this.cleanupIntervalMs);
343
+ this.cleanupInterval.unref?.();
344
+ }
345
+ stopAutoCleanup() {
346
+ if (this.cleanupInterval) {
347
+ clearInterval(this.cleanupInterval);
348
+ this.cleanupInterval = void 0;
349
+ }
330
350
  }
331
351
  /**
332
352
  * 🔧 Optimized cleanup with early exit and batched operations
@@ -335,7 +355,10 @@ var ActionGuard = class {
335
355
  */
336
356
  performCleanup() {
337
357
  const guardCount = this.guards.size;
338
- if (guardCount === 0) return;
358
+ if (guardCount === 0) {
359
+ this.stopAutoCleanup();
360
+ return;
361
+ }
339
362
  const now = Date.now();
340
363
  const keysToDelete = [];
341
364
  if (guardCount <= 10) this.guards.forEach((state, key) => {
@@ -365,6 +388,7 @@ var ActionGuard = class {
365
388
  if (accessIndex !== -1) this.accessOrder.splice(accessIndex, 1);
366
389
  });
367
390
  if (typeof process !== "undefined" && process.env?.DEBUG_CONTEXT_ACTION) console.debug(`[ActionGuard] Cleaned up ${keysToDelete.length} idle guards`);
391
+ if (this.guards.size === 0) this.stopAutoCleanup();
368
392
  }
369
393
  }
370
394
  /**
@@ -423,6 +447,7 @@ var ActionGuard = class {
423
447
  * @internal
424
448
  */
425
449
  async debounce(actionKey, debounceMs) {
450
+ this.ensureAutoCleanup();
426
451
  this.evictIfNeeded();
427
452
  /** Get or create guard state for this action */
428
453
  let state = this.guards.get(actionKey);
@@ -483,6 +508,7 @@ var ActionGuard = class {
483
508
  * @internal
484
509
  */
485
510
  throttle(actionKey, throttleMs) {
511
+ this.ensureAutoCleanup();
486
512
  this.evictIfNeeded();
487
513
  /** Get or create guard state for this action */
488
514
  let state = this.guards.get(actionKey);
@@ -550,6 +576,9 @@ var ActionGuard = class {
550
576
  state.throttleTimer = void 0;
551
577
  }
552
578
  this.guards.delete(actionKey);
579
+ const accessIndex = this.accessOrder.indexOf(actionKey);
580
+ if (accessIndex !== -1) this.accessOrder.splice(accessIndex, 1);
581
+ if (this.guards.size === 0) this.stopAutoCleanup();
553
582
  }
554
583
  }
555
584
  /**
@@ -574,6 +603,8 @@ var ActionGuard = class {
574
603
  });
575
604
  /** Remove all guard states from memory */
576
605
  this.guards.clear();
606
+ this.accessOrder = [];
607
+ this.stopAutoCleanup();
577
608
  }
578
609
  /**
579
610
  * Get current guard state for debugging purposes
@@ -611,12 +642,7 @@ var ActionGuard = class {
611
642
  * @internal
612
643
  */
613
644
  destroy() {
614
- if (this.cleanupInterval) {
615
- clearInterval(this.cleanupInterval);
616
- this.cleanupInterval = void 0;
617
- }
618
645
  this.clearAll();
619
- this.accessOrder = [];
620
646
  }
621
647
  /**
622
648
  * 🆕 Get statistics about active guards
@@ -669,27 +695,42 @@ var OperationQueue = class {
669
695
  * @returns Promise로 래핑된 작업 결과
670
696
  */
671
697
  enqueue(operation, priority = 0) {
672
- return new Promise((resolve, reject) => {
673
- const queuedOperation = {
674
- id: `${this.name}-${++this.operationCounter}`,
675
- operation,
676
- resolve,
677
- reject,
678
- priority,
679
- timestamp: Date.now()
680
- };
681
- let insertIndex = this.queue.length;
682
- for (let i = 0; i < this.queue.length; i++) {
683
- const item = this.queue[i];
684
- if (item && (item.priority || 0) < priority) {
685
- insertIndex = i;
686
- break;
698
+ return this.enqueueWithHandle(operation, priority).promise;
699
+ }
700
+ /** Enqueue an operation and retain a handle for pre-start cancellation. */
701
+ enqueueWithHandle(operation, priority = 0) {
702
+ let queuedOperation;
703
+ return {
704
+ promise: new Promise((resolve, reject) => {
705
+ queuedOperation = {
706
+ id: `${this.name}-${++this.operationCounter}`,
707
+ operation,
708
+ resolve,
709
+ reject,
710
+ priority,
711
+ timestamp: Date.now()
712
+ };
713
+ let insertIndex = this.queue.length;
714
+ for (let i = 0; i < this.queue.length; i++) {
715
+ const item = this.queue[i];
716
+ if (item && (item.priority || 0) < priority) {
717
+ insertIndex = i;
718
+ break;
719
+ }
687
720
  }
721
+ this.queue.splice(insertIndex, 0, queuedOperation);
722
+ if (this.processingPromise) this.notifyNewOperation();
723
+ this.processQueue();
724
+ }),
725
+ cancel: (reason = /* @__PURE__ */ new Error("Queue operation cancelled")) => {
726
+ const index = this.queue.indexOf(queuedOperation);
727
+ if (index === -1) return false;
728
+ this.queue.splice(index, 1);
729
+ queuedOperation.reject(reason);
730
+ this.notifyNewOperation();
731
+ return true;
688
732
  }
689
- this.queue.splice(insertIndex, 0, queuedOperation);
690
- if (this.processingPromise) this.notifyNewOperation();
691
- this.processQueue();
692
- });
733
+ };
693
734
  }
694
735
  /**
695
736
  * 🆕 큐 처리 메인 로직 - 동시성 제어 및 비동기 지원
@@ -795,12 +836,14 @@ var OperationQueue = class {
795
836
  /**
796
837
  * 큐 비우기 (테스트용)
797
838
  */
798
- clear() {
839
+ clear(options = {}) {
840
+ const rejectPending = options.rejectPending ?? true;
841
+ const reason = options.reason ?? /* @__PURE__ */ new Error("Queue cleared");
799
842
  this.queue.forEach((operation) => {
800
- operation.reject(/* @__PURE__ */ new Error("Queue cleared"));
843
+ if (rejectPending) operation.reject(reason);
844
+ else operation.resolve(void 0);
801
845
  });
802
846
  this.queue = [];
803
- this.processingPromise = null;
804
847
  this.pendingResolvers.splice(0).forEach((resolve) => resolve());
805
848
  }
806
849
  /**
@@ -900,14 +943,96 @@ var ActionValidationError = class ActionValidationError extends Error {
900
943
  }
901
944
  };
902
945
  /**
946
+ * Raised when a dispatch exceeds its configured wall-clock timeout.
947
+ * The underlying handler receives an aborted controller signal and the internal
948
+ * queue keeps draining it safely, while the caller is released immediately with
949
+ * this error.
950
+ */
951
+ var ActionTimeoutError = class ActionTimeoutError extends Error {
952
+ constructor(action, timeout) {
953
+ super(`Action "${action}" timed out after ${timeout}ms`);
954
+ this.action = action;
955
+ this.timeout = timeout;
956
+ this.name = "ActionTimeoutError";
957
+ Object.setPrototypeOf(this, ActionTimeoutError.prototype);
958
+ }
959
+ };
960
+ /** Raised when work is submitted after an ActionRegister begins shutdown. */
961
+ var ActionRegisterDestroyedError = class ActionRegisterDestroyedError extends Error {
962
+ constructor(registerName, state) {
963
+ super(`ActionRegister "${registerName}" is ${state} and cannot accept new work`);
964
+ this.registerName = registerName;
965
+ this.state = state;
966
+ this.name = "ActionRegisterDestroyedError";
967
+ Object.setPrototypeOf(this, ActionRegisterDestroyedError.prototype);
968
+ }
969
+ };
970
+ /**
903
971
  * ActionValidationError 타입 가드
904
972
  */
905
973
  function isActionValidationError(error) {
906
974
  return error instanceof ActionValidationError;
907
975
  }
976
+ /** ActionTimeoutError type guard. */
977
+ function isActionTimeoutError(error) {
978
+ return error instanceof ActionTimeoutError;
979
+ }
980
+ /** ActionRegisterDestroyedError type guard. */
981
+ function isActionRegisterDestroyedError(error) {
982
+ return error instanceof ActionRegisterDestroyedError;
983
+ }
908
984
 
909
985
  //#endregion
910
986
  //#region src/ActionRegister.ts
987
+ const dispatchOptionKeys = /* @__PURE__ */ new Set([
988
+ "autoAbort",
989
+ "debounce",
990
+ "executionMode",
991
+ "filter",
992
+ "immediate",
993
+ "queuePriority",
994
+ "result",
995
+ "retryOnError",
996
+ "signal",
997
+ "throttle",
998
+ "timeout"
999
+ ]);
1000
+ function isRecord(value) {
1001
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1002
+ }
1003
+ function isOptionalNumber(value) {
1004
+ return value === void 0 || typeof value === "number";
1005
+ }
1006
+ function isOptionalBoolean(value) {
1007
+ return value === void 0 || typeof value === "boolean";
1008
+ }
1009
+ /**
1010
+ * Recognize the deprecated options-first void-action proxy call.
1011
+ *
1012
+ * Every supplied field is validated so ordinary payloads that merely overlap
1013
+ * one option name are not silently reinterpreted as dispatch options.
1014
+ */
1015
+ function isDispatchOptions(value) {
1016
+ if (!isRecord(value)) return false;
1017
+ const keys = Object.keys(value);
1018
+ if (keys.length === 0 || keys.some((key) => !dispatchOptionKeys.has(key))) return false;
1019
+ return keys.every((key) => {
1020
+ const optionValue = value[key];
1021
+ switch (key) {
1022
+ case "debounce":
1023
+ case "queuePriority":
1024
+ case "throttle":
1025
+ case "timeout": return isOptionalNumber(optionValue);
1026
+ case "immediate": return isOptionalBoolean(optionValue);
1027
+ case "executionMode": return optionValue === void 0 || optionValue === "sequential" || optionValue === "parallel" || optionValue === "race";
1028
+ case "signal": return optionValue === void 0 || isRecord(optionValue) && typeof optionValue.aborted === "boolean" && typeof optionValue.addEventListener === "function";
1029
+ case "retryOnError": return optionValue === void 0 || isRecord(optionValue) && typeof optionValue.maxAttempts === "number" && typeof optionValue.delay === "number";
1030
+ case "autoAbort": return optionValue === void 0 || isRecord(optionValue) && typeof optionValue.enabled === "boolean" && isOptionalBoolean(optionValue.allowHandlerAbort) && (optionValue.onControllerCreated === void 0 || typeof optionValue.onControllerCreated === "function");
1031
+ case "filter": return optionValue === void 0 || isRecord(optionValue) && (optionValue.handlerIds === void 0 || Array.isArray(optionValue.handlerIds) && optionValue.handlerIds.every((item) => typeof item === "string")) && (optionValue.excludeHandlerIds === void 0 || Array.isArray(optionValue.excludeHandlerIds) && optionValue.excludeHandlerIds.every((item) => typeof item === "string")) && (optionValue.priority === void 0 || isRecord(optionValue.priority)) && (optionValue.custom === void 0 || typeof optionValue.custom === "function");
1032
+ case "result": return optionValue === void 0 || isRecord(optionValue) && (optionValue.strategy === void 0 || optionValue.strategy === "first" || optionValue.strategy === "last" || optionValue.strategy === "all" || optionValue.strategy === "merge" || optionValue.strategy === "custom") && (optionValue.merger === void 0 || typeof optionValue.merger === "function") && isOptionalBoolean(optionValue.collect) && isOptionalNumber(optionValue.maxResults) && isOptionalBoolean(optionValue.includeErrors);
1033
+ }
1034
+ });
1035
+ }
911
1036
  /**
912
1037
  * Action Register for managing action handlers with priority-based execution
913
1038
  *
@@ -923,35 +1048,6 @@ function isActionValidationError(error) {
923
1048
  *
924
1049
  * @public
925
1050
  */
926
- /**
927
- * Type guard to determine if an object is DispatchOptions
928
- * Extracted as utility function for reuse and performance
929
- *
930
- * @param obj - Object to check
931
- * @returns True if object is DispatchOptions
932
- * @internal
933
- */
934
- function isDispatchOptions(obj) {
935
- if (!obj || typeof obj !== "object") return false;
936
- if ("debounce" in obj && typeof obj.debounce === "number") return true;
937
- if ("throttle" in obj && typeof obj.throttle === "number") return true;
938
- if ("executionMode" in obj) return true;
939
- if ("signal" in obj && obj.signal instanceof AbortSignal) return true;
940
- if ("immediate" in obj && typeof obj.immediate === "boolean") return true;
941
- if ("queuePriority" in obj && typeof obj.queuePriority === "number") return true;
942
- if ("timeout" in obj && typeof obj.timeout === "number") return true;
943
- if ("retryOnError" in obj && typeof obj.retryOnError === "object") return true;
944
- if ("autoAbort" in obj && typeof obj.autoAbort === "object") return true;
945
- if ("filter" in obj && typeof obj.filter === "object" && obj.filter !== null) {
946
- const filter = obj.filter;
947
- if ("handlerIds" in filter || "excludeHandlerIds" in filter || "priority" in filter || "custom" in filter) return true;
948
- }
949
- if ("result" in obj && typeof obj.result === "object" && obj.result !== null) {
950
- const result = obj.result;
951
- if ("strategy" in result || "merger" in result || "collect" in result || "maxResults" in result || "includeErrors" in result) return true;
952
- }
953
- return false;
954
- }
955
1051
  var ActionRegister = class {
956
1052
  constructor(config = {}) {
957
1053
  this.pipelines = /* @__PURE__ */ new Map();
@@ -963,6 +1059,11 @@ var ActionRegister = class {
963
1059
  this.handlerIdCounter = 0;
964
1060
  this.controllerPool = [];
965
1061
  this.maxControllerPoolSize = 10;
1062
+ this.lifecycleState = "active";
1063
+ this.lifecycleController = new AbortController();
1064
+ this.activeDispatches = /* @__PURE__ */ new Set();
1065
+ this.activeHandlerPromises = /* @__PURE__ */ new Set();
1066
+ this.dispatchConstructionDepth = 0;
966
1067
  this.name = config.name || "ActionRegister";
967
1068
  this.registryConfig = config.registry;
968
1069
  this.maxHandlersPerAction = config.registry?.maxHandlersPerAction ?? 1e3;
@@ -979,10 +1080,10 @@ var ActionRegister = class {
979
1080
  }
980
1081
  /**
981
1082
  * 🆕 Action-based dispatcher
982
- *
1083
+ *
983
1084
  * Provides function-based access to actions for more convenient dispatching.
984
1085
  * Each action becomes a callable function that can be invoked directly.
985
- *
1086
+ *
986
1087
  * @example
987
1088
  * ```typescript
988
1089
  * interface MyActions extends ActionPayloadMap {
@@ -995,19 +1096,19 @@ var ActionRegister = class {
995
1096
  * // Function-based dispatching
996
1097
  * await registry.actions.userLogin({ userId: '123', email: 'test@example.com' });
997
1098
  * await registry.actions.resetApp();
1099
+ * await registry.actions.resetApp({ debounce: 100 }); // Legacy options-first form
1100
+ * await registry.actions.resetApp(undefined, { debounce: 100 });
998
1101
  * ```
999
1102
  *
1000
1103
  * @public
1001
1104
  */
1002
1105
  get actions() {
1003
1106
  if (!this._actionsProxy) this._actionsProxy = new Proxy({}, { get: (_target, prop) => {
1004
- if (typeof prop === "string" && this.pipelines.has(prop)) {
1005
- const actionKey = prop;
1006
- return (payloadOrOptions, options) => {
1007
- if (payloadOrOptions && isDispatchOptions(payloadOrOptions)) return this.dispatch(actionKey, void 0, payloadOrOptions);
1008
- else return this.dispatch(actionKey, payloadOrOptions, options);
1009
- };
1010
- }
1107
+ const actionKey = prop;
1108
+ if (typeof prop === "string" && this.pipelines.has(actionKey)) return (payloadOrOptions, options) => {
1109
+ if (isDispatchOptions(payloadOrOptions)) return this.dispatch(actionKey, void 0, payloadOrOptions);
1110
+ return this.dispatch(actionKey, payloadOrOptions, options);
1111
+ };
1011
1112
  } });
1012
1113
  return this._actionsProxy;
1013
1114
  }
@@ -1024,6 +1125,10 @@ var ActionRegister = class {
1024
1125
  *
1025
1126
  * // Actions without payload
1026
1127
  * const result = await registry.actionsWithResult.userLogout();
1128
+ * const debouncedResult = await registry.actionsWithResult.userLogout(
1129
+ * undefined,
1130
+ * { debounce: 100 }
1131
+ * );
1027
1132
  *
1028
1133
  * // With options
1029
1134
  * const result = await registry.actionsWithResult.processData(
@@ -1036,13 +1141,11 @@ var ActionRegister = class {
1036
1141
  */
1037
1142
  get actionsWithResult() {
1038
1143
  if (!this._actionsWithResultProxy) this._actionsWithResultProxy = new Proxy({}, { get: (_target, prop) => {
1039
- if (typeof prop === "string" && this.pipelines.has(prop)) {
1040
- const actionKey = prop;
1041
- return (payloadOrOptions, options) => {
1042
- if (payloadOrOptions && isDispatchOptions(payloadOrOptions)) return this.dispatchWithResult(actionKey, void 0, payloadOrOptions);
1043
- else return this.dispatchWithResult(actionKey, payloadOrOptions, options);
1044
- };
1045
- }
1144
+ const actionKey = prop;
1145
+ if (typeof prop === "string" && this.pipelines.has(actionKey)) return (payloadOrOptions, options) => {
1146
+ if (isDispatchOptions(payloadOrOptions)) return this.dispatchWithResult(actionKey, void 0, payloadOrOptions);
1147
+ return this.dispatchWithResult(actionKey, payloadOrOptions, options);
1148
+ };
1046
1149
  } });
1047
1150
  return this._actionsWithResultProxy;
1048
1151
  }
@@ -1062,6 +1165,7 @@ var ActionRegister = class {
1062
1165
  * @public
1063
1166
  */
1064
1167
  register(action, handler, config = {}) {
1168
+ this.assertAcceptingWork();
1065
1169
  const handlerId = config.id || this.generateHandlerId(action);
1066
1170
  return this._performRegistrationSync(action, handler, config, handlerId);
1067
1171
  }
@@ -1074,6 +1178,15 @@ var ActionRegister = class {
1074
1178
  console[level](`🎯 [${timestamp}] [${this.name}] ${message}`, data || "");
1075
1179
  }
1076
1180
  }
1181
+ assertAcceptingWork() {
1182
+ if (this.lifecycleState !== "active") throw new ActionRegisterDestroyedError(this.name, this.lifecycleState);
1183
+ }
1184
+ rejectedLifecyclePromise() {
1185
+ const error = new ActionRegisterDestroyedError(this.name, this.lifecycleState === "active" ? "destroyed" : this.lifecycleState);
1186
+ const rejected = Promise.reject(error);
1187
+ rejected.catch(() => {});
1188
+ return rejected;
1189
+ }
1077
1190
  /**
1078
1191
  * 🔧 Generate unique handler ID using optimized counter-based approach
1079
1192
  */
@@ -1212,16 +1325,245 @@ var ActionRegister = class {
1212
1325
  });
1213
1326
  return unregister;
1214
1327
  }
1215
- async dispatch(action, payload, options) {
1216
- if (options?.immediate || !this.dispatchQueue) return this._performDispatch(action, payload, options);
1217
- else return this.dispatchQueue.enqueue(async () => {
1218
- return this._performDispatch(action, payload, options);
1328
+ dispatch(action, payload, options) {
1329
+ if (this.lifecycleState !== "active") return this.rejectedLifecyclePromise();
1330
+ const timeoutScope = this.createTimeoutScope(action, options);
1331
+ const dispatchHandlerPromises = /* @__PURE__ */ new Set();
1332
+ const attemptState = { count: 0 };
1333
+ const operation = async () => {
1334
+ if (!timeoutScope.options?.signal?.aborted) this.validatePayload(action, payload);
1335
+ return this.executeWithRetry(async () => {
1336
+ const executedHandlers = [];
1337
+ try {
1338
+ return await this._performDispatch(action, payload, timeoutScope.options, attemptState.count > 1, executedHandlers, dispatchHandlerPromises);
1339
+ } finally {
1340
+ this.cleanupOneTimeHandlers(action, executedHandlers, dispatchHandlerPromises);
1341
+ }
1342
+ }, timeoutScope.options, attemptState, void 0, () => this.getHandlerCount(action) > 0);
1343
+ };
1344
+ 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;
1345
+ let dispatchPromise;
1346
+ this.dispatchConstructionDepth += 1;
1347
+ try {
1348
+ if (timeoutScope.options?.immediate || hasTimingGuard || !this.dispatchQueue) dispatchPromise = operation();
1349
+ else {
1350
+ const queued = this.dispatchQueue.enqueueWithHandle(operation, timeoutScope.options?.queuePriority ?? 0);
1351
+ timeoutScope.onTimeout((error) => queued.cancel(error));
1352
+ dispatchPromise = queued.promise;
1353
+ }
1354
+ this.trackDispatchPromise(dispatchPromise);
1355
+ } finally {
1356
+ this.dispatchConstructionDepth -= 1;
1357
+ }
1358
+ const observedPromise = this.raceWithTimeout(dispatchPromise, timeoutScope, dispatchHandlerPromises).catch((error) => {
1359
+ this.invokeErrorHandler(error, action, payload, options, attemptState.count);
1360
+ throw error;
1361
+ });
1362
+ observedPromise.catch(() => {});
1363
+ return observedPromise;
1364
+ }
1365
+ /** Execute a dispatch operation with an optional whole-action retry policy. */
1366
+ async executeWithRetry(operation, options, attemptState, shouldRetryResult, canRetry = () => true) {
1367
+ const configuredAttempts = options?.retryOnError?.maxAttempts ?? 1;
1368
+ const maxAttempts = Number.isFinite(configuredAttempts) ? Math.max(1, Math.floor(configuredAttempts)) : 1;
1369
+ const retryDelay = Math.max(0, options?.retryOnError?.delay ?? 0);
1370
+ while (attemptState.count < maxAttempts) {
1371
+ attemptState.count += 1;
1372
+ try {
1373
+ const result = await operation();
1374
+ if (!(shouldRetryResult?.(result) ?? false) || attemptState.count >= maxAttempts || options?.signal?.aborted || !canRetry()) return result;
1375
+ } catch (error) {
1376
+ if (error instanceof ActionValidationError || attemptState.count >= maxAttempts || options?.signal?.aborted || !canRetry()) throw error;
1377
+ }
1378
+ await this.waitForRetry(retryDelay, options?.signal);
1379
+ }
1380
+ return operation();
1381
+ }
1382
+ trackDispatchPromise(promise) {
1383
+ this.activeDispatches.add(promise);
1384
+ const remove = () => this.activeDispatches.delete(promise);
1385
+ promise.then(remove, remove);
1386
+ return promise;
1387
+ }
1388
+ trackHandlerPromise(promise, dispatchHandlerPromises) {
1389
+ this.activeHandlerPromises.add(promise);
1390
+ dispatchHandlerPromises.add(promise);
1391
+ const remove = () => {
1392
+ this.activeHandlerPromises.delete(promise);
1393
+ dispatchHandlerPromises.delete(promise);
1394
+ };
1395
+ promise.then(remove, remove);
1396
+ return promise;
1397
+ }
1398
+ trackGlobalHandlerPromise(promise) {
1399
+ this.activeHandlerPromises.add(promise);
1400
+ const remove = () => this.activeHandlerPromises.delete(promise);
1401
+ promise.then(remove, remove);
1402
+ return promise;
1403
+ }
1404
+ /** Abort-aware retry delay so cancellation does not wait for the full backoff. */
1405
+ waitForRetry(delay, signal) {
1406
+ if (delay <= 0 || signal?.aborted) return Promise.resolve();
1407
+ return new Promise((resolve) => {
1408
+ const timer = setTimeout(finish, delay);
1409
+ const abort = () => finish();
1410
+ function finish() {
1411
+ clearTimeout(timer);
1412
+ signal?.removeEventListener("abort", abort);
1413
+ resolve();
1414
+ }
1415
+ signal?.addEventListener("abort", abort, { once: true });
1219
1416
  });
1220
1417
  }
1418
+ /** Build a wall-clock timeout that also participates in pipeline cancellation. */
1419
+ createTimeoutScope(action, options) {
1420
+ const hasTimeout = options?.timeout !== void 0 && Number.isFinite(options.timeout);
1421
+ const timeout = hasTimeout ? Math.max(0, options.timeout) : void 0;
1422
+ const timeoutController = hasTimeout ? new AbortController() : void 0;
1423
+ const signalCleanups = [];
1424
+ const timeoutCallbacks = /* @__PURE__ */ new Set();
1425
+ const signals = [
1426
+ this.lifecycleController.signal,
1427
+ options?.signal,
1428
+ timeoutController?.signal
1429
+ ].filter((candidate) => Boolean(candidate));
1430
+ let signal = signals[0];
1431
+ if (signals.length > 1) if (typeof AbortSignal.any === "function") signal = AbortSignal.any(signals);
1432
+ else {
1433
+ const mergedController = new AbortController();
1434
+ const forwardAbort = (source) => {
1435
+ if (!mergedController.signal.aborted) mergedController.abort(source.reason);
1436
+ };
1437
+ for (const source of signals) {
1438
+ if (source.aborted) {
1439
+ forwardAbort(source);
1440
+ break;
1441
+ }
1442
+ const listener = () => forwardAbort(source);
1443
+ source.addEventListener("abort", listener, { once: true });
1444
+ signalCleanups.push(() => source.removeEventListener("abort", listener));
1445
+ }
1446
+ signal = mergedController.signal;
1447
+ }
1448
+ let timer;
1449
+ const timeoutPromise = timeoutController && timeout !== void 0 ? new Promise((_, reject) => {
1450
+ timer = setTimeout(() => {
1451
+ const error = new ActionTimeoutError(String(action), timeout);
1452
+ timeoutController.abort(error);
1453
+ timeoutCallbacks.forEach((callback) => callback(error));
1454
+ reject(error);
1455
+ }, timeout);
1456
+ }) : void 0;
1457
+ return {
1458
+ options: {
1459
+ ...options,
1460
+ signal
1461
+ },
1462
+ timeoutPromise,
1463
+ onTimeout: (callback) => timeoutCallbacks.add(callback),
1464
+ cleanup: () => {
1465
+ if (timer !== void 0) clearTimeout(timer);
1466
+ timeoutCallbacks.clear();
1467
+ },
1468
+ cleanupSignals: () => signalCleanups.forEach((cleanup) => cleanup())
1469
+ };
1470
+ }
1471
+ /** Expose timeout failure while allowing the queued operation to drain safely. */
1472
+ raceWithTimeout(operation, scope, dispatchHandlerPromises) {
1473
+ const exposed = scope.timeoutPromise ? Promise.race([operation, scope.timeoutPromise]) : operation;
1474
+ const cleanupAfterStartedHandlers = () => {
1475
+ scope.cleanup();
1476
+ this.cleanupSignalsAfterStartedHandlers(scope.cleanupSignals, dispatchHandlerPromises);
1477
+ };
1478
+ exposed.then(cleanupAfterStartedHandlers, cleanupAfterStartedHandlers);
1479
+ return exposed;
1480
+ }
1481
+ cleanupSignalsAfterStartedHandlers(cleanup, dispatchHandlerPromises) {
1482
+ const handlersStillRunning = [...dispatchHandlerPromises];
1483
+ if (handlersStillRunning.length === 0) {
1484
+ cleanup();
1485
+ return;
1486
+ }
1487
+ Promise.allSettled(handlersStillRunning).then(cleanup);
1488
+ }
1489
+ /** Invoke the configured error handler without allowing it to replace the dispatch error. */
1490
+ invokeErrorHandler(error, action, payload, options, attempts) {
1491
+ const errorHandler = this.registryConfig?.errorHandler;
1492
+ if (!errorHandler) return;
1493
+ const normalizedError = error instanceof Error ? error : new Error(String(error));
1494
+ try {
1495
+ const handlerResult = errorHandler(normalizedError, {
1496
+ action: String(action),
1497
+ payload,
1498
+ options,
1499
+ attempts,
1500
+ phase: normalizedError instanceof ActionTimeoutError ? "timeout" : normalizedError instanceof ActionValidationError ? "validation" : "execution"
1501
+ });
1502
+ if (handlerResult && typeof handlerResult.then === "function") Promise.resolve(handlerResult).catch((handlerError) => {
1503
+ this.log("Global async error handler failed", handlerError, "warn");
1504
+ });
1505
+ } catch (handlerError) {
1506
+ this.log("Global error handler failed", handlerError, "warn");
1507
+ }
1508
+ }
1509
+ /**
1510
+ * Validate an action payload against the configured schema.
1511
+ * Shared by all dispatch paths so result collection cannot bypass validation.
1512
+ */
1513
+ validatePayload(action, payload) {
1514
+ if (!this.registryConfig?.schema || this.registryConfig.validateOnDispatch === false) return;
1515
+ const actionName = String(action);
1516
+ const actionSchema = this.registryConfig.schema[actionName];
1517
+ if (!actionSchema) return;
1518
+ let result;
1519
+ try {
1520
+ result = actionSchema.safeParse(payload);
1521
+ } catch (error) {
1522
+ throw new ActionValidationError(actionName, error);
1523
+ }
1524
+ if (result.success) return {
1525
+ passed: true,
1526
+ errors: []
1527
+ };
1528
+ const mode = this.registryConfig.validationMode ?? "strict";
1529
+ if (mode === "strict") throw new ActionValidationError(actionName, result.error);
1530
+ if (mode === "warn") {
1531
+ console.warn(`Action "${actionName}" payload validation failed:`, result.error.message);
1532
+ this.log(`Validation warning for action '${actionName}'`, { issues: result.error.issues }, "warn");
1533
+ }
1534
+ return {
1535
+ passed: false,
1536
+ errors: result.error.issues.map((issue) => issue.message)
1537
+ };
1538
+ }
1539
+ createAbortedExecutionResult(startTime, handlersSkipped = 0, validation) {
1540
+ const endTime = Date.now();
1541
+ return {
1542
+ success: false,
1543
+ aborted: true,
1544
+ abortReason: "Action dispatch aborted by signal",
1545
+ terminated: false,
1546
+ validation,
1547
+ result: void 0,
1548
+ successResults: [],
1549
+ results: [],
1550
+ failedResults: [],
1551
+ execution: {
1552
+ duration: endTime - startTime,
1553
+ handlersExecuted: 0,
1554
+ handlersSkipped,
1555
+ handlersFailed: 0,
1556
+ startTime,
1557
+ endTime
1558
+ },
1559
+ handlers: [],
1560
+ errors: []
1561
+ };
1562
+ }
1221
1563
  /**
1222
1564
  * 🆕 실제 디스패치 작업 수행 (큐에서 호출됨)
1223
1565
  */
1224
- async _performDispatch(action, payload, options) {
1566
+ async _performDispatch(action, payload, options, skipGuards, executedHandlers, dispatchHandlerPromises) {
1225
1567
  this.log(`Starting dispatch for action '${String(action)}'`, {
1226
1568
  hasPayload: payload !== void 0,
1227
1569
  payloadType: payload?.constructor?.name || typeof payload,
@@ -1229,24 +1571,11 @@ var ActionRegister = class {
1229
1571
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
1230
1572
  });
1231
1573
  if (payload instanceof Event && true) console.warn(`Event object passed to action "${String(action)}"`, payload.type);
1232
- if (this.registryConfig?.schema && this.registryConfig?.validateOnDispatch !== false) {
1233
- const actionSchema = this.registryConfig.schema[action];
1234
- if (actionSchema) {
1235
- const result = actionSchema.safeParse(payload);
1236
- if (!result.success) {
1237
- const mode = this.registryConfig.validationMode ?? "strict";
1238
- if (mode === "strict") throw new ActionValidationError(action, result.error);
1239
- else if (mode === "warn") {
1240
- console.warn(`Action "${String(action)}" payload validation failed:`, result.error.message);
1241
- this.log(`Validation warning for action '${String(action)}'`, { issues: result.error.issues }, "warn");
1242
- }
1243
- }
1244
- }
1245
- }
1246
1574
  const [effectiveSignal, autoAbortController, cleanup] = this.createAbortSignal(options);
1247
1575
  if (options?.autoAbort?.onControllerCreated && autoAbortController) options.autoAbort.onControllerCreated(autoAbortController);
1248
1576
  if (effectiveSignal?.aborted) {
1249
1577
  this.log(`Dispatch aborted before execution for '${String(action)}'`);
1578
+ cleanup();
1250
1579
  return;
1251
1580
  }
1252
1581
  const pipeline = this.pipelines.get(action);
@@ -1262,6 +1591,7 @@ var ActionRegister = class {
1262
1591
  console.warn("💡 Tip: Register a handler using registry.register() before dispatching this action.");
1263
1592
  console.warn("📋 Available actions:", Array.from(this.pipelines.keys()));
1264
1593
  this.log(`No handlers found for action '${String(action)}', dispatch cancelled`, {}, "warn");
1594
+ cleanup();
1265
1595
  return;
1266
1596
  }
1267
1597
  const filteredHandlers = options?.filter ? this.filterHandlers(pipeline, options.filter) : pipeline;
@@ -1282,17 +1612,32 @@ var ActionRegister = class {
1282
1612
  break;
1283
1613
  }
1284
1614
  }
1285
- if (debounceMs !== void 0) {
1286
- if (!await this.actionGuard.debounce(actionKey, debounceMs)) return;
1615
+ if (!skipGuards && debounceMs !== void 0) {
1616
+ if (!await this.actionGuard.debounce(actionKey, debounceMs)) {
1617
+ cleanup();
1618
+ return;
1619
+ }
1287
1620
  }
1288
- if (throttleMs !== void 0) {
1289
- if (!this.actionGuard.throttle(actionKey, throttleMs)) return;
1621
+ if (!skipGuards && throttleMs !== void 0) {
1622
+ if (!this.actionGuard.throttle(actionKey, throttleMs)) {
1623
+ cleanup();
1624
+ return;
1625
+ }
1626
+ }
1627
+ if (effectiveSignal?.aborted) {
1628
+ this.log(`Dispatch aborted during guard processing for '${String(action)}'`);
1629
+ cleanup();
1630
+ return;
1290
1631
  }
1291
1632
  const currentExecutionMode = options?.executionMode || this.actionExecutionModes.get(action) || this.executionMode;
1292
1633
  const context = {
1293
1634
  action: String(action),
1294
1635
  payload,
1295
- handlers: filteredHandlers,
1636
+ handlers: [...filteredHandlers],
1637
+ executedHandlers: [],
1638
+ deferOnceCleanup: true,
1639
+ signal: effectiveSignal ?? this.lifecycleController.signal,
1640
+ trackHandlerPromise: (promise) => this.trackHandlerPromise(promise, dispatchHandlerPromises),
1296
1641
  aborted: false,
1297
1642
  abortReason: void 0,
1298
1643
  currentIndex: 0,
@@ -1306,17 +1651,19 @@ var ActionRegister = class {
1306
1651
  };
1307
1652
  const abortHandler = effectiveSignal ? () => {
1308
1653
  context.aborted = true;
1309
- context.abortReason = "Action dispatch aborted by signal";
1654
+ context.abortReason = typeof effectiveSignal.reason === "string" ? effectiveSignal.reason : "Action dispatch aborted by signal";
1310
1655
  } : void 0;
1311
- if (effectiveSignal && abortHandler) effectiveSignal.addEventListener("abort", abortHandler);
1656
+ if (effectiveSignal && abortHandler) effectiveSignal.addEventListener("abort", abortHandler, { once: true });
1312
1657
  try {
1313
- await this.executePipeline(context, autoAbortController, options?.autoAbort);
1658
+ await this.executePipeline(context, dispatchHandlerPromises, autoAbortController, options?.autoAbort);
1314
1659
  this.log(`Pipeline execution succeeded for ${String(action)}`);
1315
1660
  } catch (error) {
1316
1661
  this.log(`Pipeline execution failed for ${String(action)}`, error, "error");
1317
1662
  throw error;
1318
1663
  } finally {
1319
- cleanup();
1664
+ executedHandlers?.push(...context.executedHandlers ?? []);
1665
+ if (effectiveSignal && abortHandler) effectiveSignal.removeEventListener("abort", abortHandler);
1666
+ this.cleanupSignalsAfterStartedHandlers(cleanup, dispatchHandlerPromises);
1320
1667
  }
1321
1668
  }
1322
1669
  /**
@@ -1332,41 +1679,70 @@ var ActionRegister = class {
1332
1679
  *
1333
1680
  * @public
1334
1681
  */
1335
- async dispatchWithResult(action, payload, options) {
1682
+ dispatchWithResult(action, payload, options) {
1683
+ if (this.lifecycleState !== "active") return this.rejectedLifecyclePromise();
1684
+ const timeoutScope = this.createTimeoutScope(action, options);
1685
+ const dispatchHandlerPromises = /* @__PURE__ */ new Set();
1686
+ const attemptState = { count: 0 };
1687
+ let validation;
1688
+ const operation = async () => {
1689
+ if (!timeoutScope.options?.signal?.aborted) validation = this.validatePayload(action, payload);
1690
+ return this.executeWithRetry(async () => {
1691
+ const executedHandlers = [];
1692
+ try {
1693
+ return await this._performDispatchWithResult(action, payload, timeoutScope.options, validation, attemptState.count > 1, executedHandlers, dispatchHandlerPromises);
1694
+ } finally {
1695
+ this.cleanupOneTimeHandlers(action, executedHandlers, dispatchHandlerPromises);
1696
+ }
1697
+ }, timeoutScope.options, attemptState, (result) => !result.success && !result.aborted && this.getHandlerCount(action) > 0, () => this.getHandlerCount(action) > 0);
1698
+ };
1699
+ const shouldQueue = !timeoutScope.options?.immediate && Boolean(this.dispatchQueue) && timeoutScope.options?.queuePriority !== void 0;
1700
+ let dispatchPromise;
1701
+ this.dispatchConstructionDepth += 1;
1702
+ try {
1703
+ if (shouldQueue) {
1704
+ const queued = this.dispatchQueue.enqueueWithHandle(operation, timeoutScope.options.queuePriority);
1705
+ timeoutScope.onTimeout((error) => queued.cancel(error));
1706
+ dispatchPromise = queued.promise;
1707
+ } else dispatchPromise = operation();
1708
+ this.trackDispatchPromise(dispatchPromise);
1709
+ } finally {
1710
+ this.dispatchConstructionDepth -= 1;
1711
+ }
1712
+ const observedPromise = this.raceWithTimeout(dispatchPromise, timeoutScope, dispatchHandlerPromises).then((result) => {
1713
+ if (!result.success && !result.aborted) {
1714
+ const terminalError = result.errors[result.errors.length - 1]?.error ?? /* @__PURE__ */ new Error(`Action "${String(action)}" failed`);
1715
+ this.invokeErrorHandler(terminalError, action, payload, options, attemptState.count);
1716
+ }
1717
+ return result;
1718
+ }, (error) => {
1719
+ this.invokeErrorHandler(error, action, payload, options, attemptState.count);
1720
+ throw error;
1721
+ });
1722
+ observedPromise.catch(() => {});
1723
+ return observedPromise;
1724
+ }
1725
+ async _performDispatchWithResult(action, payload, options, validation, skipGuards, executedHandlers, dispatchHandlerPromises) {
1336
1726
  const _startTime = Date.now();
1337
1727
  const [effectiveSignal, autoAbortController, cleanup] = this.createAbortSignal(options);
1338
1728
  if (options?.autoAbort?.onControllerCreated && autoAbortController) options.autoAbort.onControllerCreated(autoAbortController);
1339
- if (effectiveSignal?.aborted) return {
1340
- success: false,
1341
- aborted: true,
1342
- abortReason: "Action dispatch aborted by signal",
1343
- terminated: false,
1344
- result: void 0,
1345
- successResults: [],
1346
- results: [],
1347
- failedResults: [],
1348
- execution: {
1349
- duration: 0,
1350
- handlersExecuted: 0,
1351
- handlersSkipped: 0,
1352
- handlersFailed: 0,
1353
- startTime: _startTime,
1354
- endTime: _startTime
1355
- },
1356
- handlers: [],
1357
- errors: []
1358
- };
1729
+ if (effectiveSignal?.aborted) {
1730
+ cleanup();
1731
+ return this.createAbortedExecutionResult(_startTime, 0, validation);
1732
+ }
1359
1733
  const pipeline = this.pipelines.get(action);
1360
1734
  if (!pipeline || pipeline.length === 0) {
1361
1735
  const warningMessage = `⚠️ Action '${String(action)}' has no registered handlers. This action will be ignored.`;
1362
1736
  console.warn(warningMessage);
1363
1737
  console.warn("💡 Tip: Register a handler using registry.register() before dispatching this action.");
1364
1738
  console.warn("📋 Available actions:", Array.from(this.pipelines.keys()));
1739
+ cleanup();
1365
1740
  return {
1366
1741
  success: true,
1367
1742
  aborted: false,
1368
1743
  abortReason: void 0,
1369
1744
  terminated: false,
1745
+ validation,
1370
1746
  result: void 0,
1371
1747
  successResults: [],
1372
1748
  results: [],
@@ -1385,13 +1761,27 @@ var ActionRegister = class {
1385
1761
  }
1386
1762
  const filteredHandlers = options?.filter ? this.filterHandlers(pipeline, options.filter) : pipeline;
1387
1763
  const actionKey = String(action);
1388
- const guardResult = await this.applyActionGuardControlsWithResult(actionKey, filteredHandlers, options, _startTime, pipeline.length);
1389
- if (guardResult) return guardResult;
1764
+ const guardResult = skipGuards ? null : await this.applyActionGuardControlsWithResult(actionKey, filteredHandlers, options, _startTime, pipeline.length);
1765
+ if (effectiveSignal?.aborted) {
1766
+ cleanup();
1767
+ return this.createAbortedExecutionResult(_startTime, pipeline.length, validation);
1768
+ }
1769
+ if (guardResult) {
1770
+ cleanup();
1771
+ return {
1772
+ ...guardResult,
1773
+ validation
1774
+ };
1775
+ }
1390
1776
  const currentExecutionMode = options?.executionMode || this.actionExecutionModes.get(action) || this.executionMode;
1391
1777
  const context = {
1392
1778
  action: String(action),
1393
1779
  payload,
1394
- handlers: filteredHandlers,
1780
+ handlers: [...filteredHandlers],
1781
+ executedHandlers: [],
1782
+ deferOnceCleanup: true,
1783
+ signal: effectiveSignal ?? this.lifecycleController.signal,
1784
+ trackHandlerPromise: (promise) => this.trackHandlerPromise(promise, dispatchHandlerPromises),
1395
1785
  aborted: false,
1396
1786
  abortReason: void 0,
1397
1787
  currentIndex: 0,
@@ -1417,12 +1807,12 @@ var ActionRegister = class {
1417
1807
  });
1418
1808
  const abortHandler = effectiveSignal ? () => {
1419
1809
  context.aborted = true;
1420
- context.abortReason = "Action dispatch aborted by signal";
1810
+ context.abortReason = typeof effectiveSignal.reason === "string" ? effectiveSignal.reason : "Action dispatch aborted by signal";
1421
1811
  } : void 0;
1422
- if (effectiveSignal && abortHandler) effectiveSignal.addEventListener("abort", abortHandler);
1812
+ if (effectiveSignal && abortHandler) effectiveSignal.addEventListener("abort", abortHandler, { once: true });
1423
1813
  let errors = [];
1424
1814
  try {
1425
- await this.executePipeline(context, autoAbortController, options?.autoAbort);
1815
+ await this.executePipeline(context, dispatchHandlerPromises, autoAbortController, options?.autoAbort);
1426
1816
  errors = context.collectedErrors || [];
1427
1817
  const executedCount = Math.min(context.currentIndex + (context.aborted ? 0 : 1), filteredHandlers.length);
1428
1818
  for (let i = 0; i < executedCount; i++) {
@@ -1448,7 +1838,9 @@ var ActionRegister = class {
1448
1838
  if (handlerResult) handlerResult.executed = true;
1449
1839
  }
1450
1840
  } finally {
1451
- cleanup();
1841
+ executedHandlers.push(...context.executedHandlers ?? []);
1842
+ if (effectiveSignal && abortHandler) effectiveSignal.removeEventListener("abort", abortHandler);
1843
+ this.cleanupSignalsAfterStartedHandlers(cleanup, dispatchHandlerPromises);
1452
1844
  }
1453
1845
  const endTime = Date.now();
1454
1846
  const processedResult = this.processResults(context, options?.result);
@@ -1458,11 +1850,12 @@ var ActionRegister = class {
1458
1850
  error: err.error,
1459
1851
  expectedType: typeof processedResult
1460
1852
  }));
1461
- const executionResult = {
1853
+ return {
1462
1854
  success: !executionError && !context.aborted,
1463
1855
  aborted: context.aborted,
1464
1856
  abortReason: context.abortReason,
1465
1857
  terminated: context.terminated,
1858
+ validation,
1466
1859
  result: processedResult,
1467
1860
  successResults,
1468
1861
  results: context.results,
@@ -1483,9 +1876,6 @@ var ActionRegister = class {
1483
1876
  severity: "non-blocking"
1484
1877
  }))
1485
1878
  };
1486
- /** Clean up one-time handlers after execution */
1487
- this.cleanupOneTimeHandlers(action, context.handlers);
1488
- return executionResult;
1489
1879
  }
1490
1880
  /**
1491
1881
  * 🔧 Unified method for dispatchWithResult that returns ExecutionResult on guard rejection
@@ -1574,6 +1964,7 @@ var ActionRegister = class {
1574
1964
  getControllerFromPool(context, autoAbortController, autoAbortOptions) {
1575
1965
  let controller = this.controllerPool.pop();
1576
1966
  if (!controller) controller = {};
1967
+ controller.signal = context.signal ?? this.lifecycleController.signal;
1577
1968
  controller.abort = (reason) => {
1578
1969
  context.aborted = true;
1579
1970
  context.abortReason = reason;
@@ -1652,7 +2043,7 @@ var ActionRegister = class {
1652
2043
  return limitedResults[limitedResults.length - 1];
1653
2044
  }
1654
2045
  }
1655
- async executePipeline(context, autoAbortController, autoAbortOptions) {
2046
+ async executePipeline(context, dispatchHandlerPromises, autoAbortController, autoAbortOptions) {
1656
2047
  const createController = (_registration, _index) => {
1657
2048
  return this.getControllerFromPool(context, autoAbortController, autoAbortOptions);
1658
2049
  };
@@ -1668,28 +2059,27 @@ var ActionRegister = class {
1668
2059
  break;
1669
2060
  default: throw new Error(`Unknown execution mode: ${context.executionMode}`);
1670
2061
  }
1671
- this.cleanupOneTimeHandlers(context.action, context.handlers);
2062
+ if (!context.deferOnceCleanup) this.cleanupOneTimeHandlers(context.action, context.executedHandlers ?? [], dispatchHandlerPromises);
1672
2063
  }
1673
- cleanupOneTimeHandlers(action, executedHandlers) {
1674
- const pipeline = this.pipelines.get(action);
1675
- if (!pipeline) return;
2064
+ cleanupOneTimeHandlers(action, executedHandlers, dispatchHandlerPromises) {
1676
2065
  const oneTimeHandlers = executedHandlers.filter((reg) => reg.config.once);
1677
2066
  if (oneTimeHandlers.length === 0) return;
2067
+ const handlersStillRunning = [...dispatchHandlerPromises];
2068
+ const shouldDeferCleanup = handlersStillRunning.length > 0;
1678
2069
  oneTimeHandlers.forEach((registration) => {
1679
- const index = pipeline.findIndex((reg) => reg.id === registration.id);
1680
- if (index !== -1) {
1681
- pipeline.splice(index, 1);
1682
- if (this.registryConfig?.debug && true) console.log(`🎯 One-time handler removed: ${String(action)}`, {
2070
+ if (this.removeRegistration(action, registration, !shouldDeferCleanup)) {
2071
+ if (shouldDeferCleanup && typeof registration.config.cleanup === "function") {
2072
+ const cleanupPromise = Promise.allSettled(handlersStillRunning).then(() => {
2073
+ this.runRegistrationCleanup(action, registration);
2074
+ });
2075
+ this.trackGlobalHandlerPromise(cleanupPromise).catch(() => {});
2076
+ }
2077
+ this.log(`One-time handler removed: ${String(action)}`, {
1683
2078
  handlerId: registration.id,
1684
- remainingHandlers: pipeline.length,
1685
- registry: this.name
2079
+ remainingHandlers: this.getHandlerCount(action)
1686
2080
  });
1687
2081
  }
1688
2082
  });
1689
- if (pipeline.length === 0) {
1690
- this.pipelines.delete(action);
1691
- this.lastRegisteredTimestamps.delete(action);
1692
- }
1693
2083
  }
1694
2084
  /**
1695
2085
  * Get the number of registered handlers for an action
@@ -1742,8 +2132,13 @@ var ActionRegister = class {
1742
2132
  * @public
1743
2133
  */
1744
2134
  clearAction(action) {
2135
+ const pipeline = this.pipelines.get(action);
2136
+ if (pipeline) [...pipeline].forEach((registration) => {
2137
+ this.removeRegistration(action, registration);
2138
+ });
1745
2139
  this.pipelines.delete(action);
1746
2140
  this.lastRegisteredTimestamps.delete(action);
2141
+ this.actionGuard.clearGuards(String(action));
1747
2142
  }
1748
2143
  /**
1749
2144
  * Remove all handlers for all actions
@@ -1753,8 +2148,13 @@ var ActionRegister = class {
1753
2148
  * @public
1754
2149
  */
1755
2150
  clearAll() {
2151
+ [...this.pipelines.keys()].forEach((action) => {
2152
+ this.clearAction(action);
2153
+ });
1756
2154
  this.pipelines.clear();
1757
2155
  this.lastRegisteredTimestamps.clear();
2156
+ this.unregisterFunctions.clear();
2157
+ this.actionGuard.clearAll();
1758
2158
  }
1759
2159
  /**
1760
2160
  * Get the name of this action register
@@ -1883,29 +2283,36 @@ var ActionRegister = class {
1883
2283
  */
1884
2284
  createUnregisterFunction(action, handlerId, registration) {
1885
2285
  return () => {
1886
- const pipeline = this.pipelines.get(action);
1887
- if (!pipeline) return;
1888
- const index = pipeline.findIndex((reg) => reg.id === handlerId && reg === registration);
1889
- if (index !== -1) {
1890
- pipeline.splice(index, 1);
1891
- this.unregisterFunctions.delete(handlerId);
1892
- if (pipeline.length === 0) {
1893
- this.pipelines.delete(action);
1894
- this.lastRegisteredTimestamps.delete(action);
1895
- }
1896
- if (registration.config.cleanup && typeof registration.config.cleanup === "function") try {
1897
- registration.config.cleanup();
1898
- } catch (cleanupError) {
1899
- this.log(`Cleanup error during unregister: ${String(action)}`, cleanupError, "warn");
1900
- }
1901
- this.log(`Handler unregistered: ${String(action)}`, {
1902
- handlerId,
1903
- remainingHandlers: pipeline.length,
1904
- actionRemoved: pipeline.length === 0
1905
- });
1906
- }
2286
+ if (this.removeRegistration(action, registration)) this.log(`Handler unregistered: ${String(action)}`, {
2287
+ handlerId,
2288
+ remainingHandlers: this.getHandlerCount(action),
2289
+ actionRemoved: !this.pipelines.has(action)
2290
+ });
1907
2291
  };
1908
2292
  }
2293
+ /** Remove a registration and release every resource owned by it exactly once. */
2294
+ removeRegistration(action, registration, runCleanup = true) {
2295
+ const pipeline = this.pipelines.get(action);
2296
+ if (!pipeline) return false;
2297
+ const index = pipeline.findIndex((candidate) => candidate === registration);
2298
+ if (index === -1) return false;
2299
+ pipeline.splice(index, 1);
2300
+ this.unregisterFunctions.delete(registration.id);
2301
+ if (runCleanup) this.runRegistrationCleanup(action, registration);
2302
+ if (pipeline.length === 0) {
2303
+ this.pipelines.delete(action);
2304
+ this.lastRegisteredTimestamps.delete(action);
2305
+ }
2306
+ return true;
2307
+ }
2308
+ runRegistrationCleanup(action, registration) {
2309
+ if (!registration.config.cleanup) return;
2310
+ try {
2311
+ registration.config.cleanup();
2312
+ } catch (cleanupError) {
2313
+ this.log(`Cleanup error while removing handler: ${String(action)}`, cleanupError, "warn");
2314
+ }
2315
+ }
1909
2316
  /**
1910
2317
  * Gets the total count of registered unregister functions
1911
2318
  *
@@ -1925,29 +2332,77 @@ var ActionRegister = class {
1925
2332
  hasUnregisterFunction(handlerId) {
1926
2333
  return this.unregisterFunctions.has(handlerId);
1927
2334
  }
1928
- /**
1929
- * 🆕 Destroy method for comprehensive cleanup
1930
- *
1931
- * Cleans up all internal resources including pipelines, guards, queues, and statistics.
1932
- * Should be called when the ActionRegister is no longer needed to prevent memory leaks.
1933
- *
1934
- * @public
1935
- */
1936
- destroy() {
1937
- this.unregisterFunctions.clear();
1938
- for (const [action, pipeline] of this.pipelines.entries()) for (const registration of pipeline) if (registration.config.cleanup && typeof registration.config.cleanup === "function") try {
1939
- registration.config.cleanup();
1940
- } catch (cleanupError) {
1941
- this.log(`Cleanup error for handler during destroy: ${String(action)}`, cleanupError, "warn");
2335
+ /** Reject queued dispatches without releasing registered handlers. */
2336
+ cancelPendingDispatches() {
2337
+ this.dispatchQueue?.clear({ rejectPending: true });
2338
+ }
2339
+ beginShutdown() {
2340
+ if (this.destroyAsyncPromise) return this.destroyAsyncPromise;
2341
+ if (this.lifecycleState === "destroyed") {
2342
+ this.destroyAsyncPromise = Promise.resolve();
2343
+ return this.destroyAsyncPromise;
1942
2344
  }
1943
- this.pipelines.clear();
1944
- this.lastRegisteredTimestamps.clear();
2345
+ this.lifecycleState = "closing";
2346
+ const shutdownError = new ActionRegisterDestroyedError(this.name, "closing");
2347
+ let resolveShutdown;
2348
+ let rejectShutdown;
2349
+ this.destroyAsyncPromise = new Promise((resolve, reject) => {
2350
+ resolveShutdown = resolve;
2351
+ rejectShutdown = reject;
2352
+ });
2353
+ if (!this.lifecycleController.signal.aborted) this.lifecycleController.abort(shutdownError);
1945
2354
  this.actionGuard.destroy();
1946
- this.dispatchQueue?.clear?.();
2355
+ this.dispatchQueue?.clear({
2356
+ rejectPending: true,
2357
+ reason: shutdownError
2358
+ });
2359
+ if (this.dispatchConstructionDepth === 0 && this.activeDispatches.size === 0 && this.activeHandlerPromises.size === 0) {
2360
+ this.finalizeDestroy();
2361
+ resolveShutdown();
2362
+ return this.destroyAsyncPromise;
2363
+ }
2364
+ const drainAndFinalize = async () => {
2365
+ while (this.activeDispatches.size > 0 || this.activeHandlerPromises.size > 0) await Promise.allSettled([...this.activeDispatches, ...this.activeHandlerPromises]);
2366
+ this.finalizeDestroy();
2367
+ };
2368
+ Promise.resolve().then(drainAndFinalize).then(resolveShutdown, rejectShutdown);
2369
+ this.destroyAsyncPromise.catch((error) => {
2370
+ this.log("ActionRegister async destroy failed", error, "warn");
2371
+ });
2372
+ return this.destroyAsyncPromise;
2373
+ }
2374
+ finalizeDestroy() {
2375
+ if (this.lifecycleState === "destroyed") return;
2376
+ this.clearAll();
1947
2377
  this.actionExecutionModes.clear();
1948
2378
  this.controllerPool.length = 0;
2379
+ this.lifecycleState = "destroyed";
1949
2380
  this.log("ActionRegister destroyed");
1950
2381
  }
2382
+ /**
2383
+ * 🆕 Destroy method for comprehensive cleanup
2384
+ *
2385
+ * Begins terminal cleanup of pipelines, guards, queues, and statistics. Cleanup
2386
+ * remains synchronous when no work has started; otherwise active handlers drain
2387
+ * in the background. Use destroyAsync() when completion must be observed.
2388
+ *
2389
+ * @public
2390
+ */
2391
+ destroy() {
2392
+ this.beginShutdown();
2393
+ }
2394
+ /**
2395
+ * Begin terminal shutdown and resolve after all started handlers have settled
2396
+ * and their registered cleanup functions have run.
2397
+ *
2398
+ * Repeated calls return the same promise. New registrations and dispatches are
2399
+ * rejected as soon as shutdown begins.
2400
+ *
2401
+ * @public
2402
+ */
2403
+ destroyAsync() {
2404
+ return this.beginShutdown();
2405
+ }
1951
2406
  };
1952
2407
 
1953
2408
  //#endregion
@@ -2036,12 +2491,18 @@ function createActionHandler(registry, action, handler, config) {
2036
2491
  let currentUnregister;
2037
2492
  let isRegistered = false;
2038
2493
  return {
2494
+ /**
2495
+ * Register the handler and return cleanup function
2496
+ */
2039
2497
  register() {
2040
2498
  if (isRegistered && currentUnregister) currentUnregister();
2041
2499
  currentUnregister = registry.register(action, handler, finalConfig);
2042
2500
  isRegistered = true;
2043
2501
  return currentUnregister;
2044
2502
  },
2503
+ /**
2504
+ * Unregister the handler if currently registered
2505
+ */
2045
2506
  unregister() {
2046
2507
  if (isRegistered && currentUnregister) {
2047
2508
  currentUnregister();
@@ -2049,6 +2510,9 @@ function createActionHandler(registry, action, handler, config) {
2049
2510
  isRegistered = false;
2050
2511
  }
2051
2512
  },
2513
+ /**
2514
+ * Register and return cleanup function (React useEffect pattern)
2515
+ */
2052
2516
  registerWithCleanup() {
2053
2517
  const unregisterFn = this.register();
2054
2518
  return () => {
@@ -2065,18 +2529,33 @@ function createActionHandler(registry, action, handler, config) {
2065
2529
  * Provides debugging and development helpers specifically for React environments.
2066
2530
  */
2067
2531
  const ReactDevUtils = {
2532
+ /**
2533
+ * Enable detailed React integration debugging
2534
+ */
2068
2535
  enableDebugMode() {
2069
2536
  if (typeof window !== "undefined") window.__CONTEXT_ACTION_REACT_DEBUG__ = true;
2070
2537
  },
2538
+ /**
2539
+ * Disable React integration debugging
2540
+ */
2071
2541
  disableDebugMode() {
2072
2542
  if (typeof window !== "undefined") window.__CONTEXT_ACTION_REACT_DEBUG__ = false;
2073
2543
  },
2544
+ /**
2545
+ * Check if React debug mode is enabled
2546
+ */
2074
2547
  isDebugMode() {
2075
2548
  return typeof window !== "undefined" && Boolean(window.__CONTEXT_ACTION_REACT_DEBUG__);
2076
2549
  },
2550
+ /**
2551
+ * Log React-specific debugging information
2552
+ */
2077
2553
  log(component, action, message, data) {
2078
2554
  if (this.isDebugMode()) console.log(`🎯 [React-ActionRegister] [${component}] ${action}: ${message}`, data || "");
2079
2555
  },
2556
+ /**
2557
+ * Get React integration statistics
2558
+ */
2080
2559
  getStats(registry) {
2081
2560
  const registryInfo = registry.getRegistryInfo();
2082
2561
  let reactHandlers = 0;
@@ -2263,5 +2742,5 @@ function createActionFactory(zodModule) {
2263
2742
  }
2264
2743
 
2265
2744
  //#endregion
2266
- export { ActionGuard, ActionRegister, ActionValidationError, ReactActionError, ReactDevUtils, createActionFactory, createActionHandler, createActionSchema, defineAction, executeParallel, executeRace, executeSequential, isActionValidationError, isReactActionError, zodToJsonSchema };
2745
+ export { ActionGuard, ActionRegister, ActionRegisterDestroyedError, ActionTimeoutError, ActionValidationError, ReactActionError, ReactDevUtils, createActionFactory, createActionHandler, createActionSchema, defineAction, executeParallel, executeRace, executeSequential, isActionRegisterDestroyedError, isActionTimeoutError, isActionValidationError, isReactActionError, zodToJsonSchema };
2267
2746
  //# sourceMappingURL=index.js.map