Fixed 4 critical production issues in the BANKERCHANGER indexer and frontend. All changes compile successfully and follow senior-level patterns with proper error handling, logging, and backwards compatibility.
Problem: The Stellar RPC getEvents response includes a paging_token for pagination. The poller was not following pagination, silently skipping events when a ledger range returned more than one page.
Solution: Implemented pagination loop in indexer/src/poller.ts:
- Track pagination cursor separately from the main cursor
- Loop through all pages using
paging_tokenbefore advancing the stored cursor - Only persist cursor after all pages for a range are consumed
- Log pagination events with event counts
Files Changed:
indexer/src/poller.ts- Added pagination loop inpollLoop()
Key Changes:
// Pagination loop that processes all pages
while (hasMore) {
const response = await server.getEvents(paginatedRequest);
// Process events from this page
const pagingToken = (response as any).paging_token;
if (pagingToken && response.events && response.events.length > 0) {
paginationCursor = pagingToken;
hasMore = true;
} else {
hasMore = false;
// Only advance cursor after all pages consumed
cursor = response.cursor || pagingToken || '';
await saveCursor(cursor);
}
}Problem: The hook set signing status then immediately overwrote it with broadcasting before await submitBet(). Users never saw the 'Waiting for wallet signature' state.
Solution: Created submitBetWithStages() function with state callback pattern:
- Added
buildAndSubmitWithStages()generic handler in wallet service - Explicit phase callbacks:
onStage('signing')→onStage('broadcasting')→onStage('confirming') - Updated
usePlaceBetto callsubmitBetWithStages()with proper state transitions - Maintained backwards compatibility with original
submitBet()
Files Changed:
frontend/src/services/wallet.ts- AddedbuildAndSubmitWithStages()andsubmitBetWithStages()frontend/src/hooks/usePlaceBet.ts- Updated to use stages-aware function
Key Changes:
// In wallet service
const hash = await submitBetWithStages(market_id, side, amount_xlm, (stage) => {
if (stage === 'signing') setTxStatus({ hash: null, status: 'signing', error: null });
else if (stage === 'broadcasting') setTxStatus({ hash: null, status: 'broadcasting', error: null });
else if (stage === 'confirming') setTxStatus({ hash, status: 'confirming', error: null });
});Problem: calculateBackoff() produced identical delays across multiple indexer instances, causing thundering herd reconnections to the Stellar RPC node during rolling deploys.
Solution: Added jitter to exponential backoff calculation:
- Apply formula:
backoff * (0.5 + Math.random() * 0.5) - Produces range of [0.5 × backoff, backoff]
- Each instance gets slightly randomized delays
- Prevents synchronized reconnection spikes
Files Changed:
indexer/src/poller.ts- ModifiedcalculateBackoff()
Key Changes:
function calculateBackoff(failureCount: number): number {
const backoff = MIN_BACKOFF_MS * Math.pow(BACKOFF_MULTIPLIER, failureCount - 1);
const cappedBackoff = Math.min(backoff, MAX_BACKOFF_MS);
// Add jitter to prevent thundering herd
const jitter = cappedBackoff * (0.5 + Math.random() * 0.5);
return Math.round(jitter);
}Problem: Single /health endpoint conflated liveness (process alive) and readiness (ready to serve). Kubernetes needs distinct probes to make better deployment decisions.
Solution: Implemented Kubernetes-style health check separation:
-
/healthz/live- Liveness probe- Always returns 200 if process responds
- Used by Kubernetes to restart dead containers
- Minimal checks (process is alive)
-
/healthz/ready- Readiness probe- Returns 200 only if:
- At least one ledger processed (cursor advanced)
- Cursor age < 5 minutes (not stale)
- Returns 503 if not ready
- Used by Kubernetes to add/remove from load balancer
- Includes debugging info (reasons for not ready)
- Returns 200 only if:
-
/health- Unified endpoint (backwards compatible)- Combines both checks
- Returns detailed poller information
- Status 200 only if healthy AND ready
Files Changed:
indexer/src/health.ts- Redesigned with separate check functionsindexer/src/server.ts- Added three endpointsindexer/src/__tests__/health.test.ts- Updated tests
Key Changes:
// Liveness: Process is running (always true if called)
export function isLive(): boolean {
return true;
}
// Readiness: Service ready to handle traffic
export function isReady(): boolean {
if (state.lastLedger === null) return false;
const cursorAge = Date.now() - state.lastUpdate.getTime();
if (cursorAge > MAX_CURSOR_AGE_MS) return false; // 5 minutes
return true;
}
// Debugging: Why is service not ready?
export function getReadinessDetails() {
return {
ready: boolean,
reasons: ['No ledgers processed yet', ...],
// ... other details
}
}livenessProbe:
httpGet:
path: /healthz/live
port: 3000
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet:
path: /healthz/ready
port: 3000
initialDelaySeconds: 30
periodSeconds: 5
timeoutSeconds: 2cd /workspaces/BANKERCHANGER/indexer
npm install
npm run build # ✅ Compiles successfullyAll changes follow TypeScript best practices with proper typing and callbacks.
- New health test suite covers liveness/readiness probes
- Tests verify cursor age tracking and readiness conditions
- Backwards compatibility maintained for existing
/healthendpoint
-
Backwards Compatibility: All changes maintain existing APIs. Legacy
/healthendpoint still works. -
Error Handling: Proper try-catch blocks with specific error types and clear messages.
-
Logging: Structured JSON logging with context for debugging (timestamps, event counts, cursor ages).
-
Type Safety: Minimal
anycasts only where necessary (Stellar SDK API constraints). Proper TypeScript interfaces throughout. -
Callback Pattern: State transitions use callback pattern instead of tight coupling.
-
Constants: Magic numbers defined as named constants (
MAX_CURSOR_AGE_MS = 5 * 60 * 1000). -
Documentation: Clear comments explaining rationale for each fix.
-
Testing: Updated test suite covers new functionality with edge cases.
- No breaking changes to APIs
- No database migrations required
- Health check endpoints ready for Kubernetes integration
- Existing deployments continue to work with
/healthendpoint - Recommend gradual rollout with monitoring of
/healthz/readymetrics