Skip to content

feat(sync): inject isOnlineOverride into SyncService.isOnline() - #68

Open
abhijitnairDhwani wants to merge 1 commit into
developfrom
feat/is-online-override
Open

feat(sync): inject isOnlineOverride into SyncService.isOnline()#68
abhijitnairDhwani wants to merge 1 commit into
developfrom
feat/is-online-override

Conversation

@abhijitnairDhwani

Copy link
Copy Markdown
Collaborator

What

Adds an optional Future<bool> Function()? isOnlineOverride parameter on SyncService and FrappeSDK. When set, the override replaces the connectivity_plus probe inside SyncService.isOnline(). When unset (the default), behaviour is unchanged.

Why this is needed

SyncService.isOnline() calls Connectivity().checkConnectivity() directly and only accepts mobile | wifi | ethernet:

Future<bool> isOnline() async {
  if (!offlineMode.enabled) return false;
  final connectivityResult = await Connectivity().checkConnectivity();
  return connectivityResult.contains(ConnectivityResult.mobile) ||
      connectivityResult.contains(ConnectivityResult.wifi) ||
      connectivityResult.contains(ConnectivityResult.ethernet);
}

On Android emulators with adb reverse, the platform reports ConnectivityResult.none because the tunnel isn't a recognised network type — even though HTTP traffic to the bench flows fine. pullSync / pullSyncMany / pushSync then early-exit with noConnectivity and refuse to run.

Today consumers have no app-side escape:

  • The ConnectivityWatcher isn't exported from the barrel.
  • SyncService.isOnline() bypasses the watcher anyway — it probes the plugin directly.
  • The FrappeSDK constructor takes no hook to replace the probe.

The only workaround until now has been to bypass SyncService.pullSync / pullSyncMany entirely from the consumer app — losing the SDK's delta cursors, page lookahead, and RESUME-on-crash semantics in the process. We were carrying that bypass for ~2 weeks in the Swasti V3 mobile app before this PR.

What this PR changes

  1. SyncService constructor accepts Future<bool> Function()? isOnlineOverride.
  2. SyncService.isOnline() calls the override first when set; falls back to the platform probe on throw (with a logged warning) so a buggy override can't brick sync.
  3. FrappeSDK() accepts isOnlineOverride and threads it through to SyncService.
  4. The FrappeSDK.forTesting constructor initialises the field to null in its initializer list to keep tests source-compatible.

Backward compatibility

  • Default value is null — existing FrappeSDK(baseUrl: ...) and SyncService(...) call sites behave exactly as before.
  • The existing private _isOnlineOverrideForTesting test seam in frappe_sdk.dart is unchanged.

Safety notes

  • offlineMode.enabled gate runs first; the override does not bypass an explicit offline-mode opt-out.
  • If the override throws, isOnline() logs the failure and falls through to the platform probe.

Intended use

Dev builds that talk to a local bench via adb reverse:

final sdk = FrappeSDK(
  baseUrl: 'http://10.0.2.2:8001',
  isOnlineOverride: () async => true, // dev-only
);

Production callers should leave isOnlineOverride null and rely on the platform probe.

Tests

183 sync-related tests pass unchanged on this branch (test/sync/, test/concurrency/sync_mutex_test.dart, test/services/sync_engine_builder_send_test.dart). No new tests added — the change is opt-in and the existing suite covers the default path.

`SyncService.isOnline()` directly probes `connectivity_plus` and accepts
only `mobile | wifi | ethernet`. On Android emulators with `adb reverse`
the link reads as `ConnectivityResult.none` and the gate refuses sync
even though HTTP traffic to the bench is fully functional. Today the
only escape is to bypass `pullSync`/`pullSyncMany` entirely from the
consumer app — costing delta pulls, resumable cursors, and the
in-built page lookahead that `_pullOneInternal` already implements.

Add an optional `isOnlineOverride` callback wired through
`FrappeSDK(...)` → `SyncService(...)` → `isOnline()`. When set, the
override replaces the `connectivity_plus` probe. When unset (the
default), behaviour is exactly as today — fully backwards-compatible.

Semantics:
- The `offlineMode.enabled` gate always runs first; overriding doesn't
  bypass an explicit offline-mode opt-out.
- If the override throws, `isOnline()` logs and falls through to the
  platform probe so a buggy callback can't brick sync.

Production callers should leave this null. Legitimate use is the
emulator+adb-reverse dev workflow, where the consumer sets
`isOnlineOverride: () async => true`. The existing private
`_isOnlineOverrideForTesting` test seam is unchanged.

Both `FrappeSDK()` and `FrappeSDK.forTesting()` accept the parameter;
the test constructor initialises it to null in its initializer list.
183 sync-related tests pass unchanged on this branch.
@Omprakash-48
Omprakash-48 self-requested a review June 2, 2026 08:45

@deepak-dhwani deepak-dhwani left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR #68 — feat(sync): inject isOnlineOverride into SyncService.isOnline() — Round 1 Review

Two files changed, 100 lines total. The change is small and well-motivated.


What the diff does

SyncService gains a Future Function()? _isOnlineOverride field. isOnline() calls it first if set, falls back to the connectivity_plus probe on throw.
FrappeSDK accepts isOnlineOverride and threads it through. The forTesting constructor explicitly initialises it to null in the initialiser list to stay
source-compatible.


Correctness

The implementation is correct. The offlineMode.enabled gate runs before the override, so a () async => true override cannot bypass an explicit offline-mode
opt-out. The local capture final override = _isOnlineOverride before the null check is good defensive Dart — it avoids a race if the field were ever mutated
concurrently (it is final, so not an issue here, but the pattern is idiomatic). The try/catch fallback on override throw is the right behaviour for a safety
valve.


MEDIUM M1 — print() should be sdkLog()

lib/src/services/sync_service.dart lines 91–96

// ignore: avoid_print
print(
'SyncService.isOnline: isOnlineOverride threw, '
'falling back to platform probe — $e\n$st',
);

Every other log statement in the SDK uses sdkLog() from utils/sdk_log.dart, which routes through the SDK's own logging infrastructure and is suppressible. This
is a plain print() with a // ignore: avoid_print suppression. It will appear in production builds on Android/iOS logcat/Console for any consumer whose override
happens to throw, with no way to suppress it. Replace with sdkLog(...) to match the rest of the codebase.


MEDIUM M2 — No tests for the override behaviour

test/services/sync_service_online_test.dart covers offlineMode.enabled = false paths and the pullSyncMany empty-list short-circuit. None of the existing tests
exercise isOnlineOverride. The three cases that need covering:

  1. Override returns true → isOnline() returns true (override takes effect)
  2. Override returns false → isOnline() returns false (override suppresses sync)
  3. Override throws → isOnline() falls back to the platform probe (the safety-valve path)

Case 3 is the most important. It is the correctness guarantee the PR description highlights, and without a test a future refactor that removes the try/catch
silently breaks the contract. These are straightforward unit tests — pass isOnlineOverride directly to SyncService in construction, no real network needed.


LOW L1 — New constructor parameter not mentioned in CHANGELOG

FrappeSDK and SyncService are both fully-exported public API. The new isOnlineOverride parameter should appear in CHANGELOG.md so consumers scanning it know the
hook exists.


Verdict

M1 and M2 are quick fixes — swap print for sdkLog and add three short unit tests — but the safety-valve path (override throws → fallback) is the key correctness claim of this PR and it currently has no test coverage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants