UNPKG

sussudio

Version:

An unofficial VS Code Internal API

209 lines (208 loc) 8.71 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 "../../../base/browser/dom.mjs"; import { RunOnceScheduler } from "../../../base/common/async.mjs"; import { VSBuffer } from "../../../base/common/buffer.mjs"; import { Emitter } from "../../../base/common/event.mjs"; import { Disposable } from "../../../base/common/lifecycle.mjs"; import { SocketDiagnostics } from "../../../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)); }); } }