@chainlink/external-adapter-framework 0.0.10 → 0.0.14
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/adapter.d.ts +22 -3
- package/adapter.js +5 -2
- package/cache/factory.js +0 -2
- package/cache/index.d.ts +6 -2
- package/cache/index.js +13 -9
- package/cache/redis.js +5 -5
- package/chainlink-external-adapter-framework-0.0.6.tgz +0 -0
- package/config/index.d.ts +15 -1
- package/config/index.js +19 -4
- package/config/provider-limits.js +5 -1
- package/examples/bank-frick/accounts.d.ts +39 -0
- package/examples/bank-frick/accounts.js +191 -0
- package/examples/bank-frick/config/index.d.ts +4 -0
- package/examples/bank-frick/config/index.js +54 -0
- package/examples/bank-frick/index.d.ts +2 -0
- package/examples/bank-frick/index.js +14 -0
- package/examples/bank-frick/util.d.ts +4 -0
- package/examples/bank-frick/util.js +39 -0
- package/index.d.ts +1 -2
- package/index.js +42 -1
- package/metrics/index.js +0 -1
- package/metrics/util.d.ts +5 -1
- package/metrics/util.js +2 -2
- package/package/adapter.d.ts +88 -0
- package/package/adapter.js +112 -0
- package/package/background-executor.d.ts +11 -0
- package/package/background-executor.js +45 -0
- package/package/cache/factory.d.ts +6 -0
- package/package/cache/factory.js +57 -0
- package/package/cache/index.d.ts +90 -0
- package/package/cache/index.js +169 -0
- package/package/cache/local.d.ts +23 -0
- package/package/cache/local.js +83 -0
- package/package/cache/metrics.d.ts +27 -0
- package/package/cache/metrics.js +120 -0
- package/package/cache/redis.d.ts +16 -0
- package/package/cache/redis.js +100 -0
- package/package/config/index.d.ts +195 -0
- package/package/config/index.js +365 -0
- package/package/config/provider-limits.d.ts +31 -0
- package/package/config/provider-limits.js +76 -0
- package/package/examples/coingecko/batch-warming.d.ts +2 -0
- package/package/examples/coingecko/batch-warming.js +52 -0
- package/package/examples/coingecko/index.d.ts +2 -0
- package/package/examples/coingecko/index.js +10 -0
- package/package/examples/coingecko/rest.d.ts +2 -0
- package/package/examples/coingecko/rest.js +50 -0
- package/package/examples/ncfx/config/index.d.ts +12 -0
- package/package/examples/ncfx/config/index.js +15 -0
- package/package/examples/ncfx/index.d.ts +2 -0
- package/package/examples/ncfx/index.js +10 -0
- package/package/examples/ncfx/websocket.d.ts +36 -0
- package/package/examples/ncfx/websocket.js +72 -0
- package/package/index.d.ts +12 -0
- package/package/index.js +92 -0
- package/package/metrics/constants.d.ts +16 -0
- package/package/metrics/constants.js +25 -0
- package/package/metrics/index.d.ts +15 -0
- package/package/metrics/index.js +123 -0
- package/package/metrics/util.d.ts +3 -0
- package/package/metrics/util.js +9 -0
- package/package/package.json +72 -0
- package/package/rate-limiting/background/fixed-frequency.d.ts +10 -0
- package/package/rate-limiting/background/fixed-frequency.js +37 -0
- package/package/rate-limiting/index.d.ts +54 -0
- package/package/rate-limiting/index.js +63 -0
- package/package/rate-limiting/metrics.d.ts +3 -0
- package/package/rate-limiting/metrics.js +44 -0
- package/package/rate-limiting/request/simple-counting.d.ts +20 -0
- package/package/rate-limiting/request/simple-counting.js +62 -0
- package/package/test.d.ts +1 -0
- package/package/test.js +6 -0
- package/package/transports/batch-warming.d.ts +34 -0
- package/package/transports/batch-warming.js +101 -0
- package/package/transports/index.d.ts +87 -0
- package/package/transports/index.js +87 -0
- package/package/transports/metrics.d.ts +21 -0
- package/package/transports/metrics.js +105 -0
- package/package/transports/rest.d.ts +43 -0
- package/package/transports/rest.js +129 -0
- package/package/transports/util.d.ts +8 -0
- package/package/transports/util.js +85 -0
- package/package/transports/websocket.d.ts +80 -0
- package/package/transports/websocket.js +169 -0
- package/package/util/expiring-sorted-set.d.ts +21 -0
- package/package/util/expiring-sorted-set.js +47 -0
- package/package/util/index.d.ts +11 -0
- package/package/util/index.js +35 -0
- package/package/util/logger.d.ts +42 -0
- package/package/util/logger.js +62 -0
- package/package/util/request.d.ts +55 -0
- package/package/util/request.js +2 -0
- package/package/validation/error.d.ts +50 -0
- package/package/validation/error.js +79 -0
- package/package/validation/index.d.ts +5 -0
- package/package/validation/index.js +86 -0
- package/package/validation/input-params.d.ts +15 -0
- package/package/validation/input-params.js +30 -0
- package/package/validation/override-functions.d.ts +3 -0
- package/package/validation/override-functions.js +40 -0
- package/package/validation/preset-tokens.json +23 -0
- package/package/validation/validator.d.ts +47 -0
- package/package/validation/validator.js +303 -0
- package/package.json +5 -3
- package/rate-limiting/background/fixed-frequency.js +0 -2
- package/test.js +2 -2
- package/transports/batch-warming.d.ts +4 -3
- package/transports/batch-warming.js +4 -4
- package/transports/index.d.ts +4 -21
- package/transports/index.js +3 -3
- package/transports/metrics.d.ts +1 -1
- package/transports/metrics.js +2 -2
- package/transports/rest.d.ts +2 -1
- package/transports/rest.js +3 -1
- package/transports/websocket.d.ts +5 -4
- package/transports/websocket.js +5 -6
- package/util/index.d.ts +2 -1
- package/util/index.js +1 -1
- package/util/request.d.ts +3 -1
- package/util/subscription-set/expiring-sorted-set.d.ts +22 -0
- package/util/subscription-set/expiring-sorted-set.js +47 -0
- package/util/subscription-set/subscription-set.d.ts +18 -0
- package/util/subscription-set/subscription-set.js +19 -0
- package/util/test-payload-loader.d.ts +25 -0
- package/util/test-payload-loader.js +83 -0
- package/validation/error.d.ts +2 -2
- package/validation/error.js +1 -1
- package/validation/index.js +8 -3
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.Validator = void 0;
|
|
7
|
+
const error_1 = require("./error");
|
|
8
|
+
const input_params_1 = require("./input-params");
|
|
9
|
+
const util_1 = require("../util");
|
|
10
|
+
const preset_tokens_json_1 = __importDefault(require("./preset-tokens.json"));
|
|
11
|
+
// Don't want to get requester in just yet, only copying the static method 'errored'
|
|
12
|
+
const requesterErrored = (jobRunID = '1', error = undefined, statusCode = 500, feedID = undefined) => {
|
|
13
|
+
if (error instanceof error_1.AdapterError) {
|
|
14
|
+
error.jobRunID = jobRunID;
|
|
15
|
+
if (feedID) {
|
|
16
|
+
error.feedID = feedID;
|
|
17
|
+
}
|
|
18
|
+
return error.toJSONResponse();
|
|
19
|
+
}
|
|
20
|
+
if (error instanceof Error) {
|
|
21
|
+
return new error_1.AdapterError({
|
|
22
|
+
jobRunID,
|
|
23
|
+
statusCode,
|
|
24
|
+
message: error.message,
|
|
25
|
+
cause: error,
|
|
26
|
+
feedID,
|
|
27
|
+
}).toJSONResponse();
|
|
28
|
+
}
|
|
29
|
+
return new error_1.AdapterError({ jobRunID, statusCode, message: error, feedID }).toJSONResponse();
|
|
30
|
+
};
|
|
31
|
+
class Validator {
|
|
32
|
+
constructor(input = { id: '1', data: {} }, inputConfigs = {}, inputOptions = {}, validatorOptions = {}) {
|
|
33
|
+
// OverrideSymbol = (adapter: string, symbol?: string | string[]): string | string[] => {
|
|
34
|
+
// Const defaultSymbol = symbol || this.validated.data.base
|
|
35
|
+
// If (!defaultSymbol) this.throwInvalid(`Required parameter not supplied: base`)
|
|
36
|
+
// // TODO: Will never be reached, because the presetSymbols are used as default overrides
|
|
37
|
+
// If (!this.validated.overrides) return defaultSymbol
|
|
38
|
+
// If (!Array.isArray(defaultSymbol))
|
|
39
|
+
// Return (
|
|
40
|
+
// This.validated.overrides.get(adapter.toLowerCase())?.get(defaultSymbol.toLowerCase()) ||
|
|
41
|
+
// DefaultSymbol
|
|
42
|
+
// )
|
|
43
|
+
// Const multiple: string[] = []
|
|
44
|
+
// For (const sym of defaultSymbol) {
|
|
45
|
+
// Const overrided = this.validated.overrides.get(adapter.toLowerCase())?.get(sym.toLowerCase())
|
|
46
|
+
// If (!overrided) multiple.push(sym)
|
|
47
|
+
// Else multiple.push(overrided)
|
|
48
|
+
// }
|
|
49
|
+
// Return multiple
|
|
50
|
+
// }
|
|
51
|
+
// OverrideToken = (symbol: string, network = 'ethereum'): string | undefined => {
|
|
52
|
+
// Return this.validated.tokenOverrides?.get(network.toLowerCase())?.get(symbol.toLowerCase())
|
|
53
|
+
// }
|
|
54
|
+
// OverrideIncludes = (from: string, to: string): IncludePair | undefined => {
|
|
55
|
+
// // Search through `presetIncludes` to find matching override for adapter and to/from pairing.
|
|
56
|
+
// Const pairs = (
|
|
57
|
+
// This.validated.includes?.filter(
|
|
58
|
+
// (val: string | Includes) => typeof val !== 'string',
|
|
59
|
+
// ) as Includes[]
|
|
60
|
+
// ).filter(
|
|
61
|
+
// (pair) =>
|
|
62
|
+
// Pair.from.toLowerCase() === from.toLowerCase() &&
|
|
63
|
+
// Pair.to.toLowerCase() === to.toLowerCase(),
|
|
64
|
+
// )
|
|
65
|
+
// If (!pairs || !pairs[0] || !pairs[0].includes || !pairs[0].includes[0]) {
|
|
66
|
+
// Return
|
|
67
|
+
// }
|
|
68
|
+
// Return pairs[0].includes[0]
|
|
69
|
+
// }
|
|
70
|
+
// OverrideReverseLookup = (adapter: string, type: OverrideType, symbol: string): string => {
|
|
71
|
+
// Const overrides: Map<string, string> | undefined = this.validated?.[type]?.get(
|
|
72
|
+
// Adapter.toLowerCase(),
|
|
73
|
+
// )
|
|
74
|
+
// If (!overrides) return symbol
|
|
75
|
+
// Let originalSymbol: string | undefined
|
|
76
|
+
// Overrides.forEach((overridden, original) => {
|
|
77
|
+
// If (overridden.toLowerCase() === symbol.toLowerCase()) originalSymbol = original
|
|
78
|
+
// })
|
|
79
|
+
// Return originalSymbol || symbol
|
|
80
|
+
// }
|
|
81
|
+
this.formatOverride = (param) => {
|
|
82
|
+
const _throwInvalid = () => this.throwInvalid(`Parameter supplied with wrong format: "override"`);
|
|
83
|
+
if (!(0, util_1.isObject)(param)) {
|
|
84
|
+
_throwInvalid();
|
|
85
|
+
}
|
|
86
|
+
const _isValid = Object.values(param).every(util_1.isObject);
|
|
87
|
+
if (!_isValid) {
|
|
88
|
+
_throwInvalid();
|
|
89
|
+
}
|
|
90
|
+
const _keyToLowerCase = (entry) => {
|
|
91
|
+
return [entry[0].toLowerCase(), entry[1]];
|
|
92
|
+
};
|
|
93
|
+
return new Map(Object.entries(param)
|
|
94
|
+
.map(_keyToLowerCase)
|
|
95
|
+
.map(([key, value]) => [key, new Map(Object.entries(value).map(_keyToLowerCase))]));
|
|
96
|
+
};
|
|
97
|
+
this.formatIncludeOverrides = (param) => {
|
|
98
|
+
const _throwInvalid = () => this.throwInvalid(`Parameter supplied with wrong format: "includes"`);
|
|
99
|
+
if (!(0, util_1.isArray)(param)) {
|
|
100
|
+
_throwInvalid();
|
|
101
|
+
}
|
|
102
|
+
const _isValid = Object.values(param).every((val) => (0, util_1.isObject)(val) || typeof val === 'string');
|
|
103
|
+
if (!_isValid) {
|
|
104
|
+
_throwInvalid();
|
|
105
|
+
}
|
|
106
|
+
return param;
|
|
107
|
+
};
|
|
108
|
+
this.throwInvalid = (message) => {
|
|
109
|
+
throw new error_1.AdapterError({ jobRunID: this.validated.id, statusCode: 400, message });
|
|
110
|
+
};
|
|
111
|
+
this.input = { ...input };
|
|
112
|
+
if (!this.input.id) {
|
|
113
|
+
this.input.id = '1';
|
|
114
|
+
} // TODO Please remove these once "no any" strict typing is enabled
|
|
115
|
+
if (!this.input.data) {
|
|
116
|
+
this.input.data = {};
|
|
117
|
+
}
|
|
118
|
+
this.inputConfigs = { ...input_params_1.baseInputParameters, ...inputConfigs };
|
|
119
|
+
this.inputOptions = { ...inputOptions };
|
|
120
|
+
this.validatorOptions = {
|
|
121
|
+
shouldThrowError: true,
|
|
122
|
+
includes: [],
|
|
123
|
+
overrides: {},
|
|
124
|
+
...validatorOptions,
|
|
125
|
+
};
|
|
126
|
+
this.validated = { id: this.input.id, data: {} };
|
|
127
|
+
this.validateInput();
|
|
128
|
+
this.validateOverrides('overrides', this.validatorOptions.overrides);
|
|
129
|
+
this.validateOverrides('tokenOverrides', preset_tokens_json_1.default);
|
|
130
|
+
this.validateIncludeOverrides();
|
|
131
|
+
this.checkDuplicateInputParams(inputConfigs);
|
|
132
|
+
}
|
|
133
|
+
validateInput() {
|
|
134
|
+
try {
|
|
135
|
+
for (const key in this.inputConfigs) {
|
|
136
|
+
this.validateObjectParam(key, this.validatorOptions.shouldThrowError);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (e) {
|
|
140
|
+
this.parseError(e);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
validateOverrides(path, preset) {
|
|
144
|
+
try {
|
|
145
|
+
if (!this.input.data?.[path]) {
|
|
146
|
+
this.validated[path] = this.formatOverride(preset);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
this.validated[path] = this.formatOverride({ ...preset, ...this.input.data[path] });
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
this.parseError(e);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
checkDuplicateInputParams(inputConfig) {
|
|
156
|
+
let aliases = [];
|
|
157
|
+
for (const key in inputConfig) {
|
|
158
|
+
const param = inputConfig[key];
|
|
159
|
+
if (Array.isArray(param)) {
|
|
160
|
+
aliases = aliases.concat(param);
|
|
161
|
+
}
|
|
162
|
+
else if (typeof inputConfig === 'boolean') {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
aliases.push(key);
|
|
167
|
+
if (typeof param === 'object' && 'aliases' in param && Array.isArray(param.aliases)) {
|
|
168
|
+
aliases = aliases.concat(param.aliases);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (aliases.length !== new Set(aliases).size) {
|
|
173
|
+
this.throwInvalid('Duplicate Input Aliases');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
validateIncludeOverrides() {
|
|
177
|
+
try {
|
|
178
|
+
this.validated.includes = this.formatIncludeOverrides([
|
|
179
|
+
...(Array.isArray(this.input.data?.includes) ? this.input.data.includes : []),
|
|
180
|
+
...(this.validatorOptions.includes || []),
|
|
181
|
+
]);
|
|
182
|
+
}
|
|
183
|
+
catch (e) {
|
|
184
|
+
this.parseError(e);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
parseError(error) {
|
|
188
|
+
if (!(error instanceof Error)) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const message = 'Error validating input.';
|
|
192
|
+
if (error instanceof error_1.AdapterError) {
|
|
193
|
+
this.error = error;
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
this.error = new error_1.AdapterError({
|
|
197
|
+
jobRunID: this.validated.id,
|
|
198
|
+
statusCode: 400,
|
|
199
|
+
message,
|
|
200
|
+
cause: error,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
this.errored = requesterErrored(this.validated.id, this.error);
|
|
204
|
+
if (this.validatorOptions.shouldThrowError) {
|
|
205
|
+
throw this.error;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
validateObjectParam(key, shouldThrowError = true) {
|
|
209
|
+
const inputConfig = this.inputConfigs[key];
|
|
210
|
+
const usedKey = this.getUsedKey(key, inputConfig.aliases ?? []);
|
|
211
|
+
const param = usedKey
|
|
212
|
+
? this.input.data[usedKey] ?? inputConfig.default
|
|
213
|
+
: inputConfig.default;
|
|
214
|
+
if (shouldThrowError) {
|
|
215
|
+
const paramIsDefined = !(param === undefined || param === null || param === '');
|
|
216
|
+
if (inputConfig.required && !paramIsDefined) {
|
|
217
|
+
this.throwInvalid(`Required parameter ${key} must be non-null and non-empty`);
|
|
218
|
+
}
|
|
219
|
+
if (paramIsDefined) {
|
|
220
|
+
if (inputConfig.type) {
|
|
221
|
+
const primitiveTypes = ['boolean', 'number', 'bigint', 'string'];
|
|
222
|
+
if (![...primitiveTypes, 'array', 'object'].includes(inputConfig.type)) {
|
|
223
|
+
this.throwInvalid(`${key} parameter has unrecognized type ${inputConfig.type}`);
|
|
224
|
+
}
|
|
225
|
+
if (primitiveTypes.includes(inputConfig.type) && typeof param !== inputConfig.type) {
|
|
226
|
+
this.throwInvalid(`${key} parameter must be of type ${inputConfig.type}`);
|
|
227
|
+
}
|
|
228
|
+
if (inputConfig.type === 'array' && (!Array.isArray(param) || param.length === 0)) {
|
|
229
|
+
this.throwInvalid(`${key} parameter must be a non-empty array`);
|
|
230
|
+
}
|
|
231
|
+
if (inputConfig.type === 'object' &&
|
|
232
|
+
(!param ||
|
|
233
|
+
Array.isArray(param) ||
|
|
234
|
+
typeof param !== inputConfig.type ||
|
|
235
|
+
Object.keys(param).length === 0)) {
|
|
236
|
+
this.throwInvalid(`${key} parameter must be an object with at least one property`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// If (inputConfig.options) {
|
|
240
|
+
// Const tolcase = (o: any) => (typeof o === 'string' ? o.toLowerCase() : o)
|
|
241
|
+
// Const formattedOptions = inputConfig.options.map(tolcase)
|
|
242
|
+
// Const formattedParam = tolcase(param)
|
|
243
|
+
// If (!formattedOptions.includes(formattedParam))
|
|
244
|
+
// This.throwInvalid(
|
|
245
|
+
// `${key} parameter '${formattedParam}' is not in the set of available options: ${formattedOptions.join(
|
|
246
|
+
// ',',
|
|
247
|
+
// )}`,
|
|
248
|
+
// )
|
|
249
|
+
// }
|
|
250
|
+
// For (const dependency of inputConfig.dependsOn ?? []) {
|
|
251
|
+
// Const usedDependencyKey = this.getUsedKey(
|
|
252
|
+
// Dependency,
|
|
253
|
+
// (this.inputConfigs[dependency] as InputParameter).aliases ?? [],
|
|
254
|
+
// )
|
|
255
|
+
// If (!usedDependencyKey) this.throwInvalid(`${key} dependency ${dependency} not supplied`)
|
|
256
|
+
// }
|
|
257
|
+
// For (const exclusive of inputConfig.exclusive ?? []) {
|
|
258
|
+
// Const usedExclusiveKey = this.getUsedKey(
|
|
259
|
+
// Exclusive,
|
|
260
|
+
// (this.inputConfigs[exclusive] as InputParameter).aliases ?? [],
|
|
261
|
+
// )
|
|
262
|
+
// If (usedExclusiveKey)
|
|
263
|
+
// This.throwInvalid(`${key} cannot be supplied concurrently with ${exclusive}`)
|
|
264
|
+
// }
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
this.validated.data[key] = param;
|
|
268
|
+
}
|
|
269
|
+
validateOptionalParam(param, key, options) {
|
|
270
|
+
if (param && options) {
|
|
271
|
+
if (!Array.isArray(options)) {
|
|
272
|
+
this.throwInvalid(`Parameter options for ${key} must be of an Array type`);
|
|
273
|
+
}
|
|
274
|
+
if (!options.includes(param)) {
|
|
275
|
+
this.throwInvalid(`${param} is not a supported ${key} option. Must be one of ${options}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
this.validated.data[key] = param;
|
|
279
|
+
}
|
|
280
|
+
validateRequiredParam(param, key, options) {
|
|
281
|
+
if (typeof param === 'undefined' || param === '') {
|
|
282
|
+
this.throwInvalid(`Required parameter not supplied: ${key}`);
|
|
283
|
+
}
|
|
284
|
+
if (options) {
|
|
285
|
+
if (!Array.isArray(options)) {
|
|
286
|
+
this.throwInvalid(`Parameter options for ${key} must be of an Array type`);
|
|
287
|
+
}
|
|
288
|
+
if (!options.includes(param)) {
|
|
289
|
+
this.throwInvalid(`${param} is not a supported ${key} option. Must be one of ${options.join(' || ')}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
this.validated.data[key] = param;
|
|
293
|
+
}
|
|
294
|
+
getUsedKey(key, keyArray) {
|
|
295
|
+
const comparisonArray = [...keyArray];
|
|
296
|
+
if (!comparisonArray.includes(key)) {
|
|
297
|
+
comparisonArray.push(key);
|
|
298
|
+
}
|
|
299
|
+
const inputParamKeys = Object.keys(this.input.data);
|
|
300
|
+
return inputParamKeys.find((k) => comparisonArray.includes(k));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
exports.Validator = Validator;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chainlink/external-adapter-framework",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.14",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"dependencies": {
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"test": "LOG_LEVEL=error EA_PORT=0 c8 ava",
|
|
20
20
|
"test-debug": "LOG_LEVEL=trace DEBUG=true EA_PORT=0 c8 ava --verbose",
|
|
21
21
|
"build": "tsc",
|
|
22
|
-
"lint": "eslint ./src"
|
|
22
|
+
"lint": "eslint ./src && prettier --check ./src/**/*.ts",
|
|
23
|
+
"dev": "NODE_ENV=develop tsnd --respawn --transpile-only --project tsconfig.json './src/test.ts'"
|
|
23
24
|
},
|
|
24
25
|
"devDependencies": {
|
|
25
26
|
"@sinonjs/fake-timers": "^9.1.2",
|
|
@@ -37,7 +38,8 @@
|
|
|
37
38
|
"nock": "^13.2.4",
|
|
38
39
|
"pino-pretty": "^7.6.0",
|
|
39
40
|
"prettier": "^2.6.1",
|
|
40
|
-
"typedoc": "^0.22.15"
|
|
41
|
+
"typedoc": "^0.22.15",
|
|
42
|
+
"ts-node-dev": "2.0.0"
|
|
41
43
|
},
|
|
42
44
|
"prettier": {
|
|
43
45
|
"semi": false,
|
|
@@ -19,11 +19,9 @@ class FixedFrequencyRateLimiter {
|
|
|
19
19
|
}
|
|
20
20
|
logger.debug('Using fixed frequency batch rate limiting');
|
|
21
21
|
for (const endpoint of endpoints) {
|
|
22
|
-
// TODO: See if we can remove this runtime check
|
|
23
22
|
if (endpoint.rateLimiting?.allocationPercentage == null) {
|
|
24
23
|
throw new Error(`Allocation percentage for endpoint "${endpoint.name}" is null`);
|
|
25
24
|
}
|
|
26
|
-
// TODO: Implement different strategy where this is not fixed, but rather divided based on whether all warmers are active
|
|
27
25
|
this.msBetweenRequestsMap[endpoint.name] =
|
|
28
26
|
(sharedMsBetweenRequests / endpoint.rateLimiting?.allocationPercentage) * 100;
|
|
29
27
|
logger.debug(`Endpoint [${endpoint.name}]: ${this.msBetweenRequestsMap[endpoint.name] / 1000}s between requests`);
|
package/test.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const _1 = require(".");
|
|
4
|
-
const
|
|
4
|
+
const bank_frick_1 = require("./examples/bank-frick");
|
|
5
5
|
// Start sample adapter
|
|
6
|
-
(0, _1.expose)(
|
|
6
|
+
(0, _1.expose)(bank_frick_1.adapter);
|
|
@@ -2,9 +2,10 @@ import { AxiosRequestConfig, AxiosResponse } from 'axios';
|
|
|
2
2
|
import { Cache } from '../cache';
|
|
3
3
|
import { AdapterConfig, SettingsMap } from '../config';
|
|
4
4
|
import { BackgroundExecuteRateLimiter } from '../rate-limiting';
|
|
5
|
-
import {
|
|
5
|
+
import { SubscriptionSet } from '../util';
|
|
6
6
|
import { AdapterRequest, ProviderResult } from '../util/request';
|
|
7
|
-
import {
|
|
7
|
+
import { Transport } from './';
|
|
8
|
+
import { AdapterContext, AdapterDependencies } from '../adapter';
|
|
8
9
|
/**
|
|
9
10
|
* Transport implementation that takes incoming batches requests and keeps a warm cache of values.
|
|
10
11
|
* Within the setup function, adapter params are added to an set that also keeps track and expires values.
|
|
@@ -21,7 +22,7 @@ export declare class BatchWarmingTransport<AdapterParams, ProviderRequestBody, P
|
|
|
21
22
|
private config;
|
|
22
23
|
cache: Cache;
|
|
23
24
|
rateLimiter: BackgroundExecuteRateLimiter;
|
|
24
|
-
|
|
25
|
+
subscriptionSet: SubscriptionSet<AdapterParams>;
|
|
25
26
|
WARMER_ACTIVE: boolean;
|
|
26
27
|
constructor(config: {
|
|
27
28
|
prepareRequest: (params: AdapterParams[], context: AdapterContext<CustomSettings>) => AxiosRequestConfig<ProviderRequestBody>;
|
|
@@ -46,7 +46,6 @@ const logger = (0, util_1.makeLogger)('BatchWarmingTransport');
|
|
|
46
46
|
class BatchWarmingTransport {
|
|
47
47
|
constructor(config) {
|
|
48
48
|
this.config = config;
|
|
49
|
-
this.expiringSortedSet = new util_1.ExpiringSortedSet(); // TODO: Move to dependencies, inject
|
|
50
49
|
// Flag used to track whether the warmer has moved from having no entries to having some and vice versa
|
|
51
50
|
// Used for recording the cache warmer active metrics accurately
|
|
52
51
|
this.WARMER_ACTIVE = false;
|
|
@@ -54,17 +53,18 @@ class BatchWarmingTransport {
|
|
|
54
53
|
async initialize(dependencies) {
|
|
55
54
|
this.cache = dependencies.cache;
|
|
56
55
|
this.rateLimiter = dependencies.backgroundExecuteRateLimiter;
|
|
56
|
+
this.subscriptionSet = dependencies.subscriptionSetFactory.buildSet();
|
|
57
57
|
}
|
|
58
58
|
async hasBeenSetUp(req) {
|
|
59
|
-
return !!this.
|
|
59
|
+
return !!(await this.subscriptionSet.get(req.requestContext.cacheKey));
|
|
60
60
|
}
|
|
61
61
|
async setup(req, config) {
|
|
62
62
|
logger.debug(`Adding entry to batch warming set: [${req.requestContext.cacheKey}] = ${req.requestContext.data}`);
|
|
63
|
-
this.
|
|
63
|
+
await this.subscriptionSet.add(req.requestContext.cacheKey, req.requestContext.data, config.WARMUP_SUBSCRIPTION_TTL);
|
|
64
64
|
}
|
|
65
65
|
async backgroundExecute(context) {
|
|
66
66
|
logger.debug('Starting background execute');
|
|
67
|
-
const entries = this.
|
|
67
|
+
const entries = await this.subscriptionSet.getAll();
|
|
68
68
|
if (!entries.length) {
|
|
69
69
|
logger.debug('No entries in batch warming set, skipping');
|
|
70
70
|
if (this.WARMER_ACTIVE) {
|
package/transports/index.d.ts
CHANGED
|
@@ -1,28 +1,11 @@
|
|
|
1
1
|
import { FastifyReply } from 'fastify';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { AdapterConfig,
|
|
5
|
-
import { BackgroundExecuteRateLimiter, RequestRateLimiter } from '../rate-limiting';
|
|
2
|
+
import { AdapterContext, AdapterDependencies, InitializedAdapter } from '../adapter';
|
|
3
|
+
import { CacheEntry } from '../cache';
|
|
4
|
+
import { AdapterConfig, SettingsMap } from '../config';
|
|
6
5
|
import { AdapterRequest, AdapterResponse, ProviderResult } from '../util/request';
|
|
7
6
|
export * from './batch-warming';
|
|
8
7
|
export * from './rest';
|
|
9
8
|
export * from './websocket';
|
|
10
|
-
/**
|
|
11
|
-
* Dependencies that will be injected into the Adapter on startup
|
|
12
|
-
*/
|
|
13
|
-
export interface AdapterDependencies {
|
|
14
|
-
cache: Cache;
|
|
15
|
-
requestRateLimiter: RequestRateLimiter;
|
|
16
|
-
backgroundExecuteRateLimiter: BackgroundExecuteRateLimiter;
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Context that will be used on background executions of a Transport.
|
|
20
|
-
* For example, the endpointName used to log statements or generate Cache keys.
|
|
21
|
-
*/
|
|
22
|
-
export interface AdapterContext<CustomSettings extends CustomSettingsType<CustomSettings> = SettingsMap> {
|
|
23
|
-
adapterEndpoint: AdapterEndpoint;
|
|
24
|
-
adapterConfig: AdapterConfig<CustomSettings>;
|
|
25
|
-
}
|
|
26
9
|
/**
|
|
27
10
|
* Generic interface for a Transport.
|
|
28
11
|
* A Transport defines the way in which an AdapterEndpoint will process incoming requests to
|
|
@@ -76,7 +59,7 @@ export interface Transport<Params, Result, CustomSettings extends SettingsMap> {
|
|
|
76
59
|
* @param context - context for the Adapter
|
|
77
60
|
* @returns a list of CacheEntries of AdapterResponses
|
|
78
61
|
*/
|
|
79
|
-
export declare const buildCacheEntriesFromResults: <Params,
|
|
62
|
+
export declare const buildCacheEntriesFromResults: <Params, CustomSettings extends SettingsMap>(results: ProviderResult<Params>[], context: AdapterContext<CustomSettings>) => CacheEntry<AdapterResponse<null>>[];
|
|
80
63
|
/**
|
|
81
64
|
* Takes an Adapter, its configuration, and its dependencies, and it creates an express middleware
|
|
82
65
|
* that will pass along the AdapterRequest to the appropriate Transport (acc. to the endpoint in the req.)
|
package/transports/index.js
CHANGED
|
@@ -43,9 +43,9 @@ const buildCacheEntriesFromResults = (results, context) => results.map((r) => {
|
|
|
43
43
|
maxAge: Date.now() + context.adapterConfig.CACHE_MAX_AGE,
|
|
44
44
|
meta: {
|
|
45
45
|
metrics: {
|
|
46
|
-
feedId: (0, cache_1.calculateFeedId)(context
|
|
47
|
-
}
|
|
48
|
-
}
|
|
46
|
+
feedId: (0, cache_1.calculateFeedId)(context, r.params),
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
49
|
};
|
|
50
50
|
cacheEntry.value = { ...cacheEntry.value, ...metrics };
|
|
51
51
|
}
|
package/transports/metrics.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as client from 'prom-client';
|
|
2
|
-
import { AdapterContext } from '
|
|
2
|
+
import { AdapterContext } from '../adapter';
|
|
3
3
|
export declare const dataProviderMetricsLabel: (providerStatusCode?: number, method?: string) => {
|
|
4
4
|
provider_status_code: number | undefined;
|
|
5
5
|
method: string;
|
package/transports/metrics.js
CHANGED
|
@@ -59,7 +59,7 @@ exports.messageSubsLabels = messageSubsLabels;
|
|
|
59
59
|
// since avoiding storing extra info in expiring sorted set
|
|
60
60
|
const recordWsMessageMetrics = (context, subscribes, unsubscrices) => {
|
|
61
61
|
subscribes.forEach((param) => {
|
|
62
|
-
const feedId = (0, cache_1.calculateFeedId)(context
|
|
62
|
+
const feedId = (0, cache_1.calculateFeedId)(context, param);
|
|
63
63
|
const cacheKey = (0, cache_1.calculateCacheKey)(context, param);
|
|
64
64
|
// Record total number of ws messages sent
|
|
65
65
|
exports.wsMessageTotal.labels((0, exports.messageSubsLabels)(feedId, cacheKey)).inc();
|
|
@@ -69,7 +69,7 @@ const recordWsMessageMetrics = (context, subscribes, unsubscrices) => {
|
|
|
69
69
|
exports.wsSubscriptionActive.labels((0, exports.messageSubsLabels)(feedId, cacheKey)).inc();
|
|
70
70
|
});
|
|
71
71
|
unsubscrices.forEach((param) => {
|
|
72
|
-
const feedId = (0, cache_1.calculateFeedId)(context
|
|
72
|
+
const feedId = (0, cache_1.calculateFeedId)(context, param);
|
|
73
73
|
const cacheKey = (0, cache_1.calculateCacheKey)(context, param);
|
|
74
74
|
// Record total number of ws messages sent
|
|
75
75
|
exports.wsMessageTotal.labels((0, exports.messageSubsLabels)(feedId, cacheKey)).inc();
|
package/transports/rest.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { AdapterRequest, AdapterResponse } from '../util/request';
|
|
2
|
-
import {
|
|
2
|
+
import { Transport } from './';
|
|
3
3
|
import { Cache } from '../cache';
|
|
4
4
|
import { AxiosRequestConfig, AxiosResponse } from 'axios';
|
|
5
5
|
import { AdapterConfig, SettingsMap } from '../config';
|
|
6
6
|
import { RequestRateLimiter } from '../rate-limiting';
|
|
7
|
+
import { AdapterDependencies } from '../adapter';
|
|
7
8
|
/**
|
|
8
9
|
* Transport implementation that takes incoming requests, transforms them into a DataProvider request,
|
|
9
10
|
* and executes that request returning the response immediately from the `setup` function.
|
package/transports/rest.js
CHANGED
|
@@ -101,7 +101,9 @@ class RestTransport {
|
|
|
101
101
|
if (config.METRICS_ENABLED && config.EXPERIMENTAL_METRICS_ENABLED) {
|
|
102
102
|
// TODO: Potentially create function to add all telemetry data
|
|
103
103
|
parsedResponse.maxAge = Date.now() + config.CACHE_MAX_AGE;
|
|
104
|
-
parsedResponse.meta = {
|
|
104
|
+
parsedResponse.meta = {
|
|
105
|
+
metrics: { feedId: req.requestContext.meta?.metrics?.feedId || 'N/A' },
|
|
106
|
+
};
|
|
105
107
|
}
|
|
106
108
|
logger.debug('Setting provider response in cache');
|
|
107
109
|
await this.cache.set(req.requestContext.cacheKey, parsedResponse, config.CACHE_MAX_AGE);
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import WebSocket from 'ws';
|
|
2
|
+
import { AdapterContext, AdapterDependencies } from '../adapter';
|
|
2
3
|
import { Cache } from '../cache';
|
|
3
4
|
import { SettingsMap } from '../config';
|
|
4
5
|
import { BackgroundExecuteRateLimiter } from '../rate-limiting';
|
|
5
|
-
import {
|
|
6
|
+
import { SubscriptionSet } from '../util';
|
|
6
7
|
import { AdapterRequest, ProviderResult } from '../util/request';
|
|
7
|
-
import {
|
|
8
|
+
import { Transport } from './';
|
|
8
9
|
export declare const DEFAULT_WS_TTL = 10000;
|
|
9
10
|
declare type WebSocketClass = new (url: string, protocols?: string | string[] | undefined) => WebSocket;
|
|
10
11
|
export declare class WebSocketClassProvider {
|
|
@@ -55,7 +56,7 @@ export interface WebSocketTransportConfig<AdapterParams, ProviderDataMessage, Cu
|
|
|
55
56
|
};
|
|
56
57
|
}
|
|
57
58
|
/**
|
|
58
|
-
* Transport implementation that takes incoming requests, adds them to an [[
|
|
59
|
+
* Transport implementation that takes incoming requests, adds them to an [[subscriptionSet]] and,
|
|
59
60
|
* through a WebSocket connection, subscribes to the relevant feeds to populate the cache.
|
|
60
61
|
*
|
|
61
62
|
* @typeParam AdapterParams - interface for the adapter request body
|
|
@@ -65,7 +66,7 @@ export declare class WebSocketTransport<AdapterParams, ProviderDataMessage, Cust
|
|
|
65
66
|
private config;
|
|
66
67
|
cache: Cache;
|
|
67
68
|
rateLimiter: BackgroundExecuteRateLimiter;
|
|
68
|
-
|
|
69
|
+
subscriptionSet: SubscriptionSet<AdapterParams>;
|
|
69
70
|
localSubscriptions: AdapterParams[];
|
|
70
71
|
wsConnection: WebSocket;
|
|
71
72
|
constructor(config: WebSocketTransportConfig<AdapterParams, ProviderDataMessage, CustomSettings>);
|
package/transports/websocket.js
CHANGED
|
@@ -45,7 +45,7 @@ class WebSocketClassProvider {
|
|
|
45
45
|
exports.WebSocketClassProvider = WebSocketClassProvider;
|
|
46
46
|
WebSocketClassProvider.ctor = ws_1.default;
|
|
47
47
|
/**
|
|
48
|
-
* Transport implementation that takes incoming requests, adds them to an [[
|
|
48
|
+
* Transport implementation that takes incoming requests, adds them to an [[subscriptionSet]] and,
|
|
49
49
|
* through a WebSocket connection, subscribes to the relevant feeds to populate the cache.
|
|
50
50
|
*
|
|
51
51
|
* @typeParam AdapterParams - interface for the adapter request body
|
|
@@ -55,21 +55,20 @@ class WebSocketTransport {
|
|
|
55
55
|
constructor(config) {
|
|
56
56
|
this.config = config;
|
|
57
57
|
// The double sets serve to create a simple polling mechanism instead of needing a subscription
|
|
58
|
-
// This one would be either local, redis, etc
|
|
59
|
-
this.expiringSortedSet = new util_1.ExpiringSortedSet(); // TODO: Move to dependencies, inject
|
|
60
58
|
// This one would not; this is always local state
|
|
61
59
|
this.localSubscriptions = [];
|
|
62
60
|
}
|
|
63
61
|
async initialize(dependencies) {
|
|
64
62
|
this.cache = dependencies.cache;
|
|
65
63
|
this.rateLimiter = dependencies.backgroundExecuteRateLimiter;
|
|
64
|
+
this.subscriptionSet = dependencies.subscriptionSetFactory.buildSet();
|
|
66
65
|
}
|
|
67
66
|
async hasBeenSetUp(req) {
|
|
68
|
-
return !!this.
|
|
67
|
+
return !!(await this.subscriptionSet.get(req.requestContext.cacheKey));
|
|
69
68
|
}
|
|
70
69
|
async setup(req) {
|
|
71
70
|
logger.debug(`Adding entry to subscription set: [${req.requestContext.cacheKey}] = ${req.requestContext.data}`);
|
|
72
|
-
this.
|
|
71
|
+
await this.subscriptionSet.add(req.requestContext.cacheKey, req.requestContext.data, exports.DEFAULT_WS_TTL);
|
|
73
72
|
}
|
|
74
73
|
// TODO: Maybe we don't do this, and leave the preparation on the adapter's side?
|
|
75
74
|
// TODO: Maybe we store adapter params pre-prepared? That would be more efficient
|
|
@@ -121,7 +120,7 @@ class WebSocketTransport {
|
|
|
121
120
|
// Unlike cache warming, this execute will manage subscriptions
|
|
122
121
|
async backgroundExecute(context) {
|
|
123
122
|
logger.debug('Starting background execute, getting subscriptions from sorted set');
|
|
124
|
-
const desiredSubs = this.
|
|
123
|
+
const desiredSubs = await this.subscriptionSet.getAll();
|
|
125
124
|
logger.debug('Generating delta (subscribes & unsubscribes)');
|
|
126
125
|
// TODO: More efficient algorithm, this is really easy to read, but high(er) time complexity
|
|
127
126
|
const subscribeParams = desiredSubs.filter((s) => !this.localSubscriptions.includes(s));
|
package/util/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export * from './request';
|
|
2
2
|
export * from './logger';
|
|
3
|
-
export * from './
|
|
3
|
+
export * from './subscription-set/subscription-set';
|
|
4
4
|
/**
|
|
5
5
|
* Sleeps for the provided number of milliseconds
|
|
6
6
|
* @param ms - The number of milliseconds to sleep for
|
|
@@ -9,3 +9,4 @@ export * from './expiring-sorted-set';
|
|
|
9
9
|
export declare const sleep: (ms: number) => Promise<void>;
|
|
10
10
|
export declare const isObject: (o: unknown) => boolean;
|
|
11
11
|
export declare const isArray: (o: unknown) => boolean;
|
|
12
|
+
export declare type PromiseOrValue<T> = Promise<T> | T;
|
package/util/index.js
CHANGED
|
@@ -17,7 +17,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
17
17
|
exports.isArray = exports.isObject = exports.sleep = void 0;
|
|
18
18
|
__exportStar(require("./request"), exports);
|
|
19
19
|
__exportStar(require("./logger"), exports);
|
|
20
|
-
__exportStar(require("./
|
|
20
|
+
__exportStar(require("./subscription-set/subscription-set"), exports);
|
|
21
21
|
/**
|
|
22
22
|
* Sleeps for the provided number of milliseconds
|
|
23
23
|
* @param ms - The number of milliseconds to sleep for
|
package/util/request.d.ts
CHANGED
|
@@ -38,7 +38,9 @@ export interface AdapterMetricsMeta {
|
|
|
38
38
|
feedId?: string;
|
|
39
39
|
cacheHit?: boolean;
|
|
40
40
|
}
|
|
41
|
-
export declare type AdapterRequestData = Record<string, unknown
|
|
41
|
+
export declare type AdapterRequestData = Record<string, unknown> & {
|
|
42
|
+
endpoint?: string;
|
|
43
|
+
};
|
|
42
44
|
export interface ProviderResult<Params> {
|
|
43
45
|
params: Params;
|
|
44
46
|
value: unknown;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { SubscriptionSet } from './subscription-set';
|
|
2
|
+
/**
|
|
3
|
+
* An object describing an entry in the expiring sorted set.
|
|
4
|
+
* @typeParam T - the type of the entry's value
|
|
5
|
+
*/
|
|
6
|
+
interface ExpiringSortedSetEntry<T> {
|
|
7
|
+
value: T;
|
|
8
|
+
expirationTimestamp: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* This class implements a set of unique items, each of which has an expiration timestamp.
|
|
12
|
+
* On reads, items that have expired will be deleted from the set and not returned.
|
|
13
|
+
*
|
|
14
|
+
* @typeParam T - the type of the set entries' values
|
|
15
|
+
*/
|
|
16
|
+
export declare class ExpiringSortedSet<T> implements SubscriptionSet<T> {
|
|
17
|
+
map: Map<string, ExpiringSortedSetEntry<T>>;
|
|
18
|
+
add(key: string, value: T, ttl: number): void;
|
|
19
|
+
get(key: string): T | undefined;
|
|
20
|
+
getAll(): T[];
|
|
21
|
+
}
|
|
22
|
+
export {};
|