Skip to content

Instantly share code, notes, and snippets.

@dtheodor
Last active August 29, 2015 14:16
Show Gist options
  • Save dtheodor/95d40833e7637e56f01e to your computer and use it in GitHub Desktop.
Save dtheodor/95d40833e7637e56f01e to your computer and use it in GitHub Desktop.
data with loaded listeners and is loading status

Problem:

  • 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:

  1. its completion can be reset or re-triggered, which will invoke all the .then methods again
  2. 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

Existing solutions

  1. 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
  2. reflux hard to say. it looks like signals.
  3. 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
});
  1. 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);
    }
  };

});

Stream implementation

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
})

Modify then method to support promises as incoming data, and new thenWithWorker method that invokes the success callback on a Web Worker.

function isPromise(obj){
  return typeof obj.then === "function";
}

function applyCallback(callback, data){
  return isPromise(data) ? data.then(callback) : callback(data);
}

PromiseStream.prototype.then = function(success, failure){

  var child = new PromiseStream();
  
  this._success.add(function (data) {
    if (success) {
      var data = applyCallback(success, data);
    }
    child._success.dispatch(data);
  });
}


PromiseStream.prototype.thenWithWorker = function(success, failure){

  var child = new PromiseStream();

  this._success.add(function (data) {
    if (success) {
      var successThroughWorker = function(data){
        var p = new Parallel(data);
        return p.spawn(success);
      };
      var data = applyCallback(successThroughWorker, data);
    }
    child._success.dispatch(data);
  });
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment