fix(spa): survive boot on browsers without scheduler.yield - #177
Merged
Conversation
The cooperative widget-boot loop yields between widgets through a yieldToMain helper whose MessageChannel fallback stored its lazy state in two module-scope `let` bindings in a later-concatenated script. The boot loop is defined in an earlier script of the same shared IIFE and calls yieldToMain during its first synchronous pass, before those bindings' declarations run. On every engine without scheduler.yield (all Safari and iOS Safari, Chrome and Edge below 129, Firefox below 142) the first yield read a binding inside its temporal dead zone, the un-awaited boot loop rejected, and only the first widget rendered — the header sat above a blank page. Desktop Chrome 129+ returns on the scheduler.yield path before the fallback, which hid the defect. Move the fallback state onto the yieldToMain function object itself. A function declaration is hoisted to the top of the shared IIFE, so it is reachable from the first statement under any concatenation order, and its properties are created at call time — no dead-zone read is possible. Add a headless regression test that boots the dashboard with `scheduler` removed before any page script runs and asserts all widget bodies render; it fails on the unfixed tree (1 of 23) and passes with the fix. Also document the load-order constraint on BIVARIATE_PALETTE, the sole remaining IIFE-scope binding in a later-concatenated file, whose readers stay off the synchronous boot path.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Critical, live on the published demo: on every browser without
scheduler.yield— all Safari and iOS Safari, Chrome/Edge <129, Firefox <142 — the SPA boot died on an unhandled promise rejection and 22 of 23 widgets never rendered (header above ~14,000px of blank white). The boot loop's first synchronous pass callsyieldToMain()before the later-concatenated helper file's module-scopeletbindings exist; on Chrome ≥129 thescheduler.yieldearly-return jumps over the temporal dead zone, which is exactly why the maintainers' own browsers never showed it.Fix is structural, per the repo's own SPA rule (no module-scope state in later files): the lazy MessageChannel state now lives on the hoisted function object itself — reachable from the IIFE's first statement under any concatenation order, no TDZ possible, no cross-file moves. The same-class audit of every post-boot file found zero other mutable IIFE-scope bindings; the one safe-but-latent
constis documented with a load-order constraint comment.The regression test ships with the fix (the durable guard): a headless variant injects a pre-evaluation script forcing
window.schedulerundefined, boots the full-widget dashboard, and asserts a vacuity guard (scheduler truly absent), all 23 widget bodies non-empty, and zero uncaught rejections. Proven bidirectionally: with only the JS fix stashed it fails with exactly the report's signature (1 of 23 widget bodies rendered); with the fix, 23/23.Verification