@roj-ai/debug 0.1.12 → 0.1.14
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/providers/EventPollingProvider.d.ts +5 -11
- package/dist/providers/EventPollingProvider.d.ts.map +1 -1
- package/dist/providers/EventPollingProvider.js +65 -22
- package/dist/providers/EventPollingProvider.js.map +1 -1
- package/package.json +4 -4
- package/src/providers/EventPollingProvider.tsx +65 -25
|
@@ -5,20 +5,14 @@ interface EventPollingProviderProps {
|
|
|
5
5
|
/**
|
|
6
6
|
* Provider component that manages event polling for a session.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* Usage:
|
|
12
|
-
* ```tsx
|
|
13
|
-
* <EventPollingProvider sessionId={sessionId}>
|
|
14
|
-
* <DebugViews />
|
|
15
|
-
* </EventPollingProvider>
|
|
16
|
-
* ```
|
|
8
|
+
* Polling is paused when the tab is hidden or the user has been idle for
|
|
9
|
+
* IDLE_TIMEOUT_MS. It resumes (and immediately catches up) on activity or
|
|
10
|
+
* tab refocus. Unmounting clears polling and resets the store.
|
|
17
11
|
*/
|
|
18
12
|
export declare function EventPollingProvider({ sessionId, children }: EventPollingProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
19
13
|
/**
|
|
20
|
-
* Hook
|
|
21
|
-
*
|
|
14
|
+
* Hook variant of EventPollingProvider for callers that need access to
|
|
15
|
+
* isLoading / error.
|
|
22
16
|
*/
|
|
23
17
|
export declare function useEventPolling(sessionId: string | undefined): {
|
|
24
18
|
isLoading: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EventPollingProvider.d.ts","sourceRoot":"","sources":["../../src/providers/EventPollingProvider.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"EventPollingProvider.d.ts","sourceRoot":"","sources":["../../src/providers/EventPollingProvider.tsx"],"names":[],"mappings":"AAMA,UAAU,yBAAyB;IAClC,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CACzB;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,yBAAyB,2CAGtF;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS;;;EAwE5D"}
|
|
@@ -1,49 +1,92 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect } from 'react';
|
|
3
3
|
import { useEventStore } from '../stores/event-store.js';
|
|
4
|
+
const IDLE_TIMEOUT_MS = 2 * 60 * 1000;
|
|
5
|
+
const ACTIVITY_EVENTS = ['mousedown', 'mousemove', 'keydown', 'touchstart', 'scroll', 'wheel'];
|
|
4
6
|
/**
|
|
5
7
|
* Provider component that manages event polling for a session.
|
|
6
8
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* Usage:
|
|
11
|
-
* ```tsx
|
|
12
|
-
* <EventPollingProvider sessionId={sessionId}>
|
|
13
|
-
* <DebugViews />
|
|
14
|
-
* </EventPollingProvider>
|
|
15
|
-
* ```
|
|
9
|
+
* Polling is paused when the tab is hidden or the user has been idle for
|
|
10
|
+
* IDLE_TIMEOUT_MS. It resumes (and immediately catches up) on activity or
|
|
11
|
+
* tab refocus. Unmounting clears polling and resets the store.
|
|
16
12
|
*/
|
|
17
13
|
export function EventPollingProvider({ sessionId, children }) {
|
|
18
|
-
|
|
19
|
-
const reset = useEventStore((s) => s.reset);
|
|
20
|
-
useEffect(() => {
|
|
21
|
-
loadSession(sessionId);
|
|
22
|
-
// Cleanup on unmount or sessionId change
|
|
23
|
-
return () => {
|
|
24
|
-
reset();
|
|
25
|
-
};
|
|
26
|
-
}, [sessionId, loadSession, reset]);
|
|
14
|
+
useEventPolling(sessionId);
|
|
27
15
|
return _jsx(_Fragment, { children: children });
|
|
28
16
|
}
|
|
29
17
|
/**
|
|
30
|
-
* Hook
|
|
31
|
-
*
|
|
18
|
+
* Hook variant of EventPollingProvider for callers that need access to
|
|
19
|
+
* isLoading / error.
|
|
32
20
|
*/
|
|
33
21
|
export function useEventPolling(sessionId) {
|
|
34
22
|
const loadSession = useEventStore((s) => s.loadSession);
|
|
35
23
|
const reset = useEventStore((s) => s.reset);
|
|
24
|
+
const startPolling = useEventStore((s) => s.startPolling);
|
|
25
|
+
const stopPolling = useEventStore((s) => s.stopPolling);
|
|
26
|
+
const fetchNewEvents = useEventStore((s) => s.fetchNewEvents);
|
|
36
27
|
const isLoading = useEventStore((s) => s.isLoading);
|
|
37
28
|
const error = useEventStore((s) => s.error);
|
|
38
29
|
useEffect(() => {
|
|
39
30
|
if (!sessionId)
|
|
40
31
|
return;
|
|
32
|
+
let idleTimer = null;
|
|
33
|
+
let isIdle = false;
|
|
34
|
+
let cancelled = false;
|
|
35
|
+
const reconcile = () => {
|
|
36
|
+
if (cancelled)
|
|
37
|
+
return;
|
|
38
|
+
const state = useEventStore.getState();
|
|
39
|
+
if (state.sessionId !== sessionId || state.isLoading)
|
|
40
|
+
return;
|
|
41
|
+
const shouldPoll = document.visibilityState === 'visible' && !isIdle;
|
|
42
|
+
if (shouldPoll && !state.isPolling) {
|
|
43
|
+
fetchNewEvents();
|
|
44
|
+
startPolling();
|
|
45
|
+
}
|
|
46
|
+
else if (!shouldPoll && state.isPolling) {
|
|
47
|
+
stopPolling();
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const onActivity = () => {
|
|
51
|
+
if (idleTimer)
|
|
52
|
+
clearTimeout(idleTimer);
|
|
53
|
+
if (isIdle) {
|
|
54
|
+
isIdle = false;
|
|
55
|
+
reconcile();
|
|
56
|
+
}
|
|
57
|
+
idleTimer = setTimeout(() => {
|
|
58
|
+
isIdle = true;
|
|
59
|
+
reconcile();
|
|
60
|
+
}, IDLE_TIMEOUT_MS);
|
|
61
|
+
};
|
|
62
|
+
const onVisibility = () => reconcile();
|
|
63
|
+
for (const ev of ACTIVITY_EVENTS) {
|
|
64
|
+
window.addEventListener(ev, onActivity, { passive: true });
|
|
65
|
+
}
|
|
66
|
+
document.addEventListener('visibilitychange', onVisibility);
|
|
67
|
+
// Reconcile after the initial load finishes — loadSession auto-starts
|
|
68
|
+
// polling on success, so we need to immediately stop it if the tab is
|
|
69
|
+
// hidden / user is idle at that moment.
|
|
70
|
+
const unsubscribe = useEventStore.subscribe((state, prev) => {
|
|
71
|
+
if (state.sessionId !== sessionId)
|
|
72
|
+
return;
|
|
73
|
+
if (state.isLoading !== prev.isLoading)
|
|
74
|
+
reconcile();
|
|
75
|
+
});
|
|
76
|
+
onActivity(); // arm the idle timer
|
|
41
77
|
loadSession(sessionId);
|
|
42
|
-
// Cleanup on unmount or sessionId change
|
|
43
78
|
return () => {
|
|
79
|
+
cancelled = true;
|
|
80
|
+
if (idleTimer)
|
|
81
|
+
clearTimeout(idleTimer);
|
|
82
|
+
for (const ev of ACTIVITY_EVENTS) {
|
|
83
|
+
window.removeEventListener(ev, onActivity);
|
|
84
|
+
}
|
|
85
|
+
document.removeEventListener('visibilitychange', onVisibility);
|
|
86
|
+
unsubscribe();
|
|
44
87
|
reset();
|
|
45
88
|
};
|
|
46
|
-
}, [sessionId, loadSession, reset]);
|
|
89
|
+
}, [sessionId, loadSession, reset, startPolling, stopPolling, fetchNewEvents]);
|
|
47
90
|
return { isLoading, error };
|
|
48
91
|
}
|
|
49
92
|
//# sourceMappingURL=EventPollingProvider.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EventPollingProvider.js","sourceRoot":"","sources":["../../src/providers/EventPollingProvider.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACjC,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;
|
|
1
|
+
{"version":3,"file":"EventPollingProvider.js","sourceRoot":"","sources":["../../src/providers/EventPollingProvider.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@roj-ai/debug",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -30,13 +30,13 @@
|
|
|
30
30
|
"type-check": "tsc --noEmit"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@roj-ai/client": "^0.1.
|
|
34
|
-
"@roj-ai/shared": "^0.1.
|
|
33
|
+
"@roj-ai/client": "^0.1.14",
|
|
34
|
+
"@roj-ai/shared": "^0.1.14",
|
|
35
35
|
"tokenx": "1.3.0",
|
|
36
36
|
"zustand": "5.0.11"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@roj-ai/sdk": "^0.1.
|
|
39
|
+
"@roj-ai/sdk": "^0.1.14",
|
|
40
40
|
"@types/react": "19.2.14",
|
|
41
41
|
"@types/react-dom": "19.2.3",
|
|
42
42
|
"react": "19.2.4",
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { useEffect } from 'react'
|
|
2
2
|
import { useEventStore } from '../stores/event-store.js'
|
|
3
3
|
|
|
4
|
+
const IDLE_TIMEOUT_MS = 2 * 60 * 1000
|
|
5
|
+
const ACTIVITY_EVENTS = ['mousedown', 'mousemove', 'keydown', 'touchstart', 'scroll', 'wheel'] as const
|
|
6
|
+
|
|
4
7
|
interface EventPollingProviderProps {
|
|
5
8
|
sessionId: string
|
|
6
9
|
children: React.ReactNode
|
|
@@ -9,52 +12,89 @@ interface EventPollingProviderProps {
|
|
|
9
12
|
/**
|
|
10
13
|
* Provider component that manages event polling for a session.
|
|
11
14
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* Usage:
|
|
16
|
-
* ```tsx
|
|
17
|
-
* <EventPollingProvider sessionId={sessionId}>
|
|
18
|
-
* <DebugViews />
|
|
19
|
-
* </EventPollingProvider>
|
|
20
|
-
* ```
|
|
15
|
+
* Polling is paused when the tab is hidden or the user has been idle for
|
|
16
|
+
* IDLE_TIMEOUT_MS. It resumes (and immediately catches up) on activity or
|
|
17
|
+
* tab refocus. Unmounting clears polling and resets the store.
|
|
21
18
|
*/
|
|
22
19
|
export function EventPollingProvider({ sessionId, children }: EventPollingProviderProps) {
|
|
23
|
-
|
|
24
|
-
const reset = useEventStore((s) => s.reset)
|
|
25
|
-
|
|
26
|
-
useEffect(() => {
|
|
27
|
-
loadSession(sessionId)
|
|
28
|
-
|
|
29
|
-
// Cleanup on unmount or sessionId change
|
|
30
|
-
return () => {
|
|
31
|
-
reset()
|
|
32
|
-
}
|
|
33
|
-
}, [sessionId, loadSession, reset])
|
|
34
|
-
|
|
20
|
+
useEventPolling(sessionId)
|
|
35
21
|
return <>{children}</>
|
|
36
22
|
}
|
|
37
23
|
|
|
38
24
|
/**
|
|
39
|
-
* Hook
|
|
40
|
-
*
|
|
25
|
+
* Hook variant of EventPollingProvider for callers that need access to
|
|
26
|
+
* isLoading / error.
|
|
41
27
|
*/
|
|
42
28
|
export function useEventPolling(sessionId: string | undefined) {
|
|
43
29
|
const loadSession = useEventStore((s) => s.loadSession)
|
|
44
30
|
const reset = useEventStore((s) => s.reset)
|
|
31
|
+
const startPolling = useEventStore((s) => s.startPolling)
|
|
32
|
+
const stopPolling = useEventStore((s) => s.stopPolling)
|
|
33
|
+
const fetchNewEvents = useEventStore((s) => s.fetchNewEvents)
|
|
45
34
|
const isLoading = useEventStore((s) => s.isLoading)
|
|
46
35
|
const error = useEventStore((s) => s.error)
|
|
47
36
|
|
|
48
37
|
useEffect(() => {
|
|
49
38
|
if (!sessionId) return
|
|
50
39
|
|
|
40
|
+
let idleTimer: ReturnType<typeof setTimeout> | null = null
|
|
41
|
+
let isIdle = false
|
|
42
|
+
let cancelled = false
|
|
43
|
+
|
|
44
|
+
const reconcile = () => {
|
|
45
|
+
if (cancelled) return
|
|
46
|
+
const state = useEventStore.getState()
|
|
47
|
+
if (state.sessionId !== sessionId || state.isLoading) return
|
|
48
|
+
const shouldPoll = document.visibilityState === 'visible' && !isIdle
|
|
49
|
+
if (shouldPoll && !state.isPolling) {
|
|
50
|
+
fetchNewEvents()
|
|
51
|
+
startPolling()
|
|
52
|
+
} else if (!shouldPoll && state.isPolling) {
|
|
53
|
+
stopPolling()
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const onActivity = () => {
|
|
58
|
+
if (idleTimer) clearTimeout(idleTimer)
|
|
59
|
+
if (isIdle) {
|
|
60
|
+
isIdle = false
|
|
61
|
+
reconcile()
|
|
62
|
+
}
|
|
63
|
+
idleTimer = setTimeout(() => {
|
|
64
|
+
isIdle = true
|
|
65
|
+
reconcile()
|
|
66
|
+
}, IDLE_TIMEOUT_MS)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const onVisibility = () => reconcile()
|
|
70
|
+
|
|
71
|
+
for (const ev of ACTIVITY_EVENTS) {
|
|
72
|
+
window.addEventListener(ev, onActivity, { passive: true })
|
|
73
|
+
}
|
|
74
|
+
document.addEventListener('visibilitychange', onVisibility)
|
|
75
|
+
|
|
76
|
+
// Reconcile after the initial load finishes — loadSession auto-starts
|
|
77
|
+
// polling on success, so we need to immediately stop it if the tab is
|
|
78
|
+
// hidden / user is idle at that moment.
|
|
79
|
+
const unsubscribe = useEventStore.subscribe((state, prev) => {
|
|
80
|
+
if (state.sessionId !== sessionId) return
|
|
81
|
+
if (state.isLoading !== prev.isLoading) reconcile()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
onActivity() // arm the idle timer
|
|
51
85
|
loadSession(sessionId)
|
|
52
86
|
|
|
53
|
-
// Cleanup on unmount or sessionId change
|
|
54
87
|
return () => {
|
|
88
|
+
cancelled = true
|
|
89
|
+
if (idleTimer) clearTimeout(idleTimer)
|
|
90
|
+
for (const ev of ACTIVITY_EVENTS) {
|
|
91
|
+
window.removeEventListener(ev, onActivity)
|
|
92
|
+
}
|
|
93
|
+
document.removeEventListener('visibilitychange', onVisibility)
|
|
94
|
+
unsubscribe()
|
|
55
95
|
reset()
|
|
56
96
|
}
|
|
57
|
-
}, [sessionId, loadSession, reset])
|
|
97
|
+
}, [sessionId, loadSession, reset, startPolling, stopPolling, fetchNewEvents])
|
|
58
98
|
|
|
59
99
|
return { isLoading, error }
|
|
60
100
|
}
|