diff --git a/FIXES-FOR-CODEX.md b/FIXES-FOR-CODEX.md new file mode 100644 index 0000000..6a92722 --- /dev/null +++ b/FIXES-FOR-CODEX.md @@ -0,0 +1,195 @@ +# Fchaty — الإصلاحات المطلوبة + +ثلاث مشاكل أساسية تحتاج إصلاح. لكل مشكلة: الملف المعني، السطر، وصف المشكلة، والحل المطلوب. + +--- + +## Fix 1: حذف الرسائل المحلية عند فك الاقتران (unpair) + +**المشكلة:** عند `unpair()` الرسائل القديمة تبقى محفوظة بملف `messages.json` المحلي. عند إعادة الاقتران مع شخص جديد، المحادثة القديمة تظهر. + +**الملف:** `FchatyApp/Sources/FchatyApp/Session/AppSession.swift` + +**السطر:** 115-124 — دالة `unpair()` + +**الكود الحالي:** +```swift +func unpair() { + incomingMessagesTask?.cancel() + incomingMessagesTask = nil + KeychainStore.clearAll() + let webSocket = webSocket + Task { + await webSocket.disconnect() + } + state = .unpaired +} +``` + +**المطلوب:** +1. إضافة دالة `deleteAll()` في `MessageStore` تحذف ملف `messages.json` من الدسك. +2. استدعاء `deleteAll()` ومسح `messages` array في `unpair()`. + +**الملف الإضافي:** `FchatyApp/Sources/FchatyApp/Storage/MessageStore.swift` + +أضف هذه الدالة في `MessageStore`: +```swift +func deleteAll() throws { + let url = try messagesURL() + if fileManager.fileExists(atPath: url.path) { + try fileManager.removeItem(at: url) + } +} +``` + +وعدّل `unpair()` في `AppSession.swift`: +```swift +func unpair() { + incomingMessagesTask?.cancel() + incomingMessagesTask = nil + messages = [] + Task { + try? await messageStore.deleteAll() + } + KeychainStore.clearAll() + let webSocket = webSocket + Task { + await webSocket.disconnect() + } + state = .unpaired +} +``` + +--- + +## Fix 2: جعل WebSocket reconnect لانهائي + +**المشكلة:** الـ reconnect يحاول 5 مرات فقط (2, 4, 8, 16, 32 ثانية) ثم يتوقف نهائياً بدون إبلاغ المستخدم. رفيقك يشوف loading لانهائي لأن التطبيق توقف عن المحاولة. + +**الملف:** `FchatyApp/Sources/FchatyApp/Networking/WSClient.swift` + +**السطر:** 243-273 — دالة `reconnect()` + +**الكود الحالي:** +```swift +private func reconnect(using token: String, id: UUID) async { + let delays: [UInt64] = [2, 4, 8, 16, 32] + + for delay in delays { + do { + try await Task.sleep(nanoseconds: delay * 1_000_000_000) + } catch { + return + } + + guard reconnectID == id, !isDisconnecting else { + return + } + + do { + try await establishConnection(token: token) + if reconnectID == id { + reconnectID = nil + reconnectTask = nil + } + return + } catch { + continue + } + } + + if reconnectID == id { + reconnectID = nil + reconnectTask = nil + } +} +``` + +**المطلوب:** تغيير الدالة لتحاول بشكل لانهائي مع exponential backoff بحد أقصى 60 ثانية: + +```swift +private func reconnect(using token: String, id: UUID) async { + var delay: UInt64 = 2 + + while !Task.isCancelled { + do { + try await Task.sleep(nanoseconds: delay * 1_000_000_000) + } catch { + return + } + + guard reconnectID == id, !isDisconnecting else { + return + } + + do { + try await establishConnection(token: token) + if reconnectID == id { + reconnectID = nil + reconnectTask = nil + } + return + } catch { + delay = min(delay * 2, 60) + continue + } + } +} +``` + +--- + +## Fix 3: تحميل fileRegistry من الدسك عند بدء السيرفر + +**المشكلة:** `fileRegistry` هو `Map` في الذاكرة فقط. بعد إعادة تشغيل السيرفر (deploy جديد مثلاً)، كل الملفات المرفوعة تصبح 404 رغم إنها موجودة على الدسك. + +**الملف:** `relay-server/src/index.js` + +**السطر:** 42 — تعريف `fileRegistry` + +**المطلوب:** إضافة دالة تقرأ الملفات الموجودة بمجلد `/data/files/` وتعبّي الـ `fileRegistry` عند بدء السيرفر. + +أضف هذه الدالة بعد سطر 42 (`const fileRegistry = new Map();`): + +```javascript +function loadFileRegistryFromDisk() { + if (!fs.existsSync(UPLOADS_DIR)) return; + + for (const entry of fs.readdirSync(UPLOADS_DIR, { withFileTypes: true })) { + if (!entry.isFile()) continue; + + const diskPath = path.join(UPLOADS_DIR, entry.name); + try { + const stats = fs.statSync(diskPath); + fileRegistry.set(entry.name, { + diskPath, + originalName: entry.name, // الاسم الأصلي ضايع — نستخدم الـ UUID كفولباك + size: stats.size, + }); + } catch (_) {} + } +} + +loadFileRegistryFromDisk(); +console.log(`[files] loaded ${fileRegistry.size} files from disk`); +``` + +**ملاحظة مهمة:** الاسم الأصلي للملف (`originalName`) لا يُحفظ على الدسك حالياً — multer يحفظ الملف باسم UUID فقط. هذا الإصلاح يسترجع الملفات لكن بدون أسمائها الأصلية. لحل كامل، يجب حفظ metadata الملف (اسمه الأصلي وحجمه) بملف `.meta.json` بجانب كل ملف عند الرفع. هذا تحسين إضافي وليس ضرورياً الآن. + +--- + +## ملخص سريع + +| # | الملف | المشكلة | الأولوية | +|---|---|---|---| +| 1 | `AppSession.swift` + `MessageStore.swift` | الرسائل القديمة تظهر بعد unpair | عالية | +| 2 | `WSClient.swift` | الاتصال يفشل نهائياً بعد 5 محاولات | عالية | +| 3 | `relay-server/src/index.js` | الملفات 404 بعد restart السيرفر | عالية | + +## الاختبارات + +بعد تنفيذ الإصلاحات: +1. `swift test --package-path FchatyApp` — لازم كل الاختبارات تنجح +2. تأكد إنو `unpair()` يمسح الرسائل — عمل pairing، أرسل رسائل، عمل unpair، تأكد إنو المحادثة فاضية +3. تأكد إنو الـ reconnect ما بيوقف — شغّل التطبيق، أوقف السيرفر، شغله بعد دقيقة، لازم يتصل تلقائياً +4. تأكد إنو الملفات تتحمل — أرسل ملف، أعد تشغيل السيرفر، حاول حمّل الملف diff --git a/FchatyApp/Sources/FchatyApp/Networking/WSClient.swift b/FchatyApp/Sources/FchatyApp/Networking/WSClient.swift index 6e04056..a9818d9 100644 --- a/FchatyApp/Sources/FchatyApp/Networking/WSClient.swift +++ b/FchatyApp/Sources/FchatyApp/Networking/WSClient.swift @@ -241,9 +241,9 @@ actor WSClient { } private func reconnect(using token: String, id: UUID) async { - let delays: [UInt64] = [2, 4, 8, 16, 32] + var delay: UInt64 = 2 - for delay in delays { + while !Task.isCancelled { do { try await Task.sleep(nanoseconds: delay * 1_000_000_000) } catch { @@ -262,14 +262,10 @@ actor WSClient { } return } catch { + delay = min(delay * 2, 60) continue } } - - if reconnectID == id { - reconnectID = nil - reconnectTask = nil - } } private func closeFailedConnection(_ task: URLSessionWebSocketTask) { diff --git a/FchatyApp/Sources/FchatyApp/Session/AppSession.swift b/FchatyApp/Sources/FchatyApp/Session/AppSession.swift index 6fb5c1f..daf3ba6 100644 --- a/FchatyApp/Sources/FchatyApp/Session/AppSession.swift +++ b/FchatyApp/Sources/FchatyApp/Session/AppSession.swift @@ -115,6 +115,10 @@ final class AppSession: ObservableObject { func unpair() { incomingMessagesTask?.cancel() incomingMessagesTask = nil + messages = [] + Task { + try? await messageStore.deleteAll() + } KeychainStore.clearAll() let webSocket = webSocket Task { diff --git a/FchatyApp/Sources/FchatyApp/Storage/MessageStore.swift b/FchatyApp/Sources/FchatyApp/Storage/MessageStore.swift index 1c426aa..e72fa35 100644 --- a/FchatyApp/Sources/FchatyApp/Storage/MessageStore.swift +++ b/FchatyApp/Sources/FchatyApp/Storage/MessageStore.swift @@ -55,6 +55,13 @@ actor MessageStore { try write(messages) } + func deleteAll() throws { + let url = try messagesURL() + if fileManager.fileExists(atPath: url.path) { + try fileManager.removeItem(at: url) + } + } + var unreadCount: Int { get async { (try? loadAll().filter { !$0.isRead }.count) ?? 0 diff --git a/relay-server/src/index.js b/relay-server/src/index.js index 29d283c..da9584b 100644 --- a/relay-server/src/index.js +++ b/relay-server/src/index.js @@ -41,6 +41,27 @@ const pairingSessions = new Map(); // code -> session const connections = new Map(); // peerID -> { ws, name } const fileRegistry = new Map(); // fileID -> { diskPath, originalName, size } +function loadFileRegistryFromDisk() { + if (!fs.existsSync(UPLOADS_DIR)) return; + + for (const entry of fs.readdirSync(UPLOADS_DIR, { withFileTypes: true })) { + if (!entry.isFile()) continue; + + const diskPath = path.join(UPLOADS_DIR, entry.name); + try { + const stats = fs.statSync(diskPath); + fileRegistry.set(entry.name, { + diskPath, + originalName: entry.name, + size: stats.size, + }); + } catch (_) {} + } +} + +loadFileRegistryFromDisk(); +console.log(`[files] loaded ${fileRegistry.size} files from disk`); + function positiveInteger(value, fallback) { const parsed = Number.parseInt(value ?? '', 10); return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;