PageSourceSearch

https://basketball.nb.ca/js/circuitStaleness.js

js basketball.nb.ca collected 2026-09-24 13:30:18 UTC 6,040 bytes, 107 lines download raw bytes

1// Keeps a tab that has been away from coming back to a page that looks fine and is dead.
2//
3// Blazor Server keeps the page's state on the server, in a circuit. Once the server drops one,
4// Blazor CANNOT tell you: a reconnect after the circuit is gone reports success exactly like a
5// real rejoin, the reconnect notice disappears, and the page sits there looking healthy while
6// nothing on it reaches the server and every action fails. Verified against a build with a short
7// retention: reconnecting after the circuit had gone reported a successful reconnect both times,
8// and a button that had worked moments earlier did nothing afterwards.
9//
10// That is what a tab left open overnight hits. A hidden tab also has its timers throttled to about
11// once a minute, so it can fail to notice the connection died at all until someone looks at it -
12// which means the reconnect signal is not reliable here either. How long the tab was away is
13// independent of all of it, so that is what this measures.
14//
15// Only the away case is handled. A tab that stays visible is not throttled, keeps pinging, and
16// keeps its circuit; if it loses one anyway the outage is real, Blazor's own reconnect UI is on
17// screen the whole time, and the user can see it.
18//
19// CIRCUIT_LIFETIME_MS must stay under DisconnectedCircuitRetentionPeriod in Program.cs. The two
20// move together - raising retention without raising this just makes the reload happen earlier than
21// it needs to; lowering retention below this brings the dead-page bug back.
22(function () {
23    'use strict';
24
25    const CIRCUIT_LIFETIME_MS = 13 * 60 * 1000;   // Program.cs holds circuits for 15 minutes
26
27    let hiddenSince = null;
28
29    // Date.now() is wall clock, so this stays correct across a suspended or sleeping machine.
30    function reloadIfAwayTooLong() {
31        const awayFor = hiddenSince === null ? 0 : Date.now() - hiddenSince;
32        hiddenSince = null;
33        if (awayFor > CIRCUIT_LIFETIME_MS) {
34            window.location.reload();
35        }
36    }
37
38    document.addEventListener('visibilitychange', function () {
39        if (document.visibilityState === 'hidden') {
40            hiddenSince = Date.now();
41            return;
42        }
43        reloadIfAwayTooLong();
44    });
45
46    // The back/forward cache freezes the page rather than hiding it: no timer of ours runs from
47    // 'pagehide' until 'pageshow', so that is the one chance to record when the page actually went,
48    // and a restore does not reliably arrive through visibilitychange. Same question, asked where
49    // it is certain to be asked.
50    window.addEventListener('pagehide', function (event) {
51        if (event.persisted && hiddenSince === null) hiddenSince = Date.now();
52    });
53
54    window.addEventListener('pageshow', function (event) {
55        if (event.persisted) reloadIfAwayTooLong();
56    });
57
58    // -- Two things Blazor does on its own, both of which kill a restored page --------------
59    //
60    //   1. Blazor registers a 'pagehide' listener that beacons POST _blazor/disconnect, and the
61    //      server TERMINATES the circuit on that beacon - terminated, not retained, so the
62    //      retention period never applies to it. pagehide also fires when the browser freezes the
63    //      page into the back/forward cache, which is not the page going away: it can come back,
64    //      DOM intact, holding a circuit id that was destroyed on the way out.
65    //
66    //   2. .NET 10 gave the reconnect loop a second chance: when rejoining fails Blazor calls
67    //      Blazor.resumeCircuit(), which asks the server to build a NEW circuit out of state the
68    //      browser was handed when the old one was PAUSED. Nothing here pauses a circuit, so there
69    //      is no such state and the resume comes back empty - and it reports success, so the
70    //      reconnect notice hides over a page whose DOM belongs to a circuit that no longer
71    //      exists. Every click then throws 'DispatchEventAsync' on the server and the page only
72    //      scrolls. On .NET 9 the same failed rejoin reloaded the page.
73    //
74    // So: beacon only on a real unload, and refuse the resume, which puts a failed rejoin back on
75    // Blazor's own 'rejected' path - a reload.
76    //
77    // Blazor sets both of these up when the circuit starts, which is after this file runs, so wait
78    // for it rather than assume. It exposes the listener it registered as Blazor.disconnect, so
79    // this replaces that listener rather than racing it - a second listener could not stop the
80    // first from firing.
81    //
82    // No deadline on the wait. A statically rendered page starts no circuit at all, and enhanced
83    // navigation from one to an interactive page does not reload the document, so the circuit can
84    // start minutes after this file ran - a wait that had given up by then would leave that page
85    // with neither guard. The wait costs two property reads twice a second and stops the moment
86    // there is a circuit to guard.
87    const POLL_MS = 500;
88
89    const waitForBlazor = setInterval(function () {
90        if (!window.Blazor || typeof window.Blazor.disconnect !== 'function') return;
91        clearInterval(waitForBlazor);
92
93        const sendDisconnectBeacon = window.Blazor.disconnect;
94        window.removeEventListener('pagehide', sendDisconnectBeacon);
95        window.addEventListener('pagehide', function (event) {
96            if (!event.persisted) sendDisconnectBeacon();
97        });
98
99        const handler = window.Blazor.defaultReconnectionHandler;
100        if (handler && '_resumeCallback' in handler) {
101            handler._resumeCallback = async function () { return false; };
102        } else {
103            // Renamed by a framework update - say so rather than quietly letting the resume back in.
104            console.warn('circuitStaleness: could not disable circuit resume; a failed rejoin may leave the page unresponsive.');
105        }
106    }, POLL_MS);
107})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.