reference-indexer.ts 4.2 KB

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