- There's a module that knows how to load (and re-load) data through an API, and stores it locally.
- Multiple modules access the loaded data, and transform them into a different representation (each module does a different transform), also storing them locally
- The transformed data is exposed to the view, with a 'loading' indication when the original data is (re)loaded and/or the transformation is in progress.
##API idea
Promises support:
- on resolution callbacks, where the transform function can be attached.
- a
isResolved
attribute, which is false when the promise has been started but not yet resolved. - propagate fully, so a chain can be built
However this support is fire-once. Once a promise is resolved, it is resolved forever. The reloading use-case is not supported.
Would be great to have a promise-like object that:
- its completion can be reset or re-triggered, which will invoke all the
.then
methods again - its start can also be re-triggered, which will set the
isResolved
attribute
This can also be implemented with signals:
- provides a
.loaded(successCallback, failureCallback, finalyCallback)
function that allows to register callbacks on load finish. Bonus: it returns another object with a.loaded
method, so it can be chained as promises. - provides a
.loadingStarted(callback)
method that allows to register a single method
- reactive programming
its a nice way to express transformation dependencies and the transformations themselves, better than promises and callbacks, and it expresses multiple fire events. Can't find anything about the
loadingStarted
concept - reflux hard to say. it looks like signals.
- signals
define(["signals"], function(Signal){
//some requirements:
// an object with a loaded(success, failure, finally) method, a
// isLoading boolean flag, and a load() method
// implementation:
// initial state: isLoading is false
function AsyncData(loadFn){
this._load = loadFn;
this.isLoading = false;
this._loadingStarted = new Signal();
this._success = new Signal();
this._failure = new Signal();
this._finaly = new Signal();
this._hasLoadedOnce = false;
this._lastSuccessArguments = undefined;
}
/**
* Register the success, failure, and finaly callbacks
* for each respective outcome.
*/
AsyncData.prototype.loaded = function(success, failure, finaly){
if (success){
if (this._hasLoadedOnce){
success(this._lastSuccessArguments);
}
this._success.add(success);
}
if (failure){
this._failure.add(failure);
}
if (finaly){
this._finaly.add(finaly);
}
};
//TODO: rename to loadingProgressed and integrate with
// promise.notify() ?
AsyncData.prototype.loadingStarted = function(callback){
if (callback){
this._loadingStarted.add(callback);
}
};
AsyncData.prototype.load = function(){
// concurrent loads? yes if we save the promise here in self._loadPromise and cancel it if it is ongoing
var self = this;
self.isLoading = true;
this._loadingStarted.dispatch();
var promise = $q.when(self._load());
promise.then(function(){
self._hasLoadedOnce = true;
this._lastSuccessArguments = arguments;
this._success.dispatch(arguments);
}, function(){
this._failure.dispatch(arguments);
});
promise['finally'](function(){
self.isLoading = false;
this._finaly.dispatch(arguments);
});
return promise;
};
return AsyncData;
});
usage
define(['async-data'], function(AsyncData){
var data = AsyncData(function(){
return $http.get('api/data');
});
data.started(function(progress){
console.log('loading data...')
});
var transformedData = data.loaded(function(data){
return transform(data);
});
transformedData.started(function(){
console.log('transforming data...')
});
transformedData.loaded(function(data){
console.log('got the transformed data', data)
});
data.load();
//we must see:
//loading data...
//transforming data...
//got the transformed data
});
- promises
define(['signals'], function(Signal){
// idea: define a wrapper over promises that provides the same interface as a promise,
// but allows to 'reset' it everytime `load` is called
// impl: discard old promise, create a new one with the same arguments
// problem: what happens to the chained promises of the discarded old promise? it needs to be reset as well somehow
function AsyncData(loadFn){
this._load = loadFn;
this.isLoading = false;
this._promise = null;
this._thens = [];
this._finalys = [];
this._lastSuccessArguments = undefined;
this._children = [];
this._parent = null;
this._loadingStarted = new Signal();
}
AsyncData.prototype.load = function(){
this._promise = this._load();
this._loadingStarted.dispatch();
// store arguments for later thens()
this._promise.then(function(){
this._lastSuccessArguments = arguments;
});
for (i = 0; i < this._thens.length; i++){
this._promise.then(this._thens[i]);
}
for (i = 0; i < this._finalys.length; i++){
this._promise['finally'](this._finalys[i]);
}
// update all chained
for (i = 0; i < this._children.length; i++){
this._children[i].child._promise = this._promise.then(this._children[i].thenArguments);
this._children[i].child._loadingStarted.dispatch();
}
};
AsyncData.prototype.then = function(success, error, notify){
// keep all registered thens to apply them to the
// new promises;
this._thens.push(arguments);
// we may already have a promise if it is loading
// otherwise trigger success result if we have one
if (this._promise){
this._promise.then(arguments);
} else if (this._lastSuccessArguments !== undefined){
success(this._lastSuccessArguments);
}
// new async data that has a load function which resolves
// AFTER the current then
// should the load() of the child trigger the parent's ?
// should the child even have a load() ? I think not
var childAsyncData = new AsyncDataChained();
childAsyncData._promise = this._promise.then(arguments);
this._children.push({
child: childAsyncData,
thenArguments: arguments
});
// need to do something when a load() is called, all childrens
// should refresh their promises as well...
return childAsyncData;
};
AsyncData.prototype['finally'] = function(callback){
this._finalys.push(arguments);
// we may already have a promise if it is loading
if (this._promise){
this._promise['finally'](arguments);
} else if (this._lastSuccessArguments !== undefined){
callback();
}
}
AsyncData.prototype.isResolved = function(){
return this._promise == null ? false : this._promise.isResolved;
};
AsyncData.prototype.loadingStarted = function(callback){
if (callback){
this._loadingStarted.add(callback);
}
};
});
An API based on streams could look as follows
var requestStream = new Stream();
var responseStream = requestStream.flatMap(function(url){
return Stream.fromPromise($http.get(...)) // return a Stream with only 1 value
});
requestStream.on(function(start){
// request started
})
responseStream.on(function(end){
// response finished
})
// with notification events
var requestStream = new Stream();
var notificationStream = requestStream.flatMap(function(url){
return // a stream with notification events
});
requestStream.on(function(start){
// request started
})
responseStream.on(function(end){
// response finished
})