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:
- 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.
- 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
Problem
sync-donefires every sync cycle regardless of whether any data actually changed. Thechangeevent 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:
changeevent — 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.sync-done— Fires every 10s (foreground default) even when nothing changed, triggering unnecessarygetAll()calls, cache reads, and store replacements that force UI re-renders.Current workaround
In inbox-rs we ended up combining both signals: register an
onChangehandler that sets a boolean flag, then check the flag in thesync-donehandler and only reload if it's true.This works, but it's not ideal for app developers:
changeonBaseClientvssync-doneonRemoteStorage) and how their timing interleaves during a sync cycle.changeevents 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 insync.ts(lines 514, 544, 582) when remote merges produce actual data changes. Surface this in thesync-donepayload:This changes the
sync-donesignature from{ completed: boolean }to{ completed: boolean, hasChanges: boolean }. Fully backwards-compatible — existing consumers that don't checkhasChangesare unaffected.With this, the app-side code simplifies to:
No flag, no dual-event wiring, no per-item change listener needed for reload purposes.
Related discussion: #1364