Skip to content

Refactor duplicate event registration error handling - #987

Merged
DZakh merged 13 commits into
mainfrom
claude/detect-duplicate-handlers-Km3yA
Feb 25, 2026
Merged

Refactor duplicate event registration error handling#987
DZakh merged 13 commits into
mainfrom
claude/detect-duplicate-handlers-Km3yA

Conversation

@DZakh

@DZakh DZakh commented Feb 24, 2026

Copy link
Copy Markdown
Member

Summary

Refactored the duplicate event registration error handling in HandlerRegister.res to use a dedicated helper function instead of relying on exception types. This improves code maintainability and enables better error logging with structured context.

Key Changes

  • Removed exception type: Deleted the DuplicateEventRegistration exception type that was previously used for error handling
  • Added helper function: Introduced raiseDuplicateRegistration function that:
    • Constructs error messages with contract and event name context
    • Creates a child logger with structured parameters (contractName, eventName)
    • Logs the error before raising it
    • Raises a JavaScript error with the full message
  • Simplified error handling: Replaced three separate error handling blocks in setEventOptions, setHandler, and setContractRegister with calls to the new helper function
  • Added test coverage: Created test case in EventHandler.test.ts that verifies duplicate handler and contractRegister registrations throw appropriate errors with correct messages
  • Updated test fixtures: Added error capture code in EventHandlers.ts to export caught errors for test assertions
  • Fixed test file reference: Corrected error message in LogTesting.res to reference Logging.test.ts instead of Logging_test.ts

Implementation Details

The new raiseDuplicateRegistration function consolidates error handling logic that was previously duplicated across three locations. It provides consistent error messaging and logging behavior while reducing code duplication. The function signature accepts the necessary context parameters (contractName, eventName, message, and logger) to construct meaningful error messages and logs.

https://claude.ai/code/session_013sDgWWA3hVAoX6YVqJg1Ex

Summary by CodeRabbit

  • New Features

    • Multiple handlers/registers for the same event with identical options now compose and run together.
    • Event-option matching now correctly handles wildcards and optional filters for more predictable duplicate detection.
    • Improved, clearer user-facing messages when duplicate registrations have conflicting options.
  • Tests

    • Added tests validating handler/register composition and the user-facing error for mismatched options.
  • Bug Fixes

    • Corrected a test runtime error message reference for clearer diagnostics.

Replace DuplicateEventRegistration ReScript exception with Js.Exn.raiseError
so callers get a standard JS Error with a descriptive message like
"Duplicate registration of event handlers not allowed for Gravatar.CustomSelection".

Also:
- Add smoke tests in EventHandlers.ts that exercise duplicate handler and
  contractRegister paths, exporting caught errors for assertion.
- Add test case in EventHandler.test.ts verifying the error messages.
- Rename _test.ts files to .test.ts so vitest discovers them.

https://claude.ai/code/session_013sDgWWA3hVAoX6YVqJg1Ex
@coderabbitai

coderabbitai Bot commented Feb 24, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@DZakh has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 13 minutes and 24 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 74f4d4f and bd2dd9a.

📒 Files selected for processing (1)
  • packages/envio/src/HandlerRegister.res
📝 Walkthrough

Walkthrough

Centralizes duplicate-registration handling in HandlerRegister.res: adds explicit duplicate-raise helper, compares eventOptions/eventFilters (including None), and enables composing multiple handlers and contract registers when their options match; updates types and wiring. Adds tests exercising composition and mismatched-option error handling and fixes a test-message typo.

Changes

Cohort / File(s) Summary
Core Handler Registration Logic
packages/envio/src/HandlerRegister.res
Add raiseDuplicateRegistration, eventFiltersMatch, eventOptionsMatch; change eventRegistration.eventOptions type to use Internal.eventFilters; refactor setEventOptions, setHandler, and setContractRegister to compose handlers/contractRegisters on identical options and raise unified errors on mismatches.
Test Scenario: Handlers
scenarios/test_codegen/src/handlers/EventHandlers.ts
Add exports and registrations to exercise composed handler and contractRegister paths and to capture mismatched-options errors (composedHandlerCalled, composedContractRegisterCalled, mismatchedHandlerOptionsError).
Test: EventHandler Case
scenarios/test_codegen/test/EventHandler.test.ts
Add test asserting successful composition for identical options and that registering with mismatched options produces a descriptive error.
Test Fixture Maintenance
scenarios/test_codegen/test/fixtures/LogTesting.res
Fix runtime error message filename from Logging_test.ts to Logging.test.ts.

Sequence Diagram(s)

(omitted)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • moose-code
  • JonoPrest

Poem

🐰
I hopped through code with nimble paws,
Merged handlers neat without a cause.
When options match we bundle tight,
If they don't—an error lights the night.
A joyful hop — registrations right.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: refactoring error handling for duplicate event registrations by introducing a dedicated helper function.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch claude/detect-duplicate-handlers-Km3yA

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

claude and others added 3 commits February 24, 2026 15:31
When an event handler or contractRegister is registered multiple times with
identical options (wildcard, eventFilters), the handlers are now composed
to run sequentially instead of throwing. Mismatched options still error
with a user-friendly message.

https://claude.ai/code/session_013sDgWWA3hVAoX6YVqJg1Ex
@DZakh
DZakh enabled auto-merge (squash) February 24, 2026 18:41
@DZakh
DZakh disabled auto-merge February 24, 2026 18:42

@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: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/envio/src/HandlerRegister.res`:
- Around line 191-195: The params passed into Logging.createChildFrom are
currently a record literal ({contractName, eventName}); change this to a JS
object payload instead so downstream JS consumers get an object. Update the call
in raiseDuplicateRegistration (the ~params argument passed to
Logging.createChildFrom / Logging.childError) to pass an object-style payload
mapping contractName and eventName to their values (i.e., a JS object with keys
"contractName" and "eventName") rather than the record literal.
- Around line 197-205: The current eventOptionsMatch function compares
eventFilters (of type Js.Json.t) with reference equality, which fails for
structurally equal JSON; update eventOptionsMatch (the function comparing
Internal.eventOptions) to perform a deep/stable comparison of a.eventFilters and
b.eventFilters instead of using == (for example by canonicalizing to a stable
string representation like JSON stringify or calling a dedicated deepEqual
helper) while keeping the wildcard equality check; ensure the deep comparison
handles None/Some cases the same way as now and applies only to the eventFilters
field of Internal.eventOptions.

In `@scenarios/test_codegen/test/EventHandler.test.ts`:
- Around line 627-649: The test is non-deterministic because
composedContractRegisterCalled may already be true from prior tests; import the
handlers module (the exported symbols composedContractRegisterCalled and
mismatchedHandlerOptionsError) before calling
MockDb.createMockDb().processEvents and explicitly reset
handlers.composedContractRegisterCalled = false (and clear
handlers.mismatchedHandlerOptionsError = undefined) so the subsequent call to
mockDbInitial.processEvents([event]) drives the composition and the assertion
reliably; ensure the import is done at the top of the test and the flags are
reset immediately before triggering event processing.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 28f19ef and f059333.

⛔ Files ignored due to path filters (1)
  • scenarios/test_codegen/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (7)
  • packages/envio/src/HandlerRegister.res
  • scenarios/test_codegen/src/handlers/EventHandlers.ts
  • scenarios/test_codegen/test/CustomSelection.test.ts
  • scenarios/test_codegen/test/EventHandler.test.ts
  • scenarios/test_codegen/test/Logging.test.ts
  • scenarios/test_codegen/test/S.test.ts
  • scenarios/test_codegen/test/fixtures/LogTesting.res

Comment on lines +191 to +195
let raiseDuplicateRegistration = (~contractName, ~eventName, ~msg, ~logger) => {
let fullMsg = msg ++ " for " ++ contractName ++ "." ++ eventName
Logging.createChildFrom(~logger, ~params={contractName, eventName})->Logging.childError(fullMsg)
Js.Exn.raiseError(fullMsg)
}

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.

⚠️ Potential issue | 🟡 Minor

Use an object literal for logger params payload.

~params={contractName, eventName} is a record literal; prefer a JS object for payloads passed between functions.

🛠️ Suggested change
-  Logging.createChildFrom(~logger, ~params={contractName, eventName})->Logging.childError(fullMsg)
+  Logging.createChildFrom(
+    ~logger,
+    ~params={"contractName": contractName, "eventName": eventName},
+  )->Logging.childError(fullMsg)
As per coding guidelines, "Use records when working with structured data, and objects to conveniently pass payload data between functions".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/envio/src/HandlerRegister.res` around lines 191 - 195, The params
passed into Logging.createChildFrom are currently a record literal
({contractName, eventName}); change this to a JS object payload instead so
downstream JS consumers get an object. Update the call in
raiseDuplicateRegistration (the ~params argument passed to
Logging.createChildFrom / Logging.childError) to pass an object-style payload
mapping contractName and eventName to their values (i.e., a JS object with keys
"contractName" and "eventName") rather than the record literal.

Comment on lines +197 to +205
let eventOptionsMatch = (
existing: option<Internal.eventOptions<Js.Json.t>>,
incoming: option<Internal.eventOptions<Js.Json.t>>,
) => {
switch (existing, incoming) {
| (None, None) => true
| (Some(a), Some(b)) => a.wildcard == b.wildcard && a.eventFilters == b.eventFilters
| _ => 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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find eventOptions type definition and eventFilters references
rg -n "type eventOptions" -g '*.res' -A 5

Repository: enviodev/hyperindex

Length of output: 378


🏁 Script executed:

#!/bin/bash
# Search for eventFilters usage and definition
rg -n "eventFilters" -g '*.res' -B 2 -A 2

Repository: enviodev/hyperindex

Length of output: 11204


🏁 Script executed:

#!/bin/bash
# Check the Internal module to find the actual type definition
fd -e res -e resi | head -20 | xargs grep -l "eventOptions\|eventFilters" 2>/dev/null

Repository: enviodev/hyperindex

Length of output: 98


Use deep equality or stable comparison for eventFilters matching.

The eventFilters field is cast from a typed variant (Static with arrays or Dynamic with functions) to opaque Js.Json.t before comparison. Comparing with == on Js.Json.t will reject structurally identical filters if they are different object references. Since the error message directs users to use "identical options," implement deep-equality comparison or document that eventFilters must be the exact same reference across registrations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/envio/src/HandlerRegister.res` around lines 197 - 205, The current
eventOptionsMatch function compares eventFilters (of type Js.Json.t) with
reference equality, which fails for structurally equal JSON; update
eventOptionsMatch (the function comparing Internal.eventOptions) to perform a
deep/stable comparison of a.eventFilters and b.eventFilters instead of using ==
(for example by canonicalizing to a stable string representation like JSON
stringify or calling a dedicated deepEqual helper) while keeping the wildcard
equality check; ensure the deep comparison handles None/Some cases the same way
as now and applies only to the eventFilters field of Internal.eventOptions.

Comment on lines +627 to +649
it("composes duplicate handlers with same options and rejects mismatched options", async () => {
const mockDbInitial = MockDb.createMockDb();

const event = Gravatar.FactoryEvent.createMockEvent({
contract: "0x1234567890123456789012345678901234567890",
testCase: "syncRegistration",
});

// Trigger module load via autoLoadFromSrcHandlers
await mockDbInitial.processEvents([event]);

// Dynamic-import EventHandlers.js to access exported error values
const handlers = await import("../src/handlers/EventHandlers");

// Same options → composed without error
// contractRegister ran during factory event processing, proving compose works
assert.strictEqual(handlers.composedContractRegisterCalled, true);

// Different options → throws a user-friendly error
assert.strictEqual(
handlers.mismatchedHandlerOptionsError?.message,
"Cannot register a second handler with different options. Make sure all handlers for the same event use identical options (wildcard, eventFilters) for Gravatar.CustomSelection"
);

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.

⚠️ Potential issue | 🟡 Minor

Make the composition test deterministic.

composedContractRegisterCalled may already be true from earlier tests that processed FactoryEvent. Import the handlers first and reset the flag before processing so this test actually drives the transition.

🧪 Suggested adjustment
-    // Trigger module load via autoLoadFromSrcHandlers
-    await mockDbInitial.processEvents([event]);
-
-    // Dynamic-import EventHandlers.js to access exported error values
-    const handlers = await import("../src/handlers/EventHandlers");
+    // Import handlers early so we can reset flags for deterministic assertions
+    const handlers = await import("../src/handlers/EventHandlers");
+    handlers.composedContractRegisterCalled = false;
+
+    await mockDbInitial.processEvents([event]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it("composes duplicate handlers with same options and rejects mismatched options", async () => {
const mockDbInitial = MockDb.createMockDb();
const event = Gravatar.FactoryEvent.createMockEvent({
contract: "0x1234567890123456789012345678901234567890",
testCase: "syncRegistration",
});
// Trigger module load via autoLoadFromSrcHandlers
await mockDbInitial.processEvents([event]);
// Dynamic-import EventHandlers.js to access exported error values
const handlers = await import("../src/handlers/EventHandlers");
// Same options → composed without error
// contractRegister ran during factory event processing, proving compose works
assert.strictEqual(handlers.composedContractRegisterCalled, true);
// Different options → throws a user-friendly error
assert.strictEqual(
handlers.mismatchedHandlerOptionsError?.message,
"Cannot register a second handler with different options. Make sure all handlers for the same event use identical options (wildcard, eventFilters) for Gravatar.CustomSelection"
);
it("composes duplicate handlers with same options and rejects mismatched options", async () => {
const mockDbInitial = MockDb.createMockDb();
const event = Gravatar.FactoryEvent.createMockEvent({
contract: "0x1234567890123456789012345678901234567890",
testCase: "syncRegistration",
});
// Import handlers early so we can reset flags for deterministic assertions
const handlers = await import("../src/handlers/EventHandlers");
handlers.composedContractRegisterCalled = false;
await mockDbInitial.processEvents([event]);
// Same options → composed without error
// contractRegister ran during factory event processing, proving compose works
assert.strictEqual(handlers.composedContractRegisterCalled, true);
// Different options → throws a user-friendly error
assert.strictEqual(
handlers.mismatchedHandlerOptionsError?.message,
"Cannot register a second handler with different options. Make sure all handlers for the same event use identical options (wildcard, eventFilters) for Gravatar.CustomSelection"
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scenarios/test_codegen/test/EventHandler.test.ts` around lines 627 - 649, The
test is non-deterministic because composedContractRegisterCalled may already be
true from prior tests; import the handlers module (the exported symbols
composedContractRegisterCalled and mismatchedHandlerOptionsError) before calling
MockDb.createMockDb().processEvents and explicitly reset
handlers.composedContractRegisterCalled = false (and clear
handlers.mismatchedHandlerOptionsError = undefined) so the subsequent call to
mockDbInitial.processEvents([event]) drives the composition and the assertion
reliably; ensure the import is done at the top of the test and the flags are
reset immediately before triggering event processing.

…d (bool)

eventFilters is typed as Js.Json.t so structural equality is safe and
needed for deep comparison. wildcard is bool so === suffices.

https://claude.ai/code/session_013sDgWWA3hVAoX6YVqJg1Ex
Pattern match on Static/Dynamic variants to use == (structural) for
Static data and === (referential) for Dynamic functions, avoiding
caml_equal throw on functional values.

https://claude.ai/code/session_013sDgWWA3hVAoX6YVqJg1Ex

@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

♻️ Duplicate comments (2)
packages/envio/src/HandlerRegister.res (2)

191-195: ~params payload is still a record literal, not a JS object.

The {contractName, eventName} at line 193 is a ReScript record. The past review comment requesting it be changed to {"contractName": contractName, "eventName": eventName} has not been addressed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/envio/src/HandlerRegister.res` around lines 191 - 195, The params
argument passed to Logging.createChildFrom in raiseDuplicateRegistration is
still a ReScript record literal; change it to a JS object literal so the logger
receives a plain JS object (replace the {contractName, eventName} record with an
object literal like {"contractName": contractName, "eventName": eventName})
while keeping the call site Logging.createChildFrom(~logger, ~params=...) and
then calling Logging.childError(fullMsg).

197-206: eventFilters structural equality concern is still unresolved.

The a.eventFilters == b.eventFilters comparison on Js.Json.t at line 203 was previously flagged. The past review comment requesting deep/stable equality for eventFilters has not been addressed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/envio/src/HandlerRegister.res` around lines 197 - 206, In
eventOptionsMatch, replace the structural JS equality check a.eventFilters ==
b.eventFilters with a proper deep JSON equality: call a JSON deep-compare helper
(e.g. deepEqualJson(a.eventFilters, b.eventFilters)) or compare canonicalized
strings (e.g. Js.Json.stringify(a.eventFilters) ===
Js.Json.stringify(b.eventFilters)) to ensure stable/deep equality; update or add
the helper (deepEqualJson) and use it in the (Some(a), Some(b)) branch inside
eventOptionsMatch to compare a.eventFilters and b.eventFilters.
🧹 Nitpick comments (1)
packages/envio/src/HandlerRegister.res (1)

189-189: Remove unused eventNamespace type.

The type is declared at line 189 but never referenced anywhere in the codebase. It's safe to remove.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/envio/src/HandlerRegister.res` at line 189, The declared type
eventNamespace ({contractName: string, eventName: string}) is unused and should
be removed; delete the type alias declaration for eventNamespace from
HandlerRegister.res to clean up dead code and ensure nothing else references it
(search for eventNamespace to confirm no usages before removing).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/envio/src/HandlerRegister.res`:
- Around line 236-241: The code writes the handler into eventRegistrations
before validating eventOptions, causing a partial-write if setEventOptions
throws; change the order in the None branch of the event registration flow so
you call setEventOptions(~contractName, ~eventName, ~eventOptions, ~logger)
first, then re-fetch the registration record (t) and only then call
set(~contractName, ~eventName, {...t, handler: Some(newHandler)}). Apply the
same change to the analogous block in setContractRegister so options are
validated first and the record is re-read before persisting the handler to avoid
overwriting freshly-set eventOptions; references: set, setEventOptions,
setContractRegister, eventRegistrations, hasRegistration, getHandler, t,
newHandler.

---

Duplicate comments:
In `@packages/envio/src/HandlerRegister.res`:
- Around line 191-195: The params argument passed to Logging.createChildFrom in
raiseDuplicateRegistration is still a ReScript record literal; change it to a JS
object literal so the logger receives a plain JS object (replace the
{contractName, eventName} record with an object literal like {"contractName":
contractName, "eventName": eventName}) while keeping the call site
Logging.createChildFrom(~logger, ~params=...) and then calling
Logging.childError(fullMsg).
- Around line 197-206: In eventOptionsMatch, replace the structural JS equality
check a.eventFilters == b.eventFilters with a proper deep JSON equality: call a
JSON deep-compare helper (e.g. deepEqualJson(a.eventFilters, b.eventFilters)) or
compare canonicalized strings (e.g. Js.Json.stringify(a.eventFilters) ===
Js.Json.stringify(b.eventFilters)) to ensure stable/deep equality; update or add
the helper (deepEqualJson) and use it in the (Some(a), Some(b)) branch inside
eventOptionsMatch to compare a.eventFilters and b.eventFilters.

---

Nitpick comments:
In `@packages/envio/src/HandlerRegister.res`:
- Line 189: The declared type eventNamespace ({contractName: string, eventName:
string}) is unused and should be removed; delete the type alias declaration for
eventNamespace from HandlerRegister.res to clean up dead code and ensure nothing
else references it (search for eventNamespace to confirm no usages before
removing).

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f059333 and 7e0f733.

📒 Files selected for processing (1)
  • packages/envio/src/HandlerRegister.res

Comment thread packages/envio/src/HandlerRegister.res

@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/envio/src/HandlerRegister.res (1)

175-177: ⚠️ Potential issue | 🔴 Critical

Fix .resi interface: getEventFilters return type must be updated to option<Internal.eventFilters>.

The pipeline confirms the build failure: the .resi interface still declares getEventFilters as returning option<Js.Json.t>, but after the eventOptions field type change (line 4) the implementation now returns option<Internal.eventFilters>. Update the interface file accordingly.

- let getEventFilters: (~contractName: string, ~eventName: string) => option<Js.Json.t>
+ let getEventFilters: (~contractName: string, ~eventName: string) => option<Internal.eventFilters>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/envio/src/HandlerRegister.res` around lines 175 - 177, The .resi
interface declaration for getEventFilters is outdated: update the return type of
getEventFilters from option<Js.Json.t> to option<Internal.eventFilters> to match
the implementation (which now returns the eventOptions -> eventFilters chain of
type Internal.eventFilters); edit the getEventFilters signature in the .resi to
use option<Internal.eventFilters> so the interface aligns with the changed
eventOptions field type and the implementation in get(~contractName,
~eventName).eventOptions -> Belt.Option.flatMap(...).
♻️ Duplicate comments (3)
packages/envio/src/HandlerRegister.res (3)

285-291: Same partial-write bug in setContractRegister's None branch.

contractRegister is committed (line 287–290) before setEventOptions validates for conflicts (line 291). Apply the same validate-first, re-fetch-then-write fix here.

🐛 Proposed fix
     | None =>
-      set(~contractName, ~eventName, {
-        ...t,
-        contractRegister: Some(newContractRegister),
-      })
-      setEventOptions(~contractName, ~eventName, ~eventOptions, ~logger)
+      setEventOptions(~contractName, ~eventName, ~eventOptions, ~logger)
+      let t = get(~contractName, ~eventName)
+      set(~contractName, ~eventName, {
+        ...t,
+        contractRegister: Some(newContractRegister),
+      })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/envio/src/HandlerRegister.res` around lines 285 - 291, In
setContractRegister's None branch avoid the partial-write by validating event
options before committing: call setEventOptions(~contractName, ~eventName,
~eventOptions, ~logger) first (so conflicts are detected), then re-fetch the
current register state (the same t/contractRegister) to ensure no race, and only
after successful validation and re-fetch assign contractRegister =
Some(newContractRegister) via set(~contractName, ~eventName, { ... }) to perform
the write; ensure you reference the existing symbols setEventOptions and the
branch updating contractRegister when applying the change.

244-250: Partial-write bug: handler is stored before setEventOptions validates for conflicts.

In the None branch, set(...) commits the new handler to eventRegistrations (line 246–249) before setEventOptions runs (line 250). If setEventOptions raises (e.g., a concurrent setContractRegister already stored conflicting options), the handler is permanently written while the registration is considered rejected — leaving the registry corrupt. Validate options first, then re-fetch t before writing:

🐛 Proposed fix
     | None =>
-      set(~contractName, ~eventName, {
-        ...t,
-        handler: Some(newHandler),
-      })
-      setEventOptions(~contractName, ~eventName, ~eventOptions, ~logger)
+      setEventOptions(~contractName, ~eventName, ~eventOptions, ~logger)
+      let t = get(~contractName, ~eventName)
+      set(~contractName, ~eventName, {
+        ...t,
+        handler: Some(newHandler),
+      })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/envio/src/HandlerRegister.res` around lines 244 - 250, The handler
is being written to eventRegistrations before setEventOptions runs, causing a
partial-write if setEventOptions fails; change the None branch so you first call
setEventOptions(~contractName, ~eventName, ~eventOptions, ~logger) to
validate/raise, then re-fetch t from eventRegistrations (to detect concurrent
changes), and only after reloading/confirming t is still None call
set(~contractName, ~eventName, {...t, handler: Some(newHandler)}) to store the
handler; use the same identifiers (set, setEventOptions, eventRegistrations,
handler) so the update validates options before mutating state and avoids
leaving a stale handler on errors.

191-195: Use a JS object literal for the ~params payload in raiseDuplicateRegistration.

~params={contractName, eventName} is a record literal; the coding guidelines require objects for data passed between functions as payloads.

🛠️ Suggested change
-  Logging.createChildFrom(~logger, ~params={contractName, eventName})->Logging.childError(fullMsg)
+  Logging.createChildFrom(
+    ~logger,
+    ~params={"contractName": contractName, "eventName": eventName},
+  )->Logging.childError(fullMsg)

As per coding guidelines, "Use records when working with structured data, and objects to conveniently pass payload data between functions".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/envio/src/HandlerRegister.res` around lines 191 - 195, The params
argument to Logging.createChildFrom in raiseDuplicateRegistration is a record
literal; change it to a JS object literal so payloads use objects per
guidelines. Replace ~params={contractName, eventName} with an object-style
payload (e.g. ~params={"contractName": contractName, "eventName": eventName}) in
the raiseDuplicateRegistration function before calling Logging.createChildFrom,
and ensure the resulting value still type-checks with Logging.createChildFrom
and Logging.childError.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@packages/envio/src/HandlerRegister.res`:
- Around line 175-177: The .resi interface declaration for getEventFilters is
outdated: update the return type of getEventFilters from option<Js.Json.t> to
option<Internal.eventFilters> to match the implementation (which now returns the
eventOptions -> eventFilters chain of type Internal.eventFilters); edit the
getEventFilters signature in the .resi to use option<Internal.eventFilters> so
the interface aligns with the changed eventOptions field type and the
implementation in get(~contractName, ~eventName).eventOptions ->
Belt.Option.flatMap(...).

---

Duplicate comments:
In `@packages/envio/src/HandlerRegister.res`:
- Around line 285-291: In setContractRegister's None branch avoid the
partial-write by validating event options before committing: call
setEventOptions(~contractName, ~eventName, ~eventOptions, ~logger) first (so
conflicts are detected), then re-fetch the current register state (the same
t/contractRegister) to ensure no race, and only after successful validation and
re-fetch assign contractRegister = Some(newContractRegister) via
set(~contractName, ~eventName, { ... }) to perform the write; ensure you
reference the existing symbols setEventOptions and the branch updating
contractRegister when applying the change.
- Around line 244-250: The handler is being written to eventRegistrations before
setEventOptions runs, causing a partial-write if setEventOptions fails; change
the None branch so you first call setEventOptions(~contractName, ~eventName,
~eventOptions, ~logger) to validate/raise, then re-fetch t from
eventRegistrations (to detect concurrent changes), and only after
reloading/confirming t is still None call set(~contractName, ~eventName, {...t,
handler: Some(newHandler)}) to store the handler; use the same identifiers (set,
setEventOptions, eventRegistrations, handler) so the update validates options
before mutating state and avoids leaving a stale handler on errors.
- Around line 191-195: The params argument to Logging.createChildFrom in
raiseDuplicateRegistration is a record literal; change it to a JS object literal
so payloads use objects per guidelines. Replace ~params={contractName,
eventName} with an object-style payload (e.g. ~params={"contractName":
contractName, "eventName": eventName}) in the raiseDuplicateRegistration
function before calling Logging.createChildFrom, and ensure the resulting value
still type-checks with Logging.createChildFrom and Logging.childError.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7e0f733 and 74f4d4f.

📒 Files selected for processing (1)
  • packages/envio/src/HandlerRegister.res

Call setEventOptions before set in the None branches of setHandler
and setContractRegister. Re-fetch the record after setEventOptions
so the handler write includes any freshly-set eventOptions.

https://claude.ai/code/session_013sDgWWA3hVAoX6YVqJg1Ex
@DZakh
DZakh merged commit ff3d6b1 into main Feb 25, 2026
12 of 13 checks passed
@DZakh
DZakh deleted the claude/detect-duplicate-handlers-Km3yA branch February 25, 2026 05:35
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