Skip to content

Fix multi ranges#179

Merged
5cript merged 9 commits intomainfrom
fix/multi-ranges
May 2, 2026
Merged

Fix multi ranges#179
5cript merged 9 commits intomainfrom
fix/multi-ranges

Conversation

@5cript
Copy link
Copy Markdown
Member

@5cript 5cript commented May 2, 2026

Fix multi ranges - PR changelog

Summary

Observed<T> now lets multiple consumers subscribe to the same container without stepping on each other. Previously, RangeEventContext was a producer-side singleton on the Observed, so two Nui::range(vec) subscribers shared one diff state and each consume-and-reset raced the other: only the first subscriber to render saw the update.

This PR moves RangeEventContext to be per-consumer, owned by each BasicObservedRenderer, and has the producer broadcast change events to all live readers.

Example:

#include <nui/frontend.hpp>

using namespace Nui;
using namespace Nui::Elements;
using namespace Nui::Attributes;

void example()
{
    Observed<std::vector<std::string>> items{{"alpha", "beta", "gamma"}};

    auto renderItem = [](long long, auto const& s) -> ElementRenderer {
        return li{}(s);
    };

    // Two views over the same Observed:
    //   - a sidebar list
    //   - a footer count + repeated list
    //
    // Before this PR, only the first range() to render would receive
    // updates; the second silently fell behind because both shared the
    // Observed's single internal RangeEventContext.
    //
    // After this PR, both stay in sync.
    return body{}(
        div{class_ = "sidebar"}(
            ul{}(range(items), renderItem)
        ),
        div{class_ = "footer"}(
            span{}([&items]() { return std::to_string(items.size()) + " items"; }),
            ul{}(range(items), renderItem)   // <-- same Observed, second subscriber
        )
    );

    // items.push_back("delta");           // both lists grow
    // items[0] = "alpha-renamed";         // both lists update at index 0
    // items.erase(items.begin() + 1);     // both lists shrink
}

Breaking changes

  • ObservedContainer::rangeContext() (both overloads) removed — the range event context is now an implementation detail of range rendering, not a property of the observed value.
  • ObservedContainer constructors taking RangeEventContext&& (4 overloads) removed.

These were unused outside the test suite; if you somehow held one, replace with the default constructor.
Because of that I consider this change not major version increasing, because its likely 0 impact.

Behavior changes

  • Multiple Nui::range(observed) subscribers over the same Observed<vector|deque|string|set|list> now all stay in sync across mutations. Previously the second-and-later subscribers would silently fall behind.
  • Observed<set> and Observed<list> automatically opt their attached reader contexts into disableOptimizations=true, since their non-random-access iteration model can't replay the optimized diff path.

New API

  • ObservedContainer::attachReaderContext(std::shared_ptr<RangeEventContext> const&): called by range renderers during attachment.
  • ObservedContainer::readerContextCount(): raw size of the reader vector (debug/test helper).
  • RangeEventContext::setDisableOptimizations(bool): replaces the removed boolean constructor.

Tests

  • ~24 new MultiSubscriber_* tests covering every mutating op (push/pop front+back, emplace, insert ×4 overloads, erase ×2, resize ×3, indexed-assign, iterator-deref-assign, swap, assign, full reassignment, clear+repopulate).
  • 8 lifecycle/stress tests: SubscriberLifetime_Detach, SubscriberLifetime_DiesMidBroadcast, FiveSubscribers_RandomMutations, InteractionAcrossSubscribers, OperatorAssignBetweenContainers, Map_Set_Deque_MultiSubscriber, MoveAssignedObservedHasNoSubscribers, RepeatedIndexedAssignAtSamePosition.

Test mock fixes

  • replaceWith now actually replaces self's entry in the parent's children / childNodes arrays (was previously redirecting self's shared ReferenceType, which only worked for the first replace at any DOM position; subsequent replaces became invisible to the parent).
  • replaceWith's no-parent fallback (used for root setBody) also rebinds document.body to the new value when self is the current body, so chained body replacements within a single test no longer leave document.body pointing through a stale reference chain.

Dependencies

  • Bumped interval-tree to the latest pinned commit.

Test results

405 passing, 0 disabled

@5cript 5cript merged commit e1cd217 into main May 2, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant