perf: incremental count 최적화 (Priority 2)

- countCache 기반 5초 주기 O(n) 전체 카운트 → countRegistry 기반 incremental delta 카운트
- 변경된 선박만 카운트 증감 (mergeFeatures, deleteFeature, clearDarkSignals)
- fullRecount는 필터 변경/통합모드 전환 시에만 사용
- processTimeoutsThrottled 분리: 타임아웃 체크만 담당 (5초 주기)
- 미사용 parseAvetdr 함수 제거, throttle 설정 정리

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
HeungTak Lee 2026-01-30 13:23:26 +09:00
부모 ce54d9d0db
커밋 3b0190e3f3
3개의 변경된 파일238개의 추가작업 그리고 172개의 파일을 삭제

파일 보기

@ -108,22 +108,6 @@ export function rowToShipObject(row) {
}; };
} }
/**
* AVETDR 문자열 파싱
* @param {string} avetdr - "A_V_E_T_D_R" 형식 (0 또는 1)
* @returns {Object} 신호원 활성 상태
*/
export function parseAvetdr(avetdr) {
const parts = (avetdr || '0_0_0_0_0_0').split('_');
return {
A: parts[0] === '1', // AIS
V: parts[1] === '1', // VPASS
E: parts[2] === '1', // ENAV
T: parts[3] === '1', // VTS_AIS
D: parts[4] === '1', // D_MF_HF
R: parts[5] === '1', // RADAR
};
}
/** /**
* STOMP 연결 시작 * STOMP 연결 시작
@ -172,7 +156,7 @@ export function disconnectStomp() {
export function subscribeShips(onMessage) { export function subscribeShips(onMessage) {
// 환경변수로 쓰로틀 설정 (VITE_SHIP_THROTTLE: 0=실시간, 5/10/30/60=쓰로틀) // 환경변수로 쓰로틀 설정 (VITE_SHIP_THROTTLE: 0=실시간, 5/10/30/60=쓰로틀)
// const throttleSeconds = parseInt(import.meta.env.VITE_SHIP_THROTTLE || '0', 10); // const throttleSeconds = parseInt(import.meta.env.VITE_SHIP_THROTTLE || '0', 10);
const throttleSeconds = 60; const throttleSeconds = 0;
const topic = throttleSeconds > 0 const topic = throttleSeconds > 0
? `${STOMP_TOPICS.SHIP_THROTTLED}${throttleSeconds}s` ? `${STOMP_TOPICS.SHIP_THROTTLED}${throttleSeconds}s`

파일 보기

@ -1,15 +0,0 @@
import ReactDOM from 'react-dom/client';
import WrapComponent from './component/WrapComponent';
import './scss/WrapComponent.scss'
import './scss/HeaderComponent.scss'
import './scss/SideComponent.scss'
import './scss/MainComponent.scss'
import './scss/ToolComponent.scss'
import { BrowserRouter } from 'react-router-dom';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<BrowserRouter>
<WrapComponent />
</BrowserRouter>
);

파일 보기

@ -93,39 +93,150 @@ function isAnyEquipmentActive(ship) {
} }
// ===================== // =====================
// 카운트 쓰로틀링 상수 및 캐시 // 타임아웃 체크 쓰로틀 간격
// 참조: mda-react-front/src/common/deck.ts (271-331) // 참조: mda-react-front/src/common/deck.ts (271-331)
// ===================== // =====================
const LIVE_COUNT_THROTTLE_MS = 5000; // 5초 const TIMEOUT_CHECK_INTERVAL_MS = 5000; // 5초
/** // =====================
* 카운트 캐시 (스토어 외부에 저장) // Incremental 카운트 레지스트리 (스토어 외부)
* - counts: 마지막으로 계산된 종류별 카운트 // =====================
* - lastCalcTime: 마지막 계산 시간 //
* - lastFilterHash: 마지막 필터 해시 (필터 변경 감지용) // 기존 방식: 5초마다 features 전체 O(n) 순회하며 카운트 재계산
*/ // 최적화: 변경된 선박만 카운트 증감 O(batch), 5초 주기는 타임아웃 체크에만 사용
const countCache = { //
counts: null, // - categories: 각 featureId가 마지막으로 기여한 카운트 카테고리
lastCalcTime: 0, // 'dark' = 다크시그널 카운트, signalKindCode = 선종 카운트, null = 카운트 미포함
lastFilterHash: '', // - kindCounts: 선종별 누적 카운트 (incremental 유지)
// - darkSignalCount: 다크시그널 누적 카운트
// =====================
const countRegistry = {
categories: new Map(), // featureId → 'dark' | signalKindCode | null
kindCounts: { ...initialKindCounts },
darkSignalCount: 0,
totalCount: 0,
}; };
/** 마지막 타임아웃 체크 시간 */
let lastTimeoutCheckTime = 0;
/** /**
* 필터 해시 생성 (필터 변경 감지용) * 선박 1건의 카운트 카테고리 판정
* @param {Object} kindVisibility - 선박 종류별 표시 여부 * 처리 순서 (메인 프로젝트 동일):
* @param {Object} sourceVisibility - 신호원별 표시 여부 * darkSignalIds.has 'dark'
* @returns {string} 필터 해시 문자열 * 단독 레이더 null (카운트 제외)
* !isAnyEquipmentActive 'dark'
* isPriority 필터 + 선종/신호원/국적 필터 signalKindCode | null
*
* ②③④ 타임아웃 판정은 processTimeoutsThrottled에서 별도 처리
*
* @param {Object} ship - 선박 데이터
* @param {string} featureId
* @param {Set} darkSignalIds
* @param {boolean} isIntegrate
* @param {Object} kindVisibility
* @param {Object} sourceVisibility
* @param {Object} nationalVisibility
* @returns {string|null} 'dark' | signalKindCode | null
*/ */
function generateFilterHash(kindVisibility, sourceVisibility, nationalVisibility, darkSignalVisible) { function resolveShipCategory(ship, featureId, darkSignalIds, isIntegrate, kindVisibility, sourceVisibility, nationalVisibility) {
const kindKeys = Object.keys(kindVisibility).sort(); if (darkSignalIds.has(featureId)) return 'dark';
const sourceKeys = Object.keys(sourceVisibility).sort(); if (ship.signalSourceCode === SIGNAL_SOURCE_RADAR && !ship.integrate) return null;
const nationalKeys = Object.keys(nationalVisibility).sort(); if (!isAnyEquipmentActive(ship)) return 'dark';
if (isIntegrate && ship.integrate && !ship.isPriority) return null;
if (!kindVisibility[ship.signalKindCode]) return null;
if (!sourceVisibility[ship.signalSourceCode]) return null;
const mapped = mapNationalCode(ship.nationalCode);
if (!nationalVisibility[mapped]) return null;
return ship.signalKindCode || null;
}
const kindHash = kindKeys.map(k => kindVisibility[k] ? '1' : '0').join(''); /**
const sourceHash = sourceKeys.map(k => sourceVisibility[k] ? '1' : '0').join(''); * 단일 featureId의 카운트 증감 (delta)
const nationalHash = nationalKeys.map(k => nationalVisibility[k] ? '1' : '0').join(''); * @param {string} featureId
* @param {string|null} newCategory - 카테고리
*/
function updateSingleCount(featureId, newCategory) {
const oldCategory = countRegistry.categories.get(featureId) || null;
if (oldCategory === newCategory) return;
return `${kindHash}_${sourceHash}_${nationalHash}_${darkSignalVisible ? '1' : '0'}`; // 이전 카테고리 카운트 감소
if (oldCategory === 'dark') {
countRegistry.darkSignalCount--;
} else if (oldCategory && countRegistry.kindCounts[oldCategory] !== undefined) {
countRegistry.kindCounts[oldCategory]--;
}
// 새 카테고리 카운트 증가
if (newCategory === 'dark') {
countRegistry.darkSignalCount++;
} else if (newCategory && countRegistry.kindCounts[newCategory] !== undefined) {
countRegistry.kindCounts[newCategory]++;
}
if (newCategory) {
countRegistry.categories.set(featureId, newCategory);
} else {
countRegistry.categories.delete(featureId);
}
}
/**
* featureId를 카운트에서 제거
* @param {string} featureId
*/
function removeFromCount(featureId) {
updateSingleCount(featureId, null);
}
/**
* countRegistry의 totalCount 재계산
*/
function recalcTotal() {
countRegistry.totalCount = Object.values(countRegistry.kindCounts).reduce((a, b) => a + b, 0);
}
/**
* 전체 카운트 재계산 (필터 변경, 통합모드 전환 )
* targetId 중복 제거 포함
* @param {Map} features
* @param {Set} darkSignalIds
* @param {boolean} isIntegrate
* @param {Object} kindVisibility
* @param {Object} sourceVisibility
* @param {Object} nationalVisibility
*/
function fullRecount(features, darkSignalIds, isIntegrate, kindVisibility, sourceVisibility, nationalVisibility) {
// 레지스트리 초기화
countRegistry.categories.clear();
Object.keys(countRegistry.kindCounts).forEach(k => { countRegistry.kindCounts[k] = 0; });
countRegistry.darkSignalCount = 0;
const seenTargetIds = new Set();
features.forEach((ship, featureId) => {
let category = resolveShipCategory(ship, featureId, darkSignalIds, isIntegrate, kindVisibility, sourceVisibility, nationalVisibility);
// targetId 중복 제거 (다크시그널은 개별 카운트이므로 제외)
if (category && category !== 'dark' && ship.targetId) {
if (seenTargetIds.has(ship.targetId)) {
category = null;
} else {
seenTargetIds.add(ship.targetId);
}
}
if (category === 'dark') {
countRegistry.darkSignalCount++;
countRegistry.categories.set(featureId, category);
} else if (category && countRegistry.kindCounts[category] !== undefined) {
countRegistry.kindCounts[category]++;
countRegistry.categories.set(featureId, category);
}
// null → categories에 저장하지 않음
});
recalcTotal();
} }
/** /**
@ -280,13 +391,13 @@ const useShipStore = create(subscribeWithSelector((set, get) => ({
* @param {Array} ships - 선박 데이터 배열 * @param {Array} ships - 선박 데이터 배열
*/ */
mergeFeatures: (ships) => { mergeFeatures: (ships) => {
// ※ 성능 최적화: Map/Set을 직접 mutate하여 O(n) 전체 복사를 제거 // ※ 성능 최적화 #1: Map/Set을 직접 mutate (O(n) 전체 복사 제거)
// 기존: new Map(state.features) → O(5000) 복사 + O(batch) 변경 // ※ 성능 최적화 #2: Incremental count (변경된 선박만 카운트 증감)
// 최적화: state.features에 직접 set/delete → O(batch)만 발생
// Zustand 변경 감지는 featuresVersion 카운터로 트리거
const state = get(); const state = get();
const { features, darkSignalIds } = state; const { features, darkSignalIds, isIntegrate, kindVisibility, sourceVisibility, nationalVisibility } = state;
let darkChanged = false; let darkChanged = false;
const deletedIds = [];
const changedIds = [];
ships.forEach((ship) => { ships.forEach((ship) => {
const featureId = ship.featureId; const featureId = ship.featureId;
@ -303,88 +414,78 @@ const useShipStore = create(subscribeWithSelector((set, get) => ({
if (!ship.lost && !hasActive) { if (!ship.lost && !hasActive) {
features.delete(featureId); features.delete(featureId);
if (darkSignalIds.delete(featureId)) darkChanged = true; if (darkSignalIds.delete(featureId)) darkChanged = true;
deletedIds.push(featureId);
return; return;
} }
// 다크시그널 상태 판정 // 다크시그널 상태 판정
if (hasActive) { if (hasActive) {
// 장비 활성 → 다크시그널 해제 (회복)
if (darkSignalIds.delete(featureId)) darkChanged = true; if (darkSignalIds.delete(featureId)) darkChanged = true;
} else { } else {
// 모든 장비 비활성 (상태 전환용 신호)
// → 이미 다크시그널이면 유지, 아니면 등록
// → 상태 플래그(위치/시간 등)는 갱신하되 카운트에 반영하지 않음
if (!darkSignalIds.has(featureId)) { if (!darkSignalIds.has(featureId)) {
darkSignalIds.add(featureId); darkSignalIds.add(featureId);
darkChanged = true; darkChanged = true;
} }
} }
// receivedTimestamp 부여: 서버 수신시간 기반 (초기 로드 시 정확한 경과 시간 판단)
// 장비 비활성 신호도 상태 플래그(위치, 시간 등)는 항상 갱신
features.set(featureId, { ...ship, receivedTimestamp: parseReceivedTime(ship.receivedTime) }); features.set(featureId, { ...ship, receivedTimestamp: parseReceivedTime(ship.receivedTime) });
changedIds.push(featureId);
}); });
// 버전 카운터 증가로 구독자에게 변경 알림 // 버전 카운터 증가
set((s) => ({ set((s) => ({
featuresVersion: s.featuresVersion + 1, featuresVersion: s.featuresVersion + 1,
...(darkChanged ? { darkSignalVersion: s.darkSignalVersion + 1 } : {}), ...(darkChanged ? { darkSignalVersion: s.darkSignalVersion + 1 } : {}),
})); }));
// 카운트 재계산 (다크시그널 전환, 레이더 삭제도 여기서 처리) // Incremental count: 삭제된 선박은 카운트에서 제거, 변경된 선박은 카테고리 재판정
// 쓰로틀 카운트 업데이트 deletedIds.forEach(fid => removeFromCount(fid));
get().updateCountsThrottled(); changedIds.forEach(fid => {
const ship = features.get(fid);
if (!ship) return;
const category = resolveShipCategory(ship, fid, darkSignalIds, isIntegrate, kindVisibility, sourceVisibility, nationalVisibility);
updateSingleCount(fid, category);
});
recalcTotal();
set({
kindCounts: { ...countRegistry.kindCounts },
totalCount: countRegistry.totalCount,
darkSignalCount: countRegistry.darkSignalCount,
});
// 타임아웃 체크 (5초 주기)
get().processTimeoutsThrottled();
}, },
/** /**
* 쓰로틀 카운트 업데이트 * 타임아웃 체크 (5 주기)
* 5 이내에는 캐시된 사용, 필터 변경 즉시 재계산 * features 전체를 순회하여 타임아웃된 선박을 삭제/다크시그널 전환
* 참조: mda-react-front/src/common/deck.ts - updateLayerData() * 카운트는 영향받는 선박만 incremental 업데이트
*
* 처리 순서 (메인 프로젝트 동일):
* 레이더(000005)+비통합 timeout? delete
* LOST=0 + INSHORE timeout delete
* LOST=1 + OFFSHORE timeout darkSignal 전환
* !isAnyEquipmentActive darkSignal 전환
*/ */
updateCountsThrottled: () => { processTimeoutsThrottled: () => {
const state = get();
const { features, kindVisibility, sourceVisibility, nationalVisibility, isIntegrate, darkSignalVisible } = state;
const now = Date.now(); const now = Date.now();
const currentFilterHash = generateFilterHash(kindVisibility, sourceVisibility, nationalVisibility, darkSignalVisible) + `_${isIntegrate}`; if (now - lastTimeoutCheckTime < TIMEOUT_CHECK_INTERVAL_MS) return;
lastTimeoutCheckTime = now;
// 5초 이내이고 필터 변경 없으면 스킵 const state = get();
const elapsed = now - countCache.lastCalcTime; const { features, darkSignalIds } = state;
const filterChanged = currentFilterHash !== countCache.lastFilterHash; const newDarkIds = [];
const deleteIds = [];
if (elapsed < LIVE_COUNT_THROTTLE_MS && !filterChanged && countCache.counts) {
return; // 캐시 사용 (업데이트 스킵)
}
// 카운트 재계산 + 상태 전환 (다크시그널/레이더 삭제)
// 참조: mda-react-front/src/common/deck.ts - updateLayerData()
//
// 처리 순서 (메인 프로젝트 동일):
// ① darkSignalIds.has(featureId) → darkCount++, return
// ② 레이더(000005)+비통합 → timeout? delete : return
// ③ LOST=0 + INSHORE timeout → delete, return
// ④ LOST=1 + OFFSHORE timeout → darkSignal 전환, return
// ⑤ !isAnyEquipmentActive → darkSignal 전환, return
// ⑥ isPriority 필터 + 필터 + 카운트
const { darkSignalIds } = state;
const newKindCounts = { ...initialKindCounts };
let newDarkSignalCount = 0;
const seenTargetIds = new Set();
const newDarkIds = []; // 다크시그널 전환 대상 (④⑤)
const deleteIds = []; // 삭제 대상 (②③)
features.forEach((ship, featureId) => { features.forEach((ship, featureId) => {
// ① 이미 다크시그널 → 카운트만 (비용 최소: Set.has O(1)) // 이미 다크시그널 → 타임아웃 체크 불필요
if (darkSignalIds.has(featureId)) { if (darkSignalIds.has(featureId)) return;
newDarkSignalCount++;
return;
}
// ② 단독 레이더(비통합) → 타임아웃이면 삭제, 카운트 제외 // ② 단독 레이더(비통합) → 타임아웃이면 삭제
if (ship.signalSourceCode === SIGNAL_SOURCE_RADAR && !ship.integrate) { if (ship.signalSourceCode === SIGNAL_SOURCE_RADAR && !ship.integrate) {
if (now - ship.receivedTimestamp > RADAR_TIMEOUT_MS) { if (now - ship.receivedTimestamp > RADAR_TIMEOUT_MS) deleteIds.push(featureId);
deleteIds.push(featureId);
}
return; return;
} }
@ -399,76 +500,54 @@ const useShipStore = create(subscribeWithSelector((set, get) => ({
// ④ 영해밖 (LOST=1) + OFFSHORE 타임아웃 → 다크시그널 전환 // ④ 영해밖 (LOST=1) + OFFSHORE 타임아웃 → 다크시그널 전환
if (ship.lost && elapsed > OFFSHORE_TIMEOUT_MS) { if (ship.lost && elapsed > OFFSHORE_TIMEOUT_MS) {
newDarkIds.push(featureId); newDarkIds.push(featureId);
newDarkSignalCount++;
return; return;
} }
// ⑤ 모든 장비 비활성 → 즉시 다크시그널 전환 // ⑤ 모든 장비 비활성 → 즉시 다크시그널 전환
if (!isAnyEquipmentActive(ship)) { if (!isAnyEquipmentActive(ship)) {
newDarkIds.push(featureId); newDarkIds.push(featureId);
newDarkSignalCount++;
return;
}
// ⑥ 통합 모드: isPriority가 아니면 카운트에서 제외 (삭제/전환은 위에서 처리 완료)
if (isIntegrate && ship.integrate && !ship.isPriority) return;
// 필터 적용된 선박만 카운트
if (!kindVisibility[ship.signalKindCode]) return;
if (!sourceVisibility[ship.signalSourceCode]) return;
const mappedNational = mapNationalCode(ship.nationalCode);
if (!nationalVisibility[mappedNational]) return;
// targetId(통합ID) 기준 중복 제거
if (ship.targetId && seenTargetIds.has(ship.targetId)) return;
if (ship.targetId) seenTargetIds.add(ship.targetId);
if (ship.signalKindCode && newKindCounts[ship.signalKindCode] !== undefined) {
newKindCounts[ship.signalKindCode]++;
} }
}); });
const totalCount = Object.values(newKindCounts).reduce((sum, count) => sum + count, 0); if (newDarkIds.length === 0 && deleteIds.length === 0) return;
// 캐시 업데이트 // features/darkSignalIds 직접 mutate
countCache.counts = newKindCounts; newDarkIds.forEach(fid => darkSignalIds.add(fid));
countCache.lastCalcTime = now; deleteIds.forEach(fid => {
countCache.lastFilterHash = currentFilterHash;
// features/darkSignalIds 변경이 필요한 경우 (직접 mutate + 버전 카운터)
if (newDarkIds.length > 0 || deleteIds.length > 0) {
newDarkIds.forEach((fid) => darkSignalIds.add(fid));
deleteIds.forEach((fid) => {
features.delete(fid); features.delete(fid);
darkSignalIds.delete(fid); darkSignalIds.delete(fid);
}); });
// Incremental count: 영향받는 선박만 카운트 조정
deleteIds.forEach(fid => removeFromCount(fid));
newDarkIds.forEach(fid => updateSingleCount(fid, 'dark'));
recalcTotal();
set((s) => ({ set((s) => ({
featuresVersion: s.featuresVersion + 1, featuresVersion: s.featuresVersion + 1,
darkSignalVersion: s.darkSignalVersion + 1, darkSignalVersion: s.darkSignalVersion + 1,
kindCounts: newKindCounts, kindCounts: { ...countRegistry.kindCounts },
totalCount, totalCount: countRegistry.totalCount,
darkSignalCount: newDarkSignalCount, darkSignalCount: countRegistry.darkSignalCount,
})); }));
} else {
set({
kindCounts: newKindCounts,
totalCount,
darkSignalCount: newDarkSignalCount,
});
}
}, },
/** /**
* 카운트 즉시 재계산 (필터 변경 호출) * 카운트 전체 재계산 (필터 변경, 통합모드 전환 )
* targetId 중복 제거 포함 전체 O(n) 순회
*/ */
recalculateCounts: () => { recalculateCounts: () => {
// 캐시 무효화하여 즉시 재계산 const state = get();
countCache.lastCalcTime = 0; const { features, darkSignalIds, isIntegrate, kindVisibility, sourceVisibility, nationalVisibility } = state;
get().updateCountsThrottled(); fullRecount(features, darkSignalIds, isIntegrate, kindVisibility, sourceVisibility, nationalVisibility);
set({
kindCounts: { ...countRegistry.kindCounts },
totalCount: countRegistry.totalCount,
darkSignalCount: countRegistry.darkSignalCount,
});
}, },
/**
/** /**
* 단일 선박 추가/업데이트 * 단일 선박 추가/업데이트
* @param {Object} ship - 선박 데이터 * @param {Object} ship - 선박 데이터
@ -492,8 +571,14 @@ const useShipStore = create(subscribeWithSelector((set, get) => ({
selectedShipId: s.selectedShipId === featureId ? null : s.selectedShipId, selectedShipId: s.selectedShipId === featureId ? null : s.selectedShipId,
})); }));
// 쓰로틀 카운트 업데이트 // Incremental count: 삭제된 선박 카운트 제거
get().updateCountsThrottled(); removeFromCount(featureId);
recalcTotal();
set({
kindCounts: { ...countRegistry.kindCounts },
totalCount: countRegistry.totalCount,
darkSignalCount: countRegistry.darkSignalCount,
});
}, },
/** /**
@ -514,8 +599,14 @@ const useShipStore = create(subscribeWithSelector((set, get) => ({
selectedShipId: featureIds.includes(s.selectedShipId) ? null : s.selectedShipId, selectedShipId: featureIds.includes(s.selectedShipId) ? null : s.selectedShipId,
})); }));
// 쓰로틀 카운트 업데이트 // Incremental count: 삭제된 선박들 카운트 제거
get().updateCountsThrottled(); featureIds.forEach(fid => removeFromCount(fid));
recalcTotal();
set({
kindCounts: { ...countRegistry.kindCounts },
totalCount: countRegistry.totalCount,
darkSignalCount: countRegistry.darkSignalCount,
});
}, },
/** /**
@ -581,16 +672,21 @@ const useShipStore = create(subscribeWithSelector((set, get) => ({
*/ */
clearDarkSignals: () => { clearDarkSignals: () => {
const state = get(); const state = get();
// Incremental count: 각 다크시그널 선박 카운트 제거
state.darkSignalIds.forEach((fid) => { state.darkSignalIds.forEach((fid) => {
removeFromCount(fid);
state.features.delete(fid); state.features.delete(fid);
}); });
state.darkSignalIds.clear(); state.darkSignalIds.clear();
recalcTotal();
set((s) => ({ set((s) => ({
featuresVersion: s.featuresVersion + 1, featuresVersion: s.featuresVersion + 1,
darkSignalVersion: s.darkSignalVersion + 1, darkSignalVersion: s.darkSignalVersion + 1,
kindCounts: { ...countRegistry.kindCounts },
totalCount: countRegistry.totalCount,
darkSignalCount: countRegistry.darkSignalCount,
})); }));
get().recalculateCounts();
}, },
/** /**
@ -844,10 +940,11 @@ const useShipStore = create(subscribeWithSelector((set, get) => ({
* 모든 선박 데이터 초기화 * 모든 선박 데이터 초기화
*/ */
clearFeatures: () => { clearFeatures: () => {
// 캐시도 초기화 // countRegistry 초기화
countCache.counts = null; countRegistry.categories.clear();
countCache.lastCalcTime = 0; Object.keys(countRegistry.kindCounts).forEach(k => { countRegistry.kindCounts[k] = 0; });
countCache.lastFilterHash = ''; countRegistry.darkSignalCount = 0;
countRegistry.totalCount = 0;
set((s) => ({ set((s) => ({
features: new Map(), features: new Map(),