Skip to content

Commit fd16d98

Browse files
committed
Run debounce callback if browser tab is closed
A common issue with debounced API calls is that when the user closes the browser tab, the debounced function may not have run yet. This is a frequent cause of data loss. The debounce implementation now prevents this issue by running the trailing debounce immediately if the tab is closed prior to the trailing timeout. The code detects whether it's running in an environment with access to `document.addEventListener()`. In runtimes where this function is unavailable (e.g. Node.js) the new functionality is ignored since would not be relevant in that context.
1 parent d122213 commit fd16d98

File tree

9 files changed

+120
-15
lines changed

9 files changed

+120
-15
lines changed

docs/modules/debounce.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -866,6 +866,15 @@ <h1>debounce.js</h1>
866866
parameter. If <code>immediate</code> is passed, the argument function will be
867867
triggered at the beginning of the sequence instead of at the end.</p>
868868

869+
<p>A common issue with other debounce libraries is that when the user closes
870+
the browser tab, the debounced function may not have run yet. This is a
871+
frequent cause of data loss. This implementation prevents that problem by
872+
running the debounced function immediately if the tab is closed prior to the
873+
timeout. (Not applicable to Node.js or other headless runtimes.)</p>
874+
875+
<p>When making debounced API calls it is recommended to use `fetch()` with the
876+
`keepalive` parameter. This allows the HTTP request to finish in the
877+
background after the user closes the browser tab.</p>
869878
</div>
870879

871880
<div class="content"><div class='highlight'><pre><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">debounce</span>(<span class="hljs-params">func, wait, immediate</span>) {

docs/underscore-esm.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2933,6 +2933,14 @@ <h1>underscore-esm.js</h1>
29332933
function is triggered. The end of a sequence is defined by the <code>wait</code>
29342934
parameter. If <code>immediate</code> is passed, the argument function will be
29352935
triggered at the beginning of the sequence instead of at the end.</p>
2936+
<p>A common issue with other debounce libraries is that when the user closes
2937+
the browser tab, the debounced function may not have run yet. This is a
2938+
frequent cause of data loss. This implementation prevents that problem by
2939+
running the debounced function immediately if the tab is closed prior to the
2940+
timeout. (Not applicable to Node.js or other headless runtimes.)</p>
2941+
<p>When making debounced API calls it is recommended to use `fetch()` with the
2942+
`keepalive` parameter. This allows the HTTP request to finish in the
2943+
background after the user closes the browser tab.</p>
29362944

29372945
</div>
29382946

modules/debounce.js

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,35 @@ import now from './now.js';
55
// function is triggered. The end of a sequence is defined by the `wait`
66
// parameter. If `immediate` is passed, the argument function will be
77
// triggered at the beginning of the sequence instead of at the end.
8+
//
9+
// A common issue with other debounce libraries is that when the user closes
10+
// the browser tab, the debounced function may not have run yet. This is a
11+
// frequent cause of data loss. This implementation prevents that problem by
12+
// running the debounced function immediately if the tab is closed prior to the
13+
// timeout. (Not applicable to Node.js or other headless runtimes.)
14+
//
15+
// When making debounced API calls it is recommended to use `fetch()` with the
16+
// `keepalive` parameter. This allows the HTTP request to finish in the
17+
// background after the user closes the browser tab.
818
export default function debounce(func, wait, immediate) {
919
var timeout, previous, args, result, context;
1020

1121
var later = function() {
1222
var passed = now() - previous;
13-
if (wait > passed) {
14-
timeout = setTimeout(later, wait - passed);
15-
} else {
23+
var pageHidden = global.document && global.document.visibilityState === 'hidden';
24+
clearTimeout(timeout);
25+
if (wait <= passed || (!immediate && pageHidden)) {
26+
if (!immediate && global.document && global.document.removeEventListener) {
27+
global.document.removeEventListener(
28+
'visibilityChange',
29+
later, { capture: true });
30+
}
1631
timeout = null;
1732
if (!immediate) result = func.apply(context, args);
1833
// This check is needed because `func` can recursively invoke `debounced`.
1934
if (!timeout) args = context = null;
35+
} else {
36+
timeout = setTimeout(later, wait - passed);
2037
}
2138
};
2239

@@ -26,6 +43,11 @@ export default function debounce(func, wait, immediate) {
2643
previous = now();
2744
if (!timeout) {
2845
timeout = setTimeout(later, wait);
46+
if (!immediate && global.document && global.document.addEventListener) {
47+
global.document.addEventListener(
48+
'visibilityChange',
49+
later, { capture: true, passive: true });
50+
}
2951
if (immediate) result = func.apply(context, args);
3052
}
3153
return result;

underscore-esm.js

Lines changed: 25 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

underscore-esm.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

underscore-node-f.cjs

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1134,18 +1134,35 @@ function throttle(func, wait, options) {
11341134
// function is triggered. The end of a sequence is defined by the `wait`
11351135
// parameter. If `immediate` is passed, the argument function will be
11361136
// triggered at the beginning of the sequence instead of at the end.
1137+
//
1138+
// A common issue with other debounce libraries is that when the user closes
1139+
// the browser tab, the debounced function may not have run yet. This is a
1140+
// frequent cause of data loss. This implementation prevents that problem by
1141+
// running the debounced function immediately if the tab is closed prior to the
1142+
// timeout. (Not applicable to Node.js or other headless runtimes.)
1143+
//
1144+
// When making debounced API calls it is recommended to use `fetch()` with the
1145+
// `keepalive` parameter. This allows the HTTP request to finish in the
1146+
// background after the user closes the browser tab.
11371147
function debounce(func, wait, immediate) {
11381148
var timeout, previous, args, result, context;
11391149

11401150
var later = function() {
11411151
var passed = now() - previous;
1142-
if (wait > passed) {
1143-
timeout = setTimeout(later, wait - passed);
1144-
} else {
1152+
var pageHidden = global.document && global.document.visibilityState === 'hidden';
1153+
clearTimeout(timeout);
1154+
if (wait <= passed || (!immediate && pageHidden)) {
1155+
if (!immediate && global.document && global.document.removeEventListener) {
1156+
global.document.removeEventListener(
1157+
'visibilityChange',
1158+
later, { capture: true });
1159+
}
11451160
timeout = null;
11461161
if (!immediate) result = func.apply(context, args);
11471162
// This check is needed because `func` can recursively invoke `debounced`.
11481163
if (!timeout) args = context = null;
1164+
} else {
1165+
timeout = setTimeout(later, wait - passed);
11491166
}
11501167
};
11511168

@@ -1155,6 +1172,11 @@ function debounce(func, wait, immediate) {
11551172
previous = now();
11561173
if (!timeout) {
11571174
timeout = setTimeout(later, wait);
1175+
if (!immediate && global.document && global.document.addEventListener) {
1176+
global.document.addEventListener(
1177+
'visibilityChange',
1178+
later, { capture: true, passive: true });
1179+
}
11581180
if (immediate) result = func.apply(context, args);
11591181
}
11601182
return result;

underscore-node-f.cjs.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

underscore-umd.js

Lines changed: 25 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

underscore-umd.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)