Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/theonetrade/backtest-kit-redis-mongo-docker/llms.txt

Use this file to discover all available pages before exploring further.

The three base services — MongooseService, RedisService, and LoggerService — form the infrastructure layer of the backtest-kit stack. They are registered in the IoC container and injected into every DB and cache service in the system. You rarely interact with them directly, but understanding their initialization contract, timeout behavior, and logging interface is essential when wiring up a new environment or debugging connectivity issues.

MongooseService

Wraps the Mongoose connection with a singleshot init guard and a 15-second timeout. Emits lifecycle events and closes gracefully on SIGINT.

RedisService

Wraps the ioredis client with the same singleshot / 15-second timeout pattern, plus a 30-second keepalive ping loop defined in redis.ts.

LoggerService

Ships a no-op logger by default. Call setLogger() to plug in any object that satisfies the ILogger interface.

MongooseService

MongooseService is the single authoritative owner of the Mongoose connection inside the IoC container. Its core responsibility is ensuring the connection is established exactly once — even when multiple services call waitForInit() concurrently.
const CONNECTION_TIMEOUT = 15_000;

export class MongooseService {
  readonly loggerService = inject<LoggerService>(TYPES.loggerService);

  public waitForInit = singleshot(async () => {
    const mongoose = await getMongoose();
    if (mongoose.connection.readyState === CONNECTED_STATE) {
      return mongoose; // already connected
    }
    const result = await Promise.race([
      waitForConnect(mongoose, this),
      sleep(CONNECTION_TIMEOUT).then(() => TIMEOUT_SYMBOL),
    ]);
    if (result === TIMEOUT_SYMBOL) {
      this.waitForInit.clear();
      throw new Error("Mongoose connection timeout");
    }
    return mongoose;
  });
}

waitForInit()

The method is wrapped with singleshot, which means the underlying async factory runs at most once and every subsequent caller receives the same cached promise. If the connection is already in readyState === 1 (connected) when the function is first called, it returns immediately. If the connection has not been established, waitForInit races the waitForConnect promise against a 15-second sleep. When the timeout wins:
  1. this.waitForInit.clear() resets the singleshot guard so the next caller can retry.
  2. An Error("Mongoose connection timeout") is thrown to the waiting caller.
If waitForInit() times out, the singleshot guard is cleared automatically so the next call will retry the connection. Do not suppress the thrown error — let it propagate so the caller can decide whether to retry or abort.

Connection events

MongooseService registers listeners on the Mongoose connection for four lifecycle events:
EventMeaning
connectedThe TCP connection to MongoDB is open and authenticated.
errorA connection-level error occurred; details are forwarded to loggerService.
disconnectedThe connection was lost; Mongoose will attempt to reconnect automatically.
reconnectedThe connection was successfully re-established after a drop.

SIGINT handler

When the process receives SIGINT (e.g., Ctrl+C in a terminal), MongooseService calls mongoose.connection.close() before allowing the process to exit. This ensures all pending write operations are flushed and the connection is torn down cleanly rather than being killed mid-operation.

IoC access

import { ioc } from "backtest-kit";

const mongoose = await ioc.mongoService.waitForInit();

RedisService

RedisService follows the identical initialization contract as MongooseService, but targets the ioredis client. The singleshot guard, the 15-second race, and the timeout-clear-and-throw pattern are all the same.
const CONNECTION_TIMEOUT = 15_000;

export class RedisService {
  readonly loggerService = inject<LoggerService>(TYPES.loggerService);

  public waitForInit = singleshot(async () => {
    const redis = await getRedis();
    if (redis.status === 'ready') return redis;
    const result = await Promise.race([
      waitForConnect(redis, this),
      sleep(CONNECTION_TIMEOUT).then(() => TIMEOUT_SYMBOL),
    ]);
    if (result === TIMEOUT_SYMBOL) {
      this.waitForInit.clear();
      throw new Error("Redis connection timeout");
    }
    return redis;
  });
}

waitForInit()

The guard checks redis.status === 'ready' before waiting. If the ioredis client is already in the ready state (e.g., the service was already initialized earlier in the process lifetime), it returns immediately without entering the race.

30-second keepalive ping

The underlying redis.ts factory sets up a setInterval that sends a PING command every 30 seconds. This prevents cloud-hosted Redis instances (e.g., Redis on Render, Railway, or ElastiCache) from closing idle connections due to inactivity timeouts.
The keepalive ping is transparent — you never need to configure or call it manually. It starts as soon as the ioredis client is created inside getRedis().

SIGINT handler

The redis.ts factory registers a SIGINT listener that calls redis.disconnect(false) before the process exits. The false argument tells ioredis to close the connection immediately without waiting for in-flight commands to complete. This mirrors the graceful-close behaviour in MongooseService, but uses the ioredis-native disconnect method rather than a server-side close command.
process.on("SIGINT", async () => {
  await redis.disconnect(false);
});
redis.disconnect(false) is distinct from redis.quit(). quit sends a Redis QUIT command and waits for the server’s acknowledgement; disconnect(false) closes the TCP socket immediately. The false flag suppresses the automatic reconnection that ioredis would otherwise attempt.

IoC access

import { ioc } from "backtest-kit";

const redis = await ioc.redisService.waitForInit();

LoggerService

LoggerService is a thin wrapper that delegates to any object satisfying the ILogger interface. By default it uses NOOP_LOGGER — a no-op implementation that silently discards all messages. This means the library emits no console output unless you explicitly opt in.

The ILogger interface

interface ILogger {
  log(topic: string, ...args: any[]): void;
  debug(topic: string, ...args: any[]): void;
  info(topic: string, ...args: any[]): void;
  warn(topic: string, ...args: any[]): void;
}
All four methods receive a topic string as their first argument. The topic is a short dot-separated or colon-separated identifier that describes the emitting service and operation (e.g., "MongooseService.waitForInit"). Remaining arguments are arbitrary values — objects, strings, errors — forwarded verbatim to the underlying logger.

LoggerService class

export class LoggerService implements ILogger {
  private _commonLogger: ILogger = NOOP_LOGGER;

  public log   = async (topic: string, ...args: any[]) => { ... };
  public debug = async (topic: string, ...args: any[]) => { ... };
  public info  = async (topic: string, ...args: any[]) => { ... };
  public warn  = async (topic: string, ...args: any[]) => { ... };

  public setLogger = (logger: ILogger) => {
    this._commonLogger = logger;
  };
}

setLogger(logger: ILogger)

Call setLogger once during application bootstrap to replace the no-op with your own implementation. Any object that satisfies ILogger is valid — a console-based adapter, a Winston logger, a Pino instance wrapped in an adapter, or a custom structured logger.
import { ioc } from "backtest-kit";

ioc.loggerService.setLogger({
  log:   (topic, ...args) => console.log(`[LOG]   ${topic}`, ...args),
  debug: (topic, ...args) => console.debug(`[DEBUG] ${topic}`, ...args),
  info:  (topic, ...args) => console.info(`[INFO]  ${topic}`, ...args),
  warn:  (topic, ...args) => console.warn(`[WARN]  ${topic}`, ...args),
});
setLogger is not thread-safe across async boundaries in the sense that any log calls already in-flight when you swap the logger will still go to the old instance. Always call setLogger before invoking waitForInfra() or any service that emits logs.

IoC access

import { ioc } from "backtest-kit";

ioc.loggerService.setLogger(myLogger);

Initialization order

All three base services are initialized together by the waitForInfra() helper in setup.ts, which calls mongoService.waitForInit() and redisService.waitForInit() in parallel before any DB or cache service is used. You do not need to call waitForInit() manually unless you are building a custom bootstrap sequence outside of the provided setup utilities.
Always call waitForInfra() (or manually await both mongoService.waitForInit() and redisService.waitForInit()) before making any call to a DbService or CacheService. Both infrastructure connections must be established before the first adapter operation.
import { ioc } from "backtest-kit";

// In your application entry point:
await Promise.all([
  ioc.mongoService.waitForInit(),
  ioc.redisService.waitForInit(),
]);

// Now all DbServices and CacheServices are ready to use.

globalThis binding

When lib/index.ts is first imported, it calls Object.assign(globalThis, { ioc }) after constructing the container. This means the ioc object is also available as a global variable in any module that runs in the same process — useful for REPL debugging or for modules that cannot easily import from backtest-kit directly.
// After importing backtest-kit anywhere in the process:
const signal = await globalThis.ioc.signalDbService.findByContext(
  "TRXUSDT",
  "jan_2026_strategy",
  "ccxt-exchange"
);
Prefer the named import { ioc } from "backtest-kit" in application code. The globalThis.ioc binding is a convenience for debugging and is not part of the public API contract.

Build docs developers (and LLMs) love