@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.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) => {
@@ -98,7 +105,6 @@ async function executeSequential(context, createController) {
98
105
  }
99
106
  i = jumpIndex;
100
107
  context.jumpToPriority = void 0;
101
- continue;
102
108
  } else {
103
109
  context.jumpToPriority = void 0;
104
110
  i++;
@@ -162,10 +168,9 @@ async function executeParallel(context, createController) {
162
168
  skipped: true
163
169
  };
164
170
  }
171
+ (context.executedHandlers ?? (context.executedHandlers = [])).push(registration);
165
172
  const result = registration.handler(context.payload, controller);
166
- let handlerResult;
167
- if (result instanceof Promise) handlerResult = await result;
168
- else handlerResult = result;
173
+ const handlerResult = isPromiseLike(result) ? await Promise.resolve(result) : result;
169
174
  /** Collect result if handler returned something and pipeline wasn't terminated */
170
175
  if (handlerResult !== void 0 && !context.terminated) context.results.push(handlerResult);
171
176
  return {
@@ -184,8 +189,9 @@ async function executeParallel(context, createController) {
184
189
  };
185
190
  }
186
191
  });
192
+ const trackedHandlerPromises = context.trackHandlerPromise ? handlerPromises.map((promise) => context.trackHandlerPromise(promise)) : handlerPromises;
187
193
  /** Wait for all handlers to complete */
188
- const results = await Promise.allSettled(handlerPromises);
194
+ const results = await Promise.allSettled(trackedHandlerPromises);
189
195
  /** Check for any rejected blocking handlers */
190
196
  const failures = results.filter((result, index) => {
191
197
  if (result.status === "rejected") return runnableHandlers[index]?.config.blocking ?? false;
@@ -204,8 +210,10 @@ async function executeParallel(context, createController) {
204
210
  *
205
211
  * Executes all qualifying handlers simultaneously using Promise.race, where
206
212
  * 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.
213
+ * continue in the background and remain tracked for lifecycle cleanup; handlers
214
+ * must observe the controller signal for cooperative external cancellation.
215
+ * Useful for scenarios where you want the fastest response from multiple
216
+ * equivalent handlers.
209
217
  *
210
218
  * @template T - The payload type for the action
211
219
  * @template R - The result type for handlers
@@ -246,10 +254,9 @@ async function executeRace(context, createController) {
246
254
  skipped: true
247
255
  };
248
256
  }
257
+ (context.executedHandlers ?? (context.executedHandlers = [])).push(registration);
249
258
  const result = registration.handler(context.payload, controller);
250
- let handlerResult;
251
- if (result instanceof Promise) handlerResult = await result;
252
- else handlerResult = result;
259
+ const handlerResult = isPromiseLike(result) ? await Promise.resolve(result) : result;
253
260
  return {
254
261
  success: true,
255
262
  handlerId: registration.id,
@@ -267,8 +274,9 @@ async function executeRace(context, createController) {
267
274
  };
268
275
  }
269
276
  });
270
- /** Race all handlers */
271
- const winner = await Promise.race(handlerPromises);
277
+ const trackedHandlerPromises = context.trackHandlerPromise ? handlerPromises.map((promise) => context.trackHandlerPromise(promise)) : handlerPromises;
278
+ /** Race all handlers while retaining every loser for lifecycle draining. */
279
+ const winner = await Promise.race(trackedHandlerPromises);
272
280
  /** If the winner failed and was blocking, throw the error */
273
281
  if (!winner.success && winner.registration?.config.blocking) throw winner.error;
274
282
  /** Collect result from the winning handler */
@@ -317,7 +325,11 @@ var ActionGuard = class {
317
325
  this.cleanupIntervalMs = 3e4;
318
326
  this.maxGuards = 1e3;
319
327
  this.accessOrder = [];
320
- if (autoCleanup) this.startAutoCleanup();
328
+ this.autoCleanupEnabled = autoCleanup;
329
+ }
330
+ /** Start cleanup only after the first guard is used. */
331
+ ensureAutoCleanup() {
332
+ if (this.autoCleanupEnabled && !this.cleanupInterval) this.startAutoCleanup();
321
333
  }
322
334
  /**
323
335
  * Start automatic cleanup of idle guard states
@@ -325,9 +337,17 @@ var ActionGuard = class {
325
337
  * @internal
326
338
  */
327
339
  startAutoCleanup() {
340
+ if (this.cleanupInterval) return;
328
341
  this.cleanupInterval = setInterval(() => {
329
342
  this.performCleanup();
330
343
  }, this.cleanupIntervalMs);
344
+ this.cleanupInterval.unref?.();
345
+ }
346
+ stopAutoCleanup() {
347
+ if (this.cleanupInterval) {
348
+ clearInterval(this.cleanupInterval);
349
+ this.cleanupInterval = void 0;
350
+ }
331
351
  }
332
352
  /**
333
353
  * 🔧 Optimized cleanup with early exit and batched operations
@@ -336,7 +356,10 @@ var ActionGuard = class {
336
356
  */
337
357
  performCleanup() {
338
358
  const guardCount = this.guards.size;
339
- if (guardCount === 0) return;
359
+ if (guardCount === 0) {
360
+ this.stopAutoCleanup();
361
+ return;
362
+ }
340
363
  const now = Date.now();
341
364
  const keysToDelete = [];
342
365
  if (guardCount <= 10) this.guards.forEach((state, key) => {
@@ -366,6 +389,7 @@ var ActionGuard = class {
366
389
  if (accessIndex !== -1) this.accessOrder.splice(accessIndex, 1);
367
390
  });
368
391
  if (typeof process !== "undefined" && process.env?.DEBUG_CONTEXT_ACTION) console.debug(`[ActionGuard] Cleaned up ${keysToDelete.length} idle guards`);
392
+ if (this.guards.size === 0) this.stopAutoCleanup();
369
393
  }
370
394
  }
371
395
  /**
@@ -424,6 +448,7 @@ var ActionGuard = class {
424
448
  * @internal
425
449
  */
426
450
  async debounce(actionKey, debounceMs) {
451
+ this.ensureAutoCleanup();
427
452
  this.evictIfNeeded();
428
453
  /** Get or create guard state for this action */
429
454
  let state = this.guards.get(actionKey);
@@ -484,6 +509,7 @@ var ActionGuard = class {
484
509
  * @internal
485
510
  */
486
511
  throttle(actionKey, throttleMs) {
512
+ this.ensureAutoCleanup();
487
513
  this.evictIfNeeded();
488
514
  /** Get or create guard state for this action */
489
515
  let state = this.guards.get(actionKey);
@@ -551,6 +577,9 @@ var ActionGuard = class {
551
577
  state.throttleTimer = void 0;
552
578
  }
553
579
  this.guards.delete(actionKey);
580
+ const accessIndex = this.accessOrder.indexOf(actionKey);
581
+ if (accessIndex !== -1) this.accessOrder.splice(accessIndex, 1);
582
+ if (this.guards.size === 0) this.stopAutoCleanup();
554
583
  }
555
584
  }
556
585
  /**
@@ -575,6 +604,8 @@ var ActionGuard = class {
575
604
  });
576
605
  /** Remove all guard states from memory */
577
606
  this.guards.clear();
607
+ this.accessOrder = [];
608
+ this.stopAutoCleanup();
578
609
  }
579
610
  /**
580
611
  * Get current guard state for debugging purposes
@@ -612,12 +643,7 @@ var ActionGuard = class {
612
643
  * @internal
613
644
  */
614
645
  destroy() {
615
- if (this.cleanupInterval) {
616
- clearInterval(this.cleanupInterval);
617
- this.cleanupInterval = void 0;
618
- }
619
646
  this.clearAll();
620
- this.accessOrder = [];
621
647
  }
622
648
  /**
623
649
  * 🆕 Get statistics about active guards
@@ -670,27 +696,42 @@ var OperationQueue = class {
670
696
  * @returns Promise로 래핑된 작업 결과
671
697
  */
672
698
  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;
699
+ return this.enqueueWithHandle(operation, priority).promise;
700
+ }
701
+ /** Enqueue an operation and retain a handle for pre-start cancellation. */
702
+ enqueueWithHandle(operation, priority = 0) {
703
+ let queuedOperation;
704
+ return {
705
+ promise: new Promise((resolve, reject) => {
706
+ queuedOperation = {
707
+ id: `${this.name}-${++this.operationCounter}`,
708
+ operation,
709
+ resolve,
710
+ reject,
711
+ priority,
712
+ timestamp: Date.now()
713
+ };
714
+ let insertIndex = this.queue.length;
715
+ for (let i = 0; i < this.queue.length; i++) {
716
+ const item = this.queue[i];
717
+ if (item && (item.priority || 0) < priority) {
718
+ insertIndex = i;
719
+ break;
720
+ }
688
721
  }
722
+ this.queue.splice(insertIndex, 0, queuedOperation);
723
+ if (this.processingPromise) this.notifyNewOperation();
724
+ this.processQueue();
725
+ }),
726
+ cancel: (reason = /* @__PURE__ */ new Error("Queue operation cancelled")) => {
727
+ const index = this.queue.indexOf(queuedOperation);
728
+ if (index === -1) return false;
729
+ this.queue.splice(index, 1);
730
+ queuedOperation.reject(reason);
731
+ this.notifyNewOperation();
732
+ return true;
689
733
  }
690
- this.queue.splice(insertIndex, 0, queuedOperation);
691
- if (this.processingPromise) this.notifyNewOperation();
692
- this.processQueue();
693
- });
734
+ };
694
735
  }
695
736
  /**
696
737
  * 🆕 큐 처리 메인 로직 - 동시성 제어 및 비동기 지원
@@ -796,12 +837,14 @@ var OperationQueue = class {
796
837
  /**
797
838
  * 큐 비우기 (테스트용)
798
839
  */
799
- clear() {
840
+ clear(options = {}) {
841
+ const rejectPending = options.rejectPending ?? true;
842
+ const reason = options.reason ?? /* @__PURE__ */ new Error("Queue cleared");
800
843
  this.queue.forEach((operation) => {
801
- operation.reject(/* @__PURE__ */ new Error("Queue cleared"));
844
+ if (rejectPending) operation.reject(reason);
845
+ else operation.resolve(void 0);
802
846
  });
803
847
  this.queue = [];
804
- this.processingPromise = null;
805
848
  this.pendingResolvers.splice(0).forEach((resolve) => resolve());
806
849
  }
807
850
  /**
@@ -901,11 +944,44 @@ var ActionValidationError = class ActionValidationError extends Error {
901
944
  }
902
945
  };
903
946
  /**
947
+ * Raised when a dispatch exceeds its configured wall-clock timeout.
948
+ * The underlying handler receives an aborted controller signal and the internal
949
+ * queue keeps draining it safely, while the caller is released immediately with
950
+ * this error.
951
+ */
952
+ var ActionTimeoutError = class ActionTimeoutError extends Error {
953
+ constructor(action, timeout) {
954
+ super(`Action "${action}" timed out after ${timeout}ms`);
955
+ this.action = action;
956
+ this.timeout = timeout;
957
+ this.name = "ActionTimeoutError";
958
+ Object.setPrototypeOf(this, ActionTimeoutError.prototype);
959
+ }
960
+ };
961
+ /** Raised when work is submitted after an ActionRegister begins shutdown. */
962
+ var ActionRegisterDestroyedError = class ActionRegisterDestroyedError extends Error {
963
+ constructor(registerName, state) {
964
+ super(`ActionRegister "${registerName}" is ${state} and cannot accept new work`);
965
+ this.registerName = registerName;
966
+ this.state = state;
967
+ this.name = "ActionRegisterDestroyedError";
968
+ Object.setPrototypeOf(this, ActionRegisterDestroyedError.prototype);
969
+ }
970
+ };
971
+ /**
904
972
  * ActionValidationError 타입 가드
905
973
  */
906
974
  function isActionValidationError(error) {
907
975
  return error instanceof ActionValidationError;
908
976
  }
977
+ /** ActionTimeoutError type guard. */
978
+ function isActionTimeoutError(error) {
979
+ return error instanceof ActionTimeoutError;
980
+ }
981
+ /** ActionRegisterDestroyedError type guard. */
982
+ function isActionRegisterDestroyedError(error) {
983
+ return error instanceof ActionRegisterDestroyedError;
984
+ }
909
985
 
910
986
  //#endregion
911
987
  //#region src/ActionRegister.ts
@@ -924,35 +1000,6 @@ function isActionValidationError(error) {
924
1000
  *
925
1001
  * @public
926
1002
  */
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
1003
  var ActionRegister = class {
957
1004
  constructor(config = {}) {
958
1005
  this.pipelines = /* @__PURE__ */ new Map();
@@ -960,10 +1007,13 @@ var ActionRegister = class {
960
1007
  this.actionExecutionModes = /* @__PURE__ */ new Map();
961
1008
  this.unregisterFunctions = /* @__PURE__ */ new Map();
962
1009
  this.lastRegisteredTimestamps = /* @__PURE__ */ new Map();
963
- this.filterCacheDisabled = true;
964
1010
  this.handlerIdCounter = 0;
965
1011
  this.controllerPool = [];
966
- this.maxControllerPoolSize = 10;
1012
+ this.lifecycleState = "active";
1013
+ this.lifecycleController = new AbortController();
1014
+ this.activeDispatches = /* @__PURE__ */ new Set();
1015
+ this.activeHandlerPromises = /* @__PURE__ */ new Set();
1016
+ this.dispatchConstructionDepth = 0;
967
1017
  this.name = config.name || "ActionRegister";
968
1018
  this.registryConfig = config.registry;
969
1019
  this.maxHandlersPerAction = config.registry?.maxHandlersPerAction ?? 1e3;
@@ -980,10 +1030,10 @@ var ActionRegister = class {
980
1030
  }
981
1031
  /**
982
1032
  * 🆕 Action-based dispatcher
983
- *
1033
+ *
984
1034
  * Provides function-based access to actions for more convenient dispatching.
985
1035
  * Each action becomes a callable function that can be invoked directly.
986
- *
1036
+ *
987
1037
  * @example
988
1038
  * ```typescript
989
1039
  * interface MyActions extends ActionPayloadMap {
@@ -996,19 +1046,17 @@ var ActionRegister = class {
996
1046
  * // Function-based dispatching
997
1047
  * await registry.actions.userLogin({ userId: '123', email: 'test@example.com' });
998
1048
  * await registry.actions.resetApp();
1049
+ * await registry.actions.resetApp(undefined, { debounce: 100 });
999
1050
  * ```
1000
1051
  *
1001
1052
  * @public
1002
1053
  */
1003
1054
  get actions() {
1004
1055
  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
- }
1056
+ const actionKey = prop;
1057
+ if (typeof prop === "string" && this.pipelines.has(actionKey)) return (payload, options) => {
1058
+ return this.dispatch(actionKey, payload, options);
1059
+ };
1012
1060
  } });
1013
1061
  return this._actionsProxy;
1014
1062
  }
@@ -1025,6 +1073,10 @@ var ActionRegister = class {
1025
1073
  *
1026
1074
  * // Actions without payload
1027
1075
  * const result = await registry.actionsWithResult.userLogout();
1076
+ * const debouncedResult = await registry.actionsWithResult.userLogout(
1077
+ * undefined,
1078
+ * { debounce: 100 }
1079
+ * );
1028
1080
  *
1029
1081
  * // With options
1030
1082
  * const result = await registry.actionsWithResult.processData(
@@ -1037,13 +1089,10 @@ var ActionRegister = class {
1037
1089
  */
1038
1090
  get actionsWithResult() {
1039
1091
  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
- }
1092
+ const actionKey = prop;
1093
+ if (typeof prop === "string" && this.pipelines.has(actionKey)) return (payload, options) => {
1094
+ return this.dispatchWithResult(actionKey, payload, options);
1095
+ };
1047
1096
  } });
1048
1097
  return this._actionsWithResultProxy;
1049
1098
  }
@@ -1063,6 +1112,7 @@ var ActionRegister = class {
1063
1112
  * @public
1064
1113
  */
1065
1114
  register(action, handler, config = {}) {
1115
+ this.assertAcceptingWork();
1066
1116
  const handlerId = config.id || this.generateHandlerId(action);
1067
1117
  return this._performRegistrationSync(action, handler, config, handlerId);
1068
1118
  }
@@ -1075,6 +1125,15 @@ var ActionRegister = class {
1075
1125
  console[level](`🎯 [${timestamp}] [${this.name}] ${message}`, data || "");
1076
1126
  }
1077
1127
  }
1128
+ assertAcceptingWork() {
1129
+ if (this.lifecycleState !== "active") throw new ActionRegisterDestroyedError(this.name, this.lifecycleState);
1130
+ }
1131
+ rejectedLifecyclePromise() {
1132
+ const error = new ActionRegisterDestroyedError(this.name, this.lifecycleState === "active" ? "destroyed" : this.lifecycleState);
1133
+ const rejected = Promise.reject(error);
1134
+ rejected.catch(() => {});
1135
+ return rejected;
1136
+ }
1078
1137
  /**
1079
1138
  * 🔧 Generate unique handler ID using optimized counter-based approach
1080
1139
  */
@@ -1165,7 +1224,7 @@ var ActionRegister = class {
1165
1224
  const existing = pipeline[existingIndex];
1166
1225
  const existingUnregister = this.unregisterFunctions.get(handlerId);
1167
1226
  if (registration.config.replaceExisting) {
1168
- if (existing && existing.config.cleanup && typeof existing.config.cleanup === "function") try {
1227
+ if (existing?.config.cleanup && typeof existing.config.cleanup === "function") try {
1169
1228
  existing.config.cleanup();
1170
1229
  } catch (cleanupError) {
1171
1230
  this.log(`Cleanup error for replaced handler: ${String(action)}`, cleanupError, "warn");
@@ -1213,16 +1272,245 @@ var ActionRegister = class {
1213
1272
  });
1214
1273
  return unregister;
1215
1274
  }
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);
1275
+ dispatch(action, payload, options) {
1276
+ if (this.lifecycleState !== "active") return this.rejectedLifecyclePromise();
1277
+ const timeoutScope = this.createTimeoutScope(action, options);
1278
+ const dispatchHandlerPromises = /* @__PURE__ */ new Set();
1279
+ const attemptState = { count: 0 };
1280
+ const operation = async () => {
1281
+ if (!timeoutScope.options?.signal?.aborted) this.validatePayload(action, payload);
1282
+ return this.executeWithRetry(async () => {
1283
+ const executedHandlers = [];
1284
+ try {
1285
+ return await this._performDispatch(action, payload, timeoutScope.options, attemptState.count > 1, executedHandlers, dispatchHandlerPromises);
1286
+ } finally {
1287
+ this.cleanupOneTimeHandlers(action, executedHandlers, dispatchHandlerPromises);
1288
+ }
1289
+ }, timeoutScope.options, attemptState, void 0, () => this.getHandlerCount(action) > 0);
1290
+ };
1291
+ const hasTimingGuard = options?.debounce !== void 0 || options?.throttle !== void 0 || this.pipelines.get(action)?.some((handler) => handler.config.debounce !== void 0 || handler.config.throttle !== void 0) === true;
1292
+ let dispatchPromise;
1293
+ this.dispatchConstructionDepth += 1;
1294
+ try {
1295
+ if (timeoutScope.options?.immediate || hasTimingGuard || !this.dispatchQueue) dispatchPromise = operation();
1296
+ else {
1297
+ const queued = this.dispatchQueue.enqueueWithHandle(operation, timeoutScope.options?.queuePriority ?? 0);
1298
+ timeoutScope.onTimeout((error) => queued.cancel(error));
1299
+ dispatchPromise = queued.promise;
1300
+ }
1301
+ this.trackDispatchPromise(dispatchPromise);
1302
+ } finally {
1303
+ this.dispatchConstructionDepth -= 1;
1304
+ }
1305
+ const observedPromise = this.raceWithTimeout(dispatchPromise, timeoutScope, dispatchHandlerPromises).catch((error) => {
1306
+ this.invokeErrorHandler(error, action, payload, options, attemptState.count);
1307
+ throw error;
1308
+ });
1309
+ observedPromise.catch(() => {});
1310
+ return observedPromise;
1311
+ }
1312
+ /** Execute a dispatch operation with an optional whole-action retry policy. */
1313
+ async executeWithRetry(operation, options, attemptState, shouldRetryResult, canRetry = () => true) {
1314
+ const configuredAttempts = options?.retryOnError?.maxAttempts ?? 1;
1315
+ const maxAttempts = Number.isFinite(configuredAttempts) ? Math.max(1, Math.floor(configuredAttempts)) : 1;
1316
+ const retryDelay = Math.max(0, options?.retryOnError?.delay ?? 0);
1317
+ while (attemptState.count < maxAttempts) {
1318
+ attemptState.count += 1;
1319
+ try {
1320
+ const result = await operation();
1321
+ if (!(shouldRetryResult?.(result) ?? false) || attemptState.count >= maxAttempts || options?.signal?.aborted || !canRetry()) return result;
1322
+ } catch (error) {
1323
+ if (error instanceof ActionValidationError || attemptState.count >= maxAttempts || options?.signal?.aborted || !canRetry()) throw error;
1324
+ }
1325
+ await this.waitForRetry(retryDelay, options?.signal);
1326
+ }
1327
+ return operation();
1328
+ }
1329
+ trackDispatchPromise(promise) {
1330
+ this.activeDispatches.add(promise);
1331
+ const remove = () => this.activeDispatches.delete(promise);
1332
+ promise.then(remove, remove);
1333
+ return promise;
1334
+ }
1335
+ trackHandlerPromise(promise, dispatchHandlerPromises) {
1336
+ this.activeHandlerPromises.add(promise);
1337
+ dispatchHandlerPromises.add(promise);
1338
+ const remove = () => {
1339
+ this.activeHandlerPromises.delete(promise);
1340
+ dispatchHandlerPromises.delete(promise);
1341
+ };
1342
+ promise.then(remove, remove);
1343
+ return promise;
1344
+ }
1345
+ trackGlobalHandlerPromise(promise) {
1346
+ this.activeHandlerPromises.add(promise);
1347
+ const remove = () => this.activeHandlerPromises.delete(promise);
1348
+ promise.then(remove, remove);
1349
+ return promise;
1350
+ }
1351
+ /** Abort-aware retry delay so cancellation does not wait for the full backoff. */
1352
+ waitForRetry(delay, signal) {
1353
+ if (delay <= 0 || signal?.aborted) return Promise.resolve();
1354
+ return new Promise((resolve) => {
1355
+ const timer = setTimeout(finish, delay);
1356
+ const abort = () => finish();
1357
+ function finish() {
1358
+ clearTimeout(timer);
1359
+ signal?.removeEventListener("abort", abort);
1360
+ resolve();
1361
+ }
1362
+ signal?.addEventListener("abort", abort, { once: true });
1220
1363
  });
1221
1364
  }
1365
+ /** Build a wall-clock timeout that also participates in pipeline cancellation. */
1366
+ createTimeoutScope(action, options) {
1367
+ const hasTimeout = options?.timeout !== void 0 && Number.isFinite(options.timeout);
1368
+ const timeout = hasTimeout ? Math.max(0, options.timeout) : void 0;
1369
+ const timeoutController = hasTimeout ? new AbortController() : void 0;
1370
+ const signalCleanups = [];
1371
+ const timeoutCallbacks = /* @__PURE__ */ new Set();
1372
+ const signals = [
1373
+ this.lifecycleController.signal,
1374
+ options?.signal,
1375
+ timeoutController?.signal
1376
+ ].filter((candidate) => Boolean(candidate));
1377
+ let signal = signals[0];
1378
+ if (signals.length > 1) if (typeof AbortSignal.any === "function") signal = AbortSignal.any(signals);
1379
+ else {
1380
+ const mergedController = new AbortController();
1381
+ const forwardAbort = (source) => {
1382
+ if (!mergedController.signal.aborted) mergedController.abort(source.reason);
1383
+ };
1384
+ for (const source of signals) {
1385
+ if (source.aborted) {
1386
+ forwardAbort(source);
1387
+ break;
1388
+ }
1389
+ const listener = () => forwardAbort(source);
1390
+ source.addEventListener("abort", listener, { once: true });
1391
+ signalCleanups.push(() => source.removeEventListener("abort", listener));
1392
+ }
1393
+ signal = mergedController.signal;
1394
+ }
1395
+ let timer;
1396
+ const timeoutPromise = timeoutController && timeout !== void 0 ? new Promise((_, reject) => {
1397
+ timer = setTimeout(() => {
1398
+ const error = new ActionTimeoutError(String(action), timeout);
1399
+ timeoutController.abort(error);
1400
+ timeoutCallbacks.forEach((callback) => callback(error));
1401
+ reject(error);
1402
+ }, timeout);
1403
+ }) : void 0;
1404
+ return {
1405
+ options: {
1406
+ ...options,
1407
+ signal
1408
+ },
1409
+ timeoutPromise,
1410
+ onTimeout: (callback) => timeoutCallbacks.add(callback),
1411
+ cleanup: () => {
1412
+ if (timer !== void 0) clearTimeout(timer);
1413
+ timeoutCallbacks.clear();
1414
+ },
1415
+ cleanupSignals: () => signalCleanups.forEach((cleanup) => cleanup())
1416
+ };
1417
+ }
1418
+ /** Expose timeout failure while allowing the queued operation to drain safely. */
1419
+ raceWithTimeout(operation, scope, dispatchHandlerPromises) {
1420
+ const exposed = scope.timeoutPromise ? Promise.race([operation, scope.timeoutPromise]) : operation;
1421
+ const cleanupAfterStartedHandlers = () => {
1422
+ scope.cleanup();
1423
+ this.cleanupSignalsAfterStartedHandlers(scope.cleanupSignals, dispatchHandlerPromises);
1424
+ };
1425
+ exposed.then(cleanupAfterStartedHandlers, cleanupAfterStartedHandlers);
1426
+ return exposed;
1427
+ }
1428
+ cleanupSignalsAfterStartedHandlers(cleanup, dispatchHandlerPromises) {
1429
+ const handlersStillRunning = [...dispatchHandlerPromises];
1430
+ if (handlersStillRunning.length === 0) {
1431
+ cleanup();
1432
+ return;
1433
+ }
1434
+ Promise.allSettled(handlersStillRunning).then(cleanup);
1435
+ }
1436
+ /** Invoke the configured error handler without allowing it to replace the dispatch error. */
1437
+ invokeErrorHandler(error, action, payload, options, attempts) {
1438
+ const errorHandler = this.registryConfig?.errorHandler;
1439
+ if (!errorHandler) return;
1440
+ const normalizedError = error instanceof Error ? error : new Error(String(error));
1441
+ try {
1442
+ const handlerResult = errorHandler(normalizedError, {
1443
+ action: String(action),
1444
+ payload,
1445
+ options,
1446
+ attempts,
1447
+ phase: normalizedError instanceof ActionTimeoutError ? "timeout" : normalizedError instanceof ActionValidationError ? "validation" : "execution"
1448
+ });
1449
+ if (handlerResult && typeof handlerResult.then === "function") Promise.resolve(handlerResult).catch((handlerError) => {
1450
+ this.log("Global async error handler failed", handlerError, "warn");
1451
+ });
1452
+ } catch (handlerError) {
1453
+ this.log("Global error handler failed", handlerError, "warn");
1454
+ }
1455
+ }
1456
+ /**
1457
+ * Validate an action payload against the configured schema.
1458
+ * Shared by all dispatch paths so result collection cannot bypass validation.
1459
+ */
1460
+ validatePayload(action, payload) {
1461
+ if (!this.registryConfig?.schema || this.registryConfig.validateOnDispatch === false) return;
1462
+ const actionName = String(action);
1463
+ const actionSchema = this.registryConfig.schema[actionName];
1464
+ if (!actionSchema) return;
1465
+ let result;
1466
+ try {
1467
+ result = actionSchema.safeParse(payload);
1468
+ } catch (error) {
1469
+ throw new ActionValidationError(actionName, error);
1470
+ }
1471
+ if (result.success) return {
1472
+ passed: true,
1473
+ errors: []
1474
+ };
1475
+ const mode = this.registryConfig.validationMode ?? "strict";
1476
+ if (mode === "strict") throw new ActionValidationError(actionName, result.error);
1477
+ if (mode === "warn") {
1478
+ console.warn(`Action "${actionName}" payload validation failed:`, result.error.message);
1479
+ this.log(`Validation warning for action '${actionName}'`, { issues: result.error.issues }, "warn");
1480
+ }
1481
+ return {
1482
+ passed: false,
1483
+ errors: result.error.issues.map((issue) => issue.message)
1484
+ };
1485
+ }
1486
+ createAbortedExecutionResult(startTime, handlersSkipped = 0, validation) {
1487
+ const endTime = Date.now();
1488
+ return {
1489
+ success: false,
1490
+ aborted: true,
1491
+ abortReason: "Action dispatch aborted by signal",
1492
+ terminated: false,
1493
+ validation,
1494
+ result: void 0,
1495
+ successResults: [],
1496
+ results: [],
1497
+ failedResults: [],
1498
+ execution: {
1499
+ duration: endTime - startTime,
1500
+ handlersExecuted: 0,
1501
+ handlersSkipped,
1502
+ handlersFailed: 0,
1503
+ startTime,
1504
+ endTime
1505
+ },
1506
+ handlers: [],
1507
+ errors: []
1508
+ };
1509
+ }
1222
1510
  /**
1223
1511
  * 🆕 실제 디스패치 작업 수행 (큐에서 호출됨)
1224
1512
  */
1225
- async _performDispatch(action, payload, options) {
1513
+ async _performDispatch(action, payload, options, skipGuards, executedHandlers, dispatchHandlerPromises) {
1226
1514
  this.log(`Starting dispatch for action '${String(action)}'`, {
1227
1515
  hasPayload: payload !== void 0,
1228
1516
  payloadType: payload?.constructor?.name || typeof payload,
@@ -1230,24 +1518,11 @@ var ActionRegister = class {
1230
1518
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
1231
1519
  });
1232
1520
  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
1521
  const [effectiveSignal, autoAbortController, cleanup] = this.createAbortSignal(options);
1248
1522
  if (options?.autoAbort?.onControllerCreated && autoAbortController) options.autoAbort.onControllerCreated(autoAbortController);
1249
1523
  if (effectiveSignal?.aborted) {
1250
1524
  this.log(`Dispatch aborted before execution for '${String(action)}'`);
1525
+ cleanup();
1251
1526
  return;
1252
1527
  }
1253
1528
  const pipeline = this.pipelines.get(action);
@@ -1265,6 +1540,7 @@ var ActionRegister = class {
1265
1540
  console.warn("📋 Available actions:", Array.from(this.pipelines.keys()));
1266
1541
  }
1267
1542
  this.log(`No handlers found for action '${String(action)}', dispatch cancelled`, {}, "warn");
1543
+ cleanup();
1268
1544
  return;
1269
1545
  }
1270
1546
  const filteredHandlers = options?.filter ? this.filterHandlers(pipeline, options.filter) : pipeline;
@@ -1285,17 +1561,32 @@ var ActionRegister = class {
1285
1561
  break;
1286
1562
  }
1287
1563
  }
1288
- if (debounceMs !== void 0) {
1289
- if (!await this.actionGuard.debounce(actionKey, debounceMs)) return;
1564
+ if (!skipGuards && debounceMs !== void 0) {
1565
+ if (!await this.actionGuard.debounce(actionKey, debounceMs)) {
1566
+ cleanup();
1567
+ return;
1568
+ }
1290
1569
  }
1291
- if (throttleMs !== void 0) {
1292
- if (!this.actionGuard.throttle(actionKey, throttleMs)) return;
1570
+ if (!skipGuards && throttleMs !== void 0) {
1571
+ if (!this.actionGuard.throttle(actionKey, throttleMs)) {
1572
+ cleanup();
1573
+ return;
1574
+ }
1575
+ }
1576
+ if (effectiveSignal?.aborted) {
1577
+ this.log(`Dispatch aborted during guard processing for '${String(action)}'`);
1578
+ cleanup();
1579
+ return;
1293
1580
  }
1294
1581
  const currentExecutionMode = options?.executionMode || this.actionExecutionModes.get(action) || this.executionMode;
1295
1582
  const context = {
1296
1583
  action: String(action),
1297
1584
  payload,
1298
- handlers: filteredHandlers,
1585
+ handlers: [...filteredHandlers],
1586
+ executedHandlers: [],
1587
+ deferOnceCleanup: true,
1588
+ signal: effectiveSignal ?? this.lifecycleController.signal,
1589
+ trackHandlerPromise: (promise) => this.trackHandlerPromise(promise, dispatchHandlerPromises),
1299
1590
  aborted: false,
1300
1591
  abortReason: void 0,
1301
1592
  currentIndex: 0,
@@ -1309,17 +1600,19 @@ var ActionRegister = class {
1309
1600
  };
1310
1601
  const abortHandler = effectiveSignal ? () => {
1311
1602
  context.aborted = true;
1312
- context.abortReason = "Action dispatch aborted by signal";
1603
+ context.abortReason = typeof effectiveSignal.reason === "string" ? effectiveSignal.reason : "Action dispatch aborted by signal";
1313
1604
  } : void 0;
1314
- if (effectiveSignal && abortHandler) effectiveSignal.addEventListener("abort", abortHandler);
1605
+ if (effectiveSignal && abortHandler) effectiveSignal.addEventListener("abort", abortHandler, { once: true });
1315
1606
  try {
1316
- await this.executePipeline(context, autoAbortController, options?.autoAbort);
1607
+ await this.executePipeline(context, dispatchHandlerPromises, autoAbortController, options?.autoAbort);
1317
1608
  this.log(`Pipeline execution succeeded for ${String(action)}`);
1318
1609
  } catch (error) {
1319
1610
  this.log(`Pipeline execution failed for ${String(action)}`, error, "error");
1320
1611
  throw error;
1321
1612
  } finally {
1322
- cleanup();
1613
+ executedHandlers?.push(...context.executedHandlers ?? []);
1614
+ if (effectiveSignal && abortHandler) effectiveSignal.removeEventListener("abort", abortHandler);
1615
+ this.cleanupSignalsAfterStartedHandlers(cleanup, dispatchHandlerPromises);
1323
1616
  }
1324
1617
  }
1325
1618
  /**
@@ -1335,30 +1628,57 @@ var ActionRegister = class {
1335
1628
  *
1336
1629
  * @public
1337
1630
  */
1338
- async dispatchWithResult(action, payload, options) {
1631
+ dispatchWithResult(action, payload, options) {
1632
+ if (this.lifecycleState !== "active") return this.rejectedLifecyclePromise();
1633
+ const timeoutScope = this.createTimeoutScope(action, options);
1634
+ const dispatchHandlerPromises = /* @__PURE__ */ new Set();
1635
+ const attemptState = { count: 0 };
1636
+ let validation;
1637
+ const operation = async () => {
1638
+ if (!timeoutScope.options?.signal?.aborted) validation = this.validatePayload(action, payload);
1639
+ return this.executeWithRetry(async () => {
1640
+ const executedHandlers = [];
1641
+ try {
1642
+ return await this._performDispatchWithResult(action, payload, timeoutScope.options, validation, attemptState.count > 1, executedHandlers, dispatchHandlerPromises);
1643
+ } finally {
1644
+ this.cleanupOneTimeHandlers(action, executedHandlers, dispatchHandlerPromises);
1645
+ }
1646
+ }, timeoutScope.options, attemptState, (result) => !result.success && !result.aborted && this.getHandlerCount(action) > 0, () => this.getHandlerCount(action) > 0);
1647
+ };
1648
+ const shouldQueue = !timeoutScope.options?.immediate && Boolean(this.dispatchQueue) && timeoutScope.options?.queuePriority !== void 0;
1649
+ let dispatchPromise;
1650
+ this.dispatchConstructionDepth += 1;
1651
+ try {
1652
+ if (shouldQueue) {
1653
+ const queued = this.dispatchQueue.enqueueWithHandle(operation, timeoutScope.options.queuePriority);
1654
+ timeoutScope.onTimeout((error) => queued.cancel(error));
1655
+ dispatchPromise = queued.promise;
1656
+ } else dispatchPromise = operation();
1657
+ this.trackDispatchPromise(dispatchPromise);
1658
+ } finally {
1659
+ this.dispatchConstructionDepth -= 1;
1660
+ }
1661
+ const observedPromise = this.raceWithTimeout(dispatchPromise, timeoutScope, dispatchHandlerPromises).then((result) => {
1662
+ if (!result.success && !result.aborted) {
1663
+ const terminalError = result.errors[result.errors.length - 1]?.error ?? /* @__PURE__ */ new Error(`Action "${String(action)}" failed`);
1664
+ this.invokeErrorHandler(terminalError, action, payload, options, attemptState.count);
1665
+ }
1666
+ return result;
1667
+ }, (error) => {
1668
+ this.invokeErrorHandler(error, action, payload, options, attemptState.count);
1669
+ throw error;
1670
+ });
1671
+ observedPromise.catch(() => {});
1672
+ return observedPromise;
1673
+ }
1674
+ async _performDispatchWithResult(action, payload, options, validation, skipGuards, executedHandlers, dispatchHandlerPromises) {
1339
1675
  const _startTime = Date.now();
1340
1676
  const [effectiveSignal, autoAbortController, cleanup] = this.createAbortSignal(options);
1341
1677
  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
- };
1678
+ if (effectiveSignal?.aborted) {
1679
+ cleanup();
1680
+ return this.createAbortedExecutionResult(_startTime, 0, validation);
1681
+ }
1362
1682
  const pipeline = this.pipelines.get(action);
1363
1683
  if (!pipeline || pipeline.length === 0) {
1364
1684
  const warningMessage = `⚠️ Action '${String(action)}' has no registered handlers. This action will be ignored.`;
@@ -1367,11 +1687,13 @@ var ActionRegister = class {
1367
1687
  console.warn("💡 Tip: Register a handler using registry.register() before dispatching this action.");
1368
1688
  console.warn("📋 Available actions:", Array.from(this.pipelines.keys()));
1369
1689
  }
1690
+ cleanup();
1370
1691
  return {
1371
1692
  success: true,
1372
1693
  aborted: false,
1373
1694
  abortReason: void 0,
1374
1695
  terminated: false,
1696
+ validation,
1375
1697
  result: void 0,
1376
1698
  successResults: [],
1377
1699
  results: [],
@@ -1390,13 +1712,27 @@ var ActionRegister = class {
1390
1712
  }
1391
1713
  const filteredHandlers = options?.filter ? this.filterHandlers(pipeline, options.filter) : pipeline;
1392
1714
  const actionKey = String(action);
1393
- const guardResult = await this.applyActionGuardControlsWithResult(actionKey, filteredHandlers, options, _startTime, pipeline.length);
1394
- if (guardResult) return guardResult;
1715
+ const guardResult = skipGuards ? null : await this.applyActionGuardControlsWithResult(actionKey, filteredHandlers, options, _startTime, pipeline.length);
1716
+ if (effectiveSignal?.aborted) {
1717
+ cleanup();
1718
+ return this.createAbortedExecutionResult(_startTime, pipeline.length, validation);
1719
+ }
1720
+ if (guardResult) {
1721
+ cleanup();
1722
+ return {
1723
+ ...guardResult,
1724
+ validation
1725
+ };
1726
+ }
1395
1727
  const currentExecutionMode = options?.executionMode || this.actionExecutionModes.get(action) || this.executionMode;
1396
1728
  const context = {
1397
1729
  action: String(action),
1398
1730
  payload,
1399
- handlers: filteredHandlers,
1731
+ handlers: [...filteredHandlers],
1732
+ executedHandlers: [],
1733
+ deferOnceCleanup: true,
1734
+ signal: effectiveSignal ?? this.lifecycleController.signal,
1735
+ trackHandlerPromise: (promise) => this.trackHandlerPromise(promise, dispatchHandlerPromises),
1400
1736
  aborted: false,
1401
1737
  abortReason: void 0,
1402
1738
  currentIndex: 0,
@@ -1422,12 +1758,12 @@ var ActionRegister = class {
1422
1758
  });
1423
1759
  const abortHandler = effectiveSignal ? () => {
1424
1760
  context.aborted = true;
1425
- context.abortReason = "Action dispatch aborted by signal";
1761
+ context.abortReason = typeof effectiveSignal.reason === "string" ? effectiveSignal.reason : "Action dispatch aborted by signal";
1426
1762
  } : void 0;
1427
- if (effectiveSignal && abortHandler) effectiveSignal.addEventListener("abort", abortHandler);
1763
+ if (effectiveSignal && abortHandler) effectiveSignal.addEventListener("abort", abortHandler, { once: true });
1428
1764
  let errors = [];
1429
1765
  try {
1430
- await this.executePipeline(context, autoAbortController, options?.autoAbort);
1766
+ await this.executePipeline(context, dispatchHandlerPromises, autoAbortController, options?.autoAbort);
1431
1767
  errors = context.collectedErrors || [];
1432
1768
  const executedCount = Math.min(context.currentIndex + (context.aborted ? 0 : 1), filteredHandlers.length);
1433
1769
  for (let i = 0; i < executedCount; i++) {
@@ -1453,7 +1789,9 @@ var ActionRegister = class {
1453
1789
  if (handlerResult) handlerResult.executed = true;
1454
1790
  }
1455
1791
  } finally {
1456
- cleanup();
1792
+ executedHandlers.push(...context.executedHandlers ?? []);
1793
+ if (effectiveSignal && abortHandler) effectiveSignal.removeEventListener("abort", abortHandler);
1794
+ this.cleanupSignalsAfterStartedHandlers(cleanup, dispatchHandlerPromises);
1457
1795
  }
1458
1796
  const endTime = Date.now();
1459
1797
  const processedResult = this.processResults(context, options?.result);
@@ -1463,11 +1801,12 @@ var ActionRegister = class {
1463
1801
  error: err.error,
1464
1802
  expectedType: typeof processedResult
1465
1803
  }));
1466
- const executionResult = {
1804
+ return {
1467
1805
  success: !executionError && !context.aborted,
1468
1806
  aborted: context.aborted,
1469
1807
  abortReason: context.abortReason,
1470
1808
  terminated: context.terminated,
1809
+ validation,
1471
1810
  result: processedResult,
1472
1811
  successResults,
1473
1812
  results: context.results,
@@ -1488,9 +1827,6 @@ var ActionRegister = class {
1488
1827
  severity: "non-blocking"
1489
1828
  }))
1490
1829
  };
1491
- /** Clean up one-time handlers after execution */
1492
- this.cleanupOneTimeHandlers(action, context.handlers);
1493
- return executionResult;
1494
1830
  }
1495
1831
  /**
1496
1832
  * 🔧 Unified method for dispatchWithResult that returns ExecutionResult on guard rejection
@@ -1559,26 +1895,12 @@ var ActionRegister = class {
1559
1895
  return null;
1560
1896
  }
1561
1897
  /**
1562
- * 🔧 Generate optimized cache key for filter options
1563
- */
1564
- generateFilterCacheKey(filterOptions) {
1565
- if (!filterOptions) return "no-filter";
1566
- const parts = [];
1567
- if (filterOptions.handlerIds?.length) parts.push(`h:${filterOptions.handlerIds.slice().sort().join(",")}`);
1568
- if (filterOptions.excludeHandlerIds?.length) parts.push(`e:${filterOptions.excludeHandlerIds.slice().sort().join(",")}`);
1569
- if (filterOptions.priority) {
1570
- const { min, max } = filterOptions.priority;
1571
- if (min !== void 0 || max !== void 0) parts.push(`p:${min ?? "*"}-${max ?? "*"}`);
1572
- }
1573
- if (filterOptions.custom) return "custom-" + Date.now() + Math.random();
1574
- return parts.length > 0 ? parts.join("|") : "no-filter";
1575
- }
1576
- /**
1577
1898
  * 🔧 Create or reuse PipelineController from pool for better performance
1578
1899
  */
1579
1900
  getControllerFromPool(context, autoAbortController, autoAbortOptions) {
1580
1901
  let controller = this.controllerPool.pop();
1581
1902
  if (!controller) controller = {};
1903
+ controller.signal = context.signal ?? this.lifecycleController.signal;
1582
1904
  controller.abort = (reason) => {
1583
1905
  context.aborted = true;
1584
1906
  context.abortReason = reason;
@@ -1612,12 +1934,6 @@ var ActionRegister = class {
1612
1934
  };
1613
1935
  return controller;
1614
1936
  }
1615
- /**
1616
- * 🔧 Return controller to pool for reuse
1617
- */
1618
- returnControllerToPool(controller) {
1619
- if (this.controllerPool.length < this.maxControllerPoolSize) this.controllerPool.push(controller);
1620
- }
1621
1937
  filterHandlers(handlers, filterOptions) {
1622
1938
  if (!filterOptions) return handlers;
1623
1939
  const handlerIdSet = filterOptions.handlerIds ? new Set(filterOptions.handlerIds) : null;
@@ -1625,7 +1941,7 @@ var ActionRegister = class {
1625
1941
  return handlers.filter((registration) => {
1626
1942
  const config = registration.config;
1627
1943
  if (handlerIdSet && !handlerIdSet.has(config.id)) return false;
1628
- if (excludeIdSet && excludeIdSet.has(config.id)) return false;
1944
+ if (excludeIdSet?.has(config.id)) return false;
1629
1945
  if (filterOptions.priority) {
1630
1946
  const priority = config.priority;
1631
1947
  if (filterOptions.priority.min !== void 0 && priority < filterOptions.priority.min) return false;
@@ -1657,7 +1973,7 @@ var ActionRegister = class {
1657
1973
  return limitedResults[limitedResults.length - 1];
1658
1974
  }
1659
1975
  }
1660
- async executePipeline(context, autoAbortController, autoAbortOptions) {
1976
+ async executePipeline(context, dispatchHandlerPromises, autoAbortController, autoAbortOptions) {
1661
1977
  const createController = (_registration, _index) => {
1662
1978
  return this.getControllerFromPool(context, autoAbortController, autoAbortOptions);
1663
1979
  };
@@ -1673,28 +1989,27 @@ var ActionRegister = class {
1673
1989
  break;
1674
1990
  default: throw new Error(`Unknown execution mode: ${context.executionMode}`);
1675
1991
  }
1676
- this.cleanupOneTimeHandlers(context.action, context.handlers);
1992
+ if (!context.deferOnceCleanup) this.cleanupOneTimeHandlers(context.action, context.executedHandlers ?? [], dispatchHandlerPromises);
1677
1993
  }
1678
- cleanupOneTimeHandlers(action, executedHandlers) {
1679
- const pipeline = this.pipelines.get(action);
1680
- if (!pipeline) return;
1994
+ cleanupOneTimeHandlers(action, executedHandlers, dispatchHandlerPromises) {
1681
1995
  const oneTimeHandlers = executedHandlers.filter((reg) => reg.config.once);
1682
1996
  if (oneTimeHandlers.length === 0) return;
1997
+ const handlersStillRunning = [...dispatchHandlerPromises];
1998
+ const shouldDeferCleanup = handlersStillRunning.length > 0;
1683
1999
  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)}`, {
2000
+ if (this.removeRegistration(action, registration, !shouldDeferCleanup)) {
2001
+ if (shouldDeferCleanup && typeof registration.config.cleanup === "function") {
2002
+ const cleanupPromise = Promise.allSettled(handlersStillRunning).then(() => {
2003
+ this.runRegistrationCleanup(action, registration);
2004
+ });
2005
+ this.trackGlobalHandlerPromise(cleanupPromise).catch(() => {});
2006
+ }
2007
+ this.log(`One-time handler removed: ${String(action)}`, {
1688
2008
  handlerId: registration.id,
1689
- remainingHandlers: pipeline.length,
1690
- registry: this.name
2009
+ remainingHandlers: this.getHandlerCount(action)
1691
2010
  });
1692
2011
  }
1693
2012
  });
1694
- if (pipeline.length === 0) {
1695
- this.pipelines.delete(action);
1696
- this.lastRegisteredTimestamps.delete(action);
1697
- }
1698
2013
  }
1699
2014
  /**
1700
2015
  * Get the number of registered handlers for an action
@@ -1747,8 +2062,13 @@ var ActionRegister = class {
1747
2062
  * @public
1748
2063
  */
1749
2064
  clearAction(action) {
2065
+ const pipeline = this.pipelines.get(action);
2066
+ if (pipeline) [...pipeline].forEach((registration) => {
2067
+ this.removeRegistration(action, registration);
2068
+ });
1750
2069
  this.pipelines.delete(action);
1751
2070
  this.lastRegisteredTimestamps.delete(action);
2071
+ this.actionGuard.clearGuards(String(action));
1752
2072
  }
1753
2073
  /**
1754
2074
  * Remove all handlers for all actions
@@ -1758,8 +2078,13 @@ var ActionRegister = class {
1758
2078
  * @public
1759
2079
  */
1760
2080
  clearAll() {
2081
+ [...this.pipelines.keys()].forEach((action) => {
2082
+ this.clearAction(action);
2083
+ });
1761
2084
  this.pipelines.clear();
1762
2085
  this.lastRegisteredTimestamps.clear();
2086
+ this.unregisterFunctions.clear();
2087
+ this.actionGuard.clearAll();
1763
2088
  }
1764
2089
  /**
1765
2090
  * Get the name of this action register
@@ -1888,29 +2213,36 @@ var ActionRegister = class {
1888
2213
  */
1889
2214
  createUnregisterFunction(action, handlerId, registration) {
1890
2215
  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
- }
2216
+ if (this.removeRegistration(action, registration)) this.log(`Handler unregistered: ${String(action)}`, {
2217
+ handlerId,
2218
+ remainingHandlers: this.getHandlerCount(action),
2219
+ actionRemoved: !this.pipelines.has(action)
2220
+ });
1912
2221
  };
1913
2222
  }
2223
+ /** Remove a registration and release every resource owned by it exactly once. */
2224
+ removeRegistration(action, registration, runCleanup = true) {
2225
+ const pipeline = this.pipelines.get(action);
2226
+ if (!pipeline) return false;
2227
+ const index = pipeline.indexOf(registration);
2228
+ if (index === -1) return false;
2229
+ pipeline.splice(index, 1);
2230
+ this.unregisterFunctions.delete(registration.id);
2231
+ if (runCleanup) this.runRegistrationCleanup(action, registration);
2232
+ if (pipeline.length === 0) {
2233
+ this.pipelines.delete(action);
2234
+ this.lastRegisteredTimestamps.delete(action);
2235
+ }
2236
+ return true;
2237
+ }
2238
+ runRegistrationCleanup(action, registration) {
2239
+ if (!registration.config.cleanup) return;
2240
+ try {
2241
+ registration.config.cleanup();
2242
+ } catch (cleanupError) {
2243
+ this.log(`Cleanup error while removing handler: ${String(action)}`, cleanupError, "warn");
2244
+ }
2245
+ }
1914
2246
  /**
1915
2247
  * Gets the total count of registered unregister functions
1916
2248
  *
@@ -1930,29 +2262,77 @@ var ActionRegister = class {
1930
2262
  hasUnregisterFunction(handlerId) {
1931
2263
  return this.unregisterFunctions.has(handlerId);
1932
2264
  }
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");
2265
+ /** Reject queued dispatches without releasing registered handlers. */
2266
+ cancelPendingDispatches() {
2267
+ this.dispatchQueue?.clear({ rejectPending: true });
2268
+ }
2269
+ beginShutdown() {
2270
+ if (this.destroyAsyncPromise) return this.destroyAsyncPromise;
2271
+ if (this.lifecycleState === "destroyed") {
2272
+ this.destroyAsyncPromise = Promise.resolve();
2273
+ return this.destroyAsyncPromise;
1947
2274
  }
1948
- this.pipelines.clear();
1949
- this.lastRegisteredTimestamps.clear();
2275
+ this.lifecycleState = "closing";
2276
+ const shutdownError = new ActionRegisterDestroyedError(this.name, "closing");
2277
+ let resolveShutdown;
2278
+ let rejectShutdown;
2279
+ this.destroyAsyncPromise = new Promise((resolve, reject) => {
2280
+ resolveShutdown = resolve;
2281
+ rejectShutdown = reject;
2282
+ });
2283
+ if (!this.lifecycleController.signal.aborted) this.lifecycleController.abort(shutdownError);
1950
2284
  this.actionGuard.destroy();
1951
- this.dispatchQueue?.clear?.();
2285
+ this.dispatchQueue?.clear({
2286
+ rejectPending: true,
2287
+ reason: shutdownError
2288
+ });
2289
+ if (this.dispatchConstructionDepth === 0 && this.activeDispatches.size === 0 && this.activeHandlerPromises.size === 0) {
2290
+ this.finalizeDestroy();
2291
+ resolveShutdown();
2292
+ return this.destroyAsyncPromise;
2293
+ }
2294
+ const drainAndFinalize = async () => {
2295
+ while (this.activeDispatches.size > 0 || this.activeHandlerPromises.size > 0) await Promise.allSettled([...this.activeDispatches, ...this.activeHandlerPromises]);
2296
+ this.finalizeDestroy();
2297
+ };
2298
+ Promise.resolve().then(drainAndFinalize).then(resolveShutdown, rejectShutdown);
2299
+ this.destroyAsyncPromise.catch((error) => {
2300
+ this.log("ActionRegister async destroy failed", error, "warn");
2301
+ });
2302
+ return this.destroyAsyncPromise;
2303
+ }
2304
+ finalizeDestroy() {
2305
+ if (this.lifecycleState === "destroyed") return;
2306
+ this.clearAll();
1952
2307
  this.actionExecutionModes.clear();
1953
2308
  this.controllerPool.length = 0;
2309
+ this.lifecycleState = "destroyed";
1954
2310
  this.log("ActionRegister destroyed");
1955
2311
  }
2312
+ /**
2313
+ * 🆕 Destroy method for comprehensive cleanup
2314
+ *
2315
+ * Begins terminal cleanup of pipelines, guards, queues, and statistics. Cleanup
2316
+ * remains synchronous when no work has started; otherwise active handlers drain
2317
+ * in the background. Use destroyAsync() when completion must be observed.
2318
+ *
2319
+ * @public
2320
+ */
2321
+ destroy() {
2322
+ this.beginShutdown();
2323
+ }
2324
+ /**
2325
+ * Begin terminal shutdown and resolve after all started handlers have settled
2326
+ * and their registered cleanup functions have run.
2327
+ *
2328
+ * Repeated calls return the same promise. New registrations and dispatches are
2329
+ * rejected as soon as shutdown begins.
2330
+ *
2331
+ * @public
2332
+ */
2333
+ destroyAsync() {
2334
+ return this.beginShutdown();
2335
+ }
1956
2336
  };
1957
2337
 
1958
2338
  //#endregion
@@ -2041,12 +2421,18 @@ function createActionHandler(registry, action, handler, config) {
2041
2421
  let currentUnregister;
2042
2422
  let isRegistered = false;
2043
2423
  return {
2424
+ /**
2425
+ * Register the handler and return cleanup function
2426
+ */
2044
2427
  register() {
2045
2428
  if (isRegistered && currentUnregister) currentUnregister();
2046
2429
  currentUnregister = registry.register(action, handler, finalConfig);
2047
2430
  isRegistered = true;
2048
2431
  return currentUnregister;
2049
2432
  },
2433
+ /**
2434
+ * Unregister the handler if currently registered
2435
+ */
2050
2436
  unregister() {
2051
2437
  if (isRegistered && currentUnregister) {
2052
2438
  currentUnregister();
@@ -2054,6 +2440,9 @@ function createActionHandler(registry, action, handler, config) {
2054
2440
  isRegistered = false;
2055
2441
  }
2056
2442
  },
2443
+ /**
2444
+ * Register and return cleanup function (React useEffect pattern)
2445
+ */
2057
2446
  registerWithCleanup() {
2058
2447
  const unregisterFn = this.register();
2059
2448
  return () => {
@@ -2070,18 +2459,33 @@ function createActionHandler(registry, action, handler, config) {
2070
2459
  * Provides debugging and development helpers specifically for React environments.
2071
2460
  */
2072
2461
  const ReactDevUtils = {
2462
+ /**
2463
+ * Enable detailed React integration debugging
2464
+ */
2073
2465
  enableDebugMode() {
2074
2466
  if (typeof window !== "undefined") window.__CONTEXT_ACTION_REACT_DEBUG__ = true;
2075
2467
  },
2468
+ /**
2469
+ * Disable React integration debugging
2470
+ */
2076
2471
  disableDebugMode() {
2077
2472
  if (typeof window !== "undefined") window.__CONTEXT_ACTION_REACT_DEBUG__ = false;
2078
2473
  },
2474
+ /**
2475
+ * Check if React debug mode is enabled
2476
+ */
2079
2477
  isDebugMode() {
2080
2478
  return typeof window !== "undefined" && Boolean(window.__CONTEXT_ACTION_REACT_DEBUG__);
2081
2479
  },
2480
+ /**
2481
+ * Log React-specific debugging information
2482
+ */
2082
2483
  log(component, action, message, data) {
2083
2484
  if (this.isDebugMode()) console.log(`🎯 [React-ActionRegister] [${component}] ${action}: ${message}`, data || "");
2084
2485
  },
2486
+ /**
2487
+ * Get React integration statistics
2488
+ */
2085
2489
  getStats(registry) {
2086
2490
  const registryInfo = registry.getRegistryInfo();
2087
2491
  let reactHandlers = 0;
@@ -2113,7 +2517,7 @@ var ReactActionError = class ReactActionError extends Error {
2113
2517
  this.payload = payload;
2114
2518
  this.handlerId = handlerId;
2115
2519
  this.timestamp = Date.now();
2116
- if (originalError && originalError.stack) this.stack = originalError.stack;
2520
+ if (originalError?.stack) this.stack = originalError.stack;
2117
2521
  }
2118
2522
  /**
2119
2523
  * Create a React Error Boundary compatible error
@@ -2132,154 +2536,19 @@ function isReactActionError(error) {
2132
2536
  return error instanceof ReactActionError;
2133
2537
  }
2134
2538
 
2135
- //#endregion
2136
- //#region src/action-schema.ts
2137
- /**
2138
- * Zod 스키마를 JSON Schema로 변환 (Zod 4 네이티브 API)
2139
- *
2140
- * @param schema - Zod 스키마
2141
- * @returns JSON Schema (draft-7)
2142
- */
2143
- function zodToJsonSchema(schema, zodModule) {
2144
- return zodModule.toJSONSchema(schema, {
2145
- target: "draft-7",
2146
- metadata: zodModule.globalRegistry
2147
- });
2148
- }
2149
- /**
2150
- * Zod 스키마 기반 Action 정의
2151
- *
2152
- * defineTool 패턴을 기반으로 context-action에 맞게 구현:
2153
- * - Single Source of Truth: Zod 스키마로 타입 + 검증 + 메타데이터 통합
2154
- * - 런타임 검증: validate(), safeParse()
2155
- * - Tool Chain 호환: toMCP(), toOpenAI(), toAnthropic()
2156
- *
2157
- * @param options - Action 정의 옵션
2158
- * @param zodModule - Zod 모듈 (peerDependency로 주입)
2159
- * @returns UnifiedAction 인스턴스
2160
- *
2161
- * @example
2162
- * ```typescript
2163
- * import { z } from 'zod';
2164
- * import { defineAction } from '@context-action/core';
2165
- *
2166
- * const updateUserAction = defineAction({
2167
- * name: 'updateUser',
2168
- * description: 'Update user profile',
2169
- * parameters: z.object({
2170
- * id: z.string().min(1).meta({ description: 'User ID' }),
2171
- * name: z.string().min(2).max(50).meta({ description: 'User name' }),
2172
- * email: z.string().email().optional(),
2173
- * }),
2174
- * }, z);
2175
- *
2176
- * // 검증
2177
- * const validated = updateUserAction.validate({ id: '123', name: 'John' });
2178
- *
2179
- * // Tool chain 변환
2180
- * const mcpTool = updateUserAction.toMCP();
2181
- * ```
2182
- */
2183
- function defineAction(options, zodModule) {
2184
- const { name, description, parameters } = options;
2185
- const jsonSchema = zodToJsonSchema(parameters, zodModule);
2186
- return {
2187
- name,
2188
- description,
2189
- zodSchema: parameters,
2190
- jsonSchema,
2191
- validate: (payload) => {
2192
- return parameters.parse(payload);
2193
- },
2194
- safeParse: (payload) => {
2195
- return parameters.safeParse(payload);
2196
- },
2197
- toJSONSchema: () => jsonSchema,
2198
- toMCP: () => ({
2199
- name,
2200
- description,
2201
- inputSchema: jsonSchema
2202
- }),
2203
- toOpenAI: () => ({
2204
- type: "function",
2205
- function: {
2206
- name,
2207
- description,
2208
- parameters: {
2209
- type: "object",
2210
- properties: jsonSchema.properties ?? {},
2211
- required: jsonSchema.required
2212
- }
2213
- }
2214
- }),
2215
- toAnthropic: () => ({
2216
- name,
2217
- description,
2218
- input_schema: jsonSchema
2219
- })
2220
- };
2221
- }
2222
- /**
2223
- * 다중 Action 스키마 생성
2224
- *
2225
- * 여러 defineAction을 묶어서 ActionSchemaMap 생성
2226
- *
2227
- * @param actions - UnifiedAction 맵
2228
- * @returns ActionSchemaMap
2229
- *
2230
- * @example
2231
- * ```typescript
2232
- * const userActionSchema = createActionSchema({
2233
- * updateUser: defineAction({ ... }, z),
2234
- * deleteUser: defineAction({ ... }, z),
2235
- * });
2236
- *
2237
- * type UserActions = InferActionPayloadMap<typeof userActionSchema>;
2238
- * ```
2239
- */
2240
- function createActionSchema(actions) {
2241
- return actions;
2242
- }
2243
- /**
2244
- * Zod 모듈을 바인딩한 defineAction 팩토리 생성
2245
- *
2246
- * 매번 z 모듈을 전달하지 않아도 되도록 팩토리 패턴 제공
2247
- *
2248
- * @param zodModule - Zod 모듈
2249
- * @returns defineAction 함수 (z 바인딩됨)
2250
- *
2251
- * @example
2252
- * ```typescript
2253
- * import { z } from 'zod';
2254
- * import { createActionFactory } from '@context-action/core';
2255
- *
2256
- * const defineAction = createActionFactory(z);
2257
- *
2258
- * const updateUser = defineAction({
2259
- * name: 'updateUser',
2260
- * parameters: z.object({ id: z.string() }),
2261
- * });
2262
- * ```
2263
- */
2264
- function createActionFactory(zodModule) {
2265
- return (options) => {
2266
- return defineAction(options, zodModule);
2267
- };
2268
- }
2269
-
2270
2539
  //#endregion
2271
2540
  exports.ActionGuard = ActionGuard;
2272
2541
  exports.ActionRegister = ActionRegister;
2542
+ exports.ActionRegisterDestroyedError = ActionRegisterDestroyedError;
2543
+ exports.ActionTimeoutError = ActionTimeoutError;
2273
2544
  exports.ActionValidationError = ActionValidationError;
2274
2545
  exports.ReactActionError = ReactActionError;
2275
2546
  exports.ReactDevUtils = ReactDevUtils;
2276
- exports.createActionFactory = createActionFactory;
2277
2547
  exports.createActionHandler = createActionHandler;
2278
- exports.createActionSchema = createActionSchema;
2279
- exports.defineAction = defineAction;
2280
2548
  exports.executeParallel = executeParallel;
2281
2549
  exports.executeRace = executeRace;
2282
2550
  exports.executeSequential = executeSequential;
2551
+ exports.isActionRegisterDestroyedError = isActionRegisterDestroyedError;
2552
+ exports.isActionTimeoutError = isActionTimeoutError;
2283
2553
  exports.isActionValidationError = isActionValidationError;
2284
- exports.isReactActionError = isReactActionError;
2285
- exports.zodToJsonSchema = zodToJsonSchema;
2554
+ exports.isReactActionError = isReactActionError;