envio 3.1.0-rc.0 → 3.1.0-rc.1

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 (37) hide show
  1. package/package.json +6 -6
  2. package/src/ChainFetcher.res +8 -23
  3. package/src/ChainFetcher.res.mjs +2 -10
  4. package/src/Env.res +0 -7
  5. package/src/Env.res.mjs +0 -3
  6. package/src/FetchState.res +1 -4
  7. package/src/FetchState.res.mjs +1 -2
  8. package/src/GlobalState.res +3 -2
  9. package/src/GlobalState.res.mjs +2 -2
  10. package/src/InMemoryStore.res +31 -7
  11. package/src/InMemoryStore.res.mjs +18 -4
  12. package/src/InMemoryTable.res +20 -4
  13. package/src/InMemoryTable.res.mjs +30 -2
  14. package/src/LogSelection.res +15 -19
  15. package/src/LogSelection.res.mjs +5 -6
  16. package/src/sources/EnvioApiClient.res +15 -0
  17. package/src/sources/EnvioApiClient.res.mjs +24 -0
  18. package/src/sources/EvmChain.res +0 -1
  19. package/src/sources/EvmChain.res.mjs +0 -1
  20. package/src/sources/HyperFuelSource.res +1 -1
  21. package/src/sources/HyperFuelSource.res.mjs +2 -1
  22. package/src/sources/HyperSync.res +20 -1
  23. package/src/sources/HyperSync.res.mjs +26 -1
  24. package/src/sources/HyperSyncClient.res +3 -2
  25. package/src/sources/HyperSyncClient.res.mjs +2 -2
  26. package/src/sources/HyperSyncSource.res +18 -19
  27. package/src/sources/HyperSyncSource.res.mjs +40 -14
  28. package/src/sources/Source.res +2 -0
  29. package/src/sources/Source.res.mjs +3 -0
  30. package/src/sources/SourceManager.res +168 -8
  31. package/src/sources/SourceManager.res.mjs +131 -3
  32. package/src/sources/SourceManager.resi +17 -0
  33. package/src/tui/Tui.res +44 -6
  34. package/src/tui/Tui.res.mjs +56 -8
  35. package/src/tui/components/TuiData.res +3 -0
  36. package/src/sources/HyperSyncJsonApi.res +0 -390
  37. package/src/sources/HyperSyncJsonApi.res.mjs +0 -237
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "envio",
3
- "version": "3.1.0-rc.0",
3
+ "version": "3.1.0-rc.1",
4
4
  "type": "module",
5
5
  "description": "A latency and sync speed optimized, developer friendly blockchain data indexer.",
6
6
  "bin": "./bin.mjs",
@@ -70,10 +70,10 @@
70
70
  "tsx": "4.21.0"
71
71
  },
72
72
  "optionalDependencies": {
73
- "envio-linux-x64": "3.1.0-rc.0",
74
- "envio-linux-x64-musl": "3.1.0-rc.0",
75
- "envio-linux-arm64": "3.1.0-rc.0",
76
- "envio-darwin-x64": "3.1.0-rc.0",
77
- "envio-darwin-arm64": "3.1.0-rc.0"
73
+ "envio-linux-x64": "3.1.0-rc.1",
74
+ "envio-linux-x64-musl": "3.1.0-rc.1",
75
+ "envio-linux-arm64": "3.1.0-rc.1",
76
+ "envio-darwin-x64": "3.1.0-rc.1",
77
+ "envio-darwin-arm64": "3.1.0-rc.1"
78
78
  }
79
79
  }
@@ -462,38 +462,23 @@ let getHighestBlockBelowThreshold = (cf: t): int => {
462
462
  Finds the last known valid block number below the reorg block
463
463
  If not found, returns the highest block below threshold
464
464
  */
465
- let getLastKnownValidBlock = async (
466
- chainFetcher: t,
467
- ~reorgBlockNumber: int,
468
- //Parameter used for dependency injecting in tests
469
- ~getBlockHashes=(chainFetcher.sourceManager->SourceManager.getActiveSource).getBlockHashes,
470
- ) => {
471
- // Improtant: It's important to not include the reorg detection block number
472
- // because there might be different instances of the source
473
- // with mismatching hashes between them.
474
- // So we MUST always rollback the block number where we detected a reorg.
465
+ let getLastKnownValidBlock = async (chainFetcher: t, ~reorgBlockNumber: int, ~isRealtime: bool) => {
466
+ // Don't include the reorg block itself — different source instances
467
+ // may have mismatching hashes at the head, so we always rollback
468
+ // the block where we detected the reorg.
475
469
  let scannedBlockNumbers =
476
470
  chainFetcher.reorgDetection->ReorgDetection.getThresholdBlockNumbersBelowBlock(
477
471
  ~blockNumber=reorgBlockNumber,
478
472
  ~knownHeight=chainFetcher.fetchState.knownHeight,
479
473
  )
480
474
 
481
- let getBlockHashes = blockNumbers => {
482
- getBlockHashes(~blockNumbers, ~logger=chainFetcher.logger)->Promise.thenResolve(res =>
483
- switch res {
484
- | Ok(v) => v
485
- | Error(exn) =>
486
- exn->ErrorHandling.mkLogAndRaise(
487
- ~msg="Failed to fetch blockHashes for given blockNumbers during rollback",
488
- )
489
- }
490
- )
491
- }
492
-
493
475
  switch scannedBlockNumbers {
494
476
  | [] => chainFetcher->getHighestBlockBelowThreshold
495
477
  | _ => {
496
- let blockNumbersAndHashes = await getBlockHashes(scannedBlockNumbers)
478
+ let blockNumbersAndHashes = await chainFetcher.sourceManager->SourceManager.getBlockHashes(
479
+ ~blockNumbers=scannedBlockNumbers,
480
+ ~isRealtime,
481
+ )
497
482
 
498
483
  switch chainFetcher.reorgDetection->ReorgDetection.getLatestValidScannedBlock(
499
484
  ~blockNumbersAndHashes,
@@ -261,20 +261,12 @@ function getHighestBlockBelowThreshold(cf) {
261
261
  }
262
262
  }
263
263
 
264
- async function getLastKnownValidBlock(chainFetcher, reorgBlockNumber, getBlockHashesOpt) {
265
- let getBlockHashes = getBlockHashesOpt !== undefined ? getBlockHashesOpt : SourceManager.getActiveSource(chainFetcher.sourceManager).getBlockHashes;
264
+ async function getLastKnownValidBlock(chainFetcher, reorgBlockNumber, isRealtime) {
266
265
  let scannedBlockNumbers = ReorgDetection.getThresholdBlockNumbersBelowBlock(chainFetcher.reorgDetection, reorgBlockNumber, chainFetcher.fetchState.knownHeight);
267
- let getBlockHashes$1 = blockNumbers => getBlockHashes(blockNumbers, chainFetcher.logger).then(res => {
268
- if (res.TAG === "Ok") {
269
- return res._0;
270
- } else {
271
- return ErrorHandling.mkLogAndRaise(undefined, "Failed to fetch blockHashes for given blockNumbers during rollback", res._0);
272
- }
273
- });
274
266
  if (scannedBlockNumbers.length === 0) {
275
267
  return getHighestBlockBelowThreshold(chainFetcher);
276
268
  }
277
- let blockNumbersAndHashes = await getBlockHashes$1(scannedBlockNumbers);
269
+ let blockNumbersAndHashes = await SourceManager.getBlockHashes(chainFetcher.sourceManager, scannedBlockNumbers, isRealtime);
278
270
  let blockNumber = ReorgDetection.getLatestValidScannedBlock(chainFetcher.reorgDetection, blockNumbersAndHashes);
279
271
  if (blockNumber !== undefined) {
280
272
  return blockNumber;
package/src/Env.res CHANGED
@@ -47,13 +47,6 @@ let envioApiToken = envSafe->EnvSafe.get("ENVIO_API_TOKEN", S.option(S.string))
47
47
  let hyperSyncClientTimeoutMillis =
48
48
  envSafe->EnvSafe.get("ENVIO_HYPERSYNC_CLIENT_TIMEOUT_MILLIS", S.int, ~fallback=120_000)
49
49
 
50
- /**
51
- This is the number of retries that the binary client makes before rejecting the promise with an error
52
- Default is 0 so that the indexer can handle retries internally
53
- */
54
- let hyperSyncClientMaxRetries =
55
- envSafe->EnvSafe.get("ENVIO_HYPERSYNC_CLIENT_MAX_RETRIES", S.int, ~fallback=0)
56
-
57
50
  let hypersyncClientSerializationFormat =
58
51
  envSafe->EnvSafe.get(
59
52
  "ENVIO_HYPERSYNC_CLIENT_SERIALIZATION_FORMAT",
package/src/Env.res.mjs CHANGED
@@ -53,8 +53,6 @@ let envioApiToken = EnvSafe.get(envSafe, "ENVIO_API_TOKEN", S$RescriptSchema.opt
53
53
 
54
54
  let hyperSyncClientTimeoutMillis = EnvSafe.get(envSafe, "ENVIO_HYPERSYNC_CLIENT_TIMEOUT_MILLIS", S$RescriptSchema.int, undefined, 120000, undefined, undefined);
55
55
 
56
- let hyperSyncClientMaxRetries = EnvSafe.get(envSafe, "ENVIO_HYPERSYNC_CLIENT_MAX_RETRIES", S$RescriptSchema.int, undefined, 0, undefined, undefined);
57
-
58
56
  let hypersyncClientSerializationFormat = EnvSafe.get(envSafe, "ENVIO_HYPERSYNC_CLIENT_SERIALIZATION_FORMAT", HyperSyncClient.serializationFormatSchema, undefined, "CapnProto", undefined, undefined);
59
57
 
60
58
  let hypersyncClientEnableQueryCaching = EnvSafe.get(envSafe, "ENVIO_HYPERSYNC_CLIENT_ENABLE_QUERY_CACHING", S$RescriptSchema.bool, undefined, true, undefined, undefined);
@@ -233,7 +231,6 @@ export {
233
231
  envioAppUrl,
234
232
  envioApiToken,
235
233
  hyperSyncClientTimeoutMillis,
236
- hyperSyncClientMaxRetries,
237
234
  hypersyncClientSerializationFormat,
238
235
  hypersyncClientEnableQueryCaching,
239
236
  hypersyncLogLevel,
@@ -1730,10 +1730,7 @@ let rollback = (fetchState: t, ~targetBlockNumber) => {
1730
1730
  let addressesToRemove = Utils.Set.make()
1731
1731
  let indexingAddresses = Dict.make()
1732
1732
 
1733
- fetchState.indexingAddresses
1734
- ->Dict.keysToArray
1735
- ->Array.forEach(address => {
1736
- let indexingContract = fetchState.indexingAddresses->Dict.getUnsafe(address)
1733
+ fetchState.indexingAddresses->Utils.Dict.forEachWithKey((indexingContract, address) => {
1737
1734
  if indexingContract.registrationBlock > targetBlockNumber {
1738
1735
  let _ = addressesToRemove->Utils.Set.add(address->Address.unsafeFromString)
1739
1736
  } else {
@@ -1360,8 +1360,7 @@ function rollbackPendingQueries(mutPendingQueries, targetBlockNumber) {
1360
1360
  function rollback(fetchState, targetBlockNumber) {
1361
1361
  let addressesToRemove = new Set();
1362
1362
  let indexingAddresses = {};
1363
- Object.keys(fetchState.indexingAddresses).forEach(address => {
1364
- let indexingContract = fetchState.indexingAddresses[address];
1363
+ Utils.Dict.forEachWithKey(fetchState.indexingAddresses, (indexingContract, address) => {
1365
1364
  if (indexingContract.registrationBlock > targetBlockNumber) {
1366
1365
  addressesToRemove.add(address);
1367
1366
  } else {
@@ -1032,6 +1032,7 @@ let injectedTaskReducer = (
1032
1032
  dispatchAction(StartFindingReorgDepth)
1033
1033
  let rollbackTargetBlockNumber = await chainFetcher->getLastKnownValidBlock(
1034
1034
  ~reorgBlockNumber,
1035
+ ~isRealtime=state.chainManager.isRealtime,
1035
1036
  )
1036
1037
 
1037
1038
  chainFetcher.sourceManager->SourceManager.onReorg(
@@ -1215,6 +1216,6 @@ let injectedTaskReducer = (
1215
1216
  let taskReducer = injectedTaskReducer(
1216
1217
  ~waitForNewBlock=SourceManager.waitForNewBlock,
1217
1218
  ~executeQuery=SourceManager.executeQuery,
1218
- ~getLastKnownValidBlock=(chainFetcher, ~reorgBlockNumber) =>
1219
- chainFetcher->ChainFetcher.getLastKnownValidBlock(~reorgBlockNumber),
1219
+ ~getLastKnownValidBlock=(chainFetcher, ~reorgBlockNumber, ~isRealtime) =>
1220
+ chainFetcher->ChainFetcher.getLastKnownValidBlock(~reorgBlockNumber, ~isRealtime),
1220
1221
  )
@@ -1051,7 +1051,7 @@ function injectedTaskReducer(waitForNewBlock, executeQuery, getLastKnownValidBlo
1051
1051
  let chain = match$1.chain;
1052
1052
  let chainFetcher = ChainMap.get(state.chainManager.chainFetchers, chain);
1053
1053
  dispatchAction("StartFindingReorgDepth");
1054
- let rollbackTargetBlockNumber = await getLastKnownValidBlock(chainFetcher, match$1.blockNumber);
1054
+ let rollbackTargetBlockNumber = await getLastKnownValidBlock(chainFetcher, match$1.blockNumber, state.chainManager.isRealtime);
1055
1055
  SourceManager.onReorg(chainFetcher.sourceManager, rollbackTargetBlockNumber);
1056
1056
  return dispatchAction({
1057
1057
  TAG: "FindReorgDepth",
@@ -1218,7 +1218,7 @@ function injectedTaskReducer(waitForNewBlock, executeQuery, getLastKnownValidBlo
1218
1218
  };
1219
1219
  }
1220
1220
 
1221
- let taskReducer = injectedTaskReducer(SourceManager.waitForNewBlock, SourceManager.executeQuery, (chainFetcher, reorgBlockNumber) => ChainFetcher.getLastKnownValidBlock(chainFetcher, reorgBlockNumber, undefined));
1221
+ let taskReducer = injectedTaskReducer(SourceManager.waitForNewBlock, SourceManager.executeQuery, ChainFetcher.getLastKnownValidBlock);
1222
1222
 
1223
1223
  export {
1224
1224
  WriteThrottlers,
@@ -165,13 +165,37 @@ let writeBatch = async (
165
165
  | Some(checkpointId) => checkpointId
166
166
  | None => committedCheckpointId
167
167
  }
168
- persistence.allEntities->Array.forEach(entityConfig => {
169
- let table = inMemoryStore->getInMemTable(~entityConfig)
170
- let resetTable = keepLatestChanges
171
- ? table->InMemoryTable.Entity.resetButKeepLatestChanges
172
- : InMemoryTable.Entity.make()
173
- inMemoryStore.entities->Dict.set((entityConfig.name :> string), resetTable)
174
- })
168
+ if keepLatestChanges {
169
+ persistence.allEntities->Array.forEach(entityConfig => {
170
+ let table = inMemoryStore->getInMemTable(~entityConfig)
171
+ inMemoryStore.entities->Dict.set(
172
+ (entityConfig.name :> string),
173
+ table->InMemoryTable.Entity.resetButKeepLatestChanges,
174
+ )
175
+ })
176
+ } else {
177
+ // Over the limit: drop everything written in a batch and keep only the
178
+ // entities loaded from the db, so the next batch can still read them
179
+ // without hitting the database.
180
+ let loadedFromDbCount = ref(0.)
181
+ let resetTables = persistence.allEntities->Array.map(entityConfig => {
182
+ let resetTable =
183
+ inMemoryStore
184
+ ->getInMemTable(~entityConfig)
185
+ ->InMemoryTable.Entity.resetButKeepLoadedFromDbChanges
186
+ loadedFromDbCount := loadedFromDbCount.contents +. resetTable.changesCount
187
+ resetTable
188
+ })
189
+ // Even the loaded-from-db entities alone exceed the limit, so there's no
190
+ // point keeping them around - drop everything.
191
+ let dropEverything = loadedFromDbCount.contents >= keepLatestChangesLimit
192
+ persistence.allEntities->Array.forEachWithIndex((entityConfig, idx) => {
193
+ inMemoryStore.entities->Dict.set(
194
+ (entityConfig.name :> string),
195
+ dropEverything ? InMemoryTable.Entity.make() : resetTables->Array.getUnsafe(idx),
196
+ )
197
+ })
198
+ }
175
199
  }
176
200
 
177
201
  let prepareRollbackDiff = async (
@@ -151,10 +151,24 @@ async function writeBatch(inMemoryStore, persistence, batch, config, isInReorgTh
151
151
  inMemoryStore.rollback = undefined;
152
152
  let checkpointId = Utils.$$Array.last(batch.checkpointIds);
153
153
  inMemoryStore.committedCheckpointId = checkpointId !== undefined ? checkpointId : committedCheckpointId;
154
- persistence.allEntities.forEach(entityConfig => {
155
- let table = getInMemTable(inMemoryStore, entityConfig);
156
- let resetTable = keepLatestChanges ? InMemoryTable.Entity.resetButKeepLatestChanges(table) : InMemoryTable.Entity.make();
157
- inMemoryStore.entities[entityConfig.name] = resetTable;
154
+ if (keepLatestChanges) {
155
+ persistence.allEntities.forEach(entityConfig => {
156
+ let table = getInMemTable(inMemoryStore, entityConfig);
157
+ inMemoryStore.entities[entityConfig.name] = InMemoryTable.Entity.resetButKeepLatestChanges(table);
158
+ });
159
+ return;
160
+ }
161
+ let loadedFromDbCount = {
162
+ contents: 0
163
+ };
164
+ let resetTables = persistence.allEntities.map(entityConfig => {
165
+ let resetTable = InMemoryTable.Entity.resetButKeepLoadedFromDbChanges(getInMemTable(inMemoryStore, entityConfig));
166
+ loadedFromDbCount.contents = loadedFromDbCount.contents + resetTable.changesCount;
167
+ return resetTable;
168
+ });
169
+ let dropEverything = loadedFromDbCount.contents >= 50000;
170
+ persistence.allEntities.forEach((entityConfig, idx) => {
171
+ inMemoryStore.entities[entityConfig.name] = dropEverything ? InMemoryTable.Entity.make() : resetTables[idx];
158
172
  });
159
173
  }
160
174
 
@@ -65,6 +65,25 @@ module Entity = {
65
65
  changesCount: self.changesCount,
66
66
  }
67
67
 
68
+ // Like resetButKeepLatestChanges, but only keeps entities loaded from the db
69
+ // (changes carrying loadedFromDbCheckpointId), dropping everything written in
70
+ // a batch. The kept count is exposed through the table's changesCount.
71
+ let resetButKeepLoadedFromDbChanges = (self: t): t => {
72
+ let latestEntityChangeById = Dict.make()
73
+ let keptCount = ref(0.)
74
+ self.latestEntityChangeById->Utils.Dict.forEachWithKey((change, key) =>
75
+ if change->Change.getCheckpointId === Internal.loadedFromDbCheckpointId {
76
+ latestEntityChangeById->Dict.set(key, change)
77
+ keptCount := keptCount.contents +. 1.
78
+ }
79
+ )
80
+ {
81
+ ...make(),
82
+ latestEntityChangeById,
83
+ changesCount: keptCount.contents,
84
+ }
85
+ }
86
+
68
87
  let updateIndices = (self: t, ~entity: Internal.entity) => {
69
88
  let entityId = entity->getEntityIdUnsafe
70
89
  //Remove any invalid indices on entity
@@ -83,10 +102,7 @@ module Entity = {
83
102
  })
84
103
  }
85
104
 
86
- self.fieldNameIndices
87
- ->Dict.keysToArray
88
- ->Array.forEach(fieldName => {
89
- let indices = self.fieldNameIndices->Dict.getUnsafe(fieldName)
105
+ self.fieldNameIndices->Utils.Dict.forEachWithKey((indices, fieldName) => {
90
106
  // A missing key reads as `undefined`, which matches the `None` arm of
91
107
  // `FieldValue.t` (`option<...>`). Mirror `addEmptyIndex` so nullable
92
108
  // FK columns that were omitted on the set entity don't crash.
@@ -70,6 +70,34 @@ function resetButKeepLatestChanges(self) {
70
70
  };
71
71
  }
72
72
 
73
+ function resetButKeepLoadedFromDbChanges(self) {
74
+ let latestEntityChangeById = {};
75
+ let keptCount = {
76
+ contents: 0
77
+ };
78
+ Utils.Dict.forEachWithKey(self.latestEntityChangeById, (change, key) => {
79
+ if (change.checkpointId === Internal.loadedFromDbCheckpointId) {
80
+ latestEntityChangeById[key] = change;
81
+ keptCount.contents = keptCount.contents + 1;
82
+ return;
83
+ }
84
+ });
85
+ let init = {
86
+ latestEntityChangeById: {},
87
+ changesCount: 0,
88
+ prevEntityChanges: [],
89
+ indicesByEntityId: {},
90
+ fieldNameIndices: {}
91
+ };
92
+ return {
93
+ latestEntityChangeById: latestEntityChangeById,
94
+ changesCount: keptCount.contents,
95
+ prevEntityChanges: init.prevEntityChanges,
96
+ indicesByEntityId: init.indicesByEntityId,
97
+ fieldNameIndices: init.fieldNameIndices
98
+ };
99
+ }
100
+
73
101
  function updateIndices(self, entity) {
74
102
  let entityId = getEntityIdUnsafe(entity);
75
103
  let entityIndices = self.indicesByEntityId[entityId];
@@ -84,8 +112,7 @@ function updateIndices(self, entity) {
84
112
  }
85
113
  });
86
114
  }
87
- Object.keys(self.fieldNameIndices).forEach(fieldName => {
88
- let indices = self.fieldNameIndices[fieldName];
115
+ Utils.Dict.forEachWithKey(self.fieldNameIndices, (indices, fieldName) => {
89
116
  let fieldValue = entity[fieldName];
90
117
  Utils.Dict.forEach(indices, param => {
91
118
  let relatedEntityIds = param[1];
@@ -233,6 +260,7 @@ let Entity = {
233
260
  makeIndicesSerializedToValue: makeIndicesSerializedToValue,
234
261
  make: make,
235
262
  resetButKeepLatestChanges: resetButKeepLatestChanges,
263
+ resetButKeepLoadedFromDbChanges: resetButKeepLoadedFromDbChanges,
236
264
  updateIndices: updateIndices,
237
265
  deleteEntityFromIndices: deleteEntityFromIndices,
238
266
  set: set,
@@ -181,23 +181,21 @@ let parseEventFiltersOrThrow = {
181
181
  // parameters of the event — TS type checking doesn't catch this when
182
182
  // `where` is a callback.
183
183
  let paramsRecordToTopicSelection = (paramsFilter: dict<JSON.t>) => {
184
- let filterKeys = paramsFilter->Dict.keysToArray
185
- switch filterKeys {
186
- | [] => default
187
- | _ => {
188
- filterKeys->Array.forEach(key => {
189
- if params->Array.includes(key)->not {
190
- JsError.throwWithMessage(
191
- `Invalid where configuration. The event doesn't have an indexed parameter "${key}" and can't use it for filtering`,
192
- )
193
- }
194
- })
195
- {
196
- Internal.topic0,
197
- topic1: topic1(paramsFilter),
198
- topic2: topic2(paramsFilter),
199
- topic3: topic3(paramsFilter),
184
+ if paramsFilter->Utils.Dict.isEmpty {
185
+ default
186
+ } else {
187
+ paramsFilter->Utils.Dict.forEachWithKey((_, key) => {
188
+ if params->Array.includes(key)->not {
189
+ JsError.throwWithMessage(
190
+ `Invalid where configuration. The event doesn't have an indexed parameter "${key}" and can't use it for filtering`,
191
+ )
200
192
  }
193
+ })
194
+ {
195
+ Internal.topic0,
196
+ topic1: topic1(paramsFilter),
197
+ topic2: topic2(paramsFilter),
198
+ topic3: topic3(paramsFilter),
201
199
  }
202
200
  }
203
201
  }
@@ -236,9 +234,7 @@ let parseEventFiltersOrThrow = {
236
234
  | Object(obj) => {
237
235
  // Catch typos (e.g. `parmas:`) and the legacy flat-filter
238
236
  // shape (`{from: ...}`) by rejecting any unknown sibling.
239
- obj
240
- ->Dict.keysToArray
241
- ->Array.forEach(key => {
237
+ obj->Utils.Dict.forEachWithKey((_, key) => {
242
238
  if acceptedWhereKeys->Array.includes(key)->not {
243
239
  JsError.throwWithMessage(
244
240
  `Invalid where configuration. Unknown field "${key}". Indexed parameter filters must be nested under \`params\` and block-range filters under \`block\``,
@@ -145,9 +145,10 @@ function parseEventFiltersOrThrow(eventFilters, sighash, params, contractName, p
145
145
  topic3: emptyTopics
146
146
  };
147
147
  let paramsRecordToTopicSelection = paramsFilter => {
148
- let filterKeys = Object.keys(paramsFilter);
149
- if (filterKeys.length !== 0) {
150
- filterKeys.forEach(key => {
148
+ if (Utils.Dict.isEmpty(paramsFilter)) {
149
+ return $$default;
150
+ } else {
151
+ Utils.Dict.forEachWithKey(paramsFilter, (param, key) => {
151
152
  if (!params.includes(key)) {
152
153
  return Stdlib_JsError.throwWithMessage(`Invalid where configuration. The event doesn't have an indexed parameter "` + key + `" and can't use it for filtering`);
153
154
  }
@@ -158,8 +159,6 @@ function parseEventFiltersOrThrow(eventFilters, sighash, params, contractName, p
158
159
  topic2: topic2(paramsFilter),
159
160
  topic3: topic3(paramsFilter)
160
161
  };
161
- } else {
162
- return $$default;
163
162
  }
164
163
  };
165
164
  let acceptedWhereKeys = [
@@ -176,7 +175,7 @@ function parseEventFiltersOrThrow(eventFilters, sighash, params, contractName, p
176
175
  if (typeof where !== "object" || where === null || Array.isArray(where)) {
177
176
  return Stdlib_JsError.throwWithMessage("Invalid where configuration. Expected an object");
178
177
  }
179
- Object.keys(where).forEach(key => {
178
+ Utils.Dict.forEachWithKey(where, (param, key) => {
180
179
  if (!acceptedWhereKeys.includes(key)) {
181
180
  return Stdlib_JsError.throwWithMessage(`Invalid where configuration. Unknown field "` + key + `". Indexed parameter filters must be nested under \`params\` and block-range filters under \`block\``);
182
181
  }
@@ -0,0 +1,15 @@
1
+ // Rest client for envio's own REST endpoints (e.g. the HyperSync/HyperFuel
2
+ // /height poll). Tags requests with the hyperindex User-Agent so they're
3
+ // attributable on the server, mirroring the SSE height stream and the Rust
4
+ // data-query client which already set it.
5
+ let make = (baseUrl: string): Rest.client => {
6
+ let userAgent = `hyperindex/${Utils.EnvioPackage.value.version}`
7
+ Rest.client(baseUrl, ~fetcher=(args: Rest.ApiFetcher.args) => {
8
+ let headers = switch args.headers {
9
+ | Some(headers) => headers
10
+ | None => Dict.make()
11
+ }
12
+ headers->Dict.set("User-Agent", userAgent->(Utils.magic: string => unknown))
13
+ Rest.ApiFetcher.default({...args, headers: Some(headers)})
14
+ })
15
+ }
@@ -0,0 +1,24 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Rest from "../vendored/Rest.res.mjs";
4
+ import * as Utils from "../Utils.res.mjs";
5
+
6
+ function make(baseUrl) {
7
+ let userAgent = `hyperindex/` + Utils.EnvioPackage.value.version;
8
+ return Rest.client(baseUrl, args => {
9
+ let headers = args.headers;
10
+ let headers$1 = headers !== undefined ? headers : ({});
11
+ headers$1["User-Agent"] = userAgent;
12
+ return Rest.ApiFetcher.$$default({
13
+ body: args.body,
14
+ headers: headers$1,
15
+ method: args.method,
16
+ path: args.path
17
+ });
18
+ });
19
+ }
20
+
21
+ export {
22
+ make,
23
+ }
24
+ /* Rest Not a pure module */
@@ -87,7 +87,6 @@ let makeSources = (
87
87
  allEventParams,
88
88
  eventRouter,
89
89
  apiToken: Env.envioApiToken,
90
- clientMaxRetries: Env.hyperSyncClientMaxRetries,
91
90
  clientTimeoutMillis: Env.hyperSyncClientTimeoutMillis,
92
91
  lowercaseAddresses,
93
92
  serializationFormat: Env.hypersyncClientSerializationFormat,
@@ -54,7 +54,6 @@ function makeSources(chain, contracts, hyperSync, rpcs, lowercaseAddresses) {
54
54
  allEventParams: allEventParams,
55
55
  eventRouter: eventRouter,
56
56
  apiToken: Env.envioApiToken,
57
- clientMaxRetries: Env.hyperSyncClientMaxRetries,
58
57
  clientTimeoutMillis: Env.hyperSyncClientTimeoutMillis,
59
58
  lowercaseAddresses: lowercaseAddresses,
60
59
  serializationFormat: Env.hypersyncClientSerializationFormat,
@@ -449,7 +449,7 @@ let make = ({chain, endpointUrl}: options): t => {
449
449
  let getBlockHashes = (~blockNumbers as _, ~logger as _) =>
450
450
  JsError.throwWithMessage("HyperFuel does not support getting block hashes")
451
451
 
452
- let jsonApiClient = Rest.client(endpointUrl)
452
+ let jsonApiClient = EnvioApiClient.make(endpointUrl)
453
453
 
454
454
  {
455
455
  name,
@@ -11,6 +11,7 @@ import * as Prometheus from "../Prometheus.res.mjs";
11
11
  import * as EventRouter from "./EventRouter.res.mjs";
12
12
  import * as ErrorHandling from "../ErrorHandling.res.mjs";
13
13
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
14
+ import * as EnvioApiClient from "./EnvioApiClient.res.mjs";
14
15
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
15
16
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
16
17
 
@@ -387,7 +388,7 @@ function make(param) {
387
388
  };
388
389
  };
389
390
  let getBlockHashes = (param, param$1) => Stdlib_JsError.throwWithMessage("HyperFuel does not support getting block hashes");
390
- let jsonApiClient = Rest.client(endpointUrl, undefined);
391
+ let jsonApiClient = EnvioApiClient.make(endpointUrl);
391
392
  return {
392
393
  name: name,
393
394
  sourceFor: "Sync",
@@ -1,3 +1,19 @@
1
+ let reraisIfRateLimited = exn =>
2
+ switch exn->JsExn.anyToExnInternal {
3
+ | JsExn(e) =>
4
+ switch e->JsExn.message {
5
+ | Some(msg) if msg->String.startsWith("RATE_LIMITED:") =>
6
+ let resetMs =
7
+ msg
8
+ ->String.slice(~start=13, ~end=msg->String.length)
9
+ ->Int.fromString
10
+ ->Option.getOr(1000)
11
+ throw(Source.RateLimited({resetMs: resetMs}))
12
+ | _ => ()
13
+ }
14
+ | _ => ()
15
+ }
16
+
1
17
  type logsQueryPage = {
2
18
  items: array<HyperSyncClient.EventItems.item>,
3
19
  nextBlock: int,
@@ -108,6 +124,7 @@ module GetLogs = {
108
124
  let res = switch await client.getEventItems(~query) {
109
125
  | res => res
110
126
  | exception exn =>
127
+ reraisIfRateLimited(exn)
111
128
  switch extractMissingParams(exn) {
112
129
  | Some(missingParams) => throw(Error(UnexpectedMissingParams({missingParams: missingParams})))
113
130
  | None => throw(exn)
@@ -193,7 +210,9 @@ module BlockData = {
193
210
 
194
211
  Prometheus.SourceRequestCount.increment(~sourceName, ~chainId, ~method="getBlockHashes")
195
212
  let maybeSuccessfulRes = switch await client.get(~query=body) {
196
- | exception _ => None
213
+ | exception exn =>
214
+ reraisIfRateLimited(exn)
215
+ None
197
216
  | res if res.nextBlock <= fromBlock => None
198
217
  | res => Some(res)
199
218
  }
@@ -2,8 +2,10 @@
2
2
 
3
3
  import * as Time from "../Time.res.mjs";
4
4
  import * as Utils from "../Utils.res.mjs";
5
+ import * as Source from "./Source.res.mjs";
5
6
  import * as Logging from "../Logging.res.mjs";
6
7
  import * as Prometheus from "../Prometheus.res.mjs";
8
+ import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
7
9
  import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
8
10
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
9
11
  import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
@@ -13,6 +15,26 @@ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
13
15
  import * as HyperSyncClient from "./HyperSyncClient.res.mjs";
14
16
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
15
17
 
18
+ function reraisIfRateLimited(exn) {
19
+ let e = Primitive_exceptions.internalToException(exn);
20
+ if (e.RE_EXN_ID !== "JsExn") {
21
+ return;
22
+ }
23
+ let msg = Stdlib_JsExn.message(e._1);
24
+ if (msg === undefined) {
25
+ return;
26
+ }
27
+ if (!msg.startsWith("RATE_LIMITED:")) {
28
+ return;
29
+ }
30
+ let resetMs = Stdlib_Option.getOr(Stdlib_Int.fromString(msg.slice(13, msg.length), undefined), 1000);
31
+ throw {
32
+ RE_EXN_ID: Source.RateLimited,
33
+ resetMs: resetMs,
34
+ Error: new Error()
35
+ };
36
+ }
37
+
16
38
  let HyperSyncQueryError = /* @__PURE__ */Primitive_exceptions.create("HyperSync.HyperSyncQueryError");
17
39
 
18
40
  function queryErrorToMsq(e) {
@@ -84,6 +106,7 @@ async function query(client, fromBlock, toBlock, logSelections, fieldSelection)
84
106
  res = await client.getEventItems(query$1);
85
107
  } catch (raw_exn) {
86
108
  let exn = Primitive_exceptions.internalToException(raw_exn);
109
+ reraisIfRateLimited(exn);
87
110
  let missingParams = extractMissingParams(exn);
88
111
  if (missingParams !== undefined) {
89
112
  throw {
@@ -178,7 +201,9 @@ async function queryBlockData(client, fromBlock, toBlock, sourceName, chainId, l
178
201
  try {
179
202
  res = await client.get(body);
180
203
  exit = 1;
181
- } catch (exn) {
204
+ } catch (raw_exn) {
205
+ let exn = Primitive_exceptions.internalToException(raw_exn);
206
+ reraisIfRateLimited(exn);
182
207
  maybeSuccessfulRes = undefined;
183
208
  }
184
209
  if (exit === 1) {
@@ -362,6 +362,7 @@ module EventItems = {
362
362
  type t = {
363
363
  get: (~query: query) => promise<queryResponse>,
364
364
  getEventItems: (~query: query) => promise<EventItems.response>,
365
+ getHeight: unit => promise<int>,
365
366
  }
366
367
 
367
368
  @send
@@ -387,7 +388,6 @@ let make = (
387
388
  ~url,
388
389
  ~apiToken,
389
390
  ~httpReqTimeoutMillis,
390
- ~maxNumRetries,
391
391
  ~eventParams,
392
392
  ~enableChecksumAddresses=true,
393
393
  ~serializationFormat=?,
@@ -404,7 +404,8 @@ let make = (
404
404
  enableChecksumAddresses,
405
405
  apiToken,
406
406
  httpReqTimeoutMillis,
407
- maxNumRetries,
407
+ // Retries are handled internally by the indexer, not the binary client
408
+ maxNumRetries: 0,
408
409
  ?serializationFormat,
409
410
  ?enableQueryCaching,
410
411
  ?retryBaseMs,