أنماط AbortController
تُنفذ AbortController وAbortSignal بالكامل وفق WHATWG في Phasis. كما تتوفر أدوات التركيب القياسية (AbortSignal.any وAbortSignal.timeout). تتناول هذه الصفحة الأنماط المستخدمة فعليًا وكيف ينتشر الإلغاء عبر الحد الفاصل بين PHP.
إلغاء طلب fetch
const ctrl = new AbortController();
const p = fetch(url, { signal: ctrl.signal });
ctrl.abort(); // p rejects with AbortError DOMException
السبب هو AbortError افتراضيًا، لكن يمكنك تحديد سببك:
ctrl.abort(new Error('user cancelled'));
يرفض fetch بالقيمة التي مررتها إليه.
المهلة الزمنية
const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
يعيد AbortSignal.timeout(ms) إشارة تلغي نفسها بعد ms. يعمل المؤقت عبر حلقة أحداث المحرك نفسها التي تشغّل setTimeout/queueMicrotask (راجع API: حلقة الأحداث). يتحقق ناقل fetch من الإشارة أثناء النقل — وبالنسبة إلى CurlTransport الافتراضي، عبر CURLOPT_PROGRESSFUNCTION — لذا يُلغى الطلب سريعًا عند حلول الموعد النهائي.
تعمل الحلقة وفق الوقت الافتراضي افتراضيًا: تقدّم ساعتها إلى الموعد النهائي المعلّق التالي بدلًا من الانتظار PHP، لذا إما أن يكتمل fetch(url, { signal: AbortSignal.timeout(5000) }) قبل الموعد النهائي أو يُلغى عند نبضة الحلقة التالية — ولا يجعل PHP ينتظر. يتطلب ضبط الإيقاع وفق الوقت الفعلي أن يقود المضيف tickEventLoop() وفق ساعة فعلية.
التسابق: الإلغاء الأول هو الفائز
const user = new AbortController();
const signal = AbortSignal.any([user.signal, AbortSignal.timeout(10_000)]);
const res = await fetch(url, { signal });
// res rejects if EITHER the user clicks cancel OR 10s elapses
الانتشار عبر الطبقات
غلّف عملك غير المتزامن بحلقة تراعي الإشارة:
async function fetchPaged(url, signal) {
const out = [];
for (let page = 1; ; page++) {
if (signal.aborted) throw signal.reason;
const r = await fetch(`${url}?page=${page}`, { signal });
const data = await r.json();
if (data.length === 0) break;
out.push(...data);
}
return out;
}
يرث طلب fetch الإشارة مباشرة؛ أما if (signal.aborted) اليدوي فيغطي الفجوات بين الطلبات التي قد تواصل فيها حلقة الصفحة التنفيذ لولا ذلك.
الاستماع إلى حدث الإلغاء
ctrl.signal.addEventListener('abort', () => {
releaseDatabaseLock();
cancelDownstreamWork();
});
تُضبط signal.reason قبل تشغيل المستمعين.
الإشارة في ناقل fetch مخصص
إذا بدّلت وسيلة النقل عبر setFetchTransport()، فستُمرر الإشارة إلى الدالة القابلة للاستدعاء لديك بوصفها JsObject. احترمها بالتحقق منها دوريًا:
$engine->setFetchTransport(function (array $req, $signal) {
$ch = curl_init($req['url']);
// ...
if ($signal !== null) {
curl_setopt($ch, CURLOPT_PROGRESSFUNCTION,
fn () => $signal->get('aborted')->value ? 1 : 0);
curl_setopt($ch, CURLOPT_NOPROGRESS, false);
}
$body = curl_exec($ch);
if (curl_errno($ch) === CURLE_ABORTED_BY_CALLBACK) {
throw new \Phasis\BuiltIn\Fetch\TransportException(
'aborted', 'aborted',
);
}
// ...
});
يؤدي إطلاق TransportException باستخدام kind: 'aborted' إلى رفض fetch في JS بقيمة AbortError DOMException — كما لو نُفذت حلقة الإلغاء المحددة في المواصفات.
بالنسبة إلى الواجهات الخلفية ذات الإلغاء الأصلي (on_progress من Guzzle وResponseInterface::cancel من Symfony)، استخدم نمط التحقق ثم الإطلاق نفسه.
أنماط لا تعمل بعد
signal.addEventListener('abort', () => fetch(cleanupUrl))— تشغيل المستمع متزامن؛ يبدأ طلب fetch للتنظيف لكنه لن يكتمل أثناء إرسال حدث الإلغاء. جدوِله عبرawaitالمحيطة.
يعمل setTimeout(() => ctrl.abort(), N) (عائلة المؤقتات متاحة ومغطاة باختبارات WPT)، لكن AbortSignal.timeout(N) أقصر ويعبّر عن المقصود بوضوح أكبر عندما تريد فقط إشارة إلغاء بعد الموعد النهائي.
راجع أيضًا
- ناقل Fetch — واجهة كاملة للطلب والاستجابة والإشارة للناقلات المخصصة.
- واجهات الويب (WPT) — معدل اجتياز اختبارات WPT لـ AbortController/AbortSignal.