websocket-ts 1.1.1 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +157 -93
  3. package/dist/cjs/src/backoff/backoff.d.ts +24 -0
  4. package/dist/cjs/src/backoff/backoff.d.ts.map +1 -0
  5. package/{lib → dist/cjs/src}/backoff/backoff.js.map +1 -1
  6. package/dist/cjs/src/backoff/constantbackoff.d.ts +18 -0
  7. package/dist/cjs/src/backoff/constantbackoff.d.ts.map +1 -0
  8. package/dist/cjs/src/backoff/constantbackoff.js +43 -0
  9. package/dist/cjs/src/backoff/constantbackoff.js.map +1 -0
  10. package/dist/cjs/src/backoff/exponentialbackoff.d.ts +48 -0
  11. package/dist/cjs/src/backoff/exponentialbackoff.d.ts.map +1 -0
  12. package/dist/cjs/src/backoff/exponentialbackoff.js +81 -0
  13. package/dist/cjs/src/backoff/exponentialbackoff.js.map +1 -0
  14. package/dist/cjs/src/backoff/linearbackoff.d.ts +51 -0
  15. package/dist/cjs/src/backoff/linearbackoff.d.ts.map +1 -0
  16. package/dist/cjs/src/backoff/linearbackoff.js +89 -0
  17. package/dist/cjs/src/backoff/linearbackoff.js.map +1 -0
  18. package/dist/cjs/src/index.d.ts +14 -0
  19. package/dist/cjs/src/index.d.ts.map +1 -0
  20. package/dist/cjs/src/index.js +20 -0
  21. package/dist/cjs/src/index.js.map +1 -0
  22. package/dist/cjs/src/queue/array_queue.d.ts +17 -0
  23. package/dist/cjs/src/queue/array_queue.d.ts.map +1 -0
  24. package/dist/cjs/src/queue/array_queue.js +36 -0
  25. package/dist/cjs/src/queue/array_queue.js.map +1 -0
  26. package/dist/cjs/src/queue/queue.d.ts +41 -0
  27. package/dist/cjs/src/queue/queue.d.ts.map +1 -0
  28. package/{lib/buffer/buffer.js → dist/cjs/src/queue/queue.js} +1 -1
  29. package/dist/cjs/src/queue/queue.js.map +1 -0
  30. package/dist/cjs/src/queue/ring_queue.d.ts +20 -0
  31. package/dist/cjs/src/queue/ring_queue.d.ts.map +1 -0
  32. package/dist/cjs/src/queue/ring_queue.js +57 -0
  33. package/dist/cjs/src/queue/ring_queue.js.map +1 -0
  34. package/dist/cjs/src/websocket.d.ts +206 -0
  35. package/dist/cjs/src/websocket.d.ts.map +1 -0
  36. package/dist/cjs/src/websocket.js +434 -0
  37. package/dist/cjs/src/websocket.js.map +1 -0
  38. package/dist/cjs/src/websocket_buffer.d.ts +16 -0
  39. package/dist/cjs/src/websocket_buffer.d.ts.map +1 -0
  40. package/dist/cjs/src/websocket_buffer.js +3 -0
  41. package/dist/cjs/src/websocket_buffer.js.map +1 -0
  42. package/dist/cjs/src/websocket_builder.d.ts +147 -0
  43. package/dist/cjs/src/websocket_builder.d.ts.map +1 -0
  44. package/dist/cjs/src/websocket_builder.js +263 -0
  45. package/dist/cjs/src/websocket_builder.js.map +1 -0
  46. package/dist/cjs/src/websocket_event.d.ts +67 -0
  47. package/dist/cjs/src/websocket_event.d.ts.map +1 -0
  48. package/dist/cjs/src/websocket_event.js +22 -0
  49. package/dist/cjs/src/websocket_event.js.map +1 -0
  50. package/dist/cjs/src/websocket_options.d.ts +21 -0
  51. package/dist/cjs/src/websocket_options.d.ts.map +1 -0
  52. package/dist/cjs/src/websocket_options.js +3 -0
  53. package/dist/cjs/src/websocket_options.js.map +1 -0
  54. package/dist/cjs/src/websocket_retry_options.d.ts +20 -0
  55. package/dist/cjs/src/websocket_retry_options.d.ts.map +1 -0
  56. package/dist/cjs/src/websocket_retry_options.js +3 -0
  57. package/dist/cjs/src/websocket_retry_options.js.map +1 -0
  58. package/dist/esm/src/backoff/backoff.d.ts +24 -0
  59. package/dist/esm/src/backoff/backoff.d.ts.map +1 -0
  60. package/dist/esm/src/backoff/backoff.js +2 -0
  61. package/dist/esm/src/backoff/backoff.js.map +1 -0
  62. package/dist/esm/src/backoff/constantbackoff.d.ts +18 -0
  63. package/dist/esm/src/backoff/constantbackoff.d.ts.map +1 -0
  64. package/dist/esm/src/backoff/constantbackoff.js +30 -0
  65. package/dist/esm/src/backoff/constantbackoff.js.map +1 -0
  66. package/dist/esm/src/backoff/exponentialbackoff.d.ts +48 -0
  67. package/dist/esm/src/backoff/exponentialbackoff.d.ts.map +1 -0
  68. package/dist/esm/src/backoff/exponentialbackoff.js +68 -0
  69. package/dist/esm/src/backoff/exponentialbackoff.js.map +1 -0
  70. package/dist/esm/src/backoff/linearbackoff.d.ts +51 -0
  71. package/dist/esm/src/backoff/linearbackoff.d.ts.map +1 -0
  72. package/dist/esm/src/backoff/linearbackoff.js +76 -0
  73. package/dist/esm/src/backoff/linearbackoff.js.map +1 -0
  74. package/dist/esm/src/index.d.ts +14 -0
  75. package/dist/esm/src/index.d.ts.map +1 -0
  76. package/dist/esm/src/index.js +9 -0
  77. package/dist/esm/src/index.js.map +1 -0
  78. package/dist/esm/src/queue/array_queue.d.ts +17 -0
  79. package/dist/esm/src/queue/array_queue.d.ts.map +1 -0
  80. package/dist/esm/src/queue/array_queue.js +31 -0
  81. package/dist/esm/src/queue/array_queue.js.map +1 -0
  82. package/dist/esm/src/queue/queue.d.ts +41 -0
  83. package/dist/esm/src/queue/queue.d.ts.map +1 -0
  84. package/dist/esm/src/queue/queue.js +2 -0
  85. package/dist/esm/src/queue/queue.js.map +1 -0
  86. package/dist/esm/src/queue/ring_queue.d.ts +20 -0
  87. package/dist/esm/src/queue/ring_queue.d.ts.map +1 -0
  88. package/dist/esm/src/queue/ring_queue.js +52 -0
  89. package/dist/esm/src/queue/ring_queue.js.map +1 -0
  90. package/dist/esm/src/websocket.d.ts +206 -0
  91. package/dist/esm/src/websocket.d.ts.map +1 -0
  92. package/dist/esm/src/websocket.js +356 -0
  93. package/dist/esm/src/websocket.js.map +1 -0
  94. package/dist/esm/src/websocket_buffer.d.ts +16 -0
  95. package/dist/esm/src/websocket_buffer.d.ts.map +1 -0
  96. package/dist/esm/src/websocket_buffer.js +2 -0
  97. package/dist/esm/src/websocket_buffer.js.map +1 -0
  98. package/dist/esm/src/websocket_builder.d.ts +147 -0
  99. package/dist/esm/src/websocket_builder.d.ts.map +1 -0
  100. package/dist/esm/src/websocket_builder.js +213 -0
  101. package/dist/esm/src/websocket_builder.js.map +1 -0
  102. package/dist/esm/src/websocket_event.d.ts +67 -0
  103. package/dist/esm/src/websocket_event.d.ts.map +1 -0
  104. package/dist/esm/src/websocket_event.js +19 -0
  105. package/dist/esm/src/websocket_event.js.map +1 -0
  106. package/dist/esm/src/websocket_options.d.ts +21 -0
  107. package/dist/esm/src/websocket_options.d.ts.map +1 -0
  108. package/dist/esm/src/websocket_options.js +2 -0
  109. package/dist/esm/src/websocket_options.js.map +1 -0
  110. package/dist/esm/src/websocket_retry_options.d.ts +20 -0
  111. package/dist/esm/src/websocket_retry_options.d.ts.map +1 -0
  112. package/dist/esm/src/websocket_retry_options.js +2 -0
  113. package/dist/esm/src/websocket_retry_options.js.map +1 -0
  114. package/package.json +27 -14
  115. package/src/backoff/backoff.ts +21 -13
  116. package/src/backoff/constantbackoff.ts +29 -11
  117. package/src/backoff/exponentialbackoff.ts +68 -28
  118. package/src/backoff/linearbackoff.ts +77 -26
  119. package/src/index.ts +23 -9
  120. package/src/queue/array_queue.ts +41 -0
  121. package/src/queue/queue.ts +46 -0
  122. package/src/queue/ring_queue.ts +69 -0
  123. package/src/websocket.ts +463 -138
  124. package/src/websocket_buffer.ts +18 -0
  125. package/src/websocket_builder.ts +274 -0
  126. package/src/websocket_event.ts +88 -0
  127. package/src/websocket_options.ts +23 -0
  128. package/src/websocket_retry_options.ts +22 -0
  129. package/{tsconfig.json → tsconfig.cjs.json} +8 -8
  130. package/tsconfig.esm.json +71 -0
  131. package/jest.config.js +0 -9
  132. package/lib/backoff/backoff.d.ts +0 -18
  133. package/lib/backoff/backoff.d.ts.map +0 -1
  134. package/lib/backoff/constantbackoff.d.ts +0 -11
  135. package/lib/backoff/constantbackoff.d.ts.map +0 -1
  136. package/lib/backoff/constantbackoff.js +0 -20
  137. package/lib/backoff/constantbackoff.js.map +0 -1
  138. package/lib/backoff/exponentialbackoff.d.ts +0 -22
  139. package/lib/backoff/exponentialbackoff.d.ts.map +0 -1
  140. package/lib/backoff/exponentialbackoff.js +0 -35
  141. package/lib/backoff/exponentialbackoff.js.map +0 -1
  142. package/lib/backoff/linearbackoff.d.ts +0 -19
  143. package/lib/backoff/linearbackoff.d.ts.map +0 -1
  144. package/lib/backoff/linearbackoff.js +0 -34
  145. package/lib/backoff/linearbackoff.js.map +0 -1
  146. package/lib/buffer/buffer.d.ts +0 -41
  147. package/lib/buffer/buffer.d.ts.map +0 -1
  148. package/lib/buffer/buffer.js.map +0 -1
  149. package/lib/buffer/lrubuffer.d.ts +0 -24
  150. package/lib/buffer/lrubuffer.d.ts.map +0 -1
  151. package/lib/buffer/lrubuffer.js +0 -76
  152. package/lib/buffer/lrubuffer.js.map +0 -1
  153. package/lib/buffer/timebuffer.d.ts +0 -25
  154. package/lib/buffer/timebuffer.d.ts.map +0 -1
  155. package/lib/buffer/timebuffer.js +0 -92
  156. package/lib/buffer/timebuffer.js.map +0 -1
  157. package/lib/index.d.ts +0 -10
  158. package/lib/index.d.ts.map +0 -1
  159. package/lib/index.js +0 -22
  160. package/lib/index.js.map +0 -1
  161. package/lib/websocket.d.ts +0 -47
  162. package/lib/websocket.d.ts.map +0 -1
  163. package/lib/websocket.js +0 -120
  164. package/lib/websocket.js.map +0 -1
  165. package/lib/websocketBuilder.d.ts +0 -32
  166. package/lib/websocketBuilder.d.ts.map +0 -1
  167. package/lib/websocketBuilder.js +0 -68
  168. package/lib/websocketBuilder.js.map +0 -1
  169. package/src/buffer/buffer.ts +0 -45
  170. package/src/buffer/lrubuffer.ts +0 -80
  171. package/src/buffer/timebuffer.ts +0 -105
  172. package/src/websocketBuilder.ts +0 -98
  173. package/test/backoff/constantbackoff.test.ts +0 -21
  174. package/test/backoff/exponentialbackoff.test.ts +0 -13
  175. package/test/backoff/linearbackoff.test.ts +0 -32
  176. package/test/buffer/common.ts +0 -10
  177. package/test/buffer/lrubuffer.test.ts +0 -128
  178. package/test/buffer/timebuffer.test.ts +0 -115
  179. package/test/websocket.test.ts +0 -341
  180. package/test/websocketBuilder.test.ts +0 -36
  181. /package/{lib → dist/cjs/src}/backoff/backoff.js +0 -0
package/src/websocket.ts CHANGED
@@ -1,162 +1,487 @@
1
- import {Backoff} from "./backoff/backoff";
2
- import {Buffer} from "./buffer/buffer";
1
+ import { Backoff } from "./backoff/backoff";
2
+ import { WebsocketBuffer } from "./websocket_buffer";
3
+ import {
4
+ ReconnectEventDetail,
5
+ RetryEventDetail,
6
+ WebsocketEvent,
7
+ WebsocketEventListener,
8
+ WebsocketEventListenerOptions,
9
+ WebsocketEventListeners,
10
+ WebsocketEventListenerWithOptions,
11
+ WebsocketEventMap,
12
+ } from "./websocket_event";
13
+ import { WebsocketOptions } from "./websocket_options";
3
14
 
4
- type eventListener<K extends WebsocketEvents> = {
5
- readonly listener: (instance: Websocket, ev: WebsocketEventMap[K]) => any;
6
- readonly options?: boolean | EventListenerOptions;
7
- }
15
+ /**
16
+ * A websocket wrapper that can be configured to reconnect automatically and buffer messages when the websocket is not connected.
17
+ */
18
+ export class Websocket {
19
+ private readonly _url: string; // the url to connect to
20
+ private readonly _protocols?: string | string[]; // the protocols to use
8
21
 
9
- export enum WebsocketEvents {
10
- open = 'open', // Connection is opened or re-opened
11
- close = 'close', // Connection is closed
12
- error = 'error', // An error occurred
13
- message = 'message', // A message was received
14
- retry = 'retry' // A try to re-connect is made
15
- }
22
+ private _closedByUser: boolean = false; // whether the websocket was closed by the user
23
+ private _lastConnection?: Date; // timestamp of the last connection
24
+ private _underlyingWebsocket: WebSocket; // the underlying websocket, e.g. native browser websocket
25
+ private retryTimeout?: number; // timeout for the next retry, if any
16
26
 
17
- interface WebsocketEventMap {
18
- close: CloseEvent;
19
- error: Event;
20
- message: MessageEvent;
21
- open: Event;
22
- retry: CustomEvent<RetryEventDetails>;
23
- }
27
+ private _options: WebsocketOptions &
28
+ Required<Pick<WebsocketOptions, "listeners" | "retry">>; // options/config for the websocket
24
29
 
25
- export interface RetryEventDetails {
26
- readonly retries: number;
27
- readonly backoff: number
28
- }
30
+ /**
31
+ * Creates a new websocket.
32
+ *
33
+ * @param url to connect to.
34
+ * @param protocols optional protocols to use.
35
+ * @param options optional options to use.
36
+ */
37
+ constructor(
38
+ url: string,
39
+ protocols?: string | string[],
40
+ options?: WebsocketOptions,
41
+ ) {
42
+ this._url = url;
43
+ this._protocols = protocols;
29
44
 
30
- type WebsocketEventListeners = {
31
- open: eventListener<WebsocketEvents.open>[];
32
- close: eventListener<WebsocketEvents.close>[];
33
- error: eventListener<WebsocketEvents.error>[];
34
- message: eventListener<WebsocketEvents.message>[];
35
- retry: eventListener<WebsocketEvents.retry>[];
36
- }
45
+ // make a copy of the options to prevent the user from changing them
46
+ this._options = {
47
+ buffer: options?.buffer,
48
+ retry: {
49
+ maxRetries: options?.retry?.maxRetries,
50
+ instantReconnect: options?.retry?.instantReconnect,
51
+ backoff: options?.retry?.backoff,
52
+ },
53
+ listeners: {
54
+ open: [...(options?.listeners?.open ?? [])],
55
+ close: [...(options?.listeners?.close ?? [])],
56
+ error: [...(options?.listeners?.error ?? [])],
57
+ message: [...(options?.listeners?.message ?? [])],
58
+ retry: [...(options?.listeners?.retry ?? [])],
59
+ reconnect: [...(options?.listeners?.reconnect ?? [])],
60
+ },
61
+ };
37
62
 
38
- type WebsocketBuffer = Buffer<string | ArrayBufferLike | Blob | ArrayBufferView>;
63
+ this._underlyingWebsocket = this.tryConnect();
64
+ }
39
65
 
40
- export class Websocket {
41
- private readonly url: string;
42
- private readonly protocols?: string | string[];
43
- private readonly buffer?: WebsocketBuffer;
44
- private readonly backoff?: Backoff;
45
- private readonly eventListeners: WebsocketEventListeners = {open: [], close: [], error: [], message: [], retry: []};
46
- private closedByUser: boolean = false;
47
- private websocket?: WebSocket;
48
- private retries: number = 0;
49
-
50
- constructor(url: string, protocols?: string | string[], buffer?: WebsocketBuffer, backoff?: Backoff) {
51
- this.url = url;
52
- this.protocols = protocols;
53
- this.buffer = buffer;
54
- this.backoff = backoff;
55
- this.tryConnect();
56
- }
66
+ /**
67
+ * Getter for the url.
68
+ *
69
+ * @return the url.
70
+ */
71
+ get url(): string {
72
+ return this._url;
73
+ }
57
74
 
58
- get underlyingWebsocket(): WebSocket | undefined {
59
- return this.websocket;
60
- }
75
+ /**
76
+ * Getter for the protocols.
77
+ *
78
+ * @return the protocols, or undefined if none were provided.
79
+ */
80
+ get protocols(): string | string[] | undefined {
81
+ return this._protocols;
82
+ }
61
83
 
62
- public send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void {
63
- if (this.closedByUser)
64
- return;
65
- if (this.websocket === undefined || this.websocket.readyState !== this.websocket.OPEN)
66
- this.buffer?.write([data]);
67
- else
68
- this.websocket.send(data);
69
- }
84
+ /**
85
+ * Getter for the buffer.
86
+ *
87
+ * @return the buffer, or undefined if none was provided.
88
+ */
89
+ get buffer(): WebsocketBuffer | undefined {
90
+ return this._options.buffer;
91
+ }
70
92
 
71
- public close(code?: number, reason?: string): void {
72
- this.closedByUser = true;
73
- this.websocket?.close(code, reason);
74
- }
93
+ /**
94
+ * Getter for the maxRetries.
95
+ *
96
+ * @return the maxRetries, or undefined if none was provided (no limit).
97
+ */
98
+ get maxRetries(): number | undefined {
99
+ return this._options.retry.maxRetries;
100
+ }
75
101
 
76
- public addEventListener<K extends WebsocketEvents>(
77
- type: K,
78
- listener: (instance: Websocket, ev: WebsocketEventMap[K]) => any,
79
- options?: boolean | AddEventListenerOptions): void {
80
- const eventListener = {listener, options} as eventListener<K>;
81
- const eventListeners = this.eventListeners[type] as eventListener<K>[];
82
- eventListeners.push(eventListener);
83
- }
102
+ /**
103
+ * Getter for the instantReconnect.
104
+ *
105
+ * @return the instantReconnect, or undefined if none was provided.
106
+ */
107
+ get instantReconnect(): boolean | undefined {
108
+ return this._options.retry.instantReconnect;
109
+ }
84
110
 
85
- public removeEventListener<K extends WebsocketEvents>(
86
- type: K,
87
- listener: (instance: Websocket, ev: WebsocketEventMap[K]) => any,
88
- options?: boolean | EventListenerOptions): void {
89
- (this.eventListeners[type] as eventListener<K>[]) =
90
- (this.eventListeners[type] as eventListener<K>[])
91
- .filter(l => {
92
- return l.listener !== listener && (l.options === undefined || l.options !== options);
93
- });
94
- }
111
+ /**
112
+ * Getter for the backoff.
113
+ *
114
+ * @return the backoff, or undefined if none was provided.
115
+ */
116
+ get backoff(): Backoff | undefined {
117
+ return this._options.retry.backoff;
118
+ }
95
119
 
96
- private dispatchEvent<K extends WebsocketEvents>(type: K, ev: WebsocketEventMap[K]) {
97
- const listeners = this.eventListeners[type] as eventListener<K>[];
98
- const onceListeners = [] as eventListener<K>[];
99
- listeners.forEach(l => {
100
- l.listener(this, ev); // call listener
101
- if (l.options !== undefined && (l.options as AddEventListenerOptions).once)
102
- onceListeners.push(l);
103
- });
104
- onceListeners.forEach(l => this.removeEventListener(type, l.listener, l.options)); // remove 'once'-listeners
105
- }
120
+ /**
121
+ * Whether the websocket was closed by the user. A websocket is closed by the user if the close().
122
+ *
123
+ * @return true if the websocket was closed by the user, false otherwise.
124
+ */
125
+ get closedByUser(): boolean {
126
+ return this._closedByUser;
127
+ }
106
128
 
107
- private tryConnect(): void {
108
- if (this.websocket !== undefined) { // remove all event-listeners from broken socket
109
- this.websocket.removeEventListener(WebsocketEvents.open, this.handleOpenEvent);
110
- this.websocket.removeEventListener(WebsocketEvents.close, this.handleCloseEvent);
111
- this.websocket.removeEventListener(WebsocketEvents.error, this.handleErrorEvent);
112
- this.websocket.removeEventListener(WebsocketEvents.message, this.handleMessageEvent);
113
- this.websocket.close();
114
- }
115
- this.websocket = new WebSocket(this.url, this.protocols); // create new socket and attach handlers
116
- this.websocket.addEventListener(WebsocketEvents.open, this.handleOpenEvent);
117
- this.websocket.addEventListener(WebsocketEvents.close, this.handleCloseEvent);
118
- this.websocket.addEventListener(WebsocketEvents.error, this.handleErrorEvent);
119
- this.websocket.addEventListener(WebsocketEvents.message, this.handleMessageEvent);
129
+ /**
130
+ * Getter for the last 'open' event, e.g. the last time the websocket was connected.
131
+ *
132
+ * @return the last 'open' event, or undefined if the websocket was never connected.
133
+ */
134
+ get lastConnection(): Date | undefined {
135
+ return this._lastConnection;
136
+ }
137
+
138
+ /**
139
+ * Getter for the underlying websocket. This can be used to access the browser's native websocket directly.
140
+ *
141
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
142
+ * @return the underlying websocket.
143
+ */
144
+ get underlyingWebsocket(): WebSocket {
145
+ return this._underlyingWebsocket;
146
+ }
147
+
148
+ /**
149
+ * Getter for the readyState of the underlying websocket.
150
+ *
151
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState
152
+ * @return the readyState of the underlying websocket.
153
+ */
154
+ get readyState(): number {
155
+ return this._underlyingWebsocket.readyState;
156
+ }
157
+
158
+ /**
159
+ * Getter for the bufferedAmount of the underlying websocket.
160
+ *
161
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/bufferedAmount
162
+ * @return the bufferedAmount of the underlying websocket.
163
+ */
164
+ get bufferedAmount(): number {
165
+ return this._underlyingWebsocket.bufferedAmount;
166
+ }
167
+
168
+ /**
169
+ * Getter for the extensions of the underlying websocket.
170
+ *
171
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/extensions
172
+ * @return the extensions of the underlying websocket.
173
+ */
174
+ get extensions(): string {
175
+ return this._underlyingWebsocket.extensions;
176
+ }
177
+
178
+ /**
179
+ * Getter for the binaryType of the underlying websocket.
180
+ *
181
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/binaryType
182
+ * @return the binaryType of the underlying websocket.
183
+ */
184
+ get binaryType(): BinaryType {
185
+ return this._underlyingWebsocket.binaryType;
186
+ }
187
+
188
+ /**
189
+ * Setter for the binaryType of the underlying websocket.
190
+ *
191
+ * @param value to set, 'blob' or 'arraybuffer'.
192
+ */
193
+ set binaryType(value: BinaryType) {
194
+ this._underlyingWebsocket.binaryType = value;
195
+ }
196
+
197
+ /**
198
+ * Sends data over the websocket.
199
+ *
200
+ * If the websocket is not connected and a buffer was provided on creation, the data will be added to the buffer.
201
+ * If no buffer was provided or the websocket was closed by the user, the data will be dropped.
202
+ *
203
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send
204
+ * @param data to send.
205
+ */
206
+ public send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void {
207
+ if (this.closedByUser) return; // no-op if closed by user
208
+
209
+ if (
210
+ this._underlyingWebsocket.readyState === this._underlyingWebsocket.OPEN
211
+ ) {
212
+ this._underlyingWebsocket.send(data); // websocket is connected, send data
213
+ } else if (this.buffer !== undefined) {
214
+ this.buffer.add(data); // websocket is not connected, add data to buffer
120
215
  }
216
+ }
217
+
218
+ /**
219
+ * Close the websocket. No connection-retry will be attempted after this.
220
+ *
221
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close
222
+ * @param code optional close code.
223
+ * @param reason optional close reason.
224
+ */
225
+ public close(code?: number, reason?: string): void {
226
+ this.cancelScheduledConnectionRetry(); // cancel any scheduled retries
227
+ this._closedByUser = true; // mark websocket as closed by user
228
+ this._underlyingWebsocket.close(code, reason); // close underlying websocket with provided code and reason
229
+ }
121
230
 
122
- private handleOpenEvent = (ev: Event) => this.handleEvent(WebsocketEvents.open, ev);
231
+ /**
232
+ * Adds an event listener for the given event-type.
233
+ *
234
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
235
+ * @param type of the event to add the listener for.
236
+ * @param listener to add.
237
+ * @param options to use when adding the listener.
238
+ */
239
+ public addEventListener<K extends WebsocketEvent>(
240
+ type: K,
241
+ listener: WebsocketEventListener<K>,
242
+ options?: WebsocketEventListenerOptions,
243
+ ): void {
244
+ this._options.listeners[type].push({ listener, options }); // add listener to list of listeners
245
+ }
123
246
 
124
- private handleCloseEvent = (ev: CloseEvent) => this.handleEvent(WebsocketEvents.close, ev);
247
+ /**
248
+ * Removes one or more event listener for the given event-type that match the given listener and options.
249
+ *
250
+ * @param type of the event to remove the listener for.
251
+ * @param listener to remove.
252
+ * @param options that were used when the listener was added.
253
+ */
254
+ public removeEventListener<K extends WebsocketEvent>(
255
+ type: K,
256
+ listener: WebsocketEventListener<K>,
257
+ options?: WebsocketEventListenerOptions,
258
+ ): void {
259
+ const isListenerNotToBeRemoved = (
260
+ l: WebsocketEventListenerWithOptions<K>,
261
+ ) => l.listener !== listener || l.options !== options;
125
262
 
126
- private handleErrorEvent = (ev: Event) => this.handleEvent(WebsocketEvents.error, ev);
263
+ (this._options.listeners[type] as WebsocketEventListenerWithOptions<K>[]) =
264
+ this._options.listeners[type].filter(isListenerNotToBeRemoved); // only keep listeners that are not to be removed
265
+ }
127
266
 
128
- private handleMessageEvent = (ev: MessageEvent) => this.handleEvent(WebsocketEvents.message, ev);
267
+ /**
268
+ * Creates a new browser-native websocket and connects it to the given URL with the given protocols
269
+ * and adds all event listeners to the browser-native websocket.
270
+ *
271
+ * @return the created browser-native websocket which is also stored in the '_underlyingWebsocket' property.
272
+ */
273
+ private tryConnect(): WebSocket {
274
+ this._underlyingWebsocket = new WebSocket(this.url, this.protocols); // create new browser-native websocket and add all event listeners
275
+ this._underlyingWebsocket.addEventListener(
276
+ WebsocketEvent.open,
277
+ this.handleOpenEvent,
278
+ );
279
+ this._underlyingWebsocket.addEventListener(
280
+ WebsocketEvent.close,
281
+ this.handleCloseEvent,
282
+ );
283
+ this._underlyingWebsocket.addEventListener(
284
+ WebsocketEvent.error,
285
+ this.handleErrorEvent,
286
+ );
287
+ this._underlyingWebsocket.addEventListener(
288
+ WebsocketEvent.message,
289
+ this.handleMessageEvent,
290
+ );
129
291
 
130
- private handleEvent<K extends WebsocketEvents>(type: K, ev: WebsocketEventMap[K]) {
131
- switch (type) {
132
- case WebsocketEvents.close:
133
- if (!this.closedByUser) // failed to connect or connection lost, try to reconnect
134
- this.reconnect();
135
- break;
136
- case WebsocketEvents.open:
137
- this.retries = 0;
138
- this.backoff?.reset(); // reset backoff
139
- this.buffer?.forEach(this.send.bind(this)); // send all buffered messages
140
- this.buffer?.clear();
141
- break;
292
+ return this._underlyingWebsocket;
293
+ }
294
+
295
+ /**
296
+ * Removes all event listeners from the browser-native websocket and closes it.
297
+ */
298
+ private clearWebsocket() {
299
+ this._underlyingWebsocket.removeEventListener(
300
+ WebsocketEvent.open,
301
+ this.handleOpenEvent,
302
+ );
303
+ this._underlyingWebsocket.removeEventListener(
304
+ WebsocketEvent.close,
305
+ this.handleCloseEvent,
306
+ );
307
+ this._underlyingWebsocket.removeEventListener(
308
+ WebsocketEvent.error,
309
+ this.handleErrorEvent,
310
+ );
311
+ this._underlyingWebsocket.removeEventListener(
312
+ WebsocketEvent.message,
313
+ this.handleMessageEvent,
314
+ );
315
+ this._underlyingWebsocket.close();
316
+ }
317
+
318
+ /**
319
+ * Handles the 'open' event of the browser-native websocket.
320
+ * @param event to handle.
321
+ */
322
+ private handleOpenEvent = (event: Event) =>
323
+ this.handleEvent(WebsocketEvent.open, event);
324
+
325
+ /**
326
+ * Handles the 'error' event of the browser-native websocket.
327
+ * @param event to handle.
328
+ */
329
+ private handleErrorEvent = (event: Event) =>
330
+ this.handleEvent(WebsocketEvent.error, event);
331
+
332
+ /**
333
+ * Handles the 'close' event of the browser-native websocket.
334
+ * @param event to handle.
335
+ */
336
+ private handleCloseEvent = (event: CloseEvent) =>
337
+ this.handleEvent(WebsocketEvent.close, event);
338
+
339
+ /**
340
+ * Handles the 'message' event of the browser-native websocket.
341
+ * @param event to handle.
342
+ */
343
+ private handleMessageEvent = (event: MessageEvent) =>
344
+ this.handleEvent(WebsocketEvent.message, event);
345
+
346
+ /**
347
+ * Dispatch an event to all listeners of the given event-type.
348
+ *
349
+ * @param type of the event to dispatch.
350
+ * @param event to dispatch.
351
+ */
352
+ private dispatchEvent<K extends WebsocketEvent>(
353
+ type: K,
354
+ event: WebsocketEventMap[K],
355
+ ) {
356
+ const eventListeners: WebsocketEventListeners[K] =
357
+ this._options.listeners[type];
358
+ const newEventListeners: WebsocketEventListeners[K] = [];
359
+
360
+ eventListeners.forEach(({ listener, options }) => {
361
+ listener(this, event); // invoke listener with event
362
+
363
+ if (
364
+ options === undefined ||
365
+ options.once === undefined ||
366
+ !options.once
367
+ ) {
368
+ newEventListeners.push({ listener, options }); // only keep listener if it isn't a once-listener
369
+ }
370
+ });
371
+
372
+ this._options.listeners[type] = newEventListeners; // replace old listeners with new listeners that don't include once-listeners
373
+ }
374
+
375
+ /**
376
+ * Handles the given event by dispatching it to all listeners of the given event-type.
377
+ *
378
+ * @param type of the event to handle.
379
+ * @param event to handle.
380
+ */
381
+ private handleEvent<K extends WebsocketEvent>(
382
+ type: K,
383
+ event: WebsocketEventMap[K],
384
+ ) {
385
+ switch (type) {
386
+ case WebsocketEvent.close:
387
+ this.dispatchEvent(type, event);
388
+ this.scheduleConnectionRetryIfNeeded(); // schedule a new connection retry if the websocket was closed by the server
389
+ break;
390
+
391
+ case WebsocketEvent.open:
392
+ if (this.backoff !== undefined && this._lastConnection !== undefined) {
393
+ // websocket was reconnected, dispatch reconnect event and reset backoff
394
+ const detail: ReconnectEventDetail = {
395
+ retries: this.backoff.retries,
396
+ lastConnection: new Date(this._lastConnection),
397
+ };
398
+ const event: CustomEvent<ReconnectEventDetail> =
399
+ new CustomEvent<ReconnectEventDetail>(WebsocketEvent.reconnect, {
400
+ detail,
401
+ });
402
+ this.dispatchEvent(WebsocketEvent.reconnect, event);
403
+ this.backoff.reset();
142
404
  }
143
- this.dispatchEvent<K>(type, ev); // forward to all listeners
405
+ this._lastConnection = new Date();
406
+ this.dispatchEvent(type, event); // dispatch open event and send buffered data
407
+ this.sendBufferedData();
408
+ break;
409
+
410
+ case WebsocketEvent.retry:
411
+ this.dispatchEvent(type, event); // dispatch retry event and try to connect
412
+ this.clearWebsocket(); // clear the old websocket
413
+ this.tryConnect();
414
+ break;
415
+
416
+ default:
417
+ this.dispatchEvent(type, event); // dispatch event to all listeners of the given event-type
418
+ break;
419
+ }
420
+ }
421
+
422
+ /**
423
+ * Sends buffered data if there is a buffer defined.
424
+ */
425
+ private sendBufferedData() {
426
+ if (this.buffer === undefined) {
427
+ return; // no buffer defined, nothing to send
428
+ }
429
+
430
+ for (
431
+ let ele = this.buffer.read();
432
+ ele !== undefined;
433
+ ele = this.buffer.read()
434
+ ) {
435
+ this.send(ele); // send buffered data
436
+ }
437
+ }
438
+
439
+ /**
440
+ * Schedules a connection-retry if there is a backoff defined and the websocket was not closed by the user.
441
+ */
442
+ private scheduleConnectionRetryIfNeeded() {
443
+ if (this.closedByUser) {
444
+ return; // user closed the websocket, no retry
445
+ }
446
+ if (this.backoff === undefined) {
447
+ return; // no backoff defined, no retry
144
448
  }
145
449
 
146
- private reconnect() {
147
- if (this.backoff === undefined) // no backoff, we're done
148
- return;
149
- const backoff = this.backoff.next();
150
- setTimeout(() => { // retry connection after waiting out the backoff-interval
151
- this.dispatchEvent(WebsocketEvents.retry, new CustomEvent<RetryEventDetails>(WebsocketEvents.retry,
152
- {
153
- detail: {
154
- retries: ++this.retries,
155
- backoff: backoff
156
- }
157
- })
158
- );
159
- this.tryConnect();
160
- }, backoff);
450
+ const handleRetryEvent = (detail: RetryEventDetail) => {
451
+ const event: CustomEvent<RetryEventDetail> = new CustomEvent(
452
+ WebsocketEvent.retry,
453
+ { detail },
454
+ );
455
+ this.handleEvent(WebsocketEvent.retry, event);
456
+ };
457
+
458
+ // create retry event detail, depending on the 'instantReconnect' option
459
+ const retryEventDetail: RetryEventDetail = {
460
+ backoff:
461
+ this._options.retry.instantReconnect === true ? 0 : this.backoff.next(),
462
+ retries:
463
+ this._options.retry.instantReconnect === true
464
+ ? 0
465
+ : this.backoff.retries,
466
+ lastConnection: this._lastConnection,
467
+ };
468
+
469
+ // schedule a new connection-retry if the maximum number of retries is not reached yet
470
+ if (
471
+ this._options.retry.maxRetries === undefined ||
472
+ retryEventDetail.retries <= this._options.retry.maxRetries
473
+ ) {
474
+ this.retryTimeout = window.setTimeout(
475
+ () => handleRetryEvent(retryEventDetail),
476
+ retryEventDetail.backoff,
477
+ );
161
478
  }
162
- }
479
+ }
480
+
481
+ /**
482
+ * Cancels the scheduled connection-retry, if there is one.
483
+ */
484
+ private cancelScheduledConnectionRetry() {
485
+ window.clearTimeout(this.retryTimeout);
486
+ }
487
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * A WebsocketBuffer is used to store messages temporarily until they can be sent.
3
+ */
4
+ export interface WebsocketBuffer<
5
+ E = string | ArrayBufferLike | Blob | ArrayBufferView,
6
+ > {
7
+ /**
8
+ * Adds an element to the buffer.
9
+ * @param element the element to add
10
+ */
11
+ add(element: E): void;
12
+
13
+ /**
14
+ * Reads an element from the buffer.
15
+ * @return an element from the buffer or undefined if the buffer is empty
16
+ */
17
+ read(): E | undefined;
18
+ }