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