├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── cjs ├── index.js └── package.json ├── es.js ├── esm └── index.js ├── index.js ├── package.json ├── rollup ├── es.config.js └── index.config.js └── test ├── .test.js ├── dodgy.js ├── index.html ├── index.js ├── package.json └── testrunner.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | node_modules/ 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | .travis.yml 4 | node_modules/ 5 | rollup/ 6 | test/ 7 | package-lock.json 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.12 4 | git: 5 | depth: 1 6 | branches: 7 | only: 8 | - master -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2015 by Andrea Giammarchi - @WebReflection 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Dodgy [![build status](https://secure.travis-ci.org/WebReflection/dodgy.svg)](http://travis-ci.org/WebReflection/dodgy) 2 | ===== 3 | 4 | The idea behind this module has been explained in [my good old blog](http://webreflection.blogspot.co.uk/2015/09/on-cancelable-promises.html), 5 | and was [born after the following gist](https://gist.github.com/WebReflection/796d1f04b1173fbcfe5a#file-lie-js) as improved and well tested 30 LOC "_life saver_". 6 | 7 | * CDN global utility via https://unpkg.com/dodgy 8 | * ESM via `import {Dodgy} from 'dodgy'` 9 | * CJS via `const {Dodgy} = require('dodgy')` 10 | 11 | ### How to opt in for .abort([optionalValue]) 12 | In order to make a promise cancelable we need to invoke the third argument which is a callback expecting to know what to do in case of abortion. 13 | ```js 14 | import {Dodgy} from 'dodgy'; 15 | 16 | var p = new Dodgy(function (res, rej, onAbort) { 17 | var t = setTimeout(res, 1000, 'OK'); 18 | onAbort(function (butWhy) { 19 | clearTimeout(t); 20 | return butWhy || 'because'; 21 | }); 22 | }); 23 | 24 | p.then( 25 | console.log.bind(console), 26 | console.warn.bind(console) // <- .abort() 27 | ).catch( 28 | console.error.bind(console) 29 | ); 30 | 31 | // at any time later on 32 | p.abort('not needed anynmore'); 33 | 34 | ``` 35 | 36 | ### How to opt in as externally resolvable 37 | Canceling a Promise is one thing, resolving it externally is a whole new "_dodger_" level but we can explicitly opt in if that's needed. 38 | ```js 39 | var p = new Dodgy( 40 | function (res, rej, optInAbort) { 41 | // we still need to opt in for abortability 42 | // simply invoke the third argument 43 | // passing a "no-op" function, if needed 44 | optInAbort(Object); // Object would do 45 | }, 46 | true // <- go even dodger !!! 47 | ); 48 | 49 | // our p now will have 3 methods: 50 | p.resolve; 51 | p.reject; 52 | p.abort; 53 | ``` 54 | At this point we can fully control our Promise, proudly riding the edges of nonsense-land! 55 | 56 | 57 | ### Dodgy.race(iterable) 58 | 59 | Either one Promise in the iterable solve or the returned dodgy promise is aborted, all other dodgy promises in the iterable will be aborted too. 60 | 61 | ```js 62 | var race = Dodgy.race(p1, d2, p3, p4, d5); 63 | 64 | // if any of those promises is resolved 65 | // all abortable will be aborted 66 | // e.g. d2.abort(); d5.abort(); will be called 67 | 68 | // the same happens if the race itself is aborted 69 | race.abort(); 70 | ``` 71 | 72 | 73 | ### Chainability 74 | We can `p.then().catch()` as much as we like, all control methods will be propagated down the road. 75 | 76 | ### Chaining abortable and resolvable promises 77 | I believe it is a very bad idea to create alchemy capable of chaining abortaability or resolutions. The resolution per promise can be different per each Promise, and so is the reason to abort. Combine these in a chain would lead to disasters. 78 | 79 | Accordingly, if we need to chain multiple abortable Promises, we can return a new one instead, creating points in the chain where abort would make sense. 80 | 81 | 82 | ### Multiple abortable/resolvable in the same chain 83 | The way we could resolve a Promise is different per promise, and so is the reason we might abort. 84 | I haven't spent much time over-complicating a thing here. If you need to pass along an abortable promise that shuold abort a received one, you are free to create promises like the following: 85 | ```js 86 | // we have a p1, we return a p2 87 | return new Dodgy(function (res, rej, onAbort) { 88 | // logic to resolve or reject here ... then ... 89 | onAbort(function () { 90 | // abort previous one 91 | p1.abort('from p2'); 92 | // now abort this one doing whatever is needed 93 | }); 94 | }); 95 | ``` 96 | 97 | 98 | ### Compatibility 99 | Every browser and JavaScript engine, but the Promise polyfill is not included. 100 | Try [es6-promise](https://github.com/jakearchibald/es6-promise) if you want, it worked for my [tests on IE8 too](http://webreflection.github.io/dodgy/test/). 101 | 102 | ### Which license? 103 | The MIT style License 104 | -------------------------------------------------------------------------------- /cjs/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | function Dodgy(callback, resolvable) { 3 | var 4 | resolve, reject, abort, 5 | status = 'pending', 6 | dog = new Promise(function (res, rej) { 7 | callback( 8 | resolve = function (value) { 9 | if (status === 'pending') { 10 | status = 'resolved'; 11 | dog.status = status; 12 | res(value); 13 | } 14 | }, 15 | reject = function (value) { 16 | if (status === 'pending') { 17 | status = 'rejected'; 18 | dog.status = status; 19 | rej(value); 20 | } 21 | }, 22 | function onAbort(callback) { 23 | abort = function (reason) { 24 | if (status === 'pending') { 25 | status = 'aborted'; 26 | dog.status = status; 27 | rej(callback(reason)); 28 | } 29 | }; 30 | } 31 | ); 32 | }) 33 | ; 34 | return evolved(dog, resolvable, abort, resolve, reject); 35 | } 36 | 37 | function evolved(dog, resolvable, abort, resolve, reject) { 38 | var 39 | currentThen = dog.then, 40 | currentCatch = dog.catch 41 | ; 42 | if (abort) dog.abort = abort; 43 | if (resolvable) { 44 | dog.resolve = resolve; 45 | dog.reject = reject; 46 | } 47 | dog.then = function () { 48 | return evolved( 49 | currentThen.apply(dog, arguments), 50 | resolvable, abort, resolve, reject 51 | ); 52 | }; 53 | dog['catch'] = function () { 54 | return evolved( 55 | currentCatch.apply(dog, arguments), 56 | resolvable, abort, resolve, reject 57 | ); 58 | }; 59 | return dog; 60 | } 61 | 62 | Dodgy.race = function (iterable) { 63 | var dog = Promise.race(iterable).then(abort); 64 | function abort(result) { 65 | for (var i = 0; i < iterable.length; i++) { 66 | if ('abort' in iterable[i]) iterable[i].abort(); 67 | } 68 | return result; 69 | } 70 | dog.abort = abort; 71 | return dog; 72 | }; 73 | 74 | Object.defineProperty(exports, '__esModule', {value: true}).default = Dodgy; 75 | exports.Dodgy = Dodgy; 76 | -------------------------------------------------------------------------------- /cjs/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} -------------------------------------------------------------------------------- /es.js: -------------------------------------------------------------------------------- 1 | var dodgy=function(n){"use strict";function t(n,t){var r,e,o,u="pending",c=new Promise((function(t,a){n(r=function(n){"pending"===u&&(u="resolved",c.status=u,t(n))},e=function(n){"pending"===u&&(u="rejected",c.status=u,a(n))},(function(n){o=function(t){"pending"===u&&(u="aborted",c.status=u,a(n(t)))}}))}));return function n(t,r,e,o,u){var c=t.then,a=t.catch;e&&(t.abort=e);r&&(t.resolve=o,t.reject=u);return t.then=function(){return n(c.apply(t,arguments),r,e,o,u)},t.catch=function(){return n(a.apply(t,arguments),r,e,o,u)},t}(c,t,o,r,e)}return t.race=function(n){var t=Promise.race(n).then(r);function r(t){for(var r=0;r 1) ok(); }, 116 | secondDog = function () { if (++counter > 1) ok(); }, 117 | ok = (function () { 118 | wru.assert('everything was canceled', counter === 2); 119 | }) 120 | ; 121 | Dodgy.race([ 122 | new Dodgy(function (res, rej, onAbort) { 123 | onAbort(firstDog); 124 | }), 125 | new Dodgy(function (res, rej, onAbort) { 126 | onAbort(secondDog); 127 | }) 128 | ]).abort(); 129 | } 130 | } ,{ 131 | name: 'Dodgy.race with one winner', 132 | test: function () { 133 | var 134 | result = [], 135 | a = new Dodgy(function (res, rej, onAbort) { 136 | setTimeout(res, 100, 123); 137 | onAbort(function () { 138 | result.push('a'); 139 | }); 140 | }), 141 | b = new Dodgy(function (res, rej, onAbort) { 142 | onAbort(function () { 143 | result.push('b'); 144 | }); 145 | }), 146 | race = Dodgy.race([a, b]).then(wru.async(function () { 147 | wru.assert('b was canceled', result.join('') === 'b'); 148 | })) 149 | ; 150 | } 151 | }, { 152 | name: 'Resolvable but not abortable', 153 | test: function () { 154 | var 155 | d = new Dodgy(function (res, rej) { 156 | // should trigger too late 157 | setTimeout(function () { 158 | res(1); 159 | }, 100); 160 | }, true).then(wru.async(function (v) { 161 | wru.assert('expected 2, got ' + v, v === 2); 162 | setTimeout(wru.async(function (v) { 163 | wru.assert('OK'); 164 | d.then(wru.async(function (v) { 165 | wru.assert('expected 2, got ' + v, v === 2); 166 | return v; 167 | })); 168 | }), 200); 169 | return v; 170 | })) 171 | ; 172 | wru.assert('abort is not set', !d.abort); 173 | // should resolve 174 | setTimeout(function () { 175 | d.resolve(2); 176 | }, 10); 177 | } 178 | } 179 | ]); 180 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | wru test 5 | 6 | 7 | 8 | 9 | 15 | 58 | 59 | 60 | 92 | 93 | 94 |
95 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | require('./dodgy'); 2 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} -------------------------------------------------------------------------------- /test/testrunner.js: -------------------------------------------------------------------------------- 1 | console.log('Loading: test.html'); 2 | var page = require('webpage').create(); 3 | var url = 'index.html'; 4 | page.open(url, function (status) { 5 | if (status === 'success') { 6 | setTimeout(function () { 7 | var results = page.evaluate(function() { 8 | // remove the first node with the total from the following counts 9 | var passed = Math.max(0, document.querySelectorAll('.pass').length - 1); 10 | return { 11 | // retrieve the total executed tests number 12 | total: ''.concat( 13 | passed, 14 | ' blocks (', 15 | document.querySelector('#wru strong').textContent.replace(/\D/g, ''), 16 | ' single tests)' 17 | ), 18 | passed: passed, 19 | failed: Math.max(0, document.querySelectorAll('.fail').length - 1), 20 | failures: [].map.call(document.querySelectorAll('.fail'), function (node) { 21 | return node.textContent; 22 | }), 23 | errored: Math.max(0, document.querySelectorAll('.error').length - 1), 24 | errors: [].map.call(document.querySelectorAll('.error'), function (node) { 25 | return node.textContent; 26 | }) 27 | }; 28 | }); 29 | console.log('- - - - - - - - - -'); 30 | console.log('total: ' + results.total); 31 | console.log('- - - - - - - - - -'); 32 | console.log('passed: ' + results.passed); 33 | if (results.failed) { 34 | console.log('failures: \n' + results.failures.join('\n')); 35 | } else { 36 | console.log('failed: ' + results.failed); 37 | } 38 | if (results.errored) { 39 | console.log('errors: \n' + results.errors.join('\n')); 40 | } else { 41 | console.log('errored: ' + results.errored); 42 | } 43 | console.log('- - - - - - - - - -'); 44 | if (0 < results.failed + results.errored) { 45 | status = 'failed'; 46 | } 47 | phantom.exit(0); 48 | }, 2000); 49 | } else { 50 | phantom.exit(1); 51 | } 52 | }); --------------------------------------------------------------------------------