envio 3.9.0 → 3.10.0-subgraph

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 (143) hide show
  1. package/evm.schema.json +26 -0
  2. package/fuel.schema.json +26 -0
  3. package/index.d.ts +41 -8
  4. package/package.json +6 -7
  5. package/src/Batch.res +9 -0
  6. package/src/ChainState.res +35 -39
  7. package/src/ChainState.res.mjs +14 -11
  8. package/src/ChainState.resi +15 -7
  9. package/src/Config.res +61 -44
  10. package/src/Config.res.mjs +56 -19
  11. package/src/Core.res +19 -0
  12. package/src/Core.res.mjs +11 -0
  13. package/src/CrossChainState.res +15 -23
  14. package/src/CrossChainState.res.mjs +10 -18
  15. package/src/CrossChainState.resi +4 -1
  16. package/src/Ecosystem.res +7 -2
  17. package/src/Envio.res +6 -4
  18. package/src/EventConfigBuilder.res +169 -26
  19. package/src/EventConfigBuilder.res.mjs +124 -15
  20. package/src/EventProcessing.res +6 -6
  21. package/src/EventProcessing.res.mjs +8 -6
  22. package/src/HandlerLoader.res +20 -8
  23. package/src/HandlerLoader.res.mjs +11 -2
  24. package/src/HandlerRegister.res +21 -4
  25. package/src/HandlerRegister.res.mjs +24 -5
  26. package/src/InMemoryStore.res +9 -7
  27. package/src/InMemoryStore.res.mjs +3 -3
  28. package/src/IndexerState.res +43 -6
  29. package/src/IndexerState.res.mjs +36 -7
  30. package/src/IndexerState.resi +4 -4
  31. package/src/Internal.res +16 -17
  32. package/src/Metrics.res +72 -6
  33. package/src/Metrics.res.mjs +30 -2
  34. package/src/Persistence.res +36 -30
  35. package/src/Persistence.res.mjs +1 -20
  36. package/src/PgStorage.res +104 -56
  37. package/src/PgStorage.res.mjs +57 -35
  38. package/src/PruneStaleHistory.res +85 -52
  39. package/src/PruneStaleHistory.res.mjs +63 -37
  40. package/src/Rollback.res +68 -45
  41. package/src/Rollback.res.mjs +38 -19
  42. package/src/RollbackCommit.res +7 -9
  43. package/src/RollbackCommit.res.mjs +5 -6
  44. package/src/SafeCheckpointTracking.res +11 -11
  45. package/src/SafeCheckpointTracking.res.mjs +7 -6
  46. package/src/SimulateItems.res +33 -18
  47. package/src/SimulateItems.res.mjs +26 -22
  48. package/src/Sink.res +38 -26
  49. package/src/Sink.res.mjs +21 -16
  50. package/src/TestIndexer.res +5 -5
  51. package/src/TestIndexer.res.mjs +1 -1
  52. package/src/UserContext.res +358 -51
  53. package/src/UserContext.res.mjs +271 -35
  54. package/src/Utils.res +170 -0
  55. package/src/Utils.res.mjs +153 -0
  56. package/src/Writing.res +2 -2
  57. package/src/Writing.res.mjs +1 -1
  58. package/src/bindings/ClickHouse.res +366 -698
  59. package/src/bindings/ClickHouse.res.mjs +371 -421
  60. package/src/bindings/ClickHouseSink.res +337 -0
  61. package/src/bindings/ClickHouseSink.res.mjs +246 -0
  62. package/src/bindings/EventSource.res +8 -2
  63. package/src/bindings/NodeJs.res +6 -0
  64. package/src/bindings/Vitest.res +23 -0
  65. package/src/bindings/Vitest.res.mjs +3 -0
  66. package/src/bindings/WebSocket.res +9 -10
  67. package/src/db/CheckpointBounds.res +53 -0
  68. package/src/db/CheckpointBounds.res.mjs +44 -0
  69. package/src/db/EntityFilter.res +36 -17
  70. package/src/db/EntityFilter.res.mjs +25 -14
  71. package/src/db/EntityHistory.res +47 -22
  72. package/src/db/EntityHistory.res.mjs +19 -12
  73. package/src/db/InternalTable.res +84 -35
  74. package/src/db/InternalTable.res.mjs +56 -23
  75. package/src/db/RollbackFloors.res +55 -0
  76. package/src/db/RollbackFloors.res.mjs +92 -0
  77. package/src/db/Table.res +22 -4
  78. package/src/db/Table.res.mjs +30 -9
  79. package/src/sources/Evm.res +2 -0
  80. package/src/sources/Evm.res.mjs +2 -0
  81. package/src/sources/EvmHyperSyncSource.res +3 -3
  82. package/src/sources/EvmHyperSyncSource.res.mjs +3 -3
  83. package/src/sources/EvmRpcWs.res +124 -0
  84. package/src/sources/EvmRpcWs.res.mjs +117 -0
  85. package/src/sources/Fuel.res +2 -0
  86. package/src/sources/Fuel.res.mjs +2 -0
  87. package/src/sources/FuelHyperSyncSource.res +1 -1
  88. package/src/sources/FuelHyperSyncSource.res.mjs +2 -1
  89. package/src/sources/HeightFeed.res +568 -0
  90. package/src/sources/HeightFeed.res.mjs +452 -0
  91. package/src/sources/HeightStream.res +257 -0
  92. package/src/sources/HeightStream.res.mjs +194 -0
  93. package/src/sources/HyperSync.res +5 -0
  94. package/src/sources/HyperSync.res.mjs +3 -0
  95. package/src/sources/HyperSync.resi +4 -0
  96. package/src/sources/HyperSyncSSE.res +49 -0
  97. package/src/sources/HyperSyncSSE.res.mjs +79 -0
  98. package/src/sources/RpcSource.res +1 -1
  99. package/src/sources/RpcSource.res.mjs +2 -2
  100. package/src/sources/Source.res +54 -1
  101. package/src/sources/Source.res.mjs +25 -0
  102. package/src/sources/SourceManager.res +220 -227
  103. package/src/sources/SourceManager.res.mjs +139 -164
  104. package/src/sources/SourceManager.resi +8 -0
  105. package/src/sources/Svm.res +2 -0
  106. package/src/sources/Svm.res.mjs +2 -0
  107. package/src/sources/SvmHyperSyncClient.res +70 -127
  108. package/src/sources/SvmHyperSyncClient.res.mjs +51 -25
  109. package/src/sources/SvmHyperSyncSource.res +18 -15
  110. package/src/sources/SvmHyperSyncSource.res.mjs +5 -4
  111. package/src/subgraph/blocks.ts +176 -0
  112. package/src/subgraph/calls.ts +217 -0
  113. package/src/subgraph/conformance.ts +102 -0
  114. package/src/subgraph/division.ts +133 -0
  115. package/src/subgraph/errors.ts +90 -0
  116. package/src/subgraph/graph-ts-types/VERSION +2 -0
  117. package/src/subgraph/graph-ts-types/chain/arweave.d.ts +70 -0
  118. package/src/subgraph/graph-ts-types/chain/cosmos.d.ts +327 -0
  119. package/src/subgraph/graph-ts-types/chain/ethereum.d.ts +233 -0
  120. package/src/subgraph/graph-ts-types/chain/near.d.ts +253 -0
  121. package/src/subgraph/graph-ts-types/chain/starknet.d.ts +32 -0
  122. package/src/subgraph/graph-ts-types/common/collections.d.ts +136 -0
  123. package/src/subgraph/graph-ts-types/common/conversion.d.ts +11 -0
  124. package/src/subgraph/graph-ts-types/common/datasource.d.ts +30 -0
  125. package/src/subgraph/graph-ts-types/common/eager-offset.d.ts +0 -0
  126. package/src/subgraph/graph-ts-types/common/json.d.ts +17 -0
  127. package/src/subgraph/graph-ts-types/common/numbers.d.ts +120 -0
  128. package/src/subgraph/graph-ts-types/common/value.d.ts +120 -0
  129. package/src/subgraph/graph-ts-types/common/yaml.d.ts +90 -0
  130. package/src/subgraph/graph-ts-types/global/global.d.ts +194 -0
  131. package/src/subgraph/graph-ts-types/helper-functions.d.ts +22 -0
  132. package/src/subgraph/graph-ts-types/index.d.ts +102 -0
  133. package/src/subgraph/graph-ts.ts +1948 -0
  134. package/src/subgraph/hosts.ts +148 -0
  135. package/src/subgraph/runtime.ts +863 -0
  136. package/src/subgraph/scope.ts +66 -0
  137. package/svm.schema.json +6 -81
  138. package/src/MemoryStorage.res +0 -729
  139. package/src/MemoryStorage.res.mjs +0 -586
  140. package/src/sources/HyperSyncHeightStream.res +0 -129
  141. package/src/sources/HyperSyncHeightStream.res.mjs +0 -110
  142. package/src/sources/RpcWebSocketHeightStream.res +0 -175
  143. package/src/sources/RpcWebSocketHeightStream.res.mjs +0 -180
@@ -355,7 +355,10 @@ let getChainState = (state: t, ~chainId: ChainId.t): ChainState.t =>
355
355
  )
356
356
  }
357
357
 
358
- let getSafeCheckpointId = (state: t) => state.crossChainState->CrossChainState.getSafeCheckpointId
358
+ let getSafeCheckpointIdByChain = (state: t) =>
359
+ state.crossChainState->CrossChainState.getSafeCheckpointIdByChain(
360
+ ~committedCheckpointId=state.committedCheckpointId,
361
+ )
359
362
 
360
363
  let createBatch = (
361
364
  state: t,
@@ -452,7 +455,14 @@ let entities = (state: t) => state.entities
452
455
  // Every in-memory entity table across all scopes, cross-chain first. The size,
453
456
  // drop and flush passes walk the whole store this way instead of assuming a
454
457
  // single partition.
455
- let eachEntityTable = (state: t, fn: (~entityConfig: Internal.entityConfig, ~scope: Internal.chainScope, ~table: InMemoryTable.Entity.t) => unit) => {
458
+ let eachEntityTable = (
459
+ state: t,
460
+ fn: (
461
+ ~entityConfig: Internal.entityConfig,
462
+ ~scope: Internal.chainScope,
463
+ ~table: InMemoryTable.Entity.t,
464
+ ) => unit,
465
+ ) => {
456
466
  let chainStates = state.crossChainState->CrossChainState.chainStates
457
467
  state.allEntities->Array.forEach(entityConfig =>
458
468
  if entityConfig.crossChain {
@@ -516,6 +526,7 @@ let toMetrics = (state: t): Metrics.t => {
516
526
  let chainStates = state.crossChainState->CrossChainState.chainStates
517
527
  let sourceRequests = []
518
528
  let sourceHeights = []
529
+ let sourceHeightStreams = []
519
530
  chainStates->Utils.Dict.forEach(cs => {
520
531
  let sourceManager = cs->ChainState.sourceManager
521
532
  sourceManager
@@ -538,6 +549,16 @@ let toMetrics = (state: t): Metrics.t => {
538
549
  height: s.height,
539
550
  })
540
551
  )
552
+ sourceManager
553
+ ->SourceManager.getHeightStreamSamples
554
+ ->Array.forEach(s =>
555
+ sourceHeightStreams->Array.push({
556
+ Metrics.source: s.sourceName,
557
+ chainId: s.chainId,
558
+ connectCount: s.stream.connectCount,
559
+ disconnectsByReason: s.stream.disconnectsByReason,
560
+ })
561
+ )
541
562
  })
542
563
  let historyPrunes = []
543
564
  state.historyPruneStats->Utils.Dict.forEachWithKey((s, entityName) =>
@@ -600,6 +621,7 @@ let toMetrics = (state: t): Metrics.t => {
600
621
  historyPrunes,
601
622
  sourceRequests,
602
623
  sourceHeights,
624
+ sourceHeightStreams,
603
625
  }
604
626
  }
605
627
 
@@ -793,6 +815,8 @@ let drainBatchRun = (state: t): Batch.t => {
793
815
  }
794
816
  }
795
817
 
818
+ let pendingRollback = (state: t): option<Persistence.rollback> => state.rollback
819
+
796
820
  // Take the pending rollback diff to write, clearing it from the store.
797
821
  let takeRollback = (state: t): option<Persistence.rollback> => {
798
822
  let rollback = state.rollback
@@ -812,9 +836,9 @@ let markCommitted = (state: t, ~upToCheckpointId) => {
812
836
  // Reset the in-memory tables and arm the rollback diff that the next write commits.
813
837
  let beginRollbackDiff = (
814
838
  state: t,
815
- ~targetCheckpointId,
816
839
  ~diffCheckpointId,
817
- ~progressBlockNumberByChainId,
840
+ ~floors,
841
+ ~progressedChains: array<InternalTable.Chains.progressedChain>,
818
842
  ~rolledBackAddresses,
819
843
  ) => {
820
844
  let perChainEntities = state.allEntities->EntityTables.perChain
@@ -829,10 +853,23 @@ let beginRollbackDiff = (
829
853
  | Some({rolledBackAddresses: pending}) => pending->Array.concat(rolledBackAddresses)
830
854
  | None => rolledBackAddresses
831
855
  }
856
+ // Same for the chains: this rollback recomputes progress from the checkpoints,
857
+ // so a chain the pending diff already moved can land on exactly the block it
858
+ // is now at and go unreported here. Its stored progress still needs
859
+ // correcting, so the pending rows carry over — this rollback's row for a
860
+ // chain wins, being the later reading of the same chain state.
861
+ let progressedChains = switch state.rollback {
862
+ | Some({progressedChains: pending}) =>
863
+ let byChainId = Dict.make()
864
+ pending->Array.forEach(chain => byChainId->ChainId.Dict.set(chain.chainId, chain))
865
+ progressedChains->Array.forEach(chain => byChainId->ChainId.Dict.set(chain.chainId, chain))
866
+ byChainId->Dict.valuesToArray
867
+ | None => progressedChains
868
+ }
832
869
  state.rollback = Some({
833
- targetCheckpointId,
834
870
  diffCheckpointId,
835
- progressBlockNumberByChainId,
871
+ floors,
872
+ progressedChains,
836
873
  rolledBackAddresses,
837
874
  })
838
875
  }
@@ -159,8 +159,8 @@ function getChainState(state, chainId) {
159
159
  }
160
160
  }
161
161
 
162
- function getSafeCheckpointId(state) {
163
- return CrossChainState.getSafeCheckpointId(state.crossChainState);
162
+ function getSafeCheckpointIdByChain(state) {
163
+ return CrossChainState.getSafeCheckpointIdByChain(state.crossChainState, state.committedCheckpointId);
164
164
  }
165
165
 
166
166
  function createBatch(state, processedCheckpointId, batchSizeTarget, isRollback) {
@@ -392,6 +392,7 @@ function toMetrics(state) {
392
392
  let chainStates = CrossChainState.chainStates(state.crossChainState);
393
393
  let sourceRequests = [];
394
394
  let sourceHeights = [];
395
+ let sourceHeightStreams = [];
395
396
  Utils.Dict.forEach(chainStates, cs => {
396
397
  let sourceManager = ChainState.sourceManager(cs);
397
398
  SourceManager.getRequestStatSamples(sourceManager).forEach(s => {
@@ -410,6 +411,14 @@ function toMetrics(state) {
410
411
  height: s.height
411
412
  });
412
413
  });
414
+ SourceManager.getHeightStreamSamples(sourceManager).forEach(s => {
415
+ sourceHeightStreams.push({
416
+ source: s.sourceName,
417
+ chainId: s.chainId,
418
+ connectCount: s.stream.connectCount,
419
+ disconnectsByReason: s.stream.disconnectsByReason
420
+ });
421
+ });
413
422
  });
414
423
  let historyPrunes = [];
415
424
  Utils.Dict.forEachWithKey(state.historyPruneStats, (s, entityName) => {
@@ -464,7 +473,8 @@ function toMetrics(state) {
464
473
  })),
465
474
  historyPrunes: historyPrunes,
466
475
  sourceRequests: sourceRequests,
467
- sourceHeights: sourceHeights
476
+ sourceHeights: sourceHeights,
477
+ sourceHeightStreams: sourceHeightStreams
468
478
  };
469
479
  }
470
480
 
@@ -658,6 +668,10 @@ function drainBatchRun(state) {
658
668
  };
659
669
  }
660
670
 
671
+ function pendingRollback(state) {
672
+ return state.rollback;
673
+ }
674
+
661
675
  function takeRollback(state) {
662
676
  let rollback = state.rollback;
663
677
  state.rollback = undefined;
@@ -668,18 +682,32 @@ function markCommitted(state, upToCheckpointId) {
668
682
  state.committedCheckpointId = upToCheckpointId;
669
683
  }
670
684
 
671
- function beginRollbackDiff(state, targetCheckpointId, diffCheckpointId, progressBlockNumberByChainId, rolledBackAddresses) {
685
+ function beginRollbackDiff(state, diffCheckpointId, floors, progressedChains, rolledBackAddresses) {
672
686
  let perChainEntities = EntityTables.perChain(state.allEntities);
673
687
  state.entities = EntityTables.make(EntityTables.crossChain(state.allEntities));
674
688
  Utils.Dict.forEach(CrossChainState.chainStates(state.crossChainState), cs => ChainState.resetEntities(cs, perChainEntities));
675
689
  EffectState.resetForRollback(state.effectState);
676
690
  let match = state.rollback;
677
691
  let rolledBackAddresses$1 = match !== undefined ? match.rolledBackAddresses.concat(rolledBackAddresses) : rolledBackAddresses;
692
+ let match$1 = state.rollback;
693
+ let progressedChains$1;
694
+ if (match$1 !== undefined) {
695
+ let byChainId = {};
696
+ match$1.progressedChains.forEach(chain => {
697
+ byChainId[chain.chainId] = chain;
698
+ });
699
+ progressedChains.forEach(chain => {
700
+ byChainId[chain.chainId] = chain;
701
+ });
702
+ progressedChains$1 = Object.values(byChainId);
703
+ } else {
704
+ progressedChains$1 = progressedChains;
705
+ }
678
706
  state.rollback = {
679
- targetCheckpointId: targetCheckpointId,
680
707
  diffCheckpointId: diffCheckpointId,
708
+ floors: floors,
681
709
  rolledBackAddresses: rolledBackAddresses$1,
682
- progressBlockNumberByChainId: progressBlockNumberByChainId
710
+ progressedChains: progressedChains$1
683
711
  };
684
712
  }
685
713
 
@@ -766,7 +794,7 @@ export {
766
794
  markProcessingStalledOnFetch,
767
795
  recordProcessedBatch,
768
796
  createBatch,
769
- getSafeCheckpointId,
797
+ getSafeCheckpointIdByChain,
770
798
  config,
771
799
  persistence,
772
800
  allEntities,
@@ -812,6 +840,7 @@ export {
812
840
  recordRollbackSuccess,
813
841
  queueProcessedBatch,
814
842
  drainBatchRun,
843
+ pendingRollback,
815
844
  takeRollback,
816
845
  markCommitted,
817
846
  beginRollbackDiff,
@@ -1,7 +1,6 @@
1
1
  // The indexer state. `t` is opaque: other modules read it through the accessors
2
2
  // and change it only through the transitions and setters exposed here.
3
3
 
4
-
5
4
  type rollbackState =
6
5
  | NoRollback
7
6
  | ReorgDetected({chainId: ChainId.t, blockNumber: int})
@@ -67,7 +66,7 @@ let createBatch: (
67
66
  ~batchSizeTarget: int,
68
67
  ~isRollback: bool,
69
68
  ) => Batch.t
70
- let getSafeCheckpointId: t => option<Internal.checkpointId>
69
+ let getSafeCheckpointIdByChain: t => array<(ChainId.t, option<Internal.checkpointId>)>
71
70
 
72
71
  // Accessors.
73
72
  let config: t => Config.t
@@ -136,13 +135,14 @@ let recordRollbackSuccess: (t, ~timeSeconds: float, ~rollbackedProcessedEvents:
136
135
  // Store domain operations.
137
136
  let queueProcessedBatch: (t, ~batch: Batch.t) => unit
138
137
  let drainBatchRun: t => Batch.t
138
+ let pendingRollback: t => option<Persistence.rollback>
139
139
  let takeRollback: t => option<Persistence.rollback>
140
140
  let markCommitted: (t, ~upToCheckpointId: Internal.checkpointId) => unit
141
141
  let beginRollbackDiff: (
142
142
  t,
143
- ~targetCheckpointId: Internal.checkpointId,
144
143
  ~diffCheckpointId: Internal.checkpointId,
145
- ~progressBlockNumberByChainId: dict<int>,
144
+ ~floors: RollbackFloors.t,
145
+ ~progressedChains: array<InternalTable.Chains.progressedChain>,
146
146
  ~rolledBackAddresses: array<AddressRows.key>,
147
147
  ) => unit
148
148
  let recordWriteFailure: (t, exn) => unit
package/src/Internal.res CHANGED
@@ -573,15 +573,6 @@ type svmInstructionEventConfig = {
573
573
  programId: SvmTypes.Pubkey.t,
574
574
  /** Hex-encoded discriminator. `None` matches every instruction in the program. */
575
575
  discriminator: option<string>,
576
- /** Length of the discriminator in bytes (0 / 1 / 2 / 4 / 8). Drives the
577
- `dN` selector at query time and the dispatch-key precomputation in the
578
- router. */
579
- discriminatorByteLen: int,
580
- /** Disjunctive normal form: outer array is OR of AND-groups, inner array is
581
- AND across positions. Empty outer array means "no account filter". */
582
- accountFilters: array<svmAccountFilterGroup>,
583
- /** `None` matches both outer and inner (CPI-invoked) instructions. */
584
- isInner: option<bool>,
585
576
  /** Positional account names from the Borsh schema, in declared order.
586
577
  `[]` means no schema is attached for this instruction. */
587
578
  accounts: array<string>,
@@ -597,10 +588,11 @@ type svmInstructionEventConfig = {
597
588
  // Per-(event, chain) registration produced when user handler code registers an
598
589
  // event (`onEvent`) or a dynamic contract registers. References its definition
599
590
  // by value as `.eventConfig` and adds the handler binding plus the
600
- // registration/`where`-derived fetch state. Not `private`: Fuel/SVM
601
- // registrations add no ecosystem-specific fields (so they're bare aliases that
602
- // must stay directly constructable), and the evm→base cast in sources is sound
603
- // by ecosystem homogeneity — an EVM chain only ever holds `evmOnEventRegistration`s.
591
+ // registration/`where`-derived fetch state. Not `private`: Fuel registrations
592
+ // add no ecosystem-specific fields (so the alias must stay directly
593
+ // constructable), and the ecosystem→base casts in sources are sound by
594
+ // ecosystem homogeneity — an EVM chain only ever holds
595
+ // `evmOnEventRegistration`s.
604
596
  type onEventRegistration = {
605
597
  // Chain-scoped sequential index — the registration's position in the
606
598
  // chain's onEventRegistrations array, assigned when registration finishes
@@ -639,11 +631,18 @@ type evmOnEventRegistration = {
639
631
  resolvedWhere: resolvedWhere,
640
632
  }
641
633
 
642
- // Fuel and SVM registrations add no ecosystem-specific fetch state (their
643
- // filters are config-derived and live on the definition), so they're bare
644
- // aliases of the base registration.
634
+ // Fuel registrations add no ecosystem-specific fetch state, so it's a bare
635
+ // alias of the base registration.
645
636
  type fuelOnEventRegistration = onEventRegistration
646
- type svmOnEventRegistration = onEventRegistration
637
+
638
+ type svmOnEventRegistration = {
639
+ ...onEventRegistration,
640
+ /** Disjunctive normal form: outer array is OR of AND-groups, inner array is
641
+ AND across positions. Empty outer array means "no account filter". */
642
+ accountFilters: array<svmAccountFilterGroup>,
643
+ /** `None` matches both outer and inner (CPI-invoked) instructions. */
644
+ isInner: option<bool>,
645
+ }
647
646
 
648
647
  type svmProgramConfig = {
649
648
  name: string,
package/src/Metrics.res CHANGED
@@ -98,6 +98,13 @@ type sourceHeightMetrics = {
98
98
  height: int,
99
99
  }
100
100
 
101
+ type sourceHeightStreamMetrics = {
102
+ source: string,
103
+ chainId: ChainId.t,
104
+ connectCount: int,
105
+ disconnectsByReason: array<(string, int)>,
106
+ }
107
+
101
108
  type t = {
102
109
  startTime: Date.t,
103
110
  // Wall clock when this snapshot was built, so a scrape can be dated.
@@ -124,6 +131,7 @@ type t = {
124
131
  historyPrunes: array<historyPruneMetrics>,
125
132
  sourceRequests: array<sourceRequestMetrics>,
126
133
  sourceHeights: array<sourceHeightMetrics>,
134
+ sourceHeightStreams: array<sourceHeightStreamMetrics>,
127
135
  }
128
136
 
129
137
  // Prometheus floats keep at most 3 decimals; integral values render without a
@@ -220,13 +228,23 @@ let renderMetrics = (b: builder, metrics: t) => {
220
228
  metrics.storageWrites->Array.map(s => (`{storage="${s.storage->escapeLabelValue}"}`, s))
221
229
  let historyPrunes =
222
230
  metrics.historyPrunes->Array.map(s => (`{entity="${s.entity->escapeLabelValue}"}`, s))
231
+ // Every per-source series shares this label set: a scrape whose labels
232
+ // disagree between two of them is one nothing can join on.
233
+ let sourceLabels = (~source, ~chainId, ~extra: option<(string, string)>=?) => {
234
+ let pair = `{source="${source->escapeLabelValue}",chainId="${chainId->ChainId.toString}"`
235
+ switch extra {
236
+ | Some((name, value)) => `${pair},${name}="${value->escapeLabelValue}"}`
237
+ | None => pair ++ "}"
238
+ }
239
+ }
240
+
223
241
  // Two sources can share a name (e.g. primary and fallback RPC urls on the
224
242
  // same host), so aggregate by label set — duplicate samples would make
225
243
  // Prometheus reject the scrape.
226
244
  let sourceRequests = {
227
245
  let byLabels: dict<sourceRequestMetrics> = Dict.make()
228
246
  metrics.sourceRequests->Array.forEach(s => {
229
- let labels = `{source="${s.source->escapeLabelValue}",chainId="${s.chainId->ChainId.toString}",method="${s.method->escapeLabelValue}"}`
247
+ let labels = sourceLabels(~source=s.source, ~chainId=s.chainId, ~extra=("method", s.method))
230
248
  switch byLabels->Utils.Dict.dangerouslyGetNonOption(labels) {
231
249
  | Some(existing) =>
232
250
  byLabels->Dict.set(
@@ -241,7 +259,7 @@ let renderMetrics = (b: builder, metrics: t) => {
241
259
  let sources = {
242
260
  let byLabels: dict<int> = Dict.make()
243
261
  metrics.sourceHeights->Array.forEach(s => {
244
- let labels = `{source="${s.source->escapeLabelValue}",chainId="${s.chainId->ChainId.toString}"}`
262
+ let labels = sourceLabels(~source=s.source, ~chainId=s.chainId)
245
263
  switch byLabels->Utils.Dict.dangerouslyGetNonOption(labels) {
246
264
  | Some(existing) if existing >= s.height => ()
247
265
  | _ => byLabels->Dict.set(labels, s.height)
@@ -250,6 +268,34 @@ let renderMetrics = (b: builder, metrics: t) => {
250
268
  byLabels->Dict.toArray
251
269
  }
252
270
 
271
+ // Zero connects is the one count worth rendering flat: it is a stream that
272
+ // has never come up, which the disconnects say nothing about — retries at a
273
+ // connection that never opened are not disconnections — and which is
274
+ // otherwise indistinguishable from a chain that was never configured to
275
+ // stream at all.
276
+ let heightStreamConnects = {
277
+ let byLabels: dict<int> = Dict.make()
278
+ metrics.sourceHeightStreams->Array.forEach(s =>
279
+ byLabels->Utils.Dict.incrementBy(
280
+ sourceLabels(~source=s.source, ~chainId=s.chainId),
281
+ s.connectCount,
282
+ )
283
+ )
284
+ byLabels->Dict.toArray
285
+ }
286
+ let heightStreamDisconnects = {
287
+ let byLabels: dict<int> = Dict.make()
288
+ metrics.sourceHeightStreams->Array.forEach(s =>
289
+ s.disconnectsByReason->Array.forEach(((reason, count)) =>
290
+ byLabels->Utils.Dict.incrementBy(
291
+ sourceLabels(~source=s.source, ~chainId=s.chainId, ~extra=("reason", reason)),
292
+ count,
293
+ )
294
+ )
295
+ )
296
+ byLabels->Dict.toArray
297
+ }
298
+
253
299
  b->single(
254
300
  ~name="envio_process_start_time_seconds",
255
301
  ~help="Start time of the process since unix epoch in seconds.",
@@ -463,6 +509,24 @@ let renderMetrics = (b: builder, metrics: t) => {
463
509
  ~entries=sourceRequests,
464
510
  ~value=s => s.seconds !== 0. ? Some(s.seconds) : None,
465
511
  )
512
+ if heightStreamConnects->Array.length > 0 {
513
+ b->series(
514
+ ~name="envio_source_height_stream_connects_total",
515
+ ~help="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.",
516
+ ~kind="counter",
517
+ ~entries=heightStreamConnects,
518
+ ~value=count => count->Int.toFloat,
519
+ )
520
+ }
521
+ if heightStreamDisconnects->Array.length > 0 {
522
+ b->series(
523
+ ~name="envio_source_height_stream_disconnects_total",
524
+ ~help="The number of times a source's height subscription lost a connection, by reason. Failed retries are not counted, so this is outages rather than their length. A rotated disconnect is a connection that served its time; unsubscribed is the source being benched; every other reason ended a connection early.",
525
+ ~kind="counter",
526
+ ~entries=heightStreamDisconnects,
527
+ ~value=count => count->Int.toFloat,
528
+ )
529
+ }
466
530
  b->series(
467
531
  ~name="envio_source_known_height",
468
532
  ~help="The latest known block number reported by the source. This value may lag behind the actual chain height, as it is updated only when queried.",
@@ -706,10 +770,12 @@ let renderMetrics = (b: builder, metrics: t) => {
706
770
  ~help="The number of address registrations per contract on chain, static and dynamic. An address shared by N contracts counts N times.",
707
771
  ~kind="gauge",
708
772
  ~entries=metrics.chains->Array.flatMap(m =>
709
- m.addressesByContract->Array.map(((contract, count)) => (
710
- `{chainId="${m.chainId->ChainId.toString}",contract="${contract->escapeLabelValue}"}`,
711
- count,
712
- ))
773
+ m.addressesByContract->Array.map(
774
+ ((contract, count)) => (
775
+ `{chainId="${m.chainId->ChainId.toString}",contract="${contract->escapeLabelValue}"}`,
776
+ count,
777
+ ),
778
+ )
713
779
  ),
714
780
  ~value=count => count->Int.toFloat,
715
781
  )
@@ -80,9 +80,20 @@ function renderMetrics(b, metrics) {
80
80
  `{entity="` + escapeLabelValue(s.entity) + `"}`,
81
81
  s
82
82
  ]);
83
+ let sourceLabels = (source, chainId, extra) => {
84
+ let pair = `{source="` + escapeLabelValue(source) + `",chainId="` + ChainId.toString(chainId) + `"`;
85
+ if (extra !== undefined) {
86
+ return pair + `,` + extra[0] + `="` + escapeLabelValue(extra[1]) + `"}`;
87
+ } else {
88
+ return pair + "}";
89
+ }
90
+ };
83
91
  let byLabels = {};
84
92
  metrics.sourceRequests.forEach(s => {
85
- let labels = `{source="` + escapeLabelValue(s.source) + `",chainId="` + ChainId.toString(s.chainId) + `",method="` + escapeLabelValue(s.method) + `"}`;
93
+ let labels = sourceLabels(s.source, s.chainId, [
94
+ "method",
95
+ s.method
96
+ ]);
86
97
  let existing = byLabels[labels];
87
98
  if (existing !== undefined) {
88
99
  byLabels[labels] = {
@@ -99,7 +110,7 @@ function renderMetrics(b, metrics) {
99
110
  let sourceRequests = Object.entries(byLabels);
100
111
  let byLabels$1 = {};
101
112
  metrics.sourceHeights.forEach(s => {
102
- let labels = `{source="` + escapeLabelValue(s.source) + `",chainId="` + ChainId.toString(s.chainId) + `"}`;
113
+ let labels = sourceLabels(s.source, s.chainId, undefined);
103
114
  let existing = byLabels$1[labels];
104
115
  if (existing !== undefined && existing >= s.height) {
105
116
  return;
@@ -109,6 +120,17 @@ function renderMetrics(b, metrics) {
109
120
  }
110
121
  });
111
122
  let sources = Object.entries(byLabels$1);
123
+ let byLabels$2 = {};
124
+ metrics.sourceHeightStreams.forEach(s => Utils.Dict.incrementBy(byLabels$2, sourceLabels(s.source, s.chainId, undefined), s.connectCount));
125
+ let heightStreamConnects = Object.entries(byLabels$2);
126
+ let byLabels$3 = {};
127
+ metrics.sourceHeightStreams.forEach(s => {
128
+ s.disconnectsByReason.forEach(param => Utils.Dict.incrementBy(byLabels$3, sourceLabels(s.source, s.chainId, [
129
+ "reason",
130
+ param[0]
131
+ ]), param[1]));
132
+ });
133
+ let heightStreamDisconnects = Object.entries(byLabels$3);
112
134
  single(b, "envio_process_start_time_seconds", "Start time of the process since unix epoch in seconds.", "gauge", metrics.startTime.getTime() / 1000);
113
135
  single(b, "envio_process_metric_time_seconds", "The time these metrics were collected. Use it to tell how fresh a snapshot is, or to measure rates between two snapshots.", "gauge", metrics.metricTime.getTime() / 1000);
114
136
  single(b, "envio_process_elapsed_seconds", "How long the indexer has been running. Divide a cumulative seconds metric by this to get the share of the run it took, eg envio_processing_seconds for time spent in event handlers.", "gauge", metrics.elapsedSeconds);
@@ -154,6 +176,12 @@ function renderMetrics(b, metrics) {
154
176
  return s.seconds;
155
177
  }
156
178
  });
179
+ if (heightStreamConnects.length !== 0) {
180
+ 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
+ }
182
+ if (heightStreamDisconnects.length !== 0) {
183
+ series(b, "envio_source_height_stream_disconnects_total", "The number of times a source's height subscription lost a connection, by reason. Failed retries are not counted, so this is outages rather than their length. A rotated disconnect is a connection that served its time; unsubscribed is the source being benched; every other reason ended a connection early.", "counter", heightStreamDisconnects, count => count);
184
+ }
157
185
  series(b, "envio_source_known_height", "The latest known block number reported by the source. This value may lag behind the actual chain height, as it is updated only when queried.", "gauge", sources, height => height);
158
186
  seriesOpt(b, "envio_reorg_detected_total", "Total number of reorgs detected", "counter", chains, m => {
159
187
  if (m.reorgCount > 0) {
@@ -59,14 +59,20 @@ type updatedEffectCache = {
59
59
  }
60
60
 
61
61
  type rollback = {
62
- targetCheckpointId: Internal.checkpointId,
63
62
  diffCheckpointId: Internal.checkpointId,
63
+ // How far back the deletes reach on each chain, travelling with the diff so
64
+ // the write leaves an untouched sibling's rows alone.
65
+ floors: RollbackFloors.t,
64
66
  // The address registrations the rollback dropped, as the chains' address
65
67
  // stores resolved them. Deleted by primary key in the same transaction.
66
68
  rolledBackAddresses: array<AddressRows.key>,
67
- // Last valid block per chain affected by the rollback. Read by
68
- // `RollbackCommit.fire` once the diff is durably written.
69
- progressBlockNumberByChainId: dict<int>,
69
+ // Where the rollback left every chain it moved. Written with the diff rather
70
+ // than waiting for a batch of those chains' own: the batch that carries the
71
+ // diff can belong to a chain the rollback never touched, and a chain whose
72
+ // stored progress outlived the checkpoints backing it would resume past
73
+ // blocks it never re-indexed. Also what `RollbackCommit.fire` reports once
74
+ // the diff is durably written.
75
+ progressedChains: array<InternalTable.Chains.progressedChain>,
70
76
  }
71
77
 
72
78
  // One flush group: the changes an entity accumulated within a single chain
@@ -101,7 +107,16 @@ type storage = {
101
107
  ~contractMapping: ContractMapping.t,
102
108
  ~envioInfo: JSON.t,
103
109
  ) => promise<initialState>,
104
- resumeInitialState: unit => promise<initialState>,
110
+ // `throwIfIncompatible` gets what the storage holds before any sink is
111
+ // resumed, so a config the stored one rules out is reported as such rather
112
+ // than as the sink tripping over tables it never created.
113
+ resumeInitialState: (
114
+ ~entities: array<Internal.entityConfig>,
115
+ ~throwIfIncompatible: (
116
+ ~storedEnvioInfo: option<JSON.t>,
117
+ ~storedContractMapping: ContractMapping.t,
118
+ ) => unit,
119
+ ) => promise<initialState>,
105
120
  // Returns rows matching the filter.
106
121
  // Field values are serialized and rows parsed with the table's field schemas.
107
122
  @raises("StorageError")
@@ -131,13 +146,13 @@ type storage = {
131
146
  // Update chain metadata
132
147
  setChainMeta: dict<InternalTable.Chains.metaFields> => promise<unknown>,
133
148
  // Prune old checkpoints
134
- pruneStaleCheckpoints: (~safeCheckpointId: Internal.checkpointId) => promise<unit>,
149
+ pruneStaleCheckpoints: (~safeCheckpoints: CheckpointBounds.t) => promise<unit>,
135
150
  // Prune stale entity history
136
151
  pruneStaleEntityHistory: (
137
152
  ~entityName: string,
138
153
  ~entityIndex: int,
139
154
  ~chainIdColumn: option<string>,
140
- ~safeCheckpointId: Internal.checkpointId,
155
+ ~safeCheckpoints: CheckpointBounds.t,
141
156
  ) => promise<unit>,
142
157
  // Get rollback target checkpoint
143
158
  getRollbackTargetCheckpoint: (
@@ -146,7 +161,7 @@ type storage = {
146
161
  ) => promise<option<Internal.checkpointId>>,
147
162
  // Get rollback progress diff
148
163
  getRollbackProgressDiff: (
149
- ~rollbackTargetCheckpointId: Internal.checkpointId,
164
+ ~floors: RollbackFloors.t,
150
165
  ) => promise<
151
166
  array<{
152
167
  "chain_id": ChainId.t,
@@ -159,7 +174,7 @@ type storage = {
159
174
  // before handing them back.
160
175
  getRollbackData: (
161
176
  ~entityConfig: Internal.entityConfig,
162
- ~rollbackTargetCheckpointId: Internal.checkpointId,
177
+ ~floors: RollbackFloors.t,
163
178
  ) => promise<(array<rollbackRemoval>, array<Internal.entity>)>,
164
179
  // Write batch to storage
165
180
  writeBatch: (
@@ -262,27 +277,18 @@ let init = {
262
277
  }
263
278
  ) {
264
279
  Logging.info(`Found existing indexer storage. Resuming indexing state...`)
265
- let initialState = await persistence.storage.resumeInitialState()
266
- let changedPaths = switch initialState.envioInfo {
267
- | None => ["storage was initialized by an older envio version"]
268
- | Some(stored) => Config.diffPaths(~stored, ~current=envioInfo)
269
- }
270
- let hasClickhouse = switch envioInfo {
271
- | Object(d) =>
272
- switch d->Dict.get("storage") {
273
- | Some(Object(s)) =>
274
- switch s->Dict.get("clickhouse") {
275
- | Some(Boolean(true)) => true
276
- | _ => false
277
- }
278
- | _ => false
279
- }
280
- | _ => false
281
- }
282
- Config.throwIfIncompatible(changedPaths, ~resetCommand, ~runCommand, ~hasClickhouse)
283
- if !(initialState.contractMapping->ContractMapping.isEqual(contractMapping)) {
284
- Config.throwIfIncompatible(["contracts"], ~resetCommand, ~runCommand, ~hasClickhouse)
285
- }
280
+ let initialState = await persistence.storage.resumeInitialState(
281
+ ~entities=persistence.allEntities,
282
+ ~throwIfIncompatible=(~storedEnvioInfo, ~storedContractMapping) =>
283
+ Config.throwIfResumeIncompatible(
284
+ ~storedEnvioInfo,
285
+ ~storedContractMapping,
286
+ ~envioInfo,
287
+ ~contractMapping,
288
+ ~resetCommand,
289
+ ~runCommand,
290
+ ),
291
+ )
286
292
  persistence.storageStatus = Ready(initialState)
287
293
  let progress = Dict.make()
288
294
  initialState.chains->Array.forEach(c => {
@@ -5,7 +5,6 @@ import * as Logging from "./Logging.res.mjs";
5
5
  import * as EntityHistory from "./db/EntityHistory.res.mjs";
6
6
  import * as ErrorHandling from "./ErrorHandling.res.mjs";
7
7
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
8
- import * as ContractMapping from "./ContractMapping.res.mjs";
9
8
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
10
9
 
11
10
  let StorageError = /* @__PURE__ */Primitive_exceptions.create("Persistence.StorageError");
@@ -61,25 +60,7 @@ async function init(persistence, chainConfigs, contractMapping, envioInfo, reset
61
60
  tmp = typeof match !== "object" ? false : match.TAG === "Initializing";
62
61
  if (tmp) {
63
62
  Logging.info(`Found existing indexer storage. Resuming indexing state...`);
64
- let initialState$1 = await persistence.storage.resumeInitialState();
65
- let stored = initialState$1.envioInfo;
66
- let changedPaths = stored !== undefined ? Config.diffPaths(stored, envioInfo) : ["storage was initialized by an older envio version"];
67
- let hasClickhouse;
68
- if (typeof envioInfo === "object" && envioInfo !== null && !Array.isArray(envioInfo)) {
69
- let match$1 = envioInfo["storage"];
70
- if (typeof match$1 === "object" && match$1 !== null && !Array.isArray(match$1)) {
71
- let match$2 = match$1["clickhouse"];
72
- hasClickhouse = match$2 === true;
73
- } else {
74
- hasClickhouse = false;
75
- }
76
- } else {
77
- hasClickhouse = false;
78
- }
79
- Config.throwIfIncompatible(changedPaths, resetCommand, runCommand, hasClickhouse);
80
- if (!ContractMapping.isEqual(initialState$1.contractMapping, contractMapping)) {
81
- Config.throwIfIncompatible(["contracts"], resetCommand, runCommand, hasClickhouse);
82
- }
63
+ let initialState$1 = await persistence.storage.resumeInitialState(persistence.allEntities, (storedEnvioInfo, storedContractMapping) => Config.throwIfResumeIncompatible(storedEnvioInfo, storedContractMapping, envioInfo, contractMapping, resetCommand, runCommand));
83
64
  persistence.storageStatus = {
84
65
  TAG: "Ready",
85
66
  _0: initialState$1