diff --git a/.editorconfig b/.editorconfig index 8591b8f..4bb7fad 100644 --- a/.editorconfig +++ b/.editorconfig @@ -19,6 +19,6 @@ indent_size = 2 [*.{json,yml}] indent_size = 2 -[*.md] +[*.{js,md,ts,vue}] max_line_length = off trim_trailing_whitespace = false diff --git a/eslint.config.mjs b/eslint.config.mjs index 8ce942c..fbb46c4 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -10,6 +10,7 @@ const gitignorePath = path.resolve(__dirname, ".gitignore"); export default [includeIgnoreFile(gitignorePath), ...eslintTs, { rules: { + "no-trailing-spaces": ["error", { "ignoreComments": true }], "@typescript-eslint/no-non-null-assertion": "off", "@typescript-eslint/unified-signatures": "off" } diff --git a/package.json b/package.json index 22a03fd..794ccb6 100644 --- a/package.json +++ b/package.json @@ -36,12 +36,12 @@ "exports": { ".": { "import": { - "default": "./dist/core.js", - "types": "./src/index.ts" + "types": "./src/index.ts", + "default": "./dist/core.js" }, "require": { - "default": "./dist/core.umd.cjs", - "types": "./src/index.ts" + "types": "./src/index.ts", + "default": "./dist/core.umd.cjs" } } }, @@ -58,10 +58,10 @@ }, "devDependencies": { "@byloth/eslint-config-typescript": "^3.0.3", - "@types/node": "^22.10.2", + "@types/node": "^22.10.7", "husky": "^9.1.7", - "typescript": "^5.7.2", - "vite": "^5.4.11" + "typescript": "^5.7.3", + "vite": "^6.0.10" }, "packageManager": "pnpm@9.15.0+sha512.76e2379760a4328ec4415815bcd6628dee727af3779aaa4c914e3944156c4299921a89f976381ee107d41f12cfa4b66681ca9c718f0668fa0831ed4c6d8ba56c" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f6705b8..4eecd06 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,19 +10,19 @@ importers: devDependencies: '@byloth/eslint-config-typescript': specifier: ^3.0.3 - version: 3.0.3(eslint@9.17.0)(typescript@5.7.2) + version: 3.0.3(eslint@9.18.0)(typescript@5.7.3) '@types/node': - specifier: ^22.10.2 - version: 22.10.2 + specifier: ^22.10.7 + version: 22.10.7 husky: specifier: ^9.1.7 version: 9.1.7 typescript: - specifier: ^5.7.2 - version: 5.7.2 + specifier: ^5.7.3 + version: 5.7.3 vite: - specifier: ^5.4.11 - version: 5.4.11(@types/node@22.10.2) + specifier: ^6.0.10 + version: 6.0.10(@types/node@22.10.7) packages: @@ -32,141 +32,153 @@ packages: '@byloth/eslint-config@3.0.3': resolution: {integrity: sha512-fXpIxZByU2Ux+95jGcEEweKYb4bxS571xaMZDJ24wsasrDuczjld63EGAulnm9yRAJiLpScruvV0mWLow+16tg==} - '@esbuild/aix-ppc64@0.21.5': - resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} - engines: {node: '>=12'} + '@esbuild/aix-ppc64@0.24.2': + resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==} + engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.21.5': - resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} - engines: {node: '>=12'} + '@esbuild/android-arm64@0.24.2': + resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==} + engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.21.5': - resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} - engines: {node: '>=12'} + '@esbuild/android-arm@0.24.2': + resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==} + engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.21.5': - resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} - engines: {node: '>=12'} + '@esbuild/android-x64@0.24.2': + resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==} + engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.21.5': - resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} - engines: {node: '>=12'} + '@esbuild/darwin-arm64@0.24.2': + resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==} + engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.21.5': - resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} - engines: {node: '>=12'} + '@esbuild/darwin-x64@0.24.2': + resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==} + engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.21.5': - resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} - engines: {node: '>=12'} + '@esbuild/freebsd-arm64@0.24.2': + resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==} + engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.21.5': - resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} - engines: {node: '>=12'} + '@esbuild/freebsd-x64@0.24.2': + resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==} + engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.21.5': - resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} - engines: {node: '>=12'} + '@esbuild/linux-arm64@0.24.2': + resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==} + engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.21.5': - resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} - engines: {node: '>=12'} + '@esbuild/linux-arm@0.24.2': + resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==} + engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.21.5': - resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} - engines: {node: '>=12'} + '@esbuild/linux-ia32@0.24.2': + resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==} + engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.21.5': - resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} - engines: {node: '>=12'} + '@esbuild/linux-loong64@0.24.2': + resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==} + engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.21.5': - resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} - engines: {node: '>=12'} + '@esbuild/linux-mips64el@0.24.2': + resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==} + engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.21.5': - resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} - engines: {node: '>=12'} + '@esbuild/linux-ppc64@0.24.2': + resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==} + engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.21.5': - resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} - engines: {node: '>=12'} + '@esbuild/linux-riscv64@0.24.2': + resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==} + engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.21.5': - resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} - engines: {node: '>=12'} + '@esbuild/linux-s390x@0.24.2': + resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==} + engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.21.5': - resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} - engines: {node: '>=12'} + '@esbuild/linux-x64@0.24.2': + resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==} + engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-x64@0.21.5': - resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} - engines: {node: '>=12'} + '@esbuild/netbsd-arm64@0.24.2': + resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.24.2': + resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==} + engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-x64@0.21.5': - resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} - engines: {node: '>=12'} + '@esbuild/openbsd-arm64@0.24.2': + resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.24.2': + resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==} + engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/sunos-x64@0.21.5': - resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} - engines: {node: '>=12'} + '@esbuild/sunos-x64@0.24.2': + resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==} + engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.21.5': - resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} - engines: {node: '>=12'} + '@esbuild/win32-arm64@0.24.2': + resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==} + engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.21.5': - resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} - engines: {node: '>=12'} + '@esbuild/win32-ia32@0.24.2': + resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==} + engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.21.5': - resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} - engines: {node: '>=12'} + '@esbuild/win32-x64@0.24.2': + resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==} + engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -180,8 +192,8 @@ packages: resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/compat@1.2.4': - resolution: {integrity: sha512-S8ZdQj/N69YAtuqFt7653jwcvuUj131+6qGLUyDqfDg1OIoBQ66OCuXC473YQfO2AaxITTutiRQiDwoo7ZLYyg==} + '@eslint/compat@1.2.5': + resolution: {integrity: sha512-5iuG/StT+7OfvhoBHPlmxkPA9om6aDUFgmD4+mWKAGsYt4vCe8rypneG03AuseyRHBmcCLXQtIH5S26tIoggLg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^9.10.0 @@ -193,24 +205,24 @@ packages: resolution: {integrity: sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@0.9.1': - resolution: {integrity: sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==} + '@eslint/core@0.10.0': + resolution: {integrity: sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/eslintrc@3.2.0': resolution: {integrity: sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.17.0': - resolution: {integrity: sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==} + '@eslint/js@9.18.0': + resolution: {integrity: sha512-fK6L7rxcq6/z+AaQMtiFTkvbHkBLNlwyRxHpKawP0x3u9+NC6MQTnFW+AdpwC6gfHTW0051cokQgtTN2FqlxQA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.5': resolution: {integrity: sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.2.4': - resolution: {integrity: sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==} + '@eslint/plugin-kit@0.2.5': + resolution: {integrity: sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@humanfs/core@0.19.1': @@ -245,98 +257,98 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@rollup/rollup-android-arm-eabi@4.29.1': - resolution: {integrity: sha512-ssKhA8RNltTZLpG6/QNkCSge+7mBQGUqJRisZ2MDQcEGaK93QESEgWK2iOpIDZ7k9zPVkG5AS3ksvD5ZWxmItw==} + '@rollup/rollup-android-arm-eabi@4.31.0': + resolution: {integrity: sha512-9NrR4033uCbUBRgvLcBrJofa2KY9DzxL2UKZ1/4xA/mnTNyhZCWBuD8X3tPm1n4KxcgaraOYgrFKSgwjASfmlA==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.29.1': - resolution: {integrity: sha512-CaRfrV0cd+NIIcVVN/jx+hVLN+VRqnuzLRmfmlzpOzB87ajixsN/+9L5xNmkaUUvEbI5BmIKS+XTwXsHEb65Ew==} + '@rollup/rollup-android-arm64@4.31.0': + resolution: {integrity: sha512-iBbODqT86YBFHajxxF8ebj2hwKm1k8PTBQSojSt3d1FFt1gN+xf4CowE47iN0vOSdnd+5ierMHBbu/rHc7nq5g==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.29.1': - resolution: {integrity: sha512-2ORr7T31Y0Mnk6qNuwtyNmy14MunTAMx06VAPI6/Ju52W10zk1i7i5U3vlDRWjhOI5quBcrvhkCHyF76bI7kEw==} + '@rollup/rollup-darwin-arm64@4.31.0': + resolution: {integrity: sha512-WHIZfXgVBX30SWuTMhlHPXTyN20AXrLH4TEeH/D0Bolvx9PjgZnn4H677PlSGvU6MKNsjCQJYczkpvBbrBnG6g==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.29.1': - resolution: {integrity: sha512-j/Ej1oanzPjmN0tirRd5K2/nncAhS9W6ICzgxV+9Y5ZsP0hiGhHJXZ2JQ53iSSjj8m6cRY6oB1GMzNn2EUt6Ng==} + '@rollup/rollup-darwin-x64@4.31.0': + resolution: {integrity: sha512-hrWL7uQacTEF8gdrQAqcDy9xllQ0w0zuL1wk1HV8wKGSGbKPVjVUv/DEwT2+Asabf8Dh/As+IvfdU+H8hhzrQQ==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.29.1': - resolution: {integrity: sha512-91C//G6Dm/cv724tpt7nTyP+JdN12iqeXGFM1SqnljCmi5yTXriH7B1r8AD9dAZByHpKAumqP1Qy2vVNIdLZqw==} + '@rollup/rollup-freebsd-arm64@4.31.0': + resolution: {integrity: sha512-S2oCsZ4hJviG1QjPY1h6sVJLBI6ekBeAEssYKad1soRFv3SocsQCzX6cwnk6fID6UQQACTjeIMB+hyYrFacRew==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.29.1': - resolution: {integrity: sha512-hEioiEQ9Dec2nIRoeHUP6hr1PSkXzQaCUyqBDQ9I9ik4gCXQZjJMIVzoNLBRGet+hIUb3CISMh9KXuCcWVW/8w==} + '@rollup/rollup-freebsd-x64@4.31.0': + resolution: {integrity: sha512-pCANqpynRS4Jirn4IKZH4tnm2+2CqCNLKD7gAdEjzdLGbH1iO0zouHz4mxqg0uEMpO030ejJ0aA6e1PJo2xrPA==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.29.1': - resolution: {integrity: sha512-Py5vFd5HWYN9zxBv3WMrLAXY3yYJ6Q/aVERoeUFwiDGiMOWsMs7FokXihSOaT/PMWUty/Pj60XDQndK3eAfE6A==} + '@rollup/rollup-linux-arm-gnueabihf@4.31.0': + resolution: {integrity: sha512-0O8ViX+QcBd3ZmGlcFTnYXZKGbFu09EhgD27tgTdGnkcYXLat4KIsBBQeKLR2xZDCXdIBAlWLkiXE1+rJpCxFw==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.29.1': - resolution: {integrity: sha512-RiWpGgbayf7LUcuSNIbahr0ys2YnEERD4gYdISA06wa0i8RALrnzflh9Wxii7zQJEB2/Eh74dX4y/sHKLWp5uQ==} + '@rollup/rollup-linux-arm-musleabihf@4.31.0': + resolution: {integrity: sha512-w5IzG0wTVv7B0/SwDnMYmbr2uERQp999q8FMkKG1I+j8hpPX2BYFjWe69xbhbP6J9h2gId/7ogesl9hwblFwwg==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.29.1': - resolution: {integrity: sha512-Z80O+taYxTQITWMjm/YqNoe9d10OX6kDh8X5/rFCMuPqsKsSyDilvfg+vd3iXIqtfmp+cnfL1UrYirkaF8SBZA==} + '@rollup/rollup-linux-arm64-gnu@4.31.0': + resolution: {integrity: sha512-JyFFshbN5xwy6fulZ8B/8qOqENRmDdEkcIMF0Zz+RsfamEW+Zabl5jAb0IozP/8UKnJ7g2FtZZPEUIAlUSX8cA==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.29.1': - resolution: {integrity: sha512-fOHRtF9gahwJk3QVp01a/GqS4hBEZCV1oKglVVq13kcK3NeVlS4BwIFzOHDbmKzt3i0OuHG4zfRP0YoG5OF/rA==} + '@rollup/rollup-linux-arm64-musl@4.31.0': + resolution: {integrity: sha512-kpQXQ0UPFeMPmPYksiBL9WS/BDiQEjRGMfklVIsA0Sng347H8W2iexch+IEwaR7OVSKtr2ZFxggt11zVIlZ25g==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.29.1': - resolution: {integrity: sha512-5a7q3tnlbcg0OodyxcAdrrCxFi0DgXJSoOuidFUzHZ2GixZXQs6Tc3CHmlvqKAmOs5eRde+JJxeIf9DonkmYkw==} + '@rollup/rollup-linux-loongarch64-gnu@4.31.0': + resolution: {integrity: sha512-pMlxLjt60iQTzt9iBb3jZphFIl55a70wexvo8p+vVFK+7ifTRookdoXX3bOsRdmfD+OKnMozKO6XM4zR0sHRrQ==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.29.1': - resolution: {integrity: sha512-9b4Mg5Yfz6mRnlSPIdROcfw1BU22FQxmfjlp/CShWwO3LilKQuMISMTtAu/bxmmrE6A902W2cZJuzx8+gJ8e9w==} + '@rollup/rollup-linux-powerpc64le-gnu@4.31.0': + resolution: {integrity: sha512-D7TXT7I/uKEuWiRkEFbed1UUYZwcJDU4vZQdPTcepK7ecPhzKOYk4Er2YR4uHKme4qDeIh6N3XrLfpuM7vzRWQ==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.29.1': - resolution: {integrity: sha512-G5pn0NChlbRM8OJWpJFMX4/i8OEU538uiSv0P6roZcbpe/WfhEO+AT8SHVKfp8qhDQzaz7Q+1/ixMy7hBRidnQ==} + '@rollup/rollup-linux-riscv64-gnu@4.31.0': + resolution: {integrity: sha512-wal2Tc8O5lMBtoePLBYRKj2CImUCJ4UNGJlLwspx7QApYny7K1cUYlzQ/4IGQBLmm+y0RS7dwc3TDO/pmcneTw==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.29.1': - resolution: {integrity: sha512-WM9lIkNdkhVwiArmLxFXpWndFGuOka4oJOZh8EP3Vb8q5lzdSCBuhjavJsw68Q9AKDGeOOIHYzYm4ZFvmWez5g==} + '@rollup/rollup-linux-s390x-gnu@4.31.0': + resolution: {integrity: sha512-O1o5EUI0+RRMkK9wiTVpk2tyzXdXefHtRTIjBbmFREmNMy7pFeYXCFGbhKFwISA3UOExlo5GGUuuj3oMKdK6JQ==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.29.1': - resolution: {integrity: sha512-87xYCwb0cPGZFoGiErT1eDcssByaLX4fc0z2nRM6eMtV9njAfEE6OW3UniAoDhX4Iq5xQVpE6qO9aJbCFumKYQ==} + '@rollup/rollup-linux-x64-gnu@4.31.0': + resolution: {integrity: sha512-zSoHl356vKnNxwOWnLd60ixHNPRBglxpv2g7q0Cd3Pmr561gf0HiAcUBRL3S1vPqRC17Zo2CX/9cPkqTIiai1g==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.29.1': - resolution: {integrity: sha512-xufkSNppNOdVRCEC4WKvlR1FBDyqCSCpQeMMgv9ZyXqqtKBfkw1yfGMTUTs9Qsl6WQbJnsGboWCp7pJGkeMhKA==} + '@rollup/rollup-linux-x64-musl@4.31.0': + resolution: {integrity: sha512-ypB/HMtcSGhKUQNiFwqgdclWNRrAYDH8iMYH4etw/ZlGwiTVxBz2tDrGRrPlfZu6QjXwtd+C3Zib5pFqID97ZA==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.29.1': - resolution: {integrity: sha512-F2OiJ42m77lSkizZQLuC+jiZ2cgueWQL5YC9tjo3AgaEw+KJmVxHGSyQfDUoYR9cci0lAywv2Clmckzulcq6ig==} + '@rollup/rollup-win32-arm64-msvc@4.31.0': + resolution: {integrity: sha512-JuhN2xdI/m8Hr+aVO3vspO7OQfUFO6bKLIRTAy0U15vmWjnZDLrEgCZ2s6+scAYaQVpYSh9tZtRijApw9IXyMw==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.29.1': - resolution: {integrity: sha512-rYRe5S0FcjlOBZQHgbTKNrqxCBUmgDJem/VQTCcTnA2KCabYSWQDrytOzX7avb79cAAweNmMUb/Zw18RNd4mng==} + '@rollup/rollup-win32-ia32-msvc@4.31.0': + resolution: {integrity: sha512-U1xZZXYkvdf5MIWmftU8wrM5PPXzyaY1nGCI4KI4BFfoZxHamsIe+BtnPLIvvPykvQWlVbqUXdLa4aJUuilwLQ==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.29.1': - resolution: {integrity: sha512-+10CMg9vt1MoHj6x1pxyjPSMjHTIlqs8/tBztXvPAx24SKs9jwVnKqHJumlH/IzhaPUaj3T6T6wfZr8okdXaIg==} + '@rollup/rollup-win32-x64-msvc@4.31.0': + resolution: {integrity: sha512-ul8rnCsUumNln5YWwz0ted2ZHFhzhRRnkpBZ+YRuHoRAlUji9KChpOUOndY7uykrPEPXVbHLlsdo6v5yXo/TXw==} cpu: [x64] os: [win32] @@ -346,54 +358,54 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/node@22.10.2': - resolution: {integrity: sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==} + '@types/node@22.10.7': + resolution: {integrity: sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==} - '@typescript-eslint/eslint-plugin@8.18.1': - resolution: {integrity: sha512-Ncvsq5CT3Gvh+uJG0Lwlho6suwDfUXH0HztslDf5I+F2wAFAZMRwYLEorumpKLzmO2suAXZ/td1tBg4NZIi9CQ==} + '@typescript-eslint/eslint-plugin@8.21.0': + resolution: {integrity: sha512-eTH+UOR4I7WbdQnG4Z48ebIA6Bgi7WO8HvFEneeYBxG8qCOYgTOFPSg6ek9ITIDvGjDQzWHcoWHCDO2biByNzA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.8.0' - '@typescript-eslint/parser@8.18.1': - resolution: {integrity: sha512-rBnTWHCdbYM2lh7hjyXqxk70wvon3p2FyaniZuey5TrcGBpfhVp0OxOa6gxr9Q9YhZFKyfbEnxc24ZnVbbUkCA==} + '@typescript-eslint/parser@8.21.0': + resolution: {integrity: sha512-Wy+/sdEH9kI3w9civgACwabHbKl+qIOu0uFZ9IMKzX3Jpv9og0ZBJrZExGrPpFAY7rWsXuxs5e7CPPP17A4eYA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.8.0' - '@typescript-eslint/scope-manager@8.18.1': - resolution: {integrity: sha512-HxfHo2b090M5s2+/9Z3gkBhI6xBH8OJCFjH9MhQ+nnoZqxU3wNxkLT+VWXWSFWc3UF3Z+CfPAyqdCTdoXtDPCQ==} + '@typescript-eslint/scope-manager@8.21.0': + resolution: {integrity: sha512-G3IBKz0/0IPfdeGRMbp+4rbjfSSdnGkXsM/pFZA8zM9t9klXDnB/YnKOBQ0GoPmoROa4bCq2NeHgJa5ydsQ4mA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/type-utils@8.18.1': - resolution: {integrity: sha512-jAhTdK/Qx2NJPNOTxXpMwlOiSymtR2j283TtPqXkKBdH8OAMmhiUfP0kJjc/qSE51Xrq02Gj9NY7MwK+UxVwHQ==} + '@typescript-eslint/type-utils@8.21.0': + resolution: {integrity: sha512-95OsL6J2BtzoBxHicoXHxgk3z+9P3BEcQTpBKriqiYzLKnM2DeSqs+sndMKdamU8FosiadQFT3D+BSL9EKnAJQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.8.0' - '@typescript-eslint/types@8.18.1': - resolution: {integrity: sha512-7uoAUsCj66qdNQNpH2G8MyTFlgerum8ubf21s3TSM3XmKXuIn+H2Sifh/ES2nPOPiYSRJWAk0fDkW0APBWcpfw==} + '@typescript-eslint/types@8.21.0': + resolution: {integrity: sha512-PAL6LUuQwotLW2a8VsySDBwYMm129vFm4tMVlylzdoTybTHaAi0oBp7Ac6LhSrHHOdLM3efH+nAR6hAWoMF89A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.18.1': - resolution: {integrity: sha512-z8U21WI5txzl2XYOW7i9hJhxoKKNG1kcU4RzyNvKrdZDmbjkmLBo8bgeiOJmA06kizLI76/CCBAAGlTlEeUfyg==} + '@typescript-eslint/typescript-estree@8.21.0': + resolution: {integrity: sha512-x+aeKh/AjAArSauz0GiQZsjT8ciadNMHdkUSwBB9Z6PrKc/4knM4g3UfHml6oDJmKC88a6//cdxnO/+P2LkMcg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <5.8.0' - '@typescript-eslint/utils@8.18.1': - resolution: {integrity: sha512-8vikiIj2ebrC4WRdcAdDcmnu9Q/MXXwg+STf40BVfT8exDqBCUPdypvzcUPxEqRGKg9ALagZ0UWcYCtn+4W2iQ==} + '@typescript-eslint/utils@8.21.0': + resolution: {integrity: sha512-xcXBfcq0Kaxgj7dwejMbFyq7IOHgpNMtVuDveK7w3ZGwG9owKzhALVwKpTF2yrZmEwl9SWdetf3fxNzJQaVuxw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.8.0' - '@typescript-eslint/visitor-keys@8.18.1': - resolution: {integrity: sha512-Vj0WLm5/ZsD013YeUKn+K0y8p1M0jPpxOkKdbD1wB0ns53a5piVY02zjf072TblEweAbcYiFiPoSMF3kp+VhhQ==} + '@typescript-eslint/visitor-keys@8.21.0': + resolution: {integrity: sha512-BkLMNpdV6prozk8LlyK/SOoWLmUFi+ZD+pcqti9ILCbVvHGk1ui1g4jJOc2WDLaeExz2qWwojxlPce5PljcT3w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} acorn-jsx@5.3.2: @@ -463,9 +475,9 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - esbuild@0.21.5: - resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} - engines: {node: '>=12'} + esbuild@0.24.2: + resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==} + engines: {node: '>=18'} hasBin: true escape-string-regexp@4.0.0: @@ -484,8 +496,8 @@ packages: resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.17.0: - resolution: {integrity: sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA==} + eslint@9.18.0: + resolution: {integrity: sha512-+waTfRWQlSbpt3KWE+CjrPPYnbq9kfZIYUqapc0uBXyjTp8aYXZDsUH16m39Ryq3NjAVP4tjuF7KaukeqoCoaA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -517,8 +529,8 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - fast-glob@3.3.2: - resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} fast-json-stable-stringify@2.1.0: @@ -527,8 +539,8 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fastq@1.17.1: - resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + fastq@1.18.0: + resolution: {integrity: sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==} file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} @@ -693,8 +705,8 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} - postcss@8.4.49: - resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} + postcss@8.5.1: + resolution: {integrity: sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==} engines: {node: ^10 || ^12 || >=14} prelude-ls@1.2.1: @@ -716,8 +728,8 @@ packages: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rollup@4.29.1: - resolution: {integrity: sha512-RaJ45M/kmJUzSWDs1Nnd5DdV4eerC98idtUOVr6FfKcgxqvjwHmxc5upLF9qZU9EpsVzzhleFahrT3shLuJzIw==} + rollup@4.31.0: + resolution: {integrity: sha512-9cCE8P4rZLx9+PjoyqHLs31V9a9Vpvfo4qNcs6JCiGWYhw2gijSetFbH6SSy1whnkgcefnUwr8sad7tgqsGvnw==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -753,18 +765,18 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} - ts-api-utils@1.4.3: - resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} - engines: {node: '>=16'} + ts-api-utils@2.0.0: + resolution: {integrity: sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ==} + engines: {node: '>=18.12'} peerDependencies: - typescript: '>=4.2.0' + typescript: '>=4.8.4' type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - typescript@5.7.2: - resolution: {integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==} + typescript@5.7.3: + resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==} engines: {node: '>=14.17'} hasBin: true @@ -774,22 +786,27 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - vite@5.4.11: - resolution: {integrity: sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==} - engines: {node: ^18.0.0 || >=20.0.0} + vite@6.0.10: + resolution: {integrity: sha512-MEszunEcMo6pFsfXN1GhCFQqnE25tWRH0MA4f0Q7uanACi4y1Us+ZGpTMnITwCTnYzB2b9cpmnelTlxgTBmaBA==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' less: '*' lightningcss: ^1.21.0 sass: '*' sass-embedded: '*' stylus: '*' sugarss: '*' - terser: ^5.4.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 peerDependenciesMeta: '@types/node': optional: true + jiti: + optional: true less: optional: true lightningcss: @@ -804,6 +821,10 @@ packages: optional: true terser: optional: true + tsx: + optional: true + yaml: + optional: true which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} @@ -820,11 +841,11 @@ packages: snapshots: - '@byloth/eslint-config-typescript@3.0.3(eslint@9.17.0)(typescript@5.7.2)': + '@byloth/eslint-config-typescript@3.0.3(eslint@9.18.0)(typescript@5.7.3)': dependencies: '@byloth/eslint-config': 3.0.3 - '@typescript-eslint/eslint-plugin': 8.18.1(@typescript-eslint/parser@8.18.1(eslint@9.17.0)(typescript@5.7.2))(eslint@9.17.0)(typescript@5.7.2) - '@typescript-eslint/parser': 8.18.1(eslint@9.17.0)(typescript@5.7.2) + '@typescript-eslint/eslint-plugin': 8.21.0(@typescript-eslint/parser@8.21.0(eslint@9.18.0)(typescript@5.7.3))(eslint@9.18.0)(typescript@5.7.3) + '@typescript-eslint/parser': 8.21.0(eslint@9.18.0)(typescript@5.7.3) transitivePeerDependencies: - eslint - jiti @@ -833,93 +854,99 @@ snapshots: '@byloth/eslint-config@3.0.3': dependencies: - '@eslint/compat': 1.2.4(eslint@9.17.0) - '@eslint/js': 9.17.0 - eslint: 9.17.0 + '@eslint/compat': 1.2.5(eslint@9.18.0) + '@eslint/js': 9.18.0 + eslint: 9.18.0 globals: 15.14.0 transitivePeerDependencies: - jiti - supports-color - '@esbuild/aix-ppc64@0.21.5': + '@esbuild/aix-ppc64@0.24.2': + optional: true + + '@esbuild/android-arm64@0.24.2': + optional: true + + '@esbuild/android-arm@0.24.2': optional: true - '@esbuild/android-arm64@0.21.5': + '@esbuild/android-x64@0.24.2': optional: true - '@esbuild/android-arm@0.21.5': + '@esbuild/darwin-arm64@0.24.2': optional: true - '@esbuild/android-x64@0.21.5': + '@esbuild/darwin-x64@0.24.2': optional: true - '@esbuild/darwin-arm64@0.21.5': + '@esbuild/freebsd-arm64@0.24.2': optional: true - '@esbuild/darwin-x64@0.21.5': + '@esbuild/freebsd-x64@0.24.2': optional: true - '@esbuild/freebsd-arm64@0.21.5': + '@esbuild/linux-arm64@0.24.2': optional: true - '@esbuild/freebsd-x64@0.21.5': + '@esbuild/linux-arm@0.24.2': optional: true - '@esbuild/linux-arm64@0.21.5': + '@esbuild/linux-ia32@0.24.2': optional: true - '@esbuild/linux-arm@0.21.5': + '@esbuild/linux-loong64@0.24.2': optional: true - '@esbuild/linux-ia32@0.21.5': + '@esbuild/linux-mips64el@0.24.2': optional: true - '@esbuild/linux-loong64@0.21.5': + '@esbuild/linux-ppc64@0.24.2': optional: true - '@esbuild/linux-mips64el@0.21.5': + '@esbuild/linux-riscv64@0.24.2': optional: true - '@esbuild/linux-ppc64@0.21.5': + '@esbuild/linux-s390x@0.24.2': optional: true - '@esbuild/linux-riscv64@0.21.5': + '@esbuild/linux-x64@0.24.2': optional: true - '@esbuild/linux-s390x@0.21.5': + '@esbuild/netbsd-arm64@0.24.2': optional: true - '@esbuild/linux-x64@0.21.5': + '@esbuild/netbsd-x64@0.24.2': optional: true - '@esbuild/netbsd-x64@0.21.5': + '@esbuild/openbsd-arm64@0.24.2': optional: true - '@esbuild/openbsd-x64@0.21.5': + '@esbuild/openbsd-x64@0.24.2': optional: true - '@esbuild/sunos-x64@0.21.5': + '@esbuild/sunos-x64@0.24.2': optional: true - '@esbuild/win32-arm64@0.21.5': + '@esbuild/win32-arm64@0.24.2': optional: true - '@esbuild/win32-ia32@0.21.5': + '@esbuild/win32-ia32@0.24.2': optional: true - '@esbuild/win32-x64@0.21.5': + '@esbuild/win32-x64@0.24.2': optional: true - '@eslint-community/eslint-utils@4.4.1(eslint@9.17.0)': + '@eslint-community/eslint-utils@4.4.1(eslint@9.18.0)': dependencies: - eslint: 9.17.0 + eslint: 9.18.0 eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} - '@eslint/compat@1.2.4(eslint@9.17.0)': + '@eslint/compat@1.2.5(eslint@9.18.0)': optionalDependencies: - eslint: 9.17.0 + eslint: 9.18.0 '@eslint/config-array@0.19.1': dependencies: @@ -929,7 +956,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/core@0.9.1': + '@eslint/core@0.10.0': dependencies: '@types/json-schema': 7.0.15 @@ -947,12 +974,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.17.0': {} + '@eslint/js@9.18.0': {} '@eslint/object-schema@2.1.5': {} - '@eslint/plugin-kit@0.2.4': + '@eslint/plugin-kit@0.2.5': dependencies: + '@eslint/core': 0.10.0 levn: 0.4.1 '@humanfs/core@0.19.1': {} @@ -978,148 +1006,148 @@ snapshots: '@nodelib/fs.walk@1.2.8': dependencies: '@nodelib/fs.scandir': 2.1.5 - fastq: 1.17.1 + fastq: 1.18.0 - '@rollup/rollup-android-arm-eabi@4.29.1': + '@rollup/rollup-android-arm-eabi@4.31.0': optional: true - '@rollup/rollup-android-arm64@4.29.1': + '@rollup/rollup-android-arm64@4.31.0': optional: true - '@rollup/rollup-darwin-arm64@4.29.1': + '@rollup/rollup-darwin-arm64@4.31.0': optional: true - '@rollup/rollup-darwin-x64@4.29.1': + '@rollup/rollup-darwin-x64@4.31.0': optional: true - '@rollup/rollup-freebsd-arm64@4.29.1': + '@rollup/rollup-freebsd-arm64@4.31.0': optional: true - '@rollup/rollup-freebsd-x64@4.29.1': + '@rollup/rollup-freebsd-x64@4.31.0': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.29.1': + '@rollup/rollup-linux-arm-gnueabihf@4.31.0': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.29.1': + '@rollup/rollup-linux-arm-musleabihf@4.31.0': optional: true - '@rollup/rollup-linux-arm64-gnu@4.29.1': + '@rollup/rollup-linux-arm64-gnu@4.31.0': optional: true - '@rollup/rollup-linux-arm64-musl@4.29.1': + '@rollup/rollup-linux-arm64-musl@4.31.0': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.29.1': + '@rollup/rollup-linux-loongarch64-gnu@4.31.0': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.29.1': + '@rollup/rollup-linux-powerpc64le-gnu@4.31.0': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.29.1': + '@rollup/rollup-linux-riscv64-gnu@4.31.0': optional: true - '@rollup/rollup-linux-s390x-gnu@4.29.1': + '@rollup/rollup-linux-s390x-gnu@4.31.0': optional: true - '@rollup/rollup-linux-x64-gnu@4.29.1': + '@rollup/rollup-linux-x64-gnu@4.31.0': optional: true - '@rollup/rollup-linux-x64-musl@4.29.1': + '@rollup/rollup-linux-x64-musl@4.31.0': optional: true - '@rollup/rollup-win32-arm64-msvc@4.29.1': + '@rollup/rollup-win32-arm64-msvc@4.31.0': optional: true - '@rollup/rollup-win32-ia32-msvc@4.29.1': + '@rollup/rollup-win32-ia32-msvc@4.31.0': optional: true - '@rollup/rollup-win32-x64-msvc@4.29.1': + '@rollup/rollup-win32-x64-msvc@4.31.0': optional: true '@types/estree@1.0.6': {} '@types/json-schema@7.0.15': {} - '@types/node@22.10.2': + '@types/node@22.10.7': dependencies: undici-types: 6.20.0 - '@typescript-eslint/eslint-plugin@8.18.1(@typescript-eslint/parser@8.18.1(eslint@9.17.0)(typescript@5.7.2))(eslint@9.17.0)(typescript@5.7.2)': + '@typescript-eslint/eslint-plugin@8.21.0(@typescript-eslint/parser@8.21.0(eslint@9.18.0)(typescript@5.7.3))(eslint@9.18.0)(typescript@5.7.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.18.1(eslint@9.17.0)(typescript@5.7.2) - '@typescript-eslint/scope-manager': 8.18.1 - '@typescript-eslint/type-utils': 8.18.1(eslint@9.17.0)(typescript@5.7.2) - '@typescript-eslint/utils': 8.18.1(eslint@9.17.0)(typescript@5.7.2) - '@typescript-eslint/visitor-keys': 8.18.1 - eslint: 9.17.0 + '@typescript-eslint/parser': 8.21.0(eslint@9.18.0)(typescript@5.7.3) + '@typescript-eslint/scope-manager': 8.21.0 + '@typescript-eslint/type-utils': 8.21.0(eslint@9.18.0)(typescript@5.7.3) + '@typescript-eslint/utils': 8.21.0(eslint@9.18.0)(typescript@5.7.3) + '@typescript-eslint/visitor-keys': 8.21.0 + eslint: 9.18.0 graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 - ts-api-utils: 1.4.3(typescript@5.7.2) - typescript: 5.7.2 + ts-api-utils: 2.0.0(typescript@5.7.3) + typescript: 5.7.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.18.1(eslint@9.17.0)(typescript@5.7.2)': + '@typescript-eslint/parser@8.21.0(eslint@9.18.0)(typescript@5.7.3)': dependencies: - '@typescript-eslint/scope-manager': 8.18.1 - '@typescript-eslint/types': 8.18.1 - '@typescript-eslint/typescript-estree': 8.18.1(typescript@5.7.2) - '@typescript-eslint/visitor-keys': 8.18.1 + '@typescript-eslint/scope-manager': 8.21.0 + '@typescript-eslint/types': 8.21.0 + '@typescript-eslint/typescript-estree': 8.21.0(typescript@5.7.3) + '@typescript-eslint/visitor-keys': 8.21.0 debug: 4.4.0 - eslint: 9.17.0 - typescript: 5.7.2 + eslint: 9.18.0 + typescript: 5.7.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.18.1': + '@typescript-eslint/scope-manager@8.21.0': dependencies: - '@typescript-eslint/types': 8.18.1 - '@typescript-eslint/visitor-keys': 8.18.1 + '@typescript-eslint/types': 8.21.0 + '@typescript-eslint/visitor-keys': 8.21.0 - '@typescript-eslint/type-utils@8.18.1(eslint@9.17.0)(typescript@5.7.2)': + '@typescript-eslint/type-utils@8.21.0(eslint@9.18.0)(typescript@5.7.3)': dependencies: - '@typescript-eslint/typescript-estree': 8.18.1(typescript@5.7.2) - '@typescript-eslint/utils': 8.18.1(eslint@9.17.0)(typescript@5.7.2) + '@typescript-eslint/typescript-estree': 8.21.0(typescript@5.7.3) + '@typescript-eslint/utils': 8.21.0(eslint@9.18.0)(typescript@5.7.3) debug: 4.4.0 - eslint: 9.17.0 - ts-api-utils: 1.4.3(typescript@5.7.2) - typescript: 5.7.2 + eslint: 9.18.0 + ts-api-utils: 2.0.0(typescript@5.7.3) + typescript: 5.7.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.18.1': {} + '@typescript-eslint/types@8.21.0': {} - '@typescript-eslint/typescript-estree@8.18.1(typescript@5.7.2)': + '@typescript-eslint/typescript-estree@8.21.0(typescript@5.7.3)': dependencies: - '@typescript-eslint/types': 8.18.1 - '@typescript-eslint/visitor-keys': 8.18.1 + '@typescript-eslint/types': 8.21.0 + '@typescript-eslint/visitor-keys': 8.21.0 debug: 4.4.0 - fast-glob: 3.3.2 + fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 semver: 7.6.3 - ts-api-utils: 1.4.3(typescript@5.7.2) - typescript: 5.7.2 + ts-api-utils: 2.0.0(typescript@5.7.3) + typescript: 5.7.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.18.1(eslint@9.17.0)(typescript@5.7.2)': + '@typescript-eslint/utils@8.21.0(eslint@9.18.0)(typescript@5.7.3)': dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.17.0) - '@typescript-eslint/scope-manager': 8.18.1 - '@typescript-eslint/types': 8.18.1 - '@typescript-eslint/typescript-estree': 8.18.1(typescript@5.7.2) - eslint: 9.17.0 - typescript: 5.7.2 + '@eslint-community/eslint-utils': 4.4.1(eslint@9.18.0) + '@typescript-eslint/scope-manager': 8.21.0 + '@typescript-eslint/types': 8.21.0 + '@typescript-eslint/typescript-estree': 8.21.0(typescript@5.7.3) + eslint: 9.18.0 + typescript: 5.7.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.18.1': + '@typescript-eslint/visitor-keys@8.21.0': dependencies: - '@typescript-eslint/types': 8.18.1 + '@typescript-eslint/types': 8.21.0 eslint-visitor-keys: 4.2.0 acorn-jsx@5.3.2(acorn@8.14.0): @@ -1183,31 +1211,33 @@ snapshots: deep-is@0.1.4: {} - esbuild@0.21.5: + esbuild@0.24.2: optionalDependencies: - '@esbuild/aix-ppc64': 0.21.5 - '@esbuild/android-arm': 0.21.5 - '@esbuild/android-arm64': 0.21.5 - '@esbuild/android-x64': 0.21.5 - '@esbuild/darwin-arm64': 0.21.5 - '@esbuild/darwin-x64': 0.21.5 - '@esbuild/freebsd-arm64': 0.21.5 - '@esbuild/freebsd-x64': 0.21.5 - '@esbuild/linux-arm': 0.21.5 - '@esbuild/linux-arm64': 0.21.5 - '@esbuild/linux-ia32': 0.21.5 - '@esbuild/linux-loong64': 0.21.5 - '@esbuild/linux-mips64el': 0.21.5 - '@esbuild/linux-ppc64': 0.21.5 - '@esbuild/linux-riscv64': 0.21.5 - '@esbuild/linux-s390x': 0.21.5 - '@esbuild/linux-x64': 0.21.5 - '@esbuild/netbsd-x64': 0.21.5 - '@esbuild/openbsd-x64': 0.21.5 - '@esbuild/sunos-x64': 0.21.5 - '@esbuild/win32-arm64': 0.21.5 - '@esbuild/win32-ia32': 0.21.5 - '@esbuild/win32-x64': 0.21.5 + '@esbuild/aix-ppc64': 0.24.2 + '@esbuild/android-arm': 0.24.2 + '@esbuild/android-arm64': 0.24.2 + '@esbuild/android-x64': 0.24.2 + '@esbuild/darwin-arm64': 0.24.2 + '@esbuild/darwin-x64': 0.24.2 + '@esbuild/freebsd-arm64': 0.24.2 + '@esbuild/freebsd-x64': 0.24.2 + '@esbuild/linux-arm': 0.24.2 + '@esbuild/linux-arm64': 0.24.2 + '@esbuild/linux-ia32': 0.24.2 + '@esbuild/linux-loong64': 0.24.2 + '@esbuild/linux-mips64el': 0.24.2 + '@esbuild/linux-ppc64': 0.24.2 + '@esbuild/linux-riscv64': 0.24.2 + '@esbuild/linux-s390x': 0.24.2 + '@esbuild/linux-x64': 0.24.2 + '@esbuild/netbsd-arm64': 0.24.2 + '@esbuild/netbsd-x64': 0.24.2 + '@esbuild/openbsd-arm64': 0.24.2 + '@esbuild/openbsd-x64': 0.24.2 + '@esbuild/sunos-x64': 0.24.2 + '@esbuild/win32-arm64': 0.24.2 + '@esbuild/win32-ia32': 0.24.2 + '@esbuild/win32-x64': 0.24.2 escape-string-regexp@4.0.0: {} @@ -1220,15 +1250,15 @@ snapshots: eslint-visitor-keys@4.2.0: {} - eslint@9.17.0: + eslint@9.18.0: dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.17.0) + '@eslint-community/eslint-utils': 4.4.1(eslint@9.18.0) '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.19.1 - '@eslint/core': 0.9.1 + '@eslint/core': 0.10.0 '@eslint/eslintrc': 3.2.0 - '@eslint/js': 9.17.0 - '@eslint/plugin-kit': 0.2.4 + '@eslint/js': 9.18.0 + '@eslint/plugin-kit': 0.2.5 '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.1 @@ -1279,7 +1309,7 @@ snapshots: fast-deep-equal@3.1.3: {} - fast-glob@3.3.2: + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 '@nodelib/fs.walk': 1.2.8 @@ -1291,7 +1321,7 @@ snapshots: fast-levenshtein@2.0.6: {} - fastq@1.17.1: + fastq@1.18.0: dependencies: reusify: 1.0.4 @@ -1430,7 +1460,7 @@ snapshots: picomatch@2.3.1: {} - postcss@8.4.49: + postcss@8.5.1: dependencies: nanoid: 3.3.8 picocolors: 1.1.1 @@ -1446,29 +1476,29 @@ snapshots: reusify@1.0.4: {} - rollup@4.29.1: + rollup@4.31.0: dependencies: '@types/estree': 1.0.6 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.29.1 - '@rollup/rollup-android-arm64': 4.29.1 - '@rollup/rollup-darwin-arm64': 4.29.1 - '@rollup/rollup-darwin-x64': 4.29.1 - '@rollup/rollup-freebsd-arm64': 4.29.1 - '@rollup/rollup-freebsd-x64': 4.29.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.29.1 - '@rollup/rollup-linux-arm-musleabihf': 4.29.1 - '@rollup/rollup-linux-arm64-gnu': 4.29.1 - '@rollup/rollup-linux-arm64-musl': 4.29.1 - '@rollup/rollup-linux-loongarch64-gnu': 4.29.1 - '@rollup/rollup-linux-powerpc64le-gnu': 4.29.1 - '@rollup/rollup-linux-riscv64-gnu': 4.29.1 - '@rollup/rollup-linux-s390x-gnu': 4.29.1 - '@rollup/rollup-linux-x64-gnu': 4.29.1 - '@rollup/rollup-linux-x64-musl': 4.29.1 - '@rollup/rollup-win32-arm64-msvc': 4.29.1 - '@rollup/rollup-win32-ia32-msvc': 4.29.1 - '@rollup/rollup-win32-x64-msvc': 4.29.1 + '@rollup/rollup-android-arm-eabi': 4.31.0 + '@rollup/rollup-android-arm64': 4.31.0 + '@rollup/rollup-darwin-arm64': 4.31.0 + '@rollup/rollup-darwin-x64': 4.31.0 + '@rollup/rollup-freebsd-arm64': 4.31.0 + '@rollup/rollup-freebsd-x64': 4.31.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.31.0 + '@rollup/rollup-linux-arm-musleabihf': 4.31.0 + '@rollup/rollup-linux-arm64-gnu': 4.31.0 + '@rollup/rollup-linux-arm64-musl': 4.31.0 + '@rollup/rollup-linux-loongarch64-gnu': 4.31.0 + '@rollup/rollup-linux-powerpc64le-gnu': 4.31.0 + '@rollup/rollup-linux-riscv64-gnu': 4.31.0 + '@rollup/rollup-linux-s390x-gnu': 4.31.0 + '@rollup/rollup-linux-x64-gnu': 4.31.0 + '@rollup/rollup-linux-x64-musl': 4.31.0 + '@rollup/rollup-win32-arm64-msvc': 4.31.0 + '@rollup/rollup-win32-ia32-msvc': 4.31.0 + '@rollup/rollup-win32-x64-msvc': 4.31.0 fsevents: 2.3.3 run-parallel@1.2.0: @@ -1495,15 +1525,15 @@ snapshots: dependencies: is-number: 7.0.0 - ts-api-utils@1.4.3(typescript@5.7.2): + ts-api-utils@2.0.0(typescript@5.7.3): dependencies: - typescript: 5.7.2 + typescript: 5.7.3 type-check@0.4.0: dependencies: prelude-ls: 1.2.1 - typescript@5.7.2: {} + typescript@5.7.3: {} undici-types@6.20.0: {} @@ -1511,13 +1541,13 @@ snapshots: dependencies: punycode: 2.3.1 - vite@5.4.11(@types/node@22.10.2): + vite@6.0.10(@types/node@22.10.7): dependencies: - esbuild: 0.21.5 - postcss: 8.4.49 - rollup: 4.29.1 + esbuild: 0.24.2 + postcss: 8.5.1 + rollup: 4.31.0 optionalDependencies: - '@types/node': 22.10.2 + '@types/node': 22.10.7 fsevents: 2.3.3 which@2.0.2: diff --git a/src/core/types.ts b/src/core/types.ts index beec26c..6a7df2b 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -1,5 +1,46 @@ +/** + * A utility type that allows to define a class constructor of a specific type. + * Is the counterpart of the native `InstanceType` utility type. + * + * ```ts + * function factory(Factory: Constructor): T { [...] } + * + * const instance: MyObject = factory(MyObject); + * ``` + */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export type Constructor = new (...args: P) => T; +/** + * A type that represents the return value of `setInterval` function, + * indipendently from the platform it's currently running on. + * + * For instance, in a browser environment, it's a `number` value representing the interval ID. + * In a Node.js environment, on the other hand, it's an object of type `NodeJS.Timeout`. + * + * This allows to seamlessly use the same code in both environments, without having to deal with the differences: + * + * ```ts + * const intervalId: Interval = setInterval(() => { [...] }, 1_000); + * + * clearInterval(intervalId); + * ``` + */ export type Interval = ReturnType; + +/** + * A type that represents the return value of `setTimeout` function, + * indipendently from the platform it's currently running on. + * + * For instance, in a browser environment, it's a `number` value representing the timeout ID. + * In a Node.js environment, on the other hand, it's an object of type `NodeJS.Timeout`. + * + * This allows to seamlessly use the same code in both environments, without having to deal with the differences: + * + * ```ts + * const timeoutId: Timeout = setTimeout(() => { [...] }, 1_000); + * + * clearTimeout(timeoutId); + * ``` + */ export type Timeout = ReturnType; diff --git a/src/helpers.ts b/src/helpers.ts index 23d1075..d36906d 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,10 +1,19 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ +/** + * An utility constant that indicates whether the current environment is a browser. + */ // @ts-ignore export const isBrowser = ((typeof window !== "undefined") && (typeof window.document !== "undefined")); +/** + * An utility constant that indicates whether the current environment is a Node.js runtime. + */ // @ts-ignore -export const isNode = ((typeof process !== "undefined") && (process.versions?.node)); +export const isNode = ((typeof process !== "undefined") && !!(process.versions?.node)); +/** + * An utility constant that indicates whether the current environment is a Web Worker. + */ // @ts-ignore export const isWebWorker = ((typeof self === "object") && (self.constructor?.name === "DedicatedWorkerGlobalScope")); diff --git a/src/index.ts b/src/index.ts index 09a528f..2739fea 100644 --- a/src/index.ts +++ b/src/index.ts @@ -40,7 +40,11 @@ export { export type { AsyncGeneratorFunction, + AsyncIteratee, AsyncIteratorLike, + AsyncKeyedIteratee, + AsyncKeyedReducer, + AsyncReducer, Callback, FulfilledHandler, GeneratorFunction, @@ -51,22 +55,20 @@ export type { JSONValue, KeyedIteratee, KeyedReducer, - KeyedTypeGuardIteratee, + KeyedTypeGuardPredicate, MaybeAsyncKeyedIteratee, MaybeAsyncKeyedReducer, - MaybeAsyncKeyedTypeGuardIteratee, MaybeAsyncGeneratorFunction, MaybeAsyncIteratee, MaybeAsyncIteratorLike, MaybeAsyncReducer, - MaybeAsyncTypeGuardIteratee, MaybePromise, PromiseExecutor, PromiseRejecter, PromiseResolver, Reducer, RejectedHandler, - TypeGuardIteratee + TypeGuardPredicate } from "./models/types.js"; @@ -82,6 +84,7 @@ export { dateRound, TimeUnit, enumerate, + getWeek, hash, loadScript, nextAnimationFrame, @@ -90,6 +93,7 @@ export { shuffle, sum, unique, + WeekDay, yieldToEventLoop, zip diff --git a/src/models/aggregators/aggregated-async-iterator.ts b/src/models/aggregators/aggregated-async-iterator.ts index b6ae933..95df719 100644 --- a/src/models/aggregators/aggregated-async-iterator.ts +++ b/src/models/aggregators/aggregated-async-iterator.ts @@ -9,24 +9,209 @@ import type { import type { MaybePromise } from "../types.js"; import ReducedIterator from "./reduced-iterator.js"; -import type { MaybeAsyncKeyedIteratee, MaybeAsyncKeyedTypeGuardIteratee, MaybeAsyncKeyedReducer } from "./types.js"; - +import type { MaybeAsyncKeyedIteratee, MaybeAsyncKeyedReducer } from "./types.js"; + +/** + * A class representing an iterator that aggregates elements in a lazy and optimized way. + * + * It's part of the {@link SmartAsyncIterator} implementation, + * providing a way to group elements of an iterable by key. + * For this reason, it isn't recommended to instantiate this class directly + * (although it's still possible), but rather use the {@link SmartAsyncIterator.groupBy} method. + * + * It isn't directly iterable like its parent class but rather needs to specify on what you want to iterate. + * See the {@link AggregatedAsyncIterator.keys}, {@link AggregatedAsyncIterator.entries} + * & {@link AggregatedAsyncIterator.values} methods. + * It does, however, provide the same set of methods to perform + * operations and transformations on the elements of the iterator, + * having also the knowledge and context of the groups to which + * they belong, allowing to handle them in a grouped manner. + * + * This is particularly useful when you need to group elements and + * then perform specific operations on the groups themselves. + * + * ```ts + * const elements = fetch([...]); // Promise<[-3, -1, 0, 2, 3, 5, 6, 8]>; + * const results = new SmartAsyncIterator(elements) + * .groupBy(async (value) => value % 2 === 0 ? "even" : "odd") + * .count(); + * + * console.log(await results.toObject()); // { odd: 4, even: 4 } + * ``` + * + * --- + * + * @template K The type of the keys used to group the elements. + * @template T The type of the elements to aggregate. + */ export default class AggregatedAsyncIterator { + /** + * The internal {@link SmartAsyncIterator} object that holds the elements to aggregate. + */ protected _elements: SmartAsyncIterator<[K, T]>; + /** + * Initializes a new instance of the {@link AggregatedAsyncIterator} class. + * + * ```ts + * const iterator = new AggregatedAsyncIterator([["A", 1], ["B", 2], ["A", 3], ["C", 4], ["B", 5]]); + * ``` + * + * --- + * + * @param iterable The iterable to aggregate. + */ public constructor(iterable: Iterable<[K, T]>); + + /** + * Initializes a new instance of the {@link AggregatedAsyncIterator} class. + * + * ```ts + * const elements = fetch([...]); // Promise<[["A", 1], ["B", 2], ["A", 3], ["C", 4], ["B", 5]]> + * const iterator = new AggregatedAsyncIterator(elements); + * ``` + * + * --- + * + * @param iterable The iterable to aggregate. + */ public constructor(iterable: AsyncIterable<[K, T]>); + + /** + * Initializes a new instance of the {@link AggregatedAsyncIterator} class. + * + * ```ts + * import { Random } from "@byloth/core"; + * + * const iterator = new AggregatedAsyncIterator({ + * _index: 0, + * next: () => + * { + * if (this._index >= 5) { return { done: true, value: undefined }; } + * this._index += 1; + * + * return { done: false, value: [Random.Choice(["A", "B", "C"]), (this._index + 1)] }; + * } + * }); + * ``` + * + * --- + * + * @param iterator The iterator to aggregate. + */ public constructor(iterator: Iterator<[K, T]>); + + /** + * Initializes a new instance of the {@link AggregatedAsyncIterator} class. + * + * ```ts + * import { Random } from "@byloth/core"; + * + * const iterator = new AggregatedAsyncIterator({ + * _index: 0, + * next: async () => + * { + * if (this._index >= 5) { return { done: true, value: undefined }; } + * this._index += 1; + * + * return { done: false, value: [Random.Choice(["A", "B", "C"]), (this._index + 1)] }; + * } + * }); + * ``` + * + * --- + * + * @param iterator The iterator to aggregate. + */ public constructor(iterator: AsyncIterator<[K, T]>); + + /** + * Initializes a new instance of the {@link AggregatedAsyncIterator} class. + * + * ```ts + * import { range, Random } from "@byloth/core"; + * + * const iterator = new AggregatedAsyncIterator(function* () + * { + * for (const index of range(5)) + * { + * yield [Random.Choice(["A", "B", "C"]), (index + 1)]; + * } + * }); + * ``` + * + * --- + * + * @param generatorFn The generator function to aggregate. + */ public constructor(generatorFn: GeneratorFunction<[K, T]>); + + /** + * Initializes a new instance of the {@link AggregatedAsyncIterator} class. + * + * ```ts + * import { range, Random } from "@byloth/core"; + * + * const iterator = new AggregatedAsyncIterator(async function* () + * { + * for await (const index of range(5)) + * { + * yield [Random.Choice(["A", "B", "C"]), (index + 1)]; + * } + * }); + * ``` + * + * --- + * + * @param generatorFn The generator function to aggregate. + */ public constructor(generatorFn: AsyncGeneratorFunction<[K, T]>); + + /** + * Initializes a new instance of the {@link AggregatedAsyncIterator} class. + * + * ```ts + * const iterator = new AggregatedAsyncIterator(asyncKeyedValues); + * ``` + * + * --- + * + * @param argument The iterable, iterator or generator function to aggregate. + */ public constructor(argument: MaybeAsyncIteratorLike<[K, T]> | MaybeAsyncGeneratorFunction<[K, T]>); public constructor(argument: MaybeAsyncIteratorLike<[K, T]> | MaybeAsyncGeneratorFunction<[K, T]>) { this._elements = new SmartAsyncIterator(argument); } + /** + * Determines whether all elements of each group of the iterator satisfy a given condition. + * See also {@link AggregatedAsyncIterator.some}. + * This method will consume the entire iterator in the process. + * + * It will iterate over all elements of the iterator checjing if they satisfy the condition. + * Once a single element of one group doesn't satisfy the condition, + * the result for the respective group will be `false`. + * + * Eventually, it will return a new {@link ReducedIterator} + * object that will contain all the boolean results for each group. + * If the iterator is infinite, the method will never return. + * + * ```ts + * const results = new SmartAsyncIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy(async (value) => value % 2 === 0 ? "even" : "odd") + * .every(async (key, value) => value >= 0); + * + * console.log(await results.toObject()); // { odd: false, even: true } + * ``` + * + * --- + * + * @param predicate The condition to check for each element of the iterator. + * + * @returns A new {@link ReducedIterator} containing the boolean results for each group. + */ public async every(predicate: MaybeAsyncKeyedIteratee): Promise> { const values = new Map(); @@ -45,6 +230,34 @@ export default class AggregatedAsyncIterator for (const [key, [_, result]] of values) { yield [key, result]; } }); } + + /** + * Determines whether any element of each group of the iterator satisfies a given condition. + * See also {@link AggregatedAsyncIterator.every}. + * This method will consume the entire iterator in the process. + * + * It will iterate over all elements of the iterator checjing if they satisfy the condition. + * Once a single element of one group satisfies the condition, + * the result for the respective group will be `true`. + * + * Eventually, it will return a new {@link ReducedIterator} + * object that will contain all the boolean results for each group. + * If the iterator is infinite, the method will never return. + * + * ```ts + * const results = new SmartAsyncIterator([-5, -4, -3, -2, -1, 0]) + * .groupBy(async (value) => value % 2 === 0 ? "even" : "odd") + * .some(async (key, value) => value >= 0); + * + * console.log(await results.toObject()); // { odd: false, even: true } + * ``` + * + * --- + * + * @param predicate The condition to check for each element of the iterator. + * + * @returns A new {@link ReducedIterator} containing the boolean results for each group. + */ public async some(predicate: MaybeAsyncKeyedIteratee): Promise> { const values = new Map(); @@ -64,8 +277,63 @@ export default class AggregatedAsyncIterator }); } + /** + * Filters the elements of the iterator based on a given condition. + * + * This method will iterate over all elements of the iterator checking if they satisfy the condition. + * If the condition is met, the element will be included in the new iterator. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const results = new SmartAsyncIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy(async (value) => value % 2 === 0 ? "even" : "odd") + * .filter(async (key, value) => value >= 0); + * + * console.log(await results.toObject()); // { odd: [3, 5], even: [0, 2, 6, 8] } + * ``` + * + * --- + * + * @param predicate The condition to check for each element of the iterator. + * + * @returns A new {@link AggregatedAsyncIterator} containing the elements that satisfy the condition. + */ public filter(predicate: MaybeAsyncKeyedIteratee): AggregatedAsyncIterator; - public filter(predicate: MaybeAsyncKeyedTypeGuardIteratee): AggregatedAsyncIterator; + + /** + * Filters the elements of the iterator based on a given condition. + * + * This method will iterate over all elements of the iterator checking if they satisfy the condition. + * If the condition is met, the element will be included in the new iterator. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const results = new SmartAsyncIterator([-3, "-1", 0, "2", "3", 5, 6, "8"]) + * .groupBy(async (value) => value % 2 === 0 ? "even" : "odd") + * .filter(async (key, value) => typeof value === "number"); + * + * console.log(await results.toObject()); // { odd: [-3, 5], even: [0, 6] } + * ``` + * + * --- + * + * @template S + * The type of the elements that satisfy the condition. + * This allows the type-system to infer the correct type of the new iterator. + * + * It must be a subtype of the original type of the elements. + * + * @param predicate The type guard condition to check for each element of the iterator. + * + * @returns A new {@link AggregatedAsyncIterator} containing the elements that satisfy the condition. + */ + public filter(predicate: MaybeAsyncKeyedIteratee): AggregatedAsyncIterator; public filter(predicate: MaybeAsyncKeyedIteratee): AggregatedAsyncIterator { const elements = this._elements; @@ -84,6 +352,36 @@ export default class AggregatedAsyncIterator } }); } + + /** + * Maps the elements of the iterator using a given transformation function. + * + * This method will iterate over all elements of the iterator applying the condition. + * The result of each transformation will be included in the new iterator. + * + * Since the iterator is lazy, the mapping process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const results = new SmartAsyncIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy(async (value) => value % 2 === 0 ? "even" : "odd") + * .map(async (key, value) => Math.abs(value)); + * + * console.log(await results.toObject()); // { odd: [3, 1, 3, 5], even: [0, 2, 6, 8] } + * ``` + * + * --- + * + * @template V The type of the elements after the transformation. + * + * @param iteratee The transformation function to apply to each element of the iterator. + * + * @returns A new {@link AggregatedAsyncIterator} containing the transformed elements. + */ public map(iteratee: MaybeAsyncKeyedIteratee): AggregatedAsyncIterator { const elements = this._elements; @@ -102,11 +400,107 @@ export default class AggregatedAsyncIterator } }); } + + /** + * Reduces the elements of the iterator using a given reducer function. + * This method will consume the entire iterator in the process. + * + * It will iterate over all elements of the iterator applying the reducer function. + * The result of each iteration will be passed as the accumulator to the next one. + * + * The first accoumulator value will be the first element of the iterator. + * The last accumulator value will be the final result of the reduction. + * + * Eventually, it will return a new {@link ReducedIterator} + * object that will contain all the reduced results for each group. + * If the iterator is infinite, the method will never return. + * + * ```ts + * const results = new SmartAsyncIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy(async (value) => value % 2 === 0 ? "even" : "odd") + * .reduce(async (key, accumulator, value) => accumulator + value); + * + * console.log(await results.toObject()); // { odd: 4, even: 16 } + * ``` + * + * --- + * + * @param reducer The reducer function to apply to each element of the iterator. + * + * @returns A new {@link ReducedIterator} containing the reduced results for each group. + */ public async reduce(reducer: MaybeAsyncKeyedReducer): Promise>; + + /** + * Reduces the elements of the iterator using a given reducer function. + * This method will consume the entire iterator in the process. + * + * It will iterate over all elements of the iterator applying the reducer function. + * The result of each iteration will be passed as the accumulator to the next one. + * + * The first accoumulator value will be the provided initial value. + * The last accumulator value will be the final result of the reduction. + * + * Eventually, it will return a new {@link ReducedIterator} + * object that will contain all the reduced results for each group. + * If the iterator is infinite, the method will never return. + * + * ```ts + * const results = new SmartAsyncIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy(async (value) => value % 2 === 0 ? "even" : "odd") + * .reduce(async (key, accumulator, value) => accumulator + value, 0); + * + * console.log(await results.toObject()); // { odd: 4, even: 16 } + * ``` + * + * --- + * + * @template A The type of the accumulator value which will also be the final result of the reduction. + * + * @param reducer The reducer function to apply to each element of the iterator. + * @param initialValue The initial value for the accumulator. + * + * @returns A new {@link ReducedIterator} containing the reduced results for each group. + */ + public async reduce(reducer: MaybeAsyncKeyedReducer, initialValue: MaybePromise) + : Promise>; + + /** + * Reduces the elements of the iterator using a given reducer function. + * This method will consume the entire iterator in the process. + * + * It will iterate over all elements of the iterator applying the reducer function. + * The result of each iteration will be passed as the accumulator to the next one. + * + * The first accoumulator value will be the provided initial value by the given function. + * The last accumulator value will be the final result of the reduction. + * + * Eventually, it will return a new {@link ReducedIterator} + * object that will contain all the reduced results for each group. + * If the iterator is infinite, the method will never return. + * + * ```ts + * const results = new SmartAsyncIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy(async (value) => value % 2 === 0 ? "even" : "odd") + * .reduce(async (key, { value }, currentValue) => ({ value: value + currentValue }), (key) => ({ value: 0 })); + * + * console.log(await results.toObject()); // { odd: { value: 4 }, even: { value: 16 } } + * ``` + * + * --- + * + * @template A The type of the accumulator value which will also be the final result of the reduction. + * + * @param reducer The reducer function to apply to each element of the iterator. + * @param initialValue The function that provides the initial value for the accumulator. + * + * @returns A new {@link ReducedIterator} containing the reduced results for each group. + */ public async reduce(reducer: MaybeAsyncKeyedReducer, initialValue: (key: K) => MaybePromise) : Promise>; - public async reduce(reducer: MaybeAsyncKeyedReducer, initialValue?: (key: K) => MaybePromise) - : Promise> + public async reduce( + reducer: MaybeAsyncKeyedReducer, initialValue?: MaybePromise | ((key: K) => MaybePromise) + ): Promise> { const values = new Map(); @@ -119,7 +513,9 @@ export default class AggregatedAsyncIterator else if (initialValue !== undefined) { index = 0; - accumulator = await initialValue(key); + + if (initialValue instanceof Function) { accumulator = await initialValue(key); } + else { accumulator = await initialValue; } } else { @@ -137,7 +533,36 @@ export default class AggregatedAsyncIterator }); } - public flatMap(iteratee: MaybeAsyncKeyedIteratee>): AggregatedAsyncIterator + /** + * Flattens the elements of the iterator using a given transformation function. + * + * This method will iterate over all elements of the iterator applying the transformation function. + * The result of each transformation will be included in the new iterator. + * + * Since the iterator is lazy, the mapping process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const results = new SmartAsyncIterator([[-3, -1], 0, 2, 3, 5, [6, 8]]) + * .groupBy(async ([value, _]) => value % 2 === 0 ? "even" : "odd") + * .flatMap(async (key, values) => values); + * + * console.log(await results.toObject()); // { odd: [-3, -1, 3, 5], even: [0, 2, 6, 8] } + * ``` + * + * --- + * + * @template V The type of the elements after the transformation. + * + * @param iteratee The transformation function to apply to each element of the iterator. + * + * @returns A new {@link AggregatedAsyncIterator} containing the transformed elements. + */ + public flatMap(iteratee: MaybeAsyncKeyedIteratee): AggregatedAsyncIterator { const elements = this._elements; @@ -150,13 +575,43 @@ export default class AggregatedAsyncIterator const index = indexes.get(key) ?? 0; const values = await iteratee(key, element, index); - for await (const value of values) { yield [key, value]; } + if (values instanceof Array) + { + for (const value of values) { yield [key, value]; } + } + else { yield [key, values]; } indexes.set(key, index + 1); } }); } + /** + * Drops a given number of elements from the beginning of each group of the iterator. + * The remaining elements will be included in the new iterator. + * See also {@link AggregatedAsyncIterator.take}. + * + * Since the iterator is lazy, the dropping process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const results = new SmartAsyncIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy(async (value) => value % 2 === 0 ? "even" : "odd") + * .drop(2); + * + * console.log(await results.toObject()); // { odd: [3, 5], even: [6, 8] } + * ``` + * + * --- + * + * @param count The number of elements to drop from the beginning of each group. + * + * @returns A new {@link AggregatedAsyncIterator} containing the remaining elements. + */ public drop(count: number): AggregatedAsyncIterator { const elements = this._elements; @@ -179,6 +634,33 @@ export default class AggregatedAsyncIterator } }); } + + /** + * Takes a given number of elements from the beginning of each group of the iterator. + * The elements will be included in the new iterator. + * See also {@link AggregatedAsyncIterator.drop}. + * + * Since the iterator is lazy, the taking process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const results = new SmartAsyncIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy(async (value) => value % 2 === 0 ? "even" : "odd") + * .take(2); + * + * console.log(await results.toObject()); // { odd: [-3, -1], even: [0, 2] } + * ``` + * + * --- + * + * @param count The number of elements to take from the beginning of each group. + * + * @returns A new {@link AggregatedAsyncIterator} containing the taken elements. + */ public take(limit: number): AggregatedAsyncIterator { const elements = this._elements; @@ -199,8 +681,67 @@ export default class AggregatedAsyncIterator }); } + /** + * Finds the first element of each group of the iterator that satisfies a given condition. + * This method will consume the entire iterator in the process. + * + * It will iterate over all elements of the iterator checking if they satisfy the condition. + * Once the first element of one group satisfies the condition, + * the result for the respective group will be the element itself. + * + * Eventually, it will return a new {@link ReducedIterator} + * object that will contain the first element that satisfies the condition for each group. + * If the iterator is infinite, the method will never return. + * + * ```ts + * const results = new SmartAsyncIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy(async (value) => value % 2 === 0 ? "even" : "odd") + * .find(async (key, value) => value > 0); + * + * console.log(await results.toObject()); // { odd: 3, even: 2 } + * ``` + * + * --- + * + * @param predicate The condition to check for each element of the iterator. + * + * @returns A new {@link ReducedIterator} containing the first element that satisfies the condition for each group. + */ public async find(predicate: MaybeAsyncKeyedIteratee): Promise>; - public async find(predicate: MaybeAsyncKeyedTypeGuardIteratee) + + /** + * Finds the first element of each group of the iterator that satisfies a given condition. + * This method will consume the entire iterator in the process. + * + * It will iterate over all elements of the iterator checking if they satisfy the condition. + * Once the first element of one group satisfies the condition, + * the result for the respective group will be the element itself. + * + * Eventually, it will return a new {@link ReducedIterator} + * object that will contain the first element that satisfies the condition for each group. + * If the iterator is infinite, the method will never return. + * + * ```ts + * const results = new SmartAsyncIterator([-3, "-1", 0, "2", "3", 5, 6, "8"]) + * .groupBy(async (value) => Number(value) % 2 === 0 ? "even" : "odd") + * .find(async (key, value) => typeof value === "number"); + * + * console.log(await results.toObject()); // { odd: -3, even: 0 } + * ``` + * + * --- + * + * @template S + * The type of the elements that satisfy the condition. + * This allows the type-system to infer the correct type of the new iterator. + * + * It must be a subtype of the original type of the elements. + * + * @param predicate The type guard condition to check for each element of the iterator. + * + * @returns A new {@link ReducedIterator} containing the first element that satisfies the condition for each group. + */ + public async find(predicate: MaybeAsyncKeyedIteratee) : Promise>; public async find(predicate: MaybeAsyncKeyedIteratee): Promise> @@ -223,6 +764,57 @@ export default class AggregatedAsyncIterator }); } + /** + * Enumerates the elements of the iterator. + * Each element is paired with its index within the group in the new iterator. + * + * Since the iterator is lazy, the enumeration process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const results = new SmartAsyncIterator([-3, 0, 2, -1, 3]) + * .groupBy(async (value) => value % 2 === 0 ? "even" : "odd") + * .enumerate(); + * + * console.log(results.toObject()); // { odd: [[0, -3], [1, -1], [2, 3]], even: [[0, 0], [1, 2]] } + * ``` + * + * --- + * + * @returns A new {@link AggregatedAsyncIterator} containing the enumerated elements. + */ + public enumerate(): AggregatedAsyncIterator + { + return this.map((key, value, index) => [index, value]); + } + + /** + * Removes all duplicate elements from within each group of the iterator. + * The first occurrence of each element will be included in the new iterator. + * + * Since the iterator is lazy, the deduplication process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const results = new SmartAsyncIterator([-3, -1, 0, 2, 3, 6, -3, -1, 0, 5, 6, 8, 0, 2]) + * .groupBy(async (value) => value % 2 === 0 ? "even" : "odd") + * .unique(); + * + * console.log(await results.toObject()); // { odd: [-3, -1, 3, 5], even: [0, 2, 6, 8] } + * ``` + * + * --- + * + * @returns A new {@link AggregatedAsyncIterator} containing only the unique elements. + */ public unique(): AggregatedAsyncIterator { const elements = this._elements; @@ -245,6 +837,24 @@ export default class AggregatedAsyncIterator }); } + /** + * Counts the number of elements within each group of the iterator. + * This method will consume the entire iterator in the process. + * + * If the iterator is infinite, the method will never return. + * + * ```ts + * const results = new SmartAsyncIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy(async (value) => value % 2 === 0 ? "even" : "odd") + * .count(); + * + * console.log(await results.toObject()); // { odd: 4, even: 4 } + * ``` + * + * --- + * + * @returns A new {@link ReducedIterator} containing the number of elements for each group. + */ public async count(): Promise> { const counters = new Map(); @@ -262,6 +872,27 @@ export default class AggregatedAsyncIterator }); } + /** + * Iterates over the elements of the iterator. + * The elements are passed to the given iteratee function along with their key and index within the group. + * + * This method will consume the entire iterator in the process. + * If the iterator is infinite, the method will never return. + * + * ```ts + * const aggregator = new SmartAsyncIterator([-3, 0, 2, -1, 3]) + * .groupBy(async (value) => value % 2 === 0 ? "even" : "odd"); + * + * await aggregator.forEach(async (key, value, index) => + * { + * console.log(`${index}: ${value}`); // "0: -3", "0: 0", "1: 2", "1: -1", "2: 3" + * }; + * ``` + * + * --- + * + * @param iteratee The function to execute for each element of the iterator. + */ public async forEach(iteratee: MaybeAsyncKeyedIteratee): Promise { const indexes = new Map(); @@ -270,12 +901,83 @@ export default class AggregatedAsyncIterator { const index = indexes.get(key) ?? 0; - iteratee(key, element, index); + await iteratee(key, element, index); indexes.set(key, index + 1); } } + /** + * Changes the key of each element on which the iterator is aggregated. + * The new key is determined by the given iteratee function. + * + * Since the iterator is lazy, the reorganization process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const results = new SmartAsyncIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy(async (value) => value % 2 === 0 ? "even" : "odd") + * .map(async (key, value, index) => index % 2 === 0 ? value : -value) + * .reorganizeBy(async (key, value) => value >= 0 ? "+" : "-"); + * + * console.log(await results.toObject()); // { "+": [1, 0, 3, 6], "-": [-3, -2, -5, -8] } + * ``` + * + * --- + * + * @template J The type of the new key. + * + * @param iteratee The function to determine the new key for each element of the iterator. + * + * @returns A new {@link AggregatedAsyncIterator} containing the elements reorganized by the new keys. + */ + public reorganizeBy(iteratee: MaybeAsyncKeyedIteratee) + : AggregatedAsyncIterator + { + const elements = this._elements; + + return new AggregatedAsyncIterator(async function* (): AsyncGenerator<[J, T]> + { + const indexes = new Map(); + + for await (const [key, element] of elements) + { + const index = indexes.get(key) ?? 0; + + yield [await iteratee(key, element, index), element]; + + indexes.set(key, index + 1); + } + }); + } + + /** + * An utility method that returns a new {@link SmartAsyncIterator} + * object containing all the keys of the iterator. + * + * Since the iterator is lazy, the keys will be extracted + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const keys = new SmartAsyncIterator([-3, Symbol(), "A", { }, null, [1 , 2, 3], false]) + * .groupBy(async (value) => typeof value) + * .keys(); + * + * console.log(await keys.toArray()); // ["number", "symbol", "string", "object", "boolean"] + * ``` + * + * --- + * + * @returns A new {@link SmartAsyncIterator} containing all the keys of the iterator. + */ public keys(): SmartAsyncIterator { const elements = this._elements; @@ -293,10 +995,59 @@ export default class AggregatedAsyncIterator } }); } - public items(): SmartAsyncIterator<[K, T]> + + /** + * An utility method that returns a new {@link SmartAsyncIterator} + * object containing all the entries of the iterator. + * Each entry is a tuple containing the key and the element. + * + * Since the iterator is lazy, the entries will be extracted + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const entries = new SmartAsyncIterator([-3, 0, 2, -1, 3]) + * .groupBy(async (value) => value % 2 === 0 ? "even" : "odd") + * .entries(); + * + * console.log(await entries.toArray()); // [["odd", -3], ["even", 0], ["even", 2], ["odd", -1], ["odd", 3]] + * ``` + * + * --- + * + * @returns A new {@link SmartAsyncIterator} containing all the entries of the iterator. + */ + public entries(): SmartAsyncIterator<[K, T]> { return this._elements; } + + /** + * An utility method that returns a new {@link SmartAsyncIterator} + * object containing all the values of the iterator. + * + * Since the iterator is lazy, the values will be extracted + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const values = new SmartAsyncIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy(async (value) => value % 2 === 0 ? "even" : "odd") + * .values(); + * + * console.log(await values.toArray()); // [-3, -1, 0, 2, 3, 5, 6, 8] + * ``` + * + * --- + * + * @returns A new {@link SmartAsyncIterator} containing all the values of the iterator. + */ public values(): SmartAsyncIterator { const elements = this._elements; @@ -307,12 +1058,47 @@ export default class AggregatedAsyncIterator }); } + /** + * Materializes the iterator into an array of arrays. + * This method will consume the entire iterator in the process. + * + * If the iterator is infinite, the method will never return. + * + * ```ts + * const aggregator = new SmartAsyncIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy(async (value) => value % 2 === 0 ? "even" : "odd"); + * + * console.log(await aggregator.toArray()); // [[-3, -1, 3, 5], [0, 2, 6, 8]] + * ``` + * + * --- + * + * @returns An {@link Array} of arrays containing the elements of the iterator. + */ public async toArray(): Promise { const map = await this.toMap(); return Array.from(map.values()); } + + /** + * Materializes the iterator into a map. + * This method will consume the entire iterator in the process. + * + * If the iterator is infinite, the method will never return. + * + * ```ts + * const aggregator = new SmartAsyncIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy(async (value) => value % 2 === 0 ? "even" : "odd"); + * + * console.log(await aggregator.toMap()); // Map(2) { "odd" => [-3, -1, 3, 5], "even" => [0, 2, 6, 8] } + * ``` + * + * --- + * + * @returns A {@link Map} containing the elements of the iterator. + */ public async toMap(): Promise> { const groups = new Map(); @@ -327,6 +1113,24 @@ export default class AggregatedAsyncIterator return groups; } + + /** + * Materializes the iterator into an object. + * This method will consume the entire iterator in the process. + * + * If the iterator is infinite, the method will never return. + * + * ```ts + * const aggregator = new SmartAsyncIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy(async (value) => value % 2 === 0 ? "even" : "odd"); + * + * console.log(await aggregator.toObject()); // { odd: [-3, -1, 3, 5], even: [0, 2, 6, 8] } + * ``` + * + * --- + * + * @returns An {@link Object} containing the elements of the iterator. + */ public async toObject(): Promise> { const groups = { } as Record; diff --git a/src/models/aggregators/aggregated-iterator.ts b/src/models/aggregators/aggregated-iterator.ts index bb5fed1..9ba4b92 100644 --- a/src/models/aggregators/aggregated-iterator.ts +++ b/src/models/aggregators/aggregated-iterator.ts @@ -2,21 +2,148 @@ import { SmartIterator } from "../iterators/index.js"; import type { GeneratorFunction, IteratorLike } from "../iterators/types.js"; import ReducedIterator from "./reduced-iterator.js"; -import type { KeyedIteratee, KeyedTypeGuardIteratee, KeyedReducer } from "./types.js"; - +import type { KeyedIteratee, KeyedTypeGuardPredicate, KeyedReducer } from "./types.js"; + +/** + * A class representing an iterator that aggregates elements in a lazy and optimized way. + * + * It's part of the {@link SmartIterator} implementation, providing a way to group elements of an iterable by key. + * For this reason, it isn't recommended to instantiate this class directly + * (although it's still possible), but rather use the {@link SmartIterator.groupBy} method. + * + * It isn't directly iterable like its parent class but rather needs to specify on what you want to iterate. + * See the {@link AggregatedIterator.keys}, {@link AggregatedIterator.entries} + * & {@link AggregatedIterator.values} methods. + * It does, however, provide the same set of methods to perform + * operations and transformation on the elements of the iterator, + * having also the knowledge and context of the groups to which + * they belong, allowing to handle them in a grouped manner. + * + * This is particularly useful when you need to group elements and + * then perform specific operations on the groups themselves. + * + * ```ts + * const results = new SmartIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .count(); + * + * console.log(results.toObject()); // { odd: 4, even: 4 } + * ``` + * + * --- + * + * @template K The type of the keys used to group the elements. + * @template T The type of the elements to aggregate. + */ export default class AggregatedIterator { + /** + * The internal {@link SmartIterator} object that holds the elements to aggregate. + */ protected _elements: SmartIterator<[K, T]>; + /** + * Initializes a new instance of the {@link AggregatedIterator} class. + * + * ```ts + * const iterator = new AggregatedIterator([["A", 1], ["B", 2], ["A", 3], ["C", 4], ["B", 5]]); + * ``` + * + * --- + * + * @param iterable The iterable to aggregate. + */ public constructor(iterable: Iterable<[K, T]>); + + /** + * Initializes a new instance of the {@link AggregatedIterator} class. + * + * ```ts + * import { Random } from "@byloth/core"; + * + * const iterator = new AggregatedIterator({ + * _index: 0, + * next: () => + * { + * if (this._index >= 5) { return { done: true, value: undefined }; } + * this._index += 1; + * + * return { done: false, value: [Random.Choice(["A", "B", "C"]), (this._index + 1)] }; + * } + * }); + * ``` + * + * --- + * + * @param iterator The iterator to aggregate. + */ public constructor(iterator: Iterator<[K, T]>); + + /** + * Initializes a new instance of the {@link AggregatedIterator} class. + * + * ```ts + * import { range, Random } from "@byloth/core"; + * + * const iterator = new AggregatedIterator(function* () + * { + * for (const index of range(5)) + * { + * yield [Random.Choice(["A", "B", "C"]), (index + 1)]; + * } + * }); + * ``` + * + * --- + * + * @param generatorFn The generator function to aggregate. + */ public constructor(generatorFn: GeneratorFunction<[K, T]>); + + /** + * Initializes a new instance of the {@link AggregatedIterator} class. + * + * ```ts + * const iterator = new AggregatedIterator(keyedValues); + * ``` + * + * --- + * + * @param argument The iterable, iterator or generator function to aggregate. + */ public constructor(argument: IteratorLike<[K, T]> | GeneratorFunction<[K, T]>); public constructor(argument: IteratorLike<[K, T]> | GeneratorFunction<[K, T]>) { this._elements = new SmartIterator(argument); } + /** + * Determines whether all elements of each group of the iterator satisfy a given condition. + * See also {@link AggregatedIterator.some}. + * This method will consume the entire iterator in the process. + * + * It will iterate over all elements of the iterator checking if they satisfy the condition. + * Once a single element of one group doesn't satisfy the condition, + * the result for the respective group will be `false`. + * + * Eventually, it will return a new {@link ReducedIterator} + * object that will contain all the boolean results for each group. + * If the iterator is infinite, the method will never return. + * + * ```ts + * const results = new SmartIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .every((key, value) => value >= 0); + * + * console.log(results.toObject()); // { odd: false, even: true } + * ``` + * + * --- + * + * @param predicate The condition to check for each element of the iterator. + * + * @returns A new {@link ReducedIterator} containing the boolean results for each group. + */ public every(predicate: KeyedIteratee): ReducedIterator { const values = new Map(); @@ -35,6 +162,34 @@ export default class AggregatedIterator for (const [key, [_, result]] of values) { yield [key, result]; } }); } + + /** + * Determines whether any elements of each group of the iterator satisfy a given condition. + * See also {@link AggregatedIterator.every}. + * This method will consume the entire iterator in the process. + * + * It will iterate over all elements of the iterator checking if they satisfy the condition. + * Once a single element of one group satisfies the condition, + * the result for the respective group will be `true`. + * + * Eventually, it will return a new {@link ReducedIterator} + * object that will contain all the boolean results for each group. + * If the iterator is infinite, the method will never return. + * + * ```ts + * const results = new SmartIterator([-5, -4, -3, -2, -1, 0]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .some((key, value) => value >= 0); + * + * console.log(results.toObject()); // { odd: false, even: true } + * ``` + * + * --- + * + * @param predicate The condition to check for each element of the iterator. + * + * @returns A {@link ReducedIterator} containing the boolean results for each group. + */ public some(predicate: KeyedIteratee): ReducedIterator { const values = new Map(); @@ -54,8 +209,69 @@ export default class AggregatedIterator }); } + /** + * Filters the elements of the iterator using a given condition. + * + * This method will iterate over all elements of the iterator checking if they satisfy the condition. + * If the condition is met, the element will be included in the new iterator. + * + * Since the iterator is lazy, the filtering process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const results = new SmartIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .filter((key, value) => value >= 0); + * + * console.log(results.toObject()); // { odd: [3, 5], even: [0, 2, 6, 8] } + * ``` + * + * --- + * + * @param predicate The condition to check for each element of the iterator. + * + * @returns A new {@link AggregatedIterator} containing only the elements that satisfy the condition. + */ public filter(predicate: KeyedIteratee): AggregatedIterator; - public filter(predicate: KeyedTypeGuardIteratee): AggregatedIterator; + + /** + * Filters the elements of the iterator using a given condition. + * + * This method will iterate over all elements of the iterator checking if they satisfy the condition. + * If the condition is met, the element will be included in the new iterator. + * + * Since the iterator is lazy, the filtering process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const results = new SmartIterator([-3, "-1", 0, "2", "3", 5, 6, "8"]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .filter((key, value) => typeof value === "number"); + * + * console.log(results.toObject()); // { odd: [-3, 5], even: [0, 6] } + * ``` + * + * --- + * + * @template S + * The type of the elements that satisfy the condition. + * This allows the type-system to infer the correct type of the new iterator. + * + * It must be a subtype of the original type of the elements. + * + * @param predicate The type guard condition to check for each element of the iterator. + * + * @returns A new {@link AggregatedIterator} containing only the elements that satisfy the condition. + */ + public filter(predicate: KeyedTypeGuardPredicate): AggregatedIterator; public filter(predicate: KeyedIteratee): AggregatedIterator { const elements = this._elements; @@ -74,6 +290,36 @@ export default class AggregatedIterator } }); } + + /** + * Maps the elements of the iterator using a given transformation function. + * + * This method will iterate over all elements of the iterator applying the transformation function. + * The result of each transformation will be included in the new iterator. + * + * Since the iterator is lazy, the mapping process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const results = new SmartIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .map((key, value) => Math.abs(value)); + * + * console.log(results.toObject()); // { odd: [3, 1, 3, 5], even: [0, 2, 6, 8] } + * ``` + * + * --- + * + * @template V The type of the elements after the transformation. + * + * @param iteratee The transformation function to apply to each element of the iterator. + * + * @returns A new {@link AggregatedIterator} containing the transformed elements. + */ public map(iteratee: KeyedIteratee): AggregatedIterator { const elements = this._elements; @@ -92,9 +338,103 @@ export default class AggregatedIterator } }); } + + /** + * Reduces the elements of the iterator using a given reducer function. + * This method will consume the entire iterator in the process. + * + * It will iterate over all elements of the iterator applying the reducer function. + * The result of each iteration will be passed as the accumulator to the next one. + * + * The first accumulator value will be the first element of the iterator. + * The last accumulator value will be the final result of the reduction. + * + * Eventually, it will return a new {@link ReducedIterator} + * object that will contain all the reduced results for each group. + * If the iterator is infinite, the method will never return. + * + * ```ts + * const results = new SmartIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .reduce((key, accumulator, value) => accumulator + value); + * + * console.log(results.toObject()); // { odd: 4, even: 16 } + * ``` + * + * --- + * + * @param reducer The reducer function to apply to each element of the iterator. + * + * @returns A new {@link ReducedIterator} containing the reduced results for each group. + */ public reduce(reducer: KeyedReducer): ReducedIterator; + + /** + * Reduces the elements of the iterator using a given reducer function. + * This method will consume the entire iterator in the process. + * + * It will iterate over all elements of the iterator applying the reducer function. + * The result of each iteration will be passed as the accumulator to the next one. + * + * The first accumulator value will be the provided initial value. + * The last accumulator value will be the final result of the reduction. + * + * Eventually, it will return a new {@link ReducedIterator} + * object that will contain all the reduced results for each group. + * If the iterator is infinite, the method will never return. + * + * ```ts + * const results = new SmartIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .reduce((key, accumulator, value) => accumulator + value, 0); + * + * console.log(results.toObject()); // { odd: 4, even: 16 } + * ``` + * + * --- + * + * @template A The type of the accumulator value which will also be the type of the final result of the reduction. + * + * @param reducer The reducer function to apply to each element of the iterator. + * @param initialValue The initial value of the accumulator. + * + * @returns A new {@link ReducedIterator} containing the reduced results for each group. + */ + public reduce(reducer: KeyedReducer, initialValue: A): ReducedIterator; + + /** + * Reduces the elements of the iterator using a given reducer function. + * This method will consume the entire iterator in the process. + * + * It will iterate over all elements of the iterator applying the reducer function. + * The result of each iteration will be passed as the accumulator to the next one. + * + * The first accumulator value will be the provided initial value by the given function. + * The last accumulator value will be the final result of the reduction. + * + * Eventually, it will return a new {@link ReducedIterator} + * object that will contain all the reduced results for each group. + * If the iterator is infinite, the method will never return. + * + * ```ts + * const results = new SmartIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .reduce((key, { value }, currentValue) => ({ value: value + currentValue }), (key) => ({ value: 0 })); + * + * console.log(results.toObject()); // { odd: { value: 4 }, even: { value: 16 } } + * ``` + * + * --- + * + * @template A The type of the accumulator value which will also be the type of the final result of the reduction. + * + * @param reducer The reducer function to apply to each element of the iterator. + * @param initialValue The function that provides the initial value for the accumulator. + * + * @returns A new {@link ReducedIterator} containing the reduced results for each group. + */ public reduce(reducer: KeyedReducer, initialValue: (key: K) => A): ReducedIterator; - public reduce(reducer: KeyedReducer, initialValue?: (key: K) => A): ReducedIterator + public reduce(reducer: KeyedReducer, initialValue?: A | ((key: K) => A)): ReducedIterator { const values = new Map(); @@ -107,7 +447,9 @@ export default class AggregatedIterator else if (initialValue !== undefined) { index = 0; - accumulator = initialValue(key); + + if (initialValue instanceof Function) { accumulator = initialValue(key); } + else { accumulator = initialValue; } } else { @@ -125,7 +467,36 @@ export default class AggregatedIterator }); } - public flatMap(iteratee: KeyedIteratee>): AggregatedIterator + /** + * Flattens the elements of the iterator using a given transformation function. + * + * This method will iterate over all elements of the iterator applying the transformation function. + * The result of each transformation will be included in the new iterator. + * + * Since the iterator is lazy, the flattening process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const results = new SmartIterator([[-3, -1], 0, 2, 3, 5, [6, 8]]) + * .groupBy(([value, _]) => value % 2 === 0 ? "even" : "odd") + * .flatMap((key, values) => values); + * + * console.log(results.toObject()); // { odd: [-3, -1, 3, 5], even: [0, 2, 6, 8] } + * ``` + * + * --- + * + * @template V The type of the elements after the transformation. + * + * @param iteratee The transformation function to apply to each element of the iterator. + * + * @returns A new {@link AggregatedIterator} containing the transformed elements. + */ + public flatMap(iteratee: KeyedIteratee): AggregatedIterator { const elements = this._elements; @@ -138,13 +509,43 @@ export default class AggregatedIterator const index = indexes.get(key) ?? 0; const values = iteratee(key, element, index); - for (const value of values) { yield [key, value]; } + if (values instanceof Array) + { + for (const value of values) { yield [key, value]; } + } + else { yield [key, values]; } indexes.set(key, index + 1); } }); } + /** + * Drops a given number of elements from the beginning of each group of the iterator. + * The remaining elements will be included in the new iterator. + * See also {@link AggregatedIterator.take}. + * + * Since the iterator is lazy, the dropping process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const results = new SmartIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .drop(2); + * + * console.log(results.toObject()); // { odd: [3, 5], even: [6, 8] } + * ``` + * + * --- + * + * @param count The number of elements to drop from the beginning of each group. + * + * @returns A new {@link AggregatedIterator} containing the remaining elements. + */ public drop(count: number): AggregatedIterator { const elements = this._elements; @@ -167,6 +568,33 @@ export default class AggregatedIterator } }); } + + /** + * Takes a given number of elements from the beginning of each group of the iterator. + * The elements will be included in the new iterator. + * See also {@link AggregatedIterator.drop}. + * + * Since the iterator is lazy, the taking process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const results = new SmartIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .take(2); + * + * console.log(results.toObject()); // { odd: [-3, -1], even: [0, 2] } + * ``` + * + * --- + * + * @param count The number of elements to take from the beginning of each group. + * + * @returns A new {@link AggregatedIterator} containing the taken elements. + */ public take(limit: number): AggregatedIterator { const elements = this._elements; @@ -187,8 +615,67 @@ export default class AggregatedIterator }); } + /** + * Finds the first element of each group of the iterator that satisfies a given condition. + * This method will consume the entire iterator in the process. + * + * It will iterate over all elements of the iterator checking if they satisfy the condition. + * Once the first element of one group satisfies the condition, + * the result for the respective group will be the element itself. + * + * Eventually, it will return a new {@link ReducedIterator} + * object that will contain the first element that satisfies the condition for each group. + * If the iterator is infinite, the method will never return. + * + * ```ts + * const results = new SmartIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .find((key, value) => value > 0); + * + * console.log(results.toObject()); // { odd: 3, even: 2 } + * ``` + * + * --- + * + * @param predicate The condition to check for each element of the iterator. + * + * @returns A new {@link ReducedIterator} containing the first element that satisfies the condition for each group. + */ public find(predicate: KeyedIteratee): ReducedIterator; - public find(predicate: KeyedTypeGuardIteratee): ReducedIterator; + + /** + * Finds the first element of each group of the iterator that satisfies a given condition. + * This method will consume the entire iterator in the process. + * + * It will iterate over all elements of the iterator checking if they satisfy the condition. + * Once the first element of one group satisfies the condition, + * the result for the respective group will be the element itself. + * + * Eventually, it will return a new {@link ReducedIterator} + * object that will contain the first element that satisfies the condition for each group. + * If the iterator is infinite, the method will never return. + * + * ```ts + * const results = new SmartIterator([-3, "-1", 0, "2", "3", 5, 6, "8"]) + * .groupBy((value) => Number(value) % 2 === 0 ? "even" : "odd") + * .find((key, value) => typeof value === "number"); + * + * console.log(results.toObject()); // { odd: -3, even: 0 } + * ``` + * + * --- + * + * @template S + * The type of the elements that satisfy the condition. + * This allows the type-system to infer the correct type of the new iterator. + * + * It must be a subtype of the original type of the elements. + * + * @param predicate The type guard condition to check for each element of the iterator. + * + * @returns A new {@link ReducedIterator} containing the first element that satisfies the condition for each group. + */ + public find(predicate: KeyedTypeGuardPredicate): ReducedIterator; public find(predicate: KeyedIteratee): ReducedIterator { const values = new Map(); @@ -209,10 +696,57 @@ export default class AggregatedIterator }); } + /** + * Enumerates the elements of the iterator. + * Each element is paired with its index within the group in a new iterator. + * + * Since the iterator is lazy, the enumeration process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const results = new SmartIterator([-3, 0, 2, -1, 3]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .enumerate(); + * + * console.log(results.toObject()); // { odd: [[0, -3], [1, -1], [2, 3]], even: [[0, 0], [1, 2]] } + * ``` + * + * --- + * + * @returns A new {@link AggregatedIterator} containing the enumerated elements. + */ public enumerate(): AggregatedIterator { return this.map((_, value, index) => [index, value]); } + + /** + * Removes all duplicate elements from within each group of the iterator. + * The first occurrence of each element will be included in the new iterator. + * + * Since the iterator is lazy, the deduplication process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const results = new SmartIterator([-3, -1, 0, 2, 3, 6, -3, -1, 0, 5, 6, 8, 0, 2]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .unique(); + * + * console.log(results.toObject()); // { odd: [-3, -1, 3, 5], even: [0, 2, 6, 8] } + * ``` + * + * --- + * + * @returns A new {@link AggregatedIterator} containing only the unique elements. + */ public unique(): AggregatedIterator { const elements = this._elements; @@ -235,6 +769,24 @@ export default class AggregatedIterator }); } + /** + * Counts the number of elements within each group of the iterator. + * This method will consume the entire iterator in the process. + * + * If the iterator is infinite, the method will never return. + * + * ```ts + * const results = new SmartIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .count(); + * + * console.log(results.toObject()); // { odd: 4, even: 4 } + * ``` + * + * --- + * + * @returns A new {@link ReducedIterator} containing the number of elements for each group. + */ public count(): ReducedIterator { const counters = new Map(); @@ -252,6 +804,27 @@ export default class AggregatedIterator }); } + /** + * Iterates over the elements of the iterator. + * The elements are passed to the given iteratee function along with their key and index within the group. + * + * This method will consume the entire iterator in the process. + * If the iterator is infinite, the method will never return. + * + * ```ts + * const aggregator = new SmartIterator([-3, 0, 2, -1, 3]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd"); + * + * aggregator.forEach((key, value, index) => + * { + * console.log(`${index}: ${value}`); // "0: -3", "0: 0", "1: 2", "1: -1", "2: 3" + * }; + * ``` + * + * --- + * + * @param iteratee The function to execute for each element of the iterator. + */ public forEach(iteratee: KeyedIteratee): void { const indexes = new Map(); @@ -266,6 +839,76 @@ export default class AggregatedIterator } } + /** + * Changes the key of each element on which the iterator is aggregated. + * The new key is determined by the given iteratee function. + * + * Since the iterator is lazy, the reorganization process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const results = new SmartIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .map((key, value, index) => index % 2 === 0 ? value : -value) + * .reorganizeBy((key, value) => value >= 0 ? "+" : "-"); + * + * console.log(results.toObject()); // { "+": [1, 0, 3, 6], "-": [-3, -2, -5, -8] } + * ``` + * + * --- + * + * @template J The type of the new key. + * + * @param iteratee The function to determine the new key for each element of the iterator. + * + * @returns A new {@link AggregatedIterator} containing the elements reorganized by the new keys. + */ + public reorganizeBy(iteratee: KeyedIteratee): AggregatedIterator + { + const elements = this._elements; + + return new AggregatedIterator(function* () + { + const indexes = new Map(); + + for (const [key, element] of elements) + { + const index = indexes.get(key) ?? 0; + + yield [iteratee(key, element, index), element]; + + indexes.set(key, index + 1); + } + }); + } + + /** + * An utility method that returns a new {@link SmartIterator} + * object containing all the keys of the iterator. + * + * Since the iterator is lazy, the keys will be extracted + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const keys = new SmartIterator([-3, Symbol(), "A", { }, null, [1 , 2, 3], false]) + * .groupBy((value) => typeof value) + * .keys(); + * + * console.log(keys.toArray()); // ["number", "symbol", "string", "object", "boolean"] + * ``` + * + * --- + * + * @returns A new {@link SmartIterator} containing all the keys of the iterator. + */ public keys(): SmartIterator { const elements = this._elements; @@ -283,10 +926,59 @@ export default class AggregatedIterator } }); } - public items(): SmartIterator<[K, T]> + + /** + * An utility method that returns a new {@link SmartIterator} + * object containing all the entries of the iterator. + * Each entry is a tuple containing the key and the element. + * + * Since the iterator is lazy, the entries will be extracted + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const entries = new SmartIterator([-3, 0, 2, -1, 3]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .entries(); + * + * console.log(entries.toArray()); // [["odd", -3], ["even", 0], ["even", 2], ["odd", -1], ["odd", 3]] + * ``` + * + * --- + * + * @returns A new {@link SmartIterator} containing all the entries of the iterator. + */ + public entries(): SmartIterator<[K, T]> { return this._elements; } + + /** + * An utility method that returns a new {@link SmartIterator} + * object containing all the values of the iterator. + * + * Since the iterator is lazy, the values will be extracted + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const values = new SmartIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .values(); + * + * console.log(values.toArray()); // [-3, -1, 0, 2, 3, 5, 6, 8] + * ``` + * + * --- + * + * @returns A new {@link SmartIterator} containing all the values of the iterator. + */ public values(): SmartIterator { const elements = this._elements; @@ -297,12 +989,47 @@ export default class AggregatedIterator }); } + /** + * Materializes the iterator into an array of arrays. + * This method will consume the entire iterator in the process. + * + * If the iterator is infinite, the method will never return. + * + * ```ts + * const aggregator = new SmartIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd"); + * + * console.log(aggregator.toArray()); // [[-3, -1, 3, 5], [0, 2, 6, 8]] + * ``` + * + * --- + * + * @returns An {@link Array} of arrays containing the elements of the iterator. + */ public toArray(): T[][] { const map = this.toMap(); return Array.from(map.values()); } + + /** + * Materializes the iterator into a map. + * This method will consume the entire iterator in the process. + * + * If the iterator is infinite, the method will never return. + * + * ```ts + * const aggregator = new SmartIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd"); + * + * console.log(aggregator.toMap()); // Map(2) { "odd" => [-3, -1, 3, 5], "even" => [0, 2, 6, 8] } + * ``` + * + * --- + * + * @returns A {@link Map} containing the elements of the iterator. + */ public toMap(): Map { const groups = new Map(); @@ -317,6 +1044,24 @@ export default class AggregatedIterator return groups; } + + /** + * Materializes the iterator into an object. + * This method will consume the entire iterator in the process. + * + * If the iterator is infinite, the method will never return. + * + * ```ts + * const aggregator = new SmartIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd"); + * + * console.log(aggregator.toObject()); // { odd: [-3, -1, 3, 5], even: [0, 2, 6, 8] } + * ``` + * + * --- + * + * @returns An {@link Object} containing the elements of the iterator. + */ public toObject(): Record { const groups = { } as Record; diff --git a/src/models/aggregators/reduced-iterator.ts b/src/models/aggregators/reduced-iterator.ts index d30b2eb..1ffe238 100644 --- a/src/models/aggregators/reduced-iterator.ts +++ b/src/models/aggregators/reduced-iterator.ts @@ -3,21 +3,148 @@ import { SmartIterator } from "../iterators/index.js"; import type { GeneratorFunction } from "../iterators/types.js"; import AggregatedIterator from "./aggregated-iterator.js"; -import type { KeyedIteratee, KeyedReducer, KeyedTypeGuardIteratee } from "./types.js"; - +import type { KeyedIteratee, KeyedReducer, KeyedTypeGuardPredicate } from "./types.js"; + +/** + * A class representing an aggregated iterator that has been reduced in a lazy and optimized way. + * + * It's part of the {@link AggregatedIterator} and {@link AggregatedAsyncIterator} implementations, + * providing a way to reduce them into a single value or another aggregated iterable. + * For this reason, it isn't recommended to instantiate this class directly + * (although it's still possible), but rather use the reducing methods provided by the aggregated iterators. + * + * It isn't directly iterable, just like its parent class, and needs to specify on what you want to iterate. + * See the {@link ReducedIterator.keys}, {@link ReducedIterator.entries} + * & {@link ReducedIterator.values} methods. + * It does, however, provide the same set of methods to perform + * operations and transformation on the elements of the iterator, + * having also the knowledge and context of the groups to which + * they belong, allowing to handle them in a grouped manner. + * + * This is particularly useful when you have group elements and + * need perform specific operations on the reduced elements. + * + * ```ts + * const results = new SmartIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .count(); + * + * console.log(results.toObject()); // { odd: 4, even: 4 } + * ``` + * + * --- + * + * @template K The type of the key used to group the elements. + * @template T The type of the elements in the iterator. + */ export default class ReducedIterator { + /** + * The internal {@link SmartIterator} object that holds the reduced elements. + */ protected _elements: SmartIterator<[K, T]>; + /** + * Initializes a new instance of the {@link ReducedIterator} class. + * + * ```ts + * const results = new ReducedIterator([["A", 1], ["B", 2], ["C", 4]]); + * ``` + * + * --- + * + * @param iterable A reduced iterable object. + */ public constructor(iterable: Iterable<[K, T]>); + + /** + * Initializes a new instance of the {@link ReducedIterator} class. + * + * ```ts + * const results = new ReducedIterator({ + * _index: 0, + * next: () => + * { + * if (this._index >= 3) { return { done: true, value: undefined }; } + * this._index += 1; + * + * return { done: false, value: [["A", "B", "C"][this._index], (this._index + 1)] }; + * } + * }); + * ``` + * + * --- + * + * @param iterator An reduced iterator object. + */ public constructor(iterator: Iterator<[K, T]>); + + /** + * Initializes a new instance of the {@link ReducedIterator} class. + * + * ```ts + * import { range, Random } from "@byloth/core"; + * + * const results = new ReducedIterator(function* () + * { + * for (const index of range(3)) + * { + * yield [["A", "B", "C"][index], (index + 1)]; + * } + * }); + * ``` + * + * --- + * + * @param generatorFn A generator function that produces the reduced elements. + */ public constructor(generatorFn: GeneratorFunction<[K, T]>); + + /** + * Initializes a new instance of the {@link ReducedIterator} class. + * + * ```ts + * const results = new ReducedIterator(reducedValues); + * ``` + * + * --- + * + * @param argument An iterable, iterator or generator function that produces the reduced elements. + */ public constructor(argument: Iterable<[K, T]> | Iterator<[K, T]> | GeneratorFunction<[K, T]>); public constructor(argument: Iterable<[K, T]> | Iterator<[K, T]> | GeneratorFunction<[K, T]>) { this._elements = new SmartIterator(argument); } + /** + * Determines whether all elements of the reduced iterator satisfy the given condition. + * See also {@link ReducedIterator.some}. + * + * This method will iterate over all the elements of the iterator checking if they satisfy the condition. + * Once a single element doesn't satisfy the condition, the method will return `false` immediately. + * + * This may lead to an unknown final state of the iterator, which may be entirely or partially consumed. + * For this reason, it's recommended to consider it as consumed in any case and to not use it anymore. + * Consider using {@link ReducedIterator.find} instead. + * + * If the iterator is infinite and every element satisfies the condition, the method will never return. + * + * ```ts + * const results = new SmartIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .reduce((key, accumulator, value) => accumulator + value) + * .every((key, value) => value > 0); + * + * console.log(results); // true + * ``` + * + * --- + * + * @param predicate The condition to check for each element of the iterator. + * + * @returns `true` if all elements satisfy the condition, `false` otherwise. + */ public every(predicate: KeyedIteratee): boolean { for (const [index, [key, element]] of this._elements.enumerate()) @@ -27,6 +154,35 @@ export default class ReducedIterator return true; } + + /** + * Determines whether any element of the reduced iterator satisfies the given condition. + * See also {@link ReducedIterator.every}. + * + * This method will iterate over all the elements of the iterator checking if they satisfy the condition. + * Once a single element satisfies the condition, the method will return `true` immediately. + * + * This may lead to an unknown final state of the iterator, which may be entirely or partially consumed. + * For this reason, it's recommended to consider it as consumed in any case and to not use it anymore. + * Consider using {@link ReducedIterator.find} instead. + * + * If the iterator is infinite and no element satisfies the condition, the method will never return. + * + * ```ts + * const results = new SmartIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .reduce((key, accumulator, value) => accumulator + value) + * .some((key, value) => value > 0); + * + * console.log(results); // true + * ``` + * + * --- + * + * @param predicate The condition to check for each element of the iterator. + * + * @returns `true` if any element satisfies the condition, `false` otherwise. + */ public some(predicate: KeyedIteratee): boolean { for (const [index, [key, element]] of this._elements.enumerate()) @@ -37,8 +193,71 @@ export default class ReducedIterator return false; } + /** + * Filters the elements of the reduced iterator using a given condition. + * + * This method will iterate over all the elements of the iterator checking if they satisfy the condition. + * If the condition is met, the element will be included in the new iterator. + * + * Since the iterator is lazy, the filtering process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const results = new SmartIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .reduce((key, accumulator, value) => accumulator + value) + * .filter((key, value) => value > 0); + * + * console.log(results.toObject()); // { odd: 4, even: 16 } + * ``` + * + * --- + * + * @param predicate The condition to check for each element of the iterator. + * + * @returns A new {@link ReducedIterator} containing only the elements that satisfy the condition. + */ public filter(predicate: KeyedIteratee): ReducedIterator; - public filter(predicate: KeyedTypeGuardIteratee): ReducedIterator; + + /** + * Filters the elements of the reduced iterator using a given type guard predicate. + * + * This method will iterate over all the elements of the iterator checking if they satisfy the condition. + * If the condition is met, the element will be included in the new iterator. + * + * Since the iterator is lazy, the filtering process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const results = new SmartIterator([-3, -1, "0", "2", 3, 5, "6", "8"]) + * .groupBy((value) => Number(value) % 2 === 0 ? "even" : "odd") + * .reduce((key, accumulator, value) => accumulator + value) + * .filter((key, value) => typeof value === "number"); + * + * console.log(results.toObject()); // { odd: 4 } + * ``` + * + * --- + * + * @template S + * The type of the elements that satisfy the condition. + * This allows the type-system to infer the correct type of the iterator. + * + * It must be a subtype of the original type of the elements. + * + * @param predicate The type guard condition to check for each element of the iterator. + * + * @returns A new {@link ReducedIterator} containing only the elements that satisfy the condition. + */ + public filter(predicate: KeyedTypeGuardPredicate): ReducedIterator; public filter(predicate: KeyedIteratee): ReducedIterator { const elements = this._elements.enumerate(); @@ -51,6 +270,37 @@ export default class ReducedIterator } }); } + + /** + * Maps the elements of the reduced iterator using a given transformation function. + * + * This method will iterate over all the elements of the iterator applying the transformation function. + * The result of the transformation will be included in the new iterator. + * + * Since the iterator is lazy, the mapping process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const results = new SmartIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .reduce((key, accumulator, value) => accumulator + value) + * .map((key, value) => value * 2); + * + * console.log(results.toObject()); // { odd: 8, even: 32 } + * ``` + * + * --- + * + * @template V The type of the elements after the transformation. + * + * @param iteratee The transformation function to apply to each element of the iterator. + * + * @returns A new {@link ReducedIterator} containing the transformed elements. + */ public map(iteratee: KeyedIteratee): ReducedIterator { const elements = this._elements.enumerate(); @@ -63,7 +313,68 @@ export default class ReducedIterator } }); } + + /** + * Reduces the elements of the reduced iterator using a given reducer function. + * This method will consume the entire iterator in the process. + * + * It will iterate over all the elements of the iterator applying the reducer function. + * The result of each iteration will be passed as the accumulator to the next one. + * + * The first accumulator value will be the first element of the iterator. + * The last accumulator value will be the final result of the reduction. + * + * Also note that: + * - If an empty iterator is provided, a {@link ValueException} will be thrown. + * - If the iterator is infinite, the method will never return. + * + * ```ts + * const result = new SmartIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .reduce((key, accumulator, value) => accumulator + value) + * .reduce((key, accumulator, value) => accumulator + value); + * + * console.log(result); // 20 + * ``` + * + * --- + * + * @param reducer The reducer function to apply to the elements of the iterator. + * + * @returns The final value after reducing all the elements of the iterator. + */ public reduce(reducer: KeyedReducer): T; + + /** + * Reduces the elements of the reduced iterator using a given reducer function. + * This method will consume the entire iterator in the process. + * + * It will iterate over all the elements of the iterator applying the reducer function. + * The result of each iteration will be passed as the accumulator to the next one. + * + * The first accumulator value will be the provided initial value. + * The last accumulator value will be the final result of the reduction. + * + * If the iterator is infinite, the method will never return. + * + * ```ts + * const result = new SmartIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .reduce((key, accumulator, value) => accumulator + value) + * .reduce((key, { value }, currentValue) => ({ value: value + currentValue }), { value: 0 }); + * + * console.log(result); // { value: 20 } + * ``` + * + * --- + * + * @template A The type of the accumulator value which will also be the type of the final result of the reduction. + * + * @param reducer The reducer function to apply to the elements of the iterator. + * @param initialValue The initial value of the accumulator. + * + * @returns The final result of the reduction. + */ public reduce(reducer: KeyedReducer, initialValue: A): A; public reduce(reducer: KeyedReducer, initialValue?: A): A { @@ -88,7 +399,37 @@ export default class ReducedIterator return accumulator; } - public flatMap(iteratee: KeyedIteratee>): AggregatedIterator + /** + * Flattens the elements of the reduced iterator using a given transformation function. + * + * This method will iterate over all the elements of the iterator applying the transformation function. + * The result of each transformation will be flattened into the new iterator. + * + * Since the iterator is lazy, the flattening process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const results = new SmartIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .reduce((key, accumulator, value) => accumulator.concat([value]), () => []) + * .flatMap((key, value) => value); + * + * console.log(results.toObject()); // { odd: [-3, -1, 3, 5], even: [0, 2, 6, 8] } + * ``` + * + * --- + * + * @template V The type of the elements after the transformation. + * + * @param iteratee The transformation function to apply to each element of the iterator. + * + * @returns A new {@link AggregatedIterator} containing the flattened elements. + */ + public flatMap(iteratee: KeyedIteratee): AggregatedIterator { const elements = this._elements.enumerate(); @@ -96,11 +437,47 @@ export default class ReducedIterator { for (const [index, [key, element]] of elements) { - for (const value of iteratee(key, element, index)) { yield [key, value]; } + const values = iteratee(key, element, index); + + if (values instanceof Array) + { + for (const value of values) { yield [key, value]; } + } + else { yield [key, values]; } } }); } + /** + * Drops a given number of elements at the beginning of the reduced iterator. + * The remaining elements will be included in the new iterator. + * See also {@link ReducedIterator.take}. + * + * Since the iterator is lazy, the dropping process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * Only the dropped elements will be consumed in the process. + * The rest of the iterator will be consumed once the new iterator is. + * + * ```ts + * const results = new SmartIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .reduce((key, accumulator, value) => accumulator.concat(value), () => []) + * .drop(1); + * + * console.log(results.toObject()); // { even: [0, 2, 6, 8] } + * ``` + * + * --- + * + * @param count The number of elements to drop. + * + * @returns A new {@link ReducedIterator} containing the remaining elements. + */ public drop(count: number): ReducedIterator { const elements = this._elements.enumerate(); @@ -113,6 +490,39 @@ export default class ReducedIterator } }); } + + /** + * Takes a given number of elements at the beginning of the reduced iterator. + * The elements will be included in the new iterator. + * See also {@link ReducedIterator.drop}. + * + * Since the iterator is lazy, the taking process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * Only the taken elements will be consumed from the original reduced iterator. + * The rest of the original reduced iterator will be available for further consumption. + * + * ```ts + * const reduced = new SmartIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .reduce((key, accumulator, value) => accumulator.concat(value), () => []); + * + * const results = iterator.take(1); + * + * console.log(results.toObject()); // { odd: [-3, -1, 3, 5] } + * console.log(reduced.toObject()); // { even: [0, 2, 6, 8] } + * ``` + * + * --- + * + * @param count The number of elements to take. + * + * @returns A new {@link ReducedIterator} containing the taken elements. + */ public take(count: number): ReducedIterator { const elements = this._elements.enumerate(); @@ -128,10 +538,62 @@ export default class ReducedIterator }); } + public find() + { + // TODO! + } + + /** + * Enumerates the elements of the reduced iterator. + * Each element is paired with its index in a new iterator. + * + * Since the iterator is lazy, the enumeration process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const results = new ReducedIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .reduce((key, accumulator, value) => accumulator + value) + * .enumerate(); + * + * console.log(results.toObject()); // [[0, 4], [1, 16]] + * ``` + * + * --- + * + * @returns A new {@link ReducedIterator} object containing the enumerated elements. + */ public enumerate(): ReducedIterator { return this.map((_, element, index) => [index, element]); } + + /** + * Removes all duplicate elements from the reduced iterator. + * The first occurrence of each element will be kept. + * + * Since the iterator is lazy, the deduplication process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const results = new ReducedIterator([-3, -1, 0, 2, 3, 6, -3, -1, 1, 5, 6, 8, 7, 2]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .map((key, value) => Math.abs(value)) + * .reduce((key, accumulator, value) => accumulator + value) + * .unique(); + * + * console.log(results.toObject()); // { odd: 24 } + * + * @returns A new {@link ReducedIterator} containing only the unique elements. + */ public unique(): ReducedIterator { const elements = this._elements; @@ -150,6 +612,25 @@ export default class ReducedIterator }); } + /** + * Counts the number of elements in the reduced iterator. + * This method will consume the entire iterator in the process. + * + * If the iterator is infinite, the method will never return. + * + * ```ts + * const results = new SmartIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .reduce((key, accumulator, value) => accumulator + value) + * .count(); + * + * console.log(results); // 2 + * ``` + * + * --- + * + * @returns The number of elements in the iterator. + */ public count(): number { let index = 0; @@ -159,6 +640,28 @@ export default class ReducedIterator return index; } + /** + * Iterates over all elements of the reduced iterator. + * The elements are passed to the function along with their key and index. + * + * This method will consume the entire iterator in the process. + * If the iterator is infinite, the method will never return. + * + * ```ts + * const reduced = new SmartIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .reduce((key, accumulator, value) => accumulator + value); + * + * reduced.forEach((key, value, index) => + * { + * console.log(`#${index} - ${key}: ${value}`); // "#0 - odd: 4", "#1 - even: 16" + * }); + * ``` + * + * --- + * + * @param iteratee The function to apply to each element of the reduced iterator. + */ public forEach(iteratee: KeyedIteratee): void { for (const [index, [key, element]] of this._elements.enumerate()) @@ -167,6 +670,71 @@ export default class ReducedIterator } } + /** + * Reaggregates the elements of the reduced iterator. + * The elements are grouped by a new key computed by the given iteratee function. + * + * Since the iterator is lazy, the reorganizing process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const results = new SmartIterator([-3, -1, 0, 2, 3, 5, -6, -8]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .reduce((key, accumulator, value) => accumulator + value) + * .reorganizeBy((key, value) => value > 0 ? "positive" : "negative"); + * + * console.log(results.toObject()); // { positive: 4, negative: -12 } + * ``` + * + * --- + * + * @template J The type of the new keys used to group the elements. + * + * @param iteratee The function to determine the new key of each element of the iterator. + * + * @returns A new {@link AggregatedIterator} containing the elements reorganized by the new keys. + */ + public reorganizeBy(iteratee: KeyedIteratee): AggregatedIterator + { + const elements = this._elements.enumerate(); + + return new AggregatedIterator(function* () + { + for (const [index, [key, element]] of elements) + { + yield [iteratee(key, element, index), element]; + } + }); + } + + /** + * An utility method that returns a new {@link SmartIterator} + * object containing all the keys of the iterator. + * + * Since the iterator is lazy, the keys will be extracted + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const keys = new SmartIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .reduce((key, accumulator, value) => accumulator + value) + * .keys(); + * + * console.log(keys.toArray()); // ["odd", "even"] + * ``` + * + * --- + * + * @returns A new {@link SmartIterator} containing all the keys of the iterator. + */ public keys(): SmartIterator { const elements = this._elements; @@ -179,10 +747,61 @@ export default class ReducedIterator } }); } - public items(): SmartIterator<[K, T]> + + /** + * An utility method that returns a new {@link SmartIterator} + * object containing all the entries of the iterator. + * Each entry is a tuple containing the key and the element. + * + * Since the iterator is lazy, the entries will be extracted + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const entries = new SmartIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .reduce((key, accumulator, value) => accumulator + value) + * .entries(); + * + * console.log(entries.toArray()); // [["odd", 4], ["even", 16]] + * ``` + * + * --- + * + * @returns A new {@link SmartIterator} containing all the entries of the iterator. + */ + public entries(): SmartIterator<[K, T]> { return this._elements; } + + /** + * An utility method that returns a new {@link SmartIterator} + * object containing all the values of the iterator. + * + * Since the iterator is lazy, the values will be extracted + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const values = new SmartIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .reduce((key, accumulator, value) => accumulator + value) + * .values(); + * + * console.log(values.toArray()); // [4, 16] + * ``` + * + * --- + * + * @returns A new {@link SmartIterator} containing all the values of the iterator. + */ public values(): SmartIterator { const elements = this._elements; @@ -196,17 +815,73 @@ export default class ReducedIterator }); } + /** + * Materializes the iterator into an array. + * This method will consume the entire iterator in the process. + * + * If the iterator is infinite, the method will never return. + * + * ```ts + * const reduced = new SmartIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .reduce((key, accumulator, value) => accumulator + value); + * + * console.log(reduced.toArray()); // [4, 16] + * ``` + * + * --- + * + * @returns The {@link Array} containing all elements of the iterator. + */ public toArray(): T[] { return Array.from(this.values()); } + + /** + * Materializes the iterator into a map. + * This method will consume the entire iterator in the process. + * + * If the iterator is infinite, the method will never return. + * + * ```ts + * const reduced = new SmartIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .reduce((key, accumulator, value) => accumulator + value); + * + * console.log(reduced.toMap()); // Map(2) { "odd" => 4, "even" => 16 } + * ``` + * + * --- + * + * @returns The {@link Map} containing all elements of the iterator. + */ public toMap(): Map { - return new Map(this.items()); + return new Map(this.entries()); } + + /** + * Materializes the iterator into an object. + * This method will consume the entire iterator in the process. + * + * If the iterator is infinite, the method will never return. + * + * ```ts + * const reduced = new SmartIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .reduce((key, accumulator, value) => accumulator + value); + * + * console.log(reduced.toObject()); // { odd: 4, even: 16 } + * ``` + * + * --- + * + * @returns The {@link Object} containing all elements of the iterator. + */ public toObject(): Record { - return Object.fromEntries(this.items()) as Record; + return Object.fromEntries(this.entries()) as Record; } public readonly [Symbol.toStringTag]: string = "ReducedIterator"; diff --git a/src/models/aggregators/types.ts b/src/models/aggregators/types.ts index 7236482..7169ca0 100644 --- a/src/models/aggregators/types.ts +++ b/src/models/aggregators/types.ts @@ -1,19 +1,176 @@ -/* eslint-disable max-len */ - import type { MaybePromise } from "../promises/types.js"; +/** + * An utility type that represents an {@link https://en.wikipedia.org/wiki/Iteratee|iteratee}-like function + * with the addition of a `key` parameter, compared to the JavaScript's standard ones. + * It can be used to transform the elements of an aggregated iterable. + * + * ```ts + * const iteratee: KeyedIteratee = (key: string, value: number) => `${value}`; + * const results = new SmartIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .map(iteratee); + * + * console.log(results.toObject()); // { odd: ["-3", "-1", "3", "5"], even: ["0", "2", "6", "8"] } + * ``` + * + * --- + * + * @template K The type of the key used to aggregate elements in the iterable. + * @template T The type of the elements in the iterable. + * @template R The type of the return value of the iteratee. Default is `void`. + */ export type KeyedIteratee = (key: K, value: T, index: number) => R; + +/** + * An utility type that represents an asynchronous {@link https://en.wikipedia.org/wiki/Iteratee|iteratee}-like + * function with the addition of a `key` parameter. + * It can be used to transform the elements of an aggregated iterable asynchronously. + * + * ```ts + * const iteratee: AsyncKeyedIteratee = async (key: string, value: number) => `${value}`; + * const results = new SmartAsyncIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .map(iteratee); + * + * console.log(await results.toObject()); // { odd: ["-3", "-1", "3", "5"], even: ["0", "2", "6", "8"] } + * ``` + * + * --- + * + * @template K The type of the key used to aggregate elements in the iterable. + * @template T The type of the elements in the iterable. + * @template R The type of the return value of the iteratee. Default is `void`. + */ export type AsyncKeyedIteratee = (key: K, value: T, index: number) => Promise; -export type MaybeAsyncKeyedIteratee = (key: K, value: T, index: number) => MaybePromise; -export type KeyedTypeGuardIteratee = (key: K, value: T, index: number) => value is R; +/** + * An utility type that represents an {@link https://en.wikipedia.org/wiki/Iteratee|iteratee}-like function + * with the addition of a `key` parameter that can be either synchronous or asynchronous. + * It can be used to transform the elements of an aggregated iterable. + * + * ```ts + * const iteratee: AsyncKeyedIteratee = [async] (key: string, value: number) => `${value}`; + * const results = new SmartAsyncIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .map(iteratee); + * + * console.log(await results.toObject()); // { odd: ["-3", "-1", "3", "5"], even: ["0", "2", "6", "8"] } + * ``` + * + * --- + * + * @template K The type of the key used to aggregate elements in the iterable. + * @template T The type of the elements in the iterable. + * @template R The type of the return value of the iteratee. Default is `void`. + */ +export type MaybeAsyncKeyedIteratee = + (key: K, value: T, index: number) => MaybePromise; -// @ts-expect-error - This is an asyncronous type guard keyed-iteratee that guarantees the return value is a promise. -export type AsyncKeyedTypeGuardIteratee = (key: K, value: T, index: number) => value is Promise; +/** + * An utility type that represents a {@link https://en.wikipedia.org/wiki/Predicate_(mathematical_logic)|predicate}-like + * function with the addition of a `key` parameter, compared to the JavaScript's standard ones, + * which act as a + * {@link https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates|type guard}. + * It can be used to filter the elements of an aggregated iterable + * while allowing the type-system to infer them correctly. + * + * ```ts + * const predicate: KeyedTypeGuardPredicate = + * (key: string, value: number | string): value is string => typeof value === "string"; + * + * const results = new SmartIterator([-3, -1, "0", 2, 3, "5", 6, "8"]) + * .groupBy((value) => Number(value) % 2 === 0 ? "even" : "odd") + * .filter(predicate); + * + * console.log(results.toObject()); // { odd: ["0", "5", "8"], even: [] } + * ``` + * + * --- + * + * @template K The type of the key used to aggregate elements in the iterable. + * @template T The type of the elements in the iterable. + * @template R + * The type of the return value of the predicate. + * It must be a subtype of `T`. Default is `T`. + */ +export type KeyedTypeGuardPredicate = + (key: K, value: T, index: number) => value is R; -// @ts-expect-error - This may be an asyncronous type guard keyed-iteratee that guarantees the return value may be a promise. -export type MaybeAsyncKeyedTypeGuardIteratee = (key: K, value: T, index: number) => value is MaybePromise; +// These types need this Issue to be solved: https://github.com/microsoft/TypeScript/issues/37681 +// +// export type AsyncKeyedTypeGuardPredicate = +// (key: K, value: T, index: number) => value is Promise; +// export type MaybeAsyncKeyedTypeGuardPredicate = +// (key: K, value: T, index: number) => value is MaybePromise; +/** + * An utility type that represents a reducer-like function. + * It can be used to reduce the elements of an aggregated iterable into a single value. + * + * ```ts + * const sum: KeyedReducer = + * (key: string, accumulator: number, value: number) => accumulator + value; + * + * const results = new SmartIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .reduce(sum); + * + * console.log(results.toObject()); // { odd: 4, even: 16 } + * ``` + * + * --- + * + * @template K The type of the key used to aggregate elements in the iterable. + * @template T The type of the elements in the iterable. + * @template A The type of the accumulator. + */ export type KeyedReducer = (key: K, accumulator: A, value: T, index: number) => A; -export type AsyncKeyedReducer = (key: K, accumulator: A, value: T, index: number) => Promise; -export type MaybeAsyncKeyedReducer = (key: K, accumulator: A, value: T, index: number) => MaybePromise; + +/** + * An utility type that represents an asynchronous reducer-like function. + * It can be used to reduce the elements of an aggregated iterable into a single value. + * + * ```ts + * const sum: AsyncKeyedReducer = + * async (key: string, accumulator: number, value: number) => accumulator + value; + * + * const results = new SmartAsyncIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .reduce(sum); + * + * console.log(await results.toObject()); // { odd: 4, even: 16 } + * ``` + * + * --- + * + * @template K The type of the key used to aggregate elements in the iterable. + * @template T The type of the elements in the iterable. + * @template A The type of the accumulator. + */ +export type AsyncKeyedReducer = + (key: K, accumulator: A, value: T, index: number) => Promise; + +/** + * An utility type that represents a reducer-like function that can be either synchronous or asynchronous. + * It can be used to reduce the elements of an aggregated iterable into a single value. + * + * ```ts + * const sum: MaybeAsyncKeyedReducer = + * [async] (key: string, accumulator: number, value: number) => accumulator + value; + * + * const results = new SmartAsyncIterator([-3, -1, 0, 2, 3, 5, 6, 8]) + * .groupBy((value) => value % 2 === 0 ? "even" : "odd") + * .reduce(sum); + * + * console.log(await results.toObject()); // { odd: 4, even: 16 } + * ``` + * + * --- + * + * @template K The type of the key used to aggregate elements in the iterable. + * @template T The type of the elements in the iterable. + * @template A The type of the accumulator. + */ +export type MaybeAsyncKeyedReducer = + (key: K, accumulator: A, value: T, index: number) => MaybePromise; diff --git a/src/models/callbacks/callable-object.ts b/src/models/callbacks/callable-object.ts index 7cd7646..41e7443 100644 --- a/src/models/callbacks/callable-object.ts +++ b/src/models/callbacks/callable-object.ts @@ -2,12 +2,41 @@ import type { Callback } from "./types.js"; -export const SmartFunction = (Function as unknown) as new(...args: string[]) +const SmartFunction = (Function as unknown) as new(...args: string[]) => (...args: A) => R; +/** + * An abstract class that can be used to implement callable objects. + * + * ```ts + * class ActivableCallback extends CallableObject<(evt: PointerEvent) => void> + * { + * public enabled = false; + * protected _invoke(): void + * { + * if (this.enabled) { [...] } + * } + * } + * + * const callback = new ActivableCallback(); + * + * window.addEventListener("pointerdown", () => { callback.enabled = true; }); + * window.addEventListener("pointermove", callback); + * window.addEventListener("pointerup", () => { callback.enabled = false; }); + * ``` + * + * --- + * + * @template T + * The type signature of the callback function. + * It must be a function. Default is `(...args: any[]) => any`. + */ export default abstract class CallableObject = () => void> extends SmartFunction, ReturnType> { + /** + * Initializes a new instance of the {@link CallableObject} class. + */ public constructor() { super(`return this._invoke(...arguments);`); @@ -18,6 +47,14 @@ export default abstract class CallableObject = () return self as this; } + /** + * The method that will be called when the object is invoked. + * It must be implemented by the derived classes. + * + * @param args The arguments that have been passed to the object. + * + * @returns The return value of the method. + */ protected abstract _invoke(...args: Parameters): ReturnType; public readonly [Symbol.toStringTag]: string = "CallableObject"; diff --git a/src/models/callbacks/index.ts b/src/models/callbacks/index.ts index 437e8fd..af6180d 100644 --- a/src/models/callbacks/index.ts +++ b/src/models/callbacks/index.ts @@ -1,5 +1,5 @@ -import CallableObject, { SmartFunction } from "./callable-object.js"; +import CallableObject from "./callable-object.js"; import Publisher from "./publisher.js"; import SwitchableCallback from "./switchable-callback.js"; -export { CallableObject, Publisher, SmartFunction, SwitchableCallback }; +export { CallableObject, Publisher, SwitchableCallback }; diff --git a/src/models/callbacks/publisher.ts b/src/models/callbacks/publisher.ts index 65cce03..f02ff9e 100644 --- a/src/models/callbacks/publisher.ts +++ b/src/models/callbacks/publisher.ts @@ -2,21 +2,106 @@ import { ReferenceException } from "../exceptions/index.js"; import type { Callback } from "./types.js"; +/** + * A class implementing the + * {@link https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern|Publish-subscribe} pattern. + * + * It can be used to create a simple event system where objects can subscribe + * to events and receive notifications when the events are published. + * It's a simple and efficient way to decouple the objects and make them communicate with each other. + * + * Using generics, it's also possible to define the type of the events and the callbacks that can be subscribed to them. + * + * ```ts + * interface EventsMap + * { + * "player:spawn": (evt: SpawnEvent) => void; + * "player:move": ({ x, y }: Point) => void; + * "player:death": () => void; + * } + * + * const publisher = new Publisher(); + * + * let unsubscribe: () => void; + * publisher.subscribe("player:death", unsubscribe); + * publisher.subscribe("player:spawn", (evt) => + * { + * unsubscribe = publisher.subscribe("player:move", ({ x, y }) => { [...] }); + * }); + * ``` + * + * --- + * + * @template T + * A map containing the names of the emittable events and the + * related callback signatures that can be subscribed to them. + * Default is `Record void>`. + */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export default class Publisher } = Record> { + /** + * A map containing all the subscribers for each event. + * + * The keys are the names of the events they are subscribed to. + * The values are the arrays of the subscribers themselves. + */ protected _subscribers: Map[]>; + /** + * Initializes a new instance of the {@link Publisher} class. + * + * ```ts + * const publisher = new Publisher(); + * ``` + */ public constructor() { this._subscribers = new Map(); } + /** + * Unsubscribes all the subscribers from all the events. + * + * ```ts + * publisher.subscribe("player:spawn", (evt) => { [...] }); + * publisher.subscribe("player:move", (coords) => { [...] }); + * publisher.subscribe("player:move", () => { [...] }); + * publisher.subscribe("player:move", ({ x, y }) => { [...] }); + * publisher.subscribe("player:death", () => { [...] }); + * + * // All these subscribers are working fine... + * + * publisher.clear(); + * + * // ... but now they're all gone! + * ``` + */ public clear(): void { this._subscribers.clear(); } + /** + * Publishes an event to all the subscribers. + * + * ```ts + * publisher.subscribe("player:move", (coords) => { [...] }); + * publisher.subscribe("player:move", ({ x, y }) => { [...] }); + * publisher.subscribe("player:move", (evt) => { [...] }); + * + * publisher.publish("player:move", { x: 10, y: 20 }); + * ``` + * + * --- + * + * @template K The key of the map containing the callback signature to publish. + * + * @param event The name of the event to publish. + * @param args The arguments to pass to the subscribers. + * + * @returns An array containing the return values of all the subscribers. + */ public publish(event: K, ...args: Parameters): ReturnType[] { const subscribers = this._subscribers.get(event); @@ -26,6 +111,27 @@ export default class Publisher .map((subscriber) => subscriber(...args)) as ReturnType[]; } + /** + * Subscribes a new subscriber to an event. + * + * ```ts + * let unsubscribe: () => void; + * publisher.subscribe("player:death", unsubscribe); + * publisher.subscribe("player:spawn", (evt) => + * { + * unsubscribe = publisher.subscribe("player:move", ({ x, y }) => { [...] }); + * }); + * ``` + * + * --- + * + * @template K The key of the map containing the callback signature to subscribe. + * + * @param event The name of the event to subscribe to. + * @param subscriber The subscriber to add to the event. + * + * @returns A function that can be used to unsubscribe the subscriber. + */ public subscribe(event: K, subscriber: T[K]): () => void { if (!(this._subscribers.has(event))) { this._subscribers.set(event, []); } @@ -45,6 +151,24 @@ export default class Publisher subscribers.splice(index, 1); }; } + + /** + * Unsubscribes a subscriber from an event. + * + * ```ts + * const onPlayerMove = ({ x, y }: Point) => { [...] }; + * + * publisher.subscribe("player:spawn", (evt) => publisher.subscribe("player:move", onPlayerMove)); + * publisher.subscribe("player:death", () => publisher.unsubscribe("player:move", onPlayerMove)); + * ``` + * + * --- + * + * @template K The key of the map containing the callback signature to unsubscribe. + * + * @param event The name of the event to unsubscribe from. + * @param subscriber The subscriber to remove from the event. + */ public unsubscribe(event: K, subscriber: T[K]): void { const subscribers = this._subscribers.get(event); diff --git a/src/models/callbacks/switchable-callback.ts b/src/models/callbacks/switchable-callback.ts index 4712ae0..592ce6e 100644 --- a/src/models/callbacks/switchable-callback.ts +++ b/src/models/callbacks/switchable-callback.ts @@ -3,20 +3,85 @@ import { KeyException, NotImplementedException, RuntimeException } from "../exce import CallableObject from "./callable-object.js"; import type { Callback } from "./types.js"; +/** + * A class representing a callback that can be switched between multiple implementations. + * + * It can be used to implement different behaviors for the same event handler, allowing + * it to respond to different states without incurring any overhead during execution. + * + * ```ts + * const onPointerMove = new SwitchableCallback<(evt: PointerEvent) => void>(); + * + * onPointerMove.register("released", () => { [...] }); + * onPointerMove.register("pressed", () => { [...] }); + * + * window.addEventListener("pointerdown", () => { onPointerMove.switch("pressed"); }); + * window.addEventListener("pointermove", onPointerMove); + * window.addEventListener("pointerup", () => { onPointerMove.switch("released"); }); + * ``` + * + * --- + * + * @template T The type signature of the callback. Default is `(...args: any[]) => any`. + */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export default class SwitchableCallback = Callback> extends CallableObject { + /** + * The currently selected implementation of the callback. + */ protected _callback: T; + + /** + * All the implementations that have been registered for the callback. + * + * The keys are the names of the implementations they were registered with. + * The values are the implementations themselves. + */ protected _callbacks: Map; + /** + * A flag indicating whether the callback is enabled or not. + * + * This protected property is the only one that can be modified directly by the derived classes. + * If you're looking for the public and readonly property, use + * the {@link SwitchableCallback.isEnabled} getter instead. + */ protected _isEnabled: boolean; + + /** + * A flag indicating whether the callback is enabled or not. + * + * It indicates whether the callback is currently able to execute the currently selected implementation. + * If it's disabled, the callback will be invoked without executing anything. + */ public get isEnabled(): boolean { return this._isEnabled; } + /** + * The key that is associated with the currently selected implementation. + * + * This protected property is the only one that can be modified directly by the derived classes. + * If you're looking for the public and readonly property, use the {@link SwitchableCallback.key} getter instead. + */ protected _key: string; + + /** + * The key that is associated with the currently selected implementation. + */ public get key(): string { return this._key; } + /** + * The function that will be called by the extended class when the object is invoked as a function. + */ protected readonly _invoke: (...args: Parameters) => ReturnType; + /** + * Initializes a new instance of the {@link SwitchableCallback} class. + * + * ```ts + * const onPointerMove = new SwitchableCallback<(evt: PointerEvent) => void>(); + * ``` + */ public constructor() { const _default = () => @@ -38,6 +103,18 @@ export default class SwitchableCallback = Callbac this._invoke = (...args: Parameters): ReturnType => this._callback(...args); } + /** + * Enables the callback, allowing it to execute the currently selected implementation. + * + * Also note that: + * - If any implementation has been registered yet, a {@link KeyException} will be thrown. + * - If the callback is already enabled, a {@link RuntimeException} will be thrown. + * + * ```ts + * window.addEventListener("pointerdown", () => { onPointerMove.enable(); }); + * window.addEventListener("pointermove", onPointerMove); + * ``` + */ public enable(): void { if (!(this._key)) @@ -55,6 +132,17 @@ export default class SwitchableCallback = Callbac this._callback = this._callbacks.get(this._key)!; this._isEnabled = true; } + + /** + * Disables the callback, allowing it to be invoked without executing any implementation. + * + * If the callback is already disabled, a {@link RuntimeException} will be thrown. + * + * ```ts + * window.addEventListener("pointermove", onPointerMove); + * window.addEventListener("pointerup", () => { onPointerMove.disable(); }); + * ``` + */ public disable(): void { if (!(this._isEnabled)) @@ -67,6 +155,23 @@ export default class SwitchableCallback = Callbac this._isEnabled = false; } + /** + * Registers a new implementation for the callback. + * + * Also note that: + * - If the callback has no other implementation registered yet, this one will be selected as default. + * - If the key has already been used for another implementation, a {@link KeyException} will be thrown. + * + * ```ts + * onPointerMove.register("pressed", () => { [...] }); + * onPointerMove.register("released", () => { [...] }); + * ``` + * + * --- + * + * @param key The key that will be associated with the implementation. + * @param callback The implementation to register. + */ public register(key: string, callback: T): void { if (this._callbacks.size === 0) @@ -81,8 +186,28 @@ export default class SwitchableCallback = Callbac this._callbacks.set(key, callback); } + + /** + * Unregisters an implementation for the callback. + * + * Also note that: + * - If the key is the currently selected implementation, a {@link KeyException} will be thrown. + * - If the key has no associated implementation yet, a {@link KeyException} will be thrown. + * + * ```ts + * onPointerMove.unregister("released"); + * ``` + * + * --- + * + * @param key The key that is associated with the implementation to unregister. + */ public unregister(key: string): void { + if (this._key === key) + { + throw new KeyException("Unable to unregister the currently selected callback."); + } if (!(this._callbacks.has(key))) { throw new KeyException(`The key '${key}' doesn't yet have any associated callback.`); @@ -91,6 +216,21 @@ export default class SwitchableCallback = Callbac this._callbacks.delete(key); } + /** + * Switches the callback to the implementation associated with the given key. + * + * If the key has no associated implementation yet, a {@link KeyException} will be thrown. + * + * ```ts + * window.addEventListener("pointerdown", () => { onPointerMove.switch("pressed"); }); + * window.addEventListener("pointermove", onPointerMove); + * window.addEventListener("pointerup", () => { onPointerMove.switch("released"); }); + * ``` + * + * --- + * + * @param key The key that is associated with the implementation to switch to. + */ public switch(key: string): void { if (!(this._callbacks.has(key))) diff --git a/src/models/callbacks/types.ts b/src/models/callbacks/types.ts index a010aa9..06ddc71 100644 --- a/src/models/callbacks/types.ts +++ b/src/models/callbacks/types.ts @@ -1 +1,19 @@ +/** + * A type that represents a generic function. + * + * It can be used to define the signature of a callback, a event handler or any other function. + * It's simply a shorthand for the `(...args: A) => R` function signature. + * + * ```ts + * const callback: Callback<[PointerEvent]> = (evt: PointerEvent): void => { [...] }; + * ``` + * + * --- + * + * @template A + * The type of the arguments that the function accepts. + * It must be an array of types, even if it's empty. Default is `[]`. + * + * @template R The return type of the function. Default is `void`. + */ export type Callback = (...args: A) => R; diff --git a/src/models/exceptions/core.ts b/src/models/exceptions/core.ts index 6b3c264..13319c3 100644 --- a/src/models/exceptions/core.ts +++ b/src/models/exceptions/core.ts @@ -1,5 +1,47 @@ +/** + * A class representing an exception, subclass of the native `Error` class. + * It's the base class for any other further exception. + * + * It allows to chain exceptions together, tracking the initial cause of an error and + * storing its stack trace while providing a clear and friendly message to the user. + * + * ```ts + * try { loadGameSaves(); } + * catch (error) + * { + * throw new Exception("The game saves may be corrupted. Try to restart the game.", error); + * // Uncaught Exception: The game saves may be corrupted. Try to restart the game. + * // at /src/game/index.ts:37:15 + * // at /src/main.ts:23:17 + * // + * // Caused by SyntaxError: Unexpected end of JSON input + * // at /src/models/saves.ts:47:17 + * // at /src/game/index.ts:12:9 + * // at /src/main.ts:23:17 + * } + * ``` + */ export default class Exception extends Error { + /** + * A static method to convert a generic caught error, ensuring it's an instance of the {@link Exception} class. + * + * ```ts + * try { [...] } + * catch (error) + * { + * const exc = Exception.FromUnknown(error); + * + * [...] + * } + * ``` + * + * --- + * + * @param error The caught error to convert. + * + * @returns An instance of the {@link Exception} class. + */ public static FromUnknown(error: unknown): Exception { if (error instanceof Exception) @@ -19,6 +61,19 @@ export default class Exception extends Error return new Exception(`${error}`); } + /** + * Initializes a new instance of the {@link Exception} class. + * + * ```ts + * throw new Exception("An error occurred while processing the request."); + * ``` + * + * --- + * + * @param message The message that describes the error. + * @param cause The previous caught error that caused this one, if any. + * @param name The name of the exception. Default is `"Exception"`. + */ public constructor(message: string, cause?: unknown, name = "Exception") { super(message); @@ -42,8 +97,40 @@ export default class Exception extends Error public readonly [Symbol.toStringTag]: string = "Exception"; } +/** + * An utility class representing that kind of situation where the program should never reach. + * Also commonly used to satisfy the type-system, but not part of a real feasible scenario. + * + * It provides a clear and friendly message by default. + * + * ```ts + * function checkCase(value: "A" | "B" | "C"): 1 | 2 | 3 + * { + * switch (value) + * { + * case "A": return 1; + * case "B": return 2; + * case "C": return 3; + * default: throw new FatalErrorException(); + * } + * } + * ``` + */ export class FatalErrorException extends Exception { + /** + * Initializes a new instance of the {@link FatalErrorException} class. + * + * ```ts + * throw new FatalErrorException("This error should never happen. Please, contact the support team."); + * ``` + * + * --- + * + * @param message The message that describes the error. + * @param cause The previous caught error that caused this one, if any. + * @param name The name of the exception. Default is `"FatalErrorException"`. + */ public constructor(message?: string, cause?: unknown, name = "FatalErrorException") { if (message === undefined) @@ -57,13 +144,43 @@ export class FatalErrorException extends Exception public override readonly [Symbol.toStringTag]: string = "FatalErrorException"; } + +/** + * An utility class representing a situation where a feature isn't implemented yet. + * It's commonly used as a placeholder for future implementations. + * + * It provides a clear and friendly message by default. + * + * ```ts + * class Database + * { + * public async connect(): Promise + * { + * throw new NotImplementedException(); + * } + * } + * ``` + */ export class NotImplementedException extends FatalErrorException { + /** + * Initializes a new instance of the {@link NotImplementedException} class. + * + * ```ts + * throw new NotImplementedException("This method hasn't been implemented yet. Check back later."); + * ``` + * + * --- + * + * @param message The message that describes the error. + * @param cause The previous caught error that caused this one, if any. + * @param name The name of the exception. Default is `"NotImplementedException"`. + */ public constructor(message?: string, cause?: unknown, name = "NotImplementedException") { if (message === undefined) { - message = "This feature is not implemented yet. Please, try again later."; + message = "This feature isn't implemented yet. Please, try again later."; } super(message, cause, name); diff --git a/src/models/exceptions/index.ts b/src/models/exceptions/index.ts index 0971dc8..bd0e59b 100644 --- a/src/models/exceptions/index.ts +++ b/src/models/exceptions/index.ts @@ -1,7 +1,37 @@ import Exception from "./core.js"; +/** + * A class representing a generic exception that can be thrown when a file + * operation fails, such as reading, writing, copying, moving, deleting, etc... + * + * It can also be used to catch all file-related exceptions at once. + * + * ```ts + * try { [...] } + * catch (error) + * { + * if (error instanceof FileException) + * { + * // A file-related exception occurred. Handle it... + * } + * } + * ``` + */ export class FileException extends Exception { + /** + * Initializes a new instance of the {@link FileException} class. + * + * ```ts + * throw new FileException("An error occurred while trying to read the file."); + * ``` + * + * --- + * + * @param message The message that describes the error. + * @param cause The previous caught error that caused this one, if any. + * @param name The name of the exception. Default is `"FileException"`. + */ public constructor(message: string, cause?: unknown, name = "FileException") { super(message, cause, name); @@ -9,8 +39,34 @@ export class FileException extends Exception public override readonly [Symbol.toStringTag]: string = "FileException"; } + +/** + * A class representing an exception that can be thrown when a file already exists. + * + * ```ts + * import { existsSync } from "node:fs"; + * + * if (existsSync("file.txt")) + * { + * throw new FileExistsException("The file named 'file.txt' already exists."); + * } + * ``` + */ export class FileExistsException extends FileException { + /** + * Initializes a new instance of the {@link FileExistsException} class. + * + * ```ts + * throw new FileExistsException("The file named 'data.json' already exists on the server."); + * ``` + * + * --- + * + * @param message The message that describes the error. + * @param cause The previous caught error that caused this one, if any. + * @param name The name of the exception. Default is `"FileExistsException"`. + */ public constructor(message: string, cause?: unknown, name = "FileExistsException") { super(message, cause, name); @@ -18,8 +74,34 @@ export class FileExistsException extends FileException public override readonly [Symbol.toStringTag]: string = "FileExistsException"; } + +/** + * A class representing an exception that can be thrown when a file isn't found. + * + * ```ts + * import { existsSync } from "node:fs"; + * + * if (!existsSync("file.txt")) + * { + * throw new FileNotFoundException("The file named 'file.txt' wasn't found."); + * } + * ``` + */ export class FileNotFoundException extends FileException { + /** + * Initializes a new instance of the {@link FileNotFoundException} class. + * + * ```ts + * throw new FileNotFoundException("The file named 'data.json' wasn't found on the server."); + * ``` + * + * --- + * + * @param message The message that describes the error. + * @param cause The previous caught error that caused this one, if any. + * @param name The name of the exception. Default is `"FileNotFoundException"`. + */ public constructor(message: string, cause?: unknown, name = "FileNotFoundException") { super(message, cause, name); @@ -28,8 +110,33 @@ export class FileNotFoundException extends FileException public override readonly [Symbol.toStringTag]: string = "FileNotFoundException"; } +/** + * A class representing an exception that can be thrown when a key is invalid or not found. + * It's commonly used when working with dictionaries, maps, objects, sets, etc... + * + * ```ts + * const map = new Map(); + * if (!map.has("hash")) + * { + * throw new KeyException("The key 'hash' wasn't found in the collection."); + * } + * ``` + */ export class KeyException extends Exception { + /** + * Initializes a new instance of the {@link KeyException} class. + * + * ```ts + * throw new KeyException("The 'id' key wasn't found in the dictionary."); + * ``` + * + * --- + * + * @param message The message that describes the error. + * @param cause The previous caught error that caused this one, if any. + * @param name The name of the exception. Default is `"KeyException"`. + */ public constructor(message: string, cause?: unknown, name = "KeyException") { super(message, cause, name); @@ -37,8 +144,42 @@ export class KeyException extends Exception public override readonly [Symbol.toStringTag]: string = "KeyException"; } + +/** + * A class representing an exception that can be thrown when a network operation fails. + * It's commonly used when it's unable to connect to a server or when a request times out. + * + * ```ts + * import axios, { isAxiosError } from "axios"; + * + * try { await axios.get("https://api.example.com/data"); } + * catch (error) + * { + * if (isAxiosError(error) && !error.response) + * { + * throw new NetworkException( + * "Unable to establish a connection to the server. " + + * "Please, check your internet connection and try again." + * ); + * } + * } + * ``` + */ export class NetworkException extends Exception { + /** + * Initializes a new instance of the {@link NetworkException} class. + * + * ```ts + * throw new NetworkException("Couldn't connect to the server. Please, try again later."); + * ``` + * + * --- + * + * @param message The message that describes the error. + * @param cause The previous caught error that caused this one, if any. + * @param name The name of the exception. Default is `"NetworkException"`. + */ public constructor(message: string, cause?: unknown, name = "NetworkException") { super(message, cause, name); @@ -46,8 +187,34 @@ export class NetworkException extends Exception public override readonly [Symbol.toStringTag]: string = "NetworkException"; } + +/** + * A class representing an exception that can be thrown when a permission is denied. + * It's commonly used when a user tries to access a restricted resource or perform a forbidden action. + * + * ```ts + * const $user = useUserStore(); + * if (!$user.isAdmin) + * { + * throw new PermissionException("You don't have permission to perform this action."); + * } + * ``` + */ export class PermissionException extends Exception { + /** + * Initializes a new instance of the {@link PermissionException} class. + * + * ```ts + * throw new PermissionException("You don't have permission to access this resource."); + * ``` + * + * --- + * + * @param message The message that describes the error. + * @param cause The previous caught error that caused this one, if any. + * @param name The name of the exception. Default is `"PermissionException"`. + */ public constructor(message: string, cause?: unknown, name = "PermissionException") { super(message, cause, name); @@ -55,8 +222,34 @@ export class PermissionException extends Exception public override readonly [Symbol.toStringTag]: string = "PermissionException"; } + +/** + * A class representing an exception that can be thrown when a reference is invalid or not found. + * It's commonly used when a variable is `null`, `undefined` or when an object doesn't exist. + * + * ```ts + * const $el = document.getElementById("app"); + * if ($el === null) + * { + * throw new ReferenceException("The element with the ID 'app' wasn't found in the document."); + * } + * ``` + */ export class ReferenceException extends Exception { + /** + * Initializes a new instance of the {@link ReferenceException} class. + * + * ```ts + * throw new ReferenceException("The 'canvas' element wasn't found in the document."); + * ``` + * + * --- + * + * @param message The message that describes the error. + * @param cause The previous caught error that caused this one, if any. + * @param name The name of the exception. Default is `"ReferenceException"`. + */ public constructor(message: string, cause?: unknown, name = "ReferenceException") { super(message, cause, name); @@ -65,8 +258,35 @@ export class ReferenceException extends Exception public override readonly [Symbol.toStringTag]: string = "ReferenceException"; } +/** + * A class representing an exception that can be thrown when a runtime error occurs. + * It's commonly used when an unexpected condition is encountered during the execution of a program. + * + * ```ts + * let status: "enabled" | "disabled" = "enabled"; + * + * function enable(): void + * { + * if (status === "enabled") { throw new RuntimeException("The feature is already enabled."); } + * status = "enabled"; + * } + * ``` + */ export class RuntimeException extends Exception { + /** + * Initializes a new instance of the {@link RuntimeException} class. + * + * ```ts + * throw new RuntimeException("The received input seems to be malformed or corrupted."); + * ``` + * + * --- + * + * @param message The message that describes the error. + * @param cause The previous caught error that caused this one, if any. + * @param name The name of the exception. Default is `"RuntimeException"`. + */ public constructor(message: string, cause?: unknown, name = "RuntimeException") { super(message, cause, name); @@ -74,8 +294,34 @@ export class RuntimeException extends Exception public override readonly [Symbol.toStringTag]: string = "RuntimeException"; } + +/** + * A class representing an exception that can be thrown when an environment + * isn't properly configured or when a required variable isn't set. + * It can also be used when the environment on which the program is running is unsupported. + * + * ```ts + * if (!navigator.geolocation) + * { + * throw new EnvironmentException("The Geolocation API isn't supported in this environment."); + * } + * ``` + */ export class EnvironmentException extends RuntimeException { + /** + * Initializes a new instance of the {@link EnvironmentException} class. + * + * ```ts + * throw new EnvironmentException("The required environment variable 'API_KEY' isn't set."); + * ``` + * + * --- + * + * @param message The message that describes the error. + * @param cause The previous caught error that caused this one, if any. + * @param name The name of the exception. Default is `"EnvironmentException"`. + */ public constructor(message: string, cause?: unknown, name = "EnvironmentException") { super(message, cause, name); @@ -84,8 +330,32 @@ export class EnvironmentException extends RuntimeException public override readonly [Symbol.toStringTag]: string = "EnvironmentException"; } +/** + * A class representing an exception that can be thrown when a timeout occurs. + * It's commonly used when a task takes too long to complete or when a request times out. + * + * ```ts + * const timeoutId = setTimeout(() => { throw new TimeoutException("The request timed out."); }, 5_000); + * const response = await fetch("https://api.example.com/data"); + * + * clearTimeout(timeoutId); + * ``` + */ export class TimeoutException extends Exception { + /** + * Initializes a new instance of the {@link TimeoutException} class. + * + * ```ts + * throw new TimeoutException("The task took too long to complete."); + * ``` + * + * --- + * + * @param message The message that describes the error. + * @param cause The previous caught error that caused this one, if any. + * @param name The name of the exception. Default is `"TimeoutException"`. + */ public constructor(message: string, cause?: unknown, name = "TimeoutException") { super(message, cause, name); @@ -93,8 +363,36 @@ export class TimeoutException extends Exception public override readonly [Symbol.toStringTag]: string = "TimeoutException"; } + +/** + * A class representing an exception that can be thrown when a type is invalid or not supported. + * It's commonly used when a function receives an unexpected type of argument. + * + * ```ts + * function greet(name: string): void + * { + * if (typeof name !== "string") + * { + * throw new TypeException("The 'name' argument must be a valid string."); + * } + * } + * ``` + */ export class TypeException extends Exception { + /** + * Initializes a new instance of the {@link TypeException} class. + * + * ```ts + * throw new TypeException("The 'username' argument must be a valid string."); + * ``` + * + * --- + * + * @param message The message that describes the error. + * @param cause The previous caught error that caused this one, if any. + * @param name The name of the exception. Default is `"TypeException"`. + */ public constructor(message: string, cause?: unknown, name = "TypeException") { super(message, cause, name); @@ -103,8 +401,35 @@ export class TypeException extends Exception public override readonly [Symbol.toStringTag]: string = "TypeException"; } +/** + * A class representing an exception that can be thrown when a value is invalid. + * It's commonly used when a function receives an unexpected value as an argument. + * + * ```ts + * function setVolume(value: number): void + * { + * if (value < 0) + * { + * throw new ValueException("The 'value' argument must be greater than or equal to 0."); + * } + * } + * ``` + */ export class ValueException extends Exception { + /** + * Initializes a new instance of the {@link ValueException} class. + * + * ```ts + * throw new ValueException("The 'grade' argument cannot be negative."); + * ``` + * + * --- + * + * @param message The message that describes the error. + * @param cause The previous caught error that caused this one, if any. + * @param name The name of the exception. Default is `"ValueException"`. + */ public constructor(message: string, cause?: unknown, name = "ValueException") { super(message, cause, name); @@ -112,8 +437,36 @@ export class ValueException extends Exception public override readonly [Symbol.toStringTag]: string = "ValueException"; } + +/** + * A class representing an exception that can be thrown when a value is out of range. + * It's commonly used when a function receives an unexpected value as an argument. + * + * ```ts + * function setVolume(value: number): void + * { + * if ((value < 0) || (value > 100)) + * { + * throw new RangeException("The 'value' argument must be between 0 and 100."); + * } + * } + * ``` + */ export class RangeException extends ValueException { + /** + * Initializes a new instance of the {@link RangeException} class. + * + * ```ts + * throw new RangeException("The 'percentage' argument must be between 0 and 100."); + * ``` + * + * --- + * + * @param message The message that describes the error. + * @param cause The previous caught error that caused this one, if any. + * @param name The name of the exception. Default is `"RangeException"`. + */ public constructor(message: string, cause?: unknown, name = "RangeException") { super(message, cause, name); diff --git a/src/models/game-loop.ts b/src/models/game-loop.ts index 372e11a..87038c3 100644 --- a/src/models/game-loop.ts +++ b/src/models/game-loop.ts @@ -1,32 +1,131 @@ import type { Interval } from "../core/types.js"; import { isBrowser } from "../helpers.js"; +import Publisher from "./callbacks/publisher.js"; import { FatalErrorException, RuntimeException } from "./exceptions/index.js"; +import type { Callback } from "./types.js"; +interface GameLoopEventMap +{ + start: () => void; + stop: () => void; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: Callback; +} + +/** + * A class representing a {@link https://en.wikipedia.org/wiki/Video_game_programming#Game_structure|game loop} pattern + * that allows to run a function at a specific frame rate. + * + * In a browser environment, it uses the native {@link requestAnimationFrame} + * function to run the callback at the refresh rate of the screen. + * In a non-browser environment, however, it uses the {@link setInterval} + * function to run the callback at the specified fixed interval of time. + * + * Every time the callback is executed, it receives the + * elapsed time since the start of the game loop. + * It's also possible to subscribe to the `start` & `stop` events to receive notifications when they occur. + * + * ```ts + * const loop = new GameLoop((elapsedTime: number) => + * { + * console.log(`The game loop has been running for ${elapsedTime}ms.`); + * }); + * + * loop.onStart(() => { console.log("The game loop has started."); }); + * loop.onStop(() => { console.log("The game loop has stopped."); }); + * + * loop.start(); + * ``` + */ export default class GameLoop { + /** + * The handle of the interval or the animation frame, depending on the environment. + * It's used to stop the game loop when the {@link GameLoop._stop} method is called. + */ protected _handle?: number | Interval; + /** + * The time when the game loop has started. + * In addition to indicating the {@link https://en.wikipedia.org/wiki/Unix_time|Unix timestamp} + * of the start of the game loop, it's also used to calculate the elapsed time. + * + * This protected property is the only one that can be modified directly by the derived classes. + * If you're looking for the public and readonly property, use the {@link GameLoop.startTime} getter instead. + */ protected _startTime: number; + + /** + * The time when the game loop has started. + * In addition to indicating the {@link https://en.wikipedia.org/wiki/Unix_time|Unix timestamp} + * of the start of the game loop, it's also used to calculate the elapsed time. + */ public get startTime(): number { return this._startTime; } + /** + * A flag indicating whether the game loop is currently running or not. + * + * This protected property is the only one that can be modified directly by the derived classes. + * If you're looking for the public and readonly property, use the {@link GameLoop.isRunning} getter instead. + */ protected _isRunning: boolean; + + /** + * A flag indicating whether the game loop is currently running or not. + */ public get isRunning(): boolean { return this._isRunning; } + /** + * The elapsed time since the start of the game loop. + * It's calculated as the difference between the current time and the {@link GameLoop.startTime}. + */ public get elapsedTime(): number { return performance.now() - this._startTime; } + /** + * The {@link Publisher} object that will be used to publish the events of the game loop. + */ + protected _publisher: Publisher; + + /** + * The internal method actually responsible for starting the game loop. + * + * Depending on the current environment, it could use the + * {@link requestAnimationFrame} or the {@link setInterval} function. + */ protected _start: () => void; + + /** + * The internal method actually responsible for stopping the game loop. + * + * Depending on the current environment, it could use the + * {@link cancelAnimationFrame} or the {@link clearInterval} function. + */ protected _stop: () => void; + /** + * Initializes a new instance of the {@link GameLoop} class. + * + * ```ts + * const loop = new GameLoop((elapsedTime: number) => { [...] }); + * ``` + * + * --- + * + * @param callback The function that will be executed at each iteration of the game loop. + * @param msIfNotBrowser + * The interval in milliseconds that will be used if the current environment isn't a browser. Default is `40`. + */ public constructor(callback: FrameRequestCallback, msIfNotBrowser = 40) { this._startTime = 0; @@ -58,8 +157,24 @@ export default class GameLoop this._stop = () => clearInterval(this._handle as Interval); } + + this._publisher = new Publisher(); } + /** + * Starts the execution of the game loop. + * + * If the game loop is already running, a {@link RuntimeException} will be thrown. + * + * ```ts + * loop.onStart(() => { [...] }); // This callback will be executed. + * loop.start(); + * ``` + * + * --- + * + * @param elapsedTime The elapsed time to set as default when the game loop starts. Default is `0`. + */ public start(elapsedTime = 0): void { if (this._isRunning) { throw new RuntimeException("The game loop has already been started."); } @@ -67,16 +182,69 @@ export default class GameLoop this._startTime = performance.now() - elapsedTime; this._start(); this._isRunning = true; + + this._publisher.publish("start"); } + /** + * Stops the execution of the game loop. + * + * If the game loop hasn't yet started, a {@link RuntimeException} will be thrown. + * + * ```ts + * loop.onStop(() => { [...] }); // This callback will be executed. + * loop.stop(); + * ``` + */ public stop(): void { - if (!(this._isRunning)) { throw new RuntimeException("The game loop hadn't yet started."); } + if (!(this._isRunning)) + { + throw new RuntimeException("The game loop had already stopped or hadn't yet started."); + } if (!(this._handle)) { throw new FatalErrorException(); } this._stop(); this._handle = undefined; this._isRunning = false; + + this._publisher.publish("stop"); + } + + /** + * Subscribes to the `start` event of the game loop. + * + * ```ts + * loop.onStart(() => { console.log("The game loop has started."); }); + * ``` + * + * --- + * + * @param callback The function that will be executed when the game loop starts. + * + * @returns A function that can be used to unsubscribe from the event. + */ + public onStart(callback: () => void): () => void + { + return this._publisher.subscribe("start", callback); + } + + /** + * Subscribes to the `stop` event of the game loop. + * + * ```ts + * loop.onStop(() => { console.log("The game loop has stopped."); }); + * ``` + * + * --- + * + * @param callback The function that will be executed when the game loop stops. + * + * @returns A function that can be used to unsubscribe from the event. + */ + public onStop(callback: () => void): () => void + { + return this._publisher.subscribe("stop", callback); } public readonly [Symbol.toStringTag]: string = "GameLoop"; diff --git a/src/models/index.ts b/src/models/index.ts index 16526ce..373071f 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -5,7 +5,7 @@ export { } from "./aggregators/index.js"; -export { CallableObject, Publisher, SmartFunction, SwitchableCallback } from "./callbacks/index.js"; +export { CallableObject, Publisher, SwitchableCallback } from "./callbacks/index.js"; export { Exception, FatalErrorException, diff --git a/src/models/iterators/smart-async-iterator.ts b/src/models/iterators/smart-async-iterator.ts index 1702588..2b4629e 100644 --- a/src/models/iterators/smart-async-iterator.ts +++ b/src/models/iterators/smart-async-iterator.ts @@ -7,25 +7,163 @@ import type { MaybeAsyncGeneratorFunction, MaybeAsyncIteratee, MaybeAsyncReducer, - MaybeAsyncIterable, - MaybeAsyncIteratorLike, - MaybeAsyncTypeGuardIteratee + MaybeAsyncIteratorLike } from "./types.js"; +/** + * A wrapper class representing an enhanced and instantiable version + * of the native {@link AsyncIterable} & {@link AsyncIterator} interfaces. + * + * It provides a set of utility methods to better manipulate and transform + * asynchronous iterators in a functional and highly performant way. + * It takes inspiration from the native {@link Array} methods like + * {@link Array.map}, {@link Array.filter}, {@link Array.reduce}, etc... + * + * The class is lazy, meaning that the transformations are applied + * only when the resulting iterator is materialized, not before. + * This allows to chain multiple transformations without + * the need to iterate over the elements multiple times. + * + * ```ts + * const result = new SmartAsyncIterator(["-5", "-4", "-3", "-2", "-1", "0", "1", "2", "3", "4", "5"]) + * .map((value) => Number(value)) + * .map((value) => value + Math.ceil(Math.abs(value / 2))) + * .filter((value) => value >= 0) + * .map((value) => value + 1) + * .reduce((acc, value) => acc + value); + * + * console.log(await result); // 31 + * ``` + * + * --- + * + * @template T The type of elements in the iterator. + * @template R The type of the final result of the iterator. Default is `void`. + * @template N The type of the argument passed to the `next` method. Default is `undefined`. + */ export default class SmartAsyncIterator implements AsyncIterator { + /** + * The native {@link AsyncIterator} object that is being wrapped by this instance. + */ protected _iterator: AsyncIterator; - public return?: (value?: R) => Promise>; - public throw?: (error?: unknown) => Promise>; - + /** + * Initializes a new instance of the {@link SmartAsyncIterator} class. + * + * ```ts + * const iterator = new SmartAsyncIterator(["A", "B", "C"]); + * ``` + * + * --- + * + * @param iterable The iterable object to wrap. + */ public constructor(iterable: Iterable); + + /** + * Initializes a new instance of the {@link SmartAsyncIterator} class. + * + * ```ts + * const iterator = new SmartAsyncIterator([1, 2, 3, 4, 5]); + * ``` + * + * --- + * + * @param iterable The asynchronous iterable object to wrap. + */ public constructor(iterable: AsyncIterable); + + /** + * Initializes a new instance of the {@link SmartAsyncIterator} class. + * + * ```ts + * const iterator = new SmartAsyncIterator({ + * _sum: 0, _count: 0, + * + * next: function (value: number) + * { + * this._sum += value; + * this._count += 1; + * + * return { done: false, value: this._sum / this._count }; + * } + * }) + * ``` + * + * --- + * + * @param iterator The iterator object to wrap. + */ public constructor(iterator: Iterator); + + /** + * Initializes a new instance of the {@link SmartAsyncIterator} class. + * + * ```ts + * const iterator = new SmartAsyncIterator({ + * _sum: 0, _count: 0, + * + * next: async function (value: number) + * { + * this._sum += value; + * this._count += 1; + * + * return { done: false, value: this._sum / this._count }; + * } + * }) + * ``` + * + * --- + * + * @param iterator The asynchronous iterator object to wrap. + */ public constructor(iterator: AsyncIterator); + + /** + * Initializes a new instance of the {@link SmartAsyncIterator} class. + * + * ```ts + * const iterator = new SmartAsyncIterator(function* () + * { + * for (let i = 2; i < 65_536; i *= 2) { yield (i - 1); } + * }); + * ``` + * + * --- + * + * @param generatorFn The generator function to wrap. + */ public constructor(generatorFn: GeneratorFunction); + + /** + * Initializes a new instance of the {@link SmartAsyncIterator} class. + * + * ```ts + * const iterator = new SmartAsyncIterator(async function* () + * { + * for await (let i = 2; i < 65_536; i *= 2) { yield (i - 1); } + * }); + * ``` + * + * --- + * + * @param generatorFn The asynchronous generator function to wrap. + */ public constructor(generatorFn: AsyncGeneratorFunction); + + /** + * Initializes a new instance of the {@link SmartAsyncIterator} class. + * + * ```ts + * const iterator = new SmartAsyncIterator(values); + * ``` + * + * --- + * + * @param argument The synchronous or asynchronous iterable, iterator or generator function to wrap. + */ public constructor(argument: MaybeAsyncIteratorLike | MaybeAsyncGeneratorFunction); public constructor(argument: MaybeAsyncIteratorLike | MaybeAsyncGeneratorFunction) { @@ -88,11 +226,34 @@ export default class SmartAsyncIterator implements A })(); } - - if (this._iterator.return) { this.return = (value?: R) => this._iterator.return!(value); } - if (this._iterator.throw) { this.throw = (error?: unknown) => this._iterator.throw!(error); } } + /** + * Determines whether all elements of the iterator satisfy a given condition. + * See also {@link SmartAsyncIterator.some}. + * + * This method will iterate over all elements of the iterator checking if they satisfy the condition. + * Once a single element doesn't satisfy the condition, the method will return `false` immediately. + * + * This may lead to an unknown final state of the iterator, which may be entirely or partially consumed. + * For this reason, it's recommended to consider it as consumed in any case and to not use it anymore. + * Consider using {@link SmartAsyncIterator.find} instead. + * + * If the iterator is infinite and every element satisfies the condition, the method will never return. + * + * ```ts + * const iterator = new SmartAsyncIterator([-2, -1, 0, 1, 2]); + * const result = await iterator.every(async (value) => value < 0); + * + * console.log(result); // false + * ``` + * + * --- + * + * @param predicate The condition to check for each element of the iterator. + * + * @returns A promise that will resolve to `true` if all elements satisfy the condition, `false` otherwise. + */ public async every(predicate: MaybeAsyncIteratee): Promise { let index = 0; @@ -107,6 +268,33 @@ export default class SmartAsyncIterator implements A index += 1; } } + + /** + * Determines whether any element of the iterator satisfies a given condition. + * See also {@link SmartAsyncIterator.every}. + * + * This method will iterate over all elements of the iterator checking if they satisfy the condition. + * Once a single element satisfies the condition, the method will return `true` immediately. + * + * This may lead to an unknown final state of the iterator, which may be entirely or partially consumed. + * For this reason, it's recommended to consider it as consumed in any case and to not use it anymore. + * Consider using {@link SmartAsyncIterator.find} instead. + * + * If the iterator is infinite and no element satisfies the condition, the method will never return. + * + * ```ts + * const iterator = new SmartAsyncIterator([-2, -1, 0, 1, 2]); + * const result = await iterator.some(async (value) => value > 0); + * + * console.log(result); // true + * ``` + * + * --- + * + * @param predicate The condition to check for each element of the iterator. + * + * @returns A promise that will resolve to `true` if any element satisfies the condition, `false` otherwise. + */ public async some(predicate: MaybeAsyncIteratee): Promise { let index = 0; @@ -122,8 +310,67 @@ export default class SmartAsyncIterator implements A } } + /** + * Filters the elements of the iterator using a given condition. + * + * This method will iterate over all elements of the iterator checking if they satisfy the condition. + * If the condition is met, the element will be included in the new iterator. + * + * Since the iterator is lazy, the filtering process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const iterator = new SmartAsyncIterator([-2, -1, 0, 1, 2]); + * const result = iterator.filter(async (value) => value < 0); + * + * console.log(await result.toArray()); // [-2, -1] + * ``` + * + * --- + * + * @param predicate The condition to check for each element of the iterator. + * + * @returns A new {@link SmartAsyncIterator} containing only the elements that satisfy the condition. + */ public filter(predicate: MaybeAsyncIteratee): SmartAsyncIterator; - public filter(predicate: MaybeAsyncTypeGuardIteratee): SmartAsyncIterator; + + /** + * Filters the elements of the iterator using a given condition. + * + * This method will iterate over all elements of the iterator checking if they satisfy the condition. + * If the condition is met, the element will be included in the new iterator. + * + * Since the iterator is lazy, the filtering process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const iterator = new SmartAsyncIterator([-2, "-1", "0", 1, "2"]); + * const result = iterator.filter(async (value) => typeof value === "number"); + * + * console.log(await result.toArray()); // [-2, 1] + * ``` + * + * --- + * + * @template S + * The type of the elements that satisfy the condition. + * This allows the type-system to infer the correct type of the new iterator. + * + * It must be a subtype of the original type of the elements. + * + * @param predicate The type guard condition to check for each element of the iterator. + * + * @returns A new {@link SmartAsyncIterator} containing only the elements that satisfy the condition. + */ + public filter(predicate: MaybeAsyncIteratee): SmartAsyncIterator; public filter(predicate: MaybeAsyncIteratee): SmartAsyncIterator { const iterator = this._iterator; @@ -143,6 +390,35 @@ export default class SmartAsyncIterator implements A } }); } + + /** + * Maps the elements of the iterator using a given transformation function. + * + * This method will iterate over all elements of the iterator applying the transformation function. + * The result of each transformation will be included in the new iterator. + * + * Since the iterator is lazy, the mapping process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const iterator = new SmartAsyncIterator([-2, -1, 0, 1, 2]); + * const result = iterator.map(async (value) => Math.abs(value)); + * + * console.log(await result.toArray()); // [2, 1, 0, 1, 2] + * ``` + * + * --- + * + * @template V The type of the elements after the transformation. + * + * @param iteratee The transformation function to apply to each element of the iterator. + * + * @returns A new {@link SmartAsyncIterator} containing the transformed elements. + */ public map(iteratee: MaybeAsyncIteratee): SmartAsyncIterator { const iterator = this._iterator; @@ -162,7 +438,64 @@ export default class SmartAsyncIterator implements A } }); } + + /** + * Reduces the elements of the iterator using a given reducer function. + * This method will consume the entire iterator in the process. + * + * It will iterate over all elements of the iterator applying the reducer function. + * The result of each iteration will be passed as the accumulator to the next one. + * + * The first accumulator value will be the first element of the iterator. + * The last accumulator value will be the final result of the reduction. + * + * Also note that: + * - If an empty iterator is provided, a {@link ValueException} will be thrown. + * - If the iterator is infinite, the method will never return. + * + * ```ts + * const iterator = new SmartAsyncIterator([1, 2, 3, 4, 5]); + * const result = await iterator.reduce(async (acc, value) => acc + value); + * + * console.log(result); // 15 + * ``` + * + * --- + * + * @param reducer The reducer function to apply to each element of the iterator. + * + * @returns A promise that will resolve to the final result of the reduction. + */ public async reduce(reducer: MaybeAsyncReducer): Promise; + + /** + * Reduces the elements of the iterator using a given reducer function. + * This method will consume the entire iterator in the process. + * + * It will iterate over all elements of the iterator applying the reducer function. + * The result of each iteration will be passed as the accumulator to the next one. + * + * The first accumulator value will be the provided initial value. + * The last accumulator value will be the final result of the reduction. + * + * If the iterator is infinite, the method will never return. + * + * ```ts + * const iterator = new SmartAsyncIterator([1, 2, 3, 4, 5]); + * const result = await iterator.reduce(async (acc, value) => acc + value, 10); + * + * console.log(result); // 25 + * ``` + * + * --- + * + * @template A The type of the accumulator value which will also be the type of the final result of the reduction. + * + * @param reducer The reducer function to apply to each element of the iterator. + * @param initialValue The initial value of the accumulator. + * + * @returns A promise that will resolve to the final result of the reduction. + */ public async reduce(reducer: MaybeAsyncReducer, initialValue: A): Promise; public async reduce(reducer: MaybeAsyncReducer, initialValue?: A): Promise { @@ -188,7 +521,35 @@ export default class SmartAsyncIterator implements A } } - public flatMap(iteratee: MaybeAsyncIteratee>): SmartAsyncIterator + /** + * Flattens the elements of the iterator using a given transformation function. + * + * This method will iterate over all elements of the iterator applying the transformation function. + * The result of each transformation will be flattened and included in the new iterator. + * + * Since the iterator is lazy, the flattening process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const iterator = new SmartAsyncIterator([[-2, -1], 0, 1, 2, [3, 4, 5]]); + * const result = iterator.flatMap(async (value) => value); + * + * console.log(await result.toArray()); // [-2, -1, 0, 1, 2, 3, 4, 5] + * ``` + * + * --- + * + * @template V The type of the elements after the transformation. + * + * @param iteratee The transformation function to apply to each element of the iterator. + * + * @returns A new {@link SmartAsyncIterator} containing the flattened elements. + */ + public flatMap(iteratee: MaybeAsyncIteratee): SmartAsyncIterator { const iterator = this._iterator; @@ -202,17 +563,45 @@ export default class SmartAsyncIterator implements A if (result.done) { return result.value; } const elements = await iteratee(result.value, index); - - for await (const element of elements) + if (elements instanceof Array) { - yield element; + for (const value of elements) { yield value; } } + else { yield elements; } index += 1; } }); } + /** + * Drops a given number of elements at the beginning of the iterator. + * The remaining elements will be included in a new iterator. + * See also {@link SmartAsyncIterator.take}. + * + * Since the iterator is lazy, the dropping process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * Only the dropped elements will be consumed in the process. + * The rest of the iterator will be consumed only once the new one is. + * + * ```ts + * const iterator = new SmartAsyncIterator([-2, -1, 0, 1, 2]); + * const result = iterator.drop(3); + * + * console.log(await result.toArray()); // [1, 2] + * ``` + * + * --- + * + * @param count The number of elements to drop. + * + * @returns A new {@link SmartAsyncIterator} containing the remaining elements. + */ public drop(count: number): SmartAsyncIterator { const iterator = this._iterator; @@ -238,6 +627,36 @@ export default class SmartAsyncIterator implements A } }); } + + /** + * Takes a given number of elements at the beginning of the iterator. + * These elements will be included in a new iterator. + * See also {@link SmartAsyncIterator.drop}. + * + * Since the iterator is lazy, the taking process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * Only the taken elements will be consumed from the original iterator. + * The rest of the original iterator will be available for further consumption. + * + * ```ts + * const iterator = new SmartAsyncIterator([-2, -1, 0, 1, 2]); + * const result = iterator.take(3); + * + * console.log(await result.toArray()); // [-2, -1, 0] + * console.log(await iterator.toArray()); // [1, 2] + * ``` + * + * --- + * + * @param limit The number of elements to take. + * + * @returns A new {@link SmartAsyncIterator} containing the taken elements. + */ public take(limit: number): SmartAsyncIterator { const iterator = this._iterator; @@ -260,6 +679,69 @@ export default class SmartAsyncIterator implements A }); } + /** + * Finds the first element of the iterator that satisfies a given condition. + * + * This method will iterate over all elements of the iterator checking if they satisfy the condition. + * The first element that satisfies the condition will be returned immediately. + * + * Only the elements that are necessary to find the first + * satisfying one will be consumed from the original iterator. + * The rest of the original iterator will be available for further consumption. + * + * Also note that: + * - If no element satisfies the condition, `undefined` will be returned once the entire iterator is consumed. + * - If the iterator is infinite and no element satisfies the condition, the method will never return. + * + * ```ts + * const iterator = new SmartAsyncIterator([-2, -1, 0, 1, 2]); + * const result = await iterator.find(async (value) => value > 0); + * + * console.log(result); // 1 + * ``` + * + * --- + * + * @param predicate The condition to check for each element of the iterator. + * + * @returns A promise that will resolve to the first element that satisfies the condition, `undefined` otherwise. + */ + public async find(predicate: MaybeAsyncIteratee): Promise; + + /** + * Finds the first element of the iterator that satisfies a given condition. + * + * This method will iterate over all elements of the iterator checking if they satisfy the condition. + * The first element that satisfies the condition will be returned immediately. + * + * Only the elements that are necessary to find the first + * satisfying one will be consumed from the original iterator. + * The rest of the original iterator will be available for further consumption. + * + * Also note that: + * - If no element satisfies the condition, `undefined` will be returned once the entire iterator is consumed. + * - If the iterator is infinite and no element satisfies the condition, the method will never return. + * + * ```ts + * const iterator = new SmartAsyncIterator([-2, "-1", "0", 1, "2"]); + * const result = await iterator.find(async (value) => typeof value === "number"); + * + * console.log(result); // -2 + * ``` + * + * --- + * + * @template S + * The type of the element that satisfies the condition. + * This allows the type-system to infer the correct type of the result. + * + * It must be a subtype of the original type of the elements. + * + * @param predicate The type guard condition to check for each element of the iterator. + * + * @returns A promise that will resolve to the first element that satisfies the condition, `undefined` otherwise. + */ + public async find(predicate: MaybeAsyncIteratee): Promise; public async find(predicate: MaybeAsyncIteratee): Promise { let index = 0; @@ -275,10 +757,58 @@ export default class SmartAsyncIterator implements A } } + /** + * Enumerates the elements of the iterator. + * Each element is be paired with its index in a new iterator. + * + * Since the iterator is lazy, the enumeration process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const iterator = new SmartAsyncIterator(["A", "M", "N", "Z"]); + * const result = iterator.enumerate(); + * + * for await (const [index, value] of result) + * { + * console.log(`${index}: ${value}`); // "0: A", "1: M", "2: N", "3: Z" + * } + * ``` + * + * --- + * + * @returns A new {@link SmartAsyncIterator} containing the enumerated elements. + */ public enumerate(): SmartAsyncIterator<[number, T], R> { return this.map((value, index) => [index, value]); } + + /** + * Removes all duplicate elements from the iterator. + * The first occurrence of each element will be kept. + * + * Since the iterator is lazy, the deduplication process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const iterator = new SmartAsyncIterator([1, 1, 2, 3, 2, 3, 4, 5, 5, 4]); + * const result = iterator.unique(); + * + * console.log(await result.toArray()); // [1, 2, 3, 4, 5] + * ``` + * + * --- + * + * @returns A new {@link SmartAsyncIterator} containing only the unique elements. + */ public unique(): SmartAsyncIterator { const iterator = this._iterator; @@ -301,6 +831,23 @@ export default class SmartAsyncIterator implements A }); } + /** + * Counts the number of elements in the iterator. + * This method will consume the entire iterator in the process. + * + * If the iterator is infinite, the method will never return. + * + * ```ts + * const iterator = new SmartAsyncIterator([1, 2, 3, 4, 5]); + * const result = await iterator.count(); + * + * console.log(result); // 5 + * ``` + * + * --- + * + * @returns A promise that will resolve to the number of elements in the iterator. + */ public async count(): Promise { let index = 0; @@ -313,6 +860,28 @@ export default class SmartAsyncIterator implements A index += 1; } } + + /** + * Iterates over all elements of the iterator. + * The elements are passed to the function along with their index. + * + * This method will consume the entire iterator in the process. + * If the iterator is infinite, the method will never return. + * + * ```ts + * const iterator = new SmartAsyncIterator(["A", "M", "N", "Z"]); + * await iterator.forEach(async (value, index) => + * { + * console.log(`${index}: ${value}`); // "0: A", "1: M", "2: N", "3: Z" + * } + * ``` + * + * --- + * + * @param iteratee The function to apply to each element of the iterator. + * + * @returns A promise that will resolve once the iteration is complete. + */ public async forEach(iteratee: MaybeAsyncIteratee): Promise { let index = 0; @@ -328,11 +897,145 @@ export default class SmartAsyncIterator implements A } } + /** + * Advances the iterator to the next element and returns the result. + * If the iterator requires it, a value must be provided to be passed to the next element. + * + * Once the iterator is done, the method will return an object with the `done` property set to `true`. + * + * ```ts + * const iterator = new SmartAsyncIterator([1, 2, 3, 4, 5]); + * + * let result = await iterator.next(); + * while (!result.done) + * { + * console.log(result.value); // 1, 2, 3, 4, 5 + * + * result = await iterator.next(); + * } + * + * console.log(result); // { done: true, value: undefined } + * ``` + * + * --- + * + * @param values The value to pass to the next element, if required. + * + * @returns A promise that will resolve to the result of the iteration, containing the value of the operation. + */ public next(...values: N extends undefined ? [] : [N]): Promise> { return this._iterator.next(...values); } + /** + * An utility method that may be used to close the iterator gracefully, + * free the resources and perform any cleanup operation. + * It may also be used to signal the end or to compute a specific final result of the iteration process. + * + * ```ts + * const iterator = new SmartAsyncIterator({ + * _index: 0, + * next: async function() + * { + * return { done: false, value: this._index += 1 }; + * }, + * return: async function() { console.log("Closing the iterator..."); } + * }); + * + * for await (const value of iterator) + * { + * if (value > 5) { break; } // Closing the iterator... + * + * console.log(value); // 1, 2, 3, 4, 5 + * } + * ``` + * + * --- + * + * @param value The final value of the iterator. + * + * @returns A promise that will resolve to the final result of the iterator. + */ + public async return(value?: R): Promise> + { + if (this._iterator.return) { return this._iterator.return(value); } + + return { done: true, value: value as R }; + } + + /** + * An utility method that may be used to close the iterator due to an error, + * free the resources and perform any cleanup operation. + * It may also be used to signal that an error occurred during the iteration process or to handle it. + * + * ```ts + * const iterator = new SmartAsyncIterator({ + * _index: 0, + * next: async function() + * { + * return { done: this._index > 10, value: this._index += 1 }; + * }, + * throw: async function(error) + * { + * console.warn(error.message); + * + * this._index = 0; + * } + * }); + * + * for await (const value of iterator) // 1, 2, 3, 4, 5, "The index is too high.", 1, 2, 3, 4, 5, ... + * { + * try + * { + * if (value > 5) { throw new Error("The index is too high."); } + * + * console.log(value); // 1, 2, 3, 4, 5 + * } + * catch (error) { await iterator.throw(error); } + * } + * ``` + * + * --- + * + * @param error The error to throw into the iterator. + * + * @returns A promise that will resolve to the final result of the iterator. + */ + public throw(error: unknown): Promise> + { + if (this._iterator.throw) { return this._iterator.throw(error); } + + throw error; + } + + /** + * An utility method that aggregates the elements of the iterator using a given key function. + * The elements will be grouped by the resulting keys in a new specialized iterator. + * See {@link AggregatedAsyncIterator}. + * + * Since the iterator is lazy, the grouping process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * the new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const iterator = new SmartAsyncIterator([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + * const result = iterator.groupBy(async (value) => value % 2 === 0 ? "even" : "odd"); + * + * console.log(await result.toObject()); // { odd: [1, 3, 5, 7, 9], even: [2, 4, 6, 8, 10] } + * ``` + * + * --- + * + * @template K The type of the keys used to group the elements. + * + * @param iteratee The key function to apply to each element of the iterator. + * + * @returns A new instance of the {@link AggregatedAsyncIterator} class containing the grouped elements. + */ public groupBy(iteratee: MaybeAsyncIteratee): AggregatedAsyncIterator { return new AggregatedAsyncIterator(this.map(async (element, index) => @@ -343,6 +1046,24 @@ export default class SmartAsyncIterator implements A })); } + /** + * Materializes the iterator into an array. + * This method will consume the entire iterator in the process. + * + * If the iterator is infinite, the method will never return. + * + * ```ts + * const iterator = new SmartAsyncIterator(async function* () + * { + * for (let i = 0; i < 5; i += 1) { yield i; } + * }); + * const result = await iterator.toArray(); + * + * console.log(result); // [0, 1, 2, 3, 4] + * ``` + * + * @returns A promise that will resolve to an array containing all elements of the iterator. + */ public toArray(): Promise { return Array.fromAsync(this as AsyncIterable); diff --git a/src/models/iterators/smart-iterator.ts b/src/models/iterators/smart-iterator.ts index 86503e2..4649bce 100644 --- a/src/models/iterators/smart-iterator.ts +++ b/src/models/iterators/smart-iterator.ts @@ -1,18 +1,109 @@ import AggregatedIterator from "../aggregators/aggregated-iterator.js"; import { ValueException } from "../exceptions/index.js"; -import type { GeneratorFunction, Iteratee, TypeGuardIteratee, Reducer, IteratorLike } from "./types.js"; - +import type { GeneratorFunction, Iteratee, TypeGuardPredicate, Reducer, IteratorLike } from "./types.js"; + +/** + * A wrapper class representing an enhanced and instantiable version + * of the native {@link Iterable} & {@link Iterator} interfaces. + * + * It provides a set of utility methods to better manipulate and + * transform iterators in a functional and highly performant way. + * It takes inspiration from the native {@link Array} methods like + * {@link Array.map}, {@link Array.filter}, {@link Array.reduce}, etc... + * + * The class is lazy, meaning that the transformations are applied + * only when the resulting iterator is materialized, not before. + * This allows to chain multiple transformations without + * the need to iterate over the elements multiple times. + * + * ```ts + * const result = new SmartIterator(["-5", "-4", "-3", "-2", "-1", "0", "1", "2", "3", "4", "5"]) + * .map(Number) + * .map((value) => value + Math.ceil(Math.abs(value / 2))) + * .filter((value) => value >= 0) + * .map((value) => value + 1) + * .reduce((acc, value) => acc + value); + * + * console.log(result); // 31 + * ``` + * + * --- + * + * @template T The type of elements in the iterator. + * @template R The type of the final result of the iterator. Default is `void`. + * @template N The type of the argument required by the `next` method. Default is `undefined`. + */ export default class SmartIterator implements Iterator { + /** + * The native {@link Iterator} object that is being wrapped by this instance. + */ protected _iterator: Iterator; - public return?: (value?: R) => IteratorResult; - public throw?: (error?: unknown) => IteratorResult; - + /** + * Initializes a new instance of the {@link SmartIterator} class. + * + * ```ts + * const iterator = new SmartIterator(["A", "B", "C"]); + * ``` + * + * --- + * + * @param iterable The iterable object to wrap. + */ public constructor(iterable: Iterable); + + /** + * Initializes a new instance of the {@link SmartIterator} class. + * + * ```ts + * const iterator = new SmartIterator({ + * _sum: 0, _count: 0, + * + * next: function (value: number) + * { + * this._sum += value; + * this._count += 1; + * + * return { done: false, value: this._sum / this._count }; + * } + * }) + * ``` + * + * --- + * + * @param iterator The iterator object to wrap. + */ public constructor(iterator: Iterator); + + /** + * Initializes a new instance of the {@link SmartIterator} class. + * + * ```ts + * const iterator = new SmartIterator(function* () + * { + * for (let i = 2; i < 65_536; i *= 2) { yield (i - 1); } + * }); + * ``` + * + * --- + * + * @param generatorFn The generator function to wrap. + */ public constructor(generatorFn: GeneratorFunction); + + /** + * Initializes a new instance of the {@link SmartIterator} class. + * + * ```ts + * const iterator = new SmartIterator(values); + * ``` + * + * --- + * + * @param argument The iterable, iterator or generator function to wrap. + */ public constructor(argument: IteratorLike | GeneratorFunction); public constructor(argument: IteratorLike | GeneratorFunction) { @@ -28,11 +119,34 @@ export default class SmartIterator implements Iterat { this._iterator = argument; } - - if (this._iterator.return) { this.return = (value) => this._iterator.return!(value); } - if (this._iterator.throw) { this.throw = (error) => this._iterator.throw!(error); } } + /** + * Determines whether all elements of the iterator satisfy a given condition. + * See also {@link SmartIterator.some}. + * + * This method will iterate over all elements of the iterator checking if they satisfy the condition. + * Once a single element doesn't satisfy the condition, the method will return `false` immediately. + * + * This may lead to an unknown final state of the iterator, which may be entirely or partially consumed. + * For this reason, it's recommended to consider it as consumed in any case and to not use it anymore. + * Consider using {@link SmartIterator.find} instead. + * + * If the iterator is infinite and every element satisfies the condition, the method will never return. + * + * ```ts + * const iterator = new SmartIterator([-2, -1, 0, 1, 2]); + * const result = iterator.every((value) => value < 0); + * + * console.log(result); // false + * ``` + * + * --- + * + * @param predicate The condition to check for each element of the iterator. + * + * @returns `true` if all elements satisfy the condition, `false` otherwise. + */ public every(predicate: Iteratee): boolean { let index = 0; @@ -47,6 +161,33 @@ export default class SmartIterator implements Iterat index += 1; } } + + /** + * Determines whether any element of the iterator satisfies a given condition. + * See also {@link SmartIterator.every}. + * + * This method will iterate over all elements of the iterator checking if they satisfy the condition. + * Once a single element satisfies the condition, the method will return `true` immediately. + * + * This may lead to an unknown final state of the iterator, which may be entirely or partially consumed. + * For this reason, it's recommended to consider it as consumed in any case and to not use it anymore. + * Consider using {@link SmartIterator.find} instead. + * + * If the iterator is infinite and no element satisfies the condition, the method will never return. + * + * ```ts + * const iterator = new SmartIterator([-2, -1, 0, 1, 2]); + * const result = iterator.some((value) => value < 0); + * + * console.log(result); // true + * ``` + * + * --- + * + * @param predicate The condition to check for each element of the iterator. + * + * @returns `true` if any element satisfies the condition, `false` otherwise. + */ public some(predicate: Iteratee): boolean { let index = 0; @@ -62,8 +203,67 @@ export default class SmartIterator implements Iterat } } + /** + * Filters the elements of the iterator using a given condition. + * + * This method will iterate over all elements of the iterator checking if they satisfy the condition. + * If the condition is met, the element will be included in the new iterator. + * + * Since the iterator is lazy, the filtering process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const iterator = new SmartIterator([-2, -1, 0, 1, 2]); + * const result = iterator.filter((value) => value < 0); + * + * console.log(result.toArray()); // [-2, -1] + * ``` + * + * --- + * + * @param predicate The condition to check for each element of the iterator. + * + * @returns A new {@link SmartIterator} containing only the elements that satisfy the condition. + */ public filter(predicate: Iteratee): SmartIterator; - public filter(predicate: TypeGuardIteratee): SmartIterator; + + /** + * Filters the elements of the iterator using a given condition. + * + * This method will iterate over all elements of the iterator checking if they satisfy the condition. + * If the condition is met, the element will be included in the new iterator. + * + * Since the iterator is lazy, the filtering process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const iterator = new SmartIterator([-2, "-1", "0", 1, "2"]); + * const result = iterator.filter((value) => typeof value === "number"); + * + * console.log(result.toArray()); // [-2, 1] + * ``` + * + * --- + * + * @template S + * The type of the elements that satisfy the condition. + * This allows the type-system to infer the correct type of the new iterator. + * + * It must be a subtype of the original type of the elements. + * + * @param predicate The type guard condition to check for each element of the iterator. + * + * @returns A new {@link SmartIterator} containing only the elements that satisfy the condition. + */ + public filter(predicate: TypeGuardPredicate): SmartIterator; public filter(predicate: Iteratee): SmartIterator { const iterator = this._iterator; @@ -83,6 +283,35 @@ export default class SmartIterator implements Iterat } }); } + + /** + * Maps the elements of the iterator using a given transformation function. + * + * This method will iterate over all elements of the iterator applying the transformation function. + * The result of each transformation will be included in the new iterator. + * + * Since the iterator is lazy, the mapping process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const iterator = new SmartIterator([-2, -1, 0, 1, 2]); + * const result = iterator.map((value) => Math.abs(value)); + * + * console.log(result.toArray()); // [2, 1, 0, 1, 2] + * ``` + * + * --- + * + * @template V The type of the elements after the transformation. + * + * @param iteratee The transformation function to apply to each element of the iterator. + * + * @returns A new {@link SmartIterator} containing the transformed elements. + */ public map(iteratee: Iteratee): SmartIterator { const iterator = this._iterator; @@ -102,7 +331,64 @@ export default class SmartIterator implements Iterat } }); } + + /** + * Reduces the elements of the iterator using a given reducer function. + * This method will consume the entire iterator in the process. + * + * It will iterate over all elements of the iterator applying the reducer function. + * The result of each iteration will be passed as the accumulator to the next one. + * + * The first accumulator value will be the first element of the iterator. + * The last accumulator value will be the final result of the reduction. + * + * Also note that: + * - If an empty iterator is provided, a {@link ValueException} will be thrown. + * - If the iterator is infinite, the method will never return. + * + * ```ts + * const iterator = new SmartIterator([1, 2, 3, 4, 5]); + * const result = iterator.reduce((acc, value) => acc + value); + * + * console.log(result); // 15 + * ``` + * + * --- + * + * @param reducer The reducer function to apply to each element of the iterator. + * + * @returns The final result of the reduction. + */ public reduce(reducer: Reducer): T; + + /** + * Reduces the elements of the iterator using a given reducer function. + * This method will consume the entire iterator in the process. + * + * It will iterate over all elements of the iterator applying the reducer function. + * The result of each iteration will be passed as the accumulator to the next one. + * + * The first accumulator value will be the provided initial value. + * The last accumulator value will be the final result of the reduction. + * + * If the iterator is infinite, the method will never return. + * + * ```ts + * const iterator = new SmartIterator([1, 2, 3, 4, 5]); + * const result = iterator.reduce((acc, value) => acc + value, 10); + * + * console.log(result); // 25 + * ``` + * + * --- + * + * @template A The type of the accumulator value which will also be the type of the final result of the reduction. + * + * @param reducer The reducer function to apply to each element of the iterator. + * @param initialValue The initial value of the accumulator. + * + * @returns The final result of the reduction. + */ public reduce(reducer: Reducer, initialValue: A): A; public reduce(reducer: Reducer, initialValue?: A): A { @@ -128,7 +414,35 @@ export default class SmartIterator implements Iterat } } - public flatMap(iteratee: Iteratee>): SmartIterator + /** + * Flattens the elements of the iterator using a given transformation function. + * + * This method will iterate over all elements of the iterator applying the transformation function. + * The result of each transformation will be flattened into the new iterator. + * + * Since the iterator is lazy, the flattening process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const iterator = new SmartIterator([[-2, -1], 0, 1, 2, [3, 4, 5]]); + * const result = iterator.flatMap((value) => value); + * + * console.log(result.toArray()); // [-2, -1, 0, 1, 2, 3, 4, 5] + * ``` + * + * --- + * + * @template V The type of the elements after the transformation. + * + * @param iteratee The transformation function to apply to each element of the iterator. + * + * @returns A new {@link SmartIterator} containing the flattened elements. + */ + public flatMap(iteratee: Iteratee): SmartIterator { const iterator = this._iterator; @@ -141,17 +455,46 @@ export default class SmartIterator implements Iterat const result = iterator.next(); if (result.done) { return result.value; } - const iterable = iteratee(result.value, index); - for (const value of iterable) + const elements = iteratee(result.value, index); + if (elements instanceof Array) { - yield value; + for (const value of elements) { yield value; } } + else { yield elements; } index += 1; } }); } + /** + * Drops a given number of elements at the beginning of the iterator. + * The remaining elements will be included in a new iterator. + * See also {@link SmartIterator.take}. + * + * Since the iterator is lazy, the dropping process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * Only the dropped elements will be consumed in the process. + * The rest of the iterator will be consumed only once the new one is. + * + * ```ts + * const iterator = new SmartIterator([-2, -1, 0, 1, 2]); + * const result = iterator.drop(3); + * + * console.log(result.toArray()); // [1, 2] + * ``` + * + * --- + * + * @param count The number of elements to drop. + * + * @returns A new {@link SmartIterator} containing the remaining elements. + */ public drop(count: number): SmartIterator { const iterator = this._iterator; @@ -176,6 +519,36 @@ export default class SmartIterator implements Iterat } }); } + + /** + * Takes a given number of elements at the beginning of the iterator. + * These elements will be included in a new iterator. + * See also {@link SmartIterator.drop}. + * + * Since the iterator is lazy, the taking process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * Only the taken elements will be consumed from the original iterator. + * The rest of the original iterator will be available for further consumption. + * + * ```ts + * const iterator = new SmartIterator([-2, -1, 0, 1, 2]); + * const result = iterator.take(3); + * + * console.log(result.toArray()); // [-2, -1, 0] + * console.log(iterator.toArray()); // [1, 2] + * ``` + * + * --- + * + * @param limit The number of elements to take. + * + * @returns A new {@link SmartIterator} containing the taken elements. + */ public take(limit: number): SmartIterator { const iterator = this._iterator; @@ -197,8 +570,69 @@ export default class SmartIterator implements Iterat }); } + /** + * Finds the first element of the iterator that satisfies a given condition. + * + * This method will iterate over all elements of the iterator checking if they satisfy the condition. + * The first element that satisfies the condition will be returned immediately. + * + * Only the elements that are necessary to find the first + * satisfying one will be consumed from the original iterator. + * The rest of the original iterator will be available for further consumption. + * + * Also note that: + * - If no element satisfies the condition, `undefined` will be returned once the entire iterator is consumed. + * - If the iterator is infinite and no element satisfies the condition, the method will never return. + * + * ```ts + * const iterator = new SmartIterator([-2, -1, 0, 1, 2]); + * const result = iterator.find((value) => value > 0); + * + * console.log(result); // 1 + * ``` + * + * --- + * + * @param predicate The condition to check for each element of the iterator. + * + * @returns The first element that satisfies the condition, `undefined` otherwise. + */ public find(predicate: Iteratee): T | undefined; - public find(predicate: TypeGuardIteratee): S | undefined; + + /** + * Finds the first element of the iterator that satisfies a given condition. + * + * This method will iterate over all elements of the iterator checking if they satisfy the condition. + * The first element that satisfies the condition will be returned immediately. + * + * Only the elements that are necessary to find the first + * satisfying one will be consumed from the original iterator. + * The rest of the original iterator will be available for further consumption. + * + * Also note that: + * - If no element satisfies the condition, `undefined` will be returned once the entire iterator is consumed. + * - If the iterator is infinite and no element satisfies the condition, the method will never return. + * + * ```ts + * const iterator = new SmartIterator([-2, "-1", "0", 1, "2"]); + * const result = iterator.find((value) => typeof value === "number"); + * + * console.log(result); // -2 + * ``` + * + * --- + * + * @template S + * The type of the element that satisfies the condition. + * This allows the type-system to infer the correct type of the result. + * + * It must be a subtype of the original type of the elements. + * + * @param predicate The type guard condition to check for each element of the iterator. + * + * @returns The first element that satisfies the condition, `undefined` otherwise. + */ + public find(predicate: TypeGuardPredicate): S | undefined; public find(predicate: Iteratee): T | undefined { let index = 0; @@ -214,10 +648,55 @@ export default class SmartIterator implements Iterat } } + /** + * Enumerates the elements of the iterator. + * Each element is be paired with its index in a new iterator. + * + * Since the iterator is lazy, the enumeration process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const iterator = new SmartIterator(["A", "M", "N", "Z"]); + * const result = iterator.enumerate(); + * + * console.log(result.toArray()); // [[0, "A"], [1, "M"], [2, "N"], [3, "Z"]] + * ``` + * + * --- + * + * @returns A new {@link SmartIterator} containing the enumerated elements. + */ public enumerate(): SmartIterator<[number, T], R> { return this.map((value, index) => [index, value]); } + + /** + * Removes all duplicate elements from the iterator. + * The first occurrence of each element will be kept. + * + * Since the iterator is lazy, the deduplication process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const iterator = new SmartIterator([1, 1, 2, 3, 2, 3, 4, 5, 5, 4]); + * const result = iterator.unique(); + * + * console.log(result.toArray()); // [1, 2, 3, 4, 5] + * ``` + * + * --- + * + * @returns A new {@link SmartIterator} containing only the unique elements. + */ public unique(): SmartIterator { const iterator = this._iterator; @@ -240,6 +719,23 @@ export default class SmartIterator implements Iterat }); } + /** + * Counts the number of elements in the iterator. + * This method will consume the entire iterator in the process. + * + * If the iterator is infinite, the method will never return. + * + * ```ts + * const iterator = new SmartIterator([1, 2, 3, 4, 5]); + * const result = iterator.count(); + * + * console.log(result); // 5 + * ``` + * + * --- + * + * @returns The number of elements in the iterator. + */ public count(): number { let index = 0; @@ -253,6 +749,25 @@ export default class SmartIterator implements Iterat } } + /** + * Iterates over all elements of the iterator. + * The elements are passed to the function along with their index. + * + * This method will consume the entire iterator in the process. + * If the iterator is infinite, the method will never return. + * + * ```ts + * const iterator = new SmartIterator(["A", "M", "N", "Z"]); + * iterator.forEach((value, index) => + * { + * console.log(`${index}: ${value}`); // "0: A", "1: M", "2: N", "3: Z" + * } + * ``` + * + * --- + * + * @param iteratee The function to apply to each element of the iterator. + */ public forEach(iteratee: Iteratee): void { let index = 0; @@ -268,11 +783,144 @@ export default class SmartIterator implements Iterat } } + /** + * Advances the iterator to the next element and returns the result. + * If the iterator requires it, a value must be provided to be passed to the next element. + * + * Once the iterator is done, the method will return an object with the `done` property set to `true`. + * + * ```ts + * const iterator = new SmartIterator([1, 2, 3, 4, 5]); + * + * let result = iterator.next(); + * while (!result.done) + * { + * console.log(result.value); // 1, 2, 3, 4, 5 + * + * result = iterator.next(); + * } + * + * console.log(result); // { done: true, value: undefined } + * ``` + * + * --- + * + * @param values The value to pass to the next element, if required. + * + * @returns The result of the iteration, containing the value of the operation. + */ public next(...values: N extends undefined ? [] : [N]): IteratorResult { return this._iterator.next(...values); } + /** + * An utility method that may be used to close the iterator gracefully, + * free the resources and perform any cleanup operation. + * It may also be used to signal the end or to compute a specific final result of the iteration process. + * + * ```ts + * const iterator = new SmartIterator({ + * _index: 0, + * next: function() + * { + * return { done: false, value: this._index += 1 }; + * }, + * return: function() { console.log("Closing the iterator..."); } + * }); + * + * for (const value of iterator) + * { + * if (value > 5) { break; } // Closing the iterator... + * + * console.log(value); // 1, 2, 3, 4, 5 + * } + * ``` + * + * --- + * + * @param value The final value of the iterator. + * + * @returns The result of the iterator. + */ + public return(value?: R): IteratorResult + { + if (this._iterator.return) { return this._iterator.return(value); } + + return { done: true, value: value as R }; + } + + /** + * An utility method that may be used to close the iterator due to an error, + * free the resources and perform any cleanup operation. + * It may also be used to signal that an error occurred during the iteration process or to handle it. + * + * ```ts + * const iterator = new SmartIterator({ + * _index: 0, + * next: function() + * { + * return { done: this._index > 10, value: this._index += 1 }; + * }, + * throw: function(error) + * { + * console.warn(error.message); + * + * this._index = 0; + * } + * }); + * + * for (const value of iterator) // 1, 2, 3, 4, 5, "The index is too high.", 1, 2, 3, 4, 5, ... + * { + * try + * { + * if (value > 5) { throw new Error("The index is too high."); } + * + * console.log(value); // 1, 2, 3, 4, 5 + * } + * catch (error) { iterator.throw(error); } + * } + * ``` + * + * --- + * + * @param error The error to throw into the iterator. + * + * @returns The final result of the iterator. + */ + public throw(error: unknown): IteratorResult + { + if (this._iterator.throw) { return this._iterator.throw(error); } + + throw error; + } + + /** + * An utility method that aggregates the elements of the iterator using a given key function. + * The elements will be grouped by the resulting keys in a new specialized iterator. See {@link AggregatedIterator}. + * + * Since the iterator is lazy, the grouping process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * the new one is and that consuming one of them will consume the other as well. + * + * ```ts + * const iterator = new SmartIterator([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + * const result = iterator.groupBy((value) => value % 2 === 0 ? "even" : "odd"); + * + * console.log(result.toObject()); // { odd: [1, 3, 5, 7, 9], even: [2, 4, 6, 8, 10] } + * ``` + * + * --- + * + * @template K The type of the keys used to group the elements. + * + * @param iteratee The key function to apply to each element of the iterator. + * + * @returns A new instance of the {@link AggregatedIterator} class containing the grouped elements. + */ public groupBy(iteratee: Iteratee): AggregatedIterator { return new AggregatedIterator(this.map((element, index) => @@ -283,6 +931,24 @@ export default class SmartIterator implements Iterat })); } + /** + * Materializes the iterator into an array. + * This method will consume the entire iterator in the process. + * + * If the iterator is infinite, the method will never return. + * + * ```ts + * const iterator = new SmartIterator(function* () + * { + * for (let i = 0; i < 5; i += 1) { yield i; } + * }); + * const result = iterator.toArray(); + * + * console.log(result); // [0, 1, 2, 3, 4] + * ``` + * + * @returns The {@link Array} containing all elements of the iterator. + */ public toArray(): T[] { return Array.from(this as Iterable); diff --git a/src/models/iterators/types.ts b/src/models/iterators/types.ts index dd7e761..f018351 100644 --- a/src/models/iterators/types.ts +++ b/src/models/iterators/types.ts @@ -1,30 +1,319 @@ - import type { MaybePromise } from "../promises/types.js"; +/** + * An union type that represents an iterable object that can be either synchronous or asynchronous. + * + * ```ts + * const iterable: MaybeAsyncIterable = [...]; + * for await (const value of iterable) + * { + * console.log(value); + * } + * ``` + * + * --- + * + * @template T The type of the elements in the iterable. + */ export type MaybeAsyncIterable = Iterable | AsyncIterable; + +/** + * An union type that represents an iterator object that can be either synchronous or asynchronous. + * + * ```ts + * const iterator: MaybeAsyncIterator = { ... }; + * for await (const value of iterator) + * { + * console.log(value); + * } + * ``` + * + * --- + * + * @template T The type of the elements in the iterator. + */ export type MaybeAsyncIterator = Iterator | AsyncIterator; + +/** + * An union type that represents a generator object that can be either synchronous or asynchronous. + * + * ```ts + * const generator: MaybeAsyncGenerator = [async] function*() { ... }; + * for await (const value of generator) + * { + * console.log(value); + * } + */ export type MaybeAsyncGenerator = Generator | AsyncGenerator; +/** + * An utility type that represents a function that returns a generator object. + * It differs from the native `GeneratorFunction` type by allowing to specify the types of the returned generator. + * + * ```ts + * const generatorFn: GeneratorFunction = function*() { ... }; + * const generator: Generator = generatorFn(); + * + * for (const value of generator) + * { + * console.log(value); + * } + * ``` + * + * --- + * + * @template T The type of the elements generated by the generator. + * @template R The type of the return value of the generator. Default is `void`. + * @template N The type of the `next` method argument. Default is `undefined`. + */ export type GeneratorFunction = () => Generator; + +/** + * An utility type that represents a function that returns an asynchronous generator object. + * It differs from the native `AsyncGeneratorFunction` type by allowing to specify the types of the returned generator. + * + * ```ts + * const asyncGeneratorFn: AsyncGeneratorFunction = async function*() { ... }; + * const generator: AsyncGenerator = asyncGeneratorFn(); + * for await (const value of generator) + * { + * console.log(value); + * } + * ``` + * + * --- + * + * @template T The type of the elements generated by the generator. + * @template R The type of the return value of the generator. Default is `void`. + * @template N The type of the `next` method argument. Default is `undefined`. + */ export type AsyncGeneratorFunction = () => AsyncGenerator; + +/** + * An utility type that represents a function that returns a + * generator object that can be either synchronous or asynchronous. + * + * ```ts + * const generatorFn: MaybeAsyncGeneratorFunction = [async] function*() { ... }; + * const generator: MaybeAsyncGenerator = generatorFn(); + * for await (const value of generator) + * { + * console.log(value); + * } + * ``` + * + * --- + * + * @template T The type of the elements generated by the generator. + * @template R The type of the return value of the generator. Default is `void`. + * @template N The type of the `next` method argument. Default is `undefined`. + */ export type MaybeAsyncGeneratorFunction = () => MaybeAsyncGenerator; +/** + * An utility type that represents the standard JavaScript's + * {@link https://en.wikipedia.org/wiki/Iteratee|iteratee} function. + * It can be used to transform the elements of an iterable. + * + * ```ts + * const iteratee: Iteratee = (value: number) => `${value}`; + * const values: string[] = [1, 2, 3, 4, 5].map(iteratee); + * + * console.log(values); // ["1", "2", "3", "4", "5"] + * ``` + * + * --- + * + * @template T The type of the elements in the iterable. + * @template R The type of the return value of the iteratee. Default is `void`. + */ export type Iteratee = (value: T, index: number) => R; + +/** + * An utility type that represents an asynchronous {@link https://en.wikipedia.org/wiki/Iteratee|iteratee} function. + * It can be used to transform the elements of an iterable asynchronously. + * + * ```ts + * const iteratee: AsyncIteratee = async (value: number) => `${value}`; + * const values: Promise[] = [1, 2, 3, 4, 5].map(iteratee); + * for await (const value of values) + * { + * console.log(value); // "1", "2", "3", "4", "5" + * } + * ``` + * + * --- + * + * @template T The type of the elements in the iterable. + * @template R The type of the return value of the iteratee. Default is `void`. + */ export type AsyncIteratee = (value: T, index: number) => Promise; -export type MaybeAsyncIteratee = (value: T, index: number) => MaybePromise; -export type TypeGuardIteratee = (value: T, index: number) => value is R; +/** + * An utility type that represents an {@link https://en.wikipedia.org/wiki/Iteratee|iteratee} + * function that can be either synchronous or asynchronous. + * It can be used to transform the elements of an iterable. + * + * ```ts + * const iteratee: MaybeAsyncIteratee = [async] (value: number) => `${value}`; + * const values: Promise[] = [1, 2, 3, 4, 5].map(iteratee); + * for await (const value of values) + * { + * console.log(value); // "1", "2", "3", "4", "5" + * } + * ``` + * + * --- + * + * @template T The type of the elements in the iterable. + * @template R The type of the return value of the iteratee. Default is `void`. + */ +export type MaybeAsyncIteratee = (value: T, index: number) => MaybePromise; -// @ts-expect-error - This is an asyncronous type guard iteratee that guarantees the return value is a promise. -export type AsyncTypeGuardIteratee = (value: T, index: number) => value is Promise; +/** + * An utility type that represents a {@link https://en.wikipedia.org/wiki/Predicate_(mathematical_logic)|predicate} + * function which acts as a + * {@link https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates|type guard}. + * It can be used to ensure the type of the elements of an iterable + * while allowing the type-system to infer them correctly. + * + * ```ts + * const iteratee: TypeGuardPredicate = (value): value is string => typeof value === "string"; + * const values: string[] = [1, "2", 3, "4", 5].filter(iteratee); + * for (const value of values) + * { + * console.log(value); // "2", "4" + * } + * ``` + * + * --- + * + * @template T The type of the elements in the iterable. + * @template R + * The type of the elements that pass the type guard. + * It must be a subtype of `T`. Default is `T`. + */ +export type TypeGuardPredicate = (value: T, index: number) => value is R; -// @ts-expect-error - This may be an asyncronous type guard iteratee that guarantees the return value may be a promise. -export type MaybeAsyncTypeGuardIteratee = (value: T, index: number) => value is MaybePromise; +// These types need this Issue to be solved: https://github.com/microsoft/TypeScript/issues/37681 +// +// export type AsyncTypeGuardPredicate = (value: T, index: number) => value is Promise; +// export type MaybeAsyncTypeGuardPredicate = (value: T, index: number) => value is MaybePromise; +/** + * An utility type that represents a reducer function. + * It can be used to reduce the elements of an iterable into a single value. + * + * ```ts + * const sum: Reducer = (accumulator, value) => accumulator + value; + * const total: number = [1, 2, 3, 4, 5].reduce(sum); + * + * console.log(total); // 15 + * ``` + * + * --- + * + * @template T The type of the elements in the iterable. + * @template A The type of the accumulator. + */ export type Reducer = (accumulator: A, value: T, index: number) => A; + +/** + * An utility type that represents an asynchronous reducer function. + * It can be used to reduce the elements of an iterable into a single value. + * + * ```ts + * const sum: AsyncReducer = async (accumulator, value) => accumulator + value; + * const result = await new SmartAsyncIterator([1, 2, 3, 4, 5]).reduce(sum); + * + * console.log(result); // 15 + * ``` + * + * --- + * + * @template T The type of the elements in the iterable. + * @template A The type of the accumulator. + */ export type AsyncReducer = (accumulator: A, value: T, index: number) => Promise; + +/** + * An utility type that represents a reducer function that can be either synchronous or asynchronous. + * It can be used to reduce the elements of an iterable into a single value. + * + * ```ts + * const sum: MaybeAsyncReducer = [async] (accumulator, value) => accumulator + value; + * const result = await new SmartAsyncIterator([1, 2, 3, 4, 5]).reduce(sum); + * + * console.log(result); // 15 + * ``` + * + * --- + * + * @template T The type of the elements in the iterable. + * @template A The type of the accumulator. + */ export type MaybeAsyncReducer = (accumulator: A, value: T, index: number) => MaybePromise; -export type IteratorLike = Iterable | Iterator; -export type AsyncIteratorLike = AsyncIterable | AsyncIterator; +/** + * An union type that represents either an iterable or an iterator object. + * More in general, it represents an object that can be looped over in one way or another. + * + * ```ts + * const elements: IteratorLike = { ... }; + * const iterator: SmartIterator = new SmartIterator(elements); + * for (const value of iterator) + * { + * console.log(value); + * } + * ``` + * + * --- + * + * @template T The type of the elements in the iterable. + * @template R The type of the return value of the iterator. Default is `void`. + * @template N The type of the `next` method argument. Default is `undefined`. + */ +export type IteratorLike = Iterable | Iterator; + +/** + * An union type that represents either an iterable or an iterator object that can be asynchronous. + * More in general, it represents an object that can be looped over in one way or another. + * + * ```ts + * const elements: AsyncIteratorLike = { ... }; + * const iterator: SmartAsyncIterator = new SmartAsyncIterator(elements); + * for await (const value of iterator) + * { + * console.log(value); + * } + * ``` + * + * --- + * + * @template T The type of the elements in the iterable. + * @template R The type of the return value of the iterator. Default is `void`. + * @template N The type of the `next` method argument. Default is `undefined`. + */ +export type AsyncIteratorLike = AsyncIterable | AsyncIterator; + +/** + * An union type that represents either an iterable or an iterator + * object that can be either synchronous or asynchronous. + * More in general, it represents an object that can be looped over in one way or another. + * + * ```ts + * const elements: MaybeAsyncIteratorLike = { ... }; + * const iterator: SmartAsyncIterator = new SmartAsyncIterator(elements); + * for await (const value of iterator) + * { + * console.log(value); + * } + * ``` + * + * --- + * + * @template T The type of the elements in the iterable. + * @template R The type of the return value of the iterator. Default is `void`. + * @template N The type of the `next` method argument. Default is `undefined`. + */ export type MaybeAsyncIteratorLike = IteratorLike | AsyncIteratorLike; diff --git a/src/models/json/json-storage.ts b/src/models/json/json-storage.ts index e6cf334..c4b3c6b 100644 --- a/src/models/json/json-storage.ts +++ b/src/models/json/json-storage.ts @@ -1,26 +1,59 @@ - import { isBrowser } from "../../helpers.js"; import { EnvironmentException } from "../exceptions/index.js"; import type { JSONValue } from "./types.js"; /** - * A wrapper around the `Storage` API to store and retrieve JSON values. + * A wrapper around the `Storage` API to better store and easily retrieve + * typed JSON values using the classical key-value pair storage system. + * + * It allows to handle either the volatile {@link sessionStorage} or the persistent + * {@link localStorage} at the same time, depending on what's your required use case. + * + * ```ts + * const jsonStorage = new JSONStorage(); * - * It allows to handle either the `sessionStorage` or the `localStorage` - * storage at the same time, depending on the required use case. + * jsonStorage.write("user:cookieAck", { value: true, version: "2023-02-15" }); + * // ... between sessions ... + * const cookieAck = jsonStorage.read<{ value: boolean; version: string; }>("user:cookieAck"); + * ``` */ export default class JSONStorage { + /** + * Whether to prefer the {@link localStorage} over the {@link sessionStorage} when calling an ambivalent method. + * + * If `true`, the persistent storage is preferred. If `false`, the volatile storage is preferred. + * Default is `true`. + */ protected _preferPersistence: boolean; + /** + * A reference to the volatile {@link sessionStorage} storage. + */ protected _volatile: Storage; + + /** + * A reference to the persistent {@link localStorage} storage. + */ protected _persistent: Storage; + /** + * Initializes a new instance of the {@link JSONStorage} class. + * It cannot be instantiated outside of a browser environment or an {@link EnvironmentException} is thrown. + * + * ```ts + * const jsonStorage = new JSONStorage(); + * ``` + * + * --- + * + * @param preferPersistence + * Whether to prefer the {@link localStorage} over the {@link sessionStorage} when calling an ambivalent method. + * If omitted, it defaults to `true` to prefer the persistent storage. + */ public constructor(preferPersistence = true) { - this._preferPersistence = preferPersistence; - if (!(isBrowser)) { throw new EnvironmentException( @@ -28,227 +61,520 @@ export default class JSONStorage ); } + this._preferPersistence = preferPersistence; + this._volatile = window.sessionStorage; this._persistent = window.localStorage; } - protected _get(storage: Storage, propertyName: string): T | undefined; - protected _get(storage: Storage, propertyName: string, defaultValue: T): T; - protected _get(storage: Storage, propertyName: string, defaultValue?: T): T | undefined; - protected _get(storage: Storage, propertyName: string, defaultValue?: T): T | undefined + protected _get(storage: Storage, key: string): T | undefined; + protected _get(storage: Storage, key: string, defaultValue: T): T; + protected _get(storage: Storage, key: string, defaultValue?: T): T | undefined; + protected _get(storage: Storage, key: string, defaultValue?: T): T | undefined { - const propertyValue = storage.getItem(propertyName); - if (propertyValue) + const value = storage.getItem(key); + if (value) { try { - return JSON.parse(propertyValue); + return JSON.parse(value); } catch { // eslint-disable-next-line no-console console.warn( - `The "${propertyValue}" value for "${propertyName}"` + + `The "${value}" value for "${key}"` + " property cannot be parsed. Clearing the storage..."); - storage.removeItem(propertyName); + storage.removeItem(key); } } return defaultValue; } - protected _set(storage: Storage, propertyName: string, newValue?: T): void + protected _set(storage: Storage, key: string, newValue?: T): void { const encodedValue = JSON.stringify(newValue); if (encodedValue) { - storage.setItem(propertyName, encodedValue); + storage.setItem(key, encodedValue); } else { - storage.removeItem(propertyName); + storage.removeItem(key); } } /** - * Retrieves the value with the specified name from the corresponding storage. + * Retrieves the value with the specified key from the default storage. + * + * ```ts + * const value: TValue = jsonStorage.get("key"); + * ``` + * + * --- + * + * @template T The type of the value to retrieve. + * + * @param key The key of the value to retrieve. + * + * @returns The value with the specified key or `undefined` if the key doesn't exist. + */ + public get(key: string): T | undefined; + + /** + * Retrieves the value with the specified key from the default storage. + * + * ```ts + * const value: TValue = jsonStorage.get("key", defaultValue); + * ``` + * + * --- + * + * @template T The type of the value to retrieve. + * + * @param key The key of the value to retrieve. + * @param defaultValue The default value to return if the key doesn't exist. + * @param persistent + * Whether to prefer the persistent {@link localStorage} over the volatile {@link sessionStorage}. + * If omitted, it defaults to the `preferPersistence` value set in the constructor. + * + * @returns The value with the specified key or the provided default value if the key doesn't exist. + */ + public get(key: string, defaultValue: T, persistent?: boolean): T; + + /** + * Retrieves the value with the specified key from the default storage. + * + * ```ts + * const value: TValue = jsonStorage.get("key", obj?.value); + * ``` + * + * --- * - * @param propertyName The name of the property to retrieve. - * @param defaultValue The default value to return if the property does not exist. - * @param persistent Whether to use the persistent `localStorage` or the volatile `sessionStorage`. + * @template T The type of the value to retrieve. * - * @returns The value of the property or the default value if the property does not exist. + * @param key The key of the value to retrieve. + * @param defaultValue The default value to return (which may be `undefined`) if the key doesn't exist. + * @param persistent + * Whether to prefer the persistent {@link localStorage} over the volatile {@link sessionStorage}. + * If omitted, it defaults to the `preferPersistence` value set in the constructor. + * + * @returns The value with the specified key or the default value if the key doesn't exist. */ - public get(propertyName: string, defaultValue: undefined, persistent?: boolean): T | undefined; - public get(propertyName: string, defaultValue: T, persistent?: boolean): T ; - public get(propertyName: string, defaultValue?: T, persistent?: boolean): T | undefined; - public get(propertyName: string, defaultValue?: T, persistent = this._preferPersistence) + public get(key: string, defaultValue?: T, persistent?: boolean): T | undefined; + public get(key: string, defaultValue?: T, persistent = this._preferPersistence) : T | undefined { const storage = persistent ? this._persistent : this._volatile; - return this._get(storage, propertyName, defaultValue); + return this._get(storage, key, defaultValue); } + + /** + * Retrieves the value with the specified key from the volatile {@link sessionStorage}. + * + * ```ts + * const value: TValue = jsonStorage.recall("key"); + * ``` + * + * --- + * + * @template T The type of the value to retrieve. + * + * @param key The key of the value to retrieve. + * + * @returns The value with the specified key or `undefined` if the key doesn't exist. + */ + public recall(key: string): T | undefined; + /** - * Retrieves the value with the specified name from the volatile `sessionStorage`. + * Retrieves the value with the specified key from the volatile {@link sessionStorage}. + * + * ```ts + * const value: TValue = jsonStorage.recall("key", defaultValue); + * ``` * - * @param propertyName The name of the property to retrieve. - * @param defaultValue The default value to return if the property does not exist. + * --- * - * @returns The value of the property or the default value if the property does not exist. + * @template T The type of the value to retrieve. + * + * @param key The key of the value to retrieve. + * @param defaultValue The default value to return if the key doesn't exist. + * + * @returns The value with the specified key or the default value if the key doesn't exist. */ - public recall(propertyName: string): T | undefined; - public recall(propertyName: string, defaultValue: T): T; - public recall(propertyName: string, defaultValue?: T): T | undefined; - public recall(propertyName: string, defaultValue?: T): T | undefined + public recall(key: string, defaultValue: T): T; + + /** + * Retrieves the value with the specified key from the volatile {@link sessionStorage}. + * + * ```ts + * const value: TValue = jsonStorage.recall("key", obj?.value); + * ``` + * + * --- + * + * @template T The type of the value to retrieve. + * + * @param key The key of the value to retrieve. + * @param defaultValue The default value to return (which may be `undefined`) if the key doesn't exist. + * + * @returns The value with the specified key or the default value if the key doesn't exist. + */ + public recall(key: string, defaultValue?: T): T | undefined; + public recall(key: string, defaultValue?: T): T | undefined { - return this._get(this._volatile, propertyName, defaultValue); + return this._get(this._volatile, key, defaultValue); } + /** - * Retrieves the value with the specified name looking first in the - * volatile `sessionStorage` and then in the persistent `localStorage`. + * Retrieves the value with the specified key looking first in the volatile + * {@link sessionStorage} and then, if not found, in the persistent {@link localStorage}. + * + * ```ts + * const value: TValue = jsonStorage.retrieve("key"); + * ``` + * + * --- * - * @param propertyName The name of the property to retrieve. - * @param defaultValue The default value to return if the property does not exist. + * @template T The type of the value to retrieve. * - * @returns The value of the property or the default value if the property does not exist. + * @param key The key of the value to retrieve. + * + * @returns The value with the specified key or `undefined` if the key doesn't exist. */ - public retrieve(propertyName: string): T | undefined; - public retrieve(propertyName: string, defaultValue: T): T; - public retrieve(propertyName: string, defaultValue?: T): T | undefined; - public retrieve(propertyName: string, defaultValue?: T): T | undefined + public retrieve(key: string): T | undefined; + + /** + * Retrieves the value with the specified key looking first in the volatile + * {@link sessionStorage} and then, if not found, in the persistent {@link localStorage}. + * + * ```ts + * const value: TValue = jsonStorage.retrieve("key", defaultValue); + * ``` + * + * --- + * + * @template T The type of the value to retrieve. + * + * @param key The key of the value to retrieve. + * @param defaultValue The default value to return if the key doesn't exist. + * + * @returns The value with the specified key or the default value if the key doesn't exist. + */ + public retrieve(key: string, defaultValue: T): T; + + /** + * Retrieves the value with the specified key looking first in the volatile + * {@link sessionStorage} and then, if not found, in the persistent {@link localStorage}. + * + * ```ts + * const value: TValue = jsonStorage.retrieve("key", obj?.value); + * ``` + * + * --- + * + * @template T The type of the value to retrieve. + * + * @param key The key of the value to retrieve. + * @param defaultValue The default value to return (which may be `undefined`) if the key doesn't exist. + * + * @returns The value with the specified key or the default value if the key doesn't exist. + */ + public retrieve(key: string, defaultValue?: T): T | undefined; + public retrieve(key: string, defaultValue?: T): T | undefined { - return this.recall(propertyName) ?? this.read(propertyName, defaultValue); + return this.recall(key) ?? this.read(key, defaultValue); } + /** - * Retrieves the value with the specified name from the persistent `localStorage`. + * Retrieves the value with the specified key from the persistent {@link localStorage}. + * + * ```ts + * const value: TValue = jsonStorage.read("key"); + * ``` + * + * --- * - * @param propertyName The name of the property to retrieve. - * @param defaultValue The default value to return if the property does not exist. + * @template T The type of the value to retrieve. * - * @returns The value of the property or the default value if the property does not exist. + * @param key The key of the value to retrieve. + * + * @returns The value with the specified key or `undefined` if the key doesn't exist. */ - public read(propertyName: string): T | undefined; - public read(propertyName: string, defaultValue: T): T; - public read(propertyName: string, defaultValue?: T): T | undefined; - public read(propertyName: string, defaultValue?: T): T | undefined + public read(key: string): T | undefined; + + /** + * Retrieves the value with the specified key from the persistent {@link localStorage}. + * + * ```ts + * const value: TValue = jsonStorage.read("key", defaultValue); + * ``` + * + * --- + * + * @template T The type of the value to retrieve. + * + * @param key The key of the value to retrieve. + * @param defaultValue The default value to return if the key doesn't exist. + * + * @returns The value with the specified key or the default value if the key doesn't exist. + */ + public read(key: string, defaultValue: T): T; + + /** + * Retrieves the value with the specified key from the persistent {@link localStorage}. + * + * ```ts + * const value: TValue = jsonStorage.read("key", obj?.value); + * ``` + * + * --- + * + * @template T The type of the value to retrieve. + * + * @param key The key of the value to retrieve. + * @param defaultValue The default value to return (which may be `undefined`) if the key doesn't exist. + * + * @returns The value with the specified key or the default value if the key doesn't exist. + */ + public read(key: string, defaultValue?: T): T | undefined; + public read(key: string, defaultValue?: T): T | undefined { - return this._get(this._persistent, propertyName, defaultValue); + return this._get(this._persistent, key, defaultValue); } /** - * Checks whether the property with the specified name exists in the corresponding storage. + * Checks whether the value with the specified key exists within the default storage. + * + * ```ts + * if (jsonStorage.has("key")) + * { + * // The key exists. Do something... + * } + * ``` + * + * --- * - * @param propertyName The name of the property to check. - * @param persistent Whether to use the persistent `localStorage` or the volatile `sessionStorage`. + * @param key The key of the value to check. + * @param persistent + * Whether to prefer the persistent {@link localStorage} over the volatile {@link sessionStorage}. + * If omitted, it defaults to the `preferPersistence` value set in the constructor. * - * @returns `true` if the property exists, `false` otherwise. + * @returns `true` if the key exists, `false` otherwise. */ - public has(propertyName: string, persistent?: boolean): boolean + public has(key: string, persistent?: boolean): boolean { const storage = persistent ? this._persistent : this._volatile; - return storage.getItem(propertyName) !== null; + return storage.getItem(key) !== null; } + /** - * Checks whether the property with the specified name exists in the volatile `sessionStorage`. + * Checks whether the value with the specified key exists within the volatile {@link sessionStorage}. + * + * ```ts + * if (jsonStorage.knows("key")) + * { + * // The key exists. Do something... + * } + * ``` * - * @param propertyName The name of the property to check. + * --- * - * @returns `true` if the property exists, `false` otherwise. + * @param key The key of the value to check. + * + * @returns `true` if the key exists, `false` otherwise. */ - public knows(propertyName: string): boolean + public knows(key: string): boolean { - return this._volatile.getItem(propertyName) !== null; + return this._volatile.getItem(key) !== null; } + /** - * Checks whether the property with the specified name exists looking first in the - * volatile `sessionStorage` and then in the persistent `localStorage`. + * Checks whether the value with the specified key exists looking first in the + * volatile {@link sessionStorage} and then, if not found, in the persistent {@link localStorage}. * - * @param propertyName The name of the property to check. + * ```ts + * if (jsonStorage.find("key")) + * { + * // The key exists. Do something... + * } + * ``` * - * @returns `true` if the property exists, `false` otherwise. + * --- + * + * @param key The key of the value to check. + * + * @returns `true` if the key exists, `false` otherwise. */ - public find(propertyName: string): boolean + public find(key: string): boolean { - return this.knows(propertyName) ?? this.exists(propertyName); + return this.knows(key) ?? this.exists(key); } + /** - * Checks whether the property with the specified name exists in the persistent `localStorage`. + * Checks whether the value with the specified key exists within the persistent {@link localStorage}. + * + * ```ts + * if (jsonStorage.exists("key")) + * { + * // The key exists. Do something... + * } + * ``` + * + * --- * - * @param propertyName The name of the property to check. + * @param key The key of the value to check. * - * @returns `true` if the property exists, `false` otherwise. + * @returns `true` if the key exists, `false` otherwise. */ - public exists(propertyName: string): boolean + public exists(key: string): boolean { - return this._persistent.getItem(propertyName) !== null; + return this._persistent.getItem(key) !== null; } /** - * Sets the value with the specified name in the corresponding storage. - * If the value is `undefined`, the property is removed from the storage. + * Sets the value with the specified key in the default storage. + * If the value is `undefined` or omitted, the key is removed from the storage. * - * @param propertyName The name of the property to set. - * @param newValue The new value to set. - * @param persistent Whether to use the persistent `localStorage` or the volatile `sessionStorage`. + * ```ts + * jsonStorage.set("key"); + * jsonStorage.set("key", value); + * jsonStorage.set("key", obj?.value); + * ``` + * + * --- + * + * @template T The type of the value to set. + * + * @param key The key of the value to set. + * @param newValue The new value to set. If it's `undefined` or omitted, the key is removed instead. + * @param persistent + * Whether to prefer the persistent {@link localStorage} over the volatile {@link sessionStorage}. + * If omitted, it defaults to the `preferPersistence` value set in the constructor. */ - public set(propertyName: string, newValue?: T, persistent = this._preferPersistence): void + public set(key: string, newValue?: T, persistent = this._preferPersistence): void { const storage = persistent ? this._persistent : this._volatile; - this._set(storage, propertyName, newValue); + this._set(storage, key, newValue); + } + + /** + * Sets the value with the specified key in the volatile {@link sessionStorage}. + * If the value is `undefined` or omitted, the key is removed from the storage. + * + * ```ts + * jsonStorage.remember("key"); + * jsonStorage.remember("key", value); + * jsonStorage.remember("key", obj?.value); + * ``` + * + * --- + * + * @template T The type of the value to set. + * + * @param key The key of the value to set. + * @param newValue The new value to set. If it's `undefined` or omitted, the key is removed instead. + */ + public remember(key: string, newValue?: T): void + { + this._set(this._volatile, key, newValue); } + /** - * Sets the value with the specified name in the volatile `sessionStorage`. - * If the value is `undefined`, the property is removed from the storage. + * Sets the value with the specified key in the persistent {@link localStorage}. + * If the value is `undefined` or omitted, the key is removed from the storage. + * + * ```ts + * jsonStorage.write("key"); + * jsonStorage.write("key", value); + * jsonStorage.write("key", obj?.value); + * ``` + * + * --- * - * @param propertyName The name of the property to set. - * @param newValue The new value to set. + * @template T The type of the value to set. + * + * @param key The key of the value to set. + * @param newValue The new value to set. If it's `undefined` or omitted, the key is removed instead. */ - public remember(propertyName: string, newValue?: T): void + public write(key: string, newValue?: T): void { - this._set(this._volatile, propertyName, newValue); + this._set(this._persistent, key, newValue); } + /** - * Sets the value with the specified name in the persistent `localStorage`. - * If the value is `undefined`, the property is removed from the storage. + * Removes the value with the specified key from the default storage. * - * @param propertyName The name of the property to set. - * @param newValue The new value to set. + * ```ts + * jsonStorage.delete("key"); + * ``` + * + * --- + * + * @param key The key of the value to remove. + * @param persistent + * Whether to prefer the persistent {@link localStorage} over the volatile {@link sessionStorage}. + * If omitted, it defaults to the `preferPersistence` value set in the constructor. */ - public write(propertyName: string, newValue?: T): void + public delete(key: string, persistent?: boolean): void { - this._set(this._persistent, propertyName, newValue); + const storage = persistent ? this._persistent : this._volatile; + + storage.removeItem(key); } /** - * Removes the value with the specified name from the volatile `sessionStorage`. + * Removes the value with the specified key from the volatile {@link sessionStorage}. * - * @param propertyName The name of the property to remove. + * ```ts + * jsonStorage.forget("key"); + * ``` + * + * --- + * + * @param key The key of the value to remove. */ - public forget(propertyName: string): void + public forget(key: string): void { - this._volatile.removeItem(propertyName); + this._volatile.removeItem(key); } + /** - * Removes the value with the specified name from the persistent `localStorage`. + * Removes the value with the specified key from the persistent {@link localStorage}. + * + * ```ts + * jsonStorage.erase("key"); + * ``` * - * @param propertyName The name of the property to remove. + * --- + * + * @param key The key of the value to remove. */ - public erase(propertyName: string): void + public erase(key: string): void { - this._persistent.removeItem(propertyName); + this._persistent.removeItem(key); } + /** - * Removes the value with the specified name from all the storages. + * Removes the value with the specified key from both the + * volatile {@link sessionStorage} and the persistent {@link localStorage}. + * + * ```ts + * jsonStorage.clear("key"); + * ``` + * + * --- * - * @param propertyName The name of the property to remove. + * @param key The key of the value to remove. */ - public clear(propertyName: string): void + public clear(key: string): void { - this._volatile.removeItem(propertyName); - this._persistent.removeItem(propertyName); + this._volatile.removeItem(key); + this._persistent.removeItem(key); } public readonly [Symbol.toStringTag]: string = "JSONStorage"; diff --git a/src/models/json/types.ts b/src/models/json/types.ts index a98b5f1..b4f87a8 100644 --- a/src/models/json/types.ts +++ b/src/models/json/types.ts @@ -1,5 +1,14 @@ +/** + * A type that represents a JSON array. + */ export type JSONArray = JSONValue[]; -// eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style +/** + * A type that represents a JSON object. + */ export interface JSONObject { [key: string]: JSONValue } + +/** + * A type that represents all the possible values of a JSON value. + */ export type JSONValue = boolean | number | string | null | JSONObject | JSONArray; diff --git a/src/models/promises/deferred-promise.ts b/src/models/promises/deferred-promise.ts index a6e93b8..8ea0e94 100644 --- a/src/models/promises/deferred-promise.ts +++ b/src/models/promises/deferred-promise.ts @@ -2,11 +2,73 @@ import type { PromiseResolver, PromiseRejecter, FulfilledHandler, RejectedHandle import SmartPromise from "./smart-promise.js"; +/** + * A class representing a {@link SmartPromise} that can be resolved or rejected from the "outside". + * The `resolve` and `reject` methods are exposed to allow the promise to be settled from another context. + * + * It's particularly useful in scenarios where the promise is created and needs to be awaited in one place, + * while being resolved or rejected in another (e.g. an event handler for an user interaction). + * + * This is a change in the approach to promises: instead of defining how the promise will be resolved (or rejected), + * you define how to handle the resolution (or rejection) when it occurs. + * + * ```ts + * const promise = new DeferredPromise((value: string) => value.split(" ")); + * + * promise.then((result) => console.log(result)); // ["Hello,", "World!"] + * promise.resolve("Hello, World!"); + * ``` + * + * --- + * + * @template T The type of value the promise expects to initially be resolved with. Default is `void`. + * @template F + * The type of value returned by the `onFulfilled` callback. + * This will be the actual type of value the promise will eventually resolve to. Default is `T`. + * @template R + * The type of value possibly returned by the `onRejected` callback. + * This will be coupled with the type of value the promise will eventually resolve to, if provided. Default is `never`. + */ export default class DeferredPromise extends SmartPromise { + /** + * The exposed function that allows to resolve the promise. + * + * This protected property is the only one that can be modified directly by the derived classes. + * If you're looking for the public and readonly property, use the {@link DeferredPromise.resolve} getter instead. + */ protected _resolve: PromiseResolver; + + /** + * The exposed function that allows to reject the promise. + */ + public get resolve(): PromiseResolver { return this._resolve; } + + /** + * The exposed function that allows to reject the promise. + * + * This protected property is the only one that can be modified directly by the derived classes. + * If you're looking for the public and readonly property, use the {@link DeferredPromise.reject} getter instead. + */ protected _reject: PromiseRejecter; + /** + * The exposed function that allows to reject the promise. + */ + public get reject(): PromiseRejecter { return this._reject; } + + /** + * Initializes a new instance of the {@link DeferredPromise} class. + * + * ```ts + * const promise = new DeferredPromise((value: string) => value.split(" ")); + * ``` + * + * --- + * + * @param onFulfilled The callback to execute once the promise is fulfilled. + * @param onRejected The callback to execute once the promise is rejected. + */ public constructor(onFulfilled?: FulfilledHandler | null, onRejected?: RejectedHandler | null) { let _resolve: PromiseResolver; @@ -27,9 +89,23 @@ export default class DeferredPromise extends SmartPr this._reject = _reject!; } - public get resolve(): PromiseResolver { return this._resolve; } - public get reject(): PromiseRejecter { return this._reject; } - + /** + * Watches another promise and resolves or rejects this promise when the other one is settled. + * + * ```ts + * const promise = new Promise((resolve) => setTimeout(() => resolve("Hello, World!"), 1_000)); + * const deferred = new DeferredPromise((value: string) => value.split(" ")); + * + * deferred.then((result) => console.log(result)); // ["Hello,", "World!"] + * deferred.watch(promise); + * ``` + * + * --- + * + * @param otherPromise The promise to watch. + * + * @returns The current instance of the {@link DeferredPromise} class. + */ public watch(otherPromise: PromiseLike): this { otherPromise.then(this.resolve, this.reject); diff --git a/src/models/promises/smart-promise.ts b/src/models/promises/smart-promise.ts index 4bdea2a..c81420e 100644 --- a/src/models/promises/smart-promise.ts +++ b/src/models/promises/smart-promise.ts @@ -1,18 +1,127 @@ import type { FulfilledHandler, PromiseExecutor, RejectedHandler } from "./types.js"; +/** + * A wrapper class representing an enhanced version of the native {@link Promise} object. + * + * It provides additional properties to check the state of the promise itself. + * The state can be either `pending`, `fulfilled` or `rejected` and is accessible through + * the {@link SmartPromise.isPending}, {@link SmartPromise.isFulfilled} & {@link SmartPromise.isRejected} properties. + * + * ```ts + * const promise = new SmartPromise((resolve, reject) => + * { + * setTimeout(() => resolve("Hello, World!"), 1_000); + * }); + * + * console.log(promise.isPending); // true + * console.log(promise.isFulfilled); // false + * + * console.log(await promise); // "Hello, World!" + * + * console.log(promise.isPending); // false + * console.log(promise.isFulfilled); // true + * ``` + * + * --- + * + * @template T The type of value the promise will eventually resolve to. Default is `void`. + */ export default class SmartPromise implements Promise { + /** + * Wraps a new {@link SmartPromise} object around an existing native {@link Promise} object. + * + * ```ts + * const request = fetch("https://api.example.com/data"); + * const smartRequest = SmartPromise.FromPromise(request); + * + * console.log(request.isPending); // Throws an error: `isPending` is not a property of `Promise`. + * console.log(smartRequest.isPending); // true + * + * const response = await request; + * console.log(smartRequest.isFulfilled); // true + * ``` + * + * --- + * + * @param promise The promise to wrap. + * + * @returns A new {@link SmartPromise} object that wraps the provided promise. + */ public static FromPromise(promise: Promise): SmartPromise { return new SmartPromise((resolve, reject) => promise.then(resolve, reject)); } + /** + * A flag indicating whether the promise is still pending or not. + * + * The protected property is the only one that can be modified directly by the derived classes. + * If you're looking for the public and readonly property, use the {@link SmartPromise.isPending} getter instead. + */ protected _isPending: boolean; + + /** + * A flag indicating whether the promise is still pending or not. + */ + public get isPending(): boolean + { + return this._isPending; + } + + /** + * A flag indicating whether the promise has been fulfilled or not. + * + * The protected property is the only one that can be modified directly by the derived classes. + * If you're looking for the public and readonly property, use the {@link SmartPromise.isFulfilled} getter instead. + */ protected _isFulfilled: boolean; + + /** + * A flag indicating whether the promise has been fulfilled or not. + */ + public get isFulfilled(): boolean + { + return this._isFulfilled; + } + + /** + * A flag indicating whether the promise has been rejected or not. + * + * The protected property is the only one that can be modified directly by the derived classes. + * If you're looking for the public and readonly property, use the {@link SmartPromise.isRejected} getter instead. + */ protected _isRejected: boolean; + /** + * A flag indicating whether the promise has been rejected or not. + */ + public get isRejected(): boolean + { + return this._isRejected; + } + + /** + * The native {@link Promise} object wrapped by this instance. + */ protected _promise: Promise; + /** + * Initializes a new instance of the {@link SmartPromise} class. + * + * ```ts + * const promise = new SmartPromise((resolve, reject) => + * { + * setTimeout(() => resolve("Hello, World!"), 1_000); + * }); + * ``` + * + * --- + * + * @param executor + * The function responsible for eventually resolving or rejecting the promise. + * Similarly to the native {@link Promise} object, it's immediately executed after the promise is created. + */ public constructor(executor: PromiseExecutor) { this._isPending = true; @@ -38,12 +147,82 @@ export default class SmartPromise implements Promise .then(_onFulfilled, _onRejected); } - public get isPending(): boolean { return this._isPending; } - public get isFulfilled(): boolean { return this._isFulfilled; } - public get isRejected(): boolean { return this._isRejected; } - + /** + * Creates a new {@link Promise} identical to the one wrapped by this instance, with a different reference. + * + * ```ts + * const promise = new SmartPromise((resolve, reject) => + * { + * setTimeout(() => resolve("Hello, World!"), 1_000); + * }); + * + * console.log(await promise.then()); // "Hello, World!" + * ``` + * + * --- + * + * @returns A new {@link Promise} identical to the original one. + */ public then(onFulfilled?: null): Promise; + + /** + * Attaches a callback that executes right after the promise is fulfilled. + * + * The previous result of the promise is passed as the argument to the callback. + * The callback's return value is considered the new promise's result instead. + * + * ```ts + * const promise = new SmartPromise((resolve, reject) => + * { + * setTimeout(() => resolve("Hello, World!"), 1_000); + * }); + * + * promise.then((result) => console.log(result)); // "Hello, World!" + * ``` + * + * --- + * + * @template F The type of value the new promise will eventually resolve to. Default is `T`. + * + * @param onFulfilled The callback to execute once the promise is fulfilled. + * + * @returns A new {@link Promise} resolved with the return value of the callback. + */ public then(onFulfilled: FulfilledHandler, onRejected?: null): Promise; + + /** + * Attaches callbacks that executes right after the promise is fulfilled or rejected. + * + * The previous result of the promise is passed as the argument to the fulfillment callback. + * The fulfillment callback's return value is considered the new promise's result instead. + * + * If an error is thrown during execution, the rejection callback is then executed instead. + * + * Also note that: + * - If the rejection callback runs properly, the error is considered handled. + * The rejection callback's return value is considered the new promise's result. + * - If the rejection callback throws an error, the new promise is rejected with that error. + * + * ```ts + * const promise = new SmartPromise((resolve, reject) => + * { + * setTimeout(resolve, Math.random() * 1_000); + * setTimeout(reject, Math.random() * 1_000); + * }); + * + * promise.then(() => console.log("OK!"), () => console.log("KO!")); // "OK!" or "KO!" + * ``` + * + * --- + * + * @template F The type of value the new promise will eventually resolve to. Default is `T`. + * @template R The type of value the new promise will eventually resolve to. Default is `never`. + * + * @param onFulfilled The callback to execute once the promise is fulfilled. + * @param onRejected The callback to execute once the promise is rejected. + * + * @returns A new {@link Promise} resolved or rejected based on the callbacks. + */ public then(onFulfilled: FulfilledHandler, onRejected: RejectedHandler) : Promise; public then( @@ -53,12 +232,79 @@ export default class SmartPromise implements Promise return this._promise.then(onFulfilled, onRejected); } + /** + * Creates a new {@link Promise} identical to the one wrapped by this instance, with a different reference. + * + * ```ts + * const promise = new SmartPromise((resolve, reject) => + * { + * setTimeout(() => reject(new Error("An unknown error occurred.")), 1_000); + * }); + * + * promise.catch(); // Uncaught Error: An unknown error occurred. + * ``` + * + * --- + * + * @returns A new {@link Promise} identical to the original one. + */ public catch(onRejected?: null): Promise; + + /** + * Attaches a callback to handle the potential rejection of the promise. + * If it happens, the callback is then executed. + * + * Also note that: + * - If the callback runs properly, the error is considered handled. + * The callback's return value is considered the new promise's result. + * - If the callback throws an error, the new promise is rejected with that error. + * + * ```ts + * const promise = new SmartPromise((resolve, reject) => + * { + * setTimeout(() => reject(new Error("An unknown error occurred.")), 1_000); + * }); + * + * promise.catch((reason) => console.error(reason)); // "Error: An unknown error occurred." + * ``` + * + * --- + * + * @template R The type of value the new promise will eventually resolve to. Default is `T`. + * + * @param onRejected The callback to execute once the promise is rejected. + * + * @returns A new {@link Promise} able to catch and handle the potential error. + */ public catch(onRejected: RejectedHandler): Promise; public catch(onRejected?: RejectedHandler | null): Promise { return this._promise.catch(onRejected); } + + /** + * Attaches a callback that executes right after the promise is settled, regardless of the outcome. + * + * ```ts + * const promise = new SmartPromise((resolve, reject) => + * { + * setTimeout(resolve, Math.random() * 1_000); + * setTimeout(reject, Math.random() * 1_000); + * }); + * + * + * promise + * .then(() => console.log("OK!")) // Logs "OK!" if the promise is fulfilled. + * .catch(() => console.log("KO!")) // Logs "KO!" if the promise is rejected. + * .finally(() => console.log("Done!")); // Always logs "Done!". + * ``` + * + * --- + * + * @param onFinally The callback to execute when once promise is settled. + * + * @returns A new {@link Promise} that executes the callback once the promise is settled. + */ public finally(onFinally?: (() => void) | null): Promise { return this._promise.finally(onFinally); diff --git a/src/models/promises/timed-promise.ts b/src/models/promises/timed-promise.ts index 5bbaa5a..9a1a2fa 100644 --- a/src/models/promises/timed-promise.ts +++ b/src/models/promises/timed-promise.ts @@ -3,8 +3,49 @@ import { TimeoutException } from "../exceptions/index.js"; import SmartPromise from "./smart-promise.js"; import type { MaybePromise, PromiseExecutor } from "./types.js"; +/** + * A class representing a {@link SmartPromise} that rejects automatically after a given time. + * It's useful for operations that must be completed within a certain time frame. + * + * If the operation takes longer than the specified time, the promise is rejected with a {@link TimeoutException}. + * + * ```ts + * const promise = new TimedPromise((resolve, reject) => + * { + * setTimeout(() => resolve("Hello, World!"), Math.random() * 10_000); + * + * }, 5_000); + * + * promise + * .then((result) => console.log(result)) // "Hello, World!" + * .catch((error) => console.error(error)); // TimeoutException: The operation has timed out. + * ``` + * + * --- + * + * @template T The type of value the promise will eventually resolve to. Default is `void`. + */ export default class TimedPromise extends SmartPromise { + /** + * Initializes a new instance of the {@link TimedPromise} class. + * + * ```ts + * const promise = new TimedPromise((resolve, reject) => + * { + * setTimeout(() => resolve("Hello, World!"), Math.random() * 10_000); + * + * }, 5_000); + * ``` + * + * --- + * + * @param executor + * The function responsible for eventually resolving or rejecting the promise. + * Similarly to the native {@link Promise} object, it's immediately executed after the promise is created. + * + * @param timeout The maximum time in milliseconds that the operation can take before timing out. + */ public constructor(executor: PromiseExecutor, timeout?: number) { super((resolve, reject) => diff --git a/src/models/promises/types.ts b/src/models/promises/types.ts index 22618a4..92c5c08 100644 --- a/src/models/promises/types.ts +++ b/src/models/promises/types.ts @@ -1,8 +1,104 @@ +/** + * An union type that represents a value that can be either a value or a promise of that value. + * This is useful when you want to handle both synchronous and asynchronous values in the same way. + * + * ```ts + * async function splitWords(value: MaybePromise): Promise + * { + * return (await value).split(" "); + * } + * ``` + * + * --- + * + * @template T The type of the value. + */ export type MaybePromise = T | PromiseLike; +/** + * An utility type that represents the callback that is executed when a promise is fulfilled. + * It's compatible with the `onFulfilled` parameter of the `then` method of the native {@link Promise} object. + * + * ```ts + * const onFulfilled: FulfilledHandler = (value) => value.split(" "); + * + * await new Promise((resolve) => resolve("Hello, World!")) + * .then(onFulfilled); + * ``` + * + * --- + * + * @template T The type of value accepted by the function. Default is `void`. + * @template R The type of value returned by the function. Default is `T`. + */ export type FulfilledHandler = (value: T) => MaybePromise; + +/** + * An utility type that represents the callback that is executed when a promise is rejected. + * It's compatible with the `onRejected` parameter of the `then`/`catch` methods of the native {@link Promise} object. + * + * ```ts + * const onRejected: RejectedHandler = (reason) => String(reason); + * + * await new Promise((_, reject) => reject(new Error("An error occurred."))) + * .catch(onRejected); + * ``` + * + * --- + * + * @template E The type of value accepted by the function. Default is `unknown`. + * @template R The type of value returned by the function. Default is `never`. + */ export type RejectedHandler = (reason: E) => MaybePromise; +/** + * An utility type that represents a function that can be used to resolve a promise. + * It's compatible with the `resolve` parameter of the native {@link Promise} executor. + * + * ```ts + * let _resolve: PromiseResolver = (result) => console.log(result); + * + * await new Promise((resolve) => { _resolve = resolve; }); + * ``` + * + * --- + * + * @template T The type of the value accepted by the function. Default is `void`. + */ export type PromiseResolver = (result: MaybePromise) => void; + +/** + * An utility type that represents a function that can be used to reject a promise. + * It's compatible with the `reject` parameter of the native {@link Promise} executor. + * + * ```ts + * let _reject: PromiseRejecter = (reason) => console.error(reason); + * + * await new Promise((_, reject) => { _reject = reject; }); + * ``` + * + * --- + * + * @template E The type of the value accepted by the function. Default is `unknown`. + */ export type PromiseRejecter = (reason?: MaybePromise) => void; + +/** + * An utility type that represents the function that will be executed by the promise. + * It's compatible with the `executor` parameter of the native {@link Promise} object. + * + * ```ts + * const executor: PromiseExecutor = (resolve, reject) => + * { + * setTimeout(() => resolve("Hello, World!"), 1_000); + * }; + * + * await new Promise(executor); + * ``` + * + * --- + * + * @template T The type of value accepted by the `resolve` function. Default is `void`. + * @template E The type of value accepted by the `reject` function. Default is `unknown`. + */ export type PromiseExecutor = (resolve: PromiseResolver, reject: PromiseRejecter) => void; diff --git a/src/models/timers/clock.ts b/src/models/timers/clock.ts index f72b7a2..6d2c03f 100644 --- a/src/models/timers/clock.ts +++ b/src/models/timers/clock.ts @@ -1,20 +1,56 @@ import { TimeUnit } from "../../utils/date.js"; -import { RangeException, RuntimeException } from "../exceptions/index.js"; import Publisher from "../callbacks/publisher.js"; +import { FatalErrorException, RangeException, RuntimeException } from "../exceptions/index.js"; import GameLoop from "../game-loop.js"; +import type { Callback } from "../types.js"; interface ClockEventMap { start: () => void; stop: () => void; tick: (elapsedTime: number) => void; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: Callback; } +/** + * A class representing a clock. + * + * It can be started, stopped and, when running, it ticks at a specific frame rate. + * It's possible to subscribe to these events to receive notifications when they occur. + * + * ```ts + * const clock = new Clock(); + * + * clock.onStart(() => { console.log("The clock has started."); }); + * clock.onTick((elapsedTime) => { console.log(`The clock has ticked at ${elapsedTime}ms.`); }); + * clock.onStop(() => { console.log("The clock has stopped."); }); + * + * clock.start(); + * ``` + */ export default class Clock extends GameLoop { - protected _publisher: Publisher; - + /** + * The {@link Publisher} object that will be used to publish the events of the clock. + */ + protected override _publisher: Publisher; + + /** + * Initializes a new instance of the {@link Clock} class. + * + * ```ts + * const clock = new Clock(); + * ``` + * + * --- + * + * @param msIfNotBrowser + * The interval in milliseconds at which the clock will tick if the environment is not a browser. + * `TimeUnit.Second` by default. + */ public constructor(msIfNotBrowser: number = TimeUnit.Second) { super((elapsedTime) => this._publisher.publish("tick", elapsedTime), msIfNotBrowser); @@ -22,33 +58,74 @@ export default class Clock extends GameLoop this._publisher = new Publisher(); } + /** + * Starts the execution of the clock. + * + * If the clock is already running, a {@link RuntimeException} will be thrown. + * + * ```ts + * clock.onStart(() => { [...] }); // This callback will be executed. + * clock.start(); + * ``` + * + * --- + * + * @param elapsedTime The elapsed time to set as default when the clock starts. Default is `0`. + */ public override start(elapsedTime = 0): void { if (this._isRunning) { throw new RuntimeException("The clock has already been started."); } - super.start(elapsedTime); + this._startTime = performance.now() - elapsedTime; + this._start(); + this._isRunning = true; this._publisher.publish("start"); } + /** + * Stops the execution of the clock. + * + * If the clock hasn't yet started, a {@link RuntimeException} will be thrown. + * + * ```ts + * clock.onStop(() => { [...] }); // This callback will be executed. + * clock.stop(); + * ``` + */ public override stop(): void { - if (!(this._isRunning)) { throw new RuntimeException("The clock hadn't yet started."); } + if (!(this._isRunning)) { throw new RuntimeException("The clock had already stopped or hadn't yet started."); } + if (!(this._handle)) { throw new FatalErrorException(); } - super.stop(); + this._stop(); + this._handle = undefined; + this._isRunning = false; this._publisher.publish("stop"); } - public onStart(callback: () => void): () => void - { - return this._publisher.subscribe("start", callback); - } - public onStop(callback: () => void): () => void - { - return this._publisher.subscribe("stop", callback); - } - + /** + * Subscribes to the `tick` event of the clock. + * + * ```ts + * clock.onTick((elapsedTime) => { [...] }); // This callback will be executed. + * clock.start(); + * ``` + * + * --- + * + * @param callback The callback that will be executed when the clock ticks. + * @param tickStep + * The minimum time in milliseconds that must pass from the previous execution of the callback to the next one. + * + * - If it's a positive number, the callback will be executed only if the + * time passed from the previous execution is greater than this number. + * - If it's `0`, the callback will be executed every tick without even checking for the time. + * - If it's a negative number, a {@link RangeException} will be thrown. + * + * @returns A function that can be used to unsubscribe from the event. + */ public onTick(callback: (elapsedTime: number) => void, tickStep = 0): () => void { if (tickStep < 0) { throw new RangeException("The tick step must be a non-negative number."); } diff --git a/src/models/timers/countdown.ts b/src/models/timers/countdown.ts index 74d57a6..2640faa 100644 --- a/src/models/timers/countdown.ts +++ b/src/models/timers/countdown.ts @@ -1,10 +1,10 @@ import { TimeUnit } from "../../utils/date.js"; -import { FatalErrorException, RangeException, RuntimeException } from "../exceptions/index.js"; -import { DeferredPromise, SmartPromise } from "../promises/index.js"; - import Publisher from "../callbacks/publisher.js"; +import { FatalErrorException, RangeException, RuntimeException } from "../exceptions/index.js"; import GameLoop from "../game-loop.js"; +import { DeferredPromise, SmartPromise } from "../promises/index.js"; +import type { Callback } from "../types.js"; interface CountdownEventMap { @@ -12,37 +12,97 @@ interface CountdownEventMap stop: (reason: unknown) => void; tick: (remainingTime: number) => void; expire: () => void; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: Callback; } +/** + * A class representing a countdown. + * + * It can be started, stopped, when running it ticks at a specific frame rate and it expires when the time's up. + * It's possible to subscribe to these events to receive notifications when they occur. + * + * ```ts + * const countdown = new Countdown(10_000); + * + * countdown.onStart(() => { console.log("The countdown has started."); }); + * countdown.onTick((remainingTime) => { console.log(`The countdown has ${remainingTime}ms remaining.`); }); + * countdown.onStop((reason) => { console.log(`The countdown has stopped because of ${reason}.`); }); + * countdown.onExpire(() => { console.log("The countdown has expired."); }); + * + * countdown.start(); + * ``` + */ export default class Countdown extends GameLoop { - protected _deferrer?: DeferredPromise; - protected _publisher: Publisher; - + /** + * The {@link Publisher} object that will be used to publish the events of the countdown. + */ + protected override _publisher: Publisher; + + /** + * The total duration of the countdown in milliseconds. + * + * This protected property is the only one that can be modified directly by the derived classes. + * If you're looking for the public and readonly property, use the {@link Countdown.duration} getter instead. + */ protected _duration: number; + + /** + * The total duration of the countdown in milliseconds. + */ public get duration(): number { return this._duration; } + /** + * The remaining time of the countdown in milliseconds. + * It's calculated as the difference between the total duration and the elapsed time. + */ public get remainingTime(): number { return this._duration - this.elapsedTime; } + /** + * The {@link DeferredPromise} that will be resolved or rejected when the countdown expires or stops. + */ + protected _deferrer?: DeferredPromise; + + /** + * Initializes a new instance of the {@link Countdown} class. + * + * ```ts + * const countdown = new Countdown(10_000); + * ``` + * + * --- + * + * @param duration + * The total duration of the countdown in milliseconds. + * + * @param msIfNotBrowser + * The interval in milliseconds at which the countdown will tick if the environment is not a browser. + * `TimeUnit.Second` by default. + */ public constructor(duration: number, msIfNotBrowser: number = TimeUnit.Second) { const callback = () => { const remainingTime = this.remainingTime; - this._publisher.publish("tick", remainingTime); - if (remainingTime <= 0) { this._deferrerStop(); + this._publisher.publish("tick", 0); this._publisher.publish("expire"); } + else + { + this._publisher.publish("tick", remainingTime); + } }; super(callback, msIfNotBrowser); @@ -51,12 +111,24 @@ export default class Countdown extends GameLoop this._duration = duration; } + /** + * The internal method actually responsible for stopping the + * countdown and resolving or rejecting the {@link Countdown._deferrer} promise. + * + * @param reason + * The reason why the countdown has stopped. + * + * - If it's `undefined`, the promise will be resolved. + * - If it's a value, the promise will be rejected with that value. + */ protected _deferrerStop(reason?: unknown): void { if (!(this._isRunning)) { throw new RuntimeException("The countdown hadn't yet started."); } if (!(this._deferrer)) { throw new FatalErrorException(); } - super.stop(); + this._stop(); + this._handle = undefined; + this._isRunning = false; if (reason !== undefined) { this._deferrer.reject(reason); } else { this._deferrer.resolve(); } @@ -64,9 +136,27 @@ export default class Countdown extends GameLoop this._deferrer = undefined; } + /** + * Starts the execution of the countdown. + * + * If the countdown is already running, a {@link RuntimeException} will be thrown. + * + * ```ts + * countdown.onStart(() => { [...] }); // This callback will be executed. + * countdown.start(); + * ``` + * + * --- + * + * @param remainingTime + * The remaining time to set as default when the countdown starts. + * Default is the {@link Countdown.duration} itself. + * + * @returns A {@link SmartPromise} that will be resolved or rejected when the countdown expires or stops. + */ public override start(remainingTime: number = this.duration): SmartPromise { - if (this._isRunning) { throw new RuntimeException("The countdown has already been started."); } + if (this._isRunning) { throw new RuntimeException("The countdown had already stopped or hadn't yet started."); } if (this._deferrer) { throw new FatalErrorException(); } this._deferrer = new DeferredPromise(); @@ -76,27 +166,74 @@ export default class Countdown extends GameLoop return this._deferrer; } + + /** + * Stops the execution of the countdown. + * + * If the countdown hasn't yet started, a {@link RuntimeException} will be thrown. + * + * ```ts + * countdown.onStop(() => { [...] }); // This callback will be executed. + * countdown.stop(); + * ``` + * + * --- + * + * @param reason + * The reason why the countdown has stopped. + * + * - If it's `undefined`, the promise will be resolved. + * - If it's a value, the promise will be rejected with that value. + */ public override stop(reason?: unknown): void { + // TODO: Once solved Issues #6 & #10, make the `reason` parameter required. + // - https://github.com/Byloth/core/issues/6 + // - https://github.com/Byloth/core/issues/10 + // this._deferrerStop(reason); this._publisher.publish("stop", reason); } + /** + * Subscribes to the `expire` event of the countdown. + * + * ```ts + * countdown.onExpire(() => { [...] }); // This callback will be executed once the countdown has expired. + * countdown.start(); + * ``` + * + * @param callback The callback that will be executed when the countdown expires. + * + * @returns A function that can be used to unsubscribe from the event. + */ public onExpire(callback: () => void): () => void { return this._publisher.subscribe("expire", callback); } - public onStart(callback: () => void): () => void - { - return this._publisher.subscribe("start", callback); - } - public onStop(callback: (reason?: unknown) => void): () => void - { - return this._publisher.subscribe("stop", callback); - } - + /** + * Subscribes to the `tick` event of the countdown. + * + * ```ts + * countdown.onTick((remainingTime) => { [...] }); // This callback will be executed. + * countdown.start(); + * ``` + * + * --- + * + * @param callback The callback that will be executed when the countdown ticks. + * @param tickStep + * The minimum time in milliseconds that must pass from the previous execution of the callback to the next one. + * + * - If it's a positive number, the callback will be executed only if the + * time passed from the previous execution is greater than this number. + * - If it's `0`, the callback will be executed every tick without even checking for the time. + * - If it's a negative number, a {@link RangeException} will be thrown. + * + * @returns A function that can be used to unsubscribe from the event. + */ public onTick(callback: (remainingTime: number) => void, tickStep = 0): () => void { if (tickStep < 0) { throw new RangeException("The tick step must be a non-negative number."); } diff --git a/src/models/types.ts b/src/models/types.ts index 63a7729..a7cc230 100644 --- a/src/models/types.ts +++ b/src/models/types.ts @@ -1,9 +1,10 @@ export type { KeyedIteratee, + AsyncKeyedIteratee, MaybeAsyncKeyedIteratee, - KeyedTypeGuardIteratee, - MaybeAsyncKeyedTypeGuardIteratee, + KeyedTypeGuardPredicate, KeyedReducer, + AsyncKeyedReducer, MaybeAsyncKeyedReducer } from "./aggregators/types.js"; @@ -13,10 +14,11 @@ export type { AsyncGeneratorFunction, MaybeAsyncGeneratorFunction, Iteratee, + AsyncIteratee, MaybeAsyncIteratee, - TypeGuardIteratee, - MaybeAsyncTypeGuardIteratee, + TypeGuardPredicate, Reducer, + AsyncReducer, MaybeAsyncReducer, IteratorLike, AsyncIteratorLike, diff --git a/src/utils/async.ts b/src/utils/async.ts index 749f35b..a655a8c 100644 --- a/src/utils/async.ts +++ b/src/utils/async.ts @@ -1,13 +1,62 @@ +/** + * Returns a promise that resolves after a certain number of milliseconds. + * It can be used to pause or delay the execution of an asynchronous function. + * + * ```ts + * doSomething(); + * await delay(1_000); + * doSomethingElse(); + * ``` + * + * --- + * + * @param milliseconds The number of milliseconds to wait before resolving the promise. + * + * @returns A promise that resolves after the specified number of milliseconds. + */ export function delay(milliseconds: number): Promise { return new Promise((resolve) => setTimeout(resolve, milliseconds)); } +/** + * Returns a promise that resolves on the next animation frame. + * It can be used to synchronize operations with the browser's rendering cycle. + * + * ```ts + * const $el = document.querySelector(".element"); + * + * $el.classList.add("animate"); + * await nextAnimationFrame(); + * $el.style.opacity = "1"; + * ``` + * + * --- + * + * @returns A promise that resolves on the next animation frame. + */ export function nextAnimationFrame(): Promise { return new Promise((resolve) => requestAnimationFrame(() => resolve())); } +/** + * Returns a promise that resolves on the next microtask. + * It can be used to yield to the event loop in long-running operations to prevent blocking the main thread. + * + * ```ts + * for (let i = 0; i < 100_000_000; i += 1) + * { + * doSomething(i); + * + * if (i % 100 === 0) await yieldToEventLoop(); + * } + * ``` + * + * --- + * + * @returns A promise that resolves on the next microtask. + */ export function yieldToEventLoop(): Promise { return new Promise((resolve) => setTimeout(resolve)); diff --git a/src/utils/curve.ts b/src/utils/curve.ts index 122b7c9..ee3c3b1 100644 --- a/src/utils/curve.ts +++ b/src/utils/curve.ts @@ -1,18 +1,70 @@ -import { SmartIterator } from "../models/index.js"; +import { SmartIterator, ValueException } from "../models/index.js"; +/** + * A utility class that provides a set of methods to generate sequences of numbers following specific curves. + * It can be used to generate sequences of values that can be + * used in animations, transitions and other different scenarios. + * + * It cannot be instantiated directly. + */ export default class Curve { + /** + * Generates a given number of values following a linear curve. + * The values are equally spaced and normalized between 0 and 1. + * + * ```ts + * for (const value of Curve.Linear(5)) + * { + * console.log(value); // 0, 0.25, 0.5, 0.75, 1 + * } + * ``` + * + * --- + * + * @param values The number of values to generate. + * + * @returns A {@link SmartIterator} object that generates the values following a linear curve. + */ public static Linear(values: number): SmartIterator { - const step = (1 / values); + const steps = (values - 1); return new SmartIterator(function* () { - for (let index = 0; index < values; index += 1) { yield index * step; } + for (let index = 0; index < values; index += 1) { yield index / steps; } }); } + + /** + * Generates a given number of values following an exponential curve. + * The values are equally spaced and normalized between 0 and 1. + * + * ```ts + * for (const value of Curve.Exponential(6)) + * { + * console.log(value); // 0, 0.04, 0.16, 0.36, 0.64, 1 + * } + * ``` + * + * --- + * + * @param values The number of values to generate. + * @param base + * The base of the exponential curve. Default is `2`. + * + * Also note that: + * - If it's equal to `1`, the curve will be linear. + * - If it's included between `0` and `1`, the curve will be logarithmic. + * + * The base cannot be negative. If so, a {@link ValueException} will be thrown. + * + * @returns A {@link SmartIterator} object that generates the values following an exponential curve. + */ public static Exponential(values: number, base = 2): SmartIterator { + if (base < 0) { throw new ValueException("The base of the exponential curve cannot be negative."); } + const steps = (values - 1); return new SmartIterator(function* () diff --git a/src/utils/date.ts b/src/utils/date.ts index becb158..a3f95e8 100644 --- a/src/utils/date.ts +++ b/src/utils/date.ts @@ -1,20 +1,129 @@ import { SmartIterator } from "../models/index.js"; +/** + * An enumeration that represents the time units and their conversion factors. + * It can be used as utility to express time values in a more + * readable way or to convert time values between different units. + * + * ```ts + * setTimeout(() => { [...] }, 5 * TimeUnit.Minute); + * ``` + */ export enum TimeUnit { /* eslint-disable @typescript-eslint/prefer-literal-enum-member */ + /** + * A millisecond: the base time unit. + */ Millisecond = 1, - Second = 1000, + + /** + * A second: 1000 milliseconds. + */ + Second = 1_000, + + /** + * A minute: 60 seconds. + */ Minute = 60 * Second, + + /** + * An hour: 60 minutes. + */ Hour = 60 * Minute, + + /** + * A day: 24 hours. + */ Day = 24 * Hour, + + /** + * A week: 7 days. + */ Week = 7 * Day, + + /** + * A month: 30 days. + */ Month = 30 * Day, + + /** + * A year: 365 days. + */ Year = 365 * Day } +/** + * An enumeration that represents the days of the week. + * It can be used as utility to identify the days of the week when working with dates. + * + * ```ts + * const today = new Date(); + * if (today.getUTCDay() === WeekDay.Sunday) + * { + * // Today is Sunday. Do something... + * } + * ``` + */ +export enum WeekDay +{ + /** + * Sunday + */ + Sunday = 0, + + /** + * Monday + */ + Monday = 1, + + /** + * Tuesday + */ + Tuesday = 2, + + /** + * Wednesday + */ + Wednesday = 3, + + /** + * Thursday + */ + Thursday = 4, + + /** + * Friday + */ + Friday = 5, + + /** + * Saturday + */ + Saturday = 6 +} + +/** + * An utility function that calculates the difference between two dates. + * The difference can be expressed in different time units. + * + * ```ts + * const start = new Date("2025-01-01"); + * const end = new Date("2025-01-31"); + * + * dateDifference(start, end, TimeUnit.Minute); // 43200 + * ``` + * + * --- + * + * @param start The start date. + * @param end The end date. + * @param unit The time unit to express the difference. `TimeUnit.Day` by default. + * + * @returns The difference between the two dates in the specified time unit. + */ export function dateDifference(start: string | Date, end: string | Date, unit = TimeUnit.Day): number { start = new Date(start); @@ -23,28 +132,92 @@ export function dateDifference(start: string | Date, end: string | Date, unit = return Math.floor((end.getTime() - start.getTime()) / unit); } -export function dateRange(start: string | Date, end: string | Date, offset = TimeUnit.Day): SmartIterator +/** + * An utility function that generates an iterator over a range of dates. + * The step between the dates can be expressed in different time units. + * + * ```ts + * const start = new Date("2025-01-01"); + * const end = new Date("2025-01-31"); + * + * for (const date of dateRange(start, end, TimeUnit.Week)) + * { + * date.toISOString().slice(8, 10); // "01", "08", "15", "22", "29" + * } + * ``` + * + * --- + * + * @param start The start date (included). + * @param end The end date (excluded). + * @param step The time unit to express the step between the dates. `TimeUnit.Day` by default. + * + * @returns A {@link SmartIterator} object that generates the dates in the range. + */ +export function dateRange(start: string | Date, end: string | Date, step = TimeUnit.Day): SmartIterator { - start = new Date(start); - end = new Date(end); - return new SmartIterator(function* () { - const endTime = end.getTime(); + const endTime = new Date(end).getTime(); - let unixTime: number = start.getTime(); + let unixTime: number = new Date(start).getTime(); while (unixTime < endTime) { yield new Date(unixTime); - unixTime += offset; + unixTime += step; } }); } +/** + * An utility function that rounds a date to the nearest time unit. + * The rounding can be expressed in different time units. + * + * ```ts + * const date = new Date("2025-01-01T12:34:56.789Z"); + * + * dateRound(date, TimeUnit.Hour); // 2025-01-01T12:00:00.000Z + * ``` + * + * --- + * + * @param date The date to round. + * @param unit The time unit to express the rounding. `TimeUnit.Day` by default. + * + * @returns The rounded date. + */ export function dateRound(date: string | Date, unit = TimeUnit.Day): Date { date = new Date(date); return new Date(Math.floor(date.getTime() / unit) * unit); } + +/** + * An utility function that gets the week of a date. + * The first day of the week can be optionally specified. + * + * ```ts + * const date = new Date("2025-01-01"); + * + * getWeek(date, WeekDay.Monday); // 2024-12-30 + * ``` + * + * --- + * + * @param date The date to get the week of. + * @param firstDay The first day of the week. `WeekDay.Sunday` by default. + * + * @returns The first day of the week of the specified date. + */ +export function getWeek(date: string | Date, firstDay = WeekDay.Sunday): Date +{ + date = new Date(date); + + const startCorrector = 7 - firstDay; + const weekDayIndex = (date.getUTCDay() + startCorrector) % 7; + const firstDayTime = date.getTime() - (TimeUnit.Day * weekDayIndex); + + return dateRound(new Date(firstDayTime)); +} diff --git a/src/utils/dom.ts b/src/utils/dom.ts index 7604a1e..2bbf6a9 100644 --- a/src/utils/dom.ts +++ b/src/utils/dom.ts @@ -1,3 +1,19 @@ +/** + * Appends a script element to the document body. + * It can be used to load external scripts dynamically. + * + * ```ts + * await loadScript("https://analytics.service/script.js?id=0123456789"); + * ``` + * + * --- + * + * @param scriptUrl The URL of the script to load. + * @param scriptType The type of the script to load. Default is `"text/javascript"`. + * + * @returns + * A promise that resolves when the script has been loaded successfully or rejects if an error occurs. + */ export function loadScript(scriptUrl: string, scriptType = "text/javascript"): Promise { return new Promise((resolve, reject) => @@ -9,8 +25,8 @@ export function loadScript(scriptUrl: string, scriptType = "text/javascript"): P script.src = scriptUrl; script.type = scriptType; - script.onload = () => resolve(); - script.onerror = () => reject(); + script.onload = (evt) => resolve(); + script.onerror = (reason) => reject(reason); document.body.appendChild(script); }); diff --git a/src/utils/index.ts b/src/utils/index.ts index e10c144..52162ed 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -2,7 +2,7 @@ import Curve from "./curve.js"; import Random from "./random.js"; export { delay, nextAnimationFrame, yieldToEventLoop } from "./async.js"; -export { dateDifference, dateRange, dateRound, TimeUnit } from "./date.js"; +export { dateDifference, dateRange, dateRound, getWeek, TimeUnit, WeekDay } from "./date.js"; export { loadScript } from "./dom.js"; export { chain, count, enumerate, range, shuffle, unique, zip } from "./iterator.js"; export { average, hash, sum } from "./math.js"; diff --git a/src/utils/iterator.ts b/src/utils/iterator.ts index 4af24fe..7e363a3 100644 --- a/src/utils/iterator.ts +++ b/src/utils/iterator.ts @@ -1,6 +1,31 @@ import { SmartIterator } from "../models/index.js"; -export function chain(...iterables: Iterable[]): SmartIterator +/** + * An utility function that chains multiple iterables into a single one. + * + * Since the iterator is lazy, the chaining process will be + * executed only once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * for (const value of chain([1, 2, 3], [4, 5, 6], [7, 8, 9])) + * { + * console.log(value); // 1, 2, 3, 4, 5, 6, 7, 8, 9 + * } + * ``` + * + * --- + * + * @template T The type of elements in the iterables. + * + * @param iterables The list of iterables to chain. + * + * @returns A new {@link SmartIterator} object that chains the iterables into a single one. + */ +export function chain(...iterables: readonly Iterable[]): SmartIterator { return new SmartIterator(function* () { @@ -11,9 +36,28 @@ export function chain(...iterables: Iterable[]): SmartIterator }); } +/** + * An utility function that counts the number of elements in an iterable. + * + * Also note that: + * - If the iterable isn't an `Array`, it will be consumed entirely in the process. + * - If the iterable is an infinite generator, the function will never return. + * + * ```ts + * count([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); // 10 + * ``` + * + * --- + * + * @template T The type of elements in the iterable. + * + * @param elements The iterable to count. + * + * @returns The number of elements in the iterable. + */ export function count(elements: Iterable): number { - if (Array.isArray(elements)) { return elements.length; } + if (elements instanceof Array) { return elements.length; } let _count = 0; for (const _ of elements) { _count += 1; } @@ -21,6 +65,32 @@ export function count(elements: Iterable): number return _count; } +/** + * An utility function that enumerates the elements of an iterable. + * Each element is paired with its index in a new iterator. + * + * Since the iterator is lazy, the enumeration process will + * be executed once the resulting iterator is materialized. + * + * A new iterator will be created, holding the reference to the original one. + * This means that the original iterator won't be consumed until the + * new one is and that consuming one of them will consume the other as well. + * + * ```ts + * for (const [index, value] of enumerate(["A", "M", "N", "Z"])) + * { + * console.log(`${index}: ${value}`); // "0: A", "1: M", "2: N", "3: Z" + * } + * ``` + * + * --- + * + * @template T The type of elements in the iterable. + * + * @param elements The iterable to enumerate. + * + * @returns A new {@link SmartIterator} object containing the enumerated elements. + */ export function enumerate(elements: Iterable): SmartIterator<[number, T]> { return new SmartIterator<[number, T]>(function* () @@ -36,9 +106,60 @@ export function enumerate(elements: Iterable): SmartIterator<[number, T]> }); } +/** + * An utility function that generates an iterator over a range of numbers. + * The values are included between `0` (included) and `end` (excluded). + * + * The default step between the numbers is `1`. + * + * ```ts + * for (const number of range(5)) + * { + * console.log(number); // 0, 1, 2, 3, 4 + * } + * ``` + * + * --- + * + * @param end + * The end value (excluded). + * + * If the `end` value is negative, the step will be `-1` leading to generate the numbers in reverse order. + * + * @returns A {@link SmartIterator} object that generates the numbers in the range. + */ export function range(end: number): SmartIterator; -export function range(start: number, end: number): SmartIterator; -export function range(start: number, end: number, step: number): SmartIterator; + +/** + * An utility function that generates an iterator over a range of numbers. + * The values are included between `start` (included) and `end` (excluded). + * + * The step between the numbers can be specified with a custom value. Default is `1`. + * + * ```ts + * for (const number of range(2, 7)) + * { + * console.log(number); // 2, 3, 4, 5, 6 + * } + * ``` + * + * --- + * + * @param start + * The start value (included). + * + * If the `start` value is greater than the `end` value, the iterator will generate the numbers in reverse order. + * + * @param end + * The end value (excluded). + * + * If the `end` value is less than the `start` value, the iterator will generate the numbers in reverse order. + * + * @param step The step between the numbers. Default is `1`. + * + * @returns A {@link SmartIterator} object that generates the numbers in the range. + */ +export function range(start: number, end: number, step?: number): SmartIterator; export function range(start: number, end?: number, step = 1): SmartIterator { return new SmartIterator(function* () @@ -55,6 +176,29 @@ export function range(start: number, end?: number, step = 1): SmartIterator(iterable: Iterable): T[] { const array = Array.from(iterable); @@ -69,6 +213,24 @@ export function shuffle(iterable: Iterable): T[] return array; } +/** + * An utility function that filters the elements of an iterable ensuring that they are all unique. + * + * ```ts + * for (const value of unique([1, 1, 2, 3, 2, 3, 4, 5, 5, 4])) + * { + * console.log(value); // 1, 2, 3, 4, 5 + * } + * ``` + * + * --- + * + * @template T The type of elements in the iterable. + * + * @param elements The iterable to filter. + * + * @returns A {@link SmartIterator} object that iterates over the unique elements of the given iterable. + */ export function unique(elements: Iterable): SmartIterator { return new SmartIterator(function* () @@ -86,6 +248,29 @@ export function unique(elements: Iterable): SmartIterator }); } +/** + * An utility function that zips two iterables into a single one. + * The resulting iterable will contain the elements of the two iterables paired together. + * + * The function will stop when one of the two iterables is exhausted. + * + * ```ts + * for (const [number, char] of zip([1, 2, 3, 4], ["A", "M", "N" "Z"])) + * { + * console.log(`${number} - ${char}`); // "1 - A", "2 - M", "3 - N", "4 - Z" + * } + * ``` + * + * --- + * + * @template T The type of elements in the first iterable. + * @template U The type of elements in the second iterable. + * + * @param first The first iterable to zip. + * @param second The second iterable to zip. + * + * @returns A {@link SmartIterator} object that iterates over the zipped elements of the two given iterables. + */ export function zip(first: Iterable, second: Iterable): SmartIterator<[T, U]> { return new SmartIterator<[T, U]>(function* () diff --git a/src/utils/math.ts b/src/utils/math.ts index 0e7de44..4f808c2 100644 --- a/src/utils/math.ts +++ b/src/utils/math.ts @@ -1,8 +1,33 @@ import { ValueException } from "../models/exceptions/index.js"; import { zip } from "./iterator.js"; -export function average(values: Iterable): number; -export function average(values: Iterable, weights: Iterable): number; +/** + * Computes the average of a given list of values. + * The values can be weighted using an additional list of weights. + * + * ```ts + * average([1, 2, 3, 4, 5]); // 3 + * average([6, 8.5, 4], [3, 2, 1]); // 6.5 + * ``` + * + * --- + * + * @template T The type of the values in the list. It must be or extend a `number` object. + * + * @param values + * The list of values to compute the average. + * + * It must contain at least one element. Otherwise, a {@link ValueException} will be thrown. + * + * @param weights + * The list of weights to apply to the values. + * It should contain the same number of elements as the values list or + * the smaller number of elements between the two lists will be considered. + * + * The sum of the weights must be greater than zero. Otherwise, a {@link ValueException} will be thrown. + * + * @returns The average of the specified values. + */ export function average(values: Iterable, weights?: Iterable): number { if (weights === undefined) @@ -43,6 +68,24 @@ export function average(values: Iterable, weights?: Iterabl return _sum / _count; } +/** + * An utility function to compute the hash of a given string. + * + * The hash is computed using a simple variation of the + * {@link http://www.cse.yorku.ca/~oz/hash.html#djb2|djb2} algorithm. + * However, the hash is garanteed to be a 32-bit signed integer. + * + * ```ts + * hash("Hello, world!"); // -1880044555 + * hash("How are you?"); // 1761539132 + * ``` + * + * --- + * + * @param value The string to hash. + * + * @returns The hash of the specified string. + */ export function hash(value: string): number { let hashedValue = 0; @@ -57,6 +100,21 @@ export function hash(value: string): number return hashedValue; } +/** + * Sums all the values of a given list. + * + * ```ts + * sum([1, 2, 3, 4, 5]); // 15 + * ``` + * + * --- + * + * @template T The type of the values in the list. It must be or extend a `number` object. + * + * @param values The list of values to sum. + * + * @returns The sum of the specified values. + */ export function sum(values: Iterable): number { let _sum = 0; diff --git a/src/utils/random.ts b/src/utils/random.ts index 73eb9c1..6252b05 100644 --- a/src/utils/random.ts +++ b/src/utils/random.ts @@ -1,13 +1,67 @@ import { ValueException } from "../models/index.js"; +/** + * A wrapper class around the native {@link Math.random} function that + * provides a set of methods to generate random values more easily. + * It can be used to generate random numbers, booleans and other different values. + * + * It cannot be instantiated directly. + */ export default class Random { + /** + * Generates a random boolean value. + * + * ```ts + * if (Random.Boolean()) + * { + * // Do something... + * } + * ``` + * + * --- + * + * @param ratio + * The probability of generating `true`. + * + * It must be included between `0` and `1`. Default is `0.5`. + * + * @returns A random boolean value. + */ public static Boolean(ratio = 0.5): boolean { return (Math.random() < ratio); } + /** + * Generates a random integer value between `0` (included) and `max` (excluded). + * + * ```ts + * Random.Integer(5); // 0, 1, 2, 3, 4 + * ``` + * + * --- + * + * @param max The maximum value (excluded). + * + * @returns A random integer value. + */ public static Integer(max: number): number; + + /** + * Generates a random integer value between `min` (included) and `max` (excluded). + * + * ```ts + * Random.Integer(2, 7); // 2, 3, 4, 5, 6 + * ``` + * + * --- + * + * @param min The minimum value (included). + * @param max The maximum value (excluded). + * + * @returns A random integer value. + */ public static Integer(min: number, max: number): number; public static Integer(min: number, max?: number): number { @@ -16,8 +70,48 @@ export default class Random return Math.floor(Math.random() * (max - min) + min); } + /** + * Generates a random decimal value between `0` (included) and `1` (excluded). + * + * ```ts + * Random.Decimal(); // 0.123456789 + * ``` + * + * --- + * + * @returns A random decimal value. + */ public static Decimal(): number; + + /** + * Generates a random decimal value between `0` (included) and `max` (excluded). + * + * ```ts + * Random.Decimal(5); // 2.3456789 + * ``` + * + * --- + * + * @param max The maximum value (excluded). + * + * @returns A random decimal value. + */ public static Decimal(max: number): number; + + /** + * Generates a random decimal value between `min` (included) and `max` (excluded). + * + * ```ts + * Random.Decimal(2, 7); // 4.56789 + * ``` + * + * --- + * + * @param min The minimum value (included). + * @param max The maximum value (excluded). + * + * @returns A random decimal value + */ public static Decimal(min: number, max: number): number; public static Decimal(min?: number, max?: number): number { @@ -27,13 +121,38 @@ export default class Random return (Math.random() * (max - min) + min); } - public static Index(elements: T[]): number + /** + * Picks a random valid index from a given array of elements. + * + * @template T The type of the elements in the array. + * + * @param elements + * The array of elements to pick from. + * + * It must contain at least one element. Otherwise, a {@link ValueException} will be thrown. + * + * @returns A valid random index from the given array. + */ + public static Index(elements: readonly T[]): number { if (elements.length === 0) { throw new ValueException("You must provide at least one element."); } return this.Integer(elements.length); } - public static Choice(elements: T[]): T + + /** + * Picks a random element from a given array of elements. + * + * @template T The type of the elements in the array. + * + * @param elements + * The array of elements to pick from. + * + * It must contain at least one element. Otherwise, a {@link ValueException} will be thrown. + * + * @returns A random element from the given array. + */ + public static Choice(elements: readonly T[]): T { return elements[Random.Index(elements)]; } diff --git a/src/utils/string.ts b/src/utils/string.ts index 4b425cb..424cb47 100644 --- a/src/utils/string.ts +++ b/src/utils/string.ts @@ -1,3 +1,16 @@ +/** + * Capitalize the first letter of a string. + * + * ```ts + * capitalize('hello'); // 'Hello' + * ``` + * + * --- + * + * @param value The string to capitalize. + * + * @returns The capitalized string. + */ export function capitalize(value: string): string { return `${value.charAt(0).toUpperCase()}${value.slice(1)}`;