github.com/slspeek/camlistore_namedsearch@v0.0.0-20140519202248-ed6f70f7721a/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 is only used by BlobItemContainer. Once we switch over to BlobItemContainerReact completely, it can be removed. 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 perfect. The describe request will return some data we don't care about: 66 // - Properties we don't use 67 // See: https://code.google.com/p/camlistore/issues/detail?id=319 68 69 depth: 1, 70 rules: [ 71 { 72 attrs: ['camliContent', 'camliContentImage'] 73 }, 74 { 75 ifCamliNodeType: 'foursquare.com:checkin', 76 attrs: ['foursquareVenuePermanode'] 77 }, 78 { 79 ifCamliNodeType: 'foursquare.com:venue', 80 attrs: ['camliPath:photos'], 81 rules: [ 82 { attrs: ['camliPath:*'] } 83 ] 84 } 85 ] 86 }; 87 88 cam.SearchSession.instanceCount_ = 0; 89 90 cam.SearchSession.prototype.getQuery = function() { 91 return this.query_; 92 } 93 94 // Returns all the data we currently have loaded. 95 // It is guaranteed to return the following properties: 96 // blobs // non-zero length 97 // description 98 // description.meta 99 cam.SearchSession.prototype.getCurrentResults = function() { 100 return this.data_; 101 }; 102 103 // 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. 104 cam.SearchSession.prototype.loadMoreResults = function() { 105 if (!this.continuation_) { 106 return; 107 } 108 109 var c = this.continuation_; 110 this.continuation_ = null; 111 c(); 112 }; 113 114 // Returns true if it is known that all data which can be loaded for this query has been. 115 cam.SearchSession.prototype.isComplete = function() { 116 return !this.continuation_; 117 } 118 119 cam.SearchSession.prototype.supportsChangeNotifications = function() { 120 return this.supportsWebSocket_; 121 }; 122 123 cam.SearchSession.prototype.refreshIfNecessary = function() { 124 if (this.supportsWebSocket_) { 125 return; 126 } 127 128 this.continuation_ = this.getContinuation_(this.constructor.SEARCH_SESSION_CHANGE_TYPE.UPDATE, null, Math.max(this.data_.blobs.length, this.constructor.PAGE_SIZE_)); 129 this.resetData_(); 130 this.loadMoreResults(); 131 }; 132 133 cam.SearchSession.prototype.close = function() { 134 if (this.socket_) { 135 this.socket_.close(); 136 } 137 }; 138 139 cam.SearchSession.prototype.getMeta = function(blobref) { 140 return this.data_.description.meta[blobref]; 141 }; 142 143 cam.SearchSession.prototype.getResolvedMeta = function(blobref) { 144 var meta = this.data_.description.meta[blobref]; 145 if (meta && meta.camliType == 'permanode') { 146 var camliContent = cam.permanodeUtils.getSingleAttr(meta.permanode, 'camliContent'); 147 if (camliContent) { 148 return this.data_.description.meta[camliContent]; 149 } 150 } 151 return meta; 152 }; 153 154 cam.SearchSession.prototype.getTitle = function(blobref) { 155 var meta = this.getMeta(blobref); 156 if (meta.camliType == 'permanode') { 157 var title = cam.permanodeUtils.getSingleAttr(meta.permanode, 'title'); 158 if (title) { 159 return title; 160 } 161 } 162 var rm = this.getResolvedMeta(blobref); 163 return (rm && rm.camliType == 'file' && rm.file.fileName) || (rm && rm.camliType == 'directory' && rm.dir.fileName) || ''; 164 }; 165 166 cam.SearchSession.prototype.resetData_ = function() { 167 this.data_ = { 168 blobs: [], 169 description: { 170 meta: {} 171 } 172 }; 173 }; 174 175 cam.SearchSession.prototype.initSocketUri_ = function(currentUri) { 176 if (!goog.global.WebSocket) { 177 return; 178 } 179 180 this.socketUri_ = currentUri; 181 var config = this.connection_.getConfig(); 182 this.socketUri_.setPath(goog.uri.utils.appendPath(config.searchRoot, 'camli/search/ws')); 183 this.socketUri_.setQuery(goog.Uri.QueryData.createFromMap({authtoken: config.wsAuthToken || ''})); 184 if (this.socketUri_.getScheme() == "https") { 185 this.socketUri_.setScheme("wss"); 186 } else { 187 this.socketUri_.setScheme("ws"); 188 } 189 }; 190 191 cam.SearchSession.prototype.getContinuation_ = function(changeType, opt_continuationToken, opt_limit) { 192 return this.connection_.search.bind(this.connection_, this.query_, this.constructor.DESCRIBE_REQUEST, opt_limit || this.PAGE_SIZE_, opt_continuationToken, 193 this.searchDone_.bind(this, changeType)); 194 }; 195 196 cam.SearchSession.prototype.searchDone_ = function(changeType, result) { 197 if (changeType == this.constructor.SEARCH_SESSION_CHANGE_TYPE.APPEND) { 198 this.data_.blobs = this.data_.blobs.concat(result.blobs); 199 goog.mixin(this.data_.description.meta, result.description.meta); 200 } else { 201 this.data_.blobs = result.blobs; 202 this.data_.description = result.description; 203 } 204 if (!this.data_.blobs || this.data_.blobs.length == 0) { 205 this.resetData_(); 206 } 207 208 if (result.continue) { 209 this.continuation_ = this.getContinuation_(this.constructor.SEARCH_SESSION_CHANGE_TYPE.APPEND, result.continue); 210 } else { 211 this.continuation_ = null; 212 } 213 214 this.dispatchEvent({type: this.constructor.SEARCH_SESSION_CHANGED, changeType: changeType}); 215 216 if (changeType == this.constructor.SEARCH_SESSION_CHANGE_TYPE.NEW || 217 changeType == this.constructor.SEARCH_SESSION_CHANGE_TYPE.APPEND) { 218 this.startSocketQuery_(); 219 } 220 }; 221 222 cam.SearchSession.prototype.startSocketQuery_ = function() { 223 if (!this.socketUri_) { 224 return; 225 } 226 227 if (this.socket_) { 228 this.socket_.close(); 229 } 230 231 var numResults = 0; 232 if (this.data_ && this.data_.blobs) { 233 numResults = this.data_.blobs.length; 234 } 235 var query = this.connection_.buildQuery(this.query_, this.constructor.DESCRIBE_REQUEST, Math.max(numResults, this.constructor.PAGE_SIZE_)); 236 237 this.socket_ = new WebSocket(this.socketUri_.toString()); 238 this.socket_.onopen = function() { 239 var message = { 240 tag: 'q' + this.instance_, 241 query: query 242 }; 243 this.socket_.send(JSON.stringify(message)); 244 }.bind(this); 245 this.socket_.onmessage = function() { 246 this.supportsWebSocket_ = true; 247 // Ignore the first response. 248 this.socket_.onmessage = function(e) { 249 var result = JSON.parse(e.data); 250 this.searchDone_(this.constructor.SEARCH_SESSION_CHANGE_TYPE.UPDATE, result.result); 251 }.bind(this); 252 }.bind(this); 253 };