HunspellManager.ts 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. /* eslint-disable no-dupe-class-members */
  2. import { v4 as uuid } from 'uuid'
  3. import getMeta from '@/utils/meta'
  4. import { debugConsole } from '@/utils/debugging'
  5. import { captureException } from '@/infrastructure/error-reporter'
  6. type SpellMessage = {
  7. type: 'spell'
  8. words: string[]
  9. }
  10. type SuggestMessage = {
  11. type: 'suggest'
  12. word: string
  13. }
  14. type AddWordMessage = {
  15. type: 'add_word'
  16. word: string
  17. }
  18. type RemoveWordMessage = {
  19. type: 'remove_word'
  20. word: string
  21. }
  22. type DestroyMessage = {
  23. type: 'destroy'
  24. }
  25. type Message = { id?: string } & (
  26. | SpellMessage
  27. | SuggestMessage
  28. | AddWordMessage
  29. | RemoveWordMessage
  30. | DestroyMessage
  31. )
  32. type EmptyResult = Record<string, never>
  33. type ErrorResult = {
  34. error: true
  35. }
  36. type SpellResult = {
  37. misspellings: { index: number }[]
  38. }
  39. type SuggestResult = {
  40. suggestions: string[]
  41. }
  42. type ResultCallback =
  43. | ((value: SpellResult | ErrorResult) => void)
  44. | ((value: SuggestResult | ErrorResult) => void)
  45. | ((value: EmptyResult | ErrorResult) => void)
  46. export class HunspellManager {
  47. baseAssetPath: string
  48. dictionariesRoot: string
  49. hunspellWorker!: Worker
  50. abortController: AbortController | undefined
  51. listening = false
  52. loaded = false
  53. loadingFailed = false
  54. pendingMessages: Message[] = []
  55. callbacks: Map<string, ResultCallback> = new Map()
  56. constructor(
  57. private readonly language: string,
  58. private readonly learnedWords: string[]
  59. ) {
  60. this.baseAssetPath = new URL(
  61. getMeta('ol-baseAssetPath'),
  62. window.location.href
  63. ).toString()
  64. this.dictionariesRoot = getMeta('ol-dictionariesRoot')
  65. this.hunspellWorker = new Worker(
  66. /* webpackChunkName: "hunspell-worker" */
  67. new URL('./hunspell.worker.ts', import.meta.url),
  68. { type: 'module' }
  69. )
  70. this.hunspellWorker.addEventListener('message', this.receive.bind(this))
  71. }
  72. destroy() {
  73. this.send({ type: 'destroy' }, () => {
  74. this.hunspellWorker.terminate()
  75. })
  76. }
  77. send(
  78. message: AddWordMessage,
  79. callback: (value: EmptyResult | ErrorResult) => void
  80. ): void
  81. send(
  82. message: RemoveWordMessage,
  83. callback: (value: EmptyResult | ErrorResult) => void
  84. ): void
  85. send(
  86. message: DestroyMessage,
  87. callback: (value: EmptyResult | ErrorResult) => void
  88. ): void
  89. send(
  90. message: SuggestMessage,
  91. callback: (value: SuggestResult | ErrorResult) => void
  92. ): void
  93. send(
  94. message: SpellMessage,
  95. callback: (value: SpellResult | ErrorResult) => void
  96. ): void
  97. send(message: Message, callback: ResultCallback): void {
  98. if (this.loadingFailed) {
  99. return // ignore the message
  100. }
  101. if (callback) {
  102. message.id = uuid()
  103. this.callbacks.set(message.id, callback)
  104. }
  105. if (this.listening) {
  106. this.hunspellWorker.postMessage(message)
  107. } else {
  108. this.pendingMessages.push(message)
  109. }
  110. }
  111. receive(event: MessageEvent) {
  112. debugConsole.log(event.data)
  113. const { id, ...rest } = event.data
  114. if (id) {
  115. const callback = this.callbacks.get(id)
  116. if (callback) {
  117. this.callbacks.delete(id)
  118. callback(rest)
  119. }
  120. } else if (rest.listening) {
  121. this.listening = true
  122. this.hunspellWorker.postMessage({
  123. type: 'init',
  124. lang: this.language,
  125. learnedWords: this.learnedWords, // TODO: add words
  126. baseAssetPath: this.baseAssetPath,
  127. dictionariesRoot: this.dictionariesRoot,
  128. })
  129. for (const message of this.pendingMessages) {
  130. this.hunspellWorker.postMessage(message)
  131. this.pendingMessages.length = 0
  132. }
  133. } else if (rest.loaded) {
  134. this.loaded = true
  135. } else if (rest.loadingFailed) {
  136. captureException(
  137. new Error('Spell check loading failed', {
  138. cause: rest.loadingFailed,
  139. }),
  140. {
  141. tags: { ol_spell_check_language: this.language },
  142. }
  143. )
  144. this.loadingFailed = true
  145. this.pendingMessages.length = 0
  146. }
  147. }
  148. }