github.com/mdaxf/iac@v0.0.0-20240519030858-58a061660378/integration/messagebus/glue/client/src/glue.js (about)

     1  /*
     2   *  Glue - Robust Go and Javascript Socket Library
     3   *  Copyright (C) 2015  Roland Singer <roland.singer[at]desertbit.com>
     4   *
     5   *  This program is free software: you can redistribute it and/or modify
     6   *  it under the terms of the GNU General Public License as published by
     7   *  the Free Software Foundation, either version 3 of the License, or
     8   *  (at your option) any later version.
     9   *
    10   *  This program is distributed in the hope that it will be useful,
    11   *  but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   *  GNU General Public License for more details.
    14   *
    15   *  You should have received a copy of the GNU General Public License
    16   *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   */
    18  
    19  var glue = function(host, options) {
    20      // Turn on strict mode.
    21      'use strict';
    22  
    23      // Include the dependencies.
    24      @@include('./emitter.js')
    25      @@include('./websocket.js')
    26      @@include('./ajaxsocket.js')
    27  
    28  
    29  
    30      /*
    31       * Constants
    32       */
    33  
    34      var Version         = "1.9.1",
    35          MainChannelName = "m";
    36  
    37      var SocketTypes = {
    38          WebSocket:  "WebSocket",
    39          AjaxSocket: "AjaxSocket"
    40      };
    41  
    42      var Commands = {
    43          Len: 	            2,
    44          Init:               'in',
    45          Ping:               'pi',
    46          Pong:               'po',
    47          Close: 	            'cl',
    48          Invalid:            'iv',
    49          DontAutoReconnect:  'dr',
    50          ChannelData:        'cd'
    51      };
    52  
    53      var States = {
    54          Disconnected:   "disconnected",
    55          Connecting:     "connecting",
    56          Reconnecting:   "reconnecting",
    57          Connected:      "connected"
    58      };
    59  
    60      var DefaultOptions = {
    61          // The base URL is appended to the host string. This value has to match with the server value.
    62          baseURL: "/glue/",
    63  
    64          // Force a socket type.
    65          // Values: false, "WebSocket", "AjaxSocket"
    66          forceSocketType: false,
    67  
    68          // Kill the connect attempt after the timeout.
    69          connectTimeout:  10000,
    70  
    71          // If the connection is idle, ping the server to check if the connection is stil alive.
    72          pingInterval:           35000,
    73          // Reconnect if the server did not response with a pong within the timeout.
    74          pingReconnectTimeout:   5000,
    75  
    76          // Whenever to automatically reconnect if the connection was lost.
    77          reconnect:          true,
    78          reconnectDelay:     1000,
    79          reconnectDelayMax:  5000,
    80          // To disable set to 0 (endless).
    81          reconnectAttempts:  10,
    82  
    83          // Reset the send buffer after the timeout.
    84          resetSendBufferTimeout: 10000
    85      };
    86  
    87  
    88  
    89      /*
    90       * Variables
    91       */
    92  
    93      var emitter                 = new Emitter,
    94          bs                      = false,
    95          mainChannel,
    96          initialConnectedOnce    = false,    // If at least one successful connection was made.
    97          bsNewFunc,                          // Function to create a new backend socket.
    98          currentSocketType,
    99          currentState            = States.Disconnected,
   100          reconnectCount          = 0,
   101          autoReconnectDisabled   = false,
   102          connectTimeout          = false,
   103          pingTimeout             = false,
   104          pingReconnectTimeout    = false,
   105          sendBuffer              = [],
   106          resetSendBufferTimeout  = false,
   107          resetSendBufferTimedOut = false,
   108          isReady                 = false,    // If true, the socket is initialized and ready.
   109          beforeReadySendBuffer   = [],       // Buffer to hold requests for the server while the socket is not ready yet.
   110          socketID               = "";
   111  
   112  
   113      /*
   114       * Include the dependencies
   115       */
   116  
   117      // Exported helper methods for the dependencies.
   118      var closeSocket, send, sendBuffered;
   119  
   120      @@include('./utils.js')
   121      @@include('./channel.js')
   122  
   123  
   124  
   125      /*
   126       * Methods
   127       */
   128  
   129      // Function variables.
   130      var reconnect, triggerEvent;
   131  
   132      // Sends the data to the server if a socket connection exists, otherwise it is discarded.
   133      // If the socket is not ready yet, the data is buffered until the socket is ready.
   134      send = function(data) {
   135          if (!bs) {
   136              return;
   137          }
   138  
   139          // If the socket is not ready yet, buffer the data.
   140          if (!isReady) {
   141              beforeReadySendBuffer.push(data);
   142              return;
   143          }
   144  
   145          // Send the data.
   146          bs.send(data);
   147      };
   148  
   149      // Hint: the isReady flag has to be true before calling this function!
   150      var sendBeforeReadyBufferedData = function() {
   151          // Skip if empty.
   152          if (beforeReadySendBuffer.length === 0) {
   153              return;
   154          }
   155  
   156          // Send the buffered data.
   157          for (var i = 0; i < beforeReadySendBuffer.length; i++) {
   158              send(beforeReadySendBuffer[i]);
   159          }
   160  
   161          // Clear the buffer.
   162          beforeReadySendBuffer = [];
   163      };
   164  
   165      var stopResetSendBufferTimeout = function() {
   166          // Reset the flag.
   167          resetSendBufferTimedOut = false;
   168  
   169          // Stop the timeout timer if present.
   170          if (resetSendBufferTimeout !== false) {
   171              clearTimeout(resetSendBufferTimeout);
   172              resetSendBufferTimeout = false;
   173          }
   174      };
   175  
   176      var startResetSendBufferTimeout = function() {
   177          // Skip if already running or if already timed out.
   178          if (resetSendBufferTimeout !== false || resetSendBufferTimedOut) {
   179              return;
   180          }
   181  
   182          // Start the timeout.
   183          resetSendBufferTimeout = setTimeout(function() {
   184              // Update the flags.
   185              resetSendBufferTimeout = false;
   186              resetSendBufferTimedOut = true;
   187  
   188              // Return if already empty.
   189              if (sendBuffer.length === 0) {
   190                  return;
   191              }
   192  
   193              // Call the discard callbacks if defined.
   194              var buf;
   195              for (var i = 0; i < sendBuffer.length; i++) {
   196                  buf = sendBuffer[i];
   197                  if (buf.discardCallback && utils.isFunction(buf.discardCallback)) {
   198                      try {
   199                          buf.discardCallback(buf.data);
   200                      }
   201                      catch (err) {
   202                         console.log("glue: failed to call discard callback: " + err.message);
   203                      }
   204                  }
   205              }
   206  
   207              // Trigger the event if any buffered send data is discarded.
   208              triggerEvent("discard_send_buffer");
   209  
   210              // Reset the buffer.
   211              sendBuffer = [];
   212          }, options.resetSendBufferTimeout);
   213      };
   214  
   215      var sendDataFromSendBuffer = function() {
   216          // Stop the reset send buffer tiemout.
   217          stopResetSendBufferTimeout();
   218  
   219          // Skip if empty.
   220          if (sendBuffer.length === 0) {
   221              return;
   222          }
   223  
   224          // Send data, which could not be send...
   225          var buf;
   226          for (var i = 0; i < sendBuffer.length; i++) {
   227              buf = sendBuffer[i];
   228              send(buf.cmd + buf.data);
   229          }
   230  
   231          // Clear the buffer again.
   232          sendBuffer = [];
   233      };
   234  
   235      // Send data to the server.
   236      // This is a helper method which handles buffering,
   237      // if the socket is currently not connected.
   238      // One optional discard callback can be passed.
   239      // It is called if the data could not be send to the server.
   240      // The data is passed as first argument to the discard callback.
   241      // returns:
   242      //  1 if immediately send,
   243      //  0 if added to the send queue and
   244      //  -1 if discarded.
   245      sendBuffered = function(cmd, data, discardCallback) {
   246          // Be sure, that the data value is an empty
   247          // string if not passed to this method.
   248          if (!data) {
   249              data = "";
   250          }
   251  
   252          // Add the data to the send buffer if disconnected.
   253          // They will be buffered for a short timeout to bridge short connection errors.
   254          if (!bs || currentState !== States.Connected) {
   255              // If already timed out, then call the discard callback and return.
   256              if (resetSendBufferTimedOut) {
   257                  if (discardCallback && utils.isFunction(discardCallback)) {
   258                      discardCallback(data);
   259                  }
   260  
   261                  return -1;
   262              }
   263  
   264              // Reset the send buffer after a specific timeout.
   265              startResetSendBufferTimeout();
   266  
   267              // Append to the buffer.
   268              sendBuffer.push({
   269                  cmd:                cmd,
   270                  data:               data,
   271                  discardCallback:    discardCallback
   272              });
   273  
   274              return 0;
   275          }
   276  
   277          // Send the data with the command to the server.
   278          send(cmd + data);
   279  
   280          return 1;
   281      };
   282  
   283      var stopConnectTimeout = function() {
   284          // Stop the timeout timer if present.
   285          if (connectTimeout !== false) {
   286              clearTimeout(connectTimeout);
   287              connectTimeout = false;
   288          }
   289      };
   290  
   291      var resetConnectTimeout = function() {
   292          // Stop the timeout.
   293          stopConnectTimeout();
   294  
   295          // Start the timeout.
   296          connectTimeout = setTimeout(function() {
   297              // Update the flag.
   298              connectTimeout = false;
   299  
   300              // Trigger the event.
   301              triggerEvent("connect_timeout");
   302  
   303              // Reconnect to the server.
   304              reconnect();
   305          }, options.connectTimeout);
   306      };
   307  
   308      var stopPingTimeout = function() {
   309          // Stop the timeout timer if present.
   310          if (pingTimeout !== false) {
   311              clearTimeout(pingTimeout);
   312              pingTimeout = false;
   313          }
   314  
   315          // Stop the reconnect timeout.
   316          if (pingReconnectTimeout !== false) {
   317              clearTimeout(pingReconnectTimeout);
   318              pingReconnectTimeout = false;
   319          }
   320      };
   321  
   322      var resetPingTimeout = function() {
   323          // Stop the timeout.
   324          stopPingTimeout();
   325  
   326          // Start the timeout.
   327          pingTimeout = setTimeout(function() {
   328              // Update the flag.
   329              pingTimeout = false;
   330  
   331              // Request a Pong response to check if the connection is still alive.
   332              send(Commands.Ping);
   333  
   334              // Start the reconnect timeout.
   335              pingReconnectTimeout = setTimeout(function() {
   336                  // Update the flag.
   337                  pingReconnectTimeout = false;
   338  
   339                  // Trigger the event.
   340                  triggerEvent("timeout");
   341  
   342                  // Reconnect to the server.
   343                  reconnect();
   344              }, options.pingReconnectTimeout);
   345          }, options.pingInterval);
   346      };
   347  
   348      var newBackendSocket = function() {
   349          // If at least one successfull connection was made,
   350          // then create a new socket using the last create socket function.
   351          // Otherwise determind which socket layer to use.
   352          if (initialConnectedOnce) {
   353              bs = bsNewFunc();
   354              return;
   355          }
   356  
   357          // Fallback to the ajax socket layer if there was no successful initial
   358          // connection and more than one reconnection attempt was made.
   359          if (reconnectCount > 1) {
   360              bsNewFunc = newAjaxSocket;
   361              bs = bsNewFunc();
   362              currentSocketType = SocketTypes.AjaxSocket;
   363              return;
   364          }
   365  
   366          // Choose the socket layer depending on the browser support.
   367          if ((!options.forceSocketType && window.WebSocket) ||
   368              options.forceSocketType === SocketTypes.WebSocket)
   369          {
   370              bsNewFunc = newWebSocket;
   371              currentSocketType = SocketTypes.WebSocket;
   372          }
   373          else
   374          {
   375              bsNewFunc = newAjaxSocket;
   376              currentSocketType = SocketTypes.AjaxSocket;
   377          }
   378  
   379          // Create the new socket.
   380          bs = bsNewFunc();
   381      };
   382  
   383      var initSocket = function(data) {
   384          // Parse the data JSON string to an object.
   385          data = JSON.parse(data);
   386  
   387          // Validate.
   388          // Close the socket and log the error on invalid data.
   389          if (!data.socketID) {
   390              closeSocket();
   391              console.log("glue: socket initialization failed: invalid initialization data received");
   392              return;
   393          }
   394  
   395          // Set the socket ID.
   396          socketID = data.socketID;
   397  
   398          // The socket initialization is done.
   399          // ##################################
   400  
   401          // Set the ready flag.
   402          isReady = true;
   403  
   404          // First send all data messages which were
   405          // buffered because the socket was not ready.
   406          sendBeforeReadyBufferedData();
   407  
   408          // Now set the state and trigger the event.
   409          currentState = States.Connected;
   410          triggerEvent("connected");
   411  
   412          // Send the queued data from the send buffer if present.
   413          // Do this after the next tick to be sure, that
   414          // the connected event gets fired first.
   415          setTimeout(sendDataFromSendBuffer, 0);
   416      };
   417  
   418      var connectSocket = function() {
   419          // Set a new backend socket.
   420          newBackendSocket();
   421  
   422          // Set the backend socket events.
   423          bs.onOpen = function() {
   424              // Stop the connect timeout.
   425              stopConnectTimeout();
   426  
   427              // Reset the reconnect count.
   428              reconnectCount = 0;
   429  
   430              // Set the flag.
   431              initialConnectedOnce = true;
   432  
   433              // Reset or start the ping timeout.
   434              resetPingTimeout();
   435  
   436              // Prepare the init data to be send to the server.
   437              var data = {
   438                  version: Version
   439              };
   440  
   441              // Marshal the data object to a JSON string.
   442              data = JSON.stringify(data);
   443  
   444              // Send the init data to the server with the init command.
   445              // Hint: the backend socket is used directly instead of the send function,
   446              // because the socket is not ready yet and this part belongs to the
   447              // initialization process.
   448              bs.send(Commands.Init + data);
   449          };
   450  
   451          bs.onClose = function() {
   452              // Reconnect the socket.
   453              reconnect();
   454          };
   455  
   456          bs.onError = function(msg) {
   457              // Trigger the error event.
   458              triggerEvent("error", [msg]);
   459  
   460              // Reconnect the socket.
   461              reconnect();
   462          };
   463  
   464          bs.onMessage = function(data) {
   465              // Reset the ping timeout.
   466              resetPingTimeout();
   467  
   468              // Log if the received data is too short.
   469              if (data.length < Commands.Len) {
   470                  console.log("glue: received invalid data from server: data is too short.");
   471                  return;
   472              }
   473  
   474              // Extract the command from the received data string.
   475              var cmd = data.substr(0, Commands.Len);
   476              data = data.substr(Commands.Len);
   477  
   478              if (cmd === Commands.Ping) {
   479                  // Response with a pong message.
   480                  send(Commands.Pong);
   481              }
   482              else if (cmd === Commands.Pong) {
   483                  // Don't do anything.
   484                  // The ping timeout was already reset.
   485              }
   486              else if (cmd === Commands.Invalid) {
   487                  // Log.
   488                  console.log("glue: server replied with an invalid request notification!");
   489              }
   490              else if (cmd === Commands.DontAutoReconnect) {
   491                  // Disable auto reconnections.
   492                  autoReconnectDisabled = true;
   493  
   494                  // Log.
   495                  console.log("glue: server replied with an don't automatically reconnect request. This might be due to an incompatible protocol version.");
   496              }
   497              else if (cmd === Commands.Init) {
   498                  initSocket(data);
   499              }
   500              else if (cmd === Commands.ChannelData) {
   501                  // Obtain the two values from the data string.
   502                  var v = utils.unmarshalValues(data);
   503                  if (!v) {
   504                      console.log("glue: server requested an invalid channel data request: " + data);
   505                      return;
   506                  }
   507  
   508                  // Trigger the event.
   509                  channel.emitOnMessage(v.first, v.second);
   510              }
   511              else {
   512                  console.log("glue: received invalid data from server with command '" + cmd + "' and data '" + data + "'!");
   513              }
   514          };
   515  
   516          // Connect during the next tick.
   517          // The user should be able to connect the event functions first.
   518          setTimeout(function() {
   519              // Set the state and trigger the event.
   520              if (reconnectCount > 0) {
   521                  currentState = States.Reconnecting;
   522                  triggerEvent("reconnecting");
   523              }
   524              else {
   525                  currentState = States.Connecting;
   526                  triggerEvent("connecting");
   527              }
   528  
   529              // Reset or start the connect timeout.
   530              resetConnectTimeout();
   531  
   532              // Connect to the server
   533              bs.open();
   534          }, 0);
   535      };
   536  
   537      var resetSocket = function() {
   538          // Stop the timeouts.
   539          stopConnectTimeout();
   540          stopPingTimeout();
   541  
   542          // Reset flags and variables.
   543          isReady = false;
   544          socketID = "";
   545  
   546          // Clear the buffer.
   547          // This buffer is attached to each single socket.
   548          beforeReadySendBuffer = [];
   549  
   550          // Reset previous backend sockets if defined.
   551          if (bs) {
   552              // Set dummy functions.
   553              // This will ensure, that previous old sockets don't
   554              // call our valid methods. This would mix things up.
   555              bs.onOpen = bs.onClose = bs.onMessage = bs.onError = function() {};
   556  
   557              // Reset everything and close the socket.
   558              bs.reset();
   559              bs = false;
   560          }
   561      };
   562  
   563      reconnect = function() {
   564          // Reset the socket.
   565          resetSocket();
   566  
   567          // If no reconnections should be made or more than max
   568          // reconnect attempts where made, trigger the disconnected event.
   569          if ((options.reconnectAttempts > 0 && reconnectCount > options.reconnectAttempts) ||
   570              options.reconnect === false || autoReconnectDisabled)
   571          {
   572              // Set the state and trigger the event.
   573              currentState = States.Disconnected;
   574              triggerEvent("disconnected");
   575  
   576              return;
   577          }
   578  
   579          // Increment the count.
   580          reconnectCount += 1;
   581  
   582          // Calculate the reconnect delay.
   583          var reconnectDelay = options.reconnectDelay * reconnectCount;
   584          if (reconnectDelay > options.reconnectDelayMax) {
   585              reconnectDelay = options.reconnectDelayMax;
   586          }
   587  
   588          // Try to reconnect.
   589          setTimeout(function() {
   590              connectSocket();
   591          }, reconnectDelay);
   592      };
   593  
   594      closeSocket = function() {
   595          // Check if the socket exists.
   596          if (!bs) {
   597              return;
   598          }
   599  
   600          // Notify the server.
   601          send(Commands.Close);
   602  
   603          // Reset the socket.
   604          resetSocket();
   605  
   606          // Set the state and trigger the event.
   607          currentState = States.Disconnected;
   608          triggerEvent("disconnected");
   609      };
   610  
   611  
   612  
   613      /*
   614       * Initialize section
   615       */
   616  
   617      // Create the main channel.
   618      mainChannel = channel.get(MainChannelName);
   619  
   620      // Prepare the host string.
   621      // Use the current location if the host string is not set.
   622      if (!host) {
   623          host = window.location.protocol + "//" + window.location.host;
   624      }
   625      // The host string has to start with http:// or https://
   626      if (!host.match("^http://") && !host.match("^https://")) {
   627          console.log("glue: invalid host: missing 'http://' or 'https://'!");
   628          return;
   629      }
   630  
   631      // Merge the options with the default options.
   632      options = utils.extend({}, DefaultOptions, options);
   633  
   634      // The max value can't be smaller than the delay.
   635      if (options.reconnectDelayMax < options.reconnectDelay) {
   636          options.reconnectDelayMax = options.reconnectDelay;
   637      }
   638  
   639      // Prepare the base URL.
   640      // The base URL has to start and end with a slash.
   641      if (options.baseURL.indexOf("/") !== 0) {
   642          options.baseURL = "/" + options.baseURL;
   643      }
   644      if (options.baseURL.slice(-1) !== "/") {
   645          options.baseURL = options.baseURL + "/";
   646      }
   647  
   648      // Create the initial backend socket and establish a connection to the server.
   649      connectSocket();
   650  
   651  
   652  
   653      /*
   654       * Socket object
   655       */
   656  
   657      var socket = {
   658          // version returns the glue socket protocol version.
   659          version: function() {
   660              return Version;
   661          },
   662  
   663          // type returns the current used socket type as string.
   664          // Either "WebSocket" or "AjaxSocket".
   665          type: function() {
   666              return currentSocketType;
   667          },
   668  
   669          // state returns the current socket state as string.
   670          // Following states are available:
   671          //  - "disconnected"
   672          //  - "connecting"
   673          //  - "reconnecting"
   674          //  - "connected"
   675          state: function() {
   676              return currentState;
   677          },
   678  
   679          // socketID returns the socket's ID.
   680          // This is a cryptographically secure pseudorandom number.
   681          socketID: function() {
   682              return socketID;
   683          },
   684  
   685          // send a data string to the server.
   686          // One optional discard callback can be passed.
   687          // It is called if the data could not be send to the server.
   688          // The data is passed as first argument to the discard callback.
   689          // returns:
   690          //  1 if immediately send,
   691          //  0 if added to the send queue and
   692          //  -1 if discarded.
   693          send: function(data, discardCallback) {
   694              mainChannel.send(data, discardCallback);
   695          },
   696  
   697          // onMessage sets the function which is triggered as soon as a message is received.
   698          onMessage: function(f) {
   699              mainChannel.onMessage(f);
   700          },
   701  
   702          // on binds event functions to events.
   703          // This function is equivalent to jQuery's on method syntax.
   704          // Following events are available:
   705          //  - "connected"
   706          //  - "connecting"
   707          //  - "disconnected"
   708          //  - "reconnecting"
   709          //  - "error"
   710          //  - "connect_timeout"
   711          //  - "timeout"
   712          //  - "discard_send_buffer"
   713          on: function() {
   714              emitter.on.apply(emitter, arguments);
   715          },
   716  
   717          // Reconnect to the server.
   718          // This is ignored if the socket is not disconnected.
   719          // It will reconnect automatically if required.
   720          reconnect: function() {
   721              if (currentState !== States.Disconnected) {
   722                  return;
   723              }
   724  
   725              // Reset the reconnect count and the auto reconnect disabled flag.
   726              reconnectCount = 0;
   727              autoReconnectDisabled = false;
   728  
   729              // Reconnect the socket.
   730              reconnect();
   731          },
   732  
   733          // close the socket connection.
   734          close: function() {
   735              closeSocket();
   736          },
   737  
   738          // channel returns the given channel object specified by name
   739          // to communicate in a separate channel than the default one.
   740          channel: function(name) {
   741              return channel.get(name);
   742          }
   743      };
   744  
   745      // Define the function body of the triggerEvent function.
   746      triggerEvent = function() {
   747          emitter.emit.apply(emitter, arguments);
   748      };
   749  
   750      // Return the newly created socket.
   751      return socket;
   752  };