envio 3.3.0-alpha.5 → 3.3.0-alpha.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "envio",
3
- "version": "3.3.0-alpha.5",
3
+ "version": "3.3.0-alpha.6",
4
4
  "type": "module",
5
5
  "description": "A latency and sync speed optimized, developer friendly blockchain data indexer.",
6
6
  "bin": "./bin.mjs",
@@ -69,10 +69,10 @@
69
69
  "tsx": "4.21.0"
70
70
  },
71
71
  "optionalDependencies": {
72
- "envio-linux-x64": "3.3.0-alpha.5",
73
- "envio-linux-x64-musl": "3.3.0-alpha.5",
74
- "envio-linux-arm64": "3.3.0-alpha.5",
75
- "envio-darwin-x64": "3.3.0-alpha.5",
76
- "envio-darwin-arm64": "3.3.0-alpha.5"
72
+ "envio-linux-x64": "3.3.0-alpha.6",
73
+ "envio-linux-x64-musl": "3.3.0-alpha.6",
74
+ "envio-linux-arm64": "3.3.0-alpha.6",
75
+ "envio-darwin-x64": "3.3.0-alpha.6",
76
+ "envio-darwin-arm64": "3.3.0-alpha.6"
77
77
  }
78
78
  }
@@ -127,6 +127,11 @@ and processNextBatch = async (state: IndexerState.t, ~scheduleFetch): unit => {
127
127
  | Ok() =>
128
128
  state->IndexerState.recordProcessedBatch
129
129
 
130
+ switch state->IndexerState.simulateDeadInputTracker {
131
+ | Some(tracker) => tracker->SimulateDeadInputTracker.recordProcessed(~batch)
132
+ | None => ()
133
+ }
134
+
130
135
  if state->IndexerState.isResolvingReorg {
131
136
  // A reorg landed while this batch was processing. Apply its progress so
132
137
  // the rollback diff is computed against up-to-date chain progress, but
@@ -10,8 +10,10 @@ import * as ErrorHandling from "./ErrorHandling.res.mjs";
10
10
  import * as InMemoryStore from "./InMemoryStore.res.mjs";
11
11
  import * as ExitOnCaughtUp from "./ExitOnCaughtUp.res.mjs";
12
12
  import * as EventProcessing from "./EventProcessing.res.mjs";
13
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
13
14
  import * as PruneStaleHistory from "./PruneStaleHistory.res.mjs";
14
15
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
16
+ import * as SimulateDeadInputTracker from "./SimulateDeadInputTracker.res.mjs";
15
17
 
16
18
  function yieldTick() {
17
19
  return new Promise((resolve, param) => {
@@ -55,6 +57,10 @@ async function processNextBatch(state, scheduleFetch) {
55
57
  return IndexerState.errorExit(state, errHandler._0);
56
58
  }
57
59
  IndexerState.recordProcessedBatch(state);
60
+ let tracker = IndexerState.simulateDeadInputTracker(state);
61
+ if (tracker !== undefined) {
62
+ SimulateDeadInputTracker.recordProcessed(Primitive_option.valFromOption(tracker), batch);
63
+ }
58
64
  if (IndexerState.isResolvingReorg(state)) {
59
65
  return IndexerState.applyBatchProgress(state, batch);
60
66
  }
@@ -4,7 +4,15 @@ let run = async (state: IndexerState.t) => {
4
4
  ChainMetadata.stage(state)
5
5
  await state->Writing.flush
6
6
  if !(state->IndexerState.isStopped) && !(state->IndexerState.isResolvingReorg) {
7
- Logging.info("Exiting with success")
8
- NodeJs.process->NodeJs.exitWithCode(Success)
7
+ // A simulate run fails here when a provided item never reached a handler —
8
+ // dead test code. The tracker is None (a no-op) off the simulate path.
9
+ switch state
10
+ ->IndexerState.simulateDeadInputTracker
11
+ ->Option.flatMap(SimulateDeadInputTracker.failureMessage) {
12
+ | None =>
13
+ Logging.info("Exiting with success")
14
+ NodeJs.process->NodeJs.exitWithCode(Success)
15
+ | Some(message) => state->IndexerState.errorExit(ErrorHandling.make(Utils.Error.make(message)))
16
+ }
9
17
  }
10
18
  }
@@ -5,11 +5,20 @@ import * as Writing from "./Writing.res.mjs";
5
5
  import * as Process from "process";
6
6
  import * as IndexerState from "./IndexerState.res.mjs";
7
7
  import * as ChainMetadata from "./ChainMetadata.res.mjs";
8
+ import * as ErrorHandling from "./ErrorHandling.res.mjs";
9
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
10
+ import * as SimulateDeadInputTracker from "./SimulateDeadInputTracker.res.mjs";
8
11
 
9
12
  async function run(state) {
10
13
  ChainMetadata.stage(state);
11
14
  await Writing.flush(state);
12
- if (!IndexerState.isStopped(state) && !IndexerState.isResolvingReorg(state)) {
15
+ if (IndexerState.isStopped(state) || IndexerState.isResolvingReorg(state)) {
16
+ return;
17
+ }
18
+ let message = Stdlib_Option.flatMap(IndexerState.simulateDeadInputTracker(state), SimulateDeadInputTracker.failureMessage);
19
+ if (message !== undefined) {
20
+ return IndexerState.errorExit(state, ErrorHandling.make(new Error(message), undefined, undefined));
21
+ } else {
13
22
  Logging.info("Exiting with success");
14
23
  Process.exit(0);
15
24
  return;
@@ -108,6 +108,8 @@ type t = {
108
108
  // waitForNewBlock waiter is bound to the old, pre-realtime source). A fetch
109
109
  // response or waiter carrying an older epoch than this is discarded.
110
110
  mutable epoch: int,
111
+ // None off the simulate path.
112
+ simulateDeadInputTracker: option<SimulateDeadInputTracker.t>,
111
113
  }
112
114
 
113
115
  let make = (
@@ -169,6 +171,7 @@ let make = (
169
171
  onError,
170
172
  isStopped: false,
171
173
  epoch: 0,
174
+ simulateDeadInputTracker: SimulateDeadInputTracker.makeFromConfig(config),
172
175
  }
173
176
  }
174
177
 
@@ -400,6 +403,7 @@ let exitAfterFirstEventBlock = (state: t) => state.exitAfterFirstEventBlock
400
403
  let isStopped = (state: t) => state.isStopped
401
404
  let epoch = (state: t) => state.epoch
402
405
  let pruneStaleEntityHistoryThrottler = (state: t) => state.writeThrottlers.pruneStaleEntityHistory
406
+ let simulateDeadInputTracker = (state: t) => state.simulateDeadInputTracker
403
407
 
404
408
  // --- Store domain operations. ---
405
409
 
@@ -18,6 +18,7 @@ import * as CrossChainState from "./CrossChainState.res.mjs";
18
18
  import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
19
19
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
20
20
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
21
+ import * as SimulateDeadInputTracker from "./SimulateDeadInputTracker.res.mjs";
21
22
 
22
23
  function make() {
23
24
  let intervalMillis = Env.ThrottleWrites.pruneStaleDataIntervalMillis;
@@ -91,7 +92,8 @@ function make$2(config, persistence, chainStates, isInReorgThreshold, isRealtime
91
92
  exitAfterFirstEventBlock: exitAfterFirstEventBlock,
92
93
  onError: onError,
93
94
  isStopped: false,
94
- epoch: 0
95
+ epoch: 0,
96
+ simulateDeadInputTracker: SimulateDeadInputTracker.makeFromConfig(config)
95
97
  };
96
98
  }
97
99
 
@@ -348,6 +350,10 @@ function pruneStaleEntityHistoryThrottler(state) {
348
350
  return state.writeThrottlers.pruneStaleEntityHistory;
349
351
  }
350
352
 
353
+ function simulateDeadInputTracker(state) {
354
+ return state.simulateDeadInputTracker;
355
+ }
356
+
351
357
  function queueProcessedBatch(state, batch) {
352
358
  state.processedBatches.push(batch);
353
359
  let checkpointId = Utils.$$Array.last(batch.checkpointIds);
@@ -527,6 +533,7 @@ export {
527
533
  isStopped,
528
534
  epoch,
529
535
  pruneStaleEntityHistoryThrottler,
536
+ simulateDeadInputTracker,
530
537
  queueProcessedBatch,
531
538
  drainBatchRun,
532
539
  takeRollback,
@@ -107,6 +107,7 @@ let exitAfterFirstEventBlock: t => bool
107
107
  let isStopped: t => bool
108
108
  let epoch: t => int
109
109
  let pruneStaleEntityHistoryThrottler: t => Throttler.t
110
+ let simulateDeadInputTracker: t => option<SimulateDeadInputTracker.t>
110
111
 
111
112
  // Store domain operations.
112
113
  let queueProcessedBatch: (t, ~batch: Batch.t) => unit
@@ -0,0 +1,85 @@
1
+ // Lives on IndexerState and is fed each processed batch, so ChainState and the
2
+ // fetch loop carry no simulate-specific state.
3
+
4
+ // Match a provided item to a processed one by its (chain, block, logIndex)
5
+ // coordinate rather than object identity, so matching survives any copy or
6
+ // transform of the item between the source and the batch.
7
+ let itemKey = (item: Internal.item): string =>
8
+ switch item {
9
+ | Internal.Event({chain, blockNumber, logIndex}) =>
10
+ `${chain
11
+ ->ChainMap.Chain.toChainId
12
+ ->Int.toString}:${blockNumber->Int.toString}:${logIndex->Int.toString}`
13
+ | _ => "" // non-event items are never a provided simulate input
14
+ }
15
+
16
+ // `index` is the item's position in its chain's `simulate` array, reported back
17
+ // so a user finds it without echoing its fields.
18
+ type entry = {chainId: int, index: int, key: string}
19
+
20
+ type t = {mutable unprocessed: array<entry>}
21
+
22
+ let makeFromConfig = (config: Config.t): option<t> => {
23
+ let entries =
24
+ config.chainMap
25
+ ->ChainMap.values
26
+ ->Array.flatMap(chainConfig =>
27
+ switch chainConfig.sourceConfig {
28
+ | Config.CustomSources(sources) =>
29
+ sources->Array.flatMap(source =>
30
+ source.simulateItems
31
+ ->Option.getOr([])
32
+ ->Array.mapWithIndex(
33
+ (item, index) => {
34
+ chainId: chainConfig.id,
35
+ index,
36
+ key: itemKey(item),
37
+ },
38
+ )
39
+ )
40
+ | _ => []
41
+ }
42
+ )
43
+ switch entries {
44
+ | [] => None
45
+ | _ => Some({unprocessed: entries})
46
+ }
47
+ }
48
+
49
+ let recordProcessed = (t: t, ~batch: Batch.t) => {
50
+ let processedKeys = batch.items->Array.map(itemKey)->Utils.Set.fromArray
51
+ t.unprocessed = t.unprocessed->Array.filter(entry => !(processedKeys->Utils.Set.has(entry.key)))
52
+ }
53
+
54
+ // Unrouted item indices grouped by chain, in the order chains were first seen.
55
+ let unroutedByChain = (t: t): array<(int, array<int>)> => {
56
+ let indicesByChain = Dict.make()
57
+ let chainOrder = []
58
+ t.unprocessed->Array.forEach(entry => {
59
+ let key = entry.chainId->Int.toString
60
+ if indicesByChain->Dict.get(key)->Option.isNone {
61
+ chainOrder->Array.push(entry.chainId)->ignore
62
+ }
63
+ indicesByChain->Utils.Dict.push(key, entry.index)
64
+ })
65
+ chainOrder->Array.map(chainId => (chainId, indicesByChain->Dict.getUnsafe(chainId->Int.toString)))
66
+ }
67
+
68
+ let failureMessage = (t: t): option<string> =>
69
+ switch t->unroutedByChain {
70
+ | [] => None
71
+ | byChain =>
72
+ let count = byChain->Array.reduce(0, (acc, (_chainId, indices)) => acc + indices->Array.length)
73
+ let itemWord = count === 1 ? "item" : "items"
74
+ let lines =
75
+ byChain
76
+ ->Array.map(((chainId, indices)) =>
77
+ ` - chain ${chainId->Int.toString}: ${indices
78
+ ->Array.map(index => index->Int.toString)
79
+ ->Array.join(", ")}`
80
+ )
81
+ ->Array.join("\n")
82
+ Some(
83
+ `simulate: ${count->Int.toString} ${itemWord} you passed to simulate never reached a handler, so nothing ran for them. Each was filtered out before the handler — usually a non-wildcard srcAddress that isn't indexed for the contract, or a where/block filter that excluded the event. Unrouted items, by index in each chain's simulate array:\n${lines}`,
84
+ )
85
+ }
@@ -0,0 +1,73 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Utils from "./Utils.res.mjs";
4
+ import * as ChainMap from "./ChainMap.res.mjs";
5
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
6
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
7
+
8
+ function itemKey(item) {
9
+ if (item.kind === 0) {
10
+ return item.chain.toString() + `:` + item.blockNumber.toString() + `:` + item.logIndex.toString();
11
+ } else {
12
+ return "";
13
+ }
14
+ }
15
+
16
+ function makeFromConfig(config) {
17
+ let entries = ChainMap.values(config.chainMap).flatMap(chainConfig => {
18
+ let sources = chainConfig.sourceConfig;
19
+ if (sources.TAG === "CustomSources") {
20
+ return sources._0.flatMap(source => Stdlib_Option.getOr(source.simulateItems, []).map((item, index) => ({
21
+ chainId: chainConfig.id,
22
+ index: index,
23
+ key: itemKey(item)
24
+ })));
25
+ } else {
26
+ return [];
27
+ }
28
+ });
29
+ if (entries.length !== 0) {
30
+ return {
31
+ unprocessed: entries
32
+ };
33
+ }
34
+ }
35
+
36
+ function recordProcessed(t, batch) {
37
+ let processedKeys = new Set(batch.items.map(itemKey));
38
+ t.unprocessed = t.unprocessed.filter(entry => !processedKeys.has(entry.key));
39
+ }
40
+
41
+ function unroutedByChain(t) {
42
+ let indicesByChain = {};
43
+ let chainOrder = [];
44
+ t.unprocessed.forEach(entry => {
45
+ let key = entry.chainId.toString();
46
+ if (Stdlib_Option.isNone(indicesByChain[key])) {
47
+ chainOrder.push(entry.chainId);
48
+ }
49
+ Utils.Dict.push(indicesByChain, key, entry.index);
50
+ });
51
+ return chainOrder.map(chainId => [
52
+ chainId,
53
+ indicesByChain[chainId.toString()]
54
+ ]);
55
+ }
56
+
57
+ function failureMessage(t) {
58
+ let byChain = unroutedByChain(t);
59
+ if (byChain.length === 0) {
60
+ return;
61
+ }
62
+ let count = Stdlib_Array.reduce(byChain, 0, (acc, param) => acc + param[1].length | 0);
63
+ let itemWord = count === 1 ? "item" : "items";
64
+ let lines = byChain.map(param => ` - chain ` + param[0].toString() + `: ` + param[1].map(index => index.toString()).join(", ")).join("\n");
65
+ return `simulate: ` + count.toString() + ` ` + itemWord + ` you passed to simulate never reached a handler, so nothing ran for them. Each was filtered out before the handler — usually a non-wildcard srcAddress that isn't indexed for the contract, or a where/block filter that excluded the event. Unrouted items, by index in each chain's simulate array:\n` + lines;
66
+ }
67
+
68
+ export {
69
+ makeFromConfig,
70
+ recordProcessed,
71
+ failureMessage,
72
+ }
73
+ /* Utils Not a pure module */
@@ -0,0 +1,12 @@
1
+ type t
2
+
3
+ // Builds a tracker from the run's config, seeded with every item each simulate
4
+ // source provided. None when no chain has a simulate source.
5
+ let makeFromConfig: Config.t => option<t>
6
+
7
+ // Drop the batch's items from the not-yet-routed set, matched by event coordinate.
8
+ let recordProcessed: (t, ~batch: Batch.t) => unit
9
+
10
+ // The error to fail the run with when items never reached a handler, listing them
11
+ // by chain and index within that chain's `simulate` array. None when all routed.
12
+ let failureMessage: t => option<string>
@@ -231,46 +231,6 @@ let deriveSrcAddress = (
231
231
  }
232
232
  }
233
233
 
234
- // A non-wildcard event is gated by `clientAddressFilter` on srcAddress ownership,
235
- // so a simulate item whose srcAddress isn't indexed on this chain would be
236
- // silently dropped (handler never runs). Fail loudly instead.
237
- //
238
- // `config` must already have handler registrations applied, otherwise an event
239
- // made wildcard purely through `indexer.onEvent({ wildcard: true })` reads back
240
- // as non-wildcard and gets wrongly rejected.
241
- let validateSrcAddresses = (
242
- ~simulateItems: array<JSON.t>,
243
- ~config: Config.t,
244
- ~chainConfig: Config.chain,
245
- ~indexingAddresses: array<Internal.indexingAddress>,
246
- ): unit => {
247
- let known = Utils.Set.make()
248
- indexingAddresses->Array.forEach(ia => known->Utils.Set.add(ia.address->Address.toString)->ignore)
249
- simulateItems->Array.forEach(rawJson => {
250
- let raw = rawJson->(Utils.magic: JSON.t => rawSimulateItem)
251
- switch (raw->getContract, raw->getEvent) {
252
- | (Some(contractName), Some(eventName)) =>
253
- switch findEventConfig(~config, ~contractName, ~eventName) {
254
- | Some(eventConfig) if !eventConfig.isWildcard =>
255
- let item = rawJson->(Utils.magic: JSON.t => Envio.evmSimulateItem)
256
- let srcAddress = deriveSrcAddress(
257
- ~providedSrcAddress=item.srcAddress,
258
- ~eventConfig,
259
- ~chainConfig,
260
- )
261
- if !(known->Utils.Set.has(srcAddress->Address.toString)) {
262
- JsError.throwWithMessage(
263
- `simulate: ${contractName}.${eventName} resolved to address ${srcAddress->Address.toString}, which isn't indexed on chain ${chainConfig.id->Int.toString}. ` ++
264
- `Provide a "srcAddress" configured or registered for ${contractName} on this chain, or use a wildcard event.`,
265
- )
266
- }
267
- | _ => ()
268
- }
269
- | _ => ()
270
- }
271
- })
272
- }
273
-
274
234
  let parse = (~simulateItems: array<JSON.t>, ~config: Config.t, ~chainConfig: Config.chain): array<
275
235
  Internal.item,
276
236
  > => {
@@ -281,8 +241,11 @@ let parse = (~simulateItems: array<JSON.t>, ~config: Config.t, ~chainConfig: Con
281
241
  let currentLogIndex = ref(0)
282
242
 
283
243
  let items = []
244
+ // Coordinate "block:logIndex" -> the index of the first item that claimed it,
245
+ // used to reject two items resolving to the same (block, logIndex).
246
+ let seenCoordinates = Dict.make()
284
247
 
285
- simulateItems->Array.forEach(rawJson => {
248
+ simulateItems->Array.forEachWithIndex((rawJson, itemIndex) => {
286
249
  let raw = rawJson->(Utils.magic: JSON.t => rawSimulateItem)
287
250
 
288
251
  switch (raw->getContract, raw->getEvent) {
@@ -306,8 +269,14 @@ let parse = (~simulateItems: array<JSON.t>, ~config: Config.t, ~chainConfig: Con
306
269
  }
307
270
  let params = paramsJson->S.convertOrThrow(eventConfig.simulateParamsSchema)
308
271
 
272
+ // An explicit logIndex advances the auto-increment counter past itself, so a
273
+ // later item that omits logIndex picks up after it instead of colliding.
309
274
  let logIndex = switch item.logIndex {
310
- | Some(li) => li
275
+ | Some(li) =>
276
+ if li >= currentLogIndex.contents {
277
+ currentLogIndex := li + 1
278
+ }
279
+ li
311
280
  | None =>
312
281
  let li = currentLogIndex.contents
313
282
  currentLogIndex := li + 1
@@ -345,6 +314,18 @@ let parse = (~simulateItems: array<JSON.t>, ~config: Config.t, ~chainConfig: Con
345
314
  // Update currentBlock for subsequent items
346
315
  currentBlock := blockNumber
347
316
 
317
+ // A simulate item must land on a distinct (block, logIndex): event ordering
318
+ // and the dead-input tracker both key on it. Catch explicit duplicates and
319
+ // explicit-vs-auto-increment collisions here, naming both offending indices.
320
+ let coordinate = `${blockNumber->Int.toString}:${logIndex->Int.toString}`
321
+ switch seenCoordinates->Dict.get(coordinate) {
322
+ | Some(firstIndex) =>
323
+ JsError.throwWithMessage(
324
+ `simulate: items at index ${firstIndex->Int.toString} and ${itemIndex->Int.toString} on chain ${chainId->Int.toString} both resolve to block ${blockNumber->Int.toString}, logIndex ${logIndex->Int.toString}. Give each item a distinct logIndex (or omit logIndex so they auto-increment).`,
325
+ )
326
+ | None => seenCoordinates->Dict.set(coordinate, itemIndex)
327
+ }
328
+
348
329
  items
349
330
  ->Array.push(
350
331
  Internal.Event({
@@ -165,34 +165,6 @@ function deriveSrcAddress(providedSrcAddress, eventConfig, chainConfig) {
165
165
  }
166
166
  }
167
167
 
168
- function validateSrcAddresses(simulateItems, config, chainConfig, indexingAddresses) {
169
- let known = new Set();
170
- indexingAddresses.forEach(ia => {
171
- known.add(ia.address);
172
- });
173
- simulateItems.forEach(rawJson => {
174
- let match = rawJson.contract;
175
- let match$1 = rawJson.event;
176
- if (match === undefined) {
177
- return;
178
- }
179
- if (match$1 === undefined) {
180
- return;
181
- }
182
- let eventConfig = findEventConfig(config, match, match$1);
183
- if (eventConfig === undefined) {
184
- return;
185
- }
186
- if (eventConfig.isWildcard) {
187
- return;
188
- }
189
- let srcAddress = deriveSrcAddress(rawJson.srcAddress, eventConfig, chainConfig);
190
- if (!known.has(srcAddress)) {
191
- return Stdlib_JsError.throwWithMessage(`simulate: ` + match + `.` + match$1 + ` resolved to address ` + srcAddress + `, which isn't indexed on chain ` + chainConfig.id.toString() + `. ` + (`Provide a "srcAddress" configured or registered for ` + match + ` on this chain, or use a wildcard event.`));
192
- }
193
- });
194
- }
195
-
196
168
  function parse(simulateItems, config, chainConfig) {
197
169
  let chain = ChainMap.Chain.makeUnsafe(chainConfig.id);
198
170
  let chainId = chainConfig.id;
@@ -204,7 +176,8 @@ function parse(simulateItems, config, chainConfig) {
204
176
  contents: 0
205
177
  };
206
178
  let items = [];
207
- simulateItems.forEach(rawJson => {
179
+ let seenCoordinates = {};
180
+ simulateItems.forEach((rawJson, itemIndex) => {
208
181
  let match = rawJson.contract;
209
182
  let match$1 = rawJson.event;
210
183
  if (match === undefined) {
@@ -221,6 +194,9 @@ function parse(simulateItems, config, chainConfig) {
221
194
  let li = rawJson.logIndex;
222
195
  let logIndex;
223
196
  if (li !== undefined) {
197
+ if (li >= currentLogIndex.contents) {
198
+ currentLogIndex.contents = li + 1 | 0;
199
+ }
224
200
  logIndex = li;
225
201
  } else {
226
202
  let li$1 = currentLogIndex.contents;
@@ -272,6 +248,13 @@ function parse(simulateItems, config, chainConfig) {
272
248
  break;
273
249
  }
274
250
  currentBlock.contents = blockNumber;
251
+ let coordinate = blockNumber.toString() + `:` + logIndex.toString();
252
+ let firstIndex = seenCoordinates[coordinate];
253
+ if (firstIndex !== undefined) {
254
+ Stdlib_JsError.throwWithMessage(`simulate: items at index ` + firstIndex.toString() + ` and ` + itemIndex.toString() + ` on chain ` + chainId.toString() + ` both resolve to block ` + blockNumber.toString() + `, logIndex ` + logIndex.toString() + `. Give each item a distinct logIndex (or omit logIndex so they auto-increment).`);
255
+ } else {
256
+ seenCoordinates[coordinate] = itemIndex;
257
+ }
275
258
  items.push({
276
259
  kind: 0,
277
260
  eventConfig: eventConfig,
@@ -360,7 +343,6 @@ export {
360
343
  dummySrcAddress,
361
344
  firstContractAddress,
362
345
  deriveSrcAddress,
363
- validateSrcAddresses,
364
346
  parse,
365
347
  patchConfig,
366
348
  }
@@ -667,11 +667,7 @@ let makeCreateTestIndexer = (~config: Config.t, ~workerPath: string): (
667
667
  Int.compare(aId, bId)
668
668
  })
669
669
 
670
- // Parse and validate the block ranges upfront before starting any
671
- // workers. srcAddress validation can't run here: it depends on which
672
- // events are wildcard, and `config` reflects config.yaml only —
673
- // handler-level `wildcard: true` takes effect only once registrations
674
- // are applied.
670
+ // Parse and validate the block ranges upfront before starting any workers.
675
671
  let chainEntries = sortedChainKeys->Array.map(chainIdStr => {
676
672
  let rawChainConfig = rawChains->Dict.getUnsafe(chainIdStr)
677
673
  let chainId = switch chainIdStr->Int.fromString {
@@ -868,26 +864,6 @@ let initTestWorker = () => {
868
864
  }
869
865
 
870
866
  let patchConfig = (config: Config.t, _registrations) => {
871
- // `config` here has handler registrations applied, so `isWildcard`
872
- // reflects `indexer.onEvent({ wildcard: true })` — which the main thread
873
- // can't see. Validate srcAddresses against it before patching in the
874
- // simulate source.
875
- switch simulate {
876
- | Some(simulateItems) =>
877
- let chainConfig = config.chainMap->ChainMap.get(ChainMap.Chain.makeUnsafe(~chainId))
878
- let indexingAddresses =
879
- initialState.chains
880
- ->Array.find(c => c.id == chainId)
881
- ->Option.mapOr([], c => c.indexingAddresses)
882
- SimulateItems.validateSrcAddresses(
883
- ~simulateItems,
884
- ~config,
885
- ~chainConfig,
886
- ~indexingAddresses,
887
- )
888
- | None => ()
889
- }
890
-
891
867
  let config = SimulateItems.patchConfig(~config, ~processConfig)
892
868
 
893
869
  // In auto-exit mode, set batchSize=1 to process one block checkpoint at a time
@@ -900,8 +876,8 @@ let initTestWorker = () => {
900
876
  Main.start(~persistence, ~isTest=true, ~patchConfig, ~exitAfterFirstEventBlock)
901
877
  ->Promise.catch(exn => {
902
878
  // `Main.start` rejects on any fatal error: a runtime failure arrives wrapped
903
- // in `Main.FatalError` (already logged), a setup throw (e.g. patchConfig's
904
- // srcAddress validation) arrives raw. The parent only learns of failures
879
+ // in `Main.FatalError` (already logged), a setup throw (e.g. an invalid
880
+ // simulate item) arrives raw. The parent only learns of failures
905
881
  // through the worker `error` event, which fires on an *uncaught* exception.
906
882
  // Throwing synchronously in this catch would just reject the catch's own
907
883
  // promise (swallowed by `ignore`); `setImmediate` re-throws outside the
@@ -586,11 +586,9 @@ function initTestWorker() {
586
586
  Process.exit(1);
587
587
  return;
588
588
  }
589
- let initialState = workerData.initialState;
590
589
  let simulate = workerData.simulate;
591
590
  let endBlock = workerData.endBlock;
592
- let chainId = workerData.chainId;
593
- let chainIdStr = chainId.toString();
591
+ let chainIdStr = workerData.chainId.toString();
594
592
  let exitAfterFirstEventBlock = Stdlib_Option.isNone(endBlock);
595
593
  let resolvedChainDict = {};
596
594
  resolvedChainDict["startBlock"] = workerData.startBlock;
@@ -605,7 +603,7 @@ function initTestWorker() {
605
603
  let processConfig = {
606
604
  chains: resolvedChainsDict
607
605
  };
608
- let proxy = TestIndexerProxyStorage.make(parentPort, initialState);
606
+ let proxy = TestIndexerProxyStorage.make(parentPort, workerData.initialState);
609
607
  let storage = TestIndexerProxyStorage.makeStorage(proxy);
610
608
  let config = Config.loadWithoutRegistrations();
611
609
  let persistence = Persistence.make(config.userEntities, config.allEnums, storage);
@@ -615,11 +613,6 @@ function initTestWorker() {
615
613
  Logging.setLogLevel("silent");
616
614
  }
617
615
  let patchConfig = (config, _registrations) => {
618
- if (simulate !== undefined) {
619
- let chainConfig = ChainMap.get(config.chainMap, ChainMap.Chain.makeUnsafe(chainId));
620
- let indexingAddresses = Stdlib_Option.mapOr(initialState.chains.find(c => c.id === chainId), [], c => c.indexingAddresses);
621
- SimulateItems.validateSrcAddresses(simulate, config, chainConfig, indexingAddresses);
622
- }
623
616
  let config$1 = SimulateItems.patchConfig(config, processConfig);
624
617
  if (exitAfterFirstEventBlock) {
625
618
  return {
@@ -32,7 +32,7 @@ let getSyncConfig = (
32
32
  intervalCeiling: Env.Configurable.SyncConfig.intervalCeiling->Option.getOr(
33
33
  intervalCeiling->Option.getOr(10_000),
34
34
  ),
35
- backoffMillis: backoffMillis->Option.getOr(5000),
35
+ backoffMillis: backoffMillis->Option.getOr(2000),
36
36
  queryTimeoutMillis,
37
37
  fallbackStallTimeout: fallbackStallTimeout->Option.getOr(queryTimeoutMillis / 2),
38
38
  pollingInterval: pollingInterval->Option.getOr(1000),
@@ -13,7 +13,7 @@ function getSyncConfig(param) {
13
13
  backoffMultiplicative: Stdlib_Option.getOr(Env.Configurable.SyncConfig.backoffMultiplicative, Stdlib_Option.getOr(param.backoffMultiplicative, 0.8)),
14
14
  accelerationAdditive: Stdlib_Option.getOr(Env.Configurable.SyncConfig.accelerationAdditive, Stdlib_Option.getOr(param.accelerationAdditive, 500)),
15
15
  intervalCeiling: Stdlib_Option.getOr(Env.Configurable.SyncConfig.intervalCeiling, Stdlib_Option.getOr(param.intervalCeiling, 10000)),
16
- backoffMillis: Stdlib_Option.getOr(param.backoffMillis, 5000),
16
+ backoffMillis: Stdlib_Option.getOr(param.backoffMillis, 2000),
17
17
  queryTimeoutMillis: queryTimeoutMillis,
18
18
  fallbackStallTimeout: Stdlib_Option.getOr(param.fallbackStallTimeout, queryTimeoutMillis / 2 | 0),
19
19
  pollingInterval: Stdlib_Option.getOr(param.pollingInterval, 1000)