789 lines
36 KiB
JavaScript
789 lines
36 KiB
JavaScript
/*
|
|
GNU LESSER GENERAL PUBLIC LICENSE
|
|
Version 3, 29 June 2007
|
|
|
|
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
|
Everyone is permitted to copy and distribute verbatim copies
|
|
of this license document, but changing it is not allowed.
|
|
|
|
|
|
This version of the GNU Lesser General Public License incorporates
|
|
the terms and conditions of version 3 of the GNU General Public
|
|
License, supplemented by the additional permissions listed below.
|
|
|
|
0. Additional Definitions.
|
|
|
|
As used herein, "this License" refers to version 3 of the GNU Lesser
|
|
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
|
General Public License.
|
|
|
|
"The Library" refers to a covered work governed by this License,
|
|
other than an Application or a Combined Work as defined below.
|
|
|
|
An "Application" is any work that makes use of an interface provided
|
|
by the Library, but which is not otherwise based on the Library.
|
|
Defining a subclass of a class defined by the Library is deemed a mode
|
|
of using an interface provided by the Library.
|
|
|
|
A "Combined Work" is a work produced by combining or linking an
|
|
Application with the Library. The particular version of the Library
|
|
with which the Combined Work was made is also called the "Linked
|
|
Version".
|
|
|
|
The "Minimal Corresponding Source" for a Combined Work means the
|
|
Corresponding Source for the Combined Work, excluding any source code
|
|
for portions of the Combined Work that, considered in isolation, are
|
|
based on the Application, and not on the Linked Version.
|
|
|
|
The "Corresponding Application Code" for a Combined Work means the
|
|
object code and/or source code for the Application, including any data
|
|
and utility programs needed for reproducing the Combined Work from the
|
|
Application, but excluding the System Libraries of the Combined Work.
|
|
|
|
1. Exception to Section 3 of the GNU GPL.
|
|
|
|
You may convey a covered work under sections 3 and 4 of this License
|
|
without being bound by section 3 of the GNU GPL.
|
|
|
|
2. Conveying Modified Versions.
|
|
|
|
If you modify a copy of the Library, and, in your modifications, a
|
|
facility refers to a function or data to be supplied by an Application
|
|
that uses the facility (other than as an argument passed when the
|
|
facility is invoked), then you may convey a copy of the modified
|
|
version:
|
|
|
|
a) under this License, provided that you make a good faith effort to
|
|
ensure that, in the event an Application does not supply the
|
|
function or data, the facility still operates, and performs
|
|
whatever part of its purpose remains meaningful, or
|
|
|
|
b) under the GNU GPL, with none of the additional permissions of
|
|
this License applicable to that copy.
|
|
|
|
3. Object Code Incorporating Material from Library Header Files.
|
|
|
|
The object code form of an Application may incorporate material from
|
|
a header file that is part of the Library. You may convey such object
|
|
code under terms of your choice, provided that, if the incorporated
|
|
material is not limited to numerical parameters, data structure
|
|
layouts and accessors, or small macros, inline functions and templates
|
|
(ten or fewer lines in length), you do both of the following:
|
|
|
|
a) Give prominent notice with each copy of the object code that the
|
|
Library is used in it and that the Library and its use are
|
|
covered by this License.
|
|
|
|
b) Accompany the object code with a copy of the GNU GPL and this license
|
|
document.
|
|
|
|
4. Combined Works.
|
|
|
|
You may convey a Combined Work under terms of your choice that,
|
|
taken together, effectively do not restrict modification of the
|
|
portions of the Library contained in the Combined Work and reverse
|
|
engineering for debugging such modifications, if you also do each of
|
|
the following:
|
|
|
|
a) Give prominent notice with each copy of the Combined Work that
|
|
the Library is used in it and that the Library and its use are
|
|
covered by this License.
|
|
|
|
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
|
document.
|
|
|
|
c) For a Combined Work that displays copyright notices during
|
|
execution, include the copyright notice for the Library among
|
|
these notices, as well as a reference directing the user to the
|
|
copies of the GNU GPL and this license document.
|
|
|
|
d) Do one of the following:
|
|
|
|
0) Convey the Minimal Corresponding Source under the terms of this
|
|
License, and the Corresponding Application Code in a form
|
|
suitable for, and under terms that permit, the user to
|
|
recombine or relink the Application with a modified version of
|
|
the Linked Version to produce a modified Combined Work, in the
|
|
manner specified by section 6 of the GNU GPL for conveying
|
|
Corresponding Source.
|
|
|
|
1) Use a suitable shared library mechanism for linking with the
|
|
Library. A suitable mechanism is one that (a) uses at run time
|
|
a copy of the Library already present on the user's computer
|
|
system, and (b) will operate properly with a modified version
|
|
of the Library that is interface-compatible with the Linked
|
|
Version.
|
|
|
|
e) Provide Installation Information, but only if you would otherwise
|
|
be required to provide such information under section 6 of the
|
|
GNU GPL, and only to the extent that such information is
|
|
necessary to install and execute a modified version of the
|
|
Combined Work produced by recombining or relinking the
|
|
Application with a modified version of the Linked Version. (If
|
|
you use option 4d0, the Installation Information must accompany
|
|
the Minimal Corresponding Source and Corresponding Application
|
|
Code. If you use option 4d1, you must provide the Installation
|
|
Information in the manner specified by section 6 of the GNU GPL
|
|
for conveying Corresponding Source.)
|
|
|
|
5. Combined Libraries.
|
|
|
|
You may place library facilities that are a work based on the
|
|
Library side by side in a single library together with other library
|
|
facilities that are not Applications and are not covered by this
|
|
License, and convey such a combined library under terms of your
|
|
choice, if you do both of the following:
|
|
|
|
a) Accompany the combined library with a copy of the same work based
|
|
on the Library, uncombined with any other library facilities,
|
|
conveyed under the terms of this License.
|
|
|
|
b) Give prominent notice with the combined library that part of it
|
|
is a work based on the Library, and explaining where to find the
|
|
accompanying uncombined form of the same work.
|
|
|
|
6. Revised Versions of the GNU Lesser General Public License.
|
|
|
|
The Free Software Foundation may publish revised and/or new versions
|
|
of the GNU Lesser General Public License from time to time. Such new
|
|
versions will be similar in spirit to the present version, but may
|
|
differ in detail to address new problems or concerns.
|
|
|
|
Each version is given a distinguishing version number. If the
|
|
Library as you received it specifies that a certain numbered version
|
|
of the GNU Lesser General Public License "or any later version"
|
|
applies to it, you have the option of following the terms and
|
|
conditions either of that published version or of any later version
|
|
published by the Free Software Foundation. If the Library as you
|
|
received it does not specify a version number of the GNU Lesser
|
|
General Public License, you may choose any version of the GNU Lesser
|
|
General Public License ever published by the Free Software Foundation.
|
|
|
|
If the Library as you received it specifies that a proxy can decide
|
|
whether future versions of the GNU Lesser General Public License shall
|
|
apply, that proxy's public statement of acceptance of any version is
|
|
permanent authorization for you to choose that version for the
|
|
Library.
|
|
*/
|
|
|
|
/*
|
|
* js_channel is a very lightweight abstraction on top of
|
|
* postMessage which defines message formats and semantics
|
|
* to support interactions more rich than just message passing
|
|
* js_channel supports:
|
|
* + query/response - traditional rpc
|
|
* + query/update/response - incremental async return of results
|
|
* to a query
|
|
* + notifications - fire and forget
|
|
* + error handling
|
|
*
|
|
* js_channel is based heavily on json-rpc, but is focused at the
|
|
* problem of inter-iframe RPC.
|
|
*
|
|
* Message types:
|
|
* There are 5 types of messages that can flow over this channel,
|
|
* and you may determine what type of message an object is by
|
|
* examining its parameters:
|
|
* 1. Requests
|
|
* + integer id
|
|
* + string method
|
|
* + (optional) any params
|
|
* 2. Callback Invocations (or just "Callbacks")
|
|
* + integer id
|
|
* + string callback
|
|
* + (optional) params
|
|
* 3. Error Responses (or just "Errors)
|
|
* + integer id
|
|
* + string error
|
|
* + (optional) string message
|
|
* 4. Responses
|
|
* + integer id
|
|
* + (optional) any result
|
|
* 5. Notifications
|
|
* + string method
|
|
* + (optional) any params
|
|
*/
|
|
|
|
var Channel = (function() {
|
|
'use strict';
|
|
|
|
// current transaction id, start out at a random *odd* number between 1 and a million
|
|
// There is one current transaction counter id per page, and it's shared between
|
|
// channel instances. That means of all messages posted from a single javascript
|
|
// evaluation context, we'll never have two with the same id.
|
|
var s_curTranId = Math.floor(Math.random() * 1000001);
|
|
|
|
// no two bound channels in the same javascript evaluation context may have the same origin, scope, and window.
|
|
// futher if two bound channels have the same window and scope, they may not have *overlapping* origins
|
|
// (either one or both support '*'). This restriction allows a single onMessage handler to efficiently
|
|
// route messages based on origin and scope. The s_boundChans maps origins to scopes, to message
|
|
// handlers. Request and Notification messages are routed using this table.
|
|
// Finally, channels are inserted into this table when built, and removed when destroyed.
|
|
var s_boundChans = { };
|
|
|
|
// add a channel to s_boundChans, throwing if a dup exists
|
|
function s_addBoundChan(win, origin, scope, handler) {
|
|
function hasWin(arr) {
|
|
for (var i = 0; i < arr.length; i++) if (arr[i].win === win) return true;
|
|
return false;
|
|
}
|
|
|
|
// does she exist?
|
|
var exists = false;
|
|
|
|
|
|
if (origin === '*') {
|
|
// we must check all other origins, sadly.
|
|
for (var k in s_boundChans) {
|
|
if (!s_boundChans.hasOwnProperty(k)) continue;
|
|
if (k === '*') continue;
|
|
if (typeof s_boundChans[k][scope] === 'object') {
|
|
exists = hasWin(s_boundChans[k][scope]);
|
|
if (exists) break;
|
|
}
|
|
}
|
|
} else {
|
|
// we must check only '*'
|
|
if ((s_boundChans['*'] && s_boundChans['*'][scope])) {
|
|
exists = hasWin(s_boundChans['*'][scope]);
|
|
}
|
|
if (!exists && s_boundChans[origin] && s_boundChans[origin][scope])
|
|
{
|
|
exists = hasWin(s_boundChans[origin][scope]);
|
|
}
|
|
}
|
|
if (exists) throw "A channel is already bound to the same window which overlaps with origin '" + origin + "' and has scope '" + scope + "'";
|
|
|
|
if (typeof s_boundChans[origin] != 'object') s_boundChans[origin] = { };
|
|
if (typeof s_boundChans[origin][scope] != 'object') s_boundChans[origin][scope] = [];
|
|
s_boundChans[origin][scope].push({win: win, handler: handler});
|
|
}
|
|
|
|
function s_removeBoundChan(win, origin, scope) {
|
|
var arr = s_boundChans[origin][scope];
|
|
for (var i = 0; i < arr.length; i++) {
|
|
if (arr[i].win === win) {
|
|
arr.splice(i, 1);
|
|
}
|
|
}
|
|
if (s_boundChans[origin][scope].length === 0) {
|
|
delete s_boundChans[origin][scope];
|
|
}
|
|
}
|
|
|
|
function s_isArray(obj) {
|
|
if (Array.isArray) return Array.isArray(obj);
|
|
else {
|
|
return (obj.constructor.toString().indexOf('Array') != -1);
|
|
}
|
|
}
|
|
|
|
// No two outstanding outbound messages may have the same id, period. Given that, a single table
|
|
// mapping "transaction ids" to message handlers, allows efficient routing of Callback, Error, and
|
|
// Response messages. Entries are added to this table when requests are sent, and removed when
|
|
// responses are received.
|
|
var s_transIds = { };
|
|
|
|
// class singleton onMessage handler
|
|
// this function is registered once and all incoming messages route through here. This
|
|
// arrangement allows certain efficiencies, message data is only parsed once and dispatch
|
|
// is more efficient, especially for large numbers of simultaneous channels.
|
|
var s_onMessage = function(e) {
|
|
try {
|
|
var m = JSON.parse(e.data);
|
|
if (typeof m !== 'object' || m === null) throw 'malformed';
|
|
} catch (e) {
|
|
// just ignore any posted messages that do not consist of valid JSON
|
|
return;
|
|
}
|
|
|
|
var w = e.source;
|
|
var o = e.origin;
|
|
var s, i, meth;
|
|
|
|
if (typeof m.method === 'string') {
|
|
var ar = m.method.split('::');
|
|
if (ar.length == 2) {
|
|
s = ar[0];
|
|
meth = ar[1];
|
|
} else {
|
|
meth = m.method;
|
|
}
|
|
}
|
|
|
|
if (typeof m.id !== 'undefined') i = m.id;
|
|
|
|
// w is message source window
|
|
// o is message origin
|
|
// m is parsed message
|
|
// s is message scope
|
|
// i is message id (or undefined)
|
|
// meth is unscoped method name
|
|
// ^^ based on these factors we can route the message
|
|
|
|
// if it has a method it's either a notification or a request,
|
|
// route using s_boundChans
|
|
if (typeof meth === 'string') {
|
|
var delivered = false;
|
|
if (s_boundChans[o] && s_boundChans[o][s]) {
|
|
for (var j = 0; j < s_boundChans[o][s].length; j++) {
|
|
if (s_boundChans[o][s][j].win === w) {
|
|
s_boundChans[o][s][j].handler(o, meth, m);
|
|
delivered = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!delivered && s_boundChans['*'] && s_boundChans['*'][s]) {
|
|
for (var j = 0; j < s_boundChans['*'][s].length; j++) {
|
|
if (s_boundChans['*'][s][j].win === w) {
|
|
s_boundChans['*'][s][j].handler(o, meth, m);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// otherwise it must have an id (or be poorly formed
|
|
else if (typeof i != 'undefined') {
|
|
if (s_transIds[i]) s_transIds[i](o, meth, m);
|
|
}
|
|
};
|
|
|
|
// Setup postMessage event listeners
|
|
if (window.addEventListener) window.addEventListener('message', s_onMessage, false);
|
|
else if (window.attachEvent) window.attachEvent('onmessage', s_onMessage);
|
|
|
|
/* a messaging channel is constructed from a window and an origin.
|
|
* the channel will assert that all messages received over the
|
|
* channel match the origin
|
|
*
|
|
* Arguments to Channel.build(cfg):
|
|
*
|
|
* cfg.window - the remote window with which we'll communicate
|
|
* cfg.origin - the expected origin of the remote window, may be '*'
|
|
* which matches any origin
|
|
* cfg.scope - the 'scope' of messages. a scope string that is
|
|
* prepended to message names. local and remote endpoints
|
|
* of a single channel must agree upon scope. Scope may
|
|
* not contain double colons ('::').
|
|
* cfg.debugOutput - A boolean value. If true and window.console.log is
|
|
* a function, then debug strings will be emitted to that
|
|
* function.
|
|
* cfg.debugOutput - A boolean value. If true and window.console.log is
|
|
* a function, then debug strings will be emitted to that
|
|
* function.
|
|
* cfg.postMessageObserver - A function that will be passed two arguments,
|
|
* an origin and a message. It will be passed these immediately
|
|
* before messages are posted.
|
|
* cfg.gotMessageObserver - A function that will be passed two arguments,
|
|
* an origin and a message. It will be passed these arguments
|
|
* immediately after they pass scope and origin checks, but before
|
|
* they are processed.
|
|
* cfg.onReady - A function that will be invoked when a channel becomes "ready",
|
|
* this occurs once both sides of the channel have been
|
|
* instantiated and an application level handshake is exchanged.
|
|
* the onReady function will be passed a single argument which is
|
|
* the channel object that was returned from build().
|
|
*/
|
|
return {
|
|
build: function(cfg) {
|
|
var debug = function(m) {
|
|
if (cfg.debugOutput && window.console && window.console.log) {
|
|
// try to stringify, if it doesn't work we'll let javascript's built in toString do its magic
|
|
try { if (typeof m !== 'string') m = JSON.stringify(m); } catch (e) { }
|
|
console.log('[' + chanId + '] ' + m);
|
|
}
|
|
};
|
|
|
|
/* browser capabilities check */
|
|
if (!window.postMessage) throw ('jschannel cannot run this browser, no postMessage');
|
|
if (!window.JSON || !window.JSON.stringify || ! window.JSON.parse) {
|
|
throw ('jschannel cannot run this browser, no JSON parsing/serialization');
|
|
}
|
|
|
|
/* basic argument validation */
|
|
if (typeof cfg != 'object') throw ('Channel build invoked without a proper object argument');
|
|
|
|
if (!cfg.window || !cfg.window.postMessage) throw ('Channel.build() called without a valid window argument');
|
|
|
|
/* we'd have to do a little more work to be able to run multiple channels that intercommunicate the same
|
|
* window... Not sure if we care to support that */
|
|
if (window === cfg.window) throw ('target window is same as present window -- not allowed');
|
|
|
|
// let's require that the client specify an origin. if we just assume '*' we'll be
|
|
// propagating unsafe practices. that would be lame.
|
|
var validOrigin = false;
|
|
if (typeof cfg.origin === 'string') {
|
|
var oMatch;
|
|
if (cfg.origin === '*') validOrigin = true;
|
|
// allow valid domains under http and https. Also, trim paths off otherwise valid origins.
|
|
else if (null !== (oMatch = cfg.origin.match(/^https?:\/\/(?:[-a-zA-Z0-9_\.])+(?::\d+)?/))) {
|
|
cfg.origin = oMatch[0].toLowerCase();
|
|
validOrigin = true;
|
|
}
|
|
}
|
|
|
|
if (!validOrigin) throw ('Channel.build() called with an invalid origin');
|
|
|
|
if (typeof cfg.scope !== 'undefined') {
|
|
if (typeof cfg.scope !== 'string') throw 'scope, when specified, must be a string';
|
|
if (cfg.scope.split('::').length > 1) throw "scope may not contain double colons: '::'";
|
|
}
|
|
|
|
/* private variables */
|
|
// generate a random and psuedo unique id for this channel
|
|
var chanId = (function() {
|
|
var text = '';
|
|
var alpha = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
for (var i = 0; i < 5; i++) text += alpha.charAt(Math.floor(Math.random() * alpha.length));
|
|
return text;
|
|
})();
|
|
|
|
// registrations: mapping method names to call objects
|
|
var regTbl = { };
|
|
// current oustanding sent requests
|
|
var outTbl = { };
|
|
// current oustanding received requests
|
|
var inTbl = { };
|
|
// are we ready yet? when false we will block outbound messages.
|
|
var ready = false;
|
|
var pendingQueue = [];
|
|
|
|
var createTransaction = function(id, origin, callbacks) {
|
|
var shouldDelayReturn = false;
|
|
var completed = false;
|
|
|
|
return {
|
|
origin: origin,
|
|
invoke: function(cbName, v) {
|
|
// verify in table
|
|
if (!inTbl[id]) throw 'attempting to invoke a callback of a nonexistent transaction: ' + id;
|
|
// verify that the callback name is valid
|
|
var valid = false;
|
|
for (var i = 0; i < callbacks.length; i++) if (cbName === callbacks[i]) { valid = true; break; }
|
|
if (!valid) throw "request supports no such callback '" + cbName + "'";
|
|
|
|
// send callback invocation
|
|
postMessage({id: id, callback: cbName, params: v});
|
|
},
|
|
error: function(error, message) {
|
|
completed = true;
|
|
// verify in table
|
|
if (!inTbl[id]) throw 'error called for nonexistent message: ' + id;
|
|
|
|
// remove transaction from table
|
|
delete inTbl[id];
|
|
|
|
// send error
|
|
postMessage({id: id, error: error, message: message});
|
|
},
|
|
complete: function(v) {
|
|
completed = true;
|
|
// verify in table
|
|
if (!inTbl[id]) throw 'complete called for nonexistent message: ' + id;
|
|
// remove transaction from table
|
|
delete inTbl[id];
|
|
// send complete
|
|
postMessage({id: id, result: v});
|
|
},
|
|
delayReturn: function(delay) {
|
|
if (typeof delay === 'boolean') {
|
|
shouldDelayReturn = (delay === true);
|
|
}
|
|
return shouldDelayReturn;
|
|
},
|
|
completed: function() {
|
|
return completed;
|
|
}
|
|
};
|
|
};
|
|
|
|
var setTransactionTimeout = function(transId, timeout, method) {
|
|
return window.setTimeout(function() {
|
|
if (outTbl[transId]) {
|
|
// XXX: what if client code raises an exception here?
|
|
var msg = 'timeout (' + timeout + "ms) exceeded on method '" + method + "'";
|
|
(1, outTbl[transId].error)('timeout_error', msg);
|
|
delete outTbl[transId];
|
|
delete s_transIds[transId];
|
|
}
|
|
}, timeout);
|
|
};
|
|
|
|
var onMessage = function(origin, method, m) {
|
|
// if an observer was specified at allocation time, invoke it
|
|
if (typeof cfg.gotMessageObserver === 'function') {
|
|
// pass observer a clone of the object so that our
|
|
// manipulations are not visible (i.e. method unscoping).
|
|
// This is not particularly efficient, but then we expect
|
|
// that message observers are primarily for debugging anyway.
|
|
try {
|
|
cfg.gotMessageObserver(origin, m);
|
|
} catch (e) {
|
|
debug('gotMessageObserver() raised an exception: ' + e.toString());
|
|
}
|
|
}
|
|
|
|
// now, what type of message is this?
|
|
if (m.id && method) {
|
|
// a request! do we have a registered handler for this request?
|
|
if (regTbl[method]) {
|
|
var trans = createTransaction(m.id, origin, m.callbacks ? m.callbacks : []);
|
|
inTbl[m.id] = { };
|
|
try {
|
|
// callback handling. we'll magically create functions inside the parameter list for each
|
|
// callback
|
|
if (m.callbacks && s_isArray(m.callbacks) && m.callbacks.length > 0) {
|
|
for (var i = 0; i < m.callbacks.length; i++) {
|
|
var path = m.callbacks[i];
|
|
var obj = m.params;
|
|
var pathItems = path.split('/');
|
|
for (var j = 0; j < pathItems.length - 1; j++) {
|
|
var cp = pathItems[j];
|
|
if (typeof obj[cp] !== 'object') obj[cp] = { };
|
|
obj = obj[cp];
|
|
}
|
|
obj[pathItems[pathItems.length - 1]] = (function() {
|
|
var cbName = path;
|
|
return function(params) {
|
|
return trans.invoke(cbName, params);
|
|
};
|
|
})();
|
|
}
|
|
}
|
|
var resp = regTbl[method](trans, m.params);
|
|
if (!trans.delayReturn() && !trans.completed()) trans.complete(resp);
|
|
} catch (e) {
|
|
// automagic handling of exceptions:
|
|
var error = 'runtime_error';
|
|
var message = null;
|
|
// * if it's a string then it gets an error code of 'runtime_error' and string is the message
|
|
if (typeof e === 'string') {
|
|
message = e;
|
|
} else if (typeof e === 'object') {
|
|
// either an array or an object
|
|
// * if it's an array of length two, then array[0] is the code, array[1] is the error message
|
|
if (e && s_isArray(e) && e.length == 2) {
|
|
error = e[0];
|
|
message = e[1];
|
|
}
|
|
// * if it's an object then we'll look form error and message parameters
|
|
else if (typeof e.error === 'string') {
|
|
error = e.error;
|
|
if (!e.message) message = '';
|
|
else if (typeof e.message === 'string') message = e.message;
|
|
else e = e.message; // let the stringify/toString message give us a reasonable verbose error string
|
|
}
|
|
}
|
|
|
|
// message is *still* null, let's try harder
|
|
if (message === null) {
|
|
try {
|
|
message = JSON.stringify(e);
|
|
/* On MSIE8, this can result in 'out of memory', which
|
|
* leaves message undefined. */
|
|
if (typeof(message) == 'undefined')
|
|
message = e.toString();
|
|
} catch (e2) {
|
|
message = e.toString();
|
|
}
|
|
}
|
|
|
|
trans.error(error, message);
|
|
}
|
|
}
|
|
} else if (m.id && m.callback) {
|
|
if (!outTbl[m.id] || !outTbl[m.id].callbacks || !outTbl[m.id].callbacks[m.callback])
|
|
{
|
|
debug('ignoring invalid callback, id:' + m.id + ' (' + m.callback + ')');
|
|
} else {
|
|
// XXX: what if client code raises an exception here?
|
|
outTbl[m.id].callbacks[m.callback](m.params);
|
|
}
|
|
} else if (m.id) {
|
|
if (!outTbl[m.id]) {
|
|
debug('ignoring invalid response: ' + m.id);
|
|
} else {
|
|
// XXX: what if client code raises an exception here?
|
|
if (m.error) {
|
|
(1, outTbl[m.id].error)(m.error, m.message);
|
|
} else {
|
|
if (m.result !== undefined) (1, outTbl[m.id].success)(m.result);
|
|
else (1, outTbl[m.id].success)();
|
|
}
|
|
delete outTbl[m.id];
|
|
delete s_transIds[m.id];
|
|
}
|
|
} else if (method) {
|
|
// tis a notification.
|
|
if (regTbl[method]) {
|
|
// yep, there's a handler for that.
|
|
// transaction has only origin for notifications.
|
|
regTbl[method]({origin: origin}, m.params);
|
|
// if the client throws, we'll just let it bubble out
|
|
// what can we do? Also, here we'll ignore return values
|
|
}
|
|
}
|
|
};
|
|
|
|
// now register our bound channel for msg routing
|
|
s_addBoundChan(cfg.window, cfg.origin, ((typeof cfg.scope === 'string') ? cfg.scope : ''), onMessage);
|
|
|
|
// scope method names based on cfg.scope specified when the Channel was instantiated
|
|
var scopeMethod = function(m) {
|
|
if (typeof cfg.scope === 'string' && cfg.scope.length) m = [cfg.scope, m].join('::');
|
|
return m;
|
|
};
|
|
|
|
// a small wrapper around postmessage whose primary function is to handle the
|
|
// case that clients start sending messages before the other end is "ready"
|
|
var postMessage = function(msg, force) {
|
|
if (!msg) throw 'postMessage called with null message';
|
|
|
|
// delay posting if we're not ready yet.
|
|
var verb = (ready ? 'post ' : 'queue ');
|
|
debug(verb + ' message: ' + JSON.stringify(msg));
|
|
if (!force && !ready) {
|
|
pendingQueue.push(msg);
|
|
} else {
|
|
if (typeof cfg.postMessageObserver === 'function') {
|
|
try {
|
|
cfg.postMessageObserver(cfg.origin, msg);
|
|
} catch (e) {
|
|
debug('postMessageObserver() raised an exception: ' + e.toString());
|
|
}
|
|
}
|
|
|
|
cfg.window.postMessage(JSON.stringify(msg), cfg.origin);
|
|
}
|
|
};
|
|
|
|
var onReady = function(trans, type) {
|
|
debug('ready msg received');
|
|
if (ready) throw 'received ready message while in ready state. help!';
|
|
|
|
if (type === 'ping') {
|
|
chanId += '-R';
|
|
} else {
|
|
chanId += '-L';
|
|
}
|
|
|
|
obj.unbind('__ready'); // now this handler isn't needed any more.
|
|
ready = true;
|
|
debug('ready msg accepted.');
|
|
|
|
if (type === 'ping') {
|
|
obj.notify({method: '__ready', params: 'pong'});
|
|
}
|
|
|
|
// flush queue
|
|
while (pendingQueue.length) {
|
|
postMessage(pendingQueue.pop());
|
|
}
|
|
|
|
// invoke onReady observer if provided
|
|
if (typeof cfg.onReady === 'function') cfg.onReady(obj);
|
|
};
|
|
|
|
var obj = {
|
|
// tries to unbind a bound message handler. returns false if not possible
|
|
unbind: function(method) {
|
|
if (regTbl[method]) {
|
|
if (!(delete regTbl[method])) throw ("can't delete method: " + method);
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
bind: function(method, cb) {
|
|
if (!method || typeof method !== 'string') throw "'method' argument to bind must be string";
|
|
if (!cb || typeof cb !== 'function') throw 'callback missing from bind params';
|
|
|
|
if (regTbl[method]) throw "method '" + method + "' is already bound!";
|
|
regTbl[method] = cb;
|
|
return this;
|
|
},
|
|
call: function(m) {
|
|
if (!m) throw 'missing arguments to call function';
|
|
if (!m.method || typeof m.method !== 'string') throw "'method' argument to call must be string";
|
|
if (!m.success || typeof m.success !== 'function') throw "'success' callback missing from call";
|
|
|
|
// now it's time to support the 'callback' feature of jschannel. We'll traverse the argument
|
|
// object and pick out all of the functions that were passed as arguments.
|
|
var callbacks = { };
|
|
var callbackNames = [];
|
|
var seen = [];
|
|
|
|
var pruneFunctions = function(path, obj) {
|
|
if (seen.indexOf(obj) >= 0) {
|
|
throw 'params cannot be a recursive data structure';
|
|
}
|
|
seen.push(obj);
|
|
|
|
if (typeof obj === 'object') {
|
|
for (var k in obj) {
|
|
if (!obj.hasOwnProperty(k)) continue;
|
|
var np = path + (path.length ? '/' : '') + k;
|
|
if (typeof obj[k] === 'function') {
|
|
callbacks[np] = obj[k];
|
|
callbackNames.push(np);
|
|
delete obj[k];
|
|
} else if (typeof obj[k] === 'object') {
|
|
pruneFunctions(np, obj[k]);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
pruneFunctions('', m.params);
|
|
|
|
// build a 'request' message and send it
|
|
var msg = {id: s_curTranId, method: scopeMethod(m.method), params: m.params};
|
|
if (callbackNames.length) msg.callbacks = callbackNames;
|
|
|
|
if (m.timeout)
|
|
// XXX: This function returns a timeout ID, but we don't do anything with it.
|
|
// We might want to keep track of it so we can cancel it using clearTimeout()
|
|
// when the transaction completes.
|
|
setTransactionTimeout(s_curTranId, m.timeout, scopeMethod(m.method));
|
|
|
|
// insert into the transaction table
|
|
outTbl[s_curTranId] = {callbacks: callbacks, error: m.error, success: m.success};
|
|
s_transIds[s_curTranId] = onMessage;
|
|
|
|
// increment current id
|
|
s_curTranId++;
|
|
|
|
postMessage(msg);
|
|
},
|
|
notify: function(m) {
|
|
if (!m) throw 'missing arguments to notify function';
|
|
if (!m.method || typeof m.method !== 'string') throw "'method' argument to notify must be string";
|
|
|
|
// no need to go into any transaction table
|
|
postMessage({method: scopeMethod(m.method), params: m.params});
|
|
},
|
|
destroy: function() {
|
|
s_removeBoundChan(cfg.window, cfg.origin, ((typeof cfg.scope === 'string') ? cfg.scope : ''));
|
|
if (window.removeEventListener) window.removeEventListener('message', onMessage, false);
|
|
else if (window.detachEvent) window.detachEvent('onmessage', onMessage);
|
|
ready = false;
|
|
regTbl = { };
|
|
inTbl = { };
|
|
outTbl = { };
|
|
cfg.origin = null;
|
|
pendingQueue = [];
|
|
debug('channel destroyed');
|
|
chanId = '';
|
|
}
|
|
};
|
|
|
|
obj.bind('__ready', onReady);
|
|
setTimeout(function() {
|
|
postMessage({method: scopeMethod('__ready'), params: 'ping'}, true);
|
|
}, 0);
|
|
|
|
return obj;
|
|
}
|
|
};
|
|
})();
|