faster-axios 0.0.1-security → 1.17.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of faster-axios might be problematic. Click here for more details.

Files changed (78) hide show
  1. package/CHANGELOG.md +1747 -0
  2. package/LICENSE +7 -0
  3. package/MIGRATION_GUIDE.md +877 -0
  4. package/README.md +2426 -5
  5. package/index.d.cts +715 -0
  6. package/index.d.ts +734 -0
  7. package/index.js +45 -0
  8. package/lib/adapters/README.md +36 -0
  9. package/lib/adapters/adapters.js +132 -0
  10. package/lib/adapters/fetch.js +473 -0
  11. package/lib/adapters/http.js +1312 -0
  12. package/lib/adapters/xhr.js +227 -0
  13. package/lib/axios.js +89 -0
  14. package/lib/cancel/CancelToken.js +135 -0
  15. package/lib/cancel/CanceledError.js +22 -0
  16. package/lib/cancel/isCancel.js +5 -0
  17. package/lib/core/Axios.js +281 -0
  18. package/lib/core/AxiosError.js +176 -0
  19. package/lib/core/AxiosHeaders.js +348 -0
  20. package/lib/core/InterceptorManager.js +72 -0
  21. package/lib/core/README.md +8 -0
  22. package/lib/core/analytics.js +0 -0
  23. package/lib/core/buildFullPath.js +22 -0
  24. package/lib/core/dispatchRequest.js +89 -0
  25. package/lib/core/eval.js +41 -0
  26. package/lib/core/mergeConfig.js +124 -0
  27. package/lib/core/settle.js +27 -0
  28. package/lib/core/transformData.js +28 -0
  29. package/lib/defaults/index.js +177 -0
  30. package/lib/defaults/transitional.js +8 -0
  31. package/lib/env/README.md +3 -0
  32. package/lib/env/classes/FormData.js +2 -0
  33. package/lib/env/data.js +1 -0
  34. package/lib/helpers/AxiosTransformStream.js +156 -0
  35. package/lib/helpers/AxiosURLSearchParams.js +61 -0
  36. package/lib/helpers/HttpStatusCode.js +77 -0
  37. package/lib/helpers/README.md +7 -0
  38. package/lib/helpers/ZlibHeaderTransformStream.js +29 -0
  39. package/lib/helpers/bind.js +14 -0
  40. package/lib/helpers/buildURL.js +66 -0
  41. package/lib/helpers/callbackify.js +18 -0
  42. package/lib/helpers/combineURLs.js +15 -0
  43. package/lib/helpers/composeSignals.js +57 -0
  44. package/lib/helpers/cookies.js +60 -0
  45. package/lib/helpers/deprecatedMethod.js +31 -0
  46. package/lib/helpers/estimateDataURLDecodedBytes.js +100 -0
  47. package/lib/helpers/formDataToJSON.js +97 -0
  48. package/lib/helpers/formDataToStream.js +119 -0
  49. package/lib/helpers/fromDataURI.js +66 -0
  50. package/lib/helpers/isAbsoluteURL.js +19 -0
  51. package/lib/helpers/isAxiosError.js +14 -0
  52. package/lib/helpers/isURLSameOrigin.js +16 -0
  53. package/lib/helpers/null.js +2 -0
  54. package/lib/helpers/parseHeaders.js +69 -0
  55. package/lib/helpers/parseProtocol.js +6 -0
  56. package/lib/helpers/progressEventReducer.js +54 -0
  57. package/lib/helpers/readBlob.js +15 -0
  58. package/lib/helpers/resolveConfig.js +106 -0
  59. package/lib/helpers/sanitizeHeaderValue.js +60 -0
  60. package/lib/helpers/shouldBypassProxy.js +178 -0
  61. package/lib/helpers/speedometer.js +55 -0
  62. package/lib/helpers/spread.js +28 -0
  63. package/lib/helpers/throttle.js +44 -0
  64. package/lib/helpers/toFormData.js +249 -0
  65. package/lib/helpers/toURLEncodedForm.js +19 -0
  66. package/lib/helpers/trackStream.js +89 -0
  67. package/lib/helpers/validator.js +112 -0
  68. package/lib/platform/browser/classes/Blob.js +3 -0
  69. package/lib/platform/browser/classes/FormData.js +3 -0
  70. package/lib/platform/browser/classes/URLSearchParams.js +4 -0
  71. package/lib/platform/browser/index.js +13 -0
  72. package/lib/platform/common/utils.js +52 -0
  73. package/lib/platform/index.js +7 -0
  74. package/lib/platform/node/classes/FormData.js +3 -0
  75. package/lib/platform/node/classes/URLSearchParams.js +4 -0
  76. package/lib/platform/node/index.js +37 -0
  77. package/lib/utils.js +932 -0
  78. package/package.json +185 -6
@@ -0,0 +1,54 @@
1
+ import speedometer from './speedometer.js';
2
+ import throttle from './throttle.js';
3
+ import utils from '../utils.js';
4
+
5
+ export const progressEventReducer = (listener, isDownloadStream, freq = 3) => {
6
+ let bytesNotified = 0;
7
+ const _speedometer = speedometer(50, 250);
8
+
9
+ return throttle((e) => {
10
+ if (!e || typeof e.loaded !== 'number') {
11
+ return;
12
+ }
13
+ const rawLoaded = e.loaded;
14
+ const total = e.lengthComputable ? e.total : undefined;
15
+ const loaded = total != null ? Math.min(rawLoaded, total) : rawLoaded;
16
+ const progressBytes = Math.max(0, loaded - bytesNotified);
17
+ const rate = _speedometer(progressBytes);
18
+
19
+ bytesNotified = Math.max(bytesNotified, loaded);
20
+
21
+ const data = {
22
+ loaded,
23
+ total,
24
+ progress: total ? loaded / total : undefined,
25
+ bytes: progressBytes,
26
+ rate: rate ? rate : undefined,
27
+ estimated: rate && total ? (total - loaded) / rate : undefined,
28
+ event: e,
29
+ lengthComputable: total != null,
30
+ [isDownloadStream ? 'download' : 'upload']: true,
31
+ };
32
+
33
+ listener(data);
34
+ }, freq);
35
+ };
36
+
37
+ export const progressEventDecorator = (total, throttled) => {
38
+ const lengthComputable = total != null;
39
+
40
+ return [
41
+ (loaded) =>
42
+ throttled[0]({
43
+ lengthComputable,
44
+ total,
45
+ loaded,
46
+ }),
47
+ throttled[1],
48
+ ];
49
+ };
50
+
51
+ export const asyncDecorator =
52
+ (fn) =>
53
+ (...args) =>
54
+ utils.asap(() => fn(...args));
@@ -0,0 +1,15 @@
1
+ const { asyncIterator } = Symbol;
2
+
3
+ const readBlob = async function* (blob) {
4
+ if (blob.stream) {
5
+ yield* blob.stream();
6
+ } else if (blob.arrayBuffer) {
7
+ yield await blob.arrayBuffer();
8
+ } else if (blob[asyncIterator]) {
9
+ yield* blob[asyncIterator]();
10
+ } else {
11
+ yield blob;
12
+ }
13
+ };
14
+
15
+ export default readBlob;
@@ -0,0 +1,106 @@
1
+ import platform from '../platform/index.js';
2
+ import utils from '../utils.js';
3
+ import isURLSameOrigin from './isURLSameOrigin.js';
4
+ import cookies from './cookies.js';
5
+ import buildFullPath from '../core/buildFullPath.js';
6
+ import mergeConfig from '../core/mergeConfig.js';
7
+ import AxiosHeaders from '../core/AxiosHeaders.js';
8
+ import buildURL from './buildURL.js';
9
+
10
+ const FORM_DATA_CONTENT_HEADERS = ['content-type', 'content-length'];
11
+
12
+ function setFormDataHeaders(headers, formHeaders, policy) {
13
+ if (policy !== 'content-only') {
14
+ headers.set(formHeaders);
15
+ return;
16
+ }
17
+
18
+ Object.entries(formHeaders).forEach(([key, val]) => {
19
+ if (FORM_DATA_CONTENT_HEADERS.includes(key.toLowerCase())) {
20
+ headers.set(key, val);
21
+ }
22
+ });
23
+ }
24
+
25
+ /**
26
+ * Encode a UTF-8 string to a Latin-1 byte string for use with btoa().
27
+ * This is a modern replacement for the deprecated unescape(encodeURIComponent(str)) pattern.
28
+ *
29
+ * @param {string} str The string to encode
30
+ *
31
+ * @returns {string} UTF-8 bytes as a Latin-1 string
32
+ */
33
+ const encodeUTF8 = (str) =>
34
+ encodeURIComponent(str).replace(/%([0-9A-F]{2})/gi, (_, hex) =>
35
+ String.fromCharCode(parseInt(hex, 16))
36
+ );
37
+
38
+ export default (config) => {
39
+ const newConfig = mergeConfig({}, config);
40
+
41
+ // Read only own properties to prevent prototype pollution gadgets
42
+ // (e.g. Object.prototype.baseURL = 'https://evil.com').
43
+ const own = (key) => (utils.hasOwnProp(newConfig, key) ? newConfig[key] : undefined);
44
+
45
+ const data = own('data');
46
+ let withXSRFToken = own('withXSRFToken');
47
+ const xsrfHeaderName = own('xsrfHeaderName');
48
+ const xsrfCookieName = own('xsrfCookieName');
49
+ let headers = own('headers');
50
+ const auth = own('auth');
51
+ const baseURL = own('baseURL');
52
+ const allowAbsoluteUrls = own('allowAbsoluteUrls');
53
+ const url = own('url');
54
+
55
+ newConfig.headers = headers = AxiosHeaders.from(headers);
56
+
57
+ newConfig.url = buildURL(
58
+ buildFullPath(baseURL, url, allowAbsoluteUrls),
59
+ config.params,
60
+ config.paramsSerializer
61
+ );
62
+
63
+ // HTTP basic authentication
64
+ if (auth) {
65
+ headers.set(
66
+ 'Authorization',
67
+ 'Basic ' +
68
+ btoa((auth.username || '') + ':' + (auth.password ? encodeUTF8(auth.password) : ''))
69
+ );
70
+ }
71
+
72
+ if (utils.isFormData(data)) {
73
+ if (platform.hasStandardBrowserEnv || platform.hasStandardBrowserWebWorkerEnv) {
74
+ headers.setContentType(undefined); // browser handles it
75
+ } else if (utils.isFunction(data.getHeaders)) {
76
+ // Node.js FormData (like form-data package)
77
+ setFormDataHeaders(headers, data.getHeaders(), own('formDataHeaderPolicy'));
78
+ }
79
+ }
80
+
81
+ // Add xsrf header
82
+ // This is only done if running in a standard browser environment.
83
+ // Specifically not if we're in a web worker, or react-native.
84
+
85
+ if (platform.hasStandardBrowserEnv) {
86
+ if (utils.isFunction(withXSRFToken)) {
87
+ withXSRFToken = withXSRFToken(newConfig);
88
+ }
89
+
90
+ // Strict boolean check — prevents proto-pollution gadgets (e.g. Object.prototype.withXSRFToken = 1)
91
+ // and misconfigurations (e.g. "false") from short-circuiting the same-origin check and leaking
92
+ // the XSRF token cross-origin.
93
+ const shouldSendXSRF =
94
+ withXSRFToken === true || (withXSRFToken == null && isURLSameOrigin(newConfig.url));
95
+
96
+ if (shouldSendXSRF) {
97
+ const xsrfValue = xsrfHeaderName && xsrfCookieName && cookies.read(xsrfCookieName);
98
+
99
+ if (xsrfValue) {
100
+ headers.set(xsrfHeaderName, xsrfValue);
101
+ }
102
+ }
103
+ }
104
+
105
+ return newConfig;
106
+ };
@@ -0,0 +1,60 @@
1
+ 'use strict';
2
+
3
+ import utils from '../utils.js';
4
+
5
+ function trimSPorHTAB(str) {
6
+ let start = 0;
7
+ let end = str.length;
8
+
9
+ while (start < end) {
10
+ const code = str.charCodeAt(start);
11
+
12
+ if (code !== 0x09 && code !== 0x20) {
13
+ break;
14
+ }
15
+
16
+ start += 1;
17
+ }
18
+
19
+ while (end > start) {
20
+ const code = str.charCodeAt(end - 1);
21
+
22
+ if (code !== 0x09 && code !== 0x20) {
23
+ break;
24
+ }
25
+
26
+ end -= 1;
27
+ }
28
+
29
+ return start === 0 && end === str.length ? str : str.slice(start, end);
30
+ }
31
+
32
+ // The control-code ranges are intentional: header sanitization strips C0/DEL bytes.
33
+ // eslint-disable-next-line no-control-regex
34
+ const INVALID_UNICODE_HEADER_VALUE_CHARS = new RegExp('[\\u0000-\\u0008\\u000a-\\u001f\\u007f]+', 'g');
35
+ // eslint-disable-next-line no-control-regex
36
+ const INVALID_BYTE_STRING_HEADER_VALUE_CHARS = new RegExp('[^\\u0009\\u0020-\\u007e\\u0080-\\u00ff]+', 'g');
37
+
38
+ function sanitizeValue(value, invalidChars) {
39
+ if (utils.isArray(value)) {
40
+ return value.map((item) => sanitizeValue(item, invalidChars));
41
+ }
42
+
43
+ return trimSPorHTAB(String(value).replace(invalidChars, ''));
44
+ }
45
+
46
+ export const sanitizeHeaderValue = (value) =>
47
+ sanitizeValue(value, INVALID_UNICODE_HEADER_VALUE_CHARS);
48
+
49
+ export const sanitizeByteStringHeaderValue = (value) =>
50
+ sanitizeValue(value, INVALID_BYTE_STRING_HEADER_VALUE_CHARS);
51
+
52
+ export function toByteStringHeaderObject(headers) {
53
+ const byteStringHeaders = Object.create(null);
54
+
55
+ utils.forEach(headers.toJSON(), (value, header) => {
56
+ byteStringHeaders[header] = sanitizeByteStringHeaderValue(value);
57
+ });
58
+
59
+ return byteStringHeaders;
60
+ }
@@ -0,0 +1,178 @@
1
+ const LOOPBACK_HOSTNAMES = new Set(['localhost']);
2
+
3
+ const isIPv4Loopback = (host) => {
4
+ const parts = host.split('.');
5
+ if (parts.length !== 4) return false;
6
+ if (parts[0] !== '127') return false;
7
+ return parts.every((p) => /^\d+$/.test(p) && Number(p) >= 0 && Number(p) <= 255);
8
+ };
9
+
10
+ const isIPv6Loopback = (host) => {
11
+ // Collapse all-zero groups: any form of ::1 / 0:0:...:0:1
12
+ // First, strip any leading "::" by normalising with Set lookup of common forms,
13
+ // then fall back to structural check.
14
+ if (host === '::1') return true;
15
+
16
+ // Check IPv4-mapped IPv6 loopback: ::ffff:<v4-loopback> or ::ffff:<hex-v4-loopback>
17
+ // Node's URL parser normalises ::ffff:127.0.0.1 → ::ffff:7f00:1
18
+ const v4MappedDotted = host.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/i);
19
+ if (v4MappedDotted) return isIPv4Loopback(v4MappedDotted[1]);
20
+
21
+ const v4MappedHex = host.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i);
22
+ if (v4MappedHex) {
23
+ const high = parseInt(v4MappedHex[1], 16);
24
+ // High 16 bits must start with 127 (0x7f) — i.e. 0x7f00..0x7fff
25
+ return high >= 0x7f00 && high <= 0x7fff;
26
+ }
27
+
28
+ // Full-form ::1 variants: any number of zero groups followed by trailing 1
29
+ // e.g. 0:0:0:0:0:0:0:1, 0000:...:0001
30
+ const groups = host.split(':');
31
+ if (groups.length === 8) {
32
+ for (let i = 0; i < 7; i++) {
33
+ if (!/^0+$/.test(groups[i])) return false;
34
+ }
35
+ return /^0*1$/.test(groups[7]);
36
+ }
37
+
38
+ return false;
39
+ };
40
+
41
+ const isLoopback = (host) => {
42
+ if (!host) return false;
43
+ if (LOOPBACK_HOSTNAMES.has(host)) return true;
44
+ if (isIPv4Loopback(host)) return true;
45
+ return isIPv6Loopback(host);
46
+ };
47
+
48
+ const DEFAULT_PORTS = {
49
+ http: 80,
50
+ https: 443,
51
+ ws: 80,
52
+ wss: 443,
53
+ ftp: 21,
54
+ };
55
+
56
+ const parseNoProxyEntry = (entry) => {
57
+ let entryHost = entry;
58
+ let entryPort = 0;
59
+
60
+ if (entryHost.charAt(0) === '[') {
61
+ const bracketIndex = entryHost.indexOf(']');
62
+
63
+ if (bracketIndex !== -1) {
64
+ const host = entryHost.slice(1, bracketIndex);
65
+ const rest = entryHost.slice(bracketIndex + 1);
66
+
67
+ if (rest.charAt(0) === ':' && /^\d+$/.test(rest.slice(1))) {
68
+ entryPort = Number.parseInt(rest.slice(1), 10);
69
+ }
70
+
71
+ return [host, entryPort];
72
+ }
73
+ }
74
+
75
+ const firstColon = entryHost.indexOf(':');
76
+ const lastColon = entryHost.lastIndexOf(':');
77
+
78
+ if (
79
+ firstColon !== -1 &&
80
+ firstColon === lastColon &&
81
+ /^\d+$/.test(entryHost.slice(lastColon + 1))
82
+ ) {
83
+ entryPort = Number.parseInt(entryHost.slice(lastColon + 1), 10);
84
+ entryHost = entryHost.slice(0, lastColon);
85
+ }
86
+
87
+ return [entryHost, entryPort];
88
+ };
89
+
90
+ // Convert IPv4-mapped IPv6 (::ffff:0:0/96 prefix) to IPv4 dotted form so both
91
+ // sides of a NO_PROXY comparison see the same canonical address. Without this,
92
+ // `NO_PROXY=192.168.1.5` would not match a request to `http://[::ffff:192.168.1.5]/`
93
+ // (Node's URL parser normalises that to `[::ffff:c0a8:105]`), and vice-versa,
94
+ // allowing the proxy-bypass policy to be circumvented by using the alternate
95
+ // representation. Returns the input unchanged when not IPv4-mapped.
96
+ const IPV4_MAPPED_DOTTED_RE = /^(?:::|(?:0{1,4}:){1,4}:|(?:0{1,4}:){5})ffff:(\d+\.\d+\.\d+\.\d+)$/i;
97
+ const IPV4_MAPPED_HEX_RE = /^(?:::|(?:0{1,4}:){1,4}:|(?:0{1,4}:){5})ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i;
98
+
99
+ const unmapIPv4MappedIPv6 = (host) => {
100
+ if (typeof host !== 'string' || host.indexOf(':') === -1) return host;
101
+
102
+ const dotted = host.match(IPV4_MAPPED_DOTTED_RE);
103
+ if (dotted) return dotted[1];
104
+
105
+ const hex = host.match(IPV4_MAPPED_HEX_RE);
106
+ if (hex) {
107
+ const high = parseInt(hex[1], 16);
108
+ const low = parseInt(hex[2], 16);
109
+ return `${high >> 8}.${high & 0xff}.${low >> 8}.${low & 0xff}`;
110
+ }
111
+
112
+ return host;
113
+ };
114
+
115
+ const normalizeNoProxyHost = (hostname) => {
116
+ if (!hostname) {
117
+ return hostname;
118
+ }
119
+
120
+ if (hostname.charAt(0) === '[' && hostname.charAt(hostname.length - 1) === ']') {
121
+ hostname = hostname.slice(1, -1);
122
+ }
123
+
124
+ return unmapIPv4MappedIPv6(hostname.replace(/\.+$/, ''));
125
+ };
126
+
127
+ export default function shouldBypassProxy(location) {
128
+ let parsed;
129
+
130
+ try {
131
+ parsed = new URL(location);
132
+ } catch (_err) {
133
+ return false;
134
+ }
135
+
136
+ const noProxy = (process.env.no_proxy || process.env.NO_PROXY || '').toLowerCase();
137
+
138
+ if (!noProxy) {
139
+ return false;
140
+ }
141
+
142
+ if (noProxy === '*') {
143
+ return true;
144
+ }
145
+
146
+ const port =
147
+ Number.parseInt(parsed.port, 10) || DEFAULT_PORTS[parsed.protocol.split(':', 1)[0]] || 0;
148
+
149
+ const hostname = normalizeNoProxyHost(parsed.hostname.toLowerCase());
150
+
151
+ return noProxy.split(/[\s,]+/).some((entry) => {
152
+ if (!entry) {
153
+ return false;
154
+ }
155
+
156
+ let [entryHost, entryPort] = parseNoProxyEntry(entry);
157
+
158
+ entryHost = normalizeNoProxyHost(entryHost);
159
+
160
+ if (!entryHost) {
161
+ return false;
162
+ }
163
+
164
+ if (entryPort && entryPort !== port) {
165
+ return false;
166
+ }
167
+
168
+ if (entryHost.charAt(0) === '*') {
169
+ entryHost = entryHost.slice(1);
170
+ }
171
+
172
+ if (entryHost.charAt(0) === '.') {
173
+ return hostname.endsWith(entryHost);
174
+ }
175
+
176
+ return hostname === entryHost || (isLoopback(hostname) && isLoopback(entryHost));
177
+ });
178
+ }
@@ -0,0 +1,55 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Calculate data maxRate
5
+ * @param {Number} [samplesCount= 10]
6
+ * @param {Number} [min= 1000]
7
+ * @returns {Function}
8
+ */
9
+ function speedometer(samplesCount, min) {
10
+ samplesCount = samplesCount || 10;
11
+ const bytes = new Array(samplesCount);
12
+ const timestamps = new Array(samplesCount);
13
+ let head = 0;
14
+ let tail = 0;
15
+ let firstSampleTS;
16
+
17
+ min = min !== undefined ? min : 1000;
18
+
19
+ return function push(chunkLength) {
20
+ const now = Date.now();
21
+
22
+ const startedAt = timestamps[tail];
23
+
24
+ if (!firstSampleTS) {
25
+ firstSampleTS = now;
26
+ }
27
+
28
+ bytes[head] = chunkLength;
29
+ timestamps[head] = now;
30
+
31
+ let i = tail;
32
+ let bytesCount = 0;
33
+
34
+ while (i !== head) {
35
+ bytesCount += bytes[i++];
36
+ i = i % samplesCount;
37
+ }
38
+
39
+ head = (head + 1) % samplesCount;
40
+
41
+ if (head === tail) {
42
+ tail = (tail + 1) % samplesCount;
43
+ }
44
+
45
+ if (now - firstSampleTS < min) {
46
+ return;
47
+ }
48
+
49
+ const passed = startedAt && now - startedAt;
50
+
51
+ return passed ? Math.round((bytesCount * 1000) / passed) : undefined;
52
+ };
53
+ }
54
+
55
+ export default speedometer;
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Syntactic sugar for invoking a function and expanding an array for arguments.
5
+ *
6
+ * Common use case would be to use `Function.prototype.apply`.
7
+ *
8
+ * ```js
9
+ * function f(x, y, z) {}
10
+ * const args = [1, 2, 3];
11
+ * f.apply(null, args);
12
+ * ```
13
+ *
14
+ * With `spread` this example can be re-written.
15
+ *
16
+ * ```js
17
+ * spread(function(x, y, z) {})([1, 2, 3]);
18
+ * ```
19
+ *
20
+ * @param {Function} callback
21
+ *
22
+ * @returns {Function}
23
+ */
24
+ export default function spread(callback) {
25
+ return function wrap(arr) {
26
+ return callback.apply(null, arr);
27
+ };
28
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Throttle decorator
3
+ * @param {Function} fn
4
+ * @param {Number} freq
5
+ * @return {Function}
6
+ */
7
+ function throttle(fn, freq) {
8
+ let timestamp = 0;
9
+ let threshold = 1000 / freq;
10
+ let lastArgs;
11
+ let timer;
12
+
13
+ const invoke = (args, now = Date.now()) => {
14
+ timestamp = now;
15
+ lastArgs = null;
16
+ if (timer) {
17
+ clearTimeout(timer);
18
+ timer = null;
19
+ }
20
+ fn(...args);
21
+ };
22
+
23
+ const throttled = (...args) => {
24
+ const now = Date.now();
25
+ const passed = now - timestamp;
26
+ if (passed >= threshold) {
27
+ invoke(args, now);
28
+ } else {
29
+ lastArgs = args;
30
+ if (!timer) {
31
+ timer = setTimeout(() => {
32
+ timer = null;
33
+ invoke(lastArgs);
34
+ }, threshold - passed);
35
+ }
36
+ }
37
+ };
38
+
39
+ const flush = () => lastArgs && invoke(lastArgs);
40
+
41
+ return [throttled, flush];
42
+ }
43
+
44
+ export default throttle;