Skip to content

Commit b5e0a04

Browse files
feat(frontend): add instance registry to handle singletons (#52)
1 parent b5b7521 commit b5e0a04

File tree

4 files changed

+47
-19
lines changed

4 files changed

+47
-19
lines changed

frontend/app/.server/redis.ts

+11-19
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,33 @@
11
import { Duration, Redacted } from 'effect';
2+
import type { RedisOptions } from 'ioredis';
23
import Redis from 'ioredis';
34

45
import { serverEnvironment } from '~/.server/environment';
56
import { LogFactory } from '~/.server/logging';
7+
import { singleton } from '~/.server/utils/instance-registry';
68

79
const log = LogFactory.getLogger(import.meta.url);
810

9-
/**
10-
* A holder for our singleton redis client instance.
11-
*/
12-
const clientHolder: { client?: Redis } = {};
13-
1411
/**
1512
* Retrieves the application's redis client instance.
1613
* If the client does not exist, it initializes a new one.
1714
*/
18-
export function getRedisClient() {
19-
return (clientHolder.client ??= createRedisClient());
20-
}
21-
22-
/**
23-
* Creates a new Redis client and sets up logging for connection and error events.
24-
*/
25-
function createRedisClient(): Redis {
26-
log.info('Creating new redis client');
15+
export function getRedisClient(): Redis {
16+
return singleton('redisClient', () => {
17+
log.info('Creating new redis client');
2718

28-
const { REDIS_CONNECTION_TYPE, REDIS_HOST, REDIS_PORT } = serverEnvironment;
19+
const { REDIS_CONNECTION_TYPE, REDIS_HOST, REDIS_PORT } = serverEnvironment;
2920

30-
return new Redis(getRedisConfig())
31-
.on('connect', () => log.info('Connected to %s://%s:%s/', REDIS_CONNECTION_TYPE, REDIS_HOST, REDIS_PORT))
32-
.on('error', (error) => log.error('Redis client error: %s', error.message));
21+
return new Redis(getRedisConfig())
22+
.on('connect', () => log.info('Connected to %s://%s:%s/', REDIS_CONNECTION_TYPE, REDIS_HOST, REDIS_PORT))
23+
.on('error', (error) => log.error('Redis client error: %s', error.message));
24+
});
3325
}
3426

3527
/**
3628
* Constructs the configuration object for the Redis client based on the server environment.
3729
*/
38-
function getRedisConfig() {
30+
function getRedisConfig(): RedisOptions {
3931
const {
4032
REDIS_COMMAND_TIMEOUT_SECONDS, //
4133
REDIS_HOST,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { AppError } from '~/errors/app-error';
2+
import { ErrorCodes } from '~/errors/error-codes';
3+
4+
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
5+
6+
export const instanceNames = ['redisClient'] as const;
7+
8+
export type InstanceName = (typeof instanceNames)[number];
9+
10+
/**
11+
* Retrieves a singleton instance. If the instance does not exist, it is created using the provided factory function.
12+
*
13+
* @throws {AppError} If the instance is not found and no factory function is provided.
14+
*/
15+
export function singleton<T>(instanceName: InstanceName, factory?: () => T): T {
16+
globalThis.__instanceRegistry ??= new Map<InstanceName, unknown>();
17+
18+
if (!globalThis.__instanceRegistry.has(instanceName)) {
19+
if (!factory) {
20+
throw new AppError(`Instance [${instanceName}] not found and factory not provided`, ErrorCodes.NO_FACTORY_PROVIDED);
21+
}
22+
23+
globalThis.__instanceRegistry.set(instanceName, factory());
24+
}
25+
26+
return globalThis.__instanceRegistry.get(instanceName) as T;
27+
}

frontend/app/@types/global.d.ts

+6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { RouteModules } from 'react-router';
22

33
import type { ClientEnvironment } from '~/.server/environment';
4+
import type { InstanceName } from '~/.server/utils/instance-registry';
45

56
/* eslint-disable no-var */
67

@@ -20,6 +21,11 @@ declare global {
2021
*/
2122
var __appEnvironment: ClientEnvironment;
2223

24+
/**
25+
* A holder for any application-scoped singletons.
26+
*/
27+
var __instanceRegistry: Map<InstanceName, unknown>;
28+
2329
/**
2430
* React Router adds the route modules to global
2531
* scope, but doesn't declare them anywhere.

frontend/app/errors/error-codes.ts

+3
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ export const ErrorCodes = {
1313
// i18n error codes
1414
NO_LANGUAGE_FOUND: 'I18N-0001',
1515

16+
// instance error codes
17+
NO_FACTORY_PROVIDED: 'INST-0001',
18+
1619
// route error codes
1720
ROUTE_NOT_FOUND: 'RTE-0001',
1821

0 commit comments

Comments
 (0)