Просмотр исходного кода

Merge pull request #14418 from overleaf/em-history-lib-es6-classes

Move overleaf-editor-core code to ES6 classes

GitOrigin-RevId: f9b50579aec0cef9d9e6aefcfcb3e380fae4b6f4
Eric Mc Sween 3 лет назад
Родитель
Сommit
d54bcc4aa9
31 измененных файлов с 1589 добавлено и 1431 удалено
  1. 50 48
      libraries/overleaf-editor-core/lib/author.js
  2. 1 1
      libraries/overleaf-editor-core/lib/author_list.js
  3. 80 76
      libraries/overleaf-editor-core/lib/blob.js
  4. 3 5
      libraries/overleaf-editor-core/lib/change.js
  5. 39 38
      libraries/overleaf-editor-core/lib/change_note.js
  6. 54 54
      libraries/overleaf-editor-core/lib/change_request.js
  7. 91 90
      libraries/overleaf-editor-core/lib/chunk.js
  8. 20 19
      libraries/overleaf-editor-core/lib/chunk_response.js
  9. 175 178
      libraries/overleaf-editor-core/lib/file.js
  10. 0 1
      libraries/overleaf-editor-core/lib/file_data/binary_file_data.js
  11. 0 1
      libraries/overleaf-editor-core/lib/file_data/hollow_binary_file_data.js
  12. 0 1
      libraries/overleaf-editor-core/lib/file_data/hollow_string_file_data.js
  13. 0 1
      libraries/overleaf-editor-core/lib/file_data/index.js
  14. 0 1
      libraries/overleaf-editor-core/lib/file_data/lazy_string_file_data.js
  15. 0 1
      libraries/overleaf-editor-core/lib/file_data/string_file_data.js
  16. 39 37
      libraries/overleaf-editor-core/lib/file_map.js
  17. 98 95
      libraries/overleaf-editor-core/lib/history.js
  18. 69 60
      libraries/overleaf-editor-core/lib/label.js
  19. 0 2
      libraries/overleaf-editor-core/lib/operation/add_file_operation.js
  20. 0 2
      libraries/overleaf-editor-core/lib/operation/edit_file_operation.js
  21. 0 1
      libraries/overleaf-editor-core/lib/operation/index.js
  22. 1 1
      libraries/overleaf-editor-core/lib/operation/move_file_operation.js
  23. 0 1
      libraries/overleaf-editor-core/lib/operation/no_operation.js
  24. 0 2
      libraries/overleaf-editor-core/lib/operation/set_file_metadata_operation.js
  25. 590 438
      libraries/overleaf-editor-core/lib/operation/text_operation.js
  26. 37 35
      libraries/overleaf-editor-core/lib/origin/index.js
  27. 0 2
      libraries/overleaf-editor-core/lib/origin/restore_origin.js
  28. 195 192
      libraries/overleaf-editor-core/lib/ot_client.js
  29. 8 9
      libraries/overleaf-editor-core/lib/snapshot.js
  30. 38 38
      libraries/overleaf-editor-core/lib/v2_doc_versions.js
  31. 1 1
      libraries/overleaf-editor-core/tsconfig.json

+ 50 - 48
libraries/overleaf-editor-core/lib/author.js

@@ -3,11 +3,6 @@
 const assert = require('check-types').assert
 
 /**
- * @constructor
- * @param {number} id
- * @param {string} email
- * @param {string} name
- * @classdesc
  * An author of a {@link Change}. We want to store user IDs, and then fill in
  * the other properties (which the user can change over time) when changes are
  * loaded.
@@ -16,55 +11,62 @@ const assert = require('check-types').assert
  * generalise this to cover users for whom we only have a name and email, e.g.
  * from git. For now, though, this seems to do what we need.
  */
-function Author(id, email, name) {
-  assert.number(id, 'bad id')
-  assert.string(email, 'bad email')
-  assert.string(name, 'bad name')
+class Author {
+  /**
+   * @param {number} id
+   * @param {string} email
+   * @param {string} name
+   */
+  constructor(id, email, name) {
+    assert.number(id, 'bad id')
+    assert.string(email, 'bad email')
+    assert.string(name, 'bad name')
 
-  this.id = id
-  this.email = email
-  this.name = name
-}
+    this.id = id
+    this.email = email
+    this.name = name
+  }
 
-/**
- * Create an Author from its raw form.
- *
- * @param {Object} [raw]
- * @return {Author | null}
- */
-Author.fromRaw = function authorFromRaw(raw) {
-  if (!raw) return null
-  return new Author(raw.id, raw.email, raw.name)
-}
+  /**
+   * Create an Author from its raw form.
+   *
+   * @param {Object} [raw]
+   * @return {Author | null}
+   */
+  static fromRaw(raw) {
+    if (!raw) return null
+    return new Author(raw.id, raw.email, raw.name)
+  }
 
-/**
- * Convert the Author to raw form for storage or transmission.
- *
- * @return {Object}
- */
-Author.prototype.toRaw = function authorToRaw() {
-  return { id: this.id, email: this.email, name: this.name }
-}
+  /**
+   * Convert the Author to raw form for storage or transmission.
+   *
+   * @return {Object}
+   */
+  toRaw() {
+    return { id: this.id, email: this.email, name: this.name }
+  }
 
-/**
- * @return {number}
- */
-Author.prototype.getId = function () {
-  return this.id
-}
+  /**
+   * @return {number}
+   */
+  getId() {
+    return this.id
+  }
 
-/**
- * @return {string}
- */
-Author.prototype.getEmail = function () {
-  return this.email
-}
+  /**
+   * @return {string}
+   */
+  getEmail() {
+    return this.email
+  }
 
-/**
- * @return {string}
- */
-Author.prototype.getName = function () {
-  return this.name
+  /**
+   * @return {string}
+   */
+  getName() {
+    return this.name
+  }
 }
 
 module.exports = Author

+ 1 - 1
libraries/overleaf-editor-core/lib/author_list.js

@@ -42,4 +42,4 @@ function assertV2(authors, msg) {
   })
 }
 
-module.exports = { assertV1: assertV1, assertV2: assertV2 }
+module.exports = { assertV1, assertV2 }

+ 80 - 76
libraries/overleaf-editor-core/lib/blob.js

@@ -5,96 +5,100 @@ const OError = require('@overleaf/o-error')
 
 const TextOperation = require('./operation/text_operation')
 
-/**
- * @constructor
- * @classdesc
- * Metadata record for the content of a file.
- */
-function Blob(hash, byteLength, stringLength) {
-  this.setHash(hash)
-  this.setByteLength(byteLength)
-  this.setStringLength(stringLength)
-}
-
 class NotFoundError extends OError {
   constructor(hash) {
     super(`blob ${hash} not found`, { hash })
     this.hash = hash
   }
 }
-Blob.NotFoundError = NotFoundError
 
-Blob.HEX_HASH_RX_STRING = '^[0-9a-f]{40,40}$'
-Blob.HEX_HASH_RX = new RegExp(Blob.HEX_HASH_RX_STRING)
+/**
+ * Metadata record for the content of a file.
+ */
+class Blob {
+  static HEX_HASH_RX_STRING = '^[0-9a-f]{40,40}$'
+  static HEX_HASH_RX = new RegExp(Blob.HEX_HASH_RX_STRING)
 
-module.exports = Blob
+  /**
+   * Size of the largest file that we'll read to determine whether we can edit it
+   * or not, in bytes. The final decision on whether a file is editable or not is
+   * based on the number of characters it contains, but we need to read the file
+   * in to determine that; so it is useful to have an upper bound on the byte
+   * length of a file that might be editable.
+   *
+   * The reason for the factor of 3 is as follows. We cannot currently edit files
+   * that contain characters outside of the basic multilingual plane, so we're
+   * limited to characters that can be represented in a single, two-byte UCS-2
+   * code unit. Encoding the largest such value, 0xFFFF (which is not actually
+   * a valid character), takes three bytes in UTF-8: 0xEF 0xBF 0xBF. A file
+   * composed entirely of three-byte UTF-8 codepoints is the worst case; in
+   * practice, this is a very conservative upper bound.
+   *
+   * @type {number}
+   */
+  static MAX_EDITABLE_BYTE_LENGTH_BOUND = 3 * TextOperation.MAX_STRING_LENGTH
+
+  static NotFoundError = NotFoundError
 
-Blob.fromRaw = function blobFromRaw(raw) {
-  if (raw) {
-    return new Blob(raw.hash, raw.byteLength, raw.stringLength)
+  constructor(hash, byteLength, stringLength) {
+    this.setHash(hash)
+    this.setByteLength(byteLength)
+    this.setStringLength(stringLength)
   }
-  return null
-}
 
-Blob.prototype.toRaw = function blobToRaw() {
-  return {
-    hash: this.hash,
-    byteLength: this.byteLength,
-    stringLength: this.stringLength,
+  static fromRaw(raw) {
+    if (raw) {
+      return new Blob(raw.hash, raw.byteLength, raw.stringLength)
+    }
+    return null
   }
-}
 
-/**
- * Hex hash.
- * @return {?String}
- */
-Blob.prototype.getHash = function () {
-  return this.hash
-}
-Blob.prototype.setHash = function (hash) {
-  assert.maybe.match(hash, Blob.HEX_HASH_RX, 'bad hash')
-  this.hash = hash
-}
+  toRaw() {
+    return {
+      hash: this.hash,
+      byteLength: this.byteLength,
+      stringLength: this.stringLength,
+    }
+  }
 
-/**
- * Length of the blob in bytes.
- * @return {number}
- */
-Blob.prototype.getByteLength = function () {
-  return this.byteLength
-}
-Blob.prototype.setByteLength = function (byteLength) {
-  assert.maybe.integer(byteLength, 'bad byteLength')
-  this.byteLength = byteLength
-}
+  /**
+   * Hex hash.
+   * @return {?String}
+   */
+  getHash() {
+    return this.hash
+  }
 
-/**
- * Utf-8 length of the blob content, if it appears to be valid UTF-8.
- * @return {?number}
- */
-Blob.prototype.getStringLength = function () {
-  return this.stringLength
-}
-Blob.prototype.setStringLength = function (stringLength) {
-  assert.maybe.integer(stringLength, 'bad stringLength')
-  this.stringLength = stringLength
+  setHash(hash) {
+    assert.maybe.match(hash, Blob.HEX_HASH_RX, 'bad hash')
+    this.hash = hash
+  }
+
+  /**
+   * Length of the blob in bytes.
+   * @return {number}
+   */
+  getByteLength() {
+    return this.byteLength
+  }
+
+  setByteLength(byteLength) {
+    assert.maybe.integer(byteLength, 'bad byteLength')
+    this.byteLength = byteLength
+  }
+
+  /**
+   * Utf-8 length of the blob content, if it appears to be valid UTF-8.
+   * @return {?number}
+   */
+  getStringLength() {
+    return this.stringLength
+  }
+
+  setStringLength(stringLength) {
+    assert.maybe.integer(stringLength, 'bad stringLength')
+    this.stringLength = stringLength
+  }
 }
 
-/**
- * Size of the largest file that we'll read to determine whether we can edit it
- * or not, in bytes. The final decision on whether a file is editable or not is
- * based on the number of characters it contains, but we need to read the file
- * in to determine that; so it is useful to have an upper bound on the byte
- * length of a file that might be editable.
- *
- * The reason for the factor of 3 is as follows. We cannot currently edit files
- * that contain characters outside of the basic multilingual plane, so we're
- * limited to characters that can be represented in a single, two-byte UCS-2
- * code unit. Encoding the largest such value, 0xFFFF (which is not actually
- * a valid character), takes three bytes in UTF-8: 0xEF 0xBF 0xBF. A file
- * composed entirely of three-byte UTF-8 codepoints is the worst case; in
- * practice, this is a very conservative upper bound.
- *
- * @type {number}
- */
-Blob.MAX_EDITABLE_BYTE_LENGTH_BOUND = 3 * TextOperation.MAX_STRING_LENGTH
+module.exports = Blob

+ 3 - 5
libraries/overleaf-editor-core/lib/change.js

@@ -17,13 +17,14 @@ const V2DocVersions = require('./v2_doc_versions')
  */
 
 /**
- * @classdesc
  * A Change is a list of {@link Operation}s applied atomically by given
  * {@link Author}(s) at a given time.
  */
 class Change {
+  static PROJECT_VERSION_RX_STRING = '^[0-9]+\\.[0-9]+$'
+  static PROJECT_VERSION_RX = new RegExp(Change.PROJECT_VERSION_RX_STRING)
+
   /**
-   * @constructor
    * @param {Array.<Operation>} operations
    * @param {Date} timestamp
    * @param {number[] | Author[]} [authors]
@@ -327,7 +328,4 @@ class Change {
   }
 }
 
-Change.PROJECT_VERSION_RX_STRING = '^[0-9]+\\.[0-9]+$'
-Change.PROJECT_VERSION_RX = new RegExp(Change.PROJECT_VERSION_RX_STRING)
-
 module.exports = Change

+ 39 - 38
libraries/overleaf-editor-core/lib/change_note.js

@@ -5,56 +5,57 @@ const assert = require('check-types').assert
 const Change = require('./change')
 
 /**
- * @constructor
- * @param {number} baseVersion the new base version for the change
- * @param {?Change} change
- * @classdesc
  * A `ChangeNote` is returned when the server has applied a {@link Change}.
  */
-function ChangeNote(baseVersion, change) {
-  assert.integer(baseVersion, 'bad baseVersion')
-  assert.maybe.instance(change, Change, 'bad change')
-
-  this.baseVersion = baseVersion
-  this.change = change
-}
-
-module.exports = ChangeNote
+class ChangeNote {
+  /**
+   * @param {number} baseVersion the new base version for the change
+   * @param {?Change} change
+   */
+  constructor(baseVersion, change) {
+    assert.integer(baseVersion, 'bad baseVersion')
+    assert.maybe.instance(change, Change, 'bad change')
+
+    this.baseVersion = baseVersion
+    this.change = change
+  }
 
-/**
- * For serialization.
- *
- * @return {Object}
- */
-ChangeNote.prototype.toRaw = function changeNoteToRaw() {
-  return {
-    baseVersion: this.baseVersion,
-    change: this.change.toRaw(),
+  /**
+   * For serialization.
+   *
+   * @return {Object}
+   */
+  toRaw() {
+    return {
+      baseVersion: this.baseVersion,
+      change: this.change.toRaw(),
+    }
   }
-}
 
-ChangeNote.prototype.toRawWithoutChange =
-  function changeNoteToRawWithoutChange() {
+  toRawWithoutChange() {
     return {
       baseVersion: this.baseVersion,
     }
   }
 
-ChangeNote.fromRaw = function changeNoteFromRaw(raw) {
-  assert.integer(raw.baseVersion, 'bad raw.baseVersion')
-  assert.maybe.object(raw.change, 'bad raw.changes')
+  static fromRaw(raw) {
+    assert.integer(raw.baseVersion, 'bad raw.baseVersion')
+    assert.maybe.object(raw.change, 'bad raw.changes')
 
-  return new ChangeNote(raw.baseVersion, Change.fromRaw(raw.change))
-}
+    return new ChangeNote(raw.baseVersion, Change.fromRaw(raw.change))
+  }
 
-ChangeNote.prototype.getBaseVersion = function () {
-  return this.baseVersion
-}
+  getBaseVersion() {
+    return this.baseVersion
+  }
 
-ChangeNote.prototype.getResultVersion = function () {
-  return this.baseVersion + 1
-}
+  getResultVersion() {
+    return this.baseVersion + 1
+  }
 
-ChangeNote.prototype.getChange = function () {
-  return this.change
+  getChange() {
+    return this.change
+  }
 }
+
+module.exports = ChangeNote

+ 54 - 54
libraries/overleaf-editor-core/lib/change_request.js

@@ -11,12 +11,6 @@ const Operation = require('./operation')
  */
 
 /**
- * @constructor
- * @param {number} baseVersion
- * @param {Array.<Operation>} operations
- * @param {boolean} [untransformable]
- * @param {number[] | Author[]} [authors]
- * @classdesc
  * A `ChangeRequest` is a list of {@link Operation}s that the server can apply
  * as a {@link Change}.
  *
@@ -28,63 +22,69 @@ const Operation = require('./operation')
  * metadata at the same time. The expectation is that if the change is rejected,
  * the client will retry on a later version.
  */
-function ChangeRequest(baseVersion, operations, untransformable, authors) {
-  assert.integer(baseVersion, 'bad baseVersion')
-  assert.array.of.object(operations, 'bad operations')
-  assert.maybe.boolean(untransformable, 'ChangeRequest: bad untransformable')
-  // TODO remove authors once we have JWTs working --- pass as parameter to
-  // makeChange instead
-  authors = authors || []
+class ChangeRequest {
+  /**
+   * @param {number} baseVersion
+   * @param {Array.<Operation>} operations
+   * @param {boolean} [untransformable]
+   * @param {number[] | Author[]} [authors]
+   */
+  constructor(baseVersion, operations, untransformable, authors) {
+    assert.integer(baseVersion, 'bad baseVersion')
+    assert.array.of.object(operations, 'bad operations')
+    assert.maybe.boolean(untransformable, 'ChangeRequest: bad untransformable')
+    // TODO remove authors once we have JWTs working --- pass as parameter to
+    // makeChange instead
+    authors = authors || []
 
-  // check all are the same type
-  AuthorList.assertV1(authors, 'bad authors')
+    // check all are the same type
+    AuthorList.assertV1(authors, 'bad authors')
 
-  this.authors = authors
-  this.baseVersion = baseVersion
-  this.operations = operations
-  this.untransformable = untransformable || false
-}
+    this.authors = authors
+    this.baseVersion = baseVersion
+    this.operations = operations
+    this.untransformable = untransformable || false
+  }
 
-module.exports = ChangeRequest
+  /**
+   * For serialization.
+   *
+   * @return {Object}
+   */
+  toRaw() {
+    function operationToRaw(operation) {
+      return operation.toRaw()
+    }
 
-/**
- * For serialization.
- *
- * @return {Object}
- */
-ChangeRequest.prototype.toRaw = function changeRequestToRaw() {
-  function operationToRaw(operation) {
-    return operation.toRaw()
+    return {
+      baseVersion: this.baseVersion,
+      operations: this.operations.map(operationToRaw),
+      untransformable: this.untransformable,
+      authors: this.authors,
+    }
   }
 
-  return {
-    baseVersion: this.baseVersion,
-    operations: this.operations.map(operationToRaw),
-    untransformable: this.untransformable,
-    authors: this.authors,
+  static fromRaw(raw) {
+    assert.array.of.object(raw.operations, 'bad raw.operations')
+    return new ChangeRequest(
+      raw.baseVersion,
+      raw.operations.map(Operation.fromRaw),
+      raw.untransformable,
+      raw.authors
+    )
   }
-}
 
-ChangeRequest.fromRaw = function changeRequestFromRaw(raw) {
-  assert.array.of.object(raw.operations, 'bad raw.operations')
-  return new ChangeRequest(
-    raw.baseVersion,
-    raw.operations.map(Operation.fromRaw),
-    raw.untransformable,
-    raw.authors
-  )
-}
+  getBaseVersion() {
+    return this.baseVersion
+  }
 
-ChangeRequest.prototype.getBaseVersion = function () {
-  return this.baseVersion
-}
+  isUntransformable() {
+    return this.untransformable
+  }
 
-ChangeRequest.prototype.isUntransformable = function () {
-  return this.untransformable
+  makeChange(timestamp) {
+    return new Change(this.operations, timestamp, this.authors)
+  }
 }
 
-ChangeRequest.prototype.makeChange = function changeRequestMakeChange(
-  timestamp
-) {
-  return new Change(this.operations, timestamp, this.authors)
-}
+module.exports = ChangeRequest

+ 91 - 90
libraries/overleaf-editor-core/lib/chunk.js

@@ -10,24 +10,6 @@ const History = require('./history')
  * @typedef {import("./change")} Change
  * @typedef {import("./snapshot")} Snapshot
  */
-
-/**
- * @constructor
- * @param {History} history
- * @param {number} startVersion
- *
- * @classdesc
- * A Chunk is a {@link History} that is part of a project's overall history. It
- * has a start and an end version that place its History in context.
- */
-function Chunk(history, startVersion) {
-  assert.instance(history, History, 'bad history')
-  assert.integer(startVersion, 'bad startVersion')
-
-  this.history = history
-  this.startVersion = startVersion
-}
-
 class ConflictingEndVersion extends OError {
   constructor(clientEndVersion, latestEndVersion) {
     const message =
@@ -40,7 +22,6 @@ class ConflictingEndVersion extends OError {
     this.latestEndVersion = latestEndVersion
   }
 }
-Chunk.ConflictingEndVersion = ConflictingEndVersion
 
 class NotFoundError extends OError {
   // `message` and `info` optional arguments allow children classes to override
@@ -53,7 +34,6 @@ class NotFoundError extends OError {
     this.projectId = projectId
   }
 }
-Chunk.NotFoundError = NotFoundError
 
 class VersionNotFoundError extends NotFoundError {
   constructor(projectId, version) {
@@ -65,7 +45,6 @@ class VersionNotFoundError extends NotFoundError {
     this.version = version
   }
 }
-Chunk.VersionNotFoundError = VersionNotFoundError
 
 class BeforeTimestampNotFoundError extends NotFoundError {
   constructor(projectId, timestamp) {
@@ -74,7 +53,6 @@ class BeforeTimestampNotFoundError extends NotFoundError {
     this.timestamp = timestamp
   }
 }
-Chunk.BeforeTimestampNotFoundError = BeforeTimestampNotFoundError
 
 class NotPersistedError extends NotFoundError {
   constructor(projectId) {
@@ -82,85 +60,108 @@ class NotPersistedError extends NotFoundError {
     this.projectId = projectId
   }
 }
-Chunk.NotPersistedError = NotPersistedError
-
-Chunk.fromRaw = function chunkFromRaw(raw) {
-  return new Chunk(History.fromRaw(raw.history), raw.startVersion)
-}
-
-Chunk.prototype.toRaw = function chunkToRaw() {
-  return { history: this.history.toRaw(), startVersion: this.startVersion }
-}
 
 /**
- * The history for this chunk.
- *
- * @return {History}
+ * A Chunk is a {@link History} that is part of a project's overall history. It
+ * has a start and an end version that place its History in context.
  */
-Chunk.prototype.getHistory = function () {
-  return this.history
-}
+class Chunk {
+  static ConflictingEndVersion = ConflictingEndVersion
+  static NotFoundError = NotFoundError
+  static VersionNotFoundError = VersionNotFoundError
+  static BeforeTimestampNotFoundError = BeforeTimestampNotFoundError
+  static NotPersistedError = NotPersistedError
+
+  /**
+   * @param {History} history
+   * @param {number} startVersion
+   */
+  constructor(history, startVersion) {
+    assert.instance(history, History, 'bad history')
+    assert.integer(startVersion, 'bad startVersion')
+
+    this.history = history
+    this.startVersion = startVersion
+  }
 
-/**
- * {@see History#getSnapshot}
- * @return {Snapshot}
- */
-Chunk.prototype.getSnapshot = function () {
-  return this.history.getSnapshot()
-}
+  static fromRaw(raw) {
+    return new Chunk(History.fromRaw(raw.history), raw.startVersion)
+  }
 
-/**
- * {@see History#getChanges}
- * @return {Array.<Change>}
- */
-Chunk.prototype.getChanges = function () {
-  return this.history.getChanges()
-}
+  toRaw() {
+    return { history: this.history.toRaw(), startVersion: this.startVersion }
+  }
 
-/**
- * {@see History#pushChanges}
- * @param {Array.<Change>} changes
- */
-Chunk.prototype.pushChanges = function chunkPushChanges(changes) {
-  this.history.pushChanges(changes)
-}
+  /**
+   * The history for this chunk.
+   *
+   * @return {History}
+   */
+  getHistory() {
+    return this.history
+  }
 
-/**
- * The version of the project after applying all changes in this chunk.
- *
- * @return {number} non-negative, greater than or equal to start version
- */
-Chunk.prototype.getEndVersion = function chunkGetEndVersion() {
-  return this.startVersion + this.history.countChanges()
-}
+  /**
+   * {@see History#getSnapshot}
+   * @return {Snapshot}
+   */
+  getSnapshot() {
+    return this.history.getSnapshot()
+  }
 
-/**
- * The timestamp of the last change in this chunk
- */
+  /**
+   * {@see History#getChanges}
+   * @return {Array.<Change>}
+   */
+  getChanges() {
+    return this.history.getChanges()
+  }
 
-Chunk.prototype.getEndTimestamp = function getEndTimestamp() {
-  if (!this.history.countChanges()) return null
-  return this.history.getChanges().slice(-1)[0].getTimestamp()
-}
+  /**
+   * {@see History#pushChanges}
+   * @param {Array.<Change>} changes
+   */
+  pushChanges(changes) {
+    this.history.pushChanges(changes)
+  }
 
-/**
- * The version of the project before applying all changes in this chunk.
- *
- * @return {number} non-negative, less than or equal to end version
- */
-Chunk.prototype.getStartVersion = function () {
-  return this.startVersion
-}
+  /**
+   * The version of the project after applying all changes in this chunk.
+   *
+   * @return {number} non-negative, greater than or equal to start version
+   */
+  getEndVersion() {
+    return this.startVersion + this.history.countChanges()
+  }
 
-/**
- * {@see History#loadFiles}
- *
- * @param {string} kind
- * @param {BlobStore} blobStore
- * @return {Promise}
- */
-Chunk.prototype.loadFiles = function chunkLoadFiles(kind, blobStore) {
-  return this.history.loadFiles(kind, blobStore)
+  /**
+   * The timestamp of the last change in this chunk
+   */
+
+  getEndTimestamp() {
+    if (!this.history.countChanges()) return null
+    return this.history.getChanges().slice(-1)[0].getTimestamp()
+  }
+
+  /**
+   * The version of the project before applying all changes in this chunk.
+   *
+   * @return {number} non-negative, less than or equal to end version
+   */
+  getStartVersion() {
+    return this.startVersion
+  }
+
+  /**
+   * {@see History#loadFiles}
+   *
+   * @param {string} kind
+   * @param {BlobStore} blobStore
+   * @return {Promise}
+   */
+  loadFiles(kind, blobStore) {
+    return this.history.loadFiles(kind, blobStore)
+  }
 }
 
 module.exports = Chunk

+ 20 - 19
libraries/overleaf-editor-core/lib/chunk_response.js

@@ -3,30 +3,31 @@
 const assert = require('check-types').assert
 const Chunk = require('./chunk')
 
-//
-// The ChunkResponse allows for additional data to be sent back with the chunk
-// at present there are no extra data to send.
-//
-
-function ChunkResponse(chunk) {
-  assert.instance(chunk, Chunk)
-  this.chunk = chunk
-}
+/**
+ * The ChunkResponse allows for additional data to be sent back with the chunk
+ * at present there are no extra data to send.
+ */
+class ChunkResponse {
+  constructor(chunk) {
+    assert.instance(chunk, Chunk)
+    this.chunk = chunk
+  }
 
-ChunkResponse.prototype.toRaw = function chunkResponseToRaw() {
-  return {
-    chunk: this.chunk.toRaw(),
+  toRaw() {
+    return {
+      chunk: this.chunk.toRaw(),
+    }
   }
-}
 
-ChunkResponse.fromRaw = function chunkResponseFromRaw(raw) {
-  if (!raw) return null
+  static fromRaw(raw) {
+    if (!raw) return null
 
-  return new ChunkResponse(Chunk.fromRaw(raw.chunk))
-}
+    return new ChunkResponse(Chunk.fromRaw(raw.chunk))
+  }
 
-ChunkResponse.prototype.getChunk = function () {
-  return this.chunk
+  getChunk() {
+    return this.chunk
+  }
 }
 
 module.exports = ChunkResponse

+ 175 - 178
libraries/overleaf-editor-core/lib/file.js

@@ -20,12 +20,13 @@ const StringFileData = require('./file_data/string_file_data')
  * @typedef {import("bluebird")<T>} BPromise
  */
 
+class NotEditableError extends OError {
+  constructor() {
+    super('File is not editable')
+  }
+}
+
 /**
- * @constructor
- * @param {FileData} data
- * @param {Object} [metadata]
- *
- * @classdesc
  * A file in a {@link Snapshot}. A file has both data and metadata. There
  * are several classes of data that represent the various types of file
  * data that are supported, namely text and binary, and also the various
@@ -33,209 +34,205 @@ const StringFileData = require('./file_data/string_file_data')
  *
  * 1. Hash only: all we know is the file's hash; this is how we encode file
  *    content in long term storage.
- * 1. Lazily loaded: the hash of the file, its length, and its type are known,
+ * 2. Lazily loaded: the hash of the file, its length, and its type are known,
  *    but its content is not loaded. Operations are cached for application
  *    later.
- * 1. Eagerly loaded: the content of a text file is fully loaded into memory
+ * 3. Eagerly loaded: the content of a text file is fully loaded into memory
  *    as a string.
- * 1. Hollow: only the byte and/or UTF-8 length of the file are known; this is
+ * 4. Hollow: only the byte and/or UTF-8 length of the file are known; this is
  *    used to allow for validation of operations when editing collaboratively
  *    without having to keep file data in memory on the server.
  */
-function File(data, metadata) {
-  assert.instance(data, FileData, 'File: bad data')
+class File {
+  /**
+   * Blob hash for an empty file.
+   *
+   * @type {String}
+   */
+  static EMPTY_FILE_HASH = 'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391'
 
-  this.data = data
-  this.setMetadata(metadata || {})
-}
+  static NotEditableError = NotEditableError
 
-File.fromRaw = function fileFromRaw(raw) {
-  if (!raw) return null
-  return new File(FileData.fromRaw(raw), raw.metadata)
-}
+  /**
+   * @param {FileData} data
+   * @param {Object} [metadata]
+   */
+  constructor(data, metadata) {
+    assert.instance(data, FileData, 'File: bad data')
 
-/**
- * @param  {string} hash
- * @param  {Object} [metadata]
- * @return {File}
- */
-File.fromHash = function fileFromHash(hash, metadata) {
-  return new File(new HashFileData(hash), metadata)
-}
+    this.data = data
+    this.setMetadata(metadata || {})
+  }
 
-/**
- * @param  {string} string
- * @param  {Object} [metadata]
- * @return {File}
- */
-File.fromString = function fileFromString(string, metadata) {
-  return new File(new StringFileData(string), metadata)
-}
+  static fromRaw(raw) {
+    if (!raw) return null
+    return new File(FileData.fromRaw(raw), raw.metadata)
+  }
 
-/**
- * @param  {number} [byteLength]
- * @param  {number} [stringLength]
- * @param  {Object} [metadata]
- * @return {File}
- */
-File.createHollow = function fileCreateHollow(
-  byteLength,
-  stringLength,
-  metadata
-) {
-  return new File(FileData.createHollow(byteLength, stringLength), metadata)
-}
+  /**
+   * @param  {string} hash
+   * @param  {Object} [metadata]
+   * @return {File}
+   */
+  static fromHash(hash, metadata) {
+    return new File(new HashFileData(hash), metadata)
+  }
 
-/**
- * @param {Blob} blob
- * @param {Object} [metadata]
- * @return {File}
- */
-File.createLazyFromBlob = function fileCreateLazyFromBlob(blob, metadata) {
-  return new File(FileData.createLazyFromBlob(blob), metadata)
-}
+  /**
+   * @param  {string} string
+   * @param  {Object} [metadata]
+   * @return {File}
+   */
+  static fromString(string, metadata) {
+    return new File(new StringFileData(string), metadata)
+  }
 
-function storeRawMetadata(metadata, raw) {
-  if (!_.isEmpty(metadata)) {
-    raw.metadata = _.cloneDeep(metadata)
+  /**
+   * @param  {number} [byteLength]
+   * @param  {number} [stringLength]
+   * @param  {Object} [metadata]
+   * @return {File}
+   */
+  static createHollow(byteLength, stringLength, metadata) {
+    return new File(FileData.createHollow(byteLength, stringLength), metadata)
   }
-}
 
-File.prototype.toRaw = function () {
-  const rawFileData = this.data.toRaw()
-  storeRawMetadata(this.metadata, rawFileData)
-  return rawFileData
-}
+  /**
+   * @param {Blob} blob
+   * @param {Object} [metadata]
+   * @return {File}
+   */
+  static createLazyFromBlob(blob, metadata) {
+    return new File(FileData.createLazyFromBlob(blob), metadata)
+  }
 
-/**
- * Hexadecimal SHA-1 hash of the file's content, if known.
- *
- * @return {string | null | undefined}
- */
-File.prototype.getHash = function () {
-  return this.data.getHash()
-}
+  toRaw() {
+    const rawFileData = this.data.toRaw()
+    storeRawMetadata(this.metadata, rawFileData)
+    return rawFileData
+  }
 
-/**
- * The content of the file, if it is known and if this file has UTF-8 encoded
- * content.
- *
- * @return {string | null | undefined}
- */
-File.prototype.getContent = function () {
-  return this.data.getContent()
-}
+  /**
+   * Hexadecimal SHA-1 hash of the file's content, if known.
+   *
+   * @return {string | null | undefined}
+   */
+  getHash() {
+    return this.data.getHash()
+  }
 
-/**
- * Whether this file has string content and is small enough to be edited using
- * {@link TextOperation}s.
- *
- * @return {boolean | null | undefined} null if it is not currently known
- */
-File.prototype.isEditable = function () {
-  return this.data.isEditable()
-}
+  /**
+   * The content of the file, if it is known and if this file has UTF-8 encoded
+   * content.
+   *
+   * @return {string | null | undefined}
+   */
+  getContent() {
+    return this.data.getContent()
+  }
 
-/**
- * The length of the file's content in bytes, if known.
- *
- * @return {number | null | undefined}
- */
-File.prototype.getByteLength = function () {
-  return this.data.getByteLength()
-}
+  /**
+   * Whether this file has string content and is small enough to be edited using
+   * {@link TextOperation}s.
+   *
+   * @return {boolean | null | undefined} null if it is not currently known
+   */
+  isEditable() {
+    return this.data.isEditable()
+  }
 
-/**
- * The length of the file's content in characters, if known.
- *
- * @return {number | null | undefined}
- */
-File.prototype.getStringLength = function () {
-  return this.data.getStringLength()
-}
+  /**
+   * The length of the file's content in bytes, if known.
+   *
+   * @return {number | null | undefined}
+   */
+  getByteLength() {
+    return this.data.getByteLength()
+  }
 
-/**
- * Return the metadata object for this file.
- *
- * @return {Object}
- */
-File.prototype.getMetadata = function () {
-  return this.metadata
-}
+  /**
+   * The length of the file's content in characters, if known.
+   *
+   * @return {number | null | undefined}
+   */
+  getStringLength() {
+    return this.data.getStringLength()
+  }
 
-/**
- * Set the metadata object for this file.
- *
- * @param {Object} metadata
- */
-File.prototype.setMetadata = function (metadata) {
-  assert.object(metadata, 'File: bad metadata')
-  this.metadata = metadata
-}
+  /**
+   * Return the metadata object for this file.
+   *
+   * @return {Object}
+   */
+  getMetadata() {
+    return this.metadata
+  }
 
-class NotEditableError extends OError {
-  constructor() {
-    super('File is not editable')
+  /**
+   * Set the metadata object for this file.
+   *
+   * @param {Object} metadata
+   */
+  setMetadata(metadata) {
+    assert.object(metadata, 'File: bad metadata')
+    this.metadata = metadata
   }
-}
 
-File.NotEditableError = NotEditableError
+  /**
+   * Edit this file, if possible.
+   *
+   * @param {TextOperation} textOperation
+   */
+  edit(textOperation) {
+    if (!this.data.isEditable()) throw new File.NotEditableError()
+    this.data.edit(textOperation)
+  }
 
-/**
- * Edit this file, if possible.
- *
- * @param {TextOperation} textOperation
- */
-File.prototype.edit = function (textOperation) {
-  if (!this.data.isEditable()) throw new File.NotEditableError()
-  this.data.edit(textOperation)
-}
+  /**
+   * Clone a file.
+   *
+   * @return {File} a new object of the same type
+   */
+  clone() {
+    return File.fromRaw(this.toRaw())
+  }
 
-/**
- * Clone a file.
- *
- * @return {File} a new object of the same type
- */
-File.prototype.clone = function fileClone() {
-  return File.fromRaw(this.toRaw())
-}
+  /**
+   * Convert this file's data to the given kind. This may require us to load file
+   * size or content from the given blob store, so this is an asynchronous
+   * operation.
+   *
+   * @param {string} kind
+   * @param {BlobStore} blobStore
+   * @return {Promise.<File>} for this
+   */
+  load(kind, blobStore) {
+    return this.data.load(kind, blobStore).then(data => {
+      this.data = data
+      return this
+    })
+  }
 
-/**
- * Convert this file's data to the given kind. This may require us to load file
- * size or content from the given blob store, so this is an asynchronous
- * operation.
- *
- * @param {string} kind
- * @param {BlobStore} blobStore
- * @return {Promise.<File>} for this
- */
-File.prototype.load = function (kind, blobStore) {
-  return this.data.load(kind, blobStore).then(data => {
-    this.data = data
-    return this
-  })
+  /**
+   * Store the file's content in the blob store and return a raw file with
+   * the corresponding hash. As a side effect, make this object consistent with
+   * the hash.
+   *
+   * @param {BlobStore} blobStore
+   * @return {BPromise<Object>} a raw HashFile
+   */
+  store(blobStore) {
+    return this.data.store(blobStore).then(raw => {
+      storeRawMetadata(this.metadata, raw)
+      return raw
+    })
+  }
 }
 
-/**
- * Store the file's content in the blob store and return a raw file with
- * the corresponding hash. As a side effect, make this object consistent with
- * the hash.
- *
- * @param {BlobStore} blobStore
- * @return {BPromise<Object>} a raw HashFile
- */
-File.prototype.store = function (blobStore) {
-  return this.data.store(blobStore).then(raw => {
-    storeRawMetadata(this.metadata, raw)
-    return raw
-  })
+function storeRawMetadata(metadata, raw) {
+  if (!_.isEmpty(metadata)) {
+    raw.metadata = _.cloneDeep(metadata)
+  }
 }
 
-/**
- * Blob hash for an empty file.
- *
- * @type {String}
- */
-File.EMPTY_FILE_HASH = 'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391'
-
 module.exports = File

+ 0 - 1
libraries/overleaf-editor-core/lib/file_data/binary_file_data.js

@@ -8,7 +8,6 @@ const FileData = require('./')
 
 class BinaryFileData extends FileData {
   /**
-   * @constructor
    * @param {string} hash
    * @param {number} byteLength
    * @see FileData

+ 0 - 1
libraries/overleaf-editor-core/lib/file_data/hollow_binary_file_data.js

@@ -7,7 +7,6 @@ const FileData = require('./')
 
 class HollowBinaryFileData extends FileData {
   /**
-   * @constructor
    * @param {number} byteLength
    * @see FileData
    */

+ 0 - 1
libraries/overleaf-editor-core/lib/file_data/hollow_string_file_data.js

@@ -7,7 +7,6 @@ const FileData = require('./')
 
 class HollowStringFileData extends FileData {
   /**
-   * @constructor
    * @param {number} stringLength
    * @see FileData
    */

+ 0 - 1
libraries/overleaf-editor-core/lib/file_data/index.js

@@ -19,7 +19,6 @@ let StringFileData = null
  */
 
 /**
- * @classdesc
  * Helper to represent the content of a file. This class and its subclasses
  * should be used only through {@link File}.
  */

+ 0 - 1
libraries/overleaf-editor-core/lib/file_data/lazy_string_file_data.js

@@ -11,7 +11,6 @@ const TextOperation = require('../operation/text_operation')
 
 class LazyStringFileData extends FileData {
   /**
-   * @constructor
    * @param {string} hash
    * @param {number} stringLength
    * @param {Array.<TextOperation>} [textOperations]

+ 0 - 1
libraries/overleaf-editor-core/lib/file_data/string_file_data.js

@@ -11,7 +11,6 @@ const FileData = require('./')
 
 class StringFileData extends FileData {
   /**
-   * @constructor
    * @param {string} content
    */
   constructor(content) {

+ 39 - 37
libraries/overleaf-editor-core/lib/file_map.js

@@ -9,6 +9,36 @@ const OError = require('@overleaf/o-error')
 const File = require('./file')
 const safePathname = require('./safe_pathname')
 
+class PathnameError extends OError {}
+
+class NonUniquePathnameError extends PathnameError {
+  constructor(pathnames) {
+    super('pathnames are not unique: ' + pathnames, { pathnames })
+    this.pathnames = pathnames
+  }
+}
+
+class BadPathnameError extends PathnameError {
+  constructor(pathname) {
+    super(pathname + ' is not a valid pathname', { pathname })
+    this.pathname = pathname
+  }
+}
+
+class PathnameConflictError extends PathnameError {
+  constructor(pathname) {
+    super(`pathname '${pathname}' conflicts with another file`, { pathname })
+    this.pathname = pathname
+  }
+}
+
+class FileNotFoundError extends PathnameError {
+  constructor(pathname) {
+    super(`file ${pathname} does not exist`, { pathname })
+    this.pathname = pathname
+  }
+}
+
 /**
  * A set of {@link File}s. Several properties are enforced on the pathnames:
  *
@@ -26,10 +56,17 @@ const safePathname = require('./safe_pathname')
  * 3. No type conflicts: A pathname cannot refer to both a file and a directory
  * within the same snapshot. That is, you can't have pathnames `a` and `a/b` in
  * the same file map; {@see FileMap#wouldConflict}.
- *
- * @param {Object.<String, File>} files
  */
 class FileMap {
+  static PathnameError = PathnameError
+  static NonUniquePathnameError = NonUniquePathnameError
+  static BadPathnameError = BadPathnameError
+  static PathnameConflictError = PathnameConflictError
+  static FileNotFoundError = FileNotFoundError
+
+  /**
+   * @param {Object.<String, File>} files
+   */
   constructor(files) {
     // create bare object for use as Map
     // http://ryanmorr.com/true-hash-maps-in-javascript/
@@ -215,41 +252,6 @@ class FileMap {
   }
 }
 
-class PathnameError extends OError {}
-FileMap.PathnameError = PathnameError
-
-class NonUniquePathnameError extends PathnameError {
-  constructor(pathnames) {
-    super('pathnames are not unique: ' + pathnames, { pathnames })
-    this.pathnames = pathnames
-  }
-}
-FileMap.NonUniquePathnameError = NonUniquePathnameError
-
-class BadPathnameError extends PathnameError {
-  constructor(pathname) {
-    super(pathname + ' is not a valid pathname', { pathname })
-    this.pathname = pathname
-  }
-}
-FileMap.BadPathnameError = BadPathnameError
-
-class PathnameConflictError extends PathnameError {
-  constructor(pathname) {
-    super(`pathname '${pathname}' conflicts with another file`, { pathname })
-    this.pathname = pathname
-  }
-}
-FileMap.PathnameConflictError = PathnameConflictError
-
-class FileNotFoundError extends PathnameError {
-  constructor(pathname) {
-    super(`file ${pathname} does not exist`, { pathname })
-    this.pathname = pathname
-  }
-}
-FileMap.FileNotFoundError = FileNotFoundError
-
 function pathnamesEqual(pathname0, pathname1) {
   return pathname0 === pathname1
 }

+ 98 - 95
libraries/overleaf-editor-core/lib/history.js

@@ -10,116 +10,119 @@ const Snapshot = require('./snapshot')
  * @typedef {import("./types").BlobStore} BlobStore
  */
 
-/**
- * @constructor
- * @param {Snapshot} snapshot
- * @param {Array.<Change>} changes
- *
- * @classdesc
- * A History is a {@link Snapshot} and a sequence of {@link Change}s that can
- * be applied to produce a new snapshot.
- */
-function History(snapshot, changes) {
-  assert.instance(snapshot, Snapshot, 'bad snapshot')
-  assert.maybe.array.of.instance(changes, Change, 'bad changes')
+class History {
+  /**
+   * @constructor
+   * @param {Snapshot} snapshot
+   * @param {Array.<Change>} changes
+   *
+   * @classdesc
+   * A History is a {@link Snapshot} and a sequence of {@link Change}s that can
+   * be applied to produce a new snapshot.
+   */
+  constructor(snapshot, changes) {
+    assert.instance(snapshot, Snapshot, 'bad snapshot')
+    assert.maybe.array.of.instance(changes, Change, 'bad changes')
 
-  this.snapshot = snapshot
-  this.changes = changes || []
-}
+    this.snapshot = snapshot
+    this.changes = changes || []
+  }
 
-History.fromRaw = function historyFromRaw(raw) {
-  return new History(
-    Snapshot.fromRaw(raw.snapshot),
-    raw.changes.map(Change.fromRaw)
-  )
-}
+  static fromRaw(raw) {
+    return new History(
+      Snapshot.fromRaw(raw.snapshot),
+      raw.changes.map(Change.fromRaw)
+    )
+  }
 
-History.prototype.toRaw = function historyToRaw() {
-  function changeToRaw(change) {
-    return change.toRaw()
+  toRaw() {
+    function changeToRaw(change) {
+      return change.toRaw()
+    }
+    return {
+      snapshot: this.snapshot.toRaw(),
+      changes: this.changes.map(changeToRaw),
+    }
   }
-  return {
-    snapshot: this.snapshot.toRaw(),
-    changes: this.changes.map(changeToRaw),
+
+  getSnapshot() {
+    return this.snapshot
   }
-}
 
-History.prototype.getSnapshot = function () {
-  return this.snapshot
-}
-History.prototype.getChanges = function () {
-  return this.changes
-}
+  getChanges() {
+    return this.changes
+  }
 
-History.prototype.countChanges = function historyCountChanges() {
-  return this.changes.length
-}
+  countChanges() {
+    return this.changes.length
+  }
 
-/**
- * Add changes to this history.
- *
- * @param {Array.<Change>} changes
- */
-History.prototype.pushChanges = function historyPushChanges(changes) {
-  this.changes.push.apply(this.changes, changes)
-}
+  /**
+   * Add changes to this history.
+   *
+   * @param {Array.<Change>} changes
+   */
+  pushChanges(changes) {
+    this.changes.push.apply(this.changes, changes)
+  }
 
-/**
- * If this History references blob hashes, either in the Snapshot or the
- * Changes, add them to the given set.
- *
- * @param  {Set.<String>} blobHashes
- */
-History.prototype.findBlobHashes = function historyFindBlobHashes(blobHashes) {
-  function findChangeBlobHashes(change) {
-    change.findBlobHashes(blobHashes)
+  /**
+   * If this History references blob hashes, either in the Snapshot or the
+   * Changes, add them to the given set.
+   *
+   * @param  {Set.<String>} blobHashes
+   */
+  findBlobHashes(blobHashes) {
+    function findChangeBlobHashes(change) {
+      change.findBlobHashes(blobHashes)
+    }
+    this.snapshot.findBlobHashes(blobHashes)
+    this.changes.forEach(findChangeBlobHashes)
   }
-  this.snapshot.findBlobHashes(blobHashes)
-  this.changes.forEach(findChangeBlobHashes)
-}
 
-/**
- * If this History contains any File objects, load them.
- *
- * @param {string} kind see {File#load}
- * @param {BlobStore} blobStore
- * @return {Promise}
- */
-History.prototype.loadFiles = function historyLoadFiles(kind, blobStore) {
-  function loadChangeFiles(change) {
-    return change.loadFiles(kind, blobStore)
+  /**
+   * If this History contains any File objects, load them.
+   *
+   * @param {string} kind see {File#load}
+   * @param {BlobStore} blobStore
+   * @return {Promise}
+   */
+  loadFiles(kind, blobStore) {
+    function loadChangeFiles(change) {
+      return change.loadFiles(kind, blobStore)
+    }
+    return BPromise.join(
+      this.snapshot.loadFiles(kind, blobStore),
+      BPromise.each(this.changes, loadChangeFiles)
+    )
   }
-  return BPromise.join(
-    this.snapshot.loadFiles(kind, blobStore),
-    BPromise.each(this.changes, loadChangeFiles)
-  )
-}
 
-/**
- * Return a version of this history that is suitable for long term storage.
- * This requires that we store the content of file objects in the provided
- * blobStore.
- *
- * @param {BlobStore} blobStore
- * @param {number} [concurrency] applies separately to files, changes and
- *                               operations
- * @return {Promise.<Object>}
- */
-History.prototype.store = function historyStoreFunc(blobStore, concurrency) {
-  assert.maybe.number(concurrency, 'bad concurrency')
+  /**
+   * Return a version of this history that is suitable for long term storage.
+   * This requires that we store the content of file objects in the provided
+   * blobStore.
+   *
+   * @param {BlobStore} blobStore
+   * @param {number} [concurrency] applies separately to files, changes and
+   *                               operations
+   * @return {Promise.<Object>}
+   */
+  store(blobStore, concurrency) {
+    assert.maybe.number(concurrency, 'bad concurrency')
 
-  function storeChange(change) {
-    return change.store(blobStore, concurrency)
-  }
-  return BPromise.join(
-    this.snapshot.store(blobStore, concurrency),
-    BPromise.map(this.changes, storeChange, { concurrency: concurrency || 1 })
-  ).then(([rawSnapshot, rawChanges]) => {
-    return {
-      snapshot: rawSnapshot,
-      changes: rawChanges,
+    function storeChange(change) {
+      return change.store(blobStore, concurrency)
     }
-  })
+    return BPromise.join(
+      this.snapshot.store(blobStore, concurrency),
+      BPromise.map(this.changes, storeChange, { concurrency: concurrency || 1 })
+    ).then(([rawSnapshot, rawChanges]) => {
+      return {
+        snapshot: rawSnapshot,
+        changes: rawChanges,
+      }
+    })
+  }
 }
 
 module.exports = History

+ 69 - 60
libraries/overleaf-editor-core/lib/label.js

@@ -3,80 +3,89 @@
 const assert = require('check-types').assert
 
 /**
- * @constructor
- * @param {string} text
  * @classdesc
  * A user-configurable label that can be attached to a specific change. Labels
  * are not versioned, and they are not stored alongside the Changes in Chunks.
  * They are instead intended to provide external markers into the history of the
  * project.
  */
-function Label(text, authorId, timestamp, version) {
-  assert.string(text, 'bad text')
-  assert.maybe.integer(authorId, 'bad author id')
-  assert.date(timestamp, 'bad timestamp')
-  assert.integer(version, 'bad version')
+class Label {
+  /**
+   * @constructor
+   * @param {string} text
+   */
+  constructor(text, authorId, timestamp, version) {
+    assert.string(text, 'bad text')
+    assert.maybe.integer(authorId, 'bad author id')
+    assert.date(timestamp, 'bad timestamp')
+    assert.integer(version, 'bad version')
 
-  this.text = text
-  this.authorId = authorId
-  this.timestamp = timestamp
-  this.version = version
-}
+    this.text = text
+    this.authorId = authorId
+    this.timestamp = timestamp
+    this.version = version
+  }
 
-/**
- * Create a Label from its raw form.
- *
- * @param {Object} raw
- * @return {Label}
- */
-Label.fromRaw = function labelFromRaw(raw) {
-  return new Label(raw.text, raw.authorId, new Date(raw.timestamp), raw.version)
-}
+  /**
+   * Create a Label from its raw form.
+   *
+   * @param {Object} raw
+   * @return {Label}
+   */
+  static fromRaw(raw) {
+    return new Label(
+      raw.text,
+      raw.authorId,
+      new Date(raw.timestamp),
+      raw.version
+    )
+  }
 
-/**
- * Convert the Label to raw form for transmission.
- *
- * @return {Object}
- */
-Label.prototype.toRaw = function labelToRaw() {
-  return {
-    text: this.text,
-    authorId: this.authorId,
-    timestamp: this.timestamp.toISOString(),
-    version: this.version,
+  /**
+   * Convert the Label to raw form for transmission.
+   *
+   * @return {Object}
+   */
+  toRaw() {
+    return {
+      text: this.text,
+      authorId: this.authorId,
+      timestamp: this.timestamp.toISOString(),
+      version: this.version,
+    }
   }
-}
 
-/**
- * @return {string}
- */
-Label.prototype.getText = function () {
-  return this.text
-}
+  /**
+   * @return {string}
+   */
+  getText() {
+    return this.text
+  }
 
-/**
- * The ID of the author, if any. Note that we now require all saved versions to
- * have an author, but this was not always the case, so we have to allow nulls
- * here for historical reasons.
- *
- * @return {number | null | undefined}
- */
-Label.prototype.getAuthorId = function () {
-  return this.authorId
-}
+  /**
+   * The ID of the author, if any. Note that we now require all saved versions to
+   * have an author, but this was not always the case, so we have to allow nulls
+   * here for historical reasons.
+   *
+   * @return {number | null | undefined}
+   */
+  getAuthorId() {
+    return this.authorId
+  }
 
-/**
- * @return {Date}
- */
-Label.prototype.getTimestamp = function () {
-  return this.timestamp
-}
+  /**
+   * @return {Date}
+   */
+  getTimestamp() {
+    return this.timestamp
+  }
 
-/**
- * @return {number | undefined}
- */
-Label.prototype.getVersion = function () {
-  return this.version
+  /**
+   * @return {number | undefined}
+   */
+  getVersion() {
+    return this.version
+  }
 }
 
 module.exports = Label

+ 0 - 2
libraries/overleaf-editor-core/lib/operation/add_file_operation.js

@@ -6,12 +6,10 @@ const File = require('../file')
 const Operation = require('./')
 
 /**
- * @classdesc
  * Adds a new file to a project.
  */
 class AddFileOperation extends Operation {
   /**
-   * @constructor
    * @param {string} pathname
    * @param {File} file
    */

+ 0 - 2
libraries/overleaf-editor-core/lib/operation/edit_file_operation.js

@@ -4,12 +4,10 @@ const Operation = require('./')
 const TextOperation = require('./text_operation')
 
 /**
- * @classdesc
  * Edit a file in place. It is a wrapper around a single TextOperation.
  */
 class EditFileOperation extends Operation {
   /**
-   * @constructor
    * @param {string} pathname
    * @param {TextOperation} textOperation
    */

+ 0 - 1
libraries/overleaf-editor-core/lib/operation/index.js

@@ -20,7 +20,6 @@ let SetFileMetadataOperation = null
  */
 
 /**
- * @classdesc
  * An `Operation` changes a `Snapshot` when it is applied. See the
  * {@tutorial OT} tutorial for background.
  */

+ 1 - 1
libraries/overleaf-editor-core/lib/operation/move_file_operation.js

@@ -3,7 +3,6 @@
 const Operation = require('./')
 
 /**
- * @classdesc
  * Moves or removes a file from a project.
  */
 class MoveFileOperation extends Operation {
@@ -51,4 +50,5 @@ class MoveFileOperation extends Operation {
     snapshot.moveFile(this.getPathname(), this.getNewPathname())
   }
 }
+
 module.exports = MoveFileOperation

+ 0 - 1
libraries/overleaf-editor-core/lib/operation/no_operation.js

@@ -3,7 +3,6 @@
 const Operation = require('./')
 
 /**
- * @classdesc
  * An explicit no-operation.
  *
  * There are several no-ops, such as moving a file to itself, but it's useful

+ 0 - 2
libraries/overleaf-editor-core/lib/operation/set_file_metadata_operation.js

@@ -6,12 +6,10 @@ const assert = require('check-types').assert
 const Operation = require('./')
 
 /**
- * @classdesc
  * Moves or removes a file from a project.
  */
 class SetFileMetadataOperation extends Operation {
   /**
-   * @constructor
    * @param {string} pathname
    * @param {Object} metadata
    */

Разница между файлами не показана из-за своего большого размера
+ 590 - 438
libraries/overleaf-editor-core/lib/operation/text_operation.js


+ 37 - 35
libraries/overleaf-editor-core/lib/origin/index.js

@@ -7,46 +7,48 @@ const assert = require('check-types').assert
 let RestoreOrigin = null
 
 /**
- * @constructor
- * @param {string} kind
- * @classdesc
  * An Origin records where a {@link Change} came from. The Origin class handles
  * simple tag origins, like "it came from rich text mode", or "it came from
  * uploading files". Its subclasses record more detailed data for Changes such
  * as restoring a version.
  */
-function Origin(kind) {
-  assert.string(kind, 'Origin: bad kind')
-
-  this.kind = kind
-}
-
-/**
- * Create an Origin from its raw form.
- *
- * @param {Object} [raw]
- * @return {Origin | null}
- */
-Origin.fromRaw = function originFromRaw(raw) {
-  if (!raw) return null
-  if (raw.kind === RestoreOrigin.KIND) return RestoreOrigin.fromRaw(raw)
-  return new Origin(raw.kind)
-}
-
-/**
- * Convert the Origin to raw form for storage or transmission.
- *
- * @return {Object}
- */
-Origin.prototype.toRaw = function originToRaw() {
-  return { kind: this.kind }
-}
-
-/**
- * @return {string}
- */
-Origin.prototype.getKind = function () {
-  return this.kind
+class Origin {
+  /**
+   * @param {string} kind
+   */
+  constructor(kind) {
+    assert.string(kind, 'Origin: bad kind')
+
+    this.kind = kind
+  }
+
+  /**
+   * Create an Origin from its raw form.
+   *
+   * @param {Object} [raw]
+   * @return {Origin | null}
+   */
+  static fromRaw(raw) {
+    if (!raw) return null
+    if (raw.kind === RestoreOrigin.KIND) return RestoreOrigin.fromRaw(raw)
+    return new Origin(raw.kind)
+  }
+
+  /**
+   * Convert the Origin to raw form for storage or transmission.
+   *
+   * @return {Object}
+   */
+  toRaw() {
+    return { kind: this.kind }
+  }
+
+  /**
+   * @return {string}
+   */
+  getKind() {
+    return this.kind
+  }
 }
 
 module.exports = Origin

+ 0 - 2
libraries/overleaf-editor-core/lib/origin/restore_origin.js

@@ -5,7 +5,6 @@ const assert = require('check-types').assert
 const Origin = require('./')
 
 /**
- * @classdesc
  * When a {@link Change} is generated by restoring a previous version, this
  * records the original version. We also store the timestamp of the restored
  * version for display; technically, this is redundant, because we could
@@ -18,7 +17,6 @@ const Origin = require('./')
  */
 class RestoreOrigin extends Origin {
   /**
-   * @constructor
    * @param {number} version that was restored
    * @param {Date} timestamp from the restored version
    */

+ 195 - 192
libraries/overleaf-editor-core/lib/ot_client.js

@@ -9,229 +9,232 @@ const Chunk = require('./chunk')
 const Operation = require('./operation')
 
 /**
- * @class
- * @classdesc
  * Operational Transformation client.
  *
  * See OT.md for explanation.
  */
-function OtClient(_projectId, _editor, _blobStore, _socket) {
-  const STATE_DISCONNECTED = 0
-  const STATE_LOADING = 1
-  const STATE_READY = 2
-  const STATE_WAITING = 3
-
-  let _version = null
-  let _state = STATE_DISCONNECTED
-  const _buffer = []
-  let _ackVersion = null
-  let _outstanding = []
-  let _pending = []
-  const _waiting = []
-
-  this.connect = function otClientConnect() {
-    switch (_state) {
-      case STATE_DISCONNECTED:
-        _state = STATE_LOADING
-        _socket.emit('authenticate', {
-          projectId: _projectId,
-          token: 'letmein',
-        })
-        break
-      default:
-        throw new Error('connect in state ' + _state)
+class OtClient {
+  constructor(_projectId, _editor, _blobStore, _socket) {
+    const STATE_DISCONNECTED = 0
+    const STATE_LOADING = 1
+    const STATE_READY = 2
+    const STATE_WAITING = 3
+
+    let _version = null
+    let _state = STATE_DISCONNECTED
+    const _buffer = []
+    let _ackVersion = null
+    let _outstanding = []
+    let _pending = []
+    const _waiting = []
+
+    this.connect = function otClientConnect() {
+      switch (_state) {
+        case STATE_DISCONNECTED:
+          _state = STATE_LOADING
+          _socket.emit('authenticate', {
+            projectId: _projectId,
+            token: 'letmein',
+          })
+          break
+        default:
+          throw new Error('connect in state ' + _state)
+      }
     }
-  }
-
-  /**
-   * The latest project version number for which the client can construct the
-   * project content.
-   *
-   * @return {number} non-negative
-   */
-  this.getVersion = function () {
-    return _version
-  }
 
-  _socket.on('load', function otClientOnLoad(data) {
-    switch (_state) {
-      case STATE_LOADING: {
-        const chunk = Chunk.fromRaw(data)
-        const snapshot = chunk.getSnapshot()
-        snapshot.applyAll(chunk.getChanges(), { strict: true })
-        _version = chunk.getEndVersion()
-        // TODO: we can get remote changes here, so it's not correct to wait for
-        // the editor to load before transitioning to the READY state
-        _editor.load(snapshot).then(function () {
-          _state = STATE_READY
-        })
-        break
-      }
-      default:
-        throw new Error('loaded in state ' + _state)
+    /**
+     * The latest project version number for which the client can construct the
+     * project content.
+     *
+     * @return {number} non-negative
+     */
+    this.getVersion = function () {
+      return _version
     }
-  })
 
-  //
-  // Local Operations
-  //
+    _socket.on('load', function otClientOnLoad(data) {
+      switch (_state) {
+        case STATE_LOADING: {
+          const chunk = Chunk.fromRaw(data)
+          const snapshot = chunk.getSnapshot()
+          snapshot.applyAll(chunk.getChanges(), { strict: true })
+          _version = chunk.getEndVersion()
+          // TODO: we can get remote changes here, so it's not correct to wait for
+          // the editor to load before transitioning to the READY state
+          _editor.load(snapshot).then(function () {
+            _state = STATE_READY
+          })
+          break
+        }
+        default:
+          throw new Error('loaded in state ' + _state)
+      }
+    })
 
-  function sendOutstandingChange() {
-    const changeRequest = new ChangeRequest(_version, _outstanding)
-    _socket.emit('change', changeRequest.toRaw())
-    _state = STATE_WAITING
-  }
+    //
+    // Local Operations
+    //
 
-  function sendLocalOperation(operation) {
-    _outstanding.push(operation)
-    sendOutstandingChange()
-  }
+    function sendOutstandingChange() {
+      const changeRequest = new ChangeRequest(_version, _outstanding)
+      _socket.emit('change', changeRequest.toRaw())
+      _state = STATE_WAITING
+    }
 
-  function queueLocalOperation(operation) {
-    _pending.push(operation)
-  }
+    function sendLocalOperation(operation) {
+      _outstanding.push(operation)
+      sendOutstandingChange()
+    }
 
-  this.handleLocalOperation = function otClientHandleLocalOperation(operation) {
-    switch (_state) {
-      case STATE_READY:
-        sendLocalOperation(operation)
-        break
-      case STATE_WAITING:
-        queueLocalOperation(operation)
-        break
-      default:
-        throw new Error('local operation in state ' + _state)
+    function queueLocalOperation(operation) {
+      _pending.push(operation)
     }
-  }
 
-  /**
-   * A promise that resolves when the project reaches the given version.
-   *
-   * @param {number} version non-negative
-   * @return {Promise}
-   */
-  this.waitForVersion = function otClientWaitForVersion(version) {
-    if (!_waiting[version]) _waiting[version] = []
-    return new BPromise(function (resolve, reject) {
-      _waiting[version].push(resolve)
-    })
-  }
+    this.handleLocalOperation = function otClientHandleLocalOperation(
+      operation
+    ) {
+      switch (_state) {
+        case STATE_READY:
+          sendLocalOperation(operation)
+          break
+        case STATE_WAITING:
+          queueLocalOperation(operation)
+          break
+        default:
+          throw new Error('local operation in state ' + _state)
+      }
+    }
 
-  function resolveWaitingPromises() {
-    for (const version in _waiting) {
-      if (!Object.prototype.hasOwnProperty.call(_waiting, version)) continue
-      if (version > _version) continue
-      _waiting[version].forEach(function (resolve) {
-        resolve()
+    /**
+     * A promise that resolves when the project reaches the given version.
+     *
+     * @param {number} version non-negative
+     * @return {Promise}
+     */
+    this.waitForVersion = function otClientWaitForVersion(version) {
+      if (!_waiting[version]) _waiting[version] = []
+      return new BPromise(function (resolve, reject) {
+        _waiting[version].push(resolve)
       })
-      delete _waiting[version]
     }
-  }
 
-  //
-  // Messages from Server
-  //
-
-  function advanceIfReady() {
-    if (_ackVersion !== null && _version === _ackVersion) {
-      _version += 1
-      _ackVersion = null
-      handleAckReady()
-      advanceIfReady()
-      return
-    }
-    const changeNotes = _.remove(_buffer, function (changeNote) {
-      return changeNote.getBaseVersion() === _version
-    })
-    if (changeNotes.length === 1) {
-      handleRemoteChangeReady(changeNotes[0].getChange())
-      _version += 1
-      advanceIfReady()
-      return
-    }
-    if (changeNotes.length !== 0) {
-      throw new Error('multiple remote changes in client version ' + _version)
+    function resolveWaitingPromises() {
+      for (const version in _waiting) {
+        if (!Object.prototype.hasOwnProperty.call(_waiting, version)) continue
+        if (version > _version) continue
+        _waiting[version].forEach(function (resolve) {
+          resolve()
+        })
+        delete _waiting[version]
+      }
     }
-  }
 
-  function bufferRemoteChangeNote(changeNote) {
-    const version = changeNote.getBaseVersion()
-    if (_.find(_buffer, 'baseVersion', version)) {
-      throw new Error('multiple changes in version ' + version)
-    }
-    if (version === _ackVersion) {
-      throw new Error('received change that was acked in ' + _ackVersion)
-    }
-    _buffer.push(changeNote)
-  }
+    //
+    // Messages from Server
+    //
 
-  function handleAckReady() {
-    // console.log('handleAckReady')
-    if (_outstanding.length === 0) {
-      throw new Error('ack complete without outstanding change')
-    }
-    if (_state !== STATE_WAITING) {
-      throw new Error('ack complete in state ' + _state)
+    function advanceIfReady() {
+      if (_ackVersion !== null && _version === _ackVersion) {
+        _version += 1
+        _ackVersion = null
+        handleAckReady()
+        advanceIfReady()
+        return
+      }
+      const changeNotes = _.remove(_buffer, function (changeNote) {
+        return changeNote.getBaseVersion() === _version
+      })
+      if (changeNotes.length === 1) {
+        handleRemoteChangeReady(changeNotes[0].getChange())
+        _version += 1
+        advanceIfReady()
+        return
+      }
+      if (changeNotes.length !== 0) {
+        throw new Error('multiple remote changes in client version ' + _version)
+      }
     }
-    _editor.handleChangeAcknowledged()
-    resolveWaitingPromises()
-    if (_pending.length > 0) {
-      _outstanding = _pending
-      _pending = []
-      sendOutstandingChange()
-    } else {
-      _outstanding = []
-      _state = STATE_READY
+
+    function bufferRemoteChangeNote(changeNote) {
+      const version = changeNote.getBaseVersion()
+      if (_.find(_buffer, 'baseVersion', version)) {
+        throw new Error('multiple changes in version ' + version)
+      }
+      if (version === _ackVersion) {
+        throw new Error('received change that was acked in ' + _ackVersion)
+      }
+      _buffer.push(changeNote)
     }
-  }
 
-  function handleRemoteChangeReady(change) {
-    if (_pending.length > 0) {
+    function handleAckReady() {
+      // console.log('handleAckReady')
       if (_outstanding.length === 0) {
-        throw new Error('pending change without outstanding change')
+        throw new Error('ack complete without outstanding change')
+      }
+      if (_state !== STATE_WAITING) {
+        throw new Error('ack complete in state ' + _state)
+      }
+      _editor.handleChangeAcknowledged()
+      resolveWaitingPromises()
+      if (_pending.length > 0) {
+        _outstanding = _pending
+        _pending = []
+        sendOutstandingChange()
+      } else {
+        _outstanding = []
+        _state = STATE_READY
       }
     }
 
-    Operation.transformMultiple(_outstanding, change.getOperations())
-    Operation.transformMultiple(_pending, change.getOperations())
+    function handleRemoteChangeReady(change) {
+      if (_pending.length > 0) {
+        if (_outstanding.length === 0) {
+          throw new Error('pending change without outstanding change')
+        }
+      }
+
+      Operation.transformMultiple(_outstanding, change.getOperations())
+      Operation.transformMultiple(_pending, change.getOperations())
 
-    _editor.applyRemoteChange(change)
-  }
+      _editor.applyRemoteChange(change)
+    }
 
-  _socket.on('ack', function otClientOnAck(data) {
-    switch (_state) {
-      case STATE_WAITING: {
-        const changeNote = ChangeNote.fromRaw(data)
-        _ackVersion = changeNote.getBaseVersion()
-        advanceIfReady()
-        break
+    _socket.on('ack', function otClientOnAck(data) {
+      switch (_state) {
+        case STATE_WAITING: {
+          const changeNote = ChangeNote.fromRaw(data)
+          _ackVersion = changeNote.getBaseVersion()
+          advanceIfReady()
+          break
+        }
+        default:
+          throw new Error('ack in state ' + _state)
       }
-      default:
-        throw new Error('ack in state ' + _state)
-    }
-  })
+    })
 
-  _socket.on('change', function otClientOnChange(data) {
-    switch (_state) {
-      case STATE_READY:
-      case STATE_WAITING:
-        bufferRemoteChangeNote(ChangeNote.fromRaw(data))
-        advanceIfReady()
-        break
-      default:
-        throw new Error('remote change in state ' + _state)
-    }
-  })
-
-  //
-  // Connection State
-  // TODO: socket.io error handling
-  //
-
-  _socket.on('disconnect', function () {
-    _state = STATE_DISCONNECTED
-    // eslint-disable-next-line no-console
-    console.log('disconnected') // TODO: how do we handle disconnect?
-  })
+    _socket.on('change', function otClientOnChange(data) {
+      switch (_state) {
+        case STATE_READY:
+        case STATE_WAITING:
+          bufferRemoteChangeNote(ChangeNote.fromRaw(data))
+          advanceIfReady()
+          break
+        default:
+          throw new Error('remote change in state ' + _state)
+      }
+    })
+
+    //
+    // Connection State
+    // TODO: socket.io error handling
+    //
+
+    _socket.on('disconnect', function () {
+      _state = STATE_DISCONNECTED
+      // eslint-disable-next-line no-console
+      console.log('disconnected') // TODO: how do we handle disconnect?
+    })
+  }
 }
+
 module.exports = OtClient

+ 8 - 9
libraries/overleaf-editor-core/lib/snapshot.js

@@ -13,11 +13,17 @@ const V2DocVersions = require('./v2_doc_versions')
  * @typedef {import("./operation/text_operation")} TextOperation
  */
 
+class EditMissingFileError extends OError {}
+
 /**
- * @classdesc A Snapshot represents the state of a {@link Project} at a
- *     particular version.
+ * A Snapshot represents the state of a {@link Project} at a
+ * particular version.
  */
 class Snapshot {
+  static PROJECT_VERSION_RX_STRING = '^[0-9]+\\.[0-9]+$'
+  static PROJECT_VERSION_RX = new RegExp(Snapshot.PROJECT_VERSION_RX_STRING)
+  static EditMissingFileError = EditMissingFileError
+
   static fromRaw(raw) {
     assert.object(raw.files, 'bad raw.files')
     return new Snapshot(
@@ -37,7 +43,6 @@ class Snapshot {
   }
 
   /**
-   * @constructor
    * @param {FileMap} [fileMap]
    * @param {string} [projectVersion]
    * @param {V2DocVersions} [v2DocVersions]
@@ -231,10 +236,4 @@ class Snapshot {
   }
 }
 
-class EditMissingFileError extends OError {}
-Snapshot.EditMissingFileError = EditMissingFileError
-
-Snapshot.PROJECT_VERSION_RX_STRING = '^[0-9]+\\.[0-9]+$'
-Snapshot.PROJECT_VERSION_RX = new RegExp(Snapshot.PROJECT_VERSION_RX_STRING)
-
 module.exports = Snapshot

+ 38 - 38
libraries/overleaf-editor-core/lib/v2_doc_versions.js

@@ -7,48 +7,48 @@ const _ = require('lodash')
  * @typedef {import("./types").RawV2DocVersions} RawV2DocVersions
  */
 
-/**
- * @constructor
- * @param {RawV2DocVersions} data
- * @classdesc
- */
-function V2DocVersions(data) {
-  this.data = data || {}
-}
-
-V2DocVersions.fromRaw = function v2DocVersionsFromRaw(raw) {
-  if (!raw) return undefined
-  return new V2DocVersions(raw)
-}
+class V2DocVersions {
+  /**
+   * @param {RawV2DocVersions} data
+   */
+  constructor(data) {
+    this.data = data || {}
+  }
 
-/**
- * @abstract
- */
-V2DocVersions.prototype.toRaw = function () {
-  if (!this.data) return null
-  const raw = _.clone(this.data)
-  return raw
-}
+  static fromRaw(raw) {
+    if (!raw) return undefined
+    return new V2DocVersions(raw)
+  }
 
-/**
- * Clone this object.
- *
- * @return {V2DocVersions} a new object of the same type
- */
-V2DocVersions.prototype.clone = function v2DocVersionsClone() {
-  return V2DocVersions.fromRaw(this.toRaw())
-}
+  /**
+   * @abstract
+   */
+  toRaw() {
+    if (!this.data) return null
+    const raw = _.clone(this.data)
+    return raw
+  }
 
-V2DocVersions.prototype.applyTo = function v2DocVersionsApplyTo(snapshot) {
-  // Only update the snapshot versions if we have new versions
-  if (!_.size(this.data)) return
+  /**
+   * Clone this object.
+   *
+   * @return {V2DocVersions} a new object of the same type
+   */
+  clone() {
+    return V2DocVersions.fromRaw(this.toRaw())
+  }
 
-  // Create v2DocVersions in snapshot if it does not exist
-  // otherwise update snapshot v2docversions
-  if (!snapshot.v2DocVersions) {
-    snapshot.v2DocVersions = this.clone()
-  } else {
-    _.assign(snapshot.v2DocVersions.data, this.data)
+  applyTo(snapshot) {
+    // Only update the snapshot versions if we have new versions
+    if (!_.size(this.data)) return
+
+    // Create v2DocVersions in snapshot if it does not exist
+    // otherwise update snapshot v2docversions
+    if (!snapshot.v2DocVersions) {
+      snapshot.v2DocVersions = this.clone()
+    } else {
+      _.assign(snapshot.v2DocVersions.data, this.data)
+    }
   }
 }
 

+ 1 - 1
libraries/overleaf-editor-core/tsconfig.json

@@ -4,7 +4,7 @@
     "allowSyntheticDefaultImports": true,
     "checkJs": true,
     "esModuleInterop": true,
-    "lib": ["es2018"],
+    "lib": ["es2022"],
     "module": "commonjs",
     "noEmit": true,
     "resolveJsonModule": true,

Некоторые файлы не были показаны из-за большого количества измененных файлов