123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473 |
- /* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
- /* Channel abstraction. Supported channels:
- - WebSocket to an address
- - postMessage between windows
- In the future:
- - XMLHttpRequest to a server (with some form of queuing)
- The interface:
- channel = new ChannelName(parameters)
- The instantiation is specific to the kind of channel
- Methods:
- onmessage: set to function (jsonData)
- rawdata: set to true if you want onmessage to receive raw string data
- onclose: set to function ()
- send: function (string or jsonData)
- close: function ()
- .send() will encode the data if it is not a string.
- (should I include readyState as an attribute?)
- Channels must accept messages immediately, caching if the connection
- is not fully established yet.
- */
- define(["util"], function (util) {
- var channels = util.Module("channels");
- /* Subclasses must define:
- - ._send(string)
- - ._setupConnection()
- - ._ready()
- - .close() (and must set this.closed to true)
- And must call:
- - ._flush() on open
- - ._incoming(string) on incoming message
- - onclose() (not onmessage - instead _incoming)
- - emit("close")
- */
- var AbstractChannel = util.mixinEvents({
- onmessage: null,
- rawdata: false,
- onclose: null,
- closed: false,
- baseConstructor: function () {
- this._buffer = [];
- this._setupConnection();
- },
- send: function (data) {
- if (this.closed) {
- throw 'Cannot send to a closed connection';
- }
- if (typeof data != "string") {
- data = JSON.stringify(data);
- }
- if (! this._ready()) {
- this._buffer.push(data);
- return;
- }
- this._send(data);
- },
- _flush: function () {
- for (var i=0; i<this._buffer.length; i++) {
- this._send(this._buffer[i]);
- }
- this._buffer = [];
- },
- _incoming: function (data) {
- if (! this.rawdata) {
- try {
- data = JSON.parse(data);
- } catch (e) {
- console.error("Got invalid JSON data:", data.substr(0, 40));
- throw e;
- }
- }
- if (this.onmessage) {
- this.onmessage(data);
- }
- this.emit("message", data);
- }
- });
- channels.WebSocketChannel = util.Class(AbstractChannel, {
- constructor: function (address) {
- if (address.search(/^https?:/i) === 0) {
- address = address.replace(/^http/i, 'ws');
- }
- this.address = address;
- this.socket = null;
- this._reopening = false;
- this._lastConnectTime = 0;
- this._backoff = 0;
- this.baseConstructor();
- },
- backoffTime: 50, // Milliseconds to add to each reconnect time
- maxBackoffTime: 1500,
- backoffDetection: 2000, // Amount of time since last connection attempt that shows we need to back off
- toString: function () {
- var s = '[WebSocketChannel to ' + this.address;
- if (! this.socket) {
- s += ' (socket unopened)';
- } else {
- s += ' readyState: ' + this.socket.readyState;
- }
- if (this.closed) {
- s += ' CLOSED';
- }
- return s + ']';
- },
- close: function () {
- this.closed = true;
- if (this.socket) {
- // socket.onclose will call this.onclose:
- this.socket.close();
- } else {
- if (this.onclose) {
- this.onclose();
- }
- this.emit("close");
- }
- },
- _send: function (data) {
- this.socket.send(data);
- },
- _ready: function () {
- return this.socket && this.socket.readyState == this.socket.OPEN;
- },
- _setupConnection: function () {
- if (this.closed) {
- return;
- }
- this._lastConnectTime = Date.now();
- this.socket = new WebSocket(this.address);
- this.socket.onopen = (function () {
- this._flush();
- this._reopening = false;
- }).bind(this);
- this.socket.onclose = (function (event) {
- this.socket = null;
- var method = "error";
- if (event.wasClean) {
- // FIXME: should I even log clean closes?
- method = "log";
- }
- console[method]('WebSocket close', event.wasClean ? 'clean' : 'unclean',
- 'code:', event.code, 'reason:', event.reason || 'none');
- if (! this.closed) {
- this._reopening = true;
- if (Date.now() - this._lastConnectTime > this.backoffDetection) {
- this._backoff = 0;
- } else {
- this._backoff++;
- }
- var time = Math.min(this._backoff * this.backoffTime, this.maxBackoffTime);
- setTimeout((function () {
- this._setupConnection();
- }).bind(this), time);
- }
- }).bind(this);
- this.socket.onmessage = (function (event) {
- this._incoming(event.data);
- }).bind(this);
- this.socket.onerror = (function (event) {
- console.error('WebSocket error:', event.data);
- }).bind(this);
- }
- });
- /* Sends TO a window or iframe */
- channels.PostMessageChannel = util.Class(AbstractChannel, {
- _pingPollPeriod: 100, // milliseconds
- _pingPollIncrease: 100, // +100 milliseconds for each failure
- _pingMax: 2000, // up to a max of 2000 milliseconds
- constructor: function (win, expectedOrigin) {
- this.expectedOrigin = expectedOrigin;
- this._pingReceived = false;
- this._receiveMessage = this._receiveMessage.bind(this);
- if (win) {
- this.bindWindow(win, true);
- }
- this._pingFailures = 0;
- this.baseConstructor();
- },
- toString: function () {
- var s = '[PostMessageChannel';
- if (this.window) {
- s += ' to window ' + this.window;
- } else {
- s += ' not bound to a window';
- }
- if (this.window && ! this._pingReceived) {
- s += ' still establishing';
- }
- return s + ']';
- },
- bindWindow: function (win, noSetup) {
- if (this.window) {
- this.close();
- // Though we deinitialized everything, we aren't exactly closed:
- this.closed = false;
- }
- if (win && win.contentWindow) {
- win = win.contentWindow;
- }
- this.window = win;
- // FIXME: The distinction between this.window and window seems unimportant
- // in the case of postMessage
- var w = this.window;
- // In a Content context we add the listener to the local window
- // object, but in the addon context we add the listener to some
- // other window, like the one we were given:
- if (typeof window != "undefined") {
- w = window;
- }
- w.addEventListener("message", this._receiveMessage, false);
- if (! noSetup) {
- this._setupConnection();
- }
- },
- _send: function (data) {
- this.window.postMessage(data, this.expectedOrigin || "*");
- },
- _ready: function () {
- return this.window && this._pingReceived;
- },
- _setupConnection: function () {
- if (this.closed || this._pingReceived || (! this.window)) {
- return;
- }
- this._pingFailures++;
- this._send("hello");
- // We'll keep sending ping messages until we get a reply
- var time = this._pingPollPeriod + (this._pingPollIncrease * this._pingFailures);
- time = time > this._pingPollMax ? this._pingPollMax : time;
- this._pingTimeout = setTimeout(this._setupConnection.bind(this), time);
- },
- _receiveMessage: function (event) {
- if (event.source !== this.window) {
- return;
- }
- if (this.expectedOrigin && event.origin != this.expectedOrigin) {
- console.info("Expected message from", this.expectedOrigin,
- "but got message from", event.origin);
- return;
- }
- if (! this.expectedOrigin) {
- this.expectedOrigin = event.origin;
- }
- if (event.data == "hello") {
- this._pingReceived = true;
- if (this._pingTimeout) {
- clearTimeout(this._pingTimeout);
- this._pingTimeout = null;
- }
- this._flush();
- return;
- }
- this._incoming(event.data);
- },
- close: function () {
- this.closed = true;
- this._pingReceived = false;
- if (this._pingTimeout) {
- clearTimeout(this._pingTimeout);
- }
- window.removeEventListener("message", this._receiveMessage, false);
- if (this.onclose) {
- this.onclose();
- }
- this.emit("close");
- }
- });
- /* Handles message FROM an exterior window/parent */
- channels.PostMessageIncomingChannel = util.Class(AbstractChannel, {
- constructor: function (expectedOrigin) {
- this.source = null;
- this.expectedOrigin = expectedOrigin;
- this._receiveMessage = this._receiveMessage.bind(this);
- window.addEventListener("message", this._receiveMessage, false);
- this.baseConstructor();
- },
- toString: function () {
- var s = '[PostMessageIncomingChannel';
- if (this.source) {
- s += ' bound to source ' + s;
- } else {
- s += ' awaiting source';
- }
- return s + ']';
- },
- _send: function (data) {
- this.source.postMessage(data, this.expectedOrigin);
- },
- _ready: function () {
- return !!this.source;
- },
- _setupConnection: function () {
- },
- _receiveMessage: function (event) {
- if (this.expectedOrigin && this.expectedOrigin != "*" &&
- event.origin != this.expectedOrigin) {
- // FIXME: Maybe not worth mentioning?
- console.info("Expected message from", this.expectedOrigin,
- "but got message from", event.origin);
- return;
- }
- if (! this.expectedOrigin) {
- this.expectedOrigin = event.origin;
- }
- if (! this.source) {
- this.source = event.source;
- }
- if (event.data == "hello") {
- // Just a ping
- this.source.postMessage("hello", this.expectedOrigin);
- return;
- }
- this._incoming(event.data);
- },
- close: function () {
- this.closed = true;
- window.removeEventListener("message", this._receiveMessage, false);
- if (this._pingTimeout) {
- clearTimeout(this._pingTimeout);
- }
- if (this.onclose) {
- this.onclose();
- }
- this.emit("close");
- }
- });
- channels.Router = util.Class(util.mixinEvents({
- constructor: function (channel) {
- this._channelMessage = this._channelMessage.bind(this);
- this._channelClosed = this._channelClosed.bind(this);
- this._routes = Object.create(null);
- if (channel) {
- this.bindChannel(channel);
- }
- },
- bindChannel: function (channel) {
- if (this.channel) {
- this.channel.removeListener("message", this._channelMessage);
- this.channel.removeListener("close", this._channelClosed);
- }
- this.channel = channel;
- this.channel.on("message", this._channelMessage.bind(this));
- this.channel.on("close", this._channelClosed.bind(this));
- },
- _channelMessage: function (msg) {
- if (msg.type == "route") {
- var id = msg.routeId;
- var route = this._routes[id];
- if (! route) {
- console.warn("No route with the id", id);
- return;
- }
- if (msg.close) {
- this._closeRoute(route.id);
- } else {
- if (route.onmessage) {
- route.onmessage(msg.message);
- }
- route.emit("message", msg.message);
- }
- }
- },
- _channelClosed: function () {
- for (var id in this._routes) {
- this._closeRoute(id);
- }
- },
- _closeRoute: function (id) {
- var route = this._routes[id];
- if (route.onclose) {
- route.onclose();
- }
- route.emit("close");
- delete this._routes[id];
- },
- makeRoute: function (id) {
- id = id || util.generateId();
- var route = Route(this, id);
- this._routes[id] = route;
- return route;
- }
- }));
- var Route = util.Class(util.mixinEvents({
- constructor: function (router, id) {
- this.router = router;
- this.id = id;
- },
- send: function (msg) {
- this.router.channel.send({
- type: "route",
- routeId: this.id,
- message: msg
- });
- },
- close: function () {
- if (this.router._routes[this.id] !== this) {
- // This route instance has been overwritten, so ignore
- return;
- }
- delete this.router._routes[this.id];
- }
- }));
- return channels;
- });
|