import 'dart:async'; /// 원본 구독 하나를 여러 화면이 나눠 쓰는 스트림. /// /// **왜 필요한가.** 이클립스 집계는 HOME 배너·GALAXY 배너·행성 HUD·방어 화면이 /// 동시에 본다. 화면마다 `snapshots()` 를 열면 같은 문서에 리스너가 화면 수만큼 /// 붙고, 화면이 겹쳐 뜨면(탭 위에 모달) 더 늘어난다. 실측에서 한 팬덤에 리스너가 /// 최대 7 건까지 붙었다. 읽기 비용이 그만큼 늘고, 값이 도착하는 순서도 갈린다. /// /// 여기서는 첫 구독자가 원본을 열고 마지막 구독자가 떠날 때 닫는다. 그 사이의 /// 구독자는 모두 같은 원본을 나눠 본다. /// /// **새 구독자는 마지막 값을 곧바로 받는다.** 그러지 않으면 나중에 열린 화면이 /// 다음 갱신이 올 때까지 빈 상태로 남는다 — 이클립스는 몇 분에 한 번 바뀌므로 /// 그 빈 상태가 오래간다. class SharedStream { SharedStream(this._open); /// 원본을 여는 함수. 실제로 구독자가 생겼을 때만 불린다. final Stream Function() _open; StreamSubscription? _sub; final _out = StreamController.broadcast(); T? _last; bool _hasLast = false; /// 구독자 수. 0 이 되면 원본을 닫는다. int _listeners = 0; Stream get stream => Stream.multi((controller) { _listeners += 1; _openIfNeeded(); // 늦게 붙은 구독자에게 현재 값을 먼저 준다. if (_hasLast) controller.add(_last as T); final sub = _out.stream.listen( controller.add, onError: controller.addError, onDone: controller.close, ); controller.onCancel = () async { await sub.cancel(); _listeners -= 1; if (_listeners == 0) await _close(); }; }); void _openIfNeeded() { if (_sub != null) return; _sub = _open().listen( (value) { _last = value; _hasLast = true; if (!_out.isClosed) _out.add(value); }, onError: (Object error, StackTrace stack) { if (!_out.isClosed) _out.addError(error, stack); }, ); } Future _close() async { await _sub?.cancel(); _sub = null; // 마지막 값은 버린다. 다시 열릴 때 옛 값을 먼저 주면 화면이 잠깐 과거를 그린다. _last = null; _hasLast = false; } /// 저장소가 버려질 때 부른다. Future dispose() async { await _close(); await _out.close(); } }