reference-indexer.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. import { ProjectSnapshot } from '@/infrastructure/project-snapshot'
  2. import { generateSHA1Hash } from '@/shared/utils/sha1'
  3. import { AdvancedReferenceSearchResult, Changes } from './types'
  4. import { debugConsole } from '@/utils/debugging'
  5. import type { ReferenceWorkerResponse } from './references.worker'
  6. const ONE_MB = 1024 * 1024
  7. const MAX_BIB_DATA_SIZE = 6 * ONE_MB
  8. export class ReferenceIndexer {
  9. private fileIndexHash: Map<string, string> = new Map()
  10. private worker: Worker
  11. private updateResolve: ((result: Set<string>) => void) | null = null
  12. private searchResolve:
  13. | ((result: AdvancedReferenceSearchResult) => void)
  14. | null = null
  15. constructor() {
  16. this.worker = new Worker(
  17. /* webpackChunkName: "references-worker" */
  18. new URL('./references.worker.ts', import.meta.url),
  19. { type: 'module' }
  20. )
  21. this.worker.addEventListener('message', evt => this.handleMessage(evt))
  22. }
  23. private handleMessage(event: MessageEvent) {
  24. const data = event.data as ReferenceWorkerResponse
  25. if (data.type === 'searchResult' && this.searchResolve) {
  26. this.searchResolve(data.result)
  27. this.searchResolve = null
  28. } else if (data.type === 'updateKeys' && this.updateResolve) {
  29. this.updateResolve(data.keys)
  30. this.updateResolve = null
  31. } else {
  32. debugConsole.warn('Received unknown message from worker:', data.type)
  33. }
  34. }
  35. async updateFromSnapshot(
  36. snapshot: Pick<
  37. ProjectSnapshot,
  38. | 'getDocPaths'
  39. | 'getDocContents'
  40. | 'getBinaryFilePathsWithHash'
  41. | 'getBinaryFileContents'
  42. >,
  43. {
  44. dataLimit = MAX_BIB_DATA_SIZE,
  45. signal,
  46. }: { dataLimit?: number; signal: AbortSignal }
  47. ): Promise<Set<string>> {
  48. const nextFileHashIndex = new Map(this.fileIndexHash)
  49. const previousPaths = new Set(this.fileIndexHash.keys())
  50. let dataBudget = dataLimit
  51. const docs = snapshot
  52. .getDocPaths()
  53. .filter(path => path.toLowerCase().endsWith('.bib'))
  54. const changes: Changes = { updates: [], deletes: [] }
  55. for (const path of docs) {
  56. previousPaths.delete(path)
  57. if (dataBudget <= 0) {
  58. continue
  59. }
  60. const content = snapshot.getDocContents(path)?.slice(0, dataBudget)
  61. if (content == null) {
  62. continue
  63. }
  64. dataBudget -= content.length
  65. const hash = generateSHA1Hash(content)
  66. const possibleMatch = nextFileHashIndex.get(path)
  67. if (possibleMatch === undefined || possibleMatch !== hash) {
  68. // New or changed file
  69. nextFileHashIndex.set(path, hash)
  70. changes.updates.push({ path, content })
  71. }
  72. }
  73. const files = snapshot
  74. .getBinaryFilePathsWithHash()
  75. .filter(({ path }) => path.toLowerCase().endsWith('.bib'))
  76. .sort((a, b) => a.size - b.size)
  77. for (const { path, hash, size } of files) {
  78. if (signal.aborted) {
  79. debugConsole.warn('Aborted indexing references due to signal')
  80. return new Set()
  81. }
  82. previousPaths.delete(path)
  83. if (nextFileHashIndex.get(path) === hash) {
  84. dataBudget -= size
  85. // Already indexed
  86. continue
  87. }
  88. if (dataBudget <= 0) {
  89. continue
  90. }
  91. const content = await snapshot.getBinaryFileContents(path, {
  92. maxSize: dataBudget,
  93. })
  94. dataBudget -= content.length
  95. nextFileHashIndex.set(path, hash)
  96. changes.updates.push({ path, content })
  97. }
  98. previousPaths.forEach(path => {
  99. // Deleted file
  100. changes.deletes.push(path)
  101. nextFileHashIndex.delete(path)
  102. })
  103. if (dataBudget <= 0) {
  104. debugConsole.warn('Data budget exceeded while updating references index')
  105. }
  106. this.fileIndexHash = nextFileHashIndex
  107. this.worker.postMessage({
  108. type: 'update',
  109. changes,
  110. })
  111. return new Promise(resolve => {
  112. this.updateResolve = resolve
  113. })
  114. }
  115. async search(query: string): Promise<AdvancedReferenceSearchResult> {
  116. this.worker.postMessage({ type: 'search', query })
  117. const { promise, resolve } =
  118. Promise.withResolvers<AdvancedReferenceSearchResult>()
  119. this.searchResolve = resolve
  120. return promise
  121. }
  122. }