diff --git a/.gitignore b/.gitignore index 5d79a3d..8418d1d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,10 +3,8 @@ node_modules/ package-lock.json yarn.lock npm-debug.log +browserstack.err local.log -index.cjs.js -index.esm.js -index.umd.js -index.cjs.js.map -index.esm.js.map -index.umd.js.map +cjs/ +esm/ +umd/ diff --git a/.lintstagedrc b/.lintstagedrc index 29448df..7535ab7 100644 --- a/.lintstagedrc +++ b/.lintstagedrc @@ -1,8 +1,7 @@ { - "*.js": ["eslint --fix", "git add"], - "*.md": ["prettier --ignore-path .gitignore --write", "git add"], + "*.js": ["eslint --fix"], + "*.md": ["prettier --ignore-path .gitignore --write"], ".!(npm|browserslist)*rc": [ - "prettier --ignore-path .gitignore --parser json --write", - "git add" + "prettier --ignore-path .gitignore --parser json --write" ] } diff --git a/.prettierrc b/.prettierrc index ba1a355..a019b0b 100644 --- a/.prettierrc +++ b/.prettierrc @@ -15,5 +15,6 @@ "proseWrap": "always", "htmlWhitespaceSensitivity": "css", "vueIndentScriptAndStyle": false, - "endOfLine": "lf" + "endOfLine": "lf", + "embeddedLanguageFormatting": "auto" } diff --git a/.travis.yml b/.travis.yml index b3be97a..d2442b6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,3 @@ language: node_js node_js: - - '8' + - '10' diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a6ff9f..5b78bea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ - Avoid having bundles like Webpack load the UMD module ([#42](https://github.com/niksy/throttle-debounce/pull/42)) +- Upgrade package + +### Removed + +- **Breaking**: Drop Node 8 support, package is no longer tested against it, + but it should still work since there are no code changes ## [2.3.0][] - 2020-08-12 @@ -24,7 +30,6 @@ ([#36](https://github.com/niksy/throttle-debounce/pull/36)) - Use ES2015+ features -[unreleased]: https://github.com/niksy/throttle-debounce/compare/v2.2.1...HEAD [2.2.1]: https://github.com/niksy/throttle-debounce/tree/v2.2.1 -[unreleased]: https://github.com/niksy/throttle-debounce/compare/v2.3.0...HEAD [2.3.0]: https://github.com/niksy/throttle-debounce/tree/v2.3.0 +[unreleased]: https://github.com/niksy/throttle-debounce/compare/v2.3.0...HEAD diff --git a/debounce.js b/debounce.js index 3add5f6..3eaed5b 100644 --- a/debounce.js +++ b/debounce.js @@ -16,7 +16,7 @@ import throttle from './throttle'; * * @returns {Function} A new, debounced function. */ -export default function(delay, atBegin, callback) { +export default function (delay, atBegin, callback) { return callback === undefined ? throttle(delay, atBegin, false) : throttle(delay, callback, atBegin !== false); diff --git a/karma.conf.js b/karma.conf.js index e995220..237e31c 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -4,9 +4,14 @@ const path = require('path'); let config; -const local = - typeof process.env.CI === 'undefined' || process.env.CI === 'false'; -const port = process.env.SERVICE_PORT; +const isCI = + typeof process.env.CI !== 'undefined' && process.env.CI !== 'false'; +const isPR = + typeof process.env.TRAVIS_PULL_REQUEST !== 'undefined' && + process.env.TRAVIS_PULL_REQUEST !== 'false'; +const local = !isCI || (isCI && isPR); + +const port = 0; if (local) { config = { @@ -57,7 +62,7 @@ if (local) { }; } -module.exports = function(baseConfig) { +module.exports = function (baseConfig) { baseConfig.set({ basePath: '', frameworks: ['qunit'], diff --git a/package.json b/package.json index 44056c3..2db76ad 100644 --- a/package.json +++ b/package.json @@ -2,86 +2,94 @@ "name": "throttle-debounce", "version": "2.3.0", "description": "Throttle and debounce functions.", - "main": "index.cjs.js", - "module": "index.esm.js", - "unpkg": "index.umd.js", - "jsdelivr": "index.umd.js", + "license": "MIT", "author": "Ivan Nikolić (http://ivannikolic.com)", "contributors": [ "Ben Alman (http://benalman.com)" ], - "license": "MIT", + "sideEffects": false, + "exports": { + ".": { + "import": "./esm/index.js", + "require": "./cjs/index.js" + } + }, + "main": "cjs/index.js", + "jsdelivr": "umd/index.js", + "unpkg": "umd/index.js", + "module": "esm/index.js", + "directories": { + "test": "test" + }, "files": [ - "index.cjs.{js,js.map}", - "index.esm.{js,js.map}", - "index.umd.{js,js.map}", + "cjs/", + "esm/", + "umd/", "CHANGELOG.md", "LICENSE.md", "README.md" ], - "sideEffects": false, - "directories": { - "test": "test" - }, "scripts": { "build": "rollup --config rollup.config.js", "lint": "eslint '{index,debounce,throttle,test/**/*}.js'", - "postpublish": "GITHUB_TOKEN=$GITHUB_RELEASE_TOKEN echo 'github-release-from-changelog'", - "prepublishOnly": "npm run build", - "release": "np", + "module-check": "node -e 'require(\"throttle-debounce\");' && node --input-type=module -e 'import \"throttle-debounce\";'", + "prepublishOnly": "npm run build && npm run module-check", + "postpublish": "GITHUB_TOKEN=$GITHUB_RELEASE_TOKEN github-release-from-changelog", + "release": "np --no-release-draft", "test": "npm run lint && npm run test:automated", - "test:automated": "BABEL_ENV=test SERVICE_PORT=$(get-port) karma start", + "test:automated": "BABEL_ENV=test karma start", "test:automated:watch": "npm run test:automated -- --auto-watch --no-single-run", "version": "version-changelog CHANGELOG.md && changelog-verify CHANGELOG.md && git add CHANGELOG.md" }, - "dependencies": {}, "devDependencies": { "@babel/cli": "^7.2.3", "@babel/core": "^7.2.2", "@babel/plugin-transform-object-assign": "^7.2.0", "@babel/plugin-transform-runtime": "^7.2.0", "@babel/runtime": "^7.2.0", - "babel-loader": "^8.0.4", + "@rollup/plugin-babel": "^5.2.1", + "babel-loader": "^8.1.0", "babel-preset-niksy": "^4.1.0", "changelog-verify": "^1.1.2", "core-js": "^2.6.5", - "eslint": "^6.7.2", - "eslint-config-niksy": "^8.0.0", - "eslint-config-prettier": "^4.2.0", + "eslint": "^7.11.0", + "eslint-config-niksy": "^9.0.0", + "eslint-config-prettier": "^6.14.0", "eslint-plugin-extend": "^0.1.1", - "eslint-plugin-import": "^2.18.2", - "eslint-plugin-jsdoc": "^18.4.3", - "eslint-plugin-mocha": "^6.2.2", - "eslint-plugin-node": "^10.0.0", + "eslint-plugin-import": "^2.22.1", + "eslint-plugin-jsdoc": "^30.7.3", + "eslint-plugin-mocha": "^8.0.0", + "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^3.0.1", - "eslint-plugin-promise": "^4.2.1", + "eslint-plugin-promise": "^4.1.1", "eslint-plugin-react": "^7.9.1", - "eslint-plugin-unicorn": "^14.0.1", + "eslint-plugin-unicorn": "^23.0.0", "esm": "^3.0.51", "get-port": "^4.0.0", "get-port-cli": "^2.0.0", "github-release-from-changelog": "^2.1.1", - "husky": "^3.1.0", - "karma": "^4.0.1", - "karma-browserstack-launcher": "^1.0.0", - "karma-chrome-launcher": "^2.2.0", + "husky": "^4.3.0", + "karma": "^5.2.3", + "karma-browserstack-launcher": "^1.6.0", + "karma-chrome-launcher": "^3.1.0", "karma-firefox-launcher": "^0.1.7", "karma-mocha-reporter": "^2.2.5", "karma-qunit": "^0.1.9", "karma-sourcemap-loader": "^0.3.7", - "karma-webpack": "^3.0.0", - "lint-staged": "^9.5.0", + "karma-webpack": "^4.0.2", + "lint-staged": "^10.4.2", "minimist": "^1.2.0", - "np": "^3.0.4", - "prettier": "^1.17.0", + "mocha": "^4.1.0", + "np": "^6.5.0", + "prettier": "^2.1.2", "qunitjs": "^1.23.1", - "rollup": "^1.0.0", + "rollup": "^2.32.1", "rollup-plugin-babel": "^4.2.0", "version-changelog": "^3.1.1", - "webpack": "^4.12.0" + "webpack": "^4.44.2" }, "engines": { - "node": ">=8" + "node": ">=10" }, "keywords": [ "debounce", diff --git a/rollup.config.js b/rollup.config.js index cfd14a9..39e007c 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,29 +1,61 @@ 'use strict'; -const babel = require('rollup-plugin-babel'); +const path = require('path'); +const { promises: fs } = require('fs'); +const { default: babel } = require('@rollup/plugin-babel'); module.exports = { input: 'index.js', output: [ { - file: 'index.cjs.js', + file: 'cjs/index.js', format: 'cjs', sourcemap: true }, { - file: 'index.esm.js', + file: 'esm/index.js', format: 'esm', sourcemap: true }, { - file: 'index.umd.js', + file: 'umd/index.js', format: 'umd', sourcemap: true, name: 'throttleDebounce' } ], plugins: [ + (() => { + return { + name: 'package-type', + async writeBundle(output) { + let prefix, type; + if (output.file.includes('cjs/')) { + prefix = 'cjs'; + type = 'commonjs'; + } else if (output.file.includes('esm/')) { + prefix = 'esm'; + type = 'module'; + } else if (output.file.includes('umd/')) { + prefix = 'umd'; + type = 'commonjs'; + } + if (typeof prefix !== 'undefined') { + const package_ = path.join(prefix, 'package.json'); + try { + await fs.unlink(package_); + } catch (error) {} + await fs.writeFile( + package_, + JSON.stringify({ type }), + 'utf8' + ); + } + } + }; + })(), babel({ + babelHelpers: 'bundled', exclude: 'node_modules/**' }) ] diff --git a/test/.eslintrc b/test/.eslintrc index 2a48f13..9df0a25 100644 --- a/test/.eslintrc +++ b/test/.eslintrc @@ -1,5 +1,4 @@ { - "extends": [ - "niksy/tests" - ] + "extends": ["niksy/tests"], + "ignorePatterns": ["/**/fixtures"] } diff --git a/test/index.js b/test/index.js index e200b3d..2e1205b 100644 --- a/test/index.js +++ b/test/index.js @@ -1,319 +1,402 @@ -/* jshint ignore:start */ -/* eslint-disable */ +/* eslint-disable mocha/no-identical-title */ /* Original QUnit test: https://github.com/cowboy/jquery-throttle-debounce/blob/master/unit/unit.js */ -import { module, test, expect, ok, equal as equals, start, stop } from 'qunitjs'; +import { + module, + test, + expect, + ok, + equal as equals, + start, + stop +} from 'qunitjs'; import throttle from '../throttle'; import debounce from '../debounce'; -QUnit.config.autostart = false; +window.QUnit.config.autostart = false; -var pause = 500, - delay = 100; +let pause = 500; +let delay = 100; -function exec_many_times( each, complete ) { - var i = 0, - repeated, - id; +function execManyTimes(each, complete) { + let index = 0; + let repeated, id; - function start(){ - id = setInterval(function(){ + function start() { + id = setInterval(function () { each(); - if ( ++i === 50 ) { - clearInterval( id ); - complete( repeated ? null : function(){ - i = 0; - repeated = true; - setTimeout( start, pause ); - }); + if (++index === 50) { + clearInterval(id); + complete( + repeated + ? null + : function () { + index = 0; + repeated = true; + setTimeout(start, pause); + } + ); } }, 20); } - setTimeout( start, pause ); -}; + setTimeout(start, pause); +} -module( 'throttle' ); +module('throttle'); -test( 'delay, callback', function() { - expect( 7 ); +test('delay, callback', function () { + expect(7); stop(); - var start_time, - i = 0, - arr = [], - fn = function( now ){ - arr.push( now - this ) + let startTime; + let index = 0; + let array = []; + let function_ = function (now) { + array.push(now - this); + }; + let throttled = throttle(delay, function_); + + equals( + throttled.guid, + function_.guid, + 'throttled-callback and callback should have the same .guid' + ); + + execManyTimes( + function () { + let now = Number(new Date()); + startTime = startTime || now; + index++; + throttled.call(startTime, now); }, - throttled = throttle( delay, fn ); - - equals( throttled.guid, fn.guid, 'throttled-callback and callback should have the same .guid' ); - - exec_many_times( function(){ - var now = +new Date(); - start_time = start_time || now; - i++; - throttled.call( start_time, now ); - }, function( callback ){ - var len = arr.length; - - setTimeout(function(){ - //console.log( arr, arr.length, len, i ); - ok( arr.length < i, 'callback should be executed less # of times than throttled-callback' ); - equals( arr[0], 0, 'callback should be executed immediately' ); - equals( arr.length - len, 1, 'callback should be executed one more time after finish' ); - - start_time = null; - arr = []; - i = 0; - - callback ? callback() : start(); - - }, delay * 2); - }) + function (callback) { + let length_ = array.length; + + setTimeout(function () { + // Console.log( arr, arr.length, len, i ); + ok( + array.length < index, + 'callback should be executed less # of times than throttled-callback' + ); + equals(array[0], 0, 'callback should be executed immediately'); + equals( + array.length - length_, + 1, + 'callback should be executed one more time after finish' + ); + + startTime = null; + array = []; + index = 0; + + callback ? callback() : start(); + }, delay * 2); + } + ); }); -test( 'delay, false, callback', function() { - expect( 7 ); +test('delay, false, callback', function () { + expect(7); stop(); - var start_time, - i = 0, - arr = [], - fn = function( now ){ - arr.push( now - this ) + let startTime; + let index = 0; + let array = []; + let function_ = function (now) { + array.push(now - this); + }; + let throttled = throttle(delay, false, function_); + + equals( + throttled.guid, + function_.guid, + 'throttled-callback and callback should have the same .guid' + ); + + execManyTimes( + function () { + let now = Number(new Date()); + startTime = startTime || now; + index++; + throttled.call(startTime, now); }, - throttled = throttle( delay, false, fn ); - - equals( throttled.guid, fn.guid, 'throttled-callback and callback should have the same .guid' ); - - exec_many_times( function(){ - var now = +new Date(); - start_time = start_time || now; - i++; - throttled.call( start_time, now ); - }, function( callback ){ - var len = arr.length; - - setTimeout(function(){ - //console.log( arr, arr.length, len, i ); - ok( arr.length < i, 'callback should be executed less # of times than throttled-callback' ); - equals( arr[0], 0, 'callback should be executed immediately' ); - equals( arr.length - len, 1, 'callback should be executed one more time after finish' ); - - start_time = null; - arr = []; - i = 0; - - callback ? callback() : start(); - - }, delay * 2); - }) + function (callback) { + let length_ = array.length; + + setTimeout(function () { + // Console.log( arr, arr.length, len, i ); + ok( + array.length < index, + 'callback should be executed less # of times than throttled-callback' + ); + equals(array[0], 0, 'callback should be executed immediately'); + equals( + array.length - length_, + 1, + 'callback should be executed one more time after finish' + ); + + startTime = null; + array = []; + index = 0; + + callback ? callback() : start(); + }, delay * 2); + } + ); }); -test( 'delay, true, callback', function() { - expect( 7 ); +test('delay, true, callback', function () { + expect(7); stop(); - var start_time, - i = 0, - arr = [], - fn = function( now ){ - arr.push( now - this ) + let startTime; + let index = 0; + let array = []; + let function_ = function (now) { + array.push(now - this); + }; + let throttled = throttle(delay, true, function_); + + equals( + throttled.guid, + function_.guid, + 'throttled-callback and callback should have the same .guid' + ); + + execManyTimes( + function () { + let now = Number(new Date()); + startTime = startTime || now; + index++; + throttled.call(startTime, now); }, - throttled = throttle( delay, true, fn ); - - equals( throttled.guid, fn.guid, 'throttled-callback and callback should have the same .guid' ); - - exec_many_times( function(){ - var now = +new Date(); - start_time = start_time || now; - i++; - throttled.call( start_time, now ); - }, function( callback ){ - var len = arr.length; - - setTimeout(function(){ - //console.log( arr, arr.length, len, i ); - ok( arr.length < i, 'callback should be executed less # of times than throttled-callback' ); - equals( arr[0], 0, 'callback should be executed immediately' ); - equals( arr.length - len, 0, 'callback should NOT be executed one more time after finish' ); - - start_time = null; - arr = []; - i = 0; - - callback ? callback() : start(); - - }, delay * 2); - }) + function (callback) { + let length_ = array.length; + + setTimeout(function () { + // Console.log( arr, arr.length, len, i ); + ok( + array.length < index, + 'callback should be executed less # of times than throttled-callback' + ); + equals(array[0], 0, 'callback should be executed immediately'); + equals( + array.length - length_, + 0, + 'callback should NOT be executed one more time after finish' + ); + + startTime = null; + array = []; + index = 0; + + callback ? callback() : start(); + }, delay * 2); + } + ); }); -test('cancel', function() { - expect( 2 ); +test('cancel', function () { + expect(2); stop(); - var callCount = 0, - throttled = throttle( delay * 100, false, function() { - callCount++; - } ); + let callCount = 0; + let throttled = throttle(delay * 100, false, function () { + callCount++; + }); - equals(1, 1); + equals(1, 1); throttled.cancel(); throttled.call(); - setTimeout(function() { + setTimeout(function () { equals(callCount, 0, 'callback should not be called'); start(); }, delay * 2); }); +module('debounce'); -module( 'debounce' ); - -test( 'delay, callback', function() { - expect( 5 ); +test('delay, callback', function () { + expect(5); stop(); - var start_time, - i = 0, - arr = [], - fn = function(){ - arr.push( +new Date() ) + let startTime; + let index = 0; + let array = []; + let function_ = function () { + array.push(Number(new Date())); + }; + let debounced = debounce(delay, function_); + + equals( + debounced.guid, + function_.guid, + 'throttled-callback and callback should have the same .guid' + ); + + execManyTimes( + function () { + startTime = startTime || Number(new Date()); + index++; + debounced.call(); }, - debounced = debounce( delay, fn ); - - equals( debounced.guid, fn.guid, 'throttled-callback and callback should have the same .guid' ); - - exec_many_times( function(){ - start_time = start_time || +new Date(); - i++; - debounced.call(); - }, function( callback ){ - var len = arr.length, - done_time = +new Date(); - - setTimeout(function(){ - //console.log( arr[0] - done_time ); - equals( arr.length, 1, 'callback was executed once' ); - ok( arr[0] >= done_time, 'callback should be executed after the finish' ); - - start_time = null; - arr = []; - i = 0; - - callback ? callback() : start(); - - }, delay * 2); - }) + function (callback) { + let length_ = array.length; + let doneTime = Number(new Date()); + + setTimeout(function () { + // Console.log( arr[0] - doneTime ); + equals(array.length, 1, 'callback was executed once'); + ok( + array[0] >= doneTime, + 'callback should be executed after the finish' + ); + + startTime = null; + array = []; + index = 0; + + callback ? callback() : start(); + }, delay * 2); + } + ); }); -test( 'delay, false, callback', function() { - expect( 5 ); +test('delay, false, callback', function () { + expect(5); stop(); - var start_time, - i = 0, - arr = [], - fn = function(){ - arr.push( +new Date() ) + let startTime; + let index = 0; + let array = []; + let function_ = function () { + array.push(Number(new Date())); + }; + let debounced = debounce(delay, false, function_); + + equals( + debounced.guid, + function_.guid, + 'throttled-callback and callback should have the same .guid' + ); + + execManyTimes( + function () { + startTime = startTime || Number(new Date()); + index++; + debounced.call(); }, - debounced = debounce( delay, false, fn ); - - equals( debounced.guid, fn.guid, 'throttled-callback and callback should have the same .guid' ); - - exec_many_times( function(){ - start_time = start_time || +new Date(); - i++; - debounced.call(); - }, function( callback ){ - var len = arr.length, - done_time = +new Date(); - - setTimeout(function(){ - //console.log( arr[0] - done_time ); - equals( arr.length, 1, 'callback was executed once' ); - ok( arr[0] >= done_time, 'callback should be executed after the finish' ); - - start_time = null; - arr = []; - i = 0; - - callback ? callback() : start(); - - }, delay * 2); - }) + function (callback) { + let length_ = array.length; + let doneTime = Number(new Date()); + + setTimeout(function () { + // Console.log( arr[0] - doneTime ); + equals(array.length, 1, 'callback was executed once'); + ok( + array[0] >= doneTime, + 'callback should be executed after the finish' + ); + + startTime = null; + array = []; + index = 0; + + callback ? callback() : start(); + }, delay * 2); + } + ); }); -test( 'delay, true, callback', function() { - expect( 5 ); +test('delay, true, callback', function () { + expect(5); stop(); - var start_time, - i = 0, - arr = [], - fn = function(){ - arr.push( +new Date() ) + let startTime; + let index = 0; + let array = []; + let function_ = function () { + array.push(Number(new Date())); + }; + let debounced = debounce(delay, true, function_); + + equals( + debounced.guid, + function_.guid, + 'throttled-callback and callback should have the same .guid' + ); + + execManyTimes( + function () { + startTime = startTime || Number(new Date()); + index++; + debounced.call(); }, - debounced = debounce( delay, true, fn ); - - equals( debounced.guid, fn.guid, 'throttled-callback and callback should have the same .guid' ); - - exec_many_times( function(){ - start_time = start_time || +new Date(); - i++; - debounced.call(); - }, function( callback ){ - var len = arr.length; - - setTimeout(function(){ - //console.log( arr[0] - start_time ); - equals( arr.length, 1, 'callback was executed once' ); - ok( arr[0] - start_time <= 5, 'callback should be executed at the start' ); - - start_time = null; - arr = []; - i = 0; - - callback ? callback() : start(); - - }, delay * 2); - }) + function (callback) { + let length_ = array.length; + + setTimeout(function () { + // Console.log( arr[0] - startTime ); + equals(array.length, 1, 'callback was executed once'); + ok( + array[0] - startTime <= 5, + 'callback should be executed at the start' + ); + + startTime = null; + array = []; + index = 0; + + callback ? callback() : start(); + }, delay * 2); + } + ); }); -test('cancel', function() { - expect( 3 ); +test('cancel', function () { + expect(3); stop(); - var start_time, - i = 0, - arr = [], - fn = function(){ - arr.push( +new Date() ) + let startTime; + let index = 0; + let array = []; + let function_ = function () { + array.push(Number(new Date())); + }; + let debounced = debounce(delay, true, function_); + + equals( + debounced.guid, + function_.guid, + 'throttled-callback and callback should have the same .guid' + ); + + setTimeout(function () { + debounced.cancel(); + }, delay / 2); + execManyTimes( + function () { + startTime = startTime || Number(new Date()); + index++; + debounced.call(); }, - debounced = debounce( delay, true, fn ); - - equals( debounced.guid, fn.guid, 'throttled-callback and callback should have the same .guid' ); - - setTimeout(function() {debounced.cancel();}, delay / 2) - exec_many_times( function(){ - start_time = start_time || +new Date(); - i++; - debounced.call(); - }, function( callback ){ - var len = arr.length; - - setTimeout(function(){ - equals( arr.length, 0, 'callback should not be executed' ); + function (callback) { + let length_ = array.length; - start_time = null; - arr = []; - i = 0; + setTimeout(function () { + equals(array.length, 0, 'callback should not be executed'); - callback ? callback() : start(); + startTime = null; + array = []; + index = 0; - }, delay * 2); - }) + callback ? callback() : start(); + }, delay * 2); + } + ); }); diff --git a/throttle.js b/throttle.js index a2db06f..e3301ee 100644 --- a/throttle.js +++ b/throttle.js @@ -16,7 +16,7 @@ * * @returns {Function} A new, throttled, function. */ -export default function(delay, noTrailing, callback, debounceMode) { +export default function (delay, noTrailing, callback, debounceMode) { /* * After wrapper has stopped being called, this timeout ensures that * `callback` is executed at the proper times in `throttle` and `end`