import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart'; import 'package:brick_offline_first_with_rest/offline_queue.dart'; import 'package:brick_sqlite/brick_sqlite.dart'; import 'package:brick_sqlite/memory_cache_provider.dart'; // Hide the brick_supabase @Supabase annotation to avoid conflict with // supabase_flutter's Supabase singleton class. import 'package:brick_supabase/brick_supabase.dart' hide Supabase; import 'package:flutter/foundation.dart' show kIsWeb, debugPrint; import 'package:http/http.dart' as http; import 'package:sqflite/sqflite.dart' show databaseFactory; import 'package:supabase_flutter/supabase_flutter.dart'; // Generated by `flutter pub run build_runner build`. // Run: flutter pub run build_runner build --delete-conflicting-outputs // ignore: uri_does_not_exist import 'brick.g.dart'; // migrations + schema are generated into db/schema.g.dart // ignore: uri_does_not_exist import 'db/schema.g.dart'; export 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart' show OfflineFirstWithSupabaseModel; export 'package:brick_supabase/brick_supabase.dart' show SupabaseSerializable; export 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart' show ConnectOfflineFirstWithSupabase; export 'package:brick_sqlite/brick_sqlite.dart' show SqliteSerializable; export 'package:brick_offline_first/brick_offline_first.dart' show OfflineFirstGetPolicy; /// Singleton offline-first repository backed by Supabase + local SQLite. /// /// Initialization sequence in [main]: /// ```dart /// // 1. Get the Brick HTTP client (mobile only; null on web) /// final httpClient = AppRepository.createHttpClient(); /// /// // 2. Initialize Supabase WITH the Brick HTTP client so all REST /// // writes are intercepted and queued when offline. /// await Supabase.initialize(url: ..., anonKey: ..., httpClient: httpClient); /// /// // 3. Configure the repository (reads model dictionary from brick.g.dart) /// await AppRepository.configure(); /// ``` class AppRepository extends OfflineFirstWithSupabaseRepository { static AppRepository? _instance; static RestOfflineRequestQueue? _pendingQueue; /// The singleton. Throws if [configure] was not called first. static AppRepository get instance { assert(_instance != null, 'Call AppRepository.configure() first'); return _instance!; } /// True when this platform supports offline SQLite storage (i.e. not web). static bool get isOfflineSupported => !kIsWeb; /// True after [configure] has completed successfully. static bool get isConfigured => _instance != null; /// The Brick HTTP offline queue. `null` before [configure] or on web. /// /// Call [RestOfflineRequestQueue.stop] when offline to silence retry logs /// and avoid hammering a network that is known to be unavailable. /// Call [RestOfflineRequestQueue.start] when back online to replay queued /// writes. The stored requests are preserved in both states. static RestOfflineRequestQueue? get offlineQueue => _instance?.offlineRequestQueue; AppRepository._({ required super.supabaseProvider, required super.sqliteProvider, required super.migrations, required super.offlineRequestQueue, super.memoryCacheProvider, }); /// Creates and returns the Brick-managed HTTP client to pass to /// [Supabase.initialize]. Returns `null` on web (SQLite unavailable). /// /// Must be called *before* [Supabase.initialize] and *before* [configure]. static http.Client? createHttpClient() { if (kIsWeb) return null; final (client, queue) = OfflineFirstWithSupabaseRepository.clientQueue( databaseFactory: databaseFactory, // Only retry on truly transient server errors. // 400/409 indicate a permanent failure (bad payload or conflict) and // should NOT be retried — they will never self-heal and cause infinite // retry loops that spam the logs. reattemptForStatusCodes: const [408, 429, 500, 502, 503, 504], ); _pendingQueue = queue; return client; } /// Configures the repository singleton. /// /// Must be called *after* [Supabase.initialize]. Reads the Brick-generated /// model dictionaries from `brick.g.dart` (created by build_runner). static Future configure() async { if (_instance != null) return; if (kIsWeb) { debugPrint('[AppRepository] web — offline queue disabled'); return; } final queue = _pendingQueue; if (queue == null) { throw StateError( 'Call AppRepository.createHttpClient() before configure()', ); } _instance = AppRepository._( supabaseProvider: SupabaseProvider( Supabase.instance.client, modelDictionary: supabaseModelDictionary, ), sqliteProvider: SqliteProvider( 'tasq_offline.sqlite', databaseFactory: databaseFactory, modelDictionary: sqliteModelDictionary, ), migrations: migrations, offlineRequestQueue: queue, memoryCacheProvider: MemoryCacheProvider(), ); await _instance!.initialize(); _pendingQueue = null; debugPrint('[AppRepository] initialized — offline queue active'); } } /// Mirrors a Supabase realtime stream into the Brick local SQLite cache. /// /// Wrap any `client.from('t').stream(...)` source so every emission is /// upserted into Brick. The cache stays fresh during normal online use, which /// means a cold launch with no network can read a recent snapshot via /// `OfflineFirstGetPolicy.localOnly` instead of rendering an empty list. /// /// Cache writes are fire-and-forget — they never block UI updates and any /// upsert error is silently logged (mirroring is best-effort). extension AppRepositoryStreamMirror on AppRepository { Stream> mirroredStream( Stream> source, ) async* { await for (final batch in source) { for (final item in batch) { // Fire-and-forget; never block the UI on cache writes. // ignore: discarded_futures upsert(item).catchError((Object e) { debugPrint('[AppRepository] mirror upsert<$T> failed: $e'); return item; }); } yield batch; } } }