New Update to Pair
This commit is contained in:
parent
9cbf88d3a0
commit
4e0dcbe873
195
FIXES-FOR-CODEX.md
Normal file
195
FIXES-FOR-CODEX.md
Normal 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. تأكد إنو الملفات تتحمل — أرسل ملف، أعد تشغيل السيرفر، حاول حمّل الملف
|
||||||
@ -241,9 +241,9 @@ actor WSClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func reconnect(using token: String, id: UUID) async {
|
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 {
|
do {
|
||||||
try await Task.sleep(nanoseconds: delay * 1_000_000_000)
|
try await Task.sleep(nanoseconds: delay * 1_000_000_000)
|
||||||
} catch {
|
} catch {
|
||||||
@ -262,14 +262,10 @@ actor WSClient {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
} catch {
|
} catch {
|
||||||
|
delay = min(delay * 2, 60)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if reconnectID == id {
|
|
||||||
reconnectID = nil
|
|
||||||
reconnectTask = nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func closeFailedConnection(_ task: URLSessionWebSocketTask) {
|
private func closeFailedConnection(_ task: URLSessionWebSocketTask) {
|
||||||
|
|||||||
@ -115,6 +115,10 @@ final class AppSession: ObservableObject {
|
|||||||
func unpair() {
|
func unpair() {
|
||||||
incomingMessagesTask?.cancel()
|
incomingMessagesTask?.cancel()
|
||||||
incomingMessagesTask = nil
|
incomingMessagesTask = nil
|
||||||
|
messages = []
|
||||||
|
Task {
|
||||||
|
try? await messageStore.deleteAll()
|
||||||
|
}
|
||||||
KeychainStore.clearAll()
|
KeychainStore.clearAll()
|
||||||
let webSocket = webSocket
|
let webSocket = webSocket
|
||||||
Task {
|
Task {
|
||||||
|
|||||||
@ -55,6 +55,13 @@ actor MessageStore {
|
|||||||
try write(messages)
|
try write(messages)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func deleteAll() throws {
|
||||||
|
let url = try messagesURL()
|
||||||
|
if fileManager.fileExists(atPath: url.path) {
|
||||||
|
try fileManager.removeItem(at: url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var unreadCount: Int {
|
var unreadCount: Int {
|
||||||
get async {
|
get async {
|
||||||
(try? loadAll().filter { !$0.isRead }.count) ?? 0
|
(try? loadAll().filter { !$0.isRead }.count) ?? 0
|
||||||
|
|||||||
@ -41,6 +41,27 @@ const pairingSessions = new Map(); // code -> session
|
|||||||
const connections = new Map(); // peerID -> { ws, name }
|
const connections = new Map(); // peerID -> { ws, name }
|
||||||
const fileRegistry = new Map(); // fileID -> { diskPath, originalName, size }
|
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) {
|
function positiveInteger(value, fallback) {
|
||||||
const parsed = Number.parseInt(value ?? '', 10);
|
const parsed = Number.parseInt(value ?? '', 10);
|
||||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user