aboutsummaryrefslogtreecommitdiff
path: root/apps/browser-extension/utils/route-detection.ts
blob: a8a4714f6fb5abb16b96de53ca7a335f4396e46b (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
/**
 * Route Detection Utilities
 * Shared logic for detecting route changes across different AI chat platforms
 */

import { UI_CONFIG } from "./constants"

export interface RouteDetectionConfig {
	platform: string
	selectors: string[]
	reinitCallback: () => void
	checkInterval?: number
	observerThrottleDelay?: number
}

export interface RouteDetectionCleanup {
	observer: MutationObserver | null
	urlCheckInterval: NodeJS.Timeout | null
	observerThrottle: NodeJS.Timeout | null
}

export function createRouteDetection(
	config: RouteDetectionConfig,
	cleanup: RouteDetectionCleanup,
): void {
	if (cleanup.observer) {
		cleanup.observer.disconnect()
	}
	if (cleanup.urlCheckInterval) {
		clearInterval(cleanup.urlCheckInterval)
	}
	if (cleanup.observerThrottle) {
		clearTimeout(cleanup.observerThrottle)
		cleanup.observerThrottle = null
	}

	let currentUrl = window.location.href

	const checkForRouteChange = () => {
		if (window.location.href !== currentUrl) {
			currentUrl = window.location.href
			console.log(`${config.platform} route changed, re-initializing`)
			setTimeout(config.reinitCallback, 1000)
		}
	}

	cleanup.urlCheckInterval = setInterval(
		checkForRouteChange,
		config.checkInterval || UI_CONFIG.ROUTE_CHECK_INTERVAL,
	)

	cleanup.observer = new MutationObserver((mutations) => {
		if (cleanup.observerThrottle) {
			return
		}

		let shouldRecheck = false
		mutations.forEach((mutation) => {
			if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
				mutation.addedNodes.forEach((node) => {
					if (node.nodeType === Node.ELEMENT_NODE) {
						const element = node as Element

						for (const selector of config.selectors) {
							if (
								element.querySelector?.(selector) ||
								element.matches?.(selector)
							) {
								shouldRecheck = true
								break
							}
						}
					}
				})
			}
		})

		if (shouldRecheck) {
			cleanup.observerThrottle = setTimeout(() => {
				try {
					cleanup.observerThrottle = null
					config.reinitCallback()
				} catch (error) {
					console.error(`Error in ${config.platform} observer callback:`, error)
				}
			}, config.observerThrottleDelay || UI_CONFIG.OBSERVER_THROTTLE_DELAY)
		}
	})

	try {
		cleanup.observer.observe(document.body, {
			childList: true,
			subtree: true,
		})
	} catch (error) {
		console.error(`Failed to set up ${config.platform} route observer:`, error)
		if (cleanup.urlCheckInterval) {
			clearInterval(cleanup.urlCheckInterval)
		}
		cleanup.urlCheckInterval = setInterval(checkForRouteChange, 1000)
	}
}

export function cleanupRouteDetection(cleanup: RouteDetectionCleanup): void {
	if (cleanup.observer) {
		cleanup.observer.disconnect()
		cleanup.observer = null
	}
	if (cleanup.urlCheckInterval) {
		clearInterval(cleanup.urlCheckInterval)
		cleanup.urlCheckInterval = null
	}
	if (cleanup.observerThrottle) {
		clearTimeout(cleanup.observerThrottle)
		cleanup.observerThrottle = null
	}
}