snapshot-context.tsx 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. import {
  2. createContext,
  3. FC,
  4. useContext,
  5. useEffect,
  6. useMemo,
  7. useState,
  8. } from 'react'
  9. import { Snapshot } from 'overleaf-editor-core'
  10. import { useProjectContext } from '@/shared/context/project-context'
  11. import { debugConsole } from '@/utils/debugging'
  12. import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
  13. import { Folder } from '../../../../../types/folder'
  14. export const StubSnapshotUtils = {
  15. SnapshotUpdater: class SnapshotUpdater {
  16. // eslint-disable-next-line no-useless-constructor
  17. constructor(readonly projectId: string) {}
  18. refresh(): Promise<{ snapshot: Snapshot; snapshotVersion: number }> {
  19. throw new Error('not implemented')
  20. }
  21. abort(): void {
  22. throw new Error('not implemented')
  23. }
  24. },
  25. // unused vars kept to document the interface
  26. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  27. buildFileTree(snapshot: Snapshot): Folder {
  28. throw new Error('not implemented')
  29. },
  30. // unused vars kept to document the interface
  31. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  32. createFolder(id: string, name: string): Folder {
  33. throw new Error('not implemented')
  34. },
  35. }
  36. const { SnapshotUpdater } =
  37. (importOverleafModules('snapshotUtils')[0]
  38. ?.import as typeof StubSnapshotUtils) || StubSnapshotUtils
  39. export type SnapshotLoadingState = '' | 'loading' | 'error'
  40. export const SnapshotContext = createContext<
  41. | {
  42. snapshotVersion: number
  43. snapshot?: Snapshot
  44. snapshotLoadingState: SnapshotLoadingState
  45. fileTreeFromHistory: boolean
  46. setFileTreeFromHistory: (v: boolean) => void
  47. }
  48. | undefined
  49. >(undefined)
  50. export const SnapshotProvider: FC<React.PropsWithChildren> = ({ children }) => {
  51. const { projectId } = useProjectContext()
  52. const [snapshotLoadingState, setSnapshotLoadingState] =
  53. useState<SnapshotLoadingState>('')
  54. const [snapshotUpdater] = useState(() => new SnapshotUpdater(projectId))
  55. const [snapshot, setSnapshot] = useState<Snapshot>()
  56. const [snapshotVersion, setSnapshotVersion] = useState(-1)
  57. const [fileTreeFromHistory, setFileTreeFromHistory] = useState(false)
  58. useEffect(() => {
  59. if (!fileTreeFromHistory) return
  60. let stop = false
  61. let handle: number
  62. const refresh = () => {
  63. setSnapshotLoadingState('loading')
  64. snapshotUpdater
  65. .refresh()
  66. .then(({ snapshot, snapshotVersion }) => {
  67. setSnapshot(snapshot)
  68. setSnapshotVersion(snapshotVersion)
  69. setSnapshotLoadingState('')
  70. })
  71. .catch(err => {
  72. debugConsole.error(err)
  73. setSnapshotLoadingState('error')
  74. })
  75. .finally(() => {
  76. if (stop) return
  77. // use a chain of timeouts to avoid concurrent updates
  78. handle = window.setTimeout(refresh, 30_000)
  79. })
  80. }
  81. refresh()
  82. return () => {
  83. stop = true
  84. snapshotUpdater.abort()
  85. clearInterval(handle)
  86. }
  87. }, [projectId, fileTreeFromHistory, snapshotUpdater])
  88. const value = useMemo(
  89. () => ({
  90. snapshot,
  91. snapshotVersion,
  92. snapshotLoadingState,
  93. fileTreeFromHistory,
  94. setFileTreeFromHistory,
  95. }),
  96. [
  97. snapshot,
  98. snapshotVersion,
  99. snapshotLoadingState,
  100. fileTreeFromHistory,
  101. setFileTreeFromHistory,
  102. ]
  103. )
  104. return (
  105. <SnapshotContext.Provider value={value}>
  106. {children}
  107. </SnapshotContext.Provider>
  108. )
  109. }
  110. export function useSnapshotContext() {
  111. const context = useContext(SnapshotContext)
  112. if (!context) {
  113. throw new Error(
  114. 'useSnapshotContext is only available within SnapshotProvider'
  115. )
  116. }
  117. return context
  118. }