@theia/core 1.71.0-next.36 → 1.71.0-next.41
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/lib/browser/catalog.json +6 -0
- package/lib/browser/messaging/ws-connection-source.js +1 -1
- package/lib/browser/messaging/ws-connection-source.js.map +1 -1
- package/lib/common/event.d.ts +16 -0
- package/lib/common/event.d.ts.map +1 -1
- package/lib/common/event.js +20 -2
- package/lib/common/event.js.map +1 -1
- package/lib/common/event.spec.js +63 -0
- package/lib/common/event.spec.js.map +1 -1
- package/lib/common/glob.d.ts +2 -0
- package/lib/common/glob.d.ts.map +1 -1
- package/lib/common/glob.js +8 -7
- package/lib/common/glob.js.map +1 -1
- package/lib/common/message-rpc/channel.d.ts +1 -1
- package/lib/common/message-rpc/channel.d.ts.map +1 -1
- package/lib/common/message-rpc/channel.js +12 -10
- package/lib/common/message-rpc/channel.js.map +1 -1
- package/lib/common/message-rpc/channel.spec.d.ts.map +1 -1
- package/lib/common/message-rpc/channel.spec.js +94 -0
- package/lib/common/message-rpc/channel.spec.js.map +1 -1
- package/lib/common/message-rpc/rpc-protocol.d.ts.map +1 -1
- package/lib/common/message-rpc/rpc-protocol.js +13 -3
- package/lib/common/message-rpc/rpc-protocol.js.map +1 -1
- package/lib/common/message-rpc/uint8-array-message-buffer.d.ts.map +1 -1
- package/lib/common/message-rpc/uint8-array-message-buffer.js +1 -1
- package/lib/common/message-rpc/uint8-array-message-buffer.js.map +1 -1
- package/lib/node/messaging/websocket-frontend-connection-service.d.ts +1 -0
- package/lib/node/messaging/websocket-frontend-connection-service.d.ts.map +1 -1
- package/lib/node/messaging/websocket-frontend-connection-service.js +4 -0
- package/lib/node/messaging/websocket-frontend-connection-service.js.map +1 -1
- package/package.json +4 -4
- package/src/browser/messaging/ws-connection-source.ts +1 -1
- package/src/common/event.spec.ts +80 -0
- package/src/common/event.ts +31 -2
- package/src/common/glob.ts +2 -2
- package/src/common/message-rpc/channel.spec.ts +116 -0
- package/src/common/message-rpc/channel.ts +13 -11
- package/src/common/message-rpc/rpc-protocol.ts +12 -3
- package/src/common/message-rpc/uint8-array-message-buffer.ts +1 -1
- package/src/node/messaging/websocket-frontend-connection-service.ts +5 -0
|
@@ -18,6 +18,7 @@ import { assert, expect, spy, use } from 'chai';
|
|
|
18
18
|
import * as spies from 'chai-spies';
|
|
19
19
|
import { Uint8ArrayReadBuffer, Uint8ArrayWriteBuffer } from './uint8-array-message-buffer';
|
|
20
20
|
import { ChannelMultiplexer, ForwardingChannel, MessageProvider } from './channel';
|
|
21
|
+
import { RpcProtocol } from './rpc-protocol';
|
|
21
22
|
|
|
22
23
|
use(spies);
|
|
23
24
|
|
|
@@ -84,5 +85,120 @@ describe('Message Channel', () => {
|
|
|
84
85
|
|
|
85
86
|
expect(openChannelSpy).to.be.called.exactly(4);
|
|
86
87
|
});
|
|
88
|
+
|
|
89
|
+
it('should reject pending open() promises when underlying channel closes', async () => {
|
|
90
|
+
const pipe = new ChannelPipe();
|
|
91
|
+
const leftMultiplexer = new ChannelMultiplexer(pipe.left);
|
|
92
|
+
// Don't create a right multiplexer, so no AckOpen will arrive
|
|
93
|
+
|
|
94
|
+
const openPromise = leftMultiplexer.open('test');
|
|
95
|
+
|
|
96
|
+
// Close the underlying channel
|
|
97
|
+
pipe.left.onCloseEmitter.fire({ reason: 'test close' });
|
|
98
|
+
|
|
99
|
+
// The open promise should reject, not hang forever
|
|
100
|
+
try {
|
|
101
|
+
await openPromise;
|
|
102
|
+
assert.fail('Expected open() promise to be rejected');
|
|
103
|
+
} catch (err) {
|
|
104
|
+
expect(err).to.be.instanceOf(Error);
|
|
105
|
+
expect((err as Error).message).to.contain('test close');
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should fire onClose on sub-channels when underlying channel closes', async () => {
|
|
110
|
+
const pipe = new ChannelPipe();
|
|
111
|
+
const leftMultiplexer = new ChannelMultiplexer(pipe.left);
|
|
112
|
+
const rightMultiplexer = new ChannelMultiplexer(pipe.right);
|
|
113
|
+
|
|
114
|
+
const leftChannel = await leftMultiplexer.open('test');
|
|
115
|
+
const rightChannel = rightMultiplexer.getOpenChannel('test');
|
|
116
|
+
assert.isDefined(rightChannel);
|
|
117
|
+
|
|
118
|
+
const leftCloseSpy = spy(() => { });
|
|
119
|
+
leftChannel.onClose(leftCloseSpy);
|
|
120
|
+
|
|
121
|
+
// Close the underlying channel from the remote side
|
|
122
|
+
pipe.left.onCloseEmitter.fire({ reason: 'underlying closed' });
|
|
123
|
+
|
|
124
|
+
expect(leftCloseSpy).to.have.been.called();
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('Channel close event ordering', () => {
|
|
129
|
+
it('should not deliver onClose after close() has been called', () => {
|
|
130
|
+
const channel = new ForwardingChannel('test', () => { }, () => new Uint8ArrayWriteBuffer());
|
|
131
|
+
|
|
132
|
+
const closeSpy = spy(() => { });
|
|
133
|
+
channel.onClose(closeSpy);
|
|
134
|
+
|
|
135
|
+
// Bug pattern: close() first (disposes emitters), then fire (no-op)
|
|
136
|
+
channel.close();
|
|
137
|
+
channel.onCloseEmitter.fire({ reason: 'too late' });
|
|
138
|
+
|
|
139
|
+
// The listener should not be called because close() already disposed the emitter
|
|
140
|
+
expect(closeSpy).to.not.have.been.called();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should deliver onClose when fired before close()', () => {
|
|
144
|
+
const channel = new ForwardingChannel('test', () => { }, () => new Uint8ArrayWriteBuffer());
|
|
145
|
+
|
|
146
|
+
const closeSpy = spy(() => { });
|
|
147
|
+
channel.onClose(closeSpy);
|
|
148
|
+
|
|
149
|
+
// Correct pattern: fire first, then close
|
|
150
|
+
channel.onCloseEmitter.fire({ reason: 'proper close' });
|
|
151
|
+
channel.close();
|
|
152
|
+
|
|
153
|
+
expect(closeSpy).to.have.been.called();
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('RPC protocol with write buffer overflow', () => {
|
|
158
|
+
it('should reject the promise when commit fails due to buffer overflow', async () => {
|
|
159
|
+
// Simulate a channel whose write buffer throws on commit (e.g. SocketWriteBuffer overflow)
|
|
160
|
+
const channel = new ForwardingChannel('test', () => { }, () => {
|
|
161
|
+
const buffer = new Uint8ArrayWriteBuffer();
|
|
162
|
+
buffer.onCommit(() => {
|
|
163
|
+
throw new Error('Max disconnected buffer size exceeded');
|
|
164
|
+
});
|
|
165
|
+
return buffer;
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const protocol = new RpcProtocol(channel, undefined, { mode: 'clientOnly' });
|
|
169
|
+
|
|
170
|
+
// sendRequest should return a rejected promise, not throw synchronously
|
|
171
|
+
const promise = protocol.sendRequest('testMethod', []);
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
await promise;
|
|
175
|
+
assert.fail('Expected promise to be rejected');
|
|
176
|
+
} catch (err) {
|
|
177
|
+
expect(err).to.be.instanceOf(Error);
|
|
178
|
+
expect((err as Error).message).to.contain('buffer size exceeded');
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should not leak pending requests when commit fails', async () => {
|
|
183
|
+
const channel = new ForwardingChannel('test', () => { }, () => {
|
|
184
|
+
const buffer = new Uint8ArrayWriteBuffer();
|
|
185
|
+
buffer.onCommit(() => {
|
|
186
|
+
throw new Error('Max disconnected buffer size exceeded');
|
|
187
|
+
});
|
|
188
|
+
return buffer;
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const protocol = new RpcProtocol(channel, undefined, { mode: 'clientOnly' });
|
|
192
|
+
|
|
193
|
+
// sendRequest should return a rejected promise and clean up pendingRequests
|
|
194
|
+
try {
|
|
195
|
+
await protocol.sendRequest('testMethod', []);
|
|
196
|
+
} catch {
|
|
197
|
+
// expected: the promise is rejected due to buffer overflow
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
201
|
+
expect((protocol as any).pendingRequests.size).to.equal(0);
|
|
202
|
+
});
|
|
87
203
|
});
|
|
88
204
|
});
|
|
@@ -154,7 +154,7 @@ export enum MessageTypes {
|
|
|
154
154
|
* messages and always in one go.
|
|
155
155
|
*/
|
|
156
156
|
export class ChannelMultiplexer implements Disposable {
|
|
157
|
-
|
|
157
|
+
private pendingOpen: Map<string, { resolve: (channel: ForwardingChannel) => void, reject: (err: Error) => void }> = new Map();
|
|
158
158
|
protected openChannels: Map<string, ForwardingChannel> = new Map();
|
|
159
159
|
|
|
160
160
|
protected readonly onOpenChannelEmitter = new Emitter<{ id: string, channel: Channel }>();
|
|
@@ -182,9 +182,11 @@ export class ChannelMultiplexer implements Disposable {
|
|
|
182
182
|
onUnderlyingChannelClose(event?: ChannelCloseEvent): void {
|
|
183
183
|
if (!this.toDispose.disposed) {
|
|
184
184
|
this.toDispose.push(Disposable.create(() => {
|
|
185
|
+
const reason = event?.reason ?? 'Multiplexer main channel has been closed from the remote side!';
|
|
186
|
+
this.pendingOpen.forEach(pending => pending.reject(new Error(reason)));
|
|
185
187
|
this.pendingOpen.clear();
|
|
186
188
|
this.openChannels.forEach(channel => {
|
|
187
|
-
channel.onCloseEmitter.fire(event ?? { reason
|
|
189
|
+
channel.onCloseEmitter.fire(event ?? { reason });
|
|
188
190
|
});
|
|
189
191
|
|
|
190
192
|
this.openChannels.clear();
|
|
@@ -214,13 +216,13 @@ export class ChannelMultiplexer implements Disposable {
|
|
|
214
216
|
}
|
|
215
217
|
|
|
216
218
|
protected handleAckOpen(id: string): void {
|
|
217
|
-
// edge case: both
|
|
218
|
-
const
|
|
219
|
-
if (
|
|
219
|
+
// edge case: both sides try to open a channel at the same time.
|
|
220
|
+
const pending = this.pendingOpen.get(id);
|
|
221
|
+
if (pending) {
|
|
220
222
|
const channel = this.createChannel(id);
|
|
221
223
|
this.pendingOpen.delete(id);
|
|
222
224
|
this.openChannels.set(id, channel);
|
|
223
|
-
resolve(channel);
|
|
225
|
+
pending.resolve(channel);
|
|
224
226
|
this.onOpenChannelEmitter.fire({ id, channel });
|
|
225
227
|
} else {
|
|
226
228
|
console.error(`not expecting ack-open on for ${id}`);
|
|
@@ -231,10 +233,10 @@ export class ChannelMultiplexer implements Disposable {
|
|
|
231
233
|
if (!this.openChannels.has(id)) {
|
|
232
234
|
const channel = this.createChannel(id);
|
|
233
235
|
this.openChannels.set(id, channel);
|
|
234
|
-
const
|
|
235
|
-
if (
|
|
236
|
-
// edge case: both
|
|
237
|
-
resolve(channel);
|
|
236
|
+
const pending = this.pendingOpen.get(id);
|
|
237
|
+
if (pending) {
|
|
238
|
+
// edge case: both sides try to open a channel at the same time.
|
|
239
|
+
pending.resolve(channel);
|
|
238
240
|
}
|
|
239
241
|
this.underlyingChannel.getWriteBuffer().writeUint8(MessageTypes.AckOpen).writeString(id).commit();
|
|
240
242
|
this.onOpenChannelEmitter.fire({ id, channel });
|
|
@@ -285,7 +287,7 @@ export class ChannelMultiplexer implements Disposable {
|
|
|
285
287
|
throw new Error(`Another channel with the id '${id}' is already open.`);
|
|
286
288
|
}
|
|
287
289
|
const result = new Promise<Channel>((resolve, reject) => {
|
|
288
|
-
this.pendingOpen.set(id, resolve);
|
|
290
|
+
this.pendingOpen.set(id, { resolve, reject });
|
|
289
291
|
});
|
|
290
292
|
this.underlyingChannel.getWriteBuffer().writeUint8(MessageTypes.Open).writeString(id).commit();
|
|
291
293
|
return result;
|
|
@@ -178,9 +178,18 @@ export class RpcProtocol {
|
|
|
178
178
|
const disposableWrapper = new DisposableWrapper();
|
|
179
179
|
this.pendingRequestCancellationEventListeners.set(id, disposableWrapper);
|
|
180
180
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
181
|
+
try {
|
|
182
|
+
const output = this.channel.getWriteBuffer();
|
|
183
|
+
this.encoder.request(output, id, method, args);
|
|
184
|
+
output.commit();
|
|
185
|
+
} catch (err) {
|
|
186
|
+
// The message could not be sent (e.g. write buffer overflow).
|
|
187
|
+
// Clean up the pending request and reject the promise.
|
|
188
|
+
this.pendingRequests.delete(id);
|
|
189
|
+
this.disposeCancellationEventListener(id);
|
|
190
|
+
reply.reject(err);
|
|
191
|
+
return reply.promise;
|
|
192
|
+
}
|
|
184
193
|
|
|
185
194
|
if (cancellationToken?.isCancellationRequested) {
|
|
186
195
|
this.sendCancel(id);
|
|
@@ -113,7 +113,7 @@ export class Uint8ArrayWriteBuffer implements WriteBuffer, Disposable {
|
|
|
113
113
|
return this;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
private onCommitEmitter = new Emitter<Uint8Array>();
|
|
116
|
+
private onCommitEmitter = new Emitter<Uint8Array>({ errorHandling: 'propagate' });
|
|
117
117
|
get onCommit(): Event<Uint8Array> {
|
|
118
118
|
return this.onCommitEmitter.event;
|
|
119
119
|
}
|
|
@@ -92,6 +92,7 @@ export class WebsocketFrontendConnectionService implements FrontendConnectionSer
|
|
|
92
92
|
this.closeTimeouts.delete(frontEndId);
|
|
93
93
|
|
|
94
94
|
connection.onCloseEmitter.fire({ reason });
|
|
95
|
+
connection.drainBuffer();
|
|
95
96
|
connection.close();
|
|
96
97
|
}
|
|
97
98
|
|
|
@@ -179,6 +180,10 @@ export class ReconnectableSocketChannel extends AbstractChannel {
|
|
|
179
180
|
this.socket = undefined;
|
|
180
181
|
}
|
|
181
182
|
|
|
183
|
+
drainBuffer(): void {
|
|
184
|
+
this.socketBuffer.drain();
|
|
185
|
+
}
|
|
186
|
+
|
|
182
187
|
override getWriteBuffer(): WriteBuffer {
|
|
183
188
|
const writeBuffer = new Uint8ArrayWriteBuffer();
|
|
184
189
|
writeBuffer.onCommit(data => {
|