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
43 changes: 37 additions & 6 deletions app/api/interview/[id]/chaos-timeout/route.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,51 @@
import { NextRequest, NextResponse } from 'next/server';
import dbConnect from '@/src/lib/db/mongoose';
import InterviewSession, { IConstraintChange } from '@/src/lib/db/models/InterviewSession';
import User from '@/src/lib/db/models/User';
import { getAuthenticatedUser } from '@/src/lib/firebase/firebaseAdmin';

export async function POST(
req: NextRequest,
context: { params: Promise<{ id: string }> }
) {
try {
await dbConnect();
const { id } = await context.params;
const { type, constraintId } = await req.json();
const body = await req.json();

// Validate request body
const { type, constraintId } = body;
if (!type || (type !== 'warning' && type !== 'penalty')) {
return new Response(JSON.stringify({ error: 'Invalid type. Must be "warning" or "penalty".' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
if (!constraintId || (typeof constraintId !== 'string' && typeof constraintId !== 'number') || constraintId === '') {
return new Response(JSON.stringify({ error: 'Invalid constraintId. Must be a non-empty string or number.' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}

// Authenticate user
const authHeader = req.headers.get('Authorization');
const authenticatedUser = await getAuthenticatedUser(authHeader);

if (!authenticatedUser) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

await dbConnect();

const user = await User.findOne({ firebaseUid: authenticatedUser.uid });
if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}

// 1. Fetch Session
const session = await InterviewSession.findOne({ id });
// 1. Fetch Session with ownership check
const session = await InterviewSession.findOne({ id, userId: user._id });
if (!session) {
return NextResponse.json({ error: 'Session not found' }, { status: 404 });
return NextResponse.json({ error: 'Session not found or access denied' }, { status: 403 });
}
if (session.status !== 'in_progress') {
return NextResponse.json({ success: true, warning: 'Session is no longer in progress' });
Expand Down Expand Up @@ -66,4 +97,4 @@ export async function POST(
console.error('Chaos Timeout Error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
}
9 changes: 8 additions & 1 deletion components/canvas/DesignCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,13 @@ export function DesignCanvas({
const [targetRps, setTargetRps] = useState(10000);
const simulationMetrics = useSimulationEngine(nodes, connections, targetRps, isSimulationRunning);

// Stop simulation when canvas becomes read-only
useEffect(() => {
if (readOnly && isSimulationRunning) {
setIsSimulationRunning(false);
}
}, [readOnly, isSimulationRunning]);

// Selection state
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
const [selectedConnectionId, setSelectedConnectionId] = useState<string | null>(null);
Expand Down Expand Up @@ -1190,4 +1197,4 @@ export function DesignCanvas({
</div>
</main>
);
}
}
8 changes: 5 additions & 3 deletions components/interview/ChaosTimer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,10 @@ export function ChaosTimer({ constraint, onTimeout }: ChaosTimerProps) {
const hasPendingPenaltyRequest = useRef(false);

useEffect(() => {
// Reset timers on DB fields updates have been removed (unnecessary toggles)

// Reset pending request flags for each new active constraint
hasPendingWarningRequest.current = false;
hasPendingPenaltyRequest.current = false;

const interval = setInterval(() => {
const current = calculateRemaining();
setState(current);
Expand Down Expand Up @@ -93,4 +95,4 @@ export function ChaosTimer({ constraint, onTimeout }: ChaosTimerProps) {
</div>
</div>
);
}
}
71 changes: 48 additions & 23 deletions src/lib/simulation/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,41 +55,50 @@ export function runSimulation(nodes: ICanvasNode[], edges: IConnection[], target
if (inDegree[e.to] !== undefined) inDegree[e.to]++;
});

// We will do a modified iterative propagation to handle cycles softly.
// Instead of strict topological sort, we do a fixed number of iterations (e.g. 5 passes)
// For pure DAGs, it propagates cleanly. For cycles, it stabilizes.

// Iterative propagation with convergence detection to handle cycles properly
// We iterate until traffic distribution stabilizes or we hit maxIterations

// Initial Load
sources.forEach(s => {
if (nodeMetrics[s.id]) {
nodeMetrics[s.id].trafficIn += targetRps / sources.length;
}
});

// 10 passes is enough for most UI architectures
for (let pass = 0; pass < 10; pass++) {
// Reset edge flows before recalculating this pass's spread
const maxIterations = 100;
const epsilon = 0.01; // Convergence threshold: stop when total change < epsilon
const sourceIds = new Set(sources.map(s => s.id));

for (let iteration = 0; iteration < maxIterations; iteration++) {
// Store previous trafficIn values to measure convergence
const prevTrafficIn: Record<string, number> = {};
Object.keys(nodeMetrics).forEach(id => {
prevTrafficIn[id] = nodeMetrics[id].trafficIn;
});

// Reset edge flows before recalculating this iteration's spread
edges.forEach(e => { edgeMetrics[e.id].trafficFlow = 0; });

// We need a snapshot of trafficIn for the current frame to distribute it properly
const sourceIds = new Set(sources.map(s => s.id));
const currentTrafficIn = Object.keys(nodeMetrics).reduce((acc, id) => {
acc[id] = nodeMetrics[id].trafficIn;
// Clear trafficIn for non-sources so they can receive the fresh wave
if (!sourceIds.has(id)) {
nodeMetrics[id].trafficIn = 0;

// Create next iteration's trafficIn map, starting with sources
const nextTrafficIn: Record<string, number> = {};
sourceIds.forEach(id => {
nextTrafficIn[id] = targetRps / sources.length;
});
nodes.forEach(n => {
if (!sourceIds.has(n.id)) {
nextTrafficIn[n.id] = 0;
}
return acc;
}, {} as Record<string, number>);
});

// Process each node: compute outFlow and distribute to children
nodes.forEach(node => {
const metrics = nodeMetrics[node.id];
const processingTraffic = currentTrafficIn[node.id];
const processingTraffic = prevTrafficIn[node.id];

// Cap out at capacity
const outFlow = Math.min(processingTraffic, metrics.capacity);
metrics.trafficOut = outFlow;

// Update status
if (processingTraffic > metrics.capacity) {
metrics.status = 'bottlenecked';
Expand All @@ -105,12 +114,28 @@ export function runSimulation(nodes: ICanvasNode[], edges: IConnection[], target
const flowPerEdge = outFlow / outgoingEdges.length;
outgoingEdges.forEach(edge => {
edgeMetrics[edge.id].trafficFlow += flowPerEdge;
if (nodeMetrics[edge.to]) {
nodeMetrics[edge.to].trafficIn += flowPerEdge;
if (nextTrafficIn[edge.to] !== undefined) {
nextTrafficIn[edge.to] += flowPerEdge;
}
});
}
});

// Update nodeMetrics with nextTrafficIn
Object.keys(nextTrafficIn).forEach(id => {
nodeMetrics[id].trafficIn = nextTrafficIn[id];
});

// Measure total delta to check for convergence
let totalDelta = 0;
Object.keys(nodeMetrics).forEach(id => {
totalDelta += Math.abs(nodeMetrics[id].trafficIn - prevTrafficIn[id]);
});

// Stop if converged
if (totalDelta < epsilon) {
break;
}
}

// Evaluate Global Status
Expand All @@ -120,4 +145,4 @@ export function runSimulation(nodes: ICanvasNode[], edges: IConnection[], target
else if (bottleneckCount > 0) globalStatus = 'degraded';

return { nodeMetrics, edgeMetrics, globalStatus };
}
}