From ab3be7e2e3ca7da1663b2acbff1bd3001351a4c5 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Sun, 26 Jul 2026 16:26:23 +0300 Subject: [PATCH] =?UTF-8?q?=D9=88=D8=B5=D9=84=20=D8=B3=D9=88=D9=83=D9=8A?= =?UTF-8?q?=D8=AA=20=D8=A7=D9=84=D8=B1=D8=A7=D9=83=D8=A8=20=D9=82=D8=A8?= =?UTF-8?q?=D9=84=20=D8=A5=D9=86=D8=B4=D8=A7=D8=A1=20=D8=A7=D9=84=D8=B1?= =?UTF-8?q?=D8=AD=D9=84=D8=A9=20=E2=80=94=20=D9=84=D9=8A=D8=B3=D8=A8=D9=82?= =?UTF-8?q?=20FCM=20=D9=88=D8=A7=D9=84=D9=80=20polling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit كان السوكيت مضموناً أن يخسر السباق الأول بنيوياً. ترتيب 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 --- .../home/map/map_socket_controller.dart | 45 ++++++++++++++++++- .../home/map/ride_lifecycle_controller.dart | 14 +++++- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/siro_rider/lib/controller/home/map/map_socket_controller.dart b/siro_rider/lib/controller/home/map/map_socket_controller.dart index 3f090f88..fc4daa58 100644 --- a/siro_rider/lib/controller/home/map/map_socket_controller.dart +++ b/siro_rider/lib/controller/home/map/map_socket_controller.dart @@ -18,6 +18,12 @@ class MapSocketController extends GetxController { late io_client.Socket socket; bool isSocketConnected = false; bool _isSocketInitialized = false; + // يغطّي نافذة الـ await على قراءة الـ JWT: بلا هذا يقدر نداءان متزامنان + // (initializeDataAfterLogin + startSearchingForDriver) يفتحان سوكيتين. + bool _isConnecting = false; + + /// جاهز لاستقبال الأحداث — أي منضمّ فعلياً لغرفة passenger_{id} على السيرفر. + bool get isReadyForEvents => isSocketConnected && _isSocketInitialized; Timer? _heartbeatTimer; DateTime? _lastSocketLocationTime; int _socketLocationUpdatesCount = 0; @@ -53,16 +59,26 @@ class MapSocketController extends GetxController { return ''; } + /// يجب أن يُنادى **قبل** إنشاء الرحلة: غرف Socket.IO لا تخزّن الأحداث، فإن + /// قبل كابتن قبل أن ينضم الراكب لغرفته فإن ride_status_change يُرمى نهائياً + /// ولا يبقى إلا الـ FCM/polling. النداء آمن ومتكرر (idempotent). Future initConnectionWithSocket() async { - if (isSocketConnected) return; + if (isSocketConnected || _isSocketInitialized || _isConnecting) return; + _isConnecting = true; String passengerId = box.read(BoxName.passengerID).toString(); final String jwt = await _getJwtForSocket(); if (jwt.isEmpty) { + _isConnecting = false; Log.print( "⚠️ No JWT available — passenger socket would be rejected by the server. Aborting connect."); return; } + if (passengerId.isEmpty || passengerId == 'null') { + _isConnecting = false; + Log.print("⚠️ No passengerID yet — deferring socket connect."); + return; + } Log.print("🔌 Initializing Socket for Passenger: $passengerId"); socket = io_client.io( @@ -80,6 +96,7 @@ class MapSocketController extends GetxController { .build(), ); _isSocketInitialized = true; + _isConnecting = false; socket.connect(); @@ -199,6 +216,32 @@ class MapSocketController extends GetxController { }); } + /// يوصل السوكيت وينتظر انضمامه فعلياً لغرفة الراكب، بسقف زمني. + /// يُنادى قبل إنشاء الرحلة حتى لا يقبل كابتن والراكب ما زال خارج غرفته + /// (الحدث يُفقد نهائياً في تلك الحالة — لا طابور في غرف Socket.IO). + /// لا يُفشل إنشاء الرحلة أبداً: عند انتهاء المهلة نكمل ونتّكل على + /// الـ FCM/polling كشبكة احتياطية. + Future 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 subscribeToTransitRoute(int routeId) async { diff --git a/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart b/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart index efeaa5fe..0fd1a236 100644 --- a/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart +++ b/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart @@ -728,6 +728,13 @@ class RideLifecycleController extends GetxController { _currentSearchPhase = 0; update(); + // ⚠️ الترتيب مقصود: نضمن انضمام الراكب لغرفته على السوكيت **قبل** إنشاء + // الرحلة. add_ride.php يوزّع الطلب على الكباتن فوراً، وكابتن ينظر لقائمة + // السوق يقدر يقبل في أقل من ثانية. وغرف Socket.IO لا تخزّن شيئاً: لو قبل + // قبل أن ينضم الراكب فإن ride_status_change يُرمى نهائياً ويبقى الاعتماد + // على FCM/polling وحدهما. كان السوكيت يُوصَل بعد الإنشاء فيخسر السباق دائماً. + await mapSocket.ensureConnectedBeforeRide(); + bool rideCreated = await postRideDetailsToServer(); if (!rideCreated) { @@ -739,7 +746,6 @@ class RideLifecycleController extends GetxController { } _addRideToWaitingTable(); - mapSocket.initConnectionWithSocket(); } void _showAiNegotiatorDialog() { @@ -4444,6 +4450,12 @@ class RideLifecycleController extends GetxController { getLocationArea(passengerLocation.latitude, passengerLocation.longitude); await _stagePricingAndState(); await _stageNiceToHave(); + + // نوصل السوكيت عند فتح الخريطة لا عند إنشاء الرحلة، حتى تكون العضوية في + // الغرفة جاهزة سلفاً فيصير انتظار ensureConnectedBeforeRide صفراً عملياً، + // ويسبق السوكيت الـ FCM والـ polling كما هو مفروض بالتصميم. + mapSocket.initConnectionWithSocket(); + startMasterTimer(); }