diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 9b7198c..644067b 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -116,6 +116,14 @@ jobs: if: steps.check.outputs.pr == 'true' run: npm run benchmark:replace:readme -- merged.benchmark + - name: Update website benchmark data + if: steps.check.outputs.pr == 'true' + run: npm run benchmark:replace:website -- merged.benchmark + + - name: Lint and format + if: steps.check.outputs.pr == 'true' + run: npm run lint + - name: Create pull request if: steps.check.outputs.pr == 'true' uses: peter-evans/create-pull-request@v8 @@ -131,4 +139,5 @@ jobs: - Ran **19** benchmark cases independently - Merged all results into a single report - Updated the benchmark section in all `README*.md` files (localized) + - Updated website benchmark data (`benchmarks`, `home`, i18n) labels: benchmark,automated diff --git a/.github/workflows/bundle.yml b/.github/workflows/bundle.yml index f973230..6ce9022 100644 --- a/.github/workflows/bundle.yml +++ b/.github/workflows/bundle.yml @@ -56,6 +56,10 @@ jobs: echo "pr=false" >> "$GITHUB_OUTPUT" fi + - name: Lint and format + if: steps.check.outputs.pr == 'true' + run: npm run lint + - name: Create pull request if: steps.check.outputs.pr == 'true' uses: peter-evans/create-pull-request@v8 diff --git a/README.ko.md b/README.ko.md index 5c63465..965352a 100644 --- a/README.ko.md +++ b/README.ko.md @@ -1,8 +1,10 @@

Solidis

-

- Zero-dependency RESP client for Redis. Fastest by design. -

+

+ The fastest Redis client for Node.js.
Zero dependencies, 2x+ faster than ioredis, battle-tested in production.
+

+ +

npm @@ -118,43 +120,45 @@ await client.subscribe('events'); # High Voltage Solidis vs ioredis High Voltage 측정일 2026-06-20 10:28:46 · linux x64 · Node.js v22.22.3 + ### ioredis 대비 최대 **2.1x 빠릅니다**! Rocket --- +
**15**개 중 **15**개 벤치마크 우위 · 평균 **72%** 성능 향상 · 최대 **109%** 성능 향상 -*100,000번 반복 × 10,000 동시 실행 · 1 KB 페이로드 · 10회 측정* - -| | 벤치마크 | 명령어 | solidis | ioredis | 차이 | 성능 | -|---:|:---|:---:|:---:|:---:|:---:|:---| -| 1st Place Medal | **Set 변경** | SADD SISMEMBER SREM | **1600ms** | 3336ms | **2.1x** FireFire | `██████████` | -| 2nd Place Medal | **List 범위** | LPUSH RPUSH LRANGE | **1914ms** | 3635ms | **1.9x** FireFire | `████████░░` | -| 3rd Place Medal | **Multi-Key** | MSET MGET | **1646ms** | 3124ms | **1.9x** FireFire | `████████░░` | -| 4. | **List 변경** | LPUSH RPUSH LPOP RPOP LLEN | **2419ms** | 4482ms | **1.9x** FireFire | `████████░░` | -| 5. | **Set** | SET | **715ms** | 1285ms | **1.8x** FireFire | `███████░░░` | -| 6. | **Expire** | SET EXPIRE TTL | **1422ms** | 2521ms | **1.8x** FireFire | `███████░░░` | -| 7. | **Hash 변경** | HMSET HMGET HDEL | **1971ms** | 3487ms | **1.8x** FireFire | `███████░░░` | -| 8. | **Sorted Set** | ZADD ZRANGE ZREM | **1881ms** | 3286ms | **1.7x** FireFire | `███████░░░` | -| 9. | **비트랜잭션** | SETPX GET | **1263ms** | 2102ms | **1.7x** FireFire | `██████░░░░` | -| 10. | **Stream** | XADD XRANGE XLEN | **1984ms** | 3266ms | **1.6x** FireFire | `██████░░░░` | -| 11. | **Set 조회** | SADD SISMEMBER SMEMBERS | **1940ms** | 3120ms | **1.6x** FireFire | `██████░░░░` | -| 12. | **파이프라인 혼합** | SET INCR GET | **1787ms** | 2839ms | **1.6x** Fire | `█████░░░░░` | -| 13. | **Counter** | INCR DECR | **952ms** | 1446ms | **1.5x** Fire | `█████░░░░░` | -| 14. | **Hash 왕복** | HSET HGET HGETALL | **1859ms** | 2798ms | **1.5x** Fire | `█████░░░░░` | -| 15. | **Get Buffer** | GETBUFFER | **628ms** | 928ms | **1.5x** Fire | `████░░░░░░` | +_100,000번 반복 × 10,000 동시 실행 · 1 KB 페이로드 · 10회 측정_ + +| | 벤치마크 | 명령어 | solidis | ioredis | 차이 | 성능 | +| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------ | :-----------------------------------------------------------------------------------------------------: | :--------: | :-----: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------- | +| 1st Place Medal | **Set 변경** | SADD SISMEMBER SREM | **1600ms** | 3336ms | **2.1x** FireFire | `██████████` | +| 2nd Place Medal | **List 범위** | LPUSH RPUSH LRANGE | **1914ms** | 3635ms | **1.9x** FireFire | `████████░░` | +| 3rd Place Medal | **Multi-Key** | MSET MGET | **1646ms** | 3124ms | **1.9x** FireFire | `████████░░` | +| 4. | **List 변경** | LPUSH RPUSH LPOP RPOP LLEN | **2419ms** | 4482ms | **1.9x** FireFire | `████████░░` | +| 5. | **Set** | SET | **715ms** | 1285ms | **1.8x** FireFire | `███████░░░` | +| 6. | **Expire** | SET EXPIRE TTL | **1422ms** | 2521ms | **1.8x** FireFire | `███████░░░` | +| 7. | **Hash 변경** | HMSET HMGET HDEL | **1971ms** | 3487ms | **1.8x** FireFire | `███████░░░` | +| 8. | **Sorted Set** | ZADD ZRANGE ZREM | **1881ms** | 3286ms | **1.7x** FireFire | `███████░░░` | +| 9. | **비트랜잭션** | SETPX GET | **1263ms** | 2102ms | **1.7x** FireFire | `██████░░░░` | +| 10. | **Stream** | XADD XRANGE XLEN | **1984ms** | 3266ms | **1.6x** FireFire | `██████░░░░` | +| 11. | **Set 조회** | SADD SISMEMBER SMEMBERS | **1940ms** | 3120ms | **1.6x** FireFire | `██████░░░░` | +| 12. | **파이프라인 혼합** | SET INCR GET | **1787ms** | 2839ms | **1.6x** Fire | `█████░░░░░` | +| 13. | **Counter** | INCR DECR | **952ms** | 1446ms | **1.5x** Fire | `█████░░░░░` | +| 14. | **Hash 왕복** | HSET HGET HGETALL | **1859ms** | 2798ms | **1.5x** Fire | `█████░░░░░` | +| 15. | **Get Buffer** | GETBUFFER | **628ms** | 928ms | **1.5x** Fire | `████░░░░░░` | ### 엄격 비교가 불가능한 벤치마크 라이브러리별 고유 동작으로 인해 엄밀한 비교가 어려운 벤치마크입니다. -| | 벤치마크 | 명령어 | solidis | ioredis | 차이 | 성능 | -|---:|:---|:---:|:---:|:---:|:---:|:---| -| 16. | **트랜잭션** | SET EXPIRE GET | 1261ms | 6006ms | **4.8x** FireFire | `██████████` | -| 17. | **트랜잭션 혼합** | SET GET | 1621ms | 6977ms | **4.3x** FireFire | `██████████` | -| 18. | **Pub/Sub** | PUBLISH MESSAGE | 775ms | 2569ms | **3.3x** FireFire | `██████████` | -| 19. | **Info / Config** | INFO CONFIGGET | 1119ms | 1745ms | **1.6x** Fire | `█████░░░░░` | +| | 벤치마크 | 명령어 | solidis | ioredis | 차이 | 성능 | +| --: | :---------------- | :-------------------------------------------------------------------: | :-----: | :-----: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------- | +| 16. | **트랜잭션** | SET EXPIRE GET | 1261ms | 6006ms | **4.8x** FireFire | `██████████` | +| 17. | **트랜잭션 혼합** | SET GET | 1621ms | 6977ms | **4.3x** FireFire | `██████████` | +| 18. | **Pub/Sub** | PUBLISH MESSAGE | 775ms | 2569ms | **3.3x** FireFire | `██████████` | +| 19. | **Info / Config** | INFO CONFIGGET | 1119ms | 1745ms | **1.6x** Fire | `█████░░░░░` | `solidis`의 `ioredis` (기준) 대비 성능 향상률 순으로 정렬. 소요 시간 = 반복 측정의 중앙값. @@ -169,38 +173,38 @@ await client.subscribe('events');

상세 지표 테이블 펼치기 -| 벤치마크 | 라이브러리 | ops/s | cmds/s | 소요 시간 | 분산 | -|:---|:---|---:|---:|---:|---:| -| **Set 변경: SADD SISMEMBER SREM**
1 KB | **solidis** | 62.5K | 187.5K | 1600ms | ±6.0% | -| | ioredis | 30.0K | 89.9K | 3336ms | ±1.1% | -| **List 범위: LPUSH RPUSH LRANGE**
1 KB | **solidis** | 52.3K | 156.8K | 1914ms | ±2.6% | -| | ioredis | 27.5K | 82.5K | 3635ms | ±1.2% | -| **Multi-Key: MSET MGET**
1 KB | **solidis** | 60.7K | 121.5K | 1646ms | ±1.4% | -| | ioredis | 32.0K | 64.0K | 3124ms | ±4.0% | -| **List 변경: LPUSH RPUSH LPOP RPOP LLEN**
1 KB | **solidis** | 41.3K | 206.7K | 2419ms | ±8.0% | -| | ioredis | 22.3K | 111.6K | 4482ms | ±0.9% | -| **Set: SET**
1 KB | **solidis** | 139.8K | 139.8K | 715ms | ±3.4% | -| | ioredis | 77.8K | 77.8K | 1285ms | ±2.3% | -| **Expire: SET EXPIRE TTL**
1 KB | **solidis** | 70.3K | 211.0K | 1422ms | ±2.4% | -| | ioredis | 39.7K | 119.0K | 2521ms | ±2.2% | -| **Hash 변경: HMSET HMGET HDEL**
1 KB | **solidis** | 50.7K | 152.2K | 1971ms | ±7.5% | -| | ioredis | 28.7K | 86.0K | 3487ms | ±1.2% | -| **Sorted Set: ZADD ZRANGE ZREM**
1 KB | **solidis** | 53.2K | 159.5K | 1881ms | ±4.0% | -| | ioredis | 30.4K | 91.3K | 3286ms | ±2.2% | -| **비트랜잭션: SETPX GET**
1 KB | **solidis** | 79.2K | 158.4K | 1263ms | ±5.1% | -| | ioredis | 47.6K | 95.1K | 2102ms | ±1.7% | -| **Stream: XADD XRANGE XLEN**
1 KB | **solidis** | 50.4K | 151.2K | 1984ms | ±6.8% | -| | ioredis | 30.6K | 91.8K | 3266ms | ±3.3% | -| **Set 조회: SADD SISMEMBER SMEMBERS**
1 KB | **solidis** | 51.6K | 154.7K | 1940ms | ±2.8% | -| | ioredis | 32.1K | 96.2K | 3120ms | ±2.0% | -| **파이프라인 혼합: SET INCR GET**
1 KB | **solidis** | 56.0K | 167.9K | 1787ms | ±2.2% | -| | ioredis | 35.2K | 105.7K | 2839ms | ±1.6% | -| **Counter: INCR DECR**
1 KB | **solidis** | 105.0K | 210.0K | 952ms | ±7.2% | -| | ioredis | 69.2K | 138.3K | 1446ms | ±1.3% | -| **Hash 왕복: HSET HGET HGETALL**
1 KB | **solidis** | 53.8K | 161.4K | 1859ms | ±7.5% | -| | ioredis | 35.7K | 107.2K | 2798ms | ±2.5% | -| **Get Buffer: GETBUFFER**
1 KB | **solidis** | 159.3K | 159.3K | 628ms | ±7.9% | -| | ioredis | 107.7K | 107.7K | 928ms | ±4.3% | +| 벤치마크 | 라이브러리 | ops/s | cmds/s | 소요 시간 | 분산 | +| :----------------------------------------------------------------------------------------------------------------------------------------- | :---------- | -----: | -----: | --------: | ----: | +| **Set 변경: SADD SISMEMBER SREM**
1 KB | **solidis** | 62.5K | 187.5K | 1600ms | ±6.0% | +| | ioredis | 30.0K | 89.9K | 3336ms | ±1.1% | +| **List 범위: LPUSH RPUSH LRANGE**
1 KB | **solidis** | 52.3K | 156.8K | 1914ms | ±2.6% | +| | ioredis | 27.5K | 82.5K | 3635ms | ±1.2% | +| **Multi-Key: MSET MGET**
1 KB | **solidis** | 60.7K | 121.5K | 1646ms | ±1.4% | +| | ioredis | 32.0K | 64.0K | 3124ms | ±4.0% | +| **List 변경: LPUSH RPUSH LPOP RPOP LLEN**
1 KB | **solidis** | 41.3K | 206.7K | 2419ms | ±8.0% | +| | ioredis | 22.3K | 111.6K | 4482ms | ±0.9% | +| **Set: SET**
1 KB | **solidis** | 139.8K | 139.8K | 715ms | ±3.4% | +| | ioredis | 77.8K | 77.8K | 1285ms | ±2.3% | +| **Expire: SET EXPIRE TTL**
1 KB | **solidis** | 70.3K | 211.0K | 1422ms | ±2.4% | +| | ioredis | 39.7K | 119.0K | 2521ms | ±2.2% | +| **Hash 변경: HMSET HMGET HDEL**
1 KB | **solidis** | 50.7K | 152.2K | 1971ms | ±7.5% | +| | ioredis | 28.7K | 86.0K | 3487ms | ±1.2% | +| **Sorted Set: ZADD ZRANGE ZREM**
1 KB | **solidis** | 53.2K | 159.5K | 1881ms | ±4.0% | +| | ioredis | 30.4K | 91.3K | 3286ms | ±2.2% | +| **비트랜잭션: SETPX GET**
1 KB | **solidis** | 79.2K | 158.4K | 1263ms | ±5.1% | +| | ioredis | 47.6K | 95.1K | 2102ms | ±1.7% | +| **Stream: XADD XRANGE XLEN**
1 KB | **solidis** | 50.4K | 151.2K | 1984ms | ±6.8% | +| | ioredis | 30.6K | 91.8K | 3266ms | ±3.3% | +| **Set 조회: SADD SISMEMBER SMEMBERS**
1 KB | **solidis** | 51.6K | 154.7K | 1940ms | ±2.8% | +| | ioredis | 32.1K | 96.2K | 3120ms | ±2.0% | +| **파이프라인 혼합: SET INCR GET**
1 KB | **solidis** | 56.0K | 167.9K | 1787ms | ±2.2% | +| | ioredis | 35.2K | 105.7K | 2839ms | ±1.6% | +| **Counter: INCR DECR**
1 KB | **solidis** | 105.0K | 210.0K | 952ms | ±7.2% | +| | ioredis | 69.2K | 138.3K | 1446ms | ±1.3% | +| **Hash 왕복: HSET HGET HGETALL**
1 KB | **solidis** | 53.8K | 161.4K | 1859ms | ±7.5% | +| | ioredis | 35.7K | 107.2K | 2798ms | ±2.5% | +| **Get Buffer: GETBUFFER**
1 KB | **solidis** | 159.3K | 159.3K | 628ms | ±7.9% | +| | ioredis | 107.7K | 107.7K | 928ms | ±4.3% |
@@ -211,20 +215,20 @@ await client.subscribe('events');
벤치마크 설정 펼치기 -| 항목 | 값 | -|:----------|:------| -| 모드 | `autopipeline` | -| 페이로드 크기 | 1 KB | -| 반복 횟수 | 100,000 | -| 워밍업 | 1,000 | -| 클라이언트 수 | 1 | -| 클라이언트당 동시 실행 | 10000 | -| 총 동시 실행 | 10000 | -| 측정 횟수 | 10 | -| 쿨다운 | 2500ms | -| 플랫폼 | linux x64 | -| Node.js | v22.22.3 | -| 날짜 | 2026-06-20 10:28:46 | +| 항목 | 값 | +| :--------------------- | :------------------ | +| 모드 | `autopipeline` | +| 페이로드 크기 | 1 KB | +| 반복 횟수 | 100,000 | +| 워밍업 | 1,000 | +| 클라이언트 수 | 1 | +| 클라이언트당 동시 실행 | 10000 | +| 총 동시 실행 | 10000 | +| 측정 횟수 | 10 | +| 쿨다운 | 2500ms | +| 플랫폼 | linux x64 | +| Node.js | v22.22.3 | +| 날짜 | 2026-06-20 10:28:46 |
@@ -415,13 +419,13 @@ sequenceDiagram Client-->>App: 'OK' ``` -| 모듈 | 역할 | -|:-------|:---------------| -| **Connection** | TCP/TLS 소켓 관리, 재연결 백오프 | -| **Requester** | 커맨드 큐, 파이프라인 청킹, 응답 매칭, 타임아웃 | -| **Parser** | RESP 디코딩, 버퍼 관리, zero-copy 슬라이싱 | -| **PubSub** | 채널/패턴/샤드 상태 추적, 메시지 디스패치 | -| **Debug Memory** | 링 버퍼 기반 디버그 로그, credential 마스킹 | +| 모듈 | 역할 | +| :--------------- | :---------------------------------------------- | +| **Connection** | TCP/TLS 소켓 관리, 재연결 백오프 | +| **Requester** | 커맨드 큐, 파이프라인 청킹, 응답 매칭, 타임아웃 | +| **Parser** | RESP 디코딩, 버퍼 관리, zero-copy 슬라이싱 | +| **PubSub** | 채널/패턴/샤드 상태 추적, 메시지 디스패치 | +| **Debug Memory** | 링 버퍼 기반 디버그 로그, credential 마스킹 | ## 이벤트 @@ -453,12 +457,12 @@ try { > Solidis가 throw하는 모든 에러는 `SolidisError`를 상속합니다. > `unwrapSolidisError()`로 root cause까지 cause chain을 추적할 수 있습니다. -| 에러 클래스 | 발생 조건 | -|:------------|:-----| -| `SolidisConnectionError` | TCP/TLS 연결 실패, 타임아웃, 소켓 리셋 | -| `SolidisRequesterError` | 커맨드 타임아웃, 파이프라인 reject, 쓰기 실패 | -| `SolidisParserError` | 잘못된 RESP 포맷, bulk string 크기 초과 | -| `SolidisPubSubError` | 구독 상태 관련 에러 | +| 에러 클래스 | 발생 조건 | +| :----------------------- | :-------------------------------------------- | +| `SolidisConnectionError` | TCP/TLS 연결 실패, 타임아웃, 소켓 리셋 | +| `SolidisRequesterError` | 커맨드 타임아웃, 파이프라인 reject, 쓰기 실패 | +| `SolidisParserError` | 잘못된 RESP 포맷, bulk string 크기 초과 | +| `SolidisPubSubError` | 구독 상태 관련 에러 | ## 확장 @@ -466,10 +470,10 @@ try { npm install @vcms-io/solidis-extensions ``` -| 확장 | 설명 | -|:----------|:------------| +| 확장 | 설명 | +| :--------------------------------------------------------------------------------------------------------- | :--------------------------------------- | | [**SpinLock**](https://github.com/vcms-io/solidis-extensions/blob/main/sources/domains/spinlock/README.md) | 가벼운 Redis 기반 뮤텍스 (단일 인스턴스) | -| [**RedLock**](https://github.com/vcms-io/solidis-extensions/blob/main/sources/domains/redlock/README.md) | 분산 잠금 (Redlock 알고리즘, 내결함성) | +| [**RedLock**](https://github.com/vcms-io/solidis-extensions/blob/main/sources/domains/redlock/README.md) | 분산 잠금 (Redlock 알고리즘, 내결함성) | ## 기여하기 diff --git a/README.md b/README.md index f80617e..1acf71d 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@

Solidis

-

- Zero-dependency RESP client for Redis. Fastest by design. -

+

+ The fastest Redis client for Node.js.
Zero dependencies, 2x+ faster than ioredis, battle-tested in production.
+

+ +

npm @@ -118,43 +120,45 @@ await client.subscribe('events'); # High Voltage Solidis vs ioredis High Voltage Generated on 2026-06-20 10:28:46 · linux x64 · Node.js v22.22.3 + ### Up to **2.1x faster** than ioredis! Rocket --- +
**15** / **15** benchmarks won · **72%** average speed improvement · **109%** peak speed improvement -*100,000 iterations × 10,000 concurrency · 1 KB payload · 10 repeats* - -| | Benchmark | Commands | solidis | ioredis | Difference | Performance | -|---:|:---|:---:|:---:|:---:|:---:|:---| -| 1st Place Medal | **Set Mutation** | SADD SISMEMBER SREM | **1600ms** | 3336ms | **2.1x** FireFire | `██████████` | -| 2nd Place Medal | **List Range** | LPUSH RPUSH LRANGE | **1914ms** | 3635ms | **1.9x** FireFire | `████████░░` | -| 3rd Place Medal | **Multi-Key** | MSET MGET | **1646ms** | 3124ms | **1.9x** FireFire | `████████░░` | -| 4. | **List Mutation** | LPUSH RPUSH LPOP RPOP LLEN | **2419ms** | 4482ms | **1.9x** FireFire | `████████░░` | -| 5. | **Set** | SET | **715ms** | 1285ms | **1.8x** FireFire | `███████░░░` | -| 6. | **Expire** | SET EXPIRE TTL | **1422ms** | 2521ms | **1.8x** FireFire | `███████░░░` | -| 7. | **Hash Mutation** | HMSET HMGET HDEL | **1971ms** | 3487ms | **1.8x** FireFire | `███████░░░` | -| 8. | **Sorted Set** | ZADD ZRANGE ZREM | **1881ms** | 3286ms | **1.7x** FireFire | `███████░░░` | -| 9. | **Non-Transaction** | SETPX GET | **1263ms** | 2102ms | **1.7x** FireFire | `██████░░░░` | -| 10. | **Stream** | XADD XRANGE XLEN | **1984ms** | 3266ms | **1.6x** FireFire | `██████░░░░` | -| 11. | **Set Read** | SADD SISMEMBER SMEMBERS | **1940ms** | 3120ms | **1.6x** FireFire | `██████░░░░` | -| 12. | **Pipeline Mixed** | SET INCR GET | **1787ms** | 2839ms | **1.6x** Fire | `█████░░░░░` | -| 13. | **Counter** | INCR DECR | **952ms** | 1446ms | **1.5x** Fire | `█████░░░░░` | -| 14. | **Hash Round-Trip** | HSET HGET HGETALL | **1859ms** | 2798ms | **1.5x** Fire | `█████░░░░░` | -| 15. | **Get Buffer** | GETBUFFER | **628ms** | 928ms | **1.5x** Fire | `████░░░░░░` | +_100,000 iterations × 10,000 concurrency · 1 KB payload · 10 repeats_ + +| | Benchmark | Commands | solidis | ioredis | Difference | Performance | +| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------ | :-----------------------------------------------------------------------------------------------------: | :--------: | :-----: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------- | +| 1st Place Medal | **Set Mutation** | SADD SISMEMBER SREM | **1600ms** | 3336ms | **2.1x** FireFire | `██████████` | +| 2nd Place Medal | **List Range** | LPUSH RPUSH LRANGE | **1914ms** | 3635ms | **1.9x** FireFire | `████████░░` | +| 3rd Place Medal | **Multi-Key** | MSET MGET | **1646ms** | 3124ms | **1.9x** FireFire | `████████░░` | +| 4. | **List Mutation** | LPUSH RPUSH LPOP RPOP LLEN | **2419ms** | 4482ms | **1.9x** FireFire | `████████░░` | +| 5. | **Set** | SET | **715ms** | 1285ms | **1.8x** FireFire | `███████░░░` | +| 6. | **Expire** | SET EXPIRE TTL | **1422ms** | 2521ms | **1.8x** FireFire | `███████░░░` | +| 7. | **Hash Mutation** | HMSET HMGET HDEL | **1971ms** | 3487ms | **1.8x** FireFire | `███████░░░` | +| 8. | **Sorted Set** | ZADD ZRANGE ZREM | **1881ms** | 3286ms | **1.7x** FireFire | `███████░░░` | +| 9. | **Non-Transaction** | SETPX GET | **1263ms** | 2102ms | **1.7x** FireFire | `██████░░░░` | +| 10. | **Stream** | XADD XRANGE XLEN | **1984ms** | 3266ms | **1.6x** FireFire | `██████░░░░` | +| 11. | **Set Read** | SADD SISMEMBER SMEMBERS | **1940ms** | 3120ms | **1.6x** FireFire | `██████░░░░` | +| 12. | **Pipeline Mixed** | SET INCR GET | **1787ms** | 2839ms | **1.6x** Fire | `█████░░░░░` | +| 13. | **Counter** | INCR DECR | **952ms** | 1446ms | **1.5x** Fire | `█████░░░░░` | +| 14. | **Hash Round-Trip** | HSET HGET HGETALL | **1859ms** | 2798ms | **1.5x** Fire | `█████░░░░░` | +| 15. | **Get Buffer** | GETBUFFER | **628ms** | 928ms | **1.5x** Fire | `████░░░░░░` | ### Non Strictly Comparable Benchmarks These benchmarks have library-specific behavior that prevents a strictly fair comparison. -| | Benchmark | Commands | solidis | ioredis | Difference | Performance | -|---:|:---|:---:|:---:|:---:|:---:|:---| -| 16. | **Transaction** | SET EXPIRE GET | 1261ms | 6006ms | **4.8x** FireFire | `██████████` | -| 17. | **Transaction Mixed** | SET GET | 1621ms | 6977ms | **4.3x** FireFire | `██████████` | -| 18. | **Pub/Sub** | PUBLISH MESSAGE | 775ms | 2569ms | **3.3x** FireFire | `██████████` | -| 19. | **Info / Config** | INFO CONFIGGET | 1119ms | 1745ms | **1.6x** Fire | `█████░░░░░` | +| | Benchmark | Commands | solidis | ioredis | Difference | Performance | +| --: | :-------------------- | :-------------------------------------------------------------------: | :-----: | :-----: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------- | +| 16. | **Transaction** | SET EXPIRE GET | 1261ms | 6006ms | **4.8x** FireFire | `██████████` | +| 17. | **Transaction Mixed** | SET GET | 1621ms | 6977ms | **4.3x** FireFire | `██████████` | +| 18. | **Pub/Sub** | PUBLISH MESSAGE | 775ms | 2569ms | **3.3x** FireFire | `██████████` | +| 19. | **Info / Config** | INFO CONFIGGET | 1119ms | 1745ms | **1.6x** Fire | `█████░░░░░` | Ranked by performance gain of `solidis` over `ioredis` (baseline). Elapsed = median time across repeats. @@ -169,38 +173,38 @@ await client.subscribe('events');

Click to expand detailed metrics table -| Benchmark | Library | ops/s | cmds/s | Elapsed | Spread | -|:---|:---|---:|---:|---:|---:| -| **Set Mutation: SADD SISMEMBER SREM**
1 KB | **solidis** | 62.5K | 187.5K | 1600ms | ±6.0% | -| | ioredis | 30.0K | 89.9K | 3336ms | ±1.1% | -| **List Range: LPUSH RPUSH LRANGE**
1 KB | **solidis** | 52.3K | 156.8K | 1914ms | ±2.6% | -| | ioredis | 27.5K | 82.5K | 3635ms | ±1.2% | -| **Multi-Key: MSET MGET**
1 KB | **solidis** | 60.7K | 121.5K | 1646ms | ±1.4% | -| | ioredis | 32.0K | 64.0K | 3124ms | ±4.0% | -| **List Mutation: LPUSH RPUSH LPOP RPOP LLEN**
1 KB | **solidis** | 41.3K | 206.7K | 2419ms | ±8.0% | -| | ioredis | 22.3K | 111.6K | 4482ms | ±0.9% | -| **Set: SET**
1 KB | **solidis** | 139.8K | 139.8K | 715ms | ±3.4% | -| | ioredis | 77.8K | 77.8K | 1285ms | ±2.3% | -| **Expire: SET EXPIRE TTL**
1 KB | **solidis** | 70.3K | 211.0K | 1422ms | ±2.4% | -| | ioredis | 39.7K | 119.0K | 2521ms | ±2.2% | -| **Hash Mutation: HMSET HMGET HDEL**
1 KB | **solidis** | 50.7K | 152.2K | 1971ms | ±7.5% | -| | ioredis | 28.7K | 86.0K | 3487ms | ±1.2% | -| **Sorted Set: ZADD ZRANGE ZREM**
1 KB | **solidis** | 53.2K | 159.5K | 1881ms | ±4.0% | -| | ioredis | 30.4K | 91.3K | 3286ms | ±2.2% | -| **Non-Transaction: SETPX GET**
1 KB | **solidis** | 79.2K | 158.4K | 1263ms | ±5.1% | -| | ioredis | 47.6K | 95.1K | 2102ms | ±1.7% | -| **Stream: XADD XRANGE XLEN**
1 KB | **solidis** | 50.4K | 151.2K | 1984ms | ±6.8% | -| | ioredis | 30.6K | 91.8K | 3266ms | ±3.3% | -| **Set Read: SADD SISMEMBER SMEMBERS**
1 KB | **solidis** | 51.6K | 154.7K | 1940ms | ±2.8% | -| | ioredis | 32.1K | 96.2K | 3120ms | ±2.0% | -| **Pipeline Mixed: SET INCR GET**
1 KB | **solidis** | 56.0K | 167.9K | 1787ms | ±2.2% | -| | ioredis | 35.2K | 105.7K | 2839ms | ±1.6% | -| **Counter: INCR DECR**
1 KB | **solidis** | 105.0K | 210.0K | 952ms | ±7.2% | -| | ioredis | 69.2K | 138.3K | 1446ms | ±1.3% | -| **Hash Round-Trip: HSET HGET HGETALL**
1 KB | **solidis** | 53.8K | 161.4K | 1859ms | ±7.5% | -| | ioredis | 35.7K | 107.2K | 2798ms | ±2.5% | -| **Get Buffer: GETBUFFER**
1 KB | **solidis** | 159.3K | 159.3K | 628ms | ±7.9% | -| | ioredis | 107.7K | 107.7K | 928ms | ±4.3% | +| Benchmark | Library | ops/s | cmds/s | Elapsed | Spread | +| :--------------------------------------------------------------------------------------------------------------------------------------------- | :---------- | -----: | -----: | ------: | -----: | +| **Set Mutation: SADD SISMEMBER SREM**
1 KB | **solidis** | 62.5K | 187.5K | 1600ms | ±6.0% | +| | ioredis | 30.0K | 89.9K | 3336ms | ±1.1% | +| **List Range: LPUSH RPUSH LRANGE**
1 KB | **solidis** | 52.3K | 156.8K | 1914ms | ±2.6% | +| | ioredis | 27.5K | 82.5K | 3635ms | ±1.2% | +| **Multi-Key: MSET MGET**
1 KB | **solidis** | 60.7K | 121.5K | 1646ms | ±1.4% | +| | ioredis | 32.0K | 64.0K | 3124ms | ±4.0% | +| **List Mutation: LPUSH RPUSH LPOP RPOP LLEN**
1 KB | **solidis** | 41.3K | 206.7K | 2419ms | ±8.0% | +| | ioredis | 22.3K | 111.6K | 4482ms | ±0.9% | +| **Set: SET**
1 KB | **solidis** | 139.8K | 139.8K | 715ms | ±3.4% | +| | ioredis | 77.8K | 77.8K | 1285ms | ±2.3% | +| **Expire: SET EXPIRE TTL**
1 KB | **solidis** | 70.3K | 211.0K | 1422ms | ±2.4% | +| | ioredis | 39.7K | 119.0K | 2521ms | ±2.2% | +| **Hash Mutation: HMSET HMGET HDEL**
1 KB | **solidis** | 50.7K | 152.2K | 1971ms | ±7.5% | +| | ioredis | 28.7K | 86.0K | 3487ms | ±1.2% | +| **Sorted Set: ZADD ZRANGE ZREM**
1 KB | **solidis** | 53.2K | 159.5K | 1881ms | ±4.0% | +| | ioredis | 30.4K | 91.3K | 3286ms | ±2.2% | +| **Non-Transaction: SETPX GET**
1 KB | **solidis** | 79.2K | 158.4K | 1263ms | ±5.1% | +| | ioredis | 47.6K | 95.1K | 2102ms | ±1.7% | +| **Stream: XADD XRANGE XLEN**
1 KB | **solidis** | 50.4K | 151.2K | 1984ms | ±6.8% | +| | ioredis | 30.6K | 91.8K | 3266ms | ±3.3% | +| **Set Read: SADD SISMEMBER SMEMBERS**
1 KB | **solidis** | 51.6K | 154.7K | 1940ms | ±2.8% | +| | ioredis | 32.1K | 96.2K | 3120ms | ±2.0% | +| **Pipeline Mixed: SET INCR GET**
1 KB | **solidis** | 56.0K | 167.9K | 1787ms | ±2.2% | +| | ioredis | 35.2K | 105.7K | 2839ms | ±1.6% | +| **Counter: INCR DECR**
1 KB | **solidis** | 105.0K | 210.0K | 952ms | ±7.2% | +| | ioredis | 69.2K | 138.3K | 1446ms | ±1.3% | +| **Hash Round-Trip: HSET HGET HGETALL**
1 KB | **solidis** | 53.8K | 161.4K | 1859ms | ±7.5% | +| | ioredis | 35.7K | 107.2K | 2798ms | ±2.5% | +| **Get Buffer: GETBUFFER**
1 KB | **solidis** | 159.3K | 159.3K | 628ms | ±7.9% | +| | ioredis | 107.7K | 107.7K | 928ms | ±4.3% |
@@ -211,20 +215,20 @@ await client.subscribe('events');
Click to expand benchmark configuration -| Parameter | Value | -|:----------|:------| -| Mode | `autopipeline` | -| Payload Sizes | 1 KB | -| Iterations | 100,000 | -| Warmup | 1,000 | -| Clients | 1 | -| Concurrency / Client | 10000 | -| Total Concurrency | 10000 | -| Repeats | 10 | -| Cooldown | 2500ms | -| Platform | linux x64 | -| Node.js | v22.22.3 | -| Date | 2026-06-20 10:28:46 | +| Parameter | Value | +| :------------------- | :------------------ | +| Mode | `autopipeline` | +| Payload Sizes | 1 KB | +| Iterations | 100,000 | +| Warmup | 1,000 | +| Clients | 1 | +| Concurrency / Client | 10000 | +| Total Concurrency | 10000 | +| Repeats | 10 | +| Cooldown | 2500ms | +| Platform | linux x64 | +| Node.js | v22.22.3 | +| Date | 2026-06-20 10:28:46 |
@@ -445,12 +449,12 @@ try { > Every error thrown by Solidis is an instance of `SolidisError`. > Use `unwrapSolidisError()` to traverse the full causal chain to the root cause. -| Error Class | When | -|:------------|:-----| -| `SolidisConnectionError` | TCP/TLS connect failure, timeout, reset | -| `SolidisRequesterError` | Command timeout, pipeline rejection, write failure | -| `SolidisParserError` | Malformed RESP, oversized bulk string | -| `SolidisPubSubError` | Subscription lifecycle error | +| Error Class | When | +| :----------------------- | :------------------------------------------------- | +| `SolidisConnectionError` | TCP/TLS connect failure, timeout, reset | +| `SolidisRequesterError` | Command timeout, pipeline rejection, write failure | +| `SolidisParserError` | Malformed RESP, oversized bulk string | +| `SolidisPubSubError` | Subscription lifecycle error | ## Extensions @@ -458,10 +462,10 @@ try { npm install @vcms-io/solidis-extensions ``` -| Extension | Description | -|:----------|:------------| -| [**SpinLock**](https://github.com/vcms-io/solidis-extensions/blob/main/sources/domains/spinlock/README.md) | Lightweight Redis-backed mutex (single instance) | -| [**RedLock**](https://github.com/vcms-io/solidis-extensions/blob/main/sources/domains/redlock/README.md) | Fault-tolerant distributed lock (Redlock algorithm) | +| Extension | Description | +| :--------------------------------------------------------------------------------------------------------- | :-------------------------------------------------- | +| [**SpinLock**](https://github.com/vcms-io/solidis-extensions/blob/main/sources/domains/spinlock/README.md) | Lightweight Redis-backed mutex (single instance) | +| [**RedLock**](https://github.com/vcms-io/solidis-extensions/blob/main/sources/domains/redlock/README.md) | Fault-tolerant distributed lock (Redlock algorithm) | ## Contributing diff --git a/package-lock.json b/package-lock.json index 9189fc0..4864485 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "cross-env": "^10.1.0", "esbuild": "^0.28.1", "ioredis": "^5.11.1", + "prettier": "^3.8.4", "redis": "^6.0.0", "typescript": "^6.0.3" } @@ -1798,6 +1799,22 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/prettier": { + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.4.tgz", + "integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/redis": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/redis/-/redis-6.0.0.tgz", diff --git a/package.json b/package.json index 5b68c4d..a5564f5 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@vcms-io/solidis", "version": "0.3.0", "author": "Jay Lee ", - "description": "High-performance, SOLID-structured RESP client for Redis and other RESP-compatible servers", + "description": "The fastest Redis client for Node.js. Zero dependencies, 2x+ faster than ioredis, battle-tested in production.", "repository": { "type": "git", "url": "https://github.com/vcms-io/solidis.git" @@ -13,8 +13,10 @@ "prepack": "npm run build", "cleanup": "npm run --silent node:ts ./scripts/cleanup.ts", "build": "npm run cleanup && tsc && npm run node:ts ./scripts/build.ts", - "lint": "concurrently --raw \"biome check --write\" \"tsc --noEmit\" \"tsc --noEmit --project scripts/tsconfig.json\"", - "format": "biome format --write", + "lint": "concurrently --raw \"npm run format\" \"npm run format:markdown\" \"npm run check\"", + "format": "biome check --write", + "format:markdown": "prettier --write \"**/*.md\" --prose-wrap preserve --single-quote --embedded-language-formatting off", + "check": "concurrently --raw \"tsc --noEmit\" \"tsc --noEmit --project scripts/tsconfig.json\"", "node:ts": "node --import=./scripts/esm/register.js", "benchmark:ioredis": "npm run --silent node:ts ./scripts/benchmarks/ioredis/benchmark.ts --", "benchmark:ioredis:snapshot": "cross-env SOLIDIS_BENCH_EXPORT_SNAPSHOT=./ioredis.benchmark npm run --silent node:ts ./scripts/benchmarks/ioredis/benchmark.ts --", @@ -22,6 +24,7 @@ "benchmark:redis:snapshot": "cross-env SOLIDIS_BENCH_EXPORT_SNAPSHOT=./redis.benchmark npm run --silent node:ts ./scripts/benchmarks/redis/benchmark.ts --", "benchmark:merge": "npm run --silent node:ts ./scripts/benchmarks/shared/markdown/merge.ts --", "benchmark:replace:readme": "npm run --silent node:ts ./scripts/benchmarks/shared/markdown/replace.readme.ts --", + "benchmark:replace:website": "npm run --silent node:ts ./scripts/benchmarks/shared/markdown/replace.website.ts --", "test": "concurrently --raw --max-processes 1 \"npm run test:commands\" \"npm run test:client\"", "test:commands": "node --import=./scripts/esm/register.js --test ./scripts/tests/commands/*.test.ts", "test:client": "node --import=./scripts/esm/register.js --test --test-concurrency=1 ./scripts/tests/client/**/*.test.ts", @@ -91,6 +94,7 @@ "cross-env": "^10.1.0", "esbuild": "^0.28.1", "ioredis": "^5.11.1", + "prettier": "^3.8.4", "redis": "^6.0.0", "typescript": "^6.0.3" } diff --git a/scripts/benchmarks/shared/markdown/replace.website.ts b/scripts/benchmarks/shared/markdown/replace.website.ts new file mode 100644 index 0000000..a14ef3c --- /dev/null +++ b/scripts/benchmarks/shared/markdown/replace.website.ts @@ -0,0 +1,301 @@ +import { readFile, writeFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; + +import { en } from './locales/index.ts'; +import { loadSnapshot, mergeSnapshots } from './snapshot.ts'; + +import type { ComparedResult, LibraryName } from '../types.ts'; + +interface WebsiteBenchmarkEntry { + name: string; + description: string; + solidisMs: number; + baselineMs: number; + ratio: string; +} + +function formatOperationName( + raw: string, + displayNames: Record, +): { display: string; commands: string } { + const colonIndex = raw.indexOf(':'); + const display = displayNames[raw] ?? raw; + + if (colonIndex === -1) { + return { + display, + commands: `${raw.toUpperCase()} (1 KB payload)`, + }; + } + + return { + display, + commands: raw + .slice(colonIndex + 1) + .split('+') + .join(' + '), + }; +} + +function buildWebsiteEntries( + results: ComparedResult[], + baselineLibrary: LibraryName, +): WebsiteBenchmarkEntry[] { + const grouped = new Map(); + + for (const result of results) { + const key = `${result.operation}\x00${result.payloadBytes}`; + const existing = grouped.get(key) ?? []; + existing.push(result); + grouped.set(key, existing); + } + + const entries: WebsiteBenchmarkEntry[] = []; + + for (const groupResults of grouped.values()) { + if (!groupResults.every((r) => r.comparable)) { + continue; + } + + const solidis = groupResults.find((r) => r.library !== baselineLibrary); + const baseline = groupResults.find((r) => r.library === baselineLibrary); + + if ( + !solidis?.elapsedMs || + !baseline?.elapsedMs || + solidis.ratioVsBaseline === null + ) { + continue; + } + + const { display, commands } = formatOperationName( + solidis.operation, + en.operationDisplayNames, + ); + + entries.push({ + name: display, + description: commands, + solidisMs: Math.round(solidis.elapsedMs), + baselineMs: Math.round(baseline.elapsedMs), + ratio: `${solidis.ratioVsBaseline.toFixed(1)}x`, + }); + } + + entries.sort((a, b) => { + const ratioA = Number.parseFloat(a.ratio); + const ratioB = Number.parseFloat(b.ratio); + return ratioB - ratioA; + }); + + return entries; +} + +function generateBenchmarksPageData(entries: WebsiteBenchmarkEntry[]): string { + const items = entries.map( + (e) => + ' {\n' + + ` name: '${e.name}',\n` + + ` description: '${e.description}',\n` + + ` solidis: '${e.solidisMs}ms',\n` + + ` ioredis: '${e.baselineMs}ms',\n` + + ` improvement: '${e.ratio}',\n` + + ` solidisNumber: ${e.solidisMs},\n` + + ` ioredisNumber: ${e.baselineMs},\n` + + ' }', + ); + + return ` const benchmarkData = [\n${items.join(',\n')},\n ];`; +} + +function generateHomePageData(entries: WebsiteBenchmarkEntry[]): string { + const items = entries.map( + (e) => + ' {\n' + + ` name: '${e.name}',\n` + + ` commands: '${e.description}',\n` + + ` solidis: ${e.solidisMs},\n` + + ` ioredis: ${e.baselineMs},\n` + + ` multiplier: '${e.ratio}',\n` + + ' }', + ); + + return `const BENCHMARK_DATA = [\n${items.join(',\n')},\n];`; +} + +const BENCHMARKS_DATA_PATTERN = / {2}const benchmarkData = \[[\s\S]*?\n {2}\];/; + +const HOME_DATA_PATTERN = /const BENCHMARK_DATA = \[[\s\S]*?\n\];/; + +const MAX_SPEED_BOOST_PATTERN = + /(
)[\d.]+x(<\/div>)/; + +const processArguments = process.argv.slice(2); + +if (processArguments.length < 1) { + console.error( + 'Usage: benchmark:replace:website [snapshot2.benchmark ...]', + ); + process.exit(1); +} + +const snapshotPaths = processArguments; +const snapshots = await Promise.all( + snapshotPaths.map((path) => loadSnapshot(path)), +); +const merged = mergeSnapshots(snapshots); +const entries = buildWebsiteEntries(merged.results, merged.baselineLibrary); + +if (entries.length === 0) { + console.error('No comparable benchmark entries found in snapshots.'); + process.exit(1); +} + +const peakRatio = entries[0].ratio; +const totalComparable = entries.length; + +const projectRoot = resolve('.'); +let updatedCount = 0; + +const benchmarksPagePath = resolve( + projectRoot, + 'website', + 'app', + 'benchmarks', + 'page.tsx', +); + +try { + let content = await readFile(benchmarksPagePath, 'utf-8'); + let changed = false; + + const newData = generateBenchmarksPageData(entries); + if (BENCHMARKS_DATA_PATTERN.test(content)) { + content = content.replace(BENCHMARKS_DATA_PATTERN, newData); + changed = true; + } + + const boostMatch = MAX_SPEED_BOOST_PATTERN.exec(content); + if (boostMatch) { + content = content.replace(MAX_SPEED_BOOST_PATTERN, `$1${peakRatio}$2`); + changed = true; + } + + if (changed) { + await writeFile(benchmarksPagePath, content, 'utf-8'); + updatedCount += 1; + console.log('Updated: website/app/benchmarks/page.tsx'); + } +} catch { + console.log('Skipped: website/app/benchmarks/page.tsx (not found)'); +} + +const homePagePath = resolve(projectRoot, 'website', 'app', 'page.tsx'); +const HOME_PAGE_TOP_COUNT = 6; + +try { + let content = await readFile(homePagePath, 'utf-8'); + let changed = false; + + const topEntries = entries.slice(0, HOME_PAGE_TOP_COUNT); + const newData = generateHomePageData(topEntries); + if (HOME_DATA_PATTERN.test(content)) { + content = content.replace(HOME_DATA_PATTERN, newData); + changed = true; + } + + if (changed) { + await writeFile(homePagePath, content, 'utf-8'); + updatedCount += 1; + console.log('Updated: website/app/page.tsx'); + } +} catch { + console.log('Skipped: website/app/page.tsx (not found)'); +} + +const i18nDir = resolve(projectRoot, 'website', 'i18n', 'messages'); + +for (const locale of ['en', 'ko']) { + const filePath = resolve(i18nDir, `${locale}.json`); + + try { + const raw = await readFile(filePath, 'utf-8'); + const json = JSON.parse(raw); + let changed = false; + + if (json.benchmarks?.benchmarkResultsDesc) { + const currentDesc: string = json.benchmarks.benchmarkResultsDesc; + const newDesc = currentDesc.replace( + /\d+\s*\/\s*\d+/, + `${totalComparable} / ${totalComparable}`, + ); + + if (newDesc !== currentDesc) { + json.benchmarks.benchmarkResultsDesc = newDesc; + changed = true; + } + } + + if (json.home?.twiceAsFastDesc) { + const currentDesc: string = json.home.twiceAsFastDesc; + const newDesc = currentDesc + .replace(/[\d.]+x faster/i, `${peakRatio} faster`) + .replace(/[\d.]+배 빠릅니다/i, `${peakRatio} 빠릅니다`) + .replace(/\d+개 벤치마크/i, `${totalComparable}개 벤치마크`) + .replace( + /\d+ benchmark categories/i, + `${totalComparable} benchmark categories`, + ); + + if (newDesc !== currentDesc) { + json.home.twiceAsFastDesc = newDesc; + changed = true; + } + } + + if (json.home?.benchmarkDesc) { + const currentDesc: string = json.home.benchmarkDesc; + const newDesc = currentDesc + .replace(/[\d.]+x faster/i, `${peakRatio} faster`) + .replace(/[\d.]+배 빠릅니다/i, `${peakRatio} 빠릅니다`) + .replace( + /\d+ benchmark categories/i, + `${totalComparable} benchmark categories`, + ) + .replace(/\d+개 벤치마크/i, `${totalComparable}개 벤치마크`); + + if (newDesc !== currentDesc) { + json.home.benchmarkDesc = newDesc; + changed = true; + } + } + + if (json.faq?.a5) { + const currentA5: string = json.faq.a5; + const peakFloor = `${Math.floor(Number.parseFloat(peakRatio))}`; + const newA5 = currentA5 + .replace(/\d+x faster/i, `${peakFloor}x faster`) + .replace( + /\d+ different operation types/i, + `${totalComparable} different operation types`, + ) + .replace(/[\d.]+배 빠릅니다/i, `${peakFloor}배 빠릅니다`) + .replace(/\d+개 카테고리/i, `${totalComparable}개 카테고리`); + + if (newA5 !== currentA5) { + json.faq.a5 = newA5; + changed = true; + } + } + + if (changed) { + await writeFile(filePath, `${JSON.stringify(json, null, 2)}\n`, 'utf-8'); + updatedCount += 1; + console.log(`Updated: website/i18n/messages/${locale}.json`); + } + } catch { + console.log(`Skipped: website/i18n/messages/${locale}.json (not found)`); + } +} + +console.log(`\nDone: ${updatedCount} file(s) updated.`);