--[[----------------------------------------------------------------------------

WFRxDocStore.lua

--------------------------------------------------------------------------------

ADOBE CONFIDENTIAL
------------------
Copyright 2015 Adobe Systems Incorporated
All Rights Reserved.

NOTICE: All information contained herein is, and remain the property of
Adobe Systems Incorporated and its suppliers, if any. The intellectual and
technical concepts contained herein are proprietary to Adobe Systems
Incorporated and its suppliers and may be covered by U.S. and Foreign
Patents, patents in process, and are protected by trade secret or copyright
law.  Dissemination of this information or reproduction of this material is
strictly forbidden unless prior written permission is obtained from
Adobe Systems Incorporated.

------------------------------------------------------------------------------]]

	-- RxLua
local Rx = require 'Rx'

	-- WF.core.diagnostic_log
local WFDiagnosticLog = require 'WFDiagnosticLog'

	-- WF.core.logging
local WFLogger = require 'WFLogger'

	-- WF.core.lua.threading
local WFCurrentThread = require 'WFCurrentThread'

	-- WF.core.os
local KSDate = require 'KSDate'
local KSSystemNotifications = require "KSSystemNotifications"
local KSPathUtils = require "KSPathUtils"
local WFFileUtils = require "WFFileUtils"

	-- WF.core.rxlua
local WFRxRunUntilDone = require 'WFRxRunUntilDone'
local WFRxWeakObservableTable = require 'WFRxWeakObservableTable'

	-- WF.core.string
local KSStringUtils = require 'KSStringUtils'

	-- WF.core.threading
local WFWorkerThread = require 'WFWorkerThread'

	-- WF.core.utils
local KSIdGenerator = require 'KSIdGenerator'
local WFArguments = require 'WFArguments'
local WFCloseSupport = require 'WFCloseSupport'
local WFConfiguration = require 'WFConfiguration'
local WFPerformanceReport = require 'WFPerformanceReport'
local WFProgressReporter = require "WFProgressReporter"

	-- WF.sqlite.doc_store
local WFRxDocStoreUtils = require "WFRxDocStoreUtils"

	-- WF.core.sqlite
local WFRxDocStoreMessageProcessor = require "WFRxDocStoreMessageProcessor"

--------------------------------------------------------------------------------
 
local kAllDocuments = "all"
local kCatalogDocuments = "catalog"
local kSpaceFeedDocuments = "spaceFeed"

--------------------------------------------------------------------------------

local check = assert( WFArguments.check )

local runUntilDone = WFRxRunUntilDone

local unpackDocValue = assert( WFRxDocStoreUtils.unpackDocValue )

local WFDebugTraceback_traceback = require 'WFDebugTraceback'.traceback

local RECORD_TRANSACTION_SOURCES = not not( WFConfiguration.WFRxDocStore.recordTransactionSources )
local RECORD_API_PERFORMANCE = not not( WFConfiguration.WFRxDocStore.recordApiPerformance )

local kDocStoreInitProgress = 'docStoreInitProgress'

local kDocStoreTypeMigratorProgress = 'docStoreTypeMigratorProgress'

--============================================================================--

-- @mark WFRxDocStore class

--------------------------------------------------------------------------------
--[[!
	\class WFRxDocStore

	TO DO: Write new documentation for this
]]--

local WFRxDocStore = {

	_path = false,
		-- (string) path to doc store DB

	_docStoreImp = false,
		-- (WFWorkerThread) reference to the worker thread

	_docStoreChangesFeedImp = false,
		-- (WFWorkerThread) reference to a worker thread that processes changes feed from db

	_changeCounter = false,
		-- (ReplaySubject< number >) most recent rev change counter value. Initially nil.
		-- Lazily instantiated.

	_changeCounterSubscription = false,
		-- (Disposable) keep-alive subscription for _changeCounter

	_ongoingDocValues = false,
		-- (WFRxWeakObservableTable) document value subjects that are currently active
			-- key: (string) doc ID
			-- value: (Rx.Observable) current document value

	_ongoingLocalDocValues = false,
		-- (WFRxWeakObservableTable) local document value subjects that are currently active
			-- key: (string) local doc name
			-- value: (Rx.Observable) current document value

	_initializationProgress = false,
		-- (WFProgressReporter) reports the progress of the docStore initialization
		-- if index creation is suspected to take considerable time.

	_log = false,
		-- (WFLogger) logger for this doc store

	_recentErrorLog = false,
		-- (table) contains the most recent errors from the doc store for the
		-- diagnostic log

	_recordTransactionSources = false,
		-- (boolean) set to true when requested to record transaction sources,
		-- either via config flag or via :recordTransactionSources API call

	_transactionSources = false,
		-- (table) used when RECORD_TRANSACTION_SOURCES is true
			-- key: (string) stack trace as string
			-- value: (number) # hits

	_recordApiPerformance = false,
		-- (boolean) set to true when requested to record cross-thread API performance,
		-- either via config flag or via :recordApiPerformance API call

	_perfLog = false,
		-- (table) used when RECORD_API_PERFORMANCE is true
			-- key: (string) API call name
			-- value: (table) info on statement (count, min, max, total)

	_captureTracebackForFunc = false,
		-- (table) used to gate access to capture traceback behavior
			-- key: (string) API call name
			-- value: (boolean) true if we should capture tracebacks for error reporting

	_captureTracebackForAny = false,
		-- (boolean) optimization: true if _captureTracebackForFunc has any members

	class = {
		changedDocsSourceAllDocuments = kAllDocuments,
		changedDocsSourceCatalogDocuments = kCatalogDocuments,
		changedDocsSourceSpaceFeedDocuments = kSpaceFeedDocuments
	},

	WFCloseSupport.addCloseSupportMethods,

}

--------------------------------------------------------------------------------
--[[!
	\brief Constructs a new document database.

	\param path (string) path to the document database.

		For testing, you can use the special ID ":memory:" which
		creates a transient catalog in memory, or ":temp:"
		which creates a transient catalog on disk that is deleted
		when the DB is no longer needed. You may optionally add any
		suffix following ":memory:" or ":temp:" to create distinct
		instances of temporary document databases that can be addressed
		from multiple threads.

		If no database exists at this path, it will be created.

		The path string must be non-empty.

	\param optArgs (optional, table) optional args to the docStore

		- initProgressRequested (boolean)

			The docStore will install a listener for progress messages
			broadcasted from sqlite during docStore initialization.
			These messages will then be put on a stream which can get
			observed on a stream which can be retrieved
			via getInitializationProgress()
]]--

function WFRxDocStore:__init( path, optArgs )

	check( 'WFRxDocStore', 'string', '?table' )

	-- Make sure :memory: and :temp: DBs don't conflict with each other.

	if path == ':memory:' or path == ':temp:' then
		path = path .. tostring( KSIdGenerator.localCounter() )
	end

	self._path = path

	if optArgs and optArgs.initProgressRequested then

		self._initializationProgress = WFProgressReporter( 'docStore initialization' )

		-- Register a callback on broadcasted 'docStoreInitProgress' notifications if requested.
		-- The following handler deals with progress events from WFSQLiteDatabase.
		
		KSSystemNotifications.ownedOnNotificationDo( kDocStoreInitProgress, self, function( ... )
			local broadcastPath, progress = unpack( arg[ 1 ] )
			if path == broadcastPath then
				-- broadcast is targeted at this docStore
				if type( progress ) == 'number' then
					-- The LrD progressbar UI insists on a minimum totalCount of 250.
					-- We scale the progress values by 10, but still progress in 1% steps.
					self._initializationProgress:setTotal( 1000 ) -- null op if total is unchanged
					self._initializationProgress:setProcessedCount( 10 * progress )
				end
			end
		end )


		KSSystemNotifications.ownedOnNotificationDo( kDocStoreTypeMigratorProgress, self, function( ... )
		
			local progressInfo = arg[ 1 ]
				
			if progressInfo then
				local progress = progressInfo.progress
				local totalCount = progressInfo.totalCount
				
				if progressInfo.dbPath == path then
				
					if type( progress ) == 'number' and
							type( totalCount ) == 'number' then
			
						self._initializationProgress:setDescription( 'docStore type/subtype migration' )
						self._initializationProgress:setTotal( totalCount ) -- null op if total is unchanged
						self._initializationProgress:setProcessedCount( progress )
							
					elseif progressInfo.hasSpaceError then
					
						local errorInfo = {
							errorType = 'NotEnoughSpace',
							spaceNeeded = progressInfo.spaceNeeded
						}
					
						self._initializationProgress:reportError( errorInfo, true )
						self._initializationProgress:done( errorInfo )
						
					elseif progressInfo.done then
						self._initializationProgress:done()
					end
				end
			end
		end )
		
	end

	self:setOwned { _docStoreImp = 
						WFRxDocStoreMessageProcessor.bootstrapWFRxDocStoreImp( path ) }

		-- TODO [scouten 2015-05-07]: Allow constructor to accept a message processor
		-- which (we hope) will have the WFRxDocStoreImp messages wired up.

	self:setOwned { _docStoreChangesFeedImp = WFWorkerThread( "WFRxDocStoreChangesFeedImp.lua", path ) }

	self._log = WFLogger( tostring( self ) )

	self._recentErrorLog = {}
	self._perfLog = {}
	self._transactionSources = {}
	self._captureTracebackForFunc = {}

	self._recordTransactionSources = RECORD_TRANSACTION_SOURCES or false
	self._recordApiPerformance = RECORD_API_PERFORMANCE or false

	WFDiagnosticLog.addLogProvider( self )

	self._ongoingDocValues = WFRxWeakObservableTable(
		function( docId )
			return self._docStoreImp:request( 'ongoingDocValue', docId )
		end, 1 ) -- replay most recent value to any new observers

	self._ongoingLocalDocValues = WFRxWeakObservableTable(
		function( name )
			return self._docStoreImp:request( 'ongoingLocalDocValue', name )
		end, 1 ) -- replay most recent value to any new observers


end

--------------------------------------------------------------------------------

function WFRxDocStore:close()

	self._log:info( "doc store closing" )

	if self._changeCounterSubscription then
		self._changeCounterSubscription:dispose()
	end

	self._ongoingDocValues:dispose()

	self._ongoingLocalDocValues:dispose()

end

--------------------------------------------------------------------------------

-- @mark shared utilities

--------------------------------------------------------------------------------

local function recordTransaction( self, methodName )

	if self._recordTransactionSources then

		local traceback = WFDebugTraceback_traceback { 'WFRxDocStore.lua', 'Class.lua' }

		local key = string.format( "%s via %s", methodName, tostring( traceback ) )
		self._transactionSources[ key ] = ( self._transactionSources[ key ] or 0 ) + 1

	end

end


--------------------------------------------------------------------------------

-- @mark logging

--------------------------------------------------------------------------------
--[[!
	\brief Configure logging level for this doc store.

	\param level (string) desired log level (See ,, etc., for values.)

	Set level to 'INFO' to see API calls with their parameters in the log.

	Set level to 'DEBUG' to see API calls and the responses in the log.
]]--

function WFRxDocStore:setLogLevel( level )

	check( 'WFRxDocStore', '+string' )

	self._log:setLevel( level )

	self._log:infof( "\n\ndocStore:setLogLevel( %s )", level )

end

--------------------------------------------------------------------------------
--[[!
	\brief (Primarily for debugging) Enable a debugging mode that records
		the sources for transactional API calls.

	Intended for use in concert with diagnostic log.
]]--

function WFRxDocStore:recordTransactionSources()

	check( 'WFRxDocStore' )

	self._recordTransactionSources = true

end

--------------------------------------------------------------------------------
--[[!
	\brief (Primarily for debugging) Enable a debugging mode that records
		the performance for cross-thread API calls.

	Intended for use in concert with diagnostic log.
]]--

function WFRxDocStore:recordApiPerformance()

	check( 'WFRxDocStore' )

	self._recordApiPerformance = true

end

--------------------------------------------------------------------------------
--[[
	\brief (Primarily for debugging) Enable a debugging mode that reocrds
		the tracebacks for cross-thread API calls.

	If you are having trouble figuring out how an error is occurring, you can
	enable captureTracebackForFunc for the corresponding function. This will
	cause the calling stack trace to be included when the promise resolves
	in error status.

	Use this mode sparingly: It will dramatically decrease performance.
]]--

function WFRxDocStore:captureTracebackForFuncs( tracebackFuncs )

	check( 'WFRxDocStore', 'table' )

	self._captureTracebackForAny = not not( next( tracebackFuncs ) )
	self._captureTracebackForFunc = tracebackFuncs

end

--------------------------------------------------------------------------------

local function maybeLogResponse( self, funcName, observable )

	if self._captureTracebackForAny
		or self._log:will_debug()
		or self._recordApiPerformance
	then

		local traceback = self._captureTracebackForFunc[ funcName ]
			and WFDebugTraceback_traceback { 'WFRxDocStore.lua', 'Class.lua' }

		local startTime = debug.timeNow()

		local gotFirstResponse = false

		local function done( status, ... )

			if gotFirstResponse then
				return
			end

			gotFirstResponse = true

			if status == 'error' and traceback then
				self._log:errorf( "\n\ndocStore:%s called via %s\n\nresolved with %s, %s %s %s",
								  funcName, tostring( traceback ), status, ... )
			else
				self._log:debugf( "\n\ndocStore:%s resolved with %s, %s %s %s",
								  funcName, status, ... )
			end

			if self._recordApiPerformance then

				local elapsedTime = debug.timeNow() - startTime

				local stat = self._perfLog[ funcName ]

				if stat then
					stat.count = stat.count + 1
					stat.min = math.min( stat.min, elapsedTime )
					stat.max = math.max( stat.max, elapsedTime )
					stat.total = stat.total + elapsedTime
				else
					stat = {
						funcName = funcName,
						count = 1,
						min = elapsedTime,
						max = elapsedTime,
						total = elapsedTime,
					}
					self._perfLog[ funcName ] = stat
				end

			end

		end

		return observable:tap(

			function( ... )
				done( 'data', ... )
			end,

			function( ... )

				local errorLog = self._recentErrorLog

				local bucketLog = errorLog[ funcName ] or {}
				errorLog[ funcName ] = bucketLog

				bucketLog[ #bucketLog + 1 ] = {

					time = KSDate.timeInIso8601FormatWithTimeZone(),
					error = ( ... ),

				}

				if #bucketLog > 10 then
					table.remove( bucketLog, 1 )
				end

				done( 'error', ... )

			end,

			function()
				done( 'completed' )
			end )

	else

		return observable

	end

end

--------------------------------------------------------------------------------

local function descriptionFromCallStack( depthOfCaller )

	local debugInfo = debug.getinfo( depthOfCaller + 1 )

	return string.format( "%s@%s:%s", ( debugInfo.name or 'unknown' ), debugInfo.source, debugInfo.currentline )

end

--------------------------------------------------------------------------------

-- @mark whole document values

--------------------------------------------------------------------------------
--[[!
	\brief Retrieves the value of a single document once.

	\param docId (string) the document's unique ID
	\param revId (optional, string) revision to retrieve (can also be a label name)
	\param optFieldSpec (optional, table) paths of fields to include.
		For example, for the purpose of retrieving a thumbnails, this optFieldSpec might be used:
		{
			_id = true
			_revId = true
			_serverProvidedMetadata: {
				base = true
				links = {
					[ "/rels/rendition_type/thumbnail2x" ] = true
				}
			}
		}
	\param simplifyDeletedDoc (optional boolean) Defaults to true. If set to false, 
		then deleted docs will be returned in their full form. If true or nil, a
		simplified tombstone will be returned in place of a full deleted document.
		
	\return <code>Observable< table ></code> value for this document

	This Observable will fire exactly once with the value of the document at the
	time of this request. To monitor a document value on an ongoing basis,
	see \c :ongoingDocValue.

	Note that the document content will be annotated with the following
	system-generated entries (at the top level of the value table):

		* \c _id (string): the document's unique ID
		* \c _revId (string): the document's current revision ID
		* \c _empty (boolean): true if there isn't actually a value for this doc ID
		* \c _deleted (boolean): true if this revision is deleted (only used when
			\c revId is present
		* \c _changeCounter (number): a monotonically-increasing change counter
			that is increased whenever any branch of this document is revised
			(The functions \c docChangeCounterB and \c docChangeCounterP will
			return the maximum value of \c _changeCounter across the entire
			database.)
		* \c _labels (table): if present, this table is the set of labels that
			have been applied to this revision.
]]--

function WFRxDocStore:docValue( docId, revId, optFieldSpec, simplifyDeletedDoc )

	check( 'WFRxDocStore', 'string|number', '?+string', '?table', '?boolean' )

	self._log:infof( "\n\ndocStore:docValue( %s, %s )", docId, revId )

	return maybeLogResponse( self, 'docValue',
		self._docStoreImp:request( 'docValue', docId, revId, nil, simplifyDeletedDoc ) )
		 :map( function( docArchive )
		 	return unpackDocValue( docArchive, optFieldSpec )
		 end )

end

--------------------------------------------------------------------------------
--[[!
	\brief Retrieves a document and assert that the docType is as expected.
		This is a wrapper around docValue that asserts that the resulting document's
		\c type field matches the \cdocType parameter. If it does not match,
		the observable signals an error.

	\param docType (string) the document's docType
	\param docId (string) the document's unique ID
	\param revId (optional, string) revision to retrieve

	\return <code>Observable< table ></code> value for this document, or an error
		if the \c docType does not match \c document.type field.

	\see docValues
]]--

function WFRxDocStore:docValueCheckType( docType, docId, revId )

	check( 'WFRxDocStore', '+string', 'string', '?+string' )

	return self:docValue( docId, revId )
		:map( function( docValue )

			assert( docValue.type == docType or docValue._deleted or docValue._empty,
				"Document type invalid. Expected " .. docType
					.. " but got " .. tostring( docValue.type ) )

			return docValue

		end )

end

--------------------------------------------------------------------------------
--[[! 
	\brief Retrieves the local Id of a document given the full doc id. If there is
	no document with the given fullDocId, nil is returned.
	
	\param docId (string) the document's full doc id
	\param includeDeleted (boolean) optional parameter to indicate whether to return the
		id of a deleted document
		
	\return <code>Observable<number></code> local doc id for this document or nil
	if the full doc id does not exist
]]--

function WFRxDocStore:docIdToLocalId( docId, includeDeleted )
	
	check( 'WFRxDocStore', '+string' )
	
	return self._docStoreImp:request( 'docIdToLocalId', docId, includeDeleted )

end

--------------------------------------------------------------------------------
--[[!
	\brief Retrieves a map of full doc ids to local ids for the given array of full doc ids. If 
	a given doc id does not exist, it will not be present in the returned map.
	
	\param docIds (table) an array of docIds
	\param includeDeleted (boolean) optional parameter to indicate whether to return the
		id of a deleted document
		
	\return <code>Observable<table></code> map of full doc ids to local ids
]]--

function WFRxDocStore:bulkDocIdToLocalIdAsMap( docIds, includeDeleted )
	
	check( 'WFRxDocStore', 'table' )
	
	return self._docStoreImp:request( 'bulkDocIdToLocalIdAsMap', docIds, includeDeleted )
end

--------------------------------------------------------------------------------
--[[! 
	\brief Retrieves the full doc id of a document given the local id. If there is
	no document with the given local id, nil is returned.
	
	\param docId (number) the document's full doc id
	\param includeDeleted (boolean) optional parameter to indicate whether to return the
		id of a deleted document
		
	\return <code>Observable<string></code> local doc id for this document or nil
	if the full doc id does not exist
]]--

function WFRxDocStore:localIdToDocId( localId, includeDeleted )
	
	check( 'WFRxDocStore', 'number' )
	
	return self._docStoreImp:request( 'localIdToDocId', localId, includeDeleted )

end

--------------------------------------------------------------------------------
--[[!
	\brief Retrieves a map of local ids to full doc ids for the given array of local ids. If 
	a given local id does not exist, it will not be present in the returned map.
	
	\param localIds (table) an array of local ids
	\param includeDeleted (boolean) optional parameter to indicate whether to return the
		id of a deleted document
		
	\return <code>Observable<table></code> map of local ids to full doc ids
]]--

function WFRxDocStore:bulkLocalIdToDocIdAsMap( localIds, includeDeleted )
	
	check( 'WFRxDocStore', 'table' )
	
	return self._docStoreImp:request( 'bulkLocalIdToDocIdAsMap', localIds, includeDeleted )
end

--------------------------------------------------------------------------------
--[[!
	\brief Gets many document values at once.

	\param docIds (table) array of document IDs
		Optional arguments to this api are passed as named parameters in the
		docIds table they are:
		* \c label (optional, string): Specifies what label-ed revision to
			get for the documents.
		* \c includeInitialChangeCounter (optional, boolean): if true, return the
			first changeCounter (sequence number) related to the documents.
		* \c simplifyDeletedDocs (optional, boolean): Defaults to true. If set to
			false, then deleted docs will be returned in their full form. If true
			or nil, simplified tombstones will be returned in place of full deleted 
			documents.

	\param optFieldSpec (optional, table) paths of fields to include, and set field to true.
		For example, for the purpose of retrieving thumbnails, this optFieldSpec might be used:
		{
			_id = true,
			_revId = true,
			_serverProvidedMetadata = {
				base = true,
				links = {
					["/rels/rendition_type/thumbnail2x"] = { href = true }
				}
			}
		}

	\param description (optional string) string used by debug panel and
		diagnostic log to identify what operations made calls.

	\param skipDocIdsValidation (optional boolean) If true, the array of docIds is not
		validated. This should only be used in select circumstances where performance
		is critical, and docIds is already known to be valid.

	\return <code>Observable< table ></code> resolves to an array of document
		values when done. The items in the array will be the document values
		in the same sequence as requested on input. Each item will contain the
		same results as would be returned for an equivalent \c :docValue call.

	It is not possible to request specific revision IDs via this API.
]]--

function WFRxDocStore:bulkDocValue( docIds, optFieldSpec, description, skipDocIdsValidation )

	check( 'WFRxDocStore', 'table', '?table', '?string', '?boolean' )
	self._log:infof( "\n\ndocStore:bulkDocValue( %s, %s )", docIds, optFieldSpec )
	
	if #docIds > 1000 then
		self._log:errorf( "docStore:bulkDocValue called with %d ids, performance will suffer \n%s", #docIds, ':traceback:' )
	end

	description = description or descriptionFromCallStack( 2 )

	return maybeLogResponse( self, 'bulkDocValue',
		self._docStoreImp:request( 'bulkDocValue', docIds, nil, description, skipDocIdsValidation ) )
		 :map( function( docArchives )
		 
		 	local result = {}
			
			for _, docArchive in ipairs( docArchives ) do
				result[ #result + 1 ] = unpackDocValue(  docArchive, optFieldSpec )
			end
			
			return result
		 
		 end )

end

--------------------------------------------------------------------------------
--[[!
	\brief Retrieves this value of a single document as a Subject.

	The behavior will be updated reasonably quickly as changes come in from
		this and other threads.

	\warning You should avoid watching many documents via \c ongoingDocValue or
		\c ongoingFieldValue simultaneously for performance reasons. Console logs
		will be generated if the number of \c ongoingDocValue instances is large.
		(Note that \c ongoingFieldValue uses \c ongoingDocValue in its implementation,
		but the performance concern is around the number of \b documents,
		not the number of fields, being watched.)

	\param docId (string) the document's unique ID

	\return <code>Observable< table ></code> value for this document

	Note that the document content will be annotated with the following
	system-generated entries (at the top level of the value table):

	-- TODO [scouten 2014-08-15]: Update for RevDB semantics.

		* \c _id (string - nonempty): the document's unique ID
		* \c _revId (string): the document's current revision ID
		* \c _empty (boolean): true if there isn't actually a value for this doc ID + slot
		* \c _changeCounter (number): a monotonically-increasing change counter
			that is increased whenever any revision of this document is revised
			(The functions \c changeCounter and \c changeCounterOnce will
			return the maximum value of \c _changeCounter across the entire
			database.)
]]--

function WFRxDocStore:ongoingDocValue( docId )

	check( 'WFRxDocStore', '+string|number' )

	self._log:infof( "\n\ndocStore:ongoingDocValue( %s )", tostring( docId ) )

	return self._ongoingDocValues:observableForKey( docId )

end

--------------------------------------------------------------------------------
--[[!
	\brief Sets the document value, replacing any existing value.

	\param value (table) new value for the entire document

	\c value \b must contain the following elements:
		* \c _id (string|integer) the document's unique ID. If the document already
			exists and you know the _localId, you may supply an integer
			local ID instead of the unique ID.
		* \c _revId (string) the document's current revision ID
			(unless the document is new, in which case it may be omitted)
		* \c _revIds (array of strings) intended for use in sync; if provided:
			* the array must be non-empty
			* the new revision's ID will be the last rev ID in the array
			* the new revision's parent ID will be the \b LAST rev ID in the
				array that matches an existing ID
			* any conflict that might arise will be resolved by making the
				new revision the \b losing revision (separate code will resolve
				the conflict)
			* in this case, \c _revId must \b not be specified
		* \c _labels (table) revision labels to apply to this revision
			(Any existing labels named here will be moved to this revision.)
			
			Note that labels starting with an underscore are treated differently than 
			plain labels *not* starting with an underscore: labels starting with an 
			underscore will not prevent a removal of the revision as part of the revision 
			compaction, i.e. if a new revision is created with a plain label, the revision 
			with the underscore label will be removed.
			
	\c value \b may contain the following elements:
		* \c _deleted (boolean): if present, must be \c true and no other
			values may be present
		* \c _newRevId (string) if present, the doc store uses this uuid as the
			new revision id

	\b Important: The top-level key \c _localOnly gets special handling. If
		no value is provided for this key, any previous value is left in place.
		This behavior does not apply to any other key. This does imply that you
		can not delete any existing \c _localOnly entry in the document.

	\param description (optional string) string used by debug panel and
		diagnostic log to identify what operations made calls.

	\return <code>Observable< string ></code> resolves with the new value's
		revision ID when the write completes. You may safely ignore this Observable.
]]--

function WFRxDocStore:setDocValue( value, description )

	check( 'WFRxDocStore', 'table', '?string' )

	description = description or descriptionFromCallStack( 2 )

	self._log:infof( "\n\ndocStore:setDocValue( %s )\n\nvia %s", value, ':traceback:' )
	recordTransaction( self, 'setDocValue' )

	return maybeLogResponse( self, 'setDocValue',
							 self._docStoreImp:request( 'setDocValue', value, description ) )
		:call( runUntilDone )

end

--------------------------------------------------------------------------------
--[[!
	\brief Sets many document values at once, replacing any existing values.

	\param docValues (table) array of entire document values
	\param localDocValues (?table) optional array of local document values
	
	The array may contain zero or more document values. Each value entry
	looks like the \c value parameter to \c :setDocValue.

	The array may also contain a <code>label = "name"</code> entry. If this exists,
	that label will be applied to all documents in this batch.

	If an error occurs while setting any one of these document values, then the
	entire set of document values will be discarded. In other words, this set
	of documents succeeds or fails atomically.

	\param description (optional string) string used by debug panel and
		diagnostic log to identify what operations made calls.

	\return <code>Observable< table ></code> resolves to a single value containing
		an array of the following tables with one entry for each of the supplied doc
		values in order:
			id - the supplied doc _id.
			ok - success or failure, currently always true.
			revId - the revId associated with the document.
			localId - the integer based local doc id that was just allocated.
		You may safely ignore this result.
]]--

function WFRxDocStore:bulkSetDocValue( docValues, localDocValues, description )

	check( 'WFRxDocStore', 'table', '?table' )

	description = description or descriptionFromCallStack( 2 )

	self._log:infof( "\n\ndocStore:bulkSetDocValue( %s )\n\nvia %s", docValues, ':traceback:' )
	recordTransaction( self, 'bulkSetDocValue' )

	return maybeLogResponse( self, 'bulkSetDocValue',
							 self._docStoreImp:request( 'bulkSetDocValue', docValues, localDocValues, description ) )
		:call( runUntilDone )

end

--------------------------------------------------------------------------------
--[[!
	\brief Returns all current revisions of a document.

	\param docId (string) the document's unique ID
	\param includeDeleted (boolean) true to include revisions that are current
		but have been deleted

	\return <code>Observable< table ></code> array of revisions

	Each revision in the array will look like the response from \c :docValue.

	If there are no conflicts (no resolved conflicts, in the case of \c includeDeleted),
	then this function will look like the single response from \c :docValue.

	The winning revision will always be first in the array.
]]--

function WFRxDocStore:currentRevisionsForDoc( docId, includeDeleted )

	check( 'WFRxDocStore', 'string', '?boolean' )

	self._log:infof( "\n\ndocStore:currentRevisionsForDoc( %s, %s )", docId, includeDeleted )

	return maybeLogResponse( self, 'currentRevisionsForDoc',
			self._docStoreImp:request( 'currentRevisionsForDoc', docId, includeDeleted ) )

end

--------------------------------------------------------------------------------
--[[!
	\brief Deletes a document or a specific revision of a document.

	\param docId (string|number) the document's unique or local ID
	\param revId (optional, string) revision to delete

	\return <code>Observable< string ></code> resolves with the new value's hash code
		when the write completes. You may safely ignore this Observable.

	This replaces the revision in question with a new value that contains
	the value <code>_deleted = true</code>. This signals that the previous
	revision is now deleted.

	Among other things, this can be used to resolve a conflict. A conflict
	exists only when multiple current revisions for a document exist. If you
	delete revisions until there is only one current revision, the conflict
	is resolved.

	The revision being deleted must be current.
]]--

function WFRxDocStore:deleteDoc( docId, optRevId )

	check( 'WFRxDocStore', 'string|number', '?+string' )

	self._log:infof( "\n\ndocStore:deleteDoc( %s, %s )\n\nvia %s",
					 docId, optRevId, ':traceback:' )
	recordTransaction( self, 'deleteDoc' )

	return maybeLogResponse( self, 'deleteDoc',
							 self._docStoreImp:request( 'deleteDoc', docId, optRevId ) )
		:call( runUntilDone )

end

--------------------------------------------------------------------------------
--[[!
	\brief Efficiently mass-delete all documents/revisions associated with a space.
		This API is meant to purge the local doc store of space-related content.
		
		The API does the following:
	
		- Deletes all revisions for all documents associated with the space.
		- Inserts a new deletion tombstone into the revs table for all documents
			associated with the space.
		- Inserts a new '_dontsendtooz' label for each of the new tombstone revisions 
			to prevent them from syncing to Oz.
		- Updates all documents associated with the space to point at the new tombstone
			revisions.
			
	\param spaceId (string) the space Id
	\param excludedDocumentIds (?table) A list of document Ids, belonging to the space,
		that should be excluded from the actions of this function.
		
	\return <code>Observable< boolean ></code> Resolves to true.
]]--
function WFRxDocStore:deleteSpace( spaceId, excludedDocumentIds )

	check( 'WFRxDocStore', '+string' )

	self._log:infof( "\n\ndocStore:deleteSpace( %s )\n\nvia %s",
					 spaceId, ':traceback:' )
					 
	recordTransaction( self, 'deleteSpace' )

	return maybeLogResponse( self, 'deleteSpace',
							 self._docStoreImp:request( 'deleteSpace', spaceId, excludedDocumentIds ) )
		:call( runUntilDone )

end

--------------------------------------------------------------------------------

function WFRxDocStore:resolveConflictWithDelete( docId, optSuppressNewRevision )

	check( 'WFRxDocStore', 'string|number', '?boolean' )

	self._log:infof( "\n\ndocStore:resolveConflictWithDelete( %s )\n\nvia %s",
					 docId, ':traceback:' )
	recordTransaction( self, 'resolveConflictWithDelete' )

	return maybeLogResponse( self, 'resolveConflictWithDelete',
							 self._docStoreImp:request( 'resolveConflictWithDelete', docId, optSuppressNewRevision ) )
		:call( runUntilDone )
end

--------------------------------------------------------------------------------

--[[!
	\brief Deletes a specific revision of a document without creating the new revision as
		a current revision. This is necessary when resolving conflicts so that, once
		processed, the deletion of the conflicting revision does not create a current 
		revision (see bug NMB-2952)
		
	\param docId (string|number) the document's unique or local ID
	\param revId (string) revision to delete
	
	\return <code>Observable< string ></code> resolves with the new value's hash code
		when the write completes. You may safely ignore this Observable.
]]--
function WFRxDocStore:deleteRevisionAsNonCurrent( docId, revId )
	
	check( 'WFRxDocStore', 'string|number', 'string' )

	self._log:infof( "\n\ndocStore:deleteRevisionAsNonCurrent( %s, %s )\n\nvia %s",
					 docId, revId, ':traceback:' )
	recordTransaction( self, 'deleteRevisionAsNonCurrent' )
	
	return maybeLogResponse( self, 'deleteRevisionAsNonCurrent',
							 self._docStoreImp:request( 'deleteRevisionAsNonCurrent', docId, revId ) )
		:call( runUntilDone )

end

--------------------------------------------------------------------------------

-- @mark revision labels

--------------------------------------------------------------------------------
--[[!
	\brief Attaches a label to a given revision of a document.

	\param docId (string) document ID
	\param label (string) label name (any non-empty string)
	\param revId (string) revision ID

	Think of a label as a movable tag that can be used later to search for
	particular document revisions. Like git tags, they are simply named aliases
	for another entity in the system (in this case document revisions).
	Unlike git tags, they apply only to a single document.

	Labeled revisions are not purged when the database is compacted.

	\return <code>Observable< empty ></code> resolves when done
]]--

function WFRxDocStore:setLabel( docId, label, revId )

	check( 'WFRxDocStore', '+string', '+string', '+string' )

	self._log:infof( "\n\ndocStore:setLabel( %s, %s, %s )\n\nvia %s",
					 docId, label, revId, ':traceback:' )

	recordTransaction( self, 'setLabel' )

	return maybeLogResponse( self, 'setLabel',
							 self._docStoreImp:request( 'setLabel', docId, label, revId ) )
		:call( runUntilDone )

end

--------------------------------------------------------------------------------
--[[!
	\brief Removes the label (if present) from a given revision of a document.

	\param docId (string) document ID
	\param label (string) label name (any non-empty string)
	\param revId (string) revision ID

	\return <code>Observable< empty ></code> resolves when done
]]--
function WFRxDocStore:removeLabel( docId, label, revId )

	check( 'WFRxDocStore', '+string', '+string', '+string' )

	self._log:infof( "\n\ndocStore:removeLabel( %s, %s, %s )\n\nvia %s",
					 docId, label, revId, ':traceback:' )

	recordTransaction( self, 'removeLabel' )

	return maybeLogResponse( self, 'removeLabel',
							 self._docStoreImp:request( 'removeLabel', docId, label, revId ) )
		:call( runUntilDone )
end

--------------------------------------------------------------------------------
--[[!
	\brief Gets the revision of a document associated with a label.

	\param docId (string) document ID
	\param label (string) label name (any non-empty string)

	\return <code>Observable< string ></code> tagged revision
]]--

function WFRxDocStore:getRevisionForLabel( docId, label )

	check( 'WFRxDocStore', '+string', '+string' )

	self._log:infof( "\n\ndocStore:getRevisionForLabel( %s, %s )\n\n",
					 docId, label, ':traceback:' )

	recordTransaction( self, 'getRevisionForLabel' )

	return maybeLogResponse( self, 'getRevisionForLabel',
							 self._docStoreImp:request( 'getRevisionForLabel', docId, label ) )

end

--------------------------------------------------------------------------------
--[[!
	\brief Update a specific label for a doc from a revision list.

	Updates the specified label of the specified docId to the latest revId 
	in the revIdList which also exists in the local docStore under the condition
	that the local sequencenumber is higher than the current tagged sequence number.

	\param docId (string) document ID
	\param label (string) label name (any non-empty string)
	\param revList (table) revision list.

	\return <code>Observable< empty ></code> resolves when done
]]--

function WFRxDocStore:updateLabelFromRevisionList( docId, label, revIdList )

	check( 'WFRxDocStore', '+string', '+string', 'table' )

	self._log:infof( "\n\ndocStore:updateLabelFromRevisionList( %s, %s, %s )\n\n",
					 docId, label, ':traceback:' )

	recordTransaction( self, 'updateLabelFromRevisionList' )

	return maybeLogResponse( self, 'updateLabelFromRevisionList',
							 self._docStoreImp:request( 'updateLabelFromRevisionList', docId, label, revIdList ) )

end

--------------------------------------------------------------------------------

-- @mark document type and subtype

--------------------------------------------------------------------------------

--[[!
	\brief Get the document ids of all docs of the specified types and subtypes
		Returns the document ids ordered by change counter, smallest to largest.
		
	\param docTypes (table) map with doc types as keys and a list of subtypes as values
	\param fromChangeCounter (optional number) A previous change-counter value. Documents
		whose change counter is less than this value will be ignored.
	
	Spezial subtype values:
	- '*' returns all docs of specified type  (regardless of subtype)
	- 'NULL' returns only docs of the specified type where the subtype is exactly NULL

	No verification is done if the passed type and subtype arguments
	are compatible

	\return <code>Observable<table,changeCounter></code> list of fulldocIds and last changeCounter (for batching)
]]--

function WFRxDocStore:getDocsOfType( docTypes, fromChangeCounter, maxDocuments )

	check( 'WFRxDocStore', 'table', '?number', '?number' )

	self._log:infof( "\n\ndocStore:getDocsOfType()\n\n", ':traceback:' )

	recordTransaction( self, 'getDocsOfType' )

	return maybeLogResponse( self, 'getDocsOfType', self._docStoreImp:request( 'getDocsOfType', docTypes, fromChangeCounter, maxDocuments ) )

end

--------------------------------------------------------------------------------

--[[
	\brief Get the document ids of all docs which have the specified label

	\param label (string)

       	\return <code>Observable<table></code> list of fulldocIds
]]
function WFRxDocStore:getIdsOfDocsWithLabel( label )

	check( 'WFRxDocStore', '+string' )

	self._log:infof( '\n\ndocStore:getIdsOfDocsWithLabel()\n\n', ':traceback:' )

	recordTransaction( self, 'getIdsOfDocsWithLabel' )

	return maybeLogResponse( self, 'getIdsOfDocsWithLabel', self._docStoreImp:request( 'getIdsOfDocsWithLabel', label ))

end

--------------------------------------------------------------------------------

--[[!
	\brief Count the documents in the doc store.

	\return <code>Observable<number></code> the number of documents.
]]--

function WFRxDocStore:countAllDocs()

	check( 'WFRxDocStore' )

	self._log:infof( "\n\ndocStore:countAllDocs()\n\n", ':traceback:' )

	recordTransaction( self, 'countAllDocs' )

	return maybeLogResponse( self, 'countAllDocs', self._docStoreImp:request( 'countAllDocs' ) )

end

--------------------------------------------------------------------------------

--[[!
	\brief Count the documents of the specified types and subtypes.

	\param docTypes (table) map with doc types as keys and a list of subtypes as values
	
	Spezial subtype values:
	- '*' returns all docs of specified type  (regardless of subtype)
	- 'NULL' returns only docs of the specified type where the subtype is exactly NULL

	No verification is done if the passed type and subtype arguments
	are compatible.

	\return <code>Observable<number></code> the number of docs matching the
		specified types and subtypes.
]]--

function WFRxDocStore:countDocsOfType( docTypes )

	check( 'WFRxDocStore', 'table' )

	self._log:infof( "\n\ndocStore:countDocsOfType()\n\n", ':traceback:' )

	recordTransaction( self, 'countDocsOfType' )

	return maybeLogResponse( self, 'countDocsOfType', self._docStoreImp:request( 'countDocsOfType', docTypes ) )

end

--------------------------------------------------------------------------------

-- @mark document type and subtype

--------------------------------------------------------------------------------

--[[!
	\brief Check the given docstore for corruption. Returns true if the docstore 
		is corrupt, false if docstore is not corrupt.

	\return <code>Observable<boolean></code>  Success if the docstore is corrupt, 
			failure if docstore is not corrupt.
]]--

function WFRxDocStore:isCorrupt()

	check( 'WFRxDocStore' )

	self._log:infof( "\n\ndocStore:isCorrupt()\n\n", ':traceback:' )

	recordTransaction( self, 'isCorrupt' )

	return maybeLogResponse( self, 'isCorrupt', self._docStoreImp:request( 'isCorrupt' ) )

end

--------------------------------------------------------------------------------

-- @mark document type and subtype

--------------------------------------------------------------------------------

--[[!
	\brief Get the number of document and arrange them according to type and subtype

	\return <code>Observable<table></code> list of number of docs for existing types and subtype
]]--

function WFRxDocStore:DEBUG_getNumberOfDocsByTypeAndSubtype()

	check( 'WFRxDocStore' )

	self._log:infof( "\n\ndocStore:DEBUG_getNumberOfDocsByTypeAndSubtype()\n\n", ':traceback:' )

	recordTransaction( self, 'DEBUG_getNumberOfDocsByTypeAndSubtype' )

	return maybeLogResponse( self, 'DEBUG_getNumberOfDocsByTypeAndSubtype', self._docStoreImp:request( 'DEBUG_getNumberOfDocsByTypeAndSubtype') )

end
--------------------------------------------------------------------------------

-- @mark partial document values

--------------------------------------------------------------------------------
--[[!
	\brief Reports the current value of a field in this document as an Observable.

	\param docId (string) The document's unique ID.

	\param fieldPath (string) The field path, a slash-delimited string like
		a Unix paths, except that the root is implied. Use the path to
		identify values within tables or nested tables. You can use number
		values to get the nth member of a table; for example, "tabA/1" to get
		the first member of an array in "tabA".

	The Subject will be updated reasonably quickly as changes come in from
		this and other threads.

	\warning You should avoid watching many documents via \c ongoingDocValue or
		\c ongoingFieldValue simultaneously for performance reasons. Console logs
		will be generated if the number of \c ongoingDocValue instances is large.
		(Note that \c ongoingFieldValue uses \c ongoingDocValue in its implementation,
		but the performance concern is around the number of \b documents,
		not the number of fields, being watched.)

	\return <code>Observable< value ></code> A Subject whose value is
		the current value of the field.
]]--

function WFRxDocStore:ongoingFieldValue( docId, fieldPath )

	check( 'WFRxDocStore', '+string', '+string' )

	self._log:infof( "\n\ndocStore:ongoingFieldValue( %s, %s )", docId, fieldPath )

	local fieldPathSegments = {}
	local nSegments = 0

	fieldPath:gsub( '([^/]+)',
		function( segment )
			nSegments = nSegments + 1
			if segment:match( '^%-?%d+$' ) then
				fieldPathSegments[ nSegments ] = tonumber( segment )
			else
				fieldPathSegments[ nSegments ] = segment
			end
		end )

	if nSegments == 0 then
		error( "WFRxDocStore:ongoingFieldValue: fieldPath didn't contain a path", 2 )
	end

	return self:ongoingDocValue( docId )
		:map( function( docValue )

			if type( docValue ) ~= 'table' then return nil end

			local t = docValue

			-- All parent nodes must be tables. If they are anything else,
			-- we just pretend they don't exist since we can't index
			-- into them. (Note that we don't support userdata values
			-- in documents since they don't persist or translate cleanly
			-- to JSON.)

			for i = 1, nSegments - 1 do
				t = t and t[ fieldPathSegments[ i ] ]
				if type( t ) ~= 'table' then
					t = nil
				end
			end

			return t and t[ fieldPathSegments[ nSegments ] ]

		end )

end

--------------------------------------------------------------------------------
--[[!
	\brief Updates the document value, applying an overlay to any existing value.

	\param overlay (table) delta to apply to the existing document value

	\c overlay \b must contain the following elements:
		* \c _id (string|integer) the document's unique ID or the document's
			integer local ID.
	
	\c overlay \b may contain the following element:
		* \c _newRevId (string) if present, the doc store uses this uuid as the
			new revision id
	
	\c overlay \b may contain the following elements:
		* \c _deleted = true, if present, this marks the document as deleted similar
			to calling <code>deleteDoc( docId )</code>
		* \c _newId (string) only allowed when _deleted = true, _newId may be set 
			to a new uuid which will cause the doc store to copy the current 
			version of the doc into a new document with the _newId uuid
		* \c _newIdOverlay (table) when _newId is set this is a separate overlay 
			that will be applied to copied document as it's being copied into the 
			new uuid. This overlay *can* change the type/subtype as this is a new 
			document.
	\c overlay <b>MUST NOT</b> contain a \c _rev entry. This function can only
		be applied to the current winning revision of a document.

	\c overlay <b>MUST NOT</b> contain a \c _labels entry. This function can not
		be used to create or update labels.

	\param description (optional string) string used by debug panel and
		diagnostic log to identify what operations made calls.

	\return <code>Observable< string ></code> resolves with the new value's
		revision ID when the write completes. You may safely ignore this promise.
]]--

function WFRxDocStore:applyOverlay( overlay, description )

	check( 'WFRxDocStore', 'table' )

	description = description or descriptionFromCallStack( 2 )

	self._log:infof( "\n\ndocStore:applyOverlay( %s )\n\nvia %s", overlay, ':traceback:' )
	recordTransaction( self, 'applyOverlay' )

	return maybeLogResponse( self, 'applyOverlay',
							 self._docStoreImp:request( 'applyOverlay', overlay, description ) )
		:call( runUntilDone )

end

--------------------------------------------------------------------------------
--[[!
	\brief Updates multiple document values, applying a different overlay to
		each existing value, if any.

	\param overlayArray (array) an array of deltas, just like those supplied to
		applyOverlay() above.

		The array may also contain a <code>label = "name"</code> entry. If this exists,
		that label will be applied to all documents in this batch.
	
		The array may also contain a <code>preserveLabels = true</code> entry. If this exists,
		the labels will not be changed from their previous value.

	\param description (optional string) string used by debug panel and
		diagnostic log to identify what operations made calls.

	\return <code>Observable< string ></code> resolves with an array of new value's
		revision IDs when the write completes. You may safely ignore this.
]]--

function WFRxDocStore:bulkApplyOverlay( overlayArray, description )

	check( 'WFRxDocStore', 'table', '?string' )

	description = description or descriptionFromCallStack( 2 )

	self._log:infof( "\n\ndocStore:bulkApplyOverlay( %s )\n\nvia %s", overlayArray, ':traceback:' )
	recordTransaction( self, 'bulkApplyOverlay' )

	return maybeLogResponse( self, 'bulkApplyOverlay',
							 self._docStoreImp:request( 'bulkApplyOverlay', overlayArray, description ) )
		:call( runUntilDone )


end

--------------------------------------------------------------------------------
--[[!
	\brief Updates multiple document values, applying the same overlay to
		each existing value, if any.

	\param overlay (table) delta to apply to the specific document Ids, just 
		like the delta supplied to applyOverlay() above with a few notable
		exceptions:
		
	\c overlay \b must NOT contain the following elements:
	
		* \c _id: Instead, the document's unique ID should be passed in via the ids
			parameter.
	
		* \c _newRevId, _newId, and _newIdOverlay: Since the overlay is applied to 
			multiple ids, you cannot supply any field that needs to be unique to 
			a specific document.

	\param ids (table) a table of either full document ids, or local ids, to
		which the overlay will be applied.
		
	\param description (optional string) string used by debug panel and
		diagnostic log to identify what operations made calls.

	\return <code>Observable< string ></code> resolves with an array of new value's
		revision IDs when the write completes. You may safely ignore this.
]]--

function WFRxDocStore:applyOverlayToIds( overlay, ids, description )

	check( 'WFRxDocStore', 'table', 'table', '?string' )

	description = description or descriptionFromCallStack( 2 )

	self._log:infof( "\n\ndocStore:applyOverlayToIds( %s, %s )\n\nvia %s", overlay, ids, ':traceback:' )
	recordTransaction( self, 'applyOverlayToIds' )

	return maybeLogResponse( self, 'applyOverlayToIds',
							 self._docStoreImp:request( 'applyOverlayToIds', overlay, ids, description ) )
		:call( runUntilDone )


end

--------------------------------------------------------------------------------

--[[!
	\brief Deletes a specific revision of a document without creating the new revision as
		a current revision followed by an applyOverlay in one transaction.
		This is necessary occasionally when resolving conflicts.

	\param docId (string|number) the document's unique or local ID
	\param revId (string) revision to delete
	\param overlay (table) the overlay

	\return <code>Observable< string ></code> resolves with the new value's hash code
		when the write completes. You may safely ignore this Observable.
]]--
function WFRxDocStore:deleteRevisionAsNonCurrentAndApplyOverlay( docId, revId, overlay )

	check( 'WFRxDocStore', 'string|number', 'string', 'table' )

	self._log:infof( "\n\ndocStore:deleteRevisionAsNonCurrentAndApplyOverlay( %s, %s )\n\nvia %s",
					 docId, revId, ':traceback:' )
	recordTransaction( self, 'deleteRevisionAsNonCurrentAndApplyOverlay' )

	return maybeLogResponse( self, 'deleteRevisionAsNonCurrentAndApplyOverlay',
							 self._docStoreImp:request( 'deleteRevisionAsNonCurrentAndApplyOverlay', docId, revId, overlay ) )
		:call( runUntilDone )

end

--------------------------------------------------------------------------------

-- @mark change notification

--------------------------------------------------------------------------------
--[[!
	\brief Tracks changes in this database by incrementing a counter for each
		change. Be aware that this value can jump, especially if many revisions
		are changed at once.

	\return <code>Observable< number ></code> the current maximum value for
		\c _changeCounter across all revisions in this database
]]--

function WFRxDocStore:changeCounter()

	check( 'WFRxDocStore' )

	self._log:info( "\n\ndocStore:changeCounter()" )

	if not self._changeCounter then

		self._changeCounter = self._docStoreImp:request( 'changeCounter' )

		-- FIXME [scouten 2015-08-05]: WFWorkerThread doesn't like it when we try
		-- to re-subscribe to a worker thread request. We want to reuse this
		-- Observable to reduce cross-thread traffic and we don't have a way to
		-- know when the Observable loses all of its subscribers, so we just open
		-- a subscription and leave it open. Andy says there may be a better example
		-- for how to do this in WFRxIndexStore so I'll look for that another day.

		self._changeCounterSubscription = self._changeCounter
			:subscribe( function() end )

	end

	return self._changeCounter

end

--------------------------------------------------------------------------------
--[[!
	\brief Reports the current change count for this revision.
		This count includes every operation that has completed to this point.

	\return <code>Observable< number ></code> the current maximum value for
		\c _changeCounter across all revisions in this database
]]--

function WFRxDocStore:changeCounterOnce()

	check( 'WFRxDocStore' )

	self._log:info( "\n\ndocStore:changeCounterOnce()" )

	return maybeLogResponse( self, 'changeCounterOnce',
		self._docStoreImp:request( 'changeCounterOnce' ) )

end

--------------------------------------------------------------------------------
--[[!
	\brief Retrieves the set of documents that have changed since a specified point.

	\param args (table) A table with these keys:
		* \c fromChangeCounter (number): A previous change-counter value. Documents
			whose change counter is equal to or greater than this value can match
			this query.
		* \c limit (optional, number): The maximum number of documents to return
		* \c includeDocumentsFromSource (optional, string): A string indicating
			which documents to consider: "all", "catalog" or "spaceFeed". The
			default is "all".
		* \c filterByTypeSubtypeMap (optional, table): map with doc types as 
			keys and a list of subtypes as values. If present, only documents
			matching the types/subtypes in this map will be returned. If this
			filter is specified, this API may return fewer results than
			specified via the limit parameter. 
			
	\param description (optional string) string used by debug panel and
		diagnostic log to identify what operations made calls.

	Note that regardless of any value specified in \c limit, a limit of 100 documents
	is always enforced. (The limit is raised to 1000 if \c idsOnly is true.)

	\return <code>Observable< table ></code> An array of fully-formed document values
		(as from \c :docValue) that satisfy the changed-documents search criteria.

	This array/table also contains a top-level element named \c continueFrom
	whose value is the change-counter number to use in the next \c :changedDocs call.
	That number may be less than the current value of \c :changeCounter if
	the number of documents returned in this call was limited.
]]--

--------------------------------------------------------------------------------

function WFRxDocStore:changedDocs( args, description )

	check( 'WFRxDocStore', 'table' )

	description = description or descriptionFromCallStack( 2 )

	self._log:infof( "\n\ndocStore:changedDocs( %s )", args )

	local nestedTypeSubtypeMap, shouldIncludeAllTypesAndSubtypes
			= WFRxDocStoreUtils.processFilterByTypeSubtypeMap( args.filterByTypeSubtypeMap )

	return maybeLogResponse( self, 'changedDocs',
		self._docStoreImp:request( 'changedDocs', args, description ) )
			:map( function( rawDocRows, changeCounter, limit )

				return WFRxDocStoreUtils.processChangedDocsRawDocRows( rawDocRows, 
						changeCounter, limit, shouldIncludeAllTypesAndSubtypes, 
						nestedTypeSubtypeMap )

			end )

end

--------------------------------------------------------------------------------
-- Since the changes feed goes to a separate thread before it goes to the
-- underlying docStoreImp, we must capture the changeCounter from docStoreImp
-- before we sending changes feed on the separate thread.
--------------------------------------------------------------------------------

local function makeScanFilterMapChangesFeed( 
					self, changeCounterStream, namespace,
					scanFilterMapFuncName, initialScanState, optDesc )

	return changeCounterStream
			:take( 1 )
			:flatMapLatest(
				function( changeCounter )

					if WFCloseSupport.hasBeenOrWillBeClosed( self ) or
							WFCloseSupport.hasBeenOrWillBeClosed( 
								self._docStoreChangesFeedImp ) then

						return Rx.Observable.empty()

					end

					return self._docStoreChangesFeedImp:request( 
									'scannedFilteredMappedChangesFeedSince',
										changeCounter, namespace,
										scanFilterMapFuncName,
										initialScanState, optDesc )

				end )

end

--------------------------------------------------------------------------------
--[[!
	\brief Watches the database for document value changes and passes them
		through a scanFilterMap function.

		The use case is for building up a filter from ongoing data. For example,
		a stream which reports intial value for a document and then filters out
		all subsequent changes to that docValue unless they meet a certain
		criteria.

		The scanFilterMap function has signature

			function scanFilterMap( scanState, docValue )

				-- process docValue, possibly updating scanScate and possibly
				-- extracting values ...

				return scanState, ...

			end

		It acts like a combination of scan and map or scan and filter depending
		on the inputs. 

		Specifically:

			The first return value of scanFilterMap, called scanState, is
			always passed to the next invocation of the scanFilterMap.

			The remaing returning values ... are processed as follows:

				When select( '#', ... ) > 0, the return values are emitted on
				the stream, meaning the scanFilterMap function acts like the
				Rx.Observable.map operator.

				When select( '#', ... ) == 0, nothing is emitted on the stream
				meaning scanFilterMap function acts like the
				Rx.Observable.filter operator.

	\param namespace(string): The name of the namespace that contains a 
		scanFilterMapFunc named scanFilterMapFuncName

	\param scanFilterMapFuncName(string): The name of the scanFilterMapFunc
		{ namespaceName, functionName } such that 
		require( namespaceName ).functionName returns a function
		scannedDataFilterFunc. This will be added to the chain as
			:scan( docScanFunc, initialValue )
			:filter( scannedDataFilterFunc )

	\param initialScanState( transitable lua value): initialScanState is passed
		to scanFilterMap the first time it is called.

	\return <code>Observable< ... ></code> The series of of values emitted by
		the scanFilterMap function when select( '#', ... ) > 0.
]]--

--------------------------------------------------------------------------------

function WFRxDocStore:scannedFilteredMappedChangesFeed(
						namespace, scanFilterMapFuncName, initialScanState )

	check( 'WFRxDocStore', 'string', 'string' )

	local changeCounterAfterNow = self:changeCounter()
		:map( function( lastChangeCounterUsed )
			return lastChangeCounterUsed + 1
		end )

	return makeScanFilterMapChangesFeed( 
				self, changeCounterAfterNow, namespace, scanFilterMapFuncName,
				initialScanState ) 

end

--------------------------------------------------------------------------------
--[[!
	\brief Watches the database for document value changes and passes them
		through a scanFilterMap function.

		The use case is for building up a filter from ongoing data. For example,
		a stream which reports intial value for a document and then filters out
		all subsequent changes to that docValue unless they meet a certain
		criteria.

		The scanFilterMap function has signature

			function scanFilterMap( scanState, docValue )

				-- process docValue, possibly updating scanScate and possibly
				-- extracting values ...

				return scanState, ...

			end

		It acts like a combination of scan and map or scan and filter depending
		on the inputs. 

		Specifically:

			The first return value of scanFilterMap, called scanState, is
			always passed to the next invocation of the scanFilterMap.

			The remaing returning values ... are processed as follows:

				When select( '#', ... ) > 0, the return values are emitted on
				the stream, meaning the scanFilterMap function acts like the
				Rx.Observable.map operator.

				When select( '#', ... ) == 0, nothing is emitted on the stream
				meaning scanFilterMap function acts like the
				Rx.Observable.filter operator.

	\param namespace(string): The name of the namespace that contains a 
		scanFilterMapFunc named scanFilterMapFuncName

	\param scanFilterMapFuncName(string): The name of the scanFilterMapFunc
		{ namespaceName, functionName } such that 
		require( namespaceName ).functionName returns a function
		scannedDataFilterFunc. This will be added to the chain as
			:scan( docScanFunc, initialValue )
			:filter( scannedDataFilterFunc )

	\param initialScanState( transitable lua value): initialScanState is passed
		to scanFilterMap the first time it is called.

	\param sinceChangeCounter (number): A previous change-counter value.
		Any changes >= fromChangeCounter will be sent, even if
		they have already occurred. 

	\return <code>Observable< ... ></code> The series of of values emitted by
		the scanFilterMap function when select( '#', ... ) > 0.
]]--

function WFRxDocStore:scannedFilteredMappedChangesFeedSince(
							namespace, scanFilterMapFuncName, initialScanState,
							sinceChangeCounter, optDesc )

	check( 'WFRxDocStore', 'string', 'string', '_', 'number', '?string' )

	return makeScanFilterMapChangesFeed( 
				self, Rx.Observable.just( sinceChangeCounter ), namespace,
				scanFilterMapFuncName, initialScanState, optDesc )

end

-------------------------------------------------------------------------------

--[[!
	\brief The same as WFRxDocStore:scannedFilteredMappedChangesFeedSince, but
		does not emit stale document revisions. See scannedFilteredMappedChangesFeedSince
		for more information.
]]--

function WFRxDocStore:scannedFilteredMappedChangesFeedSinceWithoutStaleRevisions(
							namespace, scanFilterMapFuncName, initialScanState,
							sinceChangeCounter )

	check( 'WFRxDocStore', 'string', 'string', '_', 'number' )

	return self._docStoreChangesFeedImp:request( 
					'scannedFilteredMappedChangesFeedSinceWithoutStaleRevisions',
						sinceChangeCounter, namespace,
						scanFilterMapFuncName,
						initialScanState )

end

-------------------------------------------------------------------------------

--[[!
	\brief Watches the database for document value changes for a stream
		of docIds.

	\param docIdArrayStream (Observable): An observable that will send an array
		of localIds or docIds that the caller wants to monitor.  The docValues
		of the ids will be returned in order of the initial call and any changes
		to the docValues or to the array of ids being watched will be sent as
		the changed or new doc ids to watch are provided.

	\param optDesc (string): An optional string that describes the caller that
		will be used in diagnostic logs and by other debugging uses.

	\param optOnlyChanges (boolean): An optional boolean where if true it
		indicates that the initial values of the ids being monitored should
		not be fetched as a parallel routine will be accomplishing that.

	\return <code>Observable< ... ></code> The series of of values emitted by
		the scanFilterMap function when select( '#', ... ) > 0.

]]--

function WFRxDocStore:makeDocValueWatcher( docIdArrayStream, optDesc, optOnlyChanges )

	WFArguments.check( 'WFRxDocStore', '_', '?string', '?boolean' )

	local docValueWatcherId = KSIdGenerator.localCounter()

	return self._docStoreChangesFeedImp:request( 'makeDocValueWatcher', docValueWatcherId, optDesc )

		:merge( docIdArrayStream:flatMapLatest( function( docIdsToWatch )

				return self._docStoreChangesFeedImp:request( 'updateDocValueWatcher',
							docValueWatcherId, docIdsToWatch, optOnlyChanges, optDesc )

		end ) )

end

--------------------------------------------------------------------------------
--[[!
	\brief Returns additional history about the document revisions leading to
		a specific revision.

	\param docId (string) the document's unique ID

	\param optRevId (optional, string) the revision to trace (if not specified,
		defaults to the current winning revision)

	\param optStartingFromAnyOf (optional, table) if any of the revision IDs
		specified in this array occur, stop tracing history beyond the first
		matching rev ID

	\return <code>Observable< table ></code> array of matching revision IDs in history

	An example return might be:
		<code>
			{
				"revId": "6f5ad8db0f34af24a6e0984cd1a6cfb9",
				"status": "available"
			},
			{
				"revId": "77fba3a059497f51ec99b9b478b569d2",
				"status": "deleted"
			},
			{
				"revId": "136813b440a00a24834f5cb1ddf5b1f1",
				"status": "available"
			},
			{
				"revId": "fd96acb3256302bf0dd2f32713161f2a",
				"status": "missing"
			},
			{
				"revId": "874985bc28906155ba0e2e0538f67b05",
				"status": "missing"
			},
			{
				"revId": "0de77a37463bf391d14283e626831f2e",
				"status": "missing"
			},
			{
				"revId": "d795d1b924777732fdea76538c558b62",
				"status": "missing"
			},
			{
				"revId": "917fa2381192822767f010b95b45325b",
				"status": "missing"
			}
		</code>
]]--

function WFRxDocStore:docRevisionHistory( docId, optRevId, optStartingFromAnyOf )

	check( 'WFRxDocStore', 'string', '?+string', '?table' )

	self._log:info( "\n\ndocStore:docRevisionHistory()" )

	return maybeLogResponse( self, 'docRevisionHistory',
		self._docStoreImp:request( 'docRevisionHistory', docId, optRevId, optStartingFromAnyOf ) )

end

--------------------------------------------------------------------------------

-- @mark local (non-syncing) documents

--------------------------------------------------------------------------------
--[[!
	\brief Retrieves the value of a single local (non-syncing) document.

	\param docId (string) the local document's unique ID

	\return <code>Observable< table ></code> value for this document

	Note that the document content will be annotated with the following
	system-generated entries (at the top level of the value table):

		* \c _empty (boolean): true if there isn't actually a value for this doc ID
]]--

function WFRxDocStore:localDocValue( docId )

	check( 'WFRxDocStore', 'string' )

	self._log:infof( "\n\ndocStore:localDocValue( %s, %s )", docId )

	return maybeLogResponse( self, 'localDocValue',
		self._docStoreImp:request( 'localDocValue', docId ) )

end

--------------------------------------------------------------------------------
--[[!
	\brief Retrieves the value of a single local (non-syncing) document.

	\param docId (string) the local document's unique ID

	\return <code>Observable< table ></code> value for this document

	Note that the document content will be annotated with the following
	system-generated entries (at the top level of the value table):

		* \c _empty (boolean): true if there isn't actually a value for this doc ID
]]--

function WFRxDocStore:ongoingLocalDocValue( name )

	check( 'WFRxDocStore', 'string' )

	self._log:infof( "\n\ndocStore:ongoingLocalDocValue( %s )", name )

	return self._ongoingLocalDocValues:observableForKey( name )

end

--------------------------------------------------------------------------------
--[[!
	\brief Sets the value for a local (non-syncing) document, replacing any
		existing value.

	Local documents are intended for use as state bookmarks. They differ from
	traditional syncing documents in the following ways:

		* Local document IDs are stored in a separate table and a separate
		  namespace from traditional syncing documents. You may reuse the same
		  document ID among the document types and the values will not cross-pollute.
		* Local documents are not versioned. (For that reason, the \c _rev
		  parameter must not be specified in a local document value.)
		* Local documents can not have conflicts.
		* There is no change notification for local documents.
		* Local documents are not replicated to other document databases.

	\param value (table) new value for the entire document

	\c value \b must contain the following elements:
		* \c _id (string) the document's unique ID

	\return <code>Observable< empty ></code> Resolves when the write completes.
		(You may safely ignore this promise.)
]]--

function WFRxDocStore:setLocalDocValue( value )

	check( 'WFRxDocStore', 'table' )

	self._log:infof( "\n\ndocStore:setLocalDocValue( %s )\n\nvia %s", value, ':traceback:' )
	recordTransaction( self, 'setLocalDocValue' )

	return maybeLogResponse( self, 'setLocalDocValue',
							 self._docStoreImp:request( 'setLocalDocValue', value ) )
		:call( runUntilDone )

end

--------------------------------------------------------------------------------

function WFRxDocStore:applyOverlayLocalDocValue( overlay )

	check( 'WFRxDocStore', 'table' )

	self._log:infof( "\n\ndocStore:applyOverlayLocalDocValue( %s )\n\nvia %s", overlay, ':traceback:' )
	recordTransaction( self, 'applyOverlayLocalDocValue' )

	return maybeLogResponse( self, 'applyOverlayLocalDocValue',
							 self._docStoreImp:request( 'applyOverlayLocalDocValue', overlay ) )
		:call( runUntilDone )
		
end

--------------------------------------------------------------------------------

-- @mark diagnostic log

--------------------------------------------------------------------------------
--[[!
	\brief Sends diagnostic data on database performance to the log.
]]--

function WFRxDocStore:sendDiagnosticLogData()

	check( 'WFRxDocStore' )

	-- Also include perf log as a separate section if we collected the data.

	if self._recordApiPerformance then

		local rawReport = self:getApiPerformanceLog()

		local htmlReport = string.format( "<pre>%s</pre>",
										  KSStringUtils.escapeHtml( rawReport ) )

		local threadName = WFCurrentThread.DEBUG_getThreadName() or 'main'

		local idSafeThreadName = threadName
		idSafeThreadName = idSafeThreadName:replace( ' :: ', '+' )

		WFDiagnosticLog.addSectionToLog
			{

				id = string.format( 'WFRxDocStore-perf-%s-%s',
									self._path,
									idSafeThreadName ),

				name = string.format( "Doc Store API Performance: %s", threadName),
				weight = 401,
				parentSection = WFDiagnosticLog.Sections.DOC_STORE,

				body = htmlReport,

			}

	end

	return {

		name = 'WFRxDocStore :: ' .. self._path,
		id = 'WFRxDocStore-' .. self._path,
		weight = 400,
		parentSection = WFDiagnosticLog.Sections.DOC_STORE,

		body = {
			recentErrors = self._recentErrorLog,
			transactionSources = self._transactionSources,
		},

	}

end

--------------------------------------------------------------------------------
--[[!
	\brief Gather statistics about doc store API performance.

	Only intended for debug / testing use.

	Only meaningful if RECORD_API_PERFORMANCE is true.
]]--

function WFRxDocStore:getApiPerformanceLog()

	check( 'WFRxDocStore' )

	if not self._recordApiPerformance then return "API performance log disabled" end

	local output = {
		"\nAPI PROFILE SUMMARY FOR DOC STORE ", self._path,
		"\n",
		"\n   Count       Min       Max       Total  API",
		"\n--------  --------  --------  ----------  -------------",
		"\n\n",
	}

	local sortedStats = {}
	local n = 0

	for _, v in pairs( self._perfLog ) do
		n = n + 1
		sortedStats[ n ] = v
	end

	table.sort( sortedStats, function( a, b )
		return a.total < b.total
	end )

	for i = 1, #sortedStats do

		local stat = sortedStats[ i ]

		output[ #output + 1 ] =
			string.format(
				"%8.0f  %8.3f  %8.3f  %10.3f  %s\n",
					stat.count,
					stat.min,
					stat.max,
					stat.total,
					stat.funcName )

	end

	return table.concat( output )

end

--------------------------------------------------------------------------------

-- @mark miscellaneous

--------------------------------------------------------------------------------
--[[!
	\brief Retrieves the path to this document store.

	\return (string) The catalog's path.
]]--

function WFRxDocStore:path()

	check( 'WFRxDocStore' )

	return self._path

end


--------------------------------------------------------------------------------
--[[!
	\brief Verifies that enough space is available to perform a full vacuum

	\return (boolean) whether enough space is available for a vacuum.
]]--

function WFRxDocStore:enoughSpaceForVacuum()

	check( 'WFRxDocStore' )

	local path = self._path

	if not path or path == ':memory:' or path == '' then
		return true
	end

	local docStoreSize = WFFileUtils.fileAttributes( path ).fileSize

	if not docStoreSize then return false end

	local walSize = WFFileUtils.fileAttributes( path .. '-wal' ).fileSize
	if walSize then
		docStoreSize = docStoreSize + walSize
	end

	-- This logic mirrors how SQLite finds where to store temp files. See laso WFSQLiteDatabase.lua
	local tempDirs = {
		os.getenv( "SQLITE_TMPDIR" ) or false,
		os.getenv( "TMPDIR" ) or false,
		"/var/tmp",
		"/tmp"
	}

	local fileSystemFreeSpace, _

	for _, dir in ipairs( tempDirs ) do
		if dir and WFFileUtils.exists( dir ) then
			fileSystemFreeSpace, _ = WFFileUtils.freeSpaceAtPath( dir )

			if fileSystemFreeSpace > docStoreSize then return true end

		end

	end

	local p = KSPathUtils.parent( self._path )

	fileSystemFreeSpace, _ = WFFileUtils.freeSpaceAtPath( p )

	return  fileSystemFreeSpace >= docStoreSize

end

--------------------------------------------------------------------------------
--[[!
	\brief Retrieves this document store as a string.

	\return (string) The string representation.
]]--

function WFRxDocStore:__tostring()

	check( 'WFRxDocStore' )

	return string.format( "WFRxDocStore( %s )", self._path )

end

--------------------------------------------------------------------------------
--[[!
	\brief Retrieves a stream of deleted doc values from a specific point of time,
		as provided by the change counter, onward.

	\param changeCounterName (string) The name of the changeCounter. If a change
			counter does not exist for the given name, the current changeCounter
			value will be used as a starting point.
			
	\param optFieldSpec (optional, table of arrays of strings) paths of fields to include.

	\return <code>Observable< table ></code> Fires with batches of doc values as documents are
		deleted.
		Note: When this stream fires for space-feed documents that have been purged (all metadata removed),
		the doc value will only contain the shell of the id, space id and revision id.
]]--

function WFRxDocStore:deletedDocsSinceNamedChangeCounter( changeCounterName, optFieldSpec )

	check( 'WFRxDocStore' )

	return self._docStoreImp:request( 'deletedDocsSinceNamedChangeCounter', changeCounterName, optFieldSpec )

end

--------------------------------------------------------------------------------
--[[!
	\brief Create or update a change counter associated with deleted documents.

	\param changeCounterName (string) The name of the changeCounter to update or
		create.
			
	\param newChangeCounter (number) The new change counter value.

	\return <code>Observable< table ></code> Completes without a data response.
]]--

function WFRxDocStore:updateDeletedDocsSinceNamedChangeCounter( changeCounterName, newChangeCounter )

	check( 'WFRxDocStore' )

	return self._docStoreImp:request( 'updateDeletedDocsSinceNamedChangeCounter', changeCounterName, newChangeCounter )

end

--------------------------------------------------------------------------------
--[[!
	\brief Free disk space used to describe old document revisions.

	\param optVacuum set this to true if a vacuum should be run after the compaction.
		The size of the database on disk will only shrink if this is set to true.
		It is recomended to check - by calling enoughSpaceForVacuum - whether enough
		space is available for the vacuum operation.

	\warning This function should be used cautiously as it may take significant
		time. All other doc store features will be queued up and will not
		complete until the compaction has completed.

	\return <code>Observable< { percentComplete = ..., revisionsRemoved = ... } ></code> Resolves when compaction has completed.
		(You may safely ignore this Observable.)
]]--

function WFRxDocStore:compact( optVacuum )

	check( 'WFRxDocStore' )

	return self._docStoreImp:request( 'compact', optVacuum )
		:call( runUntilDone )

end

--------------------------------------------------------------------------------
--[[!
	\brief Free disk space used to describe old document revisions that have a newer revision with the given label.

	\param label if a revision with this label exists, all previous revisions will be compacted,
		otherwise, the revision will be left alone

	\param optVacuum (boolean?) run a vacuum to reclaim the space immediately

	\warning This function should be used cautiously as it may take significant
		time. All other doc store features will be queued up and will not
		complete until the compaction has completed.

	\return <code>Observable< empty ></code> Resolves when compaction has completed.
		(You may safely ignore this Observable.)
]]--

function WFRxDocStore:compactRevisionsBeforeLabel( label, optVacuum )

	check( 'WFRxDocStore' )

	return self._docStoreImp:request( 'compactRevisionsBeforeLabel', label, optVacuum )
		:call( runUntilDone )

end

--------------------------------------------------------------------------------
--[[!
	\brief Dumps the doc store data for multiple documents to a new db at the
	specified path. This does not guarantee a fully consistent db is created at 
	the resulting path and so it should only be used for troubleshooting/diagnostic
	purposes.

	\param docIds (table) array of local docIds

	\param path (string) filename where the new db should be created

	\return <code>Observable<string> Resolves when complete with the path at which
		the db was created.
]]--
function WFRxDocStore:dumpDocsToDbAtPath( docIds, path )
	
	check( 'WFRxDocStore', 'table', '+string' )
	
	return self._docStoreImp:request( 'dumpDocsToDbAtPath', docIds, path )

end

--------------------------------------------------------------------------------
--[[!
	\brief Creates a copy of the doc store database to a new db at the specified
	path.

	\param path (string) filename where the new db should be created

	\return <code>Observable<number> stream of values relaying progress of
		the backup as a fraction indicating percent complete.
]]--
function WFRxDocStore:onlineBackup( path )

	check( 'WFRxDocStore', '+string' )

	return self._docStoreImp:request( 'onlineBackup', path )

end

--------------------------------------------------------------------------------
--[[!
	\brief Closes the underlying db on the docStoreImp thread

	\warning This function should only be used at shutdown because all other
		docStore features that require an open db will fail. This applies to
		all threads that have a docStoreImp instance

	\return <code>Observable< empty ></code> Resolves when close has completed.
		Caller must subscribe to this Observable or closeDb call might not run.
]]--

function WFRxDocStore:closeDb()

	check( 'WFRxDocStore' )

	return self._docStoreImp:request( 'closeDb' )

end


function WFRxDocStore:getInitializationProgress()

	check( 'WFRxDocStore' )

	if ( self._initializationProgress ) then
		return self._initializationProgress:status()
	else
		return Rx.Observable.empty()
	end

end

--------------------------------------------------------------------------------
--[[!
	\brief (Intended for debugging use only) Generates code coverage statistics
		for the worker thread that implemented this doc store.

	\warning This function should only be used by WFRxDocStoreTest.lua.

	\return <code>Observable< empty ></code> Resolves when compaction has completed.
		generated. (You may safely ignore this Observable.)
]]--

function WFRxDocStore:DEBUG_dumpCoverage( path )

	check( 'WFRxDocStore', '+string' )

	return Rx.Observable.concat(
		self._docStoreImp:request( 'DEBUG_dumpCoverage', path ),
		self._docStoreChangesFeedImp:request( 'DEBUG_dumpCoverage', path ) )
	:call( runUntilDone )

end

--------------------------------------------------------------------------------
--[[!
	\brief (Intended for performance testing only) Enables a mode where the doc
		store will collect performance statistics on its SQLite database.

	\return <code>Observable< empty ></code> Resolves when done.
		(You may safely ignore this Observable.)
]]--

function WFRxDocStore:PERF_collectQueryPerformanceStats()

	check( 'WFRxDocStore' )

	return self._docStoreImp:request( 'PERF_collectQueryPerformanceStats' )
		:call( runUntilDone )

end

--------------------------------------------------------------------------------
--[[!
	\brief (Intended for performance testing only) Generates a string suitable
		for logging to a text file or window describing performance results.

	\return <code>Observable< string ></code> query statistics report
]]--

function WFRxDocStore:PERF_generateQueryStatsReport()

	check( 'WFRxDocStore' )

	return self._docStoreImp:request( 'PERF_generateQueryStatsReport' )

end

--------------------------------------------------------------------------------
--[[!
	\brief (Intended for unit test use only) Perform an integrity check
		of the doc store and report errors.

	\warning This function should only be used by unit tests.

	\return <code>Observable</code> signals onCompleted if successful;
		signals onError with an array of failure messages if any problems are found
]]--

function WFRxDocStore:performIntegrityCheck()

	check( 'WFRxDocStore' )

	return self._docStoreImp:request( 'performIntegrityCheck' )

end

--------------------------------------------------------------------------------
--[[!
	\brief Permanently shuts down this doc store.

	\return <code>Observable< empty ></code> Resolves when done.
		(You may safely ignore this Observable.)
]]--

function WFRxDocStore:halt()

	check( 'WFRxDocStore' )

	local oldDocStoreImp = self._docStoreImp

	local result = oldDocStoreImp
		:request( 'halt' )
		:call( runUntilDone )

	self:close()

	return result
	
end

--------------------------------------------------------------------------------
--[[!
	\brief Permanently shuts down this doc store and deletes all related files.

	\return <code>Observable< empty ></code> Resolves when done.
		(You may safely ignore this Observable.)
]]--

function WFRxDocStore:haltAndDelete()

	check( 'WFRxDocStore' )

	local oldDocStoreImp = self._docStoreImp

	self._docStoreImp = nil

	local result = oldDocStoreImp
		:request( 'haltAndDelete' )
		:call( runUntilDone )

	self:close()

	return result

end

--------------------------------------------------------------------------------
--[[!
	\brief Get a stream that fires with SQLite error codes when a doc store operation
		encounters a critical error. Possible error codes include:

		10 : Some kind of disk I/O error occurred
		11 : The database disk image is malformed
		13 : Insertion failed because database is full

	\return <code>Observable<number>
]]--

function WFRxDocStore:getSQLiteFatalErrorCodeStream()

	check( 'WFRxDocStore' )

	return self._docStoreImp:request( 'getSQLiteFatalErrorCodeStream' )

end

--------------------------------------------------------------------------------

function WFRxDocStore:_DEBUG_simulateCorruption()

	return 
		WFRxRunUntilDone( self._docStoreImp
							:request( "_DEBUG_simulateCorruption" ) )

end

--------------------------------------------------------------------------------

function WFRxDocStore:_DEBUG_bulkRequestInfoStream( refreshSeconds )

	return self._docStoreImp:request( "_DEBUG_bulkRequestInfoStream", refreshSeconds )

end

--------------------------------------------------------------------------------

function WFRxDocStore:_DEBUG_changesFeedInfoStream( refreshSeconds )

	return self._docStoreChangesFeedImp:request( "_DEBUG_changesFeedInfoStream", refreshSeconds )

end

--------------------------------------------------------------------------------

function WFRxDocStore:_DEBUG_clearIdCache()
	return WFRxRunUntilDone( self._docStoreImp:request( "_DEBUG_clearIdCache" ) )
end

--------------------------------------------------------------------------------

function WFRxDocStore:initializeReadOnlyDocStore( tag )
	return self._docStoreImp:request( "initializeReadOnlyDocStore", self._path, tag )
end


--------------------------------------------------------------------------------
--[[!
	\brief Return the number of SQLite transactions that have been executed
		thus far. Intended for debug / testing use only.
]]--

--[[ not yet implemented in worker thread
function WFRxDocStore:nTransactionsP()

	return self._docStoreImp:requestP( 'nTransactions' )
		:mapDataP( function( _, nTransactions )

			return 'data', nTransactions, self._transactionSources

		end )

end
]]--

--------------------------------------------------------------------------------

local function monitorPerformance( apiName )
	return WFPerformanceReport.wrapObservableFunction( 'WFRxDocStore:' .. apiName, WFRxDocStore[ apiName ] )
end 

WFRxDocStore.localDocValue = monitorPerformance( 'localDocValue' )

WFRxDocStore.applyOverlayLocalDocValue = monitorPerformance( 'applyOverlayLocalDocValue' )

WFRxDocStore.setLocalDocValue = monitorPerformance( 'setLocalDocValue' )

-- SPAM [jandrew 2017-05-24]: The following metrics are hidden behind config flags. 
-- They're usually too noisey and result in very large stamina logs.

if WFConfiguration.Stamina.changedDocsMetrics then 
	WFRxDocStore.changedDocs = monitorPerformance( 'changedDocs' )
end

if WFConfiguration.Stamina.changeCounterMetrics then
	WFRxDocStore.changeCounter = monitorPerformance( 'changeCounter' )
end

WFRxDocStore.bulkApplyOverlay = monitorPerformance( 'bulkApplyOverlay' )

WFRxDocStore.applyOverlay = monitorPerformance( 'applyOverlay' )

WFRxDocStore.deleteDoc = monitorPerformance( 'deleteDoc' )

WFRxDocStore.deleteRevisionAsNonCurrent = monitorPerformance( 'deleteRevisionAsNonCurrent' )

WFRxDocStore.deleteRevisionAsNonCurrentAndApplyOverlay = monitorPerformance( 'deleteRevisionAsNonCurrentAndApplyOverlay' )

WFRxDocStore.bulkSetDocValue = monitorPerformance( 'bulkSetDocValue' )

WFRxDocStore.setDocValue = monitorPerformance( 'setDocValue' )

WFRxDocStore.bulkDocValue = monitorPerformance( 'bulkDocValue' )

WFRxDocStore.docValue = monitorPerformance( 'docValue' )

WFRxDocStore.deletedDocsSinceNamedChangeCounter = monitorPerformance( 'deletedDocsSinceNamedChangeCounter' )

WFRxDocStore.updateDeletedDocsSinceNamedChangeCounter = monitorPerformance( 'updateDeletedDocsSinceNamedChangeCounter' )

--------------------------------------------------------------------------------

return Rx.Class 'WFRxDocStore'( WFRxDocStore )
