Skip to content
redis.ts 3.08 KiB
Newer Older
Grant's avatar
Grant committed
import { RedisClientType } from "@redis/client";
import { createClient } from "redis";
Grant's avatar
Grant committed
import { getLogger } from "./Logger";

const Logger = getLogger("REDIS");
Grant's avatar
Grant committed

Grant's avatar
Grant committed
/**
 * Typedef for RedisKeys
 */
interface IRedisKeys {
  // canvas
Grant's avatar
Grant committed
  // canvas(): string;
Grant's avatar
Grant committed
  canvas_section(
    start: [x: number, y: number],
    end: [x: number, y: number]
  ): string;
  canvas_cache_write_queue(workerId: number): string;
Grant's avatar
Grant committed

  // users
  socketToSub(socketId: string): string;
Grant's avatar
Grant committed

  // pub/sub channels
  channel_heatmap(): string;
Grant's avatar
Grant committed
}

/**
 * Defined as a variable due to boottime augmentation
 */
const RedisKeys: IRedisKeys = {
Grant's avatar
Grant committed
  // canvas: () => `CANVAS:PIXELS`,
Grant's avatar
Grant committed
  canvas_section: (start, end) =>
    `CANVAS:PIXELS:${start.join(",")}:${end.join(",")}`,
  canvas_cache_write_queue: (workerId) => `CANVAS:CACHE_QUEUE:${workerId}`,
Grant's avatar
Grant committed
  socketToSub: (socketId: string) => `CANVAS:SOCKET:${socketId}`,
Grant's avatar
Grant committed
  channel_heatmap: () => `CANVAS:HEATMAP`,
Grant's avatar
Grant committed
};

class _Redis {
  isConnecting = false;
  isConnected = false;
  client: RedisClientType;
Grant's avatar
Grant committed
  sub_client: RedisClientType; // the client used for pubsub
Grant's avatar
Grant committed

  waitingForConnect: ((...args: any) => any)[] = [];

Grant's avatar
Grant committed
  keys: IRedisKeys;

  /**
   * Redis client wrapper constructor
   *
   * @param keys Definition of keys, passed as an argument to allow for augmentation from configuration on boot
   */
  constructor(keys: IRedisKeys) {
    this.client = createClient({
      url: process.env.REDIS_HOST,
    });
Grant's avatar
Grant committed
    this.sub_client = createClient({
      url: process.env.REDIS_HOST,
    });
Grant's avatar
Grant committed

    this.keys = keys;
Grant's avatar
Grant committed
  }

  async connect() {
    if (this.isConnected)
      throw new Error("Attempted to run Redis#connect when already connected");

    this.isConnecting = true;
    await this.client.connect();
Grant's avatar
Grant committed
    await this.sub_client.connect();
    Logger.info(
      `Connected to Redis, there's ${this.waitingForConnect.length} function(s) waiting for Redis`
    );
    this.isConnecting = false;
    this.isConnected = true;

    for (const func of this.waitingForConnect) {
      func();
    }
Grant's avatar
Grant committed
  async disconnect() {
    if (!this.isConnected) {
      Logger.warn("Redis#disconnect called while not connected");
      return;
    }

    await this.client.disconnect();
    Logger.info("Disconnected from Redis");
    this.isConnected = false;
  }

Grant's avatar
Grant committed
  async getClient(intent: "MAIN" | "SUB" = "MAIN") {
    if (this.isConnecting) {
      await (() =>
        new Promise((res) => {
          Logger.warn("getClient() called and is now pending in queue");
          this.waitingForConnect.push(res);
        }))();
    }

    if (!this.isConnected) {
      await this.connect();
      this.isConnected = true;
    }

Grant's avatar
Grant committed
    if (intent === "SUB") {
      return this.sub_client;
    }

    return this.client;
  }
Grant's avatar
Grant committed

  key<Key extends keyof IRedisKeys>(
    key: Key,
    ...rest: Parameters<IRedisKeys[Key]>
  ): string {
    return (this.keys[key] as any)(...rest);
  }

  keyRef<Key extends keyof IRedisKeys>(
    key: Key
  ): (...params: Parameters<IRedisKeys[Key]>) => string {
    return (...params) => this.key(key, ...params);
  }
Grant's avatar
Grant committed
export const Redis = new _Redis(RedisKeys);