Skip to content

Commit 58fe1a4

Browse files
committed
Merge branch 'fix-memory-leaks' into bbc-main-autonext
2 parents b9606ee + c0869f5 commit 58fe1a4

5 files changed

Lines changed: 82 additions & 16 deletions

File tree

packages/webui/src/client/lib/VirtualElement.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,11 @@ export class ElementObserverManager {
440440
this.resizeObserver = new ResizeObserver((entries) => {
441441
entries.forEach((entry) => {
442442
const element = entry.target as HTMLElement
443+
if (!document.contains(element)) {
444+
this.observedElements.delete(element)
445+
this.resizeObserver.unobserve(element)
446+
return
447+
}
443448
const callback = this.observedElements.get(element)
444449
if (callback) {
445450
callback()
@@ -478,6 +483,11 @@ export class ElementObserverManager {
478483
})
479484

480485
targets.forEach((element) => {
486+
if (!document.contains(element)) {
487+
this.observedElements.delete(element)
488+
this.resizeObserver.unobserve(element)
489+
return
490+
}
481491
const callback = this.observedElements.get(element)
482492
if (callback) callback()
483493
})

packages/webui/src/client/ui/ClockView/CameraScreen/index.tsx

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ import { useBlackBrowserTheme } from '../../../lib/useBlackBrowserTheme.js'
2525
import { useWakeLock } from './useWakeLock.js'
2626
import { useDebounce } from '../../../lib/lib.js'
2727
import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub'
28-
import { useSetDocumentClass, useSetDocumentDarkTheme } from '../../util/useSetDocumentClass.js'
28+
import {
29+
useSetDocumentClass,
30+
useSetDocumentDarkTheme,
31+
useOwnedElementClassToggle,
32+
} from '../../util/useSetDocumentClass.js'
2933
import type { UIStudio } from '@sofie-automation/corelib/src/dataModel/Studio.js'
3034
import type { PartInstance } from '@sofie-automation/corelib/src/dataModel/PartInstance.js'
3135
import type { PieceExtended } from '@sofie-automation/corelib/src/dataModel/Piece.js'
@@ -145,21 +149,7 @@ export function CameraScreen({ playlist, studioId }: Readonly<IProps>): JSX.Elem
145149

146150
useSetDocumentClass('dark', 'xdark', 'vertical-overflow-only')
147151
useSetDocumentDarkTheme()
148-
149-
useEffect(() => {
150-
const getContainerEl = () => document.querySelector('#render-target > .container-fluid')
151-
const currentContainerEl = getContainerEl()
152-
if (currentContainerEl instanceof HTMLElement && currentContainerEl.isConnected) {
153-
currentContainerEl.classList.remove('header-clear')
154-
}
155-
156-
return () => {
157-
const cleanupContainerEl = getContainerEl()
158-
if (cleanupContainerEl instanceof HTMLElement && cleanupContainerEl.isConnected) {
159-
cleanupContainerEl.classList.add('header-clear')
160-
}
161-
}
162-
}, [])
152+
useOwnedElementClassToggle('#render-target > .container-fluid', 'header-clear')
163153

164154
const studio = useTracker(() => UIStudios.findOne(studioId), [studioId], undefined)
165155

packages/webui/src/client/ui/PreviewPopUp/PreviewPopUp.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import type { Padding, Placement, VirtualElement } from '@popperjs/core'
55

66
import './PreviewPopUp.scss'
77

8+
function isDetachedHTMLElementAnchor(anchor: HTMLElement | VirtualElement | null): anchor is HTMLElement {
9+
return anchor instanceof HTMLElement && !anchor.isConnected
10+
}
11+
812
export const PreviewPopUp = React.forwardRef<
913
PreviewPopUpHandle,
1014
React.PropsWithChildren<{
@@ -62,6 +66,7 @@ export const PreviewPopUp = React.forwardRef<
6266
anchor?.getBoundingClientRect().y ?? 0
6367
),
6468
})
69+
const anchorRef = useRef(anchor)
6570
const anchorYRef = useRef(anchor?.getBoundingClientRect().y ?? 0)
6671
const { styles, attributes, update } = usePopper(
6772
trackMouse ? virtualElement.current : anchor,
@@ -76,12 +81,14 @@ export const PreviewPopUp = React.forwardRef<
7681
}, [update])
7782

7883
useEffect(() => {
84+
anchorRef.current = anchor
7985
anchorYRef.current = anchor?.getBoundingClientRect().y ?? 0
8086
}, [anchor])
8187

8288
useEffect(() => {
8389
if (trackMouse) {
8490
const listener = ({ clientX: x }: MouseEvent) => {
91+
if (isDetachedHTMLElementAnchor(anchorRef.current)) return
8592
virtualElement.current.getBoundingClientRect = generateGetBoundingClientRect(x, anchorYRef.current)
8693
// If update is available, call it to reposition the popper:
8794
if (updateRef.current) {
@@ -107,6 +114,7 @@ export const PreviewPopUp = React.forwardRef<
107114
useImperativeHandle(ref, () => {
108115
return {
109116
update: () => {
117+
if (isDetachedHTMLElementAnchor(anchorRef.current)) return
110118
if (!updateRef.current) return
111119
updateRef.current().catch(console.error)
112120
},

packages/webui/src/client/ui/PreviewPopUp/PreviewPopUpContext.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,10 @@ export function PreviewPopUpContextProvider({ children }: React.PropsWithChildre
395395
const [t, setTime] = useState<number | null>(null)
396396
const [previewSessionKey, setPreviewSessionKey] = useState(0)
397397

398+
const isDetachedHTMLElement = (anchor: HTMLElement | VirtualElement): boolean => {
399+
return anchor instanceof HTMLElement && !anchor.isConnected
400+
}
401+
398402
const closeSession = useCallback(() => {
399403
const previousHandle = currentHandle.current
400404
if (previousHandle) {
@@ -415,6 +419,16 @@ export function PreviewPopUpContextProvider({ children }: React.PropsWithChildre
415419

416420
const context: IPreviewPopUpContext = {
417421
requestPreview: (anchor, content, opts) => {
422+
if (isDetachedHTMLElement(anchor)) {
423+
closeSession()
424+
const closedHandle: IPreviewPopUpSession = {
425+
close: () => undefined,
426+
update: () => undefined,
427+
setPointerTime: () => undefined,
428+
}
429+
return closedHandle
430+
}
431+
418432
closeSession()
419433
setPreviewSessionKey((prev) => prev + 1)
420434

@@ -437,13 +451,21 @@ export function PreviewPopUpContextProvider({ children }: React.PropsWithChildre
437451
close: closeSession,
438452
update: (contents) => {
439453
if (currentHandle.current !== handle) return
454+
if (isDetachedHTMLElement(anchor)) {
455+
closeSession()
456+
return
457+
}
440458
if (contents) {
441459
setPreviewContent(contents)
442460
}
443461
previewRef.current?.update()
444462
},
445463
setPointerTime: (t) => {
446464
if (currentHandle.current !== handle) return
465+
if (isDetachedHTMLElement(anchor)) {
466+
closeSession()
467+
return
468+
}
447469
setTime(t)
448470
},
449471
}

packages/webui/src/client/ui/util/useSetDocumentClass.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useLayoutEffect } from 'react'
2+
import { useEffect, useRef } from 'react'
23

34
/**
45
* Adds the provided classes to `document.body` upon mount, and removes them when unmounted
@@ -23,3 +24,38 @@ export function useSetDocumentDarkTheme(): void {
2324
}
2425
}, [])
2526
}
27+
28+
/**
29+
* Removes a class from an element on mount and adds it back on unmount,
30+
* but only if this hook instance removed it.
31+
*/
32+
export function useOwnedElementClassToggle(
33+
selector: string,
34+
className: string,
35+
removeOnMount = true
36+
): void {
37+
const removedByHookRef = useRef(false)
38+
39+
useEffect(() => {
40+
const element = document.querySelector(selector)
41+
if (element instanceof HTMLElement && element.isConnected && removeOnMount) {
42+
removedByHookRef.current = element.classList.contains(className)
43+
if (removedByHookRef.current) {
44+
element.classList.remove(className)
45+
}
46+
}
47+
48+
return () => {
49+
const cleanupElement = document.querySelector(selector)
50+
if (
51+
removedByHookRef.current &&
52+
cleanupElement instanceof HTMLElement &&
53+
cleanupElement.isConnected
54+
) {
55+
cleanupElement.classList.add(className)
56+
}
57+
58+
removedByHookRef.current = false
59+
}
60+
}, [selector, className, removeOnMount])
61+
}

0 commit comments

Comments
 (0)