From 73b626ff126d4817abcc2ff94ad037692904a51d Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Thu, 2 Oct 2025 20:24:57 +0200 Subject: [PATCH 1/6] Add basic storage functions --- package-lock.json | 1041 +++----------------------- package.json | 1 + packages/js-toolkit/utils/index.ts | 1 + packages/js-toolkit/utils/storage.ts | 191 +++++ 4 files changed, 312 insertions(+), 922 deletions(-) create mode 100644 packages/js-toolkit/utils/storage.ts diff --git a/package-lock.json b/package-lock.json index 616059878..3a93ae4a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "packages/*" ], "dependencies": { + "alien-signals": "3.0.0", "esbuild": "^0.25.9", "fast-glob": "^3.3.3" }, @@ -2650,96 +2651,6 @@ "node": ">= 8" } }, - "node_modules/@oxlint-tsgolint/darwin-arm64": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-arm64/-/darwin-arm64-0.0.4.tgz", - "integrity": "sha512-qL0zqIYdYrXl6ghTIHnhJkvyYy1eKz0P8YIEp59MjY3/zNiyk/gtyp8LkwZdqb9ezbcX9UDQhSuSO1wURJsq8g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true - }, - "node_modules/@oxlint-tsgolint/darwin-x64": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-x64/-/darwin-x64-0.0.4.tgz", - "integrity": "sha512-c3nSjqmDSKzemChAEUv/zy2e9cwgkkO/7rz4Y447+8pSbeZNHi3RrNpVHdrKL/Qep4pt6nFZE+6PoczZxHNQjg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true - }, - "node_modules/@oxlint-tsgolint/linux-arm64": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/linux-arm64/-/linux-arm64-0.0.4.tgz", - "integrity": "sha512-P2BA54c/Ej5AGkChH1/7zMd6PwZfa+jnw8juB/JWops+BX+lbhbbBHz0cYduDBgWYjRo4e3OVJOTskqcpuMfNw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@oxlint-tsgolint/linux-x64": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/linux-x64/-/linux-x64-0.0.4.tgz", - "integrity": "sha512-hbgLpnDNicPrbHOAQ9nNfLOSrUrdWANP/umR7P/cwCc1sv66eEs7bm4G3mrhRU8aXFBJmbhdNqiDSUkYYvHWJQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@oxlint-tsgolint/win32-arm64": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/win32-arm64/-/win32-arm64-0.0.4.tgz", - "integrity": "sha512-ozKEppmwZhC5LMedClBEat6cXgBGUvxGOgsKK2ZZNE6zSScX7QbvJAOt3nWMGs8GQshHy/6ndMB33+uRloglQA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true - }, - "node_modules/@oxlint-tsgolint/win32-x64": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/win32-x64/-/win32-x64-0.0.4.tgz", - "integrity": "sha512-gLfx+qogW21QcaRKFg6ARgra7tSPqyn+Ems3FgTUyxV4OpJYn7KsQroygxOWElqv6JUobtvHBrxdB6YhlvERbQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true - }, "node_modules/@oxlint/darwin-arm64": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/@oxlint/darwin-arm64/-/darwin-arm64-1.13.0.tgz", @@ -2852,68 +2763,73 @@ "win32" ] }, - "node_modules/@parcel/watcher": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", - "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", - "hasInstallScript": true, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "license": "MIT", "optional": true, - "peer": true, - "dependencies": { - "detect-libc": "^1.0.3", - "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^7.0.0" - }, "engines": { - "node": ">= 10.0.0" + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.1", - "@parcel/watcher-darwin-arm64": "2.5.1", - "@parcel/watcher-darwin-x64": "2.5.1", - "@parcel/watcher-freebsd-x64": "2.5.1", - "@parcel/watcher-linux-arm-glibc": "2.5.1", - "@parcel/watcher-linux-arm-musl": "2.5.1", - "@parcel/watcher-linux-arm64-glibc": "2.5.1", - "@parcel/watcher-linux-arm64-musl": "2.5.1", - "@parcel/watcher-linux-x64-glibc": "2.5.1", - "@parcel/watcher-linux-x64-musl": "2.5.1", - "@parcel/watcher-win32-arm64": "2.5.1", - "@parcel/watcher-win32-ia32": "2.5.1", - "@parcel/watcher-win32-x64": "2.5.1" - } - }, - "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", - "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.29", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.29.tgz", + "integrity": "sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.49.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.49.0.tgz", + "integrity": "sha512-rlKIeL854Ed0e09QGYFlmDNbka6I3EQFw7iZuugQjMb11KMpJCLPFL4ZPbMfaEhLADEL1yx0oujGkBQ7+qW3eA==", "cpu": [ - "arm64" + "arm" ], "license": "MIT", "optional": true, "os": [ "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.49.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.49.0.tgz", + "integrity": "sha512-cqPpZdKUSQYRtLLr6R4X3sD4jCBO1zUmeo3qrWBCqYIeH8Q3KRL4F3V7XJ2Rm8/RJOQBZuqzQGWPjjvFUcYa/w==", + "cpu": [ + "arm64" ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } + "license": "MIT", + "optional": true, + "os": [ + "android" + ] }, - "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", - "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.49.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.49.0.tgz", + "integrity": "sha512-99kMMSMQT7got6iYX3yyIiJfFndpojBmkHfTc1rIje8VbjhmqBXE+nb7ZZP3A5skLyujvT0eIUCUsxAe6NjWbw==", "cpu": [ "arm64" ], @@ -2921,20 +2837,12 @@ "optional": true, "os": [ "darwin" - ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } + ] }, - "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", - "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.49.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.49.0.tgz", + "integrity": "sha512-y8cXoD3wdWUDpjOLMKLx6l+NFz3NlkWKcBCBfttUn+VGSfgsQ5o/yDUGtzE9HvsodkP0+16N0P4Ty1VuhtRUGg==", "cpu": [ "x64" ], @@ -2942,20 +2850,25 @@ "optional": true, "os": [ "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.49.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.49.0.tgz", + "integrity": "sha512-3mY5Pr7qv4GS4ZvWoSP8zha8YoiqrU+e0ViPvB549jvliBbdNLrg2ywPGkgLC3cmvN8ya3za+Q2xVyT6z+vZqA==", + "cpu": [ + "arm64" ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] }, - "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", - "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.49.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.49.0.tgz", + "integrity": "sha512-C9KzzOAQU5gU4kG8DTk+tjdKjpWhVWd5uVkinCwwFub2m7cDYLOdtXoMrExfeBmeRy9kBQMkiyJ+HULyF1yj9w==", "cpu": [ "x64" ], @@ -2963,20 +2876,12 @@ "optional": true, "os": [ "freebsd" - ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } + ] }, - "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", - "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.49.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.49.0.tgz", + "integrity": "sha512-OVSQgEZDVLnTbMq5NBs6xkmz3AADByCWI4RdKSFNlDsYXdFtlxS59J+w+LippJe8KcmeSSM3ba+GlsM9+WwC1w==", "cpu": [ "arm" ], @@ -2984,20 +2889,12 @@ "optional": true, "os": [ "linux" - ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } + ] }, - "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", - "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.49.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.49.0.tgz", + "integrity": "sha512-ZnfSFA7fDUHNa4P3VwAcfaBLakCbYaxCk0jUnS3dTou9P95kwoOLAMlT3WmEJDBCSrOEFFV0Y1HXiwfLYJuLlA==", "cpu": [ "arm" ], @@ -3005,20 +2902,12 @@ "optional": true, "os": [ "linux" - ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } + ] }, - "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", - "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.49.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.49.0.tgz", + "integrity": "sha512-Z81u+gfrobVK2iV7GqZCBfEB1y6+I61AH466lNK+xy1jfqFLiQ9Qv716WUM5fxFrYxwC7ziVdZRU9qvGHkYIJg==", "cpu": [ "arm64" ], @@ -3026,20 +2915,12 @@ "optional": true, "os": [ "linux" - ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } + ] }, - "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", - "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.49.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.49.0.tgz", + "integrity": "sha512-zoAwS0KCXSnTp9NH/h9aamBAIve0DXeYpll85shf9NJ0URjSTzzS+Z9evmolN+ICfD3v8skKUPyk2PO0uGdFqg==", "cpu": [ "arm64" ], @@ -3047,307 +2928,27 @@ "optional": true, "os": [ "linux" - ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } + ] }, - "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", - "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.49.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.49.0.tgz", + "integrity": "sha512-2QyUyQQ1ZtwZGiq0nvODL+vLJBtciItC3/5cYN8ncDQcv5avrt2MbKt1XU/vFAJlLta5KujqyHdYtdag4YEjYQ==", "cpu": [ - "x64" + "loong64" ], "license": "MIT", "optional": true, "os": [ "linux" - ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } + ] }, - "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", - "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.49.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.49.0.tgz", + "integrity": "sha512-k9aEmOWt+mrMuD3skjVJSSxHckJp+SiFzFG+v8JLXbc/xi9hv2icSkR3U7uQzqy+/QbbYY7iNB9eDTwrELo14g==", "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", - "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", - "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", - "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@pkgr/core": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", - "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/pkgr" - } - }, - "node_modules/@polka/url": { - "version": "1.0.0-next.29", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", - "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.29", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.29.tgz", - "integrity": "sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.49.0.tgz", - "integrity": "sha512-rlKIeL854Ed0e09QGYFlmDNbka6I3EQFw7iZuugQjMb11KMpJCLPFL4ZPbMfaEhLADEL1yx0oujGkBQ7+qW3eA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.49.0.tgz", - "integrity": "sha512-cqPpZdKUSQYRtLLr6R4X3sD4jCBO1zUmeo3qrWBCqYIeH8Q3KRL4F3V7XJ2Rm8/RJOQBZuqzQGWPjjvFUcYa/w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.49.0.tgz", - "integrity": "sha512-99kMMSMQT7got6iYX3yyIiJfFndpojBmkHfTc1rIje8VbjhmqBXE+nb7ZZP3A5skLyujvT0eIUCUsxAe6NjWbw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.49.0.tgz", - "integrity": "sha512-y8cXoD3wdWUDpjOLMKLx6l+NFz3NlkWKcBCBfttUn+VGSfgsQ5o/yDUGtzE9HvsodkP0+16N0P4Ty1VuhtRUGg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.49.0.tgz", - "integrity": "sha512-3mY5Pr7qv4GS4ZvWoSP8zha8YoiqrU+e0ViPvB549jvliBbdNLrg2ywPGkgLC3cmvN8ya3za+Q2xVyT6z+vZqA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.49.0.tgz", - "integrity": "sha512-C9KzzOAQU5gU4kG8DTk+tjdKjpWhVWd5uVkinCwwFub2m7cDYLOdtXoMrExfeBmeRy9kBQMkiyJ+HULyF1yj9w==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.49.0.tgz", - "integrity": "sha512-OVSQgEZDVLnTbMq5NBs6xkmz3AADByCWI4RdKSFNlDsYXdFtlxS59J+w+LippJe8KcmeSSM3ba+GlsM9+WwC1w==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.49.0.tgz", - "integrity": "sha512-ZnfSFA7fDUHNa4P3VwAcfaBLakCbYaxCk0jUnS3dTou9P95kwoOLAMlT3WmEJDBCSrOEFFV0Y1HXiwfLYJuLlA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.49.0.tgz", - "integrity": "sha512-Z81u+gfrobVK2iV7GqZCBfEB1y6+I61AH466lNK+xy1jfqFLiQ9Qv716WUM5fxFrYxwC7ziVdZRU9qvGHkYIJg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.49.0.tgz", - "integrity": "sha512-zoAwS0KCXSnTp9NH/h9aamBAIve0DXeYpll85shf9NJ0URjSTzzS+Z9evmolN+ICfD3v8skKUPyk2PO0uGdFqg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.49.0.tgz", - "integrity": "sha512-2QyUyQQ1ZtwZGiq0nvODL+vLJBtciItC3/5cYN8ncDQcv5avrt2MbKt1XU/vFAJlLta5KujqyHdYtdag4YEjYQ==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.49.0.tgz", - "integrity": "sha512-k9aEmOWt+mrMuD3skjVJSSxHckJp+SiFzFG+v8JLXbc/xi9hv2icSkR3U7uQzqy+/QbbYY7iNB9eDTwrELo14g==", - "cpu": [ - "ppc64" + "ppc64" ], "license": "MIT", "optional": true, @@ -4920,6 +4521,13 @@ } } }, + "node_modules/@vue/language-core/node_modules/alien-signals": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-2.0.8.tgz", + "integrity": "sha512-844G1VLkk0Pe2SJjY0J8vp8ADI73IM4KliNu2OGlYzWpO28NexEUvjHTcFjFX3VXoiUtwTbHxLNI9ImkcoBqzA==", + "dev": true, + "license": "MIT" + }, "node_modules/@vue/language-core/node_modules/picomatch": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", @@ -5421,10 +5029,9 @@ } }, "node_modules/alien-signals": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-2.0.7.tgz", - "integrity": "sha512-wE7y3jmYeb0+h6mr5BOovuqhFv22O/MV9j5p0ndJsa7z1zJNPGQ4ph5pQk/kTTCWRC3xsA4SmtwmkzQO+7NCNg==", - "dev": true, + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.0.0.tgz", + "integrity": "sha512-JHoRJf18Y6HN4/KZALr3iU+0vW9LKG+8FMThQlbn4+gv8utsLIkwpomjElGPccGeNwh0FI2HN6BLnyFLo6OyLQ==", "license": "MIT" }, "node_modules/ansi-escapes": { @@ -7593,20 +7200,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "bin": { - "detect-libc": "bin/detect-libc.js" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/dev-ip": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dev-ip/-/dev-ip-1.0.1.tgz", @@ -12684,14 +12277,6 @@ "tslib": "^2.0.3" } }, - "node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -12897,26 +12482,6 @@ } } }, - "node_modules/oxlint-tsgolint": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/oxlint-tsgolint/-/oxlint-tsgolint-0.0.4.tgz", - "integrity": "sha512-KFWVP+VU3ymgK/Dtuf6iRkqjo+aN42lS1YThY6JWlNi1GQqm7wtio/kAwssqDhm8kP+CVXbgZAtu1wgsK4XeTg==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "bin": { - "tsgolint": "bin/tsgolint.js" - }, - "optionalDependencies": { - "@oxlint-tsgolint/darwin-arm64": "0.0.4", - "@oxlint-tsgolint/darwin-x64": "0.0.4", - "@oxlint-tsgolint/linux-arm64": "0.0.4", - "@oxlint-tsgolint/linux-x64": "0.0.4", - "@oxlint-tsgolint/win32-arm64": "0.0.4", - "@oxlint-tsgolint/win32-x64": "0.0.4" - } - }, "node_modules/p-limit": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", @@ -16622,28 +16187,6 @@ "dev": true, "license": "MIT" }, - "node_modules/sass": { - "version": "1.91.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.91.0.tgz", - "integrity": "sha512-aFOZHGf+ur+bp1bCHZ+u8otKGh77ZtmFyXDo4tlYvT7PWql41Kwd8wdkPqhhT+h2879IVblcHFglIMofsFd1EA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "chokidar": "^4.0.0", - "immutable": "^5.0.2", - "source-map-js": ">=0.6.2 <2.0.0" - }, - "bin": { - "sass": "sass.js" - }, - "engines": { - "node": ">=14.0.0" - }, - "optionalDependencies": { - "@parcel/watcher": "^2.4.1" - } - }, "node_modules/sass-embedded": { "version": "1.91.0", "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.91.0.tgz", @@ -16687,312 +16230,6 @@ "sass-embedded-win32-x64": "1.91.0" } }, - "node_modules/sass-embedded-all-unknown": { - "version": "1.91.0", - "resolved": "https://registry.npmjs.org/sass-embedded-all-unknown/-/sass-embedded-all-unknown-1.91.0.tgz", - "integrity": "sha512-AXC1oPqDfLnLtcoxM+XwSnbhcQs0TxAiA5JDEstl6+tt6fhFLKxdyl1Hla39SFtxvMfB2QDUYE3Dmx49O59vYg==", - "cpu": [ - "!arm", - "!arm64", - "!riscv64", - "!x64" - ], - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "sass": "1.91.0" - } - }, - "node_modules/sass-embedded-android-arm": { - "version": "1.91.0", - "resolved": "https://registry.npmjs.org/sass-embedded-android-arm/-/sass-embedded-android-arm-1.91.0.tgz", - "integrity": "sha512-DSh1V8TlLIcpklAbn4NINEFs3yD2OzVTbawEXK93IH990upoGNFVNRTstFQ/gcvlbWph3Y3FjAJvo37zUO485A==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/sass-embedded-android-arm64": { - "version": "1.91.0", - "resolved": "https://registry.npmjs.org/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.91.0.tgz", - "integrity": "sha512-I8Eeg2CeVcZIhXcQLNEY6ZBRF0m7jc818/fypwMwvIdbxGWBekTzc3aKHTLhdBpFzGnDIyR4s7oB0/OjIpzD1A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/sass-embedded-android-riscv64": { - "version": "1.91.0", - "resolved": "https://registry.npmjs.org/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.91.0.tgz", - "integrity": "sha512-qmsl1a7IIJL0fCOwzmRB+6nxeJK5m9/W8LReXUrdgyJNH5RyxChDg+wwQPVATFffOuztmWMnlJ5CV2sCLZrXcQ==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/sass-embedded-android-x64": { - "version": "1.91.0", - "resolved": "https://registry.npmjs.org/sass-embedded-android-x64/-/sass-embedded-android-x64-1.91.0.tgz", - "integrity": "sha512-/wN0HBLATOVSeN3Tzg0yxxNTo1IQvOxxxwFv7Ki/1/UCg2AqZPxTpNoZj/mn8tUPtiVogMGbC8qclYMq1aRZsQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/sass-embedded-darwin-arm64": { - "version": "1.91.0", - "resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.91.0.tgz", - "integrity": "sha512-gQ6ScInxAN+BDUXy426BSYLRawkmGYlHpQ9i6iOxorr64dtIb3l6eb9YaBV8lPlroUnugylmwN2B3FU9BuPfhA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/sass-embedded-darwin-x64": { - "version": "1.91.0", - "resolved": "https://registry.npmjs.org/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.91.0.tgz", - "integrity": "sha512-DSvFMtECL2blYVTFMO5fLeNr5bX437Lrz8R47fdo5438TRyOkSgwKTkECkfh3YbnrL86yJIN2QQlmBMF17Z/iw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/sass-embedded-linux-arm": { - "version": "1.91.0", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.91.0.tgz", - "integrity": "sha512-ppAZLp3eZ9oTjYdQDf4nM7EehDpkxq5H1hE8FOrx8LpY7pxn6QF+SRpAbRjdfFChRw0K7vh+IiCnQEMp7uLNAg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/sass-embedded-linux-arm64": { - "version": "1.91.0", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.91.0.tgz", - "integrity": "sha512-OnKCabD7f420ZEC/6YI9WhCVGMZF+ybZ5NbAB9SsG1xlxrKbWQ1s7CIl0w/6RDALtJ+Fjn8+mrxsxqakoAkeuA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/sass-embedded-linux-musl-arm": { - "version": "1.91.0", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.91.0.tgz", - "integrity": "sha512-znEsNC2FurPF9+XwQQ6e/fVoic3e5D3/kMB41t/bE8byJVRdaPhkdsszt3pZUE56nNGYoCuieSXUkk7VvyPHsw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/sass-embedded-linux-musl-arm64": { - "version": "1.91.0", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.91.0.tgz", - "integrity": "sha512-VfbPpID1C5TT7rukob6CKgefx/TsLE+XZieMNd00hvfJ8XhqPr5DGvSMCNpXlwaedzTirbJu357m+n2PJI9TFQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/sass-embedded-linux-musl-riscv64": { - "version": "1.91.0", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.91.0.tgz", - "integrity": "sha512-ZfLGldKEEeZjuljKks835LTq7jDRI3gXsKKXXgZGzN6Yymd4UpBOGWiDQlWsWTvw5UwDU2xfFh0wSXbLGHTjVA==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/sass-embedded-linux-musl-x64": { - "version": "1.91.0", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.91.0.tgz", - "integrity": "sha512-4kSiSGPKFMbLvTRbP/ibyiKheOA3fwsJKWU0SOuekSPmybMdrhNkTm0REp6+nehZRE60kC3lXmEV4a7w8Jrwyg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/sass-embedded-linux-riscv64": { - "version": "1.91.0", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.91.0.tgz", - "integrity": "sha512-Y3Fj94SYYvMX9yo49T78yBgBWXtG3EyYUT5K05XyCYkcdl1mVXJSrEmqmRfe4vQGUCaSe/6s7MmsA9Q+mQez7Q==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/sass-embedded-linux-x64": { - "version": "1.91.0", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.91.0.tgz", - "integrity": "sha512-XwIUaE7pQP/ezS5te80hlyheYiUlo0FolQ0HBtxohpavM+DVX2fjwFm5LOUJHrLAqP+TLBtChfFeLj1Ie4Aenw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/sass-embedded-unknown-all": { - "version": "1.91.0", - "resolved": "https://registry.npmjs.org/sass-embedded-unknown-all/-/sass-embedded-unknown-all-1.91.0.tgz", - "integrity": "sha512-Bj6v7ScQp/HtO91QBy6ood9AArSIN7/RNcT4E7P9QoY3o+e6621Vd28lV81vdepPrt6u6PgJoVKmLNODqB6Q+A==", - "license": "MIT", - "optional": true, - "os": [ - "!android", - "!darwin", - "!linux", - "!win32" - ], - "peer": true, - "dependencies": { - "sass": "1.91.0" - } - }, - "node_modules/sass-embedded-win32-arm64": { - "version": "1.91.0", - "resolved": "https://registry.npmjs.org/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.91.0.tgz", - "integrity": "sha512-yDCwTiPRex03i1yo7LwiAl1YQ21UyfOxPobD7UjI8AE8ZcB0mQ28VVX66lsZ+qm91jfLslNFOFCD4v79xCG9hA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/sass-embedded-win32-x64": { - "version": "1.91.0", - "resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.91.0.tgz", - "integrity": "sha512-wiuMz/cx4vsk6rYCnNyoGE5pd73aDJ/zF3qJDose3ZLT1/vV943doJE5pICnS/v5DrUqzV6a1CNq4fN+xeSgFQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/sass-embedded/node_modules/immutable": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.3.tgz", @@ -17057,46 +16294,6 @@ } } }, - "node_modules/sass/node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/sass/node_modules/immutable": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.3.tgz", - "integrity": "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==", - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/sass/node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/sax": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", diff --git a/package.json b/package.json index a6e4b6844..cba284ff4 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "typescript": "5.9.2" }, "dependencies": { + "alien-signals": "3.0.0", "esbuild": "^0.25.9", "fast-glob": "^3.3.3" } diff --git a/packages/js-toolkit/utils/index.ts b/packages/js-toolkit/utils/index.ts index 787854ae6..b7316fbf2 100644 --- a/packages/js-toolkit/utils/index.ts +++ b/packages/js-toolkit/utils/index.ts @@ -30,3 +30,4 @@ export * from './random.js'; export * from './memo.js'; export * from './loadElement.js'; export * from './cache.js'; +export * from './storage.js'; diff --git a/packages/js-toolkit/utils/storage.ts b/packages/js-toolkit/utils/storage.ts new file mode 100644 index 000000000..967b0fd82 --- /dev/null +++ b/packages/js-toolkit/utils/storage.ts @@ -0,0 +1,191 @@ +import { signal, effect, type Signal } from 'alien-signals'; + +/** + * Storage provider interface + */ +export interface StorageProvider { + get(key: string): T | null; + set(key: string, value: T): void; + remove(key: string): void; + has(key: string): boolean; +} + +/** + * localStorage provider + */ +export const localStorageProvider: StorageProvider = { + get(key: string): string | null { + if (typeof window === 'undefined') return null; + return localStorage.getItem(key); + }, + set(key: string, value: string): void { + if (typeof window === 'undefined') return; + localStorage.setItem(key, value); + }, + remove(key: string): void { + if (typeof window === 'undefined') return; + localStorage.removeItem(key); + }, + has(key: string): boolean { + if (typeof window === 'undefined') return false; + return localStorage.getItem(key) !== null; + }, +}; + +/** + * sessionStorage provider + */ +export const sessionStorageProvider: StorageProvider = { + get(key: string): string | null { + if (typeof window === 'undefined') return null; + return sessionStorage.getItem(key); + }, + set(key: string, value: string): void { + if (typeof window === 'undefined') return; + sessionStorage.setItem(key, value); + }, + remove(key: string): void { + if (typeof window === 'undefined') return; + sessionStorage.removeItem(key); + }, + has(key: string): boolean { + if (typeof window === 'undefined') return false; + return sessionStorage.getItem(key) !== null; + }, +}; + +/** + * URLSearchParams provider + */ +export const urlSearchParamsProvider: StorageProvider = { + get(key: string): string | null { + if (typeof window === 'undefined') return null; + const params = new URLSearchParams(window.location.search); + return params.get(key); + }, + set(key: string, value: string): void { + if (typeof window === 'undefined') return; + const params = new URLSearchParams(window.location.search); + params.set(key, value); + const newUrl = `${window.location.pathname}?${params.toString()}`; + window.history.pushState({}, '', newUrl); + }, + remove(key: string): void { + if (typeof window === 'undefined') return; + const params = new URLSearchParams(window.location.search); + params.delete(key); + const newUrl = params.toString() + ? `${window.location.pathname}?${params.toString()}` + : window.location.pathname; + window.history.pushState({}, '', newUrl); + }, + has(key: string): boolean { + if (typeof window === 'undefined') return false; + const params = new URLSearchParams(window.location.search); + return params.has(key); + }, +}; + +/** + * Storage options + */ +export interface StorageOptions { + key: string; + provider?: StorageProvider; + initialValue?: T; + serializer?: { + serialize: (value: T) => string; + deserialize: (value: string) => T; + }; +} + +/** + * Default JSON serializer + */ +const jsonSerializer = { + serialize: (value: T): string => JSON.stringify(value), + deserialize: (value: string): T => JSON.parse(value), +}; + +/** + * Create a reactive storage utility using alien-signals + */ +export function createStorage(options: StorageOptions): Signal { + const { + key, + provider = localStorageProvider, + initialValue = null as T | null, + serializer = jsonSerializer, + } = options; + + // Get initial value from storage or use provided initial value + const storedValue = provider.get(key); + const parsedValue = storedValue ? serializer.deserialize(storedValue) : initialValue; + + // Create signal with initial value + const storageSignal = signal(parsedValue); + + // Create effect to sync signal changes to storage + effect(() => { + const value = storageSignal(); + if (value === null) { + provider.remove(key); + } else { + provider.set(key, serializer.serialize(value)); + } + }); + + // Listen to storage events (for localStorage/sessionStorage cross-tab sync) + if (typeof window !== 'undefined' && (provider === localStorageProvider || provider === sessionStorageProvider)) { + window.addEventListener('storage', (event) => { + if (event.key === key) { + const newValue = event.newValue ? serializer.deserialize(event.newValue) : null; + storageSignal(newValue); + } + }); + } + + // Listen to popstate events (for URLSearchParams back/forward navigation) + if (typeof window !== 'undefined' && provider === urlSearchParamsProvider) { + window.addEventListener('popstate', () => { + const value = provider.get(key); + const newValue = value ? serializer.deserialize(value) : null; + storageSignal(newValue); + }); + } + + return storageSignal; +} + +/** + * Create localStorage utility + */ +export function useLocalStorage( + key: string, + initialValue?: T, + serializer = jsonSerializer +): Signal { + return createStorage({ key, provider: localStorageProvider, initialValue, serializer }); +} + +/** + * Create sessionStorage utility + */ +export function useSessionStorage( + key: string, + initialValue?: T, + serializer = jsonSerializer +): Signal { + return createStorage({ key, provider: sessionStorageProvider, initialValue, serializer }); +} + +/** + * Create URLSearchParams utility + */ +export function useUrlSearchParams( + key: string, + initialValue?: T, + serializer = jsonSerializer +): Signal { + return createStorage({ key, provider: urlSearchParamsProvider, initialValue, serializer }); +} From a429fc76e0bf9e202984049dba502d9e690547cf Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Thu, 2 Oct 2025 20:25:17 +0200 Subject: [PATCH 2/6] Add onChange and urlSearchParamsInHashProvider --- packages/js-toolkit/utils/storage.ts | 110 +++++++++++++++++++++++++-- 1 file changed, 102 insertions(+), 8 deletions(-) diff --git a/packages/js-toolkit/utils/storage.ts b/packages/js-toolkit/utils/storage.ts index 967b0fd82..719c64745 100644 --- a/packages/js-toolkit/utils/storage.ts +++ b/packages/js-toolkit/utils/storage.ts @@ -86,6 +86,38 @@ export const urlSearchParamsProvider: StorageProvider = { }, }; +function getParamsFromHash(): URLSearchParams | null { + if (typeof window === 'undefined') return null; + const hash = window.location.hash.slice(1); // Remove the '#' + return new URLSearchParams(hash); +} + +/** + * URLSearchParams in hash provider + */ +export const urlSearchParamsInHashProvider: StorageProvider = { + get(key: string): string | null { + return getParamsFromHash()?.get(key); + }, + set(key: string, value: string): void { + const params = getParamsFromHash(); + if (!params) return; + params.set(key, value); + const newHash = params.toString(); + window.location.hash = newHash; + }, + remove(key: string): void { + const params = getParamsFromHash(); + if (!params) return; + params.delete(key); + const newHash = params.toString(); + window.location.hash = newHash; + }, + has(key: string): boolean { + return getParamsFromHash()?.has(key) ?? false; + }, +}; + /** * Storage options */ @@ -97,6 +129,7 @@ export interface StorageOptions { serialize: (value: T) => string; deserialize: (value: string) => T; }; + onChange?: (value: T | null) => void; } /** @@ -116,6 +149,7 @@ export function createStorage(options: StorageOptions): Signal(options: StorageOptions): Signal(parsedValue); - // Create effect to sync signal changes to storage + // Create effect to sync signal changes to storage and trigger callbacks effect(() => { const value = storageSignal(); if (value === null) { @@ -133,10 +167,14 @@ export function createStorage(options: StorageOptions): Signal { if (event.key === key) { const newValue = event.newValue ? serializer.deserialize(event.newValue) : null; @@ -154,6 +192,15 @@ export function createStorage(options: StorageOptions): Signal { + const value = provider.get(key); + const newValue = value ? serializer.deserialize(value) : null; + storageSignal(newValue); + }); + } + return storageSignal; } @@ -163,9 +210,18 @@ export function createStorage(options: StorageOptions): Signal( key: string, initialValue?: T, - serializer = jsonSerializer + options?: { + serializer?: StorageOptions['serializer']; + onChange?: (value: T | null) => void; + }, ): Signal { - return createStorage({ key, provider: localStorageProvider, initialValue, serializer }); + return createStorage({ + key, + provider: localStorageProvider, + initialValue, + serializer: options?.serializer ?? jsonSerializer, + onChange: options?.onChange, + }); } /** @@ -174,9 +230,18 @@ export function useLocalStorage( export function useSessionStorage( key: string, initialValue?: T, - serializer = jsonSerializer + options?: { + serializer?: StorageOptions['serializer']; + onChange?: (value: T | null) => void; + }, ): Signal { - return createStorage({ key, provider: sessionStorageProvider, initialValue, serializer }); + return createStorage({ + key, + provider: sessionStorageProvider, + initialValue, + serializer: options?.serializer ?? jsonSerializer, + onChange: options?.onChange, + }); } /** @@ -185,7 +250,36 @@ export function useSessionStorage( export function useUrlSearchParams( key: string, initialValue?: T, - serializer = jsonSerializer + options?: { + serializer?: StorageOptions['serializer']; + onChange?: (value: T | null) => void; + }, +): Signal { + return createStorage({ + key, + provider: urlSearchParamsProvider, + initialValue, + serializer: options?.serializer ?? jsonSerializer, + onChange: options?.onChange, + }); +} + +/** + * Create URLSearchParams in hash utility + */ +export function useUrlSearchParamsInHash( + key: string, + initialValue?: T, + options?: { + serializer?: StorageOptions['serializer']; + onChange?: (value: T | null) => void; + }, ): Signal { - return createStorage({ key, provider: urlSearchParamsProvider, initialValue, serializer }); + return createStorage({ + key, + provider: urlSearchParamsInHashProvider, + initialValue, + serializer: options?.serializer ?? jsonSerializer, + onChange: options?.onChange, + }); } From 51a70109b5f501707d452c1398b0a29828332495 Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Thu, 2 Oct 2025 20:30:08 +0200 Subject: [PATCH 3/6] Remove dependency to alien-signals --- package-lock.json | 7 -- package.json | 1 - packages/js-toolkit/utils/storage.ts | 123 ++++++++++++++++++--------- 3 files changed, 85 insertions(+), 46 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3a93ae4a3..71d00957b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,6 @@ "packages/*" ], "dependencies": { - "alien-signals": "3.0.0", "esbuild": "^0.25.9", "fast-glob": "^3.3.3" }, @@ -5028,12 +5027,6 @@ "ajv": "^8.8.2" } }, - "node_modules/alien-signals": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.0.0.tgz", - "integrity": "sha512-JHoRJf18Y6HN4/KZALr3iU+0vW9LKG+8FMThQlbn4+gv8utsLIkwpomjElGPccGeNwh0FI2HN6BLnyFLo6OyLQ==", - "license": "MIT" - }, "node_modules/ansi-escapes": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", diff --git a/package.json b/package.json index cba284ff4..a6e4b6844 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,6 @@ "typescript": "5.9.2" }, "dependencies": { - "alien-signals": "3.0.0", "esbuild": "^0.25.9", "fast-glob": "^3.3.3" } diff --git a/packages/js-toolkit/utils/storage.ts b/packages/js-toolkit/utils/storage.ts index 719c64745..016959255 100644 --- a/packages/js-toolkit/utils/storage.ts +++ b/packages/js-toolkit/utils/storage.ts @@ -1,5 +1,3 @@ -import { signal, effect, type Signal } from 'alien-signals'; - /** * Storage provider interface */ @@ -97,7 +95,7 @@ function getParamsFromHash(): URLSearchParams | null { */ export const urlSearchParamsInHashProvider: StorageProvider = { get(key: string): string | null { - return getParamsFromHash()?.get(key); + return getParamsFromHash()?.get(key) ?? null; }, set(key: string, value: string): void { const params = getParamsFromHash(); @@ -118,6 +116,16 @@ export const urlSearchParamsInHashProvider: StorageProvider = { }, }; +/** + * Storage instance interface + */ +export interface StorageInstance { + get value(): T | null; + set value(newValue: T | null); + subscribe(callback: (value: T | null) => void): () => void; + destroy(): void; +} + /** * Storage options */ @@ -141,9 +149,9 @@ const jsonSerializer = { }; /** - * Create a reactive storage utility using alien-signals + * Create a storage utility */ -export function createStorage(options: StorageOptions): Signal { +export function createStorage(options: StorageOptions): StorageInstance { const { key, provider = localStorageProvider, @@ -154,54 +162,93 @@ export function createStorage(options: StorageOptions): Signal(parsedValue); + const listeners = new Set<(value: T | null) => void>(); - // Create effect to sync signal changes to storage and trigger callbacks - effect(() => { - const value = storageSignal(); - if (value === null) { - provider.remove(key); - } else { - provider.set(key, serializer.serialize(value)); + // Add initial onChange callback if provided + if (onChange) { + listeners.add(onChange); + } + + const notify = (value: T | null) => { + listeners.forEach((listener) => listener(value)); + }; + + const storageHandler = (event: StorageEvent) => { + if (event.key === key) { + const newValue = event.newValue ? serializer.deserialize(event.newValue) : null; + currentValue = newValue; + notify(newValue); } - onChange?.(value); - }); + }; + + const popstateHandler = () => { + const value = provider.get(key); + const newValue = value ? serializer.deserialize(value) : null; + currentValue = newValue; + notify(newValue); + }; + + const hashchangeHandler = () => { + const value = provider.get(key); + const newValue = value ? serializer.deserialize(value) : null; + currentValue = newValue; + notify(newValue); + }; // Listen to storage events (for localStorage/sessionStorage cross-tab sync) if ( typeof window !== 'undefined' && (provider === localStorageProvider || provider === sessionStorageProvider) ) { - window.addEventListener('storage', (event) => { - if (event.key === key) { - const newValue = event.newValue ? serializer.deserialize(event.newValue) : null; - storageSignal(newValue); - } - }); + window.addEventListener('storage', storageHandler); } // Listen to popstate events (for URLSearchParams back/forward navigation) if (typeof window !== 'undefined' && provider === urlSearchParamsProvider) { - window.addEventListener('popstate', () => { - const value = provider.get(key); - const newValue = value ? serializer.deserialize(value) : null; - storageSignal(newValue); - }); + window.addEventListener('popstate', popstateHandler); } // Listen to hashchange events (for URLSearchParams in hash navigation) if (typeof window !== 'undefined' && provider === urlSearchParamsInHashProvider) { - window.addEventListener('hashchange', () => { - const value = provider.get(key); - const newValue = value ? serializer.deserialize(value) : null; - storageSignal(newValue); - }); + window.addEventListener('hashchange', hashchangeHandler); } - return storageSignal; + return { + get value() { + return currentValue; + }, + set value(newValue: T | null) { + currentValue = newValue; + if (newValue === null) { + provider.remove(key); + } else { + provider.set(key, serializer.serialize(newValue)); + } + notify(newValue); + }, + subscribe(callback: (value: T | null) => void) { + listeners.add(callback); + return () => { + listeners.delete(callback); + }; + }, + destroy() { + listeners.clear(); + if (typeof window !== 'undefined') { + if (provider === localStorageProvider || provider === sessionStorageProvider) { + window.removeEventListener('storage', storageHandler); + } + if (provider === urlSearchParamsProvider) { + window.removeEventListener('popstate', popstateHandler); + } + if (provider === urlSearchParamsInHashProvider) { + window.removeEventListener('hashchange', hashchangeHandler); + } + } + }, + }; } /** @@ -214,7 +261,7 @@ export function useLocalStorage( serializer?: StorageOptions['serializer']; onChange?: (value: T | null) => void; }, -): Signal { +): StorageInstance { return createStorage({ key, provider: localStorageProvider, @@ -234,7 +281,7 @@ export function useSessionStorage( serializer?: StorageOptions['serializer']; onChange?: (value: T | null) => void; }, -): Signal { +): StorageInstance { return createStorage({ key, provider: sessionStorageProvider, @@ -254,7 +301,7 @@ export function useUrlSearchParams( serializer?: StorageOptions['serializer']; onChange?: (value: T | null) => void; }, -): Signal { +): StorageInstance { return createStorage({ key, provider: urlSearchParamsProvider, @@ -274,7 +321,7 @@ export function useUrlSearchParamsInHash( serializer?: StorageOptions['serializer']; onChange?: (value: T | null) => void; }, -): Signal { +): StorageInstance { return createStorage({ key, provider: urlSearchParamsInHashProvider, From 0495029c4c8fd185d02ee9c2bedab55890c58dcf Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Thu, 2 Oct 2025 20:44:30 +0200 Subject: [PATCH 4/6] Add tests --- packages/tests/utils/storage.spec.ts | 291 +++++++++++++++++++++++++++ 1 file changed, 291 insertions(+) create mode 100644 packages/tests/utils/storage.spec.ts diff --git a/packages/tests/utils/storage.spec.ts b/packages/tests/utils/storage.spec.ts new file mode 100644 index 000000000..e3c4d9eff --- /dev/null +++ b/packages/tests/utils/storage.spec.ts @@ -0,0 +1,291 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { + useLocalStorage, + useSessionStorage, + useUrlSearchParams, + useUrlSearchParamsInHash, + localStorageProvider, + sessionStorageProvider, + urlSearchParamsProvider, + urlSearchParamsInHashProvider, +} from '@studiometa/js-toolkit/utils'; + +describe('Storage utilities', () => { + beforeEach(() => { + localStorage.clear(); + sessionStorage.clear(); + window.history.replaceState({}, '', '/'); + window.location.hash = ''; + }); + + describe('useLocalStorage', () => { + it('should initialize with stored value', () => { + localStorage.setItem('test-key', JSON.stringify('stored-value')); + const storage = useLocalStorage('test-key', 'default'); + expect(storage.value).toBe('stored-value'); + }); + + it('should initialize with default value when no stored value exists', () => { + const storage = useLocalStorage('test-key', 'default'); + expect(storage.value).toBe('default'); + }); + + it('should update storage when value changes', () => { + const storage = useLocalStorage('test-key', 'initial'); + storage.value = 'updated'; + expect(localStorage.getItem('test-key')).toBe(JSON.stringify('updated')); + }); + + it('should remove storage when value is set to null', () => { + const storage = useLocalStorage('test-key', 'initial'); + storage.value = null; + expect(localStorage.getItem('test-key')).toBeNull(); + }); + + it('should work with complex objects', () => { + const storage = useLocalStorage<{ name: string; age: number }>('user', { + name: 'John', + age: 30, + }); + expect(storage.value).toEqual({ name: 'John', age: 30 }); + storage.value = { name: 'Jane', age: 25 }; + expect(JSON.parse(localStorage.getItem('user')!)).toEqual({ name: 'Jane', age: 25 }); + }); + + it('should trigger onChange callback when value changes', () => { + const onChange = vi.fn(); + const storage = useLocalStorage('test-key', 'initial', { onChange }); + storage.value = 'updated'; + expect(onChange).toHaveBeenCalledWith('updated'); + }); + + it('should support subscribe method', () => { + const storage = useLocalStorage('test-key', 'initial'); + const callback = vi.fn(); + const unsubscribe = storage.subscribe(callback); + + storage.value = 'updated'; + expect(callback).toHaveBeenCalledWith('updated'); + + unsubscribe(); + storage.value = 'another'; + expect(callback).toHaveBeenCalledTimes(1); + }); + + it('should clean up event listeners on destroy', () => { + const storage = useLocalStorage('test-key', 'initial'); + const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener'); + storage.destroy(); + expect(removeEventListenerSpy).toHaveBeenCalledWith('storage', expect.any(Function)); + removeEventListenerSpy.mockRestore(); + }); + + it('should use custom serializer', () => { + const serializer = { + serialize: (value: string) => `custom:${value}`, + deserialize: (value: string) => value.replace('custom:', ''), + }; + const storage = useLocalStorage('test-key', 'initial', { serializer }); + storage.value = 'test'; + expect(localStorage.getItem('test-key')).toBe('custom:test'); + }); + }); + + describe('useSessionStorage', () => { + it('should initialize with stored value', () => { + sessionStorage.setItem('test-key', JSON.stringify('stored-value')); + const storage = useSessionStorage('test-key', 'default'); + expect(storage.value).toBe('stored-value'); + }); + + it('should initialize with default value when no stored value exists', () => { + const storage = useSessionStorage('test-key', 'default'); + expect(storage.value).toBe('default'); + }); + + it('should update storage when value changes', () => { + const storage = useSessionStorage('test-key', 'initial'); + storage.value = 'updated'; + expect(sessionStorage.getItem('test-key')).toBe(JSON.stringify('updated')); + }); + + it('should remove storage when value is set to null', () => { + const storage = useSessionStorage('test-key', 'initial'); + storage.value = null; + expect(sessionStorage.getItem('test-key')).toBeNull(); + }); + + it('should trigger onChange callback when value changes', () => { + const onChange = vi.fn(); + const storage = useSessionStorage('test-key', 'initial', { onChange }); + storage.value = 'updated'; + expect(onChange).toHaveBeenCalledWith('updated'); + }); + }); + + describe('useUrlSearchParams', () => { + it('should initialize with default value when no URL param exists', () => { + const storage = useUrlSearchParams('test-key', 'default'); + expect(storage.value).toBe('default'); + }); + + it('should trigger onChange callback when value changes', () => { + const onChange = vi.fn(); + const storage = useUrlSearchParams('test-key', 'initial', { onChange }); + storage.value = 'updated'; + expect(onChange).toHaveBeenCalledWith('updated'); + }); + + it('should call provider set method when value changes', () => { + const setSpy = vi.spyOn(urlSearchParamsProvider, 'set'); + const storage = useUrlSearchParams('test-key', 'initial'); + storage.value = 'updated'; + expect(setSpy).toHaveBeenCalledWith('test-key', JSON.stringify('updated')); + setSpy.mockRestore(); + }); + + it('should call provider remove method when value is null', () => { + const removeSpy = vi.spyOn(urlSearchParamsProvider, 'remove'); + const storage = useUrlSearchParams('test-key', 'initial'); + storage.value = null; + expect(removeSpy).toHaveBeenCalledWith('test-key'); + removeSpy.mockRestore(); + }); + }); + + describe('useUrlSearchParamsInHash', () => { + it('should initialize with hash param value', () => { + window.location.hash = '#test-key=%22hash-value%22'; + const storage = useUrlSearchParamsInHash('test-key', 'default'); + expect(storage.value).toBe('hash-value'); + }); + + it('should initialize with default value when no hash param exists', () => { + const storage = useUrlSearchParamsInHash('test-key', 'default'); + expect(storage.value).toBe('default'); + }); + + it('should update hash when value changes', () => { + const storage = useUrlSearchParamsInHash('test-key', 'initial'); + storage.value = 'updated'; + const params = new URLSearchParams(window.location.hash.slice(1)); + expect(params.get('test-key')).toBe(JSON.stringify('updated')); + }); + + it('should remove param from hash when value is set to null', () => { + const storage = useUrlSearchParamsInHash('test-key', 'value'); + storage.value = null; + const params = new URLSearchParams(window.location.hash.slice(1)); + expect(params.has('test-key')).toBe(false); + }); + + it('should call provider set method when value changes', () => { + const setSpy = vi.spyOn(urlSearchParamsInHashProvider, 'set'); + const storage = useUrlSearchParamsInHash('test-key', 'initial'); + storage.value = 'updated'; + expect(setSpy).toHaveBeenCalledWith('test-key', JSON.stringify('updated')); + setSpy.mockRestore(); + }); + + it('should trigger onChange callback when value changes', () => { + const onChange = vi.fn(); + const storage = useUrlSearchParamsInHash('test-key', 'initial', { onChange }); + storage.value = 'updated'; + expect(onChange).toHaveBeenCalledWith('updated'); + }); + }); + + describe('Storage providers', () => { + it('should handle localStorage provider correctly', () => { + expect(localStorageProvider.get('non-existent')).toBeNull(); + localStorageProvider.set('test', 'value'); + expect(localStorageProvider.get('test')).toBe('value'); + expect(localStorageProvider.has('test')).toBe(true); + localStorageProvider.remove('test'); + expect(localStorageProvider.has('test')).toBe(false); + }); + + it('should handle sessionStorage provider correctly', () => { + expect(sessionStorageProvider.get('non-existent')).toBeNull(); + sessionStorageProvider.set('test', 'value'); + expect(sessionStorageProvider.get('test')).toBe('value'); + expect(sessionStorageProvider.has('test')).toBe(true); + sessionStorageProvider.remove('test'); + expect(sessionStorageProvider.has('test')).toBe(false); + }); + + it('should handle urlSearchParams provider correctly', () => { + window.history.replaceState({}, '', '/'); + expect(urlSearchParamsProvider.get('non-existent')).toBeNull(); + const setSpy = vi.spyOn(window.history, 'pushState'); + urlSearchParamsProvider.set('test', 'value'); + expect(setSpy).toHaveBeenCalled(); + setSpy.mockRestore(); + }); + + it('should handle urlSearchParamsInHash provider correctly', () => { + window.location.hash = ''; + expect(urlSearchParamsInHashProvider.get('non-existent')).toBeNull(); + urlSearchParamsInHashProvider.set('test', 'value'); + expect(urlSearchParamsInHashProvider.get('test')).toBe('value'); + expect(urlSearchParamsInHashProvider.has('test')).toBe(true); + urlSearchParamsInHashProvider.remove('test'); + expect(urlSearchParamsInHashProvider.has('test')).toBe(false); + }); + }); + + describe('Event synchronization', () => { + it('should sync localStorage changes from storage events', () => { + const storage = useLocalStorage('test-key', 'initial'); + const callback = vi.fn(); + storage.subscribe(callback); + + // Simulate storage event from another tab + const event = new StorageEvent('storage', { + key: 'test-key', + newValue: JSON.stringify('from-another-tab'), + storageArea: localStorage, + }); + window.dispatchEvent(event); + + expect(storage.value).toBe('from-another-tab'); + expect(callback).toHaveBeenCalledWith('from-another-tab'); + }); + + it('should sync sessionStorage changes from storage events', () => { + const storage = useSessionStorage('test-key', 'initial'); + const callback = vi.fn(); + storage.subscribe(callback); + + // Simulate storage event from another tab + const event = new StorageEvent('storage', { + key: 'test-key', + newValue: JSON.stringify('from-another-tab'), + storageArea: sessionStorage, + }); + window.dispatchEvent(event); + + expect(storage.value).toBe('from-another-tab'); + expect(callback).toHaveBeenCalledWith('from-another-tab'); + }); + + it('should listen to popstate events', () => { + const addEventListener = vi.spyOn(window, 'addEventListener'); + const storage = useUrlSearchParams('test-key', 'initial'); + expect(addEventListener).toHaveBeenCalledWith('popstate', expect.any(Function)); + addEventListener.mockRestore(); + }); + + it('should sync hash params on hashchange event', () => { + const storage = useUrlSearchParamsInHash('test-key', 'initial'); + const callback = vi.fn(); + storage.subscribe(callback); + + window.location.hash = '#test-key=%22hash-value%22'; + window.dispatchEvent(new HashChangeEvent('hashchange')); + + expect(storage.value).toBe('hash-value'); + expect(callback).toHaveBeenCalledWith('hash-value'); + }); + }); +}); From 01377477a0fe6a723e1262f73192767d1231d78c Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Thu, 2 Oct 2025 21:02:09 +0200 Subject: [PATCH 5/6] Add support for multiple key storage --- packages/js-toolkit/utils/storage.ts | 180 ++++++--------- packages/tests/utils/storage.spec.ts | 316 ++++++++++++++------------- 2 files changed, 228 insertions(+), 268 deletions(-) diff --git a/packages/js-toolkit/utils/storage.ts b/packages/js-toolkit/utils/storage.ts index 016959255..c8c22da35 100644 --- a/packages/js-toolkit/utils/storage.ts +++ b/packages/js-toolkit/utils/storage.ts @@ -117,27 +117,27 @@ export const urlSearchParamsInHashProvider: StorageProvider = { }; /** - * Storage instance interface + * Storage instance interface for multi-key storage */ -export interface StorageInstance { - get value(): T | null; - set value(newValue: T | null); - subscribe(callback: (value: T | null) => void): () => void; +export interface StorageInstance { + get(key: K): T[K] | null; + set(key: K, value: T[K] | null): void; + subscribe( + key: K, + callback: (value: T[K] | null) => void, + ): () => void; destroy(): void; } /** * Storage options */ -export interface StorageOptions { - key: string; +export interface StorageOptions { provider?: StorageProvider; - initialValue?: T; serializer?: { - serialize: (value: T) => string; - deserialize: (value: string) => T; + serialize: (value: any) => string; + deserialize: (value: string) => any; }; - onChange?: (value: T | null) => void; } /** @@ -149,52 +149,36 @@ const jsonSerializer = { }; /** - * Create a storage utility + * Create a multi-key storage instance */ -export function createStorage(options: StorageOptions): StorageInstance { - const { - key, - provider = localStorageProvider, - initialValue = null as T | null, - serializer = jsonSerializer, - onChange, - } = options; - - // Get initial value from storage or use provided initial value - const storedValue = provider.get(key); - let currentValue: T | null = storedValue ? serializer.deserialize(storedValue) : initialValue; - - const listeners = new Set<(value: T | null) => void>(); - - // Add initial onChange callback if provided - if (onChange) { - listeners.add(onChange); - } - - const notify = (value: T | null) => { - listeners.forEach((listener) => listener(value)); - }; +export function createStorage = Record>( + options: StorageOptions = {}, +): StorageInstance { + const { provider = localStorageProvider, serializer = jsonSerializer } = options; + const listeners = new Map void>>(); const storageHandler = (event: StorageEvent) => { - if (event.key === key) { + const key = event.key as keyof T; + if (listeners.has(key)) { const newValue = event.newValue ? serializer.deserialize(event.newValue) : null; - currentValue = newValue; - notify(newValue); + listeners.get(key)?.forEach((callback) => callback(newValue)); } }; const popstateHandler = () => { - const value = provider.get(key); - const newValue = value ? serializer.deserialize(value) : null; - currentValue = newValue; - notify(newValue); + listeners.forEach((callbacks, key) => { + const value = provider.get(key as string); + const newValue = value ? serializer.deserialize(value) : null; + callbacks.forEach((callback) => callback(newValue)); + }); }; const hashchangeHandler = () => { - const value = provider.get(key); - const newValue = value ? serializer.deserialize(value) : null; - currentValue = newValue; - notify(newValue); + listeners.forEach((callbacks, key) => { + const value = provider.get(key as string); + const newValue = value ? serializer.deserialize(value) : null; + callbacks.forEach((callback) => callback(newValue)); + }); }; // Listen to storage events (for localStorage/sessionStorage cross-tab sync) @@ -216,25 +200,37 @@ export function createStorage(options: StorageOptions): StorageIn } return { - get value() { - return currentValue; + get(key: K): T[K] | null { + const storedValue = provider.get(key as string); + return storedValue ? serializer.deserialize(storedValue) : null; }, - set value(newValue: T | null) { - currentValue = newValue; - if (newValue === null) { - provider.remove(key); + + set(key: K, value: T[K] | null): void { + if (value === null) { + provider.remove(key as string); } else { - provider.set(key, serializer.serialize(newValue)); + provider.set(key as string, serializer.serialize(value)); } - notify(newValue); + + // Notify listeners + listeners.get(key)?.forEach((callback) => callback(value)); }, - subscribe(callback: (value: T | null) => void) { - listeners.add(callback); + + subscribe(key: K, callback: (value: T[K] | null) => void): () => void { + if (!listeners.has(key)) { + listeners.set(key, new Set()); + } + listeners.get(key)!.add(callback); + return () => { - listeners.delete(callback); + listeners.get(key)?.delete(callback); + if (listeners.get(key)?.size === 0) { + listeners.delete(key); + } }; }, - destroy() { + + destroy(): void { listeners.clear(); if (typeof window !== 'undefined') { if (provider === localStorageProvider || provider === sessionStorageProvider) { @@ -254,79 +250,35 @@ export function createStorage(options: StorageOptions): StorageIn /** * Create localStorage utility */ -export function useLocalStorage( - key: string, - initialValue?: T, - options?: { - serializer?: StorageOptions['serializer']; - onChange?: (value: T | null) => void; - }, +export function useLocalStorage = Record>( + options?: StorageOptions, ): StorageInstance { - return createStorage({ - key, - provider: localStorageProvider, - initialValue, - serializer: options?.serializer ?? jsonSerializer, - onChange: options?.onChange, - }); + return createStorage({ ...options, provider: localStorageProvider }); } /** * Create sessionStorage utility */ -export function useSessionStorage( - key: string, - initialValue?: T, - options?: { - serializer?: StorageOptions['serializer']; - onChange?: (value: T | null) => void; - }, +export function useSessionStorage = Record>( + options?: StorageOptions, ): StorageInstance { - return createStorage({ - key, - provider: sessionStorageProvider, - initialValue, - serializer: options?.serializer ?? jsonSerializer, - onChange: options?.onChange, - }); + return createStorage({ ...options, provider: sessionStorageProvider }); } /** * Create URLSearchParams utility */ -export function useUrlSearchParams( - key: string, - initialValue?: T, - options?: { - serializer?: StorageOptions['serializer']; - onChange?: (value: T | null) => void; - }, +export function useUrlSearchParams = Record>( + options?: StorageOptions, ): StorageInstance { - return createStorage({ - key, - provider: urlSearchParamsProvider, - initialValue, - serializer: options?.serializer ?? jsonSerializer, - onChange: options?.onChange, - }); + return createStorage({ ...options, provider: urlSearchParamsProvider }); } /** * Create URLSearchParams in hash utility */ -export function useUrlSearchParamsInHash( - key: string, - initialValue?: T, - options?: { - serializer?: StorageOptions['serializer']; - onChange?: (value: T | null) => void; - }, +export function useUrlSearchParamsInHash = Record>( + options?: StorageOptions, ): StorageInstance { - return createStorage({ - key, - provider: urlSearchParamsInHashProvider, - initialValue, - serializer: options?.serializer ?? jsonSerializer, - onChange: options?.onChange, - }); + return createStorage({ ...options, provider: urlSearchParamsInHashProvider }); } diff --git a/packages/tests/utils/storage.spec.ts b/packages/tests/utils/storage.spec.ts index e3c4d9eff..8836bc79c 100644 --- a/packages/tests/utils/storage.spec.ts +++ b/packages/tests/utils/storage.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; import { useLocalStorage, useSessionStorage, @@ -19,179 +19,206 @@ describe('Storage utilities', () => { }); describe('useLocalStorage', () => { - it('should initialize with stored value', () => { - localStorage.setItem('test-key', JSON.stringify('stored-value')); - const storage = useLocalStorage('test-key', 'default'); - expect(storage.value).toBe('stored-value'); - }); + it('should handle multiple keys in same instance', () => { + type Storage = { + theme: string; + user: { name: string }; + }; - it('should initialize with default value when no stored value exists', () => { - const storage = useLocalStorage('test-key', 'default'); - expect(storage.value).toBe('default'); - }); + const storage = useLocalStorage(); - it('should update storage when value changes', () => { - const storage = useLocalStorage('test-key', 'initial'); - storage.value = 'updated'; - expect(localStorage.getItem('test-key')).toBe(JSON.stringify('updated')); + storage.set('theme', 'dark'); + storage.set('user', { name: 'John' }); + + expect(storage.get('theme')).toBe('dark'); + expect(storage.get('user')).toEqual({ name: 'John' }); + expect(localStorage.getItem('theme')).toBe(JSON.stringify('dark')); + expect(localStorage.getItem('user')).toBe(JSON.stringify({ name: 'John' })); }); - it('should remove storage when value is set to null', () => { - const storage = useLocalStorage('test-key', 'initial'); - storage.value = null; - expect(localStorage.getItem('test-key')).toBeNull(); + it('should return null for non-existent keys', () => { + type Storage = { theme: string }; + const storage = useLocalStorage(); + expect(storage.get('theme')).toBeNull(); }); - it('should work with complex objects', () => { - const storage = useLocalStorage<{ name: string; age: number }>('user', { - name: 'John', - age: 30, - }); - expect(storage.value).toEqual({ name: 'John', age: 30 }); - storage.value = { name: 'Jane', age: 25 }; - expect(JSON.parse(localStorage.getItem('user')!)).toEqual({ name: 'Jane', age: 25 }); + it('should remove key when set to null', () => { + type Storage = { theme: string }; + const storage = useLocalStorage(); + + storage.set('theme', 'dark'); + expect(storage.get('theme')).toBe('dark'); + + storage.set('theme', null); + expect(storage.get('theme')).toBeNull(); + expect(localStorage.getItem('theme')).toBeNull(); }); - it('should trigger onChange callback when value changes', () => { - const onChange = vi.fn(); - const storage = useLocalStorage('test-key', 'initial', { onChange }); - storage.value = 'updated'; - expect(onChange).toHaveBeenCalledWith('updated'); + it('should trigger subscribers for specific keys', () => { + type Storage = { + theme: string; + lang: string; + }; + + const storage = useLocalStorage(); + const themeCallback = vi.fn(); + const langCallback = vi.fn(); + + storage.subscribe('theme', themeCallback); + storage.subscribe('lang', langCallback); + + storage.set('theme', 'dark'); + expect(themeCallback).toHaveBeenCalledWith('dark'); + expect(langCallback).not.toHaveBeenCalled(); + + storage.set('lang', 'fr'); + expect(langCallback).toHaveBeenCalledWith('fr'); + expect(themeCallback).toHaveBeenCalledTimes(1); }); - it('should support subscribe method', () => { - const storage = useLocalStorage('test-key', 'initial'); + it('should unsubscribe correctly', () => { + type Storage = { theme: string }; + const storage = useLocalStorage(); const callback = vi.fn(); - const unsubscribe = storage.subscribe(callback); - storage.value = 'updated'; - expect(callback).toHaveBeenCalledWith('updated'); + const unsubscribe = storage.subscribe('theme', callback); + storage.set('theme', 'dark'); + expect(callback).toHaveBeenCalledWith('dark'); unsubscribe(); - storage.value = 'another'; + storage.set('theme', 'light'); expect(callback).toHaveBeenCalledTimes(1); }); + it('should use custom serializer', () => { + type Storage = { count: string }; + const serializer = { + serialize: (value: any) => `custom:${value}`, + deserialize: (value: string) => value.replace('custom:', ''), + }; + + const storage = useLocalStorage({ serializer }); + storage.set('count', '5'); + expect(localStorage.getItem('count')).toBe('custom:5'); + expect(storage.get('count')).toBe('5'); + }); + it('should clean up event listeners on destroy', () => { - const storage = useLocalStorage('test-key', 'initial'); + const storage = useLocalStorage(); const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener'); storage.destroy(); expect(removeEventListenerSpy).toHaveBeenCalledWith('storage', expect.any(Function)); removeEventListenerSpy.mockRestore(); }); - - it('should use custom serializer', () => { - const serializer = { - serialize: (value: string) => `custom:${value}`, - deserialize: (value: string) => value.replace('custom:', ''), - }; - const storage = useLocalStorage('test-key', 'initial', { serializer }); - storage.value = 'test'; - expect(localStorage.getItem('test-key')).toBe('custom:test'); - }); }); describe('useSessionStorage', () => { - it('should initialize with stored value', () => { - sessionStorage.setItem('test-key', JSON.stringify('stored-value')); - const storage = useSessionStorage('test-key', 'default'); - expect(storage.value).toBe('stored-value'); - }); + it('should handle multiple keys in same instance', () => { + type Storage = { + token: string; + expires: number; + }; - it('should initialize with default value when no stored value exists', () => { - const storage = useSessionStorage('test-key', 'default'); - expect(storage.value).toBe('default'); - }); + const storage = useSessionStorage(); - it('should update storage when value changes', () => { - const storage = useSessionStorage('test-key', 'initial'); - storage.value = 'updated'; - expect(sessionStorage.getItem('test-key')).toBe(JSON.stringify('updated')); - }); + storage.set('token', 'abc123'); + storage.set('expires', 3600); - it('should remove storage when value is set to null', () => { - const storage = useSessionStorage('test-key', 'initial'); - storage.value = null; - expect(sessionStorage.getItem('test-key')).toBeNull(); + expect(storage.get('token')).toBe('abc123'); + expect(storage.get('expires')).toBe(3600); }); - it('should trigger onChange callback when value changes', () => { - const onChange = vi.fn(); - const storage = useSessionStorage('test-key', 'initial', { onChange }); - storage.value = 'updated'; - expect(onChange).toHaveBeenCalledWith('updated'); + it('should trigger subscribers for specific keys', () => { + type Storage = { + token: string; + expires: number; + }; + + const storage = useSessionStorage(); + const tokenCallback = vi.fn(); + + storage.subscribe('token', tokenCallback); + storage.set('token', 'abc123'); + expect(tokenCallback).toHaveBeenCalledWith('abc123'); }); }); describe('useUrlSearchParams', () => { - it('should initialize with default value when no URL param exists', () => { - const storage = useUrlSearchParams('test-key', 'default'); - expect(storage.value).toBe('default'); - }); - - it('should trigger onChange callback when value changes', () => { - const onChange = vi.fn(); - const storage = useUrlSearchParams('test-key', 'initial', { onChange }); - storage.value = 'updated'; - expect(onChange).toHaveBeenCalledWith('updated'); - }); + it('should handle multiple keys in same instance', () => { + type Storage = { + page: number; + sort: string; + }; - it('should call provider set method when value changes', () => { + const storage = useUrlSearchParams(); const setSpy = vi.spyOn(urlSearchParamsProvider, 'set'); - const storage = useUrlSearchParams('test-key', 'initial'); - storage.value = 'updated'; - expect(setSpy).toHaveBeenCalledWith('test-key', JSON.stringify('updated')); + + storage.set('page', 1); + storage.set('sort', 'name'); + + expect(setSpy).toHaveBeenCalledWith('page', JSON.stringify(1)); + expect(setSpy).toHaveBeenCalledWith('sort', JSON.stringify('name')); + setSpy.mockRestore(); }); - it('should call provider remove method when value is null', () => { - const removeSpy = vi.spyOn(urlSearchParamsProvider, 'remove'); - const storage = useUrlSearchParams('test-key', 'initial'); - storage.value = null; - expect(removeSpy).toHaveBeenCalledWith('test-key'); - removeSpy.mockRestore(); + it('should trigger subscribers for specific keys', () => { + type Storage = { + page: number; + sort: string; + }; + + const storage = useUrlSearchParams(); + const pageCallback = vi.fn(); + + storage.subscribe('page', pageCallback); + storage.set('page', 2); + expect(pageCallback).toHaveBeenCalledWith(2); + }); + + it('should listen to popstate events', () => { + const addEventListener = vi.spyOn(window, 'addEventListener'); + const storage = useUrlSearchParams(); + expect(addEventListener).toHaveBeenCalledWith('popstate', expect.any(Function)); + addEventListener.mockRestore(); }); }); describe('useUrlSearchParamsInHash', () => { - it('should initialize with hash param value', () => { - window.location.hash = '#test-key=%22hash-value%22'; - const storage = useUrlSearchParamsInHash('test-key', 'default'); - expect(storage.value).toBe('hash-value'); - }); + it('should handle multiple keys in same instance', () => { + type Storage = { + tab: string; + view: string; + }; - it('should initialize with default value when no hash param exists', () => { - const storage = useUrlSearchParamsInHash('test-key', 'default'); - expect(storage.value).toBe('default'); - }); + const storage = useUrlSearchParamsInHash(); - it('should update hash when value changes', () => { - const storage = useUrlSearchParamsInHash('test-key', 'initial'); - storage.value = 'updated'; - const params = new URLSearchParams(window.location.hash.slice(1)); - expect(params.get('test-key')).toBe(JSON.stringify('updated')); - }); + storage.set('tab', 'settings'); + storage.set('view', 'grid'); - it('should remove param from hash when value is set to null', () => { - const storage = useUrlSearchParamsInHash('test-key', 'value'); - storage.value = null; - const params = new URLSearchParams(window.location.hash.slice(1)); - expect(params.has('test-key')).toBe(false); + expect(storage.get('tab')).toBe('settings'); + expect(storage.get('view')).toBe('grid'); }); - it('should call provider set method when value changes', () => { - const setSpy = vi.spyOn(urlSearchParamsInHashProvider, 'set'); - const storage = useUrlSearchParamsInHash('test-key', 'initial'); - storage.value = 'updated'; - expect(setSpy).toHaveBeenCalledWith('test-key', JSON.stringify('updated')); - setSpy.mockRestore(); + it('should trigger subscribers for specific keys', () => { + type Storage = { + tab: string; + view: string; + }; + + const storage = useUrlSearchParamsInHash(); + const tabCallback = vi.fn(); + + storage.subscribe('tab', tabCallback); + storage.set('tab', 'profile'); + expect(tabCallback).toHaveBeenCalledWith('profile'); }); - it('should trigger onChange callback when value changes', () => { - const onChange = vi.fn(); - const storage = useUrlSearchParamsInHash('test-key', 'initial', { onChange }); - storage.value = 'updated'; - expect(onChange).toHaveBeenCalledWith('updated'); + it('should listen to hashchange events', () => { + const addEventListener = vi.spyOn(window, 'addEventListener'); + const storage = useUrlSearchParamsInHash(); + expect(addEventListener).toHaveBeenCalledWith('hashchange', expect.any(Function)); + addEventListener.mockRestore(); }); }); @@ -236,56 +263,37 @@ describe('Storage utilities', () => { describe('Event synchronization', () => { it('should sync localStorage changes from storage events', () => { - const storage = useLocalStorage('test-key', 'initial'); + type Storage = { theme: string }; + const storage = useLocalStorage(); const callback = vi.fn(); - storage.subscribe(callback); + storage.subscribe('theme', callback); // Simulate storage event from another tab const event = new StorageEvent('storage', { - key: 'test-key', - newValue: JSON.stringify('from-another-tab'), + key: 'theme', + newValue: JSON.stringify('dark'), storageArea: localStorage, }); window.dispatchEvent(event); - expect(storage.value).toBe('from-another-tab'); - expect(callback).toHaveBeenCalledWith('from-another-tab'); + expect(callback).toHaveBeenCalledWith('dark'); }); it('should sync sessionStorage changes from storage events', () => { - const storage = useSessionStorage('test-key', 'initial'); + type Storage = { token: string }; + const storage = useSessionStorage(); const callback = vi.fn(); - storage.subscribe(callback); + storage.subscribe('token', callback); // Simulate storage event from another tab const event = new StorageEvent('storage', { - key: 'test-key', - newValue: JSON.stringify('from-another-tab'), + key: 'token', + newValue: JSON.stringify('new-token'), storageArea: sessionStorage, }); window.dispatchEvent(event); - expect(storage.value).toBe('from-another-tab'); - expect(callback).toHaveBeenCalledWith('from-another-tab'); - }); - - it('should listen to popstate events', () => { - const addEventListener = vi.spyOn(window, 'addEventListener'); - const storage = useUrlSearchParams('test-key', 'initial'); - expect(addEventListener).toHaveBeenCalledWith('popstate', expect.any(Function)); - addEventListener.mockRestore(); - }); - - it('should sync hash params on hashchange event', () => { - const storage = useUrlSearchParamsInHash('test-key', 'initial'); - const callback = vi.fn(); - storage.subscribe(callback); - - window.location.hash = '#test-key=%22hash-value%22'; - window.dispatchEvent(new HashChangeEvent('hashchange')); - - expect(storage.value).toBe('hash-value'); - expect(callback).toHaveBeenCalledWith('hash-value'); + expect(callback).toHaveBeenCalledWith('new-token'); }); }); }); From d7a125f49b5e95b380bf1244fa98381bf1d9815f Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Thu, 2 Oct 2025 21:03:18 +0200 Subject: [PATCH 6/6] Update tests --- packages/tests/utils/index.spec.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/tests/utils/index.spec.ts b/packages/tests/utils/index.spec.ts index 408184b7f..d9aed1cb9 100644 --- a/packages/tests/utils/index.spec.ts +++ b/packages/tests/utils/index.spec.ts @@ -24,6 +24,7 @@ describe('@studiometa/js-toolkit/utils exports', () => { "createEaseOut", "createElement", "createRange", + "createStorage", "damp", "dashCase", "debounce", @@ -78,6 +79,7 @@ describe('@studiometa/js-toolkit/utils exports', () => { "loadImage", "loadLink", "loadScript", + "localStorageProvider", "lowerCase", "map", "matrix", @@ -99,6 +101,7 @@ describe('@studiometa/js-toolkit/utils exports', () => { "round", "saveActiveElement", "scrollTo", + "sessionStorageProvider", "smoothTo", "snakeCase", "spring", @@ -111,7 +114,13 @@ describe('@studiometa/js-toolkit/utils exports', () => { "tween", "untrapFocus", "upperCase", + "urlSearchParamsInHashProvider", + "urlSearchParamsProvider", + "useLocalStorage", "useScheduler", + "useSessionStorage", + "useUrlSearchParams", + "useUrlSearchParamsInHash", "wait", "withLeadingCharacters", "withLeadingSlash",