Class FieldReconciler

java.lang.Object
vaadinx.FieldReconciler

public final class FieldReconciler extends Object
Detects and repairs direct writes to the JDK-shaped protected fields the emulators carry per D_instance_field_surface — slider.sliderModel = m, this.orientation = VERTICAL in a migrated subclass — which bypass the setter and therefore never reach the Vaadin peer.

Why this exists at all: in real Swing the JDK's method bodies read those fields at use time, so a direct write mostly takes effect at the next repaint (minus the listener rewiring the setter would have done — the idiom is partially broken on the desktop too). The emulator's peer has no repaint that re-reads fields, so without detection a direct write would do nothing, which is worse than the JDK. The reconcile maps "next repaint" to SB-Emulators' equivalents:

  • Steady state: EHelper.callSwing(Runnable) reconciles the session's registered emulators in its epilogue (outermost envelope only) — every peer→Swing event, Timer fire, SwingWorker callback and post-park continuation funnels through callSwing (R_callswing_envelope), so the write is repaired inside the very roundtrip that made it, before the response flushes.
  • Bootstrap: register(FieldReconciler.Reconcilable) arms a one-shot beforeClientResponse on the current UI, catching field writes made while the app builds its UI during navigation attach, before any callSwing has run. Never re-armed from inside the callback: Flow runs registrations made during the flush in the same pass (StateTree loops to a fixpoint), so a self-rearming callback livelocks — measured 2026-08-31, see D_field_write_reconcile.

A detected write logs a loud ERROR (not the EHelper.onUnimplemented(String, String, Object...) WARN lane — this is not an SB-Emulators coverage gap but an app antipattern that causes stale data on the desktop as well) and then repairs by re-running the setter-equivalent push, which is deliberately more than the JDK does (the JDK leaves the setter's listener rewiring broken forever) — the accepted R_no_silent_improvements wobble the ERROR names.

The registry is session-scoped (surviving the F5 @PreserveOnRefresh teleport, like D_session_scoped_pools) and weakly referenced, so disposed frames drop out on their own. All entry points run under the session lock — callSwing's epilogue and Flow's beforeClientResponse both hold it — so a plain WeakHashMap-backed set suffices.

  • Method Details

    • register

      public static void register(FieldReconciler.Reconcilable emulator)
      Adds the emulator to its session's reconcile set and arms the one-shot bootstrap check for the current UI's next flush. Call once, from the emulator's root constructor. No-ops without a current session (bare unit construction) — in production and under Karibu one is always current. Anchored on the UI, not the peer, so a peer not built yet still gets its check.
    • reconcileAll

      public static void reconcileAll(com.vaadin.flow.server.VaadinSession session)
      Reconciles every live registered emulator of this session. Requires the session lock.
    • reportDirectWrite

      public static void reportDirectWrite(Object emulator, String field, String setter)
      The loud half of a repaired direct write. One call per detected field per reconcile — the repair itself resets the comparison, so a single write logs once.