Skip to content

Commit f2b499b

Browse files
fix: address PR review feedback
- Document single-handler assumption and late/extra response behavior on request() - Shorten RejectAction/postUpdateQueue comment - Add dispatchDirectly() to bypass the action queue for actions that need immediate processing (e.g. progress notifications) - Use dispatchDirectly() for handler responses and nested requests - Remove client-action queue bypass from dispatch(); all actions are now enqueued to preserve sequential ordering - Extract sendResponseToClient() hook in DefaultGLSPServer
1 parent 8a298db commit f2b499b

6 files changed

Lines changed: 55 additions & 31 deletions

File tree

packages/server/src/common/actions/action-dispatcher.ts

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ export const ActionDispatcher = Symbol('ActionDispatcher');
4343
export 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
}

packages/server/src/common/features/model/model-submission-handler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ export class ModelSubmissionHandler {
180180
}
181181

182182
protected async performLiveValidation(validator: ModelValidator): Promise<void> {
183-
this.actionDispatcher.dispatch(StatusAction.create('Validate Model...'));
183+
this.actionDispatcher.dispatchDirectly(StatusAction.create('Validate Model...'));
184184
const markers = await validator.validate([this.modelState.root], MarkersReason.LIVE);
185185
return this.actionDispatcher.dispatchAll(
186186
SetMarkersAction.create(markers, { reason: MarkersReason.LIVE }),

packages/server/src/common/features/model/request-model-action-handler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,12 @@ export class RequestModelActionHandler implements ActionHandler {
8181
}
8282

8383
protected reportModelLoading(message: string): ProgressMonitor | undefined {
84-
this.actionDispatcher.dispatch(StatusAction.create(message, { severity: 'INFO' }));
84+
this.actionDispatcher.dispatchDirectly(StatusAction.create(message, { severity: 'INFO' }));
8585
return this.progressService.start(message);
8686
}
8787

8888
protected reportModelLoadingFinished(monitor?: ProgressMonitor): void {
89-
this.actionDispatcher.dispatch(StatusAction.create('', { severity: 'NONE' }));
89+
this.actionDispatcher.dispatchDirectly(StatusAction.create('', { severity: 'NONE' }));
9090
monitor?.end();
9191
}
9292

packages/server/src/common/features/progress/progress-service.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,10 @@ export class DefaultProgressService implements ProgressService {
6565

6666
start(title: string, options?: ProgressOptions): ProgressMonitor {
6767
const progressId = uuid.v4();
68-
this.actionDispatcher.dispatch(StartProgressAction.create({ progressId, title, ...options }));
68+
this.actionDispatcher.dispatchDirectly(StartProgressAction.create({ progressId, title, ...options }));
6969
return {
70-
update: updateOptions => this.actionDispatcher.dispatch(UpdateProgressAction.create(progressId, updateOptions)),
71-
end: () => this.actionDispatcher.dispatch(EndProgressAction.create(progressId))
70+
update: updateOptions => this.actionDispatcher.dispatchDirectly(UpdateProgressAction.create(progressId, updateOptions)),
71+
end: () => this.actionDispatcher.dispatchDirectly(EndProgressAction.create(progressId))
7272
};
7373
}
7474
}

packages/server/src/common/protocol/glsp-server.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ export class DefaultGLSPServer implements GLSPServer {
171171
? await clientSession.actionDispatcher.requestUntil(action, action.timeout, true)
172172
: await clientSession.actionDispatcher.request(action);
173173
if (response) {
174-
this.sendToClient({ clientId, action: response });
174+
this.sendResponseToClient(clientId, response);
175175
}
176176
} catch (error) {
177177
const detail = error instanceof GLSPServerError ? error.cause?.toString?.() : error?.toString?.();
@@ -180,10 +180,14 @@ export class DefaultGLSPServer implements GLSPServer {
180180
`Failed to handle request '${action.kind}' (${action.requestId})`,
181181
{ responseId: action.requestId, detail }
182182
);
183-
this.sendToClient({ clientId, action: reject });
183+
this.sendResponseToClient(clientId, reject);
184184
}
185185
}
186186

187+
protected sendResponseToClient(clientId: string, response: ResponseAction): void {
188+
this.sendToClient({ clientId, action: response });
189+
}
190+
187191
protected handleProcessError(message: ActionMessage, reason: any): void | PromiseLike<void> {
188192
let errorMsg = `Could not process action: '${message.action.kind}`;
189193
this.logger.error(errorMsg);

packages/server/src/common/test/mock-util.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,10 @@ export class StubActionDispatcher implements ActionDispatcher {
135135
return Promise.resolve();
136136
}
137137

138+
dispatchDirectly(action: Action): Promise<void> {
139+
return Promise.resolve();
140+
}
141+
138142
dispatchAll(...actions: MaybeArray<Action>[]): Promise<void> {
139143
return Promise.resolve();
140144
}

0 commit comments

Comments
 (0)