1 /* 2 **************************************************************** 3 * Licensed Materials - Property of IBM 4 * 5725-F96 IBM MessageSight 5 * (C) Copyright IBM Corp. 2013, 2014. All Rights Reserved. 6 * 7 * US Government Users Restricted Rights - Use, duplication or 8 * disclosure restricted by GSA ADP Schedule Contract with IBM Corp. 9 **************************************************************** 10 */ 11 MessagingJSON = (function (global) { 12 13 var version = "1.0"; 14 15 /** 16 * Unique message type identifiers, with associated 17 * associated integer values. 18 * @private 19 */ 20 var MESSAGE_TYPE = { 21 Connect: 1, 22 Connected: 2, 23 Send: 3, 24 Ack: 4, 25 Subscribe: 5, 26 CloseSubscription: 6, 27 DestroySubscription: 7, 28 Ping: 8, 29 Pong: 9, 30 Close: 10 31 }; 32 33 /** 34 * Validate an object's parameter names to ensure they 35 * match a list of expected variables name for this option 36 * type. Used to ensure option object passed into the API don't 37 * contain erroneous parameters. 38 * @param {Object} obj - User options object 39 * @param {Object} keys - valid keys and types that may exist in obj. 40 * @throws {Error} Invalid option parameter found. 41 * @private 42 */ 43 var validate = function(obj, keys) { 44 for(key in obj) { 45 if (obj.hasOwnProperty(key)) { 46 if (keys.hasOwnProperty(key)) { 47 if (typeof obj[key] !== keys[key]) 48 throw new Error(format(ERROR.INVALID_TYPE, [typeof obj[key], key])); 49 } else { 50 var errorStr = "Unknown property, " + key + ". Valid properties are:"; 51 for (key in keys) 52 if (keys.hasOwnProperty(key)) 53 errorStr = errorStr+" "+key; 54 throw new Error(errorStr); 55 } 56 } 57 } 58 }; 59 60 /** 61 * Return a new function which runs the user function bound 62 * to a fixed scope. 63 * @param {function} User function 64 * @param {object} Function scope 65 * @return {function} User function bound to another scope 66 * @private 67 */ 68 var scope = function (f, scope) { 69 return function () { 70 return f.apply(scope, arguments); 71 }; 72 }; 73 74 /** 75 * Unique message type identifiers, with associated 76 * associated integer values. 77 * @private 78 */ 79 var ERROR = { 80 OK: {code:0, text:"OK."}, 81 CONNECT_TIMEOUT: {code:1, text:"Connect timed out."}, 82 SUBSCRIBE_TIMEOUT: {code:2, text:"Subscribe timed out."}, 83 UNSUBSCRIBE_TIMEOUT: {code:3, text:"Unsubscribe timed out."}, 84 PING_TIMEOUT: {code:4, text:"Ping timed out."}, 85 INTERNAL_ERROR: {code:5, text:"Internal error."}, 86 CONNACK_RETURNCODE: {code:6, text:"Bad Connack return code:{0} {1}."}, 87 SOCKET_ERROR: {code:7, text:"Socket error: {0}."}, 88 SOCKET_CLOSE: {code:8, text:"Socket closed."}, 89 MALFORMED_UTF: {code:9, text:"Malformed UTF data:{0} {1} {2}."}, 90 UNSUPPORTED: {code:10, text:"{0} is not supported by this browser."}, 91 INVALID_STATE: {code:11, text:"Invalid state {0}."}, 92 INVALID_TYPE: {code:12, text:"Invalid type {0} for {1}."}, 93 INVALID_ARGUMENT: {code:13, text:"Invalid argument {0} for {1}."}, 94 UNSUPPORTED_OPERATION: {code:14, text:"Unsupported operation."}, 95 INVALID_STORED_DATA: {code:15, text:"Invalid data in local storage key={0} value={1}."}, 96 INVALID_JSON_MESSAGE_TYPE: {code:16, text:"Invalid JSON message type {0}."}, 97 MALFORMED_UNICODE: {code:17, text:"Malformed Unicode string:{0} {1}."}, 98 SUBSCRIPTION_NOT_FOUND: {code:18, text:"A subscription for the topic {0} with the name {1} does not exist."}, 99 }; 100 101 /** 102 * Format an error message text. 103 * @private 104 * @param {error} ERROR.KEY value above. 105 * @param {substitutions} [array] substituted into the text. 106 * @return the text with the substitutions made. 107 */ 108 var format = function(error, substitutions) { 109 var text = error.text; 110 if (substitutions) { 111 for (var i=0; i<substitutions.length; i++) { 112 field = "{"+i+"}"; 113 start = text.indexOf(field); 114 if(start > 0) { 115 var part1 = text.substring(0,start); 116 var part2 = text.substring(start+field.length); 117 text = part1+substitutions[i]+part2; 118 } 119 } 120 } 121 return text; 122 }; 123 124 125 /** 126 * Repeat keepalive requests, monitor responses. 127 * @ignore 128 */ 129 var Pinger = function(client, window, keepAliveInterval) { 130 this._client = client; 131 this._window = window; 132 this._keepAliveInterval = keepAliveInterval*1000; 133 this.isReset = false; 134 135 var pingReq = { Action: "Ping" }; 136 137 this.pingReqMsg = JSON.stringify(pingReq); 138 139 var doTimeout = function (pinger) { 140 return function () { 141 return doPing.apply(pinger); 142 }; 143 }; 144 145 /** @ignore */ 146 var doPing = function() { 147 if (!this.isReset) { 148 this._client._trace("Pinger.doPing", "Timed out"); 149 this._client._disconnected( ERROR.PING_TIMEOUT.code , format(ERROR.PING_TIMEOUT)); 150 } else { 151 this.isReset = false; 152 this._client._trace("Pinger.doPing", this.pingReqMsg); 153 this._client.socket.send(this.pingReqMsg); 154 this.timeout = this._window.setTimeout(doTimeout(this), this._keepAliveInterval); 155 } 156 } 157 158 this.reset = function() { 159 this.isReset = true; 160 this._window.clearTimeout(this.timeout); 161 if (this._keepAliveInterval > 0) 162 this.timeout = setTimeout(doTimeout(this), this._keepAliveInterval); 163 } 164 165 this.cancel = function() { 166 this._window.clearTimeout(this.timeout); 167 } 168 }; 169 170 /** 171 * Monitor request completion. 172 * @ignore 173 */ 174 var Timeout = function(client, window, timeoutSeconds, action, args) { 175 this._window = window; 176 if (!timeoutSeconds) 177 timeoutSeconds = 30; 178 179 var doTimeout = function (action, client, args) { 180 return function () { 181 return action.apply(client, args); 182 }; 183 }; 184 this.timeout = setTimeout(doTimeout(action, client, args), timeoutSeconds * 1000); 185 186 this.cancel = function() { 187 this._window.clearTimeout(this.timeout); 188 } 189 }; 190 191 /* 192 * Internal implementation of the Websockets jsJSON V1.0 client. 193 * 194 * @name Messaging.ClientImpl @constructor 195 * @param {String} host the DNS nameof the webSocket host. 196 * @param {Number} port the port number for that host. 197 * @param {String} clientId the MQ client identifier. 198 */ 199 var ClientImpl = function (uri, host, port, path, clientId) { 200 // Check dependencies are satisfied in this browser. 201 if (!("WebSocket" in global && global["WebSocket"] !== null)) { 202 throw new Error(format(ERROR.UNSUPPORTED, ["WebSocket"])); 203 } 204 205 this.host = host; 206 this.port = port; 207 this.path = path; 208 this.uri = uri; 209 this.clientId = clientId; 210 211 }; 212 213 // Messaging Client public instance members. 214 ClientImpl.prototype.host; 215 ClientImpl.prototype.port; 216 ClientImpl.prototype.path; 217 ClientImpl.prototype.uri; 218 ClientImpl.prototype.clientId; 219 220 // Messaging Client private instance members. 221 ClientImpl.prototype.socket; 222 /* true once we have received an acknowledgment to a CONNECT packet. */ 223 ClientImpl.prototype.connected = false; 224 ClientImpl.prototype._connectTimeout; 225 ClientImpl.prototype.onMessageArrived; 226 ClientImpl.prototype.connectOptions; 227 ClientImpl.prototype._acksId = 1; 228 229 ClientImpl.prototype.receiveBuffer = null; 230 ClientImpl.prototype.connPinger = null; 231 ClientImpl.prototype.acks = null; 232 233 ClientImpl.prototype._traceBuffer = null; 234 ClientImpl.prototype._MAX_TRACE_ENTRIES = 100; 235 236 ClientImpl.prototype.connect = function (connectOptions) { 237 238 this._trace("Client.connect", connectOptions, this.connected); 239 if (this.connected) 240 throw new Error(format(ERROR.INVALID_STATE, ["connected state initialized"])); 241 if (this.socket) 242 throw new Error(format(ERROR.INVALID_STATE, ["socket already connected"])); 243 244 this.connectOptions = connectOptions; 245 this._doConnect(this.uri); 246 247 }; 248 249 ClientImpl.prototype.createTopicSubscription = function (filter, options) { 250 this._trace("Client.createTopicSubscription", filter, options); 251 252 if (!this.connected) 253 throw new Error(format(ERROR.INVALID_STATE, ["not connected"])); 254 255 // get a unique Ack ID 256 var id = this._acksId++; 257 this.acks[id] = {Type:MESSAGE_TYPE.Subscribe, Name:options.Name, Topic:filter, ID:id}; 258 // if there is an onFailure reference add it to the Ack entry 259 if (options.onFailure) { 260 this.acks[id].onFailure = options.onFailure; 261 } 262 263 if (options.onSuccess) { 264 this.acks[id].onSuccess = options.onSuccess; 265 } 266 267 // create the command object 268 var cmdObj = { Action: "Subscribe", Name:options.Name, Topic:filter, ID:id}; 269 var cmdString = JSON.stringify(cmdObj); 270 271 this._trace("sending JSON subscribe message...", cmdString); 272 this.socket.send(cmdString); 273 274 }; 275 276 ClientImpl.prototype.closeTopicSubscription = function (subscriptionName, options) { 277 this._trace("Client.closeSubscription", subscriptionName, options); 278 279 if (!this.connected) 280 throw new Error(format(ERROR.INVALID_STATE, ["not connected"])); 281 282 var id = this._acksId++; 283 284 // create the Ack entry... 285 this.acks[id] = {Type:MESSAGE_TYPE.CloseSubscription, Name:subscriptionName, ID:id}; 286 287 // if there is an onFailure reference add it to the Ack entry 288 if (options.onFailure) { 289 this.acks[id].onFailure = options.onFailure; 290 } 291 292 if (options.onSuccess) { 293 this.acks[id].onSuccess = options.onSuccess; 294 } 295 296 // create the cmd action... 297 var cmdObj = { Action: "CloseSubscription", ID:id, Name:subscriptionName}; 298 var cmdString = JSON.stringify(cmdObj); 299 this._trace("sending JSON close subscription message...", cmdString); 300 this.socket.send(cmdString); 301 302 }; 303 304 ClientImpl.prototype.send = function (message, options) { 305 this._trace("Client.send"); 306 307 if (!this.connected) 308 throw new Error(format(ERROR.INVALID_STATE, ["not connected"])); 309 310 var id = this._acksId++; 311 312 // create the Ack entry... 313 this.acks[id] = {Type:MESSAGE_TYPE.Send, Topic:message.destinationName, ID:id}; 314 315 // if there is an onFailure reference add it to the Ack entry 316 if (options.onFailure) { 317 this.acks[id].onFailure = options.onFailure; 318 } 319 320 if (options.onSuccess) { 321 this.acks[id].onSuccess = options.onSuccess; 322 } 323 324 var sendObj = {Action: "Send", Topic:message.destinationName, QoS:message.qos, ID:id, Body:message.payloadString }; 325 var msgString = JSON.stringify(sendObj); 326 327 this._trace("sending JSON publish", msgString); 328 this.socket.send(msgString); 329 330 331 }; 332 333 ClientImpl.prototype._processAck = function (ackObj) { 334 335 this._trace("Client._processAck"); 336 337 if (!this.acks[ackObj.ID]) { 338 this._trace("no ack entry found for the id: ", ackObj.ID); 339 return; 340 } 341 342 // look up the ack entry 343 var ackEntry = this.acks[ackObj.ID]; 344 var type = ackEntry.Type; 345 346 switch(type) { 347 case MESSAGE_TYPE.Subscribe: 348 if (ackObj.RC != 0) { 349 if (ackEntry.onFailure) { 350 ackEntry.onFailure(ackEntry.Name, ackEntry.Topic, ackObj); 351 } 352 } else if (ackEntry.onSuccess) { 353 ackEntry.onSuccess(ackEntry.Name, ackEntry.Topic); 354 } 355 break; 356 case MESSAGE_TYPE.CloseSubscription: 357 if (ackObj.RC != 0) { 358 if (ackEntry.onFailure) { 359 ackEntry.onFailure( ackEntry.Name, ackObj); 360 } 361 } else if (ackEntry.onSuccess) { 362 ackEntry.onSuccess(ackEntry.Name); 363 } 364 break; 365 case MESSAGE_TYPE.Send: 366 if (ackObj.RC != 0) { 367 if (ackEntry.onFailure) { 368 ackEntry.onFailure( ackEntry.Topic, ackObj); 369 } 370 } else if (ackEntry.onSuccess) { 371 ackEntry.onSuccess(ackEntry.Topic); 372 } 373 break; 374 default: 375 }; 376 377 // ack reference is no longer needed 378 delete this.acks[ackObj.ID]; 379 380 }; 381 382 ClientImpl.prototype.disconnect = function () { 383 this._trace("Client.disconnect"); 384 385 if (!this.socket) 386 throw new Error(format(ERROR.INVALID_STATE, ["not connecting or connected"])); 387 388 // try to close the connection gracefully 389 var msg = { Action: "Close" }; 390 391 var msgString = JSON.stringify(msg); 392 this._trace("sending JSON close message...", msgString); 393 this.socket.send(msgString); 394 395 if (this.socket) { 396 // Cancel all socket callbacks so that they cannot be driven again by this socket. 397 this.socket.onopen = null; 398 this.socket.onmessage = null; 399 this.socket.onerror = null; 400 this.socket.onclose = null; 401 if (this.socket.readyState === 1) 402 this.socket.close(); 403 delete this.socket; 404 } 405 this.connected = false; 406 407 if (this.onConnectionLost) 408 this.onConnectionLost(); 409 410 }; 411 412 ClientImpl.prototype.getTraceLog = function () { 413 if ( this._traceBuffer !== null ) { 414 return this._traceBuffer; 415 } 416 }; 417 418 ClientImpl.prototype.startTrace = function () { 419 if (this._traceBuffer) { 420 delete this._traceBuffer; 421 } 422 this._traceBuffer = []; 423 this._trace("Initializing trace...", new Date()); 424 }; 425 426 ClientImpl.prototype.stopTrace = function () { 427 delete this._traceBuffer; 428 }; 429 430 ClientImpl.prototype._doConnect = function (wsurl) { 431 432 this._trace("Client._doConnect", wsurl); 433 this.socket = new WebSocket(wsurl, "json-msg"); 434 this.socket.onopen = scope(this._on_socket_open, this); 435 this.socket.onmessage = scope(this._on_socket_message, this); 436 this.socket.onerror = scope(this._on_socket_error, this); 437 this.socket.onclose = scope(this._on_socket_close, this); 438 439 this.acks = new Object(); 440 441 // start a pinger for keep alive timeout 442 this.connPinger = new Pinger(this, window, this.connectOptions.keepAliveInterval); 443 444 this._connectTimeout = new Timeout(this, window, this.connectOptions.timeout, this._disconnected, [ERROR.CONNECT_TIMEOUT.code, format(ERROR.CONNECT_TIMEOUT)]); 445 446 }; 447 448 /** 449 * Called when the underlying websocket has been opened. 450 * @ignore 451 */ 452 ClientImpl.prototype._on_socket_open = function () { 453 console.log("SOCKET OPEN!!!"); 454 this._trace("Client._on_socket_open"); 455 456 // we have an open socket to the server - try to connect 457 var msg = { Action: "Connect", ClientID:this.clientId }; 458 if (this.connectOptions.userName) { 459 msg.User = this.connectOptions.userName; 460 } 461 if (this.connectOptions.password) { 462 msg.Password = this.connectOptions.password; 463 } 464 //if (this.connectOptions.keepAliveTimeout) { 465 msg.KeepAliveTimeout = 60; 466 //} 467 var msgString = JSON.stringify(msg); 468 this._trace("Sending connection request...", msgString); 469 this.socket.send(msgString); 470 471 }; 472 473 /** 474 * Called when the underlying websocket has received a complete packet. 475 * @ignore 476 */ 477 ClientImpl.prototype._on_socket_message = function (event) { 478 479 this._trace("Client._on_socket_message", event.data); 480 481 // create an object from the JSON string 482 var msgObj = JSON.parse(event.data); 483 var type = MESSAGE_TYPE[msgObj.Action]; 484 485 try { 486 switch(type) { 487 case MESSAGE_TYPE.Connected: 488 this.connected = true; 489 this._connectTimeout.cancel(); 490 if (this.connectOptions.onSuccess) { 491 this.connectOptions.onSuccess(); 492 } 493 break; 494 case MESSAGE_TYPE.Send: 495 if(this.onMessageArrived) { 496 this.onMessageArrived(msgObj); 497 } 498 break; 499 case MESSAGE_TYPE.Ack: 500 this._processAck(msgObj); 501 break; 502 case MESSAGE_TYPE.Close: 503 if (msgObj.RC != 0) { 504 if (this.onConnectionLost) { 505 this.onConnectionLost(msgObj.RC, msgObj.Reason); 506 } 507 } 508 break; 509 case MESSAGE_TYPE.Ping: 510 var msg = { Action: "Pong" }; 511 var msgString = JSON.stringify(msg); 512 this._trace("sending Pong message to server...", msgString); 513 this.socket.send(msgString); 514 break; 515 case MESSAGE_TYPE.Pong: 516 break; 517 default: 518 this._disconnected(ERROR.INVALID_JSON_MESSAGE_TYPE.code , format(ERROR.INVALID_JSON_MESSAGE_TYPE, [msgObj.Action])); 519 }; 520 521 } catch (error) { 522 this._disconnected(ERROR.INTERNAL_ERROR.code , format(ERROR.INTERNAL_ERROR, [error.message])); 523 return; 524 } 525 526 // Reset the receive ping timer, we now have evidence the server is alive. 527 this.connPinger.reset(); 528 529 }; 530 531 532 /** @ignore */ 533 ClientImpl.prototype._on_socket_error = function (error) { 534 535 var data = ""; 536 if (!error || !error.data) { 537 data = "Unknown connection error"; 538 } else { 539 data = error.data; 540 } 541 542 this._trace("Client._on_socket_error", data); 543 this._disconnected(ERROR.SOCKET_ERROR.code , format(ERROR.SOCKET_ERROR, [data])); 544 545 }; 546 547 /** @ignore */ 548 ClientImpl.prototype._on_socket_close = function () { 549 this._trace("Client._on_socket_close"); 550 this._disconnected(ERROR.SOCKET_CLOSE.code , format(ERROR.SOCKET_CLOSE)); 551 }; 552 553 554 /** 555 * Client has disconnected either at its own request or because the server 556 * or network disconnected it. Remove all non-durable state. 557 * @param {errorCode} [number] the error number. 558 * @param {errorText} [string] the error text. 559 * @ignore 560 */ 561 ClientImpl.prototype._disconnected = function (errorCode, errorText) { 562 this._trace("Client._disconnected", errorCode, errorText); 563 564 this.connPinger.cancel(); 565 if (this._connectTimeout) 566 this._connectTimeout.cancel(); 567 568 569 if (this.socket) { 570 571 try { 572 // try and disconnect gracefully... 573 var msg = { Action: "Close" }; 574 575 var msgString = JSON.stringify(msg); 576 this._trace("sending JSON close message...", msgString); 577 this.socket.send(msgString); 578 579 } catch (error) { 580 // nothing to do here.. 581 } 582 583 // Cancel all socket callbacks so that they cannot be driven again by this socket. 584 this.socket.onopen = null; 585 this.socket.onmessage = null; 586 this.socket.onerror = null; 587 this.socket.onclose = null; 588 if (this.socket.readyState === 1) 589 this.socket.close(); 590 delete this.socket; 591 592 } 593 594 if (errorCode === undefined) { 595 errorCode = ERROR.OK.code; 596 errorText = format(ERROR.OK); 597 } 598 599 // Run any application callbacks last as they may attempt to reconnect and hence create a new socket. 600 if (this.connected) { 601 this.connected = false; 602 // Execute the connectionLostCallback if there is one, and we were connected. 603 if (this.onConnectionLost) 604 this.onConnectionLost(errorCode, errorText); 605 } else { 606 // Otherwise we never had a connection, so indicate that the connect has failed. 607 if(this.connectOptions.onFailure) 608 this.connectOptions.onFailure({errorCode:errorCode, errorMessage:errorText}); 609 } 610 611 }; 612 613 /** @ignore */ 614 ClientImpl.prototype._trace = function () { 615 if ( this._traceBuffer !== null ) { 616 for (var i = 0, max = arguments.length; i < max; i++) { 617 if ( this._traceBuffer.length == this._MAX_TRACE_ENTRIES ) { 618 this._traceBuffer.shift(); 619 } 620 if (i === 0) this._traceBuffer.push(arguments[i]); 621 else if (typeof arguments[i] === "undefined" ) this._traceBuffer.push(arguments[i]); 622 else this._traceBuffer.push(" "+JSON.stringify(arguments[i])); 623 }; 624 }; 625 }; 626 627 628 // ------------------------------------------------------------------------ 629 // Public Programming interface. 630 // ------------------------------------------------------------------------ 631 632 /** 633 * The JavaScript application communicates with IBM MessageSight via the json_msg plug-in using a {@link MessagingJSON.Client} object. 634 * 635 * The send, subscribe and unsubscribe methods are implemented as asynchronous JavaScript methods 636 * (even though the underlying protocol exchange might be synchronous in nature). 637 * This means they signal their completion by calling back to the application 638 * via Success or Failure callback functions provided by the application on the method in question. 639 * These callbacks are called at most once per method invocation and do not persist beyond the lifetime 640 * of the script that made the invocation. 641 * <p> 642 * In contrast there are some callback functions, most notably <i>onMessageArrived</i>, 643 * that are defined on the {@link MessagingJSON.Client} object. 644 * These callbacks might be called multiple times, and are not directly related to specific method invocations made by the client. 645 * 646 * @name MessagingJSON.Client 647 * 648 * @constructor 649 * 650 * @param {string} host - the address of the IBM MessageSight host, as a fully qualified WebSocket URI, as a DNS name or dotted decimal IP address. 651 * @param {number} port - the port number to connect to - only required if host is not a URI 652 * @param {string} path - the path on the host to connect to - only used if host is not a URI. Default: '/json-msg'. 653 * @param {string} clientId - the MessagingJSON client identifier, between 0 and 65535 characters in length. 654 * 655 * @property {string} host - <i>read only</i> the IBM MessageSight DNS hostname or dotted decimal IP address. 656 * @property {number} port - <i>read only</i> the IBM MessageSight port. 657 * @property {string} path - <i>read only</i> the IBM MessageSight path. 658 * @property {string} clientId - <i>read only</i> used when connecting to IBM MessageSight. 659 */ 660 var Client = function (host, port, path, clientId) { 661 662 var uri; 663 664 if (typeof host !== "string") 665 throw new Error(format(ERROR.INVALID_TYPE, [typeof host, "host"])); 666 667 if (arguments.length == 2) { 668 clientId = port; 669 uri = host; 670 var match = uri.match(/^(wss?):\/\/((\[(.+)\])|([^\/]+?))(:(\d+))?(\/.*)$/); 671 if (match) { 672 host = match[4]||match[2]; 673 port = parseInt(match[7]); 674 path = match[8]; 675 } else { 676 throw new Error(format(ERROR.INVALID_ARGUMENT,[host,"host"])); 677 } 678 } else { 679 if (arguments.length == 3) { 680 clientId = path; 681 path = "/json-msg"; 682 } 683 if (typeof port !== "number" || port < 0) 684 throw new Error(format(ERROR.INVALID_TYPE, [typeof port, "port"])); 685 if (typeof path !== "string") 686 throw new Error(format(ERROR.INVALID_TYPE, [typeof path, "path"])); 687 688 var ipv6AddSBracket = (host.indexOf(":") != -1 && host.slice(0,1) != "[" && host.slice(-1) != "]"); 689 uri = "ws://"+(ipv6AddSBracket?"["+host+"]":host)+":"+port+path; 690 console.log("URI is " + uri); 691 } 692 693 var clientIdLength = 0; 694 for (var i = 0; i<clientId.length; i++) { 695 var charCode = clientId.charCodeAt(i); 696 if (0xD800 <= charCode && charCode <= 0xDBFF) { 697 i++; // Surrogate pair. 698 } 699 clientIdLength++; 700 } 701 if (typeof clientId !== "string" || clientIdLength > 65535) 702 throw new Error(format(ERROR.INVALID_ARGUMENT, [clientId, "clientId"])); 703 704 var client = new ClientImpl(uri, host, port, path, clientId); 705 this._getHost = function() { return host; }; 706 this._setHost = function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); }; 707 708 this._getPort = function() { return port; }; 709 this._setPort = function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); }; 710 711 this._getPath = function() { return path; }; 712 this._setPath = function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); }; 713 714 this._getURI = function() { return uri; }; 715 this._setURI = function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); }; 716 717 this._getClientId = function() { return client.clientId; }; 718 this._setClientId = function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); }; 719 720 this._getOnConnectionLost = function() { return client.onConnectionLost; }; 721 this._setOnConnectionLost = function(newOnConnectionLost) { 722 if (typeof newOnConnectionLost === "function") 723 client.onConnectionLost = newOnConnectionLost; 724 else 725 throw new Error(format(ERROR.INVALID_TYPE, [typeof newOnConnectionLost, "onConnectionLost"])); 726 }; 727 728 this._getOnMessageArrived = function() { return client.onMessageArrived; }; 729 this._setOnMessageArrived = function(newOnMessageArrived) { 730 if (typeof newOnMessageArrived === "function") 731 client.onMessageArrived = newOnMessageArrived; 732 else 733 throw new Error(format(ERROR.INVALID_TYPE, [typeof newOnMessageArrived, "onMessageArrived"])); 734 }; 735 736 /** 737 * Connect this MessagingJSON client to IBM MessageSight via the json_msg plug-in. 738 * 739 * @name MessagingJSON.Client#connect 740 * @function 741 * @param {Object} connectOptions - attributes used with the connection. 742 * @param {number} connectOptions.timeout - If the connect has not succeeded within this 743 * number of seconds, it is deemed to have failed. 744 * The default is 30 seconds. 745 * @param {string} connectOptions.userName - Authentication username for this connection. 746 * @param {string} connectOptions.password - Authentication password for this connection. 747 * @param {Number} connectOptions.keepAliveInterval - the interval in seconds to check if this 748 * client is still connected to IBM MessageSight - and that MessageSight can 749 * respond. 750 * @param {Number} connectOptions.keepAliveTimeout - the json_msg plug-in disconnects this client if 751 * there is no activity for this number of seconds. If not specified the 752 * server assumes 60 seconds. 753 * @param {boolean} connectOptions.useSSL - if present and true, use an SSL Websocket connection. 754 * @param {function} connectOptions.onSuccess - called when the connect acknowledgement 755 * has been received from IBM MessageSight. 756 * @config {function} [onFailure] called when the connect request has failed or timed out. 757 * A single response object parameter is passed to the onFailure callback 758 * containing the following fields: 759 * <ol> 760 * <li>errorCode a number indicating the nature of the error. 761 * <li>errorMessage text describing the error. 762 * </ol> 763 * @throws {InvalidState} if the client is not in disconnected state. The client must have received connectionLost 764 * or disconnected before calling connect for a second or subsequent time. 765 */ 766 this.connect = function (connectOptions) { 767 connectOptions = connectOptions || {} ; 768 validate(connectOptions, {timeout:"number", 769 userName:"string", 770 password:"string", 771 keepAliveInterval:"number", 772 keepAliveTimeout: "number", 773 useSSL:"boolean", 774 onSuccess:"function", 775 onFailure:"function"}); 776 777 // If no keep alive interval is set, assume 60 seconds. 778 if (connectOptions.keepAliveInterval === undefined) 779 connectOptions.keepAliveInterval = 60; 780 781 client.connect(connectOptions); 782 783 }; 784 785 /** 786 * Normal disconnect of this MessagingJSON client from IBM MessageSight. 787 * 788 * @name MessagingJSON.Client#disconnect 789 * @function 790 * @throws {InvalidState} if the client is already disconnected. 791 */ 792 this.disconnect = function () { 793 client.disconnect(); 794 }; 795 796 /** 797 * Subscribe for messages, request receipt of a copy of messages sent to the destinations described by the filter. 798 * 799 * @name MessagingJSON.Client#createTopicSubscription 800 * @function 801 * @param {string} filter describing the destinations to receive messages from. 802 * <br> 803 * @param {object} subscribeOptions - used to control the subscription 804 * 805 * @param {number} subscribeOptions.qos - the maiximum reliability of any publications sent 806 * as a result of making this subscription. Valid settings are: 807 * <dl> 808 * <dt>0 - at most once. 809 * <dt>1 - at least once. 810 * <dt>2 - exactly once. 811 * </dl> 812 * @param {function} subscribeOptions.onSuccess - called when the subscribe acknowledgement 813 * has been received from IBM MessageSight. 814 * @param {function} subscribeOptions.onFailure - called when the subscribe request has failed or timed out. 815 * A single response object parameter is passed to the onFailure callback containing the following fields: 816 * <ol> 817 * <li>errorCode - a number indicating the nature of the error. 818 * <li>errorMessage - text describing the error. 819 * </ol> 820 * @param {number} subscribeOptions.timeout - which, if present, determines the number of 821 * seconds after which the onFailure calback is called. 822 * The presence of a timeout does not prevent the onSuccess 823 * callback from being called when the subscribe completes. 824 * @throws {InvalidState} if the client is not in connected state. 825 */ 826 this.createTopicSubscription = function (filter, subscribeOptions) { 827 if (typeof filter !== "string") 828 throw new Error("Invalid argument:"+filter); 829 subscribeOptions = subscribeOptions || {} ; 830 validate(subscribeOptions, {QoS:"number", 831 Name: "string", 832 onSuccess:"function", 833 onFailure:"function", 834 timeout:"number" 835 }); 836 if (subscribeOptions.timeout && !subscribeOptions.onFailure) 837 throw new Error("subscribeOptions.timeout specified with no onFailure callback."); 838 if (typeof subscribeOptions.qos !== "undefined" 839 && !(subscribeOptions.qos === 0 || subscribeOptions.qos === 1 || subscribeOptions.qos === 2 )) 840 throw new Error(format(ERROR.INVALID_ARGUMENT, [subscribeOptions.qos, "subscribeOptions.qos"])); 841 client.createTopicSubscription(filter, subscribeOptions); 842 }; 843 844 /** 845 * Close a subscription identified by a subscrption name. 846 * 847 * @name MessagingJSON.Client#closeTopicSubscription 848 * @function 849 * @param {string} subscriptionName The name of the subscription to close. 850 * <br> 851 * @param {object} options - used to control the subscription 852 * 853 * @param {function} options.onSuccess - called when the subscribe acknowledgement 854 * has been received from IBM MessageSight. 855 * @param {function} options.onFailure - called when the subscribe request has failed or timed out. 856 * A single response object parameter is passed to the onFailure callback containing the following fields: 857 * <ol> 858 * <li>errorCode - a number indicating the nature of the error. 859 * <li>errorMessage - text describing the error. 860 * </ol> 861 * @param {number} options.timeout - which, if present, determines the number of 862 * seconds after which the onFailure calback is called. 863 * The presence of a timeout does not prevent the onSuccess 864 * callback from being called when the subscribe completes. 865 * @throws {InvalidState} if the client is not in connected state. 866 */ 867 this.closeTopicSubscription = function (subscriptionName, options) { 868 if (typeof subscriptionName !== "string") 869 throw new Error("Invalid argument:"+subscriptionName); 870 options = options || {} ; 871 validate(options, {onSuccess:"function", 872 onFailure:"function", 873 timeout:"number" 874 }); 875 if (options.timeout && !options.onFailure) 876 throw new Error("options.timeout specified with no onFailure callback."); 877 client.closeTopicSubscription(subscriptionName, options); 878 }; 879 880 /** 881 * Send a message to the consumers of the destination in the Message. 882 * 883 * @name MessagingJSON.Client#send 884 * @function 885 * @param {MessagingJSON.Message} message to send. 886 * @param {object} options - used to control the subscription 887 * 888 * @param {function} options.onSuccess - called when the subscribe acknowledgement 889 * has been received from IBM MessageSight. 890 * @param {function} options.onFailure - called when the subscribe request has failed or timed out. 891 * A single response object parameter is passed to the onFailure callback containing the following fields: 892 * <ol> 893 * <li>errorCode - a number indicating the nature of the error. 894 * <li>errorMessage - text describing the error. 895 * </ol> 896 * @param {number} options.timeout - which, if present, determines the number of 897 * seconds after which the onFailure calback is called. 898 * The presence of a timeout does not prevent the onSuccess 899 * callback from being called when the subscribe completes. 900 901 * @throws {InvalidState} if the client is not connected. 902 */ 903 this.send = function (message, options) { 904 if (!(message instanceof Message)) 905 throw new Error("Invalid argument:"+typeof message); 906 if (typeof message.destinationName === "undefined") 907 throw new Error("Invalid parameter Message.destinationName:"+message.destinationName); 908 options = options || {} ; 909 validate(options, {onSuccess:"function", 910 onFailure:"function", 911 timeout:"number" 912 }); 913 if (options.timeout && !options.onFailure) 914 throw new Error("options.timeout specified with no onFailure callback."); 915 916 client.send(message, options); 917 }; 918 919 /** 920 * Get the contents of the trace log. 921 * 922 * @name MessagingJSON.Client#getTraceLog 923 * @function 924 * @return {Object[]} tracebuffer containing the time ordered trace records. 925 */ 926 this.getTraceLog = function () { 927 return client.getTraceLog(); 928 } 929 930 /** 931 * Start tracing. 932 * 933 * @name MessagingJSON.Client#startTrace 934 * @function 935 */ 936 this.startTrace = function () { 937 client.startTrace(); 938 }; 939 940 /** 941 * Stop tracing. 942 * 943 * @name MessagingJSON.Client#stopTrace 944 * @function 945 */ 946 this.stopTrace = function () { 947 client.stopTrace(); 948 }; 949 950 }; 951 952 Client.prototype = { 953 954 get host() { return this._getHost(); }, 955 set host(newHost) { this._setHost(newHost); }, 956 957 get port() { return this._getPort(); }, 958 set port(newPort) { this._setPort(newPort); }, 959 960 get path() { return this._getPath(); }, 961 set path(newPath) { this._setPath(newPath); }, 962 963 get clientId() { return this._getClientId(); }, 964 set clientId(newClientId) { this._setClientId(newClientId); }, 965 966 get onConnectionLost() { return this._getOnConnectionLost(); }, 967 set onConnectionLost(newOnConnectionLost) { this._setOnConnectionLost(newOnConnectionLost); }, 968 969 get onMessageArrived() { return this._getOnMessageArrived(); }, 970 set onMessageArrived(newOnMessageArrived) { this._setOnMessageArrived(newOnMessageArrived); } 971 972 }; 973 974 /** 975 * An application message, sent or received. 976 * <p> 977 * All attributes may be null, which implies the default values. 978 * 979 * @name MessagingJSON.Message 980 * @constructor 981 * @param {String|ArrayBuffer} payload The message data to be sent. 982 * <p> 983 * @property {string} payloadString <i>read only</i> The payload as a string if the payload consists of valid UTF-8 characters. 984 * @property {ArrayBuffer} payloadBytes <i>read only</i> The payload as an ArrayBuffer. 985 * <p> 986 * @property {string} destinationName <b>mandatory</b> The name of the destination to which the message is to be sent 987 * (for messages about to be sent) or the name of the destination from which the message has been received. 988 * (for messages received by the onMessageArrived function). 989 * <p> 990 * @property {number} qos The reliability setting used to deliver the message. Applicable for destinations of type Topic only. 991 * <dl> 992 * <dt>0 Best effort (default). 993 * <dt>1 At least once. 994 * <dt>2 Exactly once. 995 * </dl> 996 * <p> 997 * 998 */ 999 var Message = function (newPayload, payloadEncoded) { 1000 1001 var payload; 1002 if ( typeof newPayload === "string" ) { 1003 payload = newPayload; 1004 } else { 1005 throw (format(ERROR.INVALID_ARGUMENT, [newPayload, "newPayload"])); 1006 } 1007 1008 this._getPayloadString = function () { 1009 return payload; 1010 }; 1011 1012 var encoded = false; 1013 if (payloadEncoded) { 1014 encoded = payloadEncoded; 1015 } 1016 this._getIsEncoded = function() { 1017 return encoded; 1018 }; 1019 1020 var destinationName = undefined; 1021 this._getDestinationName = function() { return destinationName; }; 1022 this._setDestinationName = function(newDestinationName) { 1023 if (typeof newDestinationName === "string") 1024 destinationName = newDestinationName; 1025 else 1026 throw new Error(format(ERROR.INVALID_ARGUMENT, [newDestinationName, "newDestinationName"])); 1027 }; 1028 1029 var qos = 0; 1030 this._getQos = function() { return qos; }; 1031 this._setQos = function(newQos) { 1032 if (newQos === 0 || newQos === 1 || newQos === 2 ) 1033 qos = newQos; 1034 else 1035 throw new Error("Invalid argument:"+newQos); 1036 }; 1037 1038 var destType = undefined; 1039 this._getDestinationType = function() { return destType; }; 1040 this._setDestinationType = function(newDestType) { 1041 if (newDestType === "Topic") 1042 destType = newDestType; 1043 else 1044 throw new Error("Invalid destination type: " + newDestType); 1045 }; 1046 1047 }; 1048 1049 Message.prototype = { 1050 get payloadString() { return this._getPayloadString(); }, 1051 1052 get isEncoded() { return this._getIsEncoded(); }, 1053 1054 TOPIC : "Topic", 1055 1056 get destinationName() { return this._getDestinationName(); }, 1057 set destinationName(newDestinationName) { this._setDestinationName(newDestinationName); }, 1058 1059 get qos() { return this._getQos(); }, 1060 set qos(newQos) { this._setQos(newQos); }, 1061 1062 get destinationType() { return this._getDestinationType(); }, 1063 set destinationType(newDestType) { this._setDestinationType(newDestType); }, 1064 1065 }; 1066 1067 1068 // Module contents. 1069 return { 1070 Client: Client, 1071 Message: Message 1072 }; 1073 1074 })(window); 1075