/** * Collab module for the Cloud9 that uses collab * * @copyright 2013, Ajax.org B.V. */ define(function(require, exports, module) { main.consumes = [ "c9", "Plugin", "ext", "vfs", "dialog.question", "installer" ]; main.provides = ["collab.connect"]; return main; function main(options, imports, register) { var Plugin = imports.Plugin; var c9 = imports.c9; var ext = imports.ext; var vfs = imports.vfs; var installer = imports.installer; var question = imports["dialog.question"]; /***** Initialization *****/ var plugin = new Plugin("Ajax.org", main.consumes); var emit = plugin.getEmitter(); var clientId; // 0 - production // 1 - development // 2 - tracing var debug = options.debug; // var markup = require("text!./connect.xml"); var collab; var collabInstalled = !options.isSSH; var connecting = false; var connected = false; var isMaster = null; var fatalError = false; var CONNECT_TIMEOUT = 30000; // 30 seconds var IDLE_PERIOD = 300000; // 5 minutes var connectMsg; var connectTimeout; var stream; // Idle state handling var focussed = true; var reportedIdle = false; var idleTimeout; var loaded = false; function load() { if (!installer.isInstalled("c9.ide.collab", function() { load(); })) return; if (loaded) return; loaded = true; if (c9.connected) connect(); window.addEventListener("focus", updateIdleWithFocus); window.addEventListener("blur", updateIdleWithBlur); c9.on("connect", connect); c9.on("disconnect", onDisconnect); } function updateIdleWithFocus() { focussed = true; clearTimeout(idleTimeout); if (!connected || !reportedIdle) return; send("USER_STATE", { state: "online" }); reportedIdle = false; } function updateIdleWithBlur() { focussed = false; if (reportedIdle) return; clearTimeout(idleTimeout); idleTimeout = setTimeout(function() { if (!connected) return; reportedIdle = true; send("USER_STATE", { state: "idle" }); }, IDLE_PERIOD); } function updateIdleStatus() { if (document.hasFocus()) updateIdleWithFocus(); else updateIdleWithBlur(); } var extended = false; function extendCollab(callback) { if (collab) return callback(); var t = debug && Date.now(); if (extended) return plugin.once("available", callback); extended = true; ext.loadRemotePlugin("collab", { file: "c9.ide.collab/server/collab-server.js" }, function(err, api) { if (!api) { extended = false; return callback(err); } if (debug) console.log("loaded collab server in ", Date.now() - t, "ms"); collab = api; emit.sticky("available"); callback(); }); } function onDisconnect() { if (connected || connecting) emit("disconnect"); else console.log("Collab already disconnected"); connecting = connected = extended = false; emit.unsticky("available"); collab = null; if (stream) { stream.$close(); stream = null; } clearTimeout(connectTimeout); } /***** Methods *****/ function connect() { if (fatalError) return; if (connected) { console.log("Collab already connected, ignoring reconnection attempt"); return; } if (connecting) return; connecting = true; console.log("Collab connecting"); emit("connecting"); connectTimeout = setTimeout(function() { if (stream) { stream.$close(); stream = null; } connecting = false; if (!connected) { console.warn("[OT] Collab connect timed out ! - retrying ..."); connect(); } }, CONNECT_TIMEOUT); extendCollab(function(err) { if (err) return console.error("COLLAB CONNECT ERR", err); if (collabInstalled) return doConnect(); // sshCheckInstall(); }); } function doConnect() { // socket.id clientId = vfs.id; collab.connect({ basePath: options.basePath, clientId: clientId }, function (err, meta) { if (err) { fatalError = err.code === "EFATAL"; console.error("COLLAB connect failed", err); if (fatalError) promptInstaller(err); return; } stream = meta.stream; var isClosed = false; stream.once("data", onConnect); stream.once("end", function () { console.log("COLLAB STREAM END"); onClose(); }); stream.once("close", function() { console.log("COLLAB STREAM CLOSE"); onClose(); }); stream.$close = onClose; function onData(data) { data = JSON.parse(data); if (debug) console.log("[OT] RECEIVED FROM SERVER", data); emit("message", data); } function onConnect(data) { if (isClosed || !collab) return onClose(); data = JSON.parse(data); if (debug) console.log("[OT] RECEIVED FROM SERVER", data); if (data.type !== "CONNECT") return console.log("[OT] Waiting for connect event, skipping message", data); connected = true; connecting = false; isMaster = meta.isMaster; connectMsg = data; console.log("COLLAB connected -", meta.isMaster ? "MASTER" : "SLAVE"); emit("connect", data); stream.on("data", onData); clearTimeout(connectTimeout); updateIdleStatus(); } function onClose() { if (isClosed) return; if (stream) { stream.off("data", onData); stream.destroy(); } isClosed = true; onDisconnect(); setTimeout(function() { c9.connected && connect(); }, 1000); } }); } function send(msg) { if (typeof arguments[0] !== "object") msg = { type: arguments[0], data: arguments[1] }; if (!connected) return console.log("Collab not connected - SKIPPING ", msg); if (debug) console.log("[OT] SENDING TO SERVER", msg); collab.send(clientId, msg); } function promptInstaller(err) { question.show("Missing collab dependencies", err.message, "Cloud9 detected you are missing one or more collab dependencies." + " Would you like to open the installer to update to the latest version?", function() { // Yes installer.reinstall("c9.ide.collab"); }, function() { // No // Do nothing }, { yes: "Update", no: "Not now" } ); } /***** Lifecycle *****/ plugin.on("load", function() { load(); }); plugin.on("enable", function() { }); plugin.on("disable", function() { }); plugin.on("unload", function() { loaded = false; }); // Make sure the available event is always called plugin.on("newListener", function(event, listener) { if (event == "connect" && connected && connectMsg) listener(null, connectMsg); else if (event == "connecting" && connecting) listener(); }); /***** Register and define API *****/ /** * Finder implementation using collab **/ plugin.freezePublicAPI({ _events: [ /** * Fires when the collab VFS API is extended and available to be used by collab to * connect a user to the collab server. * @event available */ "available", /** * Fires when the collab is connected, handshaked and a stream is inited * and pushing messages from the collab server. * @event connect */ "connect", /** * Fires when the collab is connecting to the collab server * @event connecting */ "connecting", /** * Fires when the collab is disconnected * @event disconnect */ "disconnect", /** * Fires when a non-connect message is received on the collab stream * when the collab is connected to the collab server * @event message */ "message" ], /** * Specifies whether the collab debug is enabled or not * @property {Boolean} debug */ get debug() { return debug; }, /** * Specifies whether the collab is connected or not * @property {Boolean} connected */ get connected() { return connected; }, /** * Specifies whether the collab is connecting or not * @property {Boolean} connecting */ get connecting() { return connecting; }, /** * Specifies whether the collab is in master mode or not * @property {Boolean} isMaster */ get isMaster() { return isMaster; }, /** * Send a message to the collab server * @param {String} type the type of the message * @param {Object} message the message body to send */ send: send }); register(null, { "collab.connect": plugin }); } });