@eddyq/nestjs

中文文档
0.2.0发布于 2026/11/05MITTypeScriptCommonJS零依赖构建来源已验证近 30 天下载 2073

NestJS module for eddyq — decorators and DI for the eddyq job queue

原始使用文档
文档目录16 节
  1. Backends supported by forRoot
  2. Quick start
  3. Module configuration
  4. forRoot(options)
  5. forRootAsync(options) — for DI-sourced config
  6. Redis-only forRoot
  7. Multi-backend forRoot — webhooks on Redis, payments on Post…
  8. Error handling
  9. Cooperative cancellation
  10. Admin, stats, and schedules
  11. Cron schedules
  12. Transactional enqueue
  13. Migrations
  14. Dashboard (Wakeboard)
  15. Requirements
  16. License

@eddyq/nestjs

NestJS module for eddyq — a Rust job queue that runs on Postgres, Redis, or both, with native Node bindings.

pnpm add @eddyq/queue @eddyq/nestjs

npm has a long-standing bug with optionalDependencies + lockfiles that breaks packages shipping prebuilt binaries. Use pnpm or yarn.

Backends supported by forRoot

ShapePassDI returnsBest for
Postgres onlydatabaseUrlEddyqTransactional enqueue, durable batches
Redis onlyredis: { url, line? }EddyqRedisHigh-throughput, ephemeral, low latency
Multi-backendboth + queues: [...] + defaultProviderEddyqAppPer-queue routing in one app

The DI token (@InjectEddyq()) returns whichever shape your forRoot config implies. @InjectQueue('foo') returns a QueueHandle regardless — it routes to the right backend transparently in the multi-backend case.

Run the dashboard. @eddyq/wakeboard mounts a UI under your Nest app (/wakeboard by default) that works against any backend shape — stats, queues, groups, schedules, job inspection, pause/resume, cancel. Stop writing one-off admin endpoints. See "Dashboard (Wakeboard)" below.

Quick start

// app.module.ts
import { Module } from "@nestjs/common";
import { EddyqModule } from "@eddyq/nestjs";
import { EmailProcessor } from "./email.processor";

@Module({
  imports: [
    EddyqModule.forRoot({
      databaseUrl: process.env.DATABASE_URL!,
      workerConcurrency: 20,
      subscribeTo: ["default", "urgent"],
    }),
  ],
  providers: [EmailProcessor],
})
export class AppModule {}
// email.processor.ts
import { Processor, JobHandler, type JobCall } from "@eddyq/nestjs";

@Processor()
export class EmailProcessor {
  @JobHandler("send.email")
  async send({ payload, id, attempt }: JobCall) {
    // throw to retry with exponential backoff.
    // throw new CancelError(...) to fail permanently.
    // throw new RetryError(..., { delayMs }) to retry at a specific delay.
    await sendgrid.send(payload);
  }
}
// some.controller.ts — enqueueing from a request handler
import { Controller, Post, Body } from "@nestjs/common";
import { InjectEddyq, type Eddyq } from "@eddyq/nestjs";

@Controller("notify")
export class NotifyController {
  constructor(@InjectEddyq() private readonly queue: Eddyq) {}

  @Post()
  async fanout(@Body() body: { to: string; subject: string }) {
    const r = await this.queue.enqueue("send.email", body, {
      uniqueKey: `${body.to}:${Date.now()}`,
      priority: 5,
    });
    return { jobId: r.id };
  }
}

That's the whole surface. Start Nest normally (nest start) — on onApplicationBootstrap the module scans every provider for @Processor() + @JobHandler(kind), registers each as a worker, and starts the runtime. On shutdown it drains in-flight jobs (default 30s grace) and closes the pool.

Module configuration

forRoot(options)

EddyqModule.forRoot({
  databaseUrl: "postgres://…",

  // Forwarded to `Eddyq.connect` — pool sizing + migration line.
  connectOptions: { maxConnections: 20, line: "main" },

  // Worker runtime (ignored if you have no @JobHandler providers).
  workerConcurrency: 20,            // default 10
  subscribeTo: ["default"],         // default ["default"]
  gracefulShutdownMs: 30_000,       // default 30_000

  // Lifecycle knobs.
  autoStart: true,                  // default true — false = register handlers only
  skipMigrationCheck: false,        // default false — match core's deploy-step guard
  runMigrations: false,             // default false — migrations are a deploy step

  // Cron schedules declared in code. Reconciled at boot; see "Cron schedules".
  schedules: [
    { name: "daily-report", cronExpr: "0 0 8 * * *", kind: "report.generate", payload: {} },
  ],

  // Worker-runtime tuning. Defaults are sensible — only tune with a measured reason.
  tuning: {
    completedRetentionSecs: 3600,    // 1h instead of 24h for a high-volume queue
    staleAfterMs:           300_000, // long handlers — bump from 60s
  },
});

forRootAsync(options) — for DI-sourced config

EddyqModule.forRootAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: (cfg: ConfigService) => ({
    databaseUrl: cfg.getOrThrow("DATABASE_URL"),
    workerConcurrency: cfg.get<number>("QUEUE_CONCURRENCY") ?? 10,
  }),
});

Redis-only forRoot

EddyqModule.forRoot({
  redis: {
    url: process.env.REDIS_URL!,
    line: "main",           // hash-tag namespace; distinct lines isolate keyspaces
  },
  workerConcurrency: 20,
  subscribeTo: ["webhooks", "fanout"],
});

Same Nest surface — @InjectEddyq() returns an EddyqRedis, processors and @InjectQueue work identically. The Redis backend skips migrations (runMigrations/skipMigrationCheck are ignored) and ignores the connectOptions pool sizing (managed by the Redis connection manager).

Multi-backend forRoot — webhooks on Redis, payments on Postgres

EddyqModule.forRoot({
  databaseUrl: process.env.DATABASE_URL!,
  redis:       { url: process.env.REDIS_URL!, line: "main" },
  queues: [
    { name: "webhooks", provider: "redis" },
    { name: "payments", provider: "postgres" },
  ],
  defaultProvider: "postgres",   // unrouted queue names land here
});

Inside one Nest app:

  • @InjectQueue('webhooks') returns a handle that enqueues onto Redis.
  • @InjectQueue('payments') returns a handle that enqueues onto Postgres.
  • A single handler registered via @JobHandler('process') services jobs of that kind from either backend — the backend that actually fetches it invokes the handler. Use payload.queue (or any payload field) to branch if the kinds are shared.
  • enqueueBatch is Postgres-only — calling it on a Redis-routed handle throws at runtime with a clear message.
  • enqueueInTx (and the SELECT eddyq_enqueue(...) SQL path) only makes sense for Postgres-routed queues. Calling it on a Redis-routed queue is a programmer error.

Full reference app: examples/nestjs-mixed.

Error handling

Handlers can throw three things:

ThrowEffect
Any ErrorJob retries with exponential backoff until maxAttempts
new CancelError(msg)Mark failed permanently — no more retries
new RetryError(msg, { delayMs: 60_000 })Retry at a specific delay (e.g. honor Retry-After)
import { CancelError, RetryError } from "@eddyq/nestjs";

@JobHandler("webhook.call")
async call({ payload }: JobCall) {
  const r = await fetch(payload.url, { method: "POST" });
  if (r.status === 429) {
    const after = Number(r.headers.get("retry-after")) * 1000;
    throw new RetryError("rate limited", { delayMs: after });
  }
  if (r.status === 400) throw new CancelError("bad request — no retry");
  if (!r.ok) throw new Error(`HTTP ${r.status}`);
}

Cooperative cancellation

On app.close() eddyq broadcasts .abort() to every in-flight handler. Destructure signal off the JobCall and pass it to anything that accepts an AbortSignal:

@JobHandler("download")
async download({ payload, signal }: JobCall) {
  const r = await fetch(payload.url, { signal });
  return await r.json();
}

Admin, stats, and schedules

The raw Eddyq client is exported globally — inject it anywhere:

import { InjectEddyq, type Eddyq } from "@eddyq/nestjs";

@Injectable()
export class QueueAdmin {
  constructor(@InjectEddyq() private readonly q: Eddyq) {}

  async pauseIntegrations() {
    await this.q.pauseQueue("integrations");
  }

  async dashboard() {
    const [stats, queues, groups] = await Promise.all([
      this.q.getStats(),
      this.q.listNamedQueues(),
      this.q.listGroups(),
    ]);
    return { stats, queues, groups };
  }
}

See @eddyq/queue for the full method list: enqueue, cancel, getStats, listJobs, setGroupConcurrency, setGroupRate, setQueueConcurrency, setQueueTimeout, addSchedule, removeSchedule, and more.

Cron schedules

Declare them in forRoot. The list is reconciled at boot — added entries are upserted, removed ones are deleted. 6-field cron (sec min hour dom month dow).

import { EddyqModule, type ScheduleDeclaration } from "@eddyq/nestjs";

const schedules: ScheduleDeclaration[] = [
  { name: "daily-report", cronExpr: "0 0 8 * * *", kind: "report.generate", payload: { scope: "daily" } },
];

EddyqModule.forRoot({ databaseUrl: process.env.DATABASE_URL!, schedules });

Single-leader election with skip-missed semantics, so N replicas never double-fire and a long outage doesn't replay every missed tick.

Transactional enqueue

The main reason to use a Postgres-backed queue over Redis: your domain write and your job enqueue commit atomically. If the commit fails, no invoice and no follow-up job. If the job is queued, the invoice definitely exists.

eddyq exposes this via a SQL function (eddyq_enqueue) that runs inside your own DB client's transaction — Prisma, Drizzle, Knex, pg, whatever you already use:

// Prisma
await prisma.$transaction(async (tx) => {
  const invoice = await tx.invoice.create({ data });
  await tx.$executeRaw`
    SELECT eddyq_enqueue(
      'send.receipt',
      ${JSON.stringify({ invoiceId: invoice.id })}::jsonb
    )
  `;
});

// Drizzle
await db.transaction(async (tx) => {
  const [invoice] = await tx.insert(invoices).values(data).returning();
  await tx.execute(sql`
    SELECT eddyq_enqueue(
      'send.receipt',
      ${JSON.stringify({ invoiceId: invoice.id })}::jsonb
    )
  `);
});

// pg
await client.query("BEGIN");
const { rows } = await client.query("INSERT INTO invoices (...) RETURNING id", [...]);
await client.query(
  `SELECT eddyq_enqueue('send.receipt', $1::jsonb)`,
  [JSON.stringify({ invoiceId: rows[0].id })],
);
await client.query("COMMIT");

The full signature:

eddyq_enqueue(
  p_kind          text,
  p_payload       jsonb,
  p_queue         text        DEFAULT 'default',
  p_priority      smallint    DEFAULT 0,
  p_max_attempts  integer     DEFAULT 3,
  p_scheduled_at  timestamptz DEFAULT now(),
  p_unique_key    text        DEFAULT null,
  p_group_key     text        DEFAULT null,
  p_tags          text[]      DEFAULT '{}',
  p_metadata      jsonb       DEFAULT '{}'
) RETURNS bigint  -- the new job id, or NULL on unique_key conflict

And a bulk variant eddyq_enqueue_many(jsonb) taking a JSONB array of job objects, returning { inserted: N, skipped: N }.

The SQL path mirrors the Rust path exactly — same INSERT, same pattern-rule materialization for group_key, same pg_notify. An integration test in eddyq-core enqueues the same job via both paths and asserts identical rows, so they can't drift.

When to use which:

  • this.queue.enqueue(...) via @InjectEddyq — simple fire-and-forget, no outer transaction. The 90% case.
  • SELECT eddyq_enqueue(...) inside your ORM's transaction — when the job must be atomic with a domain write.

Migrations

Migrations are a deploy-step concern, not a runtime one. The default start() path refuses to boot if any registered migration is unapplied — this prevents rolling deploys from running workers against a stale schema.

Three ways to apply migrations, in order of recommendation:

  1. A one-shot deploy job (e.g. Kubernetes Job, ECS one-off task) that calls Eddyq.connect(url).then((q) => q.migrate()) before workers roll.
  2. The eddyq CLI: eddyq migrate run --database-url $DATABASE_URL.
  3. runMigrations: true in forRoot — applies on bootstrap. Only use this for local dev or tests; it serializes every replica's boot behind schema migration.

Set skipMigrationCheck: true if you manage migrations out-of-band and want to silence the boot-time guard.

Dashboard (Wakeboard)

@eddyq/wakeboard mounts a complete admin UI under your Nest app. It auto-detects which backend(s) you wired in forRoot and renders the right sections — stats, queues, groups, schedules, paginated job inspection, pause/resume, cancel, set concurrency, set rate limit.

import { Module } from "@nestjs/common";
import { EddyqModule } from "@eddyq/nestjs";
import { EddyqWakeboardModule } from "@eddyq/wakeboard";

@Module({
  imports: [
    EddyqModule.forRoot({
      databaseUrl: process.env.DATABASE_URL!,
      redis:       { url: process.env.REDIS_URL!, line: "main" },
      queues: [
        { name: "webhooks", provider: "redis" },
        { name: "payments", provider: "postgres" },
      ],
      defaultProvider: "postgres",
    }),
    EddyqWakeboardModule.forRoot({
      mountPath: "/wakeboard",
      auth: { password: process.env.WAKEBOARD_PASSWORD! },
    }),
  ],
})
export class AppModule {}

Hit http://localhost:3000/wakeboard and you're looking at live queue state. The REST API under /wakeboard/api/* takes an optional ?provider=postgres|redis query param on every endpoint when both backends are configured — wire it up to whatever monitoring you want. Use this. It saves you a thousand lines of admin scaffolding.

Requirements

  • Node ≥ 20 (NestJS 12 itself needs ≥ 20.19 or ≥ 22.12)
  • PostgreSQL ≥ 14 (if using the Postgres backend)
  • Redis ≥ 7 (if using the Redis backend — Redis Functions require 7.0+)
  • @nestjs/common and @nestjs/core ^10, ^11, or ^12 (peer deps)
  • @eddyq/queue same minor version (peer dep)

License

MIT

版本与发布记录

@eddyq/nestjs 共发布 27 个版本,首次发布于 2026/04/26;过去 12 个月发布了 27 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.2.0latest2026/11/059 天前
0.1.142026/10/3015 天前
0.1.132026/10/2916 天前
0.1.122026/10/2916 天前
0.1.112026/10/2817 天前
0.1.102026/10/2420 天前

关于 @eddyq/nestjs 的常见问题

@eddyq/nestjs 是什么?

NestJS module for eddyq — decorators and DI for the eddyq job queue

如何安装 @eddyq/nestjs?

在项目目录运行 npm install @eddyq/nestjs,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:@eddyq/queue(0.2.0)、@nestjs/core(^10 || ^11 || ^12)、@nestjs/common(^10 || ^11 || ^12)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@eddyq/nestjs 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@eddyq/nestjs 支持 ESM 还是 CommonJS?

以 CommonJS 形式发布,可以直接 require();在 ES Module 项目中也可以用 import 引入(Node.js 会把 module.exports 作为默认导出)。

@eddyq/nestjs 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=20。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@eddyq/nestjs 可以商用吗?

@eddyq/nestjs 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@eddyq/nestjs 的下载量有多少?

根据 npm 官方下载统计,@eddyq/nestjs 在 2026/10/13 – 2026/11/11 这 30 天内被下载 2,073 次,日均约 69 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@eddyq/nestjs 还在维护吗?

最新版本 0.2.0 发布于 2026/11/05(9 天前);至今共发布 27 个版本,其中过去 12 个月发布了 27 个;首次发布于 2026/04/26。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。