Add the shared native event bus plugin (native-bus) - #294
Open
ScottMorris wants to merge 5 commits into
Open
Conversation
…e plugin Adds `plugins/native-bus`, the Phase 1 substrate for issue #255: a minimal Tauri plugin (no webview-invokable commands yet) whose real purpose is to carry a Kotlin Android library into the generated Gradle project graph so alarm-manager and wear-sync can depend on it in a later phase. **`NativeEventBus`** is a process-wide singleton letting different Android plugins' native code talk to each other in-process before Rust/the WebView has booted (e.g. alarm-manager telling wear-sync an alarm fired). `publish()` runs synchronously on the caller's thread by design -- callers like a `BroadcastReceiver` are expected to do their own async hand-off, since this matters concretely for the intended first real caller (`AlarmReceiver.onReceive()` in a later phase, which runs under a broadcast ANR budget). Each listener runs inside its own try/catch so one broken listener can't break delivery to others or propagate back to the publisher. **`DurableEventQueue`** is a generic, per-plugin-instantiated replacement for the "queue events until Rust is up, then drain" mechanism currently duplicated across `WearSyncQueue` and `AlarmManagerPlugin`'s four separate queues. One chronological JSON-array log per plugin instance, keyed by caller-defined `topic` strings, drains in `publishedAt` order regardless of topic. Tolerates corrupt individual entries and unrecognised schema versions by skipping just that entry rather than failing the whole drain -- production devices have carried pending events across multiple days. **`KeyValueStore`** is a tiny abstraction (with a `SharedPreferences` production implementation) purely so both of the above can be unit tested against an in-memory fake -- this codebase's `test-kotlin-plugins` CI job runs plain JUnit 4, no Robolectric. **Workspace wiring:** `tauri-plugin-native-bus` is added as a *direct* dependency of the app crate (not just transitively via alarm-manager/wear-sync in a later phase) because Cargo's `links`/`DEP_*_ANDROID_LIBRARY_PATH` metadata propagation -- which is what makes `tauri-build` auto-add a plugin's `android/` directory to the generated Gradle project -- only reaches direct dependents. Verified with a real `cargo check --workspace`, a real `pnpm tauri android build --debug -t aarch64`, and by inspecting the generated `tauri.settings.gradle`, which correctly includes `:tauri-plugin-native-bus` pointing at `plugins/native-bus/android`. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013WVm6JTTpiSY8KSKJ3p5Qq
Adds `plugins/native-bus/android` to `test-kotlin-plugins`' settings synthesis
and `gradlew testDebugUnitTest` invocation list, matching the existing pattern
for the other seven plugins in that job.
Also pre-extends `write_kts_settings` with an optional second argument so
alarm-manager and wear-sync's isolated per-plugin settings additionally
`include(":tauri-plugin-native-bus")` pointing at `plugins/native-bus/android`.
Neither plugin depends on native-bus yet, so this is currently unused there --
but a later phase of issue #255 adds that Gradle project dependency, and
without this pre-wiring their isolated CI builds would fail at that point with
an unresolved project reference.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WVm6JTTpiSY8KSKJ3p5Qq
… generic
Code review on this branch found three issues, all fixed here:
**Critical: the plugin was never registered.** `apps/threshold/src-tauri/src/lib.rs`'s
builder chain never called `.plugin(tauri_plugin_native_bus::init())`, so its `setup()`
(which calls `app.manage(native_bus)`) never ran at runtime -- any future
`NativeBusExt::native_bus()` caller would have panicked via `Manager::state()`. This also
violated CLAUDE.md's plugin rule of registering every new plugin in `lib.rs`, not just the
workspace `Cargo.toml`. Added the missing `.plugin()` call.
**Unnecessary generic.** `NativeBus<R: Runtime>` (and its `PhantomData<fn() -> R>`
workaround for the `Send + Sync` bound `app.manage()` requires) was solving a problem this
plugin doesn't have: unlike every other plugin here, native-bus registers no Kotlin
`@TauriPlugin` component and holds no `PluginHandle<R>` or other per-runtime data, so
there was nothing to be generic over `Runtime` for. `NativeBus` is now a plain
data-free marker struct. Since `NativeBusExt` is consequently non-generic too, its
formerly-blanket `impl<R: Runtime, T: Manager<R>> NativeBusExt<R> for T` (which relied on
its own `<R>` to satisfy coherence) no longer type-checks as a blanket impl -- replaced
with direct `impl NativeBusExt for App<R>` / `AppHandle<R>` impls instead.
**CI: premature cross-plugin settings wiring.** The pre-wired
`include(":tauri-plugin-native-bus")` added to alarm-manager's and wear-sync's
synthesized `settings.gradle.kts` was not harmless: `gradlew -p <dir> testDebugUnitTest`'s
unqualified task selector runs in every project in the build graph that has the task,
so native-bus's own tests would have run redundantly on every one of those two plugins'
CI steps, and (under this workflow step's errexit) a native-bus test failure surfacing
during the alarm-manager step would abort the script before native-bus's own
correctly-labelled line ever ran, misattributing the failure. Removed the pre-wiring;
`write_kts_settings` no longer takes an extra-includes parameter. A later phase of issue
#255 will add both the real Gradle project dependency and this settings-synthesis line
together, at the point they're actually needed.
Re-verified: `cargo check --workspace`, `cargo clippy --workspace --all-targets`,
`cargo fmt --check` all clean (no warnings from native-bus or threshold); a full
`pnpm tauri android build --debug -t aarch64 --ci` succeeds end-to-end with the plugin
now actually registered (proving `app.manage()` runs without panicking); native-bus's 16
JUnit tests still pass unaffected.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WVm6JTTpiSY8KSKJ3p5Qq
This was referenced Aug 13, 2026
ScottMorris
marked this pull request as ready for review
August 14, 2026 04:44
Contributor
Author
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0175f64469
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
DurableEventQueue.commit() ran the whole persisted log through parseEnvelopes() and rewrote the queue from only what fully parsed, which silently dropped entries drainAll() deliberately tolerates but can't fully understand (e.g. an unrecognised future schema version after a downgrade) even though they were never delivered. commit() now filters the original raw JSON array by extracted event ID via a new extractEventId() helper, so it only ever removes the IDs it was told to remove and leaves everything else -- including entries it can't fully parse -- untouched for a later retry. Also drops the now-unused toJsonArray() helper, since commit() no longer reconstructs the array from parsed Envelopes. Adds a regression test seeding one normally-parseable entry alongside one with an unrecognised schema version, committing only the parseable entry's ID, and asserting the future-version entry survives in the raw persisted log.
Reflows the multi-line KDoc/Rustdoc prose paragraphs introduced when this plugin was scaffolded onto single source lines each, per this repo's Markdown convention (AGENTS.md): manual line breaks mid- paragraph don't survive rendering as intended and create noisy diffs. Covers NativeEventBus.kt (including its bulleted threading-contract list, where each bullet's own prose is joined but the bullets stay separate lines) and KeyValueStore.kt on the Kotlin side, and lib.rs, desktop.rs, and mobile.rs on the Rust side. No code or comment content changes, only line-wrapping.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
Phase 1 of #255's implementation plan: a new, minimal Tauri plugin,
plugins/native-bus, that will host a KotlinNativeEventBus(a process-wide singleton letting different Android plugins' native code talk to each other in-process, without waiting for the Rust/WebView runtime to boot) and aDurableEventQueue(a generic, reusable, per-plugin-instantiated persistence+drain class that later phases will migrate alarm-manager's and wear-sync's existing hand-rolled queues onto). This PR is the substrate only — no other plugin depends on it yet.What's here
NativeEventBus.kt—subscribe/publish(topic, payload): Set<String>, synchronous in-process delivery on the caller's thread, per-listener try/catch isolation, thread-safe registry. Threading contract documented in KDoc: listeners must do cheap synchronous work only and hand blocking work to their own executor.DurableEventQueue.kt— modelled onWearSyncQueue.kt's already-proven single-generic-log shape (one prefs key, one JSON array), not alarm-manager's four-separate-queues shape. Envelope:{v, topic, payload, eventId, publishedAt, handledNatively}. Tolerates corrupt individual entries and unknown schema versions without failing the whole drain.KeyValueStorebehind a small interface (SharedPreferences in prod, in-memory fake in tests) so everything runs as plain JUnit 4, matching this repo's existingtest-kotlin-pluginsCI job.tauri-plugin-native-busis a direct Cargo path-dependency ofapps/threshold/src-tauri(in addition to being registered via.plugin()) — required because Cargo'slinks/DEP_*_ANDROID_LIBRARY_PATHmetadata propagation, which is what makestauri-buildauto-add a plugin'sandroid/directory to the generated Gradle project graph, only reaches direct dependents, not transitive ones through alarm-manager/wear-sync..github/workflows/test.yml'stest-kotlin-pluginsjob wired up for native-bus's own Kotlin unit tests.Testing checklist (automated, no device needed)
cargo check --workspace/cargo clippy --workspace --all-targets/cargo fmt --checkpnpm tauri android build --debug -t aarch64 --cisucceeds end-to-end (APK + AAB), confirming both the Gradle wiring resolves and the plugin'sapp.manage()actually runs without panicking at startupReview status
Reviewed via
/code-review hightwice. First pass found the plugin was scaffolded but never actually registered via.plugin()(soapp.manage()never ran, and any futurenative_bus()call would've panicked), an unnecessaryRuntimegeneric with no purpose here, and a CI bug where pre-wiringinclude(':tauri-plugin-native-bus')into alarm-manager's/wear-sync's synthesized settings caused Gradle to redundantly sweep native-bus's tests into their jobs. All three fixed and re-verified — that settings pre-wiring now lands with Phase 2A/2B instead, alongside the real Gradle dependency each of those actually adds.Part of #255.