---
title: 'gRPC في Node.js و TypeScript: دليل الإنتاج (2026)'
description: 'ابنِ خدمة gRPC احترافية وبأنواع محددة (typed) في Node.js و TypeScript: الـ server streaming، والـ deadlines، والـ error model، والـ health checks. دليل 2026 قابل للتنفيذ.'
url: https://nerdleveltech.com/ar/grpc-nodejs-typescript-production-guide
language: ar
date: '2026-06-14'
category: backend
tags:
  - grpc
  - node.js
  - typescript
  - microservices
  - protobuf
  - backend
  - rpc
  - api design
markdown_url: https://nerdleveltech.com/ar/grpc-nodejs-typescript-production-guide.md
attribution: 'Source: NerdLevelTech — https://nerdleveltech.com/ar/grpc-nodejs-typescript-production-guide'
terms: https://nerdleveltech.com/llms.txt
---

# gRPC في Node.js و TypeScript: دليل الإنتاج (2026)

قم ببناء خدمة gRPC جاهزة للإنتاج باستخدام Node.js و TypeScript مستخدمًا `@grpc/grpc-js` و `@grpc/proto-loader`: حدد ملف `.proto`، وقم بتوليد الـ stubs المكتوبة (typed)، ونفذ عمليات RPC الأحادية (unary) والبث من الخادم (server-streaming)، وقم بتعيين المواعيد النهائية (deadlines)، وأرجع أكواد الحالة الصحيحة، واكشف عن بروتوكول فحص الحالة (health-checking) القياسي.

## ملخص

هذا الدليل العملي يبني خدمة gRPC مكتوبة بالكامل في Node.js و TypeScript من البداية للنهاية. ستقوم بتعريف `InventoryService` في ملف `.proto`، وتوليد stubs لـ TypeScript باستخدام `proto-loader-gen-types`، ثم تنفيذ unary RPC، و server-streaming RPC، و unary RPC يفشل مع أكواد حالة gRPC المناسبة. ستقوم بتعيين مواعيد نهائية للعميل (client deadlines) ومشاهدتها تظهر كـ `DEADLINE_EXCEEDED`، وإرجاع بيانات خطأ وصفية غنية في الـ trailers، وربط بروتوكول فحص `grpc.health.v1.Health` القياسي بالإضافة إلى انعكاس الخادم (server reflection). التقنيات المستخدمة: **`@grpc/grpc-js` 1.14.4**[^grpcjs]، **`@grpc/proto-loader` 0.8.1**[^protoloader]، **`grpc-health-check` 2.1.0**[^health]، و **`@grpc/reflection` 1.0.4**[^reflection] على **TypeScript 6.0.3**[^ts]. تم فحص الأنواع في كل ملف تحت إعدادات TypeScript الصارمة وتشغيلها بالكامل في 14 يونيو 2026 — حيث نجحت مجموعة اختبار `node:test` المكونة من ستة اختبارات. الوقت المتوقع حوالي 45-60 دقيقة.

## ما ستتعلمه

- لماذا تستخدم `@grpc/grpc-js` + `@grpc/proto-loader` بدلاً من حزمة `grpc` المبنية بلغة C والمهجورة.
- كيفية تعريف خدمة ورسائل في ملف proto3 `.proto`.
- كيفية توليد أنواع TypeScript من ملف `.proto` باستخدام `proto-loader-gen-types`.
- كيفية تنفيذ unary RPC مكتوب و server-streaming RPC.
- كيفية عمل معالجة الأخطاء في gRPC داخل Node.js: أكواد الحالة و metadata trailers.
- كيفية تعيين موعد نهائي (deadline) على استدعاء gRPC ومعالجة `DEADLINE_EXCEEDED`.
- كيفية إضافة بروتوكول فحص الحالة القياسي وانعكاس الخادم (server reflection).
- كيفية التحقق من كل شيء باستخدام مجموعة اختبار `node:test` داخلية.

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

- **Node.js 24** (الاسم الكودي "Krypton"، دعم طويل الأمد نشط Active LTS، مدعوم حتى 30 أبريل 2028)[^node] أو Node 22 Maintenance LTS. كلاهما يشغل TypeScript مباشرة عبر `tsx`، لذا لا توجد خطوة بناء (build) أثناء التطوير.
- الإلمام بـ `async`/`await` وأساسيات TypeScript. لا يشترط خبرة سابقة بـ gRPC أو Protocol Buffers.
- طرفية (terminal) ومحرر أكواد. لا حاجة لـ Docker، ولا قاعدة بيانات، ولا حساب سحابي — الخادم والعميل يعملان محليًا.

الإصدارات مثبتة في جميع الأنحاء. لصق إصدارات `latest` في الدروس التعليمية هو ما يجعل الأمثلة تتوقف عن العمل بعد ثلاثة أسابيع.

## لماذا @grpc/grpc-js وليس حزمة grpc القديمة

إذا بحثت عن "grpc node"، فستجد الكثير من النتائج القديمة التي لا تزال تستورد حزمة تسمى حرفيًا `grpc`. هذا هو الرابط الأصلي المبني على إضافات C، وقد **أهمله فريق gRPC في أبريل 2021**؛ ولم يعد يتلقى تحديثات، والتوصية الرسمية هي استخدام `@grpc/grpc-js` بدلاً منه[^deprecation]. حزمة `@grpc/grpc-js` هي تنفيذ بلغة JavaScript بالكامل — لا بناء أصلي (native build)، ولا `node-gyp`، ولا مخاطرة بالملفات الثنائية المبنية مسبقًا عند صدور إصدار Node جديد. عند دمجها مع `@grpc/proto-loader`، فإنها تقوم بتحميل ملف `.proto` الخاص بك في وقت التشغيل وتمنحك واجهة نظيفة ومكتوبة. هذا هو المزيج الذي يستخدمه هذا الدليل، وهو المزيج الذي يجب أن تلجأ إليه في عام 2026.

إن gRPC في حد ذاته هو إطار عمل RPC يعتمد على العقد أولاً (contract-first): تصف خدمتك في ملف `.proto`، ويتم إنشاء كل من العميل والخادم من هذا المصدر الوحيد للحقيقة. تنتقل الاستدعاءات عبر HTTP/2 مع Protocol Buffers على الشبكة، ولهذا السبب يعد gRPC خيارًا شائعًا لحركة المرور الداخلية بين الخدمات حيث تصبح هياكل النصوص والمخططات العشوائية في REST عبئًا.

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

أنشئ مشروعًا وقم بتثبيت تبعات وقت التشغيل والتطوير، مع تثبيت الإصدارات:

```bash
mkdir grpc-inventory && cd grpc-inventory
npm init -y
npm pkg set type=module

# runtime
npm install @grpc/grpc-js@1.14.4 @grpc/proto-loader@0.8.1 \
  grpc-health-check@2.1.0 @grpc/reflection@1.0.4

# dev
npm install -D TypeScript@6.0.3 tsx@4.22.4 @types/node@25.9.3
```

تقوم `tsx` بتشغيل ملفات `.ts` مباشرة، لذا فإن حلقة التطوير هي `tsx src/server.ts` بدون خطوة تجميع. أضف ملف `tsconfig.json` مهيأ للصرامة. الخيار الوحيد غير البديهي هو `"moduleResolution": "Bundler"` — الكود الذي تولده `proto-loader-gen-types` يستخدم استيرادات نسبية بدون امتدادات، ومعالجة Bundler تتعامل مع ذلك بنظافة مع استمرار فحص الأنواع تحت `verbatimModuleSyntax`:

```json
{
  "compilerOptions": {
    "target": "ES2023",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "verbatimModuleSyntax": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "noEmit": true,
    "types": ["node"]
  },
  "include": ["src/**/*.ts"]
}
```

ثم أنشئ تخطيط المجلدات الذي يستخدمه بقية الدليل:

```bash
mkdir -p proto/inventory/v1 src/gen src/server src/client
```

## الخطوة 2 — تعريف الخدمة في ملف .proto

ملف `.proto` هو العقد. أنشئ `proto/inventory/v1/inventory.proto`:

```proto
syntax = "proto3";

package inventory.v1;

// InventoryService manages product stock for a small store.
service InventoryService {
  // Unary RPC: fetch a single product by id.
  rpc GetProduct(GetProductRequest) returns (Product);

  // Server-streaming RPC: emit a bounded sequence of stock updates.
  rpc WatchStock(WatchStockRequest) returns (stream StockUpdate);

  // Unary RPC that can fail: reserve units of a product.
  rpc ReserveStock(ReserveStockRequest) returns (ReserveStockResponse);
}

message GetProductRequest {
  string id = 1;
}

message Product {
  string id = 1;
  string name = 2;
  int32 stock = 3;
  int64 price_cents = 4;
}

message WatchStockRequest {
  string id = 1;
  int32 updates = 2; // number of updates to emit before completing
}

message StockUpdate {
  string id = 1;
  int32 stock = 2;
}

message ReserveStockRequest {
  string id = 1;
  int32 quantity = 2;
}

message ReserveStockResponse {
  string reservation_id = 1;
  int32 remaining = 2;
}
```

هناك ملاحظتان تصميميتان تستحقان الاستيعاب. أولاً، الكلمة المفتاحية `stream` في نوع إرجاع `WatchStock` هي ما يجعلها server-streaming RPC — حيث يرسل الخادم العديد من رسائل `StockUpdate` لطلب واحد. ثانيًا، `price_cents` هو `int64`. يمكن لـ Protocol Buffers حمل عدد صحيح كامل 64 بت، لكن نوع `number` في JavaScript لا يمكنه تمثيل أعداد صحيحة تتجاوز 2^53 دون فقدان الدقة، لذا فإن تعيين proto3-to-JSON الرسمي يحول حقول 64 بت إلى سلاسل نصية (strings)[^proto3json]. سترى ذلك ينعكس في الأنواع المولدة بعد قليل.

## الخطوة 3 — توليد أنواع TypeScript من ملف proto

توفر `@grpc/proto-loader` واجهة سطر أوامر `proto-loader-gen-types` التي تحول ملف `.proto` إلى ملفات تعريف TypeScript. أضفها كسكربت في `package.json`:

```json
{
  "scripts": {
    "gen": "proto-loader-gen-types --longs=String --enums=String --defaults --oneofs --grpcLib=@grpc/grpc-js -I proto --outDir=src/gen inventory/v1/inventory.proto",
    "typecheck": "tsc --noEmit",
    "server": "tsx src/server/index.ts",
    "client": "tsx src/client/index.ts",
    "test": "node --import tsx --test src/server/*.test.ts"
  }
}
```

قم بتشغيله:

```bash
npm run gen
```

الأعلام (flags) مهمة. `--longs=String` هو ما يحول حقول `int64` تلك إلى `string` في TypeScript (بما يتوافق مع تعيين JSON). تتحكم الأعلام `--enums=String` و `--defaults` و `--oneofs` في كيفية كتابة الـ enums والحقول المحذوفة ومجموعات `oneof`. مجلد التضمين `-I proto` بالإضافة إلى مسار `inventory/v1/inventory.proto` (بالنسبة لهذا المجلد) هو الاستدعاء الذي يحل الاستيرادات دون تحذير — تمرير المسار الكامل بدون `-I` مطابق يطبع رسالة محيرة "not found in any of the include paths".

تكتب `proto-loader-gen-types` شجرة تحت `src/gen`. نقطة الدخول هي `src/gen/inventory.ts`، والتي تصدر `ProtoGrpcType` تصف حزمتك بالكامل:

```TypeScript
export interface ProtoGrpcType {
  inventory: {
    v1: {
      GetProductRequest: MessageTypeDefinition<...>
      InventoryService: SubtypeConstructor<typeof grpc.Client, _inventory_v1_InventoryServiceClient> & { service: _inventory_v1_InventoryServiceDefinition }
      Product: MessageTypeDefinition<...>
      // ...one entry per message
    }
  }
}
```

إلى جانب ذلك، يصدر `src/gen/inventory/v1/InventoryService.ts` ثلاثة أنواع ستستخدمها مباشرة: `InventoryServiceClient` (العميل المكتوب)، و `InventoryServiceHandlers` (الشكل الذي يجب أن ينفذه خادمك)، و `InventoryServiceDefinition`.

> **فخ الـ camelCase.** لأننا *لم* نمرر `--keepCase`، يقوم `proto-loader` بتحويل حقول proto المكتوبة بصيغة `snake_case` إلى `camelCase` في TypeScript. لذا يصبح `price_cents` هو `priceCents`، ويصبح `reservation_id` هو `reservationId`، ويصبح `ts_unix_ms` هو `tsUnixMs`. إذا كتبت اسم snake\_case في الـ handler الخاص بك، سيرفضه المترجم الصارم (strict compiler) — وهذا هو بالضبط نوع الأمان الذي ولدت هذه الأنواع من أجله.

## الخطوة 4 — تحميل الـ proto بتعريف حزمة مكتوب النوع (typed)

تصف الأنواع المولدة *الشكل* (shape)؛ لكنك لا تزال بحاجة لتحميل واصف الخدمة الفعلي في وقت التشغيل (runtime). اجعل ذلك مركزيًا في `src/proto.ts` ليتشارك الخادم والعميل في محمل (loader) واحد:

```TypeScript
import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import type { ProtoGrpcType } from './gen/inventory';

const here = dirname(fileURLToPath(import.meta.url));
const PROTO_DIR = join(here, '..', 'proto');
const PROTO_PATH = join(PROTO_DIR, 'inventory', 'v1', 'inventory.proto');

// These options MUST match the flags passed to proto-loader-gen-types,
// or the runtime objects won't match the generated types.
export const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
  longs: String,
  enums: String,
  defaults: true,
  oneofs: true,
  includeDirs: [PROTO_DIR],
});

export const proto = grpc.loadPackageDefinition(
  packageDefinition,
) as unknown as ProtoGrpcType;
```

أحد المصادر الشائعة للأخطاء البرمجية من نوع "الأنواع تقول شيئًا ووقت التشغيل يفعل شيئًا آخر" هو عدم التطابق بين خيارات `loadSync` هذه وأعلام المولد (generator flags). حافظ على توافقهما: `longs: String` هنا تقترن بـ `--longs=String` هناك، وهكذا. عملية التحويل `as unknown as ProtoGrpcType` هي الجسر الموثق بين القيمة المرجعة غير محددة النوع لـ `loadPackageDefinition` وواجهتك المولدة.

## الخطوة 5 — تنفيذ الخادم

الآن الخدمة. أنشئ `src/server/index.ts`. ابدأ بمخزن في الذاكرة (in-memory store) ومحول (mapper) ينتج رسالة `Product` (لاحظ `priceCents` كسلسلة نصية):

```TypeScript
import * as grpc from '@grpc/grpc-js';
import { fileURLToPath } from 'node:url';
import { HealthImplementation } from 'grpc-health-check';
import { ReflectionService } from '@grpc/reflection';
import { proto, packageDefinition } from '../proto';
import type { InventoryServiceHandlers } from '../gen/inventory/v1/InventoryService';
import type { Product } from '../gen/inventory/v1/Product';

interface Row { id: string; name: string; stock: number; priceCents: bigint }

const store = new Map<string, Row>([
  ['sku-1', { id: 'sku-1', name: 'Mechanical Keyboard', stock: 12, priceCents: 8999n }],
  ['sku-2', { id: 'sku-2', name: 'USB-C Cable', stock: 0, priceCents: 1299n }],
]);

const toProduct = (r: Row): Product => ({
  id: r.id,
  name: r.name,
  stock: r.stock,
  priceCents: r.priceCents.toString(),
});
```

### المعالجات محددة النوع ونموذج الخطأ

يتم تحديد نوع كائن المعالجات (handlers) كـ `InventoryServiceHandlers`، لذا يتحقق المترجم من كل توقيع طريقة (method signature) مقابل الـ proto. يتلقى المعالج الأحادي (unary handler) المعاملات `(call, callback)`؛ تشير إلى النجاح باستخدام `callback(null, response)` وإلى الخطأ باستخدام `callback({ code, message })` مع كود من `grpc.status`:

```TypeScript
const handlers: InventoryServiceHandlers = {
  GetProduct(call, callback) {
    const row = store.get(call.request.id);
    if (!row) {
      callback({ code: grpc.status.NOT_FOUND, message: `product ${call.request.id} not found` });
      return;
    }
    callback(null, toProduct(row));
  },

  ReserveStock(call, callback) {
    const { id, quantity } = call.request;
    if (quantity <= 0) {
      const trailers = new grpc.Metadata();
      trailers.set('field-violation', 'quantity');
      callback({ code: grpc.status.INVALID_ARGUMENT, message: 'quantity must be > 0', metadata: trailers });
      return;
    }
    const row = store.get(id);
    if (!row) {
      callback({ code: grpc.status.NOT_FOUND, message: `product ${id} not found` });
      return;
    }
    if (row.stock < quantity) {
      callback({ code: grpc.status.FAILED_PRECONDITION, message: `only ${row.stock} unit(s) available` });
      return;
    }
    row.stock -= quantity;
    callback(null, { reservationId: `r_${Date.now()}`, remaining: row.stock });
  },

WatchStock(call) {
    const { id, updates } = call.request;
    const row = store.get(id);
    if (!row) {
      call.emit('error', { code: grpc.status.NOT_FOUND, message: `product ${id} not found` });
      return;
    }
    let sent = 0;
    const tick = () => {
      if (call.cancelled || sent >= updates) {
        call.end();
        return;
      }
      sent += 1;
      call.write({ id: row.id, stock: row.stock, tsUnixMs: Date.now().toString() });
      setTimeout(tick, 150);
    };
    tick();
  },
};
```

هذا هو جوهر معالجة أخطاء gRPC في Node.js. بدلاً من أكواد حالة HTTP، يمتلك gRPC مجموعته المعيارية الخاصة: `NOT_FOUND` (5)، و `INVALID_ARGUMENT` (3)، و `FAILED_PRECONDITION` (9)، و `UNAVAILABLE` (14)، و `DEADLINE_EXCEEDED` (4)، والبقية[^errors]. قم بربط إخفاقات مجالك (domain failures) بها بوعي: المنتج المفقود هو `NOT_FOUND`، والمدخل الخاطئ هو `INVALID_ARGUMENT`، و "لقد طلبت 5 ولكن يتوفر 0 فقط في المخزون" هو `FAILED_PRECONDITION`. عندما تحتاج إلى إرسال تفاصيل منظمة، أرفق كائن `grpc.Metadata` — أزواج المفتاح/القيمة هذه تنتقل في الـ trailers الخاصة بالمكالمة[^metadata]، ويمكن للعميل قراءتها من الخطأ.

معالج البث (streaming handler) مختلف. يتلقى `WatchStock` فقط `call`، وهو بث قابل للكتابة (writable stream). تقوم بدفع الرسائل باستخدام `call.write(...)` وتنهي البث بـ `call.end()`. لإفشال بث، تقوم بإصدار حدث `error` يحمل حالة. والأهم من ذلك، أنك تتحقق من `call.cancelled` قبل كل عملية كتابة: إذا أغلق العميل الاتصال أو انتهى الموعد النهائي (deadline) الخاص به، فإنك تتوقف عن العمل بدلاً من الكتابة في اتصال ميت.

### ربط فحوصات الصحة (health checks) والـ reflection

هناك شيئان يميزان الخادم الحقيقي عن الخادم التجريبي: نقطة نهاية للصحة (health endpoint) والـ reflection. كلاهما خدمات gRPC قياسية تقوم بتثبيتها على خادمك. تنفذ حزمة `grpc-health-check` بروتوكول `grpc.health.v1.Health` — وهو نفس البروتوكول الذي تتحدث به فحوصات الجاهزية (liveness probes) في Kubernetes `grpc`[^healthproto]:

```TypeScript
export function buildServer(): grpc.Server {
  const server = new grpc.Server();
  server.addService(proto.inventory.v1.InventoryService.service, handlers);

  // Standard health checking protocol (grpc.health.v1.Health).
  const health = new HealthImplementation({ 'inventory.v1.InventoryService': 'SERVING' });
  health.addToServer(server);

  // Server reflection so grpcurl / Postman can discover the schema at runtime.
  const reflection = new ReflectionService(packageDefinition);
  reflection.addToServer(server);

return server;
}
```

تأخذ `HealthImplementation` خريطة حالة أولية مفتاحها اسم الخدمة، حيث تكون `'SERVING'` و `'NOT_SERVING'` و `'UNKNOWN'` هي الحالات القانونية؛ استدعِ `health.setStatus(name, 'NOT_SERVING')` عندما يتعطل أحد التبعيات (dependencies) حتى تتوقف أدوات الإدارة (orchestrators) عن توجيه الطلبات إليك. تأخذ `ReflectionService` نفس الـ `packageDefinition` الذي قمت بتحميله بالفعل، وبمجرد تثبيتها، يمكن لأدوات مثل `grpcurl` سرد واستدعاء طرقك دون أن ترسل لها ملف الـ `.proto`.

### بدء تشغيل الخادم

أخيرًا، الربط والبدء. في `@grpc/grpc-js`، تقوم `bindAsync` ببدء الخادم لك — استدعاء `server.start()` القديم تم تمييزه كـ `@deprecated` ("لم يعد مطلوبًا اعتبارًا من الإصدار 1.10.x") واستدعاؤه الآن يطبع تحذيرًا فقط[^start]:

```TypeScript
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const server = buildServer();
const addr = process.env.ADDR ?? '127.0.0.1:50051';
server.bindAsync(addr, grpc.ServerCredentials.createInsecure(), (err, port) => {
if (err) { console.error(err); process.exit(1); }
console.log(`InventoryService listening on ${addr} (port ${port})`);
});
const shutdown = () => server.tryShutdown(() => process.exit(0));
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
}
```

استخدام `createInsecure()` جيد على localhost؛ في بيئة الإنتاج ستقوم بتمرير بيانات اعتماد TLS حقيقية. تمنح معالجات `tryShutdown` المكالمات الجارية فرصة للانتهاء عند استلام إشارة `SIGTERM` بدلاً من قطعها — وهو نفس نهج التصريف التدريجي (graceful-drain) الذي ستطبقه على أي عملية Node طويلة الأمد. قم بتشغيله:

```bash
npm run server
# InventoryService listening on 127.0.0.1:50051 (port 50051)
```

## الخطوة 6 — كتابة عميل محدد النوع مع موعد نهائي (deadline)

أنشئ `src/client/index.ts`. يوجد منشئ (constructor) `InventoryServiceClient` المولد في `proto.inventory.v1.InventoryService`. يمنحك تغليف المكالمة الأحادية بأسلوب الـ callback في Promise إمكانية استخدام `async`/`await`، والمكان المناسب لتعيين الموعد النهائي هو معامل `CallOptions`:

```TypeScript
import * as grpc from '@grpc/grpc-js';
import { proto } from '../proto';
import type { InventoryServiceClient } from '../gen/inventory/v1/InventoryService';
import type { Product__Output } from '../gen/inventory/v1/Product';

const ADDR = process.env.ADDR ?? '127.0.0.1:50051';

export function makeClient(addr = ADDR): InventoryServiceClient {
return new proto.inventory.v1.InventoryService(
addr,
grpc.credentials.createInsecure(),
);
}

// Promise wrapper around the unary GetProduct call, with a deadline.
function getProduct(
client: InventoryServiceClient,
id: string,
deadlineMs = 2000,
): Promise<Product__Output> {
const options: grpc.CallOptions = { deadline: new Date(Date.now() + deadlineMs) };
return new Promise((resolve, reject) => {
client.GetProduct({ id }, options, (err, res) => {
if (err || !res) { reject(err); return; }
resolve(res);
});
});
}
```

الـ **deadline في gRPC هو نقطة زمنية مطلقة**، وليس مدة. تقوم بحسابه كـ `new Date(Date.now() + ms)` وتمرره كـ `options.deadline`. يقوم gRPC بنشر هذا الموعد النهائي عبر المكالمة؛ إذا لم يستجب الخادم بحلول ذلك الوقت، تفشل المكالمة محليًا بحالة `DEADLINE_EXCEEDED` (4). يعد هذا أحد أهم مقابض الموثوقية في gRPC — قم دائمًا بتعيين موعد نهائي، لأنه افتراضيًا لا يوجد موعد نهائي للمكالمة وستنتظر إلى الأبد.

الآن جرب جميع الـ RPCs الثلاثة. ترجع مكالمة البث بثًا قابلاً للقراءة تستهلكه باستخدام أحداث `data` و `error` و `end`:

```TypeScript
async function main(): Promise<void> {
const client = makeClient();

const product = await getProduct(client, 'sku-1');
console.log('GetProduct:', product);

await new Promise<void>((resolve) => {
const stream = client.WatchStock({ id: 'sku-1', updates: 3 });
stream.on('data', (u) => console.log('StockUpdate:', u));
stream.on('error', (e: grpc.ServiceError) => { console.error('stream error', e.code); resolve(); });
stream.on('end', () => resolve());
});

client.ReserveStock({ id: 'sku-2', quantity: 5 }, (err, res) => {
if (err) console.error('ReserveStock failed:', grpc.status[err.code], err.details);
else console.log('ReserveStock:', res);
client.close();
});
}

main().catch((e) => { console.error(e); process.exit(1); });
```

بينما يعمل الخادم في نافذة terminal واحدة، قم بتشغيل العميل في نافذة أخرى:

```bash
npm run client
```

```text
GetProduct: {
  id: 'sku-1',
  name: 'Mechanical Keyboard',
  stock: 12,
  priceCents: '8999'
}
StockUpdate: { id: 'sku-1', stock: 12, tsUnixMs: '1781429057452' }
StockUpdate: { id: 'sku-1', stock: 12, tsUnixMs: '1781429057607' }
StockUpdate: { id: 'sku-1', stock: 12, tsUnixMs: '1781429057762' }
ReserveStock failed: FAILED_PRECONDITION only 0 unit(s) available
```

تصل `priceCents` كسلسلة نصية `'8999'` (نتيجة تحويل int64 إلى string)، ويقدم البث ثلاثة تحديثات بالضبط، ويعود طلب الحجز الزائد مقابل `sku-2` غير المتوفر في المخزون كـ `FAILED_PRECONDITION`. يقوم `grpc.status[err.code]` بتحويل الكود الرقمي مرة أخرى إلى اسمه المقروء للسجلات؛ `err.details` هي رسالة الخادم المجردة، بينما يضيف `err.message` بالإضافة إلى ذلك الكود الرقمي واسم الحالة كبادئة.

## التحقق

يثبت تشغيل العميل يدويًا المسارات الناجحة (happy paths)؛ بينما تثبت مجموعة الاختبارات الآلية مسارات الفشل وتثبت السلوك بحيث لا يمكن لأي إعادة هيكلة للكود (refactor) كسرها بصمت. الحيلة هي ربط الخادم بـ `127.0.0.1:0` — يطلب المنفذ صفر من نظام التشغيل منفذًا متاحًا — بحيث يكون الاختبار معزولاً ولا يتعارض أبدًا مع خادم قيد التشغيل. أنشئ `src/server/index.test.ts`:

```TypeScript
import { test, before, after } from 'node:test';
import assert from 'node:assert/strict';
import * as grpc from '@grpc/grpc-js';
import { buildServer } from './index';
priceCents
```
