envio 3.11.0 → 3.12.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 (89) hide show
  1. package/index.d.ts +3 -0
  2. package/package.json +6 -6
  3. package/src/Batch.res +111 -78
  4. package/src/Batch.res.mjs +53 -46
  5. package/src/BatchProcessing.res +1 -11
  6. package/src/BatchProcessing.res.mjs +1 -4
  7. package/src/ChainState.res +72 -19
  8. package/src/ChainState.res.mjs +50 -12
  9. package/src/ChainState.resi +5 -0
  10. package/src/Config.res +58 -37
  11. package/src/Config.res.mjs +63 -53
  12. package/src/CrossChainState.res +49 -52
  13. package/src/CrossChainState.res.mjs +25 -23
  14. package/src/CrossChainState.resi +6 -13
  15. package/src/EffectState.res +13 -4
  16. package/src/EffectState.resi +7 -1
  17. package/src/EventProcessing.res +6 -6
  18. package/src/EventProcessing.res.mjs +6 -6
  19. package/src/FetchState.res +14 -0
  20. package/src/FetchState.res.mjs +14 -0
  21. package/src/FinalizeBackfill.res +16 -12
  22. package/src/FinalizeBackfill.res.mjs +3 -3
  23. package/src/HistoryPolicy.res +46 -0
  24. package/src/HistoryPolicy.res.mjs +50 -0
  25. package/src/InMemoryStore.res +30 -32
  26. package/src/InMemoryStore.res.mjs +33 -28
  27. package/src/IndexerState.res +66 -45
  28. package/src/IndexerState.res.mjs +57 -29
  29. package/src/IndexerState.resi +8 -13
  30. package/src/Internal.res +4 -2
  31. package/src/LoadLayer.res +11 -4
  32. package/src/LoadLayer.res.mjs +8 -5
  33. package/src/Main.res +25 -24
  34. package/src/Main.res.mjs +24 -25
  35. package/src/Metrics.res +49 -1
  36. package/src/Metrics.res.mjs +26 -3
  37. package/src/Persistence.res +55 -16
  38. package/src/Persistence.res.mjs +18 -3
  39. package/src/PgStorage.res +194 -118
  40. package/src/PgStorage.res.mjs +132 -72
  41. package/src/PruneStaleHistory.res +9 -15
  42. package/src/PruneStaleHistory.res.mjs +11 -20
  43. package/src/Rollback.res +9 -14
  44. package/src/Rollback.res.mjs +5 -3
  45. package/src/Sink.res +7 -5
  46. package/src/Sink.res.mjs +4 -4
  47. package/src/Staging.res +298 -0
  48. package/src/Staging.res.mjs +257 -0
  49. package/src/TestIndexer.res +5 -5
  50. package/src/TestIndexer.res.mjs +7 -6
  51. package/src/UserContext.res +7 -4
  52. package/src/UserContext.res.mjs +5 -2
  53. package/src/Utils.res +9 -0
  54. package/src/Utils.res.mjs +9 -0
  55. package/src/Writing.res +34 -20
  56. package/src/Writing.res.mjs +15 -20
  57. package/src/bindings/ClickHouse.res +41 -34
  58. package/src/bindings/ClickHouse.res.mjs +36 -30
  59. package/src/bindings/ClickHouseSink.res +45 -186
  60. package/src/bindings/ClickHouseSink.res.mjs +21 -174
  61. package/src/db/CheckpointSequence.res +109 -0
  62. package/src/db/CheckpointSequence.res.mjs +99 -0
  63. package/src/db/EntityHistory.res +6 -5
  64. package/src/db/EntityHistory.res.mjs +5 -5
  65. package/src/db/Frontier.res +86 -0
  66. package/src/db/Frontier.res.mjs +126 -0
  67. package/src/db/InternalTable.res +145 -27
  68. package/src/db/InternalTable.res.mjs +115 -29
  69. package/src/db/RollbackFloors.res +34 -41
  70. package/src/db/RollbackFloors.res.mjs +27 -71
  71. package/src/sources/BlockStore.res +3 -0
  72. package/src/sources/EvmHyperSyncSource.res +3 -1
  73. package/src/sources/EvmHyperSyncSource.res.mjs +2 -1
  74. package/src/sources/HyperSync.res +2 -0
  75. package/src/sources/HyperSync.res.mjs +1 -0
  76. package/src/sources/HyperSync.resi +1 -0
  77. package/src/sources/HyperSyncClient.res +3 -0
  78. package/src/sources/RequestStat.res +8 -1
  79. package/src/sources/Source.res +4 -3
  80. package/src/sources/SourceManager.res +30 -10
  81. package/src/sources/SourceManager.res.mjs +24 -7
  82. package/src/sources/SourceManager.resi +2 -0
  83. package/src/tui/Tui.res +56 -143
  84. package/src/tui/Tui.res.mjs +70 -166
  85. package/src/tui/components/TuiData.res +61 -16
  86. package/src/tui/components/TuiData.res.mjs +53 -15
  87. package/svm.schema.json +0 -7
  88. package/src/db/CheckpointBounds.res +0 -53
  89. package/src/db/CheckpointBounds.res.mjs +0 -44
package/src/Main.res CHANGED
@@ -488,7 +488,12 @@ let getGlobalIndexer = (): 'indexer => {
488
488
  Utils.Proxy.make(Utils.Object.createNullObject(), traps)->(Utils.magic: {..} => 'indexer)
489
489
  }
490
490
 
491
- let startServer = (~getState, ~persistence: Persistence.t, ~isDevelopmentMode: bool) => {
491
+ let startServer = (
492
+ ~getMetrics: unit => option<Metrics.t>,
493
+ ~envioVersion: string,
494
+ ~persistence: Persistence.t,
495
+ ~isDevelopmentMode: bool,
496
+ ) => {
492
497
  open Express
493
498
 
494
499
  let app = make()
@@ -520,10 +525,20 @@ let startServer = (~getState, ~persistence: Persistence.t, ~isDevelopmentMode: b
520
525
  })
521
526
 
522
527
  app->get("/console/state", (_req, res) => {
523
- let state = if isDevelopmentMode {
524
- getState()
525
- } else {
528
+ let state = if !isDevelopmentMode {
526
529
  Disabled({})
530
+ } else {
531
+ switch getMetrics() {
532
+ | None => Initializing({})
533
+ | Some(metrics) =>
534
+ Active({
535
+ envioVersion,
536
+ chains: metrics.chains->Array.map(toChainData),
537
+ indexerStartTime: metrics.startTime,
538
+ isPreRegisteringDynamicContracts: false,
539
+ rollbackOnReorg: metrics.rollbackEnabled,
540
+ })
541
+ }
527
542
  }
528
543
 
529
544
  res->json(state->S.reverseConvertToJsonOrThrow(stateSchema))
@@ -543,10 +558,7 @@ let startServer = (~getState, ~persistence: Persistence.t, ~isDevelopmentMode: b
543
558
 
544
559
  app->get("/metrics", (_req, res) => {
545
560
  res->set("Content-Type", Metrics.contentType)
546
- let _ =
547
- res->endWithData(
548
- Metrics.collect(~metrics=getIndexerState()->Option.map(IndexerState.toMetrics)),
549
- )
561
+ let _ = res->endWithData(Metrics.collect(~metrics=getMetrics()))
550
562
  })
551
563
 
552
564
  app->get("/metrics/runtime", (_req, res) => {
@@ -648,6 +660,7 @@ let start = async (
648
660
  ~resetCommand=isDevelopmentMode ? "envio dev -r" : "envio start -r",
649
661
  ~runCommand=Some(isDevelopmentMode ? "envio dev" : "envio start"),
650
662
  ~lowercaseAddresses=config.lowercaseAddresses,
663
+ ~requireInitialized=config.isolated,
651
664
  )
652
665
 
653
666
  // Loads user handler files, which register handler/contractRegister/where
@@ -683,22 +696,10 @@ let start = async (
683
696
  }
684
697
  let envioVersion = Utils.EnvioPackage.value.version
685
698
 
699
+ let getMetrics = () => getIndexerState()->Option.map(IndexerState.toMetrics)
700
+
686
701
  if !isTest {
687
- startServer(~persistence, ~isDevelopmentMode, ~getState=() =>
688
- switch getIndexerState() {
689
- | None => Initializing({})
690
- | Some(state) => {
691
- let chains = (state->IndexerState.toMetrics).chains->Array.map(toChainData)
692
- Active({
693
- envioVersion,
694
- chains,
695
- indexerStartTime: state->IndexerState.indexerStartTime,
696
- isPreRegisteringDynamicContracts: false,
697
- rollbackOnReorg: config.shouldRollbackOnReorg,
698
- })
699
- }
700
- }
701
- )
702
+ startServer(~persistence, ~isDevelopmentMode, ~envioVersion, ~getMetrics)
702
703
  }
703
704
 
704
705
  let state = IndexerState.makeFromDbState(
@@ -712,7 +713,7 @@ let start = async (
712
713
  ~onError,
713
714
  )
714
715
  if shouldUseTui {
715
- let _rerender = Tui.start(~getState=() => state)
716
+ let _rerender = Tui.start(~config, ~getMetrics=() => state->IndexerState.toMetrics)
716
717
  }
717
718
  setIndexerState(state)
718
719
  state->IndexerLoop.start
package/src/Main.res.mjs CHANGED
@@ -402,7 +402,7 @@ function getGlobalIndexer() {
402
402
  return new Proxy(Object.create(null), traps);
403
403
  }
404
404
 
405
- function startServer(getState, persistence, isDevelopmentMode) {
405
+ function startServer(getMetrics, envioVersion, persistence, isDevelopmentMode) {
406
406
  let app = Express();
407
407
  let consoleCorsMiddleware = (req, res, next) => {
408
408
  let origin = req.headers["origin"];
@@ -425,9 +425,24 @@ function startServer(getState, persistence, isDevelopmentMode) {
425
425
  res.sendStatus(200);
426
426
  });
427
427
  app.get("/console/state", (_req, res) => {
428
- let state = isDevelopmentMode ? getState() : ({
428
+ let state;
429
+ if (isDevelopmentMode) {
430
+ let metrics = getMetrics();
431
+ state = metrics !== undefined ? ({
432
+ status: "active",
433
+ envioVersion: envioVersion,
434
+ chains: metrics.chains.map(toChainData),
435
+ indexerStartTime: metrics.startTime,
436
+ isPreRegisteringDynamicContracts: false,
437
+ rollbackOnReorg: metrics.rollbackEnabled
438
+ }) : ({
439
+ status: "initializing"
440
+ });
441
+ } else {
442
+ state = {
429
443
  status: "disabled"
430
- });
444
+ };
445
+ }
431
446
  res.json(S$RescriptSchema.reverseConvertToJsonOrThrow(state, stateSchema));
432
447
  });
433
448
  app.post("/console/syncCache", (_req, res) => {
@@ -442,7 +457,7 @@ function startServer(getState, persistence, isDevelopmentMode) {
442
457
  Metrics.startRuntimeCollectors();
443
458
  app.get("/metrics", (_req, res) => {
444
459
  res.set("Content-Type", Metrics.contentType);
445
- res.end(Metrics.collect(Stdlib_Option.map(EnvioGlobal.value.indexerState, IndexerState.toMetrics)));
460
+ res.end(Metrics.collect(getMetrics()));
446
461
  });
447
462
  app.get("/metrics/runtime", (_req, res) => {
448
463
  res.set("Content-Type", Metrics.contentType);
@@ -467,7 +482,7 @@ function getEnvioInfo() {
467
482
  async function migrate(reset) {
468
483
  let config = Config.load();
469
484
  let persistence = PgStorage.makePersistenceFromConfig(config, undefined);
470
- await Persistence.init(persistence, ChainMap.values(config.chainMap), config.contractMapping, Config.stripSensitiveData(Config.getPublicConfigJson()), "envio local db-migrate setup", undefined, reset, config.lowercaseAddresses, "Once");
485
+ await Persistence.init(persistence, ChainMap.values(config.chainMap), config.contractMapping, Config.stripSensitiveData(Config.getPublicConfigJson()), "envio local db-migrate setup", undefined, reset, config.lowercaseAddresses, undefined, "Once");
471
486
  return await persistence.storage.close();
472
487
  }
473
488
 
@@ -494,7 +509,7 @@ async function start(persistence, resetOpt, isTestOpt, exitAfterFirstEventBlockO
494
509
  let isDevelopmentMode = !isTest && config.isDev;
495
510
  let persistence$1 = persistence !== undefined ? persistence : PgStorage.makePersistenceFromConfig(config, undefined);
496
511
  EnvioGlobal.value.persistence = Primitive_option.some(persistence$1);
497
- await Persistence.init(persistence$1, ChainMap.values(config.chainMap), config.contractMapping, Config.stripSensitiveData(Config.getPublicConfigJson()), isDevelopmentMode ? "envio dev -r" : "envio start -r", isDevelopmentMode ? "envio dev" : "envio start", reset, config.lowercaseAddresses, undefined);
512
+ await Persistence.init(persistence$1, ChainMap.values(config.chainMap), config.contractMapping, Config.stripSensitiveData(Config.getPublicConfigJson()), isDevelopmentMode ? "envio dev -r" : "envio start -r", isDevelopmentMode ? "envio dev" : "envio start", reset, config.lowercaseAddresses, config.isolated, undefined);
498
513
  let registrationsByChainId = await HandlerLoader.registerAllHandlers(config);
499
514
  let config$1;
500
515
  if (isTest) {
@@ -519,29 +534,13 @@ async function start(persistence, resetOpt, isTestOpt, exitAfterFirstEventBlockO
519
534
  });
520
535
  };
521
536
  let envioVersion = Utils.EnvioPackage.value.version;
537
+ let getMetrics = () => Stdlib_Option.map(EnvioGlobal.value.indexerState, IndexerState.toMetrics);
522
538
  if (!isTest) {
523
- startServer(() => {
524
- let state = EnvioGlobal.value.indexerState;
525
- if (state === undefined) {
526
- return {
527
- status: "initializing"
528
- };
529
- }
530
- let state$1 = Primitive_option.valFromOption(state);
531
- let chains = IndexerState.toMetrics(state$1).chains.map(toChainData);
532
- return {
533
- status: "active",
534
- envioVersion: envioVersion,
535
- chains: chains,
536
- indexerStartTime: IndexerState.indexerStartTime(state$1),
537
- isPreRegisteringDynamicContracts: false,
538
- rollbackOnReorg: config$2.shouldRollbackOnReorg
539
- };
540
- }, persistence$1, isDevelopmentMode);
539
+ startServer(getMetrics, envioVersion, persistence$1, isDevelopmentMode);
541
540
  }
542
541
  let state = IndexerState.makeFromDbState(config$2, persistence$1, Persistence.getInitializedState(persistence$1), registrationsByChainId, isDevelopmentMode, shouldUseTui, exitAfterFirstEventBlock, undefined, undefined, onError, undefined);
543
542
  if (shouldUseTui) {
544
- Tui.start(() => state);
543
+ Tui.start(config$2, () => IndexerState.toMetrics(state));
545
544
  }
546
545
  EnvioGlobal.value.indexerState = Primitive_option.some(state);
547
546
  IndexerLoop.start(state);
package/src/Metrics.res CHANGED
@@ -20,6 +20,7 @@ type chainMetrics = {
20
20
  // Raw committed progress (may be -1), unlike the optional latestProcessedBlock.
21
21
  progressBlockNumber: int,
22
22
  progressLatencyMs: option<int>,
23
+ progressBlockTime: option<int>,
23
24
  concurrency: int,
24
25
  partitionsCount: int,
25
26
  bufferSize: int,
@@ -36,8 +37,18 @@ type chainMetrics = {
36
37
  reorgCount: int,
37
38
  reorgDetectedBlock: option<int>,
38
39
  rollbackTargetBlock: option<int>,
40
+ rateLimitTimeMs: float,
41
+ rateLimitResetInMs: option<float>,
39
42
  }
40
43
 
44
+ // Mirrors `ChainState.hasProcessedToEndblock`, which is what clamps
45
+ // `knownHeight` to the end block — the two must agree.
46
+ let hasProcessedToEndblock = (m: chainMetrics) =>
47
+ switch m.endBlock {
48
+ | Some(endBlock) => m.progressBlockNumber >= endBlock
49
+ | None => false
50
+ }
51
+
41
52
  type handlerMetrics = {
42
53
  contract: string,
43
54
  event: string,
@@ -90,6 +101,10 @@ type sourceRequestMetrics = {
90
101
  method: string,
91
102
  count: int,
92
103
  seconds: float,
104
+ // None for methods whose responses aren't measured in blocks; they render no
105
+ // block series at all.
106
+ responseBlocks: option<int>,
107
+ emptyResponseCount: int,
93
108
  }
94
109
 
95
110
  type sourceHeightMetrics = {
@@ -249,7 +264,16 @@ let renderMetrics = (b: builder, metrics: t) => {
249
264
  | Some(existing) =>
250
265
  byLabels->Dict.set(
251
266
  labels,
252
- {...existing, count: existing.count + s.count, seconds: existing.seconds +. s.seconds},
267
+ {
268
+ ...existing,
269
+ count: existing.count + s.count,
270
+ seconds: existing.seconds +. s.seconds,
271
+ responseBlocks: switch (existing.responseBlocks, s.responseBlocks) {
272
+ | (Some(a), Some(b)) => Some(a + b)
273
+ | (some, None) | (None, some) => some
274
+ },
275
+ emptyResponseCount: existing.emptyResponseCount + s.emptyResponseCount,
276
+ },
253
277
  )
254
278
  | None => byLabels->Dict.set(labels, s)
255
279
  }
@@ -509,6 +533,23 @@ let renderMetrics = (b: builder, metrics: t) => {
509
533
  ~entries=sourceRequests,
510
534
  ~value=s => s.seconds !== 0. ? Some(s.seconds) : None,
511
535
  )
536
+ b->seriesOpt(
537
+ ~name="envio_source_response_blocks_total",
538
+ ~help="The number of blocks a data source returned, summed over its responses. Counted as the source sent them, before the indexer drops the blocks no event needs. Only sources whose responses are measured in blocks report it.",
539
+ ~kind="counter",
540
+ ~entries=sourceRequests,
541
+ ~value=s => s.responseBlocks->Option.map(Int.toFloat),
542
+ )
543
+ // Rendered flat at zero for any method that reports blocks: a chain scanning
544
+ // ranges it finds nothing in is the reading this exists for, and a series
545
+ // that only appears once the first empty response lands can't be alerted on.
546
+ b->seriesOpt(
547
+ ~name="envio_source_response_empty_total",
548
+ ~help="The number of responses that came back with no blocks at all — a range the source scanned and matched nothing in. Compare against envio_source_request_total for the share of requests that returned nothing.",
549
+ ~kind="counter",
550
+ ~entries=sourceRequests,
551
+ ~value=s => s.responseBlocks->Option.map(_ => s.emptyResponseCount->Int.toFloat),
552
+ )
512
553
  if heightStreamConnects->Array.length > 0 {
513
554
  b->series(
514
555
  ~name="envio_source_height_stream_connects_total",
@@ -619,6 +660,13 @@ let renderMetrics = (b: builder, metrics: t) => {
619
660
  ~entries=chains,
620
661
  ~value=m => m.numEventsProcessed,
621
662
  )
663
+ b->seriesOpt(
664
+ ~name="envio_progress_block_time_seconds",
665
+ ~help="Unix timestamp of the block the chain has processed up to. Subtract it from the scrape time for how far behind chain time the indexer is, which stays honest when the data source itself is behind the chain. Best effort in realtime mode, absent during backfill.",
666
+ ~kind="gauge",
667
+ ~entries=chains,
668
+ ~value=m => m.progressBlockTime->Option.map(Int.toFloat),
669
+ )
622
670
  b->seriesOpt(
623
671
  ~name="envio_progress_latency",
624
672
  ~help="The latency in milliseconds between the latest processed event creation and the time it was written to storage.",
@@ -9,6 +9,15 @@ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
9
9
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
10
10
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
11
11
 
12
+ function hasProcessedToEndblock(m) {
13
+ let endBlock = m.endBlock;
14
+ if (endBlock !== undefined) {
15
+ return m.progressBlockNumber >= endBlock;
16
+ } else {
17
+ return false;
18
+ }
19
+ }
20
+
12
21
  function formatValue(value) {
13
22
  return (Math.round(value * 1000) / 1000).toString();
14
23
  }
@@ -96,16 +105,26 @@ function renderMetrics(b, metrics) {
96
105
  ]);
97
106
  let existing = byLabels[labels];
98
107
  if (existing !== undefined) {
108
+ let match = existing.responseBlocks;
109
+ let match$1 = s.responseBlocks;
99
110
  byLabels[labels] = {
100
111
  source: existing.source,
101
112
  chainId: existing.chainId,
102
113
  method: existing.method,
103
114
  count: existing.count + s.count | 0,
104
- seconds: existing.seconds + s.seconds
115
+ seconds: existing.seconds + s.seconds,
116
+ responseBlocks: match !== undefined ? (
117
+ match$1 !== undefined ? match + match$1 | 0 : (
118
+ match$1 !== undefined ? match$1 : match
119
+ )
120
+ ) : (
121
+ match$1 !== undefined ? match$1 : match
122
+ ),
123
+ emptyResponseCount: existing.emptyResponseCount + s.emptyResponseCount | 0
105
124
  };
106
- } else {
107
- byLabels[labels] = s;
125
+ return;
108
126
  }
127
+ byLabels[labels] = s;
109
128
  });
110
129
  let sourceRequests = Object.entries(byLabels);
111
130
  let byLabels$1 = {};
@@ -176,6 +195,8 @@ function renderMetrics(b, metrics) {
176
195
  return s.seconds;
177
196
  }
178
197
  });
198
+ seriesOpt(b, "envio_source_response_blocks_total", "The number of blocks a data source returned, summed over its responses. Counted as the source sent them, before the indexer drops the blocks no event needs. Only sources whose responses are measured in blocks report it.", "counter", sourceRequests, s => Stdlib_Option.map(s.responseBlocks, prim => prim));
199
+ seriesOpt(b, "envio_source_response_empty_total", "The number of responses that came back with no blocks at all — a range the source scanned and matched nothing in. Compare against envio_source_request_total for the share of requests that returned nothing.", "counter", sourceRequests, s => Stdlib_Option.map(s.responseBlocks, param => s.emptyResponseCount));
179
200
  if (heightStreamConnects.length !== 0) {
180
201
  series(b, "envio_source_height_stream_connects_total", "The number of times a source's height subscription connected. Compare against the disconnects total, which is absent until the first disconnect and counts as zero while it is: one more connect than disconnects means the stream is up, and equal counts mean it is down and the indexer is polling instead. Zero connects means the stream has not come up, which is the normal reading for a chain that is still backfilling: subscriptions are only opened once a chain reaches the head.", "counter", heightStreamConnects, count => count);
181
202
  }
@@ -200,6 +221,7 @@ function renderMetrics(b, metrics) {
200
221
  single(b, "envio_processing_max_batch_size", "The maximum number of items to process in a single batch.", "gauge", metrics.maxBatchSize);
201
222
  series(b, "envio_progress_block", "The block number of the latest block processed and stored in the database.", "gauge", chains, m => m.progressBlockNumber);
202
223
  series(b, "envio_progress_events", "The number of events processed and reflected in the database.", "gauge", chains, m => m.numEventsProcessed);
224
+ seriesOpt(b, "envio_progress_block_time_seconds", "Unix timestamp of the block the chain has processed up to. Subtract it from the scrape time for how far behind chain time the indexer is, which stays honest when the data source itself is behind the chain. Best effort in realtime mode, absent during backfill.", "gauge", chains, m => Stdlib_Option.map(m.progressBlockTime, prim => prim));
203
225
  seriesOpt(b, "envio_progress_latency", "The latency in milliseconds between the latest processed event creation and the time it was written to storage.", "gauge", chains, m => Stdlib_Option.map(m.progressLatencyMs, prim => prim));
204
226
  let ifCalled = (s, value) => {
205
227
  if (s.callCount > 0 || s.activeCallsCount > 0) {
@@ -414,6 +436,7 @@ function collectRuntime() {
414
436
  let contentType = "text/plain; version=0.0.4; charset=utf-8";
415
437
 
416
438
  export {
439
+ hasProcessedToEndblock,
417
440
  formatValue,
418
441
  escapeLabelValue,
419
442
  block,
@@ -24,6 +24,7 @@ type initialChainState = {
24
24
  endBlock: option<int>,
25
25
  maxReorgDepth: int,
26
26
  progressBlockNumber: int,
27
+ progressBlockTime: option<int>,
27
28
  numEventsProcessed: float,
28
29
  firstEventBlockNumber: option<int>,
29
30
  timestampCaughtUpToHeadOrEndblock: option<Date.t>,
@@ -43,7 +44,8 @@ type initialState = {
43
44
  envioInfo: option<JSON.t>,
44
45
  cache: dict<effectCacheRecord>,
45
46
  chains: array<initialChainState>,
46
- checkpointId: Internal.checkpointId,
47
+ // Where each chain's checkpoint sequence stands in the database.
48
+ checkpointFrontier: Frontier.t,
47
49
  // Needed to keep reorg detection logic between restarts
48
50
  reorgCheckpoints: array<Internal.reorgCheckpoint>,
49
51
  }
@@ -59,7 +61,12 @@ type updatedEffectCache = {
59
61
  }
60
62
 
61
63
  type rollback = {
62
- diffCheckpointId: Internal.checkpointId,
64
+ // The id each chain's diff rows are stamped with: the first after what that
65
+ // chain has committed.
66
+ diffFrontier: Frontier.t,
67
+ // The same ids as checkpoint rows, for a sink that resolves current state
68
+ // through them. Postgres writes the diff to the entity table and ignores these.
69
+ diffCheckpoints: array<InternalTable.Checkpoints.diffCheckpoint>,
63
70
  // How far back the deletes reach on each chain, travelling with the diff so
64
71
  // the write leaves an untouched sibling's rows alone.
65
72
  floors: RollbackFloors.t,
@@ -75,6 +82,14 @@ type rollback = {
75
82
  progressedChains: array<InternalTable.Chains.progressedChain>,
76
83
  }
77
84
 
85
+ // Where a write leaves each chain's sequence. A rollback's diff rows sit on
86
+ // chains the batch may not have progressed at all, so the write reaches them too.
87
+ let writtenFrontier = (~batch: Batch.t, ~rollback: option<rollback>) =>
88
+ switch rollback {
89
+ | Some({diffFrontier}) => Frontier.mergeMax(batch->Batch.checkpointFrontier, diffFrontier)
90
+ | None => batch->Batch.checkpointFrontier
91
+ }
92
+
78
93
  // One flush group: the changes an entity accumulated within a single chain
79
94
  // scope. A per-chain entity contributes one group per chain, and the scope is
80
95
  // what stamps the chain id onto the rows — it's never re-derived downstream.
@@ -82,6 +97,7 @@ type updatedEntity = {
82
97
  entityConfig: Internal.entityConfig,
83
98
  scope: Internal.chainScope,
84
99
  changes: array<Change.t<Internal.entity>>,
100
+ shouldSaveHistory: bool,
85
101
  }
86
102
 
87
103
  // An id the rollback must delete, together with the scope its row lives in.
@@ -110,8 +126,13 @@ type storage = {
110
126
  // `throwIfIncompatible` gets what the storage holds before any sink is
111
127
  // resumed, so a config the stored one rules out is reported as such rather
112
128
  // than as the sink tripping over tables it never created.
129
+ //
130
+ // `chainIds` is what this run drives. An isolated run resumes a subset of
131
+ // the stored chains, and the sink's resume trims past each resumed chain's
132
+ // checkpoint, so chains a sibling process is still writing must stay out.
113
133
  resumeInitialState: (
114
134
  ~entities: array<Internal.entityConfig>,
135
+ ~chainIds: array<ChainId.t>,
115
136
  ~throwIfIncompatible: (
116
137
  ~storedEnvioInfo: option<JSON.t>,
117
138
  ~storedContractMapping: ContractMapping.t,
@@ -124,17 +145,24 @@ type storage = {
124
145
  // Creates whatever indexes the filters need and aren't there yet, resolving
125
146
  // once they're queryable. Best-effort: it resolves even when a build fails,
126
147
  // leaving the query to run unindexed rather than failing the handler.
127
- ensureQueryIndexes: (~table: Table.table, ~filters: array<EntityFilter.t>) => promise<unit>,
128
- // Creates every schema-defined index still missing, without touching
129
- // `ready_at`. For a resumed indexer that is already ready and so never runs
130
- // `finalizeBackfill`: an index dropped or invalidated while it was down would
131
- // otherwise never be rebuilt. Best-effort, and safe to run with indexing live.
132
- ensureSchemaIndexes: (~entities: array<Internal.entityConfig>) => promise<unit>,
133
- // Creates every schema-defined index still missing, then stamps `ready_at` on
134
- // the given chains. Called once, when backfill completes. The indexes are
135
- // committed one at a time so a failure part way through doesn't undo the ones
136
- // already built; `ready_at` is only written once they all verify, and all
137
- // chains are stamped together.
148
+ ensureQueryIndexes: (
149
+ ~entityConfig: Internal.entityConfig,
150
+ ~scope: Internal.chainScope,
151
+ ~filters: array<EntityFilter.t>,
152
+ ) => promise<unit>,
153
+ // Creates every index the schema promises for the given chains, without
154
+ // touching `ready_at`. For a resumed indexer that is already ready and so
155
+ // never runs `finalizeBackfill`: an index dropped or invalidated while it was
156
+ // down would otherwise never be rebuilt. Best-effort, and safe to run with
157
+ // indexing live.
158
+ ensureSchemaIndexes: (
159
+ ~entities: array<Internal.entityConfig>,
160
+ ~chainIds: array<ChainId.t>,
161
+ ) => promise<unit>,
162
+ // Creates every index the schema promises for the given chains, then stamps
163
+ // `ready_at` on them. Called once those chains finish backfill. The indexes
164
+ // are committed one at a time so a failure part way through doesn't undo the
165
+ // ones already built; `ready_at` is only written once they all verify.
138
166
  finalizeBackfill: (
139
167
  ~entities: array<Internal.entityConfig>,
140
168
  ~chainIds: array<ChainId.t>,
@@ -146,13 +174,15 @@ type storage = {
146
174
  // Update chain metadata
147
175
  setChainMeta: dict<InternalTable.Chains.metaFields> => promise<unknown>,
148
176
  // Prune old checkpoints
149
- pruneStaleCheckpoints: (~safeCheckpoints: CheckpointBounds.t) => promise<unit>,
177
+ pruneStaleCheckpoints: (
178
+ ~safeCheckpoints: CheckpointSequence.checkpointBoundsByChain,
179
+ ) => promise<unit>,
150
180
  // Prune stale entity history
151
181
  pruneStaleEntityHistory: (
152
182
  ~entityName: string,
153
183
  ~entityIndex: int,
154
184
  ~chainIdColumn: option<string>,
155
- ~safeCheckpoints: CheckpointBounds.t,
185
+ ~safeCheckpoints: CheckpointSequence.checkpointBoundsByChain,
156
186
  ) => promise<unit>,
157
187
  // Get rollback target checkpoint
158
188
  getRollbackTargetCheckpoint: (
@@ -180,7 +210,6 @@ type storage = {
180
210
  writeBatch: (
181
211
  ~batch: Batch.t,
182
212
  ~rollback: option<rollback>,
183
- ~isInReorgThreshold: bool,
184
213
  ~config: Config.t,
185
214
  ~allEntities: array<Internal.entityConfig>,
186
215
  ~updatedEffectsCache: array<updatedEffectCache>,
@@ -242,6 +271,10 @@ let init = {
242
271
  ~runCommand,
243
272
  ~reset=false,
244
273
  ~lowercaseAddresses=false,
274
+ // An isolated run needs the schema to exist already: initializing under it
275
+ // would create rows for this process's chains only, leaving the ones it
276
+ // skipped with no state for their own processes to resume.
277
+ ~requireInitialized=false,
245
278
  ~startBlockRetry=StartBlockResolver.UntilItAnswers,
246
279
  ) => {
247
280
  try {
@@ -260,6 +293,11 @@ let init = {
260
293
  })
261
294
  persistence.storageStatus = Initializing(promise)
262
295
  if reset || !(await persistence.storage.isInitialized()) {
296
+ if requireInitialized {
297
+ JsError.throwWithMessage(
298
+ "`envio start --chain` needs a database that already holds every chain. Run `envio local db-migrate up` once with the full config, then start a process per chain.",
299
+ )
300
+ }
263
301
  Logging.info(`Initializing the indexer storage...`)
264
302
  // Only runs once per schema (this branch is the "first deploy or
265
303
  // reset" gate), which is exactly when a `latest` start block must be
@@ -289,6 +327,7 @@ let init = {
289
327
  Logging.info(`Found existing indexer storage. Resuming indexing state...`)
290
328
  let initialState = await persistence.storage.resumeInitialState(
291
329
  ~entities=persistence.allEntities,
330
+ ~chainIds=chainConfigs->Array.map(chain => chain.id),
292
331
  ~throwIfIncompatible=(~storedEnvioInfo, ~storedContractMapping) =>
293
332
  Config.throwIfResumeIncompatible(
294
333
  ~storedEnvioInfo,
@@ -1,13 +1,23 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as Batch from "./Batch.res.mjs";
3
4
  import * as Config from "./Config.res.mjs";
4
5
  import * as Logging from "./Logging.res.mjs";
6
+ import * as Frontier from "./db/Frontier.res.mjs";
5
7
  import * as EntityHistory from "./db/EntityHistory.res.mjs";
6
8
  import * as ErrorHandling from "./ErrorHandling.res.mjs";
7
9
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
8
10
  import * as StartBlockResolver from "./sources/StartBlockResolver.res.mjs";
9
11
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
10
12
 
13
+ function writtenFrontier(batch, rollback) {
14
+ if (rollback !== undefined) {
15
+ return Frontier.mergeMax(Batch.checkpointFrontier(batch), rollback.diffFrontier);
16
+ } else {
17
+ return Batch.checkpointFrontier(batch);
18
+ }
19
+ }
20
+
11
21
  let StorageError = /* @__PURE__ */Primitive_exceptions.create("Persistence.StorageError");
12
22
 
13
23
  function make(userEntities, allEnums, storage) {
@@ -21,9 +31,10 @@ function make(userEntities, allEnums, storage) {
21
31
  };
22
32
  }
23
33
 
24
- async function init(persistence, chainConfigs, contractMapping, envioInfo, resetCommand, runCommand, resetOpt, lowercaseAddressesOpt, startBlockRetryOpt) {
34
+ async function init(persistence, chainConfigs, contractMapping, envioInfo, resetCommand, runCommand, resetOpt, lowercaseAddressesOpt, requireInitializedOpt, startBlockRetryOpt) {
25
35
  let reset = resetOpt !== undefined ? resetOpt : false;
26
36
  let lowercaseAddresses = lowercaseAddressesOpt !== undefined ? lowercaseAddressesOpt : false;
37
+ let requireInitialized = requireInitializedOpt !== undefined ? requireInitializedOpt : false;
27
38
  let startBlockRetry = startBlockRetryOpt !== undefined ? startBlockRetryOpt : "UntilItAnswers";
28
39
  try {
29
40
  let promise = persistence.storageStatus;
@@ -50,6 +61,9 @@ async function init(persistence, chainConfigs, contractMapping, envioInfo, reset
50
61
  _0: promise$1
51
62
  };
52
63
  if (reset || !await persistence.storage.isInitialized()) {
64
+ if (requireInitialized) {
65
+ Stdlib_JsError.throwWithMessage("`envio start --chain` needs a database that already holds every chain. Run `envio local db-migrate up` once with the full config, then start a process per chain.");
66
+ }
53
67
  Logging.info(`Initializing the indexer storage...`);
54
68
  let chainConfigs$1 = await StartBlockResolver.resolveAllOrThrow(chainConfigs, lowercaseAddresses, startBlockRetry, undefined, undefined);
55
69
  let initialState = await persistence.storage.initialize(chainConfigs$1, persistence.allEntities, persistence.allEnums, contractMapping, envioInfo);
@@ -64,7 +78,7 @@ async function init(persistence, chainConfigs, contractMapping, envioInfo, reset
64
78
  tmp = typeof match !== "object" ? false : match.TAG === "Initializing";
65
79
  if (tmp) {
66
80
  Logging.info(`Found existing indexer storage. Resuming indexing state...`);
67
- let initialState$1 = await persistence.storage.resumeInitialState(persistence.allEntities, (storedEnvioInfo, storedContractMapping) => Config.throwIfResumeIncompatible(storedEnvioInfo, storedContractMapping, envioInfo, contractMapping, resetCommand, runCommand));
81
+ let initialState$1 = await persistence.storage.resumeInitialState(persistence.allEntities, chainConfigs.map(chain => chain.id), (storedEnvioInfo, storedContractMapping) => Config.throwIfResumeIncompatible(storedEnvioInfo, storedContractMapping, envioInfo, contractMapping, resetCommand, runCommand));
68
82
  persistence.storageStatus = {
69
83
  TAG: "Ready",
70
84
  _0: initialState$1
@@ -105,10 +119,11 @@ function getInitializedState(persistence) {
105
119
  }
106
120
 
107
121
  export {
122
+ writtenFrontier,
108
123
  StorageError,
109
124
  make,
110
125
  init,
111
126
  getInitializedStorageOrThrow,
112
127
  getInitializedState,
113
128
  }
114
- /* Config Not a pure module */
129
+ /* Batch Not a pure module */