UNPKG

6.72 kBPlain TextView Raw
1// Copyright (c) .NET Foundation. All rights reserved.
2// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3
4import { HttpClient } from "./HttpClient";
5import { ILogger, LogLevel } from "./ILogger";
6import { ITransport, TransferFormat } from "./ITransport";
7import { WebSocketConstructor } from "./Polyfills";
8import { Arg, getDataDetail, Platform } from "./Utils";
9
10/** @private */
11export class WebSocketTransport implements ITransport {
12 private readonly logger: ILogger;
13 private readonly accessTokenFactory: (() => string | Promise<string>) | undefined;
14 private readonly logMessageContent: boolean;
15 private readonly webSocketConstructor: WebSocketConstructor;
16 private readonly httpClient: HttpClient;
17 private webSocket?: WebSocket;
18
19 public onreceive: ((data: string | ArrayBuffer) => void) | null;
20 public onclose: ((error?: Error) => void) | null;
21
22 constructor(httpClient: HttpClient, accessTokenFactory: (() => string | Promise<string>) | undefined, logger: ILogger,
23 logMessageContent: boolean, webSocketConstructor: WebSocketConstructor) {
24 this.logger = logger;
25 this.accessTokenFactory = accessTokenFactory;
26 this.logMessageContent = logMessageContent;
27 this.webSocketConstructor = webSocketConstructor;
28 this.httpClient = httpClient;
29
30 this.onreceive = null;
31 this.onclose = null;
32 }
33
34 public async connect(url: string, transferFormat: TransferFormat): Promise<void> {
35 Arg.isRequired(url, "url");
36 Arg.isRequired(transferFormat, "transferFormat");
37 Arg.isIn(transferFormat, TransferFormat, "transferFormat");
38 this.logger.log(LogLevel.Trace, "(WebSockets transport) Connecting.");
39
40 if (this.accessTokenFactory) {
41 const token = await this.accessTokenFactory();
42 if (token) {
43 url += (url.indexOf("?") < 0 ? "?" : "&") + `access_token=${encodeURIComponent(token)}`;
44 }
45 }
46
47 return new Promise<void>((resolve, reject) => {
48 url = url.replace(/^http/, "ws");
49 let webSocket: WebSocket | undefined;
50 const cookies = this.httpClient.getCookieString(url);
51 let opened = false;
52
53 if (Platform.isNode && cookies) {
54 // Only pass cookies when in non-browser environments
55 webSocket = new this.webSocketConstructor(url, undefined, {
56 headers: {
57 Cookie: `${cookies}`,
58 },
59 });
60 }
61
62 if (!webSocket) {
63 // Chrome is not happy with passing 'undefined' as protocol
64 webSocket = new this.webSocketConstructor(url);
65 }
66
67 if (transferFormat === TransferFormat.Binary) {
68 webSocket.binaryType = "arraybuffer";
69 }
70
71 // tslint:disable-next-line:variable-name
72 webSocket.onopen = (_event: Event) => {
73 this.logger.log(LogLevel.Information, `WebSocket connected to ${url}.`);
74 this.webSocket = webSocket;
75 opened = true;
76 resolve();
77 };
78
79 webSocket.onerror = (event: Event) => {
80 let error: any = null;
81 // ErrorEvent is a browser only type we need to check if the type exists before using it
82 if (typeof ErrorEvent !== "undefined" && event instanceof ErrorEvent) {
83 error = event.error;
84 } else {
85 error = new Error("There was an error with the transport.");
86 }
87
88 reject(error);
89 };
90
91 webSocket.onmessage = (message: MessageEvent) => {
92 this.logger.log(LogLevel.Trace, `(WebSockets transport) data received. ${getDataDetail(message.data, this.logMessageContent)}.`);
93 if (this.onreceive) {
94 this.onreceive(message.data);
95 }
96 };
97
98 webSocket.onclose = (event: CloseEvent) => {
99 // Don't call close handler if connection was never established
100 // We'll reject the connect call instead
101 if (opened) {
102 this.close(event);
103 } else {
104 let error: any = null;
105 // ErrorEvent is a browser only type we need to check if the type exists before using it
106 if (typeof ErrorEvent !== "undefined" && event instanceof ErrorEvent) {
107 error = event.error;
108 } else {
109 error = new Error("There was an error with the transport.");
110 }
111
112 reject(error);
113 }
114 };
115 });
116 }
117
118 public send(data: any): Promise<void> {
119 if (this.webSocket && this.webSocket.readyState === this.webSocketConstructor.OPEN) {
120 this.logger.log(LogLevel.Trace, `(WebSockets transport) sending data. ${getDataDetail(data, this.logMessageContent)}.`);
121 this.webSocket.send(data);
122 return Promise.resolve();
123 }
124
125 return Promise.reject("WebSocket is not in the OPEN state");
126 }
127
128 public stop(): Promise<void> {
129 if (this.webSocket) {
130 // Manually invoke onclose callback inline so we know the HttpConnection was closed properly before returning
131 // This also solves an issue where websocket.onclose could take 18+ seconds to trigger during network disconnects
132 this.close(undefined);
133 }
134
135 return Promise.resolve();
136 }
137
138 private close(event?: CloseEvent): void {
139 // webSocket will be null if the transport did not start successfully
140 if (this.webSocket) {
141 // Clear websocket handlers because we are considering the socket closed now
142 this.webSocket.onclose = () => {};
143 this.webSocket.onmessage = () => {};
144 this.webSocket.onerror = () => {};
145 this.webSocket.close();
146 this.webSocket = undefined;
147 }
148
149 this.logger.log(LogLevel.Trace, "(WebSockets transport) socket closed.");
150 if (this.onclose) {
151 if (event && (event.wasClean === false || event.code !== 1000)) {
152 this.onclose(new Error(`WebSocket closed with status code: ${event.code} (${event.reason}).`));
153 } else {
154 this.onclose();
155 }
156 }
157 }
158}