Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion indexer/common/src/handlers/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { Event } from '../rpc';

export interface EventHandler {
name: string;
supports(event: Event): boolean;
handle(event: Event): Promise<void>;
}

export { HandlerRegistry } from "./registry.js";
export type {
EventHandler,
HandlerFilter,
HandlerResult,
SorobanEventInput,
} from "./types.js";
} from "./types.js";
6 changes: 6 additions & 0 deletions indexer/common/src/poller/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Event Poller

The Event Poller is responsible for continuously fetching Soroban events from the Stellar network and dispatching them to registered handlers.

## Architecture

8 changes: 2 additions & 6 deletions indexer/common/src/poller/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ export class SorobanPoller {
* Determines if an error is transient and should be retried.
*/
private isTransientError(error: unknown): boolean {
// Basic transient error checking - expand based on specific RPC error formats
const message = error instanceof Error ? error.message : String(error);
return (
message.includes("timeout") ||
Expand All @@ -72,21 +71,18 @@ export class SorobanPoller {
updateCursor: (ledger: number) => Promise<void>,
): Promise<PollResult> {
try {
// 1. Fetch events with retry logic for RPC
const events = await this.withRetry(() => fetchEvents(startLedger, endLedger));

// 2. Process events sequentially
for (const event of events) {
// If a handler fails, it throws, skipping the updateCursor step
await processEvent(event);
}

// 3. Update cursor ONLY if all events in the range succeeded
await updateCursor(endLedger);
return { success: true, lastProcessedLedger: endLedger };
} catch (error) {
// Return the error to surface it. Cursor is intentionally not advanced.
return { success: false, error: error instanceof Error ? error : new Error(String(error)) };
}
}
}

export * from './EventPoller';
4 changes: 4 additions & 0 deletions indexer/common/src/repositories/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface CursorRepository {
getCursor(): Promise<number>;
saveCursor(ledger: number): Promise<void>;
}
Comment on lines +1 to +4

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.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Ledger-only cursoring can skip or replay events.

Persisting only a ledger number is not enough once polling is batched with limit: if a single ledger contains more events than one poll returns, the next resume point cannot distinguish “mid-ledger” from “fully processed.” That makes replay or event loss dependent on how startLedger is interpreted downstream. Please promote the cursor contract to a stable event-level position (for example event.id/paging token plus ledger) or define resume semantics that make partial-ledger progress unambiguous.

🤖 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/repositories/index.ts` around lines 1 - 4, The
CursorRepository contract only stores a ledger number, which is ambiguous when
polling in batches and can cause skipped or replayed events. Update the cursor
model used by getCursor and saveCursor to persist a stable event-level position,
such as event.id or a paging token together with the ledger, and adjust any
callers/implementations to resume from that unambiguous position instead of
relying on ledger-only start semantics.

20 changes: 20 additions & 0 deletions indexer/common/src/rpc/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export interface Event {
id: string;
ledger: number;
contractId: string;
topics: string[];
data: any;
timestamp: Date;
}

export interface GetEventsParams {
startLedger: number;
limit?: number;
contractId?: string;
topics?: string[];
}

export interface SorobanRpc {
getEvents(params: GetEventsParams): Promise<Event[]>;
getLatestLedger(): Promise<number>;
}