@context-action/core 0.8.6 โ†’ 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,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) => {
@@ -97,7 +103,6 @@ async function executeSequential(context, createController) {
97
103
  }
98
104
  i = jumpIndex;
99
105
  context.jumpToPriority = void 0;
100
- continue;
101
106
  } else {
102
107
  context.jumpToPriority = void 0;
103
108
  i++;
@@ -161,10 +166,9 @@ async function executeParallel(context, createController) {
161
166
  skipped: true
162
167
  };
163
168
  }
169
+ (context.executedHandlers ?? (context.executedHandlers = [])).push(registration);
164
170
  const result = registration.handler(context.payload, controller);
165
- let handlerResult;
166
- if (result instanceof Promise) handlerResult = await result;
167
- else handlerResult = result;
171
+ const handlerResult = isPromiseLike(result) ? await Promise.resolve(result) : result;
168
172
  /** Collect result if handler returned something and pipeline wasn't terminated */
169
173
  if (handlerResult !== void 0 && !context.terminated) context.results.push(handlerResult);
170
174
  return {
@@ -183,8 +187,9 @@ async function executeParallel(context, createController) {
183
187
  };
184
188
  }
185
189
  });
190
+ const trackedHandlerPromises = context.trackHandlerPromise ? handlerPromises.map((promise) => context.trackHandlerPromise(promise)) : handlerPromises;
186
191
  /** Wait for all handlers to complete */
187
- const results = await Promise.allSettled(handlerPromises);
192
+ const results = await Promise.allSettled(trackedHandlerPromises);
188
193
  /** Check for any rejected blocking handlers */
189
194
  const failures = results.filter((result, index) => {
190
195
  if (result.status === "rejected") return runnableHandlers[index]?.config.blocking ?? false;
@@ -203,8 +208,10 @@ async function executeParallel(context, createController) {
203
208
  *
204
209
  * Executes all qualifying handlers simultaneously using Promise.race, where
205
210
  * 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.
211
+ * continue in the background and remain tracked for lifecycle cleanup; handlers
212
+ * must observe the controller signal for cooperative external cancellation.
213
+ * Useful for scenarios where you want the fastest response from multiple
214
+ * equivalent handlers.
208
215
  *
209
216
  * @template T - The payload type for the action
210
217
  * @template R - The result type for handlers
@@ -245,10 +252,9 @@ async function executeRace(context, createController) {
245
252
  skipped: true
246
253
  };
247
254
  }
255
+ (context.executedHandlers ?? (context.executedHandlers = [])).push(registration);
248
256
  const result = registration.handler(context.payload, controller);
249
- let handlerResult;
250
- if (result instanceof Promise) handlerResult = await result;
251
- else handlerResult = result;
257
+ const handlerResult = isPromiseLike(result) ? await Promise.resolve(result) : result;
252
258
  return {
253
259
  success: true,
254
260
  handlerId: registration.id,
@@ -266,8 +272,9 @@ async function executeRace(context, createController) {
266
272
  };
267
273
  }
268
274
  });
269
- /** Race all handlers */
270
- const winner = await Promise.race(handlerPromises);
275
+ const trackedHandlerPromises = context.trackHandlerPromise ? handlerPromises.map((promise) => context.trackHandlerPromise(promise)) : handlerPromises;
276
+ /** Race all handlers while retaining every loser for lifecycle draining. */
277
+ const winner = await Promise.race(trackedHandlerPromises);
271
278
  /** If the winner failed and was blocking, throw the error */
272
279
  if (!winner.success && winner.registration?.config.blocking) throw winner.error;
273
280
  /** Collect result from the winning handler */
@@ -316,7 +323,11 @@ var ActionGuard = class {
316
323
  this.cleanupIntervalMs = 3e4;
317
324
  this.maxGuards = 1e3;
318
325
  this.accessOrder = [];
319
- if (autoCleanup) this.startAutoCleanup();
326
+ this.autoCleanupEnabled = autoCleanup;
327
+ }
328
+ /** Start cleanup only after the first guard is used. */
329
+ ensureAutoCleanup() {
330
+ if (this.autoCleanupEnabled && !this.cleanupInterval) this.startAutoCleanup();
320
331
  }
321
332
  /**
322
333
  * Start automatic cleanup of idle guard states
@@ -324,9 +335,17 @@ var ActionGuard = class {
324
335
  * @internal
325
336
  */
326
337
  startAutoCleanup() {
338
+ if (this.cleanupInterval) return;
327
339
  this.cleanupInterval = setInterval(() => {
328
340
  this.performCleanup();
329
341
  }, this.cleanupIntervalMs);
342
+ this.cleanupInterval.unref?.();
343
+ }
344
+ stopAutoCleanup() {
345
+ if (this.cleanupInterval) {
346
+ clearInterval(this.cleanupInterval);
347
+ this.cleanupInterval = void 0;
348
+ }
330
349
  }
331
350
  /**
332
351
  * ๐Ÿ”ง Optimized cleanup with early exit and batched operations
@@ -335,7 +354,10 @@ var ActionGuard = class {
335
354
  */
336
355
  performCleanup() {
337
356
  const guardCount = this.guards.size;
338
- if (guardCount === 0) return;
357
+ if (guardCount === 0) {
358
+ this.stopAutoCleanup();
359
+ return;
360
+ }
339
361
  const now = Date.now();
340
362
  const keysToDelete = [];
341
363
  if (guardCount <= 10) this.guards.forEach((state, key) => {
@@ -365,6 +387,7 @@ var ActionGuard = class {
365
387
  if (accessIndex !== -1) this.accessOrder.splice(accessIndex, 1);
366
388
  });
367
389
  if (typeof process !== "undefined" && process.env?.DEBUG_CONTEXT_ACTION) console.debug(`[ActionGuard] Cleaned up ${keysToDelete.length} idle guards`);
390
+ if (this.guards.size === 0) this.stopAutoCleanup();
368
391
  }
369
392
  }
370
393
  /**
@@ -423,6 +446,7 @@ var ActionGuard = class {
423
446
  * @internal
424
447
  */
425
448
  async debounce(actionKey, debounceMs) {
449
+ this.ensureAutoCleanup();
426
450
  this.evictIfNeeded();
427
451
  /** Get or create guard state for this action */
428
452
  let state = this.guards.get(actionKey);
@@ -483,6 +507,7 @@ var ActionGuard = class {
483
507
  * @internal
484
508
  */
485
509
  throttle(actionKey, throttleMs) {
510
+ this.ensureAutoCleanup();
486
511
  this.evictIfNeeded();
487
512
  /** Get or create guard state for this action */
488
513
  let state = this.guards.get(actionKey);
@@ -550,6 +575,9 @@ var ActionGuard = class {
550
575
  state.throttleTimer = void 0;
551
576
  }
552
577
  this.guards.delete(actionKey);
578
+ const accessIndex = this.accessOrder.indexOf(actionKey);
579
+ if (accessIndex !== -1) this.accessOrder.splice(accessIndex, 1);
580
+ if (this.guards.size === 0) this.stopAutoCleanup();
553
581
  }
554
582
  }
555
583
  /**
@@ -574,6 +602,8 @@ var ActionGuard = class {
574
602
  });
575
603
  /** Remove all guard states from memory */
576
604
  this.guards.clear();
605
+ this.accessOrder = [];
606
+ this.stopAutoCleanup();
577
607
  }
578
608
  /**
579
609
  * Get current guard state for debugging purposes
@@ -611,12 +641,7 @@ var ActionGuard = class {
611
641
  * @internal
612
642
  */
613
643
  destroy() {
614
- if (this.cleanupInterval) {
615
- clearInterval(this.cleanupInterval);
616
- this.cleanupInterval = void 0;
617
- }
618
644
  this.clearAll();
619
- this.accessOrder = [];
620
645
  }
621
646
  /**
622
647
  * ๐Ÿ†• Get statistics about active guards
@@ -669,27 +694,42 @@ var OperationQueue = class {
669
694
  * @returns Promise๋กœ ๋ž˜ํ•‘๋œ ์ž‘์—… ๊ฒฐ๊ณผ
670
695
  */
671
696
  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;
697
+ return this.enqueueWithHandle(operation, priority).promise;
698
+ }
699
+ /** Enqueue an operation and retain a handle for pre-start cancellation. */
700
+ enqueueWithHandle(operation, priority = 0) {
701
+ let queuedOperation;
702
+ return {
703
+ promise: new Promise((resolve, reject) => {
704
+ queuedOperation = {
705
+ id: `${this.name}-${++this.operationCounter}`,
706
+ operation,
707
+ resolve,
708
+ reject,
709
+ priority,
710
+ timestamp: Date.now()
711
+ };
712
+ let insertIndex = this.queue.length;
713
+ for (let i = 0; i < this.queue.length; i++) {
714
+ const item = this.queue[i];
715
+ if (item && (item.priority || 0) < priority) {
716
+ insertIndex = i;
717
+ break;
718
+ }
687
719
  }
720
+ this.queue.splice(insertIndex, 0, queuedOperation);
721
+ if (this.processingPromise) this.notifyNewOperation();
722
+ this.processQueue();
723
+ }),
724
+ cancel: (reason = /* @__PURE__ */ new Error("Queue operation cancelled")) => {
725
+ const index = this.queue.indexOf(queuedOperation);
726
+ if (index === -1) return false;
727
+ this.queue.splice(index, 1);
728
+ queuedOperation.reject(reason);
729
+ this.notifyNewOperation();
730
+ return true;
688
731
  }
689
- this.queue.splice(insertIndex, 0, queuedOperation);
690
- if (this.processingPromise) this.notifyNewOperation();
691
- this.processQueue();
692
- });
732
+ };
693
733
  }
694
734
  /**
695
735
  * ๐Ÿ†• ํ ์ฒ˜๋ฆฌ ๋ฉ”์ธ ๋กœ์ง - ๋™์‹œ์„ฑ ์ œ์–ด ๋ฐ ๋น„๋™๊ธฐ ์ง€์›
@@ -795,12 +835,14 @@ var OperationQueue = class {
795
835
  /**
796
836
  * ํ ๋น„์šฐ๊ธฐ (ํ…Œ์ŠคํŠธ์šฉ)
797
837
  */
798
- clear() {
838
+ clear(options = {}) {
839
+ const rejectPending = options.rejectPending ?? true;
840
+ const reason = options.reason ?? /* @__PURE__ */ new Error("Queue cleared");
799
841
  this.queue.forEach((operation) => {
800
- operation.reject(/* @__PURE__ */ new Error("Queue cleared"));
842
+ if (rejectPending) operation.reject(reason);
843
+ else operation.resolve(void 0);
801
844
  });
802
845
  this.queue = [];
803
- this.processingPromise = null;
804
846
  this.pendingResolvers.splice(0).forEach((resolve) => resolve());
805
847
  }
806
848
  /**
@@ -900,11 +942,44 @@ var ActionValidationError = class ActionValidationError extends Error {
900
942
  }
901
943
  };
902
944
  /**
945
+ * Raised when a dispatch exceeds its configured wall-clock timeout.
946
+ * The underlying handler receives an aborted controller signal and the internal
947
+ * queue keeps draining it safely, while the caller is released immediately with
948
+ * this error.
949
+ */
950
+ var ActionTimeoutError = class ActionTimeoutError extends Error {
951
+ constructor(action, timeout) {
952
+ super(`Action "${action}" timed out after ${timeout}ms`);
953
+ this.action = action;
954
+ this.timeout = timeout;
955
+ this.name = "ActionTimeoutError";
956
+ Object.setPrototypeOf(this, ActionTimeoutError.prototype);
957
+ }
958
+ };
959
+ /** Raised when work is submitted after an ActionRegister begins shutdown. */
960
+ var ActionRegisterDestroyedError = class ActionRegisterDestroyedError extends Error {
961
+ constructor(registerName, state) {
962
+ super(`ActionRegister "${registerName}" is ${state} and cannot accept new work`);
963
+ this.registerName = registerName;
964
+ this.state = state;
965
+ this.name = "ActionRegisterDestroyedError";
966
+ Object.setPrototypeOf(this, ActionRegisterDestroyedError.prototype);
967
+ }
968
+ };
969
+ /**
903
970
  * ActionValidationError ํƒ€์ž… ๊ฐ€๋“œ
904
971
  */
905
972
  function isActionValidationError(error) {
906
973
  return error instanceof ActionValidationError;
907
974
  }
975
+ /** ActionTimeoutError type guard. */
976
+ function isActionTimeoutError(error) {
977
+ return error instanceof ActionTimeoutError;
978
+ }
979
+ /** ActionRegisterDestroyedError type guard. */
980
+ function isActionRegisterDestroyedError(error) {
981
+ return error instanceof ActionRegisterDestroyedError;
982
+ }
908
983
 
909
984
  //#endregion
910
985
  //#region src/ActionRegister.ts
@@ -923,35 +998,6 @@ function isActionValidationError(error) {
923
998
  *
924
999
  * @public
925
1000
  */
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
1001
  var ActionRegister = class {
956
1002
  constructor(config = {}) {
957
1003
  this.pipelines = /* @__PURE__ */ new Map();
@@ -959,10 +1005,13 @@ var ActionRegister = class {
959
1005
  this.actionExecutionModes = /* @__PURE__ */ new Map();
960
1006
  this.unregisterFunctions = /* @__PURE__ */ new Map();
961
1007
  this.lastRegisteredTimestamps = /* @__PURE__ */ new Map();
962
- this.filterCacheDisabled = true;
963
1008
  this.handlerIdCounter = 0;
964
1009
  this.controllerPool = [];
965
- this.maxControllerPoolSize = 10;
1010
+ this.lifecycleState = "active";
1011
+ this.lifecycleController = new AbortController();
1012
+ this.activeDispatches = /* @__PURE__ */ new Set();
1013
+ this.activeHandlerPromises = /* @__PURE__ */ new Set();
1014
+ this.dispatchConstructionDepth = 0;
966
1015
  this.name = config.name || "ActionRegister";
967
1016
  this.registryConfig = config.registry;
968
1017
  this.maxHandlersPerAction = config.registry?.maxHandlersPerAction ?? 1e3;
@@ -979,10 +1028,10 @@ var ActionRegister = class {
979
1028
  }
980
1029
  /**
981
1030
  * ๐Ÿ†• Action-based dispatcher
982
- *
1031
+ *
983
1032
  * Provides function-based access to actions for more convenient dispatching.
984
1033
  * Each action becomes a callable function that can be invoked directly.
985
- *
1034
+ *
986
1035
  * @example
987
1036
  * ```typescript
988
1037
  * interface MyActions extends ActionPayloadMap {
@@ -995,19 +1044,17 @@ var ActionRegister = class {
995
1044
  * // Function-based dispatching
996
1045
  * await registry.actions.userLogin({ userId: '123', email: 'test@example.com' });
997
1046
  * await registry.actions.resetApp();
1047
+ * await registry.actions.resetApp(undefined, { debounce: 100 });
998
1048
  * ```
999
1049
  *
1000
1050
  * @public
1001
1051
  */
1002
1052
  get actions() {
1003
1053
  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
- }
1054
+ const actionKey = prop;
1055
+ if (typeof prop === "string" && this.pipelines.has(actionKey)) return (payload, options) => {
1056
+ return this.dispatch(actionKey, payload, options);
1057
+ };
1011
1058
  } });
1012
1059
  return this._actionsProxy;
1013
1060
  }
@@ -1024,6 +1071,10 @@ var ActionRegister = class {
1024
1071
  *
1025
1072
  * // Actions without payload
1026
1073
  * const result = await registry.actionsWithResult.userLogout();
1074
+ * const debouncedResult = await registry.actionsWithResult.userLogout(
1075
+ * undefined,
1076
+ * { debounce: 100 }
1077
+ * );
1027
1078
  *
1028
1079
  * // With options
1029
1080
  * const result = await registry.actionsWithResult.processData(
@@ -1036,13 +1087,10 @@ var ActionRegister = class {
1036
1087
  */
1037
1088
  get actionsWithResult() {
1038
1089
  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
- }
1090
+ const actionKey = prop;
1091
+ if (typeof prop === "string" && this.pipelines.has(actionKey)) return (payload, options) => {
1092
+ return this.dispatchWithResult(actionKey, payload, options);
1093
+ };
1046
1094
  } });
1047
1095
  return this._actionsWithResultProxy;
1048
1096
  }
@@ -1062,6 +1110,7 @@ var ActionRegister = class {
1062
1110
  * @public
1063
1111
  */
1064
1112
  register(action, handler, config = {}) {
1113
+ this.assertAcceptingWork();
1065
1114
  const handlerId = config.id || this.generateHandlerId(action);
1066
1115
  return this._performRegistrationSync(action, handler, config, handlerId);
1067
1116
  }
@@ -1074,6 +1123,15 @@ var ActionRegister = class {
1074
1123
  console[level](`๐ŸŽฏ [${timestamp}] [${this.name}] ${message}`, data || "");
1075
1124
  }
1076
1125
  }
1126
+ assertAcceptingWork() {
1127
+ if (this.lifecycleState !== "active") throw new ActionRegisterDestroyedError(this.name, this.lifecycleState);
1128
+ }
1129
+ rejectedLifecyclePromise() {
1130
+ const error = new ActionRegisterDestroyedError(this.name, this.lifecycleState === "active" ? "destroyed" : this.lifecycleState);
1131
+ const rejected = Promise.reject(error);
1132
+ rejected.catch(() => {});
1133
+ return rejected;
1134
+ }
1077
1135
  /**
1078
1136
  * ๐Ÿ”ง Generate unique handler ID using optimized counter-based approach
1079
1137
  */
@@ -1164,7 +1222,7 @@ var ActionRegister = class {
1164
1222
  const existing = pipeline[existingIndex];
1165
1223
  const existingUnregister = this.unregisterFunctions.get(handlerId);
1166
1224
  if (registration.config.replaceExisting) {
1167
- if (existing && existing.config.cleanup && typeof existing.config.cleanup === "function") try {
1225
+ if (existing?.config.cleanup && typeof existing.config.cleanup === "function") try {
1168
1226
  existing.config.cleanup();
1169
1227
  } catch (cleanupError) {
1170
1228
  this.log(`Cleanup error for replaced handler: ${String(action)}`, cleanupError, "warn");
@@ -1212,16 +1270,245 @@ var ActionRegister = class {
1212
1270
  });
1213
1271
  return unregister;
1214
1272
  }
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);
1273
+ dispatch(action, payload, options) {
1274
+ if (this.lifecycleState !== "active") return this.rejectedLifecyclePromise();
1275
+ const timeoutScope = this.createTimeoutScope(action, options);
1276
+ const dispatchHandlerPromises = /* @__PURE__ */ new Set();
1277
+ const attemptState = { count: 0 };
1278
+ const operation = async () => {
1279
+ if (!timeoutScope.options?.signal?.aborted) this.validatePayload(action, payload);
1280
+ return this.executeWithRetry(async () => {
1281
+ const executedHandlers = [];
1282
+ try {
1283
+ return await this._performDispatch(action, payload, timeoutScope.options, attemptState.count > 1, executedHandlers, dispatchHandlerPromises);
1284
+ } finally {
1285
+ this.cleanupOneTimeHandlers(action, executedHandlers, dispatchHandlerPromises);
1286
+ }
1287
+ }, timeoutScope.options, attemptState, void 0, () => this.getHandlerCount(action) > 0);
1288
+ };
1289
+ const hasTimingGuard = options?.debounce !== void 0 || options?.throttle !== void 0 || this.pipelines.get(action)?.some((handler) => handler.config.debounce !== void 0 || handler.config.throttle !== void 0) === true;
1290
+ let dispatchPromise;
1291
+ this.dispatchConstructionDepth += 1;
1292
+ try {
1293
+ if (timeoutScope.options?.immediate || hasTimingGuard || !this.dispatchQueue) dispatchPromise = operation();
1294
+ else {
1295
+ const queued = this.dispatchQueue.enqueueWithHandle(operation, timeoutScope.options?.queuePriority ?? 0);
1296
+ timeoutScope.onTimeout((error) => queued.cancel(error));
1297
+ dispatchPromise = queued.promise;
1298
+ }
1299
+ this.trackDispatchPromise(dispatchPromise);
1300
+ } finally {
1301
+ this.dispatchConstructionDepth -= 1;
1302
+ }
1303
+ const observedPromise = this.raceWithTimeout(dispatchPromise, timeoutScope, dispatchHandlerPromises).catch((error) => {
1304
+ this.invokeErrorHandler(error, action, payload, options, attemptState.count);
1305
+ throw error;
1306
+ });
1307
+ observedPromise.catch(() => {});
1308
+ return observedPromise;
1309
+ }
1310
+ /** Execute a dispatch operation with an optional whole-action retry policy. */
1311
+ async executeWithRetry(operation, options, attemptState, shouldRetryResult, canRetry = () => true) {
1312
+ const configuredAttempts = options?.retryOnError?.maxAttempts ?? 1;
1313
+ const maxAttempts = Number.isFinite(configuredAttempts) ? Math.max(1, Math.floor(configuredAttempts)) : 1;
1314
+ const retryDelay = Math.max(0, options?.retryOnError?.delay ?? 0);
1315
+ while (attemptState.count < maxAttempts) {
1316
+ attemptState.count += 1;
1317
+ try {
1318
+ const result = await operation();
1319
+ if (!(shouldRetryResult?.(result) ?? false) || attemptState.count >= maxAttempts || options?.signal?.aborted || !canRetry()) return result;
1320
+ } catch (error) {
1321
+ if (error instanceof ActionValidationError || attemptState.count >= maxAttempts || options?.signal?.aborted || !canRetry()) throw error;
1322
+ }
1323
+ await this.waitForRetry(retryDelay, options?.signal);
1324
+ }
1325
+ return operation();
1326
+ }
1327
+ trackDispatchPromise(promise) {
1328
+ this.activeDispatches.add(promise);
1329
+ const remove = () => this.activeDispatches.delete(promise);
1330
+ promise.then(remove, remove);
1331
+ return promise;
1332
+ }
1333
+ trackHandlerPromise(promise, dispatchHandlerPromises) {
1334
+ this.activeHandlerPromises.add(promise);
1335
+ dispatchHandlerPromises.add(promise);
1336
+ const remove = () => {
1337
+ this.activeHandlerPromises.delete(promise);
1338
+ dispatchHandlerPromises.delete(promise);
1339
+ };
1340
+ promise.then(remove, remove);
1341
+ return promise;
1342
+ }
1343
+ trackGlobalHandlerPromise(promise) {
1344
+ this.activeHandlerPromises.add(promise);
1345
+ const remove = () => this.activeHandlerPromises.delete(promise);
1346
+ promise.then(remove, remove);
1347
+ return promise;
1348
+ }
1349
+ /** Abort-aware retry delay so cancellation does not wait for the full backoff. */
1350
+ waitForRetry(delay, signal) {
1351
+ if (delay <= 0 || signal?.aborted) return Promise.resolve();
1352
+ return new Promise((resolve) => {
1353
+ const timer = setTimeout(finish, delay);
1354
+ const abort = () => finish();
1355
+ function finish() {
1356
+ clearTimeout(timer);
1357
+ signal?.removeEventListener("abort", abort);
1358
+ resolve();
1359
+ }
1360
+ signal?.addEventListener("abort", abort, { once: true });
1219
1361
  });
1220
1362
  }
1363
+ /** Build a wall-clock timeout that also participates in pipeline cancellation. */
1364
+ createTimeoutScope(action, options) {
1365
+ const hasTimeout = options?.timeout !== void 0 && Number.isFinite(options.timeout);
1366
+ const timeout = hasTimeout ? Math.max(0, options.timeout) : void 0;
1367
+ const timeoutController = hasTimeout ? new AbortController() : void 0;
1368
+ const signalCleanups = [];
1369
+ const timeoutCallbacks = /* @__PURE__ */ new Set();
1370
+ const signals = [
1371
+ this.lifecycleController.signal,
1372
+ options?.signal,
1373
+ timeoutController?.signal
1374
+ ].filter((candidate) => Boolean(candidate));
1375
+ let signal = signals[0];
1376
+ if (signals.length > 1) if (typeof AbortSignal.any === "function") signal = AbortSignal.any(signals);
1377
+ else {
1378
+ const mergedController = new AbortController();
1379
+ const forwardAbort = (source) => {
1380
+ if (!mergedController.signal.aborted) mergedController.abort(source.reason);
1381
+ };
1382
+ for (const source of signals) {
1383
+ if (source.aborted) {
1384
+ forwardAbort(source);
1385
+ break;
1386
+ }
1387
+ const listener = () => forwardAbort(source);
1388
+ source.addEventListener("abort", listener, { once: true });
1389
+ signalCleanups.push(() => source.removeEventListener("abort", listener));
1390
+ }
1391
+ signal = mergedController.signal;
1392
+ }
1393
+ let timer;
1394
+ const timeoutPromise = timeoutController && timeout !== void 0 ? new Promise((_, reject) => {
1395
+ timer = setTimeout(() => {
1396
+ const error = new ActionTimeoutError(String(action), timeout);
1397
+ timeoutController.abort(error);
1398
+ timeoutCallbacks.forEach((callback) => callback(error));
1399
+ reject(error);
1400
+ }, timeout);
1401
+ }) : void 0;
1402
+ return {
1403
+ options: {
1404
+ ...options,
1405
+ signal
1406
+ },
1407
+ timeoutPromise,
1408
+ onTimeout: (callback) => timeoutCallbacks.add(callback),
1409
+ cleanup: () => {
1410
+ if (timer !== void 0) clearTimeout(timer);
1411
+ timeoutCallbacks.clear();
1412
+ },
1413
+ cleanupSignals: () => signalCleanups.forEach((cleanup) => cleanup())
1414
+ };
1415
+ }
1416
+ /** Expose timeout failure while allowing the queued operation to drain safely. */
1417
+ raceWithTimeout(operation, scope, dispatchHandlerPromises) {
1418
+ const exposed = scope.timeoutPromise ? Promise.race([operation, scope.timeoutPromise]) : operation;
1419
+ const cleanupAfterStartedHandlers = () => {
1420
+ scope.cleanup();
1421
+ this.cleanupSignalsAfterStartedHandlers(scope.cleanupSignals, dispatchHandlerPromises);
1422
+ };
1423
+ exposed.then(cleanupAfterStartedHandlers, cleanupAfterStartedHandlers);
1424
+ return exposed;
1425
+ }
1426
+ cleanupSignalsAfterStartedHandlers(cleanup, dispatchHandlerPromises) {
1427
+ const handlersStillRunning = [...dispatchHandlerPromises];
1428
+ if (handlersStillRunning.length === 0) {
1429
+ cleanup();
1430
+ return;
1431
+ }
1432
+ Promise.allSettled(handlersStillRunning).then(cleanup);
1433
+ }
1434
+ /** Invoke the configured error handler without allowing it to replace the dispatch error. */
1435
+ invokeErrorHandler(error, action, payload, options, attempts) {
1436
+ const errorHandler = this.registryConfig?.errorHandler;
1437
+ if (!errorHandler) return;
1438
+ const normalizedError = error instanceof Error ? error : new Error(String(error));
1439
+ try {
1440
+ const handlerResult = errorHandler(normalizedError, {
1441
+ action: String(action),
1442
+ payload,
1443
+ options,
1444
+ attempts,
1445
+ phase: normalizedError instanceof ActionTimeoutError ? "timeout" : normalizedError instanceof ActionValidationError ? "validation" : "execution"
1446
+ });
1447
+ if (handlerResult && typeof handlerResult.then === "function") Promise.resolve(handlerResult).catch((handlerError) => {
1448
+ this.log("Global async error handler failed", handlerError, "warn");
1449
+ });
1450
+ } catch (handlerError) {
1451
+ this.log("Global error handler failed", handlerError, "warn");
1452
+ }
1453
+ }
1454
+ /**
1455
+ * Validate an action payload against the configured schema.
1456
+ * Shared by all dispatch paths so result collection cannot bypass validation.
1457
+ */
1458
+ validatePayload(action, payload) {
1459
+ if (!this.registryConfig?.schema || this.registryConfig.validateOnDispatch === false) return;
1460
+ const actionName = String(action);
1461
+ const actionSchema = this.registryConfig.schema[actionName];
1462
+ if (!actionSchema) return;
1463
+ let result;
1464
+ try {
1465
+ result = actionSchema.safeParse(payload);
1466
+ } catch (error) {
1467
+ throw new ActionValidationError(actionName, error);
1468
+ }
1469
+ if (result.success) return {
1470
+ passed: true,
1471
+ errors: []
1472
+ };
1473
+ const mode = this.registryConfig.validationMode ?? "strict";
1474
+ if (mode === "strict") throw new ActionValidationError(actionName, result.error);
1475
+ if (mode === "warn") {
1476
+ console.warn(`Action "${actionName}" payload validation failed:`, result.error.message);
1477
+ this.log(`Validation warning for action '${actionName}'`, { issues: result.error.issues }, "warn");
1478
+ }
1479
+ return {
1480
+ passed: false,
1481
+ errors: result.error.issues.map((issue) => issue.message)
1482
+ };
1483
+ }
1484
+ createAbortedExecutionResult(startTime, handlersSkipped = 0, validation) {
1485
+ const endTime = Date.now();
1486
+ return {
1487
+ success: false,
1488
+ aborted: true,
1489
+ abortReason: "Action dispatch aborted by signal",
1490
+ terminated: false,
1491
+ validation,
1492
+ result: void 0,
1493
+ successResults: [],
1494
+ results: [],
1495
+ failedResults: [],
1496
+ execution: {
1497
+ duration: endTime - startTime,
1498
+ handlersExecuted: 0,
1499
+ handlersSkipped,
1500
+ handlersFailed: 0,
1501
+ startTime,
1502
+ endTime
1503
+ },
1504
+ handlers: [],
1505
+ errors: []
1506
+ };
1507
+ }
1221
1508
  /**
1222
1509
  * ๐Ÿ†• ์‹ค์ œ ๋””์ŠคํŒจ์น˜ ์ž‘์—… ์ˆ˜ํ–‰ (ํ์—์„œ ํ˜ธ์ถœ๋จ)
1223
1510
  */
1224
- async _performDispatch(action, payload, options) {
1511
+ async _performDispatch(action, payload, options, skipGuards, executedHandlers, dispatchHandlerPromises) {
1225
1512
  this.log(`Starting dispatch for action '${String(action)}'`, {
1226
1513
  hasPayload: payload !== void 0,
1227
1514
  payloadType: payload?.constructor?.name || typeof payload,
@@ -1229,24 +1516,11 @@ var ActionRegister = class {
1229
1516
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
1230
1517
  });
1231
1518
  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
1519
  const [effectiveSignal, autoAbortController, cleanup] = this.createAbortSignal(options);
1247
1520
  if (options?.autoAbort?.onControllerCreated && autoAbortController) options.autoAbort.onControllerCreated(autoAbortController);
1248
1521
  if (effectiveSignal?.aborted) {
1249
1522
  this.log(`Dispatch aborted before execution for '${String(action)}'`);
1523
+ cleanup();
1250
1524
  return;
1251
1525
  }
1252
1526
  const pipeline = this.pipelines.get(action);
@@ -1262,6 +1536,7 @@ var ActionRegister = class {
1262
1536
  console.warn("๐Ÿ’ก Tip: Register a handler using registry.register() before dispatching this action.");
1263
1537
  console.warn("๐Ÿ“‹ Available actions:", Array.from(this.pipelines.keys()));
1264
1538
  this.log(`No handlers found for action '${String(action)}', dispatch cancelled`, {}, "warn");
1539
+ cleanup();
1265
1540
  return;
1266
1541
  }
1267
1542
  const filteredHandlers = options?.filter ? this.filterHandlers(pipeline, options.filter) : pipeline;
@@ -1282,17 +1557,32 @@ var ActionRegister = class {
1282
1557
  break;
1283
1558
  }
1284
1559
  }
1285
- if (debounceMs !== void 0) {
1286
- if (!await this.actionGuard.debounce(actionKey, debounceMs)) return;
1560
+ if (!skipGuards && debounceMs !== void 0) {
1561
+ if (!await this.actionGuard.debounce(actionKey, debounceMs)) {
1562
+ cleanup();
1563
+ return;
1564
+ }
1287
1565
  }
1288
- if (throttleMs !== void 0) {
1289
- if (!this.actionGuard.throttle(actionKey, throttleMs)) return;
1566
+ if (!skipGuards && throttleMs !== void 0) {
1567
+ if (!this.actionGuard.throttle(actionKey, throttleMs)) {
1568
+ cleanup();
1569
+ return;
1570
+ }
1571
+ }
1572
+ if (effectiveSignal?.aborted) {
1573
+ this.log(`Dispatch aborted during guard processing for '${String(action)}'`);
1574
+ cleanup();
1575
+ return;
1290
1576
  }
1291
1577
  const currentExecutionMode = options?.executionMode || this.actionExecutionModes.get(action) || this.executionMode;
1292
1578
  const context = {
1293
1579
  action: String(action),
1294
1580
  payload,
1295
- handlers: filteredHandlers,
1581
+ handlers: [...filteredHandlers],
1582
+ executedHandlers: [],
1583
+ deferOnceCleanup: true,
1584
+ signal: effectiveSignal ?? this.lifecycleController.signal,
1585
+ trackHandlerPromise: (promise) => this.trackHandlerPromise(promise, dispatchHandlerPromises),
1296
1586
  aborted: false,
1297
1587
  abortReason: void 0,
1298
1588
  currentIndex: 0,
@@ -1306,17 +1596,19 @@ var ActionRegister = class {
1306
1596
  };
1307
1597
  const abortHandler = effectiveSignal ? () => {
1308
1598
  context.aborted = true;
1309
- context.abortReason = "Action dispatch aborted by signal";
1599
+ context.abortReason = typeof effectiveSignal.reason === "string" ? effectiveSignal.reason : "Action dispatch aborted by signal";
1310
1600
  } : void 0;
1311
- if (effectiveSignal && abortHandler) effectiveSignal.addEventListener("abort", abortHandler);
1601
+ if (effectiveSignal && abortHandler) effectiveSignal.addEventListener("abort", abortHandler, { once: true });
1312
1602
  try {
1313
- await this.executePipeline(context, autoAbortController, options?.autoAbort);
1603
+ await this.executePipeline(context, dispatchHandlerPromises, autoAbortController, options?.autoAbort);
1314
1604
  this.log(`Pipeline execution succeeded for ${String(action)}`);
1315
1605
  } catch (error) {
1316
1606
  this.log(`Pipeline execution failed for ${String(action)}`, error, "error");
1317
1607
  throw error;
1318
1608
  } finally {
1319
- cleanup();
1609
+ executedHandlers?.push(...context.executedHandlers ?? []);
1610
+ if (effectiveSignal && abortHandler) effectiveSignal.removeEventListener("abort", abortHandler);
1611
+ this.cleanupSignalsAfterStartedHandlers(cleanup, dispatchHandlerPromises);
1320
1612
  }
1321
1613
  }
1322
1614
  /**
@@ -1332,41 +1624,70 @@ var ActionRegister = class {
1332
1624
  *
1333
1625
  * @public
1334
1626
  */
1335
- async dispatchWithResult(action, payload, options) {
1627
+ dispatchWithResult(action, payload, options) {
1628
+ if (this.lifecycleState !== "active") return this.rejectedLifecyclePromise();
1629
+ const timeoutScope = this.createTimeoutScope(action, options);
1630
+ const dispatchHandlerPromises = /* @__PURE__ */ new Set();
1631
+ const attemptState = { count: 0 };
1632
+ let validation;
1633
+ const operation = async () => {
1634
+ if (!timeoutScope.options?.signal?.aborted) validation = this.validatePayload(action, payload);
1635
+ return this.executeWithRetry(async () => {
1636
+ const executedHandlers = [];
1637
+ try {
1638
+ return await this._performDispatchWithResult(action, payload, timeoutScope.options, validation, attemptState.count > 1, executedHandlers, dispatchHandlerPromises);
1639
+ } finally {
1640
+ this.cleanupOneTimeHandlers(action, executedHandlers, dispatchHandlerPromises);
1641
+ }
1642
+ }, timeoutScope.options, attemptState, (result) => !result.success && !result.aborted && this.getHandlerCount(action) > 0, () => this.getHandlerCount(action) > 0);
1643
+ };
1644
+ const shouldQueue = !timeoutScope.options?.immediate && Boolean(this.dispatchQueue) && timeoutScope.options?.queuePriority !== void 0;
1645
+ let dispatchPromise;
1646
+ this.dispatchConstructionDepth += 1;
1647
+ try {
1648
+ if (shouldQueue) {
1649
+ const queued = this.dispatchQueue.enqueueWithHandle(operation, timeoutScope.options.queuePriority);
1650
+ timeoutScope.onTimeout((error) => queued.cancel(error));
1651
+ dispatchPromise = queued.promise;
1652
+ } else dispatchPromise = operation();
1653
+ this.trackDispatchPromise(dispatchPromise);
1654
+ } finally {
1655
+ this.dispatchConstructionDepth -= 1;
1656
+ }
1657
+ const observedPromise = this.raceWithTimeout(dispatchPromise, timeoutScope, dispatchHandlerPromises).then((result) => {
1658
+ if (!result.success && !result.aborted) {
1659
+ const terminalError = result.errors[result.errors.length - 1]?.error ?? /* @__PURE__ */ new Error(`Action "${String(action)}" failed`);
1660
+ this.invokeErrorHandler(terminalError, action, payload, options, attemptState.count);
1661
+ }
1662
+ return result;
1663
+ }, (error) => {
1664
+ this.invokeErrorHandler(error, action, payload, options, attemptState.count);
1665
+ throw error;
1666
+ });
1667
+ observedPromise.catch(() => {});
1668
+ return observedPromise;
1669
+ }
1670
+ async _performDispatchWithResult(action, payload, options, validation, skipGuards, executedHandlers, dispatchHandlerPromises) {
1336
1671
  const _startTime = Date.now();
1337
1672
  const [effectiveSignal, autoAbortController, cleanup] = this.createAbortSignal(options);
1338
1673
  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
- };
1674
+ if (effectiveSignal?.aborted) {
1675
+ cleanup();
1676
+ return this.createAbortedExecutionResult(_startTime, 0, validation);
1677
+ }
1359
1678
  const pipeline = this.pipelines.get(action);
1360
1679
  if (!pipeline || pipeline.length === 0) {
1361
1680
  const warningMessage = `โš ๏ธ Action '${String(action)}' has no registered handlers. This action will be ignored.`;
1362
1681
  console.warn(warningMessage);
1363
1682
  console.warn("๐Ÿ’ก Tip: Register a handler using registry.register() before dispatching this action.");
1364
1683
  console.warn("๐Ÿ“‹ Available actions:", Array.from(this.pipelines.keys()));
1684
+ cleanup();
1365
1685
  return {
1366
1686
  success: true,
1367
1687
  aborted: false,
1368
1688
  abortReason: void 0,
1369
1689
  terminated: false,
1690
+ validation,
1370
1691
  result: void 0,
1371
1692
  successResults: [],
1372
1693
  results: [],
@@ -1385,13 +1706,27 @@ var ActionRegister = class {
1385
1706
  }
1386
1707
  const filteredHandlers = options?.filter ? this.filterHandlers(pipeline, options.filter) : pipeline;
1387
1708
  const actionKey = String(action);
1388
- const guardResult = await this.applyActionGuardControlsWithResult(actionKey, filteredHandlers, options, _startTime, pipeline.length);
1389
- if (guardResult) return guardResult;
1709
+ const guardResult = skipGuards ? null : await this.applyActionGuardControlsWithResult(actionKey, filteredHandlers, options, _startTime, pipeline.length);
1710
+ if (effectiveSignal?.aborted) {
1711
+ cleanup();
1712
+ return this.createAbortedExecutionResult(_startTime, pipeline.length, validation);
1713
+ }
1714
+ if (guardResult) {
1715
+ cleanup();
1716
+ return {
1717
+ ...guardResult,
1718
+ validation
1719
+ };
1720
+ }
1390
1721
  const currentExecutionMode = options?.executionMode || this.actionExecutionModes.get(action) || this.executionMode;
1391
1722
  const context = {
1392
1723
  action: String(action),
1393
1724
  payload,
1394
- handlers: filteredHandlers,
1725
+ handlers: [...filteredHandlers],
1726
+ executedHandlers: [],
1727
+ deferOnceCleanup: true,
1728
+ signal: effectiveSignal ?? this.lifecycleController.signal,
1729
+ trackHandlerPromise: (promise) => this.trackHandlerPromise(promise, dispatchHandlerPromises),
1395
1730
  aborted: false,
1396
1731
  abortReason: void 0,
1397
1732
  currentIndex: 0,
@@ -1417,12 +1752,12 @@ var ActionRegister = class {
1417
1752
  });
1418
1753
  const abortHandler = effectiveSignal ? () => {
1419
1754
  context.aborted = true;
1420
- context.abortReason = "Action dispatch aborted by signal";
1755
+ context.abortReason = typeof effectiveSignal.reason === "string" ? effectiveSignal.reason : "Action dispatch aborted by signal";
1421
1756
  } : void 0;
1422
- if (effectiveSignal && abortHandler) effectiveSignal.addEventListener("abort", abortHandler);
1757
+ if (effectiveSignal && abortHandler) effectiveSignal.addEventListener("abort", abortHandler, { once: true });
1423
1758
  let errors = [];
1424
1759
  try {
1425
- await this.executePipeline(context, autoAbortController, options?.autoAbort);
1760
+ await this.executePipeline(context, dispatchHandlerPromises, autoAbortController, options?.autoAbort);
1426
1761
  errors = context.collectedErrors || [];
1427
1762
  const executedCount = Math.min(context.currentIndex + (context.aborted ? 0 : 1), filteredHandlers.length);
1428
1763
  for (let i = 0; i < executedCount; i++) {
@@ -1448,7 +1783,9 @@ var ActionRegister = class {
1448
1783
  if (handlerResult) handlerResult.executed = true;
1449
1784
  }
1450
1785
  } finally {
1451
- cleanup();
1786
+ executedHandlers.push(...context.executedHandlers ?? []);
1787
+ if (effectiveSignal && abortHandler) effectiveSignal.removeEventListener("abort", abortHandler);
1788
+ this.cleanupSignalsAfterStartedHandlers(cleanup, dispatchHandlerPromises);
1452
1789
  }
1453
1790
  const endTime = Date.now();
1454
1791
  const processedResult = this.processResults(context, options?.result);
@@ -1458,11 +1795,12 @@ var ActionRegister = class {
1458
1795
  error: err.error,
1459
1796
  expectedType: typeof processedResult
1460
1797
  }));
1461
- const executionResult = {
1798
+ return {
1462
1799
  success: !executionError && !context.aborted,
1463
1800
  aborted: context.aborted,
1464
1801
  abortReason: context.abortReason,
1465
1802
  terminated: context.terminated,
1803
+ validation,
1466
1804
  result: processedResult,
1467
1805
  successResults,
1468
1806
  results: context.results,
@@ -1483,9 +1821,6 @@ var ActionRegister = class {
1483
1821
  severity: "non-blocking"
1484
1822
  }))
1485
1823
  };
1486
- /** Clean up one-time handlers after execution */
1487
- this.cleanupOneTimeHandlers(action, context.handlers);
1488
- return executionResult;
1489
1824
  }
1490
1825
  /**
1491
1826
  * ๐Ÿ”ง Unified method for dispatchWithResult that returns ExecutionResult on guard rejection
@@ -1554,26 +1889,12 @@ var ActionRegister = class {
1554
1889
  return null;
1555
1890
  }
1556
1891
  /**
1557
- * ๐Ÿ”ง Generate optimized cache key for filter options
1558
- */
1559
- generateFilterCacheKey(filterOptions) {
1560
- if (!filterOptions) return "no-filter";
1561
- const parts = [];
1562
- if (filterOptions.handlerIds?.length) parts.push(`h:${filterOptions.handlerIds.slice().sort().join(",")}`);
1563
- if (filterOptions.excludeHandlerIds?.length) parts.push(`e:${filterOptions.excludeHandlerIds.slice().sort().join(",")}`);
1564
- if (filterOptions.priority) {
1565
- const { min, max } = filterOptions.priority;
1566
- if (min !== void 0 || max !== void 0) parts.push(`p:${min ?? "*"}-${max ?? "*"}`);
1567
- }
1568
- if (filterOptions.custom) return "custom-" + Date.now() + Math.random();
1569
- return parts.length > 0 ? parts.join("|") : "no-filter";
1570
- }
1571
- /**
1572
1892
  * ๐Ÿ”ง Create or reuse PipelineController from pool for better performance
1573
1893
  */
1574
1894
  getControllerFromPool(context, autoAbortController, autoAbortOptions) {
1575
1895
  let controller = this.controllerPool.pop();
1576
1896
  if (!controller) controller = {};
1897
+ controller.signal = context.signal ?? this.lifecycleController.signal;
1577
1898
  controller.abort = (reason) => {
1578
1899
  context.aborted = true;
1579
1900
  context.abortReason = reason;
@@ -1607,12 +1928,6 @@ var ActionRegister = class {
1607
1928
  };
1608
1929
  return controller;
1609
1930
  }
1610
- /**
1611
- * ๐Ÿ”ง Return controller to pool for reuse
1612
- */
1613
- returnControllerToPool(controller) {
1614
- if (this.controllerPool.length < this.maxControllerPoolSize) this.controllerPool.push(controller);
1615
- }
1616
1931
  filterHandlers(handlers, filterOptions) {
1617
1932
  if (!filterOptions) return handlers;
1618
1933
  const handlerIdSet = filterOptions.handlerIds ? new Set(filterOptions.handlerIds) : null;
@@ -1620,7 +1935,7 @@ var ActionRegister = class {
1620
1935
  return handlers.filter((registration) => {
1621
1936
  const config = registration.config;
1622
1937
  if (handlerIdSet && !handlerIdSet.has(config.id)) return false;
1623
- if (excludeIdSet && excludeIdSet.has(config.id)) return false;
1938
+ if (excludeIdSet?.has(config.id)) return false;
1624
1939
  if (filterOptions.priority) {
1625
1940
  const priority = config.priority;
1626
1941
  if (filterOptions.priority.min !== void 0 && priority < filterOptions.priority.min) return false;
@@ -1652,7 +1967,7 @@ var ActionRegister = class {
1652
1967
  return limitedResults[limitedResults.length - 1];
1653
1968
  }
1654
1969
  }
1655
- async executePipeline(context, autoAbortController, autoAbortOptions) {
1970
+ async executePipeline(context, dispatchHandlerPromises, autoAbortController, autoAbortOptions) {
1656
1971
  const createController = (_registration, _index) => {
1657
1972
  return this.getControllerFromPool(context, autoAbortController, autoAbortOptions);
1658
1973
  };
@@ -1668,28 +1983,27 @@ var ActionRegister = class {
1668
1983
  break;
1669
1984
  default: throw new Error(`Unknown execution mode: ${context.executionMode}`);
1670
1985
  }
1671
- this.cleanupOneTimeHandlers(context.action, context.handlers);
1986
+ if (!context.deferOnceCleanup) this.cleanupOneTimeHandlers(context.action, context.executedHandlers ?? [], dispatchHandlerPromises);
1672
1987
  }
1673
- cleanupOneTimeHandlers(action, executedHandlers) {
1674
- const pipeline = this.pipelines.get(action);
1675
- if (!pipeline) return;
1988
+ cleanupOneTimeHandlers(action, executedHandlers, dispatchHandlerPromises) {
1676
1989
  const oneTimeHandlers = executedHandlers.filter((reg) => reg.config.once);
1677
1990
  if (oneTimeHandlers.length === 0) return;
1991
+ const handlersStillRunning = [...dispatchHandlerPromises];
1992
+ const shouldDeferCleanup = handlersStillRunning.length > 0;
1678
1993
  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)}`, {
1994
+ if (this.removeRegistration(action, registration, !shouldDeferCleanup)) {
1995
+ if (shouldDeferCleanup && typeof registration.config.cleanup === "function") {
1996
+ const cleanupPromise = Promise.allSettled(handlersStillRunning).then(() => {
1997
+ this.runRegistrationCleanup(action, registration);
1998
+ });
1999
+ this.trackGlobalHandlerPromise(cleanupPromise).catch(() => {});
2000
+ }
2001
+ this.log(`One-time handler removed: ${String(action)}`, {
1683
2002
  handlerId: registration.id,
1684
- remainingHandlers: pipeline.length,
1685
- registry: this.name
2003
+ remainingHandlers: this.getHandlerCount(action)
1686
2004
  });
1687
2005
  }
1688
2006
  });
1689
- if (pipeline.length === 0) {
1690
- this.pipelines.delete(action);
1691
- this.lastRegisteredTimestamps.delete(action);
1692
- }
1693
2007
  }
1694
2008
  /**
1695
2009
  * Get the number of registered handlers for an action
@@ -1742,8 +2056,13 @@ var ActionRegister = class {
1742
2056
  * @public
1743
2057
  */
1744
2058
  clearAction(action) {
2059
+ const pipeline = this.pipelines.get(action);
2060
+ if (pipeline) [...pipeline].forEach((registration) => {
2061
+ this.removeRegistration(action, registration);
2062
+ });
1745
2063
  this.pipelines.delete(action);
1746
2064
  this.lastRegisteredTimestamps.delete(action);
2065
+ this.actionGuard.clearGuards(String(action));
1747
2066
  }
1748
2067
  /**
1749
2068
  * Remove all handlers for all actions
@@ -1753,8 +2072,13 @@ var ActionRegister = class {
1753
2072
  * @public
1754
2073
  */
1755
2074
  clearAll() {
2075
+ [...this.pipelines.keys()].forEach((action) => {
2076
+ this.clearAction(action);
2077
+ });
1756
2078
  this.pipelines.clear();
1757
2079
  this.lastRegisteredTimestamps.clear();
2080
+ this.unregisterFunctions.clear();
2081
+ this.actionGuard.clearAll();
1758
2082
  }
1759
2083
  /**
1760
2084
  * Get the name of this action register
@@ -1883,29 +2207,36 @@ var ActionRegister = class {
1883
2207
  */
1884
2208
  createUnregisterFunction(action, handlerId, registration) {
1885
2209
  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
- }
2210
+ if (this.removeRegistration(action, registration)) this.log(`Handler unregistered: ${String(action)}`, {
2211
+ handlerId,
2212
+ remainingHandlers: this.getHandlerCount(action),
2213
+ actionRemoved: !this.pipelines.has(action)
2214
+ });
1907
2215
  };
1908
2216
  }
2217
+ /** Remove a registration and release every resource owned by it exactly once. */
2218
+ removeRegistration(action, registration, runCleanup = true) {
2219
+ const pipeline = this.pipelines.get(action);
2220
+ if (!pipeline) return false;
2221
+ const index = pipeline.indexOf(registration);
2222
+ if (index === -1) return false;
2223
+ pipeline.splice(index, 1);
2224
+ this.unregisterFunctions.delete(registration.id);
2225
+ if (runCleanup) this.runRegistrationCleanup(action, registration);
2226
+ if (pipeline.length === 0) {
2227
+ this.pipelines.delete(action);
2228
+ this.lastRegisteredTimestamps.delete(action);
2229
+ }
2230
+ return true;
2231
+ }
2232
+ runRegistrationCleanup(action, registration) {
2233
+ if (!registration.config.cleanup) return;
2234
+ try {
2235
+ registration.config.cleanup();
2236
+ } catch (cleanupError) {
2237
+ this.log(`Cleanup error while removing handler: ${String(action)}`, cleanupError, "warn");
2238
+ }
2239
+ }
1909
2240
  /**
1910
2241
  * Gets the total count of registered unregister functions
1911
2242
  *
@@ -1925,29 +2256,77 @@ var ActionRegister = class {
1925
2256
  hasUnregisterFunction(handlerId) {
1926
2257
  return this.unregisterFunctions.has(handlerId);
1927
2258
  }
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");
2259
+ /** Reject queued dispatches without releasing registered handlers. */
2260
+ cancelPendingDispatches() {
2261
+ this.dispatchQueue?.clear({ rejectPending: true });
2262
+ }
2263
+ beginShutdown() {
2264
+ if (this.destroyAsyncPromise) return this.destroyAsyncPromise;
2265
+ if (this.lifecycleState === "destroyed") {
2266
+ this.destroyAsyncPromise = Promise.resolve();
2267
+ return this.destroyAsyncPromise;
1942
2268
  }
1943
- this.pipelines.clear();
1944
- this.lastRegisteredTimestamps.clear();
2269
+ this.lifecycleState = "closing";
2270
+ const shutdownError = new ActionRegisterDestroyedError(this.name, "closing");
2271
+ let resolveShutdown;
2272
+ let rejectShutdown;
2273
+ this.destroyAsyncPromise = new Promise((resolve, reject) => {
2274
+ resolveShutdown = resolve;
2275
+ rejectShutdown = reject;
2276
+ });
2277
+ if (!this.lifecycleController.signal.aborted) this.lifecycleController.abort(shutdownError);
1945
2278
  this.actionGuard.destroy();
1946
- this.dispatchQueue?.clear?.();
2279
+ this.dispatchQueue?.clear({
2280
+ rejectPending: true,
2281
+ reason: shutdownError
2282
+ });
2283
+ if (this.dispatchConstructionDepth === 0 && this.activeDispatches.size === 0 && this.activeHandlerPromises.size === 0) {
2284
+ this.finalizeDestroy();
2285
+ resolveShutdown();
2286
+ return this.destroyAsyncPromise;
2287
+ }
2288
+ const drainAndFinalize = async () => {
2289
+ while (this.activeDispatches.size > 0 || this.activeHandlerPromises.size > 0) await Promise.allSettled([...this.activeDispatches, ...this.activeHandlerPromises]);
2290
+ this.finalizeDestroy();
2291
+ };
2292
+ Promise.resolve().then(drainAndFinalize).then(resolveShutdown, rejectShutdown);
2293
+ this.destroyAsyncPromise.catch((error) => {
2294
+ this.log("ActionRegister async destroy failed", error, "warn");
2295
+ });
2296
+ return this.destroyAsyncPromise;
2297
+ }
2298
+ finalizeDestroy() {
2299
+ if (this.lifecycleState === "destroyed") return;
2300
+ this.clearAll();
1947
2301
  this.actionExecutionModes.clear();
1948
2302
  this.controllerPool.length = 0;
2303
+ this.lifecycleState = "destroyed";
1949
2304
  this.log("ActionRegister destroyed");
1950
2305
  }
2306
+ /**
2307
+ * ๐Ÿ†• Destroy method for comprehensive cleanup
2308
+ *
2309
+ * Begins terminal cleanup of pipelines, guards, queues, and statistics. Cleanup
2310
+ * remains synchronous when no work has started; otherwise active handlers drain
2311
+ * in the background. Use destroyAsync() when completion must be observed.
2312
+ *
2313
+ * @public
2314
+ */
2315
+ destroy() {
2316
+ this.beginShutdown();
2317
+ }
2318
+ /**
2319
+ * Begin terminal shutdown and resolve after all started handlers have settled
2320
+ * and their registered cleanup functions have run.
2321
+ *
2322
+ * Repeated calls return the same promise. New registrations and dispatches are
2323
+ * rejected as soon as shutdown begins.
2324
+ *
2325
+ * @public
2326
+ */
2327
+ destroyAsync() {
2328
+ return this.beginShutdown();
2329
+ }
1951
2330
  };
1952
2331
 
1953
2332
  //#endregion
@@ -2036,12 +2415,18 @@ function createActionHandler(registry, action, handler, config) {
2036
2415
  let currentUnregister;
2037
2416
  let isRegistered = false;
2038
2417
  return {
2418
+ /**
2419
+ * Register the handler and return cleanup function
2420
+ */
2039
2421
  register() {
2040
2422
  if (isRegistered && currentUnregister) currentUnregister();
2041
2423
  currentUnregister = registry.register(action, handler, finalConfig);
2042
2424
  isRegistered = true;
2043
2425
  return currentUnregister;
2044
2426
  },
2427
+ /**
2428
+ * Unregister the handler if currently registered
2429
+ */
2045
2430
  unregister() {
2046
2431
  if (isRegistered && currentUnregister) {
2047
2432
  currentUnregister();
@@ -2049,6 +2434,9 @@ function createActionHandler(registry, action, handler, config) {
2049
2434
  isRegistered = false;
2050
2435
  }
2051
2436
  },
2437
+ /**
2438
+ * Register and return cleanup function (React useEffect pattern)
2439
+ */
2052
2440
  registerWithCleanup() {
2053
2441
  const unregisterFn = this.register();
2054
2442
  return () => {
@@ -2065,18 +2453,33 @@ function createActionHandler(registry, action, handler, config) {
2065
2453
  * Provides debugging and development helpers specifically for React environments.
2066
2454
  */
2067
2455
  const ReactDevUtils = {
2456
+ /**
2457
+ * Enable detailed React integration debugging
2458
+ */
2068
2459
  enableDebugMode() {
2069
2460
  if (typeof window !== "undefined") window.__CONTEXT_ACTION_REACT_DEBUG__ = true;
2070
2461
  },
2462
+ /**
2463
+ * Disable React integration debugging
2464
+ */
2071
2465
  disableDebugMode() {
2072
2466
  if (typeof window !== "undefined") window.__CONTEXT_ACTION_REACT_DEBUG__ = false;
2073
2467
  },
2468
+ /**
2469
+ * Check if React debug mode is enabled
2470
+ */
2074
2471
  isDebugMode() {
2075
2472
  return typeof window !== "undefined" && Boolean(window.__CONTEXT_ACTION_REACT_DEBUG__);
2076
2473
  },
2474
+ /**
2475
+ * Log React-specific debugging information
2476
+ */
2077
2477
  log(component, action, message, data) {
2078
2478
  if (this.isDebugMode()) console.log(`๐ŸŽฏ [React-ActionRegister] [${component}] ${action}: ${message}`, data || "");
2079
2479
  },
2480
+ /**
2481
+ * Get React integration statistics
2482
+ */
2080
2483
  getStats(registry) {
2081
2484
  const registryInfo = registry.getRegistryInfo();
2082
2485
  let reactHandlers = 0;
@@ -2108,7 +2511,7 @@ var ReactActionError = class ReactActionError extends Error {
2108
2511
  this.payload = payload;
2109
2512
  this.handlerId = handlerId;
2110
2513
  this.timestamp = Date.now();
2111
- if (originalError && originalError.stack) this.stack = originalError.stack;
2514
+ if (originalError?.stack) this.stack = originalError.stack;
2112
2515
  }
2113
2516
  /**
2114
2517
  * Create a React Error Boundary compatible error
@@ -2128,140 +2531,5 @@ function isReactActionError(error) {
2128
2531
  }
2129
2532
 
2130
2533
  //#endregion
2131
- //#region src/action-schema.ts
2132
- /**
2133
- * Zod ์Šคํ‚ค๋งˆ๋ฅผ JSON Schema๋กœ ๋ณ€ํ™˜ (Zod 4 ๋„ค์ดํ‹ฐ๋ธŒ API)
2134
- *
2135
- * @param schema - Zod ์Šคํ‚ค๋งˆ
2136
- * @returns JSON Schema (draft-7)
2137
- */
2138
- function zodToJsonSchema(schema, zodModule) {
2139
- return zodModule.toJSONSchema(schema, {
2140
- target: "draft-7",
2141
- metadata: zodModule.globalRegistry
2142
- });
2143
- }
2144
- /**
2145
- * Zod ์Šคํ‚ค๋งˆ ๊ธฐ๋ฐ˜ Action ์ •์˜
2146
- *
2147
- * defineTool ํŒจํ„ด์„ ๊ธฐ๋ฐ˜์œผ๋กœ context-action์— ๋งž๊ฒŒ ๊ตฌํ˜„:
2148
- * - Single Source of Truth: Zod ์Šคํ‚ค๋งˆ๋กœ ํƒ€์ž… + ๊ฒ€์ฆ + ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํ†ตํ•ฉ
2149
- * - ๋Ÿฐํƒ€์ž„ ๊ฒ€์ฆ: validate(), safeParse()
2150
- * - Tool Chain ํ˜ธํ™˜: toMCP(), toOpenAI(), toAnthropic()
2151
- *
2152
- * @param options - Action ์ •์˜ ์˜ต์…˜
2153
- * @param zodModule - Zod ๋ชจ๋“ˆ (peerDependency๋กœ ์ฃผ์ž…)
2154
- * @returns UnifiedAction ์ธ์Šคํ„ด์Šค
2155
- *
2156
- * @example
2157
- * ```typescript
2158
- * import { z } from 'zod';
2159
- * import { defineAction } from '@context-action/core';
2160
- *
2161
- * const updateUserAction = defineAction({
2162
- * name: 'updateUser',
2163
- * description: 'Update user profile',
2164
- * parameters: z.object({
2165
- * id: z.string().min(1).meta({ description: 'User ID' }),
2166
- * name: z.string().min(2).max(50).meta({ description: 'User name' }),
2167
- * email: z.string().email().optional(),
2168
- * }),
2169
- * }, z);
2170
- *
2171
- * // ๊ฒ€์ฆ
2172
- * const validated = updateUserAction.validate({ id: '123', name: 'John' });
2173
- *
2174
- * // Tool chain ๋ณ€ํ™˜
2175
- * const mcpTool = updateUserAction.toMCP();
2176
- * ```
2177
- */
2178
- function defineAction(options, zodModule) {
2179
- const { name, description, parameters } = options;
2180
- const jsonSchema = zodToJsonSchema(parameters, zodModule);
2181
- return {
2182
- name,
2183
- description,
2184
- zodSchema: parameters,
2185
- jsonSchema,
2186
- validate: (payload) => {
2187
- return parameters.parse(payload);
2188
- },
2189
- safeParse: (payload) => {
2190
- return parameters.safeParse(payload);
2191
- },
2192
- toJSONSchema: () => jsonSchema,
2193
- toMCP: () => ({
2194
- name,
2195
- description,
2196
- inputSchema: jsonSchema
2197
- }),
2198
- toOpenAI: () => ({
2199
- type: "function",
2200
- function: {
2201
- name,
2202
- description,
2203
- parameters: {
2204
- type: "object",
2205
- properties: jsonSchema.properties ?? {},
2206
- required: jsonSchema.required
2207
- }
2208
- }
2209
- }),
2210
- toAnthropic: () => ({
2211
- name,
2212
- description,
2213
- input_schema: jsonSchema
2214
- })
2215
- };
2216
- }
2217
- /**
2218
- * ๋‹ค์ค‘ Action ์Šคํ‚ค๋งˆ ์ƒ์„ฑ
2219
- *
2220
- * ์—ฌ๋Ÿฌ defineAction์„ ๋ฌถ์–ด์„œ ActionSchemaMap ์ƒ์„ฑ
2221
- *
2222
- * @param actions - UnifiedAction ๋งต
2223
- * @returns ActionSchemaMap
2224
- *
2225
- * @example
2226
- * ```typescript
2227
- * const userActionSchema = createActionSchema({
2228
- * updateUser: defineAction({ ... }, z),
2229
- * deleteUser: defineAction({ ... }, z),
2230
- * });
2231
- *
2232
- * type UserActions = InferActionPayloadMap<typeof userActionSchema>;
2233
- * ```
2234
- */
2235
- function createActionSchema(actions) {
2236
- return actions;
2237
- }
2238
- /**
2239
- * Zod ๋ชจ๋“ˆ์„ ๋ฐ”์ธ๋”ฉํ•œ defineAction ํŒฉํ† ๋ฆฌ ์ƒ์„ฑ
2240
- *
2241
- * ๋งค๋ฒˆ z ๋ชจ๋“ˆ์„ ์ „๋‹ฌํ•˜์ง€ ์•Š์•„๋„ ๋˜๋„๋ก ํŒฉํ† ๋ฆฌ ํŒจํ„ด ์ œ๊ณต
2242
- *
2243
- * @param zodModule - Zod ๋ชจ๋“ˆ
2244
- * @returns defineAction ํ•จ์ˆ˜ (z ๋ฐ”์ธ๋”ฉ๋จ)
2245
- *
2246
- * @example
2247
- * ```typescript
2248
- * import { z } from 'zod';
2249
- * import { createActionFactory } from '@context-action/core';
2250
- *
2251
- * const defineAction = createActionFactory(z);
2252
- *
2253
- * const updateUser = defineAction({
2254
- * name: 'updateUser',
2255
- * parameters: z.object({ id: z.string() }),
2256
- * });
2257
- * ```
2258
- */
2259
- function createActionFactory(zodModule) {
2260
- return (options) => {
2261
- return defineAction(options, zodModule);
2262
- };
2263
- }
2264
-
2265
- //#endregion
2266
- export { ActionGuard, ActionRegister, ActionValidationError, ReactActionError, ReactDevUtils, createActionFactory, createActionHandler, createActionSchema, defineAction, executeParallel, executeRace, executeSequential, isActionValidationError, isReactActionError, zodToJsonSchema };
2534
+ export { ActionGuard, ActionRegister, ActionRegisterDestroyedError, ActionTimeoutError, ActionValidationError, ReactActionError, ReactDevUtils, createActionHandler, executeParallel, executeRace, executeSequential, isActionRegisterDestroyedError, isActionTimeoutError, isActionValidationError, isReactActionError };
2267
2535
  //# sourceMappingURL=index.js.map