references-context.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  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 _ from 'lodash'
  14. import { postJSON } from '@/infrastructure/fetch-json'
  15. import { ShareJsDoc } from '@/features/ide-react/editor/share-js-doc'
  16. import useScopeValue from '@/shared/hooks/use-scope-value'
  17. import { ReactScopeValueStore } from '@/features/ide-react/scope-value-store/react-scope-value-store'
  18. import { useFileTreeData } from '@/shared/context/file-tree-data-context'
  19. import { findDocEntityById } from '@/features/ide-react/util/find-doc-entity-by-id'
  20. import { IdeEvents } from '@/features/ide-react/create-ide-event-emitter'
  21. import { debugConsole } from '@/utils/debugging'
  22. type References = {
  23. keys: string[]
  24. }
  25. type ReferencesContextValue = {
  26. indexReferencesIfDocModified: (
  27. doc: ShareJsDoc,
  28. shouldBroadcast: boolean
  29. ) => void
  30. indexAllReferences: (shouldBroadcast: boolean) => void
  31. }
  32. type IndexReferencesResponse = References
  33. const ReferencesContext = createContext<ReferencesContextValue | undefined>(
  34. undefined
  35. )
  36. export function populateReferenceScope(store: ReactScopeValueStore) {
  37. store.set('$root._references', { keys: [] })
  38. }
  39. export const ReferencesProvider: FC = ({ children }) => {
  40. const { fileTreeData } = useFileTreeData()
  41. const { eventEmitter, projectId } = useIdeReactContext()
  42. const { socket } = useConnectionContext()
  43. const [references, setReferences] =
  44. useScopeValue<References>('$root._references')
  45. const [existingIndexHash, setExistingIndexHash] = useState<
  46. Record<string, { hash: string; timestamp: number }>
  47. >({})
  48. const storeReferencesKeys = useCallback(
  49. (newKeys: string[], replaceExistingKeys: boolean) => {
  50. const oldKeys = references.keys
  51. const keys = replaceExistingKeys ? newKeys : _.union(oldKeys, newKeys)
  52. window.dispatchEvent(
  53. new CustomEvent('project:references', {
  54. detail: keys,
  55. })
  56. )
  57. setReferences({ keys })
  58. },
  59. [references.keys, setReferences]
  60. )
  61. const indexAllReferences = useCallback(
  62. (shouldBroadcast: boolean) => {
  63. postJSON(`/project/${projectId}/references/indexAll`, {
  64. body: {
  65. shouldBroadcast,
  66. },
  67. })
  68. .then((response: IndexReferencesResponse) => {
  69. storeReferencesKeys(response.keys, true)
  70. })
  71. .catch(error => {
  72. // allow the request to fail
  73. debugConsole.error(error)
  74. })
  75. },
  76. [projectId, storeReferencesKeys]
  77. )
  78. const indexReferencesIfDocModified = useCallback(
  79. (doc: ShareJsDoc, shouldBroadcast: boolean) => {
  80. // avoid reindexing references if the bib file has not changed since the
  81. // last time they were indexed
  82. const docId = doc.doc_id
  83. const snapshot = doc._doc.snapshot
  84. const now = Date.now()
  85. const sha1 = generateSHA1Hash(
  86. 'blob ' + snapshot.length + '\x00' + snapshot
  87. )
  88. const CACHE_LIFETIME = 6 * 3600 * 1000 // allow reindexing every 6 hours
  89. const cacheEntry = existingIndexHash[docId]
  90. const isCached =
  91. cacheEntry &&
  92. cacheEntry.timestamp > now - CACHE_LIFETIME &&
  93. cacheEntry.hash === sha1
  94. if (!isCached) {
  95. indexAllReferences(shouldBroadcast)
  96. setExistingIndexHash(existingIndexHash => ({
  97. ...existingIndexHash,
  98. [docId]: { hash: sha1, timestamp: now },
  99. }))
  100. }
  101. },
  102. [existingIndexHash, indexAllReferences]
  103. )
  104. useEffect(() => {
  105. const handleDocClosed = ({
  106. detail: [doc],
  107. }: CustomEvent<IdeEvents['document:closed']>) => {
  108. if (
  109. doc.doc_id &&
  110. findDocEntityById(fileTreeData, doc.doc_id)?.name?.endsWith('.bib')
  111. ) {
  112. indexReferencesIfDocModified(doc, true)
  113. }
  114. }
  115. eventEmitter.on('document:closed', handleDocClosed)
  116. return () => {
  117. eventEmitter.off('document:closed', handleDocClosed)
  118. }
  119. }, [eventEmitter, fileTreeData, indexReferencesIfDocModified])
  120. useEffect(() => {
  121. const handleShouldReindex = () => {
  122. indexAllReferences(true)
  123. }
  124. eventEmitter.on('references:should-reindex', handleShouldReindex)
  125. return () => {
  126. eventEmitter.off('references:should-reindex', handleShouldReindex)
  127. }
  128. }, [eventEmitter, indexAllReferences])
  129. useEffect(() => {
  130. const handleProjectJoined = () => {
  131. // We only need to grab the references when the editor first loads,
  132. // not on every reconnect
  133. socket.on('references:keys:updated', (keys, allDocs) =>
  134. storeReferencesKeys(keys, allDocs)
  135. )
  136. indexAllReferences(false)
  137. }
  138. eventEmitter.once('project:joined', handleProjectJoined)
  139. return () => {
  140. eventEmitter.off('project:joined', handleProjectJoined)
  141. }
  142. }, [eventEmitter, indexAllReferences, socket, storeReferencesKeys])
  143. const value = useMemo<ReferencesContextValue>(
  144. () => ({
  145. indexReferencesIfDocModified,
  146. indexAllReferences,
  147. }),
  148. [indexReferencesIfDocModified, indexAllReferences]
  149. )
  150. return (
  151. <ReferencesContext.Provider value={value}>
  152. {children}
  153. </ReferencesContext.Provider>
  154. )
  155. }
  156. export function useReferencesContext(): ReferencesContextValue {
  157. const context = useContext(ReferencesContext)
  158. if (!context) {
  159. throw new Error(
  160. 'useReferencesContext is only available inside ReferencesProvider'
  161. )
  162. }
  163. return context
  164. }