@photonite/subgraph-sync
Version:
React hooks and components for tracking subgraph synchronization states with Apollo Client
289 lines (278 loc) • 13.1 kB
JavaScript
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