refactor(l4d2-web): harden console-history.js against HTMX version drift and races
- pendingCommand captured in htmx:beforeRequest (not requestConfig). - ensureLoaded shares a single inflight Promise across concurrent calls. - Document why synthetic null-id entries are safe in the cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6f49efd44a
commit
66d14feca5
1 changed files with 28 additions and 11 deletions
|
|
@ -18,10 +18,12 @@ function bindConsoleForm(form) {
|
||||||
// Entries are stored newest-first to match the API response shape.
|
// Entries are stored newest-first to match the API response shape.
|
||||||
let cache = [];
|
let cache = [];
|
||||||
let cacheLoaded = false;
|
let cacheLoaded = false;
|
||||||
|
let loadingPromise = null;
|
||||||
let cursor = -1; // -1 = "not in history" (at the live input)
|
let cursor = -1; // -1 = "not in history" (at the live input)
|
||||||
let snapshot = ""; // saved input value for restoring via ArrowDown
|
let snapshot = ""; // saved input value for restoring via ArrowDown
|
||||||
let oldestId = null; // id of the oldest cached entry, used for pagination
|
let oldestId = null; // id of the oldest cached entry, used for pagination
|
||||||
let exhausted = false; // true when we've fetched all available history
|
let exhausted = false; // true when we've fetched all available history
|
||||||
|
let pendingCommand = null; // captured before HTMX submit; used by afterRequest
|
||||||
|
|
||||||
async function loadHistory(params) {
|
async function loadHistory(params) {
|
||||||
const url = new URL(`/servers/${serverId}/console/history`, location.origin);
|
const url = new URL(`/servers/${serverId}/console/history`, location.origin);
|
||||||
|
|
@ -40,15 +42,20 @@ function bindConsoleForm(form) {
|
||||||
|
|
||||||
async function ensureLoaded() {
|
async function ensureLoaded() {
|
||||||
if (cacheLoaded) return;
|
if (cacheLoaded) return;
|
||||||
cacheLoaded = true;
|
if (!loadingPromise) {
|
||||||
const entries = await loadHistory();
|
loadingPromise = (async () => {
|
||||||
cache = entries; // newest-first from API
|
const entries = await loadHistory();
|
||||||
if (cache.length > 0) {
|
cache = entries; // newest-first from API
|
||||||
oldestId = cache[cache.length - 1].id;
|
if (cache.length > 0) {
|
||||||
}
|
oldestId = cache[cache.length - 1].id;
|
||||||
if (entries.length < 50) {
|
}
|
||||||
exhausted = true;
|
if (entries.length < 50) {
|
||||||
|
exhausted = true;
|
||||||
|
}
|
||||||
|
cacheLoaded = true;
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
|
await loadingPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchOlderPage() {
|
async function fetchOlderPage() {
|
||||||
|
|
@ -116,13 +123,23 @@ function bindConsoleForm(form) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Capture the command value before HTMX submits the form; avoids relying on
|
||||||
|
// event.detail.requestConfig.parameters which changed between HTMX 1.x / 2.x.
|
||||||
|
form.addEventListener("htmx:beforeRequest", () => {
|
||||||
|
const commandInput = form.querySelector("input[name='command']");
|
||||||
|
pendingCommand = commandInput ? commandInput.value : null;
|
||||||
|
});
|
||||||
|
|
||||||
// After a successful HTMX POST: prepend the sent command to cache.
|
// After a successful HTMX POST: prepend the sent command to cache.
|
||||||
form.addEventListener("htmx:afterRequest", (event) => {
|
form.addEventListener("htmx:afterRequest", (event) => {
|
||||||
if (!event.detail.successful) return;
|
if (!event.detail.successful) return;
|
||||||
const params = event.detail.requestConfig && event.detail.requestConfig.parameters;
|
// Use the value captured before the request; fall back to snapshot.
|
||||||
const command = (params && params.command) || snapshot || "";
|
const command = pendingCommand || snapshot || "";
|
||||||
|
pendingCommand = null;
|
||||||
if (!command) return;
|
if (!command) return;
|
||||||
// Prepend as the newest entry (id=null is a placeholder).
|
// Prepend as the newest entry.
|
||||||
|
// id=null is safe here: pagination uses oldestId (the real persisted-row id)
|
||||||
|
// for ?before= queries, so a synthetic null-id entry doesn't break paging.
|
||||||
cache.unshift({ id: null, command });
|
cache.unshift({ id: null, command });
|
||||||
// Reset cursor so ArrowUp immediately recalls this command.
|
// Reset cursor so ArrowUp immediately recalls this command.
|
||||||
cursor = -1;
|
cursor = -1;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue