@sussudio/platform
Version:
Internal APIs for VS Code's service injection the base services.
235 lines (234 loc) • 7.57 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as dom from '@sussudio/base/browser/dom.mjs';
import { RunOnceScheduler } from '@sussudio/base/common/async.mjs';
import { VSBuffer } from '@sussudio/base/common/buffer.mjs';
import { Emitter } from '@sussudio/base/common/event.mjs';
import { Disposable } from '@sussudio/base/common/lifecycle.mjs';
import { SocketDiagnostics } from '@sussudio/base/parts/ipc/common/ipc.net.mjs';
import { RemoteAuthorityResolverError, RemoteAuthorityResolverErrorCode } from '../common/remoteAuthorityResolver.mjs';
class BrowserWebSocket extends Disposable {
_onData = new Emitter();
onData = this._onData.event;
_onOpen = this._register(new Emitter());
onOpen = this._onOpen.event;
_onClose = this._register(new Emitter());
onClose = this._onClose.event;
_onError = this._register(new Emitter());
onError = this._onError.event;
_debugLabel;
_socket;
_fileReader;
_queue;
_isReading;
_isClosed;
_socketMessageListener;
traceSocketEvent(type, data) {
SocketDiagnostics.traceSocketEvent(this._socket, this._debugLabel, type, data);
}
constructor(url, debugLabel) {
super();
this._debugLabel = debugLabel;
this._socket = new WebSocket(url);
this.traceSocketEvent('created' /* SocketDiagnosticsEventType.Created */, { type: 'BrowserWebSocket', url });
this._fileReader = new FileReader();
this._queue = [];
this._isReading = false;
this._isClosed = false;
this._fileReader.onload = (event) => {
this._isReading = false;
const buff = event.target.result;
this.traceSocketEvent('read' /* SocketDiagnosticsEventType.Read */, buff);
this._onData.fire(buff);
if (this._queue.length > 0) {
enqueue(this._queue.shift());
}
};
const enqueue = (blob) => {
if (this._isReading) {
this._queue.push(blob);
return;
}
this._isReading = true;
this._fileReader.readAsArrayBuffer(blob);
};
this._socketMessageListener = (ev) => {
const blob = ev.data;
this.traceSocketEvent(
'browserWebSocketBlobReceived' /* SocketDiagnosticsEventType.BrowserWebSocketBlobReceived */,
{ type: blob.type, size: blob.size },
);
enqueue(blob);
};
this._socket.addEventListener('message', this._socketMessageListener);
this._register(
dom.addDisposableListener(this._socket, 'open', (e) => {
this.traceSocketEvent('open' /* SocketDiagnosticsEventType.Open */);
this._onOpen.fire();
}),
);
// WebSockets emit error events that do not contain any real information
// Our only chance of getting to the root cause of an error is to
// listen to the close event which gives out some real information:
// - https://www.w3.org/TR/websockets/#closeevent
// - https://tools.ietf.org/html/rfc6455#section-11.7
//
// But the error event is emitted before the close event, so we therefore
// delay the error event processing in the hope of receiving a close event
// with more information
let pendingErrorEvent = null;
const sendPendingErrorNow = () => {
const err = pendingErrorEvent;
pendingErrorEvent = null;
this._onError.fire(err);
};
const errorRunner = this._register(new RunOnceScheduler(sendPendingErrorNow, 0));
const sendErrorSoon = (err) => {
errorRunner.cancel();
pendingErrorEvent = err;
errorRunner.schedule();
};
const sendErrorNow = (err) => {
errorRunner.cancel();
pendingErrorEvent = err;
sendPendingErrorNow();
};
this._register(
dom.addDisposableListener(this._socket, 'close', (e) => {
this.traceSocketEvent('close' /* SocketDiagnosticsEventType.Close */, {
code: e.code,
reason: e.reason,
wasClean: e.wasClean,
});
this._isClosed = true;
if (pendingErrorEvent) {
if (!window.navigator.onLine) {
// The browser is offline => this is a temporary error which might resolve itself
sendErrorNow(
new RemoteAuthorityResolverError(
'Browser is offline',
RemoteAuthorityResolverErrorCode.TemporarilyNotAvailable,
e,
),
);
} else {
// An error event is pending
// The browser appears to be online...
if (!e.wasClean) {
// Let's be optimistic and hope that perhaps the server could not be reached or something
sendErrorNow(
new RemoteAuthorityResolverError(
e.reason || `WebSocket close with status code ${e.code}`,
RemoteAuthorityResolverErrorCode.TemporarilyNotAvailable,
e,
),
);
} else {
// this was a clean close => send existing error
errorRunner.cancel();
sendPendingErrorNow();
}
}
}
this._onClose.fire({ code: e.code, reason: e.reason, wasClean: e.wasClean, event: e });
}),
);
this._register(
dom.addDisposableListener(this._socket, 'error', (err) => {
this.traceSocketEvent('error' /* SocketDiagnosticsEventType.Error */, { message: err?.message });
sendErrorSoon(err);
}),
);
}
send(data) {
if (this._isClosed) {
// Refuse to write data to closed WebSocket...
return;
}
this.traceSocketEvent('write' /* SocketDiagnosticsEventType.Write */, data);
this._socket.send(data);
}
close() {
this._isClosed = true;
this.traceSocketEvent('close' /* SocketDiagnosticsEventType.Close */);
this._socket.close();
this._socket.removeEventListener('message', this._socketMessageListener);
this.dispose();
}
}
const defaultWebSocketFactory = new (class {
create(url, debugLabel) {
return new BrowserWebSocket(url, debugLabel);
}
})();
class BrowserSocket {
socket;
debugLabel;
traceSocketEvent(type, data) {
if (typeof this.socket.traceSocketEvent === 'function') {
this.socket.traceSocketEvent(type, data);
} else {
SocketDiagnostics.traceSocketEvent(this.socket, this.debugLabel, type, data);
}
}
constructor(socket, debugLabel) {
this.socket = socket;
this.debugLabel = debugLabel;
}
dispose() {
this.socket.close();
}
onData(listener) {
return this.socket.onData((data) => listener(VSBuffer.wrap(new Uint8Array(data))));
}
onClose(listener) {
const adapter = (e) => {
if (typeof e === 'undefined') {
listener(e);
} else {
listener({
type: 1 /* SocketCloseEventType.WebSocketCloseEvent */,
code: e.code,
reason: e.reason,
wasClean: e.wasClean,
event: e.event,
});
}
};
return this.socket.onClose(adapter);
}
onEnd(listener) {
return Disposable.None;
}
write(buffer) {
this.socket.send(buffer.buffer);
}
end() {
this.socket.close();
}
drain() {
return Promise.resolve();
}
}
export class BrowserSocketFactory {
_webSocketFactory;
constructor(webSocketFactory) {
this._webSocketFactory = webSocketFactory || defaultWebSocketFactory;
}
connect(host, port, path, query, debugLabel, callback) {
const webSocketSchema = /^https:/.test(window.location.href) ? 'wss' : 'ws';
const socket = this._webSocketFactory.create(
`${webSocketSchema}://${
/:/.test(host) && !/\[/.test(host) ? `[${host}]` : host
}:${port}${path}?${query}&skipWebSocketFrames=false`,
debugLabel,
);
const errorListener = socket.onError((err) => callback(err, undefined));
socket.onOpen(() => {
errorListener.dispose();
callback(undefined, new BrowserSocket(socket, debugLabel));
});
}
}