Skip to content

fix: seven bugs from code audit (routing loop, drag overlap, resource leaks)#183

Open
pshenok wants to merge 1 commit into
mainfrom
fix/audit-bugs-batch
Open

fix: seven bugs from code audit (routing loop, drag overlap, resource leaks)#183
pshenok wants to merge 1 commit into
mainfrom
fix/audit-bugs-batch

Conversation

@pshenok

@pshenok pshenok commented Jul 10, 2026

Copy link
Copy Markdown
Owner

A follow-up audit (two parallel agents over routing, drag/drop, campaign, and resource cleanup) surfaced seven distinct bugs. All verified in-browser before fixing.

1. ALB → SQS routing loop + permanent request leak (most severe)

alb → sqs was the only rule in the connection-validity table whose reverse (sqs → alb) is also valid. SQS's forwarding pushes to ALB (Service.js downstreamTypes = ["alb"]), while ALB has no dedicated case and falls into the generic forwarding branch that pushes to any connection with no backpressure check. So an alb ⇄ sqs pair bounces a request ALB→SQS→ALB→SQS… forever — it's never handed to finishRequest/failRequest/removeRequest, leaking it from STATE.requests, growing the queue unbounded, and charging upkeep for zero throughput. Fix: removed the alb → sqs rule; sqs → alb (the real forwarding direction) stays.

2. Campaign burst callbacks leak across Retry / Next

burstPattern schedules up to burstSize setTimeout spawns spaced 20ms apart. The bail-out guard only checked this.active/ended, but Retry and Next call startCampaignLevel → loadLevel directly (which resets ended = false) without exit(). Stale callbacks from the previous attempt then passed the guard and injected phantom requests into the freshly-loaded level, polluting its scoring before the player even pressed Play. Fix: monotonic session id captured at schedule time; a callback bails if the session changed.

3. Drag-drop onto an occupied tile makes a service unremovable

The drag path never applied the overlap guard createService uses. Dropping node B onto node A's tile left them sharing a position; the raycaster only ever returns the front mesh, so the other service became permanently unselectable/undeletable/un-upgradeable while still charging upkeep. Fix: reject the drop (revert to the drag-start tile) when the target tile is occupied.

4. clearAllServices leaked connection GPU resources

Every other connection-removal path disposes geometry/material; "Clear All" only removed the mesh from the scene. Fix: dispose them too.

5. Held keys never cleared on window blur

A keyup missed during an alt-tab left the key stuck in keysPressed, panning the camera forever. Fix: window blur handler clears all held keys.

6. Dragging the Internet node never moved its ring

if (draggedNode.mesh) — but internetNode.mesh is always set, so the else branch (the only one that also moves the ring) was dead code; the cyan ring stayed frozen at spawn. Fix: branch on node identity.

7. _persistWin could poison best time with NaN

A completed[] entry missing bestTimeSec (hand-edited/future schema) made Math.min(undefined, elapsed) = NaN, permanently displaying "NaNs". Fix: Number.isFinite guard.

Verification

In-browser: alb → sqs now rejected while sqs → alb still connects; clearAllServices runs without error; campaign _session increments on each level load (stale callbacks invalidated); _persistWin returns a finite best time (42.5) and correct max stars from a malformed entry. No new console errors. No build step — served raw as before.

Findings from a two-agent audit of routing, drag/drop, campaign, and
resource cleanup:

1. ALB<->SQS routing loop + request leak. 'alb -> sqs' was the only
   bidirectional-with-its-reverse rule in the connection table. SQS's
   forwarding pushes to ALB (downstreamTypes=['alb']) while ALB's generic
   branch forwards to any connection with no backpressure, so an
   alb->sqs->alb pair bounced requests forever — never removed from
   STATE.requests, unbounded queue growth, silent money sink. Removed the
   alb->sqs rule; sqs->alb (the real path) stays.

2. Campaign burst callbacks leaked across retry/next. burstPattern schedules
   setTimeout spawns; the guard only checked this.active/ended, but
   Retry/Next go straight to loadLevel() (which resets ended=false) without
   exit(), so stale callbacks fired into the new attempt. Added a monotonic
   session id captured at schedule time.

3. Dropping a dragged node onto an occupied tile overlapped two services,
   making one permanently unselectable (raycast hits only the front mesh)
   while it kept charging upkeep. Reject the drop (revert to start tile) when
   the target tile is occupied — same guard createService already uses.

4. clearAllServices leaked connection geometry/material — every other
   removal path disposes them. Now disposes too.

5. Held keys weren't cleared on window blur, so a keyup missed during an
   alt-tab left the camera panning forever. Added a blur handler.

6. Dragging the Internet node never moved its ring (dead else branch:
   internetNode.mesh is always truthy). Branch on node identity instead.

7. _persistWin could poison best time with NaN if a completed[] entry lacked
   bestTimeSec. Guard with Number.isFinite.

Verified in-browser: alb->sqs now rejected while sqs->alb works,
clearAllServices runs clean, campaign session id increments per level load,
_persistWin returns a finite best time from a malformed entry.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant