references-context.tsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. import { generateSHA1Hash } from '../../../shared/utils/sha1'
  2. import {
  3. createContext,
  4. useContext,
  5. useEffect,
  6. FC,
  7. useCallback,
  8. useMemo,
  9. useState,
  10. useRef,
  11. } from 'react'
  12. import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
  13. import { useConnectionContext } from '@/features/ide-react/context/connection-context'
  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 useEventListener from '@/shared/hooks/use-event-listener'
  19. import { useProjectContext } from '@/shared/context/project-context'
  20. import { useEditorManagerContext } from './editor-manager-context'
  21. import { signalWithTimeout } from '@/utils/abort-signal'
  22. import { postJSON } from '@/infrastructure/fetch-json'
  23. import { debugConsole } from '@/utils/debugging'
  24. import { useFeatureFlag } from '@/shared/context/split-test-context'
  25. import type { ReferenceIndexer } from '../references/reference-indexer'
  26. import { AdvancedReferenceSearchResult } from '@/features/ide-react/references/types'
  27. export const ReferencesContext = createContext<
  28. | {
  29. referenceKeys: Set<string>
  30. indexAllReferences: (shouldBroadcast: boolean) => Promise<void>
  31. searchLocalReferences: (
  32. query: string
  33. ) => Promise<AdvancedReferenceSearchResult>
  34. }
  35. | undefined
  36. >(undefined)
  37. export const ReferencesProvider: FC<React.PropsWithChildren> = ({
  38. children,
  39. }) => {
  40. const { fileTreeData } = useFileTreeData()
  41. const { eventEmitter, projectId } = useIdeReactContext()
  42. const { socket } = useConnectionContext()
  43. const { projectSnapshot } = useProjectContext()
  44. const { openDocs } = useEditorManagerContext()
  45. const abortControllerRef = useRef<AbortController | null>(null)
  46. const [referenceKeys, setReferenceKeys] = useState(new Set<string>())
  47. const clientSideReferences = useFeatureFlag('client-side-references')
  48. const [existingIndexHash, setExistingIndexHash] = useState<
  49. Record<string, { hash: string; timestamp: number }>
  50. >({})
  51. const indexAllReferencesServerside = useCallback(
  52. async (shouldBroadcast: boolean) => {
  53. return postJSON(`/project/${projectId}/references/indexAll`, {
  54. body: {
  55. shouldBroadcast,
  56. },
  57. })
  58. .then((response: { keys: string[] }) => {
  59. setReferenceKeys(new Set(response.keys))
  60. })
  61. .catch(error => {
  62. // allow the request to fail
  63. debugConsole.error(error)
  64. })
  65. },
  66. [projectId]
  67. )
  68. const indexerRef = useRef<Promise<ReferenceIndexer> | null>(null)
  69. if (clientSideReferences && indexerRef.current === null) {
  70. indexerRef.current = import('../references/reference-indexer').then(
  71. m => new m.ReferenceIndexer()
  72. )
  73. }
  74. const indexAllReferencesLocally = useCallback(
  75. async (shouldBroadcast: boolean) => {
  76. abortControllerRef.current?.abort()
  77. if (!indexerRef.current) {
  78. return
  79. }
  80. abortControllerRef.current = new AbortController()
  81. const signal = abortControllerRef.current.signal
  82. await openDocs.awaitBufferedOps(signalWithTimeout(signal, 5000))
  83. await projectSnapshot.refresh()
  84. if (signal.aborted) {
  85. return
  86. }
  87. const indexer = await indexerRef.current
  88. const keys = await indexer.updateFromSnapshot(projectSnapshot, { signal })
  89. if (signal.aborted) {
  90. return
  91. }
  92. setReferenceKeys(keys)
  93. if (shouldBroadcast) {
  94. // Inform other clients about change in keys
  95. await postJSON(`/project/${projectId}/references/indexAll`, {
  96. body: { shouldBroadcast: true },
  97. }).catch(error => {
  98. // allow the request to fail
  99. debugConsole.error(error)
  100. })
  101. }
  102. },
  103. [projectSnapshot, openDocs, projectId]
  104. )
  105. const indexAllReferences = clientSideReferences
  106. ? indexAllReferencesLocally
  107. : indexAllReferencesServerside
  108. const indexReferencesIfDocModified = useCallback(
  109. (doc: ShareJsDoc, shouldBroadcast: boolean) => {
  110. // avoid reindexing references if the bib file has not changed since the
  111. // last time they were indexed
  112. const docId = doc.doc_id
  113. const snapshot = doc.getSnapshot()
  114. const now = Date.now()
  115. const sha1 = generateSHA1Hash(
  116. 'blob ' + snapshot.length + '\x00' + snapshot
  117. )
  118. const CACHE_LIFETIME = 6 * 3600 * 1000 // allow reindexing every 6 hours
  119. const cacheEntry = existingIndexHash[docId]
  120. const isCached =
  121. cacheEntry &&
  122. cacheEntry.timestamp > now - CACHE_LIFETIME &&
  123. cacheEntry.hash === sha1
  124. if (!isCached) {
  125. indexAllReferences(shouldBroadcast)
  126. setExistingIndexHash(existingIndexHash => ({
  127. ...existingIndexHash,
  128. [docId]: { hash: sha1, timestamp: now },
  129. }))
  130. }
  131. },
  132. [existingIndexHash, indexAllReferences]
  133. )
  134. useEffect(() => {
  135. const handleDocClosed = ({
  136. detail: [doc],
  137. }: CustomEvent<IdeEvents['document:closed']>) => {
  138. if (
  139. doc.doc_id &&
  140. findDocEntityById(fileTreeData, doc.doc_id)?.name?.endsWith('.bib')
  141. ) {
  142. indexReferencesIfDocModified(doc, true)
  143. }
  144. }
  145. eventEmitter.on('document:closed', handleDocClosed)
  146. return () => {
  147. eventEmitter.off('document:closed', handleDocClosed)
  148. }
  149. }, [eventEmitter, fileTreeData, indexReferencesIfDocModified])
  150. useEventListener(
  151. 'reference:added',
  152. useCallback(() => {
  153. indexAllReferences(true)
  154. }, [indexAllReferences])
  155. )
  156. useEffect(() => {
  157. const handleProjectJoined = () => {
  158. // We only need to grab the references when the editor first loads,
  159. // not on every reconnect
  160. socket.on('references:keys:updated', (keys, allDocs) => {
  161. if (clientSideReferences) {
  162. indexAllReferences(false)
  163. } else {
  164. setReferenceKeys(oldDocs =>
  165. allDocs ? new Set(keys) : new Set([...oldDocs, ...keys])
  166. )
  167. }
  168. })
  169. indexAllReferences(false)
  170. }
  171. eventEmitter.once('project:joined', handleProjectJoined)
  172. return () => {
  173. eventEmitter.off('project:joined', handleProjectJoined)
  174. }
  175. }, [eventEmitter, indexAllReferences, socket, clientSideReferences])
  176. const searchLocalReferences = useCallback(
  177. async (query: string): Promise<AdvancedReferenceSearchResult> => {
  178. if (!indexerRef.current) {
  179. return { hits: [] }
  180. }
  181. const indexer = await indexerRef.current
  182. return await indexer.search(query)
  183. },
  184. []
  185. )
  186. const value = useMemo(
  187. () => ({
  188. referenceKeys,
  189. indexAllReferences,
  190. searchLocalReferences,
  191. }),
  192. [indexAllReferences, referenceKeys, searchLocalReferences]
  193. )
  194. return (
  195. <ReferencesContext.Provider value={value}>
  196. {children}
  197. </ReferencesContext.Provider>
  198. )
  199. }
  200. export function useReferencesContext() {
  201. const context = useContext(ReferencesContext)
  202. if (!context) {
  203. throw new Error(
  204. 'useReferencesContext is only available inside ReferencesProvider'
  205. )
  206. }
  207. return context
  208. }