Skip to content

Commit 00632b4

Browse files
committed
Make channels and connections Promise aware for close operations
1 parent e753bdd commit 00632b4

File tree

8 files changed

+148
-26
lines changed

8 files changed

+148
-26
lines changed

src/internal/browser/browser-channel.js

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ import HeapBuffer from './browser-buf'
2121
import { newError } from '../../error'
2222
import { ENCRYPTION_OFF, ENCRYPTION_ON } from '../util'
2323

24+
// Just to be sure that these values are with us even after WebSocket is injected
25+
// for tests.
26+
const WS_CONNECTING = 0
27+
const WS_OPEN = 1
28+
const WS_CLOSING = 2
29+
const WS_CLOSED = 3
30+
2431
/**
2532
* Create a new WebSocketChannel to be used in web browsers.
2633
* @access private
@@ -131,13 +138,19 @@ export default class WebSocketChannel {
131138

132139
/**
133140
* Close the connection
134-
* @param {function} cb - Function to call on close.
141+
* @returns {Promise} A promise that will be resolved after channel is closed
135142
*/
136-
close (cb = () => null) {
137-
this._open = false
138-
this._clearConnectionTimeout()
139-
this._ws.close()
140-
this._ws.onclose = cb
143+
close () {
144+
return new Promise((resolve, reject) => {
145+
if (this._ws && this._ws.readyState != WS_CLOSED) {
146+
this._open = false
147+
this._clearConnectionTimeout()
148+
this._ws.onclose = () => resolve()
149+
this._ws.close()
150+
} else {
151+
resolve()
152+
}
153+
})
141154
}
142155

143156
/**
@@ -151,7 +164,7 @@ export default class WebSocketChannel {
151164
const webSocket = this._ws
152165

153166
return setTimeout(() => {
154-
if (webSocket.readyState !== WebSocket.OPEN) {
167+
if (webSocket.readyState !== WS_OPEN) {
155168
this._connectionTimeoutFired = true
156169
webSocket.close()
157170
}

src/internal/connection-channel.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -438,9 +438,9 @@ export default class ChannelConnection extends Connection {
438438

439439
/**
440440
* Call close on the channel.
441-
* @param {function} cb - Function to call on close.
441+
* @returns {Promise<void>} - A promise that will be resolved when the underlying channel is closed.
442442
*/
443-
close (cb = () => null) {
443+
close () {
444444
if (this._log.isDebugEnabled()) {
445445
this._log.debug(`${this} closing`)
446446
}
@@ -451,11 +451,10 @@ export default class ChannelConnection extends Connection {
451451
this._protocol.prepareToClose()
452452
}
453453

454-
this._ch.close(() => {
454+
return this._ch.close().then(() => {
455455
if (this._log.isDebugEnabled()) {
456456
this._log.debug(`${this} closed`)
457457
}
458-
cb()
459458
})
460459
}
461460

src/internal/connection-delegate.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,8 @@ export default class DelegateConnection extends Connection {
8383
return this._delegate.resetAndFlush()
8484
}
8585

86-
close (cb = () => null) {
87-
return this._delegate.close(cb)
86+
close () {
87+
return this._delegate.close()
8888
}
8989

9090
_release () {

src/internal/connection.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,10 @@ export default class Connection {
106106

107107
/**
108108
* Call close on the channel.
109-
* @param {function} cb - Function to call on close.
109+
* @returns {Promise<void>} - A promise that will be resolved when the connection is closed.
110+
*
110111
*/
111-
close (cb = () => null) {
112+
close () {
112113
throw new Error('not implemented')
113114
}
114115

src/internal/node/node-channel.js

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -334,16 +334,18 @@ export default class NodeChannel {
334334

335335
/**
336336
* Close the connection
337-
* @param {function} cb - Function to call on close.
337+
* @returns {Promise} A promise that will be resolved after channel is closed
338338
*/
339-
close (cb = () => null) {
340-
this._open = false
341-
if (this._conn) {
342-
this._conn.end()
343-
this._conn.removeListener('end', this._handleConnectionTerminated)
344-
this._conn.on('end', cb)
345-
} else {
346-
cb()
347-
}
339+
close () {
340+
return new Promise((resolve, reject) => {
341+
if (this._open) {
342+
this._open = false
343+
this._conn.removeListener('end', this._handleConnectionTerminated)
344+
this._conn.on('end', () => resolve())
345+
this._conn.end()
346+
} else {
347+
resolve()
348+
}
349+
})
348350
}
349351
}

test/browser/karma-firefox.conf.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ module.exports = function (config) {
3636
reporters: ['spec'],
3737
port: 9876, // karma web server port
3838
colors: true,
39-
logLevel: config.LOG_DEBUG,
39+
logLevel: config.LOG_ERROR,
4040
browsers: ['FirefoxHeadless'],
4141
autoWatch: false,
4242
singleRun: true,

test/internal/browser/browser-channel.test.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ import { setTimeoutMock } from '../timers-util'
2424
import { ENCRYPTION_OFF, ENCRYPTION_ON } from '../../../src/internal/util'
2525
import ServerAddress from '../../../src/internal/server-address'
2626

27+
const WS_CONNECTING = 0
28+
const WS_OPEN = 1
29+
const WS_CLOSING = 2
30+
const WS_CLOSED = 3
31+
2732
/* eslint-disable no-global-assign */
2833
describe('#unit WebSocketChannel', () => {
2934
let OriginalWebSocket
@@ -158,6 +163,44 @@ describe('#unit WebSocketChannel', () => {
158163
testWarningInMixedEnvironment(ENCRYPTION_OFF, 'https')
159164
})
160165

166+
it('should resolve close if websocket is already closed', () => {
167+
WebSocket = () => {
168+
return {
169+
readyState: WS_CLOSED
170+
}
171+
}
172+
173+
const address = ServerAddress.fromUrl('bolt://localhost:8989')
174+
const channelConfig = new ChannelConfig(address, {}, SERVICE_UNAVAILABLE)
175+
176+
const channel = new WebSocketChannel(channelConfig)
177+
178+
return expectAsync(channel.close()).toBeResolved()
179+
})
180+
181+
it('should resolve close when websocket is closed', () => {
182+
WebSocket = () => {
183+
const ws = {
184+
readyState: WS_OPEN,
185+
onclose: () => {}
186+
}
187+
188+
ws.close = () => {
189+
ws.readyState = WS_CLOSED
190+
ws.onclose()
191+
}
192+
193+
return ws
194+
}
195+
196+
const address = ServerAddress.fromUrl('bolt://localhost:8989')
197+
const channelConfig = new ChannelConfig(address, {}, SERVICE_UNAVAILABLE)
198+
199+
const channel = new WebSocketChannel(channelConfig)
200+
201+
return expectAsync(channel.close()).toBeResolved()
202+
})
203+
161204
function testFallbackToLiteralIPv6 (boltAddress, expectedWsAddress) {
162205
// replace real WebSocket with a function that throws when IPv6 address is used
163206
WebSocket = url => {
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* Copyright (c) 2002-2019 "Neo4j,"
3+
* Neo4j Sweden AB [http://neo4j.com]
4+
*
5+
* This file is part of Neo4j.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
import NodeChannel from '../../../src/internal/node/node-channel'
20+
import ChannelConfig from '../../../src/internal/channel-config'
21+
import { SERVICE_UNAVAILABLE } from '../../../src/error'
22+
import ServerAddress from '../../../src/internal/server-address'
23+
import { connect } from 'net'
24+
25+
describe('#unit NodeChannel', () => {
26+
it('should resolve close if websocket is already closed', () => {
27+
const address = ServerAddress.fromUrl('bolt://localhost:9999')
28+
const channelConfig = new ChannelConfig(address, {}, SERVICE_UNAVAILABLE)
29+
const channel = new NodeChannel(channelConfig)
30+
31+
// Modify the connection to be closed
32+
channel._open = false
33+
34+
return expectAsync(channel.close()).toBeResolved()
35+
})
36+
37+
it('should resolve close when websocket is connected', () => {
38+
const channel = createMockedChannel(true)
39+
40+
return expectAsync(channel.close()).toBeResolved()
41+
})
42+
})
43+
44+
function createMockedChannel (connected) {
45+
let endCallback = null
46+
const address = ServerAddress.fromUrl('bolt://localhost:9999')
47+
const channelConfig = new ChannelConfig(address, {}, SERVICE_UNAVAILABLE)
48+
const channel = new NodeChannel(channelConfig)
49+
const socket = {
50+
end: () => {
51+
channel._open = false
52+
endCallback()
53+
},
54+
removeListener: () => {},
55+
on: (key, cb) => {
56+
if (key === 'end') {
57+
endCallback = cb
58+
}
59+
}
60+
}
61+
channel._conn = socket
62+
channel._open = connected
63+
return channel
64+
}

0 commit comments

Comments
 (0)