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
@@ -105,12 +105,12 @@ let makePruneStaleEntityHistoryQuery = (
105
105
  ~entityIndex,
106
106
  ~pgSchema,
107
107
  ~chainIdColumn,
108
- ~safeCheckpoints: CheckpointBounds.t,
108
+ ~safeCheckpoints: CheckpointSequence.checkpointBoundsByChain,
109
109
  ) => {
110
110
  let historyTableRef = `"${pgSchema}"."${historyTableName(~entityName, ~entityIndex)}"`
111
111
  let keyColumns = makeKeyColumns(~chainIdColumn)
112
112
  let anchorKeys = keyColumns->Array.map(column => `t.${column}`)->Array.joinUnsafe(", ")
113
- let bounds = safeCheckpoints->CheckpointBounds.sql(~chainIdColumn, ~tableRef="t")
113
+ let bounds = safeCheckpoints->CheckpointSequence.sql(~chainIdColumn, ~tableRef="t")
114
114
 
115
115
  // Whether a key still has a row above the safe checkpoint is an aggregate
116
116
  // over the same groups as the anchor, so it's computed in the one pass
@@ -151,7 +151,7 @@ let pruneStaleEntityHistory = (
151
151
  ~chainIdColumn,
152
152
  ~safeCheckpoints,
153
153
  ),
154
- safeCheckpoints->CheckpointBounds.params,
154
+ safeCheckpoints->CheckpointSequence.params,
155
155
  )
156
156
 
157
157
  // If an entity doesn't have a history before the update
@@ -224,11 +224,12 @@ let rollback = (
224
224
  ~floors: RollbackFloors.t,
225
225
  ) => {
226
226
  let historyTableRef = `"${pgSchema}"."${historyTableName(~entityName, ~entityIndex)}"`
227
- let bounds = floors.floors->CheckpointBounds.sql(~chainIdColumn, ~tableRef=historyTableRef)
227
+ let bounds =
228
+ floors.checkpointBounds->CheckpointSequence.sql(~chainIdColumn, ~tableRef=historyTableRef)
228
229
  sql
229
230
  ->Postgres.preparedUnsafe(
230
231
  `DELETE FROM ${historyTableRef}${bounds.using} WHERE "${checkpointIdFieldName}" > ${bounds.checkpointId}${bounds.usingMatch};`,
231
- floors.floors->CheckpointBounds.params,
232
+ floors.checkpointBounds->CheckpointSequence.params,
232
233
  )
233
234
  ->Utils.Promise.ignoreValue
234
235
  }
@@ -3,9 +3,9 @@
3
3
  import * as Table from "./Table.res.mjs";
4
4
  import * as ChainId from "../ChainId.res.mjs";
5
5
  import * as Stdlib_BigInt from "@rescript/runtime/lib/es6/Stdlib_BigInt.js";
6
- import * as CheckpointBounds from "./CheckpointBounds.res.mjs";
7
6
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
8
7
  import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
8
+ import * as CheckpointSequence from "./CheckpointSequence.res.mjs";
9
9
 
10
10
  let variants = [
11
11
  "SET",
@@ -90,7 +90,7 @@ function makePruneStaleEntityHistoryQuery(entityName, entityIndex, pgSchema, cha
90
90
  let historyTableRef = `"` + pgSchema + `"."` + historyTableName(entityName, entityIndex) + `"`;
91
91
  let keyColumns = makeKeyColumns(chainIdColumn);
92
92
  let anchorKeys = keyColumns.map(column => `t.` + column).join(", ");
93
- let bounds = CheckpointBounds.sql(safeCheckpoints, chainIdColumn, "t");
93
+ let bounds = CheckpointSequence.sql(safeCheckpoints, chainIdColumn, "t");
94
94
  return `WITH anchors AS (
95
95
  SELECT ` + anchorKeys + `,
96
96
  MAX(t.` + checkpointIdFieldName + `) FILTER (WHERE t.` + checkpointIdFieldName + ` <= ` + bounds.checkpointId + `) AS keep_checkpoint_id,
@@ -107,7 +107,7 @@ WHERE ` + makeKeyMatch(chainIdColumn, "d", "a") + `
107
107
  }
108
108
 
109
109
  function pruneStaleEntityHistory(sql, entityName, entityIndex, pgSchema, chainIdColumn, safeCheckpoints) {
110
- return sql.unsafe(makePruneStaleEntityHistoryQuery(entityName, entityIndex, pgSchema, chainIdColumn, safeCheckpoints), CheckpointBounds.params(safeCheckpoints), {prepare: true});
110
+ return sql.unsafe(makePruneStaleEntityHistoryQuery(entityName, entityIndex, pgSchema, chainIdColumn, safeCheckpoints), CheckpointSequence.params(safeCheckpoints), {prepare: true});
111
111
  }
112
112
 
113
113
  function makeBackfillHistoryQuery(pgSchema, entityName, entityIndex, idPgType, chainIdColumn, chainId) {
@@ -137,8 +137,8 @@ function backfillHistory(sql, pgSchema, table, entityIndex, chainId, ids) {
137
137
 
138
138
  function rollback(sql, pgSchema, entityName, entityIndex, chainIdColumn, floors) {
139
139
  let historyTableRef = `"` + pgSchema + `"."` + historyTableName(entityName, entityIndex) + `"`;
140
- let bounds = CheckpointBounds.sql(floors.floors, chainIdColumn, historyTableRef);
141
- return sql.unsafe(`DELETE FROM ` + historyTableRef + bounds.using + ` WHERE "` + checkpointIdFieldName + `" > ` + bounds.checkpointId + bounds.usingMatch + `;`, CheckpointBounds.params(floors.floors), {prepare: true});
140
+ let bounds = CheckpointSequence.sql(floors.checkpointBounds, chainIdColumn, historyTableRef);
141
+ return sql.unsafe(`DELETE FROM ` + historyTableRef + bounds.using + ` WHERE "` + checkpointIdFieldName + `" > ` + bounds.checkpointId + bounds.usingMatch + `;`, CheckpointSequence.params(floors.checkpointBounds), {prepare: true});
142
142
  }
143
143
 
144
144
  let checkpointIdFieldType = "UInt64";
@@ -0,0 +1,86 @@
1
+ // Where each chain stands in its checkpoint sequence.
2
+ type t = dict<Internal.checkpointId>
3
+
4
+ let empty = (): t => Dict.make()
5
+
6
+ let fromEntries = (entries: array<(ChainId.t, Internal.checkpointId)>): t => {
7
+ let frontier = Dict.make()
8
+ entries->Array.forEach(((chainId, checkpointId)) =>
9
+ frontier->ChainId.Dict.set(chainId, checkpointId)
10
+ )
11
+ frontier
12
+ }
13
+
14
+ let copy = (frontier: t): t => frontier->Dict.copy
15
+
16
+ // A chain the frontier doesn't name has committed nothing yet, which is what
17
+ // the initial checkpoint id means.
18
+ let get = (frontier: t, chainId): Internal.checkpointId =>
19
+ switch frontier->ChainId.Dict.dangerouslyGetNonOption(chainId) {
20
+ | Some(checkpointId) => checkpointId
21
+ | None => Internal.initialCheckpointId
22
+ }
23
+
24
+ // Whether the frontier names the chain at all, as opposed to `get`'s reading of
25
+ // an absent chain as the initial id. A rollback frontier names only the chains
26
+ // it moved, and "no id" is not the same answer as "id zero" there.
27
+ let find = (frontier: t, chainId): option<Internal.checkpointId> =>
28
+ frontier->ChainId.Dict.dangerouslyGetNonOption(chainId)
29
+
30
+ let set = (frontier: t, chainId, checkpointId) => frontier->ChainId.Dict.set(chainId, checkpointId)
31
+
32
+ let entries = (frontier: t): array<(ChainId.t, Internal.checkpointId)> =>
33
+ frontier
34
+ ->Dict.toArray
35
+ ->Array.map(((key, checkpointId)) => (key->ChainId.normalizeOrThrow, checkpointId))
36
+
37
+ let chainIds = (frontier: t): array<ChainId.t> =>
38
+ frontier->Dict.keysToArray->Array.map(ChainId.normalizeOrThrow)
39
+
40
+ // The two parallel arrays an `unnest($1, $2) AS ...(chain_id, checkpoint_id)`
41
+ // relation reads positionally.
42
+ type unnestParams = (array<ChainId.t>, array<string>)
43
+
44
+ let unnestParams = (frontier: t): unnestParams => {
45
+ let chainIds = []
46
+ let checkpointIds = []
47
+ frontier->Utils.Dict.forEachWithKey((checkpointId, key) => {
48
+ chainIds->Array.push(key->ChainId.normalizeOrThrow)
49
+ checkpointIds->Array.push(checkpointId->BigInt.toString)
50
+ })
51
+ (chainIds, checkpointIds)
52
+ }
53
+
54
+ %%private(
55
+ let fold = (frontier: t, pick) =>
56
+ frontier
57
+ ->Dict.valuesToArray
58
+ ->Array.reduce(None, (acc, checkpointId) =>
59
+ switch acc {
60
+ | None => Some(checkpointId)
61
+ | Some(picked) => Some(pick(picked, checkpointId))
62
+ }
63
+ )
64
+ )
65
+
66
+ // A frontier naming no chain has committed nothing anywhere.
67
+ let max = (frontier: t) =>
68
+ frontier->fold(Pervasives.max)->Option.getOr(Internal.initialCheckpointId)
69
+ let min = (frontier: t) =>
70
+ frontier->fold(Pervasives.min)->Option.getOr(Internal.initialCheckpointId)
71
+
72
+ let mergeMin = (a: t, b: t): t => Utils.Dict.mergeWith(a, b, Pervasives.min)
73
+ let mergeMax = (a: t, b: t): t => Utils.Dict.mergeWith(a, b, Pervasives.max)
74
+
75
+ let equals = (a: t, b: t) => {
76
+ let same = (a: t, b: t) =>
77
+ a
78
+ ->Dict.toArray
79
+ ->Array.every(((key, checkpointId)) =>
80
+ switch b->Utils.Dict.dangerouslyGetNonOption(key) {
81
+ | Some(other) => other == checkpointId
82
+ | None => checkpointId == Internal.initialCheckpointId
83
+ }
84
+ )
85
+ same(a, b) && same(b, a)
86
+ }
@@ -0,0 +1,126 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Utils from "../Utils.res.mjs";
4
+ import * as ChainId from "../ChainId.res.mjs";
5
+ import * as Internal from "../Internal.res.mjs";
6
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
7
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
8
+ import * as Primitive_bigint from "@rescript/runtime/lib/es6/Primitive_bigint.js";
9
+
10
+ function empty() {
11
+ return {};
12
+ }
13
+
14
+ function fromEntries(entries) {
15
+ let frontier = {};
16
+ entries.forEach(param => {
17
+ frontier[param[0]] = param[1];
18
+ });
19
+ return frontier;
20
+ }
21
+
22
+ function copy(frontier) {
23
+ return Object.assign({}, frontier);
24
+ }
25
+
26
+ function get(frontier, chainId) {
27
+ let checkpointId = frontier[chainId];
28
+ if (checkpointId !== undefined) {
29
+ return checkpointId;
30
+ } else {
31
+ return Internal.initialCheckpointId;
32
+ }
33
+ }
34
+
35
+ function find(frontier, chainId) {
36
+ return frontier[chainId];
37
+ }
38
+
39
+ function set(frontier, chainId, checkpointId) {
40
+ frontier[chainId] = checkpointId;
41
+ }
42
+
43
+ function entries(frontier) {
44
+ return Object.entries(frontier).map(param => [
45
+ ChainId.normalizeOrThrow(param[0]),
46
+ param[1]
47
+ ]);
48
+ }
49
+
50
+ function chainIds(frontier) {
51
+ return Object.keys(frontier).map(ChainId.normalizeOrThrow);
52
+ }
53
+
54
+ function unnestParams(frontier) {
55
+ let chainIds = [];
56
+ let checkpointIds = [];
57
+ Utils.Dict.forEachWithKey(frontier, (checkpointId, key) => {
58
+ chainIds.push(ChainId.normalizeOrThrow(key));
59
+ checkpointIds.push(checkpointId.toString());
60
+ });
61
+ return [
62
+ chainIds,
63
+ checkpointIds
64
+ ];
65
+ }
66
+
67
+ function fold(frontier, pick) {
68
+ return Stdlib_Array.reduce(Object.values(frontier), undefined, (acc, checkpointId) => {
69
+ if (acc !== undefined) {
70
+ return pick(acc, checkpointId);
71
+ } else {
72
+ return checkpointId;
73
+ }
74
+ });
75
+ }
76
+
77
+ function max(frontier) {
78
+ return Stdlib_Option.getOr(fold(frontier, Primitive_bigint.max), Internal.initialCheckpointId);
79
+ }
80
+
81
+ function min(frontier) {
82
+ return Stdlib_Option.getOr(fold(frontier, Primitive_bigint.min), Internal.initialCheckpointId);
83
+ }
84
+
85
+ function mergeMin(a, b) {
86
+ return Utils.Dict.mergeWith(a, b, Primitive_bigint.min);
87
+ }
88
+
89
+ function mergeMax(a, b) {
90
+ return Utils.Dict.mergeWith(a, b, Primitive_bigint.max);
91
+ }
92
+
93
+ function equals(a, b) {
94
+ let same = (a, b) => Object.entries(a).every(param => {
95
+ let checkpointId = param[1];
96
+ let other = b[param[0]];
97
+ if (other !== undefined) {
98
+ return other === checkpointId;
99
+ } else {
100
+ return checkpointId === Internal.initialCheckpointId;
101
+ }
102
+ });
103
+ if (same(a, b)) {
104
+ return same(b, a);
105
+ } else {
106
+ return false;
107
+ }
108
+ }
109
+
110
+ export {
111
+ empty,
112
+ fromEntries,
113
+ copy,
114
+ get,
115
+ find,
116
+ set,
117
+ entries,
118
+ chainIds,
119
+ unnestParams,
120
+ max,
121
+ min,
122
+ mergeMin,
123
+ mergeMax,
124
+ equals,
125
+ }
126
+ /* Utils Not a pure module */
@@ -170,6 +170,7 @@ FROM "${pgSchema}"."${table.tableName}";`
170
170
  module Chains = {
171
171
  type progressFields = [
172
172
  | #progress_block
173
+ | #progress_block_time
173
174
  | #events_processed
174
175
  | #source_block
175
176
  ]
@@ -186,6 +187,7 @@ module Chains = {
186
187
  | #buffer_block
187
188
  | #ready_at
188
189
  | #_is_hyper_sync
190
+ | #checkpoint_id
189
191
  ]
190
192
 
191
193
  let fields: array<field> = [
@@ -198,19 +200,16 @@ module Chains = {
198
200
  #first_event_block,
199
201
  #buffer_block,
200
202
  #progress_block,
203
+ #progress_block_time,
201
204
  #ready_at,
202
205
  #events_processed,
203
206
  #_is_hyper_sync,
207
+ #checkpoint_id,
204
208
  ]
205
209
 
206
210
  type metaFields = {
207
211
  @as("first_event_block")
208
- firstEventBlockNumber: Null.t<
209
- // Push id first (for WHERE clause)
210
-
211
- // Then push all updateable field values (for SET clause)
212
- int,
213
- >,
212
+ firstEventBlockNumber: Null.t<int>,
214
213
  @as("buffer_block") latestFetchedBlockNumber: int,
215
214
  @as("ready_at")
216
215
  timestampCaughtUpToHeadOrEndblock: Null.t<Date.t>,
@@ -225,7 +224,9 @@ module Chains = {
225
224
  @as("max_reorg_depth") maxReorgDepth: int,
226
225
  @as("source_block") blockHeight: int,
227
226
  @as("progress_block") progressBlockNumber: int,
227
+ @as("progress_block_time") progressBlockTime: Null.t<Date.t>,
228
228
  @as("events_processed") numEventsProcessed: float,
229
+ @as("checkpoint_id") checkpointId: Internal.checkpointId,
229
230
  ...metaFields,
230
231
  }
231
232
 
@@ -265,6 +266,22 @@ module Chains = {
265
266
  mkField((#_is_hyper_sync: field :> string), Boolean, ~fieldSchema=S.bool),
266
267
  // Fully processed block number
267
268
  mkField((#progress_block: field :> string), Int32, ~fieldSchema=S.int),
269
+ // When the block in progress_block was produced, so that `now() -
270
+ // progress_block_time` is how far behind chain time the indexer is. Null
271
+ // while the source hasn't reported that block's header.
272
+ mkField(
273
+ (#progress_block_time: field :> string),
274
+ Date,
275
+ ~fieldSchema=S.null(Utils.Schema.dbDate),
276
+ ~isNullable,
277
+ ),
278
+ // The last checkpoint id the chain has committed. Kept here rather than
279
+ // read off the checkpoints table: checkpoint rows are only written while
280
+ // a rollback could reach them, but an append-only sink holds a row for
281
+ // every id ever handed out and resolves current state through them, so a
282
+ // resume has to continue the sequence even where no checkpoint row backs
283
+ // it.
284
+ mkField((#checkpoint_id: field :> string), UInt64, ~fieldSchema=S.bigint),
268
285
  ],
269
286
  )
270
287
 
@@ -280,8 +297,10 @@ module Chains = {
280
297
  latestFetchedBlockNumber: -1,
281
298
  timestampCaughtUpToHeadOrEndblock: Null.null,
282
299
  progressBlockNumber: -1,
300
+ progressBlockTime: Null.null,
283
301
  isHyperSync: false,
284
302
  numEventsProcessed: 0.,
303
+ checkpointId: Internal.initialCheckpointId,
285
304
  }
286
305
  }
287
306
 
@@ -358,8 +377,11 @@ WHERE "${(#id: field :> string)}" = $2
358
377
  timestampCaughtUpToHeadOrEndblock: Null.t<Date.t>,
359
378
  numEventsProcessed: float,
360
379
  progressBlockNumber: int,
380
+ progressBlockTime: Null.t<Date.t>,
361
381
  addressRows: AddressRows.seedRows,
362
382
  sourceBlockNumber: int,
383
+ // BIGINT, which the driver hands back as a string.
384
+ checkpointId: string,
363
385
  }
364
386
 
365
387
  let makeGetInitialStateQuery = (~pgSchema) => {
@@ -371,7 +393,9 @@ WHERE "${(#id: field :> string)}" = $2
371
393
  "${(#ready_at: field :> string)}" as "timestampCaughtUpToHeadOrEndblock",
372
394
  "${(#events_processed: field :> string)}"::float8 as "numEventsProcessed",
373
395
  "${(#progress_block: field :> string)}" as "progressBlockNumber",
374
- "${(#source_block: field :> string)}" as "sourceBlockNumber"
396
+ "${(#progress_block_time: field :> string)}" as "progressBlockTime",
397
+ "${(#source_block: field :> string)}" as "sourceBlockNumber",
398
+ "${(#checkpoint_id: field :> string)}"::TEXT as "checkpointId"
375
399
  FROM "${pgSchema}"."${table.tableName}";`
376
400
  }
377
401
 
@@ -403,7 +427,20 @@ FROM "${pgSchema}"."${table.tableName}";`
403
427
  })
404
428
  }
405
429
 
406
- let progressFields: array<progressFields> = [#progress_block, #events_processed, #source_block]
430
+ // Block timestamps are unix seconds everywhere inside the indexer - that's
431
+ // what a block header carries - and a timestamp in storage, where `now() -
432
+ // progress_block_time` is the query the column exists for.
433
+ let blockTimeToDb = (blockTime: option<int>) =>
434
+ blockTime->Option.map(seconds => Date.fromTime(seconds->Int.toFloat *. 1000.))->Null.fromOption
435
+ let blockTimeFromDb = (date: Null.t<Date.t>) =>
436
+ date->Null.toOption->Option.map(date => (date->Date.getTime /. 1000.)->Float.toInt)
437
+
438
+ let progressFields: array<progressFields> = [
439
+ #progress_block,
440
+ #progress_block_time,
441
+ #events_processed,
442
+ #source_block,
443
+ ]
407
444
 
408
445
  let makeProgressFieldsUpdateQuery = (~pgSchema) => {
409
446
  let setClauses = Array.mapWithIndex(progressFields, (field, index) => {
@@ -444,6 +481,7 @@ WHERE "id" = $1;`
444
481
  type progressedChain = {
445
482
  chainId: ChainId.t,
446
483
  progressBlockNumber: int,
484
+ progressBlockTime: option<int>,
447
485
  sourceBlockNumber: int,
448
486
  totalEventsProcessed: float,
449
487
  }
@@ -463,6 +501,8 @@ WHERE "id" = $1;`
463
501
  ->Array.push(
464
502
  switch field {
465
503
  | #progress_block => data.progressBlockNumber->(Utils.magic: int => unknown)
504
+ | #progress_block_time =>
505
+ data.progressBlockTime->blockTimeToDb->(Utils.magic: Null.t<Date.t> => unknown)
466
506
  | #events_processed => data.totalEventsProcessed->(Utils.magic: float => unknown)
467
507
  | #source_block => data.sourceBlockNumber->(Utils.magic: int => unknown)
468
508
  },
@@ -475,6 +515,31 @@ WHERE "id" = $1;`
475
515
 
476
516
  Promise.all(promises)->Utils.Promise.ignoreValue
477
517
  }
518
+
519
+ let makeSetCheckpointFrontierQuery = (~pgSchema, ~chainIdMode: ChainId.mode=Int32) => {
520
+ let chainIdArrayType = chainIdArrayType(~pgSchema, ~chainIdMode)
521
+ `UPDATE "${pgSchema}"."${table.tableName}"
522
+ SET "${(#checkpoint_id: field :> string)}" = envio_frontier.checkpoint_id
523
+ FROM unnest($1::${chainIdArrayType},$2::${(BigInt: Postgres.columnType :> string)}[]) AS envio_frontier(chain_id, checkpoint_id)
524
+ WHERE "${table.tableName}"."${(#id: field :> string)}" = envio_frontier.chain_id;`
525
+ }
526
+
527
+ // The chains the write moved, in one statement and in the batch's own
528
+ // transaction — so a chain's stored id can never outlive the rows it covers,
529
+ // nor lag behind them.
530
+ let setCheckpointFrontier = (
531
+ sql,
532
+ ~pgSchema,
533
+ ~frontier: Frontier.t,
534
+ ~chainIdMode: ChainId.mode=Int32,
535
+ ) => {
536
+ sql
537
+ ->Postgres.preparedUnsafe(
538
+ makeSetCheckpointFrontierQuery(~pgSchema, ~chainIdMode),
539
+ frontier->Frontier.unnestParams->(Utils.magic: Frontier.unnestParams => unknown),
540
+ )
541
+ ->Utils.Promise.ignoreValue
542
+ }
478
543
  }
479
544
 
480
545
  module EnvioInfo = {
@@ -556,31 +621,58 @@ module Checkpoints = {
556
621
 
557
622
  let initialCheckpointId = 0n
558
623
 
624
+ // The checkpoint a rollback's diff rows are stamped with. It never reaches
625
+ // Postgres — there the diff is written straight to the entity table — but an
626
+ // append-only sink resolves current state through the checkpoints, so without
627
+ // one of these the diff sits above the frontier while the rows it supersedes
628
+ // sit below it, and the orphaned values are what a reader sees.
629
+ type diffCheckpoint = {
630
+ chainId: ChainId.t,
631
+ checkpointId: Internal.checkpointId,
632
+ // Where the rollback left the chain. At or below its stored progress, so a
633
+ // resume counts the row as covered rather than as something to trim back to.
634
+ blockNumber: int,
635
+ }
636
+
559
637
  // One definition per column, carrying what each storage needs: the field
560
638
  // itself, the type ClickHouse gives it where that differs from Postgres, and
561
- // where a batch keeps the column's values.
639
+ // where a batch — or a rollback diff — keeps the column's values.
562
640
  type column = {
563
641
  field: fieldOrDerived,
564
642
  clickHouseFieldType: fieldType,
565
643
  valuesOf: Batch.t => array<unknown>,
644
+ diffValuesOf: array<diffCheckpoint> => array<unknown>,
566
645
  }
567
646
 
647
+ // The chain leads the key: ids are only unique within a chain, and every
648
+ // query that narrows to one chain reads a contiguous run of it.
568
649
  let columns: array<column> = [
569
650
  {
570
- field: mkField((#id: field :> string), UInt64, ~fieldSchema=S.bigint, ~isPrimaryKey),
571
- clickHouseFieldType: UInt64,
572
- valuesOf: batch => batch.checkpointIds->(Utils.magic: array<bigint> => array<unknown>),
573
- },
574
- {
575
- field: mkField((#chain_id: field :> string), ChainId, ~fieldSchema=ChainId.schema),
651
+ field: mkField(
652
+ (#chain_id: field :> string),
653
+ ChainId,
654
+ ~fieldSchema=ChainId.schema,
655
+ ~isPrimaryKey,
656
+ ),
576
657
  clickHouseFieldType: ChainId,
577
658
  valuesOf: batch =>
578
659
  batch.checkpointChainIds->(Utils.magic: array<ChainId.t> => array<unknown>),
660
+ diffValuesOf: diffs =>
661
+ diffs->Array.map(diff => diff.chainId->(Utils.magic: ChainId.t => unknown)),
662
+ },
663
+ {
664
+ field: mkField((#id: field :> string), UInt64, ~fieldSchema=S.bigint, ~isPrimaryKey),
665
+ clickHouseFieldType: UInt64,
666
+ valuesOf: batch => batch.checkpointIds->(Utils.magic: array<bigint> => array<unknown>),
667
+ diffValuesOf: diffs =>
668
+ diffs->Array.map(diff => diff.checkpointId->(Utils.magic: bigint => unknown)),
579
669
  },
580
670
  {
581
671
  field: mkField((#block_number: field :> string), Int32, ~fieldSchema=S.int),
582
672
  clickHouseFieldType: Int32,
583
673
  valuesOf: batch => batch.checkpointBlockNumbers->(Utils.magic: array<int> => array<unknown>),
674
+ diffValuesOf: diffs =>
675
+ diffs->Array.map(diff => diff.blockNumber->(Utils.magic: int => unknown)),
584
676
  },
585
677
  {
586
678
  field: mkField(
@@ -592,6 +684,8 @@ module Checkpoints = {
592
684
  clickHouseFieldType: String,
593
685
  valuesOf: batch =>
594
686
  batch.checkpointBlockHashes->(Utils.magic: array<Null.t<string>> => array<unknown>),
687
+ diffValuesOf: diffs =>
688
+ diffs->Array.map(_ => Null.Null->(Utils.magic: Null.t<string> => unknown)),
595
689
  },
596
690
  {
597
691
  field: mkField((#events_processed: field :> string), Int32, ~fieldSchema=S.int),
@@ -600,10 +694,34 @@ module Checkpoints = {
600
694
  clickHouseFieldType: UInt64,
601
695
  valuesOf: batch =>
602
696
  batch.checkpointEventsProcessed->(Utils.magic: array<int> => array<unknown>),
697
+ diffValuesOf: diffs => diffs->Array.map(_ => 0->(Utils.magic: int => unknown)),
603
698
  },
604
699
  ]
605
700
 
606
- let table = mkTable("envio_checkpoints", ~fields=columns->Array.map(({field}) => field))
701
+ let tableName = "envio_checkpoints"
702
+
703
+ let table = mkTable(tableName, ~fields=columns->Array.map(({field}) => field))
704
+
705
+ // Where each chain counts its own ids the chain has to be part of the key,
706
+ // and every bound a rollback or a prune applies names it. Under one shared
707
+ // sequence the id is unique by itself and those bounds are id ranges with no
708
+ // chain in them — which a key led by the chain can't serve.
709
+ let globalTable = mkTable(
710
+ tableName,
711
+ ~fields=columns->Array.map(({field: column}) =>
712
+ switch column {
713
+ | Table.Field(f) if f.fieldName === (#chain_id: field :> string) =>
714
+ Table.Field({...f, isPrimaryKey: false})
715
+ | column => column
716
+ }
717
+ ),
718
+ )
719
+
720
+ let tableFor = (sequence: CheckpointSequence.t) =>
721
+ switch sequence {
722
+ | SharedAcrossChains => globalTable
723
+ | PerChain => table
724
+ }
607
725
 
608
726
  let makeGetReorgCheckpointsQuery = (~pgSchema): string => {
609
727
  // The safe_block checkpoint itself is included, so it can be used for safe
@@ -640,10 +758,6 @@ WHERE cp."${(#block_hash: field :> string)}" IS NOT NULL
640
758
  ORDER BY cp."${(#id: field :> string)}";`
641
759
  }
642
760
 
643
- let makeCommitedCheckpointIdQuery = (~pgSchema) => {
644
- `SELECT COALESCE(MAX(${(#id: field :> string)}), ${initialCheckpointId->BigInt.toString}) AS id FROM "${pgSchema}"."${table.tableName}";`
645
- }
646
-
647
761
  let makeInsertCheckpointQuery = (~pgSchema, ~chainIdMode: ChainId.mode=Int32) => {
648
762
  let chainIdArrayType = chainIdArrayType(~pgSchema, ~chainIdMode)
649
763
  `INSERT INTO "${pgSchema}"."${table.tableName}" ("${(#id: field :> string)}", "${(#chain_id: field :> string)}", "${(#block_number: field :> string)}", "${(#block_hash: field :> string)}", "${(#events_processed: field :> string)}")
@@ -687,18 +801,21 @@ SELECT * FROM unnest($1::${(BigInt: Postgres.columnType :> string)}[],$2::${chai
687
801
 
688
802
  let rollback = (sql, ~pgSchema, ~floors: RollbackFloors.t) => {
689
803
  let tableRef = `"${pgSchema}"."${table.tableName}"`
690
- let bounds = floors.floors->CheckpointBounds.sql(~chainIdColumn, ~tableRef)
804
+ let bounds = floors.checkpointBounds->CheckpointSequence.sql(~chainIdColumn, ~tableRef)
691
805
  sql
692
806
  ->Postgres.preparedUnsafe(
693
807
  `DELETE FROM ${tableRef}${bounds.using} WHERE "${(#id: field :> string)}" > ${bounds.checkpointId}${bounds.usingMatch};`,
694
- floors.floors->CheckpointBounds.params,
808
+ floors.checkpointBounds->CheckpointSequence.params,
695
809
  )
696
810
  ->Utils.Promise.ignoreValue
697
811
  }
698
812
 
699
- let makePruneStaleCheckpointsQuery = (~pgSchema, ~safeCheckpoints: CheckpointBounds.t) => {
813
+ let makePruneStaleCheckpointsQuery = (
814
+ ~pgSchema,
815
+ ~safeCheckpoints: CheckpointSequence.checkpointBoundsByChain,
816
+ ) => {
700
817
  let tableRef = `"${pgSchema}"."${table.tableName}"`
701
- let bounds = safeCheckpoints->CheckpointBounds.sql(~chainIdColumn, ~tableRef)
818
+ let bounds = safeCheckpoints->CheckpointSequence.sql(~chainIdColumn, ~tableRef)
702
819
  `DELETE FROM ${tableRef}${bounds.using} WHERE "${(#id: field :> string)}" < ${bounds.checkpointId}${bounds.usingMatch};`
703
820
  }
704
821
 
@@ -706,7 +823,7 @@ SELECT * FROM unnest($1::${(BigInt: Postgres.columnType :> string)}[],$2::${chai
706
823
  sql
707
824
  ->Postgres.preparedUnsafe(
708
825
  makePruneStaleCheckpointsQuery(~pgSchema, ~safeCheckpoints),
709
- safeCheckpoints->CheckpointBounds.params,
826
+ safeCheckpoints->CheckpointSequence.params,
710
827
  )
711
828
  ->Utils.Promise.ignoreValue
712
829
 
@@ -738,7 +855,7 @@ LIMIT 1;`
738
855
  }
739
856
 
740
857
  let makeGetRollbackProgressDiffQuery = (~pgSchema, ~floors: RollbackFloors.t) => {
741
- let bounds = floors.floors->CheckpointBounds.sql(~chainIdColumn, ~tableRef="t")
858
+ let bounds = floors.checkpointBounds->CheckpointSequence.sql(~chainIdColumn, ~tableRef="t")
742
859
  `SELECT
743
860
  t."${(#chain_id: field :> string)}"::float8 as "${(#chain_id: field :> string)}",
744
861
  SUM(t."${(#events_processed: field :> string)}") as events_processed_diff,
@@ -752,7 +869,7 @@ GROUP BY t."${(#chain_id: field :> string)}";`
752
869
  sql
753
870
  ->Postgres.preparedUnsafe(
754
871
  makeGetRollbackProgressDiffQuery(~pgSchema, ~floors),
755
- floors.floors->CheckpointBounds.params,
872
+ floors.checkpointBounds->CheckpointSequence.params,
756
873
  )
757
874
  ->(
758
875
  Utils.magic: promise<unknown> => promise<
@@ -816,6 +933,7 @@ SELECT
816
933
  "${(#start_block: Chains.field :> string)}" AS "startBlock",
817
934
  "${(#end_block: Chains.field :> string)}" AS "endBlock",
818
935
  "${(#progress_block: Chains.field :> string)}" AS "progressBlock",
936
+ "${(#progress_block_time: Chains.field :> string)}" AS "progressBlockTime",
819
937
  "${(#buffer_block: Chains.field :> string)}" AS "bufferBlock",
820
938
  "${(#first_event_block: Chains.field :> string)}" AS "firstEventBlock",
821
939
  "${(#events_processed: Chains.field :> string)}"::float4 AS "eventsProcessed",