Skip to content

Refactor event registration to allow multiple handlers per event - #1469

Merged
DZakh merged 19 commits into
mainfrom
claude/multiple-event-registrations-4lpfgw
Jul 23, 2026
Merged

Refactor event registration to allow multiple handlers per event#1469
DZakh merged 19 commits into
mainfrom
claude/multiple-event-registrations-4lpfgw

Conversation

@DZakh

@DZakh DZakh commented Jul 22, 2026

Copy link
Copy Markdown
Member

Summary

Refactors HandlerRegister to support multiple independent handler/contractRegister registrations per event, with automatic merging of handler and contractRegister pairs that target the same event and filter. This replaces the previous model where duplicate registrations were rejected as errors.

Key Changes

  • Eager per-chain registration storage: Moved from a per-event-name dictionary to a per-chain array of onEventRegistration objects. Registrations are resolved and appended immediately as each indexer.onEvent / .contractRegister call executes, rather than being stored in a temporary per-event slot and synced later.

  • Automatic handler/contractRegister merging: When a handler and contractRegister target the same event and filter (same contract, event name, wildcard flag, and resolved where), they merge into a single registration. A handler absorbs an earlier contractRegister-only registration, or a contractRegister attaches to an earlier handler-only registration. Multiple handlers or multiple contractRegisters remain separate registrations.

  • Removed duplicate registration validation: Eliminated the per-event duplicate-detection logic that previously threw errors when two handlers or two contractRegisters registered for the same event. Each registration now stands independently.

  • Removed event ID collision validation: Deleted eventIdValidator and its per-chain dispatch-key collision checks. Moved duplicate-event detection to the config parsing layer (system_config.rs), which now rejects event definitions on the same contract that share a dispatch signature at parse time.

  • Simplified registration API: Removed logger parameter from setHandler and setContractRegister; removed buildOnEventRegistration from the public interface (replaced by getSimulateOnEventRegistrations for test/simulate use).

  • Test-only reset function: Added resetOnEventRegistrations to clear the process-global store between test cycles.

Implementation Details

  • The process-global store (onEventRegistrationsByChainId) now holds arrays of registrations keyed by chain ID, surviving import-cached re-registration cycles in tests.
  • Registrations are assigned their final chain-scoped index during finishRegistration, after all merging is complete.
  • Events with no handler/contractRegister are still fetched and dispatched only if enableRawEvents is enabled; otherwise they're reported once at startup.
  • Config parsing now validates that no two event definitions on the same contract share a dispatch signature (sighash + indexed-topic count for EVM, discriminator for SVM), preventing ambiguous routing at index time.

https://claude.ai/code/session_01EBsVqEGhVkg7EyTjAdcy52

Summary by CodeRabbit

  • New Features
    • Multiple handlers/registrations per event are supported, and event simulation now fans out to produce results for every matching registration.
    • Compatible handler and contract-register entries are resolved and merged automatically.
  • Bug Fixes
    • Duplicate event definitions on the same contract are rejected when they collide on the same dispatch key (while allowing identical signatures across different contracts).
    • Raw event writes are deduplicated by on-chain log coordinate.
  • API Changes
    • Handler registration simplified (logger argument removed) and a reset utility added for pending registrations.
  • Tests
    • Updated/added coverage for lifecycle semantics, simulation fan-out, and duplicate event parsing behavior; removed obsolete validation expectations.

claude added 2 commits July 22, 2026 13:42
Registering the same event more than once no longer errors or composes
handlers. Each `indexer.onEvent` becomes its own registration; a
`contractRegister` merges into a matching handler registration (either
registration order, matched on the resolved `where` and wildcard flag),
otherwise it stands on its own. Wildcard registrations are no longer
capped at one per signature.

The routing, buffer dedup (keyed on registration index), and query
selection layers already fanned one log out to every matching
registration, so this only reshapes the registration layer:

- HandlerRegister resolves each call eagerly into a process-global
  per-chain store (survives an import-cached re-registration cycle; one
  config per isolate). `finishRegistration` just backfills raw-event-only
  regs, drops where-empty EVM regs, and assigns the chain-scoped index.
  The raw persistent slot, per-call resolved store, and the merge/throw
  paths are gone.
- The duplicate-event guard moves from a runtime check to config parse
  (`Contract::new`), keyed on sighash + indexed-topic count for EVM and
  the discriminator for SVM. The wildcard-interference guard is dropped.
- simulate fans a simulated event out to every registration.

raw_events keeps one row per fetched item, so a log matched by N
registrations writes N rows (no dedup, by design).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBsVqEGhVkg7EyTjAdcy52
A `where` resolving to no topic selections already contributes no query
terms, so the registration fetches nothing on its own — the explicit drop
in finishRegistration wasn't needed here. Keeping the registration also
removes its awkward ordering dependency with the raw-event backfill (a
filtered-out event still counts as registered, so no bare raw-event reg
is added for it).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBsVqEGhVkg7EyTjAdcy52
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR replaces incremental event registration with chain-independent intents resolved per chain, supports multiple compatible or distinct registrations, fans simulation events across matches, deduplicates raw events, and adds contract-level duplicate dispatch-key validation.

Changes

Registration routing and dispatch validation

Layer / File(s) Summary
Contract dispatch-key validation
packages/cli/src/config_parsing/system_config.rs
Contract parsing derives dispatch keys for supported event kinds, rejects duplicates within a contract, and tests same-contract versus cross-contract signatures.
Chain-independent registration intents
packages/envio/src/EnvioGlobal.res, packages/envio/src/HandlerRegister.res, packages/envio/src/HandlerRegister.resi
Registration calls append chain-independent intents, validate options across matching chains, and expose the updated registration API.
Per-chain resolution and finalization
packages/envio/src/HandlerRegister.res, packages/envio-tests/test/HandlerRegister_test.res, scenarios/test_codegen/test/HandlerRegisterLifecycle_test.res, scenarios/test_codegen/test/EventHandler.test.ts, scenarios/test_codegen/test/EventFilters_test.res
Finalization resolves and merges registrations, applies filtering and raw-event behavior, assigns indices, and updates lifecycle and multiple-registration coverage.
Simulation registration fan-out
packages/envio/src/SimulateItems.res, scenarios/test_codegen/test/__mocks__/MockConfig.res, scenarios/test_codegen/test/OnEventRegistration_test.res
Simulation retrieves matching registrations through the new resolver and emits one event item per registration.
Raw-event persistence
packages/envio/src/PgStorage.res
Raw event rows are deduplicated by chain, block, and log coordinates before writing.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant HandlerRegister
  participant EnvioGlobal
  participant Config
  participant SimulateItems
  participant PgStorage
  HandlerRegister->>EnvioGlobal: append pending onEvent intent
  HandlerRegister->>Config: validate intent options across matching chains
  HandlerRegister->>Config: resolve and merge registrations per chain
  SimulateItems->>HandlerRegister: getSimulateOnEventRegistrations
  HandlerRegister-->>SimulateItems: matching onEvent registrations
  SimulateItems->>PgStorage: emit event items for registrations
  PgStorage->>PgStorage: deduplicate raw events by log coordinate
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: refactoring event registration to support multiple handlers per event.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0f0a8e391d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/envio/src/HandlerRegister.res Outdated
Comment on lines +33 to +35
let onEventRegistrationsByChainId =
EnvioGlobal.value.onEventRegistrationsByChainId->(
Utils.magic: dict<unknown> => dict<array<Internal.onEventRegistration>>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Rebuild cached handlers for each active config

This per-chain global cache is only populated while handler modules execute, but HandlerLoader.registerAllHandlers is called repeatedly with the active config and Node will not re-run already-imported handler modules. In the TestIndexer path the helper narrows chainMap to the chains in each process() call before registering handlers, so if the first run materializes chain 1 and a later run materializes chain 1337, finishRegistration has no config-independent handler intents left to rebuild chain 1337 registrations and the later run falls back to bare/no handlers. Keep the cached state independent of chain/config, or replay/materialize it for each startRegistration config.

Useful? React with 👍 / 👎.

Comment on lines +319 to +323
let matching =
getChainOnEventRegistrations(~chainId)->Array.filter(reg =>
reg.eventConfig.contractName === eventConfig.contractName &&
reg.eventConfig.name === eventConfig.name
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Drop EVM registrations whose where resolves false

When an EVM where callback returns false, LogSelection represents that as resolvedWhere.topicSelections = []; the previous finishRegistration explicitly dropped those registrations before assigning indexes. This new path returns every matching registration by event name, and finishRegistration also indexes builtRegs without the empty-DNF filter, so simulate can fan out to a handler that opted out of the current chain and normal fetching can keep useless address-dependent partitions. Filter EVM registrations with empty topicSelections before exposing or indexing them.

Useful? React with 👍 / 👎.

.clone()
.unwrap_or_else(|| "none".to_string()),
),
EventKind::Fuel(_) => None,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate Fuel dispatch-key collisions

This branch opts Fuel events out of the new parser-level collision check even though the runtime dispatch identity for Fuel is still event.sighash (logId for LogData, or fixed values like mint/burn/transfer). Since this commit removed HandlerRegister.validateEventIdOrThrow, a Fuel contract can now define the same logId or fixed receipt kind twice, and the Fuel router will match one receipt to both indistinguishable event definitions. Use the Fuel sighash as the dispatch key here as well.

Useful? React with 👍 / 👎.

Registration reverts to storing chain-independent handler/contractRegister
intents, resolved per-config at finishRegistration, instead of a per-chain
resolved cache. The cache was only populated while handler modules ran, but
those are import-cached and registerAllHandlers is called repeatedly with a
narrowed config (TestIndexer narrows chainMap per process() call) — so chains
absent when handlers first ran got no registrations. Intents are
config-independent, so finishRegistration materializes registrations for
whatever chains the current config has, and repeated registration is
idempotent. contractRegister→handler merge and the where-empty drop now happen
at finishRegistration.

Also from PR review:
- Fuel events participate in the parser-level duplicate-dispatch-key check,
  keyed on sighash (logId / mint/burn/transfer/call), matching the router.
- Restore dropping EVM registrations whose where resolves to no topic
  selections (per-chain opt-out) at finishRegistration.
- Remove the intentional duplicate-handler fixtures (and their two composition
  tests) that only existed to exercise the removed handler composition; they
  otherwise inflate every downstream fetch/query count assertion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBsVqEGhVkg7EyTjAdcy52

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 021b096191

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/envio/src/SimulateItems.res Outdated
Comment on lines +348 to +352
HandlerRegister.getSimulateOnEventRegistrations(
~config,
~chainId,
~eventConfig,
)
// Append into the registration array that the chain state will own and
// put that same registration object directly on the simulated item.
let onEventRegistrationIndex = onEventRegistrations->Array.length
let onEventRegistration = {...onEventRegistration, index: onEventRegistrationIndex}
onEventRegistrations->Array.push(onEventRegistration)->ignore

items
->Array.push(
Internal.Event({
onEventRegistration,
chain,
blockNumber,
logIndex,
// Simulate keeps the transaction inline on the payload, so the store
// key is unused.
transactionIndex: 0,
payload: (
{
contractName: eventConfig.contractName,
eventName: eventConfig.name,
params,
chainId,
srcAddress,
logIndex,
transaction,
block,
}: Evm.payload
)->Evm.fromPayload,
}),
)
->ignore
)->Array.forEach(reg => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Filter simulated fan-out by registration index

When this expands one simulate input into one item per registration, SimulateSource.getItemsOrThrow still decides whether a query matches by collecting only eventConfig.id from the query selection (packages/envio/src/sources/SimulateSource.res:40-52). With two registrations for the same event but different filters or address-dependency, a query that selected only registration A will also return the prebuilt item for registration B because their event ids are identical, so handlers can run in simulate even though their registration was not part of the source query that routed the log. The simulate source needs to match the item's registration index, not just the event id, once this fan-out exists.

Useful? React with 👍 / 👎.

Comment thread packages/envio/src/HandlerRegister.res Outdated
Comment on lines +311 to +314
pendingOnEventRegistrations->Array.some(p =>
p.contractName === contractName &&
p.eventName === eventName &&
p.eventOptions->Option.flatMap(v => v.wildcard)->Option.getOr(false)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Derive simulate srcAddress per registration

When the same event has both wildcard and non-wildcard registrations and a simulate item omits srcAddress, SimulateItems.deriveSrcAddress calls this helper once before fanning out. Because it now returns true if any matching registration is wildcard, the whole simulated log gets the dummy source address; the non-wildcard registration then fails its generated clientAddressFilter against the configured contract addresses and its handler is skipped. The source address fallback needs to be chosen per fanned-out registration, or non-wildcard registrations should still get the configured contract address.

Useful? React with 👍 / 👎.

Comment thread packages/envio/src/HandlerRegister.res Outdated
~duplicateMsg="Cannot register a second handler with different options. Make sure all handlers for the same event use identical options (wildcard, where)",
~logger,
)
pendingOnEventRegistrations

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate event filters before queuing handlers

This path now only queues the raw event options, so invalid where objects/callback results are no longer resolved while indexer.onEvent (or pre-registered replay) is executing. The lifecycle test still expects a bad indexed parameter to throw from setHandler, but this returns successfully and only fails later in finishRegistration after the rest of the handler module has run, losing the precise registration call-site error and allowing side effects after an invalid registration. Keep an eager validation/materialization pass for the active config before accepting the intent.

Useful? React with 👍 / 👎.

m.contractRegister->Option.isSome &&
sameEventAndFilter(m, reg, ~config)
) {
| -1 => merged := merged.contents->Array.concat([reg])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor per-registration block filters for duplicates

When two handlers are now allowed for the same contract/event and only one uses where.block.number._gte, both registrations can share the same address partition start because IndexingAddresses.makeContractConfigs collapses a contract's registration start blocks to the minimum, while the EVM client registration payload only sends topic selections (packages/envio/src/sources/HyperSyncClient.res:271-300) and not the per-registration startBlock. As a result, logs before the later _gte are still routed to that handler whenever another registration for the same contract starts earlier; the block start needs to be enforced per registration, not only as a contract-level minimum.

Useful? React with 👍 / 👎.

- Restore call-site `where` validation: setHandler/setContractRegister resolve
  the intent against the active config before storing it, so an invalid `where`
  throws at the registration call site again (not deferred to
  finishRegistration).
- Remove the intentional duplicate-registration fixtures that only exercised
  the removed handler composition / mismatched-options throw: a second
  Gravatar.FactoryEvent handler+contractRegister pair (captured-add test, whose
  property is already covered by throwOnHangingRegistration) and a
  CustomSelection wildcard re-registration. These added extra registrations to
  the Gravatar contract, inflating fetch/query counts across the E2E and
  rollback suites. Drop the now-orphaned captured-add test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBsVqEGhVkg7EyTjAdcy52

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/cli/src/config_parsing/system_config.rs (1)

1953-1956: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Report duplicate dispatch keys, not duplicate signatures.

Fuel and SVM conflicts are not necessarily signature conflicts, and the EVM key also includes indexed-topic count. Change the message to say the events “share the same dispatch key” so configuration authors receive an accurate diagnosis.

Proposed wording
- "Duplicate event detected on contract {name}: {existing} and {} share the \
-  same signature, so they can't be told apart while indexing. Remove the \
+ "Duplicate event detected on contract {name}: {existing} and {} share the \
+  same dispatch key, so they can't be told apart while indexing. Remove the \
  duplicate event.",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/config_parsing/system_config.rs` around lines 1953 - 1956,
Update the duplicate-event error constructed in the event validation flow to
report that {existing} and {name} share the same dispatch key, replacing the
inaccurate “same signature” wording while preserving the rest of the diagnostic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/cli/src/config_parsing/system_config.rs`:
- Around line 1953-1956: Update the duplicate-event error constructed in the
event validation flow to report that {existing} and {name} share the same
dispatch key, replacing the inaccurate “same signature” wording while preserving
the rest of the diagnostic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: efe35edb-87ae-40e8-8afa-7fd61b18dcee

📥 Commits

Reviewing files that changed from the base of the PR and between 0f0a8e3 and e954219.

📒 Files selected for processing (6)
  • packages/cli/src/config_parsing/system_config.rs
  • packages/envio-tests/test/HandlerRegister_test.res
  • packages/envio/src/EnvioGlobal.res
  • packages/envio/src/HandlerRegister.res
  • scenarios/test_codegen/src/handlers/EventHandlers.ts
  • scenarios/test_codegen/test/EventHandler.test.ts
💤 Files with no reviewable changes (2)
  • scenarios/test_codegen/src/handlers/EventHandlers.ts
  • scenarios/test_codegen/test/EventHandler.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/envio-tests/test/HandlerRegister_test.res

The per-contract collision key is a dispatch key (EVM sighash + indexed
count, Fuel logId/receipt kind, SVM discriminator), not strictly a
signature, so the diagnostic wording is more accurate this way.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBsVqEGhVkg7EyTjAdcy52

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 089b24a77b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/envio/src/HandlerRegister.res Outdated
let where = intent.eventOptions->Option.flatMap(v => v.where)
config.chainMap
->ChainMap.values
->Array.some(chainConfig =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate every matching chain before storing intents

The current fix still short-circuits at the first matching chain: Array.some stops as soon as buildOnEventRegistrationWith succeeds. In a multi-chain config where a where callback returns false or a valid filter on the first matching chain but an invalid params/block filter on a later matching chain, indexer.onEvent accepts and stores the intent, and the error is deferred until finishRegistration after the rest of the handler module has run. Use a full iteration over all matching chains for this eager validation before pushing the intent.

Useful? React with 👍 / 👎.

Comment thread packages/envio/src/SimulateItems.res Outdated
}),
)
->ignore
)->Array.forEach(reg => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve original simulate item indexes after fan-out

When one simulate input matches multiple registrations, this loop now pushes multiple internal source.simulateItems, but SimulateDeadInputTracker.makeFromConfig still reports array positions from that internal fanned-out list as “index in each chain's simulate array”. If a single user input at index 0 is filtered out for two registrations (for example an explicit non-indexed srcAddress), the failure reports indices 0, 1 even though the user's simulate array only had index 0; carry the original itemIndex through fan-out or dedupe tracking by the raw input index.

Useful? React with 👍 / 👎.

Comment on lines +465 to +468
if !isDroppedByWhere(~config, reg) {
onEventRegistrations
->Array.push({...reg, index: onEventRegistrations->Array.length})
->ignore

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Deduplicate raw events across fanned-out handlers

When raw_events is enabled and a single log matches two registrations for the same contract/event (for example two indexer.onEvent handlers), this loop now keeps both registrations so the source returns one Internal.Event per handler. PgStorage.writeBatch converts every event item into a raw_events row, and that table uses a serial primary key rather than (chain_id,event_id), so one on-chain log is persisted multiple times as identical raw events; dedupe raw-event insertion by the log coordinate or generate raw events before handler fan-out.

Useful? React with 👍 / 👎.

`validateIntentWhere` used `Array.some`, stopping at the first chain that
defines the event. A `where` that resolves fine on the first chain but is
structurally invalid on a later one slipped past the call-site check and only
threw at `finishRegistration`. Iterate every matching chain so the error
surfaces at the registration call site regardless of which chain's resolution
is invalid.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBsVqEGhVkg7EyTjAdcy52
- finishRegistration: when raw events are enabled, an event whose `where` opts
  out of a chain (empty topic selections) is no longer fully dropped — its
  handler still doesn't run there, but a bare (handler-less) registration is
  added so the event's logs are captured for `raw_events`. The backfill now
  keys on surviving registrations; a `where: false` event still isn't reported
  as handler-less (tracked via intentKeys).
- PgStorage.writeBatch: a single log fans out to one item per matching
  registration, but `raw_events` records the log, so dedupe rows by log
  coordinate (chain, block, logIndex) — one row per log.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBsVqEGhVkg7EyTjAdcy52

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scenarios/test_codegen/test/EventFilters_test.res`:
- Around line 489-497: Update handlerPresence to inspect all registrations named
"WithExcessField" rather than using Array.find, and assert the complete matching
set for each chain. Verify the registrations’ exact count and raw-event
registration type through the public registration API, preserving the expected
enabled result for chain "137" and absent result for chain "100".
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c9981235-40cf-45bf-a483-08e5d6ecc722

📥 Commits

Reviewing files that changed from the base of the PR and between 6dde622 and 3932d7e.

📒 Files selected for processing (4)
  • packages/envio-tests/test/HandlerRegister_test.res
  • packages/envio/src/HandlerRegister.res
  • packages/envio/src/PgStorage.res
  • scenarios/test_codegen/test/EventFilters_test.res
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/envio-tests/test/HandlerRegister_test.res
  • packages/envio/src/HandlerRegister.res

Comment on lines +489 to +497
let handlerPresence = chainId =>
switch registrationsByChainId->Dict.get(chainId) {
| Some({HandlerRegister.onEventRegistrations: regs}) =>
regs->Array.some(reg => reg.eventConfig.name === "WithExcessField")
| None => false
regs
->Array.find(reg => reg.eventConfig.name === "WithExcessField")
->Option.map(reg => reg.handler->Option.isSome)
| None => None
}
t.expect((hasEvent("137"), hasEvent("100"))).toEqual((true, false))
t.expect((handlerPresence("137"), handlerPresence("100"))).toEqual((Some(true), Some(false)))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the complete raw-event registration set.

Array.find checks only the first matching registration, and checking handler->Option.isSome does not prove that the registration is specifically the bare raw-events registration. With multiple registrations now supported, this can pass while another matching handler registration remains. Filter all WithExcessField registrations and assert their exact count/type through the public registration API where possible.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scenarios/test_codegen/test/EventFilters_test.res` around lines 489 - 497,
Update handlerPresence to inspect all registrations named "WithExcessField"
rather than using Array.find, and assert the complete matching set for each
chain. Verify the registrations’ exact count and raw-event registration type
through the public registration API, preserving the expected enabled result for
chain "137" and absent result for chain "100".

Correct the raw-events behavior: a `where: false` event has a handler that
opted out of the chain, so it gets no registration there at all — not even a
raw-events one. Event configs with no explicit handler still get a bare
raw-events registration when raw events are enabled. This reverts the
finishRegistration change from the previous commit back to keying the backfill
on the resolved registrations; the raw-events row dedup (PgStorage) stays.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBsVqEGhVkg7EyTjAdcy52

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eeb6080627

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/envio/src/HandlerRegister.res Outdated
if contract.name === intent.contractName {
switch contract.events->Array.find(e => e.name === intent.eventName) {
| Some(eventConfig) =>
let _ = buildOnEventRegistrationWith(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid invoking event where callbacks twice

For EVM registrations whose where option is a callback, this validation call already builds the registration and invokes LogSelection.parseWhereOrThrow; finishRegistration later replays the same intent through resolveChainRegistrations, so the same callback runs a second time during normal startup even though LogSelection.res documents it as invoked exactly once per chain. If a callback closes over mutable state, reads time/randomness, or has side effects, validation can accept one filter while the indexer actually registers another. Cache/materialize the resolved registration for the active config instead of discarding it here.

Useful? React with 👍 / 👎.

Comment on lines +1940 to +1942
svm.discriminator
.clone()
.unwrap_or_else(|| "none".to_string()),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize SVM discriminator keys before duplicate check

When two SVM instructions use the same discriminator bytes with different hex casing, such as 0x0f and 0x0F, this string key treats them as distinct and parsing accepts both. The SVM router decodes discriminator strings to bytes before matching, so a matching instruction then fans out to both indistinguishable registrations even though this parser check is meant to reject dispatch collisions. Normalize/lowercase the discriminator or key on decoded bytes plus length before inserting.

Useful? React with 👍 / 👎.

Comment thread packages/envio/src/HandlerRegister.res Outdated
// Validate before storing so a broken `where` never leaves a poisoned intent
// in the global store.
validateIntentWhere(~config=registration.config, intent)
pendingOnEventRegistrations->Array.push(intent)->ignore

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve pre-registered handler order

When a handler module is imported before startRegistration, withRegistration queues callbacks in preRegistered and startRegistration replays them with pop(), so this append records the last top-level registration first. Since the new pendingOnEventRegistrations order is now the dispatch order for multiple handlers on the same event, preloaded handler modules run those handlers in reverse. Replay preRegistered FIFO or otherwise preserve source order before appending intents.

Useful? React with 👍 / 👎.

Reword the same-contract duplicate-event error to drop the "dispatch key"
jargon. Normalize SVM discriminators (lowercase) before keying so hex-casing
variants collide, matching the router. Replace the two Rust `#[test]` cases
with parseYaml tests in the ReScript suite, plus an SVM casing-collision case.
Replay pre-registered handlers in FIFO source order so multi-handler dispatch
order matches registration order, with a regression test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBsVqEGhVkg7EyTjAdcy52
Cache each registration intent's per-chain resolution on the intent, so the
user's `where` callback runs exactly once per chain instead of once at the
registration call site and again at every `finishRegistration`/simulate. This
restores the "invoked exactly once per chain" invariant.

Add an end-to-end test: two handlers on one event (Gravatar.MultiHandlerOrder)
both run, ordered by (blockNumber, logIndex, registration index), plus a unit
test asserting a `where` callback is invoked once per chain across repeated
finishes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBsVqEGhVkg7EyTjAdcy52
The new Gravatar.MultiHandlerOrder event shifts the generated contract ABI,
which `Indexer_test`'s full chain-config deep-equal asserts on.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBsVqEGhVkg7EyTjAdcy52

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bcf9df7f66

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +859 to +863
let coordinate = `${item
->Internal.getItemChainId
->Int.toString}-${item
->Internal.getItemBlockNumber
->Int.toString}-${item->Internal.getItemLogIndex->Int.toString}`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include Fuel tx id in raw-event dedupe

In Fuel raw-events runs, this coordinate can collide for different receipts: the Fuel source preserves both txId and receiptIndex, and receiptIndex is not block-scoped. If two transactions in the same block each emit a matching receipt at index 0, the first adds the key <chain>-<block>-0 and the second is dropped before toRawEvent, losing a real raw event. Include the Fuel transaction id, or otherwise use an ecosystem-specific unique log coordinate, while still collapsing fan-out duplicates.

Useful? React with 👍 / 👎.

Comment on lines +420 to +423
registeredKeys->Utils.Set.has(
getKey(~contractName=contract.name, ~eventName=eventConfig.name),
)
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Backfill raw events when where skips the handler

When raw_events is enabled and an EVM handler's where resolves to false on this chain, builtRegs still adds the event to registeredKeys, but the registration is later removed by isDroppedByWhere before it reaches onEventRegistrations. This branch therefore skips creating the bare raw-events registration too, so the configured event is not fetched or stored at all even though handler-less events are normally backfilled for raw events. Base this check on surviving registrations, or add a raw registration when every matching handler registration was dropped by where.

Useful? React with 👍 / 👎.

Comment thread packages/envio/src/HandlerRegister.res Outdated
->Array.forEach(chainConfig =>
resolveIntentForChain(~config=registration.config, ~chainConfig, intent)->ignore
)
pendingOnEventRegistrations->Array.push(intent)->ignore

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make cross-file handler order deterministic

When two handlers for the same event live in different auto-loaded handler files, this append makes their registration index depend on whichever module's top-level indexer.onEvent runs first. HandlerLoader.autoLoadFromSrcHandlers starts all dynamic imports together with Promise.all, so independent files, especially with top-level await or different dependency graphs, can register in nondeterministic order; this refactor then uses that order to sort same-log fan-out. Load/record handler files in a stable sequence before assigning registration indexes.

Useful? React with 👍 / 👎.

Replace the chain-independent intent array + per-intent resolved-where cache
with a single persistent `activeRegistration` that handlers resolve into at
`onEvent`/`onBlock` call time (the `where` callback runs once per chain there,
at the call site). `startRegistration` is idempotent, so the import-cached
handlers register once and every later `finishRegistration` reuses the result;
`finishRegistration` reads the store and builds a fresh per-config output
(merge, raw-events backfill, index) without mutating it, so a run appending a
simulate/mock source stays isolated.

onEvent + onBlock now share the `chainRegistrations` type in the session store.
onBlock validation runs against the config start block (registration sees the
full, un-narrowed config); the persistence-derived resume block overrides
downstream without re-validation. MockIndexer registers once against the full
config and narrows per run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBsVqEGhVkg7EyTjAdcy52
Base changed `expectParseError` to compare the full parse-error string
exactly, so the duplicate-event assertions need the complete message
(including the `Config parse error:` prefix and, for the EVM case, the
`Failed parsing globally defined contract` context) instead of a substring.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBsVqEGhVkg7EyTjAdcy52
* Run createTestIndexer in-process instead of worker threads (#1467)

* Run createTestIndexer in-process instead of worker threads

Replace the per-chain worker + storage message-proxy with an in-process
run against an in-memory Persistence.storage, removing the dominant cost
(re-evaluating the envio module graph in a fresh isolate per process()).

- IndexerState gets an injectable ~onExit; ExitOnCaughtUp resolves it
  instead of process.exit, so a caught-up run in-process resolves a
  promise rather than killing the test runner. Production default unchanged.
- TestIndexer builds a per-instance in-memory storage (config-derived
  initial state, never a real DB) and drives IndexerState/IndexerLoop
  directly; runs bypass Persistence.init and stop the loop on completion.
- Registrations are captured once and cloned per run (patchConfig appends
  a simulate source), so independent createTestIndexer instances run in
  parallel without shared mutable registration state.
- Handlers run inside an AsyncLocalStorage scope so a handler calling
  indexer.onEvent throws (as in production) without finishing the global
  registration the test itself uses.
- Delete TestIndexerWorker and the proxy's message-channel machinery.
- Restore vitest to envio devDependencies (needed to resolve the Vitest
  binding for local scenario test runs).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fb6Yyif9GZG1DWAiCER4Gh

* Drop JSON round-trip from TestIndexer in-memory storage

The worker model serialized entities to JSON to cross the thread boundary.
In-process there is no boundary, so store and load entities decoded:

- handleLoad filters decoded entities with the already-typed filter and
  returns them directly (no serialize/parse, no rowsSchema round-trip).
- handleWriteBatch takes Persistence.updatedEntity and stores the decoded
  entities as-is instead of encoding then re-parsing them.
- Delete TestIndexerProxyStorage entirely — its serializable types were the
  only remaining use.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fb6Yyif9GZG1DWAiCER4Gh

* Isolate test indexer entity store with defensive copies (#1472)

* Harden in-process TestIndexer: isolate store, drop dead resume plumbing

- Copy entities on the set/get boundary so user code mutating a returned
  entity (or an object passed to `set`) can't corrupt the in-memory store;
  add a regression test.
- Remove the unused `resumeInitialState`/`currentInitialState` injection —
  the runner sets `storageStatus = Ready(...)` directly, so it was never
  reached; make `resumeInitialState` a throwing stub like the other unused
  storage methods.
- Await the in-flight write fiber in cleanup instead of busy-polling it.
- Document the loss of per-test isolation (shared module/effect-cache state)
  in the indexer-testing skill.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DAwNk4gUT3QV6dh7uBtef1

* Replace AsyncLocalStorage guard with plain finished-registration flag

The in-process runner used an AsyncLocalStorage scope so a handler calling
`indexer.onEvent` at runtime would throw, while still letting tests call the
registration API after a run (which required clearing the finished flag). The
only thing depending on the API staying callable post-registration was two
type-surface tests; those are compile-time only and don't need to run after
registration.

- Move the `indexer.onEvent`/`contractRegister` type-surface checks into
  EventHandlers.ts under `if (0)` (type-checked, never executed), and drop the
  two runtime `it(...)` cases.
- Delete the AsyncLocalStorage scope, `runInHandlerScope`/`isInHandlerScope`,
  and `clearActiveRegistration`; restore `throwIfFinishedRegistration` to the
  plain `finished` check. A handler registering at runtime now throws via the
  finished flag, as in production.
- getRegistrations no longer reopens the registration API after capturing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DAwNk4gUT3QV6dh7uBtef1

* Tighten test-isolation note in indexer-testing skill

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DAwNk4gUT3QV6dh7uBtef1

---------

Co-authored-by: Claude <noreply@anthropic.com>

* Collapse makeCreateTestIndexer factory into createTestIndexer

The (~config) => unit => t currying was a dependency-injection seam (it
once also threaded ~workerPath); nothing injects a custom config and its
only caller invoked it immediately. Fold it into a single
createTestIndexer() that loads the memoized config internally.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fb6Yyif9GZG1DWAiCER4Gh

* Test handler errors and improve TestIndexer error messages (#1474)

* Fix stale TestIndexer comments and drop dead chainId tuple element

- copyEntity comment no longer claims all entity fields are immutable
  scalars; shallow copy still shares array-valued fields.
- Rollback storage stubs no longer advise setting rollbackOnReorg to
  false — the runner already forces it off, making them unreachable.
- Drop the unused chainId element from chainEntries (validation kept).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCpLSmKU3MZaAyGRjmcAVc

* Add regression test for handler-throw propagation

A synchronous throw in an event handler body must reject the process()
promise at the call site, carrying the handler's original message. This
path (handler-body throw) was only covered indirectly before, via effect
and getOrThrow throws.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCpLSmKU3MZaAyGRjmcAVc

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

* Friendlier event-registration validation and simulate handler checks

- Reject duplicate event names per contract at parse time, suggesting the
  `name` alias for overloads (replaces the never-implemented uniqueness TODO).
- Reword the same-dispatch-signature error so it reads clearly (names are
  unique by the check above, so it never prints "X and X").
- Simulate now applies the same `where:false` drop as finishRegistration and
  fails loudly when an event has no handler registered, instead of silently
  fabricating a bare registration.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lcy9XY2kAFMscJRukX7Fqi

* Test the new validation and simulate handler behavior

ConfigYaml: same-signature rejection with distinct names, byte-identical
duplicate, same-name overload (alias suggestion), the SVM hex-casing case,
and a success case where a `name` alias resolves an overload.

HandlerRegister: simulate returns the handled registration, drops a
`where:false` one, and returns nothing for an unhandled event.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lcy9XY2kAFMscJRukX7Fqi

* Revert simulate-side changes that broke handler-less simulate

The where:false drop and no-handler failure lived in the shared
`getSimulateOnEventRegistrations`, which is also used by module-load-time
registration probes (`MockConfig.getOnEventRegistration`) that need the bare
fallback and the un-dropped registration. And failing on a handler-less event
contradicts the framework's intentional pattern of simulating one (e.g.
`Noop.EmptyEvent`) as a chain-advance marker.

Keeps the parse-time event-name validation (items 1 and 4), which is
unaffected. The simulate where:false drop needs to live in the simulate path
only; revisiting separately.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lcy9XY2kAFMscJRukX7Fqi

* Fail simulate on unhandled events; drop where:false in simulate path only

Apply the no-handler failure and the where:false drop in SimulateItems (the
user-facing simulate path) instead of the shared getSimulateOnEventRegistrations,
so module-load registration probes (MockConfig) keep their bare-fallback
behavior. Simulate now fans out only to registrations that would actually run
on the chain (a real handler/contractRegister, where not excluded), and throws
a clear error when none would.

Give Noop.EmptyEvent a no-op handler so the multichain-ordering test still
processes an event on chain 1 (its only event) without writing an entity, and
point the new no-handler failure test at Gravatar.TestEventWithReservedKeyword,
which is defined but genuinely handler-less.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lcy9XY2kAFMscJRukX7Fqi

---------

Co-authored-by: Claude <noreply@anthropic.com>
claude added 2 commits July 23, 2026 15:05
The merged main renamed the config helper (`MockIndexerConfig` ->
`InternalTestIndexer.fromUserApi`); update the multi-registration and
duplicate-event tests accordingly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBsVqEGhVkg7EyTjAdcy52
@DZakh
DZakh enabled auto-merge (squash) July 23, 2026 15:12
Noop.EmptyEvent's no-op handler registered on every chain that lists the Noop
contract, including chain 137 where Noop has an address — adding a fetch
partition that the rollback/reorg tests (one partition per chain) don't expect.
Pin the handler to chain 1, its only intended chain.

Drop the MultiHandlerOrder event + its two handlers and the end-to-end
dispatch-order test: adding a new event to the shared chain-1337 Gravatar
contract perturbs partition snapshots in the rollback suite. Dispatch ordering
by (blockNumber, logIndex, registration index) stays covered by the FetchState
buffer-order test and the HandlerRegister index-order tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBsVqEGhVkg7EyTjAdcy52
@DZakh
DZakh merged commit 6c163cc into main Jul 23, 2026
8 checks passed
@DZakh
DZakh deleted the claude/multiple-event-registrations-4lpfgw branch July 23, 2026 15:52

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9b90e52f40

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

| Some(fn) => fn(r)
| None => ()
switch getActiveRegistration() {
| Some(_) => ()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reinitialize when registering a different config

When registerAllHandlers is called again in the same process with a different config or handler set, this branch keeps the previous active registration (which finishRegistration has already marked finished) and does not replay/import new registrations into a fresh store. Any newly imported handler file will then hit throwIfFinishedRegistration, and even if no new file is imported finishRegistration will reuse registrations from the prior config by chain id, so a test indexer/custom config sharing a chain id can inherit unrelated handlers. Either reset for a genuinely different config or key the persistent cache by the config/handler set rather than treating every later start as the same cycle.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants