diff --git a/package-lock.json b/package-lock.json index 04a45f8..2d87bee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wasp-protocol", - "version": "0.4.1", + "version": "0.4.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wasp-protocol", - "version": "0.4.1", + "version": "0.4.2", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -147,33 +147,6 @@ "keyv": "^5.6.0" } }, - "node_modules/@cacheable/memory/node_modules/@keyv/bigmap": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", - "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "hashery": "^1.4.0", - "hookified": "^1.15.0" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "keyv": "^5.6.0" - } - }, - "node_modules/@cacheable/memory/node_modules/keyv": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", - "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@keyv/serialize": "^1.1.1" - } - }, "node_modules/@cacheable/node-cache": { "version": "1.7.6", "resolved": "https://registry.npmjs.org/@cacheable/node-cache/-/node-cache-1.7.6.tgz", @@ -189,16 +162,6 @@ "node": ">=18" } }, - "node_modules/@cacheable/node-cache/node_modules/keyv": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", - "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@keyv/serialize": "^1.1.1" - } - }, "node_modules/@cacheable/utils": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.4.1.tgz", @@ -210,16 +173,6 @@ "keyv": "^5.6.0" } }, - "node_modules/@cacheable/utils/node_modules/keyv": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", - "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@keyv/serialize": "^1.1.1" - } - }, "node_modules/@emnapi/runtime": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", @@ -1417,6 +1370,23 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, "node_modules/@keyv/serialize": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", @@ -1543,9 +1513,9 @@ "license": "BSD-3-Clause" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", - "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", + "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", "cpu": [ "arm" ], @@ -1557,9 +1527,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", - "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", + "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", "cpu": [ "arm64" ], @@ -1571,9 +1541,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", - "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", + "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", "cpu": [ "arm64" ], @@ -1585,9 +1555,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", - "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", + "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", "cpu": [ "x64" ], @@ -1599,9 +1569,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", - "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", + "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", "cpu": [ "arm64" ], @@ -1613,9 +1583,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", - "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", + "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", "cpu": [ "x64" ], @@ -1627,9 +1597,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", - "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", + "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", "cpu": [ "arm" ], @@ -1641,9 +1611,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", - "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", + "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", "cpu": [ "arm" ], @@ -1655,9 +1625,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", - "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", + "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", "cpu": [ "arm64" ], @@ -1669,9 +1639,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", - "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", + "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", "cpu": [ "arm64" ], @@ -1683,9 +1653,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", - "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", + "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", "cpu": [ "loong64" ], @@ -1697,9 +1667,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", - "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", + "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", "cpu": [ "loong64" ], @@ -1711,9 +1681,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", - "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", + "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", "cpu": [ "ppc64" ], @@ -1725,9 +1695,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", - "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", + "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", "cpu": [ "ppc64" ], @@ -1739,9 +1709,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", - "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", + "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", "cpu": [ "riscv64" ], @@ -1753,9 +1723,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", - "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", + "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", "cpu": [ "riscv64" ], @@ -1767,9 +1737,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", - "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", + "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", "cpu": [ "s390x" ], @@ -1781,9 +1751,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", - "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", + "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", "cpu": [ "x64" ], @@ -1795,9 +1765,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", - "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", + "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", "cpu": [ "x64" ], @@ -1809,9 +1779,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", - "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", + "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", "cpu": [ "x64" ], @@ -1823,9 +1793,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", - "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", + "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", "cpu": [ "arm64" ], @@ -1837,9 +1807,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", - "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", + "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", "cpu": [ "arm64" ], @@ -1851,9 +1821,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", - "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", + "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", "cpu": [ "ia32" ], @@ -1865,9 +1835,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", - "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", + "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", "cpu": [ "x64" ], @@ -1879,9 +1849,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", - "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", + "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", "cpu": [ "x64" ], @@ -2399,9 +2369,9 @@ } }, "node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -2563,16 +2533,6 @@ "qified": "^0.9.0" } }, - "node_modules/cacheable/node_modules/keyv": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", - "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@keyv/serialize": "^1.1.1" - } - }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -3236,6 +3196,16 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/flat-cache/node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/flatted": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", @@ -3683,13 +3653,13 @@ "license": "MIT" }, "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", "dev": true, "license": "MIT", "dependencies": { - "json-buffer": "3.0.1" + "@keyv/serialize": "^1.1.1" } }, "node_modules/levn": { @@ -4178,13 +4148,13 @@ } }, "node_modules/p-queue": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.1.2.tgz", - "integrity": "sha512-ktsDOALzTYTWWF1PbkNVg2rOt+HaOaMWJMUnt7T3qf5tvZ1L8dBW3tObzprBcXNMKkwj+yFSLqHso0x+UFcJXw==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.2.0.tgz", + "integrity": "sha512-dWgLE8AH0HjQ9fe74pUkKkvzzYT18Inp4zra3lKHnnwqGvcfcUBrvF2EAVX+envufDNBOzpPq/IBUONDbI7+3g==", "dev": true, "license": "MIT", "dependencies": { - "eventemitter3": "^5.0.1", + "eventemitter3": "^5.0.4", "p-timeout": "^7.0.0" }, "engines": { @@ -4391,9 +4361,9 @@ } }, "node_modules/postcss": { - "version": "8.5.10", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", - "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", + "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", "dev": true, "funding": [ { @@ -4608,9 +4578,9 @@ } }, "node_modules/qified/node_modules/hookified": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.1.1.tgz", - "integrity": "sha512-AHb76R16GB5EsPBE2J7Ko5kiEyXwviB9P5SMrAKcuAu4vJPZttViAbj9+tZeaQE5zjDme+1vcHP78Yj/WoAveA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", "dev": true, "license": "MIT" }, @@ -4718,9 +4688,9 @@ } }, "node_modules/rollup": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", - "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", + "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4734,31 +4704,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.1", - "@rollup/rollup-android-arm64": "4.60.1", - "@rollup/rollup-darwin-arm64": "4.60.1", - "@rollup/rollup-darwin-x64": "4.60.1", - "@rollup/rollup-freebsd-arm64": "4.60.1", - "@rollup/rollup-freebsd-x64": "4.60.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", - "@rollup/rollup-linux-arm-musleabihf": "4.60.1", - "@rollup/rollup-linux-arm64-gnu": "4.60.1", - "@rollup/rollup-linux-arm64-musl": "4.60.1", - "@rollup/rollup-linux-loong64-gnu": "4.60.1", - "@rollup/rollup-linux-loong64-musl": "4.60.1", - "@rollup/rollup-linux-ppc64-gnu": "4.60.1", - "@rollup/rollup-linux-ppc64-musl": "4.60.1", - "@rollup/rollup-linux-riscv64-gnu": "4.60.1", - "@rollup/rollup-linux-riscv64-musl": "4.60.1", - "@rollup/rollup-linux-s390x-gnu": "4.60.1", - "@rollup/rollup-linux-x64-gnu": "4.60.1", - "@rollup/rollup-linux-x64-musl": "4.60.1", - "@rollup/rollup-openbsd-x64": "4.60.1", - "@rollup/rollup-openharmony-arm64": "4.60.1", - "@rollup/rollup-win32-arm64-msvc": "4.60.1", - "@rollup/rollup-win32-ia32-msvc": "4.60.1", - "@rollup/rollup-win32-x64-gnu": "4.60.1", - "@rollup/rollup-win32-x64-msvc": "4.60.1", + "@rollup/rollup-android-arm-eabi": "4.60.2", + "@rollup/rollup-android-arm64": "4.60.2", + "@rollup/rollup-darwin-arm64": "4.60.2", + "@rollup/rollup-darwin-x64": "4.60.2", + "@rollup/rollup-freebsd-arm64": "4.60.2", + "@rollup/rollup-freebsd-x64": "4.60.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", + "@rollup/rollup-linux-arm-musleabihf": "4.60.2", + "@rollup/rollup-linux-arm64-gnu": "4.60.2", + "@rollup/rollup-linux-arm64-musl": "4.60.2", + "@rollup/rollup-linux-loong64-gnu": "4.60.2", + "@rollup/rollup-linux-loong64-musl": "4.60.2", + "@rollup/rollup-linux-ppc64-gnu": "4.60.2", + "@rollup/rollup-linux-ppc64-musl": "4.60.2", + "@rollup/rollup-linux-riscv64-gnu": "4.60.2", + "@rollup/rollup-linux-riscv64-musl": "4.60.2", + "@rollup/rollup-linux-s390x-gnu": "4.60.2", + "@rollup/rollup-linux-x64-gnu": "4.60.2", + "@rollup/rollup-linux-x64-musl": "4.60.2", + "@rollup/rollup-openbsd-x64": "4.60.2", + "@rollup/rollup-openharmony-arm64": "4.60.2", + "@rollup/rollup-win32-arm64-msvc": "4.60.2", + "@rollup/rollup-win32-ia32-msvc": "4.60.2", + "@rollup/rollup-win32-x64-gnu": "4.60.2", + "@rollup/rollup-win32-x64-msvc": "4.60.2", "fsevents": "~2.3.2" } }, diff --git a/src/__tests__/expiring-map.test.ts b/src/__tests__/expiring-map.test.ts new file mode 100644 index 0000000..f892cef --- /dev/null +++ b/src/__tests__/expiring-map.test.ts @@ -0,0 +1,165 @@ +/** + * ExpiringMapCache tests + */ + +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { createExpiringMapCache } from '../cache/expiring-map.js'; +import type { ExpiringMapCache } from '../cache/expiring-map.js'; + +describe('ExpiringMapCache', () => { + let cache: ExpiringMapCache; + + afterEach(() => { + if (cache) { + cache.stop(); + } + }); + + it('should set and get values', () => { + cache = createExpiringMapCache({ + ttlMs: 5 * 60_000, + }); + + cache.set('key1', { sessionId: 's1', jid: 'j1' }); + const value = cache.get('key1'); + + expect(value).toEqual({ sessionId: 's1', jid: 'j1' }); + }); + + it('should return undefined for non-existent keys', () => { + cache = createExpiringMapCache({ + ttlMs: 5 * 60_000, + }); + + const value = cache.get('non-existent'); + expect(value).toBeUndefined(); + }); + + it('should expire entries on lazy access', () => { + let currentTime = 1000; + const clock = vi.fn(() => currentTime); + + cache = createExpiringMapCache({ + ttlMs: 100, + clock, + }); + + // Set value at time 1000 + cache.set('key1', { sessionId: 's1', jid: 'j1' }); + expect(cache.get('key1')).toEqual({ sessionId: 's1', jid: 'j1' }); + + // Advance time past expiry (1000 + 100 = 1100) + currentTime = 1101; + + // Should be expired on access + expect(cache.get('key1')).toBeUndefined(); + expect(cache.has('key1')).toBe(false); + }); + + it('should prune expired entries on interval', async () => { + vi.useFakeTimers(); + + let currentTime = 1000; + const clock = vi.fn(() => currentTime); + + cache = createExpiringMapCache({ + ttlMs: 100, + pruneIntervalMs: 50, + clock, + }); + + // Set value at time 1000 + cache.set('key1', { sessionId: 's1', jid: 'j1' }); + cache.set('key2', { sessionId: 's2', jid: 'j2' }); + + // Advance time past expiry + currentTime = 1101; + + // Before prune runs, size still includes expired entries + // Run prune interval + await vi.advanceTimersByTimeAsync(50); + + // After prune, expired entries should be removed + expect(cache.get('key1')).toBeUndefined(); + expect(cache.get('key2')).toBeUndefined(); + + vi.useRealTimers(); + }); + + it('should handle has() correctly with expiry', () => { + let currentTime = 1000; + const clock = vi.fn(() => currentTime); + + cache = createExpiringMapCache({ + ttlMs: 100, + clock, + }); + + cache.set('key1', { sessionId: 's1', jid: 'j1' }); + expect(cache.has('key1')).toBe(true); + + // Advance time past expiry + currentTime = 1101; + expect(cache.has('key1')).toBe(false); + }); + + it('should delete entries', () => { + cache = createExpiringMapCache({ + ttlMs: 5 * 60_000, + }); + + cache.set('key1', { sessionId: 's1', jid: 'j1' }); + expect(cache.has('key1')).toBe(true); + + cache.delete('key1'); + expect(cache.has('key1')).toBe(false); + }); + + it('should report accurate size after lazy cleanup', () => { + let currentTime = 1000; + const clock = vi.fn(() => currentTime); + + cache = createExpiringMapCache({ + ttlMs: 100, + clock, + }); + + cache.set('key1', { sessionId: 's1', jid: 'j1' }); + cache.set('key2', { sessionId: 's2', jid: 'j2' }); + cache.set('key3', { sessionId: 's3', jid: 'j3' }); + + expect(cache.size).toBe(3); + + // Advance time past expiry + currentTime = 1101; + + // Size should trigger lazy cleanup + expect(cache.size).toBe(0); + }); + + it('should stop prune interval on stop()', () => { + vi.useFakeTimers(); + + cache = createExpiringMapCache({ + ttlMs: 100, + pruneIntervalMs: 50, + }); + + cache.stop(); + + // Should be safe to call stop() again + cache.stop(); + + vi.useRealTimers(); + }); + + it('should use default pruneIntervalMs when not specified', () => { + cache = createExpiringMapCache({ + ttlMs: 5000, + }); + + // No error, just verify it works + cache.set('key1', { sessionId: 's1', jid: 'j1' }); + expect(cache.get('key1')).toBeDefined(); + }); +}); diff --git a/src/__tests__/store-cache.test.ts b/src/__tests__/store-cache.test.ts new file mode 100644 index 0000000..4d0cfd5 --- /dev/null +++ b/src/__tests__/store-cache.test.ts @@ -0,0 +1,213 @@ +/** + * SessionStoreCache tests + */ + +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { createSessionStoreCache } from '../stores/store-cache.js'; +import type { SessionStoreCache, FsAdapter } from '../stores/store-cache.js'; + +describe('SessionStoreCache', () => { + let cache: SessionStoreCache; + let mockFs: FsAdapter; + let fileStats: Map; + let fileContents: Map; + + beforeEach(() => { + fileStats = new Map(); + fileContents = new Map(); + + mockFs = { + stat: vi.fn(async (path: string) => { + const stats = fileStats.get(path); + if (!stats) { + const error: NodeJS.ErrnoException = new Error('ENOENT'); + error.code = 'ENOENT'; + throw error; + } + return stats; + }), + readFile: vi.fn(async (path: string) => { + const content = fileContents.get(path); + if (content === undefined) { + const error: NodeJS.ErrnoException = new Error('ENOENT'); + error.code = 'ENOENT'; + throw error; + } + return content; + }), + }; + + cache = createSessionStoreCache({ fs: mockFs }); + }); + + it('should cache hit on unchanged file', async () => { + const path = '/tmp/session.json'; + const data = { sessionId: 's1', jid: 'j1@s.whatsapp.net' }; + + fileStats.set(path, { mtimeMs: 1000, size: 100 }); + fileContents.set(path, JSON.stringify(data)); + + // First read + const result1 = await cache.read(path); + expect(result1).toEqual(data); + expect(mockFs.readFile).toHaveBeenCalledTimes(1); + + // Second read - should use cache + const result2 = await cache.read(path); + expect(result2).toEqual(data); + expect(mockFs.readFile).toHaveBeenCalledTimes(1); // Still 1, not 2 + }); + + it('should cache miss on mtime change', async () => { + const path = '/tmp/session.json'; + const data1 = { sessionId: 's1', jid: 'j1@s.whatsapp.net' }; + const data2 = { sessionId: 's2', jid: 'j2@s.whatsapp.net' }; + + fileStats.set(path, { mtimeMs: 1000, size: 100 }); + fileContents.set(path, JSON.stringify(data1)); + + // First read + const result1 = await cache.read(path); + expect(result1).toEqual(data1); + + // Change mtime + fileStats.set(path, { mtimeMs: 2000, size: 100 }); + fileContents.set(path, JSON.stringify(data2)); + + // Second read - should detect change and re-read + const result2 = await cache.read(path); + expect(result2).toEqual(data2); + expect(mockFs.readFile).toHaveBeenCalledTimes(2); + }); + + it('should cache miss on size change', async () => { + const path = '/tmp/session.json'; + const data1 = { sessionId: 's1' }; + const data2 = { sessionId: 's2', jid: 'j2@s.whatsapp.net' }; + + fileStats.set(path, { mtimeMs: 1000, size: 20 }); + fileContents.set(path, JSON.stringify(data1)); + + // First read + const result1 = await cache.read(path); + expect(result1).toEqual(data1); + + // Change size + fileStats.set(path, { mtimeMs: 1000, size: 50 }); + fileContents.set(path, JSON.stringify(data2)); + + // Second read - should detect change + const result2 = await cache.read(path); + expect(result2).toEqual(data2); + expect(mockFs.readFile).toHaveBeenCalledTimes(2); + }); + + it('should implement LRU eviction', async () => { + cache = createSessionStoreCache({ maxEntries: 2, fs: mockFs }); + + // Add 3 files + for (let i = 1; i <= 3; i++) { + const path = `/tmp/session${i}.json`; + fileStats.set(path, { mtimeMs: 1000, size: 100 }); + fileContents.set(path, JSON.stringify({ sessionId: `s${i}` })); + await cache.read(path); + } + + // Cache should only hold 2 entries (LRU eviction) + expect(cache.size).toBe(2); + + // First file should have been evicted + // Reading it again should trigger a fresh load + await cache.read('/tmp/session1.json'); + expect(mockFs.readFile).toHaveBeenCalledTimes(4); // 3 initial + 1 re-read + }); + + it('should update LRU on access', async () => { + cache = createSessionStoreCache({ maxEntries: 2, fs: mockFs }); + + // Add 2 files + fileStats.set('/tmp/session1.json', { mtimeMs: 1000, size: 100 }); + fileContents.set('/tmp/session1.json', JSON.stringify({ sessionId: 's1' })); + fileStats.set('/tmp/session2.json', { mtimeMs: 1000, size: 100 }); + fileContents.set('/tmp/session2.json', JSON.stringify({ sessionId: 's2' })); + + await cache.read('/tmp/session1.json'); + await cache.read('/tmp/session2.json'); + + // Access session1 again to make it most recent + await cache.read('/tmp/session1.json'); + + // Add a third file - should evict session2 (least recent) + fileStats.set('/tmp/session3.json', { mtimeMs: 1000, size: 100 }); + fileContents.set('/tmp/session3.json', JSON.stringify({ sessionId: 's3' })); + await cache.read('/tmp/session3.json'); + + // session1 should still be cached (accessed recently) + await cache.read('/tmp/session1.json'); + expect(mockFs.readFile).toHaveBeenCalledTimes(4); // 3 unique loads + 1 for session3 + + // session2 should have been evicted (needs re-read) + await cache.read('/tmp/session2.json'); + expect(mockFs.readFile).toHaveBeenCalledTimes(5); // Re-read session2 + }); + + it('should cache raw content separately', async () => { + const path = '/tmp/session.json'; + const data = { sessionId: 's1', jid: 'j1@s.whatsapp.net' }; + + fileStats.set(path, { mtimeMs: 1000, size: 100 }); + fileContents.set(path, JSON.stringify(data)); + + // Read raw + const raw1 = await cache.readRaw(path); + expect(raw1).toBe(JSON.stringify(data)); + expect(mockFs.readFile).toHaveBeenCalledTimes(1); + + // Read parsed - should use cached raw + const parsed = await cache.read(path); + expect(parsed).toEqual(data); + expect(mockFs.readFile).toHaveBeenCalledTimes(1); // Still 1, not 2 + }); + + it('should manually invalidate entries', async () => { + const path = '/tmp/session.json'; + const data = { sessionId: 's1' }; + + fileStats.set(path, { mtimeMs: 1000, size: 100 }); + fileContents.set(path, JSON.stringify(data)); + + // First read + await cache.read(path); + expect(mockFs.readFile).toHaveBeenCalledTimes(1); + + // Invalidate + cache.invalidate(path); + + // Next read should re-load + await cache.read(path); + expect(mockFs.readFile).toHaveBeenCalledTimes(2); + }); + + it('should handle missing files gracefully', async () => { + const path = '/tmp/nonexistent.json'; + + // Should throw since we're not catching ENOENT in the implementation + await expect(cache.read(path)).rejects.toThrow(); + }); + + it('should report accurate size', async () => { + expect(cache.size).toBe(0); + + fileStats.set('/tmp/session1.json', { mtimeMs: 1000, size: 100 }); + fileContents.set('/tmp/session1.json', JSON.stringify({ sessionId: 's1' })); + await cache.read('/tmp/session1.json'); + + expect(cache.size).toBe(1); + + fileStats.set('/tmp/session2.json', { mtimeMs: 1000, size: 100 }); + fileContents.set('/tmp/session2.json', JSON.stringify({ sessionId: 's2' })); + await cache.read('/tmp/session2.json'); + + expect(cache.size).toBe(2); + }); +}); diff --git a/src/cache/expiring-map.ts b/src/cache/expiring-map.ts new file mode 100644 index 0000000..dbed41e --- /dev/null +++ b/src/cache/expiring-map.ts @@ -0,0 +1,141 @@ +/** + * Generic in-memory TTL cache with automatic pruning + * + * Auto-prunes expired entries on a configurable interval. + * Injectable clock for unit testing. + */ + +/** + * Cache entry with expiration timestamp + */ +interface CacheEntry { + value: V; + expiresAt: number; +} + +/** + * Configuration options for ExpiringMapCache + */ +export interface ExpiringMapCacheConfig { + /** Time-to-live in milliseconds */ + ttlMs: number; + /** Prune interval in milliseconds (defaults to ttlMs) */ + pruneIntervalMs?: number; + /** Injectable clock function for testing (defaults to Date.now) */ + clock?: () => number; +} + +/** + * Generic in-memory cache with TTL and automatic pruning + */ +export interface ExpiringMapCache { + /** Set a value in the cache */ + set(key: K, value: V): void; + /** Get a value from the cache (returns undefined if expired or not found) */ + get(key: K): V | undefined; + /** Check if key exists and is not expired */ + has(key: K): boolean; + /** Delete a key from the cache */ + delete(key: K): boolean; + /** Current size after lazy expiry pass */ + get size(): number; + /** Stop the prune interval (call on shutdown) */ + stop(): void; +} + +/** + * Create a new expiring map cache + * + * @example + * ```typescript + * const cache = createExpiringMapCache({ + * ttlMs: 5 * 60_000, // 5 minutes + * pruneIntervalMs: 60_000, // prune every 60s + * }); + * + * cache.set('user-1', { id: '1', name: 'Alice' }); + * const user = cache.get('user-1'); // => User object or undefined + * cache.stop(); // cleanup on shutdown + * ``` + */ +export function createExpiringMapCache( + config: ExpiringMapCacheConfig +): ExpiringMapCache { + const { ttlMs, pruneIntervalMs = ttlMs, clock = () => Date.now() } = config; + const map = new Map>(); + let pruneTimer: NodeJS.Timeout | null = null; + + /** + * Prune expired entries from the cache + */ + const prune = (): void => { + const now = clock(); + for (const [key, entry] of map.entries()) { + if (entry.expiresAt <= now) { + map.delete(key); + } + } + }; + + // Start automatic pruning + pruneTimer = setInterval(prune, pruneIntervalMs); + + return { + set(key: K, value: V): void { + const expiresAt = clock() + ttlMs; + map.set(key, { value, expiresAt }); + }, + + get(key: K): V | undefined { + const entry = map.get(key); + if (!entry) { + return undefined; + } + + // Lazy expiry on access + if (entry.expiresAt <= clock()) { + map.delete(key); + return undefined; + } + + return entry.value; + }, + + has(key: K): boolean { + const entry = map.get(key); + if (!entry) { + return false; + } + + // Lazy expiry on access + if (entry.expiresAt <= clock()) { + map.delete(key); + return false; + } + + return true; + }, + + delete(key: K): boolean { + return map.delete(key); + }, + + get size(): number { + // Perform lazy cleanup before returning size + const now = clock(); + for (const [key, entry] of map.entries()) { + if (entry.expiresAt <= now) { + map.delete(key); + } + } + return map.size; + }, + + stop(): void { + if (pruneTimer !== null) { + clearInterval(pruneTimer); + pruneTimer = null; + } + }, + }; +} diff --git a/src/stores/store-cache.ts b/src/stores/store-cache.ts new file mode 100644 index 0000000..7cf2f18 --- /dev/null +++ b/src/stores/store-cache.ts @@ -0,0 +1,201 @@ +/** + * Two-tier session store cache + * + * Caches both raw file contents and parsed JSON objects. + * Invalidates when file metadata (mtime + size) changes. + * LRU eviction at maxEntries. + */ + +import * as fs from 'node:fs/promises'; + +/** + * File system adapter interface for testability + */ +export interface FsAdapter { + stat(path: string): Promise<{ mtimeMs: number; size: number }>; + readFile(path: string, encoding: 'utf-8'): Promise; +} + +/** + * Default filesystem adapter using node:fs/promises + */ +const defaultFsAdapter: FsAdapter = { + stat: (path: string) => fs.stat(path), + readFile: (path: string, encoding: 'utf-8') => fs.readFile(path, encoding), +}; + +/** + * Cache entry with file metadata fingerprint + */ +interface CacheEntry { + mtimeMs: number; + size: number; + raw: string; + parsed: unknown; + /** Last access timestamp for LRU */ + lastAccess: number; +} + +/** + * Configuration options for SessionStoreCache + */ +export interface SessionStoreCacheConfig { + /** Maximum number of entries before LRU eviction (default: 100) */ + maxEntries?: number; + /** File system adapter for testability */ + fs?: FsAdapter; +} + +/** + * Two-tier session store cache + */ +export interface SessionStoreCache { + /** Read and parse a JSON file (returns parsed object) */ + read(path: string): Promise; + /** Read raw file contents (returns string) */ + readRaw(path: string): Promise; + /** Manually invalidate a cached file */ + invalidate(path: string): void; + /** Current cache size */ + get size(): number; +} + +/** + * Create a new session store cache + * + * @example + * ```typescript + * const cache = createSessionStoreCache({ maxEntries: 100 }); + * + * const session = await cache.read('/path/to/auth.json'); + * const raw = await cache.readRaw('/path/to/auth.json'); + * + * cache.invalidate('/path/to/auth.json'); // manual bust + * ``` + */ +export function createSessionStoreCache( + config: SessionStoreCacheConfig = {} +): SessionStoreCache { + const { maxEntries = 100, fs: fsAdapter = defaultFsAdapter } = config; + const cache = new Map(); + + /** + * Evict least recently used entry + */ + const evictLRU = (): void => { + if (cache.size === 0) return; + + let oldestPath: string | null = null; + let oldestAccess = Infinity; + + for (const [path, entry] of cache.entries()) { + if (entry.lastAccess < oldestAccess) { + oldestAccess = entry.lastAccess; + oldestPath = path; + } + } + + if (oldestPath !== null) { + cache.delete(oldestPath); + } + }; + + /** + * Check if cached entry is still valid + */ + const isValid = async ( + path: string, + entry: CacheEntry + ): Promise => { + try { + const stats = await fsAdapter.stat(path); + return stats.mtimeMs === entry.mtimeMs && stats.size === entry.size; + } catch { + // If stat fails, consider cache invalid + return false; + } + }; + + /** + * Load and cache a file + */ + const loadAndCache = async (path: string): Promise => { + // Read file and stats + const [raw, stats] = await Promise.all([ + fsAdapter.readFile(path, 'utf-8'), + fsAdapter.stat(path), + ]); + + // Parse JSON + const parsed = JSON.parse(raw); + + // Create cache entry + const entry: CacheEntry = { + mtimeMs: stats.mtimeMs, + size: stats.size, + raw, + parsed, + lastAccess: Date.now(), + }; + + // Evict LRU if at capacity + if (cache.size >= maxEntries) { + evictLRU(); + } + + // Store in cache + cache.set(path, entry); + + return entry; + }; + + return { + async read(path: string): Promise { + const cached = cache.get(path); + + // Cache hit - check if still valid + if (cached) { + if (await isValid(path, cached)) { + // Update last access for LRU + cached.lastAccess = Date.now(); + return cached.parsed as T; + } else { + // Invalid - remove from cache + cache.delete(path); + } + } + + // Cache miss or invalid - load and cache + const entry = await loadAndCache(path); + return entry.parsed as T; + }, + + async readRaw(path: string): Promise { + const cached = cache.get(path); + + // Cache hit - check if still valid + if (cached) { + if (await isValid(path, cached)) { + // Update last access for LRU + cached.lastAccess = Date.now(); + return cached.raw; + } else { + // Invalid - remove from cache + cache.delete(path); + } + } + + // Cache miss or invalid - load and cache + const entry = await loadAndCache(path); + return entry.raw; + }, + + invalidate(path: string): void { + cache.delete(path); + }, + + get size(): number { + return cache.size; + }, + }; +}