JavaScript Worker

This guide shows you how to build a Priostack job worker in JavaScript/Node.js using the built-in fetch API (Node 18+).

Prerequisites

  • Node.js 18 or later (built-in fetch API)
  • A Priostack API key set as PRIOSTACK_API_KEY environment variable

Complete Worker Implementation

// worker.mjs — Priostack Job Worker (Node.js / ESM)
"use strict";

const BASE_URL = "https://api.priostack.com";
const WORKER_TYPE = "payment-processor";
const MAX_JOBS = 5;
const POLL_TIMEOUT_MS = 30_000;

const apiKey = process.env.PRIOSTACK_API_KEY;
if (!apiKey) {
  console.error("PRIOSTACK_API_KEY environment variable is required");
  process.exit(1);
}

const headers = {
  "X-API-Key": apiKey,
  "Content-Type": "application/json",
};

// ── API helpers ────────────────────────────────────────────────

async function activateJobs() {
  const resp = await fetch(`${BASE_URL}/api/v1/jobs/activate`, {
    method: "POST",
    headers,
    body: JSON.stringify({
      type: WORKER_TYPE,
      maxJobsToActivate: MAX_JOBS,
      requestTimeout: POLL_TIMEOUT_MS,
      fetchVariables: ["orderId", "amount", "currency"],
    }),
    signal: AbortSignal.timeout(35_000),
  });

  if (!resp.ok) {
    const text = await resp.text();
    throw new Error(`Activate failed ${resp.status}: ${text}`);
  }
  const data = await resp.json();
  return data.jobs ?? [];
}

async function completeJob(jobKey, variables) {
  const resp = await fetch(`${BASE_URL}/api/v1/jobs/${jobKey}/complete`, {
    method: "POST",
    headers,
    body: JSON.stringify({ variables }),
    signal: AbortSignal.timeout(10_000),
  });
  if (!resp.ok) {
    const text = await resp.text();
    throw new Error(`Complete failed ${resp.status}: ${text}`);
  }
}

async function failJob(jobKey, retries, errorMessage) {
  await fetch(`${BASE_URL}/api/v1/jobs/${jobKey}/fail`, {
    method: "POST",
    headers,
    body: JSON.stringify({
      retries: Math.max(0, retries - 1),
      errorMessage,
      retryBackoff: 5000,
    }),
    signal: AbortSignal.timeout(10_000),
  });
}

async function throwBpmnError(jobKey, errorCode, errorMessage) {
  await fetch(`${BASE_URL}/api/v1/jobs/${jobKey}/error`, {
    method: "POST",
    headers,
    body: JSON.stringify({ errorCode, errorMessage }),
    signal: AbortSignal.timeout(10_000),
  });
}

// ── Business logic ─────────────────────────────────────────────

async function processPayment(job) {
  const { orderId, amount, currency = "EUR" } = job.variables ?? {};
  console.log(`Processing payment for order ${orderId}, amount ${amount} ${currency}`);

  // TODO: Call your actual payment gateway here
  // const intent = await stripe.paymentIntents.create({ amount: Math.round(amount * 100), currency });

  // Simulate async processing
  await new Promise((resolve) => setTimeout(resolve, 100));

  return {
    transactionId: `txn_${orderId}_${Date.now()}`,
    paymentStatus: "success",
    processedAt: new Date().toISOString(),
  };
}

// ── Worker loop ────────────────────────────────────────────────

async function handleJob(job) {
  const { key: jobKey, retries = 3 } = job;
  try {
    const result = await processPayment(job);
    await completeJob(jobKey, result);
    console.log(`Job ${jobKey} completed successfully`);
  } catch (err) {
    console.error(`Job ${jobKey} failed:`, err.message);
    try {
      await failJob(jobKey, retries, err.message);
    } catch (failErr) {
      console.error(`Failed to report job failure:`, failErr.message);
    }
  }
}

let running = true;

process.on("SIGTERM", () => { console.log("SIGTERM received. Shutting down..."); running = false; });
process.on("SIGINT",  () => { console.log("SIGINT received. Shutting down...");  running = false; });

async function main() {
  console.log(`Worker started. Polling for jobs of type "${WORKER_TYPE}"...`);

  while (running) {
    try {
      const jobs = await activateJobs();
      for (const job of jobs) {
        console.log(`Activated job ${job.key} (instance ${job.processInstanceKey})`);
        // Process jobs concurrently
        handleJob(job).catch((err) => console.error("Unhandled job error:", err));
      }
    } catch (err) {
      if (!running) break;

      if (err.name === "TimeoutError") {
        // Long poll timed out — no jobs available, loop again
        continue;
      }

      console.error("Worker error:", err.message, "— retrying in 5s");
      await new Promise((resolve) => setTimeout(resolve, 5_000));
    }
  }

  console.log("Worker stopped.");
}

main().catch((err) => {
  console.error("Fatal:", err);
  process.exit(1);
});

Running the Worker

# Set your API key
export PRIOSTACK_API_KEY=ps_live_your_key_here

# Run with Node.js (ESM)
node worker.mjs

# Or with tsx (TypeScript)
npx tsx worker.ts

# Output:
# Worker started. Polling for jobs of type "payment-processor"...
# Activated job 2251799813685290 (instance 2251799813685281)
# Processing payment for order ORD-001, amount 149.99 EUR
# Job 2251799813685290 completed successfully

TypeScript Version

To use TypeScript, add type annotations:

interface Job {
  key: number;
  type: string;
  processInstanceKey: number;
  variables: Record<string, unknown>;
  retries: number;
  deadline: number;
}

interface ActivateResponse {
  jobs: Job[];
}

async function activateJobs(): Promise<Job[]> {
  // same implementation, typed
}

Dockerfile

FROM node:20-alpine
WORKDIR /app
COPY worker.mjs .
CMD ["node", "worker.mjs"]

Production Tips

ConcernRecommendation
ConcurrencyUse Promise.all or a worker pool (e.g., piscina) to process multiple jobs in parallel within a single process.
Graceful shutdownTrack in-flight job promises and await them before exiting on SIGTERM.
Error classificationDistinguish network errors (retry) from business errors (throw BPMN error) from programming errors (log and alert).
MonitoringUse --inspect for debugging. Export metrics to Prometheus using prom-client.