@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/README.md +40 -10
- package/dist/index.cjs +637 -368
- package/dist/index.d.cts +91 -121
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +91 -121
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +632 -364
- package/dist/index.js.map +1 -1
- package/package.json +18 -25
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 =
|
|
71
|
+
const handlerResult = trackedResult ? await trackedResult : result;
|
|
65
72
|
if (handlerResult !== void 0 && !context.terminated) context.results.push(handlerResult);
|
|
66
|
-
} else if (
|
|
67
|
-
const promiseWithErrorHandling =
|
|
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
|
-
|
|
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(
|
|
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
|
-
*
|
|
208
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
271
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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
|
-
|
|
1006
|
-
|
|
1007
|
-
return (
|
|
1008
|
-
|
|
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
|
-
|
|
1041
|
-
|
|
1042
|
-
return (
|
|
1043
|
-
|
|
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
|
|
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
|
-
|
|
1217
|
-
if (
|
|
1218
|
-
|
|
1219
|
-
|
|
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))
|
|
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))
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
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:
|
|
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
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
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
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
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.
|
|
1949
|
-
this.
|
|
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
|
|
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;
|