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
|
@@ -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,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,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
|
+
}
|