light-curate-data-service
Version:
A TypeScript library for interacting with LightGeneralizedTCR contracts
272 lines (271 loc) • 8.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.fetchItemsBatch = fetchItemsBatch;
exports.fetchItems = fetchItems;
exports.fetchItemsById = fetchItemsById;
exports.fetchItemsByStatus = fetchItemsByStatus;
exports.clearItemsCache = clearItemsCache;
const sonner_1 = require("sonner");
// Constants
const BATCH_SIZE = 1000;
// Cache for GraphQL responses
const itemsCache = new Map();
// Default subgraph URLs by chain
const SUBGRAPH_URLS = {
1: "https://gateway.thegraph.com/api/500a3f85051f0de7a90d155df163a09b/subgraphs/id/A5oqWboEuDezwqpkaJjih4ckGhoHRoXZExqUbja2k1NQ", // Ethereum Mainnet
100: "https://gateway.thegraph.com/api/500a3f85051f0de7a90d155df163a09b/subgraphs/id/9hHo5MpjpC1JqfD3BsgFnojGurXRHTrHWcUcZPPCo6m8", // Gnosis Chain
};
// Common item fields to query
const ITEM_FIELDS = `
__typename
data
itemID
disputed
latestRequestSubmissionTime
metadata {
__typename
props {
__typename
description
isIdentifier
label
type
value
}
}
requests {
__typename
challenger
deposit
disputeID
disputed
requester
resolutionTime
resolved
requestType
rounds {
__typename
appealed
amountPaidChallenger
amountPaidRequester
appealPeriodEnd
appealPeriodStart
hasPaidChallenger
hasPaidRequester
ruling
}
submissionTime
evidenceGroup {
id
evidences {
id
URI
party
timestamp
}
}
}
status
`;
/**
* Creates a GraphQL query for fetching items with filters
*/
function createItemsQuery(registryAddress, lastTimestamp, filters) {
// Build where clause
const whereConditions = [`registry: "${registryAddress.toLowerCase()}"`];
// Add timestamp for pagination
if (lastTimestamp) {
whereConditions.push(`latestRequestSubmissionTime_lt: ${lastTimestamp}`);
}
// Add filter conditions
if (filters) {
Object.entries(filters).forEach(([key, values]) => {
if (values && values.length > 0) {
whereConditions.push(`${key}_in: [${values.map((v) => `"${v}"`).join(", ")}]`);
}
});
}
return `
query GetItems {
litems(
first: ${BATCH_SIZE}
orderBy: latestRequestSubmissionTime
orderDirection: desc
where: { ${whereConditions.join(", ")} }
) {
${ITEM_FIELDS}
}
}
`;
}
/**
* Generates a cache key based on parameters
*/
function generateCacheKey(chainId, registryAddress, filters) {
let key = `${chainId}-${registryAddress.toLowerCase()}`;
if (filters && Object.keys(filters).length > 0) {
const filterString = Object.entries(filters)
.sort()
.map(([key, values]) => `${key}:${(values || []).sort().join(",")}`)
.join(";");
key += `-${filterString}`;
}
return key;
}
/**
* Makes a GraphQL request to fetch items
*/
async function makeGraphQLRequest(url, query, signal) {
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query }),
signal,
});
if (!response.ok) {
throw new Error(`Network response was not ok: ${response.statusText}`);
}
const result = await response.json();
if (result.errors) {
console.error("GraphQL errors:", result.errors);
throw new Error(`GraphQL error: ${result.errors[0].message}`);
}
return result;
}
/**
* Fetches a single batch of items
*/
async function fetchItemsBatch(registryAddress, chainId = 1, lastTimestamp, customSubgraphUrl, signal, filters) {
try {
const subgraphUrl = customSubgraphUrl || SUBGRAPH_URLS[chainId];
if (!subgraphUrl) {
throw new Error(`No subgraph URL available for chain ID: ${chainId}`);
}
const query = createItemsQuery(registryAddress, lastTimestamp, filters);
const result = await makeGraphQLRequest(subgraphUrl, query, signal);
if (!result.data || !Array.isArray(result.data.litems)) {
throw new Error("Received invalid data format");
}
return {
items: result.data.litems,
hasMore: result.data.litems.length === BATCH_SIZE,
};
}
catch (error) {
if (error instanceof Error && error.name === "AbortError") {
console.log("Fetch aborted");
return { items: [], hasMore: false };
}
console.error("Error fetching batch:", error);
if (typeof sonner_1.toast !== "undefined") {
sonner_1.toast.error("Failed to load items. Please try again later.");
}
throw error;
}
}
/**
* Fetches all items with pagination and caching
*/
async function fetchItems(registryAddress, chainId = 1, options = {}) {
const { customSubgraphUrl, signal, onProgress, forceRefresh = false, maxBatches = Infinity, filters, } = options;
// Check cache
const cacheKey = generateCacheKey(chainId, registryAddress, filters);
if (!forceRefresh && itemsCache.has(cacheKey)) {
const cachedItems = itemsCache.get(cacheKey) || [];
return {
items: cachedItems,
stats: { batches: 0, total: cachedItems.length },
};
}
try {
let allItems = [];
let lastTimestamp = undefined;
let batchCount = 0;
// Fetch batches with pagination
while (batchCount < maxBatches) {
if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
throw new Error("Operation aborted");
}
const { items, hasMore } = await fetchItemsBatch(registryAddress, chainId, lastTimestamp, customSubgraphUrl, signal, filters);
batchCount++;
allItems = [...allItems, ...items];
// Report progress
if (onProgress) {
onProgress({
loaded: allItems.length,
total: hasMore ? undefined : allItems.length,
});
}
// Check if we need to fetch more
if (!hasMore || items.length === 0) {
break;
}
// Update pagination cursor
const lastItem = items[items.length - 1];
lastTimestamp = parseInt(lastItem.latestRequestSubmissionTime);
}
// Update cache
itemsCache.set(cacheKey, allItems);
return {
items: allItems,
stats: { batches: batchCount, total: allItems.length },
};
}
catch (error) {
if (error instanceof Error && error.name === "AbortError") {
console.log("Fetch aborted");
return { items: [], stats: { batches: 0, total: 0 } };
}
console.error("Error fetching items:", error);
if (typeof sonner_1.toast !== "undefined") {
sonner_1.toast.error("Failed to load all items. Please try again later.");
}
return { items: [], stats: { batches: 0, total: 0 } };
}
}
/**
* Fetches multiple items by IDs
* @param registryAddress The address of the registry contract
* @param itemID Array of item IDs
* @param chainId The chain ID (1 for Ethereum Mainnet, 100 for Gnosis Chain)
* @param options Additional options for fetching
* @returns Object containing items array and stats
*/
async function fetchItemsById(registryAddress, itemID, chainId = 1, options = {}) {
const result = await fetchItems(registryAddress, chainId, {
...options,
forceRefresh: true,
filters: { itemID },
});
return result;
}
/**
* Fetches items by status
*/
async function fetchItemsByStatus(registryAddress, status, chainId = 1, options = {}) {
const result = await fetchItems(registryAddress, chainId, {
...options,
forceRefresh: true,
filters: { status },
});
return result;
}
/**
* Clears the items cache
* @param registryAddress Optional registry address
* @param chainId Optional chain ID
* @param filters Optional filters to further narrow cache entries to clear
*/
function clearItemsCache(registryAddress, chainId, filters) {
// Clear entire cache if no parameters
if (!registryAddress && !chainId && !filters) {
itemsCache.clear();
return;
}
// Only clear specific cache entries when all required parameters are provided
if (registryAddress && chainId) {
const cacheKey = generateCacheKey(chainId, registryAddress, filters);
itemsCache.delete(cacheKey);
}
// If parameters are missing, do nothing
}