import 'dart:async'; /// 服务端返回的开关权威终态(LikeState / BookmarkState 的统一投影)。 class ToggleOutcome { const ToggleOutcome({required this.active, required this.count}); final bool active; final int count; } /// 内存副本当前读数(乐观翻转与回滚校验的基准)。 class ToggleReading { const ToggleReading({required this.active, required this.count}); final bool active; final int count; } /// 点赞 / 收藏共用的乐观更新小状态机(03 号评估 §3 定稿的数据层部分): /// **乐观翻转 + 快照回滚 + 单飞合并意图 + 代次守卫**。字段读写与端点 /// 全部参数化,like / bookmark 各持一实例,不复制两份逻辑。 /// /// 对每个 id 的一轮「操作链」: /// 1. 点击立即经 [write] 翻转内存副本(UI 同帧反馈由持有方 notify); /// 2. 非在途则记快照、发请求(PUT/DELETE 语义幂等,重放安全); /// 在途则只把新意图并入 pendingTarget,**不发新请求**(单飞); /// 3. 成功:pendingTarget 与已确认态不一致 → 以 pendingTarget 补发一次 /// (连续快速点击至多两个在途请求,中间抖动全被合并);一致 → 用服务端 /// 权威计数覆盖乐观计数(吸收他人并发造成的偏差),清状态; /// 4. 失败:恢复链起点快照(先校验 id 仍可读且当前态仍是本轮乐观目标, /// 避免覆盖新数据),经 [onError] 轻提示,**不自动重试**; /// 5. 代次守卫:响应到达时 [generation] 与链起点不符(期间发生过刷新, /// 列表已被服务端数据整体替换)→ 丢弃该响应,不覆盖不回滚。 class ToggleSync { ToggleSync({ required this._read, required this._write, required this._send, required this._generation, this._onError, }); final ToggleReading? Function(String id) _read; final void Function(String id, bool active, int count) _write; final Future Function(String id, bool target) _send; final int Function() _generation; final void Function(String id, Object error)? _onError; final Map _chains = {}; /// 该 id 是否有请求在途(测试与调试观测口)。 bool isInFlight(String id) => _chains.containsKey(id); /// 翻转一次。同步完成乐观写入;网络往返在后台收敛,不外抛。 void toggle(String id) { final current = _read(id); if (current == null) return; // 已不在列表(刷新剔除),本次点击作废。 final target = !current.active; final optimisticCount = target ? current.count + 1 : (current.count - 1 < 0 ? 0 : current.count - 1); _write(id, target, optimisticCount); final chain = _chains[id]; if (chain != null) { chain.pendingTarget = target; return; } final started = _ToggleChain( snapshot: current, generation: _generation(), target: target, ); _chains[id] = started; unawaited(_run(id, started)); } /// 登出 / 整体刷新清态:丢弃全部链(在途响应因代次或链失配被丢弃)。 void reset() => _chains.clear(); Future _run(String id, _ToggleChain chain) async { while (true) { ToggleOutcome outcome; try { outcome = await _send(id, chain.target); } catch (error) { if (_chains[id] == chain && _generation() == chain.generation) { final current = _read(id); final lastTarget = chain.pendingTarget ?? chain.target; // 回滚前校验:id 仍可读且当前态仍是本轮乐观写入的目标态。 if (current != null && current.active == lastTarget) { _write(id, chain.snapshot.active, chain.snapshot.count); } _onError?.call(id, error); } _release(id, chain); return; } if (_chains[id] != chain || _generation() != chain.generation) { _release(id, chain); return; // 代次不符 / 已被 reset:丢弃响应,不覆盖不回滚。 } final pending = chain.pendingTarget; if (pending != null && pending != outcome.active) { chain.target = pending; chain.pendingTarget = null; continue; // 以最终意图补发一次。 } _write(id, outcome.active, outcome.count); _release(id, chain); return; } } void _release(String id, _ToggleChain chain) { if (_chains[id] == chain) _chains.remove(id); } } /// 一轮操作链的在途状态。 class _ToggleChain { _ToggleChain({ required this.snapshot, required this.generation, required this.target, }); /// 链起点快照(回滚基准)。 final ToggleReading snapshot; /// 链起点的刷新代次。 final int generation; /// 当前在途请求的目标态。 bool target; /// 在途期间用户新点出的最终意图(完成后据此决定是否补发)。 bool? pendingTarget; }