hg-job-sdk
v0.1.0
Published
Client for the Hoogaaa job platform (job-management-service): register jobs, run them from Kafka or HTTP, report to JMS.
Maintainers
Readme
hg-job-sdk
Run a service's background jobs on the Hoogaaa job platform (job-management-service, "JMS"). The service writes only the job's work. The platform handles the schedule, the single fire across replicas, hand-over (Kafka or signed HTTP), execution history, per-item records, retries and dead letters.
Plain JavaScript (CommonJS), Node 18+ (uses global fetch). kafkajs is an optional peer dependency
that Kafka mode needs. The package holds no URL, key or broker address. Each service passes its own from its environment.
Install
Public npm package (no token needed). It holds no URL, key or broker: each service passes its own from its environment.
npm i hg-job-sdk kafkajsPublishing a new version
cd sdk
npm version patch # or minor; bump on every change
npm publish # runs the tests first (prepublishOnly)How it fits
JMS (Postgres) ◄── register / report ── your service (hg-job-sdk)
▲ due runs ▲
│ │ Kafka <service>.<job>.execute
platform scheduler (cron-job, CRON_ROLE=platform-scheduler, 2 replicas, one leader)
│ or HTTP POST + X-JMS-Signature- The owner creates the service once and gets its API key (and, for HTTP mode, its webhook secret):
node dist/platform/cli.js create-service rooms-track "Rooms Track"inside the JMS pod. - The service calls
register()for each job. Atstart()this upserts the job and its schedule in JMS. - When a run is due, the scheduler leader creates one execution (unique per job and scheduled time) and hands it over.
- The SDK runs your handler. It reports
running, progress, items (batched 50 at a time) andsuccessorfailure.
Example: Rooms Track
// backend/src/jobs/index.js
const { create } = require("hg-job-sdk");
const jobs = create({
service: "rooms-track",
jmsUrl: process.env.ROOMS_TRACK_JMS_URL, // e.g. http://job-management-service:4310/api/v1/job-management
apiKey: process.env.ROOMS_TRACK_JMS_API_KEY, // jk_rooms-track_… (Secrets Manager)
kafkaBrokers: process.env.ROOMS_TRACK_KAFKA_BROKERS,
});
jobs.register("rent-reminders", { name: "Rent reminders", schedule: "0 10 * * *", timezone: "Asia/Kolkata" });
jobs.handle("rent-reminders", async (run) => {
const bills = await billRepo.findDueTomorrow();
let sent = 0;
for (const [i, bill] of bills.entries()) {
try {
await notify(bill);
sent += 1;
run.item(`bill:${bill.id}`, "success", { tenantId: bill.tenant_id });
} catch (e) {
run.item(`bill:${bill.id}`, "failed", { reason: e.message });
}
run.progress(((i + 1) / bills.length) * 100);
}
return { bills: bills.length, sent }; // saved as the execution's result summary
});
jobs.start();HTTP mode (no Kafka in the service)
const jobs = create({ service: "rooms-track", jmsUrl, apiKey, webhookSecret: process.env.ROOMS_TRACK_JMS_WEBHOOK_SECRET });
jobs.register("monthly-bills", { schedule: "5 0 1 * *", mode: "http", httpUrl: "https://api.roomstrack.in/internal/jobs/monthly-bills" });
jobs.handle("monthly-bills", async (run) => ({ generated: await generateBills() }));
app.post("/internal/jobs/monthly-bills",
express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } }), // keep the exact bytes that were signed
jobs.httpHandler());
await jobs.start();httpHandler() checks X-JMS-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256(secret, "<t>.<body>")>.
It rejects a request with a bad signature or one older than 5 minutes (401). A good request gets 202 at once, and the job runs in the background.
API
| Call | What it does |
|---|---|
| create({ service, jmsUrl, apiKey, kafkaBrokers?, groupId?, webhookSecret?, retries = 3, retryDelayMs = 2000, logger? }) | One client per service process. groupId defaults to <service>-worker. |
| register(jobKey, { schedule?, timezone?, mode?, httpUrl?, name?, description?, enabled? }) | Declares the job. schedule is a 5-field cron. Without one the job only runs through Run now. Sent at start(). |
| handle(jobKey, async (run) => summary) | The work. run.progress(percent, text?), run.item(key, "success"\|"skipped"\|"failed", { reason?, ...metadata }), plus run.executionId, run.scheduledFor, run.triggerType and run.attempt. |
| start() | Registers the jobs, creates the topics and consumes <service>.<jobKey>.execute. |
| httpHandler() | Express handler for HTTP-mode jobs. |
| runNow(jobKey) | Queues a manual execution. The scheduler hands it over within ~15 s. |
| stop() | Disconnects. |
Guarantees and behaviour
- One fire per scheduled time. JMS refuses a second execution for the same (job, scheduled time). The scheduler is leader-elected.
- Skip if done. Before it runs, the SDK reads the execution. If the execution is already
successorfailed(a repeated message), the SDK skips it. If it isrunning(a crash mid-run), the SDK runs it again, so handlers should be safe to repeat (userun.itemkeys or your own flags). - Retries. A throwing handler is tried 1 +
retriestimes, waitingretryDelayMs × attemptbetween tries. After the last try the SDK reportsfailureand publishes the message plus the error to<service>.dead. - JMS down. Reports queue in memory and retry with backoff (up to 30 s), in order, until JMS answers. The job itself never waits on JMS. Reports still queued are lost only if the process exits first.
- Missed runs. If the scheduler was down, only the latest missed run fires, and only if it is less than 10 minutes old (
JMS_SCHEDULER_CATCH_UP_MINUTES).
Tests
cd sdk && npm test # node --test against a fake JMS: success + batched items, skip-if-done, retries, JMS down, HMAC