161 lines
6.2 KiB
Dart
161 lines
6.2 KiB
Dart
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<void> 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<List<T>> mirroredStream<T extends OfflineFirstWithSupabaseModel>(
|
|
Stream<List<T>> 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<T>(item).catchError((Object e) {
|
|
debugPrint('[AppRepository] mirror upsert<$T> failed: $e');
|
|
return item;
|
|
});
|
|
}
|
|
yield batch;
|
|
}
|
|
}
|
|
}
|