Skip to content

Fix change-watcher lifecycle and mode-switch handling - #114

Merged
in-jun merged 1 commit into
mainfrom
fix/changetriggers
Jul 18, 2026
Merged

Fix change-watcher lifecycle and mode-switch handling#114
in-jun merged 1 commit into
mainfrom
fix/changetriggers

Conversation

@in-jun

@in-jun in-jun commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Cleans up the realtime change-watching subsystem: leaked resources, stale watchers, and a redundant spread.

  • Close the RemoteStorage instances created by the change stream. ChangeTriggers.forPairs discarded each RemoteStorage right after calling .changes(), ignoring its AutoCloseable contract. It now creates the instance inside a flow { ... } that closes it in finally, so a backend whose watch reuses the main connection can't leak one per resubscription.
  • React to sync-mode changes while the app is foregrounded. MainViewModel.onForegrounded read settings.mode once, so switching PERIODIC↔REALTIME left the in-app watcher running alongside the foreground service's watcher (duplicate subscriptions) or stopped live sync until the next ON_RESUME. It now derives the watched pairs from combine(settings, pairs), tearing down or starting the watch immediately on a mode change.
  • Release directory observers for deleted/moved directories. DirectoryWatcher's observer map only ever grew: no DELETE_SELF in the mask, and re-registration was skipped for a recreated path, so a delete+recreate went unwatched. It now handles DELETE_SELF/MOVE_SELF and parent-side DELETE/MOVED_FROM to drop stale entries, attaches observers recursively on both CREATE and MOVED_TO, and replaces rather than skips an existing entry.
  • Use Iterable<Flow<T>>.merge() instead of merge(*flows.toTypedArray()), dropping the array allocation and spread.

Fixes #99
Fixes #20
Fixes #16
Fixes #13

@in-jun in-jun left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed against the four referenced issues — all four are resolved and the changes map cleanly onto them with no scope creep.

#20 (RemoteStorage lifecycle)remoteChanges correctly owns the instance and closes it in finally. Confirmed only remote() needs this: local() returns a plain Storage (not AutoCloseable), so leaving it unwrapped is right. I also checked that SmbRemoteStorage.close() acts on the constructor's main client, which is a distinct SMBClient from the watch's own secureClient()/watchConn (torn down in changes()'s awaitClose), so completing or cancelling this flow can't prematurely kill a live watch and there's no double-close.

#16 (mode switch) — Deriving the watched set from combine(settings, pairs) with distinctUntilChanged + flatMapLatest is the right shape. REALTIME collapses to emptyList (foreground service takes over), PERIODIC restarts immediately, and since SyncPair/AppSettings are data classes the distinctUntilChanged meaningfully avoids restarting the watch on unrelated settings edits.

#13 (directory watcher) — Good handling. Replace-not-skip fixes the delete+recreate inode case, recursive observeTree on CREATE/MOVED_TO covers bulk moves, and the parent-side releaseTree prefix sweep is robust even if individual child *_SELF events are dropped under queue pressure.

#99flows.merge() is the clean equivalent.

One minor, non-blocking observation: deleting or moving the root itself releases only the root's own entry via *_SELF, so descendant map entries then rely on their individual DELETE_SELF events rather than a parent-side releaseTree sweep — under inotify overflow a few could linger. It's an unusual case (the whole sync root vanishing while watched, where the watch is effectively broken anyway and the safety poll covers correctness), and it's strictly better than the prior always-leak behavior, so I'd leave it as-is for now.

LGTM.

@in-jun
in-jun merged commit c3cf3c6 into main Jul 18, 2026
1 check passed
@in-jun
in-jun deleted the fix/changetriggers branch July 18, 2026 15:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant