PageSourceSearch

https://www.chacos.com/on/demandware.static/Sites-chacos_us-Site/-…/v1790251290298/lib/jschannel/jschannel.js

js chacos.com collected 2026-09-24 21:09:53 UTC 29,818 bytes, 620 lines download raw bytes

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.