New Update to Pair

This commit is contained in:
diyaa 2026-07-27 23:41:27 +02:00
parent 9cbf88d3a0
commit 4e0dcbe873
5 changed files with 230 additions and 7 deletions

195
FIXES-FOR-CODEX.md Normal file
View File

@ -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. تأكد إنو الملفات تتحمل — أرسل ملف، أعد تشغيل السيرفر، حاول حمّل الملف

View File

@ -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) {

View File

@ -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 {

View File

@ -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

View File

@ -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;