envio 3.15.0 → 3.16.0

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 (99) hide show
  1. package/README.md +40 -34
  2. package/evm.schema.json +11 -2
  3. package/fuel.schema.json +10 -1
  4. package/index.d.ts +6 -0
  5. package/licenses/README.md +1 -1
  6. package/package.json +8 -8
  7. package/src/Batch.res.mjs +5 -5
  8. package/src/BatchProcessing.res +1 -0
  9. package/src/BatchProcessing.res.mjs +1 -0
  10. package/src/ChainState.res +48 -7
  11. package/src/ChainState.res.mjs +27 -11
  12. package/src/Clock.res +57 -0
  13. package/src/Clock.res.mjs +72 -0
  14. package/src/Config.res +10 -3
  15. package/src/Config.res.mjs +4 -2
  16. package/src/Core.res +4 -40
  17. package/src/EffectState.res +10 -2
  18. package/src/EffectState.res.mjs +8 -4
  19. package/src/EffectState.resi +3 -1
  20. package/src/EntityId.res +2 -7
  21. package/src/EntityId.res.mjs +1 -8
  22. package/src/EntityTables.res +4 -1
  23. package/src/EntityTables.res.mjs +1 -1
  24. package/src/FetchState.res +157 -69
  25. package/src/FetchState.res.mjs +144 -49
  26. package/src/FinalizeBackfill.res +2 -4
  27. package/src/FinalizeBackfill.res.mjs +3 -4
  28. package/src/InMemoryTable.res +17 -16
  29. package/src/InMemoryTable.res.mjs +14 -15
  30. package/src/IndexerState.res +28 -4
  31. package/src/IndexerState.res.mjs +19 -6
  32. package/src/IndexerState.resi +4 -0
  33. package/src/Internal.res +0 -49
  34. package/src/Internal.res.mjs +1 -80
  35. package/src/LoadLayer.res +14 -8
  36. package/src/LoadLayer.res.mjs +8 -6
  37. package/src/Main.res.mjs +1 -1
  38. package/src/Persistence.res +3 -23
  39. package/src/Persistence.res.mjs +3 -3
  40. package/src/Rollback.res +0 -1
  41. package/src/Rollback.res.mjs +1 -1
  42. package/src/SafeCheckpointTracking.res +4 -4
  43. package/src/SafeCheckpointTracking.res.mjs +1 -1
  44. package/src/StagedRows.res +106 -0
  45. package/src/StagedRows.res.mjs +102 -0
  46. package/src/Staging.res +32 -14
  47. package/src/Staging.res.mjs +17 -20
  48. package/src/Storage.res +277 -948
  49. package/src/Storage.res.mjs +215 -707
  50. package/src/Supervisor.res +75 -56
  51. package/src/Supervisor.res.mjs +55 -26
  52. package/src/TestIndexer.res +22 -27
  53. package/src/TestIndexer.res.mjs +20 -17
  54. package/src/Throttler.res +15 -8
  55. package/src/Throttler.res.mjs +9 -6
  56. package/src/Throttler.resi +1 -1
  57. package/src/Utils.res +0 -6
  58. package/src/Utils.res.mjs +0 -5
  59. package/src/Writing.res +0 -1
  60. package/src/Writing.res.mjs +1 -1
  61. package/src/bindings/ClickHouse.res +0 -322
  62. package/src/bindings/ClickHouse.res.mjs +0 -338
  63. package/src/bindings/StorageClient.res +107 -174
  64. package/src/bindings/StorageClient.res.mjs +19 -8
  65. package/src/bindings/Vitest.res +0 -26
  66. package/src/bindings/Vitest.res.mjs +0 -3
  67. package/src/db/Table.res +24 -150
  68. package/src/db/Table.res.mjs +38 -272
  69. package/src/sources/ChainSources.res +2 -0
  70. package/src/sources/ChainSources.res.mjs +2 -2
  71. package/src/sources/EvmChain.res +3 -0
  72. package/src/sources/EvmChain.res.mjs +4 -2
  73. package/src/sources/EvmHyperSyncSource.res +2 -2
  74. package/src/sources/EvmHyperSyncSource.res.mjs +1 -1
  75. package/src/sources/EvmRpcClient.res +13 -3
  76. package/src/sources/EvmRpcClient.res.mjs +4 -3
  77. package/src/sources/EvmRpcWs.res +4 -3
  78. package/src/sources/EvmRpcWs.res.mjs +2 -2
  79. package/src/sources/HeightFeed.res +31 -10
  80. package/src/sources/HeightFeed.res.mjs +10 -6
  81. package/src/sources/HeightStream.res +32 -17
  82. package/src/sources/HeightStream.res.mjs +10 -10
  83. package/src/sources/HyperSyncSSE.res +4 -6
  84. package/src/sources/HyperSyncSSE.res.mjs +2 -2
  85. package/src/sources/RpcSource.res +10 -1
  86. package/src/sources/RpcSource.res.mjs +4 -3
  87. package/src/sources/Source.res +3 -0
  88. package/src/sources/SourceManager.res +84 -77
  89. package/src/sources/SourceManager.res.mjs +17 -20
  90. package/src/sources/SourceManager.resi +3 -4
  91. package/src/sources/StartBlockResolver.res +1 -0
  92. package/src/sources/StartBlockResolver.res.mjs +3 -2
  93. package/src/sources/SvmHyperSyncSource.res +2 -2
  94. package/src/sources/SvmHyperSyncSource.res.mjs +1 -1
  95. package/svm.schema.json +11 -2
  96. package/src/PgWriting.res +0 -36
  97. package/src/PgWriting.res.mjs +0 -34
  98. package/src/db/IndexDefinition.res +0 -47
  99. package/src/db/IndexDefinition.res.mjs +0 -67
@@ -108,6 +108,12 @@ type t = {
108
108
  // in-process test runner injects a callback that resolves its run promise
109
109
  // instead, so a caught-up run doesn't kill the test process.
110
110
  onExit: option<unit => unit>,
111
+ // Told, in the processing pass that gets it there, that a run holding its
112
+ // realtime transitions has gone as far as it can on its own. The test
113
+ // runner's stand-in supervisor releases from it, so the release lands at a
114
+ // point in the indexer's own progress rather than on a timer.
115
+ onArrivedAtHead: option<unit => unit>,
116
+ mutable reportedArrivalAtHead: bool,
111
117
  // Set once on any fatal error. Every loop checks it to stop iterating and
112
118
  // every launch skips when it's set, so a single failure quiesces the indexer.
113
119
  mutable isStopped: bool,
@@ -157,10 +163,13 @@ let make = (
157
163
  ~exitAfterFirstEventBlock=false,
158
164
  ~onError: ErrorHandling.t => unit,
159
165
  ~onExit=?,
166
+ ~onArrivedAtHead=?,
167
+ ~clock=Clock.real,
160
168
  ) => {
161
169
  let chainMetaThrottler = {
162
170
  let intervalMillis = Env.ThrottleWrites.chainMetadataIntervalMillis
163
171
  Throttler.make(
172
+ ~clock,
164
173
  ~intervalMillis,
165
174
  ~logger=logger->Logger.withFields({
166
175
  "context": "Throttler for chain metadata writes",
@@ -175,7 +184,7 @@ let make = (
175
184
  persistence,
176
185
  allEntities: persistence.allEntities,
177
186
  entities: EntityTables.make(persistence.allEntities->EntityTables.crossChain),
178
- effectState: EffectState.make(),
187
+ effectState: EffectState.make(~clock, ()),
179
188
  rollback: None,
180
189
  committedFrontier,
181
190
  processedFrontier: committedFrontier->Frontier.copy,
@@ -205,6 +214,8 @@ let make = (
205
214
  exitAfterFirstEventBlock,
206
215
  onError,
207
216
  onExit,
217
+ onArrivedAtHead,
218
+ reportedArrivalAtHead: false,
208
219
  isStopped: false,
209
220
  epoch: 0,
210
221
  simulateDeadInputTracker: SimulateDeadInputTracker.makeFromConfig(config),
@@ -225,12 +236,13 @@ let make = (
225
236
  }
226
237
 
227
238
  // Check if progress is past the reorg threshold (safe block).
228
- // A chain is in reorg threshold when progressBlockNumber > sourceBlockNumber - maxReorgDepth.
229
- // This matches the logic in InternalTable.Checkpoints.makeGetReorgCheckpointsQuery.
239
+ // A chain is in reorg threshold once its progress passes the safe block,
240
+ // sourceBlockNumber - maxReorgDepth - 1. This matches the reorg checkpoints
241
+ // query in packages/cli/src/postgres/internal.rs.
230
242
  let isProgressInReorgThreshold = (~progressBlockNumber, ~sourceBlockNumber, ~maxReorgDepth) => {
231
243
  maxReorgDepth > 0 &&
232
244
  sourceBlockNumber > 0 &&
233
- progressBlockNumber > sourceBlockNumber - maxReorgDepth
245
+ progressBlockNumber > sourceBlockNumber - maxReorgDepth - 1
234
246
  }
235
247
 
236
248
  let makeFromDbState = (
@@ -249,6 +261,7 @@ let makeFromDbState = (
249
261
  ~holdRealtime=false,
250
262
  ~onError,
251
263
  ~onExit=?,
264
+ ~onArrivedAtHead=?,
252
265
  ) => {
253
266
  let isInReorgThreshold = if initialState.cleanRun {
254
267
  false
@@ -303,6 +316,8 @@ let makeFromDbState = (
303
316
  ~exitAfterFirstEventBlock,
304
317
  ~onError,
305
318
  ~onExit?,
319
+ ~onArrivedAtHead?,
320
+ ~clock=?sourceTiming->Option.flatMap(timing => timing.clock),
306
321
  )
307
322
  state.crossChainState->CrossChainState.markCaughtUpOnResume
308
323
  initialState.cache->Utils.Dict.forEach(({effectName, count, scope}) => {
@@ -559,6 +574,15 @@ let isHoldingRealtime = (state: t) => state.crossChainState->CrossChainState.isH
559
574
 
560
575
  let hasArrivedAtHead = (state: t) => state.crossChainState->CrossChainState.hasArrivedAtHead
561
576
 
577
+ let reportArrivalAtHead = (state: t) =>
578
+ switch state.onArrivedAtHead {
579
+ | Some(onArrivedAtHead)
580
+ if !state.reportedArrivalAtHead && state->isHoldingRealtime && state->hasArrivedAtHead =>
581
+ state.reportedArrivalAtHead = true
582
+ onArrivedAtHead()
583
+ | _ => ()
584
+ }
585
+
562
586
  let releaseRealtime = (state: t) => {
563
587
  state.crossChainState->CrossChainState.releaseRealtime
564
588
  // Every chain is parked at the head with no batch coming, so nothing would
@@ -2,6 +2,7 @@
2
2
 
3
3
  import * as Env from "./Env.res.mjs";
4
4
  import * as Batch from "./Batch.res.mjs";
5
+ import * as Clock from "./Clock.res.mjs";
5
6
  import * as Utils from "./Utils.res.mjs";
6
7
  import * as Config from "./Config.res.mjs";
7
8
  import * as Logger from "./Logger.res.mjs";
@@ -26,25 +27,26 @@ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js
26
27
  import * as CheckpointSequence from "./db/CheckpointSequence.res.mjs";
27
28
  import * as SimulateDeadInputTracker from "./SimulateDeadInputTracker.res.mjs";
28
29
 
29
- function make(config, logger, persistence, chainStates, isRealtime, targetBufferSizeOpt, holdRealtimeOpt, committedFrontierOpt, isDevelopmentModeOpt, shouldUseTuiOpt, exitAfterFirstEventBlockOpt, onError, onExit) {
30
+ function make(config, logger, persistence, chainStates, isRealtime, targetBufferSizeOpt, holdRealtimeOpt, committedFrontierOpt, isDevelopmentModeOpt, shouldUseTuiOpt, exitAfterFirstEventBlockOpt, onError, onExit, onArrivedAtHead, clockOpt) {
30
31
  let targetBufferSize = targetBufferSizeOpt !== undefined ? targetBufferSizeOpt : CrossChainState.calculateTargetBufferSize();
31
32
  let holdRealtime = holdRealtimeOpt !== undefined ? holdRealtimeOpt : false;
32
33
  let committedFrontier = committedFrontierOpt !== undefined ? committedFrontierOpt : Frontier.empty();
33
34
  let isDevelopmentMode = isDevelopmentModeOpt !== undefined ? isDevelopmentModeOpt : false;
34
35
  let shouldUseTui = shouldUseTuiOpt !== undefined ? shouldUseTuiOpt : false;
35
36
  let exitAfterFirstEventBlock = exitAfterFirstEventBlockOpt !== undefined ? exitAfterFirstEventBlockOpt : false;
37
+ let clock = clockOpt !== undefined ? clockOpt : Clock.real;
36
38
  let intervalMillis = Env.ThrottleWrites.chainMetadataIntervalMillis;
37
39
  let chainMetaThrottler = Throttler.make(intervalMillis, Logger.withFields(logger, {
38
40
  context: "Throttler for chain metadata writes",
39
41
  intervalMillis: intervalMillis
40
- }));
42
+ }), clock);
41
43
  return {
42
44
  config: config,
43
45
  logger: logger,
44
46
  persistence: persistence,
45
47
  allEntities: persistence.allEntities,
46
48
  entities: EntityTables.make(EntityTables.crossChain(persistence.allEntities)),
47
- effectState: EffectState.make(),
49
+ effectState: EffectState.make(clock, undefined),
48
50
  rollback: undefined,
49
51
  committedFrontier: committedFrontier,
50
52
  processedFrontier: Frontier.copy(committedFrontier),
@@ -68,6 +70,8 @@ function make(config, logger, persistence, chainStates, isRealtime, targetBuffer
68
70
  exitAfterFirstEventBlock: exitAfterFirstEventBlock,
69
71
  onError: onError,
70
72
  onExit: onExit,
73
+ onArrivedAtHead: onArrivedAtHead,
74
+ reportedArrivalAtHead: false,
71
75
  isStopped: false,
72
76
  epoch: 0,
73
77
  scheduleProcessing: () => {},
@@ -88,7 +92,7 @@ function make(config, logger, persistence, chainStates, isRealtime, targetBuffer
88
92
  };
89
93
  }
90
94
 
91
- function makeFromDbState(config, logger, persistence, initialState, registrationsByChainId, isDevelopmentModeOpt, shouldUseTuiOpt, exitAfterFirstEventBlockOpt, sourceTiming, targetBufferSizeOpt, holdRealtimeOpt, onError, onExit) {
95
+ function makeFromDbState(config, logger, persistence, initialState, registrationsByChainId, isDevelopmentModeOpt, shouldUseTuiOpt, exitAfterFirstEventBlockOpt, sourceTiming, targetBufferSizeOpt, holdRealtimeOpt, onError, onExit, onArrivedAtHead) {
92
96
  let isDevelopmentMode = isDevelopmentModeOpt !== undefined ? isDevelopmentModeOpt : false;
93
97
  let shouldUseTui = shouldUseTuiOpt !== undefined ? shouldUseTuiOpt : false;
94
98
  let exitAfterFirstEventBlock = exitAfterFirstEventBlockOpt !== undefined ? exitAfterFirstEventBlockOpt : false;
@@ -99,7 +103,7 @@ function makeFromDbState(config, logger, persistence, initialState, registration
99
103
  let sourceBlockNumber = resumedChainState.sourceBlockNumber;
100
104
  let maxReorgDepth = resumedChainState.maxReorgDepth;
101
105
  if (maxReorgDepth > 0 && sourceBlockNumber > 0) {
102
- return progressBlockNumber > (sourceBlockNumber - maxReorgDepth | 0);
106
+ return progressBlockNumber > ((sourceBlockNumber - maxReorgDepth | 0) - 1 | 0);
103
107
  } else {
104
108
  return false;
105
109
  }
@@ -111,7 +115,7 @@ function makeFromDbState(config, logger, persistence, initialState, registration
111
115
  let chainConfig = ChainMap.get(config.chainMap, chainId);
112
116
  chainStates[resumedChainState.id] = ChainState.makeFromDbState(chainConfig, resumedChainState, initialState.reorgCheckpoints, isInReorgThreshold, isRealtime, config, initialState.contractMapping, registrationsByChainId, logger, sourceTiming);
113
117
  });
114
- let state = make(config, logger, persistence, chainStates, isRealtime, targetBufferSize, holdRealtime, initialState.checkpointFrontier, isDevelopmentMode, shouldUseTui, exitAfterFirstEventBlock, onError, onExit);
118
+ let state = make(config, logger, persistence, chainStates, isRealtime, targetBufferSize, holdRealtime, initialState.checkpointFrontier, isDevelopmentMode, shouldUseTui, exitAfterFirstEventBlock, onError, onExit, onArrivedAtHead, Stdlib_Option.flatMap(sourceTiming, timing => timing.clock));
115
119
  CrossChainState.markCaughtUpOnResume(state.crossChainState);
116
120
  Utils.Dict.forEach(initialState.cache, param => {
117
121
  let count = param.count;
@@ -401,6 +405,14 @@ function hasArrivedAtHead(state) {
401
405
  return CrossChainState.hasArrivedAtHead(state.crossChainState);
402
406
  }
403
407
 
408
+ function reportArrivalAtHead(state) {
409
+ let onArrivedAtHead = state.onArrivedAtHead;
410
+ if (onArrivedAtHead !== undefined && !state.reportedArrivalAtHead && CrossChainState.isHoldingRealtime(state.crossChainState) && CrossChainState.hasArrivedAtHead(state.crossChainState)) {
411
+ state.reportedArrivalAtHead = true;
412
+ return onArrivedAtHead();
413
+ }
414
+ }
415
+
404
416
  function releaseRealtime(state) {
405
417
  CrossChainState.releaseRealtime(state.crossChainState);
406
418
  state.scheduleProcessing();
@@ -917,6 +929,7 @@ export {
917
929
  bindScheduleProcessing,
918
930
  isHoldingRealtime,
919
931
  hasArrivedAtHead,
932
+ reportArrivalAtHead,
920
933
  releaseRealtime,
921
934
  markReady,
922
935
  reportFinished,
@@ -24,6 +24,8 @@ let make: (
24
24
  ~exitAfterFirstEventBlock: bool=?,
25
25
  ~onError: ErrorHandling.t => unit,
26
26
  ~onExit: unit => unit=?,
27
+ ~onArrivedAtHead: unit => unit=?,
28
+ ~clock: Clock.t=?,
27
29
  ) => t
28
30
 
29
31
  let makeFromDbState: (
@@ -40,6 +42,7 @@ let makeFromDbState: (
40
42
  ~holdRealtime: bool=?,
41
43
  ~onError: ErrorHandling.t => unit,
42
44
  ~onExit: unit => unit=?,
45
+ ~onArrivedAtHead: unit => unit=?,
43
46
  ) => t
44
47
 
45
48
  let unexpectedErrorMsg: string
@@ -106,6 +109,7 @@ let shouldFinalizeIndexes: t => bool
106
109
  let bindScheduleProcessing: (t, unit => unit) => unit
107
110
  let isHoldingRealtime: t => bool
108
111
  let hasArrivedAtHead: t => bool
112
+ let reportArrivalAtHead: t => unit
109
113
  // The supervisor's go-ahead for a process driving part of a split run.
110
114
  let releaseRealtime: t => unit
111
115
  let markReady: (t, ~readyAt: Date.t) => unit
package/src/Internal.res CHANGED
@@ -950,55 +950,6 @@ module EffectCache = {
950
950
  | CrossChain => cacheTablePrefix ++ effectName
951
951
  | Chain(chainId) => `envio_${chainId->ChainId.toString}_effect_${effectName}`
952
952
  }
953
-
954
- // Only accepts a canonical decimal chain id ("7", not "007" or "1foo") —
955
- // the schema's parser follows parseFloat semantics and accepts both.
956
- let parseChainId = str =>
957
- switch try Some(str->ChainId.normalizeOrThrow) catch {
958
- | _ => None
959
- } {
960
- | Some(chainId) if chainId->ChainId.toString === str => Some(chainId)
961
- | _ => None
962
- }
963
-
964
- let chainScopedRe = /^envio_([0-9]+)_effect_(.+)$/
965
- let crossChainRe = /^envio_effect_(.+)$/
966
-
967
- // Inverse of toTableName. Returns None for any table name that isn't a cache
968
- // table. Chain-scoped is tried first: the `_effect_` separator keeps effect
969
- // names that themselves start with digits unambiguous.
970
- let fromTableName = (tableName): option<(string, chainScope)> =>
971
- switch RegExp.exec(chainScopedRe, tableName) {
972
- | Some(result) =>
973
- switch (
974
- RegExp.Result.matches(result)->Array.get(0),
975
- RegExp.Result.matches(result)->Array.get(1),
976
- ) {
977
- | (Some(Some(chainIdStr)), Some(Some(effectName))) =>
978
- switch parseChainId(chainIdStr) {
979
- | Some(chainId) => Some((effectName, Chain(chainId)))
980
- | None => None
981
- }
982
- | _ => None
983
- }
984
- | None =>
985
- switch RegExp.exec(crossChainRe, tableName) {
986
- | Some(result) =>
987
- switch RegExp.Result.matches(result)->Array.get(0) {
988
- | Some(Some(effectName)) => Some((effectName, CrossChain))
989
- | _ => None
990
- }
991
- | None => None
992
- }
993
- }
994
-
995
- // Relative posix path within .envio/cache. Chain-scoped caches live one
996
- // directory level deep, named by chain id.
997
- let toCachePath = (~effectName, ~scope) =>
998
- switch scope {
999
- | CrossChain => effectName ++ ".tsv"
1000
- | Chain(chainId) => `${chainId->ChainId.toString}/${effectName}.tsv`
1001
- }
1002
953
  }
1003
954
 
1004
955
  let cacheOutputSchema = S.json(~validate=false)->(Utils.magic: S.t<JSON.t> => S.t<effectOutput>)
@@ -240,87 +240,8 @@ function toTableName(effectName, scope) {
240
240
  }
241
241
  }
242
242
 
243
- function parseChainId(str) {
244
- let chainId;
245
- try {
246
- chainId = Primitive_option.some(ChainId.normalizeOrThrow(str));
247
- } catch (exn) {
248
- chainId = undefined;
249
- }
250
- if (chainId === undefined) {
251
- return;
252
- }
253
- let chainId$1 = Primitive_option.valFromOption(chainId);
254
- if (ChainId.toString(chainId$1) === str) {
255
- return Primitive_option.some(chainId$1);
256
- }
257
- }
258
-
259
- let chainScopedRe = /^envio_([0-9]+)_effect_(.+)$/;
260
-
261
- let crossChainRe = /^envio_effect_(.+)$/;
262
-
263
- function fromTableName(tableName) {
264
- let result = chainScopedRe.exec(tableName);
265
- if (result == null) {
266
- let result$1 = crossChainRe.exec(tableName);
267
- if (result$1 == null) {
268
- return;
269
- }
270
- let match = result$1.slice(1)[0];
271
- if (match === undefined) {
272
- return;
273
- }
274
- let effectName = Primitive_option.valFromOption(match);
275
- if (effectName !== undefined) {
276
- return [
277
- effectName,
278
- "crossChain"
279
- ];
280
- } else {
281
- return;
282
- }
283
- }
284
- let match$1 = result.slice(1)[0];
285
- let match$2 = result.slice(1)[1];
286
- if (match$1 === undefined) {
287
- return;
288
- }
289
- let chainIdStr = Primitive_option.valFromOption(match$1);
290
- if (chainIdStr === undefined) {
291
- return;
292
- }
293
- if (match$2 === undefined) {
294
- return;
295
- }
296
- let effectName$1 = Primitive_option.valFromOption(match$2);
297
- if (effectName$1 === undefined) {
298
- return;
299
- }
300
- let chainId = parseChainId(chainIdStr);
301
- if (chainId !== undefined) {
302
- return [
303
- effectName$1,
304
- Primitive_option.valFromOption(chainId)
305
- ];
306
- }
307
- }
308
-
309
- function toCachePath(effectName, scope) {
310
- if (scope === "crossChain") {
311
- return effectName + ".tsv";
312
- } else {
313
- return ChainId.toString(scope) + `/` + effectName + `.tsv`;
314
- }
315
- }
316
-
317
243
  let EffectCache = {
318
- toTableName: toTableName,
319
- parseChainId: parseChainId,
320
- chainScopedRe: chainScopedRe,
321
- crossChainRe: crossChainRe,
322
- fromTableName: fromTableName,
323
- toCachePath: toCachePath
244
+ toTableName: toTableName
324
245
  };
325
246
 
326
247
  let cacheOutputSchema = S$RescriptSchema.json(false);
package/src/LoadLayer.res CHANGED
@@ -16,7 +16,12 @@ let loadById = (
16
16
  ~entityId,
17
17
  ) => {
18
18
  let inMemTable = indexerState->InMemoryStore.getInMemTable(~entityConfig, ~scope)
19
- switch inMemTable.latestEntityChangeById->Utils.Dict.dangerouslyGetNonOption(entityId) {
19
+ // The ids here are typed as strings but hold whatever scalar the id is.
20
+ let entityIdToKey =
21
+ inMemTable.entityIdToKey->(Utils.magic: (EntityId.t => string) => string => string)
22
+ switch inMemTable.latestEntityChangeById->Utils.Dict.dangerouslyGetNonOption(
23
+ entityIdToKey(entityId),
24
+ ) {
20
25
  // A row already in memory is final, so even a grouped call skips the load
21
26
  // manager: grouping it would only resolve it a tick later to the same value.
22
27
  | Some(change) => Promise.resolve(change->InMemoryTable.Entity.mapChangeToEntity)
@@ -53,14 +58,14 @@ let loadById = (
53
58
  //Set the entity in the in memory store
54
59
  for idx in 0 to dbEntities->Array.length - 1 {
55
60
  let entity = dbEntities->Array.getUnsafe(idx)
56
- entitiesMap->Dict.set(entity.id, entity)
61
+ entitiesMap->Dict.set(entityIdToKey(entity.id), entity)
57
62
  }
58
63
  let committedCheckpointId = indexerState->IndexerState.committedCheckpointIdFor(~scope)
59
64
  idsToLoad->Array.forEach(entityId => {
60
65
  inMemTable->InMemoryTable.Entity.initValue(
61
66
  ~committedCheckpointId,
62
- ~key=entityId,
63
- ~entity=entitiesMap->Utils.Dict.dangerouslyGetNonOption(entityId),
67
+ ~entityId=entityId->EntityId.unsafeOfString,
68
+ ~entity=entitiesMap->Utils.Dict.dangerouslyGetNonOption(entityIdToKey(entityId)),
64
69
  )
65
70
  })
66
71
 
@@ -77,7 +82,7 @@ let loadById = (
77
82
  ~key,
78
83
  ~load,
79
84
  ~shouldGroup,
80
- ~hasher=LoadManager.noopHasher,
85
+ ~hasher=entityIdToKey,
81
86
  ~getUnsafeInMemory=inMemTable->InMemoryTable.Entity.getUnsafe,
82
87
  ~hasInMemory=hash => inMemTable.latestEntityChangeById->Dict.has(hash),
83
88
  ~input=entityId,
@@ -140,7 +145,7 @@ let rec executeWithRateLimit = (
140
145
  }
141
146
 
142
147
  | Some(state) =>
143
- let now = Date.now()
148
+ let now = state.clock.now()
144
149
 
145
150
  // Check if we need to reset the window
146
151
  if now >= state.windowStartTime +. state.durationMs->Int.toFloat {
@@ -188,7 +193,8 @@ let rec executeWithRateLimit = (
188
193
  | None =>
189
194
  millisUntilReset :=
190
195
  (state.windowStartTime +. state.durationMs->Int.toFloat -. now)->Float.toInt
191
- let p = Utils.delay(millisUntilReset.contents)
196
+ let p =
197
+ state.clock->Clock.delay(~label="effect rate limit window", millisUntilReset.contents)
192
198
  state.nextWindowPromise = Some(p)
193
199
  p
194
200
  }
@@ -387,7 +393,7 @@ let loadByParsedFilter = (
387
393
  entities->Array.forEach(entity => {
388
394
  inMemTable->InMemoryTable.Entity.initValue(
389
395
  ~committedCheckpointId,
390
- ~key=entity.id,
396
+ ~entityId=entity.id->EntityId.unsafeOfString,
391
397
  ~entity=Some(entity),
392
398
  )
393
399
  })
@@ -1,5 +1,6 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as Clock from "./Clock.res.mjs";
3
4
  import * as Utils from "./Utils.res.mjs";
4
5
  import * as Logger from "./Logger.res.mjs";
5
6
  import * as ChainId from "./ChainId.res.mjs";
@@ -27,7 +28,8 @@ function scopeKeySuffix(scope) {
27
28
 
28
29
  function loadById(loadManager, persistence, entityConfig, scope, indexerState, shouldGroup, entityId) {
29
30
  let inMemTable = InMemoryStore.getInMemTable(indexerState, entityConfig, scope);
30
- let change = inMemTable.latestEntityChangeById[entityId];
31
+ let entityIdToKey = inMemTable.entityIdToKey;
32
+ let change = inMemTable.latestEntityChangeById[entityIdToKey(entityId)];
31
33
  if (change !== undefined) {
32
34
  return Promise.resolve(InMemoryTable.Entity.mapChangeToEntity(change));
33
35
  }
@@ -50,13 +52,13 @@ function loadById(loadManager, persistence, entityConfig, scope, indexerState, s
50
52
  let entitiesMap = {};
51
53
  for (let idx = 0, idx_finish = dbEntities.length; idx < idx_finish; ++idx) {
52
54
  let entity = dbEntities[idx];
53
- entitiesMap[entity.id] = entity;
55
+ entitiesMap[entityIdToKey(entity.id)] = entity;
54
56
  }
55
57
  let committedCheckpointId = IndexerState.committedCheckpointIdFor(indexerState, scope);
56
- idsToLoad.forEach(entityId => InMemoryTable.Entity.initValue(inMemTable, committedCheckpointId, entityId, entitiesMap[entityId]));
58
+ idsToLoad.forEach(entityId => InMemoryTable.Entity.initValue(inMemTable, committedCheckpointId, entityId, entitiesMap[entityIdToKey(entityId)]));
57
59
  return IndexerState.endStorageLoad(indexerState, timerRef, storageName, key, idsToLoad.length, dbEntities.length);
58
60
  };
59
- return LoadManager.call(loadManager, entityId, key, load, LoadManager.noopHasher, shouldGroup, hash => hash in inMemTable.latestEntityChangeById, InMemoryTable.Entity.getUnsafe(inMemTable));
61
+ return LoadManager.call(loadManager, entityId, key, load, entityIdToKey, shouldGroup, hash => hash in inMemTable.latestEntityChangeById, InMemoryTable.Entity.getUnsafe(inMemTable));
60
62
  }
61
63
 
62
64
  function callEffect(effect, arg, inMemTable, timerRef, onError) {
@@ -69,7 +71,7 @@ function executeWithRateLimit(effect, effectArgs, inMemTable, onError, isFromQue
69
71
  let promises = [];
70
72
  let state = inMemTable.rateLimitState;
71
73
  if (state !== undefined) {
72
- let now = Date.now();
74
+ let now = state.clock.now();
73
75
  if (now >= state.windowStartTime + state.durationMs) {
74
76
  state.availableCalls = state.callsPerDuration;
75
77
  state.windowStartTime = now;
@@ -98,7 +100,7 @@ function executeWithRateLimit(effect, effectArgs, inMemTable, onError, isFromQue
98
100
  nextWindowPromise = p;
99
101
  } else {
100
102
  millisUntilReset.contents = state.windowStartTime + state.durationMs - now | 0;
101
- let p$1 = Utils.delay(millisUntilReset.contents);
103
+ let p$1 = Clock.delay(state.clock, "effect rate limit window", millisUntilReset.contents);
102
104
  state.nextWindowPromise = p$1;
103
105
  nextWindowPromise = p$1;
104
106
  }
package/src/Main.res.mjs CHANGED
@@ -413,7 +413,7 @@ async function startIndexer(config, logger, persistence, resetOpt, isTestOpt, ex
413
413
  Metrics.startRuntimeCollectors();
414
414
  Server.startServer(logger, getMetrics, envioVersion, () => Persistence.getInitializedStorageOrThrow(persistence$1).dumpEffectCache().then(prim => {}), Metrics.collectRuntime, isDevelopmentMode);
415
415
  }
416
- let state = IndexerState.makeFromDbState(config$2, logger, persistence$1, Persistence.getInitializedState(persistence$1), registrationsByChainId, isDevelopmentMode, shouldUseTui, exitAfterFirstEventBlock, undefined, undefined, Stdlib_Option.mapOr(Worker.config, false, worker => worker.holdRealtime), onError, undefined);
416
+ let state = IndexerState.makeFromDbState(config$2, logger, persistence$1, Persistence.getInitializedState(persistence$1), registrationsByChainId, isDevelopmentMode, shouldUseTui, exitAfterFirstEventBlock, undefined, undefined, Stdlib_Option.mapOr(Worker.config, false, worker => worker.holdRealtime), onError, undefined, undefined);
417
417
  if (shouldUseTui) {
418
418
  Tui.start(config$2, logger, () => IndexerState.toMetrics(state));
419
419
  }
@@ -117,8 +117,6 @@ type storage = {
117
117
  // carries a matching row — storage doesn't interpret it.
118
118
  initialize: (
119
119
  ~chainConfigs: array<Config.chain>=?,
120
- ~entities: array<Internal.entityConfig>=?,
121
- ~enums: array<Table.enumConfig<Table.enum>>=?,
122
120
  ~contractMapping: ContractMapping.t,
123
121
  ~envioInfo: JSON.t,
124
122
  ) => promise<initialState>,
@@ -129,16 +127,11 @@ type storage = {
129
127
  // Brings an initialized storage a chain it doesn't have yet: its row, its
130
128
  // partitions and its config addresses, as `initialize` would have created
131
129
  // them. The chains already there are left untouched.
132
- addChain: (
133
- ~chainConfig: Config.chain,
134
- ~entities: array<Internal.entityConfig>,
135
- ~contractMapping: ContractMapping.t,
136
- ) => promise<unit>,
130
+ addChain: (~chainConfig: Config.chain, ~contractMapping: ContractMapping.t) => promise<unit>,
137
131
  // `chainIds` is what this run drives. An isolated run resumes a subset of
138
132
  // the stored chains, and ClickHouse's resume trims past each resumed chain's
139
133
  // checkpoint, so chains a sibling process is still writing must stay out.
140
134
  resumeInitialState: (
141
- ~entities: array<Internal.entityConfig>,
142
135
  ~chainIds: array<ChainId.t>,
143
136
  ~contractMapping: ContractMapping.t,
144
137
  ) => promise<initialState>,
@@ -159,19 +152,12 @@ type storage = {
159
152
  // never runs `finalizeBackfill`: an index dropped or invalidated while it was
160
153
  // down would otherwise never be rebuilt. Best-effort, and safe to run with
161
154
  // indexing live.
162
- ensureSchemaIndexes: (
163
- ~entities: array<Internal.entityConfig>,
164
- ~chainIds: array<ChainId.t>,
165
- ) => promise<unit>,
155
+ ensureSchemaIndexes: (~chainIds: array<ChainId.t>) => promise<unit>,
166
156
  // Creates every index the schema promises for the given chains, then stamps
167
157
  // `ready_at` on them. Called once those chains finish backfill. The indexes
168
158
  // are committed one at a time so a failure part way through doesn't undo the
169
159
  // ones already built; `ready_at` is only written once they all verify.
170
- finalizeBackfill: (
171
- ~entities: array<Internal.entityConfig>,
172
- ~chainIds: array<ChainId.t>,
173
- ~readyAt: Date.t,
174
- ) => promise<unit>,
160
+ finalizeBackfill: (~chainIds: array<ChainId.t>, ~readyAt: Date.t) => promise<unit>,
175
161
  // This is to download cache from the database to .envio/cache
176
162
  dumpEffectCache: unit => promise<unit>,
177
163
  reset: unit => promise<unit>,
@@ -190,7 +176,6 @@ type storage = {
190
176
  getRollbackTargetCheckpoint: (
191
177
  ~reorgChainId: ChainId.t,
192
178
  ~lastKnownValidBlockNumber: int,
193
- ~sequence: CheckpointSequence.t,
194
179
  ) => promise<option<Internal.checkpointId>>,
195
180
  // Get rollback progress diff
196
181
  getRollbackProgressDiff: (
@@ -214,7 +199,6 @@ type storage = {
214
199
  ~batch: Batch.t,
215
200
  ~rollback: option<rollback>,
216
201
  ~config: Config.t,
217
- ~allEntities: array<Internal.entityConfig>,
218
202
  ~updatedEffectsCache: array<updatedEffectCache>,
219
203
  ~updatedEntities: array<updatedEntity>,
220
204
  // Addresses this batch registered, with the checkpoint that covers them.
@@ -339,8 +323,6 @@ let init = {
339
323
  throw(Refused(Config.setUpMeanwhileMessage))
340
324
  }
341
325
  let initialState = await persistence.storage.initialize(
342
- ~entities=persistence.allEntities,
343
- ~enums=persistence.allEnums,
344
326
  ~chainConfigs,
345
327
  ~contractMapping,
346
328
  ~envioInfo,
@@ -397,13 +379,11 @@ let init = {
397
379
  )->Array.getUnsafe(0)
398
380
  await persistence.storage.addChain(
399
381
  ~chainConfig,
400
- ~entities=persistence.allEntities,
401
382
  ~contractMapping=stored.contractMapping,
402
383
  )
403
384
  }
404
385
  if resume {
405
386
  let initialState = await persistence.storage.resumeInitialState(
406
- ~entities=persistence.allEntities,
407
387
  ~chainIds=chainConfigs->Array.map(chain => chain.id),
408
388
  ~contractMapping=stored.contractMapping,
409
389
  )
@@ -94,7 +94,7 @@ async function init(persistence, logger, chainConfigs, contractMapping, envioInf
94
94
  Error: new Error()
95
95
  };
96
96
  }
97
- let initialState = await persistence.storage.initialize(chainConfigs$1, persistence.allEntities, persistence.allEnums, contractMapping, envioInfo);
97
+ let initialState = await persistence.storage.initialize(chainConfigs$1, contractMapping, envioInfo);
98
98
  Logger.info(logger, `The indexer storage is ready. Starting indexing!`, undefined);
99
99
  persistence.storageStatus = {
100
100
  TAG: "Ready",
@@ -136,11 +136,11 @@ async function init(persistence, logger, chainConfigs, contractMapping, envioInf
136
136
  chainId: chainConfig.id
137
137
  });
138
138
  let chainConfig$1 = (await StartBlockResolver.resolveAllOrThrow([chainConfig], logger, lowercaseAddresses, startBlockRetry, sourceTiming))[0];
139
- await persistence.storage.addChain(chainConfig$1, persistence.allEntities, stored.contractMapping);
139
+ await persistence.storage.addChain(chainConfig$1, stored.contractMapping);
140
140
  }
141
141
  }
142
142
  if (resume) {
143
- let initialState$1 = await persistence.storage.resumeInitialState(persistence.allEntities, chainConfigs.map(chain => chain.id), stored.contractMapping);
143
+ let initialState$1 = await persistence.storage.resumeInitialState(chainConfigs.map(chain => chain.id), stored.contractMapping);
144
144
  persistence.storageStatus = {
145
145
  TAG: "Ready",
146
146
  _0: initialState$1
package/src/Rollback.res CHANGED
@@ -130,7 +130,6 @@ and executeRollback = async (
130
130
  switch await (state->IndexerState.persistence).storage.getRollbackTargetCheckpoint(
131
131
  ~reorgChainId=reorgChain,
132
132
  ~lastKnownValidBlockNumber=rollbackTargetBlockNumber,
133
- ~sequence=(state->IndexerState.config).checkpointSequence,
134
133
  ) {
135
134
  | Some(checkpointId) => checkpointId
136
135
  | None => 0n
@@ -42,7 +42,7 @@ async function executeRollback(state, reorgChain, rollbackTargetBlockNumber, sch
42
42
  Logger.info(logger, "Started rollback on reorg", undefined);
43
43
  ChainState.setRollbackTargetBlock(IndexerState.getChainState(state, reorgChain), rollbackTargetBlockNumber);
44
44
  await Writing.flush(state);
45
- let checkpointId = await IndexerState.persistence(state).storage.getRollbackTargetCheckpoint(reorgChain, rollbackTargetBlockNumber, IndexerState.config(state).checkpointSequence);
45
+ let checkpointId = await IndexerState.persistence(state).storage.getRollbackTargetCheckpoint(reorgChain, rollbackTargetBlockNumber);
46
46
  let rollbackTargetCheckpointId = checkpointId !== undefined ? checkpointId : 0n;
47
47
  let config = IndexerState.config(state);
48
48
  let next = RollbackFloors.make(config.checkpointSequence, ChainMap.keys(config.chainMap), reorgChain, rollbackTargetCheckpointId, rollbackTargetBlockNumber);
@@ -16,9 +16,7 @@ let make = (
16
16
  ) => {
17
17
  if maxReorgDepth > 0 && shouldRollbackOnReorg {
18
18
  let checkpointIds = Utils.Array.jsArrayCreate(chainReorgCheckpoints->Array.length)
19
- let checkpointBlockNumbers = Utils.Array.jsArrayCreate(
20
- chainReorgCheckpoints->Array.length,
21
- )
19
+ let checkpointBlockNumbers = Utils.Array.jsArrayCreate(chainReorgCheckpoints->Array.length)
22
20
  chainReorgCheckpoints->Array.forEachWithIndex((checkpoint, idx) => {
23
21
  checkpointIds->Array.setUnsafe(idx, checkpoint.checkpointId)
24
22
  checkpointBlockNumbers->Array.setUnsafe(idx, checkpoint.blockNumber)
@@ -36,7 +34,9 @@ let make = (
36
34
  let getSafeCheckpointId = (safeCheckpointTracking: t, ~sourceBlockNumber: int): option<
37
35
  Internal.checkpointId,
38
36
  > => {
39
- let safeBlockNumber = sourceBlockNumber - safeCheckpointTracking.maxReorgDepth
37
+ // The first block reorg detection compares is itself reorgable, so the safe
38
+ // block is the one below it.
39
+ let safeBlockNumber = sourceBlockNumber - safeCheckpointTracking.maxReorgDepth - 1
40
40
 
41
41
  switch safeCheckpointTracking.checkpointIds {
42
42
  | [] => None
@@ -20,7 +20,7 @@ function make(maxReorgDepth, shouldRollbackOnReorg, chainReorgCheckpoints) {
20
20
  }
21
21
 
22
22
  function getSafeCheckpointId(safeCheckpointTracking, sourceBlockNumber) {
23
- let safeBlockNumber = sourceBlockNumber - safeCheckpointTracking.maxReorgDepth | 0;
23
+ let safeBlockNumber = (sourceBlockNumber - safeCheckpointTracking.maxReorgDepth | 0) - 1 | 0;
24
24
  let match = safeCheckpointTracking.checkpointIds;
25
25
  if (match.length === 0) {
26
26
  return;