stateChange(stateObj, stateUrlHash) {
        if (!stateUrlHash.startsWith('#')) {
            throw new DetailedError("Expected a hash URL.", {stateObj, stateUrlHash});
        }
        if (this._currentMemorableStateObj === stateObj) {
            return;
        }
        if (this._historyActionsNotWorking) {
            // This is worse than using the history API, since it inserts junk after every state change, but it's also
            // better than just randomly losing the circuit.
            document.location.hash = stateUrlHash;
            return;
        }

        try {
            // 'Memorable' states should stay in the history instead of being replaced.
            if (this._currentMemorableStateObj === undefined) {
                history.replaceState(stateObj, "", stateUrlHash);
            } else {
                history.pushState(stateObj, "", stateUrlHash);
                this._currentMemorableStateObj = undefined;
            }
        } catch (ex) {
            // E.g. this happens when running from the filesystem due to same-origin constraints.
            console.warn(
                "Calling 'history.replaceState/pushState' failed. Falling back to setting location.hash.",
                ex);
            this._historyActionsNotWorking = true;
            document.location.hash = stateUrlHash;
        }
    }
@param {*} stateObj An equatable (by ===) object representing the latest state.
@param {!string} stateUrlHash A document.location.hash value that will lead to the latest state.