TCPSocketClient.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. export class TCPSocketClient {
  2. constructor(remoteAddress, remotePort, options = {}) {
  3. this.socket = null;
  4. this.remoteAddress = remoteAddress;
  5. this.remotePort = remotePort;
  6. this.options = options;
  7. // internals
  8. this._readable = null;
  9. this._writable = null;
  10. this._reader = null;
  11. this._writer;
  12. // open and closed promise as fields
  13. this._isOpenedSettled = false;
  14. this._isClosedSettled = false;
  15. this._openedPromise = new Promise((resolve, reject) => {
  16. this._resolveOpened = (value) => {
  17. this._isOpenedSettled = true;
  18. resolve(value);
  19. };
  20. this._rejectOpened = (reason) => {
  21. this._isOpenedSettled = true;
  22. reject(reason);
  23. };
  24. });
  25. this._closedPromise = new Promise((resolve, reject) => {
  26. this._resolveClosed = (value) => {
  27. this._isClosedSettled = true;
  28. resolve(value);
  29. };
  30. this._rejectClosed = (reason) => {
  31. this._isClosedSettled = true;
  32. reject(reason);
  33. };
  34. });
  35. // event callbacks
  36. this.onOpen = null;
  37. this.onData = null;
  38. this.onClose = null;
  39. this.onError = null;
  40. }
  41. get opened() {
  42. return this._openedPromise;
  43. }
  44. get closed() {
  45. return this._closedPromise;
  46. }
  47. async connect(timeoutMs = 5000) {
  48. try {
  49. let timeoutID;
  50. // added timeout cause it seems to be standard
  51. const timeoutPromise = new Promise((_, reject) => {
  52. timeoutID = setTimeout(() => {
  53. console.log(`Connection to ${this.remoteAddress}:${this.remotePort} timed out after ${timeoutMs}ms`);
  54. reject(new Error("Connection timeout"));
  55. }, timeoutMs);
  56. });
  57. this.socket = new TCPSocket(this.remoteAddress, this.remotePort, this.options);
  58. // race between socket.opened and timeout
  59. const openInfo = await Promise.race([this.socket.opened, timeoutPromise]);
  60. clearTimeout(timeoutID);
  61. this._readable = openInfo.readable;
  62. this._writable = openInfo.writable;
  63. if (this.onData) {
  64. this._startReading();
  65. }
  66. if (this.onOpen) {
  67. this.onOpen(openInfo);
  68. }
  69. this._resolveOpened(openInfo);
  70. return openInfo;
  71. } catch (e) {
  72. this._rejectOpened(e);
  73. this._rejectClosed(e);
  74. if (this.onError) {
  75. this.onError(e);
  76. }
  77. throw e;
  78. }
  79. }
  80. async _startReading() {
  81. try {
  82. this._reader = this._readable.getReader();
  83. while (true) {
  84. const {value, done} = await this._reader.read();
  85. if (done) {
  86. // releaseLock() here
  87. this._reader.releaseLock();
  88. this._reader = null;
  89. if (this.onClose) {
  90. this.onClose();
  91. }
  92. break;
  93. }
  94. if (value && value.byteLength > 0) {
  95. if (this.onData) {
  96. this.onData(value);
  97. }
  98. }
  99. }
  100. } catch (e) {
  101. if (this._reader) {
  102. this._reader.releaseLock();
  103. this._reader = null;
  104. }
  105. if (this.onClose) {
  106. this.onClose();
  107. }
  108. }
  109. }
  110. async send(data) {
  111. if (!this._writable) {
  112. throw new Error(`Socket is not connected`);
  113. }
  114. try {
  115. this._writer = this._writable.getWriter();
  116. let buffer = data;
  117. // old: for text exchange test, can probably be removed
  118. if (typeof data === `string`) {
  119. const encoder = new TextEncoder();
  120. buffer = encoder.encode(data);
  121. }
  122. await this._writer.write(buffer);
  123. await this._writer.releaseLock();
  124. this._writer = null;
  125. return true;
  126. } catch (e) {
  127. if (this.onError) {
  128. this.onError(e);
  129. }
  130. throw e;
  131. }
  132. }
  133. async close() {
  134. if (!this.socket) {
  135. throw new Error(`Socket is not connected`);
  136. }
  137. try {
  138. // try to handle leftover locks if necessary, tho should have been handled in startReading's loop and send()
  139. if (this._reader) {
  140. this._reader.releaseLock();
  141. this._reader = null;
  142. }
  143. if (this._writer) {
  144. this._writer.releaseLock();
  145. this._writer = null;
  146. }
  147. // returning this before trying to handle leftover locks errs because close before releaseLock(). I thought I had made it so it'd take care of that but guess not
  148. // just try to release before fixes it
  149. if (this._isClosedSettled) {
  150. return this._closedPromise;
  151. }
  152. await this.socket.closed;
  153. if (this.onClose) {
  154. this.onClose();
  155. }
  156. this._resolveClosed();
  157. return this._closedPromise;
  158. } catch (e) {
  159. this._rejectClosed(e);
  160. if (this.onError) {
  161. this.onError(e);
  162. }
  163. throw e;
  164. }
  165. }
  166. }