Skip to content

Add the shared native event bus plugin (native-bus) - #294

Open
ScottMorris wants to merge 5 commits into
mainfrom
feat/255-p1-native-bus
Open

Add the shared native event bus plugin (native-bus)#294
ScottMorris wants to merge 5 commits into
mainfrom
feat/255-p1-native-bus

Conversation

@ScottMorris

@ScottMorris ScottMorris commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

What this is

Phase 1 of #255's implementation plan: a new, minimal Tauri plugin, plugins/native-bus, that will host a Kotlin NativeEventBus (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 a DurableEventQueue (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.ktsubscribe/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 on WearSyncQueue.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.
  • KeyValueStore behind a small interface (SharedPreferences in prod, in-memory fake in tests) so everything runs as plain JUnit 4, matching this repo's existing test-kotlin-plugins CI job.
  • tauri-plugin-native-bus is a direct Cargo path-dependency of apps/threshold/src-tauri (in addition to being registered via .plugin()) — required 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 graph, only reaches direct dependents, not transitive ones through alarm-manager/wear-sync.
  • .github/workflows/test.yml's test-kotlin-plugins job 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 --check
  • Native-bus's JUnit suite (16 tests) green
  • Full pnpm tauri android build --debug -t aarch64 --ci succeeds end-to-end (APK + AAB), confirming both the Gradle wiring resolves and the plugin's app.manage() actually runs without panicking at startup

Review status

Reviewed via /code-review high twice. First pass found the plugin was scaffolded but never actually registered via .plugin() (so app.manage() never ran, and any future native_bus() call would've panicked), an unnecessary Runtime generic with no purpose here, and a CI bug where pre-wiring include(':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.

ScottMorris and others added 3 commits August 13, 2026 14:29
…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
@ScottMorris ScottMorris added android Android toolchain and mobile CI concerns architecture System design plugin Plugin work rust Rust tooling and build and test work labels Aug 13, 2026
@ScottMorris ScottMorris changed the title feat/255 p1 native bus Add the shared native event bus plugin (native-bus) Aug 13, 2026
@ScottMorris
ScottMorris marked this pull request as ready for review August 14, 2026 04:44
@ScottMorris

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

android Android toolchain and mobile CI concerns architecture System design plugin Plugin work rust Rust tooling and build and test work

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant