// samp
// ----
// Provides capabilities for using the SAMP Web Profile from JavaScript.
// Exported tokens are in the samp.* namespace.
// Inline documentation is somewhat patchy (partly because I don't know
// what javascript documentation is supposed to look like) - it is
// suggested to use it conjunction with the provided examples,
// currently visible at http://astrojs.github.com/sampjs/
// (gh-pages branch of github sources).
var samp = (function() {
// Constants defining well-known location of SAMP Web Profile hub etc.
var WEBSAMP_PORT = 21012;
var WEBSAMP_PATH = "/";
var WEBSAMP_PREFIX = "samp.webhub.";
var WEBSAMP_CLIENT_PREFIX = "";
// Tokens representing permissible types in a SAMP object (e.g. a message)
TYPE_STRING = "string";
TYPE_LIST = "list";
TYPE_MAP = "map";
var heir = function(proto) {
function F() {};
F.prototype = proto;
return new F();
};
// Utility functions for navigating DOM etc.
// -----------------------------------------
var getSampType = function(obj) {
if (typeof obj === "string") {
return TYPE_STRING;
}
else if (obj instanceof Array) {
return TYPE_LIST;
}
else if (obj instanceof Object && obj !== null) {
return TYPE_MAP;
}
else {
throw new Error("Not legal SAMP object type: " + obj);
}
};
var getChildElements = function(el, childTagName) {
var children = el.childNodes;
var child;
var childEls = [];
var i;
for (i = 0; i < children.length; i++) {
child = children[i];
if (child.nodeType === 1) { // Element
if (childTagName && (child.tagName !== childTagName)) {
throw new Error("Child <" + children[i].tagName + ">"
+ " of <" + el.tagName + ">"
+ " is not a <" + childTagName + ">");
}
childEls.push(child);
}
}
return childEls;
};
var getSoleChild = function(el, childTagName) {
var children = getChildElements(el, childTagName);
if (children.length === 1 ) {
return children[0];
}
else {
throw new Error("No sole child of <" + el.tagName + ">");
}
};
var getTextContent = function(el) {
var txt = "";
var i;
var child;
for (i = 0; i < el.childNodes.length; i++ ) {
child = el.childNodes[i];
if (child.nodeType === 1) { // Element
throw new Error("Element found in text content");
}
else if (child.nodeType === 3 || // Text
child.nodeType === 4 ) { // CDATASection
txt += child.nodeValue;
}
}
return txt;
};
var stringify = function(obj) {
return typeof JSON === "undefined" ? "..." : JSON.stringify(obj);
};
// XmlRpc class:
// Utilities for packing and unpacking XML-RPC messages.
// See xml-rpc.com.
var XmlRpc = {};
// Takes text and turns it into something suitable for use as the content
// of an XML-RPC string - special characters are escaped.
XmlRpc.escapeXml = function(s) {
return s.replace(/&/g, "&")
.replace(//g, ">");
};
// Asserts that the elements of paramList match the types given by typeList.
// TypeList must be an array containing only TYPE_STRING, TYPE_LIST
// and TYPE_MAP objects in some combination. paramList must be the
// same length.
// In case of mismatch an error is thrown.
XmlRpc.checkParams = function(paramList, typeList) {
var i;
for (i = 0; i < typeList.length; i++) {
if (typeList[i] !== TYPE_STRING &&
typeList[i] !== TYPE_LIST &&
typeList[i] !== TYPE_MAP) {
throw new Error("Unknown type " + typeList[i]
+ " in check list");
}
}
var npar = paramList.length;
var actualTypeList = [];
var ok = true;
for (i = 0; i < npar; i++) {
actualTypeList.push(getSampType(paramList[i]));
}
ok = ok && (typeList.length === npar);
for (i = 0; ok && i < npar; i++ ) {
ok = ok && typeList[i] === actualTypeList[i];
}
if (!ok) {
throw new Error("Param type list mismatch: "
+ "[" + typeList + "] != "
+ "[" + actualTypeList + "]");
}
};
// Turns a SAMP object (structure of strings, lists, maps) into an
// XML string suitable for use with XML-RPC.
XmlRpc.valueToXml = function v2x(obj, prefix) {
prefix = prefix || "";
var a;
var i;
var result;
var type = getSampType(obj);
if (type === TYPE_STRING) {
return prefix
+ ""
+ XmlRpc.escapeXml(obj)
+ "";
}
else if (type === TYPE_LIST) {
result = [];
result.push(prefix + "",
prefix + " ",
prefix + " ");
for (i = 0; i < obj.length; i++) {
result.push(v2x(obj[i], prefix + " "));
}
result.push(prefix + " ",
prefix + " ",
prefix + "");
return result.join("\n");
}
else if (type === TYPE_MAP) {
result = [];
result.push(prefix + "");
result.push(prefix + " ");
for (i in obj) {
result.push(prefix + " ");
result.push(prefix + " "
+ XmlRpc.escapeXml(i)
+ "");
result.push(v2x(obj[i], prefix + " "));
result.push(prefix + " ");
}
result.push(prefix + " ");
result.push(prefix + "");
return result.join("\n");
}
else {
throw new Error("bad type"); // shouldn't get here
}
};
// Turns an XML string from and XML-RPC message into a SAMP object
// (structure of strings, lists, maps).
XmlRpc.xmlToValue = function x2v(valueEl, allowInt) {
var childEls = getChildElements(valueEl);
var i;
var j;
var txt;
var node;
var childEl;
var elName;
if (childEls.length === 0) {
return getTextContent(valueEl);
}
else if (childEls.length === 1) {
childEl = childEls[0];
elName = childEl.tagName;
if (elName === "string") {
return getTextContent(childEl);
}
else if (elName === "array") {
var valueEls =
getChildElements(getSoleChild(childEl, "data"), "value");
var list = [];
for (i = 0; i < valueEls.length; i++) {
list.push(x2v(valueEls[i], allowInt));
}
return list;
}
else if (elName === "struct") {
var memberEls = getChildElements(childEl, "member");
var map = {};
var s_name;
var s_value;
var jc;
for (i = 0; i < memberEls.length; i++) {
s_name = undefined;
s_value = undefined;
for (j = 0; j < memberEls[i].childNodes.length; j++) {
jc = memberEls[i].childNodes[j];
if (jc.nodeType == 1) {
if (jc.tagName === "name") {
s_name = getTextContent(jc);
}
else if (jc.tagName === "value") {
s_value = x2v(jc, allowInt);
}
}
}
if (s_name !== undefined && s_value !== undefined) {
map[s_name] = s_value;
}
else {
throw new Error("No and/or "
+ "in ?");
}
}
return map;
}
else if (allowInt && (elName === "int" || elName === "i4")) {
return getTextContent(childEl);
}
else {
throw new Error("Non SAMP-friendly value content: "
+ "<" + elName + ">");
}
}
else {
throw new Error("Bad XML-RPC content - multiple elements");
}
};
// Turns the content of an XML-RPC element into an array of
// SAMP objects.
XmlRpc.decodeParams = function(paramsEl) {
var paramEls = getChildElements(paramsEl, "param");
var i;
var results = [];
for (i = 0; i < paramEls.length; i++) {
results.push(XmlRpc.xmlToValue(getSoleChild(paramEls[i], "value")));
}
return results;
};
// Turns the content of an XML-RPC element into an XmlRpc.Fault
// object.
XmlRpc.decodeFault = function(faultEl) {
var faultObj = XmlRpc.xmlToValue(getSoleChild(faultEl, "value"), true);
return new XmlRpc.Fault(faultObj.faultString, faultObj.faultCode);
};
// Turns an XML-RPC response element (should be ) into
// either a SAMP response object or an XmlRpc.Fault object.
// Note that a fault response does not throw an error, so check for
// the type of the result if you want to know whether a fault occurred.
// An error will however be thrown if the supplied XML does not
// correspond to a legal XML-RPC response.
XmlRpc.decodeResponse = function(xml) {
var mrEl = xml.documentElement;
if (mrEl.tagName !== "methodResponse") {
throw new Error("Response element is not ");
}
var contentEl = getSoleChild(mrEl);
if (contentEl.tagName === "fault") {
return XmlRpc.decodeFault(contentEl);
}
else if (contentEl.tagName === "params") {
return XmlRpc.decodeParams(contentEl)[0];
}
else {
throw new Error("Bad XML-RPC response - unknown element"
+ " <" + contentEl.tagName + ">");
}
};
// XmlRpc.Fault class:
// Represents an XML-RPC Fault response.
XmlRpc.Fault = function(faultString, faultCode) {
this.faultString = faultString;
this.faultCode = faultCode;
};
XmlRpc.Fault.prototype.toString = function() {
return "XML-RPC Fault (" + this.faultCode + "): " + this.faultString;
};
// XmlRpcRequest class:
// Represents an call which can be sent to an XML-RPC server.
var XmlRpcRequest = function(methodName, params) {
this.methodName = methodName;
this.params = params || [];
}
XmlRpcRequest.prototype.toString = function() {
return this.methodName + "(" + stringify(this.params) + ")";
};
XmlRpcRequest.prototype.addParam = function(param) {
this.params.push(param);
return this;
};
XmlRpcRequest.prototype.addParams = function(params) {
var i;
for (i = 0; i < params.length; i++) {
this.params.push(params[i]);
}
return this;
};
XmlRpcRequest.prototype.checkParams = function(typeList) {
XmlRpc.checkParams(this.params, typeList);
};
XmlRpcRequest.prototype.toXml = function() {
var lines = [];
lines.push(
"",
"",
" " + this.methodName + "",
" ");
for (var i = 0; i < this.params.length; i++) {
lines.push(" ",
XmlRpc.valueToXml(this.params[i], " "),
" ");
}
lines.push(
" ",
"");
return lines.join("\n");
};
// XmlRpcClient class:
// Object capable of sending XML-RPC calls to an XML-RPC server.
// That server will typically reside on the host on which the
// javascript is running; it is not likely to reside on the host
// which served the javascript. That means that sandboxing restrictions
// will be in effect. Much of the work done here is therefore to
// do the client-side work required to potentially escape the sandbox.
// The endpoint parameter, if supplied, is the URL of the XML-RPC server.
// If absent, the default SAMP Web Profile server is used.
var XmlRpcClient = function(endpoint) {
this.endpoint = endpoint ||
"http://localhost:" + WEBSAMP_PORT + WEBSAMP_PATH;
};
// Creates an XHR facade - an object that presents an interface
// resembling that of an XMLHttpRequest Level 2.
// This facade may be based on an actual XMLHttpRequest Level 2 object
// (on browsers that support it), or it may fake one using other
// available technology.
//
// The created facade in any case presents the following interface:
//
// open(method, url)
// send(body)
// abort()
// setContentType()
// responseText
// responseXML
// onload
// onerror(err) - includes timeout; abort is ignored
//
// See the documentation at http://www.w3.org/TR/XMLHttpRequest/
// for semantics.
//
// XMLHttpRequest Level 2 supports Cross-Origin Resource Sharing (CORS)
// which makes sandbox evasion possible. Faked XHRL2s returned by
// this method may use CORS or some other technology to evade the
// sandbox. The SAMP hub itself may selectively allow some of these
// technologies and not others, according to configuration.
XmlRpcClient.createXHR = function() {
// Creates an XHR facade based on a genuine XMLHttpRequest Level 2.
var XhrL2 = function(xhr) {
this.xhr = xhr;
xhr.onreadystatechange = (function(l2) {
return function() {
if (xhr.readyState !== 4) {
return;
}
else if (!l2.completed) {
if (+xhr.status === 200) {
l2.completed = true;
l2.responseText = xhr.responseText;
l2.responseXML = xhr.responseXML;
if (l2.onload) {
l2.onload();
}
}
}
};
})(this);
xhr.onerror = (function(l2) {
return function(event) {
if (!l2.completed) {
l2.completed = true;
if (l2.onerror) {
if (event) {
event.toString = function() {return "No hub?";};
}
else {
event = "No hub?";
}
l2.onerror(event);
}
}
};
})(this);
xhr.ontimeout = (function(l2) {
return function(event) {
if (!l2.completed) {
l2.completed = true;
if (l2.onerror) {
l2.onerror("timeout");
}
}
};
})(this);
};
XhrL2.prototype.open = function(method, url) {
this.xhr.open(method, url);
};
XhrL2.prototype.send = function(body) {
this.xhr.send(body);
};
XhrL2.prototype.abort = function() {
this.xhr.abort();
}
XhrL2.prototype.setContentType = function(mimeType) {
if ("setRequestHeader" in this.xhr) {
this.xhr.setRequestHeader("Content-Type", mimeType);
}
}
// Creates an XHR facade based on an XDomainRequest (IE8+ only).
var XdrL2 = function(xdr) {
this.xdr = xdr;
xdr.onload = (function(l2) {
return function() {
var e;
l2.responseText = xdr.responseText;
if (xdr.contentType === "text/xml" ||
xdr.contentType === "application/xml" ||
/\/x-/.test(xdr.contentType)) {
try {
var xdoc = new ActiveXObject("Microsoft.XMLDOM");
xdoc.loadXML(xdr.responseText);
l2.responseXML = xdoc;
}
catch (e) {
l2.responseXML = e;
}
}
if (l2.onload) {
l2.onload();
}
};
})(this);
xdr.onerror = (function(l2) {
return function(event) {
if (l2.onerror) {
l2.onerror(event);
}
};
})(this);
xdr.ontimeout = (function(l2) {
return function(event) {
if (l2.onerror) {
l2.onerror(event);
}
};
})(this);
};
XdrL2.prototype.open = function(method, url) {
this.xdr.open(method, url);
};
XdrL2.prototype.send = function(body) {
this.xdr.send(body);
};
XdrL2.prototype.abort = function() {
this.xdr.abort();
};
XdrL2.prototype.setContentType = function(mimeType) {
// can't do it.
};
// Creates an XHR Facade based on available XMLHttpRequest-type
// capabilibities.
// If an actual XMLHttpRequest Level 2 is available, use that.
if (typeof XMLHttpRequest !== "undefined") {
var xhr = new XMLHttpRequest();
if ("withCredentials" in xhr) {
return new XhrL2(xhr);
}
}
// Else if an XDomainRequest is available, use that.
if (typeof XDomainRequest !== "undefined") {
return new XdrL2(new XDomainRequest());
}
// Else fake an XMLHttpRequest using Flash/flXHR, if available
// and use that.
if (typeof flensed.flXHR !== "undefined") {
return new XhrL2(new flensed.flXHR({instancePooling: true}));
}
// No luck.
throw new Error("no cross-origin mechanism available");
};
// Executes a request by passing it to the XML-RPC server.
// On success, the result is passed to the resultHandler.
// On failure, the errHandler is called with one of two possible
// arguments: an XmlRpc.Fault object, or an Error object.
XmlRpcClient.prototype.execute = function(req, resultHandler, errHandler) {
(function(xClient) {
var xhr;
var e;
try {
xhr = XmlRpcClient.createXHR();
xhr.open("POST", xClient.endpoint);
xhr.setContentType("text/xml");
}
catch (e) {
errHandler(e);
throw e;
}
xhr.onload = function() {
var xml = xhr.responseXML;
var result;
var e;
if (xml) {
try {
result = XmlRpc.decodeResponse(xml);
}
catch (e) {
if (errHandler) {
errHandler(e);
}
return;
}
}
else {
if (errHandler) {
errHandler("no XML response");
}
return;
}
if (result instanceof XmlRpc.Fault) {
if (errHandler) {
errHandler(result);
}
}
else {
if (resultHandler) {
resultHandler(result);
}
}
};
xhr.onerror = function(event) {
if (event) {
event.toString = function() {return "No hub?";}
}
else {
event = "No hub";
}
if (errHandler) {
errHandler(event);
}
};
xhr.send(req.toXml());
return xhr;
})(this);
};
// Message class:
// Aggregates an MType string and a params map.
var Message = function(mtype, params) {
this["samp.mtype"] = mtype;
this["samp.params"] = params;
};
// Connection class:
// this is what clients use to communicate with the hub.
//
// All the methods from the Hub Abstract API as described in the
// SAMP standard are available as methods of a Connection object.
// The initial private-key argument required by the Web Profile is
// handled internally by this object - you do not need to supply it
// when calling one of the methods.
//
// All these calls have the same form:
//
// connection.method([method-args], resultHandler, errorHandler)
//
// the first argument is an array of the arguments (as per the SAMP
// abstract hub API), the second argument is a function which is
// called on successful completion with the result of the SAMP call
// as its argument, and the third argument is a function which is
// called on unsuccessful completion with an error object as its
// argument. The resultHandler and errorHandler arguments are optional.
//
// So for instance if you have a Connection object conn,
// you can send a notify message to all other clients by doing, e.g.:
//
// conn.notifyAll([new samp.Message(mtype, params)])
//
// Connection has other methods as well as the hub API ones
// as documented below.
var Connection = function(regInfo) {
this.regInfo = regInfo;
this.privateKey = regInfo["samp.private-key"];
if (! typeof(this.privateKey) === "string") {
throw new Error("Bad registration object");
}
this.xClient = new XmlRpcClient();
};
(function() {
var connMethods = {
call: [TYPE_STRING, TYPE_STRING, TYPE_MAP],
callAll: [TYPE_STRING, TYPE_MAP],
callAndWait: [TYPE_STRING, TYPE_MAP, TYPE_STRING],
declareMetadata: [TYPE_MAP],
declareSubscriptions: [TYPE_MAP],
getMetadata: [TYPE_STRING],
getRegisteredClients: [],
getSubscribedClients: [TYPE_STRING],
getSubscriptions: [TYPE_STRING],
notify: [TYPE_STRING, TYPE_MAP],
notifyAll: [TYPE_MAP],
ping: [],
reply: [TYPE_STRING, TYPE_MAP]
};
var fn;
var types;
for (fn in connMethods) {
(function(fname, types) {
// errHandler may be passed an XmlRpc.Fault or a thrown Error.
Connection.prototype[fname] =
function(sampArgs, resultHandler, errHandler) {
var closer =
(function(c) {return function() {c.close()}})(this);
errHandler = errHandler || closer
XmlRpc.checkParams(sampArgs, types);
var request = new XmlRpcRequest(WEBSAMP_PREFIX + fname);
request.addParam(this.privateKey);
request.addParams(sampArgs);
return this.xClient.
execute(request, resultHandler, errHandler);
};
})(fn, connMethods[fn]);
}
})();
Connection.prototype.unregister = function() {
var e;
if (this.callbackRequest) {
try {
this.callbackRequest.abort();
}
catch (e) {
}
}
var request = new XmlRpcRequest(WEBSAMP_PREFIX + "unregister");
request.addParam(this.privateKey);
try {
this.xClient.execute(request);
}
catch (e) {
// log unregister failed
}
delete this.regInfo;
delete this.privateKey;
};
// Closes this connection. It unregisters from the hub if still
// registered, but may harmlessly be called multiple times.
Connection.prototype.close = function() {
var e;
if (this.closed) {
return;
}
this.closed = true;
try {
if (this.regInfo) {
this.unregister();
}
}
catch (e) {
}
if (this.onclose) {
oc = this.onclose;
delete this.onclose;
try {
oc();
}
catch (e) {
}
}
};
// Arranges for this connection to receive callbacks.
//
// The callableClient argument must be an object implementing the
// SAMP callable client API, i.e. it must have the following methods:
//
// receiveNotification(string sender-id, map message)
// receiveCall(string sender-id, string msg-id, map message)
// receiveResponse(string responder-id, string msg-tag, map response)
//
// The successHandler argument will be called with no arguments if the
// allowCallbacks hub method completes successfully - it is a suitable
// hook to use for declaring subscriptions.
//
// The CallableClient class provides a suitable implementation, see below.
Connection.prototype.setCallable = function(callableClient,
successHandler) {
var e;
if (this.callbackRequest) {
try {
this.callbackRequest.abort();
}
catch (e) {
}
finally {
delete this.callbackRequest;
}
}
if (!callableClient && !this.regInfo) {
return;
}
var request =
new XmlRpcRequest(WEBSAMP_PREFIX + "allowReverseCallbacks");
request.addParam(this.privateKey);
request.addParam(callableClient ? "1" : "0");
var closer = (function(c) {return function() {c.close()}})(this);
if (callableClient) {
(function(connection) {
var invokeCallback = function(callback) {
var methodName = callback["samp.methodName"];
var methodParams = callback["samp.params"];
var handlerFunc = undefined;
if (methodName === WEBSAMP_CLIENT_PREFIX
+ "receiveNotification") {
handlerFunc = callableClient.receiveNotification;
}
else if (methodName === WEBSAMP_CLIENT_PREFIX
+ "receiveCall") {
handlerFunc = callableClient.receiveCall;
}
else if (methodName === WEBSAMP_CLIENT_PREFIX
+ "receiveResponse") {
handlerFunc = callableClient.receiveResponse;
}
else {
// unknown callback??
}
if (handlerFunc) {
handlerFunc.apply(callableClient, methodParams);
}
};
var startTime;
var resultHandler = function(result) {
if (getSampType(result) != TYPE_LIST) {
errHandler(new Error("pullCallbacks result not List"));
return;
}
var i;
var e;
for (i = 0; i < result.length; i++) {
try {
invokeCallback(result[i]);
}
catch (e) {
// log here?
}
}
callWaiter();
};
var errHandler = function(error) {
var elapsed = new Date().getTime() - startTime;
if (elapsed < 1000) {
connection.close()
}
else {
// probably a timeout
callWaiter();
}
};
var callWaiter = function() {
if (!connection.regInfo) {
return;
}
var request =
new XmlRpcRequest(WEBSAMP_PREFIX + "pullCallbacks");
request.addParam(connection.privateKey);
request.addParam("600");
startTime = new Date().getTime();
connection.callbackRequest =
connection.xClient.
execute(request, resultHandler, errHandler);
};
var sHandler = function() {
callWaiter();
successHandler();
};
connection.xClient.execute(request, sHandler, closer);
})(this);
}
else {
this.xClient.execute(request, successHandler, closer);
}
};
// Takes a public URL and returns a URL that can be used from within
// this javascript context. Some translation may be required, since
// a URL sent by an external application may be cross-domain, in which
// case browser sandboxing would typically disallow access to it.
Connection.prototype.translateUrl = function(url) {
var translator = this.regInfo["samp.url-translator"] || "";
return translator + url;
};
Connection.Action = function(actName, actArgs, resultKey) {
this.actName = actName;
this.actArgs = actArgs;
this.resultKey = resultKey;
};
// Suitable implementation for a callable client object which can
// be supplied to Connection.setCallable().
// Its callHandler and replyHandler members are string->function maps
// which can be used to provide handler functions for MTypes and
// message tags respectively.
//
// In more detail:
// The callHandler member maps a string representing an MType to
// a function with arguments (senderId, message, isCall).
// The replyHandler member maps a string representing a message tag to
// a function with arguments (responderId, msgTag, response).
var CallableClient = function(connection) {
this.callHandler = {};
this.replyHandler = {};
};
CallableClient.prototype.init = function(connection) {
};
CallableClient.prototype.receiveNotification = function(senderId, message) {
var mtype = message["samp.mtype"];
var handled = false;
var e;
if (mtype in this.callHandler) {
try {
this.callHandler[mtype](senderId, message, false);
}
catch (e) {
}
handled = true;
}
return handled;
};
CallableClient.prototype.receiveCall = function(senderId, msgId, message) {
var mtype = message["samp.mtype"];
var handled = false;
var response;
var result;
var e;
if (mtype in this.callHandler) {
try {
result = this.callHandler[mtype](senderId, message, true) || {};
response = {"samp.status": "samp.ok",
"samp.result": result};
handled = true;
}
catch (e) {
response = {"samp.status": "samp.error",
"samp.error": {"samp.errortxt": e.toString()}};
}
}
else {
response = {"samp.status": "samp.warning",
"samp.result": {},
"samp.error": {"samp.errortxt": "no action"}};
}
this.connection.reply([msgId, response]);
return handled;
};
CallableClient.prototype.receiveResponse = function(responderId, msgTag,
response) {
var handled = false;
var e;
if (msgTag in this.replyHandler) {
try {
this.replyHandler[msgTag](responderId, msgTag, response);
handled = true;
}
catch (e) {
}
}
return handled;
};
CallableClient.prototype.calculateSubscriptions = function() {
var subs = {};
var mt;
for (mt in this.callHandler) {
subs[mt] = {};
}
return subs;
};
// ClientTracker is a CallableClient which also provides tracking of
// registered clients.
//
// Its onchange member, if defined, will be called with arguments
// (client-id, change-type, associated-data) whenever the list or
// characteristics of registered clients has changed.
var ClientTracker = function() {
var tracker = this;
this.ids = {};
this.metas = {};
this.subs = {};
this.replyHandler = {};
this.callHandler = {
"samp.hub.event.shutdown": function(senderId, message) {
tracker.connection.close();
},
"samp.hub.disconnect": function(senderId, message) {
tracker.connection.close();
},
"samp.hub.event.register": function(senderId, message) {
var id = message["samp.params"]["id"];
tracker.ids[id] = true;
tracker.changed(id, "register", null);
},
"samp.hub.event.unregister": function(senderId, message) {
var id = message["samp.params"]["id"];
delete tracker.ids[id];
delete tracker.metas[id];
delete tracker.subs[id];
tracker.changed(id, "unregister", null);
},
"samp.hub.event.metadata": function(senderId, message) {
var id = message["samp.params"]["id"];
var meta = message["samp.params"]["metadata"];
tracker.metas[id] = meta;
tracker.changed(id, "meta", meta);
},
"samp.hub.event.subscriptions": function(senderId, message) {
var id = message["samp.params"]["id"];
var subs = message["samp.params"]["subscriptions"];
tracker.subs[id] = subs;
tracker.changed(id, "subs", subs);
}
};
};
ClientTracker.prototype = heir(CallableClient.prototype);
ClientTracker.prototype.changed = function(id, type, data) {
if (this.onchange) {
this.onchange(id, type, data);
}
};
ClientTracker.prototype.init = function(connection) {
var tracker = this;
this.connection = connection;
var retrieveInfo = function(id, type, infoFuncName, infoArray) {
connection[infoFuncName]([id], function(info) {
infoArray[id] = info;
tracker.changed(id, type, info);
});
};
connection.getRegisteredClients([], function(idlist) {
var i;
var id;
tracker.ids = {};
for (i = 0; i < idlist.length; i++) {
id = idlist[i];
tracker.ids[id] = true;
retrieveInfo(id, "meta", "getMetadata", tracker.metas);
retrieveInfo(id, "subs", "getSubscriptions", tracker.subs);
}
tracker.changed(null, "ids", null);
});
};
ClientTracker.prototype.getName = function(id) {
var meta = this.metas[id];
return (meta && meta["samp.name"]) ? meta["samp.name"] : "[" + id + "]";
};
// Connector class:
// A higher level class which can manage transparent hub
// registration/unregistration and client tracking.
//
// On construction, the name argument is mandatory, and corresponds
// to the samp.name item submitted at registration time.
// The other arguments are optional.
// meta is a metadata map (if absent, no metadata is declared)
// callableClient is a callable client object for receiving callbacks
// (if absent, the client is not callable).
// subs is a subscriptions map (if absent, no subscriptions are declared)
var Connector = function(name, meta, callableClient, subs) {
this.name = name;
this.meta = meta;
this.callableClient = callableClient;
this.subs = subs;
this.regTextNodes = [];
this.whenRegs = [];
this.whenUnregs = [];
this.connection = undefined;
this.onreg = undefined;
this.onunreg = undefined;
};
var setRegText = function(connector, txt) {
var i;
var nodes = connector.regTextNodes;
var node;
for (i = 0; i < nodes.length; i++) {
node = nodes[i];
node.innerHTML = "";
node.appendChild(document.createTextNode(txt));
}
};
Connector.prototype.setConnection = function(conn) {
var connector = this;
var e;
if (this.connection) {
this.connection.close();
if (this.onunreg) {
try {
this.onunreg();
}
catch (e) {
}
}
}
this.connection = conn;
if (conn) {
conn.onclose = function() {
connector.connection = null;
if (connector.onunreg) {
try {
connector.onunreg();
}
catch (e) {
}
}
connector.update();
};
if (this.meta) {
conn.declareMetadata([this.meta]);
}
if (this.callableClient) {
if (this.callableClient.init) {
this.callableClient.init(conn);
}
conn.setCallable(this.callableClient, function() {
conn.declareSubscriptions([connector.subs]);
});
}
if (this.onreg) {
try {
this.onreg(conn);
}
catch (e) {
}
}
}
this.update();
};
Connector.prototype.register = function() {
var connector = this;
var regErrHandler = function(err) {
setRegText(connector, "no (" + err.toString() + ")");
};
var regSuccessHandler = function(conn) {
connector.setConnection(conn);
setRegText(connector, conn ? "Yes" : "No");
};
register(this.name, regSuccessHandler, regErrHandler);
};
Connector.prototype.unregister = function() {
if (this.connection) {
this.connection.unregister([]);
this.setConnection(null);
}
};
// Returns a document fragment which contains Register/Unregister
// buttons for use by the user to attempt to connect/disconnect
// with the hub. This is useful for models where explicit
// user registration is encouraged or required, but when using
// the register-on-demand model such buttons are not necessary.
Connector.prototype.createRegButtons = function() {
var connector = this;
var regButt = document.createElement("button");
regButt.setAttribute("type", "button");
regButt.appendChild(document.createTextNode("Register"));
regButt.onclick = function() {connector.register();};
this.whenUnregs.push(regButt);
var unregButt = document.createElement("button");
unregButt.setAttribute("type", "button");
unregButt.appendChild(document.createTextNode("Unregister"));
unregButt.onclick = function() {connector.unregister();};
this.whenRegs.push(unregButt);
var regText = document.createElement("span");
this.regTextNodes.push(regText);
var node = document.createDocumentFragment();
node.appendChild(regButt);
node.appendChild(document.createTextNode(" "));
node.appendChild(unregButt);
var label = document.createElement("span");
label.innerHTML = " Registered: ";
node.appendChild(label);
node.appendChild(regText);
this.update();
return node;
};
Connector.prototype.update = function() {
var i;
var isConnected = !! this.connection;
var enableds = isConnected ? this.whenRegs : this.whenUnregs;
var disableds = isConnected ? this.whenUnregs : this.whenRegs;
for (i = 0; i < enableds.length; i++) {
enableds[i].removeAttribute("disabled");
}
for (i = 0; i < disableds.length; i++) {
disableds[i].setAttribute("disabled", "disabled");
}
setRegText(this, "No");
};
// Provides execution of a SAMP operation with register-on-demand.
// You can use this method to provide lightweight registration/use
// of web SAMP. Simply provide a connHandler function which
// does something with a connection (e.g. sends a message) and
// Connector.runWithConnection on it. This will connect if not
// already connected, and call the connHandler on with the connection.
// No explicit registration action is then required from the user.
//
// If the regErrorHandler argument is supplied, it is a function of
// one (error) argument called in the case that registration-on-demand
// fails.
//
// This is a more-or-less complete sampjs page:
//
//
Connector.prototype.runWithConnection =
function(connHandler, regErrorHandler) {
var connector = this;
var regSuccessHandler = function(conn) {
connector.setConnection(conn);
connHandler(conn);
};
var regFailureHandler = function(e) {
connector.setConnection(undefined);
regErrorHandler(e);
};
var pingResultHandler = function(result) {
connHandler(connector.connection);
};
var pingErrorHandler = function(err) {
register(this.name, regSuccessHandler, regFailureHandler);
};
if (this.connection) {
// Use getRegisteredClients as the most lightweight check
// I can think of that this connection is still OK.
// Ping doesn't work because the server replies even if the
// private-key is incorrect/invalid. Is that a bug or not?
this.connection.
getRegisteredClients([], pingResultHandler, pingErrorHandler);
}
else {
register(this.name, regSuccessHandler, regFailureHandler);
}
};
// Sets up an interval timer to run at intervals and notify a callback
// about whether a hub is currently running.
// Every millis milliseconds, the supplied availHandler function is
// called with a boolean argument: true if a (web profile) hub is
// running, false if not.
// Returns the interval timer (can be passed to clearInterval()).
Connector.prototype.onHubAvailability = function(availHandler, millis) {
samp.ping(availHandler);
// Could use the W3C Page Visibility API to avoid making these
// checks when the page is not visible.
return setInterval(function() {samp.ping(availHandler);}, millis);
};
// Determines whether a given subscriptions map indicates subscription
// to a given mtype.
var isSubscribed = function(subs, mtype) {
var matching = function(pattern, mtype) {
if (pattern == mtype) {
return true;
}
else if (pattern === "*") {
return true;
}
else {
var prefix;
var split = /^(.*)\.\*$/.exec(pat);
if (split) {
prefix = split[1];
if (prefix === mtype.substring(0, prefix.length)) {
return true;
}
}
}
return false;
};
var pat;
for (pat in subs) {
if (matching(pat, mtype)) {
return true;
}
}
return false;
};
// Attempts registration with a SAMP hub.
// On success the supplied connectionHandler function is called
// with the connection as an argument, on failure the supplied
// errorHandler is called with an argument that may be an Error
// or an XmlRpc.Fault.
var register = function(appName, connectionHandler, errorHandler) {
var xClient = new XmlRpcClient();
var regRequest = new XmlRpcRequest(WEBSAMP_PREFIX + "register");
var securityInfo = {"samp.name": appName};
regRequest.addParam(securityInfo);
regRequest.checkParams([TYPE_MAP]);
var resultHandler = function(result) {
var conn;
var e;
try {
conn = new Connection(result, 1000);
}
catch (e) {
errorHandler(e);
return;
}
connectionHandler(conn);
};
xClient.execute(regRequest, resultHandler, errorHandler);
};
// Calls the hub ping method once. It is not necessary to be
// registered to do this.
// The supplied pingHandler function is called with a boolean argument:
// true if a (web profile) hub is running, false if not.
var ping = function(pingHandler) {
var xClient = new XmlRpcClient();
var pingRequest = new XmlRpcRequest(WEBSAMP_PREFIX + "ping");
var resultHandler = function(result) {
pingHandler(true);
};
var errorHandler = function(error) {
pingHandler(false);
};
xClient.execute(pingRequest, resultHandler, errorHandler);
};
/* Exports. */
var jss = {};
jss.XmlRpcRequest = XmlRpcRequest;
jss.XmlRpcClient = XmlRpcClient;
jss.Message = Message;
jss.TYPE_STRING = TYPE_STRING;
jss.TYPE_LIST = TYPE_LIST;
jss.TYPE_MAP = TYPE_MAP;
jss.register = register;
jss.ping = ping;
jss.isSubscribed = isSubscribed;
jss.Connector = Connector;
jss.CallableClient = CallableClient;
jss.ClientTracker = ClientTracker;
return jss;
})();