envio 3.0.0-alpha.2 → 3.0.0-alpha.4

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 (74) hide show
  1. package/README.md +2 -2
  2. package/evm.schema.json +44 -34
  3. package/fuel.schema.json +32 -21
  4. package/index.d.ts +4 -1
  5. package/index.js +1 -0
  6. package/package.json +7 -6
  7. package/src/Batch.res.mjs +1 -1
  8. package/src/Benchmark.res +394 -0
  9. package/src/Benchmark.res.mjs +398 -0
  10. package/src/ChainFetcher.res +459 -0
  11. package/src/ChainFetcher.res.mjs +281 -0
  12. package/src/ChainManager.res +179 -0
  13. package/src/ChainManager.res.mjs +139 -0
  14. package/src/Config.res +15 -1
  15. package/src/Config.res.mjs +28 -5
  16. package/src/Ecosystem.res +9 -124
  17. package/src/Ecosystem.res.mjs +19 -160
  18. package/src/Env.res +0 -1
  19. package/src/Env.res.mjs +0 -3
  20. package/src/Envio.gen.ts +9 -1
  21. package/src/Envio.res +12 -9
  22. package/src/EventProcessing.res +476 -0
  23. package/src/EventProcessing.res.mjs +341 -0
  24. package/src/FetchState.res +54 -29
  25. package/src/FetchState.res.mjs +62 -35
  26. package/src/GlobalState.res +1169 -0
  27. package/src/GlobalState.res.mjs +1196 -0
  28. package/src/Internal.res +43 -1
  29. package/src/LoadLayer.res +444 -0
  30. package/src/LoadLayer.res.mjs +296 -0
  31. package/src/LoadLayer.resi +32 -0
  32. package/src/Prometheus.res +8 -8
  33. package/src/Prometheus.res.mjs +10 -10
  34. package/src/ReorgDetection.res +6 -10
  35. package/src/ReorgDetection.res.mjs +6 -6
  36. package/src/Types.ts +1 -1
  37. package/src/UserContext.res +356 -0
  38. package/src/UserContext.res.mjs +238 -0
  39. package/src/Utils.res +15 -0
  40. package/src/Utils.res.mjs +18 -0
  41. package/src/bindings/ClickHouse.res +31 -1
  42. package/src/bindings/ClickHouse.res.mjs +27 -1
  43. package/src/bindings/DateFns.res +71 -0
  44. package/src/bindings/DateFns.res.mjs +22 -0
  45. package/src/bindings/Ethers.res +27 -63
  46. package/src/bindings/Ethers.res.mjs +18 -65
  47. package/src/sources/Evm.res +87 -0
  48. package/src/sources/Evm.res.mjs +105 -0
  49. package/src/sources/EvmChain.res +95 -0
  50. package/src/sources/EvmChain.res.mjs +61 -0
  51. package/src/sources/Fuel.res +19 -34
  52. package/src/sources/Fuel.res.mjs +34 -16
  53. package/src/sources/FuelSDK.res +37 -0
  54. package/src/sources/FuelSDK.res.mjs +29 -0
  55. package/src/sources/HyperFuel.res +2 -2
  56. package/src/sources/HyperFuel.resi +1 -1
  57. package/src/sources/HyperFuelClient.res +2 -2
  58. package/src/sources/HyperFuelSource.res +8 -8
  59. package/src/sources/HyperFuelSource.res.mjs +5 -5
  60. package/src/sources/HyperSyncHeightStream.res +28 -110
  61. package/src/sources/HyperSyncHeightStream.res.mjs +30 -63
  62. package/src/sources/HyperSyncSource.res +16 -18
  63. package/src/sources/HyperSyncSource.res.mjs +25 -25
  64. package/src/sources/Rpc.res +43 -0
  65. package/src/sources/Rpc.res.mjs +31 -0
  66. package/src/sources/RpcSource.res +13 -8
  67. package/src/sources/RpcSource.res.mjs +12 -7
  68. package/src/sources/Source.res +3 -2
  69. package/src/sources/SourceManager.res +183 -108
  70. package/src/sources/SourceManager.res.mjs +162 -99
  71. package/src/sources/SourceManager.resi +4 -5
  72. package/src/sources/Svm.res +59 -0
  73. package/src/sources/Svm.res.mjs +79 -0
  74. package/src/bindings/Ethers.gen.ts +0 -14
@@ -503,10 +503,12 @@ let make = (
503
503
 
504
504
  let mutSuggestedBlockIntervals = Js.Dict.empty()
505
505
 
506
+ let client = Rest.client(url)
507
+
506
508
  let makeTransactionLoader = () =>
507
509
  LazyLoader.make(
508
510
  ~loaderFn=transactionHash =>
509
- provider->Ethers.JsonRpcProvider.getTransaction(~transactionHash),
511
+ Rpc.GetTransactionByHash.route->Rest.fetch(transactionHash, ~client),
510
512
  ~onError=(am, ~exn) => {
511
513
  Logging.error({
512
514
  "err": exn->Utils.prettifyExn,
@@ -560,7 +562,12 @@ let make = (
560
562
  )
561
563
  let getEventTransactionOrThrow = makeThrowingGetEventTransaction(
562
564
  ~getTransactionFields=Ethers.JsonRpcProvider.makeGetTransactionFields(
563
- ~getTransactionByHash=LazyLoader.get(transactionLoader.contents, _),
565
+ ~getTransactionByHash=async transactionHash => {
566
+ switch await transactionLoader.contents->LazyLoader.get(transactionHash) {
567
+ | Some(tx) => tx
568
+ | None => Js.Exn.raiseError(`Transaction not found for hash: ${transactionHash}`)
569
+ }
570
+ },
564
571
  ~lowercaseAddresses,
565
572
  ),
566
573
  )
@@ -601,7 +608,7 @@ let make = (
601
608
  ~toBlock,
602
609
  ~addressesByContractName,
603
610
  ~indexingContracts,
604
- ~currentBlockHeight,
611
+ ~knownHeight,
605
612
  ~partitionId,
606
613
  ~selection: FetchState.selection,
607
614
  ~retry as _,
@@ -621,8 +628,8 @@ let make = (
621
628
 
622
629
  // Always have a toBlock for an RPC worker
623
630
  let toBlock = switch toBlock {
624
- | Some(toBlock) => Pervasives.min(toBlock, currentBlockHeight)
625
- | None => currentBlockHeight
631
+ | Some(toBlock) => Pervasives.min(toBlock, knownHeight)
632
+ | None => knownHeight
626
633
  }
627
634
 
628
635
  let suggestedToBlock = Pervasives.min(fromBlock + suggestedBlockInterval - 1, toBlock)
@@ -874,7 +881,7 @@ let make = (
874
881
  stats: {
875
882
  totalTimeElapsed: totalTimeElapsed,
876
883
  },
877
- currentBlockHeight,
884
+ knownHeight,
878
885
  reorgGuard,
879
886
  fromBlockQueried: fromBlock,
880
887
  }
@@ -902,8 +909,6 @@ let make = (
902
909
  ->Promise.catch(exn => exn->Error->Promise.resolve)
903
910
  }
904
911
 
905
- let client = Rest.client(url)
906
-
907
912
  {
908
913
  name,
909
914
  sourceFor,
@@ -518,9 +518,10 @@ function make(param) {
518
518
  var provider = Ethers.JsonRpcProvider.make(url, chain);
519
519
  var getSelectionConfig = memoGetSelectionConfig(chain);
520
520
  var mutSuggestedBlockIntervals = {};
521
+ var client = Rest.client(url, undefined);
521
522
  var makeTransactionLoader = function () {
522
523
  return LazyLoader.make((function (transactionHash) {
523
- return provider.getTransaction(transactionHash);
524
+ return Rest.$$fetch(Rpc.GetTransactionByHash.route, transactionHash, client);
524
525
  }), (function (am, exn) {
525
526
  Logging.error({
526
527
  err: Utils.prettifyExn(exn),
@@ -559,8 +560,13 @@ function make(param) {
559
560
  var getEventBlockOrThrow = makeThrowingGetEventBlock(function (blockNumber) {
560
561
  return LazyLoader.get(blockLoader.contents, blockNumber);
561
562
  });
562
- var getEventTransactionOrThrow = makeThrowingGetEventTransaction(Ethers.JsonRpcProvider.makeGetTransactionFields((function (__x) {
563
- return LazyLoader.get(transactionLoader.contents, __x);
563
+ var getEventTransactionOrThrow = makeThrowingGetEventTransaction(Ethers.JsonRpcProvider.makeGetTransactionFields((async function (transactionHash) {
564
+ var tx = await LazyLoader.get(transactionLoader.contents, transactionHash);
565
+ if (tx !== undefined) {
566
+ return tx;
567
+ } else {
568
+ return Js_exn.raiseError("Transaction not found for hash: " + transactionHash);
569
+ }
564
570
  }), lowercaseAddresses));
565
571
  var contractNameAbiMapping = {};
566
572
  Belt_Array.forEach(param.contracts, (function (contract) {
@@ -604,11 +610,11 @@ function make(param) {
604
610
  return HyperSyncClient.Decoder.fromSignatures(allEventSignatures);
605
611
  }
606
612
  };
607
- var getItemsOrThrow = async function (fromBlock, toBlock, addressesByContractName, indexingContracts, currentBlockHeight, partitionId, selection, param, param$1) {
613
+ var getItemsOrThrow = async function (fromBlock, toBlock, addressesByContractName, indexingContracts, knownHeight, partitionId, selection, param, param$1) {
608
614
  var startFetchingBatchTimeRef = Hrtime.makeTimer();
609
615
  var maxSuggestedBlockInterval = mutSuggestedBlockIntervals[maxSuggestedBlockIntervalKey];
610
616
  var suggestedBlockInterval = maxSuggestedBlockInterval !== undefined ? maxSuggestedBlockInterval : Belt_Option.getWithDefault(mutSuggestedBlockIntervals[partitionId], syncConfig.initialBlockInterval);
611
- var toBlock$1 = toBlock !== undefined && toBlock < currentBlockHeight ? toBlock : currentBlockHeight;
617
+ var toBlock$1 = toBlock !== undefined && toBlock < knownHeight ? toBlock : knownHeight;
612
618
  var suggestedToBlock = Caml.int_max(Caml.int_min((fromBlock + suggestedBlockInterval | 0) - 1 | 0, toBlock$1), fromBlock);
613
619
  var firstBlockParentPromise = fromBlock > 0 ? LazyLoader.get(blockLoader.contents, fromBlock - 1 | 0).then(function (res) {
614
620
  return res;
@@ -785,7 +791,7 @@ function make(param) {
785
791
  prevRangeLastBlock: reorgGuard_prevRangeLastBlock
786
792
  };
787
793
  return {
788
- currentBlockHeight: currentBlockHeight,
794
+ knownHeight: knownHeight,
789
795
  reorgGuard: reorgGuard,
790
796
  parsedQueueItems: parsedQueueItems,
791
797
  fromBlockQueried: fromBlock,
@@ -819,7 +825,6 @@ function make(param) {
819
825
  });
820
826
  }));
821
827
  };
822
- var client = Rest.client(url, undefined);
823
828
  return {
824
829
  name: name,
825
830
  sourceFor: param.sourceFor,
@@ -11,7 +11,7 @@ type blockRangeFetchStats = {
11
11
  Thes response returned from a block range fetch
12
12
  */
13
13
  type blockRangeFetchResponse = {
14
- currentBlockHeight: int,
14
+ knownHeight: int,
15
15
  reorgGuard: ReorgDetection.reorgGuard,
16
16
  parsedQueueItems: array<Internal.item>,
17
17
  fromBlockQueried: int,
@@ -50,10 +50,11 @@ type t = {
50
50
  ~toBlock: option<int>,
51
51
  ~addressesByContractName: dict<array<Address.t>>,
52
52
  ~indexingContracts: dict<Internal.indexingContract>,
53
- ~currentBlockHeight: int,
53
+ ~knownHeight: int,
54
54
  ~partitionId: string,
55
55
  ~selection: FetchState.selection,
56
56
  ~retry: int,
57
57
  ~logger: Pino.t,
58
58
  ) => promise<blockRangeFetchResponse>,
59
+ createHeightSubscription?: (~onHeight: int => unit) => unit => unit,
59
60
  }
@@ -2,13 +2,21 @@ open Belt
2
2
 
3
3
  type sourceManagerStatus = Idle | WaitingForNewBlock | Querieng
4
4
 
5
+ type sourceState = {
6
+ source: Source.t,
7
+ mutable knownHeight: int,
8
+ mutable unsubscribe: option<unit => unit>,
9
+ mutable pendingHeightResolvers: array<int => unit>,
10
+ mutable disabled: bool,
11
+ }
12
+
5
13
  // Ideally the ChainFetcher name suits this better
6
14
  // But currently the ChainFetcher module is immutable
7
15
  // and handles both processing and fetching.
8
16
  // So this module is to encapsulate the fetching logic only
9
17
  // with a mutable state for easier reasoning and testing.
10
18
  type t = {
11
- sources: Utils.Set.t<Source.t>,
19
+ sourcesState: array<sourceState>,
12
20
  mutable statusStart: Hrtime.timeRef,
13
21
  mutable status: sourceManagerStatus,
14
22
  maxPartitionConcurrency: int,
@@ -63,7 +71,13 @@ let make = (
63
71
  )
64
72
  {
65
73
  maxPartitionConcurrency,
66
- sources: Utils.Set.fromArray(sources),
74
+ sourcesState: sources->Array.map(source => {
75
+ source,
76
+ knownHeight: 0,
77
+ unsubscribe: None,
78
+ pendingHeightResolvers: [],
79
+ disabled: false,
80
+ }),
67
81
  activeSource: initialActiveSource,
68
82
  waitingForNewBlockStateId: None,
69
83
  fetchingPartitionsCount: 0,
@@ -92,7 +106,6 @@ let trackNewStatus = (sourceManager: t, ~newStatus) => {
92
106
  let fetchNext = async (
93
107
  sourceManager: t,
94
108
  ~fetchState: FetchState.t,
95
- ~currentBlockHeight,
96
109
  ~executeQuery,
97
110
  ~waitForNewBlock,
98
111
  ~onNewBlock,
@@ -100,13 +113,14 @@ let fetchNext = async (
100
113
  ) => {
101
114
  let {maxPartitionConcurrency} = sourceManager
102
115
 
103
- switch fetchState->FetchState.getNextQuery(
116
+ let nextQuery = fetchState->FetchState.getNextQuery(
104
117
  ~concurrencyLimit={
105
118
  maxPartitionConcurrency - sourceManager.fetchingPartitionsCount
106
119
  },
107
- ~currentBlockHeight,
108
120
  ~stateId,
109
- ) {
121
+ )
122
+
123
+ switch nextQuery {
110
124
  | ReachedMaxConcurrency
111
125
  | NothingToQuery => ()
112
126
  | WaitingForNewBlock =>
@@ -116,12 +130,12 @@ let fetchNext = async (
116
130
  | None =>
117
131
  sourceManager->trackNewStatus(~newStatus=WaitingForNewBlock)
118
132
  sourceManager.waitingForNewBlockStateId = Some(stateId)
119
- let currentBlockHeight = await waitForNewBlock(~currentBlockHeight)
133
+ let knownHeight = await waitForNewBlock(~knownHeight=fetchState.knownHeight)
120
134
  switch sourceManager.waitingForNewBlockStateId {
121
135
  | Some(waitingStateId) if waitingStateId === stateId => {
122
136
  sourceManager->trackNewStatus(~newStatus=Idle)
123
137
  sourceManager.waitingForNewBlockStateId = None
124
- onNewBlock(~currentBlockHeight)
138
+ onNewBlock(~knownHeight)
125
139
  }
126
140
  | Some(_) // Don't reset it if we are waiting for another state
127
141
  | None => ()
@@ -159,91 +173,144 @@ let fetchNext = async (
159
173
 
160
174
  type status = Active | Stalled | Done
161
175
 
176
+ let disableSource = (sourceState: sourceState) => {
177
+ if !sourceState.disabled {
178
+ sourceState.disabled = true
179
+ switch sourceState.unsubscribe {
180
+ | Some(unsubscribe) => unsubscribe()
181
+ | None => ()
182
+ }
183
+ true
184
+ } else {
185
+ false
186
+ }
187
+ }
188
+
162
189
  let getSourceNewHeight = async (
163
190
  sourceManager,
164
- ~source: Source.t,
165
- ~currentBlockHeight,
191
+ ~sourceState: sourceState,
192
+ ~knownHeight,
166
193
  ~status: ref<status>,
167
194
  ~logger,
168
195
  ) => {
169
- let newHeight = ref(0)
196
+ let source = sourceState.source
197
+ let initialHeight = sourceState.knownHeight
198
+ let newHeight = ref(initialHeight)
170
199
  let retry = ref(0)
171
200
 
172
- while newHeight.contents <= currentBlockHeight && status.contents !== Done {
173
- try {
174
- // Use to detect if the source is taking too long to respond
175
- let endTimer = Prometheus.SourceGetHeightDuration.startTimer({
176
- "source": source.name,
177
- "chainId": source.chain->ChainMap.Chain.toChainId,
201
+ while newHeight.contents <= knownHeight && status.contents !== Done {
202
+ // If subscription exists, wait for next height event
203
+ switch sourceState.unsubscribe {
204
+ | Some(_) =>
205
+ let height = await Promise.make((resolve, _reject) => {
206
+ sourceState.pendingHeightResolvers->Array.push(resolve)
178
207
  })
179
- let height = await source.getHeightOrThrow()
180
- endTimer()
181
-
182
- newHeight := height
183
- if height <= currentBlockHeight {
184
- retry := 0
185
- // Slowdown polling when the chain isn't progressing
186
- let pollingInterval = if status.contents === Stalled {
187
- sourceManager.stalledPollingInterval
188
- } else {
189
- source.pollingInterval
208
+
209
+ // Only accept heights greater than initialHeight
210
+ if height > initialHeight {
211
+ newHeight := height
212
+ }
213
+ | None =>
214
+ // No subscription, use REST polling
215
+ try {
216
+ // Use to detect if the source is taking too long to respond
217
+ let endTimer = Prometheus.SourceGetHeightDuration.startTimer({
218
+ "source": source.name,
219
+ "chainId": source.chain->ChainMap.Chain.toChainId,
220
+ })
221
+ let height = await source.getHeightOrThrow()
222
+ endTimer()
223
+
224
+ newHeight := height
225
+ if height <= knownHeight {
226
+ retry := 0
227
+
228
+ // If createHeightSubscription is available and height hasn't changed,
229
+ // create subscription instead of polling
230
+ switch source.createHeightSubscription {
231
+ | Some(createSubscription) =>
232
+ let unsubscribe = createSubscription(~onHeight=newHeight => {
233
+ sourceState.knownHeight = newHeight
234
+ // Resolve all pending height resolvers
235
+ let resolvers = sourceState.pendingHeightResolvers
236
+ sourceState.pendingHeightResolvers = []
237
+ resolvers->Array.forEach(resolve => resolve(newHeight))
238
+ })
239
+ sourceState.unsubscribe = Some(unsubscribe)
240
+ | None =>
241
+ // Slowdown polling when the chain isn't progressing
242
+ let pollingInterval = if status.contents === Stalled {
243
+ sourceManager.stalledPollingInterval
244
+ } else {
245
+ source.pollingInterval
246
+ }
247
+ await Utils.delay(pollingInterval)
248
+ }
190
249
  }
191
- await Utils.delay(pollingInterval)
250
+ } catch {
251
+ | exn =>
252
+ let retryInterval = sourceManager.getHeightRetryInterval(~retry=retry.contents)
253
+ logger->Logging.childTrace({
254
+ "msg": `Height retrieval from ${source.name} source failed. Retrying in ${retryInterval->Int.toString}ms.`,
255
+ "source": source.name,
256
+ "err": exn->Utils.prettifyExn,
257
+ })
258
+ retry := retry.contents + 1
259
+ await Utils.delay(retryInterval)
192
260
  }
193
- } catch {
194
- | exn =>
195
- let retryInterval = sourceManager.getHeightRetryInterval(~retry=retry.contents)
196
- logger->Logging.childTrace({
197
- "msg": `Height retrieval from ${source.name} source failed. Retrying in ${retryInterval->Int.toString}ms.`,
198
- "source": source.name,
199
- "err": exn->Utils.prettifyExn,
200
- })
201
- retry := retry.contents + 1
202
- await Utils.delay(retryInterval)
203
261
  }
204
262
  }
205
- Prometheus.SourceHeight.set(
206
- ~sourceName=source.name,
207
- ~chainId=source.chain->ChainMap.Chain.toChainId,
208
- ~blockNumber=newHeight.contents,
209
- )
263
+
264
+ // Update Prometheus only if height increased
265
+ if newHeight.contents > initialHeight {
266
+ Prometheus.SourceHeight.set(
267
+ ~sourceName=source.name,
268
+ ~chainId=source.chain->ChainMap.Chain.toChainId,
269
+ ~blockNumber=newHeight.contents,
270
+ )
271
+ }
272
+
210
273
  newHeight.contents
211
274
  }
212
275
 
213
276
  // Polls for a block height greater than the given block number to ensure a new block is available for indexing.
214
- let waitForNewBlock = async (sourceManager: t, ~currentBlockHeight) => {
215
- let {sources} = sourceManager
277
+ let waitForNewBlock = async (sourceManager: t, ~knownHeight) => {
278
+ let {sourcesState} = sourceManager
216
279
 
217
280
  let logger = Logging.createChild(
218
281
  ~params={
219
282
  "chainId": sourceManager.activeSource.chain->ChainMap.Chain.toChainId,
220
- "currentBlockHeight": currentBlockHeight,
283
+ "knownHeight": knownHeight,
221
284
  },
222
285
  )
223
286
  logger->Logging.childTrace("Initiating check for new blocks.")
224
287
 
225
288
  // Only include Live sources if we've actually synced some blocks
226
- // (currentBlockHeight > 0 means we've fetched at least one batch)
289
+ // (knownHeight > 0 means we've fetched at least one batch)
227
290
  // This prevents Live RPC from winning the initial height race and
228
291
  // becoming activeSource, which would bypass HyperSync's smart block detection
229
- let isInitialHeightFetch = currentBlockHeight === 0
292
+ let isInitialHeightFetch = knownHeight === 0
230
293
 
231
294
  let syncSources = []
232
295
  let fallbackSources = []
233
- sources->Utils.Set.forEach(source => {
234
- if (
296
+ sourcesState->Array.forEach(sourceState => {
297
+ let source = sourceState.source
298
+ if sourceState.disabled {
299
+ // Skip disabled sources
300
+ ()
301
+ } else if (
235
302
  source.sourceFor === Sync ||
236
- // Include Live sources only after initial sync has started
237
- // Live sources are optimized for real-time indexing with lower latency
238
- (source.sourceFor === Live && !isInitialHeightFetch) ||
239
- // Even if the active source is a fallback, still include
240
- // it to the list. So we don't wait for a timeout again
241
- // if all main sync sources are still not valid
242
- source === sourceManager.activeSource
303
+ // Include Live sources only after initial sync has started
304
+ // Live sources are optimized for real-time indexing with lower latency
305
+ source.sourceFor === Live && !isInitialHeightFetch ||
306
+ // Even if the active source is a fallback, still include
307
+ // it to the list. So we don't wait for a timeout again
308
+ // if all main sync sources are still not valid
309
+ source === sourceManager.activeSource
243
310
  ) {
244
- syncSources->Array.push(source)
311
+ syncSources->Array.push(sourceState)
245
312
  } else {
246
- fallbackSources->Array.push(source)
313
+ fallbackSources->Array.push(sourceState)
247
314
  }
248
315
  })
249
316
 
@@ -251,10 +318,10 @@ let waitForNewBlock = async (sourceManager: t, ~currentBlockHeight) => {
251
318
 
252
319
  let (source, newBlockHeight) = await Promise.race(
253
320
  syncSources
254
- ->Array.map(async source => {
321
+ ->Array.map(async sourceState => {
255
322
  (
256
- source,
257
- await sourceManager->getSourceNewHeight(~source, ~currentBlockHeight, ~status, ~logger),
323
+ sourceState.source,
324
+ await sourceManager->getSourceNewHeight(~sourceState, ~knownHeight, ~status, ~logger),
258
325
  )
259
326
  })
260
327
  ->Array.concat([
@@ -278,15 +345,10 @@ let waitForNewBlock = async (sourceManager: t, ~currentBlockHeight) => {
278
345
  // Promise.race will be forever pending if fallbackSources is empty
279
346
  // which is good for this use case
280
347
  Promise.race(
281
- fallbackSources->Array.map(async source => {
348
+ fallbackSources->Array.map(async sourceState => {
282
349
  (
283
- source,
284
- await sourceManager->getSourceNewHeight(
285
- ~source,
286
- ~currentBlockHeight,
287
- ~status,
288
- ~logger,
289
- ),
350
+ sourceState.source,
351
+ await sourceManager->getSourceNewHeight(~sourceState, ~knownHeight, ~status, ~logger),
290
352
  )
291
353
  }),
292
354
  )
@@ -309,11 +371,11 @@ let waitForNewBlock = async (sourceManager: t, ~currentBlockHeight) => {
309
371
  newBlockHeight
310
372
  }
311
373
 
312
- let getNextSyncSource = (
374
+ let getNextSyncSourceState = (
313
375
  sourceManager,
314
376
  // This is needed to include the Fallback source to rotation
315
- ~initialSource,
316
- ~currentSource,
377
+ ~initialSourceState: sourceState,
378
+ ~currentSourceState: sourceState,
317
379
  // After multiple failures start returning fallback sources as well
318
380
  // But don't try it when main sync sources fail because of invalid configuration
319
381
  // note: The logic might be changed in the future
@@ -324,18 +386,23 @@ let getNextSyncSource = (
324
386
 
325
387
  let hasActive = ref(false)
326
388
 
327
- sourceManager.sources->Utils.Set.forEach(source => {
328
- if source === currentSource {
389
+ sourceManager.sourcesState->Array.forEach(sourceState => {
390
+ let source = sourceState.source
391
+
392
+ // Skip disabled sources
393
+ if sourceState.disabled {
394
+ ()
395
+ } else if sourceState === currentSourceState {
329
396
  hasActive := true
330
397
  } else if (
331
398
  switch source.sourceFor {
332
399
  | Sync => true
333
400
  // Live sources should NOT be used for historical sync rotation
334
401
  // They are only meant for real-time indexing once synced
335
- | Live | Fallback => attemptFallbacks || source === initialSource
402
+ | Live | Fallback => attemptFallbacks || sourceState === initialSourceState
336
403
  }
337
404
  ) {
338
- (hasActive.contents ? after : before)->Array.push(source)
405
+ (hasActive.contents ? after : before)->Array.push(sourceState)
339
406
  }
340
407
  })
341
408
 
@@ -344,12 +411,12 @@ let getNextSyncSource = (
344
411
  | None =>
345
412
  switch before->Array.get(0) {
346
413
  | Some(s) => s
347
- | None => currentSource
414
+ | None => currentSourceState
348
415
  }
349
416
  }
350
417
  }
351
418
 
352
- let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~currentBlockHeight) => {
419
+ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~knownHeight) => {
353
420
  let toBlockRef = ref(
354
421
  switch query.target {
355
422
  | Head => None
@@ -360,12 +427,16 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~currentBl
360
427
  )
361
428
  let responseRef = ref(None)
362
429
  let retryRef = ref(0)
363
- let initialSource = sourceManager.activeSource
364
- let sourceRef = ref(initialSource)
430
+ let initialSourceState =
431
+ sourceManager.sourcesState
432
+ ->Js.Array2.find(s => s.source === sourceManager.activeSource)
433
+ ->Option.getUnsafe
434
+ let sourceStateRef = ref(initialSourceState)
365
435
  let shouldUpdateActiveSource = ref(false)
366
436
 
367
437
  while responseRef.contents->Option.isNone {
368
- let source = sourceRef.contents
438
+ let sourceState = sourceStateRef.contents
439
+ let source = sourceState.source
369
440
  let toBlock = toBlockRef.contents
370
441
  let retry = retryRef.contents
371
442
 
@@ -389,7 +460,7 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~currentBl
389
460
  ~addressesByContractName=query.addressesByContractName,
390
461
  ~indexingContracts=query.indexingContracts,
391
462
  ~partitionId=query.partitionId,
392
- ~currentBlockHeight,
463
+ ~knownHeight,
393
464
  ~selection=query.selection,
394
465
  ~retry,
395
466
  ~logger,
@@ -406,15 +477,19 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~currentBl
406
477
  switch error {
407
478
  | UnsupportedSelection(_)
408
479
  | FailedGettingFieldSelection(_) => {
409
- let nextSource = sourceManager->getNextSyncSource(~initialSource, ~currentSource=source)
480
+ let nextSourceState =
481
+ sourceManager->getNextSyncSourceState(
482
+ ~initialSourceState,
483
+ ~currentSourceState=sourceState,
484
+ )
410
485
 
411
- // These errors are impossible to recover, so we delete the source
412
- // from sourceManager so it's not attempted anymore
413
- let notAlreadyDeleted = sourceManager.sources->Utils.Set.delete(source)
486
+ // These errors are impossible to recover, so we disable the source
487
+ // so it's not attempted anymore
488
+ let notAlreadyDisabled = disableSource(sourceState)
414
489
 
415
490
  // In case there are multiple partitions
416
491
  // failing at the same time. Log only once
417
- if notAlreadyDeleted {
492
+ if notAlreadyDisabled {
418
493
  switch error {
419
494
  | UnsupportedSelection({message}) => logger->Logging.childError(message)
420
495
  | FailedGettingFieldSelection({exn, message, blockNumber, logIndex}) =>
@@ -428,7 +503,7 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~currentBl
428
503
  }
429
504
  }
430
505
 
431
- if nextSource === source {
506
+ if nextSourceState === sourceState {
432
507
  %raw(`null`)->ErrorHandling.mkLogAndRaise(
433
508
  ~logger,
434
509
  ~msg="The indexer doesn't have data-sources which can continue fetching. Please, check the error logs or reach out to the Envio team.",
@@ -436,9 +511,9 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~currentBl
436
511
  } else {
437
512
  logger->Logging.childInfo({
438
513
  "msg": "Switching to another data-source",
439
- "source": nextSource.name,
514
+ "source": nextSourceState.source.name,
440
515
  })
441
- sourceRef := nextSource
516
+ sourceStateRef := nextSourceState
442
517
  shouldUpdateActiveSource := true
443
518
  retryRef := 0
444
519
  }
@@ -452,14 +527,14 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~currentBl
452
527
  toBlockRef := Some(toBlock)
453
528
  retryRef := 0
454
529
  | FailedGettingItems({exn, attemptedToBlock, retry: ImpossibleForTheQuery({message})}) =>
455
- let nextSource =
456
- sourceManager->getNextSyncSource(
457
- ~initialSource,
458
- ~currentSource=source,
530
+ let nextSourceState =
531
+ sourceManager->getNextSyncSourceState(
532
+ ~initialSourceState,
533
+ ~currentSourceState=sourceState,
459
534
  ~attemptFallbacks=true,
460
535
  )
461
536
 
462
- let hasAnotherSource = nextSource !== initialSource
537
+ let hasAnotherSource = nextSourceState !== initialSourceState
463
538
 
464
539
  logger->Logging.childWarn({
465
540
  "msg": message ++ (hasAnotherSource ? " - Attempting to another source" : ""),
@@ -473,7 +548,7 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~currentBl
473
548
  ~msg="The indexer doesn't have data-sources which can continue fetching. Please, check the error logs or reach out to the Envio team.",
474
549
  )
475
550
  } else {
476
- sourceRef := nextSource
551
+ sourceStateRef := nextSourceState
477
552
  shouldUpdateActiveSource := false
478
553
  retryRef := 0
479
554
  }
@@ -488,19 +563,19 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~currentBl
488
563
  // just keep the value high
489
564
  let attemptFallbacks = retry >= 10
490
565
 
491
- let nextSource = switch retry {
566
+ let nextSourceState = switch retry {
492
567
  // Don't attempt a switch on first two failure
493
- | 0 | 1 => source
568
+ | 0 | 1 => sourceState
494
569
  | _ =>
495
570
  // Then try to switch every second failure
496
571
  if retry->mod(2) === 0 {
497
- sourceManager->getNextSyncSource(
498
- ~initialSource,
572
+ sourceManager->getNextSyncSourceState(
573
+ ~initialSourceState,
499
574
  ~attemptFallbacks,
500
- ~currentSource=source,
575
+ ~currentSourceState=sourceState,
501
576
  )
502
577
  } else {
503
- source
578
+ sourceState
504
579
  }
505
580
  }
506
581
 
@@ -514,13 +589,13 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~currentBl
514
589
  "err": exn->Utils.prettifyExn,
515
590
  })
516
591
 
517
- let shouldSwitch = nextSource !== source
592
+ let shouldSwitch = nextSourceState !== sourceState
518
593
  if shouldSwitch {
519
594
  logger->Logging.childInfo({
520
595
  "msg": "Switching to another data-source",
521
- "source": nextSource.name,
596
+ "source": nextSourceState.source.name,
522
597
  })
523
- sourceRef := nextSource
598
+ sourceStateRef := nextSourceState
524
599
  shouldUpdateActiveSource := true
525
600
  } else {
526
601
  await Utils.delay(Pervasives.min(backoffMillis, 60_000))
@@ -534,7 +609,7 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~currentBl
534
609
  }
535
610
 
536
611
  if shouldUpdateActiveSource.contents {
537
- sourceManager.activeSource = sourceRef.contents
612
+ sourceManager.activeSource = sourceStateRef.contents.source
538
613
  }
539
614
 
540
615
  responseRef.contents->Option.getUnsafe