mpneuried
11/21/2013 - 10:16 AM

Backbone collection extension to solve the problem of getting a model with eventually not exists within a collection. It also handles paral

Backbone collection extension to solve the problem of getting a model with eventually not exists within a collection. It also handles parallel fetches and distributes a single .fetch() to multiple requesters.

###
EXAMPLE USAGE
	
	testColl = new Backbone.Collection.Extended()

	# PROMISE USAGE

	# define handlers
	fnSuccess = ->
		console.log( "SUCCESS", arguments )
		return
	fnError = ->
		console.log( "ERROR", arguments )
		return
	
	# get model
	testColl.getOrFetchId( 13 ).then( fnSuccess, fnError )
	# force a fetch
	testColl.getOrFetchId( 42, true ).then( fnSuccess, fnError )
	
	# CALLBACK USAGE

	# get model
	testColl.getOrFetchId( 13, ( (err, model)->
		console.log( "CALLBACK 42 A", arguments )
	) )
	# force a fetch
	testColl.getOrFetchId( 42, true ( (err, model)->
		console.log( "CALLBACK 42 B", arguments )
	) )

###



class Backbone.Collection.Extended extends Backbone.Collection
	###
	## getOrFetchId
	
	`collection.getOrFetchId( id, force, cb )`
	
	Get a single model by id an fetch it from the server if it not exists within the collection.
	It also handles parallel calles of a id and distribute a single fetch to multiple requesters
	
	@param { String|Number } id The id to get 
	@param { Boolean } force Force a fetch
	@param { Function } cb Callback function 
	
	@return { Promise } Instead of the callback you can use a promise syntax 
	
	@api public
	###
	getOrFetchId: ( id, force, cb )=>
		# initialize variable to store running calls
		@_runningGetOrFetchIds or= {}

		# fix the arguments
		if _.isFunction( force )
			cb = force
			force = false

		# general success handler
		fnSuccess = ( model )=>
			# add the model to the collection if not exists
			@add( model, silent: true ) if not @get( id )
			# answer the callback if needed
			cb( null, model ) if cb?
			# resolve the current open promise and clear it
			if @_runningGetOrFetchIds[ id ]?
				@_runningGetOrFetchIds[ id ].resolve( model )
				@_runningGetOrFetchIds[ id ] = null
			return

		# general error handler
		fnError = ( model, resp )=>
			# answer the callback if needed
			cb( resp or model ) if cb?
			# reject the current open promise and clear it
			if @_runningGetOrFetchIds[ id ]?
				@_runningGetOrFetchIds[ id ].reject( resp )
				@_runningGetOrFetchIds[ id ] = null
			return

		# check if a fetch for this requested id is allready running
		if @_runningGetOrFetchIds[ id ]?
			# reuse the running promise and answer the requester with the currently running promise
			if cb?
				@_runningGetOrFetchIds[ id ].then( fnSuccess, fnError )
			return @_runningGetOrFetchIds[ id ]

		# create a new promise
		deferred = @_runningGetOrFetchIds[ id ] = $.Deferred()
		# check if model allready exists
		_model = @get( id )
		# on force = true or missing model fetch the model
		if force or not _model?
			# create a new model to fetch
			_data = {}
			_data[ @model.prototype.idAttribute ] = id
			_model = new @model( _data )
			# run fetch and pass the results to the general handlers
			_model.fetch( success: fnSuccess, error: fnError )
		else
			# resolve the promise immediately id the model exists
			deferred = @_runningGetOrFetchIds[ id ] = $.Deferred()
			fnSuccess( _model )
		
		return deferred.promise()