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 Property | Description |
|---|---|
ctx.queue | Broker capability (pushing, claiming, acknowledging jobs) |
ctx.locks | Distributed Lock manager (heartbeats, mutually exclusive tasks) |
ctx.records | Key-Value capability (durable storage for metadata or histories) |
ctx.cache | Distributed two-tier caching store |
ctx.ratelimit | Atomic counter capabilities |
ctx.events | Internal pub/sub event bus |
ctx.logger | Engine-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.
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.
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().
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()
}