Skip to content

Include hasChanges in sync-done event payload #1366

Description

@silverbucket

Problem

sync-done fires every sync cycle regardless of whether any data actually changed. The change event fires per-item during sync. There's no signal that tells an app "the sync cycle finished and data changed (or didn't)."

This forces app developers into an awkward choice:

  1. Reload on every change event — N reloads per sync cycle when many items arrive (e.g. batch of images synced from another device). Each fires before the cycle is complete, so you're reloading mid-sync and getting partial state.
  2. Reload on every sync-done — Fires every 10s (foreground default) even when nothing changed, triggering unnecessary getAll() calls, cache reads, and store replacements that force UI re-renders.

Current workaround

In inbox-rs we ended up combining both signals: register an onChange handler that sets a boolean flag, then check the flag in the sync-done handler and only reload if it's true.

let syncHasChanges = false;

privateClient.on('change', () => { syncHasChanges = true; });

rs.on('sync-done', () => {
  if (syncHasChanges) {
    syncHasChanges = false;
    scheduleReload();
  }
});

This works, but it's not ideal for app developers:

  • It requires understanding the relationship between two different event systems (change on BaseClient vs sync-done on RemoteStorage) and how their timing interleaves during a sync cycle.
  • The flag is fragile module-level state that's invisible to the rest of the app — easy to get wrong in testing and hard to reason about.
  • Every app that wants efficient sync-driven reloads has to independently discover and implement this same pattern.
  • It still leaves change events firing during the sync cycle even though they're only used as a flag — the app is paying for per-item event dispatch it doesn't actually need.

Proposed solution

The sync module already knows when data changes — it calls emitChange() at three points in sync.ts (lines 514, 544, 582) when remote merges produce actual data changes. Surface this in the sync-done payload:

// Sync class property
private _hasChanges = false;

// At each emitChange call site:
this._hasChanges = true;
this.rs.local.emitChange(change);

// In finishSuccessfulTask:
const hasChanges = this._hasChanges;
this._hasChanges = false;
this.rs._emit('sync-done', { completed: true, hasChanges });

// In finishUnsuccessfulTask:
const hasChanges = this._hasChanges;
this._hasChanges = false;
this.rs._emit('sync-done', { completed: false, hasChanges });

This changes the sync-done signature from { completed: boolean } to { completed: boolean, hasChanges: boolean }. Fully backwards-compatible — existing consumers that don't check hasChanges are unaffected.

With this, the app-side code simplifies to:

rs.on('sync-done', (result) => {
  if (result.hasChanges) {
    reloadStores();
  }
});

No flag, no dual-event wiring, no per-item change listener needed for reload purposes.

Related discussion: #1364

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions