Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow no persist for Custom Data Access #1541

Merged
merged 11 commits into from
Jan 15, 2025
5 changes: 5 additions & 0 deletions packages/data-access/src/combined-data-access.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { DataAccessTypes } from '@requestnetwork/types';
import { NoPersistDataWrite } from './no-persist-data-write';

export abstract class CombinedDataAccess implements DataAccessTypes.IDataAccess {
constructor(
Expand All @@ -16,6 +17,10 @@ export abstract class CombinedDataAccess implements DataAccessTypes.IDataAccess
await this.reader.close();
}

isPersisting(): boolean {
kevindavee marked this conversation as resolved.
Show resolved Hide resolved
return !(this.writer instanceof NoPersistDataWrite);
}

async getTransactionsByChannelId(
channelId: string,
updatedBetween?: DataAccessTypes.ITimestampBoundaries | undefined,
Expand Down
1 change: 1 addition & 0 deletions packages/data-access/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export { DataAccessRead } from './data-read';
export { PendingStore } from './pending-store';
export { DataAccessBaseOptions } from './types';
export { MockDataAccess } from './mock-data-access';
export { NoPersistDataWrite } from './no-persist-data-write';
36 changes: 36 additions & 0 deletions packages/data-access/src/no-persist-data-write.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { DataAccessTypes, StorageTypes } from '@requestnetwork/types';
import { EventEmitter } from 'events';

export class NoPersistDataWrite implements DataAccessTypes.IDataWrite {
async initialize(): Promise<void> {
return;
}

async close(): Promise<void> {
return;
}

async persistTransaction(
transaction: DataAccessTypes.ITransaction,
channelId: string,
topics?: string[] | undefined,
): Promise<DataAccessTypes.IReturnPersistTransaction> {
kevindavee marked this conversation as resolved.
Show resolved Hide resolved
const eventEmitter = new EventEmitter() as DataAccessTypes.PersistTransactionEmitter;

const result: DataAccessTypes.IReturnPersistTransaction = Object.assign(eventEmitter, {
meta: {
topics: topics || [],
transactionStorageLocation: '',
storageMeta: {
state: StorageTypes.ContentState.PENDING,
timestamp: Date.now() / 1000,
},
},
result: {},
});

// Emit confirmation instantly since data is not going to be persisted
result.emit('confirmed', result);
return result;
}
}
9 changes: 4 additions & 5 deletions packages/request-client.js/src/api/request-network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import * as Types from '../types';
import ContentDataExtension from './content-data-extension';
import Request from './request';
import localUtils from './utils';
import { NoPersistHttpDataAccess } from '../no-persist-http-data-access';

/**
* Entry point of the request-client.js library. Create requests, get requests, manipulate requests.
Expand Down Expand Up @@ -115,7 +114,7 @@ export default class RequestNetwork {

const transactionData = requestLogicCreateResult.meta?.transactionManagerMeta.transactionData;
const requestId = requestLogicCreateResult.result.requestId;
const isSkippingPersistence = this.dataAccess instanceof NoPersistHttpDataAccess;
const isSkippingPersistence = !this.dataAccess.isPersisting();
// create the request object
const request = new Request(requestId, this.requestLogic, this.currencyManager, {
contentDataExtension: this.contentData,
Expand Down Expand Up @@ -149,7 +148,7 @@ export default class RequestNetwork {
* @param request The Request object to persist. This must be a request that was created with skipPersistence enabled.
* @returns A promise that resolves to the result of the persist transaction operation.
* @throws {Error} If the request's `inMemoryInfo` is not provided, indicating it wasn't created with skipPersistence.
* @throws {Error} If the current data access instance does not support persistence (e.g., NoPersistHttpDataAccess).
* @throws {Error} If the current data access instance does not support persistence.
*/
public async persistRequest(
request: Request,
Expand All @@ -158,7 +157,7 @@ export default class RequestNetwork {
throw new Error('Cannot persist request without inMemoryInfo.');
}

if (this.dataAccess instanceof NoPersistHttpDataAccess) {
if (!this.dataAccess.isPersisting()) {
throw new Error(
'Cannot persist request when skipPersistence is enabled. To persist the request, create a new instance of RequestNetwork without skipPersistence being set to true.',
);
Expand Down Expand Up @@ -198,7 +197,7 @@ export default class RequestNetwork {

const transactionData = requestLogicCreateResult.meta?.transactionManagerMeta.transactionData;
const requestId = requestLogicCreateResult.result.requestId;
const isSkippingPersistence = this.dataAccess instanceof NoPersistHttpDataAccess;
const isSkippingPersistence = !this.dataAccess.isPersisting();

// create the request object
const request = new Request(requestId, this.requestLogic, this.currencyManager, {
Expand Down
111 changes: 111 additions & 0 deletions packages/request-client.js/src/http-data-access-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { ClientTypes } from '@requestnetwork/types';
import { retry } from '@requestnetwork/utils';
import httpConfigDefaults from './http-config-defaults';
import { stringify } from 'qs';

// eslint-disable-next-line @typescript-eslint/no-var-requires
const packageJson = require('../package.json');
export type NodeConnectionConfig = { baseURL: string; headers: Record<string, string> };

export class HttpDataAccessConfig {
/**
* Configuration that overrides http-config-defaults,
* @see httpConfigDefaults for the default configuration.
*/
public httpConfig: ClientTypes.IHttpDataAccessConfig;

/**
* Configuration that will be sent at each request.
*/
public nodeConnectionConfig: NodeConnectionConfig;

constructor(
{
httpConfig,
nodeConnectionConfig,
}: {
httpConfig?: Partial<ClientTypes.IHttpDataAccessConfig>;
nodeConnectionConfig?: Partial<NodeConnectionConfig>;
} = {
httpConfig: {},
nodeConnectionConfig: {},
},
) {
const requestClientVersion = packageJson.version;
this.httpConfig = {
...httpConfigDefaults,
...httpConfig,
};
this.nodeConnectionConfig = {
baseURL: 'http://localhost:3000',
headers: {
[this.httpConfig.requestClientVersionHeader]: requestClientVersion,
},
...nodeConnectionConfig,
};
}

/**
* Sends an HTTP GET request to the node and retries until it succeeds.
* Throws when the retry count reaches a maximum.
*
* @param url HTTP GET request url
* @param params HTTP GET request parameters
* @param retryConfig Maximum retry count, delay between retries, exponential backoff delay, and maximum exponential backoff delay
*/
public async fetchAndRetry<T = unknown>(
path: string,
params: Record<string, unknown>,
retryConfig: {
maxRetries?: number;
retryDelay?: number;
exponentialBackoffDelay?: number;
maxExponentialBackoffDelay?: number;
} = {},
): Promise<T> {
retryConfig.maxRetries = retryConfig.maxRetries ?? this.httpConfig.httpRequestMaxRetry;
retryConfig.retryDelay = retryConfig.retryDelay ?? this.httpConfig.httpRequestRetryDelay;
retryConfig.exponentialBackoffDelay =
retryConfig.exponentialBackoffDelay ?? this.httpConfig.httpRequestExponentialBackoffDelay;
retryConfig.maxExponentialBackoffDelay =
retryConfig.maxExponentialBackoffDelay ??
this.httpConfig.httpRequestMaxExponentialBackoffDelay;
return await retry(async () => await this.fetch<T>('GET', path, params), retryConfig)();
}

public async fetch<T = unknown>(
method: 'GET' | 'POST',
path: string,
params: Record<string, unknown> | undefined,
body?: Record<string, unknown>,
): Promise<T> {
const { baseURL, headers, ...options } = this.nodeConnectionConfig;
const url = new URL(path, baseURL);
if (params) {
// qs.parse doesn't handle well mixes of string and object params
for (const [key, value] of Object.entries(params)) {
if (typeof value === 'object') {
params[key] = JSON.stringify(value);
}
}
url.search = stringify(params);
}
kevindavee marked this conversation as resolved.
Show resolved Hide resolved
const r = await fetch(url, {
method,
body: body ? JSON.stringify(body) : undefined,
headers: {
'Content-Type': 'application/json',
...headers,
},
...options,
});
if (r.ok) {
return await r.json();
}

throw Object.assign(new Error(r.statusText), {
status: r.status,
statusText: r.statusText,
});
}
kevindavee marked this conversation as resolved.
Show resolved Hide resolved
}
Loading