@chainlink/external-adapter-framework 0.0.10 → 0.0.12

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 (269) hide show
  1. package/.c8rc.json +3 -0
  2. package/.eslintignore +9 -0
  3. package/.eslintrc.js +96 -0
  4. package/.github/README.MD +17 -0
  5. package/.github/actions/setup/action.yaml +13 -0
  6. package/.github/workflows/main.yaml +39 -0
  7. package/.github/workflows/publish.yaml +20 -0
  8. package/.prettierignore +13 -0
  9. package/.yarnrc +0 -0
  10. package/README.md +103 -0
  11. package/dist/src/adapter.d.ts +135 -0
  12. package/dist/src/adapter.js +145 -0
  13. package/dist/src/background-executor.d.ts +11 -0
  14. package/{background-executor.js → dist/src/background-executor.js} +0 -0
  15. package/{cache → dist/src/cache}/factory.d.ts +0 -0
  16. package/dist/src/cache/factory.js +55 -0
  17. package/dist/src/cache/index.d.ts +94 -0
  18. package/dist/src/cache/index.js +173 -0
  19. package/{cache → dist/src/cache}/local.d.ts +0 -0
  20. package/{cache → dist/src/cache}/local.js +0 -0
  21. package/{cache → dist/src/cache}/metrics.d.ts +0 -0
  22. package/{cache → dist/src/cache}/metrics.js +0 -0
  23. package/{cache → dist/src/cache}/redis.d.ts +0 -0
  24. package/dist/src/cache/redis.js +100 -0
  25. package/dist/src/chainlink-external-adapter-framework-0.0.6.tgz +0 -0
  26. package/dist/src/config/index.d.ts +214 -0
  27. package/dist/src/config/index.js +380 -0
  28. package/{config → dist/src/config}/provider-limits.d.ts +0 -0
  29. package/{config → dist/src/config}/provider-limits.js +1 -1
  30. package/dist/src/examples/bank-frick/accounts.d.ts +39 -0
  31. package/dist/src/examples/bank-frick/accounts.js +192 -0
  32. package/dist/src/examples/bank-frick/config/index.d.ts +4 -0
  33. package/dist/src/examples/bank-frick/config/index.js +54 -0
  34. package/dist/src/examples/bank-frick/index.d.ts +2 -0
  35. package/dist/src/examples/bank-frick/index.js +15 -0
  36. package/dist/src/examples/bank-frick/util.d.ts +4 -0
  37. package/dist/src/examples/bank-frick/util.js +39 -0
  38. package/dist/src/examples/coingecko/batch-warming.d.ts +7 -0
  39. package/dist/src/examples/coingecko/batch-warming.js +53 -0
  40. package/dist/src/examples/coingecko/index.d.ts +2 -0
  41. package/dist/src/examples/coingecko/index.js +11 -0
  42. package/dist/src/examples/coingecko/rest.d.ts +12 -0
  43. package/dist/src/examples/coingecko/rest.js +51 -0
  44. package/{examples → dist/src/examples}/ncfx/config/index.d.ts +0 -0
  45. package/{examples → dist/src/examples}/ncfx/config/index.js +0 -0
  46. package/dist/src/examples/ncfx/index.d.ts +13 -0
  47. package/dist/src/examples/ncfx/index.js +11 -0
  48. package/dist/src/examples/ncfx/websocket.d.ts +47 -0
  49. package/dist/src/examples/ncfx/websocket.js +73 -0
  50. package/dist/src/index.d.ts +11 -0
  51. package/dist/src/index.js +128 -0
  52. package/{metrics → dist/src/metrics}/constants.d.ts +0 -0
  53. package/{metrics → dist/src/metrics}/constants.js +0 -0
  54. package/{metrics → dist/src/metrics}/index.d.ts +0 -0
  55. package/dist/src/metrics/index.js +122 -0
  56. package/dist/src/metrics/util.d.ts +7 -0
  57. package/dist/src/metrics/util.js +9 -0
  58. package/{adapter.d.ts → dist/src/package/adapter.d.ts} +0 -0
  59. package/{adapter.js → dist/src/package/adapter.js} +0 -0
  60. package/{background-executor.d.ts → dist/src/package/background-executor.d.ts} +0 -0
  61. package/dist/src/package/background-executor.js +45 -0
  62. package/dist/src/package/cache/factory.d.ts +6 -0
  63. package/{cache → dist/src/package/cache}/factory.js +0 -0
  64. package/{cache → dist/src/package/cache}/index.d.ts +0 -0
  65. package/{cache → dist/src/package/cache}/index.js +0 -0
  66. package/dist/src/package/cache/local.d.ts +23 -0
  67. package/dist/src/package/cache/local.js +83 -0
  68. package/dist/src/package/cache/metrics.d.ts +27 -0
  69. package/dist/src/package/cache/metrics.js +120 -0
  70. package/dist/src/package/cache/redis.d.ts +16 -0
  71. package/{cache → dist/src/package/cache}/redis.js +0 -0
  72. package/{config → dist/src/package/config}/index.d.ts +0 -0
  73. package/{config → dist/src/package/config}/index.js +0 -0
  74. package/dist/src/package/config/provider-limits.d.ts +31 -0
  75. package/dist/src/package/config/provider-limits.js +76 -0
  76. package/{examples → dist/src/package/examples}/coingecko/batch-warming.d.ts +0 -0
  77. package/{examples → dist/src/package/examples}/coingecko/batch-warming.js +0 -0
  78. package/{examples → dist/src/package/examples}/coingecko/index.d.ts +0 -0
  79. package/{examples → dist/src/package/examples}/coingecko/index.js +0 -0
  80. package/{examples → dist/src/package/examples}/coingecko/rest.d.ts +0 -0
  81. package/{examples → dist/src/package/examples}/coingecko/rest.js +0 -0
  82. package/dist/src/package/examples/ncfx/config/index.d.ts +12 -0
  83. package/dist/src/package/examples/ncfx/config/index.js +15 -0
  84. package/{examples → dist/src/package/examples}/ncfx/index.d.ts +0 -0
  85. package/{examples → dist/src/package/examples}/ncfx/index.js +0 -0
  86. package/{examples → dist/src/package/examples}/ncfx/websocket.d.ts +0 -0
  87. package/{examples → dist/src/package/examples}/ncfx/websocket.js +0 -0
  88. package/{index.d.ts → dist/src/package/index.d.ts} +0 -0
  89. package/{index.js → dist/src/package/index.js} +0 -0
  90. package/dist/src/package/metrics/constants.d.ts +16 -0
  91. package/dist/src/package/metrics/constants.js +25 -0
  92. package/dist/src/package/metrics/index.d.ts +15 -0
  93. package/{metrics → dist/src/package/metrics}/index.js +0 -0
  94. package/{metrics → dist/src/package/metrics}/util.d.ts +0 -0
  95. package/{metrics → dist/src/package/metrics}/util.js +0 -0
  96. package/dist/src/package/package.json +72 -0
  97. package/{rate-limiting → dist/src/package/rate-limiting}/background/fixed-frequency.d.ts +0 -0
  98. package/{rate-limiting → dist/src/package/rate-limiting}/background/fixed-frequency.js +0 -0
  99. package/{rate-limiting → dist/src/package/rate-limiting}/index.d.ts +0 -0
  100. package/{rate-limiting → dist/src/package/rate-limiting}/index.js +0 -0
  101. package/{rate-limiting → dist/src/package/rate-limiting}/metrics.d.ts +0 -0
  102. package/{rate-limiting → dist/src/package/rate-limiting}/metrics.js +0 -0
  103. package/{rate-limiting → dist/src/package/rate-limiting}/request/simple-counting.d.ts +0 -0
  104. package/{rate-limiting → dist/src/package/rate-limiting}/request/simple-counting.js +0 -0
  105. package/{test.d.ts → dist/src/package/test.d.ts} +0 -0
  106. package/{test.js → dist/src/package/test.js} +0 -0
  107. package/{transports → dist/src/package/transports}/batch-warming.d.ts +0 -0
  108. package/{transports → dist/src/package/transports}/batch-warming.js +0 -0
  109. package/{transports → dist/src/package/transports}/index.d.ts +0 -0
  110. package/{transports → dist/src/package/transports}/index.js +0 -0
  111. package/{transports → dist/src/package/transports}/metrics.d.ts +0 -0
  112. package/{transports → dist/src/package/transports}/metrics.js +0 -0
  113. package/{transports → dist/src/package/transports}/rest.d.ts +0 -0
  114. package/{transports → dist/src/package/transports}/rest.js +0 -0
  115. package/{transports → dist/src/package/transports}/util.d.ts +0 -0
  116. package/{transports → dist/src/package/transports}/util.js +0 -0
  117. package/{transports → dist/src/package/transports}/websocket.d.ts +0 -0
  118. package/{transports → dist/src/package/transports}/websocket.js +0 -0
  119. package/{util → dist/src/package/util}/expiring-sorted-set.d.ts +0 -0
  120. package/{util → dist/src/package/util}/expiring-sorted-set.js +0 -0
  121. package/{util → dist/src/package/util}/index.d.ts +0 -0
  122. package/{util → dist/src/package/util}/index.js +0 -0
  123. package/{util → dist/src/package/util}/logger.d.ts +0 -0
  124. package/{util → dist/src/package/util}/logger.js +0 -0
  125. package/{util → dist/src/package/util}/request.d.ts +0 -0
  126. package/{util → dist/src/package/util}/request.js +0 -0
  127. package/{validation → dist/src/package/validation}/error.d.ts +0 -0
  128. package/{validation → dist/src/package/validation}/error.js +0 -0
  129. package/{validation → dist/src/package/validation}/index.d.ts +0 -0
  130. package/{validation → dist/src/package/validation}/index.js +0 -0
  131. package/{validation → dist/src/package/validation}/input-params.d.ts +0 -0
  132. package/{validation → dist/src/package/validation}/input-params.js +0 -0
  133. package/{validation → dist/src/package/validation}/override-functions.d.ts +0 -0
  134. package/{validation → dist/src/package/validation}/override-functions.js +0 -0
  135. package/{validation → dist/src/package/validation}/preset-tokens.json +0 -0
  136. package/{validation → dist/src/package/validation}/validator.d.ts +0 -0
  137. package/{validation → dist/src/package/validation}/validator.js +0 -0
  138. package/dist/src/package.json +72 -0
  139. package/dist/src/rate-limiting/background/fixed-frequency.d.ts +11 -0
  140. package/dist/src/rate-limiting/background/fixed-frequency.js +35 -0
  141. package/dist/src/rate-limiting/index.d.ts +55 -0
  142. package/dist/src/rate-limiting/index.js +63 -0
  143. package/dist/src/rate-limiting/metrics.d.ts +3 -0
  144. package/dist/src/rate-limiting/metrics.js +44 -0
  145. package/dist/src/rate-limiting/request/simple-counting.d.ts +21 -0
  146. package/dist/src/rate-limiting/request/simple-counting.js +62 -0
  147. package/dist/src/test.d.ts +1 -0
  148. package/dist/src/test.js +6 -0
  149. package/dist/src/transports/batch-warming.d.ts +35 -0
  150. package/dist/src/transports/batch-warming.js +101 -0
  151. package/dist/src/transports/index.d.ts +70 -0
  152. package/dist/src/transports/index.js +87 -0
  153. package/dist/src/transports/metrics.d.ts +22 -0
  154. package/dist/src/transports/metrics.js +105 -0
  155. package/dist/src/transports/rest.d.ts +44 -0
  156. package/dist/src/transports/rest.js +131 -0
  157. package/dist/src/transports/util.d.ts +8 -0
  158. package/dist/src/transports/util.js +85 -0
  159. package/dist/src/transports/websocket.d.ts +80 -0
  160. package/dist/src/transports/websocket.js +166 -0
  161. package/dist/src/util/expiring-sorted-set.d.ts +21 -0
  162. package/dist/src/util/expiring-sorted-set.js +47 -0
  163. package/dist/src/util/index.d.ts +12 -0
  164. package/dist/src/util/index.js +35 -0
  165. package/dist/src/util/logger.d.ts +42 -0
  166. package/dist/src/util/logger.js +62 -0
  167. package/dist/src/util/request.d.ts +57 -0
  168. package/dist/src/util/request.js +2 -0
  169. package/dist/src/util/subscription-set/expiring-sorted-set.d.ts +22 -0
  170. package/dist/src/util/subscription-set/expiring-sorted-set.js +47 -0
  171. package/dist/src/util/subscription-set/subscription-set.d.ts +18 -0
  172. package/dist/src/util/subscription-set/subscription-set.js +19 -0
  173. package/dist/src/util/test-payload-loader.d.ts +25 -0
  174. package/dist/src/util/test-payload-loader.js +83 -0
  175. package/dist/src/validation/error.d.ts +50 -0
  176. package/dist/src/validation/error.js +79 -0
  177. package/dist/src/validation/index.d.ts +5 -0
  178. package/dist/src/validation/index.js +91 -0
  179. package/dist/src/validation/input-params.d.ts +15 -0
  180. package/dist/src/validation/input-params.js +30 -0
  181. package/dist/src/validation/override-functions.d.ts +3 -0
  182. package/dist/src/validation/override-functions.js +40 -0
  183. package/dist/src/validation/preset-tokens.json +23 -0
  184. package/dist/src/validation/validator.d.ts +47 -0
  185. package/dist/src/validation/validator.js +303 -0
  186. package/docker-compose.yaml +35 -0
  187. package/env.sh +54 -0
  188. package/env2.sh +55 -0
  189. package/package.json +5 -3
  190. package/publish.sh +0 -0
  191. package/src/adapter.ts +263 -0
  192. package/src/background-executor.ts +52 -0
  193. package/src/cache/factory.ts +26 -0
  194. package/src/cache/index.ts +258 -0
  195. package/src/cache/local.ts +73 -0
  196. package/src/cache/metrics.ts +112 -0
  197. package/src/cache/redis.ts +93 -0
  198. package/src/config/index.ts +517 -0
  199. package/src/config/provider-limits.ts +130 -0
  200. package/src/examples/bank-frick/README.MD +10 -0
  201. package/src/examples/bank-frick/accounts.ts +246 -0
  202. package/src/examples/bank-frick/config/index.ts +53 -0
  203. package/src/examples/bank-frick/index.ts +13 -0
  204. package/src/examples/bank-frick/types.d.ts +38 -0
  205. package/src/examples/bank-frick/util.ts +55 -0
  206. package/src/examples/coingecko/batch-warming.ts +78 -0
  207. package/src/examples/coingecko/index.ts +9 -0
  208. package/src/examples/coingecko/rest.ts +77 -0
  209. package/src/examples/ncfx/config/index.ts +12 -0
  210. package/src/examples/ncfx/index.ts +9 -0
  211. package/src/examples/ncfx/websocket.ts +99 -0
  212. package/src/index.ts +149 -0
  213. package/src/metrics/constants.ts +23 -0
  214. package/src/metrics/index.ts +115 -0
  215. package/src/metrics/util.ts +18 -0
  216. package/src/rate-limiting/background/fixed-frequency.ts +45 -0
  217. package/src/rate-limiting/index.ts +100 -0
  218. package/src/rate-limiting/metrics.ts +18 -0
  219. package/src/rate-limiting/request/simple-counting.ts +76 -0
  220. package/src/test.ts +5 -0
  221. package/src/transports/batch-warming.ts +122 -0
  222. package/src/transports/index.ts +152 -0
  223. package/src/transports/metrics.ts +95 -0
  224. package/src/transports/rest.ts +164 -0
  225. package/src/transports/util.ts +63 -0
  226. package/src/transports/websocket.ts +245 -0
  227. package/src/util/index.ts +22 -0
  228. package/src/util/logger.ts +69 -0
  229. package/src/util/request.ts +117 -0
  230. package/src/util/subscription-set/expiring-sorted-set.ts +54 -0
  231. package/src/util/subscription-set/subscription-set.ts +35 -0
  232. package/src/util/test-payload-loader.ts +87 -0
  233. package/src/validation/error.ts +116 -0
  234. package/src/validation/index.ts +110 -0
  235. package/src/validation/input-params.ts +45 -0
  236. package/src/validation/override-functions.ts +44 -0
  237. package/src/validation/preset-tokens.json +23 -0
  238. package/src/validation/validator.ts +384 -0
  239. package/test/adapter.test.ts +27 -0
  240. package/test/background-executor.test.ts +108 -0
  241. package/test/cache/cache-key.test.ts +114 -0
  242. package/test/cache/helper.ts +100 -0
  243. package/test/cache/local.test.ts +54 -0
  244. package/test/cache/redis.test.ts +89 -0
  245. package/test/correlation.test.ts +114 -0
  246. package/test/index.test.ts +37 -0
  247. package/test/metrics/feed-id.test.ts +38 -0
  248. package/test/metrics/helper.ts +14 -0
  249. package/test/metrics/labels.test.ts +36 -0
  250. package/test/metrics/metrics.test.ts +267 -0
  251. package/test/metrics/redis-metrics.test.ts +113 -0
  252. package/test/metrics/warmer-metrics.test.ts +192 -0
  253. package/test/metrics/ws-metrics.test.ts +225 -0
  254. package/test/rate-limit-config.test.ts +242 -0
  255. package/test/smoke.test.ts +166 -0
  256. package/test/transports/batch.test.ts +465 -0
  257. package/test/transports/rest.test.ts +242 -0
  258. package/test/transports/websocket.test.ts +183 -0
  259. package/test/tsconfig.json +5 -0
  260. package/test/util.ts +77 -0
  261. package/test/validation.test.ts +178 -0
  262. package/test-payload-fail.json +3 -0
  263. package/test-payload.js +22 -0
  264. package/test-payload.json +7 -0
  265. package/test.sh +20 -0
  266. package/test2.sh +2 -0
  267. package/tsconfig.json +25 -0
  268. package/typedoc.json +6 -0
  269. package/webpack.config.js +23 -0
package/src/adapter.ts ADDED
@@ -0,0 +1,263 @@
1
+ import { Cache, CacheFactory } from './cache'
2
+ import { AdapterConfig, BaseAdapterConfig, CustomSettingsType, SettingsMap } from './config'
3
+ import {
4
+ AdapterRateLimitTier,
5
+ BackgroundExecuteRateLimiter,
6
+ FixedFrequencyRateLimiter,
7
+ getRateLimitingTier,
8
+ RequestRateLimiter,
9
+ SimpleCountingRateLimiter,
10
+ } from './rate-limiting'
11
+ import { Transport } from './transports'
12
+ import { makeLogger, SubscriptionSetFactory } from './util'
13
+ import { InputParameters } from './validation/input-params'
14
+
15
+ const logger = makeLogger('Adapter')
16
+
17
+ /**
18
+ * Dependencies that will be injected into the Adapter on startup
19
+ */
20
+ export interface AdapterDependencies {
21
+ // TODO: Complete docs
22
+ cache: Cache
23
+ requestRateLimiter: RequestRateLimiter
24
+ backgroundExecuteRateLimiter: BackgroundExecuteRateLimiter
25
+ subscriptionSetFactory: SubscriptionSetFactory
26
+ }
27
+
28
+ /**
29
+ * Context that will be used on background executions of a Transport.
30
+ * For example, the endpointName used to log statements or generate Cache keys.
31
+ */
32
+ export interface AdapterContext<
33
+ CustomSettings extends CustomSettingsType<CustomSettings> = SettingsMap,
34
+ > {
35
+ adapterEndpoint: AdapterEndpoint
36
+ adapterConfig: AdapterConfig<CustomSettings>
37
+ }
38
+
39
+ /**
40
+ * Structure to describe rate limits specs for the Adapter
41
+ */
42
+ interface AdapterRateLimitingConfig {
43
+ /** Adapter rate limits, gotten from the specific tier requested */
44
+ tiers: Record<string, AdapterRateLimitTier>
45
+ }
46
+
47
+ /**
48
+ * Main structure of an External Adapter
49
+ */
50
+ export interface Adapter {
51
+ /** Name of the adapter */
52
+ name: string
53
+
54
+ /** If present, the string that will be used for requests with no specified endpoint */
55
+ defaultEndpoint?: string
56
+
57
+ /** List of [[AdapterEndpoint]]s in the adapter */
58
+ endpoints: AdapterEndpoint[]
59
+
60
+ /** Map of overrides to the default config values for an Adapter */
61
+ envDefaultOverrides?: Partial<BaseAdapterConfig>
62
+
63
+ /** List of custom env vars for this particular adapter (e.g. RPC_URL) */
64
+ customSettings?: SettingsMap
65
+
66
+ /** Configuration relevant to outbound (EA --\> DP) communication rate limiting */
67
+ rateLimiting?: AdapterRateLimitingConfig
68
+
69
+ /** Overrides for converting the 'base' parameter that are hardcoded into the adapter. */
70
+ // This must be included in the middleware in order to generate deterministing cache keys for hardcoded overrides
71
+ overrides?: Record<string, string>
72
+ }
73
+
74
+ /**
75
+ * Structure to describe rate limits specs for a specific adapter endpoint
76
+ */
77
+ export interface EndpointRateLimitingConfig {
78
+ /**
79
+ * How much of the total limit for the adapter will be assigned to this specific endpoint.
80
+ * Should be a non-zero positive number up to 100.
81
+ * Endpoints in the same adapter without a specific allocation will divide the remaining limits equally.
82
+ */
83
+ allocationPercentage: number
84
+ }
85
+
86
+ /**
87
+ * Structure to describe a specific endpoint in an [[Adapter]]
88
+ */
89
+ export interface AdapterEndpoint {
90
+ /** Name that will be used to match input params to this endpoint (case insensitive) */
91
+ name: string
92
+
93
+ /** List of alternative endpoint names that will resolve to this same transport (case insensitive) */
94
+ aliases?: string[]
95
+
96
+ /** Transport that will be used to handle data processing and communication for this endpoint */
97
+ transport: Transport<any, any, any>
98
+
99
+ /** Specification of what the body of a request hitting this endpoint should look like (used for validation) */
100
+ inputParameters: InputParameters
101
+
102
+ /** Specific details related to the rate limiting for this endpoint in particular */
103
+ rateLimiting?: EndpointRateLimitingConfig
104
+ }
105
+
106
+ /**
107
+ * Structure to describe an adapter that has been initialized
108
+ */
109
+ export interface InitializedAdapter extends Adapter {
110
+ /** Object containing alias translations for all endpoints */
111
+ endpointsMap: Record<string, AdapterEndpoint>
112
+
113
+ /** Initialized dependencies that the adapter will use */
114
+ dependencies: AdapterDependencies
115
+
116
+ /** Configuration params for various adapter properties */
117
+ config: AdapterConfig
118
+ }
119
+
120
+ /**
121
+ * This function will take an adapter structure and go through each endpoint, calculating
122
+ * each one's allocation of the total rate limits that are set for the adapter as a whole.
123
+ *
124
+ * @param adapter - the adapter to initialize rate limiting for
125
+ */
126
+ const calculateRateLimitAllocations = (adapter: Adapter) => {
127
+ const numberOfEndpoints = adapter.endpoints.length
128
+ const endpointsWithExplicitAllocations = adapter.endpoints.filter((e) => e.rateLimiting)
129
+
130
+ const totalExplicitAllocation = endpointsWithExplicitAllocations
131
+ .map((e) => e.rateLimiting?.allocationPercentage || 0)
132
+ .reduce((sum, next) => sum + next, 0)
133
+
134
+ if (totalExplicitAllocation > 100) {
135
+ throw new Error('The total allocation set for all endpoints summed cannot exceed 100%')
136
+ }
137
+
138
+ if (
139
+ totalExplicitAllocation === 100 &&
140
+ numberOfEndpoints - endpointsWithExplicitAllocations.length > 0
141
+ ) {
142
+ throw new Error(
143
+ 'The explicit allocation is at 100% but there are endpoints with implicit allocation',
144
+ )
145
+ }
146
+
147
+ const implicitAllocation = 100 - totalExplicitAllocation
148
+
149
+ logger.debug('Adapter rate limit allocations:')
150
+ for (const endpoint of adapter.endpoints) {
151
+ if (!endpoint.rateLimiting) {
152
+ endpoint.rateLimiting = {
153
+ allocationPercentage:
154
+ implicitAllocation / (numberOfEndpoints - endpointsWithExplicitAllocations.length),
155
+ }
156
+ }
157
+
158
+ logger.debug(`Endpoint [${endpoint.name}] - ${endpoint.rateLimiting?.allocationPercentage}%`)
159
+ }
160
+ }
161
+
162
+ /**
163
+ * This function will process dependencies for an adapter, such as caches or rate limiters,
164
+ * in order to inject them into transports and other relevant places later in the lifecycle.
165
+ *
166
+ * @param config - the configuration for this adapter
167
+ * @param inputDependencies - a partial obj of initialized dependencies to override the created ones
168
+ * @param rateLimitingConfig - details from the adapter regarding rate limiting
169
+ * @returns a set of AdapterDependencies all initialized
170
+ */
171
+ export const initializeDependencies = (
172
+ adapter: Adapter,
173
+ config: AdapterConfig,
174
+ inputDependencies?: Partial<AdapterDependencies>,
175
+ ): AdapterDependencies => {
176
+ const dependencies = inputDependencies || {}
177
+ if (!dependencies.cache) {
178
+ dependencies.cache = CacheFactory.buildCache(config)
179
+ }
180
+
181
+ // In the future we might want something more complex, but for now it's better to simplify
182
+ // and just use the same rate limiting for everything. Once we have a more complex use case we
183
+ // can think of ways to make this more configurable.
184
+ const rateLimitingTier = getRateLimitingTier(
185
+ adapter.rateLimiting?.tiers,
186
+ config.RATE_LIMIT_API_TIER,
187
+ )
188
+ if (!dependencies.requestRateLimiter) {
189
+ dependencies.requestRateLimiter = new SimpleCountingRateLimiter().initialize(
190
+ adapter.endpoints,
191
+ rateLimitingTier,
192
+ )
193
+ }
194
+ if (!dependencies.backgroundExecuteRateLimiter) {
195
+ dependencies.backgroundExecuteRateLimiter = new FixedFrequencyRateLimiter().initialize(
196
+ adapter.endpoints,
197
+ rateLimitingTier,
198
+ )
199
+ }
200
+ if (!dependencies.subscriptionSetFactory) {
201
+ dependencies.subscriptionSetFactory = new SubscriptionSetFactory(config)
202
+ }
203
+
204
+ return dependencies as AdapterDependencies
205
+ }
206
+
207
+ /**
208
+ * Takes an adapter and normalizes all endpoint names and aliases, as well as the default endpoint.
209
+ * i.e. makes them lowercase for now
210
+ * @param adapter - an instance of an Adapter
211
+ */
212
+ const normalizeEndpointNames = (adapter: Adapter) => {
213
+ // Make endpoints case insensitive, including default
214
+ adapter.defaultEndpoint = adapter.defaultEndpoint?.toLowerCase()
215
+
216
+ for (const endpoint of adapter.endpoints) {
217
+ endpoint.name = endpoint.name.toLowerCase()
218
+ endpoint.aliases = endpoint.aliases?.map((a) => a.toLowerCase())
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Initializes all of the [[Transport]]s in the adapter, passing along any [[AdapterDependencies]] and [[AdapterConfig]].
224
+ * Additionally, it builds a map out of all the endpoint names and aliases (checking for duplicates).
225
+ *
226
+ * @param adapter - an instance of an Adapter
227
+ * @param dependencies - dependencies that the adapter will need at initialization
228
+ * @param config - configuration variables already processed and validated
229
+ * @returns - the adapter with all transports initialized and aliases map built
230
+ */
231
+ export const initializeAdapter = async (
232
+ adapter: Adapter,
233
+ config: AdapterConfig,
234
+ dependencies?: Partial<AdapterDependencies>,
235
+ ): Promise<InitializedAdapter> => {
236
+ normalizeEndpointNames(adapter)
237
+ calculateRateLimitAllocations(adapter)
238
+ const initializedDependencies = initializeDependencies(adapter, config, dependencies)
239
+
240
+ const endpointsMap: Record<string, AdapterEndpoint> = {}
241
+
242
+ for (const endpoint of adapter.endpoints) {
243
+ // Add aliases to map to use in validation
244
+ const aliases = [endpoint.name, ...(endpoint.aliases || [])]
245
+ for (const alias of aliases) {
246
+ if (endpointsMap[alias]) {
247
+ throw new Error(`Duplicate endpoint / alias: "${alias}"`)
248
+ }
249
+ endpointsMap[alias] = endpoint
250
+ }
251
+
252
+ logger.debug(`Initializing transport for endpoint "${endpoint.name}"...`)
253
+ await endpoint.transport.initialize(initializedDependencies)
254
+ }
255
+
256
+ logger.debug('Adapter initialization complete.')
257
+ return {
258
+ ...adapter,
259
+ endpointsMap,
260
+ dependencies: initializedDependencies,
261
+ config,
262
+ }
263
+ }
@@ -0,0 +1,52 @@
1
+ import { Server } from 'http'
2
+ import { AdapterContext, InitializedAdapter } from './adapter'
3
+ import { makeLogger, sleep } from './util'
4
+
5
+ const logger = makeLogger('BackgroundExecutor')
6
+
7
+ /**
8
+ * Very simple background loop that will call the [[Transport.backgroundExecute]] functions in all Transports.
9
+ * It gets the time in ms to wait as the return value from those functions, and sleeps until next execution.
10
+ *
11
+ * @param adapter - an initialized External Adapter
12
+ * @param server - the http server to attach an on close listener to
13
+ */
14
+ export async function callBackgroundExecutes(adapter: InitializedAdapter, server?: Server) {
15
+ // Set up variable to check later on to see if we need to stop this background "thread"
16
+ // If no server is provided, the listener won't be set and serverClosed will always be false
17
+ let serverClosed = false
18
+ server?.on('close', () => {
19
+ serverClosed = true
20
+ })
21
+
22
+ for (const endpoint of adapter.endpoints) {
23
+ const backgroundExecute = endpoint.transport.backgroundExecute?.bind(endpoint.transport)
24
+ if (!backgroundExecute) {
25
+ logger.debug(`Endpoint "${endpoint.name}" has no background execute, skipping...`)
26
+ continue
27
+ }
28
+
29
+ const context: AdapterContext = {
30
+ adapterEndpoint: endpoint,
31
+ adapterConfig: adapter.config,
32
+ }
33
+
34
+ const handler = async () => {
35
+ if (serverClosed) {
36
+ logger.info('Server closed, stopping recursive backgroundExecute handler chain')
37
+ return
38
+ }
39
+
40
+ logger.debug(`Calling background execute for endpoint "${endpoint.name}"`)
41
+ const timeToWait = await backgroundExecute(context)
42
+ logger.debug(
43
+ `Finished background execute for endpoint "${endpoint.name}", sleeping for ${timeToWait}ms`,
44
+ )
45
+ await sleep(timeToWait)
46
+ handler()
47
+ }
48
+
49
+ // Start recursive async calls
50
+ handler()
51
+ }
52
+ }
@@ -0,0 +1,26 @@
1
+ import Redis from 'ioredis'
2
+ import { AdapterConfig } from '../config'
3
+ import { makeLogger } from '../util'
4
+ import { LocalCache } from './local'
5
+ import * as cacheMetrics from './metrics'
6
+ import { RedisCache } from './redis'
7
+
8
+ const logger = makeLogger('CacheFactory')
9
+ export class CacheFactory {
10
+ static buildCache(config: AdapterConfig) {
11
+ logger.info(`Using "${config.CACHE_TYPE}" cache.`)
12
+ if (config.CACHE_TYPE === 'local') {
13
+ return new LocalCache()
14
+ } else if (config.CACHE_TYPE === 'redis') {
15
+ const redis = new Redis({
16
+ enableAutoPipelining: true, // This will make multiple commands be batch automatically
17
+ host: config.CACHE_REDIS_HOST,
18
+ port: config.CACHE_REDIS_PORT,
19
+ })
20
+ redis.on('connect', () => {
21
+ cacheMetrics.redisConnectionsOpen.inc()
22
+ })
23
+ return new RedisCache(redis)
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,258 @@
1
+ import { FastifyReply } from 'fastify'
2
+ import { AdapterEndpoint, InitializedAdapter } from '../adapter'
3
+ import { AdapterConfig } from '../config'
4
+ import {
5
+ AdapterMiddlewareBuilder,
6
+ AdapterRequest,
7
+ AdapterResponse,
8
+ makeLogger,
9
+ sleep,
10
+ } from '../util'
11
+ import * as cacheMetrics from './metrics'
12
+
13
+ export * from './local'
14
+ export * from './redis'
15
+ export * from './factory'
16
+
17
+ const logger = makeLogger('Cache')
18
+
19
+ /**
20
+ * An object describing an entry in the cache.
21
+ * @typeParam T - the type of the entry's value
22
+ */
23
+ export interface CacheEntry<T> {
24
+ key: string
25
+ value: T
26
+ }
27
+
28
+ /**
29
+ * Generic interface for a local or remote Cache.
30
+ * @typeParam T - the type of the cache entries' values
31
+ */
32
+ export interface Cache<T = unknown> {
33
+ /**
34
+ * Gets an item from the Cache.
35
+ *
36
+ * @param key - the key of the desired entry for which to fetch its value
37
+ * @returns a Promise of the entry's value, or undefined if not found / expired.
38
+ */
39
+ get: (key: string) => Promise<T | undefined>
40
+
41
+ /**
42
+ * Sets an item in the Cache.
43
+ *
44
+ * @param key - the key of the new entry
45
+ * @param value - the value of the new entry
46
+ * @param ttl - the time in milliseconds until the entry expires
47
+ * @returns an empty Promise that resolves when the entry has been set
48
+ */
49
+ set: (key: string, value: T, ttl: number) => Promise<void>
50
+
51
+ /**
52
+ * Sets a list of items in the Cache.
53
+ *
54
+ * @param entries - a list of cache entries
55
+ * @param ttl - the time in milliseconds until the entries expire
56
+ * @returns an empty Promise that resolves when all entries have been set
57
+ */
58
+ setMany: (entries: CacheEntry<T>[], ttl: number) => Promise<void>
59
+
60
+ /**
61
+ * Deletes the specified item from the Cache
62
+ *
63
+ * @param key - the key of the entry to be deleted
64
+ * @returns an empty Promise that resolves when the entry has been deleted
65
+ */
66
+ delete: (key: string) => Promise<void>
67
+ }
68
+
69
+ // Uses calculateKey to generate a unique key from the endpoint name, data, and input parameters
70
+ export const calculateCacheKey = (
71
+ {
72
+ adapterEndpoint,
73
+ adapterConfig,
74
+ }: {
75
+ adapterEndpoint: AdapterEndpoint
76
+ adapterConfig: AdapterConfig
77
+ },
78
+ data: unknown,
79
+ ): string => {
80
+ const paramNames = Object.keys(adapterEndpoint.inputParameters)
81
+ if (paramNames.length === 0) {
82
+ logger.trace(`Using default cache key ${adapterConfig.DEFAULT_CACHE_KEY}`)
83
+ return adapterConfig.DEFAULT_CACHE_KEY
84
+ }
85
+ return `${adapterEndpoint.name}-${calculateKey(data, paramNames, adapterConfig)}`
86
+ }
87
+
88
+ export const calculateFeedId = (
89
+ {
90
+ adapterEndpoint,
91
+ adapterConfig,
92
+ }: {
93
+ adapterEndpoint: AdapterEndpoint
94
+ adapterConfig: AdapterConfig
95
+ },
96
+ data: unknown,
97
+ ): string => {
98
+ const paramNames = Object.keys(adapterEndpoint.inputParameters)
99
+ if (paramNames.length === 0) {
100
+ logger.trace(`Cannot generate Feed ID without input parameters`)
101
+ return 'N/A'
102
+ }
103
+ return calculateKey(data, paramNames, adapterConfig)
104
+ }
105
+
106
+ /**
107
+ * Calculates a unique key from the provided data.
108
+ *
109
+ * @param data - the request data/body, i.e. the adapter input params
110
+ * @param paramNames - the keys from adapter endpoint input parameters
111
+ * @returns the calculated unique key
112
+ *
113
+ * @example
114
+ * ```
115
+ * calculateKey({ base: 'ETH', quote: 'BTC' }, ['base','quote'])
116
+ * // equals `|base:eth|quote:btc`
117
+ * ```
118
+ */
119
+ export const calculateKey = (
120
+ data: unknown,
121
+ paramNames: string[],
122
+ adapterConfig: AdapterConfig,
123
+ ): string => {
124
+ if (data && typeof data !== 'object') {
125
+ throw new Error('Data to calculate cache key should be an object')
126
+ }
127
+
128
+ const params = data as Record<string, unknown>
129
+
130
+ let cacheKey = ''
131
+ for (const paramName of paramNames) {
132
+ // Ignore overrides param when generating cache keys
133
+ if (paramName === 'overrides') {
134
+ continue
135
+ }
136
+ const param = params[paramName]
137
+ if (param === undefined) {
138
+ continue
139
+ }
140
+
141
+ cacheKey += `|${paramName}:`
142
+ switch (typeof param) {
143
+ case 'string':
144
+ cacheKey += param.toLowerCase()
145
+ break
146
+ case 'number':
147
+ case 'boolean':
148
+ cacheKey += param.toString()
149
+ break
150
+ case 'object':
151
+ // Force cache keys to only use performant properties of the input params.
152
+ // If the object were to be used, we'd have to sort its properties.
153
+ logger.debug(
154
+ `Property "${paramName}" in request parameters is of type object, and won't be used in the cacheKey`,
155
+ )
156
+ }
157
+ }
158
+
159
+ if (cacheKey.length > adapterConfig.MAX_COMMON_KEY_SIZE) {
160
+ logger.warn(
161
+ `Generated cache key for adapter request is bigger than the MAX_COMMON_KEY_SIZE and will be truncated`,
162
+ )
163
+ cacheKey = cacheKey.slice(0, adapterConfig.MAX_COMMON_KEY_SIZE)
164
+ }
165
+
166
+ logger.trace(`Generated cache key for request: "${cacheKey}"`)
167
+ return cacheKey
168
+ }
169
+
170
+ // Calculate the amount of time the non-expired entry has been in the cache
171
+ const calculateStaleness = (expirationTimestamp: number | undefined, ttl: number): number => {
172
+ if (expirationTimestamp) {
173
+ const createTimestamp = expirationTimestamp - ttl
174
+ return (Date.now() - createTimestamp) / 1000
175
+ } else {
176
+ // If expirationTimestamp is not available, staleness cannot be calculated
177
+ // Defaults to ttl for metrics purposes
178
+ return ttl
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Polls the provided Cache for an AdapterResponse set in the provided key. If the maximum
184
+ * amount of retries is exceeded, it returns undefined instead.
185
+ *
186
+ * @param cache - a Cache instance
187
+ * @param key - the key generated from an AdapterRequest that corresponds to the desired AdapterResponse
188
+ * @param retry - current retry, only for internal use
189
+ * @returns the AdapterResponse if found, else undefined
190
+ */
191
+ export const pollResponseFromCache = async (
192
+ cache: Cache<AdapterResponse>,
193
+ key: string,
194
+ options: {
195
+ maxRetries: number
196
+ sleep: number
197
+ },
198
+ retry = 0,
199
+ ): Promise<AdapterResponse | undefined> => {
200
+ if (retry > options.maxRetries) {
201
+ // Ideally this shouldn't happen often (p99 of reqs should be found in the cache)
202
+ logger.info('Exceeded max cache polling retries')
203
+ return undefined
204
+ }
205
+
206
+ logger.trace('Getting response from cache...')
207
+ const response = await cache.get(key)
208
+ if (response) {
209
+ logger.trace('Got response from cache')
210
+ return response
211
+ }
212
+
213
+ if (options.maxRetries === 0) {
214
+ logger.debug(`Response not found, retries disabled`)
215
+ return undefined
216
+ }
217
+
218
+ logger.debug(`Response not found, sleeping ${options.sleep} milliseconds...`)
219
+ await sleep(options.sleep)
220
+
221
+ return pollResponseFromCache(cache, key, options, retry + 1)
222
+ }
223
+
224
+ /**
225
+ * Given a Cache instance in the adapter dependencies, builds a middleware function that will perform
226
+ * a get from said Cache and return that if found; otherwise it'll continue the middleware chain.
227
+ *
228
+ * @param adapter - an initialized adapter
229
+ * @returns the cache middleware function
230
+ */
231
+ export const buildCacheMiddleware: AdapterMiddlewareBuilder =
232
+ (adapter: InitializedAdapter) => async (req: AdapterRequest, res: FastifyReply) => {
233
+ const response = await (adapter.dependencies.cache as Cache<AdapterResponse>).get(
234
+ req.requestContext.cacheKey,
235
+ )
236
+
237
+ if (response) {
238
+ logger.debug('Found response from cache, sending that')
239
+ if (adapter.config.METRICS_ENABLED && adapter.config.EXPERIMENTAL_METRICS_ENABLED) {
240
+ const label = cacheMetrics.cacheMetricsLabel(
241
+ req.requestContext.cacheKey,
242
+ req.requestContext.meta?.metrics?.feedId || 'N/A',
243
+ adapter.config.CACHE_TYPE,
244
+ )
245
+
246
+ // Record cache staleness and cache get count and value
247
+ const staleness = calculateStaleness(response.maxAge, adapter.config.CACHE_MAX_AGE)
248
+ cacheMetrics.cacheGet(label, response.result, staleness)
249
+ req.requestContext.meta = {
250
+ ...req.requestContext.meta,
251
+ metrics: { ...req.requestContext.meta?.metrics, cacheHit: true },
252
+ }
253
+ }
254
+ return res.send(response)
255
+ }
256
+
257
+ logger.debug('Did not find response in cache, moving to next middleware')
258
+ }
@@ -0,0 +1,73 @@
1
+ import { AdapterResponse, makeLogger } from '../util'
2
+ import { Cache, CacheEntry } from './index'
3
+ import * as cacheMetrics from './metrics'
4
+
5
+ const logger = makeLogger('LocalCache')
6
+
7
+ /**
8
+ * Type for a value stored in a LocalCache entry.
9
+ *
10
+ * @typeParam T - the type for the entry's value
11
+ */
12
+ export interface LocalCacheEntry<T> {
13
+ expirationTimestamp: number
14
+ value: T
15
+ }
16
+
17
+ /**
18
+ * Local implementation of a Cache. It uses a simple js Object, storing entries with both
19
+ * a value and an expiration timestamp. Expired entries are deleted on reads (i.e. no background gc/upkeep).
20
+ *
21
+ * @typeParam T - the type for the entries' values
22
+ */
23
+ export class LocalCache<T = unknown> implements Cache<T> {
24
+ store: Record<string, LocalCacheEntry<T>> = {}
25
+
26
+ async get(key: string): Promise<T | undefined> {
27
+ logger.trace(`Getting key ${key}`)
28
+ const entry = this.store[key]
29
+
30
+ if (!entry) {
31
+ logger.debug(`No entry in local cache for key "${key}", returning undefined`)
32
+ return undefined
33
+ }
34
+
35
+ const expired = entry.expirationTimestamp <= Date.now()
36
+ if (expired) {
37
+ logger.debug('Entry in local cache expired, deleting and returning undefined')
38
+ this.delete(key)
39
+ return undefined
40
+ } else {
41
+ logger.debug('Found valid entry in local cache, returning value')
42
+ return entry.value
43
+ }
44
+ }
45
+
46
+ async delete(key: string): Promise<void> {
47
+ logger.trace(`Deleting key ${key}`)
48
+ delete this.store[key] // Deletes are slower than ignoring or setting null, fyi
49
+ }
50
+
51
+ async set(key: string, value: T, ttl: number): Promise<void> {
52
+ logger.trace(`Setting key ${key} with ttl ${ttl}`)
53
+ this.store[key] = {
54
+ value,
55
+ expirationTimestamp: Date.now() + ttl,
56
+ }
57
+
58
+ // Only record metrics if feed Id is present, otherwise assuming value is not adapter response to record
59
+ const feedId = (value as unknown as AdapterResponse).meta?.metrics?.feedId
60
+ if (feedId) {
61
+ // Record cache set count, max age, and staleness (set to 0 for cache set)
62
+ const label = cacheMetrics.cacheMetricsLabel(key, feedId, cacheMetrics.CacheTypes.Local)
63
+ cacheMetrics.cacheSet(label, ttl)
64
+ }
65
+ }
66
+
67
+ async setMany(entries: CacheEntry<T>[], ttl: number): Promise<void> {
68
+ logger.trace(`Setting a bunch of keys with ttl ${ttl}`)
69
+ for (const { key, value } of entries) {
70
+ this.set(key, value, ttl)
71
+ }
72
+ }
73
+ }