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
@@ -1,19 +1,7 @@
1
- type eventRegistration = {
2
- handler: option<Internal.handler>,
3
- contractRegister: option<Internal.contractRegister>,
4
- eventOptions: option<Internal.eventOptions<JSON.t>>,
5
- }
6
-
7
- let empty = {
8
- handler: None,
9
- contractRegister: None,
10
- eventOptions: None,
11
- }
12
-
13
- // Per-chain onEventRegistrations built from the event definitions in
14
- // `Config.t` plus whatever handler/contractRegister/eventOptions got
15
- // registered for them, and the onBlock registrations collected during
16
- // registration.
1
+ // Per-chain onEvent + onBlock registrations. Used both as the live registration
2
+ // store (`activeRegistration`, where onEvent regs are raw: registration order,
3
+ // unmerged, unindexed, `where:false` ones kept) and as the finished output of
4
+ // `finishRegistration` (merged, backfilled, indexed).
17
5
  type chainRegistrations = {
18
6
  onEventRegistrations: array<Internal.onEventRegistration>,
19
7
  onBlockRegistrations: array<Internal.onBlockRegistration>,
@@ -22,38 +10,24 @@ type chainRegistrations = {
22
10
  // The finished registration state returned by `finishRegistration`.
23
11
  type registrationsByChainId = dict<chainRegistrations>
24
12
 
25
- // Incrementally built during registration: every `indexer.onEvent` /
26
- // `.contractRegister` call resolves its `where` per chain right away and
27
- // stores the resulting registration here, keyed by "Contract.Event", so
28
- // invalid configuration throws at the user's registration call site.
29
- type pendingChainRegistrations = {
30
- onEventRegistrations: dict<Internal.onEventRegistration>,
31
- onBlockRegistrations: array<Internal.onBlockRegistration>,
32
- }
33
-
13
+ // The one registration, resolved once and reused. Handlers register into it at
14
+ // `onEvent`/`onBlock` call time (resolving for every chain in `config`, which is
15
+ // the full chain set), and it persists in `EnvioGlobal` across the many
16
+ // `finishRegistration` calls a single isolate makes (handler modules are
17
+ // import-cached and register only once). `finishRegistration` reads it and
18
+ // builds a fresh per-config output, never mutating the store.
34
19
  type activeRegistration = {
35
20
  config: Config.t,
36
- registrationsByChainId: dict<pendingChainRegistrations>,
21
+ registrationsByChainId: dict<chainRegistrations>,
37
22
  mutable finished: bool,
38
23
  }
39
24
 
40
- // Registration state lives in the process-wide `EnvioGlobal` record (shared
41
- // across duplicate envio module instances); the slots are opaque there, so
42
- // cast them to the real types once here.
43
- let eventRegistrations =
44
- EnvioGlobal.value.eventRegistrations->(Utils.magic: dict<unknown> => dict<eventRegistration>)
45
-
46
25
  let getKey = (~contractName, ~eventName) => contractName ++ "." ++ eventName
47
26
 
48
- let get = (~contractName, ~eventName) => {
49
- switch eventRegistrations->Utils.Dict.dangerouslyGetNonOption(getKey(~contractName, ~eventName)) {
50
- | Some(existing) => existing
51
- | None => empty
52
- }
53
- }
54
-
55
- let set = (~contractName, ~eventName, registration) => {
56
- eventRegistrations->Dict.set(getKey(~contractName, ~eventName), registration)
27
+ // Test-only: reset to fresh-import state so a new registration cycle starts
28
+ // empty (production starts each isolate empty and registers once).
29
+ let resetOnEventRegistrations = () => {
30
+ EnvioGlobal.value.activeRegistration = None
57
31
  }
58
32
 
59
33
  let getActiveRegistration = () =>
@@ -85,29 +59,36 @@ let withRegistration = (fn: activeRegistration => unit) => {
85
59
  }
86
60
  }
87
61
 
62
+ // Idempotent: handlers register once (import-cached), so the first call builds
63
+ // the registration and every later call reuses it. `config` must be the full
64
+ // chain set — registrations resolve for all its chains here, and
65
+ // `finishRegistration` later narrows to whatever config it's given.
88
66
  let startRegistration = (~config: Config.t) => {
89
- let r = {
90
- config,
91
- registrationsByChainId: Dict.make(),
92
- finished: false,
93
- }
94
- EnvioGlobal.value.activeRegistration = Some(r->(Utils.magic: activeRegistration => unknown))
95
- while preRegistered->Array.length > 0 {
96
- // Loop + cleanup in one go
97
- switch preRegistered->Array.pop {
98
- | Some(fn) => fn(r)
99
- | None => ()
67
+ switch getActiveRegistration() {
68
+ | Some(_) => ()
69
+ | None =>
70
+ let r = {
71
+ config,
72
+ registrationsByChainId: Dict.make(),
73
+ finished: false,
100
74
  }
75
+ EnvioGlobal.value.activeRegistration = Some(r->(Utils.magic: activeRegistration => unknown))
76
+ // Replay pre-registered callbacks in source (FIFO) order, then clear. For
77
+ // multiple handlers on one event this replay order is the dispatch order, so
78
+ // it must not reverse (which `Array.pop` would).
79
+ let queued = preRegistered->Array.copy
80
+ preRegistered->Array.splice(~start=0, ~remove=preRegistered->Array.length, ~insert=[])
81
+ queued->Array.forEach(fn => fn(r))
101
82
  }
102
83
  }
103
84
 
104
- let getPendingChainRegistrations = (r: activeRegistration, ~chainId: int) => {
85
+ let getChainRegistrations = (r: activeRegistration, ~chainId: int): chainRegistrations => {
105
86
  let key = chainId->Int.toString
106
87
  switch r.registrationsByChainId->Utils.Dict.dangerouslyGetNonOption(key) {
107
- | Some(pending) => pending
88
+ | Some(existing) => existing
108
89
  | None =>
109
- let fresh = {
110
- onEventRegistrations: Dict.make(),
90
+ let fresh: chainRegistrations = {
91
+ onEventRegistrations: [],
111
92
  onBlockRegistrations: [],
112
93
  }
113
94
  r.registrationsByChainId->Dict.set(key, fresh)
@@ -158,268 +139,367 @@ let buildOnEventRegistrationWith = (
158
139
  }
159
140
  }
160
141
 
161
- // Enrich one event definition into its (event, chain) registration using
162
- // whatever handler/contractRegister/where the user registered for it. Shared
163
- // by the incremental per-chain sync below, `simulate`, and test helpers so
164
- // they stay in sync instead of re-deriving the per-ecosystem dispatch each
165
- // place.
166
- let buildOnEventRegistration = (
167
- ~config: Config.t,
168
- ~chainId: int,
169
- ~eventConfig: Internal.eventConfig,
170
- ~startBlock=?,
171
- ): Internal.onEventRegistration => {
172
- let t = get(~contractName=eventConfig.contractName, ~eventName=eventConfig.name)
173
- buildOnEventRegistrationWith(
174
- ~config,
175
- ~chainId,
176
- ~eventConfig,
177
- ~isWildcard=t.eventOptions->Option.flatMap(v => v.wildcard)->Option.getOr(false),
178
- ~handler=t.handler,
179
- ~contractRegister=t.contractRegister,
180
- ~where=t.eventOptions->Option.flatMap(v => v.where),
181
- ~startBlock?,
182
- )
183
- }
184
-
185
- let getHandler = (~contractName, ~eventName) => get(~contractName, ~eventName).handler
186
-
187
- let getContractRegister = (~contractName, ~eventName) =>
188
- get(~contractName, ~eventName).contractRegister
189
-
190
- let isWildcard = (~contractName, ~eventName) =>
191
- get(~contractName, ~eventName).eventOptions
192
- ->Option.flatMap(value => value.wildcard)
193
- ->Option.getOr(false)
194
-
195
- let hasRegistration = (~contractName, ~eventName) => {
196
- let r = get(~contractName, ~eventName)
197
- r.handler->Option.isSome || r.contractRegister->Option.isSome
198
- }
199
-
200
- type eventNamespace = {contractName: string, eventName: string}
201
-
202
- let raiseDuplicateRegistration = (~contractName, ~eventName, ~msg, ~logger) => {
203
- let fullMsg = msg ++ " for " ++ contractName ++ "." ++ eventName
204
- Logging.createChildFrom(~logger, ~params={contractName, eventName})->Logging.childError(fullMsg)
205
- JsError.throwWithMessage(fullMsg)
206
- }
207
-
208
- // `where` equality is checked per chain on the resolved structure (see
209
- // `syncOnEventRegistrations`), so registration options only need to agree on
210
- // `wildcard` here — two callbacks that resolve to identical filters count as
211
- // identical options even when the function references differ.
212
- let eventOptionsMatch = (
213
- existing: option<Internal.eventOptions<JSON.t>>,
214
- incoming: option<Internal.eventOptions<JSON.t>>,
215
- ) => {
216
- switch (existing, incoming) {
217
- | (None, None) => true
218
- | (Some(a), Some(b)) => a.wildcard === b.wildcard
219
- | _ => false
220
- }
221
- }
222
-
223
142
  let getResolvedWhere = (reg: Internal.onEventRegistration) =>
224
143
  (
225
144
  reg->(Utils.magic: Internal.onEventRegistration => Internal.evmOnEventRegistration)
226
145
  ).resolvedWhere
227
146
 
228
- // Resolve the registration for every configured chain that defines the event
229
- // and store it in the pending per-chain registry. When the chain already
230
- // holds a registration for this event, the resolved `where` structures must
231
- // deep-compare equal (`Values` by hex arrays, `ContractAddresses` by contract
232
- // name, plus `startBlock`) otherwise it's a conflicting duplicate
233
- // registration. Both live registrations and `preRegistered` callbacks
234
- // replayed by `startRegistration` run through this single code path.
235
- let syncOnEventRegistrations = (
236
- r: activeRegistration,
237
- ~contractName,
238
- ~eventName,
239
- ~where: option<JSON.t>,
240
- ~duplicateMsg,
241
- ~logger,
242
- ) => {
243
- let config = r.config
244
- let t = get(~contractName, ~eventName)
245
- let isWildcard = t.eventOptions->Option.flatMap(v => v.wildcard)->Option.getOr(false)
246
- let key = getKey(~contractName, ~eventName)
147
+ // Two chain registrations target the same fetched log when they share the
148
+ // event, the wildcard flag, and (on EVM) the resolved `where` a handler and
149
+ // a contractRegister that agree on all three can be merged into one
150
+ // registration (so one item per log runs both). `where` is compared on the
151
+ // resolved structure (`Values` by hex arrays, `ContractAddresses` by contract
152
+ // name, plus `startBlock`); differing filters stay separate registrations.
153
+ let sameEventAndFilter = (
154
+ a: Internal.onEventRegistration,
155
+ b: Internal.onEventRegistration,
156
+ ~config: Config.t,
157
+ ) =>
158
+ a.eventConfig.contractName === b.eventConfig.contractName &&
159
+ a.eventConfig.name === b.eventConfig.name &&
160
+ a.isWildcard === b.isWildcard &&
161
+ switch config.ecosystem.name {
162
+ | Evm => getResolvedWhere(a) == getResolvedWhere(b)
163
+ | Fuel | Svm => true
164
+ }
247
165
 
248
- config.chainMap
249
- ->ChainMap.values
250
- ->Array.forEach(chainConfig => {
251
- chainConfig.contracts->Array.forEach(contract => {
252
- if contract.name === contractName {
253
- switch contract.events->Array.find(e => e.name === eventName) {
254
- | None => ()
255
- | Some(eventConfig) =>
256
- let newRegistration = buildOnEventRegistrationWith(
257
- ~config,
258
- ~chainId=chainConfig.id,
259
- ~eventConfig,
260
- ~isWildcard,
261
- ~handler=t.handler,
262
- ~contractRegister=t.contractRegister,
263
- ~where,
264
- ~startBlock=?contract.startBlock,
265
- )
266
- let pending = r->getPendingChainRegistrations(~chainId=chainConfig.id)
267
- switch pending.onEventRegistrations->Utils.Dict.dangerouslyGetNonOption(key) {
268
- | Some(existing) if config.ecosystem.name === Evm =>
269
- if !(existing->getResolvedWhere == newRegistration->getResolvedWhere) {
270
- raiseDuplicateRegistration(~contractName, ~eventName, ~msg=duplicateMsg, ~logger)
271
- }
272
- | _ => ()
273
- }
274
- pending.onEventRegistrations->Dict.set(key, newRegistration)
275
- }
166
+ // Merge each contractRegister into a matching handler registration (either
167
+ // registration order; the merged registration takes the handler's slot so
168
+ // dispatch order follows handler registration order). Two handlers (or two
169
+ // contractRegisters) for one event never merge. Operates on the raw per-chain
170
+ // registrations stored at `onEvent` time; shared by `finishRegistration` and
171
+ // simulate so both see the same registrations.
172
+ let mergeRegistrations = (resolved: array<Internal.onEventRegistration>, ~config: Config.t): array<
173
+ Internal.onEventRegistration,
174
+ > => {
175
+ let merged: ref<array<Internal.onEventRegistration>> = ref([])
176
+ resolved->Array.forEach((reg: Internal.onEventRegistration) => {
177
+ if reg.handler->Option.isSome {
178
+ // A handler absorbs a matching contractRegister-only registration,
179
+ // dropping it and taking its own (handler) slot.
180
+ switch merged.contents->Array.findIndex(m =>
181
+ m.handler->Option.isNone &&
182
+ m.contractRegister->Option.isSome &&
183
+ sameEventAndFilter(m, reg, ~config)
184
+ ) {
185
+ | -1 => merged := merged.contents->Array.concat([reg])
186
+ | i =>
187
+ let target = merged.contents->Array.getUnsafe(i)
188
+ merged :=
189
+ merged.contents
190
+ ->Array.filterWithIndex((_, j) => j !== i)
191
+ ->Array.concat([{...reg, contractRegister: target.contractRegister}])
276
192
  }
277
- })
193
+ } else {
194
+ // A contractRegister merges into a matching handler registration,
195
+ // keeping the handler's slot.
196
+ switch merged.contents->Array.findIndex(m =>
197
+ m.handler->Option.isSome &&
198
+ m.contractRegister->Option.isNone &&
199
+ sameEventAndFilter(m, reg, ~config)
200
+ ) {
201
+ | -1 => merged := merged.contents->Array.concat([reg])
202
+ | i =>
203
+ let target = merged.contents->Array.getUnsafe(i)
204
+ let next = merged.contents->Array.copy
205
+ next->Array.setUnsafe(i, {...target, contractRegister: reg.contractRegister})
206
+ merged := next
207
+ }
208
+ }
278
209
  })
210
+ merged.contents
279
211
  }
280
212
 
281
- let setEventOptions = (~contractName, ~eventName, ~eventOptions, ~logger=Logging.getLogger()) => {
282
- switch eventOptions {
283
- | Some(value) =>
284
- let value = value->(Utils.magic: Internal.eventOptions<'where> => Internal.eventOptions<JSON.t>)
285
- let t = get(~contractName, ~eventName)
286
- switch t.eventOptions {
287
- | None => set(~contractName, ~eventName, {...t, eventOptions: Some(value)})
288
- | Some(existingValue) =>
289
- if !eventOptionsMatch(Some(existingValue), Some(value)) {
290
- raiseDuplicateRegistration(
291
- ~contractName,
292
- ~eventName,
293
- ~msg="Cannot register handler with different options. Make sure all handlers for the same event use identical options (wildcard, where)",
294
- ~logger,
213
+ // A `where` that resolved to no topic selections (`false` for this chain)
214
+ // should never be fetched or dispatched here — drop it. Only meaningful on EVM.
215
+ let isDroppedByWhere = (~config: Config.t, reg: Internal.onEventRegistration) =>
216
+ config.ecosystem.name === Evm && (reg->getResolvedWhere).topicSelections->Utils.Array.isEmpty
217
+
218
+ // Resolve one `onEvent`/`contractRegister` call into a registration for every
219
+ // chain in the config (the full chain set) and store it in registration order.
220
+ // Building runs the user's `where` callback here — once per chain — so a broken
221
+ // filter throws at the call site. A chain that doesn't define the event is
222
+ // skipped.
223
+ let addOnEventRegistration = (
224
+ registration: activeRegistration,
225
+ ~contractName,
226
+ ~eventName,
227
+ ~handler: option<Internal.handler>,
228
+ ~contractRegister: option<Internal.contractRegister>,
229
+ ~eventOptions: option<Internal.eventOptions<JSON.t>>,
230
+ ) => {
231
+ let isWildcard = eventOptions->Option.flatMap(v => v.wildcard)->Option.getOr(false)
232
+ let where = eventOptions->Option.flatMap(v => v.where)
233
+ registration.config.chainMap
234
+ ->ChainMap.values
235
+ ->Array.forEach(chainConfig =>
236
+ switch chainConfig.contracts->Array.find(c => c.name === contractName) {
237
+ | None => ()
238
+ | Some(contract) =>
239
+ switch contract.events->Array.find(e => e.name === eventName) {
240
+ | None => ()
241
+ | Some(eventConfig) =>
242
+ let reg = buildOnEventRegistrationWith(
243
+ ~config=registration.config,
244
+ ~chainId=chainConfig.id,
245
+ ~eventConfig,
246
+ ~isWildcard,
247
+ ~handler,
248
+ ~contractRegister,
249
+ ~where,
250
+ ~startBlock=?contract.startBlock,
295
251
  )
252
+ (registration->getChainRegistrations(~chainId=chainConfig.id)).onEventRegistrations
253
+ ->Array.push(reg)
254
+ ->ignore
296
255
  }
297
256
  }
298
- | None => ()
299
- }
257
+ )
300
258
  }
301
259
 
302
- let setHandler = (
303
- ~contractName,
304
- ~eventName,
305
- handler,
306
- ~eventOptions,
307
- ~logger=Logging.getLogger(),
308
- ) => {
260
+ let setHandler = (~contractName, ~eventName, handler, ~eventOptions) => {
309
261
  withRegistration(registration => {
310
- let t = get(~contractName, ~eventName)
311
262
  let newHandler = handler->(Utils.magic: Internal.genericHandler<'args> => Internal.handler)
312
- let incomingEventOptions =
263
+ let eventOptions =
313
264
  eventOptions->Option.map(v =>
314
265
  v->(Utils.magic: Internal.eventOptions<'where> => Internal.eventOptions<JSON.t>)
315
266
  )
316
- switch t.handler {
317
- | None =>
318
- setEventOptions(~contractName, ~eventName, ~eventOptions, ~logger)
319
- let t = get(~contractName, ~eventName)
320
- set(
321
- ~contractName,
322
- ~eventName,
323
- {
324
- ...t,
325
- handler: Some(newHandler),
326
- },
327
- )
328
- | Some(prevHandler) =>
329
- if eventOptionsMatch(t.eventOptions, incomingEventOptions) {
330
- let composedHandler: Internal.handler = async args => {
331
- await prevHandler(args)
332
- await newHandler(args)
333
- }
334
- set(
335
- ~contractName,
336
- ~eventName,
337
- {
338
- ...t,
339
- handler: Some(composedHandler),
340
- },
341
- )
342
- } else {
343
- raiseDuplicateRegistration(
344
- ~contractName,
345
- ~eventName,
346
- ~msg="Cannot register a second handler with different options. Make sure all handlers for the same event use identical options (wildcard, where)",
347
- ~logger,
348
- )
349
- }
350
- }
351
- registration->syncOnEventRegistrations(
267
+ registration->addOnEventRegistration(
352
268
  ~contractName,
353
269
  ~eventName,
354
- ~where=incomingEventOptions->Option.flatMap(v => v.where),
355
- ~duplicateMsg="Cannot register a second handler with different options. Make sure all handlers for the same event use identical options (wildcard, where)",
356
- ~logger,
270
+ ~handler=Some(newHandler),
271
+ ~contractRegister=None,
272
+ ~eventOptions,
357
273
  )
358
274
  })
359
275
  }
360
276
 
361
- let setContractRegister = (
362
- ~contractName,
363
- ~eventName,
364
- contractRegister,
365
- ~eventOptions,
366
- ~logger=Logging.getLogger(),
367
- ) => {
277
+ let setContractRegister = (~contractName, ~eventName, contractRegister, ~eventOptions) => {
368
278
  withRegistration(registration => {
369
- let t = get(~contractName, ~eventName)
370
279
  let newContractRegister =
371
280
  contractRegister->(
372
281
  Utils.magic: Internal.genericContractRegister<
373
282
  Internal.genericContractRegisterArgs<'event, 'context>,
374
283
  > => Internal.contractRegister
375
284
  )
376
- let incomingEventOptions =
285
+ let eventOptions =
377
286
  eventOptions->Option.map(v =>
378
287
  v->(Utils.magic: Internal.eventOptions<'where> => Internal.eventOptions<JSON.t>)
379
288
  )
380
- switch t.contractRegister {
381
- | None =>
382
- setEventOptions(~contractName, ~eventName, ~eventOptions, ~logger)
383
- let t = get(~contractName, ~eventName)
384
- set(
385
- ~contractName,
386
- ~eventName,
387
- {
388
- ...t,
389
- contractRegister: Some(newContractRegister),
390
- },
289
+ registration->addOnEventRegistration(
290
+ ~contractName,
291
+ ~eventName,
292
+ ~handler=None,
293
+ ~contractRegister=Some(newContractRegister),
294
+ ~eventOptions,
295
+ )
296
+ })
297
+ }
298
+
299
+ // Raw onEvent registrations stored for a chain (empty if the chain has none).
300
+ let storedOnEventRegistrations = (r: activeRegistration, ~chainId: int): array<
301
+ Internal.onEventRegistration,
302
+ > =>
303
+ switch r.registrationsByChainId->Utils.Dict.dangerouslyGetNonOption(chainId->Int.toString) {
304
+ | Some(chainRegs) => chainRegs.onEventRegistrations
305
+ | None => []
306
+ }
307
+
308
+ // True when any registration for the event is a wildcard. Used by simulate to
309
+ // decide whether a src address needs deriving.
310
+ let isWildcard = (~contractName, ~eventName) =>
311
+ switch getActiveRegistration() {
312
+ | Some(r) =>
313
+ r.registrationsByChainId
314
+ ->Dict.valuesToArray
315
+ ->Array.some(chainRegs =>
316
+ chainRegs.onEventRegistrations->Array.some(reg =>
317
+ reg.eventConfig.contractName === contractName &&
318
+ reg.eventConfig.name === eventName &&
319
+ reg.isWildcard
391
320
  )
392
- | Some(prevContractRegister) =>
393
- if eventOptionsMatch(t.eventOptions, incomingEventOptions) {
394
- let composedContractRegister: Internal.contractRegister = async args => {
395
- await prevContractRegister(args)
396
- await newContractRegister(args)
397
- }
398
- set(
399
- ~contractName,
400
- ~eventName,
321
+ )
322
+ | None => false
323
+ }
324
+
325
+ // Every registration for one event on a chain, so simulate fans a simulated
326
+ // event out to each the way real routing does. Falls back to a bare
327
+ // registration when the event has no handler/contractRegister, so a simulated
328
+ // item still produces an item to run.
329
+ let getSimulateOnEventRegistrations = (
330
+ ~config: Config.t,
331
+ ~chainId: int,
332
+ ~eventConfig: Internal.eventConfig,
333
+ ): array<Internal.onEventRegistration> => {
334
+ let stored = switch getActiveRegistration() {
335
+ | Some(r) => r->storedOnEventRegistrations(~chainId)
336
+ | None => []
337
+ }
338
+ let matching =
339
+ mergeRegistrations(stored, ~config)->Array.filter(reg =>
340
+ reg.eventConfig.contractName === eventConfig.contractName &&
341
+ reg.eventConfig.name === eventConfig.name
342
+ )
343
+ if matching->Utils.Array.notEmpty {
344
+ matching
345
+ } else {
346
+ [
347
+ buildOnEventRegistrationWith(
348
+ ~config,
349
+ ~chainId,
350
+ ~eventConfig,
351
+ ~isWildcard=false,
352
+ ~handler=None,
353
+ ~contractRegister=None,
354
+ ~where=None,
355
+ ),
356
+ ]
357
+ }
358
+ }
359
+
360
+ let finishRegistration = (~config: Config.t): registrationsByChainId => {
361
+ switch getActiveRegistration() {
362
+ | Some(r) => {
363
+ r.finished = true
364
+ let notRegisteredEventsByContract: dict<Utils.Set.t<string>> = Dict.make()
365
+ let registrationsByChainId: registrationsByChainId = Dict.make()
366
+ config.chainMap
367
+ ->ChainMap.values
368
+ ->Array.forEach(chainConfig => {
369
+ let chainId = chainConfig.id
370
+ let key = chainId->Int.toString
371
+
372
+ let builtRegs = mergeRegistrations(r->storedOnEventRegistrations(~chainId), ~config)
373
+ let registeredKeys = Utils.Set.make()
374
+ builtRegs->Array.forEach(reg =>
375
+ registeredKeys
376
+ ->Utils.Set.add(
377
+ getKey(~contractName=reg.eventConfig.contractName, ~eventName=reg.eventConfig.name),
378
+ )
379
+ ->ignore
380
+ )
381
+
382
+ // Events with no handler/contractRegister aren't fetched or dispatched
383
+ // unless raw events are enabled, in which case a bare registration is
384
+ // added to fetch them. Otherwise they're reported once below. Keyed on
385
+ // the resolved registrations (before the where-empty drop) so a
386
+ // `where: false` event still counts as registered — its handler opted
387
+ // out of this chain, so it gets no raw-event registration either.
388
+ let rawEventRegs = []
389
+ chainConfig.contracts->Array.forEach(contract => {
390
+ contract.events->Array.forEach(
391
+ eventConfig => {
392
+ if (
393
+ !(
394
+ registeredKeys->Utils.Set.has(
395
+ getKey(~contractName=contract.name, ~eventName=eventConfig.name),
396
+ )
397
+ )
398
+ ) {
399
+ if config.enableRawEvents {
400
+ rawEventRegs
401
+ ->Array.push(
402
+ buildOnEventRegistrationWith(
403
+ ~config,
404
+ ~chainId,
405
+ ~eventConfig,
406
+ ~isWildcard=false,
407
+ ~handler=None,
408
+ ~contractRegister=None,
409
+ ~where=None,
410
+ ~startBlock=?contract.startBlock,
411
+ ),
412
+ )
413
+ ->ignore
414
+ } else {
415
+ let eventNames = switch notRegisteredEventsByContract->Utils.Dict.dangerouslyGetNonOption(
416
+ contract.name,
417
+ ) {
418
+ | Some(set) => set
419
+ | None => {
420
+ let set = Utils.Set.make()
421
+ notRegisteredEventsByContract->Dict.set(contract.name, set)
422
+ set
423
+ }
424
+ }
425
+ eventNames->Utils.Set.add(eventConfig.name)->ignore
426
+ }
427
+ }
428
+ },
429
+ )
430
+ })
431
+
432
+ // Drop registrations whose `where` opts out of this chain, then assign
433
+ // each survivor its chain-scoped index by position.
434
+ let onEventRegistrations: array<Internal.onEventRegistration> = []
435
+ builtRegs
436
+ ->Array.concat(rawEventRegs)
437
+ ->Array.forEach(reg => {
438
+ if !isDroppedByWhere(~config, reg) {
439
+ onEventRegistrations
440
+ ->Array.push({...reg, index: onEventRegistrations->Array.length})
441
+ ->ignore
442
+ }
443
+ })
444
+
445
+ registrationsByChainId->Dict.set(
446
+ key,
401
447
  {
402
- ...t,
403
- contractRegister: Some(composedContractRegister),
448
+ onEventRegistrations,
449
+ // Copy so a consumer appending to the output (e.g. simulate source
450
+ // registration) never mutates the persistent store.
451
+ onBlockRegistrations: switch r.registrationsByChainId->Utils.Dict.dangerouslyGetNonOption(
452
+ key,
453
+ ) {
454
+ | Some(chainRegs) => chainRegs.onBlockRegistrations->Array.copy
455
+ | None => []
456
+ },
404
457
  },
405
458
  )
406
- } else {
407
- raiseDuplicateRegistration(
408
- ~contractName,
409
- ~eventName,
410
- ~msg="Cannot register a second contractRegister with different options. Make sure all handlers for the same event use identical options (wildcard, where)",
411
- ~logger,
459
+ })
460
+
461
+ // Reported once for the whole indexer (a shared contract on multiple
462
+ // chains would otherwise repeat the same message per chain).
463
+ let notRegisteredEntries = notRegisteredEventsByContract->Dict.toArray
464
+ if notRegisteredEntries->Utils.Array.notEmpty {
465
+ let groups =
466
+ notRegisteredEntries
467
+ ->Array.map(((contractName, eventNames)) =>
468
+ `${contractName} (${eventNames->Utils.Set.toArray->Array.joinUnsafe(", ")})`
469
+ )
470
+ ->Array.joinUnsafe(", ")
471
+ Logging.getLogger()->Logging.childInfo(
472
+ `Events without a handler, skipped for indexing: ${groups}`,
412
473
  )
413
474
  }
475
+
476
+ registrationsByChainId
414
477
  }
415
- registration->syncOnEventRegistrations(
416
- ~contractName,
417
- ~eventName,
418
- ~where=incomingEventOptions->Option.flatMap(v => v.where),
419
- ~duplicateMsg="Cannot register a second contractRegister with different options. Make sure all handlers for the same event use identical options (wildcard, where)",
420
- ~logger,
478
+ | None =>
479
+ JsError.throwWithMessage(
480
+ "The indexer has not started registering handlers, so can't finish it.",
421
481
  )
422
- })
482
+ }
483
+ }
484
+
485
+ let isPendingRegistration = () => {
486
+ switch getActiveRegistration() {
487
+ | Some(r) => !r.finished
488
+ | None => false
489
+ }
490
+ }
491
+
492
+ // Early guard called from `indexer.onEvent` / `.contractRegister` / `.onBlock` /
493
+ // `.onSlot` so the user sees a method-specific error at the call site, instead
494
+ // of hitting the generic `withRegistration` throw deep inside `setHandler` etc.
495
+ let throwIfFinishedRegistration = (~methodName) => {
496
+ switch getActiveRegistration() {
497
+ | Some({finished: true}) =>
498
+ JsError.throwWithMessage(
499
+ `Cannot call \`indexer.${methodName}\` after the indexer has started. Make sure all handlers are registered at the top level of your handler module.`,
500
+ )
501
+ | _ => ()
502
+ }
423
503
  }
424
504
 
425
505
  // Shape of the user-returned `{_gte?, _lte?, _every?}` filter chunk after
@@ -557,12 +637,12 @@ let registerOnBlock = (
557
637
  }
558
638
  | None => ()
559
639
  }
560
- let pending = registration->getPendingChainRegistrations(~chainId)
561
- pending.onBlockRegistrations
640
+ let chainRegs = registration->getChainRegistrations(~chainId)
641
+ chainRegs.onBlockRegistrations
562
642
  ->Array.push(
563
643
  (
564
644
  {
565
- index: pending.onBlockRegistrations->Array.length,
645
+ index: chainRegs.onBlockRegistrations->Array.length,
566
646
  name,
567
647
  startBlock: range._gte,
568
648
  endBlock: range._lte,
@@ -587,160 +667,3 @@ let registerOnBlock = (
587
667
  }
588
668
  })
589
669
  }
590
-
591
- let finishRegistration = (~config: Config.t): registrationsByChainId => {
592
- switch getActiveRegistration() {
593
- | Some(r) => {
594
- r.finished = true
595
- let notRegisteredEventsByContract: dict<Utils.Set.t<string>> = Dict.make()
596
- let registrationsByChainId: registrationsByChainId = Dict.make()
597
- config.chainMap
598
- ->ChainMap.values
599
- ->Array.forEach(chainConfig => {
600
- let key = chainConfig.id->Int.toString
601
- let pending = r.registrationsByChainId->Utils.Dict.dangerouslyGetNonOption(key)
602
-
603
- // We don't need the router itself, but only validation logic,
604
- // since now event router is created for selection of events
605
- // and validation doesn't work correctly in routers.
606
- // Ideally to split it into two different parts.
607
- let eventRouter = EventRouter.empty()
608
-
609
- let onEventRegistrations: array<Internal.onEventRegistration> = []
610
-
611
- chainConfig.contracts->Array.forEach(contract => {
612
- let contractName = contract.name
613
-
614
- contract.events->Array.forEach(
615
- eventConfig => {
616
- let eventName = eventConfig.name
617
- let registration =
618
- pending->Option.flatMap(
619
- pending =>
620
- pending.onEventRegistrations->Utils.Dict.dangerouslyGetNonOption(
621
- getKey(~contractName, ~eventName),
622
- ),
623
- )
624
-
625
- // Should validate the events
626
- eventRouter->EventRouter.addOrThrow(
627
- eventConfig.id,
628
- (),
629
- ~contractName,
630
- ~chain=ChainMap.Chain.makeUnsafe(~chainId=chainConfig.id),
631
- ~eventName,
632
- ~isWildcard=switch registration {
633
- | Some(registration) => registration.isWildcard
634
- | None => isWildcard(~contractName, ~eventName)
635
- },
636
- )
637
-
638
- let registration = switch registration {
639
- | Some(_) as registration => registration
640
- | None =>
641
- // No entry in the incremental store, but the persistent dict
642
- // may still hold a handler: handler modules are import-cached,
643
- // so a repeated registration cycle in the same process (tests
644
- // restarting the indexer) never re-runs the `indexer.onEvent`
645
- // calls. Rebuild from the dict in that case. Events without a
646
- // handler/contractRegister aren't fetched or dispatched
647
- // (unless raw events are enabled).
648
- if hasRegistration(~contractName, ~eventName) || config.enableRawEvents {
649
- Some(
650
- buildOnEventRegistration(
651
- ~config,
652
- ~chainId=chainConfig.id,
653
- ~eventConfig,
654
- ~startBlock=?contract.startBlock,
655
- ),
656
- )
657
- } else {
658
- let eventNames = switch notRegisteredEventsByContract->Utils.Dict.dangerouslyGetNonOption(
659
- contractName,
660
- ) {
661
- | Some(set) => set
662
- | None => {
663
- let set = Utils.Set.make()
664
- notRegisteredEventsByContract->Dict.set(contractName, set)
665
- set
666
- }
667
- }
668
- eventNames->Utils.Set.add(eventName)->ignore
669
- None
670
- }
671
- }
672
-
673
- switch registration {
674
- | Some(registration) =>
675
- // A `where` that resolved to no topic selections (`false` for
676
- // this chain) drops the chain's registration entirely — the
677
- // event should never be fetched here.
678
- let isDroppedByWhere =
679
- config.ecosystem.name === Evm &&
680
- (registration->getResolvedWhere).topicSelections->Utils.Array.isEmpty
681
- if !isDroppedByWhere {
682
- onEventRegistrations
683
- ->Array.push({...registration, index: onEventRegistrations->Array.length})
684
- ->ignore
685
- }
686
- | None => ()
687
- }
688
- },
689
- )
690
- })
691
-
692
- registrationsByChainId->Dict.set(
693
- key,
694
- {
695
- onEventRegistrations,
696
- onBlockRegistrations: switch pending {
697
- | Some(pending) => pending.onBlockRegistrations
698
- | None => []
699
- },
700
- },
701
- )
702
- })
703
-
704
- // Reported once for the whole indexer (a shared contract on multiple
705
- // chains would otherwise repeat the same message per chain).
706
- let notRegisteredEntries = notRegisteredEventsByContract->Dict.toArray
707
- if notRegisteredEntries->Utils.Array.notEmpty {
708
- let groups =
709
- notRegisteredEntries
710
- ->Array.map(((contractName, eventNames)) =>
711
- `${contractName} (${eventNames->Utils.Set.toArray->Array.joinUnsafe(", ")})`
712
- )
713
- ->Array.joinUnsafe(", ")
714
- Logging.getLogger()->Logging.childInfo(
715
- `Events without a handler, skipped for indexing: ${groups}`,
716
- )
717
- }
718
-
719
- registrationsByChainId
720
- }
721
- | None =>
722
- JsError.throwWithMessage(
723
- "The indexer has not started registering handlers, so can't finish it.",
724
- )
725
- }
726
- }
727
-
728
- let isPendingRegistration = () => {
729
- switch getActiveRegistration() {
730
- | Some(r) => !r.finished
731
- | None => false
732
- }
733
- }
734
-
735
- // Early guard called from `indexer.onEvent` / `.contractRegister` / `.onBlock` /
736
- // `.onSlot` so the user sees a method-specific error at the call site, instead
737
- // of hitting the generic `withRegistration` throw deep inside `setHandler` etc.
738
- let throwIfFinishedRegistration = (~methodName) => {
739
- switch getActiveRegistration() {
740
- | Some({finished: true}) =>
741
- JsError.throwWithMessage(
742
- `Cannot call \`indexer.${methodName}\` after the indexer has started. Make sure all handlers are registered at the top level of your handler module.`,
743
- )
744
- | _ => ()
745
- }
746
- }