cursedbelt 5.113.0 → 5.114.0
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.
- package/dist/react/components/ErrorBoundary.d.ts.map +1 -1
- package/dist/react/components/ErrorBoundary.js +23 -13
- package/dist/react/components/ErrorBoundary.js.map +1 -1
- package/dist/react/components/InlineEditableText.d.ts +11 -1
- package/dist/react/components/InlineEditableText.d.ts.map +1 -1
- package/dist/react/components/InlineEditableText.js +5 -4
- package/dist/react/components/InlineEditableText.js.map +1 -1
- package/dist/react/lazy/staleChunk.d.ts +19 -0
- package/dist/react/lazy/staleChunk.d.ts.map +1 -1
- package/dist/react/lazy/staleChunk.js +49 -2
- package/dist/react/lazy/staleChunk.js.map +1 -1
- package/package.json +1 -1
- package/src/react/components/ErrorBoundary.tsx +19 -11
- package/src/react/components/InlineEditableText.spec.tsx +25 -0
- package/src/react/components/InlineEditableText.tsx +16 -4
- package/src/react/components/StaleDeployHealer.spec.tsx +27 -0
- package/src/react/lazy/staleChunk.spec.tsx +63 -1
- package/src/react/lazy/staleChunk.ts +55 -2
|
@@ -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,
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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":";AAAA,OAAO,EAAE,SAAS,EAAkC,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACvF,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,qBAAqB,EAAE,0BAA0B,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"ErrorBoundary.js","sourceRoot":"","sources":["../../../src/react/components/ErrorBoundary.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
1
2
|
export interface InlineEditableTextProps {
|
|
2
3
|
value: string;
|
|
3
4
|
/** Called with the trimmed next value, only when it actually changed. */
|
|
@@ -35,6 +36,15 @@ export interface InlineEditableTextProps {
|
|
|
35
36
|
* would otherwise replace everything they wrote.
|
|
36
37
|
*/
|
|
37
38
|
caret?: 'select-all' | 'end';
|
|
39
|
+
/**
|
|
40
|
+
* How the value reads when it is NOT being edited — the click-to-edit display and the
|
|
41
|
+
* read-only text alike; the editor still edits the raw string. For text with structure
|
|
42
|
+
* the string only encodes (a read table's tab-separated rows drawn as a grid). The
|
|
43
|
+
* editable display is a `<button>`, so this returns phrasing content: spans, styled
|
|
44
|
+
* into whatever shape the text needs. Never called for an empty value — the
|
|
45
|
+
* placeholder shows then.
|
|
46
|
+
*/
|
|
47
|
+
display?: (value: string) => ReactNode;
|
|
38
48
|
/** The editor's character ceiling (default 10,000) — the server's limit, so the editor stops where a save would be refused. */
|
|
39
49
|
maxLength?: number;
|
|
40
50
|
ariaLabel?: string;
|
|
@@ -47,5 +57,5 @@ export interface InlineEditableTextProps {
|
|
|
47
57
|
* selects its text on mount so typing replaces (and the cursor is never stranded
|
|
48
58
|
* at position 0) — or, with `caret='end'`, puts the caret after the text.
|
|
49
59
|
*/
|
|
50
|
-
export declare function InlineEditableText({ value, onChange, canEdit, multiline, placeholder, className, inputClassName, trigger, wrap, caret, maxLength, ariaLabel, ...rest }: InlineEditableTextProps): import("react").JSX.Element | null;
|
|
60
|
+
export declare function InlineEditableText({ value, onChange, canEdit, multiline, placeholder, className, inputClassName, trigger, wrap, caret, maxLength, ariaLabel, display, ...rest }: InlineEditableTextProps): import("react").JSX.Element | null;
|
|
51
61
|
//# sourceMappingURL=InlineEditableText.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InlineEditableText.d.ts","sourceRoot":"","sources":["../../../src/react/components/InlineEditableText.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"InlineEditableText.d.ts","sourceRoot":"","sources":["../../../src/react/components/InlineEditableText.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,SAAS,EAAyB,MAAM,OAAO,CAAC;AAS9D,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,yEAAyE;IACzE,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,uEAAuE;IACvE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;gEAC4D;IAC5D,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;gEAC4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wDAAwD;IACxD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;sDAGkD;IAClD,IAAI,CAAC,EAAE,OAAO,CAAC;IACf;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1B;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,YAAY,GAAG,KAAK,CAAC;IAC7B;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,SAAS,CAAC;IACvC,+HAA+H;IAC/H,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,KAAK,EACL,QAAQ,EACR,OAAc,EACd,SAAiB,EACjB,WAAyB,EACzB,SAAS,EACT,cAAc,EACd,OAAgB,EAChB,IAAW,EACX,KAAoB,EACpB,SAAkB,EAClB,SAAS,EACT,OAAO,EACP,GAAG,IAAI,EACR,EAAE,uBAAuB,sCAkJzB"}
|
|
@@ -16,7 +16,7 @@ import { RAW_BUTTON } from '../lib/cn.js';
|
|
|
16
16
|
* selects its text on mount so typing replaces (and the cursor is never stranded
|
|
17
17
|
* at position 0) — or, with `caret='end'`, puts the caret after the text.
|
|
18
18
|
*/
|
|
19
|
-
export function InlineEditableText({ value, onChange, canEdit = true, multiline = false, placeholder = 'Add text…', className, inputClassName, trigger = 'text', wrap = true, caret = 'select-all', maxLength = 10_000, ariaLabel, ...rest }) {
|
|
19
|
+
export function InlineEditableText({ value, onChange, canEdit = true, multiline = false, placeholder = 'Add text…', className, inputClassName, trigger = 'text', wrap = true, caret = 'select-all', maxLength = 10_000, ariaLabel, display, ...rest }) {
|
|
20
20
|
const [editing, setEditing] = useState(false);
|
|
21
21
|
const [draft, setDraft] = useState(value);
|
|
22
22
|
const begin = () => {
|
|
@@ -74,13 +74,14 @@ export function InlineEditableText({ value, onChange, canEdit = true, multiline
|
|
|
74
74
|
// Read-only with no value → render nothing.
|
|
75
75
|
if (!canEdit && !value)
|
|
76
76
|
return null;
|
|
77
|
+
const shown = value && display ? display(value) : value || placeholder;
|
|
77
78
|
// Read-only: plain text.
|
|
78
79
|
if (!canEdit)
|
|
79
|
-
return _jsx("span", { ...rest, className: className, children:
|
|
80
|
+
return _jsx("span", { ...rest, className: className, children: shown });
|
|
80
81
|
// Icon trigger: text is inert (rows where clicking the text means open/select);
|
|
81
82
|
// the pencil is the explicit edit affordance.
|
|
82
83
|
if (trigger === 'icon') {
|
|
83
|
-
return (_jsxs("span", { ...rest, className: 'inline-flex min-w-0 items-center gap-1', children: [_jsx("span", { className: cn('truncate', !value && 'italic text-muted-foreground', className), children:
|
|
84
|
+
return (_jsxs("span", { ...rest, className: 'inline-flex min-w-0 items-center gap-1', children: [_jsx("span", { className: cn('truncate', !value && 'italic text-muted-foreground', className), children: shown }), _jsx(IconButton, { size: 'sm', label: ariaLabel ? `Edit ${ariaLabel}` : 'Edit', onClick: begin, children: _jsx(Pencil, {}) })] }));
|
|
84
85
|
}
|
|
85
86
|
// Editable display: a real <button> so Enter/Space activation + focus come free,
|
|
86
87
|
// styled to read as inline text.
|
|
@@ -103,6 +104,6 @@ export function InlineEditableText({ value, onChange, canEdit = true, multiline
|
|
|
103
104
|
// measuring gate (and any tool that reasons about layout) still sees 24px.
|
|
104
105
|
// `DESIGN.md` §5's footprint is the floor and UX_STANDARDS §0 records that it
|
|
105
106
|
// wins over the band — so the box grows too, and the band stays as the extra.
|
|
106
|
-
TAP_TARGET_HEIGHT, 'pointer-coarse:before:absolute pointer-coarse:before:top-[calc(var(--hit-slop-y)*-1)] pointer-coarse:before:bottom-[calc(var(--hit-slop-y)*-1)] pointer-coarse:before:-left-1 pointer-coarse:before:-right-1 pointer-coarse:before:content-[""]', wrap ? 'whitespace-pre-wrap break-words' : 'block w-full truncate', !value && 'italic text-muted-foreground', className), children:
|
|
107
|
+
TAP_TARGET_HEIGHT, 'pointer-coarse:before:absolute pointer-coarse:before:top-[calc(var(--hit-slop-y)*-1)] pointer-coarse:before:bottom-[calc(var(--hit-slop-y)*-1)] pointer-coarse:before:-left-1 pointer-coarse:before:-right-1 pointer-coarse:before:content-[""]', wrap ? 'whitespace-pre-wrap break-words' : 'block w-full truncate', !value && 'italic text-muted-foreground', className), children: shown }) }));
|
|
107
108
|
}
|
|
108
109
|
//# sourceMappingURL=InlineEditableText.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InlineEditableText.js","sourceRoot":"","sources":["../../../src/react/components/InlineEditableText.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,
|
|
1
|
+
{"version":3,"file":"InlineEditableText.js","sourceRoot":"","sources":["../../../src/react/components/InlineEditableText.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
|
|
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
|
-
|
|
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
|
|
1
|
+
{"version":3,"file":"staleChunk.js","sourceRoot":"","sources":["../../../src/react/lazy/staleChunk.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;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;AAEnD,sGAAsG;AACtG,MAAM,CAAC,MAAM,gBAAgB,GAAG,MAAM,CAAC;AAYvC,IAAI,MAAM,GAAG,CAAC,CAAC;AACf,IAAI,QAAQ,GAAG,KAAK,CAAC;AAErB,mGAAmG;AACnG,SAAS,YAAY;IACnB,IAAI,QAAQ,IAAI,OAAO,MAAM,KAAK,WAAW,IAAI,OAAO,MAAM,CAAC,gBAAgB,KAAK,UAAU;QAAE,OAAO;IACvG,QAAQ,GAAG,IAAI,CAAC;IAChB,MAAM,CAAC,gBAAgB,CAAC,cAAc,EAAE,GAAG,EAAE;QAC3C,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACtB,CAAC,CAAC,CAAC;IACH,qEAAqE;IACrE,MAAM,CAAC,gBAAgB,CAAC,UAAU,EAAE,GAAG,EAAE;QACvC,MAAM,GAAG,CAAC,CAAC;IACb,CAAC,CAAC,CAAC;AACL,CAAC;AACD,YAAY,EAAE,CAAC;AAEf,MAAM,KAAK,GAAG,CAAC,EAAU,EAAE,EAAE,CAAC,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AAEtF,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,MAAM,EAAE,GAAG,EAAE,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC;AACzG,CAAC;AAED,4FAA4F;AAC5F,MAAM,UAAU,UAAU,CAAC,MAAqB,UAAU,EAAE;IAC1D,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,EAAE,IAAI,CAAC,CAAC;IAC/B,IAAI,EAAE,IAAI,CAAC;QAAE,OAAO,CAAC,CAAC;IACtB,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,GAAG,gBAAgB,GAAG,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC;AACxD,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,IAAI,OAAO,GAAG,GAAG,EAAE,CAAC;YACpB,MAAM,IAAI,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC;YACjC,IAAI,IAAI,GAAG,CAAC,EAAE,CAAC;gBACb,MAAM,CAAC,OAAO,CAAC,KAAK,IAAI,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC;gBACrC,IAAI,CAAC;oBACH,OAAO,MAAM,IAAI,EAAE,CAAC;gBACtB,CAAC;gBAAC,MAAM,CAAC;oBACP,OAAO,GAAG,GAAG,EAAE,CAAC;gBAClB,CAAC;YACH,CAAC;YACD,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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursedbelt",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.114.0",
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
|
|
@@ -83,4 +83,29 @@ describe('InlineEditableText', () => {
|
|
|
83
83
|
expect(queryByRole('button')).toBeNull();
|
|
84
84
|
expect(queryByRole('textbox')).toBeNull();
|
|
85
85
|
});
|
|
86
|
+
|
|
87
|
+
test('display draws the value when not editing; the editor still edits the raw text', () => {
|
|
88
|
+
const table = 'Size\tTemp\nSmall\t220 C';
|
|
89
|
+
const grid = (v: string) => <span data-testid='grid'>{v.split('\n').length} rows</span>;
|
|
90
|
+
const onChange = mock(() => {});
|
|
91
|
+
const { getByRole, getByTestId, queryByTestId } = render(
|
|
92
|
+
<InlineEditableText multiline value={table} display={grid} onChange={onChange} />,
|
|
93
|
+
);
|
|
94
|
+
expect(getByTestId('grid').textContent).toBe('2 rows');
|
|
95
|
+
fireEvent.click(getByRole('button'));
|
|
96
|
+
expect(queryByTestId('grid')).toBeNull();
|
|
97
|
+
expect((getByRole('textbox') as HTMLTextAreaElement).value).toBe(table);
|
|
98
|
+
cleanup();
|
|
99
|
+
const locked = render(<InlineEditableText canEdit={false} value={table} display={grid} onChange={() => {}} />);
|
|
100
|
+
expect(locked.getByTestId('grid').textContent).toBe('2 rows');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('display is never called for an empty value — the placeholder shows', () => {
|
|
104
|
+
const display = mock((v: string) => <span>{v}</span>);
|
|
105
|
+
const { getByRole } = render(
|
|
106
|
+
<InlineEditableText value='' placeholder='Type it' display={display} onChange={() => {}} />,
|
|
107
|
+
);
|
|
108
|
+
expect(getByRole('button').textContent).toBe('Type it');
|
|
109
|
+
expect(display).not.toHaveBeenCalled();
|
|
110
|
+
});
|
|
86
111
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Pencil } from 'lucide-react';
|
|
2
|
-
import { useCallback, useState } from 'react';
|
|
2
|
+
import { type ReactNode, useCallback, useState } from 'react';
|
|
3
3
|
import { cn } from '../lib/cn.js';
|
|
4
4
|
import { AutoGrowTextarea } from './AutoGrowTextarea.js';
|
|
5
5
|
import { IconButton } from './IconButton.js';
|
|
@@ -45,6 +45,15 @@ export interface InlineEditableTextProps {
|
|
|
45
45
|
* would otherwise replace everything they wrote.
|
|
46
46
|
*/
|
|
47
47
|
caret?: 'select-all' | 'end';
|
|
48
|
+
/**
|
|
49
|
+
* How the value reads when it is NOT being edited — the click-to-edit display and the
|
|
50
|
+
* read-only text alike; the editor still edits the raw string. For text with structure
|
|
51
|
+
* the string only encodes (a read table's tab-separated rows drawn as a grid). The
|
|
52
|
+
* editable display is a `<button>`, so this returns phrasing content: spans, styled
|
|
53
|
+
* into whatever shape the text needs. Never called for an empty value — the
|
|
54
|
+
* placeholder shows then.
|
|
55
|
+
*/
|
|
56
|
+
display?: (value: string) => ReactNode;
|
|
48
57
|
/** The editor's character ceiling (default 10,000) — the server's limit, so the editor stops where a save would be refused. */
|
|
49
58
|
maxLength?: number;
|
|
50
59
|
ariaLabel?: string;
|
|
@@ -71,6 +80,7 @@ export function InlineEditableText({
|
|
|
71
80
|
caret = 'select-all',
|
|
72
81
|
maxLength = 10_000,
|
|
73
82
|
ariaLabel,
|
|
83
|
+
display,
|
|
74
84
|
...rest
|
|
75
85
|
}: InlineEditableTextProps) {
|
|
76
86
|
const [editing, setEditing] = useState(false);
|
|
@@ -161,8 +171,10 @@ export function InlineEditableText({
|
|
|
161
171
|
// Read-only with no value → render nothing.
|
|
162
172
|
if (!canEdit && !value) return null;
|
|
163
173
|
|
|
174
|
+
const shown = value && display ? display(value) : value || placeholder;
|
|
175
|
+
|
|
164
176
|
// Read-only: plain text.
|
|
165
|
-
if (!canEdit) return <span {...rest} className={className}>{
|
|
177
|
+
if (!canEdit) return <span {...rest} className={className}>{shown}</span>;
|
|
166
178
|
|
|
167
179
|
// Icon trigger: text is inert (rows where clicking the text means open/select);
|
|
168
180
|
// the pencil is the explicit edit affordance.
|
|
@@ -170,7 +182,7 @@ export function InlineEditableText({
|
|
|
170
182
|
return (
|
|
171
183
|
<span {...rest} className='inline-flex min-w-0 items-center gap-1'>
|
|
172
184
|
<span className={cn('truncate', !value && 'italic text-muted-foreground', className)}>
|
|
173
|
-
{
|
|
185
|
+
{shown}
|
|
174
186
|
</span>
|
|
175
187
|
<IconButton size='sm' label={ariaLabel ? `Edit ${ariaLabel}` : 'Edit'} onClick={begin}>
|
|
176
188
|
<Pencil />
|
|
@@ -212,7 +224,7 @@ export function InlineEditableText({
|
|
|
212
224
|
className,
|
|
213
225
|
)}
|
|
214
226
|
>
|
|
215
|
-
{
|
|
227
|
+
{shown}
|
|
216
228
|
</button>
|
|
217
229
|
</Tooltip>
|
|
218
230
|
);
|
|
@@ -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
|
-
|
|
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);
|