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