Skip to content

Add sync reactiveStore() to pending RPC subscription#1553

Merged
mcintyre94 merged 1 commit into
mainfrom
subscription-reactive-store
May 7, 2026
Merged

Add sync reactiveStore() to pending RPC subscription#1553
mcintyre94 merged 1 commit into
mainfrom
subscription-reactive-store

Conversation

@mcintyre94
Copy link
Copy Markdown
Member

@mcintyre94 mcintyre94 commented Apr 21, 2026

Summary of Changes

The previous PR updated ReactiveStore , and added a new createReactiveStoreFromDataPublisherFactory function that takes a createDataPublisher(): Promise<DataPublisher>as input, instead of just DataPublisher. This enables retry functionality, where the publisher is re-created and re-subscribed to, without losing other store state.

But it also enables rpcSubscriptions.someNotification(...).reactive() to be made synchronous: previously it needed to create the DataPublisher asynchronously, now it can pass that promise to createReactiveStoreFromDataPublisherFactory. That function already handles the lifecycle of creating the DataPublisher from the factory.

This is a significantly better API for reactive consumers, as they have a synchronous store available immediately that will begin publishing data. They no longer need to handle the async store creation. The store's loading state covers both the DataPublisher being created and waiting for the first publish, and handles errors creating the DataPublisher. In react specifically, there's no need to awkwardly wrap the promise in a useEffect and handle extra state.

React example:

function SlotDisplay({ rpcSubscriptions }: Props) {
    const { store, controller } = useMemo(() => {
        const controller = new AbortController();
        const store = rpcSubscriptions
            .slotNotifications()
            .reactiveStore({ abortSignal: controller.signal });
        return { store, controller };
    }, [rpcSubscriptions]);

    useEffect(() => () => controller.abort(), [controller]);

    const state = useSyncExternalStore(store.subscribe, store.getUnifiedState);

    if (state.status === 'loading' || state.status === 'retrying') return <Spinner />;
    if (state.status === 'error') return <button onClick={store.retry}>Retry</button>;
    return <div>Slot: {state.data.slot}</div>;
}

Making reactive() synchronous would be a breaking change, so this PR adds .reactiveStore() as a sync method on pending RPC subscriptions. reactive() is deprecated.

TBH I think reactiveStore() might be the better name, but we could switch back to reactive() as a breaking change later if we want to.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 21, 2026

🦋 Changeset detected

Latest commit: 26ce0b0

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 47 packages
Name Type
@solana/rpc-subscriptions-spec Minor
@solana/kit Minor
@solana/plugin-interfaces Minor
@solana/rpc-subscriptions-api Minor
@solana/rpc-subscriptions-channel-websocket Minor
@solana/rpc-subscriptions Minor
@solana/program-client-core Minor
@solana/transaction-confirmation Minor
@solana/accounts Minor
@solana/addresses Minor
@solana/assertions Minor
@solana/codecs-core Minor
@solana/codecs-data-structures Minor
@solana/codecs-numbers Minor
@solana/codecs-strings Minor
@solana/codecs Minor
@solana/compat Minor
@solana/errors Minor
@solana/fast-stable-stringify Minor
@solana/fixed-points Minor
@solana/functional Minor
@solana/instruction-plans Minor
@solana/instructions Minor
@solana/keys Minor
@solana/nominal-types Minor
@solana/offchain-messages Minor
@solana/options Minor
@solana/plugin-core Minor
@solana/programs Minor
@solana/promises Minor
@solana/react Minor
@solana/rpc-api Minor
@solana/rpc-graphql Minor
@solana/rpc-parsed-types Minor
@solana/rpc-spec-types Minor
@solana/rpc-spec Minor
@solana/rpc-transformers Minor
@solana/rpc-transport-http Minor
@solana/rpc-types Minor
@solana/rpc Minor
@solana/signers Minor
@solana/subscribable Minor
@solana/sysvars Minor
@solana/transaction-messages Minor
@solana/transactions Minor
@solana/wallet-account-signer Minor
@solana/webcrypto-ed25519-polyfill Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link
Copy Markdown
Member Author

mcintyre94 commented Apr 21, 2026

@bundlemon
Copy link
Copy Markdown

bundlemon Bot commented Apr 21, 2026

BundleMon

Files updated (4)
Status Path Size Limits
@solana/kit production bundle
kit/dist/index.production.min.js
51.92KB (+170B +0.32%) -
rpc-subscriptions-spec/dist/index.native.mjs
2.2KB (+40B +1.8%) -
rpc-subscriptions-spec/dist/index.node.mjs
2.25KB (+40B +1.77%) -
rpc-subscriptions-spec/dist/index.browser.mjs
2.2KB (+37B +1.67%) -
Unchanged files (143)
Status Path Size Limits
errors/dist/index.node.mjs
20.54KB -
errors/dist/index.browser.mjs
20.52KB -
errors/dist/index.native.mjs
20.51KB -
rpc-graphql/dist/index.browser.mjs
18.82KB -
rpc-graphql/dist/index.native.mjs
18.81KB -
rpc-graphql/dist/index.node.mjs
18.81KB -
wallet-account-signer/dist/index.node.mjs
17.38KB -
wallet-account-signer/dist/index.browser.mjs
17.36KB -
wallet-account-signer/dist/index.native.mjs
17.36KB -
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 -
kit/dist/index.browser.mjs
3.97KB -
kit/dist/index.native.mjs
3.97KB -
kit/dist/index.node.mjs
3.97KB -
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 -
react/dist/index.browser.mjs
3.09KB -
react/dist/index.native.mjs
3.09KB -
react/dist/index.node.mjs
3.09KB -
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.41KB -
sysvars/dist/index.browser.mjs
2.37KB -
sysvars/dist/index.native.mjs
2.37KB -
sysvars/dist/index.node.mjs
2.37KB -
transaction-confirmation/dist/index.native.mj
s
2.36KB -
transaction-confirmation/dist/index.browser.m
js
2.35KB -
subscribable/dist/index.node.mjs
2.32KB -
subscribable/dist/index.native.mjs
2.26KB -
subscribable/dist/index.browser.mjs
2.26KB -
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.91KB -
rpc-transport-http/dist/index.native.mjs
1.9KB -
rpc/dist/index.native.mjs
1.81KB -
rpc-types/dist/index.browser.mjs
1.8KB -
rpc/dist/index.browser.mjs
1.8KB -
rpc-types/dist/index.native.mjs
1.8KB -
rpc-types/dist/index.node.mjs
1.8KB -
rpc-transport-http/dist/index.node.mjs
1.72KB -
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
976B -
rpc-api/dist/index.native.mjs
975B -
rpc-api/dist/index.node.mjs
973B -
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
870B -
rpc-subscriptions-api/dist/index.node.mjs
869B -
rpc-subscriptions-api/dist/index.browser.mjs
868B -
rpc-spec/dist/index.browser.mjs
852B -
rpc-spec/dist/index.native.mjs
851B -
rpc-spec/dist/index.node.mjs
850B -
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 +287B +0.05%

Final result: ✅

View report in BundleMon website ➡️


Current branch size history | Target branch size history

@mcintyre94 mcintyre94 force-pushed the subscription-reactive-store branch 3 times, most recently from 44cf49d to bd46726 Compare April 21, 2026 17:07
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 21, 2026

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

@mcintyre94
Copy link
Copy Markdown
Member Author

@trevor-cortex

Copy link
Copy Markdown

@trevor-cortex trevor-cortex left a comment

Choose a reason for hiding this comment

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

Summary

Adds a synchronous reactiveStore() method to PendingRpcSubscriptionsRequest alongside the existing (now @deprecated) async reactive(). The new method delegates to createReactiveStoreFromDataPublisherFactory from the prior PR, so consumers get a store they can use immediately — status: 'loading' covers publisher creation and waiting for the first notification, and retry() re-invokes the transport. This is a clear UX win for useSyncExternalStore and similar reactive primitives, where having to await a promise before you have a store is awkward.

The changeset correctly marks this as a minor bump on @solana/rpc-subscriptions-spec and @solana/kit. Since the repo is pre-1.0 and reactive() is only soft-deprecated, this is appropriately non-breaking.

Key things to watch for

  • reactive() is only JSDoc-deprecated, still fully functional. Downstream mocks in kit and transaction-confirmation add a no-op reactiveStore: jest.fn() purely to satisfy the interface — confirms nothing internal has migrated yet, which is fine for this PR.
  • Naming: as the author calls out, reactiveStore() is more descriptive than reactive(). Worth locking in the name before 1.0 — if the intent is eventually to rename back to reactive() as a breaking change, committing to one or the other now would avoid carrying both long-term.
  • Test relocation: the old rpc-subscriptions-reactive-test.ts was deleted and its suite moved verbatim into rpc-subscriptions-test.ts alongside the new reactiveStore() suite. Pure reorganization, no behavioural change.

Notes for subsequent reviewers

  • The reactiveStore() implementation in rpc-subscriptions.ts is a thin pass-through: it forwards the caller's abortSignal both to the factory (as the outer signal) and into the transport on each connect. Mirrors how reactive()/subscribe() already handle abort propagation.
  • The new test suite covers loading, notifications, error surfacing, and retry. Two gaps worth considering as follow-ups (not blocking): (1) a test that aborting the caller's signal actually stops the store from receiving further notifications, and (2) a test that getUnifiedState() returns a stable object reference between unrelated calls — that stability is explicitly documented as important for useSyncExternalStore.
  • flushMicrotasks() in the new test ticks twice — enough for the .then() inside connect() plus the factory resolution. A one-line comment explaining the two ticks would help future readers.

Overall shape LGTM.

Comment thread packages/rpc-subscriptions-spec/src/rpc-subscriptions-request.ts
Copy link
Copy Markdown
Member

@lorisleiva lorisleiva left a comment

Choose a reason for hiding this comment

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

Nice! I like that.

@mcintyre94 mcintyre94 force-pushed the reactive-store-updates branch from 4baf015 to 1d06dcc Compare April 23, 2026 09:39
@mcintyre94 mcintyre94 force-pushed the subscription-reactive-store branch from 57fd7dd to 330cffd Compare April 23, 2026 09:39
@mcintyre94 mcintyre94 force-pushed the reactive-store-updates branch from 1d06dcc to cb4c655 Compare May 7, 2026 13:25
@mcintyre94 mcintyre94 force-pushed the subscription-reactive-store branch from 330cffd to a9b4199 Compare May 7, 2026 13:25
Copy link
Copy Markdown
Member Author

mcintyre94 commented May 7, 2026

Merge activity

  • May 7, 2:56 PM UTC: A user started a stack merge that includes this pull request via Graphite.
  • May 7, 2:59 PM UTC: Graphite rebased this pull request as part of a merge.
  • May 7, 3:09 PM UTC: @mcintyre94 merged this pull request with Graphite.

@mcintyre94 mcintyre94 changed the base branch from reactive-store-updates to graphite-base/1553 May 7, 2026 14:57
@mcintyre94 mcintyre94 changed the base branch from graphite-base/1553 to main May 7, 2026 14:57
@mcintyre94 mcintyre94 force-pushed the subscription-reactive-store branch from a9b4199 to 26ce0b0 Compare May 7, 2026 14:58
@mcintyre94 mcintyre94 merged commit 15b610d into main May 7, 2026
14 checks passed
@mcintyre94 mcintyre94 deleted the subscription-reactive-store branch May 7, 2026 15:09
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 7, 2026

🔎💬 Inkeep AI search and chat service is syncing content for source 'Solana Kit Docs'

@github-actions github-actions Bot mentioned this pull request May 7, 2026
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