envio 3.3.0-alpha.10 → 3.3.0-alpha.11
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/licenses/CLA.md +35 -0
- package/licenses/EULA.md +67 -0
- package/licenses/LICENSE.md +45 -0
- package/licenses/README.md +35 -0
- package/package.json +9 -8
- package/src/Batch.res.mjs +1 -1
- package/src/ChainFetching.res +13 -10
- package/src/ChainFetching.res.mjs +3 -4
- package/src/ChainState.res +45 -30
- package/src/ChainState.res.mjs +22 -8
- package/src/ChainState.resi +5 -0
- package/src/Config.res +9 -5
- package/src/Config.res.mjs +2 -2
- package/src/Core.res +20 -0
- package/src/Core.res.mjs +12 -0
- package/src/CrossChainState.res +18 -1
- package/src/CrossChainState.res.mjs +2 -1
- package/src/EventConfigBuilder.res +21 -46
- package/src/EventConfigBuilder.res.mjs +18 -25
- package/src/EventProcessing.res +8 -5
- package/src/EventProcessing.res.mjs +2 -1
- package/src/FetchState.res +344 -263
- package/src/FetchState.res.mjs +209 -131
- package/src/HandlerRegister.res +3 -1
- package/src/HandlerRegister.res.mjs +3 -2
- package/src/Internal.res +11 -2
- package/src/LogSelection.res +0 -62
- package/src/LogSelection.res.mjs +0 -74
- package/src/RawEvent.res +2 -2
- package/src/SimulateDeadInputTracker.res +1 -1
- package/src/SimulateItems.res +38 -6
- package/src/SimulateItems.res.mjs +28 -9
- package/src/TestIndexer.res +2 -2
- package/src/TestIndexer.res.mjs +2 -2
- package/src/TopicFilter.res +1 -25
- package/src/TopicFilter.res.mjs +0 -74
- package/src/bindings/Viem.res +0 -5
- package/src/sources/EventRouter.res +0 -21
- package/src/sources/EventRouter.res.mjs +0 -12
- package/src/sources/EvmChain.res +2 -27
- package/src/sources/EvmChain.res.mjs +2 -23
- package/src/sources/EvmRpcClient.res +12 -15
- package/src/sources/EvmRpcClient.res.mjs +3 -3
- package/src/sources/HyperSync.res +9 -38
- package/src/sources/HyperSync.res.mjs +16 -28
- package/src/sources/HyperSync.resi +2 -2
- package/src/sources/HyperSyncClient.res +88 -11
- package/src/sources/HyperSyncClient.res.mjs +39 -6
- package/src/sources/HyperSyncSource.res +18 -199
- package/src/sources/HyperSyncSource.res.mjs +9 -128
- package/src/sources/Rpc.res +0 -32
- package/src/sources/Rpc.res.mjs +1 -46
- package/src/sources/RpcSource.res +29 -175
- package/src/sources/RpcSource.res.mjs +32 -110
- package/src/sources/SimulateSource.res +37 -19
- package/src/sources/SimulateSource.res.mjs +27 -10
- package/src/sources/SvmHyperSyncSource.res +3 -3
package/src/FetchState.res
CHANGED
|
@@ -629,13 +629,90 @@ let bufferReadyCount = (fetchState: t) => {
|
|
|
629
629
|
/*
|
|
630
630
|
Comparitor for two events from the same chain. No need for chain id or timestamp
|
|
631
631
|
*/
|
|
632
|
-
let
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
632
|
+
let getRegistrationIndex = (item: Internal.item): int =>
|
|
633
|
+
switch item {
|
|
634
|
+
| Event({onEventRegistration}) => onEventRegistration.index
|
|
635
|
+
| Block({onBlockRegistration}) => onBlockRegistration.index
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Total order on buffer items: block, then logIndex, then registration index.
|
|
639
|
+
// Returns a plain int (-1/0/1) with explicit field comparisons so it can be
|
|
640
|
+
// called directly from the merge/insertion loops below — no Array.sort callback,
|
|
641
|
+
// no allocated key. `0` means a true duplicate: same log routed to the same
|
|
642
|
+
// registration (two registrations for one log differ by index and are kept).
|
|
643
|
+
let compareBufferItem = (a: Internal.item, b: Internal.item): int => {
|
|
644
|
+
let ba = a->Internal.getItemBlockNumber
|
|
645
|
+
let bb = b->Internal.getItemBlockNumber
|
|
646
|
+
if ba != bb {
|
|
647
|
+
ba < bb ? -1 : 1
|
|
636
648
|
} else {
|
|
637
|
-
|
|
649
|
+
let la = a->Internal.getItemLogIndex
|
|
650
|
+
let lb = b->Internal.getItemLogIndex
|
|
651
|
+
if la != lb {
|
|
652
|
+
la < lb ? -1 : 1
|
|
653
|
+
} else {
|
|
654
|
+
let ia = a->getRegistrationIndex
|
|
655
|
+
let ib = b->getRegistrationIndex
|
|
656
|
+
ia < ib ? -1 : ia > ib ? 1 : 0
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Merge a maybe-unsorted `newItems` run into the already-sorted, already-deduped
|
|
662
|
+
// `buffer`, dropping items equal on (blockNumber, logIndex, registration index).
|
|
663
|
+
// Single linear pass over both runs after ordering `newItems` in place; every
|
|
664
|
+
// comparison is a direct `compareBufferItem` call (V8 inlines it) rather than a
|
|
665
|
+
// callback through `Array.sort`.
|
|
666
|
+
let mergeIntoBuffer = (buffer: array<Internal.item>, newItems: array<Internal.item>): array<
|
|
667
|
+
Internal.item,
|
|
668
|
+
> => {
|
|
669
|
+
let n = newItems->Array.length
|
|
670
|
+
// Insertion sort: a source response is small and usually already ascending,
|
|
671
|
+
// so this is ~O(n) here.
|
|
672
|
+
for i in 1 to n - 1 {
|
|
673
|
+
let x = newItems->Array.getUnsafe(i)
|
|
674
|
+
let j = ref(i - 1)
|
|
675
|
+
while j.contents >= 0 && compareBufferItem(newItems->Array.getUnsafe(j.contents), x) > 0 {
|
|
676
|
+
newItems->Array.setUnsafe(j.contents + 1, newItems->Array.getUnsafe(j.contents))
|
|
677
|
+
j := j.contents - 1
|
|
678
|
+
}
|
|
679
|
+
newItems->Array.setUnsafe(j.contents + 1, x)
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
let m = buffer->Array.length
|
|
683
|
+
let merged = []
|
|
684
|
+
let last = ref(None)
|
|
685
|
+
let push = item =>
|
|
686
|
+
switch last.contents {
|
|
687
|
+
| Some(l) if compareBufferItem(l, item) === 0 => ()
|
|
688
|
+
| _ => {
|
|
689
|
+
merged->Array.push(item)
|
|
690
|
+
last := Some(item)
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
let i = ref(0)
|
|
695
|
+
let j = ref(0)
|
|
696
|
+
while i.contents < m && j.contents < n {
|
|
697
|
+
let a = buffer->Array.getUnsafe(i.contents)
|
|
698
|
+
let b = newItems->Array.getUnsafe(j.contents)
|
|
699
|
+
if compareBufferItem(a, b) <= 0 {
|
|
700
|
+
push(a)
|
|
701
|
+
i := i.contents + 1
|
|
702
|
+
} else {
|
|
703
|
+
push(b)
|
|
704
|
+
j := j.contents + 1
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
while i.contents < m {
|
|
708
|
+
push(buffer->Array.getUnsafe(i.contents))
|
|
709
|
+
i := i.contents + 1
|
|
638
710
|
}
|
|
711
|
+
while j.contents < n {
|
|
712
|
+
push(newItems->Array.getUnsafe(j.contents))
|
|
713
|
+
j := j.contents + 1
|
|
714
|
+
}
|
|
715
|
+
merged
|
|
639
716
|
}
|
|
640
717
|
|
|
641
718
|
// Some big number which should be bigger than any log index
|
|
@@ -706,48 +783,46 @@ let updateInternal = (
|
|
|
706
783
|
fetchState: t,
|
|
707
784
|
~optimizedPartitions=fetchState.optimizedPartitions,
|
|
708
785
|
~mutItems=?,
|
|
786
|
+
// Set when the caller already passes a sorted, deduped buffer (hot paths merge
|
|
787
|
+
// via mergeIntoBuffer or filter the sorted buffer). Otherwise mutItems is
|
|
788
|
+
// normalized here, so callers can hand over items in any order.
|
|
789
|
+
~mutItemsSorted=false,
|
|
709
790
|
~blockLag=fetchState.blockLag,
|
|
710
791
|
~knownHeight=fetchState.knownHeight,
|
|
711
792
|
): t => {
|
|
712
|
-
|
|
793
|
+
// The buffer to build on: the caller's items (normalized to sorted if needed),
|
|
794
|
+
// or the current buffer when only onBlock items change.
|
|
795
|
+
let base = switch mutItems {
|
|
796
|
+
| Some(items) => mutItemsSorted ? items : []->mergeIntoBuffer(items)
|
|
797
|
+
| None => fetchState.buffer
|
|
798
|
+
}
|
|
713
799
|
|
|
800
|
+
// onBlock items are generated as their own ascending (block, logIndex) run and
|
|
801
|
+
// folded into `base` by the single merge below.
|
|
802
|
+
let blockItems = []
|
|
714
803
|
let latestOnBlockBlockNumber = switch fetchState.onBlockRegistrations {
|
|
715
804
|
| [] => knownHeight
|
|
716
|
-
| onBlockRegistrations =>
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
|
726
|
-
|
|
|
727
|
-
}->Array.get(fetchState.maxOnBlockBufferSize - 1) {
|
|
728
|
-
| Some(item) => item->Internal.getItemBlockNumber
|
|
729
|
-
| None =>
|
|
730
|
-
switch optimizedPartitions->OptimizedPartitions.getLatestFullyFetchedBlock {
|
|
731
|
-
| None => knownHeight
|
|
732
|
-
| Some(latestFullyFetchedBlock) => latestFullyFetchedBlock.blockNumber
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
let mutItems = switch mutItemsRef.contents {
|
|
737
|
-
| Some(mutItems) => mutItems
|
|
738
|
-
| None => fetchState.buffer->Array.copy
|
|
805
|
+
| onBlockRegistrations =>
|
|
806
|
+
// Calculate the max block number we are going to create items for
|
|
807
|
+
// Use maxOnBlockBufferSize to get the last target item in the buffer
|
|
808
|
+
// (sorted, so this is the highest-block item within the buffer cap).
|
|
809
|
+
// All this needed to prevent OOM when adding too many block items to the queue
|
|
810
|
+
let maxBlockNumber = switch base->Array.get(fetchState.maxOnBlockBufferSize - 1) {
|
|
811
|
+
| Some(item) => item->Internal.getItemBlockNumber
|
|
812
|
+
| None =>
|
|
813
|
+
switch optimizedPartitions->OptimizedPartitions.getLatestFullyFetchedBlock {
|
|
814
|
+
| None => knownHeight
|
|
815
|
+
| Some(latestFullyFetchedBlock) => latestFullyFetchedBlock.blockNumber
|
|
739
816
|
}
|
|
740
|
-
mutItemsRef := Some(mutItems)
|
|
741
|
-
|
|
742
|
-
appendOnBlockItems(
|
|
743
|
-
~mutItems,
|
|
744
|
-
~onBlockRegistrations,
|
|
745
|
-
~indexerStartBlock=fetchState.startBlock,
|
|
746
|
-
~fromBlock=fetchState.latestOnBlockBlockNumber,
|
|
747
|
-
~maxBlockNumber,
|
|
748
|
-
~maxOnBlockBufferSize=fetchState.maxOnBlockBufferSize,
|
|
749
|
-
)
|
|
750
817
|
}
|
|
818
|
+
appendOnBlockItems(
|
|
819
|
+
~mutItems=blockItems,
|
|
820
|
+
~onBlockRegistrations,
|
|
821
|
+
~indexerStartBlock=fetchState.startBlock,
|
|
822
|
+
~fromBlock=fetchState.latestOnBlockBlockNumber,
|
|
823
|
+
~maxBlockNumber,
|
|
824
|
+
~maxOnBlockBufferSize=fetchState.maxOnBlockBufferSize,
|
|
825
|
+
)
|
|
751
826
|
}
|
|
752
827
|
|
|
753
828
|
let updatedFetchState = {
|
|
@@ -762,15 +837,10 @@ let updateInternal = (
|
|
|
762
837
|
latestOnBlockBlockNumber,
|
|
763
838
|
blockLag,
|
|
764
839
|
knownHeight,
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
| Some(mutItems) => {
|
|
770
|
-
mutItems->Array.sort(compareBufferItem)
|
|
771
|
-
mutItems
|
|
772
|
-
}
|
|
773
|
-
| None => fetchState.buffer
|
|
840
|
+
// Single merge point: fold any onBlock items into the sorted base buffer.
|
|
841
|
+
buffer: switch blockItems {
|
|
842
|
+
| [] => base
|
|
843
|
+
| blockItems => base->mergeIntoBuffer(blockItems)
|
|
774
844
|
},
|
|
775
845
|
firstEventBlock: fetchState.firstEventBlock,
|
|
776
846
|
}
|
|
@@ -1287,6 +1357,26 @@ let registerDynamicContracts = (
|
|
|
1287
1357
|
}
|
|
1288
1358
|
}
|
|
1289
1359
|
|
|
1360
|
+
// Drop events an address-param filter rejects. A merged partition may
|
|
1361
|
+
// over-fetch a wildcard event whose indexed address param references an
|
|
1362
|
+
// address registered after the log's block; `clientAddressFilter` is the
|
|
1363
|
+
// param-level analogue of EventRouter's srcAddress effectiveStartBlock check.
|
|
1364
|
+
let filterByClientAddress = (
|
|
1365
|
+
items: array<Internal.item>,
|
|
1366
|
+
~indexingAddresses: IndexingAddresses.t,
|
|
1367
|
+
): array<Internal.item> =>
|
|
1368
|
+
items->Array.filter(item =>
|
|
1369
|
+
switch item {
|
|
1370
|
+
| Internal.Event({payload, blockNumber}) as item =>
|
|
1371
|
+
switch (item->Internal.castUnsafeEventItem).onEventRegistration.clientAddressFilter {
|
|
1372
|
+
| Some(filter) =>
|
|
1373
|
+
filter(payload, blockNumber, indexingAddresses->IndexingAddresses.rawForFilter)
|
|
1374
|
+
| None => true
|
|
1375
|
+
}
|
|
1376
|
+
| _ => true
|
|
1377
|
+
}
|
|
1378
|
+
)
|
|
1379
|
+
|
|
1290
1380
|
/*
|
|
1291
1381
|
Updates fetchState with a response for a given query.
|
|
1292
1382
|
Returns Error if the partition with given query cannot be found (unexpected)
|
|
@@ -1295,26 +1385,10 @@ newItems are ordered earliest to latest (as they are returned from the worker)
|
|
|
1295
1385
|
*/
|
|
1296
1386
|
let handleQueryResult = (
|
|
1297
1387
|
fetchState: t,
|
|
1298
|
-
~indexingAddresses: IndexingAddresses.t,
|
|
1299
1388
|
~query: query,
|
|
1300
1389
|
~latestFetchedBlock: blockNumberAndTimestamp,
|
|
1301
1390
|
~newItems,
|
|
1302
1391
|
): t => {
|
|
1303
|
-
// Drop events an address-param filter rejects. A merged partition may
|
|
1304
|
-
// over-fetch a wildcard event whose indexed address param references an
|
|
1305
|
-
// address registered after the log's block; `clientAddressFilter` is the
|
|
1306
|
-
// param-level analogue of EventRouter's srcAddress effectiveStartBlock check.
|
|
1307
|
-
let newItems = newItems->Array.filter(item =>
|
|
1308
|
-
switch item {
|
|
1309
|
-
| Internal.Event({onEventRegistration, payload, blockNumber}) =>
|
|
1310
|
-
switch onEventRegistration.clientAddressFilter {
|
|
1311
|
-
| Some(filter) =>
|
|
1312
|
-
filter(payload, blockNumber, indexingAddresses->IndexingAddresses.rawForFilter)
|
|
1313
|
-
| None => true
|
|
1314
|
-
}
|
|
1315
|
-
| _ => true
|
|
1316
|
-
}
|
|
1317
|
-
)
|
|
1318
1392
|
fetchState->updateInternal(
|
|
1319
1393
|
~optimizedPartitions=fetchState.optimizedPartitions->OptimizedPartitions.handleQueryResponse(
|
|
1320
1394
|
~query,
|
|
@@ -1322,10 +1396,14 @@ let handleQueryResult = (
|
|
|
1322
1396
|
~itemsCount=newItems->Array.length,
|
|
1323
1397
|
~latestFetchedBlock,
|
|
1324
1398
|
),
|
|
1399
|
+
// Merge the response into the sorted buffer, dropping duplicates an
|
|
1400
|
+
// overlapping query may re-deliver (e.g. an over-fetched log matched by two
|
|
1401
|
+
// partitions). Absorbs sorting too, so updateInternal doesn't re-sort.
|
|
1402
|
+
~mutItemsSorted=true,
|
|
1325
1403
|
~mutItems=?{
|
|
1326
1404
|
switch newItems {
|
|
1327
1405
|
| [] => None
|
|
1328
|
-
| _ => Some(fetchState.buffer->
|
|
1406
|
+
| _ => Some(fetchState.buffer->mergeIntoBuffer(newItems))
|
|
1329
1407
|
}
|
|
1330
1408
|
},
|
|
1331
1409
|
)
|
|
@@ -1370,14 +1448,14 @@ let startFetchingQueries = ({optimizedPartitions}: t, ~queries: array<query>) =>
|
|
|
1370
1448
|
// Most parallel in-flight chunk queries a single partition may have at once.
|
|
1371
1449
|
let maxPendingChunksPerPartition = 12
|
|
1372
1450
|
|
|
1373
|
-
//
|
|
1374
|
-
// out-of-order partial response)
|
|
1375
|
-
// range
|
|
1376
|
-
// partition's trusted density when it has one;
|
|
1377
|
-
// density" — the partition's equal-divide budget
|
|
1378
|
-
// range this tick — so a small gap reserves
|
|
1379
|
-
// noisy one-sample estimate. Chunks only on
|
|
1380
|
-
//
|
|
1451
|
+
// Generates candidate queries for a gap range (a hole left between
|
|
1452
|
+
// completed/pending chunks, e.g. from an out-of-order partial response). Gaps
|
|
1453
|
+
// carry the range's low fromBlock, so the acceptance pass takes them before
|
|
1454
|
+
// forward progress. Priced by the partition's trusted density when it has one;
|
|
1455
|
+
// otherwise by the "available density" — the partition's equal-divide budget
|
|
1456
|
+
// spread over its remaining range this tick — so a small gap reserves
|
|
1457
|
+
// proportionally little instead of a noisy one-sample estimate. Chunks only on
|
|
1458
|
+
// a trusted POSITIVE density. Returns the created queries' total itemsEst.
|
|
1381
1459
|
let pushGapFillQueries = (
|
|
1382
1460
|
queries: array<query>,
|
|
1383
1461
|
~partitionId: string,
|
|
@@ -1481,46 +1559,19 @@ let pushGapFillQueries = (
|
|
|
1481
1559
|
cost.contents
|
|
1482
1560
|
}
|
|
1483
1561
|
|
|
1484
|
-
//
|
|
1485
|
-
//
|
|
1486
|
-
|
|
1487
|
-
// are topped up exactly to L, so the pour equals the budget no matter how
|
|
1488
|
-
// uneven the footprints are.
|
|
1489
|
-
let waterLevel = (~budget: float, ~footprints: array<float>) => {
|
|
1490
|
-
let sorted = footprints->Array.toSorted(Float.compare)
|
|
1491
|
-
let n = sorted->Array.length
|
|
1492
|
-
let prefix = ref(0.)
|
|
1493
|
-
let level = ref(None)
|
|
1494
|
-
let idx = ref(0)
|
|
1495
|
-
while level.contents == None && idx.contents < n {
|
|
1496
|
-
let i = idx.contents
|
|
1497
|
-
prefix := prefix.contents +. sorted->Array.getUnsafe(i)
|
|
1498
|
-
// The level if only the i+1 lowest footprints receive water — correct once
|
|
1499
|
-
// it doesn't reach the next footprint up.
|
|
1500
|
-
let candidate = (budget +. prefix.contents) /. (i + 1)->Int.toFloat
|
|
1501
|
-
if i == n - 1 || candidate <= sorted->Array.getUnsafe(i + 1) {
|
|
1502
|
-
level := Some(candidate)
|
|
1503
|
-
}
|
|
1504
|
-
idx := idx.contents + 1
|
|
1505
|
-
}
|
|
1506
|
-
level.contents->Option.getOr(0.)
|
|
1507
|
-
}
|
|
1508
|
-
|
|
1509
|
-
// Per-partition mutable state threaded through the water-fill rounds below.
|
|
1510
|
-
type waterFillState = {
|
|
1562
|
+
// Per-partition state carried from the gap-fill/cursor walk to candidate
|
|
1563
|
+
// generation.
|
|
1564
|
+
type partitionFillState = {
|
|
1511
1565
|
partitionId: string,
|
|
1512
1566
|
p: partition,
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
//
|
|
1516
|
-
|
|
1567
|
+
cursor: int,
|
|
1568
|
+
// Chunks already generated for this partition during gap-fill — used with
|
|
1569
|
+
// pendingCount against maxPendingChunksPerPartition.
|
|
1570
|
+
chunksUsedThisCall: int,
|
|
1571
|
+
// Existing mutPendingQueries count before this call — fixed for the call.
|
|
1517
1572
|
pendingCount: int,
|
|
1518
1573
|
queryEndBlock: option<int>,
|
|
1519
1574
|
maybeChunkRange: option<int>,
|
|
1520
|
-
// This partition's own slot in queriesByPartitionIndex — gap-fill and every
|
|
1521
|
-
// water-fill round push here directly, so query order falls out of
|
|
1522
|
-
// idsInAscOrder for free (see getNextQuery) instead of a final sort pass.
|
|
1523
|
-
bucket: array<query>,
|
|
1524
1575
|
}
|
|
1525
1576
|
|
|
1526
1577
|
// Candidate queries are sized against chainTargetBlock, the soft querying
|
|
@@ -1531,24 +1582,24 @@ type waterFillState = {
|
|
|
1531
1582
|
// query with no other ceiling: the true hard bounds stay
|
|
1532
1583
|
// endBlock/mergeBlock/the lagged head.
|
|
1533
1584
|
//
|
|
1534
|
-
//
|
|
1535
|
-
//
|
|
1536
|
-
//
|
|
1537
|
-
//
|
|
1538
|
-
//
|
|
1539
|
-
//
|
|
1540
|
-
//
|
|
1541
|
-
//
|
|
1542
|
-
//
|
|
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.
|
|
1591
|
+
// Sorting by fromBlock spends the budget on the earliest blocks across all
|
|
1592
|
+
// partitions first, so no partition is starved by generation order and the
|
|
1593
|
+
// frontier advances evenly. In-flight reservations release as responses land,
|
|
1594
|
+
// so acceptance redistributes across ticks.
|
|
1543
1595
|
//
|
|
1544
1596
|
// A partition with a trusted positive density (two or more responses — see
|
|
1545
|
-
// getMinHistoryRange)
|
|
1546
|
-
//
|
|
1547
|
-
//
|
|
1548
|
-
//
|
|
1549
|
-
//
|
|
1550
|
-
//
|
|
1551
|
-
// allotment instead.
|
|
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.
|
|
1552
1603
|
let getNextQuery = (
|
|
1553
1604
|
{optimizedPartitions, blockLag, latestOnBlockBlockNumber, knownHeight, endBlock}: t,
|
|
1554
1605
|
~chainTargetBlock: int,
|
|
@@ -1588,8 +1639,8 @@ let getNextQuery = (
|
|
|
1588
1639
|
}
|
|
1589
1640
|
}
|
|
1590
1641
|
|
|
1591
|
-
// One bucket per partition, in idsInAscOrder order — gap-fill and
|
|
1592
|
-
//
|
|
1642
|
+
// One bucket per partition, in idsInAscOrder order — gap-fill and the
|
|
1643
|
+
// budget pass both push into a partition's own bucket, so flattening at
|
|
1593
1644
|
// the end (see below) reproduces idsInAscOrder without a sort.
|
|
1594
1645
|
let queriesByPartitionIndex: array<
|
|
1595
1646
|
array<query>,
|
|
@@ -1607,36 +1658,50 @@ let getNextQuery = (
|
|
|
1607
1658
|
}
|
|
1608
1659
|
}
|
|
1609
1660
|
|
|
1610
|
-
//
|
|
1611
|
-
//
|
|
1612
|
-
//
|
|
1613
|
-
//
|
|
1614
|
-
//
|
|
1615
|
-
let existingReservedByPartition = Dict.make()
|
|
1661
|
+
// In-flight itemsEst summed over queries still being fetched
|
|
1662
|
+
// (fetchedBlock === None). A query whose response already landed has had its
|
|
1663
|
+
// reservation released by ChainState even while it lingers in
|
|
1664
|
+
// mutPendingQueries behind an unfilled gap, so counting it here would
|
|
1665
|
+
// understate the budget. Sizes fresh forward work below.
|
|
1616
1666
|
let chainReserved = ref(0.)
|
|
1617
|
-
for idx in 0 to partitionsCount - 1 {
|
|
1618
|
-
let partitionId = optimizedPartitions.idsInAscOrder->Array.getUnsafe(idx)
|
|
1619
|
-
let p = optimizedPartitions.entities->Dict.getUnsafe(partitionId)
|
|
1620
|
-
let cost = ref(0.)
|
|
1621
|
-
for pqIdx in 0 to p.mutPendingQueries->Array.length - 1 {
|
|
1622
|
-
cost := cost.contents +. (p.mutPendingQueries->Array.getUnsafe(pqIdx)).itemsEst->Int.toFloat
|
|
1623
|
-
}
|
|
1624
|
-
existingReservedByPartition->Dict.set(partitionId, cost.contents)
|
|
1625
|
-
chainReserved := chainReserved.contents +. cost.contents
|
|
1626
|
-
}
|
|
1627
1667
|
|
|
1628
|
-
//
|
|
1629
|
-
//
|
|
1630
|
-
//
|
|
1631
|
-
//
|
|
1668
|
+
// (fromBlock, itemsEst) of each still-in-flight query. The acceptance pass
|
|
1669
|
+
// merges these into the candidate stream and draws them down in fromBlock
|
|
1670
|
+
// order, so a gap-fill sitting before an in-flight query claims budget ahead
|
|
1671
|
+
// of it and the buffer unblocks without waiting for that query to return.
|
|
1672
|
+
let reservations = []
|
|
1673
|
+
|
|
1674
|
+
// Position of each partition in idsInAscOrder, so an accepted query routes
|
|
1675
|
+
// back to its bucket and the output stays in idsInAscOrder. Filled in the
|
|
1676
|
+
// Phase A sweep below.
|
|
1677
|
+
let partitionIndexById = Dict.make()
|
|
1678
|
+
|
|
1679
|
+
// Every candidate query for this tick — gap-fill holes (Phase A) plus each
|
|
1680
|
+
// in-range partition's chunks/probe toward the target (Phase B) — generated
|
|
1681
|
+
// with no budget check, then merged with the in-flight reservations, sorted
|
|
1682
|
+
// by fromBlock, and accepted while the budget lasts (acceptance pass).
|
|
1683
|
+
// Selecting by fromBlock spends the budget on the earliest blocks across all
|
|
1684
|
+
// partitions first, so no partition is starved by iteration order and the
|
|
1685
|
+
// frontier advances evenly.
|
|
1686
|
+
let candidates = []
|
|
1687
|
+
|
|
1688
|
+
// Phase A: gap-fill. Walk each partition's pending queries once, generating
|
|
1689
|
+
// a candidate for any hole (e.g. from an out-of-order partial chunk
|
|
1690
|
+
// response). This also determines each partition's post-gap cursor and
|
|
1632
1691
|
// whether it's blocked on an unresolved single-shot query.
|
|
1633
1692
|
let fillStates = []
|
|
1634
|
-
let gapFillCost = ref(0.)
|
|
1635
1693
|
for idx in 0 to partitionsCount - 1 {
|
|
1636
1694
|
let partitionId = optimizedPartitions.idsInAscOrder->Array.getUnsafe(idx)
|
|
1637
1695
|
let p = optimizedPartitions.entities->Dict.getUnsafe(partitionId)
|
|
1638
|
-
|
|
1696
|
+
partitionIndexById->Dict.set(partitionId, idx)
|
|
1639
1697
|
let pendingCount = p.mutPendingQueries->Array.length
|
|
1698
|
+
for pqIdx in 0 to pendingCount - 1 {
|
|
1699
|
+
let pq = p.mutPendingQueries->Array.getUnsafe(pqIdx)
|
|
1700
|
+
if pq.fetchedBlock === None {
|
|
1701
|
+
chainReserved := chainReserved.contents +. pq.itemsEst->Int.toFloat
|
|
1702
|
+
reservations->Array.push((pq.fromBlock, pq.itemsEst))->ignore
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1640
1705
|
let queryEndBlock = computeQueryEndBlock(p)
|
|
1641
1706
|
let maybeChunkRange = getMinHistoryRange(p)
|
|
1642
1707
|
|
|
@@ -1648,9 +1713,9 @@ let getNextQuery = (
|
|
|
1648
1713
|
let pq = p.mutPendingQueries->Array.getUnsafe(pqIdx.contents)
|
|
1649
1714
|
|
|
1650
1715
|
if pq.fromBlock > cursor.contents {
|
|
1651
|
-
let beforeLen =
|
|
1652
|
-
|
|
1653
|
-
|
|
1716
|
+
let beforeLen = candidates->Array.length
|
|
1717
|
+
pushGapFillQueries(
|
|
1718
|
+
candidates,
|
|
1654
1719
|
~partitionId,
|
|
1655
1720
|
~rangeFromBlock=cursor.contents,
|
|
1656
1721
|
~rangeEndBlock=Utils.Math.minOptInt(Some(pq.fromBlock - 1), queryEndBlock),
|
|
@@ -1663,9 +1728,8 @@ let getNextQuery = (
|
|
|
1663
1728
|
~chunkItemsMultiplier,
|
|
1664
1729
|
~selection=p.selection,
|
|
1665
1730
|
~addressesByContractName=p.addressesByContractName,
|
|
1666
|
-
)
|
|
1667
|
-
chunksUsedThisCall := chunksUsedThisCall.contents + (
|
|
1668
|
-
gapFillCost := gapFillCost.contents +. cost
|
|
1731
|
+
)->ignore
|
|
1732
|
+
chunksUsedThisCall := chunksUsedThisCall.contents + (candidates->Array.length - beforeLen)
|
|
1669
1733
|
}
|
|
1670
1734
|
switch pq {
|
|
1671
1735
|
| {isChunk: true, toBlock: Some(toBlock), fetchedBlock: Some({blockNumber})}
|
|
@@ -1687,35 +1751,17 @@ let getNextQuery = (
|
|
|
1687
1751
|
pendingCount,
|
|
1688
1752
|
queryEndBlock,
|
|
1689
1753
|
maybeChunkRange,
|
|
1690
|
-
bucket,
|
|
1691
1754
|
})
|
|
1692
1755
|
->ignore
|
|
1693
1756
|
}
|
|
1694
1757
|
}
|
|
1695
1758
|
|
|
1696
|
-
//
|
|
1697
|
-
//
|
|
1698
|
-
//
|
|
1699
|
-
|
|
1700
|
-
let rangeItemsTarget = Pervasives.max(
|
|
1701
|
-
0.,
|
|
1702
|
-
chainTargetItems -. chainReserved.contents -. gapFillCost.contents,
|
|
1703
|
-
)
|
|
1704
|
-
|
|
1705
|
-
// Each in-range partition's running footprint: existing in-flight plus any
|
|
1706
|
-
// gap-fill just pushed into its bucket. The water-fill levels every
|
|
1707
|
-
// partition up toward a shared line, so this seed is what a partition
|
|
1708
|
-
// starts the fill already holding.
|
|
1709
|
-
let reservedByPartition = Dict.make()
|
|
1710
|
-
fillStates->Array.forEach(fs => {
|
|
1711
|
-
let cost = ref(existingReservedByPartition->Dict.getUnsafe(fs.partitionId))
|
|
1712
|
-
for i in 0 to fs.bucket->Array.length - 1 {
|
|
1713
|
-
cost := cost.contents +. (fs.bucket->Array.getUnsafe(i)).itemsEst->Int.toFloat
|
|
1714
|
-
}
|
|
1715
|
-
reservedByPartition->Dict.set(fs.partitionId, cost.contents)
|
|
1716
|
-
})
|
|
1759
|
+
// Budget for fresh forward work: chainTargetItems minus what's still in
|
|
1760
|
+
// flight. Sizes probes and bounds chunk generation below; the acceptance
|
|
1761
|
+
// pass does the final budgeting against the full chainTargetItems.
|
|
1762
|
+
let freshBudget = Pervasives.max(0., chainTargetItems -. chainReserved.contents)
|
|
1717
1763
|
|
|
1718
|
-
let isInRange = (fs:
|
|
1764
|
+
let isInRange = (fs: partitionFillState) =>
|
|
1719
1765
|
fs.cursor <= chainTargetBlock &&
|
|
1720
1766
|
switch fs.queryEndBlock {
|
|
1721
1767
|
| Some(eb) => fs.cursor <= eb
|
|
@@ -1724,16 +1770,33 @@ let getNextQuery = (
|
|
|
1724
1770
|
fs.pendingCount + fs.chunksUsedThisCall < maxPendingChunksPerPartition
|
|
1725
1771
|
|
|
1726
1772
|
let inRangeStates = fillStates->Array.filter(isInRange)
|
|
1727
|
-
|
|
1728
|
-
//
|
|
1729
|
-
//
|
|
1730
|
-
//
|
|
1773
|
+
let inRangeCount = inRangeStates->Array.length
|
|
1774
|
+
// Even share of the fresh budget across the partitions actually fetching
|
|
1775
|
+
// this tick (not every partition — so budget isn't stranded on ones below
|
|
1776
|
+
// the head, waiting, or already done). The fallback when there's no range to
|
|
1777
|
+
// the target.
|
|
1778
|
+
let probeShare = inRangeCount == 0 ? 0. : freshBudget /. inRangeCount->Int.toFloat
|
|
1779
|
+
// Items/block the budget implies over the range those partitions cover this
|
|
1780
|
+
// tick — from the furthest-behind in-range cursor to the target. A probe
|
|
1781
|
+
// covering less of that range (its partition sits further ahead) gets
|
|
1782
|
+
// proportionally fewer items; one starting at the frontier gets the full
|
|
1783
|
+
// even share.
|
|
1784
|
+
let frontierCursor =
|
|
1785
|
+
inRangeStates->Array.reduce(chainTargetBlock, (min, fs) => fs.cursor < min ? fs.cursor : min)
|
|
1786
|
+
let rangeToTarget = chainTargetBlock - frontierCursor + 1
|
|
1787
|
+
let rangeTargetDensity =
|
|
1788
|
+
inRangeCount > 0 && rangeToTarget > 0 ? freshBudget /. rangeToTarget->Int.toFloat : 0.
|
|
1789
|
+
|
|
1790
|
+
// 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.
|
|
1731
1795
|
//
|
|
1732
|
-
// Chunks require a POSITIVE trusted density: density 0 prices every chunk
|
|
1733
|
-
//
|
|
1734
|
-
// hard-bounded
|
|
1735
|
-
|
|
1736
|
-
let emitQueries = (fs: waterFillState, ~budget: float) => {
|
|
1796
|
+
// Chunks require a POSITIVE trusted density: density 0 prices every chunk at
|
|
1797
|
+
// ~nothing, so an open-ended probe (full server scan range in one response)
|
|
1798
|
+
// beats a pipeline of hard-bounded chunks that crawl 1.8× per two responses.
|
|
1799
|
+
inRangeStates->Array.forEach(fs => {
|
|
1737
1800
|
let p = fs.p
|
|
1738
1801
|
let maxBlock = switch fs.queryEndBlock {
|
|
1739
1802
|
| Some(eb) => eb
|
|
@@ -1741,23 +1804,25 @@ let getNextQuery = (
|
|
|
1741
1804
|
}
|
|
1742
1805
|
switch (fs.maybeChunkRange, p->getTrustedDensity) {
|
|
1743
1806
|
| (Some(minHistoryRange), Some(density)) if density > 0. =>
|
|
1744
|
-
// Chunking active: strict chunks with a hard endBlock, sized by real
|
|
1745
|
-
// density with the chunk headroom multiplier — at least one full
|
|
1746
|
-
// chunk this round even if the allotment falls short.
|
|
1747
1807
|
let chunkSize = Js.Math.ceil_int(minHistoryRange->Int.toFloat *. 1.8)
|
|
1748
1808
|
let maxChunksRemaining =
|
|
1749
1809
|
maxPendingChunksPerPartition - fs.pendingCount - fs.chunksUsedThisCall
|
|
1750
|
-
let consumed = ref(0.)
|
|
1751
|
-
let created = ref(0)
|
|
1752
|
-
let chunkFromBlock = ref(fs.cursor)
|
|
1753
|
-
let budgetLeft = ref(true)
|
|
1754
1810
|
// No chunk starts past chainTargetBlock; an emitted chunk still keeps
|
|
1755
1811
|
// its full span (chunkToBlock may exceed the target — only
|
|
1756
1812
|
// endBlock/mergeBlock are hard bounds).
|
|
1813
|
+
let chunkStartCeiling = Pervasives.min(maxBlock, chainTargetBlock)
|
|
1814
|
+
let created = ref(0)
|
|
1815
|
+
let chunkFromBlock = ref(fs.cursor)
|
|
1816
|
+
// Stop once this partition alone has generated more than the whole fresh
|
|
1817
|
+
// budget: the acceptance pass can hand a single partition at most the
|
|
1818
|
+
// budget plus one overshoot query, so further chunks could never be
|
|
1819
|
+
// accepted. Bounds generation (and the candidate sort) when the budget
|
|
1820
|
+
// is small relative to the pending-chunk cap.
|
|
1821
|
+
let generatedItems = ref(0.)
|
|
1757
1822
|
while (
|
|
1758
|
-
budgetLeft.contents &&
|
|
1759
1823
|
created.contents < maxChunksRemaining &&
|
|
1760
|
-
chunkFromBlock.contents <=
|
|
1824
|
+
chunkFromBlock.contents <= chunkStartCeiling &&
|
|
1825
|
+
generatedItems.contents <= freshBudget
|
|
1761
1826
|
) {
|
|
1762
1827
|
let chunkToBlock = Pervasives.min(chunkFromBlock.contents + chunkSize - 1, maxBlock)
|
|
1763
1828
|
let rawEst = density *. (chunkToBlock - chunkFromBlock.contents + 1)->Int.toFloat
|
|
@@ -1766,89 +1831,103 @@ let getNextQuery = (
|
|
|
1766
1831
|
1,
|
|
1767
1832
|
(rawEst *. chunkItemsMultiplier)->Math.ceil->Float.toInt,
|
|
1768
1833
|
)
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
budgetLeft := false
|
|
1785
|
-
}
|
|
1834
|
+
candidates
|
|
1835
|
+
->Array.push({
|
|
1836
|
+
partitionId: fs.partitionId,
|
|
1837
|
+
fromBlock: chunkFromBlock.contents,
|
|
1838
|
+
toBlock: Some(chunkToBlock),
|
|
1839
|
+
isChunk: true,
|
|
1840
|
+
selection: p.selection,
|
|
1841
|
+
itemsTarget,
|
|
1842
|
+
itemsEst,
|
|
1843
|
+
addressesByContractName: p.addressesByContractName,
|
|
1844
|
+
})
|
|
1845
|
+
->ignore
|
|
1846
|
+
generatedItems := generatedItems.contents +. itemsEst->Int.toFloat
|
|
1847
|
+
chunkFromBlock := chunkToBlock + 1
|
|
1848
|
+
created := created.contents + 1
|
|
1786
1849
|
}
|
|
1787
|
-
fs.cursor = chunkFromBlock.contents
|
|
1788
|
-
fs.chunksUsedThisCall = fs.chunksUsedThisCall + created.contents
|
|
1789
|
-
consumed.contents
|
|
1790
1850
|
| _ =>
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1851
|
+
// Size the probe by the events its range to the target is expected to
|
|
1852
|
+
// hold — rangeTargetDensity × (chainTargetBlock − fromBlock + 1), split
|
|
1853
|
+
// across the partitions fetching this tick. With no range to the target
|
|
1854
|
+
// fall back to an even share of the fresh budget, so cold chains and
|
|
1855
|
+
// caught-up partitions still probe.
|
|
1856
|
+
let itemsTarget = if rangeToTarget > 0 {
|
|
1857
|
+
Pervasives.max(
|
|
1858
|
+
1,
|
|
1859
|
+
Math.round(
|
|
1860
|
+
rangeTargetDensity *.
|
|
1861
|
+
(chainTargetBlock - fs.cursor + 1)->Int.toFloat /.
|
|
1862
|
+
inRangeCount->Int.toFloat,
|
|
1863
|
+
)->Float.toInt,
|
|
1864
|
+
)
|
|
1865
|
+
} else {
|
|
1866
|
+
Pervasives.max(1, Math.round(probeShare)->Float.toInt)
|
|
1867
|
+
}
|
|
1868
|
+
candidates
|
|
1869
|
+
->Array.push({
|
|
1794
1870
|
partitionId: fs.partitionId,
|
|
1795
1871
|
fromBlock: fs.cursor,
|
|
1796
1872
|
toBlock: fs.queryEndBlock,
|
|
1797
1873
|
isChunk: false,
|
|
1798
1874
|
selection: p.selection,
|
|
1799
1875
|
itemsTarget,
|
|
1800
|
-
itemsEst,
|
|
1876
|
+
itemsEst: itemsTarget,
|
|
1801
1877
|
addressesByContractName: p.addressesByContractName,
|
|
1802
1878
|
})
|
|
1803
|
-
|
|
1804
|
-
fs.chunksUsedThisCall = fs.chunksUsedThisCall + 1
|
|
1805
|
-
itemsEst->Int.toFloat
|
|
1879
|
+
->ignore
|
|
1806
1880
|
}
|
|
1807
|
-
}
|
|
1881
|
+
})
|
|
1808
1882
|
|
|
1809
|
-
//
|
|
1810
|
-
//
|
|
1811
|
-
//
|
|
1812
|
-
//
|
|
1813
|
-
//
|
|
1814
|
-
//
|
|
1815
|
-
//
|
|
1816
|
-
//
|
|
1817
|
-
//
|
|
1818
|
-
//
|
|
1819
|
-
//
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
if fs->isInRange {
|
|
1841
|
-
next->Array.push(fs)->ignore
|
|
1842
|
-
}
|
|
1883
|
+
// Acceptance: merge fresh candidates (Some) with the in-flight reservations
|
|
1884
|
+
// (None) and walk them in fromBlock order, starting from the full
|
|
1885
|
+
// chainTargetItems. A reservation just draws down the budget — its query is
|
|
1886
|
+
// already sent — while a candidate draws down the budget and is emitted.
|
|
1887
|
+
// Because a gap-fill's fromBlock precedes the in-flight query it unblocks,
|
|
1888
|
+
// it claims budget ahead of that reservation, so the buffer never deadlocks
|
|
1889
|
+
// waiting on a hole it can't fund. The candidate that tips the budget
|
|
1890
|
+
// negative is still emitted (a single overshoot); everything after it waits
|
|
1891
|
+
// for a tick with more budget. Accepted queries route back to their
|
|
1892
|
+
// partition bucket, so the output stays in idsInAscOrder with each
|
|
1893
|
+
// partition's queries in fromBlock order.
|
|
1894
|
+
let acceptanceStream = []
|
|
1895
|
+
candidates->Array.forEach(query =>
|
|
1896
|
+
acceptanceStream->Array.push((query.fromBlock, query.itemsEst, Some(query)))->ignore
|
|
1897
|
+
)
|
|
1898
|
+
reservations->Array.forEach(((fromBlock, itemsEst)) =>
|
|
1899
|
+
acceptanceStream->Array.push((fromBlock, itemsEst, None))->ignore
|
|
1900
|
+
)
|
|
1901
|
+
// Sort by fromBlock; on a tie charge the in-flight reservation (None) before
|
|
1902
|
+
// a fresh candidate (Some), so a same-block candidate can't overshoot the
|
|
1903
|
+
// target buffer. Only a strictly-earlier candidate — a gap-fill, whose
|
|
1904
|
+
// fromBlock precedes the query it unblocks — borrows ahead of a reservation.
|
|
1905
|
+
acceptanceStream->Array.sort(((aFrom, _, aQuery), (bFrom, _, bQuery)) =>
|
|
1906
|
+
if aFrom !== bFrom {
|
|
1907
|
+
Int.compare(aFrom, bFrom)
|
|
1908
|
+
} else {
|
|
1909
|
+
switch (aQuery, bQuery) {
|
|
1910
|
+
| (None, Some(_)) => Ordering.less
|
|
1911
|
+
| (Some(_), None) => Ordering.greater
|
|
1912
|
+
| (None, None) | (Some(_), Some(_)) => Ordering.equal
|
|
1843
1913
|
}
|
|
1844
|
-
}
|
|
1845
|
-
|
|
1914
|
+
}
|
|
1915
|
+
)
|
|
1916
|
+
let streamCount = acceptanceStream->Array.length
|
|
1917
|
+
let remainingBudget = ref(chainTargetItems)
|
|
1918
|
+
let acceptIdx = ref(0)
|
|
1919
|
+
while remainingBudget.contents > 0. && acceptIdx.contents < streamCount {
|
|
1920
|
+
let (_, itemsEst, maybeQuery) = acceptanceStream->Array.getUnsafe(acceptIdx.contents)
|
|
1921
|
+
switch maybeQuery {
|
|
1922
|
+
| Some(query) =>
|
|
1923
|
+
let partitionIdx = partitionIndexById->Dict.getUnsafe(query.partitionId)
|
|
1924
|
+
queriesByPartitionIndex->Array.getUnsafe(partitionIdx)->Array.push(query)->ignore
|
|
1925
|
+
| None => ()
|
|
1926
|
+
}
|
|
1927
|
+
remainingBudget := remainingBudget.contents -. itemsEst->Int.toFloat
|
|
1928
|
+
acceptIdx := acceptIdx.contents + 1
|
|
1846
1929
|
}
|
|
1847
1930
|
|
|
1848
|
-
// Each partition pushed only into its own bucket (indexed by its
|
|
1849
|
-
// idsInAscOrder position), so flattening reproduces idsInAscOrder order
|
|
1850
|
-
// directly — no sort needed even though water-fill rounds interleave
|
|
1851
|
-
// across partitions.
|
|
1852
1931
|
let queries = queriesByPartitionIndex->Array.flat
|
|
1853
1932
|
|
|
1854
1933
|
if queries->Utils.Array.isEmpty {
|
|
@@ -2189,6 +2268,8 @@ let rollback = (fetchState: t, ~indexingAddresses: IndexingAddresses.t, ~targetB
|
|
|
2189
2268
|
),
|
|
2190
2269
|
}->updateInternal(
|
|
2191
2270
|
~optimizedPartitions,
|
|
2271
|
+
// Filtering the sorted buffer keeps it sorted and deduped.
|
|
2272
|
+
~mutItemsSorted=true,
|
|
2192
2273
|
~mutItems=fetchState.buffer->Array.filter(item =>
|
|
2193
2274
|
switch item {
|
|
2194
2275
|
| Event({blockNumber})
|