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