extra-native-websocket 0.3.1 → 0.3.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.
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "extra-native-websocket",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "",
5
5
  "keywords": [],
6
6
  "files": [
7
7
  "lib",
8
- "dist"
8
+ "src"
9
9
  ],
10
10
  "main": "lib/es2018/index.js",
11
11
  "types": "lib/es2018/index.d.ts",
@@ -0,0 +1,137 @@
1
+ import { assert } from '@blackglory/prelude'
2
+ import { WebSocketError } from './websocket-error'
3
+ import { Queue, Emitter } from '@blackglory/structures'
4
+
5
+ export enum BinaryType {
6
+ Blob
7
+ , ArrayBuffer
8
+ }
9
+
10
+ export enum State {
11
+ Closed
12
+ , Connecting
13
+ , Connected
14
+ , Closing
15
+ }
16
+
17
+ enum ReadyState {
18
+ CONNECTING = 0
19
+ , OPEN = 1
20
+ , CLOSING = 2
21
+ , CLOSED = 3
22
+ }
23
+
24
+ export class ExtraNativeWebSocket extends Emitter<{
25
+ open: [event: Event]
26
+ message: [event: MessageEvent]
27
+ error: [event: Event]
28
+ close: [event: CloseEvent]
29
+ }> {
30
+ private instance?: WebSocket
31
+ private binaryType: BinaryType = BinaryType.Blob
32
+ protected unsentMessages = new Queue<string | ArrayBufferLike | Blob | ArrayBufferView>()
33
+
34
+ constructor(private createWebSocket: () => WebSocket) {
35
+ super()
36
+ }
37
+
38
+ getState(): State {
39
+ if (this.instance) {
40
+ switch (this.instance.readyState) {
41
+ case ReadyState.CONNECTING: return State.Connecting
42
+ case ReadyState.OPEN: return State.Connected
43
+ case ReadyState.CLOSING: return State.Closing
44
+ case ReadyState.CLOSED: return State.Closed
45
+ default: throw new Error('Unknown state')
46
+ }
47
+ } else {
48
+ return State.Closed
49
+ }
50
+ }
51
+
52
+ getBinaryType(): BinaryType {
53
+ return this.binaryType
54
+ }
55
+
56
+ setBinaryType(val: BinaryType): void {
57
+ this.binaryType = val
58
+
59
+ if (this.instance) {
60
+ switch (val) {
61
+ case BinaryType.Blob:
62
+ this.instance.binaryType = 'blob'
63
+ break
64
+ case BinaryType.ArrayBuffer:
65
+ this.instance.binaryType = 'arraybuffer'
66
+ break
67
+ default: throw new Error('Unknown binary type')
68
+ }
69
+ }
70
+ }
71
+
72
+ /**
73
+ * @throws {WebSocketError}
74
+ */
75
+ connect(): Promise<void> {
76
+ return new Promise((resolve, reject) => {
77
+ assert(this.getState() === State.Closed, 'WebSocket is not closed')
78
+
79
+ const self = this
80
+ const ws = this.instance = this.createWebSocket()
81
+
82
+ ws.addEventListener('error', errorListener, { once: true })
83
+
84
+ ws.addEventListener('open', event => this.emit('open', event))
85
+ ws.addEventListener('message', event => this.emit('message', event))
86
+ ws.addEventListener('error', event => this.emit('error', event))
87
+ ws.addEventListener('close', event => this.emit('close', event))
88
+
89
+ this.setBinaryType(this.binaryType)
90
+
91
+ ws.addEventListener('open', openListener, { once: true })
92
+
93
+ function errorListener(event: Event): void {
94
+ ws.addEventListener('close', closeListener, { once: true })
95
+ }
96
+
97
+ function closeListener(event: CloseEvent): void {
98
+ reject(new WebSocketError(event.code, event.reason))
99
+ }
100
+
101
+ function openListener(event: Event): void {
102
+ ws.removeEventListener('error', errorListener)
103
+ ws.removeEventListener('close', closeListener)
104
+ for (let size = self.unsentMessages.size; size--;) {
105
+ self.send(self.unsentMessages.dequeue()!)
106
+ }
107
+ resolve()
108
+ }
109
+ })
110
+ }
111
+
112
+ close(code?: number, reason?: string): Promise<void> {
113
+ return new Promise<void>(resolve => {
114
+ assert(this.instance, 'WebSocket is not created')
115
+
116
+ switch (this.getState()) {
117
+ case State.Closed:
118
+ resolve()
119
+ break
120
+ case State.Closing:
121
+ this.instance.addEventListener('close', () => resolve(), { once: true })
122
+ break
123
+ default:
124
+ this.instance.addEventListener('close', () => resolve(), { once: true })
125
+ this.instance.close(code, reason)
126
+ }
127
+ })
128
+ }
129
+
130
+ send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void {
131
+ if (this.getState() === State.Connected) {
132
+ this.instance!.send(data)
133
+ } else {
134
+ this.unsentMessages.enqueue(data)
135
+ }
136
+ }
137
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './extra-native-websocket'
2
+ export * from './websocket-error'
3
+ export * from './utils'
@@ -0,0 +1,60 @@
1
+ import { ExtraNativeWebSocket, State } from '@src/extra-native-websocket'
2
+ import { calculateExponentialBackoffTimeout } from 'extra-timers'
3
+ import { pass } from '@blackglory/prelude'
4
+ import { delay } from 'extra-promise'
5
+ import { waitForFunction } from '@blackglory/wait-for'
6
+
7
+ export function autoReconnectWithExponentialBackOff(
8
+ ws: ExtraNativeWebSocket
9
+ , {
10
+ baseTimeout
11
+ , maxTimeout = Infinity
12
+ , factor = 2
13
+ , jitter = true
14
+ }: {
15
+ baseTimeout: number
16
+ maxTimeout?: number
17
+ factor?: number
18
+ jitter?: boolean
19
+ }
20
+ ): () => void {
21
+ const controller = new AbortController()
22
+
23
+ // Make sure the error listener is added, prevent crashes due to uncaught errors.
24
+ const removeErrorListener = ws.on('error', pass)
25
+ let removeCloseListener = ws.once('close', closeListener)
26
+
27
+ return () => {
28
+ controller.abort()
29
+ removeCloseListener()
30
+ removeErrorListener()
31
+ }
32
+
33
+ async function closeListener(): Promise<void> {
34
+ let retries = 0
35
+ while (true) {
36
+ if (controller.signal.aborted) return
37
+
38
+ await delay(calculateExponentialBackoffTimeout({
39
+ retries
40
+ , baseTimeout
41
+ , maxTimeout
42
+ , factor
43
+ , jitter
44
+ }))
45
+ if (controller.signal.aborted) return
46
+
47
+ try {
48
+ await waitForFunction(() => ws.getState() === State.Closed)
49
+ await ws.connect()
50
+ if (controller.signal.aborted) return
51
+
52
+ removeCloseListener = ws.once('close', closeListener)
53
+ break
54
+ } catch {
55
+ retries++
56
+ pass()
57
+ }
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,36 @@
1
+ import { ExtraNativeWebSocket, State } from '@src/extra-native-websocket'
2
+ import { delay } from 'extra-promise'
3
+ import { AbortController } from 'extra-abort'
4
+ import { pass } from '@blackglory/prelude'
5
+ import { waitForFunction } from '@blackglory/wait-for'
6
+
7
+ export function autoReconnect(ws: ExtraNativeWebSocket, timeout: number = 0): () => void {
8
+ const controller = new AbortController()
9
+
10
+ let removeCloseListener = ws.once('close', closeListener)
11
+
12
+ return () => {
13
+ controller.abort()
14
+ removeCloseListener()
15
+ }
16
+
17
+ async function closeListener(): Promise<void> {
18
+ while (true) {
19
+ if (controller.signal.aborted) return
20
+
21
+ await delay(timeout)
22
+ if (controller.signal.aborted) return
23
+
24
+ try {
25
+ await waitForFunction(() => ws.getState() === State.Closed)
26
+ await ws.connect()
27
+ if (controller.signal.aborted) return
28
+
29
+ removeCloseListener = ws.once('close', closeListener)
30
+ break
31
+ } catch {
32
+ pass()
33
+ }
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,2 @@
1
+ export * from './auto-reconnect'
2
+ export * from './auto-reconnect-with-exponential-back-off'
@@ -0,0 +1,17 @@
1
+ import { CustomError } from '@blackglory/errors'
2
+
3
+ export class WebSocketError extends CustomError {
4
+ readonly code: number
5
+ readonly reason: string
6
+
7
+ constructor(code: number, reason: string) {
8
+ if (reason) {
9
+ super(`${code}: ${reason}`)
10
+ } else {
11
+ super(`${code}`)
12
+ }
13
+
14
+ this.code = code
15
+ this.reason = reason
16
+ }
17
+ }