diff --git a/.changeset/spotty-lions-bake.md b/.changeset/spotty-lions-bake.md new file mode 100644 index 0000000..37f8049 --- /dev/null +++ b/.changeset/spotty-lions-bake.md @@ -0,0 +1,8 @@ +--- +"@node-ts-cache/core": patch +"@node-ts-cache/elasticsearch-storage": patch +"@node-ts-cache/memcached-storage": patch +"@node-ts-cache/valkey-storage": patch +--- + +Add Elasticsearch and Memcached storage adapters diff --git a/README.md b/README.md index 8256350..5a17bf2 100644 --- a/README.md +++ b/README.md @@ -48,13 +48,16 @@ This is a monorepo containing the following packages: ### Storage Adapters -| Package | Version | Description | -| ---------------------------------------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------ | -| [@node-ts-cache/redis-storage](./storages/redis) | ![npm](https://img.shields.io/npm/v/@node-ts-cache/redis-storage.svg) | Redis storage using `redis` package (v4.x) | -| [@node-ts-cache/ioredis-storage](./storages/redisio) | ![npm](https://img.shields.io/npm/v/@node-ts-cache/ioredis-storage.svg) | Redis storage using `ioredis` with compression support | -| [@node-ts-cache/node-cache-storage](./storages/node-cache) | ![npm](https://img.shields.io/npm/v/@node-ts-cache/node-cache-storage.svg) | In-memory cache using `node-cache` | -| [@node-ts-cache/lru-storage](./storages/lru) | ![npm](https://img.shields.io/npm/v/@node-ts-cache/lru-storage.svg) | LRU cache with automatic eviction | -| [@node-ts-cache/lru-redis-storage](./storages/lru-redis) | ![npm](https://img.shields.io/npm/v/@node-ts-cache/lru-redis-storage.svg) | Two-tier caching (local LRU + remote Redis) | +| Package | Version | Description | +| ---------------------------------------------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------ | +| [@node-ts-cache/redis-storage](./storages/redis) | ![npm](https://img.shields.io/npm/v/@node-ts-cache/redis-storage.svg) | Redis storage using `redis` package (v4.x) | +| [@node-ts-cache/ioredis-storage](./storages/redisio) | ![npm](https://img.shields.io/npm/v/@node-ts-cache/ioredis-storage.svg) | Redis storage using `ioredis` with compression support | +| [@node-ts-cache/node-cache-storage](./storages/node-cache) | ![npm](https://img.shields.io/npm/v/@node-ts-cache/node-cache-storage.svg) | In-memory cache using `node-cache` | +| [@node-ts-cache/lru-storage](./storages/lru) | ![npm](https://img.shields.io/npm/v/@node-ts-cache/lru-storage.svg) | LRU cache with automatic eviction | +| [@node-ts-cache/lru-redis-storage](./storages/lru-redis) | ![npm](https://img.shields.io/npm/v/@node-ts-cache/lru-redis-storage.svg) | Two-tier caching (local LRU + remote Redis) | +| [@node-ts-cache/elasticsearch-storage](./storages/elasticsearch) | ![npm](https://img.shields.io/npm/v/@node-ts-cache/elasticsearch-storage.svg) | Elasticsearch storage for search-optimized caching | +| [@node-ts-cache/memcached-storage](./storages/memcached) | ![npm](https://img.shields.io/npm/v/@node-ts-cache/memcached-storage.svg) | Memcached storage for distributed caching | +| [@node-ts-cache/valkey-storage](./storages/valkey) | ![npm](https://img.shields.io/npm/v/@node-ts-cache/valkey-storage.svg) | Valkey storage (Redis-compatible, open source) | ## Documentation @@ -80,25 +83,28 @@ For detailed documentation, see the [main package README](./ts-cache/README.md). │ └───────┬────────┘ │ ├──────────────────────────┼──────────────────────────────────────┤ │ ▼ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ Storage Layer │ │ -│ ├──────────┬──────────┬──────────┬──────────┬─────────────┤ │ -│ │ Memory │ FS │ Redis │ LRU │ LRU+Redis │ │ -│ └──────────┴──────────┴──────────┴──────────┴─────────────┘ │ +│ ┌──────────────────────────────────────────────────────────────────────────────────┐ │ +│ │ Storage Layer │ │ +│ ├────────┬──────┬───────┬─────┬───────────┬───────────────┬────────────┬──────────┤ │ +│ │ Memory │ FS │ Redis │ LRU │ LRU+Redis │ Elasticsearch │ Memcached │ Valkey │ │ +│ └────────┴──────┴───────┴─────┴───────────┴───────────────┴────────────┴──────────┘ │ └─────────────────────────────────────────────────────────────────┘ ``` ## Choosing a Storage -| Storage | Type | Use Case | Features | -| ----------------------- | ----- | ---------------------------- | ----------------------------- | -| **MemoryStorage** | Sync | Development, small datasets | Zero config, bundled | -| **FsJsonStorage** | Async | Persistent local cache | File-based, survives restarts | -| **NodeCacheStorage** | Sync | Production single-instance | TTL support, multi-ops | -| **LRUStorage** | Sync | Memory-constrained apps | Auto-eviction, size limits | -| **RedisStorage** | Async | Distributed systems | Shared cache, redis v4 | -| **RedisIOStorage** | Async | Distributed systems | Compression, modern ioredis | -| **LRUWithRedisStorage** | Async | High-performance distributed | Local + remote tiers | +| Storage | Type | Use Case | Features | +| ------------------------ | ----- | ---------------------------- | ------------------------------ | +| **MemoryStorage** | Sync | Development, small datasets | Zero config, bundled | +| **FsJsonStorage** | Async | Persistent local cache | File-based, survives restarts | +| **NodeCacheStorage** | Sync | Production single-instance | TTL support, multi-ops | +| **LRUStorage** | Sync | Memory-constrained apps | Auto-eviction, size limits | +| **RedisStorage** | Async | Distributed systems | Shared cache, redis v4 | +| **RedisIOStorage** | Async | Distributed systems | Compression, modern ioredis | +| **LRUWithRedisStorage** | Async | High-performance distributed | Local + remote tiers | +| **ElasticsearchStorage** | Async | Search-integrated caching | Full-text search, scalable | +| **MemcachedStorage** | Async | High-performance distributed | Simple, fast, widely supported | +| **ValkeyStorage** | Async | Distributed systems | Redis-compatible, open source | ## Requirements diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 26ce7b8..6e00fb0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,6 +33,15 @@ importers: specifier: ^2.1.0 version: 2.1.9(@types/node@20.19.30) + storages/elasticsearch: + dependencies: + '@elastic/elasticsearch': + specifier: ^8.17.0 + version: 8.19.1 + '@node-ts-cache/core': + specifier: workspace:* + version: link:../../ts-cache + storages/lru: dependencies: '@node-ts-cache/core': @@ -58,6 +67,19 @@ importers: specifier: ^8.9.0 version: 8.13.1(@types/ioredis-mock@8.2.6(ioredis@5.9.2))(ioredis@5.9.2) + storages/memcached: + dependencies: + '@node-ts-cache/core': + specifier: workspace:* + version: link:../../ts-cache + memcached: + specifier: ^2.2.2 + version: 2.2.2 + devDependencies: + '@types/memcached': + specifier: ^2.2.10 + version: 2.2.10 + storages/node-cache: dependencies: node-cache: @@ -96,6 +118,19 @@ importers: specifier: ^8.9.0 version: 8.13.1(@types/ioredis-mock@8.2.6(ioredis@5.9.2))(ioredis@5.9.2) + storages/valkey: + dependencies: + '@node-ts-cache/core': + specifier: workspace:* + version: link:../../ts-cache + iovalkey: + specifier: ^0.2.1 + version: 0.2.2 + devDependencies: + ioredis-mock: + specifier: ^8.9.0 + version: 8.13.1(@types/ioredis-mock@8.2.6(ioredis@5.9.2))(ioredis@5.9.2) + ts-cache: devDependencies: ioredis: @@ -166,6 +201,14 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + '@elastic/elasticsearch@8.19.1': + resolution: {integrity: sha512-+1j9NnQVOX+lbWB8LhCM7IkUmjU05Y4+BmSLfusq0msCsQb1Va+OUKFCoOXjCJqQrcgdRdQCjYYyolQ/npQALQ==} + engines: {node: '>=18'} + + '@elastic/transport@8.10.1': + resolution: {integrity: sha512-xo2lPBAJEt81fQRAKa9T/gUq1SPGBHpSnVUXhoSpL996fPZRAfQwFA4BZtEUQL1p8Dezodd3ZN8Wwno+mYyKuw==} + engines: {node: '>=18'} + '@emnapi/core@1.8.1': resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} @@ -521,6 +564,20 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/core@2.5.0': + resolution: {integrity: sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.39.0': + resolution: {integrity: sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg==} + engines: {node: '>=14'} + '@redis/bloom@1.2.0': resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==} peerDependencies: @@ -675,9 +732,18 @@ packages: cpu: [x64] os: [win32] + '@swc/helpers@0.5.18': + resolution: {integrity: sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==} + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/command-line-args@5.2.3': + resolution: {integrity: sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==} + + '@types/command-line-usage@5.0.4': + resolution: {integrity: sha512-BwR5KP3Es/CSht0xqBcUXS3qCAUVXwpRKsV2+arxeb65atasuXG9LykC9Ab10Cw3s2raH92ZqOeILaQbsB2ACg==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -689,6 +755,9 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/memcached@2.2.10': + resolution: {integrity: sha512-AM9smvZN55Gzs2wRrqeMHVP7KE8KWgCJO/XL5yCly2xF6EKa4YlbpK+cLSAH4NG/Ah64HrlegmGqW8kYws7Vxg==} + '@types/node-cache@4.2.5': resolution: {integrity: sha512-faK2Owokboz53g8ooq2dw3iDJ6/HMTCIa2RvMte5WMTiABy+wA558K+iuyRtlR67Un5q9gEKysSDtqZYbSa0Pg==} deprecated: This is a stub types definition. node-cache provides its own type definitions, so you do not need this installed. @@ -699,6 +768,9 @@ packages: '@types/node@20.19.30': resolution: {integrity: sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==} + '@types/node@24.10.9': + resolution: {integrity: sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==} + '@typescript-eslint/eslint-plugin@8.53.1': resolution: {integrity: sha512-cFYYFZ+oQFi6hUnBTbLRXfTJiaQtYE3t4O692agbBl+2Zy+eqSKWtPjhPXJu1G7j4RLjKgeJPDdq3EqOwmX5Ag==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -812,12 +884,20 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + apache-arrow@21.1.0: + resolution: {integrity: sha512-kQrYLxhC+NTVVZ4CCzGF6L/uPVOzJmD1T3XgbiUnP7oTeVFOFgEUu6IKNwCDkpFoBVqDKQivlX4RUFqqnWFlEA==} + hasBin: true + argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + array-back@6.2.2: + resolution: {integrity: sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==} + engines: {node: '>=12.17'} + array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} @@ -855,6 +935,10 @@ packages: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} + chalk-template@0.4.0: + resolution: {integrity: sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==} + engines: {node: '>=12'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -885,9 +969,25 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + command-line-args@6.0.1: + resolution: {integrity: sha512-Jr3eByUjqyK0qd8W0SGFW1nZwqCaNCtbXjRo2cRJC1OYxWl3MZ5t1US3jq+cO4sPavqgw4l9BMGX0CBe+trepg==} + engines: {node: '>=12.20'} + peerDependencies: + '@75lb/nature': latest + peerDependenciesMeta: + '@75lb/nature': + optional: true + + command-line-usage@7.0.3: + resolution: {integrity: sha512-PqMLy5+YGwhMh1wS04mVG44oqDsgyLRSKJBdOo1bnYhMKBW65gZF1dRp2OZRhiTjgUHljy99qkO7bsctLaw35Q==} + engines: {node: '>=12.20.0'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + connection-parse@0.0.7: + resolution: {integrity: sha512-bTTG28diWg7R7/+qE5NZumwPbCiJOT8uPdZYu674brDjBWQctbaQbYlDKhalS+4i5HxIx+G8dZsnBHKzWpp01A==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1034,6 +1134,15 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + find-replace@5.0.2: + resolution: {integrity: sha512-Y45BAiE3mz2QsrN2fb5QEtO4qb44NcS7en/0y9PEVsg351HsLeVclP8QPMH79Le9sH3rs5RSwJu99W0WPZO43Q==} + engines: {node: '>=14'} + peerDependencies: + '@75lb/nature': latest + peerDependenciesMeta: + '@75lb/nature': + optional: true + find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -1046,6 +1155,9 @@ packages: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} + flatbuffers@25.9.23: + resolution: {integrity: sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==} + flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} @@ -1093,6 +1205,13 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + hashring@3.2.0: + resolution: {integrity: sha512-xCMovURClsQZ+TR30icCZj+34Fq1hs0y6YCASD6ZqdRfYRybb5Iadws2WS+w09mGM/kf9xyA5FCdJQGcgcraSA==} + + hpagent@1.2.0: + resolution: {integrity: sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==} + engines: {node: '>=14'} + human-id@4.1.3: resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==} hasBin: true @@ -1128,6 +1247,10 @@ packages: resolution: {integrity: sha512-tAAg/72/VxOUW7RQSX1pIxJVucYKcjFjfvj60L57jrZpYCHC3XN0WCQ3sNYL4Gmvv+7GPvTAjc+KSdeNuE8oWQ==} engines: {node: '>=12.22.0'} + iovalkey@0.2.2: + resolution: {integrity: sha512-7eVmLOYV2UamZ/YPXuUwTu/4zBDxXcfjj/wmOwlKBBhU2qjg60Th0Y/cqfED3OxNAhc6hUV2Ft4eQMCKi2EMpQ==} + engines: {node: '>=18.12.0'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1151,6 +1274,9 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + jackpot@0.0.6: + resolution: {integrity: sha512-rbWXX+A9ooq03/dfavLg9OXQ8YB57Wa7PY5c4LfU3CgFpwEhhl3WyXTQVurkaT7zBM5I9SSOaiLyJ4I0DQmC0g==} + js-yaml@3.14.2: resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} hasBin: true @@ -1159,6 +1285,10 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + json-bignum@0.0.3: + resolution: {integrity: sha512-2WHyXj3OfHSgNyuzDbSxI1w2jgw5gkWSWhS7Qg4bWXx1nLk3jnbwfUeS0PSba3IzpTUWdHxBieELUzXRjQB2zg==} + engines: {node: '>=0.8'} + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -1186,6 +1316,9 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + lodash.defaults@4.2.0: resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} @@ -1211,6 +1344,9 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + memcached@2.2.2: + resolution: {integrity: sha512-lHwUmqkT9WdUUgRsAvquO4xsKXYaBd644Orz31tuth+w/BIfFNuJMWwsG7sa7H3XXytaNfPTZ5R/yOG3d9zJMA==} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1391,6 +1527,9 @@ packages: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} + retry@0.6.0: + resolution: {integrity: sha512-RgncoxLF1GqwAzTZs/K2YpZkWrdIYbXsmesdomi+iPilSzjUyr/wzNIuteoTVaWokzdwZIJ9NHRNQa/RUiOB2g==} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -1411,6 +1550,9 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + secure-json-parse@3.0.2: + resolution: {integrity: sha512-H6nS2o8bWfpFEV6U38sOSjS7bTbdgbCGU9wEM6W14P5H0QOsz94KCusifV44GpHDTu2nqZbuDNhTzu+mjDSw1w==} + semver@7.7.3: resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} engines: {node: '>=10'} @@ -1431,6 +1573,9 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + simple-lru-cache@0.0.2: + resolution: {integrity: sha512-uEv/AFO0ADI7d99OHDmh1QfYzQk/izT1vCmu/riQfh7qjBVUUgRT87E5s5h7CxWCA/+YoZerykpEthzVrW3LIw==} + slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -1477,6 +1622,10 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + table-layout@4.1.1: + resolution: {integrity: sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==} + engines: {node: '>=12.17'} + term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} @@ -1536,9 +1685,20 @@ packages: engines: {node: '>=14.17'} hasBin: true + typical@7.3.0: + resolution: {integrity: sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==} + engines: {node: '>=12.17'} + undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + undici@6.23.0: + resolution: {integrity: sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==} + engines: {node: '>=18.17'} + universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} @@ -1621,6 +1781,10 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wordwrapjs@5.1.1: + resolution: {integrity: sha512-0yweIbkINJodk27gX9LBGMzyQdBDan3s/dEAiwBOj+Mf0PPyWL6/rikalkv8EeD0E8jm4o5RXEOrFTP3NXbhJg==} + engines: {node: '>=12.17'} + yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} @@ -1776,6 +1940,28 @@ snapshots: human-id: 4.1.3 prettier: 2.8.8 + '@elastic/elasticsearch@8.19.1': + dependencies: + '@elastic/transport': 8.10.1 + apache-arrow: 21.1.0 + tslib: 2.8.1 + transitivePeerDependencies: + - '@75lb/nature' + - supports-color + + '@elastic/transport@8.10.1': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + debug: 4.4.3 + hpagent: 1.2.0 + ms: 2.1.3 + secure-json-parse: 3.0.2 + tslib: 2.8.1 + undici: 6.23.0 + transitivePeerDependencies: + - supports-color + '@emnapi/core@1.8.1': dependencies: '@emnapi/wasi-threads': 1.1.0 @@ -2028,6 +2214,15 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 + '@opentelemetry/api@1.9.0': {} + + '@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.39.0 + + '@opentelemetry/semantic-conventions@1.39.0': {} + '@redis/bloom@1.2.0(@redis/client@1.6.1)': dependencies: '@redis/client': 1.6.1 @@ -2129,11 +2324,19 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.55.3': optional: true + '@swc/helpers@0.5.18': + dependencies: + tslib: 2.8.1 + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 optional: true + '@types/command-line-args@5.2.3': {} + + '@types/command-line-usage@5.0.4': {} + '@types/estree@1.0.8': {} '@types/ioredis-mock@8.2.6(ioredis@5.9.2)': @@ -2142,6 +2345,10 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/memcached@2.2.10': + dependencies: + '@types/node': 20.19.30 + '@types/node-cache@4.2.5': dependencies: node-cache: 5.1.2 @@ -2152,6 +2359,10 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/node@24.10.9': + dependencies: + undici-types: 7.16.0 + '@typescript-eslint/eslint-plugin@8.53.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -2304,12 +2515,28 @@ snapshots: dependencies: color-convert: 2.0.1 + apache-arrow@21.1.0: + dependencies: + '@swc/helpers': 0.5.18 + '@types/command-line-args': 5.2.3 + '@types/command-line-usage': 5.0.4 + '@types/node': 24.10.9 + command-line-args: 6.0.1 + command-line-usage: 7.0.3 + flatbuffers: 25.9.23 + json-bignum: 0.0.3 + tslib: 2.8.1 + transitivePeerDependencies: + - '@75lb/nature' + argparse@1.0.10: dependencies: sprintf-js: 1.0.3 argparse@2.0.1: {} + array-back@6.2.2: {} + array-union@2.1.0: {} assertion-error@2.0.1: {} @@ -2345,6 +2572,10 @@ snapshots: loupe: 3.2.1 pathval: 2.0.1 + chalk-template@0.4.0: + dependencies: + chalk: 4.1.2 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -2366,8 +2597,24 @@ snapshots: color-name@1.1.4: {} + command-line-args@6.0.1: + dependencies: + array-back: 6.2.2 + find-replace: 5.0.2 + lodash.camelcase: 4.3.0 + typical: 7.3.0 + + command-line-usage@7.0.3: + dependencies: + array-back: 6.2.2 + chalk-template: 0.4.0 + table-layout: 4.1.1 + typical: 7.3.0 + concat-map@0.0.1: {} + connection-parse@0.0.7: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -2541,6 +2788,8 @@ snapshots: dependencies: to-regex-range: 5.0.1 + find-replace@5.0.2: {} + find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -2556,6 +2805,8 @@ snapshots: flatted: 3.3.3 keyv: 4.5.4 + flatbuffers@25.9.23: {} + flatted@3.3.3: {} fs-extra@7.0.1: @@ -2604,6 +2855,13 @@ snapshots: has-flag@4.0.0: {} + hashring@3.2.0: + dependencies: + connection-parse: 0.0.7 + simple-lru-cache: 0.0.2 + + hpagent@1.2.0: {} + human-id@4.1.3: {} iconv-lite@0.7.2: @@ -2645,6 +2903,20 @@ snapshots: transitivePeerDependencies: - supports-color + iovalkey@0.2.2: + dependencies: + '@ioredis/commands': 1.5.0 + cluster-key-slot: 1.1.2 + debug: 4.4.3 + denque: 2.1.0 + lodash.defaults: 4.2.0 + lodash.isarguments: 3.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + is-extglob@2.1.1: {} is-glob@4.0.3: @@ -2661,6 +2933,10 @@ snapshots: isexe@2.0.0: {} + jackpot@0.0.6: + dependencies: + retry: 0.6.0 + js-yaml@3.14.2: dependencies: argparse: 1.0.10 @@ -2670,6 +2946,8 @@ snapshots: dependencies: argparse: 2.0.1 + json-bignum@0.0.3: {} + json-buffer@3.0.1: {} json-schema-traverse@0.4.1: {} @@ -2697,6 +2975,8 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.camelcase@4.3.0: {} + lodash.defaults@4.2.0: {} lodash.isarguments@3.1.0: {} @@ -2715,6 +2995,11 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + memcached@2.2.2: + dependencies: + hashring: 3.2.0 + jackpot: 0.0.6 + merge2@1.4.1: {} micromatch@4.0.8: @@ -2862,6 +3147,8 @@ snapshots: resolve-from@5.0.0: {} + retry@0.6.0: {} + reusify@1.1.0: {} rimraf@6.1.2: @@ -2906,6 +3193,8 @@ snapshots: safer-buffer@2.1.2: {} + secure-json-parse@3.0.2: {} + semver@7.7.3: {} shebang-command@2.0.0: @@ -2918,6 +3207,8 @@ snapshots: signal-exit@4.1.0: {} + simple-lru-cache@0.0.2: {} + slash@3.0.0: {} snappy@7.3.3: @@ -2970,6 +3261,11 @@ snapshots: dependencies: has-flag: 4.0.0 + table-layout@4.1.1: + dependencies: + array-back: 6.2.2 + wordwrapjs: 5.1.1 + term-size@2.2.1: {} tinybench@2.9.0: {} @@ -2997,8 +3293,7 @@ snapshots: dependencies: typescript: 5.9.3 - tslib@2.8.1: - optional: true + tslib@2.8.1: {} type-check@0.4.0: dependencies: @@ -3017,8 +3312,14 @@ snapshots: typescript@5.9.3: {} + typical@7.3.0: {} + undici-types@6.21.0: {} + undici-types@7.16.0: {} + + undici@6.23.0: {} + universalify@0.1.2: {} uri-js@4.4.1: @@ -3098,6 +3399,8 @@ snapshots: word-wrap@1.2.5: {} + wordwrapjs@5.1.1: {} + yallist@4.0.0: {} yocto-queue@0.1.0: {} diff --git a/storages/elasticsearch/README.md b/storages/elasticsearch/README.md new file mode 100644 index 0000000..900e879 --- /dev/null +++ b/storages/elasticsearch/README.md @@ -0,0 +1,84 @@ +# @node-ts-cache/elasticsearch-storage + +Elasticsearch storage adapter for [node-ts-cache](https://github.com/simllll/node-ts-cache). + +## Installation + +```bash +npm install @node-ts-cache/core @node-ts-cache/elasticsearch-storage +``` + +## Usage + +```typescript +import { Cache, ExpirationStrategy } from '@node-ts-cache/core'; +import { ElasticsearchStorage } from '@node-ts-cache/elasticsearch-storage'; + +const elasticsearchCache = new ExpirationStrategy( + new ElasticsearchStorage({ + indexName: 'my-cache-index', + clientOptions: { + node: 'http://localhost:9200' + } + }) +); + +class MyService { + @Cache(elasticsearchCache, { ttl: 60 }) + async getUsers(): Promise { + // expensive operation + } +} +``` + +## Configuration Options + +```typescript +interface ElasticsearchStorageOptions { + /** The index name to use for storing cache entries */ + indexName: string; + /** Elasticsearch client options */ + clientOptions?: ClientOptions; + /** Pre-configured Elasticsearch client instance (takes precedence over clientOptions) */ + client?: Client; +} +``` + +### Using a pre-configured client + +If you already have an Elasticsearch client instance configured (e.g., with authentication), you can pass it directly: + +```typescript +import { Client } from '@elastic/elasticsearch'; +import { ElasticsearchStorage } from '@node-ts-cache/elasticsearch-storage'; + +const client = new Client({ + node: 'https://my-elasticsearch-cluster.com', + auth: { + apiKey: 'your-api-key' + } +}); + +const storage = new ElasticsearchStorage({ + indexName: 'my-cache', + client +}); +``` + +## Running Tests Locally + +Start Elasticsearch using Docker: + +```bash +docker run -p 9200:9200 -e "discovery.type=single-node" -e "xpack.security.enabled=false" elasticsearch:8.12.0 +``` + +Then run the tests: + +```bash +npm test +``` + +## License + +MIT diff --git a/storages/elasticsearch/package.json b/storages/elasticsearch/package.json new file mode 100644 index 0000000..0df5186 --- /dev/null +++ b/storages/elasticsearch/package.json @@ -0,0 +1,56 @@ +{ + "name": "@node-ts-cache/elasticsearch-storage", + "version": "1.0.0", + "description": "Elasticsearch storage adapter for node-ts-cache", + "keywords": [ + "node", + "nodejs", + "cache", + "typescript", + "ts", + "caching", + "elasticsearch", + "elastic", + "ts-cache" + ], + "homepage": "https://github.com/simllll/node-ts-cache/tree/master/storages/elasticsearch#readme", + "bugs": { + "url": "https://github.com/simllll/node-ts-cache/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/simllll/node-ts-cache.git" + }, + "license": "MIT", + "author": "Simon Tretter ", + "contributors": [ + "Himmet Avsar " + ], + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc -p .", + "clean": "git clean -fdx src", + "dev": "tsc -p . -w", + "prepublishOnly": "tsc -p .", + "test": "vitest run" + }, + "dependencies": { + "@node-ts-cache/core": "workspace:*", + "@elastic/elasticsearch": "^8.17.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/storages/elasticsearch/src/elasticsearch.storage.ts b/storages/elasticsearch/src/elasticsearch.storage.ts new file mode 100644 index 0000000..d54c8cd --- /dev/null +++ b/storages/elasticsearch/src/elasticsearch.storage.ts @@ -0,0 +1,99 @@ +import { IAsynchronousCacheType } from '@node-ts-cache/core'; +import { Client, ClientOptions } from '@elastic/elasticsearch'; + +export interface ElasticsearchStorageOptions { + /** The index name to use for storing cache entries */ + indexName: string; + /** Elasticsearch client options */ + clientOptions?: ClientOptions; + /** Pre-configured Elasticsearch client instance (takes precedence over clientOptions) */ + client?: Client; +} + +export class ElasticsearchStorage implements IAsynchronousCacheType { + private client: Client; + private indexName: string; + + constructor(options: ElasticsearchStorageOptions) { + this.indexName = options.indexName; + + if (options.client) { + this.client = options.client; + } else if (options.clientOptions) { + this.client = new Client(options.clientOptions); + } else { + this.client = new Client({ node: 'http://localhost:9200' }); + } + } + + public async getItem(key: string): Promise { + try { + const response = await this.client.get({ + index: this.indexName, + id: key + }); + + const source = response._source as { content: unknown } | undefined; + if (source === undefined || source === null) { + return undefined; + } + + return source.content as T; + } catch (error: unknown) { + // Handle 404 (document not found) gracefully + if (this.isNotFoundError(error)) { + return undefined; + } + throw error; + } + } + + public async setItem(key: string, content: T | undefined): Promise { + if (content === undefined) { + try { + await this.client.delete({ + index: this.indexName, + id: key, + refresh: 'wait_for' + }); + } catch (error: unknown) { + // Ignore 404 errors when deleting non-existent documents + if (!this.isNotFoundError(error)) { + throw error; + } + } + return; + } + + await this.client.index({ + index: this.indexName, + id: key, + refresh: 'wait_for', + document: { content } + }); + } + + public async clear(): Promise { + try { + await this.client.indices.delete({ + index: this.indexName, + ignore_unavailable: true + }); + } catch { + // Ignore errors when clearing (index might not exist) + } + } + + public async close(): Promise { + await this.client.close(); + } + + private isNotFoundError(error: unknown): boolean { + return ( + error !== null && + typeof error === 'object' && + 'meta' in error && + (error as { meta?: { statusCode?: number } }).meta?.statusCode === 404 + ); + } +} diff --git a/storages/elasticsearch/src/index.ts b/storages/elasticsearch/src/index.ts new file mode 100644 index 0000000..e8cec7d --- /dev/null +++ b/storages/elasticsearch/src/index.ts @@ -0,0 +1,4 @@ +import { ElasticsearchStorage } from './elasticsearch.storage.js'; + +export default ElasticsearchStorage; +export { ElasticsearchStorage, ElasticsearchStorageOptions } from './elasticsearch.storage.js'; diff --git a/storages/elasticsearch/test/elasticsearch.storage.test.ts b/storages/elasticsearch/test/elasticsearch.storage.test.ts new file mode 100644 index 0000000..bd1f8c8 --- /dev/null +++ b/storages/elasticsearch/test/elasticsearch.storage.test.ts @@ -0,0 +1,134 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { ElasticsearchStorage } from '../src/elasticsearch.storage.js'; + +// Mock Elasticsearch client for testing +class MockElasticsearchClient { + private store: Map = new Map(); + + async get({ index, id }: { index: string; id: string }) { + const key = `${index}:${id}`; + if (!this.store.has(key)) { + const error = new Error('Not found'); + (error as Error & { meta?: { statusCode: number } }).meta = { statusCode: 404 }; + throw error; + } + return { _source: this.store.get(key) }; + } + + async index({ + index, + id, + document + }: { + index: string; + id: string; + refresh?: string; + document: unknown; + }) { + const key = `${index}:${id}`; + this.store.set(key, document); + return { result: 'created' }; + } + + async delete({ index, id }: { index: string; id: string; refresh?: string }) { + const key = `${index}:${id}`; + if (!this.store.has(key)) { + const error = new Error('Not found'); + (error as Error & { meta?: { statusCode: number } }).meta = { statusCode: 404 }; + throw error; + } + this.store.delete(key); + return { result: 'deleted' }; + } + + indices = { + delete: async () => { + this.store.clear(); + return { acknowledged: true }; + } + }; + + async close() { + // no-op + } +} + +describe('ElasticsearchStorage', () => { + let storage: ElasticsearchStorage; + let mockClient: MockElasticsearchClient; + + beforeEach(() => { + mockClient = new MockElasticsearchClient(); + storage = new ElasticsearchStorage({ + indexName: 'test-index', + client: mockClient as unknown as import('@elastic/elasticsearch').Client + }); + }); + + it('Should return undefined if cache not hit', async () => { + const item = await storage.getItem('nonexistent-key'); + expect(item).toBe(undefined); + }); + + it('Should set and get a string value', async () => { + await storage.setItem('testKey', 'testValue'); + const result = await storage.getItem('testKey'); + expect(result).toBe('testValue'); + }); + + it('Should set and get an object value', async () => { + const testObj = { name: 'test', value: 123 }; + await storage.setItem('objectKey', testObj); + const result = await storage.getItem('objectKey'); + expect(result).toEqual(testObj); + }); + + it('Should delete cache item when set to undefined', async () => { + await storage.setItem('deleteKey', 'value'); + expect(await storage.getItem('deleteKey')).toBe('value'); + + await storage.setItem('deleteKey', undefined); + expect(await storage.getItem('deleteKey')).toBe(undefined); + }); + + it('Should handle numeric values', async () => { + await storage.setItem('numKey', 42); + const result = await storage.getItem('numKey'); + expect(result).toBe(42); + }); + + it('Should handle array values', async () => { + const testArray = [1, 2, 3, 'test']; + await storage.setItem('arrayKey', testArray); + const result = await storage.getItem<(number | string)[]>('arrayKey'); + expect(result).toEqual(testArray); + }); + + it('Should handle nested object values', async () => { + const nested = { + level1: { + level2: { + value: 'deep' + } + } + }; + await storage.setItem('nestedKey', nested); + const result = await storage.getItem('nestedKey'); + expect(result).toEqual(nested); + }); + + it('Should clear all items', async () => { + await storage.setItem('key1', 'value1'); + await storage.setItem('key2', 'value2'); + + await storage.clear(); + + expect(await storage.getItem('key1')).toBe(undefined); + expect(await storage.getItem('key2')).toBe(undefined); + }); + + it('Should handle deleting non-existent keys gracefully', async () => { + // Should not throw + await storage.setItem('never-existed', undefined); + }); +}); diff --git a/storages/elasticsearch/tsconfig.json b/storages/elasticsearch/tsconfig.json new file mode 100644 index 0000000..ef88f77 --- /dev/null +++ b/storages/elasticsearch/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist" + }, + "include": ["./src"] +} diff --git a/storages/memcached/README.md b/storages/memcached/README.md new file mode 100644 index 0000000..8d0135f --- /dev/null +++ b/storages/memcached/README.md @@ -0,0 +1,86 @@ +# @node-ts-cache/memcached-storage + +Memcached storage adapter for [node-ts-cache](https://github.com/simllll/node-ts-cache). + +## Installation + +```bash +npm install @node-ts-cache/core @node-ts-cache/memcached-storage +``` + +## Usage + +```typescript +import { Cache, ExpirationStrategy } from '@node-ts-cache/core'; +import { MemcachedStorage } from '@node-ts-cache/memcached-storage'; + +const memcachedCache = new ExpirationStrategy( + new MemcachedStorage({ + location: 'localhost:11211' + }) +); + +class MyService { + @Cache(memcachedCache, { ttl: 60 }) + async getUsers(): Promise { + // expensive operation + } +} +``` + +## Configuration Options + +```typescript +interface MemcachedStorageOptions { + /** Memcached server location(s) - e.g., 'localhost:11211' or ['server1:11211', 'server2:11211'] */ + location: Memcached.Location; + /** Memcached client options */ + options?: Memcached.options; +} +``` + +### Multiple Servers (Distributed) + +Memcached supports distributed caching across multiple servers: + +```typescript +import { MemcachedStorage } from '@node-ts-cache/memcached-storage'; + +const storage = new MemcachedStorage({ + location: ['server1:11211', 'server2:11211', 'server3:11211'], + options: { + retries: 3, + timeout: 5000, + poolSize: 10 + } +}); +``` + +### Available Options + +The `options` parameter accepts all standard [memcached](https://www.npmjs.com/package/memcached) options: + +- `maxKeySize` - Maximum key size (default: 250) +- `maxValue` - Maximum value size (default: 1048576) +- `poolSize` - Connection pool size (default: 10) +- `retries` - Number of retries for failed operations (default: 5) +- `timeout` - Operation timeout in milliseconds (default: 5000) +- `idle` - Idle timeout for connections (default: 5000) + +## Running Tests Locally + +Start Memcached using Docker: + +```bash +docker run -p 11211:11211 memcached:latest +``` + +Then run the tests: + +```bash +npm test +``` + +## License + +MIT diff --git a/storages/memcached/package.json b/storages/memcached/package.json new file mode 100644 index 0000000..f8a978d --- /dev/null +++ b/storages/memcached/package.json @@ -0,0 +1,51 @@ +{ + "name": "@node-ts-cache/memcached-storage", + "version": "1.0.0", + "description": "Memcached storage adapter for node-ts-cache", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc -p .", + "clean": "git clean -fdx src", + "dev": "tsc -p . -w", + "prepublishOnly": "tsc -p .", + "test": "vitest run" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/simllll/node-ts-cache.git" + }, + "keywords": [ + "typescript", + "cache", + "memcached", + "storage" + ], + "author": "simllll", + "license": "MIT", + "bugs": { + "url": "https://github.com/simllll/node-ts-cache/issues" + }, + "homepage": "https://github.com/simllll/node-ts-cache#readme", + "dependencies": { + "@node-ts-cache/core": "workspace:*", + "memcached": "^2.2.2" + }, + "devDependencies": { + "@types/memcached": "^2.2.10" + }, + "engines": { + "node": ">=18.0.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/storages/memcached/src/index.ts b/storages/memcached/src/index.ts new file mode 100644 index 0000000..ff90cf1 --- /dev/null +++ b/storages/memcached/src/index.ts @@ -0,0 +1 @@ +export { MemcachedStorage, MemcachedStorageOptions } from './memcached.storage.js'; diff --git a/storages/memcached/src/memcached.storage.ts b/storages/memcached/src/memcached.storage.ts new file mode 100644 index 0000000..5b46f0c --- /dev/null +++ b/storages/memcached/src/memcached.storage.ts @@ -0,0 +1,89 @@ +import { IAsynchronousCacheType } from '@node-ts-cache/core'; +import Memcached from 'memcached'; + +export interface MemcachedStorageOptions { + /** Memcached server location(s) - e.g., 'localhost:11211' or ['server1:11211', 'server2:11211'] */ + location: Memcached.Location; + /** Memcached client options */ + options?: Memcached.options; + /** Pre-configured Memcached client instance (takes precedence over location) */ + client?: Memcached; +} + +export class MemcachedStorage implements IAsynchronousCacheType { + private client: Memcached; + + constructor(options: MemcachedStorageOptions) { + if (options.client) { + this.client = options.client; + } else { + this.client = new Memcached(options.location, options.options); + } + } + + public async getItem(key: string): Promise { + return new Promise((resolve, reject) => { + this.client.get(key, (err, data) => { + if (err) { + reject(err); + return; + } + + if (data === undefined) { + resolve(undefined); + return; + } + + try { + const parsed = JSON.parse(data as string) as T; + resolve(parsed); + } catch { + // If parsing fails, return the raw data + resolve(data as T); + } + }); + }); + } + + public async setItem(key: string, content: T | undefined): Promise { + return new Promise((resolve, reject) => { + if (content === undefined) { + this.client.del(key, err => { + if (err) { + reject(err); + return; + } + resolve(); + }); + return; + } + + const serialized = typeof content === 'string' ? content : JSON.stringify(content); + + // Default TTL of 0 means no expiration in memcached + this.client.set(key, serialized, 0, err => { + if (err) { + reject(err); + return; + } + resolve(); + }); + }); + } + + public async clear(): Promise { + return new Promise((resolve, reject) => { + this.client.flush(err => { + if (err) { + reject(err); + return; + } + resolve(); + }); + }); + } + + public end(): void { + this.client.end(); + } +} diff --git a/storages/memcached/test/memcached.storage.test.ts b/storages/memcached/test/memcached.storage.test.ts new file mode 100644 index 0000000..11b95f3 --- /dev/null +++ b/storages/memcached/test/memcached.storage.test.ts @@ -0,0 +1,78 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { MemcachedStorage } from '../src/index.js'; +import type Memcached from 'memcached'; + +// Mock Memcached client for testing +class MockMemcached { + private store: Map = new Map(); + + get(key: string, callback: (err: Error | undefined, data: string | undefined) => void): void { + const data = this.store.get(key); + callback(undefined, data); + } + + set(key: string, value: string, _ttl: number, callback: (err: Error | undefined) => void): void { + this.store.set(key, value); + callback(undefined); + } + + del(key: string, callback: (err: Error | undefined) => void): void { + this.store.delete(key); + callback(undefined); + } + + flush(callback: (err: Error | undefined) => void): void { + this.store.clear(); + callback(undefined); + } + + end(): void { + // no-op + } +} + +describe('MemcachedStorage', () => { + let storage: MemcachedStorage; + let mockClient: MockMemcached; + + beforeEach(() => { + mockClient = new MockMemcached(); + storage = new MemcachedStorage({ + location: 'mock:11211', + client: mockClient as unknown as Memcached + }); + }); + + it('Should return undefined if cache not hit', async () => { + const item = await storage.getItem('nonexistent-key'); + expect(item).toBe(undefined); + }); + + it('Should set and get a string value', async () => { + await storage.setItem('testKey', 'testValue'); + const result = await storage.getItem('testKey'); + expect(result).toBe('testValue'); + }); + + it('Should set and get an object value', async () => { + const content = { data: { name: 'test', value: 123 } }; + await storage.setItem('objectKey', content); + const result = await storage.getItem('objectKey'); + expect(result).toEqual(content); + }); + + it('Should delete cache item if set to undefined', async () => { + await storage.setItem('deleteKey', 'value'); + await storage.setItem('deleteKey', undefined); + const result = await storage.getItem('deleteKey'); + expect(result).toBe(undefined); + }); + + it('Should clear all items', async () => { + await storage.setItem('key1', 'value1'); + await storage.setItem('key2', 'value2'); + await storage.clear(); + expect(await storage.getItem('key1')).toBe(undefined); + expect(await storage.getItem('key2')).toBe(undefined); + }); +}); diff --git a/storages/memcached/tsconfig.json b/storages/memcached/tsconfig.json new file mode 100644 index 0000000..ef88f77 --- /dev/null +++ b/storages/memcached/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist" + }, + "include": ["./src"] +} diff --git a/storages/redis/src/index.ts b/storages/redis/src/index.ts index b93081c..8d0916e 100644 --- a/storages/redis/src/index.ts +++ b/storages/redis/src/index.ts @@ -1,3 +1,4 @@ -import { RedisStorage } from './redis.storage.js'; +import { RedisStorage, RedisStorageOptions } from './redis.storage.js'; +export { RedisStorage, RedisStorageOptions }; export default RedisStorage; diff --git a/storages/redis/src/redis.storage.ts b/storages/redis/src/redis.storage.ts index de41d77..2ce16bc 100644 --- a/storages/redis/src/redis.storage.ts +++ b/storages/redis/src/redis.storage.ts @@ -3,17 +3,29 @@ import { createClient, RedisClientOptions } from 'redis'; type RedisClient = ReturnType; +export interface RedisStorageOptions extends RedisClientOptions { + /** Pre-configured Redis client instance (takes precedence over other options) */ + client?: RedisClient; +} + export class RedisStorage implements IAsynchronousCacheType { private client: RedisClient; - private connectionPromise: Promise; + private connectionPromise: Promise | null; - constructor(redisOptions?: RedisClientOptions) { - this.client = createClient(redisOptions); - this.connectionPromise = this.client.connect(); + constructor(redisOptions?: RedisStorageOptions) { + if (redisOptions?.client) { + this.client = redisOptions.client; + this.connectionPromise = null; // Already connected + } else { + this.client = createClient(redisOptions); + this.connectionPromise = this.client.connect(); + } } private async ensureConnected(): Promise { - await this.connectionPromise; + if (this.connectionPromise) { + await this.connectionPromise; + } } public async getItem(key: string): Promise { diff --git a/storages/redis/test/redis.storage.test.ts b/storages/redis/test/redis.storage.test.ts index 3e8ac46..3b5f556 100644 --- a/storages/redis/test/redis.storage.test.ts +++ b/storages/redis/test/redis.storage.test.ts @@ -1,24 +1,47 @@ -import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { describe, it, expect, beforeEach } from 'vitest'; import { RedisStorage } from '../src/redis.storage.js'; +import type { createClient } from 'redis'; -// Requires Redis - use REDIS_HOST/REDIS_PORT env vars or defaults to localhost:6379 -// In CI: provided by Redis service container -// Locally: docker run -p 6379:6379 redis:7 -const host = process.env.REDIS_HOST || 'localhost'; -const port = Number(process.env.REDIS_PORT) || 6379; +type RedisClient = ReturnType; -let storage: RedisStorage; +// Mock Redis client for testing +class MockRedisClient { + private store: Map = new Map(); + + async get(key: string): Promise { + return this.store.get(key) ?? null; + } + + async set(key: string, value: string): Promise<'OK'> { + this.store.set(key, value); + return 'OK'; + } + + async del(key: string): Promise { + this.store.delete(key); + return 1; + } + + async flushDb(): Promise<'OK'> { + this.store.clear(); + return 'OK'; + } + + async quit(): Promise<'OK'> { + return 'OK'; + } +} describe('RedisStorage', () => { - beforeAll(async () => { + let storage: RedisStorage; + let mockClient: MockRedisClient; + + beforeEach(() => { + mockClient = new MockRedisClient(); storage = new RedisStorage({ - socket: { host, port } + client: mockClient as unknown as RedisClient }); - }, 10000); - - afterAll(async () => { - if (storage) await storage.disconnect(); - }, 10000); + }); it('Should clear Redis without errors', async () => { await storage.clear(); diff --git a/storages/valkey/README.md b/storages/valkey/README.md new file mode 100644 index 0000000..4e617a3 --- /dev/null +++ b/storages/valkey/README.md @@ -0,0 +1,105 @@ +# @node-ts-cache/valkey-storage + +Valkey storage adapter for [node-ts-cache](https://github.com/simllll/node-ts-cache). + +[Valkey](https://valkey.io/) is an open-source, Redis-compatible in-memory data store that emerged as a community-driven fork after Redis changed its license. + +## Installation + +```bash +npm install @node-ts-cache/core @node-ts-cache/valkey-storage +``` + +## Usage + +```typescript +import { Cache, ExpirationStrategy } from '@node-ts-cache/core'; +import { ValkeyStorage } from '@node-ts-cache/valkey-storage'; +import Valkey from 'iovalkey'; + +const valkeyClient = new Valkey({ + host: 'localhost', + port: 6379 +}); + +const valkeyCache = new ExpirationStrategy(new ValkeyStorage(() => valkeyClient, { maxAge: 3600 })); + +class MyService { + @Cache(valkeyCache, { ttl: 60 }) + async getUsers(): Promise { + // expensive operation + } +} +``` + +## Configuration Options + +```typescript +new ValkeyStorage( + valkeyFactory: () => Valkey, + options?: { + maxAge: number; // TTL in seconds (default: 86400 = 1 day) + } +) +``` + +### Constructor Parameters + +| Parameter | Type | Description | +| ---------------- | -------------- | -------------------------------------------------- | +| `valkeyFactory` | `() => Valkey` | Factory function returning a Valkey client | +| `options` | `object` | Configuration options | +| `options.maxAge` | `number` | Default TTL in seconds (default: 86400 = 24 hours) | + +### Error Handling + +You can attach an error handler for non-blocking write operations: + +```typescript +const storage = new ValkeyStorage(() => valkeyClient, { maxAge: 3600 }); + +storage.onError(error => { + console.error('Valkey error:', error); + // Log to monitoring service, etc. +}); +``` + +### Batch Operations + +ValkeyStorage supports efficient batch operations: + +```typescript +// Get multiple items +const items = await storage.getItems(['user:1', 'user:2', 'user:3']); + +// Set multiple items +await storage.setItems([ + { key: 'user:1', content: user1 }, + { key: 'user:2', content: user2 } +]); +``` + +## Running Tests Locally + +Start Valkey using Docker: + +```bash +docker run -p 6379:6379 valkey/valkey:latest +``` + +Then run the tests: + +```bash +npm test +``` + +## Why Valkey? + +- **Open Source**: Valkey is BSD-3 licensed, ensuring it remains truly open source +- **Redis Compatible**: Drop-in replacement for Redis with full protocol compatibility +- **Community Driven**: Backed by Linux Foundation with contributions from AWS, Google, Oracle, and more +- **Active Development**: Regular releases with new features and improvements + +## License + +MIT diff --git a/storages/valkey/package.json b/storages/valkey/package.json new file mode 100644 index 0000000..7e2385b --- /dev/null +++ b/storages/valkey/package.json @@ -0,0 +1,47 @@ +{ + "name": "@node-ts-cache/valkey-storage", + "version": "1.0.0", + "description": "Valkey storage adapter for node-ts-cache", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc -p .", + "clean": "git clean -fdx src", + "dev": "tsc -p . -w", + "prepublishOnly": "tsc -p .", + "test": "vitest run" + }, + "dependencies": { + "@node-ts-cache/core": "workspace:*", + "iovalkey": "^0.2.1" + }, + "devDependencies": { + "ioredis-mock": "^8.9.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "https://github.com/simllll/node-ts-cache.git", + "directory": "storages/valkey" + }, + "keywords": [ + "cache", + "typescript", + "valkey", + "redis-compatible" + ], + "license": "MIT" +} diff --git a/storages/valkey/src/index.ts b/storages/valkey/src/index.ts new file mode 100644 index 0000000..5999920 --- /dev/null +++ b/storages/valkey/src/index.ts @@ -0,0 +1 @@ +export { ValkeyStorage } from './valkey.storage.js'; diff --git a/storages/valkey/src/valkey.storage.ts b/storages/valkey/src/valkey.storage.ts new file mode 100644 index 0000000..daa92bb --- /dev/null +++ b/storages/valkey/src/valkey.storage.ts @@ -0,0 +1,123 @@ +import { IAsynchronousCacheType, IMultiIAsynchronousCacheType } from '@node-ts-cache/core'; +import * as Valkey from 'iovalkey'; + +export class ValkeyStorage implements IAsynchronousCacheType, IMultiIAsynchronousCacheType { + constructor( + private valkey: () => Valkey.default, + private options: { + maxAge: number; + } = { maxAge: 86400 } + ) {} + + private errorHandler: ((error: Error) => void) | undefined; + + onError(listener: (error: Error) => void) { + this.errorHandler = listener; + } + + async getItems(keys: string[]): Promise<{ [key: string]: T | undefined }> { + const mget: (string | null)[] = await this.valkey().mget(...keys); + const res: { [key: string]: T | undefined } = {}; + mget.forEach((entry: string | null, i: number) => { + if (entry === null) { + res[keys[i]] = undefined; // value does not exist yet + return; + } + + if (entry === '') { + res[keys[i]] = null as T; // value does exist, but is empty + return; + } + + // Try to parse as JSON + let parsedItem: T | string = entry; + try { + if (entry) { + parsedItem = JSON.parse(entry) as T; + } + } catch { + /** Not JSON, keep as string */ + } + + res[keys[i]] = parsedItem as T; + }); + return res; + } + + async setItems( + values: { key: string; content: T | undefined }[], + options?: { ttl?: number } + ): Promise { + const pipeline = this.valkey().pipeline(); + values.forEach(val => { + if (val.content === undefined) return; + + const content: string = JSON.stringify(val.content); + + const ttl = options?.ttl ?? this.options.maxAge; + if (ttl) { + pipeline.setex(val.key, ttl, content); + } else { + pipeline.set(val.key, content); + } + }); + const savePromise = pipeline.exec(); + + if (this.errorHandler) { + // if we have an error handler, we do not need to await the result + savePromise.catch((err: unknown) => this.errorHandler && this.errorHandler(err as Error)); + } else { + await savePromise; + } + } + + public async getItem(key: string): Promise { + const entry: string | null = await this.valkey().get(key); + if (entry === null) { + return undefined; + } + if (entry === '') { + return null as T; // value exists but is empty + } + + // Try to parse as JSON + let parsedItem: T | string = entry; + try { + parsedItem = JSON.parse(entry) as T; + } catch { + /** Not JSON, keep as string */ + } + return parsedItem as T; + } + + public async setItem( + key: string, + content: T | undefined, + options?: { ttl?: number } + ): Promise { + if (content === undefined) { + await this.valkey().del(key); + return; + } + + // Serialize to string + const serialized: string = + typeof content === 'object' ? JSON.stringify(content) : String(content); + + const ttl = options?.ttl ?? this.options.maxAge; + const savePromise: Promise<'OK' | null> = ttl + ? this.valkey().setex(key, ttl, serialized) + : this.valkey().set(key, serialized); + + if (this.errorHandler) { + // if we have an error handler, we do not need to await the result + savePromise.catch((err: unknown) => this.errorHandler && this.errorHandler(err as Error)); + } else { + await savePromise; + } + } + + public async clear(): Promise { + await this.valkey().flushdb(); + } +} diff --git a/storages/valkey/test/valkey.storage.test.ts b/storages/valkey/test/valkey.storage.test.ts new file mode 100644 index 0000000..d8a9bfa --- /dev/null +++ b/storages/valkey/test/valkey.storage.test.ts @@ -0,0 +1,77 @@ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { ValkeyStorage } from '../src/valkey.storage.js'; +import RedisMock from 'ioredis-mock'; + +describe('ValkeyStorage', () => { + let storage: ValkeyStorage; + let mockClient: RedisMock; + + beforeAll(async () => { + // Use ioredis-mock since iovalkey is API-compatible with ioredis + mockClient = new RedisMock(); + storage = new ValkeyStorage( + () => mockClient as unknown as ReturnType, + { + maxAge: 3600 + } + ); + }, 10000); + + afterAll(async () => { + if (mockClient) { + mockClient.disconnect(); + } + }, 10000); + + it('Should return undefined if cache not hit', async () => { + const item = await storage.getItem('nonexistent-key'); + expect(item).toBe(undefined); + }); + + it('Should set and get a string value', async () => { + await storage.setItem('testKey', 'testValue'); + const result = await storage.getItem('testKey'); + expect(result).toBe('testValue'); + }); + + it('Should set and get an object value', async () => { + const testObject = { name: 'test', value: 123 }; + await storage.setItem('objectKey', testObject); + const result = await storage.getItem('objectKey'); + expect(result).toEqual(testObject); + }); + + it('Should delete cache item when set to undefined', async () => { + await storage.setItem('deleteKey', 'toDelete'); + await storage.setItem('deleteKey', undefined); + const result = await storage.getItem('deleteKey'); + expect(result).toBe(undefined); + }); + + it('Should get multiple items', async () => { + await storage.setItem('multi1', 'value1'); + await storage.setItem('multi2', 'value2'); + const results = await storage.getItems(['multi1', 'multi2', 'nonexistent']); + expect(results['multi1']).toBe('value1'); + expect(results['multi2']).toBe('value2'); + expect(results['nonexistent']).toBe(undefined); + }); + + it('Should set multiple items', async () => { + await storage.setItems([ + { key: 'batch1', content: 'batchValue1' }, + { key: 'batch2', content: 'batchValue2' } + ]); + const result1 = await storage.getItem('batch1'); + const result2 = await storage.getItem('batch2'); + expect(result1).toBe('batchValue1'); + expect(result2).toBe('batchValue2'); + }); + + it('Should clear all items', async () => { + await storage.setItem('clearKey', 'clearValue'); + await storage.clear(); + const result = await storage.getItem('clearKey'); + expect(result).toBe(undefined); + }); +}); diff --git a/storages/valkey/tsconfig.json b/storages/valkey/tsconfig.json new file mode 100644 index 0000000..ef88f77 --- /dev/null +++ b/storages/valkey/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist" + }, + "include": ["./src"] +} diff --git a/ts-cache/ADVANCED.md b/ts-cache/ADVANCED.md index 4a02580..4eb9ccb 100644 --- a/ts-cache/ADVANCED.md +++ b/ts-cache/ADVANCED.md @@ -298,6 +298,131 @@ const strategy = new ExpirationStrategy(storage); - Reduces Redis round-trips - Good for high-traffic applications +### ElasticsearchStorage + +Elasticsearch-based storage for search-optimized caching. + +```bash +npm install @node-ts-cache/elasticsearch-storage +``` + +```typescript +import { ExpirationStrategy } from '@node-ts-cache/core'; +import { ElasticsearchStorage } from '@node-ts-cache/elasticsearch-storage'; + +// Basic usage +const storage = new ElasticsearchStorage({ + indexName: 'my-cache', + clientOptions: { + node: 'http://localhost:9200' + } +}); + +// With pre-configured client +import { Client } from '@elastic/elasticsearch'; + +const client = new Client({ + node: 'https://my-cluster.com', + auth: { apiKey: 'your-api-key' } +}); + +const storage = new ElasticsearchStorage({ + indexName: 'my-cache', + client +}); + +const strategy = new ExpirationStrategy(storage); +``` + +**Characteristics:** + +- Asynchronous operations +- Scalable distributed storage +- Useful when Elasticsearch is already part of the stack +- Supports complex document caching + +### MemcachedStorage + +High-performance distributed caching using Memcached. + +```bash +npm install @node-ts-cache/memcached-storage +``` + +```typescript +import { ExpirationStrategy } from '@node-ts-cache/core'; +import { MemcachedStorage } from '@node-ts-cache/memcached-storage'; + +// Single server +const storage = new MemcachedStorage({ + location: 'localhost:11211' +}); + +// Multiple servers (distributed) +const distributedStorage = new MemcachedStorage({ + location: ['server1:11211', 'server2:11211', 'server3:11211'], + options: { + retries: 3, + timeout: 5000, + poolSize: 10 + } +}); + +const strategy = new ExpirationStrategy(storage); +``` + +**Characteristics:** + +- Asynchronous operations +- Simple and fast key-value storage +- Excellent for distributed caching +- Lower memory overhead than Redis +- No persistence (data lost on restart) + +### ValkeyStorage + +Valkey storage using [iovalkey](https://github.com/valkey-io/iovalkey), the official Valkey client (ioredis-compatible). + +```bash +npm install @node-ts-cache/valkey-storage +``` + +```typescript +import { ExpirationStrategy } from '@node-ts-cache/core'; +import { ValkeyStorage } from '@node-ts-cache/valkey-storage'; +import Valkey from 'iovalkey'; + +const valkeyClient = new Valkey({ + host: 'localhost', + port: 6379 +}); + +const storage = new ValkeyStorage(() => valkeyClient, { + maxAge: 3600 // TTL in seconds +}); + +// With error handler (non-blocking writes) +storage.onError(error => { + console.error('Valkey error:', error); +}); + +const strategy = new ExpirationStrategy(storage); +``` + +**Constructor Options:** + +| Option | Type | Default | Description | +| -------- | -------- | ------- | ------------------------------- | +| `maxAge` | `number` | `86400` | TTL in seconds (used by Valkey) | + +**Characteristics:** + +- Asynchronous operations +- Supports multi-get/set operations +- Redis-compatible (drop-in replacement) +- Open source (BSD-3 license) +- Backed by Linux Foundation + ## @MultiCache Details ### Signature diff --git a/ts-cache/README.md b/ts-cache/README.md index e6a2023..d85f330 100644 --- a/ts-cache/README.md +++ b/ts-cache/README.md @@ -31,15 +31,18 @@ class UserService { The core package includes `MemoryStorage` and `FsJsonStorage`. Additional storage backends are available as separate packages: -| Package | Storage Type | Sync/Async | Use Case | -| ----------------------------------- | ------------------------------------------------------ | ---------- | -------------------------------------- | -| `@node-ts-cache/core` | MemoryStorage | Sync | Development, simple caching | -| `@node-ts-cache/core` | FsJsonStorage | Async | Persistent local cache | -| `@node-ts-cache/node-cache-storage` | [node-cache](https://www.npmjs.com/package/node-cache) | Sync | Production single-instance with TTL | -| `@node-ts-cache/lru-storage` | [lru-cache](https://www.npmjs.com/package/lru-cache) | Sync | Memory-bounded with automatic eviction | -| `@node-ts-cache/redis-storage` | [redis](https://www.npmjs.com/package/redis) (v4.x) | Async | Shared cache | -| `@node-ts-cache/ioredis-storage` | [ioredis](https://www.npmjs.com/package/ioredis) | Async | Shared cache with compression | -| `@node-ts-cache/lru-redis-storage` | LRU + Redis | Async | Two-tier: fast local + shared remote | +| Package | Storage Type | Sync/Async | Use Case | +| -------------------------------------- | --------------------------------------------------------------------- | ---------- | -------------------------------------- | +| `@node-ts-cache/core` | MemoryStorage | Sync | Development, simple caching | +| `@node-ts-cache/core` | FsJsonStorage | Async | Persistent local cache | +| `@node-ts-cache/node-cache-storage` | [node-cache](https://www.npmjs.com/package/node-cache) | Sync | Production single-instance with TTL | +| `@node-ts-cache/lru-storage` | [lru-cache](https://www.npmjs.com/package/lru-cache) | Sync | Memory-bounded with automatic eviction | +| `@node-ts-cache/redis-storage` | [redis](https://www.npmjs.com/package/redis) (v4.x) | Async | Shared cache | +| `@node-ts-cache/ioredis-storage` | [ioredis](https://www.npmjs.com/package/ioredis) | Async | Shared cache with compression | +| `@node-ts-cache/lru-redis-storage` | LRU + Redis | Async | Two-tier: fast local + shared remote | +| `@node-ts-cache/elasticsearch-storage` | [elasticsearch](https://www.npmjs.com/package/@elastic/elasticsearch) | Async | Search-integrated caching | +| `@node-ts-cache/memcached-storage` | [memcached](https://www.npmjs.com/package/memcached) | Async | High-performance distributed cache | +| `@node-ts-cache/valkey-storage` | [iovalkey](https://www.npmjs.com/package/iovalkey) | Async | Redis-compatible, open source | ## Decorators