UNPKG

@sussudio/platform

Version:

Internal APIs for VS Code's service injection the base services.

235 lines (234 loc) 7.57 kB
/*--------------------------------------------------------------------------------------------- * 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)); }); } }