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