envio 3.11.0 → 3.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) 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/ChainFetching.res +7 -3
  8. package/src/ChainFetching.res.mjs +1 -1
  9. package/src/ChainState.res +82 -20
  10. package/src/ChainState.res.mjs +51 -13
  11. package/src/ChainState.resi +6 -1
  12. package/src/Config.res +58 -37
  13. package/src/Config.res.mjs +63 -53
  14. package/src/CrossChainState.res +50 -53
  15. package/src/CrossChainState.res.mjs +25 -23
  16. package/src/CrossChainState.resi +6 -13
  17. package/src/EffectState.res +13 -4
  18. package/src/EffectState.resi +7 -1
  19. package/src/EventProcessing.res +6 -6
  20. package/src/EventProcessing.res.mjs +6 -6
  21. package/src/FetchState.res +65 -46
  22. package/src/FetchState.res.mjs +68 -36
  23. package/src/FinalizeBackfill.res +16 -12
  24. package/src/FinalizeBackfill.res.mjs +3 -3
  25. package/src/HistoryPolicy.res +46 -0
  26. package/src/HistoryPolicy.res.mjs +50 -0
  27. package/src/InMemoryStore.res +30 -32
  28. package/src/InMemoryStore.res.mjs +33 -28
  29. package/src/InMemoryTable.res +155 -67
  30. package/src/InMemoryTable.res.mjs +151 -60
  31. package/src/IndexerState.res +66 -45
  32. package/src/IndexerState.res.mjs +57 -29
  33. package/src/IndexerState.resi +8 -13
  34. package/src/Internal.res +4 -2
  35. package/src/LoadLayer.res +68 -38
  36. package/src/LoadLayer.res.mjs +50 -64
  37. package/src/LoadLayer.resi +1 -1
  38. package/src/Main.res +25 -24
  39. package/src/Main.res.mjs +24 -25
  40. package/src/Metrics.res +49 -1
  41. package/src/Metrics.res.mjs +26 -3
  42. package/src/Persistence.res +55 -16
  43. package/src/Persistence.res.mjs +18 -3
  44. package/src/PgStorage.res +286 -180
  45. package/src/PgStorage.res.mjs +209 -122
  46. package/src/PruneStaleHistory.res +9 -15
  47. package/src/PruneStaleHistory.res.mjs +11 -20
  48. package/src/Rollback.res +9 -14
  49. package/src/Rollback.res.mjs +5 -3
  50. package/src/Sink.res +7 -5
  51. package/src/Sink.res.mjs +4 -4
  52. package/src/Staging.res +298 -0
  53. package/src/Staging.res.mjs +257 -0
  54. package/src/TestIndexer.res +14 -30
  55. package/src/TestIndexer.res.mjs +11 -9
  56. package/src/UserContext.res +20 -36
  57. package/src/UserContext.res.mjs +6 -9
  58. package/src/Utils.res +10 -4
  59. package/src/Utils.res.mjs +16 -16
  60. package/src/Writing.res +34 -20
  61. package/src/Writing.res.mjs +15 -20
  62. package/src/bindings/ClickHouse.res +41 -34
  63. package/src/bindings/ClickHouse.res.mjs +36 -30
  64. package/src/bindings/ClickHouseSink.res +45 -186
  65. package/src/bindings/ClickHouseSink.res.mjs +21 -174
  66. package/src/db/CheckpointSequence.res +109 -0
  67. package/src/db/CheckpointSequence.res.mjs +99 -0
  68. package/src/db/EntityFilter.res +487 -275
  69. package/src/db/EntityFilter.res.mjs +557 -309
  70. package/src/db/EntityHistory.res +6 -5
  71. package/src/db/EntityHistory.res.mjs +5 -5
  72. package/src/db/Frontier.res +86 -0
  73. package/src/db/Frontier.res.mjs +126 -0
  74. package/src/db/InternalTable.res +145 -27
  75. package/src/db/InternalTable.res.mjs +115 -29
  76. package/src/db/RollbackFloors.res +34 -41
  77. package/src/db/RollbackFloors.res.mjs +27 -71
  78. package/src/db/Table.res +21 -6
  79. package/src/db/Table.res.mjs +13 -4
  80. package/src/sources/BlockStore.res +8 -0
  81. package/src/sources/EvmHyperSyncSource.res +5 -1
  82. package/src/sources/EvmHyperSyncSource.res.mjs +4 -3
  83. package/src/sources/FuelHyperSyncSource.res +1 -0
  84. package/src/sources/FuelHyperSyncSource.res.mjs +1 -1
  85. package/src/sources/HyperSync.res +6 -0
  86. package/src/sources/HyperSync.res.mjs +5 -2
  87. package/src/sources/HyperSync.resi +2 -0
  88. package/src/sources/HyperSyncClient.res +6 -0
  89. package/src/sources/HyperSyncSSE.res +1 -1
  90. package/src/sources/HyperSyncSSE.res.mjs +4 -10
  91. package/src/sources/RequestStat.res +8 -1
  92. package/src/sources/RpcSource.res +1 -0
  93. package/src/sources/RpcSource.res.mjs +1 -1
  94. package/src/sources/SimulateSource.res +1 -0
  95. package/src/sources/SimulateSource.res.mjs +1 -1
  96. package/src/sources/Source.res +11 -3
  97. package/src/sources/SourceManager.res +34 -13
  98. package/src/sources/SourceManager.res.mjs +26 -9
  99. package/src/sources/SourceManager.resi +2 -0
  100. package/src/sources/SvmHyperSyncClient.res +5 -0
  101. package/src/sources/SvmHyperSyncSource.res +3 -0
  102. package/src/sources/SvmHyperSyncSource.res.mjs +4 -2
  103. package/src/tui/Tui.res +56 -143
  104. package/src/tui/Tui.res.mjs +70 -166
  105. package/src/tui/components/TuiData.res +61 -16
  106. package/src/tui/components/TuiData.res.mjs +53 -15
  107. package/svm.schema.json +0 -7
  108. package/src/db/CheckpointBounds.res +0 -53
  109. package/src/db/CheckpointBounds.res.mjs +0 -44
@@ -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 */