references-context.tsx 5.5 KB

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