open-documents.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. // Migrated from static methods of Document in Document.js
  2. import { DocumentContainer } from '@/features/ide-react/editor/document-container'
  3. import { debugConsole } from '@/utils/debugging'
  4. import { Socket } from '@/features/ide-react/connection/types/socket'
  5. import { IdeEventEmitter } from '@/features/ide-react/create-ide-event-emitter'
  6. import EditorWatchdogManager from '@/features/ide-react/connection/editor-watchdog-manager'
  7. export class OpenDocuments {
  8. private openDocs = new Map<string, DocumentContainer>()
  9. // eslint-disable-next-line no-useless-constructor
  10. constructor(
  11. private readonly socket: Socket,
  12. private readonly globalEditorWatchdogManager: EditorWatchdogManager,
  13. private readonly events: IdeEventEmitter
  14. ) {}
  15. getDocument(docId: string) {
  16. // Try to clean up existing docs before reopening them. If the doc has no
  17. // buffered ops then it will be deleted by _cleanup() and a new instance
  18. // of the document created below. This prevents us trying to follow the
  19. // joinDoc:existing code path on an existing doc that doesn't have any
  20. // local changes and getting an error if its version is too old.
  21. if (this.openDocs.has(docId)) {
  22. debugConsole.log(
  23. `[getDocument] Cleaning up existing document instance for ${docId}`
  24. )
  25. this.openDocs.get(docId)?.cleanUp()
  26. }
  27. if (!this.openDocs.has(docId)) {
  28. debugConsole.log(
  29. `[getDocument] Creating new document instance for ${docId}`
  30. )
  31. this.createDoc(docId)
  32. } else {
  33. debugConsole.log(
  34. `[getDocument] Returning existing document instance for ${docId}`
  35. )
  36. }
  37. return this.openDocs.get(docId)
  38. }
  39. getUnsavedOpsSize() {
  40. const docs = this.unsavedDocs()
  41. let pendingOpsLength = 0
  42. let inflightOpsLength = 0
  43. for (const doc of docs) {
  44. const pendingOp = doc.getPendingOp()
  45. const inFlightOp = doc.getInflightOp()
  46. pendingOpsLength += pendingOp?.length || 0
  47. inflightOpsLength += inFlightOp?.length || 0
  48. }
  49. return {
  50. pendingOpsLength,
  51. inflightOpsLength,
  52. }
  53. }
  54. private createDoc(docId: string) {
  55. const doc = new DocumentContainer(
  56. docId,
  57. this.socket,
  58. this.globalEditorWatchdogManager,
  59. this.events,
  60. this.detachDoc.bind(this)
  61. )
  62. this.openDocs.set(docId, doc)
  63. }
  64. detachDoc(docId: string, doc: DocumentContainer) {
  65. if (this.openDocs.get(docId) === doc) {
  66. debugConsole.log(
  67. `[detach] Removing document with ID (${docId}) from openDocs`
  68. )
  69. this.openDocs.delete(docId)
  70. } else {
  71. // It's possible that this instance has error, and the doc has been reloaded.
  72. // This creates a new instance in Document.openDoc with the same id. We shouldn't
  73. // clear it because it's not this instance.
  74. debugConsole.log(
  75. `[_cleanUp] New instance of (${docId}) created. Not removing`
  76. )
  77. }
  78. }
  79. hasUnsavedChanges() {
  80. for (const doc of this.openDocs.values()) {
  81. if (doc.hasBufferedOps()) {
  82. return true
  83. }
  84. }
  85. return false
  86. }
  87. flushAll() {
  88. for (const doc of this.openDocs.values()) {
  89. doc.flush()
  90. }
  91. }
  92. unsavedDocs() {
  93. const docs = []
  94. for (const doc of this.openDocs.values()) {
  95. if (!doc.pollSavedStatus()) {
  96. docs.push(doc)
  97. }
  98. }
  99. return docs
  100. }
  101. async awaitBufferedOps(signal: AbortSignal) {
  102. if (this.hasUnsavedChanges()) {
  103. const { promise, resolve } = Promise.withResolvers<void>()
  104. let resolved = false
  105. const listener = () => {
  106. if (!this.hasUnsavedChanges()) {
  107. debugConsole.log('saved')
  108. window.removeEventListener('doc:saved', listener)
  109. resolved = true
  110. resolve()
  111. }
  112. }
  113. window.addEventListener('doc:saved', listener)
  114. signal.addEventListener('abort', () => {
  115. if (!resolved) {
  116. debugConsole.log('aborted')
  117. window.removeEventListener('doc:saved', listener)
  118. resolve()
  119. }
  120. })
  121. this.flushAll()
  122. await promise
  123. }
  124. }
  125. }