feat: faster collection mount via file cache (Beta) (#8222)

This commit is contained in:
Chirag Chandrashekhar
2026-06-22 15:03:21 +05:30
committed by GitHub
parent d1ebf578b2
commit 683d487181
24 changed files with 1559 additions and 236 deletions

213
package-lock.json generated
View File

@@ -5027,7 +5027,7 @@
"version": "7.26.3",
"resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.26.3.tgz",
"integrity": "sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.25.9",
@@ -5045,7 +5045,7 @@
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.3.tgz",
"integrity": "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-compilation-targets": "^7.22.6",
@@ -5062,7 +5062,7 @@
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@@ -5080,7 +5080,7 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/@babel/helper-globals": {
@@ -5160,7 +5160,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz",
"integrity": "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.25.9",
@@ -5235,7 +5235,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz",
"integrity": "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.25.9",
@@ -5278,7 +5278,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz",
"integrity": "sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9",
@@ -5295,7 +5295,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz",
"integrity": "sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -5311,7 +5311,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz",
"integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -5327,7 +5327,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz",
"integrity": "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9",
@@ -5345,7 +5345,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz",
"integrity": "sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9",
@@ -5380,7 +5380,7 @@
"version": "7.21.0-placeholder-for-preset-env.2",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz",
"integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -5479,7 +5479,7 @@
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz",
"integrity": "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -5495,7 +5495,7 @@
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz",
"integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -5677,7 +5677,7 @@
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz",
"integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.18.6",
@@ -5694,7 +5694,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz",
"integrity": "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -5710,7 +5710,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.9.tgz",
"integrity": "sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9",
@@ -5728,7 +5728,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz",
"integrity": "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.25.9",
@@ -5746,7 +5746,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.25.9.tgz",
"integrity": "sha512-toHc9fzab0ZfenFpsyYinOX0J/5dgJVA2fm64xPewu7CoYHWEivIWKxkK2rMi4r3yQqLnVmheMXRdG+k239CgA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -5762,7 +5762,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz",
"integrity": "sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -5794,7 +5794,7 @@
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz",
"integrity": "sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-class-features-plugin": "^7.25.9",
@@ -5811,7 +5811,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz",
"integrity": "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.25.9",
@@ -5832,7 +5832,7 @@
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=4"
@@ -5842,7 +5842,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz",
"integrity": "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9",
@@ -5859,7 +5859,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz",
"integrity": "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -5875,7 +5875,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz",
"integrity": "sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.25.9",
@@ -5892,7 +5892,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz",
"integrity": "sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -5908,7 +5908,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz",
"integrity": "sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.25.9",
@@ -5925,7 +5925,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz",
"integrity": "sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -5941,7 +5941,7 @@
"version": "7.26.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz",
"integrity": "sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -5957,7 +5957,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz",
"integrity": "sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -5989,7 +5989,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz",
"integrity": "sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9",
@@ -6006,7 +6006,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz",
"integrity": "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-compilation-targets": "^7.25.9",
@@ -6024,7 +6024,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz",
"integrity": "sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -6040,7 +6040,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz",
"integrity": "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -6056,7 +6056,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz",
"integrity": "sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -6072,7 +6072,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz",
"integrity": "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -6088,7 +6088,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz",
"integrity": "sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-transforms": "^7.25.9",
@@ -6121,7 +6121,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz",
"integrity": "sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-transforms": "^7.25.9",
@@ -6140,7 +6140,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz",
"integrity": "sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-transforms": "^7.25.9",
@@ -6157,7 +6157,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz",
"integrity": "sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.25.9",
@@ -6174,7 +6174,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz",
"integrity": "sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -6205,7 +6205,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz",
"integrity": "sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -6221,7 +6221,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz",
"integrity": "sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-compilation-targets": "^7.25.9",
@@ -6239,7 +6239,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz",
"integrity": "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9",
@@ -6256,7 +6256,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz",
"integrity": "sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -6288,7 +6288,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz",
"integrity": "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -6320,7 +6320,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz",
"integrity": "sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.25.9",
@@ -6338,7 +6338,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz",
"integrity": "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -6423,7 +6423,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz",
"integrity": "sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9",
@@ -6440,7 +6440,7 @@
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz",
"integrity": "sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.25.9",
@@ -6457,7 +6457,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz",
"integrity": "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -6473,7 +6473,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz",
"integrity": "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -6489,7 +6489,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz",
"integrity": "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9",
@@ -6506,7 +6506,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz",
"integrity": "sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -6522,7 +6522,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.9.tgz",
"integrity": "sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -6538,7 +6538,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.9.tgz",
"integrity": "sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -6573,7 +6573,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz",
"integrity": "sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
@@ -6589,7 +6589,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz",
"integrity": "sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.25.9",
@@ -6606,7 +6606,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz",
"integrity": "sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.25.9",
@@ -6623,7 +6623,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz",
"integrity": "sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.25.9",
@@ -6640,7 +6640,7 @@
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.0.tgz",
"integrity": "sha512-H84Fxq0CQJNdPFT2DrfnylZ3cf5K43rGfWK4LJGPpjKHiZlk0/RzwEus3PDDZZg+/Er7lCA03MVacueUuXdzfw==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.26.0",
@@ -6741,7 +6741,7 @@
"version": "0.1.6-no-external-plugins",
"resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz",
"integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.0.0",
@@ -12553,7 +12553,6 @@
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.10.4",
@@ -12573,7 +12572,6 @@
"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"
@@ -12586,7 +12584,6 @@
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1",
@@ -12601,7 +12598,6 @@
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true,
"license": "MIT"
},
"node_modules/@testing-library/jest-dom": {
@@ -12682,7 +12678,6 @@
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/babel__core": {
@@ -12981,7 +12976,6 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/lodash": {
@@ -13004,7 +12998,6 @@
"version": "12.2.3",
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz",
"integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/linkify-it": "*",
@@ -13015,7 +13008,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/ms": {
@@ -14507,7 +14499,6 @@
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"dequal": "^2.0.3"
@@ -14933,7 +14924,7 @@
"version": "0.4.12",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.12.tgz",
"integrity": "sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.22.6",
@@ -14948,7 +14939,7 @@
"version": "0.10.6",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz",
"integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-define-polyfill-provider": "^0.6.2",
@@ -14962,7 +14953,7 @@
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.3.tgz",
"integrity": "sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-define-polyfill-provider": "^0.6.3"
@@ -16819,7 +16810,7 @@
"version": "3.39.0",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.39.0.tgz",
"integrity": "sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"browserslist": "^4.24.2"
@@ -18049,7 +18040,6 @@
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true,
"license": "MIT"
},
"node_modules/dom-converter": {
@@ -21623,7 +21613,7 @@
"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,
"devOptional": true,
"license": "MIT",
"dependencies": {
"hasown": "^2.0.2"
@@ -24038,7 +24028,6 @@
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
"license": "MIT",
"bin": {
"lz-string": "bin/bin.js"
@@ -25663,7 +25652,7 @@
"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,
"devOptional": true,
"license": "MIT"
},
"node_modules/path-scurry": {
@@ -28056,14 +28045,14 @@
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
"integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/regenerate-unicode-properties": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz",
"integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"regenerate": "^1.4.2"
@@ -28076,7 +28065,7 @@
"version": "0.15.2",
"resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz",
"integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.8.4"
@@ -28086,7 +28075,7 @@
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz",
"integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"regenerate": "^1.4.2",
@@ -28104,14 +28093,14 @@
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz",
"integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/regjsparser": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz",
"integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==",
"dev": true,
"devOptional": true,
"license": "BSD-2-Clause",
"dependencies": {
"jsesc": "~3.0.2"
@@ -28124,7 +28113,7 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz",
"integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==",
"dev": true,
"devOptional": true,
"license": "MIT",
"bin": {
"jsesc": "bin/jsesc"
@@ -28316,7 +28305,7 @@
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"is-core-module": "^2.16.0",
@@ -30573,7 +30562,7 @@
"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,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -31767,7 +31756,7 @@
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -31828,7 +31817,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz",
"integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=4"
@@ -31838,7 +31827,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz",
"integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"unicode-canonical-property-names-ecmascript": "^2.0.0",
@@ -31852,7 +31841,7 @@
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz",
"integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=4"
@@ -31862,7 +31851,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz",
"integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=4"
@@ -32605,6 +32594,12 @@
"node": ">=0.10.0"
}
},
"node_modules/workerpool": {
"version": "10.0.2",
"resolved": "https://registry.npmjs.org/workerpool/-/workerpool-10.0.2.tgz",
"integrity": "sha512-8PCeZlCwu0+8hXruze1ahYNsY+M0LOCmbmySZ9BWWqWIXP9TAXa6FZCxACTDL/0j47pFcC4xW98Gr8nAC5oymg==",
"license": "Apache-2.0"
},
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
@@ -35226,6 +35221,7 @@
"socks-proxy-agent": "^8.0.2",
"tough-cookie": "^6.0.0",
"uuid": "^10.0.0",
"workerpool": "10.0.2",
"yup": "^0.32.11",
"zod": "^4.1.8"
},
@@ -36157,6 +36153,25 @@
"ohm-js": "^16.6.0"
}
},
"packages/bruno-pool": {
"name": "@usebruno/pool",
"version": "0.1.0",
"extraneous": true,
"license": "MIT",
"dependencies": {
"@usebruno/filestore": "0.1.0",
"workerpool": "10.0.2"
},
"devDependencies": {
"@rollup/plugin-commonjs": "23.0.2",
"@rollup/plugin-node-resolve": "15.0.1",
"@rollup/plugin-typescript": "12.1.2",
"@types/node": "^24.1.0",
"rollup": "3.30.0",
"tslib": "2.8.1",
"typescript": "5.4.5"
}
},
"packages/bruno-query": {
"name": "@usebruno/query",
"version": "0.1.0",
@@ -36385,6 +36400,16 @@
"node": ">=14.17"
}
},
"packages/bruno-storage": {
"name": "@usebruno/storage",
"version": "0.1.0",
"extraneous": true,
"license": "MIT",
"devDependencies": {
"@types/node": "^24.1.0",
"typescript": "5.4.5"
}
},
"packages/bruno-tests": {
"name": "@usebruno/tests",
"version": "0.0.1",

View File

@@ -2,11 +2,85 @@ import styled from 'styled-components';
const StyledWrapper = styled.div`
color: ${(props) => props.theme.text};
form.bruno-form {
label {
font-size: 0.8125rem;
}
.cache-section-title {
text-transform: uppercase;
font-size: ${(props) => props.theme.font.size.sm};
letter-spacing: 0.05em;
color: ${(props) => props.theme.colors.text.muted};
margin-bottom: 0.75rem;
}
.cache-item {
border: 1px solid ${(props) => props.theme.border.border1};
border-radius: ${(props) => props.theme.border.radius.md};
margin-bottom: 1rem;
overflow: hidden;
}
.cache-item-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1rem;
gap: 1rem;
background: ${(props) => props.theme.background.surface0};
border-bottom: 1px solid ${(props) => props.theme.border.border1};
}
.cache-item-title-group {
display: flex;
align-items: center;
gap: 0.5rem;
}
.cache-item-title {
font-size: ${(props) => props.theme.font.size.md};
font-weight: 600;
}
.beta-badge {
display: inline-flex;
align-items: center;
padding: 1px 6px;
border-radius: ${(props) => props.theme.border.radius.sm};
font-size: ${(props) => props.theme.font.size.xs};
font-weight: 500;
line-height: 1.5;
background: ${(props) => props.theme.status.info.background};
color: ${(props) => props.theme.status.info.text};
}
.cache-item-body {
display: flex;
align-items: flex-end;
justify-content: space-between;
padding: 0.875rem 1rem;
gap: 1.25rem;
}
.cache-item-body-text {
flex: 1;
min-width: 0;
}
.cache-item-description {
font-size: ${(props) => props.theme.font.size.base};
color: ${(props) => props.theme.colors.text.muted};
line-height: 1.5;
margin: 0;
}
.cache-item-size {
font-size: ${(props) => props.theme.font.size.base};
color: ${(props) => props.theme.colors.text.subtext2};
margin: 0.5rem 0 0 0;
}
.cache-item-size strong {
font-weight: 600;
color: ${(props) => props.theme.text};
margin-left: 0.25rem;
}
`;

View File

@@ -1,120 +1,147 @@
import React, { useEffect, useCallback, useRef } from 'react';
import { useFormik } from 'formik';
import React, { useEffect, useState, useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
savePreferences,
clearHttpHttpsAgentCache
} from 'providers/ReduxStore/slices/app';
import { savePreferences, clearHttpHttpsAgentCache } from 'providers/ReduxStore/slices/app';
import toast from 'react-hot-toast';
import StyledWrapper from './StyledWrapper';
import * as Yup from 'yup';
import debounce from 'lodash/debounce';
import get from 'lodash/get';
const cacheSchema = Yup.object().shape({
sslSession: Yup.object({
enabled: Yup.boolean()
})
});
import { IconEraser } from '@tabler/icons';
import { useTheme } from 'providers/Theme';
import ToggleSwitch from 'components/ToggleSwitch';
import ActionIcon from 'ui/ActionIcon';
import StyledWrapper from './StyledWrapper';
import { formatSize } from 'utils/common';
const Cache = () => {
const preferences = useSelector((state) => state.app.preferences);
const dispatch = useDispatch();
const { theme } = useTheme();
const { ipcRenderer } = window;
const handleSave = useCallback(
(newCachePreferences) => {
dispatch(
savePreferences({
...preferences,
cache: newCachePreferences
})
).catch(() => toast.error('Failed to update cache preferences'));
},
[dispatch, preferences]
);
const fileCacheEnabled = get(preferences, 'cache.file.enabled', false);
const sslSessionEnabled = get(preferences, 'cache.sslSession.enabled', false);
const handleSaveRef = useRef(handleSave);
handleSaveRef.current = handleSave;
const [fileCacheSize, setFileCacheSize] = useState(null);
const formik = useFormik({
initialValues: {
sslSession: {
enabled: get(preferences, 'cache.sslSession.enabled', false)
}
},
validationSchema: cacheSchema,
onSubmit: async (values) => {
try {
const newPreferences = await cacheSchema.validate(values, { abortEarly: true });
handleSave(newPreferences);
} catch (error) {
console.error('Cache preferences validation error:', error.message);
}
}
});
const debouncedSave = useCallback(
debounce((values) => {
cacheSchema
.validate(values, { abortEarly: true })
.then((validatedValues) => handleSaveRef.current(validatedValues))
.catch(() => {});
}, 500),
[]
);
const refreshFileCacheSize = useCallback(() => {
if (!ipcRenderer) return;
ipcRenderer
.invoke('renderer:get-file-cache-size')
.then((size) => setFileCacheSize(size))
.catch(() => setFileCacheSize(null));
}, [ipcRenderer]);
useEffect(() => {
if (formik.dirty && formik.isValid) {
debouncedSave(formik.values);
}
return () => {
debouncedSave.flush();
};
}, [formik.values, formik.dirty, formik.isValid, debouncedSave]);
refreshFileCacheSize();
}, [refreshFileCacheSize, fileCacheEnabled]);
const handleAgentCachingChange = (e) => {
formik.handleChange(e);
// Immediately evict all cached agents when caching is disabled
if (!e.target.checked) {
const persist = (next) => {
dispatch(savePreferences({ ...preferences, cache: next })).catch(() => {
toast.error('Failed to update cache preferences');
});
};
const handleToggleFileCache = () => {
persist({
...preferences.cache,
file: { enabled: !fileCacheEnabled }
});
};
const handleToggleSslSession = () => {
const next = !sslSessionEnabled;
persist({
...preferences.cache,
sslSession: { enabled: next }
});
if (!next) {
dispatch(clearHttpHttpsAgentCache()).catch(() => {});
}
};
const handleResetCache = () => {
const handleClearFileCache = () => {
if (!ipcRenderer) return;
ipcRenderer
.invoke('renderer:clear-file-cache')
.then((size) => {
setFileCacheSize(size);
toast.success('File cache cleared');
})
.catch(() => toast.error('Failed to clear file cache'));
};
const handleClearSslSession = () => {
dispatch(clearHttpHttpsAgentCache())
.then(() => toast.success('ssl session cache cleared'))
.catch(() => toast.error('Failed to clear ssl session cache'));
.then(() => toast.success('SSL session cache cleared'))
.catch(() => toast.error('Failed to clear SSL session cache'));
};
return (
<StyledWrapper className="w-full">
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<div className="section-title mt-6 mb-3">Cache SSL Session</div>
<div className="cache-section-title">Cache</div>
<div className="flex items-center my-2">
<input
id="sslSession.enabled"
type="checkbox"
name="sslSession.enabled"
checked={formik.values.sslSession.enabled}
onChange={handleAgentCachingChange}
className="mousetrap mr-0"
<div className="cache-item">
<div className="cache-item-header">
<div className="cache-item-title-group">
<span className="cache-item-title">File cache</span>
<span className="beta-badge">Beta</span>
</div>
<ToggleSwitch
data-testid="cache.file.enabled"
isOn={fileCacheEnabled}
handleToggle={handleToggleFileCache}
size="2xs"
activeColor={theme.primary.solid}
/>
<label className="block ml-2 select-none" htmlFor="sslSession.enabled">
Enable SSL session caching
</label>
</div>
<div className="text-xs mt-1 ml-6 opacity-70">
Reuses TLS sessions and connections across requests for faster handshakes. Disable to create a fresh connection for every
request.
<div className="cache-item-body">
<div className="cache-item-body-text">
<p className="cache-item-description">
Loads your workspace faster by caching opened collections. Bruno refreshes the cache when your collection
changes. Clearing it won't affect your original files.
</p>
<p className="cache-item-size">
Cache size <strong>{fileCacheSize == null ? '' : formatSize(fileCacheSize)}</strong>
</p>
</div>
<ActionIcon
label="Clear cache"
onClick={handleClearFileCache}
disabled={!fileCacheSize}
colorOnHover={theme.colors.text.danger}
>
<IconEraser size={16} strokeWidth={1.5} />
</ActionIcon>
</div>
</div>
<div className="mt-6">
<button type="button" className="text-link cursor-pointer hover:underline" onClick={handleResetCache}>
Clear
</button>
<div className="cache-item">
<div className="cache-item-header">
<div className="cache-item-title-group">
<span className="cache-item-title">SSL session cache</span>
</div>
<ToggleSwitch
data-testid="sslSession.enabled"
isOn={sslSessionEnabled}
handleToggle={handleToggleSslSession}
size="2xs"
activeColor={theme.primary.solid}
/>
</div>
</form>
<div className="cache-item-body">
<div className="cache-item-body-text">
<p className="cache-item-description">
Reuses TLS sessions and connections across requests for faster handshakes. Disable to create a fresh
connection for every request.
</p>
</div>
<ActionIcon
label="Clear cache"
onClick={handleClearSslSession}
colorOnHover={theme.colors.text.danger}
>
<IconEraser size={16} strokeWidth={1.5} />
</ActionIcon>
</div>
</div>
</StyledWrapper>
);
};

View File

@@ -36,7 +36,7 @@ import toast from 'react-hot-toast';
import { useDispatch, useStore } from 'react-redux';
import { isElectron } from 'utils/common/platform';
import { globalEnvironmentsUpdateEvent, updateGlobalEnvironments } from 'providers/ReduxStore/slices/global-environments';
import { collectionAddOauth2CredentialsByUrl, collectionClearOauth2CredentialsByCredentialsId, updateCollectionLoadingState } from 'providers/ReduxStore/slices/collections/index';
import { collectionAddOauth2CredentialsByUrl, collectionClearOauth2CredentialsByCredentialsId, updateCollectionLoadingState, collectionLoadedFromTree } from 'providers/ReduxStore/slices/collections/index';
import { addLog } from 'providers/ReduxStore/slices/logs';
import { loadNotifications } from 'providers/ReduxStore/slices/notifications';
import { updateSystemResources } from 'providers/ReduxStore/slices/performance';
@@ -348,7 +348,22 @@ const useIpcEvents = () => {
dispatch(loadNotifications(notifications));
});
const removeCollectionTreeLoadedListener = ipcRenderer.on('main:collection-tree-loaded', ({ collectionUid, tree }) => {
dispatch(collectionLoadedFromTree({ collectionUid, tree }));
});
const removeCollectionLoadingStateV2Listener = ipcRenderer.on('main:collection-loading-state-updated-v2', (val) => {
dispatch(updateCollectionLoadingState(val));
});
const removeBrunoConfigUpdateV2Listener = ipcRenderer.on('main:bruno-config-update-v2', (val) => {
dispatch(brunoConfigUpdateEvent(val));
});
return () => {
removeCollectionTreeLoadedListener();
removeCollectionLoadingStateV2Listener();
removeBrunoConfigUpdateV2Listener();
removeCollectionTreeUpdateListener();
removeApiSpecTreeUpdateListener();
removeOpenCollectionListener();

View File

@@ -4,7 +4,7 @@ import filter from 'lodash/filter';
import { createListenerMiddleware } from '@reduxjs/toolkit';
import { removeTaskFromQueue } from 'providers/ReduxStore/slices/app';
import { addTab } from 'providers/ReduxStore/slices/tabs';
import { collectionAddFileEvent, collectionChangeFileEvent } from 'providers/ReduxStore/slices/collections';
import { collectionAddFileEvent, collectionChangeFileEvent, collectionLoadedFromTree } from 'providers/ReduxStore/slices/collections';
import { findCollectionByUid, findItemInCollectionByPathname, getDefaultRequestPaneTab, findItemInCollectionByItemUid } from 'utils/collections/index';
import { taskTypes } from './utils';
@@ -25,31 +25,53 @@ taskMiddleware.startListening({
const openRequestTasks = filter(state.app.taskQueue, { type: taskTypes.OPEN_REQUEST });
each(openRequestTasks, (task) => {
if (collectionUid === task.collectionUid) {
const collection = findCollectionByUid(state.collections.collections, collectionUid);
if (collection && collection.mountStatus === 'mounted' && !collection.isLoading) {
const item = findItemInCollectionByPathname(collection, task.itemPathname);
if (item) {
listenerApi.dispatch(
addTab({
uid: item.uid,
collectionUid: collection.uid,
type: item.type,
pathname: item.pathname,
requestPaneTab: getDefaultRequestPaneTab(item),
preview: task?.preview ?? true,
...(item.isTransient ? { isTransient: true } : {})
})
);
}
}
if (collectionUid !== task.collectionUid) return;
const collection = findCollectionByUid(state.collections.collections, collectionUid);
if (!collection || collection.mountStatus !== 'mounted' || collection.isLoading) return;
const item = findItemInCollectionByPathname(collection, task.itemPathname);
if (!item) return;
listenerApi.dispatch(
addTab({
uid: item.uid,
collectionUid: collection.uid,
type: item.type,
pathname: item.pathname,
requestPaneTab: getDefaultRequestPaneTab(item),
preview: task?.preview ?? true,
...(item.isTransient ? { isTransient: true } : {})
})
);
listenerApi.dispatch(removeTaskFromQueue({ taskUid: task.uid }));
});
}
});
listenerApi.dispatch(
removeTaskFromQueue({
taskUid: task.uid
})
);
}
// v2 tree push also acts as a signal for queued OPEN_REQUEST tasks.
taskMiddleware.startListening({
actionCreator: collectionLoadedFromTree,
effect: (action, listenerApi) => {
const state = listenerApi.getState();
const collectionUid = get(action, 'payload.collectionUid');
const openRequestTasks = filter(state.app.taskQueue, { type: taskTypes.OPEN_REQUEST });
each(openRequestTasks, (task) => {
if (collectionUid !== task.collectionUid) return;
const collection = findCollectionByUid(state.collections.collections, collectionUid);
if (!collection || collection.mountStatus !== 'mounted' || collection.isLoading) return;
const item = findItemInCollectionByPathname(collection, task.itemPathname);
if (!item) return;
listenerApi.dispatch(
addTab({
uid: item.uid,
collectionUid: collection.uid,
type: item.type,
pathname: item.pathname,
requestPaneTab: getDefaultRequestPaneTab(item),
preview: task?.preview ?? true,
...(item.isTransient ? { isTransient: true } : {})
})
);
listenerApi.dispatch(removeTaskFromQueue({ taskUid: task.uid }));
});
}
});

View File

@@ -3122,8 +3122,10 @@ export const mountCollection
= ({ collectionUid, collectionPathname, brunoConfig, skipTabRestore = false, workspacePathname = null }) =>
(dispatch, getState) => {
dispatch(updateCollectionMountStatus({ collectionUid, mountStatus: 'mounting' }));
const fileCacheEnabled = getState().app?.preferences?.cache?.file?.enabled;
const channel = fileCacheEnabled ? 'renderer:mount-collection-v2' : 'renderer:mount-collection';
return new Promise(async (resolve, reject) => {
callIpc('renderer:mount-collection', { collectionUid, collectionPathname, brunoConfig })
callIpc(channel, { collectionUid, collectionPathname, brunoConfig })
.then(async (transientDirPath) => {
dispatch(updateCollectionMountStatus({ collectionUid, mountStatus: 'mounted' }));
dispatch(addTransientDirectory({ collectionUid, pathname: transientDirPath }));

View File

@@ -1,6 +1,6 @@
import { parseQueryParams, buildQueryString as stringifyQueryParams } from '@usebruno/common/utils';
import { uuid } from 'utils/common';
import { find, map, forOwn, concat, filter, each, cloneDeep, get, set, findIndex } from 'lodash';
import { find, map, forOwn, concat, filter, each, cloneDeep, get, set, findIndex, pick } from 'lodash';
import { createSlice } from '@reduxjs/toolkit';
import { hexy as hexdump } from 'hexy';
import {
@@ -27,6 +27,69 @@ import { getCollectionEnvironmentPath } from 'utils/snapshot';
import { getDataTypeFromValue } from '@usebruno/common/utils';
import * as exampleReducers from './exampleReducers';
const FILE_DERIVED_REQUEST_FIELDS = [
'name',
'type',
'seq',
'tags',
'request',
'settings',
'examples',
'filename',
'pathname',
'partial',
'loading',
'size',
'error',
'isTransient'
];
const FILE_DERIVED_FOLDER_FIELDS = [
'name',
'filename',
'pathname',
'seq',
'type',
'root'
];
const mergeTreeItems = (existingItems, newItems) => {
if (!Array.isArray(existingItems) || existingItems.length === 0) return newItems;
const existingByUid = new Map();
for (const item of existingItems) {
if (item && item.uid) existingByUid.set(item.uid, item);
}
return newItems.map((newItem) => {
const existing = existingByUid.get(newItem.uid);
if (!existing) return newItem;
if (newItem.type === 'folder') {
const merged = { ...existing, ...pick(newItem, FILE_DERIVED_FOLDER_FIELDS) };
merged.items = mergeTreeItems(existing.items, newItem.items || []);
return merged;
}
// seq-only change (reorder) — keep everything else, including the draft
if (areItemsTheSameExceptSeqUpdate(existing, newItem)) {
const merged = { ...existing, seq: newItem.seq };
if (merged.draft) {
merged.draft = { ...merged.draft, seq: newItem.seq };
if (areItemsTheSameExceptSeqUpdate(merged.draft, newItem)) {
merged.draft = null;
}
}
return merged;
}
const merged = { ...existing, ...pick(newItem, FILE_DERIVED_REQUEST_FIELDS) };
// only drop the draft if it matches what's on disk — user may still be typing
const draftMatchesFile = existing.draft && areItemsTheSameExceptSeqUpdate(existing.draft, newItem);
merged.draft = draftMatchesFile ? null : (existing.draft || null);
return merged;
});
};
// gRPC status code meanings
const grpcStatusCodes = {
0: 'OK',
@@ -3341,6 +3404,35 @@ export const collectionsSlice = createSlice({
}
}
},
collectionLoadedFromTree: (state, action) => {
const { collectionUid, tree } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
if (!collection) return;
collection.items = mergeTreeItems(collection.items, tree?.items || []);
collection.environments = tree?.environments || [];
if (tree?.root !== undefined) {
collection.root = tree.root;
}
if (tree?.brunoConfig) {
collection.brunoConfig = tree.brunoConfig;
}
const tempDirectory = state.tempDirectories?.[collectionUid];
if (tempDirectory) {
const annotateTransient = (items) => {
for (const item of items) {
if (item.pathname && item.pathname.startsWith(tempDirectory)) {
item.isTransient = true;
}
if (item.type === 'folder' && Array.isArray(item.items)) {
annotateTransient(item.items);
}
}
};
annotateTransient(collection.items);
}
addDepth(collection.items);
},
collectionAddOauth2CredentialsByUrl: (state, action) => {
const { collectionUid, folderUid, itemUid, url, credentials, credentialsId, debugInfo, executionMode } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
@@ -3734,6 +3826,7 @@ export const {
createCollection,
updateCollectionMountStatus,
updateCollectionLoadingState,
collectionLoadedFromTree,
setCollectionSecurityConfig,
brunoConfigUpdateEvent,
renameCollection,

View File

@@ -1344,11 +1344,20 @@ export const mountScratchCollection = (workspaceUid) => {
ignore: ['node_modules', '.git']
};
await ipcRenderer.invoke('renderer:add-collection-watcher', {
collectionPath: tempDirectoryPath,
collectionUid: scratchCollectionUid,
brunoConfig
});
const fileCacheEnabled = state.app?.preferences?.cache?.file?.enabled;
if (fileCacheEnabled) {
await ipcRenderer.invoke('renderer:mount-collection-v2', {
collectionUid: scratchCollectionUid,
collectionPathname: tempDirectoryPath,
brunoConfig
});
} else {
await ipcRenderer.invoke('renderer:add-collection-watcher', {
collectionPath: tempDirectoryPath,
collectionUid: scratchCollectionUid,
brunoConfig
});
}
// Map scratch collection to workspace so getProcessEnvVars can resolve workspace .env values
if (workspace.pathname) {

View File

@@ -79,6 +79,7 @@
"socks-proxy-agent": "^8.0.2",
"tough-cookie": "^6.0.0",
"uuid": "^10.0.0",
"workerpool": "10.0.2",
"yup": "^0.32.11",
"zod": "^4.1.8"
},

View File

@@ -33,6 +33,27 @@ const MAX_FILE_SIZE = 2.5 * 1024 * 1024;
const environmentSecretsStore = new EnvironmentSecretsStore();
// registered collections stage parsed data into the git-lite snapshot (no-op otherwise)
const fileIndexByCollection = new Map();
const stageToCache = (collectionPath, pathname, data) => {
const index = fileIndexByCollection.get(collectionPath);
if (!index) return;
try {
index.stageParsed(collectionPath, pathname, data);
} catch (err) {
console.error('[collection-watcher] cache stage failed for', pathname, err);
}
};
const unstageFromCache = (collectionPath, pathname) => {
const index = fileIndexByCollection.get(collectionPath);
if (!index) return;
try {
index.unstagePath(collectionPath, pathname);
} catch (err) {
console.error('[collection-watcher] cache unstage failed for', pathname, err);
}
};
const isBrunoConfigFile = (pathname, collectionPath) => {
const dirname = path.dirname(pathname);
const basename = path.basename(pathname);
@@ -107,6 +128,7 @@ const addEnvironmentFile = async (win, pathname, collectionUid, collectionPath)
let content = fs.readFileSync(pathname, 'utf8');
file.data = await parseEnvironment(content, { format });
stageToCache(collectionPath, pathname, file.data);
// Extract name by removing the extension
const ext = path.extname(basename);
@@ -148,6 +170,7 @@ const changeEnvironmentFile = async (win, pathname, collectionUid, collectionPat
const content = fs.readFileSync(pathname, 'utf8');
file.data = await parseEnvironment(content, { format });
stageToCache(collectionPath, pathname, file.data);
// Extract name by removing the extension
const ext = path.extname(basename);
@@ -203,6 +226,7 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread
try {
const content = fs.readFileSync(pathname, 'utf8');
let brunoConfig = JSON.parse(content);
stageToCache(collectionPath, pathname, brunoConfig);
// Transform the config to add exists metadata for protobuf files and import paths
brunoConfig = await transformBrunoConfigAfterRead(brunoConfig, collectionPath);
@@ -249,6 +273,8 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread
}
file.data = collectionRoot;
// cache the full parse result (collectionRoot + brunoConfig for yml)
stageToCache(collectionPath, pathname, parsed);
hydrateCollectionRootWithUuid(file.data);
win.webContents.send('main:collection-tree-updated', 'addFile', file);
@@ -288,6 +314,7 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread
let format = getCollectionFormat(collectionPath);
let content = fs.readFileSync(pathname, 'utf8');
file.data = await parseFolder(content, { format });
stageToCache(collectionPath, pathname, file.data);
hydrateCollectionRootWithUuid(file.data);
win.webContents.send('main:collection-tree-updated', 'addFile', file);
@@ -317,6 +344,7 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread
if (!useWorkerThread) {
try {
file.data = await parseRequest(content, { format });
stageToCache(collectionPath, pathname, file.data);
file.partial = false;
file.loading = false;
file.size = sizeInMB(fileStats?.size);
@@ -360,6 +388,7 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread
format,
filename: pathname
});
stageToCache(collectionPath, pathname, file.data);
file.partial = false;
file.loading = false;
file.data.raw = content;
@@ -429,6 +458,7 @@ const change = async (win, pathname, collectionUid, collectionPath) => {
try {
const content = fs.readFileSync(pathname, 'utf8');
let brunoConfig = JSON.parse(content);
stageToCache(collectionPath, pathname, brunoConfig);
// Transform the config to add file existence checks for protobuf files and import paths
brunoConfig = await transformBrunoConfigAfterRead(brunoConfig, collectionPath);
@@ -477,6 +507,8 @@ const change = async (win, pathname, collectionUid, collectionPath) => {
}
file.data = collectionRoot;
// cache the full parse result (collectionRoot + brunoConfig for yml)
stageToCache(collectionPath, pathname, parsed);
hydrateCollectionRootWithUuid(file.data);
win.webContents.send('main:collection-tree-updated', 'change', file);
@@ -516,6 +548,7 @@ const change = async (win, pathname, collectionUid, collectionPath) => {
let format = getCollectionFormat(collectionPath);
let content = fs.readFileSync(pathname, 'utf8');
file.data = await parseFolder(content, { format });
stageToCache(collectionPath, pathname, file.data);
hydrateCollectionRootWithUuid(file.data);
win.webContents.send('main:collection-tree-updated', 'change', file);
@@ -541,9 +574,11 @@ const change = async (win, pathname, collectionUid, collectionPath) => {
const fileStats = fs.statSync(pathname);
if (fileStats.size >= MAX_FILE_SIZE && format === 'bru') {
// redacted parse — do not write the redacted data through to the cache
file.data = await parseLargeRequestWithRedaction(content, 'bru');
} else {
file.data = await parseRequest(content, { format });
stageToCache(collectionPath, pathname, file.data);
}
file.data.raw = content;
@@ -562,6 +597,8 @@ const unlink = (win, pathname, collectionUid, collectionPath) => {
return;
}
console.log(`watcher unlink: ${pathname}`);
// drop the file from the snapshot regardless of type (request/env/config/folder root)
unstageFromCache(collectionPath, pathname);
if (isEnvironmentsFolder(pathname, collectionPath)) {
return unlinkEnvironmentFile(win, pathname, collectionUid);
@@ -729,11 +766,17 @@ class CollectionWatcher {
delete this.loadingStates[collectionUid];
}
addWatcher(win, watchPath, collectionUid, brunoConfig, forcePolling = false, useWorkerThread) {
addWatcher(win, watchPath, collectionUid, brunoConfig, forcePolling = false, useWorkerThread, options = {}) {
if (this.watchers[watchPath]) {
this.watchers[watchPath].close();
}
// v2 already loaded the tree from cache; skip startup scan and stage live edits
const { ignoreInitial = false, fileIndex = null } = options;
if (fileIndex) {
fileIndexByCollection.set(watchPath, fileIndex);
}
this.initializeLoadingState(collectionUid);
this.startCollectionDiscovery(win, collectionUid);
@@ -746,7 +789,7 @@ class CollectionWatcher {
setTimeout(() => {
const watcher = chokidar.watch(watchPath, {
ignoreInitial: false,
ignoreInitial,
usePolling: isWSLPath(watchPath) || forcePolling ? true : false,
ignored: (filepath) => {
const normalizedPath = normalizeAndResolvePath(filepath);
@@ -802,7 +845,7 @@ class CollectionWatcher {
'Update your system config to allow more concurrently watched files with:',
'"echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p"'
);
this.addWatcher(win, watchPath, collectionUid, brunoConfig, true, useWorkerThread);
this.addWatcher(win, watchPath, collectionUid, brunoConfig, true, useWorkerThread, options);
} else {
console.error(`An error occurred in the watcher for: ${watchPath}`, error);
}
@@ -824,6 +867,8 @@ class CollectionWatcher {
this.watchers[watchPath] = null;
}
fileIndexByCollection.delete(watchPath);
dotEnvWatcher.removeCollectionWatcher(watchPath);
const tempDirectoryPath = this.tempDirectoryMap[watchPath];

View File

@@ -46,6 +46,7 @@ const registerApiSpecIpc = require('./ipc/apiSpec');
const registerGitIpc = require('./ipc/git');
const registerOpenAPISyncIpc = require('./ipc/openapi-sync');
const registerAiIpc = require('./ipc/ai');
const { registerMountIpc } = require('./ipc/mount');
const collectionWatcher = require('./app/collection-watcher');
const WorkspaceWatcher = require('./app/workspace-watcher');
const ApiSpecWatcher = require('./app/apiSpecsWatcher');
@@ -477,6 +478,7 @@ app.on('ready', async () => {
registerGitIpc(mainWindow);
registerOpenAPISyncIpc(mainWindow);
registerAiIpc(mainWindow);
registerMountIpc();
// Internal delegator
ipcMain.handle('main:cache-clear', async () => {
@@ -505,6 +507,8 @@ app.on('before-quit', (event) => {
]);
} catch {}
try { await require('./ipc/mount').shutdown(); } catch {}
if (useSingleInstance && gotTheLock) {
try { app.releaseSingleInstanceLock(); } catch {}
}

View File

@@ -1263,6 +1263,8 @@ const registerRendererEventHandlers = (mainWindow, watcher) => {
}
}
await require('./mount').unmount(collectionUid).catch(() => {});
// Clean up
const { clearCollectionWorkspace } = require('../store/process-env');
clearCollectionWorkspace(collectionUid);

View File

@@ -0,0 +1,35 @@
const { ipcMain, BrowserWindow } = require('electron');
const { MountManager } = require('../services/mount');
const manager = new MountManager();
const registerMountIpc = () => {
ipcMain.handle('renderer:get-file-cache-size', () => manager.getCacheSize());
ipcMain.handle('renderer:clear-file-cache', () => {
manager.clearCache();
return manager.getCacheSize();
});
ipcMain.handle(
'renderer:mount-collection-v2',
async (event, { collectionUid, collectionPathname, brunoConfig }) => {
const win = BrowserWindow.fromWebContents(event.sender);
const send = (channel, payload) => {
if (!win || win.isDestroyed?.()) return;
win.webContents.send(channel, payload);
};
const emit = {
tree: (tree) => send('main:collection-tree-loaded', { collectionUid, tree }),
loading: (isLoading) => send('main:collection-loading-state-updated-v2', { collectionUid, isLoading }),
config: (brunoConfig) => send('main:bruno-config-update-v2', { collectionUid, brunoConfig })
};
return manager.mount({ win, collectionPath: collectionPathname, collectionUid, brunoConfig, emit });
}
);
};
const unmount = (collectionUid) => manager.unmount(collectionUid);
const shutdown = () => manager.shutdown();
module.exports = { registerMountIpc, unmount, shutdown };

View File

@@ -0,0 +1,188 @@
const fs = require('node:fs');
const path = require('node:path');
const { Database } = require('../storage');
const {
hashFile,
hashFileAsync,
normalize,
posixifyPath,
idForAbsolutePath,
resolveDenylist,
isDenied,
walk
} = require('../../utils/mount');
const MIGRATIONS = [
{
version: 1,
up: `
CREATE TABLE IF NOT EXISTS file_index_entries (
collection_path TEXT NOT NULL,
relative_path TEXT NOT NULL,
id TEXT NOT NULL,
mtime INTEGER NOT NULL,
hash TEXT NOT NULL,
data TEXT NOT NULL,
PRIMARY KEY (collection_path, relative_path)
) WITHOUT ROWID;
CREATE INDEX IF NOT EXISTS idx_collection_path ON file_index_entries(collection_path);
`
}
];
class FileIndex {
#db;
#dbPath;
constructor({ dbPath } = {}) {
this.#dbPath = dbPath || path.join(require('electron').app.getPath('userData'), 'mount-snapshots.db');
this.#db = new Database({ path: this.#dbPath, migrations: MIGRATIONS, readBigInts: true });
}
close() {
this.#db.close();
}
async status(collectionPath, options = {}) {
const root = normalize(collectionPath);
const stored = this.#loadStored(root);
const denylist = resolveDenylist(options.denylist);
const added = [];
const updated = [];
const removed = [];
const seen = new Set();
const files = walk(root, denylist);
const results = await Promise.all(files.map(async ({ relativePath, absolutePath }) => {
const stat = await fs.promises.stat(absolutePath, { bigint: true });
const mtime = stat.mtimeNs;
const prior = stored.get(relativePath);
if (!prior) {
const hash = await hashFileAsync(absolutePath);
return { kind: 'added', entry: { relativePath, absolutePath, mtime, hash } };
}
if (prior.mtime === mtime) return { kind: 'unchanged', relativePath };
const hash = await hashFileAsync(absolutePath);
if (hash === prior.hash) return { kind: 'unchanged', relativePath };
return { kind: 'updated', entry: { relativePath, absolutePath, mtime, hash, prevHash: prior.hash } };
}));
for (const r of results) {
if (r.kind === 'added') {
added.push(r.entry);
seen.add(r.entry.relativePath);
} else if (r.kind === 'updated') {
updated.push(r.entry);
seen.add(r.entry.relativePath);
} else {
seen.add(r.relativePath);
}
}
for (const [relativePath, row] of stored) {
if (seen.has(relativePath)) continue;
if (isDenied(posixifyPath(relativePath), denylist)) continue;
removed.push({ relativePath, id: row.id, hash: row.hash });
}
return { added, updated, removed };
}
clear() {
this.#db.exec('DELETE FROM file_index_entries');
// VACUUM so the file actually shrinks after the DELETE
this.#db.exec('VACUUM');
}
get dbPath() {
return this.#dbPath;
}
entries(collectionPath) {
const root = normalize(collectionPath);
const rows = this.#db.all(
'SELECT relative_path AS relativePath, data FROM file_index_entries WHERE collection_path = ?',
root
);
const map = new Map();
for (const row of rows) {
map.set(row.relativePath, { data: JSON.parse(row.data) });
}
return map;
}
stage(collectionPath, entry) {
const root = normalize(collectionPath);
const { op, relativePath } = entry;
if (op === 'remove') {
this.#db.run(
'DELETE FROM file_index_entries WHERE collection_path = ? AND relative_path = ?',
root,
relativePath
);
return;
}
const { mtime, hash, data } = entry;
const id = idForAbsolutePath(path.join(root, relativePath));
this.#db.run(
`
INSERT INTO file_index_entries (collection_path, relative_path, id, mtime, hash, data)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(collection_path, relative_path) DO UPDATE SET
mtime = excluded.mtime,
hash = excluded.hash,
data = excluded.data
`,
root,
relativePath,
id,
mtime,
hash,
JSON.stringify(data)
);
}
stageParsed(collectionPath, absolutePath, data) {
const root = normalize(collectionPath);
const relativePath = path.relative(root, normalize(absolutePath));
if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) return;
const stat = fs.statSync(absolutePath, { bigint: true });
this.stage(root, {
op: 'add',
relativePath,
mtime: stat.mtimeNs,
hash: hashFile(absolutePath),
data
});
}
unstagePath(collectionPath, absolutePath) {
const root = normalize(collectionPath);
const relativePath = path.relative(root, normalize(absolutePath));
if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) return;
this.stage(root, { op: 'remove', relativePath });
}
transaction(callback) {
return this.#db.transaction(callback);
}
#loadStored(collectionPath) {
const rows = this.#db.all(
'SELECT relative_path AS relativePath, id, mtime, hash FROM file_index_entries WHERE collection_path = ?',
collectionPath
);
const map = new Map();
for (const row of rows) {
map.set(row.relativePath, row);
}
return map;
}
}
module.exports = {
FileIndex
};

View File

@@ -0,0 +1,3 @@
const { MountManager } = require('./manager');
module.exports = { MountManager };

View File

@@ -0,0 +1,223 @@
const fs = require('node:fs');
const path = require('node:path');
const { JobType, getPool, destroyPool } = require('../pool');
const { FileIndex } = require('./file-index');
const { buildTree } = require('./tree-builder');
const { defaultClassify, uidForSeed } = require('../../utils/mount');
// cold start only — collection-watcher handles live changes and writes through to the cache
let _envSecretsStore = null;
const getEnvSecretsStore = () => {
if (!_envSecretsStore) {
const EnvironmentSecretsStore = require('../../store/env-secrets');
_envSecretsStore = new EnvironmentSecretsStore();
}
return _envSecretsStore;
};
const envHasSecrets = (env) => Array.isArray(env?.variables) && env.variables.some((v) => v.secret);
const hydrateEnvironments = (collectionPath, environments) => {
if (!Array.isArray(environments)) return;
const { decryptStringSafe } = require('../../utils/encryption');
for (const env of environments) {
if (!Array.isArray(env.variables)) continue;
env.variables.forEach((variable, i) => {
const key = variable.name || `index:${i}`;
variable.uid = uidForSeed(`${env.uid}#var#${key}`);
});
if (!envHasSecrets(env)) continue;
try {
const envSecrets = getEnvSecretsStore().getEnvSecrets(collectionPath, env);
for (const secret of envSecrets || []) {
const variable = env.variables.find((v) => v.name === secret.name);
if (variable && secret.value) {
const decrypted = decryptStringSafe(secret.value);
variable.value = decrypted.value;
}
}
} catch (err) {
console.error('[mount] env secret hydration failed', err);
}
}
};
const sendTree = async (collectionUid, collectionPath, tree, emit) => {
if (tree.brunoConfig) {
try {
const { transformBrunoConfigAfterRead } = require('../../utils/transformBrunoConfig');
const { setBrunoConfig } = require('../../store/bruno-config');
const transformed = await transformBrunoConfigAfterRead(tree.brunoConfig, collectionPath);
tree.brunoConfig = transformed;
setBrunoConfig(collectionUid, transformed);
emit.config(transformed);
} catch (err) {
console.error(`[mount:${collectionUid}] brunoConfig transform failed:`, err);
}
}
hydrateEnvironments(collectionPath, tree.environments);
emit.tree(tree);
};
const ensureTransientDirectory = () => {
const base = path.join(require('electron').app.getPath('userData'), 'tmp', 'transient');
if (!fs.existsSync(base)) fs.mkdirSync(base, { recursive: true });
return fs.mkdtempSync(path.join(base, 'bruno-'));
};
class MountManager {
#index = null;
#mounts = new Map();
async mount({ win, collectionPath, collectionUid, brunoConfig, emit }) {
collectionPath = path.resolve(collectionPath);
if (this.#mounts.has(collectionUid)) {
// renderer reload — pull fresh state from cache and re-emit
const existing = this.#mounts.get(collectionUid);
existing.win = win;
existing.emit = emit;
existing.brunoConfig = brunoConfig || existing.brunoConfig;
existing.state = this.#getIndex().entries(existing.collectionPath);
await this.#emitTree(collectionUid, existing);
return existing.tempDirectoryPath;
}
const tempDirectoryPath = ensureTransientDirectory();
fs.writeFileSync(path.join(tempDirectoryPath, 'metadata.json'), JSON.stringify({ collectionPath }));
const entry = {
state: new Map(),
collectionPath,
tempDirectoryPath,
brunoConfig,
win,
emit
};
this.#mounts.set(collectionUid, entry);
entry.emit.loading(true);
try {
entry.state = this.#getIndex().entries(collectionPath);
await this.#reconcile(entry);
await this.#emitTree(collectionUid, entry);
// skip the startup walk (already done) and stage live edits into the cache
const collectionWatcher = require('../../app/collection-watcher');
collectionWatcher.addWatcher(entry.win, collectionPath, collectionUid, brunoConfig, false, false, {
ignoreInitial: true,
fileIndex: this.#getIndex()
});
collectionWatcher.addTempDirectoryWatcher(entry.win, tempDirectoryPath, collectionUid, collectionPath);
} finally {
entry.emit.loading(false);
}
return tempDirectoryPath;
}
async unmount(collectionUid) {
const entry = this.#mounts.get(collectionUid);
if (!entry) return;
this.#mounts.delete(collectionUid);
const collectionWatcher = require('../../app/collection-watcher');
try {
collectionWatcher.removeWatcher(entry.collectionPath, entry.win, collectionUid);
} catch (_) {}
}
async shutdown() {
await Promise.all(
Array.from(this.#mounts.keys()).map((uid) => this.unmount(uid).catch(() => {}))
);
await destroyPool().catch(() => {});
if (this.#index) {
this.#index.close();
this.#index = null;
}
}
getCacheSize() {
try {
return fs.statSync(this.#getIndex().dbPath).size;
} catch (err) {
if (err && err.code === 'ENOENT') return 0;
throw err;
}
}
clearCache() {
this.#getIndex().clear();
}
async #reconcile(entry) {
const denylist = entry.brunoConfig?.ignore || [];
const { added, updated, removed } = await this.#getIndex().status(entry.collectionPath, { denylist });
const toParse = [];
for (const e of [...added, ...updated]) {
const cls = defaultClassify(e.relativePath);
if (!cls) continue;
toParse.push({ relativePath: e.relativePath, format: cls.format, type: cls.type });
}
const parsed = new Map();
if (toParse.length > 0) {
const pool = getPool();
await Promise.allSettled(
toParse.map(async (e) => {
try {
const result = await pool.run(JobType.ParseFile, {
collectionPath: entry.collectionPath,
relativePath: e.relativePath,
format: e.format,
type: e.type
});
parsed.set(e.relativePath, result);
} catch (err) {
parsed.set(e.relativePath, {
relativePath: e.relativePath,
error: { message: err.message, stack: err.stack }
});
}
})
);
}
this.#getIndex().transaction(() => {
for (const e of toParse) {
const result = parsed.get(e.relativePath);
if (!result) continue;
if (result.error) {
entry.state.set(e.relativePath, { error: result.error });
continue;
}
entry.state.set(e.relativePath, { data: result.data });
this.#getIndex().stage(entry.collectionPath, {
op: 'add',
relativePath: e.relativePath,
mtime: result.mtime,
hash: result.hash,
data: result.data
});
}
for (const e of removed) {
entry.state.delete(e.relativePath);
this.#getIndex().stage(entry.collectionPath, { op: 'remove', relativePath: e.relativePath });
}
});
}
async #emitTree(collectionUid, entry) {
const { getRequestUid } = require('../../cache/requestUids');
const tree = buildTree(entry.collectionPath, entry.state, { uidFor: getRequestUid });
await sendTree(collectionUid, entry.collectionPath, tree, entry.emit);
}
#getIndex() {
if (!this.#index) this.#index = new FileIndex({});
return this.#index;
}
}
module.exports = { MountManager };

View File

@@ -0,0 +1,237 @@
const path = require('node:path');
const {
posixifyPath,
idForAbsolutePath,
uidForSeed,
defaultClassify
} = require('../../utils/mount');
const REQUEST_EXT_RE = /\.(bru|yml|yaml)$/i;
const stripExt = (basename) => basename.replace(REQUEST_EXT_RE, '');
const isSeqValid = (seq) => Number.isFinite(seq) && Number.isInteger(seq) && seq > 0;
// lists that get deterministic uids; section tag keeps reorders within one list local
const REQUEST_UID_PATHS = [
['request.params', 'params'],
['request.headers', 'headers'],
['request.vars.req', 'vars.req'],
['request.vars.res', 'vars.res'],
['request.assertions', 'assertions'],
['request.body.formUrlEncoded', 'body.formUrlEncoded'],
['request.body.multipartForm', 'body.multipartForm'],
['request.body.file', 'body.file']
];
const EXAMPLE_UID_PATHS = [
['request.params', 'params'],
['request.headers', 'headers'],
['request.body.formUrlEncoded', 'body.formUrlEncoded'],
['request.body.multipartForm', 'body.multipartForm'],
['request.body.file', 'body.file'],
['response.headers', 'response.headers']
];
const hydrateListAt = (root, dotPath, seed, section) => {
let cur = root;
for (const p of dotPath.split('.')) {
if (!cur || typeof cur !== 'object') return;
cur = cur[p];
}
if (!Array.isArray(cur)) return;
cur.forEach((item, i) => {
item.uid = uidForSeed(`${seed}#${section}#${i}`);
});
};
const hydrateRequestUuids = (data, parentUid, absolutePath) => {
if (!data || typeof data !== 'object') return;
const seed = posixifyPath(absolutePath);
for (const [p, sec] of REQUEST_UID_PATHS) hydrateListAt(data, p, seed, sec);
if (!Array.isArray(data.examples)) return;
data.examples.forEach((example, i) => {
const exSeed = `${seed}#example#${i}`;
example.uid = uidForSeed(exSeed);
if (parentUid) example.itemUid = parentUid;
for (const [p, sec] of EXAMPLE_UID_PATHS) hydrateListAt(example, p, exSeed, sec);
});
};
// alpha by name, then any folder with a valid seq pins to that position (v1 quirk)
const sortByNameThenSequence = (items) => {
const sorted = [...items].sort((a, b) =>
a.name && b.name ? a.name.localeCompare(b.name) : 0
);
const withoutSeq = sorted.filter((f) => !isSeqValid(f.seq));
const withSeq = sorted.filter((f) => isSeqValid(f.seq)).sort((a, b) => a.seq - b.seq);
withSeq.forEach((item) => {
const existing = withoutSeq[item.seq - 1];
const collides = Array.isArray(existing)
? existing[0]?.seq === item.seq
: existing?.seq === item.seq;
if (collides) {
const group = Array.isArray(existing) ? [...existing, item] : [existing, item];
withoutSeq.splice(item.seq - 1, 1, group);
} else {
withoutSeq.splice(item.seq - 1, 0, item);
}
});
return withoutSeq.flat();
};
const sortLevel = (items) => {
const folders = items.filter((i) => i.type === 'folder');
const requests = items.filter((i) => i.type !== 'folder');
const sortedFolders = sortByNameThenSequence(folders);
for (const f of sortedFolders) f.items = sortLevel(f.items);
const sortedRequests = [...requests].sort((a, b) => (a.seq ?? 1) - (b.seq ?? 1));
return [...sortedFolders, ...sortedRequests];
};
const ensureFolder = (collectionPath, items, segments, uidFor) => {
let cursor = items;
let acc = '';
let last = null;
for (const seg of segments) {
acc = acc ? path.join(acc, seg) : seg;
const absolutePath = path.join(collectionPath, acc);
let folder = cursor.find((i) => i.type === 'folder' && i.filename === seg);
if (!folder) {
folder = {
uid: uidFor(absolutePath),
name: seg,
filename: seg,
pathname: absolutePath,
type: 'folder',
collapsed: true,
items: []
};
cursor.push(folder);
}
cursor = folder.items;
last = folder;
}
return { cursor, folder: last };
};
const buildRequestNode = (absolutePath, basename, entry, uidOverrides, uidFor) => {
const uid = uidOverrides?.get(absolutePath) || uidFor(absolutePath);
const data = entry.data || {};
hydrateRequestUuids(data, uid, absolutePath);
return {
uid,
name: data.name || stripExt(basename),
type: data.type || 'http-request',
seq: data.seq,
tags: data.tags,
request: data.request,
settings: data.settings,
examples: data.examples,
filename: basename,
pathname: absolutePath,
draft: null,
partial: false,
loading: false,
...(entry.error ? { error: entry.error, partial: true } : {})
};
};
const buildEnvironmentNode = (collectionPath, relativePath, entry, uidFor) => {
const basename = path.basename(relativePath);
const absolutePath = path.join(collectionPath, relativePath);
const data = entry.data || {};
return {
uid: uidFor(absolutePath),
name: stripExt(basename),
variables: data.variables || [],
...(entry.error ? { error: entry.error } : {})
};
};
const buildTree = (collectionPath, parserResults, options = {}) => {
const uidOverrides = options.uidOverrides;
const uidFor = options.uidFor || idForAbsolutePath;
const transientEntries = options.transientEntries || [];
const tree = {
pathname: collectionPath,
brunoConfig: null,
root: null,
items: [],
environments: []
};
const folderRoots = new Map();
const requests = [];
for (const [relativePath, entry] of parserResults) {
const cls = defaultClassify(relativePath);
if (!cls) continue;
if (cls.type === 'config') {
tree.brunoConfig = entry.error ? null : entry.data;
} else if (cls.type === 'collection') {
if (!entry.error) {
const data = entry.data;
if (data && typeof data === 'object' && 'collectionRoot' in data && 'brunoConfig' in data) {
hydrateRequestUuids(data.collectionRoot, null, collectionPath);
tree.root = data.collectionRoot;
tree.brunoConfig = data.brunoConfig;
} else {
hydrateRequestUuids(data, null, collectionPath);
tree.root = data;
}
}
} else if (cls.type === 'folder') {
folderRoots.set(path.dirname(relativePath), entry);
} else if (cls.type === 'environment') {
tree.environments.push(buildEnvironmentNode(collectionPath, relativePath, entry, uidFor));
} else {
requests.push({ relativePath, entry });
}
}
for (const { relativePath, entry } of requests) {
const segments = path.dirname(relativePath).split(path.sep).filter((s) => s && s !== '.');
const { cursor } = ensureFolder(collectionPath, tree.items, segments, uidFor);
cursor.push(buildRequestNode(
path.join(collectionPath, relativePath),
path.basename(relativePath),
entry,
uidOverrides,
uidFor
));
}
for (const [dirRel, entry] of folderRoots) {
const segments = dirRel.split(path.sep).filter((s) => s && s !== '.');
const { folder } = ensureFolder(collectionPath, tree.items, segments, uidFor);
if (!folder) continue;
const meta = entry.data?.meta || {};
if (meta.name) folder.name = meta.name;
if (isSeqValid(meta.seq)) folder.seq = meta.seq;
if (!entry.error) {
hydrateRequestUuids(entry.data, folder.uid, folder.pathname);
folder.root = entry.data;
}
}
for (const t of transientEntries) {
if (!t || !t.absolutePath) continue;
const node = buildRequestNode(
t.absolutePath,
path.basename(t.absolutePath),
t,
uidOverrides,
uidFor
);
node.isTransient = true;
tree.items.push(node);
}
tree.items = sortLevel(tree.items);
return tree;
};
module.exports = { buildTree };

View File

@@ -0,0 +1,45 @@
const workerpool = require('workerpool');
const os = require('node:os');
const path = require('node:path');
const JobType = Object.freeze({
ParseFile: 'parse-file'
});
const WORKER_FILE = path.join(__dirname, 'worker.js');
class Pool {
#pool;
constructor({ size } = {}) {
const workers = Math.max(1, size ?? os.availableParallelism());
this.#pool = workerpool.pool(WORKER_FILE, {
maxWorkers: workers,
workerType: 'thread'
});
}
run(type, args) {
return this.#pool.exec(type, [args]);
}
async destroy() {
await this.#pool.terminate();
}
}
let shared = null;
const getPool = (options) => {
if (!shared) shared = new Pool(options);
return shared;
};
const destroyPool = async () => {
if (!shared) return;
const pool = shared;
shared = null;
await pool.destroy();
};
module.exports = { Pool, getPool, destroyPool, JobType };

View File

@@ -0,0 +1,34 @@
const fs = require('node:fs');
const path = require('node:path');
const crypto = require('node:crypto');
const filestore = require('@usebruno/filestore');
const sha256 = (buf) => crypto.createHash('sha256').update(buf).digest('hex');
const parseContent = (content, format, type) => {
if (type === 'config' || format === 'json') return JSON.parse(content);
const options = { format };
switch (type) {
case 'request':
return filestore.parseRequest(content, options);
case 'collection':
return filestore.parseCollection(content, options);
case 'folder':
return filestore.parseFolder(content, options);
case 'environment':
return filestore.parseEnvironment(content, options);
default:
throw new Error(`Unknown type: ${type}`);
}
};
const parseFile = ({ collectionPath, relativePath, format, type }) => {
const absolutePath = path.join(collectionPath, relativePath);
const buf = fs.readFileSync(absolutePath);
const stat = fs.statSync(absolutePath, { bigint: true });
const content = buf.toString('utf8');
const data = parseContent(content, format, type);
return { relativePath, mtime: stat.mtimeNs, hash: sha256(buf), data, format, type };
};
module.exports = parseFile;

View File

@@ -0,0 +1,7 @@
const workerpool = require('workerpool');
const parseFile = require('./jobs/parse-file');
const { JobType } = require('./index');
workerpool.worker({
[JobType.ParseFile]: parseFile
});

View File

@@ -0,0 +1,129 @@
const { DatabaseSync } = require('node:sqlite');
// Lazy SQLite connection. The db is opened on first use, pragmas are applied,
// pending migrations are run (tracked via PRAGMA user_version), and prepared
// statements are cached by their SQL text.
class Database {
#db = null;
#path;
#migrations;
#pragmas;
#readBigInts;
#statements = new Map();
constructor({ path, migrations = [], pragmas, readBigInts = false }) {
this.#path = path;
this.#migrations = migrations;
this.#pragmas = pragmas;
this.#readBigInts = readBigInts;
this.#validateMigrations();
}
get path() {
return typeof this.#path === 'function' ? this.#path() : this.#path;
}
get schemaVersion() {
const row = this.#connect().prepare('PRAGMA user_version').get();
return row.user_version;
}
setPath(p) {
this.close();
this.#path = p;
}
prepare(sql) {
const db = this.#connect();
let stmt = this.#statements.get(sql);
if (!stmt) {
stmt = db.prepare(sql);
if (this.#readBigInts) stmt.setReadBigInts(true);
this.#statements.set(sql, stmt);
}
return stmt;
}
run(sql, ...params) {
return this.prepare(sql).run(...params);
}
get(sql, ...params) {
return this.prepare(sql).get(...params);
}
all(sql, ...params) {
return this.prepare(sql).all(...params);
}
exec(sql) {
this.#connect().exec(sql);
}
transaction(fn) {
const db = this.#connect();
db.exec('BEGIN');
try {
const result = fn();
db.exec('COMMIT');
return result;
} catch (err) {
db.exec('ROLLBACK');
throw err;
}
}
close() {
if (!this.#db) return;
this.#statements.clear();
this.#db.close();
this.#db = null;
}
#connect() {
if (this.#db) return this.#db;
const db = new DatabaseSync(this.path);
if (this.#pragmas) {
for (const [key, value] of Object.entries(this.#pragmas)) {
db.exec(`PRAGMA ${key} = ${value};`);
}
}
this.#migrate(db);
this.#db = db;
return db;
}
#migrate(db) {
if (!this.#migrations.length) return;
const sorted = [...this.#migrations].sort((a, b) => a.version - b.version);
let current = db.prepare('PRAGMA user_version').get().user_version;
for (const migration of sorted) {
if (migration.version <= current) continue;
db.exec('BEGIN');
try {
db.exec(migration.up);
db.exec(`PRAGMA user_version = ${migration.version}`);
db.exec('COMMIT');
} catch (err) {
db.exec('ROLLBACK');
throw err;
}
current = migration.version;
}
}
#validateMigrations() {
const seen = new Set();
for (const { version } of this.#migrations) {
if (!Number.isInteger(version) || version < 1) {
throw new Error(`Migration version must be a positive integer, got ${version}`);
}
if (seen.has(version)) {
throw new Error(`Duplicate migration version ${version}`);
}
seen.add(version);
}
}
}
module.exports = { Database };

View File

@@ -68,6 +68,9 @@ const defaultPreferences = {
cache: {
sslSession: {
enabled: false
},
file: {
enabled: false
}
},
ai: {
@@ -145,6 +148,9 @@ const preferencesSchema = Yup.object().shape({
cache: Yup.object({
sslSession: Yup.object({
enabled: Yup.boolean()
}),
file: Yup.object({
enabled: Yup.boolean()
})
}).optional(),
ai: Yup.object({
@@ -352,6 +358,9 @@ const preferencesUtil = {
isSslSessionCachingEnabled: () => {
return get(getPreferences(), 'cache.sslSession.enabled', false);
},
isFileCacheEnabled: () => {
return get(getPreferences(), 'cache.file.enabled', false);
},
hasLaunchedBefore: () => {
return get(getPreferences(), 'onboarding.hasLaunchedBefore', false);
},

View File

@@ -0,0 +1,92 @@
const fs = require('node:fs');
const path = require('node:path');
const crypto = require('node:crypto');
const { posixifyPath } = require('./filesystem');
const DENY_DIRS = new Set(['node_modules', '.git', '.svn', '.hg', '.bruno']);
const DEFAULT_DENYLIST = ['**/.DS_Store', '**/Thumbs.db'];
const sha256 = (input) => crypto.createHash('sha256').update(input).digest('hex');
const hashFile = (absPath) => sha256(fs.readFileSync(absPath));
const hashFileAsync = async (absPath) => sha256(await fs.promises.readFile(absPath));
const normalize = (p) => path.resolve(p);
const idForAbsolutePath = (absolutePath) => sha256(posixifyPath(absolutePath)).slice(0, 21);
const uidForSeed = (seed) => sha256(seed).slice(0, 21);
const resolveDenylist = (patterns) => [...DEFAULT_DENYLIST, ...(patterns || [])];
const isDenied = (relativePathPosix, patterns) => {
for (const pattern of patterns) {
if (path.matchesGlob(relativePathPosix, pattern)) return true;
}
return false;
};
const walk = (root, denylist) => {
const out = [];
const visit = (absDir, relDir) => {
const entries = fs.readdirSync(absDir, { withFileTypes: true });
for (const entry of entries) {
const childAbs = path.join(absDir, entry.name);
const childRel = relDir ? path.join(relDir, entry.name) : entry.name;
if (entry.isDirectory()) {
if (DENY_DIRS.has(entry.name)) continue;
visit(childAbs, childRel);
} else if (entry.isFile()) {
if (isDenied(posixifyPath(childRel), denylist)) continue;
out.push({ relativePath: childRel, absolutePath: childAbs });
}
}
};
visit(root, '');
return out;
};
const COLLECTION_ROOT_BASENAMES = new Set(['collection.bru', 'collection.yml', 'opencollection.yml']);
const FOLDER_ROOT_BASENAMES = new Set(['folder.bru', 'folder.yml']);
const BRUNO_CONFIG_BASENAME = 'bruno.json';
const ENVIRONMENTS_DIR = 'environments';
const defaultClassify = (relativePath) => {
const basename = path.basename(relativePath);
const dirname = path.dirname(relativePath);
const segments = dirname === '.' || dirname === '' ? [] : dirname.split(path.sep).filter(Boolean);
if (basename === BRUNO_CONFIG_BASENAME && segments.length === 0) {
return { format: 'json', type: 'config' };
}
const ext = path.extname(basename).slice(1).toLowerCase();
let format;
if (ext === 'bru') format = 'bru';
else if (ext === 'yml' || ext === 'yaml') format = 'yml';
else return null;
if (COLLECTION_ROOT_BASENAMES.has(basename) && segments.length === 0) {
return { format, type: 'collection' };
}
if (FOLDER_ROOT_BASENAMES.has(basename)) {
return { format, type: 'folder' };
}
if (segments[0] === ENVIRONMENTS_DIR && segments.length === 1) {
return { format, type: 'environment' };
}
return { format, type: 'request' };
};
module.exports = {
COLLECTION_ROOT_BASENAMES,
FOLDER_ROOT_BASENAMES,
BRUNO_CONFIG_BASENAME,
ENVIRONMENTS_DIR,
hashFile,
hashFileAsync,
normalize,
posixifyPath,
idForAbsolutePath,
uidForSeed,
resolveDenylist,
isDenied,
walk,
defaultClassify
};

View File

@@ -78,13 +78,14 @@ test.describe('Preferences Tab Switch Persistence', () => {
await page.getByRole('tab', { name: 'Cache' }).click();
await page.waitForTimeout(300);
// Get the initial state of SSL session caching checkbox
const sslSessionCheckbox = page.locator('#sslSession\\.enabled');
await sslSessionCheckbox.waitFor({ state: 'visible' });
// Get the initial state of SSL session caching toggle
const sslSessionToggle = page.getByTestId('sslSession.enabled');
const sslSessionCheckbox = sslSessionToggle.locator('input[type="checkbox"]');
await sslSessionToggle.waitFor({ state: 'visible' });
const initialChecked = await sslSessionCheckbox.isChecked();
// Toggle the checkbox
await sslSessionCheckbox.click();
await sslSessionToggle.click();
// Immediately switch to another tab
await page.getByRole('tab', { name: 'Themes' }).click();
@@ -92,14 +93,14 @@ test.describe('Preferences Tab Switch Persistence', () => {
// Switch back to Cache tab
await page.getByRole('tab', { name: 'Cache' }).click();
await sslSessionCheckbox.waitFor({ state: 'visible' });
await sslSessionToggle.waitFor({ state: 'visible' });
// Verify the setting was persisted
const newChecked = await sslSessionCheckbox.isChecked();
expect(newChecked).toBe(!initialChecked);
// Restore original state
await sslSessionCheckbox.click();
await sslSessionToggle.click();
await page.waitForTimeout(600);
});
@@ -154,13 +155,14 @@ test.describe('Preferences Tab Switch Persistence', () => {
await page.getByRole('tab', { name: 'Cache' }).click();
await page.waitForTimeout(300);
// Get the initial state of SSL session caching checkbox
const sslSessionCheckbox = page.locator('#sslSession\\.enabled');
await sslSessionCheckbox.waitFor({ state: 'visible' });
// Get the initial state of SSL session caching toggle
const sslSessionToggle = page.getByTestId('sslSession.enabled');
const sslSessionCheckbox = sslSessionToggle.locator('input[type="checkbox"]');
await sslSessionToggle.waitFor({ state: 'visible' });
const initialCacheState = await sslSessionCheckbox.isChecked();
// Toggle the checkbox
await sslSessionCheckbox.click();
await sslSessionToggle.click();
// Close preferences tab immediately
const preferencesTab = page.locator('.request-tab').filter({ hasText: 'Preferences' });
@@ -178,13 +180,13 @@ test.describe('Preferences Tab Switch Persistence', () => {
// Navigate to Cache tab
await page.getByRole('tab', { name: 'Cache' }).click();
await page.waitForTimeout(300);
await sslSessionCheckbox.waitFor({ state: 'visible' });
await sslSessionToggle.waitFor({ state: 'visible' });
// Verify the setting was persisted
expect(await sslSessionCheckbox.isChecked()).toBe(!initialCacheState);
// Restore original state
await sslSessionCheckbox.click();
await sslSessionToggle.click();
await page.waitForTimeout(600);
});
});