@context-action/core 0.8.4 โ 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 +126 -12
- package/dist/index.cjs +916 -188
- package/dist/index.d.cts +184 -16
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +184 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +905 -188
- package/dist/index.js.map +1 -1
- package/package.json +42 -28
package/dist/index.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
//#region src/execution-modes.ts
|
|
2
|
+
function isPromiseLike(value) {
|
|
3
|
+
return (typeof value === "object" || typeof value === "function") && value !== null && typeof value.then === "function";
|
|
4
|
+
}
|
|
2
5
|
/**
|
|
3
6
|
* Create standardized error handling for handlers
|
|
4
7
|
*
|
|
@@ -58,12 +61,15 @@ async function executeSequential(context, createController) {
|
|
|
58
61
|
i++;
|
|
59
62
|
continue;
|
|
60
63
|
}
|
|
64
|
+
(context.executedHandlers ?? (context.executedHandlers = [])).push(registration);
|
|
61
65
|
const result = registration.handler(context.payload, controller);
|
|
66
|
+
const asyncResult = isPromiseLike(result) ? Promise.resolve(result) : void 0;
|
|
67
|
+
const trackedResult = asyncResult && context.trackHandlerPromise ? context.trackHandlerPromise(asyncResult) : asyncResult;
|
|
62
68
|
if (registration.config.blocking) {
|
|
63
|
-
const handlerResult =
|
|
69
|
+
const handlerResult = trackedResult ? await trackedResult : result;
|
|
64
70
|
if (handlerResult !== void 0 && !context.terminated) context.results.push(handlerResult);
|
|
65
|
-
} else if (
|
|
66
|
-
const promiseWithErrorHandling =
|
|
71
|
+
} else if (trackedResult) {
|
|
72
|
+
const promiseWithErrorHandling = trackedResult.then((asyncResult) => {
|
|
67
73
|
if (asyncResult !== void 0 && !context.terminated) context.results.push(asyncResult);
|
|
68
74
|
return asyncResult;
|
|
69
75
|
}).catch((error) => {
|
|
@@ -161,10 +167,9 @@ async function executeParallel(context, createController) {
|
|
|
161
167
|
skipped: true
|
|
162
168
|
};
|
|
163
169
|
}
|
|
170
|
+
(context.executedHandlers ?? (context.executedHandlers = [])).push(registration);
|
|
164
171
|
const result = registration.handler(context.payload, controller);
|
|
165
|
-
|
|
166
|
-
if (result instanceof Promise) handlerResult = await result;
|
|
167
|
-
else handlerResult = result;
|
|
172
|
+
const handlerResult = isPromiseLike(result) ? await Promise.resolve(result) : result;
|
|
168
173
|
/** Collect result if handler returned something and pipeline wasn't terminated */
|
|
169
174
|
if (handlerResult !== void 0 && !context.terminated) context.results.push(handlerResult);
|
|
170
175
|
return {
|
|
@@ -183,8 +188,9 @@ async function executeParallel(context, createController) {
|
|
|
183
188
|
};
|
|
184
189
|
}
|
|
185
190
|
});
|
|
191
|
+
const trackedHandlerPromises = context.trackHandlerPromise ? handlerPromises.map((promise) => context.trackHandlerPromise(promise)) : handlerPromises;
|
|
186
192
|
/** Wait for all handlers to complete */
|
|
187
|
-
const results = await Promise.allSettled(
|
|
193
|
+
const results = await Promise.allSettled(trackedHandlerPromises);
|
|
188
194
|
/** Check for any rejected blocking handlers */
|
|
189
195
|
const failures = results.filter((result, index) => {
|
|
190
196
|
if (result.status === "rejected") return runnableHandlers[index]?.config.blocking ?? false;
|
|
@@ -203,8 +209,10 @@ async function executeParallel(context, createController) {
|
|
|
203
209
|
*
|
|
204
210
|
* Executes all qualifying handlers simultaneously using Promise.race, where
|
|
205
211
|
* the first handler to complete determines the pipeline result. Other handlers
|
|
206
|
-
*
|
|
207
|
-
*
|
|
212
|
+
* continue in the background and remain tracked for lifecycle cleanup; handlers
|
|
213
|
+
* must observe the controller signal for cooperative external cancellation.
|
|
214
|
+
* Useful for scenarios where you want the fastest response from multiple
|
|
215
|
+
* equivalent handlers.
|
|
208
216
|
*
|
|
209
217
|
* @template T - The payload type for the action
|
|
210
218
|
* @template R - The result type for handlers
|
|
@@ -245,10 +253,9 @@ async function executeRace(context, createController) {
|
|
|
245
253
|
skipped: true
|
|
246
254
|
};
|
|
247
255
|
}
|
|
256
|
+
(context.executedHandlers ?? (context.executedHandlers = [])).push(registration);
|
|
248
257
|
const result = registration.handler(context.payload, controller);
|
|
249
|
-
|
|
250
|
-
if (result instanceof Promise) handlerResult = await result;
|
|
251
|
-
else handlerResult = result;
|
|
258
|
+
const handlerResult = isPromiseLike(result) ? await Promise.resolve(result) : result;
|
|
252
259
|
return {
|
|
253
260
|
success: true,
|
|
254
261
|
handlerId: registration.id,
|
|
@@ -266,8 +273,9 @@ async function executeRace(context, createController) {
|
|
|
266
273
|
};
|
|
267
274
|
}
|
|
268
275
|
});
|
|
269
|
-
|
|
270
|
-
|
|
276
|
+
const trackedHandlerPromises = context.trackHandlerPromise ? handlerPromises.map((promise) => context.trackHandlerPromise(promise)) : handlerPromises;
|
|
277
|
+
/** Race all handlers while retaining every loser for lifecycle draining. */
|
|
278
|
+
const winner = await Promise.race(trackedHandlerPromises);
|
|
271
279
|
/** If the winner failed and was blocking, throw the error */
|
|
272
280
|
if (!winner.success && winner.registration?.config.blocking) throw winner.error;
|
|
273
281
|
/** Collect result from the winning handler */
|
|
@@ -316,7 +324,11 @@ var ActionGuard = class {
|
|
|
316
324
|
this.cleanupIntervalMs = 3e4;
|
|
317
325
|
this.maxGuards = 1e3;
|
|
318
326
|
this.accessOrder = [];
|
|
319
|
-
|
|
327
|
+
this.autoCleanupEnabled = autoCleanup;
|
|
328
|
+
}
|
|
329
|
+
/** Start cleanup only after the first guard is used. */
|
|
330
|
+
ensureAutoCleanup() {
|
|
331
|
+
if (this.autoCleanupEnabled && !this.cleanupInterval) this.startAutoCleanup();
|
|
320
332
|
}
|
|
321
333
|
/**
|
|
322
334
|
* Start automatic cleanup of idle guard states
|
|
@@ -324,9 +336,17 @@ var ActionGuard = class {
|
|
|
324
336
|
* @internal
|
|
325
337
|
*/
|
|
326
338
|
startAutoCleanup() {
|
|
339
|
+
if (this.cleanupInterval) return;
|
|
327
340
|
this.cleanupInterval = setInterval(() => {
|
|
328
341
|
this.performCleanup();
|
|
329
342
|
}, this.cleanupIntervalMs);
|
|
343
|
+
this.cleanupInterval.unref?.();
|
|
344
|
+
}
|
|
345
|
+
stopAutoCleanup() {
|
|
346
|
+
if (this.cleanupInterval) {
|
|
347
|
+
clearInterval(this.cleanupInterval);
|
|
348
|
+
this.cleanupInterval = void 0;
|
|
349
|
+
}
|
|
330
350
|
}
|
|
331
351
|
/**
|
|
332
352
|
* ๐ง Optimized cleanup with early exit and batched operations
|
|
@@ -335,7 +355,10 @@ var ActionGuard = class {
|
|
|
335
355
|
*/
|
|
336
356
|
performCleanup() {
|
|
337
357
|
const guardCount = this.guards.size;
|
|
338
|
-
if (guardCount === 0)
|
|
358
|
+
if (guardCount === 0) {
|
|
359
|
+
this.stopAutoCleanup();
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
339
362
|
const now = Date.now();
|
|
340
363
|
const keysToDelete = [];
|
|
341
364
|
if (guardCount <= 10) this.guards.forEach((state, key) => {
|
|
@@ -365,6 +388,7 @@ var ActionGuard = class {
|
|
|
365
388
|
if (accessIndex !== -1) this.accessOrder.splice(accessIndex, 1);
|
|
366
389
|
});
|
|
367
390
|
if (typeof process !== "undefined" && process.env?.DEBUG_CONTEXT_ACTION) console.debug(`[ActionGuard] Cleaned up ${keysToDelete.length} idle guards`);
|
|
391
|
+
if (this.guards.size === 0) this.stopAutoCleanup();
|
|
368
392
|
}
|
|
369
393
|
}
|
|
370
394
|
/**
|
|
@@ -423,6 +447,7 @@ var ActionGuard = class {
|
|
|
423
447
|
* @internal
|
|
424
448
|
*/
|
|
425
449
|
async debounce(actionKey, debounceMs) {
|
|
450
|
+
this.ensureAutoCleanup();
|
|
426
451
|
this.evictIfNeeded();
|
|
427
452
|
/** Get or create guard state for this action */
|
|
428
453
|
let state = this.guards.get(actionKey);
|
|
@@ -483,6 +508,7 @@ var ActionGuard = class {
|
|
|
483
508
|
* @internal
|
|
484
509
|
*/
|
|
485
510
|
throttle(actionKey, throttleMs) {
|
|
511
|
+
this.ensureAutoCleanup();
|
|
486
512
|
this.evictIfNeeded();
|
|
487
513
|
/** Get or create guard state for this action */
|
|
488
514
|
let state = this.guards.get(actionKey);
|
|
@@ -550,6 +576,9 @@ var ActionGuard = class {
|
|
|
550
576
|
state.throttleTimer = void 0;
|
|
551
577
|
}
|
|
552
578
|
this.guards.delete(actionKey);
|
|
579
|
+
const accessIndex = this.accessOrder.indexOf(actionKey);
|
|
580
|
+
if (accessIndex !== -1) this.accessOrder.splice(accessIndex, 1);
|
|
581
|
+
if (this.guards.size === 0) this.stopAutoCleanup();
|
|
553
582
|
}
|
|
554
583
|
}
|
|
555
584
|
/**
|
|
@@ -574,6 +603,8 @@ var ActionGuard = class {
|
|
|
574
603
|
});
|
|
575
604
|
/** Remove all guard states from memory */
|
|
576
605
|
this.guards.clear();
|
|
606
|
+
this.accessOrder = [];
|
|
607
|
+
this.stopAutoCleanup();
|
|
577
608
|
}
|
|
578
609
|
/**
|
|
579
610
|
* Get current guard state for debugging purposes
|
|
@@ -611,12 +642,7 @@ var ActionGuard = class {
|
|
|
611
642
|
* @internal
|
|
612
643
|
*/
|
|
613
644
|
destroy() {
|
|
614
|
-
if (this.cleanupInterval) {
|
|
615
|
-
clearInterval(this.cleanupInterval);
|
|
616
|
-
this.cleanupInterval = void 0;
|
|
617
|
-
}
|
|
618
645
|
this.clearAll();
|
|
619
|
-
this.accessOrder = [];
|
|
620
646
|
}
|
|
621
647
|
/**
|
|
622
648
|
* ๐ Get statistics about active guards
|
|
@@ -669,27 +695,42 @@ var OperationQueue = class {
|
|
|
669
695
|
* @returns Promise๋ก ๋ํ๋ ์์
๊ฒฐ๊ณผ
|
|
670
696
|
*/
|
|
671
697
|
enqueue(operation, priority = 0) {
|
|
672
|
-
return
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
698
|
+
return this.enqueueWithHandle(operation, priority).promise;
|
|
699
|
+
}
|
|
700
|
+
/** Enqueue an operation and retain a handle for pre-start cancellation. */
|
|
701
|
+
enqueueWithHandle(operation, priority = 0) {
|
|
702
|
+
let queuedOperation;
|
|
703
|
+
return {
|
|
704
|
+
promise: new Promise((resolve, reject) => {
|
|
705
|
+
queuedOperation = {
|
|
706
|
+
id: `${this.name}-${++this.operationCounter}`,
|
|
707
|
+
operation,
|
|
708
|
+
resolve,
|
|
709
|
+
reject,
|
|
710
|
+
priority,
|
|
711
|
+
timestamp: Date.now()
|
|
712
|
+
};
|
|
713
|
+
let insertIndex = this.queue.length;
|
|
714
|
+
for (let i = 0; i < this.queue.length; i++) {
|
|
715
|
+
const item = this.queue[i];
|
|
716
|
+
if (item && (item.priority || 0) < priority) {
|
|
717
|
+
insertIndex = i;
|
|
718
|
+
break;
|
|
719
|
+
}
|
|
687
720
|
}
|
|
721
|
+
this.queue.splice(insertIndex, 0, queuedOperation);
|
|
722
|
+
if (this.processingPromise) this.notifyNewOperation();
|
|
723
|
+
this.processQueue();
|
|
724
|
+
}),
|
|
725
|
+
cancel: (reason = /* @__PURE__ */ new Error("Queue operation cancelled")) => {
|
|
726
|
+
const index = this.queue.indexOf(queuedOperation);
|
|
727
|
+
if (index === -1) return false;
|
|
728
|
+
this.queue.splice(index, 1);
|
|
729
|
+
queuedOperation.reject(reason);
|
|
730
|
+
this.notifyNewOperation();
|
|
731
|
+
return true;
|
|
688
732
|
}
|
|
689
|
-
|
|
690
|
-
if (this.processingPromise) this.notifyNewOperation();
|
|
691
|
-
this.processQueue();
|
|
692
|
-
});
|
|
733
|
+
};
|
|
693
734
|
}
|
|
694
735
|
/**
|
|
695
736
|
* ๐ ํ ์ฒ๋ฆฌ ๋ฉ์ธ ๋ก์ง - ๋์์ฑ ์ ์ด ๋ฐ ๋น๋๊ธฐ ์ง์
|
|
@@ -795,12 +836,14 @@ var OperationQueue = class {
|
|
|
795
836
|
/**
|
|
796
837
|
* ํ ๋น์ฐ๊ธฐ (ํ
์คํธ์ฉ)
|
|
797
838
|
*/
|
|
798
|
-
clear() {
|
|
839
|
+
clear(options = {}) {
|
|
840
|
+
const rejectPending = options.rejectPending ?? true;
|
|
841
|
+
const reason = options.reason ?? /* @__PURE__ */ new Error("Queue cleared");
|
|
799
842
|
this.queue.forEach((operation) => {
|
|
800
|
-
operation.reject(
|
|
843
|
+
if (rejectPending) operation.reject(reason);
|
|
844
|
+
else operation.resolve(void 0);
|
|
801
845
|
});
|
|
802
846
|
this.queue = [];
|
|
803
|
-
this.processingPromise = null;
|
|
804
847
|
this.pendingResolvers.splice(0).forEach((resolve) => resolve());
|
|
805
848
|
}
|
|
806
849
|
/**
|
|
@@ -817,8 +860,179 @@ var OperationQueue = class {
|
|
|
817
860
|
}
|
|
818
861
|
};
|
|
819
862
|
|
|
863
|
+
//#endregion
|
|
864
|
+
//#region src/errors.ts
|
|
865
|
+
/**
|
|
866
|
+
* Action payload ๊ฒ์ฆ ์คํจ ์๋ฌ
|
|
867
|
+
*
|
|
868
|
+
* dispatch ์ Zod ์คํค๋ง ๊ฒ์ฆ์ด ์คํจํ๋ฉด ๋ฐ์ํฉ๋๋ค.
|
|
869
|
+
* (validationMode๊ฐ 'strict'์ผ ๋๋ง throw)
|
|
870
|
+
*
|
|
871
|
+
* @example
|
|
872
|
+
* ```typescript
|
|
873
|
+
* try {
|
|
874
|
+
* dispatch('updateUser', { id: '', name: 'John' });
|
|
875
|
+
* } catch (error) {
|
|
876
|
+
* if (error instanceof ActionValidationError) {
|
|
877
|
+
* console.log('Action:', error.action);
|
|
878
|
+
* console.log('Issues:', error.issues);
|
|
879
|
+
* console.log('Formatted:', error.formattedErrors);
|
|
880
|
+
* }
|
|
881
|
+
* }
|
|
882
|
+
* ```
|
|
883
|
+
*/
|
|
884
|
+
var ActionValidationError = class ActionValidationError extends Error {
|
|
885
|
+
/**
|
|
886
|
+
* @param action - ๊ฒ์ฆ ์คํจํ action ์ด๋ฆ
|
|
887
|
+
* @param zodError - Zod ๊ฒ์ฆ ์๋ฌ ๊ฐ์ฒด (ZodError compatible)
|
|
888
|
+
*/
|
|
889
|
+
constructor(action, zodError) {
|
|
890
|
+
const message = `Action "${action}" payload validation failed: ${zodError && typeof zodError === "object" && "message" in zodError ? String(zodError.message) : "Validation failed"}`;
|
|
891
|
+
super(message);
|
|
892
|
+
this.name = "ActionValidationError";
|
|
893
|
+
this.action = action;
|
|
894
|
+
this.zodError = zodError;
|
|
895
|
+
Object.setPrototypeOf(this, ActionValidationError.prototype);
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Zod ๊ฒ์ฆ ์ด์ ๋ชฉ๋ก
|
|
899
|
+
*/
|
|
900
|
+
get issues() {
|
|
901
|
+
if (this.zodError && typeof this.zodError === "object" && "issues" in this.zodError && Array.isArray(this.zodError.issues)) return this.zodError.issues;
|
|
902
|
+
return [];
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* ํฌ๋งท๋ ์๋ฌ ๊ฐ์ฒด (ํ๋๋ณ ์๋ฌ ๋ฉ์์ง)
|
|
906
|
+
*/
|
|
907
|
+
get formattedErrors() {
|
|
908
|
+
if (this.zodError && typeof this.zodError === "object" && "format" in this.zodError && typeof this.zodError.format === "function") return this.zodError.format();
|
|
909
|
+
return {};
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* ํ๋ซ ์๋ฌ ๋งต (ํ๋๋ช
โ ์๋ฌ ๋ฉ์์ง ๋ฐฐ์ด)
|
|
913
|
+
*/
|
|
914
|
+
get flattenedErrors() {
|
|
915
|
+
if (this.zodError && typeof this.zodError === "object" && "flatten" in this.zodError && typeof this.zodError.flatten === "function") return this.zodError.flatten();
|
|
916
|
+
return {
|
|
917
|
+
fieldErrors: {},
|
|
918
|
+
formErrors: []
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* ์ฒซ ๋ฒ์งธ ์๋ฌ ๋ฉ์์ง
|
|
923
|
+
*/
|
|
924
|
+
get firstError() {
|
|
925
|
+
return this.issues[0]?.message;
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* ์๋ฌ ๋ฐ์ ํ๋ ๊ฒฝ๋ก ๋ชฉ๋ก
|
|
929
|
+
*/
|
|
930
|
+
get errorPaths() {
|
|
931
|
+
return this.issues.map((issue) => issue.path.map((p) => String(p)).join("."));
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* JSON ์ง๋ ฌํ
|
|
935
|
+
*/
|
|
936
|
+
toJSON() {
|
|
937
|
+
return {
|
|
938
|
+
name: this.name,
|
|
939
|
+
action: this.action,
|
|
940
|
+
message: this.message,
|
|
941
|
+
issues: this.issues
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
/**
|
|
946
|
+
* Raised when a dispatch exceeds its configured wall-clock timeout.
|
|
947
|
+
* The underlying handler receives an aborted controller signal and the internal
|
|
948
|
+
* queue keeps draining it safely, while the caller is released immediately with
|
|
949
|
+
* this error.
|
|
950
|
+
*/
|
|
951
|
+
var ActionTimeoutError = class ActionTimeoutError extends Error {
|
|
952
|
+
constructor(action, timeout) {
|
|
953
|
+
super(`Action "${action}" timed out after ${timeout}ms`);
|
|
954
|
+
this.action = action;
|
|
955
|
+
this.timeout = timeout;
|
|
956
|
+
this.name = "ActionTimeoutError";
|
|
957
|
+
Object.setPrototypeOf(this, ActionTimeoutError.prototype);
|
|
958
|
+
}
|
|
959
|
+
};
|
|
960
|
+
/** Raised when work is submitted after an ActionRegister begins shutdown. */
|
|
961
|
+
var ActionRegisterDestroyedError = class ActionRegisterDestroyedError extends Error {
|
|
962
|
+
constructor(registerName, state) {
|
|
963
|
+
super(`ActionRegister "${registerName}" is ${state} and cannot accept new work`);
|
|
964
|
+
this.registerName = registerName;
|
|
965
|
+
this.state = state;
|
|
966
|
+
this.name = "ActionRegisterDestroyedError";
|
|
967
|
+
Object.setPrototypeOf(this, ActionRegisterDestroyedError.prototype);
|
|
968
|
+
}
|
|
969
|
+
};
|
|
970
|
+
/**
|
|
971
|
+
* ActionValidationError ํ์
๊ฐ๋
|
|
972
|
+
*/
|
|
973
|
+
function isActionValidationError(error) {
|
|
974
|
+
return error instanceof ActionValidationError;
|
|
975
|
+
}
|
|
976
|
+
/** ActionTimeoutError type guard. */
|
|
977
|
+
function isActionTimeoutError(error) {
|
|
978
|
+
return error instanceof ActionTimeoutError;
|
|
979
|
+
}
|
|
980
|
+
/** ActionRegisterDestroyedError type guard. */
|
|
981
|
+
function isActionRegisterDestroyedError(error) {
|
|
982
|
+
return error instanceof ActionRegisterDestroyedError;
|
|
983
|
+
}
|
|
984
|
+
|
|
820
985
|
//#endregion
|
|
821
986
|
//#region src/ActionRegister.ts
|
|
987
|
+
const dispatchOptionKeys = /* @__PURE__ */ new Set([
|
|
988
|
+
"autoAbort",
|
|
989
|
+
"debounce",
|
|
990
|
+
"executionMode",
|
|
991
|
+
"filter",
|
|
992
|
+
"immediate",
|
|
993
|
+
"queuePriority",
|
|
994
|
+
"result",
|
|
995
|
+
"retryOnError",
|
|
996
|
+
"signal",
|
|
997
|
+
"throttle",
|
|
998
|
+
"timeout"
|
|
999
|
+
]);
|
|
1000
|
+
function isRecord(value) {
|
|
1001
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1002
|
+
}
|
|
1003
|
+
function isOptionalNumber(value) {
|
|
1004
|
+
return value === void 0 || typeof value === "number";
|
|
1005
|
+
}
|
|
1006
|
+
function isOptionalBoolean(value) {
|
|
1007
|
+
return value === void 0 || typeof value === "boolean";
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Recognize the deprecated options-first void-action proxy call.
|
|
1011
|
+
*
|
|
1012
|
+
* Every supplied field is validated so ordinary payloads that merely overlap
|
|
1013
|
+
* one option name are not silently reinterpreted as dispatch options.
|
|
1014
|
+
*/
|
|
1015
|
+
function isDispatchOptions(value) {
|
|
1016
|
+
if (!isRecord(value)) return false;
|
|
1017
|
+
const keys = Object.keys(value);
|
|
1018
|
+
if (keys.length === 0 || keys.some((key) => !dispatchOptionKeys.has(key))) return false;
|
|
1019
|
+
return keys.every((key) => {
|
|
1020
|
+
const optionValue = value[key];
|
|
1021
|
+
switch (key) {
|
|
1022
|
+
case "debounce":
|
|
1023
|
+
case "queuePriority":
|
|
1024
|
+
case "throttle":
|
|
1025
|
+
case "timeout": return isOptionalNumber(optionValue);
|
|
1026
|
+
case "immediate": return isOptionalBoolean(optionValue);
|
|
1027
|
+
case "executionMode": return optionValue === void 0 || optionValue === "sequential" || optionValue === "parallel" || optionValue === "race";
|
|
1028
|
+
case "signal": return optionValue === void 0 || isRecord(optionValue) && typeof optionValue.aborted === "boolean" && typeof optionValue.addEventListener === "function";
|
|
1029
|
+
case "retryOnError": return optionValue === void 0 || isRecord(optionValue) && typeof optionValue.maxAttempts === "number" && typeof optionValue.delay === "number";
|
|
1030
|
+
case "autoAbort": return optionValue === void 0 || isRecord(optionValue) && typeof optionValue.enabled === "boolean" && isOptionalBoolean(optionValue.allowHandlerAbort) && (optionValue.onControllerCreated === void 0 || typeof optionValue.onControllerCreated === "function");
|
|
1031
|
+
case "filter": return optionValue === void 0 || isRecord(optionValue) && (optionValue.handlerIds === void 0 || Array.isArray(optionValue.handlerIds) && optionValue.handlerIds.every((item) => typeof item === "string")) && (optionValue.excludeHandlerIds === void 0 || Array.isArray(optionValue.excludeHandlerIds) && optionValue.excludeHandlerIds.every((item) => typeof item === "string")) && (optionValue.priority === void 0 || isRecord(optionValue.priority)) && (optionValue.custom === void 0 || typeof optionValue.custom === "function");
|
|
1032
|
+
case "result": return optionValue === void 0 || isRecord(optionValue) && (optionValue.strategy === void 0 || optionValue.strategy === "first" || optionValue.strategy === "last" || optionValue.strategy === "all" || optionValue.strategy === "merge" || optionValue.strategy === "custom") && (optionValue.merger === void 0 || typeof optionValue.merger === "function") && isOptionalBoolean(optionValue.collect) && isOptionalNumber(optionValue.maxResults) && isOptionalBoolean(optionValue.includeErrors);
|
|
1033
|
+
}
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
822
1036
|
/**
|
|
823
1037
|
* Action Register for managing action handlers with priority-based execution
|
|
824
1038
|
*
|
|
@@ -834,35 +1048,6 @@ var OperationQueue = class {
|
|
|
834
1048
|
*
|
|
835
1049
|
* @public
|
|
836
1050
|
*/
|
|
837
|
-
/**
|
|
838
|
-
* Type guard to determine if an object is DispatchOptions
|
|
839
|
-
* Extracted as utility function for reuse and performance
|
|
840
|
-
*
|
|
841
|
-
* @param obj - Object to check
|
|
842
|
-
* @returns True if object is DispatchOptions
|
|
843
|
-
* @internal
|
|
844
|
-
*/
|
|
845
|
-
function isDispatchOptions(obj) {
|
|
846
|
-
if (!obj || typeof obj !== "object") return false;
|
|
847
|
-
if ("debounce" in obj && typeof obj.debounce === "number") return true;
|
|
848
|
-
if ("throttle" in obj && typeof obj.throttle === "number") return true;
|
|
849
|
-
if ("executionMode" in obj) return true;
|
|
850
|
-
if ("signal" in obj && obj.signal instanceof AbortSignal) return true;
|
|
851
|
-
if ("immediate" in obj && typeof obj.immediate === "boolean") return true;
|
|
852
|
-
if ("queuePriority" in obj && typeof obj.queuePriority === "number") return true;
|
|
853
|
-
if ("timeout" in obj && typeof obj.timeout === "number") return true;
|
|
854
|
-
if ("retryOnError" in obj && typeof obj.retryOnError === "object") return true;
|
|
855
|
-
if ("autoAbort" in obj && typeof obj.autoAbort === "object") return true;
|
|
856
|
-
if ("filter" in obj && typeof obj.filter === "object" && obj.filter !== null) {
|
|
857
|
-
const filter = obj.filter;
|
|
858
|
-
if ("handlerIds" in filter || "excludeHandlerIds" in filter || "priority" in filter || "custom" in filter) return true;
|
|
859
|
-
}
|
|
860
|
-
if ("result" in obj && typeof obj.result === "object" && obj.result !== null) {
|
|
861
|
-
const result = obj.result;
|
|
862
|
-
if ("strategy" in result || "merger" in result || "collect" in result || "maxResults" in result || "includeErrors" in result) return true;
|
|
863
|
-
}
|
|
864
|
-
return false;
|
|
865
|
-
}
|
|
866
1051
|
var ActionRegister = class {
|
|
867
1052
|
constructor(config = {}) {
|
|
868
1053
|
this.pipelines = /* @__PURE__ */ new Map();
|
|
@@ -874,6 +1059,11 @@ var ActionRegister = class {
|
|
|
874
1059
|
this.handlerIdCounter = 0;
|
|
875
1060
|
this.controllerPool = [];
|
|
876
1061
|
this.maxControllerPoolSize = 10;
|
|
1062
|
+
this.lifecycleState = "active";
|
|
1063
|
+
this.lifecycleController = new AbortController();
|
|
1064
|
+
this.activeDispatches = /* @__PURE__ */ new Set();
|
|
1065
|
+
this.activeHandlerPromises = /* @__PURE__ */ new Set();
|
|
1066
|
+
this.dispatchConstructionDepth = 0;
|
|
877
1067
|
this.name = config.name || "ActionRegister";
|
|
878
1068
|
this.registryConfig = config.registry;
|
|
879
1069
|
this.maxHandlersPerAction = config.registry?.maxHandlersPerAction ?? 1e3;
|
|
@@ -890,10 +1080,10 @@ var ActionRegister = class {
|
|
|
890
1080
|
}
|
|
891
1081
|
/**
|
|
892
1082
|
* ๐ Action-based dispatcher
|
|
893
|
-
*
|
|
1083
|
+
*
|
|
894
1084
|
* Provides function-based access to actions for more convenient dispatching.
|
|
895
1085
|
* Each action becomes a callable function that can be invoked directly.
|
|
896
|
-
*
|
|
1086
|
+
*
|
|
897
1087
|
* @example
|
|
898
1088
|
* ```typescript
|
|
899
1089
|
* interface MyActions extends ActionPayloadMap {
|
|
@@ -906,19 +1096,19 @@ var ActionRegister = class {
|
|
|
906
1096
|
* // Function-based dispatching
|
|
907
1097
|
* await registry.actions.userLogin({ userId: '123', email: 'test@example.com' });
|
|
908
1098
|
* await registry.actions.resetApp();
|
|
1099
|
+
* await registry.actions.resetApp({ debounce: 100 }); // Legacy options-first form
|
|
1100
|
+
* await registry.actions.resetApp(undefined, { debounce: 100 });
|
|
909
1101
|
* ```
|
|
910
1102
|
*
|
|
911
1103
|
* @public
|
|
912
1104
|
*/
|
|
913
1105
|
get actions() {
|
|
914
1106
|
if (!this._actionsProxy) this._actionsProxy = new Proxy({}, { get: (_target, prop) => {
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
return (
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
};
|
|
921
|
-
}
|
|
1107
|
+
const actionKey = prop;
|
|
1108
|
+
if (typeof prop === "string" && this.pipelines.has(actionKey)) return (payloadOrOptions, options) => {
|
|
1109
|
+
if (isDispatchOptions(payloadOrOptions)) return this.dispatch(actionKey, void 0, payloadOrOptions);
|
|
1110
|
+
return this.dispatch(actionKey, payloadOrOptions, options);
|
|
1111
|
+
};
|
|
922
1112
|
} });
|
|
923
1113
|
return this._actionsProxy;
|
|
924
1114
|
}
|
|
@@ -935,6 +1125,10 @@ var ActionRegister = class {
|
|
|
935
1125
|
*
|
|
936
1126
|
* // Actions without payload
|
|
937
1127
|
* const result = await registry.actionsWithResult.userLogout();
|
|
1128
|
+
* const debouncedResult = await registry.actionsWithResult.userLogout(
|
|
1129
|
+
* undefined,
|
|
1130
|
+
* { debounce: 100 }
|
|
1131
|
+
* );
|
|
938
1132
|
*
|
|
939
1133
|
* // With options
|
|
940
1134
|
* const result = await registry.actionsWithResult.processData(
|
|
@@ -947,13 +1141,11 @@ var ActionRegister = class {
|
|
|
947
1141
|
*/
|
|
948
1142
|
get actionsWithResult() {
|
|
949
1143
|
if (!this._actionsWithResultProxy) this._actionsWithResultProxy = new Proxy({}, { get: (_target, prop) => {
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
return (
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
};
|
|
956
|
-
}
|
|
1144
|
+
const actionKey = prop;
|
|
1145
|
+
if (typeof prop === "string" && this.pipelines.has(actionKey)) return (payloadOrOptions, options) => {
|
|
1146
|
+
if (isDispatchOptions(payloadOrOptions)) return this.dispatchWithResult(actionKey, void 0, payloadOrOptions);
|
|
1147
|
+
return this.dispatchWithResult(actionKey, payloadOrOptions, options);
|
|
1148
|
+
};
|
|
957
1149
|
} });
|
|
958
1150
|
return this._actionsWithResultProxy;
|
|
959
1151
|
}
|
|
@@ -973,6 +1165,7 @@ var ActionRegister = class {
|
|
|
973
1165
|
* @public
|
|
974
1166
|
*/
|
|
975
1167
|
register(action, handler, config = {}) {
|
|
1168
|
+
this.assertAcceptingWork();
|
|
976
1169
|
const handlerId = config.id || this.generateHandlerId(action);
|
|
977
1170
|
return this._performRegistrationSync(action, handler, config, handlerId);
|
|
978
1171
|
}
|
|
@@ -985,6 +1178,15 @@ var ActionRegister = class {
|
|
|
985
1178
|
console[level](`๐ฏ [${timestamp}] [${this.name}] ${message}`, data || "");
|
|
986
1179
|
}
|
|
987
1180
|
}
|
|
1181
|
+
assertAcceptingWork() {
|
|
1182
|
+
if (this.lifecycleState !== "active") throw new ActionRegisterDestroyedError(this.name, this.lifecycleState);
|
|
1183
|
+
}
|
|
1184
|
+
rejectedLifecyclePromise() {
|
|
1185
|
+
const error = new ActionRegisterDestroyedError(this.name, this.lifecycleState === "active" ? "destroyed" : this.lifecycleState);
|
|
1186
|
+
const rejected = Promise.reject(error);
|
|
1187
|
+
rejected.catch(() => {});
|
|
1188
|
+
return rejected;
|
|
1189
|
+
}
|
|
988
1190
|
/**
|
|
989
1191
|
* ๐ง Generate unique handler ID using optimized counter-based approach
|
|
990
1192
|
*/
|
|
@@ -1123,16 +1325,245 @@ var ActionRegister = class {
|
|
|
1123
1325
|
});
|
|
1124
1326
|
return unregister;
|
|
1125
1327
|
}
|
|
1126
|
-
|
|
1127
|
-
if (
|
|
1128
|
-
|
|
1129
|
-
|
|
1328
|
+
dispatch(action, payload, options) {
|
|
1329
|
+
if (this.lifecycleState !== "active") return this.rejectedLifecyclePromise();
|
|
1330
|
+
const timeoutScope = this.createTimeoutScope(action, options);
|
|
1331
|
+
const dispatchHandlerPromises = /* @__PURE__ */ new Set();
|
|
1332
|
+
const attemptState = { count: 0 };
|
|
1333
|
+
const operation = async () => {
|
|
1334
|
+
if (!timeoutScope.options?.signal?.aborted) this.validatePayload(action, payload);
|
|
1335
|
+
return this.executeWithRetry(async () => {
|
|
1336
|
+
const executedHandlers = [];
|
|
1337
|
+
try {
|
|
1338
|
+
return await this._performDispatch(action, payload, timeoutScope.options, attemptState.count > 1, executedHandlers, dispatchHandlerPromises);
|
|
1339
|
+
} finally {
|
|
1340
|
+
this.cleanupOneTimeHandlers(action, executedHandlers, dispatchHandlerPromises);
|
|
1341
|
+
}
|
|
1342
|
+
}, timeoutScope.options, attemptState, void 0, () => this.getHandlerCount(action) > 0);
|
|
1343
|
+
};
|
|
1344
|
+
const hasTimingGuard = options?.debounce !== void 0 || options?.throttle !== void 0 || this.pipelines.get(action)?.some((handler) => handler.config.debounce !== void 0 || handler.config.throttle !== void 0) === true;
|
|
1345
|
+
let dispatchPromise;
|
|
1346
|
+
this.dispatchConstructionDepth += 1;
|
|
1347
|
+
try {
|
|
1348
|
+
if (timeoutScope.options?.immediate || hasTimingGuard || !this.dispatchQueue) dispatchPromise = operation();
|
|
1349
|
+
else {
|
|
1350
|
+
const queued = this.dispatchQueue.enqueueWithHandle(operation, timeoutScope.options?.queuePriority ?? 0);
|
|
1351
|
+
timeoutScope.onTimeout((error) => queued.cancel(error));
|
|
1352
|
+
dispatchPromise = queued.promise;
|
|
1353
|
+
}
|
|
1354
|
+
this.trackDispatchPromise(dispatchPromise);
|
|
1355
|
+
} finally {
|
|
1356
|
+
this.dispatchConstructionDepth -= 1;
|
|
1357
|
+
}
|
|
1358
|
+
const observedPromise = this.raceWithTimeout(dispatchPromise, timeoutScope, dispatchHandlerPromises).catch((error) => {
|
|
1359
|
+
this.invokeErrorHandler(error, action, payload, options, attemptState.count);
|
|
1360
|
+
throw error;
|
|
1361
|
+
});
|
|
1362
|
+
observedPromise.catch(() => {});
|
|
1363
|
+
return observedPromise;
|
|
1364
|
+
}
|
|
1365
|
+
/** Execute a dispatch operation with an optional whole-action retry policy. */
|
|
1366
|
+
async executeWithRetry(operation, options, attemptState, shouldRetryResult, canRetry = () => true) {
|
|
1367
|
+
const configuredAttempts = options?.retryOnError?.maxAttempts ?? 1;
|
|
1368
|
+
const maxAttempts = Number.isFinite(configuredAttempts) ? Math.max(1, Math.floor(configuredAttempts)) : 1;
|
|
1369
|
+
const retryDelay = Math.max(0, options?.retryOnError?.delay ?? 0);
|
|
1370
|
+
while (attemptState.count < maxAttempts) {
|
|
1371
|
+
attemptState.count += 1;
|
|
1372
|
+
try {
|
|
1373
|
+
const result = await operation();
|
|
1374
|
+
if (!(shouldRetryResult?.(result) ?? false) || attemptState.count >= maxAttempts || options?.signal?.aborted || !canRetry()) return result;
|
|
1375
|
+
} catch (error) {
|
|
1376
|
+
if (error instanceof ActionValidationError || attemptState.count >= maxAttempts || options?.signal?.aborted || !canRetry()) throw error;
|
|
1377
|
+
}
|
|
1378
|
+
await this.waitForRetry(retryDelay, options?.signal);
|
|
1379
|
+
}
|
|
1380
|
+
return operation();
|
|
1381
|
+
}
|
|
1382
|
+
trackDispatchPromise(promise) {
|
|
1383
|
+
this.activeDispatches.add(promise);
|
|
1384
|
+
const remove = () => this.activeDispatches.delete(promise);
|
|
1385
|
+
promise.then(remove, remove);
|
|
1386
|
+
return promise;
|
|
1387
|
+
}
|
|
1388
|
+
trackHandlerPromise(promise, dispatchHandlerPromises) {
|
|
1389
|
+
this.activeHandlerPromises.add(promise);
|
|
1390
|
+
dispatchHandlerPromises.add(promise);
|
|
1391
|
+
const remove = () => {
|
|
1392
|
+
this.activeHandlerPromises.delete(promise);
|
|
1393
|
+
dispatchHandlerPromises.delete(promise);
|
|
1394
|
+
};
|
|
1395
|
+
promise.then(remove, remove);
|
|
1396
|
+
return promise;
|
|
1397
|
+
}
|
|
1398
|
+
trackGlobalHandlerPromise(promise) {
|
|
1399
|
+
this.activeHandlerPromises.add(promise);
|
|
1400
|
+
const remove = () => this.activeHandlerPromises.delete(promise);
|
|
1401
|
+
promise.then(remove, remove);
|
|
1402
|
+
return promise;
|
|
1403
|
+
}
|
|
1404
|
+
/** Abort-aware retry delay so cancellation does not wait for the full backoff. */
|
|
1405
|
+
waitForRetry(delay, signal) {
|
|
1406
|
+
if (delay <= 0 || signal?.aborted) return Promise.resolve();
|
|
1407
|
+
return new Promise((resolve) => {
|
|
1408
|
+
const timer = setTimeout(finish, delay);
|
|
1409
|
+
const abort = () => finish();
|
|
1410
|
+
function finish() {
|
|
1411
|
+
clearTimeout(timer);
|
|
1412
|
+
signal?.removeEventListener("abort", abort);
|
|
1413
|
+
resolve();
|
|
1414
|
+
}
|
|
1415
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
1130
1416
|
});
|
|
1131
1417
|
}
|
|
1418
|
+
/** Build a wall-clock timeout that also participates in pipeline cancellation. */
|
|
1419
|
+
createTimeoutScope(action, options) {
|
|
1420
|
+
const hasTimeout = options?.timeout !== void 0 && Number.isFinite(options.timeout);
|
|
1421
|
+
const timeout = hasTimeout ? Math.max(0, options.timeout) : void 0;
|
|
1422
|
+
const timeoutController = hasTimeout ? new AbortController() : void 0;
|
|
1423
|
+
const signalCleanups = [];
|
|
1424
|
+
const timeoutCallbacks = /* @__PURE__ */ new Set();
|
|
1425
|
+
const signals = [
|
|
1426
|
+
this.lifecycleController.signal,
|
|
1427
|
+
options?.signal,
|
|
1428
|
+
timeoutController?.signal
|
|
1429
|
+
].filter((candidate) => Boolean(candidate));
|
|
1430
|
+
let signal = signals[0];
|
|
1431
|
+
if (signals.length > 1) if (typeof AbortSignal.any === "function") signal = AbortSignal.any(signals);
|
|
1432
|
+
else {
|
|
1433
|
+
const mergedController = new AbortController();
|
|
1434
|
+
const forwardAbort = (source) => {
|
|
1435
|
+
if (!mergedController.signal.aborted) mergedController.abort(source.reason);
|
|
1436
|
+
};
|
|
1437
|
+
for (const source of signals) {
|
|
1438
|
+
if (source.aborted) {
|
|
1439
|
+
forwardAbort(source);
|
|
1440
|
+
break;
|
|
1441
|
+
}
|
|
1442
|
+
const listener = () => forwardAbort(source);
|
|
1443
|
+
source.addEventListener("abort", listener, { once: true });
|
|
1444
|
+
signalCleanups.push(() => source.removeEventListener("abort", listener));
|
|
1445
|
+
}
|
|
1446
|
+
signal = mergedController.signal;
|
|
1447
|
+
}
|
|
1448
|
+
let timer;
|
|
1449
|
+
const timeoutPromise = timeoutController && timeout !== void 0 ? new Promise((_, reject) => {
|
|
1450
|
+
timer = setTimeout(() => {
|
|
1451
|
+
const error = new ActionTimeoutError(String(action), timeout);
|
|
1452
|
+
timeoutController.abort(error);
|
|
1453
|
+
timeoutCallbacks.forEach((callback) => callback(error));
|
|
1454
|
+
reject(error);
|
|
1455
|
+
}, timeout);
|
|
1456
|
+
}) : void 0;
|
|
1457
|
+
return {
|
|
1458
|
+
options: {
|
|
1459
|
+
...options,
|
|
1460
|
+
signal
|
|
1461
|
+
},
|
|
1462
|
+
timeoutPromise,
|
|
1463
|
+
onTimeout: (callback) => timeoutCallbacks.add(callback),
|
|
1464
|
+
cleanup: () => {
|
|
1465
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
1466
|
+
timeoutCallbacks.clear();
|
|
1467
|
+
},
|
|
1468
|
+
cleanupSignals: () => signalCleanups.forEach((cleanup) => cleanup())
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
/** Expose timeout failure while allowing the queued operation to drain safely. */
|
|
1472
|
+
raceWithTimeout(operation, scope, dispatchHandlerPromises) {
|
|
1473
|
+
const exposed = scope.timeoutPromise ? Promise.race([operation, scope.timeoutPromise]) : operation;
|
|
1474
|
+
const cleanupAfterStartedHandlers = () => {
|
|
1475
|
+
scope.cleanup();
|
|
1476
|
+
this.cleanupSignalsAfterStartedHandlers(scope.cleanupSignals, dispatchHandlerPromises);
|
|
1477
|
+
};
|
|
1478
|
+
exposed.then(cleanupAfterStartedHandlers, cleanupAfterStartedHandlers);
|
|
1479
|
+
return exposed;
|
|
1480
|
+
}
|
|
1481
|
+
cleanupSignalsAfterStartedHandlers(cleanup, dispatchHandlerPromises) {
|
|
1482
|
+
const handlersStillRunning = [...dispatchHandlerPromises];
|
|
1483
|
+
if (handlersStillRunning.length === 0) {
|
|
1484
|
+
cleanup();
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1487
|
+
Promise.allSettled(handlersStillRunning).then(cleanup);
|
|
1488
|
+
}
|
|
1489
|
+
/** Invoke the configured error handler without allowing it to replace the dispatch error. */
|
|
1490
|
+
invokeErrorHandler(error, action, payload, options, attempts) {
|
|
1491
|
+
const errorHandler = this.registryConfig?.errorHandler;
|
|
1492
|
+
if (!errorHandler) return;
|
|
1493
|
+
const normalizedError = error instanceof Error ? error : new Error(String(error));
|
|
1494
|
+
try {
|
|
1495
|
+
const handlerResult = errorHandler(normalizedError, {
|
|
1496
|
+
action: String(action),
|
|
1497
|
+
payload,
|
|
1498
|
+
options,
|
|
1499
|
+
attempts,
|
|
1500
|
+
phase: normalizedError instanceof ActionTimeoutError ? "timeout" : normalizedError instanceof ActionValidationError ? "validation" : "execution"
|
|
1501
|
+
});
|
|
1502
|
+
if (handlerResult && typeof handlerResult.then === "function") Promise.resolve(handlerResult).catch((handlerError) => {
|
|
1503
|
+
this.log("Global async error handler failed", handlerError, "warn");
|
|
1504
|
+
});
|
|
1505
|
+
} catch (handlerError) {
|
|
1506
|
+
this.log("Global error handler failed", handlerError, "warn");
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* Validate an action payload against the configured schema.
|
|
1511
|
+
* Shared by all dispatch paths so result collection cannot bypass validation.
|
|
1512
|
+
*/
|
|
1513
|
+
validatePayload(action, payload) {
|
|
1514
|
+
if (!this.registryConfig?.schema || this.registryConfig.validateOnDispatch === false) return;
|
|
1515
|
+
const actionName = String(action);
|
|
1516
|
+
const actionSchema = this.registryConfig.schema[actionName];
|
|
1517
|
+
if (!actionSchema) return;
|
|
1518
|
+
let result;
|
|
1519
|
+
try {
|
|
1520
|
+
result = actionSchema.safeParse(payload);
|
|
1521
|
+
} catch (error) {
|
|
1522
|
+
throw new ActionValidationError(actionName, error);
|
|
1523
|
+
}
|
|
1524
|
+
if (result.success) return {
|
|
1525
|
+
passed: true,
|
|
1526
|
+
errors: []
|
|
1527
|
+
};
|
|
1528
|
+
const mode = this.registryConfig.validationMode ?? "strict";
|
|
1529
|
+
if (mode === "strict") throw new ActionValidationError(actionName, result.error);
|
|
1530
|
+
if (mode === "warn") {
|
|
1531
|
+
console.warn(`Action "${actionName}" payload validation failed:`, result.error.message);
|
|
1532
|
+
this.log(`Validation warning for action '${actionName}'`, { issues: result.error.issues }, "warn");
|
|
1533
|
+
}
|
|
1534
|
+
return {
|
|
1535
|
+
passed: false,
|
|
1536
|
+
errors: result.error.issues.map((issue) => issue.message)
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
createAbortedExecutionResult(startTime, handlersSkipped = 0, validation) {
|
|
1540
|
+
const endTime = Date.now();
|
|
1541
|
+
return {
|
|
1542
|
+
success: false,
|
|
1543
|
+
aborted: true,
|
|
1544
|
+
abortReason: "Action dispatch aborted by signal",
|
|
1545
|
+
terminated: false,
|
|
1546
|
+
validation,
|
|
1547
|
+
result: void 0,
|
|
1548
|
+
successResults: [],
|
|
1549
|
+
results: [],
|
|
1550
|
+
failedResults: [],
|
|
1551
|
+
execution: {
|
|
1552
|
+
duration: endTime - startTime,
|
|
1553
|
+
handlersExecuted: 0,
|
|
1554
|
+
handlersSkipped,
|
|
1555
|
+
handlersFailed: 0,
|
|
1556
|
+
startTime,
|
|
1557
|
+
endTime
|
|
1558
|
+
},
|
|
1559
|
+
handlers: [],
|
|
1560
|
+
errors: []
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1132
1563
|
/**
|
|
1133
1564
|
* ๐ ์ค์ ๋์คํจ์น ์์
์ํ (ํ์์ ํธ์ถ๋จ)
|
|
1134
1565
|
*/
|
|
1135
|
-
async _performDispatch(action, payload, options) {
|
|
1566
|
+
async _performDispatch(action, payload, options, skipGuards, executedHandlers, dispatchHandlerPromises) {
|
|
1136
1567
|
this.log(`Starting dispatch for action '${String(action)}'`, {
|
|
1137
1568
|
hasPayload: payload !== void 0,
|
|
1138
1569
|
payloadType: payload?.constructor?.name || typeof payload,
|
|
@@ -1144,6 +1575,7 @@ var ActionRegister = class {
|
|
|
1144
1575
|
if (options?.autoAbort?.onControllerCreated && autoAbortController) options.autoAbort.onControllerCreated(autoAbortController);
|
|
1145
1576
|
if (effectiveSignal?.aborted) {
|
|
1146
1577
|
this.log(`Dispatch aborted before execution for '${String(action)}'`);
|
|
1578
|
+
cleanup();
|
|
1147
1579
|
return;
|
|
1148
1580
|
}
|
|
1149
1581
|
const pipeline = this.pipelines.get(action);
|
|
@@ -1159,6 +1591,7 @@ var ActionRegister = class {
|
|
|
1159
1591
|
console.warn("๐ก Tip: Register a handler using registry.register() before dispatching this action.");
|
|
1160
1592
|
console.warn("๐ Available actions:", Array.from(this.pipelines.keys()));
|
|
1161
1593
|
this.log(`No handlers found for action '${String(action)}', dispatch cancelled`, {}, "warn");
|
|
1594
|
+
cleanup();
|
|
1162
1595
|
return;
|
|
1163
1596
|
}
|
|
1164
1597
|
const filteredHandlers = options?.filter ? this.filterHandlers(pipeline, options.filter) : pipeline;
|
|
@@ -1179,17 +1612,32 @@ var ActionRegister = class {
|
|
|
1179
1612
|
break;
|
|
1180
1613
|
}
|
|
1181
1614
|
}
|
|
1182
|
-
if (debounceMs !== void 0) {
|
|
1183
|
-
if (!await this.actionGuard.debounce(actionKey, debounceMs))
|
|
1615
|
+
if (!skipGuards && debounceMs !== void 0) {
|
|
1616
|
+
if (!await this.actionGuard.debounce(actionKey, debounceMs)) {
|
|
1617
|
+
cleanup();
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1184
1620
|
}
|
|
1185
|
-
if (throttleMs !== void 0) {
|
|
1186
|
-
if (!this.actionGuard.throttle(actionKey, throttleMs))
|
|
1621
|
+
if (!skipGuards && throttleMs !== void 0) {
|
|
1622
|
+
if (!this.actionGuard.throttle(actionKey, throttleMs)) {
|
|
1623
|
+
cleanup();
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
if (effectiveSignal?.aborted) {
|
|
1628
|
+
this.log(`Dispatch aborted during guard processing for '${String(action)}'`);
|
|
1629
|
+
cleanup();
|
|
1630
|
+
return;
|
|
1187
1631
|
}
|
|
1188
1632
|
const currentExecutionMode = options?.executionMode || this.actionExecutionModes.get(action) || this.executionMode;
|
|
1189
1633
|
const context = {
|
|
1190
1634
|
action: String(action),
|
|
1191
1635
|
payload,
|
|
1192
|
-
handlers: filteredHandlers,
|
|
1636
|
+
handlers: [...filteredHandlers],
|
|
1637
|
+
executedHandlers: [],
|
|
1638
|
+
deferOnceCleanup: true,
|
|
1639
|
+
signal: effectiveSignal ?? this.lifecycleController.signal,
|
|
1640
|
+
trackHandlerPromise: (promise) => this.trackHandlerPromise(promise, dispatchHandlerPromises),
|
|
1193
1641
|
aborted: false,
|
|
1194
1642
|
abortReason: void 0,
|
|
1195
1643
|
currentIndex: 0,
|
|
@@ -1203,17 +1651,19 @@ var ActionRegister = class {
|
|
|
1203
1651
|
};
|
|
1204
1652
|
const abortHandler = effectiveSignal ? () => {
|
|
1205
1653
|
context.aborted = true;
|
|
1206
|
-
context.abortReason = "Action dispatch aborted by signal";
|
|
1654
|
+
context.abortReason = typeof effectiveSignal.reason === "string" ? effectiveSignal.reason : "Action dispatch aborted by signal";
|
|
1207
1655
|
} : void 0;
|
|
1208
|
-
if (effectiveSignal && abortHandler) effectiveSignal.addEventListener("abort", abortHandler);
|
|
1656
|
+
if (effectiveSignal && abortHandler) effectiveSignal.addEventListener("abort", abortHandler, { once: true });
|
|
1209
1657
|
try {
|
|
1210
|
-
await this.executePipeline(context, autoAbortController, options?.autoAbort);
|
|
1658
|
+
await this.executePipeline(context, dispatchHandlerPromises, autoAbortController, options?.autoAbort);
|
|
1211
1659
|
this.log(`Pipeline execution succeeded for ${String(action)}`);
|
|
1212
1660
|
} catch (error) {
|
|
1213
1661
|
this.log(`Pipeline execution failed for ${String(action)}`, error, "error");
|
|
1214
1662
|
throw error;
|
|
1215
1663
|
} finally {
|
|
1216
|
-
|
|
1664
|
+
executedHandlers?.push(...context.executedHandlers ?? []);
|
|
1665
|
+
if (effectiveSignal && abortHandler) effectiveSignal.removeEventListener("abort", abortHandler);
|
|
1666
|
+
this.cleanupSignalsAfterStartedHandlers(cleanup, dispatchHandlerPromises);
|
|
1217
1667
|
}
|
|
1218
1668
|
}
|
|
1219
1669
|
/**
|
|
@@ -1229,41 +1679,70 @@ var ActionRegister = class {
|
|
|
1229
1679
|
*
|
|
1230
1680
|
* @public
|
|
1231
1681
|
*/
|
|
1232
|
-
|
|
1682
|
+
dispatchWithResult(action, payload, options) {
|
|
1683
|
+
if (this.lifecycleState !== "active") return this.rejectedLifecyclePromise();
|
|
1684
|
+
const timeoutScope = this.createTimeoutScope(action, options);
|
|
1685
|
+
const dispatchHandlerPromises = /* @__PURE__ */ new Set();
|
|
1686
|
+
const attemptState = { count: 0 };
|
|
1687
|
+
let validation;
|
|
1688
|
+
const operation = async () => {
|
|
1689
|
+
if (!timeoutScope.options?.signal?.aborted) validation = this.validatePayload(action, payload);
|
|
1690
|
+
return this.executeWithRetry(async () => {
|
|
1691
|
+
const executedHandlers = [];
|
|
1692
|
+
try {
|
|
1693
|
+
return await this._performDispatchWithResult(action, payload, timeoutScope.options, validation, attemptState.count > 1, executedHandlers, dispatchHandlerPromises);
|
|
1694
|
+
} finally {
|
|
1695
|
+
this.cleanupOneTimeHandlers(action, executedHandlers, dispatchHandlerPromises);
|
|
1696
|
+
}
|
|
1697
|
+
}, timeoutScope.options, attemptState, (result) => !result.success && !result.aborted && this.getHandlerCount(action) > 0, () => this.getHandlerCount(action) > 0);
|
|
1698
|
+
};
|
|
1699
|
+
const shouldQueue = !timeoutScope.options?.immediate && Boolean(this.dispatchQueue) && timeoutScope.options?.queuePriority !== void 0;
|
|
1700
|
+
let dispatchPromise;
|
|
1701
|
+
this.dispatchConstructionDepth += 1;
|
|
1702
|
+
try {
|
|
1703
|
+
if (shouldQueue) {
|
|
1704
|
+
const queued = this.dispatchQueue.enqueueWithHandle(operation, timeoutScope.options.queuePriority);
|
|
1705
|
+
timeoutScope.onTimeout((error) => queued.cancel(error));
|
|
1706
|
+
dispatchPromise = queued.promise;
|
|
1707
|
+
} else dispatchPromise = operation();
|
|
1708
|
+
this.trackDispatchPromise(dispatchPromise);
|
|
1709
|
+
} finally {
|
|
1710
|
+
this.dispatchConstructionDepth -= 1;
|
|
1711
|
+
}
|
|
1712
|
+
const observedPromise = this.raceWithTimeout(dispatchPromise, timeoutScope, dispatchHandlerPromises).then((result) => {
|
|
1713
|
+
if (!result.success && !result.aborted) {
|
|
1714
|
+
const terminalError = result.errors[result.errors.length - 1]?.error ?? /* @__PURE__ */ new Error(`Action "${String(action)}" failed`);
|
|
1715
|
+
this.invokeErrorHandler(terminalError, action, payload, options, attemptState.count);
|
|
1716
|
+
}
|
|
1717
|
+
return result;
|
|
1718
|
+
}, (error) => {
|
|
1719
|
+
this.invokeErrorHandler(error, action, payload, options, attemptState.count);
|
|
1720
|
+
throw error;
|
|
1721
|
+
});
|
|
1722
|
+
observedPromise.catch(() => {});
|
|
1723
|
+
return observedPromise;
|
|
1724
|
+
}
|
|
1725
|
+
async _performDispatchWithResult(action, payload, options, validation, skipGuards, executedHandlers, dispatchHandlerPromises) {
|
|
1233
1726
|
const _startTime = Date.now();
|
|
1234
1727
|
const [effectiveSignal, autoAbortController, cleanup] = this.createAbortSignal(options);
|
|
1235
1728
|
if (options?.autoAbort?.onControllerCreated && autoAbortController) options.autoAbort.onControllerCreated(autoAbortController);
|
|
1236
|
-
if (effectiveSignal?.aborted)
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
terminated: false,
|
|
1241
|
-
result: void 0,
|
|
1242
|
-
successResults: [],
|
|
1243
|
-
results: [],
|
|
1244
|
-
failedResults: [],
|
|
1245
|
-
execution: {
|
|
1246
|
-
duration: 0,
|
|
1247
|
-
handlersExecuted: 0,
|
|
1248
|
-
handlersSkipped: 0,
|
|
1249
|
-
handlersFailed: 0,
|
|
1250
|
-
startTime: _startTime,
|
|
1251
|
-
endTime: _startTime
|
|
1252
|
-
},
|
|
1253
|
-
handlers: [],
|
|
1254
|
-
errors: []
|
|
1255
|
-
};
|
|
1729
|
+
if (effectiveSignal?.aborted) {
|
|
1730
|
+
cleanup();
|
|
1731
|
+
return this.createAbortedExecutionResult(_startTime, 0, validation);
|
|
1732
|
+
}
|
|
1256
1733
|
const pipeline = this.pipelines.get(action);
|
|
1257
1734
|
if (!pipeline || pipeline.length === 0) {
|
|
1258
1735
|
const warningMessage = `โ ๏ธ Action '${String(action)}' has no registered handlers. This action will be ignored.`;
|
|
1259
1736
|
console.warn(warningMessage);
|
|
1260
1737
|
console.warn("๐ก Tip: Register a handler using registry.register() before dispatching this action.");
|
|
1261
1738
|
console.warn("๐ Available actions:", Array.from(this.pipelines.keys()));
|
|
1739
|
+
cleanup();
|
|
1262
1740
|
return {
|
|
1263
1741
|
success: true,
|
|
1264
1742
|
aborted: false,
|
|
1265
1743
|
abortReason: void 0,
|
|
1266
1744
|
terminated: false,
|
|
1745
|
+
validation,
|
|
1267
1746
|
result: void 0,
|
|
1268
1747
|
successResults: [],
|
|
1269
1748
|
results: [],
|
|
@@ -1282,13 +1761,27 @@ var ActionRegister = class {
|
|
|
1282
1761
|
}
|
|
1283
1762
|
const filteredHandlers = options?.filter ? this.filterHandlers(pipeline, options.filter) : pipeline;
|
|
1284
1763
|
const actionKey = String(action);
|
|
1285
|
-
const guardResult = await this.applyActionGuardControlsWithResult(actionKey, filteredHandlers, options, _startTime, pipeline.length);
|
|
1286
|
-
if (
|
|
1764
|
+
const guardResult = skipGuards ? null : await this.applyActionGuardControlsWithResult(actionKey, filteredHandlers, options, _startTime, pipeline.length);
|
|
1765
|
+
if (effectiveSignal?.aborted) {
|
|
1766
|
+
cleanup();
|
|
1767
|
+
return this.createAbortedExecutionResult(_startTime, pipeline.length, validation);
|
|
1768
|
+
}
|
|
1769
|
+
if (guardResult) {
|
|
1770
|
+
cleanup();
|
|
1771
|
+
return {
|
|
1772
|
+
...guardResult,
|
|
1773
|
+
validation
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1287
1776
|
const currentExecutionMode = options?.executionMode || this.actionExecutionModes.get(action) || this.executionMode;
|
|
1288
1777
|
const context = {
|
|
1289
1778
|
action: String(action),
|
|
1290
1779
|
payload,
|
|
1291
|
-
handlers: filteredHandlers,
|
|
1780
|
+
handlers: [...filteredHandlers],
|
|
1781
|
+
executedHandlers: [],
|
|
1782
|
+
deferOnceCleanup: true,
|
|
1783
|
+
signal: effectiveSignal ?? this.lifecycleController.signal,
|
|
1784
|
+
trackHandlerPromise: (promise) => this.trackHandlerPromise(promise, dispatchHandlerPromises),
|
|
1292
1785
|
aborted: false,
|
|
1293
1786
|
abortReason: void 0,
|
|
1294
1787
|
currentIndex: 0,
|
|
@@ -1314,12 +1807,12 @@ var ActionRegister = class {
|
|
|
1314
1807
|
});
|
|
1315
1808
|
const abortHandler = effectiveSignal ? () => {
|
|
1316
1809
|
context.aborted = true;
|
|
1317
|
-
context.abortReason = "Action dispatch aborted by signal";
|
|
1810
|
+
context.abortReason = typeof effectiveSignal.reason === "string" ? effectiveSignal.reason : "Action dispatch aborted by signal";
|
|
1318
1811
|
} : void 0;
|
|
1319
|
-
if (effectiveSignal && abortHandler) effectiveSignal.addEventListener("abort", abortHandler);
|
|
1812
|
+
if (effectiveSignal && abortHandler) effectiveSignal.addEventListener("abort", abortHandler, { once: true });
|
|
1320
1813
|
let errors = [];
|
|
1321
1814
|
try {
|
|
1322
|
-
await this.executePipeline(context, autoAbortController, options?.autoAbort);
|
|
1815
|
+
await this.executePipeline(context, dispatchHandlerPromises, autoAbortController, options?.autoAbort);
|
|
1323
1816
|
errors = context.collectedErrors || [];
|
|
1324
1817
|
const executedCount = Math.min(context.currentIndex + (context.aborted ? 0 : 1), filteredHandlers.length);
|
|
1325
1818
|
for (let i = 0; i < executedCount; i++) {
|
|
@@ -1345,7 +1838,9 @@ var ActionRegister = class {
|
|
|
1345
1838
|
if (handlerResult) handlerResult.executed = true;
|
|
1346
1839
|
}
|
|
1347
1840
|
} finally {
|
|
1348
|
-
|
|
1841
|
+
executedHandlers.push(...context.executedHandlers ?? []);
|
|
1842
|
+
if (effectiveSignal && abortHandler) effectiveSignal.removeEventListener("abort", abortHandler);
|
|
1843
|
+
this.cleanupSignalsAfterStartedHandlers(cleanup, dispatchHandlerPromises);
|
|
1349
1844
|
}
|
|
1350
1845
|
const endTime = Date.now();
|
|
1351
1846
|
const processedResult = this.processResults(context, options?.result);
|
|
@@ -1355,11 +1850,12 @@ var ActionRegister = class {
|
|
|
1355
1850
|
error: err.error,
|
|
1356
1851
|
expectedType: typeof processedResult
|
|
1357
1852
|
}));
|
|
1358
|
-
|
|
1853
|
+
return {
|
|
1359
1854
|
success: !executionError && !context.aborted,
|
|
1360
1855
|
aborted: context.aborted,
|
|
1361
1856
|
abortReason: context.abortReason,
|
|
1362
1857
|
terminated: context.terminated,
|
|
1858
|
+
validation,
|
|
1363
1859
|
result: processedResult,
|
|
1364
1860
|
successResults,
|
|
1365
1861
|
results: context.results,
|
|
@@ -1380,9 +1876,6 @@ var ActionRegister = class {
|
|
|
1380
1876
|
severity: "non-blocking"
|
|
1381
1877
|
}))
|
|
1382
1878
|
};
|
|
1383
|
-
/** Clean up one-time handlers after execution */
|
|
1384
|
-
this.cleanupOneTimeHandlers(action, context.handlers);
|
|
1385
|
-
return executionResult;
|
|
1386
1879
|
}
|
|
1387
1880
|
/**
|
|
1388
1881
|
* ๐ง Unified method for dispatchWithResult that returns ExecutionResult on guard rejection
|
|
@@ -1471,6 +1964,7 @@ var ActionRegister = class {
|
|
|
1471
1964
|
getControllerFromPool(context, autoAbortController, autoAbortOptions) {
|
|
1472
1965
|
let controller = this.controllerPool.pop();
|
|
1473
1966
|
if (!controller) controller = {};
|
|
1967
|
+
controller.signal = context.signal ?? this.lifecycleController.signal;
|
|
1474
1968
|
controller.abort = (reason) => {
|
|
1475
1969
|
context.aborted = true;
|
|
1476
1970
|
context.abortReason = reason;
|
|
@@ -1549,7 +2043,7 @@ var ActionRegister = class {
|
|
|
1549
2043
|
return limitedResults[limitedResults.length - 1];
|
|
1550
2044
|
}
|
|
1551
2045
|
}
|
|
1552
|
-
async executePipeline(context, autoAbortController, autoAbortOptions) {
|
|
2046
|
+
async executePipeline(context, dispatchHandlerPromises, autoAbortController, autoAbortOptions) {
|
|
1553
2047
|
const createController = (_registration, _index) => {
|
|
1554
2048
|
return this.getControllerFromPool(context, autoAbortController, autoAbortOptions);
|
|
1555
2049
|
};
|
|
@@ -1565,28 +2059,27 @@ var ActionRegister = class {
|
|
|
1565
2059
|
break;
|
|
1566
2060
|
default: throw new Error(`Unknown execution mode: ${context.executionMode}`);
|
|
1567
2061
|
}
|
|
1568
|
-
this.cleanupOneTimeHandlers(context.action, context.
|
|
2062
|
+
if (!context.deferOnceCleanup) this.cleanupOneTimeHandlers(context.action, context.executedHandlers ?? [], dispatchHandlerPromises);
|
|
1569
2063
|
}
|
|
1570
|
-
cleanupOneTimeHandlers(action, executedHandlers) {
|
|
1571
|
-
const pipeline = this.pipelines.get(action);
|
|
1572
|
-
if (!pipeline) return;
|
|
2064
|
+
cleanupOneTimeHandlers(action, executedHandlers, dispatchHandlerPromises) {
|
|
1573
2065
|
const oneTimeHandlers = executedHandlers.filter((reg) => reg.config.once);
|
|
1574
2066
|
if (oneTimeHandlers.length === 0) return;
|
|
2067
|
+
const handlersStillRunning = [...dispatchHandlerPromises];
|
|
2068
|
+
const shouldDeferCleanup = handlersStillRunning.length > 0;
|
|
1575
2069
|
oneTimeHandlers.forEach((registration) => {
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
2070
|
+
if (this.removeRegistration(action, registration, !shouldDeferCleanup)) {
|
|
2071
|
+
if (shouldDeferCleanup && typeof registration.config.cleanup === "function") {
|
|
2072
|
+
const cleanupPromise = Promise.allSettled(handlersStillRunning).then(() => {
|
|
2073
|
+
this.runRegistrationCleanup(action, registration);
|
|
2074
|
+
});
|
|
2075
|
+
this.trackGlobalHandlerPromise(cleanupPromise).catch(() => {});
|
|
2076
|
+
}
|
|
2077
|
+
this.log(`One-time handler removed: ${String(action)}`, {
|
|
1580
2078
|
handlerId: registration.id,
|
|
1581
|
-
remainingHandlers:
|
|
1582
|
-
registry: this.name
|
|
2079
|
+
remainingHandlers: this.getHandlerCount(action)
|
|
1583
2080
|
});
|
|
1584
2081
|
}
|
|
1585
2082
|
});
|
|
1586
|
-
if (pipeline.length === 0) {
|
|
1587
|
-
this.pipelines.delete(action);
|
|
1588
|
-
this.lastRegisteredTimestamps.delete(action);
|
|
1589
|
-
}
|
|
1590
2083
|
}
|
|
1591
2084
|
/**
|
|
1592
2085
|
* Get the number of registered handlers for an action
|
|
@@ -1639,8 +2132,13 @@ var ActionRegister = class {
|
|
|
1639
2132
|
* @public
|
|
1640
2133
|
*/
|
|
1641
2134
|
clearAction(action) {
|
|
2135
|
+
const pipeline = this.pipelines.get(action);
|
|
2136
|
+
if (pipeline) [...pipeline].forEach((registration) => {
|
|
2137
|
+
this.removeRegistration(action, registration);
|
|
2138
|
+
});
|
|
1642
2139
|
this.pipelines.delete(action);
|
|
1643
2140
|
this.lastRegisteredTimestamps.delete(action);
|
|
2141
|
+
this.actionGuard.clearGuards(String(action));
|
|
1644
2142
|
}
|
|
1645
2143
|
/**
|
|
1646
2144
|
* Remove all handlers for all actions
|
|
@@ -1650,8 +2148,13 @@ var ActionRegister = class {
|
|
|
1650
2148
|
* @public
|
|
1651
2149
|
*/
|
|
1652
2150
|
clearAll() {
|
|
2151
|
+
[...this.pipelines.keys()].forEach((action) => {
|
|
2152
|
+
this.clearAction(action);
|
|
2153
|
+
});
|
|
1653
2154
|
this.pipelines.clear();
|
|
1654
2155
|
this.lastRegisteredTimestamps.clear();
|
|
2156
|
+
this.unregisterFunctions.clear();
|
|
2157
|
+
this.actionGuard.clearAll();
|
|
1655
2158
|
}
|
|
1656
2159
|
/**
|
|
1657
2160
|
* Get the name of this action register
|
|
@@ -1780,29 +2283,36 @@ var ActionRegister = class {
|
|
|
1780
2283
|
*/
|
|
1781
2284
|
createUnregisterFunction(action, handlerId, registration) {
|
|
1782
2285
|
return () => {
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
this.unregisterFunctions.delete(handlerId);
|
|
1789
|
-
if (pipeline.length === 0) {
|
|
1790
|
-
this.pipelines.delete(action);
|
|
1791
|
-
this.lastRegisteredTimestamps.delete(action);
|
|
1792
|
-
}
|
|
1793
|
-
if (registration.config.cleanup && typeof registration.config.cleanup === "function") try {
|
|
1794
|
-
registration.config.cleanup();
|
|
1795
|
-
} catch (cleanupError) {
|
|
1796
|
-
this.log(`Cleanup error during unregister: ${String(action)}`, cleanupError, "warn");
|
|
1797
|
-
}
|
|
1798
|
-
this.log(`Handler unregistered: ${String(action)}`, {
|
|
1799
|
-
handlerId,
|
|
1800
|
-
remainingHandlers: pipeline.length,
|
|
1801
|
-
actionRemoved: pipeline.length === 0
|
|
1802
|
-
});
|
|
1803
|
-
}
|
|
2286
|
+
if (this.removeRegistration(action, registration)) this.log(`Handler unregistered: ${String(action)}`, {
|
|
2287
|
+
handlerId,
|
|
2288
|
+
remainingHandlers: this.getHandlerCount(action),
|
|
2289
|
+
actionRemoved: !this.pipelines.has(action)
|
|
2290
|
+
});
|
|
1804
2291
|
};
|
|
1805
2292
|
}
|
|
2293
|
+
/** Remove a registration and release every resource owned by it exactly once. */
|
|
2294
|
+
removeRegistration(action, registration, runCleanup = true) {
|
|
2295
|
+
const pipeline = this.pipelines.get(action);
|
|
2296
|
+
if (!pipeline) return false;
|
|
2297
|
+
const index = pipeline.findIndex((candidate) => candidate === registration);
|
|
2298
|
+
if (index === -1) return false;
|
|
2299
|
+
pipeline.splice(index, 1);
|
|
2300
|
+
this.unregisterFunctions.delete(registration.id);
|
|
2301
|
+
if (runCleanup) this.runRegistrationCleanup(action, registration);
|
|
2302
|
+
if (pipeline.length === 0) {
|
|
2303
|
+
this.pipelines.delete(action);
|
|
2304
|
+
this.lastRegisteredTimestamps.delete(action);
|
|
2305
|
+
}
|
|
2306
|
+
return true;
|
|
2307
|
+
}
|
|
2308
|
+
runRegistrationCleanup(action, registration) {
|
|
2309
|
+
if (!registration.config.cleanup) return;
|
|
2310
|
+
try {
|
|
2311
|
+
registration.config.cleanup();
|
|
2312
|
+
} catch (cleanupError) {
|
|
2313
|
+
this.log(`Cleanup error while removing handler: ${String(action)}`, cleanupError, "warn");
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
1806
2316
|
/**
|
|
1807
2317
|
* Gets the total count of registered unregister functions
|
|
1808
2318
|
*
|
|
@@ -1822,29 +2332,77 @@ var ActionRegister = class {
|
|
|
1822
2332
|
hasUnregisterFunction(handlerId) {
|
|
1823
2333
|
return this.unregisterFunctions.has(handlerId);
|
|
1824
2334
|
}
|
|
1825
|
-
/**
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
this.unregisterFunctions.clear();
|
|
1835
|
-
for (const [action, pipeline] of this.pipelines.entries()) for (const registration of pipeline) if (registration.config.cleanup && typeof registration.config.cleanup === "function") try {
|
|
1836
|
-
registration.config.cleanup();
|
|
1837
|
-
} catch (cleanupError) {
|
|
1838
|
-
this.log(`Cleanup error for handler during destroy: ${String(action)}`, cleanupError, "warn");
|
|
2335
|
+
/** Reject queued dispatches without releasing registered handlers. */
|
|
2336
|
+
cancelPendingDispatches() {
|
|
2337
|
+
this.dispatchQueue?.clear({ rejectPending: true });
|
|
2338
|
+
}
|
|
2339
|
+
beginShutdown() {
|
|
2340
|
+
if (this.destroyAsyncPromise) return this.destroyAsyncPromise;
|
|
2341
|
+
if (this.lifecycleState === "destroyed") {
|
|
2342
|
+
this.destroyAsyncPromise = Promise.resolve();
|
|
2343
|
+
return this.destroyAsyncPromise;
|
|
1839
2344
|
}
|
|
1840
|
-
this.
|
|
1841
|
-
this.
|
|
2345
|
+
this.lifecycleState = "closing";
|
|
2346
|
+
const shutdownError = new ActionRegisterDestroyedError(this.name, "closing");
|
|
2347
|
+
let resolveShutdown;
|
|
2348
|
+
let rejectShutdown;
|
|
2349
|
+
this.destroyAsyncPromise = new Promise((resolve, reject) => {
|
|
2350
|
+
resolveShutdown = resolve;
|
|
2351
|
+
rejectShutdown = reject;
|
|
2352
|
+
});
|
|
2353
|
+
if (!this.lifecycleController.signal.aborted) this.lifecycleController.abort(shutdownError);
|
|
1842
2354
|
this.actionGuard.destroy();
|
|
1843
|
-
this.dispatchQueue?.clear
|
|
2355
|
+
this.dispatchQueue?.clear({
|
|
2356
|
+
rejectPending: true,
|
|
2357
|
+
reason: shutdownError
|
|
2358
|
+
});
|
|
2359
|
+
if (this.dispatchConstructionDepth === 0 && this.activeDispatches.size === 0 && this.activeHandlerPromises.size === 0) {
|
|
2360
|
+
this.finalizeDestroy();
|
|
2361
|
+
resolveShutdown();
|
|
2362
|
+
return this.destroyAsyncPromise;
|
|
2363
|
+
}
|
|
2364
|
+
const drainAndFinalize = async () => {
|
|
2365
|
+
while (this.activeDispatches.size > 0 || this.activeHandlerPromises.size > 0) await Promise.allSettled([...this.activeDispatches, ...this.activeHandlerPromises]);
|
|
2366
|
+
this.finalizeDestroy();
|
|
2367
|
+
};
|
|
2368
|
+
Promise.resolve().then(drainAndFinalize).then(resolveShutdown, rejectShutdown);
|
|
2369
|
+
this.destroyAsyncPromise.catch((error) => {
|
|
2370
|
+
this.log("ActionRegister async destroy failed", error, "warn");
|
|
2371
|
+
});
|
|
2372
|
+
return this.destroyAsyncPromise;
|
|
2373
|
+
}
|
|
2374
|
+
finalizeDestroy() {
|
|
2375
|
+
if (this.lifecycleState === "destroyed") return;
|
|
2376
|
+
this.clearAll();
|
|
1844
2377
|
this.actionExecutionModes.clear();
|
|
1845
2378
|
this.controllerPool.length = 0;
|
|
2379
|
+
this.lifecycleState = "destroyed";
|
|
1846
2380
|
this.log("ActionRegister destroyed");
|
|
1847
2381
|
}
|
|
2382
|
+
/**
|
|
2383
|
+
* ๐ Destroy method for comprehensive cleanup
|
|
2384
|
+
*
|
|
2385
|
+
* Begins terminal cleanup of pipelines, guards, queues, and statistics. Cleanup
|
|
2386
|
+
* remains synchronous when no work has started; otherwise active handlers drain
|
|
2387
|
+
* in the background. Use destroyAsync() when completion must be observed.
|
|
2388
|
+
*
|
|
2389
|
+
* @public
|
|
2390
|
+
*/
|
|
2391
|
+
destroy() {
|
|
2392
|
+
this.beginShutdown();
|
|
2393
|
+
}
|
|
2394
|
+
/**
|
|
2395
|
+
* Begin terminal shutdown and resolve after all started handlers have settled
|
|
2396
|
+
* and their registered cleanup functions have run.
|
|
2397
|
+
*
|
|
2398
|
+
* Repeated calls return the same promise. New registrations and dispatches are
|
|
2399
|
+
* rejected as soon as shutdown begins.
|
|
2400
|
+
*
|
|
2401
|
+
* @public
|
|
2402
|
+
*/
|
|
2403
|
+
destroyAsync() {
|
|
2404
|
+
return this.beginShutdown();
|
|
2405
|
+
}
|
|
1848
2406
|
};
|
|
1849
2407
|
|
|
1850
2408
|
//#endregion
|
|
@@ -1933,12 +2491,18 @@ function createActionHandler(registry, action, handler, config) {
|
|
|
1933
2491
|
let currentUnregister;
|
|
1934
2492
|
let isRegistered = false;
|
|
1935
2493
|
return {
|
|
2494
|
+
/**
|
|
2495
|
+
* Register the handler and return cleanup function
|
|
2496
|
+
*/
|
|
1936
2497
|
register() {
|
|
1937
2498
|
if (isRegistered && currentUnregister) currentUnregister();
|
|
1938
2499
|
currentUnregister = registry.register(action, handler, finalConfig);
|
|
1939
2500
|
isRegistered = true;
|
|
1940
2501
|
return currentUnregister;
|
|
1941
2502
|
},
|
|
2503
|
+
/**
|
|
2504
|
+
* Unregister the handler if currently registered
|
|
2505
|
+
*/
|
|
1942
2506
|
unregister() {
|
|
1943
2507
|
if (isRegistered && currentUnregister) {
|
|
1944
2508
|
currentUnregister();
|
|
@@ -1946,6 +2510,9 @@ function createActionHandler(registry, action, handler, config) {
|
|
|
1946
2510
|
isRegistered = false;
|
|
1947
2511
|
}
|
|
1948
2512
|
},
|
|
2513
|
+
/**
|
|
2514
|
+
* Register and return cleanup function (React useEffect pattern)
|
|
2515
|
+
*/
|
|
1949
2516
|
registerWithCleanup() {
|
|
1950
2517
|
const unregisterFn = this.register();
|
|
1951
2518
|
return () => {
|
|
@@ -1962,18 +2529,33 @@ function createActionHandler(registry, action, handler, config) {
|
|
|
1962
2529
|
* Provides debugging and development helpers specifically for React environments.
|
|
1963
2530
|
*/
|
|
1964
2531
|
const ReactDevUtils = {
|
|
2532
|
+
/**
|
|
2533
|
+
* Enable detailed React integration debugging
|
|
2534
|
+
*/
|
|
1965
2535
|
enableDebugMode() {
|
|
1966
2536
|
if (typeof window !== "undefined") window.__CONTEXT_ACTION_REACT_DEBUG__ = true;
|
|
1967
2537
|
},
|
|
2538
|
+
/**
|
|
2539
|
+
* Disable React integration debugging
|
|
2540
|
+
*/
|
|
1968
2541
|
disableDebugMode() {
|
|
1969
2542
|
if (typeof window !== "undefined") window.__CONTEXT_ACTION_REACT_DEBUG__ = false;
|
|
1970
2543
|
},
|
|
2544
|
+
/**
|
|
2545
|
+
* Check if React debug mode is enabled
|
|
2546
|
+
*/
|
|
1971
2547
|
isDebugMode() {
|
|
1972
2548
|
return typeof window !== "undefined" && Boolean(window.__CONTEXT_ACTION_REACT_DEBUG__);
|
|
1973
2549
|
},
|
|
2550
|
+
/**
|
|
2551
|
+
* Log React-specific debugging information
|
|
2552
|
+
*/
|
|
1974
2553
|
log(component, action, message, data) {
|
|
1975
2554
|
if (this.isDebugMode()) console.log(`๐ฏ [React-ActionRegister] [${component}] ${action}: ${message}`, data || "");
|
|
1976
2555
|
},
|
|
2556
|
+
/**
|
|
2557
|
+
* Get React integration statistics
|
|
2558
|
+
*/
|
|
1977
2559
|
getStats(registry) {
|
|
1978
2560
|
const registryInfo = registry.getRegistryInfo();
|
|
1979
2561
|
let reactHandlers = 0;
|
|
@@ -2025,5 +2607,140 @@ function isReactActionError(error) {
|
|
|
2025
2607
|
}
|
|
2026
2608
|
|
|
2027
2609
|
//#endregion
|
|
2028
|
-
|
|
2610
|
+
//#region src/action-schema.ts
|
|
2611
|
+
/**
|
|
2612
|
+
* Zod ์คํค๋ง๋ฅผ JSON Schema๋ก ๋ณํ (Zod 4 ๋ค์ดํฐ๋ธ API)
|
|
2613
|
+
*
|
|
2614
|
+
* @param schema - Zod ์คํค๋ง
|
|
2615
|
+
* @returns JSON Schema (draft-7)
|
|
2616
|
+
*/
|
|
2617
|
+
function zodToJsonSchema(schema, zodModule) {
|
|
2618
|
+
return zodModule.toJSONSchema(schema, {
|
|
2619
|
+
target: "draft-7",
|
|
2620
|
+
metadata: zodModule.globalRegistry
|
|
2621
|
+
});
|
|
2622
|
+
}
|
|
2623
|
+
/**
|
|
2624
|
+
* Zod ์คํค๋ง ๊ธฐ๋ฐ Action ์ ์
|
|
2625
|
+
*
|
|
2626
|
+
* defineTool ํจํด์ ๊ธฐ๋ฐ์ผ๋ก context-action์ ๋ง๊ฒ ๊ตฌํ:
|
|
2627
|
+
* - Single Source of Truth: Zod ์คํค๋ง๋ก ํ์
+ ๊ฒ์ฆ + ๋ฉํ๋ฐ์ดํฐ ํตํฉ
|
|
2628
|
+
* - ๋ฐํ์ ๊ฒ์ฆ: validate(), safeParse()
|
|
2629
|
+
* - Tool Chain ํธํ: toMCP(), toOpenAI(), toAnthropic()
|
|
2630
|
+
*
|
|
2631
|
+
* @param options - Action ์ ์ ์ต์
|
|
2632
|
+
* @param zodModule - Zod ๋ชจ๋ (peerDependency๋ก ์ฃผ์
)
|
|
2633
|
+
* @returns UnifiedAction ์ธ์คํด์ค
|
|
2634
|
+
*
|
|
2635
|
+
* @example
|
|
2636
|
+
* ```typescript
|
|
2637
|
+
* import { z } from 'zod';
|
|
2638
|
+
* import { defineAction } from '@context-action/core';
|
|
2639
|
+
*
|
|
2640
|
+
* const updateUserAction = defineAction({
|
|
2641
|
+
* name: 'updateUser',
|
|
2642
|
+
* description: 'Update user profile',
|
|
2643
|
+
* parameters: z.object({
|
|
2644
|
+
* id: z.string().min(1).meta({ description: 'User ID' }),
|
|
2645
|
+
* name: z.string().min(2).max(50).meta({ description: 'User name' }),
|
|
2646
|
+
* email: z.string().email().optional(),
|
|
2647
|
+
* }),
|
|
2648
|
+
* }, z);
|
|
2649
|
+
*
|
|
2650
|
+
* // ๊ฒ์ฆ
|
|
2651
|
+
* const validated = updateUserAction.validate({ id: '123', name: 'John' });
|
|
2652
|
+
*
|
|
2653
|
+
* // Tool chain ๋ณํ
|
|
2654
|
+
* const mcpTool = updateUserAction.toMCP();
|
|
2655
|
+
* ```
|
|
2656
|
+
*/
|
|
2657
|
+
function defineAction(options, zodModule) {
|
|
2658
|
+
const { name, description, parameters } = options;
|
|
2659
|
+
const jsonSchema = zodToJsonSchema(parameters, zodModule);
|
|
2660
|
+
return {
|
|
2661
|
+
name,
|
|
2662
|
+
description,
|
|
2663
|
+
zodSchema: parameters,
|
|
2664
|
+
jsonSchema,
|
|
2665
|
+
validate: (payload) => {
|
|
2666
|
+
return parameters.parse(payload);
|
|
2667
|
+
},
|
|
2668
|
+
safeParse: (payload) => {
|
|
2669
|
+
return parameters.safeParse(payload);
|
|
2670
|
+
},
|
|
2671
|
+
toJSONSchema: () => jsonSchema,
|
|
2672
|
+
toMCP: () => ({
|
|
2673
|
+
name,
|
|
2674
|
+
description,
|
|
2675
|
+
inputSchema: jsonSchema
|
|
2676
|
+
}),
|
|
2677
|
+
toOpenAI: () => ({
|
|
2678
|
+
type: "function",
|
|
2679
|
+
function: {
|
|
2680
|
+
name,
|
|
2681
|
+
description,
|
|
2682
|
+
parameters: {
|
|
2683
|
+
type: "object",
|
|
2684
|
+
properties: jsonSchema.properties ?? {},
|
|
2685
|
+
required: jsonSchema.required
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
}),
|
|
2689
|
+
toAnthropic: () => ({
|
|
2690
|
+
name,
|
|
2691
|
+
description,
|
|
2692
|
+
input_schema: jsonSchema
|
|
2693
|
+
})
|
|
2694
|
+
};
|
|
2695
|
+
}
|
|
2696
|
+
/**
|
|
2697
|
+
* ๋ค์ค Action ์คํค๋ง ์์ฑ
|
|
2698
|
+
*
|
|
2699
|
+
* ์ฌ๋ฌ defineAction์ ๋ฌถ์ด์ ActionSchemaMap ์์ฑ
|
|
2700
|
+
*
|
|
2701
|
+
* @param actions - UnifiedAction ๋งต
|
|
2702
|
+
* @returns ActionSchemaMap
|
|
2703
|
+
*
|
|
2704
|
+
* @example
|
|
2705
|
+
* ```typescript
|
|
2706
|
+
* const userActionSchema = createActionSchema({
|
|
2707
|
+
* updateUser: defineAction({ ... }, z),
|
|
2708
|
+
* deleteUser: defineAction({ ... }, z),
|
|
2709
|
+
* });
|
|
2710
|
+
*
|
|
2711
|
+
* type UserActions = InferActionPayloadMap<typeof userActionSchema>;
|
|
2712
|
+
* ```
|
|
2713
|
+
*/
|
|
2714
|
+
function createActionSchema(actions) {
|
|
2715
|
+
return actions;
|
|
2716
|
+
}
|
|
2717
|
+
/**
|
|
2718
|
+
* Zod ๋ชจ๋์ ๋ฐ์ธ๋ฉํ defineAction ํฉํ ๋ฆฌ ์์ฑ
|
|
2719
|
+
*
|
|
2720
|
+
* ๋งค๋ฒ z ๋ชจ๋์ ์ ๋ฌํ์ง ์์๋ ๋๋๋ก ํฉํ ๋ฆฌ ํจํด ์ ๊ณต
|
|
2721
|
+
*
|
|
2722
|
+
* @param zodModule - Zod ๋ชจ๋
|
|
2723
|
+
* @returns defineAction ํจ์ (z ๋ฐ์ธ๋ฉ๋จ)
|
|
2724
|
+
*
|
|
2725
|
+
* @example
|
|
2726
|
+
* ```typescript
|
|
2727
|
+
* import { z } from 'zod';
|
|
2728
|
+
* import { createActionFactory } from '@context-action/core';
|
|
2729
|
+
*
|
|
2730
|
+
* const defineAction = createActionFactory(z);
|
|
2731
|
+
*
|
|
2732
|
+
* const updateUser = defineAction({
|
|
2733
|
+
* name: 'updateUser',
|
|
2734
|
+
* parameters: z.object({ id: z.string() }),
|
|
2735
|
+
* });
|
|
2736
|
+
* ```
|
|
2737
|
+
*/
|
|
2738
|
+
function createActionFactory(zodModule) {
|
|
2739
|
+
return (options) => {
|
|
2740
|
+
return defineAction(options, zodModule);
|
|
2741
|
+
};
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2744
|
+
//#endregion
|
|
2745
|
+
export { ActionGuard, ActionRegister, ActionRegisterDestroyedError, ActionTimeoutError, ActionValidationError, ReactActionError, ReactDevUtils, createActionFactory, createActionHandler, createActionSchema, defineAction, executeParallel, executeRace, executeSequential, isActionRegisterDestroyedError, isActionTimeoutError, isActionValidationError, isReactActionError, zodToJsonSchema };
|
|
2029
2746
|
//# sourceMappingURL=index.js.map
|