use-editing-session-heartbeat.ts 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
  2. import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
  3. import { EditorType } from '@/features/ide-react/editor/types/editor-type'
  4. import { putJSON } from '@/infrastructure/fetch-json'
  5. import { debugConsole } from '@/utils/debugging'
  6. import { useCallback, useEffect, useRef } from 'react'
  7. import useEventListener from '@/shared/hooks/use-event-listener'
  8. import useDomEventListener from '@/shared/hooks/use-dom-event-listener'
  9. import {
  10. IdeLayout,
  11. IdeView,
  12. useLayoutContext,
  13. } from '@/shared/context/layout-context'
  14. import {
  15. RailTabKey,
  16. useRailContext,
  17. } from '@/features/ide-react/context/rail-context'
  18. function createEditingSessionHeartbeatData(
  19. editorType: EditorType,
  20. view: IdeView | null,
  21. layout: IdeLayout,
  22. railOpen: boolean,
  23. railTab: RailTabKey,
  24. hasDetachedPdf: boolean
  25. ) {
  26. const newEditorSegmentation = { railOpen, railTab }
  27. return {
  28. editorType,
  29. editorView: view,
  30. editorLayout: layout,
  31. hasDetachedPdf,
  32. ...newEditorSegmentation,
  33. }
  34. }
  35. function sendEditingSessionHeartbeat(
  36. projectId: string,
  37. segmentation: Record<string, unknown>
  38. ) {
  39. putJSON(`/editingSession/${projectId}`, {
  40. body: { segmentation },
  41. }).catch(debugConsole.error)
  42. }
  43. export function useEditingSessionHeartbeat() {
  44. const { projectId } = useIdeReactContext()
  45. const { getEditorType } = useEditorManagerContext()
  46. const { view, pdfLayout: layout, detachIsLinked } = useLayoutContext()
  47. const { isOpen: railIsOpen, selectedTab: selectedRailTab } = useRailContext()
  48. // Keep track of how many heartbeats we've sent so that we can calculate how
  49. // long to wait until the next one
  50. const heartBeatsSentRef = useRef(0)
  51. const heartBeatSentRecentlyRef = useRef(false)
  52. const heartBeatResetTimerRef = useRef<number>()
  53. useEffect(() => {
  54. return () => {
  55. window.clearTimeout(heartBeatResetTimerRef.current)
  56. }
  57. }, [])
  58. const editingSessionHeartbeat = useCallback(() => {
  59. debugConsole.log('[Event] heartbeat trigger')
  60. const editorType = getEditorType()
  61. if (editorType === null) return
  62. // Heartbeat already sent recently
  63. if (heartBeatSentRecentlyRef.current) return
  64. heartBeatSentRecentlyRef.current = true
  65. const segmentation = createEditingSessionHeartbeatData(
  66. editorType,
  67. view,
  68. layout,
  69. railIsOpen,
  70. selectedRailTab,
  71. detachIsLinked
  72. )
  73. debugConsole.log('[Event] send heartbeat request', segmentation)
  74. sendEditingSessionHeartbeat(projectId, segmentation)
  75. const heartbeatsSent = heartBeatsSentRef.current
  76. heartBeatsSentRef.current++
  77. // Send two first heartbeats at 0 and 30s then increase the backoff time
  78. // 1min per call until we reach 5 min
  79. const backoffSecs =
  80. heartbeatsSent <= 2
  81. ? 30
  82. : heartbeatsSent <= 6
  83. ? (heartbeatsSent - 2) * 60
  84. : 300
  85. heartBeatResetTimerRef.current = window.setTimeout(() => {
  86. heartBeatSentRecentlyRef.current = false
  87. }, backoffSecs * 1000)
  88. }, [
  89. getEditorType,
  90. projectId,
  91. view,
  92. layout,
  93. railIsOpen,
  94. selectedRailTab,
  95. detachIsLinked,
  96. ])
  97. // Hook the heartbeat up to editor events
  98. useEventListener('cursor:editor:update', editingSessionHeartbeat)
  99. useEventListener('scroll:editor:update', editingSessionHeartbeat)
  100. useDomEventListener(document, 'click', editingSessionHeartbeat)
  101. }