From 25bab714f83741e48863742ca102eae51627a489 Mon Sep 17 00:00:00 2001 From: darkruby Date: Fri, 27 Mar 2026 20:02:38 +0000 Subject: [PATCH 01/12] basic enrichment caching --- TODO.md | 10 +- bun.lock | 132 ++++++++++--------- package.json | 14 +- packages/backend/package.json | 4 +- packages/backend/src/enrichment/asset.ts | 11 ++ packages/backend/src/enrichment/cached.ts | 68 ++++++++++ packages/backend/src/enrichment/index.ts | 1 + packages/backend/src/enrichment/portfolio.ts | 14 ++ packages/backend/src/enrichment/summary.ts | 5 + packages/backend/src/enrichment/tx.ts | 11 +- packages/backend/src/handlers/asset.ts | 2 +- packages/backend/src/handlers/index.ts | 1 + packages/backend/src/handlers/prefs.ts | 2 +- packages/backend/src/handlers/profile.ts | 2 +- packages/backend/src/index.ts | 10 +- packages/backend/src/services/asset.ts | 26 ++-- packages/backend/src/services/cache.ts | 13 +- packages/backend/src/services/index.ts | 15 ++- packages/backend/src/yahoo/cached.ts | 19 +-- packages/core/tsconfig.json | 3 + 20 files changed, 240 insertions(+), 123 deletions(-) create mode 100644 packages/backend/src/enrichment/cached.ts diff --git a/TODO.md b/TODO.md index cf46e3c7..d44e5e02 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,11 @@ # todo +## 1.8.1 +- [ ] multi asset layered chart for portfolios +- [ ] multi portfollio layered chart for home screen +- [ ] INR +- [ ] mock yahoo api for tests + ## 1.8 - [x] chore: move assets-core to core @@ -15,10 +21,6 @@ - [x] enriched portfolio : break even - [x] chart merge in polars - [x] chart shows transactions -- [ ] multi asset layered chart for portfolios -- [ ] multi portfollio layered chart for home screen -- [ ] INR -- [ ] mock yahoo api for tests ## 1.7.1 diff --git a/bun.lock b/bun.lock index 6407457a..c92b5416 100644 --- a/bun.lock +++ b/bun.lock @@ -7,14 +7,14 @@ "dependencies": { "date-fns": "^4.1.0", "fp-ts": "^2.16.11", + "io-ts": "~2.2.21", "io-ts-types": "^0.5.19", - "ms": "^2.1.3", - "prettier": "^3.6.2", }, "devDependencies": { "@types/bun": "1.3.11", "bun-types": "^1.3.11", - "typescript": "~5.9.3", + "prettier": "^3.6.2", + "typescript": "~6.0.2", }, }, "packages/backend": { @@ -22,15 +22,14 @@ "version": "1.8.0", "dependencies": { "@darkruby/assets-core": "workspace:*", - "@types/ms": "^2.1.0", "cors": "^2.8.5", "csv-parse": "^6.1.0", "csv-stringify": "^6.6.0", "express": "~4.21.2", "heap-js": "^2.7.1", - "io-ts": "~2.2.21", "jsonwebtoken": "^9.0.2", "lru-cache": "^11.0.2", + "ms": "^2.1.3", "nodejs-polars": "^0.24.0", }, "devDependencies": { @@ -39,6 +38,7 @@ "@types/faker": "5", "@types/jsonwebtoken": "^9.0.9", "@types/lru-cache": "^7.10.10", + "@types/ms": "^2.1.0", "faker": "5", "nock": "^14.0.1", }, @@ -119,15 +119,15 @@ "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], - "@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="], + "@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="], - "@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], + "@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], - "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], + "@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], @@ -251,7 +251,7 @@ "@fortawesome/free-regular-svg-icons": ["@fortawesome/free-regular-svg-icons@7.2.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "7.2.0" } }, "sha512-iycmlN51EULlQ4D/UU9WZnHiN0CvjJ2TuuCrAh+1MVdzD+4ViKYH2deNAll4XAAYlZa8WAefHR5taSK8hYmSMw=="], - "@fortawesome/react-fontawesome": ["@fortawesome/react-fontawesome@3.2.0", "", { "peerDependencies": { "@fortawesome/fontawesome-svg-core": "~6 || ~7", "react": "^18.0.0 || ^19.0.0" } }, "sha512-E9Gu1hqd6JussVO26EC4WqRZssXMnQr2ol7ZNWkkFOH8jZUaxDJ9Z9WF9wIVkC+kJGXUdY3tlffpDwEKfgQrQw=="], + "@fortawesome/react-fontawesome": ["@fortawesome/react-fontawesome@3.3.0", "", { "peerDependencies": { "@fortawesome/fontawesome-svg-core": "~6 || ~7", "react": "^18.0.0 || ^19.0.0" } }, "sha512-EHmHeTf8WgO29sdY3iX/7ekE3gNUdlc2RW6mm/FzELlHFKfTrA9S4MlyquRR+RRCRCn8+jXfLFpLGB2l7wCWyw=="], "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], @@ -311,7 +311,7 @@ "@preact/signals-core": ["@preact/signals-core@1.14.0", "", {}, "sha512-AowtCcCU/33lFlh1zRFf/u+12rfrhtNakj7UpaGEsmMwUKpKWMVvcktOGcwBBNiB4lWrZWc01LhiyyzVklJyaQ=="], - "@preact/signals-react": ["@preact/signals-react@3.9.1", "", { "dependencies": { "@preact/signals-core": "^1.14.0", "use-sync-external-store": "^1.2.0" }, "peerDependencies": { "react": "^16.14.0 || 17.x || 18.x || 19.x" } }, "sha512-4bj3wUfXrYOqDDs6sX2Y5GBC8jgSa8ah8ZJHN2A+23ej+TdPDrtwVJ0e1cEhwemyOZ7Q7NHbilPRhtd5zVpaBA=="], + "@preact/signals-react": ["@preact/signals-react@3.10.0", "", { "dependencies": { "@preact/signals-core": "^1.14.0", "use-sync-external-store": "^1.2.0" }, "peerDependencies": { "react": "^16.14.0 || 17.x || 18.x || 19.x" } }, "sha512-Uxu6lidNVr9z27b/6DbCin86ekzHiJDrLXZii82aXSzvyMXYMr7l0Bab1cKbfWdbkxq13e7kS7paix3pjKBTLA=="], "@react-aria/ssr": ["@react-aria/ssr@3.9.10", "", { "dependencies": { "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ=="], @@ -323,55 +323,55 @@ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.0", "", { "os": "android", "cpu": "arm" }, "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A=="], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="], + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.0", "", { "os": "android", "cpu": "arm64" }, "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="], + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA=="], - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="], + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="], + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw=="], - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="], + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA=="], - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="], + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.0", "", { "os": "linux", "cpu": "arm" }, "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="], + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ=="], - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="], + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="], + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ=="], - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="], + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw=="], - "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="], + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog=="], - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="], + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ=="], - "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="], + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="], + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA=="], - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="], + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ=="], - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="], + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="], + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.0", "", { "os": "linux", "cpu": "x64" }, "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="], + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.0", "", { "os": "linux", "cpu": "x64" }, "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw=="], - "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="], + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw=="], - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="], + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.0", "", { "os": "none", "cpu": "arm64" }, "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA=="], - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="], + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ=="], - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="], + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w=="], - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="], + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.0", "", { "os": "win32", "cpu": "x64" }, "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA=="], - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="], + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.0", "", { "os": "win32", "cpu": "x64" }, "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w=="], "@scarf/scarf": ["@scarf/scarf@1.4.0", "", {}, "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ=="], @@ -379,7 +379,7 @@ "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], - "@swc/helpers": ["@swc/helpers@0.5.19", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA=="], + "@swc/helpers": ["@swc/helpers@0.5.20", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-2egEBHUMasdypIzrprsu8g+OEVd7Vp2MM3a2eVlM/cyFYto0nGz5BX5BTgh/ShZZI9ed+ozEq+Ngt+rgmUs8tw=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], @@ -455,27 +455,27 @@ "@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="], - "@types/warning": ["@types/warning@3.0.3", "", {}, "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q=="], + "@types/warning": ["@types/warning@3.0.4", "", {}, "sha512-CqN8MnISMwQbLJXO3doBAV4Yw9hx9/Pyr2rZ78+NfaCnhyRA/nKrpyk6E7mKw17ZOaQdLpK9GiUjrqLzBlN3sg=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.57.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/type-utils": "8.57.0", "@typescript-eslint/utils": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.57.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.57.2", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.57.2", "@typescript-eslint/type-utils": "8.57.2", "@typescript-eslint/utils": "8.57.2", "@typescript-eslint/visitor-keys": "8.57.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.57.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.57.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/types": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.57.2", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.57.2", "@typescript-eslint/types": "8.57.2", "@typescript-eslint/typescript-estree": "8.57.2", "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.57.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.57.0", "@typescript-eslint/types": "^8.57.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.57.2", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.57.2", "@typescript-eslint/types": "^8.57.2", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0" } }, "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw=="], + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.57.2", "", { "dependencies": { "@typescript-eslint/types": "8.57.2", "@typescript-eslint/visitor-keys": "8.57.2" } }, "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.57.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.57.2", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0", "@typescript-eslint/utils": "8.57.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.57.2", "", { "dependencies": { "@typescript-eslint/types": "8.57.2", "@typescript-eslint/typescript-estree": "8.57.2", "@typescript-eslint/utils": "8.57.2", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.57.0", "", {}, "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.57.2", "", {}, "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.57.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.57.0", "@typescript-eslint/tsconfig-utils": "8.57.0", "@typescript-eslint/types": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.57.2", "", { "dependencies": { "@typescript-eslint/project-service": "8.57.2", "@typescript-eslint/tsconfig-utils": "8.57.2", "@typescript-eslint/types": "8.57.2", "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.57.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/types": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ=="], + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.57.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.57.2", "@typescript-eslint/types": "8.57.2", "@typescript-eslint/typescript-estree": "8.57.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.57.2", "", { "dependencies": { "@typescript-eslint/types": "8.57.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw=="], "@unhead/react": ["@unhead/react@2.1.12", "", { "dependencies": { "unhead": "2.1.12" }, "peerDependencies": { "react": ">=18.3.1" } }, "sha512-1xXFrxyw29f+kScXfEb0GxjlgtnHxoYau0qpW9k8sgWhQUNnE5gNaH3u+rNhd5IqhyvbdDRJpQ25zoz0HIyGaw=="], @@ -503,13 +503,13 @@ "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.10.7", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-1ghYO3HnxGec0TCGBXiDLVns4eCSx4zJpxnHrlqFQajmhfKMQBzUGDdkMK7fUW7PTHTeLf+j87aTuKuuwWzMGw=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-DAKrHphkJyiGuau/cFieRYhcTFeK/lBuD++C7cZ6KZHbMhBrisoi+EvhQ5RZrIfV5qwsW8kgQ07JIC+MDJRAhg=="], "body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="], "bootstrap": ["bootstrap@5.3.8", "", { "peerDependencies": { "@popperjs/core": "^2.11.8" } }, "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg=="], - "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + "brace-expansion": ["brace-expansion@1.1.13", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w=="], "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], @@ -525,7 +525,7 @@ "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], - "caniuse-lite": ["caniuse-lite@1.0.30001778", "", {}, "sha512-PN7uxFL+ExFJO61aVmP1aIEG4i9whQd4eoSCebav62UwDyp5OHh06zN4jqKSMePVgxHifCw1QJxdRkA1Pisekg=="], + "caniuse-lite": ["caniuse-lite@1.0.30001781", "", {}, "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw=="], "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -561,9 +561,9 @@ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], - "csv-parse": ["csv-parse@6.1.0", "", {}, "sha512-CEE+jwpgLn+MmtCpVcPtiCZpVtB6Z2OKPTr34pycYYoL7sxdOkXDdQ4lRiw6ioC0q6BLqhc6cKweCVvral8yhw=="], + "csv-parse": ["csv-parse@6.2.1", "", {}, "sha512-LRLMV+UCyfMokp8Wb411duBf1gaBKJfOfBWU9eHMJ+b+cJYZsNu3AFmjJf3+yPGd59Exz1TsMjaSFyxnYB9+IQ=="], - "csv-stringify": ["csv-stringify@6.6.0", "", {}, "sha512-YW32lKOmIBgbxtu3g5SaiqWNwa/9ISQt2EcgOq0+RAIFufFp9is6tqNnKahqE5kuKvrnYAzs28r+s6pXJR8Vcw=="], + "csv-stringify": ["csv-stringify@6.7.0", "", {}, "sha512-UdtziYp5HuTz7e5j8Nvq+a/3HQo+2/aJZ9xntNTpmRRIg/3YYqDVgiS9fvAhtNbnyfbv2ZBe0bqCHqzhE7FqWQ=="], "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], @@ -611,7 +611,7 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "electron-to-chromium": ["electron-to-chromium@1.5.313", "", {}, "sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA=="], + "electron-to-chromium": ["electron-to-chromium@1.5.328", "", {}, "sha512-QNQ5l45DzYytThO21403XN3FvK0hOkWDG8viNf6jqS42msJ8I4tGDSpBCgvDRRPnkffafiwAym2X2eHeGD2V0w=="], "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], @@ -681,7 +681,7 @@ "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], - "flatted": ["flatted@3.4.1", "", {}, "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ=="], + "flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="], "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], @@ -719,7 +719,7 @@ "hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="], - "hookable": ["hookable@6.0.1", "", {}, "sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw=="], + "hookable": ["hookable@6.1.0", "", {}, "sha512-ZoKZSJgu8voGK2geJS+6YtYjvIzu9AOM/KZXsBxr83uhLL++e9pEv/dlgwgy3dvHg06kTz6JOh1hk3C8Ceiymw=="], "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], @@ -761,7 +761,7 @@ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - "jose": ["jose@6.2.1", "", {}, "sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw=="], + "jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], @@ -901,7 +901,7 @@ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], @@ -943,7 +943,7 @@ "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], - "react-router": ["react-router@7.13.1", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA=="], + "react-router": ["react-router@7.13.2", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-tX1Aee+ArlKQP+NIUd7SE6Li+CiGKwQtbS+FfRxPX6Pe4vHOo6nr9d++u5cwg+Z8K/x8tP+7qLmujDtfrAoUJA=="], "react-select": ["react-select@5.10.2", "", { "dependencies": { "@babel/runtime": "^7.12.0", "@emotion/cache": "^11.4.0", "@emotion/react": "^11.8.1", "@floating-ui/dom": "^1.0.1", "@types/react-transition-group": "^4.4.0", "memoize-one": "^6.0.0", "prop-types": "^15.6.0", "react-transition-group": "^4.3.0", "use-isomorphic-layout-effect": "^1.2.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Z33nHdEFWq9tfnfVXaiM12rbJmk+QjFEztWLtmXqQhz6Al4UZZ9xc0wiatmGtUOCCnHN0WizL3tCMYRENX4rVQ=="], @@ -951,7 +951,7 @@ "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], - "recharts": ["recharts@3.8.0", "", { "dependencies": { "@reduxjs/toolkit": "^1.9.0 || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Z/m38DX3L73ExO4Tpc9/iZWHmHnlzWG4njQbxsF5aSjwqmHNDDIm0rdEBArkwsBvR8U6EirlEHiQNYWCVh9sGQ=="], + "recharts": ["recharts@3.8.1", "", { "dependencies": { "@reduxjs/toolkit": "^1.9.0 || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg=="], "redux": ["redux@5.0.1", "", {}, "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="], @@ -963,7 +963,7 @@ "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], - "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="], + "rollup": ["rollup@4.60.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.0", "@rollup/rollup-android-arm64": "4.60.0", "@rollup/rollup-darwin-arm64": "4.60.0", "@rollup/rollup-darwin-x64": "4.60.0", "@rollup/rollup-freebsd-arm64": "4.60.0", "@rollup/rollup-freebsd-x64": "4.60.0", "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", "@rollup/rollup-linux-arm-musleabihf": "4.60.0", "@rollup/rollup-linux-arm64-gnu": "4.60.0", "@rollup/rollup-linux-arm64-musl": "4.60.0", "@rollup/rollup-linux-loong64-gnu": "4.60.0", "@rollup/rollup-linux-loong64-musl": "4.60.0", "@rollup/rollup-linux-ppc64-gnu": "4.60.0", "@rollup/rollup-linux-ppc64-musl": "4.60.0", "@rollup/rollup-linux-riscv64-gnu": "4.60.0", "@rollup/rollup-linux-riscv64-musl": "4.60.0", "@rollup/rollup-linux-s390x-gnu": "4.60.0", "@rollup/rollup-linux-x64-gnu": "4.60.0", "@rollup/rollup-linux-x64-musl": "4.60.0", "@rollup/rollup-openbsd-x64": "4.60.0", "@rollup/rollup-openharmony-arm64": "4.60.0", "@rollup/rollup-win32-arm64-msvc": "4.60.0", "@rollup/rollup-win32-ia32-msvc": "4.60.0", "@rollup/rollup-win32-x64-gnu": "4.60.0", "@rollup/rollup-win32-x64-msvc": "4.60.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ=="], "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], @@ -1063,7 +1063,7 @@ "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], - "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], + "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -1071,9 +1071,9 @@ "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="], - "typescript-eslint": ["typescript-eslint@8.57.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.57.0", "@typescript-eslint/parser": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0", "@typescript-eslint/utils": "8.57.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-W8GcigEMEeB07xEZol8oJ26rigm3+bfPHxHvwbYUlu1fUDsGuQ7Hiskx5xGW/xM4USc9Ephe3jtv7ZYPQntHeA=="], + "typescript-eslint": ["typescript-eslint@8.57.2", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.57.2", "@typescript-eslint/parser": "8.57.2", "@typescript-eslint/typescript-estree": "8.57.2", "@typescript-eslint/utils": "8.57.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A=="], "uncontrollable": ["uncontrollable@7.2.1", "", { "dependencies": { "@babel/runtime": "^7.6.3", "@types/react": ">=16.9.11", "invariant": "^2.2.4", "react-lifecycles-compat": "^3.0.4" }, "peerDependencies": { "react": ">=15.0.0" } }, "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ=="], @@ -1109,7 +1109,7 @@ "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - "yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="], + "yaml": ["yaml@1.10.3", "", {}, "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA=="], "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], @@ -1127,6 +1127,8 @@ "@babel/traverse/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "@darkruby/assets-web/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "@emotion/babel-plugin/convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], @@ -1167,7 +1169,7 @@ "send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], } diff --git a/package.json b/package.json index 33dd4946..568a3850 100644 --- a/package.json +++ b/package.json @@ -6,14 +6,14 @@ "dependencies": { "date-fns": "^4.1.0", "fp-ts": "^2.16.11", - "io-ts-types": "^0.5.19", - "ms": "^2.1.3", - "prettier": "^3.6.2" + "io-ts": "~2.2.21", + "io-ts-types": "^0.5.19" }, "devDependencies": { + "prettier": "^3.6.2", "@types/bun": "1.3.11", "bun-types": "^1.3.11", - "typescript": "~5.9.3" + "typescript": "~6.0.2" }, "scripts": { "web:dev": "cd packages/web && bun run dev", @@ -23,9 +23,9 @@ "backend:test": "cd packages/backend && bun test", "backend:check": "cd packages/backend && bun run check", "backend:build": "cd packages/backend && bun run build", - "assets-core:check": "cd packages/core && bun run check", - "assets-core:test": "cd packages/core && bun run test", - "check": "bun run assets-core:check && bun run backend:check && bun run web:check", + "core:check": "cd packages/core && bun run check", + "core:test": "cd packages/core && bun run test", + "check": "bun run core:check && bun run backend:check && bun run web:check", "build": "bun run web:build && bun run backend:build" }, "workspaces": [ diff --git a/packages/backend/package.json b/packages/backend/package.json index 3cd78538..0f3d371b 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -11,18 +11,18 @@ }, "dependencies": { "@darkruby/assets-core": "workspace:*", - "@types/ms": "^2.1.0", + "ms": "^2.1.3", "cors": "^2.8.5", "csv-parse": "^6.1.0", "csv-stringify": "^6.6.0", "express": "~4.21.2", "heap-js": "^2.7.1", - "io-ts": "~2.2.21", "jsonwebtoken": "^9.0.2", "lru-cache": "^11.0.2", "nodejs-polars": "^0.24.0" }, "devDependencies": { + "@types/ms": "^2.1.0", "@types/cors": "^2.8.17", "@types/faker": "5", "@types/jsonwebtoken": "^9.0.9", diff --git a/packages/backend/src/enrichment/asset.ts b/packages/backend/src/enrichment/asset.ts index 428f6219..9c373427 100644 --- a/packages/backend/src/enrichment/asset.ts +++ b/packages/backend/src/enrichment/asset.ts @@ -100,3 +100,14 @@ export const calcAssetWeights = (assets: EnrichedAsset[]): EnrichedAsset[] => { }) ); }; + +export type AssetEnricher = ReturnType; + +export const createAssetEnricher = (repo: Repository, yahooApi: YahooApi) => { + return { + enrich: getAssetEnricher(repo, yahooApi), + enrichMany: getAssetsEnricher(repo, yahooApi), + enrichMaybe: getOptionalAssetEnricher(repo, yahooApi), + calcAssetWeights + }; +}; diff --git a/packages/backend/src/enrichment/cached.ts b/packages/backend/src/enrichment/cached.ts new file mode 100644 index 00000000..c2158909 --- /dev/null +++ b/packages/backend/src/enrichment/cached.ts @@ -0,0 +1,68 @@ +import ms from "ms"; +import type { Repository } from "../repository"; +import type { AppCache } from "../services/cache"; +import type { YahooApi } from "../yahoo/client"; +import { createAssetEnricher, type AssetEnricher } from "./asset"; +import { createPortfolioEnricher, type PortfolioEnricher } from "./portfolio"; +import { createSummaryEnricher, type SummaryEnricher } from "./summary"; +import { createTxEnricher, type TxEnricher } from "./tx"; + +export type Enricher = { + tx: TxEnricher; + asset: AssetEnricher; + portfolio: PortfolioEnricher; + summary: SummaryEnricher; +}; + +export const createEnricher = ( + repo: Repository, + yahooApi: YahooApi, + cache: AppCache +): Enricher => { + const ENRICH_TTL = ms("1min"); + const txEnricher = createTxEnricher(yahooApi); + const assetEnricher = createAssetEnricher(repo, yahooApi); + const portfolioEnricher = createPortfolioEnricher(repo, yahooApi); + const summaryEnricher = createSummaryEnricher(); + return { + tx: { + // enrich: (tx: GetTx) => { + // const key = `enrich-tx-${tx.id}`; + // const action = () => txEnricher.enrich(tx); + // return cache.cachedAction(key, action, ENRICH_TTL); + // }, + // enrichMany: (txs: GetTx[]) => { + // const key = `enrich-txs-${txs.map((tx) => tx.id).join("-")}`; + // const action = () => txEnricher.enrichMany(txs); + // return cache.cachedAction(key, action, ENRICH_TTL); + // }, + ...txEnricher + }, + asset: { + // enrich: (asset: GetAsset, range: ChartRange = DEFAULT_CHART_RANGE) => { + // const key = `enrich-asset-${asset.id}-${range}`; + // const action = () => assetEnricher.enrich(asset, range); + // return cache.cachedAction(key, action, ENRICH_TTL); + // }, + // enrichMaybe: ( + // asset: Optional, + // range: ChartRange = DEFAULT_CHART_RANGE + // ) => { + // const key = `enrich-asset-${asset?.id}-${range}`; + // const action = () => assetEnricher.enrichMaybe(asset, range); + // return cache.cachedAction(key, action, ENRICH_TTL); + // }, + // enrichMany: ( + // assets: GetAsset[], + // range: ChartRange = DEFAULT_CHART_RANGE + // ) => { + // const key = `enrich-assets-${assets.map((a) => a.id).join("-")}-${range}`; + // const action = () => assetEnricher.enrichMany(assets, range); + // return cache.cachedAction(key, action, ENRICH_TTL); + // }, + ...assetEnricher + }, + portfolio: { ...portfolioEnricher }, + summary: { ...summaryEnricher } + }; +}; diff --git a/packages/backend/src/enrichment/index.ts b/packages/backend/src/enrichment/index.ts index 43621f1b..d821b867 100644 --- a/packages/backend/src/enrichment/index.ts +++ b/packages/backend/src/enrichment/index.ts @@ -1,4 +1,5 @@ export * from "./asset"; +export * from "./cached"; export * from "./portfolio"; export * from "./summary"; export * from "./tx"; diff --git a/packages/backend/src/enrichment/portfolio.ts b/packages/backend/src/enrichment/portfolio.ts index 6302b0b8..6786a896 100644 --- a/packages/backend/src/enrichment/portfolio.ts +++ b/packages/backend/src/enrichment/portfolio.ts @@ -199,3 +199,17 @@ export const calcPortfolioWeights = ( }) ); }; + +export type PortfolioEnricher = ReturnType; + +export const createPortfolioEnricher = ( + repo: Repository, + yahooApi: YahooApi +) => { + return { + enrich: getPortfolioEnricher(repo, yahooApi), + enrichMany: getPortfoliosEnricher(repo, yahooApi), + enrichMaybe: getOptionalPorfolioEnricher(repo, yahooApi), + calcPortfolioWeights + }; +}; diff --git a/packages/backend/src/enrichment/summary.ts b/packages/backend/src/enrichment/summary.ts index f3070f8b..17ea8017 100644 --- a/packages/backend/src/enrichment/summary.ts +++ b/packages/backend/src/enrichment/summary.ts @@ -147,3 +147,8 @@ export const enrichSummary = ( breakEven }; }; + +export type SummaryEnricher = ReturnType; +export const createSummaryEnricher = () => { + return { enrich: enrichSummary }; +}; diff --git a/packages/backend/src/enrichment/tx.ts b/packages/backend/src/enrichment/tx.ts index 62b023f4..e2375f8f 100644 --- a/packages/backend/src/enrichment/tx.ts +++ b/packages/backend/src/enrichment/tx.ts @@ -25,7 +25,7 @@ export const getTxEnricher = const [pnl, pnlPct] = calcPnl({ before: tx.cost, after: value }); return { ...tx, value, pnl, pnl_pct: pnlPct }; } - // consider using EnrichTx decoder + // todo: use EnrichTx decoder const { pnl, value, pnl_pct, ...rest } = tx; return { ...rest, pnl: pnl!, pnl_pct: pnl_pct!, value: value! }; }) @@ -39,3 +39,12 @@ export const getTxsEnricher = // return pipe(txs, TE.traverseSeqArray(enrichTx)) as Action; // <-- sequential to take advantage of yahoo cache return pipe(txs, TE.traverseArray(enrichTx)) as Action; // <-- sequential to take advantage of yahoo cache }; + +export type TxEnricher = ReturnType; + +export const createTxEnricher = (yahooApi: YahooApi) => { + return { + enrich: getTxEnricher(yahooApi), + enrichMany: getTxsEnricher(yahooApi) + }; +}; diff --git a/packages/backend/src/handlers/asset.ts b/packages/backend/src/handlers/asset.ts index b877af14..f8248223 100644 --- a/packages/backend/src/handlers/asset.ts +++ b/packages/backend/src/handlers/asset.ts @@ -45,7 +45,7 @@ export const getAsset: HandlerTask, Context> = ({ export const createAsset: HandlerTask, Context> = ({ params: [req, res], - context: { repo, yahooApi, service } + context: { service } }) => pipe( TE.Do, diff --git a/packages/backend/src/handlers/index.ts b/packages/backend/src/handlers/index.ts index 56da42ca..afda372c 100644 --- a/packages/backend/src/handlers/index.ts +++ b/packages/backend/src/handlers/index.ts @@ -13,6 +13,7 @@ import * as user from "./user"; import * as yahoo from "./yahoo"; export type Handlers = ReturnType; + export const createHandlers = ( expressify: (task: HandlerTask) => express.RequestHandler ) => ({ diff --git a/packages/backend/src/handlers/prefs.ts b/packages/backend/src/handlers/prefs.ts index ab2422f2..c73eafb3 100644 --- a/packages/backend/src/handlers/prefs.ts +++ b/packages/backend/src/handlers/prefs.ts @@ -13,7 +13,7 @@ export const getPrefs: HandlerTask = ({ export const updatePrefs: HandlerTask = ({ params: [req, res], - context: { repo, service } + context: { service } }) => pipe( service.auth.requireUserId(res), diff --git a/packages/backend/src/handlers/profile.ts b/packages/backend/src/handlers/profile.ts index 8b32685f..b685a427 100644 --- a/packages/backend/src/handlers/profile.ts +++ b/packages/backend/src/handlers/profile.ts @@ -43,7 +43,7 @@ export const updatePassword: HandlerTask = ({ export const deleteProfile: HandlerTask, Context> = ({ params: [, res], - context: { repo, service } + context: { service } }) => pipe( TE.Do, diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index f84c43dd..163f8574 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -7,6 +7,7 @@ import * as TE from "fp-ts/lib/TaskEither"; import { LRUCache } from "lru-cache"; import { Server } from "node:http"; import path from "node:path"; +import { createEnricher } from "./enrichment"; import { createRequestHandler } from "./fp-express"; import { createHandlers } from "./handlers"; import type { Context } from "./handlers/context"; @@ -175,9 +176,12 @@ const app = () => TE.Do, TE.bind("repo", () => repository(config)), TE.bind("cache", () => cache(config)), - TE.bind("yahooApi", ({ cache }) => TE.of(cachedYahooApi(cache))), - TE.bind("service", ({ repo, yahooApi }) => - TE.of(createWebService(repo, yahooApi)) + TE.let("yahooApi", ({ cache }) => cachedYahooApi(cache)), + TE.let("enricher", ({ repo, cache, yahooApi }) => + createEnricher(repo, yahooApi, cache) + ), + TE.bind("service", ({ repo, yahooApi, enricher }) => + TE.of(createWebService(repo, yahooApi, enricher)) ) ) ), diff --git a/packages/backend/src/services/asset.ts b/packages/backend/src/services/asset.ts index 6ccf2fa4..e49315b1 100644 --- a/packages/backend/src/services/asset.ts +++ b/packages/backend/src/services/asset.ts @@ -12,11 +12,7 @@ import { liftTE } from "@darkruby/assets-core/src/decoders/util"; import { pipe } from "fp-ts/function"; import * as TE from "fp-ts/TaskEither"; import { mapWebError } from "../domain/error"; -import { - getAssetEnricher, - getAssetsEnricher, - getOptionalAssetEnricher -} from "../enrichment"; +import { type AssetEnricher } from "../enrichment"; import type { WebAction } from "../fp-express"; import type { Repository } from "../repository"; import type { YahooApi } from "../yahoo/client"; @@ -24,34 +20,32 @@ import type { YahooApi } from "../yahoo/client"; const assetDecoder = liftTE(PostAssetDecoder); export const getAsset = - (repo: Repository, yahooApi: YahooApi) => + (repo: Repository, { enrichMaybe }: AssetEnricher) => ( assetId: AssetId, portfolioId: PortfolioId, userId: UserId, range: ChartRange ): WebAction> => { - const enrichAsset = getOptionalAssetEnricher(repo, yahooApi); return pipe( TE.Do, TE.bind("asset", () => repo.asset.get(assetId, portfolioId, userId)), - TE.chain(({ asset }) => enrichAsset(asset, range)), + TE.chain(({ asset }) => enrichMaybe(asset, range)), mapWebError ); }; export const getAssets = - (repo: Repository, yahooApi: YahooApi) => + (repo: Repository, { enrichMany }: AssetEnricher) => ( userId: UserId, portfolioId: PortfolioId, range: ChartRange ): WebAction => { - const enrichAssets = getAssetsEnricher(repo, yahooApi); return pipe( TE.Do, TE.bind("assets", () => repo.asset.getAll(portfolioId, userId)), - TE.chain(({ assets }) => enrichAssets(assets, range)), + TE.chain(({ assets }) => enrichMany(assets, range)), mapWebError ); }; @@ -71,13 +65,12 @@ export const deleteAsset = }; export const createAsset = - (repo: Repository, yahooApi: YahooApi) => + (repo: Repository, yahooApi: YahooApi, { enrich }: AssetEnricher) => ( portfolioId: PortfolioId, userId: UserId, payload: unknown ): WebAction => { - const enrichAsset = getAssetEnricher(repo, yahooApi); return pipe( TE.Do, TE.bind("asset", () => assetDecoder(payload)), @@ -85,20 +78,19 @@ export const createAsset = TE.bind("created", ({ asset }) => repo.asset.create(asset, portfolioId, userId) ), - TE.chain(({ created }) => enrichAsset(created)), + TE.chain(({ created }) => enrich(created)), mapWebError ); }; export const updateAsset = - (repo: Repository, yahooApi: YahooApi) => + (repo: Repository, yahooApi: YahooApi, { enrich }: AssetEnricher) => ( assetId: AssetId, portfolioId: PortfolioId, userId: UserId, payload: unknown ): WebAction => { - const enrichAsset = getAssetEnricher(repo, yahooApi); return pipe( TE.Do, TE.bind("asset", () => assetDecoder(payload)), @@ -106,7 +98,7 @@ export const updateAsset = TE.bind("updated", ({ asset }) => repo.asset.update(assetId, portfolioId, userId, asset) ), - TE.chain(({ updated }) => enrichAsset(updated)), + TE.chain(({ updated }) => enrich(updated)), mapWebError ); }; diff --git a/packages/backend/src/services/cache.ts b/packages/backend/src/services/cache.ts index ccb1bba8..d029917f 100644 --- a/packages/backend/src/services/cache.ts +++ b/packages/backend/src/services/cache.ts @@ -1,4 +1,4 @@ -import { type Action } from "@darkruby/assets-core"; +import { defined, type Action } from "@darkruby/assets-core"; import * as O from "fp-ts/lib/Option"; import * as TE from "fp-ts/lib/TaskEither"; import { pipe } from "fp-ts/lib/function"; @@ -12,7 +12,7 @@ export type Cache = LRUCache; const toKey = (...k: Stringifiable[]) => createHash("md5").update(k.map(String).join("")).digest("hex"); -const log = createLogger("cache"); +const log = createLogger("Memory-Cache"); const has = (cache: Cache) => (key: string) => cache.has(toKey(key)); @@ -21,7 +21,7 @@ const getter = (key: string): O.Option => { return pipe( O.tryCatch(() => cache.get(toKey(key))), - O.filter((x) => x !== null && x !== undefined) + O.filter(defined /*(x) => x !== null && x !== undefined*/) ); }; @@ -29,8 +29,7 @@ const setter = (cache: Cache) => (key: string, val: T, ttl?: number): O.Option => { return pipe( - O.of(val), - O.chain(() => O.tryCatch(() => cache.set(toKey(key), val, { ttl }))), + O.tryCatch(() => cache.set(toKey(key), val, { ttl })), O.map(() => val) ); }; @@ -41,10 +40,10 @@ const cachedAction = const get = getter(cache); const res = get(key); if (O.isSome(res)) { - // log.debug(`hit for ${key}`); + log.debug(`hit for ${key}`); return TE.of(res.value as T); } - // log.debug(`miss for ${key}`); + log.debug(`miss for ${key}`); const set = setter(cache); return pipe( diff --git a/packages/backend/src/services/index.ts b/packages/backend/src/services/index.ts index 46977e88..52cab27d 100644 --- a/packages/backend/src/services/index.ts +++ b/packages/backend/src/services/index.ts @@ -1,3 +1,4 @@ +import type { Enricher } from "../enrichment"; import type { Repository } from "../repository"; import type { YahooApi } from "../yahoo/client"; import * as asset from "./asset"; @@ -9,7 +10,11 @@ import * as user from "./user"; export type WebService = ReturnType; -export const createWebService = (repo: Repository, yahooApi: YahooApi) => { +export const createWebService = ( + repo: Repository, + yahooApi: YahooApi, + enricher: Enricher +) => { return { auth: { createToken: auth.createToken, @@ -29,11 +34,11 @@ export const createWebService = (repo: Repository, yahooApi: YahooApi) => { updateOwnPasswordOnly: user.updateOwnPasswordOnly(repo) }, assets: { - get: asset.getAsset(repo, yahooApi), - getMany: asset.getAssets(repo, yahooApi), + get: asset.getAsset(repo, enricher.asset), + getMany: asset.getAssets(repo, enricher.asset), delete: asset.deleteAsset(repo), - create: asset.createAsset(repo, yahooApi), - update: asset.updateAsset(repo, yahooApi), + create: asset.createAsset(repo, yahooApi, enricher.asset), + update: asset.updateAsset(repo, yahooApi, enricher.asset), move: asset.moveAsset(repo) }, portfolio: { diff --git a/packages/backend/src/yahoo/cached.ts b/packages/backend/src/yahoo/cached.ts index f648750e..0fa1df19 100644 --- a/packages/backend/src/yahoo/cached.ts +++ b/packages/backend/src/yahoo/cached.ts @@ -5,56 +5,57 @@ import { } from "@darkruby/assets-core/src/decoders/yahoo/meta"; import { createLogger } from "../fp-express"; +import ms from "ms"; import type { AppCache } from "../services/cache"; import { yahooApi as rawYahooApi, type YahooApi } from "./client"; const logger = createLogger("cached yahoo"); export const cachedYahooApi = (cache: AppCache): YahooApi => { - const CHART_TTL = 1000 * 60 * 10; // 1 minutes - const SEARCH_TTL = 1000 * 60 * 10; // 10 minutes - const LOOKUP_TTL = 1000 * 60 * 60; // 1 hr + const MIN_1 = ms("1min"); + const MIN_10 = ms("10min"); + const HOUR_1 = ms("1hr"); const search = (term: string) => cache.cachedAction( `yahoo-search-${term}`, () => rawYahooApi.search(term), - SEARCH_TTL + MIN_10 ); const chart = (symbol: string, range?: ChartRange) => cache.cachedAction( `yahoo-chart-${symbol}-${range ?? DEFAULT_CHART_RANGE}`, () => rawYahooApi.chart(symbol, range), - CHART_TTL + MIN_1 ); const meta = (symbol: string) => cache.cachedAction( `yahoo-meta-${symbol}`, () => rawYahooApi.meta(symbol), - LOOKUP_TTL + HOUR_1 ); const fxRate = (ccy: string, base: Ccy, date?: Optional) => cache.cachedAction( `yahoo-ccy-lookup-${ccy}-${base}-${date?.getTime() ?? "latest"}`, () => rawYahooApi.fxRate(ccy, base, date), - LOOKUP_TTL + HOUR_1 ); const checkTickerExists = (symbol: string) => cache.cachedAction( `yahoo-check-ticker-${symbol}`, () => rawYahooApi.checkTickerExists(symbol), - LOOKUP_TTL + HOUR_1 ); const fxRates = (ccy: string, base: Ccy) => cache.cachedAction( `yahoo-fx-rates-${ccy}-${base}`, () => rawYahooApi.fxRates(ccy, base), - LOOKUP_TTL + HOUR_1 ); return { diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 0f02816f..a979c738 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -1,5 +1,8 @@ { "compilerOptions": { + "types": [ + "bun-types" + ], // Enable latest features "lib": [ "ESNext", From 2c14f99c97f427694d7fc4c5a6ec4ba0e5e9d4e0 Mon Sep 17 00:00:00 2001 From: darkruby Date: Fri, 27 Mar 2026 23:10:10 +0000 Subject: [PATCH 02/12] wire up all caching --- bun.lock | 4 +- packages/backend/src/enrichment/asset.ts | 35 +++--- packages/backend/src/enrichment/cached.ts | 121 +++++++++++++------ packages/backend/src/enrichment/portfolio.ts | 31 +++-- packages/backend/src/enrichment/tx.ts | 8 +- packages/backend/src/services/cache.ts | 6 +- packages/backend/src/services/index.ts | 16 +-- packages/backend/src/services/portfolio.ts | 28 ++--- packages/backend/src/services/tx.ts | 24 ++-- packages/web/package.json | 2 +- 10 files changed, 150 insertions(+), 125 deletions(-) diff --git a/bun.lock b/bun.lock index c92b5416..104c9b56 100644 --- a/bun.lock +++ b/bun.lock @@ -74,7 +74,7 @@ "react-dropzone": "^14.3.8", "react-router": "^7.3.0", "react-select": "^5.10.1", - "recharts": "^3.8.0", + "recharts": "3.8.0", }, "devDependencies": { "@eslint/js": "^9.39.1", @@ -951,7 +951,7 @@ "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], - "recharts": ["recharts@3.8.1", "", { "dependencies": { "@reduxjs/toolkit": "^1.9.0 || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg=="], + "recharts": ["recharts@3.8.0", "", { "dependencies": { "@reduxjs/toolkit": "^1.9.0 || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Z/m38DX3L73ExO4Tpc9/iZWHmHnlzWG4njQbxsF5aSjwqmHNDDIm0rdEBArkwsBvR8U6EirlEHiQNYWCVh9sGQ=="], "redux": ["redux@5.0.1", "", {}, "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="], diff --git a/packages/backend/src/enrichment/asset.ts b/packages/backend/src/enrichment/asset.ts index 9c373427..4a39caab 100644 --- a/packages/backend/src/enrichment/asset.ts +++ b/packages/backend/src/enrichment/asset.ts @@ -14,20 +14,19 @@ import * as TE from "fp-ts/lib/TaskEither"; import type { Repository } from "../repository"; import { type YahooApi } from "../yahoo/client"; import { $enrichedAssetBase, $enrichedAssetCcy, txsWithRates } from "./returns"; -import { getTxsEnricher } from "./tx"; +import type { TxEnricher } from "./tx"; -export const getAssetEnricher = - (repo: Repository, yahooApi: YahooApi) => +const getAssetEnricher = + (repo: Repository, yahooApi: YahooApi, { enrichMany }: TxEnricher) => ( asset: GetAsset, range: ChartRange = DEFAULT_CHART_RANGE ): Action => { - const enrichTxs = getTxsEnricher(yahooApi); return pipe( TE.Do, TE.bind("chart", () => yahooApi.chart(asset.ticker, range)), TE.bind("txs", () => repo.tx.getAll(asset.id, asset.user_id, false)), - TE.bind("enrichedTxs", ({ txs }) => enrichTxs(txs)), + TE.bind("enrichedTxs", ({ txs }) => enrichMany(txs)), TE.bind("fxRates", ({ chart }) => yahooApi.fxRates(chart.meta.currency, asset.base_ccy) ), @@ -61,10 +60,10 @@ export const getAssetEnricher = ); }; -export const getAssetsEnricher = - (repo: Repository, yahooApi: YahooApi) => +const getAssetsEnricher = + (repo: Repository, yahooApi: YahooApi, txEnricher: TxEnricher) => (assets: GetAsset[], range?: ChartRange): Action => { - const enrichAsset = getAssetEnricher(repo, yahooApi); + const enrichAsset = getAssetEnricher(repo, yahooApi, txEnricher); return pipe( assets, TE.traverseArray((asset) => enrichAsset(asset, range)), @@ -72,20 +71,20 @@ export const getAssetsEnricher = ) as Action; }; -export const getOptionalAssetEnricher = - (repo: Repository, yahooApi: YahooApi) => +const getOptionalAssetEnricher = + (repo: Repository, yahooApi: YahooApi, txEnricher: TxEnricher) => ( asset: Optional, range?: ChartRange ): Action> => { if (asset) { - const enrichAsset = getAssetEnricher(repo, yahooApi); + const enrichAsset = getAssetEnricher(repo, yahooApi, txEnricher); return enrichAsset(asset, range); } return TE.of(null); }; -export const calcAssetWeights = (assets: EnrichedAsset[]): EnrichedAsset[] => { +const calcAssetWeights = (assets: EnrichedAsset[]): EnrichedAsset[] => { const total = pipe( assets, sum(({ base }) => base.invested) @@ -103,11 +102,15 @@ export const calcAssetWeights = (assets: EnrichedAsset[]): EnrichedAsset[] => { export type AssetEnricher = ReturnType; -export const createAssetEnricher = (repo: Repository, yahooApi: YahooApi) => { +export const createAssetEnricher = ( + repo: Repository, + yahooApi: YahooApi, + txEnricher: TxEnricher +) => { return { - enrich: getAssetEnricher(repo, yahooApi), - enrichMany: getAssetsEnricher(repo, yahooApi), - enrichMaybe: getOptionalAssetEnricher(repo, yahooApi), + enrich: getAssetEnricher(repo, yahooApi, txEnricher), + enrichMany: getAssetsEnricher(repo, yahooApi, txEnricher), + enrichMaybe: getOptionalAssetEnricher(repo, yahooApi, txEnricher), calcAssetWeights }; }; diff --git a/packages/backend/src/enrichment/cached.ts b/packages/backend/src/enrichment/cached.ts index c2158909..030539c3 100644 --- a/packages/backend/src/enrichment/cached.ts +++ b/packages/backend/src/enrichment/cached.ts @@ -1,3 +1,11 @@ +import { + DEFAULT_CHART_RANGE, + type ChartRange, + type GetAsset, + type GetPortfolio, + type GetTx, + type Optional +} from "@darkruby/assets-core"; import ms from "ms"; import type { Repository } from "../repository"; import type { AppCache } from "../services/cache"; @@ -20,49 +28,82 @@ export const createEnricher = ( cache: AppCache ): Enricher => { const ENRICH_TTL = ms("1min"); + const txEnricher = createTxEnricher(yahooApi); - const assetEnricher = createAssetEnricher(repo, yahooApi); - const portfolioEnricher = createPortfolioEnricher(repo, yahooApi); - const summaryEnricher = createSummaryEnricher(); - return { - tx: { - // enrich: (tx: GetTx) => { - // const key = `enrich-tx-${tx.id}`; - // const action = () => txEnricher.enrich(tx); - // return cache.cachedAction(key, action, ENRICH_TTL); - // }, - // enrichMany: (txs: GetTx[]) => { - // const key = `enrich-txs-${txs.map((tx) => tx.id).join("-")}`; - // const action = () => txEnricher.enrichMany(txs); - // return cache.cachedAction(key, action, ENRICH_TTL); - // }, - ...txEnricher + const cachedTxEnricher = { + ...txEnricher, + enrich: (tx: GetTx) => { + const key = `enrich-tx-${tx.id}`; + const action = () => txEnricher.enrich(tx); + return cache.cachedAction(key, action, ENRICH_TTL); + }, + enrichMany: (txs: GetTx[]) => { + const key = `enrich-txs-${txs.map((tx) => tx.id).join("-")}`; + const action = () => txEnricher.enrichMany(txs); + return cache.cachedAction(key, action, ENRICH_TTL); + } + }; + + const assetEnricher = createAssetEnricher(repo, yahooApi, cachedTxEnricher); + const cachedAssetEnricher = { + ...assetEnricher, + enrich: (asset: GetAsset, range: ChartRange = DEFAULT_CHART_RANGE) => { + const key = `enrich-asset-${asset.id}-${range}`; + const action = () => assetEnricher.enrich(asset, range); + return cache.cachedAction(key, action, ENRICH_TTL); + }, + enrichMaybe: ( + asset: Optional, + range: ChartRange = DEFAULT_CHART_RANGE + ) => { + const key = `enrich-asset-${asset?.id}-${range}`; + const action = () => assetEnricher.enrichMaybe(asset, range); + return cache.cachedAction(key, action, ENRICH_TTL); }, - asset: { - // enrich: (asset: GetAsset, range: ChartRange = DEFAULT_CHART_RANGE) => { - // const key = `enrich-asset-${asset.id}-${range}`; - // const action = () => assetEnricher.enrich(asset, range); - // return cache.cachedAction(key, action, ENRICH_TTL); - // }, - // enrichMaybe: ( - // asset: Optional, - // range: ChartRange = DEFAULT_CHART_RANGE - // ) => { - // const key = `enrich-asset-${asset?.id}-${range}`; - // const action = () => assetEnricher.enrichMaybe(asset, range); - // return cache.cachedAction(key, action, ENRICH_TTL); - // }, - // enrichMany: ( - // assets: GetAsset[], - // range: ChartRange = DEFAULT_CHART_RANGE - // ) => { - // const key = `enrich-assets-${assets.map((a) => a.id).join("-")}-${range}`; - // const action = () => assetEnricher.enrichMany(assets, range); - // return cache.cachedAction(key, action, ENRICH_TTL); - // }, - ...assetEnricher + enrichMany: ( + assets: GetAsset[], + range: ChartRange = DEFAULT_CHART_RANGE + ) => { + const key = `enrich-assets-${assets.map((a) => a.id).join("-")}-${range}`; + const action = () => assetEnricher.enrichMany(assets, range); + return cache.cachedAction(key, action, ENRICH_TTL); + } + }; + + const portfolioEnricher = createPortfolioEnricher(repo, cachedAssetEnricher); + const cachedPortfolioEnricher = { + ...portfolioEnricher, + enrich: ( + portfolio: GetPortfolio, + range: ChartRange = DEFAULT_CHART_RANGE + ) => { + const key = `enrich-asset-${portfolio.id}-${range}`; + const action = () => portfolioEnricher.enrich(portfolio, range); + return cache.cachedAction(key, action, ENRICH_TTL); }, - portfolio: { ...portfolioEnricher }, + enrichMaybe: ( + portfolio: Optional, + range: ChartRange = DEFAULT_CHART_RANGE + ) => { + const key = `enrich-asset-${portfolio?.id}-${range}`; + const action = () => portfolioEnricher.enrichMaybe(portfolio, range); + return cache.cachedAction(key, action, ENRICH_TTL); + }, + enrichMany: ( + portfolios: GetPortfolio[], + range: ChartRange = DEFAULT_CHART_RANGE + ) => { + const key = `enrich-portfolio-${portfolios.map((p) => p.id).join("-")}-${range}`; + const action = () => portfolioEnricher.enrichMany(portfolios, range); + return cache.cachedAction(key, action, ENRICH_TTL); + } + }; + const summaryEnricher = createSummaryEnricher(); + + return { + tx: cachedTxEnricher, + asset: cachedAssetEnricher, + portfolio: cachedPortfolioEnricher, summary: { ...summaryEnricher } }; }; diff --git a/packages/backend/src/enrichment/portfolio.ts b/packages/backend/src/enrichment/portfolio.ts index 6786a896..730c914a 100644 --- a/packages/backend/src/enrichment/portfolio.ts +++ b/packages/backend/src/enrichment/portfolio.ts @@ -22,8 +22,7 @@ import { pipe } from "fp-ts/lib/function"; import * as Ord from "fp-ts/lib/Ord"; import * as TE from "fp-ts/lib/TaskEither"; import type { Repository } from "../repository"; -import type { YahooApi } from "../yahoo/client"; -import { calcAssetWeights, getAssetsEnricher } from "./asset"; +import type { AssetEnricher } from "./asset"; import { combineAssetCharts, commonAssetRanges } from "./chart"; const sumInvested = sum(({ base }) => base.invested); @@ -105,21 +104,19 @@ const portfolioTotals = (assets: EnrichedAsset[]): Totals => { return { returnValue, returnPct }; }; -export const getPortfolioEnricher = - (repo: Repository, yahooApi: YahooApi) => +const getPortfolioEnricher = + (repo: Repository, { enrichMany, calcAssetWeights }: AssetEnricher) => ( portfolio: GetPortfolio, range: ChartRange = DEFAULT_CHART_RANGE ): Action => { - const enrichAssets = getAssetsEnricher(repo, yahooApi); - return pipe( TE.Do, TE.apS("portfolio", TE.of(portfolio)), TE.bind("assets", () => pipe( repo.asset.getAll(portfolio.id, portfolio.user_id), - TE.chain((assets) => enrichAssets(assets, range)), + TE.chain((assets) => enrichMany(assets, range)), TE.map(A.filter((a) => Boolean(a.invested))), TE.map(calcAssetWeights) ) @@ -155,13 +152,13 @@ export const getPortfolioEnricher = ); }; -export const getPortfoliosEnricher = - (repo: Repository, yahooApi: YahooApi) => +const getPortfoliosEnricher = + (repo: Repository, assetEnricher: AssetEnricher) => ( portfolios: GetPortfolio[], range?: ChartRange ): Action => { - const enrichPortfolio = getPortfolioEnricher(repo, yahooApi); + const enrichPortfolio = getPortfolioEnricher(repo, assetEnricher); return pipe( portfolios, TE.traverseArray((p) => enrichPortfolio(p, range)), @@ -170,19 +167,19 @@ export const getPortfoliosEnricher = }; export const getOptionalPorfolioEnricher = - (repo: Repository, yahooApi: YahooApi) => + (repo: Repository, assetEnricher: AssetEnricher) => ( portfolio: Optional, range?: ChartRange ): Action> => { if (portfolio) { - const enrichPortfolio = getPortfolioEnricher(repo, yahooApi); + const enrichPortfolio = getPortfolioEnricher(repo, assetEnricher); return enrichPortfolio(portfolio, range); } return TE.of(null); }; -export const calcPortfolioWeights = ( +const calcPortfolioWeights = ( portfolios: EnrichedPortfolio[] ): EnrichedPortfolio[] => { const total = pipe( @@ -204,12 +201,12 @@ export type PortfolioEnricher = ReturnType; export const createPortfolioEnricher = ( repo: Repository, - yahooApi: YahooApi + assetEnricher: AssetEnricher ) => { return { - enrich: getPortfolioEnricher(repo, yahooApi), - enrichMany: getPortfoliosEnricher(repo, yahooApi), - enrichMaybe: getOptionalPorfolioEnricher(repo, yahooApi), + enrich: getPortfolioEnricher(repo, assetEnricher), + enrichMany: getPortfoliosEnricher(repo, assetEnricher), + enrichMaybe: getOptionalPorfolioEnricher(repo, assetEnricher), calcPortfolioWeights }; }; diff --git a/packages/backend/src/enrichment/tx.ts b/packages/backend/src/enrichment/tx.ts index e2375f8f..8f4741ce 100644 --- a/packages/backend/src/enrichment/tx.ts +++ b/packages/backend/src/enrichment/tx.ts @@ -9,7 +9,7 @@ import { pipe } from "fp-ts/lib/function"; import * as TE from "fp-ts/lib/TaskEither"; import { type YahooApi } from "../yahoo/client"; -export const getTxEnricher = +const getTxEnricher = (yahooApi: YahooApi) => (tx: GetTx): Action => { return pipe( @@ -32,12 +32,12 @@ export const getTxEnricher = ); }; -export const getTxsEnricher = +const getTxsEnricher = (yahooApi: YahooApi) => (txs: GetTx[]): Action => { const enrichTx = getTxEnricher(yahooApi); - // return pipe(txs, TE.traverseSeqArray(enrichTx)) as Action; // <-- sequential to take advantage of yahoo cache - return pipe(txs, TE.traverseArray(enrichTx)) as Action; // <-- sequential to take advantage of yahoo cache + return pipe(txs, TE.traverseSeqArray(enrichTx)) as Action; // <-- sequential to take advantage of yahoo cache + // return pipe(txs, TE.traverseArray(enrichTx)) as Action; // <-- sequential to take advantage of yahoo cache }; export type TxEnricher = ReturnType; diff --git a/packages/backend/src/services/cache.ts b/packages/backend/src/services/cache.ts index d029917f..68420819 100644 --- a/packages/backend/src/services/cache.ts +++ b/packages/backend/src/services/cache.ts @@ -12,7 +12,7 @@ export type Cache = LRUCache; const toKey = (...k: Stringifiable[]) => createHash("md5").update(k.map(String).join("")).digest("hex"); -const log = createLogger("Memory-Cache"); +const log = createLogger("cache"); const has = (cache: Cache) => (key: string) => cache.has(toKey(key)); @@ -40,10 +40,10 @@ const cachedAction = const get = getter(cache); const res = get(key); if (O.isSome(res)) { - log.debug(`hit for ${key}`); + log.debug(`HIT for ${key}`); return TE.of(res.value as T); } - log.debug(`miss for ${key}`); + log.debug(`MISS for ${key}`); const set = setter(cache); return pipe( diff --git a/packages/backend/src/services/index.ts b/packages/backend/src/services/index.ts index 52cab27d..e8fc340f 100644 --- a/packages/backend/src/services/index.ts +++ b/packages/backend/src/services/index.ts @@ -42,18 +42,18 @@ export const createWebService = ( move: asset.moveAsset(repo) }, portfolio: { - get: portfolio.getPortfolio(repo, yahooApi), - getMany: portfolio.getPortfolios(repo, yahooApi), + get: portfolio.getPortfolio(repo, enricher.portfolio), + getMany: portfolio.getPortfolios(repo, enricher.portfolio), delete: portfolio.deletePortfolio(repo), - create: portfolio.createPortfolio(repo, yahooApi), - update: portfolio.updatePortfolio(repo, yahooApi) + create: portfolio.createPortfolio(repo, enricher.portfolio), + update: portfolio.updatePortfolio(repo, enricher.portfolio) }, tx: { - get: tx.getTx(repo, yahooApi), - getMany: tx.getTxs(repo, yahooApi), + get: tx.getTx(repo, enricher.tx), + getMany: tx.getTxs(repo, enricher.tx), delete: tx.deleteTx(repo), - create: tx.createTx(repo, yahooApi), - update: tx.updateTx(repo, yahooApi), + create: tx.createTx(repo, enricher.tx), + update: tx.updateTx(repo, enricher.tx), uploadAssetTxs: tx.uploadAssetTxs(repo), deleteAllAsset: tx.deleteAllAssetTxs(repo) }, diff --git a/packages/backend/src/services/portfolio.ts b/packages/backend/src/services/portfolio.ts index 495b0e12..7d9c31b6 100644 --- a/packages/backend/src/services/portfolio.ts +++ b/packages/backend/src/services/portfolio.ts @@ -11,79 +11,69 @@ import { liftTE } from "@darkruby/assets-core/src/decoders/util"; import { pipe } from "fp-ts/function"; import * as TE from "fp-ts/TaskEither"; import { mapWebError } from "../domain/error"; -import { - getOptionalPorfolioEnricher, - getPortfolioEnricher, - getPortfoliosEnricher -} from "../enrichment"; +import { type PortfolioEnricher } from "../enrichment"; import type { WebAction } from "../fp-express"; import type { Repository } from "../repository"; -import type { YahooApi } from "../yahoo/client"; -// import { getTxs as enrichedTxsGetter } from "./tx"; const portfolioDecoder = liftTE(PostPortfolioDecoder); export const getPortfolio = - (repo: Repository, yahooApi: YahooApi) => + (repo: Repository, { enrichMaybe }: PortfolioEnricher) => ( portfolioId: PortfolioId, userId: UserId, range: ChartRange ): WebAction> => { - const enrichPortfolio = getOptionalPorfolioEnricher(repo, yahooApi); return pipe( TE.Do, TE.bind("portfolio", () => repo.portfolio.get(portfolioId, userId)), - TE.chain(({ portfolio }) => enrichPortfolio(portfolio, range)), + TE.chain(({ portfolio }) => enrichMaybe(portfolio, range)), mapWebError ); }; export const getPortfolios = - (repo: Repository, yahooApi: YahooApi) => + (repo: Repository, { enrichMany }: PortfolioEnricher) => ( userId: UserId, range: ChartRange ): WebAction => { - const enrichPortfolios = getPortfoliosEnricher(repo, yahooApi); return pipe( TE.Do, TE.bind("portfolios", () => repo.portfolio.getAll(userId)), - TE.chain(({ portfolios }) => enrichPortfolios(portfolios, range)), + TE.chain(({ portfolios }) => enrichMany(portfolios, range)), mapWebError ); }; export const createPortfolio = - (repo: Repository, yahooApi: YahooApi) => + (repo: Repository, { enrich }: PortfolioEnricher) => (userId: UserId, payload: unknown): WebAction => { - const enrichPortfolio = getPortfolioEnricher(repo, yahooApi); return pipe( TE.Do, TE.bind("portfolio", () => portfolioDecoder(payload)), TE.bind("created", ({ portfolio }) => repo.portfolio.create(portfolio, userId) ), - TE.chain(({ created }) => enrichPortfolio(created)), + TE.chain(({ created }) => enrich(created)), mapWebError ); }; export const updatePortfolio = - (repo: Repository, yahooApi: YahooApi) => + (repo: Repository, { enrich }: PortfolioEnricher) => ( portfolioId: PortfolioId, userId: UserId, payload: unknown ): WebAction => { - const enrichPortfolio = getPortfolioEnricher(repo, yahooApi); return pipe( TE.Do, TE.bind("portfolio", () => portfolioDecoder(payload)), TE.bind("updated", ({ portfolio }) => repo.portfolio.update(portfolioId, portfolio, userId) ), - TE.chain(({ updated }) => enrichPortfolio(updated)), + TE.chain(({ updated }) => enrich(updated)), mapWebError ); }; diff --git a/packages/backend/src/services/tx.ts b/packages/backend/src/services/tx.ts index 89da725e..c331245d 100644 --- a/packages/backend/src/services/tx.ts +++ b/packages/backend/src/services/tx.ts @@ -13,66 +13,61 @@ import { liftTE } from "@darkruby/assets-core/src/decoders/util"; import { pipe } from "fp-ts/lib/function"; import * as TE from "fp-ts/TaskEither"; import { mapWebError } from "../domain/error"; -import { getTxEnricher, getTxsEnricher } from "../enrichment"; +import type { TxEnricher } from "../enrichment"; import type { WebAction } from "../fp-express"; import type { Repository } from "../repository"; -import type { YahooApi } from "../yahoo/client"; const txDecoder = liftTE(PostTxDecoder); const txUploadDecoder = liftTE(PostTxsUploadDecoder); export const getTx = - (repo: Repository, yahooApi: YahooApi) => + (repo: Repository, { enrich }: TxEnricher) => ( txId: TxId, assetId: AssetId, _portfolioId: PortfolioId, userId: UserId ): WebAction> => { - const enrichTx = getTxEnricher(yahooApi); return pipe( repo.tx.get(txId, assetId, userId), - TE.chain((tx) => (tx ? enrichTx(tx) : TE.of(null))), + TE.chain((tx) => (tx ? enrich(tx) : TE.of(null))), mapWebError ); }; export const getTxs = - (repo: Repository, yahooApi: YahooApi) => + (repo: Repository, { enrichMany }: TxEnricher) => ( assetId: AssetId, _portfolioId: PortfolioId, userId: UserId, finalStretch: boolean = false ): WebAction => { - const enrichTxs = getTxsEnricher(yahooApi); - return pipe( repo.tx.getAll(assetId, userId, finalStretch), - TE.chain(enrichTxs), + TE.chain(enrichMany), mapWebError ); }; export const createTx = - (repo: Repository, yahooApi: YahooApi) => + (repo: Repository, { enrich }: TxEnricher) => ( assetId: AssetId, _portfolioId: PortfolioId, userId: UserId, payload: unknown ): WebAction => { - const enrichTx = getTxEnricher(yahooApi); return pipe( txDecoder(payload), TE.chain((tx) => repo.tx.create(tx, assetId, userId)), - TE.chain(enrichTx), + TE.chain(enrich), mapWebError ); }; export const updateTx = - (repo: Repository, yahooApi: YahooApi) => + (repo: Repository, { enrich }: TxEnricher) => ( txId: TxId, assetId: AssetId, @@ -80,11 +75,10 @@ export const updateTx = userId: UserId, payload: unknown ): WebAction => { - const enrichTx = getTxEnricher(yahooApi); return pipe( txDecoder(payload), TE.chain((tx) => repo.tx.update(txId, tx, assetId, userId)), - TE.chain(enrichTx), + TE.chain(enrich), mapWebError ); }; diff --git a/packages/web/package.json b/packages/web/package.json index 5a87f71d..aad4ba09 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -29,7 +29,7 @@ "react-dropzone": "^14.3.8", "react-router": "^7.3.0", "react-select": "^5.10.1", - "recharts": "^3.8.0" + "recharts": "3.8.0" }, "devDependencies": { "sass-embedded": "^1.93.2", From 94da98b3592ad6327ecd9db6892422e9f802d383 Mon Sep 17 00:00:00 2001 From: darkruby Date: Sat, 28 Mar 2026 14:42:46 +0000 Subject: [PATCH 03/12] fixed caching --- packages/backend/src/enrichment/cached.ts | 41 +++++++++++-------- packages/backend/src/enrichment/key.ts | 10 +++++ packages/backend/src/index.ts | 17 +------- .../src/repository/sql/asset/update.sql | 2 +- .../src/repository/sql/portfolio/update.sql | 2 +- .../backend/src/repository/sql/tx/update.sql | 2 +- .../sql/user/update-profile-only.sql | 2 +- .../src/repository/sql/user/update.sql | 2 +- packages/backend/src/services/cache.ts | 9 ++-- packages/backend/test/portfolio.spec.ts | 3 ++ packages/core/src/domain/tx.ts | 4 ++ packages/core/src/utils/utils.ts | 2 +- 12 files changed, 55 insertions(+), 41 deletions(-) create mode 100644 packages/backend/src/enrichment/key.ts diff --git a/packages/backend/src/enrichment/cached.ts b/packages/backend/src/enrichment/cached.ts index 030539c3..267423fc 100644 --- a/packages/backend/src/enrichment/cached.ts +++ b/packages/backend/src/enrichment/cached.ts @@ -11,10 +11,15 @@ import type { Repository } from "../repository"; import type { AppCache } from "../services/cache"; import type { YahooApi } from "../yahoo/client"; import { createAssetEnricher, type AssetEnricher } from "./asset"; +import { keys } from "./key"; import { createPortfolioEnricher, type PortfolioEnricher } from "./portfolio"; import { createSummaryEnricher, type SummaryEnricher } from "./summary"; import { createTxEnricher, type TxEnricher } from "./tx"; +const txKey = keys("enrich-tx"); +const assetKey = keys("enrich-asset"); +const portfolioKey = keys("enrich-portfolio"); + export type Enricher = { tx: TxEnricher; asset: AssetEnricher; @@ -33,14 +38,12 @@ export const createEnricher = ( const cachedTxEnricher = { ...txEnricher, enrich: (tx: GetTx) => { - const key = `enrich-tx-${tx.id}`; const action = () => txEnricher.enrich(tx); - return cache.cachedAction(key, action, ENRICH_TTL); + return cache.cachedAction(txKey.key(tx), action, ENRICH_TTL); }, enrichMany: (txs: GetTx[]) => { - const key = `enrich-txs-${txs.map((tx) => tx.id).join("-")}`; const action = () => txEnricher.enrichMany(txs); - return cache.cachedAction(key, action, ENRICH_TTL); + return cache.cachedAction(txKey.multiKey(txs), action, ENRICH_TTL); } }; @@ -48,25 +51,22 @@ export const createEnricher = ( const cachedAssetEnricher = { ...assetEnricher, enrich: (asset: GetAsset, range: ChartRange = DEFAULT_CHART_RANGE) => { - const key = `enrich-asset-${asset.id}-${range}`; const action = () => assetEnricher.enrich(asset, range); - return cache.cachedAction(key, action, ENRICH_TTL); + return cache.cachedAction(assetKey.key(asset), action, ENRICH_TTL); }, enrichMaybe: ( asset: Optional, range: ChartRange = DEFAULT_CHART_RANGE ) => { - const key = `enrich-asset-${asset?.id}-${range}`; const action = () => assetEnricher.enrichMaybe(asset, range); - return cache.cachedAction(key, action, ENRICH_TTL); + return cache.cachedAction(assetKey.maybeKey(asset), action, ENRICH_TTL); }, enrichMany: ( assets: GetAsset[], range: ChartRange = DEFAULT_CHART_RANGE ) => { - const key = `enrich-assets-${assets.map((a) => a.id).join("-")}-${range}`; const action = () => assetEnricher.enrichMany(assets, range); - return cache.cachedAction(key, action, ENRICH_TTL); + return cache.cachedAction(assetKey.multiKey(assets), action, ENRICH_TTL); } }; @@ -77,25 +77,34 @@ export const createEnricher = ( portfolio: GetPortfolio, range: ChartRange = DEFAULT_CHART_RANGE ) => { - const key = `enrich-asset-${portfolio.id}-${range}`; const action = () => portfolioEnricher.enrich(portfolio, range); - return cache.cachedAction(key, action, ENRICH_TTL); + return cache.cachedAction( + portfolioKey.key(portfolio), + action, + ENRICH_TTL + ); }, enrichMaybe: ( portfolio: Optional, range: ChartRange = DEFAULT_CHART_RANGE ) => { - const key = `enrich-asset-${portfolio?.id}-${range}`; const action = () => portfolioEnricher.enrichMaybe(portfolio, range); - return cache.cachedAction(key, action, ENRICH_TTL); + return cache.cachedAction( + portfolioKey.maybeKey(portfolio), + action, + ENRICH_TTL + ); }, enrichMany: ( portfolios: GetPortfolio[], range: ChartRange = DEFAULT_CHART_RANGE ) => { - const key = `enrich-portfolio-${portfolios.map((p) => p.id).join("-")}-${range}`; const action = () => portfolioEnricher.enrichMany(portfolios, range); - return cache.cachedAction(key, action, ENRICH_TTL); + return cache.cachedAction( + portfolioKey.multiKey(portfolios), + action, + ENRICH_TTL + ); } }; const summaryEnricher = createSummaryEnricher(); diff --git a/packages/backend/src/enrichment/key.ts b/packages/backend/src/enrichment/key.ts new file mode 100644 index 00000000..b6e59801 --- /dev/null +++ b/packages/backend/src/enrichment/key.ts @@ -0,0 +1,10 @@ +import { defined, type Optional } from "@darkruby/assets-core"; + +export const keys = (prefix: string) => { + const key = (t: NonNullable) => `${prefix}-${JSON.stringify(t)}`; + const maybeKey = (t: Optional) => (defined(t) ? key(t) : `-`); + const multiKey = (ts: NonNullable[]) => + `${prefix}s-${ts.map(key).join("-")}`; + + return { key, maybeKey, multiKey }; +}; diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 163f8574..60b1cccc 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -4,7 +4,6 @@ import cors from "cors"; import { default as express } from "express"; import { pipe } from "fp-ts/lib/function"; import * as TE from "fp-ts/lib/TaskEither"; -import { LRUCache } from "lru-cache"; import { Server } from "node:http"; import path from "node:path"; import { createEnricher } from "./enrichment"; @@ -14,11 +13,7 @@ import type { Context } from "./handlers/context"; import { createRepository, type Repository } from "./repository"; import { execute } from "./repository/database"; import { createWebService } from "./services"; -import { - createCache, - type AppCache, - type Stringifiable -} from "./services/cache"; +import { createCache, type AppCache } from "./services/cache"; import { env, envDurationMsec, envNumber } from "./services/env"; import { initializeApp } from "./services/init"; import { cachedYahooApi } from "./yahoo/cached"; @@ -49,15 +44,7 @@ const repository = (c: Config): Action => ); const cache = ({ cacheSize, cacheTtl }: Config): Action => - pipe( - TE.of( - new LRUCache({ - max: cacheSize, - ttl: cacheTtl - }) - ), - TE.map(createCache) - ); + pipe(TE.of(createCache(cacheSize, cacheTtl))); const server = ({ port, app }: Config, ctx: Context): Action => { const expressify = createRequestHandler(ctx); diff --git a/packages/backend/src/repository/sql/asset/update.sql b/packages/backend/src/repository/sql/asset/update.sql index af648bd5..0f5b6886 100644 --- a/packages/backend/src/repository/sql/asset/update.sql +++ b/packages/backend/src/repository/sql/asset/update.sql @@ -1,6 +1,6 @@ UPDATE assets SET name = $name, ticker = $ticker, - modified = CURRENT_TIMESTAMP + modified = datetime('subsec') WHERE id = $assetId and portfolio_id = $portfolioId \ No newline at end of file diff --git a/packages/backend/src/repository/sql/portfolio/update.sql b/packages/backend/src/repository/sql/portfolio/update.sql index 6bed22d2..183a905d 100644 --- a/packages/backend/src/repository/sql/portfolio/update.sql +++ b/packages/backend/src/repository/sql/portfolio/update.sql @@ -1,6 +1,6 @@ UPDATE portfolios SET name = $name, description = $description, - modified = CURRENT_TIMESTAMP + modified = datetime('subsec') WHERE id = $portfolioId AND user_id = $userId; \ No newline at end of file diff --git a/packages/backend/src/repository/sql/tx/update.sql b/packages/backend/src/repository/sql/tx/update.sql index cdb3b917..8af8f597 100644 --- a/packages/backend/src/repository/sql/tx/update.sql +++ b/packages/backend/src/repository/sql/tx/update.sql @@ -4,6 +4,6 @@ SET type = $type, price = $price, comments = $comments, date = $date, - modified = CURRENT_TIMESTAMP + modified = datetime('subsec') WHERE id = $txId and asset_id = $assetId; \ No newline at end of file diff --git a/packages/backend/src/repository/sql/user/update-profile-only.sql b/packages/backend/src/repository/sql/user/update-profile-only.sql index 8ceabc7c..321bea43 100644 --- a/packages/backend/src/repository/sql/user/update-profile-only.sql +++ b/packages/backend/src/repository/sql/user/update-profile-only.sql @@ -3,5 +3,5 @@ set username = $username, admin = $admin, login_attempts = $login_attempts, locked = $locked, - modified = CURRENT_TIMESTAMP + modified = datetime('subsec') where id = $userId; \ No newline at end of file diff --git a/packages/backend/src/repository/sql/user/update.sql b/packages/backend/src/repository/sql/user/update.sql index 8533cc60..11553568 100644 --- a/packages/backend/src/repository/sql/user/update.sql +++ b/packages/backend/src/repository/sql/user/update.sql @@ -5,5 +5,5 @@ set username = $username, admin = $admin, login_attempts = $login_attempts, locked = $locked, - modified = CURRENT_TIMESTAMP + modified = datetime('subsec') where id = $userId; \ No newline at end of file diff --git a/packages/backend/src/services/cache.ts b/packages/backend/src/services/cache.ts index 68420819..77350a32 100644 --- a/packages/backend/src/services/cache.ts +++ b/packages/backend/src/services/cache.ts @@ -2,7 +2,7 @@ import { defined, type Action } from "@darkruby/assets-core"; import * as O from "fp-ts/lib/Option"; import * as TE from "fp-ts/lib/TaskEither"; import { pipe } from "fp-ts/lib/function"; -import { type LRUCache } from "lru-cache"; +import { LRUCache } from "lru-cache"; import { createHash } from "node:crypto"; import { createLogger } from "../fp-express"; @@ -40,10 +40,10 @@ const cachedAction = const get = getter(cache); const res = get(key); if (O.isSome(res)) { - log.debug(`HIT for ${key}`); + log.debug(`HIT for ${key.substring(0, 10)}`); return TE.of(res.value as T); } - log.debug(`MISS for ${key}`); + log.debug(`MISS for ${key.substring(0, 10)}`); const set = setter(cache); return pipe( @@ -54,7 +54,8 @@ const cachedAction = export type AppCache = ReturnType; -export const createCache = (cache: Cache) => { +export const createCache = (size: number, ttl: number) => { + const cache = new LRUCache({ max: size, ttl }); return { has: has(cache), getter: getter(cache), diff --git a/packages/backend/test/portfolio.spec.ts b/packages/backend/test/portfolio.spec.ts index b7ee3afb..2b38b3c8 100644 --- a/packages/backend/test/portfolio.spec.ts +++ b/packages/backend/test/portfolio.spec.ts @@ -33,6 +33,9 @@ test("Create portfolio", async () => { }); test("Get multiple portfolios", async () => { + for (let _ in [1, 2, 3]) { + await run(api.portfolio.create(fakePortfolio())); + } const portfolios = await run(api.portfolio.getMany()); expect(portfolios).toSatisfy((a) => Array.isArray(a) && a.length > 0); }); diff --git a/packages/core/src/domain/tx.ts b/packages/core/src/domain/tx.ts index 82d475d7..423154e5 100644 --- a/packages/core/src/domain/tx.ts +++ b/packages/core/src/domain/tx.ts @@ -40,6 +40,10 @@ export const byDateAsc = pipe( export const isBuy = ({ type }: T) => type == "buy"; export const isSell = (tx: T) => !isBuy(tx); +export const toKey = (tx: T) => + `tx-${tx.id}-${tx.modified.getTime()}`; +export const toKeys = (tx: T[]) => tx.map(toKey).join(`-`); + export const cloneTx = ({ type, quantity, diff --git a/packages/core/src/utils/utils.ts b/packages/core/src/utils/utils.ts index b13d6d62..8b821a70 100644 --- a/packages/core/src/utils/utils.ts +++ b/packages/core/src/utils/utils.ts @@ -9,7 +9,7 @@ export type Replace = Identity< Omit & { [key in K]: R } >; -export const defined = (t: Optional): t is T => +export const defined = (t: Optional): t is NonNullable => t !== null && t !== undefined; export type Result = E.Either; From 7dc71020509e96dc3d654ab0cc73d60eade58ec4 Mon Sep 17 00:00:00 2001 From: darkruby Date: Sat, 28 Mar 2026 19:31:06 +0000 Subject: [PATCH 04/12] fix cache keys --- package.json | 2 +- packages/backend/package.json | 2 +- packages/backend/src/enrichment/cached.ts | 28 +++++++++++++---------- packages/backend/src/enrichment/key.ts | 19 +++++++-------- packages/backend/src/services/cache.ts | 6 +++++ packages/core/package.json | 2 +- packages/web/package.json | 2 +- 7 files changed, 36 insertions(+), 25 deletions(-) diff --git a/package.json b/package.json index 568a3850..25a45b22 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@darkruby/assets", "private": true, - "version": "1.8.0", + "version": "1.8.1", "type": "module", "dependencies": { "date-fns": "^4.1.0", diff --git a/packages/backend/package.json b/packages/backend/package.json index 0f3d371b..b94d2016 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "@darkruby/assets-backend", - "version": "1.8.0", + "version": "1.8.1", "main": "src/index.ts", "type": "module", "scripts": { diff --git a/packages/backend/src/enrichment/cached.ts b/packages/backend/src/enrichment/cached.ts index 267423fc..f56e7cf6 100644 --- a/packages/backend/src/enrichment/cached.ts +++ b/packages/backend/src/enrichment/cached.ts @@ -11,14 +11,14 @@ import type { Repository } from "../repository"; import type { AppCache } from "../services/cache"; import type { YahooApi } from "../yahoo/client"; import { createAssetEnricher, type AssetEnricher } from "./asset"; -import { keys } from "./key"; +import { key } from "./key"; import { createPortfolioEnricher, type PortfolioEnricher } from "./portfolio"; import { createSummaryEnricher, type SummaryEnricher } from "./summary"; import { createTxEnricher, type TxEnricher } from "./tx"; -const txKey = keys("enrich-tx"); -const assetKey = keys("enrich-asset"); -const portfolioKey = keys("enrich-portfolio"); +const txKey = key("enrich-tx"); +const assetKey = key("enrich-asset"); +const portfolioKey = key("enrich-portfolio"); export type Enricher = { tx: TxEnricher; @@ -39,11 +39,11 @@ export const createEnricher = ( ...txEnricher, enrich: (tx: GetTx) => { const action = () => txEnricher.enrich(tx); - return cache.cachedAction(txKey.key(tx), action, ENRICH_TTL); + return cache.cachedAction(txKey(tx), action, ENRICH_TTL); }, enrichMany: (txs: GetTx[]) => { const action = () => txEnricher.enrichMany(txs); - return cache.cachedAction(txKey.multiKey(txs), action, ENRICH_TTL); + return cache.cachedAction(txKey({ txs }), action, ENRICH_TTL); } }; @@ -52,21 +52,25 @@ export const createEnricher = ( ...assetEnricher, enrich: (asset: GetAsset, range: ChartRange = DEFAULT_CHART_RANGE) => { const action = () => assetEnricher.enrich(asset, range); - return cache.cachedAction(assetKey.key(asset), action, ENRICH_TTL); + return cache.cachedAction(assetKey({ asset, range }), action, ENRICH_TTL); }, enrichMaybe: ( asset: Optional, range: ChartRange = DEFAULT_CHART_RANGE ) => { const action = () => assetEnricher.enrichMaybe(asset, range); - return cache.cachedAction(assetKey.maybeKey(asset), action, ENRICH_TTL); + return cache.cachedAction(assetKey({ asset, range }), action, ENRICH_TTL); }, enrichMany: ( assets: GetAsset[], range: ChartRange = DEFAULT_CHART_RANGE ) => { const action = () => assetEnricher.enrichMany(assets, range); - return cache.cachedAction(assetKey.multiKey(assets), action, ENRICH_TTL); + return cache.cachedAction( + assetKey({ assets, range }), + action, + ENRICH_TTL + ); } }; @@ -79,7 +83,7 @@ export const createEnricher = ( ) => { const action = () => portfolioEnricher.enrich(portfolio, range); return cache.cachedAction( - portfolioKey.key(portfolio), + portfolioKey({ portfolio, range }), action, ENRICH_TTL ); @@ -90,7 +94,7 @@ export const createEnricher = ( ) => { const action = () => portfolioEnricher.enrichMaybe(portfolio, range); return cache.cachedAction( - portfolioKey.maybeKey(portfolio), + portfolioKey({ portfolio, range }), action, ENRICH_TTL ); @@ -101,7 +105,7 @@ export const createEnricher = ( ) => { const action = () => portfolioEnricher.enrichMany(portfolios, range); return cache.cachedAction( - portfolioKey.multiKey(portfolios), + portfolioKey({ portfolios, range }), action, ENRICH_TTL ); diff --git a/packages/backend/src/enrichment/key.ts b/packages/backend/src/enrichment/key.ts index b6e59801..b8e6ffa5 100644 --- a/packages/backend/src/enrichment/key.ts +++ b/packages/backend/src/enrichment/key.ts @@ -1,10 +1,11 @@ -import { defined, type Optional } from "@darkruby/assets-core"; +export const key = + (prefix: string) => + (t: NonNullable) => + `${prefix}-${JSON.stringify(t)}`; +// export const keys = (prefix: string) => { +// const maybeKey = (t: Optional) => (defined(t) ? key(t) : `-`); +// const multiKey = (ts: NonNullable[]) => +// `${prefix}s-${ts.map(key).join("-")}`; -export const keys = (prefix: string) => { - const key = (t: NonNullable) => `${prefix}-${JSON.stringify(t)}`; - const maybeKey = (t: Optional) => (defined(t) ? key(t) : `-`); - const multiKey = (ts: NonNullable[]) => - `${prefix}s-${ts.map(key).join("-")}`; - - return { key, maybeKey, multiKey }; -}; +// return { key, maybeKey, multiKey }; +// }; diff --git a/packages/backend/src/services/cache.ts b/packages/backend/src/services/cache.ts index 77350a32..46efd01e 100644 --- a/packages/backend/src/services/cache.ts +++ b/packages/backend/src/services/cache.ts @@ -63,3 +63,9 @@ export const createCache = (size: number, ttl: number) => { cachedAction: cachedAction(cache) }; }; + +// const aaa = (cache: AppCache) => { +// return function wrap(f: FunctionN>) { +// return (...a: A) => B; +// } +// }; diff --git a/packages/core/package.json b/packages/core/package.json index de0bfad2..e7856f55 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@darkruby/assets-core", - "version": "1.8.0", + "version": "1.8.1", "main": "src/index.ts", "type": "module", "scripts": { diff --git a/packages/web/package.json b/packages/web/package.json index aad4ba09..05d755d7 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,7 +1,7 @@ { "name": "@darkruby/assets-web", "private": true, - "version": "1.8.0", + "version": "1.8.1", "type": "module", "scripts": { "dev": "vite --host 0.0.0.0", From fb99429b870882a268f7db9e3f7f1167ee5759fc Mon Sep 17 00:00:00 2001 From: darkruby Date: Sat, 28 Mar 2026 21:09:38 +0000 Subject: [PATCH 05/12] multi chart test --- packages/backend/src/enrichment/chart.ts | 17 ++--------------- packages/backend/src/enrichment/portfolio.ts | 8 +++++++- packages/backend/src/enrichment/summary.ts | 18 ++++++++++++------ packages/backend/test/chart.spec.ts | 6 +++--- packages/core/src/decoders/portfolio.ts | 4 ++-- packages/core/src/decoders/summary.ts | 4 ++-- .../src/components/Charts/MultiAssetChart.tsx | 8 +++----- .../web/src/components/Portfolio/Portfolio.tsx | 16 ++++++++-------- .../web/src/components/Summary/Summary.tsx | 13 +++++++++++-- 9 files changed, 50 insertions(+), 44 deletions(-) diff --git a/packages/backend/src/enrichment/chart.ts b/packages/backend/src/enrichment/chart.ts index f5c73898..3e787714 100644 --- a/packages/backend/src/enrichment/chart.ts +++ b/packages/backend/src/enrichment/chart.ts @@ -307,29 +307,16 @@ const combineMultiChart = ); }; -export const combineAssetsMultiChart = combineMultiChart( +export const portfolioMultiChart = combineMultiChart( ({ name: id, base }) => ({ id, chart: base.chart }) ); -export const combineSummaryMultiChart = combineMultiChart( +export const summaryMultiChart = combineMultiChart( ({ name: id, chart }) => ({ id, chart }) ); - -// export const flattenMultiChart = (chart: MultiChartData): ChartData => { -// return pipe( -// chart, -// R.toEntries, -// A.map(([, chart]) => readRecords(chart, { schema: ChartSchema })), -// (dfs) => concat(dfs) -// ) -// .groupBy("timestamp") -// .agg(col("price").sum(), col("volume").sum()) -// .sort("timestamp") -// .toRecords() as ChartData; -// }; diff --git a/packages/backend/src/enrichment/portfolio.ts b/packages/backend/src/enrichment/portfolio.ts index 730c914a..c05695f7 100644 --- a/packages/backend/src/enrichment/portfolio.ts +++ b/packages/backend/src/enrichment/portfolio.ts @@ -23,7 +23,11 @@ import * as Ord from "fp-ts/lib/Ord"; import * as TE from "fp-ts/lib/TaskEither"; import type { Repository } from "../repository"; import type { AssetEnricher } from "./asset"; -import { combineAssetCharts, commonAssetRanges } from "./chart"; +import { + combineAssetCharts, + commonAssetRanges, + portfolioMultiChart +} from "./chart"; const sumInvested = sum(({ base }) => base.invested); const sumRealizedPnl = sum(({ base }) => base.realizedPnl); @@ -132,6 +136,7 @@ const getPortfolioEnricher = const totals = portfolioTotals(assets); const changes = portfolioChanges(assets); const chart = combineAssetCharts(assets); + const multiChart = portfolioMultiChart(assets); return { ...portfolio, @@ -142,6 +147,7 @@ const getPortfolioEnricher = domestic, changes, chart, + multiChart, invested, breakEven, totals, diff --git a/packages/backend/src/enrichment/summary.ts b/packages/backend/src/enrichment/summary.ts index 17ea8017..5c2b9579 100644 --- a/packages/backend/src/enrichment/summary.ts +++ b/packages/backend/src/enrichment/summary.ts @@ -16,7 +16,11 @@ import { import * as A from "fp-ts/lib/Array"; import { pipe } from "fp-ts/lib/function"; import * as Ord from "fp-ts/lib/Ord"; -import { combinePortfolioCharts, commonPortfolioRanges } from "./chart"; +import { + combinePortfolioCharts, + commonPortfolioRanges, + summaryMultiChart +} from "./chart"; const summaryMeta = ( portfolios: EnrichedPortfolio[] @@ -130,22 +134,24 @@ export const enrichSummary = ( ); const chart = combinePortfolioCharts(portfolios); + const multiChart = summaryMultiChart(portfolios); const meta = summaryMeta(portfolios); const changes = summaryChanges(portfolios); const totals = summaryTotals(portfolios); return { - numPortfolios, - chart, meta, - changes, + chart, totals, + changes, invested, fxImpact, + breakEven, + multiChart, realizedPnl, - breakEven - }; + numPortfolios + } satisfies EnrichedSummary; }; export type SummaryEnricher = ReturnType; diff --git a/packages/backend/test/chart.spec.ts b/packages/backend/test/chart.spec.ts index 0fa38a5d..501ddb06 100644 --- a/packages/backend/test/chart.spec.ts +++ b/packages/backend/test/chart.spec.ts @@ -4,7 +4,7 @@ import type { UnixDate } from "@darkruby/assets-core"; import { expect, test } from "bun:test"; -import { combineAssetsMultiChart } from "../src/enrichment/chart"; +import { portfolioMultiChart } from "../src/enrichment/chart"; const createMockChartPoint = ( timestamp: number, @@ -24,14 +24,14 @@ const createMockAsset = (name: string, ts: number[]): EnrichedAsset => { } as unknown as EnrichedAsset; }; -test.failing("combineMultiChart with asset containers", () => { +test.failing("portfolioMultiChart with asset containers", () => { const assets = [ createMockAsset("aapl", [1, 3, 5]), createMockAsset("googl", [2, 4, 6]), createMockAsset("msft", [1, 4, 5]) ] as EnrichedAsset[]; - const { aapl, googl, msft } = combineAssetsMultiChart(assets); + const { aapl, googl, msft } = portfolioMultiChart(assets); expect(aapl.length).toBe(googl.length); expect(googl.length).toBe(msft.length); diff --git a/packages/core/src/decoders/portfolio.ts b/packages/core/src/decoders/portfolio.ts index b2c89462..d5120d72 100644 --- a/packages/core/src/decoders/portfolio.ts +++ b/packages/core/src/decoders/portfolio.ts @@ -2,7 +2,7 @@ import * as t from "io-ts"; import { dateDecoder } from "./date"; import { UserIdDecoder } from "./user"; import { nullableDecoder } from "./util"; -import { ChartDataDecoder } from "./yahoo/chart"; +import { ChartDataDecoder, MultiChartDataDecoder } from "./yahoo/chart"; import { RangeDecoder } from "./yahoo/meta"; import { PeriodChangesDecoder, TotalsDecoder } from "./yahoo/period"; @@ -42,7 +42,7 @@ export const EnrichedPortfolioDecoder = t.type({ weight: nullableDecoder(t.number), domestic: t.boolean, chart: ChartDataDecoder, - // multiChart: MultiChartDataDecoder, + multiChart: MultiChartDataDecoder, changes: PeriodChangesDecoder, totals: TotalsDecoder, invested: t.number, diff --git a/packages/core/src/decoders/summary.ts b/packages/core/src/decoders/summary.ts index 67a01a22..8aaa7580 100644 --- a/packages/core/src/decoders/summary.ts +++ b/packages/core/src/decoders/summary.ts @@ -1,12 +1,12 @@ import * as t from "io-ts"; import { PortfolioMetaDecoder } from "./portfolio"; -import { ChartDataDecoder } from "./yahoo/chart"; +import { ChartDataDecoder, MultiChartDataDecoder } from "./yahoo/chart"; import { PeriodChangesDecoder, TotalsDecoder } from "./yahoo/period"; const summaryTypes = { numPortfolios: t.number, chart: ChartDataDecoder, - // multiChart: MultiChartDataDecoder, + multiChart: MultiChartDataDecoder, changes: PeriodChangesDecoder, totals: TotalsDecoder, meta: PortfolioMetaDecoder, diff --git a/packages/web/src/components/Charts/MultiAssetChart.tsx b/packages/web/src/components/Charts/MultiAssetChart.tsx index ec7332d8..47b40603 100644 --- a/packages/web/src/components/Charts/MultiAssetChart.tsx +++ b/packages/web/src/components/Charts/MultiAssetChart.tsx @@ -29,9 +29,7 @@ const RawMultiAssetChart: React.FC = ({ data, timeFormatter }: MultiAssetChartProps) => { - const { money } = useFormatters(); - const tickFormatter = (n: number) => `${n.toFixed(1)}%`; - + const { percent } = useFormatters(); const names = R.keys(data); const timestamp = pipe( data, @@ -94,7 +92,7 @@ const RawMultiAssetChart: React.FC = ({ @@ -103,7 +101,7 @@ const RawMultiAssetChart: React.FC = ({ labelFormatter={(t) => `Time: ${timeFormatter(t)}`} formatter={tooltipValueFormatter} /> - {entries.map(([name, chart], idx) => ( + {entries.map(([name], idx) => ( = ({