envio 3.12.1 → 3.13.0-rc.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 (103) hide show
  1. package/package.json +6 -8
  2. package/src/BatchProcessing.res +15 -14
  3. package/src/BatchProcessing.res.mjs +8 -15
  4. package/src/Bin.res +24 -17
  5. package/src/Bin.res.mjs +5 -0
  6. package/src/ChainFetching.res +18 -24
  7. package/src/ChainFetching.res.mjs +14 -15
  8. package/src/ChainState.res +103 -38
  9. package/src/ChainState.res.mjs +79 -22
  10. package/src/ChainState.resi +14 -2
  11. package/src/Config.res +32 -0
  12. package/src/Config.res.mjs +24 -0
  13. package/src/Core.res +4 -0
  14. package/src/Core.res.mjs +4 -0
  15. package/src/CrossChainState.res +109 -11
  16. package/src/CrossChainState.res.mjs +71 -7
  17. package/src/CrossChainState.resi +11 -1
  18. package/src/Env.res +4 -0
  19. package/src/ErrorHandling.res +15 -4
  20. package/src/ErrorHandling.res.mjs +16 -5
  21. package/src/FinalizeBackfill.res +10 -4
  22. package/src/FinalizeBackfill.res.mjs +5 -3
  23. package/src/HandlerRegister.res +0 -37
  24. package/src/HandlerRegister.res.mjs +0 -18
  25. package/src/IndexerLoop.res +2 -0
  26. package/src/IndexerLoop.res.mjs +1 -0
  27. package/src/IndexerState.res +43 -1
  28. package/src/IndexerState.res.mjs +48 -4
  29. package/src/IndexerState.resi +11 -0
  30. package/src/Main.res +128 -268
  31. package/src/Main.res.mjs +27 -153
  32. package/src/Metrics.res +268 -102
  33. package/src/Metrics.res.mjs +227 -48
  34. package/src/Persistence.res +30 -2
  35. package/src/Persistence.res.mjs +12 -3
  36. package/src/PgStorage.res +19 -11
  37. package/src/PgStorage.res.mjs +13 -10
  38. package/src/Rollback.res +3 -3
  39. package/src/Server.res +178 -0
  40. package/src/Server.res.mjs +143 -0
  41. package/src/SimulateItems.res +1 -1
  42. package/src/SimulateItems.res.mjs +1 -1
  43. package/src/Supervisor.res +542 -0
  44. package/src/Supervisor.res.mjs +455 -0
  45. package/src/TestIndexer.res +1 -1
  46. package/src/TestIndexer.res.mjs +2 -2
  47. package/src/Worker.res +101 -0
  48. package/src/Worker.res.mjs +81 -0
  49. package/src/bindings/NodeJs.res +50 -0
  50. package/src/bindings/NodeJs.res.mjs +8 -1
  51. package/src/db/InternalTable.res +8 -1
  52. package/src/db/InternalTable.res.mjs +5 -1
  53. package/src/sources/AddressSet.res +5 -3
  54. package/src/sources/AddressStore.res +19 -11
  55. package/src/sources/AddressStore.res.mjs +2 -7
  56. package/src/sources/BlockStore.res +10 -9
  57. package/src/sources/BlockStore.res.mjs +4 -4
  58. package/src/sources/ChainSources.res +4 -0
  59. package/src/sources/ChainSources.res.mjs +2 -2
  60. package/src/sources/EvmChain.res +4 -0
  61. package/src/sources/EvmChain.res.mjs +3 -1
  62. package/src/sources/EvmEventItem.res +56 -0
  63. package/src/sources/EvmEventItem.res.mjs +32 -0
  64. package/src/sources/EvmHyperSyncSource.res +7 -37
  65. package/src/sources/EvmHyperSyncSource.res.mjs +8 -29
  66. package/src/sources/EvmRpcClient.res +41 -59
  67. package/src/sources/EvmRpcClient.res.mjs +4 -54
  68. package/src/sources/EvmRpcWs.res +4 -1
  69. package/src/sources/EvmRpcWs.res.mjs +6 -2
  70. package/src/sources/FuelHyperSyncSource.res +6 -4
  71. package/src/sources/FuelHyperSyncSource.res.mjs +7 -9
  72. package/src/sources/HeightFeed.res +7 -1
  73. package/src/sources/HeightFeed.res.mjs +9 -7
  74. package/src/sources/HyperSyncClient.res +1 -14
  75. package/src/sources/RequestStat.res +7 -0
  76. package/src/sources/RequestStat.res.mjs +14 -1
  77. package/src/sources/RpcSource.res +100 -1043
  78. package/src/sources/RpcSource.res.mjs +81 -1056
  79. package/src/sources/SimulateSource.res +2 -5
  80. package/src/sources/SimulateSource.res.mjs +2 -3
  81. package/src/sources/Source.res +34 -12
  82. package/src/sources/Source.res.mjs +11 -0
  83. package/src/sources/SourceManager.res +111 -31
  84. package/src/sources/SourceManager.res.mjs +65 -31
  85. package/src/sources/SourceManager.resi +4 -0
  86. package/src/sources/StartBlockResolver.res +8 -5
  87. package/src/sources/StartBlockResolver.res.mjs +4 -2
  88. package/src/sources/SvmHyperSyncSource.res +4 -2
  89. package/src/sources/SvmHyperSyncSource.res.mjs +4 -6
  90. package/src/sources/TransactionStore.res +9 -6
  91. package/src/sources/TransactionStore.res.mjs +2 -2
  92. package/src/tui/Tui.res +10 -0
  93. package/src/tui/Tui.res.mjs +15 -0
  94. package/src/tui/components/SyncETA.res +12 -6
  95. package/src/tui/components/SyncETA.res.mjs +12 -8
  96. package/src/LazyLoader.res +0 -135
  97. package/src/LazyLoader.res.mjs +0 -118
  98. package/src/bindings/SDSL.res +0 -12
  99. package/src/bindings/SDSL.res.mjs +0 -9
  100. package/src/bindings/Yargs.res +0 -8
  101. package/src/bindings/Yargs.res.mjs +0 -2
  102. package/src/sources/Rpc.res +0 -185
  103. package/src/sources/Rpc.res.mjs +0 -183
@@ -13,6 +13,7 @@ import * as Logging from "./Logging.res.mjs";
13
13
  import * as ChainMap from "./ChainMap.res.mjs";
14
14
  import * as Internal from "./Internal.res.mjs";
15
15
  import * as BigDecimal from "./bindings/BigDecimal.res.mjs";
16
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
16
17
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
17
18
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
18
19
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
@@ -470,6 +471,11 @@ function getChain(config, chainId) {
470
471
  }
471
472
  }
472
473
 
474
+ function isPerChain(config) {
475
+ let match = config.checkpointSequence;
476
+ return match !== "SharedAcrossChains";
477
+ }
478
+
473
479
  function isolate(config, chainIds) {
474
480
  let shared = config.userEntities.filter(entityConfig => entityConfig.crossChain);
475
481
  if (shared.length !== 0) {
@@ -907,6 +913,17 @@ function prime(json) {
907
913
  cached.contents = undefined;
908
914
  }
909
915
 
916
+ function withCommandFields(json, chainIds, isDev) {
917
+ let fields = Stdlib_JSON.Decode.object(json);
918
+ if (fields === undefined) {
919
+ return Stdlib_JsError.throwWithMessage("Invalid indexer config: not an object");
920
+ }
921
+ let narrowed = Object.assign({}, fields);
922
+ narrowed["isolatedChains"] = S$RescriptSchema.reverseConvertToJsonOrThrow(chainIds, S$RescriptSchema.array(ChainId.schema));
923
+ narrowed["isDev"] = isDev;
924
+ return narrowed;
925
+ }
926
+
910
927
  function getPublicConfigJson() {
911
928
  let json = primedJson.contents;
912
929
  if (json !== undefined) {
@@ -950,6 +967,10 @@ function stripSensitiveData(json) {
950
967
  return cloned;
951
968
  }
952
969
 
970
+ function envioInfo() {
971
+ return stripSensitiveData(getPublicConfigJson());
972
+ }
973
+
953
974
  function canonicalJson(json) {
954
975
  if (Array.isArray(json)) {
955
976
  return json.map(canonicalJson);
@@ -1172,14 +1193,17 @@ export {
1172
1193
  publicConfigSchema,
1173
1194
  contractMappingOf,
1174
1195
  getChain,
1196
+ isPerChain,
1175
1197
  isolate,
1176
1198
  fromPublic,
1177
1199
  normalizeUserAddress,
1178
1200
  normalizeSimulateAddress,
1179
1201
  getEventConfig,
1180
1202
  prime,
1203
+ withCommandFields,
1181
1204
  getPublicConfigJson,
1182
1205
  stripSensitiveData,
1206
+ envioInfo,
1183
1207
  canonicalJson,
1184
1208
  diffPaths,
1185
1209
  throwIfIncompatible,
package/src/Core.res CHANGED
@@ -166,6 +166,10 @@ let loadDevAddon: ({..}, string) => addon = %raw(`function(req, envioDir) {
166
166
  fs.copyFileSync(srcPath, nodePath);
167
167
  }
168
168
 
169
+ // Forked workers inherit this, so only the first process in a run pays for
170
+ // the cargo build (and they don't contend over the cargo lock).
171
+ process.env.ENVIO_DEV_ADDON = nodePath;
172
+
169
173
  return req(nodePath);
170
174
  }`)
171
175
 
package/src/Core.res.mjs CHANGED
@@ -88,6 +88,10 @@ let loadDevAddon = (function(req, envioDir) {
88
88
  fs.copyFileSync(srcPath, nodePath);
89
89
  }
90
90
 
91
+ // Forked workers inherit this, so only the first process in a run pays for
92
+ // the cargo build (and they don't contend over the cargo lock).
93
+ process.env.ENVIO_DEV_ADDON = nodePath;
94
+
91
95
  return req(nodePath);
92
96
  });
93
97
 
@@ -17,6 +17,11 @@ type t = {
17
17
  mutable isCaughtUp: bool,
18
18
  // Indexer-wide fetch buffer pool (item count), shared across all chains.
19
19
  targetBufferSize: int,
20
+ // Set on a process driving part of a split run: the chains it drives may be
21
+ // at the head while chains in another process are still backfilling, and an
22
+ // indexer switches to realtime as a whole or not at all. Cleared by the
23
+ // supervisor once every chain in the run has arrived.
24
+ mutable holdRealtime: bool,
20
25
  }
21
26
 
22
27
  // The whole-indexer fetch buffer pool, independent of chain count.
@@ -26,16 +31,50 @@ let calculateTargetBufferSize = () =>
26
31
  | None => 100_000
27
32
  }
28
33
 
29
- let make = (~chainStates, ~isRealtime, ~targetBufferSize=calculateTargetBufferSize()): t => {
34
+ let make = (
35
+ ~chainStates,
36
+ ~isRealtime,
37
+ ~targetBufferSize=calculateTargetBufferSize(),
38
+ ~holdRealtime=false,
39
+ ): t => {
30
40
  {
31
41
  chainStates,
32
42
  chainIds: chainStates->Dict.valuesToArray->Array.map(cs => (cs->ChainState.chainConfig).id),
33
43
  isRealtime,
34
44
  isCaughtUp: isRealtime,
35
45
  targetBufferSize,
46
+ holdRealtime,
36
47
  }
37
48
  }
38
49
 
50
+ // The supervisor's go-ahead: every chain in the run has reached the head, so
51
+ // this process may make the transitions it has been holding back.
52
+ let releaseRealtime = (crossChainState: t) => crossChainState.holdRealtime = false
53
+
54
+ let isHoldingRealtime = (crossChainState: t) => crossChainState.holdRealtime
55
+
56
+ // Whether this process has got as far as it can without the run's leave. What a
57
+ // supervisor reads to decide that a split run may go realtime as one.
58
+ //
59
+ // Three ways to have arrived, because a chain can be as far along as it can get
60
+ // in three different states. Its chains have caught up; or it resumed already
61
+ // realtime; or every chain is waiting to enter the reorg threshold, which is as
62
+ // far as one can fetch while the pre-threshold lag holds it at the safe block —
63
+ // entering the threshold is what lifts that lag, so a run held until its chains
64
+ // reached the head would be holding back the transition that gets them there.
65
+ //
66
+ // The process's own conclusion rather than a reading a supervisor reassembles:
67
+ // a chain committed at what was the head and resumed once the head had moved on
68
+ // has arrived, and no live reading of it can say so — which is the same reason
69
+ // `markCaughtUpOnResume` decides before any source request.
70
+ let hasArrivedAtHead = (crossChainState: t) =>
71
+ crossChainState.isCaughtUp ||
72
+ crossChainState.isRealtime || {
73
+ let chainStates = crossChainState.chainStates->Dict.valuesToArray
74
+ chainStates->Utils.Array.notEmpty &&
75
+ chainStates->Array.every(ChainState.isReadyToEnterReorgThreshold)
76
+ }
77
+
39
78
  // Resolve a chain's state by id. The id always comes from `chainIds`, which is
40
79
  // derived from `chainStates`, so the entry is guaranteed present.
41
80
  let getChainState = (crossChainState: t, chainId) =>
@@ -122,13 +161,29 @@ let createBatch = (
122
161
 
123
162
  // Enter the reorg threshold: shrink each chain's buffer by its configured
124
163
  // blockLag and flip the flag.
125
- let enterReorgThreshold = (crossChainState: t) => {
126
- Logging.info("Reorg threshold reached")
164
+ // Whether every chain this process drives has buffered close enough to the head
165
+ // to enter the threshold together — and, in a split run, whether the rest of the
166
+ // run has too. Chains enter it as one indexer, so one chain still backfilling
167
+ // holds the others back whatever process it runs in.
168
+ let isReadyToEnterReorgThreshold = (crossChainState: t, ~batch) =>
169
+ !crossChainState.holdRealtime &&
170
+ crossChainState.chainStates
171
+ ->Dict.valuesToArray
172
+ ->Array.every(cs => cs->ChainState.isReadyToEnterReorgThresholdAfterBatch(~batch))
127
173
 
174
+ // Said by each chain rather than once for the indexer: what crossing changes
175
+ // is a chain's own, and the chains of a split run cross in processes that can
176
+ // only speak for the ones they drive.
177
+ let enterReorgThreshold = (crossChainState: t) => {
128
178
  for i in 0 to crossChainState.chainIds->Array.length - 1 {
129
- crossChainState
130
- ->getChainState(crossChainState.chainIds->Array.getUnsafe(i))
131
- ->ChainState.enterReorgThreshold
179
+ let cs = crossChainState->getChainState(crossChainState.chainIds->Array.getUnsafe(i))
180
+ // A chain whose end block sits below these blocks says nothing: crossing
181
+ // gives it nothing more to index, and it will never reach one of them.
182
+ let liftsCeiling = cs->ChainState.reorgThresholdLiftsCeiling
183
+ cs->ChainState.enterReorgThreshold
184
+ if liftsCeiling {
185
+ cs->ChainState.logger->Logging.childInfo(ChainState.reorgThresholdEntryMessage)
186
+ }
132
187
  }
133
188
  }
134
189
 
@@ -150,7 +205,10 @@ let applyBatchProgress = (crossChainState: t, ~batch: Batch.t, ~blockTimestampNa
150
205
  }
151
206
 
152
207
  crossChainState.isCaughtUp =
153
- crossChainState.isCaughtUp || (crossChainState->nextItemIsNone && everyChainCaughtUp.contents)
208
+ crossChainState.isCaughtUp ||
209
+ (!crossChainState.holdRealtime &&
210
+ crossChainState->nextItemIsNone &&
211
+ everyChainCaughtUp.contents)
154
212
  }
155
213
 
156
214
  // Every chain has buffered up to its head (or endblock) with nothing
@@ -174,8 +232,13 @@ let isSettledAtHead = (crossChainState: t) => {
174
232
  }
175
233
 
176
234
  // Enter the FinalizingIndexes phase without a batch, for the resume above.
235
+ // Not while the run holds this process back: the hold keeps the pre-threshold
236
+ // lag in place, and a chain that has fetched to a lagged head it was never
237
+ // going to get past reads as settled without having indexed anything. What a
238
+ // held process may conclude about where it stands, it concludes from what was
239
+ // persisted — see `markCaughtUpOnResume`, which the hold leaves alone.
177
240
  let markCaughtUpIfSettled = (crossChainState: t) =>
178
- if crossChainState->isSettledAtHead {
241
+ if !crossChainState.holdRealtime && crossChainState->isSettledAtHead {
179
242
  crossChainState.isCaughtUp = true
180
243
  }
181
244
 
@@ -206,13 +269,48 @@ let markCaughtUpOnResume = (crossChainState: t) => {
206
269
  // and switches the indexer to realtime.
207
270
  let markReady = (crossChainState: t, ~readyAt) => {
208
271
  for i in 0 to crossChainState.chainIds->Array.length - 1 {
209
- crossChainState
210
- ->getChainState(crossChainState.chainIds->Array.getUnsafe(i))
211
- ->ChainState.markReady(~readyAt)
272
+ let cs = crossChainState->getChainState(crossChainState.chainIds->Array.getUnsafe(i))
273
+ let wasReady = cs->ChainState.isReady
274
+ cs->ChainState.markReady(~readyAt)
275
+
276
+ // One line per chain, because `ready_at` is one column per chain: what the
277
+ // log says and what a reader finds in the row are the same fact.
278
+ if !wasReady {
279
+ cs
280
+ ->ChainState.logger
281
+ ->Logging.childInfo("Ready. Fully indexed for queries.")
282
+ }
212
283
  }
213
284
  crossChainState.isRealtime = true
214
285
  }
215
286
 
287
+ // Each chain that has just finished indexing, said once, by the chain it is
288
+ // about — so a chain that finishes early says so then, rather than when the
289
+ // last chain in its process catches up.
290
+ let reportFinished = (crossChainState: t) => {
291
+ let waitingOnOthers = crossChainState.holdRealtime || crossChainState.chainIds->Array.length > 1
292
+ crossChainState.chainStates
293
+ ->Dict.valuesToArray
294
+ ->Array.forEach(cs =>
295
+ switch cs->ChainState.takeFinished {
296
+ | Some(EndBlock(block)) =>
297
+ cs
298
+ ->ChainState.logger
299
+ ->Logging.childInfo({"msg": "Indexed to the end block. This chain is done.", "block": block})
300
+ | Some(Backfill(block)) =>
301
+ cs
302
+ ->ChainState.logger
303
+ ->Logging.childInfo({
304
+ "msg": waitingOnOthers
305
+ ? "Finished backfill. Waiting for the other chains."
306
+ : "Finished backfill.",
307
+ "block": block,
308
+ })
309
+ | None => ()
310
+ }
311
+ )
312
+ }
313
+
216
314
  // --- Fetch control. ---
217
315
 
218
316
  // Chains ordered furthest-behind first by fetch-frontier progress, so the
@@ -20,19 +20,41 @@ function calculateTargetBufferSize() {
20
20
  }
21
21
  }
22
22
 
23
- function make(chainStates, isRealtime, targetBufferSizeOpt) {
23
+ function make(chainStates, isRealtime, targetBufferSizeOpt, holdRealtimeOpt) {
24
24
  let targetBufferSize = targetBufferSizeOpt !== undefined ? targetBufferSizeOpt : (
25
25
  Env.targetBufferSize !== undefined ? Env.targetBufferSize : 100000
26
26
  );
27
+ let holdRealtime = holdRealtimeOpt !== undefined ? holdRealtimeOpt : false;
27
28
  return {
28
29
  chainStates: chainStates,
29
30
  chainIds: Object.values(chainStates).map(cs => ChainState.chainConfig(cs).id),
30
31
  isRealtime: isRealtime,
31
32
  isCaughtUp: isRealtime,
32
- targetBufferSize: targetBufferSize
33
+ targetBufferSize: targetBufferSize,
34
+ holdRealtime: holdRealtime
33
35
  };
34
36
  }
35
37
 
38
+ function releaseRealtime(crossChainState) {
39
+ crossChainState.holdRealtime = false;
40
+ }
41
+
42
+ function isHoldingRealtime(crossChainState) {
43
+ return crossChainState.holdRealtime;
44
+ }
45
+
46
+ function hasArrivedAtHead(crossChainState) {
47
+ if (crossChainState.isCaughtUp || crossChainState.isRealtime) {
48
+ return true;
49
+ }
50
+ let chainStates = Object.values(crossChainState.chainStates);
51
+ if (Utils.$$Array.notEmpty(chainStates)) {
52
+ return chainStates.every(ChainState.isReadyToEnterReorgThreshold);
53
+ } else {
54
+ return false;
55
+ }
56
+ }
57
+
36
58
  function chainStates(crossChainState) {
37
59
  return crossChainState.chainStates;
38
60
  }
@@ -94,10 +116,22 @@ function createBatch(crossChainState, config, frontier, batchSizeTarget) {
94
116
  return Batch.make(config.checkpointSequence, frontier, Utils.Dict.mapValues(crossChainState.chainStates, none => ChainState.toChainBeforeBatch(none, crossChainState.isRealtime)), batchSizeTarget, HistoryPolicy.decide(config, shouldSaveHistory(crossChainState)));
95
117
  }
96
118
 
119
+ function isReadyToEnterReorgThreshold(crossChainState, batch) {
120
+ if (crossChainState.holdRealtime) {
121
+ return false;
122
+ } else {
123
+ return Object.values(crossChainState.chainStates).every(cs => ChainState.isReadyToEnterReorgThresholdAfterBatch(cs, batch));
124
+ }
125
+ }
126
+
97
127
  function enterReorgThreshold(crossChainState) {
98
- Logging.info("Reorg threshold reached");
99
128
  for (let i = 0, i_finish = crossChainState.chainIds.length; i < i_finish; ++i) {
100
- ChainState.enterReorgThreshold(crossChainState.chainStates[crossChainState.chainIds[i]]);
129
+ let cs = crossChainState.chainStates[crossChainState.chainIds[i]];
130
+ let liftsCeiling = ChainState.reorgThresholdLiftsCeiling(cs);
131
+ ChainState.enterReorgThreshold(cs);
132
+ if (liftsCeiling) {
133
+ Logging.childInfo(ChainState.logger(cs), ChainState.reorgThresholdEntryMessage);
134
+ }
101
135
  }
102
136
  }
103
137
 
@@ -111,7 +145,7 @@ function applyBatchProgress(crossChainState, batch, blockTimestampName) {
111
145
  everyChainCaughtUp = false;
112
146
  }
113
147
  }
114
- crossChainState.isCaughtUp = crossChainState.isCaughtUp || nextItemIsNone(crossChainState) && everyChainCaughtUp;
148
+ crossChainState.isCaughtUp = crossChainState.isCaughtUp || !crossChainState.holdRealtime && nextItemIsNone(crossChainState) && everyChainCaughtUp;
115
149
  }
116
150
 
117
151
  function isSettledAtHead(crossChainState) {
@@ -125,7 +159,7 @@ function isSettledAtHead(crossChainState) {
125
159
  }
126
160
 
127
161
  function markCaughtUpIfSettled(crossChainState) {
128
- if (isSettledAtHead(crossChainState)) {
162
+ if (!crossChainState.holdRealtime && isSettledAtHead(crossChainState)) {
129
163
  crossChainState.isCaughtUp = true;
130
164
  return;
131
165
  }
@@ -147,11 +181,36 @@ function markCaughtUpOnResume(crossChainState) {
147
181
 
148
182
  function markReady(crossChainState, readyAt) {
149
183
  for (let i = 0, i_finish = crossChainState.chainIds.length; i < i_finish; ++i) {
150
- ChainState.markReady(crossChainState.chainStates[crossChainState.chainIds[i]], readyAt);
184
+ let cs = crossChainState.chainStates[crossChainState.chainIds[i]];
185
+ let wasReady = ChainState.isReady(cs);
186
+ ChainState.markReady(cs, readyAt);
187
+ if (!wasReady) {
188
+ Logging.childInfo(ChainState.logger(cs), "Ready. Fully indexed for queries.");
189
+ }
151
190
  }
152
191
  crossChainState.isRealtime = true;
153
192
  }
154
193
 
194
+ function reportFinished(crossChainState) {
195
+ let waitingOnOthers = crossChainState.holdRealtime || crossChainState.chainIds.length > 1;
196
+ Object.values(crossChainState.chainStates).forEach(cs => {
197
+ let match = ChainState.takeFinished(cs);
198
+ if (match !== undefined) {
199
+ if (match.TAG === "EndBlock") {
200
+ return Logging.childInfo(ChainState.logger(cs), {
201
+ msg: "Indexed to the end block. This chain is done.",
202
+ block: match._0
203
+ });
204
+ } else {
205
+ return Logging.childInfo(ChainState.logger(cs), {
206
+ msg: waitingOnOthers ? "Finished backfill. Waiting for the other chains." : "Finished backfill.",
207
+ block: match._0
208
+ });
209
+ }
210
+ }
211
+ });
212
+ }
213
+
155
214
  function priorityOrder(crossChainState) {
156
215
  return Object.values(crossChainState.chainStates).toSorted((a, b) => Primitive_float.compare(ChainState.frontierProgress(a), ChainState.frontierProgress(b)));
157
216
  }
@@ -256,12 +315,17 @@ export {
256
315
  shouldSaveHistory,
257
316
  nextItemIsNone,
258
317
  getSafeCheckpointIdByChain,
318
+ releaseRealtime,
319
+ isHoldingRealtime,
320
+ hasArrivedAtHead,
321
+ isReadyToEnterReorgThreshold,
259
322
  createBatch,
260
323
  enterReorgThreshold,
261
324
  applyBatchProgress,
262
325
  markCaughtUpIfSettled,
263
326
  markCaughtUpOnResume,
264
327
  markReady,
328
+ reportFinished,
265
329
  priorityOrder,
266
330
  checkAndFetch,
267
331
  }
@@ -5,7 +5,12 @@ type t
5
5
 
6
6
  let calculateTargetBufferSize: unit => int
7
7
 
8
- let make: (~chainStates: dict<ChainState.t>, ~isRealtime: bool, ~targetBufferSize: int=?) => t
8
+ let make: (
9
+ ~chainStates: dict<ChainState.t>,
10
+ ~isRealtime: bool,
11
+ ~targetBufferSize: int=?,
12
+ ~holdRealtime: bool=?,
13
+ ) => t
9
14
 
10
15
  // Accessors.
11
16
  let chainStates: t => dict<ChainState.t>
@@ -25,12 +30,17 @@ let getSafeCheckpointIdByChain: (
25
30
  ) => array<(ChainId.t, option<Internal.checkpointId>)>
26
31
 
27
32
  // Cross-chain transitions.
33
+ let releaseRealtime: t => unit
34
+ let isHoldingRealtime: t => bool
35
+ let hasArrivedAtHead: t => bool
36
+ let isReadyToEnterReorgThreshold: (t, ~batch: Batch.t) => bool
28
37
  let createBatch: (t, ~config: Config.t, ~frontier: Frontier.t, ~batchSizeTarget: int) => Batch.t
29
38
  let enterReorgThreshold: t => unit
30
39
  let applyBatchProgress: (t, ~batch: Batch.t, ~blockTimestampName: string) => unit
31
40
  let markCaughtUpIfSettled: t => unit
32
41
  let markCaughtUpOnResume: t => unit
33
42
  let markReady: (t, ~readyAt: Date.t) => unit
43
+ let reportFinished: t => unit
34
44
 
35
45
  // Fetch control.
36
46
  let priorityOrder: t => array<ChainState.t>
package/src/Env.res CHANGED
@@ -127,6 +127,10 @@ module Db = {
127
127
  //the SSL modes should be provided as string otherwise as 'require' | 'allow' | 'prefer' | 'verify-full'
128
128
  ~devFallback=Bool(false),
129
129
  )
130
+ // The budget for the whole run, not for one process: a run that splits across
131
+ // workers divides it among them, and each caps its own pool to its share.
132
+ // Splitting takes two workers and a worker takes two connections, so a budget
133
+ // under 4 — the default among them — keeps the run in one process.
130
134
  let maxConnections = envSafe->EnvSafe.get("ENVIO_PG_MAX_CONNECTIONS", S.int, ~fallback=2)
131
135
  }
132
136
 
@@ -11,14 +11,25 @@ let log = (self: t) => {
11
11
  }
12
12
  }
13
13
 
14
+ // The exception to hand to whoever is outside the indexer. A failure raised
15
+ // from a message alone carries no exception of its own, so rethrowing or
16
+ // rejecting with `exn` would hand over a bare `null` and the reason would live
17
+ // only in the logs. Fall back to the message, which is the whole of what such a
18
+ // failure knows.
19
+ let toExn = (self: t) =>
20
+ switch self.exn->(Utils.magic: exn => Nullable.t<exn>)->Nullable.toOption {
21
+ | Some(exn) => exn->Utils.prettifyExn
22
+ | None => Utils.Error.make(self.msg->Option.getOr("Indexer has failed with an unexpected error"))
23
+ }
24
+
14
25
  let raiseExn = (self: t) => {
15
- self.exn->Utils.prettifyExn->throw
26
+ self->toExn->throw
16
27
  }
17
28
 
18
29
  let mkLogAndRaise = (~logger=?, ~msg=?, exn) => {
19
- let exn = exn->Utils.prettifyExn
20
- exn->make(~logger?, ~msg?)->log
21
- exn->throw
30
+ let self = exn->Utils.prettifyExn->make(~logger?, ~msg?)
31
+ self->log
32
+ self->raiseExn
22
33
  }
23
34
 
24
35
  let unwrapLogAndRaise = (~logger=?, ~msg=?, result) => {
@@ -2,6 +2,7 @@
2
2
 
3
3
  import * as Utils from "./Utils.res.mjs";
4
4
  import * as Logging from "./Logging.res.mjs";
5
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
6
 
6
7
  function make(exn, loggerOpt, msg) {
7
8
  let logger = loggerOpt !== undefined ? loggerOpt : Logging.getLogger();
@@ -23,14 +24,23 @@ function log(self) {
23
24
  }
24
25
  }
25
26
 
27
+ function toExn(self) {
28
+ let exn = self.exn;
29
+ if (exn == null) {
30
+ return new Error(Stdlib_Option.getOr(self.msg, "Indexer has failed with an unexpected error"));
31
+ } else {
32
+ return Utils.prettifyExn(exn);
33
+ }
34
+ }
35
+
26
36
  function raiseExn(self) {
27
- throw Utils.prettifyExn(self.exn);
37
+ throw toExn(self);
28
38
  }
29
39
 
30
40
  function mkLogAndRaise(logger, msg, exn) {
31
- let exn$1 = Utils.prettifyExn(exn);
32
- log(make(exn$1, logger, msg));
33
- throw exn$1;
41
+ let self = make(Utils.prettifyExn(exn), logger, msg);
42
+ log(self);
43
+ throw toExn(self);
34
44
  }
35
45
 
36
46
  function unwrapLogAndRaise(logger, msg, result) {
@@ -43,12 +53,13 @@ function unwrapLogAndRaise(logger, msg, result) {
43
53
 
44
54
  function logAndRaise(self) {
45
55
  log(self);
46
- throw Utils.prettifyExn(self.exn);
56
+ throw toExn(self);
47
57
  }
48
58
 
49
59
  export {
50
60
  make,
51
61
  log,
62
+ toExn,
52
63
  raiseExn,
53
64
  mkLogAndRaise,
54
65
  unwrapLogAndRaise,
@@ -11,9 +11,15 @@
11
11
  // `envio start --chain` process is indexing and never waits on one.
12
12
 
13
13
  let runOnce = async (state: IndexerState.t) => {
14
- Logging.info(
15
- "All chains are caught up. Finalizing the indexer before switching to realtime: flushing pending writes, then creating the indexes the schema promises.",
16
- )
14
+ // Said by the process rather than by each of its chains: the indexes are one
15
+ // build over the tables, and the pause is the whole process's. A chain has
16
+ // already said it caught up, and says it is ready once this commits. The
17
+ // chains are named because the pause is theirs, and a split run has a process
18
+ // saying this for each part of it.
19
+ Logging.info({
20
+ "msg": "Building database indexes. Indexing is paused until they are ready, which can take a while on a large database.",
21
+ "chainIds": state->IndexerState.crossChainState->CrossChainState.chainIds,
22
+ })
17
23
 
18
24
  await Writing.flush(state)
19
25
 
@@ -32,8 +38,8 @@ let runOnce = async (state: IndexerState.t) => {
32
38
 
33
39
  // Only after the commit: in-memory readiness must never run ahead of the
34
40
  // `ready_at` a restart would read back.
41
+ // Says so per chain, which is the grain `ready_at` is committed at.
35
42
  state->IndexerState.markReady(~readyAt)
36
- Logging.info("The indexer is ready. Switching to realtime indexing.")
37
43
  }
38
44
  }
39
45
 
@@ -7,7 +7,10 @@ import * as IndexerState from "./IndexerState.res.mjs";
7
7
  import * as CrossChainState from "./CrossChainState.res.mjs";
8
8
 
9
9
  async function runOnce(state) {
10
- Logging.info("All chains are caught up. Finalizing the indexer before switching to realtime: flushing pending writes, then creating the indexes the schema promises.");
10
+ Logging.info({
11
+ msg: "Building database indexes. Indexing is paused until they are ready, which can take a while on a large database.",
12
+ chainIds: CrossChainState.chainIds(IndexerState.crossChainState(state))
13
+ });
11
14
  await Writing.flush(state);
12
15
  if (IndexerState.hasFailedWrite(state)) {
13
16
  return;
@@ -16,8 +19,7 @@ async function runOnce(state) {
16
19
  let storage = Persistence.getInitializedStorageOrThrow(persistence);
17
20
  let readyAt = new Date();
18
21
  await storage.finalizeBackfill(persistence.allEntities, CrossChainState.chainIds(IndexerState.crossChainState(state)), readyAt);
19
- IndexerState.markReady(state, readyAt);
20
- return Logging.info("The indexer is ready. Switching to realtime indexing.");
22
+ return IndexerState.markReady(state, readyAt);
21
23
  }
22
24
 
23
25
  function run(state) {
@@ -493,41 +493,6 @@ let getSimulateOnEventRegistrations = (
493
493
  }
494
494
  }
495
495
 
496
- // An RPC source can only deliver the fields it knows how to parse; the rest are
497
- // silently skipped at materialisation. Every RPC on the chain counts, whatever
498
- // it's for — a fallback or realtime source runs the same parsers as a sync one,
499
- // so a field it can't deliver would go missing for whichever blocks it served.
500
- // The selection can come from either `config.yaml` or the inline `fields`
501
- // option, so the message names neither. Runs on the registrations a chain
502
- // actually keeps, so a handler whose `where` opts out of this chain isn't held
503
- // to its limits.
504
- //
505
- // The `config.yaml` half of this is also rejected at codegen, by the
506
- // `RpcTransactionField` subenum in `system_config.rs`. That one reports every
507
- // offending field at once and before the project builds; this one is the only
508
- // check an inline selection reaches. `RpcFieldSelection_test.res` pins the two
509
- // to the same field set.
510
- let validateRpcFieldSelection = (
511
- chainConfig: Config.chain,
512
- registrations: array<Internal.onEventRegistration>,
513
- ) => {
514
- let hasRpc = switch chainConfig.sourceConfig {
515
- | EvmSourceConfig({rpcs}) => !(rpcs->Utils.Array.isEmpty)
516
- | _ => false
517
- }
518
- if hasRpc {
519
- registrations->Array.forEach(reg =>
520
- reg.fieldSelection.transactionFields->Utils.Set.forEach(name =>
521
- if !RpcSource.isRpcTransactionField(name) {
522
- JsError.throwWithMessage(
523
- `The "${name}" transaction field selected for the "${reg.eventConfig.name}" event on contract "${reg.eventConfig.contractName}" is unavailable for indexing via RPC. Remove it from the field selection, or remove chain ${chainConfig.id->ChainId.toString}'s RPC source — even an RPC the chain only falls back to has to deliver the selection.`,
524
- )
525
- }
526
- )
527
- )
528
- }
529
- }
530
-
531
496
  let finishRegistration = (~config: Config.t): registrationsByChainId => {
532
497
  switch getActiveRegistration() {
533
498
  | Some(r) => {
@@ -614,8 +579,6 @@ let finishRegistration = (~config: Config.t): registrationsByChainId => {
614
579
  }
615
580
  })
616
581
 
617
- validateRpcFieldSelection(chainConfig, onEventRegistrations)
618
-
619
582
  registrationsByChainId->Dict.set(
620
583
  key,
621
584
  {
@@ -5,7 +5,6 @@ import * as ChainId from "./ChainId.res.mjs";
5
5
  import * as Logging from "./Logging.res.mjs";
6
6
  import * as ChainMap from "./ChainMap.res.mjs";
7
7
  import * as Internal from "./Internal.res.mjs";
8
- import * as RpcSource from "./sources/RpcSource.res.mjs";
9
8
  import * as EnvioGlobal from "./EnvioGlobal.res.mjs";
10
9
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
11
10
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
@@ -327,22 +326,6 @@ function getSimulateOnEventRegistrations(config, chainId, eventConfig) {
327
326
  }
328
327
  }
329
328
 
330
- function validateRpcFieldSelection(chainConfig, registrations) {
331
- let match = chainConfig.sourceConfig;
332
- let hasRpc;
333
- hasRpc = match.TAG === "EvmSourceConfig" ? !Utils.$$Array.isEmpty(match.rpcs) : false;
334
- if (hasRpc) {
335
- registrations.forEach(reg => {
336
- reg.fieldSelection.transactionFields.forEach(name => {
337
- if (!RpcSource.isRpcTransactionField(name)) {
338
- return Stdlib_JsError.throwWithMessage(`The "` + name + `" transaction field selected for the "` + reg.eventConfig.name + `" event on contract "` + reg.eventConfig.contractName + `" is unavailable for indexing via RPC. Remove it from the field selection, or remove chain ` + ChainId.toString(chainConfig.id) + `'s RPC source — even an RPC the chain only falls back to has to deliver the selection.`);
339
- }
340
- });
341
- });
342
- return;
343
- }
344
- }
345
-
346
329
  function finishRegistration(config) {
347
330
  let r = EnvioGlobal.value.activeRegistration;
348
331
  if (r === undefined) {
@@ -389,7 +372,6 @@ function finishRegistration(config) {
389
372
  let newrecord = {...reg};
390
373
  onEventRegistrations.push((newrecord.index = onEventRegistrations.length, newrecord));
391
374
  });
392
- validateRpcFieldSelection(chainConfig, onEventRegistrations);
393
375
  let chainRegs = r.registrationsByChainId[key];
394
376
  registrationsByChainId[key] = {
395
377
  onEventRegistrations: onEventRegistrations,