envio 3.3.0-alpha.11 → 3.3.0-alpha.12

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "envio",
3
- "version": "3.3.0-alpha.11",
3
+ "version": "3.3.0-alpha.12",
4
4
  "type": "module",
5
5
  "description": "A latency and sync speed optimized, developer friendly blockchain data indexer.",
6
6
  "bin": "./bin.mjs",
@@ -70,10 +70,10 @@
70
70
  "tsx": "4.21.0"
71
71
  },
72
72
  "optionalDependencies": {
73
- "envio-linux-x64": "3.3.0-alpha.11",
74
- "envio-linux-x64-musl": "3.3.0-alpha.11",
75
- "envio-linux-arm64": "3.3.0-alpha.11",
76
- "envio-darwin-x64": "3.3.0-alpha.11",
77
- "envio-darwin-arm64": "3.3.0-alpha.11"
73
+ "envio-linux-x64": "3.3.0-alpha.12",
74
+ "envio-linux-x64-musl": "3.3.0-alpha.12",
75
+ "envio-linux-arm64": "3.3.0-alpha.12",
76
+ "envio-darwin-x64": "3.3.0-alpha.12",
77
+ "envio-darwin-arm64": "3.3.0-alpha.12"
78
78
  }
79
79
  }
@@ -149,19 +149,28 @@ let rec onQueryResponse = async (
149
149
 
150
150
  let numContractRegisterEvents = parsedQueueItems->Array.reduce(0, (count, item) => {
151
151
  let eventItem = item->Internal.castUnsafeEventItem
152
- eventItem.onEventRegistration.contractRegister !== None
153
- ? count + 1
154
- : count
155
- })
156
- Logging.trace({
157
- "msg": "Finished querying",
158
- "chainId": chain->ChainMap.Chain.toChainId,
159
- "partitionId": query.partitionId,
160
- "fromBlock": fromBlockQueried,
161
- "toBlock": latestFetchedBlockNumber,
162
- "numEvents": parsedQueueItems->Array.length,
163
- "numContractRegisterEvents": numContractRegisterEvents,
152
+ eventItem.onEventRegistration.contractRegister !== None ? count + 1 : count
164
153
  })
154
+ if numContractRegisterEvents === 0 {
155
+ Logging.trace({
156
+ "msg": "Finished querying",
157
+ "chainId": chain->ChainMap.Chain.toChainId,
158
+ "partitionId": query.partitionId,
159
+ "fromBlock": fromBlockQueried,
160
+ "toBlock": latestFetchedBlockNumber,
161
+ "numEvents": parsedQueueItems->Array.length,
162
+ })
163
+ } else {
164
+ Logging.trace({
165
+ "msg": "Finished querying",
166
+ "chainId": chain->ChainMap.Chain.toChainId,
167
+ "partitionId": query.partitionId,
168
+ "fromBlock": fromBlockQueried,
169
+ "toBlock": latestFetchedBlockNumber,
170
+ "numEvents": parsedQueueItems->Array.length,
171
+ "numContractRegisterEvents": numContractRegisterEvents,
172
+ })
173
+ }
165
174
 
166
175
  let reorgResult = chainState->ChainState.registerReorgGuard(~blockHashes, ~knownHeight)
167
176
 
@@ -111,15 +111,26 @@ async function onQueryResponse(state, param, stateId, scheduleFetch, schedulePro
111
111
  return count;
112
112
  }
113
113
  });
114
- Logging.trace({
115
- msg: "Finished querying",
116
- chainId: chain,
117
- partitionId: query.partitionId,
118
- fromBlock: fromBlockQueried,
119
- toBlock: latestFetchedBlockNumber,
120
- numEvents: parsedQueueItems.length,
121
- numContractRegisterEvents: numContractRegisterEvents
122
- });
114
+ if (numContractRegisterEvents === 0) {
115
+ Logging.trace({
116
+ msg: "Finished querying",
117
+ chainId: chain,
118
+ partitionId: query.partitionId,
119
+ fromBlock: fromBlockQueried,
120
+ toBlock: latestFetchedBlockNumber,
121
+ numEvents: parsedQueueItems.length
122
+ });
123
+ } else {
124
+ Logging.trace({
125
+ msg: "Finished querying",
126
+ chainId: chain,
127
+ partitionId: query.partitionId,
128
+ fromBlock: fromBlockQueried,
129
+ toBlock: latestFetchedBlockNumber,
130
+ numEvents: parsedQueueItems.length,
131
+ numContractRegisterEvents: numContractRegisterEvents
132
+ });
133
+ }
123
134
  let reorgResult = ChainState.registerReorgGuard(chainState, response.blockHashes, knownHeight);
124
135
  let rollbackWithReorgDetectedBlockNumber;
125
136
  if (typeof reorgResult !== "object") {
@@ -170,7 +170,7 @@ let applyBatchProgress = (crossChainState: t, ~batch: Batch.t, ~blockTimestampNa
170
170
  // --- Fetch control. ---
171
171
 
172
172
  // Chains ordered furthest-behind first, so the shared buffer pool goes to the
173
- // chains with the most backfill work before the rest.
173
+ // chains with the most fetchable backfill work before the rest.
174
174
  let priorityOrder = (crossChainState: t) =>
175
175
  crossChainState.chainStates
176
176
  ->Dict.valuesToArray
@@ -198,29 +198,37 @@ let totalReservedSize = (crossChainState: t) => {
198
198
  // chain-density-derived target block, then subtract what it actually used
199
199
  // before moving to the next chain. So a chain that can only use a little
200
200
  // (density too low, or already caught up) leaves the rest for the others
201
- // automatically. A chain visited after the budget ran out simply doesn't
202
- // query this round the leader's reservations release as its responses land,
203
- // so the next tick redistributes. Chains that do get budget are additionally
204
- // capped at the most-behind chain's target progress mapped onto their own
205
- // range, so no chain runs further ahead than the chain the pool is
206
- // prioritizing.
201
+ // automatically. Starting a new query requires at least 10% of the target pool
202
+ // to be free. A chain visited after the budget falls below that admission unit
203
+ // doesn't query this round reservations release as responses land, so the
204
+ // next tick redistributes. Chains that do get budget are additionally capped
205
+ // at the first querying chain's target progress mapped onto their own range, so
206
+ // no chain runs further ahead than the chain currently using the pool. Waiting
207
+ // and idle chains do not establish this alignment line.
207
208
  let checkAndFetch = async (
208
209
  crossChainState: t,
209
210
  ~dispatchChain: (~chain: ChainMap.Chain.t, ~action: FetchState.nextQuery) => promise<unit>,
210
211
  ) => {
212
+ let targetBudget = crossChainState.targetBufferSize->Int.toFloat
211
213
  let remaining = ref(
212
214
  Pervasives.max(
213
215
  0.,
214
- crossChainState.targetBufferSize->Int.toFloat -.
216
+ targetBudget -.
215
217
  crossChainState->totalReadyCount->Int.toFloat -.
216
218
  crossChainState->totalReservedSize,
217
219
  ),
218
220
  )
219
221
 
222
+ // New fetch work is admitted in units of 10% of the target pool. Waiting
223
+ // below this floor avoids spending the last few free items on undersized
224
+ // queries; response and batch completion schedule another tick after they
225
+ // release enough budget.
226
+ let minimumAdmissionBudget = targetBudget *. 0.1
227
+
220
228
  // A chain with no density signal probes blind, so it only gets a bounded
221
229
  // slice of the pool — one unknown chain shouldn't hold the whole budget
222
- // while it takes its first measurements.
223
- let coldChainBudget = Pervasives.min(5_000., crossChainState.targetBufferSize->Int.toFloat)
230
+ // while it takes its first measurements. Its probe is one admission unit.
231
+ let coldChainBudget = minimumAdmissionBudget
224
232
 
225
233
  // Chunk reservations get headroom over the density estimate so a
226
234
  // denser-than-expected range doesn't truncate at the server cap; realtime
@@ -233,30 +241,33 @@ let checkAndFetch = async (
233
241
  ->priorityOrder
234
242
  ->Array.forEach(cs => {
235
243
  let chainId = (cs->ChainState.chainConfig).id
236
- if cs->ChainState.knownHeight == 0 {
237
- // No height yet nothing to size a budget or an alignment line
238
- // against. Skip without consuming budget or claiming leadership, so a
239
- // chain whose source hasn't reported doesn't unconstrain everyone else.
240
- actionByChain->Utils.Dict.setByInt(chainId, FetchState.WaitingForNewBlock)
244
+ if remaining.contents < minimumAdmissionBudget {
245
+ // More than 90% of the target pool is ready or reserved. Do not attempt
246
+ // any chain action until a full admission unit becomes free.
247
+ actionByChain->Utils.Dict.setByInt(chainId, FetchState.NothingToQuery)
248
+ } else if cs->ChainState.knownHeight == 0 {
249
+ // Let getNextQuery own the waiting decision without trying to size work
250
+ // against an unknown height.
251
+ actionByChain->Utils.Dict.setByInt(
252
+ chainId,
253
+ cs->ChainState.getNextQuery(~chainTargetItems=0., ~chunkItemsMultiplier),
254
+ )
241
255
  } else {
242
256
  let isCold = cs->ChainState.effectiveDensity === None
243
257
  let chainTargetItems =
244
258
  (isCold ? Pervasives.min(remaining.contents, coldChainBudget) : remaining.contents) +.
245
259
  cs->ChainState.pendingBudget
260
+ let candidateProgress = switch maxProgress.contents {
261
+ | None if !isCold =>
262
+ Some(
263
+ cs->ChainState.progressAtBlock(
264
+ ~blockNumber=cs->ChainState.targetBlock(~chainTargetItems),
265
+ ),
266
+ )
267
+ | _ => None
268
+ }
246
269
  let maxTargetBlock = switch maxProgress.contents {
247
- | None if isCold =>
248
- // A cold chain's target is a guess — it doesn't set the alignment
249
- // line; the first density-bearing chain after it does.
250
- None
251
- | None =>
252
- // The most-behind chain sets the alignment line for everyone after it.
253
- maxProgress :=
254
- Some(
255
- cs->ChainState.progressAtBlock(
256
- ~blockNumber=cs->ChainState.targetBlock(~chainTargetItems),
257
- ),
258
- )
259
- None
270
+ | None => None
260
271
  // 5% margin past the leader's line: chains whose progress tracks the
261
272
  // leader closely would otherwise flap in and out of the clamp as
262
273
  // leadership alternates, stalling their pipeline every other tick.
@@ -274,8 +285,7 @@ let checkAndFetch = async (
274
285
  // range to nothing — and has nothing else to wake it must keep polling
275
286
  // for new blocks instead of going silent. NothingToQuery isn't
276
287
  // dispatched, so such a chain would never be re-scheduled and its head
277
- // tracking would freeze (the knownHeight == 0 branch above forces a poll
278
- // for the same reason). A chain is genuinely idle — and correctly left
288
+ // tracking would freeze. A chain is genuinely idle and correctly left
279
289
  // undispatched — when it is caught up to its head/endblock, still
280
290
  // draining in-flight queries, or holding ready items that batch
281
291
  // processing will drain and re-schedule from.
@@ -287,6 +297,14 @@ let checkAndFetch = async (
287
297
  : FetchState.WaitingForNewBlock
288
298
  actionByChain->Utils.Dict.setByInt(chainId, action)
289
299
  | Ready(queries) => {
300
+ // A density-bearing chain establishes the alignment line only after
301
+ // proving that it can use the pool. Waiting and idle chains leave it
302
+ // open for the next chain; cold-chain targets remain guesses and do
303
+ // not lead, as before.
304
+ switch candidateProgress {
305
+ | Some(progress) => maxProgress := Some(progress)
306
+ | None => ()
307
+ }
290
308
  let consumed =
291
309
  queries->Array.reduce(0., (acc, query: FetchState.query) =>
292
310
  acc +. query.itemsEst->Int.toFloat
@@ -134,10 +134,11 @@ function totalReservedSize(crossChainState) {
134
134
  }
135
135
 
136
136
  async function checkAndFetch(crossChainState, dispatchChain) {
137
+ let targetBudget = crossChainState.targetBufferSize;
137
138
  let remaining = {
138
- contents: Primitive_float.max(0, crossChainState.targetBufferSize - totalReadyCount(crossChainState) - totalReservedSize(crossChainState))
139
+ contents: Primitive_float.max(0, targetBudget - totalReadyCount(crossChainState) - totalReservedSize(crossChainState))
139
140
  };
140
- let coldChainBudget = Primitive_float.min(5000, crossChainState.targetBufferSize);
141
+ let minimumAdmissionBudget = targetBudget * 0.1;
141
142
  let chunkItemsMultiplier = crossChainState.isRealtime ? 3 : 1.5;
142
143
  let actionByChain = {};
143
144
  let maxProgress = {
@@ -145,18 +146,22 @@ async function checkAndFetch(crossChainState, dispatchChain) {
145
146
  };
146
147
  priorityOrder(crossChainState).forEach(cs => {
147
148
  let chainId = ChainState.chainConfig(cs).id;
149
+ if (remaining.contents < minimumAdmissionBudget) {
150
+ actionByChain[chainId] = "NothingToQuery";
151
+ return;
152
+ }
148
153
  if (ChainState.knownHeight(cs) === 0) {
149
- actionByChain[chainId] = "WaitingForNewBlock";
154
+ actionByChain[chainId] = ChainState.getNextQuery(cs, 0, chunkItemsMultiplier, undefined);
150
155
  return;
151
156
  }
152
157
  let isCold = ChainState.effectiveDensity(cs) === undefined;
153
158
  let chainTargetItems = (
154
- isCold ? Primitive_float.min(remaining.contents, coldChainBudget) : remaining.contents
159
+ isCold ? Primitive_float.min(remaining.contents, minimumAdmissionBudget) : remaining.contents
155
160
  ) + ChainState.pendingBudget(cs);
161
+ let match = maxProgress.contents;
162
+ let candidateProgress = match !== undefined || isCold ? undefined : ChainState.progressAtBlock(cs, ChainState.targetBlock(cs, chainTargetItems));
156
163
  let progress = maxProgress.contents;
157
- let maxTargetBlock = progress !== undefined ? ChainState.blockAtProgress(cs, progress + 0.05) : (
158
- isCold ? undefined : (maxProgress.contents = ChainState.progressAtBlock(cs, ChainState.targetBlock(cs, chainTargetItems)), undefined)
159
- );
164
+ let maxTargetBlock = progress !== undefined ? ChainState.blockAtProgress(cs, progress + 0.05) : undefined;
160
165
  let action = ChainState.getNextQuery(cs, chainTargetItems, chunkItemsMultiplier, maxTargetBlock);
161
166
  if (typeof action !== "object") {
162
167
  if (action === "WaitingForNewBlock") {
@@ -168,6 +173,9 @@ async function checkAndFetch(crossChainState, dispatchChain) {
168
173
  return;
169
174
  } else {
170
175
  let queries = action._0;
176
+ if (candidateProgress !== undefined) {
177
+ maxProgress.contents = candidateProgress;
178
+ }
171
179
  let consumed = Stdlib_Array.reduce(queries, 0, (acc, query) => acc + query.itemsEst);
172
180
  let partitions = {};
173
181
  queries.forEach(query => {
@@ -47,17 +47,19 @@ type partition = {
47
47
  dynamicContract: option<string>,
48
48
  // Mutable array for SourceManager sync - queries exist only while being fetched
49
49
  mutPendingQueries: array<pendingQuery>,
50
- // Track last 3 successful query ranges for chunking heuristic (0 means no data)
51
- prevQueryRange: int,
52
- prevPrevQueryRange: int,
53
- // Item count of the response that set prevQueryRange. Paired with it to derive
54
- // the partition's event density (prevRangeSize / prevQueryRange) for sizing
55
- // queries against the buffer budget.
56
- prevRangeSize: int,
50
+ // The last two ranges that measured how many blocks the source could return.
51
+ // Both must be non-zero before the minimum is trusted for chunk sizing.
52
+ sourceRangeCapacity: int,
53
+ prevSourceRangeCapacity: int,
54
+ // Smoothed items/block observed in responses. This is independent from the
55
+ // source's range capacity: even a response truncated by our own itemsTarget
56
+ // cap is useful density evidence while saying nothing about source capacity.
57
+ // None distinguishes a new partition from a real zero-density observation.
58
+ eventDensity: option<float>,
57
59
  // Tracks the latestFetchedBlock.blockNumber of the most recent response
58
- // that updated prevQueryRange. Prevents degradation of the chunking heuristic
59
- // when parallel query responses arrive out of order.
60
- latestBlockRangeUpdateBlock: int,
60
+ // that updated sourceRangeCapacity. Prevents degradation of the chunking
61
+ // heuristic when parallel query responses arrive out of order.
62
+ latestSourceRangeCapacityUpdateBlock: int,
61
63
  }
62
64
 
63
65
  type query = {
@@ -111,22 +113,17 @@ let densityItemsTarget = (~density, ~fromBlock, ~toBlock, ~chainTargetBlock) =>
111
113
  )
112
114
  }
113
115
 
114
- // Calculate the chunk range from history using min-of-last-3-ranges heuristic
116
+ // Calculate the chunk range from the last two source-capacity observations.
115
117
  let getMinHistoryRange = (p: partition) => {
116
- switch (p.prevQueryRange, p.prevPrevQueryRange) {
118
+ switch (p.sourceRangeCapacity, p.prevSourceRangeCapacity) {
117
119
  | (0, _) | (_, 0) => None
118
120
  | (a, b) => Some(a < b ? a : b)
119
121
  }
120
122
  }
121
123
 
122
- // Density (items/block) from the last response, trusted only after two
123
- // responses a single sample is too noisy to size the next query by.
124
- let getTrustedDensity = (p: partition) => {
125
- switch (p.prevQueryRange, p.prevPrevQueryRange) {
126
- | (0, _) | (_, 0) => None
127
- | (prevQueryRange, _) => Some(p.prevRangeSize->Int.toFloat /. prevQueryRange->Int.toFloat)
128
- }
129
- }
124
+ // Density has its own initialization signal and is useful independently from
125
+ // source-capacity history, including when an itemsTarget cap truncates a response.
126
+ let getTrustedDensity = (p: partition) => p.eventDensity
130
127
 
131
128
  let getMinQueryRange = (partitions: array<partition>) => {
132
129
  let min = ref(0)
@@ -134,8 +131,8 @@ let getMinQueryRange = (partitions: array<partition>) => {
134
131
  let p = partitions->Array.getUnsafe(i)
135
132
 
136
133
  // Even if it's fetching, set dynamicContract field
137
- let a = p.prevQueryRange
138
- let b = p.prevPrevQueryRange
134
+ let a = p.sourceRangeCapacity
135
+ let b = p.prevSourceRangeCapacity
139
136
  if a > 0 && (min.contents == 0 || a < min.contents) {
140
137
  min := a
141
138
  }
@@ -208,11 +205,14 @@ module OptimizedPartitions = {
208
205
  nextPartitionIndexRef := nextPartitionIndexRef.contents + 1
209
206
  let minRange = getMinQueryRange([p1, p2])
210
207
  // The merged partition indexes both parents' addresses, so its expected
211
- // event rate is the sum of their densities. Parents without a trusted
212
- // density contribute 0; if none has one, prevRangeSize stays 0 and the
213
- // partition probes for a fresh signal instead of chunking.
214
- let inheritedDensity =
215
- p1->getTrustedDensity->Option.getOr(0.) +. p2->getTrustedDensity->Option.getOr(0.)
208
+ // event rate is the sum of their densities. If neither parent has a
209
+ // trusted density, the merged partition probes for a fresh signal
210
+ // instead of treating zero as an observed density.
211
+ let inheritedDensity = switch (p1->getTrustedDensity, p2->getTrustedDensity) {
212
+ | (Some(p1Density), Some(p2Density)) => Some(p1Density +. p2Density)
213
+ | (Some(density), None) | (None, Some(density)) => Some(density)
214
+ | (None, None) => None
215
+ }
216
216
  {
217
217
  id: newId,
218
218
  dynamicContract: Some(contractName),
@@ -221,10 +221,10 @@ module OptimizedPartitions = {
221
221
  mergeBlock: None,
222
222
  addressesByContractName: Dict.make(), // set below
223
223
  mutPendingQueries: [],
224
- prevQueryRange: minRange,
225
- prevPrevQueryRange: minRange,
226
- prevRangeSize: (inheritedDensity *. minRange->Int.toFloat)->Math.ceil->Float.toInt,
227
- latestBlockRangeUpdateBlock: 0,
224
+ sourceRangeCapacity: minRange,
225
+ prevSourceRangeCapacity: minRange,
226
+ eventDensity: inheritedDensity,
227
+ latestSourceRangeCapacityUpdateBlock: 0,
228
228
  }
229
229
  }
230
230
 
@@ -472,14 +472,28 @@ module OptimizedPartitions = {
472
472
  pendingQuery.fetchedBlock = Some(latestFetchedBlock)
473
473
 
474
474
  let blockRange = latestFetchedBlock.blockNumber - query.fromBlock + 1
475
- // Skip updating block range if a later response already updated it.
475
+ // Update density for every response, independently from whether this range
476
+ // is valid evidence of source capacity. A cap hit is still useful density
477
+ // evidence because it reports items returned across the scanned range.
478
+ let observedEventDensity = itemsCount->Int.toFloat /. blockRange->Int.toFloat
479
+ // Seed from the first observation, then smooth every later observation
480
+ // with a 1:1 moving average. Keeping initialization explicit is important:
481
+ // zero is valid density evidence and must participate in the next blend.
482
+ let updatedEventDensity = switch p.eventDensity {
483
+ | None => Some(observedEventDensity)
484
+ | Some(eventDensity) => Some((eventDensity +. observedEventDensity) /. 2.)
485
+ }
486
+
487
+ // Skip updating source capacity if a later response already updated it.
476
488
  // Prevents degradation of the chunking heuristic when parallel query
477
489
  // responses arrive out of order (e.g. earlier query with smaller range
478
490
  // arriving after a later query with bigger range).
479
- let shouldUpdateBlockRange =
480
- latestFetchedBlock.blockNumber > p.latestBlockRangeUpdateBlock &&
491
+ let shouldUpdateSourceRangeCapacity =
492
+ latestFetchedBlock.blockNumber > p.latestSourceRangeCapacityUpdateBlock &&
481
493
  switch query.toBlock {
482
- | None => latestFetchedBlock.blockNumber < knownHeight - 10 // Don't update block range when very close to the head
494
+ | None =>
495
+ // Don't update source capacity when very close to the head.
496
+ latestFetchedBlock.blockNumber < knownHeight - 10
483
497
  | Some(queryToBlock) =>
484
498
  if latestFetchedBlock.blockNumber < queryToBlock {
485
499
  // Partial response is direct capacity evidence — unless it was
@@ -497,9 +511,12 @@ module OptimizedPartitions = {
497
511
  }
498
512
  }
499
513
  }
500
- let updatedPrevQueryRange = shouldUpdateBlockRange ? blockRange : p.prevQueryRange
501
- let updatedPrevPrevQueryRange = shouldUpdateBlockRange ? p.prevQueryRange : p.prevPrevQueryRange
502
- let updatedPrevRangeSize = shouldUpdateBlockRange ? itemsCount : p.prevRangeSize
514
+ let updatedSourceRangeCapacity = shouldUpdateSourceRangeCapacity
515
+ ? blockRange
516
+ : p.sourceRangeCapacity
517
+ let updatedPrevSourceRangeCapacity = shouldUpdateSourceRangeCapacity
518
+ ? p.sourceRangeCapacity
519
+ : p.prevSourceRangeCapacity
503
520
 
504
521
  // Process fetched queries from front of queue for main partition
505
522
  let updatedLatestFetchedBlock = consumeFetchedQueries(
@@ -519,12 +536,12 @@ module OptimizedPartitions = {
519
536
  let updatedMainPartition = {
520
537
  ...p,
521
538
  latestFetchedBlock: updatedLatestFetchedBlock,
522
- prevQueryRange: updatedPrevQueryRange,
523
- prevPrevQueryRange: updatedPrevPrevQueryRange,
524
- prevRangeSize: updatedPrevRangeSize,
525
- latestBlockRangeUpdateBlock: shouldUpdateBlockRange
539
+ sourceRangeCapacity: updatedSourceRangeCapacity,
540
+ prevSourceRangeCapacity: updatedPrevSourceRangeCapacity,
541
+ eventDensity: updatedEventDensity,
542
+ latestSourceRangeCapacityUpdateBlock: shouldUpdateSourceRangeCapacity
526
543
  ? latestFetchedBlock.blockNumber
527
- : p.latestBlockRangeUpdateBlock,
544
+ : p.latestSourceRangeCapacityUpdateBlock,
528
545
  }
529
546
 
530
547
  mutEntities->Dict.set(p.id, updatedMainPartition)
@@ -986,10 +1003,10 @@ OptimizedPartitions.t => {
986
1003
  addressesByContractName,
987
1004
  mergeBlock: None,
988
1005
  mutPendingQueries: [],
989
- prevQueryRange: 0,
990
- prevPrevQueryRange: 0,
991
- prevRangeSize: 0,
992
- latestBlockRangeUpdateBlock: 0,
1006
+ sourceRangeCapacity: 0,
1007
+ prevSourceRangeCapacity: 0,
1008
+ eventDensity: None,
1009
+ latestSourceRangeCapacityUpdateBlock: 0,
993
1010
  })
994
1011
  nextPartitionIndexRef := nextPartitionIndexRef.contents + 1
995
1012
  }
@@ -1322,10 +1339,10 @@ let registerDynamicContracts = (
1322
1339
  addressesByContractName,
1323
1340
  mergeBlock: None,
1324
1341
  mutPendingQueries: p.mutPendingQueries,
1325
- prevQueryRange: p.prevQueryRange,
1326
- prevPrevQueryRange: p.prevPrevQueryRange,
1327
- prevRangeSize: p.prevRangeSize,
1328
- latestBlockRangeUpdateBlock: p.latestBlockRangeUpdateBlock,
1342
+ sourceRangeCapacity: p.sourceRangeCapacity,
1343
+ prevSourceRangeCapacity: p.prevSourceRangeCapacity,
1344
+ eventDensity: p.eventDensity,
1345
+ latestSourceRangeCapacityUpdateBlock: p.latestSourceRangeCapacityUpdateBlock,
1329
1346
  })
1330
1347
  }
1331
1348
  }
@@ -1582,24 +1599,25 @@ type partitionFillState = {
1582
1599
  // query with no other ceiling: the true hard bounds stay
1583
1600
  // endBlock/mergeBlock/the lagged head.
1584
1601
  //
1585
- // The tick's budget is chainTargetItems minus what's already in flight. Every
1586
- // candidate query gap-fill holes, plus each in-range partition's chunks or
1587
- // open-ended probe toward the target is generated with no budget check, then
1588
- // the candidates are sorted by fromBlock and accepted in that order while the
1589
- // budget stays positive. The query that tips it negative is still accepted (a
1590
- // single overshoot); everything after it waits for a tick with more budget.
1602
+ // The tick's budget is chainTargetItems minus what's already in flight. A
1603
+ // non-positive budget only resolves the wait action and generates no query
1604
+ // candidates. With a positive budget, every candidate query gap-fill holes,
1605
+ // plus each in-range partition's chunks or open-ended probe toward the target
1606
+ // is generated with no budget check, then the candidates are sorted by
1607
+ // fromBlock and accepted in that order while the budget stays positive. The
1608
+ // query that tips it negative is still accepted (a single overshoot);
1609
+ // everything after it waits for a tick with more budget.
1591
1610
  // Sorting by fromBlock spends the budget on the earliest blocks across all
1592
1611
  // partitions first, so no partition is starved by generation order and the
1593
1612
  // frontier advances evenly. In-flight reservations release as responses land,
1594
1613
  // so acceptance redistributes across ticks.
1595
1614
  //
1596
- // A partition with a trusted positive density (two or more responses — see
1597
- // getMinHistoryRange) generates real, density-sized chunks toward the target.
1598
- // Any other partition (no signal, one noisy sample, or a density-0 estimate)
1599
- // generates one open-ended probe sized to the events its range to the target is
1600
- // expected to hold — rangeTargetDensity × (chainTargetBlock − fromBlock + 1) /
1601
- // inRangeCount — so several unknown-density partitions probe in parallel within
1602
- // one budget, each scaled by how much of the range it still has to cover.
1615
+ // A partition with source-capacity history and a positive density generates
1616
+ // density-sized chunks toward the target. Any other partition (no signal, no
1617
+ // capacity history, or a density-0 estimate) generates one open-ended probe
1618
+ // sized to the events its range to the target is expected to hold —
1619
+ // rangeTargetDensity × (chainTargetBlock − fromBlock + 1) / inRangeCount — so
1620
+ // unknown-density partitions probe in parallel within one budget.
1603
1621
  let getNextQuery = (
1604
1622
  {optimizedPartitions, blockLag, latestOnBlockBlockNumber, knownHeight, endBlock}: t,
1605
1623
  ~chainTargetBlock: int,
@@ -1639,6 +1657,12 @@ let getNextQuery = (
1639
1657
  }
1640
1658
  }
1641
1659
 
1660
+ // A zero budget is an admission check: preserve the wait-state scan above,
1661
+ // but make every query-generation pass below empty. Caught-up chains also
1662
+ // skip those passes because their action is already known.
1663
+ let partitionsCount =
1664
+ chainTargetItems <= 0. || shouldWaitForNewBlock.contents ? 0 : partitionsCount
1665
+
1642
1666
  // One bucket per partition, in idsInAscOrder order — gap-fill and the
1643
1667
  // budget pass both push into a partition's own bucket, so flattening at
1644
1668
  // the end (see below) reproduces idsInAscOrder without a sort.
@@ -1788,10 +1812,9 @@ let getNextQuery = (
1788
1812
  inRangeCount > 0 && rangeToTarget > 0 ? freshBudget /. rangeToTarget->Int.toFloat : 0.
1789
1813
 
1790
1814
  // Phase B: generate each in-range partition's candidates — strict chunks
1791
- // toward the target sized by real density (up to the pending-chunk cap), or,
1792
- // for a partition without a trusted positive density, a single open-ended
1793
- // probe at its even share of the budget. No budget check here; the
1794
- // acceptance pass below decides which candidates make the cut.
1815
+ // when both source-capacity history and density are known, or an open-ended
1816
+ // budget probe otherwise. No budget check here; the acceptance pass below
1817
+ // decides which candidates make the cut.
1795
1818
  //
1796
1819
  // Chunks require a POSITIVE trusted density: density 0 prices every chunk at
1797
1820
  // ~nothing, so an open-ended probe (full server scan range in one response)
@@ -2022,10 +2045,10 @@ let make = (
2022
2045
  mergeBlock: None,
2023
2046
  dynamicContract: None,
2024
2047
  mutPendingQueries: [],
2025
- prevQueryRange: 0,
2026
- prevPrevQueryRange: 0,
2027
- prevRangeSize: 0,
2028
- latestBlockRangeUpdateBlock: 0,
2048
+ sourceRangeCapacity: 0,
2049
+ prevSourceRangeCapacity: 0,
2050
+ eventDensity: None,
2051
+ latestSourceRangeCapacityUpdateBlock: 0,
2029
2052
  })
2030
2053
  }
2031
2054
 
@@ -2345,13 +2368,15 @@ let isReadyToEnterReorgThreshold = ({endBlock, blockLag, buffer, knownHeight} as
2345
2368
  buffer->Utils.Array.isEmpty
2346
2369
  }
2347
2370
 
2348
- // Lower progress percentage = further behind = higher priority. Shared by the
2349
- // batch ordering and the cross-chain fetch priority.
2371
+ // Lower progress percentage = further behind = higher priority. Progress is
2372
+ // relative to the head this chain can actually fetch, so a chain at its lagged
2373
+ // head does not look behind relative to unavailable blocks. Shared by the batch
2374
+ // ordering and the cross-chain fetch priority.
2350
2375
  let getProgressPercentage = (fetchState: t) => {
2351
2376
  switch fetchState.firstEventBlock {
2352
2377
  | None => 0.
2353
2378
  | Some(firstEventBlock) =>
2354
- let totalRange = fetchState.knownHeight - firstEventBlock
2379
+ let totalRange = fetchState.knownHeight - fetchState.blockLag - firstEventBlock
2355
2380
  if totalRange <= 0 {
2356
2381
  0.
2357
2382
  } else {
@@ -28,27 +28,23 @@ function densityItemsTarget(density, fromBlock, toBlock, chainTargetBlock) {
28
28
  }
29
29
 
30
30
  function getMinHistoryRange(p) {
31
- let match = p.prevQueryRange;
32
- let match$1 = p.prevPrevQueryRange;
31
+ let match = p.sourceRangeCapacity;
32
+ let match$1 = p.prevSourceRangeCapacity;
33
33
  if (match !== 0 && match$1 !== 0) {
34
34
  return match < match$1 ? match : match$1;
35
35
  }
36
36
  }
37
37
 
38
38
  function getTrustedDensity(p) {
39
- let match = p.prevQueryRange;
40
- let match$1 = p.prevPrevQueryRange;
41
- if (match !== 0 && match$1 !== 0) {
42
- return p.prevRangeSize / match;
43
- }
39
+ return p.eventDensity;
44
40
  }
45
41
 
46
42
  function getMinQueryRange(partitions) {
47
43
  let min = 0;
48
44
  for (let i = 0, i_finish = partitions.length; i < i_finish; ++i) {
49
45
  let p = partitions[i];
50
- let a = p.prevQueryRange;
51
- let b = p.prevPrevQueryRange;
46
+ let a = p.sourceRangeCapacity;
47
+ let b = p.prevSourceRangeCapacity;
52
48
  if (a > 0 && (min === 0 || a < min)) {
53
49
  min = a;
54
50
  }
@@ -88,10 +84,10 @@ function mergePartitionsAtBlock(p1, p2, potentialMergeBlock, contractName, maxAd
88
84
  mergeBlock: potentialMergeBlock,
89
85
  dynamicContract: p1.dynamicContract,
90
86
  mutPendingQueries: p1.mutPendingQueries,
91
- prevQueryRange: p1.prevQueryRange,
92
- prevPrevQueryRange: p1.prevPrevQueryRange,
93
- prevRangeSize: p1.prevRangeSize,
94
- latestBlockRangeUpdateBlock: p1.latestBlockRangeUpdateBlock
87
+ sourceRangeCapacity: p1.sourceRangeCapacity,
88
+ prevSourceRangeCapacity: p1.prevSourceRangeCapacity,
89
+ eventDensity: p1.eventDensity,
90
+ latestSourceRangeCapacityUpdateBlock: p1.latestSourceRangeCapacityUpdateBlock
95
91
  });
96
92
  completed.push({
97
93
  id: p2.id,
@@ -101,10 +97,10 @@ function mergePartitionsAtBlock(p1, p2, potentialMergeBlock, contractName, maxAd
101
97
  mergeBlock: potentialMergeBlock,
102
98
  dynamicContract: p2.dynamicContract,
103
99
  mutPendingQueries: p2.mutPendingQueries,
104
- prevQueryRange: p2.prevQueryRange,
105
- prevPrevQueryRange: p2.prevPrevQueryRange,
106
- prevRangeSize: p2.prevRangeSize,
107
- latestBlockRangeUpdateBlock: p2.latestBlockRangeUpdateBlock
100
+ sourceRangeCapacity: p2.sourceRangeCapacity,
101
+ prevSourceRangeCapacity: p2.prevSourceRangeCapacity,
102
+ eventDensity: p2.eventDensity,
103
+ latestSourceRangeCapacityUpdateBlock: p2.latestSourceRangeCapacityUpdateBlock
108
104
  });
109
105
  let newId = nextPartitionIndexRef.contents.toString();
110
106
  nextPartitionIndexRef.contents = nextPartitionIndexRef.contents + 1 | 0;
@@ -112,7 +108,13 @@ function mergePartitionsAtBlock(p1, p2, potentialMergeBlock, contractName, maxAd
112
108
  p1,
113
109
  p2
114
110
  ]);
115
- let inheritedDensity = Stdlib_Option.getOr(getTrustedDensity(p1), 0) + Stdlib_Option.getOr(getTrustedDensity(p2), 0);
111
+ let match = p1.eventDensity;
112
+ let match$1 = p2.eventDensity;
113
+ let inheritedDensity = match !== undefined ? (
114
+ match$1 !== undefined ? match + match$1 : match
115
+ ) : (
116
+ match$1 !== undefined ? match$1 : undefined
117
+ );
116
118
  continuingBase = {
117
119
  id: newId,
118
120
  latestFetchedBlock: {
@@ -124,10 +126,10 @@ function mergePartitionsAtBlock(p1, p2, potentialMergeBlock, contractName, maxAd
124
126
  mergeBlock: undefined,
125
127
  dynamicContract: contractName,
126
128
  mutPendingQueries: [],
127
- prevQueryRange: minRange,
128
- prevPrevQueryRange: minRange,
129
- prevRangeSize: Math.ceil(inheritedDensity * minRange) | 0,
130
- latestBlockRangeUpdateBlock: 0
129
+ sourceRangeCapacity: minRange,
130
+ prevSourceRangeCapacity: minRange,
131
+ eventDensity: inheritedDensity,
132
+ latestSourceRangeCapacityUpdateBlock: 0
131
133
  };
132
134
  } else {
133
135
  completed.push({
@@ -138,10 +140,10 @@ function mergePartitionsAtBlock(p1, p2, potentialMergeBlock, contractName, maxAd
138
140
  mergeBlock: potentialMergeBlock,
139
141
  dynamicContract: p1.dynamicContract,
140
142
  mutPendingQueries: p1.mutPendingQueries,
141
- prevQueryRange: p1.prevQueryRange,
142
- prevPrevQueryRange: p1.prevPrevQueryRange,
143
- prevRangeSize: p1.prevRangeSize,
144
- latestBlockRangeUpdateBlock: p1.latestBlockRangeUpdateBlock
143
+ sourceRangeCapacity: p1.sourceRangeCapacity,
144
+ prevSourceRangeCapacity: p1.prevSourceRangeCapacity,
145
+ eventDensity: p1.eventDensity,
146
+ latestSourceRangeCapacityUpdateBlock: p1.latestSourceRangeCapacityUpdateBlock
145
147
  });
146
148
  continuingBase = p2;
147
149
  }
@@ -154,10 +156,10 @@ function mergePartitionsAtBlock(p1, p2, potentialMergeBlock, contractName, maxAd
154
156
  mergeBlock: potentialMergeBlock,
155
157
  dynamicContract: p2.dynamicContract,
156
158
  mutPendingQueries: p2.mutPendingQueries,
157
- prevQueryRange: p2.prevQueryRange,
158
- prevPrevQueryRange: p2.prevPrevQueryRange,
159
- prevRangeSize: p2.prevRangeSize,
160
- latestBlockRangeUpdateBlock: p2.latestBlockRangeUpdateBlock
159
+ sourceRangeCapacity: p2.sourceRangeCapacity,
160
+ prevSourceRangeCapacity: p2.prevSourceRangeCapacity,
161
+ eventDensity: p2.eventDensity,
162
+ latestSourceRangeCapacityUpdateBlock: p2.latestSourceRangeCapacityUpdateBlock
161
163
  });
162
164
  continuingBase = p1;
163
165
  } else {
@@ -178,10 +180,10 @@ function mergePartitionsAtBlock(p1, p2, potentialMergeBlock, contractName, maxAd
178
180
  mergeBlock: continuingBase.mergeBlock,
179
181
  dynamicContract: continuingBase.dynamicContract,
180
182
  mutPendingQueries: continuingBase.mutPendingQueries,
181
- prevQueryRange: continuingBase.prevQueryRange,
182
- prevPrevQueryRange: continuingBase.prevPrevQueryRange,
183
- prevRangeSize: continuingBase.prevRangeSize,
184
- latestBlockRangeUpdateBlock: continuingBase.latestBlockRangeUpdateBlock
183
+ sourceRangeCapacity: continuingBase.sourceRangeCapacity,
184
+ prevSourceRangeCapacity: continuingBase.prevSourceRangeCapacity,
185
+ eventDensity: continuingBase.eventDensity,
186
+ latestSourceRangeCapacityUpdateBlock: continuingBase.latestSourceRangeCapacityUpdateBlock
185
187
  });
186
188
  let restId = nextPartitionIndexRef.contents.toString();
187
189
  nextPartitionIndexRef.contents = nextPartitionIndexRef.contents + 1 | 0;
@@ -193,10 +195,10 @@ function mergePartitionsAtBlock(p1, p2, potentialMergeBlock, contractName, maxAd
193
195
  mergeBlock: continuingBase.mergeBlock,
194
196
  dynamicContract: continuingBase.dynamicContract,
195
197
  mutPendingQueries: [],
196
- prevQueryRange: continuingBase.prevQueryRange,
197
- prevPrevQueryRange: continuingBase.prevPrevQueryRange,
198
- prevRangeSize: continuingBase.prevRangeSize,
199
- latestBlockRangeUpdateBlock: continuingBase.latestBlockRangeUpdateBlock
198
+ sourceRangeCapacity: continuingBase.sourceRangeCapacity,
199
+ prevSourceRangeCapacity: continuingBase.prevSourceRangeCapacity,
200
+ eventDensity: continuingBase.eventDensity,
201
+ latestSourceRangeCapacityUpdateBlock: continuingBase.latestSourceRangeCapacityUpdateBlock
200
202
  });
201
203
  return completed;
202
204
  }
@@ -210,10 +212,10 @@ function mergePartitionsAtBlock(p1, p2, potentialMergeBlock, contractName, maxAd
210
212
  mergeBlock: continuingBase.mergeBlock,
211
213
  dynamicContract: continuingBase.dynamicContract,
212
214
  mutPendingQueries: continuingBase.mutPendingQueries,
213
- prevQueryRange: continuingBase.prevQueryRange,
214
- prevPrevQueryRange: continuingBase.prevPrevQueryRange,
215
- prevRangeSize: continuingBase.prevRangeSize,
216
- latestBlockRangeUpdateBlock: continuingBase.latestBlockRangeUpdateBlock
215
+ sourceRangeCapacity: continuingBase.sourceRangeCapacity,
216
+ prevSourceRangeCapacity: continuingBase.prevSourceRangeCapacity,
217
+ eventDensity: continuingBase.eventDensity,
218
+ latestSourceRangeCapacityUpdateBlock: continuingBase.latestSourceRangeCapacityUpdateBlock
217
219
  });
218
220
  return completed;
219
221
  }
@@ -358,8 +360,11 @@ function handleQueryResponse(optimizedPartitions, query, knownHeight, itemsCount
358
360
  let pendingQuery = getPendingQueryOrThrow(p$1, query.fromBlock);
359
361
  pendingQuery.fetchedBlock = latestFetchedBlock;
360
362
  let blockRange = (latestFetchedBlock.blockNumber - query.fromBlock | 0) + 1 | 0;
361
- let shouldUpdateBlockRange = false;
362
- if (latestFetchedBlock.blockNumber > p$1.latestBlockRangeUpdateBlock) {
363
+ let observedEventDensity = itemsCount / blockRange;
364
+ let eventDensity = p$1.eventDensity;
365
+ let updatedEventDensity = eventDensity !== undefined ? (eventDensity + observedEventDensity) / 2 : observedEventDensity;
366
+ let shouldUpdateSourceRangeCapacity = false;
367
+ if (latestFetchedBlock.blockNumber > p$1.latestSourceRangeCapacityUpdateBlock) {
363
368
  let queryToBlock = query.toBlock;
364
369
  let tmp;
365
370
  if (queryToBlock !== undefined) {
@@ -372,11 +377,10 @@ function handleQueryResponse(optimizedPartitions, query, knownHeight, itemsCount
372
377
  } else {
373
378
  tmp = latestFetchedBlock.blockNumber < (knownHeight - 10 | 0);
374
379
  }
375
- shouldUpdateBlockRange = tmp;
380
+ shouldUpdateSourceRangeCapacity = tmp;
376
381
  }
377
- let updatedPrevQueryRange = shouldUpdateBlockRange ? blockRange : p$1.prevQueryRange;
378
- let updatedPrevPrevQueryRange = shouldUpdateBlockRange ? p$1.prevQueryRange : p$1.prevPrevQueryRange;
379
- let updatedPrevRangeSize = shouldUpdateBlockRange ? itemsCount : p$1.prevRangeSize;
382
+ let updatedSourceRangeCapacity = shouldUpdateSourceRangeCapacity ? blockRange : p$1.sourceRangeCapacity;
383
+ let updatedPrevSourceRangeCapacity = shouldUpdateSourceRangeCapacity ? p$1.sourceRangeCapacity : p$1.prevSourceRangeCapacity;
380
384
  let mutPendingQueries = p$1.mutPendingQueries;
381
385
  let latestFetchedBlock$1 = p$1.latestFetchedBlock;
382
386
  while ((() => {
@@ -402,7 +406,7 @@ function handleQueryResponse(optimizedPartitions, query, knownHeight, itemsCount
402
406
  let updatedMainPartition_mergeBlock = p$1.mergeBlock;
403
407
  let updatedMainPartition_dynamicContract = p$1.dynamicContract;
404
408
  let updatedMainPartition_mutPendingQueries = p$1.mutPendingQueries;
405
- let updatedMainPartition_latestBlockRangeUpdateBlock = shouldUpdateBlockRange ? latestFetchedBlock.blockNumber : p$1.latestBlockRangeUpdateBlock;
409
+ let updatedMainPartition_latestSourceRangeCapacityUpdateBlock = shouldUpdateSourceRangeCapacity ? latestFetchedBlock.blockNumber : p$1.latestSourceRangeCapacityUpdateBlock;
406
410
  let updatedMainPartition = {
407
411
  id: updatedMainPartition_id,
408
412
  latestFetchedBlock: updatedLatestFetchedBlock,
@@ -411,10 +415,10 @@ function handleQueryResponse(optimizedPartitions, query, knownHeight, itemsCount
411
415
  mergeBlock: updatedMainPartition_mergeBlock,
412
416
  dynamicContract: updatedMainPartition_dynamicContract,
413
417
  mutPendingQueries: updatedMainPartition_mutPendingQueries,
414
- prevQueryRange: updatedPrevQueryRange,
415
- prevPrevQueryRange: updatedPrevPrevQueryRange,
416
- prevRangeSize: updatedPrevRangeSize,
417
- latestBlockRangeUpdateBlock: updatedMainPartition_latestBlockRangeUpdateBlock
418
+ sourceRangeCapacity: updatedSourceRangeCapacity,
419
+ prevSourceRangeCapacity: updatedPrevSourceRangeCapacity,
420
+ eventDensity: updatedEventDensity,
421
+ latestSourceRangeCapacityUpdateBlock: updatedMainPartition_latestSourceRangeCapacityUpdateBlock
418
422
  };
419
423
  mutEntities[p$1.id] = updatedMainPartition;
420
424
  }
@@ -739,10 +743,10 @@ function createPartitionsFromIndexingAddresses(registeringContractsByContract, d
739
743
  mergeBlock: undefined,
740
744
  dynamicContract: isDynamic ? contractName : undefined,
741
745
  mutPendingQueries: [],
742
- prevQueryRange: 0,
743
- prevPrevQueryRange: 0,
744
- prevRangeSize: 0,
745
- latestBlockRangeUpdateBlock: 0
746
+ sourceRangeCapacity: 0,
747
+ prevSourceRangeCapacity: 0,
748
+ eventDensity: undefined,
749
+ latestSourceRangeCapacityUpdateBlock: 0
746
750
  });
747
751
  nextPartitionIndexRef = nextPartitionIndexRef + 1 | 0;
748
752
  };
@@ -790,10 +794,10 @@ function createPartitionsFromIndexingAddresses(registeringContractsByContract, d
790
794
  mergeBlock: currentPBlock < nextPBlock ? nextPBlock : undefined,
791
795
  dynamicContract: currentP.dynamicContract,
792
796
  mutPendingQueries: currentP.mutPendingQueries,
793
- prevQueryRange: currentP.prevQueryRange,
794
- prevPrevQueryRange: currentP.prevPrevQueryRange,
795
- prevRangeSize: currentP.prevRangeSize,
796
- latestBlockRangeUpdateBlock: currentP.latestBlockRangeUpdateBlock
797
+ sourceRangeCapacity: currentP.sourceRangeCapacity,
798
+ prevSourceRangeCapacity: currentP.prevSourceRangeCapacity,
799
+ eventDensity: currentP.eventDensity,
800
+ latestSourceRangeCapacityUpdateBlock: currentP.latestSourceRangeCapacityUpdateBlock
797
801
  });
798
802
  currentPRef = {
799
803
  id: nextP.id,
@@ -803,10 +807,10 @@ function createPartitionsFromIndexingAddresses(registeringContractsByContract, d
803
807
  mergeBlock: nextP.mergeBlock,
804
808
  dynamicContract: nextP.dynamicContract,
805
809
  mutPendingQueries: nextP.mutPendingQueries,
806
- prevQueryRange: nextP.prevQueryRange,
807
- prevPrevQueryRange: nextP.prevPrevQueryRange,
808
- prevRangeSize: nextP.prevRangeSize,
809
- latestBlockRangeUpdateBlock: nextP.latestBlockRangeUpdateBlock
810
+ sourceRangeCapacity: nextP.sourceRangeCapacity,
811
+ prevSourceRangeCapacity: nextP.prevSourceRangeCapacity,
812
+ eventDensity: nextP.eventDensity,
813
+ latestSourceRangeCapacityUpdateBlock: nextP.latestSourceRangeCapacityUpdateBlock
810
814
  };
811
815
  } else {
812
816
  currentPRef = {
@@ -817,10 +821,10 @@ function createPartitionsFromIndexingAddresses(registeringContractsByContract, d
817
821
  mergeBlock: currentP.mergeBlock,
818
822
  dynamicContract: currentP.dynamicContract,
819
823
  mutPendingQueries: currentP.mutPendingQueries,
820
- prevQueryRange: currentP.prevQueryRange,
821
- prevPrevQueryRange: currentP.prevPrevQueryRange,
822
- prevRangeSize: currentP.prevRangeSize,
823
- latestBlockRangeUpdateBlock: currentP.latestBlockRangeUpdateBlock
824
+ sourceRangeCapacity: currentP.sourceRangeCapacity,
825
+ prevSourceRangeCapacity: currentP.prevSourceRangeCapacity,
826
+ eventDensity: currentP.eventDensity,
827
+ latestSourceRangeCapacityUpdateBlock: currentP.latestSourceRangeCapacityUpdateBlock
824
828
  };
825
829
  }
826
830
  }
@@ -967,10 +971,10 @@ function registerDynamicContracts(fetchState, indexingAddresses, items) {
967
971
  mergeBlock: p.mergeBlock,
968
972
  dynamicContract: p.dynamicContract,
969
973
  mutPendingQueries: p.mutPendingQueries,
970
- prevQueryRange: p.prevQueryRange,
971
- prevPrevQueryRange: p.prevPrevQueryRange,
972
- prevRangeSize: p.prevRangeSize,
973
- latestBlockRangeUpdateBlock: p.latestBlockRangeUpdateBlock
974
+ sourceRangeCapacity: p.sourceRangeCapacity,
975
+ prevSourceRangeCapacity: p.prevSourceRangeCapacity,
976
+ eventDensity: p.eventDensity,
977
+ latestSourceRangeCapacityUpdateBlock: p.latestSourceRangeCapacityUpdateBlock
974
978
  };
975
979
  let addressesByContractName = {};
976
980
  addressesByContractName[contractName] = addresses;
@@ -982,10 +986,10 @@ function registerDynamicContracts(fetchState, indexingAddresses, items) {
982
986
  mergeBlock: undefined,
983
987
  dynamicContract: contractName,
984
988
  mutPendingQueries: p.mutPendingQueries,
985
- prevQueryRange: p.prevQueryRange,
986
- prevPrevQueryRange: p.prevPrevQueryRange,
987
- prevRangeSize: p.prevRangeSize,
988
- latestBlockRangeUpdateBlock: p.latestBlockRangeUpdateBlock
989
+ sourceRangeCapacity: p.sourceRangeCapacity,
990
+ prevSourceRangeCapacity: p.prevSourceRangeCapacity,
991
+ eventDensity: p.eventDensity,
992
+ latestSourceRangeCapacityUpdateBlock: p.latestSourceRangeCapacityUpdateBlock
989
993
  });
990
994
  }
991
995
  } else {
@@ -997,10 +1001,10 @@ function registerDynamicContracts(fetchState, indexingAddresses, items) {
997
1001
  mergeBlock: p.mergeBlock,
998
1002
  dynamicContract: contractName,
999
1003
  mutPendingQueries: p.mutPendingQueries,
1000
- prevQueryRange: p.prevQueryRange,
1001
- prevPrevQueryRange: p.prevPrevQueryRange,
1002
- prevRangeSize: p.prevRangeSize,
1003
- latestBlockRangeUpdateBlock: p.latestBlockRangeUpdateBlock
1004
+ sourceRangeCapacity: p.sourceRangeCapacity,
1005
+ prevSourceRangeCapacity: p.prevSourceRangeCapacity,
1006
+ eventDensity: p.eventDensity,
1007
+ latestSourceRangeCapacityUpdateBlock: p.latestSourceRangeCapacityUpdateBlock
1004
1008
  };
1005
1009
  }
1006
1010
  }
@@ -1072,7 +1076,7 @@ function pushGapFillQueries(queries, partitionId, rangeFromBlock, rangeEndBlock,
1072
1076
  exit = 1;
1073
1077
  }
1074
1078
  if (exit === 1) {
1075
- let trustedDensity = getTrustedDensity(partition);
1079
+ let trustedDensity = partition.eventDensity;
1076
1080
  let maxBlock = rangeEndBlock !== undefined ? rangeEndBlock : chainTargetBlock;
1077
1081
  let pushSingleQuery = (density, isChunk) => {
1078
1082
  let itemsTarget = densityItemsTarget(density * chunkItemsMultiplier, rangeFromBlock, rangeEndBlock, chainTargetBlock);
@@ -1150,7 +1154,8 @@ function getNextQuery(param, chainTargetBlock, chainTargetItems, $staropt$star)
1150
1154
  shouldWaitForNewBlock = false;
1151
1155
  }
1152
1156
  }
1153
- let queriesByPartitionIndex = Stdlib_Array.fromInitializer(partitionsCount, param => []);
1157
+ let partitionsCount$1 = chainTargetItems <= 0 || shouldWaitForNewBlock ? 0 : partitionsCount;
1158
+ let queriesByPartitionIndex = Stdlib_Array.fromInitializer(partitionsCount$1, param => []);
1154
1159
  let computeQueryEndBlock = p => {
1155
1160
  let queryEndBlock = Utils.$$Math.minOptInt(endBlock, p.mergeBlock);
1156
1161
  if (blockLag !== 0) {
@@ -1164,7 +1169,7 @@ function getNextQuery(param, chainTargetBlock, chainTargetItems, $staropt$star)
1164
1169
  let partitionIndexById = {};
1165
1170
  let candidates = [];
1166
1171
  let fillStates = [];
1167
- for (let idx$1 = 0; idx$1 < partitionsCount; ++idx$1) {
1172
+ for (let idx$1 = 0; idx$1 < partitionsCount$1; ++idx$1) {
1168
1173
  let partitionId$1 = optimizedPartitions.idsInAscOrder[idx$1];
1169
1174
  let p$1 = optimizedPartitions.entities[partitionId$1];
1170
1175
  partitionIndexById[partitionId$1] = idx$1;
@@ -1189,7 +1194,7 @@ function getNextQuery(param, chainTargetBlock, chainTargetItems, $staropt$star)
1189
1194
  let pq$1 = p$1.mutPendingQueries[pqIdx$1];
1190
1195
  if (pq$1.fromBlock > cursor) {
1191
1196
  let beforeLen = candidates.length;
1192
- pushGapFillQueries(candidates, partitionId$1, cursor, Utils.$$Math.minOptInt(pq$1.fromBlock - 1 | 0, queryEndBlock), knownHeight, chainTargetBlock, maybeChunkRange, (12 - pendingCount | 0) - chunksUsedThisCall | 0, p$1, chainTargetItems / partitionsCount, chunkItemsMultiplier, p$1.selection, p$1.addressesByContractName);
1197
+ pushGapFillQueries(candidates, partitionId$1, cursor, Utils.$$Math.minOptInt(pq$1.fromBlock - 1 | 0, queryEndBlock), knownHeight, chainTargetBlock, maybeChunkRange, (12 - pendingCount | 0) - chunksUsedThisCall | 0, p$1, chainTargetItems / partitionsCount$1, chunkItemsMultiplier, p$1.selection, p$1.addressesByContractName);
1193
1198
  chunksUsedThisCall = chunksUsedThisCall + (candidates.length - beforeLen | 0) | 0;
1194
1199
  }
1195
1200
  let toBlock = pq$1.toBlock;
@@ -1248,7 +1253,7 @@ function getNextQuery(param, chainTargetBlock, chainTargetItems, $staropt$star)
1248
1253
  let eb = fs.queryEndBlock;
1249
1254
  let maxBlock = eb !== undefined ? eb : chainTargetBlock;
1250
1255
  let match = fs.maybeChunkRange;
1251
- let match$1 = getTrustedDensity(p);
1256
+ let match$1 = p.eventDensity;
1252
1257
  if (match !== undefined && match$1 !== undefined && match$1 > 0) {
1253
1258
  let chunkSize = Js_math.ceil_int(match * 1.8);
1254
1259
  let maxChunksRemaining = (12 - fs.pendingCount | 0) - fs.chunksUsedThisCall | 0;
@@ -1416,10 +1421,10 @@ function make$1(startBlock, endBlock, onEventRegistrations, contractConfigs, add
1416
1421
  mergeBlock: undefined,
1417
1422
  dynamicContract: undefined,
1418
1423
  mutPendingQueries: [],
1419
- prevQueryRange: 0,
1420
- prevPrevQueryRange: 0,
1421
- prevRangeSize: 0,
1422
- latestBlockRangeUpdateBlock: 0
1424
+ sourceRangeCapacity: 0,
1425
+ prevSourceRangeCapacity: 0,
1426
+ eventDensity: undefined,
1427
+ latestSourceRangeCapacityUpdateBlock: 0
1423
1428
  });
1424
1429
  }
1425
1430
  let normalSelection = {
@@ -1554,10 +1559,10 @@ function rollback(fetchState, indexingAddresses, targetBlockNumber) {
1554
1559
  mergeBlock: mergeBlock$1,
1555
1560
  dynamicContract: p.dynamicContract,
1556
1561
  mutPendingQueries: rollbackPendingQueries(p.mutPendingQueries, targetBlockNumber),
1557
- prevQueryRange: p.prevQueryRange,
1558
- prevPrevQueryRange: p.prevPrevQueryRange,
1559
- prevRangeSize: p.prevRangeSize,
1560
- latestBlockRangeUpdateBlock: p.latestBlockRangeUpdateBlock
1562
+ sourceRangeCapacity: p.sourceRangeCapacity,
1563
+ prevSourceRangeCapacity: p.prevSourceRangeCapacity,
1564
+ eventDensity: p.eventDensity,
1565
+ latestSourceRangeCapacityUpdateBlock: p.latestSourceRangeCapacityUpdateBlock
1561
1566
  });
1562
1567
  }
1563
1568
  }
@@ -1575,10 +1580,10 @@ function rollback(fetchState, indexingAddresses, targetBlockNumber) {
1575
1580
  mergeBlock: p.mergeBlock,
1576
1581
  dynamicContract: p.dynamicContract,
1577
1582
  mutPendingQueries: rollbackPendingQueries(p.mutPendingQueries, targetBlockNumber),
1578
- prevQueryRange: p.prevQueryRange,
1579
- prevPrevQueryRange: p.prevPrevQueryRange,
1580
- prevRangeSize: p.prevRangeSize,
1581
- latestBlockRangeUpdateBlock: p.latestBlockRangeUpdateBlock
1583
+ sourceRangeCapacity: p.sourceRangeCapacity,
1584
+ prevSourceRangeCapacity: p.prevSourceRangeCapacity,
1585
+ eventDensity: p.eventDensity,
1586
+ latestSourceRangeCapacityUpdateBlock: p.latestSourceRangeCapacityUpdateBlock
1582
1587
  });
1583
1588
  }
1584
1589
  }
@@ -1619,10 +1624,10 @@ function resetPendingQueries(fetchState) {
1619
1624
  mergeBlock: partition.mergeBlock,
1620
1625
  dynamicContract: partition.dynamicContract,
1621
1626
  mutPendingQueries: kept,
1622
- prevQueryRange: partition.prevQueryRange,
1623
- prevPrevQueryRange: partition.prevPrevQueryRange,
1624
- prevRangeSize: partition.prevRangeSize,
1625
- latestBlockRangeUpdateBlock: partition.latestBlockRangeUpdateBlock
1627
+ sourceRangeCapacity: partition.sourceRangeCapacity,
1628
+ prevSourceRangeCapacity: partition.prevSourceRangeCapacity,
1629
+ eventDensity: partition.eventDensity,
1630
+ latestSourceRangeCapacityUpdateBlock: partition.latestSourceRangeCapacityUpdateBlock
1626
1631
  };
1627
1632
  }
1628
1633
  }
@@ -1698,7 +1703,7 @@ function getProgressPercentage(fetchState) {
1698
1703
  if (firstEventBlock === undefined) {
1699
1704
  return 0;
1700
1705
  }
1701
- let totalRange = fetchState.knownHeight - firstEventBlock | 0;
1706
+ let totalRange = (fetchState.knownHeight - fetchState.blockLag | 0) - firstEventBlock | 0;
1702
1707
  if (totalRange <= 0) {
1703
1708
  return 0;
1704
1709
  }
package/src/Rollback.res CHANGED
@@ -72,7 +72,7 @@ let rec rollback = async (
72
72
  // found yet. Wait for the ReorgDetected branch above to find it and re-kick.
73
73
  | FindingReorgDepth => ()
74
74
  | FoundReorgDepth(_) if state->IndexerState.isProcessing =>
75
- Logging.info("Waiting for batch to finish processing before executing rollback")
75
+ Logging.trace("Waiting for batch to finish processing before executing rollback")
76
76
  | FoundReorgDepth({chain: reorgChain, rollbackTargetBlockNumber}) =>
77
77
  await executeRollback(
78
78
  state,
@@ -96,10 +96,10 @@ and executeRollback = async (
96
96
  ) => {
97
97
  let startTime = Performance.now()
98
98
 
99
- let chainState = state->IndexerState.getChainState(~chain=reorgChain)
100
-
101
- let logger = Logging.createChildFrom(
102
- ~logger=chainState->ChainState.logger,
99
+ // Not derived from the reorg chain's logger: that would bind its chainId onto
100
+ // every line, colliding with the per-chain chainId on the "Rollbacked" logs.
101
+ // The reorg chain is identified by the reorgChain param instead.
102
+ let logger = Logging.createChild(
103
103
  ~params={
104
104
  "action": "Rollback",
105
105
  "reorgChain": reorgChain,
@@ -161,10 +161,12 @@ and executeRollback = async (
161
161
  }
162
162
  }
163
163
 
164
+ let rolledBackChains = []
164
165
  state
165
166
  ->IndexerState.chainStates
166
167
  ->Utils.Dict.forEach(cs => {
167
168
  let chainId = (cs->ChainState.chainConfig).id
169
+ let fromBlock = cs->ChainState.committedProgressBlockNumber
168
170
  cs->ChainState.rollback(
169
171
  ~newProgressBlockNumber=newProgressBlockNumberPerChain->Utils.Dict.dangerouslyGetByIntNonOption(
170
172
  chainId,
@@ -175,6 +177,19 @@ and executeRollback = async (
175
177
  ~rollbackTargetBlockNumber,
176
178
  ~isReorgChain=chainId === reorgChainId,
177
179
  )
180
+ let toBlock = cs->ChainState.committedProgressBlockNumber
181
+ if fromBlock !== toBlock {
182
+ rolledBackChains
183
+ ->Array.push({
184
+ "chainId": chainId,
185
+ "fromBlock": fromBlock,
186
+ "toBlock": toBlock,
187
+ "rollbackedEvents": eventsProcessedDiffByChain
188
+ ->Utils.Dict.dangerouslyGetByIntNonOption(chainId)
189
+ ->Option.getOr(0.),
190
+ })
191
+ ->ignore
192
+ }
178
193
  })
179
194
 
180
195
  let diff = await state->InMemoryStore.prepareRollbackDiff(
@@ -183,15 +198,19 @@ and executeRollback = async (
183
198
  ~progressBlockNumberByChainId=newProgressBlockNumberPerChain,
184
199
  )
185
200
 
201
+ rolledBackChains->Array.forEach(chain => {
202
+ logger->Logging.childInfo({
203
+ "msg": "Rollbacked",
204
+ "chainId": chain["chainId"],
205
+ "fromBlock": chain["fromBlock"],
206
+ "toBlock": chain["toBlock"],
207
+ "rollbackedEvents": chain["rollbackedEvents"],
208
+ })
209
+ })
186
210
  logger->Logging.childTrace({
187
- "msg": "Finished rollback on reorg",
188
- "entityChanges": {
189
- "deleted": diff["deletedEntities"],
190
- "upserted": diff["setEntities"],
191
- },
192
- "rollbackedEvents": rollbackedProcessedEvents.contents,
193
- "beforeCheckpointId": state->IndexerState.committedCheckpointId,
194
- "targetCheckpointId": rollbackTargetCheckpointId,
211
+ "msg": "Rollback entity changes",
212
+ "deleted": diff["deletedEntities"],
213
+ "upserted": diff["setEntities"],
195
214
  })
196
215
  Prometheus.RollbackSuccess.increment(
197
216
  ~timeSeconds=Performance.secondsSince(startTime),
@@ -33,8 +33,7 @@ async function getLastKnownValidBlock(chainState, reorgBlockNumber, isRealtime)
33
33
 
34
34
  async function executeRollback(state, reorgChain, rollbackTargetBlockNumber, scheduleFetch, scheduleProcessing) {
35
35
  let startTime = Performance.now();
36
- let chainState = IndexerState.getChainState(state, reorgChain);
37
- let logger = Logging.createChildFrom(ChainState.logger(chainState), {
36
+ let logger = Logging.createChild({
38
37
  action: "Rollback",
39
38
  reorgChain: reorgChain,
40
39
  targetBlockNumber: rollbackTargetBlockNumber
@@ -55,20 +54,34 @@ async function executeRollback(state, reorgChain, rollbackTargetBlockNumber, sch
55
54
  eventsProcessedDiffByChain[diff.chain_id] = eventsProcessedDiff;
56
55
  newProgressBlockNumberPerChain[diff.chain_id] = rollbackTargetCheckpointId === 0n && diff.chain_id === reorgChain ? Primitive_int.min(diff.new_progress_block_number, rollbackTargetBlockNumber) : diff.new_progress_block_number;
57
56
  }
57
+ let rolledBackChains = [];
58
58
  Utils.Dict.forEach(IndexerState.chainStates(state), cs => {
59
59
  let chainId = ChainState.chainConfig(cs).id;
60
+ let fromBlock = ChainState.committedProgressBlockNumber(cs);
60
61
  ChainState.rollback(cs, newProgressBlockNumberPerChain[chainId], eventsProcessedDiffByChain[chainId], rollbackTargetBlockNumber, chainId === reorgChain);
62
+ let toBlock = ChainState.committedProgressBlockNumber(cs);
63
+ if (fromBlock !== toBlock) {
64
+ rolledBackChains.push({
65
+ chainId: chainId,
66
+ fromBlock: fromBlock,
67
+ toBlock: toBlock,
68
+ rollbackedEvents: Stdlib_Option.getOr(eventsProcessedDiffByChain[chainId], 0)
69
+ });
70
+ return;
71
+ }
61
72
  });
62
73
  let diff$1 = await InMemoryStore.prepareRollbackDiff(state, rollbackTargetCheckpointId, IndexerState.committedCheckpointId(state) + 1n, newProgressBlockNumberPerChain);
74
+ rolledBackChains.forEach(chain => Logging.childInfo(logger, {
75
+ msg: "Rollbacked",
76
+ chainId: chain.chainId,
77
+ fromBlock: chain.fromBlock,
78
+ toBlock: chain.toBlock,
79
+ rollbackedEvents: chain.rollbackedEvents
80
+ }));
63
81
  Logging.childTrace(logger, {
64
- msg: "Finished rollback on reorg",
65
- entityChanges: {
66
- deleted: diff$1.deletedEntities,
67
- upserted: diff$1.setEntities
68
- },
69
- rollbackedEvents: rollbackedProcessedEvents,
70
- beforeCheckpointId: IndexerState.committedCheckpointId(state),
71
- targetCheckpointId: rollbackTargetCheckpointId
82
+ msg: "Rollback entity changes",
83
+ deleted: diff$1.deletedEntities,
84
+ upserted: diff$1.setEntities
72
85
  });
73
86
  Prometheus.RollbackSuccess.increment(Performance.secondsSince(startTime), rollbackedProcessedEvents);
74
87
  IndexerState.completeRollback(state, eventsProcessedDiffByChain);
@@ -97,7 +110,7 @@ async function rollback(state, scheduleFetch, scheduleProcessing, scheduleRollba
97
110
  return scheduleRollback();
98
111
  case "FoundReorgDepth" :
99
112
  if (IndexerState.isProcessing(state)) {
100
- return Logging.info("Waiting for batch to finish processing before executing rollback");
113
+ return Logging.trace("Waiting for batch to finish processing before executing rollback");
101
114
  } else {
102
115
  return await executeRollback(state, match.chain, match.rollbackTargetBlockNumber, scheduleFetch, scheduleProcessing);
103
116
  }
@@ -488,6 +488,16 @@ let make = (
488
488
 
489
489
  switch maybeConfig {
490
490
  | None => ()
491
+ // Exclude instructions from failed transactions. HyperSync has no
492
+ // server-side predicate to filter instructions by parent-transaction
493
+ // success (`InstructionSelection` only exposes `is_inner`, and
494
+ // instruction/transaction selections union at block level rather than
495
+ // joining), so we filter client-side on the `isCommitted` flag HyperSync
496
+ // already delivers on every instruction row (a required column, zero extra
497
+ // bandwidth). When HyperSync adds a server-side `is_committed` predicate the
498
+ // query can push this down and the client-side check becomes a redundant
499
+ // safety net.
500
+ | Some(_) if !instr.isCommitted => ()
491
501
  | Some(onEventRegistration) =>
492
502
  let eventConfig =
493
503
  onEventRegistration.eventConfig->(
@@ -415,7 +415,7 @@ function make(param) {
415
415
  let byteLengths = Stdlib_Option.getOr(orderingByProgram[instr.programId], []);
416
416
  let contractAddress = instr.programId;
417
417
  let maybeConfig = probeRouter(eventRouter, programId, instr, byteLengths, contractAddress, contractNameByAddress);
418
- if (maybeConfig !== undefined) {
418
+ if (maybeConfig !== undefined && instr.isCommitted) {
419
419
  let eventConfig = maybeConfig.eventConfig;
420
420
  let logKey = instr.slot.toString() + ":" + instr.transactionIndex.toString() + ":" + serializeInstructionAddress(instr.instructionAddress);
421
421
  let maybeLogs = Stdlib_Option.map(logsByKey[logKey], logs => logs.map(log => ({