@getlimelight/sdk 0.1.4 → 0.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.js CHANGED
@@ -257,7 +257,20 @@ var SENSITIVE_HEADERS = [
257
257
  ];
258
258
  var DEFAULT_PORT = 9090;
259
259
  var WS_PATH = "/limelight";
260
- var SDK_VERSION = true ? "0.1.4" : "test-version";
260
+ var SDK_VERSION = true ? "0.2.0" : "test-version";
261
+ var RENDER_THRESHOLDS = {
262
+ HOT_VELOCITY: 5,
263
+ HIGH_RENDER_COUNT: 50,
264
+ VELOCITY_WINDOW_MS: 2e3,
265
+ SNAPSHOT_INTERVAL_MS: 1e3,
266
+ MIN_DELTA_TO_EMIT: 1,
267
+ MAX_PROP_KEYS_TO_TRACK: 20,
268
+ // Don't track more than this many unique props
269
+ MAX_PROP_CHANGES_PER_SNAPSHOT: 10,
270
+ // Limit delta array size
271
+ TOP_PROPS_TO_REPORT: 5
272
+ // Only report top N changed props
273
+ };
261
274
 
262
275
  // src/helpers/safety/redactSensitiveHeaders.ts
263
276
  var redactSensitiveHeaders = (headers) => {
@@ -437,6 +450,42 @@ var formatRequestName = (url) => {
437
450
  }
438
451
  };
439
452
 
453
+ // src/helpers/render/generateRenderId.ts
454
+ var counter = 0;
455
+ var generateRenderId = () => {
456
+ const timestamp = Date.now().toString(36);
457
+ const count = (counter++).toString(36);
458
+ const random = Math.random().toString(36).substring(2, 6);
459
+ if (counter > 1e3) counter = 0;
460
+ return `${timestamp}${count}-${random}`;
461
+ };
462
+
463
+ // src/helpers/render/createEmptyCauseBreakdown.ts
464
+ var createEmptyCauseBreakdown = () => {
465
+ return {
466
+ ["state_change" /* STATE_CHANGE */]: 0,
467
+ ["props_change" /* PROPS_CHANGE */]: 0,
468
+ ["context_change" /* CONTEXT_CHANGE */]: 0,
469
+ ["parent_render" /* PARENT_RENDER */]: 0,
470
+ ["force_update" /* FORCE_UPDATE */]: 0,
471
+ ["unknown" /* UNKNOWN */]: 0
472
+ };
473
+ };
474
+
475
+ // src/helpers/render/createEmptyPropChangeStats.ts
476
+ var createEmptyPropChangeStats = () => {
477
+ return {
478
+ changeCount: /* @__PURE__ */ new Map(),
479
+ referenceOnlyCount: /* @__PURE__ */ new Map()
480
+ };
481
+ };
482
+
483
+ // src/helpers/render/getCurrentTransactionId.ts
484
+ var globalGetTransactionId = null;
485
+ var getCurrentTransactionId = () => {
486
+ return globalGetTransactionId?.() ?? null;
487
+ };
488
+
440
489
  // src/limelight/interceptors/ConsoleInterceptor.ts
441
490
  var ConsoleInterceptor = class {
442
491
  constructor(sendMessage, getSessionId) {
@@ -989,6 +1038,561 @@ var XHRInterceptor = class {
989
1038
  }
990
1039
  };
991
1040
 
1041
+ // src/limelight/interceptors/RenderInterceptor.ts
1042
+ var RenderInterceptor = class {
1043
+ sendMessage;
1044
+ getSessionId;
1045
+ config = null;
1046
+ isSetup = false;
1047
+ profiles = /* @__PURE__ */ new Map();
1048
+ fiberToComponentId = /* @__PURE__ */ new WeakMap();
1049
+ componentIdCounter = 0;
1050
+ snapshotTimer = null;
1051
+ currentCommitComponents = /* @__PURE__ */ new Set();
1052
+ componentsInCurrentCommit = 0;
1053
+ originalHook = null;
1054
+ originalOnCommitFiberRoot = null;
1055
+ originalOnCommitFiberUnmount = null;
1056
+ pendingUnmounts = [];
1057
+ constructor(sendMessage, getSessionId) {
1058
+ this.sendMessage = sendMessage;
1059
+ this.getSessionId = getSessionId;
1060
+ }
1061
+ setup(config) {
1062
+ if (this.isSetup) {
1063
+ console.warn("[Limelight] Render interceptor already set up");
1064
+ return;
1065
+ }
1066
+ this.config = config;
1067
+ if (!this.installHook()) {
1068
+ console.warn("[Limelight] Failed to install render hook");
1069
+ return;
1070
+ }
1071
+ this.snapshotTimer = setInterval(() => {
1072
+ this.emitSnapshot();
1073
+ }, RENDER_THRESHOLDS.SNAPSHOT_INTERVAL_MS);
1074
+ this.isSetup = true;
1075
+ }
1076
+ installHook() {
1077
+ const globalObj = typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : null;
1078
+ if (!globalObj) return false;
1079
+ const hookKey = "__REACT_DEVTOOLS_GLOBAL_HOOK__";
1080
+ const existingHook = globalObj[hookKey];
1081
+ if (existingHook) {
1082
+ this.wrapExistingHook(existingHook);
1083
+ } else {
1084
+ this.createHook(globalObj, hookKey);
1085
+ }
1086
+ return true;
1087
+ }
1088
+ wrapExistingHook(hook) {
1089
+ this.originalHook = hook;
1090
+ this.originalOnCommitFiberRoot = hook.onCommitFiberRoot?.bind(hook);
1091
+ this.originalOnCommitFiberUnmount = hook.onCommitFiberUnmount?.bind(hook);
1092
+ hook.onCommitFiberRoot = (rendererID, root, priorityLevel) => {
1093
+ this.originalOnCommitFiberRoot?.(rendererID, root, priorityLevel);
1094
+ this.handleCommitFiberRoot(rendererID, root);
1095
+ };
1096
+ hook.onCommitFiberUnmount = (rendererID, fiber) => {
1097
+ this.originalOnCommitFiberUnmount?.(rendererID, fiber);
1098
+ this.handleCommitFiberUnmount(rendererID, fiber);
1099
+ };
1100
+ }
1101
+ createHook(globalObj, hookKey) {
1102
+ const renderers = /* @__PURE__ */ new Map();
1103
+ let rendererIdCounter = 0;
1104
+ const hook = {
1105
+ supportsFiber: true,
1106
+ inject: (renderer) => {
1107
+ const id = ++rendererIdCounter;
1108
+ renderers.set(id, renderer);
1109
+ return id;
1110
+ },
1111
+ onCommitFiberRoot: (rendererID, root, priorityLevel) => {
1112
+ this.handleCommitFiberRoot(rendererID, root);
1113
+ },
1114
+ onCommitFiberUnmount: (rendererID, fiber) => {
1115
+ this.handleCommitFiberUnmount(rendererID, fiber);
1116
+ }
1117
+ };
1118
+ globalObj[hookKey] = hook;
1119
+ }
1120
+ /**
1121
+ * Handles a fiber root commit - walks tree and ACCUMULATES into profiles.
1122
+ * Two-pass: first count components, then accumulate with distributed cost.
1123
+ */
1124
+ handleCommitFiberRoot(_rendererID, root) {
1125
+ this.currentCommitComponents.clear();
1126
+ this.componentsInCurrentCommit = 0;
1127
+ try {
1128
+ this.countRenderedComponents(root.current);
1129
+ this.walkFiberTree(root.current, null, 0);
1130
+ } catch (error) {
1131
+ if (isDevelopment()) {
1132
+ console.warn("[Limelight] Error processing fiber tree:", error);
1133
+ }
1134
+ }
1135
+ }
1136
+ /**
1137
+ * First pass: count rendered components for cost distribution.
1138
+ */
1139
+ countRenderedComponents(fiber) {
1140
+ if (!fiber) return;
1141
+ if (this.isUserComponent(fiber) && this.didFiberRender(fiber)) {
1142
+ this.componentsInCurrentCommit++;
1143
+ }
1144
+ this.countRenderedComponents(fiber.child);
1145
+ this.countRenderedComponents(fiber.sibling);
1146
+ }
1147
+ handleCommitFiberUnmount(_rendererID, fiber) {
1148
+ if (!this.isUserComponent(fiber)) return;
1149
+ const componentId = this.fiberToComponentId.get(fiber);
1150
+ if (!componentId) return;
1151
+ const profile = this.profiles.get(componentId);
1152
+ if (profile) {
1153
+ profile.unmountedAt = Date.now();
1154
+ this.pendingUnmounts.push(profile);
1155
+ this.profiles.delete(componentId);
1156
+ }
1157
+ }
1158
+ /**
1159
+ * Walks fiber tree and accumulates render stats into profiles.
1160
+ */
1161
+ walkFiberTree(fiber, parentComponentId, depth) {
1162
+ if (!fiber) return;
1163
+ if (this.isUserComponent(fiber) && this.didFiberRender(fiber)) {
1164
+ const componentId = this.getOrCreateComponentId(fiber);
1165
+ this.accumulateRender(fiber, componentId, parentComponentId, depth);
1166
+ this.currentCommitComponents.add(componentId);
1167
+ parentComponentId = componentId;
1168
+ }
1169
+ this.walkFiberTree(fiber.child, parentComponentId, depth + 1);
1170
+ this.walkFiberTree(fiber.sibling, parentComponentId, depth);
1171
+ }
1172
+ /**
1173
+ * Core accumulation logic - this is where we build up the profile.
1174
+ */
1175
+ accumulateRender(fiber, componentId, parentComponentId, depth) {
1176
+ const now = Date.now();
1177
+ const cause = this.inferRenderCause(fiber, parentComponentId);
1178
+ const renderCost = this.componentsInCurrentCommit > 0 ? 1 / this.componentsInCurrentCommit : 1;
1179
+ let profile = this.profiles.get(componentId);
1180
+ if (!profile) {
1181
+ profile = {
1182
+ id: generateRenderId(),
1183
+ componentId,
1184
+ componentName: this.getComponentName(fiber),
1185
+ componentType: this.getComponentType(fiber),
1186
+ mountedAt: now,
1187
+ totalRenders: 0,
1188
+ totalRenderCost: 0,
1189
+ velocityWindowStart: now,
1190
+ velocityWindowCount: 0,
1191
+ causeBreakdown: createEmptyCauseBreakdown(),
1192
+ causeDeltaBreakdown: createEmptyCauseBreakdown(),
1193
+ lastEmittedRenderCount: 0,
1194
+ lastEmittedRenderCost: 0,
1195
+ lastEmitTime: now,
1196
+ parentCounts: /* @__PURE__ */ new Map(),
1197
+ depth,
1198
+ isSuspicious: false,
1199
+ // NEW
1200
+ propChangeStats: createEmptyPropChangeStats(),
1201
+ propChangeDelta: []
1202
+ };
1203
+ this.profiles.set(componentId, profile);
1204
+ }
1205
+ profile.totalRenders++;
1206
+ profile.totalRenderCost += renderCost;
1207
+ profile.causeBreakdown[cause.type]++;
1208
+ profile.causeDeltaBreakdown[cause.type]++;
1209
+ if (cause.type === "props_change" /* PROPS_CHANGE */ && cause.propChanges) {
1210
+ this.accumulatePropChanges(profile, cause.propChanges);
1211
+ }
1212
+ const transactionId = getCurrentTransactionId();
1213
+ if (transactionId) {
1214
+ profile.lastTransactionId = transactionId;
1215
+ }
1216
+ if (parentComponentId) {
1217
+ const count = (profile.parentCounts.get(parentComponentId) ?? 0) + 1;
1218
+ profile.parentCounts.set(parentComponentId, count);
1219
+ if (!profile.primaryParentId || count > (profile.parentCounts.get(profile.primaryParentId) ?? 0)) {
1220
+ profile.primaryParentId = parentComponentId;
1221
+ }
1222
+ }
1223
+ profile.depth = depth;
1224
+ const windowStart = now - RENDER_THRESHOLDS.VELOCITY_WINDOW_MS;
1225
+ if (profile.velocityWindowStart < windowStart) {
1226
+ profile.velocityWindowStart = now;
1227
+ profile.velocityWindowCount = 1;
1228
+ } else {
1229
+ profile.velocityWindowCount++;
1230
+ }
1231
+ this.updateSuspiciousFlag(profile);
1232
+ }
1233
+ /**
1234
+ * NEW: Accumulate prop change details into the profile.
1235
+ */
1236
+ accumulatePropChanges(profile, changes) {
1237
+ const stats = profile.propChangeStats;
1238
+ for (const change of changes) {
1239
+ if (stats.changeCount.size >= RENDER_THRESHOLDS.MAX_PROP_KEYS_TO_TRACK && !stats.changeCount.has(change.key)) {
1240
+ continue;
1241
+ }
1242
+ stats.changeCount.set(
1243
+ change.key,
1244
+ (stats.changeCount.get(change.key) ?? 0) + 1
1245
+ );
1246
+ if (change.referenceOnly) {
1247
+ stats.referenceOnlyCount.set(
1248
+ change.key,
1249
+ (stats.referenceOnlyCount.get(change.key) ?? 0) + 1
1250
+ );
1251
+ }
1252
+ }
1253
+ if (profile.propChangeDelta.length < RENDER_THRESHOLDS.MAX_PROP_CHANGES_PER_SNAPSHOT) {
1254
+ profile.propChangeDelta.push(
1255
+ ...changes.slice(
1256
+ 0,
1257
+ RENDER_THRESHOLDS.MAX_PROP_CHANGES_PER_SNAPSHOT - profile.propChangeDelta.length
1258
+ )
1259
+ );
1260
+ }
1261
+ }
1262
+ /**
1263
+ * Build prop change snapshot for emission.
1264
+ */
1265
+ buildPropChangeSnapshot(profile) {
1266
+ const stats = profile.propChangeStats;
1267
+ if (stats.changeCount.size === 0) {
1268
+ return void 0;
1269
+ }
1270
+ const sorted = Array.from(stats.changeCount.entries()).sort((a, b) => b[1] - a[1]).slice(0, RENDER_THRESHOLDS.TOP_PROPS_TO_REPORT);
1271
+ const topChangedProps = sorted.map(([key, count]) => {
1272
+ const refOnlyCount = stats.referenceOnlyCount.get(key) ?? 0;
1273
+ return {
1274
+ key,
1275
+ count,
1276
+ referenceOnlyPercent: count > 0 ? Math.round(refOnlyCount / count * 100) : 0
1277
+ };
1278
+ });
1279
+ return { topChangedProps };
1280
+ }
1281
+ updateSuspiciousFlag(profile) {
1282
+ const velocity = this.calculateVelocity(profile);
1283
+ if (velocity > RENDER_THRESHOLDS.HOT_VELOCITY) {
1284
+ profile.isSuspicious = true;
1285
+ profile.suspiciousReason = `High render velocity: ${velocity.toFixed(
1286
+ 1
1287
+ )}/sec`;
1288
+ } else if (profile.totalRenders > RENDER_THRESHOLDS.HIGH_RENDER_COUNT) {
1289
+ profile.isSuspicious = true;
1290
+ profile.suspiciousReason = `High total renders: ${profile.totalRenders}`;
1291
+ } else {
1292
+ profile.isSuspicious = false;
1293
+ profile.suspiciousReason = void 0;
1294
+ }
1295
+ }
1296
+ /**
1297
+ * Calculates renders per second from velocity window.
1298
+ * Cheap: just count / window duration, no array operations.
1299
+ */
1300
+ calculateVelocity(profile) {
1301
+ const now = Date.now();
1302
+ const windowAge = now - profile.velocityWindowStart;
1303
+ if (windowAge > RENDER_THRESHOLDS.VELOCITY_WINDOW_MS) {
1304
+ return 0;
1305
+ }
1306
+ const effectiveWindowMs = Math.max(windowAge, 100);
1307
+ return profile.velocityWindowCount / effectiveWindowMs * 1e3;
1308
+ }
1309
+ /**
1310
+ * Emits a snapshot of all profiles with deltas.
1311
+ */
1312
+ emitSnapshot() {
1313
+ const now = Date.now();
1314
+ const snapshots = [];
1315
+ for (const profile of this.profiles.values()) {
1316
+ const rendersDelta = profile.totalRenders - profile.lastEmittedRenderCount;
1317
+ if (rendersDelta < RENDER_THRESHOLDS.MIN_DELTA_TO_EMIT && !profile.isSuspicious) {
1318
+ continue;
1319
+ }
1320
+ const velocity = this.calculateVelocity(profile);
1321
+ const isMount = profile.lastEmittedRenderCount === 0;
1322
+ const renderCostDelta = profile.totalRenderCost - profile.lastEmittedRenderCost;
1323
+ const propChanges = this.buildPropChangeSnapshot(profile);
1324
+ snapshots.push({
1325
+ id: profile.id,
1326
+ componentId: profile.componentId,
1327
+ componentName: profile.componentName,
1328
+ componentType: profile.componentType,
1329
+ totalRenders: profile.totalRenders,
1330
+ totalRenderCost: profile.totalRenderCost,
1331
+ avgRenderCost: profile.totalRenderCost / profile.totalRenders,
1332
+ rendersDelta,
1333
+ renderCostDelta,
1334
+ renderVelocity: velocity,
1335
+ causeBreakdown: { ...profile.causeBreakdown },
1336
+ causeDeltaBreakdown: { ...profile.causeDeltaBreakdown },
1337
+ parentComponentId: profile.primaryParentId,
1338
+ depth: profile.depth,
1339
+ lastTransactionId: profile.lastTransactionId,
1340
+ isSuspicious: profile.isSuspicious,
1341
+ suspiciousReason: profile.suspiciousReason,
1342
+ renderPhase: isMount ? "mount" /* MOUNT */ : "update" /* UPDATE */,
1343
+ mountedAt: profile.mountedAt,
1344
+ propChanges
1345
+ });
1346
+ profile.lastEmittedRenderCount = profile.totalRenders;
1347
+ profile.lastEmittedRenderCost = profile.totalRenderCost;
1348
+ profile.lastEmitTime = now;
1349
+ profile.causeDeltaBreakdown = createEmptyCauseBreakdown();
1350
+ profile.propChangeDelta = [];
1351
+ }
1352
+ for (const profile of this.pendingUnmounts) {
1353
+ const propChanges = this.buildPropChangeSnapshot(profile);
1354
+ snapshots.push({
1355
+ id: profile.id,
1356
+ componentId: profile.componentId,
1357
+ componentName: profile.componentName,
1358
+ componentType: profile.componentType,
1359
+ totalRenders: profile.totalRenders,
1360
+ totalRenderCost: profile.totalRenderCost,
1361
+ avgRenderCost: profile.totalRenderCost / Math.max(profile.totalRenders, 1),
1362
+ rendersDelta: 0,
1363
+ renderCostDelta: 0,
1364
+ renderVelocity: 0,
1365
+ causeBreakdown: { ...profile.causeBreakdown },
1366
+ causeDeltaBreakdown: createEmptyCauseBreakdown(),
1367
+ parentComponentId: profile.primaryParentId,
1368
+ depth: profile.depth,
1369
+ lastTransactionId: profile.lastTransactionId,
1370
+ isSuspicious: profile.isSuspicious,
1371
+ suspiciousReason: profile.suspiciousReason,
1372
+ renderPhase: "unmount" /* UNMOUNT */,
1373
+ mountedAt: profile.mountedAt,
1374
+ unmountedAt: profile.unmountedAt,
1375
+ propChanges
1376
+ });
1377
+ }
1378
+ this.pendingUnmounts = [];
1379
+ if (snapshots.length === 0) return;
1380
+ let message = {
1381
+ phase: "RENDER_SNAPSHOT",
1382
+ sessionId: this.getSessionId(),
1383
+ timestamp: now,
1384
+ profiles: snapshots
1385
+ };
1386
+ if (this.config?.beforeSend) {
1387
+ const modified = this.config.beforeSend(message);
1388
+ if (!modified) return;
1389
+ message = modified;
1390
+ }
1391
+ this.sendMessage(message);
1392
+ }
1393
+ /**
1394
+ * Now returns prop change details when applicable.
1395
+ */
1396
+ inferRenderCause(fiber, parentComponentId) {
1397
+ const alternate = fiber.alternate;
1398
+ if (!alternate) {
1399
+ return {
1400
+ type: "unknown" /* UNKNOWN */,
1401
+ confidence: "high" /* HIGH */
1402
+ };
1403
+ }
1404
+ if (parentComponentId && this.currentCommitComponents.has(parentComponentId)) {
1405
+ const prevProps = alternate.memoizedProps;
1406
+ const nextProps = fiber.memoizedProps;
1407
+ const propsChanged = prevProps !== nextProps;
1408
+ if (propsChanged) {
1409
+ const propChanges = this.diffProps(prevProps, nextProps);
1410
+ return {
1411
+ type: "props_change" /* PROPS_CHANGE */,
1412
+ confidence: "medium" /* MEDIUM */,
1413
+ triggerId: parentComponentId,
1414
+ propChanges
1415
+ };
1416
+ }
1417
+ return {
1418
+ type: "parent_render" /* PARENT_RENDER */,
1419
+ confidence: "medium" /* MEDIUM */,
1420
+ triggerId: parentComponentId
1421
+ };
1422
+ }
1423
+ if (fiber.memoizedState !== alternate.memoizedState) {
1424
+ return {
1425
+ type: "state_change" /* STATE_CHANGE */,
1426
+ confidence: "medium" /* MEDIUM */
1427
+ };
1428
+ }
1429
+ if (fiber.memoizedProps !== alternate.memoizedProps) {
1430
+ return {
1431
+ type: "context_change" /* CONTEXT_CHANGE */,
1432
+ confidence: "low" /* LOW */
1433
+ };
1434
+ }
1435
+ return {
1436
+ type: "unknown" /* UNKNOWN */,
1437
+ confidence: "unknown" /* UNKNOWN */
1438
+ };
1439
+ }
1440
+ /**
1441
+ * Diff props to find which keys changed and whether it's reference-only.
1442
+ * This is the key insight generator.
1443
+ */
1444
+ diffProps(prevProps, nextProps) {
1445
+ if (!prevProps || !nextProps) {
1446
+ return [];
1447
+ }
1448
+ const changes = [];
1449
+ const allKeys = /* @__PURE__ */ new Set([
1450
+ ...Object.keys(prevProps),
1451
+ ...Object.keys(nextProps)
1452
+ ]);
1453
+ const skipKeys = /* @__PURE__ */ new Set(["children", "key", "ref"]);
1454
+ for (const key of allKeys) {
1455
+ if (skipKeys.has(key)) continue;
1456
+ const prevValue = prevProps[key];
1457
+ const nextValue = nextProps[key];
1458
+ if (prevValue === nextValue) {
1459
+ continue;
1460
+ }
1461
+ const referenceOnly = this.isShallowEqual(prevValue, nextValue);
1462
+ changes.push({ key, referenceOnly });
1463
+ if (changes.length >= 10) {
1464
+ break;
1465
+ }
1466
+ }
1467
+ return changes;
1468
+ }
1469
+ /**
1470
+ * Shallow equality check to determine if a prop is reference-only change.
1471
+ * We only go one level deep to keep it fast.
1472
+ */
1473
+ isShallowEqual(a, b) {
1474
+ if (a === b) return true;
1475
+ if (typeof a !== typeof b) return false;
1476
+ if (a === null || b === null) return false;
1477
+ if (typeof a === "function" && typeof b === "function") {
1478
+ return true;
1479
+ }
1480
+ if (Array.isArray(a) && Array.isArray(b)) {
1481
+ if (a.length !== b.length) return false;
1482
+ for (let i = 0; i < a.length; i++) {
1483
+ if (a[i] !== b[i]) return false;
1484
+ }
1485
+ return true;
1486
+ }
1487
+ if (typeof a === "object" && typeof b === "object") {
1488
+ const keysA = Object.keys(a);
1489
+ const keysB = Object.keys(b);
1490
+ if (keysA.length !== keysB.length) return false;
1491
+ for (const key of keysA) {
1492
+ if (a[key] !== b[key]) return false;
1493
+ }
1494
+ return true;
1495
+ }
1496
+ return a === b;
1497
+ }
1498
+ isUserComponent(fiber) {
1499
+ const tag = fiber.tag;
1500
+ return tag === 0 /* FunctionComponent */ || tag === 1 /* ClassComponent */ || tag === 11 /* ForwardRef */ || tag === 14 /* MemoComponent */ || tag === 15 /* SimpleMemoComponent */;
1501
+ }
1502
+ didFiberRender(fiber) {
1503
+ return (fiber.flags & 1 /* PerformedWork */) !== 0;
1504
+ }
1505
+ getOrCreateComponentId(fiber) {
1506
+ let id = this.fiberToComponentId.get(fiber);
1507
+ if (id) return id;
1508
+ if (fiber.alternate) {
1509
+ id = this.fiberToComponentId.get(fiber.alternate);
1510
+ if (id) {
1511
+ this.fiberToComponentId.set(fiber, id);
1512
+ return id;
1513
+ }
1514
+ }
1515
+ id = `c_${++this.componentIdCounter}`;
1516
+ this.fiberToComponentId.set(fiber, id);
1517
+ return id;
1518
+ }
1519
+ getComponentName(fiber) {
1520
+ const type = fiber.type;
1521
+ if (!type) return "Unknown";
1522
+ if (typeof type === "function") {
1523
+ return type.displayName || type.name || "Anonymous";
1524
+ }
1525
+ if (typeof type === "object" && type !== null) {
1526
+ if (type.displayName) return type.displayName;
1527
+ if (type.render)
1528
+ return type.render.displayName || type.render.name || "ForwardRef";
1529
+ if (type.type) {
1530
+ const inner = type.type;
1531
+ return inner.displayName || inner.name || "Memo";
1532
+ }
1533
+ }
1534
+ return "Unknown";
1535
+ }
1536
+ getComponentType(fiber) {
1537
+ switch (fiber.tag) {
1538
+ case 0 /* FunctionComponent */:
1539
+ return "function";
1540
+ case 1 /* ClassComponent */:
1541
+ return "class";
1542
+ case 11 /* ForwardRef */:
1543
+ return "forwardRef";
1544
+ case 14 /* MemoComponent */:
1545
+ case 15 /* SimpleMemoComponent */:
1546
+ return "memo";
1547
+ default:
1548
+ return "unknown";
1549
+ }
1550
+ }
1551
+ /**
1552
+ * Force emit current state (useful for debugging or on-demand refresh).
1553
+ */
1554
+ forceEmit() {
1555
+ this.emitSnapshot();
1556
+ }
1557
+ /**
1558
+ * Get current profile for a component (useful for debugging).
1559
+ */
1560
+ getProfile(componentId) {
1561
+ return this.profiles.get(componentId);
1562
+ }
1563
+ /**
1564
+ * Get all suspicious components.
1565
+ */
1566
+ getSuspiciousComponents() {
1567
+ return Array.from(this.profiles.values()).filter((p) => p.isSuspicious);
1568
+ }
1569
+ cleanup() {
1570
+ if (!this.isSetup) return;
1571
+ this.emitSnapshot();
1572
+ if (this.snapshotTimer) {
1573
+ clearInterval(this.snapshotTimer);
1574
+ this.snapshotTimer = null;
1575
+ }
1576
+ if (this.originalHook) {
1577
+ if (this.originalOnCommitFiberRoot) {
1578
+ this.originalHook.onCommitFiberRoot = this.originalOnCommitFiberRoot;
1579
+ }
1580
+ if (this.originalOnCommitFiberUnmount) {
1581
+ this.originalHook.onCommitFiberUnmount = this.originalOnCommitFiberUnmount;
1582
+ }
1583
+ }
1584
+ this.originalHook = null;
1585
+ this.originalOnCommitFiberRoot = null;
1586
+ this.originalOnCommitFiberUnmount = null;
1587
+ this.profiles.clear();
1588
+ this.pendingUnmounts = [];
1589
+ this.currentCommitComponents.clear();
1590
+ this.componentIdCounter = 0;
1591
+ this.config = null;
1592
+ this.isSetup = false;
1593
+ }
1594
+ };
1595
+
992
1596
  // src/limelight/LimelightClient.ts
993
1597
  var LimelightClient = class {
994
1598
  ws = null;
@@ -1003,6 +1607,7 @@ var LimelightClient = class {
1003
1607
  networkInterceptor;
1004
1608
  xhrInterceptor;
1005
1609
  consoleInterceptor;
1610
+ renderInterceptor;
1006
1611
  constructor() {
1007
1612
  this.networkInterceptor = new NetworkInterceptor(
1008
1613
  this.sendMessage.bind(this),
@@ -1016,10 +1621,14 @@ var LimelightClient = class {
1016
1621
  this.sendMessage.bind(this),
1017
1622
  () => this.sessionId
1018
1623
  );
1624
+ this.renderInterceptor = new RenderInterceptor(
1625
+ this.sendMessage.bind(this),
1626
+ () => this.sessionId
1627
+ );
1019
1628
  }
1020
1629
  /**
1021
1630
  * Configures the Limelight client with the provided settings.
1022
- * Sets up network, XHR, and console interceptors based on the configuration.
1631
+ * Sets up network, XHR, console, and render interceptors based on the configuration.
1023
1632
  * @internal
1024
1633
  * @private
1025
1634
  * @param {LimelightConfig} config - Configuration object for Limelight
@@ -1034,6 +1643,7 @@ var LimelightClient = class {
1034
1643
  enableNetworkInspector: true,
1035
1644
  enableConsole: true,
1036
1645
  enableGraphQL: true,
1646
+ enableRenderInspector: true,
1037
1647
  ...config
1038
1648
  };
1039
1649
  if (!this.config.enabled) {
@@ -1048,6 +1658,9 @@ var LimelightClient = class {
1048
1658
  if (this.config.enableConsole) {
1049
1659
  this.consoleInterceptor.setup(this.config);
1050
1660
  }
1661
+ if (this.config.enableRenderInspector) {
1662
+ this.renderInterceptor.setup(this.config);
1663
+ }
1051
1664
  } catch (error) {
1052
1665
  console.error("[Limelight] Failed to setup interceptors:", error);
1053
1666
  }
@@ -1233,6 +1846,7 @@ var LimelightClient = class {
1233
1846
  this.networkInterceptor.cleanup();
1234
1847
  this.xhrInterceptor.cleanup();
1235
1848
  this.consoleInterceptor.cleanup();
1849
+ this.renderInterceptor.cleanup();
1236
1850
  this.reconnectAttempts = 0;
1237
1851
  this.messageQueue = [];
1238
1852
  }