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

NmbRootModel.lua

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

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

NOTICE: All information contained herein is, and remains 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 are protected by all applicable
intellectual property laws, including trade secret and copyright laws.
Dissemination of this information or reproduction of this material is
strictly forbidden unless prior written permission is obtained from
Adobe Systems Incorporated.

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

local Rx = require 'Rx'
local Observable = Rx.Observable
local WFRxScheduler = require "WFRxScheduler"

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

	-- WF.core.utils
local WFRxRunUntilDone = require 'WFRxRunUntilDone'
local KSPrefs = require 'KSPrefs'
local KSDate = require 'KSDate'
local KSJsonDecoder = require 'KSJsonDecoder'
local KSTableUtils = require 'KSTableUtils'
local WFModelsUtils = require 'WFModelsUtils'
local WFPlatformInfo = require 'WFPlatformInfo'
local WFRxThrottledBatchedBulkApplyOverlay = require 'WFRxThrottledBatchedBulkApplyOverlay'

	-- WF.web.utils
local WFURI = require 'WFURI'

	-- WF.oz_client.managed_catalog
local NmbCatalog = require 'NmbCatalog'
local DesktopModelUtils = require 'DesktopModelUtils'
local DebuggingStreams = require "DebuggingStreams"
local WFCatAssets = require 'WFCatAssets'
local WFRxDocStoreUtils = require 'WFRxDocStoreUtils'
local WFOzDocUtils = require 'WFOzDocUtils'
local NmbDbCorruptionUtils = require "NmbDbCorruptionUtils"

	-- WF.lightroomcc.file_management.purge
local NmbPurgeControl = require "NmbPurgeControl"
local NmbCatWatermark = require "NmbCatWatermark"

	-- WF.oz_client.managed_catalog.pixel_provider
local PixelProviderDownloadNegativeModel =
		require "PixelProviderDownloadNegativeModel"
local PixelProviderVideoLoupeModel =
		require "PixelProviderVideoLoupeModel"
local PixelProviderGridPixelPathsModel = require "PixelProviderGridPixelPathsModel"
local PixelProviderLoupePixelPathsModel = require "PixelProviderLoupePixelPathsModel"
local PixelProviderIdlePixelPathsModel = require "PixelProviderIdlePixelPathsModel"
local PixelProviderPixelPathsModel = require "PixelProviderPixelPathsModel"
local HttpShutdownModel = require "HttpShutdownModel"

	-- WF.lightroomcc.sync
local NmbDCPDocumentProcessor = require "NmbDCPDocumentProcessor"

	-- WF.oz_client.managed_catalog.develop
local WFCatDevelopSettings = require 'WFCatDevelopSettings'

	-- WF.oz_client.managed_catalog.export

local WFFileUtils = require 'WFFileUtils'
local NmbConnectors = require 'NmbConnectors'
local NmbFileManagementSettings = require 'NmbFileManagementSettings'
local NmbTrackedDataBroadcaster = require 'NmbTrackedDataBroadcaster'

local NmbAcr = require "NmbAcr"

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

local logger = WFLogger 'NmbRootModel'

local derefOpt = KSTableUtils.derefOpt

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

local function logAndRaiseError( errStr, ... )

	if select( '#', ... ) > 0 then

		errStr = string.format( errStr, ... )

	end

	logger:error( errStr )

	error( errStr )

end

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

local function isNonNil( v )

	return v ~= nil

end

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

local function returnArg( n )

	return function( ... )

		return select( n, ... )

	end

end

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

local function getNonNilWFCatalogStream( wfCatalogStream )
	return wfCatalogStream
		:filter(
			function( wfCatalog )

				return wfCatalog ~= nil

			end )
		:take( 1 )
end

--------------------------------------------------------------------------------
-- makeSimpleSubmodelWithDefaultValue returns a function that will construct
-- a submodel that has the return value of fn as the submodel's value. The
-- first parameter of fn will be a WFCatalog instance. If we fail to log in,
-- the model's value will be ... instead
--------------------------------------------------------------------------------

local function makeSimpleSubmodelWithPriorityAndDefaultValueImp(
				wfCatalogStreamOfStream, priority, fn, defaultReturnVals,
				defaultReturnValsN )

	return function( ... )

		local params = { ... }

		local nParams = select( '#', ... )

		-- return a submodel specification with the value pointing to
		-- the stream generated by fn( wfCatalog, ... )

		return {

			priority = priority,

			value = wfCatalogStreamOfStream
					:flatMapLatest(
						function( wfCatalog )

							if not wfCatalog then

								return Rx.Observable.just(
										unpack( defaultReturnVals, 1, defaultReturnValsN ) )

							end

							return fn( wfCatalog, unpack( params, 2, nParams ) )

						end )
		}

	end

end

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

local function makeSimpleSubmodelWithPriorityAndDefaultValue(
				wfCatalogStreamOfStream, priority, fn, ... )

	if type( fn ) ~= "function" then

		logAndRaiseError( "function required, but got %s", type( fn ) )

	end

	local defaultReturnVals = { ... }

	local defaultReturnValsN = select( '#', ... )

	return makeSimpleSubmodelWithPriorityAndDefaultValueImp(
				wfCatalogStreamOfStream, priority, fn, defaultReturnVals,
				defaultReturnValsN )

end

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

local function makeSimpleSubmodelWithDefaultValue(
				wfCatalogStreamOfStream, fn, ... )

	return makeSimpleSubmodelWithPriorityAndDefaultValue(
			wfCatalogStreamOfStream, nil, fn, ... )

end

--------------------------------------------------------------------------------
-- makeSimpleSubmodelForValidWFCatalog returns a function that will construct
-- a submodel that has the return value of fn as the submodel's value. The
-- first parameter of fn will be a WFCatalog instance.
--------------------------------------------------------------------------------

local function makeSimpleSubmodelForValidWFCatalog( fn, wfCatalogStream )

	if type( fn ) ~= "function" then

		logAndRaiseError( "function required, but got %s", type( fn ) )

	end

	return function( _, ... )

		local params = { ... }

		local nParams = select( '#', ... )

		return {

			value = wfCatalogStream
						:filter( isNonNil )
						:take( 1 )
						:flatMapLatest(
							function( wfCatalog )

								return fn( wfCatalog, unpack( params, 1, nParams ) )

							end )

		}

	end

end

--------------------------------------------------------------------------------
-- Given a stream that emits a stream of single values
--
-- val1
-- val2
-- ...
--
-- makeRateLimitedBufferedStream will buffer the results in an array and return
-- the array buffer no more than buffersPerSecond
--
-- Note:
--
-- this is meant to be equivalent to
--
-- stream:bufferWithTime( 1/buffersPerSecond ) but with as many
-- Rx.AnonymousObservable eliminated as possible so that it is faster and
-- lighter
--------------------------------------------------------------------------------

local _makeRateLimitedBufferedStreamWarnings = {}

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

local function makeRateLimitedBufferedStream( desc, stream, buffersPerSecond )

	if type( desc ) ~= "string" then

		assert( "makeRateLimitedBufferedStream requires a description" )

	end

	if type( buffersPerSecond ) ~= "number" or buffersPerSecond <= 0 then

		assert( "invalid buffersPerSecond" )

	end

	local buffersPerSecondAsSeconds =
			Rx.Scheduler.normalize( 1 / buffersPerSecond )

	local scheduler = require 'WFRxScheduler'.sharedScheduler()

	return Rx.Observable.create(
		function( observer )

			local currentBufferedValues = {}

			local havePendingTimer

			-- holds a "most recent" timer that can be killed via :dispose()
			local currTimerSubscr = Rx.SerialDisposable()

			local valueCollectorSubscr =
				stream
					:subscribe(
						function( v, ... )

							-- check to make sure we didn't get a multiret
							-- stream and warn once if we did
							if select( "#", ... ) > 0 and
									not _makeRateLimitedBufferedStreamWarnings[ desc ] then

								_makeRateLimitedBufferedStreamWarnings[ desc ] = true

								logger:warnf( "makeRateLimitedBufferedStream Will not buffer extra values from %s", desc )

							end

							-- buffer the value
							currentBufferedValues[ #currentBufferedValues + 1 ] = v

							if not havePendingTimer then

								-- start a  new "most recent" timer to emit the
								-- buffered values
								havePendingTimer = true

								currTimerSubscr
									:setDisposable(
        								scheduler
											:scheduleFuture(
												buffersPerSecondAsSeconds,
												function()

													-- it is time time to send the values.
													local thisIntervalBuffer = currentBufferedValues

													currentBufferedValues = {}

													-- (scheduleFuture fires once)
													havePendingTimer = false

													observer:onNext( thisIntervalBuffer )

												end ) )

							end

						end,
						function( errStr, ... )

							observer:onError( errStr, ... )  -- relay errors

						end,
						function()

							observer:onCompleted() -- relay completion

						end )

		return Rx.CompositeDisposable(
				currTimerSubscr, valueCollectorSubscr )

	end )

end

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

-- @mark NmbRootModel

local NmbRootModel = {}

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

function NmbRootModel.makeModel( wfCatalogStream, dbCorruptionEventStream )

	assert( Observable:isClassOf( wfCatalogStream ),
				"wfCatalogStream is not an observable" )

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

	wfCatalogStream =
		wfCatalogStream
			:tap(
				function( wfCatalog )

					if wfCatalog and not NmbCatalog:isClassOf( wfCatalog ) and not WFPlatformInfo.isUnitTest() then
						logger:errorf( "Catalog model was passed something that's not a catalog: %s",
									   wfCatalog )
					end

					logger:infof( "New catalog: %s",
								  wfCatalog
										and tostring( wfCatalog )
										or ":unknown:" )

				end )

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

	local wfCatalogMetadataStream =
			wfCatalogStream
				:map(
					function( maybeWFCatalog )

						return {

							hasCatalog = not not maybeWFCatalog,

							ozCatalogId = maybeWFCatalog and maybeWFCatalog:ozCatalogId(),

							ozUserId = maybeWFCatalog and maybeWFCatalog:ozUserId(),

							pixelsPath = maybeWFCatalog and
											maybeWFCatalog:fileSpec() and
											maybeWFCatalog:fileSpec():pixelsPath(),

						}

					end )


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

	local pixelProviderProcessorStream =
				getNonNilWFCatalogStream( wfCatalogStream )
					:map( returnArg( 2 ) )

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

	local function makeForwardEventToPixelProviderProccessor( methName )


		-- _ is catalogModel
		return function( _, ... )

			-- this has fire and forget semantics

			local params = { ... }

			local nParams = select( '#', ... )

			WFRxRunUntilDone(
				pixelProviderProcessorStream
					:flatMapLatest(
						function( pixelProviderProcessor )

							return pixelProviderProcessor
									:runUntilDone(
										methName,
											unpack( params, 1, nParams ) )

						end ) )

		end

	end

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

	local profileCatalogStream =
				wfCatalogStream:filter( isNonNil ):map( returnArg( 3 ) )

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

	local wfCatSynchronizerStream =
				wfCatalogStream:filter( isNonNil ):map( returnArg( 4 ) )

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

	local cameraProfileIngestInProgress = Rx.ReplaySubject( 1 )

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

	local nmbModelDefinition = {

		value = wfCatalogMetadataStream,

		-- highFidelityModels are submodels that will deliver everything sent
		-- via the subModel's Rx.Observable value field. i.e. If you call
		-- highFidelitySubModel:value():subscribe( ... ), the subscription's
		-- onData handler will fire every time value:onNext( ... ) fires.
		highFidelityModels = {

			--------------------------------------------------
			-- addImagesToCatalog
			--
			-- High fidelity because need to see all status
			-- values to collect up all errors.
			--------------------------------------------------
			addImagesToCatalog = function( _, optImportOptions )
				return require 'WFCatImport'.makeAddImagesModel( wfCatalogStream:filter( isNonNil ),
																profileCatalogStream,
																wfCatSynchronizerStream,
																optImportOptions )
			end,

			--------------------------------------------------
			-- addImageArray
			-- High fidelity because need to see all status
			-- values to collect up all errors.
			--------------------------------------------------
			addImageArray = function( _, importInfo, optImportOptions )
				return {

					value = Observable.combineLatest( wfCatalogStream:filter( isNonNil ),
													profileCatalogStream,
													wfCatSynchronizerStream )
								:take( 1 )
								:flatMapLatest( function( wfCatalog, profileCatalog, wfCatSynchronizer )

									local WFCatImport = require 'WFCatImport'
									return WFCatImport.addImageArrayToCatalog( wfCatalog, profileCatalog, importInfo, wfCatSynchronizer, optImportOptions )

								end ),
				}
			end,

			-------------------------------------------------
			-- Create Text Watermark
			-------------------------------------------------

			createWatermark = function( _, name, settings, optFilePath, optFileName, optId )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						NmbCatWatermark.createWatermark, name, settings, optFilePath, optFileName, optId )

			end,

			--------------------------------------------------
			-- Update watermark
			--------------------------------------------------

			updateWatermark = function( _, id, name, settings, optFilePath, optFileName )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						NmbCatWatermark.updateWatermark, id, name, settings, optFilePath, optFileName )

			end,


			--------------------------------------------------
			-- delete watermarks
			--------------------------------------------------
			deleteWatermarks = function( _, watermarkIds )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatWatermark'.deleteWatermarks, watermarkIds )

			end,

			--------------------------------------------------
			-- get watermark Info
			--------------------------------------------------
			getWatermarkInfo = function( _, id )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						NmbCatWatermark.getWatermarkInfo, id )

			end,

			--------------------------------------------------
			-- get All watermark Infos
			--------------------------------------------------
			getAllWatermarksInfo = function( )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						NmbCatWatermark.getAllWatermarksInfo )

			end,

			-----------------------------------------------------
			 -- download image watermark
			-----------------------------------------------------
			downloadImageWatermark = function( _, docId )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						NmbCatWatermark.downloadImageWatermark, docId )

			end,


			--------------------------------------------------
			-- purge orphan watermark assets
			--------------------------------------------------

			purgeOrphanWatermarkAssets = function()

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						NmbCatWatermark.purgeOrphanWatermarkAssets )

			end,


			--------------------------------------------------------------------------------
			-- is Camera Profile Custom
			--------------------------------------------------
			isCameraProfileCustom = function( _, cameraProfileDigest, cameraModelName )
				return {
					value = wfCatalogStream:filter( isNonNil )
								:take( 1 )
								:flatMapLatest( function( wfCatalog )
										return wfCatalog:getCatalogProfileCache():getHrefAndFileNameForProfileDigestModel(
																					cameraProfileDigest, cameraModelName )
										:map( function( fileName, href )
											return fileName ~= nil or href ~= nil
										end )
									end ),
				}
			end,

			-----------------------------------------------
			-- listenForNewPresets
			--------------------------------------------------
			listenForNewPresets = function()
				return {

					value = Observable.combineLatest( wfCatalogStream:filter( isNonNil ),
													wfCatSynchronizerStream:filter( isNonNil ) )
								:flatMapLatest( function( _, wfCatSynchronizer )

									return wfCatSynchronizer
										:getStyleSynchronizer()
										:getStyleRefreshStream()

								end )

				}
			end,

			--------------------------------------------------
			-- addPresets
			--------------------------------------------------
			addPresets = function( _, presetArray )
				return {

					-- Until ACR gives us explicit control of the file name for v2
					-- presets and profiles, so it doesn't have to match the preet/profile
					-- name, but rather allowing us to use a document id, we need
					-- to pass the styleSynchronizer instance in to allow
					-- a refresh of the ACR cache after the wf model renames the file
					-- under the hood. Alternately, we could just rename the file
					-- in the client and perform the ACR cache refresh before triggering
					-- the wf model to sync it.

					value = Observable.combineLatest( wfCatalogStream:filter( isNonNil ),
										wfCatSynchronizerStream:filter( isNonNil ) )
								:take( 1 )
								:flatMapLatest( function( wfCatalog, wfCatSynchronizer )

									local NmbCatPreset = require 'NmbCatPreset'
									return NmbCatPreset.addPresetArrayToCatalog( wfCatalog,
															presetArray,
															wfCatSynchronizer:getStyleSynchronizer() )

						end )

				}
			end,

			--------------------------------------------------
			-- renamePreset
			--------------------------------------------------
			renamePreset = function( _, params )
				return {

					-- Same (roughly) comment applies here as for 'addPreset' with regard to
					-- file name and ACR cache refreshing.

					value = Observable.combineLatest( wfCatalogStream:filter( isNonNil ),
									wfCatSynchronizerStream:filter( isNonNil ) )
								:take( 1 )
								:flatMapLatest( function( wfCatalog, wfCatSynchronizer )

									return require "NmbCatPreset".renamePreset( wfCatalog, params,
										wfCatSynchronizer:getStyleSynchronizer() )

						end )
						:call( WFRxRunUntilDone )

				}
			end,

			--------------------------------------------------
			-- deletePresets
			--------------------------------------------------
			deletePresets = function( _, presetIds )
				return {

					value = wfCatalogStream:filter( isNonNil )
								:take( 1 )
								:flatMapLatest( function( wfCatalog )

									return require "NmbCatPreset".deletePresets(
										wfCatalog, presetIds )

						end )
						:call( WFRxRunUntilDone )

				}
			end,

			--------------------------------------------------
			-- deleteProfile
			--------------------------------------------------
			deleteProfile = function( _, profileId, favoriteFingerprint )
				return {

					value = wfCatalogStream:filter( isNonNil )
								:take( 1 )
								:flatMapLatest( function( wfCatalog )

									return require "NmbCatPreset".deleteProfile(
										wfCatalog, profileId, favoriteFingerprint )

						end )
						:call( WFRxRunUntilDone )

				}
			end,

			--------------------------------------------------
			-- updatePresetSettings
			--------------------------------------------------
			updatePresetSettings = function( _, presetId, path, digest )
				return {

					value = wfCatalogStream:filter( isNonNil )
								:take( 1 )
								:flatMapLatest( function( wfCatalog )

									return require "NmbCatPreset".updatePresetSettings(
										wfCatalog, presetId, path, digest )

						end )
						:call( WFRxRunUntilDone )

				}
			end,

			--------------------------------------------------
			-- syncDCPs
			--------------------------------------------------
			syncDCPs = function( _, params )
				return {

					value = wfCatalogStream:filter( isNonNil )
								:take( 1 )
								:flatMapLatest( function( wfCatalog )

									return require "WFCatDCPs".syncDCPs(
										wfCatalog, params )

						end )

				}
			end,

			--------------------------------------------------
			-- copyAndSyncDCPs
			--------------------------------------------------
			copyAndSyncDCPs = function( _, appDataFolderInfo )
				return {

					value = wfCatalogStream:filter( isNonNil )
								:take( 1 )
								:flatMapLatest( function( wfCatalog )

									cameraProfileIngestInProgress:onNext( true )

									return require "WFCatDCPs".copyAndSyncDCPs(
										wfCatalog, appDataFolderInfo )

									:finally( function()

										cameraProfileIngestInProgress:onNext( false )

									end )

						end )
						:call( WFRxRunUntilDone )

				}
			end,

			--------------------------------------------------
			-- importDCPs
			--------------------------------------------------
			importDCPs = function( _, explicitProfilePaths )
				return {

					value = wfCatalogStream:filter( isNonNil )
								:take( 1 )
								:flatMapLatest( function( wfCatalog )

									-- Not notifying client side about camera profile
									-- import in progress in this case, since the client
									-- application would have initiated it, and thus is
									-- aware of it already.

									return require "WFCatDCPs".importDCPs(
										wfCatalog, explicitProfilePaths )

									:flatMap( function( result )

										return Observable.just( result )

									end )

						end )

				}
			end,

			--------------------------------------------------
			-- getDCPDocId
			--------------------------------------------------
			getDCPDocId = function( _, fingerPrint, profileName )
				return {

					value = wfCatalogStream:filter( isNonNil )
								:take( 1 )
								:flatMapLatest( function( wfCatalog )

									return require "WFCatDCPs".getDCPDocId(
										wfCatalog, fingerPrint, profileName )
						end )

				}
			end,

			--------------------------------------------------
			-- isCameraProfileIngestInProgress
			--
			-- Allows observation of camera profile (dcp)
			-- ingest (one-time auto-slurp) status.
			--------------------------------------------------
			isCameraProfileIngestInProgress = function()
				return {

					value = cameraProfileIngestInProgress:filter( isNonNil )
								:take( 1 )
								:flatMapLatest( function(  )
									return cameraProfileIngestInProgress
								end )
				}
			end,

			--------------------------------------------------
			-- isPresetAndProfileIngestInProgress
			--
			-- Allows observation of xmp preset/profile ingest
			-- (one-time auto-slurp) status.
			--------------------------------------------------
			isPresetAndProfileIngestInProgress = function()
				return {

					value = wfCatSynchronizerStream:filter( isNonNil )
								:take( 1 )
								:flatMapLatest( function( wfCatSynchronizer )
									return wfCatSynchronizer:getStyleSynchronizer():getPresetProfileIngestInProgressStream()
								end )
				}
			end,

			--------------------------------------------------
			-- assetsWithProfilesStats
			--
			--------------------------------------------------
			assetsWithProfilesStats = function()

				return {

					value = wfCatalogStream
								:filter( isNonNil )
								:take( 1 )
								:flatMapLatest( 
									function( wfCatalog )

										return NmbDCPDocumentProcessor.assetsWithProfilesStatsStream( wfCatalog )

									end )

				}

			end,

			--------------------------------------------------
			-- changeStyleFavoriteStatus
			--------------------------------------------------
			changeStyleFavoriteStatus = function( _, fingerprint,
				isProfile, isFavorite, isDefaultFavorite )

				return {

					value = wfCatalogStream:filter( isNonNil )
						:take( 1 )
						:flatMapLatest( function( wfCatalog )

							return require "NmbStyleFavorites".changeFavoriteStatus(
								wfCatalog, fingerprint, isProfile,
								isFavorite, isDefaultFavorite )

						end )
						:call( WFRxRunUntilDone )

				}
			end,

			--------------------------------------------------
			-- listenForNewDCPs
			--------------------------------------------------
			listenForNewDCPs = function()
				return {

					value = Observable.combineLatest( wfCatalogStream:filter( isNonNil ),
													wfCatSynchronizerStream:filter( isNonNil ),
													profileCatalogStream:filter( isNonNil ) )
								:flatMapLatest( function( wfCatalog, wfCatSynchronizer, profileCatalog )

									local styleSynchronizer = wfCatSynchronizer:getStyleSynchronizer()

									-- An NmbDCPDocumentProcessor instance is required for ongoing link fixing
									-- as Lightroom CC uploads already-referenced custom camera profiles.
									-- Note that NmbDCPDocumentProcessor relies on catalog:getAllVolumeAgents()
									-- returning a NON-empty table, so it must be initialized late enough for
									-- that to be the case. (At the time of WFCatStyleSynchronizer initialization,
									-- that is not the case.)

									styleSynchronizer:initCameraProfileLinkProcessor( wfCatalog, profileCatalog )

									return styleSynchronizer
										:getCameraProfileRefreshStream()

								end )

				}
			end,

			--------------------------------------------------
			-- doc store update progress
			--
			-- High fidelity because need update per bulk doc upgrade.
			--------------------------------------------------
			docStoreUpdateProgress = function()

				return {

					value = wfCatalogStream:filter( isNonNil )
								:take( 1 )
								:flatMapLatest( function( wfCatalog )

									local docStoreUpdater = wfCatalog:getDocStoreUpdater()

									if docStoreUpdater then
										return docStoreUpdater:blockingUpdatesProgress()
									else
										return Observable.just( true )
									end

								end ),

				}

			end,

			--------------------------------------------------
			-- asset migration
			--
			-- High fidelity because Need update per asset
			--------------------------------------------------
			assetMigration = function( _, fileMoveRequired )

				local fileMover

				return {

					value = wfCatalogStream:filter( isNonNil )
								:take( 1 )
								:flatMapLatest( function( wfCatalog )

									fileMover = wfCatalog:getFileMover()

									local performFileMove = false

									local postponeFileMoveUntilDate = KSPrefs.postponeFileMoveUntilDate or 0

									if postponeFileMoveUntilDate <= KSDate.timeNow() then
										performFileMove = true
										KSPrefs.postponeFileMoveUntilDate = nil
									end

									if fileMover and performFileMove then
										return fileMover:maybePerformMigration( fileMoveRequired )
									else
										return Observable.just( true )
									end

								end ),

					close = function()

						if fileMover then
							return fileMover:stop()
						end

					end

				}

			end,

			--------------------------------------------------
			-- importFromManifest
			--
			-- High fidelity because need to see all status
			-- values to collect up all errors.
			--------------------------------------------------
			importFromManifest = function( _, path, fileName, contextInfo )
				return {

					value = Observable.combineLatest( wfCatalogStream:filter( isNonNil ),
													profileCatalogStream,
													wfCatSynchronizerStream )
								:take( 1 )
								:flatMapLatest( function( wfCatalog, profileCatalog, wfCatSynchronizer )

									local WFCatImportManifestProcessor = require 'WFCatImportManifestProcessor'
									return WFCatImportManifestProcessor.importManifest( path, fileName, wfCatalog, profileCatalog, wfCatSynchronizer, contextInfo )

								end ),
				}
			end,

			--------------------------------------------------
			-- exportAssets
			--
			-- High fidelity because need update per asset
			--------------------------------------------------
			exportAssets = function( _, assetIds, commonExportSettings, extraSettingsByAssetId, maybeKeepSyncRunning, sessionID, useGPU, caiOptions, optReleasePurgeProtectionOnClose )

				local _exportTask
				local _cleanupFn

				return {

					value = Observable.combineLatest( wfCatalogStream:filter( isNonNil ),
														wfCatSynchronizerStream )
						:take( 1 )
						:map( function( wfCatalog, maybeSynchronizer )

							-- Prevent the exported assets from being purged during the export.

							local fileSpec = wfCatalog:fileSpec()
							local originalsVolumeGuid = fileSpec:originalsPathInfo().volumeGuid

							local volumeAgent = wfCatalog:getVolumeAgentByVolumeGuid( originalsVolumeGuid )

							volumeAgent:protectFromPurge( assetIds )

							-- `maybeKeepSyncRunning` indicates whether the export needs the sync
							-- mechanism. This is hopefully temporary and can be removed when we
							-- decouple exporting of shared-with-you images from the sync feed.

							-- Prevent idle downloader from taking up space.
							local wasSyncRunning
							if maybeSynchronizer and not maybeKeepSyncRunning then
								wasSyncRunning = maybeSynchronizer:isSyncRunningNow()
								maybeSynchronizer:stopSyncing()
							end

							_cleanupFn = function()
								volumeAgent:releaseProtectionFromPurge( assetIds )
								if maybeSynchronizer and wasSyncRunning then
									maybeSynchronizer:startSyncing()
								end
							end

							return NmbAcr.WFCatExport.exportAssetsFromCatalog(
								wfCatalog,
								pixelProviderProcessorStream,
								maybeSynchronizer,
								assetIds,
								commonExportSettings,
								extraSettingsByAssetId,
								sessionID,
								useGPU,
								caiOptions)

						end )
						:tap( function( exportTask )
							_exportTask = exportTask
						end )
						:flatMapLatest( function( exportTask )

							return exportTask:status()
						end )
						:filter( function( data )

							-- NMB-18044: Send info only when some assets are processed
							return data
									and data.processedCount
									and data.processedCount > 0

						end )
						:finally( function()
							-- This (purge protection removal) can happen before the export
							-- batch queue is finished, thus causing NMB-25874 (some of the
							-- last assets to be exported fail to get exported, because purge
							-- agent has already removed the original). To 'rock the boat' as
							-- little as possible, let the caller decide whether to go with
							-- the 'original' behavior of calling it here or to have '_cleanupFn'
							-- called at submodel close instead.
							if not optReleasePurgeProtectionOnClose then
								_cleanupFn()
							end
						end ),

					close = function()

						if optReleasePurgeProtectionOnClose and type( _cleanupFn ) == "function" then
							_cleanupFn()
						end

						if _exportTask then
							_exportTask:dispose()
							_exportTask = nil
						end

					end,
				}
			end,

			--------------------------------------------------
			-- getAssetHierarchyForArchival
			--
			-- Fetches the hierarchy of assets for the given itemIds. If an item is a folder,
			-- or album, creates a nested table with its contents. Each item will have a type,
			-- i.e., 'album', 'folder' or 'asset', id and name (string). Asssets, also have its
			-- file size and creation date.
			--------------------------------------------------
			getAssetHierarchyForArchival = function( _, itemIds )

				return {
					value = wfCatalogStream:filter( isNonNil )
						:take( 1 )
						:flatMapLatest( function( wfCatalog )
							return require 'NmbArchival'.getAssetHierarchy( wfCatalog, itemIds )
						end ),
				}

			end,
			
			--------------------------------------------------
			-- archiveAssets
			--
			-- Saves the original files into the specified destination folders
			--------------------------------------------------
			archiveAssets = function( _, archiveSettings, exportSettings, assetsList )

				return {
					value = Observable.combineLatest( wfCatalogStream:filter( isNonNil ),
														wfCatSynchronizerStream )
						:take( 1 )
						:flatMapLatest( function( wfCatalog, maybeSynchronizer )
							
							return require 'NmbArchival'.archiveAssets(
								wfCatalog,
								pixelProviderProcessorStream,
								maybeSynchronizer,
								archiveSettings,
								exportSettings,
								assetsList )
						
						end ),
				}

			end,

			--------------------------------------------------
			-- maybeGetOriginalPaths
			--
			-- TODO: Ensure nothing harmful will happen if
			-- an export is going on for the same assets at
			-- the same time as another feature is requiring
			-- local originals via this submodel.
			--------------------------------------------------
			maybeGetOriginalPaths = function( _, assetIds, oneByOne )

				local volumeAgent

				return {

					value = wfCatalogStream:filter( isNonNil )
								:take( 1 )
								:flatMapLatest( function( wfCatalog )

									local ppps = pixelProviderProcessorStream
									local provider = require "WFCatExportPixelProviderTask"( ppps )

									local fileSpec = wfCatalog:fileSpec()

									local originalsVolumeGuid = fileSpec:originalsPathInfo().volumeGuid

									volumeAgent = wfCatalog:getVolumeAgentByVolumeGuid( originalsVolumeGuid )

									volumeAgent:protectFromPurge( assetIds )

									local function streamOfGetOriginalStreams()
										return Observable.from( assetIds )
										-- intentionally using :map instead of :flatMap to 
										-- provide control over the rate provider:getOriginal() is called.
										:map( function( assetId )
											return provider:getOriginal( assetId )
											:flatMap( function( _, originalPath )
												if WFFileUtils.exists( originalPath ) then
													return Observable.just { path = originalPath, imageId = assetId }
												else
													return provider:getProxy( assetId )
													:map( function( _, proxyPath )
														-- Return what we have, even if it's just the asset id.
														return { proxyPath = proxyPath, imageId = assetId, downloadError = originalPath }
													end )
												end
											end )
										end )
									end

									local downloadOriginalsResultStream =
										-- use :flatMerge to avoid swamping
										-- the export provider with #assetIds
										-- parallel requests. 16 is arbitrary
										streamOfGetOriginalStreams()
											:flatMerge( 16 )

									if not oneByOne then
										return downloadOriginalsResultStream
										:toArray()
									else
										return downloadOriginalsResultStream
									end

								end ),

					close = function()
						volumeAgent:releaseProtectionFromPurge( assetIds )
					end
				}

			end,

			--------------------------------------------------
			-- getProxyPaths
			--------------------------------------------------

			getProxyPaths = function( _, assetIds )

				local volumeAgent

				return {

					value = wfCatalogStream:filter( isNonNil )
								:take( 1 )
								:flatMapLatest( function( wfCatalog )

									local ppps = pixelProviderProcessorStream
									local provider = require "WFCatExportPixelProviderTask"( ppps )

									local fileSpec = wfCatalog:fileSpec()

									local originalsVolumeGuid = fileSpec:originalsPathInfo().volumeGuid

									volumeAgent = wfCatalog:getVolumeAgentByVolumeGuid( originalsVolumeGuid )

									volumeAgent:protectFromPurge( assetIds )

									return Observable.from( assetIds )
									:flatMap( function( assetId )
										return provider:getProxy( assetId )
										:map( function( _, proxyPath )
											return { proxyPath = proxyPath, imageId = assetId }
										end )
									end )
									:toArray()

								end ),

					close = function()
						volumeAgent:releaseProtectionFromPurge( assetIds )
					end
				}

			end,

			--------------------------------------------------
			-- forgetVolume
			--
			-- High fidelity because need update per asset
			--------------------------------------------------
			forgetVolume = function( _, volume )

				return {

					value = wfCatalogStream:filter( isNonNil )
								:take( 1 )
								:flatMapLatest( function( wfCatalog )

									local fileMover = wfCatalog:getFileMover()

									if fileMover then
										return fileMover:forgetVolume( volume )
									else
										return Observable.just( true )
									end

								end ),

				}

			end,

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

			addToProtectedRingBuffer = function( _, assetId )

				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )

							:flatMapLatest( function( wfCatalog )

								local obs = Observable.just( true )

								local volumeAgents = wfCatalog:getAllVolumeAgents()

								for _, volumeAgent in pairs( volumeAgents ) do
									obs:concat( volumeAgent:addToProtectedRingBuffer( assetId ) )
								end

								return obs:takeLast( 1 )

							end )

				}

			end,

			--------------------------------------------------
			-- ongoingDevelopContents
			--
			-- high fidelity because we want to get updates
			-- for all of the asset ids and they may come
			-- in bursts
			--------------------------------------------------
			ongoingDevelopContents = function()

				local ongoingDevelopContentsModel =
						WFCatDevelopSettings.ongoingDevelopContentsModel(
							getNonNilWFCatalogStream( wfCatalogStream ) )

				-- ongoingDevelopContentsModel:value() is a "bursty" stream and
				-- will cause "send" backlogs unless buffered. See
				-- NMB-21223
				ongoingDevelopContentsModel.value =
						makeRateLimitedBufferedStream(
							"ongoingDevelopContents",
							ongoingDevelopContentsModel.value, 6 )

				return ongoingDevelopContentsModel

			end,

			--------------------------------------------------
			-- setCameraRawSettings
			--
			-- high fidelity because we want to get the result
			-- every time we send a save event so that the
			-- client knows when there are no more saves
			-- pending
			--------------------------------------------------
			setCameraRawSettings = function()

				return WFCatDevelopSettings.setCameraRawSettings(
							getNonNilWFCatalogStream( wfCatalogStream ) )

			end,

			--------------------------------------------------
			-- pixel path stream API
			--
			-- These are all high fidelity because we want
			-- to deliver all paths that are requested.
			--------------------------------------------------

			--------------------------------------------------
			-- pixelPaths
			--------------------------------------------------
			pixelPaths = function( _, ozCatalogId, fps )

				return PixelProviderPixelPathsModel.make(
							wfCatalogStream, ozCatalogId, fps )

			end,

			--------------------------------------------------
			-- idlePixelPaths
			--------------------------------------------------

			idlePixelPaths = function( _, pixStreamId, longSide )

				return PixelProviderIdlePixelPathsModel.make(
						wfCatalogStream, pixStreamId, longSide )

			end,

			downloadPersonThumbnailResponseStream =
				function()

					local personPathTableStream =
						wfCatSynchronizerStream
							:filter( isNonNil )
							:take ( 1 )
							:flatMapLatest(
								require 'WFCatPerson'.downloadPersonThumbnailResponseStream )

					local personPathTablesStream =
						makeRateLimitedBufferedStream(
							"downloadPersonThumbnailResponseStream",
							personPathTableStream, 24 )

					return {

						value = personPathTablesStream
					}

				end,

			--------------------------------------------------
			-- getAlbumListForImages
			--------------------------------------------------

			getAlbumListForImages = function( _, assetIds )

				return {

					value = wfCatalogStream
								:filter( function( wfCatalog )
									return wfCatalog ~= nil
								end )
								:flatMapLatest( function( wfCatalog )
									return require 'WFCatAlbums'.getAlbumListForAssetIds( wfCatalog, assetIds )
								end )
				}

			end,

			--------------------------------------------------
			-- getOzAssetIdsForLocalIds
			--------------------------------------------------

			getOzAssetIdsForLocalIds = function( _, localIds )

				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMapLatest( function( wfCatalog )
									return require 'WFCatIdUtils'.getOzAssetIdsForLocalIds( wfCatalog, localIds )
								end )
				}

			end,

			--------------------------------------------------
			-- getOzAssetIdsAndTypeForIds
			--------------------------------------------------

			getOzAssetIdsAndTypeForIds = function( _, localIds )

				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMapLatest( function( wfCatalog )
									return require 'WFCatIdUtils'.getOzAssetIdsAndTypeForIds( wfCatalog, localIds )
								end )
				}

			end,

			--------------------------------------------------
			-- _DEBUG_getLocalIdAndSubtypeForOzAssetId
			--------------------------------------------------

			_DEBUG_getLocalIdAndSubtypeForOzAssetId = function( _, ozAssetId )

				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMapLatest( function( wfCatalog )
									return require 'WFCatIdUtils'._DEBUG_getLocalIdAndSubtypeForOzAssetId( wfCatalog, ozAssetId )
								end )
				}

			end,

			--------------------------------------------------
			-- getCollaboratorAvatar
			--------------------------------------------------
			getCollaboratorAvatar = function( _, userId, optPreferredResolution )
				return {

					value = Observable.combineLatest( getNonNilWFCatalogStream( wfCatalogStream ),
													wfCatSynchronizerStream:filter( isNonNil ):take( 1 ) )
								:flatMapLatest( function( wfCatalog, wfCatSynchronizer )

									local NmbCollaborator = require 'NmbCollaborator'
									return NmbCollaborator.getAvatar( wfCatalog, wfCatSynchronizer, userId, optPreferredResolution )

								end ),
				}
			end,

			--------------------------------------------------
			-- waitForPendingDevelopSettingsSaveForAssetIds
			--------------------------------------------------
			waitForPendingDevelopSettingsSaveForAssetIds = function( _, ozAssetIds )

				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMapLatest( function()
									return require 'WFCatDevelopSettings'.waitForPendingDevelopSettingsSaveForAssetIds( ozAssetIds )
								end )

				}

			end,

			--------------------------------------------------
			-- dashboard reporting
			--------------------------------------------------
			generateDashboard = function( _, ... )

				return require 'NmbCatalogDashboard'.makeModel(
								getNonNilWFCatalogStream( wfCatalogStream ) )

			end,

			--------------------------------------------------
			-- basicSearchAndSort
			--------------------------------------------------
			basicSearchAndSort = function( _, localSearchTerms, onlineSearchTerms, sortTerms )

				return require 'WFCatSearchAndSort'.makeModel(
						wfCatalogStream, localSearchTerms, onlineSearchTerms,
						sortTerms )

			end,

			--------------------------------------------------
			-- trackedData
			--------------------------------------------------
			trackedData = function( _ )
				return {
					value = NmbTrackedDataBroadcaster.broadcastStream()
				}
			end,


			--------------------------------------------------
			-- _DEBUG_httpHistoryStream
			--------------------------------------------------
			_DEBUG_httpHistoryStream = function()

				local WFRxHttpClient = require "WFRxHttpClient"

				return {
					value = WFRxHttpClient.DEBUG_historyStream()
				}

			end,

			--------------------------------------------------
			-- _DEBUG_assetPanelInfoStream
			--------------------------------------------------
			_DEBUG_assetPanelInfoStream = function( _, docId )

				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
						:flatMapLatest( function( wfCatalog )
							return wfCatalog:docStore():ongoingDocValue( docId )
						end ),
				}

			end,

			--------------------------------------------------
			-- _DEBUG_assetPanelDocWatcherStream
			--------------------------------------------------
			_DEBUG_assetPanelDocWatcherStream = function( _, docIds )

				local docIdArrayStream = Rx.BehaviorSubject( docIds )

				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
						:flatMapLatest( function( wfCatalog )
							return wfCatalog:docStore():makeDocValueWatcher( docIdArrayStream, "_DEBUG_assetPanelDocWatcherStream" )
						end ),
				}

			end,

			--------------------------------------------------
			-- _DEBUG_ozInfoStream
			--------------------------------------------------
			_DEBUG_ozInfoStream = function( _, docId )

				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
						:flatMapLatest( function( wfCatalog )
							return Observable.combineLatest(
								Observable.just( wfCatalog ),
								wfCatalog:docStore():docValue( docId )
						)
						end )
						:flatMapLatest( function( wfCatalog, docValue )
							local assetURL = derefOpt( docValue, '_serverProvidedMetadata', 'links', 'self', 'href' )
							if assetURL then
								return wfCatalog:ozUser():get( assetURL )
								:map( function( result, headers )
									if headers and tostring( headers.status_code ) == "200" then
										local resultTable = KSJsonDecoder.decodeWebSafeJson( result )
										return resultTable
									end

									return {}
								end )
							else
								return Observable.just( {} )
							end
						end ),
				}

			end,

			--------------------------------------------------
			-- clearErrorsAndRetry
			--------------------------------------------------
			clearErrorsAndRetrySync = function( _, assetIds )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatStuckSyncUtils'.clearErrorsAndRetrySync, assetIds )

			end,

			--------------------------------------------------
			-- _DEBUG_updateLocalOnly
			--------------------------------------------------
			_DEBUG_updateLocalOnly = function( _, docId, newValue, ...  )

				local fieldList = { ... }

				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
						:flatMapLatest( function( wfCatalog )

							local timeStamp = KSDate.formatDateTimeAsUTC( KSDate.timeNow() ) .. "Z"
							local fieldDesc = table.concat( fieldList, "." )
							local newValueOrDelete = newValue or { _delete = true }
							local errorOverlay = {
								_id = docId,
								_localOnly = {
									history = {
										[ timeStamp ] = string.format( "_DEBUG_updateLocalOnly set %s to %s", fieldDesc, newValue )
									},
								}
							}

							KSTableUtils.setNested( errorOverlay._localOnly, newValueOrDelete, unpack( fieldList ) )

							return wfCatalog:docStore():applyOverlay( errorOverlay )

						end )
				}

			end,
		},

		-- mostRecentValueModels are submodels for which only the most recent
		-- value on the subModel's Rx.Observable value field is guaranteed to
		-- be delivered. i.e. mostRecentValueSubModel:value():subscribe( ... ),
		-- the subscription's onData handler may drop events on the stream if
		-- many events are sent in a short period, but it eventually
		-- send the most recent value.
		mostRecentValueModels = {

			--------------------------------------------------
			-- restartRequiredForDbIssue
			--
			-- refer to handleCorruptNmbCatalogDerivedDbResultStream
			-- in NmbDbCorruptionUtils for details, but this model's value will
			-- emit at most one result and the client app should quit and
			-- possibly restart if an event is emitted.
			--------------------------------------------------
			restartRequiredForDbIssue = function()

				return {

					value = wfCatalogStream
								:take( 1 )
								:flatMapLatest(
									function( wfCatalog, _, profileCatalog, wfCatSynchronizer )

										return NmbDbCorruptionUtils.handleCorruptNmbCatalogDerivedDbResultStream(
												dbCorruptionEventStream,
												wfCatalog, profileCatalog,
												wfCatSynchronizer._metadataSyncClient )

									end )
								:map(
									function( errorType, canBeSolvedByRestart, dbName, errStr )

										return { errorType, canBeSolvedByRestart, dbName, errStr }

									end )

				}

			end,


			--------------------------------------------------
			-- assetSyncStatus
			--------------------------------------------------
			assetSyncStatus = function( _, assetIds )

				return require 'WFCatSyncStatusModel'.makeAssetSyncStatusModel( wfCatalogStream, assetIds )

			end,

			--------------------------------------------------
			-- syncStatus
			--------------------------------------------------
			syncStatus = function()

				return require 'WFCatSyncStatusModel'.makeSyncStatusModel( wfCatalogStream )

			end,

			--------------------------------------------------
			-- estimatedTimeRemainingForUploads
			--------------------------------------------------
			estimatedTimeRemainingForUploads = function()

				-- Returns the estimated time remaining for uploads, and copies, in seconds.
				-- -1 is returned in the event there is nothing to upload.

				return {

					value = wfCatSynchronizerStream
								:filter( isNonNil )
								:take( 1 )
								:flatMap( function( wfCatSynchronizer )
									return wfCatSynchronizer:getEstimatedTimeRemainingForUploads()
								end )

				}

			end,


			--------------------------------------------------
			-- areAssetsFullySyncedWithOz
			--------------------------------------------------

			-- The following function resolves to `true` if all of the assets
			-- are fully synced with Oz, and there are no pending local changes
			-- that have yet to sync.

			areAssetsFullySyncedWithOz = function( _, assetIds )

				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )

							:flatMap( function( wfCatalog )

								return WFOzDocUtils.areAssetsFullySyncedWithOz( wfCatalog, assetIds )

							end )
				}

			end,

			--------------------------------------------------
			-- waitUntilAssetsAreFullySyncedWithOz
			--------------------------------------------------

			-- The following function resolves to `true` once all of the assets
			-- are fully synced with Oz.

			waitUntilAssetsAreFullySyncedWithOz = function( _, assetIds )

				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )

							:flatMap( function( wfCatalog )

								return WFOzDocUtils.waitUntilAssetsAreFullySyncedWithOz( wfCatalog, assetIds )

							end )
				}

			end,

			--------------------------------------------------
			-- waitUntilAssetsAreFullySyncedWithOzAndHasRendition
			--------------------------------------------------

			-- The following function resolves to `true` once all of the assets
			-- are fully synced with Oz.

			waitUntilAssetsAreFullySyncedWithOzAndHasRendition = function( _, assetIds )

				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )

							:flatMap( function( wfCatalog )

								return WFOzDocUtils.waitUntilAssetsAreFullySyncedWithOzAndHasRendition( wfCatalog, assetIds )

							end )
				}

			end,

			--------------------------------------------------
			-- storage
			--------------------------------------------------
			storage = function()

				return require 'WFCatSyncStatusModel'.makeStorageModel( wfCatalogStream )

			end,

			--------------------------------------------------
			-- checkAndReclaimSpaceForNewImages
			--------------------------------------------------

			--[[
				Checks if there is enough free space for incoming images. If the free space is
				not sufficient, it will try to free up space by purging. Checks for free space
				both on the main drive (for thumbnails, proxies etc) and originals drive (for
				originals). If the originals drive is the main drive, make enough space for both.
				If the drives are different, but the originals drive is offline, then also make
				space for both on the main drive.
				Returns a table {
					originalsAreOnMainVolume = boolean,
					originalsVolumeIsOffline = boolean,

					mainVolumeName = string,
					mainVolumePath = string,
					mainVolumeSpaceRequired = number,
					mainVolumeSpaceAvailable = number,

					originalsVolumeName = string,
					originalsVolumePath = string,
					originalsVolumeSpaceRequired = number,
					originalsVolumeSpaceAvailable = number,
				} ]]

			checkAndReclaimSpaceForNewImages = function( _, numImages, spaceNeeded )
				return {
					value = wfCatalogStream:filter( isNonNil )
								:take( 1 )
								:flatMapLatest( function( wfCatalog )

									local fileSpec = wfCatalog:fileSpec()

									local mainVolumeGuid = fileSpec:catalogPathInfo().volumeGuid
									local mainVolumeName = WFFileUtils.getVolumeNameFromGuid( mainVolumeGuid )
									local mainVolumePath = WFFileUtils.getVolumeFromGuid( mainVolumeGuid )

									local originalsVolumeGuid = fileSpec:originalsPathInfo().volumeGuid

									-- For now, be VERY conservative about the estimated space
									-- requirement for previews, smart previews, catalog info, etc.

									-- Post-1.0 we can get more clever here and disable
									-- smart preview downloading during the migration triggered
									-- import, then re-enable it, for example. That way, any space issues
									-- encountered due to smart previews can be dealt with via
									-- the normal mechanisms provided by the purge agent, and we
									-- never have to account for smart preview storage requirement
									-- in this calculation.

									-- See NMB-5937.

									local gig = 1024 * 1024 * 1024
									local additionalSpaceNeeded

									if NmbFileManagementSettings.keepAllProxies() then
										additionalSpaceNeeded = math.ceil( ( numImages / 1000 ) * 2 * gig )
									else
										-- Don't need to account for smart previews, which makes the space
										-- requirement smaller.
										additionalSpaceNeeded = math.ceil( ( numImages / 1000 ) * 0.75 * gig )
									end

									local volumeAgent = wfCatalog:getVolumeAgentByVolumeGuid( originalsVolumeGuid )

									if volumeAgent then
										return volumeAgent:getVolumeStatusStream()
											:take( 1 )
											:flatMapLatest( function( volumeStatus )

												local originalsVolumeIsOnline = volumeStatus.isOnline
												local originalsAreOnMainVolume = mainVolumeGuid == originalsVolumeGuid

												if originalsAreOnMainVolume and not originalsVolumeIsOnline then
													return Observable.throw( 'noDestinationVolume' )
												end

												local spaceNeededOnMainVolume
												local spaceNeededOnOriginalsVolume
												local spaceCheckObservable

												if not originalsAreOnMainVolume and originalsVolumeIsOnline then
													-- Looks for space on separate volumes
													local mainVolumeAgent = wfCatalog:getVolumeAgentByVolumeGuid( mainVolumeGuid )
													local originalsVolumeAgent = wfCatalog:getVolumeAgentByVolumeGuid( originalsVolumeGuid )

													spaceNeededOnMainVolume = additionalSpaceNeeded
													spaceNeededOnOriginalsVolume = spaceNeeded

													spaceCheckObservable = Observable.combineLatest(
														mainVolumeAgent:tryToClaimSpace( 'checkAndReclaimSpaceForNewImages_main', spaceNeededOnMainVolume ),
														originalsVolumeAgent:tryToClaimSpace( 'checkAndReclaimSpaceForNewImages_originals', spaceNeededOnOriginalsVolume )
													)

												else
													-- Look for space only on the main volume
													local mainVolumeAgent = wfCatalog:getVolumeAgentByVolumeGuid( mainVolumeGuid )

													spaceNeededOnMainVolume = spaceNeeded + additionalSpaceNeeded
													spaceNeededOnOriginalsVolume = 0

													spaceCheckObservable = Observable.combineLatest(
														mainVolumeAgent:tryToClaimSpace( 'checkAndReclaimSpaceForNewImages_main', spaceNeededOnMainVolume ),
														Observable.just( 0 )
													)
												end

												return spaceCheckObservable
													:map( function( spaceAvailableOnMainVolume, spaceAvailableOnOriginalsVolume )

														local originalsVolumeName = originalsVolumeIsOnline
															and WFFileUtils.getVolumeNameFromGuid( originalsVolumeGuid )

														local originalsVolumePath = originalsVolumeIsOnline
															and WFFileUtils.getVolumeFromGuid( originalsVolumeGuid )

														return {
															originalsAreOnMainVolume = originalsAreOnMainVolume,
															originalsVolumeIsOffline = not originalsVolumeIsOnline,

															mainVolumeName = mainVolumeName,
															mainVolumePath = mainVolumePath,
															mainVolumeSpaceRequired = spaceNeededOnMainVolume,
															mainVolumeSpaceAvailable = spaceAvailableOnMainVolume,

															originalsVolumeName = originalsVolumeName,
															originalsVolumePath = originalsVolumePath,
															originalsVolumeSpaceRequired = spaceNeededOnOriginalsVolume,
															originalsVolumeSpaceAvailable = spaceAvailableOnOriginalsVolume,
														}
													end )
											end )

									else
										return Observable.throw( 'errorFindingVolume' )
									end

								end )
				}
			end,

			--------------------------------------------------
			-- checkAndReclaimSpaceForExport
			--------------------------------------------------
			checkAndReclaimSpaceForExport = function( _, assetIds, settings )

				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
						:flatMapLatest( function( wfCatalog )

							return NmbAcr.WFCatExport.checkAndReclaimSpace(
								wfCatalog,
								assetIds,
								settings )

						end )

				}
			end,

			--------------------------------------------------
			-- checkAndReclaimSpaceForLocalAlbums
			--------------------------------------------------
			checkAndReclaimSpaceForLocalAlbums = function( _, albumIds )

				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
						:flatMapLatest( function( wfCatalog )

							return require 'NmbOfflineAssets'.checkAndReclaimSpaceForAlbums(
								wfCatalog,
								albumIds )

						end )

				}
			end,

			--------------------------------------------------
			-- getLocalStorageSize
			--------------------------------------------------

			--[[!

				\brief Get the disk space usage of Lr

				\return <code>Observable< table ></code> Fires with a table containing
					the bucket sizes for all possible permutations of keepAllOriginals,
					keepAllProxies and storeOriginalsOnMainVolume.

					LrD code is responsible for extracting the appropriate nested table using
					booleans as the table keys:

					sizes[ storeOriginalsOnMainVolume ][ keepAllOriginals ][ keepAllProxies ]

			--]]

			getLocalStorageSize = function()

				return {

					value = wfCatalogStream:filter( isNonNil )
						:take( 1 )
						:flatMapLatest( function( wfCatalog )

							return require 'WFCatLocalStorage'.getLocalStorageSize( wfCatalog )

						end )

				}

			end,

			--------------------------------------------------
			-- getRecommendedCacheSizes
			--------------------------------------------------

			--[[!

				\brief Returns a table containing cache size recommendations, as well as
					minimum and maximum cache size values.

				\return <code>Observable< table></code> A table of cache size values.

			--]]

			getRecommendedCacheSizes = function( _ )

				return {

					value = wfCatalogStream:filter( isNonNil )
						:take( 1 )
						:flatMapLatest( function( wfCatalog )

							return require 'WFCatLocalStorage'.getRecommendedCacheSizes( wfCatalog )

						end )

				}

			end,

			--------------------------------------------------
			-- clearCache
			--------------------------------------------------

			--[[!

				\brief Force the purge agent to purge all purgeable binaries within the
						photo cache.

				\return Observable<true> Fires with true upon completion.

			--]]

			clearCache = function( _ )

				return {

					value = wfCatalogStream:filter( isNonNil )
						:take( 1 )
						:flatMapLatest( function( wfCatalog )

							local fileSpec = wfCatalog:fileSpec()

							local mainVolumeGuid = fileSpec:catalogPathInfo().volumeGuid

							local mainVolumeAgent = wfCatalog:getVolumeAgentByVolumeGuid( mainVolumeGuid )

							return mainVolumeAgent:clearCache()

						end )

				}

			end,

			--------------------------------------------------
			-- deleteAssets
			--------------------------------------------------
			deleteAssets = function( _, assetIds )
				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
						:flatMap( function( wfCatalog, _, _, wfCatSynchronizer )
							return require 'WFCatDelete'.deleteAssets( wfCatalog, assetIds,
								wfCatSynchronizer )
						end )
						:call( WFRxRunUntilDone )
				}
			end,

			--------------------------------------------------
			-- restoreTrashAssets
			--------------------------------------------------
			restoreTrashAssets = function( _, assetIds )
				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
						:flatMap( function( wfCatalog, _, _, wfCatSynchronizer )
							return require 'WFCatDelete'.restoreTrashAssets( wfCatalog, assetIds,
								wfCatSynchronizer )
						end )
						:call( WFRxRunUntilDone )
				}
			end,

			--------------------------------------------------
			-- deleteTrashAssets
			--------------------------------------------------
			deleteTrashAssets = function( _, assetIds )
				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
						:flatMap( function( wfCatalog, pixelProvider, _, wfCatSynchronizer )
							return require 'WFCatDelete'.deleteTrashAssets( wfCatalog, assetIds,
								wfCatSynchronizer, pixelProvider, wfCatalog:getDeletionBacklog() )
						end )
						:call( WFRxRunUntilDone )
				}
			end,

			--------------------------------------------------
			-- trashAssetList
			--------------------------------------------------
			trashAssetList = function( _ )

				return require 'WFCatDelete'.trashAssetListModel( wfCatalogStream )

			end,

			--------------------------------------------------
			-- trashAssetCount
			--------------------------------------------------
			trashAssetCount = function( _ )

				return require 'WFCatDelete'.trashAssetCountModel( wfCatalogStream )

			end,

			--------------------------------------------------
			-- trashAssetsTotalFileSize
			--------------------------------------------------
			trashAssetsTotalFileSize = function( _ )

				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
						:flatMap( function( wfCatalog )
							return require 'WFCatTrash'.trashAssetsTotalFileSize( wfCatalog )
						end )
				}

			end,

			--------------------------------------------------
			-- notificationUnreadCount
			--------------------------------------------------
			notificationUnreadCount = function( _ )

				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
						:flatMap( function( _, _, _, wfCatSynchronizer )
							return wfCatSynchronizer._metadataSyncClient:notificationUnreadCount()
						end )
				}
			end,

			--------------------------------------------------
			-- notificationList
			--------------------------------------------------
			notificationList = function( _ )

				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
							:flatMap( function( wfCatalog, _, _, wfCatSynchronizer )
								return wfCatSynchronizer._metadataSyncClient:notificationList()
								:flatMap( function( notificationList )

									-- use set to elimiate the duplicate ids
									local assetAndAlbumIdSet = {}
									for _, item in ipairs( notificationList ) do
										if item.asset then
											assetAndAlbumIdSet[ item.asset.id ] = true
										end

										if item.album then
											assetAndAlbumIdSet[ item.album.id ] = true
										end

										-- NOTE [immisch 2020-11-24]: person doesn't use localIds

										for _, targetName in ipairs{ 'primaryImageUI', 'secondaryImageUI' } do
											local listOfIdTables = item[ item[ targetName ] ]

											if listOfIdTables then
												for _, idTable in ipairs( listOfIdTables ) do
													if type( idTable ) == 'table' and idTable.id then
														assetAndAlbumIdSet[ idTable.id ] = true
													end
												end
											end
										end
									end

									local assetAndAlbumIds = KSTableUtils.extractKeys( assetAndAlbumIdSet )

									return wfCatalog:docStore():bulkDocIdToLocalIdAsMap( assetAndAlbumIds )
										:map( function( assetIdToLocalIdAsMap )
											for _, item in ipairs( notificationList ) do
												if item.asset then
													item.asset.localId = assetIdToLocalIdAsMap[ item.asset.id ]
												end

												if item.album then
													item.album.localId = assetIdToLocalIdAsMap[ item.album.id ]
												end

												for _, targetName in ipairs{ 'primaryImageUI', 'secondaryImageUI' } do
													local listOfIdTables = item[ item[ targetName ] ]

													if listOfIdTables then
														for _, idTable in ipairs( listOfIdTables ) do
															if type( idTable ) == 'table' and idTable.id then
																idTable.localId = assetIdToLocalIdAsMap[ idTable.id ]
															end
														end
													end
												end
											end

											return notificationList
										end )
									end )
								end )
				}

			end,


			--------------------------------------------------
			-- setReadNotification
			--------------------------------------------------
			setReadNotification = function( _, notificationMarkRead, optNotificationMarkUnread )

				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
						:flatMap( function( _, _, _, wfCatSynchronizer )
							return wfCatSynchronizer:isSyncRunningStream()
								:take( 1 )
								:filter( function( isSyncRunning )
									return isSyncRunning
								end )
								:flatMap( function()
									return wfCatSynchronizer._metadataSyncClient:setReadNotification( notificationMarkRead, optNotificationMarkUnread )
								end )
						end )
				}

			end,

			--------------------------------------------------
			-- setVisitedNotification
			--------------------------------------------------
			setVisitedNotification = function( _, notificationMarkVisited, optNotificationMarkUnvisited )

				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
						:flatMap( function( _, _, _, wfCatSynchronizer )
							return wfCatSynchronizer:isSyncRunningStream()
								:take( 1 )
								:filter( function( isSyncRunning )
									return isSyncRunning
								end )
								:flatMap( function()
									return wfCatSynchronizer._metadataSyncClient:setVisitedNotification( notificationMarkVisited, optNotificationMarkUnvisited )
								end )
						end )
				}

			end,

			--------------------------------------------------
			-- notificationKillSwitch
			--------------------------------------------------
			notificationKillSwitch = function( _ )

				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
						:flatMap( function( _, _, _, wfCatSynchronizer )
							return wfCatSynchronizer._metadataSyncClient:notificationKillSwitch()
						end )
				}
			end,

			--------------------------------------------------
			-- performUndo
			--------------------------------------------------
			performUndo = function( _, undoToken )

				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
						:take( 1 )
						:flatMap( function()
							return require 'WFCatUndo'.performUndo( undoToken )
						end )
				}

			end,

			--------------------------------------------------
			-- allStacks
			--------------------------------------------------
			allStacks = function( _ )

				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
						:take( 1 )
						:flatMap( function( wfCatalog )
							return require 'WFCatStacks'.allStacks( wfCatalog )
						end )
				}

			end,

			--------------------------------------------------
			-- allCollaborators
			--------------------------------------------------
			allCollaborators = function( _ )

				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
						:flatMap( function( wfCatalog )
							return require 'WFCatCollaborator'.getAllCollaborators( wfCatalog )
						end )
				}

			end,

			--------------------------------------------------
			-- createStack
			--------------------------------------------------
			createStack = function( _, assetIds, maybeAlbumId )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
						:take( 1 )
						:flatMap( function( wfCatalog )
							return require 'WFCatStacks'.createStack( wfCatalog, assetIds, maybeAlbumId )
								:map( function( _, localStackId )
									return localStackId
								end )
						end )
						:call( WFRxRunUntilDone )

				}
			end,

			--------------------------------------------------------------------------------
			-- ungroupStack (fully delete the stack)
			--------------------------------------------------------------------------------
			ungroupStack = function( _, stackId )
				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
						:take( 1 )
						:flatMap( function( wfCatalog )
							return require 'WFCatStacks'.deleteStack( wfCatalog, stackId )
						end )
						:call( WFRxRunUntilDone )
				}
			end,

			--------------------------------------------------------------------------------
			-- addAssetsToStack
			--------------------------------------------------------------------------------
			addAssetsToStack = function( _, stackId, imageIds )
				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
						:take( 1 )
						:flatMap( function( wfCatalog )
							return require 'WFCatStacks'.addAssetsToStack( wfCatalog, stackId, imageIds )
						end )
						:call( WFRxRunUntilDone )
				}
			end,

			--------------------------------------------------------------------------------
			-- removeAssetsFromStack
			--------------------------------------------------------------------------------
			removeAssetsFromStack = function( _, stackId, imageIds )
				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
						:take( 1 )
						:flatMap( function( wfCatalog )
							return require 'WFCatStacks'.removeAssetsFromStack( wfCatalog, stackId, imageIds )
						end )
						:call( WFRxRunUntilDone )
				}
			end,

			--------------------------------------------------------------------------------
			-- setStackLeader
			--------------------------------------------------------------------------------
			setStackLeader = function( _, stackId, imageId )
				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
						:take( 1 )
						:flatMap( function( wfCatalog )
							return require 'WFCatStacks'.setCover( wfCatalog, stackId, imageId )
						end )
						:call( WFRxRunUntilDone )
				}
			end,

			--------------------------------------------------
			-- createCopiesOfAssets
			--------------------------------------------------
			createCopiesOfAssets = function( _, assetIds, maybeAlbumIdArray, maybeStackIdsByAssetIds )
				local WFCatCopies = require 'WFCatCopies'
				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
						:flatMap( function( wfCatalog )
							return WFCatCopies.createCopiesOfAssets( wfCatalog, assetIds, maybeAlbumIdArray, maybeStackIdsByAssetIds )
							:flatMap( function( theAssetIds )
								return wfCatalog:docStore():bulkDocIdToLocalIdAsMap( theAssetIds )
								:map( KSTableUtils.extractValues )
							end )
						end )
						:call( WFRxRunUntilDone )
				}

			end,

			--------------------------------------------------
			-- enableOfflineUseForAlbums
			--------------------------------------------------
			enableOfflineUseForAlbums = function( _, albumIds )

				return require 'NmbOfflineAssets'.offlineAlbumsModel( wfCatalogStream, albumIds, true )

			end,

			--------------------------------------------------
			-- disableOfflineUseForAlbums
			--------------------------------------------------
			disableOfflineUseForAlbums = function( _, albumIds )

				return require 'NmbOfflineAssets'.offlineAlbumsModel( wfCatalogStream, albumIds, false )

			end,

			--------------------------------------------------
			-- ozACRVersion
			--------------------------------------------------

			ozACRVersion = function()

				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
						:take( 1 )
						:flatMap( function( wfCatalog )
							local urlTail = "/v2/version?level=full&indent=1"
							local ozUser = wfCatalog:ozUser()
							local urlHead = ozUser:serverBaseUrl()
							local url = string.format( "%s/%s", urlHead, urlTail )
							return ozUser:get( url )
							:map( function( result, headers )
								if headers and tostring( headers.status_code ) == "200" then
									local resultTable = KSJsonDecoder.decodeWebSafeJson( result )
									return resultTable
								end

								return "--"
							end )
						end )
				}

			end,
			
			--------------------------------------------------
			-- ozAssetsExistCheck
			--------------------------------------------------

			ozAssetsExistCheck = function()

				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
						:take( 1 )
						:flatMap( function( wfCatalog )
							local catalogId = wfCatalog:ozCatalogId()
							local queryParams = {
								subtype = "image;video",
								limit = "1"
							}
							local query = WFURI.buildQueryString( queryParams )
							local url = string.format( "/v2/catalogs/%s/assets?%s", catalogId, query )
							return wfCatalog:ozCatalogSpec():get( url, { returnHttpErrorsAsData = true } )
							:map( function( result, headers )
								if headers and tostring( headers.status_code ) == "200" then
									local success, resultTable = pcall( KSJsonDecoder.decodeWebSafeJson, result )
									
									if success and resultTable then
										local maybeAsset = derefOpt( resultTable, "resources", 1 )
										if maybeAsset then
											return true
										end
										return false
									end
								end
								
								return "--"
								
							end )
						end )
				}

			end,

			--------------------------------------------------
			-- allRenderableLocalAssetIds
			--------------------------------------------------
			allRenderableLocalAssetIds =
				makeSimpleSubmodelForValidWFCatalog(
					WFCatAssets.allRenderableLocalAssetIdsStream,
					wfCatalogStream ),

			--------------------------------------------------
			-- facetsValueOnlineSearch
			--------------------------------------------------
			facetsValueOnlineSearch = function( _, onlineSearchTerms )

				return require 'WFCatSearchAndSort'.getFacetValues( wfCatalogStream, onlineSearchTerms )

			end,

			--------------------------------------------------
			-- categoryKeys
			--------------------------------------------------
			categoryKeys = function( _, onlineSearchTerms )

				return require 'WFCatSearchSortOnlineHandler'.categoryKeys( wfCatalogStream, onlineSearchTerms )

			end,

			-- autoCompleteSearch
			--------------------------------------------------
			autoCompleteSearch = function( _, onlineSearchTerms )

				return require 'WFCatSearchSortOnlineHandler'.getAutoCompleteValues( wfCatalogStream, onlineSearchTerms )

			end,

			--------------------------------------------------
			-- basicSearchCount
			--------------------------------------------------
			basicSearchCount = function( _, searchDescriptor )

				return require 'WFCatSearch'.makeCountModel( wfCatalogStream, searchDescriptor )

			end,
			
			--------------------------------------------------
			-- basicSearchExistence
			--------------------------------------------------
			basicSearchExistence = function( _, searchDescriptor )

				return require 'WFCatSearch'.makeExistsModel( wfCatalogStream, searchDescriptor )

			end,

			--------------------------------------------------
			-- queryTerms
			--------------------------------------------------
			queryTerms = function( _, termCategory, searchDescription )

				return require 'WFCatSearch'.makeQueryTermsModel( wfCatalogStream, termCategory, searchDescription )

			end,

			--------------------------------------------------
			-- queryTermCategories
			--------------------------------------------------
			queryTermCategories = function()

				return require 'WFCatSearch'.makeQueryTermCategoriesModel( wfCatalogStream )

			end,

			--------------------------------------------------
			-- collectKeywordDetailsOfRequiredAssets
			--------------------------------------------------
			collectKeywordDetailsOfRequiredAssets =
				makeSimpleSubmodelWithPriorityAndDefaultValue(
					wfCatalogStream, 'low', require
					'WFCatAsset'.collectKeywordDetailsOfRequiredAssets, {} ),

			--------------------------------------------------
			-- collectLCKeywordDetailsOfAllAssets
			--------------------------------------------------
			collectLCKeywordDetailsOfAllAssets =
				makeSimpleSubmodelWithPriorityAndDefaultValue(
					wfCatalogStream, 'low', require
					'WFCatAsset'.collectLCKeywordDetailsOfAllAssets, {} ),

			--------------------------------------------------
			-- collectKeywordDetailsOfAllAssets
			--------------------------------------------------
			collectKeywordDetailsOfAllAssets =
				makeSimpleSubmodelWithPriorityAndDefaultValue(
					wfCatalogStream, 'low', require
					'WFCatAsset'.collectKeywordDetailsOfAllAssets, {} ),

			--------------------------------------------------
			-- collectCameraDetailsOfRequiredAssets
			--------------------------------------------------
			collectCameraDetailsOfRequiredAssets =
				makeSimpleSubmodelWithPriorityAndDefaultValue(
					wfCatalogStream, 'low', require
					'WFCatAsset'.collectCameraDetailsOfRequiredAssets, {} ),

			--------------------------------------------------
			-- collectCameraDetailsOfAllAssets
			--------------------------------------------------
			collectCameraDetailsOfAllAssets =
				makeSimpleSubmodelWithPriorityAndDefaultValue(
					wfCatalogStream, 'low', require
					'WFCatAsset'.collectCameraDetailsOfAllAssets, {} ),


			--------------------------------------------------
			-- collectLocationDetailsOfRequiredAssets
			--------------------------------------------------
			collectLocationDetailsOfRequiredAssets =
				makeSimpleSubmodelWithPriorityAndDefaultValue(
					wfCatalogStream, 'low', require
					'WFCatAsset'.collectLocationDetailsOfRequiredAssets, {} ),

			--------------------------------------------------
			-- collectLocationDetailsOfAllAssets
			--------------------------------------------------
			collectLocationDetailsOfAllAssets =
				makeSimpleSubmodelWithPriorityAndDefaultValue(
					wfCatalogStream, 'low', require
					'WFCatAsset'.collectLocationDetailsOfAllAssets, {} ),

			--------------------------------------------------
			-- collectSyncStatusDetailsOfRequiredAssets
			--------------------------------------------------
			collectSyncStatusDetailsOfRequiredAssets =
				makeSimpleSubmodelWithPriorityAndDefaultValue(
					wfCatalogStream, 'low', require
					'WFCatAsset'.collectSyncStatusDetailsOfRequiredAssets, {} ),

			--------------------------------------------------
			-- collectSyncStatusDetailsOfAllAssets
			--------------------------------------------------
			collectSyncStatusDetailsOfAllAssets =
				makeSimpleSubmodelWithPriorityAndDefaultValue(
					wfCatalogStream, 'low', require
					'WFCatAsset'.collectSyncStatusDetailsOfAllAssets, {} ),

			--------------------------------------------------
			-- collectAssetTypeDetailsOfRequiredAssets
			--------------------------------------------------
			collectAssetTypeDetailsOfRequiredAssets =
				makeSimpleSubmodelWithPriorityAndDefaultValue(
					wfCatalogStream, 'low', require
					'WFCatAsset'.collectAssetTypeDetailsOfRequiredAssets, {} ),

			--------------------------------------------------
			-- collectAssetTypeDetailsOfAllAssets
			--------------------------------------------------
			collectAssetTypeDetailsOfAllAssets =
				makeSimpleSubmodelWithPriorityAndDefaultValue(
					wfCatalogStream, 'low', require
					'WFCatAsset'.collectAssetTypeDetailsOfAllAssets, {} ),

			--------------------------------------------------
			-- collectExtensionDetailsOfRequiredAssets
			--------------------------------------------------
			collectExtensionDetailsOfRequiredAssets =
				makeSimpleSubmodelWithPriorityAndDefaultValue(
					wfCatalogStream, 'low', require
					'WFCatAsset'.collectExtensionDetailsOfRequiredAssets, {} ),
			
			--------------------------------------------------
			-- collectExtensionDetailsOfAllAssets
			--------------------------------------------------
			collectExtensionDetailsOfAllAssets =
				makeSimpleSubmodelWithPriorityAndDefaultValue(
					wfCatalogStream, 'low', require
					'WFCatAsset'.collectExtensionDetailsOfAllAssets, {} ),
			
			--------------------------------------------------
			-- collectUniqueImportTimeOnAssets
			--------------------------------------------------
			collectUniqueImportTimeAndCountOfAssets =
				makeSimpleSubmodelWithDefaultValue(
					wfCatalogStream,
					require 'WFCatAsset'.collectSortedUniqueImportTimeAndCountOfAssets,
					{} ),

			--------------------------------------------------
			-- collectUniqueCaptureDatesOfAssets
			--------------------------------------------------
			collectUniqueCaptureDatesAndCountOfAssets =
				makeSimpleSubmodelWithDefaultValue(
					wfCatalogStream,
					require 'WFCatAsset'.collectSortedUniqueCaptureDatesAndCountOfAssets,
					{} ),

			--------------------------------------------------
			-- collectInfoForAssetsFromIndexStore
			--------------------------------------------------
			collectInfoForAssetsFromIndexStore =
				makeSimpleSubmodelWithDefaultValue(
					wfCatalogStream,
					require 'WFCatAsset'.collectInfoForAssetsFromIndexStore,
					{} ),

			--------------------------------------------------
			-- collectInfoForAssetsFromDocStore
			--------------------------------------------------
			collectInfoForAssetsFromDocStore =
				makeSimpleSubmodelWithDefaultValue(
					wfCatalogStream,
					require 'WFCatAsset'.collectInfoForAssetsFromDocStore,
					{} ),

			--------------------------------------------------
			-- collectPathsForAssetFromDocStore
			--------------------------------------------------
			collectPathsForAssetFromDocStore =
				makeSimpleSubmodelWithDefaultValue(
					wfCatalogStream,
					require 'WFCatAsset'.collectPathsForAssetFromDocStore,
					{} ),

			--------------------------------------------------
			-- collectCaptureDateTimeRangeForAssets
			--------------------------------------------------
			collectCaptureDateTimeRangeForAssets =
				makeSimpleSubmodelWithDefaultValue(
					wfCatalogStream,
					require 'WFCatAsset'.collectCaptureDateTimeRangeForAssets,
					{} ),

			--------------------------------------------------
			-- collectImportedByUserOfAllAssets
			--------------------------------------------------
			collectImportedByUserOfAllAssets =
				makeSimpleSubmodelWithPriorityAndDefaultValue(
					wfCatalogStream, 'low', require
					'WFCatSearchByImportedByUser'.collectImportedByUserOfAllAssets, {} ),


			-- collectImportedByUserOfRequiredAssets
			--------------------------------------------------
			collectImportedByUserOfRequiredAssets =
				makeSimpleSubmodelWithPriorityAndDefaultValue(
					wfCatalogStream, 'low', require
					'WFCatSearchByImportedByUser'.collectImportedByUserOfRequiredAssets, {} ),

			--------------------------------------------------
			-- setRatings
			--------------------------------------------------
			setRatings = function( _, imageIds, newRating, optUndoStateToken )

				return {

					value = wfCatalogStream
								:take( 1 )
								:flatMapLatest(
									function( wfCatalog )

										return require 'WFCatAsset'.setRatings( wfCatalog, imageIds, newRating, optUndoStateToken )

									end )

				}

			end,

			--------------------------------------------------
			-- setFlags
			--------------------------------------------------
			setFlags = function( _, imageIds, newFlag, optUndoToken )

				return {

					value = wfCatalogStream
								:take( 1 )
								:flatMapLatest(
									function( wfCatalog )

										return require 'WFCatAsset'.setFlags( wfCatalog, imageIds, newFlag, nil, optUndoToken )

									end )

				}
			end,

			--------------------------------------------------
			-- toggleFlags
			--------------------------------------------------
			toggleFlags = function( _, imageIds, flag, optUntoToken )

				return {

					value = wfCatalogStream
								:take( 1 )
								:flatMapLatest(
									function( wfCatalog )

										return require 'WFCatAsset'.toggleFlags( wfCatalog, imageIds, flag, nil, optUntoToken )

									end )

				}
			end,

			--------------------------------------------------
			-- adjustFlags
			--------------------------------------------------
			adjustFlags = function( _, imageIds, direction, optUntoToken )

				return {

					value = wfCatalogStream
								:take( 1 )
								:flatMapLatest(
									function( wfCatalog )

										return require 'WFCatAsset'.adjustFlags( wfCatalog, imageIds, direction, nil, optUntoToken )

									end )

				}
			end,

			--------------------------------------------------
			-- postUserFeedback
			--------------------------------------------------
			postUserFeedback = function( _, assetId, autoSavedVersionId, rating, feature, categories, comments, feedbackId, deviceInfo, modelInfo )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFUserFeedback'.postUserFeedback, assetId, autoSavedVersionId, rating, feature, categories, comments, feedbackId, deviceInfo, modelInfo )

			end,

			--------------------------------------------------
			-- rotateOrFlipImages
			--------------------------------------------------
			rotateOrFlipImages = function( _, imageIds, rotation, flip, optUndoStateToken )

				return {

					value = wfCatalogStream
								:take( 1 )
								:flatMapLatest(
									function( wfCatalog )

										return require 'WFCatAsset'.rotateOrFlipImages( wfCatalog, imageIds, rotation, flip, optUndoStateToken )

									end )

				}

			end,

			--------------------------------------------------
			-- setKeywords
			--------------------------------------------------
			setKeywords = function( _, imageIds, keywords, delete, optUndoStateToken )

				return {

					value = wfCatalogStream
								:take( 1 )
								:flatMapLatest(
									function( wfCatalog )

										return require 'WFCatAsset'.setKeywords( wfCatalog, imageIds, keywords, delete, optUndoStateToken )

									end )

				}
			end,

			--------------------------------------------------
			-- reorderAssetsInAlbum
			--------------------------------------------------
			reorderAssetsInAlbum = function( _, albumId, anchorAssetId, assetIdsToReorder, insertWhere, maybeAllAssetIds, optUndoStateToken )

				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:take( 1 )
								:flatMapLatest(
									function( wfCatalog )

										return require 'WFCatAlbums'.reorderAssetsInAlbum( wfCatalog, albumId, anchorAssetId, assetIdsToReorder, insertWhere, maybeAllAssetIds, optUndoStateToken )

									end )

				}
			end,

			-------------------------------------------------
			-- forceReorderAssetsInAlbum
			--------------------------------------------------
			forceReorderAssetsInAlbum = function( _, albumId, anchorAssetId, assetIdsToReorder, insertWhere, maybeAllAssetIds, optUndoStateToken )

				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:take( 1 )
								:flatMapLatest(
									function( wfCatalog )

										return require 'WFCatAlbums'.forceReorderAssetsInAlbum( wfCatalog, albumId, anchorAssetId, assetIdsToReorder, insertWhere, maybeAllAssetIds, optUndoStateToken )

									end )

				}
			end,

			--------------------------------------------------
			-- copyMetadataAndSetEnhanceDetailsForImages
			--------------------------------------------------
			copyMetadataAndSetEnhanceDetailsForImages = function( _, imageIdMap )

				return {

					value = wfCatalogStream
								:take( 1 )
								:flatMapLatest(
									function( wfCatalog )

										return require 'WFCatAsset'.copyMetadataAndSetEnhanceDetailsForImages( wfCatalog, imageIdMap )

									end )

				}
			end,

			--------------------------------------------------
			-- editCaptureDate
			--------------------------------------------------
			editCaptureDate = function( _, imageIds, timeDelta, newDate, optUndoStateToken )

				return {

					value = wfCatalogStream
						:take( 1 )
						:flatMapLatest(
							function( wfCatalog )

								return require 'WFCatAsset'.editCaptureDate( wfCatalog, imageIds, timeDelta, newDate, optUndoStateToken )

							end )

				}
			end,

			--------------------------------------------------
			-- albums
			--------------------------------------------------
			albums = function()

				return require 'NmbAlbums'.makeAlbumsModel( wfCatalogStream )

			end,

			--------------------------------------------------
			-- upload priority
			--------------------------------------------------
			flagUploadAsHighPriority = function( _, assetId, isTimeCritical )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatPhotoImportance'.increaseUploadPriorityForAsset, assetId, isTimeCritical )

			end,

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

			removeHighPriorityUploadFlag = function( _, assetId )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatPhotoImportance'.removeUploadPriorityModifier, assetId )

			end,

			--------------------------------------------------
			-- shared-with-you albums
			--------------------------------------------------
			sharedWithYouAlbums = function()

				return require 'WFCatSharedWithYouAlbums'.makeModel( wfCatalogStream )

			end,
			
			--------------------------------------------------
			-- canEditAsset
			--------------------------------------------------
			canEditAsset = function( _, assetId )
			
				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap( function( wfCatalog )
									return wfCatalog:docStore():docValue( assetId )
								:map( function( docValue )
									return require 'WFCatSharedWithYouUtils'.canEditAsset( docValue )
								end )
							end )
				}

			end,
			
			

			--------------------------------------------------
			-- Oz Maintenance
			--------------------------------------------------

			getOzMaintenanceNotificationStream = function()

				return {

					value = wfCatSynchronizerStream:filter( isNonNil )

						:take( 1 )

						:flatMap( function( wfCatSynchronizer )

							return wfCatSynchronizer:getOzMaintenanceNotificationStream()

						end )

				}

			end,

			--------------------------------------------------
			-- syncDiagnostics
			--------------------------------------------------

			--[[! This model that can be used gather real-time sync diagnostic data.

				Example response structure:

					{

						metadataSyncDiagnostics = {

							catalogPullerSyncDiagnostics = { < diagnostic data > },

							catalogPusherSyncDiagnostics = { < diagnostic data > },

							spaceFeedsManagerSyncDiagnostics = {

								spacePusherSyncDiagnostics = { < diagnostic data > },

								feedOfFeedsPullerSyncDiagnostics = { < diagnostic data > },

								spacePullersSyncDiagnostics = {

									{ < space 1 diagnostic data > },

									{ < space 2 diagnostic data > },

									...
								}

							}

						},

						binaryUploaderSyncDiagnostics = { < diagnostic data > },

						idleDownloaderSyncDiagnostics = { < diagnostic data > }

					}

					For more information on the diagnostic data, please see the syncDiagnostics
					functions with the associated sync classes.

			--]]

			syncDiagnostics = function( _, optCalm )

				return {

					value = wfCatSynchronizerStream:filter( isNonNil )

						:take( 1 )

						:flatMap( function( wfCatSynchronizer )

							return wfCatSynchronizer:syncDiagnostics( optCalm )

						end )

				}

			end,

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

			-- Get a stream that returns asset document IDs that the system believes are in
			-- a stuck sync state due to non-uploaded metadata.

			getDocsStuckSyncingMetadataStream = function( _, optCalm, optStuckReportingTimeThreshold )

				return {

					value = Observable.combineLatest( wfCatalogStream:filter( isNonNil ), wfCatSynchronizerStream:filter( isNonNil ) )
								:take( 1 )
								:flatMap( function( wfCatalog, wfCatSynchronizer )

										local isNormalSyncRunningStream = wfCatSynchronizer:isNormalSyncRunningStream()

										return require 'WFCatStuckSyncUtils'.getDocsStuckSyncingMetadataStream(
												wfCatalog, isNormalSyncRunningStream, optCalm, optStuckReportingTimeThreshold )

								end )
				}

			end,

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

			-- Get a stream that fires with SQLite error codes (integer) when a doc store or
			-- index 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

			getSQLiteFatalErrorCodeStream = function( _ )

				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
						:flatMap( function( _, _, _, wfCatSynchronizer )
							return wfCatSynchronizer:getSQLiteFatalErrorCodeStream()
						end )
				}

			end,
			
			--------------------------------------------------
			-- Versions
			--------------------------------------------------

			createVersionForAsset = function( _, assetId, createdManually, versionName, returnVersionInfo )

				local WFCatVersions = require 'WFCatVersions'

				local result = getNonNilWFCatalogStream( wfCatalogStream )

					:flatMap( function( wfCatalog )

						return WFCatVersions.createVersionForAsset( wfCatalog, assetId, createdManually, versionName )

							:flatMap( function( maybeVersionId )

								if returnVersionInfo and maybeVersionId then
									return WFCatVersions.getVersionInfo( wfCatalog, maybeVersionId )
								else
									return Observable.just( maybeVersionId )
								end

							end )

					end )

				return {
					value = result
				}

			end,

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

			createVersionForAssetWithSettings = function( _, assetId, createdManually, versionName, developSettings )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatVersions'.createVersionForAssetWithSettings,
								assetId, createdManually, versionName, developSettings )

			end,

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

			createVersionForAssetWithExternalSettings = function( _, assetId, createdManually, versionName, developSettings )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatVersions'.createVersionForAssetWithExternalSettings,
								assetId, createdManually, versionName, developSettings )

			end,

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

			duplicateVersion = function( _, sourceVersionId, createdManually, versionName )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatVersions'.duplicateVersion, sourceVersionId, createdManually, versionName )

			end,

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

			getAutoVersionCountForAsset = function( _, assetId )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatVersions'.getAutoVersionCountForAsset, assetId )

			end,

			--------------------------------------------------------------------------------
			
			checkManualVersionForAssets = function( _, assetDocIds )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatVersions'.assetsHaveManualVersions, assetDocIds )
				
			end,
			
			--------------------------------------------------------------------------------

			getAllVersionsStreamForAsset= function( _, assetId, includeFakeImportStateVersion )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatVersions'.getAllVersionsStreamForAsset, assetId, includeFakeImportStateVersion )

			end,

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

			getManualVersionsStreamForAsset= function( _, assetId, includeFakeImportStateVersion )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatVersions'.getManualVersionsStreamForAsset, assetId, includeFakeImportStateVersion )

			end,

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

			getAutoVersionsStreamForAsset= function( _, assetId, includeFakeImportStateVersion )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatVersions'.getAutoVersionsStreamForAsset, assetId, includeFakeImportStateVersion )

			end,

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

			renameVersion = function( _, versionId, versionName )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatVersions'.renameVersion, versionId, versionName )

			end,

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

			getVersionsAnalytics = function()

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatVersions'.getVersionsAnalytics )

			end,

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

			deleteVersion = function( _, versionId )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatVersions'.deleteVersion, versionId )

			end,

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

			deleteAllVersionsForAsset = function( _, assetId )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatVersions'.deleteAllVersionsForAsset, assetId )

			end,

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

			deleteManualVersionsForAsset = function( _, assetId )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatVersions'.deleteManualVersionsForAsset, assetId )

			end,

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

			deleteAutoVersionsForAsset = function( _, assetId )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatVersions'.deleteAutoVersionsForAsset, assetId )

			end,

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

			compareCurrentSettings = function( _, docId, developSettings )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatVersions'.compareCurrentSettings, docId, developSettings )

			end,

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

			downloadExternalizedVersionXmp = function( _, versionId )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatVersions'.downloadExternalizedXmpForVersion, versionId,
								WFCatDevelopSettings.saveExternalizedXmp )

			end,

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

			maybeDeleteTempAutoVersion = function( _, assetId, withinSeconds )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatVersions'.maybeDeleteTempAutoVersion, assetId, withinSeconds )

			end,

			--------------------------------------------------
			-- EditDefaults
			--------------------------------------------------

			getEditDefaultsEnabled = function()

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatEditDefaults'.getEditDefaultsEnabled )

			end,

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

			setEditDefaultsEnabled = function(_, enabled)

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatEditDefaults'.setEditDefaultsEnabled, enabled )

			end,

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

			getDefaultImportPreset = function()

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatEditDefaults'.getDefaultImportPreset )

			end,

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

			setDefaultImportPreset = function( _, presetName, presetDigest )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatEditDefaults'.setDefaultImportPreset, presetName, presetDigest )

			end,

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

			getEditDefaultsEnabledWithImportPreset = function()

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatEditDefaults'.getEditDefaultsEnabledWithImportPreset )


			end,

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

			getPresetForCameraModel = function( _, modelName )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatEditDefaults'.getPresetForCameraModel, modelName )

			end,

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

			setPresetForCameraModel = function( _, modelName, presetName, presetDigest )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatEditDefaults'.setPresetForCameraModel, modelName, presetName, presetDigest )

			end,

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

			removePresetForCameraModel = function(_, modelName)

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatEditDefaults'.removePresetForCameraModel, modelName )

			end,

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

			listPresetsByCameraModel = function()

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
						require 'WFCatEditDefaults'.listPresetsByCameraModel )

			end,

			--------------------------------------------------
			-- getPeopleForImageIds
			--------------------------------------------------
			getPeopleForImageIds = function( _, assetIds )

				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
    							:flatMap( function( wfCatalog )

										return require 'WFCatFaces'.getPeopleForImageIds( wfCatalog, assetIds )

									end
								)
				}
			end,

			--------------------------------------------------
			-- getDistinctPeopleForImageIds
			--------------------------------------------------
			getDistinctPeopleForImageIds = function( _, assetIds )

				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
    							:flatMapLatest( function( wfCatalog )

										return require 'WFCatFaces'.getDistinctPeopleForImageIds( wfCatalog, assetIds )

									end
								)
				}

			end,

			--------------------------------------------------
			-- getDistinctPeopleForAllPhotos
			--------------------------------------------------
			getDistinctPeopleForAllPhotos = function( _ )

				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
    							:flatMapLatest( function( wfCatalog )

										return require 'WFCatFaces'.getDistinctPeopleForAllPhotos( wfCatalog )

									end
								)
				}

			end,

			--------------------------------------------------
			-- getPeopleInfoForAllAssets
			--------------------------------------------------
			getPeopleInfoForAllAssets = function( _, minimumImagesCount, dontIncludeHidden, namedOnly )

				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
    							:flatMap( function( wfCatalog )

										return require 'WFCatFaces'.getPeopleInfoForAllAssets( wfCatalog, minimumImagesCount, dontIncludeHidden, namedOnly )

									end
								)
				}

			end,

			--------------------------------------------------
			-- getPhotoCountforPersonIds
			--------------------------------------------------
			getPhotoCountforPersonIds = function( _, personIds )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap(
									function( wfCatalog )

										return require 'WFCatFaces'.getPhotoCountforPersonIds( wfCatalog, personIds )

									end
								)
				}
			end,

			--------------------------------------------------
			-- addPerson
			--------------------------------------------------
			addPerson = function( _, person )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap(
									function( wfCatalog )

										return require 'WFCatPerson'.addPerson( wfCatalog, person )

									end
								)
				}
			end,

			--------------------------------------------------
			-- updatePersonName
			--------------------------------------------------
			updatePersonName = function( _, personId, personName )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap(
									function( wfCatalog )

										return require 'WFCatPerson'.updatePersonName( wfCatalog, personId, personName )

									end
								)
				}
			end,

			--------------------------------------------------
			-- updatePeopleName
			--------------------------------------------------
			updatePeopleName = function( _, peopleIdNameList )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap(
									function( wfCatalog )

										return require 'WFCatPerson'.updatePeopleName( wfCatalog, peopleIdNameList )

									end
								)
				}
			end,

			--------------------------------------------------
			-- deletePerson
			--------------------------------------------------
			deletePerson = function( _, personIds )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap(
									function( wfCatalog )

										return require 'WFCatPerson'.deletePerson( wfCatalog, personIds )

									end
								)
				}
			end,

			--------------------------------------------------
			-- hidePerson
			--------------------------------------------------
			hidePerson = function( _, personId, hide )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap(
									function( wfCatalog )

										return require 'WFCatPerson'.hidePerson( wfCatalog, personId, hide )

									end
								)
				}
			end,

			--------------------------------------------------
			-- hidePeople
			--------------------------------------------------
			hidePeople = function( _, peopleHideSet )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap(
									function( wfCatalog )

										return require 'WFCatPerson'.hidePeople( wfCatalog, peopleHideSet )

									end
								)
				}
			end,

			--------------------------------------------------
			-- downloadPersonThumbnail
			--------------------------------------------------
			downloadPersonThumbnail = function( _, personId )
				return {

					value = Observable.combineLatest( wfCatalogStream:filter( isNonNil ), wfCatSynchronizerStream:filter( isNonNil ) )
								:take( 1 )
								:flatMap( function( wfCatalog, wfCatSynchronizer )

										return require 'WFCatPerson'.downloadPersonThumbnail( wfCatalog, wfCatSynchronizer, personId )

									end
								)
				}
			end,

			--------------------------------------------------
			-- downloadPeopleThumbnails
			--------------------------------------------------
			downloadPeopleThumbnails = function( _, personIds )
				return {

					value = Observable.combineLatest( wfCatalogStream:filter( isNonNil ), wfCatSynchronizerStream:filter( isNonNil ) )
								:take( 1 )
								:flatMap( function( wfCatalog, wfCatSynchronizer )

										return require 'WFCatPerson'.downloadPeopleThumbnails( wfCatalog, wfCatSynchronizer, personIds )

									end
								)
				}
			end,

			--------------------------------------------------
			-- shallowMergePeople
			--------------------------------------------------
			shallowMergePeople = function( _, parentId, personIds, personName )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap(
									function( wfCatalog )

										return require 'WFCatPerson'.shallowMergePeople( wfCatalog, parentId, personIds, personName )

									end
								)
				}
			end,

			--------------------------------------------------
			-- splitPeople
			--------------------------------------------------
			splitPeople = function( _, peopleInfoList )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap(
									function( wfCatalog )

										return require 'WFCatPerson'.splitPeople( wfCatalog, peopleInfoList )

									end
								)
				}
			end,

			--------------------------------------------------
			-- rejectSimilarPerson
			--------------------------------------------------
			rejectSimilarPerson = function( _, personId, rejectedPersonId, undo )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap(
									function( wfCatalog )

										return require 'WFCatPerson'.rejectSimilarPerson( wfCatalog, personId, rejectedPersonId, undo )

									end
								)
				}
			end,

			--------------------------------------------------
			-- rejectSimilarPeople
			--------------------------------------------------
			rejectSimilarPeople = function( _, personId, rejectedPersonIds, undo )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap(
									function( wfCatalog )

										return require 'WFCatPerson'.rejectSimilarPeople( wfCatalog, personId, rejectedPersonIds, undo )

									end
								)
				}
			end,

			--------------------------------------------------
			-- getSimilarPeopleForPersonId
			--------------------------------------------------
			getSimilarPeopleForPersonId = function( _, personId )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap(
									function( wfCatalog )

										return require 'WFCatPerson'.getSimilarPeopleForPersonId( wfCatalog, personId )

									end
								)
				}
			end,

			--------------------------------------------------
			-- setPersonCover
			--------------------------------------------------
			setPersonCover = function( _, personId, assetId )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap(
									function( wfCatalog )

										return require 'WFCatPerson'.setPersonCover( wfCatalog, personId, assetId )

									end
								)
				}
			end,

			--------------------------------------------------
			-- setPersonCoverReturnFaceInfo
			--------------------------------------------------
			setPersonCoverReturnFaceInfo = function( _, personIds, assetId )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap(
									function( wfCatalog )

										return require 'WFCatPerson'.setPersonCoverReturnFaceInfo( wfCatalog, personIds, assetId )

									end
								)
				}
			end,

			--------------------------------------------------
			-- setPersonCoverReturnFaceInFoExcept
			--------------------------------------------------
			setPersonCoverReturnFaceInfoExcept = function( _, personIds, assetIds )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap(
									function( wfCatalog )

										return require 'WFCatPerson'.setPersonCoverReturnFaceInfoExcept( wfCatalog, personIds, assetIds )

									end
								)
				}
			end,

			--------------------------------------------------
			-- removePeopleFromAssets
			--------------------------------------------------
			removePeopleFromAssets = function( _, personIds, assetIds )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap(
									function( wfCatalog )

										return require 'WFCatFaces'.removePeopleFromAssets( wfCatalog, personIds, assetIds )

									end )
									:call( WFRxRunUntilDone )
				}
			end,

			--------------------------------------------------
			-- removePersonFromTagsPanel
			--------------------------------------------------
			removePersonFromTagsPanel = function( _, ozFaceIdsAssetIdsPersonIds )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap(
									function( wfCatalog )

										return require 'WFCatFaces'.removePersonFromTagsPanel( wfCatalog, ozFaceIdsAssetIdsPersonIds )

									end )
									:call( WFRxRunUntilDone )
				}
			end,

			--------------------------------------------------
			-- moveAssetsToAnotherPerson
			--------------------------------------------------
			moveAssetsToAnotherPerson = function( _, personIdTo, personIds, assetIds )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap(
									function( wfCatalog )

										return require 'WFCatFaces'.moveAssetsToAnotherPerson( wfCatalog, personIdTo, personIds, assetIds )

									end )
									:call( WFRxRunUntilDone )
				}
			end,

			--------------------------------------------------
			-- moveAssetsToAnotherPersonTable
			--------------------------------------------------
			moveAssetsToAnotherPersonTable = function( _, personIdTo, ozFaceIdsAssetIdsPersonIds )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap(
									function( wfCatalog )

										return require 'WFCatFaces'.moveAssetsToAnotherPersonTable( wfCatalog, personIdTo, ozFaceIdsAssetIdsPersonIds )

									end )
									:call( WFRxRunUntilDone )
				}
			end,

			--------------------------------------------------
			-- addFaceToAsset
			--------------------------------------------------
			addFaceToAsset = function( _, assetId, faceId, personId )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap(
									function( wfCatalog )

										return require 'WFCatFaces'.addFaceToAsset( wfCatalog, assetId, faceId, personId )

									end )
									:call( WFRxRunUntilDone )
				}
			end,

			--------------------------------------------------
			-- addFacesToAsset
			--------------------------------------------------
			addFacesToAsset = function( _, assetId, appliedToDimensions, faceDetailsArray )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap(
									function( wfCatalog )

										return require 'WFCatFaces'.addFacesToAsset( wfCatalog, assetId, appliedToDimensions, faceDetailsArray )

									end )
									:call( WFRxRunUntilDone )
				}
			end,

			--------------------------------------------------
			-- updateFaceRegion
			--------------------------------------------------
			updateFaceRegion = function( _, assetId, faceId, faceArea )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap(
									function( wfCatalog )

										return require 'WFCatFaces'.updateFaceRegion( wfCatalog, assetId, faceId, faceArea )

									end )
									:call( WFRxRunUntilDone )
				}
			end,

			--------------------------------------------------
			-- removeFaceRegionsFromAssets
			--------------------------------------------------
			removeFaceRegionsFromAssets = function( _, assetFaceMap )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMap(
									function( wfCatalog )

										return require 'WFCatFaces'.removeFaceRegionsFromAssets( wfCatalog, assetFaceMap )

									end )
									:call( WFRxRunUntilDone )
				}
			end,

			--------------------------------------------------
			-- Style favorite info
			--------------------------------------------------
			styleFavoriteInfo = function()

				return {

					value = wfCatalogStream:filter( isNonNil )
								:flatMapLatest( function( wfCatalog )

									return require "NmbStyleFavorites".favoriteInfoStream( wfCatalog )

						end )

				}
			end,

			--------------------------------------------------
			-- developContent
			--------------------------------------------------
			developContent = function( _, assetId )

				return WFCatDevelopSettings.developContentModel(
							getNonNilWFCatalogStream( wfCatalogStream ), assetId )

			end,

			--------------------------------------------------
			-- developContentsWithOriginalPriorityLocalNegativePath
			--------------------------------------------------
			developContentWithLocalOriginalOrProxyFallback = function( _, assetId )
			
					return WFCatDevelopSettings.developContentWithLocalOriginalOrProxyFallbackModel(
								getNonNilWFCatalogStream( wfCatalogStream ), assetId )

			end,
			
			--------------------------------------------------
			-- developContentsWithLocalNegativePath
			--------------------------------------------------
			developContentWithLocalNegativePath = function( _, assetId )
			
					return WFCatDevelopSettings.developContentWithLocalNegativePathModel(
								getNonNilWFCatalogStream( wfCatalogStream ), assetId )

			end,

			--------------------------------------------------
			-- ongoingDevelopContent
			--------------------------------------------------
			ongoingDevelopContent = function()

				return WFCatDevelopSettings.ongoingDevelopContentModel(
							getNonNilWFCatalogStream( wfCatalogStream ) )

			end,

			--------------------------------------------------
			-- getOtherClientEditDetails
			--------------------------------------------------
			getOtherClientEditDetails = function( _, assetId )
				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
						:flatMapLatest( function( wfCatalog )

							return require 'WFCatAsset'.ongoingGetOtherClientEditDetails( wfCatalog, assetId )

						end )
				}
			end,

			--------------------------------------------------
			-- purgeBlackout
			--------------------------------------------------
			purgeBlackout = function()

				return NmbPurgeControl.makeModel(
							getNonNilWFCatalogStream( wfCatalogStream ) )

			end,

			--------------------------------------------------
			-- Undo state management
			--------------------------------------------------

			deleteUndoState = function( _, undoStateToken )

				return {

					value = wfCatalogStream
								:take( 1 )
								:flatMapLatest( function()
									return require'WFCatUndo'.deleteUndoState( undoStateToken )
								end )
								:call( WFRxRunUntilDone )
				}

			end,

			getUndoStateCount = function()

				return {

					value = wfCatalogStream
								:take( 1 )
								:flatMapLatest(
									function()

										local count = require'WFCatUndo'.getUndoStateCount()
										return Observable.just( count )

									end )
				}

			end,

			--------------------------------------------------
			-- setMetadata
			--------------------------------------------------
			setMetadata = function( _, imageIds, formatterId, value, optUndoStateToken )

				return {

					value = wfCatalogStream
								:take( 1 )
								:flatMapLatest(
									function( wfCatalog )

										return require'WFCatFormattedMetadata'.setMetadata( wfCatalog, imageIds, formatterId, value, optUndoStateToken )

									end )

				}

			end,

			--------------------------------------------------
			-- harvestedMetadata
			--------------------------------------------------
			harvestedMetadata = function( _, imageIds, formatterIds )

				return require 'WFCatFormattedMetadata'.harvestedMetadataModel( wfCatalogStream, imageIds, formatterIds )

			end,

			--------------------------------------------------
			-- gridPixelPaths
			--------------------------------------------------
			gridPixelPaths = function( _, pixStreamId )

				return PixelProviderGridPixelPathsModel.make(
						wfCatalogStream, pixStreamId )

			end,

			--------------------------------------------------
			-- bestLoupeRenditionPaths
			--------------------------------------------------
			bestLoupeRenditionPaths = function( _, pixStreamId,
												defaultLoupeRenditionSize )

				return PixelProviderLoupePixelPathsModel.make(
						wfCatalogStream, pixStreamId,
						defaultLoupeRenditionSize )

			end,

			--------------------------------------------------
			-- getNegativePathASAP
			--------------------------------------------------
			getNegativePathASAP = function( _, pixStreamId )

				return PixelProviderDownloadNegativeModel.getNegativePathASAP(
						pixelProviderProcessorStream, pixStreamId )

			end,

			--------------------------------------------------
			-- videoLoupe
			--------------------------------------------------
			videoLoupe = function( _, pixStreamId )

				return PixelProviderVideoLoupeModel.make(
						pixelProviderProcessorStream, pixStreamId )

			end,

			--------------------------------------------------
			-- ensureLocalNegativePathForPrefetchImageIds
			--------------------------------------------------
			ensureLocalNegativePathForPrefetchImageIds= function( _, pixStreamId, preferredSize )

				return PixelProviderDownloadNegativeModel.ensureLocalNegativePathForPrefetchImageIds(
						pixelProviderProcessorStream, pixStreamId, preferredSize )

			end,

			--------------------------------------------------
			-- _DEBUG_isSyncRunning
			--------------------------------------------------
			_DEBUG_isSyncRunning = function( _ )
				return {
					value = Observable.combineLatest( wfCatalogStream:filter( isNonNil ),
														wfCatSynchronizerStream )
						:flatMapLatest( function( _, wfCatSynchronizer )
							return wfCatSynchronizer:isSyncRunningStream()
						end )
				}
			end,

			--------------------------------------------------
			-- _DEBUG_enableDetailedPixelProviderStats
			--------------------------------------------------
			_DEBUG_enableDetailedPixelProviderStats= function()

				return {

					value = pixelProviderProcessorStream
								:flatMapLatest(
									function( pixelProviderProcessor )

										if not pixelProviderProcessor then

											return Rx.Observable.empty()

										end

										return pixelProviderProcessor:runUntilDone(
												'_DEBUG_enableDetailedPixelProviderStats' )

									end )

				}

			end,

			--------------------------------------------------
			-- _DEBUG_pixelRequestStats
			--------------------------------------------------
			_DEBUG_pixelRequestStats = function()

				return {

					value = pixelProviderProcessorStream
								:flatMapLatest(
									function( pixelProviderProcessor )

										return pixelProviderProcessor
												:_DEBUG_requestStatsStream()

									end )

				}

			end,

			--------------------------------------------------
			-- _DEBUG_localAssetIdPixelProviderStates
			--------------------------------------------------
			_DEBUG_localAssetIdPixelProviderStates = function( _, localAssetId )

				return {

					value = pixelProviderProcessorStream
								:flatMapLatest(
									function( pixelProviderProcessor )

										return pixelProviderProcessor
												:_DEBUG_localAssetIdStatesStream(
													localAssetId )

									end )

				}

			end,

			--------------------------------------------------
			-- _DEBUG_tryCreateRendition
			--------------------------------------------------
			_DEBUG_tryCreateRendition = function( _, localAssetId )

				local pixStreamId = string.format( "_DEBUG_tryCreateRendition %s", localAssetId )

				return {

					value = pixelProviderProcessorStream
								:flatMapLatest(
									function( pixelProviderProcessor )

										pixelProviderProcessor
											:runUntilDone(
												"requestFullSizeRenditionForExport",
													pixStreamId,
													localAssetId )

										return pixelProviderProcessor
												:dispatch(
													"pixStreamIdPixelPathStream",
														pixStreamId )
												:map(
													function( ... )

														return { ... }

													end )

									end ),

					close = function()

						WFRxRunUntilDone(
							pixelProviderProcessorStream
								:flatMapLatest(
									function( pixelProviderProcessor )

										pixelProviderProcessor
											:runUntilDone(
												"abandonAllPixStreamRequests",
													pixStreamId )
									end ) )

					end,

				}

			end,

			--------------------------------------------------
			-- _DEBUG_getSimulateDbCorruptionDbList
			--------------------------------------------------
			_DEBUG_getSimulateDbCorruptionDbList = function()

				return {

					value = Rx.Observable.just(
								NmbDbCorruptionUtils.simulateCorruptionDbList );

				}

			end,

			--------------------------------------------------
			-- _DEBUG_simulateDbCorruption
			--------------------------------------------------
			_DEBUG_simulateDbCorruption = function( _, dbName )

				return {

					value =
						wfCatalogStream
							:take( 1 )
							:flatMapLatest(
								function( wfCatalog, _, profileCatalog, wfCatSynchronizer )

									return
										NmbDbCorruptionUtils.makeSimulateCorruptionForDbResultStream(
											wfCatalog, profileCatalog,
											wfCatSynchronizer._metadataSyncClient,
											dbName )

								end )

				}

			end,

			--------------------------------------------------
			-- _DEBUG_bulkRequestInfoStream
			--------------------------------------------------
			_DEBUG_bulkRequestInfoStream = function( _, refreshSeconds )

				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
						:flatMapLatest( function( wfCatalog )

							return wfCatalog:docStore():_DEBUG_bulkRequestInfoStream( refreshSeconds )

						end )

				}

			end,

			--------------------------------------------------
			-- _DEBUG_changesFeedInfoStream
			--------------------------------------------------
			_DEBUG_changesFeedInfoStream = function( _, refreshSeconds )

				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
						:flatMapLatest( function( wfCatalog )

							return wfCatalog:docStore():_DEBUG_changesFeedInfoStream( refreshSeconds )

						end )

				}

			end,

			--------------------------------------------------
			-- _DEBUG_changeCounterAndSizeOfIndexQueue
			--------------------------------------------------
			_DEBUG_changeCounterAndSizeOfIndexQueue = function( _, refreshSeconds )

				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
						:flatMapLatest( function( wfCatalog )

							return Observable.combineLatest(
								wfCatalog:docStore():changeCounter(),
								wfCatalog:indexStore():sizeOfIndexQueue()
							)
							:map( function( changeCounter, indexedSoFar, totalDocuments )
								return {
									changeCounter = changeCounter,
									indexedSoFar = indexedSoFar,
									totalDocuments = totalDocuments,
								}
							end )
							:calm( refreshSeconds, WFRxScheduler.sharedScheduler() )

						end )

				}

			end,

			--------------------------------------------------
			-- _DEBUG_httpActivityStream
			--------------------------------------------------
			_DEBUG_httpActivityStream = function( _, refreshSeconds )

				local WFRxHttpClient = require "WFRxHttpClient"

				return {
					value = WFRxHttpClient.DEBUG_activityStream( refreshSeconds )
				}

			end,

			--------------------------------------------------
			-- _DEBUG_stopMetadataPuller
			--
			-- ################## WARNING ####################
			-- This is for DEBUG use only and MUST be used
			-- with caution and specific purpose. Stopping the
			-- metadata puller without also stopping the
			-- binary uploader can lead to very undesirable
			-- behavior. For example a binary can be uploaded,
			-- and no confirmation is received (because we're
			-- not pulling updates) and so the binary will be
			-- uploaded again and again (with 'blackout'
			-- delays between each upload).
			-- ###############################################
			--------------------------------------------------
			_DEBUG_stopMetadataPuller = function()
				return {

					value = wfCatSynchronizerStream:filter( isNonNil )
								:flatMapLatest( function( wfCatSynchronizer )

									if wfCatSynchronizer._metadataSyncClient then
										return wfCatSynchronizer._metadataSyncClient:stopSyncInResidualMode()
											-- Stops metadata puller only.
									end

									return Observable.empty()
								end )

				}
			end,

			--------------------------------------------------
			-- _DEBUG_startMetadataPuller
			--------------------------------------------------
			_DEBUG_startMetadataPuller = function()
				return {

					value = wfCatSynchronizerStream:filter( isNonNil )
								:flatMapLatest( function( wfCatSynchronizer )

									if wfCatSynchronizer._metadataSyncClient then
										return wfCatSynchronizer._metadataSyncClient:startSyncInResidualMode()
											-- Starts metadata puller only.
										end

									return Observable.empty()
								end )

				}
			end,

			--------------------------------------------------
			-- _DEBUG_stopMetadataPusher
			--------------------------------------------------
			_DEBUG_stopMetadataPusher = function()
				return {

					value = wfCatSynchronizerStream:filter( isNonNil )
								:flatMapLatest( function( wfCatSynchronizer )

									if wfCatSynchronizer._metadataSyncClient then
										return wfCatSynchronizer._metadataSyncClient:_DEBUG_stopMetadataPusher()
									end

									return Observable.empty()

								end )

				}
			end,

			--------------------------------------------------
			-- _DEBUG_startMetadataPusher
			--------------------------------------------------
			_DEBUG_startMetadataPusher = function()
				return {

					value = wfCatSynchronizerStream:filter( isNonNil )
								:flatMapLatest( function( wfCatSynchronizer )

									if wfCatSynchronizer._metadataSyncClient then
										return wfCatSynchronizer._metadataSyncClient:_DEBUG_startMetadataPusher()
									end

									return Observable.empty()

								end )

				}
			end,

			--------------------------------------------------
			-- _DEBUG_startBinaryUploader
			--------------------------------------------------
			_DEBUG_startBinaryUploader = function()
				return {

					value = wfCatSynchronizerStream:filter( isNonNil )
								:flatMapLatest( function( wfCatSynchronizer )

									if wfCatSynchronizer._binaryUploader then
										wfCatSynchronizer._binaryUploader:startContinuousUploads()
									end

									return Observable.empty()

								end )

				}
			end,

			--------------------------------------------------
			-- _DEBUG_stopBinaryUploader
			--------------------------------------------------
			_DEBUG_stopBinaryUploader = function()
				return {

					value = wfCatSynchronizerStream:filter( isNonNil )
								:flatMapLatest( function( wfCatSynchronizer )

									if wfCatSynchronizer._binaryUploader then
										wfCatSynchronizer._binaryUploader:stopContinuousUploads()
									end

									return Observable.empty()

								end )

				}
			end,

			--------------------------------------------------
			-- _DEBUG_startIdleDownloader
			--------------------------------------------------
			_DEBUG_startIdleDownloader = function()
				return {

					value = wfCatSynchronizerStream:filter( isNonNil )
								:flatMapLatest( function( wfCatSynchronizer )

									if wfCatSynchronizer._idleDownloader then
										wfCatSynchronizer._idleDownloader:start()
									end

									return Observable.empty()

								end )

				}
			end,

			--------------------------------------------------
			-- _DEBUG_stopIdleDownloader
			--------------------------------------------------
			_DEBUG_stopIdleDownloader = function()
				return {

					value = wfCatSynchronizerStream:filter( isNonNil )
								:flatMapLatest( function( wfCatSynchronizer )

									if wfCatSynchronizer._idleDownloader then

										-- idle downloader starts delayed, don't let it start up later
										if wfCatSynchronizer._idleDownloaderDelaySubscription then
											wfCatSynchronizer._idleDownloaderDelaySubscription:dispose()
											wfCatSynchronizer._idleDownloaderDelaySubscription = nil
										end

										wfCatSynchronizer._idleDownloader:stop()
									end

									return Observable.empty()

								end )

				}
			end,

			--------------------------------------------------
			-- _DEBUG_startStyleSynchronizer
			--------------------------------------------------
			_DEBUG_startStyleSync = function()
				return {

					value = wfCatSynchronizerStream:filter( isNonNil )
								:flatMapLatest( function( wfCatSynchronizer )

									if wfCatSynchronizer._styleSynchronizer then
										wfCatSynchronizer._styleSynchronizer:start()
									end

									return Observable.empty()

								end )

				}
			end,

			--------------------------------------------------
			-- _DEBUG_stopStyleSynchronizer
			--------------------------------------------------
			_DEBUG_stopStyleSync = function()
				return {

					value = wfCatSynchronizerStream:filter( isNonNil )
								:flatMapLatest( function( wfCatSynchronizer )

									if wfCatSynchronizer._styleSynchronizer then
										wfCatSynchronizer._styleSynchronizer:stop()
									end

									return Observable.empty()

								end )

				}
			end,

			--------------------------------------------------
			-- _DEBUG_startProfileSync
			--------------------------------------------------
			_DEBUG_startProfileSync = function()
				return {

					value = wfCatSynchronizerStream:filter( isNonNil )
								:flatMapLatest( function( wfCatSynchronizer )

									if wfCatSynchronizer._profileSyncClient then
										return wfCatSynchronizer._profileSyncClient:stopSync():call( WFRxRunUntilDone )
									end

									return Observable.empty()

								end )

				}
			end,

			--------------------------------------------------
			-- _DEBUG_stopProfileSync
			--------------------------------------------------
			_DEBUG_stopProfileSync = function()
				return {

					value = wfCatSynchronizerStream:filter( isNonNil )
								:flatMapLatest( function( wfCatSynchronizer )

									if wfCatSynchronizer._metadataSyncClient then
										return wfCatSynchronizer._profileSyncClient:startSync():call( WFRxRunUntilDone )
									end

									return Observable.empty()

								end )

				}
			end,

			--------------------------------------------------
			-- _DEBUG_sendBinarySyncDiagnosticLogData
			--------------------------------------------------

			_DEBUG_sendBinarySyncDiagnosticLogData = function()
				return {
					value = wfCatSynchronizerStream:filter( isNonNil )
								:take( 1 )
								:flatMapLatest(
									function( wfCatSynchronizer )

										if wfCatSynchronizer._binaryUploader then

											return Observable.combineLatest (
													wfCatSynchronizer:isUploadingBinaries(),
													wfCatSynchronizer._binaryUploader:uploadStatusChangeStream(),
													wfCatSynchronizer._binaryUploader:binaryUploadCompletionStream() )
												:map( function()

													return wfCatSynchronizer._binaryUploader:sendDiagnosticLogData()

											end )

										end

										return Observable.empty()

								end )
				}
			end,





			--------------------------------------------------
			-- libraryPath
			--------------------------------------------------
			libraryPath = function()

				return {

					value = wfCatalogStream:filter( isNonNil )
								:take( 1 )
								:map(
									function( wfCatalog )

										local fileSpec = wfCatalog:fileSpec()

										return fileSpec:rootPath()

									end )

				}

			end,

			--------------------------------------------------
			-- catalogFolderPath
			--------------------------------------------------
			crUserPathInfo = function()

				return {

					value = wfCatalogStream:filter( isNonNil )
								:take( 1 )
								:map(
									function( wfCatalog )

										local fileSpec = wfCatalog:fileSpec()

										local catalogPath = fileSpec:catalogPath()
										local crUserPathInfo = { require "NmbCrPathOverrides".get( catalogPath ) }

										return { crUserPathInfo, catalogPath }

									end )

				}

			end,

			--------------------------------------------------
			-- pixelsPath
			--------------------------------------------------
			pixelsPath = function()

				return {
					priority = 'high',

					value = wfCatalogStream:filter( isNonNil )
								:take( 1 )
								:map(
									function( wfCatalog )

										local fileSpec = wfCatalog:fileSpec()

										return fileSpec:pixelsPath()

									end )
				}

			end,

			--------------------------------------------------
			-- assetListInfo
			--------------------------------------------------
			assetListInfo = function( _ )

				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMapLatest(
									function( wfCatalog )

										return require 'WFCatImportDuplicateFiles'.assetListInfo( wfCatalog )

									end )

				}

			end,
			
			--------------------------------------------------
			-- getAllOriginalDigests
			--------------------------------------------------
			getAllOriginalDigests = function( _ )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
					require 'WFCatImportDuplicateFiles'.getAllOriginalDigests )

			end,

			--------------------------------------------------
			-- canGetOriginal
			--------------------------------------------------
			canGetOriginal = function( _, assetId )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
					function( wfCatalog, _ )
						if not assetId then
							return false
						end
						
						-- Get docValue from assetId
						return wfCatalog:docStore():docValue( assetId )
							:map( function( docValue )
								if not docValue then
									return false
								end
								
								-- Check if original is available and valid on Oz
								return WFOzDocUtils.isValidOriginalAvailableOnOz(docValue)
							end )
					end )

			end,
			
			--------------------------------------------------
			-- findExistingFilesBySha256
			--------------------------------------------------
			findExistingFilesBySha256 = function( _, rawImageInfoArray )

				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMapLatest(
									function( wfCatalog )

										return require 'WFCatImportDuplicateFiles'.findExistingFilesBySha256( wfCatalog, rawImageInfoArray )

									end )

				}

			end,

			--------------------------------------------------
			-- findExistingFilesByOriginalDigests
			--------------------------------------------------
			findExistingFilesByOriginalDigests = function( _, originalDigestList )
			
				return WFModelsUtils.createModelForFunction( wfCatalogStream,
					require 'WFCatImportDuplicateFiles'.findExistingFilesByOriginalDigests, originalDigestList )

			end,

			--------------------------------------------------
			-- getMigratedLightroomCatalogs
			--------------------------------------------------
			getMigratedLightroomCatalogs = function( _ )

				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )
								:flatMapLatest(
									function( wfCatalog )

										return require 'WFCatImportManifestMigratedCatalogs'.getMigratedCatalogs( wfCatalog )

									end )

				}

			end,

			--------------------------------------------------
			-- _debugAssetFileInfoModel
			--------------------------------------------------
			_debugAssetFileInfoModel =
				makeSimpleSubmodelWithPriorityAndDefaultValue(
					wfCatalogStream, 'low', DebuggingStreams.makeAssetFileInfoStream, {} ),


			--------------------------------------------------
			-- _nmb5582fixupInvalidOriginals
			--------------------------------------------------
			_nmb5582fixupInvalidOriginals = function ( _ )
				return {
					value = getNonNilWFCatalogStream( wfCatalogStream )
						:flatMapLatest(
							function( wfCatalog )
								require 'WFCatInvalidOriginalHandler'.fixInvalidOriginals( wfCatalog )
								return Observable.empty()
							end )
				}
			end,

			--------------------------------------------------
			-- MapBox Access Token
			--------------------------------------------------

			getMapBoxAccessToken = function()

				return {

					value = getNonNilWFCatalogStream( wfCatalogStream )

						:flatMapLatest( function( wfCatalog )

							local catalogSpec = wfCatalog:ozCatalogSpec()

							local requestUrl = string.format( '%stokens/mapbox', catalogSpec:baseHref() )

							return catalogSpec:get( requestUrl, {
								returnHttpErrorsAsData = true
							} )

						end )

						:map( function( data, headers )

							if headers and headers.status_code == "200" then

								local decodedJson = KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty( data )

								local accessToken = decodedJson and derefOpt( decodedJson, 'access_token' )

								return accessToken

							end

							return nil

						end )

						:catch( function()
							return Observable.just()
						end )

				}

			end,


			setCodecEntitlement = function ( _, entitlement )
				return {
					value = require 'WFCodecUsage'.setEntitlement( entitlement )
				}
			end,


			codecUsage = function ( _ )
				return {
					value = require 'WFCodecUsage'.getCodecUsageStream()
				}
			end,

			--------------------------------------------------
			-- ensureProfilesExistsForCamera
			--------------------------------------------------
			ensureProfilesExistsForCamera = function( )

				return require "NmbEnsureProfileExistsModel".make( wfCatSynchronizerStream )

			end,

			--------------------------------------------------
			-- getFavoritesForAsset
			--------------------------------------------------
			getFavoritesForAsset = function( _, albumId, assetId )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
					require 'WFCatCollaborationActivity'.getFavoritesForAsset, albumId, assetId )

			end,

			--------------------------------------------------
			-- addFavorite
			--------------------------------------------------
			addFavorite = function( _, albumId, assetId )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
					require 'WFCatCollaborationActivity'.addFavorite, albumId, assetId )

			end,

			--------------------------------------------------
			-- removeFavorite
			--------------------------------------------------
			removeFavorite = function( _, albumId, assetId )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
					require 'WFCatCollaborationActivity'.removeFavorite, albumId, assetId )

			end,

			--------------------------------------------------
			-- getFavoritedAlbumsForAssetId
			--------------------------------------------------
			getFavoritedAlbumsForAssetId = function( _, assetId )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
					require 'WFCatCollaborationActivity'.getFavoritedAlbumsForAssetId, assetId )

			end,

			--------------------------------------------------
			-- getCommentsForAsset
			--------------------------------------------------
			getCommentsForAsset = function( _, albumId, assetId )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
					require 'WFCatCollaborationActivity'.getCommentsForAsset, albumId, assetId )

			end,

			--------------------------------------------------
			-- addCommentForAsset
			--------------------------------------------------
			addCommentForAsset = function( _, albumId, assetId, body )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
					require 'WFCatCollaborationActivity'.addCommentForAsset, albumId, assetId, body )

			end,

			--------------------------------------------------
			-- deleteComment
			--------------------------------------------------
			deleteComment = function( _, commentId )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
					require 'WFCatCollaborationActivity'.deleteComment, commentId )

			end,

			--------------------------------------------------
			-- getCommentedAlbumsForAssetId
			--------------------------------------------------
			getCommentedAlbumsForAssetId = function( _, assetId )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
					require 'WFCatCollaborationActivity'.getCommentedAlbumsForAssetId, assetId )

			end,

			--------------------------------------------------
			-- generateProxies
			--------------------------------------------------
			generateProxies = function( _, assetId, optArgs )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
					require 'WFCatVideo'.generateProxies, assetId, optArgs )

			end,

			--------------------------------------------------
			-- generateProxiesForSpaceAsset
			--------------------------------------------------
			generateProxiesForSpaceAsset = function( _, spaceId, assetId, optArgs )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
					require 'WFCatVideo'.generateProxiesForSpaceAsset, spaceId, assetId, optArgs )

			end,
			
			--------------------------------------------------
			-- generateSpecialRenditionForSpaceAsset
			--------------------------------------------------
			generateSpecialRenditionForSpaceAsset = function( _, spaceId, assetId, specialRendition )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
					require 'WFCatVideo'.generateSpecialRenditionForSpaceAsset, spaceId, assetId, specialRendition )

			end,
			
			--------------------------------------------------
			-- generateSpecialRenditionForSpaceAsset
			--------------------------------------------------
			requestSpecialProxyOrRenditionGenerationForVideoStream = function( _, localId, spaceId, isSharedAlbum, canEditInAlbum )

				return WFModelsUtils.createModelForFunction( wfCatalogStream,
					require 'WFCatVideo'.requestSpecialProxyOrRenditionGenerationForVideoStream, localId, spaceId, isSharedAlbum, canEditInAlbum )

			end,

			--------------------------------------------------
			-- shutdownHTTPRequests
			--------------------------------------------------
			shutdownHTTPRequests = function()

				return HttpShutdownModel.make(
							pixelProviderProcessorStream )

			end,

		},

		-- use events to send "global" commands to anything that's created using
		-- the wfCatalogStream
		events = {

			startRenderBlackout = makeForwardEventToPixelProviderProccessor(
									"startRenderBlackout" ),

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

			endRenderBlackout = makeForwardEventToPixelProviderProccessor(
									"endRenderBlackout" ),

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

			setPreferRenderForNewRenditions =
				makeForwardEventToPixelProviderProccessor(
					"setPreferRenderForNewRenditions" ),

		},

		close = function()

			logger:info( "Closing" )

		end,

	}

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

	-- SPAM [matthewj 2018-08-02]: Formalize this builder, probably in DesktopModelUtils
	local modelBuilder = {

		addCatalogBasedMostRecentValueModel = function( subModelName, fn )
			nmbModelDefinition.mostRecentValueModels[ subModelName ] = makeSimpleSubmodelForValidWFCatalog( fn, wfCatalogStream )
		end,

		addMostRecentValueModelWithDefaultValue = function( subModelName, fn, ... )
			nmbModelDefinition.mostRecentValueModels[ subModelName ] = makeSimpleSubmodelWithDefaultValue( fn, wfCatalogStream, ... )
		end
	}

	NmbConnectors.addConnectorModels( modelBuilder )

	modelBuilder.addCatalogBasedMostRecentValueModel( 'dumpSyncingDocsToPath',
		WFRxDocStoreUtils.dumpSyncingDocsToPath )

	modelBuilder.addCatalogBasedMostRecentValueModel( 'onlineBackup',
		function( wfCatalog, mcatPath, wfIndexPath )

			return Observable.combineLatest( function( mcatProgress, wfIndexProgress )
					return mcatProgress / 2 + wfIndexProgress / 2
				end,

				wfCatalog:docStore():onlineBackup( mcatPath ),
				wfCatalog:indexStore():onlineBackup( wfIndexPath ) )

		end )

	return DesktopModelUtils.makeWFModelDefinition( nmbModelDefinition )

end

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

return NmbRootModel
