Skip to content

Commit 7af16ab

Browse files
committed
Revert "fix: added service clean up detached nodes - add description"
This reverts commit 40d0598.
1 parent 40d0598 commit 7af16ab

File tree

1 file changed

+1
-356
lines changed

1 file changed

+1
-356
lines changed

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

Lines changed: 1 addition & 356 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Meteor } from 'meteor/meteor'
2-
import React, { useContext, useEffect, useMemo } from 'react'
2+
import React, { useContext, useMemo } from 'react'
33
import { ParsedQuery, parse as queryStringParse } from 'query-string'
44
import { Translated, translateWithTracker, useTracker } from '../lib/ReactMeteorData/react-meteor-data.js'
55
import { VTContent, NoteSeverity, ISourceLayer } from '@sofie-automation/blueprints-integration'
@@ -284,361 +284,6 @@ export function RundownView(props: Readonly<IProps>): JSX.Element {
284284
partInstances?.currentPartInstance
285285
)
286286

287-
// This is a hack, as the VirtualElement is not releasing its memory properly:
288-
useEffect(() => {
289-
const detachedNodes = new Set<Element>()
290-
291-
// Set up observer to catch nodes when they get removed
292-
const observer = new MutationObserver((mutations) => {
293-
mutations.forEach((mutation) => {
294-
mutation.removedNodes.forEach((node) => {
295-
if (node instanceof Element) {
296-
detachedNodes.add(node)
297-
}
298-
})
299-
})
300-
})
301-
302-
observer.observe(document.body, {
303-
childList: true,
304-
subtree: true,
305-
})
306-
307-
function cleanupDetachedNodesFromMemory(): number {
308-
let removedCount = 0
309-
// Delay the cleanup to allow stabilize after mutations:
310-
setTimeout(() => {
311-
detachedNodes.forEach((node) => {
312-
if (!document.contains(node)) {
313-
// Recursively clean up the entire subtree
314-
cleanupNodeAndChildren(node)
315-
detachedNodes.delete(node)
316-
removedCount++
317-
}
318-
})
319-
}, 100)
320-
321-
return removedCount
322-
}
323-
324-
function cleanupNodeAndChildren(node: any): void {
325-
// Recursively clean up all children:
326-
if (node.children && node.children.length > 0) {
327-
Array.from(node.children).forEach((child: any) => {
328-
cleanupNodeAndChildren(child)
329-
})
330-
}
331-
332-
// Also handle text nodes and other node types
333-
if (node.childNodes && node.childNodes.length > 0) {
334-
Array.from(node.childNodes).forEach((child: any) => {
335-
if (child.nodeType === Node.ELEMENT_NODE) {
336-
cleanupNodeAndChildren(child)
337-
}
338-
// Clear text nodes and other node types
339-
clearNodeReferences(child)
340-
})
341-
}
342-
343-
// Clear the current node
344-
clearNodeReferences(node)
345-
}
346-
347-
function clearNodeReferences(node: any): void {
348-
if (!node) return
349-
350-
try {
351-
if (node._reactRootContainer) {
352-
// This is a React root, skip it as it's still active
353-
return
354-
}
355-
356-
// First, recursively destroy all children
357-
if (node.childNodes) {
358-
Array.from(node.childNodes).forEach((child: any) => {
359-
clearNodeReferences(child)
360-
})
361-
}
362-
363-
// Force remove from parent if still attached
364-
if (node.parentNode && !document.contains(node)) {
365-
node.parentNode.removeChild(node)
366-
}
367-
368-
// Enhanced canvas cleanup - clear any stored references
369-
if (node.tagName === 'CANVAS') {
370-
const canvas = node as HTMLCanvasElement
371-
// Clear any custom properties that might hold references
372-
Object.keys(canvas).forEach((key) => {
373-
if (!key.startsWith('__react') && typeof (canvas as any)[key] === 'object') {
374-
//@ts-expect-error node[key] = null
375-
canvas[key] = null
376-
}
377-
})
378-
}
379-
380-
// More aggressive React Context Menu cleanup
381-
if (node.classList?.contains('react-contextmenu-wrapper')) {
382-
// Remove from any global registry
383-
const menuId =
384-
node.getAttribute('data-menu-id') || node.querySelector('[data-menu-id]')?.getAttribute('data-menu-id')
385-
if (menuId && (window as any).__reactContextMenus) {
386-
delete (window as any).__reactContextMenus[menuId]
387-
}
388-
// React context menu components often have special cleanup needs
389-
try {
390-
if (node._reactInternalFiber) {
391-
node._reactInternalFiber = null
392-
}
393-
if (node.__reactEventHandlers) {
394-
node.__reactEventHandlers = null
395-
}
396-
} catch (e) {
397-
// Ignore cleanup errors
398-
console.warn('Failed to clean up React context menu:', e)
399-
}
400-
}
401-
402-
// Clear any transform or style-based animations that might hold references
403-
if (node.style) {
404-
node.style.transform = ''
405-
node.style.transition = ''
406-
node.style.animation = ''
407-
}
408-
409-
// Break all React references
410-
const reactKeys = Object.keys(node).filter(
411-
(key) =>
412-
key.startsWith('__react') ||
413-
key.startsWith('_react') ||
414-
key.includes('react') ||
415-
key.includes('React') ||
416-
key.startsWith('__fiber') ||
417-
key === '_owner' ||
418-
key === '_store'
419-
)
420-
421-
reactKeys.forEach((key) => {
422-
try {
423-
const value = node[key]
424-
// Clear any circular references in React objects
425-
if (value && typeof value === 'object') {
426-
if (value.stateNode === node) {
427-
value.stateNode = null
428-
}
429-
if (value.child) value.child = null
430-
if (value.sibling) value.sibling = null
431-
if (value.return) value.return = null
432-
}
433-
node[key] = null
434-
delete node[key]
435-
} catch (e) {
436-
// Some properties might be non-configurable
437-
console.warn(`Failed to clear React property ${key} on node:`, e)
438-
}
439-
})
440-
441-
// Remove all event listeners
442-
if (node.removeEventListener) {
443-
// Common React event types
444-
// Add missing ones that we use but not on the list:
445-
446-
const eventTypes = [
447-
'click',
448-
'mousedown',
449-
'mouseup',
450-
'mouseover',
451-
'mouseout',
452-
'focus',
453-
'blur',
454-
'change',
455-
'input',
456-
'keydown',
457-
'keyup',
458-
'touchstart',
459-
'touchend',
460-
'touchmove',
461-
]
462-
463-
eventTypes.forEach((eventType) => {
464-
try {
465-
// Remove all listeners:
466-
const listeners = node.getEventListeners?.(eventType) || []
467-
listeners.forEach((listener: any) => {
468-
node.removeEventListener(eventType, listener.listener)
469-
})
470-
} catch (e) {
471-
// getEventListeners might not be available
472-
console.warn(`Failed to remove event listeners for ${eventType}:`, e)
473-
}
474-
})
475-
}
476-
477-
// Add this section for ARIA cleanup
478-
const ariaReferenceAttrs = [
479-
'aria-labelledby',
480-
'aria-describedby',
481-
'aria-controls',
482-
'aria-owns',
483-
'aria-flowto',
484-
'aria-activedescendant',
485-
]
486-
487-
ariaReferenceAttrs.forEach((attr) => {
488-
if (node.hasAttribute?.(attr)) {
489-
const referencedIds = node.getAttribute(attr)?.split(' ') || []
490-
referencedIds.forEach((id: any) => {
491-
// Clear any cached references to these elements
492-
const referencedEl = document.getElementById(id)
493-
if (referencedEl && referencedEl !== node) {
494-
// Clear back-references if any
495-
Object.keys(referencedEl as any).forEach((key) => {
496-
if ((referencedEl as any)[key] === node) {
497-
delete (referencedEl as any)[key]
498-
}
499-
})
500-
}
501-
})
502-
node.removeAttribute(attr)
503-
}
504-
})
505-
506-
// Enhanced canvas cleanup
507-
if (node.tagName === 'CANVAS') {
508-
const canvas = node as HTMLCanvasElement
509-
// Get all possible contexts
510-
;['2d', 'webgl', 'webgl2', 'bitmaprenderer'].forEach((contextType) => {
511-
try {
512-
const ctx = canvas.getContext(contextType as any)
513-
if (ctx) {
514-
if (contextType === '2d' && ctx) {
515-
;(ctx as CanvasRenderingContext2D).clearRect(0, 0, canvas.width, canvas.height)
516-
// Reset transform and clear path
517-
;(ctx as CanvasRenderingContext2D).setTransform(1, 0, 0, 1, 0, 0)
518-
;(ctx as CanvasRenderingContext2D).beginPath()
519-
}
520-
// Clear the context reference
521-
if ('loseContext' in ctx) {
522-
;(ctx as any).loseContext()
523-
}
524-
}
525-
} catch (e) {
526-
// Some contexts might not be available or supported
527-
console.warn(`Failed to clear canvas context ${contextType}:`, e)
528-
}
529-
})
530-
// Clear size to free memory
531-
canvas.width = 0
532-
canvas.height = 0
533-
}
534-
535-
// Clear React Context Menu global listeners
536-
if (node.classList?.contains('react-contextmenu-wrapper')) {
537-
// React context menu often uses global document listeners
538-
if ((window as any).ReactContextMenu) {
539-
try {
540-
;(window as any).ReactContextMenu.hideAll()
541-
} catch (e) {
542-
// If ReactContextMenu is not available, ignore
543-
console.warn('Failed to hide React Context Menu:', e)
544-
}
545-
}
546-
547-
// Clear any menu state
548-
node.classList.remove('react-contextmenu-wrapper--visible')
549-
node.classList.remove('react-contextmenu-wrapper--active')
550-
}
551-
552-
// Clear focus/blur related registrations
553-
if (node.tabIndex !== undefined && node.tabIndex >= 0) {
554-
node.tabIndex = -1
555-
node.blur?.()
556-
}
557-
558-
// Clear custom event listeners that might be using delegation
559-
const customDataAttrs = [
560-
'data-obj-id',
561-
'data-part-id',
562-
'data-layer-id',
563-
'data-source-id',
564-
'data-output-id',
565-
'data-identifier',
566-
]
567-
customDataAttrs.forEach((attr) => {
568-
if (node.hasAttribute?.(attr)) {
569-
const value = node.getAttribute(attr)
570-
// These might be used as keys in event delegation maps
571-
node.removeAttribute(attr)
572-
573-
// Check for global event delegation systems
574-
if ((window as any).__eventDelegation) {
575-
delete (window as any).__eventDelegation[value]
576-
}
577-
}
578-
})
579-
580-
// Clear intersection/resize/mutation observers
581-
if ((window as any).IntersectionObserver) {
582-
// Some libraries attach observer instances to elements
583-
const observerKeys = Object.keys(node).filter((key) => key.includes('observer') || key.includes('Observer'))
584-
observerKeys.forEach((key) => {
585-
try {
586-
if (node[key] && typeof node[key].disconnect === 'function') {
587-
node[key].disconnect()
588-
}
589-
delete node[key]
590-
} catch (e) {
591-
// Ignore errors if the property is not a valid observer
592-
console.warn(`Failed to disconnect observer ${key}:`, e)
593-
}
594-
})
595-
}
596-
597-
// Clear any animation frame callbacks
598-
if (node.__rafId) {
599-
cancelAnimationFrame(node.__rafId)
600-
delete node.__rafId
601-
}
602-
603-
// Clear any pending timers stored on the element
604-
;['__timeoutId', '__intervalId', '_timer', '_timeout'].forEach((prop) => {
605-
if (node[prop]) {
606-
clearTimeout(node[prop])
607-
clearInterval(node[prop])
608-
delete node[prop]
609-
}
610-
})
611-
612-
// Clear any jQuery data if present
613-
if (typeof window !== 'undefined' && (window as any).jQuery) {
614-
try {
615-
;(window as any).jQuery(node).off()
616-
;(window as any).jQuery.removeData(node)
617-
} catch (e) {
618-
// If jQuery is not available or node is not a jQuery object, ignore
619-
console.warn('Failed to clear jQuery data:', e)
620-
}
621-
}
622-
623-
// Don't clear innerHTML for React nodes - it can break cleanup
624-
// Instead, remove child nodes properly
625-
while (node.firstChild) {
626-
node.removeChild(node.firstChild)
627-
}
628-
} catch (error) {
629-
// Continue cleanup even if individual operations fail
630-
console.warn('Error during node cleanup:', error)
631-
}
632-
}
633-
634-
setInterval(
635-
() => {
636-
cleanupDetachedNodesFromMemory()
637-
},
638-
1 * 60 * 1000
639-
)
640-
}, [])
641-
642287
return (
643288
<div className="container-fluid header-clear">
644289
<RundownViewContent

0 commit comments

Comments
 (0)