Skip to content

Commit

Permalink
fix(logger, errors, http): Updated to axios and axios-retry, added wi…
Browse files Browse the repository at this point in the history
…nston logger, more extensive custom error objects
  • Loading branch information
atticusofsparta committed Feb 13, 2024
1 parent 4949514 commit b944f4d
Show file tree
Hide file tree
Showing 14 changed files with 462 additions and 137 deletions.
4 changes: 2 additions & 2 deletions bundle.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import { polyfillNode } from 'esbuild-plugin-polyfill-node';
const bundle = () => {
console.log('Building web bundle esm.');
const result = build({
entryPoints: ['./src/index.ts'],
entryPoints: ['./src/web/index.ts'],
bundle: true,
platform: 'browser',
target: ['esnext'],
format: 'esm',
globalName: 'ar-io',
globalName: 'ar.io',
plugins: [
polyfillNode({
polyfills: {
Expand Down
1 change: 1 addition & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
module.exports = {
preset: 'ts-jest',
clearMocks: true,
moduleFileExtensions: ['ts', 'js', 'mjs'],
testMatch: ['**/src/**/*.test.ts', '**/tests/**/*.test.ts'],
Expand Down
15 changes: 9 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "@ardrive/node-sdk-client",
"name": "@ar-io/node-sdk-client",
"version": "0.0.1",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
Expand All @@ -12,8 +12,8 @@
],
"author": {
"name": "Permanent Data Solutions Inc",
"email": "info@ardrive.io",
"website": "https://ardrive.io"
"email": "info@ar.io",
"website": "https://ar.io"
},
"exports": {
".": {
Expand Down Expand Up @@ -61,6 +61,7 @@
"@commitlint/config-conventional": "^17.1.0",
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/exec": "^6.0.3",
"@semantic-release/git": "^10.0.1",
"@trivago/prettier-plugin-sort-imports": "^4.2.0",
"@types/jest": "^29.5.12",
Expand Down Expand Up @@ -92,10 +93,12 @@
"ts-node": "^10.9.1",
"typescript": "^5.1.6"
},
"repository": "https://github.com/ardriveapp/node-sdk-template.git",
"repository": "https://github.com/ar-io/ar-io-sdk.git",
"dependencies": {
"arweave": "^1.14.4",
"fetch-retry": "^5.0.6",
"warp-contracts": "^1.4.34"
"axios": "^1.6.7",
"axios-retry": "^4.0.0",
"warp-contracts": "^1.4.34",
"winston": "^3.11.0"
}
}
85 changes: 31 additions & 54 deletions src/common/ArIo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,83 +15,60 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import Arweave from 'arweave';
import fetchBuilder from 'fetch-retry';
import axios, { AxiosInstance } from 'axios';
import axiosRetry from 'axios-retry';

import { IContractStateProvider } from '../types.js';
import { ContractStateProvider } from '../types.js';
import { BaseError } from './error.js';
import { ArIoWinstonLogger } from './logger.js';

export class ArIoError extends Error {
export class ArIoError extends BaseError {
constructor(message: string) {
super(message);
this.name = 'ArIoError';
}
}

export class ArIo implements IContractStateProvider {
const RESPONSE_RETRY_CODES = new Set([429, 503]);

export class ArIo implements ContractStateProvider {
_arweave: Arweave;
_contractStateProviders: IContractStateProvider[];
http: typeof fetch;
log: (message: string) => void;
_contractStateProvider: ContractStateProvider;
http: AxiosInstance;
logger: ArIoWinstonLogger;

constructor({
arweave,
contractStateProviders,
logger,
contractStateProvider,
logger = new ArIoWinstonLogger({
level: 'debug',
logFormat: 'simple',
}),
}: {
arweave?: Arweave;
contractStateProviders: IContractStateProvider[];
logger?: (message: string) => void;
contractStateProvider: ContractStateProvider;
logger?: ArIoWinstonLogger;
}) {
this._arweave = arweave ?? Arweave.init({}); // use default arweave instance if not provided
this._contractStateProviders = contractStateProviders;
this.http = fetchBuilder(fetch, {
this._contractStateProvider = contractStateProvider;
this.logger = logger;
this.http = axiosRetry(axios, {
retries: 3,
retryDelay: 2000,
retryOn: [429, 500, 502, 503, 504],
});
this.log =
logger ??
((message: string) => {
console.debug(`[ArIo Client]: ${message}`);
});
retryDelay: axiosRetry.exponentialDelay,
retryCondition: (error) => {
this.logger.debug(`Retrying request: ${error.message}`);
return RESPONSE_RETRY_CODES.has(error.response!.status);
},
}) as any as AxiosInstance;
}

/**
* Fetches the state of a contract from the Arweave network.
* @param contractId - The contract ID to fetch the state for.
* @param strategy - The strategy to use when fetching the state - 'race', 'compare', or 'fallback'.
* - 'race' will call each provider and return the first result.
* - 'compare' will call each provider and return the result that has the highest blockheight evaluated.
* - 'fallback' will call first remote providers, then gql providers if remote fetch failed.
* @returns The state of the contract.
*
* @example
* const state = await ario.getContractState('contractId', 'fallback');
* Fetches the state of a contract.
* @param {string} contractId - The Arweave transaction id of the contract.
*/
async getContractState<ContractState>(
contractId: string,
strategy: 'race' | 'compare' | 'fallback' = 'race',
): Promise<ContractState> {
this.log(
`Fetching contract state for contract [${contractId}] using a ${strategy} strategy `,
);
switch (strategy) {
case 'race':
return Promise.race(
this._contractStateProviders.map((provider) =>
provider.getContractState<ContractState>(contractId),
),
);
case 'compare':
// TODO: implement compare strategy
throw new Error('Not implemented');
case 'fallback':
// TODO: implement fallback strategy
throw new Error('Not implemented');
default: {
const message = `Invalid strategy provided for contract [${contractId}]: ${strategy}`;
this.log(message);
throw new ArIoError(message);
}
}
return await this._contractStateProvider.getContractState(contractId);
}
}
102 changes: 40 additions & 62 deletions src/common/ContractStateProviders/ArNSRemoteCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,93 +14,71 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import fetchBuilder from 'fetch-retry';
import axios, { AxiosInstance } from 'axios';
import axiosRetry from 'axios-retry';

import { IContractStateProvider } from '../../types.js';
import { ContractStateProvider } from '../../types.js';
import { validateArweaveId } from '../../utils/index.js';
import { BadRequest, BaseError } from '../error.js';
import { ArIoWinstonLogger } from '../logger.js';

export class ArNSRemoteCacheError extends Error {
export class ArNSRemoteCacheError extends BaseError {
constructor(message: string) {
super(message);
this.name = 'ArNSRemoteCacheError';
}
}

/**
* ArNSRemoteCache class implements the IContractStateProvider interface.
* It provides methods to interact with a remote ArNS SmartWeave State Evaluator.
*
* @property {string} remoteCacheUrl - The URL of the remote cache. Defaults to 'api.arns.app'.
* @property {string} apiVersion - The API version to use for the remote cache. Defaults to 'v1'.
* @property {(message: string) => void} log - A logging function. If not provided, it defaults to a function that logs debug messages to the console.
* @property {typeof fetch} http - A fetch function with retry capabilities.
* @property {Object} httpOptions - Options to pass to the fetch function.
*
* @example
* const cache = new ArNSRemoteCache({}) || new ArNSRemoteCache({
* url: 'https://example.com/cache',
* logger: message => console.log(`Custom logger: ${message}`),
* version: 'v1',
* httpOptions: {
* retries: 3,
* retryDelay: 2000,
* retryOn: [404, 429, 503],
* },
* });
*/
export class ArNSRemoteCache implements IContractStateProvider {
remoteCacheUrl: string;
apiVersion: string;
log: (message: string) => void;
http: typeof fetch;
const RESPONSE_RETRY_CODES = new Set([429, 503]);
export class ArNSRemoteCache implements ContractStateProvider {
protected logger: ArIoWinstonLogger;
http: AxiosInstance;
constructor({
url = 'api.arns.app',
logger,
logger = new ArIoWinstonLogger({
level: 'debug',
logFormat: 'simple',
}),
version = 'v1',
httpOptions = {
retries: 3,
retryDelay: 2000,
retryOn: [404, 429, 503],
},
}: {
url?: string;
logger?: (message: string) => void;
logger?: ArIoWinstonLogger;
version?: string;
httpOptions?: Parameters<typeof fetchBuilder>[1];
}) {
this.remoteCacheUrl = url;
this.apiVersion = version;
this.log =
logger ??
((message: string) => {
console.debug(`[ArNS Remote Cache]: ${message}`);
});
this.http = fetchBuilder(fetch, httpOptions);
this.logger = logger;
const arnsServiceClient = axios.create({
baseURL: `${url}/${version}`,
});
this.http = axiosRetry(arnsServiceClient, {
retries: 3,
retryDelay: axiosRetry.exponentialDelay,
retryCondition: (error) => {
this.logger.debug(`Retrying request. Error: ${error}`);
return RESPONSE_RETRY_CODES.has(error.response!.status);
},
}) as any as AxiosInstance;
}

/**
* Fetches the state of a contract from the remote cache.
* @param {string} contractId - The Arweave transaction id of the contract.
*/
async getContractState<ContractState>(
contractId: string,
): Promise<ContractState> {
validateArweaveId(contractId);
const contractLogger = this.logger.logger.child({ contractId });
contractLogger.debug(`Fetching contract state`);

this.log(`Fetching contract state for [${contractId}]`);

const response = await this.http(
`${this.remoteCacheUrl}/${this.apiVersion}/contract/${contractId}`,
).catch((error) => {
const message = `Failed to fetch contract state for [${contractId}]: ${error}`;

this.log(message);
const response = await this.http<any, any>(`/contract/${contractId}`).catch(
(error) =>
contractLogger.debug(`Failed to fetch contract state: ${error}`),
);

throw new ArNSRemoteCacheError(message);
});
if (!response) {
throw new BadRequest(
`Failed to fetch contract state. ${response?.status} ${response?.statusText()}`,
);
}

this.log(
`Fetched contract state for [${contractId}]. State size: ${response.headers.get('content-length')} bytes.`,
contractLogger.debug(
`Fetched contract state. Size: ${response?.headers?.get('content-length')} bytes.`,
);

return response.json();
Expand Down
36 changes: 36 additions & 0 deletions src/common/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Copyright (C) 2022-2024 Permanent Data Solutions, Inc. All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
export class BaseError extends Error {
constructor(message: string) {
super(message);
this.name = this.constructor.name;
}
}

export class NotFound extends BaseError {
constructor(message: string) {
super(message);
this.name = 'NotFound';
}
}

export class BadRequest extends BaseError {
constructor(message: string) {
super(message);
this.name = 'BadRequest';
}
}

0 comments on commit b944f4d

Please sign in to comment.