envio 2.32.3 → 2.32.7

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.
@@ -19,6 +19,7 @@ var HyperSyncClient = require("./HyperSyncClient.res.js");
19
19
  var Caml_splice_call = require("rescript/lib/js/caml_splice_call.js");
20
20
  var HyperSyncJsonApi = require("./HyperSyncJsonApi.res.js");
21
21
  var Caml_js_exceptions = require("rescript/lib/js/caml_js_exceptions.js");
22
+ var HyperSyncHeightStream = require("./HyperSyncHeightStream.res.js");
22
23
 
23
24
  function getSelectionConfig(selection, chain) {
24
25
  var nonOptionalBlockFieldNames = new Set();
@@ -52,7 +53,13 @@ function getSelectionConfig(selection, chain) {
52
53
  Caml_splice_call.spliceObjApply(noAddressesTopicSelections, "push", [tmp]);
53
54
  }));
54
55
  var fieldSelection_block = Array.from(capitalizedBlockFields);
55
- var fieldSelection_transaction = Array.from(capitalizedTransactionFields);
56
+ var fieldSelection_transaction = Belt_Array.map(Array.from(capitalizedTransactionFields), (function (field) {
57
+ if (field !== "Kind") {
58
+ return field;
59
+ } else {
60
+ return "Type";
61
+ }
62
+ }));
56
63
  var fieldSelection_log = [
57
64
  "Address",
58
65
  "Data",
@@ -132,7 +139,7 @@ function make(param) {
132
139
  var chain = param.chain;
133
140
  var getSelectionConfig = memoGetSelectionConfig(chain);
134
141
  var apiToken = Belt_Option.getWithDefault(param.apiToken, "3dc856dd-b0ea-494f-b27e-017b8b6b7e07");
135
- var client = HyperSyncClient.make(endpointUrl, apiToken, param.clientTimeoutMillis, param.clientMaxRetries, !lowercaseAddresses);
142
+ var client = HyperSyncClient.make(endpointUrl, apiToken, param.clientTimeoutMillis, param.clientMaxRetries, !lowercaseAddresses, param.serializationFormat, param.enableQueryCaching, undefined, undefined, undefined);
136
143
  var hscDecoder = {
137
144
  contents: undefined
138
145
  };
@@ -407,7 +414,10 @@ function make(param) {
407
414
  return Js_exn.raiseError(height);
408
415
  }
409
416
  }),
410
- getItemsOrThrow: getItemsOrThrow
417
+ getItemsOrThrow: getItemsOrThrow,
418
+ createHeightSubscription: (function (onHeight) {
419
+ return HyperSyncHeightStream.subscribe(endpointUrl, apiToken, onHeight);
420
+ })
411
421
  };
412
422
  }
413
423
 
@@ -56,4 +56,5 @@ type t = {
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,
@@ -100,13 +114,15 @@ let fetchNext = async (
100
114
  ) => {
101
115
  let {maxPartitionConcurrency} = sourceManager
102
116
 
103
- switch fetchState->FetchState.getNextQuery(
117
+ let nextQuery = fetchState->FetchState.getNextQuery(
104
118
  ~concurrencyLimit={
105
119
  maxPartitionConcurrency - sourceManager.fetchingPartitionsCount
106
120
  },
107
121
  ~currentBlockHeight,
108
122
  ~stateId,
109
- ) {
123
+ )
124
+
125
+ switch nextQuery {
110
126
  | ReachedMaxConcurrency
111
127
  | NothingToQuery => ()
112
128
  | WaitingForNewBlock =>
@@ -116,12 +132,12 @@ let fetchNext = async (
116
132
  | None =>
117
133
  sourceManager->trackNewStatus(~newStatus=WaitingForNewBlock)
118
134
  sourceManager.waitingForNewBlockStateId = Some(stateId)
119
- let currentBlockHeight = await waitForNewBlock(~currentBlockHeight)
135
+ let knownHeight = await waitForNewBlock(~knownHeight=currentBlockHeight)
120
136
  switch sourceManager.waitingForNewBlockStateId {
121
137
  | Some(waitingStateId) if waitingStateId === stateId => {
122
138
  sourceManager->trackNewStatus(~newStatus=Idle)
123
139
  sourceManager.waitingForNewBlockStateId = None
124
- onNewBlock(~currentBlockHeight)
140
+ onNewBlock(~knownHeight)
125
141
  }
126
142
  | Some(_) // Don't reset it if we are waiting for another state
127
143
  | None => ()
@@ -159,82 +175,135 @@ let fetchNext = async (
159
175
 
160
176
  type status = Active | Stalled | Done
161
177
 
178
+ let disableSource = (sourceState: sourceState) => {
179
+ if !sourceState.disabled {
180
+ sourceState.disabled = true
181
+ switch sourceState.unsubscribe {
182
+ | Some(unsubscribe) => unsubscribe()
183
+ | None => ()
184
+ }
185
+ true
186
+ } else {
187
+ false
188
+ }
189
+ }
190
+
162
191
  let getSourceNewHeight = async (
163
192
  sourceManager,
164
- ~source: Source.t,
165
- ~currentBlockHeight,
193
+ ~sourceState: sourceState,
194
+ ~knownHeight,
166
195
  ~status: ref<status>,
167
196
  ~logger,
168
197
  ) => {
169
- let newHeight = ref(0)
198
+ let source = sourceState.source
199
+ let initialHeight = sourceState.knownHeight
200
+ let newHeight = ref(initialHeight)
170
201
  let retry = ref(0)
171
202
 
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,
203
+ while newHeight.contents <= knownHeight && status.contents !== Done {
204
+ // If subscription exists, wait for next height event
205
+ switch sourceState.unsubscribe {
206
+ | Some(_) =>
207
+ let height = await Promise.make((resolve, _reject) => {
208
+ sourceState.pendingHeightResolvers->Array.push(resolve)
178
209
  })
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
210
+
211
+ // Only accept heights greater than initialHeight
212
+ if height > initialHeight {
213
+ newHeight := height
214
+ }
215
+ | None =>
216
+ // No subscription, use REST polling
217
+ try {
218
+ // Use to detect if the source is taking too long to respond
219
+ let endTimer = Prometheus.SourceGetHeightDuration.startTimer({
220
+ "source": source.name,
221
+ "chainId": source.chain->ChainMap.Chain.toChainId,
222
+ })
223
+ let height = await source.getHeightOrThrow()
224
+ endTimer()
225
+
226
+ newHeight := height
227
+ if height <= knownHeight {
228
+ retry := 0
229
+
230
+ // If createHeightSubscription is available and height hasn't changed,
231
+ // create subscription instead of polling
232
+ switch source.createHeightSubscription {
233
+ | Some(createSubscription) =>
234
+ let unsubscribe = createSubscription(~onHeight=newHeight => {
235
+ sourceState.knownHeight = newHeight
236
+ // Resolve all pending height resolvers
237
+ let resolvers = sourceState.pendingHeightResolvers
238
+ sourceState.pendingHeightResolvers = []
239
+ resolvers->Array.forEach(resolve => resolve(newHeight))
240
+ })
241
+ sourceState.unsubscribe = Some(unsubscribe)
242
+ | None =>
243
+ // Slowdown polling when the chain isn't progressing
244
+ let pollingInterval = if status.contents === Stalled {
245
+ sourceManager.stalledPollingInterval
246
+ } else {
247
+ source.pollingInterval
248
+ }
249
+ await Utils.delay(pollingInterval)
250
+ }
190
251
  }
191
- await Utils.delay(pollingInterval)
252
+ } catch {
253
+ | exn =>
254
+ let retryInterval = sourceManager.getHeightRetryInterval(~retry=retry.contents)
255
+ logger->Logging.childTrace({
256
+ "msg": `Height retrieval from ${source.name} source failed. Retrying in ${retryInterval->Int.toString}ms.`,
257
+ "source": source.name,
258
+ "err": exn->Utils.prettifyExn,
259
+ })
260
+ retry := retry.contents + 1
261
+ await Utils.delay(retryInterval)
192
262
  }
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
263
  }
204
264
  }
205
- Prometheus.SourceHeight.set(
206
- ~sourceName=source.name,
207
- ~chainId=source.chain->ChainMap.Chain.toChainId,
208
- ~blockNumber=newHeight.contents,
209
- )
265
+
266
+ // Update Prometheus only if height increased
267
+ if newHeight.contents > initialHeight {
268
+ Prometheus.SourceHeight.set(
269
+ ~sourceName=source.name,
270
+ ~chainId=source.chain->ChainMap.Chain.toChainId,
271
+ ~blockNumber=newHeight.contents,
272
+ )
273
+ }
274
+
210
275
  newHeight.contents
211
276
  }
212
277
 
213
278
  // 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
279
+ let waitForNewBlock = async (sourceManager: t, ~knownHeight) => {
280
+ let {sourcesState} = sourceManager
216
281
 
217
282
  let logger = Logging.createChild(
218
283
  ~params={
219
284
  "chainId": sourceManager.activeSource.chain->ChainMap.Chain.toChainId,
220
- "currentBlockHeight": currentBlockHeight,
285
+ "knownHeight": knownHeight,
221
286
  },
222
287
  )
223
288
  logger->Logging.childTrace("Initiating check for new blocks.")
224
289
 
225
290
  let syncSources = []
226
291
  let fallbackSources = []
227
- sources->Utils.Set.forEach(source => {
228
- if (
292
+ sourcesState->Array.forEach(sourceState => {
293
+ let source = sourceState.source
294
+ if sourceState.disabled {
295
+ // Skip disabled sources
296
+ ()
297
+ } else if (
229
298
  source.sourceFor === Sync ||
230
299
  // Even if the active source is a fallback, still include
231
300
  // it to the list. So we don't wait for a timeout again
232
301
  // if all main sync sources are still not valid
233
302
  source === sourceManager.activeSource
234
303
  ) {
235
- syncSources->Array.push(source)
304
+ syncSources->Array.push(sourceState)
236
305
  } else {
237
- fallbackSources->Array.push(source)
306
+ fallbackSources->Array.push(sourceState)
238
307
  }
239
308
  })
240
309
 
@@ -242,10 +311,10 @@ let waitForNewBlock = async (sourceManager: t, ~currentBlockHeight) => {
242
311
 
243
312
  let (source, newBlockHeight) = await Promise.race(
244
313
  syncSources
245
- ->Array.map(async source => {
314
+ ->Array.map(async sourceState => {
246
315
  (
247
- source,
248
- await sourceManager->getSourceNewHeight(~source, ~currentBlockHeight, ~status, ~logger),
316
+ sourceState.source,
317
+ await sourceManager->getSourceNewHeight(~sourceState, ~knownHeight, ~status, ~logger),
249
318
  )
250
319
  })
251
320
  ->Array.concat([
@@ -269,15 +338,10 @@ let waitForNewBlock = async (sourceManager: t, ~currentBlockHeight) => {
269
338
  // Promise.race will be forever pending if fallbackSources is empty
270
339
  // which is good for this use case
271
340
  Promise.race(
272
- fallbackSources->Array.map(async source => {
341
+ fallbackSources->Array.map(async sourceState => {
273
342
  (
274
- source,
275
- await sourceManager->getSourceNewHeight(
276
- ~source,
277
- ~currentBlockHeight,
278
- ~status,
279
- ~logger,
280
- ),
343
+ sourceState.source,
344
+ await sourceManager->getSourceNewHeight(~sourceState, ~knownHeight, ~status, ~logger),
281
345
  )
282
346
  }),
283
347
  )
@@ -300,11 +364,11 @@ let waitForNewBlock = async (sourceManager: t, ~currentBlockHeight) => {
300
364
  newBlockHeight
301
365
  }
302
366
 
303
- let getNextSyncSource = (
367
+ let getNextSyncSourceState = (
304
368
  sourceManager,
305
369
  // This is needed to include the Fallback source to rotation
306
- ~initialSource,
307
- ~currentSource,
370
+ ~initialSourceState: sourceState,
371
+ ~currentSourceState: sourceState,
308
372
  // After multiple failures start returning fallback sources as well
309
373
  // But don't try it when main sync sources fail because of invalid configuration
310
374
  // note: The logic might be changed in the future
@@ -315,16 +379,21 @@ let getNextSyncSource = (
315
379
 
316
380
  let hasActive = ref(false)
317
381
 
318
- sourceManager.sources->Utils.Set.forEach(source => {
319
- if source === currentSource {
382
+ sourceManager.sourcesState->Array.forEach(sourceState => {
383
+ let source = sourceState.source
384
+
385
+ // Skip disabled sources
386
+ if sourceState.disabled {
387
+ ()
388
+ } else if sourceState === currentSourceState {
320
389
  hasActive := true
321
390
  } else if (
322
391
  switch source.sourceFor {
323
392
  | Sync => true
324
- | Fallback => attemptFallbacks || source === initialSource
393
+ | Fallback => attemptFallbacks || sourceState === initialSourceState
325
394
  }
326
395
  ) {
327
- (hasActive.contents ? after : before)->Array.push(source)
396
+ (hasActive.contents ? after : before)->Array.push(sourceState)
328
397
  }
329
398
  })
330
399
 
@@ -333,7 +402,7 @@ let getNextSyncSource = (
333
402
  | None =>
334
403
  switch before->Array.get(0) {
335
404
  | Some(s) => s
336
- | None => currentSource
405
+ | None => currentSourceState
337
406
  }
338
407
  }
339
408
  }
@@ -349,12 +418,16 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~currentBl
349
418
  )
350
419
  let responseRef = ref(None)
351
420
  let retryRef = ref(0)
352
- let initialSource = sourceManager.activeSource
353
- let sourceRef = ref(initialSource)
421
+ let initialSourceState =
422
+ sourceManager.sourcesState
423
+ ->Js.Array2.find(s => s.source === sourceManager.activeSource)
424
+ ->Option.getUnsafe
425
+ let sourceStateRef = ref(initialSourceState)
354
426
  let shouldUpdateActiveSource = ref(false)
355
427
 
356
428
  while responseRef.contents->Option.isNone {
357
- let source = sourceRef.contents
429
+ let sourceState = sourceStateRef.contents
430
+ let source = sourceState.source
358
431
  let toBlock = toBlockRef.contents
359
432
  let retry = retryRef.contents
360
433
 
@@ -395,15 +468,19 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~currentBl
395
468
  switch error {
396
469
  | UnsupportedSelection(_)
397
470
  | FailedGettingFieldSelection(_) => {
398
- let nextSource = sourceManager->getNextSyncSource(~initialSource, ~currentSource=source)
471
+ let nextSourceState =
472
+ sourceManager->getNextSyncSourceState(
473
+ ~initialSourceState,
474
+ ~currentSourceState=sourceState,
475
+ )
399
476
 
400
- // These errors are impossible to recover, so we delete the source
401
- // from sourceManager so it's not attempted anymore
402
- let notAlreadyDeleted = sourceManager.sources->Utils.Set.delete(source)
477
+ // These errors are impossible to recover, so we disable the source
478
+ // so it's not attempted anymore
479
+ let notAlreadyDisabled = disableSource(sourceState)
403
480
 
404
481
  // In case there are multiple partitions
405
482
  // failing at the same time. Log only once
406
- if notAlreadyDeleted {
483
+ if notAlreadyDisabled {
407
484
  switch error {
408
485
  | UnsupportedSelection({message}) => logger->Logging.childError(message)
409
486
  | FailedGettingFieldSelection({exn, message, blockNumber, logIndex}) =>
@@ -417,7 +494,7 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~currentBl
417
494
  }
418
495
  }
419
496
 
420
- if nextSource === source {
497
+ if nextSourceState === sourceState {
421
498
  %raw(`null`)->ErrorHandling.mkLogAndRaise(
422
499
  ~logger,
423
500
  ~msg="The indexer doesn't have data-sources which can continue fetching. Please, check the error logs or reach out to the Envio team.",
@@ -425,9 +502,9 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~currentBl
425
502
  } else {
426
503
  logger->Logging.childInfo({
427
504
  "msg": "Switching to another data-source",
428
- "source": nextSource.name,
505
+ "source": nextSourceState.source.name,
429
506
  })
430
- sourceRef := nextSource
507
+ sourceStateRef := nextSourceState
431
508
  shouldUpdateActiveSource := true
432
509
  retryRef := 0
433
510
  }
@@ -441,14 +518,14 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~currentBl
441
518
  toBlockRef := Some(toBlock)
442
519
  retryRef := 0
443
520
  | FailedGettingItems({exn, attemptedToBlock, retry: ImpossibleForTheQuery({message})}) =>
444
- let nextSource =
445
- sourceManager->getNextSyncSource(
446
- ~initialSource,
447
- ~currentSource=source,
521
+ let nextSourceState =
522
+ sourceManager->getNextSyncSourceState(
523
+ ~initialSourceState,
524
+ ~currentSourceState=sourceState,
448
525
  ~attemptFallbacks=true,
449
526
  )
450
527
 
451
- let hasAnotherSource = nextSource !== initialSource
528
+ let hasAnotherSource = nextSourceState !== initialSourceState
452
529
 
453
530
  logger->Logging.childWarn({
454
531
  "msg": message ++ (hasAnotherSource ? " - Attempting to another source" : ""),
@@ -462,7 +539,7 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~currentBl
462
539
  ~msg="The indexer doesn't have data-sources which can continue fetching. Please, check the error logs or reach out to the Envio team.",
463
540
  )
464
541
  } else {
465
- sourceRef := nextSource
542
+ sourceStateRef := nextSourceState
466
543
  shouldUpdateActiveSource := false
467
544
  retryRef := 0
468
545
  }
@@ -477,19 +554,19 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~currentBl
477
554
  // just keep the value high
478
555
  let attemptFallbacks = retry >= 10
479
556
 
480
- let nextSource = switch retry {
557
+ let nextSourceState = switch retry {
481
558
  // Don't attempt a switch on first two failure
482
- | 0 | 1 => source
559
+ | 0 | 1 => sourceState
483
560
  | _ =>
484
561
  // Then try to switch every second failure
485
562
  if retry->mod(2) === 0 {
486
- sourceManager->getNextSyncSource(
487
- ~initialSource,
563
+ sourceManager->getNextSyncSourceState(
564
+ ~initialSourceState,
488
565
  ~attemptFallbacks,
489
- ~currentSource=source,
566
+ ~currentSourceState=sourceState,
490
567
  )
491
568
  } else {
492
- source
569
+ sourceState
493
570
  }
494
571
  }
495
572
 
@@ -503,13 +580,13 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~currentBl
503
580
  "err": exn->Utils.prettifyExn,
504
581
  })
505
582
 
506
- let shouldSwitch = nextSource !== source
583
+ let shouldSwitch = nextSourceState !== sourceState
507
584
  if shouldSwitch {
508
585
  logger->Logging.childInfo({
509
586
  "msg": "Switching to another data-source",
510
- "source": nextSource.name,
587
+ "source": nextSourceState.source.name,
511
588
  })
512
- sourceRef := nextSource
589
+ sourceStateRef := nextSourceState
513
590
  shouldUpdateActiveSource := true
514
591
  } else {
515
592
  await Utils.delay(Pervasives.min(backoffMillis, 60_000))
@@ -523,7 +600,7 @@ let executeQuery = async (sourceManager: t, ~query: FetchState.query, ~currentBl
523
600
  }
524
601
 
525
602
  if shouldUpdateActiveSource.contents {
526
- sourceManager.activeSource = sourceRef.contents
603
+ sourceManager.activeSource = sourceStateRef.contents.source
527
604
  }
528
605
 
529
606
  responseRef.contents->Option.getUnsafe