import 'dart:math'; import 'package:matrix/matrix.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; /// Synthetische Matrix-Daten für Tests und `benchmark/` – komplett offline: /// kein Homeserver, keine echten Konten, keine Secrets. /// /// Die Daten gehen über die ECHTE SDK-Datenbankschicht (`MatrixSdkDatabase`, /// `storeRoomUpdate`/`storeEventUpdate` in Sync-artigen Transaktionen) – also /// denselben Weg wie beim echten Sync. Dadurch messen/prüfen Tests und /// Benchmarks die Speicherstruktur, die die App wirklich auf der Platte hat. class SyntheticMatrixData { SyntheticMatrixData._(); static const homeserverDomain = 'bench.invalid'; static const _senders = 5; /// Wörter für die Nachrichtentexte (bewusst mit Umlauten – die Suche /// arbeitet mit `toLowerCase`). static const _words = [ 'hallo', 'morgen', 'treffen', 'kaffee', 'bahnhof', 'urlaub', 'foto', 'danke', 'später', 'gleich', 'abend', 'essen', 'kino', 'wetter', 'regen', 'sonne', 'arbeit', 'pause', 'auto', 'zug', 'buch', 'musik', 'spiel', 'garten', 'hund', 'katze', 'einkaufen', 'termin', 'grüße', 'übermorgen', ]; static String roomId(int r) => '!room$r:$homeserverDomain'; static String eventId(int r, int i) => '\$r${r}e$i'; static String sender(int i) => '@user${i % _senders}:$homeserverDomain'; /// Öffnet die SDK-Datenbank auf [db] und baut einen Offline-Client darauf. static Future openClient( Database db, { String name = 'PyramidBench', }) async { final sdkDb = await MatrixSdkDatabase.init('pyramid', database: db); return Client(name, database: sdkDb); } /// Schreibt [rooms] Räume mit je [eventsPerRoom] Nachrichten in die DB. /// /// Wie beim echten Sync landen die Events in Transaktionen à [batchSize] /// (≈ eine Sync-Antwort) und in zeitlicher Reihenfolge (älteste zuerst). /// Jede 10. Nachricht ist ein Bild (`m.image`). Ist [needleEvery] gesetzt, /// enthält jede n-te Nachricht zusätzlich das Wort [needle] – so kennen /// Tests die exakte Trefferzahl der Suche. static Future populate( Client client, { required int rooms, required int eventsPerRoom, int batchSize = 50, String needle = 'nadelwort', int? needleEvery, int seed = 42, }) async { final db = client.database; final rnd = Random(seed); final baseTs = DateTime.utc(2026, 1, 1).millisecondsSinceEpoch; for (var r = 0; r < rooms; r++) { final id = roomId(r); await db.transaction(() async { await db.storeRoomUpdate( id, JoinedRoomUpdate( summary: RoomSummary.fromJson({ 'm.joined_member_count': _senders, 'm.invited_member_count': 0, }), ), null, client, ); for (final state in _roomState(r)) { await db.storeEventUpdate(id, state, EventUpdateType.state, client); } }); for (var start = 0; start < eventsPerRoom; start += batchSize) { final end = min(start + batchSize, eventsPerRoom); await db.transaction(() async { for (var i = start; i < end; i++) { final hasNeedle = needleEvery != null && i % needleEvery == 0; await db.storeEventUpdate( id, _message(r, i, baseTs, rnd, hasNeedle ? needle : null), EventUpdateType.timeline, client, ); } }); } } } /// Lädt die Raumliste aus der DB in `client.rooms` – das, was beim /// App-Start `client.init()` macht, ohne Netzwerk/Login. static Future> loadRooms(Client client) async { final rooms = await client.database.getRoomList(client); client.rooms ..clear() ..addAll(rooms); return rooms; } static List _roomState(int r) { final ts = DateTime.utc(2025, 12, 1).millisecondsSinceEpoch; MatrixEvent state( String type, String stateKey, Map content, int n, ) => MatrixEvent( type: type, eventId: '\$state${r}_$n', senderId: sender(0), originServerTs: DateTime.fromMillisecondsSinceEpoch(ts + n), stateKey: stateKey, content: content, roomId: roomId(r), ); return [ state(EventTypes.RoomCreate, '', {'creator': sender(0)}, 0), state(EventTypes.RoomName, '', {'name': 'Raum $r'}, 1), for (var u = 0; u < _senders; u++) state(EventTypes.RoomMember, sender(u), { 'membership': 'join', 'displayname': 'Nutzer $u', }, 2 + u), ]; } static MatrixEvent _message( int r, int i, int baseTs, Random rnd, String? needle, ) { final isImage = i % 10 == 9; final Map content; if (isImage) { content = { 'msgtype': 'm.image', 'body': 'IMG_${r}_$i.jpg', 'url': 'mxc://$homeserverDomain/img$r$i', 'info': { 'w': 1920, 'h': 1080, 'mimetype': 'image/jpeg', 'size': 250000, }, }; } else { final words = List.generate( 4 + rnd.nextInt(20), (_) => _words[rnd.nextInt(_words.length)], ); if (needle != null) words.insert(rnd.nextInt(words.length), needle); content = {'msgtype': 'm.text', 'body': words.join(' ')}; } return MatrixEvent( type: EventTypes.Message, eventId: eventId(r, i), senderId: sender(i), originServerTs: DateTime.fromMillisecondsSinceEpoch( baseTs + r * 1000 + i * 60000, ), content: content, roomId: roomId(r), ); } }