@solana/rpc-subscriptions-spec 6.3.1 → 6.3.2-canary-20260313112147

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solana/rpc-subscriptions-spec",
3
- "version": "6.3.1",
3
+ "version": "6.3.2-canary-20260313112147",
4
4
  "description": "A generic implementation of JSON RPC Subscriptions using proxies",
5
5
  "homepage": "https://www.solanakit.com/api#solanarpc-subscriptions-spec",
6
6
  "exports": {
@@ -33,7 +33,8 @@
33
33
  "types": "./dist/types/index.d.ts",
34
34
  "type": "commonjs",
35
35
  "files": [
36
- "./dist/"
36
+ "./dist/",
37
+ "./src/"
37
38
  ],
38
39
  "sideEffects": false,
39
40
  "keywords": [
@@ -55,10 +56,10 @@
55
56
  "maintained node versions"
56
57
  ],
57
58
  "dependencies": {
58
- "@solana/errors": "6.3.1",
59
- "@solana/promises": "6.3.1",
60
- "@solana/subscribable": "6.3.1",
61
- "@solana/rpc-spec-types": "6.3.1"
59
+ "@solana/errors": "6.3.2-canary-20260313112147",
60
+ "@solana/promises": "6.3.2-canary-20260313112147",
61
+ "@solana/rpc-spec-types": "6.3.2-canary-20260313112147",
62
+ "@solana/subscribable": "6.3.2-canary-20260313112147"
62
63
  },
63
64
  "peerDependencies": {
64
65
  "typescript": "^5.0.0"
package/src/index.ts ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * This package contains types that describe the implementation of the JSON RPC Subscriptions API,
3
+ * as well as methods to create one. It can be used standalone, but it is also exported as part of
4
+ * Kit [`@solana/kit`](https://github.com/anza-xyz/kit/tree/main/packages/kit).
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * const rpcSubscriptions =
9
+ * // Step 1 - Create a `RpcSubscriptions` instance. This may be stateful.
10
+ * createSolanaRpcSubscriptions(mainnet('wss://api.mainnet-beta.solana.com'));
11
+ * const response = await rpcSubscriptions
12
+ * // Step 2 - Call supported methods on it to produce `PendingRpcSubscriptionsRequest` objects.
13
+ * .slotNotifications({ commitment: 'confirmed' })
14
+ * // Step 3 - Call the `subscribe()` method on those pending requests to trigger them.
15
+ * .subscribe({ abortSignal: AbortSignal.timeout(10_000) });
16
+ * // Step 4 - Iterate over the result.
17
+ * try {
18
+ * for await (const slotNotification of slotNotifications) {
19
+ * console.log('Got a slot notification', slotNotification);
20
+ * }
21
+ * } catch (e) {
22
+ * console.error('The subscription closed unexpectedly', e);
23
+ * } finally {
24
+ * console.log('We have stopped listening for notifications');
25
+ * }
26
+ * ```
27
+ *
28
+ * @packageDocumentation
29
+ */
30
+ export * from './rpc-subscriptions-request';
31
+ export * from './rpc-subscriptions';
32
+ export * from './rpc-subscriptions-api';
33
+ export * from './rpc-subscriptions-channel';
34
+ export * from './rpc-subscriptions-pubsub-plan';
35
+ export * from './rpc-subscriptions-transport';
@@ -0,0 +1,168 @@
1
+ import { Callable, RpcRequest, RpcRequestTransformer } from '@solana/rpc-spec-types';
2
+ import { DataPublisher } from '@solana/subscribable';
3
+
4
+ import { RpcSubscriptionsChannel } from './rpc-subscriptions-channel';
5
+ import { RpcSubscriptionsTransportDataEvents } from './rpc-subscriptions-transport';
6
+
7
+ export type RpcSubscriptionsApiConfig<TApiMethods extends RpcSubscriptionsApiMethods> = Readonly<{
8
+ planExecutor: RpcSubscriptionsPlanExecutor<ReturnType<TApiMethods[keyof TApiMethods]>>;
9
+ /**
10
+ * An optional function that transforms the {@link RpcRequest} before it is sent to the JSON RPC
11
+ * server.
12
+ *
13
+ * This is useful when the params supplied by the caller need to be transformed before
14
+ * forwarding the message to the server. Use cases for this include applying defaults,
15
+ * forwarding calls to renamed methods, and serializing complex values.
16
+ */
17
+ requestTransformer?: RpcRequestTransformer;
18
+ }>;
19
+
20
+ /**
21
+ * A function that implements a protocol for subscribing and unsubscribing from notifications given
22
+ * a {@link RpcSubscriptionsChannel}, a {@link RpcRequest}, and an `AbortSignal`.
23
+ *
24
+ * @returns A {@link DataPublisher} that emits {@link RpcSubscriptionsTransportDataEvents}
25
+ */
26
+ type RpcSubscriptionsPlanExecutor<TNotification> = (
27
+ config: Readonly<{
28
+ channel: RpcSubscriptionsChannel<unknown, unknown>;
29
+ request: RpcRequest;
30
+ signal: AbortSignal;
31
+ }>,
32
+ ) => Promise<DataPublisher<RpcSubscriptionsTransportDataEvents<TNotification>>>;
33
+
34
+ /**
35
+ * This type allows an {@link RpcSubscriptionsApi} to describe how a particular subscription should
36
+ * be issued to the JSON RPC server.
37
+ *
38
+ * Given a function that was called on a {@link RpcSubscriptions}, this object exposes an `execute`
39
+ * function that dictates which subscription request will be sent, how the underlying transport will
40
+ * be used, and how the notifications will be transformed.
41
+ *
42
+ * This function accepts a {@link RpcSubscriptionsChannel} and an `AbortSignal` and asynchronously
43
+ * returns a {@link DataPublisher}. This gives us the opportunity to:
44
+ *
45
+ * - define the `payload` from the requested method name and parameters before passing it to the
46
+ * channel.
47
+ * - call the underlying channel zero, one or multiple times depending on the use-case (e.g.
48
+ * caching or coalescing multiple subscriptions).
49
+ * - transform the notification from the JSON RPC server, in case it does not match the
50
+ * `TNotification` specified by the
51
+ * {@link PendingRpcSubscriptionsRequest | PendingRpcSubscriptionsRequest<TNotification>} emitted
52
+ * from the publisher returned.
53
+ */
54
+ export type RpcSubscriptionsPlan<TNotification> = Readonly<{
55
+ /**
56
+ * This method may be called with a newly-opened channel or a pre-established channel.
57
+ */
58
+ execute: (
59
+ config: Readonly<{
60
+ channel: RpcSubscriptionsChannel<unknown, unknown>;
61
+ signal: AbortSignal;
62
+ }>,
63
+ ) => Promise<DataPublisher<RpcSubscriptionsTransportDataEvents<TNotification>>>;
64
+ /**
65
+ * This request is used to uniquely identify the subscription.
66
+ * It typically comes from the method name and parameters of the subscription call,
67
+ * after potentially being transformed by the RPC Subscriptions API.
68
+ */
69
+ request: RpcRequest;
70
+ }>;
71
+
72
+ /**
73
+ * For each of `TRpcSubscriptionsMethods`, this object exposes a method with the same name that maps
74
+ * between its input arguments and a
75
+ * {@link RpcSubscriptionsPlan | RpcSubscriptionsPlan<TNotification>} that implements the execution
76
+ * of a JSON RPC subscription for `TNotifications`.
77
+ */
78
+ export type RpcSubscriptionsApi<TRpcSubscriptionMethods> = {
79
+ [MethodName in keyof TRpcSubscriptionMethods]: RpcSubscriptionsReturnTypeMapper<
80
+ TRpcSubscriptionMethods[MethodName]
81
+ >;
82
+ };
83
+
84
+ type RpcSubscriptionsReturnTypeMapper<TRpcMethod> = TRpcMethod extends Callable
85
+ ? (...rawParams: unknown[]) => RpcSubscriptionsPlan<ReturnType<TRpcMethod>>
86
+ : never;
87
+
88
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
89
+ type RpcSubscriptionsApiMethod = (...args: any) => any;
90
+ export interface RpcSubscriptionsApiMethods {
91
+ [methodName: string]: RpcSubscriptionsApiMethod;
92
+ }
93
+
94
+ /**
95
+ * Creates a JavaScript proxy that converts _any_ function call called on it to a
96
+ * {@link RpcSubscriptionsPlan} by creating an `execute` function that:
97
+ *
98
+ * - calls the supplied {@link RpcSubscriptionsApiConfig.planExecutor} with a JSON RPC v2 payload
99
+ * object with the requested `methodName` and `params` properties, optionally transformed by
100
+ * {@link RpcSubscriptionsApiConfig.requestTransformer}.
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * // For example, given this `RpcSubscriptionsApi`:
105
+ * const rpcSubscriptionsApi = createJsonRpcSubscriptionsApi({
106
+ * async planExecutor({ channel, request }) {
107
+ * await channel.send(request);
108
+ * return {
109
+ * ...channel,
110
+ * on(type, listener, options) {
111
+ * if (type !== 'message') {
112
+ * return channel.on(type, listener, options);
113
+ * }
114
+ * return channel.on(
115
+ * 'message',
116
+ * function resultGettingListener(message) {
117
+ * listener(message.result);
118
+ * },
119
+ * options,
120
+ * );
121
+ * }
122
+ * }
123
+ * },
124
+ * requestTransformer: (...rawParams) => rawParams.reverse(),
125
+ * });
126
+ *
127
+ * // ...the following function call:
128
+ * rpcSubscriptionsApi.foo('bar', { baz: 'bat' });
129
+ *
130
+ * // ...will produce a `RpcSubscriptionsPlan` that:
131
+ * // - Uses the following payload: { id: 1, jsonrpc: '2.0', method: 'foo', params: [{ baz: 'bat' }, 'bar'] }.
132
+ * // - Emits the "result" property of each RPC Subscriptions message.
133
+ * ```
134
+ */
135
+ export function createRpcSubscriptionsApi<TRpcSubscriptionsApiMethods extends RpcSubscriptionsApiMethods>(
136
+ config: RpcSubscriptionsApiConfig<TRpcSubscriptionsApiMethods>,
137
+ ): RpcSubscriptionsApi<TRpcSubscriptionsApiMethods> {
138
+ return new Proxy({} as RpcSubscriptionsApi<TRpcSubscriptionsApiMethods>, {
139
+ defineProperty() {
140
+ return false;
141
+ },
142
+ deleteProperty() {
143
+ return false;
144
+ },
145
+ get<TNotificationName extends keyof RpcSubscriptionsApi<TRpcSubscriptionsApiMethods>>(
146
+ ...args: Parameters<NonNullable<ProxyHandler<RpcSubscriptionsApi<TRpcSubscriptionsApiMethods>>['get']>>
147
+ ) {
148
+ const [_, p] = args;
149
+ const methodName = p.toString() as keyof TRpcSubscriptionsApiMethods as string;
150
+ return function (
151
+ ...params: Parameters<
152
+ TRpcSubscriptionsApiMethods[TNotificationName] extends CallableFunction
153
+ ? TRpcSubscriptionsApiMethods[TNotificationName]
154
+ : never
155
+ >
156
+ ): RpcSubscriptionsPlan<ReturnType<TRpcSubscriptionsApiMethods[TNotificationName]>> {
157
+ const rawRequest = { methodName, params };
158
+ const request = config.requestTransformer ? config.requestTransformer(rawRequest) : rawRequest;
159
+ return {
160
+ execute(planConfig) {
161
+ return config.planExecutor({ ...planConfig, request });
162
+ },
163
+ request,
164
+ };
165
+ };
166
+ },
167
+ });
168
+ }
@@ -0,0 +1,101 @@
1
+ import {
2
+ SOLANA_ERROR__RPC_SUBSCRIPTIONS__CHANNEL_CLOSED_BEFORE_MESSAGE_BUFFERED,
3
+ SOLANA_ERROR__RPC_SUBSCRIPTIONS__CHANNEL_CONNECTION_CLOSED,
4
+ SOLANA_ERROR__RPC_SUBSCRIPTIONS__CHANNEL_FAILED_TO_CONNECT,
5
+ SolanaError,
6
+ } from '@solana/errors';
7
+ import { DataPublisher } from '@solana/subscribable';
8
+
9
+ type RpcSubscriptionsChannelSolanaErrorCode =
10
+ | typeof SOLANA_ERROR__RPC_SUBSCRIPTIONS__CHANNEL_CLOSED_BEFORE_MESSAGE_BUFFERED
11
+ | typeof SOLANA_ERROR__RPC_SUBSCRIPTIONS__CHANNEL_CONNECTION_CLOSED
12
+ | typeof SOLANA_ERROR__RPC_SUBSCRIPTIONS__CHANNEL_FAILED_TO_CONNECT;
13
+
14
+ export type RpcSubscriptionChannelEvents<TInboundMessage> = {
15
+ /**
16
+ * Fires when the channel closes unexpectedly.
17
+ * @eventProperty
18
+ */
19
+ error: SolanaError<RpcSubscriptionsChannelSolanaErrorCode>;
20
+ /**
21
+ * Fires on every message received from the remote end.
22
+ * @eventProperty
23
+ */
24
+ message: TInboundMessage;
25
+ };
26
+
27
+ /**
28
+ * A {@link DataPublisher} on which you can subscribe to events of type
29
+ * {@link RpcSubscriptionChannelEvents | RpcSubscriptionChannelEvents<TInboundMessage>}.
30
+ * Additionally, you can use this object to send messages of type `TOutboundMessage` back to the
31
+ * remote end by calling its {@link RpcSubscriptionsChannel.send | `send(message)`} method.
32
+ */
33
+ export interface RpcSubscriptionsChannel<TOutboundMessage, TInboundMessage> extends DataPublisher<
34
+ RpcSubscriptionChannelEvents<TInboundMessage>
35
+ > {
36
+ send(message: TOutboundMessage): Promise<void>;
37
+ }
38
+
39
+ /**
40
+ * A channel creator is a function that accepts an `AbortSignal`, returns a new
41
+ * {@link RpcSubscriptionsChannel}, and tears down the channel when the abort signal fires.
42
+ */
43
+ export type RpcSubscriptionsChannelCreator<TOutboundMessage, TInboundMessage> = (
44
+ config: Readonly<{
45
+ abortSignal: AbortSignal;
46
+ }>,
47
+ ) => Promise<RpcSubscriptionsChannel<TOutboundMessage, TInboundMessage>>;
48
+
49
+ /**
50
+ * Given a channel with inbound messages of type `T` and a function of type `T => U`, returns a new
51
+ * channel with inbound messages of type `U`.
52
+ *
53
+ * Note that this only affects messages of type `"message"` and thus, does not affect incoming error
54
+ * messages.
55
+ *
56
+ * @example Parsing incoming JSON messages
57
+ * ```ts
58
+ * const transformedChannel = transformChannelInboundMessages(channel, JSON.parse);
59
+ * ```
60
+ */
61
+ export function transformChannelInboundMessages<TOutboundMessage, TNewInboundMessage, TInboundMessage>(
62
+ channel: RpcSubscriptionsChannel<TOutboundMessage, TInboundMessage>,
63
+ transform: (message: TInboundMessage) => TNewInboundMessage,
64
+ ): RpcSubscriptionsChannel<TOutboundMessage, TNewInboundMessage> {
65
+ return Object.freeze<RpcSubscriptionsChannel<TOutboundMessage, TNewInboundMessage>>({
66
+ ...channel,
67
+ on(type, subscriber, options) {
68
+ if (type !== 'message') {
69
+ return channel.on(
70
+ type,
71
+ subscriber as (data: RpcSubscriptionChannelEvents<TInboundMessage>[typeof type]) => void,
72
+ options,
73
+ );
74
+ }
75
+ return channel.on(
76
+ 'message',
77
+ message => (subscriber as (data: TNewInboundMessage) => void)(transform(message)),
78
+ options,
79
+ );
80
+ },
81
+ });
82
+ }
83
+
84
+ /**
85
+ * Given a channel with outbound messages of type `T` and a function of type `U => T`, returns a new
86
+ * channel with outbound messages of type `U`.
87
+ *
88
+ * @example Stringifying JSON messages before sending them over the wire
89
+ * ```ts
90
+ * const transformedChannel = transformChannelOutboundMessages(channel, JSON.stringify);
91
+ * ```
92
+ */
93
+ export function transformChannelOutboundMessages<TNewOutboundMessage, TOutboundMessage, TInboundMessage>(
94
+ channel: RpcSubscriptionsChannel<TOutboundMessage, TInboundMessage>,
95
+ transform: (message: TNewOutboundMessage) => TOutboundMessage,
96
+ ): RpcSubscriptionsChannel<TNewOutboundMessage, TInboundMessage> {
97
+ return Object.freeze<RpcSubscriptionsChannel<TNewOutboundMessage, TInboundMessage>>({
98
+ ...channel,
99
+ send: message => channel.send(transform(message)),
100
+ });
101
+ }
@@ -0,0 +1,241 @@
1
+ import {
2
+ getSolanaErrorFromJsonRpcError,
3
+ SOLANA_ERROR__INVARIANT_VIOLATION__DATA_PUBLISHER_CHANNEL_UNIMPLEMENTED,
4
+ SOLANA_ERROR__RPC_SUBSCRIPTIONS__EXPECTED_SERVER_SUBSCRIPTION_ID,
5
+ SolanaError,
6
+ } from '@solana/errors';
7
+ import { AbortController } from '@solana/event-target-impl';
8
+ import { safeRace } from '@solana/promises';
9
+ import { createRpcMessage, RpcRequest, RpcResponseData, RpcResponseTransformer } from '@solana/rpc-spec-types';
10
+ import { DataPublisher } from '@solana/subscribable';
11
+ import { demultiplexDataPublisher } from '@solana/subscribable';
12
+
13
+ import { RpcSubscriptionChannelEvents } from './rpc-subscriptions-channel';
14
+ import { RpcSubscriptionsChannel } from './rpc-subscriptions-channel';
15
+
16
+ type Config<TNotification> = Readonly<{
17
+ channel: RpcSubscriptionsChannel<unknown, RpcNotification<TNotification> | RpcResponseData<RpcSubscriptionId>>;
18
+ responseTransformer?: RpcResponseTransformer;
19
+ signal: AbortSignal;
20
+ subscribeRequest: RpcRequest;
21
+ unsubscribeMethodName: string;
22
+ }>;
23
+
24
+ type RpcNotification<TNotification> = Readonly<{
25
+ method: string;
26
+ params: Readonly<{
27
+ result: TNotification;
28
+ subscription: number;
29
+ }>;
30
+ }>;
31
+
32
+ type RpcSubscriptionId = number;
33
+
34
+ type RpcSubscriptionNotificationEvents<TNotification> = Omit<RpcSubscriptionChannelEvents<TNotification>, 'message'> & {
35
+ notification: TNotification;
36
+ };
37
+
38
+ const subscriberCountBySubscriptionIdByChannel = new WeakMap<WeakKey, Record<number, number>>();
39
+ function decrementSubscriberCountAndReturnNewCount(channel: WeakKey, subscriptionId?: number): number | undefined {
40
+ return augmentSubscriberCountAndReturnNewCount(-1, channel, subscriptionId);
41
+ }
42
+ function incrementSubscriberCount(channel: WeakKey, subscriptionId?: number): void {
43
+ augmentSubscriberCountAndReturnNewCount(1, channel, subscriptionId);
44
+ }
45
+ function getSubscriberCountBySubscriptionIdForChannel(channel: WeakKey): Record<number, number> {
46
+ let subscriberCountBySubscriptionId = subscriberCountBySubscriptionIdByChannel.get(channel);
47
+ if (!subscriberCountBySubscriptionId) {
48
+ subscriberCountBySubscriptionIdByChannel.set(channel, (subscriberCountBySubscriptionId = {}));
49
+ }
50
+ return subscriberCountBySubscriptionId;
51
+ }
52
+ function augmentSubscriberCountAndReturnNewCount(
53
+ amount: -1 | 1,
54
+ channel: WeakKey,
55
+ subscriptionId?: number,
56
+ ): number | undefined {
57
+ if (subscriptionId === undefined) {
58
+ return;
59
+ }
60
+ const subscriberCountBySubscriptionId = getSubscriberCountBySubscriptionIdForChannel(channel);
61
+ if (!subscriberCountBySubscriptionId[subscriptionId] && amount > 0) {
62
+ subscriberCountBySubscriptionId[subscriptionId] = 0;
63
+ }
64
+ const newCount = amount + subscriberCountBySubscriptionId[subscriptionId];
65
+ if (newCount <= 0) {
66
+ delete subscriberCountBySubscriptionId[subscriptionId];
67
+ } else {
68
+ subscriberCountBySubscriptionId[subscriptionId] = newCount;
69
+ }
70
+ return newCount;
71
+ }
72
+
73
+ const cache = new WeakMap();
74
+ function getMemoizedDemultiplexedNotificationPublisherFromChannelAndResponseTransformer<TNotification>(
75
+ channel: RpcSubscriptionsChannel<unknown, RpcNotification<TNotification>>,
76
+ subscribeRequest: RpcRequest,
77
+ responseTransformer?: RpcResponseTransformer,
78
+ ): DataPublisher<{
79
+ [channelName: `notification:${number}`]: TNotification;
80
+ }> {
81
+ let publisherByResponseTransformer = cache.get(channel);
82
+ if (!publisherByResponseTransformer) {
83
+ cache.set(channel, (publisherByResponseTransformer = new WeakMap()));
84
+ }
85
+ const responseTransformerKey = responseTransformer ?? channel;
86
+ let publisher = publisherByResponseTransformer.get(responseTransformerKey);
87
+ if (!publisher) {
88
+ publisherByResponseTransformer.set(
89
+ responseTransformerKey,
90
+ (publisher = demultiplexDataPublisher(channel, 'message', rawMessage => {
91
+ const message = rawMessage as RpcNotification<unknown> | RpcResponseData<unknown>;
92
+ if (!('method' in message)) {
93
+ return;
94
+ }
95
+ const transformedNotification = responseTransformer
96
+ ? responseTransformer(message.params.result, subscribeRequest)
97
+ : message.params.result;
98
+ return [`notification:${message.params.subscription}`, transformedNotification];
99
+ })),
100
+ );
101
+ }
102
+ return publisher;
103
+ }
104
+
105
+ /**
106
+ * Given a channel, this function executes the particular subscription plan required by the Solana
107
+ * JSON RPC Subscriptions API.
108
+ *
109
+ * @param config
110
+ *
111
+ * 1. Calls the `subscribeRequest` on the remote RPC
112
+ * 2. Waits for a response containing the subscription id
113
+ * 3. Returns a {@link DataPublisher} that publishes notifications related to that subscriptions id,
114
+ * filtering out all others
115
+ * 4. Calls the `unsubscribeMethodName` on the remote RPC when the abort signal is fired.
116
+ */
117
+ export async function executeRpcPubSubSubscriptionPlan<TNotification>({
118
+ channel,
119
+ responseTransformer,
120
+ signal,
121
+ subscribeRequest,
122
+ unsubscribeMethodName,
123
+ }: Config<TNotification>): Promise<DataPublisher<RpcSubscriptionNotificationEvents<TNotification>>> {
124
+ let subscriptionId: number | undefined;
125
+ channel.on(
126
+ 'error',
127
+ () => {
128
+ // An error on the channel indicates that the subscriptions are dead.
129
+ // There is no longer any sense hanging on to subscription ids.
130
+ // Erasing it here will prevent the unsubscribe code from running.
131
+ subscriptionId = undefined;
132
+ subscriberCountBySubscriptionIdByChannel.delete(channel);
133
+ },
134
+ { signal },
135
+ );
136
+ /**
137
+ * STEP 1
138
+ * Create a promise that rejects if this subscription is aborted and sends
139
+ * the unsubscribe message if the subscription is active at that time.
140
+ */
141
+ const abortPromise = new Promise<never>((_, reject) => {
142
+ function handleAbort(this: AbortSignal) {
143
+ /**
144
+ * Because of https://github.com/solana-labs/solana/pull/18943, two subscriptions for
145
+ * materially the same notification will be coalesced on the server. This means they
146
+ * will be assigned the same subscription id, and will occupy one subscription slot. We
147
+ * must be careful not to send the unsubscribe message until the last subscriber aborts.
148
+ */
149
+ if (decrementSubscriberCountAndReturnNewCount(channel, subscriptionId) === 0) {
150
+ const unsubscribePayload = createRpcMessage({
151
+ methodName: unsubscribeMethodName,
152
+ params: [subscriptionId],
153
+ });
154
+ subscriptionId = undefined;
155
+ channel.send(unsubscribePayload).catch(() => {});
156
+ }
157
+ // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
158
+ reject(this.reason);
159
+ }
160
+ if (signal.aborted) {
161
+ handleAbort.call(signal);
162
+ } else {
163
+ signal.addEventListener('abort', handleAbort);
164
+ }
165
+ });
166
+ /**
167
+ * STEP 2
168
+ * Send the subscription request.
169
+ */
170
+ const subscribePayload = createRpcMessage(subscribeRequest);
171
+ await channel.send(subscribePayload);
172
+ /**
173
+ * STEP 3
174
+ * Wait for the acknowledgement from the server with the subscription id.
175
+ */
176
+ const subscriptionIdPromise = new Promise<RpcSubscriptionId>((resolve, reject) => {
177
+ const abortController = new AbortController();
178
+ signal.addEventListener('abort', abortController.abort.bind(abortController));
179
+ const options = { signal: abortController.signal } as const;
180
+ channel.on(
181
+ 'error',
182
+ err => {
183
+ abortController.abort();
184
+ reject(err);
185
+ },
186
+ options,
187
+ );
188
+ channel.on(
189
+ 'message',
190
+ message => {
191
+ if (message && typeof message === 'object' && 'id' in message && message.id === subscribePayload.id) {
192
+ abortController.abort();
193
+ if ('error' in message) {
194
+ reject(getSolanaErrorFromJsonRpcError(message.error));
195
+ } else {
196
+ resolve(message.result);
197
+ }
198
+ }
199
+ },
200
+ options,
201
+ );
202
+ });
203
+ subscriptionId = await safeRace([abortPromise, subscriptionIdPromise]);
204
+ if (subscriptionId == null) {
205
+ throw new SolanaError(SOLANA_ERROR__RPC_SUBSCRIPTIONS__EXPECTED_SERVER_SUBSCRIPTION_ID);
206
+ }
207
+ incrementSubscriberCount(channel, subscriptionId);
208
+ /**
209
+ * STEP 4
210
+ * Filter out notifications unrelated to this subscription.
211
+ */
212
+ const notificationPublisher = getMemoizedDemultiplexedNotificationPublisherFromChannelAndResponseTransformer(
213
+ channel,
214
+ subscribeRequest,
215
+ responseTransformer,
216
+ );
217
+ const notificationKey = `notification:${subscriptionId}` as const;
218
+ return {
219
+ on(type, listener, options) {
220
+ switch (type) {
221
+ case 'notification':
222
+ return notificationPublisher.on(
223
+ notificationKey,
224
+ listener as (data: RpcSubscriptionNotificationEvents<TNotification>['notification']) => void,
225
+ options,
226
+ );
227
+ case 'error':
228
+ return channel.on(
229
+ 'error',
230
+ listener as (data: RpcSubscriptionNotificationEvents<TNotification>['error']) => void,
231
+ options,
232
+ );
233
+ default:
234
+ throw new SolanaError(SOLANA_ERROR__INVARIANT_VIOLATION__DATA_PUBLISHER_CHANNEL_UNIMPLEMENTED, {
235
+ channelName: type,
236
+ supportedChannelNames: ['notification', 'error'],
237
+ });
238
+ }
239
+ },
240
+ };
241
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Pending subscriptions are the result of calling a supported method on a {@link RpcSubscriptions}
3
+ * object. They encapsulate all of the information necessary to make the subscription without
4
+ * actually making it.
5
+ *
6
+ * Calling the {@link PendingRpcSubscriptionsRequest.subscribe | `subscribe(options)`} method on a
7
+ * {@link PendingRpcSubscriptionsRequest | PendingRpcSubscriptionsRequest<TNotification>} will
8
+ * trigger the subscription and return a promise for an async iterable that vends `TNotifications`.
9
+ */
10
+ export type PendingRpcSubscriptionsRequest<TNotification> = {
11
+ subscribe(options: RpcSubscribeOptions): Promise<AsyncIterable<TNotification>>;
12
+ };
13
+
14
+ export type RpcSubscribeOptions = Readonly<{
15
+ /** An `AbortSignal` to fire when you want to unsubscribe */
16
+ abortSignal: AbortSignal;
17
+ }>;
@@ -0,0 +1,32 @@
1
+ import { SolanaError } from '@solana/errors';
2
+ import { DataPublisher } from '@solana/subscribable';
3
+
4
+ import { RpcSubscriptionsPlan } from './rpc-subscriptions-api';
5
+
6
+ export type RpcSubscriptionsTransportDataEvents<TNotification> = {
7
+ /**
8
+ * Fires when there is an error with the subscription or the channel.
9
+ * @eventProperty
10
+ */
11
+ error: SolanaError;
12
+ /**
13
+ * Fires on every notification received.
14
+ * @eventProperty
15
+ */
16
+ notification: TNotification;
17
+ };
18
+
19
+ interface RpcSubscriptionsTransportConfig<TNotification> extends RpcSubscriptionsPlan<TNotification> {
20
+ /** An `AbortSignal` to fire when you want to unsubscribe */
21
+ signal: AbortSignal;
22
+ }
23
+
24
+ /**
25
+ * A function that can act as a transport for a {@link RpcSubscriptions}. It need only return a
26
+ * promise for a {@link DataPublisher} given the supplied config.
27
+ */
28
+ export interface RpcSubscriptionsTransport {
29
+ <TNotification>(
30
+ config: RpcSubscriptionsTransportConfig<TNotification>,
31
+ ): Promise<DataPublisher<RpcSubscriptionsTransportDataEvents<TNotification>>>;
32
+ }
@@ -0,0 +1,95 @@
1
+ import { SOLANA_ERROR__RPC_SUBSCRIPTIONS__CANNOT_CREATE_SUBSCRIPTION_PLAN, SolanaError } from '@solana/errors';
2
+ import { Callable, Flatten, OverloadImplementations, UnionToIntersection } from '@solana/rpc-spec-types';
3
+ import { createAsyncIterableFromDataPublisher } from '@solana/subscribable';
4
+
5
+ import { RpcSubscriptionsApi, RpcSubscriptionsPlan } from './rpc-subscriptions-api';
6
+ import { PendingRpcSubscriptionsRequest, RpcSubscribeOptions } from './rpc-subscriptions-request';
7
+ import { RpcSubscriptionsTransport } from './rpc-subscriptions-transport';
8
+
9
+ export type RpcSubscriptionsConfig<TRpcMethods> = Readonly<{
10
+ api: RpcSubscriptionsApi<TRpcMethods>;
11
+ transport: RpcSubscriptionsTransport;
12
+ }>;
13
+
14
+ /**
15
+ * An object that exposes all of the functions described by `TRpcSubscriptionsMethods`.
16
+ *
17
+ * Calling each method returns a
18
+ * {@link PendingRpcSubscriptionsRequest | PendingRpcSubscriptionsRequest<TNotification>} where
19
+ * `TNotification` is that method's notification type.
20
+ */
21
+ export type RpcSubscriptions<TRpcSubscriptionsMethods> = {
22
+ [TMethodName in keyof TRpcSubscriptionsMethods]: PendingRpcSubscriptionsRequestBuilder<
23
+ OverloadImplementations<TRpcSubscriptionsMethods, TMethodName>
24
+ >;
25
+ };
26
+
27
+ type PendingRpcSubscriptionsRequestBuilder<TSubscriptionMethodImplementations> = UnionToIntersection<
28
+ Flatten<{
29
+ [P in keyof TSubscriptionMethodImplementations]: PendingRpcSubscriptionsRequestReturnTypeMapper<
30
+ TSubscriptionMethodImplementations[P]
31
+ >;
32
+ }>
33
+ >;
34
+
35
+ type PendingRpcSubscriptionsRequestReturnTypeMapper<TSubscriptionMethodImplementation> =
36
+ // Check that this property of the TRpcSubscriptionMethods interface is, in fact, a function.
37
+ TSubscriptionMethodImplementation extends Callable
38
+ ? (
39
+ ...args: Parameters<TSubscriptionMethodImplementation>
40
+ ) => PendingRpcSubscriptionsRequest<ReturnType<TSubscriptionMethodImplementation>>
41
+ : never;
42
+
43
+ /**
44
+ * Creates a {@link RpcSubscriptions} instance given a
45
+ * {@link RpcSubscriptionsApi | RpcSubscriptionsApi<TRpcSubscriptionsApiMethods>} and a
46
+ * {@link RpcSubscriptionsTransport} capable of fulfilling them.
47
+ */
48
+ export function createSubscriptionRpc<TRpcSubscriptionsApiMethods>(
49
+ rpcConfig: RpcSubscriptionsConfig<TRpcSubscriptionsApiMethods>,
50
+ ): RpcSubscriptions<TRpcSubscriptionsApiMethods> {
51
+ return new Proxy(rpcConfig.api, {
52
+ defineProperty() {
53
+ return false;
54
+ },
55
+ deleteProperty() {
56
+ return false;
57
+ },
58
+ get(target, p, receiver) {
59
+ if (p === 'then') {
60
+ return undefined;
61
+ }
62
+ return function (...rawParams: unknown[]) {
63
+ const notificationName = p.toString();
64
+ const createRpcSubscriptionPlan = Reflect.get(target, notificationName, receiver);
65
+ if (!createRpcSubscriptionPlan) {
66
+ throw new SolanaError(SOLANA_ERROR__RPC_SUBSCRIPTIONS__CANNOT_CREATE_SUBSCRIPTION_PLAN, {
67
+ notificationName,
68
+ });
69
+ }
70
+ const subscriptionPlan = createRpcSubscriptionPlan(...rawParams);
71
+ return createPendingRpcSubscription(rpcConfig.transport, subscriptionPlan);
72
+ };
73
+ },
74
+ }) as RpcSubscriptions<TRpcSubscriptionsApiMethods>;
75
+ }
76
+
77
+ function createPendingRpcSubscription<TNotification>(
78
+ transport: RpcSubscriptionsTransport,
79
+ subscriptionsPlan: RpcSubscriptionsPlan<TNotification>,
80
+ ): PendingRpcSubscriptionsRequest<TNotification> {
81
+ return {
82
+ async subscribe({ abortSignal }: RpcSubscribeOptions): Promise<AsyncIterable<TNotification>> {
83
+ const notificationsDataPublisher = await transport({
84
+ signal: abortSignal,
85
+ ...subscriptionsPlan,
86
+ });
87
+ return createAsyncIterableFromDataPublisher<TNotification>({
88
+ abortSignal,
89
+ dataChannelName: 'notification',
90
+ dataPublisher: notificationsDataPublisher,
91
+ errorChannelName: 'error',
92
+ });
93
+ },
94
+ };
95
+ }