backend

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

١٤ يونيو ٢٠٢٦

gRPC in Node.js and TypeScript: Production Guide (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.41، @grpc/proto-loader 0.8.12، grpc-health-check 2.1.03، و @grpc/reflection 1.0.44 على TypeScript 6.0.35. تم فحص الأنواع في كل ملف تحت إعدادات 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)6 أو 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 بدلاً منه7. حزمة @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 — هيكلة المشروع

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

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:

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

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

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

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

ملف .proto هو العقد. أنشئ proto/inventory/v1/inventory.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)8. سترى ذلك ينعكس في الأنواع المولدة بعد قليل.

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

توفر @grpc/proto-loader واجهة سطر أوامر proto-loader-gen-types التي تحول ملف .proto إلى ملفات تعريف TypeScript. أضفها كسكربت في package.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"
  }
}

قم بتشغيله:

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 تصف حزمتك بالكامل:

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) واحد:

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 كسلسلة نصية):

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:

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)، والبقية9. قم بربط إخفاقات مجالك (domain failures) بها بوعي: المنتج المفقود هو NOT_FOUND، والمدخل الخاطئ هو INVALID_ARGUMENT، و "لقد طلبت 5 ولكن يتوفر 0 فقط في المخزون" هو FAILED_PRECONDITION. عندما تحتاج إلى إرسال تفاصيل منظمة، أرفق كائن grpc.Metadata — أزواج المفتاح/القيمة هذه تنتقل في الـ trailers الخاصة بالمكالمة10، ويمكن للعميل قراءتها من الخطأ.

معالج البث (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 grpc11:

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") واستدعاؤه الآن يطبع تحذيرًا فقط12:

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 طويلة الأمد. قم بتشغيله:

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:

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:

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 واحدة، قم بتشغيل العميل في نافذة أخرى:

npm run client
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:

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?" لقد كتبت اسم حقل بتنسيق snake_case. بدون --keepCase، يقوم proto-loader بتحويل الحقول إلى تنسيق camel-case. استخدم priceCents، و reservationId، و tsUnixMs — أو أعد التوليد باستخدام --keepCase إذا كنت تفضل snake_case في كل مكان.
  • الأنواع ووقت التشغيل غير متوافقين (حقل قيمته undefined بينما كنت تتوقعه، أو قيمة 64-بت عبارة عن رقم وليس سلسلة نصية). خيارات protoLoader.loadSync الخاصة بك انحرفت عن أعلام proto-loader-gen-types. حافظ على تطابق longs، و enums، و defaults، و oneofs في كلا المكانين.
  • DeprecationWarning بخصوص server.start(). احذف استدعاء server.start(). بدءاً من @grpc/grpc-js الإصدار 1.10.x، يقوم bindAsync ببدء تشغيل الخادم؛ أما start() فهي عملية لا تفعل شيئاً (no-op) وتم الإبقاء عليها فقط للتوافق مع الإصدارات السابقة.
  • مكالمة البث (streaming call) تتوقف ولا تنتهي أبداً. لم يقم المعالج الخاص بك باستدعاء call.end() أبداً (أو لم يتوقف عن الكتابة أبداً). قم دائماً بإنهاء البث، واربط عمليات الكتابة بـ call.cancelled حتى يتوقف العميل المنقطع عن الحلقة التكرارية.
  • الخطوات التالية

    لديك الآن خدمة gRPC ذات أنواع محددة مع البث، والمواعيد النهائية (deadlines)، ونموذج خطأ حقيقي، وفحوصات الحالة — وهو شكل الخدمة التي يمكنك تشغيلها فعلياً. من هنا:

    • أضف mutual TLS عن طريق استبدال createInsecure() بـ grpc.ServerCredentials.createSsl(...) وبيانات اعتماد العميل المطابقة. اربطها بأنماط النشر بدون مفاتيح (keyless-deploy) في دليل GitHub Actions OIDC حتى لا تظل أي شهادات طويلة الأمد في CI.
    • ضع الخدمة خلف نشر بدون توقف (zero-downtime rollout): إن انضباط تصريف preStop وفحص الجاهزية (readiness-probe) في عمليات النشر بدون توقف في Kubernetes ينطبق مباشرة على نقطة نهاية grpc.health.v1.Health التي قمت بتركيبها للتو.
    • قم بتهيئة أدوات القياس (Instrument it). يوفر لك إعداد المجمع (collector) في درس تتبع OpenTelemetry في Node.js تتبعاً لكل RPC (spans)، وزمن الاستجابة، وتحليلات أكواد الحالة بمجرد إضافة أدوات قياس gRPC.

    المصدر الكامل — proto، والأنواع المولدة، والخادم، والعميل، ومجموعة الاختبارات الستة — يعمل باستخدام npm install، و npm run gen، و npm test. تم تنفيذ كل أمر في هذا الدليل في 14 يونيو 2026 مقابل الإصدارات المثبتة أعلاه.

    المصادر

    Footnotes

    1. @grpc/grpc-js, npm. https://www.npmjs.com/package/@grpc/grpc-js

    2. @grpc/proto-loader, npm. https://www.npmjs.com/package/@grpc/proto-loader

    3. grpc-health-check, npm and README. https://GitHub.com/grpc/grpc-node/blob/master/packages/grpc-health-check/README.md

    4. @grpc/reflection, npm. https://www.npmjs.com/package/@grpc/reflection

    5. TypeScript releases, npm. https://www.npmjs.com/package/TypeScript

    6. Node.js Release schedule (Node 24 "Krypton" Active LTS through 2028-04-30). https://GitHub.com/nodejs/Release

    7. "Announcing gRPC-JS 1.0" and the grpc package deprecation, gRPC blog / npm. https://grpc.io/blog/grpc-js-1.0/

    8. Protocol Buffers proto3 JSON mapping (64-bit integers encode as strings). https://protobuf.dev/programming-guides/proto3/#json

    9. Error handling and status codes, gRPC docs. https://grpc.io/docs/guides/error/

    10. Metadata, gRPC docs. https://grpc.io/docs/guides/metadata/

    11. gRPC Health Checking Protocol. https://GitHub.com/grpc/grpc/blob/master/doc/health-checking.md

    12. Server.start() deprecation ("No longer needed as of version 1.10.x"), @grpc/grpc-js type definitions. https://GitHub.com/grpc/grpc-node/tree/master/packages/grpc-js