npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@munchi_oy/printer-js-sdk

v1.0.5

Published

Munchi JavaScript printer SDK

Downloads

393

Readme

@munchi_oy/printer-js-sdk

Shared JavaScript printer SDK used by Munchi apps.

Release

Create a release with version bump, build, commit, git tag, and remote push:

pnpm release 1.0.3

Create and publish a GitHub release after pushing tags:

pnpm release 1.0.3 --publish

Other publish modes:

pnpm release 1.0.3 --publish=custom
pnpm release 1.0.3 --publish=file

Queue Options

PrinterQueueController

In-memory sequential queue for lightweight use-cases. Jobs are not recoverable after app restart.

import { PrinterQueueController } from '@munchi_oy/printer-js-sdk';

const queue = new PrinterQueueController();
await queue.enqueue(async () => {
  await printSomething();
});

DurablePrinterQueueController

Persistent queue with retries, dead-letter support, idempotency keys, and restart recovery for in_progress jobs.

import {
  DurablePrinterQueueController,
  type DurablePrinterQueueStorage,
} from '@munchi_oy/printer-js-sdk';
import AsyncStorage from '@react-native-async-storage/async-storage';

type PrintJobType = 'receipt' | 'kitchen' | 'refund';
type PrintPayload = { orderId: string; printerId: string };

const storage: DurablePrinterQueueStorage = {
  getItem: (key) => AsyncStorage.getItem(key),
  setItem: (key, value) => AsyncStorage.setItem(key, value),
};

const queue = new DurablePrinterQueueController<PrintJobType, PrintPayload>({
  storage,
  handlers: {
    receipt: async ({ payload }) => {
      await printReceipt(payload.orderId, payload.printerId);
    },
    kitchen: async ({ payload }) => {
      await printKitchen(payload.orderId, payload.printerId);
    },
    refund: async ({ payload }) => {
      await printRefund(payload.orderId, payload.printerId);
    },
  },
  defaultMaxAttempts: 6,
  baseRetryDelayMs: 1000,
  maxRetryDelayMs: 30000,
  leaseTimeoutMs: 120000,
  yieldEveryJobs: 1,
  yieldDelayMs: 0,
  isRetryableError: (error) => {
    const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
    return (
      message.includes('timeout') ||
      message.includes('network') ||
      message.includes('disconnected')
    );
  },
  onJobFailed: ({ job, errorMessage }) => {
    console.warn(`Print job ${job.id} failed: ${errorMessage}`);
  },
});

await queue.start();
await queue.enqueue({
  type: 'receipt',
  payload: { orderId: 'order_123', printerId: 'printer_1' },
  idempotencyKey: 'receipt:order_123:printer_1',
});

Queue responsiveness controls

For JS-thread workloads, these options help reduce burst lag:

  1. yieldEveryJobs: yield back to event loop after N processed jobs.
  2. yieldDelayMs: delay used during each cooperative yield.

Recommended baseline for UI safety:

  1. yieldEveryJobs: 1
  2. yieldDelayMs: 0

Persistence Model

DurablePrinterQueueController persists queue state through a pluggable storage interface:

interface DurablePrinterQueueStorage {
  getItem(key: string): Promise<string | null> | string | null;
  setItem(key: string, value: string): Promise<void> | void;
}

What this means:

  1. Queue processing happens in memory at runtime.
  2. Job state is snapshotted to your storage adapter on each state transition.
  3. On startup, jobs are restored from storage.
  4. in_progress jobs with expired lease are recovered back to queued.

Which persistence backend should you use?

  1. SQLite (recommended for restaurant POS): best durability and corruption resilience.
  2. MMKV: fast, good local durability, lighter than SQLite.
  3. AsyncStorage: acceptable for many cases, but less robust under heavy write volume.
  4. In-memory map: testing only, no durability.

Recommended POS Architecture (Strategy + Adapter)

For multiple printer models and transports, use this split:

  1. Queue: controls retry/order/recovery.
  2. Strategy: builds commands from business payload (receipt, kitchen, refund).
  3. Adapter: sends commands to physical transport (Star Bluetooth, Epson Network, Sunmi Bluetooth).

Queue -> Job Handler -> Strategy Registry + Adapter Registry -> Device

Multi-printer example

import {
  DurablePrinterQueueController,
  type DurablePrinterQueueJob,
  type DurablePrinterQueueStorage,
} from '@munchi_oy/printer-js-sdk';

type PrintJobType = 'receipt' | 'kitchen' | 'refund';
type PrinterModel = 'Star' | 'Epson' | 'Sunmi';
type ConnectionType = 'Bluetooth' | 'Network';
type PrintCommands = string | Uint8Array;

type PrinterProfile = {
  printerId: string;
  model: PrinterModel;
  connection: ConnectionType;
  address: string;
};

type PrintPayload = {
  orderId: string;
  printerId: string;
  data: unknown;
};

interface PrinterStrategy {
  build(
    jobType: PrintJobType,
    payload: PrintPayload,
    printer: PrinterProfile
  ): Promise<PrintCommands>;
}

interface PrinterAdapter {
  send(commands: PrintCommands, printer: PrinterProfile): Promise<void>;
}

const strategyRegistry = {
  get(jobType: PrintJobType, model: PrinterModel): PrinterStrategy {
    return resolveStrategy(jobType, model);
  },
};

const adapterRegistry = {
  get(model: PrinterModel, connection: ConnectionType): PrinterAdapter {
    return resolveAdapter(model, connection);
  },
};

async function executePrintJob(
  job: DurablePrinterQueueJob<PrintJobType, PrintPayload>
): Promise<void> {
  const printer = await printerRepo.getById(job.payload.printerId);
  if (!printer) throw new Error(`Printer not found: ${job.payload.printerId}`);

  const strategy = strategyRegistry.get(job.type, printer.model);
  const adapter = adapterRegistry.get(printer.model, printer.connection);
  const commands = await strategy.build(job.type, job.payload, printer);

  await adapter.send(commands, printer);
}

const storage: DurablePrinterQueueStorage = yourStorageAdapter;

const queue = new DurablePrinterQueueController<PrintJobType, PrintPayload>({
  storage,
  handlers: {
    receipt: executePrintJob,
    kitchen: executePrintJob,
    refund: executePrintJob,
  },
  defaultMaxAttempts: 6,
  baseRetryDelayMs: 1000,
  maxRetryDelayMs: 30000,
  leaseTimeoutMs: 120000,
});

await queue.start();

SQLite Adapter Example

You can implement DurablePrinterQueueStorage with any SQLite wrapper. Example shape:

const sqliteStorage: DurablePrinterQueueStorage = {
  async getItem(key) {
    const [result] = await db.executeSql(
      'SELECT value FROM kv_store WHERE key = ? LIMIT 1',
      [key]
    );
    if (result.rows.length === 0) return null;
    return result.rows.item(0).value as string;
  },
  async setItem(key, value) {
    await db.executeSql(
      'INSERT OR REPLACE INTO kv_store (key, value, updated_at) VALUES (?, ?, strftime("%s","now"))',
      [key, value]
    );
  },
};

Suggested table:

CREATE TABLE IF NOT EXISTS kv_store (
  key TEXT PRIMARY KEY,
  value TEXT NOT NULL,
  updated_at INTEGER NOT NULL
);

Strategy and Adapter Helpers

The SDK includes typed helpers so you can standardize strategy and transport wiring across Star, Epson, and Sunmi.

Types you can import

import type {
  PrinterStrategy,
  PrinterAdapter,
} from '@munchi_oy/printer-js-sdk';

Create a reusable job executor

import {
  createPrinterJobExecutor,
  type DurablePrinterQueueJob,
} from '@munchi_oy/printer-js-sdk';

type PrintJobType = 'receipt' | 'kitchen' | 'refund';
type PrinterModel = 'Star' | 'Epson' | 'Sunmi';
type ConnectionType = 'Bluetooth' | 'Network';
type PrintCommands = string | Uint8Array;

type PrinterProfile = {
  printerId: string;
  model: PrinterModel;
  connection: ConnectionType;
  address: string;
};

type PrintPayload = {
  orderId: string;
  printerId: string;
  data: unknown;
};

const executePrintJob = createPrinterJobExecutor<
  PrintJobType,
  PrintPayload,
  PrinterProfile,
  PrintCommands
>({
  findPrinter: async (payload) => {
    return await printerRepo.getById(payload.printerId);
  },
  getStrategy: async (jobType, printer) => {
    return strategyRegistry.get(jobType, printer.model);
  },
  getAdapter: async (printer) => {
    return adapterRegistry.get(printer.model, printer.connection);
  },
  createPrinterNotFoundError: (payload, job) => {
    return new Error(`Printer ${payload.printerId} missing for job ${job.id}`);
  },
});

Queue wiring with the executor

const queue = new DurablePrinterQueueController<PrintJobType, PrintPayload>({
  storage: sqliteStorage,
  handlers: {
    receipt: executePrintJob,
    kitchen: executePrintJob,
    refund: executePrintJob,
  },
  defaultMaxAttempts: 6,
  baseRetryDelayMs: 1000,
  maxRetryDelayMs: 30000,
  leaseTimeoutMs: 120000,
});

await queue.start();
await queue.enqueue({
  type: 'kitchen',
  payload: { orderId: 'o-123', printerId: 'p-1', data: orderDto },
  idempotencyKey: 'kitchen:o-123:p-1:v1',
});

Minimal strategy and adapter example

import type {
  PrinterAdapter,
  PrinterStrategy,
} from '@munchi_oy/printer-js-sdk';

const kitchenStarStrategy: PrinterStrategy<
  'kitchen' | 'receipt' | 'refund',
  PrintPayload,
  PrinterProfile,
  string
> = {
  async build(jobType, payload, printer) {
    if (jobType === 'kitchen') return buildStarKitchenCommands(payload.data);
    if (jobType === 'receipt') return buildStarReceiptCommands(payload.data);
    return buildStarRefundCommands(payload.data);
  },
};

const starBluetoothAdapter: PrinterAdapter<PrinterProfile, string> = {
  async send(commands, printer) {
    await starSdk.sendBluetooth(printer.address, commands);
  },
};

Settings-driven runtime (single queue, multiple printers)

Use createPrinterRuntime when your app has one global queue and many printer settings.

import {
  createPrinterRuntime,
  type ManagedPrinterAdapter,
  type PrinterRuntimePayload,
  type PrinterRuntimeSetting,
} from '@munchi_oy/printer-js-sdk';

type PrintJobType = 'receipt' | 'kitchen' | 'refund';

type PrintPayload = PrinterRuntimePayload & {
  orderId: string;
  data: unknown;
};

type PrinterSetting = PrinterRuntimeSetting & {
  printerModel: 'Star' | 'Epson' | 'Sunmi';
  device: 'Bluetooth' | 'Network';
  printerName: string;
};

const runtime = createPrinterRuntime<
  PrintJobType,
  PrintPayload,
  PrinterSetting,
  string | Uint8Array
>({
  storage: sqliteStorage,
  jobTypes: ['receipt', 'kitchen', 'refund'],
  createAdapter: async (setting): Promise<ManagedPrinterAdapter<string | Uint8Array>> => {
    return adapterFactory.create(setting);
  },
  getStrategy: async (jobType, setting) => {
    return strategyRegistry.get(jobType, setting.printerModel);
  },
  queue: {
    storageKey: 'printer_queue_v1',
    defaultMaxAttempts: 6,
    baseRetryDelayMs: 1000,
    maxRetryDelayMs: 30000,
    leaseTimeoutMs: 120000,
  },
});

await runtime.syncSettings(printerSettingsFromStore);
await runtime.start();

await runtime.enqueue({
  type: 'receipt',
  payload: {
    orderId: 'o-1001',
    printerSettingId: 'setting-1',
    data: orderDto,
  },
  idempotencyKey: 'receipt:o-1001:setting-1:v1',
});

Vendor adapter and strategy wiring

Use the built-in vendor helpers to keep Epson/Star/Sunmi logic modular.

import {
  createVendorAdapterFactory,
  createVendorStrategyResolver,
  createEpsonConnectionManagedAdapter,
  resolveEpsonTarget,
  createStarManagedAdapter,
  createSunmiManagedAdapter,
} from '@munchi_oy/printer-js-sdk';

type Vendor = 'epson' | 'star' | 'sunmi';
type PrintJobType = 'receipt' | 'kitchen' | 'refund';
type PrintCommands = string | Uint8Array;

type PrinterSetting = {
  settingId: string;
  enabled: boolean;
  vendor: Vendor;
  printerId: string;
  printerModel: 'Star' | 'Epson' | 'Sunmi';
  device: 'Bluetooth' | 'Network';
  connection:
    | { kind: 'bluetooth'; address: string; name?: string }
    | { kind: 'tcp'; host: string; port?: number }
    | { kind: 'tcps'; host: string; port?: number }
    | { kind: 'ip'; ip: string; port?: number };
};

type PrintPayload = {
  orderId: string;
  printerSettingId: string;
  data: unknown;
};

const createAdapter = createVendorAdapterFactory<Vendor, PrinterSetting, PrintCommands>({
  epson: async (setting) =>
    createEpsonConnectionManagedAdapter({
      setting,
      drivers: {
        bluetooth: {
          open: async (s) => {
            const target = resolveEpsonTarget(s.connection);
            return epsonBridge.openBluetooth(target, s);
          },
          print: async (session, commands, s) =>
            epsonBridge.printBluetooth(session, commands, s),
          close: async (session, s) => epsonBridge.closeBluetooth(session, s),
        },
        tcp: {
          open: async (s) => {
            const target = resolveEpsonTarget(s.connection);
            return epsonBridge.openTcp(target, s);
          },
          print: async (session, commands, s) =>
            epsonBridge.printTcp(session, commands, s),
          close: async (session, s) => epsonBridge.closeTcp(session, s),
        },
        tcps: {
          open: async (s) => {
            const target = resolveEpsonTarget(s.connection);
            return epsonBridge.openTcp(target, s);
          },
          print: async (session, commands, s) =>
            epsonBridge.printTcp(session, commands, s),
          close: async (session, s) => epsonBridge.closeTcp(session, s),
        },
        ip: {
          open: async (s) => {
            const target = resolveEpsonTarget(s.connection);
            return epsonBridge.openTcp(target, s);
          },
          print: async (session, commands, s) =>
            epsonBridge.printTcp(session, commands, s),
          close: async (session, s) => epsonBridge.closeTcp(session, s),
        },
      },
    }),
  star: async (setting) =>
    createStarManagedAdapter(setting, {
      subscribeConnection: async (s, listener) =>
        starBridge.subscribeConnection(s, listener),
      send: async (commands, s) => starBridge.send(s, commands),
    }),
  sunmi: async (setting) =>
    createSunmiManagedAdapter(setting, {
      start: async (s) => sunmiBridge.startSession(s),
      send: async (commands, s) => sunmiBridge.send(s, commands),
      subscribeConnection: async (s, listener) =>
        sunmiBridge.subscribeConnection(s, listener),
      stop: async (s) => sunmiBridge.stopSession(s),
    }),
});

const getStrategy = createVendorStrategyResolver<
  PrintJobType,
  Vendor,
  PrintPayload,
  PrinterSetting,
  PrintCommands
>({
  receipt: {
    epson: epsonReceiptStrategy,
    star: starReceiptStrategy,
    sunmi: sunmiReceiptStrategy,
  },
  kitchen: {
    epson: epsonKitchenStrategy,
    star: starKitchenStrategy,
    sunmi: sunmiKitchenStrategy,
  },
  refund: {
    epson: epsonRefundStrategy,
    star: starRefundStrategy,
    sunmi: sunmiRefundStrategy,
  },
});

For Epson tcp, tcps, and ip connections, port is optional. When omitted, resolveEpsonTarget returns TCP:<hostOrIp> or TCPS:<host> and lets Epson SDK use its default port behavior.

React Native / React wrapper

Import from @munchi_oy/printer-js-sdk/react for provider + hooks.

import {
  PrinterRuntimeProvider,
  useEnqueuePrint,
  usePrinterConnection,
} from '@munchi_oy/printer-js-sdk/react';

function AppRoot() {
  const settings = usePrinterSettingsStore((state) => state.printerSettings);

  return (
    <PrinterRuntimeProvider runtime={runtime} settings={settings}>
      <YourScreens />
    </PrinterRuntimeProvider>
  );
}

function PrintButton() {
  const enqueuePrint = useEnqueuePrint<
    'receipt' | 'kitchen' | 'refund',
    PrintPayload,
    PrinterSetting
  >();

  const onPress = async () => {
    await enqueuePrint({
      type: 'receipt',
      payload: {
        orderId: 'o-1001',
        printerSettingId: 'setting-1',
        data: orderDto,
      },
      idempotencyKey: 'receipt:o-1001:setting-1:v1',
    });
  };

  return <Button title="Print receipt" onPress={onPress} />;
}

function PrinterStatus({ settingId }: { settingId: string }) {
  const connectionState = usePrinterConnection(settingId);
  return <Text>{connectionState}</Text>;
}

Multi-printer runtime wrapper (isolated queue per printer)

Use this when you want one runtime/queue per settingId while keeping JS implementation.

Supported behavior:

  1. Enqueue routes by payload.printerSettingId.
  2. Each printer setting has its own durable queue storage key.
  3. Queue retention is trimmed per printer (old completed/dead-letter jobs are pruned).
  4. Queue capabilities remain available per printer (enqueue, retries, dead-letter, persistence, cold boot recovery).
import {
  MultiPrinterRuntimeProvider,
  createSettingScopedRuntimeFactory,
  useMultiEnqueuePrint,
  useMultiPrinterConnection,
} from '@munchi_oy/printer-js-sdk/react';

const createRuntime = createSettingScopedRuntimeFactory<
  PrintJobType,
  PrintPayload,
  PrinterSetting,
  string | Uint8Array
>({
  storage: sqliteStorage,
  storageKeyPrefix: 'printer_queue',
  keepCompletedJobsPerPrinter: 10,
  keepDeadLetterJobsPerPrinter: 10,
  jobTypes: ['receipt', 'kitchen', 'refund'],
  createAdapter: async (setting) => adapterFactory.create(setting),
  getStrategy: async (jobType, setting) =>
    strategyRegistry.get(jobType, setting.printerModel),
  queue: {
    defaultMaxAttempts: 6,
    baseRetryDelayMs: 1000,
    maxRetryDelayMs: 30000,
    leaseTimeoutMs: 120000,
    yieldEveryJobs: 1,
    yieldDelayMs: 0,
  },
});

function AppRoot() {
  const settings = usePrinterSettingsStore((state) => state.printerSettings);

  return (
    <MultiPrinterRuntimeProvider settings={settings} createRuntime={createRuntime}>
      <YourScreens />
    </MultiPrinterRuntimeProvider>
  );
}

function PrintButton() {
  const enqueuePrint = useMultiEnqueuePrint<
    'receipt' | 'kitchen' | 'refund',
    PrintPayload,
    PrinterSetting
  >();

  return (
    <Button
      title="Print receipt"
      onPress={async () => {
        await enqueuePrint({
          type: 'receipt',
          payload: {
            orderId: 'o-1001',
            printerSettingId: 'setting-1',
            data: orderDto,
          },
          idempotencyKey: 'receipt:o-1001:setting-1:v1',
        });
      }}
    />
  );
}

function PrinterStatus({ settingId }: { settingId: string }) {
  const connectionState = useMultiPrinterConnection(settingId);
  return <Text>{connectionState}</Text>;
}

Overflow retention behavior

If queue history overflows retention limits:

  1. Older completed jobs are trimmed first.
  2. Older dead_letter jobs are trimmed independently.
  3. Active jobs (queued, in_progress) are never trimmed.

This allows “new success overwrites old success history” behavior while keeping active reliability.

Current limitations (JS runtime mode)

  1. Queue processing still runs on JS runtime, not on a dedicated native worker thread.
  2. Long synchronous work in strategy/adapter code can still cause UI jank.
  3. Very large payloads increase memory pressure and serialization overhead.
  4. Cross-printer global ordering is not guaranteed when using per-printer isolated queues.

Performance roadmap (future)

  1. Move queue worker + transport send to native layer (Swift/Kotlin) while JS remains enqueue/orchestration.
  2. Keep payloads minimal (orderId, printerSettingId) and fetch heavy data late.
  3. Use SQLite-backed storage for durability and write performance.
  4. Add queue telemetry (enqueue latency, handler duration, retry count, queue depth) and alert on thresholds.

Provider durability policy (recommended)

  1. Epson: native queue can be primary; keep JS durable queue for offline persistence and cross-provider consistency.
  2. Star: if using spool APIs with status/retry, native queue can be primary; JS queue still handles app-level persistence and routing.
  3. Sunmi: keep JS durable queue as primary retry/dead-letter mechanism unless native retry/status confirmation is verified.

Consumer Operating Modes (Dynamic Control)

Use queue controls based on app lifecycle and operational intent:

  1. start / stop: full runtime lifecycle (mount/unmount app shell, logout, hard shutdown).
  2. pause / resume: keep runtime alive but temporarily stop job execution.
  3. syncSettings: always run when printer settings change.

Recommended React Native pattern:

import { AppState } from 'react-native';

const queue = runtime.getQueueController();

const subscription = AppState.addEventListener('change', async (state) => {
  if (state === 'active') {
    await runtime.syncSettings(latestSettings);
    await runtime.start();
    queue.resume();
    return;
  }

  if (state === 'background' || state === 'inactive') {
    queue.pause();
  }
});

Notes:

  1. pause is safer than stop for short background transitions.
  2. stop is appropriate when session/app context is no longer valid.
  3. leaseTimeoutMs must be greater than 0. 0 is treated as invalid and falls back to the queue default.

Dead-letter Policy (Restaurant Ready)

Recommended policy for unstable printer connectivity:

  1. Retry locally first. The device that owns the printer connection should execute retries.
  2. Use isRetryableError to retry transient failures only (timeout, disconnected, busy, network jitter).
  3. Move terminal failures to dead-letter quickly (invalid payload, unsupported format, missing mapping).
  4. Expose dead-letter actions in UI: Retry, Retry all, Reroute to another printer.
  5. Send dead-letter telemetry to backend for monitoring/support, not for backend-side printing.
  6. Use onJobFailed for immediate user feedback; it fires only when a job becomes dead_letter.

Example retry classifier:

isRetryableError: (error) => {
  const message =
    error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
  return (
    message.includes('timeout') ||
    message.includes('network') ||
    message.includes('disconnect') ||
    message.includes('busy')
  );
}

Operational Defaults for Restaurants

Choose one profile based on how aggressive recovery should be.

| Profile | Recommended queue values | Use when | | --- | --- | --- | | Balanced | defaultMaxAttempts: 5, baseRetryDelayMs: 1000, maxRetryDelayMs: 8000, leaseTimeoutMs: 5000 | Most stores; good reliability without queue buildup | | Fast recovery | defaultMaxAttempts: 3, baseRetryDelayMs: 500, maxRetryDelayMs: 3000, leaseTimeoutMs: 1 | Real-time kitchen flow where stale in_progress must recover immediately | | High resilience | defaultMaxAttempts: 8, baseRetryDelayMs: 1000, maxRetryDelayMs: 30000, leaseTimeoutMs: 10000 | Unstable network/printer environments where temporary outages are common |

Also recommended for every profile:

  1. Use idempotency key format: <jobType>:<orderId>:<printerId>:<version>.
  2. Keep dead-letter jobs visible in support/settings UI.
  3. Prune old completed/dead-letter jobs with keepCompletedJobs and keepDeadLetterJobs.