@buoy-gg/route-events 4.0.2 → 6.0.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.
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.RouteEventItemCompact = RouteEventItemCompact;
6
+ exports.RouteEventItemCompact = void 0;
7
7
  var _react = require("react");
8
8
  var _reactNative = require("react-native");
9
9
  var _sharedUi = require("@buoy-gg/shared-ui");
@@ -114,12 +114,17 @@ function getBadgeText(event) {
114
114
  // Don't show badge text - route type is already shown in status label
115
115
  return undefined;
116
116
  }
117
- function RouteEventItemCompact({
117
+
118
+ // Memo justified by profiling (tool-bench 2026-07-06): 400 row renders
119
+ // during a 5s dwell at 500 events (timeline re-renders cascaded into all
120
+ // mounted rows via an inline onPress closure).
121
+ const RouteEventItemCompact = exports.RouteEventItemCompact = /*#__PURE__*/(0, _react.memo)(function RouteEventItemCompact({
118
122
  event,
119
123
  visitNumber,
120
124
  isExpanded,
121
125
  onPress,
122
- onNavigate
126
+ onNavigate,
127
+ index
123
128
  }) {
124
129
  const routeType = (0, _react.useMemo)(() => getRouteType(event), [event]);
125
130
  const routeTemplate = (0, _react.useMemo)(() => getRouteTemplate(event.pathname, event.segments), [event.pathname, event.segments]);
@@ -127,7 +132,6 @@ function RouteEventItemCompact({
127
132
  const statusLabel = (0, _react.useMemo)(() => getStatusLabel(routeType), [routeType]);
128
133
  const statusSublabel = (0, _react.useMemo)(() => getStatusSublabel(event), [event]);
129
134
  const badgeText = (0, _react.useMemo)(() => getBadgeText(event), [event]);
130
- const timeLabel = (0, _sharedUi.useRelativeTime)(event.timestamp);
131
135
  const expandedContent = (0, _react.useMemo)(() => {
132
136
  if (!isExpanded) return undefined;
133
137
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_RouteEventExpandedContent.RouteEventExpandedContent, {
@@ -170,14 +174,16 @@ function RouteEventItemCompact({
170
174
  badgeText: onNavigate ? undefined : badgeText,
171
175
  badgeColor: statusColor,
172
176
  customBadge: customBadge,
173
- bottomRightText: timeLabel,
177
+ bottomRightText: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.RelativeTime, {
178
+ timestamp: event.timestamp
179
+ }),
174
180
  showChevron: true,
175
181
  expandedContent: expandedContent,
176
182
  isExpanded: isExpanded,
177
183
  expandedGlowColor: statusColor,
178
- onPress: onPress
184
+ onPress: () => onPress(index)
179
185
  });
180
- }
186
+ });
181
187
  const styles = _reactNative.StyleSheet.create({
182
188
  badgeContainer: {
183
189
  flexDirection: "row",
@@ -42,16 +42,40 @@ function RouteEventsTimeline({
42
42
  })]
43
43
  });
44
44
  }
45
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ScrollView, {
46
- contentContainerStyle: styles.listContent,
47
- children: events.map((event, index) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_RouteEventItemCompact.RouteEventItemCompact, {
48
- event: event,
49
- visitNumber: visitCounts.get(index) || 1,
50
- isExpanded: expandedIndex === index,
51
- onPress: () => handleItemPress(index),
52
- onNavigate: onNavigate
53
- }, `event-${index}-${event.timestamp}`))
54
- });
45
+ return (
46
+ /*#__PURE__*/
47
+ // Virtualized — the previous ScrollView + map mounted ALL events (at
48
+ // the 500-event cap that measured 32.8 JS FPS during an idle dwell,
49
+ // tool-bench 2026-07-06). extraData re-renders visible rows when the
50
+ // expanded row changes.
51
+ (0, _jsxRuntime.jsx)(_reactNative.FlatList, {
52
+ contentContainerStyle: styles.listContent,
53
+ data: events
54
+ // STABLE content keys — index-based keys remount every row when a new
55
+ // event prepends (measured 4 JS FPS during automation, which emits
56
+ // route events on every bounce; tool-bench 2026-07-06).
57
+ ,
58
+ keyExtractor: event => `${event.timestamp}-${event.pathname}`,
59
+ extraData: expandedIndex,
60
+ renderItem: ({
61
+ item: event,
62
+ index
63
+ }) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_RouteEventItemCompact.RouteEventItemCompact, {
64
+ event: event,
65
+ visitNumber: visitCounts.get(index) || 1,
66
+ isExpanded: expandedIndex === index,
67
+ index: index,
68
+ onPress: handleItemPress,
69
+ onNavigate: onNavigate
70
+ }),
71
+ showsVerticalScrollIndicator: true,
72
+ removeClippedSubviews: true,
73
+ initialNumToRender: 12,
74
+ maxToRenderPerBatch: 8,
75
+ windowSize: 5,
76
+ updateCellsBatchingPeriod: 50
77
+ })
78
+ );
55
79
  }
56
80
  const styles = _reactNative.StyleSheet.create({
57
81
  listContent: {
@@ -51,19 +51,28 @@ function useRouteEvents(options = {}) {
51
51
  setIsListening(false);
52
52
  return;
53
53
  }
54
+
55
+ // Frame-coalesced (see network's useNetworkEventList): navigation
56
+ // bursts land as one commit per frame instead of one per emission.
57
+ let rafId = null;
58
+ let pending = null;
54
59
  const unsubscribe = _routeEventStore.routeEventStore.subscribeToEvents(allEvents => {
55
- // Limit events
56
- if (allEvents.length > maxEvents) {
57
- setEvents(allEvents.slice(0, maxEvents));
58
- } else {
59
- setEvents(allEvents);
60
- }
60
+ pending = allEvents;
61
+ if (rafId != null) return;
62
+ rafId = requestAnimationFrame(() => {
63
+ rafId = null;
64
+ if (!pending) return;
65
+ const latest = pending;
66
+ pending = null;
67
+ setEvents(latest.length > maxEvents ? latest.slice(0, maxEvents) : latest);
68
+ });
61
69
  });
62
70
 
63
71
  // Mark as listening after successful subscription
64
72
  setIsListening(true);
65
73
  return () => {
66
74
  unsubscribe();
75
+ if (rafId != null) cancelAnimationFrame(rafId);
67
76
  // Note: Don't set isListening=false here because we might be
68
77
  // re-subscribing due to dependency change, not actually stopping
69
78
  };
@@ -6,9 +6,9 @@
6
6
  * Uses CompactRow pattern matching network/storage/redux components
7
7
  */
8
8
 
9
- import { useMemo } from "react";
9
+ import { memo, useMemo } from "react";
10
10
  import { View, Text, TouchableOpacity, StyleSheet } from "react-native";
11
- import { CompactRow, useRelativeTime, buoyColors } from "@buoy-gg/shared-ui";
11
+ import { CompactRow, RelativeTime, buoyColors } from "@buoy-gg/shared-ui";
12
12
  import { RouteEventExpandedContent } from "./RouteEventExpandedContent";
13
13
 
14
14
  // Route type for color coding
@@ -110,12 +110,17 @@ function getBadgeText(event) {
110
110
  // Don't show badge text - route type is already shown in status label
111
111
  return undefined;
112
112
  }
113
- export function RouteEventItemCompact({
113
+
114
+ // Memo justified by profiling (tool-bench 2026-07-06): 400 row renders
115
+ // during a 5s dwell at 500 events (timeline re-renders cascaded into all
116
+ // mounted rows via an inline onPress closure).
117
+ export const RouteEventItemCompact = /*#__PURE__*/memo(function RouteEventItemCompact({
114
118
  event,
115
119
  visitNumber,
116
120
  isExpanded,
117
121
  onPress,
118
- onNavigate
122
+ onNavigate,
123
+ index
119
124
  }) {
120
125
  const routeType = useMemo(() => getRouteType(event), [event]);
121
126
  const routeTemplate = useMemo(() => getRouteTemplate(event.pathname, event.segments), [event.pathname, event.segments]);
@@ -123,7 +128,6 @@ export function RouteEventItemCompact({
123
128
  const statusLabel = useMemo(() => getStatusLabel(routeType), [routeType]);
124
129
  const statusSublabel = useMemo(() => getStatusSublabel(event), [event]);
125
130
  const badgeText = useMemo(() => getBadgeText(event), [event]);
126
- const timeLabel = useRelativeTime(event.timestamp);
127
131
  const expandedContent = useMemo(() => {
128
132
  if (!isExpanded) return undefined;
129
133
  return /*#__PURE__*/_jsx(RouteEventExpandedContent, {
@@ -166,14 +170,16 @@ export function RouteEventItemCompact({
166
170
  badgeText: onNavigate ? undefined : badgeText,
167
171
  badgeColor: statusColor,
168
172
  customBadge: customBadge,
169
- bottomRightText: timeLabel,
173
+ bottomRightText: /*#__PURE__*/_jsx(RelativeTime, {
174
+ timestamp: event.timestamp
175
+ }),
170
176
  showChevron: true,
171
177
  expandedContent: expandedContent,
172
178
  isExpanded: isExpanded,
173
179
  expandedGlowColor: statusColor,
174
- onPress: onPress
180
+ onPress: () => onPress(index)
175
181
  });
176
- }
182
+ });
177
183
  const styles = StyleSheet.create({
178
184
  badgeContainer: {
179
185
  flexDirection: "row",
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { useCallback, useState } from "react";
13
- import { View, Text, ScrollView, StyleSheet } from "react-native";
13
+ import { View, Text, FlatList, StyleSheet } from "react-native";
14
14
  import { Navigation, buoyColors } from "@buoy-gg/shared-ui";
15
15
  import { RouteEventItemCompact } from "./RouteEventItemCompact";
16
16
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
@@ -38,16 +38,40 @@ export function RouteEventsTimeline({
38
38
  })]
39
39
  });
40
40
  }
41
- return /*#__PURE__*/_jsx(ScrollView, {
42
- contentContainerStyle: styles.listContent,
43
- children: events.map((event, index) => /*#__PURE__*/_jsx(RouteEventItemCompact, {
44
- event: event,
45
- visitNumber: visitCounts.get(index) || 1,
46
- isExpanded: expandedIndex === index,
47
- onPress: () => handleItemPress(index),
48
- onNavigate: onNavigate
49
- }, `event-${index}-${event.timestamp}`))
50
- });
41
+ return (
42
+ /*#__PURE__*/
43
+ // Virtualized — the previous ScrollView + map mounted ALL events (at
44
+ // the 500-event cap that measured 32.8 JS FPS during an idle dwell,
45
+ // tool-bench 2026-07-06). extraData re-renders visible rows when the
46
+ // expanded row changes.
47
+ _jsx(FlatList, {
48
+ contentContainerStyle: styles.listContent,
49
+ data: events
50
+ // STABLE content keys — index-based keys remount every row when a new
51
+ // event prepends (measured 4 JS FPS during automation, which emits
52
+ // route events on every bounce; tool-bench 2026-07-06).
53
+ ,
54
+ keyExtractor: event => `${event.timestamp}-${event.pathname}`,
55
+ extraData: expandedIndex,
56
+ renderItem: ({
57
+ item: event,
58
+ index
59
+ }) => /*#__PURE__*/_jsx(RouteEventItemCompact, {
60
+ event: event,
61
+ visitNumber: visitCounts.get(index) || 1,
62
+ isExpanded: expandedIndex === index,
63
+ index: index,
64
+ onPress: handleItemPress,
65
+ onNavigate: onNavigate
66
+ }),
67
+ showsVerticalScrollIndicator: true,
68
+ removeClippedSubviews: true,
69
+ initialNumToRender: 12,
70
+ maxToRenderPerBatch: 8,
71
+ windowSize: 5,
72
+ updateCellsBatchingPeriod: 50
73
+ })
74
+ );
51
75
  }
52
76
  const styles = StyleSheet.create({
53
77
  listContent: {
@@ -48,19 +48,28 @@ export function useRouteEvents(options = {}) {
48
48
  setIsListening(false);
49
49
  return;
50
50
  }
51
+
52
+ // Frame-coalesced (see network's useNetworkEventList): navigation
53
+ // bursts land as one commit per frame instead of one per emission.
54
+ let rafId = null;
55
+ let pending = null;
51
56
  const unsubscribe = routeEventStore.subscribeToEvents(allEvents => {
52
- // Limit events
53
- if (allEvents.length > maxEvents) {
54
- setEvents(allEvents.slice(0, maxEvents));
55
- } else {
56
- setEvents(allEvents);
57
- }
57
+ pending = allEvents;
58
+ if (rafId != null) return;
59
+ rafId = requestAnimationFrame(() => {
60
+ rafId = null;
61
+ if (!pending) return;
62
+ const latest = pending;
63
+ pending = null;
64
+ setEvents(latest.length > maxEvents ? latest.slice(0, maxEvents) : latest);
65
+ });
58
66
  });
59
67
 
60
68
  // Mark as listening after successful subscription
61
69
  setIsListening(true);
62
70
  return () => {
63
71
  unsubscribe();
72
+ if (rafId != null) cancelAnimationFrame(rafId);
64
73
  // Note: Don't set isListening=false here because we might be
65
74
  // re-subscribing due to dependency change, not actually stopping
66
75
  };
@@ -8,8 +8,11 @@ export interface RouteEventItemCompactProps {
8
8
  event: RouteChangeEvent;
9
9
  visitNumber: number;
10
10
  isExpanded: boolean;
11
- onPress: () => void;
11
+ /** Row's list index — passed back through onPress so the handler can
12
+ * stay a single stable callback (memo-compatible). */
13
+ index: number;
14
+ onPress: (index: number) => void;
12
15
  onNavigate?: (pathname: string) => void;
13
16
  }
14
- export declare function RouteEventItemCompact({ event, visitNumber, isExpanded, onPress, onNavigate, }: RouteEventItemCompactProps): import("react").JSX.Element;
17
+ export declare const RouteEventItemCompact: import("react").NamedExoticComponent<RouteEventItemCompactProps>;
15
18
  //# sourceMappingURL=RouteEventItemCompact.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"RouteEventItemCompact.d.ts","sourceRoot":"","sources":["../../../src/components/RouteEventItemCompact.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAGzD,MAAM,WAAW,0BAA0B;IACzC,KAAK,EAAE,gBAAgB,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AA6GD,wBAAgB,qBAAqB,CAAC,EACpC,KAAK,EACL,WAAW,EACX,UAAU,EACV,OAAO,EACP,UAAU,GACX,EAAE,0BAA0B,+BA6D5B"}
1
+ {"version":3,"file":"RouteEventItemCompact.d.ts","sourceRoot":"","sources":["../../../src/components/RouteEventItemCompact.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAGzD,MAAM,WAAW,0BAA0B;IACzC,KAAK,EAAE,gBAAgB,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB;0DACsD;IACtD,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AAgHD,eAAO,MAAM,qBAAqB,kEAmEhC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"RouteEventsTimeline.d.ts","sourceRoot":"","sources":["../../../src/components/RouteEventsTimeline.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAaH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAGzD,UAAU,wBAAwB;IAChC,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3B,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AAED,wBAAgB,mBAAmB,CAAC,EAClC,MAAM,EACN,WAAW,EACX,UAAU,GACX,EAAE,wBAAwB,+BAiC1B"}
1
+ {"version":3,"file":"RouteEventsTimeline.d.ts","sourceRoot":"","sources":["../../../src/components/RouteEventsTimeline.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAaH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAGzD,UAAU,wBAAwB;IAChC,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3B,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AAED,wBAAgB,mBAAmB,CAAC,EAClC,MAAM,EACN,WAAW,EACX,UAAU,GACX,EAAE,wBAAwB,+BAoD1B"}
@@ -1 +1 @@
1
- {"version":3,"file":"useRouteEvents.d.ts","sourceRoot":"","sources":["../../../src/hooks/useRouteEvents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAGH,OAAO,EAEL,KAAK,gBAAgB,EACtB,MAAM,2BAA2B,CAAC;AAGnC,YAAY,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAElE,MAAM,WAAW,qBAAqB;IACpC,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mFAAmF;IACnF,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,oBAAoB;IACnC,sCAAsC;IACtC,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3B,uBAAuB;IACvB,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,6DAA6D;IAC7D,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,wBAAgB,cAAc,CAC5B,OAAO,GAAE,qBAA0B,GAClC,oBAAoB,CA4CtB"}
1
+ {"version":3,"file":"useRouteEvents.d.ts","sourceRoot":"","sources":["../../../src/hooks/useRouteEvents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAGH,OAAO,EAEL,KAAK,gBAAgB,EACtB,MAAM,2BAA2B,CAAC;AAGnC,YAAY,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAElE,MAAM,WAAW,qBAAqB;IACpC,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mFAAmF;IACnF,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,oBAAoB;IACnC,sCAAsC;IACtC,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3B,uBAAuB;IACvB,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,6DAA6D;IAC7D,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,wBAAgB,cAAc,CAC5B,OAAO,GAAE,qBAA0B,GAClC,oBAAoB,CAoDtB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buoy-gg/route-events",
3
- "version": "4.0.2",
3
+ "version": "6.0.0",
4
4
  "description": "route-events package",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -26,11 +26,11 @@
26
26
  ],
27
27
  "sideEffects": false,
28
28
  "dependencies": {
29
- "@buoy-gg/floating-tools-core": "4.0.2",
30
- "@buoy-gg/shared-ui": "4.0.2"
29
+ "@buoy-gg/shared-ui": "6.0.0",
30
+ "@buoy-gg/floating-tools-core": "6.0.0"
31
31
  },
32
32
  "peerDependencies": {
33
- "@buoy-gg/license": "4.0.2",
33
+ "@buoy-gg/license": "6.0.0",
34
34
  "@react-native-async-storage/async-storage": "^2.0.0 || ^3.1.0",
35
35
  "@react-navigation/native": "*",
36
36
  "expo-router": "*",
@@ -49,7 +49,7 @@
49
49
  "@types/react-native": "^0.73.0",
50
50
  "expo-router": "~5.0.7",
51
51
  "typescript": "~5.8.3",
52
- "@buoy-gg/license": "4.0.2"
52
+ "@buoy-gg/license": "6.0.0"
53
53
  },
54
54
  "react-native-builder-bob": {
55
55
  "source": "src",