Skip to content

Add concurrent transaction plan leaf executor - #1995

Merged
mcintyre94 merged 1 commit into
mainfrom
feature/concurrent-transaction-plan-leaves
Aug 27, 2026
Merged

mcintyre94 merged 1 commit into
mainfrom
feature/concurrent-transaction-plan-leaves

Conversation

@mcintyre94

@mcintyre94 mcintyre94 commented Aug 26, 2026 •

Copy link
Copy Markdown
Member

This PR adds a new helper createTransactionPlanExecutorWithConcurrentLeaves as a sibling to createTransactionPlanExecutor.

This creates an intentionally much simpler executor, that ignores dependencies between transactions and concurrently executes the callback (slightly simplified) SingleTransactionPlan -> Promise<SingleTransactionPlanResult<TContext>> for every leaf. It returns a TransactionPlanResult with the same shape as the input TransactionPlan. It is intended for cases where dependencies are irrelevant, such as signing the transactions without executing them.

See also anza-xyz/kit-plugins#395 which first added a similar more specific version of this function. That PR will be refactored to use this function.

Failures are aggregated, similar to createTransactionPlanExecutor: if a callback throws for any leaf then this is returned as a FailedSingleTransactionPlanResult at that leaf, independently of other callbacks. Unlike createTransactionPlanExecutor, this does not cancel dependent leafs, as they are treated independently. If there are any failures then we throw SOLANA_ERROR__INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN with the plan.

@changeset-bot

changeset-bot Bot commented Aug 26, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ba9f75e

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

This PR includes changesets to release 48 packages
Name Type
@solana/instruction-plans Minor
@solana/kit Minor
@solana/plugin-interfaces Minor
@solana/program-client-core Minor
@solana/react 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/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/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-introspection 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

bundlemon Bot commented Aug 26, 2026 •

Copy link
Copy Markdown

BundleMon

Files updated (4)
Status Path Size Limits
✅ instruction-plans/dist/index.native.mjs
7.33KB (+281B +3.89%) -
✅ instruction-plans/dist/index.browser.mjs
7.33KB (+280B +3.87%) -
✅ instruction-plans/dist/index.node.mjs
7.33KB (+280B +3.87%) -
✅ @solana/kit production bundle
kit/dist/index.production.min.js
56.18KB (+179B +0.31%) -
Unchanged files (146)
Status Path Size Limits
✅ errors/dist/index.node.mjs
21.85KB -
✅ errors/dist/index.browser.mjs
21.82KB -
✅ errors/dist/index.native.mjs
21.82KB -
✅ rpc-graphql/dist/index.browser.mjs
18.87KB -
✅ rpc-graphql/dist/index.native.mjs
18.87KB -
✅ rpc-graphql/dist/index.node.mjs
18.86KB -
✅ wallet-account-signer/dist/index.node.mjs
18.53KB -
✅ wallet-account-signer/dist/index.native.mjs
18.51KB -
✅ wallet-account-signer/dist/index.browser.mjs
18.51KB -
✅ transaction-messages/dist/index.browser.mjs
11.34KB -
✅ transaction-messages/dist/index.native.mjs
11.34KB -
✅ transaction-messages/dist/index.node.mjs
11.34KB -
✅ react/dist/index.browser.mjs
5.32KB -
✅ react/dist/index.native.mjs
5.32KB -
✅ react/dist/index.node.mjs
5.32KB -
✅ codecs-data-structures/dist/index.browser.mjs
5.29KB -
✅ codecs-data-structures/dist/index.native.mjs
5.29KB -
✅ codecs-data-structures/dist/index.node.mjs
5.29KB -
✅ offchain-messages/dist/index.browser.mjs
5.25KB -
✅ offchain-messages/dist/index.native.mjs
5.24KB -
✅ offchain-messages/dist/index.node.mjs
5.24KB -
✅ fixed-points/dist/index.browser.mjs
5.08KB -
✅ fixed-points/dist/index.native.mjs
5.07KB -
✅ fixed-points/dist/index.node.mjs
5.07KB -
✅ kit/dist/index.browser.mjs
4.69KB -
✅ kit/dist/index.native.mjs
4.69KB -
✅ kit/dist/index.node.mjs
4.69KB -
✅ transactions/dist/index.browser.mjs
4.05KB -
✅ transactions/dist/index.native.mjs
4.05KB -
✅ transactions/dist/index.node.mjs
4.05KB -
✅ 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 -
✅ subscribable/dist/index.node.mjs
3.13KB -
✅ rpc-transformers/dist/index.browser.mjs
3.1KB -
✅ rpc-transformers/dist/index.native.mjs
3.1KB -
✅ rpc-transformers/dist/index.node.mjs
3.1KB -
✅ keys/dist/index.node.mjs
3.06KB -
✅ subscribable/dist/index.native.mjs
3.06KB -
✅ subscribable/dist/index.browser.mjs
3.05KB -
✅ 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 -
✅ transaction-introspection/dist/index.browser.
mjs
2.73KB -
✅ transaction-introspection/dist/index.native.m
js
2.73KB -
✅ transaction-introspection/dist/index.node.mjs
2.73KB -
✅ codecs-strings/dist/index.browser.mjs
2.64KB -
✅ codecs-strings/dist/index.node.mjs
2.5KB -
✅ 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 -
✅ sysvars/dist/index.node.mjs
2.37KB -
✅ transaction-confirmation/dist/index.browser.m
js
2.36KB -
✅ rpc-subscriptions-spec/dist/index.node.mjs
2.33KB -
✅ rpc-subscriptions-spec/dist/index.native.mjs
2.29KB -
✅ rpc-subscriptions-spec/dist/index.browser.mjs
2.29KB -
✅ 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/dist/index.node.mjs
1.94KB -
✅ rpc-types/dist/index.browser.mjs
1.9KB -
✅ rpc-types/dist/index.native.mjs
1.9KB -
✅ rpc-types/dist/index.node.mjs
1.9KB -
✅ rpc-transport-http/dist/index.browser.mjs
1.89KB -
✅ rpc-transport-http/dist/index.native.mjs
1.89KB -
✅ rpc/dist/index.native.mjs
1.8KB -
✅ rpc/dist/index.browser.mjs
1.79KB -
✅ rpc-transport-http/dist/index.node.mjs
1.71KB -
✅ rpc-spec-types/dist/index.browser.mjs
1.54KB -
✅ rpc-spec-types/dist/index.native.mjs
1.54KB -
✅ rpc-spec-types/dist/index.node.mjs
1.54KB -
✅ 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 -
✅ plugin-core/dist/index.browser.mjs
1.18KB -
✅ plugin-core/dist/index.native.mjs
1.18KB -
✅ plugin-core/dist/index.node.mjs
1.18KB -
✅ 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
1.04KB -
✅ rpc-api/dist/index.native.mjs
1.04KB -
✅ rpc-api/dist/index.node.mjs
1.04KB -
✅ compat/dist/index.browser.mjs
969B -
✅ compat/dist/index.native.mjs
968B -
✅ compat/dist/index.node.mjs
966B -
✅ rpc-spec/dist/index.browser.mjs
928B -
✅ rpc-spec/dist/index.native.mjs
928B -
✅ rpc-spec/dist/index.node.mjs
926B -
✅ promises/dist/index.native.mjs
841B -
✅ promises/dist/index.node.mjs
840B -
✅ promises/dist/index.browser.mjs
839B -
✅ rpc-subscriptions-api/dist/index.browser.mjs
810B -
✅ rpc-subscriptions-api/dist/index.native.mjs
808B -
✅ rpc-subscriptions-api/dist/index.node.mjs
807B -
✅ 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 +1020B +0.18%

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.

This PR adds createTransactionPlanExecutorWithConcurrentLeaves, a deliberately simple sibling to createTransactionPlanExecutor that maps a SingleTransactionPlan -> Promise<SingleTransactionPlanResult<TContext>> callback over every leaf concurrently, ignoring sequential dependencies, and rebuilds a TransactionPlanResult with the same nesting, order, and divisibility as the input plan. Intended for dependency-irrelevant operations like signing without executing (extracted from anza-xyz/kit-plugins#395).

The implementation is tidy and well-scoped — this would be an approval if I had the repo access to file one:

  • The recursive traversal handles all three plan kinds with an exhaustive kind satisfies never fallback throwing SOLANA_ERROR__INVARIANT_VIOLATION__INVALID_TRANSACTION_PLAN_KIND, consistent with the existing traverse function in the same file.
  • Unlike createTransactionPlanExecutor, this one supports non-divisible sequential plans and reconstructs them via nonDivisibleSequentialTransactionPlanResult — that's correct given dependencies are irrelevant here, and the first unit test covers it explicitly.
  • The abort signal is forwarded rather than enforced (no getAbortablePromise, no pre-flight aborted check). The docs are explicit that cancellation handling is the callback's responsibility, so I read this as intentional.
  • TSDoc is thorough and matches the actual behavior, including the @throws documentation for callback errors.
  • Tests cover shape preservation, concurrent start of sequential leaves, abort-signal forwarding, and the unknown-kind invariant. Type tests cover explicit and inferred TContext plus the callback signature and context obligation.
  • Changeset is present with a minor bump, which matches precedent for new public API in this repo.

Two things flagged inline:

  1. ConcurrentLeafTransactionPlanExecutorConfig is imported in the typetest but never used — likely a lint failure waiting to happen.
  2. A question on error semantics: a throwing leaf callback propagates the raw error via Promise.all, with no partial result shape (unlike the FAILED_TO_EXECUTE_TRANSACTION_PLAN error the sibling executor throws, which passthroughFailedTransactionPlanExecution understands). This looks intentional given callbacks are expected to return failed/canceled results rather than throw, but worth confirming.

For subsequent reviewers: the main thing to sanity-check is the error-propagation contract in point 2 above, especially how the kit-plugins#395 refactor will consume this — if that call site relies on passthroughFailedTransactionPlanExecution, callbacks there must never throw.

@mcintyre94
mcintyre94 marked this pull request as draft August 26, 2026 18:02
Transaction preparation workflows such as signing need to process all transaction plan leaves without enforcing sequential send dependencies. Add a concurrent-leaf executor that preserves result nesting, order, and divisibility while keeping result context caller-defined. Aggregate independent leaf failures into the complete result tree before throwing the standard execution error, and align preflight and in-flight abort behavior with the existing transaction plan executor. Cover traversal, failure aggregation, cancellation, and type inference behavior.
@mcintyre94
mcintyre94 force-pushed the feature/concurrent-transaction-plan-leaves branch from f9bff3a to ba9f75e Compare August 27, 2026 09:20
@mcintyre94

mcintyre94 commented Aug 27, 2026 •

Copy link
Copy Markdown
Member Author

I've aligned the failure aggregation behaviour, and the abort signal handling, with createTransactionPlanExecutor, and added symmetric tests.

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

Re-reviewed after the latest changes — both points from my previous review are resolved, and the error/abort semantics have been substantially strengthened. This would be an approval if I could file one.

What changed since last review:

  1. Unused import — resolved. ConcurrentLeafTransactionPlanExecutorConfig is now exercised by the new typetest block asserting that the config type has no default TContext (via @ts-expect-error).

  2. Error semantics — resolved, and better than what I asked for. Instead of propagating raw callback errors through Promise.all, thrown errors are now caught per-leaf and converted to FailedSingleTransactionPlanResults without affecting sibling leaves. After all leaves settle, any failed/canceled result in the tree produces a SOLANA_ERROR__INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN via createFailedToExecuteTransactionPlanError, carrying the complete result tree. This means passthroughFailedTransactionPlanExecution now works uniformly across both executors — which directly resolves my concern about the kit-plugins#395 refactor inheriting surprising failure behavior.

  3. Abort signal is now enforced, not just forwarded. Pre-aborted signals cancel leaves without invoking callbacks; mid-flight aborts race callbacks via getAbortablePromise and record failed results carrying the abort reason, while preserving already-completed results. The abortReason lands in the error context. TSDoc was updated to match.

Verification notes:

  • The failed-vs-canceled asymmetry (pre-abort → canceled, mid-flight abort → failed with the abort reason as the error) mirrors traverseSingle in the sibling executor exactly, is documented in the TSDoc, and is asserted by the abort tests — I read it as intentional and consistent.
  • The error as Error cast in the leaf catch matches the identical pre-existing pattern in traverseSingle; no new concern.
  • Test coverage is thorough: shape preservation, concurrent starts of sequential leaves, per-leaf failure aggregation with error.cause pointing at the first tree failure, waiting for all siblings to settle before throwing (the fake-timers assertion at 99ms/100ms is a nice touch), failed and canceled callback results, both abort paths, and the invariant-violation fallback.

One tiny observation, no action needed: there's a theoretical window where the signal aborts after every leaf has settled successfully but before the final aborted check, producing a FAILED_TO_EXECUTE error wrapping a fully successful result tree. The sibling executor has an equivalent window with its canceled flag, so behavior is consistent across the two — just noting it for completeness.

For subsequent reviewers: nothing outstanding from my side. The main thing left to verify is the kit-plugins#395 refactor consuming this cleanly, which the new aggregation contract should make straightforward.

@mcintyre94
mcintyre94 marked this pull request as ready for review August 27, 2026 10:20
@mcintyre94
mcintyre94 requested a review from lorisleiva August 27, 2026 10:20

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

Perfect, thank you!

@mcintyre94
mcintyre94 added this pull request to the merge queue Aug 27, 2026
Merged via the queue into main with commit 95a2788 Aug 27, 2026
12 checks passed
@mcintyre94
mcintyre94 deleted the feature/concurrent-transaction-plan-leaves branch August 27, 2026 10:54
@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 Sep 11, 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