Skip to content

Commit ac1a210

Browse files
wip
1 parent 45465f2 commit ac1a210

File tree

5 files changed

+71
-54
lines changed

5 files changed

+71
-54
lines changed

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -232,15 +232,15 @@ The default implementation forwards the `cookie` header.
232232
The `wsReconnect` option contains the configuration for the WebSocket reconnection feature; is an object with the following properties:
233233

234234
- `pingInterval`: The interval between ping messages in ms (default: `30_000`).
235-
- `maxReconnectionRetries`: The maximum number of reconnection attempts (default: `3`). The counter is reset when the connection is established.
235+
- `maxReconnectionRetries`: The maximum number of reconnection retries (`1` to `Infinity`, default: `Infinity`).
236236
- `reconnectInterval`: The interval between reconnection attempts in ms (default: `1_000`).
237237
- `reconnectDecay`: The decay factor for the reconnection interval (default: `1.5`).
238-
- `connectionTimeout`: The timeout for the connection in ms (default: `5_000`).
239-
- `reconnectOnClose`: Whether to reconnect on close, as long as the source connection is active (default: `false`).
238+
- `connectionTimeout`: The timeout for establishing the connection in ms (default: `5_000`).
239+
- `reconnectOnClose`: Whether to reconnect on close, as long as the connection from the related client to the proxy is active (default: `false`).
240240
- TODO logs option?
241241

242-
Reconnection feature detects and closes broken connections and reconnects automatically, see [how to detect and close broken connections](https://github.com/websockets/ws#how-to-detect-and-close-broken-connections); the mechanism is based on ping/pong messages.
243-
It verifies the connection status from the service to the target.
242+
Reconnection feature detects and closes broken connections and reconnects automatically, see [how to detect and close broken connections](https://github.com/websockets/ws#how-to-detect-and-close-broken-connections).
243+
The connection is considered broken if the target does not respond to the ping messages or no data is received from the target.
244244

245245
Example:
246246

index.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ function waitConnection (socket, write) {
4343
}
4444
}
4545

46-
// TODO merge with waitConnection
4746
function waitForConnection (target, timeout) {
4847
return new Promise((resolve, reject) => {
4948
const timeoutId = setTimeout(() => {
@@ -145,7 +144,7 @@ function proxyWebSocketsWithReconnection (logger, source, target, options, targe
145144
if (source.isAlive && (target.broken || options.reconnectOnClose)) {
146145
target.isAlive = false
147146
target.removeAllListeners()
148-
// TODO source.removeAllListeners()
147+
// TODO FIXME! source.removeAllListeners()
149148
reconnect(logger, source, options, targetParams)
150149
return
151150
}
@@ -185,11 +184,17 @@ function proxyWebSocketsWithReconnection (logger, source, target, options, targe
185184
/* c8 ignore stop */
186185

187186
// source WebSocket is already connected because it is created by ws server
188-
target.on('message', (data, binary) => source.send(data, { binary }))
187+
target.on('message', (data, binary) => {
188+
target.isAlive = true
189+
source.send(data, { binary })
190+
})
189191
/* c8 ignore start */
190192
target.on('ping', data => source.ping(data))
191193
/* c8 ignore stop */
192-
target.on('pong', data => source.pong(data))
194+
target.on('pong', data => {
195+
target.isAlive = true
196+
source.pong(data)
197+
})
193198
target.on('close', (code, reason) => {
194199
logger.warn({ target: targetParams.url, code, reason }, 'proxy ws target close event')
195200
close(code, reason)

src/options.js

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
'use strict'
22

33
const DEFAULT_PING_INTERVAL = 30_000
4-
const DEFAULT_MAX_RECONNECT_ATTEMPTS = 3
54
const DEFAULT_MAX_RECONNECTION_RETRIES = Infinity
65
const DEFAULT_RECONNECT_INTERVAL = 1_000
76
const DEFAULT_RECONNECT_DECAY = 1.5
@@ -21,10 +20,10 @@ function validateOptions (options) {
2120
}
2221
wsReconnect.pingInterval = wsReconnect.pingInterval ?? DEFAULT_PING_INTERVAL
2322

24-
if (wsReconnect.maxReconnectionRetries !== undefined && (typeof wsReconnect.maxReconnectionRetries !== 'number' || wsReconnect.maxReconnectionRetries < 0)) {
25-
throw new Error('wsReconnect.maxReconnectionRetries must be a non-negative number')
23+
if (wsReconnect.maxReconnectionRetries !== undefined && (typeof wsReconnect.maxReconnectionRetries !== 'number' || wsReconnect.maxReconnectionRetries < 1)) {
24+
throw new Error('wsReconnect.maxReconnectionRetries must be a number greater than or equal to 1')
2625
}
27-
wsReconnect.maxReconnectionRetries = wsReconnect.maxReconnectionRetries ?? DEFAULT_MAX_RECONNECT_ATTEMPTS
26+
wsReconnect.maxReconnectionRetries = wsReconnect.maxReconnectionRetries ?? DEFAULT_MAX_RECONNECTION_RETRIES
2827

2928
if (wsReconnect.reconnectInterval !== undefined && (typeof wsReconnect.reconnectInterval !== 'number' || wsReconnect.reconnectInterval < 100)) {
3029
throw new Error('wsReconnect.reconnectInterval (ms) must be a number greater than or equal to 100')
@@ -53,7 +52,6 @@ function validateOptions (options) {
5352
module.exports = {
5453
validateOptions,
5554
DEFAULT_PING_INTERVAL,
56-
DEFAULT_MAX_RECONNECT_ATTEMPTS,
5755
DEFAULT_MAX_RECONNECTION_RETRIES,
5856
DEFAULT_RECONNECT_INTERVAL,
5957
DEFAULT_RECONNECT_DECAY,

test/options.js

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,53 @@
22

33
const { test } = require('tap')
44
const { validateOptions } = require('../src/options')
5-
5+
const { DEFAULT_PING_INTERVAL, DEFAULT_MAX_RECONNECTION_RETRIES, DEFAULT_RECONNECT_INTERVAL, DEFAULT_RECONNECT_DECAY, DEFAULT_CONNECTION_TIMEOUT, DEFAULT_RECONNECT_ON_CLOSE } = require('../src/options')
66
test('validateOptions', (t) => {
77
const requiredOptions = {
88
upstream: 'someUpstream'
99
}
1010

11-
// TODO
11+
t.throws(() => validateOptions({}), 'upstream must be specified')
12+
13+
t.throws(() => validateOptions({ ...requiredOptions, wsReconnect: { pingInterval: -1 } }), 'wsReconnect.pingInterval must be a non-negative number')
14+
t.throws(() => validateOptions({ ...requiredOptions, wsReconnect: { pingInterval: '1' } }), 'wsReconnect.pingInterval must be a non-negative number')
15+
t.doesNotThrow(() => validateOptions({ ...requiredOptions, wsReconnect: { pingInterval: 1 } }))
16+
17+
t.throws(() => validateOptions({ ...requiredOptions, wsReconnect: { maxReconnectionRetries: 0 } }), 'wsReconnect.maxReconnectionRetries must be a number greater than or equal to 1')
18+
t.throws(() => validateOptions({ ...requiredOptions, wsReconnect: { maxReconnectionRetries: -1 } }), 'wsReconnect.maxReconnectionRetries must be a number greater than or equal to 1')
19+
t.throws(() => validateOptions({ ...requiredOptions, wsReconnect: { maxReconnectionRetries: '1' } }), 'wsReconnect.maxReconnectionRetries must be a number greater than or equal to 1')
20+
t.doesNotThrow(() => validateOptions({ ...requiredOptions, wsReconnect: { maxReconnectionRetries: 1 } }))
21+
22+
t.throws(() => validateOptions({ ...requiredOptions, wsReconnect: { reconnectInterval: 0 } }), 'wsReconnect.reconnectInterval (ms) must be a number greater than or equal to 100')
23+
t.throws(() => validateOptions({ ...requiredOptions, wsReconnect: { reconnectInterval: -1 } }), 'wsReconnect.reconnectInterval (ms) must be a number greater than or equal to 100')
24+
t.throws(() => validateOptions({ ...requiredOptions, wsReconnect: { reconnectInterval: '1' } }), 'wsReconnect.reconnectInterval (ms) must be a number greater than or equal to 100')
25+
t.doesNotThrow(() => validateOptions({ ...requiredOptions, wsReconnect: { reconnectInterval: 100 } }))
26+
27+
t.throws(() => validateOptions({ ...requiredOptions, wsReconnect: { reconnectDecay: 0 } }), 'wsReconnect.reconnectDecay must be a number greater than or equal to 1')
28+
t.throws(() => validateOptions({ ...requiredOptions, wsReconnect: { reconnectDecay: -1 } }), 'wsReconnect.reconnectDecay must be a number greater than or equal to 1')
29+
t.throws(() => validateOptions({ ...requiredOptions, wsReconnect: { reconnectDecay: '1' } }), 'wsReconnect.reconnectDecay must be a number greater than or equal to 1')
30+
t.doesNotThrow(() => validateOptions({ ...requiredOptions, wsReconnect: { reconnectDecay: 1 } }))
31+
32+
t.throws(() => validateOptions({ ...requiredOptions, wsReconnect: { connectionTimeout: -1 } }), 'wsReconnect.connectionTimeout must be a non-negative number')
33+
t.throws(() => validateOptions({ ...requiredOptions, wsReconnect: { connectionTimeout: '1' } }), 'wsReconnect.connectionTimeout must be a non-negative number')
34+
t.doesNotThrow(() => validateOptions({ ...requiredOptions, wsReconnect: { connectionTimeout: 1 } }))
35+
36+
t.throws(() => validateOptions({ ...requiredOptions, wsReconnect: { reconnectOnClose: '1' } }), 'wsReconnect.reconnectOnClose must be a boolean')
37+
t.doesNotThrow(() => validateOptions({ ...requiredOptions, wsReconnect: { reconnectOnClose: true } }))
38+
39+
t.doesNotThrow(() => validateOptions({ ...requiredOptions, wsReconnect: { pingInterval: 1, maxReconnectionRetries: 1, reconnectInterval: 100, reconnectDecay: 1, connectionTimeout: 1, reconnectOnClose: true } }))
40+
41+
t.equal(validateOptions({ ...requiredOptions, wsReconnect: { } }), {
42+
...requiredOptions,
43+
wsReconnect: {
44+
pingInterval: DEFAULT_PING_INTERVAL,
45+
maxReconnectionRetries: DEFAULT_MAX_RECONNECTION_RETRIES,
46+
reconnectInterval: DEFAULT_RECONNECT_INTERVAL,
47+
reconnectDecay: DEFAULT_RECONNECT_DECAY,
48+
connectionTimeout: DEFAULT_CONNECTION_TIMEOUT,
49+
reconnectOnClose: DEFAULT_RECONNECT_ON_CLOSE
50+
}
51+
})
1252

1353
t.end()
1454
})

test/ws-reconnect.js

Lines changed: 12 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ const pinoTest = require('pino-test')
1111
const pino = require('pino')
1212
const proxyPlugin = require('../')
1313

14-
function waitForLogMessage(loggerSpy, message, max = 100) {
14+
function waitForLogMessage (loggerSpy, message, max = 100) {
1515
return new Promise((resolve, reject) => {
1616
let count = 0
1717
const fn = (received) => {
18-
console.log(received)
18+
// console.log(received)
1919

2020
if (received.msg === message) {
2121
loggerSpy.off('data', fn)
@@ -31,7 +31,7 @@ function waitForLogMessage(loggerSpy, message, max = 100) {
3131
})
3232
}
3333

34-
async function createServices({ t, upstream, wsReconnectOptions, wsTargetOptions, wsServerOptions }) {
34+
async function createServices ({ t, upstream, wsReconnectOptions, wsTargetOptions, wsServerOptions }) {
3535
const targetServer = createServer()
3636
const targetWs = new WebSocket.Server({ server: targetServer, ...wsTargetOptions })
3737

@@ -70,7 +70,7 @@ async function createServices({ t, upstream, wsReconnectOptions, wsTargetOptions
7070
upstream
7171
}
7272
}
73-
/*
73+
7474
test('should use ping/pong to verify connection is alive - from source (server on proxy) to target', async (t) => {
7575
const wsReconnectOptions = { pingInterval: 100, reconnectInterval: 100, maxReconnectionRetries: 1 }
7676

@@ -110,8 +110,6 @@ test('should reconnect on broken connection', async (t) => {
110110
await waitForLogMessage(loggerSpy, 'proxy ws reconnected')
111111

112112
// TODO fix with source.removeAllListeners
113-
114-
t.end()
115113
})
116114

117115
test('should not reconnect after max retries', async (t) => {
@@ -140,48 +138,28 @@ test('should not reconnect after max retries', async (t) => {
140138
await waitForLogMessage(loggerSpy, 'proxy ws target close event')
141139
await waitForLogMessage(loggerSpy, 'proxy ws reconnect error')
142140
await waitForLogMessage(loggerSpy, 'proxy ws failed to reconnect! No more retries')
143-
144-
t.end()
145141
})
146-
*/
147-
148-
test('should not reconnect because of connection timeout', async (t) => {
149-
const wsReconnectOptions = { pingInterval: 150, reconnectInterval: 100, maxReconnectionRetries: 1, connectionTimeout: 100 }
150-
151-
const { target, loggerSpy } = await createServices({ t, wsReconnectOptions, wsTargetOptions: { autoPong: false } })
152142

153-
let breakConnection = true
143+
test('should reconnect on regular target connection close', async (t) => {
144+
const wsReconnectOptions = { pingInterval: 200, reconnectInterval: 100, maxReconnectionRetries: 1, reconnectOnClose: false }
154145

155-
target.ws.on('upgrade', (request, socket, head) => {
156-
console.log('upgrade')
157-
})
146+
const { target, loggerSpy } = await createServices({ t, wsReconnectOptions })
158147

159148
target.ws.on('connection', async (socket) => {
160149
socket.on('ping', async () => {
161-
// add latency to break the connection once
162-
if (breakConnection) {
163-
await wait(wsReconnectOptions.pingInterval * 2)
164-
breakConnection = false
165-
}
166150
socket.pong()
167151
})
168-
})
169-
170-
await waitForLogMessage(loggerSpy, 'proxy ws connection is broken')
171152

172-
target.ws.close()
173-
target.server.close()
153+
await wait(1_000)
154+
socket.close()
155+
})
174156

175157
await waitForLogMessage(loggerSpy, 'proxy ws target close event')
176-
await waitForLogMessage(loggerSpy, 'proxy ws reconnect error')
177-
await waitForLogMessage(loggerSpy, 'proxy ws failed to reconnect! No more retries')
178-
179-
t.end()
158+
await waitForLogMessage(loggerSpy, 'proxy ws close link')
180159
})
181160

182-
// TODO reconnect regular close
183-
184161
/*
162+
TODO fix!
185163
test('should reconnect with retry', async (t) => {
186164
const wsReconnectOptions = { pingInterval: 150, reconnectInterval: 100, reconnectOnClose: true }
187165
@@ -211,9 +189,5 @@ test('should reconnect with retry', async (t) => {
211189
await waitForLogMessage(loggerSpy, 'proxy ws target close event')
212190
await waitForLogMessage(loggerSpy, 'proxy ws reconnect error')
213191
await waitForLogMessage(loggerSpy, 'proxy ws reconnected')
214-
215-
t.end()
216192
})
217193
*/
218-
219-
// TODO reconnectOnClose but close all on shutdown

0 commit comments

Comments
 (0)