Postgres 上のバックグラウンドジョブ:永続キュー、バックオフ付き再試行、cron、サーバーレス用エンドポイント
コード9 ファイルコンテキスト約 696 トークンスキャン合格
インストール
$
genpm add @core/jobs含まれるもの
- src/lib/jobs/ にソースコード(9 ファイル)。 (19.2 KB)
- src/lib/jobs/AGENTS.md に AI ルール、加えて IDE 用のルールファイル。
- .env.example に追加される環境変数: CRON_SECRET。
- @core/db を自動で解決します。
README
このパッケージには README がありません。
約 696 トークン→ src/lib/jobs/AGENTS.md→ .cursor/rules/genpm-core-jobs.mdc
これは AI が src/lib/jobs で作業するときに読む内容そのものです。それ以外はコンテキストに追加されません。
@core/jobs — rules for AI agents
Purpose
Durable background jobs on the same Postgres: queue (FOR UPDATE SKIP LOCKED), retries with exponential backoff,
timeouts, de-duplication keys, cron schedules (5 fields, with timezone) and an HTTP endpoint so serverless hosts can
run due jobs from their cron. No Redis, no extra service. Table owner of jobs and job_schedules.
Map
index.ts— public API:defineJob,enqueue,schedule,unschedule,runDue,runWorker,retryJob,pruneJobs.queue.ts— claim/run logic.registry.ts— in-memory job types.schema.ts— tables.adapters/hono.ts—cronRoutes().adapters/next.ts—cronRoute(GET/POST).
Integration
- Env:
CRON_SECRET(≥ 32 random chars). Generate and apply migrations (seesrc/lib/db/AGENTS.md). - Define jobs in one module imported by every process (web and worker), e.g.
src/jobs.ts:
Enqueue:import { z } from 'zod'; import { defineJob } from './lib/jobs/index.ts'; export const sendWelcome = defineJob('email.welcome', z.object({ userId: z.string() }), async ({ userId }, { signal }) => { /* … */ });await sendWelcome.enqueue({ userId }, { dedupeKey:welcome:${userId}}). - Run them, one of:
- Serverless (Vercel, Netlify, Workers): mount the cron endpoint (
app.route('/api/cron', cronRoutes())orapp/api/cron/route.tswithexport { cronRoute as GET, cronRoute as POST }) and call it every minute withAuthorization: Bearer $CRON_SECRET(Vercel Cron sends it automatically whenCRON_SECRETis set). - Long-running Node: a worker process calling
runWorker({ concurrency: 4 }).
- Serverless (Vercel, Netlify, Workers): mount the cron endpoint (
- Recurring work:
await schedule('report.daily', '0 8 * * *', { timezone: 'Europe/Madrid' })once at startup. - Verify: enqueue a job, call the cron endpoint, the
jobsrow becomesdone.
Conventions
- Handlers must be idempotent: a job can run more than once (retries, expired locks).
- Expired locks count as attempts: a job whose worker dies on its last attempt is marked
failedwithlastError = 'lock expired'(failExhaustedJobs, run byrunDue) instead of being reclaimed forever. - Each schedule tick is claimed with one conditional
UPDATE(claimScheduleTick), so overlapping cron calls enqueue it once. - Payloads carry IDs and small values; load fresh data inside the handler. Never put secrets in payloads.
- Pass
ctx.signaltofetch()and long operations so timeouts stop them. - Job types are dotted lowercase names owned by a module (
newsletter.send-batch).
Don't
- Don't call slow external APIs (email, suppliers, payments) inside a request when a job fits.
- Don't edit
jobsrows by hand; useretryJobfor failed ones. - Don't expose the cron endpoint without
CRON_SECRET.
# @core/jobs — rules for AI agents
## Purpose
Durable background jobs on the same Postgres: queue (`FOR UPDATE SKIP LOCKED`), retries with exponential backoff,
timeouts, de-duplication keys, cron schedules (5 fields, with timezone) and an HTTP endpoint so serverless hosts can
run due jobs from their cron. No Redis, no extra service. Table owner of `jobs` and `job_schedules`.
## Map
- `index.ts` — public API: `defineJob`, `enqueue`, `schedule`, `unschedule`, `runDue`, `runWorker`, `retryJob`, `pruneJobs`.
- `queue.ts` — claim/run logic. `registry.ts` — in-memory job types. `schema.ts` — tables.
- `adapters/hono.ts` — `cronRoutes()`. `adapters/next.ts` — `cronRoute` (GET/POST).
## Integration
1. Env: `CRON_SECRET` (≥ 32 random chars). Generate and apply migrations (see `src/lib/db/AGENTS.md`).
2. Define jobs in one module imported by every process (web and worker), e.g. `src/jobs.ts`:
```ts
import { z } from 'zod';
import { defineJob } from './lib/jobs/index.ts';
export const sendWelcome = defineJob('email.welcome', z.object({ userId: z.string() }), async ({ userId }, { signal }) => { /* … */ });
```
Enqueue: `await sendWelcome.enqueue({ userId }, { dedupeKey: `welcome:${userId}` })`.
3. Run them, one of:
- Serverless (Vercel, Netlify, Workers): mount the cron endpoint (`app.route('/api/cron', cronRoutes())` or
`app/api/cron/route.ts` with `export { cronRoute as GET, cronRoute as POST }`) and call it every minute with
`Authorization: Bearer $CRON_SECRET` (Vercel Cron sends it automatically when `CRON_SECRET` is set).
- Long-running Node: a worker process calling `runWorker({ concurrency: 4 })`.
4. Recurring work: `await schedule('report.daily', '0 8 * * *', { timezone: 'Europe/Madrid' })` once at startup.
5. Verify: enqueue a job, call the cron endpoint, the `jobs` row becomes `done`.
## Conventions
- Handlers must be idempotent: a job can run more than once (retries, expired locks).
- Expired locks count as attempts: a job whose worker dies on its last attempt is marked `failed` with
`lastError = 'lock expired'` (`failExhaustedJobs`, run by `runDue`) instead of being reclaimed forever.
- Each schedule tick is claimed with one conditional `UPDATE` (`claimScheduleTick`), so overlapping cron calls enqueue it once.
- Payloads carry IDs and small values; load fresh data inside the handler. Never put secrets in payloads.
- Pass `ctx.signal` to `fetch()` and long operations so timeouts stop them.
- Job types are dotted lowercase names owned by a module (`newsletter.send-batch`).
## Don't
- Don't call slow external APIs (email, suppliers, payments) inside a request when a job fits.
- Don't edit `jobs` rows by hand; use `retryJob` for failed ones.
- Don't expose the cron endpoint without `CRON_SECRET`.
.genpmignore 適用後に組み込まれる正確なツリーです。固定先:
// Tablas de @core/jobs. Las recoge drizzle-kit vía src/lib/db/drizzle.config.ts.
import { sql } from 'drizzle-orm';
import { index, integer, jsonb, pgTable, text, timestamp, uniqueIndex } from 'drizzle-orm/pg-core';
import { primaryId, timestamps } from '../db/index.ts';
const ts = (name: string) => timestamp(name, { withTimezone: true, mode: 'date' });
export const jobs = pgTable(
'jobs',
{
id: primaryId('job'),
type: text('type').notNull(),
payload: jsonb('payload').$type<unknown>().notNull(),
status: text('status', { enum: ['queued', 'running', 'done', 'failed'] })
.notNull()
.default('queued'),
runAt: ts('run_at').notNull().defaultNow(),
attempts: integer('attempts').notNull().default(0),
maxAttempts: integer('max_attempts').notNull().default(5),
/** Mientras `running`, nadie más la toma hasta esta hora (si el worker muere, se reintenta al vencer). */
lockedUntil: ts('locked_until'),
lastError: text('last_error'),
/** Evita duplicados mientras haya una pendiente o en curso con la misma clave. */
dedupeKey: text('dedupe_key'),
finishedAt: ts('finished_at'),
...timestamps,
},
(t) => [
index('jobs_due_idx').on(t.status, t.runAt),
uniqueIndex('jobs_dedupe_idx')
.on(t.dedupeKey)
.where(sql`${t.dedupeKey} is not null and ${t.status} in ('queued', 'running')`),
],
);
export const jobSchedules = pgTable('job_schedules', {
/** Un horario por tipo de tarea. */
type: text('type').primaryKey(),
cron: text('cron').notNull(),
timezone: text('timezone').notNull().default('UTC'),
payload: jsonb('payload').$type<unknown>().notNull(),
nextRunAt: ts('next_run_at').notNull(),
lastRunAt: ts('last_run_at'),
...timestamps,
});
export type Job = typeof jobs.$inferSelect;
export type JobSchedule = typeof jobSchedules.$inferSelect;
このパッケージは MCP サーバーを宣言していません。
| バージョン | コミット | 公開日 | スキャン |
|---|---|---|---|
| 1.1.0 | eef017d | 3 時間前 | スキャン合格 |
- genpm
- @core/db ^1.0.0
- 提案
- GenPM は npm コマンドを提案し、あなたが承認した場合にのみ実行します。
- スキャン
- スキャン合格 · 指摘 0 件
- コミット
- v1.1.0 → eef017d6b6e8036d93cc1a5e7941cb9dcc10e15f · 取得後に検証済み
- スクリプト
- なし。GenPM はパッケージのコードを実行しません。
- ライセンス
- MIT
- 品質
- 100/100
- 認識されたライセンス達成
- AGENTS.md に目的の説明がある達成
- AGENTS.md に統合手順がある達成
- AGENTS.md に規約や禁止事項がある達成
- テストを含む達成
- セキュリティスキャンに合格達成
- 過去 6 か月以内に公開達成
- 認証済みの公開者達成
- 概要とキーワード達成
- 報告
- 問題を見つけましたか?