@getlimelight/sdk 0.4.5 → 0.5.1

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
@@ -231,7 +231,7 @@ var SENSITIVE_HEADERS = [
231
231
  var LIMELIGHT_WEB_WSS_URL = "wss://api.getlimelight.io";
232
232
  var LIMELIGHT_DESKTOP_WSS_URL = "ws://localhost:8484";
233
233
  var WS_PATH = "/limelight";
234
- var SDK_VERSION = true ? "0.4.5" : "test-version";
234
+ var SDK_VERSION = true ? "0.5.1" : "test-version";
235
235
  var RENDER_THRESHOLDS = {
236
236
  HOT_VELOCITY: 5,
237
237
  HIGH_RENDER_COUNT: 50,
@@ -1126,12 +1126,10 @@ var RenderInterceptor = class {
1126
1126
  }, RENDER_THRESHOLDS.SNAPSHOT_INTERVAL_MS);
1127
1127
  this.isSetup = true;
1128
1128
  }
1129
- resetProfiles() {
1130
- this.profiles.clear();
1131
- this.pendingUnmounts = [];
1132
- this.currentCommitComponents.clear();
1133
- this.componentIdCounter = 0;
1134
- }
1129
+ /**
1130
+ * Installs or wraps the React DevTools global hook.
1131
+ * Returns true if successful, false otherwise.
1132
+ */
1135
1133
  installHook() {
1136
1134
  const globalObj = typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : null;
1137
1135
  if (!globalObj) return false;
@@ -1144,6 +1142,11 @@ var RenderInterceptor = class {
1144
1142
  }
1145
1143
  return true;
1146
1144
  }
1145
+ /**
1146
+ * Wraps an existing React DevTools hook to intercept render events.
1147
+ * Preserves original functionality.
1148
+ * @param hook - The existing React DevTools hook
1149
+ */
1147
1150
  wrapExistingHook(hook) {
1148
1151
  this.originalHook = hook;
1149
1152
  this.originalOnCommitFiberRoot = hook.onCommitFiberRoot?.bind(hook);
@@ -1157,6 +1160,11 @@ var RenderInterceptor = class {
1157
1160
  this.handleCommitFiberUnmount(rendererID, fiber);
1158
1161
  };
1159
1162
  }
1163
+ /**
1164
+ * Creates a new React DevTools hook to intercept render events.
1165
+ * @param globalObj - The global object (window or global)
1166
+ * @param hookKey - The key for the React DevTools hook
1167
+ */
1160
1168
  createHook(globalObj, hookKey) {
1161
1169
  const renderers = /* @__PURE__ */ new Map();
1162
1170
  let rendererIdCounter = 0;
@@ -1179,6 +1187,8 @@ var RenderInterceptor = class {
1179
1187
  /**
1180
1188
  * Handles a fiber root commit - walks tree and ACCUMULATES into profiles.
1181
1189
  * Two-pass: first count components, then accumulate with distributed cost.
1190
+ * @param rendererID - The renderer ID
1191
+ * @param root - The fiber root
1182
1192
  */
1183
1193
  handleCommitFiberRoot(_rendererID, root) {
1184
1194
  this.currentCommitComponents.clear();
@@ -1194,6 +1204,7 @@ var RenderInterceptor = class {
1194
1204
  }
1195
1205
  /**
1196
1206
  * First pass: count rendered components for cost distribution.
1207
+ * @param fiber - The current fiber node
1197
1208
  */
1198
1209
  countRenderedComponents(fiber) {
1199
1210
  if (!fiber) return;
@@ -1203,6 +1214,11 @@ var RenderInterceptor = class {
1203
1214
  this.countRenderedComponents(fiber.child);
1204
1215
  this.countRenderedComponents(fiber.sibling);
1205
1216
  }
1217
+ /**
1218
+ * Handles a fiber unmount - marks component as unmounted.
1219
+ * @param rendererID - The renderer ID
1220
+ * @param fiber - The fiber being unmounted
1221
+ */
1206
1222
  handleCommitFiberUnmount(_rendererID, fiber) {
1207
1223
  if (!this.isUserComponent(fiber)) return;
1208
1224
  const componentId = this.fiberToComponentId.get(fiber);
@@ -1216,6 +1232,9 @@ var RenderInterceptor = class {
1216
1232
  }
1217
1233
  /**
1218
1234
  * Walks fiber tree and accumulates render stats into profiles.
1235
+ * @param fiber - The current fiber node
1236
+ * @param parentComponentId - The parent component ID
1237
+ * @param depth - The current depth in the tree
1219
1238
  */
1220
1239
  walkFiberTree(fiber, parentComponentId, depth) {
1221
1240
  if (!fiber) return;
@@ -1230,6 +1249,10 @@ var RenderInterceptor = class {
1230
1249
  }
1231
1250
  /**
1232
1251
  * Core accumulation logic - this is where we build up the profile.
1252
+ * @param fiber - The current fiber node
1253
+ * @param componentId - The component ID
1254
+ * @param parentComponentId - The parent component ID
1255
+ * @param depth - The current depth in the tree
1233
1256
  */
1234
1257
  accumulateRender(fiber, componentId, parentComponentId, depth) {
1235
1258
  const now = Date.now();
@@ -1290,7 +1313,9 @@ var RenderInterceptor = class {
1290
1313
  this.updateSuspiciousFlag(profile);
1291
1314
  }
1292
1315
  /**
1293
- * NEW: Accumulate prop change details into the profile.
1316
+ * Accumulate prop change details into the profile.
1317
+ * @param profile - The component profile
1318
+ * @param changes - The list of prop change details
1294
1319
  */
1295
1320
  accumulatePropChanges(profile, changes) {
1296
1321
  const stats = profile.propChangeStats;
@@ -1319,7 +1344,9 @@ var RenderInterceptor = class {
1319
1344
  }
1320
1345
  }
1321
1346
  /**
1322
- * Build prop change snapshot for emission.
1347
+ * Build prop change snapshot for emission.
1348
+ * @param profile - The component profile
1349
+ * @returns The prop change snapshot or undefined
1323
1350
  */
1324
1351
  buildPropChangeSnapshot(profile) {
1325
1352
  const stats = profile.propChangeStats;
@@ -1337,6 +1364,10 @@ var RenderInterceptor = class {
1337
1364
  });
1338
1365
  return { topChangedProps };
1339
1366
  }
1367
+ /**
1368
+ * Updates the suspicious flag based on render velocity and count.
1369
+ * @param profile - The component profile
1370
+ */
1340
1371
  updateSuspiciousFlag(profile) {
1341
1372
  const velocity = this.calculateVelocity(profile);
1342
1373
  if (velocity > RENDER_THRESHOLDS.HOT_VELOCITY) {
@@ -1355,6 +1386,8 @@ var RenderInterceptor = class {
1355
1386
  /**
1356
1387
  * Calculates renders per second from velocity window.
1357
1388
  * Cheap: just count / window duration, no array operations.
1389
+ * @param profile - The component profile
1390
+ * @returns The calculated velocity
1358
1391
  */
1359
1392
  calculateVelocity(profile) {
1360
1393
  const now = Date.now();
@@ -1367,6 +1400,8 @@ var RenderInterceptor = class {
1367
1400
  }
1368
1401
  /**
1369
1402
  * Emits a snapshot of all profiles with deltas.
1403
+ * Only emits profiles that have significant changes since last emit.
1404
+ * Also emits unmounts.
1370
1405
  */
1371
1406
  emitSnapshot() {
1372
1407
  const now = Date.now();
@@ -1450,7 +1485,11 @@ var RenderInterceptor = class {
1450
1485
  this.sendMessage(message);
1451
1486
  }
1452
1487
  /**
1453
- * Now returns prop change details when applicable.
1488
+ * Now returns prop change details when applicable.
1489
+ * Infers the cause of the render by comparing current and previous fiber states.
1490
+ * @param fiber - The current fiber node
1491
+ * @param parentComponentId - The parent component ID
1492
+ * @returns The inferred render cause
1454
1493
  */
1455
1494
  inferRenderCause(fiber, parentComponentId) {
1456
1495
  const alternate = fiber.alternate;
@@ -1499,6 +1538,9 @@ var RenderInterceptor = class {
1499
1538
  /**
1500
1539
  * Diff props to find which keys changed and whether it's reference-only.
1501
1540
  * This is the key insight generator.
1541
+ * @param prevProps - The previous props
1542
+ * @param nextProps - The next props
1543
+ * @returns List of prop change details
1502
1544
  */
1503
1545
  diffProps(prevProps, nextProps) {
1504
1546
  if (!prevProps || !nextProps) {
@@ -1526,8 +1568,11 @@ var RenderInterceptor = class {
1526
1568
  return changes;
1527
1569
  }
1528
1570
  /**
1529
- * Shallow equality check to determine if a prop is reference-only change.
1571
+ * Shallow equality check to determine if a prop is reference-only change.
1530
1572
  * We only go one level deep to keep it fast.
1573
+ * @param a - The first value
1574
+ * @param b - The second value
1575
+ * @returns True if shallow equal, false otherwise
1531
1576
  */
1532
1577
  isShallowEqual(a, b) {
1533
1578
  if (a === b) return true;
@@ -1554,13 +1599,28 @@ var RenderInterceptor = class {
1554
1599
  }
1555
1600
  return a === b;
1556
1601
  }
1602
+ /**
1603
+ * Determines if a fiber represents a user-defined component.
1604
+ * @param fiber - The fiber node
1605
+ * @returns True if user component, false otherwise
1606
+ */
1557
1607
  isUserComponent(fiber) {
1558
1608
  const tag = fiber.tag;
1559
1609
  return tag === 0 /* FunctionComponent */ || tag === 1 /* ClassComponent */ || tag === 11 /* ForwardRef */ || tag === 14 /* MemoComponent */ || tag === 15 /* SimpleMemoComponent */;
1560
1610
  }
1611
+ /**
1612
+ * Determines if a fiber performed work during the commit.
1613
+ * @param fiber - The fiber node
1614
+ * @returns True if performed work, false otherwise
1615
+ */
1561
1616
  didFiberRender(fiber) {
1562
1617
  return (fiber.flags & 1 /* PerformedWork */) !== 0;
1563
1618
  }
1619
+ /**
1620
+ * Gets or creates a unique component ID for a fiber.
1621
+ * @param fiber - The fiber node
1622
+ * @returns The unique component ID
1623
+ */
1564
1624
  getOrCreateComponentId(fiber) {
1565
1625
  let id = this.fiberToComponentId.get(fiber);
1566
1626
  if (id) return id;
@@ -1575,6 +1635,11 @@ var RenderInterceptor = class {
1575
1635
  this.fiberToComponentId.set(fiber, id);
1576
1636
  return id;
1577
1637
  }
1638
+ /**
1639
+ * Gets the display name of a component from a fiber.
1640
+ * @param fiber - The fiber node
1641
+ * @returns The component name
1642
+ */
1578
1643
  getComponentName(fiber) {
1579
1644
  const type = fiber.type;
1580
1645
  if (!type) return "Unknown";
@@ -1592,6 +1657,11 @@ var RenderInterceptor = class {
1592
1657
  }
1593
1658
  return "Unknown";
1594
1659
  }
1660
+ /**
1661
+ * Gets the component type from a fiber.
1662
+ * @param fiber - The fiber node
1663
+ * @returns The component type
1664
+ */
1595
1665
  getComponentType(fiber) {
1596
1666
  switch (fiber.tag) {
1597
1667
  case 0 /* FunctionComponent */:
@@ -1625,6 +1695,9 @@ var RenderInterceptor = class {
1625
1695
  getSuspiciousComponents() {
1626
1696
  return Array.from(this.profiles.values()).filter((p) => p.isSuspicious);
1627
1697
  }
1698
+ /**
1699
+ * Cleans up and restores original hook behavior.
1700
+ */
1628
1701
  cleanup() {
1629
1702
  if (!this.isSetup) return;
1630
1703
  this.emitSnapshot();
@@ -1650,6 +1723,16 @@ var RenderInterceptor = class {
1650
1723
  this.config = null;
1651
1724
  this.isSetup = false;
1652
1725
  }
1726
+ /**
1727
+ * Resets all collected profiles
1728
+ */
1729
+ resetProfiles() {
1730
+ this.profiles.clear();
1731
+ this.fiberToComponentId = /* @__PURE__ */ new WeakMap();
1732
+ this.pendingUnmounts = [];
1733
+ this.currentCommitComponents.clear();
1734
+ this.componentIdCounter = 0;
1735
+ }
1653
1736
  };
1654
1737
 
1655
1738
  // src/limelight/interceptors/StateInterceptor.ts
@@ -1676,7 +1759,9 @@ var StateInterceptor = class {
1676
1759
  */
1677
1760
  registerStore(name, store) {
1678
1761
  if (this.stores.has(name)) {
1679
- console.warn(`[Limelight] Store "${name}" already registered`);
1762
+ if (this.config?.enableInternalLogging) {
1763
+ console.warn(`[Limelight] Store "${name}" already registered`);
1764
+ }
1680
1765
  return;
1681
1766
  }
1682
1767
  const library = this.detectLibrary(store);
@@ -1686,7 +1771,7 @@ var StateInterceptor = class {
1686
1771
  );
1687
1772
  return;
1688
1773
  }
1689
- const state = this.getState(store, library);
1774
+ const state = this.getState(store);
1690
1775
  const initEvent = {
1691
1776
  phase: "STATE:INIT" /* INIT */,
1692
1777
  sessionId: this.getSessionId(),
@@ -1703,6 +1788,8 @@ var StateInterceptor = class {
1703
1788
  }
1704
1789
  /**
1705
1790
  * Unregister a store and stop listening to changes.
1791
+ * Can be called manually via Limelight.removeStore().
1792
+ * @param name The name of the store to unregister
1706
1793
  */
1707
1794
  unregisterStore(name) {
1708
1795
  const store = this.stores.get(name);
@@ -1713,6 +1800,7 @@ var StateInterceptor = class {
1713
1800
  }
1714
1801
  /**
1715
1802
  * Emit an event, applying beforeSend hook if configured
1803
+ * @param event The event to emit
1716
1804
  */
1717
1805
  emitEvent(event) {
1718
1806
  if (this.config?.beforeSend) {
@@ -1731,6 +1819,8 @@ var StateInterceptor = class {
1731
1819
  }
1732
1820
  /**
1733
1821
  * Detect whether a store is Zustand or Redux
1822
+ * @param store The store to inspect
1823
+ * @return StateLibrary or null if unknown
1734
1824
  */
1735
1825
  detectLibrary(store) {
1736
1826
  if (!store || typeof store !== "function" && typeof store !== "object") {
@@ -1749,13 +1839,19 @@ var StateInterceptor = class {
1749
1839
  }
1750
1840
  /**
1751
1841
  * Get current state from a store
1842
+ * @param store The store to get state from
1843
+ * @return The current state
1752
1844
  */
1753
- getState(store, library) {
1845
+ getState(store) {
1754
1846
  const storeAny = store;
1755
1847
  return storeAny.getState();
1756
1848
  }
1757
1849
  /**
1758
1850
  * Subscribe to store changes
1851
+ * @param store The store to subscribe to
1852
+ * @param library The detected state library
1853
+ * @param storeName The name of the store
1854
+ * @return Unsubscribe function
1759
1855
  */
1760
1856
  subscribe(store, library, storeName) {
1761
1857
  const storeAny = store;
@@ -1767,6 +1863,9 @@ var StateInterceptor = class {
1767
1863
  }
1768
1864
  /**
1769
1865
  * Subscribe to Zustand store changes
1866
+ * @param store The Zustand store
1867
+ * @param storeName The name of the store
1868
+ * @return Unsubscribe function
1770
1869
  */
1771
1870
  subscribeZustand(store, storeName) {
1772
1871
  return store.subscribe((state, prevState) => {
@@ -1789,6 +1888,9 @@ var StateInterceptor = class {
1789
1888
  }
1790
1889
  /**
1791
1890
  * Subscribe to Redux store changes
1891
+ * @param store The Redux store
1892
+ * @param storeName The name of the store
1893
+ * @return Unsubscribe function
1792
1894
  */
1793
1895
  subscribeRedux(store, storeName) {
1794
1896
  let lastAction = { type: "@@INIT" };
@@ -1824,6 +1926,9 @@ var StateInterceptor = class {
1824
1926
  }
1825
1927
  /**
1826
1928
  * Infer action name from stack trace for Zustand
1929
+ * @param state The new state
1930
+ * @param prevState The previous state
1931
+ * @return Inferred StateAction
1827
1932
  */
1828
1933
  inferZustandAction(state, prevState) {
1829
1934
  const actionType = this.parseActionFromStack(this.captureStackTrace());
@@ -1835,6 +1940,8 @@ var StateInterceptor = class {
1835
1940
  }
1836
1941
  /**
1837
1942
  * Parse function name from stack trace
1943
+ * @param stack The stack trace string
1944
+ * @return The inferred action name
1838
1945
  */
1839
1946
  parseActionFromStack(stack) {
1840
1947
  if (!stack) return "set";
@@ -1865,6 +1972,9 @@ var StateInterceptor = class {
1865
1972
  }
1866
1973
  /**
1867
1974
  * Compute what keys changed between states (shallow)
1975
+ * @param state The new state
1976
+ * @param prevState The previous state
1977
+ * @return Partial state with only changed keys
1868
1978
  */
1869
1979
  computePartialState(state, prevState) {
1870
1980
  if (typeof state !== "object" || state === null || typeof prevState !== "object" || prevState === null) {
@@ -1905,7 +2015,7 @@ var StateInterceptor = class {
1905
2015
  }
1906
2016
  };
1907
2017
 
1908
- // src/limelight/interceptors/RequestBridge.ts
2018
+ // src/limelight/bridges/RequestBridge.ts
1909
2019
  var RequestBridge = class {
1910
2020
  constructor(sendMessage, getSessionId) {
1911
2021
  this.sendMessage = sendMessage;
@@ -2072,6 +2182,42 @@ var RequestBridge = class {
2072
2182
  }
2073
2183
  };
2074
2184
 
2185
+ // src/limelight/handlers/CommandHandler.ts
2186
+ var CommandHandler = class {
2187
+ constructor(interceptors, sendMessage, getConfig) {
2188
+ this.interceptors = interceptors;
2189
+ this.sendMessage = sendMessage;
2190
+ this.getConfig = getConfig;
2191
+ }
2192
+ /**
2193
+ * Handles an incoming command.
2194
+ * @param command - The command to handle
2195
+ */
2196
+ handle(command) {
2197
+ const config = this.getConfig();
2198
+ if (config?.enableInternalLogging) {
2199
+ console.log("[Limelight] Received command:", command.type);
2200
+ }
2201
+ switch (command.type) {
2202
+ case "CLEAR_RENDERS" /* CLEAR_RENDERS */:
2203
+ this.interceptors.render.resetProfiles();
2204
+ break;
2205
+ default:
2206
+ if (config?.enableInternalLogging) {
2207
+ console.warn("[Limelight] Unknown command:", command.type);
2208
+ }
2209
+ }
2210
+ if (command.id) {
2211
+ this.sendMessage({
2212
+ phase: "ACK" /* ACK */,
2213
+ commandId: command.id,
2214
+ type: command.type,
2215
+ success: true
2216
+ });
2217
+ }
2218
+ }
2219
+ };
2220
+
2075
2221
  // src/limelight/LimelightClient.ts
2076
2222
  var LimelightClient = class {
2077
2223
  ws = null;
@@ -2089,6 +2235,7 @@ var LimelightClient = class {
2089
2235
  renderInterceptor;
2090
2236
  stateInterceptor;
2091
2237
  requestBridge;
2238
+ commandHandler = null;
2092
2239
  constructor() {
2093
2240
  this.networkInterceptor = new NetworkInterceptor(
2094
2241
  this.sendMessage.bind(this),
@@ -2114,6 +2261,11 @@ var LimelightClient = class {
2114
2261
  this.sendMessage.bind(this),
2115
2262
  () => this.sessionId
2116
2263
  );
2264
+ this.commandHandler = new CommandHandler(
2265
+ { render: this.renderInterceptor },
2266
+ this.sendMessage.bind(this),
2267
+ () => this.config
2268
+ );
2117
2269
  }
2118
2270
  /**
2119
2271
  * Configures the Limelight client with the provided settings.
@@ -2220,6 +2372,16 @@ var LimelightClient = class {
2220
2372
  this.flushMessageQueue();
2221
2373
  this.sendMessage(message);
2222
2374
  };
2375
+ this.ws.onmessage = (event) => {
2376
+ try {
2377
+ const command = JSON.parse(event.data);
2378
+ this.commandHandler?.handle(command);
2379
+ } catch (error) {
2380
+ if (this.config?.enableInternalLogging) {
2381
+ console.error("[Limelight] Failed to parse command:", error);
2382
+ }
2383
+ }
2384
+ };
2223
2385
  this.ws.onerror = (error) => {
2224
2386
  if (this.config?.enableInternalLogging) {
2225
2387
  console.error("[Limelight] WebSocket error:", error);