Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(frontend): add instance registry to handle singletons #52

Merged
merged 1 commit into from
Dec 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading