@context-action/core 0.9.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -38
- package/dist/index.cjs +1469 -742
- package/dist/index.d.cts +187 -44
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +187 -44
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1469 -742
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,292 +1,3 @@
|
|
|
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
|
-
}
|
|
5
|
-
/**
|
|
6
|
-
* Create standardized error handling for handlers
|
|
7
|
-
*
|
|
8
|
-
* @param error - The error that occurred
|
|
9
|
-
* @param registration - The handler registration that failed
|
|
10
|
-
* @returns Standardized HandlerError object
|
|
11
|
-
*
|
|
12
|
-
* @internal
|
|
13
|
-
*/
|
|
14
|
-
function handleExecutionError(error, registration) {
|
|
15
|
-
const errorObj = error instanceof Error ? error : new Error(String(error));
|
|
16
|
-
return {
|
|
17
|
-
handlerId: registration.id,
|
|
18
|
-
error: errorObj,
|
|
19
|
-
timestamp: Date.now(),
|
|
20
|
-
severity: registration.config.blocking ? "blocking" : "non-blocking"
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Execute handlers in sequential mode (one after another)
|
|
25
|
-
*
|
|
26
|
-
* Executes action handlers one at a time in priority order (highest first).
|
|
27
|
-
* Supports both blocking and non-blocking handlers, with proper abort and
|
|
28
|
-
* termination handling. Handlers can modify payload for subsequent handlers
|
|
29
|
-
* and jump to different priority levels.
|
|
30
|
-
*
|
|
31
|
-
* @template T - The payload type for the action
|
|
32
|
-
* @template R - The result type for handlers
|
|
33
|
-
*
|
|
34
|
-
* @param context - Pipeline execution context containing handlers and state
|
|
35
|
-
* @param createController - Factory function for creating pipeline controllers
|
|
36
|
-
*
|
|
37
|
-
* @throws {Error} When a blocking handler fails
|
|
38
|
-
*
|
|
39
|
-
* @see https://mineclover.github.io/context-action/en/guide/patterns/action/dispatch-patterns
|
|
40
|
-
*
|
|
41
|
-
* @public
|
|
42
|
-
*/
|
|
43
|
-
async function executeSequential(context, createController) {
|
|
44
|
-
let i = 0;
|
|
45
|
-
const nonBlockingPromises = [];
|
|
46
|
-
const errors = [];
|
|
47
|
-
while (i < context.handlers.length) {
|
|
48
|
-
if (context.aborted || context.terminated) break;
|
|
49
|
-
const registration = context.handlers[i];
|
|
50
|
-
if (!registration) continue;
|
|
51
|
-
context.currentIndex = i;
|
|
52
|
-
const controller = createController(registration, i);
|
|
53
|
-
try {
|
|
54
|
-
if (context.aborted) break;
|
|
55
|
-
if (registration.config.condition) try {
|
|
56
|
-
if (!registration.config.condition(context.payload)) {
|
|
57
|
-
i++;
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
} catch {
|
|
61
|
-
i++;
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
(context.executedHandlers ?? (context.executedHandlers = [])).push(registration);
|
|
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;
|
|
68
|
-
if (registration.config.blocking) {
|
|
69
|
-
const handlerResult = trackedResult ? await trackedResult : result;
|
|
70
|
-
if (handlerResult !== void 0 && !context.terminated) context.results.push(handlerResult);
|
|
71
|
-
} else if (trackedResult) {
|
|
72
|
-
const promiseWithErrorHandling = trackedResult.then((asyncResult) => {
|
|
73
|
-
if (asyncResult !== void 0 && !context.terminated) context.results.push(asyncResult);
|
|
74
|
-
return asyncResult;
|
|
75
|
-
}).catch((error) => {
|
|
76
|
-
const handlerError = handleExecutionError(error, registration);
|
|
77
|
-
errors.push({
|
|
78
|
-
handlerId: handlerError.handlerId,
|
|
79
|
-
error: handlerError.error,
|
|
80
|
-
timestamp: handlerError.timestamp,
|
|
81
|
-
severity: "non-blocking"
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
nonBlockingPromises.push(promiseWithErrorHandling);
|
|
85
|
-
} else if (result !== void 0 && !context.terminated) context.results.push(result);
|
|
86
|
-
/** Check if pipeline was terminated by controller.return() */
|
|
87
|
-
if (context.terminated) break;
|
|
88
|
-
/** Handle jump to priority AFTER handler execution */
|
|
89
|
-
if (context.jumpToPriority !== void 0) {
|
|
90
|
-
context.jumpCount = (context.jumpCount || 0) + 1;
|
|
91
|
-
if (context.jumpCount > (context.maxJumps || 10)) {
|
|
92
|
-
console.error(`[ActionRegister] ERROR: Maximum jump limit (${context.maxJumps || 10}) exceeded. Aborting to prevent infinite loop. Check your jumpToPriority logic and conditions.`);
|
|
93
|
-
context.aborted = true;
|
|
94
|
-
context.abortReason = `Maximum jump limit exceeded (${context.jumpCount} jumps)`;
|
|
95
|
-
context.jumpToPriority = void 0;
|
|
96
|
-
break;
|
|
97
|
-
}
|
|
98
|
-
const jumpIndex = context.handlers.findIndex((handler) => (handler.config.priority || 0) <= context.jumpToPriority);
|
|
99
|
-
if (jumpIndex !== -1 && jumpIndex !== i) {
|
|
100
|
-
if (jumpIndex < i) {
|
|
101
|
-
const targetHandler = context.handlers[jumpIndex];
|
|
102
|
-
if (targetHandler && !targetHandler.config.condition) console.warn(`[ActionRegister] WARNING: Backward jumpToPriority to handler '${targetHandler.config.id || "unnamed"}' without condition. This may cause infinite loops! Consider adding a condition to prevent re-execution. Jump count: ${context.jumpCount}/${context.maxJumps || 10}`);
|
|
103
|
-
}
|
|
104
|
-
i = jumpIndex;
|
|
105
|
-
context.jumpToPriority = void 0;
|
|
106
|
-
} else {
|
|
107
|
-
context.jumpToPriority = void 0;
|
|
108
|
-
i++;
|
|
109
|
-
}
|
|
110
|
-
} else i++;
|
|
111
|
-
} catch (error) {
|
|
112
|
-
const handlerError = handleExecutionError(error, registration);
|
|
113
|
-
errors.push(handlerError);
|
|
114
|
-
if (registration.config.blocking) throw handlerError.error;
|
|
115
|
-
i++;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
if (nonBlockingPromises.length > 0) await Promise.allSettled(nonBlockingPromises);
|
|
119
|
-
if (errors.length > 0) context.collectedErrors = errors.map((err) => ({
|
|
120
|
-
handlerId: err.handlerId,
|
|
121
|
-
error: err.error,
|
|
122
|
-
timestamp: err.timestamp,
|
|
123
|
-
severity: "non-blocking"
|
|
124
|
-
}));
|
|
125
|
-
}
|
|
126
|
-
/**
|
|
127
|
-
* Execute handlers in parallel mode (all at once)
|
|
128
|
-
*
|
|
129
|
-
* Executes all qualifying action handlers simultaneously using Promise.allSettled.
|
|
130
|
-
* Supports both blocking and non-blocking handlers. Blocking handlers can still
|
|
131
|
-
* fail the entire pipeline if they throw errors.
|
|
132
|
-
*
|
|
133
|
-
* @template T - The payload type for the action
|
|
134
|
-
* @template R - The result type for handlers
|
|
135
|
-
*
|
|
136
|
-
* @param context - Pipeline execution context containing handlers and state
|
|
137
|
-
* @param createController - Factory function for creating pipeline controllers
|
|
138
|
-
*
|
|
139
|
-
* @throws {Error} When any blocking handler fails
|
|
140
|
-
*
|
|
141
|
-
* @see https://mineclover.github.io/context-action/en/guide/patterns/action/dispatch-patterns#parallel-execution
|
|
142
|
-
*
|
|
143
|
-
* @public
|
|
144
|
-
*/
|
|
145
|
-
async function executeParallel(context, createController) {
|
|
146
|
-
/** All handlers are runnable */
|
|
147
|
-
const runnableHandlers = context.handlers;
|
|
148
|
-
/** Create promises for all handlers */
|
|
149
|
-
const handlerPromises = runnableHandlers.map(async (registration, _index) => {
|
|
150
|
-
const controller = createController(registration, _index);
|
|
151
|
-
try {
|
|
152
|
-
if (registration.config.condition) try {
|
|
153
|
-
if (!registration.config.condition(context.payload)) return {
|
|
154
|
-
success: true,
|
|
155
|
-
handlerId: registration.id,
|
|
156
|
-
result: void 0,
|
|
157
|
-
terminated: false,
|
|
158
|
-
skipped: true
|
|
159
|
-
};
|
|
160
|
-
} catch {
|
|
161
|
-
return {
|
|
162
|
-
success: true,
|
|
163
|
-
handlerId: registration.id,
|
|
164
|
-
result: void 0,
|
|
165
|
-
terminated: false,
|
|
166
|
-
skipped: true
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
(context.executedHandlers ?? (context.executedHandlers = [])).push(registration);
|
|
170
|
-
const result = registration.handler(context.payload, controller);
|
|
171
|
-
const handlerResult = isPromiseLike(result) ? await Promise.resolve(result) : result;
|
|
172
|
-
/** Collect result if handler returned something and pipeline wasn't terminated */
|
|
173
|
-
if (handlerResult !== void 0 && !context.terminated) context.results.push(handlerResult);
|
|
174
|
-
return {
|
|
175
|
-
success: true,
|
|
176
|
-
handlerId: registration.id,
|
|
177
|
-
result: handlerResult,
|
|
178
|
-
terminated: context.terminated
|
|
179
|
-
};
|
|
180
|
-
} catch (error) {
|
|
181
|
-
const handlerError = handleExecutionError(error, registration);
|
|
182
|
-
if (handlerError.severity === "blocking") throw handlerError.error;
|
|
183
|
-
return {
|
|
184
|
-
success: false,
|
|
185
|
-
handlerId: registration.id,
|
|
186
|
-
error: handlerError.error
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
});
|
|
190
|
-
const trackedHandlerPromises = context.trackHandlerPromise ? handlerPromises.map((promise) => context.trackHandlerPromise(promise)) : handlerPromises;
|
|
191
|
-
/** Wait for all handlers to complete */
|
|
192
|
-
const results = await Promise.allSettled(trackedHandlerPromises);
|
|
193
|
-
/** Check for any rejected blocking handlers */
|
|
194
|
-
const failures = results.filter((result, index) => {
|
|
195
|
-
if (result.status === "rejected") return runnableHandlers[index]?.config.blocking ?? false;
|
|
196
|
-
return false;
|
|
197
|
-
});
|
|
198
|
-
if (failures.length > 0) throw failures[0].reason;
|
|
199
|
-
/** Check if any handler terminated the pipeline */
|
|
200
|
-
const terminatedResults = results.filter((result) => result.status === "fulfilled" && result.value.terminated);
|
|
201
|
-
if (terminatedResults.length > 0) {
|
|
202
|
-
context.terminated = true;
|
|
203
|
-
context.terminationResult = terminatedResults[0].value.result;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
/**
|
|
207
|
-
* Execute handlers in race mode (first to complete wins)
|
|
208
|
-
*
|
|
209
|
-
* Executes all qualifying handlers simultaneously using Promise.race, where
|
|
210
|
-
* the first handler to complete determines the pipeline result. Other handlers
|
|
211
|
-
* continue in the background and remain tracked for lifecycle cleanup; handlers
|
|
212
|
-
* must observe the controller signal for cooperative external cancellation.
|
|
213
|
-
* Useful for scenarios where you want the fastest response from multiple
|
|
214
|
-
* equivalent handlers.
|
|
215
|
-
*
|
|
216
|
-
* @template T - The payload type for the action
|
|
217
|
-
* @template R - The result type for handlers
|
|
218
|
-
*
|
|
219
|
-
* @param context - Pipeline execution context containing handlers and state
|
|
220
|
-
* @param createController - Factory function for creating pipeline controllers
|
|
221
|
-
*
|
|
222
|
-
* @throws {Error} When the winning handler fails and is blocking
|
|
223
|
-
*
|
|
224
|
-
* @see https://mineclover.github.io/context-action/en/guide/patterns/action/dispatch-patterns#race-execution
|
|
225
|
-
*
|
|
226
|
-
* @public
|
|
227
|
-
*/
|
|
228
|
-
async function executeRace(context, createController) {
|
|
229
|
-
/** All handlers are runnable */
|
|
230
|
-
const runnableHandlers = context.handlers;
|
|
231
|
-
if (runnableHandlers.length === 0) return;
|
|
232
|
-
/** Create promises for all handlers */
|
|
233
|
-
const handlerPromises = runnableHandlers.map(async (registration, _index) => {
|
|
234
|
-
const controller = createController(registration, _index);
|
|
235
|
-
try {
|
|
236
|
-
if (registration.config.condition) try {
|
|
237
|
-
if (!registration.config.condition(context.payload)) return {
|
|
238
|
-
success: true,
|
|
239
|
-
handlerId: registration.id,
|
|
240
|
-
registration,
|
|
241
|
-
result: void 0,
|
|
242
|
-
terminated: false,
|
|
243
|
-
skipped: true
|
|
244
|
-
};
|
|
245
|
-
} catch {
|
|
246
|
-
return {
|
|
247
|
-
success: true,
|
|
248
|
-
handlerId: registration.id,
|
|
249
|
-
registration,
|
|
250
|
-
result: void 0,
|
|
251
|
-
terminated: false,
|
|
252
|
-
skipped: true
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
(context.executedHandlers ?? (context.executedHandlers = [])).push(registration);
|
|
256
|
-
const result = registration.handler(context.payload, controller);
|
|
257
|
-
const handlerResult = isPromiseLike(result) ? await Promise.resolve(result) : result;
|
|
258
|
-
return {
|
|
259
|
-
success: true,
|
|
260
|
-
handlerId: registration.id,
|
|
261
|
-
registration,
|
|
262
|
-
result: handlerResult,
|
|
263
|
-
terminated: context.terminated
|
|
264
|
-
};
|
|
265
|
-
} catch (error) {
|
|
266
|
-
const handlerError = handleExecutionError(error, registration);
|
|
267
|
-
return {
|
|
268
|
-
success: false,
|
|
269
|
-
handlerId: registration.id,
|
|
270
|
-
error: handlerError.error,
|
|
271
|
-
registration
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
});
|
|
275
|
-
const trackedHandlerPromises = context.trackHandlerPromise ? handlerPromises.map((promise) => context.trackHandlerPromise(promise)) : handlerPromises;
|
|
276
|
-
/** Race all handlers while retaining every loser for lifecycle draining. */
|
|
277
|
-
const winner = await Promise.race(trackedHandlerPromises);
|
|
278
|
-
/** If the winner failed and was blocking, throw the error */
|
|
279
|
-
if (!winner.success && winner.registration?.config.blocking) throw winner.error;
|
|
280
|
-
/** Collect result from the winning handler */
|
|
281
|
-
if (winner.success && winner.result !== void 0) context.results.push(winner.result);
|
|
282
|
-
/** Check if the winning handler terminated the pipeline */
|
|
283
|
-
if (winner.success && winner.terminated) {
|
|
284
|
-
context.terminated = true;
|
|
285
|
-
context.terminationResult = winner.result;
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
//#endregion
|
|
290
1
|
//#region src/action-guard.ts
|
|
291
2
|
/**
|
|
292
3
|
* Action Guard system for managing action execution timing
|
|
@@ -321,8 +32,6 @@ var ActionGuard = class {
|
|
|
321
32
|
this.guards = /* @__PURE__ */ new Map();
|
|
322
33
|
this.maxIdleTime = 6e4;
|
|
323
34
|
this.cleanupIntervalMs = 3e4;
|
|
324
|
-
this.maxGuards = 1e3;
|
|
325
|
-
this.accessOrder = [];
|
|
326
35
|
this.autoCleanupEnabled = autoCleanup;
|
|
327
36
|
}
|
|
328
37
|
/** Start cleanup only after the first guard is used. */
|
|
@@ -353,75 +62,17 @@ var ActionGuard = class {
|
|
|
353
62
|
* @internal
|
|
354
63
|
*/
|
|
355
64
|
performCleanup() {
|
|
356
|
-
|
|
357
|
-
if (guardCount === 0) {
|
|
65
|
+
if (this.guards.size === 0) {
|
|
358
66
|
this.stopAutoCleanup();
|
|
359
67
|
return;
|
|
360
68
|
}
|
|
361
69
|
const now = Date.now();
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
const isIdle = now - state.lastExecuted > this.maxIdleTime;
|
|
70
|
+
for (const [key, state] of this.guards) {
|
|
71
|
+
const isIdle = now - Math.max(state.lastThrottleExecutedAt, state.lastDebounceSettledAt) > this.maxIdleTime;
|
|
365
72
|
const hasActiveTimers = state.debounceTimer || state.throttleTimer;
|
|
366
|
-
if (isIdle && !hasActiveTimers)
|
|
367
|
-
});
|
|
368
|
-
else {
|
|
369
|
-
const entriesToCheck = Math.min(this.accessOrder.length, Math.ceil(guardCount / 4));
|
|
370
|
-
for (let i = 0; i < entriesToCheck; i++) {
|
|
371
|
-
const key = this.accessOrder[i];
|
|
372
|
-
if (!key) continue;
|
|
373
|
-
const state = this.guards.get(key);
|
|
374
|
-
if (!state) {
|
|
375
|
-
keysToDelete.push(key);
|
|
376
|
-
continue;
|
|
377
|
-
}
|
|
378
|
-
const isIdle = now - state.lastExecuted > this.maxIdleTime;
|
|
379
|
-
const hasActiveTimers = state.debounceTimer || state.throttleTimer;
|
|
380
|
-
if (isIdle && !hasActiveTimers) keysToDelete.push(key);
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
if (keysToDelete.length > 0) {
|
|
384
|
-
keysToDelete.forEach((key) => {
|
|
385
|
-
this.guards.delete(key);
|
|
386
|
-
const accessIndex = this.accessOrder.indexOf(key);
|
|
387
|
-
if (accessIndex !== -1) this.accessOrder.splice(accessIndex, 1);
|
|
388
|
-
});
|
|
389
|
-
if (typeof process !== "undefined" && process.env?.DEBUG_CONTEXT_ACTION) console.debug(`[ActionGuard] Cleaned up ${keysToDelete.length} idle guards`);
|
|
390
|
-
if (this.guards.size === 0) this.stopAutoCleanup();
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
/**
|
|
394
|
-
* 🔧 Update access order for LRU tracking
|
|
395
|
-
*
|
|
396
|
-
* @internal
|
|
397
|
-
*/
|
|
398
|
-
updateAccessOrder(key) {
|
|
399
|
-
const existingIndex = this.accessOrder.indexOf(key);
|
|
400
|
-
if (existingIndex !== -1) this.accessOrder.splice(existingIndex, 1);
|
|
401
|
-
this.accessOrder.push(key);
|
|
402
|
-
}
|
|
403
|
-
/**
|
|
404
|
-
* 🔧 Evict oldest guards if max limit exceeded
|
|
405
|
-
*
|
|
406
|
-
* @internal
|
|
407
|
-
*/
|
|
408
|
-
evictIfNeeded() {
|
|
409
|
-
if (this.guards.size >= this.maxGuards) {
|
|
410
|
-
const evictCount = Math.ceil(this.maxGuards * .1);
|
|
411
|
-
this.accessOrder.slice(0, evictCount).forEach((key) => {
|
|
412
|
-
const state = this.guards.get(key);
|
|
413
|
-
if (state) {
|
|
414
|
-
if (state.debounceTimer) {
|
|
415
|
-
clearTimeout(state.debounceTimer);
|
|
416
|
-
if (state.debounceResolve) state.debounceResolve(false);
|
|
417
|
-
}
|
|
418
|
-
if (state.throttleTimer) clearTimeout(state.throttleTimer);
|
|
419
|
-
}
|
|
420
|
-
this.guards.delete(key);
|
|
421
|
-
});
|
|
422
|
-
this.accessOrder = this.accessOrder.slice(evictCount);
|
|
423
|
-
if (typeof process !== "undefined" && process.env?.DEBUG_CONTEXT_ACTION) console.debug(`[ActionGuard] Evicted ${evictCount} oldest guards due to limit`);
|
|
73
|
+
if (isIdle && !hasActiveTimers) this.guards.delete(key);
|
|
424
74
|
}
|
|
75
|
+
if (this.guards.size === 0) this.stopAutoCleanup();
|
|
425
76
|
}
|
|
426
77
|
/**
|
|
427
78
|
* Apply debouncing to an action
|
|
@@ -445,24 +96,25 @@ var ActionGuard = class {
|
|
|
445
96
|
*
|
|
446
97
|
* @internal
|
|
447
98
|
*/
|
|
448
|
-
async debounce(actionKey, debounceMs) {
|
|
99
|
+
async debounce(actionKey, debounceMs, signal) {
|
|
449
100
|
this.ensureAutoCleanup();
|
|
450
|
-
|
|
101
|
+
if (signal?.aborted) return false;
|
|
451
102
|
/** Get or create guard state for this action */
|
|
452
103
|
let state = this.guards.get(actionKey);
|
|
453
104
|
if (!state) {
|
|
454
105
|
/** Initialize new guard state with default values */
|
|
455
106
|
state = {
|
|
456
|
-
|
|
107
|
+
lastThrottleExecutedAt: 0,
|
|
108
|
+
lastDebounceSettledAt: 0,
|
|
457
109
|
isThrottled: false,
|
|
458
110
|
debounceTimer: void 0,
|
|
459
111
|
throttleTimer: void 0,
|
|
460
|
-
|
|
461
|
-
|
|
112
|
+
debounceResolve: void 0,
|
|
113
|
+
debounceAbortCleanup: void 0,
|
|
114
|
+
debounceRequestId: 0
|
|
462
115
|
};
|
|
463
116
|
this.guards.set(actionKey, state);
|
|
464
117
|
}
|
|
465
|
-
this.updateAccessOrder(actionKey);
|
|
466
118
|
/** Clear any existing debounce timer to restart the delay period */
|
|
467
119
|
if (state.debounceTimer) {
|
|
468
120
|
clearTimeout(state.debounceTimer);
|
|
@@ -470,18 +122,35 @@ var ActionGuard = class {
|
|
|
470
122
|
state.debounceResolve(false);
|
|
471
123
|
state.debounceResolve = void 0;
|
|
472
124
|
}
|
|
125
|
+
state.debounceAbortCleanup?.();
|
|
126
|
+
state.debounceAbortCleanup = void 0;
|
|
473
127
|
}
|
|
474
|
-
|
|
128
|
+
const requestId = ++state.debounceRequestId;
|
|
129
|
+
/** Create a new abort-aware debounce promise. */
|
|
475
130
|
return new Promise((resolve) => {
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
131
|
+
let settled = false;
|
|
132
|
+
let abortCleanup;
|
|
133
|
+
const finish = (allowed) => {
|
|
134
|
+
if (settled) return;
|
|
135
|
+
settled = true;
|
|
136
|
+
if (state.debounceRequestId === requestId) {
|
|
137
|
+
if (state.debounceTimer) clearTimeout(state.debounceTimer);
|
|
138
|
+
state.debounceTimer = void 0;
|
|
139
|
+
state.debounceResolve = void 0;
|
|
140
|
+
state.debounceAbortCleanup = void 0;
|
|
141
|
+
if (allowed) state.lastDebounceSettledAt = Date.now();
|
|
142
|
+
}
|
|
143
|
+
abortCleanup?.();
|
|
144
|
+
resolve(allowed);
|
|
145
|
+
};
|
|
146
|
+
state.debounceResolve = finish;
|
|
147
|
+
state.debounceTimer = setTimeout(() => finish(true), debounceMs);
|
|
148
|
+
if (signal) {
|
|
149
|
+
const abort = () => finish(false);
|
|
150
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
151
|
+
abortCleanup = () => signal.removeEventListener("abort", abort);
|
|
152
|
+
state.debounceAbortCleanup = abortCleanup;
|
|
153
|
+
}
|
|
485
154
|
});
|
|
486
155
|
}
|
|
487
156
|
/**
|
|
@@ -506,31 +175,32 @@ var ActionGuard = class {
|
|
|
506
175
|
*
|
|
507
176
|
* @internal
|
|
508
177
|
*/
|
|
509
|
-
throttle(actionKey, throttleMs) {
|
|
178
|
+
throttle(actionKey, throttleMs, signal) {
|
|
510
179
|
this.ensureAutoCleanup();
|
|
511
|
-
|
|
180
|
+
if (signal?.aborted) return false;
|
|
512
181
|
/** Get or create guard state for this action */
|
|
513
182
|
let state = this.guards.get(actionKey);
|
|
514
183
|
if (!state) {
|
|
515
184
|
/** Initialize new guard state with default values */
|
|
516
185
|
state = {
|
|
517
|
-
|
|
186
|
+
lastThrottleExecutedAt: 0,
|
|
187
|
+
lastDebounceSettledAt: 0,
|
|
518
188
|
isThrottled: false,
|
|
519
189
|
debounceTimer: void 0,
|
|
520
190
|
throttleTimer: void 0,
|
|
521
|
-
|
|
522
|
-
|
|
191
|
+
debounceResolve: void 0,
|
|
192
|
+
debounceAbortCleanup: void 0,
|
|
193
|
+
debounceRequestId: 0
|
|
523
194
|
};
|
|
524
195
|
this.guards.set(actionKey, state);
|
|
525
196
|
}
|
|
526
|
-
this.updateAccessOrder(actionKey);
|
|
527
197
|
const now = Date.now();
|
|
528
|
-
const timeSinceLastExecution = now - state.
|
|
198
|
+
const timeSinceLastExecution = now - state.lastThrottleExecutedAt;
|
|
529
199
|
/** Check if enough time has passed since last execution */
|
|
530
200
|
/** If throttle period has elapsed, allow immediate execution */
|
|
531
201
|
if (timeSinceLastExecution >= throttleMs) {
|
|
532
202
|
/** Update execution timestamp and clear throttled state */
|
|
533
|
-
state.
|
|
203
|
+
state.lastThrottleExecutedAt = now;
|
|
534
204
|
state.isThrottled = false;
|
|
535
205
|
return true;
|
|
536
206
|
}
|
|
@@ -570,13 +240,13 @@ var ActionGuard = class {
|
|
|
570
240
|
}
|
|
571
241
|
state.debounceTimer = void 0;
|
|
572
242
|
}
|
|
243
|
+
state.debounceAbortCleanup?.();
|
|
244
|
+
state.debounceAbortCleanup = void 0;
|
|
573
245
|
if (state.throttleTimer) {
|
|
574
246
|
clearTimeout(state.throttleTimer);
|
|
575
247
|
state.throttleTimer = void 0;
|
|
576
248
|
}
|
|
577
249
|
this.guards.delete(actionKey);
|
|
578
|
-
const accessIndex = this.accessOrder.indexOf(actionKey);
|
|
579
|
-
if (accessIndex !== -1) this.accessOrder.splice(accessIndex, 1);
|
|
580
250
|
if (this.guards.size === 0) this.stopAutoCleanup();
|
|
581
251
|
}
|
|
582
252
|
}
|
|
@@ -597,12 +267,12 @@ var ActionGuard = class {
|
|
|
597
267
|
clearTimeout(state.debounceTimer);
|
|
598
268
|
if (state.debounceResolve) state.debounceResolve(false);
|
|
599
269
|
}
|
|
270
|
+
state.debounceAbortCleanup?.();
|
|
600
271
|
/** Clear any active throttle timers */
|
|
601
272
|
if (state.throttleTimer) clearTimeout(state.throttleTimer);
|
|
602
273
|
});
|
|
603
274
|
/** Remove all guard states from memory */
|
|
604
275
|
this.guards.clear();
|
|
605
|
-
this.accessOrder = [];
|
|
606
276
|
this.stopAutoCleanup();
|
|
607
277
|
}
|
|
608
278
|
/**
|
|
@@ -861,11 +531,31 @@ var OperationQueue = class {
|
|
|
861
531
|
|
|
862
532
|
//#endregion
|
|
863
533
|
//#region src/errors.ts
|
|
864
|
-
/**
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
534
|
+
/** Raised when dispatch result aggregation options cannot be processed. */
|
|
535
|
+
var ActionResultProcessingError = class ActionResultProcessingError extends Error {
|
|
536
|
+
constructor(message) {
|
|
537
|
+
super(message);
|
|
538
|
+
this.name = "ActionResultProcessingError";
|
|
539
|
+
Object.setPrototypeOf(this, ActionResultProcessingError.prototype);
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
/** Signals work from a completed race attempt to stop before the next retry. */
|
|
543
|
+
var ActionAttemptSupersededError = class ActionAttemptSupersededError extends Error {
|
|
544
|
+
constructor(attempt) {
|
|
545
|
+
super(`Action attempt ${attempt} was superseded by a retry.`);
|
|
546
|
+
this.attempt = attempt;
|
|
547
|
+
this.name = "ActionAttemptSupersededError";
|
|
548
|
+
Object.setPrototypeOf(this, ActionAttemptSupersededError.prototype);
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
function isActionResultProcessingError(error) {
|
|
552
|
+
return error instanceof ActionResultProcessingError;
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Action payload 검증 실패 에러
|
|
556
|
+
*
|
|
557
|
+
* dispatch 시 Zod 스키마 검증이 실패하면 발생합니다.
|
|
558
|
+
* (validationMode가 'strict'일 때만 throw)
|
|
869
559
|
*
|
|
870
560
|
* @example
|
|
871
561
|
* ```typescript
|
|
@@ -981,8 +671,443 @@ function isActionRegisterDestroyedError(error) {
|
|
|
981
671
|
return error instanceof ActionRegisterDestroyedError;
|
|
982
672
|
}
|
|
983
673
|
|
|
674
|
+
//#endregion
|
|
675
|
+
//#region src/execution-modes.ts
|
|
676
|
+
function isPromiseLike(value) {
|
|
677
|
+
return (typeof value === "object" || typeof value === "function") && value !== null && typeof value.then === "function";
|
|
678
|
+
}
|
|
679
|
+
function beginOutcome(registration) {
|
|
680
|
+
return {
|
|
681
|
+
id: registration.id,
|
|
682
|
+
status: "running",
|
|
683
|
+
executed: true,
|
|
684
|
+
duration: void 0,
|
|
685
|
+
result: void 0,
|
|
686
|
+
error: void 0,
|
|
687
|
+
metadata: registration.config.metadata ? { ...registration.config.metadata } : void 0
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
function createSkippedOutcome(registration) {
|
|
691
|
+
return {
|
|
692
|
+
id: registration.id,
|
|
693
|
+
status: "skipped",
|
|
694
|
+
executed: false,
|
|
695
|
+
duration: 0,
|
|
696
|
+
result: void 0,
|
|
697
|
+
error: void 0,
|
|
698
|
+
metadata: registration.config.metadata ? { ...registration.config.metadata } : void 0
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
function finishOutcome(outcome, startedAt, status, result, error) {
|
|
702
|
+
outcome.status = status;
|
|
703
|
+
outcome.duration = Date.now() - startedAt;
|
|
704
|
+
outcome.result = result;
|
|
705
|
+
outcome.error = error;
|
|
706
|
+
}
|
|
707
|
+
function appendLocalResults(context, state, returnedResult, registration, target = context.results) {
|
|
708
|
+
if (registration.role === "guard") return;
|
|
709
|
+
if (state.results.length > 0) target.push(...state.results);
|
|
710
|
+
if (returnedResult !== void 0 && !state.terminated) target.push(returnedResult);
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Create standardized error handling for handlers
|
|
714
|
+
*
|
|
715
|
+
* @param error - The error that occurred
|
|
716
|
+
* @param registration - The handler registration that failed
|
|
717
|
+
* @returns Standardized HandlerError object
|
|
718
|
+
*
|
|
719
|
+
* @internal
|
|
720
|
+
*/
|
|
721
|
+
function handleExecutionError(error, registration) {
|
|
722
|
+
const errorObj = error instanceof Error ? error : new Error(String(error));
|
|
723
|
+
return {
|
|
724
|
+
handlerId: registration.id,
|
|
725
|
+
error: errorObj,
|
|
726
|
+
timestamp: Date.now(),
|
|
727
|
+
severity: registration.config.errorPolicy === "fatal" ? "blocking" : "non-blocking"
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Execute handlers in sequential mode (one after another)
|
|
732
|
+
*
|
|
733
|
+
* Executes action handlers one at a time in priority order (highest first).
|
|
734
|
+
* Supports both blocking and non-blocking handlers, with proper abort and
|
|
735
|
+
* termination handling. Handlers can modify payload for subsequent handlers
|
|
736
|
+
* and jump to different priority levels.
|
|
737
|
+
*
|
|
738
|
+
* @template T - The payload type for the action
|
|
739
|
+
* @template R - The result type for handlers
|
|
740
|
+
*
|
|
741
|
+
* @param context - Pipeline execution context containing handlers and state
|
|
742
|
+
* @param createController - Factory function for creating pipeline controllers
|
|
743
|
+
*
|
|
744
|
+
* @throws {Error} When a blocking handler fails
|
|
745
|
+
*
|
|
746
|
+
* @see https://mineclover.github.io/context-action/en/guide/patterns/action/dispatch-patterns
|
|
747
|
+
*
|
|
748
|
+
* @public
|
|
749
|
+
*/
|
|
750
|
+
async function executeSequential(context, createController) {
|
|
751
|
+
let i = 0;
|
|
752
|
+
const nonBlockingPromises = [];
|
|
753
|
+
const errors = [];
|
|
754
|
+
while (i < context.handlers.length) {
|
|
755
|
+
if (context.aborted || context.terminated) break;
|
|
756
|
+
const registration = context.handlers[i];
|
|
757
|
+
if (!registration) continue;
|
|
758
|
+
context.currentIndex = i;
|
|
759
|
+
const controller = createController(registration, i);
|
|
760
|
+
if (registration.config.condition) {
|
|
761
|
+
if (!registration.config.condition(context.payload)) {
|
|
762
|
+
(context.handlerOutcomes ?? (context.handlerOutcomes = [])).push(createSkippedOutcome(registration));
|
|
763
|
+
i++;
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
if (context.claimOnce && !context.claimOnce(registration)) {
|
|
768
|
+
(context.handlerOutcomes ?? (context.handlerOutcomes = [])).push(createSkippedOutcome(registration));
|
|
769
|
+
i++;
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
const outcome = beginOutcome(registration);
|
|
773
|
+
const startedAt = Date.now();
|
|
774
|
+
(context.handlerOutcomes ?? (context.handlerOutcomes = [])).push(outcome);
|
|
775
|
+
try {
|
|
776
|
+
if (context.aborted) {
|
|
777
|
+
outcome.status = "cancelled";
|
|
778
|
+
outcome.executed = false;
|
|
779
|
+
outcome.duration = 0;
|
|
780
|
+
break;
|
|
781
|
+
}
|
|
782
|
+
(context.executedHandlers ?? (context.executedHandlers = [])).push(registration);
|
|
783
|
+
const result = registration.handler(context.payload, controller);
|
|
784
|
+
const asyncResult = isPromiseLike(result) ? Promise.resolve(result) : void 0;
|
|
785
|
+
const trackedResult = asyncResult && context.trackHandlerPromise ? context.trackHandlerPromise(asyncResult) : asyncResult;
|
|
786
|
+
if (registration.config.scheduling === "await-before-next") {
|
|
787
|
+
const handlerResult = trackedResult ? await trackedResult : result;
|
|
788
|
+
finishOutcome(outcome, startedAt, "succeeded", registration.role === "guard" ? void 0 : handlerResult);
|
|
789
|
+
if (registration.role !== "guard" && handlerResult !== void 0 && !context.terminated) context.results.push(handlerResult);
|
|
790
|
+
} else if (trackedResult) {
|
|
791
|
+
const promiseWithErrorHandling = trackedResult.then((asyncResult) => {
|
|
792
|
+
finishOutcome(outcome, startedAt, "succeeded", registration.role === "guard" ? void 0 : asyncResult);
|
|
793
|
+
if (registration.role !== "guard" && asyncResult !== void 0 && !context.terminated) context.results.push(asyncResult);
|
|
794
|
+
return asyncResult;
|
|
795
|
+
}).catch((error) => {
|
|
796
|
+
const handlerError = handleExecutionError(error, registration);
|
|
797
|
+
errors.push(handlerError);
|
|
798
|
+
finishOutcome(outcome, startedAt, "failed", void 0, handlerError.error);
|
|
799
|
+
});
|
|
800
|
+
nonBlockingPromises.push(promiseWithErrorHandling);
|
|
801
|
+
} else if (registration.role !== "guard" && result !== void 0 && !context.terminated) {
|
|
802
|
+
finishOutcome(outcome, startedAt, "succeeded", result);
|
|
803
|
+
context.results.push(result);
|
|
804
|
+
} else finishOutcome(outcome, startedAt, "succeeded", registration.role === "guard" ? void 0 : result);
|
|
805
|
+
outcome.terminationRequested = context.terminated;
|
|
806
|
+
if (context.terminated) outcome.terminationResult = context.terminationResult;
|
|
807
|
+
/** Check if pipeline was terminated by controller.return() */
|
|
808
|
+
if (context.terminated) break;
|
|
809
|
+
/** Handle jump to priority AFTER handler execution */
|
|
810
|
+
if (context.jumpToPriority !== void 0) {
|
|
811
|
+
context.jumpCount = (context.jumpCount || 0) + 1;
|
|
812
|
+
if (context.jumpCount > (context.maxJumps || 10)) {
|
|
813
|
+
context.aborted = true;
|
|
814
|
+
context.abortReason = `Maximum jump limit exceeded (${context.jumpCount} jumps)`;
|
|
815
|
+
context.jumpToPriority = void 0;
|
|
816
|
+
break;
|
|
817
|
+
}
|
|
818
|
+
const jumpIndex = context.handlers.findIndex((handler) => (handler.config.priority || 0) <= context.jumpToPriority);
|
|
819
|
+
if (jumpIndex !== -1 && jumpIndex !== i) {
|
|
820
|
+
i = jumpIndex;
|
|
821
|
+
context.jumpToPriority = void 0;
|
|
822
|
+
} else {
|
|
823
|
+
context.jumpToPriority = void 0;
|
|
824
|
+
i++;
|
|
825
|
+
}
|
|
826
|
+
} else i++;
|
|
827
|
+
} catch (error) {
|
|
828
|
+
const handlerError = handleExecutionError(error, registration);
|
|
829
|
+
finishOutcome(outcome, startedAt, "failed", void 0, handlerError.error);
|
|
830
|
+
errors.push(handlerError);
|
|
831
|
+
(context.collectedErrors ?? (context.collectedErrors = [])).push(handlerError);
|
|
832
|
+
if (registration.config.errorPolicy === "fatal") throw handlerError.error;
|
|
833
|
+
i++;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
if (nonBlockingPromises.length > 0) await Promise.allSettled(nonBlockingPromises);
|
|
837
|
+
if (errors.length > 0) context.collectedErrors = errors;
|
|
838
|
+
const fatalError = errors.find((error) => error.severity === "blocking");
|
|
839
|
+
if (fatalError) throw fatalError.error;
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Execute handlers in parallel mode (all at once)
|
|
843
|
+
*
|
|
844
|
+
* Executes all qualifying action handlers simultaneously using Promise.allSettled.
|
|
845
|
+
* Supports both blocking and non-blocking handlers. Blocking handlers can still
|
|
846
|
+
* fail the entire pipeline if they throw errors.
|
|
847
|
+
*
|
|
848
|
+
* @template T - The payload type for the action
|
|
849
|
+
* @template R - The result type for handlers
|
|
850
|
+
*
|
|
851
|
+
* @param context - Pipeline execution context containing handlers and state
|
|
852
|
+
* @param createController - Factory function for creating pipeline controllers
|
|
853
|
+
*
|
|
854
|
+
* @throws {Error} When any blocking handler fails
|
|
855
|
+
*
|
|
856
|
+
* @see https://mineclover.github.io/context-action/en/guide/patterns/action/dispatch-patterns#parallel-execution
|
|
857
|
+
*
|
|
858
|
+
* @public
|
|
859
|
+
*/
|
|
860
|
+
async function executeParallel(context, createController) {
|
|
861
|
+
/**
|
|
862
|
+
* Conditions are dispatch preconditions in concurrent modes. Evaluate them
|
|
863
|
+
* before any handler starts so a predicate error rejects the dispatch rather
|
|
864
|
+
* than being mistaken for a non-blocking handler failure.
|
|
865
|
+
*/
|
|
866
|
+
const runnableHandlers = [];
|
|
867
|
+
for (const registration of context.handlers) {
|
|
868
|
+
if (registration.config.condition && !registration.config.condition(context.payload)) {
|
|
869
|
+
(context.handlerOutcomes ?? (context.handlerOutcomes = [])).push(createSkippedOutcome(registration));
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
if (context.claimOnce && !context.claimOnce(registration)) {
|
|
873
|
+
(context.handlerOutcomes ?? (context.handlerOutcomes = [])).push(createSkippedOutcome(registration));
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
runnableHandlers.push(registration);
|
|
877
|
+
}
|
|
878
|
+
const terminationSlots = runnableHandlers.map(() => ({
|
|
879
|
+
requested: false,
|
|
880
|
+
result: void 0
|
|
881
|
+
}));
|
|
882
|
+
const resultSlots = runnableHandlers.map(() => []);
|
|
883
|
+
/** Create promises for all handlers */
|
|
884
|
+
const handlerPromises = runnableHandlers.map(async (registration, _index) => {
|
|
885
|
+
const state = {
|
|
886
|
+
payload: context.payload,
|
|
887
|
+
aborted: false,
|
|
888
|
+
abortReason: void 0,
|
|
889
|
+
jumpToPriority: void 0,
|
|
890
|
+
terminated: false,
|
|
891
|
+
terminationResult: void 0,
|
|
892
|
+
results: []
|
|
893
|
+
};
|
|
894
|
+
const controller = createController(registration, _index, state);
|
|
895
|
+
const outcome = beginOutcome(registration);
|
|
896
|
+
const startedAt = Date.now();
|
|
897
|
+
(context.handlerOutcomes ?? (context.handlerOutcomes = [])).push(outcome);
|
|
898
|
+
try {
|
|
899
|
+
(context.executedHandlers ?? (context.executedHandlers = [])).push(registration);
|
|
900
|
+
const result = registration.handler(state.payload, controller);
|
|
901
|
+
const handlerResult = isPromiseLike(result) ? await Promise.resolve(result) : result;
|
|
902
|
+
finishOutcome(outcome, startedAt, "succeeded", registration.role === "guard" ? void 0 : handlerResult);
|
|
903
|
+
outcome.terminationRequested = state.terminated;
|
|
904
|
+
if (state.terminated && registration.role !== "guard") {
|
|
905
|
+
outcome.terminationResult = state.terminationResult;
|
|
906
|
+
terminationSlots[_index] = {
|
|
907
|
+
requested: true,
|
|
908
|
+
result: state.terminationResult
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
appendLocalResults(context, state, handlerResult, registration, resultSlots[_index]);
|
|
912
|
+
return {
|
|
913
|
+
success: true,
|
|
914
|
+
handlerId: registration.id,
|
|
915
|
+
result: handlerResult,
|
|
916
|
+
terminated: state.terminated,
|
|
917
|
+
state,
|
|
918
|
+
outcome
|
|
919
|
+
};
|
|
920
|
+
} catch (error) {
|
|
921
|
+
const handlerError = handleExecutionError(error, registration);
|
|
922
|
+
finishOutcome(outcome, startedAt, "failed", void 0, handlerError.error);
|
|
923
|
+
(context.collectedErrors ?? (context.collectedErrors = [])).push(handlerError);
|
|
924
|
+
if (handlerError.severity === "blocking") throw handlerError.error;
|
|
925
|
+
return {
|
|
926
|
+
success: false,
|
|
927
|
+
handlerId: registration.id,
|
|
928
|
+
error: handlerError.error,
|
|
929
|
+
state,
|
|
930
|
+
outcome,
|
|
931
|
+
registration
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
});
|
|
935
|
+
const trackedHandlerPromises = context.trackHandlerPromise ? handlerPromises.map((promise) => context.trackHandlerPromise(promise)) : handlerPromises;
|
|
936
|
+
/** Wait for all handlers to complete */
|
|
937
|
+
const results = await Promise.allSettled(trackedHandlerPromises);
|
|
938
|
+
context.results.push(...resultSlots.flat());
|
|
939
|
+
/** Check for any rejected blocking handlers */
|
|
940
|
+
const failures = results.filter((result, index) => {
|
|
941
|
+
if (result.status === "rejected") return runnableHandlers[index]?.config.errorPolicy === "fatal";
|
|
942
|
+
return false;
|
|
943
|
+
});
|
|
944
|
+
if (failures.length > 0) throw failures[0].reason;
|
|
945
|
+
/** Check if any handler terminated the pipeline */
|
|
946
|
+
const firstTerminated = terminationSlots.find((slot) => slot.requested);
|
|
947
|
+
if (firstTerminated) {
|
|
948
|
+
context.terminated = true;
|
|
949
|
+
context.terminationResult = firstTerminated.result;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Execute handlers in race mode (first to complete wins)
|
|
954
|
+
*
|
|
955
|
+
* Executes all qualifying handlers simultaneously using Promise.race, where
|
|
956
|
+
* the first handler to complete determines the pipeline result. Other handlers
|
|
957
|
+
* continue in the background and remain tracked for lifecycle cleanup; handlers
|
|
958
|
+
* must observe the controller signal for cooperative external cancellation.
|
|
959
|
+
* Useful for scenarios where you want the fastest response from multiple
|
|
960
|
+
* equivalent handlers.
|
|
961
|
+
*
|
|
962
|
+
* @template T - The payload type for the action
|
|
963
|
+
* @template R - The result type for handlers
|
|
964
|
+
*
|
|
965
|
+
* @param context - Pipeline execution context containing handlers and state
|
|
966
|
+
* @param createController - Factory function for creating pipeline controllers
|
|
967
|
+
*
|
|
968
|
+
* @throws {Error} When the winning handler fails and is blocking
|
|
969
|
+
*
|
|
970
|
+
* @see https://mineclover.github.io/context-action/en/guide/patterns/action/dispatch-patterns#race-execution
|
|
971
|
+
*
|
|
972
|
+
* @public
|
|
973
|
+
*/
|
|
974
|
+
async function executeRace(context, createController) {
|
|
975
|
+
/** See executeParallel: condition errors are dispatch errors in concurrent modes. */
|
|
976
|
+
const runnableHandlers = [];
|
|
977
|
+
for (const registration of context.handlers) {
|
|
978
|
+
if (registration.config.condition && !registration.config.condition(context.payload)) {
|
|
979
|
+
(context.handlerOutcomes ?? (context.handlerOutcomes = [])).push(createSkippedOutcome(registration));
|
|
980
|
+
continue;
|
|
981
|
+
}
|
|
982
|
+
if (context.claimOnce && !context.claimOnce(registration)) {
|
|
983
|
+
(context.handlerOutcomes ?? (context.handlerOutcomes = [])).push(createSkippedOutcome(registration));
|
|
984
|
+
continue;
|
|
985
|
+
}
|
|
986
|
+
runnableHandlers.push(registration);
|
|
987
|
+
}
|
|
988
|
+
if (runnableHandlers.length === 0) return;
|
|
989
|
+
/** Create promises for all handlers */
|
|
990
|
+
const handlerPromises = runnableHandlers.map(async (registration, _index) => {
|
|
991
|
+
const state = {
|
|
992
|
+
payload: context.payload,
|
|
993
|
+
aborted: false,
|
|
994
|
+
abortReason: void 0,
|
|
995
|
+
jumpToPriority: void 0,
|
|
996
|
+
terminated: false,
|
|
997
|
+
terminationResult: void 0,
|
|
998
|
+
results: []
|
|
999
|
+
};
|
|
1000
|
+
const controller = createController(registration, _index, state);
|
|
1001
|
+
const outcome = beginOutcome(registration);
|
|
1002
|
+
const startedAt = Date.now();
|
|
1003
|
+
(context.handlerOutcomes ?? (context.handlerOutcomes = [])).push(outcome);
|
|
1004
|
+
try {
|
|
1005
|
+
(context.executedHandlers ?? (context.executedHandlers = [])).push(registration);
|
|
1006
|
+
const result = registration.handler(state.payload, controller);
|
|
1007
|
+
const handlerResult = isPromiseLike(result) ? await Promise.resolve(result) : result;
|
|
1008
|
+
finishOutcome(outcome, startedAt, "succeeded", registration.role === "guard" ? void 0 : handlerResult);
|
|
1009
|
+
outcome.terminationRequested = state.terminated;
|
|
1010
|
+
if (state.terminated) outcome.terminationResult = state.terminationResult;
|
|
1011
|
+
return {
|
|
1012
|
+
success: true,
|
|
1013
|
+
handlerId: registration.id,
|
|
1014
|
+
registration,
|
|
1015
|
+
result: handlerResult,
|
|
1016
|
+
terminated: state.terminated,
|
|
1017
|
+
state,
|
|
1018
|
+
outcome
|
|
1019
|
+
};
|
|
1020
|
+
} catch (error) {
|
|
1021
|
+
const handlerError = handleExecutionError(error, registration);
|
|
1022
|
+
finishOutcome(outcome, startedAt, "failed", void 0, handlerError.error);
|
|
1023
|
+
return {
|
|
1024
|
+
success: false,
|
|
1025
|
+
handlerId: registration.id,
|
|
1026
|
+
error: handlerError.error,
|
|
1027
|
+
registration,
|
|
1028
|
+
state,
|
|
1029
|
+
outcome
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
});
|
|
1033
|
+
const trackedHandlerPromises = context.trackHandlerPromise ? handlerPromises.map((promise) => context.trackHandlerPromise(promise)) : handlerPromises;
|
|
1034
|
+
const winnerCandidates = runnableHandlers.some((handler) => handler.role !== "guard") ? trackedHandlerPromises.filter((_, index) => runnableHandlers[index]?.role !== "guard") : trackedHandlerPromises;
|
|
1035
|
+
/** Race all handlers while retaining every loser for lifecycle draining. */
|
|
1036
|
+
const winner = await Promise.race(winnerCandidates);
|
|
1037
|
+
context.raceWinnerId = winner.handlerId;
|
|
1038
|
+
context.raceLoserOutcomes = (context.handlerOutcomes ?? []).filter((outcome) => outcome.id !== winner.handlerId).map((outcome) => ({
|
|
1039
|
+
...outcome,
|
|
1040
|
+
metadata: outcome.metadata ? { ...outcome.metadata } : void 0
|
|
1041
|
+
}));
|
|
1042
|
+
/** If the winner failed and was blocking, throw the error */
|
|
1043
|
+
if (!winner.success && winner.registration?.config.errorPolicy === "fatal") {
|
|
1044
|
+
(context.collectedErrors ?? (context.collectedErrors = [])).push(handleExecutionError(winner.error, winner.registration));
|
|
1045
|
+
throw winner.error;
|
|
1046
|
+
}
|
|
1047
|
+
if (!winner.success) (context.collectedErrors ?? (context.collectedErrors = [])).push(handleExecutionError(winner.error, winner.registration));
|
|
1048
|
+
/** Only the winner contributes results to the race snapshot. */
|
|
1049
|
+
if (winner.success) {
|
|
1050
|
+
appendLocalResults(context, winner.state, winner.result, winner.registration);
|
|
1051
|
+
if (winner.state.aborted) {
|
|
1052
|
+
context.aborted = true;
|
|
1053
|
+
context.abortReason = winner.state.abortReason;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
/** Check if the winning handler terminated the pipeline */
|
|
1057
|
+
if (winner.success && winner.terminated) {
|
|
1058
|
+
context.terminated = true;
|
|
1059
|
+
context.terminationResult = winner.state.terminationResult;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
//#endregion
|
|
1064
|
+
//#region src/types.ts
|
|
1065
|
+
/**
|
|
1066
|
+
* Resolve the supported `blocking` shorthand and all registration
|
|
1067
|
+
* defaults in one place. Adapters should pass their original config to the
|
|
1068
|
+
* registry and use this helper only when they need to expose resolved values.
|
|
1069
|
+
*/
|
|
1070
|
+
function resolveHandlerConfig(config, handlerId) {
|
|
1071
|
+
return {
|
|
1072
|
+
priority: config?.priority ?? 0,
|
|
1073
|
+
id: handlerId,
|
|
1074
|
+
blocking: config?.errorPolicy === "fatal" || config?.blocking === true,
|
|
1075
|
+
scheduling: config?.scheduling ?? (config?.blocking === false ? "start-and-continue" : "await-before-next"),
|
|
1076
|
+
errorPolicy: config?.errorPolicy ?? (config?.blocking === true ? "fatal" : "collect"),
|
|
1077
|
+
once: config?.once ?? false,
|
|
1078
|
+
debounce: config?.debounce,
|
|
1079
|
+
throttle: config?.throttle,
|
|
1080
|
+
replaceExisting: config?.replaceExisting ?? true,
|
|
1081
|
+
cleanup: config?.cleanup,
|
|
1082
|
+
condition: config?.condition,
|
|
1083
|
+
metadata: config?.metadata
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
|
|
984
1087
|
//#endregion
|
|
985
1088
|
//#region src/ActionRegister.ts
|
|
1089
|
+
const RESERVED_PROXY_KEYS = /* @__PURE__ */ new Set([
|
|
1090
|
+
"then",
|
|
1091
|
+
"catch",
|
|
1092
|
+
"finally",
|
|
1093
|
+
"toJSON",
|
|
1094
|
+
"constructor",
|
|
1095
|
+
"__proto__",
|
|
1096
|
+
"prototype"
|
|
1097
|
+
]);
|
|
1098
|
+
const ATTEMPT_SIGNAL_CLEANUP = Symbol("attemptSignalCleanup");
|
|
1099
|
+
function snapshotHandlerOutcome(outcome) {
|
|
1100
|
+
return {
|
|
1101
|
+
...outcome,
|
|
1102
|
+
metadata: outcome.metadata ? { ...outcome.metadata } : void 0
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
function normalizePositiveLimit(value, fallback, label) {
|
|
1106
|
+
const limit = value ?? fallback;
|
|
1107
|
+
if (limit === Infinity) return limit;
|
|
1108
|
+
if (!Number.isSafeInteger(limit) || limit <= 0) throw new RangeError(`${label} must be a positive safe integer or Infinity.`);
|
|
1109
|
+
return limit;
|
|
1110
|
+
}
|
|
986
1111
|
/**
|
|
987
1112
|
* Action Register for managing action handlers with priority-based execution
|
|
988
1113
|
*
|
|
@@ -1001,23 +1126,27 @@ function isActionRegisterDestroyedError(error) {
|
|
|
1001
1126
|
var ActionRegister = class {
|
|
1002
1127
|
constructor(config = {}) {
|
|
1003
1128
|
this.pipelines = /* @__PURE__ */ new Map();
|
|
1129
|
+
this.observerHandlers = /* @__PURE__ */ new Map();
|
|
1130
|
+
this.claimedOnceHandlers = /* @__PURE__ */ new WeakSet();
|
|
1004
1131
|
this.executionMode = "sequential";
|
|
1005
1132
|
this.actionExecutionModes = /* @__PURE__ */ new Map();
|
|
1006
1133
|
this.unregisterFunctions = /* @__PURE__ */ new Map();
|
|
1007
1134
|
this.lastRegisteredTimestamps = /* @__PURE__ */ new Map();
|
|
1008
1135
|
this.handlerIdCounter = 0;
|
|
1009
|
-
this.controllerPool = [];
|
|
1010
1136
|
this.lifecycleState = "active";
|
|
1011
1137
|
this.lifecycleController = new AbortController();
|
|
1012
1138
|
this.activeDispatches = /* @__PURE__ */ new Set();
|
|
1013
1139
|
this.activeHandlerPromises = /* @__PURE__ */ new Set();
|
|
1014
1140
|
this.dispatchConstructionDepth = 0;
|
|
1141
|
+
this.actionDispatchers = /* @__PURE__ */ new Map();
|
|
1142
|
+
this.actionResultDispatchers = /* @__PURE__ */ new Map();
|
|
1015
1143
|
this.name = config.name || "ActionRegister";
|
|
1016
1144
|
this.registryConfig = config.registry;
|
|
1017
|
-
this.maxHandlersPerAction = config.registry?.maxHandlersPerAction
|
|
1018
|
-
this.
|
|
1145
|
+
this.maxHandlersPerAction = normalizePositiveLimit(config.registry?.maxHandlersPerAction, Infinity, "maxHandlersPerAction");
|
|
1146
|
+
this.maxJumps = normalizePositiveLimit(config.registry?.maxJumps, 10, "maxJumps");
|
|
1147
|
+
this.isDebugMode = this.registryConfig?.debug === true;
|
|
1019
1148
|
this.actionGuard = new ActionGuard(this.registryConfig?.autoCleanup !== false);
|
|
1020
|
-
if (config.registry?.useConcurrencyQueue
|
|
1149
|
+
if (config.registry?.useConcurrencyQueue === true) this.dispatchQueue = new OperationQueue(`${this.name}-Dispatch`);
|
|
1021
1150
|
if (this.registryConfig?.defaultExecutionMode) this.executionMode = this.registryConfig.defaultExecutionMode;
|
|
1022
1151
|
this.log("ActionRegister initialized", {
|
|
1023
1152
|
defaultExecutionMode: this.executionMode,
|
|
@@ -1051,10 +1180,15 @@ var ActionRegister = class {
|
|
|
1051
1180
|
*/
|
|
1052
1181
|
get actions() {
|
|
1053
1182
|
if (!this._actionsProxy) this._actionsProxy = new Proxy({}, { get: (_target, prop) => {
|
|
1183
|
+
if (typeof prop !== "string") return void 0;
|
|
1184
|
+
if (RESERVED_PROXY_KEYS.has(prop)) return void 0;
|
|
1054
1185
|
const actionKey = prop;
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1186
|
+
let dispatcher = this.actionDispatchers.get(prop);
|
|
1187
|
+
if (!dispatcher) {
|
|
1188
|
+
dispatcher = (payload, options) => this.dispatch(actionKey, ...[payload, options]);
|
|
1189
|
+
this.actionDispatchers.set(prop, dispatcher);
|
|
1190
|
+
}
|
|
1191
|
+
return dispatcher;
|
|
1058
1192
|
} });
|
|
1059
1193
|
return this._actionsProxy;
|
|
1060
1194
|
}
|
|
@@ -1087,32 +1221,76 @@ var ActionRegister = class {
|
|
|
1087
1221
|
*/
|
|
1088
1222
|
get actionsWithResult() {
|
|
1089
1223
|
if (!this._actionsWithResultProxy) this._actionsWithResultProxy = new Proxy({}, { get: (_target, prop) => {
|
|
1224
|
+
if (typeof prop !== "string") return void 0;
|
|
1225
|
+
if (RESERVED_PROXY_KEYS.has(prop)) return void 0;
|
|
1090
1226
|
const actionKey = prop;
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1227
|
+
let dispatcher = this.actionResultDispatchers.get(prop);
|
|
1228
|
+
if (!dispatcher) {
|
|
1229
|
+
const dispatchAction = this.dispatchWithResult.bind(this);
|
|
1230
|
+
dispatcher = (payload, options) => dispatchAction(actionKey, ...[payload, options]);
|
|
1231
|
+
this.actionResultDispatchers.set(prop, dispatcher);
|
|
1232
|
+
}
|
|
1233
|
+
return dispatcher;
|
|
1094
1234
|
} });
|
|
1095
1235
|
return this._actionsWithResultProxy;
|
|
1096
1236
|
}
|
|
1237
|
+
register(action, handler, config = {}) {
|
|
1238
|
+
return this.registerWithRole(action, handler, config, "legacy");
|
|
1239
|
+
}
|
|
1240
|
+
registerEffect(action, handler, config) {
|
|
1241
|
+
if (config.effectKind === "guard") return this.registerGuard(action, handler, config);
|
|
1242
|
+
return this.registerObserver(action, (event) => handler(event.payload, {
|
|
1243
|
+
signal: event.signal,
|
|
1244
|
+
getPayload: () => event.payload
|
|
1245
|
+
}), config);
|
|
1246
|
+
}
|
|
1247
|
+
/** Register an authorization or validation guard that always runs before
|
|
1248
|
+
* concurrent result arbitration. */
|
|
1249
|
+
registerGuard(action, handler, config = {}) {
|
|
1250
|
+
return this.registerWithRole(action, handler, {
|
|
1251
|
+
...config,
|
|
1252
|
+
scheduling: "await-before-next",
|
|
1253
|
+
errorPolicy: "fatal"
|
|
1254
|
+
}, "guard");
|
|
1255
|
+
}
|
|
1256
|
+
/** Register a terminal observer. It runs after result aggregation and has
|
|
1257
|
+
* no controller, result, payload, or winner-selection capabilities. */
|
|
1258
|
+
registerObserver(action, handler, config = {}) {
|
|
1259
|
+
const handlerId = config.id ?? this.generateHandlerId(action);
|
|
1260
|
+
const existing = this.pipelines.get(action)?.find((item) => item.id === handlerId);
|
|
1261
|
+
if (existing && (existing.role ?? "legacy") !== "observer") throw new Error(`Action handler role conflict for "${String(action)}" and id "${handlerId}": cannot replace ${existing.role ?? "legacy"} with observer.`);
|
|
1262
|
+
if (existing && config.replaceExisting === false) return () => {};
|
|
1263
|
+
const unregister = this.registerWithRole(action, (() => void 0), {
|
|
1264
|
+
...config,
|
|
1265
|
+
id: handlerId
|
|
1266
|
+
}, "observer");
|
|
1267
|
+
const registration = this.pipelines.get(action)?.find((item) => item.id === handlerId);
|
|
1268
|
+
if (!registration) {
|
|
1269
|
+
unregister();
|
|
1270
|
+
throw new Error(`Observer registration "${handlerId}" was not retained.`);
|
|
1271
|
+
}
|
|
1272
|
+
this.observerHandlers.set(registration, {
|
|
1273
|
+
handler,
|
|
1274
|
+
when: config.when ?? "always"
|
|
1275
|
+
});
|
|
1276
|
+
return () => {
|
|
1277
|
+
this.observerHandlers.delete(registration);
|
|
1278
|
+
unregister();
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1097
1281
|
/**
|
|
1098
|
-
* Register
|
|
1099
|
-
*
|
|
1100
|
-
* @param action - The action type to register handler for
|
|
1101
|
-
* @param handler - The handler function to execute
|
|
1102
|
-
* @param config - Optional handler configuration including priority, tags, etc.
|
|
1103
|
-
*
|
|
1104
|
-
* @returns Unregister function to remove this handler
|
|
1105
|
-
*
|
|
1106
|
-
* @throws {Error} When maximum handlers limit is reached
|
|
1107
|
-
*
|
|
1108
|
-
* @see https://mineclover.github.io/context-action/en/guide/patterns/action/basic-usage
|
|
1109
|
-
*
|
|
1282
|
+
* Register a handler that contributes the result declared for an action.
|
|
1283
|
+
*
|
|
1110
1284
|
* @public
|
|
1111
1285
|
*/
|
|
1112
|
-
|
|
1286
|
+
registerResult(action, handler, config) {
|
|
1287
|
+
return this.registerWithRole(action, handler, config ?? {}, "result");
|
|
1288
|
+
}
|
|
1289
|
+
registerWithRole(action, handler, config, role) {
|
|
1290
|
+
this.assertStringActionKey(action);
|
|
1113
1291
|
this.assertAcceptingWork();
|
|
1114
1292
|
const handlerId = config.id || this.generateHandlerId(action);
|
|
1115
|
-
return this._performRegistrationSync(action, handler, config, handlerId);
|
|
1293
|
+
return this._performRegistrationSync(action, handler, config, handlerId, role);
|
|
1116
1294
|
}
|
|
1117
1295
|
/**
|
|
1118
1296
|
* 🆕 Unified logging method with cached debug mode check
|
|
@@ -1126,6 +1304,9 @@ var ActionRegister = class {
|
|
|
1126
1304
|
assertAcceptingWork() {
|
|
1127
1305
|
if (this.lifecycleState !== "active") throw new ActionRegisterDestroyedError(this.name, this.lifecycleState);
|
|
1128
1306
|
}
|
|
1307
|
+
assertStringActionKey(action) {
|
|
1308
|
+
if (typeof action !== "string") throw new TypeError("Action keys must be strings.");
|
|
1309
|
+
}
|
|
1129
1310
|
rejectedLifecyclePromise() {
|
|
1130
1311
|
const error = new ActionRegisterDestroyedError(this.name, this.lifecycleState === "active" ? "destroyed" : this.lifecycleState);
|
|
1131
1312
|
const rejected = Promise.reject(error);
|
|
@@ -1195,44 +1376,35 @@ var ActionRegister = class {
|
|
|
1195
1376
|
/**
|
|
1196
1377
|
* 🆕 Perform synchronous handler registration
|
|
1197
1378
|
*/
|
|
1198
|
-
_performRegistrationSync(action, handler, config, handlerId) {
|
|
1379
|
+
_performRegistrationSync(action, handler, config, handlerId, role = "legacy") {
|
|
1199
1380
|
const registration = {
|
|
1200
1381
|
handler,
|
|
1201
|
-
config:
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
blocking: config.blocking ?? false,
|
|
1205
|
-
once: config.once ?? false,
|
|
1206
|
-
debounce: config.debounce ?? void 0,
|
|
1207
|
-
throttle: config.throttle ?? void 0,
|
|
1208
|
-
replaceExisting: config.replaceExisting ?? true,
|
|
1209
|
-
cleanup: config.cleanup,
|
|
1210
|
-
condition: config.condition
|
|
1211
|
-
},
|
|
1212
|
-
id: handlerId
|
|
1382
|
+
config: resolveHandlerConfig(config, handlerId),
|
|
1383
|
+
id: handlerId,
|
|
1384
|
+
role
|
|
1213
1385
|
};
|
|
1214
1386
|
if (!this.pipelines.has(action)) this.pipelines.set(action, []);
|
|
1215
1387
|
const pipeline = this.pipelines.get(action);
|
|
1216
|
-
|
|
1217
|
-
console.warn(`Handler limit (${this.maxHandlersPerAction}) reached for action "${String(action)}". Registration ignored.`);
|
|
1218
|
-
return () => {};
|
|
1219
|
-
}
|
|
1388
|
+
const actionUnregisterFunctions = this.getUnregisterFunctions(action);
|
|
1220
1389
|
const existingIndex = pipeline.findIndex((reg) => reg.id === handlerId);
|
|
1390
|
+
if (existingIndex === -1 && pipeline.length >= this.maxHandlersPerAction) throw new RangeError(`Handler limit (${this.maxHandlersPerAction}) reached for action "${String(action)}".`);
|
|
1221
1391
|
if (existingIndex !== -1) {
|
|
1222
1392
|
const existing = pipeline[existingIndex];
|
|
1223
|
-
const existingUnregister =
|
|
1393
|
+
const existingUnregister = actionUnregisterFunctions.get(handlerId);
|
|
1394
|
+
if (existing && (existing.role ?? "legacy") !== role) throw new Error(`Action handler role conflict for "${String(action)}" and id "${handlerId}": cannot replace ${existing.role ?? "legacy"} with ${role}.`);
|
|
1224
1395
|
if (registration.config.replaceExisting) {
|
|
1225
1396
|
if (existing?.config.cleanup && typeof existing.config.cleanup === "function") try {
|
|
1226
1397
|
existing.config.cleanup();
|
|
1227
1398
|
} catch (cleanupError) {
|
|
1228
1399
|
this.log(`Cleanup error for replaced handler: ${String(action)}`, cleanupError, "warn");
|
|
1229
1400
|
}
|
|
1230
|
-
if (
|
|
1401
|
+
if (existing) this.observerHandlers.delete(existing);
|
|
1402
|
+
if (existingUnregister) actionUnregisterFunctions.delete(handlerId);
|
|
1231
1403
|
pipeline[existingIndex] = registration;
|
|
1232
1404
|
pipeline.sort((a, b) => b.config.priority - a.config.priority);
|
|
1233
1405
|
this.lastRegisteredTimestamps.set(action, /* @__PURE__ */ new Date());
|
|
1234
1406
|
const newUnregister = this.createUnregisterFunction(action, handlerId, registration);
|
|
1235
|
-
|
|
1407
|
+
actionUnregisterFunctions.set(handlerId, newUnregister);
|
|
1236
1408
|
this.log(`Handler replaced: ${String(action)}`, {
|
|
1237
1409
|
handlerId,
|
|
1238
1410
|
priority: config.priority,
|
|
@@ -1253,7 +1425,7 @@ var ActionRegister = class {
|
|
|
1253
1425
|
if (existingUnregister) return existingUnregister;
|
|
1254
1426
|
else {
|
|
1255
1427
|
const newUnregister = this.createUnregisterFunction(action, handlerId, existing);
|
|
1256
|
-
|
|
1428
|
+
actionUnregisterFunctions.set(handlerId, newUnregister);
|
|
1257
1429
|
return newUnregister;
|
|
1258
1430
|
}
|
|
1259
1431
|
}
|
|
@@ -1262,7 +1434,7 @@ var ActionRegister = class {
|
|
|
1262
1434
|
pipeline.sort((a, b) => b.config.priority - a.config.priority);
|
|
1263
1435
|
this.lastRegisteredTimestamps.set(action, /* @__PURE__ */ new Date());
|
|
1264
1436
|
const unregister = this.createUnregisterFunction(action, handlerId, registration);
|
|
1265
|
-
|
|
1437
|
+
actionUnregisterFunctions.set(handlerId, unregister);
|
|
1266
1438
|
this.log(`Handler registered: ${String(action)}`, {
|
|
1267
1439
|
handlerId,
|
|
1268
1440
|
priority: config.priority,
|
|
@@ -1270,59 +1442,233 @@ var ActionRegister = class {
|
|
|
1270
1442
|
});
|
|
1271
1443
|
return unregister;
|
|
1272
1444
|
}
|
|
1273
|
-
dispatch(action,
|
|
1445
|
+
dispatch(action, ...args) {
|
|
1446
|
+
this.assertStringActionKey(action);
|
|
1447
|
+
const [payload, options] = args;
|
|
1274
1448
|
if (this.lifecycleState !== "active") return this.rejectedLifecyclePromise();
|
|
1275
1449
|
const timeoutScope = this.createTimeoutScope(action, options);
|
|
1276
1450
|
const dispatchHandlerPromises = /* @__PURE__ */ new Set();
|
|
1277
1451
|
const attemptState = { count: 0 };
|
|
1278
|
-
const
|
|
1279
|
-
|
|
1280
|
-
|
|
1452
|
+
const plan = this.resolveDispatchPlan(action, options);
|
|
1453
|
+
const hasTimingGuard = plan.debounceMs !== void 0 || plan.throttleMs !== void 0;
|
|
1454
|
+
let observerNotified = false;
|
|
1455
|
+
let terminalErrorReported = false;
|
|
1456
|
+
const reportTerminalError = (error) => {
|
|
1457
|
+
if (terminalErrorReported) return;
|
|
1458
|
+
terminalErrorReported = true;
|
|
1459
|
+
this.invokeErrorHandler(error, action, payload, options, attemptState.count);
|
|
1460
|
+
};
|
|
1461
|
+
const notifyObservers = async (event) => {
|
|
1462
|
+
if (observerNotified) return;
|
|
1463
|
+
observerNotified = true;
|
|
1464
|
+
await this.executeObservers(action, plan, event);
|
|
1465
|
+
};
|
|
1466
|
+
const pipelineOperation = async () => {
|
|
1467
|
+
const guard = plan.guards.length > 0 ? await this.executeGuardPhase(action, payload, timeoutScope.options, plan, dispatchHandlerPromises) : {
|
|
1468
|
+
allowed: true,
|
|
1469
|
+
payload,
|
|
1470
|
+
aborted: false,
|
|
1471
|
+
abortReason: void 0,
|
|
1472
|
+
error: void 0,
|
|
1473
|
+
errors: [],
|
|
1474
|
+
outcomes: [],
|
|
1475
|
+
executedHandlers: [],
|
|
1476
|
+
duration: 0
|
|
1477
|
+
};
|
|
1478
|
+
this.cleanupOneTimeHandlers(action, guard.executedHandlers, dispatchHandlerPromises);
|
|
1479
|
+
if (!guard.allowed) {
|
|
1480
|
+
await notifyObservers({
|
|
1481
|
+
action: String(action),
|
|
1482
|
+
payload: guard.payload,
|
|
1483
|
+
outcome: guard.aborted ? "cancelled" : "failed",
|
|
1484
|
+
result: void 0,
|
|
1485
|
+
errors: guard.errors,
|
|
1486
|
+
signal: timeoutScope.options?.signal
|
|
1487
|
+
});
|
|
1488
|
+
if (!guard.aborted && guard.errors.length > 0) throw guard.error ?? guard.errors[0]?.error ?? /* @__PURE__ */ new Error(`Guard phase failed for "${String(action)}"`);
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
const execution = await this.executeWithRetry(async (attemptSignal) => {
|
|
1281
1492
|
const executedHandlers = [];
|
|
1282
1493
|
try {
|
|
1283
|
-
return await this.
|
|
1494
|
+
return await this._performDispatchWithResult(action, guard.payload, this.withAttemptSignal(timeoutScope.options, attemptSignal), void 0, plan, executedHandlers, dispatchHandlerPromises);
|
|
1284
1495
|
} finally {
|
|
1285
1496
|
this.cleanupOneTimeHandlers(action, executedHandlers, dispatchHandlerPromises);
|
|
1286
1497
|
}
|
|
1287
|
-
}, timeoutScope.options, attemptState,
|
|
1498
|
+
}, timeoutScope.options, attemptState, (result) => result.outcome === "failed", () => this.getAttemptHandlers(action, plan).length > 0, void 0, this.shouldDrainBeforeRetry(plan, timeoutScope.options) ? () => this.drainAttemptHandlers(dispatchHandlerPromises) : void 0);
|
|
1499
|
+
if (execution.outcome === "failed") throw execution.errors[execution.errors.length - 1]?.error ?? /* @__PURE__ */ new Error(`Action "${String(action)}" failed`);
|
|
1500
|
+
const result = this.processResults(execution.results, execution.terminated, execution.terminated ? execution.result : void 0, options?.result);
|
|
1501
|
+
await notifyObservers({
|
|
1502
|
+
action: String(action),
|
|
1503
|
+
payload: guard.payload,
|
|
1504
|
+
outcome: execution.outcome,
|
|
1505
|
+
result,
|
|
1506
|
+
errors: execution.errors,
|
|
1507
|
+
signal: timeoutScope.options?.signal
|
|
1508
|
+
});
|
|
1509
|
+
};
|
|
1510
|
+
const operation = async () => {
|
|
1511
|
+
if (timeoutScope.options?.signal?.aborted) {
|
|
1512
|
+
await notifyObservers({
|
|
1513
|
+
action: String(action),
|
|
1514
|
+
payload,
|
|
1515
|
+
outcome: "cancelled",
|
|
1516
|
+
result: void 0,
|
|
1517
|
+
errors: [],
|
|
1518
|
+
signal: timeoutScope.options?.signal
|
|
1519
|
+
});
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
this.validatePayload(action, payload);
|
|
1523
|
+
this.validateResultOptions(options?.result);
|
|
1524
|
+
if (timeoutScope.options?.signal?.aborted) {
|
|
1525
|
+
await notifyObservers({
|
|
1526
|
+
action: String(action),
|
|
1527
|
+
payload,
|
|
1528
|
+
outcome: "cancelled",
|
|
1529
|
+
result: void 0,
|
|
1530
|
+
errors: [],
|
|
1531
|
+
signal: timeoutScope.options?.signal
|
|
1532
|
+
});
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
if (hasTimingGuard) {
|
|
1536
|
+
const admission = await this.evaluateTimingGuards(String(action), plan, timeoutScope.options?.signal);
|
|
1537
|
+
if (admission.aborted || timeoutScope.options?.signal?.aborted || admission.reason) {
|
|
1538
|
+
await notifyObservers({
|
|
1539
|
+
action: String(action),
|
|
1540
|
+
payload,
|
|
1541
|
+
outcome: admission.aborted || timeoutScope.options?.signal?.aborted ? "cancelled" : admission.reason === "Debounced execution" ? "debounced" : "throttled",
|
|
1542
|
+
result: void 0,
|
|
1543
|
+
errors: admission.reason ? [{
|
|
1544
|
+
handlerId: "admission",
|
|
1545
|
+
error: new Error(admission.reason),
|
|
1546
|
+
timestamp: Date.now(),
|
|
1547
|
+
severity: "blocking"
|
|
1548
|
+
}] : [],
|
|
1549
|
+
signal: timeoutScope.options?.signal
|
|
1550
|
+
});
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
if (timeoutScope.options?.signal?.aborted) {
|
|
1555
|
+
await notifyObservers({
|
|
1556
|
+
action: String(action),
|
|
1557
|
+
payload,
|
|
1558
|
+
outcome: "cancelled",
|
|
1559
|
+
result: void 0,
|
|
1560
|
+
errors: [],
|
|
1561
|
+
signal: timeoutScope.options?.signal
|
|
1562
|
+
});
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
if (timeoutScope.options?.immediate || !this.dispatchQueue) return pipelineOperation();
|
|
1566
|
+
const queued = this.dispatchQueue.enqueueWithHandle(pipelineOperation, timeoutScope.options?.queuePriority ?? 0);
|
|
1567
|
+
timeoutScope.onTimeout((error) => queued.cancel(error));
|
|
1568
|
+
return queued.promise;
|
|
1288
1569
|
};
|
|
1289
|
-
const hasTimingGuard = options?.debounce !== void 0 || options?.throttle !== void 0 || this.pipelines.get(action)?.some((handler) => handler.config.debounce !== void 0 || handler.config.throttle !== void 0) === true;
|
|
1290
1570
|
let dispatchPromise;
|
|
1571
|
+
let observedDispatchPromise;
|
|
1291
1572
|
this.dispatchConstructionDepth += 1;
|
|
1292
1573
|
try {
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1574
|
+
dispatchPromise = operation();
|
|
1575
|
+
observedDispatchPromise = dispatchPromise.catch(async (error) => {
|
|
1576
|
+
reportTerminalError(error);
|
|
1577
|
+
await notifyObservers({
|
|
1578
|
+
action: String(action),
|
|
1579
|
+
payload,
|
|
1580
|
+
outcome: "failed",
|
|
1581
|
+
result: void 0,
|
|
1582
|
+
errors: [{
|
|
1583
|
+
handlerId: "dispatch",
|
|
1584
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
1585
|
+
timestamp: Date.now(),
|
|
1586
|
+
severity: "blocking"
|
|
1587
|
+
}],
|
|
1588
|
+
signal: timeoutScope.options?.signal
|
|
1589
|
+
});
|
|
1590
|
+
throw error;
|
|
1591
|
+
});
|
|
1592
|
+
this.trackDispatchPromise(observedDispatchPromise);
|
|
1300
1593
|
} finally {
|
|
1301
1594
|
this.dispatchConstructionDepth -= 1;
|
|
1302
1595
|
}
|
|
1303
|
-
const observedPromise = this.raceWithTimeout(
|
|
1304
|
-
|
|
1596
|
+
const observedPromise = this.raceWithTimeout(observedDispatchPromise, timeoutScope, dispatchHandlerPromises).catch(async (error) => {
|
|
1597
|
+
reportTerminalError(error);
|
|
1598
|
+
if (!observerNotified) await this.trackGlobalHandlerPromise(notifyObservers({
|
|
1599
|
+
action: String(action),
|
|
1600
|
+
payload,
|
|
1601
|
+
outcome: "failed",
|
|
1602
|
+
result: void 0,
|
|
1603
|
+
errors: [{
|
|
1604
|
+
handlerId: "dispatch",
|
|
1605
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
1606
|
+
timestamp: Date.now(),
|
|
1607
|
+
severity: "blocking"
|
|
1608
|
+
}],
|
|
1609
|
+
signal: timeoutScope.options?.signal
|
|
1610
|
+
}));
|
|
1305
1611
|
throw error;
|
|
1306
1612
|
});
|
|
1307
1613
|
observedPromise.catch(() => {});
|
|
1308
1614
|
return observedPromise;
|
|
1309
1615
|
}
|
|
1310
1616
|
/** Execute a dispatch operation with an optional whole-action retry policy. */
|
|
1311
|
-
async executeWithRetry(operation, options, attemptState, shouldRetryResult, canRetry = () => true) {
|
|
1617
|
+
async executeWithRetry(operation, options, attemptState, shouldRetryResult, canRetry = () => true, telemetry, beforeRetry) {
|
|
1312
1618
|
const configuredAttempts = options?.retryOnError?.maxAttempts ?? 1;
|
|
1313
1619
|
const maxAttempts = Number.isFinite(configuredAttempts) ? Math.max(1, Math.floor(configuredAttempts)) : 1;
|
|
1314
1620
|
const retryDelay = Math.max(0, options?.retryOnError?.delay ?? 0);
|
|
1315
1621
|
while (attemptState.count < maxAttempts) {
|
|
1316
1622
|
attemptState.count += 1;
|
|
1623
|
+
const attemptStartedAt = Date.now();
|
|
1624
|
+
const attemptController = new AbortController();
|
|
1317
1625
|
try {
|
|
1318
|
-
const result = await operation();
|
|
1319
|
-
|
|
1626
|
+
const result = await operation(attemptController.signal);
|
|
1627
|
+
const shouldRetry = shouldRetryResult?.(result) ?? false;
|
|
1628
|
+
const canRetryAttempt = shouldRetry && attemptState.count < maxAttempts && !options?.signal?.aborted && canRetry();
|
|
1629
|
+
const attemptEndedAt = Date.now();
|
|
1630
|
+
telemetry?.attempts.push({
|
|
1631
|
+
startTime: attemptStartedAt,
|
|
1632
|
+
endTime: attemptEndedAt,
|
|
1633
|
+
duration: attemptEndedAt - attemptStartedAt,
|
|
1634
|
+
outcome: shouldRetry ? canRetryAttempt ? "retried" : "failed" : "succeeded"
|
|
1635
|
+
});
|
|
1636
|
+
if (telemetry) telemetry.pipelineDuration += attemptEndedAt - attemptStartedAt;
|
|
1637
|
+
if (!canRetryAttempt) return result;
|
|
1638
|
+
attemptController.abort(new ActionAttemptSupersededError(attemptState.count));
|
|
1639
|
+
await beforeRetry?.();
|
|
1640
|
+
const retryStartedAt = Date.now();
|
|
1641
|
+
const shouldContinue = await this.waitForRetry(retryDelay, options?.signal);
|
|
1642
|
+
if (telemetry) telemetry.retryDelayDuration += Date.now() - retryStartedAt;
|
|
1643
|
+
if (!shouldContinue) {
|
|
1644
|
+
telemetry?.attempts.push({
|
|
1645
|
+
startTime: Date.now(),
|
|
1646
|
+
endTime: Date.now(),
|
|
1647
|
+
duration: 0,
|
|
1648
|
+
outcome: "cancelled"
|
|
1649
|
+
});
|
|
1650
|
+
return result;
|
|
1651
|
+
}
|
|
1320
1652
|
} catch (error) {
|
|
1321
|
-
|
|
1653
|
+
const attemptEndedAt = Date.now();
|
|
1654
|
+
const canRetryAttempt = !(error instanceof ActionValidationError || attemptState.count >= maxAttempts || options?.signal?.aborted || !canRetry());
|
|
1655
|
+
telemetry?.attempts.push({
|
|
1656
|
+
startTime: attemptStartedAt,
|
|
1657
|
+
endTime: attemptEndedAt,
|
|
1658
|
+
duration: attemptEndedAt - attemptStartedAt,
|
|
1659
|
+
outcome: canRetryAttempt ? "retried" : "failed"
|
|
1660
|
+
});
|
|
1661
|
+
if (telemetry) telemetry.pipelineDuration += attemptEndedAt - attemptStartedAt;
|
|
1662
|
+
if (!canRetryAttempt) throw error;
|
|
1663
|
+
attemptController.abort(new ActionAttemptSupersededError(attemptState.count));
|
|
1664
|
+
await beforeRetry?.();
|
|
1665
|
+
const retryStartedAt = Date.now();
|
|
1666
|
+
const shouldContinue = await this.waitForRetry(retryDelay, options?.signal);
|
|
1667
|
+
if (telemetry) telemetry.retryDelayDuration += Date.now() - retryStartedAt;
|
|
1668
|
+
if (!shouldContinue) throw error;
|
|
1322
1669
|
}
|
|
1323
|
-
await this.waitForRetry(retryDelay, options?.signal);
|
|
1324
1670
|
}
|
|
1325
|
-
return operation();
|
|
1671
|
+
return operation(new AbortController().signal);
|
|
1326
1672
|
}
|
|
1327
1673
|
trackDispatchPromise(promise) {
|
|
1328
1674
|
this.activeDispatches.add(promise);
|
|
@@ -1340,6 +1686,43 @@ var ActionRegister = class {
|
|
|
1340
1686
|
promise.then(remove, remove);
|
|
1341
1687
|
return promise;
|
|
1342
1688
|
}
|
|
1689
|
+
withAttemptSignal(options, attemptSignal) {
|
|
1690
|
+
const outerSignal = options?.signal;
|
|
1691
|
+
if (!outerSignal) return {
|
|
1692
|
+
...options,
|
|
1693
|
+
signal: attemptSignal
|
|
1694
|
+
};
|
|
1695
|
+
if (typeof AbortSignal.any === "function") return {
|
|
1696
|
+
...options,
|
|
1697
|
+
signal: AbortSignal.any([outerSignal, attemptSignal])
|
|
1698
|
+
};
|
|
1699
|
+
const controller = new AbortController();
|
|
1700
|
+
const forwardOuter = () => controller.abort(outerSignal.reason);
|
|
1701
|
+
const forwardAttempt = () => controller.abort(attemptSignal.reason);
|
|
1702
|
+
if (outerSignal.aborted) forwardOuter();
|
|
1703
|
+
else outerSignal.addEventListener("abort", forwardOuter, { once: true });
|
|
1704
|
+
if (attemptSignal.aborted) forwardAttempt();
|
|
1705
|
+
else attemptSignal.addEventListener("abort", forwardAttempt, { once: true });
|
|
1706
|
+
const cleanup = () => {
|
|
1707
|
+
outerSignal.removeEventListener("abort", forwardOuter);
|
|
1708
|
+
attemptSignal.removeEventListener("abort", forwardAttempt);
|
|
1709
|
+
};
|
|
1710
|
+
controller.signal.addEventListener("abort", cleanup, { once: true });
|
|
1711
|
+
return {
|
|
1712
|
+
...options,
|
|
1713
|
+
signal: controller.signal,
|
|
1714
|
+
[ATTEMPT_SIGNAL_CLEANUP]: cleanup
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
shouldDrainBeforeRetry(plan, options) {
|
|
1718
|
+
return (options?.retryOnError?.attemptBarrier ?? (plan.executionMode === "race" ? "abort-and-drain" : "abort-and-overlap")) === "abort-and-drain";
|
|
1719
|
+
}
|
|
1720
|
+
/** Do not begin a whole-action retry while a previous race loser is still
|
|
1721
|
+
* running. Handlers should still observe their signal for cancellation. */
|
|
1722
|
+
async drainAttemptHandlers(dispatchHandlerPromises) {
|
|
1723
|
+
const pending = [...dispatchHandlerPromises];
|
|
1724
|
+
if (pending.length > 0) await Promise.allSettled(pending);
|
|
1725
|
+
}
|
|
1343
1726
|
trackGlobalHandlerPromise(promise) {
|
|
1344
1727
|
this.activeHandlerPromises.add(promise);
|
|
1345
1728
|
const remove = () => this.activeHandlerPromises.delete(promise);
|
|
@@ -1348,22 +1731,25 @@ var ActionRegister = class {
|
|
|
1348
1731
|
}
|
|
1349
1732
|
/** Abort-aware retry delay so cancellation does not wait for the full backoff. */
|
|
1350
1733
|
waitForRetry(delay, signal) {
|
|
1351
|
-
if (
|
|
1734
|
+
if (signal?.aborted) return Promise.resolve(false);
|
|
1735
|
+
if (delay <= 0) return Promise.resolve(true);
|
|
1352
1736
|
return new Promise((resolve) => {
|
|
1353
1737
|
const timer = setTimeout(finish, delay);
|
|
1354
|
-
const abort = () => finish();
|
|
1355
|
-
function finish() {
|
|
1738
|
+
const abort = () => finish(false);
|
|
1739
|
+
function finish(shouldContinue = true) {
|
|
1356
1740
|
clearTimeout(timer);
|
|
1357
1741
|
signal?.removeEventListener("abort", abort);
|
|
1358
|
-
resolve();
|
|
1742
|
+
resolve(shouldContinue);
|
|
1359
1743
|
}
|
|
1360
1744
|
signal?.addEventListener("abort", abort, { once: true });
|
|
1361
1745
|
});
|
|
1362
1746
|
}
|
|
1363
1747
|
/** Build a wall-clock timeout that also participates in pipeline cancellation. */
|
|
1364
1748
|
createTimeoutScope(action, options) {
|
|
1365
|
-
const
|
|
1366
|
-
|
|
1749
|
+
const configuredTimeout = options?.timeout;
|
|
1750
|
+
if (configuredTimeout !== void 0 && (!Number.isFinite(configuredTimeout) || configuredTimeout < 0)) throw new RangeError("timeout must be a non-negative finite number.");
|
|
1751
|
+
const hasTimeout = configuredTimeout !== void 0;
|
|
1752
|
+
const timeout = configuredTimeout;
|
|
1367
1753
|
const timeoutController = hasTimeout ? new AbortController() : void 0;
|
|
1368
1754
|
const signalCleanups = [];
|
|
1369
1755
|
const timeoutCallbacks = /* @__PURE__ */ new Set();
|
|
@@ -1481,13 +1867,23 @@ var ActionRegister = class {
|
|
|
1481
1867
|
errors: result.error.issues.map((issue) => issue.message)
|
|
1482
1868
|
};
|
|
1483
1869
|
}
|
|
1484
|
-
createAbortedExecutionResult(startTime,
|
|
1870
|
+
createAbortedExecutionResult(startTime, skippedRegistrations = [], validation) {
|
|
1485
1871
|
const endTime = Date.now();
|
|
1872
|
+
const handlers = skippedRegistrations.map((registration) => ({
|
|
1873
|
+
id: registration.id,
|
|
1874
|
+
status: "skipped",
|
|
1875
|
+
executed: false,
|
|
1876
|
+
duration: 0,
|
|
1877
|
+
result: void 0,
|
|
1878
|
+
error: void 0,
|
|
1879
|
+
metadata: registration.config.metadata ? { ...registration.config.metadata } : void 0
|
|
1880
|
+
}));
|
|
1486
1881
|
return {
|
|
1487
1882
|
success: false,
|
|
1488
1883
|
aborted: true,
|
|
1489
1884
|
abortReason: "Action dispatch aborted by signal",
|
|
1490
1885
|
terminated: false,
|
|
1886
|
+
outcome: "cancelled",
|
|
1491
1887
|
validation,
|
|
1492
1888
|
result: void 0,
|
|
1493
1889
|
successResults: [],
|
|
@@ -1495,198 +1891,524 @@ var ActionRegister = class {
|
|
|
1495
1891
|
failedResults: [],
|
|
1496
1892
|
execution: {
|
|
1497
1893
|
duration: endTime - startTime,
|
|
1894
|
+
admissionDuration: endTime - startTime,
|
|
1895
|
+
queueWaitDuration: 0,
|
|
1896
|
+
pipelineDuration: 0,
|
|
1498
1897
|
handlersExecuted: 0,
|
|
1499
|
-
handlersSkipped,
|
|
1898
|
+
handlersSkipped: handlers.length,
|
|
1500
1899
|
handlersFailed: 0,
|
|
1501
1900
|
startTime,
|
|
1502
1901
|
endTime
|
|
1503
1902
|
},
|
|
1504
|
-
handlers
|
|
1903
|
+
handlers,
|
|
1505
1904
|
errors: []
|
|
1506
1905
|
};
|
|
1507
1906
|
}
|
|
1907
|
+
resolveDispatchPlan(action, options) {
|
|
1908
|
+
const pipelineSnapshot = [...this.pipelines.get(action) ?? []];
|
|
1909
|
+
const guards = pipelineSnapshot.filter((handler) => handler.role === "guard");
|
|
1910
|
+
const filterableHandlers = pipelineSnapshot.filter((handler) => handler.role !== "guard");
|
|
1911
|
+
const filteredHandlers = options?.filter ? this.filterHandlers(filterableHandlers, options.filter) : filterableHandlers;
|
|
1912
|
+
const eligibleHandlers = [...guards, ...filteredHandlers];
|
|
1913
|
+
const admissionHandlers = eligibleHandlers.filter((handler) => handler.role !== "observer");
|
|
1914
|
+
const debounceMs = options?.debounce ?? admissionHandlers.find((handler) => handler.config.debounce !== void 0)?.config.debounce;
|
|
1915
|
+
const throttleMs = options?.throttle ?? admissionHandlers.find((handler) => handler.config.throttle !== void 0)?.config.throttle;
|
|
1916
|
+
return {
|
|
1917
|
+
pipelineSnapshot,
|
|
1918
|
+
eligibleHandlers,
|
|
1919
|
+
guards,
|
|
1920
|
+
results: filteredHandlers.filter((handler) => handler.role !== "observer"),
|
|
1921
|
+
observers: filteredHandlers.filter((handler) => handler.role === "observer"),
|
|
1922
|
+
debounceMs,
|
|
1923
|
+
throttleMs,
|
|
1924
|
+
executionMode: options?.executionMode ?? this.actionExecutionModes.get(action) ?? this.executionMode
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1927
|
+
async evaluateTimingGuards(actionKey, plan, signal) {
|
|
1928
|
+
const { debounceMs, throttleMs } = plan;
|
|
1929
|
+
if (signal?.aborted) return { aborted: true };
|
|
1930
|
+
if (debounceMs !== void 0 && !await this.actionGuard.debounce(actionKey, debounceMs, signal)) return signal?.aborted ? { aborted: true } : {
|
|
1931
|
+
aborted: false,
|
|
1932
|
+
reason: "Debounced execution"
|
|
1933
|
+
};
|
|
1934
|
+
if (throttleMs !== void 0 && !this.actionGuard.throttle(actionKey, throttleMs, signal)) return signal?.aborted ? { aborted: true } : {
|
|
1935
|
+
aborted: false,
|
|
1936
|
+
reason: "Throttled execution"
|
|
1937
|
+
};
|
|
1938
|
+
return { aborted: false };
|
|
1939
|
+
}
|
|
1508
1940
|
/**
|
|
1509
|
-
*
|
|
1941
|
+
* Keep the dispatch plan stable across retries while honoring handlers that
|
|
1942
|
+
* were consumed by the `once` lifecycle after an earlier attempt.
|
|
1510
1943
|
*/
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
if (options?.autoAbort?.onControllerCreated && autoAbortController) options.autoAbort.onControllerCreated(autoAbortController);
|
|
1521
|
-
if (effectiveSignal?.aborted) {
|
|
1522
|
-
this.log(`Dispatch aborted before execution for '${String(action)}'`);
|
|
1523
|
-
cleanup();
|
|
1524
|
-
return;
|
|
1525
|
-
}
|
|
1526
|
-
const pipeline = this.pipelines.get(action);
|
|
1527
|
-
this.log(`Pipeline lookup for '${String(action)}'`, {
|
|
1528
|
-
pipelineExists: Boolean(pipeline),
|
|
1529
|
-
handlersCount: pipeline?.length || 0,
|
|
1530
|
-
allRegisteredActions: Array.from(this.pipelines.keys()),
|
|
1531
|
-
pipelineMap: Object.fromEntries(Array.from(this.pipelines.entries()).map(([k, v]) => [k, v.length]))
|
|
1944
|
+
getAttemptHandlers(action, plan) {
|
|
1945
|
+
const activePipeline = this.pipelines.get(action) ?? [];
|
|
1946
|
+
return plan.results.filter((handler) => !handler.config.once || activePipeline.includes(handler));
|
|
1947
|
+
}
|
|
1948
|
+
getObservers(action, plan) {
|
|
1949
|
+
const activePipeline = this.pipelines.get(action) ?? [];
|
|
1950
|
+
return plan.observers.flatMap((registration) => {
|
|
1951
|
+
const observer = this.observerHandlers.get(registration);
|
|
1952
|
+
return registration.role === "observer" && activePipeline.includes(registration) && observer ? [[registration, observer]] : [];
|
|
1532
1953
|
});
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
throttleMs = handler.config.throttle;
|
|
1550
|
-
break;
|
|
1551
|
-
}
|
|
1552
|
-
}
|
|
1553
|
-
if (options?.debounce !== void 0) debounceMs = options.debounce;
|
|
1554
|
-
else if (filteredHandlers.length > 0) {
|
|
1555
|
-
for (const handler of filteredHandlers) if (handler.config.debounce !== void 0) {
|
|
1556
|
-
debounceMs = handler.config.debounce;
|
|
1557
|
-
break;
|
|
1558
|
-
}
|
|
1559
|
-
}
|
|
1560
|
-
if (!skipGuards && debounceMs !== void 0) {
|
|
1561
|
-
if (!await this.actionGuard.debounce(actionKey, debounceMs)) {
|
|
1562
|
-
cleanup();
|
|
1563
|
-
return;
|
|
1954
|
+
}
|
|
1955
|
+
/** Observers run after the canonical result has been constructed. Their
|
|
1956
|
+
* failures are isolated from that immutable result; detached observers are
|
|
1957
|
+
* still tracked for registry shutdown. */
|
|
1958
|
+
async executeObservers(action, plan, event) {
|
|
1959
|
+
const observerEvent = this.safeSnapshotObserverEvent(event);
|
|
1960
|
+
for (const [registration, observerEntry] of this.getObservers(action, plan)) {
|
|
1961
|
+
const successful = observerEvent.outcome === "completed" || observerEvent.outcome === "completed_with_errors";
|
|
1962
|
+
if (observerEntry.when === "success" && !successful) continue;
|
|
1963
|
+
if (observerEntry.when === "failure" && successful) continue;
|
|
1964
|
+
let shouldRun = true;
|
|
1965
|
+
try {
|
|
1966
|
+
shouldRun = registration.config.condition?.(observerEvent.payload) ?? true;
|
|
1967
|
+
} catch (error) {
|
|
1968
|
+
this.log(`Observer condition failed for ${String(action)}`, error, "warn");
|
|
1969
|
+
continue;
|
|
1564
1970
|
}
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1971
|
+
if (!shouldRun) continue;
|
|
1972
|
+
const detachedOnce = registration.config.once && this.removeRegistration(action, registration, false);
|
|
1973
|
+
const cleanupOnce = () => {
|
|
1974
|
+
if (detachedOnce) this.runRegistrationCleanup(action, registration);
|
|
1975
|
+
};
|
|
1976
|
+
const invocation = Promise.resolve().then(() => observerEntry.handler(observerEvent));
|
|
1977
|
+
if (registration.config.scheduling === "start-and-continue") this.trackGlobalHandlerPromise(invocation).then(cleanupOnce, (error) => {
|
|
1978
|
+
this.log(`Observer failed for ${String(action)}`, error, "warn");
|
|
1979
|
+
cleanupOnce();
|
|
1980
|
+
});
|
|
1981
|
+
else try {
|
|
1982
|
+
await invocation;
|
|
1983
|
+
} catch (error) {
|
|
1984
|
+
this.log(`Observer failed for ${String(action)}`, error, "warn");
|
|
1985
|
+
} finally {
|
|
1986
|
+
cleanupOnce();
|
|
1570
1987
|
}
|
|
1571
1988
|
}
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1989
|
+
}
|
|
1990
|
+
/** Give JavaScript observers an isolated, shallowly immutable terminal view.
|
|
1991
|
+
* Result payloads are intentionally not deep-cloned: arbitrary result values
|
|
1992
|
+
* may be class instances, streams, or identity-bearing domain objects. */
|
|
1993
|
+
snapshotObserverEvent(event) {
|
|
1994
|
+
const freezeValue = (value) => {
|
|
1995
|
+
if (Array.isArray(value)) return Object.freeze([...value]);
|
|
1996
|
+
if (value !== null && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype) return Object.freeze({ ...value });
|
|
1997
|
+
return value;
|
|
1998
|
+
};
|
|
1999
|
+
return Object.freeze({
|
|
2000
|
+
...event,
|
|
2001
|
+
payload: freezeValue(event.payload),
|
|
2002
|
+
result: freezeValue(event.result),
|
|
2003
|
+
errors: Object.freeze(event.errors.map((error) => Object.freeze({ ...error })))
|
|
2004
|
+
});
|
|
2005
|
+
}
|
|
2006
|
+
/** A diagnostic observer must never make an already constructed canonical
|
|
2007
|
+
* result reject, including when shallow-copying a Proxy/getter throws. */
|
|
2008
|
+
safeSnapshotObserverEvent(event) {
|
|
2009
|
+
try {
|
|
2010
|
+
return this.snapshotObserverEvent(event);
|
|
2011
|
+
} catch (error) {
|
|
2012
|
+
this.log("Observer snapshot failed", error, "warn");
|
|
2013
|
+
return Object.freeze({
|
|
2014
|
+
action: event.action,
|
|
2015
|
+
payload: event.payload,
|
|
2016
|
+
outcome: event.outcome,
|
|
2017
|
+
result: void 0,
|
|
2018
|
+
errors: Object.freeze([]),
|
|
2019
|
+
...event.signal === void 0 ? {} : { signal: event.signal }
|
|
2020
|
+
});
|
|
1576
2021
|
}
|
|
1577
|
-
|
|
2022
|
+
}
|
|
2023
|
+
/** Execute the selected guard snapshot once for the whole dispatch. Guards
|
|
2024
|
+
* are deliberately outside the retry loop: authorization and normalization
|
|
2025
|
+
* belong to admission, not to each provider attempt. */
|
|
2026
|
+
async executeGuardPhase(action, payload, options, plan, dispatchHandlerPromises) {
|
|
2027
|
+
if (plan.guards.length === 0) return {
|
|
2028
|
+
allowed: true,
|
|
2029
|
+
payload,
|
|
2030
|
+
aborted: false,
|
|
2031
|
+
abortReason: void 0,
|
|
2032
|
+
error: void 0,
|
|
2033
|
+
errors: [],
|
|
2034
|
+
outcomes: [],
|
|
2035
|
+
executedHandlers: [],
|
|
2036
|
+
duration: 0
|
|
2037
|
+
};
|
|
2038
|
+
const [signal, autoAbortController, cleanup] = this.createAbortSignal(options);
|
|
1578
2039
|
const context = {
|
|
1579
2040
|
action: String(action),
|
|
1580
2041
|
payload,
|
|
1581
|
-
handlers: [...
|
|
2042
|
+
handlers: [...plan.guards],
|
|
1582
2043
|
executedHandlers: [],
|
|
1583
|
-
|
|
1584
|
-
|
|
2044
|
+
handlerOutcomes: [],
|
|
2045
|
+
claimOnce: (registration) => this.claimOnceRegistration(action, registration),
|
|
2046
|
+
signal: signal ?? this.lifecycleController.signal,
|
|
1585
2047
|
trackHandlerPromise: (promise) => this.trackHandlerPromise(promise, dispatchHandlerPromises),
|
|
1586
2048
|
aborted: false,
|
|
1587
2049
|
abortReason: void 0,
|
|
1588
2050
|
currentIndex: 0,
|
|
1589
2051
|
jumpToPriority: void 0,
|
|
1590
2052
|
jumpCount: 0,
|
|
1591
|
-
maxJumps:
|
|
1592
|
-
executionMode:
|
|
2053
|
+
maxJumps: this.maxJumps,
|
|
2054
|
+
executionMode: "sequential",
|
|
1593
2055
|
results: [],
|
|
1594
2056
|
terminated: false,
|
|
1595
2057
|
terminationResult: void 0
|
|
1596
2058
|
};
|
|
1597
|
-
const abortHandler =
|
|
2059
|
+
const abortHandler = signal ? () => {
|
|
1598
2060
|
context.aborted = true;
|
|
1599
|
-
context.abortReason = typeof
|
|
2061
|
+
context.abortReason = typeof signal.reason === "string" ? signal.reason : "Action dispatch aborted by signal";
|
|
1600
2062
|
} : void 0;
|
|
1601
|
-
|
|
2063
|
+
signal?.addEventListener("abort", abortHandler, { once: true });
|
|
2064
|
+
let error;
|
|
1602
2065
|
try {
|
|
1603
|
-
await
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
2066
|
+
await executeSequential(context, (registration, _index) => {
|
|
2067
|
+
const controller = this.createController(context, autoAbortController, options?.autoAbort, void 0, false);
|
|
2068
|
+
return {
|
|
2069
|
+
signal: controller.signal,
|
|
2070
|
+
getPayload: controller.getPayload,
|
|
2071
|
+
modifyPayload: controller.modifyPayload,
|
|
2072
|
+
abort: controller.abort
|
|
2073
|
+
};
|
|
2074
|
+
});
|
|
2075
|
+
} catch (caught) {
|
|
2076
|
+
error = caught instanceof Error ? caught : new Error(String(caught));
|
|
1608
2077
|
} finally {
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
2078
|
+
if (signal && abortHandler) signal.removeEventListener("abort", abortHandler);
|
|
2079
|
+
this.cleanupSignalsAfterStartedHandlers(() => {
|
|
2080
|
+
cleanup();
|
|
2081
|
+
options?.[ATTEMPT_SIGNAL_CLEANUP]?.();
|
|
2082
|
+
}, dispatchHandlerPromises);
|
|
1612
2083
|
}
|
|
2084
|
+
const errors = [...context.collectedErrors ?? []];
|
|
2085
|
+
if (error && !errors.some((entry) => entry.error === error)) errors.push({
|
|
2086
|
+
handlerId: "guard",
|
|
2087
|
+
error,
|
|
2088
|
+
timestamp: Date.now(),
|
|
2089
|
+
severity: "blocking"
|
|
2090
|
+
});
|
|
2091
|
+
return {
|
|
2092
|
+
allowed: !context.aborted && error === void 0 && errors.length === 0,
|
|
2093
|
+
payload: context.payload,
|
|
2094
|
+
aborted: context.aborted,
|
|
2095
|
+
abortReason: context.abortReason,
|
|
2096
|
+
error,
|
|
2097
|
+
errors,
|
|
2098
|
+
outcomes: (context.handlerOutcomes ?? []).map(snapshotHandlerOutcome),
|
|
2099
|
+
executedHandlers: context.executedHandlers ?? [],
|
|
2100
|
+
duration: (context.handlerOutcomes ?? []).reduce((total, outcome) => total + (outcome.duration ?? 0), 0)
|
|
2101
|
+
};
|
|
1613
2102
|
}
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
2103
|
+
createGuardRejectedResult(startTime, validation, guard, selectedHandlers) {
|
|
2104
|
+
const endTime = Date.now();
|
|
2105
|
+
const outcomeById = new Map(guard.outcomes.map((outcome) => [outcome.id, outcome]));
|
|
2106
|
+
const handlers = selectedHandlers.map((registration) => {
|
|
2107
|
+
const outcome = outcomeById.get(registration.id);
|
|
2108
|
+
return outcome ? {
|
|
2109
|
+
...snapshotHandlerOutcome(outcome),
|
|
2110
|
+
result: void 0
|
|
2111
|
+
} : {
|
|
2112
|
+
id: registration.id,
|
|
2113
|
+
status: "skipped",
|
|
2114
|
+
executed: false,
|
|
2115
|
+
duration: 0,
|
|
2116
|
+
result: void 0,
|
|
2117
|
+
error: void 0,
|
|
2118
|
+
metadata: registration.config.metadata ? { ...registration.config.metadata } : void 0
|
|
2119
|
+
};
|
|
2120
|
+
});
|
|
2121
|
+
return {
|
|
2122
|
+
success: false,
|
|
2123
|
+
aborted: guard.aborted,
|
|
2124
|
+
abortReason: guard.abortReason ?? guard.error?.message,
|
|
2125
|
+
terminated: false,
|
|
2126
|
+
outcome: guard.aborted ? "cancelled" : "failed",
|
|
2127
|
+
validation,
|
|
2128
|
+
result: void 0,
|
|
2129
|
+
successResults: [],
|
|
2130
|
+
results: [],
|
|
2131
|
+
failedResults: guard.errors.map((error) => ({
|
|
2132
|
+
handlerId: error.handlerId,
|
|
2133
|
+
error: error.error,
|
|
2134
|
+
expectedType: "unknown"
|
|
2135
|
+
})),
|
|
2136
|
+
execution: {
|
|
2137
|
+
duration: endTime - startTime,
|
|
2138
|
+
admissionDuration: 0,
|
|
2139
|
+
queueWaitDuration: 0,
|
|
2140
|
+
pipelineDuration: endTime - startTime,
|
|
2141
|
+
handlersExecuted: handlers.filter((handler) => handler.executed).length,
|
|
2142
|
+
handlersSkipped: handlers.filter((handler) => !handler.executed).length,
|
|
2143
|
+
handlersFailed: handlers.filter((handler) => handler.status === "failed").length,
|
|
2144
|
+
startTime,
|
|
2145
|
+
endTime
|
|
2146
|
+
},
|
|
2147
|
+
handlers,
|
|
2148
|
+
errors: guard.errors
|
|
2149
|
+
};
|
|
2150
|
+
}
|
|
2151
|
+
createTimingGuardResult(reason, startTime, handlers, validation) {
|
|
2152
|
+
const endTime = Date.now();
|
|
2153
|
+
return {
|
|
2154
|
+
success: false,
|
|
2155
|
+
aborted: false,
|
|
2156
|
+
abortReason: reason,
|
|
2157
|
+
terminated: false,
|
|
2158
|
+
outcome: reason === "Debounced execution" ? "debounced" : "throttled",
|
|
2159
|
+
validation,
|
|
2160
|
+
result: void 0,
|
|
2161
|
+
successResults: [],
|
|
2162
|
+
results: [],
|
|
2163
|
+
failedResults: [],
|
|
2164
|
+
execution: {
|
|
2165
|
+
duration: endTime - startTime,
|
|
2166
|
+
admissionDuration: endTime - startTime,
|
|
2167
|
+
queueWaitDuration: 0,
|
|
2168
|
+
pipelineDuration: 0,
|
|
2169
|
+
handlersExecuted: 0,
|
|
2170
|
+
handlersSkipped: handlers.length,
|
|
2171
|
+
handlersFailed: 0,
|
|
2172
|
+
startTime,
|
|
2173
|
+
endTime
|
|
2174
|
+
},
|
|
2175
|
+
handlers: handlers.map((handler) => ({
|
|
2176
|
+
id: handler.id,
|
|
2177
|
+
status: "skipped",
|
|
2178
|
+
executed: false,
|
|
2179
|
+
duration: 0,
|
|
2180
|
+
result: void 0,
|
|
2181
|
+
error: void 0,
|
|
2182
|
+
metadata: handler.config.metadata ? { ...handler.config.metadata } : void 0
|
|
2183
|
+
})),
|
|
2184
|
+
errors: []
|
|
2185
|
+
};
|
|
2186
|
+
}
|
|
2187
|
+
dispatchWithResult(action, ...args) {
|
|
2188
|
+
this.assertStringActionKey(action);
|
|
2189
|
+
const [payload, options] = args;
|
|
1628
2190
|
if (this.lifecycleState !== "active") return this.rejectedLifecyclePromise();
|
|
1629
2191
|
const timeoutScope = this.createTimeoutScope(action, options);
|
|
2192
|
+
const dispatchStartTime = Date.now();
|
|
1630
2193
|
const dispatchHandlerPromises = /* @__PURE__ */ new Set();
|
|
1631
2194
|
const attemptState = { count: 0 };
|
|
1632
2195
|
let validation;
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
2196
|
+
let observerPayload = payload;
|
|
2197
|
+
let admissionEndedAt = dispatchStartTime;
|
|
2198
|
+
let pipelineStartedAt;
|
|
2199
|
+
const retryTelemetry = {
|
|
2200
|
+
pipelineDuration: 0,
|
|
2201
|
+
retryDelayDuration: 0,
|
|
2202
|
+
attempts: []
|
|
2203
|
+
};
|
|
2204
|
+
const plan = this.resolveDispatchPlan(action, options);
|
|
2205
|
+
const hasTimingGuard = plan.debounceMs !== void 0 || plan.throttleMs !== void 0;
|
|
2206
|
+
const pipelineOperation = async () => {
|
|
2207
|
+
pipelineStartedAt = Date.now();
|
|
2208
|
+
const guard = plan.guards.length > 0 ? await this.executeGuardPhase(action, payload, timeoutScope.options, plan, dispatchHandlerPromises) : {
|
|
2209
|
+
allowed: true,
|
|
2210
|
+
payload,
|
|
2211
|
+
aborted: false,
|
|
2212
|
+
abortReason: void 0,
|
|
2213
|
+
error: void 0,
|
|
2214
|
+
errors: [],
|
|
2215
|
+
outcomes: [],
|
|
2216
|
+
executedHandlers: [],
|
|
2217
|
+
duration: 0
|
|
2218
|
+
};
|
|
2219
|
+
this.cleanupOneTimeHandlers(action, guard.executedHandlers, dispatchHandlerPromises);
|
|
2220
|
+
observerPayload = guard.payload;
|
|
2221
|
+
if (!guard.allowed) return this.createGuardRejectedResult(pipelineStartedAt, validation, guard, [...plan.guards, ...plan.results]);
|
|
2222
|
+
const rawExecution = await this.executeWithRetry(async (attemptSignal) => {
|
|
1636
2223
|
const executedHandlers = [];
|
|
1637
2224
|
try {
|
|
1638
|
-
return await this._performDispatchWithResult(action, payload, timeoutScope.options, validation,
|
|
2225
|
+
return await this._performDispatchWithResult(action, guard.payload, this.withAttemptSignal(timeoutScope.options, attemptSignal), validation, plan, executedHandlers, dispatchHandlerPromises);
|
|
1639
2226
|
} finally {
|
|
1640
2227
|
this.cleanupOneTimeHandlers(action, executedHandlers, dispatchHandlerPromises);
|
|
1641
2228
|
}
|
|
1642
|
-
}, timeoutScope.options, attemptState, (result) =>
|
|
2229
|
+
}, timeoutScope.options, attemptState, (result) => result.outcome === "failed", () => this.getAttemptHandlers(action, plan).length > 0, retryTelemetry, this.shouldDrainBeforeRetry(plan, timeoutScope.options) ? () => this.drainAttemptHandlers(dispatchHandlerPromises) : void 0);
|
|
2230
|
+
if (timeoutScope.options?.signal?.aborted) return {
|
|
2231
|
+
...rawExecution,
|
|
2232
|
+
success: false,
|
|
2233
|
+
aborted: true,
|
|
2234
|
+
abortReason: typeof timeoutScope.options.signal.reason === "string" ? timeoutScope.options.signal.reason : "Action dispatch aborted by signal",
|
|
2235
|
+
outcome: "cancelled"
|
|
2236
|
+
};
|
|
2237
|
+
const executionWithGuards = {
|
|
2238
|
+
...rawExecution,
|
|
2239
|
+
handlers: [...guard.outcomes.map((outcome) => ({
|
|
2240
|
+
...outcome,
|
|
2241
|
+
result: void 0
|
|
2242
|
+
})), ...rawExecution.handlers],
|
|
2243
|
+
execution: {
|
|
2244
|
+
...rawExecution.execution,
|
|
2245
|
+
handlersExecuted: guard.outcomes.filter((outcome) => outcome.executed).length + rawExecution.execution.handlersExecuted,
|
|
2246
|
+
handlersSkipped: guard.outcomes.filter((outcome) => !outcome.executed).length + rawExecution.execution.handlersSkipped,
|
|
2247
|
+
handlersFailed: guard.outcomes.filter((outcome) => outcome.status === "failed").length + rawExecution.execution.handlersFailed
|
|
2248
|
+
}
|
|
2249
|
+
};
|
|
2250
|
+
const resultProcessingStartedAt = Date.now();
|
|
2251
|
+
const result = this.processResults(executionWithGuards.results, executionWithGuards.terminated, executionWithGuards.terminated ? executionWithGuards.result : void 0, options?.result);
|
|
2252
|
+
const resultProcessingDuration = Date.now() - resultProcessingStartedAt;
|
|
2253
|
+
return {
|
|
2254
|
+
...executionWithGuards,
|
|
2255
|
+
result,
|
|
2256
|
+
execution: {
|
|
2257
|
+
...executionWithGuards.execution,
|
|
2258
|
+
pipelineDuration: guard.duration + retryTelemetry.pipelineDuration,
|
|
2259
|
+
retryDelayDuration: retryTelemetry.retryDelayDuration,
|
|
2260
|
+
resultProcessingDuration,
|
|
2261
|
+
attempts: retryTelemetry.attempts
|
|
2262
|
+
}
|
|
2263
|
+
};
|
|
2264
|
+
};
|
|
2265
|
+
const operation = async () => {
|
|
2266
|
+
if (timeoutScope.options?.signal?.aborted) {
|
|
2267
|
+
admissionEndedAt = Date.now();
|
|
2268
|
+
return this.createAbortedExecutionResult(dispatchStartTime, [...plan.guards, ...plan.results]);
|
|
2269
|
+
}
|
|
2270
|
+
validation = this.validatePayload(action, payload);
|
|
2271
|
+
if (timeoutScope.options?.signal?.aborted) {
|
|
2272
|
+
admissionEndedAt = Date.now();
|
|
2273
|
+
return this.createAbortedExecutionResult(dispatchStartTime, [...plan.guards, ...plan.results], validation);
|
|
2274
|
+
}
|
|
2275
|
+
this.validateResultOptions(options?.result);
|
|
2276
|
+
if (hasTimingGuard) {
|
|
2277
|
+
const admission = await this.evaluateTimingGuards(String(action), plan, timeoutScope.options?.signal);
|
|
2278
|
+
if (admission.aborted || timeoutScope.options?.signal?.aborted) {
|
|
2279
|
+
admissionEndedAt = Date.now();
|
|
2280
|
+
return this.createAbortedExecutionResult(dispatchStartTime, [...plan.guards, ...plan.results], validation);
|
|
2281
|
+
}
|
|
2282
|
+
if (admission.reason) {
|
|
2283
|
+
admissionEndedAt = Date.now();
|
|
2284
|
+
return this.createTimingGuardResult(admission.reason, dispatchStartTime, [...plan.guards, ...plan.results], validation);
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
if (timeoutScope.options?.signal?.aborted) {
|
|
2288
|
+
admissionEndedAt = Date.now();
|
|
2289
|
+
return this.createAbortedExecutionResult(dispatchStartTime, [...plan.guards, ...plan.results], validation);
|
|
2290
|
+
}
|
|
2291
|
+
admissionEndedAt = Date.now();
|
|
2292
|
+
if (timeoutScope.options?.immediate || !this.dispatchQueue) return pipelineOperation();
|
|
2293
|
+
const queued = this.dispatchQueue.enqueueWithHandle(pipelineOperation, timeoutScope.options?.queuePriority ?? 0);
|
|
2294
|
+
timeoutScope.onTimeout((error) => queued.cancel(error));
|
|
2295
|
+
return queued.promise;
|
|
2296
|
+
};
|
|
2297
|
+
let observerNotified = false;
|
|
2298
|
+
let terminalErrorReported = false;
|
|
2299
|
+
const reportTerminalError = (error) => {
|
|
2300
|
+
if (terminalErrorReported) return;
|
|
2301
|
+
terminalErrorReported = true;
|
|
2302
|
+
this.invokeErrorHandler(error, action, payload, options, attemptState.count);
|
|
2303
|
+
};
|
|
2304
|
+
const notifyObservers = async (event) => {
|
|
2305
|
+
if (observerNotified) return;
|
|
2306
|
+
observerNotified = true;
|
|
2307
|
+
await this.executeObservers(action, plan, event);
|
|
1643
2308
|
};
|
|
1644
|
-
const shouldQueue = !timeoutScope.options?.immediate && Boolean(this.dispatchQueue) && timeoutScope.options?.queuePriority !== void 0;
|
|
1645
2309
|
let dispatchPromise;
|
|
2310
|
+
let observedDispatchPromise;
|
|
1646
2311
|
this.dispatchConstructionDepth += 1;
|
|
1647
2312
|
try {
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
2313
|
+
dispatchPromise = operation();
|
|
2314
|
+
observedDispatchPromise = dispatchPromise.then(async (result) => {
|
|
2315
|
+
const dispatchEndedAt = Date.now();
|
|
2316
|
+
const pipelineDuration = pipelineStartedAt === void 0 ? 0 : result.execution.pipelineDuration;
|
|
2317
|
+
const completedResult = {
|
|
2318
|
+
...result,
|
|
2319
|
+
execution: {
|
|
2320
|
+
...result.execution,
|
|
2321
|
+
duration: dispatchEndedAt - dispatchStartTime,
|
|
2322
|
+
admissionDuration: Math.max(0, admissionEndedAt - dispatchStartTime),
|
|
2323
|
+
queueWaitDuration: pipelineStartedAt === void 0 ? 0 : Math.max(0, pipelineStartedAt - admissionEndedAt),
|
|
2324
|
+
pipelineDuration,
|
|
2325
|
+
startTime: dispatchStartTime,
|
|
2326
|
+
endTime: dispatchEndedAt
|
|
2327
|
+
}
|
|
2328
|
+
};
|
|
2329
|
+
if (completedResult.outcome === "failed") {
|
|
2330
|
+
const terminalError = completedResult.errors[completedResult.errors.length - 1]?.error ?? /* @__PURE__ */ new Error(`Action "${String(action)}" failed`);
|
|
2331
|
+
reportTerminalError(terminalError);
|
|
2332
|
+
}
|
|
2333
|
+
await notifyObservers({
|
|
2334
|
+
action: String(action),
|
|
2335
|
+
payload: observerPayload,
|
|
2336
|
+
outcome: completedResult.outcome,
|
|
2337
|
+
result: completedResult.result,
|
|
2338
|
+
errors: completedResult.errors,
|
|
2339
|
+
signal: timeoutScope.options?.signal
|
|
2340
|
+
});
|
|
2341
|
+
return completedResult;
|
|
2342
|
+
}, async (error) => {
|
|
2343
|
+
reportTerminalError(error);
|
|
2344
|
+
await notifyObservers({
|
|
2345
|
+
action: String(action),
|
|
2346
|
+
payload: observerPayload,
|
|
2347
|
+
outcome: "failed",
|
|
2348
|
+
result: void 0,
|
|
2349
|
+
errors: [{
|
|
2350
|
+
handlerId: "dispatch",
|
|
2351
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
2352
|
+
timestamp: Date.now(),
|
|
2353
|
+
severity: "blocking"
|
|
2354
|
+
}],
|
|
2355
|
+
signal: timeoutScope.options?.signal
|
|
2356
|
+
});
|
|
2357
|
+
throw error;
|
|
2358
|
+
});
|
|
2359
|
+
this.trackDispatchPromise(observedDispatchPromise);
|
|
1654
2360
|
} finally {
|
|
1655
2361
|
this.dispatchConstructionDepth -= 1;
|
|
1656
2362
|
}
|
|
1657
|
-
const observedPromise = this.raceWithTimeout(
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
2363
|
+
const observedPromise = this.raceWithTimeout(observedDispatchPromise, timeoutScope, dispatchHandlerPromises).catch(async (error) => {
|
|
2364
|
+
reportTerminalError(error);
|
|
2365
|
+
if (!observerNotified) await this.trackGlobalHandlerPromise(notifyObservers({
|
|
2366
|
+
action: String(action),
|
|
2367
|
+
payload: observerPayload,
|
|
2368
|
+
outcome: "failed",
|
|
2369
|
+
result: void 0,
|
|
2370
|
+
errors: [{
|
|
2371
|
+
handlerId: "dispatch",
|
|
2372
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
2373
|
+
timestamp: Date.now(),
|
|
2374
|
+
severity: "blocking"
|
|
2375
|
+
}],
|
|
2376
|
+
signal: timeoutScope.options?.signal
|
|
2377
|
+
}));
|
|
1665
2378
|
throw error;
|
|
1666
2379
|
});
|
|
1667
2380
|
observedPromise.catch(() => {});
|
|
1668
2381
|
return observedPromise;
|
|
1669
2382
|
}
|
|
1670
|
-
async _performDispatchWithResult(action, payload, options, validation,
|
|
2383
|
+
async _performDispatchWithResult(action, payload, options, validation, plan, executedHandlers, dispatchHandlerPromises) {
|
|
1671
2384
|
const _startTime = Date.now();
|
|
1672
2385
|
const [effectiveSignal, autoAbortController, cleanup] = this.createAbortSignal(options);
|
|
1673
2386
|
if (options?.autoAbort?.onControllerCreated && autoAbortController) options.autoAbort.onControllerCreated(autoAbortController);
|
|
1674
2387
|
if (effectiveSignal?.aborted) {
|
|
1675
2388
|
cleanup();
|
|
1676
|
-
return this.createAbortedExecutionResult(_startTime,
|
|
2389
|
+
return this.createAbortedExecutionResult(_startTime, plan.results, validation);
|
|
1677
2390
|
}
|
|
1678
|
-
const pipeline =
|
|
2391
|
+
const pipeline = plan.pipelineSnapshot;
|
|
1679
2392
|
if (!pipeline || pipeline.length === 0) {
|
|
2393
|
+
this.log(`Pipeline lookup for '${String(action)}'`, {
|
|
2394
|
+
pipelineExists: false,
|
|
2395
|
+
handlersCount: 0,
|
|
2396
|
+
allRegisteredActions: Array.from(this.pipelines.keys())
|
|
2397
|
+
});
|
|
1680
2398
|
const warningMessage = `⚠️ Action '${String(action)}' has no registered handlers. This action will be ignored.`;
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
2399
|
+
if (this.isDebugMode) {
|
|
2400
|
+
console.warn(warningMessage);
|
|
2401
|
+
console.warn("💡 Tip: Register a handler using registry.register() before dispatching this action.");
|
|
2402
|
+
console.warn("📋 Available actions:", Array.from(this.pipelines.keys()));
|
|
2403
|
+
}
|
|
2404
|
+
this.log(`No handlers found for action '${String(action)}', dispatch cancelled`, {}, "warn");
|
|
1684
2405
|
cleanup();
|
|
1685
2406
|
return {
|
|
1686
2407
|
success: true,
|
|
1687
2408
|
aborted: false,
|
|
1688
2409
|
abortReason: void 0,
|
|
1689
2410
|
terminated: false,
|
|
2411
|
+
outcome: "completed",
|
|
1690
2412
|
validation,
|
|
1691
2413
|
result: void 0,
|
|
1692
2414
|
successResults: [],
|
|
@@ -1694,6 +2416,9 @@ var ActionRegister = class {
|
|
|
1694
2416
|
failedResults: [],
|
|
1695
2417
|
execution: {
|
|
1696
2418
|
duration: 0,
|
|
2419
|
+
admissionDuration: 0,
|
|
2420
|
+
queueWaitDuration: 0,
|
|
2421
|
+
pipelineDuration: 0,
|
|
1697
2422
|
handlersExecuted: 0,
|
|
1698
2423
|
handlersSkipped: 0,
|
|
1699
2424
|
handlersFailed: 0,
|
|
@@ -1704,27 +2429,15 @@ var ActionRegister = class {
|
|
|
1704
2429
|
errors: []
|
|
1705
2430
|
};
|
|
1706
2431
|
}
|
|
1707
|
-
const filteredHandlers =
|
|
1708
|
-
const actionKey = String(action);
|
|
1709
|
-
const guardResult = skipGuards ? null : await this.applyActionGuardControlsWithResult(actionKey, filteredHandlers, options, _startTime, pipeline.length);
|
|
1710
|
-
if (effectiveSignal?.aborted) {
|
|
1711
|
-
cleanup();
|
|
1712
|
-
return this.createAbortedExecutionResult(_startTime, pipeline.length, validation);
|
|
1713
|
-
}
|
|
1714
|
-
if (guardResult) {
|
|
1715
|
-
cleanup();
|
|
1716
|
-
return {
|
|
1717
|
-
...guardResult,
|
|
1718
|
-
validation
|
|
1719
|
-
};
|
|
1720
|
-
}
|
|
1721
|
-
const currentExecutionMode = options?.executionMode || this.actionExecutionModes.get(action) || this.executionMode;
|
|
2432
|
+
const filteredHandlers = this.getAttemptHandlers(action, plan);
|
|
1722
2433
|
const context = {
|
|
1723
2434
|
action: String(action),
|
|
1724
2435
|
payload,
|
|
1725
2436
|
handlers: [...filteredHandlers],
|
|
1726
2437
|
executedHandlers: [],
|
|
2438
|
+
handlerOutcomes: [],
|
|
1727
2439
|
deferOnceCleanup: true,
|
|
2440
|
+
claimOnce: (registration) => this.claimOnceRegistration(action, registration),
|
|
1728
2441
|
signal: effectiveSignal ?? this.lifecycleController.signal,
|
|
1729
2442
|
trackHandlerPromise: (promise) => this.trackHandlerPromise(promise, dispatchHandlerPromises),
|
|
1730
2443
|
aborted: false,
|
|
@@ -1732,24 +2445,13 @@ var ActionRegister = class {
|
|
|
1732
2445
|
currentIndex: 0,
|
|
1733
2446
|
jumpToPriority: void 0,
|
|
1734
2447
|
jumpCount: 0,
|
|
1735
|
-
maxJumps:
|
|
1736
|
-
executionMode:
|
|
2448
|
+
maxJumps: this.maxJumps,
|
|
2449
|
+
executionMode: plan.executionMode,
|
|
1737
2450
|
results: [],
|
|
1738
2451
|
terminated: false,
|
|
1739
2452
|
terminationResult: void 0
|
|
1740
2453
|
};
|
|
1741
2454
|
let executionError;
|
|
1742
|
-
const handlerResults = [];
|
|
1743
|
-
filteredHandlers.forEach((handler) => {
|
|
1744
|
-
handlerResults.push({
|
|
1745
|
-
id: handler.config.id,
|
|
1746
|
-
executed: false,
|
|
1747
|
-
duration: void 0,
|
|
1748
|
-
result: void 0,
|
|
1749
|
-
error: void 0,
|
|
1750
|
-
metadata: void 0
|
|
1751
|
-
});
|
|
1752
|
-
});
|
|
1753
2455
|
const abortHandler = effectiveSignal ? () => {
|
|
1754
2456
|
context.aborted = true;
|
|
1755
2457
|
context.abortReason = typeof effectiveSignal.reason === "string" ? effectiveSignal.reason : "Action dispatch aborted by signal";
|
|
@@ -1759,13 +2461,6 @@ var ActionRegister = class {
|
|
|
1759
2461
|
try {
|
|
1760
2462
|
await this.executePipeline(context, dispatchHandlerPromises, autoAbortController, options?.autoAbort);
|
|
1761
2463
|
errors = context.collectedErrors || [];
|
|
1762
|
-
const executedCount = Math.min(context.currentIndex + (context.aborted ? 0 : 1), filteredHandlers.length);
|
|
1763
|
-
for (let i = 0; i < executedCount; i++) {
|
|
1764
|
-
const handler = filteredHandlers[i];
|
|
1765
|
-
if (!handler) continue;
|
|
1766
|
-
const handlerResult = handlerResults.find((hr) => hr.id === handler.config.id);
|
|
1767
|
-
if (handlerResult) handlerResult.executed = true;
|
|
1768
|
-
}
|
|
1769
2464
|
} catch (error) {
|
|
1770
2465
|
errors = context.collectedErrors || [];
|
|
1771
2466
|
executionError = error instanceof Error ? error : new Error(String(error));
|
|
@@ -1775,156 +2470,118 @@ var ActionRegister = class {
|
|
|
1775
2470
|
timestamp: Date.now(),
|
|
1776
2471
|
severity: "blocking"
|
|
1777
2472
|
});
|
|
1778
|
-
const executedCount = Math.min(context.currentIndex + 1, filteredHandlers.length);
|
|
1779
|
-
for (let i = 0; i < executedCount; i++) {
|
|
1780
|
-
const handler = filteredHandlers[i];
|
|
1781
|
-
if (!handler) continue;
|
|
1782
|
-
const handlerResult = handlerResults.find((hr) => hr.id === handler.config.id);
|
|
1783
|
-
if (handlerResult) handlerResult.executed = true;
|
|
1784
|
-
}
|
|
1785
2473
|
} finally {
|
|
1786
2474
|
executedHandlers.push(...context.executedHandlers ?? []);
|
|
1787
2475
|
if (effectiveSignal && abortHandler) effectiveSignal.removeEventListener("abort", abortHandler);
|
|
1788
|
-
this.cleanupSignalsAfterStartedHandlers(
|
|
2476
|
+
this.cleanupSignalsAfterStartedHandlers(() => {
|
|
2477
|
+
cleanup();
|
|
2478
|
+
options?.[ATTEMPT_SIGNAL_CLEANUP]?.();
|
|
2479
|
+
}, dispatchHandlerPromises);
|
|
1789
2480
|
}
|
|
1790
2481
|
const endTime = Date.now();
|
|
1791
|
-
const
|
|
2482
|
+
const recordedOutcomes = context.handlerOutcomes ?? [];
|
|
2483
|
+
const outcomesById = new Map(recordedOutcomes.map((outcome) => [outcome.id, outcome]));
|
|
2484
|
+
const handlerResults = filteredHandlers.map((handler) => {
|
|
2485
|
+
const outcome = outcomesById.get(handler.id);
|
|
2486
|
+
return outcome ? snapshotHandlerOutcome(outcome) : {
|
|
2487
|
+
id: handler.id,
|
|
2488
|
+
status: "skipped",
|
|
2489
|
+
executed: false,
|
|
2490
|
+
duration: 0,
|
|
2491
|
+
result: void 0,
|
|
2492
|
+
error: void 0,
|
|
2493
|
+
metadata: handler.config.metadata ? { ...handler.config.metadata } : void 0
|
|
2494
|
+
};
|
|
2495
|
+
});
|
|
2496
|
+
const handlerErrors = errors.filter((error) => error.handlerId !== "pipeline");
|
|
2497
|
+
const reportedErrors = executionError ? errors.filter((error) => error.handlerId === "pipeline") : errors;
|
|
2498
|
+
const executionHandlersCount = handlerResults.filter((handler) => handler.executed).length;
|
|
1792
2499
|
const successResults = context.results.filter((result) => result !== void 0);
|
|
1793
|
-
const failedResults =
|
|
2500
|
+
const failedResults = handlerErrors.map((err) => ({
|
|
1794
2501
|
handlerId: err.handlerId,
|
|
1795
2502
|
error: err.error,
|
|
1796
|
-
expectedType:
|
|
2503
|
+
expectedType: "unknown"
|
|
1797
2504
|
}));
|
|
1798
2505
|
return {
|
|
1799
2506
|
success: !executionError && !context.aborted,
|
|
1800
2507
|
aborted: context.aborted,
|
|
1801
2508
|
abortReason: context.abortReason,
|
|
1802
2509
|
terminated: context.terminated,
|
|
2510
|
+
outcome: context.aborted ? "cancelled" : executionError ? "failed" : handlerErrors.length > 0 ? "completed_with_errors" : "completed",
|
|
1803
2511
|
validation,
|
|
1804
|
-
result:
|
|
2512
|
+
result: context.terminated ? context.terminationResult : void 0,
|
|
1805
2513
|
successResults,
|
|
1806
2514
|
results: context.results,
|
|
1807
2515
|
failedResults,
|
|
1808
2516
|
execution: {
|
|
1809
2517
|
duration: endTime - _startTime,
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
2518
|
+
admissionDuration: 0,
|
|
2519
|
+
queueWaitDuration: 0,
|
|
2520
|
+
pipelineDuration: endTime - _startTime,
|
|
2521
|
+
handlersExecuted: executionHandlersCount,
|
|
2522
|
+
handlersSkipped: Math.max(0, filteredHandlers.length - executionHandlersCount),
|
|
2523
|
+
handlersFailed: context.executionMode === "race" ? context.raceWinnerId && outcomesById.get(context.raceWinnerId)?.status === "failed" ? 1 : 0 : handlerResults.filter((handler) => handler.status === "failed").length,
|
|
1813
2524
|
startTime: _startTime,
|
|
1814
2525
|
endTime
|
|
1815
2526
|
},
|
|
1816
2527
|
handlers: handlerResults,
|
|
1817
|
-
|
|
2528
|
+
...context.executionMode !== "race" ? {} : { raceDiagnostics: {
|
|
2529
|
+
...context.raceWinnerId === void 0 ? {} : { winnerId: context.raceWinnerId },
|
|
2530
|
+
...context.raceWinnerId === void 0 ? {} : { winner: snapshotHandlerOutcome(outcomesById.get(context.raceWinnerId)) },
|
|
2531
|
+
loserSnapshots: (context.raceLoserOutcomes ?? []).map(snapshotHandlerOutcome),
|
|
2532
|
+
pendingLosersAtReturn: (context.raceLoserOutcomes ?? []).filter((outcome) => outcome.status === "running").length,
|
|
2533
|
+
observedLoserFailures: (context.raceLoserOutcomes ?? []).filter((outcome) => outcome.status === "failed").length
|
|
2534
|
+
} },
|
|
2535
|
+
errors: reportedErrors.map((err) => ({
|
|
1818
2536
|
handlerId: err.handlerId,
|
|
1819
2537
|
error: err.error,
|
|
1820
2538
|
timestamp: err.timestamp,
|
|
1821
|
-
severity:
|
|
2539
|
+
severity: err.severity
|
|
1822
2540
|
}))
|
|
1823
2541
|
};
|
|
1824
2542
|
}
|
|
1825
|
-
/**
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
async applyActionGuardControlsWithResult(actionKey, filteredHandlers, options, startTime, pipelineLength) {
|
|
1829
|
-
let throttleMs;
|
|
1830
|
-
let debounceMs;
|
|
1831
|
-
if (options?.throttle !== void 0) throttleMs = options.throttle;
|
|
1832
|
-
else if (filteredHandlers.length > 0) {
|
|
1833
|
-
for (const handler of filteredHandlers) if (handler.config.throttle !== void 0) {
|
|
1834
|
-
throttleMs = handler.config.throttle;
|
|
1835
|
-
break;
|
|
1836
|
-
}
|
|
1837
|
-
}
|
|
1838
|
-
if (options?.debounce !== void 0) debounceMs = options.debounce;
|
|
1839
|
-
else if (filteredHandlers.length > 0) {
|
|
1840
|
-
for (const handler of filteredHandlers) if (handler.config.debounce !== void 0) {
|
|
1841
|
-
debounceMs = handler.config.debounce;
|
|
1842
|
-
break;
|
|
1843
|
-
}
|
|
1844
|
-
}
|
|
1845
|
-
if (debounceMs !== void 0) {
|
|
1846
|
-
if (!await this.actionGuard.debounce(actionKey, debounceMs)) return {
|
|
1847
|
-
success: false,
|
|
1848
|
-
aborted: true,
|
|
1849
|
-
abortReason: "Debounced execution",
|
|
1850
|
-
terminated: false,
|
|
1851
|
-
result: void 0,
|
|
1852
|
-
successResults: [],
|
|
1853
|
-
results: [],
|
|
1854
|
-
failedResults: [],
|
|
1855
|
-
execution: {
|
|
1856
|
-
duration: Date.now() - startTime,
|
|
1857
|
-
handlersExecuted: 0,
|
|
1858
|
-
handlersSkipped: pipelineLength,
|
|
1859
|
-
handlersFailed: 0,
|
|
1860
|
-
startTime,
|
|
1861
|
-
endTime: Date.now()
|
|
1862
|
-
},
|
|
1863
|
-
handlers: [],
|
|
1864
|
-
errors: []
|
|
1865
|
-
};
|
|
1866
|
-
}
|
|
1867
|
-
if (throttleMs !== void 0) {
|
|
1868
|
-
if (!this.actionGuard.throttle(actionKey, throttleMs)) return {
|
|
1869
|
-
success: false,
|
|
1870
|
-
aborted: true,
|
|
1871
|
-
abortReason: "Throttled execution",
|
|
1872
|
-
terminated: false,
|
|
1873
|
-
result: void 0,
|
|
1874
|
-
successResults: [],
|
|
1875
|
-
results: [],
|
|
1876
|
-
failedResults: [],
|
|
1877
|
-
execution: {
|
|
1878
|
-
duration: Date.now() - startTime,
|
|
1879
|
-
handlersExecuted: 0,
|
|
1880
|
-
handlersSkipped: pipelineLength,
|
|
1881
|
-
handlersFailed: 0,
|
|
1882
|
-
startTime,
|
|
1883
|
-
endTime: Date.now()
|
|
1884
|
-
},
|
|
1885
|
-
handlers: [],
|
|
1886
|
-
errors: []
|
|
1887
|
-
};
|
|
1888
|
-
}
|
|
1889
|
-
return null;
|
|
1890
|
-
}
|
|
1891
|
-
/**
|
|
1892
|
-
* 🔧 Create or reuse PipelineController from pool for better performance
|
|
1893
|
-
*/
|
|
1894
|
-
getControllerFromPool(context, autoAbortController, autoAbortOptions) {
|
|
1895
|
-
let controller = this.controllerPool.pop();
|
|
1896
|
-
if (!controller) controller = {};
|
|
2543
|
+
/** Create a pipeline controller for one handler execution. */
|
|
2544
|
+
createController(context, autoAbortController, autoAbortOptions, isolatedState, collectResults = true) {
|
|
2545
|
+
const controller = {};
|
|
1897
2546
|
controller.signal = context.signal ?? this.lifecycleController.signal;
|
|
2547
|
+
const state = isolatedState ?? context;
|
|
1898
2548
|
controller.abort = (reason) => {
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
2549
|
+
state.aborted = true;
|
|
2550
|
+
state.abortReason = reason;
|
|
2551
|
+
const propagateAbort = !isolatedState || context.executionMode !== "race";
|
|
2552
|
+
if (propagateAbort) {
|
|
2553
|
+
context.aborted = true;
|
|
2554
|
+
context.abortReason = reason;
|
|
2555
|
+
}
|
|
2556
|
+
if (propagateAbort && autoAbortController && autoAbortOptions?.allowHandlerAbort) autoAbortController.abort(reason);
|
|
1902
2557
|
};
|
|
1903
2558
|
controller.modifyPayload = (modifier) => {
|
|
1904
|
-
|
|
1905
|
-
context.payload = modifier(context.payload);
|
|
1906
|
-
} catch (modificationError) {
|
|
1907
|
-
this.log("Payload modification error", modificationError, "warn");
|
|
1908
|
-
}
|
|
2559
|
+
state.payload = modifier(state.payload);
|
|
1909
2560
|
};
|
|
1910
|
-
controller.getPayload = () =>
|
|
2561
|
+
controller.getPayload = () => state.payload;
|
|
1911
2562
|
controller.jumpToPriority = (priority) => {
|
|
1912
|
-
|
|
2563
|
+
state.jumpToPriority = priority;
|
|
1913
2564
|
};
|
|
1914
2565
|
controller.return = (result) => {
|
|
1915
|
-
|
|
1916
|
-
|
|
2566
|
+
state.terminated = true;
|
|
2567
|
+
state.terminationResult = collectResults ? result : void 0;
|
|
2568
|
+
if (!isolatedState) {
|
|
2569
|
+
context.terminated = true;
|
|
2570
|
+
context.terminationResult = collectResults ? result : void 0;
|
|
2571
|
+
}
|
|
2572
|
+
return result;
|
|
1917
2573
|
};
|
|
1918
2574
|
controller.setResult = (result) => {
|
|
1919
|
-
|
|
2575
|
+
if (collectResults) state.results.push(result);
|
|
1920
2576
|
};
|
|
1921
2577
|
controller.getResults = () => {
|
|
1922
|
-
return [...
|
|
2578
|
+
return [...state.results];
|
|
1923
2579
|
};
|
|
1924
2580
|
controller.mergeResult = (merger) => {
|
|
1925
|
-
|
|
1926
|
-
const
|
|
1927
|
-
|
|
2581
|
+
if (!collectResults) return;
|
|
2582
|
+
const currentResult = state.results[state.results.length - 1];
|
|
2583
|
+
const mergedResult = merger(state.results.slice(0, -1), currentResult);
|
|
2584
|
+
state.results[state.results.length - 1] = mergedResult;
|
|
1928
2585
|
};
|
|
1929
2586
|
return controller;
|
|
1930
2587
|
}
|
|
@@ -1941,17 +2598,32 @@ var ActionRegister = class {
|
|
|
1941
2598
|
if (filterOptions.priority.min !== void 0 && priority < filterOptions.priority.min) return false;
|
|
1942
2599
|
if (filterOptions.priority.max !== void 0 && priority > filterOptions.priority.max) return false;
|
|
1943
2600
|
}
|
|
1944
|
-
if (filterOptions.custom
|
|
2601
|
+
if (filterOptions.custom) {
|
|
2602
|
+
const configSnapshot = Object.freeze({
|
|
2603
|
+
...config,
|
|
2604
|
+
metadata: config.metadata ? Object.freeze({ ...config.metadata }) : void 0
|
|
2605
|
+
});
|
|
2606
|
+
if (!filterOptions.custom(configSnapshot)) return false;
|
|
2607
|
+
}
|
|
1945
2608
|
return true;
|
|
1946
2609
|
});
|
|
1947
2610
|
}
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
if (
|
|
2611
|
+
validateResultOptions(resultOptions) {
|
|
2612
|
+
if (!resultOptions) return;
|
|
2613
|
+
if (resultOptions.strategy === "custom" && typeof resultOptions.merger !== "function") throw new ActionResultProcessingError("Custom result strategy requires a merger function");
|
|
2614
|
+
if (resultOptions.maxResults !== void 0 && (!Number.isSafeInteger(resultOptions.maxResults) || resultOptions.maxResults < 0)) throw new RangeError("maxResults must be a non-negative safe integer.");
|
|
2615
|
+
}
|
|
2616
|
+
processResults(results, terminated, terminationResult, resultOptions) {
|
|
2617
|
+
if (terminated && terminationResult !== void 0) return terminationResult;
|
|
1951
2618
|
if (!resultOptions) return results.length > 0 ? results[results.length - 1] : void 0;
|
|
1952
2619
|
if (!resultOptions.collect && !resultOptions.strategy) return;
|
|
1953
|
-
const
|
|
1954
|
-
|
|
2620
|
+
const collectedResults = results.filter((result) => result !== void 0);
|
|
2621
|
+
const limitedResults = resultOptions.maxResults !== void 0 ? collectedResults.slice(0, resultOptions.maxResults) : collectedResults;
|
|
2622
|
+
if (limitedResults.length === 0) {
|
|
2623
|
+
if (resultOptions.strategy === "all" || resultOptions.collect && !resultOptions.strategy) return [];
|
|
2624
|
+
if (resultOptions.strategy === "custom" || resultOptions.strategy === "merge" && resultOptions.merger) return resultOptions.merger(limitedResults);
|
|
2625
|
+
return;
|
|
2626
|
+
}
|
|
1955
2627
|
switch (resultOptions.strategy) {
|
|
1956
2628
|
case "first": return limitedResults[0];
|
|
1957
2629
|
case "last": return limitedResults[limitedResults.length - 1];
|
|
@@ -1968,9 +2640,36 @@ var ActionRegister = class {
|
|
|
1968
2640
|
}
|
|
1969
2641
|
}
|
|
1970
2642
|
async executePipeline(context, dispatchHandlerPromises, autoAbortController, autoAbortOptions) {
|
|
1971
|
-
const createController = (
|
|
1972
|
-
|
|
2643
|
+
const createController = (registration, _index, state) => {
|
|
2644
|
+
const controller = this.createController(context, autoAbortController, autoAbortOptions, state, registration.role !== "guard");
|
|
2645
|
+
if (registration.role === "guard") return {
|
|
2646
|
+
signal: controller.signal,
|
|
2647
|
+
getPayload: controller.getPayload,
|
|
2648
|
+
modifyPayload: controller.modifyPayload,
|
|
2649
|
+
abort: controller.abort
|
|
2650
|
+
};
|
|
2651
|
+
if (registration.role === "result") return {
|
|
2652
|
+
signal: controller.signal,
|
|
2653
|
+
getPayload: controller.getPayload,
|
|
2654
|
+
abort: controller.abort,
|
|
2655
|
+
return: controller.return,
|
|
2656
|
+
setResult: controller.setResult,
|
|
2657
|
+
getResults: controller.getResults,
|
|
2658
|
+
mergeResult: controller.mergeResult
|
|
2659
|
+
};
|
|
2660
|
+
return controller;
|
|
1973
2661
|
};
|
|
2662
|
+
const originalHandlers = context.handlers;
|
|
2663
|
+
const preflightHandlers = originalHandlers.filter((handler) => handler.role === "guard");
|
|
2664
|
+
if (preflightHandlers.length > 0) {
|
|
2665
|
+
context.handlers = preflightHandlers;
|
|
2666
|
+
await executeSequential(context, createController);
|
|
2667
|
+
if (context.aborted || context.terminated) {
|
|
2668
|
+
context.handlers = originalHandlers;
|
|
2669
|
+
return;
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
context.handlers = originalHandlers.filter((handler) => !preflightHandlers.includes(handler) && handler.role !== "observer");
|
|
1974
2673
|
switch (context.executionMode) {
|
|
1975
2674
|
case "sequential":
|
|
1976
2675
|
await executeSequential(context, createController);
|
|
@@ -1983,6 +2682,7 @@ var ActionRegister = class {
|
|
|
1983
2682
|
break;
|
|
1984
2683
|
default: throw new Error(`Unknown execution mode: ${context.executionMode}`);
|
|
1985
2684
|
}
|
|
2685
|
+
context.handlers = originalHandlers;
|
|
1986
2686
|
if (!context.deferOnceCleanup) this.cleanupOneTimeHandlers(context.action, context.executedHandlers ?? [], dispatchHandlerPromises);
|
|
1987
2687
|
}
|
|
1988
2688
|
cleanupOneTimeHandlers(action, executedHandlers, dispatchHandlerPromises) {
|
|
@@ -1991,7 +2691,9 @@ var ActionRegister = class {
|
|
|
1991
2691
|
const handlersStillRunning = [...dispatchHandlerPromises];
|
|
1992
2692
|
const shouldDeferCleanup = handlersStillRunning.length > 0;
|
|
1993
2693
|
oneTimeHandlers.forEach((registration) => {
|
|
1994
|
-
|
|
2694
|
+
const claimed = this.claimedOnceHandlers.delete(registration);
|
|
2695
|
+
if (claimed || this.removeRegistration(action, registration, !shouldDeferCleanup)) {
|
|
2696
|
+
if (claimed && !shouldDeferCleanup) this.runRegistrationCleanup(action, registration);
|
|
1995
2697
|
if (shouldDeferCleanup && typeof registration.config.cleanup === "function") {
|
|
1996
2698
|
const cleanupPromise = Promise.allSettled(handlersStillRunning).then(() => {
|
|
1997
2699
|
this.runRegistrationCleanup(action, registration);
|
|
@@ -2000,11 +2702,20 @@ var ActionRegister = class {
|
|
|
2000
2702
|
}
|
|
2001
2703
|
this.log(`One-time handler removed: ${String(action)}`, {
|
|
2002
2704
|
handlerId: registration.id,
|
|
2003
|
-
remainingHandlers: this.
|
|
2705
|
+
remainingHandlers: this.pipelines.get(action)?.length ?? 0
|
|
2004
2706
|
});
|
|
2005
2707
|
}
|
|
2006
2708
|
});
|
|
2007
2709
|
}
|
|
2710
|
+
/** Reserve a once registration before user code starts. This is synchronous,
|
|
2711
|
+
* so independent dispatches cannot both invoke the same registration. */
|
|
2712
|
+
claimOnceRegistration(action, registration) {
|
|
2713
|
+
if (!registration.config.once) return true;
|
|
2714
|
+
if (this.claimedOnceHandlers.has(registration)) return false;
|
|
2715
|
+
if (!this.removeRegistration(action, registration, false)) return false;
|
|
2716
|
+
this.claimedOnceHandlers.add(registration);
|
|
2717
|
+
return true;
|
|
2718
|
+
}
|
|
2008
2719
|
/**
|
|
2009
2720
|
* Get the number of registered handlers for an action
|
|
2010
2721
|
*
|
|
@@ -2077,8 +2788,10 @@ var ActionRegister = class {
|
|
|
2077
2788
|
});
|
|
2078
2789
|
this.pipelines.clear();
|
|
2079
2790
|
this.lastRegisteredTimestamps.clear();
|
|
2791
|
+
this.unregisterFunctions.forEach((unregisters) => unregisters.clear());
|
|
2080
2792
|
this.unregisterFunctions.clear();
|
|
2081
2793
|
this.actionGuard.clearAll();
|
|
2794
|
+
this.observerHandlers.clear();
|
|
2082
2795
|
}
|
|
2083
2796
|
/**
|
|
2084
2797
|
* Get the name of this action register
|
|
@@ -2150,7 +2863,7 @@ var ActionRegister = class {
|
|
|
2150
2863
|
*/
|
|
2151
2864
|
setExecutionMode(mode) {
|
|
2152
2865
|
this.executionMode = mode;
|
|
2153
|
-
if (this.
|
|
2866
|
+
if (this.isDebugMode) console.log(`🎯 Global execution mode set to: ${mode}`);
|
|
2154
2867
|
}
|
|
2155
2868
|
/**
|
|
2156
2869
|
* Set execution mode for a specific action
|
|
@@ -2160,7 +2873,7 @@ var ActionRegister = class {
|
|
|
2160
2873
|
*/
|
|
2161
2874
|
setActionExecutionMode(action, mode) {
|
|
2162
2875
|
this.actionExecutionModes.set(action, mode);
|
|
2163
|
-
if (this.
|
|
2876
|
+
if (this.isDebugMode) console.log(`🎯 Execution mode set for action '${String(action)}': ${mode}`);
|
|
2164
2877
|
}
|
|
2165
2878
|
/**
|
|
2166
2879
|
* Get execution mode for a specific action
|
|
@@ -2178,7 +2891,7 @@ var ActionRegister = class {
|
|
|
2178
2891
|
*/
|
|
2179
2892
|
removeActionExecutionMode(action) {
|
|
2180
2893
|
this.actionExecutionModes.delete(action);
|
|
2181
|
-
if (this.
|
|
2894
|
+
if (this.isDebugMode) console.log(`🎯 Execution mode reset for action '${String(action)}' to default: ${this.executionMode}`);
|
|
2182
2895
|
}
|
|
2183
2896
|
/**
|
|
2184
2897
|
* Get registry configuration (for debugging and inspection)
|
|
@@ -2209,11 +2922,19 @@ var ActionRegister = class {
|
|
|
2209
2922
|
return () => {
|
|
2210
2923
|
if (this.removeRegistration(action, registration)) this.log(`Handler unregistered: ${String(action)}`, {
|
|
2211
2924
|
handlerId,
|
|
2212
|
-
remainingHandlers: this.
|
|
2925
|
+
remainingHandlers: this.pipelines.get(action)?.length ?? 0,
|
|
2213
2926
|
actionRemoved: !this.pipelines.has(action)
|
|
2214
2927
|
});
|
|
2215
2928
|
};
|
|
2216
2929
|
}
|
|
2930
|
+
getUnregisterFunctions(action) {
|
|
2931
|
+
let unregisters = this.unregisterFunctions.get(action);
|
|
2932
|
+
if (!unregisters) {
|
|
2933
|
+
unregisters = /* @__PURE__ */ new Map();
|
|
2934
|
+
this.unregisterFunctions.set(action, unregisters);
|
|
2935
|
+
}
|
|
2936
|
+
return unregisters;
|
|
2937
|
+
}
|
|
2217
2938
|
/** Remove a registration and release every resource owned by it exactly once. */
|
|
2218
2939
|
removeRegistration(action, registration, runCleanup = true) {
|
|
2219
2940
|
const pipeline = this.pipelines.get(action);
|
|
@@ -2221,11 +2942,13 @@ var ActionRegister = class {
|
|
|
2221
2942
|
const index = pipeline.indexOf(registration);
|
|
2222
2943
|
if (index === -1) return false;
|
|
2223
2944
|
pipeline.splice(index, 1);
|
|
2224
|
-
this.
|
|
2945
|
+
this.observerHandlers.delete(registration);
|
|
2946
|
+
this.unregisterFunctions.get(action)?.delete(registration.id);
|
|
2225
2947
|
if (runCleanup) this.runRegistrationCleanup(action, registration);
|
|
2226
2948
|
if (pipeline.length === 0) {
|
|
2227
2949
|
this.pipelines.delete(action);
|
|
2228
2950
|
this.lastRegisteredTimestamps.delete(action);
|
|
2951
|
+
this.unregisterFunctions.delete(action);
|
|
2229
2952
|
}
|
|
2230
2953
|
return true;
|
|
2231
2954
|
}
|
|
@@ -2244,7 +2967,11 @@ var ActionRegister = class {
|
|
|
2244
2967
|
* @public
|
|
2245
2968
|
*/
|
|
2246
2969
|
getUnregisterFunctionCount() {
|
|
2247
|
-
|
|
2970
|
+
let count = 0;
|
|
2971
|
+
this.unregisterFunctions.forEach((unregisters) => {
|
|
2972
|
+
count += unregisters.size;
|
|
2973
|
+
});
|
|
2974
|
+
return count;
|
|
2248
2975
|
}
|
|
2249
2976
|
/**
|
|
2250
2977
|
* Checks if an unregister function exists for the given handler ID
|
|
@@ -2254,7 +2981,8 @@ var ActionRegister = class {
|
|
|
2254
2981
|
* @public
|
|
2255
2982
|
*/
|
|
2256
2983
|
hasUnregisterFunction(handlerId) {
|
|
2257
|
-
|
|
2984
|
+
for (const unregisters of this.unregisterFunctions.values()) if (unregisters.has(handlerId)) return true;
|
|
2985
|
+
return false;
|
|
2258
2986
|
}
|
|
2259
2987
|
/** Reject queued dispatches without releasing registered handlers. */
|
|
2260
2988
|
cancelPendingDispatches() {
|
|
@@ -2299,7 +3027,6 @@ var ActionRegister = class {
|
|
|
2299
3027
|
if (this.lifecycleState === "destroyed") return;
|
|
2300
3028
|
this.clearAll();
|
|
2301
3029
|
this.actionExecutionModes.clear();
|
|
2302
|
-
this.controllerPool.length = 0;
|
|
2303
3030
|
this.lifecycleState = "destroyed";
|
|
2304
3031
|
this.log("ActionRegister destroyed");
|
|
2305
3032
|
}
|
|
@@ -2330,5 +3057,5 @@ var ActionRegister = class {
|
|
|
2330
3057
|
};
|
|
2331
3058
|
|
|
2332
3059
|
//#endregion
|
|
2333
|
-
export { ActionGuard, ActionRegister, ActionRegisterDestroyedError, ActionTimeoutError, ActionValidationError, executeParallel, executeRace, executeSequential, isActionRegisterDestroyedError, isActionTimeoutError, isActionValidationError };
|
|
3060
|
+
export { ActionAttemptSupersededError, ActionGuard, ActionRegister, ActionRegisterDestroyedError, ActionResultProcessingError, ActionTimeoutError, ActionValidationError, executeParallel, executeRace, executeSequential, isActionRegisterDestroyedError, isActionResultProcessingError, isActionTimeoutError, isActionValidationError, resolveHandlerConfig };
|
|
2334
3061
|
//# sourceMappingURL=index.js.map
|