@saberhq/sail
Version:
Account caching and batched loading for React-based Solana applications.
194 lines • 7.82 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.useAccountsInternal = exports.fetchKeysMaybe = exports.getCacheKeyOfPublicKey = void 0;
const tslib_1 = require("tslib");
const solana_contrib_1 = require("@saberhq/solana-contrib");
const use_solana_1 = require("@saberhq/use-solana");
const web3_js_1 = require("@solana/web3.js");
const dataloader_1 = tslib_1.__importDefault(require("dataloader"));
const react_1 = require("react");
const __1 = require("..");
const batchProvider_1 = require("./batchProvider");
const emitter_1 = require("./emitter");
const fetchers_1 = require("./fetchers");
const fetchKeysUsingLoader_1 = require("./fetchKeysUsingLoader");
/**
* Gets the cache key associated with the given pubkey.
* @param pubkey
* @returns
*/
const getCacheKeyOfPublicKey = (pubkey) => pubkey.toString();
exports.getCacheKeyOfPublicKey = getCacheKeyOfPublicKey;
const newState = () => ({
accountsCache: new Map(),
emitter: new emitter_1.AccountsEmitter(),
subscribedAccounts: new Map(),
});
/**
* Fetches keys, passing through null/undefined values.
* @param fetchKeys
* @param keys
* @returns
*/
const fetchKeysMaybe = async (fetchKeys, keys) => {
const keysWithIndex = keys.map((k, i) => [k, i]);
const nonEmptyKeysWithIndex = keysWithIndex.filter((key) => (0, solana_contrib_1.exists)(key[0]));
const nonEmptyKeys = nonEmptyKeysWithIndex.map((n) => n[0]);
const accountsData = await fetchKeys(nonEmptyKeys);
const result = keys.slice();
nonEmptyKeysWithIndex.forEach(([_, originalIndex], i) => {
result[originalIndex] = accountsData[i];
});
return result;
};
exports.fetchKeysMaybe = fetchKeysMaybe;
const useAccountsInternal = (args) => {
const { batchDurationMs = 500, refreshIntervalMs = 60000, onError, useWebsocketAccountUpdates = false, disableAutoRefresh: disableRefresh = false, } = args;
const { network, connection, providerMut } = (0, use_solana_1.useSolana)();
// Cache of accounts
const [{ accountsCache, emitter, subscribedAccounts }, setState] = (0, react_1.useState)(newState());
(0, react_1.useEffect)(() => {
setState((prevState) => {
// clear accounts cache and subscriptions whenever the network changes
prevState.accountsCache.clear();
prevState.subscribedAccounts.clear();
prevState.emitter.raiseCacheCleared();
return newState();
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [network]);
const accountLoader = (0, react_1.useMemo)(() => new dataloader_1.default(async (keys) => {
const result = await (0, fetchers_1.getMultipleAccounts)(connection, keys, onError, "confirmed");
const batch = new Set();
result.array.forEach((info, i) => {
const addr = keys[i];
if (addr && !(info instanceof Error)) {
const cacheKey = (0, exports.getCacheKeyOfPublicKey)(addr);
accountsCache.set(cacheKey, info);
batch.add(cacheKey);
}
});
emitter.raiseBatchCacheUpdated(batch);
return result.array;
}, {
// aggregate all requests over 500ms
batchScheduleFn: (callback) => setTimeout(callback, batchDurationMs),
cacheKeyFn: exports.getCacheKeyOfPublicKey,
}), [accountsCache, batchDurationMs, connection, emitter, onError]);
const { batchFetcher, batchProviderMut } = (0, react_1.useMemo)(() => {
return {
batchFetcher: new batchProvider_1.SailBatchFetcher(accountLoader),
batchProviderMut: providerMut
? new batchProvider_1.SailBatchProvider(providerMut, accountLoader)
: null,
};
}, [providerMut, accountLoader]);
const fetchKeys = (0, react_1.useCallback)(async (keys) => {
return await (0, fetchKeysUsingLoader_1.fetchKeysUsingLoader)(accountLoader, keys);
}, [accountLoader]);
const onBatchCache = emitter.onBatchCache;
const refetch = (0, react_1.useCallback)(async (key) => {
const result = await accountLoader.clear(key).load(key);
return result;
}, [accountLoader]);
const refetchMany = (0, react_1.useCallback)(async (keys) => {
keys.forEach((key) => {
accountLoader.clear(key);
});
return await accountLoader.loadMany(keys);
}, [accountLoader]);
const getCached = (0, react_1.useCallback)((key) => {
// null: account not found on blockchain
// undefined: cache miss (not yet fetched)
return accountsCache.get((0, exports.getCacheKeyOfPublicKey)(key));
}, [accountsCache]);
const subscribe = (0, react_1.useCallback)((key) => {
const keyStr = (0, exports.getCacheKeyOfPublicKey)(key);
const amount = subscribedAccounts.get(keyStr);
if (amount === undefined || amount === 0) {
subscribedAccounts.set(keyStr, 1);
}
else {
subscribedAccounts.set(keyStr, amount + 1);
}
let listener = null;
if (useWebsocketAccountUpdates) {
listener = connection.onAccountChange(key, (data) => {
const cacheKey = (0, exports.getCacheKeyOfPublicKey)(key);
accountsCache.set(cacheKey, data);
accountLoader.clear(key).prime(key, data);
emitter.raiseBatchCacheUpdated(new Set([cacheKey]));
});
}
return async () => {
const currentAmount = subscribedAccounts.get(keyStr);
if ((currentAmount !== null && currentAmount !== void 0 ? currentAmount : 0) > 1) {
subscribedAccounts.set(keyStr, (currentAmount !== null && currentAmount !== void 0 ? currentAmount : 0) - 1);
}
else {
subscribedAccounts.delete(keyStr);
}
if (listener) {
await connection.removeAccountChangeListener(listener);
}
};
}, [
accountLoader,
accountsCache,
connection,
emitter,
subscribedAccounts,
useWebsocketAccountUpdates,
]);
const refetchAllSubscriptions = (0, react_1.useCallback)(async () => {
const keysToFetch = [...subscribedAccounts.keys()].map((keyStr) => {
return new web3_js_1.PublicKey(keyStr);
});
await refetchMany(keysToFetch);
}, [refetchMany, subscribedAccounts]);
(0, react_1.useEffect)(() => {
// don't auto refetch if we're disabling the refresh
if (disableRefresh) {
return;
}
const interval = setInterval(() => {
void refetchAllSubscriptions().catch((e) => {
onError(new __1.SailRefetchSubscriptionsError(e));
});
}, refreshIntervalMs);
return () => clearInterval(interval);
}, [disableRefresh, onError, refetchAllSubscriptions, refreshIntervalMs]);
const getDatum = (0, react_1.useCallback)((k) => {
if (!k) {
return k;
}
const accountInfo = getCached(k);
if (accountInfo) {
return {
accountId: k,
accountInfo,
};
}
return accountInfo;
}, [getCached]);
return {
loader: accountLoader,
getCached,
getDatum,
refetch,
refetchMany,
refetchAllSubscriptions,
onBatchCache,
fetchKeys,
subscribe,
batchDurationMs,
refreshIntervalMs,
useWebsocketAccountUpdates,
disableAutoRefresh: disableRefresh,
onError,
batchFetcher,
batchProviderMut,
};
};
exports.useAccountsInternal = useAccountsInternal;
//# sourceMappingURL=useAccountsInternal.js.map
;