@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.
- package/lib/browser/session-aware-connection-source.d.ts +5 -2
- package/lib/browser/session-aware-connection-source.d.ts.map +1 -1
- package/lib/browser/session-aware-connection-source.js +21 -4
- package/lib/browser/session-aware-connection-source.js.map +1 -1
- package/lib/common/connection-resilience-options.d.ts +6 -0
- package/lib/common/connection-resilience-options.d.ts.map +1 -1
- package/lib/common/framed-socket-write-buffer.d.ts +55 -3
- package/lib/common/framed-socket-write-buffer.d.ts.map +1 -1
- package/lib/common/framed-socket-write-buffer.js +111 -8
- package/lib/common/framed-socket-write-buffer.js.map +1 -1
- package/lib/common/inbound-message-sequence.d.ts +56 -0
- package/lib/common/inbound-message-sequence.d.ts.map +1 -0
- package/lib/common/inbound-message-sequence.js +98 -0
- package/lib/common/inbound-message-sequence.js.map +1 -0
- package/lib/common/index.d.ts +1 -0
- package/lib/common/index.d.ts.map +1 -1
- package/lib/common/index.js +1 -0
- package/lib/common/index.js.map +1 -1
- package/lib/node/session-bound-frontend-connection-service.d.ts +41 -0
- package/lib/node/session-bound-frontend-connection-service.d.ts.map +1 -1
- package/lib/node/session-bound-frontend-connection-service.js +98 -2
- package/lib/node/session-bound-frontend-connection-service.js.map +1 -1
- package/package.json +3 -3
- package/src/browser/session-aware-connection-source.ts +21 -4
- package/src/common/connection-resilience-options.ts +6 -0
- package/src/common/framed-socket-write-buffer.ts +127 -9
- package/src/common/inbound-message-sequence.ts +102 -0
- package/src/common/index.ts +1 -0
- 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;
|
|
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
|
|
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":";AAAA;;;;;;;kFAOkF;;;;;;;;;;AAElF,kHAA4G;AAC5G,8FAAyF;AACzF,gIAA0H;AAC1H,4DAA2E;AAE3E,
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
96
|
-
* and exposes no narrower seam. Re-diff when the supported
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
221
|
-
export function createFramedSocketWriteBuffer(
|
|
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 (
|
|
224
|
-
buffer.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
|
+
}
|
package/src/common/index.ts
CHANGED
|
@@ -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';
|