Files
noVNC/include/web-socket-js/web_socket.js
Joel Martin 071f2818a8 gimite/web-socket-js issue #35: async onclose.
Filed this bug about this issue:
http://github.com/gimite/web-socket-js/issues#issue/35

To work around the flash "recursive call" problem, WebSocket.as has
the onclose event disabled in the close() call and the javascript half
of the close() call does the onclose() call instead. This is fine, but
it needs to be asynchronous to act more like what happens with
a normal WebSockets object. The current behavior is that the onclose()
method is called inline (synchronously) when the close() is called and
this inconsistency make state handling more difficult.
2010-09-11 12:44:39 -05:00

388 lines
12 KiB
JavaScript
Executable File

// Copyright: Hiroshi Ichikawa <http://gimite.net/en/>
// License: New BSD License
// Reference: http://dev.w3.org/html5/websockets/
// Reference: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol
(function() {
if (window.WebSocket) return;
var console = window.console;
if (!console) console = {log: function(){ }, error: function(){ }};
if (!swfobject.hasFlashPlayerVersion("9.0.0")) {
console.error("Flash Player is not installed.");
return;
}
if (location.protocol == "file:") {
console.error(
"WARNING: web-socket-js doesn't work in file:///... URL " +
"unless you set Flash Security Settings properly. " +
"Open the page via Web server i.e. http://...");
}
WebSocket = function(url, protocol, proxyHost, proxyPort, headers) {
var self = this;
self.readyState = WebSocket.CONNECTING;
self.bufferedAmount = 0;
// Uses setTimeout() to make sure __createFlash() runs after the caller sets ws.onopen etc.
// Otherwise, when onopen fires immediately, onopen is called before it is set.
setTimeout(function() {
WebSocket.__addTask(function() {
self.__createFlash(url, protocol, proxyHost, proxyPort, headers);
});
}, 1);
}
WebSocket.prototype.__createFlash = function(url, protocol, proxyHost, proxyPort, headers) {
var self = this;
self.__flash =
WebSocket.__flash.create(url, protocol, proxyHost || null, proxyPort || 0, headers || null);
self.__flash.addEventListener("open", function(fe) {
try {
self.readyState = self.__flash.getReadyState();
if (self.__timer) clearInterval(self.__timer);
if (window.opera) {
// Workaround for weird behavior of Opera which sometimes drops events.
self.__timer = setInterval(function () {
self.__handleMessages();
}, 500);
}
if (self.onopen) self.onopen();
} catch (e) {
console.error(e.toString());
}
});
self.__flash.addEventListener("close", function(fe) {
try {
self.readyState = self.__flash.getReadyState();
if (self.__timer) clearInterval(self.__timer);
if (self.onclose) self.onclose();
} catch (e) {
console.error(e.toString());
}
});
self.__flash.addEventListener("message", function() {
try {
self.__handleMessages();
} catch (e) {
console.error(e.toString());
}
});
self.__flash.addEventListener("error", function(fe) {
try {
if (self.__timer) clearInterval(self.__timer);
if (self.onerror) self.onerror();
} catch (e) {
console.error(e.toString());
}
});
self.__flash.addEventListener("stateChange", function(fe) {
try {
self.readyState = self.__flash.getReadyState();
self.bufferedAmount = fe.getBufferedAmount();
} catch (e) {
console.error(e.toString());
}
});
//console.log("[WebSocket] Flash object is ready");
};
WebSocket.prototype.send = function(data) {
if (this.__flash) {
this.readyState = this.__flash.getReadyState();
}
if (!this.__flash || this.readyState == WebSocket.CONNECTING) {
throw "INVALID_STATE_ERR: Web Socket connection has not been established";
}
// We use encodeURIComponent() here, because FABridge doesn't work if
// the argument includes some characters. We don't use escape() here
// because of this:
// https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Functions#escape_and_unescape_Functions
// But it looks decodeURIComponent(encodeURIComponent(s)) doesn't
// preserve all Unicode characters either e.g. "\uffff" in Firefox.
var result = this.__flash.send(encodeURIComponent(data));
if (result < 0) { // success
return true;
} else {
this.bufferedAmount = result;
return false;
}
};
WebSocket.prototype.close = function() {
if (!this.__flash) return;
this.readyState = this.__flash.getReadyState();
if (this.readyState != WebSocket.OPEN) return;
this.__flash.close();
// Sets/calls them manually here because Flash WebSocketConnection.close cannot fire events
// which causes weird error:
// > You are trying to call recursively into the Flash Player which is not allowed.
this.readyState = WebSocket.CLOSED;
if (this.__timer) clearInterval(this.__timer);
if (this.onclose) {
// Make it asynchronous so that it looks more like an actual
// close event
setTimeout(this.onclose, 1);
}
};
/**
* Implementation of {@link <a href="http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-registration">DOM 2 EventTarget Interface</a>}
*
* @param {string} type
* @param {function} listener
* @param {boolean} useCapture !NB Not implemented yet
* @return void
*/
WebSocket.prototype.addEventListener = function(type, listener, useCapture) {
if (!('__events' in this)) {
this.__events = {};
}
if (!(type in this.__events)) {
this.__events[type] = [];
if ('function' == typeof this['on' + type]) {
this.__events[type].defaultHandler = this['on' + type];
this['on' + type] = this.__createEventHandler(this, type);
}
}
this.__events[type].push(listener);
};
/**
* Implementation of {@link <a href="http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-registration">DOM 2 EventTarget Interface</a>}
*
* @param {string} type
* @param {function} listener
* @param {boolean} useCapture NB! Not implemented yet
* @return void
*/
WebSocket.prototype.removeEventListener = function(type, listener, useCapture) {
if (!('__events' in this)) {
this.__events = {};
}
if (!(type in this.__events)) return;
for (var i = this.__events.length; i > -1; --i) {
if (listener === this.__events[type][i]) {
this.__events[type].splice(i, 1);
break;
}
}
};
/**
* Implementation of {@link <a href="http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-registration">DOM 2 EventTarget Interface</a>}
*
* @param {WebSocketEvent} event
* @return void
*/
WebSocket.prototype.dispatchEvent = function(event) {
if (!('__events' in this)) throw 'UNSPECIFIED_EVENT_TYPE_ERR';
if (!(event.type in this.__events)) throw 'UNSPECIFIED_EVENT_TYPE_ERR';
for (var i = 0, l = this.__events[event.type].length; i < l; ++ i) {
this.__events[event.type][i](event);
if (event.cancelBubble) break;
}
if (false !== event.returnValue &&
'function' == typeof this.__events[event.type].defaultHandler)
{
this.__events[event.type].defaultHandler(event);
}
};
WebSocket.prototype.__handleMessages = function() {
// Gets data using readSocketData() instead of getting it from event object
// of Flash event. This is to make sure to keep message order.
// It seems sometimes Flash events don't arrive in the same order as they are sent.
var arr = this.__flash.readSocketData();
for (var i = 0; i < arr.length; i++) {
var data = decodeURIComponent(arr[i]);
try {
if (this.onmessage) {
var e;
if (window.MessageEvent) {
e = document.createEvent("MessageEvent");
e.initMessageEvent("message", false, false, data, null, null, window, null);
} else { // IE
e = {data: data};
}
this.onmessage(e);
}
} catch (e) {
console.error(e.toString());
}
}
};
/**
* @param {object} object
* @param {string} type
*/
WebSocket.prototype.__createEventHandler = function(object, type) {
return function(data) {
var event = new WebSocketEvent();
event.initEvent(type, true, true);
event.target = event.currentTarget = object;
for (var key in data) {
event[key] = data[key];
}
object.dispatchEvent(event, arguments);
};
}
/**
* Basic implementation of {@link <a href="http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-interface">DOM 2 EventInterface</a>}
*
* @class
* @constructor
*/
function WebSocketEvent(){}
/**
*
* @type boolean
*/
WebSocketEvent.prototype.cancelable = true;
/**
*
* @type boolean
*/
WebSocketEvent.prototype.cancelBubble = false;
/**
*
* @return void
*/
WebSocketEvent.prototype.preventDefault = function() {
if (this.cancelable) {
this.returnValue = false;
}
};
/**
*
* @return void
*/
WebSocketEvent.prototype.stopPropagation = function() {
this.cancelBubble = true;
};
/**
*
* @param {string} eventTypeArg
* @param {boolean} canBubbleArg
* @param {boolean} cancelableArg
* @return void
*/
WebSocketEvent.prototype.initEvent = function(eventTypeArg, canBubbleArg, cancelableArg) {
this.type = eventTypeArg;
this.cancelable = cancelableArg;
this.timeStamp = new Date();
};
WebSocket.CONNECTING = 0;
WebSocket.OPEN = 1;
WebSocket.CLOSING = 2;
WebSocket.CLOSED = 3;
WebSocket.__tasks = [];
WebSocket.__initialize = function() {
if (WebSocket.__swfLocation) {
// For backword compatibility.
window.WEB_SOCKET_SWF_LOCATION = WebSocket.__swfLocation;
}
if (!window.WEB_SOCKET_SWF_LOCATION) {
console.error("[WebSocket] set WEB_SOCKET_SWF_LOCATION to location of WebSocketMain.swf");
return;
}
var container = document.createElement("div");
container.id = "webSocketContainer";
// Hides Flash box. We cannot use display: none or visibility: hidden because it prevents
// Flash from loading at least in IE. So we move it out of the screen at (-100, -100).
// But this even doesn't work with Flash Lite (e.g. in Droid Incredible). So with Flash
// Lite, we put it at (0, 0). This shows 1x1 box visible at left-top corner but this is
// the best we can do as far as we know now.
container.style.position = "absolute";
if (WebSocket.__isFlashLite()) {
container.style.left = "0px";
container.style.top = "0px";
} else {
container.style.left = "-100px";
container.style.top = "-100px";
}
var holder = document.createElement("div");
holder.id = "webSocketFlash";
container.appendChild(holder);
document.body.appendChild(container);
// See this article for hasPriority:
// http://help.adobe.com/en_US/as3/mobile/WS4bebcd66a74275c36cfb8137124318eebc6-7ffd.html
swfobject.embedSWF(
WEB_SOCKET_SWF_LOCATION, "webSocketFlash",
"1" /* width */, "1" /* height */, "9.0.0" /* SWF version */,
null, {bridgeName: "webSocket"}, {hasPriority: true, allowScriptAccess: "always"}, null,
function(e) {
if (!e.success) console.error("[WebSocket] swfobject.embedSWF failed");
}
);
FABridge.addInitializationCallback("webSocket", function() {
try {
//console.log("[WebSocket] FABridge initializad");
WebSocket.__flash = FABridge.webSocket.root();
WebSocket.__flash.setCallerUrl(location.href);
WebSocket.__flash.setDebug(!!window.WEB_SOCKET_DEBUG);
for (var i = 0; i < WebSocket.__tasks.length; ++i) {
WebSocket.__tasks[i]();
}
WebSocket.__tasks = [];
} catch (e) {
console.error("[WebSocket] " + e.toString());
}
});
};
WebSocket.__addTask = function(task) {
if (WebSocket.__flash) {
task();
} else {
WebSocket.__tasks.push(task);
}
};
WebSocket.__isFlashLite = function() {
if (!window.navigator || !window.navigator.mimeTypes) return false;
var mimeType = window.navigator.mimeTypes["application/x-shockwave-flash"];
if (!mimeType || !mimeType.enabledPlugin || !mimeType.enabledPlugin.filename) return false;
return mimeType.enabledPlugin.filename.match(/flashlite/i) ? true : false;
};
// called from Flash
window.webSocketLog = function(message) {
console.log(decodeURIComponent(message));
};
// called from Flash
window.webSocketError = function(message) {
console.error(decodeURIComponent(message));
};
if (!window.WEB_SOCKET_DISABLE_AUTO_INITIALIZATION) {
if (window.addEventListener) {
window.addEventListener("load", WebSocket.__initialize, false);
} else {
window.attachEvent("onload", WebSocket.__initialize);
}
}
})();