envio 3.3.1 → 3.4.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 (60) hide show
  1. package/package.json +6 -6
  2. package/src/Api.res +1 -8
  3. package/src/Api.res.mjs +1 -5
  4. package/src/ChainState.res +6 -1
  5. package/src/ChainState.res.mjs +4 -3
  6. package/src/Config.res +33 -4
  7. package/src/Config.res.mjs +34 -5
  8. package/src/Core.res +30 -19
  9. package/src/Core.res.mjs +5 -5
  10. package/src/EnvioGlobal.res +0 -2
  11. package/src/EnvioGlobal.res.mjs +0 -1
  12. package/src/ExitOnCaughtUp.res +6 -2
  13. package/src/ExitOnCaughtUp.res.mjs +4 -0
  14. package/src/FetchState.res +3 -3
  15. package/src/HandlerRegister.res +357 -434
  16. package/src/HandlerRegister.res.mjs +202 -242
  17. package/src/HandlerRegister.resi +7 -15
  18. package/src/IndexerState.res +10 -0
  19. package/src/IndexerState.res.mjs +9 -3
  20. package/src/IndexerState.resi +3 -0
  21. package/src/Internal.res +10 -1
  22. package/src/Main.res.mjs +4 -4
  23. package/src/PgStorage.res +52 -26
  24. package/src/PgStorage.res.mjs +32 -23
  25. package/src/SimulateItems.res +61 -40
  26. package/src/SimulateItems.res.mjs +37 -21
  27. package/src/TestIndexer.res +446 -453
  28. package/src/TestIndexer.res.mjs +320 -343
  29. package/src/bindings/ClickHouse.res +68 -2
  30. package/src/bindings/ClickHouse.res.mjs +47 -2
  31. package/src/sources/EvmChain.res +1 -1
  32. package/src/sources/EvmChain.res.mjs +2 -2
  33. package/src/sources/FuelHyperSync.res +74 -0
  34. package/src/sources/FuelHyperSync.res.mjs +80 -0
  35. package/src/sources/FuelHyperSync.resi +22 -0
  36. package/src/sources/FuelHyperSyncClient.res +120 -0
  37. package/src/sources/FuelHyperSyncClient.res.mjs +71 -0
  38. package/src/sources/FuelHyperSyncSource.res +257 -0
  39. package/src/sources/FuelHyperSyncSource.res.mjs +252 -0
  40. package/src/sources/HyperSyncClient.res +2 -2
  41. package/src/sources/HyperSyncClient.res.mjs +1 -1
  42. package/src/sources/SvmHyperSyncClient.res +139 -102
  43. package/src/sources/SvmHyperSyncClient.res.mjs +45 -5
  44. package/src/sources/SvmHyperSyncSource.res +60 -440
  45. package/src/sources/SvmHyperSyncSource.res.mjs +42 -363
  46. package/src/TestIndexerProxyStorage.res +0 -196
  47. package/src/TestIndexerProxyStorage.res.mjs +0 -121
  48. package/src/TestIndexerWorker.res +0 -4
  49. package/src/TestIndexerWorker.res.mjs +0 -7
  50. package/src/sources/EventRouter.res +0 -165
  51. package/src/sources/EventRouter.res.mjs +0 -153
  52. package/src/sources/HyperFuel.res +0 -179
  53. package/src/sources/HyperFuel.res.mjs +0 -146
  54. package/src/sources/HyperFuel.resi +0 -36
  55. package/src/sources/HyperFuelClient.res +0 -127
  56. package/src/sources/HyperFuelClient.res.mjs +0 -20
  57. package/src/sources/HyperFuelSource.res +0 -502
  58. package/src/sources/HyperFuelSource.res.mjs +0 -481
  59. /package/src/sources/{HyperSyncSource.res → EvmHyperSyncSource.res} +0 -0
  60. /package/src/sources/{HyperSyncSource.res.mjs → EvmHyperSyncSource.res.mjs} +0 -0
@@ -44,7 +44,7 @@ function get(self, entityName) {
44
44
  }
45
45
  }
46
46
 
47
- function make$1(config, persistence, chainStates, isInReorgThreshold, isRealtime, targetBufferSizeOpt, committedCheckpointIdOpt, isDevelopmentModeOpt, shouldUseTuiOpt, exitAfterFirstEventBlockOpt, onError) {
47
+ function make$1(config, persistence, chainStates, isInReorgThreshold, isRealtime, targetBufferSizeOpt, committedCheckpointIdOpt, isDevelopmentModeOpt, shouldUseTuiOpt, exitAfterFirstEventBlockOpt, onError, onExit) {
48
48
  let targetBufferSize = targetBufferSizeOpt !== undefined ? targetBufferSizeOpt : CrossChainState.calculateTargetBufferSize();
49
49
  let committedCheckpointId = committedCheckpointIdOpt !== undefined ? committedCheckpointIdOpt : Internal.initialCheckpointId;
50
50
  let isDevelopmentMode = isDevelopmentModeOpt !== undefined ? isDevelopmentModeOpt : false;
@@ -81,6 +81,7 @@ function make$1(config, persistence, chainStates, isInReorgThreshold, isRealtime
81
81
  keepProcessAlive: isDevelopmentMode || shouldUseTui,
82
82
  exitAfterFirstEventBlock: exitAfterFirstEventBlock,
83
83
  onError: onError,
84
+ onExit: onExit,
84
85
  isStopped: false,
85
86
  epoch: 0,
86
87
  simulateDeadInputTracker: SimulateDeadInputTracker.makeFromConfig(config),
@@ -96,7 +97,7 @@ function make$1(config, persistence, chainStates, isInReorgThreshold, isRealtime
96
97
  };
97
98
  }
98
99
 
99
- function makeFromDbState(config, persistence, initialState, registrationsByChainId, isDevelopmentModeOpt, shouldUseTuiOpt, exitAfterFirstEventBlockOpt, reducedPollingInterval, targetBufferSizeOpt, onError) {
100
+ function makeFromDbState(config, persistence, initialState, registrationsByChainId, isDevelopmentModeOpt, shouldUseTuiOpt, exitAfterFirstEventBlockOpt, reducedPollingInterval, targetBufferSizeOpt, onError, onExit) {
100
101
  let isDevelopmentMode = isDevelopmentModeOpt !== undefined ? isDevelopmentModeOpt : false;
101
102
  let shouldUseTui = shouldUseTuiOpt !== undefined ? shouldUseTuiOpt : false;
102
103
  let exitAfterFirstEventBlock = exitAfterFirstEventBlockOpt !== undefined ? exitAfterFirstEventBlockOpt : false;
@@ -118,7 +119,7 @@ function makeFromDbState(config, persistence, initialState, registrationsByChain
118
119
  let chainConfig = ChainMap.get(config.chainMap, chain);
119
120
  chainStates[resumedChainState.id] = ChainState.makeFromDbState(chainConfig, resumedChainState, initialState.reorgCheckpoints, isInReorgThreshold, isRealtime, config, registrationsByChainId, reducedPollingInterval);
120
121
  });
121
- let state = make$1(config, persistence, chainStates, isInReorgThreshold, isRealtime, targetBufferSize, initialState.checkpointId, isDevelopmentMode, shouldUseTui, exitAfterFirstEventBlock, onError);
122
+ let state = make$1(config, persistence, chainStates, isInReorgThreshold, isRealtime, targetBufferSize, initialState.checkpointId, isDevelopmentMode, shouldUseTui, exitAfterFirstEventBlock, onError, onExit);
122
123
  Utils.Dict.forEach(initialState.cache, param => {
123
124
  let count = param.count;
124
125
  EffectState.setUnregisteredCacheCount(state.effectState, param.effectName, param.scope, count);
@@ -319,6 +320,10 @@ function exitAfterFirstEventBlock(state) {
319
320
  return state.exitAfterFirstEventBlock;
320
321
  }
321
322
 
323
+ function onExit(state) {
324
+ return state.onExit;
325
+ }
326
+
322
327
  function isStopped(state) {
323
328
  return state.isStopped;
324
329
  }
@@ -718,6 +723,7 @@ export {
718
723
  loadManager,
719
724
  keepProcessAlive,
720
725
  exitAfterFirstEventBlock,
726
+ onExit,
721
727
  isStopped,
722
728
  epoch,
723
729
  lastPrunedAtMillis,
@@ -30,6 +30,7 @@ let make: (
30
30
  ~shouldUseTui: bool=?,
31
31
  ~exitAfterFirstEventBlock: bool=?,
32
32
  ~onError: ErrorHandling.t => unit,
33
+ ~onExit: unit => unit=?,
33
34
  ) => t
34
35
 
35
36
  let makeFromDbState: (
@@ -43,6 +44,7 @@ let makeFromDbState: (
43
44
  ~reducedPollingInterval: int=?,
44
45
  ~targetBufferSize: int=?,
45
46
  ~onError: ErrorHandling.t => unit,
47
+ ~onExit: unit => unit=?,
46
48
  ) => t
47
49
 
48
50
  let unexpectedErrorMsg: string
@@ -96,6 +98,7 @@ let indexerStartTime: t => Date.t
96
98
  let loadManager: t => LoadManager.t
97
99
  let keepProcessAlive: t => bool
98
100
  let exitAfterFirstEventBlock: t => bool
101
+ let onExit: t => option<unit => unit>
99
102
  let isStopped: t => bool
100
103
  let epoch: t => int
101
104
  let lastPrunedAtMillis: t => dict<float>
package/src/Internal.res CHANGED
@@ -179,7 +179,7 @@ type svmBlockField =
179
179
  let allSvmBlockFields: array<svmBlockField> = [Height, ParentSlot, ParentHash]
180
180
  let svmBlockFieldSchema = S.enum(allSvmBlockFields)
181
181
 
182
- // Static sets of nullable field names used by RpcSource and HyperSyncSource to wrap schemas with S.nullable
182
+ // Static sets of field names whose source schemas must be wrapped with S.nullable.
183
183
  let evmNullableBlockFields = Utils.Set.fromArray(
184
184
  (
185
185
  [
@@ -720,11 +720,20 @@ let fuelTransferParamsSchema = S.schema(s => {
720
720
 
721
721
  type entity = private {id: string}
722
722
 
723
+ // Raw ClickHouse expressions/field names from the entity's
724
+ // @storage(clickhouse: {...}) directive, applied to the history table DDL.
725
+ type clickhouseTableOptions = {
726
+ partitionBy?: string,
727
+ orderBy?: array<string>,
728
+ ttl?: string,
729
+ }
730
+
723
731
  // Per-entity storage resolved at parse time against the global storage
724
732
  // config. Downstream PG/CH consumers just check the matching boolean.
725
733
  type entityStorage = {
726
734
  postgres: bool,
727
735
  clickhouse: bool,
736
+ clickhouseOptions?: clickhouseTableOptions,
728
737
  }
729
738
 
730
739
  type genericEntityConfig<'entity> = {
package/src/Main.res.mjs CHANGED
@@ -247,7 +247,7 @@ function getGlobalIndexer() {
247
247
  let onEventFn = (identityConfig, handler) => {
248
248
  HandlerRegister.throwIfFinishedRegistration("onEvent");
249
249
  let match = parseIdentityConfig(identityConfig);
250
- HandlerRegister.setHandler(match[0], match[1], handler, match[2], undefined);
250
+ HandlerRegister.setHandler(match[0], match[1], handler, match[2]);
251
251
  };
252
252
  let parseSvmIdentityConfig = identityConfig => {
253
253
  let match;
@@ -279,12 +279,12 @@ function getGlobalIndexer() {
279
279
  HandlerRegister.setHandler(match[0], match[1], args => handler({
280
280
  instruction: args.event,
281
281
  context: args.context
282
- }), match[2], undefined);
282
+ }), match[2]);
283
283
  };
284
284
  let contractRegisterFn = (identityConfig, handler) => {
285
285
  HandlerRegister.throwIfFinishedRegistration("contractRegister");
286
286
  let match = parseIdentityConfig(identityConfig);
287
- HandlerRegister.setContractRegister(match[0], match[1], handler, match[2], undefined);
287
+ HandlerRegister.setContractRegister(match[0], match[1], handler, match[2]);
288
288
  };
289
289
  let onRollbackCommitFn = callback => {
290
290
  HandlerRegister.throwIfFinishedRegistration("~internalAndWillBeRemovedSoon_onRollbackCommit");
@@ -524,7 +524,7 @@ async function start(persistence, resetOpt, isTestOpt, exitAfterFirstEventBlockO
524
524
  };
525
525
  }, persistence$1, isDevelopmentMode);
526
526
  }
527
- let state = IndexerState.makeFromDbState(config$2, persistence$1, Persistence.getInitializedState(persistence$1), registrationsByChainId, isDevelopmentMode, shouldUseTui, exitAfterFirstEventBlock, undefined, undefined, onError);
527
+ let state = IndexerState.makeFromDbState(config$2, persistence$1, Persistence.getInitializedState(persistence$1), registrationsByChainId, isDevelopmentMode, shouldUseTui, exitAfterFirstEventBlock, undefined, undefined, onError, undefined);
528
528
  if (shouldUseTui) {
529
529
  Tui.start(() => state);
530
530
  }
package/src/PgStorage.res CHANGED
@@ -1,5 +1,3 @@
1
- let getCacheRowCountFnName = "get_cache_row_count"
2
-
3
1
  let makeClient = () => {
4
2
  Postgres.makeSql(
5
3
  ~config={
@@ -267,18 +265,7 @@ GRANT ALL ON SCHEMA "${pgSchema}" TO public;`,
267
265
  | None => ()
268
266
  }
269
267
 
270
- [
271
- query.contents,
272
- `CREATE OR REPLACE FUNCTION ${getCacheRowCountFnName}(table_name text)
273
- RETURNS integer AS $$
274
- DECLARE
275
- result integer;
276
- BEGIN
277
- EXECUTE format('SELECT COUNT(*) FROM "${pgSchema}".%I', table_name) INTO result;
278
- RETURN result;
279
- END;
280
- $$ LANGUAGE plpgsql;`,
281
- ]
268
+ [query.contents]
282
269
  }
283
270
 
284
271
  let makeLoadQuery = (~pgSchema, ~tableName, ~condition) => {
@@ -661,16 +648,19 @@ type schemaCacheTableInfo = {
661
648
  count: int,
662
649
  }
663
650
 
651
+ type cacheRowCount = {
652
+ @as("count")
653
+ count: int,
654
+ }
655
+
664
656
  // Matches both the cross-chain (`envio_effect_<name>`) and chain-scoped
665
657
  // (`envio_<chainId>_effect_<name>`) cache-table formats. Kept in sync with
666
658
  // Internal.EffectCache.
667
- let makeSchemaCacheTableInfoQuery = (~pgSchema) => {
659
+ let makeEffectCacheTableNamesQuery = (~pgSchema) => {
668
660
  // The column guard requires the effect-cache shape (exactly an `id` + `output`
669
661
  // pair) so a user entity table that happens to match the name pattern is never
670
662
  // mistaken for an effect cache.
671
- `SELECT
672
- t.table_name,
673
- ${getCacheRowCountFnName}(t.table_name) as count
663
+ `SELECT t.table_name
674
664
  FROM information_schema.tables t
675
665
  WHERE t.table_schema = '${pgSchema}'
676
666
  AND t.table_name ~ '^envio_([0-9]+_)?effect_.+'
@@ -681,6 +671,15 @@ let makeSchemaCacheTableInfoQuery = (~pgSchema) => {
681
671
  ) = ARRAY['id', 'output'];`
682
672
  }
683
673
 
674
+ let makeCacheRowCountQuery = (~pgSchema, ~tableName) => {
675
+ // The table name comes from information_schema, so anything cache-shaped that
676
+ // was created out-of-band in the schema reaches here. Splice both identifiers
677
+ // as quoted identifiers, doubling embedded quotes, so a crafted name can't
678
+ // break out into raw SQL.
679
+ let quoteIdent = ident => `"${ident->String.replaceAll("\"", "\"\"")}"`
680
+ `SELECT COUNT(*)::int AS count FROM ${quoteIdent(pgSchema)}.${quoteIdent(tableName)};`
681
+ }
682
+
684
683
  type psqlExecState =
685
684
  Unknown | Pending(promise<result<string, string>>) | Resolved(result<string, string>)
686
685
 
@@ -850,9 +849,24 @@ let rec writeBatch = async (
850
849
  let specificError = ref(None)
851
850
 
852
851
  let rawEvents = if config.enableRawEvents {
852
+ // A single on-chain log fans out to one item per matching registration;
853
+ // `raw_events` records the log itself, so dedupe by its coordinate
854
+ // (chain, block, logIndex) to keep one row per log.
855
+ let seenLogCoordinates = Utils.Set.make()
853
856
  let rows = batch.items->Array.filterMap(item =>
854
857
  switch item {
855
- | Internal.Event(_) => Some(config.ecosystem.toRawEvent(item->Internal.castUnsafeEventItem))
858
+ | Internal.Event(_) =>
859
+ let coordinate = `${item
860
+ ->Internal.getItemChainId
861
+ ->Int.toString}-${item
862
+ ->Internal.getItemBlockNumber
863
+ ->Int.toString}-${item->Internal.getItemLogIndex->Int.toString}`
864
+ if seenLogCoordinates->Utils.Set.has(coordinate) {
865
+ None
866
+ } else {
867
+ seenLogCoordinates->Utils.Set.add(coordinate)->ignore
868
+ Some(config.ecosystem.toRawEvent(item->Internal.castUnsafeEventItem))
869
+ }
856
870
  | Internal.Block(_) => None
857
871
  }
858
872
  )
@@ -1301,6 +1315,23 @@ let make = (
1301
1315
  result
1302
1316
  }
1303
1317
 
1318
+ // Each indexer counts the effect-cache tables in its own schema. The counts
1319
+ // are computed here per table rather than through a shared SQL helper so
1320
+ // indexers isolated by schema in one database never touch each other's state.
1321
+ let queryCacheTableInfo = async (): array<schemaCacheTableInfo> => {
1322
+ let tableNames: array<schemaTableName> = await sql->Postgres.unsafe(
1323
+ makeEffectCacheTableNamesQuery(~pgSchema),
1324
+ )
1325
+ await tableNames
1326
+ ->Array.map(async ({tableName}) => {
1327
+ let rows: array<cacheRowCount> = await sql->Postgres.unsafe(
1328
+ makeCacheRowCountQuery(~pgSchema, ~tableName),
1329
+ )
1330
+ ({tableName, count: (rows->Array.getUnsafe(0)).count}: schemaCacheTableInfo)
1331
+ })
1332
+ ->Promise.all
1333
+ }
1334
+
1304
1335
  let restoreEffectCache = async (~withUpload) => {
1305
1336
  if withUpload {
1306
1337
  // Try to restore cache tables from the .envio/cache TSV files
@@ -1340,9 +1371,7 @@ let make = (
1340
1371
  }
1341
1372
  }
1342
1373
 
1343
- let cacheTableInfo: array<schemaCacheTableInfo> = await sql->Postgres.unsafe(
1344
- makeSchemaCacheTableInfoQuery(~pgSchema),
1345
- )
1374
+ let cacheTableInfo = await queryCacheTableInfo()
1346
1375
 
1347
1376
  if withUpload && cacheTableInfo->Utils.Array.notEmpty {
1348
1377
  // Integration with other tools like Hasura
@@ -1551,10 +1580,7 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
1551
1580
 
1552
1581
  let dumpEffectCache = async () => {
1553
1582
  try {
1554
- let cacheTableInfo: array<schemaCacheTableInfo> =
1555
- (await sql->Postgres.unsafe(makeSchemaCacheTableInfoQuery(~pgSchema)))->Array.filter(i =>
1556
- i.count > 0
1557
- )
1583
+ let cacheTableInfo = (await queryCacheTableInfo())->Array.filter(i => i.count > 0)
1558
1584
 
1559
1585
  if cacheTableInfo->Utils.Array.notEmpty {
1560
1586
  // Create .envio/cache directory if it doesn't exist
@@ -27,8 +27,6 @@ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js
27
27
  import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
28
28
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
29
29
 
30
- let getCacheRowCountFnName = "get_cache_row_count";
31
-
32
30
  function makeClient() {
33
31
  return Postgres({
34
32
  host: Env.Db.host,
@@ -222,18 +220,7 @@ GRANT ALL ON SCHEMA "` + pgSchema + `" TO public;`)
222
220
  if (initialChainsValuesQuery !== undefined) {
223
221
  query.contents = query.contents + "\n" + initialChainsValuesQuery;
224
222
  }
225
- return [
226
- query.contents,
227
- `CREATE OR REPLACE FUNCTION ` + getCacheRowCountFnName + `(table_name text)
228
- RETURNS integer AS $$
229
- DECLARE
230
- result integer;
231
- BEGIN
232
- EXECUTE format('SELECT COUNT(*) FROM "` + pgSchema + `".%I', table_name) INTO result;
233
- RETURN result;
234
- END;
235
- $$ LANGUAGE plpgsql;`
236
- ];
223
+ return [query.contents];
237
224
  }
238
225
 
239
226
  function makeLoadQuery(pgSchema, tableName, condition) {
@@ -492,10 +479,8 @@ function makeSchemaTableNamesQuery(pgSchema) {
492
479
  return `SELECT table_name FROM information_schema.tables WHERE table_schema = '` + pgSchema + `';`;
493
480
  }
494
481
 
495
- function makeSchemaCacheTableInfoQuery(pgSchema) {
496
- return `SELECT
497
- t.table_name,
498
- ` + getCacheRowCountFnName + `(t.table_name) as count
482
+ function makeEffectCacheTableNamesQuery(pgSchema) {
483
+ return `SELECT t.table_name
499
484
  FROM information_schema.tables t
500
485
  WHERE t.table_schema = '` + pgSchema + `'
501
486
  AND t.table_name ~ '^envio_([0-9]+_)?effect_.+'
@@ -506,6 +491,11 @@ function makeSchemaCacheTableInfoQuery(pgSchema) {
506
491
  ) = ARRAY['id', 'output'];`;
507
492
  }
508
493
 
494
+ function makeCacheRowCountQuery(pgSchema, tableName) {
495
+ let quoteIdent = ident => `"` + ident.replaceAll("\"", "\"\"") + `"`;
496
+ return `SELECT COUNT(*)::int AS count FROM ` + quoteIdent(pgSchema) + `.` + quoteIdent(tableName) + `;`;
497
+ }
498
+
509
499
  let psqlExecState = {
510
500
  contents: "Unknown"
511
501
  };
@@ -617,8 +607,16 @@ async function writeBatch(sql, batch, pgSchema, rollback, isInReorgThreshold, co
617
607
  };
618
608
  let rawEvents;
619
609
  if (config.enableRawEvents) {
610
+ let seenLogCoordinates = new Set();
620
611
  let rows = Stdlib_Array.filterMap(batch.items, item => {
621
- if (item.kind === 0) {
612
+ if (item.kind !== 0) {
613
+ return;
614
+ }
615
+ let coordinate = Internal.getItemChainId(item).toString() + `-` + item.blockNumber.toString() + `-` + item.logIndex.toString();
616
+ if (seenLogCoordinates.has(coordinate)) {
617
+ return;
618
+ } else {
619
+ seenLogCoordinates.add(coordinate);
622
620
  return config.ecosystem.toRawEvent(item);
623
621
  }
624
622
  });
@@ -889,6 +887,17 @@ function make(sql, pgHost, pgSchema, pgPort, pgUser, pgDatabase, pgPassword, isH
889
887
  }));
890
888
  return result;
891
889
  };
890
+ let queryCacheTableInfo = async () => {
891
+ let tableNames = await sql.unsafe(makeEffectCacheTableNamesQuery(pgSchema));
892
+ return await Promise.all(tableNames.map(async param => {
893
+ let tableName = param.table_name;
894
+ let rows = await sql.unsafe(makeCacheRowCountQuery(pgSchema, tableName));
895
+ return {
896
+ table_name: tableName,
897
+ count: rows[0].count
898
+ };
899
+ }));
900
+ };
892
901
  let restoreEffectCache = async withUpload => {
893
902
  if (withUpload) {
894
903
  let entries = await scanCacheDir();
@@ -920,7 +929,7 @@ function make(sql, pgHost, pgSchema, pgPort, pgUser, pgDatabase, pgPassword, isH
920
929
  Logging.info("No cache found to upload.");
921
930
  }
922
931
  }
923
- let cacheTableInfo = await sql.unsafe(makeSchemaCacheTableInfoQuery(pgSchema));
932
+ let cacheTableInfo = await queryCacheTableInfo();
924
933
  if (withUpload && Utils.$$Array.notEmpty(cacheTableInfo) && onNewTables !== undefined) {
925
934
  await onNewTables(cacheTableInfo.map(info => info.table_name));
926
935
  }
@@ -1041,7 +1050,7 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
1041
1050
  };
1042
1051
  let dumpEffectCache = async () => {
1043
1052
  try {
1044
- let cacheTableInfo = (await sql.unsafe(makeSchemaCacheTableInfoQuery(pgSchema))).filter(i => i.count > 0);
1053
+ let cacheTableInfo = (await queryCacheTableInfo()).filter(i => i.count > 0);
1045
1054
  if (!Utils.$$Array.notEmpty(cacheTableInfo)) {
1046
1055
  return;
1047
1056
  }
@@ -1250,7 +1259,6 @@ function makePersistenceFromConfig(config, storageOpt) {
1250
1259
  let maxItemsPerQuery = 500;
1251
1260
 
1252
1261
  export {
1253
- getCacheRowCountFnName,
1254
1262
  makeClient,
1255
1263
  makeCreateIndexQuery,
1256
1264
  directionToSql,
@@ -1279,7 +1287,8 @@ export {
1279
1287
  setQueryCache,
1280
1288
  setOrThrow,
1281
1289
  makeSchemaTableNamesQuery,
1282
- makeSchemaCacheTableInfoQuery,
1290
+ makeEffectCacheTableNamesQuery,
1291
+ makeCacheRowCountQuery,
1283
1292
  getConnectedPsqlExec,
1284
1293
  deleteByIdsOrThrow,
1285
1294
  makeInsertDeleteUpdatesQuery,
@@ -339,46 +339,67 @@ let parse = (
339
339
  | None => seenCoordinates->Dict.set(coordinate, itemIndex)
340
340
  }
341
341
 
342
- // Build a real registration the same way `HandlerRegister.finishRegistration`
343
- // does at startup (not a stub), so the address filter and `where`
344
- // behave identically to real indexing — the dead-input tracker relies
345
- // on `clientAddressFilter` actually gating unrouted items.
346
- let onEventRegistration = HandlerRegister.buildOnEventRegistration(
347
- ~config,
348
- ~chainId,
349
- ~eventConfig,
350
- )
351
- // Append into the registration array that the chain state will own and
352
- // put that same registration object directly on the simulated item.
353
- let onEventRegistrationIndex = onEventRegistrations->Array.length
354
- let onEventRegistration = {...onEventRegistration, index: onEventRegistrationIndex}
355
- onEventRegistrations->Array.push(onEventRegistration)->ignore
356
-
357
- items
358
- ->Array.push(
359
- Internal.Event({
360
- onEventRegistration,
361
- chain,
362
- blockNumber,
363
- logIndex,
364
- // Simulate keeps the transaction inline on the payload, so the store
365
- // key is unused.
366
- transactionIndex: 0,
367
- payload: (
368
- {
369
- contractName: eventConfig.contractName,
370
- eventName: eventConfig.name,
371
- params,
372
- chainId,
373
- srcAddress,
374
- logIndex,
375
- transaction,
376
- block,
377
- }: Evm.payload
378
- )->Evm.fromPayload,
379
- }),
380
- )
381
- ->ignore
342
+ // Fan the simulated event out to every registration that would actually
343
+ // run here the way real routing does (one item per registration).
344
+ // Registrations are built the same way `HandlerRegister.finishRegistration`
345
+ // does at startup (not stubs), so the address filter and `where` behave
346
+ // identically to real indexing — the dead-input tracker relies on
347
+ // `clientAddressFilter` actually gating unrouted items. Drop registrations
348
+ // whose `where` excludes this chain (they wouldn't be fetched live), and
349
+ // ignore the bare handler-less fallback so a missing handler surfaces below
350
+ // instead of silently running nothing.
351
+ let liveRegistrations =
352
+ HandlerRegister.getSimulateOnEventRegistrations(
353
+ ~config,
354
+ ~chainId,
355
+ ~eventConfig,
356
+ )->Array.filter(reg =>
357
+ (reg.handler->Option.isSome || reg.contractRegister->Option.isSome) &&
358
+ !HandlerRegister.isDroppedByWhere(~config, reg)
359
+ )
360
+ if liveRegistrations->Utils.Array.isEmpty {
361
+ JsError.throwWithMessage(
362
+ `simulate: no handler runs for event "${eventName}" on contract "${contractName}"${switch config.chainMap
363
+ ->ChainMap.values
364
+ ->Array.length {
365
+ | 1 => ""
366
+ | _ => ` on chain ${chainId->Int.toString}`
367
+ }}. Register a handler with indexer.onEvent (and check any \`where\` filter isn't excluding this chain) before simulating it.`,
368
+ )
369
+ }
370
+ liveRegistrations->Array.forEach(reg => {
371
+ // Append into the registration array that the chain state will own and
372
+ // put that same registration object directly on the simulated item.
373
+ let onEventRegistrationIndex = onEventRegistrations->Array.length
374
+ let onEventRegistration = {...reg, index: onEventRegistrationIndex}
375
+ onEventRegistrations->Array.push(onEventRegistration)->ignore
376
+
377
+ items
378
+ ->Array.push(
379
+ Internal.Event({
380
+ onEventRegistration,
381
+ chain,
382
+ blockNumber,
383
+ logIndex,
384
+ // Simulate keeps the transaction inline on the payload, so the store
385
+ // key is unused.
386
+ transactionIndex: 0,
387
+ payload: (
388
+ {
389
+ contractName: eventConfig.contractName,
390
+ eventName: eventConfig.name,
391
+ params,
392
+ chainId,
393
+ srcAddress,
394
+ logIndex,
395
+ transaction,
396
+ block,
397
+ }: Evm.payload
398
+ )->Evm.fromPayload,
399
+ }),
400
+ )
401
+ ->ignore
402
+ })
382
403
 
383
404
  | _ =>
384
405
  JsError.throwWithMessage(`simulate: Invalid item. Each item must have "contract" and "event" fields.`)
@@ -1,5 +1,6 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as Utils from "./Utils.res.mjs";
3
4
  import * as Config from "./Config.res.mjs";
4
5
  import * as Address from "./Address.res.mjs";
5
6
  import * as ChainMap from "./ChainMap.res.mjs";
@@ -232,6 +233,7 @@ function parse(simulateItems, config, chainConfig, onEventRegistrations) {
232
233
  break;
233
234
  }
234
235
  let blockNumber = match$3[1];
236
+ let block$2 = match$3[0];
235
237
  let match$4 = config.ecosystem.name;
236
238
  let transaction;
237
239
  switch (match$4) {
@@ -253,29 +255,43 @@ function parse(simulateItems, config, chainConfig, onEventRegistrations) {
253
255
  } else {
254
256
  seenCoordinates[coordinate] = itemIndex;
255
257
  }
256
- let onEventRegistration = HandlerRegister.buildOnEventRegistration(config, chainId, eventConfig, undefined);
257
- let onEventRegistrationIndex = onEventRegistrations.length;
258
- let newrecord = {...onEventRegistration};
259
- newrecord.index = onEventRegistrationIndex;
260
- onEventRegistrations.push(newrecord);
261
- items.push({
262
- kind: 0,
263
- onEventRegistration: newrecord,
264
- chain: chain,
265
- blockNumber: blockNumber,
266
- logIndex: logIndex,
267
- transactionIndex: 0,
268
- payload: {
269
- contractName: eventConfig.contractName,
270
- eventName: eventConfig.name,
271
- params: params,
272
- chainId: chainId,
273
- srcAddress: srcAddress,
274
- logIndex: logIndex,
275
- transaction: Primitive_option.some(transaction),
276
- block: Primitive_option.some(match$3[0])
258
+ let liveRegistrations = HandlerRegister.getSimulateOnEventRegistrations(config, chainId, eventConfig).filter(reg => {
259
+ if (Stdlib_Option.isSome(reg.handler) || Stdlib_Option.isSome(reg.contractRegister)) {
260
+ return !HandlerRegister.isDroppedByWhere(config, reg);
261
+ } else {
262
+ return false;
277
263
  }
278
264
  });
265
+ if (Utils.$$Array.isEmpty(liveRegistrations)) {
266
+ let match$5 = ChainMap.values(config.chainMap).length;
267
+ Stdlib_JsError.throwWithMessage(`simulate: no handler runs for event "` + match$1 + `" on contract "` + match + `"` + (
268
+ match$5 !== 1 ? ` on chain ` + chainId.toString() : ""
269
+ ) + `. Register a handler with indexer.onEvent (and check any \`where\` filter isn't excluding this chain) before simulating it.`);
270
+ }
271
+ liveRegistrations.forEach(reg => {
272
+ let onEventRegistrationIndex = onEventRegistrations.length;
273
+ let newrecord = {...reg};
274
+ newrecord.index = onEventRegistrationIndex;
275
+ onEventRegistrations.push(newrecord);
276
+ items.push({
277
+ kind: 0,
278
+ onEventRegistration: newrecord,
279
+ chain: chain,
280
+ blockNumber: blockNumber,
281
+ logIndex: logIndex,
282
+ transactionIndex: 0,
283
+ payload: {
284
+ contractName: eventConfig.contractName,
285
+ eventName: eventConfig.name,
286
+ params: params,
287
+ chainId: chainId,
288
+ srcAddress: srcAddress,
289
+ logIndex: logIndex,
290
+ transaction: Primitive_option.some(transaction),
291
+ block: Primitive_option.some(block$2)
292
+ }
293
+ });
294
+ });
279
295
  });
280
296
  return items;
281
297
  }