Bug? - ActionListener and ActionBroadcaster asserts on program exit

Even though I’m careful to call removeActionListener for every time I call addActionListener.

This is on a Windows 11 64-bit executable in debug mode using Juce version 7.5. It works in release mode, but I don’t really trust it.

The assertion I’m getting is as follows:

ActionBroadcaster::~ActionBroadcaster()
{
    // all event-based objects must be deleted BEFORE juce is shut down!
    JUCE_ASSERT_MESSAGE_MANAGER_EXISTS
}

Am I doing something wrong or is it a bug?

Here’s the minimum code to reproduce the problem:

GlobalState.h

#pragma once

#include <JuceHeader.h>

//  ========================================================================================================================
/*
*   Implements a singleton that provides a place for global variables and optionally, an event to notify other
*	components of changes.
*/
class GlobalState : public juce::ActionBroadcaster
{
public:
    static GlobalState& Instance()
    {
        static GlobalState S;
        return S;
    }

    int getCurrentBeat() const;

    void setCurrentBeat(int CurrentBeat);

    int getLoopCount();

private:
    GlobalState();
    ~GlobalState();

    //  ========================================================================================================================

    int				currentBeat{ 4 };			// The current beat of the metronome.
    int             loopCount{ 0 };
};

GlobalState.cpp

#include "GlobalState.h"

GlobalState::GlobalState()
{

}

GlobalState::~GlobalState()
{

}

int GlobalState::getCurrentBeat() const
{
    return currentBeat;
}

void GlobalState::setCurrentBeat(int CurrentBeat)
{
    currentBeat = CurrentBeat;
    sendActionMessage("GlobalState:DownBeat");
}

int GlobalState::getLoopCount()
{
	return loopCount;
}

MainComponent.h

#pragma once

#include <JuceHeader.h>
#include "GlobalState.h"

//==============================================================================
/*
    This component lives inside our window, and this is where you should put all
    your controls and content.
*/
class MainComponent  : public juce::Component, public juce::ActionListener
{
public:
    //==============================================================================
    MainComponent();
    ~MainComponent() override;

    //==============================================================================
    void paint (juce::Graphics&) override;
    void resized() override;

    void actionListenerCallback(const juce::String& message) override;

private:

    //==============================================================================
    GlobalState& globalState = GlobalState::Instance();

    juce::TextButton buttonQuit{ "Quit" };

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
};

MainComponen.cpp

#include "MainComponent.h"

//==============================================================================
MainComponent::MainComponent()
{

    addAndMakeVisible(buttonQuit);
    buttonQuit.onClick = [this] { juce::JUCEApplication::quit(); };

    globalState.addActionListener(this);
    setSize (600, 400);
}

MainComponent::~MainComponent()
{
    globalState.removeActionListener(this);
}

//==============================================================================
void MainComponent::paint (juce::Graphics& g)
{
    // (Our component is opaque, so we must completely fill the background with a solid colour)
    g.fillAll (getLookAndFeel().findColour (juce::ResizableWindow::backgroundColourId));

    g.setFont (juce::Font (16.0f));
    g.setColour (juce::Colours::white);
    g.drawText ("Hello World!", getLocalBounds(), juce::Justification::centred, true);
}

void MainComponent::resized()
{
    buttonQuit.setBounds(getLocalBounds().getWidth() / 2, 8, 60, 30);
}

void MainComponent::actionListenerCallback(const juce::String& message)
{
    if (message == "GlobalState:DownBeat")
        DBG("MainComponent::actionListenerCallback");
}