-
Notifications
You must be signed in to change notification settings - Fork 17
feat: add request/response support to server ActionDispatcher #131
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 3 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
8a298db
feat: add request/response support to server ActionDispatcher
martin-fleck-at f2b499b
fix: address PR review feedback
martin-fleck-at 0410c3e
refactor: align ActionDispatcher reentrancy with Java
martin-fleck-at 1986802
refactor: address ActionDispatcher review feedback
martin-fleck-at 0af3f62
refactor: decouple AsyncLocalStorage via DI
martin-fleck-at 62faece
Update next dependencies
tortmayr aaee3db
Fix dispatching for browser entrypoint`
tortmayr 1220160
refactor: address review feedback and rename to ActionDispatchScope
martin-fleck-at 2724171
refactor: address tortmayr review feedback (round 3)
martin-fleck-at File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
371 changes: 367 additions & 4 deletions
371
packages/server/src/common/actions/action-dispatcher.spec.ts
Large diffs are not rendered by default.
Oops, something went wrong.
274 changes: 255 additions & 19 deletions
274
packages/server/src/common/actions/action-dispatcher.ts
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 1 addition & 1 deletion
2
packages/server/src/common/features/model/request-model-action-handler.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 1 addition & 1 deletion
2
packages/server/src/common/features/progress/progress-service.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
120 changes: 120 additions & 0 deletions
120
packages/server/src/common/utils/action-channel.spec.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| /******************************************************************************** | ||
| * Copyright (c) 2026 EclipseSource and others. | ||
| * | ||
| * This program and the accompanying materials are made available under the | ||
| * terms of the Eclipse Public License v. 2.0 which is available at | ||
| * http://www.eclipse.org/legal/epl-2.0. | ||
| * | ||
| * This Source Code may also be made available under the following Secondary | ||
| * Licenses when the conditions for such availability set forth in the Eclipse | ||
| * Public License v. 2.0 are satisfied: GNU General Public License, version 2 | ||
| * with the GNU Classpath Exception which is available at | ||
| * https://www.gnu.org/software/classpath/license.html. | ||
| * | ||
| * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 | ||
| ********************************************************************************/ | ||
| import { expect } from 'chai'; | ||
| import { expectToThrowAsync } from '../test/mock-util'; | ||
| import { ActionChannel } from './action-channel'; | ||
|
|
||
| describe('ActionChannel', () => { | ||
| it('yields pushed items in FIFO order', async () => { | ||
| const channel = new ActionChannel<number>(); | ||
| const consumed: number[] = []; | ||
|
|
||
| const consumer = (async (): Promise<void> => { | ||
| for await (const entry of channel.consume()) { | ||
| consumed.push(entry.item); | ||
| entry.resolve(); | ||
| } | ||
| })(); | ||
|
|
||
| await Promise.all([channel.push(1), channel.push(2), channel.push(3)]); | ||
| channel.stop(); | ||
| await consumer; | ||
|
|
||
| expect(consumed).to.deep.equal([1, 2, 3]); | ||
| }); | ||
|
|
||
| it('resolves the push promise once the consumer resolves the entry', async () => { | ||
| const channel = new ActionChannel<string>(); | ||
| let entryResolver: (() => void) | undefined; | ||
|
|
||
| const consumer = (async (): Promise<void> => { | ||
| for await (const entry of channel.consume()) { | ||
| entryResolver = entry.resolve; | ||
| return; | ||
| } | ||
| })(); | ||
|
|
||
| const pushed = channel.push('a'); | ||
| // Give the consumer a turn to pick up the entry. | ||
| await Promise.resolve(); | ||
| await consumer; | ||
| expect(entryResolver).to.exist; | ||
| entryResolver!(); | ||
| await pushed; | ||
| }); | ||
|
|
||
| it('propagates reject() from the consumer back to the pushing caller', async () => { | ||
| const channel = new ActionChannel<number>(); | ||
|
|
||
| const consumer = (async (): Promise<void> => { | ||
| for await (const entry of channel.consume()) { | ||
| entry.reject(new Error('boom')); | ||
| return; | ||
| } | ||
| })(); | ||
|
|
||
| const pushed = channel.push(1); | ||
| await consumer; | ||
| await expectToThrowAsync(() => pushed, 'boom'); | ||
| }); | ||
|
|
||
| it('rejects push() after stop()', async () => { | ||
| const channel = new ActionChannel<number>(); | ||
| channel.stop(); | ||
| await expectToThrowAsync(() => channel.push(1), 'ActionChannel is stopped'); | ||
| }); | ||
|
|
||
| it('consumer exits after stop() and drain', async () => { | ||
| const channel = new ActionChannel<number>(); | ||
| const consumed: number[] = []; | ||
|
|
||
| const consumer = (async (): Promise<void> => { | ||
| for await (const entry of channel.consume()) { | ||
| consumed.push(entry.item); | ||
| entry.resolve(); | ||
| } | ||
| })(); | ||
|
|
||
| await channel.push(1); | ||
| await channel.push(2); | ||
| channel.stop(); | ||
| await consumer; | ||
|
|
||
| expect(consumed).to.deep.equal([1, 2]); | ||
| expect(channel.isStopped).to.be.true; | ||
| }); | ||
|
|
||
| it('rejectPending() rejects all queued push() promises without stopping', async () => { | ||
| const channel = new ActionChannel<number>(); | ||
| const pushes = [channel.push(1), channel.push(2)]; | ||
| expect(channel.size).to.equal(2); | ||
|
|
||
| channel.rejectPending(new Error('cleared')); | ||
|
|
||
| await expectToThrowAsync(() => pushes[0], 'cleared'); | ||
| await expectToThrowAsync(() => pushes[1], 'cleared'); | ||
| expect(channel.size).to.equal(0); | ||
| expect(channel.isStopped).to.be.false; | ||
| }); | ||
|
|
||
| it('size reflects the number of unconsumed entries', async () => { | ||
| const channel = new ActionChannel<number>(); | ||
| channel.push(1); | ||
| channel.push(2); | ||
| channel.push(3); | ||
| expect(channel.size).to.equal(3); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| /******************************************************************************** | ||
| * Copyright (c) 2026 EclipseSource and others. | ||
| * | ||
| * This program and the accompanying materials are made available under the | ||
| * terms of the Eclipse Public License v. 2.0 which is available at | ||
| * http://www.eclipse.org/legal/epl-2.0. | ||
| * | ||
| * This Source Code may also be made available under the following Secondary | ||
| * Licenses when the conditions for such availability set forth in the Eclipse | ||
| * Public License v. 2.0 are satisfied: GNU General Public License, version 2 | ||
| * with the GNU Classpath Exception which is available at | ||
| * https://www.gnu.org/software/classpath/license.html. | ||
| * | ||
| * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 | ||
| ********************************************************************************/ | ||
|
|
||
| /** | ||
| * An entry yielded by {@link ActionChannel.consume}. The consumer must call either | ||
| * `resolve()` or `reject(error)` exactly once after processing `item`. | ||
| */ | ||
| export interface ActionChannelEntry<T> { | ||
| item: T; | ||
| resolve: () => void; | ||
| reject: (error: unknown) => void; | ||
| } | ||
|
|
||
| /** | ||
| * Producer/consumer channel with a single async consumer loop. Mirrors the Java | ||
| * dispatcher's `BlockingQueue` + consumer thread architecture. | ||
| * | ||
| * Items pushed via {@link push} are yielded by {@link consume} in FIFO order. | ||
| * The promise returned by `push()` resolves or rejects when the consumer finishes | ||
| * processing the item (so producers can propagate errors back to callers). | ||
| */ | ||
| export class ActionChannel<T> { | ||
| protected queue: ActionChannelEntry<T>[] = []; | ||
| protected notify: (() => void) | undefined; | ||
| protected stopped = false; | ||
|
|
||
| /** | ||
| * Enqueues an item and returns a promise that settles when the consumer processes it. | ||
| * Rejects immediately if the channel has been stopped. | ||
| */ | ||
| push(item: T): Promise<void> { | ||
| if (this.stopped) { | ||
| return Promise.reject(new Error('ActionChannel is stopped')); | ||
| } | ||
| return new Promise((resolve, reject) => { | ||
| this.queue.push({ item, resolve, reject }); | ||
| this.notify?.(); | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Yields pending entries in FIFO order, blocking on a notification promise when empty. | ||
| * Exits once the channel is stopped and the queue has been drained. | ||
| */ | ||
| async *consume(): AsyncGenerator<ActionChannelEntry<T>> { | ||
| while (!this.stopped || this.queue.length > 0) { | ||
|
tortmayr marked this conversation as resolved.
Outdated
|
||
| while (this.queue.length > 0) { | ||
| yield this.queue.shift()!; | ||
| } | ||
| if (this.stopped) { | ||
| return; | ||
| } | ||
| await new Promise<void>(resolve => { | ||
| this.notify = resolve; | ||
| }); | ||
| this.notify = undefined; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Stops the channel. Further {@link push} calls reject. The consumer loop exits after | ||
| * the remaining queued entries have been yielded (or immediately if the queue is empty). | ||
| */ | ||
| stop(): void { | ||
| this.stopped = true; | ||
| this.notify?.(); | ||
| } | ||
|
|
||
| /** | ||
| * Rejects all queued entries with the given reason so producers awaiting their | ||
| * `push()` promises do not hang. Does not stop the channel. | ||
| */ | ||
| rejectPending(reason: Error = new Error('ActionChannel cleared')): void { | ||
| const pending = this.queue; | ||
| this.queue = []; | ||
| for (const entry of pending) { | ||
| entry.reject(reason); | ||
| } | ||
| } | ||
|
|
||
| get size(): number { | ||
| return this.queue.length; | ||
| } | ||
|
|
||
| get isStopped(): boolean { | ||
| return this.stopped; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.