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
@@ -6,8 +6,10 @@ import * as Utils from "./Utils.res.mjs";
6
6
  import * as Logging from "./Logging.res.mjs";
7
7
  import * as ChainState from "./ChainState.res.mjs";
8
8
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
9
+ import * as HistoryPolicy from "./HistoryPolicy.res.mjs";
9
10
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
10
11
  import * as Primitive_float from "@rescript/runtime/lib/es6/Primitive_float.js";
12
+ import * as CheckpointSequence from "./db/CheckpointSequence.res.mjs";
11
13
  import * as SafeCheckpointTracking from "./SafeCheckpointTracking.res.mjs";
12
14
 
13
15
  function calculateTargetBufferSize() {
@@ -18,7 +20,7 @@ function calculateTargetBufferSize() {
18
20
  }
19
21
  }
20
22
 
21
- function make(chainStates, isInReorgThreshold, isRealtime, targetBufferSizeOpt) {
23
+ function make(chainStates, isRealtime, targetBufferSizeOpt) {
22
24
  let targetBufferSize = targetBufferSizeOpt !== undefined ? targetBufferSizeOpt : (
23
25
  Env.targetBufferSize !== undefined ? Env.targetBufferSize : 100000
24
26
  );
@@ -27,7 +29,6 @@ function make(chainStates, isInReorgThreshold, isRealtime, targetBufferSizeOpt)
27
29
  chainIds: Object.values(chainStates).map(cs => ChainState.chainConfig(cs).id),
28
30
  isRealtime: isRealtime,
29
31
  isCaughtUp: isRealtime,
30
- isInReorgThreshold: isInReorgThreshold,
31
32
  targetBufferSize: targetBufferSize
32
33
  };
33
34
  }
@@ -40,18 +41,31 @@ function isRealtime(crossChainState) {
40
41
  return crossChainState.isRealtime;
41
42
  }
42
43
 
44
+ function chainIds(crossChainState) {
45
+ return crossChainState.chainIds;
46
+ }
47
+
43
48
  function isCaughtUp(crossChainState) {
44
49
  return crossChainState.isCaughtUp;
45
50
  }
46
51
 
47
52
  function isInReorgThreshold(crossChainState) {
48
- return crossChainState.isInReorgThreshold;
53
+ let chainStates = Object.values(crossChainState.chainStates);
54
+ if (Utils.$$Array.notEmpty(chainStates)) {
55
+ return chainStates.every(ChainState.isInReorgThreshold);
56
+ } else {
57
+ return false;
58
+ }
49
59
  }
50
60
 
51
61
  function targetBufferSize(crossChainState) {
52
62
  return crossChainState.targetBufferSize;
53
63
  }
54
64
 
65
+ function shouldSaveHistory(crossChainState) {
66
+ return Utils.Dict.mapValues(crossChainState.chainStates, ChainState.shouldSaveHistory);
67
+ }
68
+
55
69
  function totalReadyCount(crossChainState) {
56
70
  let total = 0;
57
71
  for (let i = 0, i_finish = crossChainState.chainIds.length; i < i_finish; ++i) {
@@ -65,21 +79,19 @@ function nextItemIsNone(crossChainState) {
65
79
  return !Object.values(crossChainState.chainStates).some(ChainState.hasReadyItem);
66
80
  }
67
81
 
68
- function getSafeCheckpointIdByChain(crossChainState, committedCheckpointId) {
82
+ function getSafeCheckpointIdByChain(crossChainState, sequence, committedFrontier) {
69
83
  return crossChainState.chainIds.map(chainId => {
70
84
  let cs = crossChainState.chainStates[chainId];
71
85
  let tracking = ChainState.safeCheckpointTracking(cs);
72
86
  return [
73
87
  chainId,
74
- tracking !== undefined ? SafeCheckpointTracking.getSafeCheckpointId(tracking, ChainState.knownHeight(cs)) : committedCheckpointId
88
+ tracking !== undefined ? SafeCheckpointTracking.getSafeCheckpointId(tracking, ChainState.knownHeight(cs)) : CheckpointSequence.position(sequence, committedFrontier, chainId)
75
89
  ];
76
90
  });
77
91
  }
78
92
 
79
- function createBatch(crossChainState, processedCheckpointId, batchSizeTarget, isRollback) {
80
- return Batch.make(processedCheckpointId + (
81
- isRollback ? 1n : 0n
82
- ), Utils.Dict.mapValues(crossChainState.chainStates, ChainState.toChainBeforeBatch), batchSizeTarget, crossChainState.isInReorgThreshold);
93
+ function createBatch(crossChainState, config, frontier, batchSizeTarget) {
94
+ return Batch.make(config.checkpointSequence, frontier, Utils.Dict.mapValues(crossChainState.chainStates, none => ChainState.toChainBeforeBatch(none, crossChainState.isRealtime)), batchSizeTarget, HistoryPolicy.decide(config, shouldSaveHistory(crossChainState)));
83
95
  }
84
96
 
85
97
  function enterReorgThreshold(crossChainState) {
@@ -87,7 +99,6 @@ function enterReorgThreshold(crossChainState) {
87
99
  for (let i = 0, i_finish = crossChainState.chainIds.length; i < i_finish; ++i) {
88
100
  ChainState.enterReorgThreshold(crossChainState.chainStates[crossChainState.chainIds[i]]);
89
101
  }
90
- crossChainState.isInReorgThreshold = true;
91
102
  }
92
103
 
93
104
  function applyBatchProgress(crossChainState, batch, blockTimestampName) {
@@ -101,13 +112,6 @@ function applyBatchProgress(crossChainState, batch, blockTimestampName) {
101
112
  }
102
113
  }
103
114
  crossChainState.isCaughtUp = crossChainState.isCaughtUp || nextItemIsNone(crossChainState) && everyChainCaughtUp;
104
- let allChainsReady = true;
105
- for (let i$1 = 0, i_finish$1 = chainIds.length; i$1 < i_finish$1; ++i$1) {
106
- if (!ChainState.isReady(crossChainState.chainStates[chainIds[i$1]])) {
107
- allChainsReady = false;
108
- }
109
- }
110
- crossChainState.isRealtime = crossChainState.isRealtime || allChainsReady;
111
115
  }
112
116
 
113
117
  function isSettledAtHead(crossChainState) {
@@ -129,17 +133,13 @@ function markCaughtUpIfSettled(crossChainState) {
129
133
 
130
134
  function markCaughtUpOnResume(crossChainState) {
131
135
  let everyChainCaughtUp = crossChainState.chainIds.length !== 0;
132
- let everyChainReady = true;
133
136
  for (let i = 0, i_finish = crossChainState.chainIds.length; i < i_finish; ++i) {
134
137
  let cs = crossChainState.chainStates[crossChainState.chainIds[i]];
135
138
  if (!ChainState.isDurablyCaughtUp(cs)) {
136
139
  everyChainCaughtUp = false;
137
140
  }
138
- if (!ChainState.isReady(cs)) {
139
- everyChainReady = false;
140
- }
141
141
  }
142
- if (everyChainCaughtUp && !everyChainReady && nextItemIsNone(crossChainState)) {
142
+ if (everyChainCaughtUp && !crossChainState.isRealtime && nextItemIsNone(crossChainState)) {
143
143
  crossChainState.isCaughtUp = true;
144
144
  return;
145
145
  }
@@ -199,7 +199,7 @@ async function checkAndFetch(crossChainState, dispatchChain) {
199
199
  let chainTargetItems = (
200
200
  isCold ? Primitive_float.min(remaining.contents, minimumAdmissionBudget) : remaining.contents
201
201
  ) + ChainState.pendingBudget(cs);
202
- let maxTargetBlock = alignment !== undefined && alignment[0] !== chainId ? ChainState.blockAtProgress(cs, alignment[1] + 0.1) : undefined;
202
+ let maxTargetBlock = alignment !== undefined && alignment[0] !== chainId ? ChainState.blockAtProgress(cs, alignment[1] + 0.2) : undefined;
203
203
  let action = ChainState.getNextQuery(cs, chainTargetItems, maxTargetBlock);
204
204
  if (typeof action !== "object") {
205
205
  if (action === "WaitingForNewBlock") {
@@ -249,9 +249,11 @@ export {
249
249
  make,
250
250
  chainStates,
251
251
  isRealtime,
252
+ chainIds,
252
253
  isCaughtUp,
253
254
  isInReorgThreshold,
254
255
  targetBufferSize,
256
+ shouldSaveHistory,
255
257
  nextItemIsNone,
256
258
  getSafeCheckpointIdByChain,
257
259
  createBatch,
@@ -5,34 +5,27 @@ type t
5
5
 
6
6
  let calculateTargetBufferSize: unit => int
7
7
 
8
- let make: (
9
- ~chainStates: dict<ChainState.t>,
10
- ~isInReorgThreshold: bool,
11
- ~isRealtime: bool,
12
- ~targetBufferSize: int=?,
13
- ) => t
8
+ let make: (~chainStates: dict<ChainState.t>, ~isRealtime: bool, ~targetBufferSize: int=?) => t
14
9
 
15
10
  // Accessors.
16
11
  let chainStates: t => dict<ChainState.t>
17
12
  let isRealtime: t => bool
13
+ let chainIds: t => array<ChainId.t>
18
14
  let isCaughtUp: t => bool
19
15
  let isInReorgThreshold: t => bool
20
16
  let targetBufferSize: t => int
17
+ let shouldSaveHistory: t => dict<bool>
21
18
 
22
19
  // Derived (pure).
23
20
  let nextItemIsNone: t => bool
24
21
  let getSafeCheckpointIdByChain: (
25
22
  t,
26
- ~committedCheckpointId: Internal.checkpointId,
23
+ ~sequence: CheckpointSequence.t,
24
+ ~committedFrontier: Frontier.t,
27
25
  ) => array<(ChainId.t, option<Internal.checkpointId>)>
28
26
 
29
27
  // Cross-chain transitions.
30
- let createBatch: (
31
- t,
32
- ~processedCheckpointId: Internal.checkpointId,
33
- ~batchSizeTarget: int,
34
- ~isRollback: bool,
35
- ) => Batch.t
28
+ let createBatch: (t, ~config: Config.t, ~frontier: Frontier.t, ~batchSizeTarget: int) => Batch.t
36
29
  let enterReorgThreshold: t => unit
37
30
  let applyBatchProgress: (t, ~batch: Batch.t, ~blockTimestampName: string) => unit
38
31
  let markCaughtUpIfSettled: t => unit
@@ -36,15 +36,24 @@ type effectStats = {
36
36
  mutable hasCache: bool,
37
37
  }
38
38
 
39
+ // A handler output, stamped with the checkpoint that produced it on the chain
40
+ // whose handler ran it. A cross-chain effect's entries come from several
41
+ // chains, and under per-chain sequences an id only compares against its own
42
+ // chain's committed one.
43
+ type cacheEntry = {
44
+ output: Internal.effectOutput,
45
+ chainId: ChainId.t,
46
+ checkpointId: Internal.checkpointId,
47
+ }
48
+
39
49
  type effectCacheInMemTable = {
40
50
  // Cache keys whose handler output is persisted on the next write. Drained
41
51
  // each write; eviction is driven by the per-entry checkpointId instead.
42
52
  mutable idsToStore: array<string>,
43
53
  mutable invalidationsCount: int,
44
- // Each entry is stamped with the checkpoint that referenced it (or
45
- // loadedFromDbCheckpointId for db reads), so committed entries can be
46
- // dropped once persisted/re-derivable, mirroring entity changes.
47
- mutable dict: dict<Change.t<Internal.effectOutput>>,
54
+ // Committed entries are dropped once persisted or re-derivable, mirroring
55
+ // entity changes; a db read is stamped loadedFromDbCheckpointId.
56
+ mutable dict: dict<cacheEntry>,
48
57
  mutable changesCount: float,
49
58
  effect: Internal.effect,
50
59
  // The scope this cache is for and its resolved db table (the address). The
@@ -11,10 +11,16 @@ type effectRateLimitState = {
11
11
  // toMetrics.
12
12
  type effectStats
13
13
 
14
+ type cacheEntry = {
15
+ output: Internal.effectOutput,
16
+ chainId: ChainId.t,
17
+ checkpointId: Internal.checkpointId,
18
+ }
19
+
14
20
  type effectCacheInMemTable = {
15
21
  mutable idsToStore: array<string>,
16
22
  mutable invalidationsCount: int,
17
- mutable dict: dict<Change.t<Internal.effectOutput>>,
23
+ mutable dict: dict<cacheEntry>,
18
24
  mutable changesCount: float,
19
25
  effect: Internal.effect,
20
26
  scope: Internal.chainScope,
@@ -157,9 +157,9 @@ let preloadBatchOrThrow = async (
157
157
 
158
158
  for checkpointIdx in 0 to batch.checkpointIds->Array.length - 1 {
159
159
  let checkpointId = batch.checkpointIds->Array.getUnsafe(checkpointIdx)
160
- let checkpointEventsProcessed = batch.checkpointEventsProcessed->Array.getUnsafe(checkpointIdx)
160
+ let checkpointItemsCount = batch.checkpointItemsCount->Array.getUnsafe(checkpointIdx)
161
161
 
162
- for idx in 0 to checkpointEventsProcessed - 1 {
162
+ for idx in 0 to checkpointItemsCount - 1 {
163
163
  let item = batch.items->Array.getUnsafe(itemIdx.contents + idx)
164
164
  switch item {
165
165
  | Event(_) =>
@@ -233,7 +233,7 @@ let preloadBatchOrThrow = async (
233
233
  }
234
234
  }
235
235
 
236
- itemIdx := itemIdx.contents + checkpointEventsProcessed
236
+ itemIdx := itemIdx.contents + checkpointItemsCount
237
237
  }
238
238
 
239
239
  let _ = await Promise.all(promises)
@@ -251,9 +251,9 @@ let runBatchHandlersOrThrow = async (
251
251
 
252
252
  for checkpointIdx in 0 to batch.checkpointIds->Array.length - 1 {
253
253
  let checkpointId = batch.checkpointIds->Array.getUnsafe(checkpointIdx)
254
- let checkpointEventsProcessed = batch.checkpointEventsProcessed->Array.getUnsafe(checkpointIdx)
254
+ let checkpointItemsCount = batch.checkpointItemsCount->Array.getUnsafe(checkpointIdx)
255
255
 
256
- for idx in 0 to checkpointEventsProcessed - 1 {
256
+ for idx in 0 to checkpointItemsCount - 1 {
257
257
  let item = batch.items->Array.getUnsafe(itemIdx.contents + idx)
258
258
 
259
259
  await runHandlerOrThrow(
@@ -266,7 +266,7 @@ let runBatchHandlersOrThrow = async (
266
266
  ~chains,
267
267
  )
268
268
  }
269
- itemIdx := itemIdx.contents + checkpointEventsProcessed
269
+ itemIdx := itemIdx.contents + checkpointItemsCount
270
270
  }
271
271
  }
272
272
 
@@ -110,8 +110,8 @@ async function preloadBatchOrThrow(batch, loadManager, persistence, config, inde
110
110
  let itemIdx = 0;
111
111
  for (let checkpointIdx = 0, checkpointIdx_finish = batch.checkpointIds.length; checkpointIdx < checkpointIdx_finish; ++checkpointIdx) {
112
112
  let checkpointId = batch.checkpointIds[checkpointIdx];
113
- let checkpointEventsProcessed = batch.checkpointEventsProcessed[checkpointIdx];
114
- for (let idx = 0; idx < checkpointEventsProcessed; ++idx) {
113
+ let checkpointItemsCount = batch.checkpointItemsCount[checkpointIdx];
114
+ for (let idx = 0; idx < checkpointItemsCount; ++idx) {
115
115
  let item = batch.items[itemIdx + idx | 0];
116
116
  if (item.kind === 0) {
117
117
  let match = item.onEventRegistration;
@@ -158,7 +158,7 @@ async function preloadBatchOrThrow(batch, loadManager, persistence, config, inde
158
158
  }
159
159
  }
160
160
  }
161
- itemIdx = itemIdx + checkpointEventsProcessed | 0;
161
+ itemIdx = itemIdx + checkpointItemsCount | 0;
162
162
  }
163
163
  await Promise.all(promises);
164
164
  }
@@ -167,12 +167,12 @@ async function runBatchHandlersOrThrow(batch, indexerState, loadManager, persist
167
167
  let itemIdx = 0;
168
168
  for (let checkpointIdx = 0, checkpointIdx_finish = batch.checkpointIds.length; checkpointIdx < checkpointIdx_finish; ++checkpointIdx) {
169
169
  let checkpointId = batch.checkpointIds[checkpointIdx];
170
- let checkpointEventsProcessed = batch.checkpointEventsProcessed[checkpointIdx];
171
- for (let idx = 0; idx < checkpointEventsProcessed; ++idx) {
170
+ let checkpointItemsCount = batch.checkpointItemsCount[checkpointIdx];
171
+ for (let idx = 0; idx < checkpointItemsCount; ++idx) {
172
172
  let item = batch.items[itemIdx + idx | 0];
173
173
  await runHandlerOrThrow(item, checkpointId, indexerState, loadManager, persistence, config, chains);
174
174
  }
175
- itemIdx = itemIdx + checkpointEventsProcessed | 0;
175
+ itemIdx = itemIdx + checkpointItemsCount | 0;
176
176
  }
177
177
  }
178
178
 
@@ -50,6 +50,22 @@ let makeSelection = (~onEventRegistrations, ~dependsOnAddresses, ~clientFiltered
50
50
  startBlock: ?deriveSelectionStartBlock(onEventRegistrations),
51
51
  }
52
52
 
53
+ // A partition's frontier never sits below the block before its selection can
54
+ // first match: the blocks below hold nothing for it, so they count as fetched
55
+ // the same way the blocks before an address's start block do. The chain's
56
+ // buffer frontier is its lowest partition frontier, so a partition left at the
57
+ // chain start would hold every other partition's events back as unprocessable
58
+ // and keep the chain's query target — sized from that frontier — short of
59
+ // anything worth asking for. A start block the chain has not reached yet leaves
60
+ // the partition waiting there, like an address partition does, rather than
61
+ // scanning empty ranges up to it; bufferBlockNumber caps the chain's frontier at
62
+ // the head meanwhile.
63
+ let floorAtSelectionStart = (latestFetchedBlock, ~selection) =>
64
+ switch selection.startBlock {
65
+ | Some(startBlock) => Pervasives.max(latestFetchedBlock, startBlock - 1)
66
+ | None => latestFetchedBlock
67
+ }
68
+
53
69
  type pendingQuery = {
54
70
  fromBlock: int,
55
71
  toBlock: option<int>,
@@ -398,6 +414,10 @@ module OptimizedPartitions = {
398
414
  ~dynamicContracts: Utils.Set.t<string>,
399
415
  ~clientFilteredContracts: Utils.Set.t<string>,
400
416
  ) => {
417
+ let partitions = partitions->Array.map(p => {
418
+ let floored = p.latestFetchedBlock->floorAtSelectionStart(~selection=p.selection)
419
+ floored === p.latestFetchedBlock ? p : {...p, latestFetchedBlock: floored}
420
+ })
401
421
  let newPartitions = []
402
422
  let mergingPartitions = Dict.make()
403
423
  let nextPartitionIndexRef = ref(nextPartitionIndex)
@@ -847,28 +867,17 @@ type t = {
847
867
  clientFilterAddressThreshold: option<int>,
848
868
  }
849
869
 
870
+ // The latest block whose items are all in the buffer: the lowest partition
871
+ // frontier, held back by the onBlock pointer. Without onBlock registrations the
872
+ // pointer tracks the known height, so this also caps the frontier at the head: a
873
+ // partition can sit past it, on a response that reported a block the chain
874
+ // hadn't heard of yet or on a start block the chain hasn't reached.
850
875
  @inline
851
876
  let bufferBlockNumber = ({latestOnBlockBlockNumber, optimizedPartitions}: t) => {
852
877
  switch optimizedPartitions->OptimizedPartitions.getLatestFullyFetchedBlock {
853
878
  | None => latestOnBlockBlockNumber
854
879
  | Some(latestFullyFetchedBlock) =>
855
- latestOnBlockBlockNumber < latestFullyFetchedBlock
856
- ? latestOnBlockBlockNumber
857
- : latestFullyFetchedBlock
858
- }
859
- }
860
-
861
- /**
862
- * Returns the latest block which is ready to be consumed
863
- */
864
- @inline
865
- let bufferBlock = ({optimizedPartitions, latestOnBlockBlockNumber}: t) => {
866
- switch optimizedPartitions->OptimizedPartitions.getLatestFullyFetchedBlock {
867
- | None => latestOnBlockBlockNumber
868
- | Some(latestFullyFetchedBlock) =>
869
- latestOnBlockBlockNumber < latestFullyFetchedBlock
870
- ? latestOnBlockBlockNumber
871
- : latestFullyFetchedBlock
880
+ Pervasives.min(latestOnBlockBlockNumber, latestFullyFetchedBlock)
872
881
  }
873
882
  }
874
883
 
@@ -979,6 +988,20 @@ let compareBufferItem = (a: Internal.item, b: Internal.item): int => {
979
988
  }
980
989
  }
981
990
 
991
+ // Whether two adjacent buffer items came from one log routed to two
992
+ // registrations: everything `compareBufferItem` orders on except the
993
+ // registration index is equal. Only meaningful on neighbours of a sorted
994
+ // buffer, where such items sit next to each other.
995
+ let isSameLog = (a: Internal.item, b: Internal.item): bool =>
996
+ a->Internal.getItemKind === 0 &&
997
+ b->Internal.getItemKind === 0 &&
998
+ a->Internal.getItemBlockNumber === b->Internal.getItemBlockNumber &&
999
+ a->Internal.getItemLogIndex === b->Internal.getItemLogIndex &&
1000
+ switch (a->Internal.getItemOrderPath, b->Internal.getItemOrderPath) {
1001
+ | (Value(pa), Value(pb)) => comparePath(pa, pb) === 0
1002
+ | _ => true
1003
+ }
1004
+
982
1005
  // Merge a maybe-unsorted `newItems` run into the already-sorted, already-deduped
983
1006
  // `buffer`, dropping items equal on every component of `compareBufferItem`.
984
1007
  // Single linear pass over both runs after ordering `newItems` in place; every
@@ -1124,14 +1147,20 @@ let updateInternal = (
1124
1147
  // Use maxOnBlockBufferSize to get the last target item in the buffer
1125
1148
  // (sorted, so this is the highest-block item within the buffer cap).
1126
1149
  // All this needed to prevent OOM when adding too many block items to the queue
1127
- let maxBlockNumber = switch base->Array.get(fetchState.maxOnBlockBufferSize - 1) {
1128
- | Some(item) => item->Internal.getItemBlockNumber
1129
- | None =>
1130
- switch optimizedPartitions->OptimizedPartitions.getLatestFullyFetchedBlock {
1131
- | None => knownHeight
1132
- | Some(latestFullyFetchedBlock) => latestFullyFetchedBlock
1133
- }
1134
- }
1150
+ // Never past the head: a partition can sit there, on a start block the
1151
+ // chain hasn't reached, and a response is applied before the height it
1152
+ // reported, so its events can run past the height still known here.
1153
+ let maxBlockNumber = Pervasives.min(
1154
+ switch base->Array.get(fetchState.maxOnBlockBufferSize - 1) {
1155
+ | Some(item) => item->Internal.getItemBlockNumber
1156
+ | None =>
1157
+ switch optimizedPartitions->OptimizedPartitions.getLatestFullyFetchedBlock {
1158
+ | None => knownHeight
1159
+ | Some(latestFullyFetchedBlock) => latestFullyFetchedBlock
1160
+ }
1161
+ },
1162
+ knownHeight,
1163
+ )
1135
1164
  appendOnBlockItems(
1136
1165
  ~mutItems=blockItems,
1137
1166
  ~onBlockRegistrations,
@@ -2151,27 +2180,11 @@ let walkPartitionPending = (
2151
2180
  pqIdx := pqIdx.contents + 1
2152
2181
  }
2153
2182
 
2154
- // Nothing in this partition's selection can match below its earliest start
2155
- // block, so forward work skips straight to it instead of scanning up to it and
2156
- // discarding whole pages. Only the cursor moves — `latestFetchedBlock` still
2157
- // advances solely on a response, so no block is ever reported fetched that
2158
- // wasn't. Bounded by the head and the query end block: past either, the
2159
- // partition would offer no candidate at all, so it queries as before rather
2160
- // than going quiet until the chain reaches its start block.
2161
- let cursor = switch p.selection.startBlock {
2162
- | Some(startBlock) if startBlock > cursor.contents =>
2163
- switch Utils.Math.minOptInt(Some(headBlockNumber), queryEndBlock) {
2164
- | Some(reachable) if startBlock <= reachable => startBlock
2165
- | _ => cursor.contents
2166
- }
2167
- | _ => cursor.contents
2168
- }
2169
-
2170
2183
  canContinue.contents
2171
2184
  ? Some({
2172
2185
  partitionId,
2173
2186
  p,
2174
- cursor,
2187
+ cursor: cursor.contents,
2175
2188
  chunksUsedThisCall: chunksUsedThisCall.contents,
2176
2189
  inFlightCount,
2177
2190
  queryEndBlock,
@@ -2766,10 +2779,17 @@ let make = (
2766
2779
  // fetching, so without seeding the buffer here getNextQuery would return
2767
2780
  // NothingToQuery and the indexer would get stuck.
2768
2781
  let buffer = []
2769
- let latestOnBlockBlockNumber = if knownHeight > 0 && onBlockRegistrations->Utils.Array.notEmpty {
2782
+ let latestOnBlockBlockNumber = switch onBlockRegistrations {
2783
+ // As updateInternal keeps it: with nothing to generate per block the pointer
2784
+ // is the head cap on the buffer frontier, and left at the progress block it
2785
+ // would hold the frontier there until the first height update — below a
2786
+ // partition that starts later, whose queries the chain sizes off that
2787
+ // frontier and so never reaches.
2788
+ | [] if knownHeight > 0 => knownHeight
2789
+ | onBlockRegistrations if knownHeight > 0 =>
2770
2790
  let maxBlockNumber = switch optimizedPartitions->OptimizedPartitions.getLatestFullyFetchedBlock {
2771
2791
  | None => knownHeight
2772
- | Some(latestFullyFetchedBlock) => latestFullyFetchedBlock
2792
+ | Some(latestFullyFetchedBlock) => Pervasives.min(latestFullyFetchedBlock, knownHeight)
2773
2793
  }
2774
2794
  appendOnBlockItems(
2775
2795
  ~mutItems=buffer,
@@ -2779,8 +2799,7 @@ let make = (
2779
2799
  ~maxBlockNumber,
2780
2800
  ~maxOnBlockBufferSize,
2781
2801
  )
2782
- } else {
2783
- progressBlockNumber
2802
+ | _ => progressBlockNumber
2784
2803
  }
2785
2804
 
2786
2805
  let fetchState = {