cursedbelt 5.113.0 → 5.113.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"ErrorBoundary.d.ts","sourceRoot":"","sources":["../../../src/react/components/ErrorBoundary.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,SAAS,EAAE,KAAK,SAAS,EAAuB,MAAM,OAAO,CAAC;AAIvF,OAAO,EAAwC,KAAK,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAerG;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,SAAS,CAAC;IACpB,wFAAwF;IACxF,QAAQ,CAAC,EAAE,SAAS,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,IAAI,KAAK,SAAS,CAAC,CAAC;IACxE,iEAAiE;IACjE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,KAAK,IAAI,CAAC;IAClD,qFAAqF;IACrF,SAAS,CAAC,EAAE,SAAS,OAAO,EAAE,CAAC;IAC/B;;;kFAG8E;IAC9E,QAAQ,CAAC,EAAE,OAAO,GAAG,iBAAiB,CAAC;CACxC;AAED,UAAU,kBAAkB;IAC1B,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB;AAQD,qBAAa,aAAc,SAAQ,SAAS,CAAC,kBAAkB,EAAE,kBAAkB,CAAC;IAClF,KAAK,EAAE,kBAAkB,CAAmB;IAE5C,MAAM,CAAC,wBAAwB,CAAC,KAAK,EAAE,KAAK,GAAG,kBAAkB;IAIjE,iBAAiB,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,GAAG,IAAI;IAItD,kBAAkB,CAAC,IAAI,EAAE,kBAAkB,GAAG,IAAI;IAMlD,KAAK,QAAO,IAAI,CAEd;IAEF,MAAM,IAAI,SAAS;CA0BpB;AA4CD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAMzD;AAED;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,IAAI,OAAO,CAGnD;AAED,MAAM,WAAW,sBAAsB;IACrC,gFAAgF;IAChF,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,4EAA4E;IAC5E,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;CACxB;AAED;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,GAAI,+BAA+B,sBAAsB,gCAsCtF,CAAC"}
1
+ {"version":3,"file":"ErrorBoundary.d.ts","sourceRoot":"","sources":["../../../src/react/components/ErrorBoundary.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,SAAS,EAAE,KAAK,SAAS,EAAuB,MAAM,OAAO,CAAC;AAIvF,OAAO,EAAwC,KAAK,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAerG;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,SAAS,CAAC;IACpB,wFAAwF;IACxF,QAAQ,CAAC,EAAE,SAAS,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,IAAI,KAAK,SAAS,CAAC,CAAC;IACxE,iEAAiE;IACjE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,KAAK,IAAI,CAAC;IAClD,qFAAqF;IACrF,SAAS,CAAC,EAAE,SAAS,OAAO,EAAE,CAAC;IAC/B;;;kFAG8E;IAC9E,QAAQ,CAAC,EAAE,OAAO,GAAG,iBAAiB,CAAC;CACxC;AAED,UAAU,kBAAkB;IAC1B,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB;AAQD,qBAAa,aAAc,SAAQ,SAAS,CAAC,kBAAkB,EAAE,kBAAkB,CAAC;IAClF,KAAK,EAAE,kBAAkB,CAAmB;IAE5C,MAAM,CAAC,wBAAwB,CAAC,KAAK,EAAE,KAAK,GAAG,kBAAkB;IAIjE,iBAAiB,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,GAAG,IAAI;IAItD,kBAAkB,CAAC,IAAI,EAAE,kBAAkB,GAAG,IAAI;IAMlD,KAAK,QAAO,IAAI,CAEd;IAEF,MAAM,IAAI,SAAS;CA0BpB;AA4CD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAMzD;AAED;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,IAAI,OAAO,CAGnD;AAED,MAAM,WAAW,sBAAsB;IACrC,gFAAgF;IAChF,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,4EAA4E;IAC5E,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;CACxB;AAED;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,GAAI,+BAA+B,sBAAsB,gCA8CtF,CAAC"}
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Component, useEffect, useState } from 'react';
3
3
  import { Alert } from './Alert.js';
4
4
  import { Spinner } from './Spinner.js';
5
- import { claimStaleChunkReload, staleChunkReloadedRecently } from '../lazy/staleChunk.js';
5
+ import { claimStaleChunkReload, leavingFor, staleChunkReloadedRecently } from '../lazy/staleChunk.js';
6
6
  import { ConnectionHealer, isUnreachableError } from './ConnectionHealer.js';
7
7
  const keysChanged = (a, b) => {
8
8
  if (!a || !b)
@@ -129,18 +129,28 @@ export const StaleDeployHealer = ({ error, onRecover, ...rest }) => {
129
129
  useEffect(() => {
130
130
  if (typeof window === 'undefined')
131
131
  return;
132
- if (!claimStaleChunkReload()) {
133
- setExhausted(true);
134
- return;
135
- }
136
- // A tick of delay so the banner paints first — an instant reload on a slow
137
- // connection reads as a flicker with no explanation.
138
- const timer = setTimeout(() => {
139
- if (onRecover)
140
- onRecover();
141
- else
142
- window.location.reload();
143
- }, 400);
132
+ let timer;
133
+ const heal = () => {
134
+ if (!claimStaleChunkReload()) {
135
+ setExhausted(true);
136
+ return;
137
+ }
138
+ // A tick of delay so the banner paints first — an instant reload on a slow
139
+ // connection reads as a flicker with no explanation.
140
+ timer = setTimeout(() => {
141
+ if (onRecover)
142
+ onRecover();
143
+ else
144
+ window.location.reload();
145
+ }, 400);
146
+ };
147
+ // A chunk cut off by the page being left (Safari cancels its loads at `beforeunload`) is
148
+ // not stale, and a reload would pull the tab back from where it was going — `staleChunk.ts`.
149
+ const leaving = leavingFor();
150
+ if (leaving > 0)
151
+ timer = setTimeout(heal, leaving);
152
+ else
153
+ heal();
144
154
  return () => clearTimeout(timer);
145
155
  }, [onRecover]);
146
156
  if (exhausted)
@@ -1 +1 @@
1
- {"version":3,"file":"ErrorBoundary.js","sourceRoot":"","sources":["../../../src/react/components/ErrorBoundary.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ErrorBoundary.js","sourceRoot":"","sources":["../../../src/react/components/ErrorBoundary.tsx"],"names":[],"mappings":"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"}
@@ -27,6 +27,17 @@
27
27
  * because a chunk genuinely missing from the deployed build would otherwise reload
28
28
  * forever with nothing able to remember trying.
29
29
  *
30
+ * 🔴 A chunk cut off by the page being LEFT is not stale. Safari (WebKit) cancels the
31
+ * old document's in-flight loads the moment a document navigation begins, right after
32
+ * `beforeunload`, and every lazy import still on its way rejects "Importing a module
33
+ * script failed." — the same words a deleted chunk gets. Reloading then sends the tab
34
+ * back to the page it was leaving: measured 2026-11-04 on roms' stage, a `goto
35
+ * /settings` 0–300 ms after the library loaded was "interrupted by another navigation
36
+ * to /" in 9 of 12 WebKit runs (forge task 4425). So a failure inside
37
+ * {@link LEAVING_GRACE_MS} of `beforeunload` waits instead: a page that goes never
38
+ * runs the timer, and one still here after it (the navigation was canceled, or was a
39
+ * download) retries the chunk once before treating it as stale.
40
+ *
30
41
  * `guardrailsEnforce`'s `no-raw-lazy` is the WHETHER: a bare `React.lazy` in an app
31
42
  * is refused, so the next lazy view cannot quietly reopen this.
32
43
  */
@@ -34,12 +45,20 @@ import { type ComponentType, type LazyExoticComponent } from 'react';
34
45
  export declare const STALE_CHUNK_RELOADED_AT_KEY = "cursedbelt.staleChunkReloadAt";
35
46
  /** A reload within this long of the last one is a loop, not a stale page. */
36
47
  export declare const STALE_CHUNK_RELOAD_WINDOW_MS = 60000;
48
+ /** How long after `beforeunload` a failed chunk is read as the navigation's doing, not a deploy's. */
49
+ export declare const LEAVING_GRACE_MS = 10000;
37
50
  export interface StaleChunkEnv {
38
51
  storage: Pick<Storage, 'getItem' | 'setItem'> | null;
39
52
  reload: () => void;
40
53
  now: () => number;
54
+ /** When this page last heard `beforeunload` (0: never, or it came back from the bfcache since). */
55
+ leftAt?: () => number;
56
+ /** A timer — one that never fires on a page that has gone. */
57
+ sleep?: (ms: number) => Promise<void>;
41
58
  }
42
59
  export declare function browserEnv(): StaleChunkEnv;
60
+ /** How much longer a chunk failure now is the page being left (0: it is not being left). */
61
+ export declare function leavingFor(env?: StaleChunkEnv): number;
43
62
  /**
44
63
  * Claim this tab's one stale-chunk reload for the window: true when a reload may
45
64
  * happen now, and the claim is already recorded. False when one happened inside the
@@ -1 +1 @@
1
- {"version":3,"file":"staleChunk.d.ts","sourceRoot":"","sources":["../../../src/react/lazy/staleChunk.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,mBAAmB,EAAQ,MAAM,OAAO,CAAC;AAE3E,eAAO,MAAM,2BAA2B,kCAAkC,CAAC;AAE3E,6EAA6E;AAC7E,eAAO,MAAM,4BAA4B,QAAS,CAAC;AAEnD,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,SAAS,GAAG,SAAS,CAAC,GAAG,IAAI,CAAC;IACrD,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,GAAG,EAAE,MAAM,MAAM,CAAC;CACnB;AAED,wBAAgB,UAAU,IAAI,aAAa,CAQ1C;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,GAAE,aAA4B,GAAG,OAAO,CAShF;AAED,sGAAsG;AACtG,wBAAgB,0BAA0B,CAAC,GAAG,GAAE,aAA4B,GAAG,OAAO,CASrF;AAED,iGAAiG;AACjG,wBAAgB,kBAAkB,CAAC,CAAC,EAClC,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACtB,GAAG,GAAE,MAAM,aAA0B,GACpC,MAAM,OAAO,CAAC,CAAC,CAAC,CAWlB;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,aAAa,CAAC,GAAG,CAAC,EACzD,IAAI,EAAE,MAAM,OAAO,CAAC;IAAE,OAAO,EAAE,CAAC,CAAA;CAAE,CAAC,EACnC,GAAG,CAAC,EAAE,MAAM,aAAa,GACxB,mBAAmB,CAAC,CAAC,CAAC,CAExB"}
1
+ {"version":3,"file":"staleChunk.d.ts","sourceRoot":"","sources":["../../../src/react/lazy/staleChunk.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,mBAAmB,EAAQ,MAAM,OAAO,CAAC;AAE3E,eAAO,MAAM,2BAA2B,kCAAkC,CAAC;AAE3E,6EAA6E;AAC7E,eAAO,MAAM,4BAA4B,QAAS,CAAC;AAEnD,sGAAsG;AACtG,eAAO,MAAM,gBAAgB,QAAS,CAAC;AAEvC,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,SAAS,GAAG,SAAS,CAAC,GAAG,IAAI,CAAC;IACrD,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,GAAG,EAAE,MAAM,MAAM,CAAC;IAClB,mGAAmG;IACnG,MAAM,CAAC,EAAE,MAAM,MAAM,CAAC;IACtB,8DAA8D;IAC9D,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AAqBD,wBAAgB,UAAU,IAAI,aAAa,CAQ1C;AAED,4FAA4F;AAC5F,wBAAgB,UAAU,CAAC,GAAG,GAAE,aAA4B,GAAG,MAAM,CAIpE;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,GAAE,aAA4B,GAAG,OAAO,CAShF;AAED,sGAAsG;AACtG,wBAAgB,0BAA0B,CAAC,GAAG,GAAE,aAA4B,GAAG,OAAO,CASrF;AAED,iGAAiG;AACjG,wBAAgB,kBAAkB,CAAC,CAAC,EAClC,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACtB,GAAG,GAAE,MAAM,aAA0B,GACpC,MAAM,OAAO,CAAC,CAAC,CAAC,CAoBlB;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,aAAa,CAAC,GAAG,CAAC,EACzD,IAAI,EAAE,MAAM,OAAO,CAAC;IAAE,OAAO,EAAE,CAAC,CAAA;CAAE,CAAC,EACnC,GAAG,CAAC,EAAE,MAAM,aAAa,GACxB,mBAAmB,CAAC,CAAC,CAAC,CAExB"}
@@ -27,6 +27,17 @@
27
27
  * because a chunk genuinely missing from the deployed build would otherwise reload
28
28
  * forever with nothing able to remember trying.
29
29
  *
30
+ * 🔴 A chunk cut off by the page being LEFT is not stale. Safari (WebKit) cancels the
31
+ * old document's in-flight loads the moment a document navigation begins, right after
32
+ * `beforeunload`, and every lazy import still on its way rejects "Importing a module
33
+ * script failed." — the same words a deleted chunk gets. Reloading then sends the tab
34
+ * back to the page it was leaving: measured 2026-11-04 on roms' stage, a `goto
35
+ * /settings` 0–300 ms after the library loaded was "interrupted by another navigation
36
+ * to /" in 9 of 12 WebKit runs (forge task 4425). So a failure inside
37
+ * {@link LEAVING_GRACE_MS} of `beforeunload` waits instead: a page that goes never
38
+ * runs the timer, and one still here after it (the navigation was canceled, or was a
39
+ * download) retries the chunk once before treating it as stale.
40
+ *
30
41
  * `guardrailsEnforce`'s `no-raw-lazy` is the WHETHER: a bare `React.lazy` in an app
31
42
  * is refused, so the next lazy view cannot quietly reopen this.
32
43
  */
@@ -34,6 +45,25 @@ import { lazy } from 'react';
34
45
  export const STALE_CHUNK_RELOADED_AT_KEY = 'cursedbelt.staleChunkReloadAt';
35
46
  /** A reload within this long of the last one is a loop, not a stale page. */
36
47
  export const STALE_CHUNK_RELOAD_WINDOW_MS = 60_000;
48
+ /** How long after `beforeunload` a failed chunk is read as the navigation's doing, not a deploy's. */
49
+ export const LEAVING_GRACE_MS = 10_000;
50
+ let leftAt = 0;
51
+ let watching = false;
52
+ /** Note when the page starts being left. Installed on import, so it hears the first navigation. */
53
+ function watchLeaving() {
54
+ if (watching || typeof window === 'undefined' || typeof window.addEventListener !== 'function')
55
+ return;
56
+ watching = true;
57
+ window.addEventListener('beforeunload', () => {
58
+ leftAt = Date.now();
59
+ });
60
+ // Back from the bfcache: the navigation that was leaving it is over.
61
+ window.addEventListener('pageshow', () => {
62
+ leftAt = 0;
63
+ });
64
+ }
65
+ watchLeaving();
66
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
37
67
  export function browserEnv() {
38
68
  let storage = null;
39
69
  try {
@@ -42,7 +72,14 @@ export function browserEnv() {
42
72
  catch {
43
73
  // Storage refused (blocked site data) — no memory, so no reload: the guard fails closed.
44
74
  }
45
- return { storage, reload: () => window.location.reload(), now: Date.now };
75
+ return { storage, reload: () => window.location.reload(), now: Date.now, leftAt: () => leftAt, sleep };
76
+ }
77
+ /** How much longer a chunk failure now is the page being left (0: it is not being left). */
78
+ export function leavingFor(env = browserEnv()) {
79
+ const at = env.leftAt?.() ?? 0;
80
+ if (at <= 0)
81
+ return 0;
82
+ return Math.max(0, at + LEAVING_GRACE_MS - env.now());
46
83
  }
47
84
  /**
48
85
  * Claim this tab's one stale-chunk reload for the window: true when a reload may
@@ -81,7 +118,17 @@ export function reloadOnStaleChunk(load, env = browserEnv) {
81
118
  return await load();
82
119
  }
83
120
  catch (error) {
84
- const current = env();
121
+ let current = env();
122
+ const wait = leavingFor(current);
123
+ if (wait > 0) {
124
+ await (current.sleep ?? sleep)(wait);
125
+ try {
126
+ return await load();
127
+ }
128
+ catch {
129
+ current = env();
130
+ }
131
+ }
85
132
  if (!claimStaleChunkReload(current))
86
133
  throw error;
87
134
  current.reload();
@@ -1 +1 @@
1
- {"version":3,"file":"staleChunk.js","sourceRoot":"","sources":["../../../src/react/lazy/staleChunk.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,OAAO,EAAgD,IAAI,EAAE,MAAM,OAAO,CAAC;AAE3E,MAAM,CAAC,MAAM,2BAA2B,GAAG,+BAA+B,CAAC;AAE3E,6EAA6E;AAC7E,MAAM,CAAC,MAAM,4BAA4B,GAAG,MAAM,CAAC;AAQnD,MAAM,UAAU,UAAU;IACxB,IAAI,OAAO,GAA6B,IAAI,CAAC;IAC7C,IAAI,CAAC;QACH,OAAO,GAAG,MAAM,CAAC,cAAc,CAAC;IAClC,CAAC;IAAC,MAAM,CAAC;QACP,yFAAyF;IAC3F,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC;AAC5E,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,MAAqB,UAAU,EAAE;IACrE,IAAI,0BAA0B,CAAC,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IAClD,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC;IAChC,IAAI,CAAC;QACH,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,2BAA2B,EAAE,KAAK,CAAC,CAAC;QACzD,OAAO,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,2BAA2B,CAAC,KAAK,KAAK,CAAC;IACrE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,sGAAsG;AACtG,MAAM,UAAU,0BAA0B,CAAC,MAAqB,UAAU,EAAE;IAC1E,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,GAAG,CAAC;IAC7B,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAC1B,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,2BAA2B,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC;QAC5E,OAAO,IAAI,GAAG,CAAC,IAAI,GAAG,EAAE,GAAG,IAAI,GAAG,4BAA4B,CAAC;IACjE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,iGAAiG;AACjG,MAAM,UAAU,kBAAkB,CAChC,IAAsB,EACtB,MAA2B,UAAU;IAErC,OAAO,KAAK,IAAI,EAAE;QAChB,IAAI,CAAC;YACH,OAAO,MAAM,IAAI,EAAE,CAAC;QACtB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC;YACtB,IAAI,CAAC,qBAAqB,CAAC,OAAO,CAAC;gBAAE,MAAM,KAAK,CAAC;YACjD,OAAO,CAAC,MAAM,EAAE,CAAC;YACjB,OAAO,IAAI,OAAO,CAAI,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QACzC,CAAC;IACH,CAAC,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAC5B,IAAmC,EACnC,GAAyB;IAEzB,OAAO,IAAI,CAAC,kBAAkB,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC;AAC7C,CAAC"}
1
+ {"version":3,"file":"staleChunk.js","sourceRoot":"","sources":["../../../src/react/lazy/staleChunk.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursedbelt",
3
- "version": "5.113.0",
3
+ "version": "5.113.1",
4
4
  "license": "ISC",
5
5
  "type": "module",
6
6
  "description": "The React design system of the cursedbelt split — components, styles, theme. cursedbelt-core below it; server tier in cursedbelt-server.",
@@ -1,7 +1,7 @@
1
1
  import { Component, type ErrorInfo, type ReactNode, useEffect, useState } from 'react';
2
2
  import { Alert } from './Alert.js';
3
3
  import { Spinner } from './Spinner.js';
4
- import { claimStaleChunkReload, staleChunkReloadedRecently } from '../lazy/staleChunk.js';
4
+ import { claimStaleChunkReload, leavingFor, staleChunkReloadedRecently } from '../lazy/staleChunk.js';
5
5
  import { ConnectionHealer, isUnreachableError, type ServerHealOptions } from './ConnectionHealer.js';
6
6
 
7
7
  /* ───────────────────────────────────────────────────────────────────────────
@@ -191,16 +191,24 @@ export const StaleDeployHealer = ({ error, onRecover, ...rest }: StaleDeployHeal
191
191
 
192
192
  useEffect(() => {
193
193
  if (typeof window === 'undefined') return;
194
- if (!claimStaleChunkReload()) {
195
- setExhausted(true);
196
- return;
197
- }
198
- // A tick of delay so the banner paints first — an instant reload on a slow
199
- // connection reads as a flicker with no explanation.
200
- const timer = setTimeout(() => {
201
- if (onRecover) onRecover();
202
- else window.location.reload();
203
- }, 400);
194
+ let timer: ReturnType<typeof setTimeout> | undefined;
195
+ const heal = () => {
196
+ if (!claimStaleChunkReload()) {
197
+ setExhausted(true);
198
+ return;
199
+ }
200
+ // A tick of delay so the banner paints first — an instant reload on a slow
201
+ // connection reads as a flicker with no explanation.
202
+ timer = setTimeout(() => {
203
+ if (onRecover) onRecover();
204
+ else window.location.reload();
205
+ }, 400);
206
+ };
207
+ // A chunk cut off by the page being left (Safari cancels its loads at `beforeunload`) is
208
+ // not stale, and a reload would pull the tab back from where it was going — `staleChunk.ts`.
209
+ const leaving = leavingFor();
210
+ if (leaving > 0) timer = setTimeout(heal, leaving);
211
+ else heal();
204
212
  return () => clearTimeout(timer);
205
213
  }, [onRecover]);
206
214
 
@@ -136,3 +136,30 @@ describe('StaleDeployHealer shares lazyWithReload\'s memory', () => {
136
136
  expect(recovered).toBe(0);
137
137
  });
138
138
  });
139
+
140
+ /**
141
+ * 🔴 Safari cancels the old page's chunks at `beforeunload`, with a stale chunk's words: a boundary
142
+ * that reloaded for it pulled the tab back from where it was going (forge task 4425).
143
+ */
144
+ describe('StaleDeployHealer leaves a page that is being left alone', () => {
145
+ afterEach(() => {
146
+ window.dispatchEvent(new Event('pageshow'));
147
+ });
148
+
149
+ test('no reload and no claim within the grace after beforeunload', async () => {
150
+ window.dispatchEvent(new Event('beforeunload'));
151
+ let recovered = 0;
152
+ render(<StaleDeployHealer error={new TypeError('Importing a module script failed.')} onRecover={() => recovered++} />);
153
+ await new Promise((r) => setTimeout(r, 800));
154
+ expect(recovered).toBe(0);
155
+ expect(window.sessionStorage.getItem(STALE_CHUNK_RELOADED_AT_KEY)).toBeNull();
156
+ });
157
+
158
+ test('back from the bfcache, it heals as before', async () => {
159
+ window.dispatchEvent(new Event('beforeunload'));
160
+ window.dispatchEvent(new Event('pageshow'));
161
+ let recovered = 0;
162
+ render(<StaleDeployHealer error={OWNERS_ERROR} onRecover={() => recovered++} />);
163
+ await waitFor(() => expect(recovered).toBe(1), { timeout: 2000 });
164
+ });
165
+ });
@@ -15,13 +15,23 @@ import {
15
15
 
16
16
  function fakeEnv(at: number) {
17
17
  const store = new Map<string, string>();
18
- const state = { reloads: 0, now: at };
18
+ const state = { reloads: 0, now: at, leftAt: 0, slept: [] as number[], wake: () => {} };
19
19
  const env: StaleChunkEnv = {
20
20
  storage: { getItem: (k) => store.get(k) ?? null, setItem: (k, v) => void store.set(k, v) },
21
21
  reload: () => {
22
22
  state.reloads += 1;
23
23
  },
24
24
  now: () => state.now,
25
+ leftAt: () => state.leftAt,
26
+ // A timer the test fires by hand — or never, as on a page that has gone.
27
+ sleep: (ms) =>
28
+ new Promise<void>((resolve) => {
29
+ state.slept.push(ms);
30
+ state.wake = () => {
31
+ state.now += ms;
32
+ resolve();
33
+ };
34
+ }),
25
35
  };
26
36
  return { env: () => env, state, store };
27
37
  }
@@ -110,6 +120,58 @@ describe('a chunk a deploy removed', () => {
110
120
  });
111
121
  });
112
122
 
123
+ /*
124
+ * 🔴 Safari cancels the old page's in-flight chunks the moment a navigation begins (right after
125
+ * `beforeunload`), with the very words a deleted chunk gets. Reloading for it pulled roms' tab back
126
+ * to the library from the /settings it was going to (forge task 4425, 9 of 12 WebKit runs).
127
+ */
128
+ describe('a chunk cut off by the page being left', () => {
129
+ /** `staleChunk.ts`'s `LEAVING_GRACE_MS`, pinned: a test of the bug must not need the fix to compile. */
130
+ const LEAVING_GRACE_MS = 10_000;
131
+ const cutOff = () => Promise.reject(new TypeError('Importing a module script failed.'));
132
+
133
+ test('never reloads, and claims no reload, while the page goes', async () => {
134
+ const { env, state, store } = fakeEnv(1_000_000);
135
+ state.leftAt = state.now - 50;
136
+ const pending = reloadOnStaleChunk(cutOff, env)();
137
+ expect(await settled(pending)).toBe(false);
138
+ expect(state.reloads).toBe(0);
139
+ expect(store.get(STALE_CHUNK_RELOADED_AT_KEY)).toBeUndefined();
140
+ expect(state.slept).toEqual([LEAVING_GRACE_MS - 50]);
141
+ });
142
+
143
+ test('a page still here after the grace retries the chunk, and a chunk that loads is the view', async () => {
144
+ const { env, state } = fakeEnv(1_000_000);
145
+ state.leftAt = state.now;
146
+ let calls = 0;
147
+ const pending = reloadOnStaleChunk(() => (++calls === 1 ? cutOff() : Promise.resolve('view')), env)();
148
+ await tick();
149
+ state.wake();
150
+ expect(await pending).toBe('view');
151
+ expect(calls).toBe(2);
152
+ expect(state.reloads).toBe(0);
153
+ });
154
+
155
+ test('a page still here whose chunk fails again is stale after all, and reloads once', async () => {
156
+ const { env, state } = fakeEnv(1_000_000);
157
+ state.leftAt = state.now;
158
+ const pending = reloadOnStaleChunk(cutOff, env)();
159
+ await tick();
160
+ state.wake();
161
+ expect(await settled(pending)).toBe(false);
162
+ expect(state.reloads).toBe(1);
163
+ });
164
+
165
+ test('a beforeunload older than the grace is no excuse: a gone chunk reloads at once', async () => {
166
+ const { env, state } = fakeEnv(1_000_000);
167
+ state.leftAt = state.now - LEAVING_GRACE_MS;
168
+ void reloadOnStaleChunk(gone, env)();
169
+ await tick();
170
+ expect(state.slept).toEqual([]);
171
+ expect(state.reloads).toBe(1);
172
+ });
173
+ });
174
+
113
175
  describe('lazyWithReload', () => {
114
176
  test('renders the view when its chunk loads', async () => {
115
177
  const { env } = fakeEnv(1_000_000);
@@ -27,6 +27,17 @@
27
27
  * because a chunk genuinely missing from the deployed build would otherwise reload
28
28
  * forever with nothing able to remember trying.
29
29
  *
30
+ * 🔴 A chunk cut off by the page being LEFT is not stale. Safari (WebKit) cancels the
31
+ * old document's in-flight loads the moment a document navigation begins, right after
32
+ * `beforeunload`, and every lazy import still on its way rejects "Importing a module
33
+ * script failed." — the same words a deleted chunk gets. Reloading then sends the tab
34
+ * back to the page it was leaving: measured 2026-11-04 on roms' stage, a `goto
35
+ * /settings` 0–300 ms after the library loaded was "interrupted by another navigation
36
+ * to /" in 9 of 12 WebKit runs (forge task 4425). So a failure inside
37
+ * {@link LEAVING_GRACE_MS} of `beforeunload` waits instead: a page that goes never
38
+ * runs the timer, and one still here after it (the navigation was canceled, or was a
39
+ * download) retries the chunk once before treating it as stale.
40
+ *
30
41
  * `guardrailsEnforce`'s `no-raw-lazy` is the WHETHER: a bare `React.lazy` in an app
31
42
  * is refused, so the next lazy view cannot quietly reopen this.
32
43
  */
@@ -37,11 +48,37 @@ export const STALE_CHUNK_RELOADED_AT_KEY = 'cursedbelt.staleChunkReloadAt';
37
48
  /** A reload within this long of the last one is a loop, not a stale page. */
38
49
  export const STALE_CHUNK_RELOAD_WINDOW_MS = 60_000;
39
50
 
51
+ /** How long after `beforeunload` a failed chunk is read as the navigation's doing, not a deploy's. */
52
+ export const LEAVING_GRACE_MS = 10_000;
53
+
40
54
  export interface StaleChunkEnv {
41
55
  storage: Pick<Storage, 'getItem' | 'setItem'> | null;
42
56
  reload: () => void;
43
57
  now: () => number;
58
+ /** When this page last heard `beforeunload` (0: never, or it came back from the bfcache since). */
59
+ leftAt?: () => number;
60
+ /** A timer — one that never fires on a page that has gone. */
61
+ sleep?: (ms: number) => Promise<void>;
62
+ }
63
+
64
+ let leftAt = 0;
65
+ let watching = false;
66
+
67
+ /** Note when the page starts being left. Installed on import, so it hears the first navigation. */
68
+ function watchLeaving(): void {
69
+ if (watching || typeof window === 'undefined' || typeof window.addEventListener !== 'function') return;
70
+ watching = true;
71
+ window.addEventListener('beforeunload', () => {
72
+ leftAt = Date.now();
73
+ });
74
+ // Back from the bfcache: the navigation that was leaving it is over.
75
+ window.addEventListener('pageshow', () => {
76
+ leftAt = 0;
77
+ });
44
78
  }
79
+ watchLeaving();
80
+
81
+ const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
45
82
 
46
83
  export function browserEnv(): StaleChunkEnv {
47
84
  let storage: StaleChunkEnv['storage'] = null;
@@ -50,7 +87,14 @@ export function browserEnv(): StaleChunkEnv {
50
87
  } catch {
51
88
  // Storage refused (blocked site data) — no memory, so no reload: the guard fails closed.
52
89
  }
53
- return { storage, reload: () => window.location.reload(), now: Date.now };
90
+ return { storage, reload: () => window.location.reload(), now: Date.now, leftAt: () => leftAt, sleep };
91
+ }
92
+
93
+ /** How much longer a chunk failure now is the page being left (0: it is not being left). */
94
+ export function leavingFor(env: StaleChunkEnv = browserEnv()): number {
95
+ const at = env.leftAt?.() ?? 0;
96
+ if (at <= 0) return 0;
97
+ return Math.max(0, at + LEAVING_GRACE_MS - env.now());
54
98
  }
55
99
 
56
100
  /**
@@ -90,7 +134,16 @@ export function reloadOnStaleChunk<T>(
90
134
  try {
91
135
  return await load();
92
136
  } catch (error) {
93
- const current = env();
137
+ let current = env();
138
+ const wait = leavingFor(current);
139
+ if (wait > 0) {
140
+ await (current.sleep ?? sleep)(wait);
141
+ try {
142
+ return await load();
143
+ } catch {
144
+ current = env();
145
+ }
146
+ }
94
147
  if (!claimStaleChunkReload(current)) throw error;
95
148
  current.reload();
96
149
  return new Promise<T>(() => undefined);