Skip to content

Commit 6e7688f

Browse files
committed
fix(client): ensure Redis is ready in all lifecycle hooks
1 parent 5e2f73f commit 6e7688f

File tree

2 files changed

+149
-26
lines changed

2 files changed

+149
-26
lines changed

packages/client/src/lib/module.int.spec.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
1+
import {
2+
BeforeApplicationShutdown,
3+
Injectable,
4+
OnApplicationBootstrap,
5+
OnApplicationShutdown,
6+
OnModuleDestroy,
7+
OnModuleInit,
8+
} from '@nestjs/common';
19
import { ConfigModule, ConfigService } from '@nestjs/config';
210
import { Test, TestingModule } from '@nestjs/testing';
11+
import { InjectRedis } from './decorators';
312
import { RedisModule } from './module';
413
import { RedisToken } from './tokens';
514
import { Redis, RedisModuleOptions } from './types';
@@ -452,3 +461,122 @@ describe('Multi-connection Integration', () => {
452461
expect(ping2).toBe('PONG');
453462
});
454463
});
464+
465+
describe('RedisModule Service Lifecycle Integration', () => {
466+
// Test service that implements all lifecycle hooks
467+
@Injectable()
468+
class TestLifecycleService
469+
implements
470+
OnModuleInit,
471+
OnApplicationBootstrap,
472+
OnModuleDestroy,
473+
OnApplicationShutdown,
474+
BeforeApplicationShutdown
475+
{
476+
constructor(
477+
@InjectRedis() readonly redis: Redis,
478+
private readonly prefix = 'root',
479+
) {}
480+
481+
async beforeApplicationShutdown(signal?: string) {
482+
await this.redis.set(
483+
`${this.prefix}:lifecycle:beforeShutdown`,
484+
`application-before-shutdown-${signal || `unknown`}`,
485+
);
486+
}
487+
488+
async onModuleInit(): Promise<void> {
489+
await this.redis.set(
490+
`${this.prefix}:lifecycle:init`,
491+
`module-initialized`,
492+
);
493+
}
494+
495+
async onApplicationBootstrap(): Promise<void> {
496+
await this.redis.set(
497+
`${this.prefix}:lifecycle:bootstrap`,
498+
`application-bootstrapped`,
499+
);
500+
}
501+
502+
async onModuleDestroy(): Promise<void> {
503+
await this.redis.set(
504+
`${this.prefix}:lifecycle:destroy`,
505+
`module-destroyed`,
506+
);
507+
}
508+
509+
async onApplicationShutdown(signal?: string): Promise<void> {
510+
await this.redis.set(
511+
`${this.prefix}:lifecycle:shutdown`,
512+
`application-shutdown-${signal || 'unknown'}`,
513+
);
514+
}
515+
}
516+
517+
let module: TestingModule;
518+
let asyncModule: TestingModule;
519+
520+
beforeEach(async () => {
521+
// Create the testing module with the lifecycle service
522+
module = await Test.createTestingModule({
523+
imports: [
524+
RedisModule.forRoot({ options: { url: process.env.REDIS_URL } }),
525+
],
526+
providers: [
527+
{
528+
provide: TestLifecycleService,
529+
inject: [RedisToken()],
530+
useFactory: (redis: Redis) => new TestLifecycleService(redis, 'sync'),
531+
},
532+
],
533+
}).compile();
534+
535+
asyncModule = await Test.createTestingModule({
536+
imports: [
537+
RedisModule.forRootAsync({
538+
useFactory: () => ({ options: { url: process.env.REDIS_URL } }),
539+
inject: [],
540+
}),
541+
],
542+
providers: [
543+
{
544+
provide: TestLifecycleService,
545+
inject: [RedisToken()],
546+
useFactory: (redis: Redis) =>
547+
new TestLifecycleService(redis, 'async'),
548+
},
549+
],
550+
}).compile();
551+
});
552+
553+
describe('Service Lifecycle with Redis', () => {
554+
it('should have Redis available in onModuleInit', async () => {
555+
await module.init();
556+
await asyncModule.init();
557+
558+
await module.close();
559+
await asyncModule.close();
560+
});
561+
562+
it('should disconnect Redis after onApplicationShutdown', async () => {
563+
const service = module.get(TestLifecycleService);
564+
const asyncService = asyncModule.get(TestLifecycleService);
565+
566+
await module.init();
567+
await asyncModule.init();
568+
569+
// Verify that the redis client is connected
570+
expect(service.redis.isReady).toBe(true);
571+
expect(asyncService.redis.isReady).toBe(true);
572+
573+
// Trigger application shutdown
574+
await module.close();
575+
await asyncModule.close();
576+
577+
// Verify that the redis client is disconnected
578+
expect(service.redis.isReady).toBe(false);
579+
expect(asyncService.redis.isReady).toBe(false);
580+
});
581+
});
582+
});

packages/client/src/lib/module.ts

Lines changed: 21 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import {
22
DynamicModule,
33
FactoryProvider,
44
Module,
5-
OnApplicationBootstrap,
65
OnApplicationShutdown,
76
} from '@nestjs/common';
87
import { ModuleRef } from '@nestjs/core';
@@ -18,7 +17,7 @@ import { Redis, RedisModuleForRootOptions, RedisModuleOptions } from './types';
1817
@Module({})
1918
export class RedisModule
2019
extends ConfigurableModuleClass
21-
implements OnApplicationBootstrap, OnApplicationShutdown
20+
implements OnApplicationShutdown
2221
{
2322
protected connectionToken?: string;
2423

@@ -66,36 +65,32 @@ export class RedisModule
6665
): FactoryProvider {
6766
return {
6867
provide: RedisToken(connectionName),
69-
useFactory: (config: RedisModuleOptions): Redis => {
70-
switch (config?.type) {
71-
case 'client':
72-
case undefined:
73-
return createClient(config?.options);
74-
case 'cluster':
75-
return createCluster(config.options);
76-
case 'sentinel':
77-
return createSentinel(config.options);
78-
default:
79-
throw new Error(
80-
// @ts-expect-error check for config type
81-
`Unsupported Redis type: ${config?.type}. Supported types are 'client', 'cluster' and 'sentinel'`,
82-
);
68+
useFactory: async (config: RedisModuleOptions): Promise<Redis> => {
69+
function getClient(): Redis {
70+
switch (config?.type) {
71+
case 'client':
72+
case undefined:
73+
return createClient(config?.options);
74+
case 'cluster':
75+
return createCluster(config.options);
76+
case 'sentinel':
77+
return createSentinel(config.options);
78+
default:
79+
throw new Error(
80+
// @ts-expect-error check for config type
81+
`Unsupported Redis type: ${config?.type}. Supported types are 'client', 'cluster' and 'sentinel'`,
82+
);
83+
}
8384
}
85+
86+
const client = getClient();
87+
await client.connect();
88+
return client;
8489
},
8590
inject: [MODULE_OPTIONS_TOKEN],
8691
};
8792
}
8893

89-
async onApplicationBootstrap() {
90-
if (!this.connectionToken) {
91-
throw new Error(
92-
'Connection token is not defined. Ensure to call forRoot or forRootAsync.',
93-
);
94-
}
95-
96-
await this.moduleRef.get<Redis>(this.connectionToken).connect();
97-
}
98-
9994
async onApplicationShutdown() {
10095
if (!this.connectionToken) {
10196
throw new Error(

0 commit comments

Comments
 (0)