From 4ae3646700c5694a02d6891692c70593b1137e29 Mon Sep 17 00:00:00 2001 From: jason5ng32 Date: Sat, 22 Aug 2026 13:32:57 +0800 Subject: [PATCH 01/11] Feat(core): add the app command bus and route cross-component triggers through it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Events keep saying "this happened"; commands say "do this" — single owner, dispatch resolves when the work finishes with the owner's result, reject codes unavailable/timeout (bus) and auth/quota/input (owners, via appCommandError). Homepage sections register ipinfo:refresh / connectivity:run / webrtc:run / dnsleak:run / speedtest:toggle; keyboard shortcuts, the refresh orchestrator, and Persona Check's dependency repair dispatch instead of reaching through template refs, and Home.vue sheds the test-section refs. Co-Authored-By: Claude Fable 5 --- frontend/components/ConnectivityTest.vue | 15 +- frontend/components/DnsLeaksTest.vue | 16 +- frontend/components/Home.vue | 22 +-- frontend/components/IpInfos.vue | 23 ++- frontend/components/SpeedTest.vue | 8 +- frontend/components/WebRtcTest.vue | 17 +- .../advanced-tools/PersonaCheck.vue | 49 ++--- frontend/composables/use-app-command.js | 13 ++ .../composables/use-refresh-orchestrator.js | 35 ++-- frontend/composables/use-shortcuts.js | 31 +-- frontend/utils/app-commands.js | 95 ++++++++++ frontend/utils/app-events.js | 22 +++ tests/app-commands.test.js | 177 ++++++++++++++++++ tests/app-events.test.js | 33 +++- tests/composable-refresh-orchestrator.test.js | 80 +++++--- tests/composable-shortcuts.test.js | 55 ++++-- 16 files changed, 562 insertions(+), 129 deletions(-) create mode 100644 frontend/composables/use-app-command.js create mode 100644 frontend/utils/app-commands.js create mode 100644 tests/app-commands.test.js diff --git a/frontend/components/ConnectivityTest.vue b/frontend/components/ConnectivityTest.vue index c69bf92e8..35a591ba9 100644 --- a/frontend/components/ConnectivityTest.vue +++ b/frontend/components/ConnectivityTest.vue @@ -106,6 +106,7 @@ + @@ -114,7 +115,8 @@ import { ref, computed, onMounted, onBeforeUnmount, reactive, watch } from 'vue' import { useMainStore } from '@/store'; import { useI18n } from 'vue-i18n'; import { trackEvent } from '@/utils/analytics'; -import { emitAppEvent } from '@/utils/app-events'; +import { emitAppEvent, waitForAppEvent } from '@/utils/app-events'; +import { useAppCommand } from '@/composables/use-app-command.js'; import { CONNECTIVITY_STATUS } from '@/utils/report-schema.js'; import { TILE_PREVIEW, faviconPath } from '@/data/connectivity-import-lists.js'; import { removeTarget } from '@/utils/connectivity-import.js'; @@ -512,6 +514,15 @@ const handelCheckStart = async (trigger = 'boot') => { } }; +// Command owner: run the connectivity pass. `trigger` keeps handelCheckStart's +// toast / card-reset semantics; resolves with the next connectivity:finished +// snapshot (the first pass in multi-round mode). +useAppCommand('connectivity:run', ({ trigger = 'manual' } = {}) => { + const finished = waitForAppEvent('connectivity:finished'); + handelCheckStart(trigger); + return finished; +}); + onMounted(() => { store.setMountingStatus('Connectivity', true); }); @@ -528,6 +539,4 @@ onBeforeUnmount(() => { // Either signal flipping fires sendAlert; the gates inside pick the winner. watch(() => store.allHasLoaded, (v) => { if (v) sendAlert(); }); watch(allRoundsDone, (v) => { if (v) sendAlert(); }); - -defineExpose({ handelCheckStart }); diff --git a/frontend/components/DnsLeaksTest.vue b/frontend/components/DnsLeaksTest.vue index 573c1e694..5a50ba106 100644 --- a/frontend/components/DnsLeaksTest.vue +++ b/frontend/components/DnsLeaksTest.vue @@ -98,7 +98,8 @@ import { useRouter } from 'vue-router'; import { useMainStore } from '@/store'; import { useI18n } from 'vue-i18n'; import { trackEvent } from '@/utils/analytics'; -import { emitAppEvent } from '@/utils/app-events'; +import { emitAppEvent, waitForAppEvent } from '@/utils/app-events'; +import { useAppCommand } from '@/composables/use-app-command.js'; import { JnTooltip } from '@/components/ui/tooltip'; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; @@ -252,12 +253,15 @@ const checkAllDNSLeakTest = async (isRefresh) => { }); }; -onMounted(() => { - store.setMountingStatus('DNSLeakTest', true); +// Command owner: run all leak providers. Resolves with the next +// dnsleak:finished snapshot. +useAppCommand('dnsleak:run', ({ isRefresh = false } = {}) => { + const finished = waitForAppEvent('dnsleak:finished'); + checkAllDNSLeakTest(isRefresh); + return finished; }); -defineExpose({ - checkAllDNSLeakTest, - leakTest, +onMounted(() => { + store.setMountingStatus('DNSLeakTest', true); }); diff --git a/frontend/components/Home.vue b/frontend/components/Home.vue index 106501419..43c188ca8 100644 --- a/frontend/components/Home.vue +++ b/frontend/components/Home.vue @@ -5,11 +5,11 @@
- - - - - + + + + +
@@ -75,18 +75,14 @@ const store = useMainStore(); const configs = computed(() => store.configs); const userPreferences = computed(() => store.userPreferences); -// Template refs +// Template refs — UI chrome only; the test sections are reached through the +// command bus (utils/app-commands.js), not refs. const userRef = ref(null); const achievementsRef = ref(null); const queryIPRef = ref(null); const helpModalRef = ref(null); const shareReportRef = ref(null); -const speedTestRef = ref(null); const advancedToolsRef = ref(null); -const IPCheckRef = ref(null); -const connectivityRef = ref(null); -const webRTCRef = ref(null); -const dnsLeaksRef = ref(null); // Info mask const { infoMaskLevel, isInfosLoaded, showMaskButton, toggleInfoMask } = useInfoMask({ @@ -96,7 +92,6 @@ const { infoMaskLevel, isInfosLoaded, showMaskButton, toggleInfoMask } = useInfo // Refresh / initial load sequence const { loadingControl } = useRefreshOrchestrator({ - refs: { IPCheckRef, connectivityRef, webRTCRef, dnsLeaksRef }, store, t, userPreferences, @@ -106,8 +101,7 @@ const { loadingControl } = useRefreshOrchestrator({ // Shortcuts const { loadShortcuts } = useShortcuts({ refs: { - queryIPRef, helpModalRef, shareReportRef, - speedTestRef, advancedToolsRef, IPCheckRef, connectivityRef, webRTCRef, dnsLeaksRef, + queryIPRef, helpModalRef, shareReportRef, advancedToolsRef, isInfosLoaded, toggleInfoMask, }, store, t, configs, userPreferences, diff --git a/frontend/components/IpInfos.vue b/frontend/components/IpInfos.vue index 25267ebb7..e07c92b6a 100644 --- a/frontend/components/IpInfos.vue +++ b/frontend/components/IpInfos.vue @@ -22,6 +22,7 @@ :asnConnectivityInfos="asnConnectivityInfos" @refresh-card="refreshCard" /> + @@ -34,7 +35,8 @@ import { trackEvent } from '@/utils/analytics'; import { isUsablePublicIP } from '@/utils/valid-ip.js'; import { transformDataFromIPapi } from '@/utils/transform-ip-data.js'; import { getIPFromIPIP, getIPFromCloudflare_V4, getIPFromCloudflare_V6, getIPFromIPChecking64, getIPFromIPChecking4, getIPFromIPChecking6 } from '@/utils/getips'; -import { emitAppEvent } from '@/utils/app-events'; +import { emitAppEvent, waitForAppEvent } from '@/utils/app-events'; +import { useAppCommand } from '@/composables/use-app-command.js'; import { authenticatedFetch, fetchErrorLabel } from '@/utils/authenticated-fetch'; import IPCard from './ip-infos/IPCard.vue'; @@ -430,14 +432,21 @@ watch(IPArray, () => { store.updateAllIPs(IPArray.value); }); -onMounted(() => { - store.setMountingStatus('IPInfo', true); +// Command owner: refresh one card ({ index }) or the whole grid. Resolves +// with the next ipinfo:finished snapshot — the grid re-emits it whenever a +// card settles, single-card refreshes included. +useAppCommand('ipinfo:refresh', ({ index } = {}) => { + const finished = waitForAppEvent('ipinfo:finished'); + if (Number.isInteger(index) && ipDataCards[index]) { + refreshCard(ipDataCards[index], index); + } else { + checkAllIPs(); + } + return finished; }); -defineExpose({ - checkAllIPs, - ipDataCards, - refreshCard, +onMounted(() => { + store.setMountingStatus('IPInfo', true); }); diff --git a/frontend/components/SpeedTest.vue b/frontend/components/SpeedTest.vue index 26549c6fe..adb953985 100644 --- a/frontend/components/SpeedTest.vue +++ b/frontend/components/SpeedTest.vue @@ -165,6 +165,7 @@ + @@ -174,6 +175,7 @@ import { useMainStore } from '@/store'; import { useI18n } from 'vue-i18n'; import { trackEvent } from '@/utils/analytics'; import { emitAppEvent } from '@/utils/app-events.js'; +import { useAppCommand } from '@/composables/use-app-command.js'; import { fetchWithTimeout } from '@/utils/fetch-with-timeout.js'; import { isValidIP } from '@/utils/valid-ip.js'; import { parseTrace } from '@/utils/parse-trace.js'; @@ -529,6 +531,10 @@ const speedTestController = async () => { } }; +// Command owner: run / pause / resume toggle, same semantics as the section's +// own button. Resolves when the toggle applies, not when the test finishes. +useAppCommand('speedtest:toggle', () => speedTestController()); + // --- Lifecycle ---------------------------------------------------------- onMounted(() => { store.setMountingStatus('SpeedTest', true); }); @@ -546,8 +552,6 @@ onUnmounted(() => { } destroyCharts(); }); - -defineExpose({ speedTestController });