@flotrace/runtime-core 2.0.1 → 2.2.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/index.mjs CHANGED
@@ -1551,8 +1551,8 @@ function runAnalysis(tree, fiberRefMap2) {
1551
1551
  }
1552
1552
  function schedulePropDrillingAnalysis(tree, fiberRefMap2, client2) {
1553
1553
  if (analyzeTimer) clearTimeout(analyzeTimer);
1554
- const now = Date.now();
1555
- const elapsed = now - lastAnalysisTime;
1554
+ const now2 = Date.now();
1555
+ const elapsed = now2 - lastAnalysisTime;
1556
1556
  const delay = elapsed >= ANALYZE_INTERVAL_MS ? 0 : ANALYZE_INTERVAL_MS - elapsed;
1557
1557
  analyzeTimer = setTimeout(() => {
1558
1558
  analyzeTimer = null;
@@ -2579,7 +2579,7 @@ function executeSnapshot(root) {
2579
2579
  }
2580
2580
  previousFlatTree = currentFlatTree;
2581
2581
  if (pendingLocalStateCorrelations.length > 0) {
2582
- const now = Date.now();
2582
+ const now2 = Date.now();
2583
2583
  const toSend = pendingLocalStateCorrelations.splice(0);
2584
2584
  for (const corr of toSend) {
2585
2585
  try {
@@ -2588,7 +2588,7 @@ function executeSnapshot(root) {
2588
2588
  requestId: corr.requestId,
2589
2589
  componentName: corr.componentName,
2590
2590
  hookIndex: corr.hookIndex,
2591
- timestamp: now
2591
+ timestamp: now2
2592
2592
  });
2593
2593
  } catch {
2594
2594
  }
@@ -3033,6 +3033,7 @@ var activeUnsubscribers = [];
3033
3033
  var isInstalled4 = false;
3034
3034
  var debounceTimers = /* @__PURE__ */ new Map();
3035
3035
  var DEBOUNCE_MS = 200;
3036
+ var trackedStores = /* @__PURE__ */ new Map();
3036
3037
  function installZustandTracker(stores, client2) {
3037
3038
  if (isInstalled4) {
3038
3039
  console.warn("[FloTrace] Zustand tracker already installed, reinstalling");
@@ -3048,6 +3049,7 @@ function installZustandTracker(stores, client2) {
3048
3049
  continue;
3049
3050
  }
3050
3051
  try {
3052
+ trackedStores.set(storeName, store);
3051
3053
  const initialState = store.getState();
3052
3054
  sendStoreUpdate(storeName, initialState, Object.keys(initialState), client2);
3053
3055
  const unsubscribe = store.subscribe((newState, prevState) => {
@@ -3077,9 +3079,20 @@ function uninstallZustandTracker() {
3077
3079
  }
3078
3080
  }
3079
3081
  activeUnsubscribers = [];
3082
+ trackedStores.clear();
3080
3083
  isInstalled4 = false;
3081
3084
  console.log("[FloTrace] Zustand tracker uninstalled");
3082
3085
  }
3086
+ function getZustandSnapshot() {
3087
+ const snapshot = /* @__PURE__ */ new Map();
3088
+ for (const [name, store] of trackedStores) {
3089
+ try {
3090
+ snapshot.set(name, store.getState());
3091
+ } catch {
3092
+ }
3093
+ }
3094
+ return snapshot;
3095
+ }
3083
3096
  function scheduleStoreUpdate(storeName, prevState, newState, client2) {
3084
3097
  let changedKeys;
3085
3098
  try {
@@ -3118,6 +3131,7 @@ var isInstalled5 = false;
3118
3131
  var debounceTimer = null;
3119
3132
  var previousState = null;
3120
3133
  var DEBOUNCE_MS2 = 200;
3134
+ var trackedStore = null;
3121
3135
  function isReduxStore(obj) {
3122
3136
  return typeof obj === "object" && obj !== null && typeof obj.getState === "function" && typeof obj.subscribe === "function" && typeof obj.dispatch === "function";
3123
3137
  }
@@ -3129,6 +3143,7 @@ function installReduxTracker(store, client2) {
3129
3143
  isInstalled5 = true;
3130
3144
  console.log("[FloTrace] Installing Redux tracker");
3131
3145
  try {
3146
+ trackedStore = store;
3132
3147
  const initialState = store.getState();
3133
3148
  previousState = initialState;
3134
3149
  sendReduxUpdate(initialState, Object.keys(initialState), client2);
@@ -3160,9 +3175,18 @@ function uninstallReduxTracker() {
3160
3175
  activeUnsubscribe = null;
3161
3176
  }
3162
3177
  previousState = null;
3178
+ trackedStore = null;
3163
3179
  isInstalled5 = false;
3164
3180
  console.log("[FloTrace] Redux tracker uninstalled");
3165
3181
  }
3182
+ function getReduxSnapshot() {
3183
+ if (!trackedStore) return null;
3184
+ try {
3185
+ return trackedStore.getState();
3186
+ } catch {
3187
+ return null;
3188
+ }
3189
+ }
3166
3190
  function scheduleReduxUpdate(newState, client2) {
3167
3191
  let changedKeys;
3168
3192
  try {
@@ -3200,6 +3224,7 @@ var queryUnsubscribe = null;
3200
3224
  var mutationUnsubscribe = null;
3201
3225
  var debounceTimer2 = null;
3202
3226
  var DEBOUNCE_MS3 = 300;
3227
+ var trackedClient = null;
3203
3228
  var MAX_EVENTS_PER_QUERY = 50;
3204
3229
  var queryTracking = /* @__PURE__ */ new Map();
3205
3230
  var CORRELATION_WINDOW_MS = 500;
@@ -3222,6 +3247,7 @@ function installTanStackQueryTracker(queryClient, client2) {
3222
3247
  isInstalled6 = true;
3223
3248
  console.log("[FloTrace] Installing TanStack Query tracker");
3224
3249
  try {
3250
+ trackedClient = queryClient;
3225
3251
  const queryCache = queryClient.getQueryCache();
3226
3252
  const mutationCache = queryClient.getMutationCache();
3227
3253
  for (const query of queryCache.getAll()) {
@@ -3286,9 +3312,24 @@ function uninstallTanStackQueryTracker() {
3286
3312
  clearTimeout(pending.timeoutId);
3287
3313
  }
3288
3314
  pendingCorrelations.clear();
3315
+ trackedClient = null;
3289
3316
  isInstalled6 = false;
3290
3317
  console.log("[FloTrace] TanStack Query tracker uninstalled");
3291
3318
  }
3319
+ function getTanstackSnapshot() {
3320
+ const snapshot = /* @__PURE__ */ new Map();
3321
+ if (!trackedClient) return snapshot;
3322
+ try {
3323
+ const queryCache = trackedClient.getQueryCache();
3324
+ for (const query of queryCache.getAll()) {
3325
+ if (query.state.data !== void 0) {
3326
+ snapshot.set(query.queryHash, { queryKey: query.queryKey, data: query.state.data });
3327
+ }
3328
+ }
3329
+ } catch {
3330
+ }
3331
+ return snapshot;
3332
+ }
3292
3333
  function computeDataHash(data) {
3293
3334
  if (data === null || data === void 0) return "__null__";
3294
3335
  try {
@@ -3351,11 +3392,11 @@ function updateQueryTracking(query, eventType) {
3351
3392
  }
3352
3393
  }
3353
3394
  if (tracking.prevFetchStatus === "idle" && currentFetchStatus === "fetching") {
3354
- const now = Date.now();
3395
+ const now2 = Date.now();
3355
3396
  for (const pending of pendingCorrelations.values()) {
3356
3397
  if (pending.idleQueryHashes.has(query.queryHash)) {
3357
3398
  pending.affectedQueries.set(query.queryHash, {
3358
- fetchStartedAt: now,
3399
+ fetchStartedAt: now2,
3359
3400
  queryKey: query.queryKey
3360
3401
  });
3361
3402
  }
@@ -3367,7 +3408,7 @@ function updateQueryTracking(query, eventType) {
3367
3408
  }
3368
3409
  function openCorrelationWindow(mutation, queryCache, mutationCache, client2) {
3369
3410
  const correlationId = `corr-${++correlationCounter}`;
3370
- const now = Date.now();
3411
+ const now2 = Date.now();
3371
3412
  const idleQueryHashes = /* @__PURE__ */ new Set();
3372
3413
  for (const query of queryCache.getAll()) {
3373
3414
  if (query.state.fetchStatus === "idle") {
@@ -3381,7 +3422,7 @@ function openCorrelationWindow(mutation, queryCache, mutationCache, client2) {
3381
3422
  correlationId,
3382
3423
  mutationId: mutation.mutationId,
3383
3424
  mutationKey: mutation.options.mutationKey,
3384
- completedAt: now,
3425
+ completedAt: now2,
3385
3426
  idleQueryHashes,
3386
3427
  affectedQueries: /* @__PURE__ */ new Map(),
3387
3428
  timeoutId
@@ -3581,6 +3622,319 @@ function safeCall(fn, fallback) {
3581
3622
  }
3582
3623
  }
3583
3624
 
3625
+ // src/valueTraceResolver.ts
3626
+ var FIBER_TAG_CONTEXT_PROVIDER = 10;
3627
+ var BUDGET_MS = 50;
3628
+ var SCAN_DEPTH = 3;
3629
+ var MAX_PROP_CHAIN_DEPTH = 30;
3630
+ function now() {
3631
+ return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
3632
+ }
3633
+ function walkPath(root, path) {
3634
+ let cur = root;
3635
+ for (const key of path) {
3636
+ if (cur === null || cur === void 0 || typeof cur !== "object") return void 0;
3637
+ cur = cur[key];
3638
+ }
3639
+ return cur;
3640
+ }
3641
+ function getHookValueAt(fiber, hookIndex) {
3642
+ let hook = fiber.memoizedState;
3643
+ let i = 0;
3644
+ while (hook && i < hookIndex) {
3645
+ hook = hook.next;
3646
+ i++;
3647
+ }
3648
+ if (!hook) return void 0;
3649
+ return hook.memoizedState;
3650
+ }
3651
+ function valuesMatch(target, targetFp, candidate) {
3652
+ const targetIsObject = target !== null && typeof target === "object";
3653
+ const candidateIsObject = candidate !== null && typeof candidate === "object";
3654
+ if (targetIsObject && candidateIsObject && target === candidate) return "exact";
3655
+ if (!shouldFlagRename(target) || !shouldFlagRename(candidate)) return null;
3656
+ if (valueFingerprint(candidate) === targetFp) return "fingerprint-match";
3657
+ return null;
3658
+ }
3659
+ function findMatchingPathInObject(target, targetFp, container, currentPath, depth, deadline) {
3660
+ if (now() > deadline) return null;
3661
+ if (depth > SCAN_DEPTH) return null;
3662
+ if (container === null || typeof container !== "object") return null;
3663
+ const selfMatch = valuesMatch(target, targetFp, container);
3664
+ if (selfMatch) return { path: [...currentPath], confidence: selfMatch };
3665
+ if (Array.isArray(container)) {
3666
+ for (let i = 0; i < Math.min(container.length, 50); i++) {
3667
+ const child = container[i];
3668
+ const directMatch = valuesMatch(target, targetFp, child);
3669
+ if (directMatch) return { path: [...currentPath, String(i)], confidence: directMatch };
3670
+ const nested = findMatchingPathInObject(target, targetFp, child, [...currentPath, String(i)], depth + 1, deadline);
3671
+ if (nested) return nested;
3672
+ }
3673
+ } else {
3674
+ for (const key of Object.keys(container)) {
3675
+ const child = container[key];
3676
+ const directMatch = valuesMatch(target, targetFp, child);
3677
+ if (directMatch) return { path: [...currentPath, key], confidence: directMatch };
3678
+ const nested = findMatchingPathInObject(target, targetFp, child, [...currentPath, key], depth + 1, deadline);
3679
+ if (nested) return nested;
3680
+ }
3681
+ }
3682
+ return null;
3683
+ }
3684
+ function buildFiberToNodeIdMap() {
3685
+ const reverse = /* @__PURE__ */ new Map();
3686
+ for (const [nodeId, fiber] of getFiberRefMap()) {
3687
+ reverse.set(fiber, nodeId);
3688
+ }
3689
+ return reverse;
3690
+ }
3691
+ function resolveValueTrace(input) {
3692
+ const startedAt = now();
3693
+ const deadline = startedAt + BUDGET_MS;
3694
+ const steps = [];
3695
+ const base = {
3696
+ rootNodeId: input.nodeId,
3697
+ rootPropPath: input.propPath,
3698
+ rootHookPath: input.hookPath,
3699
+ steps,
3700
+ resolvedAtMs: now()
3701
+ };
3702
+ const fiber = getFiberRefMap().get(input.nodeId);
3703
+ if (!fiber) {
3704
+ return { ...base, error: "no-fiber", resolvedAtMs: now() };
3705
+ }
3706
+ let rootValue;
3707
+ if (input.propPath && input.propPath.length > 0) {
3708
+ if (!fiber.memoizedProps) return { ...base, error: "value-not-found", resolvedAtMs: now() };
3709
+ rootValue = walkPath(fiber.memoizedProps, input.propPath);
3710
+ } else if (input.hookPath) {
3711
+ const hookValue = getHookValueAt(fiber, input.hookPath.hookIndex);
3712
+ rootValue = input.hookPath.subPath && input.hookPath.subPath.length > 0 ? walkPath(hookValue, input.hookPath.subPath) : hookValue;
3713
+ } else {
3714
+ return { ...base, error: "value-not-found", resolvedAtMs: now() };
3715
+ }
3716
+ if (rootValue === void 0) {
3717
+ return { ...base, error: "value-not-found", resolvedAtMs: now() };
3718
+ }
3719
+ const rootFp = valueFingerprint(rootValue);
3720
+ const fiberToNodeId = buildFiberToNodeIdMap();
3721
+ const rootComponentName = getComponentNameFromFiber(fiber) ?? "Unknown";
3722
+ if (input.propPath) {
3723
+ steps.push({
3724
+ kind: "prop",
3725
+ nodeId: input.nodeId,
3726
+ componentName: rootComponentName,
3727
+ propPath: input.propPath,
3728
+ confidence: "exact"
3729
+ });
3730
+ } else if (input.hookPath) {
3731
+ steps.push({
3732
+ kind: "hook-state",
3733
+ nodeId: input.nodeId,
3734
+ componentName: rootComponentName,
3735
+ hookIndex: input.hookPath.hookIndex,
3736
+ hookType: "unknown",
3737
+ // Cheap: full hook classification is expensive; caller can fetch separately.
3738
+ subPath: input.hookPath.subPath,
3739
+ confidence: "exact"
3740
+ });
3741
+ }
3742
+ if (input.propPath) {
3743
+ let current = fiber.return;
3744
+ let hops = 0;
3745
+ while (current && hops < MAX_PROP_CHAIN_DEPTH) {
3746
+ if (now() > deadline) return { ...base, steps, truncated: true, resolvedAtMs: now() };
3747
+ if (current.tag !== FIBER_TAG_CONTEXT_PROVIDER) {
3748
+ const props = current.memoizedProps;
3749
+ if (props) {
3750
+ const match = findMatchingPathInObject(rootValue, rootFp, props, [], 0, deadline);
3751
+ if (match) {
3752
+ const ancestorNodeId = fiberToNodeId.get(current);
3753
+ const ancestorName = getComponentNameFromFiber(current) ?? "Unknown";
3754
+ if (ancestorNodeId) {
3755
+ steps.push({
3756
+ kind: "prop",
3757
+ nodeId: ancestorNodeId,
3758
+ componentName: ancestorName,
3759
+ propPath: match.path,
3760
+ confidence: match.confidence
3761
+ });
3762
+ }
3763
+ }
3764
+ }
3765
+ }
3766
+ current = current.return;
3767
+ hops++;
3768
+ }
3769
+ }
3770
+ if (input.hookPath) {
3771
+ const origin = findFetchOrigin(rootValue);
3772
+ if (origin) {
3773
+ steps.push({
3774
+ kind: "api",
3775
+ requestId: origin,
3776
+ method: "UNKNOWN",
3777
+ urlPath: "",
3778
+ ageMs: 0
3779
+ });
3780
+ return { ...base, steps, resolvedAtMs: now() };
3781
+ }
3782
+ }
3783
+ const derivedMatch = findDerivationMatch(fiber, rootValue, rootFp, rootComponentName);
3784
+ if (derivedMatch) {
3785
+ steps.push({ ...derivedMatch, nodeId: input.nodeId });
3786
+ return { ...base, steps, resolvedAtMs: now() };
3787
+ }
3788
+ const contextMatch = findContextMatch(fiber, rootValue, rootFp, fiberToNodeId);
3789
+ if (contextMatch) {
3790
+ steps.push(contextMatch.step);
3791
+ const providerStoreMatch = findStoreMatch(contextMatch.providerValue, valueFingerprint(contextMatch.providerValue), deadline);
3792
+ if (providerStoreMatch) {
3793
+ steps.push({
3794
+ kind: "store",
3795
+ source: providerStoreMatch.source,
3796
+ storeName: providerStoreMatch.storeName,
3797
+ keyPath: providerStoreMatch.keyPath,
3798
+ confidence: providerStoreMatch.confidence
3799
+ });
3800
+ const origin = findFetchOrigin(providerStoreMatch.matchedValue);
3801
+ if (origin) {
3802
+ steps.push({ kind: "api", requestId: origin, method: "UNKNOWN", urlPath: "", ageMs: 0 });
3803
+ }
3804
+ } else {
3805
+ const origin = findFetchOrigin(contextMatch.providerValue);
3806
+ if (origin) {
3807
+ steps.push({ kind: "api", requestId: origin, method: "UNKNOWN", urlPath: "", ageMs: 0 });
3808
+ }
3809
+ }
3810
+ return { ...base, steps, resolvedAtMs: now() };
3811
+ }
3812
+ const storeMatch = findStoreMatch(rootValue, rootFp, deadline);
3813
+ if (storeMatch) {
3814
+ steps.push({
3815
+ kind: "store",
3816
+ source: storeMatch.source,
3817
+ storeName: storeMatch.storeName,
3818
+ keyPath: storeMatch.keyPath,
3819
+ confidence: storeMatch.confidence
3820
+ });
3821
+ const origin = findFetchOrigin(storeMatch.matchedValue);
3822
+ if (origin) {
3823
+ steps.push({
3824
+ kind: "api",
3825
+ requestId: origin,
3826
+ method: "UNKNOWN",
3827
+ urlPath: "",
3828
+ ageMs: 0
3829
+ });
3830
+ }
3831
+ }
3832
+ return { ...base, steps, resolvedAtMs: now() };
3833
+ }
3834
+ function findContextMatch(consumer, target, targetFp, fiberToNodeId) {
3835
+ const deps = consumer.dependencies?.firstContext;
3836
+ if (!deps) return null;
3837
+ let dep = deps;
3838
+ while (dep) {
3839
+ const match = valuesMatch(target, targetFp, dep.memoizedValue);
3840
+ if (match) {
3841
+ const provider = findNearestProvider(consumer, dep.context);
3842
+ const step = {
3843
+ kind: "context",
3844
+ contextName: dep.context.displayName || "Context",
3845
+ providerNodeId: provider ? fiberToNodeId.get(provider) : void 0,
3846
+ confidence: match
3847
+ };
3848
+ const providerValue = provider?.memoizedProps?.value ?? dep.memoizedValue;
3849
+ return { step, providerValue };
3850
+ }
3851
+ dep = dep.next;
3852
+ }
3853
+ return null;
3854
+ }
3855
+ function findDerivationMatch(fiber, target, targetFp, componentName) {
3856
+ let hook = fiber.memoizedState;
3857
+ let index = 0;
3858
+ while (hook) {
3859
+ const ms = hook.memoizedState;
3860
+ if (Array.isArray(ms) && ms.length === 2 && Array.isArray(ms[1])) {
3861
+ const [computed, deps] = ms;
3862
+ const match = valuesMatch(target, targetFp, computed);
3863
+ if (match) {
3864
+ const hookType = typeof computed === "function" ? "useCallback" : "useMemo";
3865
+ return {
3866
+ kind: "derived",
3867
+ nodeId: "",
3868
+ componentName,
3869
+ hookIndex: index,
3870
+ hookType,
3871
+ depCount: deps.length,
3872
+ confidence: match
3873
+ };
3874
+ }
3875
+ }
3876
+ hook = hook.next;
3877
+ index++;
3878
+ }
3879
+ return null;
3880
+ }
3881
+ function findNearestProvider(consumer, contextObj) {
3882
+ let current = consumer.return;
3883
+ let hops = 0;
3884
+ while (current && hops < MAX_PROP_CHAIN_DEPTH) {
3885
+ if (current.tag === FIBER_TAG_CONTEXT_PROVIDER) {
3886
+ const providerType = current.type;
3887
+ if (providerType && providerType._context === contextObj) return current;
3888
+ }
3889
+ current = current.return;
3890
+ hops++;
3891
+ }
3892
+ return null;
3893
+ }
3894
+ function findStoreMatch(target, targetFp, deadline) {
3895
+ for (const [storeName, state] of getZustandSnapshot()) {
3896
+ if (now() > deadline) return null;
3897
+ const hit = findMatchingPathInObject(target, targetFp, state, [], 0, deadline);
3898
+ if (hit) {
3899
+ return {
3900
+ source: "zustand",
3901
+ storeName,
3902
+ keyPath: hit.path,
3903
+ confidence: hit.confidence,
3904
+ matchedValue: walkPath(state, hit.path)
3905
+ };
3906
+ }
3907
+ }
3908
+ const redux = getReduxSnapshot();
3909
+ if (redux) {
3910
+ if (now() > deadline) return null;
3911
+ const hit = findMatchingPathInObject(target, targetFp, redux, [], 0, deadline);
3912
+ if (hit) {
3913
+ return {
3914
+ source: "redux",
3915
+ storeName: "redux",
3916
+ keyPath: hit.path,
3917
+ confidence: hit.confidence,
3918
+ matchedValue: walkPath(redux, hit.path)
3919
+ };
3920
+ }
3921
+ }
3922
+ for (const [queryHash, entry] of getTanstackSnapshot()) {
3923
+ if (now() > deadline) return null;
3924
+ const hit = findMatchingPathInObject(target, targetFp, entry.data, [], 0, deadline);
3925
+ if (hit) {
3926
+ return {
3927
+ source: "tanstack-query",
3928
+ storeName: queryHash,
3929
+ keyPath: hit.path,
3930
+ confidence: hit.confidence,
3931
+ matchedValue: walkPath(entry.data, hit.path)
3932
+ };
3933
+ }
3934
+ }
3935
+ return null;
3936
+ }
3937
+
3584
3938
  // src/frameworkDetect.ts
3585
3939
  function detectWebFramework() {
3586
3940
  if (typeof document === "undefined") return {};
@@ -3615,8 +3969,11 @@ export {
3615
3969
  getNodeEffects,
3616
3970
  getNodeHooks,
3617
3971
  getNodeProps,
3972
+ getReduxSnapshot,
3973
+ getTanstackSnapshot,
3618
3974
  getTimeline,
3619
3975
  getWebSocketClient,
3976
+ getZustandSnapshot,
3620
3977
  hasActiveTags,
3621
3978
  inspectEffects,
3622
3979
  inspectHooks,
@@ -3633,6 +3990,7 @@ export {
3633
3990
  requestFullSnapshot,
3634
3991
  requestTreeSnapshot,
3635
3992
  resetNextjsDetection,
3993
+ resolveValueTrace,
3636
3994
  serializeProps,
3637
3995
  serializeValue,
3638
3996
  tagFetchData,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flotrace/runtime-core",
3
- "version": "2.0.1",
3
+ "version": "2.2.0",
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",