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