Skip to content

Commit d303d3d

Browse files
committed
Add RpcSendable / RpcSubscribable duck-types and loosen reactive-store config
Add `RpcSendable<T>` to `@solana/rpc-spec` and `RpcSubscribable<T>` to `@solana/rpc-subscriptions-spec` — structural duck-types covering just `send({ abortSignal })` and `subscribe({ abortSignal })` respectively. Both are intentionally narrower than the concrete `PendingRpcRequest<T>` / `PendingRpcSubscriptionsRequest<T>`, which also expose `reactiveStore()`. Use them at consumer boundaries to accept request-like objects without forcing producers to implement the full store-bearing shape. Loosens the `rpcRequest` and `rpcSubscriptionRequest` fields on `CreateReactiveStoreWithInitialValueAndSlotTrackingConfig` (exported) and the internal config for `createAsyncGeneratorWithInitialValueAndSlotTracking` to use the new types. Both primitives only ever call `.send()` and `.subscribe()` on their inputs, so the previous `Pending*Request` constraint was over-tight — existing callers passing concrete `rpc.getAccountInfo(addr)` / `rpcSubscriptions.accountNotifications(addr)` results still satisfy the loosened types structurally. Motivation: plugin-authored wrappers (caching layers, request batchers, request dedup) and test mocks no longer need to stub out an unused `reactiveStore()` method just to satisfy the type. Parallels the existing `ReactiveStreamSource<T>` / `ReactiveActionSource<T>` duck-types in `@solana/subscribable` — same pattern, applied to the producer side of one-shot and subscription requests.
1 parent 543f1d9 commit d303d3d

7 files changed

Lines changed: 66 additions & 41 deletions

.changeset/empty-deserts-happen.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
'@solana/rpc-spec': minor
3+
'@solana/rpc-subscriptions-spec': minor
4+
'@solana/kit': minor
5+
---
6+
7+
Add `RpcSendable<T>` and `RpcSubscribable<T>` structural duck-types alongside the concrete `PendingRpcRequest<T>` and `PendingRpcSubscriptionsRequest<T>`. Both new types are intentionally narrower — `RpcSendable<T>` covers just `send({ abortSignal })` and `RpcSubscribable<T>` covers just `subscribe({ abortSignal })` — so consumers (higher-level kit helpers, plugin-authored wrappers, test mocks) can accept request-like objects without taking on the full `{ reactiveStore, send }` / `{ reactiveStore, subscribe }` shape.
8+
9+
```ts
10+
import type { RpcSendable, RpcSubscribable } from '@solana/kit';
11+
12+
type RpcSendable<TResponse> = {
13+
send(options?: RpcSendOptions): Promise<TResponse>;
14+
};
15+
16+
type RpcSubscribable<TNotification> = {
17+
subscribe(options: RpcSubscribeOptions): Promise<AsyncIterable<TNotification>>;
18+
};
19+
```
20+
21+
`PendingRpcRequest<T>` still structurally satisfies `RpcSendable<T>`; `PendingRpcSubscriptionsRequest<T>` still satisfies `RpcSubscribable<T>`. No change at producer boundaries (`rpc.<method>(...)` / `rpcSubscriptions.<method>(...)` returns).
22+
23+
Loosens the `rpcRequest` / `rpcSubscriptionRequest` fields on `CreateReactiveStoreWithInitialValueAndSlotTrackingConfig` (exported) and the internal config for `createAsyncGeneratorWithInitialValueAndSlotTracking` from `PendingRpcRequest<...>` / `PendingRpcSubscriptionsRequest<...>` to `RpcSendable<...>` / `RpcSubscribable<...>` — both primitives only ever call `.send()` and `.subscribe()` on those inputs, so they now accept any compatible duck-type. Existing callers passing concrete `Pending*Request` objects continue to work.

packages/kit/src/__tests__/create-async-generator-with-initial-value-and-slot-tracking-test.ts

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,19 @@
1-
import type { PendingRpcRequest } from '@solana/rpc';
2-
import type { PendingRpcSubscriptionsRequest } from '@solana/rpc-subscriptions';
1+
import type { RpcSendable } from '@solana/rpc';
2+
import type { RpcSubscribable } from '@solana/rpc-subscriptions';
33
import type { SolanaRpcResponse } from '@solana/rpc-types';
44

55
import { createAsyncGeneratorWithInitialValueAndSlotTracking } from '../create-async-generator-with-initial-value-and-slot-tracking';
66

77
type TestValue = { count: number };
88

99
function createMockRpcRequest(): {
10-
mockRequest: PendingRpcRequest<SolanaRpcResponse<TestValue>>;
10+
mockRequest: RpcSendable<SolanaRpcResponse<TestValue>>;
1111
reject(error: unknown): void;
1212
resolve(response: SolanaRpcResponse<TestValue>): void;
1313
} {
1414
const { promise, resolve, reject } = Promise.withResolvers<SolanaRpcResponse<TestValue>>();
1515
return {
1616
mockRequest: {
17-
reactiveStore: jest.fn().mockImplementation(() => {
18-
throw new Error('not implemented');
19-
}),
2017
send: jest.fn().mockReturnValue(promise),
2118
},
2219
reject,
@@ -27,7 +24,7 @@ function createMockRpcRequest(): {
2724
function createMockSubscriptionRequest(): {
2825
complete(): void;
2926
error(err: unknown): void;
30-
mockRequest: PendingRpcSubscriptionsRequest<SolanaRpcResponse<TestValue>>;
27+
mockRequest: RpcSubscribable<SolanaRpcResponse<TestValue>>;
3128
pushNotification(notification: SolanaRpcResponse<TestValue>): void;
3229
} {
3330
const notifications: SolanaRpcResponse<TestValue>[] = [];
@@ -93,9 +90,6 @@ function createMockSubscriptionRequest(): {
9390
complete,
9491
error,
9592
mockRequest: {
96-
reactiveStore: jest.fn().mockImplementation(() => {
97-
throw new Error('not implemented');
98-
}),
9993
subscribe: jest.fn().mockResolvedValue(asyncIterable),
10094
},
10195
pushNotification,

packages/kit/src/__tests__/create-reactive-store-with-initial-value-and-slot-tracking-test.ts

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,19 @@
1-
import type { PendingRpcRequest } from '@solana/rpc';
2-
import type { PendingRpcSubscriptionsRequest } from '@solana/rpc-subscriptions';
1+
import type { RpcSendable } from '@solana/rpc';
2+
import type { RpcSubscribable } from '@solana/rpc-subscriptions';
33
import type { SolanaRpcResponse } from '@solana/rpc-types';
44

55
import { createReactiveStoreWithInitialValueAndSlotTracking } from '../create-reactive-store-with-initial-value-and-slot-tracking';
66

77
type TestValue = { count: number };
88

99
function createMockRpcRequest(): {
10-
mockRequest: PendingRpcRequest<SolanaRpcResponse<TestValue>>;
10+
mockRequest: RpcSendable<SolanaRpcResponse<TestValue>>;
1111
reject(error: unknown): void;
1212
resolve(response: SolanaRpcResponse<TestValue>): void;
1313
} {
1414
const { promise, resolve, reject } = Promise.withResolvers<SolanaRpcResponse<TestValue>>();
1515
return {
1616
mockRequest: {
17-
reactiveStore: jest.fn().mockImplementation(() => {
18-
throw new Error('not implemented');
19-
}),
2017
send: jest.fn().mockReturnValue(promise),
2118
},
2219
reject,
@@ -27,7 +24,7 @@ function createMockRpcRequest(): {
2724
function createMockSubscriptionRequest(): {
2825
complete(): void;
2926
error(err: unknown): void;
30-
mockRequest: PendingRpcSubscriptionsRequest<SolanaRpcResponse<TestValue>>;
27+
mockRequest: RpcSubscribable<SolanaRpcResponse<TestValue>>;
3128
pushNotification(notification: SolanaRpcResponse<TestValue>): void;
3229
} {
3330
const notifications: SolanaRpcResponse<TestValue>[] = [];
@@ -93,9 +90,6 @@ function createMockSubscriptionRequest(): {
9390
complete,
9491
error,
9592
mockRequest: {
96-
reactiveStore: jest.fn().mockImplementation(() => {
97-
throw new Error('not implemented');
98-
}),
9993
subscribe: jest.fn().mockResolvedValue(asyncIterable),
10094
},
10195
pushNotification,
@@ -627,20 +621,14 @@ describe('createReactiveStoreWithInitialValueAndSlotTracking', () => {
627621
error(err: unknown): void;
628622
pushNotification(notification: SolanaRpcResponse<TestValue>): void;
629623
}[] = [];
630-
const rpcRequest: PendingRpcRequest<SolanaRpcResponse<TestValue>> = {
631-
reactiveStore: jest.fn().mockImplementation(() => {
632-
throw new Error('not implemented');
633-
}),
624+
const rpcRequest: RpcSendable<SolanaRpcResponse<TestValue>> = {
634625
send: jest.fn().mockImplementation(() => {
635626
const { promise, resolve, reject } = Promise.withResolvers<SolanaRpcResponse<TestValue>>();
636627
rpcInstances.push({ reject, resolve });
637628
return promise;
638629
}),
639630
};
640-
const rpcSubscriptionRequest: PendingRpcSubscriptionsRequest<SolanaRpcResponse<TestValue>> = {
641-
reactiveStore: jest.fn().mockImplementation(() => {
642-
throw new Error('not implemented');
643-
}),
631+
const rpcSubscriptionRequest: RpcSubscribable<SolanaRpcResponse<TestValue>> = {
644632
subscribe: jest.fn().mockImplementation(() => {
645633
const instance = createMockSubscriptionRequest();
646634
subscriptionInstances.push({

packages/kit/src/create-async-generator-with-initial-value-and-slot-tracking.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { PendingRpcRequest } from '@solana/rpc';
2-
import type { PendingRpcSubscriptionsRequest } from '@solana/rpc-subscriptions';
1+
import type { RpcSendable } from '@solana/rpc';
2+
import type { RpcSubscribable } from '@solana/rpc-subscriptions';
33
import type { SolanaRpcResponse } from '@solana/rpc-types';
44

55
type CreateAsyncGeneratorWithInitialValueAndSlotTrackingConfig<TRpcValue, TSubscriptionValue, TItem> = Readonly<{
@@ -9,18 +9,18 @@ type CreateAsyncGeneratorWithInitialValueAndSlotTrackingConfig<TRpcValue, TSubsc
99
*/
1010
abortSignal: AbortSignal;
1111
/**
12-
* A pending RPC request whose response will be yielded as the generator's first value
12+
* A one-shot RPC request whose response will be yielded as the generator's first value
1313
* (unless a subscription notification with a newer slot arrives first).
1414
* The response must be a {@link SolanaRpcResponse} so that its slot can be compared with
1515
* subscription notifications.
1616
*/
17-
rpcRequest: PendingRpcRequest<SolanaRpcResponse<TRpcValue>>;
17+
rpcRequest: RpcSendable<SolanaRpcResponse<TRpcValue>>;
1818
/**
19-
* A pending RPC subscription request whose notifications will be yielded as they arrive.
19+
* An RPC subscription request whose notifications will be yielded as they arrive.
2020
* Each notification must be a {@link SolanaRpcResponse} so that its slot can be compared
2121
* with the initial RPC response and other notifications.
2222
*/
23-
rpcSubscriptionRequest: PendingRpcSubscriptionsRequest<SolanaRpcResponse<TSubscriptionValue>>;
23+
rpcSubscriptionRequest: RpcSubscribable<SolanaRpcResponse<TSubscriptionValue>>;
2424
/**
2525
* Maps the value from a subscription notification to the item type yielded by the generator.
2626
*/

packages/kit/src/create-reactive-store-with-initial-value-and-slot-tracking.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { PendingRpcRequest } from '@solana/rpc';
2-
import type { PendingRpcSubscriptionsRequest } from '@solana/rpc-subscriptions';
1+
import type { RpcSendable } from '@solana/rpc';
2+
import type { RpcSubscribable } from '@solana/rpc-subscriptions';
33
import type { SolanaRpcResponse } from '@solana/rpc-types';
44
import type { ReactiveState, ReactiveStreamStore } from '@solana/subscribable';
55

@@ -16,17 +16,17 @@ import type { ReactiveState, ReactiveStreamStore } from '@solana/subscribable';
1616
*/
1717
export type CreateReactiveStoreWithInitialValueAndSlotTrackingConfig<TRpcValue, TSubscriptionValue, TItem> = Readonly<{
1818
/**
19-
* A pending RPC request whose response will be used to set the store's initial state.
19+
* A one-shot RPC request whose response will be used to set the store's initial state.
2020
* The response must be a {@link SolanaRpcResponse} so that its slot can be compared with
2121
* subscription notifications.
2222
*/
23-
rpcRequest: PendingRpcRequest<SolanaRpcResponse<TRpcValue>>;
23+
rpcRequest: RpcSendable<SolanaRpcResponse<TRpcValue>>;
2424
/**
25-
* A pending RPC subscription request whose notifications will be used to keep the store
26-
* up to date. Each notification must be a {@link SolanaRpcResponse} so that its slot can be
25+
* An RPC subscription request whose notifications will be used to keep the store up to
26+
* date. Each notification must be a {@link SolanaRpcResponse} so that its slot can be
2727
* compared with the initial RPC response and other notifications.
2828
*/
29-
rpcSubscriptionRequest: PendingRpcSubscriptionsRequest<SolanaRpcResponse<TSubscriptionValue>>;
29+
rpcSubscriptionRequest: RpcSubscribable<SolanaRpcResponse<TSubscriptionValue>>;
3030
/**
3131
* Maps the value from a subscription notification to the item type stored in the reactive store.
3232
*/

packages/rpc-spec/src/rpc.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,16 @@ export type RpcSendOptions = Readonly<{
6767
abortSignal?: AbortSignal;
6868
}>;
6969

70+
/**
71+
* Structural duck-type for anything fireable via `send({ abortSignal })`. Satisfied by
72+
* {@link PendingRpcRequest | PendingRpcRequest<TResponse>}.
73+
*
74+
* @typeParam TResponse - The value `send()` resolves to.
75+
*/
76+
export type RpcSendable<TResponse> = {
77+
send(options?: RpcSendOptions): Promise<TResponse>;
78+
};
79+
7080
type PendingRpcRequestBuilder<TMethodImplementations> = UnionToIntersection<
7181
Flatten<{
7282
[P in keyof TMethodImplementations]: PendingRpcRequestReturnTypeMapper<TMethodImplementations[P]>;

packages/rpc-subscriptions-spec/src/rpc-subscriptions-request.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,13 @@ export type RpcSubscribeOptions = Readonly<{
5858
/** An `AbortSignal` to fire when you want to unsubscribe */
5959
abortSignal: AbortSignal;
6060
}>;
61+
62+
/**
63+
* Structural duck-type for anything subscribable via `subscribe({ abortSignal })`. Satisfied by
64+
* {@link PendingRpcSubscriptionsRequest | PendingRpcSubscriptionsRequest<TNotification>}.
65+
*
66+
* @typeParam TNotification - The notification type yielded by the returned async iterable.
67+
*/
68+
export type RpcSubscribable<TNotification> = {
69+
subscribe(options: RpcSubscribeOptions): Promise<AsyncIterable<TNotification>>;
70+
};

0 commit comments

Comments
 (0)