UNPKG

@photonite/subgraph-sync

Version:

React hooks and components for tracking subgraph synchronization states with Apollo Client

289 lines (278 loc) 13.1 kB
import { gql, useQuery } from '@apollo/client'; import { useRef, useEffect, useState, useCallback } from 'react'; import { jsxs, jsx } from 'react/jsx-runtime'; /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ var __assign = function() { __assign = Object.assign || function __assign(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; function __rest(s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; } function __makeTemplateObject(cooked, raw) { if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } return cooked; } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; var GET_SUBGRAPH_STATUS = gql(templateObject_1 || (templateObject_1 = __makeTemplateObject(["\n query GetSubgraphStatus {\n _meta {\n block {\n number\n timestamp\n }\n hasIndexingErrors\n deployment\n }\n }\n"], ["\n query GetSubgraphStatus {\n _meta {\n block {\n number\n timestamp\n }\n hasIndexingErrors\n deployment\n }\n }\n"]))); var DEFAULT_CONFIG = { pollInterval: 2000, timeout: 120000, maxRetries: 3, onTimeout: function () { }, onError: function () { }, onSyncComplete: function () { }, }; /** * Hook for tracking subgraph synchronization progress to a target block or timestamp. * * @param target - The sync target (block number and/or timestamp) * @param config - Optional configuration for polling, timeout, retries, and callbacks * * @returns Sync state including indexing status, progress, current block, and control functions * * @example * ```tsx * const syncState = useSubgraphSync( * { blockNumber: 1000000 }, * { * pollInterval: 2000, * timeout: 60000, * onSyncComplete: () => console.log('Synced!'), * } * ); * * if (syncState.isIndexing) { * return <div>Progress: {syncState.progress}%</div>; * } * ``` */ var useSubgraphSync = function (target, config) { if (config === void 0) { config = {}; } var mergedConfig = useRef(__assign(__assign({}, DEFAULT_CONFIG), config)); // Update mergedConfig ref when config changes useEffect(function () { mergedConfig.current = __assign(__assign({}, DEFAULT_CONFIG), config); }, [config]); var _a = useState({ isIndexing: false, isSynced: false, isTimeout: false, isError: false, currentBlock: null, targetBlock: null, progress: 0, error: null, retries: 0, }), state = _a[0], setState = _a[1]; var timeoutRef = useRef(); var startTimeRef = useRef(Date.now()); var retryCountRef = useRef(0); var _b = useQuery(GET_SUBGRAPH_STATUS, { pollInterval: mergedConfig.current.pollInterval, notifyOnNetworkStatusChange: true, skip: !target.blockNumber && !target.timestamp, onError: function (error) { var _a, _b; if (retryCountRef.current < mergedConfig.current.maxRetries) { retryCountRef.current += 1; setState(function (prev) { return (__assign(__assign({}, prev), { retries: retryCountRef.current })); }); } else { setState(function (prev) { return (__assign(__assign({}, prev), { isError: true, error: error.message })); }); (_b = (_a = mergedConfig.current).onError) === null || _b === void 0 ? void 0 : _b.call(_a, error); } }, }), data = _b.data; _b.loading; _b.error; _b.networkStatus; var refetch = _b.refetch; var calculateProgress = useCallback(function (currentBlock) { if (!currentBlock || !target.blockNumber) return 0; if (currentBlock.number >= target.blockNumber) return 100; // Cap at 95% until fully synced to avoid showing 100% prematurely return Math.min(95, (currentBlock.number / target.blockNumber) * 100); }, [target.blockNumber]); var reset = useCallback(function () { startTimeRef.current = Date.now(); retryCountRef.current = 0; if (timeoutRef.current) { clearTimeout(timeoutRef.current); } setState({ isIndexing: !!(target.blockNumber || target.timestamp), isSynced: false, isTimeout: false, isError: false, currentBlock: null, targetBlock: target.blockNumber ? { number: target.blockNumber, timestamp: target.timestamp || 0, } : null, progress: 0, error: null, retries: 0, }); }, [target.blockNumber, target.timestamp]); useEffect(function () { startTimeRef.current = Date.now(); retryCountRef.current = 0; if (timeoutRef.current) { clearTimeout(timeoutRef.current); } setState({ isIndexing: !!(target.blockNumber || target.timestamp), isSynced: false, isTimeout: false, isError: false, currentBlock: null, targetBlock: target.blockNumber ? { number: target.blockNumber, timestamp: target.timestamp || 0, } : null, progress: 0, error: null, retries: 0, }); // Cleanup timeout on unmount return function () { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } }; }, [target.blockNumber, target.timestamp]); useEffect(function () { var _a, _b, _c, _d, _e; // Early exit if no target specified - already synced if (!target.blockNumber && !target.timestamp) { setState(function (prev) { return (__assign(__assign({}, prev), { isIndexing: false, isSynced: true })); }); return; } // Check if timeout has been exceeded if (Date.now() - startTimeRef.current > mergedConfig.current.timeout) { setState(function (prev) { return (__assign(__assign({}, prev), { isIndexing: false, isTimeout: true, error: "Subgraph sync timeout" })); }); (_b = (_a = mergedConfig.current).onTimeout) === null || _b === void 0 ? void 0 : _b.call(_a); return; } var currentBlock = (_c = data === null || data === void 0 ? void 0 : data._meta) === null || _c === void 0 ? void 0 : _c.block; if (!currentBlock) return; var progress = calculateProgress(currentBlock); // Check if we've reached both block and timestamp targets var hasReachedTargetBlock = target.blockNumber ? currentBlock.number >= target.blockNumber : true; var hasReachedTargetTimestamp = target.timestamp ? currentBlock.timestamp >= target.timestamp : true; var isSynced = hasReachedTargetBlock && hasReachedTargetTimestamp; setState(function (prev) { var _a, _b; return (__assign(__assign({}, prev), { isIndexing: !isSynced, isSynced: isSynced, currentBlock: currentBlock, progress: isSynced ? 100 : progress, error: ((_a = data === null || data === void 0 ? void 0 : data._meta) === null || _a === void 0 ? void 0 : _a.hasIndexingErrors) ? "Subgraph has indexing errors" : null, isError: !!((_b = data === null || data === void 0 ? void 0 : data._meta) === null || _b === void 0 ? void 0 : _b.hasIndexingErrors) })); }); if (isSynced) { (_e = (_d = mergedConfig.current).onSyncComplete) === null || _e === void 0 ? void 0 : _e.call(_d, data._meta); } }, [data, target.blockNumber, target.timestamp, calculateProgress]); return __assign(__assign({}, state), { refetch: refetch, reset: reset }); }; var templateObject_1; /** * Enhanced Apollo useQuery hook that includes subgraph synchronization tracking. * Combines GraphQL query execution with sync state monitoring. * * @param query - GraphQL query document * @param options - Combined Apollo query options and sync configuration * * @returns All Apollo query result properties plus sync state * * @example * ```tsx * const { data, loading, isIndexing, isSynced, progress } = useSubgraphQuery( * GET_USER_QUERY, * { * variables: { id: '0x123' }, * targetBlockNumber: 1000000, * syncConfig: { pollInterval: 2000 }, * } * ); * ``` */ function useSubgraphQuery(query, options) { if (options === void 0) { options = {}; } var syncConfig = options.syncConfig, targetBlockNumber = options.targetBlockNumber, targetTimestamp = options.targetTimestamp, queryOptions = __rest(options, ["syncConfig", "targetBlockNumber", "targetTimestamp"]); var queryResult = useQuery(query, queryOptions); var syncTarget = { blockNumber: targetBlockNumber, timestamp: targetTimestamp, }; var syncState = useSubgraphSync(syncTarget, syncConfig); return __assign(__assign(__assign({}, queryResult), syncState), { // Combined loading state isLoading: queryResult.loading || syncState.isIndexing, // Enhanced error state hasError: !!queryResult.error || syncState.isError }); } /** * Pre-built UI component for displaying subgraph synchronization state. * Shows sync status, progress bar, and block information. * * @example * ```tsx * const syncState = useSubgraphSync({ blockNumber: 1000000 }); * * return ( * <SyncIndicator * syncState={syncState} * showProgress={true} * showBlockInfo={true} * /> * ); * ``` */ var SyncIndicator = function (_a) { var syncState = _a.syncState, _b = _a.showProgress, showProgress = _b === void 0 ? true : _b, _c = _a.showBlockInfo, showBlockInfo = _c === void 0 ? true : _c, _d = _a.className, className = _d === void 0 ? "" : _d; var isIndexing = syncState.isIndexing, isSynced = syncState.isSynced, isError = syncState.isError, currentBlock = syncState.currentBlock, targetBlock = syncState.targetBlock, progress = syncState.progress, error = syncState.error; if (isError) { return (jsxs("div", __assign({ className: "subgraph-sync-error ".concat(className) }, { children: ["\u26A0\uFE0F Sync Error: ", error] }))); } if (!isIndexing && isSynced) { return null; } return (jsxs("div", __assign({ className: "subgraph-sync-indicator ".concat(className) }, { children: [jsx("div", __assign({ className: "sync-status" }, { children: isIndexing ? "🔄 Syncing..." : "✅ Synced" })), showProgress && isIndexing && (jsxs("div", __assign({ className: "sync-progress" }, { children: [jsx("progress", { value: progress, max: "100" }), jsxs("span", { children: [Math.round(progress), "%"] })] }))), showBlockInfo && currentBlock && targetBlock && (jsxs("div", __assign({ className: "block-info" }, { children: [jsxs("div", { children: ["Current: #", currentBlock.number] }), jsxs("div", { children: ["Target: #", targetBlock.number] }), jsxs("div", { children: ["Blocks behind: ", targetBlock.number - currentBlock.number] })] }))), isIndexing && (jsx("button", __assign({ className: "refresh-button", onClick: function () { return syncState.refetch(); }, type: "button" }, { children: "Refresh" })))] }))); }; export { SyncIndicator, useSubgraphQuery, useSubgraphSync }; //# sourceMappingURL=index.esm.js.map