2014-01-01 10:04:14 +00:00
/ *
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
2014-01-08 03:41:58 +00:00
http : //www.apache.org/licenses/LICENSE-2.0
2014-01-01 10:04:14 +00:00
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 .
* /
2014-01-08 05:28:01 +00:00
goog . provide ( 'cam.SearchSession' ) ;
2014-01-01 10:04:14 +00:00
goog . require ( 'goog.events.EventTarget' ) ;
goog . require ( 'goog.Uri' ) ;
goog . require ( 'goog.Uri.QueryData' ) ;
goog . require ( 'goog.uri.utils' ) ;
2014-01-08 05:28:01 +00:00
goog . require ( 'cam.ServerConnection' ) ;
2014-01-01 10:04:14 +00:00
// 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.
2014-01-08 05:28:01 +00:00
cam . SearchSession = function ( connection , currentUri , query ) {
2014-01-05 05:31:32 +00:00
goog . base ( this ) ;
2014-01-01 10:04:14 +00:00
this . connection _ = connection ;
this . initSocketUri _ ( currentUri ) ;
this . query _ = query ;
this . instance _ = this . constructor . instanceCount _ ++ ;
this . continuation _ = this . getContinuation _ ( this . constructor . SEARCH _SESSION _CHANGE _TYPE . NEW ) ;
this . socket _ = null ;
this . supportsWebSocket _ = false ;
2014-01-18 08:44:04 +00:00
this . resetData _ ( ) ;
2014-01-01 10:04:14 +00:00
} ;
2014-01-08 05:28:01 +00:00
goog . inherits ( cam . SearchSession , goog . events . EventTarget ) ;
2014-01-01 10:04:14 +00:00
// We fire this event when the data changes in any way.
2014-01-08 05:28:01 +00:00
cam . SearchSession . SEARCH _SESSION _CHANGED = 'search-session-change' ;
2014-01-01 10:04:14 +00:00
2014-03-22 13:19:29 +00:00
// TODO(aa): This is only used by BlobItemContainer. Once we switch over to BlobItemContainerReact completely, it can be removed.
2014-01-08 05:28:01 +00:00
cam . SearchSession . SEARCH _SESSION _CHANGE _TYPE = {
2014-01-01 10:04:14 +00:00
NEW : 1 ,
APPEND : 2 ,
UPDATE : 3
} ;
2014-05-20 22:59:53 +00:00
cam . SearchSession . PAGE _SIZE _ = 50 ;
2014-01-01 10:04:14 +00:00
2014-01-08 05:28:01 +00:00
cam . SearchSession . instanceCount _ = 0 ;
2014-01-01 10:04:14 +00:00
2014-01-08 05:28:01 +00:00
cam . SearchSession . prototype . getQuery = function ( ) {
2014-01-02 05:56:03 +00:00
return this . query _ ;
}
2014-01-01 10:04:14 +00:00
// Returns all the data we currently have loaded.
2014-03-17 15:44:14 +00:00
// It is guaranteed to return the following properties:
// blobs // non-zero length
// description
// description.meta
2014-01-08 05:28:01 +00:00
cam . SearchSession . prototype . getCurrentResults = function ( ) {
2014-01-01 10:04:14 +00:00
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.
2014-01-08 05:28:01 +00:00
cam . SearchSession . prototype . loadMoreResults = function ( ) {
2014-01-01 10:04:14 +00:00
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.
2014-01-08 05:28:01 +00:00
cam . SearchSession . prototype . isComplete = function ( ) {
2014-01-11 05:20:47 +00:00
return ! this . continuation _ ;
2014-01-01 10:04:14 +00:00
}
2014-01-08 05:28:01 +00:00
cam . SearchSession . prototype . supportsChangeNotifications = function ( ) {
2014-01-01 10:04:14 +00:00
return this . supportsWebSocket _ ;
} ;
2014-01-18 08:44:04 +00:00
cam . SearchSession . prototype . refreshIfNecessary = function ( ) {
if ( this . supportsWebSocket _ ) {
return ;
}
this . continuation _ = this . getContinuation _ ( this . constructor . SEARCH _SESSION _CHANGE _TYPE . UPDATE , null , Math . max ( this . data _ . blobs . length , this . constructor . PAGE _SIZE _ ) ) ;
this . resetData _ ( ) ;
this . loadMoreResults ( ) ;
} ;
2014-01-08 05:28:01 +00:00
cam . SearchSession . prototype . close = function ( ) {
2014-01-01 10:04:14 +00:00
if ( this . socket _ ) {
2014-01-02 05:56:03 +00:00
this . socket _ . close ( ) ;
2014-01-01 10:04:14 +00:00
}
} ;
2014-04-01 02:01:44 +00:00
cam . SearchSession . prototype . getMeta = function ( blobref ) {
return this . data _ . description . meta [ blobref ] ;
} ;
cam . SearchSession . prototype . getResolvedMeta = function ( blobref ) {
var meta = this . data _ . description . meta [ blobref ] ;
2014-04-06 07:56:32 +00:00
if ( meta && meta . camliType == 'permanode' ) {
2014-04-01 02:01:44 +00:00
var camliContent = cam . permanodeUtils . getSingleAttr ( meta . permanode , 'camliContent' ) ;
if ( camliContent ) {
return this . data _ . description . meta [ camliContent ] ;
}
}
return meta ;
} ;
cam . SearchSession . prototype . getTitle = function ( blobref ) {
var meta = this . getMeta ( blobref ) ;
if ( meta . camliType == 'permanode' ) {
var title = cam . permanodeUtils . getSingleAttr ( meta . permanode , 'title' ) ;
if ( title ) {
return title ;
}
}
var rm = this . getResolvedMeta ( blobref ) ;
return ( rm && rm . camliType == 'file' && rm . file . fileName ) || ( rm && rm . camliType == 'directory' && rm . dir . fileName ) || '' ;
} ;
2014-01-18 08:44:04 +00:00
cam . SearchSession . prototype . resetData _ = function ( ) {
this . data _ = {
blobs : [ ] ,
description : {
meta : { }
}
} ;
} ;
2014-01-08 05:28:01 +00:00
cam . SearchSession . prototype . initSocketUri _ = function ( currentUri ) {
2014-01-01 10:04:14 +00:00
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" ) ;
}
} ;
2014-01-18 08:44:04 +00:00
cam . SearchSession . prototype . getContinuation _ = function ( changeType , opt _continuationToken , opt _limit ) {
2014-05-21 17:05:07 +00:00
return this . connection _ . search . bind ( this . connection _ , this . query _ , cam . ServerConnection . DESCRIBE _REQUEST , opt _limit || this . constructor . PAGE _SIZE _ , opt _continuationToken ,
2014-01-01 10:04:14 +00:00
this . searchDone _ . bind ( this , changeType ) ) ;
} ;
2014-01-08 05:28:01 +00:00
cam . SearchSession . prototype . searchDone _ = function ( changeType , result ) {
2014-01-01 10:04:14 +00:00
if ( changeType == this . constructor . SEARCH _SESSION _CHANGE _TYPE . APPEND ) {
this . data _ . blobs = this . data _ . blobs . concat ( result . blobs ) ;
2014-01-02 05:56:03 +00:00
goog . mixin ( this . data _ . description . meta , result . description . meta ) ;
2014-01-01 10:04:14 +00:00
} else {
this . data _ . blobs = result . blobs ;
this . data _ . description = result . description ;
}
2014-03-17 15:44:14 +00:00
if ( ! this . data _ . blobs || this . data _ . blobs . length == 0 ) {
this . resetData _ ( ) ;
}
2014-01-01 10:04:14 +00:00
if ( result . continue ) {
this . continuation _ = this . getContinuation _ ( this . constructor . SEARCH _SESSION _CHANGE _TYPE . APPEND , result . continue ) ;
} else {
2014-01-11 05:20:47 +00:00
this . continuation _ = null ;
2014-01-01 10:04:14 +00:00
}
2014-01-02 05:56:03 +00:00
this . dispatchEvent ( { type : this . constructor . SEARCH _SESSION _CHANGED , changeType : changeType } ) ;
2014-01-01 10:04:14 +00:00
if ( changeType == this . constructor . SEARCH _SESSION _CHANGE _TYPE . NEW ||
changeType == this . constructor . SEARCH _SESSION _CHANGE _TYPE . APPEND ) {
this . startSocketQuery _ ( ) ;
}
} ;
2014-01-08 05:28:01 +00:00
cam . SearchSession . prototype . startSocketQuery _ = function ( ) {
2014-01-01 10:04:14 +00:00
if ( ! this . socketUri _ ) {
return ;
}
if ( this . socket _ ) {
this . socket _ . close ( ) ;
}
2014-01-27 23:13:51 +00:00
var numResults = 0 ;
if ( this . data _ && this . data _ . blobs ) {
numResults = this . data _ . blobs . length ;
}
2014-05-21 17:05:07 +00:00
var query = this . connection _ . buildQuery ( this . query _ , cam . ServerConnection . DESCRIBE _REQUEST , Math . max ( numResults , this . constructor . PAGE _SIZE _ ) ) ;
2014-01-01 10:04:14 +00:00
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 ) ;
} ;