@hydranium/client-theia 1.0.0-next.183 → 1.0.0-next.187

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 (29) hide show
  1. package/lib/browser/session-aware-connection-source.d.ts +5 -2
  2. package/lib/browser/session-aware-connection-source.d.ts.map +1 -1
  3. package/lib/browser/session-aware-connection-source.js +21 -4
  4. package/lib/browser/session-aware-connection-source.js.map +1 -1
  5. package/lib/common/connection-resilience-options.d.ts +6 -0
  6. package/lib/common/connection-resilience-options.d.ts.map +1 -1
  7. package/lib/common/framed-socket-write-buffer.d.ts +55 -3
  8. package/lib/common/framed-socket-write-buffer.d.ts.map +1 -1
  9. package/lib/common/framed-socket-write-buffer.js +111 -8
  10. package/lib/common/framed-socket-write-buffer.js.map +1 -1
  11. package/lib/common/inbound-message-sequence.d.ts +56 -0
  12. package/lib/common/inbound-message-sequence.d.ts.map +1 -0
  13. package/lib/common/inbound-message-sequence.js +98 -0
  14. package/lib/common/inbound-message-sequence.js.map +1 -0
  15. package/lib/common/index.d.ts +1 -0
  16. package/lib/common/index.d.ts.map +1 -1
  17. package/lib/common/index.js +1 -0
  18. package/lib/common/index.js.map +1 -1
  19. package/lib/node/session-bound-frontend-connection-service.d.ts +41 -0
  20. package/lib/node/session-bound-frontend-connection-service.d.ts.map +1 -1
  21. package/lib/node/session-bound-frontend-connection-service.js +98 -2
  22. package/lib/node/session-bound-frontend-connection-service.js.map +1 -1
  23. package/package.json +3 -3
  24. package/src/browser/session-aware-connection-source.ts +21 -4
  25. package/src/common/connection-resilience-options.ts +6 -0
  26. package/src/common/framed-socket-write-buffer.ts +127 -9
  27. package/src/common/inbound-message-sequence.ts +102 -0
  28. package/src/common/index.ts +1 -0
  29. package/src/node/session-bound-frontend-connection-service.ts +106 -1
@@ -6,9 +6,14 @@
6
6
  *
7
7
  * SPDX-License-Identifier: MIT
8
8
  ********************************************************************************/
9
+ import { DisposableCollection } from '@theia/core/lib/common/disposable';
10
+ import { AbstractChannel } from '@theia/core/lib/common/message-rpc/channel';
11
+ import { type WriteBuffer } from '@theia/core/lib/common/message-rpc/message-buffer';
9
12
  import { WebsocketFrontendConnectionService } from '@theia/core/lib/node/messaging/websocket-frontend-connection-service';
10
13
  import { type interfaces } from '@theia/core/shared/inversify';
11
14
  import { type ConnectionResilienceOptions } from '../common/connection-resilience-options';
15
+ import { FramedSocketWriteBuffer } from '../common/framed-socket-write-buffer';
16
+ import { InboundMessageSequence } from '../common/inbound-message-sequence';
12
17
  /**
13
18
  * Socket Theia hands the disconnect handler, read off the base signature rather
14
19
  * than imported, which spares a direct `socket.io` dependency carried purely
@@ -26,6 +31,29 @@ export type FrontendSocket = Parameters<WebsocketFrontendConnectionService['hand
26
31
  * even be using.
27
32
  */
28
33
  export type FrontendChannel = Parameters<WebsocketFrontendConnectionService['handleSocketDisconnect']>[1];
34
+ /**
35
+ * The server end of one frontend's session, carried across the sockets the
36
+ * frontend reconnects on.
37
+ *
38
+ * Theia's own channel sends straight into any socket that reports itself
39
+ * connected. A socket whose far end died unnoticed still does, so whatever is
40
+ * sent until the server notices is lost. This one numbers and keeps every
41
+ * message through {@link FramedSocketWriteBuffer}, resends what is still
42
+ * unacknowledged when the frontend reconnects, and drops what it has received
43
+ * twice. Otherwise it behaves as Theia's.
44
+ */
45
+ export declare class AcknowledgedSocketChannel extends AbstractChannel {
46
+ protected readonly socketBuffer: FramedSocketWriteBuffer;
47
+ protected socket: FrontendSocket | undefined;
48
+ protected socketDisposables: DisposableCollection;
49
+ protected readonly inbound: InboundMessageSequence;
50
+ constructor(socketBuffer: FramedSocketWriteBuffer);
51
+ /** Flushes in the same step that attaches the listeners, so nothing new can overtake the resend. */
52
+ connect(socket: FrontendSocket): void;
53
+ disconnect(): void;
54
+ drainBuffer(): void;
55
+ getWriteBuffer(): WriteBuffer;
56
+ }
29
57
  /**
30
58
  * Ignores a disconnect from a socket that no longer belongs to the frontend.
31
59
  *
@@ -54,6 +82,19 @@ export declare class SessionBoundFrontendConnectionService extends WebsocketFron
54
82
  /** When a frontend's live socket went away, so the reconnect gap can be reported. */
55
83
  protected readonly offlineSince: Map<string, number>;
56
84
  handleSocketDisconnect(socket: FrontendSocket, channel: FrontendChannel, frontEndId: string): void;
85
+ /**
86
+ * Theia's body, building an {@link AcknowledgedSocketChannel} in place of its own channel.
87
+ *
88
+ * Handed over through a cast, because Theia types the slot as its own channel class. The cast
89
+ * also hides from the compiler what Theia calls on the channel, so a release that calls a member
90
+ * this one lacks fails at runtime: re-diff with the disconnect handler.
91
+ */
92
+ protected createConnection(socket: FrontendSocket, frontEndId: string): FrontendChannel;
93
+ /**
94
+ * One buffer per session, from the container Theia resolves its own channel's buffer from.
95
+ * Reached through a structural view because Theia 1.70 does not declare `container`.
96
+ */
97
+ protected createWriteBuffer(): FramedSocketWriteBuffer;
57
98
  protected closeConnection(frontEndId: string, reason: string): void;
58
99
  /** Mirrors Theia's private `frontendConnectionTimeout`, including `Number('')` resolving to 0.
59
100
  * Private there, so the copy is forced; re-diff it with the handler above. */
@@ -1 +1 @@
1
- {"version":3,"file":"session-bound-frontend-connection-service.d.ts","sourceRoot":"","sources":["../../src/node/session-bound-frontend-connection-service.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAIlF,OAAO,EAAE,kCAAkC,EAAE,MAAM,sEAAsE,CAAC;AAC1H,OAAO,EAAc,KAAK,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAC3E,OAAO,EAAE,KAAK,2BAA2B,EAAE,MAAM,yCAAyC,CAAC;AAQ3F;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,UAAU,CAAC,kCAAkC,CAAC,wBAAwB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAEzG;;;;;;;;;GASG;AACH,MAAM,MAAM,eAAe,GAAG,UAAU,CAAC,kCAAkC,CAAC,wBAAwB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAE1G;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBACa,qCAAsC,SAAQ,kCAAkC;IAC1F,wFAAwF;IACxF,SAAS,CAAC,QAAQ,CAAC,QAAQ;kBAA+B,MAAM;iBAAW,MAAM;OAAM;IACvF,qFAAqF;IACrF,SAAS,CAAC,QAAQ,CAAC,YAAY,sBAA6B;IAEnD,sBAAsB,CAAC,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAwC1G;IAED,UAAmB,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAK3E;IAED;mFAC+E;IAC/E,SAAS,CAAC,iBAAiB,IAAI,MAAM,CAMpC;CACH;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,wBAAwB,CACrC,IAAI,EAAE,UAAU,CAAC,IAAI,EACrB,OAAO,EAAE,UAAU,CAAC,OAAO,EAC3B,MAAM,EAAE,UAAU,CAAC,MAAM,EACzB,OAAO,GAAE,2BAAgC,GACzC,OAAO,CAST"}
1
+ {"version":3,"file":"session-bound-frontend-connection-service.d.ts","sourceRoot":"","sources":["../../src/node/session-bound-frontend-connection-service.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,OAAO,EAAc,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AACrF,OAAO,EAAE,eAAe,EAAE,MAAM,4CAA4C,CAAC;AAC7E,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,mDAAmD,CAAC;AAIrF,OAAO,EAAE,kCAAkC,EAAE,MAAM,sEAAsE,CAAC;AAC1H,OAAO,EAAc,KAAK,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAC3E,OAAO,EAAE,KAAK,2BAA2B,EAAE,MAAM,yCAAyC,CAAC;AAC3F,OAAO,EAGJ,uBAAuB,EAGzB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EAAyB,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAEnG;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,UAAU,CAAC,kCAAkC,CAAC,wBAAwB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAEzG;;;;;;;;;GASG;AACH,MAAM,MAAM,eAAe,GAAG,UAAU,CAAC,kCAAkC,CAAC,wBAAwB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAE1G;;;;;;;;;;GAUG;AACH,qBAAa,yBAA0B,SAAQ,eAAe;IAK/C,SAAS,CAAC,QAAQ,CAAC,YAAY,EAAE,uBAAuB;IAJpE,SAAS,CAAC,MAAM,EAAE,cAAc,GAAG,SAAS,CAAC;IAC7C,SAAS,CAAC,iBAAiB,uBAA8B;IACzD,SAAS,CAAC,QAAQ,CAAC,OAAO,yBAA8F;IAExH,YAA+B,YAAY,EAAE,uBAAuB,EAInE;IAED,oGAAoG;IACpG,OAAO,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CA+BpC;IAED,UAAU,IAAI,IAAI,CAGjB;IAED,WAAW,IAAI,IAAI,CAElB;IAED,cAAc,IAAI,WAAW,CAI5B;CACH;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBACa,qCAAsC,SAAQ,kCAAkC;IAC1F,wFAAwF;IACxF,SAAS,CAAC,QAAQ,CAAC,QAAQ;kBAA+B,MAAM;iBAAW,MAAM;OAAM;IACvF,qFAAqF;IACrF,SAAS,CAAC,QAAQ,CAAC,YAAY,sBAA6B;IAEnD,sBAAsB,CAAC,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAwC1G;IAED;;;;;;OAMG;IACH,UAAmB,gBAAgB,CAAC,MAAM,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,GAAG,eAAe,CAK/F;IAED;;;OAGG;IACH,SAAS,CAAC,iBAAiB,IAAI,uBAAuB,CAOrD;IAED,UAAmB,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAK3E;IAED;mFAC+E;IAC/E,SAAS,CAAC,iBAAiB,IAAI,MAAM,CAMpC;CACH;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,wBAAwB,CACrC,IAAI,EAAE,UAAU,CAAC,IAAI,EACrB,OAAO,EAAE,UAAU,CAAC,OAAO,EAC3B,MAAM,EAAE,UAAU,CAAC,MAAM,EACzB,OAAO,GAAE,2BAAgC,GACzC,OAAO,CAST"}
@@ -14,13 +14,84 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
14
14
  return c > 3 && r && Object.defineProperty(target, key, r), r;
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.SessionBoundFrontendConnectionService = void 0;
17
+ exports.SessionBoundFrontendConnectionService = exports.AcknowledgedSocketChannel = void 0;
18
18
  exports.bindConnectionResilience = bindConnectionResilience;
19
+ const disposable_1 = require("@theia/core/lib/common/disposable");
20
+ const channel_1 = require("@theia/core/lib/common/message-rpc/channel");
21
+ const uint8_array_message_buffer_1 = require("@theia/core/lib/common/message-rpc/uint8-array-message-buffer");
19
22
  const backend_application_config_provider_1 = require("@theia/core/lib/node/backend-application-config-provider");
20
23
  const socket_write_buffer_1 = require("@theia/core/lib/common/messaging/socket-write-buffer");
21
24
  const websocket_frontend_connection_service_1 = require("@theia/core/lib/node/messaging/websocket-frontend-connection-service");
22
25
  const inversify_1 = require("@theia/core/shared/inversify");
23
26
  const framed_socket_write_buffer_1 = require("../common/framed-socket-write-buffer");
27
+ const inbound_message_sequence_1 = require("../common/inbound-message-sequence");
28
+ /**
29
+ * The server end of one frontend's session, carried across the sockets the
30
+ * frontend reconnects on.
31
+ *
32
+ * Theia's own channel sends straight into any socket that reports itself
33
+ * connected. A socket whose far end died unnoticed still does, so whatever is
34
+ * sent until the server notices is lost. This one numbers and keeps every
35
+ * message through {@link FramedSocketWriteBuffer}, resends what is still
36
+ * unacknowledged when the frontend reconnects, and drops what it has received
37
+ * twice. Otherwise it behaves as Theia's.
38
+ */
39
+ class AcknowledgedSocketChannel extends channel_1.AbstractChannel {
40
+ socketBuffer;
41
+ socket;
42
+ socketDisposables = new disposable_1.DisposableCollection();
43
+ inbound = new inbound_message_sequence_1.InboundMessageSequence(sequence => this.socket?.emit(inbound_message_sequence_1.ACKNOWLEDGEMENT_EVENT, sequence));
44
+ constructor(socketBuffer) {
45
+ super();
46
+ this.socketBuffer = socketBuffer;
47
+ this.toDispose.push(this.inbound);
48
+ this.toDispose.push(disposable_1.Disposable.create(() => this.socketDisposables.dispose()));
49
+ }
50
+ /** Flushes in the same step that attaches the listeners, so nothing new can overtake the resend. */
51
+ connect(socket) {
52
+ this.socketDisposables.dispose();
53
+ this.socketDisposables = new disposable_1.DisposableCollection();
54
+ this.socket = socket;
55
+ const errorHandler = (error) => this.onErrorEmitter.fire(error);
56
+ const dataListener = (data, sequence) => {
57
+ if (typeof sequence === 'number') {
58
+ this.socketBuffer.markPeerDeduplicates();
59
+ }
60
+ if (!this.inbound.accept(sequence, data.byteLength)) {
61
+ return;
62
+ }
63
+ const buffer = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
64
+ this.onMessageEmitter.fire(() => new uint8_array_message_buffer_1.Uint8ArrayReadBuffer(buffer));
65
+ };
66
+ const acknowledgementListener = (sequence) => {
67
+ if (typeof sequence === 'number') {
68
+ this.socketBuffer.acknowledge(sequence);
69
+ }
70
+ };
71
+ socket.on('error', errorHandler);
72
+ socket.on('message', dataListener);
73
+ socket.on(inbound_message_sequence_1.ACKNOWLEDGEMENT_EVENT, acknowledgementListener);
74
+ this.socketDisposables.push(disposable_1.Disposable.create(() => {
75
+ socket.off('error', errorHandler);
76
+ socket.off('message', dataListener);
77
+ socket.off(inbound_message_sequence_1.ACKNOWLEDGEMENT_EVENT, acknowledgementListener);
78
+ }));
79
+ this.socketBuffer.flush(socket);
80
+ }
81
+ disconnect() {
82
+ this.socketDisposables.dispose();
83
+ this.socket = undefined;
84
+ }
85
+ drainBuffer() {
86
+ this.socketBuffer.drain();
87
+ }
88
+ getWriteBuffer() {
89
+ const writeBuffer = new uint8_array_message_buffer_1.Uint8ArrayWriteBuffer();
90
+ writeBuffer.onCommit(data => this.socketBuffer.sendOrQueue(this.socket?.connected ? this.socket : undefined, data));
91
+ return writeBuffer;
92
+ }
93
+ }
94
+ exports.AcknowledgedSocketChannel = AcknowledgedSocketChannel;
24
95
  /**
25
96
  * Ignores a disconnect from a socket that no longer belongs to the frontend.
26
97
  *
@@ -81,6 +152,31 @@ let SessionBoundFrontendConnectionService = class SessionBoundFrontendConnection
81
152
  // timeout < 0: never close the back end.
82
153
  });
83
154
  }
155
+ /**
156
+ * Theia's body, building an {@link AcknowledgedSocketChannel} in place of its own channel.
157
+ *
158
+ * Handed over through a cast, because Theia types the slot as its own channel class. The cast
159
+ * also hides from the compiler what Theia calls on the channel, so a release that calls a member
160
+ * this one lacks fails at runtime: re-diff with the disconnect handler.
161
+ */
162
+ createConnection(socket, frontEndId) {
163
+ const channel = new AcknowledgedSocketChannel(this.createWriteBuffer());
164
+ channel.connect(socket);
165
+ this.connectionsByFrontend.set(frontEndId, channel);
166
+ return channel;
167
+ }
168
+ /**
169
+ * One buffer per session, from the container Theia resolves its own channel's buffer from.
170
+ * Reached through a structural view because Theia 1.70 does not declare `container`.
171
+ */
172
+ createWriteBuffer() {
173
+ const container = this.container;
174
+ const buffer = container.get(socket_write_buffer_1.SocketWriteBuffer);
175
+ if (!(buffer instanceof framed_socket_write_buffer_1.FramedSocketWriteBuffer)) {
176
+ throw new Error('SessionBoundFrontendConnectionService requires FramedSocketWriteBuffer to be bound');
177
+ }
178
+ return buffer;
179
+ }
84
180
  closeConnection(frontEndId, reason) {
85
181
  console.info(`${framed_socket_write_buffer_1.CONNECTION_LOG_PREFIX} closing frontend ${frontEndId}: ${reason}`);
86
182
  this.bindings.delete(frontEndId);
@@ -122,7 +218,7 @@ function bindConnectionResilience(bind, isBound, rebind, options = {}) {
122
218
  }
123
219
  bind(SessionBoundFrontendConnectionService).toSelf().inSingletonScope();
124
220
  rebind(websocket_frontend_connection_service_1.WebsocketFrontendConnectionService).toService(SessionBoundFrontendConnectionService);
125
- rebind(socket_write_buffer_1.SocketWriteBuffer).toDynamicValue(() => (0, framed_socket_write_buffer_1.createFramedSocketWriteBuffer)(options.bufferBytes));
221
+ rebind(socket_write_buffer_1.SocketWriteBuffer).toDynamicValue(() => (0, framed_socket_write_buffer_1.createFramedSocketWriteBuffer)(options));
126
222
  return true;
127
223
  }
128
224
  //# sourceMappingURL=session-bound-frontend-connection-service.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"session-bound-frontend-connection-service.js","sourceRoot":"","sources":["../../src/node/session-bound-frontend-connection-service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"session-bound-frontend-connection-service.js","sourceRoot":"","sources":["../../src/node/session-bound-frontend-connection-service.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hydranium/client-theia",
3
- "version": "1.0.0-next.183",
3
+ "version": "1.0.0-next.187",
4
4
  "description": "Cross-head Theia client primitives shared by the hydranium protocol heads. Provides the socket-forwarding connection-handler base that the data-server and GLSP Theia integrations subclass, plus the preference-driven Output-channel logger; carries no head-specific dependency.",
5
5
  "keywords": [
6
6
  "hydranium",
@@ -81,7 +81,7 @@
81
81
  "watch": "tsc -b -w --preserveWatchOutput"
82
82
  },
83
83
  "devDependencies": {
84
- "@hydranium/protocol": "1.0.0-next.183",
84
+ "@hydranium/protocol": "1.0.0-next.187",
85
85
  "@theia/core": "^1.70.0",
86
86
  "@theia/editor": "^1.70.0",
87
87
  "@theia/filesystem": "^1.70.0",
@@ -94,7 +94,7 @@
94
94
  "vscode-jsonrpc": "9.0.1"
95
95
  },
96
96
  "peerDependencies": {
97
- "@hydranium/protocol": "1.0.0-next.183",
97
+ "@hydranium/protocol": "1.0.0-next.187",
98
98
  "@theia/core": "^1.70.0",
99
99
  "@theia/editor": "^1.70.0",
100
100
  "@theia/filesystem": "^1.70.0",
@@ -23,6 +23,7 @@ import {
23
23
  supportsConnectionResilience,
24
24
  warnConnectionResilienceUnavailable
25
25
  } from '../common/framed-socket-write-buffer';
26
+ import { ACKNOWLEDGEMENT_EVENT, InboundMessageSequence } from '../common/inbound-message-sequence';
26
27
 
27
28
  /**
28
29
  * Sends a message only once the server has confirmed the session, and only
@@ -84,6 +85,11 @@ export class SessionAwareConnectionSource extends WebSocketConnectionSource {
84
85
  this.socket.on(ConnectionManagementMessages.RECONNECT, (hasConnection: boolean) => {
85
86
  this.sessionResumed = hasConnection;
86
87
  });
88
+ this.socket.on(ACKNOWLEDGEMENT_EVENT, (sequence: unknown) => {
89
+ if (typeof sequence === 'number') {
90
+ this.framedBuffer.acknowledge(sequence);
91
+ }
92
+ });
87
93
  }
88
94
 
89
95
  /** Whether a message may go out now, as opposed to waiting in the buffer. */
@@ -92,13 +98,24 @@ export class SessionAwareConnectionSource extends WebSocketConnectionSource {
92
98
  }
93
99
 
94
100
  /**
95
- * Copied from Theia apart from the `onCommit` body, because the base builds that callback inline
96
- * and exposes no narrower seam. Re-diff when the supported range moves.
101
+ * Copied from Theia apart from the duplicate check on receipt and the `onCommit` body, because
102
+ * the base builds both callbacks inline and exposes no narrower seam. Re-diff when the supported
103
+ * range moves.
104
+ *
105
+ * Called once per session, so the receive-side state starts afresh with the server's numbering.
97
106
  */
98
107
  protected override createChannel(): AbstractChannel {
99
108
  const toDispose = new DisposableCollection();
100
- const messageHandler = (data: ArrayBuffer | Uint8Array): void => {
109
+ const inbound = new InboundMessageSequence(sequence => this.socket.emit(ACKNOWLEDGEMENT_EVENT, sequence));
110
+ toDispose.push(Disposable.create(() => inbound.dispose()));
111
+ const messageHandler = (data: ArrayBuffer | Uint8Array, sequence?: unknown): void => {
101
112
  this.onIncomingMessageActivityEmitter.fire();
113
+ if (typeof sequence === 'number') {
114
+ this.framedBuffer.markPeerDeduplicates();
115
+ }
116
+ if (!inbound.accept(sequence, data.byteLength)) {
117
+ return;
118
+ }
102
119
  if (this.currentChannel) {
103
120
  // socket.io hands binary over as ArrayBuffer in the browser.
104
121
  const buffer = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
@@ -173,7 +190,7 @@ export function bindConnectionResilience(
173
190
  return false;
174
191
  }
175
192
  // Scopes are kept as Theia declares them: one buffer per connection source, one shared socket owner.
176
- rebind(SocketWriteBuffer).toDynamicValue(() => createFramedSocketWriteBuffer(options.bufferBytes));
193
+ rebind(SocketWriteBuffer).toDynamicValue(() => createFramedSocketWriteBuffer(options));
177
194
  rebind(WebSocketConnectionSource).to(SessionAwareConnectionSource).inSingletonScope();
178
195
  return true;
179
196
  }
@@ -21,4 +21,10 @@ export interface ConnectionResilienceOptions {
21
21
  * trades memory for tolerance here.
22
22
  */
23
23
  readonly bufferBytes?: number;
24
+ /**
25
+ * How much each side keeps of what it sent and the peer has not acknowledged
26
+ * yet, in bytes. Past it the copies are discarded and a dropped connection
27
+ * loses them, so it bounds what a peer that never acknowledges can cost.
28
+ */
29
+ readonly unacknowledgedBytes?: number;
24
30
  }
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { Emitter, type Event } from '@theia/core/lib/common/event';
11
11
  import { SocketWriteBuffer } from '@theia/core/lib/common/messaging/socket-write-buffer';
12
+ import { type ConnectionResilienceOptions } from './connection-resilience-options';
12
13
 
13
14
  /**
14
15
  * Prefix on every connection log line, deliberately shared by the browser and the
@@ -22,7 +23,8 @@ export const CONNECTION_LOG_PREFIX = '[connection]';
22
23
  * without a connection.
23
24
  */
24
25
  export interface MessageSink {
25
- send(data: Uint8Array): void;
26
+ /** `sequence` travels as a second socket.io argument, which a reader that does not expect it ignores. */
27
+ send(data: Uint8Array, sequence: number): void;
26
28
  }
27
29
 
28
30
  /**
@@ -58,6 +60,11 @@ export interface ConnectionBufferOverflow {
58
60
  * dropping something silently. The limit is settable, since the right value
59
61
  * depends on how long an outage has to survive.
60
62
  *
63
+ * Added here: every message sent is numbered and kept until the peer
64
+ * acknowledges it, and {@link flush} resends what is still unacknowledged ahead
65
+ * of the backlog. A socket whose far end died unnoticed still accepts writes, so
66
+ * a message is not delivered just because it was sent.
67
+ *
61
68
  * Logging goes to `console` rather than through a `Logger`. On the browser side
62
69
  * Theia's preloader builds this before any Output channel exists, so there is
63
70
  * nothing else to write to; the {@link CONNECTION_LOG_PREFIX} keeps the lines
@@ -78,11 +85,33 @@ export class FramedSocketWriteBuffer extends SocketWriteBuffer {
78
85
  */
79
86
  maxBytes = FramedSocketWriteBuffer.DEFAULT_MAX_BYTES;
80
87
 
88
+ /** Kept copies past this are discarded, so a peer that never acknowledges cannot grow them without bound. */
89
+ static readonly DEFAULT_MAX_UNACKNOWLEDGED_BYTES = 1024 * 1024;
90
+
91
+ /**
92
+ * Separate from {@link maxBytes}, which counts only what is produced while
93
+ * the peer is away: copies kept on a healthy connection must not use up the
94
+ * room an outage needs.
95
+ */
96
+ maxUnacknowledgedBytes = FramedSocketWriteBuffer.DEFAULT_MAX_UNACKNOWLEDGED_BYTES;
97
+
81
98
  protected pending: Uint8Array[] = [];
82
99
  protected pendingBytes = 0;
83
100
  protected overflowReported = false;
84
101
  protected readonly onOverflowEmitter = new Emitter<ConnectionBufferOverflow>();
85
102
 
103
+ protected nextSequence = 1;
104
+ /** Oldest first: {@link acknowledge} stops at the first entry the peer has not accepted. */
105
+ protected unacknowledged: { readonly sequence: number; readonly data: Uint8Array }[] = [];
106
+ protected unacknowledgedBytes = 0;
107
+ protected unacknowledgedOverflowReported = false;
108
+ /**
109
+ * Whether the peer drops a message it already has. One without the hardening
110
+ * applies a resent message a second time, so nothing is resent until the
111
+ * peer has shown otherwise.
112
+ */
113
+ protected peerDeduplicates = false;
114
+
86
115
  /** Fires once per outage when the limit is reached. */
87
116
  get onOverflow(): Event<ConnectionBufferOverflow> {
88
117
  return this.onOverflowEmitter.event;
@@ -98,7 +127,7 @@ export class FramedSocketWriteBuffer extends SocketWriteBuffer {
98
127
  */
99
128
  sendOrQueue(socket: MessageSink | undefined, data: Uint8Array): void {
100
129
  if (socket && !this.hasBacklog) {
101
- socket.send(data);
130
+ this.send(socket, data);
102
131
  } else {
103
132
  this.buffer(data);
104
133
  }
@@ -117,7 +146,80 @@ export class FramedSocketWriteBuffer extends SocketWriteBuffer {
117
146
  }
118
147
  }
119
148
 
149
+ /**
150
+ * Kept only once `send` returns: a message whose send threw stays in the
151
+ * backlog, and keeping it as well would send it again under a second number,
152
+ * which the receiver cannot recognise as a duplicate.
153
+ */
154
+ protected send(socket: MessageSink, data: Uint8Array): void {
155
+ const sequence = this.nextSequence++;
156
+ socket.send(data, sequence);
157
+ this.keep(sequence, data);
158
+ }
159
+
160
+ protected keep(sequence: number, data: Uint8Array): void {
161
+ if (this.unacknowledgedBytes + data.byteLength > this.maxUnacknowledgedBytes) {
162
+ // Said only to a peer known to acknowledge: one without the hardening never does, and
163
+ // nothing would be resent to it anyway.
164
+ if (this.peerDeduplicates && !this.unacknowledgedOverflowReported) {
165
+ this.unacknowledgedOverflowReported = true;
166
+ console.warn(
167
+ `${CONNECTION_LOG_PREFIX} the peer has not acknowledged ${this.unacknowledged.length} message(s), ` +
168
+ `${this.unacknowledgedBytes} bytes; discarding their copies, so a connection dropped now loses them`
169
+ );
170
+ }
171
+ this.unacknowledged = [];
172
+ this.unacknowledgedBytes = 0;
173
+ return;
174
+ }
175
+ // Not copied: a committed write buffer hands over a fresh array and is disposed.
176
+ this.unacknowledged.push({ sequence, data });
177
+ this.unacknowledgedBytes += data.byteLength;
178
+ }
179
+
180
+ /**
181
+ * Records that the peer numbers its messages, which only a peer that also
182
+ * drops duplicates does, so resending to it is safe.
183
+ */
184
+ markPeerDeduplicates(): void {
185
+ this.peerDeduplicates = true;
186
+ }
187
+
188
+ /** Releases every kept message numbered up to `sequence`, which the peer has accepted. */
189
+ acknowledge(sequence: number): void {
190
+ this.peerDeduplicates = true;
191
+ let index = 0;
192
+ while (index < this.unacknowledged.length && this.unacknowledged[index].sequence <= sequence) {
193
+ this.unacknowledgedBytes -= this.unacknowledged[index].data.byteLength;
194
+ index++;
195
+ }
196
+ if (index > 0) {
197
+ this.unacknowledged = this.unacknowledged.slice(index);
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Resends what the peer has not acknowledged, under the same numbers, then
203
+ * sends the backlog. Some of the resent messages may have arrived already;
204
+ * the receiver recognises them by number and drops them.
205
+ *
206
+ * Resends only to a peer known to drop duplicates, and otherwise discards the
207
+ * copies, leaving any message lost with the old socket lost.
208
+ */
120
209
  override flush(socket: MessageSink): void {
210
+ if (this.peerDeduplicates) {
211
+ for (const { sequence, data } of this.unacknowledged) {
212
+ socket.send(data, sequence);
213
+ }
214
+ if (this.unacknowledged.length > 0) {
215
+ console.info(
216
+ `${CONNECTION_LOG_PREFIX} resent ${this.unacknowledged.length} unacknowledged message(s), ${this.unacknowledgedBytes} bytes`
217
+ );
218
+ }
219
+ } else {
220
+ this.unacknowledged = [];
221
+ this.unacknowledgedBytes = 0;
222
+ }
121
223
  if (this.pending.length === 0) {
122
224
  return;
123
225
  }
@@ -131,7 +233,7 @@ export class FramedSocketWriteBuffer extends SocketWriteBuffer {
131
233
  try {
132
234
  while (index < this.pending.length) {
133
235
  const message = this.pending[index];
134
- socket.send(message);
236
+ this.send(socket, message);
135
237
  index++;
136
238
  this.pendingBytes -= message.byteLength;
137
239
  }
@@ -153,8 +255,11 @@ export class FramedSocketWriteBuffer extends SocketWriteBuffer {
153
255
  }
154
256
 
155
257
  override drain(): void {
156
- if (this.pending.length > 0) {
157
- console.warn(`${CONNECTION_LOG_PREFIX} discarded ${this.pending.length} buffered message(s), ${this.pendingBytes} bytes`);
258
+ if (this.pending.length > 0 || this.unacknowledged.length > 0) {
259
+ console.warn(
260
+ `${CONNECTION_LOG_PREFIX} discarded ${this.pending.length} buffered message(s), ${this.pendingBytes} bytes, ` +
261
+ `and ${this.unacknowledged.length} sent but unacknowledged, ${this.unacknowledgedBytes} bytes`
262
+ );
158
263
  }
159
264
  this.reset();
160
265
  }
@@ -177,10 +282,16 @@ export class FramedSocketWriteBuffer extends SocketWriteBuffer {
177
282
  this.onOverflowEmitter.fire(overflow);
178
283
  }
179
284
 
285
+ /** Also discards the kept copies: a drain ends the session, and resending them would replay it into the next one. */
180
286
  protected reset(): void {
181
287
  this.pending = [];
182
288
  this.pendingBytes = 0;
183
289
  this.overflowReported = false;
290
+ this.nextSequence = 1;
291
+ this.unacknowledged = [];
292
+ this.unacknowledgedBytes = 0;
293
+ this.unacknowledgedOverflowReported = false;
294
+ this.peerDeduplicates = false;
184
295
  }
185
296
  }
186
297
 
@@ -217,11 +328,18 @@ export function warnConnectionResilienceUnavailable(tier: 'frontend' | 'backend'
217
328
  );
218
329
  }
219
330
 
220
- /** Builds a buffer with the given limit, or Theia's default when none is configured. */
221
- export function createFramedSocketWriteBuffer(maxBytes?: number): FramedSocketWriteBuffer {
331
+ /** Builds a buffer with the configured limits, or the defaults for any not configured. */
332
+ export function createFramedSocketWriteBuffer(options: ConnectionResilienceOptions = {}): FramedSocketWriteBuffer {
222
333
  const buffer = new FramedSocketWriteBuffer();
223
- if (typeof maxBytes === 'number' && maxBytes > 0) {
224
- buffer.maxBytes = maxBytes;
334
+ if (isPositive(options.bufferBytes)) {
335
+ buffer.maxBytes = options.bufferBytes;
336
+ }
337
+ if (isPositive(options.unacknowledgedBytes)) {
338
+ buffer.maxUnacknowledgedBytes = options.unacknowledgedBytes;
225
339
  }
226
340
  return buffer;
227
341
  }
342
+
343
+ function isPositive(value: number | undefined): value is number {
344
+ return typeof value === 'number' && value > 0;
345
+ }
@@ -0,0 +1,102 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the MIT License which is available in the project root.
6
+ *
7
+ * SPDX-License-Identifier: MIT
8
+ ********************************************************************************/
9
+
10
+ import { CONNECTION_LOG_PREFIX } from './framed-socket-write-buffer';
11
+
12
+ /** The socket.io event that carries the highest message number a side has accepted. */
13
+ export const ACKNOWLEDGEMENT_EVENT = 'hydranium:ack';
14
+
15
+ /**
16
+ * What one side has received on a session: drops a message it already accepted
17
+ * and acknowledges the rest.
18
+ *
19
+ * The peer resends everything unacknowledged when a session resumes, because it
20
+ * cannot tell which of its messages died with the old socket, so every resumed
21
+ * session can repeat messages already delivered. Delivering one twice corrupts
22
+ * anything applied by position, just as losing one does.
23
+ *
24
+ * Acknowledges once enough has arrived, and otherwise after a short delay. A
25
+ * timer alone is not enough: a background tab throttles timers, and the peer
26
+ * holds every unacknowledged message until the acknowledgement arrives. The
27
+ * first message of a session is acknowledged at once, because the peer resends
28
+ * nothing until an acknowledgement shows it may.
29
+ *
30
+ * Belongs to one session. A new session numbers from the start again, so it
31
+ * needs a new instance.
32
+ */
33
+ export class InboundMessageSequence {
34
+ static readonly ACKNOWLEDGE_AFTER_MESSAGES = 64;
35
+ static readonly ACKNOWLEDGE_AFTER_BYTES = 32 * 1024;
36
+ static readonly ACKNOWLEDGE_DELAY_MS = 100;
37
+
38
+ protected lastAccepted = 0;
39
+ protected messagesSinceAcknowledgement = 0;
40
+ protected bytesSinceAcknowledgement = 0;
41
+ protected acknowledgementTimer: ReturnType<typeof setTimeout> | undefined;
42
+
43
+ constructor(protected readonly sendAcknowledgement: (sequence: number) => void) {}
44
+
45
+ /**
46
+ * Whether to deliver a message that arrived with `sequence`.
47
+ *
48
+ * A message without a number comes from a peer that does not number them,
49
+ * and is delivered as it is. A duplicate is still acknowledged: the peer
50
+ * resent it because an earlier acknowledgement was lost, and keeps it until
51
+ * one arrives.
52
+ *
53
+ * A skipped number is delivered past, with a warning: the skipped messages
54
+ * are lost, and this is the one place that can tell.
55
+ */
56
+ accept(sequence: unknown, bytes: number): boolean {
57
+ if (typeof sequence !== 'number') {
58
+ return true;
59
+ }
60
+ const isFirst = this.lastAccepted === 0;
61
+ const isNew = sequence > this.lastAccepted;
62
+ if (isNew) {
63
+ if (sequence > this.lastAccepted + 1) {
64
+ const missing = sequence - this.lastAccepted - 1;
65
+ console.warn(
66
+ `${CONNECTION_LOG_PREFIX} lost ${missing} message(s), numbered ${this.lastAccepted + 1} to ${sequence - 1}; ` +
67
+ 'they never arrived and nothing will resend them'
68
+ );
69
+ }
70
+ this.lastAccepted = sequence;
71
+ }
72
+ this.messagesSinceAcknowledgement++;
73
+ this.bytesSinceAcknowledgement += bytes;
74
+ if (
75
+ isFirst ||
76
+ this.messagesSinceAcknowledgement >= InboundMessageSequence.ACKNOWLEDGE_AFTER_MESSAGES ||
77
+ this.bytesSinceAcknowledgement >= InboundMessageSequence.ACKNOWLEDGE_AFTER_BYTES
78
+ ) {
79
+ this.acknowledge();
80
+ } else {
81
+ this.acknowledgementTimer ??= setTimeout(() => this.acknowledge(), InboundMessageSequence.ACKNOWLEDGE_DELAY_MS);
82
+ }
83
+ return isNew;
84
+ }
85
+
86
+ /** Sends the acknowledgement now rather than waiting for more traffic. */
87
+ acknowledge(): void {
88
+ this.cancelTimer();
89
+ this.messagesSinceAcknowledgement = 0;
90
+ this.bytesSinceAcknowledgement = 0;
91
+ this.sendAcknowledgement(this.lastAccepted);
92
+ }
93
+
94
+ dispose(): void {
95
+ this.cancelTimer();
96
+ }
97
+
98
+ protected cancelTimer(): void {
99
+ clearTimeout(this.acknowledgementTimer);
100
+ this.acknowledgementTimer = undefined;
101
+ }
102
+ }
@@ -11,5 +11,6 @@
11
11
  // bind for their own side, such as the buffer for each half of the socket.
12
12
  // Kept out of both tier barrels for that reason.
13
13
  export * from './framed-socket-write-buffer';
14
+ export * from './inbound-message-sequence';
14
15
  export * from './connection-resilience-options';
15
16
  export * from './clock';