Skip to content

Commit

Permalink
feat: improve live/api mode logic
Browse files Browse the repository at this point in the history
  • Loading branch information
jackmellis committed Feb 12, 2024
1 parent d94f26a commit c6f5253
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 69 deletions.
6 changes: 2 additions & 4 deletions packages/api/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
import queryApiBase from './queryApi';
import nsync from './nsync';

export const queryApi = nsync(queryApiBase);
export { default as queryApi } from './queryApi';
export * as nsync from './nsync';
138 changes: 80 additions & 58 deletions packages/api/src/utils/nsync.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
import config from '@nftx/config';
import queryApi from './queryApi';

const isApiBehind = () => {
const { network, internal } = config;
const apiBlockNumber = internal.apiBlockNumber[network];
const requiredBlockNumber = internal.requiredBlockNumber[network];

return apiBlockNumber < requiredBlockNumber;
};
import { query } from '@nftx/utils';

// Wraps an async function.
// While the promise is unresolved, all subsequent calls will receive the same promise
const throttleFn = <F extends (...args: any[]) => Promise<any>>(fn: F): F => {
const throttleAsyncFn = <F extends (...args: any[]) => Promise<any>>(
fn: F
): F => {
let p: Promise<any> | undefined;
return ((...args: Parameters<F>) => {
if (p) {
Expand All @@ -31,65 +25,93 @@ const throttleFn = <F extends (...args: any[]) => Promise<any>>(fn: F): F => {
}) as any as F;
};

const fetchLastIndexedBlock = throttleFn(async () => {
const url = `/${config.network}/block`;
type Response = { block: number };
const response = await queryApi<Response>({
url,
query: { source: 'live' },
});
return response?.block;
});
/** Gets the required block number */
export const getRequiredBlockNumber = (network = config.network) => {
return config.internal.requiredBlockNumber[network] ?? 0;
};

const updateApiBlock = ({ source }: { source: 'live' | 'api' }) => {
// Switch to live mode
if (source !== 'live') {
config.internal.source = 'live';
}
// Wait a few seconds before polling the api again
setTimeout(async () => {
// Get the last indexed block on the api
config.internal.apiBlockNumber[config.network] =
await fetchLastIndexedBlock();
// Run this fn again until we're up to date...
checkApiBlock();
}, 5000);
/** Sets the required block number */
export const setRequiredBlockNumber = (
value: number,
network: number = config.network
) => {
config.internal.requiredBlockNumber[network] = value;
};

const resetRequiredBlock = () => {
// Reset the required block number
config.internal.requiredBlockNumber[config.network] = 0;
// Switch back to using the api as the SoT
config.internal.source = 'api';
/** Gets the current block number from the api */
export const getApiBlockNumber = (network = config.network) => {
return config.internal.apiBlockNumber[network] ?? 0;
};

const checkApiBlock = (): void => {
const requiredBlockNumber =
config.internal.requiredBlockNumber[config.network];
export const getBlockBuffer = () => {
return config.internal.blockBuffer;
};

// We don't need to worry about syncing if there's no required block number
// Checks whether the required block number is ahead of the api block number
const isApiBehind = ({
network,
requiredBlockNumber,
}: {
network: number;
requiredBlockNumber: number;
}) => {
if (!requiredBlockNumber) {
return;
// No required block number so we don't need to worry about the api
return false;
}
const apiBlockNumber = getApiBlockNumber(network);

const { source } = config.internal;
return apiBlockNumber < requiredBlockNumber;
};

// The API is behind the required block
if (isApiBehind()) {
// Switch to live mode and start polling the api to see what the last-indexed block is
updateApiBlock({ source });
} else if (source === 'live') {
// Api has caught up so we no longer need to be in live mode
resetRequiredBlock();
}
const updateLastIndexedBlock = async ({ network }: { network: number }) => {
// Get the last indexed block on the api
const response = await query<{ block: number }>({
url: `/${network}/block`,
query: { source: 'live', ebn: 'true' },
headers: {
'Content-Type': 'application/json',
Authorization: config.keys.NFTX_API,
},
});
const block = response?.block ?? 0;
// Save it (writes to local storage)
config.internal.apiBlockNumber[network] = block;
};

/** Wrap another function so that when it gets called, we first check the last-indexed block from the api */
const nsync = <F extends (...args: any[]) => any>(f: F): F => {
return ((...args: any[]) => {
checkApiBlock();
return f(...args);
}) as F;
const resetRequiredBlock = ({ network }: { network: number }) => {
// reset the required block number
setRequiredBlockNumber(0, network);
// Switch back to using the api as the SoT
config.internal.source = 'api';
};

export default nsync;
/** Checks if the api is behind the latest block.
* If it is behind, it will switch to live mode and continue checking the api until it has caught up
**/
export const syncApiBlock = throttleAsyncFn(
async (network: number = config.network) => {
// We throttle this method so even if 1k requests are made in quick succession,
// we'll only attempt to sync the api one time

// Keep looping while the api is behind the current block
while (
isApiBehind({
network,
requiredBlockNumber: getRequiredBlockNumber(network),
})
) {
if (config.internal.source !== 'live') {
// Switch to live mode
config.internal.source = 'live';
}
// Wait 5s before polling again
await new Promise<void>((res) => setTimeout(res, 5000));
// Fetch the latest block from the api
await updateLastIndexedBlock({ network });
}

// The api has caught up and we no longer need to be in live mode
resetRequiredBlock({ network });
}
);
4 changes: 4 additions & 0 deletions packages/api/src/utils/queryApi.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import config from '@nftx/config';
import { query as sendQuery } from '@nftx/utils';
import { syncApiBlock } from './nsync';

const queryApi = async <T>({
url,
Expand All @@ -10,6 +11,9 @@ const queryApi = async <T>({
query?: Record<string, any>;
method?: string;
}) => {
// Make sure the api is up to date (or switch to live mode if necessary)
syncApiBlock();

const uri = new URL(url, config.urls.NFTX_API_URL);
const query: Record<string, any> = {
...givenQuery,
Expand Down
5 changes: 3 additions & 2 deletions packages/config/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,15 @@ export interface Config {
keys: {
/** Your specific nftx.js API key, this must be provided in order to use @nftx/api methods */
NFTX_API: string;
ALCHEMY: Record<string, string>;
RESERVOIR: Record<string, string>;
};
/** Internal config settings managed by nftx.js */
internal: {
source: 'api' | 'live';
requiredBlockNumber: Record<string, number>;
apiBlockNumber: Record<string, number>;
/** The number of blocks to buffer when syncing data from the api */
blockBuffer: number;
};
}

Expand Down Expand Up @@ -130,7 +131,6 @@ const defaultConfig: Config = {

keys: {
NFTX_API: null as unknown as string,
ALCHEMY: {},
RESERVOIR: {},
},

Expand All @@ -146,6 +146,7 @@ const defaultConfig: Config = {
[Network.Goerli]: 0,
[Network.Sepolia]: 0,
},
blockBuffer: 10,
},
};

Expand Down
16 changes: 11 additions & 5 deletions packages/react/src/useTransaction/useWrapTransaction.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useNftx } from '../contexts/nftx';
import { t } from '../utils';
import { useAddEvent } from '../contexts/events';
import { config, Transaction } from 'nftx.js';
import {
Transaction,
TransactionExceptionError,
TransactionFailedError,
} from '@nftx/errors';
nsync,
} from 'nftx.js';

type Fn = (...args: any) => Promise<Transaction>;

Expand Down Expand Up @@ -113,9 +114,14 @@ export default function useWrapTransaction<F extends Fn>(
description,
});

config.internal.requiredBlockNumber[network] = Number(
receipt.blockNumber
);
// We ideally want data to be up to the block for this transaction
// So we store the block number and any subsequent api calls will
// be made with "live mode" enabled until the api has caught up
const txnBlock = Number(receipt.blockNumber);
// For a bit of leeway we add a buffer to the required block number
// so the api must be at least this far ahead before we switch back to "api mode"
const buffer = nsync.getBlockBuffer();
nsync.setRequiredBlockNumber(txnBlock + buffer);

return receipt;
},
Expand Down

0 comments on commit c6f5253

Please sign in to comment.