Skip to content

Instantly share code, notes, and snippets.

@markmarijnissen
Forked from dtheodor/data.md
Last active August 29, 2015 14:16
Show Gist options
  • Save markmarijnissen/4750d952e93bd565ad62 to your computer and use it in GitHub Desktop.
Save markmarijnissen/4750d952e93bd565ad62 to your computer and use it in GitHub Desktop.

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 - see other files

// Implementation:
var requestStream = new Stream();
var notificationStream = requestStream.flatMap(function(url){
return Stream.fromAngularPromise(....) // a stream with notification events
});
var responseStream = notificationStream
.filter(function(event){
return event.type === 'result';
}).map(function(event){
return event.value;
});
// Helper method:
// Convert Promise reject,resolve and notify in an event stream
Stream.fromAngularPromise = function(promise){
var stream = new Stream();
stream.emit({type:'start',value: null });
promise.then(function(value){
stream.emit({type:'result',value:value});
stream.end();
},function(error){
stream.error(error);
stream.end();
},function(value){
stream.emit({type:'progress',value:value})
});
}
// Usage
// 1. Do a request
requestStream.emit('...my url....')
// 2. Update date on response
responseStream.on(function(data){
// update UI (graph, table, etc
})
// 3. Update notifications while busy
notificationStream.on(function(event){
if(event.type === 'start' || event.type === 'progress'){
// show loader and update progress
} else {
// hide loader
}
});
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, show loader + disable UI (i.e. make it grey to show it's invalid)
})
responseStream.on(function(end){
// response finished, hide loader + update UI
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment