Skip to content

fix: keep short-circuited bindings subscribed so they can update again#7649

Open
AKnassa wants to merge 1 commit into
microsoft:mainfrom
AKnassa:fix/7035-shortcircuit-zero-dep-binding
Open

fix: keep short-circuited bindings subscribed so they can update again#7649
AKnassa wants to merge 1 commit into
microsoft:mainfrom
AKnassa:fix/7035-shortcircuit-zero-dep-binding

Conversation

@AKnassa

@AKnassa AKnassa commented Jul 13, 2026

Copy link
Copy Markdown

What this does

Fixes a bug where a piece of your UI could quietly stop responding to data — forever — just because of the order you wrote two conditions in.

Why

Consider a condition like "show this if the feature is on and we have a value":

when(x => x.observableValue && x.plainValue, html`...`)   // works
when(x => x.plainValue && x.observableValue, html`...`)   // silently dead

Both read the same, but only the first one keeps working. JavaScript stops evaluating an && as soon as it finds something falsy, so in the second version the observable value is never actually read. FAST tracks what a binding reads, so nothing gets tracked, the binding ends up watching nothing at all, and once that happens it can never wake up again.

It is worse than a binding that never works: a binding can start out healthy and go permanently dead the first time the guard is false. There is no error and no warning. Reversing two words in a condition should not silently break your app.

What changed

  • A binding that finishes an evaluation having read no observable data now watches its source object as a whole, so a later change still wakes it. This uses a subscription path that already existed and was simply unused.
  • repeat binds each item, and items can be plain strings or numbers, so the fallback is guarded to only apply where it is safe.
  • A binding that observes nothing no longer reports a phantom dependency record.
  • when's handling of a value that arrives while unbound is now covered too.

Scope

This rescues the "read nothing at all" case, which is the one that dies permanently. A binding that reads some of its dependencies (x => x.tracked && x.getter && x.other) still tracks only what it read on the last pass — that is existing, intended behavior and is unchanged.

How to see it

Run the fast-element test suite. The new tests in observation/observable.pw.spec.ts and templating/when.pw.spec.ts assert that both orderings of && produce identical update counts. They fail on main and pass here.

Performance was measured, since this touches the code every binding in every FAST app runs through. Benchmarks (basic, when, repeat, in both client-render and hydration) were run interleaved against main across multiple rounds: all differences fell inside the harness's own round-to-round noise. The added cost on the common path is a single field read and comparison, which short-circuits before any new work.

Fixes #7035

A binding whose expression short-circuits before reading any observable
(for example `x => x.plainValue && x.observable`) collected no dependencies
and could therefore never be re-evaluated, leaving it permanently dead.

Volatile bindings that end an evaluation with no dependencies now subscribe
to the source itself, via the existing no-property path on PropertyChangeNotifier,
so a later change still wakes them. Guarded so non-object sources (repeat binds
primitives) cannot reach the WeakMap.

Fixes microsoft#7035
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@AKnassa AKnassa marked this pull request as ready for review July 14, 2026 02:28
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

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.

fix: inconsistent observable behavior when used with getter

1 participant