fix(boot): native offline-first launch is silent and fast (Android 10)
Two native offline-first boot glitches (seen on Android 10): - bootOffline forced offlineNoNetwork, so its first successful reconcile healed to online with a spurious "back online" toast on a first launch that actually had connectivity. The boot-assumed offline is now PROVISIONAL: the first probe confirms it — a success heals silently (nothing to come back from), a failure turns it into a real offline whose later recovery does toast. - A native launch with a cached guest session (no email) skipped the offline short-circuit and hung ~25 s on adoptSession's retrying profile fetch in airplane mode. The native channel is offline-first, so it now takes the same reachability-gated fallback (~3 s bound) as an installed email PWA.
This commit is contained in:
@@ -982,7 +982,11 @@ export async function bootstrap(): Promise<void> {
|
||||
// to online when the network returns. Web-only reaches here (the Mini-App branches returned above),
|
||||
// so isStandalone gates it.
|
||||
const cachedProfile = await loadProfile();
|
||||
const canOffline = !!cachedProfile && !vkcb && isStandalone() && !!cachedProfile.email;
|
||||
// A native app is offline-first, so a native launch — even a guest with no email — must fall back
|
||||
// to the cached session's offline lobby rather than hang ~25 s on adoptSession's retrying profile
|
||||
// fetch when the device is offline (e.g. airplane mode). Web keeps the installed-PWA + email gate.
|
||||
const nativeChannel = clientChannel() === 'android' || clientChannel() === 'ios';
|
||||
const canOffline = !vkcb && (nativeChannel || (!!cachedProfile && isStandalone() && !!cachedProfile.email));
|
||||
if (canOffline) {
|
||||
const interfaceOffline = typeof navigator !== 'undefined' && navigator.onLine === false;
|
||||
gateway.setToken(saved.token);
|
||||
|
||||
@@ -175,7 +175,10 @@ function applyEffect(effect: Effect): void {
|
||||
* poll immediately (the native reconcile) rather than after the first interval.
|
||||
*/
|
||||
export function bootOffline(kickNow = false): void {
|
||||
snap = { state: 'offlineNoNetwork', fails: 0, connectingSince: 0 };
|
||||
// provisional: this offline is an assumption, not an observation — the first probe result confirms
|
||||
// it (silent heal if reachable, so a first launch that had connectivity does not flash a spurious
|
||||
// "back online" toast) or turns it into a real offline (see the reducer).
|
||||
snap = { state: 'offlineNoNetwork', fails: 0, connectingSince: 0, provisional: true };
|
||||
arm(kickNow ? 0 : OFFLINE_POLL_MS);
|
||||
}
|
||||
|
||||
|
||||
@@ -248,3 +248,31 @@ describe('reduce — purity', () => {
|
||||
expect(prev).toEqual({ state: 'connecting', fails: 1, connectingSince: 5 });
|
||||
});
|
||||
});
|
||||
|
||||
// A boot-assumed (provisional) offline — bootOffline sets this — heals SILENTLY on its first
|
||||
// successful probe (a first launch that actually had connectivity must not flash "back online"),
|
||||
// while a failed probe confirms it as a real offline whose later recovery does toast.
|
||||
describe('provisional (boot-assumed) offline', () => {
|
||||
const bootOffline: NetSnapshot = { state: 'offlineNoNetwork', fails: 0, connectingSince: 0, provisional: true };
|
||||
|
||||
it('heals to online with NO toast when the first probe succeeds', () => {
|
||||
const r = reduce(bootOffline, e('probeOk'), cfg);
|
||||
expect(r.next.state).toBe('online');
|
||||
expect(r.effects).toEqual([]); // no "back online" toast — it was never really offline
|
||||
});
|
||||
|
||||
it('clears provisional on a failed probe, then toasts on the eventual recovery', () => {
|
||||
const failed = reduce(bootOffline, e('probeFailed'), cfg);
|
||||
expect(failed.next.state).toBe('offlineNoNetwork');
|
||||
expect(failed.next.provisional).toBe(false);
|
||||
expect(failed.effects).toEqual([]);
|
||||
const healed = reduce(failed.next, e('probeOk'), cfg);
|
||||
expect(healed.next.state).toBe('online');
|
||||
expect(healed.effects).toEqual([{ kind: 'toast', toast: 'online' }]); // now a real recovery
|
||||
});
|
||||
|
||||
it('a non-provisional (observed) offline still toasts on recovery', () => {
|
||||
const r = reduce(offline, e('probeOk'), cfg);
|
||||
expect(r.effects).toEqual([{ kind: 'toast', toast: 'online' }]);
|
||||
});
|
||||
});
|
||||
|
||||
+18
-3
@@ -74,6 +74,14 @@ export interface NetSnapshot {
|
||||
* it (0 while not `connecting`).
|
||||
*/
|
||||
connectingSince: number;
|
||||
/**
|
||||
* provisional marks an `offlineNoNetwork` that was ASSUMED at boot (bootOffline), not observed from
|
||||
* a real online→offline transition. The first probe confirms it: a success means we were reachable
|
||||
* all along, so the machine heals to online SILENTLY (no "back online" toast — there was nothing to
|
||||
* come back from); a failure clears the flag, turning it into a confirmed offline whose later
|
||||
* recovery does toast. Absent/false in every non-boot snapshot.
|
||||
*/
|
||||
provisional?: boolean;
|
||||
}
|
||||
|
||||
/** NetInput is an event stamped with the wall-clock time it occurred at; only the debounce branch
|
||||
@@ -198,13 +206,20 @@ export function reduce(prev: NetSnapshot, ev: NetInput, cfg: NetConfig): NetResu
|
||||
switch (ev.type) {
|
||||
case 'callOk':
|
||||
case 'probeOk':
|
||||
// The probe (or a reconcile call) won — self-heal to online with a "back online" toast.
|
||||
return { next: onlineSnapshot(), effects: [{ kind: 'toast', toast: 'online' }] };
|
||||
// The probe (or a reconcile call) won — heal to online. A boot-assumed (provisional) offline
|
||||
// that is reachable on its very first probe was never really offline, so heal SILENTLY; a
|
||||
// confirmed offline gets the "back online" toast.
|
||||
return { next: onlineSnapshot(), effects: prev.provisional ? [] : [{ kind: 'toast', toast: 'online' }] };
|
||||
case 'callFailed':
|
||||
case 'probeFailed':
|
||||
// A failed probe confirms a boot-assumed offline as real (its eventual recovery will then
|
||||
// toast); a non-provisional offline is unchanged.
|
||||
return prev.provisional ? { next: { ...prev, provisional: false }, effects: [] } : { next: prev, effects: [] };
|
||||
case 'osOnline':
|
||||
// Trigger a probe; stay offline until it actually wins (a captive portal can report online).
|
||||
return { next: prev, effects: [{ kind: 'startProbe' }] };
|
||||
default:
|
||||
// callFailed / probeFailed / osOffline (still offline), versionRecommended (no nudge): no-op.
|
||||
// osOffline (still offline), versionRecommended (no nudge): no-op.
|
||||
return { next: prev, effects: [] };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user