Skip to content
Merged
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
9 changes: 9 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ JWT_SECRET=your-jwt-secret-here
# Indexer configuration
INDEXER_POLL_INTERVAL_MS=10000

# Fallback polling mode (default: false).
# When false, the indexer uses Stellar RPC getEvents with cursor-based pagination.
# When true, the indexer falls back to the legacy fixed-interval polling approach.
INDEXER_FALLBACK_POLLING_ENABLED=false

# Polling interval for fallback mode (milliseconds, minimum 5000, default 10000).
# Only used when INDEXER_FALLBACK_POLLING_ENABLED=true.
INDEXER_FALLBACK_POLL_INTERVAL_MS=10000

# Reconciliation job interval in milliseconds (minimum 10000, default 60000 = 1 minute)
RECONCILIATION_INTERVAL_MS=60000

Expand Down
24 changes: 12 additions & 12 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 16 additions & 4 deletions backend/src/config/validateEnv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ const archiveCronIntervalSchema = z
message: "must be a valid number >= 60000 (minimum 1 minute)",
});

// Indexer fallback polling interval validation
const fallbackPollIntervalSchema = z
.string()
.transform((val: string) => parseInt(val, 10))
.refine((val: number) => !isNaN(val) && val >= 1000, {
message: "must be a valid number >= 1000 (minimum 1 second)",
});

Comment on lines +58 to +65

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Fallback poll interval minimum disagrees across docs, schema, and runtime floor (5000ms vs 1000ms). backend/.env.example documents a 5000ms minimum, but backend/src/config/validateEnv.ts's Zod schema enforces >= 1000 and backend/src/services/indexer.ts's runtime floor is Math.max(1000, ...) — both consistent with each other at 1000ms, but not with the documented 5000ms.

  • backend/src/config/validateEnv.ts#L58-L65: either update the .refine(val >= 1000, ...) check (and message) to >= 5000 if 5000ms is the intended floor, or update the .env.example comment to say 1000ms if 1000ms is intended.
  • backend/.env.example#L39-L41: update the "minimum 5000" comment to match whichever floor is chosen in validateEnv.ts.
  • backend/src/services/indexer.ts#L105-L107: update Math.max(1000, FALLBACK_POLL_INTERVAL_MS) to use the same floor constant as validateEnv.ts (ideally sourced from one shared constant) so all three stay in sync.
📍 Affects 3 files
  • backend/src/config/validateEnv.ts#L58-L65 (this comment)
  • backend/.env.example#L39-L41
  • backend/src/services/indexer.ts#L105-L107
🤖 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 `@backend/src/config/validateEnv.ts` around lines 58 - 65, Align the fallback
polling interval floor across all three sites, using one shared constant where
possible: update backend/src/config/validateEnv.ts lines 58-65 and its
validation message, backend/.env.example lines 39-41, and
backend/src/services/indexer.ts lines 105-107 so they consistently enforce and
document the intended minimum, with the runtime Math.max using the same floor as
the schema.

// Admin API key validation
const adminApiKeySchema = z
.string()
Expand Down Expand Up @@ -84,7 +92,8 @@ const envSchema = z.object({
INDEXER_POLL_INTERVAL_MS: indexerPollIntervalSchema.optional().default(10000),
RECONCILIATION_INTERVAL_MS: reconciliationIntervalSchema.optional().default(60000),
ARCHIVE_CRON_INTERVAL_MS: archiveCronIntervalSchema.optional().default(86400000),
WEBHOOK_DEAD_LETTER_PRUNE_INTERVAL_MS: archiveCronIntervalSchema.optional().default(86400000),
INDEXER_FALLBACK_POLLING_ENABLED: z.string().optional().default("false"),
INDEXER_FALLBACK_POLL_INTERVAL_MS: fallbackPollIntervalSchema.optional().default(10000),
ALLOWED_ORIGINS: z.string().optional(),
});

Expand All @@ -105,7 +114,8 @@ export interface ValidatedConfig {
indexerPollIntervalMs: number;
reconciliationIntervalMs: number;
archiveCronIntervalMs: number;
webhookDeadLetterPruneIntervalMs: number;
indexerFallbackPollingEnabled: boolean;
indexerFallbackPollIntervalMs: number;
adminApiKey: string | null;
allowedOrigins: string | undefined;
}
Expand Down Expand Up @@ -270,7 +280,8 @@ export function validateEnv(): ValidatedConfig {
indexerPollIntervalMs: env.INDEXER_POLL_INTERVAL_MS,
reconciliationIntervalMs: env.RECONCILIATION_INTERVAL_MS,
archiveCronIntervalMs: env.ARCHIVE_CRON_INTERVAL_MS,
webhookDeadLetterPruneIntervalMs: env.WEBHOOK_DEAD_LETTER_PRUNE_INTERVAL_MS,
indexerFallbackPollingEnabled: env.INDEXER_FALLBACK_POLLING_ENABLED,
indexerFallbackPollIntervalMs: env.INDEXER_FALLBACK_POLL_INTERVAL_MS,
},
"configuration validated",
);
Expand All @@ -292,7 +303,8 @@ export function validateEnv(): ValidatedConfig {
indexerPollIntervalMs: env.INDEXER_POLL_INTERVAL_MS,
reconciliationIntervalMs: env.RECONCILIATION_INTERVAL_MS,
archiveCronIntervalMs: env.ARCHIVE_CRON_INTERVAL_MS,
webhookDeadLetterPruneIntervalMs: env.WEBHOOK_DEAD_LETTER_PRUNE_INTERVAL_MS,
indexerFallbackPollingEnabled: process.env.INDEXER_FALLBACK_POLLING_ENABLED === "true",
indexerFallbackPollIntervalMs: env.INDEXER_FALLBACK_POLL_INTERVAL_MS,
adminApiKey,
allowedOrigins: env.ALLOWED_ORIGINS,
};
Expand Down
4 changes: 4 additions & 0 deletions backend/src/migrations/0003_add_indexer_cursor.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-- Migration down: 0003_add_indexer_cursor
-- Purpose : Remove the indexer checkpoint table.

DROP TABLE IF EXISTS indexer_cursor;
12 changes: 12 additions & 0 deletions backend/src/migrations/0003_add_indexer_cursor.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-- Migration: 0003_add_indexer_cursor
-- Purpose : Add an indexer checkpoint table to persist the last
-- successfully processed ledger sequence. This allows
-- the indexer to resume from the correct position after
-- a restart, preventing event loss and duplicate processing.
-- Safe to run multiple times: uses CREATE TABLE IF NOT EXISTS
-- and a singleton row pattern (id = 1).

CREATE TABLE IF NOT EXISTS indexer_cursor (
id INTEGER PRIMARY KEY CHECK (id = 1),
last_ledger_sequence INTEGER NOT NULL
);
21 changes: 21 additions & 0 deletions backend/src/migrations/0003_add_indexer_cursor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Database from "better-sqlite3";

export function up(db: Database.Database): void {
db.exec(`
CREATE TABLE IF NOT EXISTS indexer_cursor (
id INTEGER PRIMARY KEY CHECK (id = 1),
last_ledger_sequence INTEGER NOT NULL
);
`);
}

export function down(db: Database.Database): void {
db.exec(`DROP TABLE IF EXISTS indexer_cursor;`);
}

if (require.main === module) {
const dbPath = process.env.DB_PATH ?? "backend/data/streams.db";
const db = new Database(dbPath);
up(db);
db.close();
}
Loading