fix: do not expire Infinity gcTime via setTimeout overflow - #25
Conversation
Host setTimeout coerces Infinity and delays above 2^31-1 ms to 1ms.
createRouter({ gcTime: Infinity }) and a 30-day gcTime therefore
scheduled a 1ms GC timer (TimeoutOverflowWarning) and rescheduled
it every millisecond.
Skip non-finite gcTime so those matches stay cached. Clamp finite
overflows to 2^31-1 ms so the existing reschedule path can expire
them later.
Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
|
🦞👀 Pull request received. I will update this pull request when review starts. ClawSweeper review completeClawSweeper finished reviewing this revision. The review result is being finalized. |
|
Codex review: blocked before merge. Reviewed September 4, 2026, 5:54 PM ET / 21:54 UTC. ClawSweeper reviewWhat this changesThis PR prevents route-cache garbage collection from passing infinite or overly large retention periods to host timers, with documentation and regression tests. Regression provenancePossible regression — suspected (reviewed change). No predecessor PR is attributed. Merge readiness⛔ Blocked before merge - 3 items remain Keep this PR open: it fixes the timer-overflow problem with convincing real-runtime evidence, but its new non-finite guard also turns Priority: P1 Review scores
Verification
How this fits togetherThe UI router receives application navigation and preload requests and maintains active and cached route matches. When a match becomes unused, its configured retention period drives garbage collection that either retains or removes the cached match. flowchart LR
A[Navigation or preload] --> B[Cached route match]
B --> C[Garbage collection scheduler]
C --> D{Retention duration}
D --> E[Host timer]
D --> F[Retain or remove cache entry]
E --> F
Before merge
Findings
Agent review detailsSecurityNone. Review metrics
Merge-risk optionsMaintainer options:
Copy recommended automerge instructionTechnical reviewBest possible solution: Special-case only Do we have a high-confidence way to reproduce the issue? Yes. Current-main source sends configured retention values directly to Is this the best way to solve the issue? No. Clamping large finite values is appropriate, but treating every non-finite number as positive Infinity changes established expiry behavior; only positive Infinity should bypass scheduling. Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against ea06377b0e80. LabelsLabel justifications:
EvidenceAcceptance criteria:
What I checked:
Likely related people:
Rank-up movesOptional improvements that raise the rating; they are not merge blockers.
Rating scale
Overall follows the weaker of proof and patch quality. Workflow
HistoryReview history (12 earlier review cycles; latest 8 shown)
|
What Problem This Solves
Fixes an issue where consumers that call
createRouter({ gcTime: Infinity }), setpreloadGcTime: Infinity, or pass a retention longer than the host timer limit (for example30 * 24 * 60 * 60 * 1000) would schedule a 1ms garbage-collection timer. Node and browsers coercesetTimeoutdelays outside the signed 32-bit range to 1ms, so the unused match is re-checked every millisecond and Node printsTimeoutOverflowWarning.staleTime: Infinityis not affected because it is a comparison, not a timer.Why This Change Was Made
scheduleGcnow skips a timer whengcTimeorpreloadGcTimeis not finite, soInfinitykeeps the cached match until it is replaced orstop()runs. Finite values above2^31-1ms are clamped to that limit. The existing callback still reschedules when the match is younger thangcTime, so a 30-day retention can expire later instead of spinning 1ms timers. No public exports, types, history adapter shape, or loader signature changed.User Impact
gcTime: InfinityandpreloadGcTime: Infinitykeep unused cached matches without overflowing the host timer. A 30-day (or other overflow) retention no longer emitsTimeoutOverflowWarningor wake the event loop every millisecond. Default 30-minute GC is unchanged. Apps that never passInfinityor a delay above about 24.8 days behave as before.Evidence
Live
nodeagainst the builtdist/index.json this branch, macOS 26.6.2, Node v26.7.0. The same public sequence (createRouter({ gcTime }),navigate("chat"),navigate("fast"), then 20ms) was run against the unpatched bundle fromupstream/mainand the patched bundle.Unpatched
dist/index.js(gcTime: Infinity, 16 overflow warnings in 20ms):Unpatched
dist/index.js(gcTime: 30 * 24 * 60 * 60 * 1000):Patched
dist/index.js(gcTime: Infinity, no overflow warning):Patched
dist/index.js(gcTime: 2592000000, no overflow warning):pnpm run checkpassed locally (format, typecheck, lint, 23 tests, pack/import).This timer path has been in
scheduleGcsincef047b64a87a5(2026-06-20,refactor: finalize router match loading). Same class of host-timer overflow: TanStack Query #6287 (TimeoutOverflowWarninginQuery.scheduleGc). Adjacent but different: #20 (stale navigation cancellation), #21 (loader redirect hop cap), #24 (history listener context).Real behavior proof
Behavior or issue addressed:
createRouter({ gcTime: Infinity })and a 30-daygcTimeno longer overflowsetTimeoutinto a 1ms GC timer (TimeoutOverflowWarning). Unused cached matches stay cached without a 1ms reschedule loop.Real environment tested: macOS 26.6.2 arm64, Node v26.7.0,
@openclaw/uirouterbuilt fromfix/f003-gctime-overflowat/tmp/oc-pr-uirouter-F003.Exact steps or command run after this patch:
cd /tmp/oc-pr-uirouter-F003 node /tmp/proof-uirouter-f003.mjs /tmp/oc-pr-uirouter-F003/dist/index.js Infinity node /tmp/proof-uirouter-f003.mjs /tmp/oc-pr-uirouter-F003/dist/index.js 30dEvidence after fix: terminal output from the patched
dist/index.js:Observed result after fix: After
createRouter({ gcTime: Infinity })(and the 30-day value) plusnavigate("chat")thennavigate("fast"), Node emitted 0TimeoutOverflowWarningevents andcachedMatchesstill heldchatafter 20ms. The same commands against the unpatched bundle emitted 16 and 18 overflow warnings (Timeout duration was set to 1).What was not tested: A browser
setTimeoutin a running OpenClaw UI shell, and a per-routegcTime: Infinityoverride (samescheduleGchelper).