channels.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  3. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. /* Channel abstraction. Supported channels:
  5. - WebSocket to an address
  6. - postMessage between windows
  7. In the future:
  8. - XMLHttpRequest to a server (with some form of queuing)
  9. The interface:
  10. channel = new ChannelName(parameters)
  11. The instantiation is specific to the kind of channel
  12. Methods:
  13. onmessage: set to function (jsonData)
  14. rawdata: set to true if you want onmessage to receive raw string data
  15. onclose: set to function ()
  16. send: function (string or jsonData)
  17. close: function ()
  18. .send() will encode the data if it is not a string.
  19. (should I include readyState as an attribute?)
  20. Channels must accept messages immediately, caching if the connection
  21. is not fully established yet.
  22. */
  23. define(["util"], function (util) {
  24. var channels = util.Module("channels");
  25. /* Subclasses must define:
  26. - ._send(string)
  27. - ._setupConnection()
  28. - ._ready()
  29. - .close() (and must set this.closed to true)
  30. And must call:
  31. - ._flush() on open
  32. - ._incoming(string) on incoming message
  33. - onclose() (not onmessage - instead _incoming)
  34. - emit("close")
  35. */
  36. var AbstractChannel = util.mixinEvents({
  37. onmessage: null,
  38. rawdata: false,
  39. onclose: null,
  40. closed: false,
  41. baseConstructor: function () {
  42. this._buffer = [];
  43. this._setupConnection();
  44. },
  45. send: function (data) {
  46. if (this.closed) {
  47. throw 'Cannot send to a closed connection';
  48. }
  49. if (typeof data != "string") {
  50. data = JSON.stringify(data);
  51. }
  52. if (! this._ready()) {
  53. this._buffer.push(data);
  54. return;
  55. }
  56. this._send(data);
  57. },
  58. _flush: function () {
  59. for (var i=0; i<this._buffer.length; i++) {
  60. this._send(this._buffer[i]);
  61. }
  62. this._buffer = [];
  63. },
  64. _incoming: function (data) {
  65. if (! this.rawdata) {
  66. try {
  67. data = JSON.parse(data);
  68. } catch (e) {
  69. console.error("Got invalid JSON data:", data.substr(0, 40));
  70. throw e;
  71. }
  72. }
  73. if (this.onmessage) {
  74. this.onmessage(data);
  75. }
  76. this.emit("message", data);
  77. }
  78. });
  79. channels.WebSocketChannel = util.Class(AbstractChannel, {
  80. constructor: function (address) {
  81. if (address.search(/^https?:/i) === 0) {
  82. address = address.replace(/^http/i, 'ws');
  83. }
  84. this.address = address;
  85. this.socket = null;
  86. this._reopening = false;
  87. this._lastConnectTime = 0;
  88. this._backoff = 0;
  89. this.baseConstructor();
  90. },
  91. backoffTime: 50, // Milliseconds to add to each reconnect time
  92. maxBackoffTime: 1500,
  93. backoffDetection: 2000, // Amount of time since last connection attempt that shows we need to back off
  94. toString: function () {
  95. var s = '[WebSocketChannel to ' + this.address;
  96. if (! this.socket) {
  97. s += ' (socket unopened)';
  98. } else {
  99. s += ' readyState: ' + this.socket.readyState;
  100. }
  101. if (this.closed) {
  102. s += ' CLOSED';
  103. }
  104. return s + ']';
  105. },
  106. close: function () {
  107. this.closed = true;
  108. if (this.socket) {
  109. // socket.onclose will call this.onclose:
  110. this.socket.close();
  111. } else {
  112. if (this.onclose) {
  113. this.onclose();
  114. }
  115. this.emit("close");
  116. }
  117. },
  118. _send: function (data) {
  119. this.socket.send(data);
  120. },
  121. _ready: function () {
  122. return this.socket && this.socket.readyState == this.socket.OPEN;
  123. },
  124. _setupConnection: function () {
  125. if (this.closed) {
  126. return;
  127. }
  128. this._lastConnectTime = Date.now();
  129. this.socket = new WebSocket(this.address);
  130. this.socket.onopen = (function () {
  131. this._flush();
  132. this._reopening = false;
  133. }).bind(this);
  134. this.socket.onclose = (function (event) {
  135. this.socket = null;
  136. var method = "error";
  137. if (event.wasClean) {
  138. // FIXME: should I even log clean closes?
  139. method = "log";
  140. }
  141. console[method]('WebSocket close', event.wasClean ? 'clean' : 'unclean',
  142. 'code:', event.code, 'reason:', event.reason || 'none');
  143. if (! this.closed) {
  144. this._reopening = true;
  145. if (Date.now() - this._lastConnectTime > this.backoffDetection) {
  146. this._backoff = 0;
  147. } else {
  148. this._backoff++;
  149. }
  150. var time = Math.min(this._backoff * this.backoffTime, this.maxBackoffTime);
  151. setTimeout((function () {
  152. this._setupConnection();
  153. }).bind(this), time);
  154. }
  155. }).bind(this);
  156. this.socket.onmessage = (function (event) {
  157. this._incoming(event.data);
  158. }).bind(this);
  159. this.socket.onerror = (function (event) {
  160. console.error('WebSocket error:', event.data);
  161. }).bind(this);
  162. }
  163. });
  164. /* Sends TO a window or iframe */
  165. channels.PostMessageChannel = util.Class(AbstractChannel, {
  166. _pingPollPeriod: 100, // milliseconds
  167. _pingPollIncrease: 100, // +100 milliseconds for each failure
  168. _pingMax: 2000, // up to a max of 2000 milliseconds
  169. constructor: function (win, expectedOrigin) {
  170. this.expectedOrigin = expectedOrigin;
  171. this._pingReceived = false;
  172. this._receiveMessage = this._receiveMessage.bind(this);
  173. if (win) {
  174. this.bindWindow(win, true);
  175. }
  176. this._pingFailures = 0;
  177. this.baseConstructor();
  178. },
  179. toString: function () {
  180. var s = '[PostMessageChannel';
  181. if (this.window) {
  182. s += ' to window ' + this.window;
  183. } else {
  184. s += ' not bound to a window';
  185. }
  186. if (this.window && ! this._pingReceived) {
  187. s += ' still establishing';
  188. }
  189. return s + ']';
  190. },
  191. bindWindow: function (win, noSetup) {
  192. if (this.window) {
  193. this.close();
  194. // Though we deinitialized everything, we aren't exactly closed:
  195. this.closed = false;
  196. }
  197. if (win && win.contentWindow) {
  198. win = win.contentWindow;
  199. }
  200. this.window = win;
  201. // FIXME: The distinction between this.window and window seems unimportant
  202. // in the case of postMessage
  203. var w = this.window;
  204. // In a Content context we add the listener to the local window
  205. // object, but in the addon context we add the listener to some
  206. // other window, like the one we were given:
  207. if (typeof window != "undefined") {
  208. w = window;
  209. }
  210. w.addEventListener("message", this._receiveMessage, false);
  211. if (! noSetup) {
  212. this._setupConnection();
  213. }
  214. },
  215. _send: function (data) {
  216. this.window.postMessage(data, this.expectedOrigin || "*");
  217. },
  218. _ready: function () {
  219. return this.window && this._pingReceived;
  220. },
  221. _setupConnection: function () {
  222. if (this.closed || this._pingReceived || (! this.window)) {
  223. return;
  224. }
  225. this._pingFailures++;
  226. this._send("hello");
  227. // We'll keep sending ping messages until we get a reply
  228. var time = this._pingPollPeriod + (this._pingPollIncrease * this._pingFailures);
  229. time = time > this._pingPollMax ? this._pingPollMax : time;
  230. this._pingTimeout = setTimeout(this._setupConnection.bind(this), time);
  231. },
  232. _receiveMessage: function (event) {
  233. if (event.source !== this.window) {
  234. return;
  235. }
  236. if (this.expectedOrigin && event.origin != this.expectedOrigin) {
  237. console.info("Expected message from", this.expectedOrigin,
  238. "but got message from", event.origin);
  239. return;
  240. }
  241. if (! this.expectedOrigin) {
  242. this.expectedOrigin = event.origin;
  243. }
  244. if (event.data == "hello") {
  245. this._pingReceived = true;
  246. if (this._pingTimeout) {
  247. clearTimeout(this._pingTimeout);
  248. this._pingTimeout = null;
  249. }
  250. this._flush();
  251. return;
  252. }
  253. this._incoming(event.data);
  254. },
  255. close: function () {
  256. this.closed = true;
  257. this._pingReceived = false;
  258. if (this._pingTimeout) {
  259. clearTimeout(this._pingTimeout);
  260. }
  261. window.removeEventListener("message", this._receiveMessage, false);
  262. if (this.onclose) {
  263. this.onclose();
  264. }
  265. this.emit("close");
  266. }
  267. });
  268. /* Handles message FROM an exterior window/parent */
  269. channels.PostMessageIncomingChannel = util.Class(AbstractChannel, {
  270. constructor: function (expectedOrigin) {
  271. this.source = null;
  272. this.expectedOrigin = expectedOrigin;
  273. this._receiveMessage = this._receiveMessage.bind(this);
  274. window.addEventListener("message", this._receiveMessage, false);
  275. this.baseConstructor();
  276. },
  277. toString: function () {
  278. var s = '[PostMessageIncomingChannel';
  279. if (this.source) {
  280. s += ' bound to source ' + s;
  281. } else {
  282. s += ' awaiting source';
  283. }
  284. return s + ']';
  285. },
  286. _send: function (data) {
  287. this.source.postMessage(data, this.expectedOrigin);
  288. },
  289. _ready: function () {
  290. return !!this.source;
  291. },
  292. _setupConnection: function () {
  293. },
  294. _receiveMessage: function (event) {
  295. if (this.expectedOrigin && this.expectedOrigin != "*" &&
  296. event.origin != this.expectedOrigin) {
  297. // FIXME: Maybe not worth mentioning?
  298. console.info("Expected message from", this.expectedOrigin,
  299. "but got message from", event.origin);
  300. return;
  301. }
  302. if (! this.expectedOrigin) {
  303. this.expectedOrigin = event.origin;
  304. }
  305. if (! this.source) {
  306. this.source = event.source;
  307. }
  308. if (event.data == "hello") {
  309. // Just a ping
  310. this.source.postMessage("hello", this.expectedOrigin);
  311. return;
  312. }
  313. this._incoming(event.data);
  314. },
  315. close: function () {
  316. this.closed = true;
  317. window.removeEventListener("message", this._receiveMessage, false);
  318. if (this._pingTimeout) {
  319. clearTimeout(this._pingTimeout);
  320. }
  321. if (this.onclose) {
  322. this.onclose();
  323. }
  324. this.emit("close");
  325. }
  326. });
  327. channels.Router = util.Class(util.mixinEvents({
  328. constructor: function (channel) {
  329. this._channelMessage = this._channelMessage.bind(this);
  330. this._channelClosed = this._channelClosed.bind(this);
  331. this._routes = Object.create(null);
  332. if (channel) {
  333. this.bindChannel(channel);
  334. }
  335. },
  336. bindChannel: function (channel) {
  337. if (this.channel) {
  338. this.channel.removeListener("message", this._channelMessage);
  339. this.channel.removeListener("close", this._channelClosed);
  340. }
  341. this.channel = channel;
  342. this.channel.on("message", this._channelMessage.bind(this));
  343. this.channel.on("close", this._channelClosed.bind(this));
  344. },
  345. _channelMessage: function (msg) {
  346. if (msg.type == "route") {
  347. var id = msg.routeId;
  348. var route = this._routes[id];
  349. if (! route) {
  350. console.warn("No route with the id", id);
  351. return;
  352. }
  353. if (msg.close) {
  354. this._closeRoute(route.id);
  355. } else {
  356. if (route.onmessage) {
  357. route.onmessage(msg.message);
  358. }
  359. route.emit("message", msg.message);
  360. }
  361. }
  362. },
  363. _channelClosed: function () {
  364. for (var id in this._routes) {
  365. this._closeRoute(id);
  366. }
  367. },
  368. _closeRoute: function (id) {
  369. var route = this._routes[id];
  370. if (route.onclose) {
  371. route.onclose();
  372. }
  373. route.emit("close");
  374. delete this._routes[id];
  375. },
  376. makeRoute: function (id) {
  377. id = id || util.generateId();
  378. var route = Route(this, id);
  379. this._routes[id] = route;
  380. return route;
  381. }
  382. }));
  383. var Route = util.Class(util.mixinEvents({
  384. constructor: function (router, id) {
  385. this.router = router;
  386. this.id = id;
  387. },
  388. send: function (msg) {
  389. this.router.channel.send({
  390. type: "route",
  391. routeId: this.id,
  392. message: msg
  393. });
  394. },
  395. close: function () {
  396. if (this.router._routes[this.id] !== this) {
  397. // This route instance has been overwritten, so ignore
  398. return;
  399. }
  400. delete this.router._routes[this.id];
  401. }
  402. }));
  403. return channels;
  404. });