Skip to content

Commit

Permalink
refactor(experimental): add cluster level subscriptions API for trans…
Browse files Browse the repository at this point in the history
…ports

This change mirrors the first change in the stack, but for subscriptions.
  • Loading branch information
buffalojoec authored Feb 2, 2024
1 parent 5a6335d commit cbf8f38
Show file tree
Hide file tree
Showing 7 changed files with 246 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { devnet, IRpcApiSubscriptions, mainnet, RpcSubscriptions, testnet } from '@solana/rpc-types';

import { createJsonRpcSubscriptionsApi } from '../apis/subscriptions/subscriptions-api';
import { createJsonSubscriptionRpc } from '../json-rpc-subscription';
import { RpcSubscriptionsDevnet, RpcSubscriptionsMainnet, RpcSubscriptionsTestnet } from '../json-rpc-types';
import { createWebSocketTransport } from '../transports/websocket/websocket-transport';

interface MySubscriptionApiMethods extends IRpcApiSubscriptions {
foo(): number;
bar(): string;
}

const api = null as unknown as ReturnType<typeof createJsonRpcSubscriptionsApi<MySubscriptionApiMethods>>;

const genericTransport = createWebSocketTransport({ sendBufferHighWatermark: 0, url: 'http://localhost:8899' });
const devnetTransport = createWebSocketTransport({
sendBufferHighWatermark: 0,
url: devnet('https://api.devnet.solana.com'),
});
const testnetTransport = createWebSocketTransport({
sendBufferHighWatermark: 0,
url: testnet('https://api.testnet.solana.com'),
});
const mainnetTransport = createWebSocketTransport({
sendBufferHighWatermark: 0,
url: mainnet('https://api.mainnet-beta.solana.com'),
});

// When providing a generic transport, the RPC should be typed as RpcSubscription
createJsonSubscriptionRpc({ api, transport: genericTransport }) satisfies RpcSubscriptions<MySubscriptionApiMethods>;
createJsonSubscriptionRpc({
api,
transport: genericTransport,
//@ts-expect-error Should not be a devnet transport
}) satisfies RpcSubscriptionsDevnet<MySubscriptionApiMethods>;
createJsonSubscriptionRpc({
api,
transport: genericTransport,
//@ts-expect-error Should not be a testnet transport
}) satisfies RpcSubscriptionsTestnet<MySubscriptionApiMethods>;
createJsonSubscriptionRpc({
api,
transport: genericTransport,
//@ts-expect-error Should not be a mainnet transport
}) satisfies RpcSubscriptionsMainnet<MySubscriptionApiMethods>;

// When providing a devnet transport, the RPC should be typed as RpcSubscriptionsDevnet
createJsonSubscriptionRpc({
api,
transport: devnetTransport,
}) satisfies RpcSubscriptionsDevnet<MySubscriptionApiMethods>;
createJsonSubscriptionRpc({
api,
transport: devnetTransport,
//@ts-expect-error Should not be a testnet transport
}) satisfies RpcSubscriptionsTestnet<MySubscriptionApiMethods>;
createJsonSubscriptionRpc({
api,
transport: devnetTransport,
//@ts-expect-error Should not be a mainnet transport
}) satisfies RpcSubscriptionsMainnet<MySubscriptionApiMethods>;

// When providing a testnet transport, the RPC should be typed as RpcSubscriptionsTestnet
createJsonSubscriptionRpc({
api,
transport: testnetTransport,
}) satisfies RpcSubscriptionsTestnet<MySubscriptionApiMethods>;
createJsonSubscriptionRpc({
api,
transport: testnetTransport,
//@ts-expect-error Should not be a devnet transport
}) satisfies RpcSubscriptionsDevnet<MySubscriptionApiMethods>;
createJsonSubscriptionRpc({
api,
transport: testnetTransport,
//@ts-expect-error Should not be a mainnet transport
}) satisfies RpcSubscriptionsMainnet<MySubscriptionApiMethods>;

// When providing a mainnet transport, the RPC should be typed as RpcSubscriptionsMainnet
createJsonSubscriptionRpc({
api,
transport: mainnetTransport,
}) satisfies RpcSubscriptionsMainnet<MySubscriptionApiMethods>;
createJsonSubscriptionRpc({
api,
transport: mainnetTransport,
//@ts-expect-error Should not be a devnet transport
}) satisfies RpcSubscriptionsDevnet<MySubscriptionApiMethods>;
createJsonSubscriptionRpc({
api,
transport: mainnetTransport,
//@ts-expect-error Should not be a testnet transport
}) satisfies RpcSubscriptionsTestnet<MySubscriptionApiMethods>;
9 changes: 7 additions & 2 deletions packages/rpc-transport/src/json-rpc-config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { IRpcApi, IRpcSubscriptionsApi } from '@solana/rpc-types';

import { IRpcTransport, IRpcTransportWithCluster, IRpcWebSocketTransport } from './transports/transport-types';
import {
IRpcTransport,
IRpcTransportWithCluster,
IRpcWebSocketTransport,
IRpcWebSocketTransportWithCluster,
} from './transports/transport-types';

export type RpcConfig<TRpcMethods> = Readonly<{
api: IRpcApi<TRpcMethods>;
Expand All @@ -9,5 +14,5 @@ export type RpcConfig<TRpcMethods> = Readonly<{

export type RpcSubscriptionConfig<TRpcMethods> = Readonly<{
api: IRpcSubscriptionsApi<TRpcMethods>;
transport: IRpcWebSocketTransport;
transport: IRpcWebSocketTransport | IRpcWebSocketTransportWithCluster;
}>;
51 changes: 47 additions & 4 deletions packages/rpc-transport/src/json-rpc-subscription.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,27 @@
import { PendingRpcSubscription, RpcSubscription, RpcSubscriptions, SubscribeOptions } from '@solana/rpc-types';
import {
IRpcSubscriptionsApi,
PendingRpcSubscription,
RpcSubscription,
RpcSubscriptions,
SubscribeOptions,
} from '@solana/rpc-types';

import { JsonRpcResponse } from './json-rpc';
import { RpcSubscriptionConfig } from './json-rpc-config';
import { SolanaJsonRpcError } from './json-rpc-errors';
import { createJsonRpcMessage } from './json-rpc-message';
import {
RpcSubscriptionsDevnet,
RpcSubscriptionsFromTransport,
RpcSubscriptionsMainnet,
RpcSubscriptionsTestnet,
} from './json-rpc-types';
import {
IRpcWebSocketTransport,
IRpcWebSocketTransportDevnet,
IRpcWebSocketTransportMainnet,
IRpcWebSocketTransportTestnet,
} from './transports/transport-types';

type JsonRpcNotification<TNotification> = Readonly<{
params: Readonly<{
Expand Down Expand Up @@ -136,7 +154,32 @@ function makeProxy<TRpcSubscriptionMethods>(
}

export function createJsonSubscriptionRpc<TRpcSubscriptionMethods>(
rpcConfig: RpcSubscriptionConfig<TRpcSubscriptionMethods>,
): RpcSubscriptions<TRpcSubscriptionMethods> {
return makeProxy(rpcConfig);
rpcConfig: Readonly<{
api: IRpcSubscriptionsApi<TRpcSubscriptionMethods>;
transport: IRpcWebSocketTransportDevnet;
}>,
): RpcSubscriptionsDevnet<TRpcSubscriptionMethods>;
export function createJsonSubscriptionRpc<TRpcSubscriptionMethods>(
rpcConfig: Readonly<{
api: IRpcSubscriptionsApi<TRpcSubscriptionMethods>;
transport: IRpcWebSocketTransportTestnet;
}>,
): RpcSubscriptionsTestnet<TRpcSubscriptionMethods>;
export function createJsonSubscriptionRpc<TRpcSubscriptionMethods>(
rpcConfig: Readonly<{
api: IRpcSubscriptionsApi<TRpcSubscriptionMethods>;
transport: IRpcWebSocketTransportMainnet;
}>,
): RpcSubscriptionsMainnet<TRpcSubscriptionMethods>;
export function createJsonSubscriptionRpc<TRpcSubscriptionMethods>(
rpcConfig: Readonly<{
api: IRpcSubscriptionsApi<TRpcSubscriptionMethods>;
transport: IRpcWebSocketTransport;
}>,
): RpcSubscriptions<TRpcSubscriptionMethods>;
export function createJsonSubscriptionRpc<
TRpcSubscriptionMethods,
TConfig extends RpcSubscriptionConfig<TRpcSubscriptionMethods>,
>(rpcConfig: TConfig): RpcSubscriptionsFromTransport<TRpcSubscriptionMethods, TConfig['transport']> {
return makeProxy(rpcConfig) as RpcSubscriptionsFromTransport<TRpcSubscriptionMethods, TConfig['transport']>;
}
27 changes: 26 additions & 1 deletion packages/rpc-transport/src/json-rpc-types.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { Rpc } from '@solana/rpc-types';
import { Rpc, RpcSubscriptions } from '@solana/rpc-types';

import {
IRpcTransport,
IRpcTransportDevnet,
IRpcTransportMainnet,
IRpcTransportTestnet,
IRpcTransportWithCluster,
IRpcWebSocketTransport,
IRpcWebSocketTransportDevnet,
IRpcWebSocketTransportMainnet,
IRpcWebSocketTransportTestnet,
IRpcWebSocketTransportWithCluster,
} from './transports/transport-types';

export type RpcDevnet<TRpcMethods> = Rpc<TRpcMethods> & { '~cluster': 'devnet' };
Expand All @@ -21,3 +26,23 @@ export type RpcFromTransport<
: TRpcTransport extends IRpcTransportMainnet
? RpcMainnet<TRpcMethods>
: Rpc<TRpcMethods>;

export type RpcSubscriptionsDevnet<TRpcSubscriptionMethods> = RpcSubscriptions<TRpcSubscriptionMethods> & {
'~cluster': 'devnet';
};
export type RpcSubscriptionsTestnet<TRpcSubscriptionMethods> = RpcSubscriptions<TRpcSubscriptionMethods> & {
'~cluster': 'testnet';
};
export type RpcSubscriptionsMainnet<TRpcSubscriptionMethods> = RpcSubscriptions<TRpcSubscriptionMethods> & {
'~cluster': 'mainnet';
};
export type RpcSubscriptionsFromTransport<
TRpcSubscriptionMethods,
TRpcTransport extends IRpcWebSocketTransport | IRpcWebSocketTransportWithCluster,
> = TRpcTransport extends IRpcWebSocketTransportDevnet
? RpcSubscriptionsDevnet<TRpcSubscriptionMethods>
: TRpcTransport extends IRpcWebSocketTransportTestnet
? RpcSubscriptionsTestnet<TRpcSubscriptionMethods>
: TRpcTransport extends IRpcWebSocketTransportMainnet
? RpcSubscriptionsMainnet<TRpcSubscriptionMethods>
: RpcSubscriptions<TRpcSubscriptionMethods>;
19 changes: 19 additions & 0 deletions packages/rpc-transport/src/transports/transport-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { ClusterUrl, DevnetUrl, MainnetUrl, TestnetUrl } from '@solana/rpc-types

import { RpcWebSocketConnection } from './websocket/websocket-connection';

// HTTP transport

type RpcTransportConfig = Readonly<{
payload: unknown;
signal?: AbortSignal;
Expand All @@ -27,6 +29,8 @@ type RpcWebSocketTransportConfig = Readonly<{
signal: AbortSignal;
}>;

// WebSocket transport

export interface IRpcWebSocketTransport {
(config: RpcWebSocketTransportConfig): Promise<
Readonly<
Expand All @@ -36,3 +40,18 @@ export interface IRpcWebSocketTransport {
>
>;
}

export type IRpcWebSocketTransportDevnet = IRpcWebSocketTransport & { '~cluster': 'devnet' };
export type IRpcWebSocketTransportTestnet = IRpcWebSocketTransport & { '~cluster': 'testnet' };
export type IRpcWebSocketTransportMainnet = IRpcWebSocketTransport & { '~cluster': 'mainnet' };
export type IRpcWebSocketTransportWithCluster =
| IRpcWebSocketTransportDevnet
| IRpcWebSocketTransportTestnet
| IRpcWebSocketTransportMainnet;
export type IRpcWebSocketTransportFromClusterUrl<TClusterUrl extends ClusterUrl> = TClusterUrl extends DevnetUrl
? IRpcWebSocketTransportDevnet
: TClusterUrl extends TestnetUrl
? IRpcWebSocketTransportTestnet
: TClusterUrl extends MainnetUrl
? IRpcWebSocketTransportMainnet
: IRpcWebSocketTransport;
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { devnet, mainnet, testnet } from '@solana/rpc-types';

import {
IRpcWebSocketTransport,
IRpcWebSocketTransportDevnet,
IRpcWebSocketTransportMainnet,
IRpcWebSocketTransportTestnet,
} from '../../transport-types';
import { createWebSocketTransport } from '../websocket-transport';

const genericConfig = { sendBufferHighWatermark: 0, url: 'http://localhost:8899' };
const devnetConfig = { sendBufferHighWatermark: 0, url: devnet('https://api.devnet.solana.com') };
const testnetConfig = { sendBufferHighWatermark: 0, url: testnet('https://api.testnet.solana.com') };
const mainnetConfig = { sendBufferHighWatermark: 0, url: mainnet('https://api.mainnet-beta.solana.com') };

// When providing a generic URL, the transport should be typed as an IRpcWebSocketTransport
createWebSocketTransport(genericConfig) satisfies IRpcWebSocketTransport;
//@ts-expect-error Should not be a devnet transport
createWebSocketTransport(genericConfig) satisfies IRpcWebSocketTransportDevnet;
//@ts-expect-error Should not be a testnet transport
createWebSocketTransport(genericConfig) satisfies IRpcWebSocketTransportTestnet;
//@ts-expect-error Should not be a mainnet transport
createWebSocketTransport(genericConfig) satisfies IRpcWebSocketTransportMainnet;

// When providing a devnet URL, the transport should be typed as an IRpcWebSocketTransportDevnet
createWebSocketTransport(devnetConfig) satisfies IRpcWebSocketTransportDevnet;
//@ts-expect-error Should not be a testnet transport
createWebSocketTransport(devnetConfig) satisfies IRpcWebSocketTransportTestnet;
//@ts-expect-error Should not be a mainnet transport
createWebSocketTransport(devnetConfig) satisfies IRpcWebSocketTransportMainnet;

// When providing a testnet URL, the transport should be typed as an IRpcWebSocketTransportTestnet
createWebSocketTransport(testnetConfig) satisfies IRpcWebSocketTransportTestnet;
//@ts-expect-error Should not be a devnet transport
createWebSocketTransport(testnetConfig) satisfies IRpcWebSocketTransportDevnet;
//@ts-expect-error Should not be a mainnet transport
createWebSocketTransport(testnetConfig) satisfies IRpcWebSocketTransportMainnet;

// When providing a mainnet URL, the transport should be typed as an IRpcWebSocketTransportMainnet
createWebSocketTransport(mainnetConfig) satisfies IRpcWebSocketTransportMainnet;
//@ts-expect-error Should not be a devnet transport
createWebSocketTransport(mainnetConfig) satisfies IRpcWebSocketTransportDevnet;
//@ts-expect-error Should not be a testnet transport
createWebSocketTransport(mainnetConfig) satisfies IRpcWebSocketTransportTestnet;
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { IRpcWebSocketTransport } from '../transport-types';
import { ClusterUrl } from '@solana/rpc-types';

import { IRpcWebSocketTransport, IRpcWebSocketTransportFromClusterUrl } from '../transport-types';
import { createWebSocketConnection } from './websocket-connection';

type Config = Readonly<{
type Config<TClusterUrl extends ClusterUrl> = Readonly<{
sendBufferHighWatermark: number;
url: string;
url: TClusterUrl;
}>;

export function createWebSocketTransport({ sendBufferHighWatermark, url }: Config): IRpcWebSocketTransport {
export function createWebSocketTransport<TClusterUrl extends ClusterUrl>({
sendBufferHighWatermark,
url,
}: Config<TClusterUrl>): IRpcWebSocketTransportFromClusterUrl<TClusterUrl> {
if (/^wss?:/i.test(url) === false) {
const protocolMatch = url.match(/^([^:]+):/);
throw new DOMException(
Expand All @@ -28,5 +33,5 @@ export function createWebSocketTransport({ sendBufferHighWatermark, url }: Confi
[Symbol.asyncIterator]: connection[Symbol.asyncIterator].bind(connection),
send_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: connection.send.bind(connection),
};
};
} as IRpcWebSocketTransportFromClusterUrl<TClusterUrl>;
}

0 comments on commit cbf8f38

Please sign in to comment.