Skip to content

Settle the store back to loaded when a reconnect window receives only a stale-slot value#1740

Open
mcintyre94 wants to merge 1 commit into
rpc/sendable-subscribablefrom
reactive-store/settle-stale-reconnect
Open

Settle the store back to loaded when a reconnect window receives only a stale-slot value#1740
mcintyre94 wants to merge 1 commit into
rpc/sendable-subscribablefrom
reactive-store/settle-stale-reconnect

Conversation

@mcintyre94

@mcintyre94 mcintyre94 commented Jun 12, 2026

Copy link
Copy Markdown
Member

Problem

Spotted while reviewing the react stack, but the issue is in createReactiveStoreWithInitialValueAndSlotTracking

It persists the last seen slot across refreshes (good), and drops stale data. But there was a race condition: if a refresh happens, and the RPC returns outdated data on an older slot than previously seen, it gets dropped and the store stays stuck in a loading state until a subscription message happens (which might never happen).

Summary of Changes

Now in this case, we change the status to loaded, while keeping the previous newer data intact.

…ly a stale-slot value

`createReactiveStoreWithInitialValueAndSlotTracking` keeps `lastUpdateSlot` across `connect()` windows so the surfaced value never regresses to an older slot. The slot guard in `handleSlottedValue`, however, dropped a stale-slot response wholesale — value and status transition alike — so a successful-but-older response could not lift the store out of `loading`.

In practice this stranded a refresh: `useTrackedData`'s `refresh()` maps to `connect()` (not `reset()`), so `lastUpdateSlot` carries over. If the re-fetch is answered by a lagging RPC node at an older slot and the account is quiet (its subscription emits nothing), the store sits in `loading` indefinitely — a perpetual refresh spinner over retained stale data, with no error and no settle until the account next changes.

A stale-slot response now settles the store back to `loaded` while retaining the newer data it already holds, rather than adopting the older value. The monotonic-value guarantee is unchanged; only the liveness of the status transition is fixed. Adds tests covering the stale-slot reconnect via both the initial-value and stream sources, plus the subscriber notification on settle.

mcintyre94 commented Jun 12, 2026

Copy link
Copy Markdown
Member Author

@bundlemon

bundlemon Bot commented Jun 12, 2026

Copy link
Copy Markdown

BundleMon

Files updated (25)
Status Path Size Limits
react/dist/index.browser.mjs
5KB (+1.9KB +61.5%) -
react/dist/index.native.mjs
5KB (+1.9KB +61.52%) -
react/dist/index.node.mjs
5KB (+1.9KB +61.54%) -
@solana/kit production bundle
kit/dist/index.production.min.js
53.03KB (+429B +0.8%) -
kit/dist/index.browser.mjs
4.81KB (+349B +7.62%) -
kit/dist/index.native.mjs
4.81KB (+349B +7.63%) -
kit/dist/index.node.mjs
4.81KB (+349B +7.63%) -
errors/dist/index.browser.mjs
20.9KB (+179B +0.84%) -
errors/dist/index.native.mjs
20.9KB (+179B +0.84%) -
errors/dist/index.node.mjs
20.92KB (+179B +0.84%) -
wallet-account-signer/dist/index.browser.mjs
17.69KB (+169B +0.94%) -
wallet-account-signer/dist/index.native.mjs
17.69KB (+168B +0.94%) -
wallet-account-signer/dist/index.node.mjs
17.71KB (+168B +0.93%) -
subscribable/dist/index.browser.mjs
2.76KB (+160B +6%) -
subscribable/dist/index.native.mjs
2.77KB (+160B +5.99%) -
subscribable/dist/index.node.mjs
2.83KB (+158B +5.76%) -
rpc-types/dist/index.browser.mjs
1.9KB (+102B +5.53%) -
rpc-types/dist/index.native.mjs
1.9KB (+101B +5.48%) -
rpc-types/dist/index.node.mjs
1.9KB (+101B +5.48%) -
rpc-subscriptions-spec/dist/index.browser.mjs
2.19KB (-13B -0.58%) -
rpc-subscriptions-spec/dist/index.native.mjs
2.19KB (-13B -0.58%) -
rpc-subscriptions-spec/dist/index.node.mjs
2.23KB (-13B -0.56%) -
rpc-spec/dist/index.browser.mjs
898B (-20B -2.18%) -
rpc-spec/dist/index.native.mjs
897B (-21B -2.29%) -
rpc-spec/dist/index.node.mjs
896B (-21B -2.29%) -
Unchanged files (122)
Status Path Size Limits
rpc-graphql/dist/index.browser.mjs
18.82KB -
rpc-graphql/dist/index.native.mjs
18.81KB -
rpc-graphql/dist/index.node.mjs
18.81KB -
transaction-messages/dist/index.browser.mjs
11.32KB -
transaction-messages/dist/index.native.mjs
11.32KB -
transaction-messages/dist/index.node.mjs
11.32KB -
instruction-plans/dist/index.browser.mjs
6.58KB -
instruction-plans/dist/index.native.mjs
6.58KB -
instruction-plans/dist/index.node.mjs
6.58KB -
fixed-points/dist/index.browser.mjs
5.08KB -
fixed-points/dist/index.native.mjs
5.07KB -
fixed-points/dist/index.node.mjs
5.07KB -
codecs-data-structures/dist/index.browser.mjs
5.04KB -
codecs-data-structures/dist/index.native.mjs
5.03KB -
codecs-data-structures/dist/index.node.mjs
5.03KB -
offchain-messages/dist/index.browser.mjs
4.89KB -
offchain-messages/dist/index.native.mjs
4.89KB -
offchain-messages/dist/index.node.mjs
4.89KB -
transactions/dist/index.browser.mjs
4.07KB -
transactions/dist/index.native.mjs
4.07KB -
transactions/dist/index.node.mjs
4.07KB -
codecs-core/dist/index.browser.mjs
3.62KB -
codecs-core/dist/index.native.mjs
3.62KB -
codecs-core/dist/index.node.mjs
3.62KB -
webcrypto-ed25519-polyfill/dist/index.node.mj
s
3.61KB -
webcrypto-ed25519-polyfill/dist/index.browser
.mjs
3.59KB -
webcrypto-ed25519-polyfill/dist/index.native.
mjs
3.57KB -
rpc-subscriptions/dist/index.browser.mjs
3.37KB -
rpc-subscriptions/dist/index.node.mjs
3.34KB -
rpc-subscriptions/dist/index.native.mjs
3.31KB -
signers/dist/index.browser.mjs
3.26KB -
signers/dist/index.native.mjs
3.26KB -
signers/dist/index.node.mjs
3.26KB -
rpc-transformers/dist/index.browser.mjs
3.16KB -
rpc-transformers/dist/index.native.mjs
3.16KB -
rpc-transformers/dist/index.node.mjs
3.16KB -
keys/dist/index.node.mjs
3.06KB -
addresses/dist/index.browser.mjs
2.93KB -
addresses/dist/index.native.mjs
2.92KB -
addresses/dist/index.node.mjs
2.92KB -
keys/dist/index.browser.mjs
2.85KB -
keys/dist/index.native.mjs
2.85KB -
codecs-strings/dist/index.browser.mjs
2.55KB -
codecs-strings/dist/index.node.mjs
2.51KB -
codecs-strings/dist/index.native.mjs
2.47KB -
transaction-confirmation/dist/index.node.mjs
2.42KB -
transaction-confirmation/dist/index.native.mj
s
2.37KB -
sysvars/dist/index.browser.mjs
2.37KB -
sysvars/dist/index.native.mjs
2.37KB -
transaction-confirmation/dist/index.browser.m
js
2.37KB -
sysvars/dist/index.node.mjs
2.37KB -
rpc/dist/index.node.mjs
1.95KB -
codecs-numbers/dist/index.browser.mjs
1.95KB -
codecs-numbers/dist/index.native.mjs
1.95KB -
codecs-numbers/dist/index.node.mjs
1.94KB -
rpc-transport-http/dist/index.browser.mjs
1.89KB -
rpc-transport-http/dist/index.native.mjs
1.89KB -
rpc/dist/index.native.mjs
1.81KB -
rpc/dist/index.browser.mjs
1.8KB -
rpc-transport-http/dist/index.node.mjs
1.71KB -
rpc-subscriptions-channel-websocket/dist/inde
x.node.mjs
1.33KB -
rpc-subscriptions-channel-websocket/dist/inde
x.native.mjs
1.27KB -
rpc-subscriptions-channel-websocket/dist/inde
x.browser.mjs
1.26KB -
program-client-core/dist/index.browser.mjs
1.21KB -
program-client-core/dist/index.native.mjs
1.21KB -
program-client-core/dist/index.node.mjs
1.21KB -
options/dist/index.browser.mjs
1.18KB -
options/dist/index.native.mjs
1.18KB -
options/dist/index.node.mjs
1.17KB -
accounts/dist/index.browser.mjs
1.17KB -
accounts/dist/index.native.mjs
1.17KB -
accounts/dist/index.node.mjs
1.16KB -
rpc-api/dist/index.browser.mjs
998B -
rpc-api/dist/index.native.mjs
997B -
rpc-api/dist/index.node.mjs
995B -
compat/dist/index.browser.mjs
969B -
compat/dist/index.native.mjs
968B -
compat/dist/index.node.mjs
966B -
rpc-spec-types/dist/index.browser.mjs
962B -
rpc-spec-types/dist/index.native.mjs
961B -
rpc-spec-types/dist/index.node.mjs
959B -
rpc-subscriptions-api/dist/index.native.mjs
871B -
rpc-subscriptions-api/dist/index.browser.mjs
870B -
rpc-subscriptions-api/dist/index.node.mjs
870B -
promises/dist/index.native.mjs
841B -
promises/dist/index.node.mjs
840B -
promises/dist/index.browser.mjs
839B -
plugin-core/dist/index.browser.mjs
820B -
plugin-core/dist/index.native.mjs
819B -
plugin-core/dist/index.node.mjs
817B -
assertions/dist/index.browser.mjs
783B -
instructions/dist/index.browser.mjs
771B -
instructions/dist/index.native.mjs
770B -
instructions/dist/index.node.mjs
768B -
fast-stable-stringify/dist/index.browser.mjs
726B -
fast-stable-stringify/dist/index.native.mjs
725B -
assertions/dist/index.native.mjs
724B -
fast-stable-stringify/dist/index.node.mjs
724B -
assertions/dist/index.node.mjs
723B -
programs/dist/index.browser.mjs
329B -
programs/dist/index.native.mjs
327B -
programs/dist/index.node.mjs
325B -
fs-impl/dist/index.browser.mjs
245B -
event-target-impl/dist/index.node.mjs
230B -
functional/dist/index.browser.mjs
154B -
functional/dist/index.native.mjs
152B -
text-encoding-impl/dist/index.native.mjs
152B -
functional/dist/index.node.mjs
151B -
codecs/dist/index.browser.mjs
145B -
codecs/dist/index.native.mjs
144B -
codecs/dist/index.node.mjs
142B -
event-target-impl/dist/index.browser.mjs
133B -
ws-impl/dist/index.node.mjs
131B -
text-encoding-impl/dist/index.browser.mjs
122B -
fs-impl/dist/index.node.mjs
120B -
text-encoding-impl/dist/index.node.mjs
119B -
ws-impl/dist/index.browser.mjs
113B -
crypto-impl/dist/index.node.mjs
111B -
crypto-impl/dist/index.browser.mjs
109B -
rpc-parsed-types/dist/index.browser.mjs
66B -
rpc-parsed-types/dist/index.native.mjs
65B -
rpc-parsed-types/dist/index.node.mjs
63B -

Total files change +8.83KB +1.67%

Final result: ✅

View report in BundleMon website ➡️


Current branch size history | Target branch size history

@mcintyre94

Copy link
Copy Markdown
Member Author

@trevor-cortex

@trevor-cortex trevor-cortex 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.

Summary

Fixes a stuck-in-loading bug in createReactiveStoreWithInitialValueAndSlotTracking. lastUpdateSlot persists across connect() windows (deliberately, to prevent regression), but the previous code dropped stale-slot responses entirely — including the status transition. If a refresh window was answered only by a lagging RPC (stale slot) and the subscription stayed quiet, the store sat in loading forever.

The fix keeps the previous (newer) data and lastUpdateSlot, but still transitions the status back to loaded. The drop-the-value invariant is preserved; only the status now gets to escape loading.

Looks good to me — minimal, targeted, well-tested. Approving in spirit (no write access to formally approve).

Correctness notes

  • lastUpdateSlot is intentionally not updated in the stale branch — the surfaced value still cannot regress. ✅
  • currentState.data !== undefined guard is defensively correct. In practice, hitting slot < lastUpdateSlot with lastUpdateSlot > -1n implies a value was previously set, and reset() clears both together. The guard is belt-and-braces against any future path that might desync them — fine to keep.
  • error: undefined on the stale-loaded transition mirrors the fresh-slot path. If a prior connection window errored after loading data, a stale-but-successful response on the new window now clears the error and surfaces loaded — consistent with treating any successful response (even stale-slot) as recovery.
  • No spurious notifies on repeated stale arrivals: after the first stale → loaded, subsequent stale values hit setState's status/data/error equality short-circuit. ✅
  • First-ever value (lastUpdateSlot === -1n) can't take the stale branch since any non-negative slot is >= -1n. ✅

Tests

Good coverage: initial-value source stale path, stream source stale path, and subscriber-notification semantics on the loading → loaded settle. All three use jest.runAllTimersAsync() per the repo convention.

Changeset

Present, patch, scoped to @solana/kit. Appropriate for a bug fix.

For subsequent reviewers

  • Double-check whether you want the stale-slot path to clear a prior error (current behavior — I think it's right, but worth a sanity check given it's a behavior change beyond just unsticking loading).
  • The currentState.data !== undefined guard means a stale slot arriving while data is still undefined (e.g. a hypothetical future path where lastUpdateSlot gets bumped without setting data) would silently do nothing. Today that path doesn't exist, so this is fine.

@github-actions

Copy link
Copy Markdown
Contributor

Documentation Preview: https://kit-docs-4ve3r9gcg-anza-tech.vercel.app

@mcintyre94 mcintyre94 requested a review from lorisleiva June 12, 2026 18:38
@mcintyre94 mcintyre94 marked this pull request as ready for review June 12, 2026 18:38

@lorisleiva lorisleiva 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.

Good catch!

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.

3 participants