Files
tasq/lib/brick/repository.dart
T
2026-04-27 06:20:39 +08:00

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;
}
}
}