Skip to content

feat: reactive watch API (watch* subscriptions) - #3

Merged
edisdev merged 7 commits into
mainfrom
feat/reactive-watch-api
Aug 23, 2026
Merged

feat: reactive watch API (watch* subscriptions)#3
edisdev merged 7 commits into
mainfrom
feat/reactive-watch-api

Conversation

@edisdev

@edisdev edisdev commented Aug 23, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a reactive watch API so consumers can subscribe to a kind of device
information and get a callback whenever the value changes, instead of polling a
getter on a timer. The current value is delivered immediately, then on every
change.

import { watchBattery } from "tauri-plugin-device-info-api";

const unwatch = await watchBattery((b) =>
  console.log(`Battery: ${b.level}% (charging: ${b.isCharging})`)
);
// await unwatch(); // stop when done

New functions: watchBattery, watchNetwork, watchStorage, watchDisplay,
watchDevice — each returns a Promise<UnwatchFn>. New exported types:
WatchOptions (intervalMs) and UnwatchFn.

How it works

The public API and event names are identical across platforms; the engine per
kind differs:

  • Native, event-driven on macOS — battery (IOKit), display (Core Graphics),
    network (SystemConfiguration). CPU stays idle between changes; updates arrive
    the instant the OS reports them.
  • Change-detecting polling fallback for kinds/platforms with no OS event
    (storage and device everywhere; all kinds off macOS). Emits only on a real
    change.

Monitors are reference-counted: one per kind, started on the first
subscriber and torn down on the last.

Performance

  • Per-kind polling defaults/floors so expensive getters aren't hammered:
    device 60s (min 10s), storage 10s (min 1s), others 2s (min 250ms).
    device shells out to system_profiler on macOS and rarely changes, so it's
    no longer read every couple seconds.
  • Poller sleeps on a condvar for the full interval and wakes immediately on stop
    (no 100ms busy-wake), so the CPU can idle.
  • Change events emitted by reference — no deep clone per snapshot.

Correctness / safety

These came out of a self-review of the new backend code:

  • Race-free macOS run-loop teardown — stop is signalled through the run
    loop via a CFRunLoopSource, so a stop that races startup can't be lost
    (previously CFRunLoopStop before CFRunLoopRun could hang stop_watching).
  • stop_watching no longer holds the shared watcher lock while joining the
    monitor thread, so a slow teardown can't block other kinds' subscribe/stop.
  • OS callbacks contain panics so they can't unwind across the C ABI and
    abort the process.
  • The JS watch() removes its event listener if the backend fails to start; the
    example surfaces a failed start instead of leaving an unhandled rejection.

Testing

  • cargo test — added watcher lifecycle tests (spawn → emit → stop → join → free)
    for every kind, including the native macOS paths; ran the start/stop test in a
    loop to confirm no teardown hang.
  • cargo clippy clean.

Docs & versioning

  • New Reactive Watch API reference page + guide/examples/README updates.
  • Version bumped 1.0.1 → 1.1.0 (backwards-compatible), new CHANGELOG.md.

Commits

Split into reviewable layers: backend watcher · JS bindings · example app ·
docs · release/changelog.

🤖 Generated with Claude Code

edisdev and others added 7 commits August 24, 2026 01:32
Add a reactive watch API so consumers can subscribe to a kind of device
information and receive a Tauri event whenever it changes, instead of
polling a getter on a timer.

- New `start_watching`/`stop_watching` commands (with matching permissions)
  backed by a reference-counted per-kind monitor registry.
- Native, event-driven monitors on macOS: battery (IOKit), display (Core
  Graphics), and network (SystemConfiguration) — CPU stays idle between
  changes. Storage and device fall back to polling.
- Change-detecting polling fallback with per-kind default/min intervals so
  expensive getters (e.g. `device` via system_profiler) are not polled
  aggressively; sleeps on a condvar and wakes immediately on stop.
- macOS run-loop teardown is race-free (stop is signalled through the run
  loop itself) and stop() no longer holds the shared lock while joining;
  OS callbacks contain panics so they cannot unwind across the C ABI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Expose the reactive watch API to JS: watchBattery, watchNetwork,
watchStorage, watchDisplay, and watchDevice. Each listens on the
per-kind change event, starts the backend monitor, and returns an
async unwatch that stops it and removes the listener. If starting the
backend fails, the event listener is removed so it does not leak.

Add and export the WatchOptions (intervalMs) and UnwatchFn types.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Use watchBattery/watchNetwork/watchDisplay instead of a manual
setInterval loop. The charging overlay now fires only on a real
false → true transition (not on the initial value), and all watchers
are unsubscribed on destroy. A failed start is surfaced as an error
rather than left as an unhandled promise rejection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a dedicated Reactive Watch API reference page (functions, options,
native-vs-polling behavior, per-kind intervals, permissions) and wire it
into the sidebar and API overview. Update the README, getting-started
guide, examples, and home page to cover watch* alongside the getters, and
replace the manual setInterval example with a watch-based one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Minor bump for the backwards-compatible reactive watch API. Add a
dev-dependency on tauri's test feature for the watcher lifecycle tests,
and introduce CHANGELOG.md documenting the 1.1.0 release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes the `cargo fmt -- --check` CI failure (comment indentation in the
reference-counting test).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The mock-runtime test binary crashed on load on windows-latest with
STATUS_ENTRYPOINT_NOT_FOUND (0xc0000139) before any test ran. Enable
Tauri's `test` feature only off Windows (target-gated dev-dependency)
and cfg-gate the three mock-based watcher tests to match. The pure-logic
tests still run on every platform.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@edisdev
edisdev merged commit d22d4fd into main Aug 23, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant