Add framework-agnostic source types for reactive bindings#1606
Conversation
🦋 Changeset detectedLatest commit: 4085fd5 The changes in this PR will be included in the next version bump. This PR includes changesets to release 47 packages
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 |
This stack of pull requests is managed by Graphite. Learn more about stacking. |
BundleMonUnchanged files (147)
No change in files bundle size Final result: ✅ View report in BundleMon website ➡️ |
|
Documentation Preview: https://kit-docs-66x1ez9mv-anza-tech.vercel.app |
trevor-cortex
left a comment
There was a problem hiding this comment.
Summary
Adds two structural duck-types in @solana/subscribable that decouple reactive-framework bindings from the concrete PendingRpcRequest / PendingRpcSubscriptionsRequest producer types:
ReactiveStreamSource<T>— anything withreactiveStore({ abortSignal })returning aReactiveStreamStore<T>.ReactiveActionSource<T>— anything with a zero-argreactiveStore()returning aReactiveActionStore<[], T>.
Also promotes CreateReactiveStoreWithInitialValueAndSlotTrackingConfig from file-private to a public export so non-React consumers can name the config shape, and adds two compile-only typetests asserting the producer types satisfy the new duck-types.
The changes are purely additive types — no runtime impact, tree-shake-safe. Both producer types already happen to match the duck-types exactly, so the typetests should hold without any signature changes.
Things to watch out for
-
ReactiveStreamSource/ReactiveActionSourceare not re-exported from@solana/kit.packages/kit/src/index.tsdoesn't blanket re-export@solana/subscribable, and nothing in this PR adds these two types to the kit barrel. Given that the PR description positions them as the duck-types React hooks and plugin authors will consume — and plugin authors typically import from@solana/kit— this looks like a gap worth closing in this PR (or worth an explicit decision to require importing from@solana/subscribabledirectly). The@solana/kitminor bump in the changeset is still justified by the newly-exportedCreateReactiveStoreWithInitialValueAndSlotTrackingConfig, so this is about API surface, not the bump. -
Per-repo JSDoc convention.
CLAUDE.mdrequires@typeParamon exported generics and an@examplewhen helpful. The newReactiveStreamSource<T>/ReactiveActionSource<T>types have prose docblocks but no@typeParam Tand no@example. The promotedCreateReactiveStoreWithInitialValueAndSlotTrackingConfigis fully documented and is a good model. -
Inlined options shape.
ReactiveStreamSourceinlines{ abortSignal: AbortSignal }rather than reusingRpcSubscribeOptions. That's the right call here —@solana/subscribablemustn't depend on@solana/rpc-subscriptions-spec— but it does mean the duck-type and the producer share the shape only structurally. IfRpcSubscribeOptionsever grows a non-optional field, the duck-type silently won't match anymore. Not an issue today; worth keeping in mind.
Notes for subsequent reviewers
- Verify whether the intent is to expose these new types via
@solana/kit(likely yes, given the PR motivation). A one-line addition topackages/kit/src/index.tsre-exporting@solana/subscribable(or a targetedexport type { ... }) would do it. - The two new typetest files use the existing repo idiom (
null as unknown as X satisfies Y) and are correctly placed in packages that already depend on@solana/subscribable— nopackage.jsonchanges needed. - No runtime code paths are touched, so existing tests should be sufficient; the compile-time tests are the meaningful coverage here.
| /** | ||
| * Duck-type for objects that build a {@link ReactiveStreamStore} on demand via a | ||
| * `reactiveStore({ abortSignal })` method. Satisfied by `PendingRpcSubscriptionsRequest<T>`. | ||
| * Reactive-framework bindings (e.g. React's `useSubscription`) consume this duck-type so they | ||
| * don't have to name a concrete producer type. | ||
| * | ||
| * @see {@link ReactiveStreamStore} | ||
| * @see {@link ReactiveActionSource} | ||
| */ | ||
| export type ReactiveStreamSource<T> = { | ||
| reactiveStore(options: { abortSignal: AbortSignal }): ReactiveStreamStore<T>; |
There was a problem hiding this comment.
Per the repo's JSDoc convention (see CLAUDE.md → "TypeScript Docblocks"): exported generic types should carry @typeParam for each type parameter, and an @example where helpful. Suggest adding:
/**
* ...existing prose...
*
* @typeParam T - The notification value type emitted by the produced {@link ReactiveStreamStore}.
*
* @example
* ```ts
* function bindStream<T>(source: ReactiveStreamSource<T>, abortSignal: AbortSignal) {
* const store = source.reactiveStore({ abortSignal });
* return store.subscribe(() => { /* ... *\/ });
* }
* ```
*/(The promoted CreateReactiveStoreWithInitialValueAndSlotTrackingConfig in this same PR is a good model — fully tagged with @typeParam × 3.)
| // `PendingRpcSubscriptionsRequest<T>` is structurally assignable to `ReactiveStreamSource<T>` — | ||
| // the duck-type reactive-framework bindings consume so they don't have to name a concrete | ||
| // producer type. | ||
| null as unknown as PendingRpcSubscriptionsRequest<number> satisfies ReactiveStreamSource<number>; |
There was a problem hiding this comment.
Minor: as a stronger signal of intent, consider also asserting the inverse direction is not required — i.e. that a ReactiveStreamSource<T> is not assignable back to PendingRpcSubscriptionsRequest<T>. Something like:
// @ts-expect-error — the duck-type does not require `subscribe()` / `reactive()`.
null as unknown as ReactiveStreamSource<number> satisfies PendingRpcSubscriptionsRequest<number>;This pins down the "strictly broader" relationship and would catch a future change that accidentally tightens ReactiveStreamSource to require methods that only the concrete producer has. Not blocking — the existing assertion already covers the contract the bindings rely on.
Adds `ReactiveStreamSource<T>` and `ReactiveActionSource<T>` to `@solana/subscribable` as framework-agnostic duck-types over the existing `.reactiveStore()` methods on pending RPC requests and subscriptions. Reactive-framework bindings (React's `useSubscription` / `useRequest`, plus future Vue / Svelte / Solid bindings) consume these instead of naming concrete producer types, and plugin authors get a stable contract for their own pending-request objects. Also promotes `CreateReactiveStoreWithInitialValueAndSlotTrackingConfig` from a private alias in `@solana/kit` to a public export. `kit-plugin-rpc`'s upcoming spec builders (`createBalanceLiveData`, `createAccountLiveData`, `createTransactionConfirmationLiveData`) declare their return shape as `Omit<CreateReactiveStoreWithInitialValueAndSlotTrackingConfig<...>, 'abortSignal'>` so the consuming hook supplies the abort signal — letting kit-plugin-rpc avoid a dependency on `@solana/react`. Type-only additions; no runtime changes. Type tests in `@solana/rpc-spec` and `@solana/rpc-subscriptions-spec` confirm the structural assignability.
This is intentional - we're exporting these from |
|
🔎💬 Inkeep AI search and chat service is syncing content for source 'Solana Kit Docs' |

Summary of Changes
This PR adds some types that are useful for reactive UI consumers:
ReactiveStreamSource<T>— anything with areactiveStore({ abortSignal })method that returns aReactiveStreamStore<T>.PendingRpcSubscriptionsRequest<T>satisfies this by design.ReactiveActionSource<T>— anything with a zero-argumentreactiveStore()method that returns aReactiveActionStore<[], T>.PendingRpcRequest<T>satisfies this by design.These are intended to allow plugins to expose their own functionality to reactive UI acting on actions/streams, without having to fit the precise
PendingRpc[Subscriptions]Requesttypes. React UI hooks will use these types as input.CreateReactiveStoreWithInitialValueAndSlotTrackingConfigis now exported from@solana/kit, was private. This type is useful for plugins to return, egkit-plugin-rpccan provide a function that returns the config for auseBalancetype subscription in this shape, which can then be used with any reactive UI, without depending on@solana/react.