وصل سوكيت الراكب قبل إنشاء الرحلة — ليسبق FCM والـ polling

كان السوكيت مضموناً أن يخسر السباق الأول بنيوياً. ترتيب
startSearchingForDriver كان:
  ① await postRideDetailsToServer()  → add_ride.php يوزّع الطلب على الكباتن فوراً
  ② _addRideToWaitingTable()          (بلا await)
  ③ initConnectionWithSocket()        ← السوكيت يبدأ الاتصال الآن فقط

بين ① و③ نافذة = ذهاب/عودة HTTP + مصافحة WebSocket + قراءة الـ JWT، ثانية
إلى ثلاث (أسوأ على 3G). وغرف Socket.IO لا تخزّن الأحداث: لو قبل كابتن داخل
تلك النافذة فإن $io->to('passenger_X')->emit() يُرمى نهائياً لأن الغرفة
فارغة — لا طابور ولا إعادة إرسال. وكابتن ينظر لقائمة السوق يقبل في أقل من
ثانية، فالنافذة مُرجَّحة لا نادرة.

هذا يفسّر لماذا كان الـ 404 على getRideOrderID.php قاتلاً: السوكيت يخسر،
والـ FCM يصل بعد أن ضبط الـ polling _isAcceptanceProcessed=true فيُرفض،
والشبكة الاحتياطية الوحيدة مكسورة.

ترتيب السيرفر كان صحيحاً أصلاً (acceptRide.php ينادي
notifyPassengerOnRideServer قبل sendFCM_Internal) — المشكلة أن المستقبِل
لم يكن موجوداً بعد.

التغييرات:
- ensureConnectedBeforeRide(): يوصل وينتظر الانضمام للغرفة بسقف 4 ثوانٍ،
  ولا يُفشل إنشاء الرحلة عند انتهاء المهلة (نكمل ونتّكل على FCM/polling).
- startSearchingForDriver ينتظرها قبل postRideDetailsToServer.
- وصل السوكيت في initializeDataAfterLogin (عند فتح الخريطة) فيصير الانتظار
  صفراً عملياً في الحالة الطبيعية.
- _isConnecting يغطّي نافذة الـ await على الـ JWT حتى لا يفتح النداءان
  المتزامنان سوكيتين، وحرس على passengerID الفارغ.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hamza-Ayed
2026-07-26 16:26:23 +03:00
co-authored by Claude Opus 5
parent f66db7db42
commit ab3be7e2e3
2 changed files with 57 additions and 2 deletions
@@ -18,6 +18,12 @@ class MapSocketController extends GetxController {
late io_client.Socket socket; late io_client.Socket socket;
bool isSocketConnected = false; bool isSocketConnected = false;
bool _isSocketInitialized = false; bool _isSocketInitialized = false;
// يغطّي نافذة الـ await على قراءة الـ JWT: بلا هذا يقدر نداءان متزامنان
// (initializeDataAfterLogin + startSearchingForDriver) يفتحان سوكيتين.
bool _isConnecting = false;
/// جاهز لاستقبال الأحداث — أي منضمّ فعلياً لغرفة passenger_{id} على السيرفر.
bool get isReadyForEvents => isSocketConnected && _isSocketInitialized;
Timer? _heartbeatTimer; Timer? _heartbeatTimer;
DateTime? _lastSocketLocationTime; DateTime? _lastSocketLocationTime;
int _socketLocationUpdatesCount = 0; int _socketLocationUpdatesCount = 0;
@@ -53,16 +59,26 @@ class MapSocketController extends GetxController {
return ''; return '';
} }
/// يجب أن يُنادى **قبل** إنشاء الرحلة: غرف Socket.IO لا تخزّن الأحداث، فإن
/// قبل كابتن قبل أن ينضم الراكب لغرفته فإن ride_status_change يُرمى نهائياً
/// ولا يبقى إلا الـ FCM/polling. النداء آمن ومتكرر (idempotent).
Future<void> initConnectionWithSocket() async { Future<void> initConnectionWithSocket() async {
if (isSocketConnected) return; if (isSocketConnected || _isSocketInitialized || _isConnecting) return;
_isConnecting = true;
String passengerId = box.read(BoxName.passengerID).toString(); String passengerId = box.read(BoxName.passengerID).toString();
final String jwt = await _getJwtForSocket(); final String jwt = await _getJwtForSocket();
if (jwt.isEmpty) { if (jwt.isEmpty) {
_isConnecting = false;
Log.print( Log.print(
"⚠️ No JWT available — passenger socket would be rejected by the server. Aborting connect."); "⚠️ No JWT available — passenger socket would be rejected by the server. Aborting connect.");
return; return;
} }
if (passengerId.isEmpty || passengerId == 'null') {
_isConnecting = false;
Log.print("⚠️ No passengerID yet — deferring socket connect.");
return;
}
Log.print("🔌 Initializing Socket for Passenger: $passengerId"); Log.print("🔌 Initializing Socket for Passenger: $passengerId");
socket = io_client.io( socket = io_client.io(
@@ -80,6 +96,7 @@ class MapSocketController extends GetxController {
.build(), .build(),
); );
_isSocketInitialized = true; _isSocketInitialized = true;
_isConnecting = false;
socket.connect(); socket.connect();
@@ -199,6 +216,32 @@ class MapSocketController extends GetxController {
}); });
} }
/// يوصل السوكيت وينتظر انضمامه فعلياً لغرفة الراكب، بسقف زمني.
/// يُنادى قبل إنشاء الرحلة حتى لا يقبل كابتن والراكب ما زال خارج غرفته
/// (الحدث يُفقد نهائياً في تلك الحالة — لا طابور في غرف Socket.IO).
/// لا يُفشل إنشاء الرحلة أبداً: عند انتهاء المهلة نكمل ونتّكل على
/// الـ FCM/polling كشبكة احتياطية.
Future<bool> ensureConnectedBeforeRide({
Duration timeout = const Duration(seconds: 4),
}) async {
if (isReadyForEvents) return true;
await initConnectionWithSocket();
final deadline = DateTime.now().add(timeout);
while (!isSocketConnected && DateTime.now().isBefore(deadline)) {
await Future.delayed(const Duration(milliseconds: 100));
}
if (isSocketConnected) {
Log.print("✅ Socket ready before ride creation.");
return true;
}
Log.print(
"⚠️ Socket not ready within ${timeout.inSeconds}s — continuing; FCM/polling will cover.");
return false;
}
// ── مواصلاتي: اشتراك/إلغاء اشتراك ببثّ موقع خط ───────────── // ── مواصلاتي: اشتراك/إلغاء اشتراك ببثّ موقع خط ─────────────
// يُستدعى عند فتح/إغلاق شاشة تتبع الباص الحي. يضمن السوكيت متصلاً أولاً. // يُستدعى عند فتح/إغلاق شاشة تتبع الباص الحي. يضمن السوكيت متصلاً أولاً.
Future<void> subscribeToTransitRoute(int routeId) async { Future<void> subscribeToTransitRoute(int routeId) async {
@@ -728,6 +728,13 @@ class RideLifecycleController extends GetxController {
_currentSearchPhase = 0; _currentSearchPhase = 0;
update(); update();
// ⚠️ الترتيب مقصود: نضمن انضمام الراكب لغرفته على السوكيت **قبل** إنشاء
// الرحلة. add_ride.php يوزّع الطلب على الكباتن فوراً، وكابتن ينظر لقائمة
// السوق يقدر يقبل في أقل من ثانية. وغرف Socket.IO لا تخزّن شيئاً: لو قبل
// قبل أن ينضم الراكب فإن ride_status_change يُرمى نهائياً ويبقى الاعتماد
// على FCM/polling وحدهما. كان السوكيت يُوصَل بعد الإنشاء فيخسر السباق دائماً.
await mapSocket.ensureConnectedBeforeRide();
bool rideCreated = await postRideDetailsToServer(); bool rideCreated = await postRideDetailsToServer();
if (!rideCreated) { if (!rideCreated) {
@@ -739,7 +746,6 @@ class RideLifecycleController extends GetxController {
} }
_addRideToWaitingTable(); _addRideToWaitingTable();
mapSocket.initConnectionWithSocket();
} }
void _showAiNegotiatorDialog() { void _showAiNegotiatorDialog() {
@@ -4444,6 +4450,12 @@ class RideLifecycleController extends GetxController {
getLocationArea(passengerLocation.latitude, passengerLocation.longitude); getLocationArea(passengerLocation.latitude, passengerLocation.longitude);
await _stagePricingAndState(); await _stagePricingAndState();
await _stageNiceToHave(); await _stageNiceToHave();
// نوصل السوكيت عند فتح الخريطة لا عند إنشاء الرحلة، حتى تكون العضوية في
// الغرفة جاهزة سلفاً فيصير انتظار ensureConnectedBeforeRide صفراً عملياً،
// ويسبق السوكيت الـ FCM والـ polling كما هو مفروض بالتصميم.
mapSocket.initConnectionWithSocket();
startMasterTimer(); startMasterTimer();
} }