mirror of https://github.com/perkeep/perkeep.git
183 lines
5.8 KiB
JavaScript
183 lines
5.8 KiB
JavaScript
/*
|
|
Copyright 2013 Google Inc.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
goog.provide('SearchSession');
|
|
|
|
goog.require('goog.events.EventTarget');
|
|
goog.require('goog.Uri');
|
|
goog.require('goog.Uri.QueryData');
|
|
goog.require('goog.uri.utils');
|
|
|
|
goog.require('camlistore.ServerConnection');
|
|
|
|
// 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.
|
|
//
|
|
// TODO(aa): Only deltas should be sent from server to client
|
|
// TODO(aa): Need some way to avoid the duplicate query when websocket starts. Ideas:
|
|
// - Initial XHR query can also specify tag. This tag times out if not used rapidly. Send this same tag in socket query.
|
|
// - Socket assumes that client already has first batch of results (slightly racey though)
|
|
// - Prefer to use socket on client-side, test whether it works and fall back to XHR if not.
|
|
var SearchSession = function(connection, currentUri, query) {
|
|
this.connection_ = connection;
|
|
this.initSocketUri_(currentUri);
|
|
this.query_ = query;
|
|
|
|
this.data_ = {
|
|
blobs: [],
|
|
description: {
|
|
meta: {}
|
|
}
|
|
};
|
|
this.instance_ = this.constructor.instanceCount_++;
|
|
this.isComplete_ = false;
|
|
this.continuation_ = this.getContinuation_(this.constructor.SEARCH_SESSION_CHANGE_TYPE.NEW);
|
|
this.socket_ = null;
|
|
this.supportsWebSocket_ = false;
|
|
};
|
|
goog.inherits(SearchSession, goog.events.EventTarget);
|
|
|
|
// We fire this event when the data changes in any way.
|
|
SearchSession.SEARCH_SESSION_CHANGED = 'search-session-change';
|
|
|
|
// TODO(aa): This should go away once BlobItemContainer can reconcile changes for itself.
|
|
SearchSession.SEARCH_SESSION_CHANGE_TYPE = {
|
|
NEW: 1,
|
|
APPEND: 2,
|
|
UPDATE: 3
|
|
};
|
|
|
|
// This size doesn't matter, we don't use it. We only care about the aspect ratio.
|
|
// TODO(aa): Change describe to just return aspect directly.
|
|
SearchSession.prototype.THUMBNAIL_SIZE_ = 1000;
|
|
|
|
SearchSession.prototype.PAGE_SIZE_ = 50;
|
|
|
|
SearchSession.instanceCount_ = 0;
|
|
|
|
SearchSession.prototype.getQuery = function() {
|
|
return this.query_;
|
|
}
|
|
|
|
// Returns all the data we currently have loaded.
|
|
SearchSession.prototype.getCurrentResults = function() {
|
|
return this.data_;
|
|
};
|
|
|
|
// 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.
|
|
SearchSession.prototype.loadMoreResults = function() {
|
|
if (!this.continuation_) {
|
|
return;
|
|
}
|
|
|
|
var c = this.continuation_;
|
|
this.continuation_ = null;
|
|
c();
|
|
};
|
|
|
|
// Returns true if it is known that all data which can be loaded for this query has been.
|
|
SearchSession.prototype.isComplete = function() {
|
|
return this.isComplete_;
|
|
}
|
|
|
|
SearchSession.prototype.supportsChangeNotifications = function() {
|
|
return this.supportsWebSocket_;
|
|
};
|
|
|
|
SearchSession.prototype.close = function() {
|
|
if (this.socket_) {
|
|
this.socket_.close();
|
|
}
|
|
};
|
|
|
|
SearchSession.prototype.initSocketUri_ = function(currentUri) {
|
|
if (!goog.global.WebSocket) {
|
|
return;
|
|
}
|
|
|
|
this.socketUri_ = currentUri;
|
|
var config = this.connection_.getConfig();
|
|
this.socketUri_.setPath(goog.uri.utils.appendPath(config.searchRoot, 'camli/search/ws'));
|
|
this.socketUri_.setQuery(goog.Uri.QueryData.createFromMap({authtoken: config.wsAuthToken || ''}));
|
|
if (this.socketUri_.getScheme() == "https") {
|
|
this.socketUri_.setScheme("wss");
|
|
} else {
|
|
this.socketUri_.setScheme("ws");
|
|
}
|
|
};
|
|
|
|
SearchSession.prototype.getContinuation_ = function(changeType, opt_continuationToken) {
|
|
var describe = {
|
|
thumbnailSize: this.THUMBNAIL_SIZE_
|
|
};
|
|
return this.connection_.search.bind(this.connection_, this.query_, describe, this.PAGE_SIZE_, opt_continuationToken,
|
|
this.searchDone_.bind(this, changeType));
|
|
};
|
|
|
|
SearchSession.prototype.searchDone_ = function(changeType, result) {
|
|
if (changeType == this.constructor.SEARCH_SESSION_CHANGE_TYPE.APPEND) {
|
|
this.data_.blobs = this.data_.blobs.concat(result.blobs);
|
|
goog.mixin(this.data_.description.meta, result.description.meta);
|
|
} else {
|
|
this.data_.blobs = result.blobs;
|
|
this.data_.description = result.description;
|
|
}
|
|
|
|
if (result.continue) {
|
|
this.continuation_ = this.getContinuation_(this.constructor.SEARCH_SESSION_CHANGE_TYPE.APPEND, result.continue);
|
|
} else {
|
|
this.isComplete_ = true;
|
|
}
|
|
|
|
this.dispatchEvent({type: this.constructor.SEARCH_SESSION_CHANGED, changeType: changeType});
|
|
|
|
if (changeType == this.constructor.SEARCH_SESSION_CHANGE_TYPE.NEW ||
|
|
changeType == this.constructor.SEARCH_SESSION_CHANGE_TYPE.APPEND) {
|
|
this.startSocketQuery_();
|
|
}
|
|
};
|
|
|
|
SearchSession.prototype.startSocketQuery_ = function() {
|
|
if (!this.socketUri_) {
|
|
return;
|
|
}
|
|
|
|
if (this.socket_) {
|
|
this.socket_.close();
|
|
}
|
|
|
|
var describe = {
|
|
thumbnailSize: this.THUMBNAIL_SIZE_
|
|
};
|
|
var query = this.connection_.buildQuery(this.query_, describe, this.data_.blobs.length);
|
|
|
|
this.socket_ = new WebSocket(this.socketUri_.toString());
|
|
this.socket_.onopen = function() {
|
|
var message = {
|
|
tag: 'q' + this.instance_,
|
|
query: query
|
|
};
|
|
this.socket_.send(JSON.stringify(message));
|
|
}.bind(this);
|
|
this.socket_.onmessage = function() {
|
|
this.supportsWebSocket_ = true;
|
|
// Ignore the first response.
|
|
this.socket_.onmessage = function(e) {
|
|
var result = JSON.parse(e.data);
|
|
this.searchDone_(this.constructor.SEARCH_SESSION_CHANGE_TYPE.UPDATE, result.result);
|
|
}.bind(this);
|
|
}.bind(this);
|
|
};
|