1/* 2 * js_channel is a very lightweight abstraction on top of 3 * postMessage which defines message formats and semantics 4 * to support interactions more rich than just message passing 5 * js_channel supports: 6 * + query/response - traditional rpc 7 * + query/update/response - incremental async return of results 8 * to a query 9 * + notifications - fire and forget 10 * + error handling 11 * 12 * js_channel is based heavily on json-rpc, but is focused at the 13 * problem of inter-iframe RPC. 14 * 15 * Message types: 16 * There are 5 types of messages that can flow over this channel, 17 * and you may determine what type of message an object is by 18 * examining its parameters: 19 * 1. Requests 20 * + integer id 21 * + string method 22 * + (optional) any params 23 * 2. Callback Invocations (or just "Callbacks") 24 * + integer id 25 * + string callback 26 * + (optional) params 27 * 3. Error Responses (or just "Errors) 28 * + integer id 29 * + string error 30 * + (optional) string message 31 * 4. Responses 32 * + integer id 33 * + (optional) any result 34 * 5. Notifications 35 * + string method 36 * + (optional) any params 37 */ 38 39;var Channel = (function() { 40 "use strict"; 41 42 // current transaction id, start out at a random *odd* number between 1 and a million 43 // There is one current transaction counter id per page, and it's shared between 44 // channel instances. That means of all messages posted from a single javascript 45 // evaluation context, we'll never have two with the same id. 46 var s_curTranId = Math.floor(Math.random()*1000001); 47 48 // no two bound channels in the same javascript evaluation context may have the same origin, scope, and window. 49 // futher if two bound channels have the same window and scope, they may not have *overlapping* origins 50 // (either one or both support '*'). This restriction allows a single onMessage handler to efficiently 51 // route messages based on origin and scope. The s_boundChans maps origins to scopes, to message 52 // handlers. Request and Notification messages are routed using this table. 53 // Finally, channels are inserted into this table when built, and removed when destroyed. 54 var s_boundChans = { }; 55 56 // add a channel to s_boundChans, throwing if a dup exists 57 function s_addBoundChan(win, origin, scope, handler) { 58 function hasWin(arr) { 59 for (var i = 0; i < arr.length; i++) if (arr[i].win === win) return true; 60 return false; 61 } 62 63 // does she exist? 64 var exists = false; 65 66 67 if (origin === '*') { 68 // we must check all other origins, sadly. 69 for (var k in s_boundChans) { 70 if (!s_boundChans.hasOwnProperty(k)) continue; 71 if (k === '*') continue; 72 if (typeof s_boundChans[k][scope] === 'object') { 73 exists = hasWin(s_boundChans[k][scope]); 74 if (exists) break; 75 } 76 } 77 } else { 78 // we must check only '*' 79 if ((s_boundChans['*'] && s_boundChans['*'][scope])) { 80 exists = hasWin(s_boundChans['*'][scope]); 81 } 82 if (!exists && s_boundChans[origin] && s_boundChans[origin][scope]) 83 { 84 exists = hasWin(s_boundChans[origin][scope]); 85 } 86 } 87 if (exists) throw "A channel is already bound to the same window which overlaps with origin '"+ origin +"' and has scope '"+scope+"'"; 88 89 if (typeof s_boundChans[origin] != 'object') s_boundChans[origin] = { }; 90 if (typeof s_boundChans[origin][scope] != 'object') s_boundChans[origin][scope] = [ ]; 91 s_boundChans[origin][scope].push({win: win, handler: handler}); 92 } 93 94 function s_removeBoundChan(win, origin, scope) { 95 var arr = s_boundChans[origin][scope]; 96 for (var i = 0; i < arr.length; i++) { 97 if (arr[i].win === win) { 98 arr.splice(i,1); 99 } 100 } 101 if (s_boundChans[origin][scope].length === 0) { 102 delete s_boundChans[origin][scope]; 103 } 104 } 105 106 function s_isArray(obj) { 107 if (Array.isArray) return Array.isArray(obj); 108 else { 109 return (obj.constructor.toString().indexOf("Array") != -1); 110 } 111 } 112 113 // No two outstanding outbound messages may have the same id, period. Given that, a single table 114 // mapping "transaction ids" to message handlers, allows efficie
114nt routing of Callback, Error, and 115 // Response messages. Entries are added to this table when requests are sent, and removed when 116 // responses are received. 117 var s_transIds = { }; 118 119 // class singleton onMessage handler 120 // this function is registered once and all incoming messages route through here. This 121 // arrangement allows certain efficiencies, message data is only parsed once and dispatch 122 // is more efficient, especially for large numbers of simultaneous channels. 123 var s_onMessage = function(e) { 124 try { 125 var m = JSON.parse(e.data); 126 if (typeof m !== 'object' || m === null) throw "malformed"; 127 } catch(e) { 128 // just ignore any posted messages that do not consist of valid JSON 129 return; 130 } 131 132 var w = e.source; 133 var o = e.origin; 134 var s, i, meth; 135 136 if (typeof m.method === 'string') { 137 var ar = m.method.split('::'); 138 if (ar.length == 2) { 139 s = ar[0]; 140 meth = ar[1]; 141 } else { 142 meth = m.method; 143 } 144 } 145 146 if (typeof m.id !== 'undefined') i = m.id; 147 148 // w is message source window 149 // o is message origin 150 // m is parsed message 151 // s is message scope 152 // i is message id (or undefined) 153 // meth is unscoped method name 154 // ^^ based on these factors we can route the message 155 156 // if it has a method it's either a notification or a request, 157 // route using s_boundChans 158 if (typeof meth === 'string') { 159 var delivered = false; 160 if (s_boundChans[o] && s_boundChans[o][s]) { 161 for (var j = 0; j < s_boundChans[o][s].length; j++) { 162 if (s_boundChans[o][s][j].win === w) { 163 s_boundChans[o][s][j].handler(o, meth, m); 164 delivered = true; 165 break; 166 } 167 } 168 } 169 170 if (!delivered && s_boundChans['*'] && s_boundChans['*'][s]) { 171 for (var j = 0; j < s_boundChans['*'][s].length; j++) { 172 if (s_boundChans['*'][s][j].win === w) { 173 s_boundChans['*'][s][j].handler(o, meth, m); 174 break; 175 } 176 } 177 } 178 } 179 // otherwise it must have an id (or be poorly formed 180 else if (typeof i != 'undefined') { 181 if (s_transIds[i]) s_transIds[i](o, meth, m); 182 } 183 }; 184 185 // Setup postMessage event listeners 186 if (window.addEventListener) window.addEventListener('message', s_onMessage, false); 187 else if(window.attachEvent) window.attachEvent('onmessage', s_onMessage); 188 189 /* a messaging channel is constructed from a window and an origin. 190 * the channel will assert that all messages received over the 191 * channel match the origin 192 * 193 * Arguments to Channel.build(cfg): 194 * 195 * cfg.window - the remote window with which we'll communicate 196 * cfg.origin - the expected origin of the remote window, may be '*' 197 * which matches any origin 198 * cfg.scope - the 'scope' of messages. a scope string that is 199 * prepended to message names. local and remote endpoints 200 * of a single channel must agree upon scope. Scope may 201 * not contain double colons ('::'). 202 * cfg.debugOutput - A boolean value. If true and window.console.log is 203 * a function, then debug strings will be emitted to that 204 * function. 205 * cfg.debugOutput - A boolean value. If true and window.console.log is 206 * a function, then debug strings will be emitted to that 207 * function. 208 * cfg.postMessageObserver - A function that will be passed two arguments, 209 * an origin and a message. It will be passed these immediately 210 * before messages are posted. 211 * cfg.gotMessageObserver - A function that will be passed two arguments, 212 * an origin and a message. It will be passed these arguments 213 * immediately after they pass scope and origin checks, but before 214 * they are processed. 215 * cfg.onReady - A function that will be invoked when a channel becomes "ready",
216 * this occurs once both sides of the channel have been 217 * instantiated and an application level handshake is exchanged. 218 * the onReady function will be passed a single argument which is 219 * the channel object that was returned from build(). 220 */ 221 return { 222 build: function(cfg) { 223 var debug = function(m) { 224 if (cfg.debugOutput && window.console && window.console.log) { 225 // try to stringify, if it doesn't work we'll let javascript's built in toString do its magic 226 try { if (typeof m !== 'string') m = JSON.stringify(m); } catch(e) { } 227 console.log("["+chanId+"] " + m); 228 } 229 }; 230 231 /* browser capabilities check */ 232 if (!window.postMessage) throw("jschannel cannot run this browser, no postMessage"); 233 if (!window.JSON || !window.JSON.stringify || ! window.JSON.parse) { 234 throw("jschannel cannot run this browser, no JSON parsing/serialization"); 235 } 236 237 /* basic argument validation */ 238 if (typeof cfg != 'object') throw("Channel build invoked without a proper object argument"); 239 240 if (!cfg.window || !cfg.window.postMessage) throw("Channel.build() called without a valid window argument"); 241 242 /* we'd have to do a little more work to be able to run multiple channels that intercommunicate the same 243 * window... Not sure if we care to support that */ 244 if (window === cfg.window) throw("target window is same as present window -- not allowed"); 245 246 // let's require that the client specify an origin. if we just assume '*' we'll be 247 // propagating unsafe practices. that would be lame. 248 var validOrigin = false; 249 if (typeof cfg.origin === 'string') { 250 var oMatch; 251 if (cfg.origin === "*") validOrigin = true; 252 // allow valid domains under http and https. Also, trim paths off otherwise valid origins. 253 else if (null !== (oMatch = cfg.origin.match(/^https?:\/\/(?:[-a-zA-Z0-9_\.])+(?::\d+)?/))) { 254 cfg.origin = oMatch[0].toLowerCase(); 255 validOrigin = true; 256 } 257 } 258 259 if (!validOrigin) throw ("Channel.build() called with an invalid origin"); 260 261 if (typeof cfg.scope !== 'undefined') { 262 if (typeof cfg.scope !== 'string') throw 'scope, when specified, must be a string'; 263 if (cfg.scope.split('::').length > 1) throw "scope may not contain double colons: '::'"; 264 } 265 266 /* private variables */ 267 // generate a random and psuedo unique id for this channel 268 var chanId = (function () { 269 var text = ""; 270 var alpha = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; 271 for(var i=0; i < 5; i++) text += alpha.charAt(Math.floor(Math.random() * alpha.length)); 272 return text; 273 })(); 274 275 // registrations: mapping method names to call objects 276 var regTbl = { }; 277 // current oustanding sent requests 278 var outTbl = { }; 279 // current oustanding received requests 280 var inTbl = { }; 281 // are we ready yet? when false we will block outbound messages. 282 var ready = false; 283 var pendingQueue = [ ]; 284 285 var createTransaction = function(id,origin,callbacks) { 286 var shouldDelayReturn = false; 287 var completed = false; 288 289 return { 290 origin: origin, 291 invoke: function(cbName, v) { 292 // verify in table 293 if (!inTbl[id]) throw "attempting to invoke a callback of a nonexistent transaction: " + id; 294 // verify that the callback name is valid
295 var valid = false; 296 for (var i = 0; i < callbacks.length; i++) if (cbName === callbacks[i]) { valid = true; break; } 297 if (!valid) throw "request supports no such callback '" + cbName + "'"; 298 299 // send callback invocation 300 postMessage({ id: id, callback: cbName, params: v}); 301 }, 302 error: function(error, message) { 303 completed = true; 304 // verify in table 305 if (!inTbl[id]) throw "error called for nonexistent message: " + id; 306 307 // remove transaction from table 308 delete inTbl[id]; 309 310 // send error 311 postMessage({ id: id, error: error, message: message }); 312 }, 313 complete: function(v) { 314 completed = true; 315 // verify in table 316 if (!inTbl[id]) throw "complete called for nonexistent message: " + id; 317 // remove transaction from table 318 delete inTbl[id]; 319 // send complete 320 postMessage({ id: id, result: v }); 321 }, 322 delayReturn: function(delay) { 323 if (typeof delay === 'boolean') { 324 shouldDelayReturn = (delay === true); 325 } 326 return shouldDelayReturn; 327 }, 328 completed: function() { 329 return completed; 330 } 331 }; 332 }; 333 334 var setTransactionTimeout = function(transId, timeout, method) { 335 return window.setTimeout(function() { 336 if (outTbl[transId]) { 337 // XXX: what if client code raises an exception here? 338 var msg = "timeout (" + timeout + "ms) exceeded on method '" + method + "'"; 339 (1,outTbl[transId].error)("timeout_error", msg); 340 delete outTbl[transId]; 341 delete s_transIds[transId]; 342 } 343 }, timeout); 344 }; 345 346 var onMessage = function(origin, method, m) { 347 // if an observer was specified at allocation time, invoke it 348 if (typeof cfg.gotMessageObserver === 'function') { 349 // pass observer a clone of the object so that our 350 // manipulations are not visible (i.e. method unscoping).
351 // This is not particularly efficient, but then we expect 352 // that message observers are primarily for debugging anyway. 353 try { 354 cfg.gotMessageObserver(origin, m); 355 } catch (e) { 356 debug("gotMessageObserver() raised an exception: " + e.toString()); 357 } 358 } 359 360 // now, what type of message is this? 361 if (m.id && method) { 362 // a request! do we have a registered handler for this request? 363 if (regTbl[method]) { 364 var trans = createTransaction(m.id, origin, m.callbacks ? m.callbacks : [ ]); 365 inTbl[m.id] = { }; 366 try { 367 // callback handling. we'll magically create functions inside the parameter list for each 368 // callback 369 if (m.callbacks && s_isArray(m.callbacks) && m.callbacks.length > 0) { 370 for (var i = 0; i < m.callbacks.length; i++) { 371 var path = m.callbacks[i]; 372 var obj = m.params; 373 var pathItems = path.split('/'); 374 for (var j = 0; j < pathItems.length - 1; j++) { 375 var cp = pathItems[j]; 376 if (typeof obj[cp] !== 'object') obj[cp] = { }; 377 obj = obj[cp]; 378 } 379 obj[pathItems[pathItems.length - 1]] = (function() { 380 var cbName = path; 381 return function(params) { 382 return trans.invoke(cbName, params); 383 }; 384 })(); 385 } 386 } 387 var resp = regTbl[method](trans, m.params); 388 if (!trans.delayReturn() && !trans.completed()) trans.complete(resp); 389 } catch(e) { 390 // automagic handling of exceptions: 391 var error = "runtime_error"; 392 var message = null; 393 // * if it's a string then it gets an error code of 'runtime_error' and string is the message 394 if (typeof e === 'string') { 395 message = e; 396 } else if (typeof e === 'object') { 397 // either an array or an object 398 // * if it's an array of length two, then array[0] is the code, array[1] is the error message 399 if (e && s_isArray(e) && e.length == 2) { 400 error = e[0]; 401 message = e[1]; 402 } 403 // * if it's an object then we'll look form error and message parameters 404 else if (typeof e.error === 'string') { 405 error = e.error; 406 if (!e.message) message = ""; 407 else if (typeof e.message === 'string') message = e.message; 408 else e = e.message; // let the stringify/toString message give us a reasonable verbose error string 409 } 410 } 411 412 // message is *still* null, let's try harder 413 if (message === null) { 414 try { 415 message = JSON.stringify(e); 416 /* On MSIE8, this can result in 'out of memory', which 417 * leaves message undefined. */ 418 if (typeof(message) == 'undefined') 419 message = e.toString(); 420 } catch (e2) { 421 message = e.toString(); 422 } 423 } 424 425 trans.error(error,message); 426 } 427 } 428 } else if (m.id && m.callback) { 429 if (!outTbl[m.id] ||!outTbl[m.id].callbacks || !outTbl[m.id].callbacks[m.callback]) 430 { 431 debug("ignoring invalid callback, id:"+m.id+ " (" + m.callback +")"); 432 } else { 433 // XXX: what if client code raises an exception here? 434 outTbl[m.id].callbacks[m.callback](m.params); 435 } 436 } else if (m.id) { 437 if (!outTbl[m.id]) { 438 debug("ignoring invalid response: " + m.id); 439 } else { 440 // XXX: what if client code raises an exception here? 441 if (m.error) { 442 (1,outTbl[m.id].error)(m.error, m.message); 443 } else { 444 if (m.result !== undefined) (1,outTbl[m.id].success)(m.result); 445 else (1,outTbl[m.id].success)(); 446 } 447 delete outTbl[m.id]; 448 delete s_transIds[m.id]; 449 } 450 } else if (method) { 451 // tis a notification. 452 if (regTbl[method]) { 453 // yep, there's a handler for that. 454 // transaction has only origin for notifications. 455 regTbl[method]({ origin: origin }, m.params); 456 // if the client throws, we'll just let it bubble out 457 // what can we do? Also, here we'll ignore return values 458 } 459 } 460 }; 461 462 // now register our bound channel for msg routing 463 s_addBoundChan(cfg.window, cfg.origin, ((typeof cfg.scope === 'string') ? cfg.scope : ''), onMessage); 464
465 // scope method names based on cfg.scope specified when the Channel was instantiated 466 var scopeMethod = function(m) { 467 if (typeof cfg.scope === 'string' && cfg.scope.length) m = [cfg.scope, m].join("::"); 468 return m; 469 }; 470 471 // a small wrapper around postmessage whose primary function is to handle the 472 // case that clients start sending messages before the other end is "ready" 473 var postMessage = function(msg, force) { 474 if (!msg) throw "postMessage called with null message"; 475 476 // delay posting if we're not ready yet. 477 var verb = (ready ? "post " : "queue "); 478 debug(verb + " message: " + JSON.stringify(msg)); 479 if (!force && !ready) { 480 pendingQueue.push(msg); 481 } else { 482 if (typeof cfg.postMessageObserver === 'function') { 483 try { 484 cfg.postMessageObserver(cfg.origin, msg); 485 } catch (e) { 486 debug("postMessageObserver() raised an exception: " + e.toString()); 487 } 488 } 489 490 cfg.window.postMessage(JSON.stringify(msg), cfg.origin); 491 } 492 }; 493 494 var onReady = function(trans, type) { 495 debug('ready msg received'); 496 if (ready) throw "received ready message while in ready state. help!"; 497 498 if (type === 'ping') { 499 chanId += '-R'; 500 } else { 501 chanId += '-L'; 502 } 503 504 obj.unbind('__ready'); // now this handler isn't needed any more. 505 ready = true; 506 debug('ready msg accepted.'); 507 508 if (type === 'ping') { 509 obj.notify({ method: '__ready', params: 'pong' }); 510 } 511 512 // flush queue 513 while (pendingQueue.length) { 514 postMessage(pendingQueue.pop()); 515 } 516 517 // invoke onReady observer if provided 518 if (typeof cfg.onReady === 'function') cfg.onReady(obj); 519 }; 520 521 var obj = {
522 // tries to unbind a bound message handler. returns false if not possible 523 unbind: function (method) { 524 if (regTbl[method]) { 525 if (!(delete regTbl[method])) throw ("can't delete method: " + method); 526 return true; 527 } 528 return false; 529 }, 530 bind: function (method, cb) { 531 if (!method || typeof method !== 'string') throw "'method' argument to bind must be string"; 532 if (!cb || typeof cb !== 'function') throw "callback missing from bind params"; 533 534 if (regTbl[method]) throw "method '"+method+"' is already bound!"; 535 regTbl[method] = cb; 536 return this; 537 }, 538 call: function(m) { 539 if (!m) throw 'missing arguments to call function'; 540 if (!m.method || typeof m.method !== 'string') throw "'method' argument to call must be string"; 541 if (!m.success || typeof m.success !== 'function') throw "'success' callback missing from call"; 542
543 // now it's time to support the 'callback' feature of jschannel. We'll traverse the argument 544 // object and pick out all of the functions that were passed as arguments. 545 var callbacks = { }; 546 var callbackNames = [ ]; 547 var seen = [ ]; 548 549 var pruneFunctions = function (path, obj) { 550 if (seen.indexOf(obj) >= 0) { 551 throw "params cannot be a recursive data structure" 552 } 553 seen.push(obj); 554 555 if (typeof obj === 'object') { 556 for (var k in obj) { 557 if (!obj.hasOwnProperty(k)) continue; 558 var np = path + (path.length ? '/' : '') + k; 559 if (typeof obj[k] === 'function') { 560 callbacks[np] = obj[k]; 561 callbackNames.push(np); 562 delete obj[k]; 563 } else if (typeof obj[k] === 'object') { 564 pruneFunctions(np, obj[k]); 565 } 566 } 567 } 568 }; 569 pruneFunctions("", m.params); 570 571 // build a 'request' message and send it 572 var msg = { id: s_curTranId, method: scopeMethod(m.method), params: m.params }; 573 if (callbackNames.length) msg.callbacks = callbackNames; 574 575 if (m.timeout) 576 // XXX: This function returns a timeout ID, but we don't do anything with it. 577 // We might want to keep track of it so we can cancel it using clearTimeout() 578 // when the transaction completes. 579 setTransactionTimeout(s_curTranId, m.timeout, scopeMethod(m.method)); 580 581 // insert into the transaction table 582 outTbl[s_curTranId] = { callbacks: callbacks, error: m.error, success: m.success }; 583 s_transIds[s_curTranId] = onMessage; 584 585 // increment current id 586 s_curTranId++; 587 588 postMessage(msg); 589 }, 590 notify: function(m) { 591 if (!m) throw 'missing arguments to notify function'; 592 if (!m.method || typeof m.method !== 'string') throw "'method' argument to notify must be string"; 593 594 // no need to go into any transaction table 595 postMessage({ method: scopeMethod(m.method), params: m.params }); 596 }, 597 destroy: function () { 598 s_removeBoundChan(cfg.window, cfg.origin, ((typeof cfg.scope === 'string') ? cfg.scope : '')); 599 if (window.removeEventListener) window.removeEventListener('message', onMessage, false); 600 else if(window.detachEvent) window.detachEvent('onmessage', onMessage); 601 ready = false; 602 regTbl = { }; 603 inTbl = { }; 604 outTbl = { }; 605 cfg.origin = null; 606 pendingQueue = [ ]; 607 debug("channel destroyed"); 608 chanId = ""; 609 } 610 }; 611 612 obj.bind('__ready', onReady); 613 setTimeout(function() { 614 postMessage({ method: scopeMethod('__ready'), params: "ping" }, true); 615 }, 0); 616 617 return obj; 618 } 619 }; 620})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.