1 |
|
2 |
|
3 |
|
4 | import { HttpClient } from "./HttpClient";
|
5 | import { ILogger, LogLevel } from "./ILogger";
|
6 | import { ITransport, TransferFormat } from "./ITransport";
|
7 | import { WebSocketConstructor } from "./Polyfills";
|
8 | import { Arg, getDataDetail, Platform } from "./Utils";
|
9 |
|
10 |
|
11 | export 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 |
|
55 | webSocket = new this.webSocketConstructor(url, undefined, {
|
56 | headers: {
|
57 | Cookie: `${cookies}`,
|
58 | },
|
59 | });
|
60 | }
|
61 |
|
62 | if (!webSocket) {
|
63 |
|
64 | webSocket = new this.webSocketConstructor(url);
|
65 | }
|
66 |
|
67 | if (transferFormat === TransferFormat.Binary) {
|
68 | webSocket.binaryType = "arraybuffer";
|
69 | }
|
70 |
|
71 |
|
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 |
|
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 |
|
100 |
|
101 | if (opened) {
|
102 | this.close(event);
|
103 | } else {
|
104 | let error: any = null;
|
105 |
|
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 | }
|