-
Notifications
You must be signed in to change notification settings - Fork 0
Extract core runtime utilities for script reuse #300
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5fa5ecb
632460c
f333421
5ff1182
ae6f368
c3362b8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| export interface ContinuousLoopOptions { | ||
| /** Interval between cycles in seconds */ | ||
| readonly intervalSeconds: number; | ||
| /** | ||
| * Callback to run on each cycle. | ||
| * | ||
| * @param isShutdownRequested - Function to check if shutdown has been requested during the cycle | ||
| * @returns Promise that resolves when the cycle is complete (return value is ignored) | ||
| */ | ||
| readonly runCycle: (isShutdownRequested: () => boolean) => Promise<unknown>; | ||
| /** Optional callback for cleanup on shutdown */ | ||
| readonly onShutdown?: () => Promise<void>; | ||
| /** Optional callback for per-cycle errors */ | ||
| readonly onCycleError?: (error: unknown) => void; | ||
| } | ||
|
|
||
| /** | ||
| * Run a function in a continuous loop with graceful shutdown support. | ||
| */ | ||
| export async function runContinuousLoop(options: ContinuousLoopOptions): Promise<void> { | ||
| const { intervalSeconds, runCycle, onShutdown, onCycleError } = options; | ||
|
|
||
| let shutdownRequested = false; | ||
| let sleepResolve: (() => void) | null = null; | ||
| let sleepTimeout: NodeJS.Timeout | null = null; | ||
|
|
||
| const handleShutdown = (_signal: string): void => { | ||
| shutdownRequested = true; | ||
| if (sleepTimeout) { | ||
| clearTimeout(sleepTimeout); | ||
| sleepTimeout = null; | ||
| } | ||
| if (sleepResolve) { | ||
| sleepResolve(); | ||
| sleepResolve = null; | ||
| } | ||
| }; | ||
|
|
||
| const sigintHandler = (): void => handleShutdown('SIGINT'); | ||
| const sigtermHandler = (): void => handleShutdown('SIGTERM'); | ||
|
|
||
| process.on('SIGINT', sigintHandler); | ||
| process.on('SIGTERM', sigtermHandler); | ||
|
|
||
| const isShutdownRequested = (): boolean => shutdownRequested; | ||
|
|
||
| try { | ||
| for (;;) { | ||
| if (isShutdownRequested()) { | ||
| break; | ||
| } | ||
| try { | ||
| await runCycle(isShutdownRequested); | ||
| } catch (error) { | ||
| try { | ||
| onCycleError?.(error); | ||
| } catch { | ||
| // Prevent a failing error callback from terminating the loop | ||
| } | ||
| } | ||
|
|
||
| if (isShutdownRequested()) { | ||
| break; | ||
| } | ||
| await new Promise<void>((resolve) => { | ||
| sleepResolve = resolve; | ||
| sleepTimeout = setTimeout(() => { | ||
| sleepTimeout = null; | ||
| sleepResolve = null; | ||
| resolve(); | ||
| }, intervalSeconds * 1000); | ||
| }); | ||
| } | ||
| } finally { | ||
| process.off('SIGINT', sigintHandler); | ||
| process.off('SIGTERM', sigtermHandler); | ||
| if (typeof onShutdown === 'function') { | ||
| await onShutdown(); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,4 @@ | ||
| export { waitForCondition, type WaitForConditionOptions } from './polling'; | ||
| export { extractString, hasStringProperty, isNonEmptyString, isNumber, isRecord, isString } from './type-guards'; | ||
| export { runContinuousLoop, type ContinuousLoopOptions } from './continuous-loop'; | ||
| export { SyncStateTracker, type SyncState, type SyncStateTrackerOptions } from './sync-state-tracker'; |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,154 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import * as fs from 'fs'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import * as os from 'os'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import * as path from 'path'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1
to
+3
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Run the full test command sequence for this change.
As per coding guidelines "Run npm run lint && npm run build && npm test after any code change". 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export interface SyncState { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** The last offset we successfully processed */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| readonly lastSuccessfulOffset: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** The ledger end at the time of our last successful sync */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| readonly ledgerEndAtSync: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** ISO timestamp of last update */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| readonly lastUpdated: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** Additional metadata for debugging */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| readonly metadata?: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| readonly hostname?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| readonly processId?: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export interface SyncStateTrackerOptions { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** Network name (mainnet, devnet) */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| readonly network: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** Provider name (intellect, 5n) */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| readonly provider: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** Optional: Custom directory for state files (defaults to ~/.canton-sync-state) */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| readonly stateDirectory?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function getDefaultStateDirectory(): string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const envDir = process.env['CANTON_SYNC_STATE_DIR']; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (envDir) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return envDir; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const homeDir = os.homedir(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return path.join(homeDir, '.canton-sync-state'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Local file-based tracking of ledger sync progress. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export class SyncStateTracker { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private readonly stateFilePath: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private readonly directoryWritable: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private cachedState: SyncState | null = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| constructor(options: SyncStateTrackerOptions) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const stateDir = options.stateDirectory ?? getDefaultStateDirectory(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let isWritable = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!fs.existsSync(stateDir)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fs.mkdirSync(stateDir, { recursive: true }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isWritable = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.directoryWritable = isWritable; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.stateFilePath = path.join(stateDir, `${options.network}-${options.provider}.json`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, let's examine the sync-state-tracker.ts file
cat -n src/core/utils/sync-state-tracker.tsRepository: Fairmint/canton-node-sdk Length of output: 5026 🏁 Script executed: # Search for type definitions of SyncStateTrackerOptions
rg -nP 'interface\s+SyncStateTrackerOptions|type\s+SyncStateTrackerOptions' --type=ts -A 10Repository: Fairmint/canton-node-sdk Length of output: 1107 🏁 Script executed: # Search for SyncStateTracker instantiations
rg -nP 'new\s+SyncStateTracker|SyncStateTracker\(' --type=ts -B 3 -A 3Repository: Fairmint/canton-node-sdk Length of output: 50 🏁 Script executed: # Check for any sanitization or validation functions already in the codebase
rg -nP 'sanitize|validate.*path|pathTraversal' --type=ts -iRepository: Fairmint/canton-node-sdk Length of output: 2372 🏁 Script executed: # Search for SyncStateTracker references with broader pattern
rg -nP 'SyncStateTracker' --type=ts -B 2 -A 2Repository: Fairmint/canton-node-sdk Length of output: 1581 🏁 Script executed: # Look in test files to see how it's constructed
find . -name '*.test.ts' -o -name '*.spec.ts' | xargs grep -l 'SyncStateTracker' 2>/dev/null | head -5Repository: Fairmint/canton-node-sdk Length of output: 50 🏁 Script executed: # Check if there are any tests for sync-state-tracker
fd -e 'test.ts' -e 'spec.ts' sync-state-trackerRepository: Fairmint/canton-node-sdk Length of output: 50 🏁 Script executed: # Search for instantiation patterns more broadly
rg -nP 'new\s+SyncStateTracker|SyncStateTracker\s*\(' --type=tsRepository: Fairmint/canton-node-sdk Length of output: 50 🏁 Script executed: # Find all test files in the repository
fd -e 'test.ts' -e 'spec.ts' | head -20Repository: Fairmint/canton-node-sdk Length of output: 1108 🏁 Script executed: # Check if sync-state-tracker has any tests at all
find . -path '*/test*' -o -path '*/spec*' | xargs grep -l 'SyncStateTracker' 2>/dev/nullRepository: Fairmint/canton-node-sdk Length of output: 52 🏁 Script executed: # Search in the entire codebase for where SyncStateTracker is instantiated
rg -nP 'SyncStateTracker\s*\{|new\s+SyncStateTracker' --type=ts -A 5Repository: Fairmint/canton-node-sdk Length of output: 527 🏁 Script executed: # Let's verify what path.join does with path traversal attempts
node <<'EOF'
const path = require('path');
// Test cases
const stateDir = '/home/user/.canton-sync-state';
// Case 1: Normal input
const normal = path.join(stateDir, 'mainnet-intellect.json');
console.log('Normal:', normal);
// Case 2: Traversal attempt
const traversal = path.join(stateDir, '../../etc-passwd.json');
console.log('Traversal attempt:', traversal);
// Case 3: Another traversal
const traversal2 = path.join(stateDir, '../../../sensitive.json');
console.log('Traversal attempt 2:', traversal2);
// Case 4: What if network contains traversal
const network = '../../etc';
const provider = 'passwd';
const result = path.join(stateDir, `${network}-${provider}.json`);
console.log('Network with traversal:', result);
EOFRepository: Fairmint/canton-node-sdk Length of output: 252 🏁 Script executed: # Check if there are any validation or sanitization of network/provider elsewhere
rg -nP 'network|provider' --type=ts -B 2 -A 2 src/core/ | head -100Repository: Fairmint/canton-node-sdk Length of output: 5798 🏁 Script executed: # Look for how these values might be passed from external sources
rg -nP 'SyncStateTrackerOptions|network.*provider' --type=ts -B 5 -A 5 | head -80Repository: Fairmint/canton-node-sdk Length of output: 5780 Sanitize The 🛡️ Proposed fix to sanitize inputs constructor(options: SyncStateTrackerOptions) {
const stateDir = options.stateDirectory ?? getDefaultStateDirectory();
+
+ // Sanitize inputs to prevent path traversal
+ const sanitize = (s: string): string => s.replace(/[^a-zA-Z0-9_-]/g, '_');
+ const safeNetwork = sanitize(options.network);
+ const safeProvider = sanitize(options.provider);
let isWritable = true;
if (!fs.existsSync(stateDir)) {
try {
fs.mkdirSync(stateDir, { recursive: true });
} catch {
isWritable = false;
}
}
this.directoryWritable = isWritable;
- this.stateFilePath = path.join(stateDir, `${options.network}-${options.provider}.json`);
+ this.stateFilePath = path.join(stateDir, `${safeNetwork}-${safeProvider}.json`);
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isWritable(): boolean { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return this.directoryWritable; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+46
to
+64
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# First, let's view the actual file to see the current state
head -100 src/core/utils/sync-state-tracker.tsRepository: Fairmint/canton-node-sdk Length of output: 2644 🏁 Script executed: #!/bin/bash
# Find all usages of isWritable() to understand downstream reliance
rg -nP '\.isWritable\s*\(' -C3 --type=tsRepository: Fairmint/canton-node-sdk Length of output: 50 🏁 Script executed: #!/bin/bash
# Try different patterns to find isWritable() usages
rg 'isWritable' --type=ts -nRepository: Fairmint/canton-node-sdk Length of output: 349 🏁 Script executed: #!/bin/bash
# View the rest of the file to see full updateState method and any other relevant code
tail -50 src/core/utils/sync-state-tracker.tsRepository: Fairmint/canton-node-sdk Length of output: 1301 Add explicit write permission check for existing directories to prevent silent persistence failures. If the directory exists but lacks write permissions, Proposed fix constructor(options: SyncStateTrackerOptions) {
const stateDir = options.stateDirectory ?? getDefaultStateDirectory();
let isWritable = true;
if (!fs.existsSync(stateDir)) {
try {
fs.mkdirSync(stateDir, { recursive: true });
} catch {
isWritable = false;
}
}
+
+ if (isWritable) {
+ try {
+ fs.accessSync(stateDir, fs.constants.W_OK);
+ } catch {
+ isWritable = false;
+ }
+ }
this.directoryWritable = isWritable;
this.stateFilePath = path.join(stateDir, `${options.network}-${options.provider}.json`);
}🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| getState(): SyncState | null { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (this.cachedState) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return this.cachedState; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!this.directoryWritable) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!fs.existsSync(this.stateFilePath)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const content = fs.readFileSync(this.stateFilePath, 'utf-8'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const state = JSON.parse(content) as SyncState; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| typeof state.lastSuccessfulOffset !== 'number' || | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| typeof state.ledgerEndAtSync !== 'number' || | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| typeof state.lastUpdated !== 'string' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+83
to
+89
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Consider validating non-negative integers. The validation checks types but doesn't verify that 🛡️ Optional enhancement if (
typeof state.lastSuccessfulOffset !== 'number' ||
typeof state.ledgerEndAtSync !== 'number' ||
- typeof state.lastUpdated !== 'string'
+ typeof state.lastUpdated !== 'string' ||
+ !Number.isInteger(state.lastSuccessfulOffset) ||
+ !Number.isInteger(state.ledgerEndAtSync) ||
+ state.lastSuccessfulOffset < 0 ||
+ state.ledgerEndAtSync < 0
) {
return null;
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.cachedState = state; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return state; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| updateState(offset: number, ledgerEnd: number): void { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!this.directoryWritable) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const state: SyncState = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lastSuccessfulOffset: offset, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ledgerEndAtSync: ledgerEnd, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lastUpdated: new Date().toISOString(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| metadata: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hostname: process.env['HOSTNAME'] ?? 'unknown', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| processId: process.pid, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.cachedState = state; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const tempPath = `${this.stateFilePath}.tmp`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fs.writeFileSync(tempPath, JSON.stringify(state, null, 2)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fs.renameSync(tempPath, this.stateFilePath); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Ignore local persistence errors and continue with in-memory progress. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
cursor[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| getStartingOffset(currentLedgerEnd: number): { offset: number; wasReset: boolean } | null { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const state = this.getState(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!state) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (state.lastSuccessfulOffset > currentLedgerEnd) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.resetState(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { offset: 0, wasReset: true }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
cursor[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const safeOffset = Math.max(0, state.lastSuccessfulOffset - 1); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { offset: safeOffset, wasReset: false }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resetState(): void { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.cachedState = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (fs.existsSync(this.stateFilePath)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fs.unlinkSync(this.stateFilePath); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Best-effort deletion; in-memory cache is already cleared. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
cursor[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| getStateFilePath(): string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return this.stateFilePath; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: Fairmint/canton-node-sdk
Length of output: 103
🏁 Script executed:
Repository: Fairmint/canton-node-sdk
Length of output: 1261
🏁 Script executed:
Repository: Fairmint/canton-node-sdk
Length of output: 2852
🏁 Script executed:
Repository: Fairmint/canton-node-sdk
Length of output: 562
🏁 Script executed:
Repository: Fairmint/canton-node-sdk
Length of output: 302
🏁 Script executed:
Repository: Fairmint/canton-node-sdk
Length of output: 1148
🏁 Script executed:
Repository: Fairmint/canton-node-sdk
Length of output: 50
🏁 Script executed:
Repository: Fairmint/canton-node-sdk
Length of output: 3792
🏁 Script executed:
Repository: Fairmint/canton-node-sdk
Length of output: 379
🏁 Script executed:
Repository: Fairmint/canton-node-sdk
Length of output: 2649
🏁 Script executed:
Repository: Fairmint/canton-node-sdk
Length of output: 50
🏁 Script executed:
Repository: Fairmint/canton-node-sdk
Length of output: 50
Add validation for
intervalSecondsto prevent hot loops from invalid values.The
intervalSecondsparameter lacks type-level constraints and is passed directly tosetTimeoutat line 67. Invalid inputs (≤0, NaN, or non-finite) would causesetTimeoutto resolve immediately, creating a CPU-pegging hot loop. Add a guard at function entry:Proposed fix
export async function runContinuousLoop(options: ContinuousLoopOptions): Promise<void> { const { intervalSeconds, runCycle, onShutdown, onCycleError } = options; + if (!Number.isFinite(intervalSeconds) || intervalSeconds <= 0) { + throw new Error('intervalSeconds must be a positive, finite number'); + }📝 Committable suggestion
🤖 Prompt for AI Agents