envio 3.15.0 → 3.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +40 -34
  2. package/evm.schema.json +11 -2
  3. package/fuel.schema.json +10 -1
  4. package/index.d.ts +6 -0
  5. package/licenses/README.md +1 -1
  6. package/package.json +8 -8
  7. package/src/Batch.res.mjs +5 -5
  8. package/src/BatchProcessing.res +1 -0
  9. package/src/BatchProcessing.res.mjs +1 -0
  10. package/src/ChainState.res +48 -7
  11. package/src/ChainState.res.mjs +27 -11
  12. package/src/Clock.res +57 -0
  13. package/src/Clock.res.mjs +72 -0
  14. package/src/Config.res +10 -3
  15. package/src/Config.res.mjs +4 -2
  16. package/src/Core.res +4 -40
  17. package/src/EffectState.res +10 -2
  18. package/src/EffectState.res.mjs +8 -4
  19. package/src/EffectState.resi +3 -1
  20. package/src/EntityId.res +2 -7
  21. package/src/EntityId.res.mjs +1 -8
  22. package/src/EntityTables.res +4 -1
  23. package/src/EntityTables.res.mjs +1 -1
  24. package/src/FetchState.res +157 -69
  25. package/src/FetchState.res.mjs +144 -49
  26. package/src/FinalizeBackfill.res +2 -4
  27. package/src/FinalizeBackfill.res.mjs +3 -4
  28. package/src/InMemoryTable.res +17 -16
  29. package/src/InMemoryTable.res.mjs +14 -15
  30. package/src/IndexerState.res +28 -4
  31. package/src/IndexerState.res.mjs +19 -6
  32. package/src/IndexerState.resi +4 -0
  33. package/src/Internal.res +0 -49
  34. package/src/Internal.res.mjs +1 -80
  35. package/src/LoadLayer.res +14 -8
  36. package/src/LoadLayer.res.mjs +8 -6
  37. package/src/Main.res.mjs +1 -1
  38. package/src/Persistence.res +3 -23
  39. package/src/Persistence.res.mjs +3 -3
  40. package/src/Rollback.res +0 -1
  41. package/src/Rollback.res.mjs +1 -1
  42. package/src/SafeCheckpointTracking.res +4 -4
  43. package/src/SafeCheckpointTracking.res.mjs +1 -1
  44. package/src/StagedRows.res +106 -0
  45. package/src/StagedRows.res.mjs +102 -0
  46. package/src/Staging.res +32 -14
  47. package/src/Staging.res.mjs +17 -20
  48. package/src/Storage.res +277 -948
  49. package/src/Storage.res.mjs +215 -707
  50. package/src/Supervisor.res +75 -56
  51. package/src/Supervisor.res.mjs +55 -26
  52. package/src/TestIndexer.res +22 -27
  53. package/src/TestIndexer.res.mjs +20 -17
  54. package/src/Throttler.res +15 -8
  55. package/src/Throttler.res.mjs +9 -6
  56. package/src/Throttler.resi +1 -1
  57. package/src/Utils.res +0 -6
  58. package/src/Utils.res.mjs +0 -5
  59. package/src/Writing.res +0 -1
  60. package/src/Writing.res.mjs +1 -1
  61. package/src/bindings/ClickHouse.res +0 -322
  62. package/src/bindings/ClickHouse.res.mjs +0 -338
  63. package/src/bindings/StorageClient.res +107 -174
  64. package/src/bindings/StorageClient.res.mjs +19 -8
  65. package/src/bindings/Vitest.res +0 -26
  66. package/src/bindings/Vitest.res.mjs +0 -3
  67. package/src/db/Table.res +24 -150
  68. package/src/db/Table.res.mjs +38 -272
  69. package/src/sources/ChainSources.res +2 -0
  70. package/src/sources/ChainSources.res.mjs +2 -2
  71. package/src/sources/EvmChain.res +3 -0
  72. package/src/sources/EvmChain.res.mjs +4 -2
  73. package/src/sources/EvmHyperSyncSource.res +2 -2
  74. package/src/sources/EvmHyperSyncSource.res.mjs +1 -1
  75. package/src/sources/EvmRpcClient.res +13 -3
  76. package/src/sources/EvmRpcClient.res.mjs +4 -3
  77. package/src/sources/EvmRpcWs.res +4 -3
  78. package/src/sources/EvmRpcWs.res.mjs +2 -2
  79. package/src/sources/HeightFeed.res +31 -10
  80. package/src/sources/HeightFeed.res.mjs +10 -6
  81. package/src/sources/HeightStream.res +32 -17
  82. package/src/sources/HeightStream.res.mjs +10 -10
  83. package/src/sources/HyperSyncSSE.res +4 -6
  84. package/src/sources/HyperSyncSSE.res.mjs +2 -2
  85. package/src/sources/RpcSource.res +10 -1
  86. package/src/sources/RpcSource.res.mjs +4 -3
  87. package/src/sources/Source.res +3 -0
  88. package/src/sources/SourceManager.res +84 -77
  89. package/src/sources/SourceManager.res.mjs +17 -20
  90. package/src/sources/SourceManager.resi +3 -4
  91. package/src/sources/StartBlockResolver.res +1 -0
  92. package/src/sources/StartBlockResolver.res.mjs +3 -2
  93. package/src/sources/SvmHyperSyncSource.res +2 -2
  94. package/src/sources/SvmHyperSyncSource.res.mjs +1 -1
  95. package/svm.schema.json +11 -2
  96. package/src/PgWriting.res +0 -36
  97. package/src/PgWriting.res.mjs +0 -34
  98. package/src/db/IndexDefinition.res +0 -47
  99. package/src/db/IndexDefinition.res.mjs +0 -67
@@ -374,74 +374,93 @@ let releaseIfAtHead = group =>
374
374
  )
375
375
  }
376
376
 
377
- // The run's chains as an unsplit indexer reports them before it has fetched
378
- // anything: at their configured blocks, with nothing indexed. A display draws
379
- // these for a worker it hasn't heard from yet.
380
- let configuredChains = (config: Config.t): array<Metrics.chainMetrics> =>
377
+ // The run's chains as a worker reports them before it has fetched anything:
378
+ // where `ChainState.makeFromDbState` resumes them. A display draws these for a
379
+ // worker it hasn't heard from yet, and its first sight of a chain is what it
380
+ // measures the sync pace from, so a resumed chain drawn from its start block
381
+ // would pass the earlier runs' progress off as this one's.
382
+ let resumedChains = (config: Config.t, ~resumed: array<Persistence.initialChainState>): array<
383
+ Metrics.chainMetrics,
384
+ > =>
381
385
  config.chainMap
382
386
  ->ChainMap.values
383
387
  ->Array.map((chain): Metrics.chainMetrics => {
384
- chainId: chain.id,
385
- poweredByHyperSync: switch chain.sourceConfig {
386
- | EvmSourceConfig({hypersync}) => hypersync->Option.isSome
387
- | FuelSourceConfig(_) | SvmSourceConfig(_) => true
388
- | SimulateSourceConfig(_) | CustomSources(_) => false
389
- },
390
- firstEventBlockNumber: None,
391
- latestProcessedBlock: None,
392
- timestampCaughtUpToHeadOrEndblock: None,
393
- numEventsProcessed: 0.,
394
- latestFetchedBlockNumber: 0,
395
- knownHeight: 0,
396
- numBatchesFetched: 0,
397
- // A chain resolves `start_block: latest` against its own head as it starts,
398
- // which is a worker's to do and no supervisor's to guess.
399
- startBlock: switch chain.startBlock {
400
- | Block(block) => block
401
- | Latest => 0
402
- },
403
- endBlock: chain.endBlock,
404
- numAddresses: 0,
405
- addressesByContract: [],
406
- isReady: false,
407
- sourceBlockNumber: 0,
408
- progressBlockNumber: -1,
409
- progressLatencyMs: None,
410
- progressBlockTime: None,
411
- concurrency: 0,
412
- partitionsCount: 0,
413
- bufferSize: 0,
414
- bufferBlockNumber: -1,
415
- idleSeconds: 0.,
416
- waitingForNewBlockSeconds: 0.,
417
- queryingSeconds: 0.,
418
- blockRangeFetchSeconds: 0.,
419
- blockRangeParseSeconds: 0.,
420
- blockRangeFetchCount: 0.,
421
- blockRangeFetchedEvents: 0.,
422
- blockRangeFetchedBlocks: 0.,
423
- reorgCount: 0,
424
- reorgDetectedBlock: None,
425
- rollbackTargetBlock: None,
426
- rateLimitTimeMs: 0.,
427
- rateLimitResetInMs: None,
388
+ let row = resumed->Array.find(row => row.id == chain.id)
389
+ let startBlock = row->Option.mapOr(chain->Config.startBlockOrZero, row => row.startBlock)
390
+ let endBlock = row->Option.mapOr(chain.endBlock, row => row.endBlock)
391
+ let progressBlockNumber = switch row {
392
+ | Some({progressBlockNumber}) if progressBlockNumber >= 0 => progressBlockNumber
393
+ | _ => startBlock - 1
394
+ }
395
+ let sourceBlockNumber = row->Option.mapOr(0, row => row.sourceBlockNumber)
396
+ let timestampCaughtUpToHeadOrEndblock =
397
+ row->Option.flatMap(row => row.timestampCaughtUpToHeadOrEndblock)
398
+ let metrics: Metrics.chainMetrics = {
399
+ chainId: chain.id,
400
+ poweredByHyperSync: switch chain.sourceConfig {
401
+ | EvmSourceConfig({hypersync}) => hypersync->Option.isSome
402
+ | FuelSourceConfig(_) | SvmSourceConfig(_) => true
403
+ | SimulateSourceConfig(_) | CustomSources(_) => false
404
+ },
405
+ firstEventBlockNumber: row->Option.flatMap(row => row.firstEventBlockNumber),
406
+ latestProcessedBlock: progressBlockNumber === -1 ? None : Some(progressBlockNumber),
407
+ timestampCaughtUpToHeadOrEndblock,
408
+ numEventsProcessed: row->Option.mapOr(0., row => row.numEventsProcessed),
409
+ latestFetchedBlockNumber: Pervasives.max(progressBlockNumber, 0),
410
+ knownHeight: sourceBlockNumber,
411
+ numBatchesFetched: 0,
412
+ startBlock,
413
+ endBlock,
414
+ numAddresses: 0,
415
+ addressesByContract: [],
416
+ isReady: timestampCaughtUpToHeadOrEndblock->Option.isSome,
417
+ sourceBlockNumber,
418
+ progressBlockNumber,
419
+ progressLatencyMs: None,
420
+ progressBlockTime: row->Option.flatMap(row => row.progressBlockTime),
421
+ concurrency: 0,
422
+ partitionsCount: 0,
423
+ bufferSize: 0,
424
+ bufferBlockNumber: progressBlockNumber,
425
+ idleSeconds: 0.,
426
+ waitingForNewBlockSeconds: 0.,
427
+ queryingSeconds: 0.,
428
+ blockRangeFetchSeconds: 0.,
429
+ blockRangeParseSeconds: 0.,
430
+ blockRangeFetchCount: 0.,
431
+ blockRangeFetchedEvents: 0.,
432
+ blockRangeFetchedBlocks: 0.,
433
+ reorgCount: 0,
434
+ reorgDetectedBlock: None,
435
+ rollbackTargetBlock: None,
436
+ rateLimitTimeMs: 0.,
437
+ rateLimitResetInMs: None,
438
+ }
439
+ switch (metrics->Metrics.hasProcessedToEndblock, endBlock) {
440
+ | (true, Some(endBlock)) => {...metrics, knownHeight: endBlock}
441
+ | _ => metrics
442
+ }
428
443
  })
429
444
 
430
445
  // Per worker rather than all or nothing: a worker done within its first report
431
446
  // interval is heard from only as it exits, and a display holding just its
432
447
  // siblings' finished chains would read as synced while it still indexes.
433
- let displayedSnapshots = (running: array<running>, ~config, ~empty: Metrics.t) => {
434
- let configured = configuredChains(config)
448
+ let displayedSnapshots = (
449
+ running: array<running>,
450
+ ~resumedChains: array<Metrics.chainMetrics>,
451
+ ~empty: Metrics.t,
452
+ ) =>
435
453
  running->Array.map(r =>
436
454
  switch r.snapshot {
437
455
  | Some(snapshot) => snapshot
438
456
  | None => {
439
457
  ...empty,
440
- chains: configured->Array.filter(chain => r.worker.chainIds->Array.includes(chain.chainId)),
458
+ chains: resumedChains->Array.filter(chain =>
459
+ r.worker.chainIds->Array.includes(chain.chainId)
460
+ ),
441
461
  }
442
462
  }
443
463
  )
444
- }
445
464
 
446
465
  // Runs the group: creates the schema for every chain, forks a worker per plan
447
466
  // entry, and serves the run's metrics, console and display from what they
@@ -481,10 +500,9 @@ let run = async (~config: Config.t, ~logger, ~workers: array<worker>, ~reset) =>
481
500
  let shouldUseTui = Tui.shouldUse()
482
501
  // A run that resumed with every chain already caught up owes nobody a wait:
483
502
  // its workers start realtime and there is no barrier to open.
503
+ let resumed = (persistence->Persistence.getInitializedState).chains
484
504
  let holdRealtime =
485
- (persistence->Persistence.getInitializedState).chains->Array.some(chain =>
486
- chain.timestampCaughtUpToHeadOrEndblock->Option.isNone
487
- )
505
+ resumed->Array.some(chain => chain.timestampCaughtUpToHeadOrEndblock->Option.isNone)
488
506
  let group = {running: [], stopping: false, holdingRealtime: holdRealtime}
489
507
  group.running =
490
508
  workers->Array.mapWithIndex((worker, workerIndex) =>
@@ -534,8 +552,9 @@ let run = async (~config: Config.t, ~logger, ~workers: array<worker>, ~reset) =>
534
552
  )
535
553
 
536
554
  if shouldUseTui {
555
+ let resumedChains = resumedChains(config, ~resumed)
537
556
  Tui.start(~config, ~logger, ~getMetrics=() =>
538
- group.running->displayedSnapshots(~config, ~empty=[]->merge)->merge
557
+ group.running->displayedSnapshots(~resumedChains, ~empty=[]->merge)->merge
539
558
  )
540
559
  }
541
560
 
@@ -22,6 +22,7 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
22
22
  import * as Child_process from "child_process";
23
23
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
24
24
  import * as CrossChainState from "./CrossChainState.res.mjs";
25
+ import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
25
26
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
26
27
  import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
27
28
 
@@ -313,8 +314,20 @@ function releaseIfAtHead(group) {
313
314
  }
314
315
  }
315
316
 
316
- function configuredChains(config) {
317
+ function resumedChains(config, resumed) {
317
318
  return ChainMap.values(config.chainMap).map(chain => {
319
+ let row = resumed.find(row => Primitive_object.equal(row.id, chain.id));
320
+ let startBlock = Stdlib_Option.mapOr(row, Config.startBlockOrZero(chain), row => row.startBlock);
321
+ let endBlock = Stdlib_Option.mapOr(row, chain.endBlock, row => row.endBlock);
322
+ let progressBlockNumber;
323
+ if (row !== undefined) {
324
+ let progressBlockNumber$1 = row.progressBlockNumber;
325
+ progressBlockNumber = progressBlockNumber$1 >= 0 ? progressBlockNumber$1 : startBlock - 1 | 0;
326
+ } else {
327
+ progressBlockNumber = startBlock - 1 | 0;
328
+ }
329
+ let sourceBlockNumber = Stdlib_Option.mapOr(row, 0, row => row.sourceBlockNumber);
330
+ let timestampCaughtUpToHeadOrEndblock = Stdlib_Option.flatMap(row, row => row.timestampCaughtUpToHeadOrEndblock);
318
331
  let match = chain.sourceConfig;
319
332
  let tmp;
320
333
  switch (match.TAG) {
@@ -330,32 +343,37 @@ function configuredChains(config) {
330
343
  tmp = false;
331
344
  break;
332
345
  }
333
- let block = chain.startBlock;
334
- let tmp$1;
335
- tmp$1 = block === "latest" ? 0 : block;
336
- return {
337
- chainId: chain.id,
346
+ let metrics_chainId = chain.id;
347
+ let metrics_firstEventBlockNumber = Stdlib_Option.flatMap(row, row => row.firstEventBlockNumber);
348
+ let metrics_latestProcessedBlock = progressBlockNumber === -1 ? undefined : progressBlockNumber;
349
+ let metrics_numEventsProcessed = Stdlib_Option.mapOr(row, 0, row => row.numEventsProcessed);
350
+ let metrics_latestFetchedBlockNumber = Primitive_int.max(progressBlockNumber, 0);
351
+ let metrics_addressesByContract = [];
352
+ let metrics_isReady = Stdlib_Option.isSome(timestampCaughtUpToHeadOrEndblock);
353
+ let metrics_progressBlockTime = Stdlib_Option.flatMap(row, row => row.progressBlockTime);
354
+ let metrics = {
355
+ chainId: metrics_chainId,
338
356
  poweredByHyperSync: tmp,
339
- firstEventBlockNumber: undefined,
340
- latestProcessedBlock: undefined,
341
- timestampCaughtUpToHeadOrEndblock: undefined,
342
- numEventsProcessed: 0,
343
- latestFetchedBlockNumber: 0,
344
- knownHeight: 0,
357
+ firstEventBlockNumber: metrics_firstEventBlockNumber,
358
+ latestProcessedBlock: metrics_latestProcessedBlock,
359
+ timestampCaughtUpToHeadOrEndblock: timestampCaughtUpToHeadOrEndblock,
360
+ numEventsProcessed: metrics_numEventsProcessed,
361
+ latestFetchedBlockNumber: metrics_latestFetchedBlockNumber,
362
+ knownHeight: sourceBlockNumber,
345
363
  numBatchesFetched: 0,
346
- startBlock: tmp$1,
347
- endBlock: chain.endBlock,
364
+ startBlock: startBlock,
365
+ endBlock: endBlock,
348
366
  numAddresses: 0,
349
- addressesByContract: [],
350
- isReady: false,
351
- sourceBlockNumber: 0,
352
- progressBlockNumber: -1,
367
+ addressesByContract: metrics_addressesByContract,
368
+ isReady: metrics_isReady,
369
+ sourceBlockNumber: sourceBlockNumber,
370
+ progressBlockNumber: progressBlockNumber,
353
371
  progressLatencyMs: undefined,
354
- progressBlockTime: undefined,
372
+ progressBlockTime: metrics_progressBlockTime,
355
373
  concurrency: 0,
356
374
  partitionsCount: 0,
357
375
  bufferSize: 0,
358
- bufferBlockNumber: -1,
376
+ bufferBlockNumber: progressBlockNumber,
359
377
  idleSeconds: 0,
360
378
  waitingForNewBlockSeconds: 0,
361
379
  queryingSeconds: 0,
@@ -370,18 +388,27 @@ function configuredChains(config) {
370
388
  rateLimitTimeMs: 0,
371
389
  rateLimitResetInMs: undefined
372
390
  };
391
+ let match$1 = Metrics.hasProcessedToEndblock(metrics);
392
+ if (!match$1) {
393
+ return metrics;
394
+ }
395
+ if (endBlock === undefined) {
396
+ return metrics;
397
+ }
398
+ let newrecord = {...metrics};
399
+ newrecord.knownHeight = endBlock;
400
+ return newrecord;
373
401
  });
374
402
  }
375
403
 
376
- function displayedSnapshots(running, config, empty) {
377
- let configured = configuredChains(config);
404
+ function displayedSnapshots(running, resumedChains, empty) {
378
405
  return running.map(r => {
379
406
  let snapshot = r.snapshot;
380
407
  if (snapshot !== undefined) {
381
408
  return snapshot;
382
409
  }
383
410
  let newrecord = {...empty};
384
- newrecord.chains = configured.filter(chain => r.worker.chainIds.includes(chain.chainId));
411
+ newrecord.chains = resumedChains.filter(chain => r.worker.chainIds.includes(chain.chainId));
385
412
  return newrecord;
386
413
  });
387
414
  }
@@ -402,7 +429,8 @@ async function run(config, logger, workers, reset) {
402
429
  maxConnections: Env.Db.maxConnections
403
430
  });
404
431
  let shouldUseTui = Tui.shouldUse(undefined, undefined);
405
- let holdRealtime = Persistence.getInitializedState(persistence).chains.some(chain => Stdlib_Option.isNone(chain.timestampCaughtUpToHeadOrEndblock));
432
+ let resumed = Persistence.getInitializedState(persistence).chains;
433
+ let holdRealtime = resumed.some(chain => Stdlib_Option.isNone(chain.timestampCaughtUpToHeadOrEndblock));
406
434
  let group = {
407
435
  running: [],
408
436
  stopping: false,
@@ -421,7 +449,8 @@ async function run(config, logger, workers, reset) {
421
449
  runtime
422
450
  ]))), config.isDev);
423
451
  if (shouldUseTui) {
424
- Tui.start(config, logger, () => merge(displayedSnapshots(group.running, config, merge([]))));
452
+ let resumedChains$1 = resumedChains(config, resumed);
453
+ Tui.start(config, logger, () => merge(displayedSnapshots(group.running, resumedChains$1, merge([]))));
425
454
  }
426
455
  process.on("SIGTERM", () => stop(group));
427
456
  process.on("SIGINT", () => stop(group));
@@ -465,7 +494,7 @@ export {
465
494
  awaitExit,
466
495
  isRunAtHead,
467
496
  releaseIfAtHead,
468
- configuredChains,
497
+ resumedChains,
469
498
  displayedSnapshots,
470
499
  run,
471
500
  }
@@ -53,11 +53,13 @@ let renderRows = (rows: array<AddressRows.row>, ~config: Config.t) =>
53
53
 
54
54
  // Rows of a per-chain entity are keyed per (chain, id): the same id exists
55
55
  // independently on every chain.
56
- let rowKey = (~scope: Internal.chainScope, ~entityId: EntityId.t) =>
56
+ let rowKey = (~table: Table.table, ~scope: Internal.chainScope, ~entityId: EntityId.t) => {
57
+ let entityKey = table.entityIdToKey(entityId)
57
58
  switch scope {
58
- | CrossChain => entityId->EntityId.toKey
59
- | Chain(chainId) => `${chainId->ChainId.toString}|${entityId->EntityId.toKey}`
59
+ | CrossChain => entityKey
60
+ | Chain(chainId) => `${chainId->ChainId.toString}|${entityKey}`
60
61
  }
62
+ }
61
63
 
62
64
  let readChainId = (entity: Internal.entity, ~field: Table.field): option<ChainId.t> =>
63
65
  entity
@@ -158,19 +160,18 @@ let handleWriteBatch = (
158
160
  switch change {
159
161
  | Set({entityId, entity, checkpointId}) =>
160
162
  // The store keeps decoded entities so load comparisons (bigint /
161
- // BigDecimal) work on real values. Ids are keyed by their string form
162
- // since they may be string/int/bigint.
163
+ // BigDecimal) work on real values.
163
164
  let storedEntity = switch (chainIdField, scope->Internal.chainScopeChainId) {
164
165
  | (Some(field), Some(chainId)) =>
165
166
  entity->Internal.stampChainId(~fieldName=field.fieldName, ~chainId)
166
167
  | _ => entity
167
168
  }
168
- entityDict->Dict.set(rowKey(~scope, ~entityId), storedEntity)
169
+ entityDict->Dict.set(rowKey(~table=entityConfig.table, ~scope, ~entityId), storedEntity)
169
170
  // The change already carries the checkpoint's chainId, so the entity
170
171
  // inside it stays unstamped.
171
172
  entityChangeFor(checkpointId).sets->Array.push(entity->Utils.magic)->ignore
172
173
  | Delete({entityId, checkpointId}) =>
173
- Dict.delete(entityDict->Obj.magic, rowKey(~scope, ~entityId))
174
+ Dict.delete(entityDict->Obj.magic, rowKey(~table=entityConfig.table, ~scope, ~entityId))
174
175
  entityChangeFor(checkpointId).deleted->Array.push(entityId)->ignore
175
176
  }
176
177
  }
@@ -433,10 +434,15 @@ let getEntityFromState = (
433
434
  )
434
435
  }
435
436
  let entityDict = state.entities->Dict.get(entityConfig.name)->Option.getOr(Dict.make())
437
+ let {entityIdToKey} = entityConfig.table
438
+ let key = entityIdToKey(entityId->EntityId.unsafeOfString)
436
439
  switch entityConfig.table->Table.getChainIdField {
437
- | None => entityDict->Dict.get(entityId)->Option.map(copyEntity)
440
+ | None => entityDict->Dict.get(key)->Option.map(copyEntity)
438
441
  | Some(field) =>
439
- let matches = entityDict->Dict.valuesToArray->Array.filter(entity => entity.id === entityId)
442
+ let matches =
443
+ entityDict
444
+ ->Dict.valuesToArray
445
+ ->Array.filter(entity => entityIdToKey(entity.id->EntityId.unsafeOfString) === key)
440
446
  switch matches {
441
447
  | [] => None
442
448
  | [entity] => Some(copyEntity(entity))
@@ -508,7 +514,7 @@ let makeEntitySet = (~state: testIndexerState, ~entityConfig: Internal.entityCon
508
514
  }
509
515
  }
510
516
  entityDict->Dict.set(
511
- rowKey(~scope, ~entityId=entity.id->EntityId.unsafeOfString),
517
+ rowKey(~table=entityConfig.table, ~scope, ~entityId=entity.id->EntityId.unsafeOfString),
512
518
  copyEntity(entity),
513
519
  )
514
520
  }
@@ -568,13 +574,7 @@ let makeInMemoryStorage = (~state: testIndexerState): Persistence.storage => {
568
574
  // The runner injects the config-derived initial state by setting
569
575
  // `persistence.storageStatus = Ready(...)` directly, bypassing `Persistence.init`,
570
576
  // so none of these is reached.
571
- initialize: async (
572
- ~chainConfigs as _=?,
573
- ~entities as _=?,
574
- ~enums as _=?,
575
- ~contractMapping as _,
576
- ~envioInfo as _,
577
- ) =>
577
+ initialize: async (~chainConfigs as _=?, ~contractMapping as _, ~envioInfo as _) =>
578
578
  JsError.throwWithMessage(
579
579
  "TestIndexer: initialize should not be called; the initial state is derived from config.",
580
580
  ),
@@ -582,11 +582,11 @@ let makeInMemoryStorage = (~state: testIndexerState): Persistence.storage => {
582
582
  JsError.throwWithMessage(
583
583
  "TestIndexer: readStoredConfig should not be called; the initial state is derived from config.",
584
584
  ),
585
- addChain: async (~chainConfig as _, ~entities as _, ~contractMapping as _) =>
585
+ addChain: async (~chainConfig as _, ~contractMapping as _) =>
586
586
  JsError.throwWithMessage(
587
587
  "TestIndexer: addChain should not be called; the initial state is derived from config.",
588
588
  ),
589
- resumeInitialState: async (~entities as _, ~chainIds as _, ~contractMapping as _) =>
589
+ resumeInitialState: async (~chainIds as _, ~contractMapping as _) =>
590
590
  JsError.throwWithMessage(
591
591
  "TestIndexer: resumeInitialState should not be called; the initial state is derived from config.",
592
592
  ),
@@ -596,13 +596,12 @@ let makeInMemoryStorage = (~state: testIndexerState): Persistence.storage => {
596
596
  ->(Utils.magic: array<Internal.entity> => array<unknown>),
597
597
  // The in-memory storage has no indexes to build, and it's always ready.
598
598
  ensureQueryIndexes: async (~entityConfig as _, ~scope as _, ~filters as _) => (),
599
- ensureSchemaIndexes: async (~entities as _, ~chainIds as _) => (),
600
- finalizeBackfill: async (~entities as _, ~chainIds as _, ~readyAt as _) => (),
599
+ ensureSchemaIndexes: async (~chainIds as _) => (),
600
+ finalizeBackfill: async (~chainIds as _, ~readyAt as _) => (),
601
601
  writeBatch: async (
602
602
  ~batch,
603
603
  ~rollback as _,
604
604
  ~config,
605
- ~allEntities as _,
606
605
  ~updatedEffectsCache as _,
607
606
  ~updatedEntities,
608
607
  ~registeredAddresses,
@@ -623,11 +622,7 @@ let makeInMemoryStorage = (~state: testIndexerState): Persistence.storage => {
623
622
  setChainMeta: async _ => Obj.magic(),
624
623
  pruneStaleCheckpoints: async (~safeCheckpoints as _) => (),
625
624
  pruneStaleEntityHistory: async (~entityConfig as _, ~safeCheckpoints as _) => (),
626
- getRollbackTargetCheckpoint: async (
627
- ~reorgChainId as _,
628
- ~lastKnownValidBlockNumber as _,
629
- ~sequence as _,
630
- ) =>
625
+ getRollbackTargetCheckpoint: async (~reorgChainId as _, ~lastKnownValidBlockNumber as _) =>
631
626
  JsError.throwWithMessage(
632
627
  "TestIndexer: Rollback is not supported. The runner forces rollbackOnReorg off, so this should be unreachable.",
633
628
  ),
@@ -7,7 +7,6 @@ import * as Config from "./Config.res.mjs";
7
7
  import * as Logger from "./Logger.res.mjs";
8
8
  import * as ChainId from "./ChainId.res.mjs";
9
9
  import * as ChainMap from "./ChainMap.res.mjs";
10
- import * as EntityId from "./EntityId.res.mjs";
11
10
  import * as Frontier from "./db/Frontier.res.mjs";
12
11
  import * as Internal from "./Internal.res.mjs";
13
12
  import * as ChainState from "./ChainState.res.mjs";
@@ -39,11 +38,12 @@ function renderRows(rows, config) {
39
38
  return AddressRows.render(rows, config.ecosystem.name, !config.lowercaseAddresses);
40
39
  }
41
40
 
42
- function rowKey(scope, entityId) {
41
+ function rowKey(table, scope, entityId) {
42
+ let entityKey = table.entityIdToKey(entityId);
43
43
  if (scope === "crossChain") {
44
- return EntityId.toKey(entityId);
44
+ return entityKey;
45
45
  } else {
46
- return ChainId.toString(scope) + `|` + EntityId.toKey(entityId);
46
+ return ChainId.toString(scope) + `|` + entityKey;
47
47
  }
48
48
  }
49
49
 
@@ -126,12 +126,12 @@ function handleWriteBatch(state, config, updatedEntities, registeredAddresses, c
126
126
  let entity = change.entity;
127
127
  let match = Internal.chainScopeChainId(scope);
128
128
  let storedEntity = chainIdField !== undefined && match !== undefined ? Internal.stampChainId(entity, chainIdField.fieldName, Primitive_option.valFromOption(match)) : entity;
129
- entityDict[rowKey(scope, change.entityId)] = storedEntity;
129
+ entityDict[rowKey(entityConfig.table, scope, change.entityId)] = storedEntity;
130
130
  entityChangeFor(change.checkpointId).sets.push(entity);
131
131
  return;
132
132
  }
133
133
  let entityId = change.entityId;
134
- Stdlib_Dict.$$delete(entityDict, rowKey(scope, entityId));
134
+ Stdlib_Dict.$$delete(entityDict, rowKey(entityConfig.table, scope, entityId));
135
135
  entityChangeFor(change.checkpointId).deleted.push(entityId);
136
136
  };
137
137
  param.changes.forEach(processChange);
@@ -304,11 +304,14 @@ function getEntityFromState(state, entityConfig, entityId, methodName) {
304
304
  Stdlib_JsError.throwWithMessage(`Cannot call ` + entityConfig.name + `.` + methodName + `() while indexer.process() is running. ` + "Wait for process() to complete before accessing entities directly.");
305
305
  }
306
306
  let entityDict = Stdlib_Option.getOr(state.entities[entityConfig.name], {});
307
+ let match = entityConfig.table;
308
+ let entityIdToKey = match.entityIdToKey;
309
+ let key = entityIdToKey(entityId);
307
310
  let field = Table.getChainIdField(entityConfig.table);
308
311
  if (field === undefined) {
309
- return Stdlib_Option.map(entityDict[entityId], copyEntity);
312
+ return Stdlib_Option.map(entityDict[key], copyEntity);
310
313
  }
311
- let matches = Object.values(entityDict).filter(entity => entity.id === entityId);
314
+ let matches = Object.values(entityDict).filter(entity => entityIdToKey(entity.id) === key);
312
315
  let len = matches.length;
313
316
  if (len !== 1) {
314
317
  if (len === 0) {
@@ -358,7 +361,7 @@ function makeEntitySet(state, entityConfig) {
358
361
  } else {
359
362
  scope = "crossChain";
360
363
  }
361
- entityDict[rowKey(scope, entity.id)] = Utils.Dict.shallowCopy(entity);
364
+ entityDict[rowKey(entityConfig.table, scope, entity.id)] = Utils.Dict.shallowCopy(entity);
362
365
  };
363
366
  }
364
367
 
@@ -388,23 +391,23 @@ function makeInMemoryStorage(state) {
388
391
  name: "test-inmemory",
389
392
  entityStoreName: param => "test-inmemory",
390
393
  holding: async () => "Initialized",
391
- initialize: async (param, param$1, param$2, param$3, param$4) => Stdlib_JsError.throwWithMessage("TestIndexer: initialize should not be called; the initial state is derived from config."),
394
+ initialize: async (param, param$1, param$2) => Stdlib_JsError.throwWithMessage("TestIndexer: initialize should not be called; the initial state is derived from config."),
392
395
  readStoredConfig: async () => Stdlib_JsError.throwWithMessage("TestIndexer: readStoredConfig should not be called; the initial state is derived from config."),
393
- addChain: async (param, param$1, param$2) => Stdlib_JsError.throwWithMessage("TestIndexer: addChain should not be called; the initial state is derived from config."),
394
- resumeInitialState: async (param, param$1, param$2) => Stdlib_JsError.throwWithMessage("TestIndexer: resumeInitialState should not be called; the initial state is derived from config."),
396
+ addChain: async (param, param$1) => Stdlib_JsError.throwWithMessage("TestIndexer: addChain should not be called; the initial state is derived from config."),
397
+ resumeInitialState: async (param, param$1) => Stdlib_JsError.throwWithMessage("TestIndexer: resumeInitialState should not be called; the initial state is derived from config."),
395
398
  loadOrThrow: async (filter, table) => handleLoad(state, table.tableName, filter),
396
399
  ensureQueryIndexes: async (param, param$1, param$2) => {},
397
- ensureSchemaIndexes: async (param, param$1) => {},
398
- finalizeBackfill: async (param, param$1, param$2) => {},
400
+ ensureSchemaIndexes: async param => {},
401
+ finalizeBackfill: async (param, param$1) => {},
399
402
  dumpEffectCache: async () => {},
400
403
  reset: async () => {},
401
404
  setChainMeta: async param => {},
402
405
  pruneStaleCheckpoints: async param => {},
403
406
  pruneStaleEntityHistory: async (param, param$1) => {},
404
- getRollbackTargetCheckpoint: async (param, param$1, param$2) => Stdlib_JsError.throwWithMessage("TestIndexer: Rollback is not supported. The runner forces rollbackOnReorg off, so this should be unreachable."),
407
+ getRollbackTargetCheckpoint: async (param, param$1) => Stdlib_JsError.throwWithMessage("TestIndexer: Rollback is not supported. The runner forces rollbackOnReorg off, so this should be unreachable."),
405
408
  getRollbackProgressDiff: async param => Stdlib_JsError.throwWithMessage("TestIndexer: Rollback is not supported. The runner forces rollbackOnReorg off, so this should be unreachable."),
406
409
  getRollbackData: async (param, param$1) => Stdlib_JsError.throwWithMessage("TestIndexer: Rollback is not supported. The runner forces rollbackOnReorg off, so this should be unreachable."),
407
- writeBatch: async (batch, param, config, param$1, param$2, updatedEntities, registeredAddresses, param$3, param$4) => handleWriteBatch(state, config, updatedEntities, registeredAddresses, batch.checkpointIds, batch.checkpointChainIds, batch.checkpointBlockNumbers, batch.checkpointEventsProcessed),
410
+ writeBatch: async (batch, param, config, param$1, updatedEntities, registeredAddresses, param$2, param$3) => handleWriteBatch(state, config, updatedEntities, registeredAddresses, batch.checkpointIds, batch.checkpointChainIds, batch.checkpointBlockNumbers, batch.checkpointEventsProcessed),
408
411
  close: async () => {}
409
412
  };
410
413
  }
@@ -648,7 +651,7 @@ function createTestIndexer() {
648
651
  let indexerState = IndexerState.makeFromDbState(runConfig, logger$1, persistence, initialState, registrationsByChainId, undefined, undefined, exitAfterFirstEventBlock, undefined, undefined, undefined, errHandler => {
649
652
  ErrorHandling.log(errHandler);
650
653
  reject(ErrorHandling.toExn(errHandler));
651
- }, () => resolve());
654
+ }, () => resolve(), undefined);
652
655
  indexerStateRef.contents = Primitive_option.some(indexerState);
653
656
  IndexerLoop.start(indexerState);
654
657
  });
package/src/Throttler.res CHANGED
@@ -5,27 +5,30 @@ type t = {
5
5
  mutable scheduled: option<unit => promise<unit>>,
6
6
  intervalMillis: float,
7
7
  logger: Logger.t,
8
+ clock: Clock.t,
8
9
  }
9
10
 
10
- let make = (~intervalMillis: int, ~logger) => {
11
- lastRunTimeMillis: 0.,
11
+ let make = (~intervalMillis: int, ~logger, ~clock=Clock.real) => {
12
+ // A clock reads from its own origin, which the first run must not wait on.
13
+ lastRunTimeMillis: -.(intervalMillis->Int.toFloat),
12
14
  isRunning: false,
13
15
  isAwaitingInterval: false,
14
16
  scheduled: None,
15
17
  intervalMillis: intervalMillis->Int.toFloat,
16
18
  logger,
19
+ clock,
17
20
  }
18
21
 
19
22
  let rec startInternal = (throttler: t) => {
20
23
  switch throttler {
21
24
  | {scheduled: Some(fn), isRunning: false, isAwaitingInterval: false} =>
22
- let timeSinceLastRun = Date.now() -. throttler.lastRunTimeMillis
25
+ let timeSinceLastRun = throttler.clock.now() -. throttler.lastRunTimeMillis
23
26
 
24
27
  //Only execute if we are passed the interval
25
28
  if timeSinceLastRun >= throttler.intervalMillis {
26
29
  throttler.isRunning = true
27
30
  throttler.scheduled = None
28
- throttler.lastRunTimeMillis = Date.now()
31
+ throttler.lastRunTimeMillis = throttler.clock.now()
29
32
 
30
33
  // Defer off the schedule call so work queued before it (e.g. a batch) runs first.
31
34
  NodeJs.setImmediate(() => {
@@ -45,10 +48,14 @@ let rec startInternal = (throttler: t) => {
45
48
  } else {
46
49
  //Store isAwaitingInterval in state so that timers don't continuously get created
47
50
  throttler.isAwaitingInterval = true
48
- let _ = setTimeout(() => {
49
- throttler.isAwaitingInterval = false
50
- throttler->startInternal
51
- }, Int.fromFloat(throttler.intervalMillis -. timeSinceLastRun))
51
+ let _ = throttler.clock.setTimeout(
52
+ ~label="throttled write",
53
+ () => {
54
+ throttler.isAwaitingInterval = false
55
+ throttler->startInternal
56
+ },
57
+ Int.fromFloat(throttler.intervalMillis -. timeSinceLastRun),
58
+ )
52
59
  }
53
60
  | _ => ()
54
61
  }
@@ -1,16 +1,19 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as Clock from "./Clock.res.mjs";
3
4
  import * as Logger from "./Logger.res.mjs";
4
5
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
5
6
 
6
- function make(intervalMillis, logger) {
7
+ function make(intervalMillis, logger, clockOpt) {
8
+ let clock = clockOpt !== undefined ? clockOpt : Clock.real;
7
9
  return {
8
- lastRunTimeMillis: 0,
10
+ lastRunTimeMillis: - intervalMillis,
9
11
  isRunning: false,
10
12
  isAwaitingInterval: false,
11
13
  scheduled: undefined,
12
14
  intervalMillis: intervalMillis,
13
- logger: logger
15
+ logger: logger,
16
+ clock: clock
14
17
  };
15
18
  }
16
19
 
@@ -27,11 +30,11 @@ function startInternal(throttler) {
27
30
  if (fn === undefined) {
28
31
  return;
29
32
  }
30
- let timeSinceLastRun = Date.now() - throttler.lastRunTimeMillis;
33
+ let timeSinceLastRun = throttler.clock.now() - throttler.lastRunTimeMillis;
31
34
  if (timeSinceLastRun >= throttler.intervalMillis) {
32
35
  throttler.isRunning = true;
33
36
  throttler.scheduled = undefined;
34
- throttler.lastRunTimeMillis = Date.now();
37
+ throttler.lastRunTimeMillis = throttler.clock.now();
35
38
  setImmediate(() => {
36
39
  (async () => {
37
40
  try {
@@ -46,7 +49,7 @@ function startInternal(throttler) {
46
49
  });
47
50
  } else {
48
51
  throttler.isAwaitingInterval = true;
49
- setTimeout(() => {
52
+ throttler.clock.setTimeout("throttled write", () => {
50
53
  throttler.isAwaitingInterval = false;
51
54
  startInternal(throttler);
52
55
  }, throttler.intervalMillis - timeSinceLastRun | 0);
@@ -15,7 +15,7 @@ Does NOT queue scheduled functions but rather overwrites them
15
15
  The logger will be used to log any errors that occur in the scheduled
16
16
  functions.
17
17
  */
18
- let make: (~intervalMillis: int, ~logger: Logger.t) => t
18
+ let make: (~intervalMillis: int, ~logger: Logger.t, ~clock: Clock.t=?) => t
19
19
 
20
20
  /**
21
21
  Schedules a function to be run on a throttler, overwriting any