@solana/rpc-subscriptions 6.3.1 → 6.3.2-canary-20260313143218

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",
3
- "version": "6.3.1",
3
+ "version": "6.3.2-canary-20260313143218",
4
4
  "description": "A library for subscribing to Solana RPC notifications",
5
5
  "homepage": "https://www.solanakit.com/api#solanarpc-subscriptions",
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,17 +56,17 @@
55
56
  "maintained node versions"
56
57
  ],
57
58
  "dependencies": {
58
- "@solana/errors": "6.3.1",
59
- "@solana/functional": "6.3.1",
60
- "@solana/promises": "6.3.1",
61
- "@solana/rpc-spec-types": "6.3.1",
62
- "@solana/fast-stable-stringify": "6.3.1",
63
- "@solana/rpc-subscriptions-channel-websocket": "6.3.1",
64
- "@solana/rpc-subscriptions-api": "6.3.1",
65
- "@solana/rpc-transformers": "6.3.1",
66
- "@solana/rpc-subscriptions-spec": "6.3.1",
67
- "@solana/rpc-types": "6.3.1",
68
- "@solana/subscribable": "6.3.1"
59
+ "@solana/errors": "6.3.2-canary-20260313143218",
60
+ "@solana/fast-stable-stringify": "6.3.2-canary-20260313143218",
61
+ "@solana/functional": "6.3.2-canary-20260313143218",
62
+ "@solana/promises": "6.3.2-canary-20260313143218",
63
+ "@solana/rpc-spec-types": "6.3.2-canary-20260313143218",
64
+ "@solana/rpc-subscriptions-api": "6.3.2-canary-20260313143218",
65
+ "@solana/rpc-subscriptions-channel-websocket": "6.3.2-canary-20260313143218",
66
+ "@solana/rpc-subscriptions-spec": "6.3.2-canary-20260313143218",
67
+ "@solana/rpc-transformers": "6.3.2-canary-20260313143218",
68
+ "@solana/rpc-types": "6.3.2-canary-20260313143218",
69
+ "@solana/subscribable": "6.3.2-canary-20260313143218"
69
70
  },
70
71
  "peerDependencies": {
71
72
  "typescript": "^5.0.0"
package/src/index.ts ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * This package contains types that implement RPC subscriptions as required by the Solana RPC.
3
+ * Additionally, it incorporates some useful defaults that make working with subscriptions easier,
4
+ * more performant, and more reliable. It can be used standalone, but it is also exported as part of
5
+ * Kit [`@solana/kit`](https://github.com/anza-xyz/kit/tree/main/packages/kit).
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+ export * from '@solana/rpc-subscriptions-api';
10
+ export * from '@solana/rpc-subscriptions-spec';
11
+
12
+ export * from './rpc-default-config';
13
+ export * from './rpc-subscriptions-autopinger';
14
+ export * from './rpc-subscriptions-channel-pool';
15
+ export * from './rpc-subscriptions-channel';
16
+ export * from './rpc-subscriptions-clusters';
17
+ export * from './rpc-subscriptions-coalescer';
18
+ export * from './rpc-subscriptions-json-bigint';
19
+ export * from './rpc-subscriptions-json';
20
+ export * from './rpc-subscriptions-transport';
21
+ export * from './rpc-subscriptions';
@@ -0,0 +1,12 @@
1
+ import type { createSolanaRpcSubscriptionsApi } from '@solana/rpc-subscriptions-api';
2
+
3
+ import { createSolanaJsonRpcIntegerOverflowError } from './rpc-integer-overflow-error';
4
+
5
+ export const DEFAULT_RPC_SUBSCRIPTIONS_CONFIG: Partial<
6
+ NonNullable<Parameters<typeof createSolanaRpcSubscriptionsApi>[0]>
7
+ > = {
8
+ defaultCommitment: 'confirmed',
9
+ onIntegerOverflow(request, keyPath, value) {
10
+ throw createSolanaJsonRpcIntegerOverflowError(request.methodName, keyPath, value);
11
+ },
12
+ };
@@ -0,0 +1,43 @@
1
+ import { safeCaptureStackTrace, SOLANA_ERROR__RPC__INTEGER_OVERFLOW, SolanaError } from '@solana/errors';
2
+ import type { KeyPath } from '@solana/rpc-transformers';
3
+
4
+ export function createSolanaJsonRpcIntegerOverflowError(
5
+ methodName: string,
6
+ keyPath: KeyPath,
7
+ value: bigint,
8
+ ): SolanaError<typeof SOLANA_ERROR__RPC__INTEGER_OVERFLOW> {
9
+ let argumentLabel = '';
10
+ if (typeof keyPath[0] === 'number') {
11
+ const argPosition = keyPath[0] + 1;
12
+ const lastDigit = argPosition % 10;
13
+ const lastTwoDigits = argPosition % 100;
14
+ if (lastDigit == 1 && lastTwoDigits != 11) {
15
+ argumentLabel = argPosition + 'st';
16
+ } else if (lastDigit == 2 && lastTwoDigits != 12) {
17
+ argumentLabel = argPosition + 'nd';
18
+ } else if (lastDigit == 3 && lastTwoDigits != 13) {
19
+ argumentLabel = argPosition + 'rd';
20
+ } else {
21
+ argumentLabel = argPosition + 'th';
22
+ }
23
+ } else {
24
+ argumentLabel = `\`${keyPath[0].toString()}\``;
25
+ }
26
+ const path =
27
+ keyPath.length > 1
28
+ ? keyPath
29
+ .slice(1)
30
+ .map(pathPart => (typeof pathPart === 'number' ? `[${pathPart}]` : pathPart))
31
+ .join('.')
32
+ : undefined;
33
+ const error = new SolanaError(SOLANA_ERROR__RPC__INTEGER_OVERFLOW, {
34
+ argumentLabel,
35
+ keyPath: keyPath as readonly (number | string | symbol)[],
36
+ methodName,
37
+ optionalPathLabel: path ? ` at path \`${path}\`` : '',
38
+ value,
39
+ ...(path !== undefined ? { path } : undefined),
40
+ });
41
+ safeCaptureStackTrace(error, createSolanaJsonRpcIntegerOverflowError);
42
+ return error;
43
+ }
@@ -0,0 +1,83 @@
1
+ import { isSolanaError, SOLANA_ERROR__RPC_SUBSCRIPTIONS__CHANNEL_CONNECTION_CLOSED } from '@solana/errors';
2
+ import { AbortController } from '@solana/event-target-impl';
3
+ import type { RpcSubscriptionsChannel } from '@solana/rpc-subscriptions-spec';
4
+
5
+ type Config<TChannel extends RpcSubscriptionsChannel<unknown, unknown>> = Readonly<{
6
+ abortSignal: AbortSignal;
7
+ channel: TChannel;
8
+ intervalMs: number;
9
+ }>;
10
+
11
+ const PING_PAYLOAD = {
12
+ jsonrpc: '2.0',
13
+ method: 'ping',
14
+ } as const;
15
+
16
+ /**
17
+ * Given a {@link RpcSubscriptionsChannel}, will return a new channel that sends a ping message to
18
+ * the inner channel if a message has not been sent or received in the last `intervalMs`. In web
19
+ * browsers, this implementation sends no ping when the network is down, and sends a ping
20
+ * immediately upon the network coming back up.
21
+ */
22
+ export function getRpcSubscriptionsChannelWithAutoping<TChannel extends RpcSubscriptionsChannel<object, unknown>>({
23
+ abortSignal: callerAbortSignal,
24
+ channel,
25
+ intervalMs,
26
+ }: Config<TChannel>): TChannel {
27
+ let intervalId: ReturnType<typeof setInterval> | undefined;
28
+ function sendPing() {
29
+ channel.send(PING_PAYLOAD).catch((e: unknown) => {
30
+ if (isSolanaError(e, SOLANA_ERROR__RPC_SUBSCRIPTIONS__CHANNEL_CONNECTION_CLOSED)) {
31
+ pingerAbortController.abort();
32
+ }
33
+ });
34
+ }
35
+ function restartPingTimer() {
36
+ clearInterval(intervalId);
37
+ intervalId = setInterval(sendPing, intervalMs);
38
+ }
39
+ const pingerAbortController = new AbortController();
40
+ pingerAbortController.signal.addEventListener('abort', () => {
41
+ clearInterval(intervalId);
42
+ });
43
+ callerAbortSignal.addEventListener('abort', () => {
44
+ pingerAbortController.abort();
45
+ });
46
+ channel.on(
47
+ 'error',
48
+ () => {
49
+ pingerAbortController.abort();
50
+ },
51
+ { signal: pingerAbortController.signal },
52
+ );
53
+ channel.on('message', restartPingTimer, { signal: pingerAbortController.signal });
54
+ if (!__BROWSER__ || globalThis.navigator.onLine) {
55
+ restartPingTimer();
56
+ }
57
+ if (__BROWSER__) {
58
+ globalThis.addEventListener(
59
+ 'offline',
60
+ function handleOffline() {
61
+ clearInterval(intervalId);
62
+ },
63
+ { signal: pingerAbortController.signal },
64
+ );
65
+ globalThis.addEventListener(
66
+ 'online',
67
+ function handleOnline() {
68
+ sendPing();
69
+ restartPingTimer();
70
+ },
71
+ { signal: pingerAbortController.signal },
72
+ );
73
+ }
74
+ return {
75
+ ...channel,
76
+ send(...args) {
77
+ if (!pingerAbortController.signal.aborted) {
78
+ restartPingTimer();
79
+ }
80
+ return channel.send(...args);
81
+ },
82
+ };
83
+ }
@@ -0,0 +1,16 @@
1
+ import { RpcSubscriptionsChannel } from '@solana/rpc-subscriptions-spec';
2
+
3
+ export type ChannelPoolEntry = {
4
+ channel: PromiseLike<RpcSubscriptionsChannel<unknown, unknown>> | RpcSubscriptionsChannel<unknown, unknown>;
5
+ readonly dispose: () => void;
6
+ subscriptionCount: number;
7
+ };
8
+
9
+ type ChannelPool = { readonly entries: ChannelPoolEntry[]; freeChannelIndex: number };
10
+
11
+ export function createChannelPool(): ChannelPool {
12
+ return {
13
+ entries: [],
14
+ freeChannelIndex: -1,
15
+ };
16
+ }
@@ -0,0 +1,113 @@
1
+ import { AbortController } from '@solana/event-target-impl';
2
+ import { RpcSubscriptionsChannelCreator } from '@solana/rpc-subscriptions-spec';
3
+
4
+ import { ChannelPoolEntry, createChannelPool } from './rpc-subscriptions-channel-pool-internal';
5
+
6
+ type Config = Readonly<{
7
+ maxSubscriptionsPerChannel: number;
8
+ minChannels: number;
9
+ }>;
10
+
11
+ /**
12
+ * Given a channel creator, will return a new channel creator with the following behavior.
13
+ *
14
+ * 1. When called, returns a {@link RpcSubscriptionsChannel}. Adds that channel to a pool.
15
+ * 2. When called again, creates and returns new
16
+ * {@link RpcSubscriptionChannel | RpcSubscriptionChannels} up to the number specified by
17
+ * `minChannels`.
18
+ * 3. When `minChannels` channels have been created, subsequent calls vend whichever existing
19
+ * channel from the pool has the fewest subscribers, or the next one in rotation in the event of
20
+ * a tie.
21
+ * 4. Once all channels carry the number of subscribers specified by the number
22
+ * `maxSubscriptionsPerChannel`, new channels in excess of `minChannel` will be created,
23
+ * returned, and added to the pool.
24
+ * 5. A channel will be destroyed once all of its subscribers' abort signals fire.
25
+ */
26
+ export function getChannelPoolingChannelCreator<
27
+ TChannelCreator extends RpcSubscriptionsChannelCreator<unknown, unknown>,
28
+ >(createChannel: TChannelCreator, { maxSubscriptionsPerChannel, minChannels }: Config): TChannelCreator {
29
+ const pool = createChannelPool();
30
+ /**
31
+ * This function advances the free channel index to the pool entry with the most capacity. It
32
+ * sets the index to `-1` if all channels are full.
33
+ */
34
+ function recomputeFreeChannelIndex() {
35
+ if (pool.entries.length < minChannels) {
36
+ // Don't set the free channel index until the pool fills up; we want to keep creating
37
+ // channels before we start rotating among them.
38
+ pool.freeChannelIndex = -1;
39
+ return;
40
+ }
41
+ let mostFreeChannel: Readonly<{ poolIndex: number; subscriptionCount: number }> | undefined;
42
+ for (let ii = 0; ii < pool.entries.length; ii++) {
43
+ const nextPoolIndex = (pool.freeChannelIndex + ii + 2) % pool.entries.length;
44
+ const nextPoolEntry =
45
+ // Start from the item two positions after the current item. This way, the
46
+ // search will finish on the item after the current one. This ensures that, if
47
+ // any channels tie for having the most capacity, the one that will be chosen is
48
+ // the one immediately to the current one's right (wrapping around).
49
+ pool.entries[nextPoolIndex];
50
+ if (
51
+ nextPoolEntry.subscriptionCount < maxSubscriptionsPerChannel &&
52
+ (!mostFreeChannel || mostFreeChannel.subscriptionCount >= nextPoolEntry.subscriptionCount)
53
+ ) {
54
+ mostFreeChannel = {
55
+ poolIndex: nextPoolIndex,
56
+ subscriptionCount: nextPoolEntry.subscriptionCount,
57
+ };
58
+ }
59
+ }
60
+ pool.freeChannelIndex = mostFreeChannel?.poolIndex ?? -1;
61
+ }
62
+ return function getExistingChannelWithMostCapacityOrCreateChannel({ abortSignal }) {
63
+ let poolEntry: ChannelPoolEntry;
64
+ function destroyPoolEntry() {
65
+ const index = pool.entries.findIndex(entry => entry === poolEntry);
66
+ pool.entries.splice(index, 1);
67
+ poolEntry.dispose();
68
+ recomputeFreeChannelIndex();
69
+ }
70
+ if (pool.freeChannelIndex === -1) {
71
+ const abortController = new AbortController();
72
+ const newChannelPromise = createChannel({ abortSignal: abortController.signal });
73
+ newChannelPromise
74
+ .then(newChannel => {
75
+ newChannel.on('error', destroyPoolEntry, { signal: abortController.signal });
76
+ })
77
+ .catch(destroyPoolEntry);
78
+ poolEntry = {
79
+ channel: newChannelPromise,
80
+ dispose() {
81
+ abortController.abort();
82
+ },
83
+ subscriptionCount: 0,
84
+ };
85
+ pool.entries.push(poolEntry);
86
+ } else {
87
+ poolEntry = pool.entries[pool.freeChannelIndex];
88
+ }
89
+ /**
90
+ * A note about subscription counts.
91
+ * Because of https://github.com/solana-labs/solana/pull/18943, two subscriptions for
92
+ * materially the same notification will be coalesced on the server. This means they will be
93
+ * assigned the same subscription id, and will occupy one subscription slot. We can't tell,
94
+ * from here, whether a subscription will be treated in this way or not, so we
95
+ * unconditionally increment the subscription count every time a subscription request is
96
+ * made. This may result in subscription channels being treated as out-of-capacity when in
97
+ * fact they are not.
98
+ */
99
+ poolEntry.subscriptionCount++;
100
+ abortSignal.addEventListener('abort', function destroyConsumer() {
101
+ poolEntry.subscriptionCount--;
102
+ if (poolEntry.subscriptionCount === 0) {
103
+ destroyPoolEntry();
104
+ } else if (pool.freeChannelIndex !== -1) {
105
+ // Back the free channel index up one position, and recompute it.
106
+ pool.freeChannelIndex--;
107
+ recomputeFreeChannelIndex();
108
+ }
109
+ });
110
+ recomputeFreeChannelIndex();
111
+ return poolEntry.channel;
112
+ } as TChannelCreator;
113
+ }
@@ -0,0 +1,114 @@
1
+ import { createWebSocketChannel } from '@solana/rpc-subscriptions-channel-websocket';
2
+ import type { RpcSubscriptionsChannel } from '@solana/rpc-subscriptions-spec';
3
+ import type { ClusterUrl } from '@solana/rpc-types';
4
+
5
+ import { getRpcSubscriptionsChannelWithAutoping } from './rpc-subscriptions-autopinger';
6
+ import { getChannelPoolingChannelCreator } from './rpc-subscriptions-channel-pool';
7
+ import { RpcSubscriptionsChannelCreatorFromClusterUrl } from './rpc-subscriptions-clusters';
8
+ import { getRpcSubscriptionsChannelWithJSONSerialization } from './rpc-subscriptions-json';
9
+ import { getRpcSubscriptionsChannelWithBigIntJSONSerialization } from './rpc-subscriptions-json-bigint';
10
+
11
+ export type DefaultRpcSubscriptionsChannelConfig<TClusterUrl extends ClusterUrl> = Readonly<{
12
+ /**
13
+ * The number of milliseconds to wait since the last message sent or received over the channel
14
+ * before sending a ping message to keep the channel open.
15
+ */
16
+ intervalMs?: number;
17
+ /**
18
+ * The number of subscribers that may share a channel before a new channel must be created.
19
+ *
20
+ * It is important that you set this to the maximum number of subscriptions that your RPC
21
+ * provider recommends making over a single connection; the default is set deliberately low, so
22
+ * as to comply with the restrictive limits of the public mainnet RPC node.
23
+ *
24
+ * @defaultValue 100
25
+ */
26
+ maxSubscriptionsPerChannel?: number;
27
+ /** The number of channels to create before reusing a channel for a new subscription. */
28
+ minChannels?: number;
29
+ /**
30
+ * The number of bytes of data to admit into the
31
+ * [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) buffer before
32
+ * buffering data on the client.
33
+ */
34
+ sendBufferHighWatermark?: number;
35
+ /** The URL of the web socket server. Must use the `ws` or `wss` protocols. */
36
+ url: TClusterUrl;
37
+ }>;
38
+
39
+ /**
40
+ * Similar to {@link createDefaultRpcSubscriptionsChannelCreator} with some Solana-specific
41
+ * defaults.
42
+ *
43
+ * For instance, it safely handles `BigInt` values in JSON messages since Solana RPC servers accept
44
+ * and return integers larger than [`Number.MAX_SAFE_INTEGER`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER).
45
+ */
46
+ export function createDefaultSolanaRpcSubscriptionsChannelCreator<TClusterUrl extends ClusterUrl>(
47
+ config: DefaultRpcSubscriptionsChannelConfig<TClusterUrl>,
48
+ ): RpcSubscriptionsChannelCreatorFromClusterUrl<TClusterUrl, unknown, unknown> {
49
+ return createDefaultRpcSubscriptionsChannelCreatorImpl({
50
+ ...config,
51
+ jsonSerializer: getRpcSubscriptionsChannelWithBigIntJSONSerialization,
52
+ });
53
+ }
54
+
55
+ /**
56
+ * Creates a function that returns new subscription channels when called.
57
+ */
58
+ export function createDefaultRpcSubscriptionsChannelCreator<TClusterUrl extends ClusterUrl>(
59
+ config: DefaultRpcSubscriptionsChannelConfig<TClusterUrl>,
60
+ ): RpcSubscriptionsChannelCreatorFromClusterUrl<TClusterUrl, unknown, unknown> {
61
+ return createDefaultRpcSubscriptionsChannelCreatorImpl({
62
+ ...config,
63
+ jsonSerializer: getRpcSubscriptionsChannelWithJSONSerialization,
64
+ });
65
+ }
66
+
67
+ function createDefaultRpcSubscriptionsChannelCreatorImpl<TClusterUrl extends ClusterUrl>(
68
+ config: DefaultRpcSubscriptionsChannelConfig<TClusterUrl> & {
69
+ jsonSerializer: (channel: RpcSubscriptionsChannel<string, string>) => RpcSubscriptionsChannel<unknown, unknown>;
70
+ },
71
+ ): RpcSubscriptionsChannelCreatorFromClusterUrl<TClusterUrl, unknown, unknown> {
72
+ if (/^wss?:/i.test(config.url) === false) {
73
+ const protocolMatch = config.url.match(/^([^:]+):/);
74
+ throw new DOMException(
75
+ protocolMatch
76
+ ? "Failed to construct 'WebSocket': The URL's scheme must be either 'ws' or " +
77
+ `'wss'. '${protocolMatch[1]}:' is not allowed.`
78
+ : `Failed to construct 'WebSocket': The URL '${config.url}' is invalid.`,
79
+ );
80
+ }
81
+ const { intervalMs, ...rest } = config;
82
+ const createDefaultRpcSubscriptionsChannel = (({ abortSignal }) => {
83
+ return createWebSocketChannel({
84
+ ...rest,
85
+ sendBufferHighWatermark:
86
+ config.sendBufferHighWatermark ??
87
+ // Let 128KB of data into the WebSocket buffer before buffering it in the app.
88
+ 131_072,
89
+ signal: abortSignal,
90
+ })
91
+ .then(config.jsonSerializer)
92
+ .then(channel =>
93
+ getRpcSubscriptionsChannelWithAutoping({
94
+ abortSignal,
95
+ channel,
96
+ intervalMs: intervalMs ?? 5_000,
97
+ }),
98
+ );
99
+ }) as RpcSubscriptionsChannelCreatorFromClusterUrl<TClusterUrl, unknown, unknown>;
100
+ return getChannelPoolingChannelCreator(createDefaultRpcSubscriptionsChannel, {
101
+ maxSubscriptionsPerChannel:
102
+ config.maxSubscriptionsPerChannel ??
103
+ /**
104
+ * A note about this default. The idea here is that, because some RPC providers impose
105
+ * an upper limit on the number of subscriptions you can make per channel, we must
106
+ * choose a number low enough to avoid hitting that limit. Without knowing what provider
107
+ * a given person is using, or what their limit is, we have to choose the lowest of all
108
+ * known limits. As of this writing (October 2024) that is the public mainnet RPC node
109
+ * (api.mainnet-beta.solana.com) at 100 subscriptions.
110
+ */
111
+ 100,
112
+ minChannels: config.minChannels ?? 1,
113
+ });
114
+ }
@@ -0,0 +1,305 @@
1
+ import type {
2
+ RpcSubscriptions,
3
+ RpcSubscriptionsChannel,
4
+ RpcSubscriptionsChannelCreator,
5
+ RpcSubscriptionsTransport,
6
+ } from '@solana/rpc-subscriptions-spec';
7
+ import type { ClusterUrl, DevnetUrl, MainnetUrl, TestnetUrl } from '@solana/rpc-types';
8
+
9
+ export type RpcSubscriptionsChannelCreatorDevnet<TOutboundMessage, TInboundMessage> = RpcSubscriptionsChannelCreator<
10
+ TOutboundMessage,
11
+ TInboundMessage
12
+ > & {
13
+ '~cluster': 'devnet';
14
+ };
15
+ export type RpcSubscriptionsChannelCreatorTestnet<TOutboundMessage, TInboundMessage> = RpcSubscriptionsChannelCreator<
16
+ TOutboundMessage,
17
+ TInboundMessage
18
+ > & {
19
+ '~cluster': 'testnet';
20
+ };
21
+ export type RpcSubscriptionsChannelCreatorMainnet<TOutboundMessage, TInboundMessage> = RpcSubscriptionsChannelCreator<
22
+ TOutboundMessage,
23
+ TInboundMessage
24
+ > & {
25
+ '~cluster': 'mainnet';
26
+ };
27
+ export type RpcSubscriptionsChannelCreatorWithCluster<TOutboundMessage, TInboundMessage> =
28
+ | RpcSubscriptionsChannelCreatorDevnet<TOutboundMessage, TInboundMessage>
29
+ | RpcSubscriptionsChannelCreatorMainnet<TOutboundMessage, TInboundMessage>
30
+ | RpcSubscriptionsChannelCreatorTestnet<TOutboundMessage, TInboundMessage>;
31
+ export type RpcSubscriptionsChannelCreatorFromClusterUrl<
32
+ TClusterUrl extends ClusterUrl,
33
+ TOutboundMessage,
34
+ TInboundMessage,
35
+ > = TClusterUrl extends DevnetUrl
36
+ ? RpcSubscriptionsChannelCreatorDevnet<TOutboundMessage, TInboundMessage>
37
+ : TClusterUrl extends TestnetUrl
38
+ ? RpcSubscriptionsChannelCreatorTestnet<TOutboundMessage, TInboundMessage>
39
+ : TClusterUrl extends MainnetUrl
40
+ ? RpcSubscriptionsChannelCreatorMainnet<TOutboundMessage, TInboundMessage>
41
+ : RpcSubscriptionsChannelCreator<TOutboundMessage, TInboundMessage>;
42
+
43
+ /**
44
+ * A {@link RpcSubscriptionsChannel} that communicates with the devnet cluster.
45
+ *
46
+ * Such channels are understood to communicate with a RPC server that services devnet, and as such
47
+ * might only be accepted for use as the channel of a {@link RpcSubscriptionsTransportDevnet}.
48
+ *
49
+ * This is useful in cases where you need to make assertions about what capabilities a RPC offers.
50
+ * You can use the ability to assert on the type of RPC channel at compile time to prevent calling
51
+ * unimplemented methods or presuming the existence of unavailable programs or data.
52
+ */
53
+ export type RpcSubscriptionsChannelDevnet<TOutboundMessage, TInboundMessage> = RpcSubscriptionsChannel<
54
+ TOutboundMessage,
55
+ TInboundMessage
56
+ > & { '~cluster': 'devnet' };
57
+ /**
58
+ * A {@link RpcSubscriptionsChannel} that communicates with the testnet cluster.
59
+ *
60
+ * Such channels are understood to communicate with a RPC server that services testnet, and as such
61
+ * might only be accepted for use as the channel of a {@link RpcSubscriptionsTransportTestnet}.
62
+ *
63
+ * This is useful in cases where you need to make assertions about what capabilities a RPC offers.
64
+ * You can use the ability to assert on the type of RPC channel at compile time to prevent calling
65
+ * unimplemented methods or presuming the existence of unavailable programs or data.
66
+ */
67
+ export type RpcSubscriptionsChannelTestnet<TOutboundMessage, TInboundMessage> = RpcSubscriptionsChannel<
68
+ TOutboundMessage,
69
+ TInboundMessage
70
+ > & { '~cluster': 'testnet' };
71
+ /**
72
+ * A {@link RpcSubscriptionsChannel} that communicates with the mainnet cluster.
73
+ *
74
+ * Such channels are understood to communicate with a RPC server that services mainnet, and as such
75
+ * might only be accepted for use as the channel of a {@link RpcSubscriptionsTransportMainnet}.
76
+ *
77
+ * This is useful in cases where you need to make assertions about what capabilities a RPC offers.
78
+ * You can use the ability to assert on the type of RPC channel at compile time to prevent calling
79
+ * unimplemented methods or presuming the existence of unavailable programs or data.
80
+ */
81
+ export type RpcSubscriptionsChannelMainnet<TOutboundMessage, TInboundMessage> = RpcSubscriptionsChannel<
82
+ TOutboundMessage,
83
+ TInboundMessage
84
+ > & { '~cluster': 'mainnet' };
85
+ export type RpcSubscriptionsChannelWithCluster<TOutboundMessage, TInboundMessage> =
86
+ | RpcSubscriptionsChannelDevnet<TOutboundMessage, TInboundMessage>
87
+ | RpcSubscriptionsChannelMainnet<TOutboundMessage, TInboundMessage>
88
+ | RpcSubscriptionsChannelTestnet<TOutboundMessage, TInboundMessage>;
89
+ /**
90
+ * Given a {@link ClusterUrl}, this utility type will resolve to as specific a
91
+ * {@link RpcSubscriptionsChannel} as possible.
92
+ *
93
+ * @example
94
+ * ```ts
95
+ * function createCustomSubscriptionsChannel<TClusterUrl extends ClusterUrl>(
96
+ * clusterUrl: TClusterUrl,
97
+ * ): RpcSubscriptionsChannelFromClusterUrl<TClusterUrl> {
98
+ * /* ... *\/
99
+ * }
100
+ *
101
+ * const channel = createCustomSubscriptionsChannel(testnet('ws://api.testnet.solana.com'));
102
+ * channel satisfies RpcSubscriptionsChannelTestnet; // OK
103
+ * ```
104
+ */
105
+ export type RpcSubscriptionsChannelFromClusterUrl<
106
+ TClusterUrl extends ClusterUrl,
107
+ TOutboundMessage,
108
+ TInboundMessage,
109
+ > = TClusterUrl extends DevnetUrl
110
+ ? RpcSubscriptionsChannelDevnet<TOutboundMessage, TInboundMessage>
111
+ : TClusterUrl extends TestnetUrl
112
+ ? RpcSubscriptionsChannelTestnet<TOutboundMessage, TInboundMessage>
113
+ : TClusterUrl extends MainnetUrl
114
+ ? RpcSubscriptionsChannelMainnet<TOutboundMessage, TInboundMessage>
115
+ : RpcSubscriptionsChannel<TOutboundMessage, TInboundMessage>;
116
+
117
+ /**
118
+ * A {@link RpcSubscriptionsTransport} that communicates with the devnet cluster.
119
+ *
120
+ * Such transports are understood to communicate with a RPC server that services devnet, and as such
121
+ * might only be accepted for use as the transport of a {@link RpcSubscriptionsDevnet}.
122
+ *
123
+ * This is useful in cases where you need to make assertions about what capabilities a RPC offers.
124
+ * You can use the ability to assert on the type of RPC transport at compile time to prevent calling
125
+ * unimplemented methods or presuming the existence of unavailable programs or data.
126
+ */
127
+ export type RpcSubscriptionsTransportDevnet = RpcSubscriptionsTransport & { '~cluster': 'devnet' };
128
+ /**
129
+ * A {@link RpcSubscriptionsTransport} that communicates with the testnet cluster.
130
+ *
131
+ * Such transports are understood to communicate with a RPC server that services testnet, and as
132
+ * such might only be accepted for use as the transport of a {@link RpcSubscriptionsTestnet}.
133
+ *
134
+ * This is useful in cases where you need to make assertions about what capabilities a RPC offers.
135
+ * You can use the ability to assert on the type of RPC transport at compile time to prevent calling
136
+ * unimplemented methods or presuming the existence of unavailable programs or data.
137
+ */
138
+ export type RpcSubscriptionsTransportTestnet = RpcSubscriptionsTransport & { '~cluster': 'testnet' };
139
+ /**
140
+ * A {@link RpcSubscriptionsTransport} that communicates with the mainnet cluster.
141
+ *
142
+ * Such transports are understood to communicate with a RPC server that services mainnet, and as
143
+ * such might only be accepted for use as the transport of a {@link RpcSubscriptionsMainnet}.
144
+ *
145
+ * This is useful in cases where you need to make assertions about what capabilities a RPC offers.
146
+ * You can use the ability to assert on the type of RPC transport at compile time to prevent calling
147
+ * unimplemented methods or presuming the existence of unavailable programs or data.
148
+ */
149
+ export type RpcSubscriptionsTransportMainnet = RpcSubscriptionsTransport & { '~cluster': 'mainnet' };
150
+ export type RpcSubscriptionsTransportWithCluster =
151
+ | RpcSubscriptionsTransportDevnet
152
+ | RpcSubscriptionsTransportMainnet
153
+ | RpcSubscriptionsTransportTestnet;
154
+ /**
155
+ * Given a {@link ClusterUrl}, this utility type will resolve to as specific a
156
+ * {@link RpcSubscriptionsTransport} as possible.
157
+ *
158
+ * @example
159
+ * ```ts
160
+ * function createCustomSubscriptionsTransport<TClusterUrl extends ClusterUrl>(
161
+ * clusterUrl: TClusterUrl,
162
+ * ): RpcSubscriptionsTransportFromClusterUrl<TClusterUrl> {
163
+ * /* ... *\/
164
+ * }
165
+ *
166
+ * const transport = createCustomSubscriptionsTransport(testnet('ws://api.testnet.solana.com'));
167
+ * transport satisfies RpcSubscriptionsTransportTestnet; // OK
168
+ * ```
169
+ */
170
+ export type RpcSubscriptionsTransportFromClusterUrl<TClusterUrl extends ClusterUrl> = TClusterUrl extends DevnetUrl
171
+ ? RpcSubscriptionsTransportDevnet
172
+ : TClusterUrl extends TestnetUrl
173
+ ? RpcSubscriptionsTransportTestnet
174
+ : TClusterUrl extends MainnetUrl
175
+ ? RpcSubscriptionsTransportMainnet
176
+ : RpcSubscriptionsTransport;
177
+ /**
178
+ * A {@link RpcSubscriptions} that supports the RPC Subscriptions methods available on the devnet
179
+ * cluster.
180
+ *
181
+ * This is useful in cases where you need to make assertions about the suitability of a RPC for a
182
+ * given purpose. For example, you might like to make it a type error to combine certain types with
183
+ * RPCs belonging to certain clusters, at compile time.
184
+ *
185
+ * @example
186
+ * ```ts
187
+ * async function subscribeToSpecialAccountNotifications(
188
+ * address: Address<'ReAL1111111111111111111111111111'>,
189
+ * rpcSubscriptions: RpcSubscriptionsMainnet<unknown>,
190
+ * abortSignal: AbortSignal,
191
+ * ): Promise<AsyncIterable<SpecialAccountInfo>>;
192
+ * async function subscribeToSpecialAccountNotifications(
193
+ * address: Address<'TeST1111111111111111111111111111'>,
194
+ * rpcSubscriptions: RpcSubscriptionsDevnet<unknown> | RpcTestnet<unknown>,
195
+ * abortSignal: AbortSignal,
196
+ * ): Promise<AsyncIterable<SpecialAccountInfo>>;
197
+ * async function subscribeToSpecialAccountNotifications(
198
+ * address: Address,
199
+ * rpcSubscriptions: RpcSubscriptions<unknown>,
200
+ * abortSignal: AbortSignal,
201
+ * ): Promise<AsyncIterable<SpecialAccountInfo>> {
202
+ * /* ... *\/
203
+ * }
204
+ * const rpcSubscriptions = createSolanaRpcSubscriptions(devnet('https://api.devnet.solana.com'));
205
+ * await subscribeToSpecialAccountNotifications(address('ReAL1111111111111111111111111111'), rpcSubscriptions); // ERROR
206
+ * ```
207
+ */
208
+ export type RpcSubscriptionsDevnet<TRpcMethods> = RpcSubscriptions<TRpcMethods> & { '~cluster': 'devnet' };
209
+ /**
210
+ * A {@link RpcSubscriptions} that supports the RPC Subscriptions methods available on the testnet
211
+ * cluster.
212
+ *
213
+ * This is useful in cases where you need to make assertions about the suitability of a RPC for a
214
+ * given purpose. For example, you might like to make it a type error to combine certain types with
215
+ * RPCs belonging to certain clusters, at compile time.
216
+ *
217
+ * @example
218
+ * ```ts
219
+ * async function subscribeToSpecialAccountNotifications(
220
+ * address: Address<'ReAL1111111111111111111111111111'>,
221
+ * rpcSubscriptions: RpcSubscriptionsMainnet<unknown>,
222
+ * abortSignal: AbortSignal,
223
+ * ): Promise<AsyncIterable<SpecialAccountInfo>>;
224
+ * async function subscribeToSpecialAccountNotifications(
225
+ * address: Address<'TeST1111111111111111111111111111'>,
226
+ * rpcSubscriptions: RpcSubscriptionsDevnet<unknown> | RpcTestnet<unknown>,
227
+ * abortSignal: AbortSignal,
228
+ * ): Promise<AsyncIterable<SpecialAccountInfo>>;
229
+ * async function subscribeToSpecialAccountNotifications(
230
+ * address: Address,
231
+ * rpcSubscriptions: RpcSubscriptions<unknown>,
232
+ * abortSignal: AbortSignal,
233
+ * ): Promise<AsyncIterable<SpecialAccountInfo>> {
234
+ * /* ... *\/
235
+ * }
236
+ * const rpcSubscriptions = createSolanaRpcSubscriptions(devnet('https://api.devnet.solana.com'));
237
+ * await subscribeToSpecialAccountNotifications(address('ReAL1111111111111111111111111111'), rpcSubscriptions); // ERROR
238
+ * ```
239
+ */
240
+
241
+ export type RpcSubscriptionsTestnet<TRpcMethods> = RpcSubscriptions<TRpcMethods> & { '~cluster': 'testnet' };
242
+ /**
243
+ * A {@link RpcSubscriptions} that supports the RPC Subscriptions methods available on the mainnet
244
+ * cluster.
245
+ *
246
+ * This is useful in cases where you need to make assertions about the suitability of a RPC for a
247
+ * given purpose. For example, you might like to make it a type error to combine certain types with
248
+ * RPCs belonging to certain clusters, at compile time.
249
+ *
250
+ * @example
251
+ * ```ts
252
+ * async function subscribeToSpecialAccountNotifications(
253
+ * address: Address<'ReAL1111111111111111111111111111'>,
254
+ * rpcSubscriptions: RpcSubscriptionsMainnet<unknown>,
255
+ * abortSignal: AbortSignal,
256
+ * ): Promise<AsyncIterable<SpecialAccountInfo>>;
257
+ * async function subscribeToSpecialAccountNotifications(
258
+ * address: Address<'TeST1111111111111111111111111111'>,
259
+ * rpcSubscriptions: RpcSubscriptionsDevnet<unknown> | RpcTestnet<unknown>,
260
+ * abortSignal: AbortSignal,
261
+ * ): Promise<AsyncIterable<SpecialAccountInfo>>;
262
+ * async function subscribeToSpecialAccountNotifications(
263
+ * address: Address,
264
+ * rpcSubscriptions: RpcSubscriptions<unknown>,
265
+ * abortSignal: AbortSignal,
266
+ * ): Promise<AsyncIterable<SpecialAccountInfo>> {
267
+ * /* ... *\/
268
+ * }
269
+ * const rpcSubscriptions = createSolanaRpcSubscriptions(devnet('https://api.devnet.solana.com'));
270
+ * await subscribeToSpecialAccountNotifications(address('ReAL1111111111111111111111111111'), rpcSubscriptions); // ERROR
271
+ * ```
272
+ */
273
+ export type RpcSubscriptionsMainnet<TRpcMethods> = RpcSubscriptions<TRpcMethods> & { '~cluster': 'mainnet' };
274
+ /**
275
+ * Given a {@link RpcSubscriptionsTransport} and a set of RPC methods denoted by `TRpcMethods`, this
276
+ * utility type will resolve to a {@link RpcSubscriptions} that supports those methods on as
277
+ * specific a cluster as possible.
278
+ *
279
+ * @example
280
+ * ```ts
281
+ * function createCustomRpcSubscriptions<TRpcSubscriptionsTransport extends RpcSubscriptionsTransport>(
282
+ * transport: TRpcSubscriptionsTransport,
283
+ * ): RpcSubscriptionsFromTransport<MyCustomRpcMethods, TRpcSubscriptionsTransport> {
284
+ * /* ... *\/
285
+ * }
286
+ * const transport = createDefaultRpcSubscriptionsTransport({
287
+ * createChannel: createDefaultSolanaRpcSubscriptionsChannelCreator({
288
+ * url: mainnet('ws://rpc.company'),
289
+ * }),
290
+ * });
291
+ * transport satisfies RpcSubscriptionsTransportMainnet; // OK
292
+ * const rpcSubscriptions = createCustomRpcSubscriptions(transport);
293
+ * rpcSubscriptions satisfies RpcSubscriptionsMainnet<MyCustomRpcMethods>; // OK
294
+ * ```
295
+ */
296
+ export type RpcSubscriptionsFromTransport<
297
+ TRpcMethods,
298
+ TRpcSubscriptionsTransport extends RpcSubscriptionsTransport,
299
+ > = TRpcSubscriptionsTransport extends RpcSubscriptionsTransportDevnet
300
+ ? RpcSubscriptionsDevnet<TRpcMethods>
301
+ : TRpcSubscriptionsTransport extends RpcSubscriptionsTransportTestnet
302
+ ? RpcSubscriptionsTestnet<TRpcMethods>
303
+ : TRpcSubscriptionsTransport extends RpcSubscriptionsTransportMainnet
304
+ ? RpcSubscriptionsMainnet<TRpcMethods>
305
+ : RpcSubscriptions<TRpcMethods>;
@@ -0,0 +1,73 @@
1
+ import { AbortController } from '@solana/event-target-impl';
2
+ import fastStableStringify from '@solana/fast-stable-stringify';
3
+ import { RpcSubscriptionsTransport } from '@solana/rpc-subscriptions-spec';
4
+ import { DataPublisher } from '@solana/subscribable';
5
+
6
+ type CacheEntry = {
7
+ readonly abortController: AbortController;
8
+ readonly dataPublisherPromise: Promise<DataPublisher>;
9
+ numSubscribers: number;
10
+ };
11
+
12
+ /**
13
+ * Given a {@link RpcSubscriptionsTransport}, will return a new transport that coalesces identical
14
+ * subscriptions into a single subscription request to the server. The determination of whether a
15
+ * subscription is the same as another is based on the `rpcRequest` returned by its
16
+ * {@link RpcSubscriptionsPlan}. The subscription will only be aborted once all subscribers abort,
17
+ * or there is an error.
18
+ */
19
+ export function getRpcSubscriptionsTransportWithSubscriptionCoalescing<TTransport extends RpcSubscriptionsTransport>(
20
+ transport: TTransport,
21
+ ): TTransport {
22
+ const cache = new Map<string, CacheEntry>();
23
+ return function rpcSubscriptionsTransportWithSubscriptionCoalescing(config) {
24
+ const { request, signal } = config;
25
+ const subscriptionConfigurationHash = fastStableStringify([request.methodName, request.params]);
26
+
27
+ let cachedDataPublisherPromise = cache.get(subscriptionConfigurationHash);
28
+ if (!cachedDataPublisherPromise) {
29
+ const abortController = new AbortController();
30
+ const dataPublisherPromise = transport({
31
+ ...config,
32
+ signal: abortController.signal,
33
+ });
34
+ dataPublisherPromise
35
+ .then(dataPublisher => {
36
+ dataPublisher.on(
37
+ 'error',
38
+ () => {
39
+ cache.delete(subscriptionConfigurationHash);
40
+ abortController.abort();
41
+ },
42
+ { signal: abortController.signal },
43
+ );
44
+ })
45
+ .catch(() => {});
46
+ cache.set(
47
+ subscriptionConfigurationHash,
48
+ (cachedDataPublisherPromise = {
49
+ abortController,
50
+ dataPublisherPromise,
51
+ numSubscribers: 0,
52
+ }),
53
+ );
54
+ }
55
+ cachedDataPublisherPromise.numSubscribers++;
56
+ signal.addEventListener(
57
+ 'abort',
58
+ () => {
59
+ cachedDataPublisherPromise.numSubscribers--;
60
+ if (cachedDataPublisherPromise.numSubscribers === 0) {
61
+ queueMicrotask(() => {
62
+ if (cachedDataPublisherPromise.numSubscribers === 0) {
63
+ cache.delete(subscriptionConfigurationHash);
64
+ cachedDataPublisherPromise.abortController.abort();
65
+ }
66
+ });
67
+ }
68
+ },
69
+ { signal: cachedDataPublisherPromise.abortController.signal },
70
+ );
71
+ return cachedDataPublisherPromise.dataPublisherPromise;
72
+ } as TTransport;
73
+ }
@@ -0,0 +1,24 @@
1
+ import { pipe } from '@solana/functional';
2
+ import { parseJsonWithBigInts, stringifyJsonWithBigInts } from '@solana/rpc-spec-types';
3
+ import {
4
+ RpcSubscriptionsChannel,
5
+ transformChannelInboundMessages,
6
+ transformChannelOutboundMessages,
7
+ } from '@solana/rpc-subscriptions-spec';
8
+
9
+ /**
10
+ * Similarly, to {@link getRpcSubscriptionsChannelWithJSONSerialization}, this function will
11
+ * stringify and parse JSON message to and from the given `string` channel. However, this function
12
+ * parses any integer value as a `BigInt` in order to safely handle numbers that exceed the
13
+ * JavaScript [`Number.MAX_SAFE_INTEGER`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER)
14
+ * value.
15
+ */
16
+ export function getRpcSubscriptionsChannelWithBigIntJSONSerialization(
17
+ channel: RpcSubscriptionsChannel<string, string>,
18
+ ): RpcSubscriptionsChannel<unknown, unknown> {
19
+ return pipe(
20
+ channel,
21
+ c => transformChannelInboundMessages(c, parseJsonWithBigInts),
22
+ c => transformChannelOutboundMessages(c, stringifyJsonWithBigInts),
23
+ );
24
+ }
@@ -0,0 +1,21 @@
1
+ import { pipe } from '@solana/functional';
2
+ import {
3
+ RpcSubscriptionsChannel,
4
+ transformChannelInboundMessages,
5
+ transformChannelOutboundMessages,
6
+ } from '@solana/rpc-subscriptions-spec';
7
+
8
+ /**
9
+ * Given a {@link RpcSubscriptionsChannel}, will return a new channel that parses data published to
10
+ * the `'message'` channel as JSON, and JSON-stringifies messages sent via the
11
+ * {@link RpcSubscriptionsChannel.send | send(message)} method.
12
+ */
13
+ export function getRpcSubscriptionsChannelWithJSONSerialization(
14
+ channel: RpcSubscriptionsChannel<string, string>,
15
+ ): RpcSubscriptionsChannel<unknown, unknown> {
16
+ return pipe(
17
+ channel,
18
+ c => transformChannelInboundMessages(c, JSON.parse),
19
+ c => transformChannelOutboundMessages(c, JSON.stringify),
20
+ );
21
+ }
@@ -0,0 +1,56 @@
1
+ import { pipe } from '@solana/functional';
2
+ import { RpcSubscriptionsChannelCreator, RpcSubscriptionsTransport } from '@solana/rpc-subscriptions-spec';
3
+ import { ClusterUrl } from '@solana/rpc-types';
4
+
5
+ import {
6
+ RpcSubscriptionsChannelCreatorDevnet,
7
+ RpcSubscriptionsChannelCreatorFromClusterUrl,
8
+ RpcSubscriptionsChannelCreatorMainnet,
9
+ RpcSubscriptionsChannelCreatorTestnet,
10
+ RpcSubscriptionsTransportDevnet,
11
+ RpcSubscriptionsTransportFromClusterUrl,
12
+ RpcSubscriptionsTransportMainnet,
13
+ RpcSubscriptionsTransportTestnet,
14
+ } from './rpc-subscriptions-clusters';
15
+ import { getRpcSubscriptionsTransportWithSubscriptionCoalescing } from './rpc-subscriptions-coalescer';
16
+
17
+ export type DefaultRpcSubscriptionsTransportConfig<TClusterUrl extends ClusterUrl> = Readonly<{
18
+ createChannel: RpcSubscriptionsChannelCreatorFromClusterUrl<TClusterUrl, unknown, unknown>;
19
+ }>;
20
+
21
+ /**
22
+ * Creates a {@link RpcSubscriptionsTransport} with some default behaviours.
23
+ *
24
+ * The default behaviours include:
25
+ * - Logic that coalesces multiple subscriptions for the same notifications with the same arguments
26
+ * into a single subscription.
27
+ *
28
+ * @param config
29
+ */
30
+ export function createDefaultRpcSubscriptionsTransport<TClusterUrl extends ClusterUrl>({
31
+ createChannel,
32
+ }: DefaultRpcSubscriptionsTransportConfig<TClusterUrl>) {
33
+ return pipe(
34
+ createRpcSubscriptionsTransportFromChannelCreator(
35
+ createChannel,
36
+ ) as RpcSubscriptionsTransport as RpcSubscriptionsTransportFromClusterUrl<TClusterUrl>,
37
+ transport => getRpcSubscriptionsTransportWithSubscriptionCoalescing(transport),
38
+ );
39
+ }
40
+
41
+ export function createRpcSubscriptionsTransportFromChannelCreator<
42
+ TChannelCreator extends RpcSubscriptionsChannelCreator<TOutboundMessage, TInboundMessage>,
43
+ TInboundMessage,
44
+ TOutboundMessage,
45
+ >(createChannel: TChannelCreator) {
46
+ return (async ({ execute, signal }) => {
47
+ const channel = await createChannel({ abortSignal: signal });
48
+ return await execute({ channel, signal });
49
+ }) as TChannelCreator extends RpcSubscriptionsChannelCreatorDevnet<TOutboundMessage, TInboundMessage>
50
+ ? RpcSubscriptionsTransportDevnet
51
+ : TChannelCreator extends RpcSubscriptionsChannelCreatorTestnet<TOutboundMessage, TInboundMessage>
52
+ ? RpcSubscriptionsTransportTestnet
53
+ : TChannelCreator extends RpcSubscriptionsChannelCreatorMainnet<TOutboundMessage, TInboundMessage>
54
+ ? RpcSubscriptionsTransportMainnet
55
+ : RpcSubscriptionsTransport;
56
+ }
@@ -0,0 +1,69 @@
1
+ import type { SolanaRpcSubscriptionsApi, SolanaRpcSubscriptionsApiUnstable } from '@solana/rpc-subscriptions-api';
2
+ import { createSolanaRpcSubscriptionsApi } from '@solana/rpc-subscriptions-api';
3
+ import {
4
+ createSubscriptionRpc,
5
+ RpcSubscriptionsApiMethods,
6
+ type RpcSubscriptionsTransport,
7
+ } from '@solana/rpc-subscriptions-spec';
8
+ import { ClusterUrl } from '@solana/rpc-types';
9
+
10
+ import { DEFAULT_RPC_SUBSCRIPTIONS_CONFIG } from './rpc-default-config';
11
+ import {
12
+ createDefaultSolanaRpcSubscriptionsChannelCreator,
13
+ DefaultRpcSubscriptionsChannelConfig,
14
+ } from './rpc-subscriptions-channel';
15
+ import type { RpcSubscriptionsFromTransport } from './rpc-subscriptions-clusters';
16
+ import { createDefaultRpcSubscriptionsTransport } from './rpc-subscriptions-transport';
17
+
18
+ type Config<TClusterUrl extends ClusterUrl> = DefaultRpcSubscriptionsChannelConfig<TClusterUrl>;
19
+
20
+ function createSolanaRpcSubscriptionsImpl<TClusterUrl extends ClusterUrl, TApi extends RpcSubscriptionsApiMethods>(
21
+ clusterUrl: TClusterUrl,
22
+ config?: Omit<Config<TClusterUrl>, 'url'>,
23
+ ) {
24
+ const transport = createDefaultRpcSubscriptionsTransport({
25
+ createChannel: createDefaultSolanaRpcSubscriptionsChannelCreator({ ...config, url: clusterUrl }),
26
+ });
27
+ return createSolanaRpcSubscriptionsFromTransport<typeof transport, TApi>(transport);
28
+ }
29
+
30
+ /**
31
+ * Creates a {@link RpcSubscriptions} instance that exposes the Solana JSON RPC WebSocket API given
32
+ * a cluster URL and some optional channel config. See
33
+ * {@link createDefaultRpcSubscriptionsChannelCreator} for the shape of the channel config.
34
+ */
35
+ export function createSolanaRpcSubscriptions<TClusterUrl extends ClusterUrl>(
36
+ clusterUrl: TClusterUrl,
37
+ config?: Omit<Config<TClusterUrl>, 'url'>,
38
+ ) {
39
+ return createSolanaRpcSubscriptionsImpl<TClusterUrl, SolanaRpcSubscriptionsApi>(clusterUrl, config);
40
+ }
41
+
42
+ /**
43
+ * Creates a {@link RpcSubscriptions} instance that exposes the Solana JSON RPC WebSocket API,
44
+ * including its unstable methods, given a cluster URL and some optional channel config. See
45
+ * {@link createDefaultRpcSubscriptionsChannelCreator} for the shape of the channel config.
46
+ */
47
+ export function createSolanaRpcSubscriptions_UNSTABLE<TClusterUrl extends ClusterUrl>(
48
+ clusterUrl: TClusterUrl,
49
+ config?: Omit<Config<TClusterUrl>, 'url'>,
50
+ ) {
51
+ return createSolanaRpcSubscriptionsImpl<TClusterUrl, SolanaRpcSubscriptionsApi & SolanaRpcSubscriptionsApiUnstable>(
52
+ clusterUrl,
53
+ config,
54
+ );
55
+ }
56
+
57
+ /**
58
+ * Creates a {@link RpcSubscriptions} instance that exposes the Solana JSON RPC WebSocket API given
59
+ * the supplied {@link RpcSubscriptionsTransport}.
60
+ */
61
+ export function createSolanaRpcSubscriptionsFromTransport<
62
+ TTransport extends RpcSubscriptionsTransport,
63
+ TApi extends RpcSubscriptionsApiMethods = SolanaRpcSubscriptionsApi,
64
+ >(transport: TTransport) {
65
+ return createSubscriptionRpc({
66
+ api: createSolanaRpcSubscriptionsApi<TApi>(DEFAULT_RPC_SUBSCRIPTIONS_CONFIG),
67
+ transport,
68
+ }) as RpcSubscriptionsFromTransport<TApi, TTransport>;
69
+ }