-
Notifications
You must be signed in to change notification settings - Fork 1
Add Bloc-like architecture layer for offline sync workflows #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d1d5ba3
d032f9e
2f41bef
5977aca
40c160b
3a7aaf3
43bbd52
645e566
2c291fe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| abstract class CloudDataSource<T> { | ||
| Future<void> create(T data); | ||
| Future<void> update(T data); | ||
| Future<void> delete(String id); | ||
| Future<T?> fetch(String id); | ||
| Future<List<T>> fetchAll(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| import '../models/sync_log.dart'; | ||
| import 'sync_log_store.dart'; | ||
|
|
||
| class InMemorySyncLogStore implements SyncLogStore { | ||
| final List<SyncLog> _logs = []; | ||
|
|
||
| @override | ||
| Future<void> add(SyncLog log) async { | ||
| _logs.add(log); | ||
| } | ||
|
|
||
| @override | ||
| Future<List<SyncLog>> getFailedLogs() async { | ||
| return _logs.where((log) => log.status == SyncStatus.failed).toList(); | ||
| } | ||
|
|
||
| @override | ||
| Future<List<SyncLog>> getPendingLogs() async { | ||
| return _logs.where((log) => log.status == SyncStatus.pending).toList(); | ||
| } | ||
|
|
||
| @override | ||
| Future<void> markFailed(String logId) async { | ||
| _update(logId, SyncStatus.failed); | ||
| } | ||
|
|
||
| @override | ||
| Future<void> markSynced(String logId) async { | ||
| _update(logId, SyncStatus.synced); | ||
| } | ||
|
|
||
| @override | ||
| Future<void> markSyncing(String logId) async { | ||
| _update(logId, SyncStatus.syncing); | ||
| } | ||
|
|
||
| void _update(String logId, SyncStatus status) { | ||
| final index = _logs.indexWhere((log) => log.id == logId); | ||
| if (index == -1) return; | ||
|
|
||
| _logs[index] = _logs[index].copyWith(status: status); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| abstract class LocalDataSource<T> { | ||
| Future<void> insert(T data); | ||
| Future<void> update(T data); | ||
| Future<void> delete(String id); | ||
| Future<T?> getById(String id); | ||
| Future<List<T>> getAll(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import '../models/sync_log.dart'; | ||
|
|
||
| abstract class SyncLogStore { | ||
| Future<void> add(SyncLog log); | ||
| Future<List<SyncLog>> getPendingLogs(); | ||
| Future<List<SyncLog>> getFailedLogs(); | ||
| Future<void> markSyncing(String logId); | ||
| Future<void> markSynced(String logId); | ||
| Future<void> markFailed(String logId); | ||
| } |
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -0,0 +1,56 @@ | ||||
| import 'dart:async'; | ||||
|
|
||||
| import '../events/sync_event.dart'; | ||||
| import '../repository/sync_repository.dart'; | ||||
| import '../states/sync_state.dart'; | ||||
|
|
||||
| class SyncController<T> { | ||||
| final SyncRepository<T> repository; | ||||
| final StreamController<SyncState> _states = | ||||
| StreamController<SyncState>.broadcast(); | ||||
|
|
||||
| SyncState _current = const SyncInitial(); | ||||
|
|
||||
| SyncController({required this.repository}) { | ||||
| _states.add(_current); | ||||
| } | ||||
|
|
||||
| Stream<SyncState> get states => _states.stream; | ||||
|
|
||||
| SyncState get currentState => _current; | ||||
|
|
||||
| Future<void> handle(SyncEvent<T> event) async { | ||||
| _emit(const SyncInProgress()); | ||||
|
|
||||
| try { | ||||
| if (event is AddData<T>) { | ||||
| await repository.add(event.data); | ||||
| } else if (event is UpdateData<T>) { | ||||
| await repository.update(event.data); | ||||
| } else if (event is DeleteData<T>) { | ||||
| await repository.delete(event.id); | ||||
| } else if (event is StartSync<T>) { | ||||
| await repository.syncPending(); | ||||
| } else if (event is RetryFailed<T>) { | ||||
| await repository.retryFailed(); | ||||
|
Harsh4114 marked this conversation as resolved.
|
||||
| } else { | ||||
| throw UnsupportedError( | ||||
| 'Unsupported SyncEvent type: ${event.runtimeType}'); | ||||
| } | ||||
|
|
||||
| _emit(const SyncSuccess()); | ||||
| } catch (e) { | ||||
| _emit(SyncFailure(e.toString())); | ||||
| rethrow; | ||||
|
||||
| rethrow; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import '../controller/sync_controller.dart'; | ||
| import '../events/sync_event.dart'; | ||
| import '../states/sync_state.dart'; | ||
|
|
||
| class OfflineSyncEngine<T> { | ||
| final SyncController<T> _controller; | ||
|
|
||
| OfflineSyncEngine({required SyncController<T> controller}) | ||
| : _controller = controller; | ||
|
|
||
| Stream<SyncState> get states => _controller.states; | ||
|
|
||
| SyncState get currentState => _controller.currentState; | ||
|
|
||
| Future<void> add(T data) => _controller.handle(AddData<T>(data)); | ||
|
|
||
| Future<void> update(T data) => _controller.handle(UpdateData<T>(data)); | ||
|
|
||
| Future<void> delete(String id) => _controller.handle(DeleteData<T>(id)); | ||
|
|
||
| Future<void> sync() => _controller.handle(const StartSync<T>()); | ||
|
|
||
| Future<void> retryFailed() => _controller.handle(const RetryFailed<T>()); | ||
|
|
||
| Future<void> dispose() => _controller.dispose(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| abstract class SyncEvent<T> { | ||
| const SyncEvent(); | ||
| } | ||
|
|
||
| class AddData<T> extends SyncEvent<T> { | ||
| final T data; | ||
| const AddData(this.data); | ||
| } | ||
|
|
||
| class UpdateData<T> extends SyncEvent<T> { | ||
| final T data; | ||
| const UpdateData(this.data); | ||
| } | ||
|
|
||
| class DeleteData<T> extends SyncEvent<T> { | ||
| final String id; | ||
| const DeleteData(this.id); | ||
| } | ||
|
|
||
| class StartSync<T> extends SyncEvent<T> { | ||
| const StartSync(); | ||
| } | ||
|
|
||
| class RetryFailed<T> extends SyncEvent<T> { | ||
| const RetryFailed(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| enum SyncOperationType { create, update, delete } | ||
|
|
||
| enum SyncStatus { pending, syncing, synced, failed } | ||
|
|
||
| class SyncLog { | ||
| final String id; | ||
| final String entityId; | ||
| final SyncOperationType operation; | ||
| final DateTime timestamp; | ||
| final SyncStatus status; | ||
|
|
||
| const SyncLog({ | ||
| required this.id, | ||
| required this.entityId, | ||
| required this.operation, | ||
| required this.timestamp, | ||
| required this.status, | ||
| }); | ||
|
|
||
| SyncLog copyWith({ | ||
| String? id, | ||
| String? entityId, | ||
| SyncOperationType? operation, | ||
| DateTime? timestamp, | ||
| SyncStatus? status, | ||
| }) { | ||
| return SyncLog( | ||
| id: id ?? this.id, | ||
| entityId: entityId ?? this.entityId, | ||
| operation: operation ?? this.operation, | ||
| timestamp: timestamp ?? this.timestamp, | ||
| status: status ?? this.status, | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_statesis a broadcast stream and the initialSyncInitialis added in the constructor. Any listener that subscribes after construction will not receive that initial state (broadcast streams don’t replay). If consumers are expected to observe the initial state viastates, consider emitting the current state inonListen(or using a BehaviorSubject-style controller).