| .. | .. |
|---|
| 15 | 15 | static Directory? _baseDir; |
|---|
| 16 | 16 | static Timer? _debounceTimer; |
|---|
| 17 | 17 | static final Map<String, List<Message>> _pendingSaves = {}; |
|---|
| 18 | + // Per-session lock to prevent concurrent read/write on the same file |
|---|
| 19 | + static final Map<String, Completer<void>> _locks = {}; |
|---|
| 18 | 20 | |
|---|
| 19 | 21 | static const _backupChannel = |
|---|
| 20 | 22 | MethodChannel('com.mnsoft.pailot/backup'); |
|---|
| .. | .. |
|---|
| 60 | 62 | |
|---|
| 61 | 63 | /// Write directly to disk, bypassing debounce. For critical saves. |
|---|
| 62 | 64 | static Future<void> writeDirect(String sessionId, List<Message> messages) async { |
|---|
| 63 | | - // Cancel ALL pending debounce to prevent race with frozen iOS timers |
|---|
| 64 | 65 | _debounceTimer?.cancel(); |
|---|
| 65 | 66 | _pendingSaves.remove(sessionId); |
|---|
| 66 | | - await _writeSession(sessionId, messages); |
|---|
| 67 | + await _withLock(sessionId, () => _writeSession(sessionId, messages)); |
|---|
| 68 | + } |
|---|
| 69 | + |
|---|
| 70 | + /// Acquire a per-session lock, run the operation, release. |
|---|
| 71 | + static Future<T> _withLock<T>(String sessionId, Future<T> Function() fn) async { |
|---|
| 72 | + // Wait for any existing operation on this session to finish |
|---|
| 73 | + while (_locks.containsKey(sessionId)) { |
|---|
| 74 | + await _locks[sessionId]!.future; |
|---|
| 75 | + } |
|---|
| 76 | + final completer = Completer<void>(); |
|---|
| 77 | + _locks[sessionId] = completer; |
|---|
| 78 | + try { |
|---|
| 79 | + return await fn(); |
|---|
| 80 | + } finally { |
|---|
| 81 | + _locks.remove(sessionId); |
|---|
| 82 | + completer.complete(); |
|---|
| 83 | + } |
|---|
| 67 | 84 | } |
|---|
| 68 | 85 | |
|---|
| 69 | 86 | /// Immediately flush all pending saves. |
|---|
| .. | .. |
|---|
| 77 | 94 | _pendingSaves.clear(); |
|---|
| 78 | 95 | |
|---|
| 79 | 96 | for (final entry in entries.entries) { |
|---|
| 80 | | - await _writeSession(entry.key, entry.value); |
|---|
| 97 | + await _withLock(entry.key, () => _writeSession(entry.key, entry.value)); |
|---|
| 81 | 98 | } |
|---|
| 82 | 99 | } |
|---|
| 83 | 100 | |
|---|
| .. | .. |
|---|
| 128 | 145 | |
|---|
| 129 | 146 | /// Load all messages for a session (no pagination). |
|---|
| 130 | 147 | static Future<List<Message>> loadAll(String sessionId) async { |
|---|
| 148 | + return _withLock(sessionId, () => _loadAllImpl(sessionId)); |
|---|
| 149 | + } |
|---|
| 150 | + |
|---|
| 151 | + static Future<List<Message>> _loadAllImpl(String sessionId) async { |
|---|
| 131 | 152 | try { |
|---|
| 132 | 153 | final dir = await _getBaseDir(); |
|---|
| 133 | 154 | final file = File('${dir.path}/${_fileForSession(sessionId)}'); |
|---|