livewire icon indicating copy to clipboard operation
livewire copied to clipboard

Fix race condition causing data loss in wire:model.live updates

Open ralphjsmit opened this issue 2 months ago • 0 comments

Under certain timing conditions (slow network, long-running pages, parallel requests), wire:model.live updates could cause data loss by replacing entire nested objects instead of updating individual properties.

The MessageBus buffers messages for 5ms before sending them (js/request/messageBus.js:81-85) to allow lifecycle hooks and interceptors to attach. While necessary, this creates a timing window where component state can change. Since getUpdates() is called at send time (js/request/index.js:175), the canonical state may have already been modified by other actions during the buffer. Combined with slow networks or parallel wire:model.live requests (allowed per js/request/interactions.js:68-74), this small window is enough for canonical and ephemeral to fall out of sync, triggering the race condition.

In real-life, this can cause the following component property (Filament):

public array $mountedActions = [
    ['data' => ['source' => 'incoming_call'], 'name' => 'foo']
];

With wire:model.live="mountedActions.0.data.source", changing the value would sometimes:

  • Expected: Update only mountedActions.0.data.source
  • Actual: Replace entire mountedActions[0], losing the name property

Correct: Good

Replaces the whole mountedActions.0 with this payload:

Bad

Root cause

A race condition in the state synchronization system:

  1. The diff() function compares canonical (server state) vs ephemeral (client state)
  2. When these states have type mismatches (e.g., empty array vs object), diff() returns the entire nested object instead of flat paths
  3. This happens when:
    • Network requests are slow (canonical/ephemeral diverge during request)
    • Multiple wire:model.live requests run in parallel
    • Component state changes between capturing the diff and sending the request
    • Pages have been open for extended periods

Code flow

  // js/utils.js:119-122 - The problematic behavior
  if (typeof left !== typeof right || ...) {
      diffs[path] = right;  // ← Returns entire nested object!
      return diffs
  }

When the server receives a nested object, it replaces the entire property (as designed), causing sibling properties to be lost.

Solution

Added a safety net in Component.getUpdates() that:

  1. Detects when diff() returned nested objects (indicates the race condition occurred)
  2. Automatically flattens those objects into proper dot-notated paths
  3. Preserves all properties and prevents data loss

The fix is safe, as the server-side code (HandleComponents.php) is explicitly designed to handle dot-notated paths only:

public function updateProperty($component, $path, $value, $context)
  {
      $segments = explode('.', $path);  // ← Expects dots
      // ...
  }

Tests

The reproduction of this was very sporadic and I wasn't able to even consistently reproduce it in the browser unfortunately.

ralphjsmit avatar Oct 15 '25 15:10 ralphjsmit