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