Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 14 additions & 18 deletions debug_toolbar/static/debug_toolbar/js/toolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -292,20 +292,24 @@ const djdt = {
window.removeEventListener("resize", djdt.ensureHandleVisibility);
},
updateOnAjax() {
const sidebarUrl =
document.getElementById("djDebug").dataset.sidebarUrl;
const slowjax = debounce(ajax, 200);
const handleAjaxResponse = debounce(async (requestId) => {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of throttling the request, we debounce the entire function and properly cancel it if it is replaced by a new call.

const sidebarUrl =
document.getElementById("djDebug").dataset.sidebarUrl;

function handleAjaxResponse(requestId) {
const encodedRequestId = encodeURIComponent(requestId);
const dest = `${sidebarUrl}?request_id=${encodedRequestId}`;
slowjax(dest).then((data) => {
if (djdt.needUpdateOnFetch) {
if (djdt.needUpdateOnFetch) {
try {
const data = await ajax(dest);
replaceToolbarState(encodedRequestId, data);
} catch (error) {
console.error(
`"${error.name}" occurred within django-debug-toolbar: ${error.message}`,
error
);
}
});
}

}
}, 200);
// Patch XHR / traditional AJAX requests
const origOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (...args) {
Expand Down Expand Up @@ -333,15 +337,7 @@ const djdt = {
const promise = origFetch.apply(this, args);
return promise.then((response) => {
if (response.headers.get("djdt-request-id") !== null) {
try {
handleAjaxResponse(
response.headers.get("djdt-request-id")
);
} catch (err) {
throw new Error(
`"${err.name}" occurred within django-debug-toolbar: ${err.message}`
);
}
handleAjaxResponse(response.headers.get("djdt-request-id"));
}
return response;
});
Expand Down
27 changes: 13 additions & 14 deletions debug_toolbar/static/debug_toolbar/js/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,22 +123,21 @@ function replaceToolbarState(newRequestId, data) {
}
}

function debounce(func, delay) {
let timer = null;
let resolves = [];

/**
* Return function that delays invoking `func` until after `timeout` elapsed.
*
* Previous calls will be dismissed if the timeout hasn't elapsed.
*
* @param {Function} func - Function to be executed.
* @param {number} timeout - Time to wait before executing function in milliseconds.
* @returns {Function} - Debounced function.
*/
export function debounce(func, timeout) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
const result = func(...args);
for (const r of resolves) {
r(result);
}
resolves = [];
}, delay);

return new Promise((r) => resolves.push(r));
timer = setTimeout(() => Promise.try(func, ...args), timeout);
Comment thread
codingjoe marked this conversation as resolved.
};
Comment thread
codingjoe marked this conversation as resolved.
}

export { $$, ajax, ajaxForm, debounce, replaceToolbarState };
export { $$, ajax, ajaxForm, replaceToolbarState };
2 changes: 2 additions & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Change log
Pending
-------
* Prevent check from failing when ``ROOT_URLCONF`` is not defined.
* Prevent debounce race conditions in the history panel for rapid
fetch requests.

6.3.0 (2026-04-01)
------------------
Expand Down
1 change: 1 addition & 0 deletions docs/spelling_wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ biome
checkbox
contrib
csp
debounce
deduplicated
dicts
django
Expand Down
31 changes: 23 additions & 8 deletions tests/js/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -282,25 +282,40 @@ describe("utils.js", () => {
});

describe("debounce", () => {
it("debounces function calls", async () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it("debounces sync function calls", async () => {
const fn = vi.fn((val) => val);
const debounced = debounce(fn, 100);

const promise1 = debounced("first");
const promise2 = debounced("second");
debounced("first");
debounced("second");

vi.advanceTimersByTime(50);
const promise3 = debounced("third");
debounced("third");

vi.advanceTimersByTime(100);

const results = await Promise.all([promise1, promise2, promise3]);

expect(fn).toHaveBeenCalledTimes(1);
expect(fn).toHaveBeenCalledWith("third");
expect(results).toEqual(["third", "third", "third"]);
Comment thread
codingjoe marked this conversation as resolved.
vi.useRealTimers();
});
it("debounces async function calls", async () => {
const fn = vi.fn(async (val) => val);
const debounced = debounce(fn, 100);

debounced("first");
debounced("second");

vi.advanceTimersByTime(50);
debounced("third");

vi.advanceTimersByTime(100);

expect(fn).toHaveBeenCalledWith("third");
});
});
});
Loading