UNPKG

@solana/rpc-subscriptions-spec

Version:

A generic implementation of JSON RPC Subscriptions using proxies

260 lines (255 loc) • 8.73 kB
import { SolanaError, SOLANA_ERROR__RPC_SUBSCRIPTIONS__CANNOT_CREATE_SUBSCRIPTION_PLAN, getSolanaErrorFromJsonRpcError, SOLANA_ERROR__RPC_SUBSCRIPTIONS__EXPECTED_SERVER_SUBSCRIPTION_ID, SOLANA_ERROR__INVARIANT_VIOLATION__DATA_PUBLISHER_CHANNEL_UNIMPLEMENTED } from '@solana/errors'; import { createAsyncIterableFromDataPublisher, demultiplexDataPublisher } from '@solana/subscribable'; import { safeRace } from '@solana/promises'; import { createRpcMessage } from '@solana/rpc-spec-types'; // src/rpc-subscriptions.ts function createSubscriptionRpc(rpcConfig) { return new Proxy(rpcConfig.api, { defineProperty() { return false; }, deleteProperty() { return false; }, get(target, p, receiver) { if (p === "then") { return void 0; } return function(...rawParams) { const notificationName = p.toString(); const createRpcSubscriptionPlan = Reflect.get(target, notificationName, receiver); if (!createRpcSubscriptionPlan) { throw new SolanaError(SOLANA_ERROR__RPC_SUBSCRIPTIONS__CANNOT_CREATE_SUBSCRIPTION_PLAN, { notificationName }); } const subscriptionPlan = createRpcSubscriptionPlan(...rawParams); return createPendingRpcSubscription(rpcConfig.transport, subscriptionPlan); }; } }); } function createPendingRpcSubscription(transport, subscriptionsPlan) { return { async subscribe({ abortSignal }) { const notificationsDataPublisher = await transport({ signal: abortSignal, ...subscriptionsPlan }); return createAsyncIterableFromDataPublisher({ abortSignal, dataChannelName: "notification", dataPublisher: notificationsDataPublisher, errorChannelName: "error" }); } }; } // src/rpc-subscriptions-api.ts function createRpcSubscriptionsApi(config) { return new Proxy({}, { defineProperty() { return false; }, deleteProperty() { return false; }, get(...args) { const [_, p] = args; const methodName = p.toString(); return function(...params) { const rawRequest = { methodName, params }; const request = config.requestTransformer ? config.requestTransformer(rawRequest) : rawRequest; return { execute(planConfig) { return config.planExecutor({ ...planConfig, request }); }, request }; }; } }); } // src/rpc-subscriptions-channel.ts function transformChannelInboundMessages(channel, transform) { return Object.freeze({ ...channel, on(type, subscriber, options) { if (type !== "message") { return channel.on( type, subscriber, options ); } return channel.on( "message", (message) => subscriber(transform(message)), options ); } }); } function transformChannelOutboundMessages(channel, transform) { return Object.freeze({ ...channel, send: (message) => channel.send(transform(message)) }); } // ../event-target-impl/dist/index.browser.mjs var o = globalThis.AbortController; var subscriberCountBySubscriptionIdByChannel = /* @__PURE__ */ new WeakMap(); function decrementSubscriberCountAndReturnNewCount(channel, subscriptionId) { return augmentSubscriberCountAndReturnNewCount(-1, channel, subscriptionId); } function incrementSubscriberCount(channel, subscriptionId) { augmentSubscriberCountAndReturnNewCount(1, channel, subscriptionId); } function getSubscriberCountBySubscriptionIdForChannel(channel) { let subscriberCountBySubscriptionId = subscriberCountBySubscriptionIdByChannel.get(channel); if (!subscriberCountBySubscriptionId) { subscriberCountBySubscriptionIdByChannel.set(channel, subscriberCountBySubscriptionId = {}); } return subscriberCountBySubscriptionId; } function augmentSubscriberCountAndReturnNewCount(amount, channel, subscriptionId) { if (subscriptionId === void 0) { return; } const subscriberCountBySubscriptionId = getSubscriberCountBySubscriptionIdForChannel(channel); if (!subscriberCountBySubscriptionId[subscriptionId] && amount > 0) { subscriberCountBySubscriptionId[subscriptionId] = 0; } const newCount = amount + subscriberCountBySubscriptionId[subscriptionId]; if (newCount <= 0) { delete subscriberCountBySubscriptionId[subscriptionId]; } else { subscriberCountBySubscriptionId[subscriptionId] = newCount; } return newCount; } var cache = /* @__PURE__ */ new WeakMap(); function getMemoizedDemultiplexedNotificationPublisherFromChannelAndResponseTransformer(channel, subscribeRequest, responseTransformer) { let publisherByResponseTransformer = cache.get(channel); if (!publisherByResponseTransformer) { cache.set(channel, publisherByResponseTransformer = /* @__PURE__ */ new WeakMap()); } const responseTransformerKey = responseTransformer ?? channel; let publisher = publisherByResponseTransformer.get(responseTransformerKey); if (!publisher) { publisherByResponseTransformer.set( responseTransformerKey, publisher = demultiplexDataPublisher(channel, "message", (rawMessage) => { const message = rawMessage; if (!("method" in message)) { return; } const transformedNotification = responseTransformer ? responseTransformer(message.params.result, subscribeRequest) : message.params.result; return [`notification:${message.params.subscription}`, transformedNotification]; }) ); } return publisher; } async function executeRpcPubSubSubscriptionPlan({ channel, responseTransformer, signal, subscribeRequest, unsubscribeMethodName }) { let subscriptionId; channel.on( "error", () => { subscriptionId = void 0; subscriberCountBySubscriptionIdByChannel.delete(channel); }, { signal } ); const abortPromise = new Promise((_, reject) => { function handleAbort() { if (decrementSubscriberCountAndReturnNewCount(channel, subscriptionId) === 0) { const unsubscribePayload = createRpcMessage({ methodName: unsubscribeMethodName, params: [subscriptionId] }); subscriptionId = void 0; channel.send(unsubscribePayload).catch(() => { }); } reject(this.reason); } if (signal.aborted) { handleAbort.call(signal); } else { signal.addEventListener("abort", handleAbort); } }); const subscribePayload = createRpcMessage(subscribeRequest); await channel.send(subscribePayload); const subscriptionIdPromise = new Promise((resolve, reject) => { const abortController = new o(); signal.addEventListener("abort", abortController.abort.bind(abortController)); const options = { signal: abortController.signal }; channel.on( "error", (err) => { abortController.abort(); reject(err); }, options ); channel.on( "message", (message) => { if (message && typeof message === "object" && "id" in message && message.id === subscribePayload.id) { abortController.abort(); if ("error" in message) { reject(getSolanaErrorFromJsonRpcError(message.error)); } else { resolve(message.result); } } }, options ); }); subscriptionId = await safeRace([abortPromise, subscriptionIdPromise]); if (subscriptionId == null) { throw new SolanaError(SOLANA_ERROR__RPC_SUBSCRIPTIONS__EXPECTED_SERVER_SUBSCRIPTION_ID); } incrementSubscriberCount(channel, subscriptionId); const notificationPublisher = getMemoizedDemultiplexedNotificationPublisherFromChannelAndResponseTransformer( channel, subscribeRequest, responseTransformer ); const notificationKey = `notification:${subscriptionId}`; return { on(type, listener, options) { switch (type) { case "notification": return notificationPublisher.on( notificationKey, listener, options ); case "error": return channel.on( "error", listener, options ); default: throw new SolanaError(SOLANA_ERROR__INVARIANT_VIOLATION__DATA_PUBLISHER_CHANNEL_UNIMPLEMENTED, { channelName: type, supportedChannelNames: ["notification", "error"] }); } } }; } export { createRpcSubscriptionsApi, createSubscriptionRpc, executeRpcPubSubSubscriptionPlan, transformChannelInboundMessages, transformChannelOutboundMessages }; //# sourceMappingURL=index.browser.mjs.map //# sourceMappingURL=index.browser.mjs.map