|
1 | 1 | import { Meteor } from 'meteor/meteor' |
2 | | -import React, { useContext, useEffect, useMemo } from 'react' |
| 2 | +import React, { useContext, useMemo } from 'react' |
3 | 3 | import { ParsedQuery, parse as queryStringParse } from 'query-string' |
4 | 4 | import { Translated, translateWithTracker, useTracker } from '../lib/ReactMeteorData/react-meteor-data.js' |
5 | 5 | import { VTContent, NoteSeverity, ISourceLayer } from '@sofie-automation/blueprints-integration' |
@@ -284,361 +284,6 @@ export function RundownView(props: Readonly<IProps>): JSX.Element { |
284 | 284 | partInstances?.currentPartInstance |
285 | 285 | ) |
286 | 286 |
|
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 | | - |
642 | 287 | return ( |
643 | 288 | <div className="container-fluid header-clear"> |
644 | 289 | <RundownViewContent |
|
0 commit comments