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