var repoindex = (function() { // a list of all possible connection states repoindex.ConnectionStatus = { Disconnected: "Disconnected", Connecting: "Connecting", Connected: "Connected" }; // a list of all possible request/response types repoindex.RequestType = { BasicRepoInfo: "basicrepoinfo", BasicPackageInfo: "basicpkginfo", FullPackageInfo: "fullpkginfo", GroupInfo: "groupinfo", UpgradeLookup: "upgradelookup", Suggestions: "suggestions", Postponed: "postponed" }; repoindex.isLoopback = function(domain) { return domain === "localhost" || domain === "127.0.0.1" || domain === "::1"; }; // responsible for the connection to the server; holds all repo/package information returned by the server var Client = function(url) { // basic initialization this.url = url; this.socket = undefined; this.hasBasicRepoInfo = false; this.hasGroupInfo = false; this.scheduledRequests = []; this.sentRequests = []; this.repos = {}; // define functions control the connections this.resetReconnectTries = function(tries) { if(tries !== undefined && tries >= 0) { this.remainingReconnectTries = this.reconnectTries = tries; } else { this.remainingReconnectTries = this.reconnectTries; } }; this.resetReconnectTries(2); this.reconnect = function() { if(this.remainingReconnectTries > 0) { --this.remainingReconnectTries; var reconnectTimer = window.setInterval(function() { window.clearInterval(reconnectTimer); repoindex.client.init(); }, 5000); } }; this.init = function() { repoindex.pageManager.setConnectionStatus(repoindex.ConnectionStatus.Connecting); if(typeof WebSocket != 'function' && typeof MozWebSocket == 'function') { WebSocket = MozWebSocket; } if(this.socket && this.socket.readyState === 1) { this.socket.onclose = function() { this.client.init(); }; this.socket.close(); return; } this.socket = new WebSocket(this.url); this.socket.client = this; this.socket.onopen = function(event) { repoindex.pageManager.setConnectionStatus(repoindex.ConnectionStatus.Connected); this.client.resetReconnectTries(); this.client.doScheduledRequests(); }; this.socket.onclose = function(event) { repoindex.pageManager.setConnectionStatus(repoindex.ConnectionStatus.Disconnected); this.client.reconnect(); }; this.socket.onmessage = function(event) { var msg = JSON.parse(event.data); switch(msg.class) { case "error": repoindex.pageManager.addError("Server replied error: " + msg.msg); break; case "results": this.client.parseResults(msg.id, msg.what, msg.values ? msg.values : msg.value); break; } }; this.socket.onerror = function(event) { repoindex.pageManager.addError("Connecting to server failed: " + event.reason); repoindex.pageManager.setConnectionStatus(repoindex.ConnectionStatus.Disconnected); }; }; this.isOpen = function() { return this.socket.readyState === 1; }; this.stop = function() { if(this.socket) { this.socket.close(); } }; // define functions to manage requests this.requestById = function(id) { if(id) { var i; // check scheduled requests as well as sent requests for(i = 0; i < this.scheduledRequests.length; ++i) { if(this.scheduledRequests[i].params.id === id) { return this.scheduledRequests[i]; } } for(i = 0; i < this.sentRequests.length; ++i) { if(this.sentRequests[i].params.id === id) { return this.sentRequests[i]; } } } }; this.scheduleRequest = function(type, params, callback) { // add request information to params params.class = "query"; params.what = type; // check whether the same kind of request has already been scheduled var i = 0; switch(type) { case repoindex.RequestType.BasicRepoInfo: case repoindex.RequestType.GroupInfo: // -> upgrade existing reuquest in this case for(; i < this.scheduledRequests.length; ++i) { if(this.scheduledRequests[i].params.class === "query" && this.scheduledRequests[i].params.what === type) { // there is already such a request this.scheduledRequests[i].callbacks.push(callback); // add callback params.id = this.scheduledRequests[i].params.id; // keep the old ID this.scheduledRequests[i].params = params; // upgrade params break; } } break; default: // -> other request types can't be upgraded i = this.scheduledRequests.length; } if(i >= this.scheduledRequests.length) { // a new request is required // -> add callback array var callbacks = []; if(callback) { callbacks.push(callback); } // -> add unique request ID while(this.requestById(params.id = repoindex.makeId())); // -> schedule request this.scheduledRequests.push({params: params, sent: false, callbacks: callbacks}); } // try to send scheduled requests this.doScheduledRequests(); }; this.doScheduledRequests = function() { // can only send requests if connection is established if(this.isOpen()) { while(this.scheduledRequests.length > 0) { var request = this.scheduledRequests.shift(); request.sent = true; this.sentRequests.push(request); this.socket.send(JSON.stringify(request.params)); } } }; this.parseResults = function(id, what, values) { if(!what) { repoindex.pageManager.addError("Server replied insufficiant results."); } else if(!values) { repoindex.pageManager.addError("Server replied \"" + what + "\" with insufficiant data."); } else { var request = this.requestById(id); if(!id || (request && request.sent)) { // use the received information switch(what) { case repoindex.RequestType.BasicRepoInfo: this.useBasicRepoInfo(values); break; case repoindex.RequestType.BasicPackageInfo: this.usePackageInfo(values); break; case repoindex.RequestType.FullPackageInfo: this.usePackageInfo(values); break; case repoindex.RequestType.GroupInfo: this.useGroupInfo(values); break; case repoindex.RequestType.UpgradeLookup: // the info is used via the registred callbacks only break; case repoindex.RequestType.Suggestions: this.useSuggestions(values); break; case repoindex.RequestType.Postponed: // server is busy, but will send results later pageManager.addError("The server is busy."); // -> do not call the callbacks already request = undefined; break; default: pageManager.addError("Server replied unknown results: " + what); return; // don't invoke callbacks when results are of unknown type } if(request) { // also invoke callbacks registred for this request for(var i = 0; i < request.callbacks.length; ++i) { request.callbacks[i](values); } // TODO: remove request object from list of sent requests } } else { repoindex.pageManager.addError("Server replied \"" + what + "\" with unknown ID."); } } }; // define functions to perform several requests this.requestBasicRepoInfo = function(callback) { this.scheduleRequest(repoindex.RequestType.BasicRepoInfo, {updates: true}, callback); }; this.requestBasicPackagesInfo = function(packageSelection, callback) { this.scheduleRequest(repoindex.RequestType.BasicPackageInfo, {sel: packageSelection, updates: true}, callback); }; this.requestFullPackagesInfo = function(packageSelection, callback) { this.scheduleRequest(repoindex.RequestType.FullPackageInfo, {sel: packageSelection}, callback); }; this.requestGroupInfo = function(callback) { this.scheduleRequest(repoindex.RequestType.GroupInfo, {updates: true}, callback); }; this.requestSuggestions = function(repoNames, searchTerm, callback) { this.scheduleRequest(repoindex.RequestType.Suggestions, {repos: repoNames, term: searchTerm}); }; this.requestAurSuggestions = function(term) { var xmlhttp = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP"); xmlhttp.onreadystatechange = function() { if(xmlhttp.readyState === 4 && xmlhttp.status === 200) { repoindex.client.useAurSuggestions(JSON.parse(xmlhttp.responseText)); } }; xmlhttp.open("GET", "http://aur.archlinux.org/rpc.php?type=suggest&arg=" + encodeURIComponent(term), true); xmlhttp.send(); }; this.sendCmd = function(cmd) { if(this.isOpen()) { this.socket.send(JSON.stringify({class: "cmd", what: cmd})); } else { repoindex.pageManager.addError("Not connected to a server."); } }; this.stopServer = function() { this.sendCmd("stop"); }; this.reinitAlpm = function() { this.sendCmd("reinitalpm"); }; this.updateAlpm = function() { this.sendCmd("updatealpm"); }; this.checkForUpgrades = function(dbName, syncdbNames, callback) { var params = { db: dbName }; if(syncdbNames) { params.syncdbs = syncdbNames; } this.scheduleRequest(repoindex.RequestType.UpgradeLookup, params, callback); }; // define helper functions to access repo/package information this.getPackageInfo = function(repoName, packageName) { var repoInfo = this.repos[repoName]; if(!repoInfo) { // the repo doesn't exists; might happen when receiving AUR suggestions directly // -> just create a new, empty repo this.repo[repoName] = (repoInfo = {name: repoName, packages: {}}); } var packageInfo = repoInfo.packages[packageName]; if(!packageInfo) { repoInfo.packages[packageName] = (packageInfo = {repo: repoName}); } return packageInfo; }; // define functions to use differend kinds of results this.useBasicRepoInfo = function(value) { if(!value) { repoindex.pageManager.addError("Server replied insufficiant basic repo info."); return false; } // upgrade repos and package list this.repos = value; var repoMgr = repoindex.pageManager.repoManager; var pkgMgr = repoindex.pageManager.packageManager; repoMgr.removeEntries(); pkgMgr.removeEntries(); var incomplete = false; var reposInOrder = []; for(var repoName in value) { if(value.hasOwnProperty(repoName)) { var info = value[repoName]; if(info.incomplete) { incomplete = true; } else { reposInOrder.push({name: repoName, info: info}); } } } reposInOrder.sort(function(lhs, rhs) { if(lhs === rhs) { return 1; } else if(lhs.info.index !== undefined) { if(rhs.info.index !== undefined) { return lhs.info.index > rhs.info.index; } else { return -1; } } else if(rhs.info.index !== undefined) { return 1; } else { return lhs.name > rhs.name; } }); for(var i = 0; i < reposInOrder.length; ++i) { var repoName = reposInOrder[i].name; var repoInfo = reposInOrder[i].info; var repoEntry = repoMgr.addEntry(repoName, repoInfo); var packages = repoInfo.packages; for(var packageName in packages) { if(value.hasOwnProperty(repoName)) { var packageInfo = packages[packageName]; pkgMgr.addEntry(repoEntry, packageName, packageInfo); } } } if(incomplete) { repoindex.pageManager.addError("Server replied incomplete repository info: Server is busy."); } this.hasBasicRepoInfo = true; pkgMgr.invalidate(); repoMgr.invalidate(); repoMgr.applyRepoStatusChange(); }; this.usePackageInfo = function(values) { if(!Array.isArray(values)) { repoindex.pageManager.addError("Server replied insufficiant package info."); return false; } for(var i = 0; i < values.length; ++i) { var value = values[i]; var repo = repoindex.client.repos[value.repo]; if(repo) { var package = repo.packages[value.name]; if(package) { if(value.basics) { package.basics = value.basics; } if(value.details) { package.details = value.details; } } else { repoindex.pageManager.addError("Package info replied by server refers to unknown package."); } } else { repoindex.pageManager.addError("Package info replied by server refers to unknown repository."); } } if(value.error) { switch(value.error) { case "na": if(value.repo) { if(value.name) { repoindex.pageManager.addError("Server replied error: The package " + value.name + " doesn't exist in the repository " + value.repo + "."); } else { repoindex.pageManager.addError("Server replied error: The repository " + value.repo + " doesn't exist."); } } else { repoindex.pageManager.addError("Server replied error: The requested info is not available."); } break; case "busy": repoindex.pageManager.addError("Server replied error in package info: The server is busy."); break; default: repoindex.pageManager.addError("Server replied unknown error code in package info: " + value.error); } } // updating table rows or any other GUI elements is done via callbacks }; this.useGroupInfo = function(values) { var groupMgr = repoindex.pageManager.groupManager; groupMgr.removeEntries(); var incomplete = false; var groupEntries = groupMgr.entries; for(var i1 = 0; i1 < values.length; ++i1) { var info = values[i1]; if(info.incomplete) { incomplete = true; } if(info.repo && info.groups) { for(var i2 = 0; i2 < info.groups.length; ++i2) { var group = info.groups[i2]; groupMgr.addEntry(info.repo, group.name, group.pkgs); } } } if(incomplete) { repoindex.pageManager.addError("Server replied incomplete group info: Server is busy."); } this.hasGroupInfo = true; groupMgr.useRequestedData(); }; this.useSuggestions = function(values) { if(Array.isArray(values)) { for(var i1 = 0; i1 < values.length; ++i1) { var value = values[i1]; var repoName = value.repo; var suggestions = value.res; if(Array.isArray(suggestions)) { if(suggestions.length > 0) { var repoEntry = repoindex.pageManager.repoManager.entryByName(repoName); if(repoEntry) { var pkgMgr = repoindex.pageManager.packageManager; for(var i2 = 0; i2 < suggestions.length; ++i2) { var packageName = suggestions[i2]; var entryExists = false; for(var i = 0, len = pkgMgr.entries.length; i < len; ++i) { var pkgEntry = pkgMgr.entries[i]; if(pkgEntry.name === packageName && pkgEntry.repoEntry.name === repoName) { entryExists = true; break; } } if(!entryExists) { var packageInfo = this.getPackageInfo(repoName, packageName); pkgMgr.addEntry(repoEntry, packageName, packageInfo); } } pkgMgr.invalidate(); } else { repoindex.pageManager.addError("There is no repo entry for the suggestions replied by the server."); } var entries = pkgMgr.entries; entries.sort(function(lhs, rhs) { return lhs.name > rhs.name; }); for(var i = 0; i < entries.length; ++i) { entries[i].index = i; } } } else { // TODO: handle error } } } else if(Array.isArray(values.errors)) { for(var i = 0; i < values.errors.length; ++i) { repoindex.pageManager.addError("Error replied by server in response of suggestion request: " + value.errors[i]); } } else { repoindex.pageManager.addError("Server replied insufficiant suggestion reply."); } }; this.useAurSuggestions = function(suggestions) { if(Array.isArray(suggestions)) { if(suggestions.length > 0) { var aurEntry = repoindex.pageManager.repoManager.entryByName("AUR"); if(aurEntry) { var pkgMgr = repoindex.pageManager.packageManager; for(var i = 0; i < suggestions.length; ++i) { var packageName = suggestions[i]; if(!pkgMgr.entryByName(packageName)) { var packageInfo = this.getPackageInfo("AUR", packageName); pkgMgr.addEntry(aurEntry, packageName, packageInfo); } } pkgMgr.invalidate(); } else { // there is no AUR entry but we requested AUR suggestions // -> shouldn't happen } } } else { // TODO: handle error } }; }; // create a global client used within the entire document repoindex.client = new Client((window.location.protocol === "https:" ? "wss://" : "ws://") + document.domain + ":1234"); if(!repoindex.isLoopback(document.domain)) { // hide stop button if server is not on loopback interface document.getElementById("nav_server").style.display = "none"; } return repoindex; })(repoindex || {});