@rabbitio/ui-kit 1.0.0-beta.37 → 1.0.0-beta.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +2042 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.modern.js +1193 -1
- package/dist/index.modern.js.map +1 -1
- package/dist/index.module.js +2029 -5
- package/dist/index.module.js.map +1 -1
- package/dist/index.umd.js +2044 -8
- package/dist/index.umd.js.map +1 -1
- package/package.json +4 -2
- package/src/common/adapters/axiosAdapter.js +35 -0
- package/src/common/errorUtils.js +15 -0
- package/src/common/utils/postponeExecution.js +11 -0
- package/src/components/utils/uiUtils.js +14 -0
- package/src/components/utils/urlQueryUtils.js +87 -0
- package/src/index.js +16 -0
- package/src/robustExteranlApiCallerService/cacheAndConcurrentRequestsResolver.js +559 -0
- package/src/robustExteranlApiCallerService/cachedRobustExternalApiCallerService.js +188 -0
- package/src/robustExteranlApiCallerService/cancelProcessing.js +29 -0
- package/src/robustExteranlApiCallerService/concurrentCalculationsMetadataHolder.js +103 -0
- package/src/robustExteranlApiCallerService/externalApiProvider.js +156 -0
- package/src/robustExteranlApiCallerService/externalServicesStatsCollector.js +82 -0
- package/src/robustExteranlApiCallerService/robustExternalAPICallerService.js +386 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import Hashes from "jshashes";
|
|
2
|
+
|
|
3
|
+
import { improveAndRethrow } from "../common/errorUtils.js";
|
|
4
|
+
import { safeStringify } from "../common/utils/safeStringify.js";
|
|
5
|
+
import { Logger } from "../common/utils/logging/logger.js";
|
|
6
|
+
|
|
7
|
+
import { RobustExternalAPICallerService } from "./robustExternalAPICallerService.js";
|
|
8
|
+
import { CacheAndConcurrentRequestsResolver } from "./cacheAndConcurrentRequestsResolver.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Extended edit of RobustExternalApiCallerService supporting cache and management of concurrent requests
|
|
12
|
+
* to the same resource.
|
|
13
|
+
* TODO: [tests, critical] Massively used logic
|
|
14
|
+
*/
|
|
15
|
+
export class CachedRobustExternalApiCallerService {
|
|
16
|
+
/**
|
|
17
|
+
* @param bio {string} unique service identifier
|
|
18
|
+
* @param cache {Cache} cache instance
|
|
19
|
+
* @param providersData {ExternalApiProvider[]} array of providers
|
|
20
|
+
* @param [cacheTtlMs=10000] {number} time to live for cache ms
|
|
21
|
+
* @param [maxCallAttemptsToWaitForAlreadyRunningRequest=50] {number} see details in CacheAndConcurrentRequestsResolver
|
|
22
|
+
* @param [timeoutBetweenAttemptsToCheckWhetherAlreadyRunningRequestFinished=3000] {number} see details in CacheAndConcurrentRequestsResolver
|
|
23
|
+
* @param [removeExpiredCacheAutomatically=true] {boolean} whether to remove cached data automatically when ttl exceeds
|
|
24
|
+
* @param [mergeCachedAndNewlyRetrievedData=null] {function} function accepting cached data, newly retrieved data and id field name for list items
|
|
25
|
+
* and merging them. use if needed
|
|
26
|
+
*/
|
|
27
|
+
constructor(
|
|
28
|
+
bio,
|
|
29
|
+
cache,
|
|
30
|
+
providersData,
|
|
31
|
+
cacheTtlMs = 10000,
|
|
32
|
+
removeExpiredCacheAutomatically = true,
|
|
33
|
+
mergeCachedAndNewlyRetrievedData = null,
|
|
34
|
+
maxCallAttemptsToWaitForAlreadyRunningRequest = 100,
|
|
35
|
+
timeoutBetweenAttemptsToCheckWhetherAlreadyRunningRequestFinished = 1000
|
|
36
|
+
) {
|
|
37
|
+
this._provider = new RobustExternalAPICallerService(
|
|
38
|
+
`cached_${bio}`,
|
|
39
|
+
providersData,
|
|
40
|
+
Logger.logError
|
|
41
|
+
);
|
|
42
|
+
this._cacheTtlMs = cacheTtlMs;
|
|
43
|
+
this._cahceAndRequestsResolver = new CacheAndConcurrentRequestsResolver(
|
|
44
|
+
bio,
|
|
45
|
+
cache,
|
|
46
|
+
cacheTtlMs,
|
|
47
|
+
removeExpiredCacheAutomatically,
|
|
48
|
+
maxCallAttemptsToWaitForAlreadyRunningRequest,
|
|
49
|
+
timeoutBetweenAttemptsToCheckWhetherAlreadyRunningRequestFinished
|
|
50
|
+
);
|
|
51
|
+
this._cahceIds = [];
|
|
52
|
+
this._mergeCachedAndNewlyRetrievedData =
|
|
53
|
+
mergeCachedAndNewlyRetrievedData;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Calls the external API or returns data from cache. Just waits if the same data already requested.
|
|
58
|
+
*
|
|
59
|
+
* @param parametersValues {array} array of values of the parameters for URL query string [and/or body]
|
|
60
|
+
* @param timeoutMS {number} http timeout to wait for response. If provider has its specific timeout value then it is used
|
|
61
|
+
* @param [cancelToken] {object|undefined} axios token to force-cancel requests from high-level code
|
|
62
|
+
* @param [attemptsCount] {number|undefined} number of attempts to be performed
|
|
63
|
+
* @param [customHashFunctionForParams] {function|undefined} function without params calculating the hash to be
|
|
64
|
+
* added to bio of the service to compose a unique parameters-specific cache id
|
|
65
|
+
* @param [doNotFailForNowData] {boolean|undefined} pass true if you do not want us to throw an error if we retrieved null data from all the providers
|
|
66
|
+
* @return {Promise<any>} resolving to retrieved data (or array of results if specific provider requires
|
|
67
|
+
* several requests. NOTE: we flatten nested arrays - results of each separate request done for the specific provider)
|
|
68
|
+
* @throws Error if requests to all providers are failed
|
|
69
|
+
*/
|
|
70
|
+
async callExternalAPICached(
|
|
71
|
+
parametersValues = [],
|
|
72
|
+
timeoutMS = 3500,
|
|
73
|
+
cancelToken = null,
|
|
74
|
+
attemptsCount = 1,
|
|
75
|
+
customHashFunctionForParams = null,
|
|
76
|
+
doNotFailForNowData = false
|
|
77
|
+
) {
|
|
78
|
+
const loggerSource = `${this._provider.bio}.callExternalAPICached`;
|
|
79
|
+
let cacheId;
|
|
80
|
+
let result;
|
|
81
|
+
try {
|
|
82
|
+
cacheId = this._calculateCacheId(
|
|
83
|
+
parametersValues,
|
|
84
|
+
customHashFunctionForParams
|
|
85
|
+
);
|
|
86
|
+
result =
|
|
87
|
+
await this._cahceAndRequestsResolver.getCachedOrWaitForCachedOrAcquireLock(
|
|
88
|
+
cacheId
|
|
89
|
+
);
|
|
90
|
+
if (!result?.canStartDataRetrieval) {
|
|
91
|
+
return result?.cachedData;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let data = await this._provider.callExternalAPI(
|
|
95
|
+
parametersValues,
|
|
96
|
+
timeoutMS,
|
|
97
|
+
cancelToken,
|
|
98
|
+
attemptsCount,
|
|
99
|
+
doNotFailForNowData
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const canPerformMerge =
|
|
103
|
+
typeof this._mergeCachedAndNewlyRetrievedData === "function";
|
|
104
|
+
if (canPerformMerge) {
|
|
105
|
+
const mostRecentCached =
|
|
106
|
+
this._cahceAndRequestsResolver.getCached(cacheId);
|
|
107
|
+
data = this._mergeCachedAndNewlyRetrievedData(
|
|
108
|
+
mostRecentCached,
|
|
109
|
+
data,
|
|
110
|
+
parametersValues
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
if (data != null) {
|
|
114
|
+
this._cahceAndRequestsResolver.saveCachedData(
|
|
115
|
+
cacheId,
|
|
116
|
+
result?.lockId,
|
|
117
|
+
data,
|
|
118
|
+
true,
|
|
119
|
+
canPerformMerge
|
|
120
|
+
);
|
|
121
|
+
this._cahceIds.indexOf(cacheId) < 0 &&
|
|
122
|
+
this._cahceIds.push(cacheId);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return data;
|
|
126
|
+
} catch (e) {
|
|
127
|
+
improveAndRethrow(e, loggerSource);
|
|
128
|
+
} finally {
|
|
129
|
+
this._cahceAndRequestsResolver.releaseLock(cacheId, result?.lockId);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
invalidateCaches() {
|
|
134
|
+
this._cahceIds.forEach((key) =>
|
|
135
|
+
this._cahceAndRequestsResolver.invalidate(key)
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
actualizeCachedData(
|
|
140
|
+
params,
|
|
141
|
+
synchronousCurrentCacheProcessor,
|
|
142
|
+
customHashFunctionForParams = null,
|
|
143
|
+
sessionDependent = true,
|
|
144
|
+
actualizedAtTimestamp
|
|
145
|
+
) {
|
|
146
|
+
const cacheId = this._calculateCacheId(
|
|
147
|
+
params,
|
|
148
|
+
customHashFunctionForParams
|
|
149
|
+
);
|
|
150
|
+
this._cahceAndRequestsResolver.actualizeCachedData(
|
|
151
|
+
cacheId,
|
|
152
|
+
synchronousCurrentCacheProcessor,
|
|
153
|
+
sessionDependent
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
markCacheAsExpiredButDontRemove(
|
|
158
|
+
parametersValues,
|
|
159
|
+
customHashFunctionForParams
|
|
160
|
+
) {
|
|
161
|
+
try {
|
|
162
|
+
this._cahceAndRequestsResolver.markAsExpiredButDontRemove(
|
|
163
|
+
this._calculateCacheId(
|
|
164
|
+
parametersValues,
|
|
165
|
+
customHashFunctionForParams
|
|
166
|
+
)
|
|
167
|
+
);
|
|
168
|
+
} catch (e) {
|
|
169
|
+
improveAndRethrow(e, "markCacheAsExpiredButDontRemove");
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
_calculateCacheId(parametersValues, customHashFunctionForParams = null) {
|
|
174
|
+
try {
|
|
175
|
+
const hash =
|
|
176
|
+
typeof customHashFunctionForParams === "function"
|
|
177
|
+
? customHashFunctionForParams(parametersValues)
|
|
178
|
+
: !parametersValues
|
|
179
|
+
? ""
|
|
180
|
+
: new Hashes.SHA512().hex(
|
|
181
|
+
safeStringify(parametersValues)
|
|
182
|
+
);
|
|
183
|
+
return `${this._provider.bio}-${hash}`;
|
|
184
|
+
} catch (e) {
|
|
185
|
+
improveAndRethrow(e, this._provider.bio + "_calculateCacheId");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Utils class needed to perform cancelling of axios request inside some process.
|
|
5
|
+
* Provides cancel state and axios token for HTTP requests
|
|
6
|
+
*/
|
|
7
|
+
export class CancelProcessing {
|
|
8
|
+
constructor() {
|
|
9
|
+
this._cancelToken = axios.CancelToken.source();
|
|
10
|
+
this._isCanceled = false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
cancel() {
|
|
14
|
+
this._isCanceled = true;
|
|
15
|
+
this._cancelToken.cancel();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
isCanceled() {
|
|
19
|
+
return this._isCanceled;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
getToken() {
|
|
23
|
+
return this._cancelToken.token;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static instance() {
|
|
27
|
+
return new CancelProcessing();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { v4 } from "uuid";
|
|
2
|
+
|
|
3
|
+
import { Logger } from "../common/utils/logging/logger.js";
|
|
4
|
+
|
|
5
|
+
// TODO: [refactoring, low] Consider removing this logic task_id=c360f2af75764bde8badd9ff1cc00d48
|
|
6
|
+
class ConcurrentCalculationsMetadataHolder {
|
|
7
|
+
constructor() {
|
|
8
|
+
this._calculations = {};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
startCalculation(domain, calculationsHistoryMaxLength = 100) {
|
|
12
|
+
if (!this._calculations[domain]) {
|
|
13
|
+
this._calculations[domain] = [];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (this._calculations[domain].length > calculationsHistoryMaxLength) {
|
|
17
|
+
this._calculations[domain] = this._calculations[domain].slice(
|
|
18
|
+
Math.round(calculationsHistoryMaxLength * 0.2)
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const newCalculation = {
|
|
23
|
+
startTimestamp: Date.now(),
|
|
24
|
+
endTimestamp: null,
|
|
25
|
+
uuid: v4(),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
this._calculations[domain].push(newCalculation);
|
|
29
|
+
|
|
30
|
+
return newCalculation.uuid;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
endCalculation(domain, uuid, isFailed = false) {
|
|
34
|
+
try {
|
|
35
|
+
const calculation = this._calculations[domain].find(
|
|
36
|
+
(calculation) => calculation?.uuid === uuid
|
|
37
|
+
);
|
|
38
|
+
if (calculation) {
|
|
39
|
+
calculation.endTimestamp = Date.now();
|
|
40
|
+
calculation.isFiled = isFailed;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const elapsed = (
|
|
44
|
+
((calculation?.endTimestamp ?? 0) -
|
|
45
|
+
(calculation?.startTimestamp ?? 0)) /
|
|
46
|
+
1000
|
|
47
|
+
).toFixed(1);
|
|
48
|
+
Logger.log(
|
|
49
|
+
"endCalculation",
|
|
50
|
+
`${elapsed} ms: ${domain}.${(calculation?.uuid ?? "").slice(
|
|
51
|
+
0,
|
|
52
|
+
7
|
|
53
|
+
)}`
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return calculation;
|
|
57
|
+
} catch (e) {
|
|
58
|
+
Logger.logError(e, "endCalculation");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
isCalculationLate(domain, uuid) {
|
|
63
|
+
const queue = this._calculations[domain];
|
|
64
|
+
const analysingCalculation = queue.find((item) => item.uuid === uuid);
|
|
65
|
+
return (
|
|
66
|
+
analysingCalculation &&
|
|
67
|
+
!!queue.find(
|
|
68
|
+
(calculation) =>
|
|
69
|
+
calculation.endTimestamp != null &&
|
|
70
|
+
calculation.startTimestamp >
|
|
71
|
+
analysingCalculation.startTimestamp
|
|
72
|
+
)
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
printCalculationsWaitingMoreThanSpecifiedSeconds(waitingLastsMs = 2000) {
|
|
77
|
+
const calculations = Object.keys(this._calculations)
|
|
78
|
+
.map((domain) =>
|
|
79
|
+
this._calculations[domain].map((c) => ({ ...c, domain }))
|
|
80
|
+
)
|
|
81
|
+
.flat()
|
|
82
|
+
.filter(
|
|
83
|
+
(c) =>
|
|
84
|
+
c.endTimestamp === null &&
|
|
85
|
+
Date.now() - c.startTimestamp > waitingLastsMs
|
|
86
|
+
);
|
|
87
|
+
Logger.log(
|
|
88
|
+
"printCalculationsWaitingMoreThanSpecifiedSeconds",
|
|
89
|
+
`Calculations waiting more than ${(waitingLastsMs / 1000).toFixed(
|
|
90
|
+
1
|
|
91
|
+
)}s:\n` +
|
|
92
|
+
calculations.map(
|
|
93
|
+
(c) =>
|
|
94
|
+
`${c.domain}.${c.uuid.slice(0, 8)}: ${
|
|
95
|
+
Date.now() - c.startTimestamp
|
|
96
|
+
}\n`
|
|
97
|
+
)
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const concurrentCalculationsMetadataHolder =
|
|
103
|
+
new ConcurrentCalculationsMetadataHolder();
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
export class ExternalApiProvider {
|
|
2
|
+
/**
|
|
3
|
+
* Creates an instance of external api provider.
|
|
4
|
+
*
|
|
5
|
+
* If you need sub-request then use 'subRequestIndex' to check current request index in functions below.
|
|
6
|
+
* Also use array for 'httpMethod'.
|
|
7
|
+
*
|
|
8
|
+
* If the endpoint of dedicated provider has pagination then you should customize the behavior using
|
|
9
|
+
* "changeQueryParametersForPageNumber", "checkWhetherResponseIsForLastPage".
|
|
10
|
+
*
|
|
11
|
+
* We perform RPS counting all over the App to avoid blocking our clients due to abuses of the providers.
|
|
12
|
+
*
|
|
13
|
+
* @param endpoint {string} URL to the provider's endpoint. Note: you can customize it using composeQueryString
|
|
14
|
+
* @param [httpMethod] {string|string[]} one of "get", "post", "put", "patch", "delete" or an array of these values
|
|
15
|
+
* for request having sub-requests
|
|
16
|
+
* @param [timeout] {number} number of milliseconds to wait for the response
|
|
17
|
+
* @param [apiGroup] {ApiGroup} singleton object containing parameters of API group. Helpful when you use the same
|
|
18
|
+
* api for different providers to avoid hardcoding RPS inside each provider what can cause mistakes
|
|
19
|
+
* @param [specificHeaders] {Object} contains specific keys (headers) and values (their content) if needed for this provider
|
|
20
|
+
* @param [maxPageLength] {number} optional number of items per page if the request supports pagination
|
|
21
|
+
*/
|
|
22
|
+
constructor(
|
|
23
|
+
endpoint,
|
|
24
|
+
httpMethod,
|
|
25
|
+
timeout,
|
|
26
|
+
apiGroup,
|
|
27
|
+
specificHeaders = {},
|
|
28
|
+
maxPageLength = Number.MAX_SAFE_INTEGER
|
|
29
|
+
) {
|
|
30
|
+
this.endpoint = endpoint;
|
|
31
|
+
this.httpMethod = httpMethod ?? "get";
|
|
32
|
+
// TODO: [refactoring, critical] We have two timeouts for robust data retrieval - here and inside the service method call, need to remain the only
|
|
33
|
+
this.timeout = timeout ?? 10000;
|
|
34
|
+
// TODO: [refactoring, critical] We need single place for all RPSes as we use them as hardcoded constants now inside different services
|
|
35
|
+
this.apiGroup = apiGroup;
|
|
36
|
+
this.maxPageLength = maxPageLength ?? Number.MAX_SAFE_INTEGER;
|
|
37
|
+
this.niceFactor = 1;
|
|
38
|
+
this.specificHeaders = specificHeaders ?? {};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
getRps() {
|
|
42
|
+
return this.apiGroup.rps ?? 2;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
isRpsExceeded() {
|
|
46
|
+
return this.apiGroup.isRpsExceeded();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
actualizeLastCalledTimestamp() {
|
|
50
|
+
this.apiGroup.actualizeLastCalledTimestamp();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
getApiGroupId() {
|
|
54
|
+
return this.apiGroup.id;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Some endpoint can require several sub requests. Example is one request to get confirmed transactions
|
|
59
|
+
* and another request for unconfirmed transactions. You should override this method to return true for such requests.
|
|
60
|
+
*
|
|
61
|
+
* @return {boolean} true if this provider requires several requests to retrieve the data
|
|
62
|
+
*/
|
|
63
|
+
doesRequireSubRequests() {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Some endpoint support pagination. Override this method if so and implement corresponding methods.
|
|
69
|
+
*
|
|
70
|
+
* @return {boolean} true if this provider requires several requests to retrieve the data
|
|
71
|
+
*/
|
|
72
|
+
doesSupportPagination() {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Composes a query string to be added to the endpoint of this provider.
|
|
78
|
+
*
|
|
79
|
+
* @param params {any[]} params array passed to the RobustExternalAPICallerService
|
|
80
|
+
* @param [subRequestIndex] {number} optional number of the sub-request the call is performed for
|
|
81
|
+
* @returns {string} query string to be concatenated with endpoint
|
|
82
|
+
*/
|
|
83
|
+
composeQueryString(params, subRequestIndex = 0) {
|
|
84
|
+
return "";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Composes a body to be added to the request
|
|
89
|
+
*
|
|
90
|
+
* @param params {any[]} params array passed to the RobustExternalAPICallerService
|
|
91
|
+
* @param [subRequestIndex] {number} optional number of the sub-request the call is performed for
|
|
92
|
+
* @returns {string}
|
|
93
|
+
*/
|
|
94
|
+
composeBody(params, subRequestIndex = 0) {
|
|
95
|
+
return "";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Extracts data from the response and returns it
|
|
100
|
+
*
|
|
101
|
+
* @param response {Object} HTTP response returned by provider
|
|
102
|
+
* @param [params] {any[]} params array passed to the RobustExternalAPICallerService
|
|
103
|
+
* @param [subRequestIndex] {number} optional number of the sub-request the call is performed for
|
|
104
|
+
* @param iterationsData {any[]} array of data retrieved from previous sub-requests
|
|
105
|
+
* @returns {any}
|
|
106
|
+
*/
|
|
107
|
+
getDataByResponse(response, params = [], subRequestIndex = 0, iterationsData = []) {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Function changing the query string according to page number and previous response
|
|
113
|
+
* Only for endpoints supporting pagination
|
|
114
|
+
*
|
|
115
|
+
* @param params {any[]} params array passed to the RobustExternalAPICallerService
|
|
116
|
+
* @param previousResponse {Object} HTTP response returned by provider for previous call (previous page)
|
|
117
|
+
* @param pageNumber {number} new page number. We count from 0. You need to manually increment with 1 if your
|
|
118
|
+
* provider counts pages starting with 1
|
|
119
|
+
* @param [subRequestIndex] {number} optional number of the sub-request the call is performed for
|
|
120
|
+
* @returns {any[]}
|
|
121
|
+
*/
|
|
122
|
+
changeQueryParametersForPageNumber(params, previousResponse, pageNumber, subRequestIndex = 0) {
|
|
123
|
+
return params;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Function checking whether the response is for the last page to stop requesting for a next page.
|
|
128
|
+
* Only for endpoints supporting pagination.
|
|
129
|
+
*
|
|
130
|
+
* @param previousResponse {Object} HTTP response returned by provider for previous call (previous page)
|
|
131
|
+
* @param currentResponse {Object} HTTP response returned by provider for current call (current page, next after the previous)
|
|
132
|
+
* @param currentPageNumber {number} current page number (for current response)
|
|
133
|
+
* @param [subRequestIndex] {number} optional number of the sub-request the call is performed for
|
|
134
|
+
* @returns {boolean}
|
|
135
|
+
*/
|
|
136
|
+
checkWhetherResponseIsForLastPage(previousResponse, currentResponse, currentPageNumber, subRequestIndex = 0) {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Resets the nice factor to default value
|
|
142
|
+
*/
|
|
143
|
+
resetNiceFactor() {
|
|
144
|
+
this.niceFactor = 1;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Internal method used for requests requiring sub-requests.
|
|
149
|
+
*
|
|
150
|
+
* @param iterationsData {any[]} iterations data retrieved from getDataByResponse called per sub-request.
|
|
151
|
+
* @return {any} by default flatten the passed iterations data array. Should be redefined if you need another logic.
|
|
152
|
+
*/
|
|
153
|
+
incorporateIterationsData(iterationsData) {
|
|
154
|
+
return iterationsData.flat();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { improveAndRethrow } from "../common/errorUtils.js";
|
|
2
|
+
import { Logger } from "../common/utils/logging/logger.js";
|
|
3
|
+
|
|
4
|
+
export class ExternalServicesStatsCollector {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.stats = new Map();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
externalServiceFailed(serviceUrl, message) {
|
|
10
|
+
try {
|
|
11
|
+
const processMessage = (stat, errorMessage) => {
|
|
12
|
+
const errors = stat.errors ?? {};
|
|
13
|
+
errorMessage = errorMessage ?? "";
|
|
14
|
+
if (errorMessage.match(/.*network.+error.*/i)) {
|
|
15
|
+
errors["networkError"] = (errors["networkError"] || 0) + 1;
|
|
16
|
+
} else if (errorMessage.match(/.*timeout.+exceeded.*/i)) {
|
|
17
|
+
errors["timeoutExceeded"] =
|
|
18
|
+
(errors["timeoutExceeded"] || 0) + 1;
|
|
19
|
+
} else if (errors["other"]) {
|
|
20
|
+
errors["other"].push(message);
|
|
21
|
+
} else {
|
|
22
|
+
errors["other"] = [message];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
stat.errors = errors;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
if (this.stats.has(serviceUrl)) {
|
|
29
|
+
const stat = this.stats.get(serviceUrl);
|
|
30
|
+
stat.callsCount += 1;
|
|
31
|
+
stat.failsCount += 1;
|
|
32
|
+
processMessage(stat, message);
|
|
33
|
+
} else {
|
|
34
|
+
this.stats.set(serviceUrl, { callsCount: 1, failsCount: 1 });
|
|
35
|
+
processMessage(this.stats.get(serviceUrl), message);
|
|
36
|
+
}
|
|
37
|
+
} catch (e) {
|
|
38
|
+
improveAndRethrow(e, "externalServiceFailed");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
externalServiceCalledWithoutError(serviceUrl) {
|
|
43
|
+
try {
|
|
44
|
+
if (this.stats.has(serviceUrl)) {
|
|
45
|
+
const stat = this.stats.get(serviceUrl);
|
|
46
|
+
stat.callsCount += 1;
|
|
47
|
+
} else {
|
|
48
|
+
this.stats.set(serviceUrl, { callsCount: 1, failsCount: 0 });
|
|
49
|
+
}
|
|
50
|
+
} catch (e) {
|
|
51
|
+
improveAndRethrow(e, "externalServiceCalledWithoutError");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Returns statistics about external services failures.
|
|
57
|
+
* Provides how many calls were performed and what the percent of failed calls. Also returns errors stat.
|
|
58
|
+
*
|
|
59
|
+
* @return {Array<object>} Array of objects of type { failsPerCent: number, calls: number }
|
|
60
|
+
* sorted by the highest fails percent desc
|
|
61
|
+
*/
|
|
62
|
+
getStats() {
|
|
63
|
+
try {
|
|
64
|
+
return Array.from(this.stats.keys())
|
|
65
|
+
.map((key) => {
|
|
66
|
+
const stat = this.stats.get(key);
|
|
67
|
+
return {
|
|
68
|
+
url: key,
|
|
69
|
+
failsPerCent: (
|
|
70
|
+
(stat.failsCount / stat.callsCount) *
|
|
71
|
+
100
|
|
72
|
+
).toFixed(2),
|
|
73
|
+
calls: stat.callsCount,
|
|
74
|
+
errors: stat.errors ?? [],
|
|
75
|
+
};
|
|
76
|
+
})
|
|
77
|
+
.sort((s1, s2) => s1.failsPerCent - s2.failsPerCent);
|
|
78
|
+
} catch (e) {
|
|
79
|
+
Logger.logError(e, "getStats");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|