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

WFCatSynchronizer.lua

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

ADOBE CONFIDENTIAL
------------------
Copyright 2016 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.

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

	-- RxLua
local Rx = require 'Rx'
local Observable = Rx.Observable

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

	-- WF.oz_client.managed_catalog
local WFCatBinaryUploader = require 'WFCatBinaryUploader'
local WFCatConflictResolver = require 'WFCatConflictResolver'
local WFCatIdleDownloader = require 'WFCatIdleDownloader'
local WFCatOzMaintenanceTracker = require 'WFCatOzMaintenanceTracker'
local WFCatPersonBinaryDownloader = require 'WFCatPersonBinaryDownloader'
local WFCatStaleBinaryRemover = require 'WFCatStaleBinaryRemover'
local WFCatStorage = require 'WFCatStorage'
local WFCatStuckSyncUtils = require 'WFCatStuckSyncUtils'
local WFCatStyleSynchronizer = require 'WFCatStyleSynchronizer'
local WFCatSyncAutoDeleter = require 'WFCatSyncAutoDeleter'
local WFCatSyncClient = require 'WFCatSyncClient'

	-- WF.core.utils
local KSIdGenerator = require 'KSIdGenerator'
local KSPrefs = require 'KSPrefs'
local KSTableUtils = require 'KSTableUtils'
local WFArguments = require 'WFArguments'
local WFCloseSupport = require 'WFCloseSupport'
local WFConfiguration = require 'WFConfiguration'
local WFCapabilities = require "WFCapabilities"

	-- Lightroom CC
local NmbConnectors = require 'NmbConnectors'
local NmbErrorHandler = require 'NmbErrorHandler'
local NmbOfflineAssetsTask = require 'NmbOfflineAssetsTask'
local NmbFileManagementSettings = require 'NmbFileManagementSettings'

--------------------------------------------------------------------------------
	
local debug_disableSync = WFConfiguration.Sync.disable or false

local check = assert( WFArguments.check )

local runUntilDone = WFRxRunUntilDone

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

local WFCatSynchronizerMethods = {

	_wfCatalog = false,
		-- (WFCatalog)

	_pixelProviderProcessor = false,
		-- (PixelProviderProcessor)

	_metadataSyncClient = false,
		-- (WFCatSyncClient)
		
	_ozStorageMonitor = false,
		-- (WFCatStorage)

	_conflictResolver = false,
		-- (WFCatConflictResolver)
		
	_autoDeleter = false,
		-- (WFCatSyncAutoDeleter)
		
	_profileSyncClient = false,
		-- (WFCatSyncClient)

	_idleDownloader = false,
		-- (WFCatIdleDownloader)

	_staleBinaryRemover = false,
		-- (WFCatStaleBinaryRemover)

	_binaryUploader = false,
		-- (WFCatBinaryUploader)
	
	_styleSynchronizer = false,
		-- (WFCatStyleSynchronizer)
		
	_personThumbnailDownloader = false,
		-- (WFCatPersonBinaryDownloader)
		
	_nmbOfflineAssetsTask = false,
		-- (NmbOfflineAssetsTask)
		
	_errorHandler = false,
		-- (WFCatErrorHandler)
		
	_isSyncRunning = false,
		-- (boolean)
		
	_residualMode = false,
		-- (boolean)
		
	_freemiumMode = false,
		-- (boolean)

	_restrictedCountryMode = false,
		-- (boolean)

	_catalogValidationStream = false,
		-- (Observable<boolean> denoting Oz/local catalog validity for syncing)
		
	_idleDownloaderDelaySubscription = false,
		-- (Rx Timer Subscription)
		
	_isSyncRunningStream = false,
		-- (BehaviorSubject) A stream that fires true when sync is running, false otherwise.
		-- This fires true even when syncing in residual mode.
	
	_isNormalSyncRunningStream = false,
		-- (BehaviorSubject) A stream that fires true when sync is running, false otherwise.
		-- Note that this only fires true when sync is running it's normal mode; not in
		-- residual mode, when paused, or when there's no Oz catalog.
	
	_checkOzForCatalogUpdatesSubscription = false,
		-- (Disposable) A subscription that monitors for binary upload completions and triggers
		-- the puller to check for Oz catalog updates.
	
	_ozMaintenanceNotificationStream = false,
		-- (Rx Subject) A stream that fires with Oz outage/maintenance updates for both the
		-- users own account and all shared-with-you spaces.
		
	_ozMaintenanceSyncHandlerSubscription = false,
		-- (Disposable) A subscription that monitors _ozMaintenanceNotificationStream and 
		-- starts/stops sync activities accordingly.
		
	_accountIsUnderMaintenance = false,
		-- (boolean) Keeps track of whether or not the user's Oz account is under maintenance,
		-- or if Oz is down.
		
	_docsStuckSyncingMetadataSubscription = false,
		-- (Rx Disposable) A subscription that monitors and reports metadata related stuck syncs.
		
	_docsStuckUploadingOriginalSubscription = false,
		-- (Rx Disposable) A subscription that monitors and reports uploader related stuck syncs.
		
	WFCloseSupport.addCloseSupportMethods,

}

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

local function buildDownloadBinaryStream( pixelProviderProcessor, idlePixStreamIds )

	local pixStreamIdsLut = KSTableUtils.invert( idlePixStreamIds )

	return pixelProviderProcessor
			:pixStreamIdsPixelPathStream( pixStreamIdsLut )
			:tap( 

				function( pixStreamId, localAssetId )

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

				end )

end

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

local function buildDownloadBinaryFunc( pixelProviderProcessor, idlePixStreamIds )

	return function( localAssetId, binaryType, binarySize, isMarkedForOffline, hasLocalOriginal )

		local pixStreamId = idlePixStreamIds[ binaryType ]

		pixelProviderProcessor
			:runUntilDone( 
				"requestBinaryWhenIdle", 
					pixStreamId, localAssetId, binaryType, binarySize,
					isMarkedForOffline, hasLocalOriginal )
		
	end

end

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

local function buildBatchCancelDownloadsFunc( pixelProviderProcessor, idlePixStreamIds )

	return function( binaryType, localAssetIds )

		local pixStreamId = idlePixStreamIds[ binaryType ]

		pixelProviderProcessor
			:runUntilDone( 
				"batchRemoveRequests", pixStreamId, localAssetIds )
		
	end

end

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

local function validateLocalAndOzCatalogMatch( self )
	
	self._wfCatalog:docStore():localDocValue( 'Oz-nextUpdateFeedLink' )
		:map( function( maybeNextFeedLinkTbl )
			local isValid = true
			local ozCatalogId = self._wfCatalog:ozCatalogId()
			
			if ozCatalogId and type( maybeNextFeedLinkTbl ) == 'table'  then
				if maybeNextFeedLinkTbl.ozCatalogId then
					isValid = maybeNextFeedLinkTbl.ozCatalogId == ozCatalogId
				elseif maybeNextFeedLinkTbl.link then
					isValid = not not string.find( maybeNextFeedLinkTbl.link, ozCatalogId )
				end
			end
			
			self._catalogValidationStream:onNext( isValid )
			
			return isValid
		end )
		:call( runUntilDone )

end

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

function WFCatSynchronizerMethods:__init( wfCatalog, profileCatalog, pixelProviderProcessor, 
	workerThreadAgrs, syncEnabled, residualMode, freemiumMode, locale )

	check( 'WFCatSynchronizer', 'WFCatalog', '?WFCatalog', 'PixelProviderProcessor', 
			'table', '?boolean', '?boolean', '?boolean', '?string' )

	self._wfCatalog = wfCatalog
	self._pixelProviderProcessor = pixelProviderProcessor
	
	self._conflictResolver = WFCatConflictResolver( wfCatalog:docStore(), wfCatalog:indexStore() )
	self._ozStorageMonitor = WFCatStorage( wfCatalog )
	
	self._metadataSyncClient = WFCatSyncClient( wfCatalog, { locale = locale } )
	
	self._styleSynchronizer = WFCatStyleSynchronizer( wfCatalog, profileCatalog, workerThreadAgrs )
	
	self._personThumbnailDownloader = WFCatPersonBinaryDownloader(wfCatalog)
	
	self._catalogValidationStream = Rx.ReplaySubject( 1 )
	validateLocalAndOzCatalogMatch( self )

	-- monitor quota
	self._ozStorageMonitor:isOverStorage()
		:distinctUntilChanged()
		:flatMap( function( isOverStorage )
			return self._metadataSyncClient:setUserIsOverQuota( isOverStorage )
		end )
		:call( runUntilDone )
	
	-- monitor expiration
	wfCatalog:ozUser():isSubscriptionExpired()
		:distinctUntilChanged()
		:flatMap( function( isExpired )
			return self._metadataSyncClient:setUserIsExpired( isExpired )
		end )
		:call( runUntilDone )
	
	self._autoDeleter = WFCatSyncAutoDeleter( wfCatalog )
	
	if profileCatalog then					  
		self._profileSyncClient = WFCatSyncClient( profileCatalog, {
																	metadataPullerArgs = {
																	 	progressiveBackoff = false,
																		noWorkDelay = 60 * 60 * 24, -- 1 day
																		errorDelay = 60 * 60, -- 1 hour
																		maxDelay = 60 * 60 * 24, -- 1 day 
																	},
																	isSyncClientForProfileCatalog = true
																} )
		self._profileSyncClient:addCloseAction( function()
			profileCatalog:close()
		end )
	end
	
	local idlePixStreamBaseId = string.format( "%s:%s", 'WFCatSynchronizer', KSIdGenerator.uuidInLowercaseHex() )
	
	local idlePixStreamIds = {}
	idlePixStreamIds[ 'original' ] = string.format( "%s:%s", idlePixStreamBaseId, 'original' )
	idlePixStreamIds[ 'proxy' ] = string.format( "%s:%s", idlePixStreamBaseId, 'proxy' )
	idlePixStreamIds[ 'xmp_develop' ] = string.format( "%s:%s", idlePixStreamBaseId, 'xmp_develop' )
	
	local idleDownloadStream = buildDownloadBinaryStream( pixelProviderProcessor, idlePixStreamIds )
	local idleDownloadFunc = buildDownloadBinaryFunc( pixelProviderProcessor, idlePixStreamIds )
	local batchCancelDownloadsFunc = buildBatchCancelDownloadsFunc( pixelProviderProcessor, idlePixStreamIds ) 
	
	self._idleDownloader = WFCatIdleDownloader( 
			wfCatalog, idleDownloadFunc, idleDownloadStream, batchCancelDownloadsFunc,
			NmbFileManagementSettings.keepAllOriginals(),
			NmbFileManagementSettings.keepAllProxies() )
	
	local optArgs = {
		useDirectUploadIfPossible = WFCapabilities.getCapability( "WFCatBinaryUploader.enableDirectUpload" ) ~= "false",
		enableVideoProxies = WFCapabilities.getCapability( "WFCatBinaryUploader.enableVideoProxies" ) == "true"
	}
	
	self._binaryUploader = WFCatBinaryUploader( wfCatalog, nil, nil, nil, optArgs )

	-- WF-357: Reset Catalog MetadataPuller polling frequency after uploading a binary.
	
	self._checkOzForCatalogUpdatesSubscription = self._binaryUploader:binaryUploadCompletionStream()
		:flatMap( function()
			return self._metadataSyncClient:checkOzForUpdates()
		end )
		:subscribe()

	self._staleBinaryRemover = WFCatStaleBinaryRemover( wfCatalog, self._idleDownloader )

	self._nmbOfflineAssetsTask = NmbOfflineAssetsTask( wfCatalog )

	self._errorHandler = NmbErrorHandler( wfCatalog )
	
	self._residualMode = residualMode
	
	self._freemiumMode = freemiumMode
	
	self._isSyncRunningStream = Rx.BehaviorSubject( syncEnabled )
	
	self._isNormalSyncRunningStream = Rx.BehaviorSubject( false )

	local catalogOzMaintenanceNotificationStream 
			= WFCatOzMaintenanceTracker.getOzMaintenanceNotificationStream( wfCatalog )
	
	local spaceFeedsMaintenanceNotificationStream
			= self._metadataSyncClient:spaceFeedsMaintenanceNotificationStream()
	
	self:addCloseAction( function()

		catalogOzMaintenanceNotificationStream:dispose()

	end )
	
	self._ozMaintenanceNotificationStream = 
			catalogOzMaintenanceNotificationStream
				:merge( spaceFeedsMaintenanceNotificationStream )
				:replay( nil, 1 )
				:refCount()
				
	self._ozMaintenanceSyncHandlerSubscription = self._ozMaintenanceNotificationStream
	
		:subscribe( function( ozMaintenanceInfo )
		
			local catalogOrSpaceId = ozMaintenanceInfo and ozMaintenanceInfo.id
			local accountIsUnderMaintenance = ozMaintenanceInfo and ozMaintenanceInfo.isUnderMaintenance
			
			if catalogOrSpaceId == 'catalog' then
			
				if accountIsUnderMaintenance then
				
					self._accountIsUnderMaintenance = true
					self:stopSyncing()
					
				else
				
					self._accountIsUnderMaintenance = false
					self:startSyncing()

				end
				
			end
		
		end )
	
	-- Stuck Sync Analytics
	
	local kStuckSyncAnalyticsInterval = 180 -- seconds

	self._docsStuckSyncingMetadataSubscription = 
			WFCatStuckSyncUtils.getDocsStuckSyncingMetadataStream( wfCatalog, self._isNormalSyncRunningStream )
			
				:calm( kStuckSyncAnalyticsInterval, WFRxScheduler.sharedScheduler() )
				
				:flatMapLatest( function( stuckDocIds )

					return WFCatStuckSyncUtils.resolveStuckSync( wfCatalog, stuckDocIds )

				end )

				:subscribe()
					
	self._docsStuckUploadingOriginalSubscription = 
			WFCatStuckSyncUtils.getDocsStuckUploadingOriginalStream( wfCatalog, self._isNormalSyncRunningStream )
			
				:calm( kStuckSyncAnalyticsInterval, WFRxScheduler.sharedScheduler() )
			
				:subscribe( function( stuckDocIds )
					
					WFCatStuckSyncUtils.trackStuckBinaryDocuments( stuckDocIds )
					
				end )
	
	-- check for the latest connectors available 
	Observable.timer( 10, WFRxScheduler.sharedScheduler() )
		:flatMap( function()
			return NmbConnectors.updateAvailableConnectorsCache( wfCatalog )
		end )
		:call( runUntilDone )
	
	if syncEnabled then
		
		-- delay the start of the idle downloader a bit so that we're not overaggressively pulling binaries at the 
		-- start of sync when other things are likely more important to get through the sync process
		-- The purge process is setup to delay for 15 seconds on startup; it's good to have the idle downloader
		-- delay slightly longer than purge so purge can prevent the idle downloader from running if needed.
		
		self:startSyncing( 20 )
		
	end

end

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

function WFCatSynchronizerMethods:close()

	check( 'WFCatSynchronizer' )

	self:stopSyncing()
	
	if self._profileSyncClient then 
		self._profileSyncClient:close()
	end
	
	if self._catalogValidationStream then
		self._catalogValidationStream:dispose()
	end
	
	if self._isSyncRunningStream then
		self._isSyncRunningStream:dispose()
	end
	
	if self._isNormalSyncRunningStream then
		self._isNormalSyncRunningStream:dispose()
	end
	
	if self._checkOzForCatalogUpdatesSubscription then
		self._checkOzForCatalogUpdatesSubscription:dispose()
	end
	
	if self._ozMaintenanceSyncHandlerSubscription then
		self._ozMaintenanceSyncHandlerSubscription:dispose()
	end
	
	if self._docsStuckSyncingMetadataSubscription then
		self._docsStuckSyncingMetadataSubscription:dispose()
	end
	
	if self._docsStuckUploadingOriginalSubscription then
		self._docsStuckUploadingOriginalSubscription:dispose()
	end
	
	self._autoDeleter:close()
	
	self._metadataSyncClient:close()
	self._ozStorageMonitor:close()
	self._idleDownloader:close()
	self._styleSynchronizer:close()
	self._personThumbnailDownloader:close()
	self._binaryUploader:close()
	self._errorHandler:close()
	self._staleBinaryRemover:close()
	self._conflictResolver:close()
	self._nmbOfflineAssetsTask:close()
	
end
		
--------------------------------------------------------------------------------

function WFCatSynchronizerMethods:startSyncing( maybeIdleDownloaderDelay )
	
	if WFCloseSupport.hasBeenOrWillBeClosed( self._wfCatalog ) or 
			not self:shouldSyncBeRunning() then
		-- ignore start request if catalog is closed/closing or
		-- if sync is paused/Oz is down.
		return
	end
	
	local ozUserId = self._wfCatalog:ozUserId()
	
	if not ozUserId:match( '^0+$' ) and not debug_disableSync and not self._restrictedCountryMode then
		
		if self._isSyncRunning then
			return
		end
		
		self._catalogValidationStream
			:take( 1 )
			:map( function( isValid )
			
				if not isValid or self._freemiumMode then	
					-- catalog is not valid, don't allow syncing
					self:stopSyncing()
			
				else
				
					-- catalog is fine, start up sync
					self._isSyncRunning = true
					self._isSyncRunningStream:onNext( true )
							
					if not self._residualMode then
						self._isNormalSyncRunningStream:onNext( true )	
					else
						self._isNormalSyncRunningStream:onNext( false )	
					end
							
					if self._profileSyncClient then
						self._profileSyncClient:startSync():call( runUntilDone )
					end
					
					if self._residualMode then
						self._metadataSyncClient:startSyncInResidualMode():call( runUntilDone )
					else
						self._metadataSyncClient:startSync():call( runUntilDone )
					end
					
					self._idleDownloaderDelaySubscription =
						Observable.timer( maybeIdleDownloaderDelay or 0.1, WFRxScheduler.sharedScheduler() )
							:map( function()
								self._idleDownloader:start()
							end )
							:subscribe()
							
					self._styleSynchronizer:start()
			
					if self._residualMode then
						self._binaryUploader:startContinuousUploadOfOnlySpaceAssets()
					else
						self._binaryUploader:startContinuousUploads()
					end
	
				end
			end )
			:call( runUntilDone )
	end

end

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

function WFCatSynchronizerMethods:stopSyncing()

	if self._profileSyncClient then 
		self._profileSyncClient:stopSync():call( runUntilDone )
	end
	
	if self._residualMode then
		self._metadataSyncClient:stopSyncInResidualMode():call( runUntilDone )
	else
		self._metadataSyncClient:stopSync():call( runUntilDone )
	end
	
	if self._idleDownloaderDelaySubscription then
		self._idleDownloaderDelaySubscription:dispose()
		self._idleDownloaderDelaySubscription = nil
	end
	
	self._idleDownloader:stop()
	
	self._styleSynchronizer:stop()
	
	self._binaryUploader:stopContinuousUploads()
	
	self._isSyncRunning = false
	self._isSyncRunningStream:onNext( false )
	self._isNormalSyncRunningStream:onNext( false )
	
end

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

function WFCatSynchronizerMethods:enableResidualMode()
	
	self._freemiumMode = false
	self._residualMode = true

end

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

function WFCatSynchronizerMethods:disableResidualMode()
	
	self._residualMode = false

end

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

function WFCatSynchronizerMethods:enableFreemiumMode()
	
	self._residualMode = false
	self._freemiumMode = true

end

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

function WFCatSynchronizerMethods:disableFreemiumMode()
	
	self._freemiumMode = false

end

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

function WFCatSynchronizerMethods:enableRestrictedCountryMode()
	
	self._restrictedCountryMode = true

end

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

function WFCatSynchronizerMethods:catalogValidationStream()

	return self._catalogValidationStream

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

function WFCatSynchronizerMethods:isSyncRunningNow()
	
	return self._isSyncRunning

end

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

function WFCatSynchronizerMethods:isSyncRunningStream()
	
	return self._isSyncRunningStream

end

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

function WFCatSynchronizerMethods:isNormalSyncRunningStream()
	
	return self._isNormalSyncRunningStream

end

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

function WFCatSynchronizerMethods:shouldSyncBeRunning()
	
	return not KSPrefs.syncPaused and not self._accountIsUnderMaintenance
		-- KSPrefs.syncPaused always contains the correct value for whether or
		-- not sync should be enabled.
		
end

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

function WFCatSynchronizerMethods:isUploadingBinaries()
	return self._binaryUploader:isDoingWork()
end

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

function WFCatSynchronizerMethods:isDownloadingRequiredOriginalsOrProxies()
	return self._idleDownloader:isDownloadingRequiredBinary()
end

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

function WFCatSynchronizerMethods:isUploadingMetadata()
	return self._metadataSyncClient:isUploadingMetadata()
end

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

function WFCatSynchronizerMethods:isDownloadingMetadata()
	return self._metadataSyncClient:isDownloadingMetadata()
end

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

function WFCatSynchronizerMethods:isDownloadingStyles()
	return self._styleSynchronizer:isDownloadingStyles()
end

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

function WFCatSynchronizerMethods:getLatestDownloadedAssetIdsStream()
	return self._metadataSyncClient:getLatestDownloadedAssetIdsStream()
end

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

function WFCatSynchronizerMethods:isOzCatalogDeleted()
	return self._metadataSyncClient:isOzCatalogDeleted()
end

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

function WFCatSynchronizerMethods:getIdleDownloader()
	return self._idleDownloader
end

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

function WFCatSynchronizerMethods:getBinaryUploader()
	return self._binaryUploader
end

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

function WFCatSynchronizerMethods:getStyleSynchronizer()
	return self._styleSynchronizer
end

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

function WFCatSynchronizerMethods:getPersonThumbnailDownloader()
	return self._personThumbnailDownloader
end

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

function WFCatSynchronizerMethods:getRequiredUpdateStream()

	-- Listen on different threads, all of which make requests to Oz, for a required update error.

	return Rx.Observable.combineLatest( function( maybeRequiresUpdate1, maybeRequiresUpdate2, maybeRequiresUpdate3 )
			return maybeRequiresUpdate1 or maybeRequiresUpdate2 or maybeRequiresUpdate3
		end,
		self._metadataSyncClient:getRequiredUpdateStream(),
		self._profileSyncClient:getRequiredUpdateStream(),
		self._wfCatalog:ozUser():getRequiredUpdateStream() )
		
end

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

function WFCatSynchronizerMethods:getOzMaintenanceNotificationStream()

	return self._ozMaintenanceNotificationStream
		
end

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

--[[!
	\brief This API provides an estimation -- in seconds -- for the time remaining
		to upload any outstanding binaries. -1 is returned in the event there is
		nothing to upload.

	\return Observable< number > An Observable that fires with numbers representing
		the time remaining in seconds.
--]]

function WFCatSynchronizerMethods:getEstimatedTimeRemainingForUploads()

	return self._binaryUploader:getEstimatedTimeRemaining()
		
end

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

--[[!
	\brief This API that can be used gather real-time sync diagnostic data.

	\param optCalm (optional number) The maximum frequency at which the stream will
		fire with updates.

	\return Observable< table > An Observable whose value tracks a table of
		sync diagnostic data.
--]]

function WFCatSynchronizerMethods:syncDiagnostics( optCalm )

	check( 'WFCatSynchronizer', '?number' )

	return Observable.combineLatest(
			self._metadataSyncClient:syncDiagnostics( optCalm ),
			self._binaryUploader:syncDiagnostics( optCalm ),
			self._idleDownloader:syncDiagnostics( optCalm )
		)
		
		:map( function( metadataSyncDiagnostics, 
						binaryUploaderSyncDiagnostics, 
						idleDownloaderSyncDiagnostics )
		
			return {
			
				metadataSyncDiagnostics = metadataSyncDiagnostics,
				binaryUploaderSyncDiagnostics = binaryUploaderSyncDiagnostics,
				idleDownloaderSyncDiagnostics = idleDownloaderSyncDiagnostics
				
			}
		
		end )
		
end

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

return Rx.Class( 'WFCatSynchronizer', WFCatSynchronizerMethods )
