envio 3.3.2 → 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 +16 -1
  24. package/src/PgStorage.res.mjs +9 -1
  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
@@ -849,9 +849,24 @@ let rec writeBatch = async (
849
849
  let specificError = ref(None)
850
850
 
851
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()
852
856
  let rows = batch.items->Array.filterMap(item =>
853
857
  switch item {
854
- | 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
+ }
855
870
  | Internal.Block(_) => None
856
871
  }
857
872
  )
@@ -607,8 +607,16 @@ async function writeBatch(sql, batch, pgSchema, rollback, isInReorgThreshold, co
607
607
  };
608
608
  let rawEvents;
609
609
  if (config.enableRawEvents) {
610
+ let seenLogCoordinates = new Set();
610
611
  let rows = Stdlib_Array.filterMap(batch.items, item => {
611
- 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);
612
620
  return config.ecosystem.toRawEvent(item);
613
621
  }
614
622
  });
@@ -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
  }