OqronKitOqronKit

Custom Modules

How to build and register your own custom modules in OqronKit

Custom Modules

OqronKit is designed to be fully extensible. All of the built-in modules (Queue, Worker, Cron, Schedule, Cache, Webhook, RateLimiter) are built on top of the exact same public internal APIs that you can use to build your own custom engines.

You can author a custom module by implementing the IOqronModule interface and registering it using the defineModule function.

The Module Lifecycle

Every OqronKit module follows a strict lifecycle managed by the global ModuleRegistry. When you call await oqron.start(), Oqron iterates over all registered modules and calls their lifecycle hooks.

export interface IOqronModule {
  readonly name: string;
  enabled: boolean;

  init(): Promise<void>;
  start(): Promise<void>;
  stop(): Promise<void>;

  enable(): Promise<void>;
  disable(): Promise<void>;
}

The Module Context

When Oqron instantiates your custom engine, it injects a ModuleContext into your class constructor. This is your gateway to the OqronKit architecture, giving you direct access to the Backend capabilities (queues, locks, key-value stores) without needing to worry about whether the user configured Redis, Postgres, or Memory.

Context PropertyDescription
ctx.queueBroker capability (pushing, claiming, acknowledging jobs)
ctx.locksDistributed Lock manager (heartbeats, mutually exclusive tasks)
ctx.recordsKey-Value capability (durable storage for metadata or histories)
ctx.cacheDistributed two-tier caching store
ctx.ratelimitAtomic counter capabilities
ctx.eventsInternal pub/sub event bus
ctx.loggerEngine-level logging

Step-by-Step Example

Let's build a custom module that acts as a simple background "Heartbeat Monitor" that logs a ping every 5 seconds.

1. Define the Engine Class

First, create a class that implements IOqronModule and accepts ModuleContext.

modules/ping-engine.ts
import { IOqronModule, ModuleContext } from "oqronkit";

export class PingEngine implements IOqronModule {
  public readonly name = "ping-monitor";
  public enabled = true;

  private intervalId?: NodeJS.Timeout;

  constructor(private ctx: ModuleContext) {}

  async init() {
    this.ctx.logger.info("Ping Engine initializing...");
  }

  async start() {
    this.ctx.logger.info("Ping Engine started!");

    // Start our background loop
    this.intervalId = setInterval(() => {
      if (this.enabled) {
        this.ctx.logger.info(`Ping from Node ${this.ctx.nodeId}`);
      }
    }, 5000);
  }

  async stop() {
    this.ctx.logger.info("Ping Engine stopping...");
    if (this.intervalId) {
      clearInterval(this.intervalId);
    }
  }

  async enable() {
    this.enabled = true;
  }

  async disable() {
    this.enabled = false;
  }
}

2. Register the Module

Once you have your engine class, use defineModule to expose it to OqronKit. defineModule automatically registers it into Oqron's ModuleRegistry.

modules/ping-module.ts
import { defineModule } from "oqronkit";
import { PingEngine } from "./ping-engine.js";

export const pingModuleHandle = defineModule({
  name: "ping-monitor",
  engine: PingEngine,

  // Oqron uses this to determine if the module should be started.
  // You can check an environment variable or verify if the user
  // registered any definitions for your module.
  hasDefinitions: (environment) => {
    return true;
  },
});

3. Boot the Module

Because you called defineModule, Oqron instantly knows about your custom module. All you need to do is ensure the file is imported before you call oqron.start().

index.ts
import { Oqron } from "oqronkit";

// Import your module so `defineModule` executes!
import "./modules/ping-module.js";

const oqron = new Oqron({
  mode: "redis",
  redis: "redis://localhost:6379",
});

// Oqron will automatically instantiate PingEngine and call init() and start()
await oqron.start();

Advanced: Using Capabilities

You can build extremely complex distributed systems by leveraging the ModuleContext. For example, you can acquire distributed locks to ensure your custom module only runs as a single leader across your cluster:

async start() {
  // Use the built-in leader election capability
  const leader = this.ctx.leader('ping-leader-lock')

  leader.on('promoted', () => {
    this.ctx.logger.info("I am the cluster leader! Starting ping loop...")
    // Start loop
  })

  leader.on('demoted', () => {
    this.ctx.logger.warn("Lost leadership. Stopping loop...")
    // Stop loop
  })

  // Attempt to acquire leadership in the background
  await leader.start()
}

On this page