Skip to content

Commit dc752d9

Browse files
authored
feat: allow no persist for Custom Data Access (#1541)
1 parent a6ddcae commit dc752d9

17 files changed

+458
-246
lines changed

packages/data-access/src/combined-data-access.ts

+5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { DataAccessTypes } from '@requestnetwork/types';
2+
import { NoPersistDataWrite } from './no-persist-data-write';
23

34
export abstract class CombinedDataAccess implements DataAccessTypes.IDataAccess {
45
constructor(
@@ -16,6 +17,10 @@ export abstract class CombinedDataAccess implements DataAccessTypes.IDataAccess
1617
await this.reader.close();
1718
}
1819

20+
skipPersistence(): boolean {
21+
return this.writer instanceof NoPersistDataWrite;
22+
}
23+
1924
async getTransactionsByChannelId(
2025
channelId: string,
2126
updatedBetween?: DataAccessTypes.ITimestampBoundaries | undefined,

packages/data-access/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export { DataAccessRead } from './data-read';
55
export { PendingStore } from './pending-store';
66
export { DataAccessBaseOptions } from './types';
77
export { MockDataAccess } from './mock-data-access';
8+
export { NoPersistDataWrite } from './no-persist-data-write';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { DataAccessTypes, StorageTypes } from '@requestnetwork/types';
2+
import { EventEmitter } from 'events';
3+
4+
export class NoPersistDataWrite implements DataAccessTypes.IDataWrite {
5+
async initialize(): Promise<void> {
6+
return;
7+
}
8+
9+
async close(): Promise<void> {
10+
return;
11+
}
12+
13+
async persistTransaction(
14+
transaction: DataAccessTypes.ITransaction,
15+
channelId: string,
16+
topics?: string[] | undefined,
17+
): Promise<DataAccessTypes.IReturnPersistTransaction> {
18+
const eventEmitter = new EventEmitter() as DataAccessTypes.PersistTransactionEmitter;
19+
20+
const result: DataAccessTypes.IReturnPersistTransaction = Object.assign(eventEmitter, {
21+
meta: {
22+
topics: topics || [],
23+
transactionStorageLocation: '',
24+
storageMeta: {
25+
state: StorageTypes.ContentState.PENDING,
26+
timestamp: Date.now() / 1000,
27+
},
28+
},
29+
result: {},
30+
});
31+
32+
// Emit confirmation instantly since data is not going to be persisted
33+
result.emit('confirmed', result);
34+
return result;
35+
}
36+
}

packages/request-client.js/src/api/request-network.ts

+4-5
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import * as Types from '../types';
2323
import ContentDataExtension from './content-data-extension';
2424
import Request from './request';
2525
import localUtils from './utils';
26-
import { NoPersistHttpDataAccess } from '../no-persist-http-data-access';
2726

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

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

161-
if (this.dataAccess instanceof NoPersistHttpDataAccess) {
160+
if (this.dataAccess.skipPersistence()) {
162161
throw new Error(
163162
'Cannot persist request when skipPersistence is enabled. To persist the request, create a new instance of RequestNetwork without skipPersistence being set to true.',
164163
);
@@ -198,7 +197,7 @@ export default class RequestNetwork {
198197

199198
const transactionData = requestLogicCreateResult.meta?.transactionManagerMeta.transactionData;
200199
const requestId = requestLogicCreateResult.result.requestId;
201-
const isSkippingPersistence = this.dataAccess instanceof NoPersistHttpDataAccess;
200+
const isSkippingPersistence = this.dataAccess.skipPersistence();
202201

203202
// create the request object
204203
const request = new Request(requestId, this.requestLogic, this.currencyManager, {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { ClientTypes } from '@requestnetwork/types';
2+
import { retry } from '@requestnetwork/utils';
3+
import httpConfigDefaults from './http-config-defaults';
4+
import { stringify } from 'qs';
5+
6+
// eslint-disable-next-line @typescript-eslint/no-var-requires
7+
const packageJson = require('../package.json');
8+
export type NodeConnectionConfig = { baseURL: string; headers: Record<string, string> };
9+
10+
export class HttpDataAccessConfig {
11+
/**
12+
* Configuration that overrides http-config-defaults,
13+
* @see httpConfigDefaults for the default configuration.
14+
*/
15+
public httpConfig: ClientTypes.IHttpDataAccessConfig;
16+
17+
/**
18+
* Configuration that will be sent at each request.
19+
*/
20+
public nodeConnectionConfig: NodeConnectionConfig;
21+
22+
constructor(
23+
{
24+
httpConfig,
25+
nodeConnectionConfig,
26+
}: {
27+
httpConfig?: Partial<ClientTypes.IHttpDataAccessConfig>;
28+
nodeConnectionConfig?: Partial<NodeConnectionConfig>;
29+
} = {
30+
httpConfig: {},
31+
nodeConnectionConfig: {},
32+
},
33+
) {
34+
const requestClientVersion = packageJson.version;
35+
this.httpConfig = {
36+
...httpConfigDefaults,
37+
...httpConfig,
38+
};
39+
this.nodeConnectionConfig = {
40+
baseURL: 'http://localhost:3000',
41+
headers: {
42+
[this.httpConfig.requestClientVersionHeader]: requestClientVersion,
43+
},
44+
...nodeConnectionConfig,
45+
};
46+
}
47+
48+
/**
49+
* Sends an HTTP GET request to the node and retries until it succeeds.
50+
* Throws when the retry count reaches a maximum.
51+
*
52+
* @param url HTTP GET request url
53+
* @param params HTTP GET request parameters
54+
* @param retryConfig Maximum retry count, delay between retries, exponential backoff delay, and maximum exponential backoff delay
55+
*/
56+
public async fetchAndRetry<T = unknown>(
57+
path: string,
58+
params: Record<string, unknown>,
59+
retryConfig: {
60+
maxRetries?: number;
61+
retryDelay?: number;
62+
exponentialBackoffDelay?: number;
63+
maxExponentialBackoffDelay?: number;
64+
} = {},
65+
): Promise<T> {
66+
retryConfig.maxRetries = retryConfig.maxRetries ?? this.httpConfig.httpRequestMaxRetry;
67+
retryConfig.retryDelay = retryConfig.retryDelay ?? this.httpConfig.httpRequestRetryDelay;
68+
retryConfig.exponentialBackoffDelay =
69+
retryConfig.exponentialBackoffDelay ?? this.httpConfig.httpRequestExponentialBackoffDelay;
70+
retryConfig.maxExponentialBackoffDelay =
71+
retryConfig.maxExponentialBackoffDelay ??
72+
this.httpConfig.httpRequestMaxExponentialBackoffDelay;
73+
return await retry(async () => await this.fetch<T>('GET', path, params), retryConfig)();
74+
}
75+
76+
public async fetch<T = unknown>(
77+
method: 'GET' | 'POST',
78+
path: string,
79+
params: Record<string, unknown> | undefined,
80+
body?: Record<string, unknown>,
81+
): Promise<T> {
82+
const { baseURL, headers, ...options } = this.nodeConnectionConfig;
83+
const url = new URL(path, baseURL);
84+
if (params) {
85+
// qs.parse doesn't handle well mixes of string and object params
86+
for (const [key, value] of Object.entries(params)) {
87+
if (typeof value === 'object') {
88+
params[key] = JSON.stringify(value);
89+
}
90+
}
91+
url.search = stringify(params);
92+
}
93+
const r = await fetch(url, {
94+
method,
95+
body: body ? JSON.stringify(body) : undefined,
96+
headers: {
97+
'Content-Type': 'application/json',
98+
...headers,
99+
},
100+
...options,
101+
});
102+
if (r.ok) {
103+
return await r.json();
104+
}
105+
106+
throw Object.assign(new Error(r.statusText), {
107+
status: r.status,
108+
statusText: r.statusText,
109+
});
110+
}
111+
}

0 commit comments

Comments
 (0)