fix(client): 自动连接竞态真因 — 等设置加载完再判(1.0.22)
日志实证:节点就绪(~450ms)与设置加载几乎同时完成,节点回调早 1ms 读到还没加载完的 autoConnect=false → 不连。AppSettings 加 loaded 标志(_load 完成置 true,失败也置); _maybeAutoConnect 同时监听 nodes + settings,二者都就绪(settings.loaded && 节点 uuid 非空) 才决策一次。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+25
-20
@@ -121,34 +121,39 @@ class _RootFlowState extends ConsumerState<RootFlow> {
|
||||
bool _autoConnectArmed = false; // 自动连接每进程只尝试一次
|
||||
bool _homeLogged = false; // 仅日志:HomeShell 首次显示打一行
|
||||
|
||||
// 进入登录态主界面后:若开了「自动连接」且当前未连接,等节点就绪后自动连一次。
|
||||
// 进入登录态主界面后:开了「自动连接」且当前未连接,就自动连一次。
|
||||
// 关键:必须同时等「设置已从持久化加载完(settings.loaded)」与「节点已就绪」——
|
||||
// 二者都是异步且完成时刻接近,早读 autoConnect 会撞上还没加载完的默认 false(实测差 1ms)。
|
||||
// 同时监听两者,任一变化就重判,直到都就绪再决策(只决策一次)。
|
||||
void _maybeAutoConnect() {
|
||||
if (_autoConnectArmed) return;
|
||||
_autoConnectArmed = true;
|
||||
logLine('AutoConnect', 'armed; waiting for nodes ready');
|
||||
// 注意:不在此处早读 settingsProvider.autoConnect —— 其 _load 是异步(SharedPreferences),
|
||||
// 启动这一刻多半还没加载完,会读到默认 false 而永久跳过。改到「节点就绪」回调里再判:
|
||||
// 节点要走网络拉取,远慢于本地设置读取,那时 autoConnect 必已加载到位。
|
||||
// 不用回调的 next(裸 ProviderSubscription 会让它退化成 dynamic,valueOrNull 这种
|
||||
// 扩展 getter 在 dynamic 上不解析 → 运行期 NoSuchMethod)。直接 ref.read 拿强类型值。
|
||||
late final ProviderSubscription<AsyncValue<List<Node>>> sub;
|
||||
sub = ref.listenManual(nodesProvider, (prev, next) {
|
||||
final av = ref.read(nodesProvider);
|
||||
final nodes = av.valueOrNull ?? const <Node>[];
|
||||
final ready = nodes.any((n) => n.uuid.isNotEmpty);
|
||||
logLine('AutoConnect', 'nodes update: total=${nodes.length} ready=$ready loading=${av.isLoading} err=${av.hasError}');
|
||||
if (!ready) return;
|
||||
sub.close();
|
||||
final ac = ref.read(settingsProvider).autoConnect;
|
||||
logLine('AutoConnect', 'armed; waiting for settings loaded + nodes ready');
|
||||
ProviderSubscription<AsyncValue<List<Node>>>? nodeSub;
|
||||
ProviderSubscription<AppSettings>? settingsSub;
|
||||
var decided = false;
|
||||
void tryGo() {
|
||||
if (decided) return;
|
||||
final settings = ref.read(settingsProvider);
|
||||
final nodes = ref.read(nodesProvider).valueOrNull ?? const <Node>[];
|
||||
final nodesReady = nodes.any((n) => n.uuid.isNotEmpty);
|
||||
if (!settings.loaded || !nodesReady) return; // 等两者都就绪
|
||||
decided = true;
|
||||
nodeSub?.close();
|
||||
settingsSub?.close();
|
||||
final phase = ref.read(connectionProvider).phase;
|
||||
logLine('AutoConnect', 'nodes ready → autoConnect=$ac phase=$phase');
|
||||
if (ac && phase == VpnPhase.off) {
|
||||
logLine('AutoConnect', 'ready: autoConnect=${settings.autoConnect} phase=$phase nodes=${nodes.length}');
|
||||
if (settings.autoConnect && phase == VpnPhase.off) {
|
||||
logLine('AutoConnect', 'triggering connect');
|
||||
ref.read(connectionProvider.notifier).toggle();
|
||||
} else {
|
||||
logLine('AutoConnect', 'NOT connecting (autoConnect=$ac phase=$phase)');
|
||||
logLine('AutoConnect', 'NOT connecting (autoConnect=${settings.autoConnect} phase=$phase)');
|
||||
}
|
||||
}, fireImmediately: true);
|
||||
}
|
||||
|
||||
nodeSub = ref.listenManual(nodesProvider, (_, __) => tryGo());
|
||||
settingsSub = ref.listenManual(settingsProvider, (_, __) => tryGo());
|
||||
tryGo();
|
||||
}
|
||||
|
||||
// 登录/注册成功后:未看过引导才进引导,否则直接主界面。
|
||||
|
||||
Reference in New Issue
Block a user