كان نموذج الاستدعاء في جافا سكريبت الآلية الوحيدة للإدخال والإخراج غير المتزامن عند ظهور Node.js عام ٢٠٠٩. وبحلول وقت ظهور الوعود في ES6، ثم async/await في ES2017، كانت معظم قواعد البيانات البرمجية المستخدمة في الإنتاج تعتمد بالفعل على منطق الاستدعاء لسنوات: معالجات الأخطاء المتداخلة، وحالة قابلة للتغيير مشتركة عبر الدوال المغلقة، ومنطق إعادة المحاولة المضمن في دوال مجهولة بثلاثة مستويات. تغيرت الصيغة، لكن الكود قيد التشغيل لم يتغير. واليوم، ترث معظم فرق الهندسة هذا الوضع تحديدًا: قاعدة بيانات تتعايش فيها الأنماط الجديدة والقديمة، حيث يرغب الفريق في الترقية لكن النظام لا يستطيع التوقف أثناء ذلك.
قم بنقل قاعدة التعليمات البرمجية غير المتزامنة الخاصة بك بأمان
SMART TS XL ويحدد مخاطر الترحيل عبر قاعدة التعليمات البرمجية بأكملها قبل تغيير أي سطر.
اكتشف المزيدالخبر السار هو أن عملية الترحيل لا تتطلب إعادة كتابة الكود. تتكامل وظائف الاستدعاء، والوعود، و async/await ضمن حدود محددة بوضوح. يأتي Node.js مزودًا بـ util.promisify يهدف هذا الحل تحديدًا إلى سد الفجوة بين دوال الاستدعاء التي تعالج الأخطاء أولًا والدوال التي تُرجع وعودًا. تسمح طبقات التغليف بتعايش الكود القديم والجديد أثناء عملية الانتقال. يضمن الترحيل التدريجي، وحدةً تلو الأخرى، استمرار تشغيل بيئة الإنتاج مع تقدم قاعدة الكود. لا يكمن التحدي في عملية التحويل نفسها، بل في تنفيذها بشكل منهجي: فهم أي دوال الاستدعاء آمنة للتحويل أولًا، وأي أنماط البرمجة الخاطئة ستؤدي إلى أخطاء إذا أُعيدت كتابتها بشكل عشوائي، وكيفية التحقق من أن كل دالة مُحولة تتصرف تمامًا مثل الدالة التي استبدلتها.
فهم نموذج الاستدعاء وأسباب فشله عند التعامل مع كميات كبيرة من البيانات
اصطلاح الاستدعاء في Node.js بسيط: تقبل الدوال التي تُنفذ عمليات غير متزامنة دالة استدعاء كوسيطها الأخير. تستقبل دالة الاستدعاء خطأً كوسيطها الأول، والنتيجة كوسيطها الثاني. تتبع جميع دوال المكتبة القياسية في Node.js هذا الاصطلاح. fs.readFile, http.get, child_process.execومئات غيرهم.
جافا سكريبت
// Standard Node.js error-first callback pattern
const fs = require('fs');
fs.readFile('config.json', 'utf8', (err, data) => {
if (err) {
console.error('Failed to read config:', err);
return;
}
const config = JSON.parse(data);
console.log('Config loaded:', config);
});
يُعدّ هذا النمط بسيطًا لعملية واحدة. لكنه يصبح غير عملي عند ربط العمليات، إذ يجب بدء كل عملية لاحقة داخل دالة الاستدعاء السابقة. ينتج عن تسلسل من ثلاث خطوات (قراءة، تحويل، كتابة) ثلاثة مستويات متداخلة من دوال الاستدعاء. بينما ينتج عن تسلسل من خمس خطوات خمسة مستويات. هذا هو الهيكل الذي يُطلق عليه المطورون "جحيم دوال الاستدعاء" أو "هرم الهلاك"، وهي ليست مجرد مشكلة جمالية. فدوال الاستدعاء المتداخلة بعمق تجعل معالجة الأخطاء غير متسقة (إذ يجب على كل مستوى التحقق من وسيط الخطأ الخاص به بشكل مستقل)، وتجعل ترتيب التنفيذ صعب الفهم، وتجعل إعادة هيكلة الكود أمرًا خطيرًا لأن تدفق البيانات يكون ضمنيًا وليس صريحًا.
تكمن المشكلة الأكثر أهمية لأنظمة الإنتاج في أن وظائف الاستدعاء لا تملك آلية مدمجة للتنسيق. يتطلب تشغيل عمليتين غير متزامنتين وانتظار اكتمالهما تتبعًا يدويًا للعدادات. كما يتطلب تشغيل سلسلة من العمليات على مصفوفة أنماطًا تكرارية أو مكتبات خارجية مثل async. لا يظهر أي من هذا التعقيد في توقيعات الوظائف: واجهة برمجة التطبيقات القائمة على ردود الاتصال والتنسيق المتزامن المبني عليها تبدو متطابقة من الخارج، حتى تفشل.
ما يبدو عليه جحيم المكالمات الواردة في الواقع
هرم استدعاءات واقعي من خدمة Node.js تقرأ بيانات المستخدم، وتتحقق من الأذونات، وتسجل الوصول:
جافا سكريبت
// Three-level callback pyramid -- representative of real legacy code
function getUserReport(userId, callback) {
db.query('SELECT * FROM users WHERE id = ?', [userId], (err, user) => {
if (err) return callback(err);
if (!user) return callback(new Error('User not found'));
permissions.check(userId, 'read:reports', (err, allowed) => {
if (err) return callback(err);
if (!allowed) return callback(new Error('Permission denied'));
auditLog.write({ userId, action: 'read:reports' }, (err) => {
if (err) return callback(err);
// Finally, the actual work
callback(null, buildReport(user));
});
});
});
}
تتكرر معالجة الأخطاء في كل مستوى. يؤدي التباعد إلى تعقيد تدفق البيانات بصريًا. تتطلب إضافة خطوة رابعة مستوىً متداخلًا آخر. يتطلب اختبار هذه الوظيفة محاكاة جميع التبعيات الثلاث بالترتيب، كما تتطلب محاكاة خطأ في المستوى الثالث محاكاة أول اثنين بنجاح. تتفاقم كل هذه الخصائص مع ازدياد طول السلسلة.
استخدام util.promisify لربط الاستدعاءات والوعود
تم تقديم Node.js 8.0 util.promisifyتقوم هذه الدالة بتحويل أي دالة تتبع اصطلاح الاستدعاء القياسي الذي يعتمد على الخطأ أولاً إلى دالة تُرجع وعدًا (Promise). تُعد هذه نقطة البداية الصحيحة لأي عملية ترحيل باستخدام async/await في قاعدة بيانات Node.js.
جافا سكريبت
const { promisify } = require('util');
const fs = require('fs');
// Convert Node.js built-ins
const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);
// Now usable with async/await
async function processConfig(path) {
const data = await readFile(path, 'utf8');
const config = JSON.parse(data);
config.lastLoaded = Date.now();
await writeFile(path, JSON.stringify(config, null, 2), 'utf8');
return config;
}
util.promisify يتعامل النظام تلقائيًا مع اصطلاح "الخطأ أولًا": إذا استقبلت دالة الاستدعاء وسيطًا أولًا غير فارغ، فإن الوعد المُعاد يُرفض مع هذا الخطأ. أما إذا استقبلت دالة الاستدعاء وسيطًا أولًا فارغًا ونتيجة، فإن الوعد يُنفذ مع النتيجة. بالنسبة للغالبية العظمى من واجهات برمجة تطبيقات Node.js الأساسية ومكتبات الطرف الثالث التي تتبع هذا الاصطلاح، لا حاجة إلى تغليف مخصص.
تحويل وظائف الاستدعاء المخصصة إلى وعود
بالنسبة للوظائف المخصصة التي تتبع اصطلاح معالجة الأخطاء أولاً ولكنها ليست وظائف مضمنة في Node.js، util.promisify يعمل بنفس الطريقة:
جافا سكريبت
const { promisify } = require('util');
// Your existing callback-based function
function fetchUserFromDB(userId, callback) {
db.query('SELECT * FROM users WHERE id = ?', [userId], (err, rows) => {
if (err) return callback(err);
callback(null, rows[0] || null);
});
}
// Promisified version -- no changes to the original function needed
const fetchUser = promisify(fetchUserFromDB);
// Use in async context
async function getUser(userId) {
const user = await fetchUser(userId);
if (!user) throw new Error(`User ${userId} not found`);
return user;
}
هذا النهج مهم: util.promisify يُغلّف هذا الإصدار الدالة الأصلية دون تعديلها. ويستمر إصدار الاستدعاء الأصلي في العمل. وتستمر الدوال التي لم تُرحّل بعد في استخدام إصدار الاستدعاء. أما الدوال التي رُحّلت فتستخدم الإصدار المُحوّل إلى وعود. هذا التعايش هو ما يُتيح الترحيل التدريجي.
معالجة توقيعات الاستدعاء غير القياسية
تقوم بعض المكتبات القديمة بتمرير قيم نتائج متعددة إلى وظائف الاستدعاء الخاصة بها، والتي util.promisify تُختزل إلى القيمة الأولى فقط. في هذه الحالات، util.promisify.custom يسمح الرمز بتحديد وعد مخصص:
جافا سكريبت
const { promisify } = require('util');
// A function that passes two results to its callback
function parseData(input, callback) {
// callback(err, parsedData, metadata)
callback(null, { value: input.trim() }, { length: input.length });
}
// Custom promisification that returns both results
parseData[promisify.custom] = (input) => {
return new Promise((resolve, reject) => {
parseData(input, (err, data, meta) => {
if (err) reject(err);
else resolve({ data, meta });
});
});
};
const parseDataAsync = promisify(parseData);
const result = await parseDataAsync(' hello ');
// result === { data: { value: 'hello' }, meta: { length: 9 } }
تحويل الدوال المستدعاة إلى وعود: النمط التدريجي
بالنسبة للتعليمات البرمجية التي لا يمكن معالجتها بواسطة util.promisify بشكل مباشر، يُعد غلاف Promise اليدوي مسار الترحيل. النمط متسق:
جافا سكريبت
// Step 1: Original callback-based function
function checkPermission(userId, resource, callback) {
acl.check({ userId, resource }, (err, result) => {
if (err) return callback(err);
callback(null, result.allowed);
});
}
// Step 2: Promise wrapper (coexists with the original)
function checkPermissionAsync(userId, resource) {
return new Promise((resolve, reject) => {
checkPermission(userId, resource, (err, allowed) => {
if (err) reject(err);
else resolve(allowed);
});
});
}
// Step 3: async/await consumer
async function authorizeRequest(userId, resource) {
const allowed = await checkPermissionAsync(userId, resource);
if (!allowed) {
throw new Error(`${userId} does not have access to ${resource}`);
}
}
تم إعادة كتابة هرم الاستدعاء ذي المستويات الثلاثة من السابق باستخدام async/await:
جافا سكريبت
// After migration: same logic, linear structure
async function getUserReport(userId) {
const user = await db.queryAsync('SELECT * FROM users WHERE id = ?', [userId]);
if (!user) throw new Error('User not found');
const allowed = await permissions.checkAsync(userId, 'read:reports');
if (!allowed) throw new Error('Permission denied');
await auditLog.writeAsync({ userId, action: 'read:reports' });
return buildReport(user);
}
تتم معالجة الأخطاء الآن بواسطة كتلة try/catch واحدة عند نقطة الاستدعاء بدلاً من تكرارها في كل مستوى. المسافة البادئة ثابتة. إضافة خطوة رابعة تتطلب خطوة إضافية. await يتطلب الاختبار محاكاة كل تبعية على حدة، بأي ترتيب.
التنفيذ المتوازي مع Promise.all وPromise.allSettled
من أكثر الأخطاء شيوعًا عند الانتقال من الدوال المستدعاة إلى async/await هو التنفيذ التسلسلي للعمليات التي يمكن تنفيذها بالتوازي. جعلت الدوال المستدعاة التنفيذ المتوازي معقدًا لدرجة أن العديد من المطورين لجأوا إلى السلاسل التسلسلية. أما async/await فيجعل التوازي يبدو تسلسليًا، مما قد يُضلل المطورين ويدفعهم إلى كتابة كود أبطأ من الكود السابق الذي كان يعتمد على الدوال المستدعاة.
جافا سكريبت
// WRONG: sequential execution -- each awaits the previous result
async function loadDashboardData(userId) {
const profile = await fetchProfile(userId); // 100ms
const orders = await fetchOrders(userId); // 150ms
const reviews = await fetchReviews(userId); // 80ms
return { profile, orders, reviews }; // Total: ~330ms
}
// RIGHT: parallel execution with Promise.all
async function loadDashboardData(userId) {
const [profile, orders, reviews] = await Promise.all([
fetchProfile(userId),
fetchOrders(userId),
fetchReviews(userId),
]);
return { profile, orders, reviews }; // Total: ~150ms
}
Promise.all يرفض إذا رفض أي وعد في المصفوفة. عندما يمكن أن تفشل العمليات المستقلة دون أن تؤثر على بعضها البعض، ويحتاج المستدعي إلى معرفة جميع حالات الفشل، Promise.allSettled هي الأداة الصحيحة:
جافا سكريبت
// Promise.allSettled: runs all, reports success or failure per operation
async function syncAllSources(userId) {
const results = await Promise.allSettled([
syncFromGitHub(userId),
syncFromJira(userId),
syncFromSlack(userId),
]);
const failed = results
.filter(r => r.status === 'rejected')
.map(r => r.reason.message);
if (failed.length > 0) {
console.warn('Some syncs failed:', failed);
}
return results
.filter(r => r.status === 'fulfilled')
.map(r => r.value);
}
لا يوجد لهذا النمط مكافئ طبيعي في التعليمات البرمجية القائمة على ردود الاتصال بدون عداد يدوي أو مكتبة مثل async.parallelالانتقال إلى Promise.allSettled غالباً ما يكون أحد التغييرات ذات التأثير الأكبر في قاعدة بيانات التعليمات البرمجية غير المتزامنة القديمة.
تجنب نمط الانتظار في الحلقة المفرغة
يظهر خطأ التسلسل بدلاً من التوازي في أغلب الأحيان داخل الحلقات:
جافا سكريبت
// WRONG: sequential -- processes items one at a time
async function processOrders(orderIds) {
const results = [];
for (const id of orderIds) {
const result = await processOrder(id); // blocks until each completes
results.push(result);
}
return results;
}
// RIGHT: parallel -- all orders processed concurrently
async function processOrders(orderIds) {
return Promise.all(orderIds.map(id => processOrder(id)));
}
// RIGHT (with concurrency limit): parallel but bounded
const pLimit = require('p-limit');
const limit = pLimit(5); // max 5 concurrent
async function processOrders(orderIds) {
return Promise.all(
orderIds.map(id => limit(() => processOrder(id)))
);
}
يُعد نمط await-in-loop أحد أكثر التراجعات شيوعًا التي تم إدخالها أثناء ترحيل async/await للتعليمات البرمجية القائمة على ردود الاتصال، لأن ردود الاتصال أجبرت المطورين على التفكير في التزامن بشكل صريح بينما يحجب async/await ذلك.
معالجة الأخطاء في Async/Await: استبدال نشر أخطاء الاستدعاء
تُمرر الدوال المستدعاة الأخطاء وفقًا للاتفاقية: الوسيط الأول لكل دالة مستدعاة هو خطأ أو قيمة فارغة. هذا الأسلوب فعال، لكنه يتطلب من كل مستدعي التحقق يدويًا من وسيط الخطأ. أما Async/await فيمرر الأخطاء عبر آلية رفض الوعود، والتي تتكامل مع بنية try/catch الأصلية في جافا سكريبت.
جافا سكريبت
// Callback error propagation: repeated at every level
function processPayment(orderId, callback) {
validateOrder(orderId, (err, order) => {
if (err) return callback(err); // propagate
chargeCard(order.amount, (err, charge) => {
if (err) return callback(err); // propagate again
updateInventory(orderId, (err) => {
if (err) return callback(err); // propagate again
callback(null, charge.id);
});
});
});
}
// Async/await: error propagation is automatic
async function processPayment(orderId) {
const order = await validateOrder(orderId); // throws on error
const charge = await chargeCard(order.amount); // throws on error
await updateInventory(orderId); // throws on error
return charge.id;
}
من الاعتبارات الأساسية أثناء عملية الترحيل الحفاظ على سياق الخطأ. غالبًا ما يمرر الكود القائم على ردود الاتصال أخطاءً مُعززة بمعلومات سياقية على كل مستوى. عند الترحيل إلى async/await، تأكد من أن تغليف الخطأ يحافظ على هذا السياق.
جافا سكريبت
// Preserving error context during async migration
async function processPayment(orderId) {
let order;
try {
order = await validateOrder(orderId);
} catch (err) {
throw new Error(`Payment validation failed for order ${orderId}: ${err.message}`);
}
try {
const charge = await chargeCard(order.amount);
await updateInventory(orderId);
return charge.id;
} catch (err) {
// Attempt rollback, then rethrow with context
await refundCharge(order.amount).catch(console.error);
throw new Error(`Payment processing failed for order ${orderId}: ${err.message}`);
}
}
رفض الوعود غير المعالجة
يتجاهل الكود القائم على الاستدعاءات الأخطاء بصمت عند تجاهل وسيط الخطأ. ينتج عن استخدام async/await رفض غير معالج للوعود، مما يؤدي في Node.js 15 والإصدارات الأحدث إلى إنهاء العملية افتراضيًا. يُعد هذا تغييرًا جذريًا عند الترقية: فالكود الذي كان يفشل سابقًا بصمت سيتعطل الآن.
جافا سكريبت
// This produces an unhandled rejection in Node.js 15+
async function riskyOperation() {
throw new Error('Something failed');
}
riskyOperation(); // Promise rejected, but rejection is not caught
// Fix: always await or chain .catch()
await riskyOperation(); // throws, caller handles it
riskyOperation().catch(console.error); // handles inline
قم بمراجعة جميع استدعاءات الدوال غير المتزامنة التي تُنفذ دون انتظار أو ربطها بسلسلة من الدوال. أي استدعاء لدالة غير متزامنة لا يتم انتظار قيمتها المُعادة أو ربطها بسلسلة من الدوال. .catch() يُعد فشلاً صامتاً محتملاً في عالم الاستدعاءات، ولكنه عطل رفض غير معالج في عالم async/await.
ترحيل أنماط EventEmitter إلى الوعود والمكررات غير المتزامنة
تُعدّ مُصدرات الأحداث في Node.js نمطًا من أنماط الاستدعاء، حيث يتم تسجيل عدة استدعاءات لأحداث مُسماة. وهي شائعة في تدفقات البيانات، واتصالات الشبكة، وحافلات الأحداث المُخصصة. ويتطلب الانتقال المباشر إلى async/await تغليف واجهة برمجة التطبيقات القائمة على الأحداث.
جافا سكريبت
const { EventEmitter } = require('events');
// Original EventEmitter-based pattern
function fetchDataLegacy(source) {
const emitter = new EventEmitter();
setTimeout(() => {
emitter.emit('data', { records: [1, 2, 3] });
emitter.emit('end');
}, 100);
return emitter;
}
// Usage: callback registration
const stream = fetchDataLegacy('api');
stream.on('data', chunk => console.log('received', chunk));
stream.on('error', err => console.error('error', err));
stream.on('end', () => console.log('done'));
تحويل حدث لمرة واحدة إلى وعد هو أمر بسيط:
جافا سكريبت
// Converting a single-event completion to Promise
function waitForEvent(emitter, successEvent, errorEvent = 'error') {
return new Promise((resolve, reject) => {
emitter.once(successEvent, resolve);
emitter.once(errorEvent, reject);
});
}
async function fetchData(source) {
const emitter = fetchDataLegacy(source);
const data = await waitForEvent(emitter, 'data');
await waitForEvent(emitter, 'end');
return data;
}
بالنسبة للتدفقات التي تُصدر أحداث بيانات متعددة، يوفر Node.js events.on والذي يُعيد مُكرِّرًا غير متزامن، مما يسمح باستهلاك التدفق الكامل باستخدام for await...of:
جافا سكريبت
const { on } = require('events');
async function processStream(readable) {
for await (const chunk of on(readable, 'data')) {
await processChunk(chunk);
}
}
تُعتبر تدفقات Node.js القابلة للقراءة قابلة للتكرار بشكل مباشر كعناصر قابلة للتكرار غير متزامنة منذ Node 10:
جافا سكريبت
const fs = require('fs');
async function countLines(filePath) {
let lines = 0;
const stream = fs.createReadStream(filePath, { encoding: 'utf8' });
for await (const chunk of stream) {
lines += chunk.split('\n').length - 1;
}
return lines;
}
أنماط ترحيل TypeScript غير المتزامنة/الانتظار
تتطلب قواعد بيانات TypeScript اعتبارات إضافية عند الانتقال إلى async/await. يجب تحديث أنواع الإرجاع من توقيعات الاستدعاء إلى Promise<T>ويفرض المترجم أن يتم استخدام await فقط داخل الدوال غير المتزامنة.
نسخة مطبوعة على الآلة الكاتبة
// Before: callback signature
function fetchUser(
id: string,
callback: (err: Error | null, user: User | null) => void
): void {
db.findOne({ id }, callback);
}
// After: async/await signature with proper return type
async function fetchUser(id: string): Promise<User> {
const user = await db.findOneAsync<User>({ id });
if (!user) throw new Error(`User ${id} not found`);
return user;
}
تتفاعل عمليات التحقق الصارمة من القيم الفارغة في TypeScript مع التعليمات البرمجية غير المتزامنة بطريقة تكشف أخطاء الترحيل الشائعة. إذا أعادت دالة ما قيمة فارغة سابقًا User | null من خلال رد نداء، ويغير الترحيل ذلك إلى Promise<User> (باستخدام throwing بدلاً من إرجاع null)، سيلتقط TypeScript المتصلين الذين تحققوا من null ولكنهم لم يعودوا بحاجة إلى ذلك، والمتصلين الذين لا يتحققون من الأخطاء التي يحتاجون الآن إلى معالجتها.
بالنسبة لبرامج TypeScript القديمة التي تستخدم @types/node توقيعات الاستدعاء، util.promisify يتم تحديد نوع البيانات بالكامل ويستنتج نوع الإرجاع الصحيح لـ Promise لوظائف Node.js المضمنة تلقائيًا.
استراتيجية الترحيل التدريجي لأنظمة الإنتاج
لا يمكن ترحيل نظام الإنتاج دفعة واحدة. يعتمد النهج التدريجي على تحويل وحدة واحدة في كل مرة، والتحقق من صحتها، ثم الانتقال إلى الوحدة التالية. يكمن السر في الحفاظ على حدود واضحة بين الكود المُحوَّل وغير المُحوَّل في كل مرحلة.
ينبغي أن يتبع ترتيب الترحيل اتجاه التبعيات: تحويل التبعيات الأعمق أولاً، ثم الانتقال تصاعدياً نحو الدوال المستدعِية. يضمن هذا أنه بحلول وقت تحويل دالة ذات مستوى أعلى، تكون تبعياتها قد بدأت بالفعل في إرجاع وعود، وبالتالي لم تعد طبقة التغليف ضرورية.
جافا سكريبت
// Stage 1: Promisify the data layer (deepest dependency)
const db = {
queryAsync: promisify(db.query.bind(db)),
insertAsync: promisify(db.insert.bind(db)),
};
// Stage 2: Convert the service layer (depends on db)
class UserService {
async getUser(id) {
return db.queryAsync('SELECT * FROM users WHERE id = ?', [id]);
}
async createUser(data) {
return db.insertAsync('users', data);
}
}
// Stage 3: Convert the controller layer (depends on service)
// -- only after Stage 2 is validated and deployed
async function handleGetUser(req, res) {
try {
const user = await userService.getUser(req.params.id);
res.json(user);
} catch (err) {
res.status(500).json({ error: err.message });
}
}
يدعم هذا النهج المرحلي أدوات التحليل بشكل مباشر. وكما نوقش في تحليل تدفق البيانات والتحكم ، فإن فهم كيفية تدفق البيانات عبر الطبقات غير المتزامنة قبل تعديلها شرط أساسي لإعادة هيكلة البرمجيات بشكل تدريجي وآمن. ويحدد اتجاه التبعية أي الوحدات البرمجية آمنة للتحويل أولاً، وأيها يجب أن تنتظر حتى يتم ترحيل تبعياتها.
طبقات التغليف للتوافق مع الإصدارات السابقة
أثناء عملية الترحيل، سيظل بعض المستخدمين يتوقعون واجهات برمجة تطبيقات تعتمد على ردود الاتصال. util.callbackify الدالة هي معكوس util.promisify: يقوم بتحويل الدالة غير المتزامنة إلى واجهة رد نداء تعتمد على معالجة الأخطاء أولاً:
جافا سكريبت
const { callbackify } = require('util');
// New async implementation
async function fetchUserAsync(id) {
return db.queryAsync('SELECT * FROM users WHERE id = ?', [id]);
}
// Backward-compatible callback version for unconverted callers
const fetchUser = callbackify(fetchUserAsync);
// Old callers continue to work unchanged
fetchUser(userId, (err, user) => {
if (err) return handleError(err);
render(user);
});
// New callers use the async version directly
const user = await fetchUserAsync(userId);
تتيح هذه التوافقية ثنائية الاتجاه إمكانية التحويل دون الحاجة إلى التنسيق مع جميع المتصلين في وقت واحد.
الأخطاء الشائعة في استخدام Async/Await أثناء عملية الترحيل
يُغفل استخدام await في استدعاءات الدوال غير المتزامنة
أكثر أخطاء الترحيل شيوعًا هو استدعاء دالة غير متزامنة دون انتظار اكتمالها. هذا الخطأ غير مرئي لبيئة تشغيل جافا سكريبت إلا إذا رفضت الدالة، وفي هذه الحالة، يصبح الرفض رفضًا غير معالج للوعد بدلًا من خطأ مُثار.
جافا سكريبت
// Bug: missing await -- function runs but result is a Promise, not the user
async function updateUserName(id, name) {
const user = fetchUser(id); // BUG: forgot await, user is a Promise object
user.name = name; // setting .name on a Promise, not a user
await saveUser(user); // saves the Promise object
}
// Fix
async function updateUserName(id, name) {
const user = await fetchUser(id);
user.name = name;
await saveUser(user);
}
TypeScript و ESLint no-floating-promises تكتشف القاعدة هذا النمط تلقائيًا. يُنصح بشدة بإضافة قاعدة التدقيق هذه أثناء عملية الترحيل.
الدوال غير المتزامنة في طرق المصفوفات
Array.prototype.forEach لا ينتظر استدعاءات الدوال غير المتزامنة. ينتج عن هذا نفس السلوك التسلسلي الخاطئ الذي ينتج عن استخدام await-in-loop، باستثناء أن الكود يبدو أنه يعمل أثناء تشغيل جميع الدوال بشكل متزامن دون انتظار أي منها.
جافا سكريبت
// Bug: forEach does not await async callbacks
async function processAll(ids) {
ids.forEach(async (id) => {
await processItem(id); // these run concurrently, forEach completes immediately
});
// function returns before any processItem completes
}
// Fix: use Promise.all with map
async function processAll(ids) {
await Promise.all(ids.map(id => processItem(id)));
}
لا تستطيع عبارة try/catch التقاط الأخطاء غير المتزامنة خارج عبارة await.
لا تلتقط كتلة try/catch إلا الأخطاء الناتجة عن التعبيرات المُنتظرة. لن يتم التقاط رفض استدعاء دالة غير متزامنة بدون await بواسطة كتلة try/catch المحيطة بها.
جافا سكريبت
// Bug: the rejection from riskyOp() is not caught
async function run() {
try {
riskyOp(); // not awaited -- rejection escapes the try/catch
} catch (err) {
console.error(err); // never reached
}
}
// Fix: await inside the try block
async function run() {
try {
await riskyOp();
} catch (err) {
console.error(err);
}
}
اختبار التعليمات البرمجية غير المتزامنة بعد الترحيل
تتطلب الدوال غير المتزامنة المُرحّلة حالات اختبار غير متزامنة. وتدعم أطر الاختبار الحديثة هذا الأمر بشكل أصلي.
جافا سكريبت
// Jest async test patterns
describe('UserService', () => {
// Pattern 1: async/await in test
test('fetches user by id', async () => {
const user = await userService.getUser('user-123');
expect(user.id).toBe('user-123');
});
// Pattern 2: testing rejection
test('throws when user not found', async () => {
await expect(userService.getUser('nonexistent'))
.rejects.toThrow('User nonexistent not found');
});
// Pattern 3: parallel setup
beforeAll(async () => {
await db.connect();
await db.seed(testData);
});
afterAll(async () => {
await db.cleanup();
await db.disconnect();
});
});
عند التحقق من أن الدالة التي تم ترحيلها تنتج نفس المخرجات التي تنتجها الدالة السابقة التي تم استدعاؤها، يكون اختبار المقارنة فعالاً:
جافا سكريبت
// Comparison test: callback version vs. async version must agree
test('async version matches callback version output', async () => {
const callbackResult = await promisify(fetchUserLegacy)('user-123');
const asyncResult = await fetchUserAsync('user-123');
expect(asyncResult).toEqual(callbackResult);
});
يُعدّ نمط الاختبار هذا مفيدًا بشكل خاص خلال فترة الانتقال عندما تعمل النسختان بالتوازي. وكما هو موضح في سياق تحليل الأثر قبل إجراء أي تغييرات على الكود ، فإن التحقق من أن الدالة المُعاد هيكلتها تُنتج مخرجات مطابقة لتلك التي تُنتجها سابقتها يُعدّ تأكيدًا قائمًا على الأدلة على صحة عملية الترحيل قبل إزالة النسخة القديمة.
كيفية SMART TS XL يدعم الترحيل غير المتزامن الآمن على نطاق واسع
بالنسبة لقواعد بيانات المؤسسات حيث تمتد سلاسل الاستدعاء عبر ملفات وخدمات وفرق متعددة، فإن الشرط الأول للهجرة الآمنة هو وجود خريطة كاملة للتبعيات غير المتزامنة الحالية: أي الوظائف تستدعي أي وظائف أخرى، وما هي البيانات التي تمر بينها، وأي السلاسل تمثل مسارات حرجة للعمليات الأساسية للتطبيق، وأيها أدوات مساعدة معزولة يمكن ترحيلها بشكل مستقل.
SMART TS XL يقوم هذا التحليل بإنشاء خريطة التبعيات هذه من خلال تحليل قاعدة التعليمات البرمجية بأكملها، وليس الملفات الفردية فقط. ويحدد كيفية إشارة الدوال القائمة على ردود الاتصال إلى بعضها البعض عبر حدود الوحدات، ويحدد الحالة المشتركة التي يتم تمريرها عبر الإغلاقات، ويعرض سلاسل التنفيذ التي يجب أن تظل سليمة أثناء عملية الترحيل. يوفر هذا التحليل الهيكلي المدخلات اللازمة لنهج الترحيل المرحلي الموضح في هذا الدليل: أي الوحدات تقع في أسفل مخطط التبعيات ويمكن تحويلها أولاً، وأي الوحدات يجب أن تنتظر حتى يتم ترحيل تبعياتها بالفعل.
المنصة تحليل الأثر تُوسّع هذه الإمكانية نطاق التقييم ليشمل تقييم التغييرات. قبل تحويل وحدة نمطية تعتمد على ردود الاتصال لإرجاع وعود، يُحدد تحليل التأثير جميع الوحدات النمطية الأخرى في قاعدة التعليمات البرمجية التي تستدعيها بواجهة ردود اتصال. تُمثل هذه الوحدات المستدعِية نطاق الترحيل للمرحلة التالية: يجب تحويلها إما في وقت واحد، أو باستخدام غلاف متوافق مع الإصدارات السابقة. util.callbackify يجب الاحتفاظ بهذه البيانات حتى يتم تحويلها. بدون هذا التعداد، تتم عمليات الترحيل بنطاق غير معروف، مما يؤدي إلى حدوث أعطال غير متوقعة عندما يواجه المستخدمون الذين لم يتم تحديدهم وعدًا (Promise) حيث كانوا يتوقعون رد اتصال (callback).
بالنسبة لقواعد البيانات البرمجية التي تمزج بين جافا سكريبت وتايب سكريبت، أو التي تستدعي خدمات الواجهة الخلفية بلغات أخرى، SMART TS XLالصورة تحليل التبعية بين اللغات يُتيح ذلك رؤية مسار التنفيذ الكامل، وليس فقط طبقة جافا سكريبت. سلسلة الاستدعاءات التي تنتهي باستدعاء خدمة خارجية مكتوبة بلغة جافا أو بايثون لها تبعيات لا تستطيع أدوات اللغة الواحدة رؤيتها، وبالتالي فإن تخطيط الترحيل الذي يتجاهل هذه التبعيات يكون ناقصًا. تصور التبعية أن SMART TS XL يوفر ذلك إمكانية رؤية تلك العلاقات العابرة للحدود قبل إجراء أي تغييرات في الهجرة.
دعم عملية الترحيل: من الاستدعاءات إلى البرمجة غير المتزامنة/الانتظار عبر قاعدة التعليمات البرمجية الكاملة
لا يكتمل الانتقال من الدوال المستدعاة إلى async/await عند تحويل الوحدة الأولى. بل يكتمل عند إزالة طبقة التغليف الأخيرة وعدم وجود أي شيء متبقٍ في قاعدة التعليمات البرمجية. callback يكمن جوهر هذا المفهوم في اصطلاحاته. ويتطلب الوصول إلى ذلك التزاماً بالانضباط طوال فترة الترحيل: يجب كتابة الكود الجديد باستخدام async/await، ويجب التعامل مع طبقات التغليف على أنها مؤقتة، ويجب أن تفرض قواعد ESLint عدم إدخال وظائف من نوع callback في الوحدات المحولة.
المؤشرات العملية لإتمام عملية الهجرة هي: لا util.promisify استدعاءات في كود التطبيق (كانت مطلوبة فقط خلال الفترة الانتقالية)، لا (err, result) => أنماط في منطق الأعمال الأساسي (تم استبدالها بـ try/catch في الدوال غير المتزامنة)، لا توجد مُنشئات Promise يدوية حيث async/await يكفي ذلك، و Promise.all في كل مكان كانت تُدار فيه العمليات المستقلة بشكل متسلسل سابقًا.
يمكن قياس كلٍّ من هذه الجوانب من خلال التحليل الثابت، ما يعني إمكانية تتبّع التقدّم والإبلاغ عنه بموضوعية بدلاً من تقديره. بالنسبة للفرق العاملة على نطاق واسع، فإنّ الجمع بين التحليل الثابت الآلي لاكتشاف أنماط الاستدعاء وتحليل التبعيات لتحديد نطاق كل مرحلة من مراحل الترحيل هو ما يُحدث الفرق بين ترحيل يكتمل ضمن إطار زمني مُحدّد وترحيل يستمر إلى أجل غير مسمى لعدم معرفة نطاقه بشكل كامل.


