@datum-cloud/activity-ui 0.6.0-dev.5b51180 → 0.6.0-dev.917556e
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/hooks/useActivityFeed.d.ts.map +1 -1
- package/dist/index.esm.js +71 -12
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +71 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -9794,6 +9794,14 @@ function ActionToggle({ value, onChange, className = '', disabled = false, }) {
|
|
|
9794
9794
|
|
|
9795
9795
|
// Debounce delay for filter changes (ms)
|
|
9796
9796
|
const FILTER_DEBOUNCE_MS$1 = 300;
|
|
9797
|
+
// Backoff for watch stream reconnects. Without this, a persistently failing
|
|
9798
|
+
// upstream gets re-opened on every render with no delay — see
|
|
9799
|
+
// https://github.com/datum-cloud/infra/issues/5060, where a single browser
|
|
9800
|
+
// tab reconnected roughly 2.5 times a minute against a stream that kept
|
|
9801
|
+
// aborting, driving the proxy's memory up until the process was OOMKilled.
|
|
9802
|
+
const RECONNECT_BASE_DELAY_MS = 1000;
|
|
9803
|
+
const RECONNECT_MAX_DELAY_MS = 30000;
|
|
9804
|
+
const MAX_RECONNECT_ATTEMPTS = 8;
|
|
9797
9805
|
/**
|
|
9798
9806
|
* Escape a value for safe interpolation into a CEL string literal.
|
|
9799
9807
|
* Prevents injection via values containing backslashes or double quotes.
|
|
@@ -9888,6 +9896,12 @@ function useActivityFeed({ client, initialFilters = {}, initialTimeRange = { sta
|
|
|
9888
9896
|
const resourceVersionRef = React.useRef();
|
|
9889
9897
|
// Track the watch stop function
|
|
9890
9898
|
const watchStopRef = React.useRef(null);
|
|
9899
|
+
// Consecutive watch failures since the last successful event, used to back
|
|
9900
|
+
// off reconnect attempts. Reset on any real event (including BOOKMARK) and
|
|
9901
|
+
// on a manual startStreaming() call.
|
|
9902
|
+
const reconnectAttemptsRef = React.useRef(0);
|
|
9903
|
+
// Pending scheduled reconnect, if any.
|
|
9904
|
+
const reconnectTimerRef = React.useRef(null);
|
|
9891
9905
|
// Track whether streaming should restart after filter change
|
|
9892
9906
|
const shouldRestartStreamingRef = React.useRef(false);
|
|
9893
9907
|
// Track if we've done the initial load
|
|
@@ -9962,6 +9976,9 @@ function useActivityFeed({ client, initialFilters = {}, initialTimeRange = { sta
|
|
|
9962
9976
|
console.error('Watch error:', event.object);
|
|
9963
9977
|
return;
|
|
9964
9978
|
}
|
|
9979
|
+
// A real event (including a BOOKMARK keep-alive) means the connection is
|
|
9980
|
+
// healthy — reset backoff so a later failure starts from the base delay.
|
|
9981
|
+
reconnectAttemptsRef.current = 0;
|
|
9965
9982
|
if (event.type === 'BOOKMARK') {
|
|
9966
9983
|
// Update resource version for resume capability
|
|
9967
9984
|
if (event.object.metadata?.resourceVersion) {
|
|
@@ -10027,15 +10044,13 @@ function useActivityFeed({ client, initialFilters = {}, initialTimeRange = { sta
|
|
|
10027
10044
|
setActivities((prev) => prev.filter((a) => a.metadata?.name !== event.object.metadata?.name));
|
|
10028
10045
|
}
|
|
10029
10046
|
}, [filters]);
|
|
10030
|
-
//
|
|
10031
|
-
|
|
10047
|
+
// Open the watch connection. Does not touch userPaused/backoff state —
|
|
10048
|
+
// callers (startStreaming, scheduleReconnect) own that.
|
|
10049
|
+
const connectWatch = React.useCallback(() => {
|
|
10032
10050
|
if (watchStopRef.current) {
|
|
10033
10051
|
// Already watching
|
|
10034
10052
|
return;
|
|
10035
10053
|
}
|
|
10036
|
-
// Explicit start clears the user-paused flag so the auto-restart
|
|
10037
|
-
// effects can keep the stream alive after subsequent filter changes.
|
|
10038
|
-
setUserPaused(false);
|
|
10039
10054
|
// Clear any previous watch error when starting a new stream
|
|
10040
10055
|
setWatchError(null);
|
|
10041
10056
|
const params = buildWatchParams();
|
|
@@ -10049,6 +10064,7 @@ function useActivityFeed({ client, initialFilters = {}, initialTimeRange = { sta
|
|
|
10049
10064
|
setWatchError(streamError);
|
|
10050
10065
|
setIsStreaming(false);
|
|
10051
10066
|
watchStopRef.current = null;
|
|
10067
|
+
reconnectAttemptsRef.current += 1;
|
|
10052
10068
|
},
|
|
10053
10069
|
onClose: () => {
|
|
10054
10070
|
setIsStreaming(false);
|
|
@@ -10059,11 +10075,50 @@ function useActivityFeed({ client, initialFilters = {}, initialTimeRange = { sta
|
|
|
10059
10075
|
setIsStreaming(true);
|
|
10060
10076
|
setNewActivitiesCount(0);
|
|
10061
10077
|
}, [client, buildWatchParams, handleWatchEvent]);
|
|
10078
|
+
// Reconnect with exponential backoff. The first attempt (no prior
|
|
10079
|
+
// failures) connects immediately; each subsequent failure doubles the
|
|
10080
|
+
// delay up to RECONNECT_MAX_DELAY_MS. After MAX_RECONNECT_ATTEMPTS
|
|
10081
|
+
// consecutive failures, this stops retrying automatically — the watch
|
|
10082
|
+
// stays stopped (with watchError set) until the user calls
|
|
10083
|
+
// startStreaming() again, which resets the attempt count.
|
|
10084
|
+
const scheduleReconnect = React.useCallback(() => {
|
|
10085
|
+
if (reconnectTimerRef.current) {
|
|
10086
|
+
// A reconnect is already scheduled.
|
|
10087
|
+
return;
|
|
10088
|
+
}
|
|
10089
|
+
if (reconnectAttemptsRef.current >= MAX_RECONNECT_ATTEMPTS) {
|
|
10090
|
+
return;
|
|
10091
|
+
}
|
|
10092
|
+
const attempt = reconnectAttemptsRef.current;
|
|
10093
|
+
const delay = attempt === 0 ? 0 : Math.min(RECONNECT_MAX_DELAY_MS, RECONNECT_BASE_DELAY_MS * 2 ** (attempt - 1));
|
|
10094
|
+
reconnectTimerRef.current = setTimeout(() => {
|
|
10095
|
+
reconnectTimerRef.current = null;
|
|
10096
|
+
connectWatch();
|
|
10097
|
+
}, delay);
|
|
10098
|
+
}, [connectWatch]);
|
|
10099
|
+
// Start watching for real-time updates. This is the user/effect-facing
|
|
10100
|
+
// entry point: it resets backoff and any pending scheduled reconnect
|
|
10101
|
+
// before connecting, so a manual retry always starts from a clean slate.
|
|
10102
|
+
const startStreaming = React.useCallback(() => {
|
|
10103
|
+
// Explicit start clears the user-paused flag so the auto-restart
|
|
10104
|
+
// effects can keep the stream alive after subsequent filter changes.
|
|
10105
|
+
setUserPaused(false);
|
|
10106
|
+
reconnectAttemptsRef.current = 0;
|
|
10107
|
+
if (reconnectTimerRef.current) {
|
|
10108
|
+
clearTimeout(reconnectTimerRef.current);
|
|
10109
|
+
reconnectTimerRef.current = null;
|
|
10110
|
+
}
|
|
10111
|
+
connectWatch();
|
|
10112
|
+
}, [connectWatch]);
|
|
10062
10113
|
// Stop watching. Marks the user as paused so the auto-start effects
|
|
10063
10114
|
// below don't immediately re-open the stream this call just closed.
|
|
10064
10115
|
const stopStreaming = React.useCallback(() => {
|
|
10065
10116
|
setUserPaused(true);
|
|
10066
10117
|
setIsStreaming(false);
|
|
10118
|
+
if (reconnectTimerRef.current) {
|
|
10119
|
+
clearTimeout(reconnectTimerRef.current);
|
|
10120
|
+
reconnectTimerRef.current = null;
|
|
10121
|
+
}
|
|
10067
10122
|
if (watchStopRef.current) {
|
|
10068
10123
|
watchStopRef.current();
|
|
10069
10124
|
watchStopRef.current = null;
|
|
@@ -10226,9 +10281,10 @@ function useActivityFeed({ client, initialFilters = {}, initialTimeRange = { sta
|
|
|
10226
10281
|
}
|
|
10227
10282
|
};
|
|
10228
10283
|
}, [filters, timeRange]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
10229
|
-
// Auto-start streaming after initial load when enabled
|
|
10230
|
-
//
|
|
10231
|
-
//
|
|
10284
|
+
// Auto-start streaming after initial load when enabled, and reconnect
|
|
10285
|
+
// (with backoff — see scheduleReconnect) whenever a connection drops.
|
|
10286
|
+
// Skipped when the user has explicitly paused — otherwise clicking Pause
|
|
10287
|
+
// would be immediately undone by this effect on the next render.
|
|
10232
10288
|
React.useEffect(() => {
|
|
10233
10289
|
if (enableStreaming &&
|
|
10234
10290
|
autoStartStreaming &&
|
|
@@ -10236,9 +10292,9 @@ function useActivityFeed({ client, initialFilters = {}, initialTimeRange = { sta
|
|
|
10236
10292
|
activities.length > 0 &&
|
|
10237
10293
|
!isStreaming &&
|
|
10238
10294
|
!isLoading) {
|
|
10239
|
-
|
|
10295
|
+
scheduleReconnect();
|
|
10240
10296
|
}
|
|
10241
|
-
}, [enableStreaming, autoStartStreaming, userPaused, activities.length, isStreaming, isLoading,
|
|
10297
|
+
}, [enableStreaming, autoStartStreaming, userPaused, activities.length, isStreaming, isLoading, scheduleReconnect]);
|
|
10242
10298
|
// Restart streaming after filter change refresh completes. Also skipped
|
|
10243
10299
|
// when the user has paused — a filter change shouldn't silently resume
|
|
10244
10300
|
// a stream the user intentionally stopped.
|
|
@@ -10250,15 +10306,18 @@ function useActivityFeed({ client, initialFilters = {}, initialTimeRange = { sta
|
|
|
10250
10306
|
!isStreaming &&
|
|
10251
10307
|
!isLoading) {
|
|
10252
10308
|
shouldRestartStreamingRef.current = false;
|
|
10253
|
-
|
|
10309
|
+
scheduleReconnect();
|
|
10254
10310
|
}
|
|
10255
|
-
}, [enableStreaming, userPaused, activities.length, isStreaming, isLoading,
|
|
10311
|
+
}, [enableStreaming, userPaused, activities.length, isStreaming, isLoading, scheduleReconnect]);
|
|
10256
10312
|
// Cleanup on unmount
|
|
10257
10313
|
React.useEffect(() => {
|
|
10258
10314
|
return () => {
|
|
10259
10315
|
if (watchStopRef.current) {
|
|
10260
10316
|
watchStopRef.current();
|
|
10261
10317
|
}
|
|
10318
|
+
if (reconnectTimerRef.current) {
|
|
10319
|
+
clearTimeout(reconnectTimerRef.current);
|
|
10320
|
+
}
|
|
10262
10321
|
if (filterDebounceRef.current) {
|
|
10263
10322
|
clearTimeout(filterDebounceRef.current);
|
|
10264
10323
|
}
|