[MS-1299] Sync architecture revamp, part 2#1561
Conversation
… SyncOrchestrator
… call sites - test adjustments
…astSyncId retrieval
… call sites - test adjustments 2
… are in ExecuteSyncCommandUseCase
…or awaited before
…coroutine management correctness)
There was a problem hiding this comment.
Pull request overview
This PR implements phase 2 of a comprehensive sync architecture revamp, introducing a structured command-based API for sync operations. The changes replace the imperative sync orchestration with a reactive, command-driven approach using expressive DSL-style builders.
Changes:
- Introduced
SyncCommandsDSL builders with autocomplete-friendly structure for scheduling and one-time sync operations - Created
SyncResponsewithawait()extension for suspendable command execution - Migrated most
SyncOrchestratorandEventSyncManagerfunctionality intoExecuteSyncCommandUseCaseand updatedSyncUseCase - Updated all callsites throughout features (login-check, dashboard, validate-subject-pool) and Application.kt to use new sync commands
Reviewed changes
Copilot reviewed 39 out of 39 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| infra/sync/src/main/java/com/simprints/infra/sync/SyncCommands.kt | New DSL-based command builder infrastructure with expressive API |
| infra/sync/src/main/java/com/simprints/infra/sync/SyncResponse.kt | Response wrapper with suspend-aware await() for command completion |
| infra/sync/src/main/java/com/simprints/infra/sync/usecase/internal/ExecuteSyncCommandUseCase.kt | Core command execution logic extracted from SyncOrchestrator |
| infra/sync/src/main/java/com/simprints/infra/sync/usecase/SyncUseCase.kt | Updated to accept and execute sync commands |
| infra/sync/src/main/java/com/simprints/infra/sync/SyncOrchestratorImpl.kt | Slimmed down - removed methods migrated to ExecuteSyncCommandUseCase |
| infra/sync/src/main/java/com/simprints/infra/sync/SyncOrchestrator.kt | Interface reduced with methods moved to use cases |
| infra/sync/src/main/java/com/simprints/infra/sync/config/usecase/*.kt | Config use cases updated to use new sync commands with stopAndStartAround |
| id/src/main/java/com/simprints/id/Application.kt | Application startup now uses sync commands for background work scheduling |
| id/src/main/java/com/simprints/id/services/sync/events/down/EventDownSyncResetService.kt | Service updated to use sync commands with await() |
| feature/login-check/src/main/java/com/simprints/feature/logincheck/*.kt | Login flow uses sync commands with proper await() semantics |
| feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/syncinfo/*.kt | Dashboard sync UI migrated to sync commands |
| feature/validate-subject-pool/src/main/java/com/simprints/feature/validatepool/usecase/*.kt | Validation flow updated with sync commands |
| All test files | Comprehensive test coverage for new command infrastructure and updated existing tests |
...shboard/src/main/java/com/simprints/feature/dashboard/settings/syncinfo/SyncInfoViewModel.kt
Outdated
Show resolved
Hide resolved
...om/simprints/feature/dashboard/settings/syncinfo/moduleselection/ModuleSelectionViewModel.kt
Outdated
Show resolved
Hide resolved
...ync/src/test/java/com/simprints/infra/sync/usecase/internal/ExecuteSyncCommandUseCaseTest.kt
Outdated
Show resolved
Hide resolved
feature/dashboard/src/main/java/com/simprints/feature/dashboard/debug/DebugFragment.kt
Outdated
Show resolved
Hide resolved
|
luhmirin-s
left a comment
There was a problem hiding this comment.
I have couple of concerns regarding the complexity of this solution, as it does not seem to simplify sync usage compared to the original sync orchestration.
infra/sync/src/main/java/com/simprints/infra/sync/usecase/internal/ExecuteSyncCommandUseCase.kt
Outdated
Show resolved
Hide resolved
infra/sync/src/main/java/com/simprints/infra/sync/SyncCommands.kt
Outdated
Show resolved
Hide resolved
infra/sync/src/main/java/com/simprints/infra/sync/SyncCommands.kt
Outdated
Show resolved
Hide resolved
infra/sync/src/main/java/com/simprints/infra/sync/usecase/SyncUseCase.kt
Outdated
Show resolved
Hide resolved
...main/java/com/simprints/infra/sync/config/usecase/RescheduleWorkersIfConfigChangedUseCase.kt
Outdated
Show resolved
Hide resolved
infra/sync/src/main/java/com/simprints/infra/sync/usecase/SyncUseCase.kt
Outdated
Show resolved
Hide resolved
BurningAXE
left a comment
There was a problem hiding this comment.
What started out as an attempt to simplify sync VM implementation has spiralled into a general sync refactoring. Given that we are discussing architecture and not implementation in the PR - I think it makes sense to "go back to the drawing board" and actually pass this through a design doc. There we can discuss the pros and cons of different approaches. Once we agree on the architecture (which shouldn't necessarily change (much)), we can move to the implementation part.
Just to keep the scope already narrowed before re-planning: the part about the usage of workers isn't up to a change, at least not recommended - that was already researched before Phase 1. Workers are already the reliable solution and shouldn't be replaced, even if the app is in the foreground - at any moment it isn't anymore, and other solutions to keep the sync going would be no better. Also, the VM is already using reactive pattern as well. So, the part about reactive sync architecture focuses on the SyncOrchestrator. Also, the Phase 1 had already outlined the work that's being done here. It was the exploration part, marked as "spike", and is now in |
…dulingCommand moved to SyncOrchestrator to avoid a stateful usecase
luhmirin-s
left a comment
There was a problem hiding this comment.
Left some minor nitpicks and suggestions, but overall this iteration LGTM - it is a nice balance of API improvements with manageable amount of complexity.
| stop: () -> Unit, | ||
| start: suspend () -> Unit, | ||
| ): Job { | ||
| val shouldStop = |
There was a problem hiding this comment.
Nitpick: I don't feel that those variables provide anything meaningful.
Wouldn't this be simpler?
return appScope.launch(ioDispatcher) {
when (action) {
START -> start()
STOP -> {
stop()
Job().apply { complete() }
}
RESTART -> {
stop()
start()
}
}
}
Also, looking at the logic, is there a case where we would want to explicitly call "START" instead of "RESTART" (to be sure there are no 2 jobs running at the same time)?
There was a problem hiding this comment.
I'll take another look at these in the phase 3 intended for cleanup. Now we have the signatures stabilized and can proceed with this.
|
|
||
| val shouldStart = | ||
| action == OneTime.Action.START || action == OneTime.Action.RESTART | ||
| if (!shouldStart) { |
There was a problem hiding this comment.
Could be just if (action == OneTime.Action.STOP) {} here.
|
|
||
| val shouldSchedule = | ||
| action == ScheduleCommand.Action.RESCHEDULE | ||
| if (!shouldSchedule) { |
There was a problem hiding this comment.
This is literally if (action != ScheduleCommand.Action.RESCHEDULE), tho.
| ): Job { | ||
| val shouldUnschedule = | ||
| action == ScheduleCommand.Action.UNSCHEDULE || blockWhileUnscheduled != null | ||
| if (shouldUnschedule) { |
There was a problem hiding this comment.
Nitpick, IMO it would be more readable without extra variable.
| lifecycleScope.launch(dispatcher) { | ||
| sync(SyncCommands.OneTimeNow.Events.stop()) | ||
| sync(SyncCommands.ScheduleOf.Events.stop()) | ||
| syncOrchestrator.executeOneTime(OneTime.Events.stop()) |
There was a problem hiding this comment.
IMO, this is much clearer than the previous version.
There was a problem hiding this comment.
Thanks! Let's keep the signature as it is (I followed your suggestion verbatim), for expressiveness.
| * Executes an immediate (one-time) sync control command. | ||
| * Returns a job of the ongoing command execution. | ||
| */ | ||
| fun executeOneTime(command: OneTime): Job |
There was a problem hiding this comment.
I would also suggest to take advantage of polymorphism and define both methods as fun execute(...): Job with different parameter types to reduce a bit of duplication in the API:
syncOrchestrator.execute(OneTime.Events.stop())
syncOrchestrator.execute(ScheduleCommand.Events.unschedule())
| UNSCHEDULE, | ||
| } | ||
|
|
||
| internal data class EverythingCommand( |
There was a problem hiding this comment.
Do I understand correctly, that "everything" in this case also includes the scheduled project/config sync? Are commands for those workers also in the works?
There was a problem hiding this comment.
Yes, it already includes these.
There was a problem hiding this comment.
What about the explicit commands to start/stop and schedule/unschedule the config sync workers separately?
There was a problem hiding this comment.
Config sync wasn't planned to be touched here.
feature/login-check/src/main/java/com/simprints/feature/logincheck/LoginCheckViewModel.kt
Show resolved
Hide resolved
| private val eventSyncManager: EventSyncManager, | ||
| private val enrolmentRecordRepository: EnrolmentRecordRepository, | ||
| private val syncOrchestrator: SyncOrchestrator, |
There was a problem hiding this comment.
The order of constructor parameters has been changed, placing syncOrchestrator after eventSyncManager and enrolmentRecordRepository, while it was first in the original constructor. While this doesn't affect functionality due to named parameters in Dagger injection, it breaks consistency with the pattern of having dependencies in a stable, logical order.
Consider maintaining the original parameter order to avoid confusion and maintain consistency with the codebase.
| private val eventSyncManager: EventSyncManager, | |
| private val enrolmentRecordRepository: EnrolmentRecordRepository, | |
| private val syncOrchestrator: SyncOrchestrator, | |
| private val syncOrchestrator: SyncOrchestrator, | |
| private val eventSyncManager: EventSyncManager, | |
| private val enrolmentRecordRepository: EnrolmentRecordRepository, |
| private val configRepository: ConfigRepository, | ||
| private val syncOrchestrator: SyncOrchestrator, |
There was a problem hiding this comment.
The order of constructor parameters has been changed, placing syncOrchestrator after configRepository, while the test setup order at line 36 keeps the original order. This inconsistency can cause confusion during code review and maintenance.
Consider keeping constructor parameters in a consistent order across production and test code.
| private val configRepository: ConfigRepository, | |
| private val syncOrchestrator: SyncOrchestrator, | |
| private val syncOrchestrator: SyncOrchestrator, | |
| private val configRepository: ConfigRepository, |



JIRA ticket
Will be released in: 2026.2.0
Boundaries for the changes - what's intentionally out of scope:
WorkManagerand workers. This is already the optimal foundation for sync - both foreground and background.SyncInfoViewModel. Already observes sync state.Previously done in phase 1:
ObserveSyncInfoUseCasemade fully reactive - without suspension points.SyncUseCase, a unified (events + images) sync status reactive observation use case introduced. It is aStateFlow, with up-to-date sync status available as.valuesyncronously from anywhere.CountSyncableUseCase, a unified (events + images) counters reactive observation use case introduced.Notable changes
observeSyncState(): StateFlow<SyncStatus>introduced inSyncOrchestratorexecuteOneTime(command: OneTime): Jobintroduced inSyncOrchestratorexecuteSchedulingCommand(command: ScheduleCommand): Jobintroduced inSyncOrchestratorTesting guidance
Additional work checklist