detach-context.tsx 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. import {
  2. createContext,
  3. useContext,
  4. useCallback,
  5. useMemo,
  6. useEffect,
  7. useState,
  8. FC,
  9. } from 'react'
  10. import getMeta from '../../utils/meta'
  11. import { buildUrlWithDetachRole } from '../utils/url-helper'
  12. import useCallbackHandlers from '../hooks/use-callback-handlers'
  13. import { debugConsole } from '@/utils/debugging'
  14. export type DetachRole = 'detacher' | 'detached' | null
  15. type Message = {
  16. role: DetachRole
  17. event: string
  18. data?: any
  19. }
  20. export const DetachContext = createContext<
  21. | {
  22. role: DetachRole
  23. setRole: (role: DetachRole) => void
  24. broadcastEvent: (event: string, data?: any) => void
  25. addEventHandler: (handler: (...args: any[]) => void) => void
  26. deleteEventHandler: (handler: (...args: any[]) => void) => void
  27. }
  28. | undefined
  29. >(undefined)
  30. const debugPdfDetach = getMeta('ol-debugPdfDetach')
  31. const projectId = getMeta('ol-project_id')
  32. export const detachChannelId = `detach-${projectId}`
  33. export const detachChannel =
  34. 'BroadcastChannel' in window
  35. ? new BroadcastChannel(detachChannelId)
  36. : undefined
  37. export const DetachProvider: FC<React.PropsWithChildren> = ({ children }) => {
  38. const [lastDetachedConnectedAt, setLastDetachedConnectedAt] = useState<Date>()
  39. const [role, setRole] = useState(() => getMeta('ol-detachRole') || null)
  40. const {
  41. addHandler: addEventHandler,
  42. deleteHandler: deleteEventHandler,
  43. callHandlers: callEventHandlers,
  44. } = useCallbackHandlers()
  45. useEffect(() => {
  46. if (debugPdfDetach) {
  47. debugConsole.warn('Effect', { role })
  48. }
  49. window.history.replaceState({}, '', buildUrlWithDetachRole(role).toString())
  50. }, [role])
  51. useEffect(() => {
  52. if (detachChannel) {
  53. const listener = (event: MessageEvent) => {
  54. if (debugPdfDetach) {
  55. debugConsole.warn(`Receiving:`, event.data)
  56. }
  57. callEventHandlers(event.data)
  58. }
  59. detachChannel.addEventListener('message', listener)
  60. return () => {
  61. detachChannel.removeEventListener('message', listener)
  62. }
  63. }
  64. }, [callEventHandlers])
  65. const broadcastEvent = useCallback(
  66. (event: string, data?: any) => {
  67. if (!role) {
  68. if (debugPdfDetach) {
  69. debugConsole.warn('Not Broadcasting (no role)', {
  70. role,
  71. event,
  72. data,
  73. })
  74. }
  75. return
  76. }
  77. if (debugPdfDetach) {
  78. debugConsole.warn('Broadcasting', {
  79. role,
  80. event,
  81. data,
  82. })
  83. }
  84. const message: Message = { role, event }
  85. if (data) {
  86. message.data = data
  87. }
  88. detachChannel?.postMessage(message)
  89. },
  90. [role]
  91. )
  92. useEffect(() => {
  93. broadcastEvent('connected')
  94. }, [broadcastEvent])
  95. useEffect(() => {
  96. const onBeforeUnload = () => broadcastEvent('closed')
  97. window.addEventListener('beforeunload', onBeforeUnload)
  98. return () => window.removeEventListener('beforeunload', onBeforeUnload)
  99. }, [broadcastEvent])
  100. useEffect(() => {
  101. const updateLastDetachedConnectedAt = (message: Message) => {
  102. if (message.role === 'detached' && message.event === 'connected') {
  103. setLastDetachedConnectedAt(new Date())
  104. }
  105. }
  106. addEventHandler(updateLastDetachedConnectedAt)
  107. return () => deleteEventHandler(updateLastDetachedConnectedAt)
  108. }, [addEventHandler, deleteEventHandler])
  109. const value = useMemo(
  110. () => ({
  111. role,
  112. setRole,
  113. broadcastEvent,
  114. lastDetachedConnectedAt,
  115. addEventHandler,
  116. deleteEventHandler,
  117. }),
  118. [
  119. role,
  120. setRole,
  121. broadcastEvent,
  122. lastDetachedConnectedAt,
  123. addEventHandler,
  124. deleteEventHandler,
  125. ]
  126. )
  127. return (
  128. <DetachContext.Provider value={value}>{children}</DetachContext.Provider>
  129. )
  130. }
  131. export function useDetachContext() {
  132. const data = useContext(DetachContext)
  133. if (!data) {
  134. throw new Error('useDetachContext is only available inside DetachProvider')
  135. }
  136. return data
  137. }