وصل سوكيت الراكب قبل إنشاء الرحلة — ليسبق 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:
co-authored by
Claude Opus 5
parent
f66db7db42
commit
ab3be7e2e3
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user