├── .eslintignore ├── .npmignore ├── .babelrc ├── .gitignore ├── .travis.yml ├── test ├── .eslintrc ├── .babelrc └── index.js ├── example.js ├── LICENSE ├── package.json ├── index.js └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | .idea 3 | yarn.lock -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015" 4 | ] 5 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | npm-debug.log 4 | yarn.lock -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "4" -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "id-length": [2, {"exceptions": ["t"]}] 4 | } 5 | } -------------------------------------------------------------------------------- /test/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015" 4 | ], 5 | "plugins": [ 6 | "transform-async-to-generator" 7 | ] 8 | } -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | const debounce = require('debounce-promise') 2 | 3 | function expensiveOperation (value, delay) { 4 | return Promise.resolve(value) 5 | } 6 | 7 | // Simple example 8 | { 9 | const saveCycles = debounce(expensiveOperation, 100); 10 | 11 | [1, 2, 3, 4].forEach(num => { 12 | return saveCycles('call no #' + num).then(value => { 13 | console.log(value) 14 | }) 15 | }) 16 | } 17 | 18 | // With leading=true 19 | { 20 | const saveCycles = debounce(expensiveOperation, 100, {leading: true}); 21 | 22 | [1, 2, 3, 4].forEach(num => { 23 | return saveCycles('call no #' + num).then(value => { 24 | console.log(value) 25 | }) 26 | }) 27 | } 28 | 29 | // With accumulate=true 30 | { 31 | function squareValues (values) { 32 | return Promise.all(values.map(val => val * val)) 33 | } 34 | 35 | const square = debounce(squareValues, 100, {accumulate: true}); 36 | 37 | [1, 2, 3, 4].forEach(num => { 38 | return square(num).then(value => { 39 | console.log(value) 40 | }) 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Bjørge Næss 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "debounce-promise", 3 | "version": "3.1.2", 4 | "description": "Create a debounced version of a promise returning function", 5 | "main": "dist/index", 6 | "runkitExampleFilename": "./example.js", 7 | "scripts": { 8 | "test": "tap --node-arg -r --node-arg babel-register --node-arg -r --node-arg babel-polyfill test && npm run lint", 9 | "lint": "standard | snazzy", 10 | "compile": "babel index.js --source-maps --out-dir dist", 11 | "clean": "rimraf dist", 12 | "prepare": "in-publish && npm run compile || not-in-publish", 13 | "postpublish": "in-publish && npm run clean || not-in-publish" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/bjoerge/debounce-promise.git" 18 | }, 19 | "keywords": [ 20 | "promise", 21 | "batch", 22 | "accumulate", 23 | "debounce", 24 | "throttle", 25 | "ratelimit" 26 | ], 27 | "author": "Bjørge Næss", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/bjoerge/debounce-promise/issues" 31 | }, 32 | "homepage": "https://github.com/bjoerge/debounce-promise", 33 | "devDependencies": { 34 | "babel-cli": "^6.5.2", 35 | "babel-plugin-transform-async-to-generator": "^6.16.0", 36 | "babel-polyfill": "^6.16.0", 37 | "babel-preset-es2015": "^6.16.0", 38 | "babel-register": "^6.16.3", 39 | "eslint": "^3.8.0", 40 | "in-publish": "^2.0.0", 41 | "rimraf": "^2.5.4", 42 | "snazzy": "^5.0.0", 43 | "standard": "^8.4.0", 44 | "tap": "^7.1.2" 45 | }, 46 | "dependencies": {} 47 | } 48 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* global setTimeout, clearTimeout */ 2 | 3 | module.exports = function debounce (fn, wait = 0, options = {}) { 4 | let lastCallAt 5 | let deferred 6 | let timer 7 | let pendingArgs = [] 8 | return function debounced (...args) { 9 | const currentWait = getWait(wait) 10 | const currentTime = new Date().getTime() 11 | 12 | const isCold = !lastCallAt || (currentTime - lastCallAt) > currentWait 13 | 14 | lastCallAt = currentTime 15 | 16 | if (isCold && options.leading) { 17 | return options.accumulate 18 | ? Promise.resolve(fn.call(this, [args])).then(result => result[0]) 19 | : Promise.resolve(fn.call(this, ...args)) 20 | } 21 | 22 | if (deferred) { 23 | clearTimeout(timer) 24 | } else { 25 | deferred = defer() 26 | } 27 | 28 | pendingArgs.push(args) 29 | timer = setTimeout(flush.bind(this), currentWait) 30 | 31 | if (options.accumulate) { 32 | const argsIndex = pendingArgs.length - 1 33 | return deferred.promise.then(results => results[argsIndex]) 34 | } 35 | 36 | return deferred.promise 37 | } 38 | 39 | function flush () { 40 | const thisDeferred = deferred 41 | clearTimeout(timer) 42 | 43 | Promise.resolve( 44 | options.accumulate 45 | ? fn.call(this, pendingArgs) 46 | : fn.apply(this, pendingArgs[pendingArgs.length - 1]) 47 | ) 48 | .then(thisDeferred.resolve, thisDeferred.reject) 49 | 50 | pendingArgs = [] 51 | deferred = null 52 | } 53 | } 54 | 55 | function getWait (wait) { 56 | return (typeof wait === 'function') ? wait() : wait 57 | } 58 | 59 | function defer () { 60 | const deferred = {} 61 | deferred.promise = new Promise((resolve, reject) => { 62 | deferred.resolve = resolve 63 | deferred.reject = reject 64 | }) 65 | return deferred 66 | } 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # debounce-promise 2 | 3 | [![Build Status](https://travis-ci.org/bjoerge/debounce-promise.svg)](https://travis-ci.org/bjoerge/debounce-promise) 4 | [![Standard - JavaScript Style Guide](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/) 5 | 6 | [![NPM](https://nodei.co/npm/debounce-promise.png)](https://nodei.co/npm/debounce-promise/) 7 | 8 | Create a debounced version of a promise returning function 9 | 10 | ## Install 11 | 12 | npm i -S debounce-promise 13 | 14 | 15 | ## Usage example 16 | 17 | ```js 18 | 19 | var debounce = require('debounce-promise') 20 | 21 | function expensiveOperation(value) { 22 | return Promise.resolve(value) 23 | } 24 | 25 | var saveCycles = debounce(expensiveOperation, 100); 26 | 27 | [1, 2, 3, 4].forEach(num => { 28 | return saveCycles('call no #' + num).then(value => { 29 | console.log(value) 30 | }) 31 | }) 32 | 33 | // Will only call expensiveOperation once with argument `4` and print: 34 | //=> call no #4 35 | //=> call no #4 36 | //=> call no #4 37 | //=> call no #4 38 | ``` 39 | 40 | ### With leading=true 41 | 42 | ```js 43 | var debounce = require('debounce-promise') 44 | 45 | function expensiveOperation(value) { 46 | return Promise.resolve(value) 47 | } 48 | 49 | var saveCycles = debounce(expensiveOperation, 100, {leading: true}); 50 | 51 | [1, 2, 3, 4].forEach(num => { 52 | return saveCycles('call no #' + num).then(value => { 53 | console.log(value) 54 | }) 55 | }) 56 | 57 | //=> call no #1 58 | //=> call no #4 59 | //=> call no #4 60 | //=> call no #4 61 | ``` 62 | 63 | ### With accumulate=true 64 | 65 | ```js 66 | var debounce = require('debounce-promise') 67 | 68 | function squareValues (argTuples) { 69 | return Promise.all(argTuples.map(args => args[0] * args[0])) 70 | } 71 | 72 | var square = debounce(squareValues, 100, {accumulate: true}); 73 | 74 | [1, 2, 3, 4].forEach(num => { 75 | return square(num).then(value => { 76 | console.log(value) 77 | }) 78 | }) 79 | 80 | //=> 1 81 | //=> 4 82 | //=> 9 83 | //=> 16 84 | ``` 85 | 86 | ## Api 87 | `debounce(func, [wait=0], [{leading: true|false, accumulate: true|false})` 88 | 89 | Returns a debounced version of `func` that delays invoking until after `wait` milliseconds. 90 | 91 | Set `leading: true` if you 92 | want to call `func` and return its promise immediately. 93 | 94 | Set `accumulate: true` if you want the debounced function to be called with an array of all the arguments received while waiting. 95 | 96 | Supports passing a function as the `wait` parameter, which provides a way to lazily or dynamically define a wait timeout. 97 | 98 | 99 | ## Example timeline illustration 100 | 101 | ```js 102 | function refresh() { 103 | return fetch('/my/api/something') 104 | } 105 | const debounced = debounce(refresh, 100) 106 | ``` 107 | 108 | ``` 109 | time (ms) -> 0 --- 10 --- 50 --- 100 --- 110 | ----------------------------------------------- 111 | debounced() | --- P(1) --- P(1) --- P(1) --- 112 | refresh() | --------------------- P(1) --- 113 | ``` 114 | 115 | ```js 116 | const debounced = debounce(refresh, 100, {leading: true}) 117 | ``` 118 | ``` 119 | time (ms) -> 0 --- 10 --- 50 --- 100 --- 110 --- 120 | -------------------------------------------------------- 121 | debounced() | --- P(1) --- P(2) --- P(2) --- P(2) --- 122 | refresh() | --- P(1) --------------------- P(2) --- 123 | ``` 124 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /* global setTimeout */ 2 | import {test} from 'tap' 3 | import debounce from '../index' 4 | 5 | function sleep (ms) { 6 | return new Promise(resolve => setTimeout(resolve, ms)) 7 | } 8 | 9 | test('returns the result of a single operation ', async t => { 10 | const debounced = debounce(async (value) => value, 100) 11 | const promise = debounced('foo') 12 | const result = await promise 13 | 14 | t.equal(result, 'foo') 15 | }) 16 | 17 | test('returns the result of the latest operation ', async t => { 18 | const debounced = debounce(async (value) => value, 100) 19 | const promises = ['foo', 'bar', 'baz', 'qux'].map(debounced) 20 | const results = await Promise.all(promises) 21 | 22 | t.deepEqual(results, ['qux', 'qux', 'qux', 'qux']) 23 | }) 24 | 25 | test('if leading=true, the value from the first promise is used', async t => { 26 | const debounced = debounce(async (value) => value, 100, {leading: true}) 27 | const promises = ['foo', 'bar', 'baz', 'qux'].map(debounced) 28 | const results = await Promise.all(promises) 29 | 30 | t.deepEqual(results, ['foo', 'qux', 'qux', 'qux']) 31 | }) 32 | 33 | test('do not call the given function repeatedly', async t => { 34 | let callCount = 0 35 | const debounced = debounce(async () => callCount++, 100) 36 | await Promise.all([1, 2, 3, 4].map(debounced)) 37 | t.equal(callCount, 1) 38 | }) 39 | 40 | test('does not call the given function again after the timeout when leading=true if executed only once', async t => { 41 | let callCount = 0 42 | const debounced = debounce(async () => callCount++, 100, {leading: true}) 43 | await debounced() 44 | await sleep(200) 45 | t.equal(callCount, 1) 46 | }) 47 | 48 | test('calls the given function again after the timeout when leading=true if executed multiple times', async t => { 49 | let callCount = 0 50 | const debounced = debounce(async () => callCount++, 100, {leading: true}) 51 | await Promise.all([1, 2, 3, 4].map(debounced)) 52 | await sleep(200) 53 | t.equal(callCount, 2) 54 | }) 55 | 56 | test('waits until the wait time has passed', async t => { 57 | let callCount = 0 58 | const debounced = debounce(async () => callCount++, 10) 59 | debounced() 60 | debounced() 61 | debounced() 62 | t.equal(callCount, 0) 63 | await sleep(20) 64 | t.equal(callCount, 1) 65 | }) 66 | 67 | test('supports passing function as wait parameter', async t => { 68 | let callCount = 0 69 | let getWaitCallCount = 0 70 | const debounced = debounce(async () => callCount++, () => { 71 | getWaitCallCount++ 72 | return 100 73 | }) 74 | debounced() 75 | debounced() 76 | debounced() 77 | await sleep(90) 78 | t.equal(callCount, 0) 79 | await sleep(20) 80 | t.inequal(getWaitCallCount, 0) 81 | t.equal(callCount, 1) 82 | }) 83 | 84 | test('calls the given function again if wait time has passed', async t => { 85 | let callCount = 0 86 | const debounced = debounce(async () => callCount++, 10) 87 | debounced() 88 | 89 | await sleep(20) 90 | t.equal(callCount, 1) 91 | 92 | debounced() 93 | 94 | await sleep(20) 95 | t.equal(callCount, 2) 96 | }) 97 | 98 | test('maintains the context of the original function', async t => { 99 | const context = { 100 | foo: 1, 101 | debounced: debounce(async function () { 102 | await this.foo++ 103 | }, 10) 104 | } 105 | 106 | context.debounced() 107 | 108 | await sleep(20) 109 | t.equal(context.foo, 2) 110 | }) 111 | 112 | test('maintains the context of the original function when leading=true', async t => { 113 | const context = { 114 | foo: 1, 115 | debounced: debounce(async function () { 116 | await this.foo++ 117 | }, 10, {leading: true}) 118 | } 119 | 120 | await context.debounced() 121 | 122 | t.equal(context.foo, 2) 123 | }) 124 | 125 | test('Converts the return value from the producer function to a promise', async t => { 126 | let callCount = 0 127 | const debounced = debounce(() => ++callCount, 10) 128 | 129 | debounced() 130 | debounced() 131 | await debounced() 132 | 133 | t.equal(callCount, 1) 134 | }) 135 | 136 | test('calls debounced function and accumulates arguments', async t => { 137 | function squareBatch (args) { 138 | t.deepEqual(args, [[1], [2], [3]]) 139 | return Promise.resolve(args.map(arg => arg * arg)) 140 | } 141 | 142 | const square = debounce(squareBatch, 10, {accumulate: true}) 143 | 144 | const one = square(1) 145 | const two = square(2) 146 | const three = square(3) 147 | 148 | await sleep(20) 149 | 150 | t.equal(await one, 1) 151 | t.equal(await two, 4) 152 | t.equal(await three, 9) 153 | }) 154 | 155 | test('accumulate works with leading=true', async t => { 156 | let callNo = 1 157 | function squareBatch (args) { 158 | if (callNo === 1) { 159 | t.deepEqual(args, [[1]]) 160 | } 161 | if (callNo === 2) { 162 | t.deepEqual(args, [[2], [3]]) 163 | } 164 | callNo++ 165 | return Promise.resolve(args.map(arg => arg * arg)) 166 | } 167 | 168 | const square = debounce(squareBatch, 10, {leading: true, accumulate: true}) 169 | 170 | const one = square(1) 171 | const two = square(2) 172 | const three = square(3) 173 | 174 | await sleep(20) 175 | 176 | t.equal(await one, 1) 177 | t.equal(await two, 4) 178 | t.equal(await three, 9) 179 | }) 180 | 181 | test('accumulate works with non-promise return value', async t => { 182 | let callNo = 1 183 | function squareBatch (args) { 184 | if (callNo === 1) { 185 | t.deepEqual(args, [[1]]) 186 | } 187 | if (callNo === 2) { 188 | t.deepEqual(args, [[2], [3]]) 189 | } 190 | callNo++ 191 | return args.map(arg => arg * arg) 192 | } 193 | 194 | const square = debounce(squareBatch, 10, {leading: true, accumulate: true}) 195 | 196 | const one = square(1) 197 | const two = square(2) 198 | const three = square(3) 199 | 200 | await sleep(20) 201 | 202 | t.equal(await one, 1) 203 | t.equal(await two, 4) 204 | t.equal(await three, 9) 205 | }) 206 | --------------------------------------------------------------------------------