@@ -43,6 +43,8 @@ export const ActionDispatcher = Symbol('ActionDispatcher');
4343export interface ActionDispatcher {
4444 /**
4545 * Processes the given action by dispatching it to all registered handlers.
46+ * Actions are enqueued to preserve sequential ordering. Response actions
47+ * produced by handlers are dispatched directly via {@link dispatchDirectly}.
4648 *
4749 * @param action The action that should be dispatched.
4850 * @returns A promise indicating when the action processing is complete.
@@ -58,6 +60,18 @@ export interface ActionDispatcher {
5860 dispatchAll ( actions : Action [ ] ) : Promise < void > ;
5961 dispatchAll ( ...actions : Action [ ] ) : Promise < void > ;
6062
63+ /**
64+ * Dispatches an action directly, bypassing the action queue. Use this for actions that
65+ * need to be processed immediately, e.g. progress notifications sent from inside a handler.
66+ *
67+ * Actions dispatched this way are not sequenced with other queued actions. Callers are
68+ * responsible for ensuring that concurrent execution is safe.
69+ *
70+ * @param action The action to dispatch directly.
71+ * @returns A promise indicating when the action processing is complete.
72+ */
73+ dispatchDirectly ( action : Action ) : Promise < void > ;
74+
6175 /**
6276 * Processes all given actions, by dispatching them to the corresponding handlers, after the next model update.
6377 * The given actions are queued until the next model update cycle has been completed i.e. an
@@ -74,9 +88,12 @@ export interface ActionDispatcher {
7488 * _not_ passed to the registered action handlers. Instead, it is the responsibility of the
7589 * caller of this method to handle the response properly.
7690 *
77- * If the request's `kind` is registered in `ClientActionKinds`, it is forwarded to the client
78- * via {@link ClientActionForwarder}. Otherwise it is dispatched locally through server-side
79- * handlers.
91+ * The request is dispatched directly (bypassing the queue). If its `kind` is registered in
92+ * `ClientActionKinds`, it is forwarded to the client via {@link ClientActionForwarder}.
93+ * If server-side handlers are registered, they are also executed.
94+ *
95+ * Only the first matching response resolves the request. Any additional or late responses
96+ * are dispatched as normal actions.
8097 *
8198 * The promise waits indefinitely until a response arrives or the dispatcher is disposed.
8299 * Use {@link requestUntil} if a timeout is needed.
@@ -137,20 +154,16 @@ export class DefaultActionDispatcher implements ActionDispatcher, Disposable {
137154 }
138155
139156 dispatch ( action : Action ) : Promise < void > {
140- // Fast-path: resolve pending requests before the queue to prevent deadlock
141- // when request() is awaited inside a queued handler and the response
142- // arrives via process() -> dispatch().
143157 if ( this . interceptPendingResponse ( action ) ) {
144158 return Promise . resolve ( ) ;
145159 }
146-
147- // Dont queue actions that are just delegated to the client
148- if ( this . clientActionForwarder . shouldForwardToClient ( action ) ) {
149- return this . doDispatch ( action ) ;
150- }
151160 return this . actionQueue . enqueue ( ( ) => this . doDispatch ( action ) ) ;
152161 }
153162
163+ dispatchDirectly ( action : Action ) : Promise < void > {
164+ return this . doDispatch ( action ) ;
165+ }
166+
154167 protected async doDispatch ( action : Action ) : Promise < void > {
155168 // Intercept responses to pending requests produced by local handlers
156169 // (via dispatchResponses). This enables server→server requests.
@@ -183,12 +196,16 @@ export class DefaultActionDispatcher implements ActionDispatcher, Disposable {
183196 return responseActions . map ( action => respond ( request , action ) ) ;
184197 }
185198
199+ /**
200+ * Dispatches response actions produced by handlers. Responses are dispatched directly
201+ * (bypassing the queue) but sequenced relative to each other via an internal response queue.
202+ */
186203 protected dispatchResponses ( actions : Action [ ] ) : Promise < void > {
187204 if ( actions . length === 0 ) {
188205 return Promise . resolve ( ) ;
189206 }
190207 const responseQueue = new PromiseQueue ( ) ;
191- const responses = actions . map ( action => responseQueue . enqueue ( ( ) => this . doDispatch ( action ) ) ) ;
208+ const responses = actions . map ( action => responseQueue . enqueue ( ( ) => this . dispatchDirectly ( action ) ) ) ;
192209 return Promise . all ( responses ) . then ( ( ) => Promise . resolve ( ) ) ;
193210 }
194211
@@ -254,12 +271,10 @@ export class DefaultActionDispatcher implements ActionDispatcher, Disposable {
254271 this . timeouts . set ( action . requestId , timeout ) ;
255272 }
256273
257- // When the queue is busy (request() called from inside a handler),
258- // bypass it via doDispatch() to avoid deadlock.
259- // When idle, go through dispatch() to preserve action ordering.
260- const dispatchPromise = this . actionQueue . isBusy
261- ? this . doDispatch ( action )
262- : this . dispatch ( action ) ;
274+ // Always dispatch directly to avoid deadlock when request() is called
275+ // from inside a queued handler. The response is intercepted before
276+ // normal dispatch, so queue ordering is not affected.
277+ const dispatchPromise = this . dispatchDirectly ( action ) ;
263278
264279 dispatchPromise . catch ( error => {
265280 if ( this . pendingRequests . delete ( action . requestId ) ) {
@@ -301,8 +316,8 @@ export class DefaultActionDispatcher implements ActionDispatcher, Disposable {
301316 * by {@link ClientActionForwarder}. If no stale entry exists, the `responseId` is left intact
302317 * for normal forwarding.
303318 *
304- * Called from both `dispatch()` (for responses arriving from the client) and `doDispatch()`
305- * ( for responses produced by local handlers).
319+ * Called from `dispatch()` (for responses arriving from the client, before the queue) and
320+ * from `doDispatch()` ( for responses produced by local handlers via `dispatchDirectly` ).
306321 */
307322 protected interceptPendingResponse ( action : Action ) : boolean {
308323 if ( ! ResponseAction . hasValidResponseId ( action ) ) {
@@ -317,16 +332,17 @@ export class DefaultActionDispatcher implements ActionDispatcher, Disposable {
317332 clearTimeout ( timeout ) ;
318333 this . timeouts . delete ( action . responseId ) ;
319334 }
320- // Drain post-update actions before resolving — in the intercept path
321- // there's no responses array, so dispatch them directly.
335+ // Drain post-update actions. A RejectAction won't trigger a drain,
336+ // so pending post-update actions remain queued until the next
337+ // successful model update.
322338 const postUpdateActions = this . drainPostUpdateQueue ( action ) ;
323339 if ( RejectAction . is ( action ) ) {
324340 deferred . reject ( new Error ( `${ action . message } ${ action . detail ? ': ' + action . detail : '' } ` ) ) ;
325341 } else {
326342 deferred . resolve ( action ) ;
327343 }
328344 if ( postUpdateActions . length > 0 ) {
329- this . dispatchAll ( postUpdateActions ) ;
345+ this . dispatchResponses ( postUpdateActions ) ;
330346 }
331347 return true ;
332348 }
0 commit comments