envio 3.5.0-alpha.2 → 3.5.0-alpha.3

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.
@@ -270,6 +270,47 @@ module OptimizedPartitions = {
270
270
  let ascSortFn = (a, b) =>
271
271
  Int.compare(a.latestFetchedBlock.blockNumber, b.latestFetchedBlock.blockNumber)
272
272
 
273
+ // Contracts a standing address-free partition already fetches client-side.
274
+ // Addresses registered for them after that partition passed get a normal
275
+ // address-bound partition which dies once it catches up (see make), instead
276
+ // of being folded into the address-free one.
277
+ let anchoredContracts = (partitions: array<partition>) => {
278
+ let set = Utils.Set.make()
279
+ partitions->Array.forEach(p =>
280
+ switch (p.mergeBlock, p.selection.clientFilteredContracts) {
281
+ | (None, Some(contractNames)) =>
282
+ contractNames->Array.forEach(name => set->Utils.Set.add(name)->ignore)
283
+ | _ => ()
284
+ }
285
+ )
286
+ set
287
+ }
288
+
289
+ // The furthest block an address-free partition's already-dispatched queries
290
+ // can deliver without the addresses registered from now on. A settled query
291
+ // claims only what it actually fetched — the rest of its range is re-queried
292
+ // later, by then against the updated address store — while an in-flight
293
+ // bounded query claims its whole toBlock. An in-flight unbounded query has no
294
+ // ceiling at all, so nothing is safe to stop a catch-up partition at yet.
295
+ let getAnchorSafeBlock = (p: partition) => {
296
+ let safeRef = ref(Some(p.latestFetchedBlock.blockNumber))
297
+ p.mutPendingQueries->Array.forEach(pq =>
298
+ switch (safeRef.contents, pq) {
299
+ | (None, _) => ()
300
+ | (Some(safe), {fetchedBlock: Some({blockNumber})}) =>
301
+ if blockNumber > safe {
302
+ safeRef := Some(blockNumber)
303
+ }
304
+ | (Some(safe), {toBlock: Some(toBlock)}) =>
305
+ if toBlock > safe {
306
+ safeRef := Some(toBlock)
307
+ }
308
+ | (Some(_), _) => safeRef := None
309
+ }
310
+ )
311
+ safeRef.contents
312
+ }
313
+
273
314
  /**
274
315
  * Optimizes partitions by finding opportunities to merge partitions that
275
316
  * are behind other partitions with same/superset of contract names.
@@ -288,19 +329,49 @@ module OptimizedPartitions = {
288
329
  let mergingPartitions = Dict.make()
289
330
  let nextPartitionIndexRef = ref(nextPartitionIndex)
290
331
 
332
+ // Where a catch-up partition for a client-filtered contract may stop, keyed
333
+ // by contract name. A contract absent from the dict while present in
334
+ // anchoredContractsSet has an unbounded query in flight on its address-free
335
+ // partition — its catch-up can't be bounded yet, and a later call will bound it.
336
+ let anchorSafeBlocks = Dict.make()
337
+ let anchoredContractsSet = Utils.Set.make()
338
+ if clientFilteredContracts->Utils.Set.size > 0 {
339
+ partitions->Array.forEach(p =>
340
+ switch (p.mergeBlock, p.selection.clientFilteredContracts) {
341
+ | (None, Some(contractNames)) =>
342
+ let safeBlock = p->getAnchorSafeBlock
343
+ contractNames->Array.forEach(name => {
344
+ anchoredContractsSet->Utils.Set.add(name)->ignore
345
+ switch safeBlock {
346
+ | Some(safeBlock) => anchorSafeBlocks->Dict.set(name, safeBlock)
347
+ | None => ()
348
+ }
349
+ })
350
+ | _ => ()
351
+ }
352
+ )
353
+ }
354
+
291
355
  for idx in 0 to partitions->Array.length - 1 {
292
356
  let p = partitions->Array.getUnsafe(idx)
293
357
  switch p {
358
+ // For now don't merge partitions with mergeBlock,
359
+ // assuming they are already merged,
360
+ // TODO: Although there might be cases with too far away mergeBlock,
361
+ // which is worth merging.
362
+ // A partition already at or past its merge block is done — normally
363
+ // handleQueryResponse removes it when the response lands, but a rollback
364
+ // can cap the merge block at the rolled-back frontier, leaving no
365
+ // response to ever remove it on.
366
+ | {mergeBlock: Some(mergeBlock)} =>
367
+ if p.latestFetchedBlock.blockNumber < mergeBlock {
368
+ newPartitions->Array.push(p)->ignore
369
+ }
294
370
  // Since it's not a dynamic contract partition,
295
371
  // there's no need for merge logic
296
372
  | {dynamicContract: None}
297
373
  | // Wildcard doesn't need merging
298
- {selection: {dependsOnAddresses: false}}
299
- | // For now don't merge partitions with mergeBlock,
300
- // assuming they are already merged,
301
- // TODO: Although there might be cases with too far away mergeBlock,
302
- // which is worth merging
303
- {mergeBlock: Some(_)} =>
374
+ {selection: {dependsOnAddresses: false}} =>
304
375
  newPartitions->Array.push(p)->ignore
305
376
  | {dynamicContract: Some(contractName)} =>
306
377
  let pAddressesCount = p.addresses->AddressSet.countFor(contractName)
@@ -398,14 +469,40 @@ module OptimizedPartitions = {
398
469
  newPartitions->Array.push(currentPRef.contents)->ignore
399
470
  }
400
471
 
472
+ // A dynamic partition for a contract the address-free partition already
473
+ // fetches is a catch-up for addresses registered after that partition
474
+ // passed them. It merges by disappearing once it reaches the last block
475
+ // that partition's dispatched queries could have missed them on — its
476
+ // addresses are in the chain's store, so there is nothing to hand over.
477
+ let finalPartitions = if anchoredContractsSet->Utils.Set.size === 0 {
478
+ newPartitions
479
+ } else {
480
+ let anchored = []
481
+ newPartitions->Array.forEach(p =>
482
+ switch p {
483
+ | {dynamicContract: Some(contractName), mergeBlock: None}
484
+ if anchoredContractsSet->Utils.Set.has(contractName) =>
485
+ switch anchorSafeBlocks->Utils.Dict.dangerouslyGetNonOption(contractName) {
486
+ | None => anchored->Array.push(p)->ignore
487
+ | Some(anchorSafeBlock) =>
488
+ if p.latestFetchedBlock.blockNumber < anchorSafeBlock {
489
+ anchored->Array.push({...p, mergeBlock: Some(anchorSafeBlock)})->ignore
490
+ }
491
+ }
492
+ | _ => anchored->Array.push(p)->ignore
493
+ }
494
+ )
495
+ anchored
496
+ }
497
+
401
498
  // Sort partitions by latestFetchedBlock ascending
402
- let _ = newPartitions->Array.sort(ascSortFn)
499
+ let _ = finalPartitions->Array.sort(ascSortFn)
403
500
 
404
- let partitionsCount = newPartitions->Array.length
501
+ let partitionsCount = finalPartitions->Array.length
405
502
  let idsInAscOrder = Utils.Array.jsArrayCreate(partitionsCount)
406
503
  let entities = Dict.make()
407
504
  for idx in 0 to partitionsCount - 1 {
408
- let p = newPartitions->Array.getUnsafe(idx)
505
+ let p = finalPartitions->Array.getUnsafe(idx)
409
506
  idsInAscOrder->Array.setUnsafe(idx, p.id)
410
507
  entities->Dict.set(p.id, p)
411
508
  }
@@ -951,18 +1048,24 @@ let addClientFilteredContract = (
951
1048
  ~chainId,
952
1049
  ~addressCount,
953
1050
  ~threshold,
1051
+ // A resumed fetch state re-derives the switch from the persisted addresses,
1052
+ // but it already happened - and was logged - in the run that crossed the
1053
+ // threshold.
1054
+ ~shouldLog=true,
954
1055
  ) => {
955
1056
  clientFilteredContracts->Utils.Set.add(contractName)->ignore
956
- Logging.createChild(
957
- ~params={
958
- "chainId": chainId,
959
- "contractName": contractName,
960
- "addressCount": addressCount,
961
- "threshold": threshold,
962
- },
963
- )->Logging.childTrace(
964
- "Switching contract to client-side address filtering: registered address count crossed the server-side threshold.",
965
- )
1057
+ if shouldLog {
1058
+ Logging.createChild(
1059
+ ~params={
1060
+ "chainId": chainId,
1061
+ "contractName": contractName,
1062
+ "addressCount": addressCount,
1063
+ "threshold": threshold,
1064
+ },
1065
+ )->Logging.childTrace(
1066
+ "Switching contract to client-side address filtering: registered address count crossed the server-side threshold.",
1067
+ )
1068
+ }
966
1069
  }
967
1070
 
968
1071
  // The block a partition will have fetched to once everything on its pending
@@ -998,6 +1101,10 @@ let claimedFetchedBlock = (p: partition, ~knownHeight) =>
998
1101
  // it on arrival. The overlap it re-delivers is deduped by mergeIntoBuffer, and
999
1102
  // the re-fetch doubles as history for freshly registered addresses: events
1000
1103
  // dropped before the address was registered now pass the address gate.
1104
+ // - A dynamic partition for a contract the standing partition already covers is
1105
+ // left alone: it is a catch-up for addresses registered after that partition
1106
+ // passed them, and OptimizedPartitions.make bounds it against the standing
1107
+ // partition's own claims instead.
1001
1108
  // - A partition mixing client-filtered and server-side contracts stays,
1002
1109
  // stripped of the client-filtered contracts' addresses — the address-free
1003
1110
  // partition covers those logs, so fetching them server-side too would only
@@ -1014,6 +1121,10 @@ let collapseClientFilteredContracts = (
1014
1121
  ~normalSelection: selection,
1015
1122
  ~nextPartitionIndexRef: ref<int>,
1016
1123
  ~addressStore: AddressStore.t,
1124
+ // Frontiers of the addresses that were never given a server-side partition
1125
+ // because their contract is already client-filtered. They stand in for the
1126
+ // partitions that would otherwise have been created only to be absorbed here.
1127
+ ~clientFilteredFrontiers: array<blockNumberAndTimestamp>=[],
1017
1128
  ~knownHeight: int,
1018
1129
  ) => {
1019
1130
  if clientFilteredContracts->Utils.Set.size === 0 {
@@ -1024,6 +1135,7 @@ let collapseClientFilteredContracts = (
1024
1135
  let backfills = []
1025
1136
  let absorbedPartitions = []
1026
1137
  let strippedFrontiers = []
1138
+ let anchoredContracts = partitions->OptimizedPartitions.anchoredContracts
1027
1139
 
1028
1140
  partitions->Array.forEach(p =>
1029
1141
  switch p {
@@ -1039,6 +1151,17 @@ let collapseClientFilteredContracts = (
1039
1151
  contractNames->Array.filter(c => !(clientFilteredContracts->Utils.Set.has(c)))
1040
1152
  if serverSideNames->Array.length === contractNames->Array.length {
1041
1153
  kept->Array.push(p)->ignore
1154
+ } else if (
1155
+ switch p.dynamicContract {
1156
+ | Some(contractName) => anchoredContracts->Utils.Set.has(contractName)
1157
+ | None => false
1158
+ }
1159
+ ) {
1160
+ // A catch-up partition for addresses registered after the address-free
1161
+ // partition already covered their contract. It is the correctness
1162
+ // barrier for those addresses until it catches up — absorbing it would
1163
+ // hand that job back to a guessed ceiling.
1164
+ kept->Array.push(p)->ignore
1042
1165
  } else if serverSideNames->Utils.Array.isEmpty {
1043
1166
  absorbedPartitions->Array.push(p)->ignore
1044
1167
  } else {
@@ -1060,6 +1183,7 @@ let collapseClientFilteredContracts = (
1060
1183
  if (
1061
1184
  absorbedPartitions->Utils.Array.isEmpty &&
1062
1185
  strippedFrontiers->Utils.Array.isEmpty &&
1186
+ clientFilteredFrontiers->Utils.Array.isEmpty &&
1063
1187
  !selectionChanged
1064
1188
  ) {
1065
1189
  // Nothing to fold in and no newly-switched contract: leave the standing
@@ -1075,6 +1199,7 @@ let collapseClientFilteredContracts = (
1075
1199
  absorbedPartitions->Array.forEach(p => considerFrontier(p.latestFetchedBlock))
1076
1200
  backfills->Array.forEach(p => considerFrontier(p.latestFetchedBlock))
1077
1201
  strippedFrontiers->Array.forEach(considerFrontier)
1202
+ clientFilteredFrontiers->Array.forEach(considerFrontier)
1078
1203
 
1079
1204
  let regByIndex = Dict.make()
1080
1205
  let addRegs = (regs: array<Internal.onEventRegistration>) =>
@@ -1199,68 +1324,96 @@ OptimizedPartitions.t => {
1199
1324
  // ── Phase 1: Create per-contract-name partitions ──
1200
1325
  let dynamicPartitions = []
1201
1326
  let nonDynamicPartitions = []
1327
+ let clientFilteredFrontiers = []
1328
+
1329
+ // Contracts an address-free partition already fetches. Their new addresses
1330
+ // need a partition of their own: the address-free partition passed the blocks
1331
+ // they were registered at without them in the store.
1332
+ let anchoredContracts = existingPartitions->OptimizedPartitions.anchoredContracts
1202
1333
 
1203
1334
  let contractNames = registeringSetsByContract->Dict.keysToArray
1204
1335
  for cIdx in 0 to contractNames->Array.length - 1 {
1205
1336
  let contractName = contractNames->Array.getUnsafe(cIdx)
1206
1337
  let contractSet = registeringSetsByContract->Dict.getUnsafe(contractName)
1207
1338
 
1208
- let isDynamic = dynamicContracts->Utils.Set.has(contractName)
1339
+ let isAnchored = anchoredContracts->Utils.Set.has(contractName)
1340
+ // A catch-up partition must go through the dynamic merge logic — that's
1341
+ // what bounds it against its anchor and removes it once it catches up.
1342
+ let isDynamic = dynamicContracts->Utils.Set.has(contractName) || isAnchored
1209
1343
  let partitions = isDynamic ? dynamicPartitions : nonDynamicPartitions
1210
1344
 
1211
1345
  // A set is ordered by effectiveStartBlock, so its start-block groups are
1212
1346
  // ascending and each group's addresses are a contiguous slice.
1213
1347
  let groups = contractSet->AddressSet.startBlockGroups
1214
- let offsetRef = ref(0)
1215
- let groupIdx = ref(0)
1216
- while groupIdx.contents < groups->Array.length {
1217
- let startBlock = (groups->Array.getUnsafe(groupIdx.contents)).startBlock
1218
- // Addresses with different start blocks within range share a partition;
1219
- // events before each address's effectiveStartBlock are dropped by the
1220
- // source's address gate.
1221
- let countRef = ref(0)
1222
- let nextIdx = ref(groupIdx.contents)
1223
- let joining = ref(true)
1224
- while joining.contents && nextIdx.contents < groups->Array.length {
1225
- let group = groups->Array.getUnsafe(nextIdx.contents)
1226
- if group.startBlock - startBlock < OptimizedPartitions.tooFarBlockRange {
1227
- countRef := countRef.contents + group.count
1228
- nextIdx := nextIdx.contents + 1
1229
- } else {
1230
- joining := false
1231
- }
1232
- }
1233
1348
 
1234
- let latestFetchedBlock = {
1235
- blockNumber: Pervasives.max(startBlock - 1, progressBlockNumber),
1236
- blockTimestamp: 0,
1237
- }
1238
- let remainingRef = ref(countRef.contents)
1239
- let chunkOffsetRef = ref(offsetRef.contents)
1240
- while remainingRef.contents > 0 {
1241
- let take = Pervasives.min(remainingRef.contents, maxAddrInPartition)
1242
- let pAddresses =
1243
- contractSet->AddressSet.slice(~offset=chunkOffsetRef.contents, ~limit=Some(take))
1244
- partitions->Array.push({
1245
- id: nextPartitionIndexRef.contents->Int.toString,
1246
- latestFetchedBlock,
1247
- selection: normalSelection,
1248
- dynamicContract: isDynamic ? Some(contractName) : None,
1249
- addresses: pAddresses,
1250
- mergeBlock: None,
1251
- mutPendingQueries: [],
1252
- sourceRangeCapacity: 0,
1253
- prevSourceRangeCapacity: 0,
1254
- eventDensity: None,
1255
- latestSourceRangeCapacityUpdateBlock: 0,
1349
+ if clientFilteredContracts->Utils.Set.has(contractName) && !isAnchored {
1350
+ // The contract is switching to client-side filtering in this very call, so
1351
+ // the collapse below folds everything it has into the address-free
1352
+ // partition anyway. Chunking these addresses by maxAddrInPartition would
1353
+ // only build partitions for it to absorb - hundreds of them for a contract
1354
+ // big enough to be client-filtered in the first place. All the collapse
1355
+ // needs is the earliest block the addresses aren't covered from, which is
1356
+ // the first group's since groups are ascending.
1357
+ switch groups->Array.get(0) {
1358
+ | Some({startBlock}) =>
1359
+ clientFilteredFrontiers->Array.push({
1360
+ blockNumber: Pervasives.max(startBlock - 1, progressBlockNumber),
1361
+ blockTimestamp: 0,
1256
1362
  })
1257
- nextPartitionIndexRef := nextPartitionIndexRef.contents + 1
1258
- chunkOffsetRef := chunkOffsetRef.contents + take
1259
- remainingRef := remainingRef.contents - take
1363
+ | None => ()
1260
1364
  }
1365
+ } else {
1366
+ let offsetRef = ref(0)
1367
+ let groupIdx = ref(0)
1368
+ while groupIdx.contents < groups->Array.length {
1369
+ let startBlock = (groups->Array.getUnsafe(groupIdx.contents)).startBlock
1370
+ // Addresses with different start blocks within range share a partition;
1371
+ // events before each address's effectiveStartBlock are dropped by the
1372
+ // source's address gate.
1373
+ let countRef = ref(0)
1374
+ let nextIdx = ref(groupIdx.contents)
1375
+ let joining = ref(true)
1376
+ while joining.contents && nextIdx.contents < groups->Array.length {
1377
+ let group = groups->Array.getUnsafe(nextIdx.contents)
1378
+ if group.startBlock - startBlock < OptimizedPartitions.tooFarBlockRange {
1379
+ countRef := countRef.contents + group.count
1380
+ nextIdx := nextIdx.contents + 1
1381
+ } else {
1382
+ joining := false
1383
+ }
1384
+ }
1385
+
1386
+ let latestFetchedBlock = {
1387
+ blockNumber: Pervasives.max(startBlock - 1, progressBlockNumber),
1388
+ blockTimestamp: 0,
1389
+ }
1390
+ let remainingRef = ref(countRef.contents)
1391
+ let chunkOffsetRef = ref(offsetRef.contents)
1392
+ while remainingRef.contents > 0 {
1393
+ let take = Pervasives.min(remainingRef.contents, maxAddrInPartition)
1394
+ let pAddresses =
1395
+ contractSet->AddressSet.slice(~offset=chunkOffsetRef.contents, ~limit=Some(take))
1396
+ partitions->Array.push({
1397
+ id: nextPartitionIndexRef.contents->Int.toString,
1398
+ latestFetchedBlock,
1399
+ selection: normalSelection,
1400
+ dynamicContract: isDynamic ? Some(contractName) : None,
1401
+ addresses: pAddresses,
1402
+ mergeBlock: None,
1403
+ mutPendingQueries: [],
1404
+ sourceRangeCapacity: 0,
1405
+ prevSourceRangeCapacity: 0,
1406
+ eventDensity: None,
1407
+ latestSourceRangeCapacityUpdateBlock: 0,
1408
+ })
1409
+ nextPartitionIndexRef := nextPartitionIndexRef.contents + 1
1410
+ chunkOffsetRef := chunkOffsetRef.contents + take
1411
+ remainingRef := remainingRef.contents - take
1412
+ }
1261
1413
 
1262
- offsetRef := offsetRef.contents + countRef.contents
1263
- groupIdx := nextIdx.contents
1414
+ offsetRef := offsetRef.contents + countRef.contents
1415
+ groupIdx := nextIdx.contents
1416
+ }
1264
1417
  }
1265
1418
  }
1266
1419
 
@@ -1324,6 +1477,7 @@ OptimizedPartitions.t => {
1324
1477
  ~normalSelection,
1325
1478
  ~nextPartitionIndexRef,
1326
1479
  ~addressStore,
1480
+ ~clientFilteredFrontiers,
1327
1481
  ~knownHeight,
1328
1482
  )
1329
1483
  OptimizedPartitions.make(
@@ -2343,6 +2497,7 @@ let make = (
2343
2497
  ~blockLag=0,
2344
2498
  ~firstEventBlock=None,
2345
2499
  ~clientFilterAddressThreshold=None,
2500
+ ~isResumed=false,
2346
2501
  ): t => {
2347
2502
  let latestFetchedBlock = {
2348
2503
  blockTimestamp: 0,
@@ -2438,6 +2593,7 @@ let make = (
2438
2593
  ~chainId,
2439
2594
  ~addressCount,
2440
2595
  ~threshold,
2596
+ ~shouldLog=!isResumed,
2441
2597
  )
2442
2598
  }
2443
2599
  })
@@ -2586,6 +2742,14 @@ let rollback = (fetchState: t, ~addressStore: AddressStore.t, ~targetBlockNumber
2586
2742
  latestFetchedBlock: p.latestFetchedBlock.blockNumber > targetBlockNumber
2587
2743
  ? {blockNumber: targetBlockNumber, blockTimestamp: 0}
2588
2744
  : p.latestFetchedBlock,
2745
+ // Everything above the target is refetched by whichever partition this
2746
+ // one was catching up to, so there is nothing left to catch up on past
2747
+ // it. createPartitions drops the partition outright when the capped
2748
+ // block is already reached.
2749
+ mergeBlock: switch p.mergeBlock {
2750
+ | Some(mergeBlock) if mergeBlock > targetBlockNumber => Some(targetBlockNumber)
2751
+ | other => other
2752
+ },
2589
2753
  mutPendingQueries: rollbackPendingQueries(p.mutPendingQueries, ~targetBlockNumber),
2590
2754
  })
2591
2755
  ->ignore