@solana/rpc-subscriptions-spec
Version:
A generic implementation of JSON RPC Subscriptions using proxies
255 lines (249 loc) • 8.33 kB
JavaScript
var errors = require('@solana/errors');
var subscribable = require('@solana/subscribable');
var promises = require('@solana/promises');
var rpcSpecTypes = require('@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) {
return function(...rawParams) {
const notificationName = p.toString();
const createRpcSubscriptionPlan = Reflect.get(target, notificationName, receiver);
if (!createRpcSubscriptionPlan) {
throw new errors.SolanaError(errors.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 subscribable.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 augmentSubscriberCountAndReturnNewCount(amount, channel, subscriptionId) {
if (subscriptionId === void 0) {
return;
}
let subscriberCountBySubscriptionId = subscriberCountBySubscriptionIdByChannel.get(channel);
if (!subscriberCountBySubscriptionId && amount > 0) {
subscriberCountBySubscriptionIdByChannel.set(
channel,
subscriberCountBySubscriptionId = { [subscriptionId]: 0 }
);
}
if (subscriberCountBySubscriptionId?.[subscriptionId] !== void 0) {
return subscriberCountBySubscriptionId[subscriptionId] = amount + subscriberCountBySubscriptionId[subscriptionId];
}
}
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 = subscribable.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 = rpcSpecTypes.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 = rpcSpecTypes.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(errors.getSolanaErrorFromJsonRpcError(message.error));
} else {
resolve(message.result);
}
}
},
options
);
});
subscriptionId = await promises.safeRace([abortPromise, subscriptionIdPromise]);
if (subscriptionId == null) {
throw new errors.SolanaError(errors.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 errors.SolanaError(errors.SOLANA_ERROR__INVARIANT_VIOLATION__DATA_PUBLISHER_CHANNEL_UNIMPLEMENTED, {
channelName: type,
supportedChannelNames: ["notification", "error"]
});
}
}
};
}
exports.createRpcSubscriptionsApi = createRpcSubscriptionsApi;
exports.createSubscriptionRpc = createSubscriptionRpc;
exports.executeRpcPubSubSubscriptionPlan = executeRpcPubSubSubscriptionPlan;
exports.transformChannelInboundMessages = transformChannelInboundMessages;
exports.transformChannelOutboundMessages = transformChannelOutboundMessages;
//# sourceMappingURL=index.browser.cjs.map
//# sourceMappingURL=index.browser.cjs.map
;