Skip to content

Commit 0af3f62

Browse files
refactor: decouple AsyncLocalStorage via DI
- Introduce ActionDispatchContext interface + DI token in common - Dispatcher injects the context instance directly (toDynamicValue) - Node app-module binds native AsyncLocalStorage from async_hooks - Browser app-module binds AsyncLocalStorage from als-browser polyfill - Keeps the common package runtime-neutral for browser bundling
1 parent 1986802 commit 0af3f62

8 files changed

Lines changed: 48 additions & 19 deletions

File tree

packages/server/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"@eclipse-glsp/graph": "2.7.0-next",
6262
"@eclipse-glsp/protocol": "next",
6363
"@types/uuid": "8.3.1",
64+
"als-browser": "^1.0.1",
6465
"commander": "^8.3.0",
6566
"fast-json-patch": "^3.1.0",
6667
"lodash": "4.17.21",

packages/server/src/browser/di/app-module.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/********************************************************************************
2-
* Copyright (c) 2022-2023 EclipseSource and others.
2+
* Copyright (c) 2022-2026 EclipseSource and others.
33
*
44
* This program and the accompanying materials are made available under the
55
* terms of the Eclipse Public License v. 2.0 which is available at
@@ -14,13 +14,16 @@
1414
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
1515
********************************************************************************/
1616

17+
// Side-effect import: patches Promise, timers, XHR, observers on the current realm to preserve async context across awaits.
18+
import { AsyncLocalStorage } from 'als-browser';
1719
import { ContainerModule } from 'inversify';
18-
import { InjectionContainer, LogLevel, LoggerConfigOptions, configureConsoleLogger } from '../../common/';
20+
import { ActionDispatchContext, InjectionContainer, LogLevel, LoggerConfigOptions, configureConsoleLogger } from '../../common/';
1921

2022
export function createAppModule(options: LoggerConfigOptions = {}): ContainerModule {
2123
const resolvedOptions: LoggerConfigOptions = { consoleLog: true, logLevel: LogLevel.info, ...options };
2224
return new ContainerModule((bind, unbind, isBound, rebind) => {
2325
bind(InjectionContainer).toDynamicValue(dynamicContext => dynamicContext.container);
26+
bind(ActionDispatchContext).toDynamicValue(() => new AsyncLocalStorage<boolean>());
2427
const context = { bind, unbind, isBound, rebind };
2528
configureConsoleLogger(context, resolvedOptions);
2629
});

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,10 @@ import {
2525
UpdateModelAction,
2626
flatPush
2727
} from '@eclipse-glsp/protocol';
28-
import { AsyncLocalStorage } from 'async_hooks';
2928
import { inject, injectable, postConstruct } from 'inversify';
30-
import { ClientId } from '../di/service-identifiers';
31-
import { GLSPServerError } from '../utils/glsp-server-error';
29+
import { ActionDispatchContext, ClientId } from '../di/service-identifiers';
3230
import { ActionChannel } from '../utils/action-channel';
31+
import { GLSPServerError } from '../utils/glsp-server-error';
3332
import { Logger } from '../utils/logger';
3433
import { ActionHandler } from './action-handler';
3534
import { ActionHandlerRegistry } from './action-handler-registry';
@@ -132,8 +131,10 @@ export class DefaultActionDispatcher implements ActionDispatcher, Disposable {
132131
@inject(ClientId)
133132
protected clientId: string;
134133

134+
@inject(ActionDispatchContext)
135+
protected dispatchContext: ActionDispatchContext;
136+
135137
protected channel = new ActionChannel<Action>();
136-
protected dispatchContext = new AsyncLocalStorage<boolean>();
137138

138139
protected postUpdateQueue: Action[] = [];
139140
protected readonly pendingRequests = new Map<string, Deferred<ResponseAction | undefined>>();

packages/server/src/common/di/service-identifiers.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/********************************************************************************
2-
* Copyright (c) 2022-2023 STMicroelectronics and others.
2+
* Copyright (c) 2022-2026 STMicroelectronics and others.
33
*
44
* This program and the accompanying materials are made available under the
55
* terms of the Eclipse Public License v. 2.0 which is available at
@@ -37,3 +37,14 @@ export const NavigationTargetProviders = Symbol('NavigationTargetProviders');
3737
export type ValidateLabelEditAdapterFactory = (validator: LabelEditValidator) => ValidateLabelEditAdapter;
3838

3939
export const Operations = Symbol('Operations');
40+
41+
/**
42+
* Scope marker that lets the {@link ActionDispatcher} know whether a call to `dispatch()`
43+
* originates from inside a running handler (reentrant) or from outside (external).
44+
*/
45+
export interface ActionDispatchContext {
46+
run<R>(store: boolean, callback: () => R): R;
47+
getStore(): boolean | undefined;
48+
}
49+
50+
export const ActionDispatchContext = Symbol('ActionDispatchContext');

packages/server/src/common/utils/promise-queue.spec.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/********************************************************************************
2-
* Copyright (c) 2022-2023 STMicroelectronics and others.
2+
* Copyright (c) 2022-2026 STMicroelectronics and others.
33
*
44
* This program and the accompanying materials are made available under the
55
* terms of the Eclipse Public License v. 2.0 which is available at
@@ -14,8 +14,10 @@
1414
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
1515
********************************************************************************/
1616
import { delay } from '../test/mock-util';
17-
import { PromiseQueue } from './promise-queue';
17+
1818
import { expect } from 'chai';
19+
// eslint-disable-next-line import-x/no-deprecated
20+
import { PromiseQueue } from './promise-queue';
1921

2022
// Helper types and functions that are needed for test setup
2123

@@ -74,11 +76,13 @@ function newTestPromise(resolveTime: number): TestPromise {
7476
return { state, promise };
7577
}
7678

79+
// eslint-disable-next-line @typescript-eslint/no-deprecated, import-x/no-deprecated
7780
let queue = new PromiseQueue();
7881

7982
// Test execution
8083
describe('test PromiseQueue', () => {
8184
beforeEach(() => {
85+
// eslint-disable-next-line import-x/no-deprecated, @typescript-eslint/no-deprecated
8286
queue = new PromiseQueue();
8387
});
8488
it('enqueue - one element', async () => {

packages/server/src/common/actions/action-dispatcher.spec.ts renamed to packages/server/src/node/actions/action-dispatcher.spec.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,18 @@
1414
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
1515
********************************************************************************/
1616
import { Action, Deferred, RequestAction, ResponseAction, UpdateModelAction } from '@eclipse-glsp/protocol';
17+
import { AsyncLocalStorage } from 'async_hooks';
1718
import { expect } from 'chai';
1819
import { Container, ContainerModule } from 'inversify';
1920
import * as sinon from 'sinon';
20-
import { ClientActionKinds, ClientId } from '../di/service-identifiers';
21-
import { ClientSessionManager } from '../session/client-session-manager';
22-
import * as mock from '../test/mock-util';
23-
import { Logger } from '../utils/logger';
24-
import { DefaultActionDispatcher } from './action-dispatcher';
25-
import { ActionHandler } from './action-handler';
26-
import { ActionHandlerRegistry } from './action-handler-registry';
27-
import { ClientActionForwarder } from './client-action-handler';
21+
import { DefaultActionDispatcher } from '../../common/actions/action-dispatcher';
22+
import { ActionHandler } from '../../common/actions/action-handler';
23+
import { ActionHandlerRegistry } from '../../common/actions/action-handler-registry';
24+
import { ClientActionForwarder } from '../../common/actions/client-action-handler';
25+
import { ActionDispatchContext, ClientActionKinds, ClientId } from '../../common/di/service-identifiers';
26+
import { ClientSessionManager } from '../../common/session/client-session-manager';
27+
import * as mock from '../../common/test/mock-util';
28+
import { Logger } from '../../common/utils/logger';
2829

2930
function waitSync(timeInMillis: number): void {
3031
const start = Date.now();
@@ -51,6 +52,7 @@ describe('test DefaultActionDispatcher', () => {
5152
bind(ActionHandlerRegistry).toConstantValue(actionHandlerRegistry);
5253
bind(ClientActionKinds).toConstantValue(new Set(['response', 'response1', 'response2']));
5354
bind(ClientActionForwarder).toConstantValue(clientActionForwarderStub);
55+
bind(ActionDispatchContext).toDynamicValue(() => new AsyncLocalStorage<boolean>());
5456
})
5557
);
5658
const actionDispatcher = container.resolve(DefaultActionDispatcher);

packages/server/src/node/di/app-module.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/********************************************************************************
2-
* Copyright (c) 2022-2025 STMicroelectronics and others.
2+
* Copyright (c) 2022-2026 STMicroelectronics and others.
33
*
44
* This program and the accompanying materials are made available under the
55
* terms of the Eclipse Public License v. 2.0 which is available at
@@ -14,15 +14,17 @@
1414
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
1515
********************************************************************************/
1616
import { BindingContext } from '@eclipse-glsp/protocol/lib/di';
17+
import { AsyncLocalStorage } from 'async_hooks';
1718
import { ContainerModule } from 'inversify';
1819
import * as winston from 'winston';
19-
import { InjectionContainer, LogLevel, Logger, LoggerFactory, NullLogger, getRequestParentName } from '../../common';
20+
import { ActionDispatchContext, InjectionContainer, LogLevel, Logger, LoggerFactory, NullLogger, getRequestParentName } from '../../common';
2021
import { LaunchOptions } from '../launch/cli-parser';
2122
import { WinstonLogger } from './winston-logger';
2223

2324
export function createAppModule(options: LaunchOptions): ContainerModule {
2425
return new ContainerModule((bind, unbind, isBound, rebind) => {
2526
bind(InjectionContainer).toDynamicValue(dynamicContext => dynamicContext.container);
27+
bind(ActionDispatchContext).toDynamicValue(() => new AsyncLocalStorage<boolean>());
2628
const context = { bind, unbind, isBound, rebind };
2729
configureWinstonLogger(context, options);
2830
});

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1982,6 +1982,11 @@ ajv@^6.12.4, ajv@^6.12.5:
19821982
json-schema-traverse "^0.4.1"
19831983
uri-js "^4.2.2"
19841984

1985+
als-browser@^1.0.1:
1986+
version "1.0.1"
1987+
resolved "https://registry.yarnpkg.com/als-browser/-/als-browser-1.0.1.tgz#ddd9c2ac8ad2817e7d55f0d470b76aaa70f3d521"
1988+
integrity sha512-DjavKf6zf4DFPdEmgsEM474MBjFcZG/1amv2/+WHGf61kVQWqf7XEn4jvpjFS4ssQbh/pkmYThaPfQK1ERC+3g==
1989+
19851990
ansi-colors@4.1.1:
19861991
version "4.1.1"
19871992
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"

0 commit comments

Comments
 (0)