Skip to content

fix(yew-link): surface initial internal fetch errors instead of retrying indefinitely - #4219

Open
cuishuang wants to merge 1 commit into
yewstack:masterfrom
cuishuang:master
Open

cuishuang wants to merge 1 commit into
yewstack:masterfrom
cuishuang:master

Conversation

@cuishuang

@cuishuang cuishuang commented Sep 7, 2026 •

Copy link
Copy Markdown

Description

Fix the client-side use_linked_state retry loop caused by initial LinkError::Internal results not being cached.

Initial fetches now cache all completed results, including infrastructure errors. This allows the suspension to resolve and exposes the error through LinkedStateHandle::as_result() instead of creating another identical request.

Refresh behavior remains unchanged: LinkError::Internal results from background refreshes are not cached, so an existing stale value is preserved.

Fixes #4218

Checklist

  • I have reviewed my own code
  • I have added tests

@github-actions

github-actions Bot commented Sep 7, 2026 •

Copy link
Copy Markdown

Visit the preview URL for this PR (updated for commit 1a9fd92):

https://yew-rs-api--pr4219-master-im3mvlbo.web.app

(expires Sat, 14 Nov 2026 11:51:35 GMT)

🔥 via Firebase Hosting GitHub Action 🌎

@github-actions

github-actions Bot commented Sep 7, 2026 •

Copy link
Copy Markdown

Size Comparison

Details
examples master (KB) pull request (KB) diff (KB) diff (%)
async_clock 100.169 100.169 0 0.000%
boids 164.983 164.983 0 0.000%
communication_child_to_parent 93.702 93.702 0 0.000%
communication_grandchild_with_grandparent 105.983 105.983 0 0.000%
communication_grandparent_to_grandchild 102.312 102.312 0 0.000%
communication_parent_to_child 91.099 91.099 0 0.000%
contexts 106.298 106.298 0 0.000%
counter 86.243 86.243 0 0.000%
counter_functional 88.285 88.285 0 0.000%
dyn_create_destroy_apps 90.153 90.153 0 0.000%
file_upload 98.562 98.562 0 0.000%
function_delayed_input 94.622 94.622 0 0.000%
function_memory_game 168.692 168.692 0 0.000%
function_router 401.547 401.547 0 0.000%
function_todomvc 164.991 164.991 0 0.000%
futures 236.843 236.843 0 0.000%
game_of_life 100.612 100.612 0 0.000%
immutable 259.449 259.449 0 0.000%
inner_html 80.784 80.784 0 0.000%
js_callback 109.766 109.766 0 0.000%
keyed_list 176.314 176.314 0 0.000%
mount_point 84.267 84.267 0 0.000%
nested_list 112.704 112.704 0 0.000%
node_refs 91.772 91.772 0 0.000%
password_strength 1767.894 1767.894 0 0.000%
portals 93.578 93.578 0 0.000%
router 367.774 367.774 0 0.000%
suspense 113.953 113.953 0 0.000%
timer 88.515 88.515 0 0.000%
timer_functional 99.403 99.403 0 0.000%
todomvc 142.016 142.016 0 0.000%
two_apps 86.185 86.185 0 0.000%
web_worker_fib 136.346 136.346 0 0.000%
web_worker_prime 186.646 186.646 0 0.000%
webgl 82.914 82.914 0 0.000%

✅ None of the examples has changed their size significantly.

Comment thread packages/yew-link/src/lib.rs Resolved
@cuishuang
cuishuang force-pushed the master branch 2 times, most recently from 603c1eb to 80cd486 Compare September 8, 2026 13:12
@cuishuang

Copy link
Copy Markdown
Author

@WorldSEnder Modified. Please review it again.

@WorldSEnder WorldSEnder left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Took a bit, since I am myself not familiar with this part of the library and was hoping Mado would chime in. So bear with and feel free to correct me too.

completed being an (unbounded) hashmap is I believe a memory leak (of all failed requests that never succeed). Keeping the old property of the LRU cache seems worthwhile.

I would phrase the property we want as follows: Every use of the use_linked_state hook has associated with it an input, call this the query. So when the input changes, the query does but it's the same hook. Every query should lead to at most one request to the backend (this is what I understand the bug is about). Multiple simultaneous queries from different uses of the hook for the same input should share the request (only one at best) - implemented by in_flight and refreshing. A query we already answered in SSR should lead to no request (subtly a different hook might have the same query without SSR, this can lead to a query request since cache is unreliable). And finally, memory should be proportional to the hooks used. After unmounting all, only a constant should be left, that's why LRU cache is used but HashMap is used only for things in flight.

The last property I think implies that two different hooks querying for the same input might not always share the result (which could have dropped out of the LRU already) and the hook needs to have a local cache, too.

EDIT: To come back to my earlier comment: each component should remember if a request failed and not retry again. The shared cache should be used only for successful requests.

Sorry this took so long, caching is hard.

@WorldSEnder

Copy link
Copy Markdown
Member

Note: prior art for request coalescing in rs-singleflight could be helpful.

@cuishuang

Copy link
Copy Markdown
Author

@WorldSEnder Thanks for spelling out the query lifetime and for the rs-singleflight pointer. I removed the shared completed map. Each hook now keeps the result for its current input, including failures, and the bounded LRU stores only successful values.

Initial fetches and explicit refreshes share one in-flight request per key. Hooks that join a request can still read its result after it leaves the in-flight map, even if the LRU entry has been evicted. SSR-prepared results are used directly, and an internal refresh error keeps the previous value visible.

I added a browser regression test for request coalescing, explicit retries, and LRU eviction. The updated PR is squashed into one commit and ready for another look.

This branch has not been deployed

No deployments
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.

use_linked_state retries indefinitely after an initial LinkError::Internal

2 participants