Skip to content

feat(indexer): add handler registry, stream lifecycle handlers, distribution event handlers#48

Closed
pre-cious-Igwealor wants to merge 1 commit into
Fundable-Protocol:devfrom
pre-cious-Igwealor:fix/precious-backend-30-35-38
Closed

feat(indexer): add handler registry, stream lifecycle handlers, distribution event handlers#48
pre-cious-Igwealor wants to merge 1 commit into
Fundable-Protocol:devfrom
pre-cious-Igwealor:fix/precious-backend-30-35-38

Conversation

@pre-cious-Igwealor

@pre-cious-Igwealor pre-cious-Igwealor commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Summary

issue #30 — Handler registration interface (indexer/common/src/handlers/)

  • types.tsSorobanEvent, HandlerResult, EventHandler<T>, HandlerFilter, HandlerEntry, HandlerRegistry interface
  • registry.tsEventHandlerRegistry implementation: filter by contractId, eventName, or both; empty filter matches all; sequential dispatch with per-handler error capture
  • 15 unit tests: matching rules, registration order, error isolation, idempotency, factory independence
  • Exports added to common/src/index.ts so domain packages import without coupling to internals

issue #35 — Stream funding, withdrawal, and cancel handlers (indexer/streams/src/handlers/)

  • streamFunded.tsStreamFunded event handler
  • streamWithdrawal.tsStreamWithdrawal event handler
  • streamCancel.tsStreamCancelled event handler
  • utils.ts — shared parseEventData, requireStringField, requireTopic, getEventIdentity
  • 24 unit tests: valid parse, wrong topic, missing/empty/wrong-type fields, record mapping, idempotency
  • Each handler follows the same parse → validate → map → identity pattern as streamCreated.ts (PR feat: implement StreamCreated handler #43)

issue #38 — Distribution event handlers (indexer/distributions/src/handlers/)

  • distributionCreated.tsDistributionCreated handler (includes recipientCount: number validated as integer)
  • tokensClaimed.tsTokensClaimed handler
  • batchPause.tsBatchPaused and BatchResumed handlers
  • Record types align with DistributionBatch + ClaimAction entity schema (PR feat(indexer/distributions): define distributions database schema #44)
  • 26 unit tests: happy path, wrong topics, missing fields, integer validation, identity uniqueness, idempotency

closes #30
closes #35
closes #38

Summary by CodeRabbit

  • New Features

    • Added support for handling additional event types across stream and distribution indexing, including funding, withdrawal, cancellation, creation, claims, and batch pause/resume events.
    • Introduced a shared event handler registry for registering, matching, and dispatching event handlers in order.
    • Added stable event identity generation to help track and deduplicate indexed events.
  • Bug Fixes

    • Improved validation for event payloads and topics to better reject malformed or unexpected event data.
    • Dispatch now continues processing other handlers even if one handler fails.

…ibution event handlers

issue Fundable-Protocol#30 — indexer/common/src/handlers/
- HandlerRegistry interface with register/match/dispatch API
- Filter by contractId, eventName, or both; empty filter matches all
- EventHandlerRegistry implementation: sequential dispatch, error captured per handler
- 15 unit tests: matching rules, ordering, error isolation, idempotency

issue Fundable-Protocol#35 — indexer/streams/src/handlers/
- StreamFunded, StreamWithdrawal, StreamCancelled handlers
- Shared utils: parseEventData, requireStringField, requireTopic, getEventIdentity
- Each handler: parse → validate → map → identity following streamCreated.ts pattern
- 24 unit tests: valid parse, wrong topic, missing/empty fields, record shape, idempotency

issue Fundable-Protocol#38 — indexer/distributions/src/handlers/
- DistributionCreated, TokensClaimed, BatchPaused, BatchResumed handlers
- requireIntField validator for recipientCount (integer, not string)
- Record types align with DistributionBatch + ClaimAction entity schema (PR Fundable-Protocol#44)
- 26 unit tests: happy path, wrong topics, missing fields, identity uniqueness, idempotency

closes Fundable-Protocol#30
closes Fundable-Protocol#35
closes Fundable-Protocol#38
@drips-wave

drips-wave Bot commented Jun 27, 2026

Copy link
Copy Markdown

@pre-cious-Igwealor Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a shared handler registry API and shared event/record types, then implements stream and distribution event handlers that parse payloads, map records, derive event identities, and validate the behavior with Vitest tests.

Changes

Shared handler registry

Layer / File(s) Summary
Registry contract and dispatch
indexer/common/src/handlers/types.ts, indexer/common/src/handlers/registry.ts, indexer/common/src/index.ts
Shared handler types, filter matching, registry dispatch, and public re-exports are added.
Registry tests
indexer/common/src/handlers/registry.test.ts
Matching, dispatch, and factory behavior are covered in Vitest cases.

Stream lifecycle handlers

Layer / File(s) Summary
Stream contract and helpers
indexer/streams/src/handlers/types.ts, indexer/streams/src/handlers/utils.ts
Shared stream event shapes, identity creation, JSON parsing, topic checks, and field validation helpers are added.
Stream handlers
indexer/streams/src/handlers/streamFunded.ts, indexer/streams/src/handlers/streamWithdrawal.ts, indexer/streams/src/handlers/streamCancel.ts
Funded, withdrawal, and cancel handlers parse payloads, build records from event metadata, and return records with identities.
Stream lifecycle tests
indexer/streams/src/handlers/stream-lifecycle.test.ts
Funded, withdrawal, and cancel cases cover parsing, mapping, identity generation, and idempotency.

Distribution event handlers

Layer / File(s) Summary
Distribution contract and helpers
indexer/distributions/src/handlers/types.ts, indexer/distributions/src/handlers/utils.ts
Shared distribution event shapes, identity creation, JSON parsing, topic checks, and string/integer field validation helpers are added.
Distribution handlers
indexer/distributions/src/handlers/distributionCreated.ts, indexer/distributions/src/handlers/tokensClaimed.ts, indexer/distributions/src/handlers/batchPause.ts
Created, claimed, paused, and resumed handlers parse payloads, build records from event metadata, and return records with identities.
Distribution event tests
indexer/distributions/src/handlers/distribution-events.test.ts
Created, claimed, paused, and resumed cases cover parsing, mapping, identity generation, and idempotency.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 I hopped through handlers, one by one,
With registry ears and event-scent sun.
Streams and batches now dance in line,
Identities blink, all neat and fine.
I nibble carrots, job well done!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description summarizes changes, but it omits required template sections like Area, Scope, Verification, Indexer Safety, and Notes. Add the missing template sections and fill in the relevant checkboxes, validation commands, safety notes, and issue-closing note.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly names the registry plus stream and distribution handler additions.
Linked Issues check ✅ Passed The PR covers the requested registry, stream, and distribution handlers, plus tests and exports, matching #30, #35, and #38.
Out of Scope Changes check ✅ Passed No obvious out-of-scope changes are present; the touched files all align with the three linked indexer handler tasks.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


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.

@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

🧹 Nitpick comments (3)
indexer/streams/src/handlers/stream-lifecycle.test.ts (1)

56-72: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Good coverage; consider one more parse case.

These tests exercise invalid JSON and missing/empty/wrong-type fields well. If you adopt the non-object-JSON guard suggested in utils.ts, add a case where event.data = "null" to lock in the clean validation error rather than a raw TypeError.

🤖 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 `@indexer/streams/src/handlers/stream-lifecycle.test.ts` around lines 56 - 72,
The parseStreamFundedPayload tests cover invalid JSON and missing fields, but
they should also lock in the non-object JSON guard from utils.ts. Add a test
case in stream-lifecycle.test.ts where event.data is set to "null" and assert
the same clean validation error path is thrown instead of allowing a raw
TypeError, using parseStreamFundedPayload and makeEvent as the entry points.
indexer/streams/src/handlers/utils.ts (1)

1-37: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Duplicated helpers across streams and distributions.

getEventIdentity, parseEventData, requireStringField, and requireTopic here are essentially identical to indexer/distributions/src/handlers/utils.ts. Since @fundable-indexer/common is already a workspace dependency, these generic helpers are a good candidate to hoist into common (parameterized by the event identity fields) to prevent the two copies from diverging.

🤖 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 `@indexer/streams/src/handlers/utils.ts` around lines 1 - 37, The helper
functions in this module are duplicated in the distributions handlers, so move
the shared logic from getEventIdentity, parseEventData, requireStringField, and
requireTopic into `@fundable-indexer/common` and update both callers to use the
shared implementation. Make getEventIdentity generic enough to work across both
event shapes by parameterizing the identity fields, then keep the
streams-specific wrapper or direct import here aligned with the common version
so the two copies cannot diverge.
indexer/streams/src/handlers/types.ts (1)

3-10: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the redundant StreamEvent field redeclarations. StreamEvent repeats fields already declared on SorobanEvent; if no extra fields are needed, use the base type directly or make this a type alias.

🤖 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 `@indexer/streams/src/handlers/types.ts` around lines 3 - 10, The StreamEvent
definition is redundantly redeclaring fields already inherited from
SorobanEvent, so simplify the type in types.ts by removing the duplicate
property list. If StreamEvent adds no new shape beyond SorobanEvent, replace the
interface with a direct type alias to SorobanEvent or otherwise rely on the base
type; keep the change centered on the StreamEvent symbol so downstream handlers
continue using the same event contract.
🤖 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 `@indexer/common/src/handlers/types.ts`:
- Around line 17-23: The shared SorobanEvent contract is inconsistent with how
eventName filtering works: SorobanEvent.topics is documented as raw XDR, but the
registry and tests assume topics[0] is the decoded event name used by eventName
filtering. Update the shared type and its related handler/registry logic
(SorobanEvent, eventName matching, and any tests) so they all agree on whether
topics contains decoded names or encoded values, and make the filter compare
against the same representation everywhere.

In `@indexer/distributions/src/handlers/utils.ts`:
- Around line 7-13: The parseEventData helper is only guarding invalid JSON, but
it still allows valid non-object payloads like null, arrays, or primitives to
pass through and later break requireStringField/requireIntField with uncaught
TypeError. Update parseEventData in utils.ts to validate the parsed value is a
non-null plain object before returning it, and throw the same validation error
path when the payload is not an object so downstream field access stays safe.

In `@indexer/streams/src/handlers/utils.ts`:
- Around line 7-13: parseEventData currently accepts any valid JSON and can
return non-object values like null or primitives, which later breaks
requireStringField with a raw TypeError. Update parseEventData in utils.ts to
validate that JSON.parse(event.data) returns a non-null object before casting,
and throw a clear validation error when the parsed value is not an object; keep
the invalid-JSON error path distinct from the shape check.

---

Nitpick comments:
In `@indexer/streams/src/handlers/stream-lifecycle.test.ts`:
- Around line 56-72: The parseStreamFundedPayload tests cover invalid JSON and
missing fields, but they should also lock in the non-object JSON guard from
utils.ts. Add a test case in stream-lifecycle.test.ts where event.data is set to
"null" and assert the same clean validation error path is thrown instead of
allowing a raw TypeError, using parseStreamFundedPayload and makeEvent as the
entry points.

In `@indexer/streams/src/handlers/types.ts`:
- Around line 3-10: The StreamEvent definition is redundantly redeclaring fields
already inherited from SorobanEvent, so simplify the type in types.ts by
removing the duplicate property list. If StreamEvent adds no new shape beyond
SorobanEvent, replace the interface with a direct type alias to SorobanEvent or
otherwise rely on the base type; keep the change centered on the StreamEvent
symbol so downstream handlers continue using the same event contract.

In `@indexer/streams/src/handlers/utils.ts`:
- Around line 1-37: The helper functions in this module are duplicated in the
distributions handlers, so move the shared logic from getEventIdentity,
parseEventData, requireStringField, and requireTopic into
`@fundable-indexer/common` and update both callers to use the shared
implementation. Make getEventIdentity generic enough to work across both event
shapes by parameterizing the identity fields, then keep the streams-specific
wrapper or direct import here aligned with the common version so the two copies
cannot diverge.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 198df6f4-023f-4407-9e06-75afe3629abd

📥 Commits

Reviewing files that changed from the base of the PR and between bcfa08d and 8f344ec.

📒 Files selected for processing (16)
  • indexer/common/src/handlers/registry.test.ts
  • indexer/common/src/handlers/registry.ts
  • indexer/common/src/handlers/types.ts
  • indexer/common/src/index.ts
  • indexer/distributions/src/handlers/batchPause.ts
  • indexer/distributions/src/handlers/distribution-events.test.ts
  • indexer/distributions/src/handlers/distributionCreated.ts
  • indexer/distributions/src/handlers/tokensClaimed.ts
  • indexer/distributions/src/handlers/types.ts
  • indexer/distributions/src/handlers/utils.ts
  • indexer/streams/src/handlers/stream-lifecycle.test.ts
  • indexer/streams/src/handlers/streamCancel.ts
  • indexer/streams/src/handlers/streamFunded.ts
  • indexer/streams/src/handlers/streamWithdrawal.ts
  • indexer/streams/src/handlers/types.ts
  • indexer/streams/src/handlers/utils.ts

Comment on lines +17 to +23
/** Minimal shape of a Soroban event delivered to handlers. */
export interface SorobanEvent extends SorobanEventIdentity {
/** Array of XDR-encoded topics. The first element is conventionally the event name. */
topics: string[];
/** JSON-encoded event data. */
data: string;
}

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 | 🟠 Major | ⚡ Quick win

Align the shared topic contract with eventName filtering.

SorobanEvent.topics is documented as XDR-encoded here, but the registry and tests treat topics[0] as a decoded name like "StreamCreated". If dispatch receives raw Soroban topics, every eventName filter will miss. Either expose decoded topics/event names in the shared event contract or make the filter explicitly operate on the encoded value.

🤖 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 `@indexer/common/src/handlers/types.ts` around lines 17 - 23, The shared
SorobanEvent contract is inconsistent with how eventName filtering works:
SorobanEvent.topics is documented as raw XDR, but the registry and tests assume
topics[0] is the decoded event name used by eventName filtering. Update the
shared type and its related handler/registry logic (SorobanEvent, eventName
matching, and any tests) so they all agree on whether topics contains decoded
names or encoded values, and make the filter compare against the same
representation everywhere.

Comment on lines +7 to +13
export function parseEventData(event: DistributionEvent): Record<string, unknown> {
try {
return JSON.parse(event.data) as Record<string, unknown>;
} catch {
throw new Error("Failed to parse event data: invalid JSON");
}
}

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Guard against non-object JSON payloads.

JSON.parse can return null (or a primitive/array) for valid JSON that isn't an object. When it returns null, the subsequent parsed[field] access in requireStringField/requireIntField throws an uncaught TypeError ("Cannot read properties of null") instead of the intended validation error, defeating the clean error path here.

🛡️ Proposed guard
 export function parseEventData(event: DistributionEvent): Record<string, unknown> {
+  let parsed: unknown;
   try {
-    return JSON.parse(event.data) as Record<string, unknown>;
+    parsed = JSON.parse(event.data);
   } catch {
     throw new Error("Failed to parse event data: invalid JSON");
   }
+  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
+    throw new Error("Failed to parse event data: expected a JSON object");
+  }
+  return parsed as Record<string, unknown>;
 }
📝 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
export function parseEventData(event: DistributionEvent): Record<string, unknown> {
try {
return JSON.parse(event.data) as Record<string, unknown>;
} catch {
throw new Error("Failed to parse event data: invalid JSON");
}
}
export function parseEventData(event: DistributionEvent): Record<string, unknown> {
let parsed: unknown;
try {
parsed = JSON.parse(event.data);
} catch {
throw new Error("Failed to parse event data: invalid JSON");
}
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
throw new Error("Failed to parse event data: expected a JSON object");
}
return parsed as Record<string, unknown>;
}
🤖 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 `@indexer/distributions/src/handlers/utils.ts` around lines 7 - 13, The
parseEventData helper is only guarding invalid JSON, but it still allows valid
non-object payloads like null, arrays, or primitives to pass through and later
break requireStringField/requireIntField with uncaught TypeError. Update
parseEventData in utils.ts to validate the parsed value is a non-null plain
object before returning it, and throw the same validation error path when the
payload is not an object so downstream field access stays safe.

Comment on lines +7 to +13
export function parseEventData(event: StreamEvent): Record<string, unknown> {
try {
return JSON.parse(event.data) as Record<string, unknown>;
} catch {
throw new Error("Failed to parse event data: invalid JSON");
}
}

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

parseEventData doesn't guard non-object JSON.

JSON.parse succeeds for valid-but-non-object JSON like null, 42, or "abc". For null, the subsequent parsed[field] access in requireStringField throws a raw TypeError ("Cannot read properties of null") instead of the intended validation error. Consider asserting the parsed value is a non-null object.

🛡️ Proposed guard
 export function parseEventData(event: StreamEvent): Record<string, unknown> {
   try {
-    return JSON.parse(event.data) as Record<string, unknown>;
+    const parsed = JSON.parse(event.data);
+    if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
+      throw new Error("Failed to parse event data: expected a JSON object");
+    }
+    return parsed as Record<string, unknown>;
   } catch {
     throw new Error("Failed to parse event data: invalid JSON");
   }
 }

Note: the throw inside try is recaught here; if you want the object-shape error distinct from the JSON error, move the shape check outside the try.

📝 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
export function parseEventData(event: StreamEvent): Record<string, unknown> {
try {
return JSON.parse(event.data) as Record<string, unknown>;
} catch {
throw new Error("Failed to parse event data: invalid JSON");
}
}
export function parseEventData(event: StreamEvent): Record<string, unknown> {
try {
const parsed = JSON.parse(event.data);
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
throw new Error("Failed to parse event data: expected a JSON object");
}
return parsed as Record<string, unknown>;
} catch {
throw new Error("Failed to parse event data: invalid JSON");
}
}
🤖 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 `@indexer/streams/src/handlers/utils.ts` around lines 7 - 13, parseEventData
currently accepts any valid JSON and can return non-object values like null or
primitives, which later breaks requireStringField with a raw TypeError. Update
parseEventData in utils.ts to validate that JSON.parse(event.data) returns a
non-null object before casting, and throw a clear validation error when the
parsed value is not an object; keep the invalid-JSON error path distinct from
the shape check.

@pre-cious-Igwealor

Copy link
Copy Markdown
Contributor Author

Superseded by #49 which covers this same work plus #33 (GraphQL schema) in a single PR.

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.

Implement distribution event handlers Implement stream funding, withdrawal, and cancel handlers Add event handler registration interface

1 participant