├── .gitignore ├── LICENSE ├── README.md ├── auto_mock_off.js ├── demo_promise1_single.js ├── demo_promise2_chaining.js ├── demo_promise3_flattening.js ├── demo_promise4_exceptions.js ├── package.json └── spec └── demo_promise_spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | .DS_Store 4 | 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Axel Rauschmayer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DemoPromise 2 | 3 | A toy implementation of (an approximation of) the ECMAScript 6 promise API, for educational purposes: 4 | 5 | * `spec/demo_promise_spec.js` contains usage examples. 6 | * The following blog post explains how it works: “[ECMAScript 6 promises (2/2): the API](http://www.2ality.com/2014/10/es6-promises-api.html)” 7 | 8 | ## Running the tests 9 | 10 | Installing Babel and Jest: 11 | 12 | ``` 13 | cd demo_promise/ 14 | npm install 15 | ``` 16 | 17 | Running the tests: 18 | 19 | ``` 20 | npm test 21 | ``` 22 | -------------------------------------------------------------------------------- /auto_mock_off.js: -------------------------------------------------------------------------------- 1 | /* 2 | Module imports are hoisted, which is why you can’t turn off 3 | auto-mocking beforehand. 4 | Work-around: make this module the first import. 5 | */ 6 | 7 | jest.autoMockOff(); 8 | -------------------------------------------------------------------------------- /demo_promise1_single.js: -------------------------------------------------------------------------------- 1 | // Features: 2 | // * then() must work independently if the promise is 3 | // settled either before or after it is called 4 | // * You can only resolve or reject once 5 | 6 | export class DemoPromise { 7 | constructor() { 8 | this.fulfillReactions = []; 9 | this.rejectReactions = []; 10 | this.promiseResult = undefined; 11 | this.promiseState = 'pending'; 12 | } 13 | then(onFulfilled, onRejected) { 14 | let self = this; 15 | let fulfilledTask = function () { 16 | onFulfilled(self.promiseResult); 17 | }; 18 | let rejectedTask = function () { 19 | onRejected(self.promiseResult); 20 | }; 21 | switch (this.promiseState) { 22 | case 'pending': 23 | this.fulfillReactions.push(fulfilledTask); 24 | this.rejectReactions.push(rejectedTask); 25 | break; 26 | case 'fulfilled': 27 | addToTaskQueue(fulfilledTask); 28 | break; 29 | case 'rejected': 30 | addToTaskQueue(rejectedTask); 31 | break; 32 | } 33 | } 34 | resolve(value) { 35 | if (this.promiseState !== 'pending') return; 36 | this.promiseState = 'fulfilled'; 37 | this.promiseResult = value; 38 | this._clearAndEnqueueReactions(this.fulfillReactions); 39 | return this; // enable chaining 40 | } 41 | reject(error) { 42 | if (this.promiseState !== 'pending') return; 43 | this.promiseState = 'rejected'; 44 | this.promiseResult = error; 45 | this._clearAndEnqueueReactions(this.rejectReactions); 46 | return this; // enable chaining 47 | } 48 | _clearAndEnqueueReactions(reactions) { 49 | this.fulfillReactions = undefined; 50 | this.rejectReactions = undefined; 51 | reactions.map(addToTaskQueue); 52 | } 53 | } 54 | 55 | function addToTaskQueue(task) { 56 | setTimeout(task, 0); 57 | } 58 | -------------------------------------------------------------------------------- /demo_promise2_chaining.js: -------------------------------------------------------------------------------- 1 | // Features: 2 | // * then() returns a promise, which fulfills with what 3 | // either onFulfilled or onRejected return 4 | // * Missing onFulfilled and onRejected pass on what they receive 5 | 6 | export class DemoPromise { 7 | constructor() { 8 | this.fulfillReactions = []; 9 | this.rejectReactions = []; 10 | this.promiseResult = undefined; 11 | this.promiseState = 'pending'; 12 | } 13 | then(onFulfilled, onRejected) { 14 | let returnValue = new Promise(); // [new] 15 | let self = this; 16 | 17 | let fulfilledTask; 18 | if (typeof onFulfilled === 'function') { 19 | fulfilledTask = function () { 20 | let r = onFulfilled(self.promiseResult); 21 | returnValue.resolve(r); // [new] 22 | }; 23 | } else { // [new] 24 | fulfilledTask = function () { 25 | returnValue.resolve(self.promiseResult); 26 | }; 27 | } 28 | 29 | let rejectedTask; 30 | if (typeof onRejected === 'function') { 31 | rejectedTask = function () { 32 | let r = onRejected(self.promiseResult); 33 | returnValue.resolve(r); // [new] 34 | }; 35 | } else { // [new] 36 | rejectedTask = function () { 37 | // `onRejected` has not been provided 38 | // => we must pass on the rejection 39 | returnValue.reject(self.promiseResult); 40 | }; 41 | } 42 | 43 | switch (this.promiseState) { 44 | case 'pending': 45 | this.fulfillReactions.push(fulfilledTask); 46 | this.rejectReactions.push(rejectedTask); 47 | break; 48 | case 'fulfilled': 49 | addToTaskQueue(fulfilledTask); 50 | break; 51 | case 'rejected': 52 | addToTaskQueue(rejectedTask); 53 | break; 54 | } 55 | return returnValue; 56 | } 57 | resolve(value) { 58 | if (this.promiseState !== 'pending') return; 59 | this.promiseState = 'fulfilled'; 60 | this.promiseResult = value; 61 | this._clearAndEnqueueReactions(this.fulfillReactions); 62 | return this; // enable chaining 63 | } 64 | reject(error) { 65 | if (this.promiseState !== 'pending') return; 66 | this.promiseState = 'rejected'; 67 | this.promiseResult = error; 68 | this._clearAndEnqueueReactions(this.rejectReactions); 69 | return this; // enable chaining 70 | } 71 | _clearAndEnqueueReactions(reactions) { 72 | this.fulfillReactions = undefined; 73 | this.rejectReactions = undefined; 74 | reactions.map(addToTaskQueue); 75 | } 76 | } 77 | function addToTaskQueue(task) { 78 | setTimeout(task, 0); 79 | } 80 | -------------------------------------------------------------------------------- /demo_promise3_flattening.js: -------------------------------------------------------------------------------- 1 | // Features: 2 | // * resolve() “flattens” parameter `value` if it is a promise 3 | // (the state of`this` becomes locked in on `value`) 4 | 5 | export class DemoPromise { 6 | constructor() { 7 | this.fulfillReactions = []; 8 | this.rejectReactions = []; 9 | this.promiseResult = undefined; 10 | this.promiseState = 'pending'; 11 | // Settled or locked-in? 12 | this.alreadyResolved = false; 13 | } 14 | then(onFulfilled, onRejected) { 15 | let returnValue = new DemoPromise(); 16 | let self = this; 17 | 18 | let fulfilledTask; 19 | if (typeof onFulfilled === 'function') { 20 | fulfilledTask = function () { 21 | let r = onFulfilled(self.promiseResult); 22 | returnValue.resolve(r); 23 | }; 24 | } else { 25 | fulfilledTask = function () { 26 | returnValue.resolve(self.promiseResult); 27 | }; 28 | } 29 | 30 | let rejectedTask; 31 | if (typeof onRejected === 'function') { 32 | rejectedTask = function () { 33 | let r = onRejected(self.promiseResult); 34 | returnValue.resolve(r); 35 | }; 36 | } else { 37 | rejectedTask = function () { 38 | // `onRejected` has not been provided 39 | // => we must pass on the rejection 40 | returnValue.reject(self.promiseResult); 41 | }; 42 | } 43 | 44 | switch (this.promiseState) { 45 | case 'pending': 46 | this.fulfillReactions.push(fulfilledTask); 47 | this.rejectReactions.push(rejectedTask); 48 | break; 49 | case 'fulfilled': 50 | addToTaskQueue(fulfilledTask); 51 | break; 52 | case 'rejected': 53 | addToTaskQueue(rejectedTask); 54 | break; 55 | } 56 | return returnValue; 57 | } 58 | resolve(value) { // [new] 59 | if (this.alreadyResolved) return; 60 | this.alreadyResolved = true; 61 | this._doResolve(value); 62 | return this; // enable chaining 63 | } 64 | _doResolve(value) { // [new] 65 | let self = this; 66 | // Is `value` a thenable? 67 | if (typeof value === 'object' && value !== null && 'then' in value) { 68 | // Forward fulfillments and rejections from `value` to `this`. 69 | // Added as a task (vs. done immediately) to preserve async semantics. 70 | addToTaskQueue(function () { 71 | value.then( 72 | function onFulfilled(result) { 73 | self._doResolve(result); 74 | }, 75 | function onRejected(error) { 76 | self._doReject(error); 77 | }); 78 | }); 79 | } else { 80 | this.promiseState = 'fulfilled'; 81 | this.promiseResult = value; 82 | this._clearAndEnqueueReactions(this.fulfillReactions); 83 | } 84 | } 85 | 86 | reject(error) { // [new] 87 | if (this.alreadyResolved) return; 88 | this.alreadyResolved = true; 89 | this._doReject(error); 90 | return this; // enable chaining 91 | } 92 | _doReject(error) { // [new] 93 | this.promiseState = 'rejected'; 94 | this.promiseResult = error; 95 | this._clearAndEnqueueReactions(this.rejectReactions); 96 | } 97 | 98 | _clearAndEnqueueReactions(reactions) { 99 | this.fulfillReactions = undefined; 100 | this.rejectReactions = undefined; 101 | reactions.map(addToTaskQueue); 102 | } 103 | } 104 | function addToTaskQueue(task) { 105 | setTimeout(task, 0); 106 | } 107 | -------------------------------------------------------------------------------- /demo_promise4_exceptions.js: -------------------------------------------------------------------------------- 1 | // Features: 2 | // * Turn exceptions in user code into rejections 3 | 4 | // MISSING: revealing constructor pattern 5 | 6 | export class DemoPromise { 7 | constructor() { 8 | this.fulfillReactions = []; 9 | this.rejectReactions = []; 10 | this.promiseResult = undefined; 11 | this.promiseState = 'pending'; 12 | // Settled or locked-in? 13 | this.alreadyResolved = false; 14 | } 15 | then(onFulfilled, onRejected) { 16 | let returnValue = new DemoPromise(); 17 | let self = this; 18 | 19 | let fulfilledTask; 20 | if (typeof onFulfilled === 'function') { 21 | fulfilledTask = function () { 22 | try { // [new] 23 | let r = onFulfilled(self.promiseResult); 24 | returnValue.resolve(r); 25 | } catch (e) { 26 | returnValue.reject(e); 27 | } 28 | }; 29 | } else { 30 | fulfilledTask = function () { 31 | returnValue.resolve(self.promiseResult); 32 | }; 33 | } 34 | 35 | let rejectedTask; 36 | if (typeof onRejected === 'function') { 37 | rejectedTask = function () { 38 | try { // [new] 39 | let r = onRejected(self.promiseResult); 40 | returnValue.resolve(r); 41 | } catch (e) { 42 | returnValue.reject(e); 43 | } 44 | }; 45 | } else { 46 | rejectedTask = function () { 47 | // `onRejected` has not been provided 48 | // => we must pass on the rejection 49 | returnValue.reject(self.promiseResult); 50 | }; 51 | } 52 | 53 | switch (this.promiseState) { 54 | case 'pending': 55 | this.fulfillReactions.push(fulfilledTask); 56 | this.rejectReactions.push(rejectedTask); 57 | break; 58 | case 'fulfilled': 59 | addToTaskQueue(fulfilledTask); 60 | break; 61 | case 'rejected': 62 | addToTaskQueue(rejectedTask); 63 | break; 64 | } 65 | return returnValue; 66 | } 67 | catch(onRejected) { 68 | return this.then(null, onRejected); 69 | } 70 | resolve(value) { 71 | if (this.alreadyResolved) return; 72 | this.alreadyResolved = true; 73 | this._doResolve(value); 74 | return this; // enable chaining 75 | } 76 | _doResolve(value) { 77 | let self = this; 78 | if (typeof value === 'object' && value !== null && 'then' in value) { 79 | // Forward fulfillments and rejections from `value` to `this`. 80 | // Added as a task (vs. done immediately) to preserve async semantics. 81 | addToTaskQueue(function () { 82 | value.then( 83 | function onFulfilled(result) { 84 | self._doResolve(result); 85 | }, 86 | function onRejected(error) { 87 | self._doReject(error); 88 | }); 89 | }); 90 | } else { 91 | this.promiseState = 'fulfilled'; 92 | this.promiseResult = value; 93 | this._clearAndEnqueueReactions(this.fulfillReactions); 94 | } 95 | } 96 | 97 | reject(error) { 98 | if (this.alreadyResolved) return; 99 | this.alreadyResolved = true; 100 | this._doReject(error); 101 | return this; // enable chaining 102 | } 103 | _doReject(error) { 104 | this.promiseState = 'rejected'; 105 | this.promiseResult = error; 106 | this._clearAndEnqueueReactions(this.rejectReactions); 107 | } 108 | 109 | _clearAndEnqueueReactions(reactions) { 110 | this.fulfillReactions = undefined; 111 | this.rejectReactions = undefined; 112 | reactions.map(addToTaskQueue); 113 | } 114 | } 115 | function addToTaskQueue(task) { 116 | setTimeout(task, 0); 117 | } 118 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "babel-jest": "*", 4 | "jest-cli": "*" 5 | }, 6 | "scripts": { 7 | "test": "jest" 8 | }, 9 | "jest": { 10 | "scriptPreprocessor": "/node_modules/babel-jest", 11 | "testFileExtensions": ["js"], 12 | "moduleFileExtensions": ["js", "json"], 13 | "testDirectoryName": "spec" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /spec/demo_promise_spec.js: -------------------------------------------------------------------------------- 1 | import '../auto_mock_off'; 2 | import { DemoPromise } from '../demo_promise4_exceptions'; 3 | 4 | describe('Order of resolving', function () { 5 | it('resolves before then()', function (done) { 6 | let dp = new DemoPromise(); 7 | dp.resolve('abc'); 8 | dp.then(function (value) { 9 | expect(value).toBe('abc'); 10 | done(); 11 | }); 12 | }); 13 | it('resolves after then()', function (done) { 14 | let dp = new DemoPromise(); 15 | dp.then(function (value) { 16 | expect(value).toBe('abc'); 17 | done(); 18 | }); 19 | dp.resolve('abc'); 20 | }); 21 | }); 22 | describe('Chaining', function () { 23 | it('chains with a non-thenable', function (done) { 24 | let dp = new DemoPromise(); 25 | dp.resolve('a'); 26 | dp 27 | .then(function (value1) { 28 | expect(value1).toBe('a'); 29 | return 'b'; 30 | }) 31 | .then(function (value2) { 32 | expect(value2).toBe('b'); 33 | done(); 34 | }); 35 | }); 36 | 37 | it('chains with a promise', function (done) { 38 | let dp1 = new DemoPromise(); 39 | let dp2 = new DemoPromise(); 40 | dp1.resolve(dp2); 41 | dp2.resolve(123); 42 | // Has the value been passed on to dp1? 43 | dp1.then(function (value) { 44 | expect(value).toBe(123); 45 | done(); 46 | }); 47 | }); 48 | }); 49 | 50 | describe('Fulfilling by returning in reactions', function () { 51 | it('fulfills via onFulfilled', function (done) { 52 | let dp = new DemoPromise(); 53 | dp.resolve(); 54 | dp 55 | .then(function (value1) { 56 | expect(value1).toBe(undefined); 57 | return 123; 58 | }) 59 | .then(function (value2) { 60 | expect(value2).toBe(123); 61 | done(); 62 | }); 63 | }); 64 | it('fulfills via onRejected', function (done) { 65 | let dp = new DemoPromise(); 66 | dp.reject(); 67 | dp 68 | .catch(function (reason) { 69 | expect(reason).toBe(undefined); 70 | return 123; 71 | }) 72 | .then(function (value) { 73 | expect(value).toBe(123); 74 | done(); 75 | }); 76 | }); 77 | }); 78 | describe('Rejecting by throwing in reactions', function () { 79 | it('rejects via onFulfilled', function (done) { 80 | let myError; 81 | let dp = new DemoPromise(); 82 | dp.resolve(); 83 | dp 84 | .then(function (value) { 85 | expect(value).toBe(undefined); 86 | throw myError = new Error(); 87 | }) 88 | .catch(function (reason) { 89 | expect(reason).toBe(myError); 90 | done(); 91 | }); 92 | }); 93 | it('rejects via onRejected', function (done) { 94 | let myError; 95 | let dp = new DemoPromise(); 96 | dp.reject(); 97 | dp 98 | .catch(function (reason1) { 99 | expect(reason1).toBe(undefined); 100 | throw myError = new Error(); 101 | }) 102 | .catch(function (reason2) { 103 | expect(reason2).toBe(myError); 104 | done(); 105 | }); 106 | }); 107 | }); 108 | --------------------------------------------------------------------------------