@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.
Files changed (40) hide show
  1. package/lib/browser/catalog.json +6 -0
  2. package/lib/browser/messaging/ws-connection-source.js +1 -1
  3. package/lib/browser/messaging/ws-connection-source.js.map +1 -1
  4. package/lib/common/event.d.ts +16 -0
  5. package/lib/common/event.d.ts.map +1 -1
  6. package/lib/common/event.js +20 -2
  7. package/lib/common/event.js.map +1 -1
  8. package/lib/common/event.spec.js +63 -0
  9. package/lib/common/event.spec.js.map +1 -1
  10. package/lib/common/glob.d.ts +2 -0
  11. package/lib/common/glob.d.ts.map +1 -1
  12. package/lib/common/glob.js +8 -7
  13. package/lib/common/glob.js.map +1 -1
  14. package/lib/common/message-rpc/channel.d.ts +1 -1
  15. package/lib/common/message-rpc/channel.d.ts.map +1 -1
  16. package/lib/common/message-rpc/channel.js +12 -10
  17. package/lib/common/message-rpc/channel.js.map +1 -1
  18. package/lib/common/message-rpc/channel.spec.d.ts.map +1 -1
  19. package/lib/common/message-rpc/channel.spec.js +94 -0
  20. package/lib/common/message-rpc/channel.spec.js.map +1 -1
  21. package/lib/common/message-rpc/rpc-protocol.d.ts.map +1 -1
  22. package/lib/common/message-rpc/rpc-protocol.js +13 -3
  23. package/lib/common/message-rpc/rpc-protocol.js.map +1 -1
  24. package/lib/common/message-rpc/uint8-array-message-buffer.d.ts.map +1 -1
  25. package/lib/common/message-rpc/uint8-array-message-buffer.js +1 -1
  26. package/lib/common/message-rpc/uint8-array-message-buffer.js.map +1 -1
  27. package/lib/node/messaging/websocket-frontend-connection-service.d.ts +1 -0
  28. package/lib/node/messaging/websocket-frontend-connection-service.d.ts.map +1 -1
  29. package/lib/node/messaging/websocket-frontend-connection-service.js +4 -0
  30. package/lib/node/messaging/websocket-frontend-connection-service.js.map +1 -1
  31. package/package.json +4 -4
  32. package/src/browser/messaging/ws-connection-source.ts +1 -1
  33. package/src/common/event.spec.ts +80 -0
  34. package/src/common/event.ts +31 -2
  35. package/src/common/glob.ts +2 -2
  36. package/src/common/message-rpc/channel.spec.ts +116 -0
  37. package/src/common/message-rpc/channel.ts +13 -11
  38. package/src/common/message-rpc/rpc-protocol.ts +12 -3
  39. package/src/common/message-rpc/uint8-array-message-buffer.ts +1 -1
  40. 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
- protected pendingOpen: Map<string, (channel: ForwardingChannel) => void> = new Map();
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: 'Multiplexer main channel has been closed from the remote side!' });
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 side try to open a channel at the same time.
218
- const resolve = this.pendingOpen.get(id);
219
- if (resolve) {
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 resolve = this.pendingOpen.get(id);
235
- if (resolve) {
236
- // edge case: both side try to open a channel at the same time.
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
- const output = this.channel.getWriteBuffer();
182
- this.encoder.request(output, id, method, args);
183
- output.commit();
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 => {