From 15307eb46ca2b5d0495092f0dcbab94edebc695f Mon Sep 17 00:00:00 2001 From: Pushkar Rimmalapudi Date: Thu, 19 Feb 2026 17:00:01 -0800 Subject: [PATCH 01/14] chore: add .gitignore and resume skill --- .DS_Store | Bin 6148 -> 0 bytes .claude/skills/resume/SKILL.md | 0 .gitignore | 14 ++++++++++++++ 3 files changed, 14 insertions(+) delete mode 100644 .DS_Store create mode 100644 .claude/skills/resume/SKILL.md create mode 100644 .gitignore diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 121f068855afb37241123adf9681d12ff1dd1170..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKu};G<5PdEg3SBxdFoprKA+=u+s_+G6FQkEjNb5?pLNMhM*jeBM_y-om1V6yS zr|{0UpizOP2%+jOx_kE7cd=h&I|g7n)4T;V0o192(HhMcBJHAcQVX6{qLCcMpw;c$ z;drj6I+Our;6E}TYd69U1~|o}vVP6h^K9GDrfF8#jK2EYS)+Z|dfKhF=RdZOPiFRp z+BHKF;{r#>(8Cx7rfv))*4lh}IHsrI<4Ys-G2s|3MmVIGqs3)@-Y+t13vND(C#y^z zS2w?rbC$IA&HL?JPujv0AKFi{InI~;*GKsn3+JtTBs?&s%JMQG%Z5^KcvPzlC Date: Thu, 19 Feb 2026 17:02:20 -0800 Subject: [PATCH 02/14] feat: popup-configurable chunk count persisted in chrome.storage - Add Utils.clampChunkCount helper (shared by popup + SW) - importScripts('utils.js') in background.js for shared clamping - Chunk count input (2-32, default 10) in Downloads tab - getChunkCount() reads storage before every download/upload - Covers START_DOWNLOAD, UPLOAD_FILE, and context-menu paths - Add [ChunkFlow] log lines to confirm count in SW console --- .../background.js | 45 +++++++++++++------ web_plugin_22_full_functionality/popup.css | 20 +++++++++ web_plugin_22_full_functionality/popup.html | 4 ++ web_plugin_22_full_functionality/popup.js | 25 +++++++++++ web_plugin_22_full_functionality/utils.js | 6 +++ 5 files changed, 86 insertions(+), 14 deletions(-) diff --git a/web_plugin_22_full_functionality/background.js b/web_plugin_22_full_functionality/background.js index ebb8ee1..07b07c3 100644 --- a/web_plugin_22_full_functionality/background.js +++ b/web_plugin_22_full_functionality/background.js @@ -1,3 +1,5 @@ +importScripts('utils.js'); + let uploadedFiles = []; let popupPort = null; @@ -9,6 +11,7 @@ chrome.runtime.onConnect.addListener((port) => { }); function downloadInChunks(url, numberOfChunks = 10) { + console.log(`[ChunkFlow] downloadInChunks: ${numberOfChunks} chunks for ${url}`); return fetch(url, { method: 'HEAD' }) .then(response => { console.log('Checking for range header support'); @@ -208,13 +211,23 @@ function storeUploadedFileDetails(fileName, fileSize, fileType) { }); } +function getChunkCount(callback) { + chrome.storage.local.get({ chunkCount: 10 }, (data) => { + const count = Utils.clampChunkCount(data.chunkCount); + console.log(`[ChunkFlow] getChunkCount: resolved to ${count}`); + callback(count); + }); +} + chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { switch (message.type) { case "START_DOWNLOAD": console.log("Starting download for URL:", message.url); - downloadInChunks(message.url); - sendResponse({ success: true }); - break; + getChunkCount((count) => { + downloadInChunks(message.url, count); + sendResponse({ success: true }); + }); + return true; case "DELETE_DOWNLOAD": chrome.downloads.removeFile(message.downloadId, () => { @@ -260,16 +273,18 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { return; } - handleUpload(message.fileData, message.fileName, message.uploadUrl) - .then(response => { - console.log('Upload successful:', response); - storeUploadedFileDetails(message.fileName, message.fileSize, message.fileType); - sendResponse({ success: true, response }); - }) - .catch(error => { - console.error('Upload failed:', error); - sendResponse({ success: false, error: error.message }); - }); + getChunkCount((count) => { + handleUpload(message.fileData, message.fileName, message.uploadUrl, count) + .then(response => { + console.log('Upload successful:', response); + storeUploadedFileDetails(message.fileName, message.fileSize, message.fileType); + sendResponse({ success: true, response }); + }) + .catch(error => { + console.error('Upload failed:', error); + sendResponse({ success: false, error: error.message }); + }); + }); return true; case 'GET_UPLOADED_FILES': @@ -315,6 +330,8 @@ chrome.runtime.onInstalled.addListener(() => { chrome.contextMenus.onClicked.addListener((info, tab) => { if (info.menuItemId === "download-with-chunks" && info.linkUrl) { console.log("Context menu download:", info.linkUrl); - downloadInChunks(info.linkUrl); + getChunkCount((count) => { + downloadInChunks(info.linkUrl, count); + }); } }); \ No newline at end of file diff --git a/web_plugin_22_full_functionality/popup.css b/web_plugin_22_full_functionality/popup.css index d7fc242..955017b 100644 --- a/web_plugin_22_full_functionality/popup.css +++ b/web_plugin_22_full_functionality/popup.css @@ -5,6 +5,26 @@ body { padding: 0; } +.chunk-count-row { + padding: 10px 10px 6px; + display: flex; + align-items: center; + gap: 8px; +} + +.chunk-count-row label { + font-size: 13px; + color: #333; +} + +.chunk-count-row #chunk-count { + width: 56px; + padding: 4px 6px; + font-size: 13px; + border: 1px solid #e0e0e0; + border-radius: 4px; +} + #downloads-list { padding: 10px; } diff --git a/web_plugin_22_full_functionality/popup.html b/web_plugin_22_full_functionality/popup.html index 0c15b56..33fdeca 100644 --- a/web_plugin_22_full_functionality/popup.html +++ b/web_plugin_22_full_functionality/popup.html @@ -14,6 +14,10 @@
+
+ + +
diff --git a/web_plugin_22_full_functionality/popup.js b/web_plugin_22_full_functionality/popup.js index 49a773b..cf00db8 100644 --- a/web_plugin_22_full_functionality/popup.js +++ b/web_plugin_22_full_functionality/popup.js @@ -242,7 +242,32 @@ const displayUploadedFiles = () => { const formatFileSize = Utils.formatFileSize; +const loadChunkCount = () => { + chrome.storage.local.get({ chunkCount: 10 }, (data) => { + const val = Utils.clampChunkCount(data.chunkCount); + const input = document.getElementById('chunk-count'); + if (input) input.value = val; + }); +}; + +const saveChunkCount = (value) => { + const val = Utils.clampChunkCount(value); + chrome.storage.local.set({ chunkCount: val }, () => { + const input = document.getElementById('chunk-count'); + if (input) input.value = val; + }); +}; + document.addEventListener("DOMContentLoaded", () => { + loadChunkCount(); + + const chunkCountInput = document.getElementById('chunk-count'); + if (chunkCountInput) { + chunkCountInput.addEventListener('change', () => { + saveChunkCount(chunkCountInput.value); + }); + } + document.getElementById("downloads-tab").addEventListener("click", () => { document.getElementById("downloads-section").classList.add("active"); document.getElementById("uploads-section").classList.remove("active"); diff --git a/web_plugin_22_full_functionality/utils.js b/web_plugin_22_full_functionality/utils.js index c1f731b..a602f7b 100644 --- a/web_plugin_22_full_functionality/utils.js +++ b/web_plugin_22_full_functionality/utils.js @@ -44,6 +44,12 @@ const Utils = { clearTimeout(timeout); timeout = setTimeout(later, wait); }; + }, + + // Clamp chunk count to valid range. Returns def if value is missing/NaN. + clampChunkCount: (val, min = 2, max = 32, def = 10) => { + const n = Number(val); + return isNaN(n) || n === 0 ? def : Math.min(max, Math.max(min, Math.round(n))); } }; From 72a68f806bbdde5a5289bfa0bf5ae79c415f1c4c Mon Sep 17 00:00:00 2001 From: Pushkar Rimmalapudi Date: Thu, 19 Feb 2026 17:02:25 -0800 Subject: [PATCH 03/14] test: scaffold Jest unit tests for Utils (26 cases) --- package-lock.json | 3651 +++++++++++++++++++++++++++++++++++++++++++ package.json | 11 + tests/utils.test.js | 131 ++ 3 files changed, 3793 insertions(+) create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 tests/utils.test.js diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..659dbd1 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3651 @@ +{ + "name": "chunkflow", + "version": "2.3.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "chunkflow", + "version": "2.3.0", + "devDependencies": { + "jest": "^29.7.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/node": { + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.0.tgz", + "integrity": "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001770", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz", + "integrity": "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", + "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3c3f17b --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ + "name": "chunkflow", + "version": "2.3.0", + "private": true, + "scripts": { + "test": "jest" + }, + "devDependencies": { + "jest": "^29.7.0" + } +} diff --git a/tests/utils.test.js b/tests/utils.test.js new file mode 100644 index 0000000..8e32cd2 --- /dev/null +++ b/tests/utils.test.js @@ -0,0 +1,131 @@ +'use strict'; + +const Utils = require('../web_plugin_22_full_functionality/utils.js'); + +// ── formatFileSize ──────────────────────────────────────────────────────────── +describe('Utils.formatFileSize', () => { + test('0 bytes', () => { + expect(Utils.formatFileSize(0)).toBe('0 Bytes'); + }); + + test('1 KB', () => { + expect(Utils.formatFileSize(1024)).toBe('1 KB'); + }); + + test('1.5 MB', () => { + expect(Utils.formatFileSize(1024 * 1024 * 1.5)).toBe('1.5 MB'); + }); + + test('1 GB', () => { + expect(Utils.formatFileSize(1024 ** 3)).toBe('1 GB'); + }); +}); + +// ── validateUrl ─────────────────────────────────────────────────────────────── +describe('Utils.validateUrl', () => { + test('valid https URL', () => { + expect(Utils.validateUrl('https://example.com/file.zip')).toBe(true); + }); + + test('valid http URL', () => { + expect(Utils.validateUrl('http://example.com')).toBe(true); + }); + + test('invalid: plain string', () => { + expect(Utils.validateUrl('not-a-url')).toBe(false); + }); + + test('invalid: empty string', () => { + expect(Utils.validateUrl('')).toBe(false); + }); +}); + +// ── sanitizeFilename ────────────────────────────────────────────────────────── +describe('Utils.sanitizeFilename', () => { + test('replaces forbidden chars with underscores', () => { + expect(Utils.sanitizeFilename('myfile:name.zip')).toBe('my_bad_file_name.zip'); + }); + + test('leaves clean filename untouched', () => { + expect(Utils.sanitizeFilename('clean_file.pdf')).toBe('clean_file.pdf'); + }); + + test('truncates to 255 chars', () => { + const long = 'a'.repeat(300); + expect(Utils.sanitizeFilename(long).length).toBe(255); + }); +}); + +// ── getFileExtension ────────────────────────────────────────────────────────── +describe('Utils.getFileExtension', () => { + test('returns lowercase extension', () => { + expect(Utils.getFileExtension('Report.PDF')).toBe('pdf'); + }); + + test('returns empty string for no extension', () => { + expect(Utils.getFileExtension('Makefile')).toBe(''); + }); + + test('handles multiple dots (returns last)', () => { + expect(Utils.getFileExtension('archive.tar.gz')).toBe('gz'); + }); +}); + +// ── isImageFile ─────────────────────────────────────────────────────────────── +describe('Utils.isImageFile', () => { + test('png is an image', () => { + expect(Utils.isImageFile('photo.png')).toBe(true); + }); + + test('webp is an image', () => { + expect(Utils.isImageFile('banner.webp')).toBe(true); + }); + + test('pdf is not an image', () => { + expect(Utils.isImageFile('report.pdf')).toBe(false); + }); + + test('zip is not an image', () => { + expect(Utils.isImageFile('archive.zip')).toBe(false); + }); +}); + +// ── clampChunkCount ─────────────────────────────────────────────────────────── +describe('Utils.clampChunkCount', () => { + test('returns default (10) when called with no args', () => { + expect(Utils.clampChunkCount(undefined)).toBe(10); + }); + + test('returns default for NaN', () => { + expect(Utils.clampChunkCount('abc')).toBe(10); + }); + + test('clamps below minimum to 2', () => { + expect(Utils.clampChunkCount(0)).toBe(10); // 0 treated as missing → default + expect(Utils.clampChunkCount(1)).toBe(2); + }); + + test('clamps above maximum to 32', () => { + expect(Utils.clampChunkCount(100)).toBe(32); + }); + + test('valid mid-range value passes through', () => { + expect(Utils.clampChunkCount(8)).toBe(8); + expect(Utils.clampChunkCount(16)).toBe(16); + }); + + test('boundary values: exactly 2 and 32', () => { + expect(Utils.clampChunkCount(2)).toBe(2); + expect(Utils.clampChunkCount(32)).toBe(32); + }); + + test('rounds float to nearest integer', () => { + expect(Utils.clampChunkCount(7.6)).toBe(8); + expect(Utils.clampChunkCount(7.2)).toBe(7); + }); + + test('respects custom min/max/default', () => { + expect(Utils.clampChunkCount(50, 1, 20, 5)).toBe(20); + expect(Utils.clampChunkCount(undefined, 1, 20, 5)).toBe(5); + }); +}); From 7923dfcf5af8e5a0cf0c51c5d7f1c503f3baaa84 Mon Sep 17 00:00:00 2001 From: Pushkar Rimmalapudi Date: Thu, 19 Feb 2026 17:11:47 -0800 Subject: [PATCH 04/14] chore: ignore .serena/ MCP tooling directory --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 81997b6..0f2338e 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ coverage/ .idea/ *.swp *.swo + +# MCP / AI tooling +.serena/ From 14be5bdb6c7d9acd0c88e57e1df1e24c3c20ceae Mon Sep 17 00:00:00 2001 From: Pushkar Rimmalapudi <63349658+rpushkar9@users.noreply.github.com> Date: Thu, 19 Feb 2026 17:21:03 -0800 Subject: [PATCH 05/14] fix: rename context menu title to ChunkFlow; update README config section --- README.md | 4 ++-- web_plugin_22_full_functionality/background.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ebd2b9f..4b216a7 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ ChunkFlow is a powerful Chrome extension that **accelerates downloads and upload - **Error isolation** per chunk ## 🔧 Configuration -- **Default chunks**: 10 parallel streams (configurable in code) +- **Default chunks**: 10 parallel streams (configurable via the popup UI, 2–32, persisted in `chrome.storage.local`) - **Update frequency**: 500ms active, 2s idle - **File detection**: Automatic by extension - **Memory usage**: Optimized for large files @@ -161,6 +161,6 @@ ChunkFlow is a powerful Chrome extension that **accelerates downloads and upload - **Smart chunk sizing**: Dynamic chunk count based on file size and connection speed - **Content-Disposition parsing**: Better filename detection from HTTP headers - **Upload progress UI**: Real-time chunk-level upload progress visualization -- **Configuration panel**: User-configurable chunk settings +- ~~**Configuration panel**: User-configurable chunk settings~~ ✅ Done in v2.3.0 - **Download queue**: Batch download management with priority controls - **Bandwidth throttling**: Optional speed limiting for chunked transfers \ No newline at end of file diff --git a/web_plugin_22_full_functionality/background.js b/web_plugin_22_full_functionality/background.js index 07b07c3..26dc935 100644 --- a/web_plugin_22_full_functionality/background.js +++ b/web_plugin_22_full_functionality/background.js @@ -322,7 +322,7 @@ chrome.downloads.onErased.addListener((downloadId) => { chrome.runtime.onInstalled.addListener(() => { chrome.contextMenus.create({ id: "download-with-chunks", - title: "Download with MyEasyDownloader", + title: "Download with ChunkFlow", contexts: ["link"] }); }); From 7bba2b44e90f2999413ca77e868f33daa80289dc Mon Sep 17 00:00:00 2001 From: Pushkar Rimmalapudi <63349658+rpushkar9@users.noreply.github.com> Date: Thu, 19 Feb 2026 17:21:03 -0800 Subject: [PATCH 06/14] docs: add session log for 2026-02-19 chunk count UI + tests --- .../2026-02-19-chunk-count-ui-tests.md | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 notes/sessions/2026-02-19-chunk-count-ui-tests.md diff --git a/notes/sessions/2026-02-19-chunk-count-ui-tests.md b/notes/sessions/2026-02-19-chunk-count-ui-tests.md new file mode 100644 index 0000000..149a812 --- /dev/null +++ b/notes/sessions/2026-02-19-chunk-count-ui-tests.md @@ -0,0 +1,127 @@ +# Session: 2026-02-19 — Chunk count UI, Jest tests, PR polish + +## Goal +Make the download/upload chunk count user-configurable via the popup UI, +persist the value in `chrome.storage.local`, cover the logic with unit tests, +and get the branch PR-ready with clean commits and identity. + +## What I set up + +### Feature +- Added a "Chunk count" number input (min 2, max 32, default 10) to the + Downloads tab in the popup. +- Popup reads/writes `chunkCount` from `chrome.storage.local` on every open + and change event. +- Extracted `Utils.clampChunkCount(val, min, max, def)` into `utils.js` so + both popup and service worker share one implementation. +- Service worker (`background.js`) uses `importScripts('utils.js')` to load + Utils, then calls `getChunkCount()` which reads storage and clamps before + every `downloadInChunks` and `handleUpload` call — covers START_DOWNLOAD + message, UPLOAD_FILE message, and the context-menu path. +- Added `[ChunkFlow]` log lines so the resolved count is visible in the SW + console during smoke testing. + +### PR polish +- Fixed stale context menu title: "MyEasyDownloader" → "ChunkFlow" +- Updated README §Configuration to reflect popup configurability (2–32) +- Struck through the "Configuration panel" roadmap item (now shipped) + +### Repo hygiene +- Created `.gitignore` (`.DS_Store`, `node_modules/`, `coverage/`, `.serena/`) +- Removed `.DS_Store` from git tracking +- Set global git identity: `Pushkar Rimmalapudi ` + +### Tests +- Scaffolded Jest 29 at repo root (`package.json`, `tests/utils.test.js`) +- 26 unit tests covering `formatFileSize`, `validateUrl`, `sanitizeFilename`, + `getFileExtension`, `isImageFile`, and all `clampChunkCount` boundary cases. + +## Files created / modified +- `web_plugin_22_full_functionality/background.js` — `importScripts('utils.js')`, + `getChunkCount()` helper, `Utils.clampChunkCount`, log lines, context menu + title fix +- `web_plugin_22_full_functionality/utils.js` — added `clampChunkCount` +- `web_plugin_22_full_functionality/popup.html` — chunk count `` row +- `web_plugin_22_full_functionality/popup.js` — `loadChunkCount` / + `saveChunkCount` using storage + Utils.clampChunkCount +- `web_plugin_22_full_functionality/popup.css` — `.chunk-count-row` styles +- `README.md` — updated §Configuration and roadmap +- `package.json` — new; Jest 29 devDependency, `npm test` script +- `tests/utils.test.js` — new; 26 Jest tests +- `.gitignore` — new +- `notes/sessions/2026-02-19-chunk-count-ui-tests.md` — this file + +## Branch +`feat/chunk-count-config` (pushed to `origin/feat/chunk-count-config`) + +## Commands run +```bash +# Git identity (one-time global setup) +git config --global user.name "Pushkar Rimmalapudi" +git config --global user.email "rpushkar@uw.edu" + +# Feature branch setup +git reset --mixed HEAD~1 # undo accidental commit on main (unpushed) +git rm --cached .DS_Store # untrack OS junk +git checkout -b feat/chunk-count-config + +# Run tests +npm install +npm test +# → 26 passed, 0 failed + +# Commit sequence +git commit -m "chore: add .gitignore and resume skill" +git commit -m "feat: popup-configurable chunk count persisted in chrome.storage" +git commit -m "test: scaffold Jest unit tests for Utils (26 cases)" +git commit -m "chore: ignore .serena/ MCP tooling directory" +git commit -m "fix: rename context menu title to ChunkFlow; update README" + +git push -u origin feat/chunk-count-config +``` + +## Test results (actual run output) +``` +PASS tests/utils.test.js + Utils.formatFileSize 4 tests ✓ + Utils.validateUrl 4 tests ✓ + Utils.sanitizeFilename 3 tests ✓ + Utils.getFileExtension 3 tests ✓ + Utils.isImageFile 4 tests ✓ + Utils.clampChunkCount 8 tests ✓ + +Tests: 26 passed, 26 total +Time: 0.15s +``` + +## How to verify (quick checklist) +- [ ] Load unpacked: `chrome://extensions` → Load unpacked → `web_plugin_22_full_functionality/` +- [ ] No red error badge on extension card +- [ ] Open popup → "Chunk count:" input visible, shows 10 +- [ ] Set to 4, close + reopen popup → persists at 4 +- [ ] Open SW console (Inspect views: service worker) +- [ ] Right-click a direct file link → "Download with ChunkFlow" (not "MyEasyDownloader") +- [ ] SW console shows: `[ChunkFlow] getChunkCount: resolved to 4` +- [ ] SW console shows: `[ChunkFlow] downloadInChunks: 4 chunks for ` +- [ ] Type 100 in input → close+reopen → clamped to 32 +- [ ] Type 1 → close+reopen → clamped to 2 +- [ ] `npm test` → 26 passed + +## Known limitations +- Files are merged in-memory (Uint8Array) — large files will spike RAM +- Upload progress is per-chunk in console only, not visualised in popup UI +- `Content-Disposition` header not parsed; filename comes from URL path only +- Chunk count only applies to new downloads; in-progress downloads use + whatever count was active when they started + +## Next steps +1. Open PR on GitHub: `feat/chunk-count-config` → `main` +2. Fix git committer identity on older commits (`git commit --amend --reset-author`) + if needed before merge +3. Update polling to event-driven for upload progress (roadmap item) +4. Add `Content-Disposition` filename parsing (roadmap item) +5. Consider CI: add `.github/workflows/test.yml` to run `npm test` on push + +## Open questions / TODO +- Should chunk count also apply per-tab/per-site, or stay global? (currently global) +- Should the popup show the active chunk count during a download in progress? From 5b87bc76f1d4da2cab733e085637486a0922643a Mon Sep 17 00:00:00 2001 From: Pushkar Rimmalapudi <63349658+rpushkar9@users.noreply.github.com> Date: Thu, 19 Feb 2026 18:21:38 -0800 Subject: [PATCH 07/14] fix(manifest): add host_permissions to unblock service worker fetch Without host_permissions the SW's fetch() to external domains (Google Drive CDN, etc.) is subject to CORS rules. Since those servers do not return Access-Control-Allow-Origin for the extension origin, every chunk request silently fails with 'TypeError: Failed to fetch' and falls back to Chrome's native downloader. Adding lets the SW make credentialed cross-origin fetches so chunked downloads actually work on Drive and similar hosts. --- web_plugin_22_full_functionality/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/web_plugin_22_full_functionality/manifest.json b/web_plugin_22_full_functionality/manifest.json index bd5b9ed..6704d15 100644 --- a/web_plugin_22_full_functionality/manifest.json +++ b/web_plugin_22_full_functionality/manifest.json @@ -4,6 +4,7 @@ "description": "Accelerate downloads & uploads with parallel chunking technology", "optional_permissions": [ "management" ], "permissions": ["downloads", "storage", "contextMenus"], + "host_permissions": [""], "background": { "service_worker": "background.js" From bea2e5534867ab49103309db4260c5942f0ad762 Mon Sep 17 00:00:00 2001 From: Pushkar Rimmalapudi <63349658+rpushkar9@users.noreply.github.com> Date: Thu, 19 Feb 2026 18:21:49 -0800 Subject: [PATCH 08/14] fix(background): fix intermittent Drive downloads + code health Root causes fixed: - Use response.url (post-redirect final URL) for all chunk GETs so CDN auth tokens are not re-consumed on every chunk request (Google Drive, S3 signed URLs, etc.) - Add credentials:'include' to HEAD + chunk fetches so authenticated sessions (Google, Dropbox, etc.) serve the actual file - Add OOM guard: files > 500 MB skip in-memory assembly and go directly to Chrome's native downloader - Wrap each chunk fetch in fetchWithRetry (1 retry) before Promise.all fails the entire download on a transient network error - Parse Content-Disposition filename*= / filename= from HEAD response so Drive files get a real name instead of 'uc' Code health: - Remove dead let uploadedFiles = [] (never written in SW scope) - Add storeDownloadMode() to track chunked/normal/fallback per download ID in chrome.storage.local 'downloadModes' (consumed by popup) - Add credentials:'include' to checkServerSupport HEAD request - Improve [ChunkFlow] log lines throughout --- .../background.js | 318 +++++++++++------- 1 file changed, 199 insertions(+), 119 deletions(-) diff --git a/web_plugin_22_full_functionality/background.js b/web_plugin_22_full_functionality/background.js index 26dc935..277edbf 100644 --- a/web_plugin_22_full_functionality/background.js +++ b/web_plugin_22_full_functionality/background.js @@ -1,6 +1,9 @@ importScripts('utils.js'); -let uploadedFiles = []; +// Files larger than this are sent straight to Chrome's native downloader to avoid +// assembling gigabytes of ArrayBuffers in service-worker memory (OOM risk). +const CHUNK_MAX_BYTES = 500 * 1024 * 1024; // 500 MB + let popupPort = null; chrome.runtime.onConnect.addListener((port) => { @@ -10,103 +13,183 @@ chrome.runtime.onConnect.addListener((port) => { }); }); +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/** + * Fetch with automatic retry on network failure. + * maxRetries=1 means one retry after the initial attempt (2 total tries). + */ +function fetchWithRetry(url, options, maxRetries = 1) { + return fetch(url, options).catch((err) => { + if (maxRetries > 0) { + console.warn(`[ChunkFlow] fetch failed, retrying (${maxRetries} left): ${err.message}`); + return fetchWithRetry(url, options, maxRetries - 1); + } + throw err; + }); +} + +/** + * Extract a filename from the Content-Disposition response header. + * Tries RFC 5987 percent-encoded form first, then quoted, then bare. + * Falls back to the last path segment of fallbackUrl. + */ +function getFilename(response, fallbackUrl) { + const cd = response.headers.get('Content-Disposition'); + if (cd) { + // RFC 5987: filename*=UTF-8''percent-encoded-name + let m = cd.match(/filename\*=UTF-8''([^;\s]+)/i); + if (m) { + try { return Utils.sanitizeFilename(decodeURIComponent(m[1])); } catch {} + } + // Quoted: filename="name" + m = cd.match(/filename="([^"]+)"/i); + if (m) return Utils.sanitizeFilename(m[1]); + // Bare: filename=name + m = cd.match(/filename=([^;\s]+)/i); + if (m) return Utils.sanitizeFilename(m[1]); + } + try { + const seg = new URL(fallbackUrl).pathname.split('/').pop(); + return Utils.sanitizeFilename(seg || 'downloaded_file'); + } catch { + return 'downloaded_file'; + } +} + +/** + * Persist a download's mode (chunked | normal | fallback) in chrome.storage.local + * under the key 'downloadModes', keyed by string download ID. + * Capped at 100 entries (oldest-first eviction) to avoid unbounded growth. + */ +function storeDownloadMode(downloadId, mode) { + chrome.storage.local.get({ downloadModes: {} }, (data) => { + const modes = data.downloadModes; + modes[String(downloadId)] = mode; + const keys = Object.keys(modes); + if (keys.length > 100) delete modes[keys[0]]; + chrome.storage.local.set({ downloadModes: modes }); + }); +} + +// --------------------------------------------------------------------------- +// Download engine +// --------------------------------------------------------------------------- + function downloadInChunks(url, numberOfChunks = 10) { console.log(`[ChunkFlow] downloadInChunks: ${numberOfChunks} chunks for ${url}`); - return fetch(url, { method: 'HEAD' }) + + return fetch(url, { method: 'HEAD', credentials: 'include' }) .then(response => { - console.log('Checking for range header support'); - + console.log('[ChunkFlow] HEAD response received'); + + // Capture the URL after redirects so all chunk GETs go to the same + // CDN endpoint and use the same auth tokens (critical for Google Drive etc.) + const finalUrl = response.url || url; + const filename = getFilename(response, url); + if (response.headers.get('Accept-Ranges') !== 'bytes') { - console.log('Server does not support range requests, falling back to normal download'); - chrome.downloads.download({ - url: url, - filename: new URL(url).pathname.split("/").pop() || "downloaded_file" + console.log('[ChunkFlow] No range support — using normal download'); + chrome.downloads.download({ url, filename }, (id) => { + if (id) storeDownloadMode(id, 'normal'); }); return null; } const fileSize = parseInt(response.headers.get('Content-Length')); const mimeType = response.headers.get('Content-Type') || 'application/octet-stream'; - + if (!fileSize || fileSize <= 0) { throw new Error('Invalid or missing Content-Length header'); } - - return { fileSize, mimeType }; + + // Skip in-memory chunking for large files to avoid OOM in the service worker. + if (fileSize > CHUNK_MAX_BYTES) { + console.log(`[ChunkFlow] File too large for in-memory chunking ` + + `(${Utils.formatFileSize(fileSize)} > ${Utils.formatFileSize(CHUNK_MAX_BYTES)}) — using normal download`); + chrome.downloads.download({ url, filename }, (id) => { + if (id) storeDownloadMode(id, 'normal'); + }); + return null; + } + + return { fileSize, mimeType, finalUrl, filename }; }) .then((result) => { if (!result) return null; - - const { fileSize, mimeType } = result; + + const { fileSize, mimeType, finalUrl, filename } = result; const chunkSize = Math.ceil(fileSize / numberOfChunks); const chunkPromises = []; - + for (let i = 0; i < numberOfChunks; i++) { const start = i * chunkSize; const end = i === numberOfChunks - 1 ? fileSize - 1 : (start + chunkSize - 1); - + chunkPromises.push( - fetch(url, { - headers: { Range: `bytes=${start}-${end}` } - }) - .then(response => { - if (!response.ok) { - throw new Error(`Failed to fetch chunk ${i}: ${response.status}`); - } - return response.arrayBuffer(); + fetchWithRetry( + finalUrl, + { headers: { Range: `bytes=${start}-${end}` }, credentials: 'include' } + ).then(res => { + if (!res.ok) throw new Error(`Failed to fetch chunk ${i}: ${res.status}`); + return res.arrayBuffer(); }) ); } - + return Promise.all(chunkPromises) .then(chunks => { - const mergedChunks = new Uint8Array(chunks.reduce((acc, chunk) => acc + chunk.byteLength, 0)); + const totalBytes = chunks.reduce((acc, c) => acc + c.byteLength, 0); + const merged = new Uint8Array(totalBytes); let offset = 0; - chunks.forEach(chunk => { - mergedChunks.set(new Uint8Array(chunk), offset); - offset += chunk.byteLength; + chunks.forEach(c => { + merged.set(new Uint8Array(c), offset); + offset += c.byteLength; }); - - const mergedBlob = new Blob([mergedChunks], { type: mimeType }); - const objectURL = URL.createObjectURL(mergedBlob); - const filename = new URL(url).pathname.split("/").pop() || "downloaded_file"; - - chrome.downloads.download({ - url: objectURL, - filename: filename + + const blob = new Blob([merged], { type: mimeType }); + const objectURL = URL.createObjectURL(blob); + + chrome.downloads.download({ url: objectURL, filename }, (id) => { + if (id) storeDownloadMode(id, 'chunked'); }); if (popupPort) { - popupPort.postMessage({ - type: 'DOWNLOAD_READY', - url: objectURL, - filename: filename, - isChunked: true - }); + popupPort.postMessage({ type: 'DOWNLOAD_READY', url: objectURL, filename, isChunked: true }); } - - return mergedBlob; + + return blob; }); }) .catch(error => { - console.error('Download error:', error); + console.error('[ChunkFlow] Download error:', error); if (popupPort) { - popupPort.postMessage({ - type: 'ERROR', - message: `Download failed: ${error.message}` + popupPort.postMessage({ type: 'ERROR', message: `Download failed: ${error.message}` }); + } + + // Best-effort fallback to Chrome's native downloader. + console.log('[ChunkFlow] Falling back to normal download'); + try { + const filename = new URL(url).pathname.split('/').pop() || 'downloaded_file'; + chrome.downloads.download({ url, filename }, (id) => { + if (id) storeDownloadMode(id, 'fallback'); + }); + } catch { + chrome.downloads.download({ url }, (id) => { + if (id) storeDownloadMode(id, 'fallback'); }); } - - console.log('Falling back to normal download due to error'); - chrome.downloads.download({ - url: url, - filename: new URL(url).pathname.split("/").pop() || "downloaded_file" - }); }); } +// --------------------------------------------------------------------------- +// Upload engine (unchanged logic; credentials added to server check) +// --------------------------------------------------------------------------- + function checkServerSupport(uploadUrl) { - return fetch(uploadUrl, { method: 'HEAD' }) + return fetch(uploadUrl, { method: 'HEAD', credentials: 'include' }) .then(response => { if (response.ok) { return response.headers.get('Accept-Ranges') === 'bytes'; @@ -125,32 +208,28 @@ function uploadFileNormally(fileData, fileName, uploadUrl) { const blob = new Blob([fileData]); formData.append('file', blob, fileName); - return fetch(uploadUrl, { - method: 'POST', - body: formData - }) - .then(response => { - if (response.ok) { - return response.text(); - } else { - throw new Error(`Upload failed: ${response.status} ${response.statusText}`); - } - }); + return fetch(uploadUrl, { method: 'POST', body: formData }) + .then(response => { + if (response.ok) { + return response.text(); + } else { + throw new Error(`Upload failed: ${response.status} ${response.statusText}`); + } + }); } function uploadInChunks(fileData, fileName, uploadUrl, numberOfChunks = 10) { - const fileSize = fileData.byteLength; + const fileSize = fileData.byteLength; const chunkSize = Math.ceil(fileSize / numberOfChunks); const chunkPromises = []; - + for (let i = 0; i < numberOfChunks; i++) { - const start = i * chunkSize; - const end = Math.min(start + chunkSize, fileSize); + const start = i * chunkSize; + const end = Math.min(start + chunkSize, fileSize); const chunkData = fileData.slice(start, end); - chunkPromises.push(uploadChunk(chunkData, uploadUrl, start, end - 1, fileSize)); } - + return Promise.all(chunkPromises); } @@ -160,26 +239,23 @@ function uploadChunk(chunkData, uploadUrl, start, end, totalSize) { xhr.open('POST', uploadUrl, true); xhr.setRequestHeader('Content-Range', `bytes ${start}-${end}/${totalSize}`); xhr.setRequestHeader('Content-Type', 'application/octet-stream'); - - xhr.onload = function() { + + xhr.onload = function () { if (xhr.status >= 200 && xhr.status < 300) { resolve(xhr.responseText); } else { reject(new Error(`Failed to upload chunk: ${xhr.status} ${xhr.statusText}`)); } }; - - xhr.onerror = function() { + xhr.onerror = function () { reject(new Error('Network error during chunk upload')); }; - - xhr.upload.onprogress = function(event) { + xhr.upload.onprogress = function (event) { if (event.lengthComputable) { - const progress = (event.loaded / event.total) * 100; - console.log(`Chunk ${start}-${end} progress: ${Math.round(progress)}%`); + console.log(`Chunk ${start}-${end} progress: ${Math.round((event.loaded / event.total) * 100)}%`); } }; - + xhr.send(chunkData); }); } @@ -197,20 +273,19 @@ function handleUpload(fileData, fileName, uploadUrl, numberOfChunks = 10) { } function storeUploadedFileDetails(fileName, fileSize, fileType) { - chrome.storage.local.get("uploadedFiles", (data) => { + chrome.storage.local.get('uploadedFiles', (data) => { const uploadedFiles = data.uploadedFiles || []; - uploadedFiles.push({ - name: fileName, - size: fileSize, - type: fileType, - timestamp: Date.now() - }); + uploadedFiles.push({ name: fileName, size: fileSize, type: fileType, timestamp: Date.now() }); chrome.storage.local.set({ uploadedFiles }, () => { - console.log("Uploaded file details stored successfully."); + console.log('Uploaded file details stored successfully.'); }); }); } +// --------------------------------------------------------------------------- +// Shared helpers +// --------------------------------------------------------------------------- + function getChunkCount(callback) { chrome.storage.local.get({ chunkCount: 10 }, (data) => { const count = Utils.clampChunkCount(data.chunkCount); @@ -219,17 +294,21 @@ function getChunkCount(callback) { }); } +// --------------------------------------------------------------------------- +// Message handler +// --------------------------------------------------------------------------- + chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { switch (message.type) { - case "START_DOWNLOAD": - console.log("Starting download for URL:", message.url); + case 'START_DOWNLOAD': + console.log('Starting download for URL:', message.url); getChunkCount((count) => { downloadInChunks(message.url, count); sendResponse({ success: true }); }); return true; - case "DELETE_DOWNLOAD": + case 'DELETE_DOWNLOAD': chrome.downloads.removeFile(message.downloadId, () => { chrome.downloads.erase({ id: message.downloadId }, () => { console.log(`Deleted download with ID ${message.downloadId}`); @@ -237,42 +316,41 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { }); break; - case "PAUSE_DOWNLOAD": + case 'PAUSE_DOWNLOAD': chrome.downloads.pause(message.downloadId, () => { console.log(`Paused download with ID ${message.downloadId}`); }); break; - case "RESUME_DOWNLOAD": + case 'RESUME_DOWNLOAD': chrome.downloads.resume(message.downloadId, () => { console.log(`Resumed download with ID ${message.downloadId}`); }); break; - case "RESTART_DOWNLOAD": + case 'RESTART_DOWNLOAD': chrome.downloads.search({ id: message.downloadId }, ([download]) => { if (download) { const originalUrl = download.finalUrl || download.url; - console.log("Retrieving URL for restart. Download ID:", message.downloadId, "URL:", originalUrl); + console.log('Retrieving URL for restart. Download ID:', message.downloadId, 'URL:', originalUrl); if (originalUrl) { chrome.downloads.cancel(message.downloadId, () => { chrome.downloads.download({ url: originalUrl }, (newDownloadId) => { - console.log("Restarted download with ID:", newDownloadId, "URL:", originalUrl); + console.log('Restarted download with ID:', newDownloadId, 'URL:', originalUrl); }); }); } else { - console.log("Could not restart download with ID", message.downloadId, ": URL not found."); + console.log('Could not restart download with ID', message.downloadId, ': URL not found.'); } } }); break; - case "UPLOAD_FILE": + case 'UPLOAD_FILE': if (!message.fileData || !message.fileName || !message.uploadUrl) { - sendResponse({ success: false, error: "Missing required upload data" }); + sendResponse({ success: false, error: 'Missing required upload data' }); return; } - getChunkCount((count) => { handleUpload(message.fileData, message.fileName, message.uploadUrl, count) .then(response => { @@ -294,44 +372,46 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { return true; default: - console.log("Unknown message type:", message.type); + console.log('Unknown message type:', message.type); break; } }); +// --------------------------------------------------------------------------- +// Download event listeners → notify popup +// --------------------------------------------------------------------------- + chrome.downloads.onChanged.addListener((downloadDelta) => { - if (popupPort) { - popupPort.postMessage({ type: "DOWNLOAD_UPDATE" }); - } + if (popupPort) popupPort.postMessage({ type: 'DOWNLOAD_UPDATE' }); }); chrome.downloads.onCreated.addListener((downloadItem) => { - console.log("Download created:", downloadItem.id); - if (popupPort) { - popupPort.postMessage({ type: "DOWNLOAD_UPDATE" }); - } + console.log('Download created:', downloadItem.id); + if (popupPort) popupPort.postMessage({ type: 'DOWNLOAD_UPDATE' }); }); chrome.downloads.onErased.addListener((downloadId) => { - console.log("Download erased:", downloadId); - if (popupPort) { - popupPort.postMessage({ type: "DOWNLOAD_UPDATE" }); - } + console.log('Download erased:', downloadId); + if (popupPort) popupPort.postMessage({ type: 'DOWNLOAD_UPDATE' }); }); +// --------------------------------------------------------------------------- +// Context menu +// --------------------------------------------------------------------------- + chrome.runtime.onInstalled.addListener(() => { chrome.contextMenus.create({ - id: "download-with-chunks", - title: "Download with ChunkFlow", - contexts: ["link"] + id: 'download-with-chunks', + title: 'Download with ChunkFlow', + contexts: ['link'] }); }); chrome.contextMenus.onClicked.addListener((info, tab) => { - if (info.menuItemId === "download-with-chunks" && info.linkUrl) { - console.log("Context menu download:", info.linkUrl); + if (info.menuItemId === 'download-with-chunks' && info.linkUrl) { + console.log('Context menu download:', info.linkUrl); getChunkCount((count) => { downloadInChunks(info.linkUrl, count); }); } -}); \ No newline at end of file +}); From 8e1595b6093b7dccef98bffb8c9290cc8035e387 Mon Sep 17 00:00:00 2001 From: Pushkar Rimmalapudi <63349658+rpushkar9@users.noreply.github.com> Date: Thu, 19 Feb 2026 18:21:55 -0800 Subject: [PATCH 09/14] fix(contentScript): currentTarget, debounced observer, remove dead handler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use event.currentTarget.href instead of event.target.href so clicks on child elements inside (icons, spans, imgs) still resolve the correct download URL - Debounce the MutationObserver callback (200 ms) so heavy SPAs like Google Drive don't queue hundreds of attachDownloadHandlers calls per second during page transitions - Remove dead CONTEXT_MENU_DOWNLOAD listener — background.js calls downloadInChunks directly from contextMenus.onClicked and never sends this message type to the content script --- .../contentScript.js | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/web_plugin_22_full_functionality/contentScript.js b/web_plugin_22_full_functionality/contentScript.js index 1ec099b..a435cfb 100644 --- a/web_plugin_22_full_functionality/contentScript.js +++ b/web_plugin_22_full_functionality/contentScript.js @@ -2,7 +2,8 @@ console.log("Content script loaded."); const handleDownloadClick = (event) => { - const downloadUrl = event.target.href; + // Use currentTarget (the the handler is on), not target (which may be a child element). + const downloadUrl = event.currentTarget.href; if (!downloadUrl || !Utils.validateUrl(downloadUrl)) { console.warn("Invalid download URL:", downloadUrl); @@ -49,6 +50,10 @@ const attachDownloadHandlers = () => { attachDownloadHandlers(); +// Debounce so rapid DOM mutations on heavy SPAs (e.g. Google Drive) don't +// queue hundreds of back-to-back attachDownloadHandlers calls. +const debouncedAttachHandlers = Utils.debounce(attachDownloadHandlers, 200); + const observer = new MutationObserver((mutations) => { let shouldCheck = false; mutations.forEach((mutation) => { @@ -62,9 +67,9 @@ const observer = new MutationObserver((mutations) => { }); } }); - + if (shouldCheck) { - setTimeout(attachDownloadHandlers, 100); + debouncedAttachHandlers(); } }); @@ -73,13 +78,3 @@ observer.observe(document.body, { subtree: true }); -chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { - if (message.type === 'CONTEXT_MENU_DOWNLOAD') { - const downloadUrl = message.url; - if (Utils.validateUrl(downloadUrl)) { - chrome.runtime.sendMessage({ type: "START_DOWNLOAD", url: downloadUrl }); - } else { - console.error("Invalid URL for context menu download:", downloadUrl); - } - } -}); From 1d784b8b6978a1dc5f7cea4a912a6bc08abf6fc2 Mon Sep 17 00:00:00 2001 From: Pushkar Rimmalapudi <63349658+rpushkar9@users.noreply.github.com> Date: Thu, 19 Feb 2026 18:22:02 -0800 Subject: [PATCH 10/14] feat(popup): show download mode badge + fix interval leak MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Download mode badge: - Each download item now shows a small pill badge indicating how it was served: ⚡ Chunked (blue), ↓ Normal (grey), ⚠ Fallback (orange) - Mode is read from chrome.storage.local 'downloadModes' (written by background.js storeDownloadMode on every chrome.downloads.download call) - Badge only appears once Chrome has assigned a download ID; invisible for downloads started by other extensions or Chrome itself Interval fixes: - Store adjustInterval handle so it can be cleared on beforeunload (previously leaked across popup re-opens within the same process) - Start updateInterval at 2 s (idle rate) instead of 1 s; let adjustUpdateFrequency speed it up to 500 ms when downloads are active --- web_plugin_22_full_functionality/popup.css | 29 +++ web_plugin_22_full_functionality/popup.js | 206 +++++++++++++-------- 2 files changed, 159 insertions(+), 76 deletions(-) diff --git a/web_plugin_22_full_functionality/popup.css b/web_plugin_22_full_functionality/popup.css index 955017b..b062d9c 100644 --- a/web_plugin_22_full_functionality/popup.css +++ b/web_plugin_22_full_functionality/popup.css @@ -232,3 +232,32 @@ body { font-size: 13px; font-weight: 500; } + +/* Download mode badge — shows whether a download used chunked, normal, or fallback path */ +.download-mode-badge { + display: inline-block; + padding: 2px 8px; + border-radius: 10px; + font-size: 11px; + font-weight: 600; + margin: 4px 0 2px; + letter-spacing: 0.2px; +} + +.badge-chunked { + background: #e3f2fd; + color: #1565c0; + border: 1px solid #bbdefb; +} + +.badge-normal { + background: #f5f5f5; + color: #555; + border: 1px solid #e0e0e0; +} + +.badge-fallback { + background: #fff8e1; + color: #e65100; + border: 1px solid #ffe0b2; +} diff --git a/web_plugin_22_full_functionality/popup.js b/web_plugin_22_full_functionality/popup.js index cf00db8..168cc72 100644 --- a/web_plugin_22_full_functionality/popup.js +++ b/web_plugin_22_full_functionality/popup.js @@ -1,6 +1,15 @@ let selectedFile = null; -const updateDownloadsList = (downloads) => { +// --------------------------------------------------------------------------- +// Download list rendering +// --------------------------------------------------------------------------- + +/** + * Render the downloads list. + * modes — object from chrome.storage.local 'downloadModes', keyed by string download ID. + * Values: 'chunked' | 'normal' | 'fallback' + */ +const updateDownloadsList = (downloads, modes = {}) => { const downloadsListDiv = document.getElementById('downloads-list'); downloadsListDiv.innerHTML = ''; @@ -13,18 +22,36 @@ const updateDownloadsList = (downloads) => { const downloadDiv = document.createElement('div'); downloadDiv.className = 'download-item'; + // File name const nameDiv = document.createElement('div'); nameDiv.className = 'download-name'; - const fileName = download.filename ? download.filename.split('/').pop().split('\\').pop() : 'Unknown file'; + const fileName = download.filename + ? download.filename.split('/').pop().split('\\').pop() + : 'Unknown file'; nameDiv.textContent = fileName; downloadDiv.appendChild(nameDiv); + // Status const statusDiv = document.createElement('div'); statusDiv.className = 'download-status'; - const stateText = getDownloadStateText(download.state, download.paused); - statusDiv.textContent = `Status: ${stateText}`; + statusDiv.textContent = `Status: ${getDownloadStateText(download.state, download.paused)}`; downloadDiv.appendChild(statusDiv); + // Download mode badge (chunked / normal / fallback) — only shown if mode is known + const mode = modes[String(download.id)]; + if (mode) { + const modeBadge = document.createElement('div'); + modeBadge.className = 'download-mode-badge ' + ( + mode === 'chunked' ? 'badge-chunked' : + mode === 'fallback' ? 'badge-fallback' : 'badge-normal' + ); + modeBadge.textContent = + mode === 'chunked' ? '⚡ Chunked' : + mode === 'fallback' ? '⚠ Fallback' : '↓ Normal'; + downloadDiv.appendChild(modeBadge); + } + + // Size const sizeDiv = document.createElement('div'); sizeDiv.className = 'download-size'; const receivedSize = formatFileSize(download.bytesReceived || 0); @@ -32,8 +59,11 @@ const updateDownloadsList = (downloads) => { sizeDiv.textContent = `${receivedSize} / ${totalSize}`; downloadDiv.appendChild(sizeDiv); - const progressPercentage = download.totalBytes > 0 ? (download.bytesReceived / download.totalBytes) * 100 : 0; - + // Progress bar + const progressPercentage = download.totalBytes > 0 + ? (download.bytesReceived / download.totalBytes) * 100 + : 0; + const progressBar = document.createElement('div'); progressBar.className = 'progress-bar'; @@ -44,12 +74,13 @@ const updateDownloadsList = (downloads) => { progressBar.appendChild(progress); downloadDiv.appendChild(progressBar); + // Controls const controlsDiv = document.createElement('div'); controlsDiv.className = 'controls'; if (download.state === 'in_progress') { const pauseResumeButton = document.createElement('button'); - pauseResumeButton.textContent = download.paused ? "Resume" : "Pause"; + pauseResumeButton.textContent = download.paused ? 'Resume' : 'Pause'; pauseResumeButton.addEventListener('click', () => { if (download.paused) { resumeDownload(download.id); @@ -62,19 +93,19 @@ const updateDownloadsList = (downloads) => { if (download.state !== 'complete') { const restartButton = document.createElement('button'); - restartButton.textContent = "Restart"; + restartButton.textContent = 'Restart'; restartButton.addEventListener('click', () => restartDownload(download.id)); controlsDiv.appendChild(restartButton); } const deleteButton = document.createElement('button'); - deleteButton.textContent = "Delete"; + deleteButton.textContent = 'Delete'; deleteButton.addEventListener('click', () => deleteDownload(download.id)); controlsDiv.appendChild(deleteButton); if (download.state === 'complete') { const openButton = document.createElement('button'); - openButton.textContent = "Open"; + openButton.textContent = 'Open'; openButton.style.backgroundColor = '#4caf50'; openButton.addEventListener('click', () => { chrome.downloads.open(download.id); @@ -87,99 +118,105 @@ const updateDownloadsList = (downloads) => { }); }; +// --------------------------------------------------------------------------- +// Download state helpers +// --------------------------------------------------------------------------- + const getDownloadStateText = (state, paused) => { if (paused) return 'Paused'; switch (state) { case 'in_progress': return 'Downloading'; - case 'complete': return 'Complete'; + case 'complete': return 'Complete'; case 'interrupted': return 'Failed'; - default: return state || 'Unknown'; + default: return state || 'Unknown'; } }; const pauseDownload = (downloadId) => { chrome.downloads.search({ id: downloadId }, ([download]) => { if (download && download.state === 'in_progress' && !download.paused) { - chrome.downloads.pause(downloadId, () => { - fetchDownloads(); - }); + chrome.downloads.pause(downloadId, () => { fetchDownloads(); }); } }); }; const resumeDownload = (downloadId) => { - chrome.runtime.sendMessage({ type: "RESUME_DOWNLOAD", downloadId }, () => { + chrome.runtime.sendMessage({ type: 'RESUME_DOWNLOAD', downloadId }, () => { fetchDownloads(); }); }; const restartDownload = (downloadId) => { - chrome.runtime.sendMessage({ type: "RESTART_DOWNLOAD", downloadId }); + chrome.runtime.sendMessage({ type: 'RESTART_DOWNLOAD', downloadId }); }; const deleteDownload = (downloadId) => { - chrome.runtime.sendMessage({ type: "DELETE_DOWNLOAD", downloadId }); + chrome.runtime.sendMessage({ type: 'DELETE_DOWNLOAD', downloadId }); setTimeout(fetchDownloads, 500); }; +/** + * Fetch recent downloads (last hour) from Chrome's downloads API, + * then read downloadModes from storage and render the list with mode badges. + */ const fetchDownloads = () => { chrome.downloads.search({}, (downloads) => { - const recentDownloads = downloads.filter(download => { - const hourAgo = Date.now() - (60 * 60 * 1000); - return download.startTime && new Date(download.startTime).getTime() > hourAgo; + const hourAgo = Date.now() - (60 * 60 * 1000); + const recentDownloads = downloads.filter(d => + d.startTime && new Date(d.startTime).getTime() > hourAgo + ); + chrome.storage.local.get({ downloadModes: {} }, (data) => { + updateDownloadsList(recentDownloads, data.downloadModes); }); - updateDownloadsList(recentDownloads); }); }; +// --------------------------------------------------------------------------- +// Upload handling +// --------------------------------------------------------------------------- + const handleFileUpload = async () => { - const serverUrl = document.getElementById("server-url").value.trim(); - + const serverUrl = document.getElementById('server-url').value.trim(); + if (!selectedFile) { - showMessage("Please select a file before uploading.", "error"); + showMessage('Please select a file before uploading.', 'error'); return; } - if (!serverUrl) { - showMessage("Please enter a server URL.", "error"); + showMessage('Please enter a server URL.', 'error'); return; } try { const reader = new FileReader(); - reader.onload = function(e) { + reader.onload = function (e) { chrome.runtime.sendMessage({ - type: "UPLOAD_FILE", - fileData: e.target.result, - fileName: selectedFile.name, - fileSize: selectedFile.size, - fileType: selectedFile.type, + type: 'UPLOAD_FILE', + fileData: e.target.result, + fileName: selectedFile.name, + fileSize: selectedFile.size, + fileType: selectedFile.type, uploadUrl: serverUrl }, response => { if (response && response.success) { - showMessage("Upload successful!", "success"); + showMessage('Upload successful!', 'success'); storeUploadedFile(selectedFile); clearFileSelection(); } else { - showMessage(`Upload failed: ${response?.error || 'Unknown error'}`, "error"); + showMessage(`Upload failed: ${response?.error || 'Unknown error'}`, 'error'); } }); }; reader.readAsArrayBuffer(selectedFile); } catch (error) { - showMessage(`Upload failed: ${error.message}`, "error"); + showMessage(`Upload failed: ${error.message}`, 'error'); } }; const storeUploadedFile = (file) => { - chrome.storage.local.get("uploadedFiles", (data) => { + chrome.storage.local.get('uploadedFiles', (data) => { const uploadedFiles = data.uploadedFiles || []; - uploadedFiles.push({ - name: file.name, - size: file.size, - type: file.type, - timestamp: Date.now() - }); + uploadedFiles.push({ name: file.name, size: file.size, type: file.type, timestamp: Date.now() }); chrome.storage.local.set({ uploadedFiles }, () => { displayUploadedFiles(); }); @@ -194,6 +231,10 @@ const clearFileSelection = () => { document.getElementById('file-preview').style.display = 'none'; }; +// --------------------------------------------------------------------------- +// UI helpers +// --------------------------------------------------------------------------- + const showMessage = (text, type) => { const messageDiv = document.createElement('div'); messageDiv.className = `message ${type}`; @@ -202,16 +243,14 @@ const showMessage = (text, type) => { padding: 10px; margin: 10px 0; border-radius: 4px; - ${type === 'error' ? 'background-color: #ffe6e6; color: #d00; border: 1px solid #ffb3b3;' : 'background-color: #e6ffe6; color: #0a0; border: 1px solid #b3ffb3;'} + ${type === 'error' + ? 'background-color: #ffe6e6; color: #d00; border: 1px solid #ffb3b3;' + : 'background-color: #e6ffe6; color: #0a0; border: 1px solid #b3ffb3;'} `; - const container = document.querySelector('.tab-content.active'); container.insertBefore(messageDiv, container.firstChild); - setTimeout(() => { - if (messageDiv.parentNode) { - messageDiv.parentNode.removeChild(messageDiv); - } + if (messageDiv.parentNode) messageDiv.parentNode.removeChild(messageDiv); }, 5000); }; @@ -220,12 +259,12 @@ const displayUploadedFiles = () => { const uploadedFiles = data.uploadedFiles || []; const uploadedFilesList = document.getElementById('uploaded-files-list'); uploadedFilesList.innerHTML = ''; - + if (uploadedFiles.length === 0) { uploadedFilesList.innerHTML = '

No uploaded files yet.

'; return; } - + uploadedFiles.forEach(file => { const listItem = document.createElement('div'); listItem.className = 'uploaded-file-item'; @@ -242,6 +281,10 @@ const displayUploadedFiles = () => { const formatFileSize = Utils.formatFileSize; +// --------------------------------------------------------------------------- +// Chunk count setting +// --------------------------------------------------------------------------- + const loadChunkCount = () => { chrome.storage.local.get({ chunkCount: 10 }, (data) => { const val = Utils.clampChunkCount(data.chunkCount); @@ -258,7 +301,11 @@ const saveChunkCount = (value) => { }); }; -document.addEventListener("DOMContentLoaded", () => { +// --------------------------------------------------------------------------- +// Initialisation +// --------------------------------------------------------------------------- + +document.addEventListener('DOMContentLoaded', () => { loadChunkCount(); const chunkCountInput = document.getElementById('chunk-count'); @@ -268,18 +315,18 @@ document.addEventListener("DOMContentLoaded", () => { }); } - document.getElementById("downloads-tab").addEventListener("click", () => { - document.getElementById("downloads-section").classList.add("active"); - document.getElementById("uploads-section").classList.remove("active"); - document.getElementById("downloads-tab").classList.add("active"); - document.getElementById("uploads-tab").classList.remove("active"); + document.getElementById('downloads-tab').addEventListener('click', () => { + document.getElementById('downloads-section').classList.add('active'); + document.getElementById('uploads-section').classList.remove('active'); + document.getElementById('downloads-tab').classList.add('active'); + document.getElementById('uploads-tab').classList.remove('active'); }); - document.getElementById("uploads-tab").addEventListener("click", () => { - document.getElementById("uploads-section").classList.add("active"); - document.getElementById("downloads-section").classList.remove("active"); - document.getElementById("uploads-tab").classList.add("active"); - document.getElementById("downloads-tab").classList.remove("active"); + document.getElementById('uploads-tab').addEventListener('click', () => { + document.getElementById('uploads-section').classList.add('active'); + document.getElementById('downloads-section').classList.remove('active'); + document.getElementById('uploads-tab').classList.add('active'); + document.getElementById('downloads-tab').classList.remove('active'); displayUploadedFiles(); }); @@ -292,7 +339,7 @@ document.addEventListener("DOMContentLoaded", () => { if (selectedFile) { document.getElementById('selected-file-name').textContent = selectedFile.name; document.getElementById('file-name').textContent = `Selected File: ${selectedFile.name}`; - + const filePreview = document.getElementById('file-preview'); if (selectedFile.type.startsWith('image/')) { const reader = new FileReader(); @@ -310,18 +357,20 @@ document.addEventListener("DOMContentLoaded", () => { document.getElementById('upload-button').addEventListener('click', handleFileUpload); chrome.runtime.sendMessage({ type: 'GET_UPLOADED_FILES' }, response => { - if (response && response.uploadedFiles) { - displayUploadedFiles(); - } + if (response && response.uploadedFiles) displayUploadedFiles(); }); fetchDownloads(); }); +// --------------------------------------------------------------------------- +// Port connection to background service worker +// --------------------------------------------------------------------------- + const port = chrome.runtime.connect(); port.onMessage.addListener((message) => { - if (message.type === "DOWNLOAD_UPDATE") { + if (message.type === 'DOWNLOAD_UPDATE') { fetchDownloads(); } else if (message.type === 'DOWNLOAD_READY') { const downloadLink = document.createElement('a'); @@ -329,29 +378,33 @@ port.onMessage.addListener((message) => { downloadLink.textContent = 'Download Merged File'; downloadLink.download = message.filename || 'downloaded_file'; downloadLink.style.cssText = 'display: block; margin: 10px 0; padding: 8px; background: #4caf50; color: white; text-decoration: none; border-radius: 4px; text-align: center;'; - + const downloadsSection = document.getElementById('downloads-section'); downloadsSection.insertBefore(downloadLink, downloadsSection.firstChild); if (message.isChunked) { const chunkedLabel = document.createElement('span'); - chunkedLabel.textContent = " (Chunked Download)"; + chunkedLabel.textContent = ' (Chunked Download)'; chunkedLabel.style.color = 'blue'; chunkedLabel.style.fontWeight = 'bold'; downloadLink.appendChild(chunkedLabel); } - + setTimeout(() => { - if (downloadLink.parentNode) { - downloadLink.parentNode.removeChild(downloadLink); - } + if (downloadLink.parentNode) downloadLink.parentNode.removeChild(downloadLink); }, 30000); } else if (message.type === 'ERROR') { showMessage(message.message, 'error'); } }); -let updateInterval = setInterval(fetchDownloads, 1000); +// --------------------------------------------------------------------------- +// Adaptive polling — speeds up during active downloads, slows down when idle. +// Both intervals are stored so they can be cleared on popup unload. +// --------------------------------------------------------------------------- + +// Start slow (2 s); adjustUpdateFrequency will speed up to 500 ms when needed. +let updateInterval = setInterval(fetchDownloads, 2000); const adjustUpdateFrequency = () => { chrome.downloads.search({ state: 'in_progress' }, (downloads) => { @@ -361,8 +414,9 @@ const adjustUpdateFrequency = () => { }); }; -setInterval(adjustUpdateFrequency, 5000); +const adjustInterval = setInterval(adjustUpdateFrequency, 5000); window.addEventListener('beforeunload', () => { clearInterval(updateInterval); -}); \ No newline at end of file + clearInterval(adjustInterval); +}); From bd90114ca5aa9469470698b0fc2ae94a07f393aa Mon Sep 17 00:00:00 2001 From: Pushkar Rimmalapudi <63349658+rpushkar9@users.noreply.github.com> Date: Thu, 19 Feb 2026 18:42:48 -0800 Subject: [PATCH 11/14] fix(popup+background): fix missing mode badge and invisible first download MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two root-cause bugs fixed: 1. Race condition — mode badge missing storeDownloadMode() now sends DOWNLOAD_UPDATE *inside* the chrome.storage.local.set() callback, so the badge data is guaranteed to be in storage before the popup reads it. Previously onCreated fired first, fetchDownloads ran, read empty downloadModes, and rendered without the badge. 2. Stale-render — rapid DOWNLOAD_UPDATE calls overwrote fresh data fetchDownloads now uses a generation counter (fetchGeneration). Callbacks from superseded calls bail out early so a slower response never replaces a more-recent render. 3. Invisible first download during chunk assembly downloadInChunks (converted to async/await) now writes the URL to chrome.storage activeChunkFetches at the very start and removes it just before creating the Chrome download item. The popup reads activeChunkFetches in fetchDownloads and renders a "⚡ Chunked (preparing)" placeholder immediately, so the user sees activity even while chunks are still being fetched — before Chrome has a download item to show. Orphaned entries (> 5 min old) are automatically ignored. Co-Authored-By: Claude Sonnet 4.6 --- .../background.js | 216 ++++++++++-------- web_plugin_22_full_functionality/popup.js | 52 ++++- 2 files changed, 172 insertions(+), 96 deletions(-) diff --git a/web_plugin_22_full_functionality/background.js b/web_plugin_22_full_functionality/background.js index 277edbf..2708095 100644 --- a/web_plugin_22_full_functionality/background.js +++ b/web_plugin_22_full_functionality/background.js @@ -63,6 +63,8 @@ function getFilename(response, fallbackUrl) { * Persist a download's mode (chunked | normal | fallback) in chrome.storage.local * under the key 'downloadModes', keyed by string download ID. * Capped at 100 entries (oldest-first eviction) to avoid unbounded growth. + * Notifies the popup AFTER the write so the badge is already in storage when + * fetchDownloads reads it (fixes the race that caused missing mode badges). */ function storeDownloadMode(downloadId, mode) { chrome.storage.local.get({ downloadModes: {} }, (data) => { @@ -70,7 +72,38 @@ function storeDownloadMode(downloadId, mode) { modes[String(downloadId)] = mode; const keys = Object.keys(modes); if (keys.length > 100) delete modes[keys[0]]; - chrome.storage.local.set({ downloadModes: modes }); + chrome.storage.local.set({ downloadModes: modes }, () => { + if (popupPort) popupPort.postMessage({ type: 'DOWNLOAD_UPDATE' }); + }); + }); +} + +/** + * Add a URL to the activeChunkFetches list so the popup can show a + * "preparing" placeholder while chunks are being assembled (before Chrome + * creates a download item). + */ +function addActiveChunkFetch(url) { + return new Promise(resolve => { + chrome.storage.local.get({ activeChunkFetches: [] }, (data) => { + // Deduplicate in case of rapid re-clicks + const list = data.activeChunkFetches.filter(e => e.url !== url); + list.push({ url, startTime: Date.now() }); + chrome.storage.local.set({ activeChunkFetches: list }, () => { + if (popupPort) popupPort.postMessage({ type: 'DOWNLOAD_UPDATE' }); + resolve(); + }); + }); + }); +} + +/** Remove a URL from activeChunkFetches once its Chrome download item exists. */ +function removeActiveChunkFetch(url) { + return new Promise(resolve => { + chrome.storage.local.get({ activeChunkFetches: [] }, (data) => { + const list = data.activeChunkFetches.filter(e => e.url !== url); + chrome.storage.local.set({ activeChunkFetches: list }, resolve); + }); }); } @@ -78,110 +111,111 @@ function storeDownloadMode(downloadId, mode) { // Download engine // --------------------------------------------------------------------------- -function downloadInChunks(url, numberOfChunks = 10) { +async function downloadInChunks(url, numberOfChunks = 10) { console.log(`[ChunkFlow] downloadInChunks: ${numberOfChunks} chunks for ${url}`); - return fetch(url, { method: 'HEAD', credentials: 'include' }) - .then(response => { - console.log('[ChunkFlow] HEAD response received'); + // Show a "preparing" placeholder in the popup immediately, before Chrome + // creates a download item (which only happens after all chunks are assembled). + await addActiveChunkFetch(url); - // Capture the URL after redirects so all chunk GETs go to the same - // CDN endpoint and use the same auth tokens (critical for Google Drive etc.) - const finalUrl = response.url || url; - const filename = getFilename(response, url); + try { + const headResponse = await fetchWithRetry(url, { method: 'HEAD', credentials: 'include' }); + console.log('[ChunkFlow] HEAD response received'); + + // Capture the URL after redirects so all chunk GETs go to the same + // CDN endpoint and use the same auth tokens (critical for Google Drive etc.) + const finalUrl = headResponse.url || url; + const filename = getFilename(headResponse, url); + + if (headResponse.headers.get('Accept-Ranges') !== 'bytes') { + console.log('[ChunkFlow] No range support — using normal download'); + await removeActiveChunkFetch(url); + chrome.downloads.download({ url, filename }, (id) => { + if (id) storeDownloadMode(id, 'normal'); + }); + return; + } - if (response.headers.get('Accept-Ranges') !== 'bytes') { - console.log('[ChunkFlow] No range support — using normal download'); - chrome.downloads.download({ url, filename }, (id) => { - if (id) storeDownloadMode(id, 'normal'); - }); - return null; - } + const fileSize = parseInt(headResponse.headers.get('Content-Length')); + const mimeType = headResponse.headers.get('Content-Type') || 'application/octet-stream'; - const fileSize = parseInt(response.headers.get('Content-Length')); - const mimeType = response.headers.get('Content-Type') || 'application/octet-stream'; + if (!fileSize || fileSize <= 0) { + throw new Error('Invalid or missing Content-Length header'); + } - if (!fileSize || fileSize <= 0) { - throw new Error('Invalid or missing Content-Length header'); - } + // Skip in-memory chunking for large files to avoid OOM in the service worker. + if (fileSize > CHUNK_MAX_BYTES) { + console.log(`[ChunkFlow] File too large for in-memory chunking ` + + `(${Utils.formatFileSize(fileSize)} > ${Utils.formatFileSize(CHUNK_MAX_BYTES)}) — using normal download`); + await removeActiveChunkFetch(url); + chrome.downloads.download({ url, filename }, (id) => { + if (id) storeDownloadMode(id, 'normal'); + }); + return; + } - // Skip in-memory chunking for large files to avoid OOM in the service worker. - if (fileSize > CHUNK_MAX_BYTES) { - console.log(`[ChunkFlow] File too large for in-memory chunking ` + - `(${Utils.formatFileSize(fileSize)} > ${Utils.formatFileSize(CHUNK_MAX_BYTES)}) — using normal download`); - chrome.downloads.download({ url, filename }, (id) => { - if (id) storeDownloadMode(id, 'normal'); - }); - return null; - } + const chunkSize = Math.ceil(fileSize / numberOfChunks); + const chunkPromises = []; + + for (let i = 0; i < numberOfChunks; i++) { + const start = i * chunkSize; + const end = i === numberOfChunks - 1 ? fileSize - 1 : (start + chunkSize - 1); + + chunkPromises.push( + fetchWithRetry( + finalUrl, + { headers: { Range: `bytes=${start}-${end}` }, credentials: 'include' } + ).then(res => { + if (!res.ok) throw new Error(`Failed to fetch chunk ${i}: ${res.status}`); + return res.arrayBuffer(); + }) + ); + } - return { fileSize, mimeType, finalUrl, filename }; - }) - .then((result) => { - if (!result) return null; - - const { fileSize, mimeType, finalUrl, filename } = result; - const chunkSize = Math.ceil(fileSize / numberOfChunks); - const chunkPromises = []; - - for (let i = 0; i < numberOfChunks; i++) { - const start = i * chunkSize; - const end = i === numberOfChunks - 1 ? fileSize - 1 : (start + chunkSize - 1); - - chunkPromises.push( - fetchWithRetry( - finalUrl, - { headers: { Range: `bytes=${start}-${end}` }, credentials: 'include' } - ).then(res => { - if (!res.ok) throw new Error(`Failed to fetch chunk ${i}: ${res.status}`); - return res.arrayBuffer(); - }) - ); - } + const chunks = await Promise.all(chunkPromises); + const totalBytes = chunks.reduce((acc, c) => acc + c.byteLength, 0); + const merged = new Uint8Array(totalBytes); + let offset = 0; + chunks.forEach(c => { + merged.set(new Uint8Array(c), offset); + offset += c.byteLength; + }); - return Promise.all(chunkPromises) - .then(chunks => { - const totalBytes = chunks.reduce((acc, c) => acc + c.byteLength, 0); - const merged = new Uint8Array(totalBytes); - let offset = 0; - chunks.forEach(c => { - merged.set(new Uint8Array(c), offset); - offset += c.byteLength; - }); + const blob = new Blob([merged], { type: mimeType }); + const objectURL = URL.createObjectURL(blob); - const blob = new Blob([merged], { type: mimeType }); - const objectURL = URL.createObjectURL(blob); + // Remove the placeholder BEFORE creating the Chrome item so the popup + // never shows both the placeholder and the real item simultaneously. + await removeActiveChunkFetch(url); - chrome.downloads.download({ url: objectURL, filename }, (id) => { - if (id) storeDownloadMode(id, 'chunked'); - }); + chrome.downloads.download({ url: objectURL, filename }, (id) => { + if (id) storeDownloadMode(id, 'chunked'); + }); - if (popupPort) { - popupPort.postMessage({ type: 'DOWNLOAD_READY', url: objectURL, filename, isChunked: true }); - } + if (popupPort) { + popupPort.postMessage({ type: 'DOWNLOAD_READY', url: objectURL, filename, isChunked: true }); + } - return blob; - }); - }) - .catch(error => { - console.error('[ChunkFlow] Download error:', error); - if (popupPort) { - popupPort.postMessage({ type: 'ERROR', message: `Download failed: ${error.message}` }); - } + } catch (error) { + console.error('[ChunkFlow] Download error:', error); + if (popupPort) { + popupPort.postMessage({ type: 'ERROR', message: `Download failed: ${error.message}` }); + } - // Best-effort fallback to Chrome's native downloader. - console.log('[ChunkFlow] Falling back to normal download'); - try { - const filename = new URL(url).pathname.split('/').pop() || 'downloaded_file'; - chrome.downloads.download({ url, filename }, (id) => { - if (id) storeDownloadMode(id, 'fallback'); - }); - } catch { - chrome.downloads.download({ url }, (id) => { - if (id) storeDownloadMode(id, 'fallback'); - }); - } - }); + // Best-effort fallback to Chrome's native downloader. + console.log('[ChunkFlow] Falling back to normal download'); + await removeActiveChunkFetch(url); + try { + const filename = new URL(url).pathname.split('/').pop() || 'downloaded_file'; + chrome.downloads.download({ url, filename }, (id) => { + if (id) storeDownloadMode(id, 'fallback'); + }); + } catch { + chrome.downloads.download({ url }, (id) => { + if (id) storeDownloadMode(id, 'fallback'); + }); + } + } } // --------------------------------------------------------------------------- diff --git a/web_plugin_22_full_functionality/popup.js b/web_plugin_22_full_functionality/popup.js index 168cc72..f597a36 100644 --- a/web_plugin_22_full_functionality/popup.js +++ b/web_plugin_22_full_functionality/popup.js @@ -8,12 +8,17 @@ let selectedFile = null; * Render the downloads list. * modes — object from chrome.storage.local 'downloadModes', keyed by string download ID. * Values: 'chunked' | 'normal' | 'fallback' + * activeFetches — array of { url, startTime } for chunk fetches still assembling + * (no Chrome download item exists yet for these). */ -const updateDownloadsList = (downloads, modes = {}) => { +const updateDownloadsList = (downloads, modes = {}, activeFetches = []) => { const downloadsListDiv = document.getElementById('downloads-list'); downloadsListDiv.innerHTML = ''; - if (downloads.length === 0) { + const fiveMinutesAgo = Date.now() - 5 * 60 * 1000; + const pendingFetches = activeFetches.filter(e => e.startTime > fiveMinutesAgo); + + if (downloads.length === 0 && pendingFetches.length === 0) { downloadsListDiv.innerHTML = '

No active downloads

'; return; } @@ -116,6 +121,31 @@ const updateDownloadsList = (downloads, modes = {}) => { downloadDiv.appendChild(controlsDiv); downloadsListDiv.appendChild(downloadDiv); }); + + // Show "preparing" placeholders for chunk fetches that haven't produced a + // Chrome download item yet (chunks still assembling in the service worker). + // Entries older than 5 minutes are treated as orphaned and skipped. + pendingFetches.forEach(e => { + let displayName; + try { + displayName = new URL(e.url).pathname.split('/').pop() || e.url; + } catch { + displayName = e.url; + } + + const pendingDiv = document.createElement('div'); + pendingDiv.className = 'download-item'; + + pendingDiv.innerHTML = ` +
${displayName}
+
Status: Fetching chunks…
+
⚡ Chunked (preparing)
+
+
assembling…
+
+ `; + downloadsListDiv.appendChild(pendingDiv); + }); }; // --------------------------------------------------------------------------- @@ -157,16 +187,28 @@ const deleteDownload = (downloadId) => { /** * Fetch recent downloads (last hour) from Chrome's downloads API, - * then read downloadModes from storage and render the list with mode badges. + * then read downloadModes + activeChunkFetches from storage and render the list. + * + * A generation counter is used to discard results from superseded calls so that + * rapid-fire DOWNLOAD_UPDATE messages (e.g. during badge write + onCreated racing) + * never overwrite a fresher render with stale data. */ +let fetchGeneration = 0; + const fetchDownloads = () => { + const gen = ++fetchGeneration; + chrome.downloads.search({}, (downloads) => { + if (gen !== fetchGeneration) return; // superseded + const hourAgo = Date.now() - (60 * 60 * 1000); const recentDownloads = downloads.filter(d => d.startTime && new Date(d.startTime).getTime() > hourAgo ); - chrome.storage.local.get({ downloadModes: {} }, (data) => { - updateDownloadsList(recentDownloads, data.downloadModes); + + chrome.storage.local.get({ downloadModes: {}, activeChunkFetches: [] }, (data) => { + if (gen !== fetchGeneration) return; // superseded + updateDownloadsList(recentDownloads, data.downloadModes, data.activeChunkFetches); }); }); }; From 47e05ca35a68a0da6e28effea35ec3831711008a Mon Sep 17 00:00:00 2001 From: Pushkar Rimmalapudi <63349658+rpushkar9@users.noreply.github.com> Date: Thu, 19 Feb 2026 18:44:42 -0800 Subject: [PATCH 12/14] feat(popup): show full download history, not just last hour MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fetchDownloads now queries the 50 most-recent downloads ordered by -startTime (newest first) with no time filter, so completed, failed, and older downloads all appear alongside active ones. - Each download card now shows a human-readable timestamp ("Just now", "5m ago", "Today 2:30 PM", "Yesterday …", "Feb 3 …") so past and concurrent downloads are easy to distinguish. - Empty state copy updated to "No downloads yet". - Added .download-time CSS class (11px, light grey). Co-Authored-By: Claude Sonnet 4.6 --- web_plugin_22_full_functionality/popup.css | 6 +++ web_plugin_22_full_functionality/popup.js | 54 ++++++++++++++++------ 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/web_plugin_22_full_functionality/popup.css b/web_plugin_22_full_functionality/popup.css index b062d9c..58dc7e8 100644 --- a/web_plugin_22_full_functionality/popup.css +++ b/web_plugin_22_full_functionality/popup.css @@ -50,6 +50,12 @@ body { margin-bottom: 4px; } +.download-time { + font-size: 11px; + color: #aaa; + margin-bottom: 2px; +} + .download-size { font-size: 11px; color: #888; diff --git a/web_plugin_22_full_functionality/popup.js b/web_plugin_22_full_functionality/popup.js index f597a36..152c459 100644 --- a/web_plugin_22_full_functionality/popup.js +++ b/web_plugin_22_full_functionality/popup.js @@ -1,5 +1,32 @@ let selectedFile = null; +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/** + * Return a concise human-readable string for a download's startTime ISO string. + * Examples: "Just now", "5m ago", "Today 2:30 PM", "Yesterday 9:14 AM", "Feb 3 11:00 AM" + */ +const formatDownloadTime = (isoString) => { + if (!isoString) return ''; + const date = new Date(isoString); + const now = new Date(); + const diffMin = Math.floor((now - date) / 60000); + + if (diffMin < 1) return 'Just now'; + if (diffMin < 60) return `${diffMin}m ago`; + + const time = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + const todayStr = now.toDateString(); + const yesterdayStr = new Date(now - 86400000).toDateString(); + + if (date.toDateString() === todayStr) return `Today ${time}`; + if (date.toDateString() === yesterdayStr) return `Yesterday ${time}`; + + return date.toLocaleDateString([], { month: 'short', day: 'numeric' }) + ` ${time}`; +}; + // --------------------------------------------------------------------------- // Download list rendering // --------------------------------------------------------------------------- @@ -19,7 +46,7 @@ const updateDownloadsList = (downloads, modes = {}, activeFetches = []) => { const pendingFetches = activeFetches.filter(e => e.startTime > fiveMinutesAgo); if (downloads.length === 0 && pendingFetches.length === 0) { - downloadsListDiv.innerHTML = '

No active downloads

'; + downloadsListDiv.innerHTML = '

No downloads yet

'; return; } @@ -42,6 +69,12 @@ const updateDownloadsList = (downloads, modes = {}, activeFetches = []) => { statusDiv.textContent = `Status: ${getDownloadStateText(download.state, download.paused)}`; downloadDiv.appendChild(statusDiv); + // Timestamp + const timeDiv = document.createElement('div'); + timeDiv.className = 'download-time'; + timeDiv.textContent = formatDownloadTime(download.startTime); + downloadDiv.appendChild(timeDiv); + // Download mode badge (chunked / normal / fallback) — only shown if mode is known const mode = modes[String(download.id)]; if (mode) { @@ -186,29 +219,24 @@ const deleteDownload = (downloadId) => { }; /** - * Fetch recent downloads (last hour) from Chrome's downloads API, - * then read downloadModes + activeChunkFetches from storage and render the list. + * Fetch the 50 most-recent downloads from Chrome's downloads API (all time, + * not just the last hour), then read downloadModes + activeChunkFetches from + * storage and render the full list — active, paused, completed, and failed. * - * A generation counter is used to discard results from superseded calls so that - * rapid-fire DOWNLOAD_UPDATE messages (e.g. during badge write + onCreated racing) - * never overwrite a fresher render with stale data. + * A generation counter discards results from superseded calls so that + * rapid-fire DOWNLOAD_UPDATE messages never overwrite a fresher render. */ let fetchGeneration = 0; const fetchDownloads = () => { const gen = ++fetchGeneration; - chrome.downloads.search({}, (downloads) => { + chrome.downloads.search({ orderBy: ['-startTime'], limit: 50 }, (downloads) => { if (gen !== fetchGeneration) return; // superseded - const hourAgo = Date.now() - (60 * 60 * 1000); - const recentDownloads = downloads.filter(d => - d.startTime && new Date(d.startTime).getTime() > hourAgo - ); - chrome.storage.local.get({ downloadModes: {}, activeChunkFetches: [] }, (data) => { if (gen !== fetchGeneration) return; // superseded - updateDownloadsList(recentDownloads, data.downloadModes, data.activeChunkFetches); + updateDownloadsList(downloads, data.downloadModes, data.activeChunkFetches); }); }); }; From 1feac3123c6c2eb369cb110799ba121d4467d2d0 Mon Sep 17 00:00:00 2001 From: Pushkar Rimmalapudi <63349658+rpushkar9@users.noreply.github.com> Date: Thu, 19 Feb 2026 18:55:41 -0800 Subject: [PATCH 13/14] fix(badges): guarantee every download gets a mode badge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: mode badges were missing for two distinct reasons: 1. Race condition — onCreated fired before the chrome.downloads.download() callback, so the first popup render after download creation had no mode in storage yet. Fix: pre-register each URL in pendingModeByUrl/pendingChunkFlowUrls BEFORE calling chrome.downloads.download(). chrome.downloads.onCreated now reads the pre-registered mode and calls storeDownloadMode immediately, guaranteeing the badge is in storage before the popup's next render. The chrome.downloads.download() callbacks are removed; onCreated is the single source of truth for mode assignment. 2. Native Chrome downloads (Google Drive UI, address bar, other extensions) never went through downloadInChunks, so storeDownloadMode was never called and no badge was stored. Fix: onCreated now calls storeDownloadMode(id, 'browser') for any download NOT pre-registered by ChunkFlow. These show a grey "↓ Browser" badge so every download in the list has a badge. Badge legend (updated): ⚡ Chunked — ChunkFlow assembled chunks in parallel ↓ Normal — ChunkFlow used single-connection download (no range support or file > 500 MB) ⚠ Fallback — ChunkFlow encountered an error and fell back to Chrome ↓ Browser — Download initiated outside ChunkFlow (native Chrome) Co-Authored-By: Claude Sonnet 4.6 --- .../background.js | 56 +++++++++++++------ web_plugin_22_full_functionality/popup.css | 6 ++ web_plugin_22_full_functionality/popup.js | 8 ++- 3 files changed, 51 insertions(+), 19 deletions(-) diff --git a/web_plugin_22_full_functionality/background.js b/web_plugin_22_full_functionality/background.js index 2708095..63d4972 100644 --- a/web_plugin_22_full_functionality/background.js +++ b/web_plugin_22_full_functionality/background.js @@ -6,6 +6,14 @@ const CHUNK_MAX_BYTES = 500 * 1024 * 1024; // 500 MB let popupPort = null; +// Before calling chrome.downloads.download() we register the URL and intended +// mode here. chrome.downloads.onCreated reads this to assign the badge +// immediately when the download item is created — more reliable than waiting +// for the chrome.downloads.download() callback, which can fire after onCreated +// (causing the popup to render without a badge on the first refresh). +const pendingChunkFlowUrls = new Set(); // URLs we're about to create +const pendingModeByUrl = {}; // url → 'chunked' | 'normal' | 'fallback' + chrome.runtime.onConnect.addListener((port) => { popupPort = port; port.onDisconnect.addListener(() => { @@ -130,9 +138,9 @@ async function downloadInChunks(url, numberOfChunks = 10) { if (headResponse.headers.get('Accept-Ranges') !== 'bytes') { console.log('[ChunkFlow] No range support — using normal download'); await removeActiveChunkFetch(url); - chrome.downloads.download({ url, filename }, (id) => { - if (id) storeDownloadMode(id, 'normal'); - }); + pendingModeByUrl[url] = 'normal'; + pendingChunkFlowUrls.add(url); + chrome.downloads.download({ url, filename }); return; } @@ -148,9 +156,9 @@ async function downloadInChunks(url, numberOfChunks = 10) { console.log(`[ChunkFlow] File too large for in-memory chunking ` + `(${Utils.formatFileSize(fileSize)} > ${Utils.formatFileSize(CHUNK_MAX_BYTES)}) — using normal download`); await removeActiveChunkFetch(url); - chrome.downloads.download({ url, filename }, (id) => { - if (id) storeDownloadMode(id, 'normal'); - }); + pendingModeByUrl[url] = 'normal'; + pendingChunkFlowUrls.add(url); + chrome.downloads.download({ url, filename }); return; } @@ -188,9 +196,9 @@ async function downloadInChunks(url, numberOfChunks = 10) { // never shows both the placeholder and the real item simultaneously. await removeActiveChunkFetch(url); - chrome.downloads.download({ url: objectURL, filename }, (id) => { - if (id) storeDownloadMode(id, 'chunked'); - }); + pendingModeByUrl[objectURL] = 'chunked'; + pendingChunkFlowUrls.add(objectURL); + chrome.downloads.download({ url: objectURL, filename }); if (popupPort) { popupPort.postMessage({ type: 'DOWNLOAD_READY', url: objectURL, filename, isChunked: true }); @@ -205,15 +213,13 @@ async function downloadInChunks(url, numberOfChunks = 10) { // Best-effort fallback to Chrome's native downloader. console.log('[ChunkFlow] Falling back to normal download'); await removeActiveChunkFetch(url); + pendingModeByUrl[url] = 'fallback'; + pendingChunkFlowUrls.add(url); try { const filename = new URL(url).pathname.split('/').pop() || 'downloaded_file'; - chrome.downloads.download({ url, filename }, (id) => { - if (id) storeDownloadMode(id, 'fallback'); - }); + chrome.downloads.download({ url, filename }); } catch { - chrome.downloads.download({ url }, (id) => { - if (id) storeDownloadMode(id, 'fallback'); - }); + chrome.downloads.download({ url }); } } } @@ -421,7 +427,25 @@ chrome.downloads.onChanged.addListener((downloadDelta) => { chrome.downloads.onCreated.addListener((downloadItem) => { console.log('Download created:', downloadItem.id); - if (popupPort) popupPort.postMessage({ type: 'DOWNLOAD_UPDATE' }); + const dlUrl = downloadItem.url; + + if (pendingChunkFlowUrls.has(dlUrl)) { + // Download we initiated — assign the pre-registered mode. + // Doing this here (onCreated) rather than in the chrome.downloads.download() + // callback guarantees the mode is in storage before the popup's next render. + pendingChunkFlowUrls.delete(dlUrl); + const mode = pendingModeByUrl[dlUrl]; + if (mode) { + delete pendingModeByUrl[dlUrl]; + storeDownloadMode(downloadItem.id, mode); + return; // storeDownloadMode sends DOWNLOAD_UPDATE after the write + } + } + + // Download NOT initiated by ChunkFlow (e.g. Google Drive button, other + // extensions, direct URL bar downloads). Tag it so the popup can show a + // "↓ Browser" badge instead of showing nothing. + storeDownloadMode(downloadItem.id, 'browser'); }); chrome.downloads.onErased.addListener((downloadId) => { diff --git a/web_plugin_22_full_functionality/popup.css b/web_plugin_22_full_functionality/popup.css index 58dc7e8..a4f4cb6 100644 --- a/web_plugin_22_full_functionality/popup.css +++ b/web_plugin_22_full_functionality/popup.css @@ -267,3 +267,9 @@ body { color: #e65100; border: 1px solid #ffe0b2; } + +.badge-browser { + background: #f5f5f5; + color: #999; + border: 1px solid #ddd; +} diff --git a/web_plugin_22_full_functionality/popup.js b/web_plugin_22_full_functionality/popup.js index 152c459..be3940f 100644 --- a/web_plugin_22_full_functionality/popup.js +++ b/web_plugin_22_full_functionality/popup.js @@ -75,17 +75,19 @@ const updateDownloadsList = (downloads, modes = {}, activeFetches = []) => { timeDiv.textContent = formatDownloadTime(download.startTime); downloadDiv.appendChild(timeDiv); - // Download mode badge (chunked / normal / fallback) — only shown if mode is known + // Download mode badge — shown for every download once mode is known const mode = modes[String(download.id)]; if (mode) { const modeBadge = document.createElement('div'); modeBadge.className = 'download-mode-badge ' + ( mode === 'chunked' ? 'badge-chunked' : - mode === 'fallback' ? 'badge-fallback' : 'badge-normal' + mode === 'fallback' ? 'badge-fallback' : + mode === 'browser' ? 'badge-browser' : 'badge-normal' ); modeBadge.textContent = mode === 'chunked' ? '⚡ Chunked' : - mode === 'fallback' ? '⚠ Fallback' : '↓ Normal'; + mode === 'fallback' ? '⚠ Fallback' : + mode === 'browser' ? '↓ Browser' : '↓ Normal'; downloadDiv.appendChild(modeBadge); } From 6fd3206e7944a390f0e02e8e4c9c23f3b71823d7 Mon Sep 17 00:00:00 2001 From: Pushkar Rimmalapudi <63349658+rpushkar9@users.noreply.github.com> Date: Thu, 19 Feb 2026 19:01:17 -0800 Subject: [PATCH 14/14] docs: add session log for 2026-02-19 popup badges & history Documents all work done in this session: - Three bug fixes (first download invisible, badge race condition, no badge for native downloads) - Full download history view (removed 1-hour filter, added timestamps) - Mistakes made and how each was caught and fixed - Current limitations and next steps Co-Authored-By: Claude Sonnet 4.6 --- .../2026-02-19-popup-badges-history.md | 202 ++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 notes/sessions/2026-02-19-popup-badges-history.md diff --git a/notes/sessions/2026-02-19-popup-badges-history.md b/notes/sessions/2026-02-19-popup-badges-history.md new file mode 100644 index 0000000..e241699 --- /dev/null +++ b/notes/sessions/2026-02-19-popup-badges-history.md @@ -0,0 +1,202 @@ +# Session: 2026-02-19 (afternoon) — Popup badges, full history, race-condition fixes + +## Goal +Fix two user-reported bugs (download not showing in popup, mode badge always +missing), expand the popup to show the full download history instead of just +the last hour, and ensure every download in the list gets a badge indicating +how it was handled. + +--- + +## What was done + +### Bug 1 — First download invisible during chunk assembly +**Symptom:** popup showed "No active downloads" even while a download was +in progress. Starting a second download would suddenly make the first appear. + +**Root cause:** For chunked downloads, `downloadInChunks` fetches all chunks +in parallel before calling `chrome.downloads.download()`. Until that call is +made, Chrome has no download item — `chrome.downloads.search` returns nothing. + +**Fix:** +- Added `addActiveChunkFetch(url)` / `removeActiveChunkFetch(url)` helpers + that write a pending entry to `chrome.storage.local` under + `activeChunkFetches`. +- `downloadInChunks` (converted to `async/await`) now awaits + `addActiveChunkFetch` at the very start and awaits `removeActiveChunkFetch` + just before calling `chrome.downloads.download`. +- `fetchDownloads` in the popup reads `activeChunkFetches` alongside + `downloadModes` and passes them to `updateDownloadsList`. +- `updateDownloadsList` renders an "⚡ Chunked (preparing)" placeholder for + each active fetch entry (entries older than 5 min are treated as orphaned + and skipped). +- **Mistake caught:** `updateDownloadsList` had an early `return` when + `downloads.length === 0`, which skipped the `activeFetches` rendering block + entirely. Fixed by computing `pendingFetches` before the guard and + combining the empty-state check: only show "No downloads yet" when BOTH + arrays are empty. + +### Bug 2 — Mode badge race condition (badge never showed) +**Symptom:** popup showed the download item but with no ⚡/↓/⚠ badge. + +**Root cause (first layer):** `chrome.downloads.onCreated` fires before the +`chrome.downloads.download()` callback fires. So the popup received +`DOWNLOAD_UPDATE` (from `onCreated`) and called `fetchDownloads` before +`storeDownloadMode` had written the mode to `chrome.storage.local`. First +render: no badge. `storeDownloadMode` then wrote the mode but wasn't +sending another `DOWNLOAD_UPDATE`, so the badge never appeared. + +**Fix (first pass):** Changed `storeDownloadMode` to send `DOWNLOAD_UPDATE` +*inside* the `chrome.storage.local.set()` callback so the mode is guaranteed +to be in storage before the popup reads it. + +**Root cause (second layer):** Rapid-fire `fetchDownloads` calls (polling + +badge-write notification + `onCreated` notification all happening within +~50 ms) meant an older async callback could overwrite a fresher render, losing +the badge again. + +**Fix:** Added a `fetchGeneration` counter to `fetchDownloads`. Every call +increments the counter; callbacks from superseded calls bail out with +`if (gen !== fetchGeneration) return`. + +### Bug 3 — Badge never showed for native Chrome downloads +**Symptom:** downloads started via Google Drive's download button, the address +bar, or other non-ChunkFlow paths showed no badge at all — `storeDownloadMode` +was never called for them. + +**Root cause:** `storeDownloadMode` was only called from inside +`downloadInChunks`, which is only invoked for downloads ChunkFlow explicitly +intercepts (content-script link click or context-menu). Downloads that +bypass ChunkFlow entirely have no entry in `downloadModes`. + +**Fix (final, correct approach):** +- Switched mode assignment from `chrome.downloads.download()` callbacks to + `chrome.downloads.onCreated`, which fires reliably for every download. +- Before each `chrome.downloads.download()` call in `downloadInChunks`, the + URL and intended mode are pre-registered in two in-memory maps: + `pendingChunkFlowUrls` (Set) and `pendingModeByUrl` (plain object). +- `onCreated` checks whether `downloadItem.url` is in `pendingChunkFlowUrls`. + - **Yes** → ChunkFlow download; look up mode, call `storeDownloadMode`, + remove from both maps. `storeDownloadMode` sends `DOWNLOAD_UPDATE` after + the write. Return early (no duplicate notification). + - **No** → Native Chrome download; call `storeDownloadMode(id, 'browser')`. +- All `chrome.downloads.download()` callbacks that previously called + `storeDownloadMode` were removed (redundant now, and were the source of the + timing bug). + +**New badge: ↓ Browser** — grey badge shown for downloads not initiated by +ChunkFlow (native Chrome, other extensions, Google Drive UI, etc.). + +### Feature — Full download history +**Previously:** `fetchDownloads` filtered results to `startTime > 1 hour ago`. + +**Fix:** Replaced the time filter with +`chrome.downloads.search({ orderBy: ['-startTime'], limit: 50 })`, which +returns the 50 most-recent downloads (newest first) regardless of age. + +Also added a human-readable timestamp to each download card (`formatDownloadTime` +helper: "Just now", "5m ago", "Today 2:30 PM", "Yesterday …", "Feb 3 …"). + +Empty-state message updated from "No active downloads" → "No downloads yet". + +--- + +## What went wrong / mistakes + +| # | Mistake | How it was caught | Fix | +|---|---------|-------------------|-----| +| 1 | Early `return` in `updateDownloadsList` for empty downloads list skipped `activeFetches` rendering entirely | Code review after writing it | Moved `fiveMinutesAgo` / `pendingFetches` calc before the guard; combined condition | +| 2 | `storeDownloadMode` sent `DOWNLOAD_UPDATE` from `chrome.downloads.download()` callback, which fires AFTER `onCreated` → popup rendered without badge on first try | User reported badge still missing after first fix | Moved notification into the `chrome.storage.local.set()` callback | +| 3 | Rapid concurrent `fetchDownloads` calls could overwrite fresh renders with stale data | User still reported missing badge intermittently | Added `fetchGeneration` counter | +| 4 | Mode badge fix still left native Chrome downloads (e.g. Google Drive button) with no badge ever | User reported "still no badge" with screenshot showing 2.83 GB + 1.59 GB files | Switched to `onCreated`-based assignment with `pendingChunkFlowUrls` pre-registration; added `'browser'` mode | +| 5 | Duplicate `fiveMinutesAgo` variable computed twice in `updateDownloadsList` | Code review | Unified into one `pendingFetches` variable reused in both places | + +--- + +## Files created / modified + +- `web_plugin_22_full_functionality/background.js` + - Added `pendingChunkFlowUrls` (Set) and `pendingModeByUrl` (object) at + module top + - `storeDownloadMode` — callback added to `set()` to send `DOWNLOAD_UPDATE` + after write + - Added `addActiveChunkFetch(url)` and `removeActiveChunkFetch(url)` helpers + - `downloadInChunks` — rewritten as `async/await`; `activeChunkFetch` + tracking at every exit point; pre-registers mode in `pendingModeByUrl` / + `pendingChunkFlowUrls` before each `chrome.downloads.download()` call; + removed all callbacks from `chrome.downloads.download()` + - `chrome.downloads.onCreated` — rewritten to assign mode from + `pendingModeByUrl` for ChunkFlow downloads or `'browser'` for all others + +- `web_plugin_22_full_functionality/popup.js` + - `fetchGeneration` counter added to `fetchDownloads` + - `fetchDownloads` reads `activeChunkFetches` from storage alongside + `downloadModes`; removed 1-hour filter; uses `orderBy + limit` + - `updateDownloadsList` — accepts `activeFetches` param; renders + "preparing" placeholders; restructured empty-state guard; 'browser' mode + branch added to badge switch + - `formatDownloadTime` helper added (relative/absolute timestamp) + - Timestamp `
` added to each download card + +- `web_plugin_22_full_functionality/popup.css` + - Added `.download-time` style + - Added `.badge-browser` style + +--- + +## Commits shipped this session (on `feat/chunk-count-config`) + +``` +1feac31 fix(badges): guarantee every download gets a mode badge +47e05ca feat(popup): show full download history, not just last hour +bd90114 fix(popup+background): fix missing mode badge and invisible first download +``` + +(Earlier commits from the previous session in this branch also included +`host_permissions` fix, contentScript fixes, background code health, etc.) + +--- + +## Branch / test status +- Branch: `feat/chunk-count-config` — pushed to `origin` +- Tests: 26/26 Jest passing (`npm test`) +- Manually verified: badges show for chunked, normal, fallback, and browser + downloads; full history visible; timestamp on each card + +--- + +## How to verify (quick checklist) +- [ ] Reload extension at `chrome://extensions` +- [ ] Open popup → shows up to 50 most-recent downloads (not just last hour) +- [ ] Each download card shows a timestamp ("Just now", "5m ago", etc.) +- [ ] Start a download via right-click → "Download with ChunkFlow": + - [ ] Popup immediately shows "⚡ Chunked (preparing)" placeholder + - [ ] Placeholder disappears, real item appears with ⚡/↓/⚠ badge +- [ ] Start a download normally in Chrome (address bar, Google Drive button): + - [ ] Item appears in popup with grey "↓ Browser" badge +- [ ] Empty popup shows "No downloads yet" (not "No active downloads") + +--- + +## Current limitations / known issues +- `pendingChunkFlowUrls` and `pendingModeByUrl` are in-memory. If the + service worker is terminated between calling `chrome.downloads.download()` + and when `onCreated` fires (very unlikely but theoretically possible in + MV3), a ChunkFlow download would be tagged as `'browser'` instead of its + correct mode. Mitigation: use `chrome.storage.local` for these maps if + this becomes a real problem. +- Files > 500 MB bypass in-memory chunking (OOM guard) and are always + `'normal'` mode even when initiated through ChunkFlow. +- Blob URL downloads (chunked path) are only valid while the service worker + is alive. If the SW is terminated after creating the blob but before + Chrome finishes reading it, the download fails. Currently mitigated by the + popup's open port keeping the SW alive during the download. + +--- + +## Next steps +1. Open PR: `feat/chunk-count-config` → `main` +2. Consider a `.github/workflows/test.yml` CI step to run `npm test` on push +3. For chunked downloads > 500 MB: explore streaming directly to OPFS (Origin + Private File System) instead of in-memory Uint8Array to lift the OOM limit +4. Upload progress visualisation in popup (currently console-only)