diff --git a/pom.xml b/pom.xml index af3ec7ad2..ddee8763e 100644 --- a/pom.xml +++ b/pom.xml @@ -43,7 +43,7 @@ OSV config contains a narrow temporary ignore because OSV Scanner v2.3.8 still reports 2.22.1 even though the advisory text names it as patched. --> - 2.22.1 + 2.22.2 1.5.35 diff --git a/src/main/resources/static/assets/viewer/demo.js b/src/main/resources/static/assets/viewer/demo.js index 42998be56..6c863cf98 100644 --- a/src/main/resources/static/assets/viewer/demo.js +++ b/src/main/resources/static/assets/viewer/demo.js @@ -114,6 +114,9 @@ function renderHistory(history = loadHistory()) { el.historyBody.textContent = ""; el.emptyHistory.hidden = history.length > 0; + // ⚡ Bolt: Use DocumentFragment to batch DOM insertions + // Impact: Reduces DOM reflows from O(n) to O(1), improving render performance for large history lists. + const fragment = document.createDocumentFragment(); for (const job of history) { const row = document.createElement("tr"); const fileCell = document.createElement("td"); @@ -143,18 +146,19 @@ function renderHistory(history = loadHistory()) { } row.append(fileCell, statusCell, submittedCell, actionsCell); - el.historyBody.appendChild(row); + fragment.appendChild(row); } + el.historyBody.appendChild(fragment); renderRecoveryEvidence(history); } -function addDetailRow(label, value) { +function addDetailRow(fragment, label, value) { const term = document.createElement("dt"); const description = document.createElement("dd"); term.textContent = label; description.textContent = formatDetailValue(value); - el.jobDetailBody.append(term, description); + fragment.append(term, description); } function formatDetailValue(value) { @@ -177,17 +181,21 @@ function renderJobDetail(detail) { : "Operational evidence"; el.jobDetailBody.textContent = ""; - addDetailRow("Job ID", detail.jobId); - addDetailRow("Tenant", detail.tenantId); - addDetailRow("Status", detail.status); - addDetailRow("Message", detail.message); - addDetailRow("Attempts", `${detail.attemptCount ?? 0} / ${detail.maxAttempts ?? "n/a"}`); - addDetailRow("Dead-lettered", Boolean(detail.deadLettered)); - addDetailRow("Retry at", detail.retryAt); - addDetailRow("Created", detail.createdAt); - addDetailRow("Started", detail.startedAt); - addDetailRow("Completed", detail.completedAt); - addDetailRow("Artifact", detail.convertedResourcePath); + // ⚡ Bolt: Use DocumentFragment to batch DOM insertions for detail rows + // Impact: Prevents multiple layout recalculations by applying all detail rows in a single DOM update. + const fragment = document.createDocumentFragment(); + addDetailRow(fragment, "Job ID", detail.jobId); + addDetailRow(fragment, "Tenant", detail.tenantId); + addDetailRow(fragment, "Status", detail.status); + addDetailRow(fragment, "Message", detail.message); + addDetailRow(fragment, "Attempts", `${detail.attemptCount ?? 0} / ${detail.maxAttempts ?? "n/a"}`); + addDetailRow(fragment, "Dead-lettered", Boolean(detail.deadLettered)); + addDetailRow(fragment, "Retry at", detail.retryAt); + addDetailRow(fragment, "Created", detail.createdAt); + addDetailRow(fragment, "Started", detail.startedAt); + addDetailRow(fragment, "Completed", detail.completedAt); + addDetailRow(fragment, "Artifact", detail.convertedResourcePath); + el.jobDetailBody.appendChild(fragment); el.retryJobBtn.hidden = !detail.deadLettered; } diff --git a/src/test/js/demo-integration.test.mjs b/src/test/js/demo-integration.test.mjs index 0f0437ea2..bd7e6963c 100644 --- a/src/test/js/demo-integration.test.mjs +++ b/src/test/js/demo-integration.test.mjs @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { MockElement } from "./mock-dom.mjs"; +import { MockElement, MockDocumentFragment } from "./mock-dom.mjs"; const elementIds = [ "upload-form", @@ -54,6 +54,9 @@ test("the executable demo renders inert actions and blocks repeated status activ createElement(tagName) { return new MockElement(tagName); }, + createDocumentFragment() { + return new MockDocumentFragment(); + } }; globalThis.window = { confirm() { diff --git a/src/test/js/dom-utils.test.mjs b/src/test/js/dom-utils.test.mjs index 94b151063..9daf9358a 100644 --- a/src/test/js/dom-utils.test.mjs +++ b/src/test/js/dom-utils.test.mjs @@ -6,11 +6,14 @@ import { createLink, setBusyState } from "../../main/resources/static/assets/viewer/dom-utils.js"; -import { MockElement, MockTextNode } from "./mock-dom.mjs"; +import { MockElement, MockTextNode, MockDocumentFragment } from "./mock-dom.mjs"; globalThis.document = { createElement(tagName) { return new MockElement(tagName); + }, + createDocumentFragment() { + return new MockDocumentFragment(); } }; diff --git a/src/test/js/mock-dom.mjs b/src/test/js/mock-dom.mjs index 774116bc0..f4d0a91c1 100644 --- a/src/test/js/mock-dom.mjs +++ b/src/test/js/mock-dom.mjs @@ -1,3 +1,23 @@ +export class MockDocumentFragment { + constructor() { + this.childNodes = []; + } + + appendChild(node) { + if (node && node.constructor && node.constructor.name === "MockDocumentFragment") { + this.childNodes.push(...node.childNodes); + node.childNodes = []; // DocumentFragment gets emptied when appended + return node; + } + this.childNodes.push(node); + return node; + } + + append(...nodes) { + this.childNodes.push(...nodes); + } +} + export class MockTextNode { constructor(text) { this.type = "text"; @@ -31,6 +51,11 @@ export class MockElement { } appendChild(node) { + if (node && node.constructor && node.constructor.name === "MockDocumentFragment") { + this.childNodes.push(...node.childNodes); + node.childNodes = []; // DocumentFragment gets emptied when appended + return node; + } this.childNodes.push(node); return node; }