├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── index.d.ts ├── license ├── package.json ├── priority.d.ts ├── readme.md ├── src ├── priority.js └── single.js └── test ├── priority.js ├── single.js └── utils └── index.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = 2 5 | indent_style = tab 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.{json,yml}] 12 | indent_style = space 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: push 4 | 5 | jobs: 6 | test: 7 | name: Node.js v${{ matrix.nodejs }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | nodejs: [8, 10, 12, 14] 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/setup-node@v1 15 | with: 16 | node-version: ${{ matrix.nodejs }} 17 | 18 | - name: Install 19 | run: | 20 | npm install 21 | npm install -g nyc 22 | 23 | - name: Test w/ Coverage 24 | run: nyc --include=src npm test 25 | 26 | - name: Report 27 | if: matrix.nodejs >= 14 28 | run: | 29 | nyc report --reporter=text-lcov > coverage.lcov 30 | bash <(curl -s https://codecov.io/bash) 31 | env: 32 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .nyc_output 3 | .DS_Store 4 | *-lock.* 5 | *.lock 6 | *.log 7 | 8 | /priority 9 | /dist 10 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'throttles' { 2 | type isDone = () => void; 3 | type toAdd = (fn: Function) => void; 4 | function throttles(limit?: number): [toAdd, isDone]; 5 | export = throttles; 6 | } 7 | 8 | declare module 'throttles/priority' { 9 | type isDone = () => void; 10 | type toAdd = (fn: Function, isHigh?: boolean) => void; 11 | function throttles(limit?: number): [toAdd, isDone]; 12 | export = throttles; 13 | } 14 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Luke Edwards (lukeed.com) 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "throttles", 3 | "version": "1.0.1", 4 | "repository": "lukeed/throttles", 5 | "description": "A tiny (139B to 204B) utility to regulate the execution rate of your functions", 6 | "unpkg": "dist/index.min.js", 7 | "module": "dist/index.mjs", 8 | "main": "dist/index.js", 9 | "types": "index.d.ts", 10 | "license": "MIT", 11 | "files": [ 12 | "index.d.ts", 13 | "priority", 14 | "dist" 15 | ], 16 | "modes": { 17 | "default": "src/single.js", 18 | "priority": "src/priority.js" 19 | }, 20 | "scripts": { 21 | "build": "bundt", 22 | "pretest": "npm run build", 23 | "test": "uvu -r esm test -i utils", 24 | "postbuild": "cp priority.d.ts priority/index.d.ts" 25 | }, 26 | "author": { 27 | "name": "Luke Edwards", 28 | "email": "luke.edwards05@gmail.com", 29 | "url": "https://lukeed.com" 30 | }, 31 | "engines": { 32 | "node": ">=6" 33 | }, 34 | "devDependencies": { 35 | "bundt": "1.0.1", 36 | "esm": "3.2.25", 37 | "uvu": "0.0.14" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /priority.d.ts: -------------------------------------------------------------------------------- 1 | type isDone = () => void; 2 | type toAdd = (fn: Function, isHigh?: boolean) => void; 3 | declare function throttles(limit?: number): [toAdd, isDone]; 4 | export default throttles; 5 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # throttles [![build status](https://badgen.now.sh/github/status/lukeed/throttles)](https://github.com/lukeed/throttles/actions) [![codecov](https://badgen.now.sh/codecov/c/github/lukeed/throttles)](https://codecov.io/gh/lukeed/throttles) 2 | 3 | > A tiny (139B to 204B) utility to regulate the execution rate of your functions 4 | 5 | 6 | ## Install 7 | 8 | ``` 9 | $ npm install --save throttles 10 | ``` 11 | 12 | 13 | ## Modes 14 | 15 | There are two "versions" of `throttles`, each of which different purpose: 16 | 17 | #### "single" 18 | > **Size (gzip):** 139 bytes
19 | > **Availability:** [UMD](https://unpkg.com/throttles), [CommonJS](https://unpkg.com/throttles/dist/index.js), [ES Module](https://unpkg.com/throttles?module) 20 | 21 | This is the primary/default mode, meant for managing single queues. 22 | 23 | #### "priority" 24 | > **Size (gzip):** 204 bytes
25 | > **Availability:** [UMD](https://unpkg.com/throttles/priority), [ES Module](https://unpkg.com/throttles/priority/index.mjs) 26 | 27 | This is the opt-in mode, meant for managing a low priority _and_ a high priority queue system.
28 | Items within the "high priority" queue are handled before the low/general queue. The `limit` is still enforced. 29 | 30 | 31 | ## Usage 32 | 33 | ***Selecting a Mode*** 34 | 35 | ```js 36 | // import via npm module 37 | import throttles from 'throttles'; 38 | import throttles from 'throttles/priority'; 39 | 40 | // import via unpkg 41 | import throttles from 'https://unpkg.com/throttles/index.mjs'; 42 | import throttles from 'https://unpkg.com/throttles/priority/index.mjs'; 43 | ``` 44 | 45 | ***Example Usage*** 46 | 47 | ```js 48 | import throttles from 'throttles'; 49 | 50 | const API = 'https://pokeapi.co/api/v2/pokemon'; 51 | const getPokemon = id => fetch(`${API}/${id}`).then(r => r.json()); 52 | 53 | // Limit concurrency to 3 54 | const [toAdd, isDone] = throttles(3); 55 | 56 | // What we'll fetch 57 | const pokemon = ['bulbasaur', 'ivysaur', 'venusaur', 'charmander', 'charmeleon', 'charizard', ...]; 58 | 59 | // Loop list, enqueuing each Pokemon 60 | // ~> Always keeps 3 requests active at a time 61 | // ~> When complete, marks itself complete via `isDone()` 62 | pokemon.forEach(name => { 63 | toAdd(() => { 64 | getPokemon(name).then(isDone); 65 | }); 66 | }); 67 | 68 | // Or, use `Array.map` to wrap our `getPokemon` function 69 | // ~> This still fetches Pokemon 3 at once 70 | pokemon.map(x => () => getPokemon(x).then(isDone)).forEach(toAdd); 71 | ``` 72 | 73 | 74 | ## API 75 | 76 | ### throttles(limit) 77 | Returns: `Array` 78 | 79 | Returns a tuple of [[`toAdd`](#toaddfn-ishigh), [`isDone`](#isdone)] actions. 80 | 81 | #### limit 82 | Type: `Number`
83 | Default: `1` 84 | 85 | The throttle's concurrency limit. By default, runs your functions one at a time. 86 | 87 | 88 | ### toAdd(fn[, isHigh]) 89 | Type: `Function`
90 | Returns: `void` 91 | 92 | Add a function to the throttle's queue. 93 | 94 | > **Important:** In "priority" mode, identical functions are ignored. 95 | 96 | #### fn 97 | Type: `Function`
98 | The function to add to the queue. 99 | 100 | #### isHigh 101 | Type: `Boolean`
102 | Default: `false`
103 | If the `fn` should be added to the "high priority" queue. 104 | 105 | > **Important:** Only available in "priority" mode! 106 | 107 | 108 | ### isDone 109 | Type: `Function`
110 | Returns: `void` 111 | 112 | Signifies that a function has been completed. 113 | 114 | > **Important:** Failure to call this will prevent `throttles` from continuing to the next item! 115 | 116 | 117 | ## License 118 | 119 | MIT © [Luke Edwards](https://lukeed.com) 120 | -------------------------------------------------------------------------------- /src/priority.js: -------------------------------------------------------------------------------- 1 | export default function (limit) { 2 | limit = limit || 1; 3 | var qlow=[], idx, qhigh=[], sum=0, wip=0; 4 | 5 | function toAdd(fn, isHigh) { 6 | if (fn.__t) { 7 | if (isHigh) { 8 | idx = qlow.indexOf(fn); 9 | // must decrement (increments again) 10 | if (!!~idx) qlow.splice(idx, 1).length && sum--; 11 | } else return; 12 | } 13 | fn.__t = 1; 14 | (isHigh ? qhigh : qlow).push(fn); 15 | sum++ || run(); // initializes if 1st 16 | } 17 | 18 | function isDone() { 19 | wip--; // make room for next 20 | run(); 21 | } 22 | 23 | function run() { 24 | if (wip < limit && sum > 0) { 25 | (qhigh.shift() || qlow.shift())(); 26 | sum--; wip++; // is now WIP 27 | } 28 | } 29 | 30 | return [toAdd, isDone]; 31 | } 32 | -------------------------------------------------------------------------------- /src/single.js: -------------------------------------------------------------------------------- 1 | export default function (limit) { 2 | limit = limit || 1; 3 | var queue=[], wip=0; 4 | 5 | function toAdd(fn) { 6 | queue.push(fn) > 1 || run(); // initializes if 1st 7 | } 8 | 9 | function isDone() { 10 | wip--; // make room for next 11 | run(); 12 | } 13 | 14 | function run() { 15 | if (wip < limit && queue.length > 0) { 16 | queue.shift()(); wip++; // is now WIP 17 | } 18 | } 19 | 20 | return [toAdd, isDone]; 21 | } 22 | -------------------------------------------------------------------------------- /test/priority.js: -------------------------------------------------------------------------------- 1 | import { test } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import { sleep, inRange, timer } from './utils'; 4 | import throttle from '../src/priority'; 5 | 6 | test('exports', () => { 7 | assert.type(throttle, 'function'); 8 | }); 9 | 10 | test('returns', () => { 11 | const out = throttle(); 12 | assert.ok(Array.isArray(out), 'returns an Array'); 13 | assert.is(out.length, 2, '~> has two items'); 14 | 15 | out.forEach(item => { 16 | assert.type(item, 'function'); 17 | }); 18 | }); 19 | 20 | test('usage :: discard repeats', async () => { 21 | let last, num=5, step=500; 22 | const [toAdd, isDone] = throttle(); 23 | 24 | const t1 = timer(); 25 | const toSave = () => last = t1(); 26 | const demo = () => sleep(step).then(toSave).then(isDone); 27 | Array.from({ length: num }, () => toAdd(demo)); 28 | 29 | await sleep(++num * step); 30 | 31 | const bool = inRange(last, 500); 32 | assert.ok(bool, '~> ONLY RAN 1 ITEM'); 33 | }); 34 | 35 | test('usage :: default limit', async () => { 36 | let last, num=5, step=500; 37 | const [toAdd, isDone] = throttle(); 38 | 39 | const t1 = timer(); 40 | const toSave = () => last = t1(); 41 | const demo = () => sleep(step).then(toSave).then(isDone); 42 | Array.from({ length: num }, () => toAdd(() => demo())); // new function per add 43 | 44 | await sleep(++num * step); 45 | 46 | const bool = inRange(last, 2500); 47 | assert.ok(bool, '~> ran 1 at a time'); 48 | 49 | }); 50 | 51 | test('usage :: custom limit', async () => { 52 | let last, num=10, step=500; 53 | const [toAdd, isDone] = throttle(5); 54 | 55 | const t1 = timer(); 56 | const toSave = () => last = t1(); 57 | const demo = () => sleep(step).then(toSave).then(isDone); 58 | Array.from({ length: num }, () => toAdd(() => demo())); // new function per add 59 | 60 | await sleep(++num * step); 61 | 62 | const bool = inRange(last, 1000); 63 | assert.ok(bool, '~> ran 5 at a time'); 64 | }); 65 | 66 | test('isHigh :: init', async () => { 67 | let plan = 0; 68 | let last, low=0, num=3, step=500; 69 | const [toAdd, isDone] = throttle(1); 70 | 71 | const t1 = timer(); 72 | 73 | const toLow = () => (last = t1(), low++); 74 | const isLow = () => sleep(step).then(toLow).then(isDone); 75 | 76 | const isHigh = () => sleep(step).then(() => { 77 | assert.ok(low <= 1, '~> high-priority item ran before low-priotity queue finished'); 78 | last = t1(); 79 | isDone(); 80 | plan++; 81 | }); 82 | 83 | // Add 3 low-priority items first 84 | Array.from({ length: num }, () => { 85 | toAdd(() => isLow()); // new function per add 86 | }); 87 | 88 | // Then add 3 high-priority items 89 | Array.from({ length: num }, () => { 90 | toAdd(() => isHigh(), true); // new function per add 91 | }); 92 | 93 | await sleep((2*num + 1) * step); 94 | assert.is(low, 3, '~> eventually ran all low-priority items'); 95 | 96 | const bool = inRange(last, 3000); 97 | assert.ok(bool, '~> ran 1 at a time'); 98 | 99 | assert.is(plan, 3); 100 | }); 101 | 102 | test('isHigh :: upgrade', async () => { 103 | let plan = 0; 104 | let last, low=0, high=0, step=500; 105 | const [toAdd, isDone] = throttle(1); 106 | 107 | // Make sure right functions are called 108 | let aaa=0, bbb=0, ccc=0, ddd=0, eee=0; 109 | 110 | const t1 = timer(); 111 | const toLow = () => (last = t1(), low++); 112 | 113 | // [isLow, isLow, isLow, isLow, isHigh] 114 | const items = [ 115 | () => sleep(step).then(() => aaa=1).then(toLow).then(isDone), 116 | () => sleep(step).then(() => bbb=1).then(toLow).then(isDone), 117 | () => sleep(step).then(() => ccc=1).then(toLow).then(isDone), 118 | () => sleep(step).then(() => ddd=1).then(toLow).then(isDone), 119 | () => sleep(step).then(() => eee=1).then(() => { 120 | assert.ok(low <= 1, '~> high-priority item ran before low-priotity queue finished'); 121 | last = t1(); 122 | isDone(); 123 | high++; 124 | plan++; 125 | }) 126 | ]; 127 | 128 | // Add all `items` as low-priority 129 | items.forEach(fn => toAdd(fn)); 130 | 131 | // Re-add the last item as high-priority – upgrades! 132 | toAdd(items[4], true); 133 | 134 | await sleep(6 * step); 135 | 136 | assert.is(low, 4, '~> ran 4 items as low-priority'); 137 | assert.is(high, 1, '~> ran 1 item as high-priority'); 138 | 139 | assert.is(aaa, 1, '~> ran the "aaa" function'); 140 | assert.is(bbb, 1, '~> ran the "bbb" function'); 141 | assert.is(ccc, 1, '~> ran the "ccc" function'); 142 | assert.is(ddd, 1, '~> ran the "ddd" function'); 143 | assert.is(eee, 1, '~> ran the "eee" function'); 144 | 145 | const bool = inRange(last, 2500); 146 | assert.ok(bool, '~> ran 1 at a time'); 147 | 148 | assert.is(plan, 1); 149 | }); 150 | 151 | 152 | test('isHigh :: but unseen!', async () => { 153 | let plan = 0; 154 | let last, low=0, high=0, step=500; 155 | const [toAdd, isDone] = throttle(1); 156 | 157 | // Make sure right functions are called 158 | let aaa=0, bbb=0, ccc=0, ddd=0; 159 | 160 | const t1 = timer(); 161 | const toLow = () => (last = t1(), low++); 162 | 163 | // [isLow, isLow, isLow] 164 | const items = [ 165 | () => sleep(step).then(() => aaa=1).then(toLow).then(isDone), 166 | () => sleep(step).then(() => bbb=1).then(toLow).then(isDone), 167 | () => sleep(step).then(() => ccc=1).then(toLow).then(isDone), 168 | ]; 169 | 170 | const custom = () => { 171 | return sleep(step).then(() => ddd=1).then(() => { 172 | assert.ok(low <= 1, '~> high-priority item ran before low-priotity queue finished'); 173 | last = t1(); 174 | isDone(); 175 | high++; 176 | plan++; 177 | }); 178 | }; 179 | 180 | custom.__t = 1; 181 | 182 | // Add all `items` as low-priority 183 | items.forEach(fn => toAdd(fn)); 184 | 185 | // Add a high-priority item we've not seen before 186 | // but that happens to have same minification property (unlikely, but hey) 187 | toAdd(custom, true); 188 | 189 | await sleep(5 * step); 190 | 191 | assert.is(low, 3, '~> ran 3 items as low-priority'); 192 | assert.is(high, 1, '~> ran 1 item as high-priority'); 193 | 194 | assert.is(aaa, 1, '~> ran the "aaa" function'); 195 | assert.is(bbb, 1, '~> ran the "bbb" function'); 196 | assert.is(ccc, 1, '~> ran the "ccc" function'); 197 | assert.is(ddd, 1, '~> ran the "ddd" function'); 198 | 199 | const bool = inRange(last, 2000); 200 | assert.ok(bool, '~> ran 1 at a time'); 201 | 202 | assert.is(plan, 1); 203 | }); 204 | 205 | test.run(); 206 | -------------------------------------------------------------------------------- /test/single.js: -------------------------------------------------------------------------------- 1 | import { test } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import { sleep, inRange, timer } from './utils'; 4 | import throttle from '../src/single'; 5 | 6 | test('exports', () => { 7 | assert.type(throttle, 'function'); 8 | }); 9 | 10 | test('returns', () => { 11 | const out = throttle(); 12 | assert.ok(Array.isArray(out), 'returns an Array'); 13 | assert.is(out.length, 2, '~> has two items'); 14 | 15 | out.forEach(item => { 16 | assert.type(item, 'function'); 17 | }); 18 | }); 19 | 20 | test('usage :: default limit', async () => { 21 | let last, num=5, step=500; 22 | const [toAdd, isDone] = throttle(); 23 | 24 | const t1 = timer(); 25 | const toSave = () => last = t1(); 26 | const demo = () => sleep(step).then(toSave).then(isDone); 27 | Array.from({ length: num }, () => toAdd(demo)); 28 | 29 | await sleep(++num * step); 30 | 31 | const bool = inRange(last, 2500); 32 | assert.ok(bool, '~> ran 1 at a time'); 33 | }); 34 | 35 | test('usage :: custom limit', async () => { 36 | let last, num=10, step=500; 37 | const [toAdd, isDone] = throttle(5); 38 | 39 | const t1 = timer(); 40 | const toSave = () => last = t1(); 41 | const demo = () => sleep(step).then(toSave).then(isDone); 42 | Array.from({ length: num }, () => toAdd(demo)); 43 | 44 | await sleep(++num * step); 45 | 46 | const bool = inRange(last, 1000); 47 | assert.ok(bool, '~> ran 5 at a time'); 48 | }); 49 | 50 | test.run(); 51 | -------------------------------------------------------------------------------- /test/utils/index.js: -------------------------------------------------------------------------------- 1 | export function toDuration(arr) { 2 | return (arr[0]*1e3 + arr[1]/1e6).toFixed(2); 3 | } 4 | 5 | export function timer() { 6 | const start = process.hrtime(); 7 | return () => toDuration(process.hrtime(start)); 8 | } 9 | 10 | export function sleep(ms) { 11 | return new Promise(r => setTimeout(r, ms)); 12 | } 13 | 14 | export function inRange(str, min, extra = 0.02) { 15 | console.log(`~> completed in ${str}ms`); 16 | 17 | const num = parseInt(str, 10); 18 | const max = min * (1 + extra); 19 | 20 | return num >= min && num < max; 21 | } 22 | --------------------------------------------------------------------------------