references-context.tsx 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. import { generateSHA1Hash } from '../../../shared/utils/sha1'
  2. import {
  3. createContext,
  4. useContext,
  5. useEffect,
  6. FC,
  7. useCallback,
  8. useMemo,
  9. useState,
  10. } from 'react'
  11. import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
  12. import { useConnectionContext } from '@/features/ide-react/context/connection-context'
  13. import { postJSON } from '@/infrastructure/fetch-json'
  14. import { ShareJsDoc } from '@/features/ide-react/editor/share-js-doc'
  15. import { useFileTreeData } from '@/shared/context/file-tree-data-context'
  16. import { findDocEntityById } from '@/features/ide-react/util/find-doc-entity-by-id'
  17. import { IdeEvents } from '@/features/ide-react/create-ide-event-emitter'
  18. import { debugConsole } from '@/utils/debugging'
  19. import useEventListener from '@/shared/hooks/use-event-listener'
  20. export const ReferencesContext = createContext<
  21. | {
  22. referenceKeys: Set<string>
  23. indexAllReferences: (shouldBroadcast: boolean) => Promise<void>
  24. }
  25. | undefined
  26. >(undefined)
  27. export const ReferencesProvider: FC<React.PropsWithChildren> = ({
  28. children,
  29. }) => {
  30. const { fileTreeData } = useFileTreeData()
  31. const { eventEmitter, projectId } = useIdeReactContext()
  32. const { socket } = useConnectionContext()
  33. const [referenceKeys, setReferenceKeys] = useState(new Set<string>())
  34. const [existingIndexHash, setExistingIndexHash] = useState<
  35. Record<string, { hash: string; timestamp: number }>
  36. >({})
  37. const indexAllReferences = useCallback(
  38. async (shouldBroadcast: boolean) => {
  39. return postJSON(`/project/${projectId}/references/indexAll`, {
  40. body: {
  41. shouldBroadcast,
  42. },
  43. })
  44. .then((response: { keys: string[] }) => {
  45. setReferenceKeys(new Set(response.keys))
  46. })
  47. .catch(error => {
  48. // allow the request to fail
  49. debugConsole.error(error)
  50. })
  51. },
  52. [projectId]
  53. )
  54. const indexReferencesIfDocModified = useCallback(
  55. (doc: ShareJsDoc, shouldBroadcast: boolean) => {
  56. // avoid reindexing references if the bib file has not changed since the
  57. // last time they were indexed
  58. const docId = doc.doc_id
  59. const snapshot = doc.getSnapshot()
  60. const now = Date.now()
  61. const sha1 = generateSHA1Hash(
  62. 'blob ' + snapshot.length + '\x00' + snapshot
  63. )
  64. const CACHE_LIFETIME = 6 * 3600 * 1000 // allow reindexing every 6 hours
  65. const cacheEntry = existingIndexHash[docId]
  66. const isCached =
  67. cacheEntry &&
  68. cacheEntry.timestamp > now - CACHE_LIFETIME &&
  69. cacheEntry.hash === sha1
  70. if (!isCached) {
  71. indexAllReferences(shouldBroadcast)
  72. setExistingIndexHash(existingIndexHash => ({
  73. ...existingIndexHash,
  74. [docId]: { hash: sha1, timestamp: now },
  75. }))
  76. }
  77. },
  78. [existingIndexHash, indexAllReferences]
  79. )
  80. useEffect(() => {
  81. const handleDocClosed = ({
  82. detail: [doc],
  83. }: CustomEvent<IdeEvents['document:closed']>) => {
  84. if (
  85. doc.doc_id &&
  86. findDocEntityById(fileTreeData, doc.doc_id)?.name?.endsWith('.bib')
  87. ) {
  88. indexReferencesIfDocModified(doc, true)
  89. }
  90. }
  91. eventEmitter.on('document:closed', handleDocClosed)
  92. return () => {
  93. eventEmitter.off('document:closed', handleDocClosed)
  94. }
  95. }, [eventEmitter, fileTreeData, indexReferencesIfDocModified])
  96. useEventListener(
  97. 'reference:added',
  98. useCallback(() => {
  99. indexAllReferences(true)
  100. }, [indexAllReferences])
  101. )
  102. useEffect(() => {
  103. const handleProjectJoined = () => {
  104. // We only need to grab the references when the editor first loads,
  105. // not on every reconnect
  106. socket.on('references:keys:updated', (keys, allDocs) => {
  107. setReferenceKeys(oldKeys =>
  108. allDocs ? new Set(keys) : new Set([...oldKeys, ...keys])
  109. )
  110. })
  111. indexAllReferences(false)
  112. }
  113. eventEmitter.once('project:joined', handleProjectJoined)
  114. return () => {
  115. eventEmitter.off('project:joined', handleProjectJoined)
  116. }
  117. }, [eventEmitter, indexAllReferences, socket])
  118. const value = useMemo(
  119. () => ({
  120. referenceKeys,
  121. indexAllReferences,
  122. }),
  123. [indexAllReferences, referenceKeys]
  124. )
  125. return (
  126. <ReferencesContext.Provider value={value}>
  127. {children}
  128. </ReferencesContext.Provider>
  129. )
  130. }
  131. export function useReferencesContext() {
  132. const context = useContext(ReferencesContext)
  133. if (!context) {
  134. throw new Error(
  135. 'useReferencesContext is only available inside ReferencesProvider'
  136. )
  137. }
  138. return context
  139. }