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 };