Skip to content

Commit dafe6f4

Browse files
akai07Apoorv
andauthored
fix: resolve allowHalfOpen behavior (#230)
Co-authored-by: Apoorv <[email protected]>
1 parent 86f43af commit dafe6f4

File tree

10 files changed

+225
-17
lines changed

10 files changed

+225
-17
lines changed

__tests__/allowHalfOpen.test.js

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { expect, test, jest } from '@jest/globals';
2+
import net from '../src/index';
3+
import { nativeEventEmitter } from '../src/Globals';
4+
import { NativeModules } from 'react-native';
5+
6+
const Sockets = NativeModules.TcpSockets;
7+
8+
jest.mock('../src/Globals', () => {
9+
const { EventEmitter } = require('events');
10+
const emitter = new EventEmitter();
11+
const originalAddListener = emitter.addListener.bind(emitter);
12+
// @ts-ignore
13+
emitter.addListener = (event, listener) => {
14+
originalAddListener(event, listener);
15+
return { remove: () => emitter.removeListener(event, listener) };
16+
};
17+
18+
let idCounter = 1000;
19+
return {
20+
__esModule: true,
21+
nativeEventEmitter: emitter,
22+
getNextId: () => idCounter++,
23+
};
24+
});
25+
26+
test('allowHalfOpen: false (default) should call Sockets.end() on end event', () => {
27+
return new Promise((resolve, reject) => {
28+
// Reset mocks
29+
Sockets.end.mockClear();
30+
31+
const server = net.createServer(); // allowHalfOpen default false
32+
// @ts-ignore
33+
const serverId = server._id;
34+
server.listen(12345);
35+
36+
server.on('connection', (socket) => {
37+
socket.on('end', () => {
38+
try {
39+
// When we receive 'end', if allowHalfOpen is false, socket.end() should be called
40+
// which calls Sockets.end(id)
41+
expect(Sockets.end).toHaveBeenCalled();
42+
resolve(undefined);
43+
} catch (e) {
44+
reject(e);
45+
}
46+
});
47+
});
48+
49+
// Simulate connection
50+
nativeEventEmitter.emit('connection', {
51+
id: serverId,
52+
info: {
53+
id: 456,
54+
connection: {
55+
localAddress: '127.0.0.1',
56+
localPort: 12345,
57+
remoteAddress: '127.0.0.1',
58+
remotePort: 54321,
59+
remoteFamily: 'IPv4',
60+
},
61+
},
62+
});
63+
64+
// Simulate 'end' event from native for socket 456
65+
nativeEventEmitter.emit('end', { id: 456 });
66+
});
67+
});
68+
69+
test('allowHalfOpen: true should NOT call Sockets.end() on end event', () => {
70+
return new Promise((resolve, reject) => {
71+
// Reset mocks
72+
Sockets.end.mockClear();
73+
74+
const server = net.createServer({ allowHalfOpen: true });
75+
// @ts-ignore
76+
const serverId = server._id;
77+
server.listen(12346);
78+
79+
server.on('connection', (socket) => {
80+
socket.on('end', () => {
81+
try {
82+
// When we receive 'end', if allowHalfOpen is true, socket.end() should NOT be called
83+
expect(Sockets.end).not.toHaveBeenCalled();
84+
resolve(undefined);
85+
} catch (e) {
86+
reject(e);
87+
}
88+
});
89+
});
90+
91+
// Simulate connection
92+
nativeEventEmitter.emit('connection', {
93+
id: serverId,
94+
info: {
95+
id: 457,
96+
connection: {
97+
localAddress: '127.0.0.1',
98+
localPort: 12346,
99+
remoteAddress: '127.0.0.1',
100+
remotePort: 54321,
101+
remoteFamily: 'IPv4',
102+
},
103+
},
104+
});
105+
106+
// Simulate 'end' event from native for socket 457
107+
nativeEventEmitter.emit('end', { id: 457 });
108+
});
109+
});

__tests__/server_options.test.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { expect, test, jest } from '@jest/globals';
2+
3+
jest.mock('../src/Globals', () => {
4+
const { EventEmitter } = require('events');
5+
const emitter = new EventEmitter();
6+
const originalAddListener = emitter.addListener.bind(emitter);
7+
// @ts-ignore
8+
emitter.addListener = (event, listener) => {
9+
originalAddListener(event, listener);
10+
return { remove: () => emitter.removeListener(event, listener) };
11+
};
12+
return {
13+
__esModule: true,
14+
nativeEventEmitter: emitter,
15+
getNextId: () => 123,
16+
};
17+
});
18+
19+
import net from '../src/index';
20+
import { nativeEventEmitter } from '../src/Globals';
21+
22+
test('server option pauseOnConnect should pause the socket', () => {
23+
return new Promise((resolve, reject) => {
24+
const server = net.createServer({ pauseOnConnect: true });
25+
26+
server.listen(12345);
27+
28+
server.on('connection', (socket) => {
29+
try {
30+
// Check if socket is paused
31+
// @ts-ignore
32+
expect(socket._paused).toBe(true);
33+
resolve(undefined);
34+
} catch (error) {
35+
reject(error);
36+
}
37+
});
38+
39+
nativeEventEmitter.emit('connection', {
40+
id: 123,
41+
info: {
42+
id: 456,
43+
connection: {
44+
localAddress: '127.0.0.1',
45+
localPort: 12345,
46+
remoteAddress: '127.0.0.1',
47+
remotePort: 54321,
48+
remoteFamily: 'IPv4',
49+
},
50+
},
51+
});
52+
});
53+
});

android/src/main/java/com/asterinet/react/tcpsocket/TcpEventListener.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@ public void onData(int id, byte[] data) {
9292
sendEvent("data", eventParams);
9393
}
9494

95+
public void onEnd(int id) {
96+
WritableMap eventParams = Arguments.createMap();
97+
eventParams.putInt("id", id);
98+
sendEvent("end", eventParams);
99+
}
100+
95101
public void onWritten(int id, int msgId, @Nullable Exception e) {
96102
String error = null;
97103
if (e != null) {

android/src/main/java/com/asterinet/react/tcpsocket/TcpSocketClient.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,8 @@ public void run() {
267267
if (bufferCount > 0) {
268268
receiverListener.onData(socketId, Arrays.copyOfRange(buffer, 0, bufferCount));
269269
} else if (bufferCount == -1) {
270-
clientSocket.destroy();
270+
receiverListener.onEnd(socketId);
271+
break;
271272
}
272273
}
273274
} catch (IOException | InterruptedException ioe) {

ios/TcpSocketClient.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ typedef enum RCTTCPError RCTTCPError;
4040
- (void)onSecureConnection:(TcpSocketClient *)client
4141
toClient:(NSNumber *)clientID;
4242
- (void)onData:(NSNumber *)clientID data:(NSData *)data;
43+
- (void)onEnd:(NSNumber *)clientID;
4344
- (void)onClose:(TcpSocketClient *)client withError:(NSError *)err;
4445
- (void)onError:(TcpSocketClient *)client withError:(NSError *)err;
4546
- (void)onWrittenData:(TcpSocketClient *)client msgId:(NSNumber *)msgId;

ios/TcpSocketClient.m

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -580,9 +580,9 @@ - (void)socket:(GCDAsyncSocket *)sock
580580
}
581581

582582
- (void)socketDidCloseReadStream:(GCDAsyncSocket *)sock {
583-
// TODO : investigate for half-closed sockets
584-
// for now close the stream completely
585-
[sock disconnect];
583+
if (_clientDelegate) {
584+
[_clientDelegate onEnd:[sock userData]];
585+
}
586586
}
587587

588588
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err {

ios/TcpSockets.m

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ @implementation TcpSockets {
2222
- (NSArray<NSString *> *)supportedEvents {
2323
return @[
2424
@"connect", @"listening", @"connection", @"secureConnection", @"data",
25-
@"close", @"error", @"written"
25+
@"close", @"error", @"written", @"end"
2626
];
2727
}
2828

@@ -294,7 +294,17 @@ - (void)onSocketConnection:(TcpSocketClient *)client
294294
- (void)onData:(NSNumber *)clientID data:(NSData *)data {
295295
NSString *base64String = [data base64EncodedStringWithOptions:0];
296296
[self sendEventWithName:@"data"
297-
body:@{@"id" : clientID, @"data" : base64String}];
297+
body:@{
298+
@"id" : clientID,
299+
@"data" : base64String
300+
}];
301+
}
302+
303+
- (void)onEnd:(NSNumber *)clientID {
304+
[self sendEventWithName:@"end"
305+
body:@{
306+
@"id" : clientID
307+
}];
298308
}
299309

300310
- (void)onClose:(NSNumber *)clientID withError:(NSError *)err {

jest.setup.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ jest.mock('react-native', () => {
1414
destroy: jest.fn(),
1515
write: jest.fn(),
1616
listen: jest.fn(),
17+
pause: jest.fn(),
18+
resume: jest.fn(),
19+
setKeepAlive: jest.fn(),
20+
setNoDelay: jest.fn(),
1721
},
1822
},
1923
NativeEventEmitter: NativeEventEmitter,

src/Server.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export default class Server extends EventEmitter {
3838
this._id = getNextId();
3939
/** @protected @readonly */
4040
this._eventEmitter = nativeEventEmitter;
41+
// console.log('Server eventEmitter:', this._eventEmitter);
4142
/** @private @type {Set<Socket>} */
4243
this._connections = new Set();
4344
/** @private */
@@ -249,6 +250,14 @@ export default class Server extends EventEmitter {
249250
const keepAliveDelay = this._serverOptions.keepAliveInitialDelay || 0;
250251
newSocket.setKeepAlive(this._serverOptions.keepAlive, keepAliveDelay);
251252
}
253+
254+
if (this._serverOptions.allowHalfOpen !== undefined) {
255+
newSocket.allowHalfOpen = this._serverOptions.allowHalfOpen;
256+
}
257+
258+
if (this._serverOptions.pauseOnConnect) {
259+
newSocket.pause();
260+
}
252261
}
253262

254263
return newSocket;

0 commit comments

Comments
 (0)