@macalinao/grill
Version:
Modern Solana development kit for React applications with automatic account batching, caching, and transaction notifications
122 lines • 4.41 kB
JavaScript
import { getBase64Encoder } from "@solana/kit";
import { createContext, useContext } from "react";
import { createAccountQueryKey } from "../query-keys.js";
/**
* Parse a base64 encoded account notification into an EncodedAccount
*/
function parseAccountNotification(address, value) {
// The data is [base64String, "base64"]
const base64Data = value.data[0];
const data = getBase64Encoder().encode(base64Data);
return {
address,
data,
executable: value.executable,
lamports: value.lamports,
programAddress: value.owner,
space: value.space,
};
}
/**
* Creates a subscription manager instance.
*/
export function createSubscriptionManager(rpcSubscriptions, queryClient) {
const subscriptions = new Map();
const setupSubscription = async (address, decoder, abortSignal) => {
try {
const accountNotifications = await rpcSubscriptions
.accountNotifications(address, {
commitment: "confirmed",
encoding: "base64",
})
.subscribe({ abortSignal });
for await (const notification of accountNotifications) {
try {
const value = notification.value;
// Handle account closure (lamports = 0)
if (value.lamports === 0n) {
queryClient.setQueryData(createAccountQueryKey(address), null);
continue;
}
// Parse and decode, then update cache
const encodedAccount = parseAccountNotification(address, value);
const decoded = decoder(encodedAccount);
queryClient.setQueryData(createAccountQueryKey(address), decoded);
}
catch (decodeError) {
console.error(`[SubscriptionManager] Error decoding account ${address}:`, decodeError);
}
}
}
catch (error) {
// AbortError is expected when unsubscribing
if (error instanceof Error && error.name === "AbortError") {
return;
}
console.error(`[SubscriptionManager] Subscription error for ${address}:`, error);
}
};
const subscribe = (address, decoder) => {
const key = address;
const existing = subscriptions.get(key);
if (existing) {
// Increment reference count for existing subscription
existing.refCount++;
return () => {
existing.refCount--;
if (existing.refCount === 0) {
existing.abortController.abort();
subscriptions.delete(key);
}
};
}
// Create new subscription
const abortController = new AbortController();
const entry = {
abortController,
refCount: 1,
decoder: decoder,
};
subscriptions.set(key, entry);
// Start the WebSocket subscription
void setupSubscription(address, decoder, abortController.signal);
// Return unsubscribe function
return () => {
const currentEntry = subscriptions.get(key);
if (!currentEntry) {
return;
}
currentEntry.refCount--;
if (currentEntry.refCount === 0) {
currentEntry.abortController.abort();
subscriptions.delete(key);
}
};
};
const getSubscriptionCount = (address) => {
const entry = subscriptions.get(address);
return entry?.refCount ?? 0;
};
return {
subscribe,
getSubscriptionCount,
};
}
/**
* React context for subscription manager.
*/
export const SubscriptionContext = createContext(null);
/**
* Hook to access the subscription manager.
* Must be used within a provider that sets up SubscriptionContext.
*
* @throws Error if used outside of a provider with SubscriptionContext
*/
export function useSubscriptionManager() {
const context = useContext(SubscriptionContext);
if (!context) {
throw new Error("useSubscriptionManager must be used within a GrillProvider with subscriptions enabled");
}
return context;
}
//# sourceMappingURL=subscription-context.js.map