Skip to content

Add createAsyncGeneratorWithInitialValueAndSlotTracking#1536

Merged
mcintyre94 merged 5 commits into
mainfrom
async-gen-slot-track
Apr 9, 2026
Merged

Add createAsyncGeneratorWithInitialValueAndSlotTracking#1536
mcintyre94 merged 5 commits into
mainfrom
async-gen-slot-track

Conversation

@mcintyre94
Copy link
Copy Markdown
Member

Summary of Changes

This PR adds createAsyncGeneratorWithInitialValueAndSlotTracking. Same idea as the reactive store version, but presented as an async generator API.

It allows you to take any RPC request and subscription with a SolanaRpcResponse shape and that can map to the same value, and iterate over them while silently dropping any information from an older slot than has already been received.

While the store only needs to provide the most recent data, this requires us to queue all responses received (from either source) in order, where the slot is not older than the previous last seen slot.

Note that avoiding race conditions is mostly implicit on the JS single threaded runtime, and the fact that Promise then blocks are synchronous.

@mcintyre94 mcintyre94 requested a review from lorisleiva April 9, 2026 08:41
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 9, 2026

🦋 Changeset detected

Latest commit: 8856d01

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

This PR includes changesets to release 46 packages
Name Type
@solana/kit 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/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/plugin-interfaces Minor
@solana/program-client-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-subscriptions-api Minor
@solana/rpc-subscriptions-channel-websocket Minor
@solana/rpc-subscriptions-spec Minor
@solana/rpc-subscriptions 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-confirmation 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

@bundlemon
Copy link
Copy Markdown

bundlemon Bot commented Apr 9, 2026

BundleMon

Unchanged files (142)
Status Path Size Limits
@solana/kit production bundle
kit/dist/index.production.min.js
46.62KB -
errors/dist/index.node.mjs
19.38KB -
errors/dist/index.browser.mjs
19.36KB -
errors/dist/index.native.mjs
19.36KB -
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
16.43KB -
wallet-account-signer/dist/index.native.mjs
16.42KB -
wallet-account-signer/dist/index.browser.mjs
16.42KB -
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 -
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 -
kit/dist/index.browser.mjs
3.29KB -
kit/dist/index.native.mjs
3.29KB -
kit/dist/index.node.mjs
3.29KB -
rpc-transformers/dist/index.browser.mjs
3.16KB -
rpc-transformers/dist/index.native.mjs
3.16KB -
rpc-transformers/dist/index.node.mjs
3.16KB -
signers/dist/index.browser.mjs
3.15KB -
signers/dist/index.native.mjs
3.15KB -
signers/dist/index.node.mjs
3.15KB -
react/dist/index.browser.mjs
3.09KB -
react/dist/index.native.mjs
3.09KB -
react/dist/index.node.mjs
3.09KB -
addresses/dist/index.browser.mjs
2.93KB -
addresses/dist/index.native.mjs
2.92KB -
addresses/dist/index.node.mjs
2.92KB -
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 -
rpc-subscriptions-spec/dist/index.node.mjs
2.21KB -
rpc-subscriptions-spec/dist/index.native.mjs
2.17KB -
rpc-subscriptions-spec/dist/index.browser.mjs
2.16KB -
keys/dist/index.browser.mjs
2.07KB -
keys/dist/index.native.mjs
2.06KB -
keys/dist/index.node.mjs
2.06KB -
subscribable/dist/index.node.mjs
1.97KB -
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 -
subscribable/dist/index.native.mjs
1.92KB -
subscribable/dist/index.browser.mjs
1.91KB -
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/dist/index.browser.mjs
1.8KB -
rpc-transport-http/dist/index.node.mjs
1.72KB -
rpc-types/dist/index.browser.mjs
1.53KB -
rpc-types/dist/index.native.mjs
1.53KB -
rpc-types/dist/index.node.mjs
1.53KB -
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.browser.mjs
799B -
promises/dist/index.native.mjs
798B -
promises/dist/index.node.mjs
797B -
assertions/dist/index.browser.mjs
783B -
instructions/dist/index.browser.mjs
771B -
instructions/dist/index.native.mjs
770B -
instructions/dist/index.node.mjs
768B -
plugin-core/dist/index.browser.mjs
749B -
plugin-core/dist/index.native.mjs
749B -
plugin-core/dist/index.node.mjs
747B -
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 -
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
137B -
codecs/dist/index.native.mjs
136B -
codecs/dist/index.node.mjs
134B -
event-target-impl/dist/index.browser.mjs
133B -
ws-impl/dist/index.node.mjs
131B -
text-encoding-impl/dist/index.browser.mjs
122B -
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 -

No change in files bundle size

Final result: ✅

View report in BundleMon website ➡️


Current branch size history | Target branch size history

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 9, 2026

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

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 createAsyncGeneratorWithInitialValueAndSlotTracking, an async generator API that merges an initial RPC response with an ongoing subscription into a single stream, using slot numbers to silently drop out-of-order (stale) values. It's the async-iterator analogue of the existing createReactiveStoreWithInitialValueAndSlotTracking.

The implementation looks sound overall: single-threaded JS runtime + synchronous promise .then continuations mean the slot-comparison-and-update sequence in each source handler is race-free, which is the right intuition noted in the PR description. Test coverage is thorough — yield ordering, slot dropping from both directions, buffering, error propagation from each source, abort-signal plumbing, early abort, early break cleanup. Nice.

Things to watch out for

  1. Behavioral divergence from the reactive store when the subscription ends before the RPC resolves. The subscription's .then handler unconditionally sets done = true once the iterator completes, which terminates the generator and discards any RPC response still in flight (the later enqueue call is gated by if (done || signal.aborted) return). The reactive store doesn't do this — the RPC can still populate the store after the subscription ends. This isn't necessarily wrong (and the JSDoc does say "completes when the subscription ends"), but it's an easy edge case to miss, and there's no test for "subscription completes while RPC is still pending". Worth adding one to pin the intended semantics.

  2. Queue drain on abort. When the abort signal fires, onAbort sets done = true but leaves any already-buffered items in queue. If a consumer calls .next() after abort, they'll still receive buffered values before done is observed. The yields values received before abort, then completes test happens to exercise a similar path but collects the value before calling abort(), so the actual drain-after-abort behavior isn't asserted. Either is defensible, but please make the intent explicit with a test.

  3. Minor code smells (see inline comments): a redundant pendingError check, a waitingReject = null missing from one branch (benign — resolve-after-settle is a no-op), an unused reactive mock field, and an inconsistent as SolanaRpcResponse<TestValue> cast in the new test file vs the one being removed in the existing file in the same PR.

Notes for subsequent reviewers

  • The slot-ordering/race reasoning hinges on the fact that check lastUpdateSlotupdate lastUpdateSlotenqueue runs without any intervening await in each source handler. Worth double-checking that reasoning if anyone refactors this to introduce an await between those steps.
  • The subscription-completion semantics (point 1 above) is the main design question I'd want a second opinion on. Everything else is polish.

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.

Inline notes follow.

Comment thread packages/kit/src/create-async-generator-with-initial-value-and-slot-tracking.ts Outdated
@mcintyre94 mcintyre94 requested a review from trevor-cortex April 9, 2026 10:46
@mcintyre94 mcintyre94 force-pushed the async-gen-slot-track branch from e87866b to eb14b56 Compare April 9, 2026 11:38
@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.

Re-review

The main design concern from the previous review — premature termination when the subscription ends before the RPC resolves — has been fixed cleanly with the rpcDone/subscriptionDone flags and the markSourcesDone() helper. The waitingReject = null inconsistency is also fixed. 👍

However, two tests will hang under the new semantics because they were updated to not resolve the RPC but still expect the generator to complete after complete(). Details inline. These should fail / time out in CI — worth running the suite locally to confirm.

Things to watch out for

  • The two hanging tests (inline) are the main blocker.
  • Please add a test for the specific fix: subscription completes before RPC resolves, then RPC resolves, and the yielded values include the RPC response. This directly exercises the behavior change and would have caught the hanging-test issue too.
  • Minor: the reactive field on the mock is still unused, and the as SolanaRpcResponse<TestValue> cast in the test helper is still present even though the reactive store test removes its equivalent in this same PR.

For subsequent reviewers

  • Focus on the termination logic in the two .then(() => { ... markSourcesDone(); }) branches and the interaction with handleError / abort.
  • Verify that the test suite actually passes — the two tests I flagged look like they hang rather than fail loudly.

if (slot < lastUpdateSlot) continue;
lastUpdateSlot = slot;
enqueue({ context: { slot }, value: rpcSubscriptionValueMapper(value) });
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nice fix on the termination semantics — this correctly waits for both sources now and matches the reactive store behavior. Consider adding a dedicated test that exercises the specific case: subscription completes (with or without yielding), then RPC resolves later, and the RPC value is still yielded. That directly covers the behavior change and makes the intent explicit.

@anza-xyz anza-xyz deleted a comment from trevor-cortex Apr 9, 2026
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.

Jesus my brain hurts haha. Looks good though. That being said, I have more faith in Trevor than me on that one. 😅

Comment on lines +17 to +22
let resolve!: (response: SolanaRpcResponse<TestValue>) => void;
let reject!: (error: unknown) => void;
const promise = new Promise<SolanaRpcResponse<TestValue>>((res, rej) => {
resolve = res;
reject = rej;
});
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.

nit: you can use Promise.withResolvers here.

let { promise, resolve, reject } = Promise<SolanaRpcResponse<TestValue>>.withResolvers();

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Nice I forgot about that, thanks! I had to add lib: "ES2024" to make this work, so just making sure that works in CI 🤞

@mcintyre94 mcintyre94 enabled auto-merge April 9, 2026 15:25
@mcintyre94 mcintyre94 added this pull request to the merge queue Apr 9, 2026
Merged via the queue into main with commit cec688e Apr 9, 2026
13 checks passed
@mcintyre94 mcintyre94 deleted the async-gen-slot-track branch April 9, 2026 15:29
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 9, 2026

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

@github-actions
Copy link
Copy Markdown
Contributor

Because there has been no activity on this PR for 14 days since it was merged, it has been automatically locked. Please open a new issue if it requires a follow up.

@github-actions github-actions Bot locked as resolved and limited conversation to collaborators Apr 24, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants