<!DOCTYPE html>
<html>

  <head>
    <link data-require="jasmine@2.0.0" data-semver="2.0.0" rel="stylesheet" href="//cdn.jsdelivr.net/jasmine/2.0.0/jasmine.css" />
    <script data-require="jasmine@2.0.0" data-semver="2.0.0" src="//cdn.jsdelivr.net/jasmine/2.0.0/jasmine.js"></script>
    <script data-require="jasmine@2.0.0" data-semver="2.0.0" src="//cdn.jsdelivr.net/jasmine/2.0.0/jasmine-html.js"></script>
    <script data-require="jasmine@2.0.0" data-semver="2.0.0" src="//cdn.jsdelivr.net/jasmine/2.0.0/boot.js"></script>
    <script src="attendee.js"></script>
    <script src="fakeAttendeeWebApi.js"></script>
    <script src="attendeeWebApiDecorator.js"></script>
    <!--
    <script src="fakeAttendeeWebApi_tests.js"></script>
    -->
    <script src="attendeeWebApiDecorator_tests.js"></script>
  </head>

  <body></body>

</html>
This plunk shows how to solve a common HTTP problem with Promises. The material is excerpted from the upcoming book, *Reliable JavaScript*, co-authored by Lawrence D. Spencer and Seth H. Richards (Wiley Publishing), as presented at Boston Code Camp 22 in November, 2014.

The `fakeAttendeeWebApi` module imitates a Web API service that can POST and GET attendees at a conference, through methods `post` and `getAll`. The problem is that if a `getAll` is issued soon after a `post`, then the `getAll` may not include the newly `post`ed data. The problem is solved using a `Promise`-based decorator, `attendeeWebApiDecorator`.

See the Jasmine unit tests in `attendeeWebApiDecorator_tests.js` for the expected behavior.
var Conference = Conference || {};
Conference.attendee = function(firstName, lastName){
  
  var attendeeId,
    checkedIn = false,
    first = firstName || 'None', 
    last = lastName || 'None',
    checkInNumber;
    
  return {
    setId: function(id) {
      attendeeId = id;
    },
    getId: function() {
      return attendeeId;
    },
    
    getFullName: function(){
      return first + ' ' + last;
    },
    
    isCheckedIn: function(){
      return checkedIn;
    },
    
    checkIn: function(){
      checkedIn = true;
    },
    
    undoCheckIn: function() {
      checkedIn = false;
      checkInNumber = undefined;
    },
    
    setCheckInNumber: function(number) {
      checkInNumber = number;
    },
    
    getCheckInNumber: function() {
      return checkInNumber;
    },
    
    // Tells whether this attendee is the same person as another attendee.
    // To keep things simple, the comparison is based on the name only.
    // In real life, there would be other data such as a date of birth.
    isSamePersonAs: function(otherAttendee) {
        return otherAttendee !== undefined
        && otherAttendee.getFullName() === this.getFullName();
    },
    
    // Return a copy of this attendee
    copy: function() {
      var copy = Conference.attendee(first,last);
      copy.setId(this.getId());
      copy.checkIn(this.isCheckedIn());
      copy.setCheckInNumber(this.getCheckInNumber());
      return copy;
    }
  };
}

var Conference = Conference || {};

// A fake version of attendeeWebApi. It has the same methods as the real one,
// but is entirely client-side.
Conference.fakeAttendeeWebApi = function(){
  
  var attendees = []; // Fake database table.
      
  return {
    
    // Pretend to POST the attendee to the server.
    // Returns a Promise that resolves to a copy of the attendee
    // (to mimic getting a new version from the server), which 
    // will at that point have a primary key (attendeeId) that was
    // assigned by the database.
    // If a test requires the Promise to reject, use a spy.
    post: function post(attendee) {
      return new Promise( function(resolve, reject) {
        // setTimeout, even with a delay of only 5 milliseconds, causes
        // the resolution of the promise to be delayed to the next turn.
        setTimeout(function pretendPostingToServer() {
          var copyOfAttendee = attendee.copy();
          copyOfAttendee.setId(attendees.length+1);
          attendees.push(copyOfAttendee);
          resolve(copyOfAttendee);
        },5);
      });
    },
    
    // Return a Promise for all attendees. This Promise always resolves,
    // but in testing a spy can make it reject if necessary.
    getAll: function getAll() {
      return new Promise( function(resolve,reject) {
        // This setTimeout has a shorter delay than post's,
        // to imitate the conditions observed in real life.
        setTimeout(function pretendToGetAllFromServer() {
          var copies = [];
          attendees.forEach(function(a) {
            copies.push(a.copy());
          });
          resolve(copies);
        },1);
      });
    }
  };
};

describe('fakeAttendeeWebApi', function() {
  var webApi,
      attendeeA,
      attendeeB;
  beforeEach(function() {
    webApi = Conference.fakeAttendeeWebApi();
    attendeeA = Conference.attendee('Mariano','Tezanos');
    attendeeB = Conference.attendee('Gregorio ','Perez');
  });
  describe('post(attendee)', function() {
    it('if successful, resolves to an attendee with an ID',
    function(done/*See Chapter 6*/) {
      webApi.post(attendeeA).then(
        function promiseResolved(attendee) {
          expect(attendee.getId()).not.toBeUndefined();
          done();
        },
        function promiseRejected() {
          expect('Promise rejected').toBe(false);
          done();
        });
    });
  });
  
  describe('getAll()', function () {
    it('returns a resolved promise for all attendees posted'
    +' if you wait for their promises to resolve', function (done) {
      webApi.post(attendeeA)
      .then(function() {
        return webApi.post(attendeeB);
      })
      .then(function() {
        return webApi.getAll();
      })
      .then(
        function promiseResolved(attendees) {
          expect(attendees[0].getFullName()).toEqual(attendeeA.getFullName());
          expect(attendees[1].getFullName()).toEqual(attendeeB.getFullName());
          done();
        },
        function promiseRejected() {
          expect('Promise rejected').toBe(false);
          done();
        });
    });
    it('does not include attendees whose posts are not resolved',
    function(done) {
      webApi.post(attendeeA);
      webApi.post(attendeeB);
      webApi.getAll().then(
        function promiseResolved(attendees) {
          expect(attendees.length).toBe(0);
          done();
        },
        function promiseRejected() {
          expect('Promise rejected').toBe(false);
          done();
        });
    });
  });
});
var Conference = Conference || {};
Conference.attendeeWebApiDecorator = function(baseWebApi){
  var self = this,
  
      // The records passed to the post function, 
      // whose calls are not yet resolved.
      pendingPosts = [],
      
      messages = {
        postPending: 'It appears that a post is pending for this attendee'
      }
      
  // Return the element of 'posts' that is for the attendee,
  // or -1 if there is no such element.
  function indexOfPostForSameAttendee(posts,attendee) {
    var ix;
    for (ix=0; ix<posts.length; ++ix) {
      if (posts[ix].isSamePersonAs(attendee)) {
        return ix;
      }
    }
    return -1;
  }
  
  return {
    
    post: function post(attendee) {
      if (indexOfPostForSameAttendee(pendingPosts, attendee) >=0 ) {
        return Promise.reject(new Error(messages.postPending));
      }
      
      pendingPosts.push(attendee);
      
      return baseWebApi.post(attendee).then(
        function onPostSucceeded(attendeeWithId) {
          // When the post returns the attendee with an ID, put the ID in
          // the pending record because that record may have been added to
          // a getAll result and we want that result to benefit from the ID.
          var ix = pendingPosts.indexOf(attendee);
          if (ix >= 0) {
              pendingPosts[ix].setId(attendeeWithId.getId());
              pendingPosts.splice(ix, 1);
          }
          return attendeeWithId;
        },
        function onPostFailed(reason) {
          var ix = pendingPosts.indexOf(attendee);
          if (ix >= 0) {
              pendingPosts.splice(ix, 1);
          }
          return Promise.reject(reason);
        });
    },
    
    getAll: function getAll() {
      return baseWebApi.getAll().then(function(records) {
        pendingPosts.forEach(function(pending) {
          var ix = indexOfPostForSameAttendee(records,pending);
          if (ix<0) {
            records.push(pending);
          }
        });
        return records;
      });
    },
    
    getMessages: function getMessages() {
      return messages;
    }
  };
};
describe('attendeeWebApiDecorator', function() {
  var decoratedWebApi,
      baseWebApi,
      attendeeA,
      attendeeB,
      underlyingFailure = 'Failure in underlying function';
  
  // Execute decoratedWebApi.getAll(), expecting it to return a resolved
  // Promise.
  // done        - The prevailing Jasmine done() function for async support.
  // expectation - A function that gives expectations on the returned 
  //               attendees.
  function getAllWithSuccessExpectation(done,expectation) {
    decoratedWebApi.getAll().then(
      function onSuccess(attendees) {
        expectation(attendees);
        done();
      },
      function onFailure() {
        expect('Failed in getAll').toBe(false);
        done();
      });
  }
  beforeEach(function() {
    baseWebApi = Conference.fakeAttendeeWebApi();
    decoratedWebApi = Conference.attendeeWebApiDecorator(baseWebApi);
    attendeeA = Conference.attendee('Mariano','Tezanos');
    attendeeB = Conference.attendee('Gregorio ','Perez');
  });
  
  describe('post(attendee)', function() {
    
    describe('on success of the underlying post', function() {
      it('returns a Promise that resolves to an attendee with ID', 
      function(done) {
        decoratedWebApi.post(attendeeA).then(
          function onSuccess(attendee) {
            expect(attendee.getFullName()).toBe(attendeeA.getFullName());
            expect(attendee.getId()).not.toBeUndefined();
            done();
          },
          function onFailure() {
            expect('Failed').toBe(false);
            done();
          });
      });
      it('causes an immediate getAll to include the record without ID',
      function(done) {
        decoratedWebApi.post(attendeeA);
        // Execute getAll without waiting for the post to resolve.
        getAllWithSuccessExpectation(done, function onSuccess(attendees) {
          expect(attendees.length).toBe(1);
          expect(attendees[0].getId()).toBeUndefined();
        });
      });
      it('causes a delayed getAll to include the record with ID', 
      function(done) {
        decoratedWebApi.post(attendeeA).then(function() {
          // This time execute getAll after post resolves.
          getAllWithSuccessExpectation(done, function onSuccess(attendees) {
            expect(attendees.length).toBe(1);
            expect(attendees[0].getId()).not.toBeUndefined();
          });
        });
      });
      it('fills in IDs of records already appended to getAll',function(done){
        var recordsFromGetAll, promiseFromPostA;
        // Issue the post and don't wait for it.
        promiseFromPostA = decoratedWebApi.post(attendeeA);
        // Immediately issue the getAll, and capture its results.
        decoratedWebApi.getAll().then(function onSuccess(attendees) {
          recordsFromGetAll = attendees;
          expect(recordsFromGetAll[0].getId()).toBeUndefined();
        });
        // Now wait for the post to finally resolve. (Remember that
        // its timeout is longer than getAll's.) When it does resolve,
        // We should see the attendeeId appear in the pending record that
        // getAll() obtained.
        promiseFromPostA.then(function() {
          expect(recordsFromGetAll[0].getId()).not.toBeUndefined();
          done();
        });
      });
    });
    
    describe('on failure of the underlying post', function() {
      beforeEach(function() {
        // Cause the base's post to fail, but not until the next turn.
       spyOn(baseWebApi,'post').and.returnValue(
        new Promise( function(resolve,reject) {
          setTimeout(function() {
            reject(underlyingFailure);
          },5);
        }));       
      });
      it('returns a Promise rejected with the underlying reason',function() {
        decoratedWebApi.post(attendeeA).then(
          function onSuccessfulPost() {
            expect('Post succeeded').toBe(false);
            done();
          },
          function onRejectedPost(reason) {
            expect(reason).toBe(underlyingFailure);
            done();
          });
      });
      it('still allows an immediate getAll to include the record without an ID', 
      function(done) {
        decoratedWebApi.post(attendeeA);
        getAllWithSuccessExpectation(done, function onSuccess(attendees) {
          expect(attendees.length).toBe(1);
        });
      });
      it('causes a delayed getAll to exclude the record', 
      function(done) {
        decoratedWebApi.post(attendeeA).then(
          function onSuccessfulPost() {
            expect('Post succeeded').toBe(false);
            done();
          },
          function onRejectedPost() {
            getAllWithSuccessExpectation(done, function onSuccess(attendees) {
              expect(attendees.length).toBe(0);
            });
          });
      });
    });
    describe('when called for an attendee just posted', function() {
      it('returns a rejected promise',function(done) {
        decoratedWebApi.post(attendeeA);
        decoratedWebApi.post(attendeeA).then(
          function onSuccess() {
            expect('Post succeeded').toBe(false);
            done();
          },
          function onFailure(error) {
            expect(error instanceof Error).toBe(true);
            expect(error.message).toBe(
              decoratedWebApi.getMessages().postPending);
            done();
          });
      });
    }); 
  });
  
  describe('getAll()', function() {
    
    describe('on success of underlying getAll', function() {
      it('returns a Promise for all processed records, '
      +'if there are none pending',function(done) {
        spyOn(baseWebApi,'getAll').and.returnValue(
          new Promise( function(resolve,reject) {
            setTimeout(function() {
              resolve([attendeeA,attendeeB]);
            },1);
          }));
        getAllWithSuccessExpectation(done,function onSuccess(attendees) {
          expect(attendees.length).toBe(2);
        });
      });
      it('returns a Promise for all processed records plus all pending ones',
      function(done) {
        decoratedWebApi.post(attendeeA).then(function() {
          decoratedWebApi.post(attendeeB); // Leave pending.
          getAllWithSuccessExpectation(done,function onSuccess(attendees) {
            expect(attendees.length).toBe(2);
            expect(attendees[0].getId()).not.toBeUndefined();
            expect(attendees[1].getId()).toBeUndefined();
          });
        });
      });
    });
    
    describe('on failure of underlying getAll', function() {
      it('returns the underlying rejected Promise', function(done) {
        spyOn(baseWebApi,'getAll').and.returnValue(
          new Promise( function(resolve,reject) {
            setTimeout(function() {
              reject(underlyingFailure);
            },1);
          }));  
        decoratedWebApi.getAll().then(
          function onSuccess() {
            expect('Underlying getAll succeeded').toBe(false);
            done();
          },
          function onFailure(reason) {
            expect(reason).toBe(underlyingFailure);
            done();
          });
      });
    });
  });
});