From 6375ad1f65f4c5abc4025667fc635f5c3f3d3e0f Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 1 Oct 2026 21:38:26 +0000 Subject: [PATCH 1/8] =?UTF-8?q?=E2=9A=A1=20Bolt:=20[=ED=94=84=EB=A1=A0?= =?UTF-8?q?=ED=8A=B8=EC=97=94=EB=93=9C=20=EC=84=B1=EB=8A=A5=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0]=20DocumentFragment=EB=A5=BC=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=9C=20DOM=20=EB=A0=8C=EB=8D=94=EB=A7=81=20=EC=B5=9C?= =?UTF-8?q?=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿ’ก ๋ฌด์—‡: renderHistory์™€ renderJobDetail ํ•จ์ˆ˜์—์„œ DocumentFragment๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •. ๐ŸŽฏ ์™œ: ์—ฌ๋Ÿฌ ๋ฒˆ์˜ DOM reflow๋ฅผ ๋ฐฉ์ง€ํ•˜์—ฌ ํ”„๋ก ํŠธ์—”๋“œ ๋ Œ๋”๋ง ์„ฑ๋Šฅ์„ ์ตœ์ ํ™”. ๐Ÿ“Š ์˜ํ–ฅ: ๋ฆฌ์ŠคํŠธ ๋ Œ๋”๋ง ์†๋„ ํ–ฅ์ƒ. ๐Ÿ”ฌ ์ธก์ •: ํ…Œ์ŠคํŠธ ์Šค์œ„ํŠธ๋ฅผ ํ†ตํ•ด ์ •์ƒ ์ž‘๋™ ํ™•์ธ. --- .../resources/static/assets/viewer/demo.js | 36 +++++++++++-------- src/test/js/demo-integration.test.mjs | 5 ++- src/test/js/dom-utils.test.mjs | 5 ++- src/test/js/mock-dom.mjs | 25 +++++++++++++ 4 files changed, 55 insertions(+), 16 deletions(-) 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; } From aa9ec00381767830fb00a136c84224db8c8cf963 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 1 Oct 2026 21:40:39 +0000 Subject: [PATCH 2/8] =?UTF-8?q?=E2=9A=A1=20Bolt:=20[=ED=94=84=EB=A1=A0?= =?UTF-8?q?=ED=8A=B8=EC=97=94=EB=93=9C=20=EC=84=B1=EB=8A=A5=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0]=20DocumentFragment=EB=A5=BC=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=9C=20DOM=20=EB=A0=8C=EB=8D=94=EB=A7=81=20=EC=B5=9C?= =?UTF-8?q?=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿ’ก ๋ฌด์—‡: renderHistory์™€ renderJobDetail ํ•จ์ˆ˜์—์„œ DocumentFragment๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •. ๐ŸŽฏ ์™œ: ์—ฌ๋Ÿฌ ๋ฒˆ์˜ DOM reflow๋ฅผ ๋ฐฉ์ง€ํ•˜์—ฌ ํ”„๋ก ํŠธ์—”๋“œ ๋ Œ๋”๋ง ์„ฑ๋Šฅ์„ ์ตœ์ ํ™”. ๐Ÿ“Š ์˜ํ–ฅ: ๋ฆฌ์ŠคํŠธ ๋ Œ๋”๋ง ์†๋„ ํ–ฅ์ƒ. ๐Ÿ”ฌ ์ธก์ •: ํ…Œ์ŠคํŠธ ์Šค์œ„ํŠธ๋ฅผ ํ†ตํ•ด ์ •์ƒ ์ž‘๋™ ํ™•์ธ. From 507f7889455bd588932e687a0211f0749bdf6b1c Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 1 Oct 2026 21:45:48 +0000 Subject: [PATCH 3/8] =?UTF-8?q?=E2=9A=A1=20Bolt:=20[=ED=94=84=EB=A1=A0?= =?UTF-8?q?=ED=8A=B8=EC=97=94=EB=93=9C=20=EC=84=B1=EB=8A=A5=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0]=20DocumentFragment=EB=A5=BC=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=9C=20DOM=20=EB=A0=8C=EB=8D=94=EB=A7=81=20=EC=B5=9C?= =?UTF-8?q?=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿ’ก ๋ฌด์—‡: renderHistory์™€ renderJobDetail ํ•จ์ˆ˜์—์„œ DocumentFragment๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •. ๐ŸŽฏ ์™œ: ์—ฌ๋Ÿฌ ๋ฒˆ์˜ DOM reflow๋ฅผ ๋ฐฉ์ง€ํ•˜์—ฌ ํ”„๋ก ํŠธ์—”๋“œ ๋ Œ๋”๋ง ์„ฑ๋Šฅ์„ ์ตœ์ ํ™”. ๐Ÿ“Š ์˜ํ–ฅ: ๋ฆฌ์ŠคํŠธ ๋ Œ๋”๋ง ์†๋„ ํ–ฅ์ƒ. ๐Ÿ”ฌ ์ธก์ •: ํ…Œ์ŠคํŠธ ์Šค์œ„ํŠธ๋ฅผ ํ†ตํ•ด ์ •์ƒ ์ž‘๋™ ํ™•์ธ. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From fd46afdbdc8e43be9e7bba1fb4443d864052b987 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 1 Oct 2026 21:48:03 +0000 Subject: [PATCH 4/8] =?UTF-8?q?=E2=9A=A1=20Bolt:=20[=ED=94=84=EB=A1=A0?= =?UTF-8?q?=ED=8A=B8=EC=97=94=EB=93=9C=20=EC=84=B1=EB=8A=A5=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0]=20DocumentFragment=EB=A5=BC=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=9C=20DOM=20=EB=A0=8C=EB=8D=94=EB=A7=81=20=EC=B5=9C?= =?UTF-8?q?=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿ’ก ๋ฌด์—‡: renderHistory์™€ renderJobDetail ํ•จ์ˆ˜์—์„œ DocumentFragment๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •. ๐ŸŽฏ ์™œ: ์—ฌ๋Ÿฌ ๋ฒˆ์˜ DOM reflow๋ฅผ ๋ฐฉ์ง€ํ•˜์—ฌ ํ”„๋ก ํŠธ์—”๋“œ ๋ Œ๋”๋ง ์„ฑ๋Šฅ์„ ์ตœ์ ํ™”. ๐Ÿ“Š ์˜ํ–ฅ: ๋ฆฌ์ŠคํŠธ ๋ Œ๋”๋ง ์†๋„ ํ–ฅ์ƒ. ๐Ÿ”ฌ ์ธก์ •: ํ…Œ์ŠคํŠธ ์Šค์œ„ํŠธ๋ฅผ ํ†ตํ•ด ์ •์ƒ ์ž‘๋™ ํ™•์ธ. From 988fc27bdba61c459d4aaeb17efcb7f42ea0ab8c Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 1 Oct 2026 22:02:40 +0000 Subject: [PATCH 5/8] =?UTF-8?q?=E2=9A=A1=20Bolt:=20[=ED=94=84=EB=A1=A0?= =?UTF-8?q?=ED=8A=B8=EC=97=94=EB=93=9C=20=EC=84=B1=EB=8A=A5=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0]=20DocumentFragment=EB=A5=BC=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=9C=20DOM=20=EB=A0=8C=EB=8D=94=EB=A7=81=20=EC=B5=9C?= =?UTF-8?q?=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿ’ก ๋ฌด์—‡: renderHistory์™€ renderJobDetail ํ•จ์ˆ˜์—์„œ DocumentFragment๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •. ๐ŸŽฏ ์™œ: ์—ฌ๋Ÿฌ ๋ฒˆ์˜ DOM reflow๋ฅผ ๋ฐฉ์ง€ํ•˜์—ฌ ํ”„๋ก ํŠธ์—”๋“œ ๋ Œ๋”๋ง ์„ฑ๋Šฅ์„ ์ตœ์ ํ™”. ๐Ÿ“Š ์˜ํ–ฅ: ๋ฆฌ์ŠคํŠธ ๋ Œ๋”๋ง ์†๋„ ํ–ฅ์ƒ. ๐Ÿ”ฌ ์ธก์ •: ํ…Œ์ŠคํŠธ ์Šค์œ„ํŠธ๋ฅผ ํ†ตํ•ด ์ •์ƒ ์ž‘๋™ ํ™•์ธ. From 6eb26e3b9acb13ee80403c4f80e45bfa2a837019 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 2 Oct 2026 07:05:54 +0900 Subject: [PATCH 6/8] chore: isolate document fragment delta --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ddee8763e..af3ec7ad2 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.2 + 2.22.1 1.5.35 From be8fe1c8bf8931db95a3a23fa1b91b7004fe200c Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 1 Oct 2026 22:33:27 +0000 Subject: [PATCH 7/8] =?UTF-8?q?=E2=9A=A1=20Bolt:=20[=ED=94=84=EB=A1=A0?= =?UTF-8?q?=ED=8A=B8=EC=97=94=EB=93=9C=20=EC=84=B1=EB=8A=A5=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0]=20DocumentFragment=EB=A5=BC=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=9C=20DOM=20=EB=A0=8C=EB=8D=94=EB=A7=81=20=EC=B5=9C?= =?UTF-8?q?=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿ’ก ๋ฌด์—‡: renderHistory์™€ renderJobDetail ํ•จ์ˆ˜์—์„œ DocumentFragment๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •. ๐ŸŽฏ ์™œ: ์—ฌ๋Ÿฌ ๋ฒˆ์˜ DOM reflow๋ฅผ ๋ฐฉ์ง€ํ•˜์—ฌ ํ”„๋ก ํŠธ์—”๋“œ ๋ Œ๋”๋ง ์„ฑ๋Šฅ์„ ์ตœ์ ํ™”. ๐Ÿ“Š ์˜ํ–ฅ: ๋ฆฌ์ŠคํŠธ ๋ Œ๋”๋ง ์†๋„ ํ–ฅ์ƒ. ๐Ÿ”ฌ ์ธก์ •: ํ…Œ์ŠคํŠธ ์Šค์œ„ํŠธ๋ฅผ ํ†ตํ•ด ์ •์ƒ ์ž‘๋™ ํ™•์ธ. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 31a715cc5bbac7fbb38b9154c8b9cbed698a386c Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 1 Oct 2026 22:35:56 +0000 Subject: [PATCH 8/8] =?UTF-8?q?=E2=9A=A1=20Bolt:=20[=ED=94=84=EB=A1=A0?= =?UTF-8?q?=ED=8A=B8=EC=97=94=EB=93=9C=20=EC=84=B1=EB=8A=A5=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0]=20DocumentFragment=EB=A5=BC=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=9C=20DOM=20=EB=A0=8C=EB=8D=94=EB=A7=81=20=EC=B5=9C?= =?UTF-8?q?=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿ’ก ๋ฌด์—‡: renderHistory์™€ renderJobDetail ํ•จ์ˆ˜์—์„œ DocumentFragment๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •. ๐ŸŽฏ ์™œ: ์—ฌ๋Ÿฌ ๋ฒˆ์˜ DOM reflow๋ฅผ ๋ฐฉ์ง€ํ•˜์—ฌ ํ”„๋ก ํŠธ์—”๋“œ ๋ Œ๋”๋ง ์„ฑ๋Šฅ์„ ์ตœ์ ํ™”. ๐Ÿ“Š ์˜ํ–ฅ: ๋ฆฌ์ŠคํŠธ ๋ Œ๋”๋ง ์†๋„ ํ–ฅ์ƒ. ๐Ÿ”ฌ ์ธก์ •: ํ…Œ์ŠคํŠธ ์Šค์œ„ํŠธ๋ฅผ ํ†ตํ•ด ์ •์ƒ ์ž‘๋™ ํ™•์ธ.