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  };