@flotrace/runtime-core 2.2.1 → 2.2.3
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/LICENSE +21 -0
- package/README.md +59 -16
- package/dist/index.d.mts +29 -8
- package/dist/index.d.ts +29 -8
- package/dist/index.js +203 -50
- package/dist/index.mjs +203 -50
- package/package.json +9 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sameer Sitre
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,33 +1,76 @@
|
|
|
1
1
|
# @flotrace/runtime-core
|
|
2
2
|
|
|
3
|
-
Platform-agnostic core for
|
|
3
|
+
Platform-agnostic core for [FloTrace](https://flotrace.dev) — fiber walker, hook/effect inspectors, state-store trackers (Zustand / Redux / TanStack Query), serializer, and WebSocket client. Shared between [`@flotrace/runtime`](https://www.npmjs.com/package/@flotrace/runtime) (web) and [`@flotrace/runtime-native`](https://www.npmjs.com/package/@flotrace/runtime-native) (React Native).
|
|
4
4
|
|
|
5
|
-
> **You
|
|
6
|
-
>
|
|
5
|
+
> **You almost certainly want one of the adapter packages instead.**
|
|
6
|
+
>
|
|
7
|
+
> - **Web React app?** → [`@flotrace/runtime`](https://www.npmjs.com/package/@flotrace/runtime)
|
|
8
|
+
> - **React Native (Expo / bare)?** → [`@flotrace/runtime-native`](https://www.npmjs.com/package/@flotrace/runtime-native)
|
|
9
|
+
>
|
|
10
|
+
> The adapters depend on `runtime-core` and provide the wiring (provider component, network tracker, platform-specific hooks) you actually need. This package on its own does nothing useful at runtime.
|
|
7
11
|
|
|
8
|
-
`runtime-core` is published
|
|
12
|
+
`runtime-core` is published publicly so adapters can pin a compatible version and so users can audit the open-source half of FloTrace. It has zero runtime dependency on `window` / `document` / `XMLHttpRequest` — all platform-specific features live in the adapters.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## About FloTrace Desktop
|
|
17
|
+
|
|
18
|
+
[**FloTrace Desktop**](https://flotrace.dev) is a free Electron app (macOS / Windows / Linux) that visualizes a running React app's component hierarchy in real time. It pairs with this runtime over a local WebSocket on port `3457` — the runtime sits inside your app and emits metadata; the desktop app renders the live tree, props, hooks, effects, and state. **Source code never leaves your machine.**
|
|
19
|
+
|
|
20
|
+
What you get when this runtime is paired with the desktop:
|
|
21
|
+
|
|
22
|
+
- **Live component tree** — React Flow graph, render-flash animation, frequency-based heatmap, breadcrumb navigation.
|
|
23
|
+
- **Per-node inspection** — props (with diff history), hooks (14 classified types + dep diffs), effects (willRun + dep diffs), component timeline.
|
|
24
|
+
- **State tracking** — Zustand (per-store), Redux (with change highlighting), Router, TanStack Query (with health warnings + wasted-refetch detection), Context.
|
|
25
|
+
- **Render cascade tracing** — trigger log, cascade tree, flame chart, cascade compare modal.
|
|
26
|
+
- **Prop drilling detection** — chain detection (≥3 levels deep), severity badges, heatmap overlay, refactor recommendations.
|
|
27
|
+
- **Network health** — fetch / XHR tracking, method badges, status dots, duplicate detection, API → store causal correlation.
|
|
28
|
+
- **Watch expressions** — pin values from 8 sources (Zustand / Redux / Router / Context / Props / Hooks / TanStack Query / API).
|
|
29
|
+
- **AI Code Review Dashboard** — 6-tab review (Re-renders, Memo, Drilling, Effects, Compiler, Network) with Lighthouse-style scores.
|
|
30
|
+
- **Copy-as-Prompt** — turn any panel into an AI-ready prompt for Cursor / Claude / ChatGPT in one click.
|
|
31
|
+
|
|
32
|
+
How it fits together:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
your React app ←→ @flotrace/runtime[-native] ←→ ws://localhost:3457 ←→ FloTrace Desktop
|
|
36
|
+
(this stack — open source, MIT) (closed-source commercial)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
[**Download FloTrace Desktop →**](https://flotrace.dev) · [Docs](https://flotrace.dev/docs) · [Security model](https://flotrace.dev/security)
|
|
9
40
|
|
|
10
41
|
## What's inside
|
|
11
42
|
|
|
12
43
|
| Module | Purpose |
|
|
13
44
|
|---|---|
|
|
14
|
-
| `fiberTreeWalker` | Incremental fiber walk, diffed tree emission
|
|
15
|
-
| `hookInspector` / `effectInspector` | Classify hooks and effects from a fiber; diff deps between commits. |
|
|
16
|
-
| `zustandTracker` / `reduxTracker` / `tanstackQueryTracker` | Duck-typed subscribers for the major state libraries. |
|
|
17
|
-
| `timelineTracker` | Per-component lifecycle events. |
|
|
18
|
-
| `cascadeAnalyzer` / `propDrillingAnalyzer` | Render-cascade tracing + prop-drilling chain detection. |
|
|
19
|
-
| `serializer` | Safe JSON serialization (depth 5, circular
|
|
20
|
-
| `websocketClient` | Singleton WS client with reconnect, batching, auth
|
|
45
|
+
| `fiberTreeWalker` | Incremental fiber walk, diffed tree emission. Pluggable `pruneSubtree` / `frameworkComponentNames` / `hostComponentSkipPrefixes` for platform adapters. |
|
|
46
|
+
| `hookInspector` / `effectInspector` | Classify hooks (14 types) and effects from a fiber; diff deps between commits. |
|
|
47
|
+
| `zustandTracker` / `reduxTracker` / `tanstackQueryTracker` | Duck-typed subscribers for the major state libraries — no peer-dep bloat. |
|
|
48
|
+
| `timelineTracker` | Per-component lifecycle events (mount, unmount, update, prop diff). |
|
|
49
|
+
| `cascadeAnalyzer` / `propDrillingAnalyzer` | Render-cascade tracing + prop-drilling DFS chain detection with severity scoring. |
|
|
50
|
+
| `serializer` | Safe JSON serialization (depth 5, circular-ref guard, truncation). |
|
|
51
|
+
| `websocketClient` | Singleton WS client with exponential backoff reconnect, message batching, optional auth token. |
|
|
21
52
|
|
|
22
53
|
## Version compatibility
|
|
23
54
|
|
|
24
|
-
`@flotrace/runtime-core@
|
|
55
|
+
`@flotrace/runtime-core@2.x` is the companion release for:
|
|
56
|
+
|
|
57
|
+
- `@flotrace/runtime@2.x`
|
|
58
|
+
- `@flotrace/runtime-native@2.x`
|
|
59
|
+
|
|
60
|
+
All three are released in lockstep — pin the same major.minor across the trio. The desktop app and runtime versions are independent (the WebSocket protocol is versioned).
|
|
61
|
+
|
|
62
|
+
## Why open?
|
|
25
63
|
|
|
26
|
-
-
|
|
27
|
-
- `@flotrace/runtime-native@0.1.x`
|
|
64
|
+
The runtime is what lives inside your app. Open-source means you can read every byte of the code that touches your fibers, audit the WebSocket payloads, and fork if FloTrace ever disappears. The desktop app is closed-source commercial — that's the bit we charge for. See [flotrace.dev/security](https://flotrace.dev/security) for the full threat model.
|
|
28
65
|
|
|
29
|
-
|
|
66
|
+
## Contributing
|
|
67
|
+
|
|
68
|
+
Issues and PRs welcome at [github.com/sameersitre/runtime-core](https://github.com/sameersitre/runtime-core). The runtime packages target Hermes, V8 (Chromium), and JavaScriptCore — please test against all three when changing fiber-walker or serializer code.
|
|
30
69
|
|
|
31
70
|
## License
|
|
32
71
|
|
|
33
|
-
MIT
|
|
72
|
+
MIT — see [LICENSE](./LICENSE).
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
> **Mirrored from the [flotrace-desktop](https://github.com/sameersitre/flotrace-desktop) monorepo.** This repo is read-only — every release is regenerated by the lockstep publisher in the desktop monorepo. Issues filed here are tracked, but PRs are best opened against the upstream monorepo where the canonical source lives.
|
package/dist/index.d.mts
CHANGED
|
@@ -48,6 +48,13 @@ interface RuntimeReadyMessage {
|
|
|
48
48
|
/** React Native version from `Platform.constants.reactNativeVersion`, formatted
|
|
49
49
|
* as "major.minor.patch". Native-only; web adapter leaves this undefined. */
|
|
50
50
|
reactNativeVersion?: string;
|
|
51
|
+
/** Version of the @flotrace/runtime or @flotrace/runtime-native package the
|
|
52
|
+
* user installed in their app. Lets the desktop diagnose runtime/desktop
|
|
53
|
+
* drift (e.g., user pinned an older runtime that lacks a new feature).
|
|
54
|
+
* Read from the adapter's own package.json at build time. The lockstep
|
|
55
|
+
* release script keeps runtime-core pinned identically, so a separate
|
|
56
|
+
* core-version field would be redundant. */
|
|
57
|
+
runtimeVersion?: string;
|
|
51
58
|
}
|
|
52
59
|
interface RuntimeRenderMessage {
|
|
53
60
|
type: 'runtime:render';
|
|
@@ -867,10 +874,14 @@ interface FloTraceConfig {
|
|
|
867
874
|
/** React Native version from `Platform.constants.reactNativeVersion`, formatted
|
|
868
875
|
* "major.minor.patch". Native adapter only. */
|
|
869
876
|
reactNativeVersion?: string;
|
|
877
|
+
/** Version of the active runtime adapter (`@flotrace/runtime` on web,
|
|
878
|
+
* `@flotrace/runtime-native` on RN). Adapters auto-populate from their
|
|
879
|
+
* own package.json. Surfaced on `runtime:ready` for diagnostics. */
|
|
880
|
+
runtimeVersion?: string;
|
|
870
881
|
}
|
|
871
882
|
/** Keys that stay optional in DEFAULT_CONFIG. These are populated by adapters (web/native)
|
|
872
883
|
* at call-time — the default object should not pretend to know a platform or LAN token. */
|
|
873
|
-
type OptionalConfigKeys = 'getAppUrl' | 'platform' | 'appId' | 'appVersion' | 'host' | 'authToken' | 'userOnlyStrict' | 'userAllowPatterns' | 'frameworkName' | 'frameworkVersion' | 'reactNativeVersion';
|
|
884
|
+
type OptionalConfigKeys = 'getAppUrl' | 'platform' | 'appId' | 'appVersion' | 'host' | 'authToken' | 'userOnlyStrict' | 'userAllowPatterns' | 'frameworkName' | 'frameworkVersion' | 'reactNativeVersion' | 'runtimeVersion';
|
|
874
885
|
type ResolvedFloTraceConfig = Required<Omit<FloTraceConfig, OptionalConfigKeys>> & Pick<FloTraceConfig, OptionalConfigKeys>;
|
|
875
886
|
/**
|
|
876
887
|
* Default configuration
|
|
@@ -1589,7 +1600,7 @@ declare function getChangedKeys(prev: Record<string, unknown> | undefined, next:
|
|
|
1589
1600
|
* State lives here — in platform-agnostic core — so both the web and native
|
|
1590
1601
|
* network trackers can write to the same registry that the analyzers read.
|
|
1591
1602
|
*/
|
|
1592
|
-
/** Tag an object and its nested children (depth ≤
|
|
1603
|
+
/** Tag an object and its nested children (depth ≤ FETCH_ORIGIN_SCAN_DEPTH) with the requestId. */
|
|
1593
1604
|
declare function tagFetchData(obj: unknown, requestId: string, depth?: number): void;
|
|
1594
1605
|
/** Returns true if any API request's response data is currently tagged (within TTL window). */
|
|
1595
1606
|
declare function hasActiveTags(): boolean;
|
|
@@ -1600,13 +1611,23 @@ declare function hasActiveTags(): boolean;
|
|
|
1600
1611
|
*/
|
|
1601
1612
|
declare function clearFetchOriginTags(): void;
|
|
1602
1613
|
/**
|
|
1603
|
-
* Scan an object (and nested children up to
|
|
1604
|
-
*
|
|
1605
|
-
*
|
|
1606
|
-
*
|
|
1607
|
-
*
|
|
1614
|
+
* Scan an object (and nested children up to FETCH_ORIGIN_SCAN_DEPTH) for a
|
|
1615
|
+
* WeakMap-tagged fetch origin. Returns the requestId if this object was the
|
|
1616
|
+
* result of a tracked fetch, else undefined.
|
|
1617
|
+
*
|
|
1618
|
+
* By default the result must be within FETCH_ORIGIN_TTL_MS of the original
|
|
1619
|
+
* fetch. Network-panel features (live API → Store correlation in
|
|
1620
|
+
* `storeUtils.buildCorrelatedRequests`, `tanstackQueryTracker.updateQueryTracking`,
|
|
1621
|
+
* `fiberTreeWalker.scanFiberStateForOrigin`) rely on this gate to avoid
|
|
1622
|
+
* mis-attributing later mutations.
|
|
1623
|
+
*
|
|
1624
|
+
* Pass `{ ignoreTTL: true }` to bypass the gate. Used by the value-lineage
|
|
1625
|
+
* resolver, which wants the *causal* origin regardless of how long ago the
|
|
1626
|
+
* fetch resolved (the trace UI displays an "expired" hint for old origins).
|
|
1608
1627
|
*/
|
|
1609
|
-
declare function findFetchOrigin(obj: unknown,
|
|
1628
|
+
declare function findFetchOrigin(obj: unknown, options?: {
|
|
1629
|
+
ignoreTTL?: boolean;
|
|
1630
|
+
}): string | undefined;
|
|
1610
1631
|
|
|
1611
1632
|
/**
|
|
1612
1633
|
* Next.js App Router client-side detection.
|
package/dist/index.d.ts
CHANGED
|
@@ -48,6 +48,13 @@ interface RuntimeReadyMessage {
|
|
|
48
48
|
/** React Native version from `Platform.constants.reactNativeVersion`, formatted
|
|
49
49
|
* as "major.minor.patch". Native-only; web adapter leaves this undefined. */
|
|
50
50
|
reactNativeVersion?: string;
|
|
51
|
+
/** Version of the @flotrace/runtime or @flotrace/runtime-native package the
|
|
52
|
+
* user installed in their app. Lets the desktop diagnose runtime/desktop
|
|
53
|
+
* drift (e.g., user pinned an older runtime that lacks a new feature).
|
|
54
|
+
* Read from the adapter's own package.json at build time. The lockstep
|
|
55
|
+
* release script keeps runtime-core pinned identically, so a separate
|
|
56
|
+
* core-version field would be redundant. */
|
|
57
|
+
runtimeVersion?: string;
|
|
51
58
|
}
|
|
52
59
|
interface RuntimeRenderMessage {
|
|
53
60
|
type: 'runtime:render';
|
|
@@ -867,10 +874,14 @@ interface FloTraceConfig {
|
|
|
867
874
|
/** React Native version from `Platform.constants.reactNativeVersion`, formatted
|
|
868
875
|
* "major.minor.patch". Native adapter only. */
|
|
869
876
|
reactNativeVersion?: string;
|
|
877
|
+
/** Version of the active runtime adapter (`@flotrace/runtime` on web,
|
|
878
|
+
* `@flotrace/runtime-native` on RN). Adapters auto-populate from their
|
|
879
|
+
* own package.json. Surfaced on `runtime:ready` for diagnostics. */
|
|
880
|
+
runtimeVersion?: string;
|
|
870
881
|
}
|
|
871
882
|
/** Keys that stay optional in DEFAULT_CONFIG. These are populated by adapters (web/native)
|
|
872
883
|
* at call-time — the default object should not pretend to know a platform or LAN token. */
|
|
873
|
-
type OptionalConfigKeys = 'getAppUrl' | 'platform' | 'appId' | 'appVersion' | 'host' | 'authToken' | 'userOnlyStrict' | 'userAllowPatterns' | 'frameworkName' | 'frameworkVersion' | 'reactNativeVersion';
|
|
884
|
+
type OptionalConfigKeys = 'getAppUrl' | 'platform' | 'appId' | 'appVersion' | 'host' | 'authToken' | 'userOnlyStrict' | 'userAllowPatterns' | 'frameworkName' | 'frameworkVersion' | 'reactNativeVersion' | 'runtimeVersion';
|
|
874
885
|
type ResolvedFloTraceConfig = Required<Omit<FloTraceConfig, OptionalConfigKeys>> & Pick<FloTraceConfig, OptionalConfigKeys>;
|
|
875
886
|
/**
|
|
876
887
|
* Default configuration
|
|
@@ -1589,7 +1600,7 @@ declare function getChangedKeys(prev: Record<string, unknown> | undefined, next:
|
|
|
1589
1600
|
* State lives here — in platform-agnostic core — so both the web and native
|
|
1590
1601
|
* network trackers can write to the same registry that the analyzers read.
|
|
1591
1602
|
*/
|
|
1592
|
-
/** Tag an object and its nested children (depth ≤
|
|
1603
|
+
/** Tag an object and its nested children (depth ≤ FETCH_ORIGIN_SCAN_DEPTH) with the requestId. */
|
|
1593
1604
|
declare function tagFetchData(obj: unknown, requestId: string, depth?: number): void;
|
|
1594
1605
|
/** Returns true if any API request's response data is currently tagged (within TTL window). */
|
|
1595
1606
|
declare function hasActiveTags(): boolean;
|
|
@@ -1600,13 +1611,23 @@ declare function hasActiveTags(): boolean;
|
|
|
1600
1611
|
*/
|
|
1601
1612
|
declare function clearFetchOriginTags(): void;
|
|
1602
1613
|
/**
|
|
1603
|
-
* Scan an object (and nested children up to
|
|
1604
|
-
*
|
|
1605
|
-
*
|
|
1606
|
-
*
|
|
1607
|
-
*
|
|
1614
|
+
* Scan an object (and nested children up to FETCH_ORIGIN_SCAN_DEPTH) for a
|
|
1615
|
+
* WeakMap-tagged fetch origin. Returns the requestId if this object was the
|
|
1616
|
+
* result of a tracked fetch, else undefined.
|
|
1617
|
+
*
|
|
1618
|
+
* By default the result must be within FETCH_ORIGIN_TTL_MS of the original
|
|
1619
|
+
* fetch. Network-panel features (live API → Store correlation in
|
|
1620
|
+
* `storeUtils.buildCorrelatedRequests`, `tanstackQueryTracker.updateQueryTracking`,
|
|
1621
|
+
* `fiberTreeWalker.scanFiberStateForOrigin`) rely on this gate to avoid
|
|
1622
|
+
* mis-attributing later mutations.
|
|
1623
|
+
*
|
|
1624
|
+
* Pass `{ ignoreTTL: true }` to bypass the gate. Used by the value-lineage
|
|
1625
|
+
* resolver, which wants the *causal* origin regardless of how long ago the
|
|
1626
|
+
* fetch resolved (the trace UI displays an "expired" hint for old origins).
|
|
1608
1627
|
*/
|
|
1609
|
-
declare function findFetchOrigin(obj: unknown,
|
|
1628
|
+
declare function findFetchOrigin(obj: unknown, options?: {
|
|
1629
|
+
ignoreTTL?: boolean;
|
|
1630
|
+
}): string | undefined;
|
|
1610
1631
|
|
|
1611
1632
|
/**
|
|
1612
1633
|
* Next.js App Router client-side detection.
|
package/dist/index.js
CHANGED
|
@@ -84,7 +84,13 @@ module.exports = __toCommonJS(index_exports);
|
|
|
84
84
|
var DEFAULT_CONFIG = {
|
|
85
85
|
port: 3457,
|
|
86
86
|
appName: "React App",
|
|
87
|
-
|
|
87
|
+
// Default-on unless an explicit `process.env.NODE_ENV === 'production'` is
|
|
88
|
+
// detected. The previous heuristic (`=== 'development'`) silently disabled
|
|
89
|
+
// the runtime in any browser context that doesn't shim `process` (e.g. Vite,
|
|
90
|
+
// Webpack 5 with `node: false`, Rsbuild) — making the README quickstart fail
|
|
91
|
+
// for everyone. Production safety is handled by users gating the import via
|
|
92
|
+
// the dynamic-import pattern documented in the runtime READMEs.
|
|
93
|
+
enabled: globalThis.process?.env?.NODE_ENV !== "production",
|
|
88
94
|
autoReconnect: true,
|
|
89
95
|
reconnectInterval: 2e3,
|
|
90
96
|
trackAllRenders: true,
|
|
@@ -309,7 +315,8 @@ var _FloTraceWebSocketClient = class _FloTraceWebSocketClient {
|
|
|
309
315
|
appVersion: this.config.appVersion,
|
|
310
316
|
frameworkName: this.config.frameworkName,
|
|
311
317
|
frameworkVersion: this.config.frameworkVersion,
|
|
312
|
-
reactNativeVersion: this.config.reactNativeVersion
|
|
318
|
+
reactNativeVersion: this.config.reactNativeVersion,
|
|
319
|
+
runtimeVersion: this.config.runtimeVersion
|
|
313
320
|
});
|
|
314
321
|
this.flush();
|
|
315
322
|
};
|
|
@@ -1840,20 +1847,23 @@ function extractRoute(url) {
|
|
|
1840
1847
|
var originalFetch = null;
|
|
1841
1848
|
var interceptorClient = null;
|
|
1842
1849
|
var isInstalled2 = false;
|
|
1850
|
+
var patchedFetchRef = null;
|
|
1843
1851
|
function installRscPayloadInterceptor(client2) {
|
|
1844
1852
|
if (isInstalled2 || typeof globalThis.fetch !== "function") return;
|
|
1845
1853
|
isInstalled2 = true;
|
|
1846
1854
|
interceptorClient = client2;
|
|
1847
1855
|
originalFetch = globalThis.fetch;
|
|
1848
|
-
|
|
1856
|
+
const capturedOriginalFetch = originalFetch;
|
|
1857
|
+
const capturedClient = client2;
|
|
1858
|
+
const patchedFetch = async function patchedFetch2(input, init) {
|
|
1849
1859
|
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
1850
1860
|
const isRscRequest = RSC_URL_PATTERNS.some((p) => p.test(url));
|
|
1851
|
-
const response = await
|
|
1852
|
-
if (isRscRequest && interceptorClient
|
|
1861
|
+
const response = await capturedOriginalFetch.call(globalThis, input, init);
|
|
1862
|
+
if (isRscRequest && interceptorClient === capturedClient && capturedClient.connected) {
|
|
1853
1863
|
try {
|
|
1854
1864
|
const sizeHeader = response.headers.get("content-length");
|
|
1855
1865
|
const payloadSizeBytes = sizeHeader ? parseInt(sizeHeader, 10) : 0;
|
|
1856
|
-
|
|
1866
|
+
capturedClient.send({
|
|
1857
1867
|
type: "runtime:rscPayload",
|
|
1858
1868
|
route: extractRoute(url),
|
|
1859
1869
|
payloadSizeBytes: isNaN(payloadSizeBytes) ? 0 : payloadSizeBytes,
|
|
@@ -1865,11 +1875,16 @@ function installRscPayloadInterceptor(client2) {
|
|
|
1865
1875
|
}
|
|
1866
1876
|
return response;
|
|
1867
1877
|
};
|
|
1878
|
+
patchedFetchRef = patchedFetch;
|
|
1879
|
+
globalThis.fetch = patchedFetch;
|
|
1868
1880
|
}
|
|
1869
1881
|
function uninstallRscPayloadInterceptor() {
|
|
1870
1882
|
if (!isInstalled2 || !originalFetch) return;
|
|
1871
|
-
globalThis.fetch
|
|
1883
|
+
if (globalThis.fetch === patchedFetchRef) {
|
|
1884
|
+
globalThis.fetch = originalFetch;
|
|
1885
|
+
}
|
|
1872
1886
|
originalFetch = null;
|
|
1887
|
+
patchedFetchRef = null;
|
|
1873
1888
|
interceptorClient = null;
|
|
1874
1889
|
isInstalled2 = false;
|
|
1875
1890
|
}
|
|
@@ -1878,12 +1893,16 @@ function uninstallRscPayloadInterceptor() {
|
|
|
1878
1893
|
var fetchDataOrigin = /* @__PURE__ */ new WeakMap();
|
|
1879
1894
|
var requestTagTimestamps = /* @__PURE__ */ new Map();
|
|
1880
1895
|
var FETCH_ORIGIN_TTL_MS = 3e3;
|
|
1896
|
+
var FETCH_ORIGIN_SCAN_DEPTH = 4;
|
|
1897
|
+
var FETCH_ORIGIN_TAG_ARRAY_LIMIT = 50;
|
|
1898
|
+
var FETCH_ORIGIN_SCAN_ARRAY_LIMIT = 20;
|
|
1881
1899
|
function tagFetchData(obj, requestId, depth = 0) {
|
|
1882
|
-
if (depth >
|
|
1900
|
+
if (depth > FETCH_ORIGIN_SCAN_DEPTH || obj === null || typeof obj !== "object") return;
|
|
1883
1901
|
fetchDataOrigin.set(obj, requestId);
|
|
1884
1902
|
if (depth === 0) requestTagTimestamps.set(requestId, Date.now());
|
|
1885
1903
|
if (Array.isArray(obj)) {
|
|
1886
|
-
|
|
1904
|
+
const limit = Math.min(obj.length, FETCH_ORIGIN_TAG_ARRAY_LIMIT);
|
|
1905
|
+
for (let i = 0; i < limit; i++) tagFetchData(obj[i], requestId, depth + 1);
|
|
1887
1906
|
} else {
|
|
1888
1907
|
for (const val of Object.values(obj)) tagFetchData(val, requestId, depth + 1);
|
|
1889
1908
|
}
|
|
@@ -1894,24 +1913,29 @@ function hasActiveTags() {
|
|
|
1894
1913
|
function clearFetchOriginTags() {
|
|
1895
1914
|
requestTagTimestamps.clear();
|
|
1896
1915
|
}
|
|
1897
|
-
function findFetchOrigin(obj,
|
|
1898
|
-
|
|
1916
|
+
function findFetchOrigin(obj, options) {
|
|
1917
|
+
return scanForOrigin(obj, 0, options?.ignoreTTL === true);
|
|
1918
|
+
}
|
|
1919
|
+
function scanForOrigin(obj, depth, ignoreTTL) {
|
|
1920
|
+
if (depth > FETCH_ORIGIN_SCAN_DEPTH || obj === null || typeof obj !== "object") return void 0;
|
|
1899
1921
|
const rid = fetchDataOrigin.get(obj);
|
|
1900
1922
|
if (rid) {
|
|
1923
|
+
if (ignoreTTL) return rid;
|
|
1901
1924
|
const tagTime = requestTagTimestamps.get(rid);
|
|
1902
1925
|
if (tagTime && Date.now() - tagTime <= FETCH_ORIGIN_TTL_MS) return rid;
|
|
1903
1926
|
requestTagTimestamps.delete(rid);
|
|
1904
1927
|
}
|
|
1905
1928
|
if (Array.isArray(obj)) {
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
}
|
|
1910
|
-
} else {
|
|
1911
|
-
for (const val of Object.values(obj)) {
|
|
1912
|
-
const found = findFetchOrigin(val, depth + 1);
|
|
1929
|
+
const limit = Math.min(obj.length, FETCH_ORIGIN_SCAN_ARRAY_LIMIT);
|
|
1930
|
+
for (let i = 0; i < limit; i++) {
|
|
1931
|
+
const found = scanForOrigin(obj[i], depth + 1, ignoreTTL);
|
|
1913
1932
|
if (found) return found;
|
|
1914
1933
|
}
|
|
1934
|
+
return void 0;
|
|
1935
|
+
}
|
|
1936
|
+
for (const val of Object.values(obj)) {
|
|
1937
|
+
const found = scanForOrigin(val, depth + 1, ignoreTTL);
|
|
1938
|
+
if (found) return found;
|
|
1915
1939
|
}
|
|
1916
1940
|
return void 0;
|
|
1917
1941
|
}
|
|
@@ -3706,7 +3730,7 @@ function safeCall(fn, fallback) {
|
|
|
3706
3730
|
|
|
3707
3731
|
// src/valueTraceResolver.ts
|
|
3708
3732
|
var FIBER_TAG_CONTEXT_PROVIDER = 10;
|
|
3709
|
-
var BUDGET_MS =
|
|
3733
|
+
var BUDGET_MS = 100;
|
|
3710
3734
|
var SCAN_DEPTH = 3;
|
|
3711
3735
|
var MAX_PROP_CHAIN_DEPTH = 30;
|
|
3712
3736
|
function now() {
|
|
@@ -3730,34 +3754,49 @@ function getHookValueAt(fiber, hookIndex) {
|
|
|
3730
3754
|
if (!hook) return void 0;
|
|
3731
3755
|
return hook.memoizedState;
|
|
3732
3756
|
}
|
|
3733
|
-
function
|
|
3757
|
+
function cachedFp(value, cache) {
|
|
3758
|
+
if (value === null || typeof value !== "object") return valueFingerprint(value);
|
|
3759
|
+
const cached = cache.get(value);
|
|
3760
|
+
if (cached !== void 0) return cached;
|
|
3761
|
+
const fp = valueFingerprint(value);
|
|
3762
|
+
cache.set(value, fp);
|
|
3763
|
+
return fp;
|
|
3764
|
+
}
|
|
3765
|
+
function valuesMatch(target, targetFp, candidate, cache) {
|
|
3734
3766
|
const targetIsObject = target !== null && typeof target === "object";
|
|
3735
3767
|
const candidateIsObject = candidate !== null && typeof candidate === "object";
|
|
3736
3768
|
if (targetIsObject && candidateIsObject && target === candidate) return "exact";
|
|
3737
3769
|
if (!shouldFlagRename(target) || !shouldFlagRename(candidate)) return null;
|
|
3738
|
-
if (
|
|
3770
|
+
if (cachedFp(candidate, cache) === targetFp) return "fingerprint-match";
|
|
3739
3771
|
return null;
|
|
3740
3772
|
}
|
|
3741
|
-
function
|
|
3773
|
+
function findReferenceMatchAtTopLevel(target, container) {
|
|
3774
|
+
if (target === null || typeof target !== "object") return null;
|
|
3775
|
+
for (const key of Object.keys(container)) {
|
|
3776
|
+
if (container[key] === target) return key;
|
|
3777
|
+
}
|
|
3778
|
+
return null;
|
|
3779
|
+
}
|
|
3780
|
+
function findMatchingPathInObject(target, targetFp, container, currentPath, depth, deadline, cache) {
|
|
3742
3781
|
if (now() > deadline) return null;
|
|
3743
3782
|
if (depth > SCAN_DEPTH) return null;
|
|
3744
3783
|
if (container === null || typeof container !== "object") return null;
|
|
3745
|
-
const selfMatch = valuesMatch(target, targetFp, container);
|
|
3784
|
+
const selfMatch = valuesMatch(target, targetFp, container, cache);
|
|
3746
3785
|
if (selfMatch) return { path: [...currentPath], confidence: selfMatch };
|
|
3747
3786
|
if (Array.isArray(container)) {
|
|
3748
3787
|
for (let i = 0; i < Math.min(container.length, 50); i++) {
|
|
3749
3788
|
const child = container[i];
|
|
3750
|
-
const directMatch = valuesMatch(target, targetFp, child);
|
|
3789
|
+
const directMatch = valuesMatch(target, targetFp, child, cache);
|
|
3751
3790
|
if (directMatch) return { path: [...currentPath, String(i)], confidence: directMatch };
|
|
3752
|
-
const nested = findMatchingPathInObject(target, targetFp, child, [...currentPath, String(i)], depth + 1, deadline);
|
|
3791
|
+
const nested = findMatchingPathInObject(target, targetFp, child, [...currentPath, String(i)], depth + 1, deadline, cache);
|
|
3753
3792
|
if (nested) return nested;
|
|
3754
3793
|
}
|
|
3755
3794
|
} else {
|
|
3756
3795
|
for (const key of Object.keys(container)) {
|
|
3757
3796
|
const child = container[key];
|
|
3758
|
-
const directMatch = valuesMatch(target, targetFp, child);
|
|
3797
|
+
const directMatch = valuesMatch(target, targetFp, child, cache);
|
|
3759
3798
|
if (directMatch) return { path: [...currentPath, key], confidence: directMatch };
|
|
3760
|
-
const nested = findMatchingPathInObject(target, targetFp, child, [...currentPath, key], depth + 1, deadline);
|
|
3799
|
+
const nested = findMatchingPathInObject(target, targetFp, child, [...currentPath, key], depth + 1, deadline, cache);
|
|
3761
3800
|
if (nested) return nested;
|
|
3762
3801
|
}
|
|
3763
3802
|
}
|
|
@@ -3770,6 +3809,59 @@ function buildFiberToNodeIdMap() {
|
|
|
3770
3809
|
}
|
|
3771
3810
|
return reverse;
|
|
3772
3811
|
}
|
|
3812
|
+
function retreatToEnclosingObject(fiber, propPath, rootValue) {
|
|
3813
|
+
const isObject = rootValue !== null && typeof rootValue === "object";
|
|
3814
|
+
if (isObject || propPath.length <= 1 || !fiber.memoizedProps) {
|
|
3815
|
+
return { rootValue, trailingSubPath: [] };
|
|
3816
|
+
}
|
|
3817
|
+
let path = propPath.slice();
|
|
3818
|
+
let value = rootValue;
|
|
3819
|
+
const trail = [];
|
|
3820
|
+
while (path.length > 1 && (value === null || typeof value !== "object")) {
|
|
3821
|
+
trail.unshift(path[path.length - 1]);
|
|
3822
|
+
path = path.slice(0, -1);
|
|
3823
|
+
value = walkPath(fiber.memoizedProps, path);
|
|
3824
|
+
}
|
|
3825
|
+
if (value === null || typeof value !== "object") {
|
|
3826
|
+
return { rootValue, trailingSubPath: [] };
|
|
3827
|
+
}
|
|
3828
|
+
return { rootValue: value, trailingSubPath: trail };
|
|
3829
|
+
}
|
|
3830
|
+
function isDebugEnabled() {
|
|
3831
|
+
try {
|
|
3832
|
+
return !!globalThis.__FLOTRACE_DEBUG__;
|
|
3833
|
+
} catch {
|
|
3834
|
+
return false;
|
|
3835
|
+
}
|
|
3836
|
+
}
|
|
3837
|
+
function findFetchOriginUpKeyPath(stateRoot, keyPath) {
|
|
3838
|
+
const ancestors = [stateRoot];
|
|
3839
|
+
let cursor = stateRoot;
|
|
3840
|
+
for (const segment of keyPath) {
|
|
3841
|
+
if (cursor === null || typeof cursor !== "object") break;
|
|
3842
|
+
cursor = cursor[segment];
|
|
3843
|
+
ancestors.push(cursor);
|
|
3844
|
+
}
|
|
3845
|
+
for (let i = ancestors.length - 1; i >= 0; i--) {
|
|
3846
|
+
const value = ancestors[i];
|
|
3847
|
+
if (value === null || typeof value !== "object") continue;
|
|
3848
|
+
const rid = findFetchOrigin(value, { ignoreTTL: true });
|
|
3849
|
+
if (rid) {
|
|
3850
|
+
if (isDebugEnabled()) {
|
|
3851
|
+
console.debug("[FloTrace] origin via keyPath retreat", {
|
|
3852
|
+
keyPath,
|
|
3853
|
+
depthHit: i,
|
|
3854
|
+
requestId: rid
|
|
3855
|
+
});
|
|
3856
|
+
}
|
|
3857
|
+
return rid;
|
|
3858
|
+
}
|
|
3859
|
+
}
|
|
3860
|
+
return void 0;
|
|
3861
|
+
}
|
|
3862
|
+
function resolveOriginViaTagOrKeyPath(matchedValue, stateRoot, keyPath) {
|
|
3863
|
+
return findFetchOrigin(matchedValue, { ignoreTTL: true }) ?? findFetchOriginUpKeyPath(stateRoot, keyPath);
|
|
3864
|
+
}
|
|
3773
3865
|
function resolveValueTrace(input) {
|
|
3774
3866
|
const startedAt = now();
|
|
3775
3867
|
const deadline = startedAt + BUDGET_MS;
|
|
@@ -3798,9 +3890,13 @@ function resolveValueTrace(input) {
|
|
|
3798
3890
|
if (rootValue === void 0) {
|
|
3799
3891
|
return { ...base, error: "value-not-found", resolvedAtMs: now() };
|
|
3800
3892
|
}
|
|
3893
|
+
const retreated = input.propPath ? retreatToEnclosingObject(fiber, input.propPath, rootValue) : { rootValue, trailingSubPath: [] };
|
|
3894
|
+
rootValue = retreated.rootValue;
|
|
3895
|
+
const trailingSubPath = retreated.trailingSubPath;
|
|
3801
3896
|
const rootFp = valueFingerprint(rootValue);
|
|
3802
3897
|
const fiberToNodeId = buildFiberToNodeIdMap();
|
|
3803
3898
|
const rootComponentName = getComponentNameFromFiber(fiber) ?? "Unknown";
|
|
3899
|
+
const fpCache = /* @__PURE__ */ new WeakMap();
|
|
3804
3900
|
if (input.propPath) {
|
|
3805
3901
|
steps.push({
|
|
3806
3902
|
kind: "prop",
|
|
@@ -3829,8 +3925,17 @@ function resolveValueTrace(input) {
|
|
|
3829
3925
|
if (current.tag !== FIBER_TAG_CONTEXT_PROVIDER) {
|
|
3830
3926
|
const props = current.memoizedProps;
|
|
3831
3927
|
if (props) {
|
|
3832
|
-
const
|
|
3833
|
-
|
|
3928
|
+
const refKey = findReferenceMatchAtTopLevel(rootValue, props);
|
|
3929
|
+
let matchPath = refKey !== null ? [refKey] : null;
|
|
3930
|
+
let matchConfidence = "exact";
|
|
3931
|
+
if (matchPath === null) {
|
|
3932
|
+
const match = findMatchingPathInObject(rootValue, rootFp, props, [], 0, deadline, fpCache);
|
|
3933
|
+
if (match) {
|
|
3934
|
+
matchPath = match.path;
|
|
3935
|
+
matchConfidence = match.confidence;
|
|
3936
|
+
}
|
|
3937
|
+
}
|
|
3938
|
+
if (matchPath !== null) {
|
|
3834
3939
|
const ancestorNodeId = fiberToNodeId.get(current);
|
|
3835
3940
|
const ancestorName = getComponentNameFromFiber(current) ?? "Unknown";
|
|
3836
3941
|
if (ancestorNodeId) {
|
|
@@ -3838,10 +3943,28 @@ function resolveValueTrace(input) {
|
|
|
3838
3943
|
kind: "prop",
|
|
3839
3944
|
nodeId: ancestorNodeId,
|
|
3840
3945
|
componentName: ancestorName,
|
|
3841
|
-
propPath:
|
|
3842
|
-
confidence:
|
|
3946
|
+
propPath: trailingSubPath.length > 0 ? [...matchPath, ...trailingSubPath] : matchPath,
|
|
3947
|
+
confidence: matchConfidence
|
|
3843
3948
|
});
|
|
3844
3949
|
}
|
|
3950
|
+
} else {
|
|
3951
|
+
const hookMatch = findMatchingHookState(current, rootValue, rootFp, fpCache);
|
|
3952
|
+
if (hookMatch) {
|
|
3953
|
+
const ancestorNodeId = fiberToNodeId.get(current);
|
|
3954
|
+
const ancestorName = getComponentNameFromFiber(current) ?? "Unknown";
|
|
3955
|
+
if (ancestorNodeId) {
|
|
3956
|
+
steps.push({
|
|
3957
|
+
kind: "hook-state",
|
|
3958
|
+
nodeId: ancestorNodeId,
|
|
3959
|
+
componentName: ancestorName,
|
|
3960
|
+
hookIndex: hookMatch.hookIndex,
|
|
3961
|
+
hookType: hookMatch.hookType,
|
|
3962
|
+
subPath: trailingSubPath.length > 0 ? trailingSubPath : void 0,
|
|
3963
|
+
confidence: hookMatch.confidence
|
|
3964
|
+
});
|
|
3965
|
+
return { ...base, steps, resolvedAtMs: now() };
|
|
3966
|
+
}
|
|
3967
|
+
}
|
|
3845
3968
|
}
|
|
3846
3969
|
}
|
|
3847
3970
|
}
|
|
@@ -3850,7 +3973,7 @@ function resolveValueTrace(input) {
|
|
|
3850
3973
|
}
|
|
3851
3974
|
}
|
|
3852
3975
|
if (input.hookPath) {
|
|
3853
|
-
const origin = findFetchOrigin(rootValue);
|
|
3976
|
+
const origin = findFetchOrigin(rootValue, { ignoreTTL: true });
|
|
3854
3977
|
if (origin) {
|
|
3855
3978
|
steps.push({
|
|
3856
3979
|
kind: "api",
|
|
@@ -3862,15 +3985,15 @@ function resolveValueTrace(input) {
|
|
|
3862
3985
|
return { ...base, steps, resolvedAtMs: now() };
|
|
3863
3986
|
}
|
|
3864
3987
|
}
|
|
3865
|
-
const derivedMatch = findDerivationMatch(fiber, rootValue, rootFp, rootComponentName);
|
|
3988
|
+
const derivedMatch = findDerivationMatch(fiber, rootValue, rootFp, rootComponentName, fpCache);
|
|
3866
3989
|
if (derivedMatch) {
|
|
3867
3990
|
steps.push({ ...derivedMatch, nodeId: input.nodeId });
|
|
3868
3991
|
return { ...base, steps, resolvedAtMs: now() };
|
|
3869
3992
|
}
|
|
3870
|
-
const contextMatch = findContextMatch(fiber, rootValue, rootFp, fiberToNodeId);
|
|
3993
|
+
const contextMatch = findContextMatch(fiber, rootValue, rootFp, fiberToNodeId, fpCache);
|
|
3871
3994
|
if (contextMatch) {
|
|
3872
3995
|
steps.push(contextMatch.step);
|
|
3873
|
-
const providerStoreMatch = findStoreMatch(contextMatch.providerValue,
|
|
3996
|
+
const providerStoreMatch = findStoreMatch(contextMatch.providerValue, cachedFp(contextMatch.providerValue, fpCache), deadline, fpCache);
|
|
3874
3997
|
if (providerStoreMatch) {
|
|
3875
3998
|
steps.push({
|
|
3876
3999
|
kind: "store",
|
|
@@ -3879,19 +4002,23 @@ function resolveValueTrace(input) {
|
|
|
3879
4002
|
keyPath: providerStoreMatch.keyPath,
|
|
3880
4003
|
confidence: providerStoreMatch.confidence
|
|
3881
4004
|
});
|
|
3882
|
-
const origin =
|
|
4005
|
+
const origin = resolveOriginViaTagOrKeyPath(
|
|
4006
|
+
providerStoreMatch.matchedValue,
|
|
4007
|
+
providerStoreMatch.stateRoot,
|
|
4008
|
+
providerStoreMatch.keyPath
|
|
4009
|
+
);
|
|
3883
4010
|
if (origin) {
|
|
3884
4011
|
steps.push({ kind: "api", requestId: origin, method: "UNKNOWN", urlPath: "", ageMs: 0 });
|
|
3885
4012
|
}
|
|
3886
4013
|
} else {
|
|
3887
|
-
const origin = findFetchOrigin(contextMatch.providerValue);
|
|
4014
|
+
const origin = findFetchOrigin(contextMatch.providerValue, { ignoreTTL: true });
|
|
3888
4015
|
if (origin) {
|
|
3889
4016
|
steps.push({ kind: "api", requestId: origin, method: "UNKNOWN", urlPath: "", ageMs: 0 });
|
|
3890
4017
|
}
|
|
3891
4018
|
}
|
|
3892
4019
|
return { ...base, steps, resolvedAtMs: now() };
|
|
3893
4020
|
}
|
|
3894
|
-
const storeMatch = findStoreMatch(rootValue, rootFp, deadline);
|
|
4021
|
+
const storeMatch = findStoreMatch(rootValue, rootFp, deadline, fpCache);
|
|
3895
4022
|
if (storeMatch) {
|
|
3896
4023
|
steps.push({
|
|
3897
4024
|
kind: "store",
|
|
@@ -3900,7 +4027,11 @@ function resolveValueTrace(input) {
|
|
|
3900
4027
|
keyPath: storeMatch.keyPath,
|
|
3901
4028
|
confidence: storeMatch.confidence
|
|
3902
4029
|
});
|
|
3903
|
-
const origin =
|
|
4030
|
+
const origin = resolveOriginViaTagOrKeyPath(
|
|
4031
|
+
storeMatch.matchedValue,
|
|
4032
|
+
storeMatch.stateRoot,
|
|
4033
|
+
storeMatch.keyPath
|
|
4034
|
+
);
|
|
3904
4035
|
if (origin) {
|
|
3905
4036
|
steps.push({
|
|
3906
4037
|
kind: "api",
|
|
@@ -3913,12 +4044,12 @@ function resolveValueTrace(input) {
|
|
|
3913
4044
|
}
|
|
3914
4045
|
return { ...base, steps, resolvedAtMs: now() };
|
|
3915
4046
|
}
|
|
3916
|
-
function findContextMatch(consumer, target, targetFp, fiberToNodeId) {
|
|
4047
|
+
function findContextMatch(consumer, target, targetFp, fiberToNodeId, cache) {
|
|
3917
4048
|
const deps = consumer.dependencies?.firstContext;
|
|
3918
4049
|
if (!deps) return null;
|
|
3919
4050
|
let dep = deps;
|
|
3920
4051
|
while (dep) {
|
|
3921
|
-
const match = valuesMatch(target, targetFp, dep.memoizedValue);
|
|
4052
|
+
const match = valuesMatch(target, targetFp, dep.memoizedValue, cache);
|
|
3922
4053
|
if (match) {
|
|
3923
4054
|
const provider = findNearestProvider(consumer, dep.context);
|
|
3924
4055
|
const step = {
|
|
@@ -3934,14 +4065,14 @@ function findContextMatch(consumer, target, targetFp, fiberToNodeId) {
|
|
|
3934
4065
|
}
|
|
3935
4066
|
return null;
|
|
3936
4067
|
}
|
|
3937
|
-
function findDerivationMatch(fiber, target, targetFp, componentName) {
|
|
4068
|
+
function findDerivationMatch(fiber, target, targetFp, componentName, cache) {
|
|
3938
4069
|
let hook = fiber.memoizedState;
|
|
3939
4070
|
let index = 0;
|
|
3940
4071
|
while (hook) {
|
|
3941
4072
|
const ms = hook.memoizedState;
|
|
3942
4073
|
if (Array.isArray(ms) && ms.length === 2 && Array.isArray(ms[1])) {
|
|
3943
4074
|
const [computed, deps] = ms;
|
|
3944
|
-
const match = valuesMatch(target, targetFp, computed);
|
|
4075
|
+
const match = valuesMatch(target, targetFp, computed, cache);
|
|
3945
4076
|
if (match) {
|
|
3946
4077
|
const hookType = typeof computed === "function" ? "useCallback" : "useMemo";
|
|
3947
4078
|
return {
|
|
@@ -3960,6 +4091,25 @@ function findDerivationMatch(fiber, target, targetFp, componentName) {
|
|
|
3960
4091
|
}
|
|
3961
4092
|
return null;
|
|
3962
4093
|
}
|
|
4094
|
+
function findMatchingHookState(fiber, target, targetFp, cache) {
|
|
4095
|
+
let hook = fiber.memoizedState;
|
|
4096
|
+
let index = 0;
|
|
4097
|
+
while (hook) {
|
|
4098
|
+
const ms = hook.memoizedState;
|
|
4099
|
+
const isMemoTuple = Array.isArray(ms) && ms.length === 2 && Array.isArray(ms[1]);
|
|
4100
|
+
const isEffectShape2 = ms !== null && typeof ms === "object" && "create" in ms && "deps" in ms;
|
|
4101
|
+
const isRefShape = ms !== null && typeof ms === "object" && Object.keys(ms).length === 1 && "current" in ms;
|
|
4102
|
+
if (!isMemoTuple && !isEffectShape2 && !isRefShape) {
|
|
4103
|
+
const match = valuesMatch(target, targetFp, ms, cache);
|
|
4104
|
+
if (match) {
|
|
4105
|
+
return { hookIndex: index, hookType: "useState", confidence: match };
|
|
4106
|
+
}
|
|
4107
|
+
}
|
|
4108
|
+
hook = hook.next;
|
|
4109
|
+
index++;
|
|
4110
|
+
}
|
|
4111
|
+
return null;
|
|
4112
|
+
}
|
|
3963
4113
|
function findNearestProvider(consumer, contextObj) {
|
|
3964
4114
|
let current = consumer.return;
|
|
3965
4115
|
let hops = 0;
|
|
@@ -3973,44 +4123,47 @@ function findNearestProvider(consumer, contextObj) {
|
|
|
3973
4123
|
}
|
|
3974
4124
|
return null;
|
|
3975
4125
|
}
|
|
3976
|
-
function findStoreMatch(target, targetFp, deadline) {
|
|
4126
|
+
function findStoreMatch(target, targetFp, deadline, cache) {
|
|
3977
4127
|
for (const [storeName, state] of getZustandSnapshot()) {
|
|
3978
4128
|
if (now() > deadline) return null;
|
|
3979
|
-
const hit = findMatchingPathInObject(target, targetFp, state, [], 0, deadline);
|
|
4129
|
+
const hit = findMatchingPathInObject(target, targetFp, state, [], 0, deadline, cache);
|
|
3980
4130
|
if (hit) {
|
|
3981
4131
|
return {
|
|
3982
4132
|
source: "zustand",
|
|
3983
4133
|
storeName,
|
|
3984
4134
|
keyPath: hit.path,
|
|
3985
4135
|
confidence: hit.confidence,
|
|
3986
|
-
matchedValue: walkPath(state, hit.path)
|
|
4136
|
+
matchedValue: walkPath(state, hit.path),
|
|
4137
|
+
stateRoot: state
|
|
3987
4138
|
};
|
|
3988
4139
|
}
|
|
3989
4140
|
}
|
|
3990
4141
|
const redux = getReduxSnapshot();
|
|
3991
4142
|
if (redux) {
|
|
3992
4143
|
if (now() > deadline) return null;
|
|
3993
|
-
const hit = findMatchingPathInObject(target, targetFp, redux, [], 0, deadline);
|
|
4144
|
+
const hit = findMatchingPathInObject(target, targetFp, redux, [], 0, deadline, cache);
|
|
3994
4145
|
if (hit) {
|
|
3995
4146
|
return {
|
|
3996
4147
|
source: "redux",
|
|
3997
4148
|
storeName: "redux",
|
|
3998
4149
|
keyPath: hit.path,
|
|
3999
4150
|
confidence: hit.confidence,
|
|
4000
|
-
matchedValue: walkPath(redux, hit.path)
|
|
4151
|
+
matchedValue: walkPath(redux, hit.path),
|
|
4152
|
+
stateRoot: redux
|
|
4001
4153
|
};
|
|
4002
4154
|
}
|
|
4003
4155
|
}
|
|
4004
4156
|
for (const [queryHash, entry] of getTanstackSnapshot()) {
|
|
4005
4157
|
if (now() > deadline) return null;
|
|
4006
|
-
const hit = findMatchingPathInObject(target, targetFp, entry.data, [], 0, deadline);
|
|
4158
|
+
const hit = findMatchingPathInObject(target, targetFp, entry.data, [], 0, deadline, cache);
|
|
4007
4159
|
if (hit) {
|
|
4008
4160
|
return {
|
|
4009
4161
|
source: "tanstack-query",
|
|
4010
4162
|
storeName: queryHash,
|
|
4011
4163
|
keyPath: hit.path,
|
|
4012
4164
|
confidence: hit.confidence,
|
|
4013
|
-
matchedValue: walkPath(entry.data, hit.path)
|
|
4165
|
+
matchedValue: walkPath(entry.data, hit.path),
|
|
4166
|
+
stateRoot: entry.data
|
|
4014
4167
|
};
|
|
4015
4168
|
}
|
|
4016
4169
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
var DEFAULT_CONFIG = {
|
|
3
3
|
port: 3457,
|
|
4
4
|
appName: "React App",
|
|
5
|
-
|
|
5
|
+
// Default-on unless an explicit `process.env.NODE_ENV === 'production'` is
|
|
6
|
+
// detected. The previous heuristic (`=== 'development'`) silently disabled
|
|
7
|
+
// the runtime in any browser context that doesn't shim `process` (e.g. Vite,
|
|
8
|
+
// Webpack 5 with `node: false`, Rsbuild) — making the README quickstart fail
|
|
9
|
+
// for everyone. Production safety is handled by users gating the import via
|
|
10
|
+
// the dynamic-import pattern documented in the runtime READMEs.
|
|
11
|
+
enabled: globalThis.process?.env?.NODE_ENV !== "production",
|
|
6
12
|
autoReconnect: true,
|
|
7
13
|
reconnectInterval: 2e3,
|
|
8
14
|
trackAllRenders: true,
|
|
@@ -227,7 +233,8 @@ var _FloTraceWebSocketClient = class _FloTraceWebSocketClient {
|
|
|
227
233
|
appVersion: this.config.appVersion,
|
|
228
234
|
frameworkName: this.config.frameworkName,
|
|
229
235
|
frameworkVersion: this.config.frameworkVersion,
|
|
230
|
-
reactNativeVersion: this.config.reactNativeVersion
|
|
236
|
+
reactNativeVersion: this.config.reactNativeVersion,
|
|
237
|
+
runtimeVersion: this.config.runtimeVersion
|
|
231
238
|
});
|
|
232
239
|
this.flush();
|
|
233
240
|
};
|
|
@@ -1758,20 +1765,23 @@ function extractRoute(url) {
|
|
|
1758
1765
|
var originalFetch = null;
|
|
1759
1766
|
var interceptorClient = null;
|
|
1760
1767
|
var isInstalled2 = false;
|
|
1768
|
+
var patchedFetchRef = null;
|
|
1761
1769
|
function installRscPayloadInterceptor(client2) {
|
|
1762
1770
|
if (isInstalled2 || typeof globalThis.fetch !== "function") return;
|
|
1763
1771
|
isInstalled2 = true;
|
|
1764
1772
|
interceptorClient = client2;
|
|
1765
1773
|
originalFetch = globalThis.fetch;
|
|
1766
|
-
|
|
1774
|
+
const capturedOriginalFetch = originalFetch;
|
|
1775
|
+
const capturedClient = client2;
|
|
1776
|
+
const patchedFetch = async function patchedFetch2(input, init) {
|
|
1767
1777
|
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
1768
1778
|
const isRscRequest = RSC_URL_PATTERNS.some((p) => p.test(url));
|
|
1769
|
-
const response = await
|
|
1770
|
-
if (isRscRequest && interceptorClient
|
|
1779
|
+
const response = await capturedOriginalFetch.call(globalThis, input, init);
|
|
1780
|
+
if (isRscRequest && interceptorClient === capturedClient && capturedClient.connected) {
|
|
1771
1781
|
try {
|
|
1772
1782
|
const sizeHeader = response.headers.get("content-length");
|
|
1773
1783
|
const payloadSizeBytes = sizeHeader ? parseInt(sizeHeader, 10) : 0;
|
|
1774
|
-
|
|
1784
|
+
capturedClient.send({
|
|
1775
1785
|
type: "runtime:rscPayload",
|
|
1776
1786
|
route: extractRoute(url),
|
|
1777
1787
|
payloadSizeBytes: isNaN(payloadSizeBytes) ? 0 : payloadSizeBytes,
|
|
@@ -1783,11 +1793,16 @@ function installRscPayloadInterceptor(client2) {
|
|
|
1783
1793
|
}
|
|
1784
1794
|
return response;
|
|
1785
1795
|
};
|
|
1796
|
+
patchedFetchRef = patchedFetch;
|
|
1797
|
+
globalThis.fetch = patchedFetch;
|
|
1786
1798
|
}
|
|
1787
1799
|
function uninstallRscPayloadInterceptor() {
|
|
1788
1800
|
if (!isInstalled2 || !originalFetch) return;
|
|
1789
|
-
globalThis.fetch
|
|
1801
|
+
if (globalThis.fetch === patchedFetchRef) {
|
|
1802
|
+
globalThis.fetch = originalFetch;
|
|
1803
|
+
}
|
|
1790
1804
|
originalFetch = null;
|
|
1805
|
+
patchedFetchRef = null;
|
|
1791
1806
|
interceptorClient = null;
|
|
1792
1807
|
isInstalled2 = false;
|
|
1793
1808
|
}
|
|
@@ -1796,12 +1811,16 @@ function uninstallRscPayloadInterceptor() {
|
|
|
1796
1811
|
var fetchDataOrigin = /* @__PURE__ */ new WeakMap();
|
|
1797
1812
|
var requestTagTimestamps = /* @__PURE__ */ new Map();
|
|
1798
1813
|
var FETCH_ORIGIN_TTL_MS = 3e3;
|
|
1814
|
+
var FETCH_ORIGIN_SCAN_DEPTH = 4;
|
|
1815
|
+
var FETCH_ORIGIN_TAG_ARRAY_LIMIT = 50;
|
|
1816
|
+
var FETCH_ORIGIN_SCAN_ARRAY_LIMIT = 20;
|
|
1799
1817
|
function tagFetchData(obj, requestId, depth = 0) {
|
|
1800
|
-
if (depth >
|
|
1818
|
+
if (depth > FETCH_ORIGIN_SCAN_DEPTH || obj === null || typeof obj !== "object") return;
|
|
1801
1819
|
fetchDataOrigin.set(obj, requestId);
|
|
1802
1820
|
if (depth === 0) requestTagTimestamps.set(requestId, Date.now());
|
|
1803
1821
|
if (Array.isArray(obj)) {
|
|
1804
|
-
|
|
1822
|
+
const limit = Math.min(obj.length, FETCH_ORIGIN_TAG_ARRAY_LIMIT);
|
|
1823
|
+
for (let i = 0; i < limit; i++) tagFetchData(obj[i], requestId, depth + 1);
|
|
1805
1824
|
} else {
|
|
1806
1825
|
for (const val of Object.values(obj)) tagFetchData(val, requestId, depth + 1);
|
|
1807
1826
|
}
|
|
@@ -1812,24 +1831,29 @@ function hasActiveTags() {
|
|
|
1812
1831
|
function clearFetchOriginTags() {
|
|
1813
1832
|
requestTagTimestamps.clear();
|
|
1814
1833
|
}
|
|
1815
|
-
function findFetchOrigin(obj,
|
|
1816
|
-
|
|
1834
|
+
function findFetchOrigin(obj, options) {
|
|
1835
|
+
return scanForOrigin(obj, 0, options?.ignoreTTL === true);
|
|
1836
|
+
}
|
|
1837
|
+
function scanForOrigin(obj, depth, ignoreTTL) {
|
|
1838
|
+
if (depth > FETCH_ORIGIN_SCAN_DEPTH || obj === null || typeof obj !== "object") return void 0;
|
|
1817
1839
|
const rid = fetchDataOrigin.get(obj);
|
|
1818
1840
|
if (rid) {
|
|
1841
|
+
if (ignoreTTL) return rid;
|
|
1819
1842
|
const tagTime = requestTagTimestamps.get(rid);
|
|
1820
1843
|
if (tagTime && Date.now() - tagTime <= FETCH_ORIGIN_TTL_MS) return rid;
|
|
1821
1844
|
requestTagTimestamps.delete(rid);
|
|
1822
1845
|
}
|
|
1823
1846
|
if (Array.isArray(obj)) {
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
}
|
|
1828
|
-
} else {
|
|
1829
|
-
for (const val of Object.values(obj)) {
|
|
1830
|
-
const found = findFetchOrigin(val, depth + 1);
|
|
1847
|
+
const limit = Math.min(obj.length, FETCH_ORIGIN_SCAN_ARRAY_LIMIT);
|
|
1848
|
+
for (let i = 0; i < limit; i++) {
|
|
1849
|
+
const found = scanForOrigin(obj[i], depth + 1, ignoreTTL);
|
|
1831
1850
|
if (found) return found;
|
|
1832
1851
|
}
|
|
1852
|
+
return void 0;
|
|
1853
|
+
}
|
|
1854
|
+
for (const val of Object.values(obj)) {
|
|
1855
|
+
const found = scanForOrigin(val, depth + 1, ignoreTTL);
|
|
1856
|
+
if (found) return found;
|
|
1833
1857
|
}
|
|
1834
1858
|
return void 0;
|
|
1835
1859
|
}
|
|
@@ -3624,7 +3648,7 @@ function safeCall(fn, fallback) {
|
|
|
3624
3648
|
|
|
3625
3649
|
// src/valueTraceResolver.ts
|
|
3626
3650
|
var FIBER_TAG_CONTEXT_PROVIDER = 10;
|
|
3627
|
-
var BUDGET_MS =
|
|
3651
|
+
var BUDGET_MS = 100;
|
|
3628
3652
|
var SCAN_DEPTH = 3;
|
|
3629
3653
|
var MAX_PROP_CHAIN_DEPTH = 30;
|
|
3630
3654
|
function now() {
|
|
@@ -3648,34 +3672,49 @@ function getHookValueAt(fiber, hookIndex) {
|
|
|
3648
3672
|
if (!hook) return void 0;
|
|
3649
3673
|
return hook.memoizedState;
|
|
3650
3674
|
}
|
|
3651
|
-
function
|
|
3675
|
+
function cachedFp(value, cache) {
|
|
3676
|
+
if (value === null || typeof value !== "object") return valueFingerprint(value);
|
|
3677
|
+
const cached = cache.get(value);
|
|
3678
|
+
if (cached !== void 0) return cached;
|
|
3679
|
+
const fp = valueFingerprint(value);
|
|
3680
|
+
cache.set(value, fp);
|
|
3681
|
+
return fp;
|
|
3682
|
+
}
|
|
3683
|
+
function valuesMatch(target, targetFp, candidate, cache) {
|
|
3652
3684
|
const targetIsObject = target !== null && typeof target === "object";
|
|
3653
3685
|
const candidateIsObject = candidate !== null && typeof candidate === "object";
|
|
3654
3686
|
if (targetIsObject && candidateIsObject && target === candidate) return "exact";
|
|
3655
3687
|
if (!shouldFlagRename(target) || !shouldFlagRename(candidate)) return null;
|
|
3656
|
-
if (
|
|
3688
|
+
if (cachedFp(candidate, cache) === targetFp) return "fingerprint-match";
|
|
3657
3689
|
return null;
|
|
3658
3690
|
}
|
|
3659
|
-
function
|
|
3691
|
+
function findReferenceMatchAtTopLevel(target, container) {
|
|
3692
|
+
if (target === null || typeof target !== "object") return null;
|
|
3693
|
+
for (const key of Object.keys(container)) {
|
|
3694
|
+
if (container[key] === target) return key;
|
|
3695
|
+
}
|
|
3696
|
+
return null;
|
|
3697
|
+
}
|
|
3698
|
+
function findMatchingPathInObject(target, targetFp, container, currentPath, depth, deadline, cache) {
|
|
3660
3699
|
if (now() > deadline) return null;
|
|
3661
3700
|
if (depth > SCAN_DEPTH) return null;
|
|
3662
3701
|
if (container === null || typeof container !== "object") return null;
|
|
3663
|
-
const selfMatch = valuesMatch(target, targetFp, container);
|
|
3702
|
+
const selfMatch = valuesMatch(target, targetFp, container, cache);
|
|
3664
3703
|
if (selfMatch) return { path: [...currentPath], confidence: selfMatch };
|
|
3665
3704
|
if (Array.isArray(container)) {
|
|
3666
3705
|
for (let i = 0; i < Math.min(container.length, 50); i++) {
|
|
3667
3706
|
const child = container[i];
|
|
3668
|
-
const directMatch = valuesMatch(target, targetFp, child);
|
|
3707
|
+
const directMatch = valuesMatch(target, targetFp, child, cache);
|
|
3669
3708
|
if (directMatch) return { path: [...currentPath, String(i)], confidence: directMatch };
|
|
3670
|
-
const nested = findMatchingPathInObject(target, targetFp, child, [...currentPath, String(i)], depth + 1, deadline);
|
|
3709
|
+
const nested = findMatchingPathInObject(target, targetFp, child, [...currentPath, String(i)], depth + 1, deadline, cache);
|
|
3671
3710
|
if (nested) return nested;
|
|
3672
3711
|
}
|
|
3673
3712
|
} else {
|
|
3674
3713
|
for (const key of Object.keys(container)) {
|
|
3675
3714
|
const child = container[key];
|
|
3676
|
-
const directMatch = valuesMatch(target, targetFp, child);
|
|
3715
|
+
const directMatch = valuesMatch(target, targetFp, child, cache);
|
|
3677
3716
|
if (directMatch) return { path: [...currentPath, key], confidence: directMatch };
|
|
3678
|
-
const nested = findMatchingPathInObject(target, targetFp, child, [...currentPath, key], depth + 1, deadline);
|
|
3717
|
+
const nested = findMatchingPathInObject(target, targetFp, child, [...currentPath, key], depth + 1, deadline, cache);
|
|
3679
3718
|
if (nested) return nested;
|
|
3680
3719
|
}
|
|
3681
3720
|
}
|
|
@@ -3688,6 +3727,59 @@ function buildFiberToNodeIdMap() {
|
|
|
3688
3727
|
}
|
|
3689
3728
|
return reverse;
|
|
3690
3729
|
}
|
|
3730
|
+
function retreatToEnclosingObject(fiber, propPath, rootValue) {
|
|
3731
|
+
const isObject = rootValue !== null && typeof rootValue === "object";
|
|
3732
|
+
if (isObject || propPath.length <= 1 || !fiber.memoizedProps) {
|
|
3733
|
+
return { rootValue, trailingSubPath: [] };
|
|
3734
|
+
}
|
|
3735
|
+
let path = propPath.slice();
|
|
3736
|
+
let value = rootValue;
|
|
3737
|
+
const trail = [];
|
|
3738
|
+
while (path.length > 1 && (value === null || typeof value !== "object")) {
|
|
3739
|
+
trail.unshift(path[path.length - 1]);
|
|
3740
|
+
path = path.slice(0, -1);
|
|
3741
|
+
value = walkPath(fiber.memoizedProps, path);
|
|
3742
|
+
}
|
|
3743
|
+
if (value === null || typeof value !== "object") {
|
|
3744
|
+
return { rootValue, trailingSubPath: [] };
|
|
3745
|
+
}
|
|
3746
|
+
return { rootValue: value, trailingSubPath: trail };
|
|
3747
|
+
}
|
|
3748
|
+
function isDebugEnabled() {
|
|
3749
|
+
try {
|
|
3750
|
+
return !!globalThis.__FLOTRACE_DEBUG__;
|
|
3751
|
+
} catch {
|
|
3752
|
+
return false;
|
|
3753
|
+
}
|
|
3754
|
+
}
|
|
3755
|
+
function findFetchOriginUpKeyPath(stateRoot, keyPath) {
|
|
3756
|
+
const ancestors = [stateRoot];
|
|
3757
|
+
let cursor = stateRoot;
|
|
3758
|
+
for (const segment of keyPath) {
|
|
3759
|
+
if (cursor === null || typeof cursor !== "object") break;
|
|
3760
|
+
cursor = cursor[segment];
|
|
3761
|
+
ancestors.push(cursor);
|
|
3762
|
+
}
|
|
3763
|
+
for (let i = ancestors.length - 1; i >= 0; i--) {
|
|
3764
|
+
const value = ancestors[i];
|
|
3765
|
+
if (value === null || typeof value !== "object") continue;
|
|
3766
|
+
const rid = findFetchOrigin(value, { ignoreTTL: true });
|
|
3767
|
+
if (rid) {
|
|
3768
|
+
if (isDebugEnabled()) {
|
|
3769
|
+
console.debug("[FloTrace] origin via keyPath retreat", {
|
|
3770
|
+
keyPath,
|
|
3771
|
+
depthHit: i,
|
|
3772
|
+
requestId: rid
|
|
3773
|
+
});
|
|
3774
|
+
}
|
|
3775
|
+
return rid;
|
|
3776
|
+
}
|
|
3777
|
+
}
|
|
3778
|
+
return void 0;
|
|
3779
|
+
}
|
|
3780
|
+
function resolveOriginViaTagOrKeyPath(matchedValue, stateRoot, keyPath) {
|
|
3781
|
+
return findFetchOrigin(matchedValue, { ignoreTTL: true }) ?? findFetchOriginUpKeyPath(stateRoot, keyPath);
|
|
3782
|
+
}
|
|
3691
3783
|
function resolveValueTrace(input) {
|
|
3692
3784
|
const startedAt = now();
|
|
3693
3785
|
const deadline = startedAt + BUDGET_MS;
|
|
@@ -3716,9 +3808,13 @@ function resolveValueTrace(input) {
|
|
|
3716
3808
|
if (rootValue === void 0) {
|
|
3717
3809
|
return { ...base, error: "value-not-found", resolvedAtMs: now() };
|
|
3718
3810
|
}
|
|
3811
|
+
const retreated = input.propPath ? retreatToEnclosingObject(fiber, input.propPath, rootValue) : { rootValue, trailingSubPath: [] };
|
|
3812
|
+
rootValue = retreated.rootValue;
|
|
3813
|
+
const trailingSubPath = retreated.trailingSubPath;
|
|
3719
3814
|
const rootFp = valueFingerprint(rootValue);
|
|
3720
3815
|
const fiberToNodeId = buildFiberToNodeIdMap();
|
|
3721
3816
|
const rootComponentName = getComponentNameFromFiber(fiber) ?? "Unknown";
|
|
3817
|
+
const fpCache = /* @__PURE__ */ new WeakMap();
|
|
3722
3818
|
if (input.propPath) {
|
|
3723
3819
|
steps.push({
|
|
3724
3820
|
kind: "prop",
|
|
@@ -3747,8 +3843,17 @@ function resolveValueTrace(input) {
|
|
|
3747
3843
|
if (current.tag !== FIBER_TAG_CONTEXT_PROVIDER) {
|
|
3748
3844
|
const props = current.memoizedProps;
|
|
3749
3845
|
if (props) {
|
|
3750
|
-
const
|
|
3751
|
-
|
|
3846
|
+
const refKey = findReferenceMatchAtTopLevel(rootValue, props);
|
|
3847
|
+
let matchPath = refKey !== null ? [refKey] : null;
|
|
3848
|
+
let matchConfidence = "exact";
|
|
3849
|
+
if (matchPath === null) {
|
|
3850
|
+
const match = findMatchingPathInObject(rootValue, rootFp, props, [], 0, deadline, fpCache);
|
|
3851
|
+
if (match) {
|
|
3852
|
+
matchPath = match.path;
|
|
3853
|
+
matchConfidence = match.confidence;
|
|
3854
|
+
}
|
|
3855
|
+
}
|
|
3856
|
+
if (matchPath !== null) {
|
|
3752
3857
|
const ancestorNodeId = fiberToNodeId.get(current);
|
|
3753
3858
|
const ancestorName = getComponentNameFromFiber(current) ?? "Unknown";
|
|
3754
3859
|
if (ancestorNodeId) {
|
|
@@ -3756,10 +3861,28 @@ function resolveValueTrace(input) {
|
|
|
3756
3861
|
kind: "prop",
|
|
3757
3862
|
nodeId: ancestorNodeId,
|
|
3758
3863
|
componentName: ancestorName,
|
|
3759
|
-
propPath:
|
|
3760
|
-
confidence:
|
|
3864
|
+
propPath: trailingSubPath.length > 0 ? [...matchPath, ...trailingSubPath] : matchPath,
|
|
3865
|
+
confidence: matchConfidence
|
|
3761
3866
|
});
|
|
3762
3867
|
}
|
|
3868
|
+
} else {
|
|
3869
|
+
const hookMatch = findMatchingHookState(current, rootValue, rootFp, fpCache);
|
|
3870
|
+
if (hookMatch) {
|
|
3871
|
+
const ancestorNodeId = fiberToNodeId.get(current);
|
|
3872
|
+
const ancestorName = getComponentNameFromFiber(current) ?? "Unknown";
|
|
3873
|
+
if (ancestorNodeId) {
|
|
3874
|
+
steps.push({
|
|
3875
|
+
kind: "hook-state",
|
|
3876
|
+
nodeId: ancestorNodeId,
|
|
3877
|
+
componentName: ancestorName,
|
|
3878
|
+
hookIndex: hookMatch.hookIndex,
|
|
3879
|
+
hookType: hookMatch.hookType,
|
|
3880
|
+
subPath: trailingSubPath.length > 0 ? trailingSubPath : void 0,
|
|
3881
|
+
confidence: hookMatch.confidence
|
|
3882
|
+
});
|
|
3883
|
+
return { ...base, steps, resolvedAtMs: now() };
|
|
3884
|
+
}
|
|
3885
|
+
}
|
|
3763
3886
|
}
|
|
3764
3887
|
}
|
|
3765
3888
|
}
|
|
@@ -3768,7 +3891,7 @@ function resolveValueTrace(input) {
|
|
|
3768
3891
|
}
|
|
3769
3892
|
}
|
|
3770
3893
|
if (input.hookPath) {
|
|
3771
|
-
const origin = findFetchOrigin(rootValue);
|
|
3894
|
+
const origin = findFetchOrigin(rootValue, { ignoreTTL: true });
|
|
3772
3895
|
if (origin) {
|
|
3773
3896
|
steps.push({
|
|
3774
3897
|
kind: "api",
|
|
@@ -3780,15 +3903,15 @@ function resolveValueTrace(input) {
|
|
|
3780
3903
|
return { ...base, steps, resolvedAtMs: now() };
|
|
3781
3904
|
}
|
|
3782
3905
|
}
|
|
3783
|
-
const derivedMatch = findDerivationMatch(fiber, rootValue, rootFp, rootComponentName);
|
|
3906
|
+
const derivedMatch = findDerivationMatch(fiber, rootValue, rootFp, rootComponentName, fpCache);
|
|
3784
3907
|
if (derivedMatch) {
|
|
3785
3908
|
steps.push({ ...derivedMatch, nodeId: input.nodeId });
|
|
3786
3909
|
return { ...base, steps, resolvedAtMs: now() };
|
|
3787
3910
|
}
|
|
3788
|
-
const contextMatch = findContextMatch(fiber, rootValue, rootFp, fiberToNodeId);
|
|
3911
|
+
const contextMatch = findContextMatch(fiber, rootValue, rootFp, fiberToNodeId, fpCache);
|
|
3789
3912
|
if (contextMatch) {
|
|
3790
3913
|
steps.push(contextMatch.step);
|
|
3791
|
-
const providerStoreMatch = findStoreMatch(contextMatch.providerValue,
|
|
3914
|
+
const providerStoreMatch = findStoreMatch(contextMatch.providerValue, cachedFp(contextMatch.providerValue, fpCache), deadline, fpCache);
|
|
3792
3915
|
if (providerStoreMatch) {
|
|
3793
3916
|
steps.push({
|
|
3794
3917
|
kind: "store",
|
|
@@ -3797,19 +3920,23 @@ function resolveValueTrace(input) {
|
|
|
3797
3920
|
keyPath: providerStoreMatch.keyPath,
|
|
3798
3921
|
confidence: providerStoreMatch.confidence
|
|
3799
3922
|
});
|
|
3800
|
-
const origin =
|
|
3923
|
+
const origin = resolveOriginViaTagOrKeyPath(
|
|
3924
|
+
providerStoreMatch.matchedValue,
|
|
3925
|
+
providerStoreMatch.stateRoot,
|
|
3926
|
+
providerStoreMatch.keyPath
|
|
3927
|
+
);
|
|
3801
3928
|
if (origin) {
|
|
3802
3929
|
steps.push({ kind: "api", requestId: origin, method: "UNKNOWN", urlPath: "", ageMs: 0 });
|
|
3803
3930
|
}
|
|
3804
3931
|
} else {
|
|
3805
|
-
const origin = findFetchOrigin(contextMatch.providerValue);
|
|
3932
|
+
const origin = findFetchOrigin(contextMatch.providerValue, { ignoreTTL: true });
|
|
3806
3933
|
if (origin) {
|
|
3807
3934
|
steps.push({ kind: "api", requestId: origin, method: "UNKNOWN", urlPath: "", ageMs: 0 });
|
|
3808
3935
|
}
|
|
3809
3936
|
}
|
|
3810
3937
|
return { ...base, steps, resolvedAtMs: now() };
|
|
3811
3938
|
}
|
|
3812
|
-
const storeMatch = findStoreMatch(rootValue, rootFp, deadline);
|
|
3939
|
+
const storeMatch = findStoreMatch(rootValue, rootFp, deadline, fpCache);
|
|
3813
3940
|
if (storeMatch) {
|
|
3814
3941
|
steps.push({
|
|
3815
3942
|
kind: "store",
|
|
@@ -3818,7 +3945,11 @@ function resolveValueTrace(input) {
|
|
|
3818
3945
|
keyPath: storeMatch.keyPath,
|
|
3819
3946
|
confidence: storeMatch.confidence
|
|
3820
3947
|
});
|
|
3821
|
-
const origin =
|
|
3948
|
+
const origin = resolveOriginViaTagOrKeyPath(
|
|
3949
|
+
storeMatch.matchedValue,
|
|
3950
|
+
storeMatch.stateRoot,
|
|
3951
|
+
storeMatch.keyPath
|
|
3952
|
+
);
|
|
3822
3953
|
if (origin) {
|
|
3823
3954
|
steps.push({
|
|
3824
3955
|
kind: "api",
|
|
@@ -3831,12 +3962,12 @@ function resolveValueTrace(input) {
|
|
|
3831
3962
|
}
|
|
3832
3963
|
return { ...base, steps, resolvedAtMs: now() };
|
|
3833
3964
|
}
|
|
3834
|
-
function findContextMatch(consumer, target, targetFp, fiberToNodeId) {
|
|
3965
|
+
function findContextMatch(consumer, target, targetFp, fiberToNodeId, cache) {
|
|
3835
3966
|
const deps = consumer.dependencies?.firstContext;
|
|
3836
3967
|
if (!deps) return null;
|
|
3837
3968
|
let dep = deps;
|
|
3838
3969
|
while (dep) {
|
|
3839
|
-
const match = valuesMatch(target, targetFp, dep.memoizedValue);
|
|
3970
|
+
const match = valuesMatch(target, targetFp, dep.memoizedValue, cache);
|
|
3840
3971
|
if (match) {
|
|
3841
3972
|
const provider = findNearestProvider(consumer, dep.context);
|
|
3842
3973
|
const step = {
|
|
@@ -3852,14 +3983,14 @@ function findContextMatch(consumer, target, targetFp, fiberToNodeId) {
|
|
|
3852
3983
|
}
|
|
3853
3984
|
return null;
|
|
3854
3985
|
}
|
|
3855
|
-
function findDerivationMatch(fiber, target, targetFp, componentName) {
|
|
3986
|
+
function findDerivationMatch(fiber, target, targetFp, componentName, cache) {
|
|
3856
3987
|
let hook = fiber.memoizedState;
|
|
3857
3988
|
let index = 0;
|
|
3858
3989
|
while (hook) {
|
|
3859
3990
|
const ms = hook.memoizedState;
|
|
3860
3991
|
if (Array.isArray(ms) && ms.length === 2 && Array.isArray(ms[1])) {
|
|
3861
3992
|
const [computed, deps] = ms;
|
|
3862
|
-
const match = valuesMatch(target, targetFp, computed);
|
|
3993
|
+
const match = valuesMatch(target, targetFp, computed, cache);
|
|
3863
3994
|
if (match) {
|
|
3864
3995
|
const hookType = typeof computed === "function" ? "useCallback" : "useMemo";
|
|
3865
3996
|
return {
|
|
@@ -3878,6 +4009,25 @@ function findDerivationMatch(fiber, target, targetFp, componentName) {
|
|
|
3878
4009
|
}
|
|
3879
4010
|
return null;
|
|
3880
4011
|
}
|
|
4012
|
+
function findMatchingHookState(fiber, target, targetFp, cache) {
|
|
4013
|
+
let hook = fiber.memoizedState;
|
|
4014
|
+
let index = 0;
|
|
4015
|
+
while (hook) {
|
|
4016
|
+
const ms = hook.memoizedState;
|
|
4017
|
+
const isMemoTuple = Array.isArray(ms) && ms.length === 2 && Array.isArray(ms[1]);
|
|
4018
|
+
const isEffectShape2 = ms !== null && typeof ms === "object" && "create" in ms && "deps" in ms;
|
|
4019
|
+
const isRefShape = ms !== null && typeof ms === "object" && Object.keys(ms).length === 1 && "current" in ms;
|
|
4020
|
+
if (!isMemoTuple && !isEffectShape2 && !isRefShape) {
|
|
4021
|
+
const match = valuesMatch(target, targetFp, ms, cache);
|
|
4022
|
+
if (match) {
|
|
4023
|
+
return { hookIndex: index, hookType: "useState", confidence: match };
|
|
4024
|
+
}
|
|
4025
|
+
}
|
|
4026
|
+
hook = hook.next;
|
|
4027
|
+
index++;
|
|
4028
|
+
}
|
|
4029
|
+
return null;
|
|
4030
|
+
}
|
|
3881
4031
|
function findNearestProvider(consumer, contextObj) {
|
|
3882
4032
|
let current = consumer.return;
|
|
3883
4033
|
let hops = 0;
|
|
@@ -3891,44 +4041,47 @@ function findNearestProvider(consumer, contextObj) {
|
|
|
3891
4041
|
}
|
|
3892
4042
|
return null;
|
|
3893
4043
|
}
|
|
3894
|
-
function findStoreMatch(target, targetFp, deadline) {
|
|
4044
|
+
function findStoreMatch(target, targetFp, deadline, cache) {
|
|
3895
4045
|
for (const [storeName, state] of getZustandSnapshot()) {
|
|
3896
4046
|
if (now() > deadline) return null;
|
|
3897
|
-
const hit = findMatchingPathInObject(target, targetFp, state, [], 0, deadline);
|
|
4047
|
+
const hit = findMatchingPathInObject(target, targetFp, state, [], 0, deadline, cache);
|
|
3898
4048
|
if (hit) {
|
|
3899
4049
|
return {
|
|
3900
4050
|
source: "zustand",
|
|
3901
4051
|
storeName,
|
|
3902
4052
|
keyPath: hit.path,
|
|
3903
4053
|
confidence: hit.confidence,
|
|
3904
|
-
matchedValue: walkPath(state, hit.path)
|
|
4054
|
+
matchedValue: walkPath(state, hit.path),
|
|
4055
|
+
stateRoot: state
|
|
3905
4056
|
};
|
|
3906
4057
|
}
|
|
3907
4058
|
}
|
|
3908
4059
|
const redux = getReduxSnapshot();
|
|
3909
4060
|
if (redux) {
|
|
3910
4061
|
if (now() > deadline) return null;
|
|
3911
|
-
const hit = findMatchingPathInObject(target, targetFp, redux, [], 0, deadline);
|
|
4062
|
+
const hit = findMatchingPathInObject(target, targetFp, redux, [], 0, deadline, cache);
|
|
3912
4063
|
if (hit) {
|
|
3913
4064
|
return {
|
|
3914
4065
|
source: "redux",
|
|
3915
4066
|
storeName: "redux",
|
|
3916
4067
|
keyPath: hit.path,
|
|
3917
4068
|
confidence: hit.confidence,
|
|
3918
|
-
matchedValue: walkPath(redux, hit.path)
|
|
4069
|
+
matchedValue: walkPath(redux, hit.path),
|
|
4070
|
+
stateRoot: redux
|
|
3919
4071
|
};
|
|
3920
4072
|
}
|
|
3921
4073
|
}
|
|
3922
4074
|
for (const [queryHash, entry] of getTanstackSnapshot()) {
|
|
3923
4075
|
if (now() > deadline) return null;
|
|
3924
|
-
const hit = findMatchingPathInObject(target, targetFp, entry.data, [], 0, deadline);
|
|
4076
|
+
const hit = findMatchingPathInObject(target, targetFp, entry.data, [], 0, deadline, cache);
|
|
3925
4077
|
if (hit) {
|
|
3926
4078
|
return {
|
|
3927
4079
|
source: "tanstack-query",
|
|
3928
4080
|
storeName: queryHash,
|
|
3929
4081
|
keyPath: hit.path,
|
|
3930
4082
|
confidence: hit.confidence,
|
|
3931
|
-
matchedValue: walkPath(entry.data, hit.path)
|
|
4083
|
+
matchedValue: walkPath(entry.data, hit.path),
|
|
4084
|
+
stateRoot: entry.data
|
|
3932
4085
|
};
|
|
3933
4086
|
}
|
|
3934
4087
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flotrace/runtime-core",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.3",
|
|
4
4
|
"description": "Platform-agnostic core for FloTrace runtime — fiber walker, analyzers, trackers. Shared by @flotrace/runtime (web) and @flotrace/runtime-native (React Native).",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
|
-
"dist"
|
|
16
|
+
"dist",
|
|
17
|
+
"LICENSE",
|
|
18
|
+
"README.md"
|
|
17
19
|
],
|
|
18
20
|
"scripts": {
|
|
19
21
|
"build": "tsup",
|
|
@@ -46,10 +48,13 @@
|
|
|
46
48
|
"flotrace"
|
|
47
49
|
],
|
|
48
50
|
"license": "MIT",
|
|
51
|
+
"homepage": "https://flotrace.dev",
|
|
49
52
|
"repository": {
|
|
50
53
|
"type": "git",
|
|
51
|
-
"url": "https://github.com/
|
|
52
|
-
|
|
54
|
+
"url": "https://github.com/sameersitre/runtime-core.git"
|
|
55
|
+
},
|
|
56
|
+
"bugs": {
|
|
57
|
+
"url": "https://github.com/sameersitre/runtime-core/issues"
|
|
53
58
|
},
|
|
54
59
|
"publishConfig": {
|
|
55
60
|
"access": "public"
|