Matthias Nott
2026-03-21 a85c355d27a2016d3fe3f942ae6edfd9978d0e30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:path_provider/path_provider.dart';
import '../models/message.dart';
/// Per-session JSON file persistence with debounced saves.
class MessageStore {
  MessageStore._();
  static Directory? _baseDir;
  static Timer? _debounceTimer;
  static final Map<String, List<Message>> _pendingSaves = {};
  /// Initialize the base directory for message storage.
  static Future<Directory> _getBaseDir() async {
    if (_baseDir != null) return _baseDir!;
    final appDir = await getApplicationDocumentsDirectory();
    _baseDir = Directory('${appDir.path}/messages');
    if (!await _baseDir!.exists()) {
      await _baseDir!.create(recursive: true);
    }
    return _baseDir!;
  }
  static String _fileForSession(String sessionId) {
    // Sanitize session ID for filename
    final safe = sessionId.replaceAll(RegExp(r'[^\w\-]'), '_');
    return 'session_$safe.json';
  }
  /// Save messages for a session with 1-second debounce.
  static void save(String sessionId, List<Message> messages) {
    _pendingSaves[sessionId] = messages;
    _debounceTimer?.cancel();
    _debounceTimer = Timer(const Duration(seconds: 1), _flushAll);
  }
  /// Immediately flush all pending saves.
  static Future<void> flush() async {
    _debounceTimer?.cancel();
    await _flushAll();
  }
  static Future<void> _flushAll() async {
    final entries = Map<String, List<Message>>.from(_pendingSaves);
    _pendingSaves.clear();
    for (final entry in entries.entries) {
      await _writeSession(entry.key, entry.value);
    }
  }
  static Future<void> _writeSession(
      String sessionId, List<Message> messages) async {
    try {
      final dir = await _getBaseDir();
      final file = File('${dir.path}/${_fileForSession(sessionId)}');
      // Strip heavy fields for persistence
      final lightMessages = messages.map((m) => m.toJsonLight()).toList();
      await file.writeAsString(jsonEncode(lightMessages));
    } catch (e) {
      // Silently fail - message persistence is best-effort
    }
  }
  /// Load messages for a session.
  /// [limit] controls how many recent messages to return (default: 50).
  /// [offset] is the number of messages to skip from the end (for pagination).
  static Future<List<Message>> load(
    String sessionId, {
    int limit = 50,
    int offset = 0,
  }) async {
    try {
      final dir = await _getBaseDir();
      final file = File('${dir.path}/${_fileForSession(sessionId)}');
      if (!await file.exists()) return [];
      final jsonStr = await file.readAsString();
      final List<dynamic> jsonList = jsonDecode(jsonStr) as List<dynamic>;
      final allMessages = jsonList
          .map((j) => Message.fromJson(j as Map<String, dynamic>))
          .where((m) => !m.isEmptyVoice) // Filter out voice msgs with no content
          .toList();
      // Paginate from the end (newest messages first in storage)
      if (offset >= allMessages.length) return [];
      final end = allMessages.length - offset;
      final start = (end - limit).clamp(0, end);
      return allMessages.sublist(start, end);
    } catch (e) {
      return [];
    }
  }
  /// Load all messages for a session (no pagination).
  static Future<List<Message>> loadAll(String sessionId) async {
    try {
      final dir = await _getBaseDir();
      final file = File('${dir.path}/${_fileForSession(sessionId)}');
      if (!await file.exists()) return [];
      final jsonStr = await file.readAsString();
      final List<dynamic> jsonList = jsonDecode(jsonStr) as List<dynamic>;
      return jsonList
          .map((j) => Message.fromJson(j as Map<String, dynamic>))
          .where((m) => !m.isEmptyVoice)
          .toList();
    } catch (e) {
      return [];
    }
  }
  /// Delete stored messages for a session.
  static Future<void> delete(String sessionId) async {
    try {
      final dir = await _getBaseDir();
      final file = File('${dir.path}/${_fileForSession(sessionId)}');
      if (await file.exists()) {
        await file.delete();
      }
    } catch (_) {}
  }
  /// Clear all stored messages.
  static Future<void> clearAll() async {
    try {
      final dir = await _getBaseDir();
      if (await dir.exists()) {
        await dir.delete(recursive: true);
        await dir.create(recursive: true);
      }
    } catch (_) {}
  }
}