---
title: Drizzle ORM + pg-boss دليل تعليمي عن المعاملات الذرية (2026)
description: أرسل ١٢ jobs لـ pg-boss المدعوم بـ Postgres داخل Drizzle ORM transaction باستخدام fromDrizzle adapter، مع retries و dead letters و cron schedules.
url: https://nerdleveltech.com/ar/drizzle-orm-pg-boss-atomic-transactions-tutorial
language: ar
date: '2026-05-31'
category: backend
tags:
  - pg-boss
  - drizzle-orm
  - postgres
  - typescript
  - background-jobs
  - transactions
  - node
  - tutorial
markdown_url: https://nerdleveltech.com/ar/drizzle-orm-pg-boss-atomic-transactions-tutorial.md
attribution: 'Source: NerdLevelTech — https://nerdleveltech.com/ar/drizzle-orm-pg-boss-atomic-transactions-tutorial'
terms: https://nerdleveltech.com/llms.txt
---

# Drizzle ORM + pg-boss دليل تعليمي عن المعاملات الذرية (2026)

قم بتوصيل Drizzle ORM 0.45.2 بـ pg-boss 12.18.2 من خلال محول `fromDrizzle` المدمج في pg-boss بحيث تشترك عملية إدخال صف وإرسال الوظيفة المطابقة في معاملة Postgres واحدة. يتم الاعتماد (Commit) معاً، أو التراجع (Roll back) معاً. أنواع TypeScript من البداية للنهاية، سياسات إعادة المحاولة، طوابير الرسائل المهملة (dead-letter queues)، جداول cron، وقابل للتشغيل مقابل `postgres:18-alpine`.

## ملخص

نمط "إرسال بريد إلكتروني بعد إنشاء صف" يُكتب دائماً تقريباً باستدعاءين غير مرتبطين: `INSERT INTO orders ...` ثم `boss.send('email', ...)`. إذا توقفت العملية بينهما، فستقوم بشحن طلب بدون بريد إلكتروني - أو والأسوأ من ذلك، إرسال بريد إلكتروني لطلب تم التراجع عنه. يوفر pg-boss 12 محول `fromDrizzle(tx, sql)` الذي يوجه إدخال الوظيفة عبر كائن معاملة Drizzle الخاص بك، بحيث تشترك كلتا الكتابتين في معاملة Postgres واحدة. يتم الاعتماد معاً، والتراجع معاً، بدون كود ربط إضافي. هذا المنشور يبني النظام الكامل في 8 خطوات.

## ما ستتعلمه

- كيف يجعل `pg-boss.fromDrizzle` إدخال الطابور يشترك في معاملة مع كتابات Drizzle الخاصة بك
- كيفية هيكلة مشروع Postgres 18 + pg-boss 12 + Drizzle 0.45.2 مع أنواع TypeScript شاملة
- كيفية إرسال وظيفة بشكل ذري (atomically) مع إدخال صف بحيث يتم اعتمادهما معاً أو التراجع عنهما معاً
- كيفية تعيين حدود إعادة المحاولة، والتراجع الأسي (exponential backoff)، وطابور الرسائل المهملة لكل طابور
- كيفية جدولة الوظائف المتكررة باستخدام تعبيرات cron ومنطقة زمنية
- كيفية كتابة عامل (worker) يرسل وظائف تابعة داخل معاملته الخاصة
- كيفية التحقق من سلوك التراجع باستخدام معاملة تفشل عمداً
- كيفية إغلاق الطابور والمجمع (pool) بشكل نظيف عند استلام `SIGTERM`

## المتطلبات الأساسية

تحتاج إلى Node.js 24 (إصدار LTS النشط اعتباراً من مايو 2026، مدعوم حتى أبريل 2028 - يعمل Node 22 أيضاً لأن pg-boss 12 يتطلب Node ≥22.12.0)[^node-lts]. تحتاج إلى Docker (أو طريقة أخرى لتشغيل Postgres 18) ومحطة طرفية (terminal) مع توفر `npm` و `psql` محلياً وهو أمر مفيد ولكنه غير مطلوب. يُفترض الإلمام بأساسيات TypeScript و async/await.

تم اختبار جميع الأوامر على macOS و Linux. يجب على مستخدمي Windows تشغيل كل شيء من WSL2 بحيث تشترك Docker و Node و `psql` في مساحة مستخدم Linux واحدة.

## لماذا تهم "الجدولة الذرية" (atomic enqueue)

الوظائف الخلفية التي تعمل بعد الكتابة في قاعدة البيانات موجودة في كل مكان: إيصالات الطلبات، رسائل الترحيب بعد التسجيل، خطافات الويب (webhooks) التابعة، تحديثات فهرس البحث، وبدايات التنفيذ. النمط الساذج يبدو كالتالي:

```ts
await db.insert(orders).values(input);     // commit 1
await boss.send('order.receipt-email', { ... });  // commit 2
```

هناك أربعة أوضاع للفشل بين commit 1 و commit 2 تؤدي لتلف بياناتك:

1. تعطل العملية بعد commit 1 ← الطلب موجود، لا يوجد بريد إلكتروني. العميل دفع، ولم يحصل على شيء.
2. تعطل العملية بعد commit 2، قبل الرد على مستدعي HTTP ← يعيد المستدعي المحاولة، فتقوم بإنشاء طلب ثانٍ، ويحصل العميل على بريدين إلكترونيين.
3. رفض `boss.send` بسبب خطأ اتصال Postgres عابر ← الطلب موجود، لا يوجد بريد إلكتروني.
4. خطوة لاحقة في نفس المعالج تطرح خطأ ← تقوم بعمل `try/catch` والتراجع عن الصف، لكن الوظيفة موجودة بالفعل في الطابور، ويرسل العامل بريداً إلكترونياً لعميل حول طلب لم يعد موجوداً.

الحل هو وضع كلتا الكتابتين في نفس معاملة Postgres. يقبل `boss.send` الخاص بـ pg-boss خيار `{ db }` الذي يتجاوز مكان إدخال الوظيفة. إذا وجهت ذلك الـ `db` إلى طريقة `execute` الخاصة بمعاملة Drizzle، فسيستقر صف الوظيفة داخل نفس المعاملة مثل إدخال الصف.[^pgboss-readme]

## الخطوة 1 — هيكلة المشروع

أنشئ دليلاً جديداً وقم بتثبيت الإصدارات المحددة:

```bash
mkdir orders-API && cd orders-API
npm init -y
npm pkg set type=module
npm install --save-exact pg-boss@12.18.2 drizzle-orm@0.45.2 pg@8.21.0
npm install --save-exact --save-dev TypeScript@6.0.3 \
  drizzle-kit@0.31.10 tsx@4.22.3 dotenv@17.4.2 \
  @types/pg@8.20.0 @types/node@22.19.17
```

علامة `--save-exact` ضرورية: `npm install pg-boss@12.18.2` بدونها يكتب `^12.18.2`، لذا فإن أي `npm install` مستقبلي قد يقوم بالترقية إلى 12.19.x ويغير مخطط خيارات الطابور بهدوء. قم بقفل مستوى التصحيح (patch level).[^npm-save-exact]

أضف ملف `tsconfig.json` مع أكثر الخيارات صرامة التي يمكنك تحملها. المجموعة أدناه هي الأساس للإنتاج المستخدم في بقية المنشور:

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "lib": ["ES2022"],
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "resolveJsonModule": true,
    "verbatimModuleSyntax": true,
    "allowImportingTsExtensions": true,
    "isolatedModules": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*.ts"]
}
```

يسمح لك `allowImportingTsExtensions: true` باستيراد الملفات كـ `'./schema.ts'`. يقوم Node 22.18+ و Node 24 بتجريد الأنواع محلياً، ويقبل `tsx` نفس الصيغة، لذا يعمل نفس المصدر في كليهما[^node-strip-types].

أضف ملف `.env` مع سلسلة الاتصال التي ستوجه كل شيء إليها:

```bash
echo 'DATABASE_URL=postgres://orders:orders@127.0.0.1:5432/orders' > .env
echo '.env' >> .gitignore
echo 'node_modules' >> .gitignore
```

## الخطوة 2 — تشغيل Postgres 18

استخدم صورة `postgres:18-alpine` الرسمية - يشير الوسم العائم حالياً إلى `18-alpine3.23` (قاعدة Alpine 3.23)[^pg-Docker]:

```bash
Docker run --rm -d --name orders-pg \
  -e POSTGRES_USER=orders \
  -e POSTGRES_PASSWORD=orders \
  -e POSTGRES_DB=orders \
  -p 5432:5432 \
  postgres:18-alpine
```

تحقق من أنه يعمل:

```bash
Docker exec orders-pg pg_isready -U orders -d orders
# المتوقع: /var/run/postgresql:5432 - accepting connections
```

سيقوم `pg-boss` بإنشاء المخطط الخاص به (`pgboss` افتراضياً - سنقوم بقفله صراحة في الخطوة 4) وتشغيل عمليات الهجرة الخاصة به عند البدء الأول. لست بحاجة لإنشاء أي شيء مسبقاً.

## الخطوة 3 — تعريف مخطط Drizzle

أنشئ `src/db/schema.ts` مع جدول الأعمال الوحيد الذي يهتم به هذا البرنامج التعليمي - جدول `orders` مع تعداد (enum) للحالة:

```ts
// src/db/schema.ts
import {
  pgTable,
  uuid,
  text,
  integer,
  timestamp,
  index,
  pgEnum,
} from 'drizzle-orm/pg-core';

export const orderStatus = pgEnum('order_status', [
  'pending',
  'paid',
  'fulfilled',
  'failed',
]);

export const orders = pgTable(
  'orders',
  {
    id: uuid('id').defaultRandom().primaryKey(),
    customerEmail: text('customer_email').notNull(),
    amountCents: integer('amount_cents').notNull(),
    currency: text('currency').notNull().default('USD'),
    status: orderStatus('status').notNull().default('pending'),
    createdAt: timestamp('created_at', { withTimezone: true )
      .notNull()
      .defaultNow(),
  },
  (t) => [
    index('orders_email_idx').on(t.customerEmail),
    index('orders_status_idx').on(t.status),
  ],
);

export type Order = typeof orders.$inferSelect;
export type OrderInsert = typeof orders.$inferInsert;
```

يوفر لك `$inferSelect` و `$inferInsert` أشكال صفوف مكتوبة مجاناً - بدون ملف DTO منفصل[^drizzle-types].

قم بتوصيل عميل Drizzle بـ `pg.Pool` في `src/db/client.ts`:

```ts
// src/db/client.ts
import 'dotenv/config';
import { Pool } from 'pg';
import { drizzle } from 'drizzle-orm/node-postgres';
import * as schema from './schema.ts';

const url = process.env.DATABASE_URL;
if (!url) throw new Error('DATABASE_URL is required');

export const pool = new Pool({
  connectionString: url,
  max: 10,
  idleTimeoutMillis: 30_000,
  connectionTimeoutMillis: 5_000,
});

pool.on('error', (err) => {
  console.error('pg pool error', err);
});

export const db = drizzle({ client: pool, schema, casing: 'snake_case' });
export type Database = typeof db;
```

خيار `casing: 'snake_case'` هو إعداد افتراضي للأمان. يمرر المخطط أعلاه أسماء الأعمدة الصريحة (`text('customer_email')`)، لذا ليس لحالة الأحرف (casing) أي تأثير هناك - ولكن أي عمود تضيفه لاحقاً بدون اسم صريح (`text()` وحده) سيظل يُصدر كـ snake\_case في SQL الناتج، مما يطابق بقية جداولك.

أضف تكوين Drizzle Kit وقم بإنشاء الهجرة:

```ts
// drizzle.config.ts
import 'dotenv/config';
import type { Config } from 'drizzle-kit';

const url = process.env.DATABASE_URL;
if (!url) throw new Error('DATABASE_URL is required');

export default {
  schema: './src/db/schema.ts',
  out: './drizzle',
  dialect: 'postgresql',
  dbCredentials: { url ,
} satisfies Config;
```

أضف البرامج النصية (scripts) إلى `package.json`:

```json
{
  "scripts": {
    "db:generate": "drizzle-kit generate",
    "db:push": "drizzle-kit push",
    "typecheck": "tsc --noEmit",
    "dev": "tsx watch src/index.ts"
  }
}
```

ثم ادفع المخطط:

```bash
npm run db:push
# [✓] Pulling schema from database...
# [✓] Changes applied
```

## الخطوة 4 — إعداد pg-boss

أنشئ `src/queue.ts`:

```ts
// src/queue.ts
import 'dotenv/config';
import { PgBoss } from 'pg-boss';

const url = process.env.DATABASE_URL;
if (!url) throw new Error('DATABASE_URL is required');

export const boss = new PgBoss({
  connectionString: url,
  schema: 'pgboss',
  max: 5,
  monitorIntervalSeconds: 30,
  application_name: 'orders-API',
});

boss.on('error', (err: Error) => {
  console.error('pg-boss error', err);
});

export const QUEUES = {
  receiptEmail: 'order.receipt-email',
  fulfillment: 'order.fulfillment',
} as const;
```

هناك شيئان غير بديهيين هنا:

- الاستيراد هو `import { PgBoss } from 'pg-boss'` - مسمى، وليس افتراضياً. pg-boss 12 هو ESM نقي (Node ≥22.12.0) ويتم تصدير الفئة كاستيراد مسمى فقط[^pgboss-esm].
- يتدفق `application_name` إلى `pg_stat_activity.application_name` في Postgres، مما يجعل من السهل العثور على اتصالات pg-boss (`SELECT pid, query FROM pg_stat_activity WHERE application_name = 'orders-API';`) عند الفحص. قم بتعيينه صراحة على الرغم من أنه اختياري.

عندما تستدعي `boss.start()` لأول مرة، يقوم pg-boss بإنشاء مخطط `pgboss` وتشغيل عمليات الهجرة الخاصة به. إصدار المخطط في pg-boss 12.18.2 هو 30 (يمكنك التحقق في وقت التشغيل باستخدام `boss.schemaVersion()` أو عبر CLI: `npx pg-boss version`).

## الخطوة 5 — وظيفة إنشاء الطلب الذرية

هذا هو جوهر البرنامج التعليمي. أنشئ `src/orders/create.ts`:

```ts
// src/orders/create.ts
import { sql from 'drizzle-orm';
import { fromDrizzle from 'pg-boss';
import { db from '../db/client.ts';
import { orders, type OrderInsert from '../db/schema.ts';
import { boss, QUEUES from ;

export interface CreateOrderInput {
  customerEmail: string;
  amountCents: number;
  currency?: string;
}

export interface ReceiptEmailJob {
  orderId: string;
  customerEmail: string;
  amountCents: number;
  currency: string;
}

export async function createOrderAtomic(
  input: CreateOrderInput,
): Promise<{ orderId: string; receiptJobId: string | null > {
  return db.transaction(async (tx) => {
    const insert: OrderInsert = {
      customerEmail: input.customerEmail,
      amountCents: input.amountCents,
      currency: input.currency ?? 'USD',
      status: 'pending',
    };

    const [row] = await tx
      .insert(orders)
      .values(insert)
      .returning({ id: orders.id );

    if (!row) throw new Error('order insert returned no row');

    const receiptJobId = await boss.send(
      QUEUES.receiptEmail,
      {
        orderId: row.id,
        customerEmail: input.customerEmail,
        amountCents: input.amountCents,
        currency: input.currency ?? 'USD',
      } satisfies ReceiptEmailJob,
      { db: fromDrizzle(tx, sql) ,
    );

    return { orderId: row.id, receiptJobId ;
  });
}
```

ثلاثة أشياء يجب استيعابها:

1. تفتح `db.transaction(async (tx) => {...})` معاملة Postgres واحدة. كل استعلام مقابل `tx` يعمل على نفس الاتصال الفعلي حتى يتم حل رد الاتصال (callback) أو طرح خطأ.

- `boss.send(name, data, { db: fromDrizzle(tx, sql) })` هو المكان الذي يحدث فيه السحر. خيار `db` يتجاوز تجمع الاتصالات الافتراضي لـ pg-boss باستخدام محول `IDatabase` الذي يتم إرجاعه بواسطة `fromDrizzle`. يقوم المحول `executeSql(text, values)` بإعادة كتابة العناصر النائبة `$1`/`$2` إلى تنسيق template-tag الخاص بـ Drizzle ويستدعي `tx.execute(sql(strings, ...reordered))` — نفس المعاملة، نفس الاتصال، ونفس حدود الحفظ (commit)[^pgboss-drizzle-adapter].
- الكلمة المفتاحية `satisfies` في حمولة الوظيفة (job payload) تفرض الهيكل دون توسيعه: إذا قمت لاحقًا بإضافة حقل مطلوب إلى `ReceiptEmailJob`، فستفشل عملية فحص النوع (type-checking) في كل مكان يتم استدعاؤه فيه حتى تقوم بإصلاحه.

إذا حدث أي خطأ داخل رد الاستدعاء (callback)، يقوم Drizzle بالتراجع (rollback) تلقائيًا — ويتراجع إرسال الوظيفة معه. لا توجد وظيفة "معلقة" في الطابور لطلب غير موجود.

## الخطوة 6 — تهيئة عمليات إعادة المحاولة وطابور الرسائل المهملة (DLQ)

ميزانية إعادة المحاولة الافتراضية هي 2 بدون تأخير، وهو أمر جيد لمشاريع "hello-world" ولكنه غير مسؤول في بيئة الإنتاج[^pgboss-retries]. قم بتجاوزها لكل طابور. أنشئ `src/orders/retry.ts`:

```ts
// src/orders/retry.ts
import { boss } from '../queue.ts';

export async function configureRetries(): Promise<void> {
  await boss.createQueue('order.receipt-email', {
    policy: 'standard',
    retryLimit: 5,
    retryDelay: 30,
    retryBackoff: true,
    expireInSeconds: 60,
    retentionSeconds: 60 * 60 * 24 * 14,
    deleteAfterSeconds: 60 * 60 * 24 * 7,
    deadLetter: 'order.receipt-email.dlq',
  });

  await boss.createQueue('order.receipt-email.dlq');
}
```

أسماء الحقول التي تستحق الحفظ لأنها خطأ شائع في أوراق البيانات:

| الحقل                | الافتراضي          | ملاحظات                                                                |
| -------------------- | ------------------ | ---------------------------------------------------------------------- |
| `retryLimit`         | 2                  | أقصى عدد من المحاولات بعد التشغيل الأولي                               |
| `retryDelay`         | 0                  | الثواني بين المحاولات                                                  |
| `retryBackoff`       | `false`            | عند تفعيلها، تطبق تراجعًا أسيًا (exponential backoff) على `retryDelay` |
| `expireInSeconds`    | 900                | أقصى عدد من الثواني في حالة `active` قبل اعتبار الوظيفة فاشلة          |
| `retentionSeconds`   | 1209600 (14 يومًا) | مدة الاحتفاظ بالوظائف غير المكتملة                                     |
| `deleteAfterSeconds` | 604800 (7 أيام)    | مدة الاحتفاظ بالوظائف المكتملة                                         |
| `deadLetter`         | لا يوجد            | اسم طابور آخر يستقبل الحمولة بعد الفشل النهائي                         |

`retryLimit: 5` + `retryDelay: 30` + `retryBackoff: true` يعطي تراجعًا مبسطًا `30 * 2^n` ثانية بين المحاولات — تقريبًا 30 ثانية، 60 ثانية، 120 ثانية، 240 ثانية، 480 ثانية، مع بعض التذبذب (jitter) — ثم تستقر الحمولة في `order.receipt-email.dlq` للمراجعة البشرية. يوثق مصدر pg-boss الصيغة كـ `Math.min(retryDelayMax, retryDelay * (2 ** Math.min(16, retryCount) / 2 + 2 * Math.min(16, retryCount) / 2 * Math.random()))`.

لا يوجد حقل `retentionDays` في الطابور. محاولة تمرير واحد ستؤدي إلى خطأ TypeScript مقابل المعامل المكتوب `Omit<Queue, 'name'>`؛ استخدم حقول `*Seconds` المذكورة أعلاه بدلاً من ذلك[^pgboss-types]. (لاحظ أن pg-boss يوفر حقلاً واحدًا بصيغة `*Days` — وهو `warningRetentionDays` في خيارات `MaintenanceOptions` العالمية — ولكن هذا يتحكم في سجل تحذيرات BAM، وليس الاحتفاظ بالوظائف.)

## الخطوة 7 — العمال (Workers) الذين يرسلون الوظائف التابعة بشكل ذري (atomically)

يعمل العمال داخل معاملاتهم الخاصة. عندما يتفرع العامل إلى وظائف أخرى، فأنت تريد نفس دلالات التراجع. أنشئ `src/orders/fulfill.ts`:

```ts
// src/orders/fulfill.ts
import { eq, sql } from 'drizzle-orm';
import { fromDrizzle } from 'pg-boss';
import { db } from '../db/client.ts';
import { orders } from '../db/schema.ts';
import { boss, QUEUES } from '../queue.ts';

export interface FulfillmentJob {
  orderId: string;
}

export async function registerFulfillmentWorker(): Promise<void> {
  await boss.work<FulfillmentJob, void>(
    QUEUES.fulfillment,
    {
      batchSize: 1,
      pollingIntervalSeconds: 1,
    },
    async (jobs) => {
      for (const job of jobs) {
        const { orderId } = job.data;
        await db.transaction(async (tx) => {
          const [row] = await tx
            .update(orders)
            .set({ status: 'fulfilled' })
            .where(eq(orders.id, orderId))
            .returning({ id: orders.id });

          if (!row) {
            throw new Error(`order ${orderId} not found`);
          }

          await boss.send(
            QUEUES.receiptEmail,
            { orderId, type: 'fulfilled' },
            { db: fromDrizzle(tx, sql) },
          );
        });
      }
    },
  );
}

export async function scheduleNightlyReconcile(): Promise<void> {
  await boss.schedule(
    'reconcile.orders',
    '0 2 * * *',
    {},
    { tz: 'UTC' },
  );

  await boss.work('reconcile.orders', async () => {
    console.log('[reconcile] running nightly check at', new Date().toISOString());
  });
}
```

ثلاث ملاحظات هامة:

- `boss.work<ReqData, ResData>(name, options, handler)` — يتلقى المعالج **مصفوفة** من `Job<ReqData>`، وليس وظيفة واحدة. `batchSize: 1` تجعل طول المصفوفة دائمًا 1، وهو ما يريده معظم الناس فعليًا عند استخدام API.
- يفتح المعالج معاملة Drizzle. في الداخل، يتشارك كل من تحديث الصف وإرسال البريد الإلكتروني للإيصال في تلك المعاملة. إذا فشل إرسال الإيصال بعد تحديث الصف، يتراجع تحديث الصف — ويقوم الفحص التالي بالتقاط نفس وظيفة التنفيذ (fulfillment) وإعادة المحاولة بشكل نظيف.
- `boss.schedule(name, cron, data, { tz })` يسجل جدول cron. الخيار `tz` يكون افتراضيًا `'UTC'`؛ قم بتمرير منطقة IANA مختلفة (مثل `'America/New_York'`) فقط عندما تحتاج فعليًا إلى جدولة تعتمد على التوقيت المحلي.[^pgboss-cron]

## الخطوة 8 — إثبات عمل التراجع (Rollback)

دالة تجريبية تتعمد إلقاء خطأ بعد إرسال الوظيفة، حتى تتمكن من رؤية الطابور يظل فارغًا عندما تتراجع المعاملة. أنشئ `src/orders/rollback-demo.ts`:

```ts
// src/orders/rollback-demo.ts
import { sql } from 'drizzle-orm';
import { fromDrizzle } from 'pg-boss';
import { db } from '../db/client.ts';
import { orders } from '../db/schema.ts';
import { boss, QUEUES } from '../queue.ts';

export class FraudCheckFailed extends Error {
  override readonly name = 'FraudCheckFailed';
}

export async function createOrderWithFraudCheck(input: {
  customerEmail: string;
  amountCents: number;
}): Promise<{ orderId: string } | { rolledBack: true }> {
  try {
    return await db.transaction(async (tx) => {
      const [row] = await tx
        .insert(orders)
        .values({
          customerEmail: input.customerEmail,
          amountCents: input.amountCents,
          status: 'pending',
        })
        .returning({ id: orders.id });

      if (!row) throw new Error('insert returned no row');

      await boss.send(
        QUEUES.receiptEmail,
        { orderId: row.id, customerEmail: input.customerEmail },
        { db: fromDrizzle(tx, sql) },
      );

      if (input.amountCents > 1_000_000) {
        throw new FraudCheckFailed('amount exceeds $10,000 limit');
      }

      return { orderId: row.id };
    });
  } catch (err) {
    if (err instanceof FraudCheckFailed) {
      console.warn('fraud check failed; transaction rolled back', err.message);
      return { rolledBack: true };
    }
    throw err;
  }
}
```

قم بربط كل شيء في نقطة دخول في `src/index.ts`:

```ts
// src/index.ts
import 'dotenv/config';
import { boss } from './queue.ts';
import { createOrderAtomic } from './orders/create.ts';
import { configureRetries } from './orders/retry.ts';
import { registerFulfillmentWorker, scheduleNightlyReconcile } from './orders/fulfill.ts';
import { createOrderWithFraudCheck } from './orders/rollback-demo.ts';
import { pool } from './db/client.ts';

async function shutdown(): Promise<void> {
  console.log('shutting down...');
  await boss.stop({ close: true, graceful: true, timeout: 30_000 });
  await pool.end();
  process.exit(0);
}

process.on('SIGTERM', () => void shutdown());
process.on('SIGINT', () => void shutdown());

async function main(): Promise<void> {
  await boss.start();
  await configureRetries();
  await registerFulfillmentWorker();
  await scheduleNightlyReconcile();

  const ok = await createOrderAtomic({
    customerEmail: 'alice@example.com',
    amountCents: 4999,
  });
  console.log('committed order:', ok);

  const rb = await createOrderWithFraudCheck({
    customerEmail: 'mallory@example.com',
    amountCents: 2_500_000,
  });
  console.log('fraud demo:', rb);
}

main().catch((err) => {
  console.error('fatal', err);
  process.exit(1);
});
```

`boss.stop({ close: true, graceful: true, timeout: 30_000 })` يسمح للوظائف النشطة بالانتهاء لمدة تصل إلى 30 ثانية قبل فرض الإغلاق. الخيارات الثلاثة هي أيضًا القيم الافتراضية لـ pg-boss 12.18.2 — `close = true, graceful = true, timeout = 30000` — لذا فإن تمريرها صراحةً هنا يوثق النية فقط عند الاستدعاء. عندما يكون `close: true`، يغلق pg-boss تجمع `pg.Pool` الداخلي الخاص به (الذي أنشأه من سلسلة الاتصال الخاصة بك) حتى تتمكن العملية من الخروج بشكل نظيف. لاحظ أن `stop` **لا** يقبل علامة `wait`؛ هذا الحقل موجود في `OffWorkOptions` (المستخدم بواسطة `offWork`)، وهو مصدر شائع لأخطاء `TS2353` عند تعديل الدروس التعليمية من الإصدارات القديمة.

## التحقق

قم بتشغيل التطبيق:

```bash
npm run dev
```

يجب أن ترى شيئًا مثل:

```text
committed order: { orderId: '8f3a...', receiptJobId: 'a9c1...' }
fraud check failed; transaction rolled back amount exceeds $10,000 limit
fraud demo: { rolledBack: true }
```

(لم يتم تسجيل أي عامل لـ `order.receipt-email` في هذا العرض التوضيحي البسيط، لذا تستقر وظيفة الإيصال في الطابور وتنتظر هناك. هذا هو بالضبط ما نريده لفحص التراجع.) الآن تأكد من التراجع في Postgres مباشرة:

```bash
Docker exec -it orders-pg psql -U orders -d orders \
  -c "SELECT count(*) FROM orders WHERE customer_email='alice@example.com';"
# count: 1
Docker exec -it orders-pg psql -U orders -d orders \
  -c "SELECT count(*) FROM orders WHERE customer_email='mallory@example.com';"
# count: 0
Docker exec -it orders-pg psql -U orders -d orders \
  -c "SELECT count(*) FROM pgboss.job WHERE name='order.receipt-email' AND data->>'customerEmail'='mallory@example.com';"
# count: 0
```

تم حفظ الطلب الناجح، لكن صف "mallory" لم يوجد أبدًا ولم يتم إدراج وظيفتها في الطابور أبدًا. هذا هو ضمان الإدراج الذري (atomic-enqueue). (`pgboss.job` هو الجدول الأب المقسم الذي تذهب إليه صفوف كل طابور؛ الطوابير غير المقسمة (`non-partition`) تشترك جميعها في قسم `pgboss.job_common` افتراضيًا.)

## الأخطاء الشائعة وإصلاحها

**`TS2613: Module 'pg-boss' has no default export.`** قم بتغيير `import PgBoss from 'pg-boss'` إلى `import { PgBoss } from 'pg-boss'`. إصدار pg-boss 12 هو ESM خالص ويصدر `PgBoss` كـ named export فقط — ملف `dist/index.d.ts` المنشور يعلن عنه كـ `export declare class PgBoss`.

**`TS2353: Object literal may only specify known properties, and 'retentionDays' does not exist in type 'Omit<Queue, "name">'.`** لا يوجد حقل `retentionDays` في `QueueOptions`. استخدم `retentionSeconds: 60 * 60 * 24 * 14` للحصول على نفس التأثير[^pgboss-types]. (يوفر pg-boss `warningRetentionDays` في `MaintenanceOptions`، ولكن هذا يتحكم في سجل تحذيرات BAM — نطاق مختلف.)

**`error: relation "pgboss.job" does not exist`** لقد استدعيت `boss.send` أو استعلمت عن الهيكل قبل `boss.start()`. أول استدعاء لـ `start()` يقوم بتشغيل عمليات الهجرة (migrations) التي تنشئ الهيكل، والجدول الأب المقسم `pgboss.job`، والقسم الافتراضي `pgboss.job_common`. انتظر دائمًا `boss.start()` قبل أي طريقة أخرى من طرق pg-boss.

**أنواع معالج العامل (Worker handler) تظهر كـ `any` بالنسبة لـ `jobs`.** قم بتوفير كلا النوعين العامين (generics): `boss.work<ReqData, ResData>(name, options, handler)`. عند استخدام واحد فقط، يسقط بارامتر المعالج إلى توقيع `any` الافتراضي. بارامتر `jobs` هو دائمًا مصفوفة `Job<ReqData>[]`، وليس كائنًا فرديًا `Job<ReqData>` — راجع ملاحظة "حجم الدفعة" (batch size) في ملف README[^pgboss-readme].

**`fromDrizzle` غير معرف (undefined) وقت التشغيل.** أنت تستخدم إصدار pg-boss أقل من 12.17.0. المحولات `fromDrizzle`/`fromKnex`/`fromKysely`/`fromPrisma` جميعها أضيفت في **pg-boss 12.17.0** (تم نشره في 2026-04-24)؛ إصدارات 12.x السابقة شحنت فقط واجهة `IDatabase` اليدوية. قم بالترقية باستخدام `npm install pg-boss@>=12.17.0`. إذا لم تتمكن من الترقية، فقم بكتابة غلاف `IDatabase` صغير يستدعي `tx.execute(sql.raw(...))` مع إدراج العناصر النائبة بالفعل — `sql.raw` يقبل سلسلة نصية فقط، لذا سيتعين عليك استبدال قيم `$1`/`$2` بنفسك، وهو بالضبط ما يفعله المحول الرسمي نيابة عنك.

## الخطوات التالية

تغطي هذه المجموعة حالة الإدراج الذري (atomic-enqueue) من البداية إلى النهاية، ولكن أنظمة الإنتاج عادةً ما تحتاج لثلاثة أشياء إضافية:

- **قابلية الملاحظة (Observability)**: إصدار نطاقات OpenTelemetry لكل استدعاء لـ `send`/`work` حتى يظهر الطابور بجانب تتبعات HTTP الخاصة بك. يغطي [درس Hono middleware للفشل المفتوح مقابل الفشل المغلق](https://nerdleveltech.com/fail-open-vs-fail-closed-hono-middleware-Redis-tutorial) نفس النمط لتوزيع المهام في الـ middleware.
- **تحديد معدل الناشر (Rate-limiting)**: اربط هذا الطابور بمحدد نافذة منزلقة (sliding-window limiter) حتى لا يتمكن مستخدم واحد من إغراق طابور رسائل البريد الإلكتروني للإيصالات. راجع [درس تحديد المعدل بالنافذة المنزلقة باستخدام Upstash](https://nerdleveltech.com/API-rate-limiting-upstash-Redis-sliding-window-tutorial) للتعرف على المحدد؛ استبدل التخزين في الذاكرة بجدول عدادات Postgres للبقاء داخل قاعدة بيانات واحدة.
- **تقسيم جدول المهام (Partitioning)**: يقوم إصدار pg-boss 12 بالفعل بتقسيم `pgboss.job`، مع خيار تفعيل مخصص `partition: true` لكل طابور عالي الكثافة (اسم جدول التقسيم هو `j<sha224hash>`). للتنظيف التلقائي المعتمد على الوقت لأي جداول مرافقة كبيرة تقوم بتشغيلها بجانبه، يوضح [درس أتمتة التقسيم في Postgres 18 باستخدام pg\_partman + pg\_cron](https://nerdleveltech.com/pg-partman-pg-cron-postgres-18-partition-automation-tutorial) كيفية التقسيم حسب `created_at` بحيث يكون التنظيف عبارة عن `DROP PARTITION` بدلاً من `DELETE`.

[^node-lts]: جدول إصدارات Node.js، [`nodejs.org/en/about/previous-releases`](https://nodejs.org/en/about/previous-releases). دخل Node 24 مرحلة الدعم طويل الأمد النشط (Active LTS) في 2025-10-28 (الاسم الرمزي "Krypton") وهو مدعوم حتى أبريل 2028؛ انتقل Node 22 إلى مرحلة دعم الصيانة (Maintenance LTS) في نفس الفترة تقريبًا.

[^pgboss-readme]: ملف README الخاص بـ pg-boss 12، قسم "ORM Transaction Adapters"، [`GitHub.com/timgit/pg-boss`](https://GitHub.com/timgit/pg-boss). يوثق ملف README تصديرات واستخدام `fromKnex`، و `fromKysely`، و `fromDrizzle`، و `fromPrisma` داخل معاملات ORM. ملاحظة: وفقًا لـ `npm view pg-boss time --json`، تم شحن المحولات لأول مرة في **pg-boss 12.17.0** (2026-04-24)؛ وهي غير موجودة في الإصدارات من 12.0.0 إلى 12.16.0.

[^npm-save-exact]: وثائق npm، "install — save-exact flag"، [`docs.npmjs.com/cli/v10/commands/npm-install`](https://docs.npmjs.com/cli/v10/commands/npm-install). الافتراضي هو نطاق علامة الإقحام (caret range)؛ بينما يقوم `--save-exact` بكتابة الإصدار المحدد. قم بضبط `save-exact=true` في ملف `.npmrc` لجعله إعدادًا عالميًا.

[^node-strip-types]: وثائق Node.js، "Type stripping"، [`nodejs.org/API/TypeScript.html#type-stripping`](https://nodejs.org/API/TypeScript.html#type-stripping). وصل تنفيذ ملفات `.ts` الأصلي كخيار `--experimental-strip-types` في Node 22.6.0 وتمت ترقيته ليصبح مفعلًا افتراضيًا في Node 22.18.0 (و Node 24).

[^pg-Docker]: صورة Postgres الرسمية على Docker، [`hub.Docker.com/_/postgres`](https://hub.Docker.com/_/postgres). يشير وسم `postgres:18-alpine` حاليًا إلى `18-alpine3.23` (قاعدة Alpine 3.23)؛ يتوفر أيضًا إصدار مثبت `18.4-alpine3.23` إذا كنت بحاجة إلى تثبيت مستوى تصحيح (patch level) Postgres.

[^drizzle-types]: وثائق Drizzle ORM، "Type API"، [`orm.drizzle.team/docs/goodies#type-API`](https://orm.drizzle.team/docs/goodies). يتم تصدير `$inferSelect` و `$inferInsert` من كل مثيل لـ `pgTable`.

[^pgboss-esm]: يصرح ملف `package.json` الخاص بـ pg-boss عن `"type": "module"` ويقوم ملف `dist/index.d.ts` المنشور بتصدير `PgBoss` كفئة مسماة. تم التحقق من ذلك مقابل حزمة npm لـ `pg-boss@12.18.2`.

[^pgboss-drizzle-adapter]: مصدر pg-boss، `dist/adapters/drizzle.ts`. تقوم وظيفة `executeSql(text, values)` الخاصة بالمحول بإعادة كتابة العناصر النائبة `$1`، و `$2` إلى صيغة القالب الموسوم (tagged-template) الخاصة بـ Drizzle وتستدعي `tx.execute(sql(strings, ...reordered))`. تنص JSDoc على "بدون تبعية وقت تشغيل على `drizzle-orm`" — حيث يتم توفير استيراد `sql` من قبل المستدعي.

[^pgboss-retries]: وثائق pg-boss API، [`timgit.GitHub.io/pg-boss/`](https://timgit.GitHub.io/pg-boss/)، و JSDoc الخاص بـ `QueueOptions` في `node_modules/pg-boss/dist/types.d.ts`. القيمة الافتراضية لـ `retryLimit` هي 2 و `retryDelay` هي 0؛ صيغة التراجع (backoff) موثقة تحت `retryBackoff`.

[^pgboss-types]: تعريفات أنواع pg-boss، `node_modules/pg-boss/dist/types.d.ts`. تصدر واجهة `QueueOptions` كلاً من `expireInSeconds`، و `retentionSeconds`، و `deleteAfterSeconds`، و `retryLimit`، و `retryDelay`، و `retryBackoff`، و `retryDelayMax`، و `deadLetter`. لا يوجد `retentionDays`.

[^pgboss-cron]: يقوم ملف المصدر `timekeeper.js` في pg-boss بتفكيك خيارات `schedule()` كـ `{ tz = 'UTC', key = '', ...rest }` — المنطقة الزمنية الافتراضية هي UTC، وتم التحقق من ذلك في حزمة `pg-boss@12.18.2`. يتم تحليل تعبيرات Cron بواسطة `cron-parser` 5.x (معيار cron المكون من 5 حقول، بالإضافة إلى متغير مكون من 6 حقول للثواني).
