Skip to content

feat(tauri): autoupdate on app resume#2822

Merged
seanaye merged 4 commits into
mainfrom
seanaye/feat/update-on-resume
Apr 24, 2026
Merged

feat(tauri): autoupdate on app resume#2822
seanaye merged 4 commits into
mainfrom
seanaye/feat/update-on-resume

Conversation

@seanaye
Copy link
Copy Markdown
Contributor

@seanaye seanaye commented Apr 23, 2026

  • apply update on app resume

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 23, 2026

Warning

Rate limit exceeded

@seanaye has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 23 minutes and 0 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 23 minutes and 0 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: cb054b7c-24a1-4afa-bf87-01e85b4f13ea

📥 Commits

Reviewing files that changed from the base of the PR and between 2298c60 and 4e55eed.

⛔ Files ignored due to path filters (1)
  • rust/cloud-storage/Cargo.lock is excluded by !**/*.lock, !**/Cargo.lock
📒 Files selected for processing (5)
  • js/app/tauri/macro_bundle_updater_plugin/src/domain/service.rs
  • js/app/tauri/macro_bundle_updater_plugin/src/inbound/plugin.rs
  • rust/cloud-storage/native_app_service/Cargo.toml
  • rust/cloud-storage/native_app_service/src/outbound.rs
  • rust/cloud-storage/native_app_service/src/tests.rs
📝 Walkthrough

Walkthrough

This PR refactors the Tauri bundle updater plugin to auto-apply pending updates on app resume, consolidates error handling using rootcause::Report, transitions from static to dynamic SHA-256 checksum computation, and simplifies token creation via Default trait implementation.

Changes

Cohort / File(s) Summary
Tauri application build system
js/app/justfile
Adds a new dist-archive task that builds the platform-specific web distribution and packages it into a timestamped ZIP archive in the native app server tooling directory.
Tauri bundle updater plugin
js/app/tauri/macro_bundle_updater_plugin/src/domain/models.rs, js/app/tauri/macro_bundle_updater_plugin/src/domain/service.rs, js/app/tauri/macro_bundle_updater_plugin/src/inbound/plugin.rs, js/app/tauri/src-tauri/src/lib.rs
Refactors bundle update orchestration: adds apply_update method to service layer, makes set_bundle_root and cleanup_old_bundles private, removes explicit new() constructors for token types in favor of Default derives, consolidates update-commit logic into apply_completed_update helper, and wires auto-application on RunEvent::Resumed during app startup.
Input accessory plugin
js/app/tauri/input_accessory_plugin/src/lib.rs
Reorders items within the use tauri::{ ... } import block without changing functionality or public API.
Native app error handling and checksum computation
rust/cloud-storage/native_app_service/Cargo.toml, rust/cloud-storage/native_app_service/src/domain/models.rs, rust/cloud-storage/native_app_service/src/domain/ports.rs, rust/cloud-storage/native_app_service/src/domain/service.rs, rust/cloud-storage/native_app_service/src/outbound.rs, rust/cloud-storage/tools/native_app_server/Cargo.toml, rust/cloud-storage/tools/native_app_server/src/main.rs
Replaces anyhow with rootcause for error reporting throughout service and server codebases, simplifies UpdateErr variants from error-carrying to unit variants, updates trait and method signatures to wrap errors with Report<UpdateErr>, and switches checksum calculation from static env-configured values to dynamic SHA-256 hex digest computation from ZIP archives.

Possibly related PRs

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title follows conventional commits format with 'feat:' prefix and accurately describes the main change of auto-updating the app on resume.
Description check ✅ Passed The description is related to the changeset, indicating the primary objective of applying updates when the app resumes from background.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@seanaye seanaye force-pushed the seanaye/feat/update-on-resume branch from 1d6bda7 to aea7ce9 Compare April 23, 2026 22:12
@seanaye seanaye changed the base branch from main to seanaye/fix/tauri-deadlock-safari April 23, 2026 22:13
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 23, 2026

@seanaye seanaye force-pushed the seanaye/fix/tauri-deadlock-safari branch from 5da236f to b678ff2 Compare April 24, 2026 13:54
@seanaye seanaye force-pushed the seanaye/feat/update-on-resume branch from aea7ce9 to 48bb5c3 Compare April 24, 2026 13:54
@seanaye seanaye force-pushed the seanaye/fix/tauri-deadlock-safari branch from b678ff2 to 892b134 Compare April 24, 2026 17:50
@seanaye seanaye force-pushed the seanaye/feat/update-on-resume branch from 48bb5c3 to aa1685f Compare April 24, 2026 17:50
Base automatically changed from seanaye/fix/tauri-deadlock-safari to main April 24, 2026 19:00
@seanaye seanaye force-pushed the seanaye/feat/update-on-resume branch from 12ee8c8 to 2298c60 Compare April 24, 2026 19:01
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
js/app/tauri/macro_bundle_updater_plugin/src/inbound/plugin.rs (1)

116-144: ⚠️ Potential issue | 🔴 Critical

Bug: apply_completed_update ignores apply_update's return value and always returns Ok(true).

Service::apply_update returns Result<bool, Report> where the bool signals whether a completed update was actually applied. That value is discarded by the ? operator on line 131-133, and the function unconditionally returns Ok(true) on line 143. This has two user-visible consequences:

  1. The webview is reloaded on every app resume, even when there is no pending update. RunEvent::Resumed fires every time the app returns to the foreground (especially on mobile); with this bug, line 141's window.location.reload() runs on every foreground, dropping in-flight navigation, ephemeral UI state, and WebSocket connections. This is the opposite of the "apply update on resume" intent.
  2. perform_update's false => Err("No pending update") branch at line 152 becomes unreachable — the frontend error previously surfaced when clicking "Update" with no pending bundle will now silently succeed.

The tracing::info!("auto-applied pending bundle update on foreground") log in lib.rs will also fire on every resume, masking whether updates are actually being applied.

🔧 Proposed fix
-    let mut service = service_state.lock().await;
-
-    service
-        .apply_update(&cache_dir)
-        .await
-        .map_err(|e| e.to_string())?;
-
-    drop(service);
-
-    // Reload to pick up the new bundle. Using location.reload() instead of
-    // navigating to a new URL preserves WKWebView's cookie store.
-    if let Some(webview) = app_handle.webview_windows().values().next() {
-        tracing::info!("Bundle update complete, reloading to pick up new assets");
-        let _ = webview.eval("window.location.reload();");
-    }
-    Ok(true)
-}
+    let applied = {
+        let mut service = service_state.lock().await;
+        service
+            .apply_update(&cache_dir)
+            .await
+            .map_err(|e| e.to_string())?
+    };
+
+    if !applied {
+        return Ok(false);
+    }
+
+    // Reload to pick up the new bundle. Using location.reload() instead of
+    // navigating to a new URL preserves WKWebView's cookie store.
+    if let Some(webview) = app_handle.webview_windows().values().next() {
+        tracing::info!("Bundle update complete, reloading to pick up new assets");
+        let _ = webview.eval("window.location.reload();");
+    }
+    Ok(true)
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@js/app/tauri/macro_bundle_updater_plugin/src/inbound/plugin.rs` around lines
116 - 144, The function apply_completed_update currently discards the boolean
returned by PluginService::apply_update and always returns Ok(true); change it
to capture the returned bool (e.g., let applied =
service.apply_update(&cache_dir).await.map_err(|e| e.to_string())?;) and then
only trigger the webview reload and return Ok(true) when applied is true,
otherwise return Ok(false); ensure you still drop the service lock before
evaluating the webview and preserve error mapping to String so perform_update's
false branch remains reachable and the reload/log only happen on actual applied
updates.
rust/cloud-storage/native_app_service/src/outbound.rs (1)

68-83: 🧹 Nitpick | 🔵 Trivial

Minor: concurrent cache-miss requests can double-fetch.

Two concurrent callers that observe the cache miss will each execute compute_checksum and re-download the bundle. Because both compute the same hash, the final cache state is still correct — just wasteful. Consider a tokio::sync::OnceCell-per-version or a write-lock-first check pattern if duplicate fetches become expensive.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/cloud-storage/native_app_service/src/outbound.rs` around lines 68 - 83,
get_app_bundle_checksum currently suffers duplicate work when two callers
observe a cache miss concurrently; change the caching to avoid double-fetch by
using a per-version synchronization primitive (e.g., tokio::sync::OnceCell) or a
write-lock-first double-check: instead of only read-locking then computing, on
miss acquire the checksum_cache write lock, check again for a hit and if still
missing insert or fetch a per-version OnceCell (or similar) and call its
get_or_init to run compute_checksum exactly once; update references to
checksum_cache and the get_app_bundle_checksum logic to use the per-version
OnceCell (or the write-lock-first double-check) so concurrent callers wait for
the single compute_checksum result rather than running it twice.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@js/app/tauri/macro_bundle_updater_plugin/src/domain/service.rs`:
- Around line 335-358: apply_update can race and run twice because it relies on
the worker to reset the status; to make it idempotent, explicitly reset the
status watch to Idle at the end of the successful apply path (before or
immediately after calling self.set_bundle_root / cleanup_old_bundles) so
subsequent concurrent apply_update calls will see Ok(false). Locate apply_update
and set self.status_tx (or the status sender used by the service) to
Ok(UpdateStatus::Idle) after persisting the new bundle root (references:
apply_update, set_bundle_root, cleanup_old_bundles, UpdateStatus::Idle,
self.status_tx) so repeated/resumed invocations short-circuit; alternatively,
you can detect start() returning Err(Full) and return Ok(false) to treat an
in-progress reset as already handled.

In `@rust/cloud-storage/native_app_service/src/outbound.rs`:
- Around line 36-45: Add the "stream" feature to the workspace reqwest
dependency in rust/cloud-storage/Cargo.toml, then refactor compute_checksum to
stream the bundle instead of buffering: in compute_checksum (the async fn
returning Result<String, Report<UpdateErr>>), call
reqwest::get(self.get_app_bundle_path()).await?.error_for_status()? and use
.bytes_stream() to iterate chunks (use futures::StreamExt), feed each chunk into
a Sha256 hasher with hasher.update(&chunk), finalize with hasher.finalize() and
format!("{:x}", sum), and keep the existing .context(UpdateErr::Network) error
mapping; ensure necessary imports (futures::StreamExt, sha2::Sha256,
sha2::Digest) are added and the function signature and error type
(Report<UpdateErr>) remain unchanged.

In `@rust/cloud-storage/tools/native_app_server/src/main.rs`:
- Around line 35-43: The error mapping in get_app_bundle_checksum incorrectly
classifies local I/O failures from sha256_hex as UpdateErr::Network; update the
mapping to either wrap with a more accurate variant (e.g., UpdateErr::Io or
UpdateErr::Filesystem) or avoid adding Network context and return the original
io error via report!(e) so filesystem errors are preserved; locate
get_app_bundle_checksum and the sha256_hex call to change the map_err closure to
use the correct UpdateErr variant or omit the Network context.

---

Outside diff comments:
In `@js/app/tauri/macro_bundle_updater_plugin/src/inbound/plugin.rs`:
- Around line 116-144: The function apply_completed_update currently discards
the boolean returned by PluginService::apply_update and always returns Ok(true);
change it to capture the returned bool (e.g., let applied =
service.apply_update(&cache_dir).await.map_err(|e| e.to_string())?;) and then
only trigger the webview reload and return Ok(true) when applied is true,
otherwise return Ok(false); ensure you still drop the service lock before
evaluating the webview and preserve error mapping to String so perform_update's
false branch remains reachable and the reload/log only happen on actual applied
updates.

In `@rust/cloud-storage/native_app_service/src/outbound.rs`:
- Around line 68-83: get_app_bundle_checksum currently suffers duplicate work
when two callers observe a cache miss concurrently; change the caching to avoid
double-fetch by using a per-version synchronization primitive (e.g.,
tokio::sync::OnceCell) or a write-lock-first double-check: instead of only
read-locking then computing, on miss acquire the checksum_cache write lock,
check again for a hit and if still missing insert or fetch a per-version
OnceCell (or similar) and call its get_or_init to run compute_checksum exactly
once; update references to checksum_cache and the get_app_bundle_checksum logic
to use the per-version OnceCell (or the write-lock-first double-check) so
concurrent callers wait for the single compute_checksum result rather than
running it twice.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9d9e86bf-79d4-45a5-91a1-565f78972972

📥 Commits

Reviewing files that changed from the base of the PR and between bf31c0f and 2298c60.

⛔ Files ignored due to path filters (1)
  • rust/cloud-storage/Cargo.lock is excluded by !**/*.lock, !**/Cargo.lock
📒 Files selected for processing (13)
  • js/app/justfile
  • js/app/tauri/input_accessory_plugin/src/lib.rs
  • js/app/tauri/macro_bundle_updater_plugin/src/domain/models.rs
  • js/app/tauri/macro_bundle_updater_plugin/src/domain/service.rs
  • js/app/tauri/macro_bundle_updater_plugin/src/inbound/plugin.rs
  • js/app/tauri/src-tauri/src/lib.rs
  • rust/cloud-storage/native_app_service/Cargo.toml
  • rust/cloud-storage/native_app_service/src/domain/models.rs
  • rust/cloud-storage/native_app_service/src/domain/ports.rs
  • rust/cloud-storage/native_app_service/src/domain/service.rs
  • rust/cloud-storage/native_app_service/src/outbound.rs
  • rust/cloud-storage/tools/native_app_server/Cargo.toml
  • rust/cloud-storage/tools/native_app_server/src/main.rs

Comment thread js/app/tauri/macro_bundle_updater_plugin/src/domain/service.rs
Comment on lines +36 to 45
async fn compute_checksum(&self) -> Result<String, Report<UpdateErr>> {
async move {
let url = self.get_app_bundle_path();
let bytes = reqwest::get(url).await?.error_for_status()?.bytes().await?;
let hash = Sha256::digest(&bytes);
Result::<_, Report>::Ok(format!("{:x}", hash))
}
.await
.context(UpdateErr::Network)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

reqwest Response bytes_stream method documentation

💡 Result:

The bytes_stream method on reqwest::Response converts the response body into a stream of Bytes chunks. It requires the stream feature of the reqwest crate to be enabled. Signature: pub fn bytes_stream(self) -> impl Stream<Item = Result> It consumes self (the Response), returning an asynchronous stream that yields Result<Bytes, reqwest::Error> for each chunk of the response body. Example usage: use futures_util::StreamExt; let mut stream = reqwest::get("http://httpbin.org/ip") .await? .bytes_stream; while let Some(item) = stream.next.await { println!("Chunk: {:?}", item?); } This is useful for streaming large responses without loading the entire body into memory. The implementation uses http_body_util::BodyDataStream::new internally, mapping errors appropriately. Source code confirms it's available only with feature = "stream", and the stream is produced from the response's body.

Citations:


🏁 Script executed:

# First, find and read the Cargo.toml for native_app_service
fd -t f Cargo.toml | grep -i native_app_service | head -5

Repository: macro-inc/macro

Length of output: 108


🏁 Script executed:

# Also find the exact location of outbound.rs
fd -t f outbound.rs | grep native_app_service

Repository: macro-inc/macro

Length of output: 113


🏁 Script executed:

# Read the current Cargo.toml to check dependencies and features
cat rust/cloud-storage/native_app_service/Cargo.toml

Repository: macro-inc/macro

Length of output: 714


🏁 Script executed:

# Read the outbound.rs file to see imports and full context
cat -n rust/cloud-storage/native_app_service/src/outbound.rs | head -80

Repository: macro-inc/macro

Length of output: 3457


🏁 Script executed:

# Check the workspace Cargo.toml at the root
cat rust/Cargo.toml | grep -A 5 reqwest

Repository: macro-inc/macro

Length of output: 107


🏁 Script executed:

# Check if there's a workspace definition in the cloud-storage folder
cat rust/cloud-storage/Cargo.toml | head -30

Repository: macro-inc/macro

Length of output: 631


🏁 Script executed:

# Search for futures as a workspace dependency
rg "^futures" Cargo.toml

Repository: macro-inc/macro

Length of output: 110


🏁 Script executed:

# Find the root Cargo.toml
fd -t f Cargo.toml | head -3

Repository: macro-inc/macro

Length of output: 161


🏁 Script executed:

# Check the workspace dependencies at the root
head -100 Cargo.toml | tail -50

Repository: macro-inc/macro

Length of output: 129


🏁 Script executed:

# Look for reqwest and futures in workspace dependencies
cat Cargo.toml | grep -A 2 "reqwest\|futures"

Repository: macro-inc/macro

Length of output: 102


🏁 Script executed:

# The workspace root is in rust/cloud-storage. Let's check that Cargo.toml for workspace.dependencies
cat rust/cloud-storage/Cargo.toml | grep -A 50 "\[workspace.dependencies\]"

Repository: macro-inc/macro

Length of output: 1548


🏁 Script executed:

# Also check for reqwest and futures definitions
cat rust/cloud-storage/Cargo.toml | tail -100

Repository: macro-inc/macro

Length of output: 2919


Add stream feature to reqwest and refactor compute_checksum to stream the bundle instead of buffering it entirely.

reqwest::Response::bytes() loads the full bundle into memory before hashing. Streaming with bytes_stream() keeps memory bounded and is especially important as bundle sizes grow.

Update the workspace dependency in rust/cloud-storage/Cargo.toml:

 reqwest = { version = "0.12.12", features = ["hickory-dns", "json"] }

to:

 reqwest = { version = "0.12.12", features = ["hickory-dns", "json", "stream"] }

Then update the method:

♻️ Streaming fix
 async fn compute_checksum(&self) -> Result<String, Report<UpdateErr>> {
     async move {
         let url = self.get_app_bundle_path();
-        let bytes = reqwest::get(url).await?.error_for_status()?.bytes().await?;
-        let hash = Sha256::digest(&bytes);
+        use futures::StreamExt;
+        let mut stream = reqwest::get(url).await?.error_for_status()?.bytes_stream();
+        let mut hasher = Sha256::new();
+        while let Some(chunk) = stream.next().await {
+            hasher.update(&chunk?);
+        }
+        let hash = hasher.finalize();
         Result::<_, Report>::Ok(format!("{:x}", hash))
     }
     .await
     .context(UpdateErr::Network)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/cloud-storage/native_app_service/src/outbound.rs` around lines 36 - 45,
Add the "stream" feature to the workspace reqwest dependency in
rust/cloud-storage/Cargo.toml, then refactor compute_checksum to stream the
bundle instead of buffering: in compute_checksum (the async fn returning
Result<String, Report<UpdateErr>>), call
reqwest::get(self.get_app_bundle_path()).await?.error_for_status()? and use
.bytes_stream() to iterate chunks (use futures::StreamExt), feed each chunk into
a Sha256 hasher with hasher.update(&chunk), finalize with hasher.finalize() and
format!("{:x}", sum), and keep the existing .context(UpdateErr::Network) error
mapping; ensure necessary imports (futures::StreamExt, sha2::Sha256,
sha2::Digest) are added and the function signature and error type
(Report<UpdateErr>) remain unchanged.

Comment on lines 35 to 43
async fn get_app_bundle_checksum(
&self,
_version: &semver::Version,
) -> Result<String, UpdateErr> {
Ok(self.checksum.clone())
) -> Result<String, Report<UpdateErr>> {
sha256_hex(&self.archive_path).await.map_err(|e| {
tracing::error!(error=?e, path=?self.archive_path, "failed to hash bundle archive");
report!(e).context(UpdateErr::Network)
})
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Minor: mislabeling a local I/O error as UpdateErr::Network.

sha256_hex fails on local filesystem errors (e.g., missing app-archive.zip), but those are mapped to UpdateErr::Network. For this dev-only test server it is inconsequential, but a dedicated variant (or keeping the original io::Error via report! without context) would be more accurate and easier to debug. Not blocking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/cloud-storage/tools/native_app_server/src/main.rs` around lines 35 - 43,
The error mapping in get_app_bundle_checksum incorrectly classifies local I/O
failures from sha256_hex as UpdateErr::Network; update the mapping to either
wrap with a more accurate variant (e.g., UpdateErr::Io or UpdateErr::Filesystem)
or avoid adding Network context and return the original io error via report!(e)
so filesystem errors are preserved; locate get_app_bundle_checksum and the
sha256_hex call to change the map_err closure to use the correct UpdateErr
variant or omit the Network context.

@seanaye seanaye force-pushed the seanaye/feat/update-on-resume branch from cf5dea5 to 4e55eed Compare April 24, 2026 19:38
@seanaye seanaye merged commit a01f52f into main Apr 24, 2026
42 checks passed
@seanaye seanaye deleted the seanaye/feat/update-on-resume branch April 24, 2026 19:56
This was referenced Apr 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant