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.
- package/package.json +6 -6
- package/src/ChainFetcher.res +8 -23
- package/src/ChainFetcher.res.mjs +2 -10
- package/src/Env.res +0 -7
- package/src/Env.res.mjs +0 -3
- package/src/FetchState.res +1 -4
- package/src/FetchState.res.mjs +1 -2
- package/src/GlobalState.res +3 -2
- package/src/GlobalState.res.mjs +2 -2
- package/src/InMemoryStore.res +31 -7
- package/src/InMemoryStore.res.mjs +18 -4
- package/src/InMemoryTable.res +20 -4
- package/src/InMemoryTable.res.mjs +30 -2
- package/src/LogSelection.res +15 -19
- package/src/LogSelection.res.mjs +5 -6
- package/src/sources/EnvioApiClient.res +15 -0
- package/src/sources/EnvioApiClient.res.mjs +24 -0
- package/src/sources/EvmChain.res +0 -1
- package/src/sources/EvmChain.res.mjs +0 -1
- package/src/sources/HyperFuelSource.res +1 -1
- package/src/sources/HyperFuelSource.res.mjs +2 -1
- package/src/sources/HyperSync.res +20 -1
- package/src/sources/HyperSync.res.mjs +26 -1
- package/src/sources/HyperSyncClient.res +3 -2
- package/src/sources/HyperSyncClient.res.mjs +2 -2
- package/src/sources/HyperSyncSource.res +18 -19
- package/src/sources/HyperSyncSource.res.mjs +40 -14
- package/src/sources/Source.res +2 -0
- package/src/sources/Source.res.mjs +3 -0
- package/src/sources/SourceManager.res +168 -8
- package/src/sources/SourceManager.res.mjs +131 -3
- package/src/sources/SourceManager.resi +17 -0
- package/src/tui/Tui.res +44 -6
- package/src/tui/Tui.res.mjs +56 -8
- package/src/tui/components/TuiData.res +3 -0
- package/src/sources/HyperSyncJsonApi.res +0 -390
- 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.
|
|
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.
|
|
74
|
-
"envio-linux-x64-musl": "3.1.0-rc.
|
|
75
|
-
"envio-linux-arm64": "3.1.0-rc.
|
|
76
|
-
"envio-darwin-x64": "3.1.0-rc.
|
|
77
|
-
"envio-darwin-arm64": "3.1.0-rc.
|
|
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
|
}
|
package/src/ChainFetcher.res
CHANGED
|
@@ -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
|
-
|
|
467
|
-
|
|
468
|
-
//
|
|
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(
|
|
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,
|
package/src/ChainFetcher.res.mjs
CHANGED
|
@@ -261,20 +261,12 @@ function getHighestBlockBelowThreshold(cf) {
|
|
|
261
261
|
}
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
-
async function getLastKnownValidBlock(chainFetcher, reorgBlockNumber,
|
|
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
|
|
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,
|
package/src/FetchState.res
CHANGED
|
@@ -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 {
|
package/src/FetchState.res.mjs
CHANGED
|
@@ -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
|
-
|
|
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 {
|
package/src/GlobalState.res
CHANGED
|
@@ -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
|
)
|
package/src/GlobalState.res.mjs
CHANGED
|
@@ -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,
|
|
1221
|
+
let taskReducer = injectedTaskReducer(SourceManager.waitForNewBlock, SourceManager.executeQuery, ChainFetcher.getLastKnownValidBlock);
|
|
1222
1222
|
|
|
1223
1223
|
export {
|
|
1224
1224
|
WriteThrottlers,
|
package/src/InMemoryStore.res
CHANGED
|
@@ -165,13 +165,37 @@ let writeBatch = async (
|
|
|
165
165
|
| Some(checkpointId) => checkpointId
|
|
166
166
|
| None => committedCheckpointId
|
|
167
167
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
package/src/InMemoryTable.res
CHANGED
|
@@ -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
|
-
|
|
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,
|
package/src/LogSelection.res
CHANGED
|
@@ -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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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\``,
|
package/src/LogSelection.res.mjs
CHANGED
|
@@ -145,9 +145,10 @@ function parseEventFiltersOrThrow(eventFilters, sighash, params, contractName, p
|
|
|
145
145
|
topic3: emptyTopics
|
|
146
146
|
};
|
|
147
147
|
let paramsRecordToTopicSelection = paramsFilter => {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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 */
|
package/src/sources/EvmChain.res
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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
|
|
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 (
|
|
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
|
-
|
|
407
|
+
// Retries are handled internally by the indexer, not the binary client
|
|
408
|
+
maxNumRetries: 0,
|
|
408
409
|
?serializationFormat,
|
|
409
410
|
?enableQueryCaching,
|
|
410
411
|
?retryBaseMs,
|