diff --git a/android/src/main/java/com/asterinet/react/tcpsocket/TcpEventListener.java b/android/src/main/java/com/asterinet/react/tcpsocket/TcpEventListener.java index aed1561..d95d00f 100644 --- a/android/src/main/java/com/asterinet/react/tcpsocket/TcpEventListener.java +++ b/android/src/main/java/com/asterinet/react/tcpsocket/TcpEventListener.java @@ -117,6 +117,12 @@ public void onClose(int id, Exception e) { sendEvent("close", eventParams); } + public void onEnd(int id) { + WritableMap eventParams = Arguments.createMap(); + eventParams.putInt("id", id); + sendEvent("end", eventParams); + } + public void onError(int id, Exception e) { Log.e(TcpSocketModule.TAG, "Exception on socket " + id, e); String error = e.getMessage(); diff --git a/android/src/main/java/com/asterinet/react/tcpsocket/TcpSocketClient.java b/android/src/main/java/com/asterinet/react/tcpsocket/TcpSocketClient.java index 6ef8269..4c59e25 100644 --- a/android/src/main/java/com/asterinet/react/tcpsocket/TcpSocketClient.java +++ b/android/src/main/java/com/asterinet/react/tcpsocket/TcpSocketClient.java @@ -255,7 +255,8 @@ public void run() { if (bufferCount > 0) { receiverListener.onData(socketId, Arrays.copyOfRange(buffer, 0, bufferCount)); } else if (bufferCount == -1) { - clientSocket.destroy(); + receiverListener.onEnd(socketId); + break; } } } catch (IOException | InterruptedException ioe) { diff --git a/ios/TcpSocketClient.h b/ios/TcpSocketClient.h index aa74c26..9cb183f 100644 --- a/ios/TcpSocketClient.h +++ b/ios/TcpSocketClient.h @@ -43,6 +43,7 @@ typedef enum RCTTCPError RCTTCPError; - (void)onClose:(TcpSocketClient *)client withError:(NSError *)err; - (void)onError:(TcpSocketClient *)client withError:(NSError *)err; - (void)onWrittenData:(TcpSocketClient *)client msgId:(NSNumber *)msgId; +- (void)onEnd:(NSNumber *)clientID; - (NSNumber *)getNextId; @end diff --git a/ios/TcpSocketClient.m b/ios/TcpSocketClient.m index ad52d6c..c07bfe5 100644 --- a/ios/TcpSocketClient.m +++ b/ios/TcpSocketClient.m @@ -579,9 +579,8 @@ - (void)socket:(GCDAsyncSocket *)sock } - (void)socketDidCloseReadStream:(GCDAsyncSocket *)sock { - // TODO : investigate for half-closed sockets - // for now close the stream completely - [sock disconnect]; + // Half-closed socket support + [_clientDelegate onEnd:_id]; } - (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err { diff --git a/ios/TcpSockets.m b/ios/TcpSockets.m index fc99b73..6282232 100644 --- a/ios/TcpSockets.m +++ b/ios/TcpSockets.m @@ -22,7 +22,7 @@ @implementation TcpSockets { - (NSArray *)supportedEvents { return @[ @"connect", @"listening", @"connection", @"secureConnection", @"data", - @"close", @"error", @"written" + @"close", @"error", @"written", @"end" ]; } @@ -217,6 +217,13 @@ - (void)onWrittenData:(TcpSocketClient *)client msgId:(NSNumber *)msgId { }]; } +- (void)onEnd:(NSNumber *)clientID { + [self sendEventWithName:@"end" + body:@{ + @"id" : clientID + }]; +} + - (void)onConnect:(TcpSocketClient *)client { GCDAsyncSocket *socket = [client getSocket]; [self sendEventWithName:@"connect" diff --git a/src/Server.js b/src/Server.js index af746cc..33147fe 100644 --- a/src/Server.js +++ b/src/Server.js @@ -249,6 +249,10 @@ export default class Server extends EventEmitter { const keepAliveDelay = this._serverOptions.keepAliveInitialDelay || 0; newSocket.setKeepAlive(this._serverOptions.keepAlive, keepAliveDelay); } + + if (this._serverOptions.allowHalfOpen !== undefined) { + newSocket.allowHalfOpen = this._serverOptions.allowHalfOpen; + } } return newSocket; diff --git a/src/Socket.js b/src/Socket.js index 6ed2141..a7638fd 100644 --- a/src/Socket.js +++ b/src/Socket.js @@ -95,6 +95,7 @@ export default class Socket extends EventEmitter { this.remoteAddress = undefined; this.remotePort = undefined; this.remoteFamily = undefined; + this.allowHalfOpen = false; // Default per Node.js spec this._registerEvents(); } @@ -457,6 +458,15 @@ export default class Socket extends EventEmitter { this.destroy(); this.emit('error', evt.error); }); + this._endListener = this._eventEmitter.addListener('end', (evt) => { + if (evt.id !== this._id) return; + this._readyState = 'readOnly'; // Can still write but not read + this.emit('end'); + // If allowHalfOpen is false, auto-close the writable side + if (!this.allowHalfOpen) { + this.end(); + } + }); this._closeListener = this._eventEmitter.addListener('close', (evt) => { if (evt.id !== this._id) return; this._setDisconnected(); @@ -479,6 +489,7 @@ export default class Socket extends EventEmitter { _unregisterEvents() { this._dataListener?.remove(); this._errorListener?.remove(); + this._endListener?.remove(); this._closeListener?.remove(); this._connectListener?.remove(); this._writtenListener?.remove();