@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.
Files changed (128) hide show
  1. package/adapter.d.ts +22 -3
  2. package/adapter.js +5 -2
  3. package/cache/factory.js +0 -2
  4. package/cache/index.d.ts +6 -2
  5. package/cache/index.js +13 -9
  6. package/cache/redis.js +5 -5
  7. package/chainlink-external-adapter-framework-0.0.6.tgz +0 -0
  8. package/config/index.d.ts +15 -1
  9. package/config/index.js +19 -4
  10. package/config/provider-limits.js +5 -1
  11. package/examples/bank-frick/accounts.d.ts +39 -0
  12. package/examples/bank-frick/accounts.js +191 -0
  13. package/examples/bank-frick/config/index.d.ts +4 -0
  14. package/examples/bank-frick/config/index.js +54 -0
  15. package/examples/bank-frick/index.d.ts +2 -0
  16. package/examples/bank-frick/index.js +14 -0
  17. package/examples/bank-frick/util.d.ts +4 -0
  18. package/examples/bank-frick/util.js +39 -0
  19. package/index.d.ts +1 -2
  20. package/index.js +42 -1
  21. package/metrics/index.js +0 -1
  22. package/metrics/util.d.ts +5 -1
  23. package/metrics/util.js +2 -2
  24. package/package/adapter.d.ts +88 -0
  25. package/package/adapter.js +112 -0
  26. package/package/background-executor.d.ts +11 -0
  27. package/package/background-executor.js +45 -0
  28. package/package/cache/factory.d.ts +6 -0
  29. package/package/cache/factory.js +57 -0
  30. package/package/cache/index.d.ts +90 -0
  31. package/package/cache/index.js +169 -0
  32. package/package/cache/local.d.ts +23 -0
  33. package/package/cache/local.js +83 -0
  34. package/package/cache/metrics.d.ts +27 -0
  35. package/package/cache/metrics.js +120 -0
  36. package/package/cache/redis.d.ts +16 -0
  37. package/package/cache/redis.js +100 -0
  38. package/package/config/index.d.ts +195 -0
  39. package/package/config/index.js +365 -0
  40. package/package/config/provider-limits.d.ts +31 -0
  41. package/package/config/provider-limits.js +76 -0
  42. package/package/examples/coingecko/batch-warming.d.ts +2 -0
  43. package/package/examples/coingecko/batch-warming.js +52 -0
  44. package/package/examples/coingecko/index.d.ts +2 -0
  45. package/package/examples/coingecko/index.js +10 -0
  46. package/package/examples/coingecko/rest.d.ts +2 -0
  47. package/package/examples/coingecko/rest.js +50 -0
  48. package/package/examples/ncfx/config/index.d.ts +12 -0
  49. package/package/examples/ncfx/config/index.js +15 -0
  50. package/package/examples/ncfx/index.d.ts +2 -0
  51. package/package/examples/ncfx/index.js +10 -0
  52. package/package/examples/ncfx/websocket.d.ts +36 -0
  53. package/package/examples/ncfx/websocket.js +72 -0
  54. package/package/index.d.ts +12 -0
  55. package/package/index.js +92 -0
  56. package/package/metrics/constants.d.ts +16 -0
  57. package/package/metrics/constants.js +25 -0
  58. package/package/metrics/index.d.ts +15 -0
  59. package/package/metrics/index.js +123 -0
  60. package/package/metrics/util.d.ts +3 -0
  61. package/package/metrics/util.js +9 -0
  62. package/package/package.json +72 -0
  63. package/package/rate-limiting/background/fixed-frequency.d.ts +10 -0
  64. package/package/rate-limiting/background/fixed-frequency.js +37 -0
  65. package/package/rate-limiting/index.d.ts +54 -0
  66. package/package/rate-limiting/index.js +63 -0
  67. package/package/rate-limiting/metrics.d.ts +3 -0
  68. package/package/rate-limiting/metrics.js +44 -0
  69. package/package/rate-limiting/request/simple-counting.d.ts +20 -0
  70. package/package/rate-limiting/request/simple-counting.js +62 -0
  71. package/package/test.d.ts +1 -0
  72. package/package/test.js +6 -0
  73. package/package/transports/batch-warming.d.ts +34 -0
  74. package/package/transports/batch-warming.js +101 -0
  75. package/package/transports/index.d.ts +87 -0
  76. package/package/transports/index.js +87 -0
  77. package/package/transports/metrics.d.ts +21 -0
  78. package/package/transports/metrics.js +105 -0
  79. package/package/transports/rest.d.ts +43 -0
  80. package/package/transports/rest.js +129 -0
  81. package/package/transports/util.d.ts +8 -0
  82. package/package/transports/util.js +85 -0
  83. package/package/transports/websocket.d.ts +80 -0
  84. package/package/transports/websocket.js +169 -0
  85. package/package/util/expiring-sorted-set.d.ts +21 -0
  86. package/package/util/expiring-sorted-set.js +47 -0
  87. package/package/util/index.d.ts +11 -0
  88. package/package/util/index.js +35 -0
  89. package/package/util/logger.d.ts +42 -0
  90. package/package/util/logger.js +62 -0
  91. package/package/util/request.d.ts +55 -0
  92. package/package/util/request.js +2 -0
  93. package/package/validation/error.d.ts +50 -0
  94. package/package/validation/error.js +79 -0
  95. package/package/validation/index.d.ts +5 -0
  96. package/package/validation/index.js +86 -0
  97. package/package/validation/input-params.d.ts +15 -0
  98. package/package/validation/input-params.js +30 -0
  99. package/package/validation/override-functions.d.ts +3 -0
  100. package/package/validation/override-functions.js +40 -0
  101. package/package/validation/preset-tokens.json +23 -0
  102. package/package/validation/validator.d.ts +47 -0
  103. package/package/validation/validator.js +303 -0
  104. package/package.json +5 -3
  105. package/rate-limiting/background/fixed-frequency.js +0 -2
  106. package/test.js +2 -2
  107. package/transports/batch-warming.d.ts +4 -3
  108. package/transports/batch-warming.js +4 -4
  109. package/transports/index.d.ts +4 -21
  110. package/transports/index.js +3 -3
  111. package/transports/metrics.d.ts +1 -1
  112. package/transports/metrics.js +2 -2
  113. package/transports/rest.d.ts +2 -1
  114. package/transports/rest.js +3 -1
  115. package/transports/websocket.d.ts +5 -4
  116. package/transports/websocket.js +5 -6
  117. package/util/index.d.ts +2 -1
  118. package/util/index.js +1 -1
  119. package/util/request.d.ts +3 -1
  120. package/util/subscription-set/expiring-sorted-set.d.ts +22 -0
  121. package/util/subscription-set/expiring-sorted-set.js +47 -0
  122. package/util/subscription-set/subscription-set.d.ts +18 -0
  123. package/util/subscription-set/subscription-set.js +19 -0
  124. package/util/test-payload-loader.d.ts +25 -0
  125. package/util/test-payload-loader.js +83 -0
  126. package/validation/error.d.ts +2 -2
  127. package/validation/error.js +1 -1
  128. 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.10",
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 coingecko_1 = require("./examples/coingecko");
4
+ const bank_frick_1 = require("./examples/bank-frick");
5
5
  // Start sample adapter
6
- (0, _1.expose)(coingecko_1.adapter);
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 { ExpiringSortedSet } from '../util';
5
+ import { SubscriptionSet } from '../util';
6
6
  import { AdapterRequest, ProviderResult } from '../util/request';
7
- import { AdapterContext, AdapterDependencies, Transport } from './';
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
- expiringSortedSet: ExpiringSortedSet<AdapterParams>;
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.expiringSortedSet.get(req.requestContext.cacheKey);
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.expiringSortedSet.add(req.requestContext.cacheKey, req.requestContext.data, config.WARMUP_SUBSCRIPTION_TTL);
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.expiringSortedSet.getAll();
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) {
@@ -1,28 +1,11 @@
1
1
  import { FastifyReply } from 'fastify';
2
- import { AdapterEndpoint, InitializedAdapter } from '../adapter';
3
- import { Cache, CacheEntry } from '../cache';
4
- import { AdapterConfig, CustomSettingsType, SettingsMap } from '../config';
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, Context extends AdapterContext<any>>(results: ProviderResult<Params>[], context: Context) => CacheEntry<AdapterResponse<null>>[];
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.)
@@ -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.adapterEndpoint, r.params)
47
- }
48
- }
46
+ feedId: (0, cache_1.calculateFeedId)(context, r.params),
47
+ },
48
+ },
49
49
  };
50
50
  cacheEntry.value = { ...cacheEntry.value, ...metrics };
51
51
  }
@@ -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;
@@ -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.adapterEndpoint, param);
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.adapterEndpoint, param);
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();
@@ -1,9 +1,10 @@
1
1
  import { AdapterRequest, AdapterResponse } from '../util/request';
2
- import { AdapterDependencies, Transport } from './';
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.
@@ -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 = { metrics: { feedId: req.requestContext.meta?.metrics?.feedId || 'N/A' } };
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 { ExpiringSortedSet } from '../util';
6
+ import { SubscriptionSet } from '../util';
6
7
  import { AdapterRequest, ProviderResult } from '../util/request';
7
- import { AdapterContext, AdapterDependencies, Transport } from './';
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 [[ExpiringSortedSet]] and,
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
- expiringSortedSet: ExpiringSortedSet<AdapterParams>;
69
+ subscriptionSet: SubscriptionSet<AdapterParams>;
69
70
  localSubscriptions: AdapterParams[];
70
71
  wsConnection: WebSocket;
71
72
  constructor(config: WebSocketTransportConfig<AdapterParams, ProviderDataMessage, CustomSettings>);
@@ -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 [[ExpiringSortedSet]] and,
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.expiringSortedSet.get(req.requestContext.cacheKey);
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.expiringSortedSet.add(req.requestContext.cacheKey, req.requestContext.data, exports.DEFAULT_WS_TTL);
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.expiringSortedSet.getAll();
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 './expiring-sorted-set';
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("./expiring-sorted-set"), exports);
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 {};