@rangojs/router 0.20.0 → 0.22.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/bin/rango.js +39 -15
- package/dist/types/browser/event-controller.d.ts +49 -0
- package/dist/types/browser/history-state.d.ts +16 -4
- package/dist/types/browser/navigation-store.d.ts +9 -0
- package/dist/types/browser/navigation-transaction.d.ts +7 -0
- package/dist/types/browser/partial-update.d.ts +2 -1
- package/dist/types/browser/prefetch/default-strategy.d.ts +16 -20
- package/dist/types/browser/rango-state.d.ts +3 -1
- package/dist/types/browser/react/NavigationProvider.d.ts +14 -1
- package/dist/types/browser/react/context.d.ts +25 -0
- package/dist/types/browser/react/location-state-shared.d.ts +84 -10
- package/dist/types/browser/react/location-state.d.ts +7 -2
- package/dist/types/browser/rsc-router.d.ts +13 -2
- package/dist/types/browser/types.d.ts +10 -0
- package/dist/types/build/generate-manifest.d.ts +2 -0
- package/dist/types/cache/background-task.d.ts +22 -0
- package/dist/types/cache/cache-key-utils.d.ts +50 -5
- package/dist/types/cache/cache-policy.d.ts +17 -0
- package/dist/types/cache/cache-scope.d.ts +5 -6
- package/dist/types/cache/cf/cf-cache-store.d.ts +34 -7
- package/dist/types/cache/cf/cf-cache-types.d.ts +12 -4
- package/dist/types/cache/document-cache.d.ts +8 -5
- package/dist/types/cache/handle-snapshot.d.ts +59 -50
- package/dist/types/cache/index.d.ts +3 -1
- package/dist/types/cache/invalidation-order.d.ts +14 -0
- package/dist/types/cache/memory-segment-store.d.ts +33 -1
- package/dist/types/cache/shell-snapshot.d.ts +29 -4
- package/dist/types/cache/store-scope.d.ts +22 -0
- package/dist/types/cache/tag-invalidation.d.ts +7 -6
- package/dist/types/cache/types.d.ts +84 -24
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +35 -17
- package/dist/types/client-urls/navigation.d.ts +7 -1
- package/dist/types/client-urls/optimistic-location.d.ts +10 -4
- package/dist/types/errors.d.ts +6 -0
- package/dist/types/handles/deferred-resolution.d.ts +18 -1
- package/dist/types/internal-suspense-audit.d.ts +1 -0
- package/dist/types/loader-store.d.ts +4 -8
- package/dist/types/outlet-provider.d.ts +2 -1
- package/dist/types/prerender/build-shell-capture.d.ts +8 -6
- package/dist/types/prerender/cloudflare.d.ts +49 -0
- package/dist/types/prerender/create-prerender-trigger.d.ts +121 -0
- package/dist/types/prerender/index.d.ts +12 -0
- package/dist/types/prerender/memory-prerender-store.d.ts +29 -0
- package/dist/types/prerender/on-demand.d.ts +272 -0
- package/dist/types/prerender/producer-guard.d.ts +33 -0
- package/dist/types/prerender/shell-manifest-key.d.ts +1 -18
- package/dist/types/prerender/warm-request.d.ts +65 -0
- package/dist/types/prerender/warm.d.ts +65 -0
- package/dist/types/prerender/writable-store.d.ts +170 -0
- package/dist/types/prerender.d.ts +27 -0
- package/dist/types/reverse.d.ts +1 -1
- package/dist/types/route-content-wrapper.d.ts +3 -1
- package/dist/types/router/error-handling.d.ts +2 -0
- package/dist/types/router/handler-context.d.ts +1 -1
- package/dist/types/router/match-middleware/cache-lookup.d.ts +15 -1
- package/dist/types/router/navigation-snapshot.d.ts +8 -1
- package/dist/types/router/pattern-matching.d.ts +2 -0
- package/dist/types/router/prefetch-default.d.ts +4 -4
- package/dist/types/router/prerender-match.d.ts +23 -1
- package/dist/types/router/route-trie-builder.d.ts +8 -1
- package/dist/types/router/router-context.d.ts +1 -0
- package/dist/types/router/router-interfaces.d.ts +32 -1
- package/dist/types/router/router-options.d.ts +39 -5
- package/dist/types/router/segment-resolution/fresh.d.ts +7 -0
- package/dist/types/router/segment-resolution/loader-cache.d.ts +23 -10
- package/dist/types/router/segment-resolution/revalidation.d.ts +5 -1
- package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +43 -0
- package/dist/types/router/trie-matching.d.ts +2 -0
- package/dist/types/router-versions.d.ts +39 -0
- package/dist/types/rsc/handler-context.d.ts +5 -0
- package/dist/types/rsc/shell-build-manifest.d.ts +9 -6
- package/dist/types/rsc/shell-capture-constants.d.ts +20 -5
- package/dist/types/rsc/shell-capture.d.ts +14 -1
- package/dist/types/rsc/shell-serve.d.ts +2 -2
- package/dist/types/rsc/ssr-module-loader.d.ts +15 -0
- package/dist/types/rsc/types.d.ts +12 -6
- package/dist/types/segment-boundary-content.d.ts +17 -0
- package/dist/types/segment-loader-promise.d.ts +2 -6
- package/dist/types/server/build-version-table.d.ts +53 -0
- package/dist/types/server/context.d.ts +12 -4
- package/dist/types/server/handle-store.d.ts +89 -46
- package/dist/types/server/request-context.d.ts +84 -9
- package/dist/types/suspense-audit-react.d.ts +26 -0
- package/dist/types/suspense-audit.d.ts +157 -0
- package/dist/types/testing/action-form.d.ts +34 -0
- package/dist/types/testing/build-versions.d.ts +32 -0
- package/dist/types/testing/dom.entry.d.ts +1 -1
- package/dist/types/testing/flight.entry.d.ts +3 -1
- package/dist/types/testing/index.d.ts +6 -0
- package/dist/types/testing/internal/seed-vars.d.ts +7 -2
- package/dist/types/testing/internal/server-modules.d.ts +8 -0
- package/dist/types/testing/location-state-key.d.ts +5 -1
- package/dist/types/testing/render-route.d.ts +77 -6
- package/dist/types/testing/serve-shell-request.d.ts +55 -4
- package/dist/types/testing/shell-status.d.ts +14 -6
- package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +9 -6
- package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
- package/dist/types/thenable-status.d.ts +6 -0
- package/dist/types/types/handler-context.d.ts +15 -5
- package/dist/types/types/route-entry.d.ts +5 -0
- package/dist/types/urls/path-helper.d.ts +18 -0
- package/dist/types/vite/discovery/build-versions.d.ts +393 -0
- package/dist/types/vite/discovery/installed-packages.d.ts +26 -0
- package/dist/types/vite/discovery/router-versions-phase.d.ts +52 -0
- package/dist/types/vite/discovery/shell-prerender-phase.d.ts +7 -17
- package/dist/types/vite/discovery/state.d.ts +25 -0
- package/dist/types/vite/encryption-key.d.ts +18 -0
- package/dist/types/vite/inject-client-debug.d.ts +6 -25
- package/dist/types/vite/plugin-types.d.ts +27 -0
- package/dist/types/vite/plugins/{version-injector.d.ts → entry-bootstrap-injector.d.ts} +4 -6
- package/dist/types/vite/plugins/expose-ids/export-analysis.d.ts +1 -0
- package/dist/types/vite/plugins/expose-ids/router-transform.d.ts +14 -0
- package/dist/types/vite/plugins/stable-rsc-output.d.ts +51 -0
- package/dist/types/vite/plugins/version-plugin.d.ts +9 -11
- package/dist/types/vite/plugins/virtual-entries.d.ts +18 -1
- package/dist/types/vite/router-discovery.d.ts +6 -0
- package/dist/types/vite/utils/bundle-analysis.d.ts +6 -2
- package/dist/types/vite/utils/client-chunks.d.ts +2 -0
- package/dist/types/vite/utils/compare-strings.d.ts +7 -0
- package/dist/types/vite/utils/package-resolution.d.ts +8 -0
- package/dist/types/vite/utils/prerender-utils.d.ts +2 -0
- package/dist/vite/index.js +1707 -931
- package/package.json +12 -3
- package/skills/cache-guide/SKILL.md +102 -37
- package/skills/caching/SKILL.md +56 -9
- package/skills/catalog.json +1 -1
- package/skills/client-urls/SKILL.md +16 -0
- package/skills/cloudflare/SKILL.md +133 -1
- package/skills/comparison/references/framework-comparison.md +17 -13
- package/skills/deployment-caching/SKILL.md +33 -12
- package/skills/document-cache/SKILL.md +16 -14
- package/skills/hooks/SKILL.md +25 -0
- package/skills/hooks/handle-and-actions.md +5 -0
- package/skills/hooks/navigation.md +38 -0
- package/skills/hooks/state.md +213 -11
- package/skills/hooks/urls.md +40 -1
- package/skills/host-router/SKILL.md +31 -0
- package/skills/intercept/SKILL.md +6 -0
- package/skills/loader/SKILL.md +25 -5
- package/skills/middleware/SKILL.md +2 -0
- package/skills/migrate-nextjs/SKILL.md +82 -9
- package/skills/ppr/SKILL.md +150 -48
- package/skills/prerender/SKILL.md +651 -10
- package/skills/rango/SKILL.md +9 -7
- package/skills/response-routes/SKILL.md +5 -5
- package/skills/route/SKILL.md +29 -0
- package/skills/router-setup/SKILL.md +15 -1
- package/skills/testing/SKILL.md +32 -27
- package/skills/testing/cache-prerender.md +18 -17
- package/skills/testing/client-components.md +193 -8
- package/skills/testing/handles.md +2 -0
- package/skills/testing/loader.md +1 -1
- package/skills/testing/middleware.md +1 -1
- package/skills/testing/render-handler.md +18 -18
- package/skills/testing/serve-shell-request.md +167 -22
- package/skills/testing/server-actions.md +17 -17
- package/skills/testing/server-tree.md +1 -1
- package/skills/typesafety/generated-files-and-cli.md +1 -1
- package/skills/use-cache/SKILL.md +25 -10
- package/skills/vercel/SKILL.md +5 -3
- package/skills/view-transitions/SKILL.md +9 -4
- package/src/bin/rango.ts +1 -1
- package/src/browser/event-controller.ts +119 -2
- package/src/browser/history-state.ts +39 -17
- package/src/browser/link-interceptor.ts +2 -2
- package/src/browser/navigation-bridge.ts +104 -41
- package/src/browser/navigation-store.ts +26 -0
- package/src/browser/navigation-transaction.ts +24 -12
- package/src/browser/network-error-handler.ts +2 -0
- package/src/browser/partial-update.ts +58 -14
- package/src/browser/prefetch/default-strategy.ts +22 -42
- package/src/browser/rango-state.ts +3 -1
- package/src/browser/react/Link.tsx +37 -43
- package/src/browser/react/NavigationProvider.tsx +85 -1
- package/src/browser/react/context.ts +30 -0
- package/src/browser/react/location-state-shared.ts +200 -34
- package/src/browser/react/location-state.ts +28 -77
- package/src/browser/react/use-action.ts +4 -1
- package/src/browser/react/use-handle.ts +3 -0
- package/src/browser/react/use-link-status.ts +4 -3
- package/src/browser/react/use-navigation.ts +4 -1
- package/src/browser/react/use-params.ts +4 -1
- package/src/browser/react/use-pathname.ts +4 -1
- package/src/browser/react/use-search-params.ts +5 -3
- package/src/browser/react/use-segments.ts +36 -54
- package/src/browser/rsc-router.tsx +93 -31
- package/src/browser/segment-reconciler.ts +3 -3
- package/src/browser/server-action-bridge.ts +43 -7
- package/src/browser/types.ts +11 -0
- package/src/build/generate-manifest.ts +17 -0
- package/src/build/merge-full-manifests.ts +5 -0
- package/src/build/route-trie.ts +1 -0
- package/src/cache/background-task.ts +61 -0
- package/src/cache/cache-key-utils.ts +91 -12
- package/src/cache/cache-policy.ts +11 -2
- package/src/cache/cache-runtime.ts +23 -23
- package/src/cache/cache-scope.ts +48 -36
- package/src/cache/cf/cf-cache-store.ts +91 -66
- package/src/cache/cf/cf-cache-types.ts +12 -4
- package/src/cache/document-cache.ts +35 -14
- package/src/cache/handle-snapshot.ts +118 -86
- package/src/cache/index.ts +5 -0
- package/src/cache/invalidation-order.ts +53 -0
- package/src/cache/memory-segment-store.ts +52 -1
- package/src/cache/shell-snapshot.ts +52 -5
- package/src/cache/store-scope.ts +36 -0
- package/src/cache/tag-invalidation.ts +7 -5
- package/src/cache/types.ts +90 -24
- package/src/cache/vercel/vercel-cache-store.ts +68 -26
- package/src/client-urls/client-root.tsx +1 -0
- package/src/client-urls/navigation.ts +10 -0
- package/src/client-urls/optimistic-location.ts +10 -4
- package/src/client.tsx +40 -3
- package/src/errors.ts +13 -6
- package/src/handles/deferred-resolution.ts +36 -0
- package/src/internal-suspense-audit.ts +12 -0
- package/src/loader-store.ts +4 -9
- package/src/outlet-provider.tsx +25 -1
- package/src/prerender/build-shell-capture.ts +11 -9
- package/src/prerender/cloudflare.ts +130 -0
- package/src/prerender/create-prerender-trigger.ts +780 -0
- package/src/prerender/index.ts +39 -0
- package/src/prerender/memory-prerender-store.ts +115 -0
- package/src/prerender/on-demand.ts +339 -0
- package/src/prerender/producer-guard.ts +66 -0
- package/src/prerender/shell-manifest-key.ts +12 -17
- package/src/prerender/warm-request.ts +154 -0
- package/src/prerender/warm.ts +193 -0
- package/src/prerender/writable-store.ts +278 -0
- package/src/prerender.ts +29 -0
- package/src/reverse.ts +1 -1
- package/src/route-content-wrapper.tsx +107 -13
- package/src/router/error-handling.ts +12 -12
- package/src/router/find-match.ts +1 -0
- package/src/router/handler-context.ts +2 -0
- package/src/router/intercept-resolution.ts +15 -8
- package/src/router/loader-resolution.ts +2 -10
- package/src/router/manifest.ts +2 -0
- package/src/router/match-middleware/background-revalidation.ts +2 -0
- package/src/router/match-middleware/cache-lookup.ts +239 -54
- package/src/router/navigation-snapshot.ts +25 -6
- package/src/router/pattern-matching.ts +14 -0
- package/src/router/prefetch-default.ts +4 -4
- package/src/router/prerender-match.ts +460 -352
- package/src/router/request-classification.ts +15 -12
- package/src/router/route-trie-builder.ts +9 -0
- package/src/router/router-context.ts +1 -0
- package/src/router/router-interfaces.ts +35 -1
- package/src/router/router-options.ts +45 -5
- package/src/router/segment-resolution/fresh.ts +20 -19
- package/src/router/segment-resolution/loader-cache.ts +110 -77
- package/src/router/segment-resolution/revalidation.ts +34 -24
- package/src/router/segment-resolution/streamed-handler-boundary.ts +133 -0
- package/src/router/trie-matching.ts +3 -0
- package/src/router-versions.ts +60 -0
- package/src/router.ts +107 -1
- package/src/rsc/full-payload.ts +6 -2
- package/src/rsc/handler-context.ts +5 -0
- package/src/rsc/handler.ts +64 -7
- package/src/rsc/progressive-enhancement.ts +5 -1
- package/src/rsc/response-cache-serve.ts +10 -4
- package/src/rsc/rsc-rendering.ts +65 -12
- package/src/rsc/shell-build-manifest.ts +17 -12
- package/src/rsc/shell-capture-constants.ts +39 -7
- package/src/rsc/shell-capture.ts +144 -45
- package/src/rsc/shell-serve.ts +12 -6
- package/src/rsc/ssr-module-loader.ts +26 -0
- package/src/rsc/types.ts +12 -6
- package/src/segment-boundary-content.ts +25 -0
- package/src/segment-loader-promise.ts +28 -38
- package/src/segment-system.tsx +117 -57
- package/src/server/build-version-table.ts +130 -0
- package/src/server/context.ts +40 -17
- package/src/server/cookie-store.ts +5 -0
- package/src/server/handle-store.ts +357 -127
- package/src/server/request-context.ts +149 -12
- package/src/ssr/ssr-root.tsx +32 -13
- package/src/suspense-audit-react.tsx +98 -0
- package/src/suspense-audit.ts +1030 -0
- package/src/testing/action-form.ts +66 -0
- package/src/testing/build-versions.ts +64 -0
- package/src/testing/dispatch.ts +6 -0
- package/src/testing/dom.entry.ts +1 -0
- package/src/testing/flight-runtime.d.ts +27 -0
- package/src/testing/flight.entry.ts +4 -0
- package/src/testing/index.ts +22 -0
- package/src/testing/internal/context.ts +2 -1
- package/src/testing/internal/seed-vars.ts +14 -2
- package/src/testing/internal/server-modules.ts +32 -0
- package/src/testing/location-state-key.ts +18 -8
- package/src/testing/render-handler.ts +2 -1
- package/src/testing/render-route.tsx +293 -46
- package/src/testing/serve-shell-request.ts +262 -37
- package/src/testing/shell-status.ts +17 -6
- package/src/testing/vitest-stubs/plugin-rsc.ts +37 -11
- package/src/testing/vitest-stubs/version.ts +4 -1
- package/src/thenable-status.ts +10 -0
- package/src/types/handler-context.ts +15 -5
- package/src/types/route-entry.ts +6 -0
- package/src/urls/path-helper.ts +69 -9
- package/src/use-loader.tsx +57 -30
- package/src/vite/discovery/build-versions.ts +1281 -0
- package/src/vite/discovery/bundle-postprocess.ts +4 -0
- package/src/vite/discovery/discover-routers.ts +15 -0
- package/src/vite/discovery/installed-packages.ts +159 -0
- package/src/vite/discovery/prerender-collection.ts +38 -1
- package/src/vite/discovery/router-versions-phase.ts +336 -0
- package/src/vite/discovery/shell-prerender-phase.ts +33 -43
- package/src/vite/discovery/state.ts +36 -1
- package/src/vite/discovery/virtual-module-codegen.ts +7 -2
- package/src/vite/encryption-key.ts +101 -9
- package/src/vite/inject-client-debug.ts +42 -20
- package/src/vite/plugin-types.ts +28 -0
- package/src/vite/plugins/{version-injector.ts → entry-bootstrap-injector.ts} +7 -22
- package/src/vite/plugins/expose-ids/export-analysis.ts +1 -1
- package/src/vite/plugins/expose-ids/router-transform.ts +36 -15
- package/src/vite/plugins/expose-internal-ids.ts +12 -3
- package/src/vite/plugins/stable-rsc-output.ts +147 -0
- package/src/vite/plugins/version-plugin.ts +19 -24
- package/src/vite/plugins/version.d.ts +17 -2
- package/src/vite/plugins/virtual-entries.ts +39 -6
- package/src/vite/rango.ts +28 -3
- package/src/vite/router-discovery.ts +96 -13
- package/src/vite/utils/bundle-analysis.ts +39 -7
- package/src/vite/utils/client-chunks.ts +5 -1
- package/src/vite/utils/compare-strings.ts +9 -0
- package/src/vite/utils/package-resolution.ts +9 -0
- package/src/vite/utils/prerender-utils.ts +4 -1
- package/src/vite/utils/shared-utils.ts +1 -1
- package/dist/types/segment-content-promise.d.ts +0 -13
- package/src/segment-content-promise.ts +0 -67
|
@@ -0,0 +1,1030 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev-only audit of what the router hands its Suspense boundaries and of the
|
|
3
|
+
* tree those boundaries sit in (docs/internal/suspense-contract.md). Every
|
|
4
|
+
* call site is behind `process.env.NODE_ENV !== "production"`, so a build
|
|
5
|
+
* carries none of this module (tools/check-bundle-guards.mjs), and the audit
|
|
6
|
+
* runs only where INTERNAL_RANGO_SUSPENSE_AUDIT is set: this repo's e2e apps
|
|
7
|
+
* and the router's own tests, never a consumer's dev server.
|
|
8
|
+
*
|
|
9
|
+
* React warns about none of this: its uncached-promise warning covers a
|
|
10
|
+
* promise created during render (a different thenable on a replay of one
|
|
11
|
+
* render attempt), never a boundary handed a different promise by a later
|
|
12
|
+
* render.
|
|
13
|
+
*
|
|
14
|
+
* The audit never attaches a reaction to a promise it is handed: that marks a
|
|
15
|
+
* rejected promise handled, and dev would stop raising the unhandled
|
|
16
|
+
* rejection a build raises. It reads the `status` React's use() and the
|
|
17
|
+
* Flight client leave on a thenable instead.
|
|
18
|
+
*
|
|
19
|
+
* No React import: segment-system.tsx is evaluated outside the browser too.
|
|
20
|
+
* The hooks are in suspense-audit-react.tsx.
|
|
21
|
+
*
|
|
22
|
+
* Imports nothing a product module does not already import first: a new
|
|
23
|
+
* static import here, or in suspense-audit-react.tsx, moves that module
|
|
24
|
+
* earlier in the client router chunk (route-content-wrapper.tsx imports both)
|
|
25
|
+
* and a build stops matching main byte for byte, although every audit call
|
|
26
|
+
* is folded away. Measured in cloudflare-basic for handles/is-thenable.ts and
|
|
27
|
+
* browser/react/context.ts.
|
|
28
|
+
*/
|
|
29
|
+
import { INTERNAL_RANGO_DEBUG } from "./internal-debug.js";
|
|
30
|
+
import { INTERNAL_RANGO_SUSPENSE_AUDIT } from "./internal-suspense-audit.js";
|
|
31
|
+
|
|
32
|
+
export type SuspenseAuditKind =
|
|
33
|
+
| "swap"
|
|
34
|
+
| "untracked"
|
|
35
|
+
| "idle-fallback"
|
|
36
|
+
| "resuspended"
|
|
37
|
+
| "remount"
|
|
38
|
+
| "drift"
|
|
39
|
+
| "uncaused"
|
|
40
|
+
| "mutated";
|
|
41
|
+
|
|
42
|
+
/** What may hand React a tree (I6). */
|
|
43
|
+
export type TreeUpdateCause =
|
|
44
|
+
| "navigation"
|
|
45
|
+
| "popstate"
|
|
46
|
+
| "stale-revalidation"
|
|
47
|
+
| "action"
|
|
48
|
+
| "error"
|
|
49
|
+
| "hmr";
|
|
50
|
+
|
|
51
|
+
export interface SuspenseAuditEvent {
|
|
52
|
+
/** I1 swap, I2 untracked, I3 idle-fallback and resuspended, I4 remount, I5 drift, I6 uncaused, I7 mutated. */
|
|
53
|
+
kind: SuspenseAuditKind;
|
|
54
|
+
/** `content:<segment id>`, `loaders:<outlet key>`, `outlet:<segment id>`, `read:<loader id>`, `segment:<segment id>`. */
|
|
55
|
+
boundary: string;
|
|
56
|
+
detail: string;
|
|
57
|
+
url: string;
|
|
58
|
+
at: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface SuspenseAuditReport {
|
|
62
|
+
swaps: number;
|
|
63
|
+
untracked: number;
|
|
64
|
+
idleFallbacks: number;
|
|
65
|
+
resuspended: number;
|
|
66
|
+
remounts: number;
|
|
67
|
+
drifts: number;
|
|
68
|
+
uncaused: number;
|
|
69
|
+
mutations: number;
|
|
70
|
+
/**
|
|
71
|
+
* Not a violation: fallbacks the router cannot attribute. Nothing it handed
|
|
72
|
+
* the boundary was a promise React read, so the content's own suspension
|
|
73
|
+
* put the fallback up: an app's use(), a React.lazy, a client reference.
|
|
74
|
+
*/
|
|
75
|
+
unattributedFallbacks: number;
|
|
76
|
+
/** Not a violation: tree updates React was handed, by cause ("none" without one). */
|
|
77
|
+
treeUpdates: Record<string, number>;
|
|
78
|
+
/**
|
|
79
|
+
* Not a violation: the distinct thenables each boundary or read was handed.
|
|
80
|
+
* One per value it waited for; two for one navigation is a gate replaced
|
|
81
|
+
* by the real promise.
|
|
82
|
+
*/
|
|
83
|
+
readonly handed: Record<string, number>;
|
|
84
|
+
/**
|
|
85
|
+
* Not a violation: fallbacks shown over content on screen while what the
|
|
86
|
+
* boundary waits for was pending. An urgent commit does that by design.
|
|
87
|
+
*/
|
|
88
|
+
shownWhilePending: number;
|
|
89
|
+
/** Mounts per boundary since the document loaded. */
|
|
90
|
+
readonly mounts: Record<string, number>;
|
|
91
|
+
/** The last events, oldest first. */
|
|
92
|
+
events: SuspenseAuditEvent[];
|
|
93
|
+
reset(): void;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** One boundary, or one `useLoader` read. Lives as long as its fiber. */
|
|
97
|
+
export interface BoundaryAudit {
|
|
98
|
+
boundary: string;
|
|
99
|
+
/** Segment id, for the place in the tree. */
|
|
100
|
+
segmentId: string;
|
|
101
|
+
/** The React key the segment was mounted under (I4 identity). */
|
|
102
|
+
key: string;
|
|
103
|
+
/** 0 until the boundary is on screen. */
|
|
104
|
+
mountId: number;
|
|
105
|
+
/** Last value a render ran with, and the URL it ran under. */
|
|
106
|
+
seen: unknown;
|
|
107
|
+
hasSeen: boolean;
|
|
108
|
+
seenUrl: string;
|
|
109
|
+
/** Last value a commit carried. */
|
|
110
|
+
committed: unknown;
|
|
111
|
+
hasCommitted: boolean;
|
|
112
|
+
/** What else the boundary's content waits for: per-loader streams, nodes. */
|
|
113
|
+
streams: Record<string, unknown> | undefined;
|
|
114
|
+
nodes: unknown;
|
|
115
|
+
/** The content has been on screen. */
|
|
116
|
+
revealed: boolean;
|
|
117
|
+
/** The fallback's effect was cleaned up in this task (auditFallback). */
|
|
118
|
+
fallbackGone: boolean;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const PREFIX = "[rango][suspense]";
|
|
122
|
+
// What a failing test prints; older events repeat what the counters say.
|
|
123
|
+
const MAX_EVENTS = 50;
|
|
124
|
+
// Boundary names carry params: a long dev session must not grow without bound.
|
|
125
|
+
const MAX_BOUNDARIES = 500;
|
|
126
|
+
// One fallback check's client-reference scan; types are visited first.
|
|
127
|
+
const MAX_SCAN = 400;
|
|
128
|
+
|
|
129
|
+
type Tracked = PromiseLike<unknown> & { status?: string; value?: unknown };
|
|
130
|
+
|
|
131
|
+
// handles/is-thenable.ts, inlined (see the header).
|
|
132
|
+
function isThenable(value: unknown): value is PromiseLike<unknown> {
|
|
133
|
+
return (
|
|
134
|
+
value !== null &&
|
|
135
|
+
typeof value === "object" &&
|
|
136
|
+
typeof (value as { then?: unknown }).then === "function"
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// A Flight chunk extends Promise.prototype with its own `then` and always
|
|
141
|
+
// carries a status.
|
|
142
|
+
function isNative(value: PromiseLike<unknown>): boolean {
|
|
143
|
+
return value.then === Promise.prototype.then;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Set by React's use() on a thenable it has read, and by the Flight client
|
|
147
|
+
// on every chunk. Undefined on a native promise React has not read.
|
|
148
|
+
function statusOf(value: unknown): string | undefined {
|
|
149
|
+
return isThenable(value) ? (value as Tracked).status : undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function isPending(value: unknown): boolean {
|
|
153
|
+
const status = statusOf(value);
|
|
154
|
+
return status === "pending" || status === "blocked" || status === "halted";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// What a settled thenable holds, where the audit can know it.
|
|
158
|
+
function settledValue(value: unknown): unknown {
|
|
159
|
+
if (!isThenable(value)) return value;
|
|
160
|
+
const tracked = value as Tracked;
|
|
161
|
+
return tracked.status === "fulfilled" ? tracked.value : undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const LAZY = Symbol.for("react.lazy");
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* A node that names a lazy still loading: a client reference (Flight's
|
|
168
|
+
* `_payload.status`) or a React.lazy (its numeric `_payload._status`, -1
|
|
169
|
+
* uninitialized and 0 pending). In dev @vitejs/plugin-rsc tags client
|
|
170
|
+
* reference ids per render, so the first render of every fresh payload meets
|
|
171
|
+
* them blocked for a few milliseconds and the boundary shows its fallback.
|
|
172
|
+
* That is the bundler's doing, not a value the router handed over.
|
|
173
|
+
*/
|
|
174
|
+
function hasBlockedReference(root: unknown): boolean {
|
|
175
|
+
const stack: unknown[] = [root];
|
|
176
|
+
let visited = 0;
|
|
177
|
+
while (stack.length > 0 && visited++ < MAX_SCAN) {
|
|
178
|
+
const node = stack.pop();
|
|
179
|
+
if (node === null || typeof node !== "object") continue;
|
|
180
|
+
if (Array.isArray(node)) {
|
|
181
|
+
for (let i = node.length - 1; i >= 0; i--) stack.push(node[i]);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const lazy = node as {
|
|
185
|
+
$$typeof?: unknown;
|
|
186
|
+
_payload?: { status?: string; _status?: number };
|
|
187
|
+
};
|
|
188
|
+
if (lazy.$$typeof === LAZY) {
|
|
189
|
+
const payload = lazy._payload;
|
|
190
|
+
if (payload?.status === "pending" || payload?.status === "blocked") {
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
if (payload?._status === -1 || payload?._status === 0) return true;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
const element = node as { type?: unknown; props?: Record<string, unknown> };
|
|
197
|
+
if (element.props === null || typeof element.props !== "object") continue;
|
|
198
|
+
for (const name in element.props) stack.push(element.props[name]);
|
|
199
|
+
// Popped first: a large list prop must not use the scan up before it.
|
|
200
|
+
stack.push(element.type);
|
|
201
|
+
}
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function describe(value: unknown): string {
|
|
206
|
+
if (isThenable(value)) {
|
|
207
|
+
return `${isNative(value) ? "promise" : "Flight chunk"} (${statusOf(value) ?? "unread"})`;
|
|
208
|
+
}
|
|
209
|
+
if (Array.isArray(value)) return `array(${value.length})`;
|
|
210
|
+
if (value === null || value === undefined) return String(value);
|
|
211
|
+
return typeof value === "object" ? "node" : typeof value;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function href(): string {
|
|
215
|
+
return typeof location === "undefined"
|
|
216
|
+
? ""
|
|
217
|
+
: location.pathname + location.search;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// The newest MAX_BOUNDARIES keys, oldest dropped first.
|
|
221
|
+
function setBounded<V>(map: Map<string, V>, key: string, value: V): void {
|
|
222
|
+
map.delete(key);
|
|
223
|
+
map.set(key, value);
|
|
224
|
+
if (map.size > MAX_BOUNDARIES) map.delete(map.keys().next().value!);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const handedCounts = new Map<string, number>();
|
|
228
|
+
const mountCounts = new Map<string, number>();
|
|
229
|
+
// By boundary, not by record: a reader that suspends while mounting keeps
|
|
230
|
+
// no record between its attempts.
|
|
231
|
+
const handedSeen = new Map<string, WeakSet<object>>();
|
|
232
|
+
|
|
233
|
+
// Thenables the router handed unread that React found already settled when
|
|
234
|
+
// it read them: it suspended on a value that was there. The one cause of a
|
|
235
|
+
// fallback the router owns (I3); consumed by the fallback it caused.
|
|
236
|
+
const needless = new WeakSet<object>();
|
|
237
|
+
|
|
238
|
+
let report: SuspenseAuditReport | undefined;
|
|
239
|
+
|
|
240
|
+
function getReport(): SuspenseAuditReport {
|
|
241
|
+
if (report) return report;
|
|
242
|
+
const created: SuspenseAuditReport = {
|
|
243
|
+
swaps: 0,
|
|
244
|
+
untracked: 0,
|
|
245
|
+
idleFallbacks: 0,
|
|
246
|
+
resuspended: 0,
|
|
247
|
+
remounts: 0,
|
|
248
|
+
drifts: 0,
|
|
249
|
+
uncaused: 0,
|
|
250
|
+
mutations: 0,
|
|
251
|
+
unattributedFallbacks: 0,
|
|
252
|
+
treeUpdates: {},
|
|
253
|
+
get handed() {
|
|
254
|
+
return Object.fromEntries(handedCounts);
|
|
255
|
+
},
|
|
256
|
+
shownWhilePending: 0,
|
|
257
|
+
get mounts() {
|
|
258
|
+
return Object.fromEntries(mountCounts);
|
|
259
|
+
},
|
|
260
|
+
events: [],
|
|
261
|
+
reset() {
|
|
262
|
+
created.swaps = 0;
|
|
263
|
+
created.untracked = 0;
|
|
264
|
+
created.idleFallbacks = 0;
|
|
265
|
+
created.resuspended = 0;
|
|
266
|
+
created.remounts = 0;
|
|
267
|
+
created.drifts = 0;
|
|
268
|
+
created.uncaused = 0;
|
|
269
|
+
created.mutations = 0;
|
|
270
|
+
created.unattributedFallbacks = 0;
|
|
271
|
+
created.treeUpdates = {};
|
|
272
|
+
handedCounts.clear();
|
|
273
|
+
handedSeen.clear();
|
|
274
|
+
created.shownWhilePending = 0;
|
|
275
|
+
created.events = [];
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
report = created;
|
|
279
|
+
if (typeof window !== "undefined") {
|
|
280
|
+
(
|
|
281
|
+
window as unknown as { __rangoSuspenseAudit?: SuspenseAuditReport }
|
|
282
|
+
).__rangoSuspenseAudit = created;
|
|
283
|
+
}
|
|
284
|
+
return created;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
type Counter =
|
|
288
|
+
| "swaps"
|
|
289
|
+
| "untracked"
|
|
290
|
+
| "idleFallbacks"
|
|
291
|
+
| "resuspended"
|
|
292
|
+
| "remounts"
|
|
293
|
+
| "drifts"
|
|
294
|
+
| "uncaused"
|
|
295
|
+
| "mutations";
|
|
296
|
+
|
|
297
|
+
const RULES: Record<SuspenseAuditKind, [invariant: string, counter: Counter]> =
|
|
298
|
+
{
|
|
299
|
+
swap: ["I1", "swaps"],
|
|
300
|
+
untracked: ["I2", "untracked"],
|
|
301
|
+
"idle-fallback": ["I3", "idleFallbacks"],
|
|
302
|
+
resuspended: ["I3", "resuspended"],
|
|
303
|
+
remount: ["I4", "remounts"],
|
|
304
|
+
drift: ["I5", "drifts"],
|
|
305
|
+
uncaused: ["I6", "uncaused"],
|
|
306
|
+
mutated: ["I7", "mutations"],
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
function violate(
|
|
310
|
+
kind: SuspenseAuditKind,
|
|
311
|
+
boundary: string,
|
|
312
|
+
detail: string,
|
|
313
|
+
): void {
|
|
314
|
+
const [invariant, counter] = RULES[kind];
|
|
315
|
+
const r = getReport();
|
|
316
|
+
r[counter] += 1;
|
|
317
|
+
r.events.push({ kind, boundary, detail, url: href(), at: performance.now() });
|
|
318
|
+
if (r.events.length > MAX_EVENTS) r.events.shift();
|
|
319
|
+
console.error(`${PREFIX} ${invariant} ${kind} at ${boundary}: ${detail}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Every hand-over, mount and fallback, for a debug run to read back. Call
|
|
323
|
+
// sites test INTERNAL_RANGO_DEBUG first, so the data is not built otherwise.
|
|
324
|
+
function trace(type: string, data: Record<string, unknown>): void {
|
|
325
|
+
console.log(
|
|
326
|
+
`${PREFIX}[trace] ${JSON.stringify({ type, ...data, url: href(), at: Math.round(performance.now()) })}`,
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// One microtask for every job the audit defers in a task.
|
|
331
|
+
const jobs: Array<() => void> = [];
|
|
332
|
+
|
|
333
|
+
function later(job: () => void): void {
|
|
334
|
+
jobs.push(job);
|
|
335
|
+
if (jobs.length === 1) queueMicrotask(runJobs);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function runJobs(): void {
|
|
339
|
+
for (const job of jobs.splice(0)) job();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// After React's own reaction to a thenable it read in this task: the first
|
|
343
|
+
// job runs before it, the second after.
|
|
344
|
+
function afterMicrotasks(run: () => void): void {
|
|
345
|
+
later(() => later(run));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function createBoundaryAudit(
|
|
349
|
+
boundary: string,
|
|
350
|
+
segmentId: string,
|
|
351
|
+
): BoundaryAudit {
|
|
352
|
+
return {
|
|
353
|
+
boundary,
|
|
354
|
+
segmentId,
|
|
355
|
+
key: segmentId,
|
|
356
|
+
mountId: 0,
|
|
357
|
+
seen: undefined,
|
|
358
|
+
hasSeen: false,
|
|
359
|
+
seenUrl: "",
|
|
360
|
+
committed: undefined,
|
|
361
|
+
hasCommitted: false,
|
|
362
|
+
streams: undefined,
|
|
363
|
+
nodes: undefined,
|
|
364
|
+
revealed: false,
|
|
365
|
+
fallbackGone: false,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function countHanded(boundary: string, value: unknown): void {
|
|
370
|
+
if (!isThenable(value)) return;
|
|
371
|
+
let seen = handedSeen.get(boundary);
|
|
372
|
+
if (!seen) setBounded(handedSeen, boundary, (seen = new WeakSet()));
|
|
373
|
+
if (seen.has(value)) return;
|
|
374
|
+
seen.add(value);
|
|
375
|
+
getReport();
|
|
376
|
+
setBounded(handedCounts, boundary, (handedCounts.get(boundary) ?? 0) + 1);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Render phase: the value this render hands the boundary.
|
|
381
|
+
*
|
|
382
|
+
* I1: a boundary on screen was handed a thenable that is still pending, and
|
|
383
|
+
* is handed another one that is not already settled, for the same URL. React
|
|
384
|
+
* starts waiting again. A settled replacement is read at once, and another
|
|
385
|
+
* URL is another navigation: both are exempt.
|
|
386
|
+
* I2: a boundary whose content has been on screen is handed a native promise
|
|
387
|
+
* React has not read, and React finds it settled when it reads it. A render
|
|
388
|
+
* that cannot wait suspends on it and commits the fallback over the content,
|
|
389
|
+
* which React keeps up for 300 ms. Judged from the status React leaves on
|
|
390
|
+
* the promise when it reads it in the same task.
|
|
391
|
+
*/
|
|
392
|
+
export function auditHandover(rec: BoundaryAudit, value: unknown): void {
|
|
393
|
+
if (typeof window === "undefined") return;
|
|
394
|
+
countHanded(rec.boundary, value);
|
|
395
|
+
// A replay, or a re-render of the parent, hands the same object.
|
|
396
|
+
if (rec.hasSeen && rec.seen === value) return;
|
|
397
|
+
const previous = rec.seen;
|
|
398
|
+
const hadPrevious = rec.hasSeen;
|
|
399
|
+
const url = href();
|
|
400
|
+
const sameUrl = rec.seenUrl === url;
|
|
401
|
+
rec.seen = value;
|
|
402
|
+
rec.hasSeen = true;
|
|
403
|
+
rec.seenUrl = url;
|
|
404
|
+
const unread =
|
|
405
|
+
isThenable(value) && isNative(value) && statusOf(value) === undefined;
|
|
406
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
407
|
+
trace("handover", {
|
|
408
|
+
boundary: rec.boundary,
|
|
409
|
+
mountId: rec.mountId,
|
|
410
|
+
revealed: rec.revealed,
|
|
411
|
+
handed: describe(value),
|
|
412
|
+
previous: hadPrevious ? describe(previous) : "none",
|
|
413
|
+
previousPending: hadPrevious && isPending(previous),
|
|
414
|
+
sameUrl,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
if (
|
|
418
|
+
rec.mountId > 0 &&
|
|
419
|
+
hadPrevious &&
|
|
420
|
+
sameUrl &&
|
|
421
|
+
isPending(previous) &&
|
|
422
|
+
isThenable(value)
|
|
423
|
+
) {
|
|
424
|
+
const swapped = (): void => {
|
|
425
|
+
// Settled by now: React reads it without waiting again.
|
|
426
|
+
if (!isPending(value) || !isPending(previous)) return;
|
|
427
|
+
violate(
|
|
428
|
+
"swap",
|
|
429
|
+
rec.boundary,
|
|
430
|
+
`handed ${describe(value)} while ${describe(previous)} was still pending`,
|
|
431
|
+
);
|
|
432
|
+
};
|
|
433
|
+
if (unread) afterMicrotasks(swapped);
|
|
434
|
+
else swapped();
|
|
435
|
+
}
|
|
436
|
+
if (unread) {
|
|
437
|
+
const revealed = rec.revealed;
|
|
438
|
+
afterMicrotasks(() => {
|
|
439
|
+
if (statusOf(value) !== "fulfilled") return;
|
|
440
|
+
needless.add(value);
|
|
441
|
+
if (!revealed) return;
|
|
442
|
+
violate(
|
|
443
|
+
"untracked",
|
|
444
|
+
rec.boundary,
|
|
445
|
+
"content on screen was handed a settled promise React has not read; hand the value itself",
|
|
446
|
+
);
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** Commit phase: what the boundary on screen now reads. */
|
|
452
|
+
export function auditCommit(
|
|
453
|
+
rec: BoundaryAudit,
|
|
454
|
+
value: unknown,
|
|
455
|
+
streams?: Record<string, unknown>,
|
|
456
|
+
nodes?: unknown,
|
|
457
|
+
): void {
|
|
458
|
+
rec.committed = value;
|
|
459
|
+
rec.hasCommitted = true;
|
|
460
|
+
rec.streams = streams;
|
|
461
|
+
rec.nodes = nodes;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export function auditReveal(rec: BoundaryAudit): void {
|
|
465
|
+
if (!rec.revealed && INTERNAL_RANGO_DEBUG) {
|
|
466
|
+
trace("reveal", { boundary: rec.boundary, mountId: rec.mountId });
|
|
467
|
+
}
|
|
468
|
+
rec.revealed = true;
|
|
469
|
+
// On screen: whatever React suspended on is read, and owns no later fallback.
|
|
470
|
+
for (const value of handedValues(rec)) {
|
|
471
|
+
if (isThenable(value)) needless.delete(value);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// What a boundary waits for: what it was handed, and its loader streams.
|
|
476
|
+
function handedValues(rec: BoundaryAudit): unknown[] {
|
|
477
|
+
const handed = rec.hasCommitted ? rec.committed : rec.seen;
|
|
478
|
+
return rec.streams ? [handed, ...Object.values(rec.streams)] : [handed];
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** One `useLoader` read on screen: it has a mount and reveals with it. */
|
|
482
|
+
export function auditRead(rec: BoundaryAudit, stream: unknown): void {
|
|
483
|
+
rec.mountId = 1;
|
|
484
|
+
auditCommit(rec, stream);
|
|
485
|
+
auditReveal(rec);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
let streamProbe: (() => boolean) | undefined;
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* How the audit learns that a payload is still streaming: the event
|
|
492
|
+
* controller, set at boot (browser/rsc-router.tsx). Not read from
|
|
493
|
+
* NavigationStoreContext in AuditedFallback: see the header.
|
|
494
|
+
*/
|
|
495
|
+
export function setSuspenseAuditStreamProbe(probe: () => boolean): void {
|
|
496
|
+
streamProbe = probe;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function streamOpen(): boolean {
|
|
500
|
+
if (typeof document !== "undefined" && document.readyState === "loading") {
|
|
501
|
+
return true;
|
|
502
|
+
}
|
|
503
|
+
return streamProbe?.() === true;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* I3: the boundary's fallback is on screen with nothing to wait for: nothing
|
|
508
|
+
* the router handed the boundary is pending, no payload is streaming and no
|
|
509
|
+
* client reference is loading. `idle-fallback` on a boundary new to the page,
|
|
510
|
+
* `resuspended` when the fallback replaced content that had been on screen.
|
|
511
|
+
*
|
|
512
|
+
* Reported only when the router owns the fallback: React suspended on a
|
|
513
|
+
* promise the router handed unread that was already settled (`needless`).
|
|
514
|
+
* Any other fallback is the content's own (an app's use(), a React.lazy, a
|
|
515
|
+
* client reference) and is counted as `unattributedFallbacks`, not reported,
|
|
516
|
+
* also when the boundary holds a promise React has read.
|
|
517
|
+
*
|
|
518
|
+
* A fallback over content on screen while something is pending is counted
|
|
519
|
+
* (`shownWhilePending`) and not reported: an urgent commit does that by
|
|
520
|
+
* design (transition({ when }) gated off, #995), and so does dev while a
|
|
521
|
+
* client reference loads.
|
|
522
|
+
*/
|
|
523
|
+
export function auditFallback(rec: BoundaryAudit): () => void {
|
|
524
|
+
const shownAt = performance.now();
|
|
525
|
+
const revealed = rec.revealed;
|
|
526
|
+
// StrictMode runs this effect, its cleanup and the effect again in one
|
|
527
|
+
// task, some time after the commit: the second run is the same fallback,
|
|
528
|
+
// and what it waited for may have settled since.
|
|
529
|
+
if (!rec.fallbackGone) {
|
|
530
|
+
afterMicrotasks(() => {
|
|
531
|
+
const handed = rec.hasCommitted ? rec.committed : rec.seen;
|
|
532
|
+
const values = handedValues(rec);
|
|
533
|
+
const waiting = values.some(isPending);
|
|
534
|
+
const streaming = !waiting && streamOpen();
|
|
535
|
+
let owned = false;
|
|
536
|
+
for (const value of values) {
|
|
537
|
+
if (isThenable(value) && needless.delete(value)) owned = true;
|
|
538
|
+
}
|
|
539
|
+
const loading =
|
|
540
|
+
!waiting &&
|
|
541
|
+
!streaming &&
|
|
542
|
+
owned &&
|
|
543
|
+
(hasBlockedReference(settledValue(handed)) ||
|
|
544
|
+
hasBlockedReference(rec.nodes));
|
|
545
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
546
|
+
trace("fallback", {
|
|
547
|
+
boundary: rec.boundary,
|
|
548
|
+
mountId: rec.mountId,
|
|
549
|
+
revealed,
|
|
550
|
+
waiting,
|
|
551
|
+
streaming,
|
|
552
|
+
owned,
|
|
553
|
+
loading,
|
|
554
|
+
handed: describe(handed),
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
const r = getReport();
|
|
558
|
+
if (waiting || streaming || loading) {
|
|
559
|
+
if (revealed) r.shownWhilePending += 1;
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
if (!owned) {
|
|
563
|
+
r.unattributedFallbacks += 1;
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (revealed) {
|
|
567
|
+
violate(
|
|
568
|
+
"resuspended",
|
|
569
|
+
rec.boundary,
|
|
570
|
+
"the fallback replaced content on screen with nothing pending",
|
|
571
|
+
);
|
|
572
|
+
} else {
|
|
573
|
+
violate(
|
|
574
|
+
"idle-fallback",
|
|
575
|
+
rec.boundary,
|
|
576
|
+
"the fallback mounted with nothing pending",
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
return () => {
|
|
582
|
+
rec.fallbackGone = true;
|
|
583
|
+
later(() => {
|
|
584
|
+
rec.fallbackGone = false;
|
|
585
|
+
});
|
|
586
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
587
|
+
trace("fallback-gone", {
|
|
588
|
+
boundary: rec.boundary,
|
|
589
|
+
mountId: rec.mountId,
|
|
590
|
+
ms: Math.round(performance.now() - shownAt),
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/** One renderSegments call, collected while it builds. */
|
|
597
|
+
export interface TreeAudit {
|
|
598
|
+
/** Segment ids, leaf first while building, root first once finished. */
|
|
599
|
+
order: string[];
|
|
600
|
+
keys: Map<string, string>;
|
|
601
|
+
types: Map<string, string>;
|
|
602
|
+
chains: Map<string, string[]>;
|
|
603
|
+
/** Segments this tree remounts by the documented rules. */
|
|
604
|
+
replaced: Set<string>;
|
|
605
|
+
/** The segment objects the tree is built from (I7), until it is held. */
|
|
606
|
+
segments: object[];
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/** The tree React holds: what I4, I5 and I7 compare with. */
|
|
610
|
+
let held: TreeAudit | undefined;
|
|
611
|
+
// Every finished tree by the root renderSegments returned, until an emit
|
|
612
|
+
// hands that root to React. A tree that is never emitted (an aborted
|
|
613
|
+
// navigation's) is never held.
|
|
614
|
+
const builtTrees = new WeakMap<object, TreeAudit>();
|
|
615
|
+
// A root emitted as a promise (HMR): held once React's use() has read it.
|
|
616
|
+
let pendingRoot: PromiseLike<unknown> | undefined;
|
|
617
|
+
// I7: the fields of each segment object handed to React, at hand-over, and
|
|
618
|
+
// the segments themselves, weakly: the history cache keeps a tree's segments
|
|
619
|
+
// after the next tree drops them, and a write there is still a write.
|
|
620
|
+
const handedFields = new WeakMap<object, Record<string, unknown>>();
|
|
621
|
+
const handedSegments = new Set<WeakRef<object>>();
|
|
622
|
+
const trackedSegments = new WeakSet<object>();
|
|
623
|
+
|
|
624
|
+
function heldTree(): TreeAudit | undefined {
|
|
625
|
+
if (pendingRoot) {
|
|
626
|
+
const status = statusOf(pendingRoot);
|
|
627
|
+
if (status === "fulfilled") {
|
|
628
|
+
const root = (pendingRoot as Tracked).value;
|
|
629
|
+
pendingRoot = undefined;
|
|
630
|
+
const tree =
|
|
631
|
+
root !== null && typeof root === "object"
|
|
632
|
+
? builtTrees.get(root)
|
|
633
|
+
: undefined;
|
|
634
|
+
if (tree) hold(tree);
|
|
635
|
+
} else if (status === "rejected") {
|
|
636
|
+
pendingRoot = undefined;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return held;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// The router's wrapper components, by function name. A boundary component
|
|
643
|
+
// and its dev variant (route-content-wrapper.tsx, outlet-provider.tsx) are
|
|
644
|
+
// one link, named after the product component.
|
|
645
|
+
const WRAPPERS = new Map([
|
|
646
|
+
["MountContextProvider", "MountContextProvider"],
|
|
647
|
+
["LoaderBoundary", "LoaderBoundary"],
|
|
648
|
+
["AuditedLoaderBoundary", "LoaderBoundary"],
|
|
649
|
+
["OutletProvider", "OutletProvider"],
|
|
650
|
+
["AuditedOutletProvider", "OutletProvider"],
|
|
651
|
+
["StreamedLoaderErrorBoundary", "StreamedLoaderErrorBoundary"],
|
|
652
|
+
["RouteContentWrapper", "RouteContentWrapper"],
|
|
653
|
+
["AuditedRouteContent", "RouteContentWrapper"],
|
|
654
|
+
]);
|
|
655
|
+
|
|
656
|
+
function wrapperName(type: unknown): string | undefined {
|
|
657
|
+
if (typeof type === "function") return WRAPPERS.get(type.name);
|
|
658
|
+
if (typeof type === "symbol") {
|
|
659
|
+
return type.description === "react.view_transition"
|
|
660
|
+
? "ViewTransition"
|
|
661
|
+
: undefined;
|
|
662
|
+
}
|
|
663
|
+
return undefined;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
interface ElementLike {
|
|
667
|
+
type: unknown;
|
|
668
|
+
key: string | null;
|
|
669
|
+
props: { children?: unknown };
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function isElement(node: unknown): node is ElementLike {
|
|
673
|
+
return (
|
|
674
|
+
node !== null &&
|
|
675
|
+
typeof node === "object" &&
|
|
676
|
+
"type" in node &&
|
|
677
|
+
"props" in node &&
|
|
678
|
+
typeof (node as ElementLike).props === "object"
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// The router's own wrappers from a segment's outermost element down to its
|
|
683
|
+
// content: `Type#key` per link.
|
|
684
|
+
function wrapperChain(node: unknown): string[] {
|
|
685
|
+
const chain: string[] = [];
|
|
686
|
+
let current = node;
|
|
687
|
+
while (isElement(current)) {
|
|
688
|
+
const name = wrapperName(current.type);
|
|
689
|
+
if (!name) break;
|
|
690
|
+
chain.push(current.key === null ? name : `${name}#${current.key}`);
|
|
691
|
+
current = current.props.children;
|
|
692
|
+
}
|
|
693
|
+
return chain;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
export function startTreeAudit(): TreeAudit | undefined {
|
|
697
|
+
if (!INTERNAL_RANGO_SUSPENSE_AUDIT || typeof window === "undefined") {
|
|
698
|
+
return undefined;
|
|
699
|
+
}
|
|
700
|
+
getReport();
|
|
701
|
+
// A root emitted as a promise is held before this tree reads its segments.
|
|
702
|
+
heldTree();
|
|
703
|
+
for (const ref of handedSegments) {
|
|
704
|
+
const segment = ref.deref();
|
|
705
|
+
if (segment) auditHandedSegment(segment);
|
|
706
|
+
else handedSegments.delete(ref);
|
|
707
|
+
}
|
|
708
|
+
return {
|
|
709
|
+
order: [],
|
|
710
|
+
keys: new Map(),
|
|
711
|
+
types: new Map(),
|
|
712
|
+
chains: new Map(),
|
|
713
|
+
replaced: new Set(),
|
|
714
|
+
segments: [],
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* I7: a segment object handed to React was written since. Judged when the
|
|
720
|
+
* next tree starts, for every handed segment still alive (in the history
|
|
721
|
+
* cache or the tree on screen), and again as the tree reuses one, where
|
|
722
|
+
* renderSegments' own rewrites happen.
|
|
723
|
+
*/
|
|
724
|
+
function auditHandedSegment(segment: object): void {
|
|
725
|
+
const before = handedFields.get(segment);
|
|
726
|
+
if (!before) return;
|
|
727
|
+
const now = segment as Record<string, unknown>;
|
|
728
|
+
for (const key in before) {
|
|
729
|
+
if (Object.is(before[key], now[key])) continue;
|
|
730
|
+
violate(
|
|
731
|
+
"mutated",
|
|
732
|
+
`segment:${String(now.id ?? "?")}`,
|
|
733
|
+
`${key} was written after the tree holding this segment was handed to React; build a new segment object instead`,
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
// One report per write: the next build compares with what this one read.
|
|
737
|
+
handedFields.set(segment, { ...now });
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/** The element renderSegments built for one segment, leaf first. */
|
|
741
|
+
export function auditSegmentElement(
|
|
742
|
+
tree: TreeAudit | undefined,
|
|
743
|
+
id: string,
|
|
744
|
+
type: string,
|
|
745
|
+
key: string,
|
|
746
|
+
element: unknown,
|
|
747
|
+
outletTransition: boolean,
|
|
748
|
+
slots: readonly string[],
|
|
749
|
+
segments: readonly object[],
|
|
750
|
+
): void {
|
|
751
|
+
if (!tree) return;
|
|
752
|
+
tree.order.push(id);
|
|
753
|
+
tree.keys.set(id, key);
|
|
754
|
+
tree.types.set(id, type);
|
|
755
|
+
// A slot has no key of its own: it stays while its id is in the tree.
|
|
756
|
+
for (const slot of slots) tree.keys.set(slot, slot);
|
|
757
|
+
for (const segment of segments) {
|
|
758
|
+
auditHandedSegment(segment);
|
|
759
|
+
tree.segments.push(segment);
|
|
760
|
+
}
|
|
761
|
+
const chain = wrapperChain(element);
|
|
762
|
+
if (outletTransition) chain.push("outlet:ViewTransition");
|
|
763
|
+
tree.chains.set(id, chain);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* A finished tree. The first is the document's, which React holds from
|
|
768
|
+
* hydration; any later one is held when an emit hands its root to React
|
|
769
|
+
* (auditTreeUpdate).
|
|
770
|
+
*/
|
|
771
|
+
export function finishTreeAudit(
|
|
772
|
+
tree: TreeAudit | undefined,
|
|
773
|
+
root: unknown,
|
|
774
|
+
): void {
|
|
775
|
+
if (!tree) return;
|
|
776
|
+
tree.order.reverse();
|
|
777
|
+
if (heldTree() === undefined) hold(tree);
|
|
778
|
+
else if (root !== null && typeof root === "object") {
|
|
779
|
+
builtTrees.set(root, tree);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* I5: a segment that keeps its React key between two held trees must keep
|
|
785
|
+
* its wrapper chain, or React remounts it (docs/tree-structure.md). The
|
|
786
|
+
* documented remounts are exempt, with everything below them: a key that
|
|
787
|
+
* changes (a param change outside a transition scope), and a segment
|
|
788
|
+
* replaced by another type under the same id (an error or notFound segment
|
|
789
|
+
* takes its route's id, with its own key rule and wrappers). A key whose
|
|
790
|
+
* shape alone changes, `id` against `id-params`, is not one of them: it means
|
|
791
|
+
* `inTransitionScope` differed between the two renders.
|
|
792
|
+
*/
|
|
793
|
+
function hold(tree: TreeAudit): void {
|
|
794
|
+
const prev = held;
|
|
795
|
+
held = tree;
|
|
796
|
+
// What unmounted under the tree before this one left by something else:
|
|
797
|
+
// an error boundary took the page over, or a layout stopped rendering its
|
|
798
|
+
// outlet. Its next mount is a new one.
|
|
799
|
+
unmounted.clear();
|
|
800
|
+
settledSinceTree.clear();
|
|
801
|
+
for (const segment of tree.segments) {
|
|
802
|
+
handedFields.set(segment, { ...(segment as Record<string, unknown>) });
|
|
803
|
+
if (!trackedSegments.has(segment)) {
|
|
804
|
+
trackedSegments.add(segment);
|
|
805
|
+
handedSegments.add(new WeakRef(segment));
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
tree.segments = [];
|
|
809
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
810
|
+
trace("tree", {
|
|
811
|
+
segments: tree.order.map((id) => ({
|
|
812
|
+
id,
|
|
813
|
+
type: tree.types.get(id),
|
|
814
|
+
key: tree.keys.get(id),
|
|
815
|
+
chain: tree.chains.get(id),
|
|
816
|
+
})),
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
if (!prev) return;
|
|
820
|
+
for (let at = 0; at < tree.order.length; at++) {
|
|
821
|
+
const id = tree.order[at]!;
|
|
822
|
+
const prevKey = prev.keys.get(id);
|
|
823
|
+
if (prevKey === undefined) continue;
|
|
824
|
+
const key = tree.keys.get(id)!;
|
|
825
|
+
const retyped = prev.types.get(id) !== tree.types.get(id);
|
|
826
|
+
if (retyped || prevKey !== key) {
|
|
827
|
+
if (!retyped && (prevKey === id) !== (key === id)) {
|
|
828
|
+
violate(
|
|
829
|
+
"drift",
|
|
830
|
+
`outlet:${id}`,
|
|
831
|
+
`key shape changed from "${prevKey}" to "${key}"`,
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
for (let below = at; below < tree.order.length; below++) {
|
|
835
|
+
tree.replaced.add(tree.order[below]!);
|
|
836
|
+
}
|
|
837
|
+
break;
|
|
838
|
+
}
|
|
839
|
+
const before = prev.chains.get(id)!;
|
|
840
|
+
const after = tree.chains.get(id)!;
|
|
841
|
+
const length = Math.max(before.length, after.length);
|
|
842
|
+
for (let i = 0; i < length; i++) {
|
|
843
|
+
if (before[i] === after[i]) continue;
|
|
844
|
+
violate(
|
|
845
|
+
"drift",
|
|
846
|
+
`outlet:${id}`,
|
|
847
|
+
`wrapper chain link ${i} changed from ${before[i] ?? "nothing"} to ${after[i] ?? "nothing"}`,
|
|
848
|
+
);
|
|
849
|
+
break;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function keyOf(segmentId: string): string {
|
|
855
|
+
return heldTree()?.keys.get(segmentId) ?? segmentId;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// A boundary unmounted while the held tree still holds its segment under the
|
|
859
|
+
// same key, until it mounts again or the next tree is held. Not one
|
|
860
|
+
// commit's worth: a remounted boundary that suspends mounts its content
|
|
861
|
+
// after its fallback.
|
|
862
|
+
const unmounted = new Map<string, Set<BoundaryAudit>>();
|
|
863
|
+
const remounted: BoundaryAudit[] = [];
|
|
864
|
+
// Segments mounted for the first time, or remounted, since the last tree.
|
|
865
|
+
const settledSinceTree = new Set<string>();
|
|
866
|
+
let flushScheduled = false;
|
|
867
|
+
|
|
868
|
+
function slotOf(rec: BoundaryAudit): string {
|
|
869
|
+
return `${rec.boundary}|${rec.segmentId}|${rec.key}`;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function flushRemounts(): void {
|
|
873
|
+
flushScheduled = false;
|
|
874
|
+
const tree = heldTree();
|
|
875
|
+
const order = tree?.order ?? [];
|
|
876
|
+
const reported = new Set<string>();
|
|
877
|
+
for (const rec of remounted) {
|
|
878
|
+
if (reported.has(rec.segmentId)) continue;
|
|
879
|
+
// A slot (`<parent>.@name`) hangs off its parent segment.
|
|
880
|
+
const parent = rec.segmentId.split(".")[0]!;
|
|
881
|
+
const slot = parent !== rec.segmentId;
|
|
882
|
+
const depth = order.indexOf(parent);
|
|
883
|
+
// Not in the tree React holds: its place is unknown.
|
|
884
|
+
if (depth === -1) continue;
|
|
885
|
+
// Replaced by the documented rules (hold).
|
|
886
|
+
if (tree!.replaced.has(parent)) continue;
|
|
887
|
+
// Below a segment that is new to the page or was itself remounted, a
|
|
888
|
+
// remount is that segment's.
|
|
889
|
+
const end = slot ? depth + 1 : depth;
|
|
890
|
+
let below = false;
|
|
891
|
+
for (let i = 0; i < end && !below; i++) {
|
|
892
|
+
below = settledSinceTree.has(order[i]!);
|
|
893
|
+
}
|
|
894
|
+
if (below) continue;
|
|
895
|
+
reported.add(rec.segmentId);
|
|
896
|
+
violate(
|
|
897
|
+
"remount",
|
|
898
|
+
rec.boundary,
|
|
899
|
+
`unmounted and mounted again under the same key "${rec.key}"`,
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
for (const rec of remounted) settledSinceTree.add(rec.segmentId);
|
|
903
|
+
remounted.length = 0;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* I4: a boundary that stays on the page under the same React key is
|
|
908
|
+
* unmounted and mounted again. A different key is the documented remount.
|
|
909
|
+
*/
|
|
910
|
+
export function auditMount(rec: BoundaryAudit): void {
|
|
911
|
+
getReport();
|
|
912
|
+
rec.key = keyOf(rec.segmentId);
|
|
913
|
+
const gone = unmounted.get(slotOf(rec));
|
|
914
|
+
// StrictMode runs the effects of one instance twice: mount, unmount, mount.
|
|
915
|
+
if (gone?.delete(rec)) return;
|
|
916
|
+
const remount = gone !== undefined && gone.size > 0;
|
|
917
|
+
gone?.clear();
|
|
918
|
+
rec.mountId = (mountCounts.get(rec.boundary) ?? 0) + 1;
|
|
919
|
+
setBounded(mountCounts, rec.boundary, rec.mountId);
|
|
920
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
921
|
+
trace("mount", {
|
|
922
|
+
boundary: rec.boundary,
|
|
923
|
+
key: rec.key,
|
|
924
|
+
mountId: rec.mountId,
|
|
925
|
+
remount,
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
if (remount) remounted.push(rec);
|
|
929
|
+
else settledSinceTree.add(rec.segmentId);
|
|
930
|
+
if (!flushScheduled) {
|
|
931
|
+
flushScheduled = true;
|
|
932
|
+
later(flushRemounts);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
export function auditUnmount(rec: BoundaryAudit): void {
|
|
937
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
938
|
+
trace("unmount", {
|
|
939
|
+
boundary: rec.boundary,
|
|
940
|
+
key: rec.key,
|
|
941
|
+
mountId: rec.mountId,
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
const slot = slotOf(rec);
|
|
945
|
+
const gone = unmounted.get(slot);
|
|
946
|
+
if (gone) gone.add(rec);
|
|
947
|
+
else unmounted.set(slot, new Set([rec]));
|
|
948
|
+
// Judged once the commit is over: StrictMode mounts this instance again
|
|
949
|
+
// at once, and a segment the held tree dropped or re-keyed is leaving.
|
|
950
|
+
later(() => {
|
|
951
|
+
const now = unmounted.get(slot);
|
|
952
|
+
if (now?.has(rec) && heldTree()?.keys.get(rec.segmentId) !== rec.key) {
|
|
953
|
+
now.delete(rec);
|
|
954
|
+
}
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
const TREE_CAUSES: ReadonlySet<string> = new Set<TreeUpdateCause>([
|
|
959
|
+
"navigation",
|
|
960
|
+
"popstate",
|
|
961
|
+
"stale-revalidation",
|
|
962
|
+
"action",
|
|
963
|
+
"error",
|
|
964
|
+
"hmr",
|
|
965
|
+
]);
|
|
966
|
+
|
|
967
|
+
let treeCause: string | undefined;
|
|
968
|
+
let treeCauseTurn = 0;
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* I6: the cause of the tree update the caller emits next, in the same task.
|
|
972
|
+
* It does not outlive the task: an emit after an await names its cause again,
|
|
973
|
+
* or a cause left behind would cover an emitter that names none.
|
|
974
|
+
*/
|
|
975
|
+
export function auditTreeCause(cause: TreeUpdateCause): void {
|
|
976
|
+
if (!INTERNAL_RANGO_SUSPENSE_AUDIT || typeof window === "undefined") return;
|
|
977
|
+
treeCause = cause;
|
|
978
|
+
const turn = ++treeCauseTurn;
|
|
979
|
+
later(() => {
|
|
980
|
+
if (treeCauseTurn === turn) treeCause = undefined;
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/**
|
|
985
|
+
* I6: a tree update reached React (the store.onUpdate subscriber in
|
|
986
|
+
* browser/react/NavigationProvider.tsx), with the root it hands React. Only a
|
|
987
|
+
* navigation, a back/forward, its stale revalidation, an action, an error and
|
|
988
|
+
* HMR hand React a tree; anything else updates in place through a store its
|
|
989
|
+
* readers subscribe to, or through a pending promise read with use().
|
|
990
|
+
*/
|
|
991
|
+
export function auditTreeUpdate(root?: unknown): void {
|
|
992
|
+
if (!INTERNAL_RANGO_SUSPENSE_AUDIT || typeof window === "undefined") return;
|
|
993
|
+
const cause = treeCause;
|
|
994
|
+
treeCause = undefined;
|
|
995
|
+
const r = getReport();
|
|
996
|
+
const name = cause ?? "none";
|
|
997
|
+
r.treeUpdates[name] = (r.treeUpdates[name] ?? 0) + 1;
|
|
998
|
+
if (isThenable(root)) {
|
|
999
|
+
pendingRoot = root;
|
|
1000
|
+
} else if (root !== null && typeof root === "object") {
|
|
1001
|
+
const tree = builtTrees.get(root);
|
|
1002
|
+
if (tree) {
|
|
1003
|
+
pendingRoot = undefined;
|
|
1004
|
+
hold(tree);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
if (INTERNAL_RANGO_DEBUG) trace("tree-update", { cause: name });
|
|
1008
|
+
if (cause === undefined) {
|
|
1009
|
+
violate(
|
|
1010
|
+
"uncaused",
|
|
1011
|
+
"tree",
|
|
1012
|
+
"React was handed a tree by an emitter that named no cause; an update that is not a navigation or an action goes through a store or a promise read with use()",
|
|
1013
|
+
);
|
|
1014
|
+
} else if (!TREE_CAUSES.has(cause)) {
|
|
1015
|
+
violate("uncaused", "tree", `"${cause}" is not a cause of a tree update`);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
/** Forget every tree and mount seen so far. For tests of the audit itself. */
|
|
1020
|
+
export function forgetSuspenseAudit(): void {
|
|
1021
|
+
held = undefined;
|
|
1022
|
+
handedSegments.clear();
|
|
1023
|
+
pendingRoot = undefined;
|
|
1024
|
+
treeCause = undefined;
|
|
1025
|
+
unmounted.clear();
|
|
1026
|
+
remounted.length = 0;
|
|
1027
|
+
settledSinceTree.clear();
|
|
1028
|
+
mountCounts.clear();
|
|
1029
|
+
getReport().reset();
|
|
1030
|
+
}
|