fix(frontend): initializeStores dedup + retryAllMessages guard + as any cleanup

- index.ts: add _storesInitialized guard to prevent triple initialization
- offlineStore.ts: add isRetrying mutex for retryAllMessages concurrency
- PropertyPanel.tsx: replace 13x (data as any) with typed d accessor
- chatStore.ts: replace window as any with Record<string, unknown>
- kernel-*.ts: replace prototype as any with Record<string, unknown>
- gateway-heartbeat.ts: delete dead code (9 as any, zero imports)
This commit is contained in:
iven
2026-04-05 01:06:48 +08:00
parent 13a40dbbf5
commit 82842c4258
12 changed files with 45 additions and 143 deletions

View File

@@ -354,8 +354,10 @@ if (import.meta.hot) {
// Dev-only: Expose stores to window for E2E testing
if (import.meta.env.DEV && typeof window !== 'undefined') {
(window as any).__ZCLAW_STORES__ = (window as any).__ZCLAW_STORES__ || {};
(window as any).__ZCLAW_STORES__.chat = useChatStore;
(window as any).__ZCLAW_STORES__.message = useMessageStore;
(window as any).__ZCLAW_STORES__.stream = useStreamStore;
const w = window as Record<string, unknown>;
w.__ZCLAW_STORES__ = (w.__ZCLAW_STORES__ as Record<string, unknown>) || {};
const stores = w.__ZCLAW_STORES__ as Record<string, unknown>;
stores.chat = useChatStore;
stores.message = useMessageStore;
stores.stream = useStreamStore;
}

View File

@@ -84,11 +84,16 @@ import { setConfigStoreClient } from './configStore';
import { setSecurityStoreClient } from './securityStore';
import { setSessionStoreClient } from './sessionStore';
let _storesInitialized = false;
/**
* Initialize all stores with the shared client.
* Called once when the application mounts.
* Guard ensures it only runs once even if called from multiple connection paths.
*/
export function initializeStores(): void {
if (_storesInitialized) return;
_storesInitialized = true;
const client = getClient();
// Inject client into all stores

View File

@@ -86,6 +86,7 @@ function generateMessageId(): string {
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
let healthCheckInterval: ReturnType<typeof setInterval> | null = null;
let isRetrying = false;
export const useOfflineStore = create<OfflineStore>()(
persist(
@@ -186,6 +187,12 @@ export const useOfflineStore = create<OfflineStore>()(
},
retryAllMessages: async () => {
if (isRetrying) {
log.debug('retryAllMessages already in progress, skipping');
return;
}
isRetrying = true;
try {
const state = get();
const pending = state.queuedMessages.filter(
(m) => m.status === 'pending' || m.status === 'failed'
@@ -230,6 +237,9 @@ export const useOfflineStore = create<OfflineStore>()(
log.warn(`Message ${msg.id} failed:`, errorMessage);
}
}
} finally {
isRetrying = false;
}
},
// === Reconnection ===