Skip to content

Commit

Permalink
feat(frontend): add instance registry to handle singletons (#52)
Browse files Browse the repository at this point in the history
  • Loading branch information
gregory-j-baker authored Dec 24, 2024
1 parent b5b7521 commit b5e0a04
Show file tree
Hide file tree
Showing 4 changed files with 47 additions and 19 deletions.
30 changes: 11 additions & 19 deletions frontend/app/.server/redis.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,33 @@
import { Duration, Redacted } from 'effect';
import type { RedisOptions } from 'ioredis';
import Redis from 'ioredis';

import { serverEnvironment } from '~/.server/environment';
import { LogFactory } from '~/.server/logging';
import { singleton } from '~/.server/utils/instance-registry';

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

/**
* A holder for our singleton redis client instance.
*/
const clientHolder: { client?: Redis } = {};

/**
* Retrieves the application's redis client instance.
* If the client does not exist, it initializes a new one.
*/
export function getRedisClient() {
return (clientHolder.client ??= createRedisClient());
}

/**
* Creates a new Redis client and sets up logging for connection and error events.
*/
function createRedisClient(): Redis {
log.info('Creating new redis client');
export function getRedisClient(): Redis {
return singleton('redisClient', () => {
log.info('Creating new redis client');

const { REDIS_CONNECTION_TYPE, REDIS_HOST, REDIS_PORT } = serverEnvironment;
const { REDIS_CONNECTION_TYPE, REDIS_HOST, REDIS_PORT } = serverEnvironment;

return new Redis(getRedisConfig())
.on('connect', () => log.info('Connected to %s://%s:%s/', REDIS_CONNECTION_TYPE, REDIS_HOST, REDIS_PORT))
.on('error', (error) => log.error('Redis client error: %s', error.message));
return new Redis(getRedisConfig())
.on('connect', () => log.info('Connected to %s://%s:%s/', REDIS_CONNECTION_TYPE, REDIS_HOST, REDIS_PORT))
.on('error', (error) => log.error('Redis client error: %s', error.message));
});
}

/**
* Constructs the configuration object for the Redis client based on the server environment.
*/
function getRedisConfig() {
function getRedisConfig(): RedisOptions {
const {
REDIS_COMMAND_TIMEOUT_SECONDS, //
REDIS_HOST,
Expand Down
27 changes: 27 additions & 0 deletions frontend/app/.server/utils/instance-registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { AppError } from '~/errors/app-error';
import { ErrorCodes } from '~/errors/error-codes';

/* eslint-disable @typescript-eslint/no-unnecessary-condition */

export const instanceNames = ['redisClient'] as const;

export type InstanceName = (typeof instanceNames)[number];

/**
* Retrieves a singleton instance. If the instance does not exist, it is created using the provided factory function.
*
* @throws {AppError} If the instance is not found and no factory function is provided.
*/
export function singleton<T>(instanceName: InstanceName, factory?: () => T): T {
globalThis.__instanceRegistry ??= new Map<InstanceName, unknown>();

if (!globalThis.__instanceRegistry.has(instanceName)) {
if (!factory) {
throw new AppError(`Instance [${instanceName}] not found and factory not provided`, ErrorCodes.NO_FACTORY_PROVIDED);
}

globalThis.__instanceRegistry.set(instanceName, factory());
}

return globalThis.__instanceRegistry.get(instanceName) as T;
}
6 changes: 6 additions & 0 deletions frontend/app/@types/global.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { RouteModules } from 'react-router';

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

/* eslint-disable no-var */

Expand All @@ -20,6 +21,11 @@ declare global {
*/
var __appEnvironment: ClientEnvironment;

/**
* A holder for any application-scoped singletons.
*/
var __instanceRegistry: Map<InstanceName, unknown>;

/**
* React Router adds the route modules to global
* scope, but doesn't declare them anywhere.
Expand Down
3 changes: 3 additions & 0 deletions frontend/app/errors/error-codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ export const ErrorCodes = {
// i18n error codes
NO_LANGUAGE_FOUND: 'I18N-0001',

// instance error codes
NO_FACTORY_PROVIDED: 'INST-0001',

// route error codes
ROUTE_NOT_FOUND: 'RTE-0001',

Expand Down

0 comments on commit b5e0a04

Please sign in to comment.