github.com/olivere/camlistore@v0.0.0-20140121221811-1b7ac2da0199/server/camlistored/ui/search_session.js (about) 1 /* 2 Copyright 2013 Google Inc. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 goog.provide('cam.SearchSession'); 18 19 goog.require('goog.events.EventTarget'); 20 goog.require('goog.Uri'); 21 goog.require('goog.Uri.QueryData'); 22 goog.require('goog.uri.utils'); 23 24 goog.require('cam.ServerConnection'); 25 26 // A search session is a standing query that notifies you when results change. It caches previous results and handles merging new data as it is received. It does not tell you _what_ changed; clients must reconcile as they see fit. 27 // 28 // TODO(aa): Only deltas should be sent from server to client 29 // TODO(aa): Need some way to avoid the duplicate query when websocket starts. Ideas: 30 // - Initial XHR query can also specify tag. This tag times out if not used rapidly. Send this same tag in socket query. 31 // - Socket assumes that client already has first batch of results (slightly racey though) 32 // - Prefer to use socket on client-side, test whether it works and fall back to XHR if not. 33 cam.SearchSession = function(connection, currentUri, query) { 34 goog.base(this); 35 36 this.connection_ = connection; 37 this.initSocketUri_(currentUri); 38 this.query_ = query; 39 this.instance_ = this.constructor.instanceCount_++; 40 this.continuation_ = this.getContinuation_(this.constructor.SEARCH_SESSION_CHANGE_TYPE.NEW); 41 this.socket_ = null; 42 this.supportsWebSocket_ = false; 43 44 this.resetData_(); 45 }; 46 goog.inherits(cam.SearchSession, goog.events.EventTarget); 47 48 // We fire this event when the data changes in any way. 49 cam.SearchSession.SEARCH_SESSION_CHANGED = 'search-session-change'; 50 51 // TODO(aa): This should go away once BlobItemContainer can reconcile changes for itself. 52 cam.SearchSession.SEARCH_SESSION_CHANGE_TYPE = { 53 NEW: 1, 54 APPEND: 2, 55 UPDATE: 3 56 }; 57 58 cam.SearchSession.prototype.PAGE_SIZE_ = 50; 59 60 cam.SearchSession.DESCRIBE_REQUEST = { 61 // This size doesn't matter, we don't use it. We only care about the aspect ratio. 62 // TODO(aa): This needs to die: https://code.google.com/p/camlistore/issues/detail?id=321 63 thumbnailSize: 1000, 64 65 // TODO(aa): This is not great. The describe request will still return tons of data we don't care about: 66 // - Children of folders 67 // - Properties we don't use 68 // See: https://code.google.com/p/camlistore/issues/detail?id=319 69 depth: 2 70 }; 71 72 cam.SearchSession.instanceCount_ = 0; 73 74 cam.SearchSession.prototype.getQuery = function() { 75 return this.query_; 76 } 77 78 // Returns all the data we currently have loaded. 79 cam.SearchSession.prototype.getCurrentResults = function() { 80 return this.data_; 81 }; 82 83 // Loads the next page of data. This is safe to call while a load is in progress; multiple calls for the same page will be collapsed. The SEARCH_SESSION_CHANGED event will be dispatched when the new data is available. 84 cam.SearchSession.prototype.loadMoreResults = function() { 85 if (!this.continuation_) { 86 return; 87 } 88 89 var c = this.continuation_; 90 this.continuation_ = null; 91 c(); 92 }; 93 94 // Returns true if it is known that all data which can be loaded for this query has been. 95 cam.SearchSession.prototype.isComplete = function() { 96 return !this.continuation_; 97 } 98 99 cam.SearchSession.prototype.supportsChangeNotifications = function() { 100 return this.supportsWebSocket_; 101 }; 102 103 cam.SearchSession.prototype.refreshIfNecessary = function() { 104 if (this.supportsWebSocket_) { 105 return; 106 } 107 108 this.continuation_ = this.getContinuation_(this.constructor.SEARCH_SESSION_CHANGE_TYPE.UPDATE, null, Math.max(this.data_.blobs.length, this.constructor.PAGE_SIZE_)); 109 this.resetData_(); 110 this.loadMoreResults(); 111 }; 112 113 cam.SearchSession.prototype.close = function() { 114 if (this.socket_) { 115 this.socket_.close(); 116 } 117 }; 118 119 cam.SearchSession.prototype.resetData_ = function() { 120 this.data_ = { 121 blobs: [], 122 description: { 123 meta: {} 124 } 125 }; 126 }; 127 128 cam.SearchSession.prototype.initSocketUri_ = function(currentUri) { 129 if (!goog.global.WebSocket) { 130 return; 131 } 132 133 this.socketUri_ = currentUri; 134 var config = this.connection_.getConfig(); 135 this.socketUri_.setPath(goog.uri.utils.appendPath(config.searchRoot, 'camli/search/ws')); 136 this.socketUri_.setQuery(goog.Uri.QueryData.createFromMap({authtoken: config.wsAuthToken || ''})); 137 if (this.socketUri_.getScheme() == "https") { 138 this.socketUri_.setScheme("wss"); 139 } else { 140 this.socketUri_.setScheme("ws"); 141 } 142 }; 143 144 cam.SearchSession.prototype.getContinuation_ = function(changeType, opt_continuationToken, opt_limit) { 145 return this.connection_.search.bind(this.connection_, this.query_, this.constructor.DESCRIBE_REQUEST, opt_limit || this.PAGE_SIZE_, opt_continuationToken, 146 this.searchDone_.bind(this, changeType)); 147 }; 148 149 cam.SearchSession.prototype.searchDone_ = function(changeType, result) { 150 if (changeType == this.constructor.SEARCH_SESSION_CHANGE_TYPE.APPEND) { 151 this.data_.blobs = this.data_.blobs.concat(result.blobs); 152 goog.mixin(this.data_.description.meta, result.description.meta); 153 } else { 154 this.data_.blobs = result.blobs; 155 this.data_.description = result.description; 156 } 157 158 if (result.continue) { 159 this.continuation_ = this.getContinuation_(this.constructor.SEARCH_SESSION_CHANGE_TYPE.APPEND, result.continue); 160 } else { 161 this.continuation_ = null; 162 } 163 164 this.dispatchEvent({type: this.constructor.SEARCH_SESSION_CHANGED, changeType: changeType}); 165 166 if (changeType == this.constructor.SEARCH_SESSION_CHANGE_TYPE.NEW || 167 changeType == this.constructor.SEARCH_SESSION_CHANGE_TYPE.APPEND) { 168 this.startSocketQuery_(); 169 } 170 }; 171 172 cam.SearchSession.prototype.startSocketQuery_ = function() { 173 if (!this.socketUri_) { 174 return; 175 } 176 177 if (this.socket_) { 178 this.socket_.close(); 179 } 180 181 var query = this.connection_.buildQuery(this.query_, this.constructor.DESCRIBE_REQUEST, Math.max(this.data_.blobs.length, this.constructor.PAGE_SIZE_)); 182 183 this.socket_ = new WebSocket(this.socketUri_.toString()); 184 this.socket_.onopen = function() { 185 var message = { 186 tag: 'q' + this.instance_, 187 query: query 188 }; 189 this.socket_.send(JSON.stringify(message)); 190 }.bind(this); 191 this.socket_.onmessage = function() { 192 this.supportsWebSocket_ = true; 193 // Ignore the first response. 194 this.socket_.onmessage = function(e) { 195 var result = JSON.parse(e.data); 196 this.searchDone_(this.constructor.SEARCH_SESSION_CHANGE_TYPE.UPDATE, result.result); 197 }.bind(this); 198 }.bind(this); 199 };