@fluojs/redis 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ko.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  <p><a href="./README.md"><kbd>English</kbd></a> <strong><kbd>한국어</kbd></strong></p>
4
4
 
5
+ Node.js 지원 범위는 `>=24.0.0 <27`입니다. 업그레이드 절차는 [Node.js 지원 및 마이그레이션](../../docs/reference/node-support.ko.md)을 참조하세요.
6
+
5
7
  fluo를 위한 공유 Redis 연결 계층입니다. 기본 app-scoped `ioredis` client와 선택적인 named client를 제공하며, 모두 애플리케이션 lifecycle로 관리됩니다.
6
8
 
7
9
  ## 목차
@@ -77,7 +79,7 @@ export class CacheRepository {
77
79
 
78
80
  ### 수명 주기 소유권
79
81
 
80
- `RedisModule.forRoot(...)` 등록은 각각 새 client를 생성하며, `@fluojs/redis`는 `RedisModule.forRoot({ name, ... })`로 등록한 이름 있는 연결을 포함해 그 client의 lifecycle을 직접 관리합니다. 이 module은 기존 client instance를 채택하지 않습니다.
82
+ `RedisModule.forRoot(...)` 등록은 각각 새 client를 생성하며, `@fluojs/redis`는 `RedisModule.forRoot({ name, ... })`로 등록한 이름 있는 연결을 포함해 그 client의 lifecycle을 직접 관리합니다. 이 module은 기존 client instance를 채택하지 않습니다. 등록 identity는 애플리케이션 전체에서 유일해야 합니다. 이름 없는 기본 등록은 하나만 허용하고, trim한 각 name도 하나만 허용하며, bootstrap은 Redis client를 만들기 전에 중복 identity를 거부합니다.
81
83
 
82
84
  - 호출자가 옵션을 강제로 캐스팅하더라도 Fluo는 항상 `lazyConnect: true`를 강제하므로, 소켓은 import 시점이 아니라 애플리케이션 bootstrap 중에 열립니다.
83
85
  - bootstrap 단계에서는 클라이언트가 ioredis `wait` 상태일 때만 lifecycle service가 `connect()`를 호출합니다.
@@ -85,6 +87,25 @@ export class CacheRepository {
85
87
  - shutdown 단계에서는 ready/connecting 계열 상태에 `quit()`를 우선 시도해 정상 종료를 노리고, monitoring, wait/종료 전이 상태에서는 `disconnect()`를 직접 사용합니다.
86
88
  - `quit()`가 실패하면 Fluo는 `disconnect()`로 fallback하고, 그 뒤에도 클라이언트가 닫히지 않은 경우에만 에러를 다시 던집니다.
87
89
 
90
+ ### 옵션 정규화
91
+
92
+ `RedisModuleOptions`는 최종 `ioredis` 생성자 형태가 아니라 `RedisModule.forRoot(...)`가 받는 caller-facing 입력입니다. 일반 `ioredis` 옵션 중 `lazyConnect`와 `name`을 제외한 필드에 Fluo 전용 필드 네 개를 추가합니다.
93
+
94
+ - `name`은 Fluo 등록과 해당 DI 토큰을 식별합니다. 이 값은 `ioredis` 생성자 `name`이 되지 않습니다. trim한 각 name은 애플리케이션 전체에서 하나만 등록할 수 있는 ownership identity입니다.
95
+ - `global`은 module visibility를 제어합니다. 기본 등록은 `false`로 지정하지 않는 한 global이고, named registration은 항상 scoped이며 `global: true`를 거부합니다.
96
+ - `lifecycle`은 Fluo가 소유한 `connect()`와 `quit()` timeout guardrail을 설정합니다.
97
+ - `sentinelName`은 입력의 `name` 필드가 Fluo 등록 식별자에 예약되어 있으므로 ioredis Sentinel master name을 별도로 받습니다.
98
+
99
+ Client 생성 전에 다음 순서로 정규화합니다.
100
+
101
+ 1. 일반 ioredis option에서 `name`, `global`, `lifecycle`, `sentinelName`을 분리합니다.
102
+ 2. 등록 `name`을 trim한 뒤 lifecycle timeout을 정규화하고 검증합니다.
103
+ 3. 빈 등록 `name`을 거부하고 named registration의 `global: true`를 거부합니다.
104
+ 4. `sentinelName`이 있으면 ioredis 생성자 `name`으로 매핑해 `RedisClientOptions`를 만듭니다.
105
+ 5. Provider가 `{ ...clientOptions, lazyConnect: true }`로 client를 생성합니다. 호출자가 type restriction을 우회해 `lazyConnect: false`를 강제로 cast하더라도 마지막 할당이 항상 우선합니다.
106
+
107
+ 따라서 `RedisClientOptions`는 provider factory가 소비하는 정규화된 constructor-facing option을 나타냅니다. 일반 ioredis option과 선택적 Sentinel `name`을 포함하지만 Fluo 전용 field와 caller-controlled `lazyConnect`는 포함하지 않습니다. `forRoot(...)`가 받는 `RedisModuleOptions`와 같은 의미가 아닙니다.
108
+
88
109
  ### 이름 있는 클라이언트
89
110
 
90
111
  하나의 애플리케이션에서 여러 Redis 연결이 필요하면 `RedisModule.forRoot({ name, ...options })`를 사용하세요. `name` 없는 `RedisModule.forRoot(options)`는 기본 `REDIS_CLIENT`와 `RedisService` 별칭을 제공하고, 이름 있는 등록은 `getRedisClientToken(name)`과 `getRedisServiceToken(name)`으로 해석합니다.
@@ -161,14 +182,6 @@ import type Redis from 'ioredis';
161
182
  const COMMAND_REDIS = getRedisClientToken();
162
183
  const SUBSCRIBER_REDIS = getRedisClientToken('subscriber');
163
184
 
164
- @Module({
165
- imports: [
166
- RedisModule.forRoot({ host: 'localhost', port: 6379 }),
167
- RedisModule.forRoot({ name: 'subscriber', host: 'localhost', port: 6379 }),
168
- ],
169
- })
170
- export class RedisConnectionsModule {}
171
-
172
185
  @Inject(COMMAND_REDIS, SUBSCRIBER_REDIS)
173
186
  export class PubSubTransportFactory {
174
187
  constructor(
@@ -183,8 +196,19 @@ export class PubSubTransportFactory {
183
196
  });
184
197
  }
185
198
  }
199
+
200
+ @Module({
201
+ imports: [
202
+ RedisModule.forRoot({ host: 'localhost', port: 6379 }),
203
+ RedisModule.forRoot({ name: 'subscriber', host: 'localhost', port: 6379 }),
204
+ ],
205
+ providers: [PubSubTransportFactory],
206
+ })
207
+ export class RedisConnectionsModule {}
186
208
  ```
187
209
 
210
+ `@Inject(...)`는 클래스의 생성자 토큰만 선언하며 클래스를 등록하지 않습니다. fluo는 명시적 모듈 등록에서 프로바이더를 resolve하므로 `PubSubTransportFactory`는 Redis 등록을 import하는 모듈의 `providers` 배열에 반드시 나열해야 합니다. 어느 모듈에도 나열되지 않은 decorated 클래스는 resolve할 수 없습니다.
211
+
188
212
  ## 공개 API 개요
189
213
 
190
214
  ### 핵심 구성 요소
@@ -202,8 +226,8 @@ export class PubSubTransportFactory {
202
226
  ### 타입
203
227
  - `DefaultRedisModuleOptions`: 이름 없는 기본 Redis 등록이 받는 옵션입니다. 선택적 global alias visibility와 lifecycle timeout control을 포함합니다.
204
228
  - `NamedRedisModuleOptions`: 추가 이름 있는 Redis 등록이 받는 옵션입니다. 필수 `name`과 scoped lifecycle timeout control을 포함합니다.
205
- - `RedisModuleOptions`: Fluo가 module-only `name`, `global`, `lifecycle`, `sentinelName` 필드를 제거한 뒤 `ioredis` 생성자에 전달하는 설정 옵션입니다. `sentinelName`은 ioredis Sentinel master `name`으로 전달되고, `name`은 Fluo 등록 식별자로 유지됩니다.
206
- - `RedisClientOptions`: Fluo가 module-only field를 제거하고 내부에서 `lazyConnect: true`를 강제하기 전의 Redis constructor option입니다.
229
+ - `RedisModuleOptions`: `RedisModule.forRoot(...)`가 받는 caller-facing union입니다. `lazyConnect`와 `name`을 제외한 일반 ioredis option에 Fluo 전용 `name`, `global`, `lifecycle`, `sentinelName` field를 결합합니다.
230
+ - `RedisClientOptions`: Fluo가 module-only field를 제거하고 `sentinelName`을 ioredis Sentinel `name`으로 매핑한 뒤의 정규화된 constructor-facing option입니다. Provider는 그 다음 마지막 override로 `lazyConnect: true`를 추가합니다.
207
231
  - `RedisLifecycleOptions`: Fluo가 소유한 `connect()`와 `quit()` lifecycle command의 timeout을 조정하는 선택적 옵션입니다.
208
232
  - `PersistencePlatformStatusSnapshot`, `RedisStatusAdapterInput`: status snapshot input/output type입니다.
209
233
 
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  <p><strong><kbd>English</kbd></strong> <a href="./README.ko.md"><kbd>한국어</kbd></a></p>
4
4
 
5
+ Node.js support is `>=24.0.0 <27`. See [Node.js support and migration](../../docs/reference/node-support.md) before upgrading.
6
+
5
7
  Shared Redis connection layer for fluo. It provides a default app-scoped `ioredis` client plus optional named clients, all managed by the application lifecycle.
6
8
 
7
9
  ## Table of Contents
@@ -77,7 +79,7 @@ export class CacheRepository {
77
79
 
78
80
  ### Lifecycle Ownership
79
81
 
80
- Every `RedisModule.forRoot(...)` registration creates a new client that `@fluojs/redis` owns, including named clients registered through `RedisModule.forRoot({ name, ... })`. The module never adopts an existing client instance.
82
+ Every `RedisModule.forRoot(...)` registration creates a new client that `@fluojs/redis` owns, including named clients registered through `RedisModule.forRoot({ name, ... })`. The module never adopts an existing client instance. Registration identity is application-wide: only one unnamed default registration and one registration for each trimmed name are allowed. Bootstrap rejects duplicate identities before creating a Redis client.
81
83
 
82
84
  - Fluo always forces `lazyConnect: true`, even if callers cast options manually, so sockets open during application bootstrap instead of import time.
83
85
  - During bootstrap, the lifecycle service only calls `connect()` while the client is still in ioredis `wait` state.
@@ -85,6 +87,25 @@ Every `RedisModule.forRoot(...)` registration creates a new client that `@fluojs
85
87
  - During shutdown, ready/connecting clients attempt `quit()` first for graceful teardown, while monitoring, wait, and closed-transition states use `disconnect()` directly.
86
88
  - If `quit()` fails, Fluo falls back to `disconnect()` and only rethrows when the client still remains open afterward.
87
89
 
90
+ ### Option Normalization
91
+
92
+ `RedisModuleOptions` is the caller-facing input to `RedisModule.forRoot(...)`, not the final `ioredis` constructor shape. It accepts ordinary `ioredis` options except `lazyConnect` and `name`, then adds four Fluo-only fields:
93
+
94
+ - `name` identifies the Fluo registration and its DI tokens. It never becomes the `ioredis` constructor `name`. Each trimmed name is an application-wide ownership identity and can be registered only once.
95
+ - `global` controls module visibility. The default registration is global unless set to `false`; named registrations are always scoped and reject `global: true`.
96
+ - `lifecycle` configures Fluo-owned `connect()` and `quit()` timeout guardrails.
97
+ - `sentinelName` supplies the ioredis Sentinel master name because the input `name` field is reserved for Fluo registration identity.
98
+
99
+ Normalization happens before client construction in this order:
100
+
101
+ 1. Fluo removes `name`, `global`, `lifecycle`, and `sentinelName` from the ordinary ioredis options.
102
+ 2. It trims the registration `name`, then normalizes and validates the lifecycle timeouts.
103
+ 3. It rejects a blank registration name and rejects `global: true` on a named registration.
104
+ 4. It maps `sentinelName`, when present, to the ioredis constructor `name` and produces `RedisClientOptions`.
105
+ 5. The provider creates the client with `{ ...clientOptions, lazyConnect: true }`. This final assignment always wins, including when a caller bypasses the type restriction and casts `lazyConnect: false` into the input.
106
+
107
+ `RedisClientOptions` therefore describes the normalized constructor-facing options consumed by the provider factory: ordinary ioredis options plus an optional Sentinel `name`, but no Fluo-only fields and no caller-controlled `lazyConnect`. It is not a synonym for the `RedisModuleOptions` accepted by `forRoot(...)`.
108
+
88
109
  ### Named Clients
89
110
 
90
111
  Use `RedisModule.forRoot({ name, ...options })` when one application needs more than one Redis connection. `RedisModule.forRoot(options)` without `name` provides the default `REDIS_CLIENT` and `RedisService` aliases, and named registrations are resolved with `getRedisClientToken(name)` and `getRedisServiceToken(name)`.
@@ -161,14 +182,6 @@ import type Redis from 'ioredis';
161
182
  const COMMAND_REDIS = getRedisClientToken();
162
183
  const SUBSCRIBER_REDIS = getRedisClientToken('subscriber');
163
184
 
164
- @Module({
165
- imports: [
166
- RedisModule.forRoot({ host: 'localhost', port: 6379 }),
167
- RedisModule.forRoot({ name: 'subscriber', host: 'localhost', port: 6379 }),
168
- ],
169
- })
170
- export class RedisConnectionsModule {}
171
-
172
185
  @Inject(COMMAND_REDIS, SUBSCRIBER_REDIS)
173
186
  export class PubSubTransportFactory {
174
187
  constructor(
@@ -183,8 +196,19 @@ export class PubSubTransportFactory {
183
196
  });
184
197
  }
185
198
  }
199
+
200
+ @Module({
201
+ imports: [
202
+ RedisModule.forRoot({ host: 'localhost', port: 6379 }),
203
+ RedisModule.forRoot({ name: 'subscriber', host: 'localhost', port: 6379 }),
204
+ ],
205
+ providers: [PubSubTransportFactory],
206
+ })
207
+ export class RedisConnectionsModule {}
186
208
  ```
187
209
 
210
+ `@Inject(...)` only declares the constructor tokens of a class; it does not register that class. fluo resolves providers from explicit module registration, so `PubSubTransportFactory` must be listed in the `providers` array of the module that imports the Redis registrations it depends on. A decorated class that no module lists is not resolvable.
211
+
188
212
  ## Public API Overview
189
213
 
190
214
  ### Core
@@ -202,8 +226,8 @@ export class PubSubTransportFactory {
202
226
  ### Types
203
227
  - `DefaultRedisModuleOptions`: Options accepted by the unnamed default Redis registration, including optional global alias visibility and lifecycle timeout controls.
204
228
  - `NamedRedisModuleOptions`: Options accepted by additional named Redis registrations, including required `name` and scoped lifecycle timeout controls.
205
- - `RedisModuleOptions`: Configuration options passed to the `ioredis` constructor after Fluo removes module-only `name`, `global`, `lifecycle`, and `sentinelName` fields. `sentinelName` is forwarded as the ioredis Sentinel master `name`, while `name` remains the Fluo registration identifier.
206
- - `RedisClientOptions`: Redis constructor options after Fluo removes module-only fields and before it forces `lazyConnect: true` internally.
229
+ - `RedisModuleOptions`: Caller-facing union accepted by `RedisModule.forRoot(...)`. It combines ordinary ioredis options except `lazyConnect` and `name` with the Fluo-only `name`, `global`, `lifecycle`, and `sentinelName` fields.
230
+ - `RedisClientOptions`: Normalized constructor-facing options after Fluo removes its module-only fields and maps `sentinelName` to the ioredis Sentinel `name`. The provider adds `lazyConnect: true` afterward as the final override.
207
231
  - `RedisLifecycleOptions`: Optional timeout controls for Fluo-owned `connect()` and `quit()` lifecycle commands.
208
232
  - `PersistencePlatformStatusSnapshot`, `RedisStatusAdapterInput`: Status snapshot input/output types.
209
233
 
@@ -1 +1 @@
1
- {"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AACA,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAMhE,OAAO,KAAK,EAA6C,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAuIhG,yEAAyE;AACzE,qBAAa,WAAW;IACtB;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,kBAAkB,GAAG,UAAU;CAYxD"}
1
+ {"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AACA,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAMhE,OAAO,KAAK,EAA6C,kBAAkB,EAAE,MAAM,YAAY,CAAC;AA4KhG,yEAAyE;AACzE,qBAAa,WAAW;IACtB;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,kBAAkB,GAAG,UAAU;CAYxD"}
package/dist/module.js CHANGED
@@ -2,8 +2,9 @@ import { defineModule } from '@fluojs/runtime';
2
2
  import Redis from 'ioredis';
3
3
  import { getRedisServiceToken, RedisService } from './redis-service.js';
4
4
  import { RedisLifecycleService } from './service.js';
5
- import { getRedisClientToken, REDIS_CLIENT } from './tokens.js';
5
+ import { DEFAULT_REDIS_CLIENT_NAME, getRedisClientToken, REDIS_CLIENT } from './tokens.js';
6
6
  const redisLifecycleTokens = new Map();
7
+ const REDIS_REGISTRATION_IDENTITIES = Symbol('fluo.redis.registration-identities');
7
8
  function getRedisLifecycleToken(name) {
8
9
  const existing = redisLifecycleTokens.get(name);
9
10
  if (existing) {
@@ -65,10 +66,35 @@ function assertValidLifecycleTimeoutMs(fieldName, value) {
65
66
  throw new Error(`Redis lifecycle.${fieldName} must be a finite non-negative number.`);
66
67
  }
67
68
  }
69
+ function assertUniqueRedisRegistrationIdentities(identities) {
70
+ const seen = new Set();
71
+ for (const identity of identities) {
72
+ if (seen.has(identity)) {
73
+ throw new Error(`Duplicate @fluojs/redis registration identity "${identity}". Every RedisModule.forRoot(...) registration owns one lifecycle-managed client, so pass a distinct name to each additional registration.`);
74
+ }
75
+ seen.add(identity);
76
+ }
77
+ }
68
78
  function createRedisProviders(options, lifecycleOptions, name) {
69
79
  const clientToken = getRedisClientToken(name);
80
+ const registrationIdentity = name ?? DEFAULT_REDIS_CLIENT_NAME;
81
+ const registrationGuardToken = Symbol(`fluo.redis.registration-guard:${registrationIdentity}`);
82
+ const registrationProviders = [{
83
+ multi: true,
84
+ provide: REDIS_REGISTRATION_IDENTITIES,
85
+ useValue: registrationIdentity
86
+ }, {
87
+ inject: [REDIS_REGISTRATION_IDENTITIES],
88
+ provide: registrationGuardToken,
89
+ scope: 'singleton',
90
+ useFactory: (...deps) => {
91
+ const [identities] = deps;
92
+ assertUniqueRedisRegistrationIdentities(identities);
93
+ }
94
+ }];
70
95
  if (clientToken === REDIS_CLIENT) {
71
- return [{
96
+ return [...registrationProviders, {
97
+ inject: [registrationGuardToken],
72
98
  scope: 'singleton',
73
99
  provide: REDIS_CLIENT,
74
100
  useFactory: () => new Redis({
@@ -89,7 +115,8 @@ function createRedisProviders(options, lifecycleOptions, name) {
89
115
  throw new Error('Redis client name must be defined for named provider creation.');
90
116
  }
91
117
  const serviceToken = getRedisServiceToken(name);
92
- return [{
118
+ return [...registrationProviders, {
119
+ inject: [registrationGuardToken],
93
120
  scope: 'singleton',
94
121
  provide: clientToken,
95
122
  useFactory: () => new Redis({
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "connection",
10
10
  "lifecycle"
11
11
  ],
12
- "version": "1.1.0",
12
+ "version": "2.0.0",
13
13
  "private": false,
14
14
  "license": "MIT",
15
15
  "repository": {
@@ -18,7 +18,7 @@
18
18
  "directory": "packages/redis"
19
19
  },
20
20
  "engines": {
21
- "node": ">=20.0.0"
21
+ "node": ">=24.0.0 <27"
22
22
  },
23
23
  "publishConfig": {
24
24
  "access": "public"
@@ -36,16 +36,16 @@
36
36
  "dist"
37
37
  ],
38
38
  "dependencies": {
39
- "@fluojs/di": "^2.0.0",
40
- "@fluojs/core": "^1.1.0",
41
- "@fluojs/runtime": "^2.0.1"
39
+ "@fluojs/core": "^2.0.0",
40
+ "@fluojs/di": "^3.0.0",
41
+ "@fluojs/runtime": "^3.0.0"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "ioredis": "^5.10.0"
45
45
  },
46
46
  "devDependencies": {
47
47
  "ioredis": "^5.10.0",
48
- "vitest": "^3.2.4"
48
+ "vitest": "^4.1.11"
49
49
  },
50
50
  "scripts": {
51
51
  "prebuild": "node ../../tooling/scripts/clean-dist.mjs",