Bladeren bron

Remove o-error update script (#15070)

GitOrigin-RevId: dfa0c582453eadc6834056a7c8a0832ddb878b70
Alf Eaton 3 jaren geleden
bovenliggende
commit
c14359df74
76 gewijzigde bestanden met toevoegingen van 0 en 17571 verwijderingen
  1. 0 49
      libraries/o-error/doc/update-readme.js
  2. 0 2
      libraries/o-error/package.json
  3. 0 737
      package-lock.json
  4. 0 1
      services/track-changes/.eslintignore
  5. 0 38
      services/track-changes/.github/ISSUE_TEMPLATE.md
  6. 0 48
      services/track-changes/.github/PULL_REQUEST_TEMPLATE.md
  7. 0 7
      services/track-changes/.gitignore
  8. 0 3
      services/track-changes/.mocharc.json
  9. 0 1
      services/track-changes/.nvmrc
  10. 0 27
      services/track-changes/Dockerfile
  11. 0 662
      services/track-changes/LICENSE
  12. 0 110
      services/track-changes/Makefile
  13. 0 20
      services/track-changes/README.md
  14. 0 164
      services/track-changes/app.js
  15. 0 343
      services/track-changes/app/js/DiffGenerator.js
  16. 0 184
      services/track-changes/app/js/DiffManager.js
  17. 0 42
      services/track-changes/app/js/DocIterator.js
  18. 0 52
      services/track-changes/app/js/DocstoreManager.js
  19. 0 128
      services/track-changes/app/js/DocumentUpdaterManager.js
  20. 0 5
      services/track-changes/app/js/Errors.js
  21. 0 84
      services/track-changes/app/js/HealthChecker.js
  22. 0 364
      services/track-changes/app/js/HttpController.js
  23. 0 161
      services/track-changes/app/js/LockManager.js
  24. 0 197
      services/track-changes/app/js/MongoAWS.js
  25. 0 200
      services/track-changes/app/js/MongoManager.js
  26. 0 1202
      services/track-changes/app/js/PackManager.js
  27. 0 211
      services/track-changes/app/js/PackWorker.js
  28. 0 113
      services/track-changes/app/js/ProjectIterator.js
  29. 0 169
      services/track-changes/app/js/RedisManager.js
  30. 0 46
      services/track-changes/app/js/RestoreManager.js
  31. 0 326
      services/track-changes/app/js/UpdateCompressor.js
  32. 0 66
      services/track-changes/app/js/UpdateTrimmer.js
  33. 0 883
      services/track-changes/app/js/UpdatesManager.js
  34. 0 112
      services/track-changes/app/js/WebApiManager.js
  35. 0 203
      services/track-changes/app/js/ZipManager.js
  36. 0 42
      services/track-changes/app/js/mongodb.js
  37. 0 18
      services/track-changes/app/js/util/PackUtils.js
  38. 0 2340
      services/track-changes/app/lib/diff_match_patch.js
  39. 0 9
      services/track-changes/buildscript.txt
  40. 0 90
      services/track-changes/config/settings.defaults.js
  41. 0 74
      services/track-changes/docker-compose.ci.yml
  42. 0 77
      services/track-changes/docker-compose.yml
  43. 0 24
      services/track-changes/pack.sh
  44. 0 54
      services/track-changes/package.json
  45. 0 27
      services/track-changes/scripts/flush_all.js
  46. 0 4
      services/track-changes/test/acceptance/deps/Dockerfile.s3mock
  47. 0 9
      services/track-changes/test/acceptance/deps/healthcheck.sh
  48. 0 587
      services/track-changes/test/acceptance/js/AppendingUpdatesTests.js
  49. 0 319
      services/track-changes/test/acceptance/js/ArchivingUpdatesTests.js
  50. 0 34
      services/track-changes/test/acceptance/js/ExportProjectTests.js
  51. 0 277
      services/track-changes/test/acceptance/js/FlushingUpdatesTests.js
  52. 0 127
      services/track-changes/test/acceptance/js/GettingADiffTests.js
  53. 0 185
      services/track-changes/test/acceptance/js/GettingUpdatesTests.js
  54. 0 64
      services/track-changes/test/acceptance/js/LockManagerTests.js
  55. 0 116
      services/track-changes/test/acceptance/js/RestoringVersions.js
  56. 0 50
      services/track-changes/test/acceptance/js/helpers/MockDocStoreApi.js
  57. 0 87
      services/track-changes/test/acceptance/js/helpers/MockDocUpdaterApi.js
  58. 0 72
      services/track-changes/test/acceptance/js/helpers/MockWebApi.js
  59. 0 64
      services/track-changes/test/acceptance/js/helpers/TrackChangesApp.js
  60. 0 302
      services/track-changes/test/acceptance/js/helpers/TrackChangesClient.js
  61. 0 21
      services/track-changes/test/setup.js
  62. 0 456
      services/track-changes/test/unit/js/DiffGenerator/DiffGeneratorTests.js
  63. 0 443
      services/track-changes/test/unit/js/DiffManager/DiffManagerTests.js
  64. 0 110
      services/track-changes/test/unit/js/DocArchive/MongoAWS.js
  65. 0 218
      services/track-changes/test/unit/js/DocumentUpdaterManager/DocumentUpdaterManagerTests.js
  66. 0 200
      services/track-changes/test/unit/js/HttpController/HttpControllerTests.js
  67. 0 315
      services/track-changes/test/unit/js/LockManager/LockManagerTests.js
  68. 0 239
      services/track-changes/test/unit/js/MongoManager/MongoManagerTests.js
  69. 0 704
      services/track-changes/test/unit/js/PackManager/PackManagerTests.js
  70. 0 163
      services/track-changes/test/unit/js/RedisManager/RedisManagerTests.js
  71. 0 64
      services/track-changes/test/unit/js/RestoreManager/RestoreManagerTests.js
  72. 0 848
      services/track-changes/test/unit/js/UpdateCompressor/UpdateCompressorTests.js
  73. 0 182
      services/track-changes/test/unit/js/UpdateTrimmer/UpdateTrimmerTests.js
  74. 0 1333
      services/track-changes/test/unit/js/UpdatesManager/UpdatesManagerTests.js
  75. 0 55
      services/track-changes/test/unit/js/Util/PackUtilsTests.js
  76. 0 208
      services/track-changes/test/unit/js/WebApiManager/WebApiManagerTests.js

+ 0 - 49
libraries/o-error/doc/update-readme.js

@@ -1,49 +0,0 @@
-#!/usr/bin/env node
-
-const fs = require('fs')
-const jsdoc2md = require('jsdoc-to-markdown')
-const toc = require('markdown-toc')
-
-const README = 'README.md'
-const HEADER = '## OError API Reference'
-const FOOTER = '<!-- END API REFERENCE -->'
-
-async function main() {
-  const apiDocs = await jsdoc2md.render({ files: 'index.js' })
-  const apiDocLines = apiDocs.trim().split(/\r?\n/g)
-
-  // The first few lines don't make much sense when included in the README.
-  const apiDocStart = apiDocLines.indexOf('* [OError](#OError)')
-  if (apiDocStart === -1) {
-    console.error('API docs not in expected format for insertion.')
-    process.exit(1)
-  }
-  apiDocLines.splice(1, apiDocStart - 1)
-  apiDocLines.unshift(HEADER, '')
-
-  const readme = await fs.promises.readFile(README, { encoding: 'utf8' })
-  const readmeLines = readme.split(/\r?\n/g)
-
-  const apiStart = readmeLines.indexOf(HEADER)
-  const apiEnd = readmeLines.indexOf(FOOTER)
-
-  if (apiStart === -1 || apiEnd === -1) {
-    console.error('Could not find the API Reference section.')
-    process.exit(1)
-  }
-
-  Array.prototype.splice.apply(
-    readmeLines,
-    [apiStart, apiEnd - apiStart].concat(apiDocLines)
-  )
-
-  const readmeWithApi = readmeLines.join('\n')
-
-  let readmeWithApiAndToc = toc.insert(readmeWithApi)
-
-  // Unfortunately, the ⇒ breaks the generated TOC links.
-  readmeWithApiAndToc = readmeWithApiAndToc.replace(/-%E2%87%92-/g, '--')
-
-  await fs.promises.writeFile(README, readmeWithApiAndToc)
-}
-main()

+ 0 - 2
libraries/o-error/package.json

@@ -39,8 +39,6 @@
     "@types/chai": "^4.3.0",
     "@types/node": "^18.17.4",
     "chai": "^4.3.6",
-    "jsdoc-to-markdown": "^7.1.0",
-    "markdown-toc": "^1.2.0",
     "mocha": "^10.2.0",
     "nyc": "^15.1.0",
     "typescript": "^5.0.4"

File diff suppressed because it is too large
+ 0 - 737
package-lock.json


+ 0 - 1
services/track-changes/.eslintignore

@@ -1 +0,0 @@
-app/lib/diff_match_patch.js

+ 0 - 38
services/track-changes/.github/ISSUE_TEMPLATE.md

@@ -1,38 +0,0 @@
-<!-- BUG REPORT TEMPLATE -->
-
-## Steps to Reproduce
-<!-- Describe the steps leading up to when / where you found the bug. -->
-<!-- Screenshots may be helpful here. -->
-
-1.
-2.
-3.
-
-## Expected Behaviour
-<!-- What should have happened when you completed the steps above? -->
-
-## Observed Behaviour
-<!-- What actually happened when you completed the steps above? -->
-<!-- Screenshots may be helpful here. -->
-
-## Context
-<!-- How has this issue affected you? What were you trying to accomplish? -->
-
-## Technical Info
-<!-- Provide any technical details that may be applicable (or N/A if not applicable). -->
-
-* URL:
-* Browser Name and version:
-* Operating System and version (desktop or mobile):
-* Signed in as:
-* Project and/or file:
-
-## Analysis
-<!--- Optionally, document investigation of / suggest a fix for the bug, e.g. 'comes from this line / commit' -->
-
-## Who Needs to Know?
-<!-- If you want to bring this to the attention of particular people, @-mention them below. -->
-<!-- If a user reported this bug and should be notified when it is fixed, provide the Front conversation link. -->
-
--
--

+ 0 - 48
services/track-changes/.github/PULL_REQUEST_TEMPLATE.md

@@ -1,48 +0,0 @@
-
-<!-- ** This is an Overleaf public repository ** -->
-
-<!-- Please review https://github.com/overleaf/overleaf/blob/master/CONTRIBUTING.md for guidance on what is expected of a contribution. -->
-
-### Description
-
-
-
-#### Screenshots
-
-
-
-#### Related Issues / PRs
-
-
-
-### Review
-
-
-
-#### Potential Impact
-
-
-
-#### Manual Testing Performed
-
-- [ ]
-- [ ]
-
-#### Accessibility
-
-
-
-### Deployment
-
-
-
-#### Deployment Checklist
-
-- [ ] Update documentation not included in the PR (if any)
-- [ ]
-
-#### Metrics and Monitoring
-
-
-
-#### Who Needs to Know?

+ 0 - 7
services/track-changes/.gitignore

@@ -1,7 +0,0 @@
-**.swp
-node_modules/
-forever/
-*.js.map
-
-# managed by dev-environment$ bin/update_build_scripts
-.npmrc

+ 0 - 3
services/track-changes/.mocharc.json

@@ -1,3 +0,0 @@
-{
-  "require": "test/setup.js"
-}

+ 0 - 1
services/track-changes/.nvmrc

@@ -1 +0,0 @@
-18.18.0

+ 0 - 27
services/track-changes/Dockerfile

@@ -1,27 +0,0 @@
-# This file was auto-generated, do not edit it directly.
-# Instead run bin/update_build_scripts from
-# https://github.com/overleaf/internal/
-
-FROM node:18.18.0 as base
-
-WORKDIR /overleaf/services/track-changes
-
-# Google Cloud Storage needs a writable $HOME/.config for resumable uploads
-# (see https://googleapis.dev/nodejs/storage/latest/File.html#createWriteStream)
-RUN mkdir /home/node/.config && chown node:node /home/node/.config
-
-FROM base as app
-
-COPY package.json package-lock.json /overleaf/
-COPY services/track-changes/package.json /overleaf/services/track-changes/
-COPY libraries/ /overleaf/libraries/
-COPY patches/ /overleaf/patches/
-
-RUN cd /overleaf && npm ci --quiet
-
-COPY services/track-changes/ /overleaf/services/track-changes/
-
-FROM app
-USER node
-
-CMD ["node", "--expose-gc", "app.js"]

+ 0 - 662
services/track-changes/LICENSE

@@ -1,662 +0,0 @@
-
-                    GNU AFFERO GENERAL PUBLIC LICENSE
-                       Version 3, 19 November 2007
-
- Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
-                            Preamble
-
-  The GNU Affero General Public License is a free, copyleft license for
-software and other kinds of works, specifically designed to ensure
-cooperation with the community in the case of network server software.
-
-  The licenses for most software and other practical works are designed
-to take away your freedom to share and change the works.  By contrast,
-our General Public Licenses are intended to guarantee your freedom to
-share and change all versions of a program--to make sure it remains free
-software for all its users.
-
-  When we speak of free software, we are referring to freedom, not
-price.  Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-them if you wish), that you receive source code or can get it if you
-want it, that you can change the software or use pieces of it in new
-free programs, and that you know you can do these things.
-
-  Developers that use our General Public Licenses protect your rights
-with two steps: (1) assert copyright on the software, and (2) offer
-you this License which gives you legal permission to copy, distribute
-and/or modify the software.
-
-  A secondary benefit of defending all users' freedom is that
-improvements made in alternate versions of the program, if they
-receive widespread use, become available for other developers to
-incorporate.  Many developers of free software are heartened and
-encouraged by the resulting cooperation.  However, in the case of
-software used on network servers, this result may fail to come about.
-The GNU General Public License permits making a modified version and
-letting the public access it on a server without ever releasing its
-source code to the public.
-
-  The GNU Affero General Public License is designed specifically to
-ensure that, in such cases, the modified source code becomes available
-to the community.  It requires the operator of a network server to
-provide the source code of the modified version running there to the
-users of that server.  Therefore, public use of a modified version, on
-a publicly accessible server, gives the public access to the source
-code of the modified version.
-
-  An older license, called the Affero General Public License and
-published by Affero, was designed to accomplish similar goals.  This is
-a different license, not a version of the Affero GPL, but Affero has
-released a new version of the Affero GPL which permits relicensing under
-this license.
-
-  The precise terms and conditions for copying, distribution and
-modification follow.
-
-                       TERMS AND CONDITIONS
-
-  0. Definitions.
-
-  "This License" refers to version 3 of the GNU Affero General Public License.
-
-  "Copyright" also means copyright-like laws that apply to other kinds of
-works, such as semiconductor masks.
-
-  "The Program" refers to any copyrightable work licensed under this
-License.  Each licensee is addressed as "you".  "Licensees" and
-"recipients" may be individuals or organizations.
-
-  To "modify" a work means to copy from or adapt all or part of the work
-in a fashion requiring copyright permission, other than the making of an
-exact copy.  The resulting work is called a "modified version" of the
-earlier work or a work "based on" the earlier work.
-
-  A "covered work" means either the unmodified Program or a work based
-on the Program.
-
-  To "propagate" a work means to do anything with it that, without
-permission, would make you directly or secondarily liable for
-infringement under applicable copyright law, except executing it on a
-computer or modifying a private copy.  Propagation includes copying,
-distribution (with or without modification), making available to the
-public, and in some countries other activities as well.
-
-  To "convey" a work means any kind of propagation that enables other
-parties to make or receive copies.  Mere interaction with a user through
-a computer network, with no transfer of a copy, is not conveying.
-
-  An interactive user interface displays "Appropriate Legal Notices"
-to the extent that it includes a convenient and prominently visible
-feature that (1) displays an appropriate copyright notice, and (2)
-tells the user that there is no warranty for the work (except to the
-extent that warranties are provided), that licensees may convey the
-work under this License, and how to view a copy of this License.  If
-the interface presents a list of user commands or options, such as a
-menu, a prominent item in the list meets this criterion.
-
-  1. Source Code.
-
-  The "source code" for a work means the preferred form of the work
-for making modifications to it.  "Object code" means any non-source
-form of a work.
-
-  A "Standard Interface" means an interface that either is an official
-standard defined by a recognized standards body, or, in the case of
-interfaces specified for a particular programming language, one that
-is widely used among developers working in that language.
-
-  The "System Libraries" of an executable work include anything, other
-than the work as a whole, that (a) is included in the normal form of
-packaging a Major Component, but which is not part of that Major
-Component, and (b) serves only to enable use of the work with that
-Major Component, or to implement a Standard Interface for which an
-implementation is available to the public in source code form.  A
-"Major Component", in this context, means a major essential component
-(kernel, window system, and so on) of the specific operating system
-(if any) on which the executable work runs, or a compiler used to
-produce the work, or an object code interpreter used to run it.
-
-  The "Corresponding Source" for a work in object code form means all
-the source code needed to generate, install, and (for an executable
-work) run the object code and to modify the work, including scripts to
-control those activities.  However, it does not include the work's
-System Libraries, or general-purpose tools or generally available free
-programs which are used unmodified in performing those activities but
-which are not part of the work.  For example, Corresponding Source
-includes interface definition files associated with source files for
-the work, and the source code for shared libraries and dynamically
-linked subprograms that the work is specifically designed to require,
-such as by intimate data communication or control flow between those
-subprograms and other parts of the work.
-
-  The Corresponding Source need not include anything that users
-can regenerate automatically from other parts of the Corresponding
-Source.
-
-  The Corresponding Source for a work in source code form is that
-same work.
-
-  2. Basic Permissions.
-
-  All rights granted under this License are granted for the term of
-copyright on the Program, and are irrevocable provided the stated
-conditions are met.  This License explicitly affirms your unlimited
-permission to run the unmodified Program.  The output from running a
-covered work is covered by this License only if the output, given its
-content, constitutes a covered work.  This License acknowledges your
-rights of fair use or other equivalent, as provided by copyright law.
-
-  You may make, run and propagate covered works that you do not
-convey, without conditions so long as your license otherwise remains
-in force.  You may convey covered works to others for the sole purpose
-of having them make modifications exclusively for you, or provide you
-with facilities for running those works, provided that you comply with
-the terms of this License in conveying all material for which you do
-not control copyright.  Those thus making or running the covered works
-for you must do so exclusively on your behalf, under your direction
-and control, on terms that prohibit them from making any copies of
-your copyrighted material outside their relationship with you.
-
-  Conveying under any other circumstances is permitted solely under
-the conditions stated below.  Sublicensing is not allowed; section 10
-makes it unnecessary.
-
-  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
-
-  No covered work shall be deemed part of an effective technological
-measure under any applicable law fulfilling obligations under article
-11 of the WIPO copyright treaty adopted on 20 December 1996, or
-similar laws prohibiting or restricting circumvention of such
-measures.
-
-  When you convey a covered work, you waive any legal power to forbid
-circumvention of technological measures to the extent such circumvention
-is effected by exercising rights under this License with respect to
-the covered work, and you disclaim any intention to limit operation or
-modification of the work as a means of enforcing, against the work's
-users, your or third parties' legal rights to forbid circumvention of
-technological measures.
-
-  4. Conveying Verbatim Copies.
-
-  You may convey verbatim copies of the Program's source code as you
-receive it, in any medium, provided that you conspicuously and
-appropriately publish on each copy an appropriate copyright notice;
-keep intact all notices stating that this License and any
-non-permissive terms added in accord with section 7 apply to the code;
-keep intact all notices of the absence of any warranty; and give all
-recipients a copy of this License along with the Program.
-
-  You may charge any price or no price for each copy that you convey,
-and you may offer support or warranty protection for a fee.
-
-  5. Conveying Modified Source Versions.
-
-  You may convey a work based on the Program, or the modifications to
-produce it from the Program, in the form of source code under the
-terms of section 4, provided that you also meet all of these conditions:
-
-    a) The work must carry prominent notices stating that you modified
-    it, and giving a relevant date.
-
-    b) The work must carry prominent notices stating that it is
-    released under this License and any conditions added under section
-    7.  This requirement modifies the requirement in section 4 to
-    "keep intact all notices".
-
-    c) You must license the entire work, as a whole, under this
-    License to anyone who comes into possession of a copy.  This
-    License will therefore apply, along with any applicable section 7
-    additional terms, to the whole of the work, and all its parts,
-    regardless of how they are packaged.  This License gives no
-    permission to license the work in any other way, but it does not
-    invalidate such permission if you have separately received it.
-
-    d) If the work has interactive user interfaces, each must display
-    Appropriate Legal Notices; however, if the Program has interactive
-    interfaces that do not display Appropriate Legal Notices, your
-    work need not make them do so.
-
-  A compilation of a covered work with other separate and independent
-works, which are not by their nature extensions of the covered work,
-and which are not combined with it such as to form a larger program,
-in or on a volume of a storage or distribution medium, is called an
-"aggregate" if the compilation and its resulting copyright are not
-used to limit the access or legal rights of the compilation's users
-beyond what the individual works permit.  Inclusion of a covered work
-in an aggregate does not cause this License to apply to the other
-parts of the aggregate.
-
-  6. Conveying Non-Source Forms.
-
-  You may convey a covered work in object code form under the terms
-of sections 4 and 5, provided that you also convey the
-machine-readable Corresponding Source under the terms of this License,
-in one of these ways:
-
-    a) Convey the object code in, or embodied in, a physical product
-    (including a physical distribution medium), accompanied by the
-    Corresponding Source fixed on a durable physical medium
-    customarily used for software interchange.
-
-    b) Convey the object code in, or embodied in, a physical product
-    (including a physical distribution medium), accompanied by a
-    written offer, valid for at least three years and valid for as
-    long as you offer spare parts or customer support for that product
-    model, to give anyone who possesses the object code either (1) a
-    copy of the Corresponding Source for all the software in the
-    product that is covered by this License, on a durable physical
-    medium customarily used for software interchange, for a price no
-    more than your reasonable cost of physically performing this
-    conveying of source, or (2) access to copy the
-    Corresponding Source from a network server at no charge.
-
-    c) Convey individual copies of the object code with a copy of the
-    written offer to provide the Corresponding Source.  This
-    alternative is allowed only occasionally and noncommercially, and
-    only if you received the object code with such an offer, in accord
-    with subsection 6b.
-
-    d) Convey the object code by offering access from a designated
-    place (gratis or for a charge), and offer equivalent access to the
-    Corresponding Source in the same way through the same place at no
-    further charge.  You need not require recipients to copy the
-    Corresponding Source along with the object code.  If the place to
-    copy the object code is a network server, the Corresponding Source
-    may be on a different server (operated by you or a third party)
-    that supports equivalent copying facilities, provided you maintain
-    clear directions next to the object code saying where to find the
-    Corresponding Source.  Regardless of what server hosts the
-    Corresponding Source, you remain obligated to ensure that it is
-    available for as long as needed to satisfy these requirements.
-
-    e) Convey the object code using peer-to-peer transmission, provided
-    you inform other peers where the object code and Corresponding
-    Source of the work are being offered to the general public at no
-    charge under subsection 6d.
-
-  A separable portion of the object code, whose source code is excluded
-from the Corresponding Source as a System Library, need not be
-included in conveying the object code work.
-
-  A "User Product" is either (1) a "consumer product", which means any
-tangible personal property which is normally used for personal, family,
-or household purposes, or (2) anything designed or sold for incorporation
-into a dwelling.  In determining whether a product is a consumer product,
-doubtful cases shall be resolved in favor of coverage.  For a particular
-product received by a particular user, "normally used" refers to a
-typical or common use of that class of product, regardless of the status
-of the particular user or of the way in which the particular user
-actually uses, or expects or is expected to use, the product.  A product
-is a consumer product regardless of whether the product has substantial
-commercial, industrial or non-consumer uses, unless such uses represent
-the only significant mode of use of the product.
-
-  "Installation Information" for a User Product means any methods,
-procedures, authorization keys, or other information required to install
-and execute modified versions of a covered work in that User Product from
-a modified version of its Corresponding Source.  The information must
-suffice to ensure that the continued functioning of the modified object
-code is in no case prevented or interfered with solely because
-modification has been made.
-
-  If you convey an object code work under this section in, or with, or
-specifically for use in, a User Product, and the conveying occurs as
-part of a transaction in which the right of possession and use of the
-User Product is transferred to the recipient in perpetuity or for a
-fixed term (regardless of how the transaction is characterized), the
-Corresponding Source conveyed under this section must be accompanied
-by the Installation Information.  But this requirement does not apply
-if neither you nor any third party retains the ability to install
-modified object code on the User Product (for example, the work has
-been installed in ROM).
-
-  The requirement to provide Installation Information does not include a
-requirement to continue to provide support service, warranty, or updates
-for a work that has been modified or installed by the recipient, or for
-the User Product in which it has been modified or installed.  Access to a
-network may be denied when the modification itself materially and
-adversely affects the operation of the network or violates the rules and
-protocols for communication across the network.
-
-  Corresponding Source conveyed, and Installation Information provided,
-in accord with this section must be in a format that is publicly
-documented (and with an implementation available to the public in
-source code form), and must require no special password or key for
-unpacking, reading or copying.
-
-  7. Additional Terms.
-
-  "Additional permissions" are terms that supplement the terms of this
-License by making exceptions from one or more of its conditions.
-Additional permissions that are applicable to the entire Program shall
-be treated as though they were included in this License, to the extent
-that they are valid under applicable law.  If additional permissions
-apply only to part of the Program, that part may be used separately
-under those permissions, but the entire Program remains governed by
-this License without regard to the additional permissions.
-
-  When you convey a copy of a covered work, you may at your option
-remove any additional permissions from that copy, or from any part of
-it.  (Additional permissions may be written to require their own
-removal in certain cases when you modify the work.)  You may place
-additional permissions on material, added by you to a covered work,
-for which you have or can give appropriate copyright permission.
-
-  Notwithstanding any other provision of this License, for material you
-add to a covered work, you may (if authorized by the copyright holders of
-that material) supplement the terms of this License with terms:
-
-    a) Disclaiming warranty or limiting liability differently from the
-    terms of sections 15 and 16 of this License; or
-
-    b) Requiring preservation of specified reasonable legal notices or
-    author attributions in that material or in the Appropriate Legal
-    Notices displayed by works containing it; or
-
-    c) Prohibiting misrepresentation of the origin of that material, or
-    requiring that modified versions of such material be marked in
-    reasonable ways as different from the original version; or
-
-    d) Limiting the use for publicity purposes of names of licensors or
-    authors of the material; or
-
-    e) Declining to grant rights under trademark law for use of some
-    trade names, trademarks, or service marks; or
-
-    f) Requiring indemnification of licensors and authors of that
-    material by anyone who conveys the material (or modified versions of
-    it) with contractual assumptions of liability to the recipient, for
-    any liability that these contractual assumptions directly impose on
-    those licensors and authors.
-
-  All other non-permissive additional terms are considered "further
-restrictions" within the meaning of section 10.  If the Program as you
-received it, or any part of it, contains a notice stating that it is
-governed by this License along with a term that is a further
-restriction, you may remove that term.  If a license document contains
-a further restriction but permits relicensing or conveying under this
-License, you may add to a covered work material governed by the terms
-of that license document, provided that the further restriction does
-not survive such relicensing or conveying.
-
-  If you add terms to a covered work in accord with this section, you
-must place, in the relevant source files, a statement of the
-additional terms that apply to those files, or a notice indicating
-where to find the applicable terms.
-
-  Additional terms, permissive or non-permissive, may be stated in the
-form of a separately written license, or stated as exceptions;
-the above requirements apply either way.
-
-  8. Termination.
-
-  You may not propagate or modify a covered work except as expressly
-provided under this License.  Any attempt otherwise to propagate or
-modify it is void, and will automatically terminate your rights under
-this License (including any patent licenses granted under the third
-paragraph of section 11).
-
-  However, if you cease all violation of this License, then your
-license from a particular copyright holder is reinstated (a)
-provisionally, unless and until the copyright holder explicitly and
-finally terminates your license, and (b) permanently, if the copyright
-holder fails to notify you of the violation by some reasonable means
-prior to 60 days after the cessation.
-
-  Moreover, your license from a particular copyright holder is
-reinstated permanently if the copyright holder notifies you of the
-violation by some reasonable means, this is the first time you have
-received notice of violation of this License (for any work) from that
-copyright holder, and you cure the violation prior to 30 days after
-your receipt of the notice.
-
-  Termination of your rights under this section does not terminate the
-licenses of parties who have received copies or rights from you under
-this License.  If your rights have been terminated and not permanently
-reinstated, you do not qualify to receive new licenses for the same
-material under section 10.
-
-  9. Acceptance Not Required for Having Copies.
-
-  You are not required to accept this License in order to receive or
-run a copy of the Program.  Ancillary propagation of a covered work
-occurring solely as a consequence of using peer-to-peer transmission
-to receive a copy likewise does not require acceptance.  However,
-nothing other than this License grants you permission to propagate or
-modify any covered work.  These actions infringe copyright if you do
-not accept this License.  Therefore, by modifying or propagating a
-covered work, you indicate your acceptance of this License to do so.
-
-  10. Automatic Licensing of Downstream Recipients.
-
-  Each time you convey a covered work, the recipient automatically
-receives a license from the original licensors, to run, modify and
-propagate that work, subject to this License.  You are not responsible
-for enforcing compliance by third parties with this License.
-
-  An "entity transaction" is a transaction transferring control of an
-organization, or substantially all assets of one, or subdividing an
-organization, or merging organizations.  If propagation of a covered
-work results from an entity transaction, each party to that
-transaction who receives a copy of the work also receives whatever
-licenses to the work the party's predecessor in interest had or could
-give under the previous paragraph, plus a right to possession of the
-Corresponding Source of the work from the predecessor in interest, if
-the predecessor has it or can get it with reasonable efforts.
-
-  You may not impose any further restrictions on the exercise of the
-rights granted or affirmed under this License.  For example, you may
-not impose a license fee, royalty, or other charge for exercise of
-rights granted under this License, and you may not initiate litigation
-(including a cross-claim or counterclaim in a lawsuit) alleging that
-any patent claim is infringed by making, using, selling, offering for
-sale, or importing the Program or any portion of it.
-
-  11. Patents.
-
-  A "contributor" is a copyright holder who authorizes use under this
-License of the Program or a work on which the Program is based.  The
-work thus licensed is called the contributor's "contributor version".
-
-  A contributor's "essential patent claims" are all patent claims
-owned or controlled by the contributor, whether already acquired or
-hereafter acquired, that would be infringed by some manner, permitted
-by this License, of making, using, or selling its contributor version,
-but do not include claims that would be infringed only as a
-consequence of further modification of the contributor version.  For
-purposes of this definition, "control" includes the right to grant
-patent sublicenses in a manner consistent with the requirements of
-this License.
-
-  Each contributor grants you a non-exclusive, worldwide, royalty-free
-patent license under the contributor's essential patent claims, to
-make, use, sell, offer for sale, import and otherwise run, modify and
-propagate the contents of its contributor version.
-
-  In the following three paragraphs, a "patent license" is any express
-agreement or commitment, however denominated, not to enforce a patent
-(such as an express permission to practice a patent or covenant not to
-sue for patent infringement).  To "grant" such a patent license to a
-party means to make such an agreement or commitment not to enforce a
-patent against the party.
-
-  If you convey a covered work, knowingly relying on a patent license,
-and the Corresponding Source of the work is not available for anyone
-to copy, free of charge and under the terms of this License, through a
-publicly available network server or other readily accessible means,
-then you must either (1) cause the Corresponding Source to be so
-available, or (2) arrange to deprive yourself of the benefit of the
-patent license for this particular work, or (3) arrange, in a manner
-consistent with the requirements of this License, to extend the patent
-license to downstream recipients.  "Knowingly relying" means you have
-actual knowledge that, but for the patent license, your conveying the
-covered work in a country, or your recipient's use of the covered work
-in a country, would infringe one or more identifiable patents in that
-country that you have reason to believe are valid.
-
-  If, pursuant to or in connection with a single transaction or
-arrangement, you convey, or propagate by procuring conveyance of, a
-covered work, and grant a patent license to some of the parties
-receiving the covered work authorizing them to use, propagate, modify
-or convey a specific copy of the covered work, then the patent license
-you grant is automatically extended to all recipients of the covered
-work and works based on it.
-
-  A patent license is "discriminatory" if it does not include within
-the scope of its coverage, prohibits the exercise of, or is
-conditioned on the non-exercise of one or more of the rights that are
-specifically granted under this License.  You may not convey a covered
-work if you are a party to an arrangement with a third party that is
-in the business of distributing software, under which you make payment
-to the third party based on the extent of your activity of conveying
-the work, and under which the third party grants, to any of the
-parties who would receive the covered work from you, a discriminatory
-patent license (a) in connection with copies of the covered work
-conveyed by you (or copies made from those copies), or (b) primarily
-for and in connection with specific products or compilations that
-contain the covered work, unless you entered into that arrangement,
-or that patent license was granted, prior to 28 March 2007.
-
-  Nothing in this License shall be construed as excluding or limiting
-any implied license or other defenses to infringement that may
-otherwise be available to you under applicable patent law.
-
-  12. No Surrender of Others' Freedom.
-
-  If conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License.  If you cannot convey a
-covered work so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you may
-not convey it at all.  For example, if you agree to terms that obligate you
-to collect a royalty for further conveying from those to whom you convey
-the Program, the only way you could satisfy both those terms and this
-License would be to refrain entirely from conveying the Program.
-
-  13. Remote Network Interaction; Use with the GNU General Public License.
-
-  Notwithstanding any other provision of this License, if you modify the
-Program, your modified version must prominently offer all users
-interacting with it remotely through a computer network (if your version
-supports such interaction) an opportunity to receive the Corresponding
-Source of your version by providing access to the Corresponding Source
-from a network server at no charge, through some standard or customary
-means of facilitating copying of software.  This Corresponding Source
-shall include the Corresponding Source for any work covered by version 3
-of the GNU General Public License that is incorporated pursuant to the
-following paragraph.
-
-  Notwithstanding any other provision of this License, you have
-permission to link or combine any covered work with a work licensed
-under version 3 of the GNU General Public License into a single
-combined work, and to convey the resulting work.  The terms of this
-License will continue to apply to the part which is the covered work,
-but the work with which it is combined will remain governed by version
-3 of the GNU General Public License.
-
-  14. Revised Versions of this License.
-
-  The Free Software Foundation may publish revised and/or new versions of
-the GNU Affero General Public License from time to time.  Such new versions
-will be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
-  Each version is given a distinguishing version number.  If the
-Program specifies that a certain numbered version of the GNU Affero General
-Public License "or any later version" applies to it, you have the
-option of following the terms and conditions either of that numbered
-version or of any later version published by the Free Software
-Foundation.  If the Program does not specify a version number of the
-GNU Affero General Public License, you may choose any version ever published
-by the Free Software Foundation.
-
-  If the Program specifies that a proxy can decide which future
-versions of the GNU Affero General Public License can be used, that proxy's
-public statement of acceptance of a version permanently authorizes you
-to choose that version for the Program.
-
-  Later license versions may give you additional or different
-permissions.  However, no additional obligations are imposed on any
-author or copyright holder as a result of your choosing to follow a
-later version.
-
-  15. Disclaimer of Warranty.
-
-  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
-APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
-HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
-OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
-THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
-IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
-ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
-  16. Limitation of Liability.
-
-  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
-THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
-GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
-USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
-DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
-PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
-EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
-SUCH DAMAGES.
-
-  17. Interpretation of Sections 15 and 16.
-
-  If the disclaimer of warranty and limitation of liability provided
-above cannot be given local legal effect according to their terms,
-reviewing courts shall apply local law that most closely approximates
-an absolute waiver of all civil liability in connection with the
-Program, unless a warranty or assumption of liability accompanies a
-copy of the Program in return for a fee.
-
-                     END OF TERMS AND CONDITIONS
-
-            How to Apply These Terms to Your New Programs
-
-  If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these terms.
-
-  To do so, attach the following notices to the program.  It is safest
-to attach them to the start of each source file to most effectively
-state the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
-    <one line to give the program's name and a brief idea of what it does.>
-    Copyright (C) <year>  <name of author>
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as published by
-    the Free Software Foundation, either version 3 of the License, or
-    (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-Also add information on how to contact you by electronic and paper mail.
-
-  If your software can interact with users remotely through a computer
-network, you should also make sure that it provides a way for users to
-get its source.  For example, if your program is a web application, its
-interface could display a "Source" link that leads users to an archive
-of the code.  There are many ways you could offer source, and different
-solutions will be better for different programs; see section 13 for the
-specific requirements.
-
-  You should also get your employer (if you work as a programmer) or school,
-if any, to sign a "copyright disclaimer" for the program, if necessary.
-For more information on this, and how to apply and follow the GNU AGPL, see
-<http://www.gnu.org/licenses/>.

+ 0 - 110
services/track-changes/Makefile

@@ -1,110 +0,0 @@
-# This file was auto-generated, do not edit it directly.
-# Instead run bin/update_build_scripts from
-# https://github.com/overleaf/internal/
-
-BUILD_NUMBER ?= local
-BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD)
-PROJECT_NAME = track-changes
-BUILD_DIR_NAME = $(shell pwd | xargs basename | tr -cd '[a-zA-Z0-9_.\-]')
-
-DOCKER_COMPOSE_FLAGS ?= -f docker-compose.yml
-DOCKER_COMPOSE := BUILD_NUMBER=$(BUILD_NUMBER) \
-	BRANCH_NAME=$(BRANCH_NAME) \
-	PROJECT_NAME=$(PROJECT_NAME) \
-	MOCHA_GREP=${MOCHA_GREP} \
-	docker-compose ${DOCKER_COMPOSE_FLAGS}
-
-DOCKER_COMPOSE_TEST_ACCEPTANCE = \
-	COMPOSE_PROJECT_NAME=test_acceptance_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE)
-
-DOCKER_COMPOSE_TEST_UNIT = \
-	COMPOSE_PROJECT_NAME=test_unit_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE)
-
-clean:
-	-docker rmi ci/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER)
-	-docker rmi gcr.io/overleaf-ops/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER)
-	-$(DOCKER_COMPOSE_TEST_UNIT) down --rmi local
-	-$(DOCKER_COMPOSE_TEST_ACCEPTANCE) down --rmi local
-
-HERE=$(shell pwd)
-MONOREPO=$(shell cd ../../ && pwd)
-# Run the linting commands in the scope of the monorepo.
-# Eslint and prettier (plus some configs) are on the root.
-RUN_LINTING = docker run --rm -v $(MONOREPO):$(MONOREPO) -w $(HERE) node:18.18.0 npm run --silent
-
-format:
-	$(RUN_LINTING) format
-
-format_fix:
-	$(RUN_LINTING) format:fix
-
-lint:
-	$(RUN_LINTING) lint
-
-lint_fix:
-	$(RUN_LINTING) lint:fix
-
-test: format lint test_unit test_acceptance
-
-test_unit:
-ifneq (,$(wildcard test/unit))
-	$(DOCKER_COMPOSE_TEST_UNIT) run --rm test_unit
-	$(MAKE) test_unit_clean
-endif
-
-test_clean: test_unit_clean
-test_unit_clean:
-ifneq (,$(wildcard test/unit))
-	$(DOCKER_COMPOSE_TEST_UNIT) down -v -t 0
-endif
-
-test_acceptance: test_acceptance_clean test_acceptance_pre_run test_acceptance_run
-	$(MAKE) test_acceptance_clean
-
-test_acceptance_debug: test_acceptance_clean test_acceptance_pre_run test_acceptance_run_debug
-	$(MAKE) test_acceptance_clean
-
-test_acceptance_run:
-ifneq (,$(wildcard test/acceptance))
-	$(DOCKER_COMPOSE_TEST_ACCEPTANCE) run --rm test_acceptance
-endif
-
-test_acceptance_run_debug:
-ifneq (,$(wildcard test/acceptance))
-	$(DOCKER_COMPOSE_TEST_ACCEPTANCE) run -p 127.0.0.9:19999:19999 --rm test_acceptance npm run test:acceptance -- --inspect=0.0.0.0:19999 --inspect-brk
-endif
-
-test_clean: test_acceptance_clean
-test_acceptance_clean:
-	$(DOCKER_COMPOSE_TEST_ACCEPTANCE) down -v -t 0
-
-test_acceptance_pre_run:
-	$(DOCKER_COMPOSE_TEST_ACCEPTANCE) up -d mongo
-	$(DOCKER_COMPOSE_TEST_ACCEPTANCE) exec -T mongo sh -c ' \
-		while ! mongo --eval "db.version()" > /dev/null; do \
-			echo "Waiting for Mongo..."; \
-			sleep 1; \
-		done; \
-		mongo --eval "rs.initiate({ _id: \"overleaf\", members: [ { _id: 0, host: \"mongo:27017\" } ] })"'
-ifneq (,$(wildcard test/acceptance/js/scripts/pre-run))
-	$(DOCKER_COMPOSE_TEST_ACCEPTANCE) run --rm test_acceptance test/acceptance/js/scripts/pre-run
-endif
-
-benchmarks:
-	$(DOCKER_COMPOSE_TEST_ACCEPTANCE) run --rm test_acceptance npm run benchmarks
-
-build:
-	docker build --pull --tag ci/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) \
-		--tag gcr.io/overleaf-ops/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) \
-		--file Dockerfile \
-		../..
-
-tar:
-	$(DOCKER_COMPOSE) up tar
-
-publish:
-
-	docker push $(DOCKER_REPO)/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER)
-
-
-.PHONY: clean test test_unit test_acceptance test_clean benchmarks build publish

+ 0 - 20
services/track-changes/README.md

@@ -1,20 +0,0 @@
-overleaf/track-changes
-========================
-
-An API for converting raw editor updates into a compressed and browseable history.
-
-Acceptance tests can be run with the command
-```
-AWS_BUCKET=<bucket-name> AWS_ACCESS_KEY_ID=<aws-access-key> AWS_SECRET_ACCESS_KEY=<aws-secret-access-key> make test
-```
-where `bucket-name`, `aws-access-key` and `aws-secret-access-key` are the credentials for an AWS S3 bucket.
-
-
-
-
-License
--------
-
-The code in this repository is released under the GNU AFFERO GENERAL PUBLIC LICENSE, version 3. A copy can be found in the `LICENSE` file.
-
-Copyright (c) Overleaf, 2014-2019.

+ 0 - 164
services/track-changes/app.js

@@ -1,164 +0,0 @@
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * DS103: Rewrite code to no longer use __guard__
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-const Metrics = require('@overleaf/metrics')
-Metrics.initialize('track-changes')
-const Settings = require('@overleaf/settings')
-const logger = require('@overleaf/logger')
-const TrackChangesLogger = logger.initialize('track-changes').logger
-
-if ((Settings.sentry != null ? Settings.sentry.dsn : undefined) != null) {
-  logger.initializeErrorReporting(Settings.sentry.dsn)
-}
-
-// log updates as truncated strings
-const truncateFn = updates =>
-  JSON.parse(
-    JSON.stringify(updates, function (key, value) {
-      let len
-      if (typeof value === 'string' && (len = value.length) > 80) {
-        return (
-          value.substr(0, 32) +
-          `...(message of length ${len} truncated)...` +
-          value.substr(-32)
-        )
-      } else {
-        return value
-      }
-    })
-  )
-TrackChangesLogger.addSerializers({
-  rawUpdate: truncateFn,
-  rawUpdates: truncateFn,
-  newUpdates: truncateFn,
-  lastUpdate: truncateFn,
-})
-
-const Path = require('path')
-
-Metrics.memory.monitor(logger)
-Metrics.open_sockets.monitor()
-
-const childProcess = require('child_process')
-
-const mongodb = require('./app/js/mongodb')
-const HttpController = require('./app/js/HttpController')
-const express = require('express')
-const bodyParser = require('body-parser')
-
-const app = express()
-
-app.use(bodyParser.json())
-
-app.use(Metrics.http.monitor(logger))
-
-Metrics.injectMetricsRoute(app)
-
-app.post('/project/:project_id/doc/:doc_id/flush', HttpController.flushDoc)
-
-app.get('/project/:project_id/doc/:doc_id/diff', HttpController.getDiff)
-
-app.get('/project/:project_id/doc/:doc_id/check', HttpController.checkDoc)
-
-app.get('/project/:project_id/updates', HttpController.getUpdates)
-app.get('/project/:project_id/export', HttpController.exportProject)
-
-app.get('/project/:project_id/zip', HttpController.zipProject)
-
-app.post('/project/:project_id/flush', HttpController.flushProject)
-
-app.post(
-  '/project/:project_id/doc/:doc_id/version/:version/restore',
-  HttpController.restore
-)
-
-app.post('/project/:project_id/doc/:doc_id/push', HttpController.pushDocHistory)
-app.post('/project/:project_id/doc/:doc_id/pull', HttpController.pullDocHistory)
-
-app.post('/flush/all', HttpController.flushAll)
-app.post('/check/dangling', HttpController.checkDanglingUpdates)
-
-let packWorker = null // use a single packing worker
-
-app.post('/pack', function (req, res, next) {
-  if (packWorker != null) {
-    return res.send('pack already running')
-  } else {
-    logger.debug('running pack')
-    packWorker = childProcess.fork(
-      Path.join(__dirname, '/app/js/PackWorker.js'),
-      [
-        req.query.limit || 1000,
-        req.query.delay || 1000,
-        req.query.timeout || 30 * 60 * 1000,
-      ]
-    )
-    packWorker.on('exit', function (code, signal) {
-      logger.debug({ code, signal }, 'history auto pack exited')
-      return (packWorker = null)
-    })
-    return res.send('pack started')
-  }
-})
-
-app.get('/status', (req, res, next) => res.send('track-changes is alive'))
-
-app.get('/oops', function (req, res, next) {
-  throw new Error('dummy test error')
-})
-
-app.get('/check_lock', HttpController.checkLock)
-
-app.get('/health_check', HttpController.healthCheck)
-
-app.use(function (error, req, res, next) {
-  logger.error({ err: error, req }, 'an internal error occured')
-  return res.sendStatus(500)
-})
-
-const port =
-  __guard__(
-    Settings.internal != null ? Settings.internal.trackchanges : undefined,
-    x => x.port
-  ) || 3015
-const host =
-  __guard__(
-    Settings.internal != null ? Settings.internal.trackchanges : undefined,
-    x1 => x1.host
-  ) || 'localhost'
-
-if (!module.parent) {
-  // Called directly
-  mongodb
-    .waitForDb()
-    .then(() => {
-      app.listen(port, host, function (error) {
-        if (error != null) {
-          return logger.error(
-            { err: error },
-            'could not start track-changes server'
-          )
-        } else {
-          return logger.debug(
-            `trackchanges starting up, listening on ${host}:${port}`
-          )
-        }
-      })
-    })
-    .catch(err => {
-      logger.fatal({ err }, 'Cannot connect to mongo. Exiting.')
-      process.exit(1)
-    })
-}
-
-module.exports = app
-
-function __guard__(value, transform) {
-  return typeof value !== 'undefined' && value !== null
-    ? transform(value)
-    : undefined
-}

+ 0 - 343
services/track-changes/app/js/DiffGenerator.js

@@ -1,343 +0,0 @@
-/* eslint-disable
-    no-proto,
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS101: Remove unnecessary use of Array.from
- * DS102: Remove unnecessary code created because of implicit returns
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-let DiffGenerator
-function ConsistencyError(message) {
-  const error = new Error(message)
-  error.name = 'ConsistencyError'
-  error.__proto__ = ConsistencyError.prototype
-  return error
-}
-ConsistencyError.prototype.__proto__ = Error.prototype
-
-const logger = require('@overleaf/logger')
-
-module.exports = DiffGenerator = {
-  ConsistencyError,
-
-  rewindUpdate(content, update) {
-    for (let j = update.op.length - 1, i = j; j >= 0; j--, i = j) {
-      const op = update.op[i]
-      if (op.broken !== true) {
-        try {
-          content = DiffGenerator.rewindOp(content, op)
-        } catch (e) {
-          if (e instanceof ConsistencyError && (i = update.op.length - 1)) {
-            // catch known case where the last op in an array has been
-            // merged into a later op
-            logger.warn(
-              { err: e, update, op: JSON.stringify(op) },
-              'marking op as broken'
-            )
-            op.broken = true
-          } else {
-            throw e // rethrow the execption
-          }
-        }
-      }
-    }
-    return content
-  },
-
-  rewindOp(content, op) {
-    let p
-    if (op.i != null) {
-      // ShareJS will accept an op where p > content.length when applied,
-      // and it applies as though p == content.length. However, the op is
-      // passed to us with the original p > content.length. Detect if that
-      // is the case with this op, and shift p back appropriately to match
-      // ShareJS if so.
-      ;({ p } = op)
-      const maxP = content.length - op.i.length
-      if (p > maxP) {
-        logger.warn({ maxP, p }, 'truncating position to content length')
-        p = maxP
-        op.p = p // fix out of range offsets to avoid invalid history exports in ZipManager
-      }
-
-      const textToBeRemoved = content.slice(p, p + op.i.length)
-      if (op.i !== textToBeRemoved) {
-        throw new ConsistencyError(
-          `Inserted content, '${op.i}', does not match text to be removed, '${textToBeRemoved}'`
-        )
-      }
-
-      return content.slice(0, p) + content.slice(p + op.i.length)
-    } else if (op.d != null) {
-      if (op.p > content.length) {
-        op.p = content.length // fix out of range offsets to avoid invalid history exports in ZipManager
-      }
-      return content.slice(0, op.p) + op.d + content.slice(op.p)
-    } else {
-      return content
-    }
-  },
-
-  rewindUpdates(content, updates) {
-    for (const update of Array.from(updates.reverse())) {
-      try {
-        content = DiffGenerator.rewindUpdate(content, update)
-      } catch (e) {
-        e.attempted_update = update // keep a record of the attempted update
-        throw e // rethrow the exception
-      }
-    }
-    return content
-  },
-
-  buildDiff(initialContent, updates) {
-    let diff = [{ u: initialContent }]
-    for (const update of Array.from(updates)) {
-      diff = DiffGenerator.applyUpdateToDiff(diff, update)
-    }
-    diff = DiffGenerator.compressDiff(diff)
-    return diff
-  },
-
-  compressDiff(diff) {
-    const newDiff = []
-    for (const part of Array.from(diff)) {
-      const lastPart = newDiff[newDiff.length - 1]
-      if (
-        lastPart != null &&
-        (lastPart.meta != null ? lastPart.meta.user : undefined) != null &&
-        (part.meta != null ? part.meta.user : undefined) != null
-      ) {
-        if (
-          lastPart.i != null &&
-          part.i != null &&
-          lastPart.meta.user.id === part.meta.user.id
-        ) {
-          lastPart.i += part.i
-          lastPart.meta.start_ts = Math.min(
-            lastPart.meta.start_ts,
-            part.meta.start_ts
-          )
-          lastPart.meta.end_ts = Math.max(
-            lastPart.meta.end_ts,
-            part.meta.end_ts
-          )
-        } else if (
-          lastPart.d != null &&
-          part.d != null &&
-          lastPart.meta.user.id === part.meta.user.id
-        ) {
-          lastPart.d += part.d
-          lastPart.meta.start_ts = Math.min(
-            lastPart.meta.start_ts,
-            part.meta.start_ts
-          )
-          lastPart.meta.end_ts = Math.max(
-            lastPart.meta.end_ts,
-            part.meta.end_ts
-          )
-        } else {
-          newDiff.push(part)
-        }
-      } else {
-        newDiff.push(part)
-      }
-    }
-    return newDiff
-  },
-
-  applyOpToDiff(diff, op, meta) {
-    let consumedDiff
-    const position = 0
-
-    let remainingDiff = diff.slice()
-    ;({ consumedDiff, remainingDiff } = DiffGenerator._consumeToOffset(
-      remainingDiff,
-      op.p
-    ))
-    const newDiff = consumedDiff
-
-    if (op.i != null) {
-      newDiff.push({
-        i: op.i,
-        meta,
-      })
-    } else if (op.d != null) {
-      ;({ consumedDiff, remainingDiff } =
-        DiffGenerator._consumeDiffAffectedByDeleteOp(remainingDiff, op, meta))
-      newDiff.push(...Array.from(consumedDiff || []))
-    }
-
-    newDiff.push(...Array.from(remainingDiff || []))
-
-    return newDiff
-  },
-
-  applyUpdateToDiff(diff, update) {
-    for (const op of Array.from(update.op)) {
-      if (op.broken !== true) {
-        diff = DiffGenerator.applyOpToDiff(diff, op, update.meta)
-      }
-    }
-    return diff
-  },
-
-  _consumeToOffset(remainingDiff, totalOffset) {
-    let part
-    const consumedDiff = []
-    let position = 0
-    while ((part = remainingDiff.shift())) {
-      const length = DiffGenerator._getLengthOfDiffPart(part)
-      if (part.d != null) {
-        consumedDiff.push(part)
-      } else if (position + length >= totalOffset) {
-        const partOffset = totalOffset - position
-        if (partOffset > 0) {
-          consumedDiff.push(DiffGenerator._slicePart(part, 0, partOffset))
-        }
-        if (partOffset < length) {
-          remainingDiff.unshift(DiffGenerator._slicePart(part, partOffset))
-        }
-        break
-      } else {
-        position += length
-        consumedDiff.push(part)
-      }
-    }
-
-    return {
-      consumedDiff,
-      remainingDiff,
-    }
-  },
-
-  _consumeDiffAffectedByDeleteOp(remainingDiff, deleteOp, meta) {
-    const consumedDiff = []
-    let remainingOp = deleteOp
-    while (remainingOp && remainingDiff.length > 0) {
-      let newPart
-      ;({ newPart, remainingDiff, remainingOp } =
-        DiffGenerator._consumeDeletedPart(remainingDiff, remainingOp, meta))
-      if (newPart != null) {
-        consumedDiff.push(newPart)
-      }
-    }
-    return {
-      consumedDiff,
-      remainingDiff,
-    }
-  },
-
-  _consumeDeletedPart(remainingDiff, op, meta) {
-    let deletedContent, newPart, remainingOp
-    const part = remainingDiff.shift()
-    const partLength = DiffGenerator._getLengthOfDiffPart(part)
-
-    if (part.d != null) {
-      // Skip existing deletes
-      remainingOp = op
-      newPart = part
-    } else if (partLength > op.d.length) {
-      // Only the first bit of the part has been deleted
-      const remainingPart = DiffGenerator._slicePart(part, op.d.length)
-      remainingDiff.unshift(remainingPart)
-
-      deletedContent = DiffGenerator._getContentOfPart(part).slice(
-        0,
-        op.d.length
-      )
-      if (deletedContent !== op.d) {
-        throw new ConsistencyError(
-          `deleted content, '${deletedContent}', does not match delete op, '${op.d}'`
-        )
-      }
-
-      if (part.u != null) {
-        newPart = {
-          d: op.d,
-          meta,
-        }
-      } else if (part.i != null) {
-        newPart = null
-      }
-
-      remainingOp = null
-    } else if (partLength === op.d.length) {
-      // The entire part has been deleted, but it is the last part
-
-      deletedContent = DiffGenerator._getContentOfPart(part)
-      if (deletedContent !== op.d) {
-        throw new ConsistencyError(
-          `deleted content, '${deletedContent}', does not match delete op, '${op.d}'`
-        )
-      }
-
-      if (part.u != null) {
-        newPart = {
-          d: op.d,
-          meta,
-        }
-      } else if (part.i != null) {
-        newPart = null
-      }
-
-      remainingOp = null
-    } else if (partLength < op.d.length) {
-      // The entire part has been deleted and there is more
-
-      deletedContent = DiffGenerator._getContentOfPart(part)
-      const opContent = op.d.slice(0, deletedContent.length)
-      if (deletedContent !== opContent) {
-        throw new ConsistencyError(
-          `deleted content, '${deletedContent}', does not match delete op, '${opContent}'`
-        )
-      }
-
-      if (part.u) {
-        newPart = {
-          d: part.u,
-          meta,
-        }
-      } else if (part.i != null) {
-        newPart = null
-      }
-
-      remainingOp = {
-        p: op.p,
-        d: op.d.slice(DiffGenerator._getLengthOfDiffPart(part)),
-      }
-    }
-
-    return {
-      newPart,
-      remainingDiff,
-      remainingOp,
-    }
-  },
-
-  _slicePart(basePart, from, to) {
-    let part
-    if (basePart.u != null) {
-      part = { u: basePart.u.slice(from, to) }
-    } else if (basePart.i != null) {
-      part = { i: basePart.i.slice(from, to) }
-    }
-    if (basePart.meta != null) {
-      part.meta = basePart.meta
-    }
-    return part
-  },
-
-  _getLengthOfDiffPart(part) {
-    return (part.u || part.d || part.i || '').length
-  },
-
-  _getContentOfPart(part) {
-    return part.u || part.d || part.i || ''
-  },
-}

+ 0 - 184
services/track-changes/app/js/DiffManager.js

@@ -1,184 +0,0 @@
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS101: Remove unnecessary use of Array.from
- * DS102: Remove unnecessary code created because of implicit returns
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-let DiffManager
-const UpdatesManager = require('./UpdatesManager')
-const DocumentUpdaterManager = require('./DocumentUpdaterManager')
-const DiffGenerator = require('./DiffGenerator')
-const logger = require('@overleaf/logger')
-
-module.exports = DiffManager = {
-  getLatestDocAndUpdates(projectId, docId, fromVersion, callback) {
-    // Get updates last, since then they must be ahead and it
-    // might be possible to rewind to the same version as the doc.
-    if (callback == null) {
-      callback = function () {}
-    }
-    return DocumentUpdaterManager.getDocument(
-      projectId,
-      docId,
-      function (error, content, version) {
-        if (error != null) {
-          return callback(error)
-        }
-        if (fromVersion == null) {
-          // If we haven't been given a version, just return lastest doc and no updates
-          return callback(null, content, version, [])
-        }
-        return UpdatesManager.getDocUpdatesWithUserInfo(
-          projectId,
-          docId,
-          { from: fromVersion },
-          function (error, updates) {
-            if (error != null) {
-              return callback(error)
-            }
-            return callback(null, content, version, updates)
-          }
-        )
-      }
-    )
-  },
-
-  getDiff(projectId, docId, fromVersion, toVersion, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    return DiffManager.getDocumentBeforeVersion(
-      projectId,
-      docId,
-      fromVersion,
-      function (error, startingContent, updates) {
-        let diff
-        if (error != null) {
-          if (error.message === 'broken-history') {
-            return callback(null, 'history unavailable')
-          } else {
-            return callback(error)
-          }
-        }
-
-        const updatesToApply = []
-        for (const update of Array.from(updates.slice().reverse())) {
-          if (update.v <= toVersion) {
-            updatesToApply.push(update)
-          }
-        }
-
-        try {
-          diff = DiffGenerator.buildDiff(startingContent, updatesToApply)
-        } catch (e) {
-          return callback(e)
-        }
-
-        return callback(null, diff)
-      }
-    )
-  },
-
-  getDocumentBeforeVersion(projectId, docId, version, _callback) {
-    // Whichever order we get the latest document and the latest updates,
-    // there is potential for updates to be applied between them so that
-    // they do not return the same 'latest' versions.
-    // If this happens, we just retry and hopefully get them at the compatible
-    // versions.
-    let retry
-    if (_callback == null) {
-      _callback = function () {}
-    }
-    let retries = 3
-    const callback = function (error, ...args) {
-      if (error != null) {
-        if (error.retry && retries > 0) {
-          logger.warn(
-            { error, projectId, docId, version, retries },
-            'retrying getDocumentBeforeVersion'
-          )
-          return retry()
-        } else {
-          return _callback(error)
-        }
-      } else {
-        return _callback(null, ...Array.from(args))
-      }
-    }
-
-    return (retry = function () {
-      retries--
-      return DiffManager._tryGetDocumentBeforeVersion(
-        projectId,
-        docId,
-        version,
-        callback
-      )
-    })()
-  },
-
-  _tryGetDocumentBeforeVersion(projectId, docId, version, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    logger.debug(
-      { projectId, docId, version },
-      'getting document before version'
-    )
-    return DiffManager.getLatestDocAndUpdates(
-      projectId,
-      docId,
-      version,
-      function (error, content, version, updates) {
-        let startingContent
-        if (error != null) {
-          return callback(error)
-        }
-
-        // bail out if we hit a broken update
-        for (const u of Array.from(updates)) {
-          if (u.broken) {
-            return callback(new Error('broken-history'))
-          }
-        }
-
-        // discard any updates which are ahead of this document version
-        while ((updates[0] != null ? updates[0].v : undefined) >= version) {
-          updates.shift()
-        }
-
-        const lastUpdate = updates[0]
-        if (lastUpdate != null && lastUpdate.v !== version - 1) {
-          error = new Error(
-            `latest update version, ${lastUpdate.v}, does not match doc version, ${version}`
-          )
-          error.retry = true
-          return callback(error)
-        }
-
-        logger.debug(
-          {
-            docVersion: version,
-            lastUpdateVersion: lastUpdate != null ? lastUpdate.v : undefined,
-            updateCount: updates.length,
-          },
-          'rewinding updates'
-        )
-
-        const tryUpdates = updates.slice().reverse()
-
-        try {
-          startingContent = DiffGenerator.rewindUpdates(content, tryUpdates)
-          // tryUpdates is reversed, and any unapplied ops are marked as broken
-        } catch (e) {
-          return callback(e)
-        }
-
-        return callback(null, startingContent, tryUpdates)
-      }
-    )
-  },
-}

+ 0 - 42
services/track-changes/app/js/DocIterator.js

@@ -1,42 +0,0 @@
-module.exports = class DocIterator {
-  constructor(packs, getPackByIdFn) {
-    this.getPackByIdFn = getPackByIdFn
-    // sort packs in descending order by version (i.e. most recent first)
-    const byVersion = (a, b) => b.v - a.v
-    this.packs = packs.slice().sort(byVersion)
-    this.queue = []
-  }
-
-  next(callback) {
-    const update = this.queue.shift()
-    if (update) {
-      return callback(null, update)
-    }
-    if (!this.packs.length) {
-      this._done = true
-      return callback(null)
-    }
-    const nextPack = this.packs[0]
-    this.getPackByIdFn(
-      nextPack.project_id,
-      nextPack.doc_id,
-      nextPack._id,
-      (err, pack) => {
-        if (err != null) {
-          return callback(err)
-        }
-        this.packs.shift() // have now retrieved this pack, remove it
-        for (const op of pack.pack.reverse()) {
-          op.doc_id = nextPack.doc_id
-          op.project_id = nextPack.project_id
-          this.queue.push(op)
-        }
-        return this.next(callback)
-      }
-    )
-  }
-
-  done() {
-    return this._done
-  }
-}

+ 0 - 52
services/track-changes/app/js/DocstoreManager.js

@@ -1,52 +0,0 @@
-const request = require('request')
-const logger = require('@overleaf/logger')
-const Settings = require('@overleaf/settings')
-const Errors = require('./Errors')
-
-function peekDocument(projectId, docId, callback) {
-  const url = `${Settings.apis.docstore.url}/project/${projectId}/doc/${docId}/peek`
-  logger.debug({ projectId, docId }, 'getting doc from docstore')
-  request.get(url, function (error, res, body) {
-    if (error != null) {
-      return callback(error)
-    }
-    if (res.statusCode >= 200 && res.statusCode < 300) {
-      try {
-        body = JSON.parse(body)
-      } catch (error) {
-        return callback(error)
-      }
-      logger.debug(
-        { projectId, docId, version: body.version },
-        'got doc from docstore'
-      )
-      return callback(null, body.lines.join('\n'), body.version)
-    } else if (res.statusCode === 404) {
-      return callback(
-        new Errors.NotFoundError('doc not found', { projectId, docId })
-      )
-    } else {
-      return callback(
-        new Error(
-          `docstore returned a non-success status code: ${res.statusCode}`
-        )
-      )
-    }
-  })
-}
-
-module.exports = {
-  promises: {
-    peekDocument: (projectId, docId) => {
-      return new Promise((resolve, reject) => {
-        peekDocument(projectId, docId, (err, content, version) => {
-          if (err) {
-            reject(err)
-          } else {
-            resolve([content, version])
-          }
-        })
-      })
-    },
-  },
-}

+ 0 - 128
services/track-changes/app/js/DocumentUpdaterManager.js

@@ -1,128 +0,0 @@
-/* eslint-disable
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-let DocumentUpdaterManager
-const request = require('request')
-const logger = require('@overleaf/logger')
-const Settings = require('@overleaf/settings')
-const Errors = require('./Errors')
-
-module.exports = DocumentUpdaterManager = {
-  _requestDocument(projectId, docId, url, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-
-    logger.debug({ projectId, docId }, 'getting doc from document updater')
-    return request.get(url, function (error, res, body) {
-      if (error != null) {
-        return callback(error)
-      }
-      if (res.statusCode >= 200 && res.statusCode < 300) {
-        try {
-          body = JSON.parse(body)
-        } catch (error1) {
-          error = error1
-          return callback(error)
-        }
-        logger.debug(
-          { projectId, docId, version: body.version },
-          'got doc from document updater'
-        )
-        return callback(null, body.lines.join('\n'), body.version)
-      } else {
-        error = new Error(
-          `doc updater returned a non-success status code: ${res.statusCode}`
-        )
-        logger.error(
-          { err: error, projectId, docId, url },
-          'error accessing doc updater'
-        )
-        if (res.statusCode === 404) {
-          return callback(
-            new Errors.NotFoundError('doc not found', {
-              projectId,
-              docId,
-            })
-          )
-        } else {
-          return callback(error)
-        }
-      }
-    })
-  },
-
-  getDocument(projectId, docId, callback) {
-    const url = `${Settings.apis.documentupdater.url}/project/${projectId}/doc/${docId}`
-    DocumentUpdaterManager._requestDocument(projectId, docId, url, callback)
-  },
-
-  peekDocument(projectId, docId, callback) {
-    const url = `${Settings.apis.documentupdater.url}/project/${projectId}/doc/${docId}/peek`
-    DocumentUpdaterManager._requestDocument(projectId, docId, url, callback)
-  },
-
-  setDocument(projectId, docId, content, userId, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    const url = `${Settings.apis.documentupdater.url}/project/${projectId}/doc/${docId}`
-    logger.debug({ projectId, docId }, 'setting doc in document updater')
-    return request.post(
-      {
-        url,
-        json: {
-          lines: content.split('\n'),
-          source: 'restore',
-          user_id: userId,
-          undoing: true,
-        },
-      },
-      function (error, res, body) {
-        if (error != null) {
-          return callback(error)
-        }
-        if (res.statusCode >= 200 && res.statusCode < 300) {
-          return callback(null)
-        } else {
-          error = new Error(
-            `doc updater returned a non-success status code: ${res.statusCode}`
-          )
-          logger.error(
-            { err: error, projectId, docId, url },
-            'error accessing doc updater'
-          )
-          return callback(error)
-        }
-      }
-    )
-  },
-}
-
-module.exports.promises = {
-  // peekDocument returns two arguments so we can't use util.promisify, which only handles a single argument, we need
-  // to treat this it as a special case.
-  peekDocument: (projectId, docId) => {
-    return new Promise((resolve, reject) => {
-      DocumentUpdaterManager.peekDocument(
-        projectId,
-        docId,
-        (err, content, version) => {
-          if (err) {
-            reject(err)
-          } else {
-            resolve([content, version])
-          }
-        }
-      )
-    })
-  },
-}

+ 0 - 5
services/track-changes/app/js/Errors.js

@@ -1,5 +0,0 @@
-const OError = require('@overleaf/o-error')
-
-class NotFoundError extends OError {}
-
-module.exports = { NotFoundError }

+ 0 - 84
services/track-changes/app/js/HealthChecker.js

@@ -1,84 +0,0 @@
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-const { ObjectId } = require('./mongodb')
-const request = require('request')
-const async = require('async')
-const settings = require('@overleaf/settings')
-const { port } = settings.internal.trackchanges
-const logger = require('@overleaf/logger')
-const LockManager = require('./LockManager')
-
-module.exports = {
-  check(callback) {
-    const projectId = ObjectId(settings.trackchanges.healthCheck.project_id)
-    const url = `http://localhost:${port}/project/${projectId}`
-    logger.debug({ projectId }, 'running health check')
-    const jobs = [
-      cb =>
-        request.get(
-          { url: `http://localhost:${port}/check_lock`, timeout: 3000 },
-          function (err, res, body) {
-            if (err != null) {
-              logger.err(
-                { err, projectId },
-                'error checking lock for health check'
-              )
-              return cb(err)
-            } else if ((res != null ? res.statusCode : undefined) !== 200) {
-              return cb(
-                new Error(`status code not 200, it's ${res.statusCode}`)
-              )
-            } else {
-              return cb()
-            }
-          }
-        ),
-      cb =>
-        request.post(
-          { url: `${url}/flush`, timeout: 10000 },
-          function (err, res, body) {
-            if (err != null) {
-              logger.err({ err, projectId }, 'error flushing for health check')
-              return cb(err)
-            } else if ((res != null ? res.statusCode : undefined) !== 204) {
-              return cb(
-                new Error(`status code not 204, it's ${res.statusCode}`)
-              )
-            } else {
-              return cb()
-            }
-          }
-        ),
-      cb =>
-        request.get(
-          { url: `${url}/updates`, timeout: 10000 },
-          function (err, res, body) {
-            if (err != null) {
-              logger.err(
-                { err, projectId },
-                'error getting updates for health check'
-              )
-              return cb(err)
-            } else if ((res != null ? res.statusCode : undefined) !== 200) {
-              return cb(
-                new Error(`status code not 200, it's ${res.statusCode}`)
-              )
-            } else {
-              return cb()
-            }
-          }
-        ),
-    ]
-    return async.series(jobs, callback)
-  },
-
-  checkLock(callback) {
-    return LockManager.healthCheck(callback)
-  },
-}

+ 0 - 364
services/track-changes/app/js/HttpController.js

@@ -1,364 +0,0 @@
-/* eslint-disable
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS101: Remove unnecessary use of Array.from
- * DS102: Remove unnecessary code created because of implicit returns
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-let HttpController
-const UpdatesManager = require('./UpdatesManager')
-const DiffManager = require('./DiffManager')
-const PackManager = require('./PackManager')
-const RestoreManager = require('./RestoreManager')
-const ZipManager = require('./ZipManager')
-const logger = require('@overleaf/logger')
-const HealthChecker = require('./HealthChecker')
-const _ = require('underscore')
-const Path = require('path')
-const { pipeline } = require('stream')
-
-module.exports = HttpController = {
-  flushDoc(req, res, next) {
-    if (next == null) {
-      next = function () {}
-    }
-    const { doc_id: docId } = req.params
-    const { project_id: projectId } = req.params
-    logger.debug({ projectId, docId }, 'compressing doc history')
-    return UpdatesManager.processUncompressedUpdatesWithLock(
-      projectId,
-      docId,
-      function (error) {
-        if (error != null) {
-          return next(error)
-        }
-        return res.sendStatus(204)
-      }
-    )
-  },
-
-  flushProject(req, res, next) {
-    if (next == null) {
-      next = function () {}
-    }
-    const { project_id: projectId } = req.params
-    logger.debug({ projectId }, 'compressing project history')
-    return UpdatesManager.processUncompressedUpdatesForProject(
-      projectId,
-      function (error) {
-        if (error != null) {
-          return next(error)
-        }
-        return res.sendStatus(204)
-      }
-    )
-  },
-
-  flushAll(req, res, next) {
-    // limit on projects to flush or -1 for all (default)
-    if (next == null) {
-      next = function () {}
-    }
-    const limit = req.query.limit != null ? parseInt(req.query.limit, 10) : -1
-    logger.debug({ limit }, 'flushing all projects')
-    return UpdatesManager.flushAll(limit, function (error, result) {
-      if (error != null) {
-        return next(error)
-      }
-      const { failed, succeeded, all } = result
-      const status = `${succeeded.length} succeeded, ${failed.length} failed`
-      if (limit === 0) {
-        return res
-          .status(200)
-          .send(`${status}\nwould flush:\n${all.join('\n')}\n`)
-      } else if (failed.length > 0) {
-        logger.debug({ failed, succeeded }, 'error flushing projects')
-        return res
-          .status(500)
-          .send(`${status}\nfailed to flush:\n${failed.join('\n')}\n`)
-      } else {
-        return res
-          .status(200)
-          .send(
-            `${status}\nflushed ${succeeded.length} projects of ${all.length}\n`
-          )
-      }
-    })
-  },
-
-  checkDanglingUpdates(req, res, next) {
-    if (next == null) {
-      next = function () {}
-    }
-    logger.debug('checking dangling updates')
-    return UpdatesManager.getDanglingUpdates(function (error, result) {
-      if (error != null) {
-        return next(error)
-      }
-      if (result.length > 0) {
-        logger.debug({ dangling: result }, 'found dangling updates')
-        return res.status(500).send(`dangling updates:\n${result.join('\n')}\n`)
-      } else {
-        return res.status(200).send('no dangling updates found\n')
-      }
-    })
-  },
-
-  checkDoc(req, res, next) {
-    if (next == null) {
-      next = function () {}
-    }
-    const { doc_id: docId } = req.params
-    const { project_id: projectId } = req.params
-    logger.debug({ projectId, docId }, 'checking doc history')
-    return DiffManager.getDocumentBeforeVersion(
-      projectId,
-      docId,
-      1,
-      function (error, document, rewoundUpdates) {
-        if (error != null) {
-          return next(error)
-        }
-        const broken = []
-        for (const update of Array.from(rewoundUpdates)) {
-          for (const op of Array.from(update.op)) {
-            if (op.broken === true) {
-              broken.push(op)
-            }
-          }
-        }
-        if (broken.length > 0) {
-          return res.send(broken)
-        } else {
-          return res.sendStatus(204)
-        }
-      }
-    )
-  },
-
-  getDiff(req, res, next) {
-    let from, to
-    if (next == null) {
-      next = function () {}
-    }
-    const { doc_id: docId } = req.params
-    const { project_id: projectId } = req.params
-
-    if (req.query.from != null) {
-      from = parseInt(req.query.from, 10)
-    } else {
-      from = null
-    }
-    if (req.query.to != null) {
-      to = parseInt(req.query.to, 10)
-    } else {
-      to = null
-    }
-
-    logger.debug({ projectId, docId, from, to }, 'getting diff')
-    return DiffManager.getDiff(
-      projectId,
-      docId,
-      from,
-      to,
-      function (error, diff) {
-        if (error != null) {
-          return next(error)
-        }
-        return res.json({ diff })
-      }
-    )
-  },
-
-  getUpdates(req, res, next) {
-    let before, minCount
-    if (next == null) {
-      next = function () {}
-    }
-    const { project_id: projectId } = req.params
-
-    if (req.query.before != null) {
-      before = parseInt(req.query.before, 10)
-    }
-    if (req.query.min_count != null) {
-      minCount = parseInt(req.query.min_count, 10)
-    }
-
-    return UpdatesManager.getSummarizedProjectUpdates(
-      projectId,
-      { before, min_count: minCount },
-      function (error, updates, nextBeforeTimestamp) {
-        if (error != null) {
-          return next(error)
-        }
-        return res.json({
-          updates,
-          nextBeforeTimestamp,
-        })
-      }
-    )
-  },
-
-  zipProject(req, res, next) {
-    const { project_id: projectId } = req.params
-    logger.debug({ projectId }, 'exporting project history as zip file')
-    ZipManager.makeTempDirectory((err, tmpdir) => {
-      if (err) {
-        return next(err)
-      }
-      const zipFilePath = Path.join(tmpdir, 'export.zip')
-      ZipManager.exportProject(projectId, zipFilePath, err => {
-        if (err) {
-          ZipManager.cleanupTempDirectory(tmpdir)
-          return next(err)
-        }
-        res.download(zipFilePath, `${projectId}-track-changes.zip`, err => {
-          ZipManager.cleanupTempDirectory(tmpdir)
-          if (err && !res.headersSent) {
-            return next(err)
-          }
-        })
-      })
-    })
-  },
-
-  exportProject(req, res, next) {
-    // The project history can be huge:
-    //  - updates can weight MBs for insert/delete of full doc
-    //  - multiple updates form a pack
-    // Flush updates per pack onto the wire.
-    const { project_id: projectId } = req.params
-    logger.debug({ projectId }, 'exporting project history')
-    UpdatesManager.exportProject(
-      projectId,
-      function (err, { updates, userIds }, confirmWrite) {
-        const abortStreaming = req.destroyed || res.finished || res.destroyed
-        if (abortStreaming) {
-          // Tell the producer to stop emitting data
-          if (confirmWrite) confirmWrite(new Error('stop'))
-          return
-        }
-        const hasStartedStreamingResponse = res.headersSent
-        if (err) {
-          logger.error({ projectId, err }, 'export failed')
-          if (!hasStartedStreamingResponse) {
-            // Generate a nice 500
-            return next(err)
-          } else {
-            // Stop streaming
-            return res.destroy()
-          }
-        }
-        // Compose the response incrementally
-        const isFirstWrite = !hasStartedStreamingResponse
-        const isLastWrite = updates.length === 0
-        if (isFirstWrite) {
-          // The first write will emit the 200 status, headers and start of the
-          //  response payload (open array)
-          res.setHeader('Content-Type', 'application/json')
-          res.setHeader('Trailer', 'X-User-Ids')
-          res.writeHead(200)
-          res.write('[')
-        }
-        if (!isFirstWrite && !isLastWrite) {
-          // Starting from the 2nd non-empty write, emit a continuing comma.
-          // write 1: [updates1
-          // write 2: ,updates2
-          // write 3: ,updates3
-          // write N: ]
-          res.write(',')
-        }
-
-        // Every write will emit a blob onto the response stream:
-        // '[update1,update2,...]'
-        //   ^^^^^^^^^^^^^^^^^^^
-        res.write(JSON.stringify(updates).slice(1, -1), confirmWrite)
-
-        if (isLastWrite) {
-          // The last write will have no updates and will finish the response
-          //  payload (close array) and emit the userIds as trailer.
-          res.addTrailers({ 'X-User-Ids': JSON.stringify(userIds) })
-          res.end(']')
-        }
-      }
-    )
-  },
-
-  restore(req, res, next) {
-    if (next == null) {
-      next = function () {}
-    }
-    let { doc_id: docId, project_id: projectId, version } = req.params
-    const userId = req.headers['x-user-id']
-    version = parseInt(version, 10)
-    return RestoreManager.restoreToBeforeVersion(
-      projectId,
-      docId,
-      version,
-      userId,
-      function (error) {
-        if (error != null) {
-          return next(error)
-        }
-        return res.sendStatus(204)
-      }
-    )
-  },
-
-  pushDocHistory(req, res, next) {
-    if (next == null) {
-      next = function () {}
-    }
-    const { project_id: projectId } = req.params
-    const { doc_id: docId } = req.params
-    logger.debug({ projectId, docId }, 'pushing all finalised changes to s3')
-    return PackManager.pushOldPacks(projectId, docId, function (error) {
-      if (error != null) {
-        return next(error)
-      }
-      return res.sendStatus(204)
-    })
-  },
-
-  pullDocHistory(req, res, next) {
-    if (next == null) {
-      next = function () {}
-    }
-    const { project_id: projectId } = req.params
-    const { doc_id: docId } = req.params
-    logger.debug({ projectId, docId }, 'pulling all packs from s3')
-    return PackManager.pullOldPacks(projectId, docId, function (error) {
-      if (error != null) {
-        return next(error)
-      }
-      return res.sendStatus(204)
-    })
-  },
-
-  healthCheck(req, res) {
-    return HealthChecker.check(function (err) {
-      if (err != null) {
-        logger.err({ err }, 'error performing health check')
-        return res.sendStatus(500)
-      } else {
-        return res.sendStatus(200)
-      }
-    })
-  },
-
-  checkLock(req, res) {
-    return HealthChecker.checkLock(function (err) {
-      if (err != null) {
-        logger.err({ err }, 'error performing lock check')
-        return res.sendStatus(500)
-      } else {
-        return res.sendStatus(200)
-      }
-    })
-  },
-}

+ 0 - 161
services/track-changes/app/js/LockManager.js

@@ -1,161 +0,0 @@
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-let LockManager
-const Settings = require('@overleaf/settings')
-const redis = require('@overleaf/redis-wrapper')
-const rclient = redis.createClient(Settings.redis.lock)
-const os = require('os')
-const crypto = require('crypto')
-const logger = require('@overleaf/logger')
-
-const HOST = os.hostname()
-const PID = process.pid
-const RND = crypto.randomBytes(4).toString('hex')
-let COUNT = 0
-
-module.exports = LockManager = {
-  LOCK_TEST_INTERVAL: 50, // 50ms between each test of the lock
-  MAX_LOCK_WAIT_TIME: 10000, // 10s maximum time to spend trying to get the lock
-  LOCK_TTL: 300, // seconds (allow 5 minutes for any operation to complete)
-
-  // Use a signed lock value as described in
-  // http://redis.io/topics/distlock#correct-implementation-with-a-single-instance
-  // to prevent accidental unlocking by multiple processes
-  randomLock() {
-    const time = Date.now()
-    return `locked:host=${HOST}:pid=${PID}:random=${RND}:time=${time}:count=${COUNT++}`
-  },
-
-  unlockScript:
-    'if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end',
-
-  tryLock(key, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    const lockValue = LockManager.randomLock()
-    return rclient.set(
-      key,
-      lockValue,
-      'EX',
-      this.LOCK_TTL,
-      'NX',
-      function (err, gotLock) {
-        if (err != null) {
-          return callback(err)
-        }
-        if (gotLock === 'OK') {
-          return callback(err, true, lockValue)
-        } else {
-          return callback(err, false)
-        }
-      }
-    )
-  },
-
-  getLock(key, callback) {
-    let attempt
-    if (callback == null) {
-      callback = function () {}
-    }
-    const startTime = Date.now()
-    return (attempt = function () {
-      if (Date.now() - startTime > LockManager.MAX_LOCK_WAIT_TIME) {
-        const e = new Error('Timeout')
-        e.key = key
-        return callback(e)
-      }
-
-      return LockManager.tryLock(key, function (error, gotLock, lockValue) {
-        if (error != null) {
-          return callback(error)
-        }
-        if (gotLock) {
-          return callback(null, lockValue)
-        } else {
-          return setTimeout(attempt, LockManager.LOCK_TEST_INTERVAL)
-        }
-      })
-    })()
-  },
-
-  checkLock(key, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    return rclient.exists(key, function (err, exists) {
-      if (err != null) {
-        return callback(err)
-      }
-      exists = parseInt(exists)
-      if (exists === 1) {
-        return callback(err, false)
-      } else {
-        return callback(err, true)
-      }
-    })
-  },
-
-  releaseLock(key, lockValue, callback) {
-    return rclient.eval(
-      LockManager.unlockScript,
-      1,
-      key,
-      lockValue,
-      function (err, result) {
-        if (err != null) {
-          return callback(err)
-        }
-        if (result != null && result !== 1) {
-          // successful unlock should release exactly one key
-          logger.error(
-            { key, lockValue, redisErr: err, redisResult: result },
-            'unlocking error'
-          )
-          return callback(new Error('tried to release timed out lock'))
-        }
-        return callback(err, result)
-      }
-    )
-  },
-
-  runWithLock(key, runner, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    return LockManager.getLock(key, function (error, lockValue) {
-      if (error != null) {
-        return callback(error)
-      }
-      return runner(error1 =>
-        LockManager.releaseLock(key, lockValue, function (error2) {
-          error = error1 || error2
-          if (error != null) {
-            return callback(error)
-          }
-          return callback()
-        })
-      )
-    })
-  },
-
-  healthCheck(callback) {
-    const action = releaseLock => releaseLock()
-    return LockManager.runWithLock(
-      `HistoryLock:HealthCheck:host=${HOST}:pid=${PID}:random=${RND}`,
-      action,
-      callback
-    )
-  },
-
-  close(callback) {
-    rclient.quit()
-    return rclient.once('end', callback)
-  },
-}

+ 0 - 197
services/track-changes/app/js/MongoAWS.js

@@ -1,197 +0,0 @@
-/* eslint-disable
-    no-return-assign,
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS101: Remove unnecessary use of Array.from
- * DS102: Remove unnecessary code created because of implicit returns
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-let MongoAWS
-const settings = require('@overleaf/settings')
-const logger = require('@overleaf/logger')
-const AWS = require('aws-sdk')
-const S3S = require('s3-streams')
-const { db, ObjectId } = require('./mongodb')
-const JSONStream = require('JSONStream')
-const ReadlineStream = require('byline')
-const zlib = require('zlib')
-const Metrics = require('@overleaf/metrics')
-
-const DAYS = 24 * 3600 * 1000 // one day in milliseconds
-
-const createStream = function (streamConstructor, projectId, docId, packId) {
-  const AWS_CONFIG = {
-    accessKeyId: settings.trackchanges.s3.key,
-    secretAccessKey: settings.trackchanges.s3.secret,
-    endpoint: settings.trackchanges.s3.endpoint,
-    s3ForcePathStyle: settings.trackchanges.s3.pathStyle,
-  }
-
-  return streamConstructor(new AWS.S3(AWS_CONFIG), {
-    Bucket: settings.trackchanges.stores.doc_history,
-    Key: projectId + '/changes-' + docId + '/pack-' + packId,
-  })
-}
-
-module.exports = MongoAWS = {
-  archivePack(projectId, docId, packId, _callback) {
-    if (_callback == null) {
-      _callback = function () {}
-    }
-    const callback = function (...args) {
-      _callback(...Array.from(args || []))
-      return (_callback = function () {})
-    }
-
-    const query = {
-      _id: ObjectId(packId),
-      doc_id: ObjectId(docId),
-    }
-
-    if (projectId == null) {
-      return callback(new Error('invalid project id'))
-    }
-    if (docId == null) {
-      return callback(new Error('invalid doc id'))
-    }
-    if (packId == null) {
-      return callback(new Error('invalid pack id'))
-    }
-
-    logger.debug({ projectId, docId, packId }, 'uploading data to s3')
-
-    const upload = createStream(S3S.WriteStream, projectId, docId, packId)
-
-    return db.docHistory.findOne(query, function (err, result) {
-      if (err != null) {
-        return callback(err)
-      }
-      if (result == null) {
-        return callback(new Error('cannot find pack to send to s3'))
-      }
-      if (result.expiresAt != null) {
-        return callback(new Error('refusing to send pack with TTL to s3'))
-      }
-      const uncompressedData = JSON.stringify(result)
-      if (uncompressedData.indexOf('\u0000') !== -1) {
-        const error = new Error('null bytes found in upload')
-        logger.error({ err: error, projectId, docId, packId }, error.message)
-        return callback(error)
-      }
-      return zlib.gzip(uncompressedData, function (err, buf) {
-        logger.debug(
-          {
-            projectId,
-            docId,
-            packId,
-            origSize: uncompressedData.length,
-            newSize: buf.length,
-          },
-          'compressed pack'
-        )
-        if (err != null) {
-          return callback(err)
-        }
-        upload.on('error', err => callback(err))
-        upload.on('finish', function () {
-          Metrics.inc('archive-pack')
-          logger.debug({ projectId, docId, packId }, 'upload to s3 completed')
-          return callback(null)
-        })
-        upload.write(buf)
-        return upload.end()
-      })
-    })
-  },
-
-  readArchivedPack(projectId, docId, packId, _callback) {
-    if (_callback == null) {
-      _callback = function () {}
-    }
-    const callback = function (...args) {
-      _callback(...Array.from(args || []))
-      return (_callback = function () {})
-    }
-
-    if (projectId == null) {
-      return callback(new Error('invalid project id'))
-    }
-    if (docId == null) {
-      return callback(new Error('invalid doc id'))
-    }
-    if (packId == null) {
-      return callback(new Error('invalid pack id'))
-    }
-
-    logger.debug({ projectId, docId, packId }, 'downloading data from s3')
-
-    const download = createStream(S3S.ReadStream, projectId, docId, packId)
-
-    const inputStream = download
-      .on('open', obj => 1)
-      .on('error', err => callback(err))
-
-    const gunzip = zlib.createGunzip()
-    gunzip.setEncoding('utf8')
-    gunzip.on('error', function (err) {
-      logger.debug(
-        { projectId, docId, packId, err },
-        'error uncompressing gzip stream'
-      )
-      return callback(err)
-    })
-
-    const outputStream = inputStream.pipe(gunzip)
-    const parts = []
-    outputStream.on('error', err => callback(err))
-    outputStream.on('end', function () {
-      let object
-      logger.debug({ projectId, docId, packId }, 'download from s3 completed')
-      try {
-        object = JSON.parse(parts.join(''))
-      } catch (e) {
-        return callback(e)
-      }
-      object._id = ObjectId(object._id)
-      object.doc_id = ObjectId(object.doc_id)
-      object.project_id = ObjectId(object.project_id)
-      for (const op of Array.from(object.pack)) {
-        if (op._id != null) {
-          op._id = ObjectId(op._id)
-        }
-      }
-      return callback(null, object)
-    })
-    return outputStream.on('data', data => parts.push(data))
-  },
-
-  unArchivePack(projectId, docId, packId, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    return MongoAWS.readArchivedPack(
-      projectId,
-      docId,
-      packId,
-      function (err, object) {
-        if (err != null) {
-          return callback(err)
-        }
-        Metrics.inc('unarchive-pack')
-        // allow the object to expire, we can always retrieve it again
-        object.expiresAt = new Date(Date.now() + 7 * DAYS)
-        logger.debug({ projectId, docId, packId }, 'inserting object from s3')
-        return db.docHistory.insertOne(object, (err, confirmation) => {
-          if (err) return callback(err)
-          object._id = confirmation.insertedId
-          callback(null, object)
-        })
-      }
-    )
-  },
-}

+ 0 - 200
services/track-changes/app/js/MongoManager.js

@@ -1,200 +0,0 @@
-/* eslint-disable
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-let MongoManager
-const { db, ObjectId } = require('./mongodb')
-const PackManager = require('./PackManager')
-const async = require('async')
-const _ = require('underscore')
-const metrics = require('@overleaf/metrics')
-const logger = require('@overleaf/logger')
-
-module.exports = MongoManager = {
-  getLastCompressedUpdate(docId, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    return db.docHistory
-      .find(
-        { doc_id: ObjectId(docId.toString()) },
-        // only return the last entry in a pack
-        { projection: { pack: { $slice: -1 } } }
-      )
-      .sort({ v: -1 })
-      .limit(1)
-      .toArray(function (error, compressedUpdates) {
-        if (error != null) {
-          return callback(error)
-        }
-        return callback(null, compressedUpdates[0] || null)
-      })
-  },
-
-  peekLastCompressedUpdate(docId, callback) {
-    // under normal use we pass back the last update as
-    // callback(null,update,version).
-    //
-    // when we have an existing last update but want to force a new one
-    // to start, we pass it back as callback(null,null,version), just
-    // giving the version so we can check consistency.
-    if (callback == null) {
-      callback = function () {}
-    }
-    return MongoManager.getLastCompressedUpdate(
-      docId,
-      function (error, update) {
-        if (error != null) {
-          return callback(error)
-        }
-        if (update != null) {
-          if (update.broken) {
-            // marked as broken so we will force a new op
-            return callback(null, null)
-          } else if (update.pack != null) {
-            if (update.finalised) {
-              // no more ops can be appended
-              return callback(
-                null,
-                null,
-                update.pack[0] != null ? update.pack[0].v : undefined
-              )
-            } else {
-              return callback(
-                null,
-                update,
-                update.pack[0] != null ? update.pack[0].v : undefined
-              )
-            }
-          } else {
-            return callback(null, update, update.v)
-          }
-        } else {
-          return PackManager.getLastPackFromIndex(
-            docId,
-            function (error, pack) {
-              if (error != null) {
-                return callback(error)
-              }
-              if (
-                (pack != null ? pack.inS3 : undefined) != null &&
-                (pack != null ? pack.v_end : undefined) != null
-              ) {
-                return callback(null, null, pack.v_end)
-              }
-              return callback(null, null)
-            }
-          )
-        }
-      }
-    )
-  },
-
-  backportProjectId(projectId, docId, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    return db.docHistory.updateMany(
-      {
-        doc_id: ObjectId(docId.toString()),
-        project_id: { $exists: false },
-      },
-      {
-        $set: { project_id: ObjectId(projectId.toString()) },
-      },
-      callback
-    )
-  },
-
-  getProjectMetaData(projectId, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    return db.projectHistoryMetaData.findOne(
-      {
-        project_id: ObjectId(projectId.toString()),
-      },
-      callback
-    )
-  },
-
-  setProjectMetaData(projectId, metadata, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    return db.projectHistoryMetaData.updateOne(
-      {
-        project_id: ObjectId(projectId),
-      },
-      {
-        $set: metadata,
-      },
-      {
-        upsert: true,
-      },
-      callback
-    )
-  },
-
-  upgradeHistory(projectId, callback) {
-    // preserve the project's existing history
-    if (callback == null) {
-      callback = function () {}
-    }
-    return db.docHistory.updateMany(
-      {
-        project_id: ObjectId(projectId),
-        temporary: true,
-        expiresAt: { $exists: true },
-      },
-      {
-        $set: { temporary: false },
-        $unset: { expiresAt: '' },
-      },
-      callback
-    )
-  },
-
-  ensureIndices() {
-    // For finding all updates that go into a diff for a doc
-    db.docHistory.ensureIndex({ doc_id: 1, v: 1 }, { background: true })
-    // For finding all updates that affect a project
-    db.docHistory.ensureIndex(
-      { project_id: 1, 'meta.end_ts': 1 },
-      { background: true }
-    )
-    // For finding updates that don't yet have a project_id and need it inserting
-    db.docHistory.ensureIndex(
-      { doc_id: 1, project_id: 1 },
-      { background: true }
-    )
-    // For finding project meta-data
-    db.projectHistoryMetaData.ensureIndex(
-      { project_id: 1 },
-      { background: true }
-    )
-    // TTL index for auto deleting week old temporary ops
-    db.docHistory.ensureIndex(
-      { expiresAt: 1 },
-      { expireAfterSeconds: 0, background: true }
-    )
-    // For finding packs to be checked for archiving
-    db.docHistory.ensureIndex({ last_checked: 1 }, { background: true })
-    // For finding archived packs
-    return db.docHistoryIndex.ensureIndex(
-      { project_id: 1 },
-      { background: true }
-    )
-  },
-}
-;['getLastCompressedUpdate', 'getProjectMetaData', 'setProjectMetaData'].map(
-  method =>
-    metrics.timeAsyncMethod(MongoManager, method, 'mongo.MongoManager', logger)
-)

+ 0 - 1202
services/track-changes/app/js/PackManager.js

@@ -1,1202 +0,0 @@
-/* eslint-disable
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS101: Remove unnecessary use of Array.from
- * DS102: Remove unnecessary code created because of implicit returns
- * DS205: Consider reworking code to avoid use of IIFEs
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-let PackManager
-const async = require('async')
-const _ = require('underscore')
-const Bson = require('bson')
-const BSON = new Bson()
-const { db, ObjectId } = require('./mongodb')
-const logger = require('@overleaf/logger')
-const LockManager = require('./LockManager')
-const MongoAWS = require('./MongoAWS')
-const Metrics = require('@overleaf/metrics')
-const ProjectIterator = require('./ProjectIterator')
-const DocIterator = require('./DocIterator')
-const Settings = require('@overleaf/settings')
-const util = require('util')
-const keys = Settings.redis.lock.key_schema
-
-// Sharejs operations are stored in a 'pack' object
-//
-//  e.g.  a single sharejs update looks like
-//
-//   {
-//     "doc_id" : 549dae9e0a2a615c0c7f0c98,
-//     "project_id" : 549dae9c0a2a615c0c7f0c8c,
-//     "op" : [ {"p" : 6981,	"d" : "?"	} ],
-//     "meta" : {	"user_id" : 52933..., "start_ts" : 1422310693931,	"end_ts" : 1422310693931 },
-//     "v" : 17082
-//   }
-//
-//  and a pack looks like this
-//
-//   {
-//     "doc_id" : 549dae9e0a2a615c0c7f0c98,
-//     "project_id" : 549dae9c0a2a615c0c7f0c8c,
-//     "pack" : [ U1, U2, U3, ...., UN],
-//     "meta" : {	"user_id" : 52933..., "start_ts" : 1422310693931,	"end_ts" : 1422310693931 },
-//     "v" : 17082
-//     "v_end" : ...
-//   }
-//
-//  where U1, U2, U3, .... are single updates stripped of their
-//  doc_id and project_id fields (which are the same for all the
-//  updates in the pack).
-//
-//  The pack itself has v and meta fields, this makes it possible to
-//  treat packs and single updates in a similar way.
-//
-//  The v field of the pack itself is from the first entry U1, the
-//  v_end field from UN.  The meta.end_ts field of the pack itself is
-//  from the last entry UN, the meta.start_ts field from U1.
-
-const DAYS = 24 * 3600 * 1000 // one day in milliseconds
-
-module.exports = PackManager = {
-  MAX_SIZE: 1024 * 1024, // make these configurable parameters
-  MAX_COUNT: 1024,
-
-  insertCompressedUpdates(
-    projectId,
-    docId,
-    lastUpdate,
-    newUpdates,
-    temporary,
-    callback
-  ) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    if (newUpdates.length === 0) {
-      return callback()
-    }
-
-    // never append permanent ops to a pack that will expire
-    if (
-      (lastUpdate != null ? lastUpdate.expiresAt : undefined) != null &&
-      !temporary
-    ) {
-      lastUpdate = null
-    }
-
-    const updatesToFlush = []
-    const updatesRemaining = newUpdates.slice()
-
-    let n = (lastUpdate != null ? lastUpdate.n : undefined) || 0
-    let sz = (lastUpdate != null ? lastUpdate.sz : undefined) || 0
-
-    while (
-      updatesRemaining.length &&
-      n < PackManager.MAX_COUNT &&
-      sz < PackManager.MAX_SIZE
-    ) {
-      const nextUpdate = updatesRemaining[0]
-      const nextUpdateSize = BSON.calculateObjectSize(nextUpdate)
-      if (nextUpdateSize + sz > PackManager.MAX_SIZE && n > 0) {
-        break
-      }
-      n++
-      sz += nextUpdateSize
-      updatesToFlush.push(updatesRemaining.shift())
-    }
-
-    return PackManager.flushCompressedUpdates(
-      projectId,
-      docId,
-      lastUpdate,
-      updatesToFlush,
-      temporary,
-      function (error) {
-        if (error != null) {
-          return callback(error)
-        }
-        return PackManager.insertCompressedUpdates(
-          projectId,
-          docId,
-          null,
-          updatesRemaining,
-          temporary,
-          callback
-        )
-      }
-    )
-  },
-
-  flushCompressedUpdates(
-    projectId,
-    docId,
-    lastUpdate,
-    newUpdates,
-    temporary,
-    callback
-  ) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    if (newUpdates.length === 0) {
-      return callback()
-    }
-
-    let canAppend = false
-    // check if it is safe to append to an existing pack
-    if (lastUpdate != null) {
-      if (!temporary && lastUpdate.expiresAt == null) {
-        // permanent pack appends to permanent pack
-        canAppend = true
-      }
-      const age =
-        Date.now() -
-        (lastUpdate.meta != null ? lastUpdate.meta.start_ts : undefined)
-      if (temporary && lastUpdate.expiresAt != null && age < 1 * DAYS) {
-        // temporary pack appends to temporary pack if same day
-        canAppend = true
-      }
-    }
-
-    if (canAppend) {
-      return PackManager.appendUpdatesToExistingPack(
-        projectId,
-        docId,
-        lastUpdate,
-        newUpdates,
-        temporary,
-        callback
-      )
-    } else {
-      return PackManager.insertUpdatesIntoNewPack(
-        projectId,
-        docId,
-        newUpdates,
-        temporary,
-        callback
-      )
-    }
-  },
-
-  insertUpdatesIntoNewPack(projectId, docId, newUpdates, temporary, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    const first = newUpdates[0]
-    const last = newUpdates[newUpdates.length - 1]
-    const n = newUpdates.length
-    const sz = BSON.calculateObjectSize(newUpdates)
-    const newPack = {
-      project_id: ObjectId(projectId.toString()),
-      doc_id: ObjectId(docId.toString()),
-      pack: newUpdates,
-      n,
-      sz,
-      meta: {
-        start_ts: first.meta.start_ts,
-        end_ts: last.meta.end_ts,
-      },
-      v: first.v,
-      v_end: last.v,
-      temporary,
-    }
-    if (temporary) {
-      newPack.expiresAt = new Date(Date.now() + 7 * DAYS)
-      newPack.last_checked = new Date(Date.now() + 30 * DAYS) // never check temporary packs
-    }
-    logger.debug(
-      { projectId, docId, newUpdates },
-      'inserting updates into new pack'
-    )
-    return db.docHistory.insertOne(newPack, function (err) {
-      if (err != null) {
-        return callback(err)
-      }
-      Metrics.inc(`insert-pack-${temporary ? 'temporary' : 'permanent'}`)
-      if (temporary) {
-        return callback()
-      } else {
-        return PackManager.updateIndex(projectId, docId, callback)
-      }
-    })
-  },
-
-  appendUpdatesToExistingPack(
-    projectId,
-    docId,
-    lastUpdate,
-    newUpdates,
-    temporary,
-    callback
-  ) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    const first = newUpdates[0]
-    const last = newUpdates[newUpdates.length - 1]
-    const n = newUpdates.length
-    const sz = BSON.calculateObjectSize(newUpdates)
-    const query = {
-      _id: lastUpdate._id,
-      project_id: ObjectId(projectId.toString()),
-      doc_id: ObjectId(docId.toString()),
-      pack: { $exists: true },
-    }
-    const update = {
-      $push: {
-        pack: { $each: newUpdates },
-      },
-      $inc: {
-        n,
-        sz,
-      },
-      $set: {
-        'meta.end_ts': last.meta.end_ts,
-        v_end: last.v,
-      },
-    }
-    if (lastUpdate.expiresAt && temporary) {
-      update.$set.expiresAt = new Date(Date.now() + 7 * DAYS)
-    }
-    logger.debug(
-      { projectId, docId, lastUpdate, newUpdates },
-      'appending updates to existing pack'
-    )
-    Metrics.inc(`append-pack-${temporary ? 'temporary' : 'permanent'}`)
-    return db.docHistory.updateOne(query, update, callback)
-  },
-
-  // Retrieve all changes for a document
-
-  getOpsByVersionRange(projectId, docId, fromVersion, toVersion, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    return PackManager.loadPacksByVersionRange(
-      projectId,
-      docId,
-      fromVersion,
-      toVersion,
-      function (error) {
-        if (error) return callback(error)
-        const query = { doc_id: ObjectId(docId.toString()) }
-        if (toVersion != null) {
-          query.v = { $lte: toVersion }
-        }
-        if (fromVersion != null) {
-          query.v_end = { $gte: fromVersion }
-        }
-        // console.log "query:", query
-        return db.docHistory
-          .find(query)
-          .sort({ v: -1 })
-          .toArray(function (err, result) {
-            if (err != null) {
-              return callback(err)
-            }
-            // console.log "getOpsByVersionRange:", err, result
-            const updates = []
-            const opInRange = function (op, from, to) {
-              if (fromVersion != null && op.v < fromVersion) {
-                return false
-              }
-              if (toVersion != null && op.v > toVersion) {
-                return false
-              }
-              return true
-            }
-            for (const docHistory of Array.from(result)) {
-              // console.log 'adding', docHistory.pack
-              for (const op of Array.from(docHistory.pack.reverse())) {
-                if (opInRange(op, fromVersion, toVersion)) {
-                  op.project_id = docHistory.project_id
-                  op.doc_id = docHistory.doc_id
-                  // console.log "added op", op.v, fromVersion, toVersion
-                  updates.push(op)
-                }
-              }
-            }
-            return callback(null, updates)
-          })
-      }
-    )
-  },
-
-  loadPacksByVersionRange(projectId, docId, fromVersion, toVersion, callback) {
-    return PackManager.getIndex(docId, function (err, indexResult) {
-      let pack
-      if (err != null) {
-        return callback(err)
-      }
-      const indexPacks =
-        (indexResult != null ? indexResult.packs : undefined) || []
-      const packInRange = function (pack, from, to) {
-        if (fromVersion != null && pack.v_end < fromVersion) {
-          return false
-        }
-        if (toVersion != null && pack.v > toVersion) {
-          return false
-        }
-        return true
-      }
-      const neededIds = (() => {
-        const result = []
-        for (pack of Array.from(indexPacks)) {
-          if (packInRange(pack, fromVersion, toVersion)) {
-            result.push(pack._id)
-          }
-        }
-        return result
-      })()
-      if (neededIds.length) {
-        return PackManager.fetchPacksIfNeeded(
-          projectId,
-          docId,
-          neededIds,
-          callback
-        )
-      } else {
-        return callback()
-      }
-    })
-  },
-
-  fetchPacksIfNeeded(projectId, docId, packIds, callback) {
-    let id
-    return db.docHistory
-      .find({ _id: { $in: packIds.map(ObjectId) } }, { projection: { _id: 1 } })
-      .toArray(function (err, loadedPacks) {
-        if (err != null) {
-          return callback(err)
-        }
-        const allPackIds = (() => {
-          const result1 = []
-          for (id of Array.from(packIds)) {
-            result1.push(id.toString())
-          }
-          return result1
-        })()
-        const loadedPackIds = Array.from(loadedPacks).map(pack =>
-          pack._id.toString()
-        )
-        const packIdsToFetch = _.difference(allPackIds, loadedPackIds)
-        logger.debug(
-          { projectId, docId, loadedPackIds, allPackIds, packIdsToFetch },
-          'analysed packs'
-        )
-        if (packIdsToFetch.length === 0) {
-          return callback()
-        }
-        return async.eachLimit(
-          packIdsToFetch,
-          4,
-          (packId, cb) => MongoAWS.unArchivePack(projectId, docId, packId, cb),
-          function (err) {
-            if (err != null) {
-              return callback(err)
-            }
-            logger.debug({ projectId, docId }, 'done unarchiving')
-            return callback()
-          }
-        )
-      })
-  },
-
-  findAllDocsInProject(projectId, callback) {
-    const docIdSet = new Set()
-    async.series(
-      [
-        cb => {
-          db.docHistory
-            .find(
-              { project_id: ObjectId(projectId) },
-              { projection: { pack: false } }
-            )
-            .toArray((err, packs) => {
-              if (err) return callback(err)
-              packs.forEach(pack => {
-                docIdSet.add(pack.doc_id.toString())
-              })
-              return cb()
-            })
-        },
-        cb => {
-          db.docHistoryIndex
-            .find({ project_id: ObjectId(projectId) })
-            .toArray((err, indexes) => {
-              if (err) return callback(err)
-              indexes.forEach(index => {
-                docIdSet.add(index._id.toString())
-              })
-              return cb()
-            })
-        },
-      ],
-      err => {
-        if (err) return callback(err)
-        callback(null, [...docIdSet])
-      }
-    )
-  },
-
-  // rewrite any query using doc_id to use _id instead
-  // (because docHistoryIndex uses the doc_id)
-
-  _rewriteQueryForIndex(query) {
-    const indexQuery = _.omit(query, 'doc_id')
-    if ('doc_id' in query) {
-      indexQuery._id = query.doc_id
-    }
-    return indexQuery
-  },
-
-  // Retrieve all changes across a project
-
-  _findPacks(query, sortKeys, callback) {
-    // get all the docHistory Entries
-    return db.docHistory
-      .find(query, { projection: { pack: false } })
-      .sort(sortKeys)
-      .toArray(function (err, packs) {
-        let pack
-        if (err != null) {
-          return callback(err)
-        }
-        const allPacks = []
-        const seenIds = {}
-        for (pack of Array.from(packs)) {
-          allPacks.push(pack)
-          seenIds[pack._id] = true
-        }
-        const indexQuery = PackManager._rewriteQueryForIndex(query)
-        return db.docHistoryIndex
-          .find(indexQuery)
-          .toArray(function (err, indexes) {
-            if (err != null) {
-              return callback(err)
-            }
-            for (const index of Array.from(indexes)) {
-              for (pack of Array.from(index.packs)) {
-                if (!seenIds[pack._id]) {
-                  pack.project_id = index.project_id
-                  pack.doc_id = index._id
-                  pack.fromIndex = true
-                  allPacks.push(pack)
-                  seenIds[pack._id] = true
-                }
-              }
-            }
-            return callback(null, allPacks)
-          })
-      })
-  },
-
-  makeProjectIterator(projectId, before, callback) {
-    PackManager._findPacks(
-      { project_id: ObjectId(projectId) },
-      { 'meta.end_ts': -1 },
-      function (err, allPacks) {
-        if (err) return callback(err)
-        callback(
-          null,
-          new ProjectIterator(allPacks, before, PackManager.getPackById)
-        )
-      }
-    )
-  },
-
-  makeDocIterator(docId, callback) {
-    PackManager._findPacks(
-      { doc_id: ObjectId(docId) },
-      { v: -1 },
-      function (err, allPacks) {
-        if (err) return callback(err)
-        callback(null, new DocIterator(allPacks, PackManager.getPackById))
-      }
-    )
-  },
-
-  getPackById(projectId, docId, packId, callback) {
-    return db.docHistory.findOne({ _id: packId }, function (err, pack) {
-      if (err != null) {
-        return callback(err)
-      }
-      if (pack == null) {
-        return MongoAWS.unArchivePack(projectId, docId, packId, callback)
-      } else if (pack.expiresAt != null && pack.temporary === false) {
-        // we only need to touch the TTL when listing the changes in the project
-        // because diffs on individual documents are always done after that
-        return PackManager.increaseTTL(pack, callback)
-        // only do this for cached packs, not temporary ones to avoid older packs
-        // being kept longer than newer ones (which messes up the last update version)
-      } else {
-        return callback(null, pack)
-      }
-    })
-  },
-
-  increaseTTL(pack, callback) {
-    if (pack.expiresAt < new Date(Date.now() + 6 * DAYS)) {
-      // update cache expiry since we are using this pack
-      return db.docHistory.updateOne(
-        { _id: pack._id },
-        { $set: { expiresAt: new Date(Date.now() + 7 * DAYS) } },
-        err => callback(err, pack)
-      )
-    } else {
-      return callback(null, pack)
-    }
-  },
-
-  // Manage docHistoryIndex collection
-
-  getIndex(docId, callback) {
-    return db.docHistoryIndex.findOne(
-      { _id: ObjectId(docId.toString()) },
-      callback
-    )
-  },
-
-  getPackFromIndex(docId, packId, callback) {
-    return db.docHistoryIndex.findOne(
-      { _id: ObjectId(docId.toString()), 'packs._id': packId },
-      { projection: { 'packs.$': 1 } },
-      callback
-    )
-  },
-
-  getLastPackFromIndex(docId, callback) {
-    return db.docHistoryIndex.findOne(
-      { _id: ObjectId(docId.toString()) },
-      { projection: { packs: { $slice: -1 } } },
-      function (err, indexPack) {
-        if (err != null) {
-          return callback(err)
-        }
-        if (indexPack == null) {
-          return callback()
-        }
-        return callback(null, indexPack[0])
-      }
-    )
-  },
-
-  getIndexWithKeys(docId, callback) {
-    return PackManager.getIndex(docId, function (err, index) {
-      if (err != null) {
-        return callback(err)
-      }
-      if (index == null) {
-        return callback()
-      }
-      for (const pack of Array.from(
-        (index != null ? index.packs : undefined) || []
-      )) {
-        index[pack._id] = pack
-      }
-      return callback(null, index)
-    })
-  },
-
-  initialiseIndex(projectId, docId, callback) {
-    return PackManager.findCompletedPacks(
-      projectId,
-      docId,
-      function (err, packs) {
-        // console.log 'err', err, 'packs', packs, packs?.length
-        if (err != null) {
-          return callback(err)
-        }
-        if (packs == null) {
-          return callback()
-        }
-        return PackManager.insertPacksIntoIndexWithLock(
-          projectId,
-          docId,
-          packs,
-          callback
-        )
-      }
-    )
-  },
-
-  updateIndex(projectId, docId, callback) {
-    // find all packs prior to current pack
-    return PackManager.findUnindexedPacks(
-      projectId,
-      docId,
-      function (err, newPacks) {
-        if (err != null) {
-          return callback(err)
-        }
-        if (newPacks == null || newPacks.length === 0) {
-          return callback()
-        }
-        return PackManager.insertPacksIntoIndexWithLock(
-          projectId,
-          docId,
-          newPacks,
-          function (err) {
-            if (err != null) {
-              return callback(err)
-            }
-            logger.debug(
-              { projectId, docId, newPacks },
-              'added new packs to index'
-            )
-            return callback()
-          }
-        )
-      }
-    )
-  },
-
-  findCompletedPacks(projectId, docId, callback) {
-    const query = {
-      doc_id: ObjectId(docId.toString()),
-      expiresAt: { $exists: false },
-    }
-    return db.docHistory
-      .find(query, { projection: { pack: false } })
-      .sort({ v: 1 })
-      .toArray(function (err, packs) {
-        if (err != null) {
-          return callback(err)
-        }
-        if (packs == null) {
-          return callback()
-        }
-        if (!(packs != null ? packs.length : undefined)) {
-          return callback()
-        }
-        const last = packs.pop() // discard the last pack, if it's still in progress
-        if (last.finalised) {
-          packs.push(last)
-        } // it's finalised so we push it back to archive it
-        return callback(null, packs)
-      })
-  },
-
-  findPacks(projectId, docId, callback) {
-    const query = {
-      doc_id: ObjectId(docId.toString()),
-      expiresAt: { $exists: false },
-    }
-    return db.docHistory
-      .find(query, { projection: { pack: false } })
-      .sort({ v: 1 })
-      .toArray(function (err, packs) {
-        if (err != null) {
-          return callback(err)
-        }
-        if (packs == null) {
-          return callback()
-        }
-        if (!(packs != null ? packs.length : undefined)) {
-          return callback()
-        }
-        return callback(null, packs)
-      })
-  },
-
-  findUnindexedPacks(projectId, docId, callback) {
-    return PackManager.getIndexWithKeys(docId, function (err, indexResult) {
-      if (err != null) {
-        return callback(err)
-      }
-      return PackManager.findCompletedPacks(
-        projectId,
-        docId,
-        function (err, historyPacks) {
-          let pack
-          if (err != null) {
-            return callback(err)
-          }
-          if (historyPacks == null) {
-            return callback()
-          }
-          // select only the new packs not already in the index
-          let newPacks = (() => {
-            const result = []
-            for (pack of Array.from(historyPacks)) {
-              if (
-                (indexResult != null ? indexResult[pack._id] : undefined) ==
-                null
-              ) {
-                result.push(pack)
-              }
-            }
-            return result
-          })()
-          newPacks = (() => {
-            const result1 = []
-            for (pack of Array.from(newPacks)) {
-              result1.push(
-                _.omit(
-                  pack,
-                  'doc_id',
-                  'project_id',
-                  'n',
-                  'sz',
-                  'last_checked',
-                  'finalised'
-                )
-              )
-            }
-            return result1
-          })()
-          if (newPacks.length) {
-            logger.debug(
-              { projectId, docId, n: newPacks.length },
-              'found new packs'
-            )
-          }
-          return callback(null, newPacks)
-        }
-      )
-    })
-  },
-
-  insertPacksIntoIndexWithLock(projectId, docId, newPacks, callback) {
-    return LockManager.runWithLock(
-      keys.historyIndexLock({ doc_id: docId }),
-      releaseLock =>
-        PackManager._insertPacksIntoIndex(
-          projectId,
-          docId,
-          newPacks,
-          releaseLock
-        ),
-      callback
-    )
-  },
-
-  _insertPacksIntoIndex(projectId, docId, newPacks, callback) {
-    return db.docHistoryIndex.updateOne(
-      { _id: ObjectId(docId.toString()) },
-      {
-        $setOnInsert: { project_id: ObjectId(projectId.toString()) },
-        $push: {
-          packs: { $each: newPacks, $sort: { v: 1 } },
-        },
-      },
-      {
-        upsert: true,
-      },
-      callback
-    )
-  },
-
-  // Archiving packs to S3
-
-  archivePack(projectId, docId, packId, callback) {
-    const clearFlagOnError = function (err, cb) {
-      if (err != null) {
-        // clear the inS3 flag on error
-        return PackManager.clearPackAsArchiveInProgress(
-          projectId,
-          docId,
-          packId,
-          function (err2) {
-            if (err2 != null) {
-              return cb(err2)
-            }
-            return cb(err)
-          }
-        )
-      } else {
-        return cb()
-      }
-    }
-    return async.series(
-      [
-        cb =>
-          PackManager.checkArchiveNotInProgress(projectId, docId, packId, cb),
-        cb =>
-          PackManager.markPackAsArchiveInProgress(projectId, docId, packId, cb),
-        cb =>
-          MongoAWS.archivePack(projectId, docId, packId, err =>
-            clearFlagOnError(err, cb)
-          ),
-        cb =>
-          PackManager.checkArchivedPack(projectId, docId, packId, err =>
-            clearFlagOnError(err, cb)
-          ),
-        cb => PackManager.markPackAsArchived(projectId, docId, packId, cb),
-        cb =>
-          PackManager.setTTLOnArchivedPack(projectId, docId, packId, callback),
-      ],
-      callback
-    )
-  },
-
-  checkArchivedPack(projectId, docId, packId, callback) {
-    return db.docHistory.findOne({ _id: packId }, function (err, pack) {
-      if (err != null) {
-        return callback(err)
-      }
-      if (pack == null) {
-        return callback(new Error('pack not found'))
-      }
-      return MongoAWS.readArchivedPack(
-        projectId,
-        docId,
-        packId,
-        function (err, result) {
-          if (err) return callback(err)
-          delete result.last_checked
-          delete pack.last_checked
-          // need to compare ids as ObjectIds with .equals()
-          for (const key of ['_id', 'project_id', 'doc_id']) {
-            if (result[key].equals(pack[key])) {
-              result[key] = pack[key]
-            }
-          }
-          for (let i = 0; i < result.pack.length; i++) {
-            const op = result.pack[i]
-            if (op._id != null && op._id.equals(pack.pack[i]._id)) {
-              op._id = pack.pack[i]._id
-            }
-          }
-          if (_.isEqual(pack, result)) {
-            return callback()
-          } else {
-            logger.err(
-              {
-                pack,
-                result,
-                jsondiff: JSON.stringify(pack) === JSON.stringify(result),
-              },
-              'difference when comparing packs'
-            )
-            return callback(
-              new Error('pack retrieved from s3 does not match pack in mongo')
-            )
-          }
-        }
-      )
-    })
-  },
-  // Extra methods to test archive/unarchive for a doc_id
-
-  pushOldPacks(projectId, docId, callback) {
-    return PackManager.findPacks(projectId, docId, function (err, packs) {
-      if (err != null) {
-        return callback(err)
-      }
-      if (!(packs != null ? packs.length : undefined)) {
-        return callback()
-      }
-      return PackManager.processOldPack(
-        projectId,
-        docId,
-        packs[0]._id,
-        callback
-      )
-    })
-  },
-
-  pullOldPacks(projectId, docId, callback) {
-    return PackManager.loadPacksByVersionRange(
-      projectId,
-      docId,
-      null,
-      null,
-      callback
-    )
-  },
-
-  // Processing old packs via worker
-
-  processOldPack(projectId, docId, packId, callback) {
-    const markAsChecked = err =>
-      PackManager.markPackAsChecked(projectId, docId, packId, function (err2) {
-        if (err2 != null) {
-          return callback(err2)
-        }
-        return callback(err)
-      })
-    logger.debug({ projectId, docId }, 'processing old packs')
-    return db.docHistory.findOne({ _id: packId }, function (err, pack) {
-      if (err != null) {
-        return markAsChecked(err)
-      }
-      if (pack == null) {
-        return markAsChecked()
-      }
-      if (pack.expiresAt != null) {
-        return callback()
-      } // return directly
-      return PackManager.finaliseIfNeeded(
-        projectId,
-        docId,
-        pack._id,
-        pack,
-        function (err) {
-          if (err != null) {
-            return markAsChecked(err)
-          }
-          return PackManager.updateIndexIfNeeded(
-            projectId,
-            docId,
-            function (err) {
-              if (err != null) {
-                return markAsChecked(err)
-              }
-              return PackManager.findUnarchivedPacks(
-                projectId,
-                docId,
-                function (err, unarchivedPacks) {
-                  if (err != null) {
-                    return markAsChecked(err)
-                  }
-                  if (
-                    !(unarchivedPacks != null
-                      ? unarchivedPacks.length
-                      : undefined)
-                  ) {
-                    logger.debug(
-                      { projectId, docId },
-                      'no packs need archiving'
-                    )
-                    return markAsChecked()
-                  }
-                  return async.eachSeries(
-                    unarchivedPacks,
-                    (pack, cb) =>
-                      PackManager.archivePack(projectId, docId, pack._id, cb),
-                    function (err) {
-                      if (err != null) {
-                        return markAsChecked(err)
-                      }
-                      logger.debug({ projectId, docId }, 'done processing')
-                      return markAsChecked()
-                    }
-                  )
-                }
-              )
-            }
-          )
-        }
-      )
-    })
-  },
-
-  finaliseIfNeeded(projectId, docId, packId, pack, callback) {
-    const sz = pack.sz / (1024 * 1024) // in fractions of a megabyte
-    const n = pack.n / 1024 // in fraction of 1024 ops
-    const age = (Date.now() - pack.meta.end_ts) / DAYS
-    if (age < 30) {
-      // always keep if less than 1 month old
-      logger.debug({ projectId, docId, packId, age }, 'less than 30 days old')
-      return callback()
-    }
-    // compute an archiving threshold which decreases for each month of age
-    const archiveThreshold = 30 / age
-    if (sz > archiveThreshold || n > archiveThreshold || age > 90) {
-      logger.debug(
-        { projectId, docId, packId, age, archiveThreshold, sz, n },
-        'meets archive threshold'
-      )
-      return PackManager.markPackAsFinalisedWithLock(
-        projectId,
-        docId,
-        packId,
-        callback
-      )
-    } else {
-      logger.debug(
-        { projectId, docId, packId, age, archiveThreshold, sz, n },
-        'does not meet archive threshold'
-      )
-      return callback()
-    }
-  },
-
-  markPackAsFinalisedWithLock(projectId, docId, packId, callback) {
-    return LockManager.runWithLock(
-      keys.historyLock({ doc_id: docId }),
-      releaseLock =>
-        PackManager._markPackAsFinalised(projectId, docId, packId, releaseLock),
-      callback
-    )
-  },
-
-  _markPackAsFinalised(projectId, docId, packId, callback) {
-    logger.debug({ projectId, docId, packId }, 'marking pack as finalised')
-    return db.docHistory.updateOne(
-      { _id: packId },
-      { $set: { finalised: true } },
-      callback
-    )
-  },
-
-  updateIndexIfNeeded(projectId, docId, callback) {
-    logger.debug({ projectId, docId }, 'archiving old packs')
-    return PackManager.getIndexWithKeys(docId, function (err, index) {
-      if (err != null) {
-        return callback(err)
-      }
-      if (index == null) {
-        return PackManager.initialiseIndex(projectId, docId, callback)
-      } else {
-        return PackManager.updateIndex(projectId, docId, callback)
-      }
-    })
-  },
-
-  markPackAsChecked(projectId, docId, packId, callback) {
-    logger.debug({ projectId, docId, packId }, 'marking pack as checked')
-    return db.docHistory.updateOne(
-      { _id: packId },
-      { $currentDate: { last_checked: true } },
-      callback
-    )
-  },
-
-  findUnarchivedPacks(projectId, docId, callback) {
-    return PackManager.getIndex(docId, function (err, indexResult) {
-      if (err != null) {
-        return callback(err)
-      }
-      const indexPacks =
-        (indexResult != null ? indexResult.packs : undefined) || []
-      const unArchivedPacks = (() => {
-        const result = []
-        for (const pack of Array.from(indexPacks)) {
-          if (pack.inS3 == null) {
-            result.push(pack)
-          }
-        }
-        return result
-      })()
-      if (unArchivedPacks.length) {
-        logger.debug(
-          { projectId, docId, n: unArchivedPacks.length },
-          'find unarchived packs'
-        )
-      }
-      return callback(null, unArchivedPacks)
-    })
-  },
-
-  // Archive locking flags
-
-  checkArchiveNotInProgress(projectId, docId, packId, callback) {
-    logger.debug(
-      { projectId, docId, packId },
-      'checking if archive in progress'
-    )
-    return PackManager.getPackFromIndex(docId, packId, function (err, result) {
-      if (err != null) {
-        return callback(err)
-      }
-      if (result == null) {
-        return callback(new Error('pack not found in index'))
-      }
-      if (result.inS3) {
-        return callback(new Error('pack archiving already done'))
-      } else if (result.inS3 != null) {
-        return callback(new Error('pack archiving already in progress'))
-      } else {
-        return callback()
-      }
-    })
-  },
-
-  markPackAsArchiveInProgress(projectId, docId, packId, callback) {
-    logger.debug(
-      { projectId, docId },
-      'marking pack as archive in progress status'
-    )
-    return db.docHistoryIndex.findOneAndUpdate(
-      {
-        _id: ObjectId(docId.toString()),
-        packs: { $elemMatch: { _id: packId, inS3: { $exists: false } } },
-      },
-      { $set: { 'packs.$.inS3': false } },
-      { projection: { 'packs.$': 1 } },
-      function (err, result) {
-        if (err != null) {
-          return callback(err)
-        }
-        if (!result.value) {
-          return callback(new Error('archive is already in progress'))
-        }
-        logger.debug(
-          { projectId, docId, packId },
-          'marked as archive in progress'
-        )
-        return callback()
-      }
-    )
-  },
-
-  clearPackAsArchiveInProgress(projectId, docId, packId, callback) {
-    logger.debug(
-      { projectId, docId, packId },
-      'clearing as archive in progress'
-    )
-    return db.docHistoryIndex.updateOne(
-      {
-        _id: ObjectId(docId.toString()),
-        packs: { $elemMatch: { _id: packId, inS3: false } },
-      },
-      { $unset: { 'packs.$.inS3': true } },
-      callback
-    )
-  },
-
-  markPackAsArchived(projectId, docId, packId, callback) {
-    logger.debug({ projectId, docId, packId }, 'marking pack as archived')
-    return db.docHistoryIndex.findOneAndUpdate(
-      {
-        _id: ObjectId(docId.toString()),
-        packs: { $elemMatch: { _id: packId, inS3: false } },
-      },
-      { $set: { 'packs.$.inS3': true } },
-      { projection: { 'packs.$': 1 } },
-      function (err, result) {
-        if (err != null) {
-          return callback(err)
-        }
-        if (!result.value) {
-          return callback(new Error('archive is not marked as progress'))
-        }
-        logger.debug({ projectId, docId, packId }, 'marked as archived')
-        return callback()
-      }
-    )
-  },
-
-  setTTLOnArchivedPack(projectId, docId, packId, callback) {
-    return db.docHistory.updateOne(
-      { _id: packId },
-      { $set: { expiresAt: new Date(Date.now() + 1 * DAYS) } },
-      function (err) {
-        if (err) {
-          return callback(err)
-        }
-        logger.debug({ projectId, docId, packId }, 'set expiry on pack')
-        return callback()
-      }
-    )
-  },
-}
-
-module.exports.promises = {
-  getOpsByVersionRange: util.promisify(PackManager.getOpsByVersionRange),
-  findAllDocsInProject: util.promisify(PackManager.findAllDocsInProject),
-  makeDocIterator: util.promisify(PackManager.makeDocIterator),
-}
-
-//	_getOneDayInFutureWithRandomDelay: ->
-//		thirtyMins = 1000 * 60 * 30
-//		randomThirtyMinMax = Math.ceil(Math.random() * thirtyMins)
-//		return new Date(Date.now() + randomThirtyMinMax + 1*DAYS)

+ 0 - 211
services/track-changes/app/js/PackWorker.js

@@ -1,211 +0,0 @@
-/* eslint-disable
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS101: Remove unnecessary use of Array.from
- * DS102: Remove unnecessary code created because of implicit returns
- * DS103: Rewrite code to no longer use __guard__
- * DS205: Consider reworking code to avoid use of IIFEs
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-let LIMIT, pending
-let projectId, docId
-const { callbackify } = require('util')
-const Settings = require('@overleaf/settings')
-const async = require('async')
-const _ = require('underscore')
-const { db, ObjectId, waitForDb, closeDb } = require('./mongodb')
-const fs = require('fs')
-const Metrics = require('@overleaf/metrics')
-Metrics.initialize('track-changes')
-const logger = require('@overleaf/logger')
-logger.initialize('track-changes-packworker')
-if ((Settings.sentry != null ? Settings.sentry.dsn : undefined) != null) {
-  logger.initializeErrorReporting(Settings.sentry.dsn)
-}
-
-const DAYS = 24 * 3600 * 1000
-
-const LockManager = require('./LockManager')
-const PackManager = require('./PackManager')
-
-// this worker script is forked by the main process to look for
-// document histories which can be archived
-
-const source = process.argv[2]
-const DOCUMENT_PACK_DELAY = Number(process.argv[3]) || 1000
-const TIMEOUT = Number(process.argv[4]) || 30 * 60 * 1000
-let COUNT = 0 // number processed
-let TOTAL = 0 // total number to process
-
-if (!source.match(/^[0-9]+$/)) {
-  const file = fs.readFileSync(source)
-  const result = (() => {
-    const result1 = []
-    for (const line of Array.from(file.toString().split('\n'))) {
-      ;[projectId, docId] = Array.from(line.split(' '))
-      result1.push({ doc_id: docId, project_id: projectId })
-    }
-    return result1
-  })()
-  pending = _.filter(result, row =>
-    __guard__(row != null ? row.doc_id : undefined, x =>
-      x.match(/^[a-f0-9]{24}$/)
-    )
-  )
-} else {
-  LIMIT = Number(process.argv[2]) || 1000
-}
-
-let shutDownRequested = false
-const shutDownTimer = setTimeout(function () {
-  logger.debug('pack timed out, requesting shutdown')
-  // start the shutdown on the next pack
-  shutDownRequested = true
-  // do a hard shutdown after a further 5 minutes
-  const hardTimeout = setTimeout(function () {
-    logger.error('HARD TIMEOUT in pack archive worker')
-    return process.exit()
-  }, 5 * 60 * 1000)
-  return hardTimeout.unref()
-}, TIMEOUT)
-
-logger.debug(
-  `checking for updates, limit=${LIMIT}, delay=${DOCUMENT_PACK_DELAY}, timeout=${TIMEOUT}`
-)
-
-const finish = function () {
-  if (shutDownTimer != null) {
-    logger.debug('cancelling timeout')
-    clearTimeout(shutDownTimer)
-  }
-  logger.debug('closing db')
-  callbackify(closeDb)(function () {
-    logger.debug('closing LockManager Redis Connection')
-    return LockManager.close(function () {
-      logger.debug(
-        { processedCount: COUNT, allCount: TOTAL },
-        'ready to exit from pack archive worker'
-      )
-      const hardTimeout = setTimeout(function () {
-        logger.error('hard exit from pack archive worker')
-        return process.exit(1)
-      }, 5 * 1000)
-      return hardTimeout.unref()
-    })
-  })
-}
-
-process.on('exit', code => logger.debug({ code }, 'pack archive worker exited'))
-
-const processUpdates = pending =>
-  async.eachSeries(
-    pending,
-    function (result, callback) {
-      let _id
-      ;({ _id, project_id: projectId, doc_id: docId } = result)
-      COUNT++
-      logger.debug({ projectId, docId }, `processing ${COUNT}/${TOTAL}`)
-      if (projectId == null || docId == null) {
-        logger.debug(
-          { projectId, docId },
-          'skipping pack, missing project/doc id'
-        )
-        return callback()
-      }
-      const handler = function (err, result) {
-        if (err != null && err.code === 'InternalError' && err.retryable) {
-          logger.warn(
-            { err, result },
-            'ignoring S3 error in pack archive worker'
-          )
-          // Ignore any s3 errors due to random problems
-          err = null
-        }
-        if (err != null) {
-          logger.error({ err, result }, 'error in pack archive worker')
-          return callback(err)
-        }
-        if (shutDownRequested) {
-          logger.warn('shutting down pack archive worker')
-          return callback(new Error('shutdown'))
-        }
-        return setTimeout(() => callback(err, result), DOCUMENT_PACK_DELAY)
-      }
-      if (_id == null) {
-        return PackManager.pushOldPacks(projectId, docId, handler)
-      } else {
-        return PackManager.processOldPack(projectId, docId, _id, handler)
-      }
-    },
-    function (err, results) {
-      if (err != null && err.message !== 'shutdown') {
-        logger.error({ err }, 'error in pack archive worker processUpdates')
-      }
-      return finish()
-    }
-  )
-// find the packs which can be archived
-
-const ObjectIdFromDate = function (date) {
-  const id = Math.floor(date.getTime() / 1000).toString(16) + '0000000000000000'
-  return ObjectId(id)
-}
-
-// new approach, two passes
-// find packs to be marked as finalised:true, those which have a newer pack present
-// then only consider finalised:true packs for archiving
-
-waitForDb()
-  .then(() => {
-    if (pending != null) {
-      logger.debug(`got ${pending.length} entries from ${source}`)
-      processUpdates(pending)
-    } else {
-      processFromOneWeekAgo()
-    }
-  })
-  .catch(err => {
-    logger.fatal({ err }, 'cannot connect to mongo, exiting')
-    process.exit(1)
-  })
-
-function processFromOneWeekAgo() {
-  const oneWeekAgo = new Date(Date.now() - 7 * DAYS)
-  db.docHistory
-    .find(
-      {
-        expiresAt: { $exists: false },
-        project_id: { $exists: true },
-        v_end: { $exists: true },
-        _id: { $lt: ObjectIdFromDate(oneWeekAgo) },
-        last_checked: { $lt: oneWeekAgo },
-      },
-      { projection: { _id: 1, doc_id: 1, project_id: 1 } }
-    )
-    .sort({
-      last_checked: 1,
-    })
-    .limit(LIMIT)
-    .toArray(function (err, results) {
-      if (err != null) {
-        logger.debug({ err }, 'error checking for updates')
-        finish()
-        return
-      }
-      pending = _.uniq(results, false, result => result.doc_id.toString())
-      TOTAL = pending.length
-      logger.debug(`found ${TOTAL} documents to archive`)
-      return processUpdates(pending)
-    })
-}
-
-function __guard__(value, transform) {
-  return typeof value !== 'undefined' && value !== null
-    ? transform(value)
-    : undefined
-}

+ 0 - 113
services/track-changes/app/js/ProjectIterator.js

@@ -1,113 +0,0 @@
-/* eslint-disable
-    no-unmodified-loop-condition,
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS101: Remove unnecessary use of Array.from
- * DS102: Remove unnecessary code created because of implicit returns
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-let ProjectIterator
-const Heap = require('heap')
-
-module.exports =
-  ProjectIterator =
-  ProjectIterator =
-    class ProjectIterator {
-      constructor(packs, before, getPackByIdFn) {
-        this.before = before
-        this.getPackByIdFn = getPackByIdFn
-        const byEndTs = (a, b) =>
-          b.meta.end_ts - a.meta.end_ts || a.fromIndex - b.fromIndex
-        this.packs = packs.slice().sort(byEndTs)
-        this.queue = new Heap(byEndTs)
-      }
-
-      next(callback) {
-        //  what's up next
-        // console.log ">>> top item", iterator.packs[0]
-        const iterator = this
-        const { before } = this
-        const { queue } = iterator
-        const opsToReturn = []
-        let nextPack = iterator.packs[0]
-        let lowWaterMark =
-          (nextPack != null ? nextPack.meta.end_ts : undefined) || 0
-        let nextItem = queue.peek()
-
-        // console.log "queue empty?", queue.empty()
-        // console.log "nextItem", nextItem
-        // console.log "nextItem.meta.end_ts", nextItem?.meta.end_ts
-        // console.log "lowWaterMark", lowWaterMark
-
-        while (
-          before != null &&
-          (nextPack != null ? nextPack.meta.start_ts : undefined) > before
-        ) {
-          // discard pack that is outside range
-          iterator.packs.shift()
-          nextPack = iterator.packs[0]
-          lowWaterMark =
-            (nextPack != null ? nextPack.meta.end_ts : undefined) || 0
-        }
-
-        if (
-          (queue.empty() ||
-            (nextItem != null ? nextItem.meta.end_ts : undefined) <=
-              lowWaterMark) &&
-          nextPack != null
-        ) {
-          // retrieve the next pack and populate the queue
-          return this.getPackByIdFn(
-            nextPack.project_id,
-            nextPack.doc_id,
-            nextPack._id,
-            function (err, pack) {
-              if (err != null) {
-                return callback(err)
-              }
-              iterator.packs.shift() // have now retrieved this pack, remove it
-              // console.log "got pack", pack
-              for (const op of Array.from(pack.pack)) {
-                // console.log "adding op", op
-                if (before == null || op.meta.end_ts < before) {
-                  op.doc_id = nextPack.doc_id
-                  op.project_id = nextPack.project_id
-                  queue.push(op)
-                }
-              }
-              // now try again
-              return iterator.next(callback)
-            }
-          )
-        }
-
-        // console.log "nextItem", nextItem, "lowWaterMark", lowWaterMark
-        while (
-          nextItem != null &&
-          (nextItem != null ? nextItem.meta.end_ts : undefined) > lowWaterMark
-        ) {
-          opsToReturn.push(nextItem)
-          queue.pop()
-          nextItem = queue.peek()
-        }
-
-        // console.log "queue empty?", queue.empty()
-        // console.log "nextPack", nextPack?
-
-        if (queue.empty() && nextPack == null) {
-          // got everything
-          iterator._done = true
-        }
-
-        return callback(null, opsToReturn)
-      }
-
-      done() {
-        return this._done
-      }
-    }

+ 0 - 169
services/track-changes/app/js/RedisManager.js

@@ -1,169 +0,0 @@
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS101: Remove unnecessary use of Array.from
- * DS102: Remove unnecessary code created because of implicit returns
- * DS205: Consider reworking code to avoid use of IIFEs
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-let RedisManager
-const Settings = require('@overleaf/settings')
-const redis = require('@overleaf/redis-wrapper')
-const rclient = redis.createClient(Settings.redis.history)
-const Keys = Settings.redis.history.key_schema
-const async = require('async')
-
-module.exports = RedisManager = {
-  getOldestDocUpdates(docId, batchSize, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    const key = Keys.uncompressedHistoryOps({ doc_id: docId })
-    return rclient.lrange(key, 0, batchSize - 1, callback)
-  },
-
-  expandDocUpdates(jsonUpdates, callback) {
-    let rawUpdates
-    if (callback == null) {
-      callback = function () {}
-    }
-    try {
-      rawUpdates = Array.from(jsonUpdates || []).map(update =>
-        JSON.parse(update)
-      )
-    } catch (e) {
-      return callback(e)
-    }
-    return callback(null, rawUpdates)
-  },
-
-  deleteAppliedDocUpdates(projectId, docId, docUpdates, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    const multi = rclient.multi()
-    // Delete all the updates which have been applied (exact match)
-    for (const update of Array.from(docUpdates || [])) {
-      multi.lrem(Keys.uncompressedHistoryOps({ doc_id: docId }), 1, update)
-    }
-    return multi.exec(function (error, results) {
-      if (error != null) {
-        return callback(error)
-      }
-      // It's ok to delete the doc_id from the set here. Even though the list
-      // of updates may not be empty, we will continue to process it until it is.
-      return rclient.srem(
-        Keys.docsWithHistoryOps({ project_id: projectId }),
-        docId,
-        function (error) {
-          if (error != null) {
-            return callback(error)
-          }
-          return callback(null)
-        }
-      )
-    })
-  },
-
-  getDocIdsWithHistoryOps(projectId, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    return rclient.smembers(
-      Keys.docsWithHistoryOps({ project_id: projectId }),
-      callback
-    )
-  },
-
-  // iterate over keys asynchronously using redis scan (non-blocking)
-  // handle all the cluster nodes or single redis server
-  _getKeys(pattern, callback) {
-    const nodes = (typeof rclient.nodes === 'function'
-      ? rclient.nodes('master')
-      : undefined) || [rclient]
-    const doKeyLookupForNode = (node, cb) =>
-      RedisManager._getKeysFromNode(node, pattern, cb)
-    return async.concatSeries(nodes, doKeyLookupForNode, callback)
-  },
-
-  _getKeysFromNode(node, pattern, callback) {
-    let cursor = 0 // redis iterator
-    const keySet = {} // use hash to avoid duplicate results
-    // scan over all keys looking for pattern
-    const doIteration = cb =>
-      node.scan(
-        cursor,
-        'MATCH',
-        pattern,
-        'COUNT',
-        1000,
-        function (error, reply) {
-          let keys
-          if (error != null) {
-            return callback(error)
-          }
-          ;[cursor, keys] = Array.from(reply)
-          for (const key of Array.from(keys)) {
-            keySet[key] = true
-          }
-          if (cursor === '0') {
-            // note redis returns string result not numeric
-            return callback(null, Object.keys(keySet))
-          } else {
-            return doIteration()
-          }
-        }
-      )
-    return doIteration()
-  },
-
-  // extract ids from keys like DocsWithHistoryOps:57fd0b1f53a8396d22b2c24b
-  // or DocsWithHistoryOps:{57fd0b1f53a8396d22b2c24b} (for redis cluster)
-  _extractIds(keyList) {
-    const ids = (() => {
-      const result = []
-      for (const key of Array.from(keyList)) {
-        const m = key.match(/:\{?([0-9a-f]{24})\}?/) // extract object id
-        result.push(m[1])
-      }
-      return result
-    })()
-    return ids
-  },
-
-  getProjectIdsWithHistoryOps(callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    return RedisManager._getKeys(
-      Keys.docsWithHistoryOps({ project_id: '*' }),
-      function (error, projectKeys) {
-        if (error != null) {
-          return callback(error)
-        }
-        const projectIds = RedisManager._extractIds(projectKeys)
-        return callback(error, projectIds)
-      }
-    )
-  },
-
-  getAllDocIdsWithHistoryOps(callback) {
-    // return all the docids, to find dangling history entries after
-    // everything is flushed.
-    if (callback == null) {
-      callback = function () {}
-    }
-    return RedisManager._getKeys(
-      Keys.uncompressedHistoryOps({ doc_id: '*' }),
-      function (error, docKeys) {
-        if (error != null) {
-          return callback(error)
-        }
-        const docIds = RedisManager._extractIds(docKeys)
-        return callback(error, docIds)
-      }
-    )
-  },
-}

+ 0 - 46
services/track-changes/app/js/RestoreManager.js

@@ -1,46 +0,0 @@
-/* eslint-disable
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-let RestoreManager
-const DocumentUpdaterManager = require('./DocumentUpdaterManager')
-const DiffManager = require('./DiffManager')
-const logger = require('@overleaf/logger')
-
-module.exports = RestoreManager = {
-  restoreToBeforeVersion(projectId, docId, version, userId, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    logger.debug({ projectId, docId, version, userId }, 'restoring document')
-    return DiffManager.getDocumentBeforeVersion(
-      projectId,
-      docId,
-      version,
-      function (error, content) {
-        if (error != null) {
-          return callback(error)
-        }
-        return DocumentUpdaterManager.setDocument(
-          projectId,
-          docId,
-          content,
-          userId,
-          function (error) {
-            if (error != null) {
-              return callback(error)
-            }
-            return callback()
-          }
-        )
-      }
-    )
-  },
-}

+ 0 - 326
services/track-changes/app/js/UpdateCompressor.js

@@ -1,326 +0,0 @@
-/* eslint-disable
-    new-cap,
-    no-throw-literal,
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS101: Remove unnecessary use of Array.from
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-let oneMinute, twoMegabytes, UpdateCompressor
-const strInject = (s1, pos, s2) => s1.slice(0, pos) + s2 + s1.slice(pos)
-const strRemove = (s1, pos, length) => s1.slice(0, pos) + s1.slice(pos + length)
-
-const { diff_match_patch: diffMatchPatch } = require('../lib/diff_match_patch')
-const dmp = new diffMatchPatch()
-
-module.exports = UpdateCompressor = {
-  NOOP: 'noop',
-
-  // Updates come from the doc updater in format
-  // {
-  // 	op:   [ { ... op1 ... }, { ... op2 ... } ]
-  // 	meta: { ts: ..., user_id: ... }
-  // }
-  // but it's easier to work with on op per update, so convert these updates to
-  // our compressed format
-  // [{
-  // 	op: op1
-  // 	meta: { start_ts: ... , end_ts: ..., user_id: ... }
-  // }, {
-  // 	op: op2
-  // 	meta: { start_ts: ... , end_ts: ..., user_id: ... }
-  // }]
-  convertToSingleOpUpdates(updates) {
-    const splitUpdates = []
-    for (const update of Array.from(updates)) {
-      // Reject any non-insert or delete ops, i.e. comments
-      const ops = update.op.filter(o => o.i != null || o.d != null)
-      if (ops.length === 0) {
-        splitUpdates.push({
-          op: UpdateCompressor.NOOP,
-          meta: {
-            start_ts: update.meta.start_ts || update.meta.ts,
-            end_ts: update.meta.end_ts || update.meta.ts,
-            user_id: update.meta.user_id,
-          },
-          v: update.v,
-        })
-      } else {
-        for (const op of Array.from(ops)) {
-          splitUpdates.push({
-            op,
-            meta: {
-              start_ts: update.meta.start_ts || update.meta.ts,
-              end_ts: update.meta.end_ts || update.meta.ts,
-              user_id: update.meta.user_id,
-            },
-            v: update.v,
-          })
-        }
-      }
-    }
-    return splitUpdates
-  },
-
-  concatUpdatesWithSameVersion(updates) {
-    const concattedUpdates = []
-    for (const update of Array.from(updates)) {
-      const lastUpdate = concattedUpdates[concattedUpdates.length - 1]
-      if (lastUpdate != null && lastUpdate.v === update.v) {
-        if (update.op !== UpdateCompressor.NOOP) {
-          lastUpdate.op.push(update.op)
-        }
-      } else {
-        const nextUpdate = {
-          op: [],
-          meta: update.meta,
-          v: update.v,
-        }
-        if (update.op !== UpdateCompressor.NOOP) {
-          nextUpdate.op.push(update.op)
-        }
-        concattedUpdates.push(nextUpdate)
-      }
-    }
-    return concattedUpdates
-  },
-
-  compressRawUpdates(lastPreviousUpdate, rawUpdates) {
-    if (lastPreviousUpdate?.op?.length > 1) {
-      // if the last previous update was an array op, don't compress onto it.
-      // The avoids cases where array length changes but version number doesn't
-      return [lastPreviousUpdate].concat(
-        UpdateCompressor.compressRawUpdates(null, rawUpdates)
-      )
-    }
-    if (lastPreviousUpdate != null) {
-      rawUpdates = [lastPreviousUpdate].concat(rawUpdates)
-    }
-    let updates = UpdateCompressor.convertToSingleOpUpdates(rawUpdates)
-    updates = UpdateCompressor.compressUpdates(updates)
-    return UpdateCompressor.concatUpdatesWithSameVersion(updates)
-  },
-
-  compressUpdates(updates) {
-    if (updates.length === 0) {
-      return []
-    }
-
-    let compressedUpdates = [updates.shift()]
-    for (const update of Array.from(updates)) {
-      const lastCompressedUpdate = compressedUpdates.pop()
-      if (lastCompressedUpdate != null) {
-        compressedUpdates = compressedUpdates.concat(
-          UpdateCompressor._concatTwoUpdates(lastCompressedUpdate, update)
-        )
-      } else {
-        compressedUpdates.push(update)
-      }
-    }
-
-    return compressedUpdates
-  },
-
-  MAX_TIME_BETWEEN_UPDATES: (oneMinute = 60 * 1000),
-  MAX_UPDATE_SIZE: (twoMegabytes = 2 * 1024 * 1024),
-
-  _concatTwoUpdates(firstUpdate, secondUpdate) {
-    let offset
-    firstUpdate = {
-      op: firstUpdate.op,
-      meta: {
-        user_id: firstUpdate.meta.user_id || null,
-        start_ts: firstUpdate.meta.start_ts || firstUpdate.meta.ts,
-        end_ts: firstUpdate.meta.end_ts || firstUpdate.meta.ts,
-      },
-      v: firstUpdate.v,
-    }
-    secondUpdate = {
-      op: secondUpdate.op,
-      meta: {
-        user_id: secondUpdate.meta.user_id || null,
-        start_ts: secondUpdate.meta.start_ts || secondUpdate.meta.ts,
-        end_ts: secondUpdate.meta.end_ts || secondUpdate.meta.ts,
-      },
-      v: secondUpdate.v,
-    }
-
-    if (firstUpdate.meta.user_id !== secondUpdate.meta.user_id) {
-      return [firstUpdate, secondUpdate]
-    }
-
-    if (
-      secondUpdate.meta.start_ts - firstUpdate.meta.end_ts >
-      UpdateCompressor.MAX_TIME_BETWEEN_UPDATES
-    ) {
-      return [firstUpdate, secondUpdate]
-    }
-
-    const firstOp = firstUpdate.op
-    const secondOp = secondUpdate.op
-
-    const firstSize =
-      (firstOp.i != null ? firstOp.i.length : undefined) ||
-      (firstOp.d != null ? firstOp.d.length : undefined)
-    const secondSize =
-      (secondOp.i != null ? secondOp.i.length : undefined) ||
-      (secondOp.d != null ? secondOp.d.length : undefined)
-
-    // Two inserts
-    if (
-      firstOp.i != null &&
-      secondOp.i != null &&
-      firstOp.p <= secondOp.p &&
-      secondOp.p <= firstOp.p + firstOp.i.length &&
-      firstSize + secondSize < UpdateCompressor.MAX_UPDATE_SIZE
-    ) {
-      return [
-        {
-          meta: {
-            start_ts: firstUpdate.meta.start_ts,
-            end_ts: secondUpdate.meta.end_ts,
-            user_id: firstUpdate.meta.user_id,
-          },
-          op: {
-            p: firstOp.p,
-            i: strInject(firstOp.i, secondOp.p - firstOp.p, secondOp.i),
-          },
-          v: secondUpdate.v,
-        },
-      ]
-      // Two deletes
-    } else if (
-      firstOp.d != null &&
-      secondOp.d != null &&
-      secondOp.p <= firstOp.p &&
-      firstOp.p <= secondOp.p + secondOp.d.length &&
-      firstSize + secondSize < UpdateCompressor.MAX_UPDATE_SIZE
-    ) {
-      return [
-        {
-          meta: {
-            start_ts: firstUpdate.meta.start_ts,
-            end_ts: secondUpdate.meta.end_ts,
-            user_id: firstUpdate.meta.user_id,
-          },
-          op: {
-            p: secondOp.p,
-            d: strInject(secondOp.d, firstOp.p - secondOp.p, firstOp.d),
-          },
-          v: secondUpdate.v,
-        },
-      ]
-      // An insert and then a delete
-    } else if (
-      firstOp.i != null &&
-      secondOp.d != null &&
-      firstOp.p <= secondOp.p &&
-      secondOp.p <= firstOp.p + firstOp.i.length
-    ) {
-      offset = secondOp.p - firstOp.p
-      const insertedText = firstOp.i.slice(offset, offset + secondOp.d.length)
-      // Only trim the insert when the delete is fully contained within in it
-      if (insertedText === secondOp.d) {
-        const insert = strRemove(firstOp.i, offset, secondOp.d.length)
-        return [
-          {
-            meta: {
-              start_ts: firstUpdate.meta.start_ts,
-              end_ts: secondUpdate.meta.end_ts,
-              user_id: firstUpdate.meta.user_id,
-            },
-            op: {
-              p: firstOp.p,
-              i: insert,
-            },
-            v: secondUpdate.v,
-          },
-        ]
-      } else {
-        // This will only happen if the delete extends outside the insert
-        return [firstUpdate, secondUpdate]
-      }
-
-      // A delete then an insert at the same place, likely a copy-paste of a chunk of content
-    } else if (
-      firstOp.d != null &&
-      secondOp.i != null &&
-      firstOp.p === secondOp.p
-    ) {
-      offset = firstOp.p
-      const diffOps = this.diffAsShareJsOps(firstOp.d, secondOp.i)
-      if (diffOps.length === 0) {
-        return [
-          {
-            // Noop
-            meta: {
-              start_ts: firstUpdate.meta.start_ts,
-              end_ts: secondUpdate.meta.end_ts,
-              user_id: firstUpdate.meta.user_id,
-            },
-            op: {
-              p: firstOp.p,
-              i: '',
-            },
-            v: secondUpdate.v,
-          },
-        ]
-      } else {
-        return diffOps.map(function (op) {
-          op.p += offset
-          return {
-            meta: {
-              start_ts: firstUpdate.meta.start_ts,
-              end_ts: secondUpdate.meta.end_ts,
-              user_id: firstUpdate.meta.user_id,
-            },
-            op,
-            v: secondUpdate.v,
-          }
-        })
-      }
-    } else {
-      return [firstUpdate, secondUpdate]
-    }
-  },
-
-  ADDED: 1,
-  REMOVED: -1,
-  UNCHANGED: 0,
-  diffAsShareJsOps(before, after, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    const diffs = dmp.diff_main(before, after)
-    dmp.diff_cleanupSemantic(diffs)
-
-    const ops = []
-    let position = 0
-    for (const diff of Array.from(diffs)) {
-      const type = diff[0]
-      const content = diff[1]
-      if (type === this.ADDED) {
-        ops.push({
-          i: content,
-          p: position,
-        })
-        position += content.length
-      } else if (type === this.REMOVED) {
-        ops.push({
-          d: content,
-          p: position,
-        })
-      } else if (type === this.UNCHANGED) {
-        position += content.length
-      } else {
-        throw 'Unknown type'
-      }
-    }
-    return ops
-  },
-}

+ 0 - 66
services/track-changes/app/js/UpdateTrimmer.js

@@ -1,66 +0,0 @@
-/* eslint-disable
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-let UpdateTrimmer
-const MongoManager = require('./MongoManager')
-const WebApiManager = require('./WebApiManager')
-const logger = require('@overleaf/logger')
-
-module.exports = UpdateTrimmer = {
-  shouldTrimUpdates(projectId, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    return MongoManager.getProjectMetaData(
-      projectId,
-      function (error, metadata) {
-        if (error != null) {
-          return callback(error)
-        }
-        if (metadata != null ? metadata.preserveHistory : undefined) {
-          return callback(null, false)
-        } else {
-          return WebApiManager.getProjectDetails(
-            projectId,
-            function (error, details) {
-              if (error != null) {
-                return callback(error)
-              }
-              logger.debug({ projectId, details }, 'got details')
-              if (details?.features?.versioning) {
-                return MongoManager.setProjectMetaData(
-                  projectId,
-                  { preserveHistory: true },
-                  function (error) {
-                    if (error != null) {
-                      return callback(error)
-                    }
-                    return MongoManager.upgradeHistory(
-                      projectId,
-                      function (error) {
-                        if (error != null) {
-                          return callback(error)
-                        }
-                        return callback(null, false)
-                      }
-                    )
-                  }
-                )
-              } else {
-                return callback(null, true)
-              }
-            }
-          )
-        }
-      }
-    )
-  },
-}

+ 0 - 883
services/track-changes/app/js/UpdatesManager.js

@@ -1,883 +0,0 @@
-/* eslint-disable
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS101: Remove unnecessary use of Array.from
- * DS102: Remove unnecessary code created because of implicit returns
- * DS103: Rewrite code to no longer use __guard__
- * DS205: Consider reworking code to avoid use of IIFEs
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-let fiveMinutes, UpdatesManager
-const MongoManager = require('./MongoManager')
-const PackManager = require('./PackManager')
-const RedisManager = require('./RedisManager')
-const UpdateCompressor = require('./UpdateCompressor')
-const LockManager = require('./LockManager')
-const WebApiManager = require('./WebApiManager')
-const UpdateTrimmer = require('./UpdateTrimmer')
-const logger = require('@overleaf/logger')
-const async = require('async')
-const _ = require('underscore')
-const Settings = require('@overleaf/settings')
-const keys = Settings.redis.lock.key_schema
-const util = require('util')
-
-module.exports = UpdatesManager = {
-  compressAndSaveRawUpdates(projectId, docId, rawUpdates, temporary, callback) {
-    let i
-    if (callback == null) {
-      callback = function () {}
-    }
-    const { length } = rawUpdates
-    if (length === 0) {
-      return callback()
-    }
-
-    // check that ops are in the correct order
-    for (i = 0; i < rawUpdates.length; i++) {
-      const op = rawUpdates[i]
-      if (i > 0) {
-        const thisVersion = op != null ? op.v : undefined
-        const prevVersion = rawUpdates[i - 1]?.v
-        if (!(prevVersion < thisVersion)) {
-          logger.error(
-            {
-              projectId,
-              docId,
-              rawUpdates,
-              temporary,
-              thisVersion,
-              prevVersion,
-            },
-            'op versions out of order'
-          )
-        }
-      }
-    }
-
-    // FIXME: we no longer need the lastCompressedUpdate, so change functions not to need it
-    // CORRECTION:  we do use it to log the time in case of error
-    return MongoManager.peekLastCompressedUpdate(
-      docId,
-      function (error, lastCompressedUpdate, lastVersion) {
-        // lastCompressedUpdate is the most recent update in Mongo, and
-        // lastVersion is its sharejs version number.
-        //
-        // The peekLastCompressedUpdate method may pass the update back
-        // as 'null' (for example if the previous compressed update has
-        // been archived).  In this case it can still pass back the
-        // lastVersion from the update to allow us to check consistency.
-        let op
-        if (error != null) {
-          return callback(error)
-        }
-
-        // Ensure that raw updates start where lastVersion left off
-        if (lastVersion != null) {
-          const discardedUpdates = []
-          rawUpdates = rawUpdates.slice(0)
-          while (rawUpdates[0] != null && rawUpdates[0].v <= lastVersion) {
-            discardedUpdates.push(rawUpdates.shift())
-          }
-          if (discardedUpdates.length) {
-            logger.error(
-              { projectId, docId, discardedUpdates, temporary, lastVersion },
-              'discarded updates already present'
-            )
-          }
-
-          if (rawUpdates[0] != null && rawUpdates[0].v !== lastVersion + 1) {
-            const ts = lastCompressedUpdate?.meta?.end_ts
-            const lastTimestamp = ts != null ? new Date(ts) : 'unknown time'
-            error = new Error(
-              `Tried to apply raw op at version ${rawUpdates[0].v} to last compressed update with version ${lastVersion} from ${lastTimestamp}`
-            )
-            logger.error(
-              {
-                err: error,
-                docId,
-                projectId,
-                prevEndTs: ts,
-                temporary,
-                lastCompressedUpdate,
-              },
-              'inconsistent doc versions'
-            )
-            if (
-              (Settings.trackchanges != null
-                ? Settings.trackchanges.continueOnError
-                : undefined) &&
-              rawUpdates[0].v > lastVersion + 1
-            ) {
-              // we have lost some ops - continue to write into the database, we can't recover at this point
-              lastCompressedUpdate = null
-            } else {
-              return callback(error)
-            }
-          }
-        }
-
-        if (rawUpdates.length === 0) {
-          return callback()
-        }
-
-        // some old large ops in redis need to be rejected, they predate
-        // the size limit that now prevents them going through the system
-        const REJECT_LARGE_OP_SIZE = 4 * 1024 * 1024
-        for (const rawUpdate of Array.from(rawUpdates)) {
-          const opSizes = (() => {
-            const result = []
-            for (op of Array.from(
-              (rawUpdate != null ? rawUpdate.op : undefined) || []
-            )) {
-              result.push(
-                (op.i != null ? op.i.length : undefined) ||
-                  (op.d != null ? op.d.length : undefined)
-              )
-            }
-            return result
-          })()
-          const size = _.max(opSizes)
-          if (size > REJECT_LARGE_OP_SIZE) {
-            error = new Error(
-              `dropped op exceeding maximum allowed size of ${REJECT_LARGE_OP_SIZE}`
-            )
-            logger.error(
-              { err: error, docId, projectId, size, rawUpdate },
-              'dropped op - too big'
-            )
-            rawUpdate.op = []
-          }
-        }
-
-        const compressedUpdates = UpdateCompressor.compressRawUpdates(
-          null,
-          rawUpdates
-        )
-        return PackManager.insertCompressedUpdates(
-          projectId,
-          docId,
-          lastCompressedUpdate,
-          compressedUpdates,
-          temporary,
-          function (error, result) {
-            if (error != null) {
-              return callback(error)
-            }
-            if (result != null) {
-              logger.debug(
-                {
-                  projectId,
-                  docId,
-                  origV:
-                    lastCompressedUpdate != null
-                      ? lastCompressedUpdate.v
-                      : undefined,
-                  newV: result.v,
-                },
-                'inserted updates into pack'
-              )
-            }
-            return callback()
-          }
-        )
-      }
-    )
-  },
-
-  // Check whether the updates are temporary (per-project property)
-  _prepareProjectForUpdates(projectId, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    return UpdateTrimmer.shouldTrimUpdates(
-      projectId,
-      function (error, temporary) {
-        if (error != null) {
-          return callback(error)
-        }
-        return callback(null, temporary)
-      }
-    )
-  },
-
-  // Check for project id on document history (per-document property)
-  _prepareDocForUpdates(projectId, docId, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    return MongoManager.backportProjectId(projectId, docId, function (error) {
-      if (error != null) {
-        return callback(error)
-      }
-      return callback(null)
-    })
-  },
-
-  // Apply updates for specific project/doc after preparing at project and doc level
-  REDIS_READ_BATCH_SIZE: 100,
-  processUncompressedUpdates(projectId, docId, temporary, callback) {
-    // get the updates as strings from redis (so we can delete them after they are applied)
-    if (callback == null) {
-      callback = function () {}
-    }
-    return RedisManager.getOldestDocUpdates(
-      docId,
-      UpdatesManager.REDIS_READ_BATCH_SIZE,
-      function (error, docUpdates) {
-        if (error != null) {
-          return callback(error)
-        }
-        const { length } = docUpdates
-        // parse the redis strings into ShareJs updates
-        return RedisManager.expandDocUpdates(
-          docUpdates,
-          function (error, rawUpdates) {
-            if (error != null) {
-              logger.err(
-                { projectId, docId, docUpdates },
-                'failed to parse docUpdates'
-              )
-              return callback(error)
-            }
-            logger.debug(
-              { projectId, docId, rawUpdates },
-              'retrieved raw updates from redis'
-            )
-            return UpdatesManager.compressAndSaveRawUpdates(
-              projectId,
-              docId,
-              rawUpdates,
-              temporary,
-              function (error) {
-                if (error != null) {
-                  return callback(error)
-                }
-                logger.debug(
-                  { projectId, docId },
-                  'compressed and saved doc updates'
-                )
-                // delete the applied updates from redis
-                return RedisManager.deleteAppliedDocUpdates(
-                  projectId,
-                  docId,
-                  docUpdates,
-                  function (error) {
-                    if (error != null) {
-                      return callback(error)
-                    }
-                    if (length === UpdatesManager.REDIS_READ_BATCH_SIZE) {
-                      // There might be more updates
-                      logger.debug(
-                        { projectId, docId },
-                        'continuing processing updates'
-                      )
-                      return setTimeout(
-                        () =>
-                          UpdatesManager.processUncompressedUpdates(
-                            projectId,
-                            docId,
-                            temporary,
-                            callback
-                          ),
-                        0
-                      )
-                    } else {
-                      logger.debug(
-                        { projectId, docId },
-                        'all raw updates processed'
-                      )
-                      return callback()
-                    }
-                  }
-                )
-              }
-            )
-          }
-        )
-      }
-    )
-  },
-
-  // Process updates for a doc when we flush it individually
-  processUncompressedUpdatesWithLock(projectId, docId, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    return UpdatesManager._prepareProjectForUpdates(
-      projectId,
-      function (error, temporary) {
-        if (error != null) {
-          return callback(error)
-        }
-        return UpdatesManager._processUncompressedUpdatesForDocWithLock(
-          projectId,
-          docId,
-          temporary,
-          callback
-        )
-      }
-    )
-  },
-
-  // Process updates for a doc when the whole project is flushed (internal method)
-  _processUncompressedUpdatesForDocWithLock(
-    projectId,
-    docId,
-    temporary,
-    callback
-  ) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    return UpdatesManager._prepareDocForUpdates(
-      projectId,
-      docId,
-      function (error) {
-        if (error != null) {
-          return callback(error)
-        }
-        return LockManager.runWithLock(
-          keys.historyLock({ doc_id: docId }),
-          releaseLock =>
-            UpdatesManager.processUncompressedUpdates(
-              projectId,
-              docId,
-              temporary,
-              releaseLock
-            ),
-          callback
-        )
-      }
-    )
-  },
-
-  // Process all updates for a project, only check project-level information once
-  processUncompressedUpdatesForProject(projectId, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    return RedisManager.getDocIdsWithHistoryOps(
-      projectId,
-      function (error, docIds) {
-        if (error != null) {
-          return callback(error)
-        }
-        return UpdatesManager._prepareProjectForUpdates(
-          projectId,
-          function (error, temporary) {
-            if (error) return callback(error)
-            const jobs = []
-            for (const docId of Array.from(docIds)) {
-              ;(docId =>
-                jobs.push(cb =>
-                  UpdatesManager._processUncompressedUpdatesForDocWithLock(
-                    projectId,
-                    docId,
-                    temporary,
-                    cb
-                  )
-                ))(docId)
-            }
-            return async.parallelLimit(jobs, 5, callback)
-          }
-        )
-      }
-    )
-  },
-
-  // flush all outstanding changes
-  flushAll(limit, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    return RedisManager.getProjectIdsWithHistoryOps(function (
-      error,
-      projectIds
-    ) {
-      let projectId
-      if (error != null) {
-        return callback(error)
-      }
-      logger.debug(
-        {
-          count: projectIds != null ? projectIds.length : undefined,
-          projectIds,
-        },
-        'found projects'
-      )
-      const jobs = []
-      projectIds = _.shuffle(projectIds) // randomise to avoid hitting same projects each time
-      const selectedProjects =
-        limit < 0 ? projectIds : projectIds.slice(0, limit)
-      for (projectId of Array.from(selectedProjects)) {
-        ;(projectId =>
-          jobs.push(cb =>
-            UpdatesManager.processUncompressedUpdatesForProject(
-              projectId,
-              err => cb(null, { failed: err != null, project_id: projectId })
-            )
-          ))(projectId)
-      }
-      return async.series(jobs, function (error, result) {
-        let x
-        if (error != null) {
-          return callback(error)
-        }
-        const failedProjects = (() => {
-          const result1 = []
-          for (x of Array.from(result)) {
-            if (x.failed) {
-              result1.push(x.project_id)
-            }
-          }
-          return result1
-        })()
-        const succeededProjects = (() => {
-          const result2 = []
-          for (x of Array.from(result)) {
-            if (!x.failed) {
-              result2.push(x.project_id)
-            }
-          }
-          return result2
-        })()
-        return callback(null, {
-          failed: failedProjects,
-          succeeded: succeededProjects,
-          all: projectIds,
-        })
-      })
-    })
-  },
-
-  getDanglingUpdates(callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    return RedisManager.getAllDocIdsWithHistoryOps(function (error, allDocIds) {
-      if (error != null) {
-        return callback(error)
-      }
-      return RedisManager.getProjectIdsWithHistoryOps(function (
-        error,
-        allProjectIds
-      ) {
-        if (error != null) {
-          return callback(error)
-        }
-        // function to get doc_ids for each project
-        const task = cb =>
-          async.concatSeries(
-            allProjectIds,
-            RedisManager.getDocIdsWithHistoryOps,
-            cb
-          )
-        // find the dangling doc ids
-        return task(function (error, projectDocIds) {
-          if (error) return callback(error)
-          const danglingDocIds = _.difference(allDocIds, projectDocIds)
-          logger.debug(
-            { allDocIds, allProjectIds, projectDocIds, danglingDocIds },
-            'checking for dangling doc ids'
-          )
-          return callback(null, danglingDocIds)
-        })
-      })
-    })
-  },
-
-  getDocUpdates(projectId, docId, options, callback) {
-    if (options == null) {
-      options = {}
-    }
-    if (callback == null) {
-      callback = function () {}
-    }
-    return UpdatesManager.processUncompressedUpdatesWithLock(
-      projectId,
-      docId,
-      function (error) {
-        if (error != null) {
-          return callback(error)
-        }
-        // console.log "options", options
-        return PackManager.getOpsByVersionRange(
-          projectId,
-          docId,
-          options.from,
-          options.to,
-          function (error, updates) {
-            if (error != null) {
-              return callback(error)
-            }
-            return callback(null, updates)
-          }
-        )
-      }
-    )
-  },
-
-  getDocUpdatesWithUserInfo(projectId, docId, options, callback) {
-    if (options == null) {
-      options = {}
-    }
-    if (callback == null) {
-      callback = function () {}
-    }
-    return UpdatesManager.getDocUpdates(
-      projectId,
-      docId,
-      options,
-      function (error, updates) {
-        if (error != null) {
-          return callback(error)
-        }
-        return UpdatesManager.fillUserInfo(updates, function (error, updates) {
-          if (error != null) {
-            return callback(error)
-          }
-          return callback(null, updates)
-        })
-      }
-    )
-  },
-
-  getSummarizedProjectUpdates(projectId, options, callback) {
-    if (options == null) {
-      options = {}
-    }
-    if (callback == null) {
-      callback = function () {}
-    }
-    if (!options.min_count) {
-      options.min_count = 25
-    }
-    let summarizedUpdates = []
-    const { before } = options
-    let nextBeforeTimestamp = null
-    return UpdatesManager.processUncompressedUpdatesForProject(
-      projectId,
-      function (error) {
-        if (error != null) {
-          return callback(error)
-        }
-        return PackManager.makeProjectIterator(
-          projectId,
-          before,
-          function (err, iterator) {
-            if (err != null) {
-              return callback(err)
-            }
-            // repeatedly get updates and pass them through the summariser to get an final output with user info
-            return async.whilst(
-              cb =>
-                // console.log "checking iterator.done", iterator.done()
-                cb(
-                  null,
-                  summarizedUpdates.length < options.min_count &&
-                    !iterator.done()
-                ),
-              cb =>
-                iterator.next(function (err, partialUpdates) {
-                  if (err != null) {
-                    return callback(err)
-                  }
-                  // logger.log {partialUpdates}, 'got partialUpdates'
-                  if (partialUpdates.length === 0) {
-                    return cb()
-                  } // # FIXME should try to avoid this happening
-                  nextBeforeTimestamp =
-                    partialUpdates[partialUpdates.length - 1].meta.end_ts
-                  // add the updates to the summary list
-                  summarizedUpdates = UpdatesManager._summarizeUpdates(
-                    partialUpdates,
-                    summarizedUpdates
-                  )
-                  return cb()
-                }),
-
-              () =>
-                // finally done all updates
-                // console.log 'summarized Updates', summarizedUpdates
-                UpdatesManager.fillSummarizedUserInfo(
-                  summarizedUpdates,
-                  function (err, results) {
-                    if (err != null) {
-                      return callback(err)
-                    }
-                    return callback(
-                      null,
-                      results,
-                      !iterator.done() ? nextBeforeTimestamp : undefined
-                    )
-                  }
-                )
-            )
-          }
-        )
-      }
-    )
-  },
-
-  exportProject(projectId, consumer) {
-    // Flush anything before collecting updates.
-    UpdatesManager.processUncompressedUpdatesForProject(projectId, err => {
-      if (err) return consumer(err)
-
-      // Fetch all the packs.
-      const before = undefined
-      PackManager.makeProjectIterator(projectId, before, (err, iterator) => {
-        if (err) return consumer(err)
-
-        const accumulatedUserIds = new Set()
-
-        async.whilst(
-          cb => cb(null, !iterator.done()),
-
-          cb =>
-            iterator.next((err, updatesFromASinglePack) => {
-              if (err) return cb(err)
-
-              if (updatesFromASinglePack.length === 0) {
-                // This should not happen when `iterator.done() == false`.
-                // Emitting an empty array would signal the consumer the final
-                //  call.
-                return cb()
-              }
-              updatesFromASinglePack.forEach(update => {
-                accumulatedUserIds.add(
-                  // Super defensive access on update details.
-                  String(update && update.meta && update.meta.user_id)
-                )
-              })
-              // Emit updates and wait for the consumer.
-              consumer(null, { updates: updatesFromASinglePack }, cb)
-            }),
-
-          err => {
-            if (err) return consumer(err)
-
-            // Adding undefined can happen for broken updates.
-            accumulatedUserIds.delete('undefined')
-
-            consumer(null, {
-              updates: [],
-              userIds: Array.from(accumulatedUserIds).sort(),
-            })
-          }
-        )
-      })
-    })
-  },
-
-  fetchUserInfo(users, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    const jobs = []
-    const fetchedUserInfo = {}
-    for (const userId in users) {
-      ;(userId =>
-        jobs.push(callback =>
-          WebApiManager.getUserInfo(userId, function (error, userInfo) {
-            if (error != null) {
-              return callback(error)
-            }
-            fetchedUserInfo[userId] = userInfo
-            return callback()
-          })
-        ))(userId)
-    }
-
-    return async.series(jobs, function (err) {
-      if (err != null) {
-        return callback(err)
-      }
-      return callback(null, fetchedUserInfo)
-    })
-  },
-
-  fillUserInfo(updates, callback) {
-    let update, userId
-    if (callback == null) {
-      callback = function () {}
-    }
-    const users = {}
-    for (update of Array.from(updates)) {
-      ;({ user_id: userId } = update.meta)
-      if (UpdatesManager._validUserId(userId)) {
-        users[userId] = true
-      }
-    }
-
-    return UpdatesManager.fetchUserInfo(
-      users,
-      function (error, fetchedUserInfo) {
-        if (error != null) {
-          return callback(error)
-        }
-        for (update of Array.from(updates)) {
-          ;({ user_id: userId } = update.meta)
-          delete update.meta.user_id
-          if (UpdatesManager._validUserId(userId)) {
-            update.meta.user = fetchedUserInfo[userId]
-          }
-        }
-        return callback(null, updates)
-      }
-    )
-  },
-
-  fillSummarizedUserInfo(updates, callback) {
-    let update, userId, userIds
-    if (callback == null) {
-      callback = function () {}
-    }
-    const users = {}
-    for (update of Array.from(updates)) {
-      userIds = update.meta.user_ids || []
-      for (userId of Array.from(userIds)) {
-        if (UpdatesManager._validUserId(userId)) {
-          users[userId] = true
-        }
-      }
-    }
-
-    return UpdatesManager.fetchUserInfo(
-      users,
-      function (error, fetchedUserInfo) {
-        if (error != null) {
-          return callback(error)
-        }
-        for (update of Array.from(updates)) {
-          userIds = update.meta.user_ids || []
-          update.meta.users = []
-          delete update.meta.user_ids
-          for (userId of Array.from(userIds)) {
-            if (UpdatesManager._validUserId(userId)) {
-              update.meta.users.push(fetchedUserInfo[userId])
-            } else {
-              update.meta.users.push(null)
-            }
-          }
-        }
-        return callback(null, updates)
-      }
-    )
-  },
-
-  _validUserId(userId) {
-    if (userId == null) {
-      return false
-    } else {
-      return !!userId.match(/^[a-f0-9]{24}$/)
-    }
-  },
-
-  TIME_BETWEEN_DISTINCT_UPDATES: (fiveMinutes = 5 * 60 * 1000),
-  SPLIT_ON_DELETE_SIZE: 16, // characters
-  _summarizeUpdates(updates, existingSummarizedUpdates) {
-    if (existingSummarizedUpdates == null) {
-      existingSummarizedUpdates = []
-    }
-    const summarizedUpdates = existingSummarizedUpdates.slice()
-    let previousUpdateWasBigDelete = false
-    for (const update of Array.from(updates)) {
-      let docId
-      const earliestUpdate = summarizedUpdates[summarizedUpdates.length - 1]
-      let shouldConcat = false
-
-      // If a user inserts some text, then deletes a big chunk including that text,
-      // the update we show might concat the insert and delete, and there will be no sign
-      // of that insert having happened, or be able to restore to it (restoring after a big delete is common).
-      // So, we split the summary on 'big' deletes. However, we've stepping backwards in time with
-      // most recent changes considered first, so if this update is a big delete, we want to start
-      // a new summarized update next timge, hence we monitor the previous update.
-      if (previousUpdateWasBigDelete) {
-        shouldConcat = false
-      } else if (
-        earliestUpdate &&
-        earliestUpdate.meta.end_ts - update.meta.start_ts <
-          this.TIME_BETWEEN_DISTINCT_UPDATES
-      ) {
-        // We're going backwards in time through the updates, so only combine if this update starts less than 5 minutes before
-        // the end of current summarized block, so no block spans more than 5 minutes.
-        shouldConcat = true
-      }
-
-      let isBigDelete = false
-      for (const op of Array.from(update.op || [])) {
-        if (op.d != null && op.d.length > this.SPLIT_ON_DELETE_SIZE) {
-          isBigDelete = true
-        }
-      }
-
-      previousUpdateWasBigDelete = isBigDelete
-
-      if (shouldConcat) {
-        // check if the user in this update is already present in the earliest update,
-        // if not, add them to the users list of the earliest update
-        earliestUpdate.meta.user_ids = _.union(earliestUpdate.meta.user_ids, [
-          update.meta.user_id,
-        ])
-
-        docId = update.doc_id.toString()
-        const doc = earliestUpdate.docs[docId]
-        if (doc != null) {
-          doc.fromV = Math.min(doc.fromV, update.v)
-          doc.toV = Math.max(doc.toV, update.v)
-        } else {
-          earliestUpdate.docs[docId] = {
-            fromV: update.v,
-            toV: update.v,
-          }
-        }
-
-        earliestUpdate.meta.start_ts = Math.min(
-          earliestUpdate.meta.start_ts,
-          update.meta.start_ts
-        )
-        earliestUpdate.meta.end_ts = Math.max(
-          earliestUpdate.meta.end_ts,
-          update.meta.end_ts
-        )
-      } else {
-        const newUpdate = {
-          meta: {
-            user_ids: [],
-            start_ts: update.meta.start_ts,
-            end_ts: update.meta.end_ts,
-          },
-          docs: {},
-        }
-
-        newUpdate.docs[update.doc_id.toString()] = {
-          fromV: update.v,
-          toV: update.v,
-        }
-        newUpdate.meta.user_ids.push(update.meta.user_id)
-        summarizedUpdates.push(newUpdate)
-      }
-    }
-
-    return summarizedUpdates
-  },
-}
-
-module.exports.promises = {
-  processUncompressedUpdatesForProject: util.promisify(
-    UpdatesManager.processUncompressedUpdatesForProject
-  ),
-}
-
-function __guard__(value, transform) {
-  return typeof value !== 'undefined' && value !== null
-    ? transform(value)
-    : undefined
-}

+ 0 - 112
services/track-changes/app/js/WebApiManager.js

@@ -1,112 +0,0 @@
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-let WebApiManager
-const request = require('requestretry') // allow retry on error https://github.com/FGRibreau/node-request-retry
-const logger = require('@overleaf/logger')
-const Settings = require('@overleaf/settings')
-
-// Don't let HTTP calls hang for a long time
-const MAX_HTTP_REQUEST_LENGTH = 15000 // 15 seconds
-
-// DEPRECATED! This method of getting user details via track-changes is deprecated
-// in the way we lay out our services.
-// Instead, web should be responsible for collecting the raw data (user_ids) and
-// filling it out with calls to other services. All API calls should create a
-// tree-like structure as much as possible, with web as the root.
-module.exports = WebApiManager = {
-  sendRequest(url, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    return request.get(
-      {
-        url: `${Settings.apis.web.url}${url}`,
-        timeout: MAX_HTTP_REQUEST_LENGTH,
-        maxAttempts: 2, // for node-request-retry
-        auth: {
-          user: Settings.apis.web.user,
-          pass: Settings.apis.web.pass,
-          sendImmediately: true,
-        },
-      },
-      function (error, res, body) {
-        if (error != null) {
-          return callback(error)
-        }
-        if (res.statusCode === 404) {
-          logger.debug({ url }, 'got 404 from web api')
-          return callback(null, null)
-        }
-        if (res.statusCode >= 200 && res.statusCode < 300) {
-          return callback(null, body)
-        } else {
-          error = new Error(
-            `web returned a non-success status code: ${res.statusCode} (attempts: ${res.attempts})`
-          )
-          return callback(error)
-        }
-      }
-    )
-  },
-
-  getUserInfo(userId, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    const url = `/user/${userId}/personal_info`
-    logger.debug({ userId }, 'getting user info from web')
-    return WebApiManager.sendRequest(url, function (error, body) {
-      let user
-      if (error != null) {
-        logger.error({ err: error, userId, url }, 'error accessing web')
-        return callback(error)
-      }
-
-      if (body === null) {
-        logger.error({ userId, url }, 'no user found')
-        return callback(null, null)
-      }
-      try {
-        user = JSON.parse(body)
-      } catch (error1) {
-        error = error1
-        return callback(error)
-      }
-      return callback(null, {
-        id: user.id,
-        email: user.email,
-        first_name: user.first_name,
-        last_name: user.last_name,
-      })
-    })
-  },
-
-  getProjectDetails(projectId, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    const url = `/project/${projectId}/details`
-    logger.debug({ projectId }, 'getting project details from web')
-    return WebApiManager.sendRequest(url, function (error, body) {
-      let project
-      if (error != null) {
-        logger.error({ err: error, projectId, url }, 'error accessing web')
-        return callback(error)
-      }
-
-      try {
-        project = JSON.parse(body)
-      } catch (error1) {
-        error = error1
-        return callback(error)
-      }
-      return callback(null, project)
-    })
-  },
-}

+ 0 - 203
services/track-changes/app/js/ZipManager.js

@@ -1,203 +0,0 @@
-const logger = require('@overleaf/logger')
-const UpdatesManager = require('./UpdatesManager')
-const DiffGenerator = require('./DiffGenerator')
-const DocumentUpdaterManager = require('./DocumentUpdaterManager')
-const DocstoreManager = require('./DocstoreManager')
-const Errors = require('./Errors')
-const PackManager = require('./PackManager')
-const yazl = require('yazl')
-const util = require('util')
-const stream = require('stream')
-const fs = require('fs')
-const os = require('os')
-const Path = require('path')
-const { packsAreDuplicated } = require('./util/PackUtils')
-
-const streamPipeline = util.promisify(stream.pipeline)
-
-// look in docstore or docupdater for the latest version of the document
-async function getLatestContent(projectId, docId, lastUpdateVersion) {
-  const [docstoreContent, docstoreVersion] =
-    await DocstoreManager.promises.peekDocument(projectId, docId)
-
-  // if docstore is out of date, check for a newer version in docupdater
-  // and return that instead
-  if (docstoreVersion <= lastUpdateVersion) {
-    const [docupdaterContent, docupdaterVersion] =
-      await DocumentUpdaterManager.promises.peekDocument(projectId, docId)
-    if (docupdaterVersion > docstoreVersion) {
-      return [docupdaterContent, docupdaterVersion]
-    }
-  }
-
-  return [docstoreContent, docstoreVersion]
-}
-
-async function rewindDoc(projectId, docId, zipfile) {
-  logger.debug({ projectId, docId }, 'rewinding document')
-
-  // Prepare to rewind content
-
-  const docIterator = await PackManager.promises.makeDocIterator(docId)
-
-  const getUpdate = util.promisify(docIterator.next).bind(docIterator)
-
-  const lastUpdate = await getUpdate()
-  if (!lastUpdate) {
-    return null
-  }
-
-  const lastUpdateVersion = lastUpdate.v
-
-  let latestContent
-  let version
-  try {
-    ;[latestContent, version] = await getLatestContent(
-      projectId,
-      docId,
-      lastUpdateVersion
-    )
-  } catch (err) {
-    if (err instanceof Errors.NotFoundError) {
-      // Doc not found in docstore. We can't build its history
-      return null
-    } else {
-      throw err
-    }
-  }
-
-  const id = docId.toString()
-
-  const contentEndPath = `${id}/content/end/${version}`
-  zipfile.addBuffer(Buffer.from(latestContent), contentEndPath)
-
-  const metadata = {
-    id,
-    version,
-    content: {
-      end: {
-        path: contentEndPath,
-        version,
-      },
-    },
-    updates: [],
-  }
-
-  let content = latestContent
-  let v = version
-  let update = lastUpdate
-  let previousUpdate = null
-
-  while (update) {
-    if (packsAreDuplicated(update, previousUpdate)) {
-      previousUpdate = update
-      update = await getUpdate()
-      continue
-    }
-
-    const updatePath = `${id}/updates/${update.v}`
-
-    try {
-      content = DiffGenerator.rewindUpdate(content, update)
-      // filter out any known "broken ops" as these may be recoverable
-      update.op = update.op.filter(op => !op.broken)
-      // only store the update in the zip file when we have applied it
-      // successfully, and after filtering out broken ops.
-      zipfile.addBuffer(Buffer.from(JSON.stringify(update)), updatePath, {
-        mtime: new Date(update.meta.start_ts),
-      })
-      v = update.v
-    } catch (e) {
-      e.attempted_update = update // keep a record of the attempted update
-      logger.warn({ projectId, docId, err: e }, 'rewind error')
-      break // stop attempting to rewind on error
-    }
-
-    metadata.updates.push({
-      path: updatePath,
-      version: update.v,
-      ts: update.meta.start_ts,
-      doc_length: content.length,
-    })
-    previousUpdate = update
-    update = await getUpdate()
-  }
-
-  const contentStartPath = `${id}/content/start/${v}`
-  zipfile.addBuffer(Buffer.from(content), contentStartPath)
-
-  metadata.content.start = {
-    path: contentStartPath,
-    version: v,
-  }
-
-  return metadata
-}
-
-async function generateZip(projectId, zipfile) {
-  await UpdatesManager.promises.processUncompressedUpdatesForProject(projectId)
-  const docIds = await PackManager.promises.findAllDocsInProject(projectId)
-  const manifest = { projectId, docs: [] }
-  for (const docId of docIds) {
-    const doc = await rewindDoc(projectId, docId, zipfile)
-    if (doc) {
-      manifest.docs.push(doc)
-    }
-  }
-  zipfile.addBuffer(
-    Buffer.from(JSON.stringify(manifest, null, 2)),
-    'manifest.json'
-  )
-  zipfile.addBuffer(
-    Buffer.from(
-      `History Migration Data
-
-This zip file contains a copy of the raw history for this project.
-
-If this file is present in a project it means the history could not
-be fully recovered or migrated.
-
-A new history should have been created starting at the datestamp of
-this file.`
-    ),
-    'README.txt'
-  )
-  zipfile.end()
-}
-
-async function exportProject(projectId, zipPath) {
-  const zipfile = new yazl.ZipFile()
-  const pipeline = streamPipeline(
-    zipfile.outputStream,
-    fs.createWriteStream(zipPath)
-  )
-  await generateZip(projectId, zipfile)
-  await pipeline
-}
-
-/**
- * Create a temporary directory for use with exportProject()
- */
-async function makeTempDirectory() {
-  const tmpdir = await fs.promises.mkdtemp(
-    (await fs.promises.realpath(os.tmpdir())) + Path.sep
-  )
-  return tmpdir
-}
-
-/**
- * Clean up a temporary directory made with makeTempDirectory()
- */
-function cleanupTempDirectory(tmpdir) {
-  fs.promises.rm(tmpdir, { recursive: true, force: true }).catch(err => {
-    if (err) {
-      logger.warn({ err, tmpdir }, 'Failed to clean up temp directory')
-    }
-  })
-}
-
-module.exports = {
-  exportProject: util.callbackify(exportProject),
-  makeTempDirectory: util.callbackify(makeTempDirectory),
-  cleanupTempDirectory,
-}

+ 0 - 42
services/track-changes/app/js/mongodb.js

@@ -1,42 +0,0 @@
-const Settings = require('@overleaf/settings')
-const { MongoClient, ObjectId } = require('mongodb')
-
-const clientPromise = MongoClient.connect(
-  Settings.mongo.url,
-  Settings.mongo.options
-)
-
-let setupDbPromise
-async function waitForDb() {
-  if (!setupDbPromise) {
-    setupDbPromise = setupDb()
-  }
-  await setupDbPromise
-}
-
-const db = {}
-async function setupDb() {
-  const internalDb = (await clientPromise).db()
-
-  db.docHistory = internalDb.collection('docHistory')
-  db.docHistoryIndex = internalDb.collection('docHistoryIndex')
-  db.projectHistoryMetaData = internalDb.collection('projectHistoryMetaData')
-}
-
-async function closeDb() {
-  let client
-  try {
-    client = await clientPromise
-  } catch (e) {
-    // there is nothing to close
-    return
-  }
-  return client.close()
-}
-
-module.exports = {
-  db,
-  ObjectId,
-  closeDb,
-  waitForDb,
-}

+ 0 - 18
services/track-changes/app/js/util/PackUtils.js

@@ -1,18 +0,0 @@
-const _ = require('lodash')
-
-/**
- * Compares a deep equality of Packs excluding _id
- */
-function packsAreDuplicated(pack1, pack2) {
-  return Boolean(
-    pack1 &&
-      pack2 &&
-      pack1.v === pack2.v &&
-      _.isEqual(pack1.meta, pack2.meta) &&
-      _.isEqual(pack1.op, pack2.op)
-  )
-}
-
-module.exports = {
-  packsAreDuplicated,
-}

+ 0 - 2340
services/track-changes/app/lib/diff_match_patch.js

@@ -1,2340 +0,0 @@
-/* eslint-disable */
-/**
- * Diff Match and Patch
- *
- * Copyright 2006 Google Inc.
- * http://code.google.com/p/google-diff-match-patch/
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-/**
- * @fileoverview Computes the difference between two texts to create a patch.
- * Applies the patch onto another text, allowing for errors.
- * @author fraser@google.com (Neil Fraser)
- */
-
-/**
- * Class containing the diff, match and patch methods.
- * @constructor
- */
-function diff_match_patch() {
-  // Defaults.
-  // Redefine these in your program to override the defaults.
-
-  // Number of seconds to map a diff before giving up (0 for infinity).
-  this.Diff_Timeout = 1.0
-  // Cost of an empty edit operation in terms of edit characters.
-  this.Diff_EditCost = 4
-  // At what point is no match declared (0.0 = perfection, 1.0 = very loose).
-  this.Match_Threshold = 0.5
-  // How far to search for a match (0 = exact location, 1000+ = broad match).
-  // A match this many characters away from the expected location will add
-  // 1.0 to the score (0.0 is a perfect match).
-  this.Match_Distance = 1000
-  // When deleting a large block of text (over ~64 characters), how close do
-  // the contents have to be to match the expected contents. (0.0 = perfection,
-  // 1.0 = very loose).  Note that Match_Threshold controls how closely the
-  // end points of a delete need to match.
-  this.Patch_DeleteThreshold = 0.5
-  // Chunk size for context length.
-  this.Patch_Margin = 4
-
-  // The number of bits in an int.
-  this.Match_MaxBits = 32
-}
-
-//  DIFF FUNCTIONS
-
-/**
- * The data structure representing a diff is an array of tuples:
- * [[DIFF_DELETE, 'Hello'], [DIFF_INSERT, 'Goodbye'], [DIFF_EQUAL, ' world.']]
- * which means: delete 'Hello', add 'Goodbye' and keep ' world.'
- */
-var DIFF_DELETE = -1
-var DIFF_INSERT = 1
-var DIFF_EQUAL = 0
-
-/** @typedef {{0: number, 1: string}} */
-diff_match_patch.Diff
-
-/**
- * Find the differences between two texts.  Simplifies the problem by stripping
- * any common prefix or suffix off the texts before diffing.
- * @param {string} text1 Old string to be diffed.
- * @param {string} text2 New string to be diffed.
- * @param {boolean=} opt_checklines Optional speedup flag. If present and false,
- *     then don't run a line-level diff first to identify the changed areas.
- *     Defaults to true, which does a faster, slightly less optimal diff.
- * @param {number} opt_deadline Optional time when the diff should be complete
- *     by.  Used internally for recursive calls.  Users should set DiffTimeout
- *     instead.
- * @return {!Array.<!diff_match_patch.Diff>} Array of diff tuples.
- */
-diff_match_patch.prototype.diff_main = function (
-  text1,
-  text2,
-  opt_checklines,
-  opt_deadline
-) {
-  // Set a deadline by which time the diff must be complete.
-  if (typeof opt_deadline === 'undefined') {
-    if (this.Diff_Timeout <= 0) {
-      opt_deadline = Number.MAX_VALUE
-    } else {
-      opt_deadline = new Date().getTime() + this.Diff_Timeout * 1000
-    }
-  }
-  var deadline = opt_deadline
-
-  // Check for null inputs.
-  if (text1 == null || text2 == null) {
-    throw new Error('Null input. (diff_main)')
-  }
-
-  // Check for equality (speedup).
-  if (text1 == text2) {
-    if (text1) {
-      return [[DIFF_EQUAL, text1]]
-    }
-    return []
-  }
-
-  if (typeof opt_checklines === 'undefined') {
-    opt_checklines = true
-  }
-  var checklines = opt_checklines
-
-  // Trim off common prefix (speedup).
-  var commonlength = this.diff_commonPrefix(text1, text2)
-  var commonprefix = text1.substring(0, commonlength)
-  text1 = text1.substring(commonlength)
-  text2 = text2.substring(commonlength)
-
-  // Trim off common suffix (speedup).
-  commonlength = this.diff_commonSuffix(text1, text2)
-  var commonsuffix = text1.substring(text1.length - commonlength)
-  text1 = text1.substring(0, text1.length - commonlength)
-  text2 = text2.substring(0, text2.length - commonlength)
-
-  // Compute the diff on the middle block.
-  var diffs = this.diff_compute_(text1, text2, checklines, deadline)
-
-  // Restore the prefix and suffix.
-  if (commonprefix) {
-    diffs.unshift([DIFF_EQUAL, commonprefix])
-  }
-  if (commonsuffix) {
-    diffs.push([DIFF_EQUAL, commonsuffix])
-  }
-  this.diff_cleanupMerge(diffs)
-  return diffs
-}
-
-/**
- * Find the differences between two texts.  Assumes that the texts do not
- * have any common prefix or suffix.
- * @param {string} text1 Old string to be diffed.
- * @param {string} text2 New string to be diffed.
- * @param {boolean} checklines Speedup flag.  If false, then don't run a
- *     line-level diff first to identify the changed areas.
- *     If true, then run a faster, slightly less optimal diff.
- * @param {number} deadline Time when the diff should be complete by.
- * @return {!Array.<!diff_match_patch.Diff>} Array of diff tuples.
- * @private
- */
-diff_match_patch.prototype.diff_compute_ = function (
-  text1,
-  text2,
-  checklines,
-  deadline
-) {
-  var diffs
-
-  if (!text1) {
-    // Just add some text (speedup).
-    return [[DIFF_INSERT, text2]]
-  }
-
-  if (!text2) {
-    // Just delete some text (speedup).
-    return [[DIFF_DELETE, text1]]
-  }
-
-  var longtext = text1.length > text2.length ? text1 : text2
-  var shorttext = text1.length > text2.length ? text2 : text1
-  var i = longtext.indexOf(shorttext)
-  if (i != -1) {
-    // Shorter text is inside the longer text (speedup).
-    diffs = [
-      [DIFF_INSERT, longtext.substring(0, i)],
-      [DIFF_EQUAL, shorttext],
-      [DIFF_INSERT, longtext.substring(i + shorttext.length)],
-    ]
-    // Swap insertions for deletions if diff is reversed.
-    if (text1.length > text2.length) {
-      diffs[0][0] = diffs[2][0] = DIFF_DELETE
-    }
-    return diffs
-  }
-
-  if (shorttext.length == 1) {
-    // Single character string.
-    // After the previous speedup, the character can't be an equality.
-    return [
-      [DIFF_DELETE, text1],
-      [DIFF_INSERT, text2],
-    ]
-  }
-
-  // Check to see if the problem can be split in two.
-  var hm = this.diff_halfMatch_(text1, text2)
-  if (hm) {
-    // A half-match was found, sort out the return data.
-    var text1_a = hm[0]
-    var text1_b = hm[1]
-    var text2_a = hm[2]
-    var text2_b = hm[3]
-    var mid_common = hm[4]
-    // Send both pairs off for separate processing.
-    var diffs_a = this.diff_main(text1_a, text2_a, checklines, deadline)
-    var diffs_b = this.diff_main(text1_b, text2_b, checklines, deadline)
-    // Merge the results.
-    return diffs_a.concat([[DIFF_EQUAL, mid_common]], diffs_b)
-  }
-
-  if (checklines && text1.length > 100 && text2.length > 100) {
-    return this.diff_lineMode_(text1, text2, deadline)
-  }
-
-  return this.diff_bisect_(text1, text2, deadline)
-}
-
-/**
- * Do a quick line-level diff on both strings, then rediff the parts for
- * greater accuracy.
- * This speedup can produce non-minimal diffs.
- * @param {string} text1 Old string to be diffed.
- * @param {string} text2 New string to be diffed.
- * @param {number} deadline Time when the diff should be complete by.
- * @return {!Array.<!diff_match_patch.Diff>} Array of diff tuples.
- * @private
- */
-diff_match_patch.prototype.diff_lineMode_ = function (text1, text2, deadline) {
-  // Scan the text on a line-by-line basis first.
-  var a = this.diff_linesToChars_(text1, text2)
-  text1 = a.chars1
-  text2 = a.chars2
-  var linearray = a.lineArray
-
-  var diffs = this.diff_main(text1, text2, false, deadline)
-
-  // Convert the diff back to original text.
-  this.diff_charsToLines_(diffs, linearray)
-  // Eliminate freak matches (e.g. blank lines)
-  this.diff_cleanupSemantic(diffs)
-
-  // Rediff any replacement blocks, this time character-by-character.
-  // Add a dummy entry at the end.
-  diffs.push([DIFF_EQUAL, ''])
-  var pointer = 0
-  var count_delete = 0
-  var count_insert = 0
-  var text_delete = ''
-  var text_insert = ''
-  while (pointer < diffs.length) {
-    switch (diffs[pointer][0]) {
-      case DIFF_INSERT:
-        count_insert++
-        text_insert += diffs[pointer][1]
-        break
-      case DIFF_DELETE:
-        count_delete++
-        text_delete += diffs[pointer][1]
-        break
-      case DIFF_EQUAL:
-        // Upon reaching an equality, check for prior redundancies.
-        if (count_delete >= 1 && count_insert >= 1) {
-          // Delete the offending records and add the merged ones.
-          diffs.splice(
-            pointer - count_delete - count_insert,
-            count_delete + count_insert
-          )
-          pointer = pointer - count_delete - count_insert
-          var a = this.diff_main(text_delete, text_insert, false, deadline)
-          for (var j = a.length - 1; j >= 0; j--) {
-            diffs.splice(pointer, 0, a[j])
-          }
-          pointer = pointer + a.length
-        }
-        count_insert = 0
-        count_delete = 0
-        text_delete = ''
-        text_insert = ''
-        break
-    }
-    pointer++
-  }
-  diffs.pop() // Remove the dummy entry at the end.
-
-  return diffs
-}
-
-/**
- * Find the 'middle snake' of a diff, split the problem in two
- * and return the recursively constructed diff.
- * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations.
- * @param {string} text1 Old string to be diffed.
- * @param {string} text2 New string to be diffed.
- * @param {number} deadline Time at which to bail if not yet complete.
- * @return {!Array.<!diff_match_patch.Diff>} Array of diff tuples.
- * @private
- */
-diff_match_patch.prototype.diff_bisect_ = function (text1, text2, deadline) {
-  // Cache the text lengths to prevent multiple calls.
-  var text1_length = text1.length
-  var text2_length = text2.length
-  var max_d = Math.ceil((text1_length + text2_length) / 2)
-  var v_offset = max_d
-  var v_length = 2 * max_d
-  var v1 = new Array(v_length)
-  var v2 = new Array(v_length)
-  // Setting all elements to -1 is faster in Chrome & Firefox than mixing
-  // integers and undefined.
-  for (var x = 0; x < v_length; x++) {
-    v1[x] = -1
-    v2[x] = -1
-  }
-  v1[v_offset + 1] = 0
-  v2[v_offset + 1] = 0
-  var delta = text1_length - text2_length
-  // If the total number of characters is odd, then the front path will collide
-  // with the reverse path.
-  var front = delta % 2 != 0
-  // Offsets for start and end of k loop.
-  // Prevents mapping of space beyond the grid.
-  var k1start = 0
-  var k1end = 0
-  var k2start = 0
-  var k2end = 0
-  for (var d = 0; d < max_d; d++) {
-    // Bail out if deadline is reached.
-    if (new Date().getTime() > deadline) {
-      break
-    }
-
-    // Walk the front path one step.
-    for (var k1 = -d + k1start; k1 <= d - k1end; k1 += 2) {
-      var k1_offset = v_offset + k1
-      var x1
-      if (k1 == -d || (k1 != d && v1[k1_offset - 1] < v1[k1_offset + 1])) {
-        x1 = v1[k1_offset + 1]
-      } else {
-        x1 = v1[k1_offset - 1] + 1
-      }
-      var y1 = x1 - k1
-      while (
-        x1 < text1_length &&
-        y1 < text2_length &&
-        text1.charAt(x1) == text2.charAt(y1)
-      ) {
-        x1++
-        y1++
-      }
-      v1[k1_offset] = x1
-      if (x1 > text1_length) {
-        // Ran off the right of the graph.
-        k1end += 2
-      } else if (y1 > text2_length) {
-        // Ran off the bottom of the graph.
-        k1start += 2
-      } else if (front) {
-        var k2_offset = v_offset + delta - k1
-        if (k2_offset >= 0 && k2_offset < v_length && v2[k2_offset] != -1) {
-          // Mirror x2 onto top-left coordinate system.
-          var x2 = text1_length - v2[k2_offset]
-          if (x1 >= x2) {
-            // Overlap detected.
-            return this.diff_bisectSplit_(text1, text2, x1, y1, deadline)
-          }
-        }
-      }
-    }
-
-    // Walk the reverse path one step.
-    for (var k2 = -d + k2start; k2 <= d - k2end; k2 += 2) {
-      var k2_offset = v_offset + k2
-      var x2
-      if (k2 == -d || (k2 != d && v2[k2_offset - 1] < v2[k2_offset + 1])) {
-        x2 = v2[k2_offset + 1]
-      } else {
-        x2 = v2[k2_offset - 1] + 1
-      }
-      var y2 = x2 - k2
-      while (
-        x2 < text1_length &&
-        y2 < text2_length &&
-        text1.charAt(text1_length - x2 - 1) ==
-          text2.charAt(text2_length - y2 - 1)
-      ) {
-        x2++
-        y2++
-      }
-      v2[k2_offset] = x2
-      if (x2 > text1_length) {
-        // Ran off the left of the graph.
-        k2end += 2
-      } else if (y2 > text2_length) {
-        // Ran off the top of the graph.
-        k2start += 2
-      } else if (!front) {
-        var k1_offset = v_offset + delta - k2
-        if (k1_offset >= 0 && k1_offset < v_length && v1[k1_offset] != -1) {
-          var x1 = v1[k1_offset]
-          var y1 = v_offset + x1 - k1_offset
-          // Mirror x2 onto top-left coordinate system.
-          x2 = text1_length - x2
-          if (x1 >= x2) {
-            // Overlap detected.
-            return this.diff_bisectSplit_(text1, text2, x1, y1, deadline)
-          }
-        }
-      }
-    }
-  }
-  // Diff took too long and hit the deadline or
-  // number of diffs equals number of characters, no commonality at all.
-  return [
-    [DIFF_DELETE, text1],
-    [DIFF_INSERT, text2],
-  ]
-}
-
-/**
- * Given the location of the 'middle snake', split the diff in two parts
- * and recurse.
- * @param {string} text1 Old string to be diffed.
- * @param {string} text2 New string to be diffed.
- * @param {number} x Index of split point in text1.
- * @param {number} y Index of split point in text2.
- * @param {number} deadline Time at which to bail if not yet complete.
- * @return {!Array.<!diff_match_patch.Diff>} Array of diff tuples.
- * @private
- */
-diff_match_patch.prototype.diff_bisectSplit_ = function (
-  text1,
-  text2,
-  x,
-  y,
-  deadline
-) {
-  var text1a = text1.substring(0, x)
-  var text2a = text2.substring(0, y)
-  var text1b = text1.substring(x)
-  var text2b = text2.substring(y)
-
-  // Compute both diffs serially.
-  var diffs = this.diff_main(text1a, text2a, false, deadline)
-  var diffsb = this.diff_main(text1b, text2b, false, deadline)
-
-  return diffs.concat(diffsb)
-}
-
-/**
- * Split two texts into an array of strings.  Reduce the texts to a string of
- * hashes where each Unicode character represents one line.
- * @param {string} text1 First string.
- * @param {string} text2 Second string.
- * @return {{chars1: string, chars2: string, lineArray: !Array.<string>}}
- *     An object containing the encoded text1, the encoded text2 and
- *     the array of unique strings.
- *     The zeroth element of the array of unique strings is intentionally blank.
- * @private
- */
-diff_match_patch.prototype.diff_linesToChars_ = function (text1, text2) {
-  var lineArray = [] // e.g. lineArray[4] == 'Hello\n'
-  var lineHash = {} // e.g. lineHash['Hello\n'] == 4
-
-  // '\x00' is a valid character, but various debuggers don't like it.
-  // So we'll insert a junk entry to avoid generating a null character.
-  lineArray[0] = ''
-
-  /**
-   * Split a text into an array of strings.  Reduce the texts to a string of
-   * hashes where each Unicode character represents one line.
-   * Modifies linearray and linehash through being a closure.
-   * @param {string} text String to encode.
-   * @return {string} Encoded string.
-   * @private
-   */
-  function diff_linesToCharsMunge_(text) {
-    var chars = ''
-    // Walk the text, pulling out a substring for each line.
-    // text.split('\n') would would temporarily double our memory footprint.
-    // Modifying text would create many large strings to garbage collect.
-    var lineStart = 0
-    var lineEnd = -1
-    // Keeping our own length variable is faster than looking it up.
-    var lineArrayLength = lineArray.length
-    while (lineEnd < text.length - 1) {
-      lineEnd = text.indexOf('\n', lineStart)
-      if (lineEnd == -1) {
-        lineEnd = text.length - 1
-      }
-      var line = text.substring(lineStart, lineEnd + 1)
-      lineStart = lineEnd + 1
-
-      if (
-        lineHash.hasOwnProperty
-          ? lineHash.hasOwnProperty(line)
-          : lineHash[line] !== undefined
-      ) {
-        chars += String.fromCharCode(lineHash[line])
-      } else {
-        chars += String.fromCharCode(lineArrayLength)
-        lineHash[line] = lineArrayLength
-        lineArray[lineArrayLength++] = line
-      }
-    }
-    return chars
-  }
-
-  var chars1 = diff_linesToCharsMunge_(text1)
-  var chars2 = diff_linesToCharsMunge_(text2)
-  return { chars1: chars1, chars2: chars2, lineArray: lineArray }
-}
-
-/**
- * Rehydrate the text in a diff from a string of line hashes to real lines of
- * text.
- * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples.
- * @param {!Array.<string>} lineArray Array of unique strings.
- * @private
- */
-diff_match_patch.prototype.diff_charsToLines_ = function (diffs, lineArray) {
-  for (var x = 0; x < diffs.length; x++) {
-    var chars = diffs[x][1]
-    var text = []
-    for (var y = 0; y < chars.length; y++) {
-      text[y] = lineArray[chars.charCodeAt(y)]
-    }
-    diffs[x][1] = text.join('')
-  }
-}
-
-/**
- * Determine the common prefix of two strings.
- * @param {string} text1 First string.
- * @param {string} text2 Second string.
- * @return {number} The number of characters common to the start of each
- *     string.
- */
-diff_match_patch.prototype.diff_commonPrefix = function (text1, text2) {
-  // Quick check for common null cases.
-  if (!text1 || !text2 || text1.charAt(0) != text2.charAt(0)) {
-    return 0
-  }
-  // Binary search.
-  // Performance analysis: http://neil.fraser.name/news/2007/10/09/
-  var pointermin = 0
-  var pointermax = Math.min(text1.length, text2.length)
-  var pointermid = pointermax
-  var pointerstart = 0
-  while (pointermin < pointermid) {
-    if (
-      text1.substring(pointerstart, pointermid) ==
-      text2.substring(pointerstart, pointermid)
-    ) {
-      pointermin = pointermid
-      pointerstart = pointermin
-    } else {
-      pointermax = pointermid
-    }
-    pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin)
-  }
-  return pointermid
-}
-
-/**
- * Determine the common suffix of two strings.
- * @param {string} text1 First string.
- * @param {string} text2 Second string.
- * @return {number} The number of characters common to the end of each string.
- */
-diff_match_patch.prototype.diff_commonSuffix = function (text1, text2) {
-  // Quick check for common null cases.
-  if (
-    !text1 ||
-    !text2 ||
-    text1.charAt(text1.length - 1) != text2.charAt(text2.length - 1)
-  ) {
-    return 0
-  }
-  // Binary search.
-  // Performance analysis: http://neil.fraser.name/news/2007/10/09/
-  var pointermin = 0
-  var pointermax = Math.min(text1.length, text2.length)
-  var pointermid = pointermax
-  var pointerend = 0
-  while (pointermin < pointermid) {
-    if (
-      text1.substring(text1.length - pointermid, text1.length - pointerend) ==
-      text2.substring(text2.length - pointermid, text2.length - pointerend)
-    ) {
-      pointermin = pointermid
-      pointerend = pointermin
-    } else {
-      pointermax = pointermid
-    }
-    pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin)
-  }
-  return pointermid
-}
-
-/**
- * Determine if the suffix of one string is the prefix of another.
- * @param {string} text1 First string.
- * @param {string} text2 Second string.
- * @return {number} The number of characters common to the end of the first
- *     string and the start of the second string.
- * @private
- */
-diff_match_patch.prototype.diff_commonOverlap_ = function (text1, text2) {
-  // Cache the text lengths to prevent multiple calls.
-  var text1_length = text1.length
-  var text2_length = text2.length
-  // Eliminate the null case.
-  if (text1_length == 0 || text2_length == 0) {
-    return 0
-  }
-  // Truncate the longer string.
-  if (text1_length > text2_length) {
-    text1 = text1.substring(text1_length - text2_length)
-  } else if (text1_length < text2_length) {
-    text2 = text2.substring(0, text1_length)
-  }
-  var text_length = Math.min(text1_length, text2_length)
-  // Quick check for the worst case.
-  if (text1 == text2) {
-    return text_length
-  }
-
-  // Start by looking for a single character match
-  // and increase length until no match is found.
-  // Performance analysis: http://neil.fraser.name/news/2010/11/04/
-  var best = 0
-  var length = 1
-  while (true) {
-    var pattern = text1.substring(text_length - length)
-    var found = text2.indexOf(pattern)
-    if (found == -1) {
-      return best
-    }
-    length += found
-    if (
-      found == 0 ||
-      text1.substring(text_length - length) == text2.substring(0, length)
-    ) {
-      best = length
-      length++
-    }
-  }
-}
-
-/**
- * Do the two texts share a substring which is at least half the length of the
- * longer text?
- * This speedup can produce non-minimal diffs.
- * @param {string} text1 First string.
- * @param {string} text2 Second string.
- * @return {Array.<string>} Five element Array, containing the prefix of
- *     text1, the suffix of text1, the prefix of text2, the suffix of
- *     text2 and the common middle.  Or null if there was no match.
- * @private
- */
-diff_match_patch.prototype.diff_halfMatch_ = function (text1, text2) {
-  if (this.Diff_Timeout <= 0) {
-    // Don't risk returning a non-optimal diff if we have unlimited time.
-    return null
-  }
-  var longtext = text1.length > text2.length ? text1 : text2
-  var shorttext = text1.length > text2.length ? text2 : text1
-  if (longtext.length < 4 || shorttext.length * 2 < longtext.length) {
-    return null // Pointless.
-  }
-  var dmp = this // 'this' becomes 'window' in a closure.
-
-  /**
-   * Does a substring of shorttext exist within longtext such that the substring
-   * is at least half the length of longtext?
-   * Closure, but does not reference any external variables.
-   * @param {string} longtext Longer string.
-   * @param {string} shorttext Shorter string.
-   * @param {number} i Start index of quarter length substring within longtext.
-   * @return {Array.<string>} Five element Array, containing the prefix of
-   *     longtext, the suffix of longtext, the prefix of shorttext, the suffix
-   *     of shorttext and the common middle.  Or null if there was no match.
-   * @private
-   */
-  function diff_halfMatchI_(longtext, shorttext, i) {
-    // Start with a 1/4 length substring at position i as a seed.
-    var seed = longtext.substring(i, i + Math.floor(longtext.length / 4))
-    var j = -1
-    var best_common = ''
-    var best_longtext_a, best_longtext_b, best_shorttext_a, best_shorttext_b
-    while ((j = shorttext.indexOf(seed, j + 1)) != -1) {
-      var prefixLength = dmp.diff_commonPrefix(
-        longtext.substring(i),
-        shorttext.substring(j)
-      )
-      var suffixLength = dmp.diff_commonSuffix(
-        longtext.substring(0, i),
-        shorttext.substring(0, j)
-      )
-      if (best_common.length < suffixLength + prefixLength) {
-        best_common =
-          shorttext.substring(j - suffixLength, j) +
-          shorttext.substring(j, j + prefixLength)
-        best_longtext_a = longtext.substring(0, i - suffixLength)
-        best_longtext_b = longtext.substring(i + prefixLength)
-        best_shorttext_a = shorttext.substring(0, j - suffixLength)
-        best_shorttext_b = shorttext.substring(j + prefixLength)
-      }
-    }
-    if (best_common.length * 2 >= longtext.length) {
-      return [
-        best_longtext_a,
-        best_longtext_b,
-        best_shorttext_a,
-        best_shorttext_b,
-        best_common,
-      ]
-    } else {
-      return null
-    }
-  }
-
-  // First check if the second quarter is the seed for a half-match.
-  var hm1 = diff_halfMatchI_(
-    longtext,
-    shorttext,
-    Math.ceil(longtext.length / 4)
-  )
-  // Check again based on the third quarter.
-  var hm2 = diff_halfMatchI_(
-    longtext,
-    shorttext,
-    Math.ceil(longtext.length / 2)
-  )
-  var hm
-  if (!hm1 && !hm2) {
-    return null
-  } else if (!hm2) {
-    hm = hm1
-  } else if (!hm1) {
-    hm = hm2
-  } else {
-    // Both matched.  Select the longest.
-    hm = hm1[4].length > hm2[4].length ? hm1 : hm2
-  }
-
-  // A half-match was found, sort out the return data.
-  var text1_a, text1_b, text2_a, text2_b
-  if (text1.length > text2.length) {
-    text1_a = hm[0]
-    text1_b = hm[1]
-    text2_a = hm[2]
-    text2_b = hm[3]
-  } else {
-    text2_a = hm[0]
-    text2_b = hm[1]
-    text1_a = hm[2]
-    text1_b = hm[3]
-  }
-  var mid_common = hm[4]
-  return [text1_a, text1_b, text2_a, text2_b, mid_common]
-}
-
-/**
- * Reduce the number of edits by eliminating semantically trivial equalities.
- * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples.
- */
-diff_match_patch.prototype.diff_cleanupSemantic = function (diffs) {
-  var changes = false
-  var equalities = [] // Stack of indices where equalities are found.
-  var equalitiesLength = 0 // Keeping our own length var is faster in JS.
-  /** @type {?string} */
-  var lastequality = null
-  // Always equal to diffs[equalities[equalitiesLength - 1]][1]
-  var pointer = 0 // Index of current position.
-  // Number of characters that changed prior to the equality.
-  var length_insertions1 = 0
-  var length_deletions1 = 0
-  // Number of characters that changed after the equality.
-  var length_insertions2 = 0
-  var length_deletions2 = 0
-  while (pointer < diffs.length) {
-    if (diffs[pointer][0] == DIFF_EQUAL) {
-      // Equality found.
-      equalities[equalitiesLength++] = pointer
-      length_insertions1 = length_insertions2
-      length_deletions1 = length_deletions2
-      length_insertions2 = 0
-      length_deletions2 = 0
-      lastequality = diffs[pointer][1]
-    } else {
-      // An insertion or deletion.
-      if (diffs[pointer][0] == DIFF_INSERT) {
-        length_insertions2 += diffs[pointer][1].length
-      } else {
-        length_deletions2 += diffs[pointer][1].length
-      }
-      // Eliminate an equality that is smaller or equal to the edits on both
-      // sides of it.
-      if (
-        lastequality &&
-        lastequality.length <=
-          Math.max(length_insertions1, length_deletions1) &&
-        lastequality.length <= Math.max(length_insertions2, length_deletions2)
-      ) {
-        // Duplicate record.
-        diffs.splice(equalities[equalitiesLength - 1], 0, [
-          DIFF_DELETE,
-          lastequality,
-        ])
-        // Change second copy to insert.
-        diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT
-        // Throw away the equality we just deleted.
-        equalitiesLength--
-        // Throw away the previous equality (it needs to be reevaluated).
-        equalitiesLength--
-        pointer = equalitiesLength > 0 ? equalities[equalitiesLength - 1] : -1
-        length_insertions1 = 0 // Reset the counters.
-        length_deletions1 = 0
-        length_insertions2 = 0
-        length_deletions2 = 0
-        lastequality = null
-        changes = true
-      }
-    }
-    pointer++
-  }
-
-  // Normalize the diff.
-  if (changes) {
-    this.diff_cleanupMerge(diffs)
-  }
-  this.diff_cleanupSemanticLossless(diffs)
-
-  // Find any overlaps between deletions and insertions.
-  // e.g: <del>abcxxx</del><ins>xxxdef</ins>
-  //   -> <del>abc</del>xxx<ins>def</ins>
-  // e.g: <del>xxxabc</del><ins>defxxx</ins>
-  //   -> <ins>def</ins>xxx<del>abc</del>
-  // Only extract an overlap if it is as big as the edit ahead or behind it.
-  pointer = 1
-  while (pointer < diffs.length) {
-    if (
-      diffs[pointer - 1][0] == DIFF_DELETE &&
-      diffs[pointer][0] == DIFF_INSERT
-    ) {
-      var deletion = diffs[pointer - 1][1]
-      var insertion = diffs[pointer][1]
-      var overlap_length1 = this.diff_commonOverlap_(deletion, insertion)
-      var overlap_length2 = this.diff_commonOverlap_(insertion, deletion)
-      if (overlap_length1 >= overlap_length2) {
-        if (
-          overlap_length1 >= deletion.length / 2 ||
-          overlap_length1 >= insertion.length / 2
-        ) {
-          // Overlap found.  Insert an equality and trim the surrounding edits.
-          diffs.splice(pointer, 0, [
-            DIFF_EQUAL,
-            insertion.substring(0, overlap_length1),
-          ])
-          diffs[pointer - 1][1] = deletion.substring(
-            0,
-            deletion.length - overlap_length1
-          )
-          diffs[pointer + 1][1] = insertion.substring(overlap_length1)
-          pointer++
-        }
-      } else {
-        if (
-          overlap_length2 >= deletion.length / 2 ||
-          overlap_length2 >= insertion.length / 2
-        ) {
-          // Reverse overlap found.
-          // Insert an equality and swap and trim the surrounding edits.
-          diffs.splice(pointer, 0, [
-            DIFF_EQUAL,
-            deletion.substring(0, overlap_length2),
-          ])
-          diffs[pointer - 1][0] = DIFF_INSERT
-          diffs[pointer - 1][1] = insertion.substring(
-            0,
-            insertion.length - overlap_length2
-          )
-          diffs[pointer + 1][0] = DIFF_DELETE
-          diffs[pointer + 1][1] = deletion.substring(overlap_length2)
-          pointer++
-        }
-      }
-      pointer++
-    }
-    pointer++
-  }
-}
-
-/**
- * Look for single edits surrounded on both sides by equalities
- * which can be shifted sideways to align the edit to a word boundary.
- * e.g: The c<ins>at c</ins>ame. -> The <ins>cat </ins>came.
- * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples.
- */
-diff_match_patch.prototype.diff_cleanupSemanticLossless = function (diffs) {
-  /**
-   * Given two strings, compute a score representing whether the internal
-   * boundary falls on logical boundaries.
-   * Scores range from 6 (best) to 0 (worst).
-   * Closure, but does not reference any external variables.
-   * @param {string} one First string.
-   * @param {string} two Second string.
-   * @return {number} The score.
-   * @private
-   */
-  function diff_cleanupSemanticScore_(one, two) {
-    if (!one || !two) {
-      // Edges are the best.
-      return 6
-    }
-
-    // Each port of this function behaves slightly differently due to
-    // subtle differences in each language's definition of things like
-    // 'whitespace'.  Since this function's purpose is largely cosmetic,
-    // the choice has been made to use each language's native features
-    // rather than force total conformity.
-    var char1 = one.charAt(one.length - 1)
-    var char2 = two.charAt(0)
-    var nonAlphaNumeric1 = char1.match(diff_match_patch.nonAlphaNumericRegex_)
-    var nonAlphaNumeric2 = char2.match(diff_match_patch.nonAlphaNumericRegex_)
-    var whitespace1 =
-      nonAlphaNumeric1 && char1.match(diff_match_patch.whitespaceRegex_)
-    var whitespace2 =
-      nonAlphaNumeric2 && char2.match(diff_match_patch.whitespaceRegex_)
-    var lineBreak1 =
-      whitespace1 && char1.match(diff_match_patch.linebreakRegex_)
-    var lineBreak2 =
-      whitespace2 && char2.match(diff_match_patch.linebreakRegex_)
-    var blankLine1 =
-      lineBreak1 && one.match(diff_match_patch.blanklineEndRegex_)
-    var blankLine2 =
-      lineBreak2 && two.match(diff_match_patch.blanklineStartRegex_)
-
-    if (blankLine1 || blankLine2) {
-      // Five points for blank lines.
-      return 5
-    } else if (lineBreak1 || lineBreak2) {
-      // Four points for line breaks.
-      return 4
-    } else if (nonAlphaNumeric1 && !whitespace1 && whitespace2) {
-      // Three points for end of sentences.
-      return 3
-    } else if (whitespace1 || whitespace2) {
-      // Two points for whitespace.
-      return 2
-    } else if (nonAlphaNumeric1 || nonAlphaNumeric2) {
-      // One point for non-alphanumeric.
-      return 1
-    }
-    return 0
-  }
-
-  var pointer = 1
-  // Intentionally ignore the first and last element (don't need checking).
-  while (pointer < diffs.length - 1) {
-    if (
-      diffs[pointer - 1][0] == DIFF_EQUAL &&
-      diffs[pointer + 1][0] == DIFF_EQUAL
-    ) {
-      // This is a single edit surrounded by equalities.
-      var equality1 = diffs[pointer - 1][1]
-      var edit = diffs[pointer][1]
-      var equality2 = diffs[pointer + 1][1]
-
-      // First, shift the edit as far left as possible.
-      var commonOffset = this.diff_commonSuffix(equality1, edit)
-      if (commonOffset) {
-        var commonString = edit.substring(edit.length - commonOffset)
-        equality1 = equality1.substring(0, equality1.length - commonOffset)
-        edit = commonString + edit.substring(0, edit.length - commonOffset)
-        equality2 = commonString + equality2
-      }
-
-      // Second, step character by character right, looking for the best fit.
-      var bestEquality1 = equality1
-      var bestEdit = edit
-      var bestEquality2 = equality2
-      var bestScore =
-        diff_cleanupSemanticScore_(equality1, edit) +
-        diff_cleanupSemanticScore_(edit, equality2)
-      while (edit.charAt(0) === equality2.charAt(0)) {
-        equality1 += edit.charAt(0)
-        edit = edit.substring(1) + equality2.charAt(0)
-        equality2 = equality2.substring(1)
-        var score =
-          diff_cleanupSemanticScore_(equality1, edit) +
-          diff_cleanupSemanticScore_(edit, equality2)
-        // The >= encourages trailing rather than leading whitespace on edits.
-        if (score >= bestScore) {
-          bestScore = score
-          bestEquality1 = equality1
-          bestEdit = edit
-          bestEquality2 = equality2
-        }
-      }
-
-      if (diffs[pointer - 1][1] != bestEquality1) {
-        // We have an improvement, save it back to the diff.
-        if (bestEquality1) {
-          diffs[pointer - 1][1] = bestEquality1
-        } else {
-          diffs.splice(pointer - 1, 1)
-          pointer--
-        }
-        diffs[pointer][1] = bestEdit
-        if (bestEquality2) {
-          diffs[pointer + 1][1] = bestEquality2
-        } else {
-          diffs.splice(pointer + 1, 1)
-          pointer--
-        }
-      }
-    }
-    pointer++
-  }
-}
-
-// Define some regex patterns for matching boundaries.
-diff_match_patch.nonAlphaNumericRegex_ = /[^a-zA-Z0-9]/
-diff_match_patch.whitespaceRegex_ = /\s/
-diff_match_patch.linebreakRegex_ = /[\r\n]/
-diff_match_patch.blanklineEndRegex_ = /\n\r?\n$/
-diff_match_patch.blanklineStartRegex_ = /^\r?\n\r?\n/
-
-/**
- * Reduce the number of edits by eliminating operationally trivial equalities.
- * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples.
- */
-diff_match_patch.prototype.diff_cleanupEfficiency = function (diffs) {
-  var changes = false
-  var equalities = [] // Stack of indices where equalities are found.
-  var equalitiesLength = 0 // Keeping our own length var is faster in JS.
-  /** @type {?string} */
-  var lastequality = null
-  // Always equal to diffs[equalities[equalitiesLength - 1]][1]
-  var pointer = 0 // Index of current position.
-  // Is there an insertion operation before the last equality.
-  var pre_ins = false
-  // Is there a deletion operation before the last equality.
-  var pre_del = false
-  // Is there an insertion operation after the last equality.
-  var post_ins = false
-  // Is there a deletion operation after the last equality.
-  var post_del = false
-  while (pointer < diffs.length) {
-    if (diffs[pointer][0] == DIFF_EQUAL) {
-      // Equality found.
-      if (
-        diffs[pointer][1].length < this.Diff_EditCost &&
-        (post_ins || post_del)
-      ) {
-        // Candidate found.
-        equalities[equalitiesLength++] = pointer
-        pre_ins = post_ins
-        pre_del = post_del
-        lastequality = diffs[pointer][1]
-      } else {
-        // Not a candidate, and can never become one.
-        equalitiesLength = 0
-        lastequality = null
-      }
-      post_ins = post_del = false
-    } else {
-      // An insertion or deletion.
-      if (diffs[pointer][0] == DIFF_DELETE) {
-        post_del = true
-      } else {
-        post_ins = true
-      }
-      /*
-       * Five types to be split:
-       * <ins>A</ins><del>B</del>XY<ins>C</ins><del>D</del>
-       * <ins>A</ins>X<ins>C</ins><del>D</del>
-       * <ins>A</ins><del>B</del>X<ins>C</ins>
-       * <ins>A</del>X<ins>C</ins><del>D</del>
-       * <ins>A</ins><del>B</del>X<del>C</del>
-       */
-      if (
-        lastequality &&
-        ((pre_ins && pre_del && post_ins && post_del) ||
-          (lastequality.length < this.Diff_EditCost / 2 &&
-            pre_ins + pre_del + post_ins + post_del == 3))
-      ) {
-        // Duplicate record.
-        diffs.splice(equalities[equalitiesLength - 1], 0, [
-          DIFF_DELETE,
-          lastequality,
-        ])
-        // Change second copy to insert.
-        diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT
-        equalitiesLength-- // Throw away the equality we just deleted;
-        lastequality = null
-        if (pre_ins && pre_del) {
-          // No changes made which could affect previous entry, keep going.
-          post_ins = post_del = true
-          equalitiesLength = 0
-        } else {
-          equalitiesLength-- // Throw away the previous equality.
-          pointer = equalitiesLength > 0 ? equalities[equalitiesLength - 1] : -1
-          post_ins = post_del = false
-        }
-        changes = true
-      }
-    }
-    pointer++
-  }
-
-  if (changes) {
-    this.diff_cleanupMerge(diffs)
-  }
-}
-
-/**
- * Reorder and merge like edit sections.  Merge equalities.
- * Any edit section can move as long as it doesn't cross an equality.
- * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples.
- */
-diff_match_patch.prototype.diff_cleanupMerge = function (diffs) {
-  diffs.push([DIFF_EQUAL, '']) // Add a dummy entry at the end.
-  var pointer = 0
-  var count_delete = 0
-  var count_insert = 0
-  var text_delete = ''
-  var text_insert = ''
-  var commonlength
-  while (pointer < diffs.length) {
-    switch (diffs[pointer][0]) {
-      case DIFF_INSERT:
-        count_insert++
-        text_insert += diffs[pointer][1]
-        pointer++
-        break
-      case DIFF_DELETE:
-        count_delete++
-        text_delete += diffs[pointer][1]
-        pointer++
-        break
-      case DIFF_EQUAL:
-        // Upon reaching an equality, check for prior redundancies.
-        if (count_delete + count_insert > 1) {
-          if (count_delete !== 0 && count_insert !== 0) {
-            // Factor out any common prefixies.
-            commonlength = this.diff_commonPrefix(text_insert, text_delete)
-            if (commonlength !== 0) {
-              if (
-                pointer - count_delete - count_insert > 0 &&
-                diffs[pointer - count_delete - count_insert - 1][0] ==
-                  DIFF_EQUAL
-              ) {
-                diffs[pointer - count_delete - count_insert - 1][1] +=
-                  text_insert.substring(0, commonlength)
-              } else {
-                diffs.splice(0, 0, [
-                  DIFF_EQUAL,
-                  text_insert.substring(0, commonlength),
-                ])
-                pointer++
-              }
-              text_insert = text_insert.substring(commonlength)
-              text_delete = text_delete.substring(commonlength)
-            }
-            // Factor out any common suffixies.
-            commonlength = this.diff_commonSuffix(text_insert, text_delete)
-            if (commonlength !== 0) {
-              diffs[pointer][1] =
-                text_insert.substring(text_insert.length - commonlength) +
-                diffs[pointer][1]
-              text_insert = text_insert.substring(
-                0,
-                text_insert.length - commonlength
-              )
-              text_delete = text_delete.substring(
-                0,
-                text_delete.length - commonlength
-              )
-            }
-          }
-          // Delete the offending records and add the merged ones.
-          if (count_delete === 0) {
-            diffs.splice(pointer - count_insert, count_delete + count_insert, [
-              DIFF_INSERT,
-              text_insert,
-            ])
-          } else if (count_insert === 0) {
-            diffs.splice(pointer - count_delete, count_delete + count_insert, [
-              DIFF_DELETE,
-              text_delete,
-            ])
-          } else {
-            diffs.splice(
-              pointer - count_delete - count_insert,
-              count_delete + count_insert,
-              [DIFF_DELETE, text_delete],
-              [DIFF_INSERT, text_insert]
-            )
-          }
-          pointer =
-            pointer -
-            count_delete -
-            count_insert +
-            (count_delete ? 1 : 0) +
-            (count_insert ? 1 : 0) +
-            1
-        } else if (pointer !== 0 && diffs[pointer - 1][0] == DIFF_EQUAL) {
-          // Merge this equality with the previous one.
-          diffs[pointer - 1][1] += diffs[pointer][1]
-          diffs.splice(pointer, 1)
-        } else {
-          pointer++
-        }
-        count_insert = 0
-        count_delete = 0
-        text_delete = ''
-        text_insert = ''
-        break
-    }
-  }
-  if (diffs[diffs.length - 1][1] === '') {
-    diffs.pop() // Remove the dummy entry at the end.
-  }
-
-  // Second pass: look for single edits surrounded on both sides by equalities
-  // which can be shifted sideways to eliminate an equality.
-  // e.g: A<ins>BA</ins>C -> <ins>AB</ins>AC
-  var changes = false
-  pointer = 1
-  // Intentionally ignore the first and last element (don't need checking).
-  while (pointer < diffs.length - 1) {
-    if (
-      diffs[pointer - 1][0] == DIFF_EQUAL &&
-      diffs[pointer + 1][0] == DIFF_EQUAL
-    ) {
-      // This is a single edit surrounded by equalities.
-      if (
-        diffs[pointer][1].substring(
-          diffs[pointer][1].length - diffs[pointer - 1][1].length
-        ) == diffs[pointer - 1][1]
-      ) {
-        // Shift the edit over the previous equality.
-        diffs[pointer][1] =
-          diffs[pointer - 1][1] +
-          diffs[pointer][1].substring(
-            0,
-            diffs[pointer][1].length - diffs[pointer - 1][1].length
-          )
-        diffs[pointer + 1][1] = diffs[pointer - 1][1] + diffs[pointer + 1][1]
-        diffs.splice(pointer - 1, 1)
-        changes = true
-      } else if (
-        diffs[pointer][1].substring(0, diffs[pointer + 1][1].length) ==
-        diffs[pointer + 1][1]
-      ) {
-        // Shift the edit over the next equality.
-        diffs[pointer - 1][1] += diffs[pointer + 1][1]
-        diffs[pointer][1] =
-          diffs[pointer][1].substring(diffs[pointer + 1][1].length) +
-          diffs[pointer + 1][1]
-        diffs.splice(pointer + 1, 1)
-        changes = true
-      }
-    }
-    pointer++
-  }
-  // If shifts were made, the diff needs reordering and another shift sweep.
-  if (changes) {
-    this.diff_cleanupMerge(diffs)
-  }
-}
-
-/**
- * loc is a location in text1, compute and return the equivalent location in
- * text2.
- * e.g. 'The cat' vs 'The big cat', 1->1, 5->8
- * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples.
- * @param {number} loc Location within text1.
- * @return {number} Location within text2.
- */
-diff_match_patch.prototype.diff_xIndex = function (diffs, loc) {
-  var chars1 = 0
-  var chars2 = 0
-  var last_chars1 = 0
-  var last_chars2 = 0
-  var x
-  for (x = 0; x < diffs.length; x++) {
-    if (diffs[x][0] !== DIFF_INSERT) {
-      // Equality or deletion.
-      chars1 += diffs[x][1].length
-    }
-    if (diffs[x][0] !== DIFF_DELETE) {
-      // Equality or insertion.
-      chars2 += diffs[x][1].length
-    }
-    if (chars1 > loc) {
-      // Overshot the location.
-      break
-    }
-    last_chars1 = chars1
-    last_chars2 = chars2
-  }
-  // Was the location was deleted?
-  if (diffs.length != x && diffs[x][0] === DIFF_DELETE) {
-    return last_chars2
-  }
-  // Add the remaining character length.
-  return last_chars2 + (loc - last_chars1)
-}
-
-/**
- * Convert a diff array into a pretty HTML report.
- * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples.
- * @return {string} HTML representation.
- */
-diff_match_patch.prototype.diff_prettyHtml = function (diffs) {
-  var html = []
-  var pattern_amp = /&/g
-  var pattern_lt = /</g
-  var pattern_gt = />/g
-  var pattern_para = /\n/g
-  for (var x = 0; x < diffs.length; x++) {
-    var op = diffs[x][0] // Operation (insert, delete, equal)
-    var data = diffs[x][1] // Text of change.
-    var text = data
-      .replace(pattern_amp, '&amp;')
-      .replace(pattern_lt, '&lt;')
-      .replace(pattern_gt, '&gt;')
-      .replace(pattern_para, '&para;<br>')
-    switch (op) {
-      case DIFF_INSERT:
-        html[x] = '<ins style="background:#e6ffe6;">' + text + '</ins>'
-        break
-      case DIFF_DELETE:
-        html[x] = '<del style="background:#ffe6e6;">' + text + '</del>'
-        break
-      case DIFF_EQUAL:
-        html[x] = '<span>' + text + '</span>'
-        break
-    }
-  }
-  return html.join('')
-}
-
-/**
- * Compute and return the source text (all equalities and deletions).
- * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples.
- * @return {string} Source text.
- */
-diff_match_patch.prototype.diff_text1 = function (diffs) {
-  var text = []
-  for (var x = 0; x < diffs.length; x++) {
-    if (diffs[x][0] !== DIFF_INSERT) {
-      text[x] = diffs[x][1]
-    }
-  }
-  return text.join('')
-}
-
-/**
- * Compute and return the destination text (all equalities and insertions).
- * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples.
- * @return {string} Destination text.
- */
-diff_match_patch.prototype.diff_text2 = function (diffs) {
-  var text = []
-  for (var x = 0; x < diffs.length; x++) {
-    if (diffs[x][0] !== DIFF_DELETE) {
-      text[x] = diffs[x][1]
-    }
-  }
-  return text.join('')
-}
-
-/**
- * Compute the Levenshtein distance; the number of inserted, deleted or
- * substituted characters.
- * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples.
- * @return {number} Number of changes.
- */
-diff_match_patch.prototype.diff_levenshtein = function (diffs) {
-  var levenshtein = 0
-  var insertions = 0
-  var deletions = 0
-  for (var x = 0; x < diffs.length; x++) {
-    var op = diffs[x][0]
-    var data = diffs[x][1]
-    switch (op) {
-      case DIFF_INSERT:
-        insertions += data.length
-        break
-      case DIFF_DELETE:
-        deletions += data.length
-        break
-      case DIFF_EQUAL:
-        // A deletion and an insertion is one substitution.
-        levenshtein += Math.max(insertions, deletions)
-        insertions = 0
-        deletions = 0
-        break
-    }
-  }
-  levenshtein += Math.max(insertions, deletions)
-  return levenshtein
-}
-
-/**
- * Crush the diff into an encoded string which describes the operations
- * required to transform text1 into text2.
- * E.g. =3\t-2\t+ing  -> Keep 3 chars, delete 2 chars, insert 'ing'.
- * Operations are tab-separated.  Inserted text is escaped using %xx notation.
- * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples.
- * @return {string} Delta text.
- */
-diff_match_patch.prototype.diff_toDelta = function (diffs) {
-  var text = []
-  for (var x = 0; x < diffs.length; x++) {
-    switch (diffs[x][0]) {
-      case DIFF_INSERT:
-        text[x] = '+' + encodeURI(diffs[x][1])
-        break
-      case DIFF_DELETE:
-        text[x] = '-' + diffs[x][1].length
-        break
-      case DIFF_EQUAL:
-        text[x] = '=' + diffs[x][1].length
-        break
-    }
-  }
-  return text.join('\t').replace(/%20/g, ' ')
-}
-
-/**
- * Given the original text1, and an encoded string which describes the
- * operations required to transform text1 into text2, compute the full diff.
- * @param {string} text1 Source string for the diff.
- * @param {string} delta Delta text.
- * @return {!Array.<!diff_match_patch.Diff>} Array of diff tuples.
- * @throws {!Error} If invalid input.
- */
-diff_match_patch.prototype.diff_fromDelta = function (text1, delta) {
-  var diffs = []
-  var diffsLength = 0 // Keeping our own length var is faster in JS.
-  var pointer = 0 // Cursor in text1
-  var tokens = delta.split(/\t/g)
-  for (var x = 0; x < tokens.length; x++) {
-    // Each token begins with a one character parameter which specifies the
-    // operation of this token (delete, insert, equality).
-    var param = tokens[x].substring(1)
-    switch (tokens[x].charAt(0)) {
-      case '+':
-        try {
-          diffs[diffsLength++] = [DIFF_INSERT, decodeURI(param)]
-        } catch (ex) {
-          // Malformed URI sequence.
-          throw new Error('Illegal escape in diff_fromDelta: ' + param)
-        }
-        break
-      case '-':
-      // Fall through.
-      case '=':
-        var n = parseInt(param, 10)
-        if (isNaN(n) || n < 0) {
-          throw new Error('Invalid number in diff_fromDelta: ' + param)
-        }
-        var text = text1.substring(pointer, (pointer += n))
-        if (tokens[x].charAt(0) == '=') {
-          diffs[diffsLength++] = [DIFF_EQUAL, text]
-        } else {
-          diffs[diffsLength++] = [DIFF_DELETE, text]
-        }
-        break
-      default:
-        // Blank tokens are ok (from a trailing \t).
-        // Anything else is an error.
-        if (tokens[x]) {
-          throw new Error(
-            'Invalid diff operation in diff_fromDelta: ' + tokens[x]
-          )
-        }
-    }
-  }
-  if (pointer != text1.length) {
-    throw new Error(
-      'Delta length (' +
-        pointer +
-        ') does not equal source text length (' +
-        text1.length +
-        ').'
-    )
-  }
-  return diffs
-}
-
-//  MATCH FUNCTIONS
-
-/**
- * Locate the best instance of 'pattern' in 'text' near 'loc'.
- * @param {string} text The text to search.
- * @param {string} pattern The pattern to search for.
- * @param {number} loc The location to search around.
- * @return {number} Best match index or -1.
- */
-diff_match_patch.prototype.match_main = function (text, pattern, loc) {
-  // Check for null inputs.
-  if (text == null || pattern == null || loc == null) {
-    throw new Error('Null input. (match_main)')
-  }
-
-  loc = Math.max(0, Math.min(loc, text.length))
-  if (text == pattern) {
-    // Shortcut (potentially not guaranteed by the algorithm)
-    return 0
-  } else if (!text.length) {
-    // Nothing to match.
-    return -1
-  } else if (text.substring(loc, loc + pattern.length) == pattern) {
-    // Perfect match at the perfect spot!  (Includes case of null pattern)
-    return loc
-  } else {
-    // Do a fuzzy compare.
-    return this.match_bitap_(text, pattern, loc)
-  }
-}
-
-/**
- * Locate the best instance of 'pattern' in 'text' near 'loc' using the
- * Bitap algorithm.
- * @param {string} text The text to search.
- * @param {string} pattern The pattern to search for.
- * @param {number} loc The location to search around.
- * @return {number} Best match index or -1.
- * @private
- */
-diff_match_patch.prototype.match_bitap_ = function (text, pattern, loc) {
-  if (pattern.length > this.Match_MaxBits) {
-    throw new Error('Pattern too long for this browser.')
-  }
-
-  // Initialise the alphabet.
-  var s = this.match_alphabet_(pattern)
-
-  var dmp = this // 'this' becomes 'window' in a closure.
-
-  /**
-   * Compute and return the score for a match with e errors and x location.
-   * Accesses loc and pattern through being a closure.
-   * @param {number} e Number of errors in match.
-   * @param {number} x Location of match.
-   * @return {number} Overall score for match (0.0 = good, 1.0 = bad).
-   * @private
-   */
-  function match_bitapScore_(e, x) {
-    var accuracy = e / pattern.length
-    var proximity = Math.abs(loc - x)
-    if (!dmp.Match_Distance) {
-      // Dodge divide by zero error.
-      return proximity ? 1.0 : accuracy
-    }
-    return accuracy + proximity / dmp.Match_Distance
-  }
-
-  // Highest score beyond which we give up.
-  var score_threshold = this.Match_Threshold
-  // Is there a nearby exact match? (speedup)
-  var best_loc = text.indexOf(pattern, loc)
-  if (best_loc != -1) {
-    score_threshold = Math.min(match_bitapScore_(0, best_loc), score_threshold)
-    // What about in the other direction? (speedup)
-    best_loc = text.lastIndexOf(pattern, loc + pattern.length)
-    if (best_loc != -1) {
-      score_threshold = Math.min(
-        match_bitapScore_(0, best_loc),
-        score_threshold
-      )
-    }
-  }
-
-  // Initialise the bit arrays.
-  var matchmask = 1 << (pattern.length - 1)
-  best_loc = -1
-
-  var bin_min, bin_mid
-  var bin_max = pattern.length + text.length
-  var last_rd
-  for (var d = 0; d < pattern.length; d++) {
-    // Scan for the best match; each iteration allows for one more error.
-    // Run a binary search to determine how far from 'loc' we can stray at this
-    // error level.
-    bin_min = 0
-    bin_mid = bin_max
-    while (bin_min < bin_mid) {
-      if (match_bitapScore_(d, loc + bin_mid) <= score_threshold) {
-        bin_min = bin_mid
-      } else {
-        bin_max = bin_mid
-      }
-      bin_mid = Math.floor((bin_max - bin_min) / 2 + bin_min)
-    }
-    // Use the result from this iteration as the maximum for the next.
-    bin_max = bin_mid
-    var start = Math.max(1, loc - bin_mid + 1)
-    var finish = Math.min(loc + bin_mid, text.length) + pattern.length
-
-    var rd = Array(finish + 2)
-    rd[finish + 1] = (1 << d) - 1
-    for (var j = finish; j >= start; j--) {
-      // The alphabet (s) is a sparse hash, so the following line generates
-      // warnings.
-      var charMatch = s[text.charAt(j - 1)]
-      if (d === 0) {
-        // First pass: exact match.
-        rd[j] = ((rd[j + 1] << 1) | 1) & charMatch
-      } else {
-        // Subsequent passes: fuzzy match.
-        rd[j] =
-          (((rd[j + 1] << 1) | 1) & charMatch) |
-          (((last_rd[j + 1] | last_rd[j]) << 1) | 1) |
-          last_rd[j + 1]
-      }
-      if (rd[j] & matchmask) {
-        var score = match_bitapScore_(d, j - 1)
-        // This match will almost certainly be better than any existing match.
-        // But check anyway.
-        if (score <= score_threshold) {
-          // Told you so.
-          score_threshold = score
-          best_loc = j - 1
-          if (best_loc > loc) {
-            // When passing loc, don't exceed our current distance from loc.
-            start = Math.max(1, 2 * loc - best_loc)
-          } else {
-            // Already passed loc, downhill from here on in.
-            break
-          }
-        }
-      }
-    }
-    // No hope for a (better) match at greater error levels.
-    if (match_bitapScore_(d + 1, loc) > score_threshold) {
-      break
-    }
-    last_rd = rd
-  }
-  return best_loc
-}
-
-/**
- * Initialise the alphabet for the Bitap algorithm.
- * @param {string} pattern The text to encode.
- * @return {!Object} Hash of character locations.
- * @private
- */
-diff_match_patch.prototype.match_alphabet_ = function (pattern) {
-  var s = {}
-  for (var i = 0; i < pattern.length; i++) {
-    s[pattern.charAt(i)] = 0
-  }
-  for (var i = 0; i < pattern.length; i++) {
-    s[pattern.charAt(i)] |= 1 << (pattern.length - i - 1)
-  }
-  return s
-}
-
-//  PATCH FUNCTIONS
-
-/**
- * Increase the context until it is unique,
- * but don't let the pattern expand beyond Match_MaxBits.
- * @param {!diff_match_patch.patch_obj} patch The patch to grow.
- * @param {string} text Source text.
- * @private
- */
-diff_match_patch.prototype.patch_addContext_ = function (patch, text) {
-  if (text.length == 0) {
-    return
-  }
-  var pattern = text.substring(patch.start2, patch.start2 + patch.length1)
-  var padding = 0
-
-  // Look for the first and last matches of pattern in text.  If two different
-  // matches are found, increase the pattern length.
-  while (
-    text.indexOf(pattern) != text.lastIndexOf(pattern) &&
-    pattern.length < this.Match_MaxBits - this.Patch_Margin - this.Patch_Margin
-  ) {
-    padding += this.Patch_Margin
-    pattern = text.substring(
-      patch.start2 - padding,
-      patch.start2 + patch.length1 + padding
-    )
-  }
-  // Add one chunk for good luck.
-  padding += this.Patch_Margin
-
-  // Add the prefix.
-  var prefix = text.substring(patch.start2 - padding, patch.start2)
-  if (prefix) {
-    patch.diffs.unshift([DIFF_EQUAL, prefix])
-  }
-  // Add the suffix.
-  var suffix = text.substring(
-    patch.start2 + patch.length1,
-    patch.start2 + patch.length1 + padding
-  )
-  if (suffix) {
-    patch.diffs.push([DIFF_EQUAL, suffix])
-  }
-
-  // Roll back the start points.
-  patch.start1 -= prefix.length
-  patch.start2 -= prefix.length
-  // Extend the lengths.
-  patch.length1 += prefix.length + suffix.length
-  patch.length2 += prefix.length + suffix.length
-}
-
-/**
- * Compute a list of patches to turn text1 into text2.
- * Use diffs if provided, otherwise compute it ourselves.
- * There are four ways to call this function, depending on what data is
- * available to the caller:
- * Method 1:
- * a = text1, b = text2
- * Method 2:
- * a = diffs
- * Method 3 (optimal):
- * a = text1, b = diffs
- * Method 4 (deprecated, use method 3):
- * a = text1, b = text2, c = diffs
- *
- * @param {string|!Array.<!diff_match_patch.Diff>} a text1 (methods 1,3,4) or
- * Array of diff tuples for text1 to text2 (method 2).
- * @param {string|!Array.<!diff_match_patch.Diff>} opt_b text2 (methods 1,4) or
- * Array of diff tuples for text1 to text2 (method 3) or undefined (method 2).
- * @param {string|!Array.<!diff_match_patch.Diff>} opt_c Array of diff tuples
- * for text1 to text2 (method 4) or undefined (methods 1,2,3).
- * @return {!Array.<!diff_match_patch.patch_obj>} Array of Patch objects.
- */
-diff_match_patch.prototype.patch_make = function (a, opt_b, opt_c) {
-  var text1, diffs
-  if (
-    typeof a === 'string' &&
-    typeof opt_b === 'string' &&
-    typeof opt_c === 'undefined'
-  ) {
-    // Method 1: text1, text2
-    // Compute diffs from text1 and text2.
-    text1 = /** @type {string} */ (a)
-    diffs = this.diff_main(text1, /** @type {string} */ (opt_b), true)
-    if (diffs.length > 2) {
-      this.diff_cleanupSemantic(diffs)
-      this.diff_cleanupEfficiency(diffs)
-    }
-  } else if (
-    a &&
-    typeof a === 'object' &&
-    typeof opt_b === 'undefined' &&
-    typeof opt_c === 'undefined'
-  ) {
-    // Method 2: diffs
-    // Compute text1 from diffs.
-    diffs = /** @type {!Array.<!diff_match_patch.Diff>} */ (a)
-    text1 = this.diff_text1(diffs)
-  } else if (
-    typeof a === 'string' &&
-    opt_b &&
-    typeof opt_b === 'object' &&
-    typeof opt_c === 'undefined'
-  ) {
-    // Method 3: text1, diffs
-    text1 = /** @type {string} */ (a)
-    diffs = /** @type {!Array.<!diff_match_patch.Diff>} */ (opt_b)
-  } else if (
-    typeof a === 'string' &&
-    typeof opt_b === 'string' &&
-    opt_c &&
-    typeof opt_c === 'object'
-  ) {
-    // Method 4: text1, text2, diffs
-    // text2 is not used.
-    text1 = /** @type {string} */ (a)
-    diffs = /** @type {!Array.<!diff_match_patch.Diff>} */ (opt_c)
-  } else {
-    throw new Error('Unknown call format to patch_make.')
-  }
-
-  if (diffs.length === 0) {
-    return [] // Get rid of the null case.
-  }
-  var patches = []
-  var patch = new diff_match_patch.patch_obj()
-  var patchDiffLength = 0 // Keeping our own length var is faster in JS.
-  var char_count1 = 0 // Number of characters into the text1 string.
-  var char_count2 = 0 // Number of characters into the text2 string.
-  // Start with text1 (prepatch_text) and apply the diffs until we arrive at
-  // text2 (postpatch_text).  We recreate the patches one by one to determine
-  // context info.
-  var prepatch_text = text1
-  var postpatch_text = text1
-  for (var x = 0; x < diffs.length; x++) {
-    var diff_type = diffs[x][0]
-    var diff_text = diffs[x][1]
-
-    if (!patchDiffLength && diff_type !== DIFF_EQUAL) {
-      // A new patch starts here.
-      patch.start1 = char_count1
-      patch.start2 = char_count2
-    }
-
-    switch (diff_type) {
-      case DIFF_INSERT:
-        patch.diffs[patchDiffLength++] = diffs[x]
-        patch.length2 += diff_text.length
-        postpatch_text =
-          postpatch_text.substring(0, char_count2) +
-          diff_text +
-          postpatch_text.substring(char_count2)
-        break
-      case DIFF_DELETE:
-        patch.length1 += diff_text.length
-        patch.diffs[patchDiffLength++] = diffs[x]
-        postpatch_text =
-          postpatch_text.substring(0, char_count2) +
-          postpatch_text.substring(char_count2 + diff_text.length)
-        break
-      case DIFF_EQUAL:
-        if (
-          diff_text.length <= 2 * this.Patch_Margin &&
-          patchDiffLength &&
-          diffs.length != x + 1
-        ) {
-          // Small equality inside a patch.
-          patch.diffs[patchDiffLength++] = diffs[x]
-          patch.length1 += diff_text.length
-          patch.length2 += diff_text.length
-        } else if (diff_text.length >= 2 * this.Patch_Margin) {
-          // Time for a new patch.
-          if (patchDiffLength) {
-            this.patch_addContext_(patch, prepatch_text)
-            patches.push(patch)
-            patch = new diff_match_patch.patch_obj()
-            patchDiffLength = 0
-            // Unlike Unidiff, our patch lists have a rolling context.
-            // http://code.google.com/p/google-diff-match-patch/wiki/Unidiff
-            // Update prepatch text & pos to reflect the application of the
-            // just completed patch.
-            prepatch_text = postpatch_text
-            char_count1 = char_count2
-          }
-        }
-        break
-    }
-
-    // Update the current character count.
-    if (diff_type !== DIFF_INSERT) {
-      char_count1 += diff_text.length
-    }
-    if (diff_type !== DIFF_DELETE) {
-      char_count2 += diff_text.length
-    }
-  }
-  // Pick up the leftover patch if not empty.
-  if (patchDiffLength) {
-    this.patch_addContext_(patch, prepatch_text)
-    patches.push(patch)
-  }
-
-  return patches
-}
-
-/**
- * Given an array of patches, return another array that is identical.
- * @param {!Array.<!diff_match_patch.patch_obj>} patches Array of Patch objects.
- * @return {!Array.<!diff_match_patch.patch_obj>} Array of Patch objects.
- */
-diff_match_patch.prototype.patch_deepCopy = function (patches) {
-  // Making deep copies is hard in JavaScript.
-  var patchesCopy = []
-  for (var x = 0; x < patches.length; x++) {
-    var patch = patches[x]
-    var patchCopy = new diff_match_patch.patch_obj()
-    patchCopy.diffs = []
-    for (var y = 0; y < patch.diffs.length; y++) {
-      patchCopy.diffs[y] = patch.diffs[y].slice()
-    }
-    patchCopy.start1 = patch.start1
-    patchCopy.start2 = patch.start2
-    patchCopy.length1 = patch.length1
-    patchCopy.length2 = patch.length2
-    patchesCopy[x] = patchCopy
-  }
-  return patchesCopy
-}
-
-/**
- * Merge a set of patches onto the text.  Return a patched text, as well
- * as a list of true/false values indicating which patches were applied.
- * @param {!Array.<!diff_match_patch.patch_obj>} patches Array of Patch objects.
- * @param {string} text Old text.
- * @return {!Array.<string|!Array.<boolean>>} Two element Array, containing the
- *      new text and an array of boolean values.
- */
-diff_match_patch.prototype.patch_apply = function (patches, text) {
-  if (patches.length == 0) {
-    return [text, []]
-  }
-
-  // Deep copy the patches so that no changes are made to originals.
-  patches = this.patch_deepCopy(patches)
-
-  var nullPadding = this.patch_addPadding(patches)
-  text = nullPadding + text + nullPadding
-
-  this.patch_splitMax(patches)
-  // delta keeps track of the offset between the expected and actual location
-  // of the previous patch.  If there are patches expected at positions 10 and
-  // 20, but the first patch was found at 12, delta is 2 and the second patch
-  // has an effective expected position of 22.
-  var delta = 0
-  var results = []
-  for (var x = 0; x < patches.length; x++) {
-    var expected_loc = patches[x].start2 + delta
-    var text1 = this.diff_text1(patches[x].diffs)
-    var start_loc
-    var end_loc = -1
-    if (text1.length > this.Match_MaxBits) {
-      // patch_splitMax will only provide an oversized pattern in the case of
-      // a monster delete.
-      start_loc = this.match_main(
-        text,
-        text1.substring(0, this.Match_MaxBits),
-        expected_loc
-      )
-      if (start_loc != -1) {
-        end_loc = this.match_main(
-          text,
-          text1.substring(text1.length - this.Match_MaxBits),
-          expected_loc + text1.length - this.Match_MaxBits
-        )
-        if (end_loc == -1 || start_loc >= end_loc) {
-          // Can't find valid trailing context.  Drop this patch.
-          start_loc = -1
-        }
-      }
-    } else {
-      start_loc = this.match_main(text, text1, expected_loc)
-    }
-    if (start_loc == -1) {
-      // No match found.  :(
-      results[x] = false
-      // Subtract the delta for this failed patch from subsequent patches.
-      delta -= patches[x].length2 - patches[x].length1
-    } else {
-      // Found a match.  :)
-      results[x] = true
-      delta = start_loc - expected_loc
-      var text2
-      if (end_loc == -1) {
-        text2 = text.substring(start_loc, start_loc + text1.length)
-      } else {
-        text2 = text.substring(start_loc, end_loc + this.Match_MaxBits)
-      }
-      if (text1 == text2) {
-        // Perfect match, just shove the replacement text in.
-        text =
-          text.substring(0, start_loc) +
-          this.diff_text2(patches[x].diffs) +
-          text.substring(start_loc + text1.length)
-      } else {
-        // Imperfect match.  Run a diff to get a framework of equivalent
-        // indices.
-        var diffs = this.diff_main(text1, text2, false)
-        if (
-          text1.length > this.Match_MaxBits &&
-          this.diff_levenshtein(diffs) / text1.length >
-            this.Patch_DeleteThreshold
-        ) {
-          // The end points match, but the content is unacceptably bad.
-          results[x] = false
-        } else {
-          this.diff_cleanupSemanticLossless(diffs)
-          var index1 = 0
-          var index2
-          for (var y = 0; y < patches[x].diffs.length; y++) {
-            var mod = patches[x].diffs[y]
-            if (mod[0] !== DIFF_EQUAL) {
-              index2 = this.diff_xIndex(diffs, index1)
-            }
-            if (mod[0] === DIFF_INSERT) {
-              // Insertion
-              text =
-                text.substring(0, start_loc + index2) +
-                mod[1] +
-                text.substring(start_loc + index2)
-            } else if (mod[0] === DIFF_DELETE) {
-              // Deletion
-              text =
-                text.substring(0, start_loc + index2) +
-                text.substring(
-                  start_loc + this.diff_xIndex(diffs, index1 + mod[1].length)
-                )
-            }
-            if (mod[0] !== DIFF_DELETE) {
-              index1 += mod[1].length
-            }
-          }
-        }
-      }
-    }
-  }
-  // Strip the padding off.
-  text = text.substring(nullPadding.length, text.length - nullPadding.length)
-  return [text, results]
-}
-
-/**
- * Add some padding on text start and end so that edges can match something.
- * Intended to be called only from within patch_apply.
- * @param {!Array.<!diff_match_patch.patch_obj>} patches Array of Patch objects.
- * @return {string} The padding string added to each side.
- */
-diff_match_patch.prototype.patch_addPadding = function (patches) {
-  var paddingLength = this.Patch_Margin
-  var nullPadding = ''
-  for (var x = 1; x <= paddingLength; x++) {
-    nullPadding += String.fromCharCode(x)
-  }
-
-  // Bump all the patches forward.
-  for (var x = 0; x < patches.length; x++) {
-    patches[x].start1 += paddingLength
-    patches[x].start2 += paddingLength
-  }
-
-  // Add some padding on start of first diff.
-  var patch = patches[0]
-  var diffs = patch.diffs
-  if (diffs.length == 0 || diffs[0][0] != DIFF_EQUAL) {
-    // Add nullPadding equality.
-    diffs.unshift([DIFF_EQUAL, nullPadding])
-    patch.start1 -= paddingLength // Should be 0.
-    patch.start2 -= paddingLength // Should be 0.
-    patch.length1 += paddingLength
-    patch.length2 += paddingLength
-  } else if (paddingLength > diffs[0][1].length) {
-    // Grow first equality.
-    var extraLength = paddingLength - diffs[0][1].length
-    diffs[0][1] = nullPadding.substring(diffs[0][1].length) + diffs[0][1]
-    patch.start1 -= extraLength
-    patch.start2 -= extraLength
-    patch.length1 += extraLength
-    patch.length2 += extraLength
-  }
-
-  // Add some padding on end of last diff.
-  patch = patches[patches.length - 1]
-  diffs = patch.diffs
-  if (diffs.length == 0 || diffs[diffs.length - 1][0] != DIFF_EQUAL) {
-    // Add nullPadding equality.
-    diffs.push([DIFF_EQUAL, nullPadding])
-    patch.length1 += paddingLength
-    patch.length2 += paddingLength
-  } else if (paddingLength > diffs[diffs.length - 1][1].length) {
-    // Grow last equality.
-    var extraLength = paddingLength - diffs[diffs.length - 1][1].length
-    diffs[diffs.length - 1][1] += nullPadding.substring(0, extraLength)
-    patch.length1 += extraLength
-    patch.length2 += extraLength
-  }
-
-  return nullPadding
-}
-
-/**
- * Look through the patches and break up any which are longer than the maximum
- * limit of the match algorithm.
- * Intended to be called only from within patch_apply.
- * @param {!Array.<!diff_match_patch.patch_obj>} patches Array of Patch objects.
- */
-diff_match_patch.prototype.patch_splitMax = function (patches) {
-  var patch_size = this.Match_MaxBits
-  for (var x = 0; x < patches.length; x++) {
-    if (patches[x].length1 <= patch_size) {
-      continue
-    }
-    var bigpatch = patches[x]
-    // Remove the big old patch.
-    patches.splice(x--, 1)
-    var start1 = bigpatch.start1
-    var start2 = bigpatch.start2
-    var precontext = ''
-    while (bigpatch.diffs.length !== 0) {
-      // Create one of several smaller patches.
-      var patch = new diff_match_patch.patch_obj()
-      var empty = true
-      patch.start1 = start1 - precontext.length
-      patch.start2 = start2 - precontext.length
-      if (precontext !== '') {
-        patch.length1 = patch.length2 = precontext.length
-        patch.diffs.push([DIFF_EQUAL, precontext])
-      }
-      while (
-        bigpatch.diffs.length !== 0 &&
-        patch.length1 < patch_size - this.Patch_Margin
-      ) {
-        var diff_type = bigpatch.diffs[0][0]
-        var diff_text = bigpatch.diffs[0][1]
-        if (diff_type === DIFF_INSERT) {
-          // Insertions are harmless.
-          patch.length2 += diff_text.length
-          start2 += diff_text.length
-          patch.diffs.push(bigpatch.diffs.shift())
-          empty = false
-        } else if (
-          diff_type === DIFF_DELETE &&
-          patch.diffs.length == 1 &&
-          patch.diffs[0][0] == DIFF_EQUAL &&
-          diff_text.length > 2 * patch_size
-        ) {
-          // This is a large deletion.  Let it pass in one chunk.
-          patch.length1 += diff_text.length
-          start1 += diff_text.length
-          empty = false
-          patch.diffs.push([diff_type, diff_text])
-          bigpatch.diffs.shift()
-        } else {
-          // Deletion or equality.  Only take as much as we can stomach.
-          diff_text = diff_text.substring(
-            0,
-            patch_size - patch.length1 - this.Patch_Margin
-          )
-          patch.length1 += diff_text.length
-          start1 += diff_text.length
-          if (diff_type === DIFF_EQUAL) {
-            patch.length2 += diff_text.length
-            start2 += diff_text.length
-          } else {
-            empty = false
-          }
-          patch.diffs.push([diff_type, diff_text])
-          if (diff_text == bigpatch.diffs[0][1]) {
-            bigpatch.diffs.shift()
-          } else {
-            bigpatch.diffs[0][1] = bigpatch.diffs[0][1].substring(
-              diff_text.length
-            )
-          }
-        }
-      }
-      // Compute the head context for the next patch.
-      precontext = this.diff_text2(patch.diffs)
-      precontext = precontext.substring(precontext.length - this.Patch_Margin)
-      // Append the end context for this patch.
-      var postcontext = this.diff_text1(bigpatch.diffs).substring(
-        0,
-        this.Patch_Margin
-      )
-      if (postcontext !== '') {
-        patch.length1 += postcontext.length
-        patch.length2 += postcontext.length
-        if (
-          patch.diffs.length !== 0 &&
-          patch.diffs[patch.diffs.length - 1][0] === DIFF_EQUAL
-        ) {
-          patch.diffs[patch.diffs.length - 1][1] += postcontext
-        } else {
-          patch.diffs.push([DIFF_EQUAL, postcontext])
-        }
-      }
-      if (!empty) {
-        patches.splice(++x, 0, patch)
-      }
-    }
-  }
-}
-
-/**
- * Take a list of patches and return a textual representation.
- * @param {!Array.<!diff_match_patch.patch_obj>} patches Array of Patch objects.
- * @return {string} Text representation of patches.
- */
-diff_match_patch.prototype.patch_toText = function (patches) {
-  var text = []
-  for (var x = 0; x < patches.length; x++) {
-    text[x] = patches[x]
-  }
-  return text.join('')
-}
-
-/**
- * Parse a textual representation of patches and return a list of Patch objects.
- * @param {string} textline Text representation of patches.
- * @return {!Array.<!diff_match_patch.patch_obj>} Array of Patch objects.
- * @throws {!Error} If invalid input.
- */
-diff_match_patch.prototype.patch_fromText = function (textline) {
-  var patches = []
-  if (!textline) {
-    return patches
-  }
-  var text = textline.split('\n')
-  var textPointer = 0
-  var patchHeader = /^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@$/
-  while (textPointer < text.length) {
-    var m = text[textPointer].match(patchHeader)
-    if (!m) {
-      throw new Error('Invalid patch string: ' + text[textPointer])
-    }
-    var patch = new diff_match_patch.patch_obj()
-    patches.push(patch)
-    patch.start1 = parseInt(m[1], 10)
-    if (m[2] === '') {
-      patch.start1--
-      patch.length1 = 1
-    } else if (m[2] == '0') {
-      patch.length1 = 0
-    } else {
-      patch.start1--
-      patch.length1 = parseInt(m[2], 10)
-    }
-
-    patch.start2 = parseInt(m[3], 10)
-    if (m[4] === '') {
-      patch.start2--
-      patch.length2 = 1
-    } else if (m[4] == '0') {
-      patch.length2 = 0
-    } else {
-      patch.start2--
-      patch.length2 = parseInt(m[4], 10)
-    }
-    textPointer++
-
-    while (textPointer < text.length) {
-      var sign = text[textPointer].charAt(0)
-      try {
-        var line = decodeURI(text[textPointer].substring(1))
-      } catch (ex) {
-        // Malformed URI sequence.
-        throw new Error('Illegal escape in patch_fromText: ' + line)
-      }
-      if (sign == '-') {
-        // Deletion.
-        patch.diffs.push([DIFF_DELETE, line])
-      } else if (sign == '+') {
-        // Insertion.
-        patch.diffs.push([DIFF_INSERT, line])
-      } else if (sign == ' ') {
-        // Minor equality.
-        patch.diffs.push([DIFF_EQUAL, line])
-      } else if (sign == '@') {
-        // Start of next patch.
-        break
-      } else if (sign === '') {
-        // Blank line?  Whatever.
-      } else {
-        // WTF?
-        throw new Error('Invalid patch mode "' + sign + '" in: ' + line)
-      }
-      textPointer++
-    }
-  }
-  return patches
-}
-
-/**
- * Class representing one patch operation.
- * @constructor
- */
-diff_match_patch.patch_obj = function () {
-  /** @type {!Array.<!diff_match_patch.Diff>} */
-  this.diffs = []
-  /** @type {?number} */
-  this.start1 = null
-  /** @type {?number} */
-  this.start2 = null
-  /** @type {number} */
-  this.length1 = 0
-  /** @type {number} */
-  this.length2 = 0
-}
-
-/**
- * Emmulate GNU diff's format.
- * Header: @@ -382,8 +481,9 @@
- * Indicies are printed as 1-based, not 0-based.
- * @return {string} The GNU diff string.
- */
-diff_match_patch.patch_obj.prototype.toString = function () {
-  var coords1, coords2
-  if (this.length1 === 0) {
-    coords1 = this.start1 + ',0'
-  } else if (this.length1 == 1) {
-    coords1 = this.start1 + 1
-  } else {
-    coords1 = this.start1 + 1 + ',' + this.length1
-  }
-  if (this.length2 === 0) {
-    coords2 = this.start2 + ',0'
-  } else if (this.length2 == 1) {
-    coords2 = this.start2 + 1
-  } else {
-    coords2 = this.start2 + 1 + ',' + this.length2
-  }
-  var text = ['@@ -' + coords1 + ' +' + coords2 + ' @@\n']
-  var op
-  // Escape the body of the patch with %xx notation.
-  for (var x = 0; x < this.diffs.length; x++) {
-    switch (this.diffs[x][0]) {
-      case DIFF_INSERT:
-        op = '+'
-        break
-      case DIFF_DELETE:
-        op = '-'
-        break
-      case DIFF_EQUAL:
-        op = ' '
-        break
-    }
-    text[x + 1] = op + encodeURI(this.diffs[x][1]) + '\n'
-  }
-  return text.join('').replace(/%20/g, ' ')
-}
-
-// Export these global variables so that they survive Google's JS compiler.
-// In a browser, 'this' will be 'window'.
-// Users of node.js should 'require' the uncompressed version since Google's
-// JS compiler may break the following exports for non-browser environments.
-this.diff_match_patch = diff_match_patch
-this.DIFF_DELETE = DIFF_DELETE
-this.DIFF_INSERT = DIFF_INSERT
-this.DIFF_EQUAL = DIFF_EQUAL

+ 0 - 9
services/track-changes/buildscript.txt

@@ -1,9 +0,0 @@
-track-changes
---dependencies=mongo,redis,s3
---docker-repos=gcr.io/overleaf-ops
---env-add=AWS_BUCKET=bucket
---env-pass-through=
---esmock-loader=False
---node-version=18.18.0
---public-repo=True
---script-version=4.4.0

+ 0 - 90
services/track-changes/config/settings.defaults.js

@@ -1,90 +0,0 @@
-const Path = require('path')
-const TMP_DIR =
-  process.env.TMP_PATH || Path.resolve(Path.join(__dirname, '../../', 'tmp'))
-
-module.exports = {
-  mongo: {
-    options: {
-      useUnifiedTopology:
-        (process.env.MONGO_USE_UNIFIED_TOPOLOGY || 'true') === 'true',
-    },
-    url:
-      process.env.MONGO_CONNECTION_STRING ||
-      `mongodb://${process.env.MONGO_HOST || 'localhost'}/sharelatex`,
-  },
-
-  internal: {
-    trackchanges: {
-      port: 3015,
-      host: process.env.LISTEN_ADDRESS || 'localhost',
-    },
-  },
-  apis: {
-    documentupdater: {
-      url: `http://${
-        process.env.DOCUMENT_UPDATER_HOST ||
-        process.env.DOCUPDATER_HOST ||
-        'localhost'
-      }:3003`,
-    },
-    docstore: {
-      url: `http://${process.env.DOCSTORE_HOST || 'localhost'}:3016`,
-    },
-    web: {
-      url: `http://${
-        process.env.WEB_API_HOST || process.env.WEB_HOST || 'localhost'
-      }:${process.env.WEB_API_PORT || process.env.WEB_PORT || 3000}`,
-      user: process.env.WEB_API_USER || 'sharelatex',
-      pass: process.env.WEB_API_PASSWORD || 'password',
-    },
-  },
-  redis: {
-    lock: {
-      host: process.env.REDIS_HOST || 'localhost',
-      port: process.env.REDIS_PORT || 6379,
-      password: process.env.REDIS_PASSWORD || '',
-      key_schema: {
-        historyLock({ doc_id: docId }) {
-          return `HistoryLock:{${docId}}`
-        },
-        historyIndexLock({ project_id: projectId }) {
-          return `HistoryIndexLock:{${projectId}}`
-        },
-      },
-    },
-    history: {
-      host: process.env.REDIS_HOST || 'localhost',
-      port: process.env.REDIS_PORT || 6379,
-      password: process.env.REDIS_PASSWORD || '',
-      key_schema: {
-        uncompressedHistoryOps({ doc_id: docId }) {
-          return `UncompressedHistoryOps:{${docId}}`
-        },
-        docsWithHistoryOps({ project_id: projectId }) {
-          return `DocsWithHistoryOps:{${projectId}}`
-        },
-      },
-    },
-  },
-
-  trackchanges: {
-    s3: {
-      key: process.env.AWS_ACCESS_KEY_ID,
-      secret: process.env.AWS_SECRET_ACCESS_KEY,
-      endpoint: process.env.AWS_S3_ENDPOINT,
-      pathStyle: process.env.AWS_S3_PATH_STYLE === 'true',
-    },
-    stores: {
-      doc_history: process.env.AWS_BUCKET,
-    },
-    continueOnError: process.env.TRACK_CHANGES_CONTINUE_ON_ERROR || false,
-  },
-
-  path: {
-    dumpFolder: Path.join(TMP_DIR, 'dumpFolder'),
-  },
-
-  sentry: {
-    dsn: process.env.SENTRY_DSN,
-  },
-}

+ 0 - 74
services/track-changes/docker-compose.ci.yml

@@ -1,74 +0,0 @@
-# This file was auto-generated, do not edit it directly.
-# Instead run bin/update_build_scripts from
-# https://github.com/overleaf/internal/
-
-version: "2.3"
-
-services:
-  test_unit:
-    image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER
-    user: node
-    command: npm run test:unit:_run
-    environment:
-      NODE_ENV: test
-      NODE_OPTIONS: "--unhandled-rejections=strict"
-
-
-  test_acceptance:
-    build: .
-    image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER
-    environment:
-      ELASTIC_SEARCH_DSN: es:9200
-      REDIS_HOST: redis
-      QUEUES_REDIS_HOST: redis
-      ANALYTICS_QUEUES_REDIS_HOST: redis
-      MONGO_HOST: mongo
-      POSTGRES_HOST: postgres
-      AWS_S3_ENDPOINT: http://s3:9090
-      AWS_S3_PATH_STYLE: 'true'
-      AWS_ACCESS_KEY_ID: fake
-      AWS_SECRET_ACCESS_KEY: fake
-      MOCHA_GREP: ${MOCHA_GREP}
-      NODE_ENV: test
-      NODE_OPTIONS: "--unhandled-rejections=strict"
-      AWS_BUCKET: bucket
-    depends_on:
-      mongo:
-        condition: service_healthy
-      redis:
-        condition: service_healthy
-      s3:
-        condition: service_healthy
-    user: node
-    command: npm run test:acceptance:_run
-
-
-  tar:
-    build: .
-    image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER
-    volumes:
-      - ./:/tmp/build/
-    command: tar -czf /tmp/build/build.tar.gz --exclude=build.tar.gz --exclude-vcs .
-    user: root
-  redis:
-    image: redis
-    healthcheck:
-      test: ping="$$(redis-cli ping)" && [ "$$ping" = 'PONG' ]
-      interval: 1s
-      retries: 20
-
-  mongo:
-    image: mongo:5.0.17
-    command: --replSet overleaf
-    healthcheck:
-      test: "mongo --quiet localhost/test --eval 'quit(db.runCommand({ ping: 1 }).ok ? 0 : 1)'"
-      interval: 1s
-      retries: 20
-  s3:
-    image: adobe/s3mock:2.4.14
-    environment:
-      - initialBuckets=fake_user_files,fake_template_files,fake_public_files,bucket
-    healthcheck:
-      test: wget --quiet --output-document=/dev/null http://localhost:9090
-      interval: 1s
-      retries: 20

+ 0 - 77
services/track-changes/docker-compose.yml

@@ -1,77 +0,0 @@
-# This file was auto-generated, do not edit it directly.
-# Instead run bin/update_build_scripts from
-# https://github.com/overleaf/internal/
-
-version: "2.3"
-
-services:
-  test_unit:
-    image: node:18.18.0
-    volumes:
-      - .:/overleaf/services/track-changes
-      - ../../node_modules:/overleaf/node_modules
-      - ../../libraries:/overleaf/libraries
-    working_dir: /overleaf/services/track-changes
-    environment:
-      MOCHA_GREP: ${MOCHA_GREP}
-      NODE_ENV: test
-      NODE_OPTIONS: "--unhandled-rejections=strict"
-    command: npm run --silent test:unit
-    user: node
-
-  test_acceptance:
-    image: node:18.18.0
-    volumes:
-      - .:/overleaf/services/track-changes
-      - ../../node_modules:/overleaf/node_modules
-      - ../../libraries:/overleaf/libraries
-    working_dir: /overleaf/services/track-changes
-    environment:
-      ELASTIC_SEARCH_DSN: es:9200
-      REDIS_HOST: redis
-      QUEUES_REDIS_HOST: redis
-      ANALYTICS_QUEUES_REDIS_HOST: redis
-      MONGO_HOST: mongo
-      POSTGRES_HOST: postgres
-      AWS_S3_ENDPOINT: http://s3:9090
-      AWS_S3_PATH_STYLE: 'true'
-      AWS_ACCESS_KEY_ID: fake
-      AWS_SECRET_ACCESS_KEY: fake
-      MOCHA_GREP: ${MOCHA_GREP}
-      LOG_LEVEL: ERROR
-      NODE_ENV: test
-      NODE_OPTIONS: "--unhandled-rejections=strict"
-      AWS_BUCKET: bucket
-    user: node
-    depends_on:
-      mongo:
-        condition: service_healthy
-      redis:
-        condition: service_healthy
-      s3:
-        condition: service_healthy
-    command: npm run --silent test:acceptance
-
-  redis:
-    image: redis
-    healthcheck:
-      test: ping=$$(redis-cli ping) && [ "$$ping" = 'PONG' ]
-      interval: 1s
-      retries: 20
-
-  mongo:
-    image: mongo:5.0.17
-    command: --replSet overleaf
-    healthcheck:
-      test: "mongo --quiet localhost/test --eval 'quit(db.runCommand({ ping: 1 }).ok ? 0 : 1)'"
-      interval: 1s
-      retries: 20
-
-  s3:
-    image: adobe/s3mock:2.4.14
-    environment:
-      - initialBuckets=fake_user_files,fake_template_files,fake_public_files,bucket
-    healthcheck:
-      test: wget --quiet --output-document=/dev/null http://localhost:9090
-      interval: 1s
-      retries: 20

+ 0 - 24
services/track-changes/pack.sh

@@ -1,24 +0,0 @@
-#!/bin/bash -x
-
-# find all the docHistories with unpacked ops and pack them
-
-# need to keep track of docs already done
-
-HOST=${1:-"localhost:3015"}
-T=${2:-10}
-
-echo packing all docHistory on $HOST with delay of $T
-for n in $(seq 5 -1 1) ; do
-    echo starting in $n seconds
-    sleep 1
-done
-
-while docs=$(curl "$HOST/doc/list?limit=1000&doc_id=$last_doc"); do
-    if [ -z "$docs" ] ; then break ; fi
-    for d in $docs ; do 
-	echo "packing $d"
-	curl -X POST "$HOST/doc/$d/pack"
-	sleep $T
-	last_doc=$d
-    done
-done

+ 0 - 54
services/track-changes/package.json

@@ -1,54 +0,0 @@
-{
-  "name": "@overleaf/track-changes",
-  "description": "An API for saving and compressing individual document updates into a browsable history",
-  "private": true,
-  "main": "app.js",
-  "scripts": {
-    "start": "node $NODE_APP_OPTIONS app.js",
-    "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 15000 --exit $@ test/acceptance/js",
-    "test:acceptance": "npm run test:acceptance:_run -- --grep=$MOCHA_GREP",
-    "test:unit:_run": "mocha --recursive --reporter spec $@ test/unit/js",
-    "test:unit": "npm run test:unit:_run -- --grep=$MOCHA_GREP",
-    "nodemon": "node --watch $NODE_APP_OPTIONS app.js",
-    "lint": "eslint --max-warnings 0 --format unix .",
-    "format": "prettier --list-different $PWD/'**/*.js'",
-    "format:fix": "prettier --write $PWD/'**/*.js'",
-    "lint:fix": "eslint --fix ."
-  },
-  "dependencies": {
-    "@overleaf/logger": "*",
-    "@overleaf/metrics": "*",
-    "@overleaf/o-error": "*",
-    "@overleaf/redis-wrapper": "*",
-    "@overleaf/settings": "*",
-    "async": "^3.2.2",
-    "aws-sdk": "^2.643.0",
-    "body-parser": "^1.19.0",
-    "bson": "^1.1.5",
-    "bunyan": "^1.8.15",
-    "byline": "^5.0.0",
-    "express": "^4.18.2",
-    "heap": "^0.2.6",
-    "JSONStream": "^1.3.5",
-    "line-reader": "^0.4.0",
-    "lodash": "^4.17.21",
-    "mongo-uri": "^0.1.2",
-    "mongodb": "^3.6.0",
-    "redis": "~0.10.1",
-    "request": "~2.88.2",
-    "requestretry": "^7.1.0",
-    "s3-streams": "^0.4.0",
-    "underscore": "~1.13.1",
-    "yazl": "^2.5.1"
-  },
-  "devDependencies": {
-    "chai": "^4.3.6",
-    "chai-as-promised": "^7.1.1",
-    "cli": "^1.0.1",
-    "memorystream": "0.3.1",
-    "mocha": "^10.2.0",
-    "sandboxed-module": "~2.0.3",
-    "sinon": "~9.0.1",
-    "timekeeper": "2.2.0"
-  }
-}

+ 0 - 27
services/track-changes/scripts/flush_all.js

@@ -1,27 +0,0 @@
-const UpdatesManager = require('../app/js/UpdatesManager')
-const { waitForDb } = require('../app/js/mongodb')
-
-async function main() {
-  await waitForDb()
-  return new Promise((resolve, reject) => {
-    const limit = -1
-    console.log('Flushing all updates')
-    UpdatesManager.flushAll(limit, err => {
-      if (err) {
-        reject(err)
-      } else {
-        resolve()
-      }
-    })
-  })
-}
-
-main()
-  .then(() => {
-    console.log('Done flushing all updates')
-    process.exit(0)
-  })
-  .catch(error => {
-    console.error('There was an error flushing updates', { error })
-    process.exit(1)
-  })

+ 0 - 4
services/track-changes/test/acceptance/deps/Dockerfile.s3mock

@@ -1,4 +0,0 @@
-FROM adobe/s3mock:2.4.14
-RUN apk add --update --no-cache curl
-COPY healthcheck.sh /healthcheck.sh
-HEALTHCHECK --interval=1s --timeout=1s --retries=30 CMD /healthcheck.sh http://localhost:9090

+ 0 - 9
services/track-changes/test/acceptance/deps/healthcheck.sh

@@ -1,9 +0,0 @@
-#!/bin/sh
-
-# health check to allow 404 status code as valid
-STATUSCODE=$(curl --silent --output /dev/null --write-out "%{http_code}" $1)
-# will be 000 on non-http error (e.g. connection failure)
-if test $STATUSCODE -ge 500 || test $STATUSCODE -lt 200; then
-  exit 1
-fi
-exit 0

+ 0 - 587
services/track-changes/test/acceptance/js/AppendingUpdatesTests.js

@@ -1,587 +0,0 @@
-/* eslint-disable
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-const sinon = require('sinon')
-const { expect } = require('chai')
-const { ObjectId } = require('../../../app/js/mongodb')
-const Settings = require('@overleaf/settings')
-const request = require('request')
-const rclient = require('redis').createClient(Settings.redis.history) // Only works locally for now
-
-const TrackChangesApp = require('./helpers/TrackChangesApp')
-const TrackChangesClient = require('./helpers/TrackChangesClient')
-const MockWebApi = require('./helpers/MockWebApi')
-
-describe('Appending doc ops to the history', function () {
-  before(function (done) {
-    return TrackChangesApp.ensureRunning(done)
-  })
-
-  describe('when the history does not exist yet', function () {
-    before(function (done) {
-      this.project_id = ObjectId().toString()
-      this.doc_id = ObjectId().toString()
-      this.user_id = ObjectId().toString()
-      MockWebApi.projects[this.project_id] = { features: { versioning: false } }
-      TrackChangesClient.pushRawUpdates(
-        this.project_id,
-        this.doc_id,
-        [
-          {
-            op: [{ i: 'f', p: 3 }],
-            meta: { ts: Date.now(), user_id: this.user_id },
-            v: 3,
-          },
-          {
-            op: [{ i: 'o', p: 4 }],
-            meta: { ts: Date.now(), user_id: this.user_id },
-            v: 4,
-          },
-          {
-            op: [{ i: 'o', p: 5 }],
-            meta: { ts: Date.now(), user_id: this.user_id },
-            v: 5,
-          },
-        ],
-        error => {
-          if (error != null) {
-            throw error
-          }
-          return TrackChangesClient.flushAndGetCompressedUpdates(
-            this.project_id,
-            this.doc_id,
-            (error, updates) => {
-              this.updates = updates
-              if (error != null) {
-                throw error
-              }
-              return done()
-            }
-          )
-        }
-      )
-      return null
-    })
-
-    it('should insert the compressed op into mongo', function () {
-      return expect(this.updates[0].pack[0].op).to.deep.equal([
-        {
-          p: 3,
-          i: 'foo',
-        },
-      ])
-    })
-
-    it('should insert the correct version number into mongo', function () {
-      return expect(this.updates[0].v).to.equal(5)
-    })
-
-    it('should store the doc id', function () {
-      return expect(this.updates[0].doc_id.toString()).to.equal(this.doc_id)
-    })
-
-    it('should store the project id', function () {
-      return expect(this.updates[0].project_id.toString()).to.equal(
-        this.project_id
-      )
-    })
-
-    return it('should clear the doc from the DocsWithHistoryOps set', function (done) {
-      rclient.sismember(
-        `DocsWithHistoryOps:${this.project_id}`,
-        this.doc_id,
-        (error, member) => {
-          if (error) return done(error)
-          member.should.equal(0)
-          return done()
-        }
-      )
-      return null
-    })
-  })
-
-  describe('when the history has already been started', function () {
-    beforeEach(function (done) {
-      this.project_id = ObjectId().toString()
-      this.doc_id = ObjectId().toString()
-      this.user_id = ObjectId().toString()
-      MockWebApi.projects[this.project_id] = { features: { versioning: false } }
-      TrackChangesClient.pushRawUpdates(
-        this.project_id,
-        this.doc_id,
-        [
-          {
-            op: [{ i: 'f', p: 3 }],
-            meta: { ts: Date.now(), user_id: this.user_id },
-            v: 3,
-          },
-          {
-            op: [{ i: 'o', p: 4 }],
-            meta: { ts: Date.now(), user_id: this.user_id },
-            v: 4,
-          },
-          {
-            op: [{ i: 'o', p: 5 }],
-            meta: { ts: Date.now(), user_id: this.user_id },
-            v: 5,
-          },
-        ],
-        error => {
-          if (error != null) {
-            throw error
-          }
-          return TrackChangesClient.flushAndGetCompressedUpdates(
-            this.project_id,
-            this.doc_id,
-            (error, updates) => {
-              if (error != null) {
-                throw error
-              }
-              return done()
-            }
-          )
-        }
-      )
-      return null
-    })
-
-    describe('when the updates are recent and from the same user', function () {
-      beforeEach(function (done) {
-        TrackChangesClient.pushRawUpdates(
-          this.project_id,
-          this.doc_id,
-          [
-            {
-              op: [{ i: 'b', p: 6 }],
-              meta: { ts: Date.now(), user_id: this.user_id },
-              v: 6,
-            },
-            {
-              op: [{ i: 'a', p: 7 }],
-              meta: { ts: Date.now(), user_id: this.user_id },
-              v: 7,
-            },
-            {
-              op: [{ i: 'r', p: 8 }],
-              meta: { ts: Date.now(), user_id: this.user_id },
-              v: 8,
-            },
-          ],
-          error => {
-            if (error != null) {
-              throw error
-            }
-            return TrackChangesClient.flushAndGetCompressedUpdates(
-              this.project_id,
-              this.doc_id,
-              (error, updates) => {
-                this.updates = updates
-                if (error != null) {
-                  throw error
-                }
-                return done()
-              }
-            )
-          }
-        )
-        return null
-      })
-
-      it('should combine all the updates into one pack', function () {
-        return expect(this.updates[0].pack[1].op).to.deep.equal([
-          {
-            p: 6,
-            i: 'bar',
-          },
-        ])
-      })
-
-      return it('should insert the correct version number into mongo', function () {
-        return expect(this.updates[0].v_end).to.equal(8)
-      })
-    })
-
-    return describe('when the updates are far apart', function () {
-      beforeEach(function (done) {
-        const oneDay = 24 * 60 * 60 * 1000
-        TrackChangesClient.pushRawUpdates(
-          this.project_id,
-          this.doc_id,
-          [
-            {
-              op: [{ i: 'b', p: 6 }],
-              meta: { ts: Date.now() + oneDay, user_id: this.user_id },
-              v: 6,
-            },
-            {
-              op: [{ i: 'a', p: 7 }],
-              meta: { ts: Date.now() + oneDay, user_id: this.user_id },
-              v: 7,
-            },
-            {
-              op: [{ i: 'r', p: 8 }],
-              meta: { ts: Date.now() + oneDay, user_id: this.user_id },
-              v: 8,
-            },
-          ],
-          error => {
-            if (error != null) {
-              throw error
-            }
-            return TrackChangesClient.flushAndGetCompressedUpdates(
-              this.project_id,
-              this.doc_id,
-              (error, updates) => {
-                this.updates = updates
-                if (error != null) {
-                  throw error
-                }
-                return done()
-              }
-            )
-          }
-        )
-        return null
-      })
-
-      return it('should combine the updates into one pack', function () {
-        expect(this.updates[0].pack[0].op).to.deep.equal([
-          {
-            p: 3,
-            i: 'foo',
-          },
-        ])
-        return expect(this.updates[0].pack[1].op).to.deep.equal([
-          {
-            p: 6,
-            i: 'bar',
-          },
-        ])
-      })
-    })
-  })
-
-  describe('when the updates need processing in batches', function () {
-    before(function (done) {
-      this.project_id = ObjectId().toString()
-      this.doc_id = ObjectId().toString()
-      this.user_id = ObjectId().toString()
-      MockWebApi.projects[this.project_id] = { features: { versioning: false } }
-      const updates = []
-      this.expectedOp = [{ p: 0, i: '' }]
-      for (let i = 0; i <= 250; i++) {
-        updates.push({
-          op: [{ i: 'a', p: 0 }],
-          meta: { ts: Date.now(), user_id: this.user_id },
-          v: i,
-        })
-        this.expectedOp[0].i = `a${this.expectedOp[0].i}`
-      }
-
-      TrackChangesClient.pushRawUpdates(
-        this.project_id,
-        this.doc_id,
-        updates,
-        error => {
-          if (error != null) {
-            throw error
-          }
-          return TrackChangesClient.flushAndGetCompressedUpdates(
-            this.project_id,
-            this.doc_id,
-            (error, updates1) => {
-              this.updates = updates1
-              if (error != null) {
-                throw error
-              }
-              return done()
-            }
-          )
-        }
-      )
-      return null
-    })
-
-    it('should concat the compressed op into mongo', function () {
-      return expect(this.updates[0].pack.length).to.deep.equal(3)
-    }) // batch size is 100
-
-    return it('should insert the correct version number into mongo', function () {
-      return expect(this.updates[0].v_end).to.equal(250)
-    })
-  })
-
-  describe('when there are multiple ops in each update', function () {
-    before(function (done) {
-      this.project_id = ObjectId().toString()
-      this.doc_id = ObjectId().toString()
-      this.user_id = ObjectId().toString()
-      MockWebApi.projects[this.project_id] = { features: { versioning: false } }
-      const oneDay = 24 * 60 * 60 * 1000
-      TrackChangesClient.pushRawUpdates(
-        this.project_id,
-        this.doc_id,
-        [
-          {
-            op: [
-              { i: 'f', p: 3 },
-              { i: 'o', p: 4 },
-              { i: 'o', p: 5 },
-            ],
-            meta: { ts: Date.now(), user_id: this.user_id },
-            v: 3,
-          },
-          {
-            op: [
-              { i: 'b', p: 6 },
-              { i: 'a', p: 7 },
-              { i: 'r', p: 8 },
-            ],
-            meta: { ts: Date.now() + oneDay, user_id: this.user_id },
-            v: 4,
-          },
-        ],
-        error => {
-          if (error != null) {
-            throw error
-          }
-          return TrackChangesClient.flushAndGetCompressedUpdates(
-            this.project_id,
-            this.doc_id,
-            (error, updates) => {
-              this.updates = updates
-              if (error != null) {
-                throw error
-              }
-              return done()
-            }
-          )
-        }
-      )
-      return null
-    })
-
-    it('should insert the compressed ops into mongo', function () {
-      expect(this.updates[0].pack[0].op).to.deep.equal([
-        {
-          p: 3,
-          i: 'foo',
-        },
-      ])
-      return expect(this.updates[0].pack[1].op).to.deep.equal([
-        {
-          p: 6,
-          i: 'bar',
-        },
-      ])
-    })
-
-    return it('should insert the correct version numbers into mongo', function () {
-      expect(this.updates[0].pack[0].v).to.equal(3)
-      return expect(this.updates[0].pack[1].v).to.equal(4)
-    })
-  })
-
-  describe('when there is a no-op update', function () {
-    before(function (done) {
-      this.project_id = ObjectId().toString()
-      this.doc_id = ObjectId().toString()
-      this.user_id = ObjectId().toString()
-      MockWebApi.projects[this.project_id] = { features: { versioning: false } }
-      const oneDay = 24 * 60 * 60 * 1000
-      TrackChangesClient.pushRawUpdates(
-        this.project_id,
-        this.doc_id,
-        [
-          {
-            op: [],
-            meta: { ts: Date.now(), user_id: this.user_id },
-            v: 3,
-          },
-          {
-            op: [{ i: 'foo', p: 3 }],
-            meta: { ts: Date.now() + oneDay, user_id: this.user_id },
-            v: 4,
-          },
-        ],
-        error => {
-          if (error != null) {
-            throw error
-          }
-          return TrackChangesClient.flushAndGetCompressedUpdates(
-            this.project_id,
-            this.doc_id,
-            (error, updates) => {
-              this.updates = updates
-              if (error != null) {
-                throw error
-              }
-              return done()
-            }
-          )
-        }
-      )
-      return null
-    })
-
-    it('should insert the compressed no-op into mongo', function () {
-      return expect(this.updates[0].pack[0].op).to.deep.equal([])
-    })
-
-    it('should insert the compressed next update into mongo', function () {
-      return expect(this.updates[0].pack[1].op).to.deep.equal([
-        {
-          p: 3,
-          i: 'foo',
-        },
-      ])
-    })
-
-    return it('should insert the correct version numbers into mongo', function () {
-      expect(this.updates[0].pack[0].v).to.equal(3)
-      return expect(this.updates[0].pack[1].v).to.equal(4)
-    })
-  })
-
-  describe('when there is a comment update', function () {
-    before(function (done) {
-      this.project_id = ObjectId().toString()
-      this.doc_id = ObjectId().toString()
-      this.user_id = ObjectId().toString()
-      MockWebApi.projects[this.project_id] = { features: { versioning: false } }
-      TrackChangesClient.pushRawUpdates(
-        this.project_id,
-        this.doc_id,
-        [
-          {
-            op: [
-              { c: 'foo', p: 3 },
-              { d: 'bar', p: 6 },
-            ],
-            meta: { ts: Date.now(), user_id: this.user_id },
-            v: 3,
-          },
-        ],
-        error => {
-          if (error != null) {
-            throw error
-          }
-          return TrackChangesClient.flushAndGetCompressedUpdates(
-            this.project_id,
-            this.doc_id,
-            (error, updates) => {
-              this.updates = updates
-              if (error != null) {
-                throw error
-              }
-              return done()
-            }
-          )
-        }
-      )
-      return null
-    })
-
-    it('should ignore the comment op', function () {
-      return expect(this.updates[0].pack[0].op).to.deep.equal([
-        { d: 'bar', p: 6 },
-      ])
-    })
-
-    return it('should insert the correct version numbers into mongo', function () {
-      return expect(this.updates[0].pack[0].v).to.equal(3)
-    })
-  })
-
-  describe('when the project has versioning enabled', function () {
-    before(function (done) {
-      this.project_id = ObjectId().toString()
-      this.doc_id = ObjectId().toString()
-      this.user_id = ObjectId().toString()
-      MockWebApi.projects[this.project_id] = { features: { versioning: true } }
-
-      TrackChangesClient.pushRawUpdates(
-        this.project_id,
-        this.doc_id,
-        [
-          {
-            op: [{ i: 'f', p: 3 }],
-            meta: { ts: Date.now(), user_id: this.user_id },
-            v: 3,
-          },
-        ],
-        error => {
-          if (error != null) {
-            throw error
-          }
-          return TrackChangesClient.flushAndGetCompressedUpdates(
-            this.project_id,
-            this.doc_id,
-            (error, updates) => {
-              this.updates = updates
-              if (error != null) {
-                throw error
-              }
-              return done()
-            }
-          )
-        }
-      )
-      return null
-    })
-
-    return it('should not add a expiresAt entry in the update in mongo', function () {
-      return expect(this.updates[0].expiresAt).to.be.undefined
-    })
-  })
-
-  return describe('when the project does not have versioning enabled', function () {
-    before(function (done) {
-      this.project_id = ObjectId().toString()
-      this.doc_id = ObjectId().toString()
-      this.user_id = ObjectId().toString()
-      MockWebApi.projects[this.project_id] = { features: { versioning: false } }
-
-      TrackChangesClient.pushRawUpdates(
-        this.project_id,
-        this.doc_id,
-        [
-          {
-            op: [{ i: 'f', p: 3 }],
-            meta: { ts: Date.now(), user_id: this.user_id },
-            v: 3,
-          },
-        ],
-        error => {
-          if (error != null) {
-            throw error
-          }
-          return TrackChangesClient.flushAndGetCompressedUpdates(
-            this.project_id,
-            this.doc_id,
-            (error, updates) => {
-              this.updates = updates
-              if (error != null) {
-                throw error
-              }
-              return done()
-            }
-          )
-        }
-      )
-      return null
-    })
-
-    return it('should add a expiresAt entry in the update in mongo', function () {
-      return expect(this.updates[0].expiresAt).to.exist
-    })
-  })
-})

+ 0 - 319
services/track-changes/test/acceptance/js/ArchivingUpdatesTests.js

@@ -1,319 +0,0 @@
-/* eslint-disable
-    no-undef,
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * DS103: Rewrite code to no longer use __guard__
- * DS202: Simplify dynamic range loops
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-const sinon = require('sinon')
-const { expect } = require('chai')
-const { db, ObjectId } = require('../../../app/js/mongodb')
-const Settings = require('@overleaf/settings')
-const request = require('request')
-const rclient = require('redis').createClient(Settings.redis.history) // Only works locally for now
-
-const TrackChangesApp = require('./helpers/TrackChangesApp')
-const TrackChangesClient = require('./helpers/TrackChangesClient')
-const MockDocStoreApi = require('./helpers/MockDocStoreApi')
-const MockWebApi = require('./helpers/MockWebApi')
-
-describe('Archiving updates', function () {
-  before(function (done) {
-    if (
-      __guard__(
-        __guard__(
-          Settings != null ? Settings.trackchanges : undefined,
-          x1 => x1.s3
-        ),
-        x => x.key.length
-      ) < 1
-    ) {
-      const message = new Error('s3 keys not setup, this test setup will fail')
-      return done(message)
-    }
-
-    return TrackChangesClient.waitForS3(done)
-  })
-
-  before(function (done) {
-    this.now = Date.now()
-    this.to = this.now
-    this.user_id = ObjectId().toString()
-    this.user_id_2 = ObjectId().toString()
-    this.doc_id = ObjectId().toString()
-    this.project_id = ObjectId().toString()
-
-    this.minutes = 60 * 1000
-    this.hours = 60 * this.minutes
-
-    MockWebApi.projects[this.project_id] = {
-      features: {
-        versioning: true,
-      },
-    }
-    sinon.spy(MockWebApi, 'getProjectDetails')
-
-    MockWebApi.users[this.user_id] = this.user = {
-      email: 'user@sharelatex.com',
-      first_name: 'Leo',
-      last_name: 'Lion',
-      id: this.user_id,
-    }
-    sinon.spy(MockWebApi, 'getUserInfo')
-
-    MockDocStoreApi.docs[this.doc_id] = this.doc = {
-      _id: this.doc_id,
-      project_id: this.project_id,
-    }
-    sinon.spy(MockDocStoreApi, 'getAllDoc')
-
-    this.updates = []
-    for (
-      let i = 0, end = 512 + 10, asc = end >= 0;
-      asc ? i <= end : i >= end;
-      asc ? i++ : i--
-    ) {
-      this.updates.push({
-        op: [{ i: 'a', p: 0 }],
-        meta: { ts: this.now + (i - 2048) * this.hours, user_id: this.user_id },
-        v: 2 * i + 1,
-      })
-      this.updates.push({
-        op: [{ i: 'b', p: 0 }],
-        meta: {
-          ts: this.now + (i - 2048) * this.hours + 10 * this.minutes,
-          user_id: this.user_id_2,
-        },
-        v: 2 * i + 2,
-      })
-    }
-    TrackChangesApp.ensureRunning(() => {
-      return TrackChangesClient.pushRawUpdates(
-        this.project_id,
-        this.doc_id,
-        this.updates,
-        error => {
-          if (error != null) {
-            throw error
-          }
-          return TrackChangesClient.flushDoc(
-            this.project_id,
-            this.doc_id,
-            error => {
-              if (error != null) {
-                throw error
-              }
-              return done()
-            }
-          )
-        }
-      )
-    })
-    return null
-  })
-
-  after(function (done) {
-    MockWebApi.getUserInfo.restore()
-    return db.docHistory.deleteMany(
-      { project_id: ObjectId(this.project_id) },
-      () => {
-        return db.docHistoryIndex.remove(
-          { project_id: ObjectId(this.project_id) },
-          () => {
-            return TrackChangesClient.removeS3Doc(
-              this.project_id,
-              this.doc_id,
-              done
-            )
-          }
-        )
-      }
-    )
-  })
-
-  function testExportFeature() {
-    describe('exporting the project', function () {
-      before('fetch export', function (done) {
-        TrackChangesClient.exportProject(
-          this.project_id,
-          (error, updates, userIds) => {
-            if (error) {
-              return done(error)
-            }
-            this.exportedUpdates = updates
-            this.exportedUserIds = userIds
-            done()
-          }
-        )
-      })
-
-      it('should include all the imported updates, with ids, sorted by timestamp', function () {
-        // Add a safe guard for an empty array matching an empty export.
-        expect(this.updates).to.have.length(1024 + 22)
-
-        const expectedExportedUpdates = this.updates
-          .slice()
-          .reverse()
-          .map(update => {
-            // clone object, updates are created once in before handler
-            const exportedUpdate = Object.assign({}, update)
-            exportedUpdate.meta = Object.assign({}, update.meta)
-
-            exportedUpdate.doc_id = this.doc_id
-            exportedUpdate.project_id = this.project_id
-
-            // This is for merged updates, which does not apply here.
-            exportedUpdate.meta.start_ts = exportedUpdate.meta.end_ts =
-              exportedUpdate.meta.ts
-            delete exportedUpdate.meta.ts
-            return exportedUpdate
-          })
-        expect(this.exportedUpdates).to.deep.equal(expectedExportedUpdates)
-        expect(this.exportedUserIds).to.deep.equal([
-          this.user_id,
-          this.user_id_2,
-        ])
-      })
-    })
-  }
-
-  describe("before archiving a doc's updates", function () {
-    testExportFeature()
-  })
-
-  describe("archiving a doc's updates", function () {
-    before(function (done) {
-      TrackChangesClient.pushDocHistory(this.project_id, this.doc_id, error => {
-        if (error != null) {
-          throw error
-        }
-        return done()
-      })
-      return null
-    })
-
-    it('should have one cached pack', function (done) {
-      return db.docHistory.count(
-        { doc_id: ObjectId(this.doc_id), expiresAt: { $exists: true } },
-        (error, count) => {
-          if (error != null) {
-            throw error
-          }
-          count.should.equal(1)
-          return done()
-        }
-      )
-    })
-
-    it('should have one remaining pack after cache is expired', function (done) {
-      return db.docHistory.deleteMany(
-        {
-          doc_id: ObjectId(this.doc_id),
-          expiresAt: { $exists: true },
-        },
-        (err, result) => {
-          if (err) return done(err)
-          return db.docHistory.count(
-            { doc_id: ObjectId(this.doc_id) },
-            (error, count) => {
-              if (error != null) {
-                throw error
-              }
-              count.should.equal(1)
-              return done()
-            }
-          )
-        }
-      )
-    })
-
-    it('should have a docHistoryIndex entry marked as inS3', function (done) {
-      return db.docHistoryIndex.findOne(
-        { _id: ObjectId(this.doc_id) },
-        (error, index) => {
-          if (error != null) {
-            throw error
-          }
-          index.packs[0].inS3.should.equal(true)
-          return done()
-        }
-      )
-    })
-
-    it('should have a docHistoryIndex entry with the last version', function (done) {
-      return db.docHistoryIndex.findOne(
-        { _id: ObjectId(this.doc_id) },
-        (error, index) => {
-          if (error != null) {
-            throw error
-          }
-          index.packs[0].v_end.should.equal(1024)
-          return done()
-        }
-      )
-    })
-
-    it('should store 1024 doc changes in S3 in one pack', function (done) {
-      return db.docHistoryIndex.findOne(
-        { _id: ObjectId(this.doc_id) },
-        (error, index) => {
-          if (error != null) {
-            throw error
-          }
-          const packId = index.packs[0]._id
-          return TrackChangesClient.getS3Doc(
-            this.project_id,
-            this.doc_id,
-            packId,
-            (error, doc) => {
-              if (error) return done(error)
-              doc.n.should.equal(1024)
-              doc.pack.length.should.equal(1024)
-              return done()
-            }
-          )
-        }
-      )
-    })
-
-    testExportFeature()
-  })
-
-  return describe("unarchiving a doc's updates", function () {
-    before(function (done) {
-      TrackChangesClient.pullDocHistory(this.project_id, this.doc_id, error => {
-        if (error != null) {
-          throw error
-        }
-        return done()
-      })
-      return null
-    })
-
-    return it('should restore both packs', function (done) {
-      return db.docHistory.count(
-        { doc_id: ObjectId(this.doc_id) },
-        (error, count) => {
-          if (error != null) {
-            throw error
-          }
-          count.should.equal(2)
-          return done()
-        }
-      )
-    })
-  })
-})
-
-function __guard__(value, transform) {
-  return typeof value !== 'undefined' && value !== null
-    ? transform(value)
-    : undefined
-}

+ 0 - 34
services/track-changes/test/acceptance/js/ExportProjectTests.js

@@ -1,34 +0,0 @@
-const { expect } = require('chai')
-const { ObjectId } = require('../../../app/js/mongodb')
-
-const TrackChangesApp = require('./helpers/TrackChangesApp')
-const TrackChangesClient = require('./helpers/TrackChangesClient')
-
-describe('ExportProject', function () {
-  before('start app', function (done) {
-    TrackChangesApp.ensureRunning(done)
-  })
-
-  describe('when there are no updates', function () {
-    before('fetch export', function (done) {
-      TrackChangesClient.exportProject(
-        ObjectId(),
-        (error, updates, userIds) => {
-          if (error) {
-            return done(error)
-          }
-          this.exportedUpdates = updates
-          this.exportedUserIds = userIds
-          done()
-        }
-      )
-    })
-
-    it('should export an empty array', function () {
-      expect(this.exportedUpdates).to.deep.equal([])
-      expect(this.exportedUserIds).to.deep.equal([])
-    })
-  })
-
-  // see ArchivingUpdatesTests for tests with data in mongo/s3
-})

+ 0 - 277
services/track-changes/test/acceptance/js/FlushingUpdatesTests.js

@@ -1,277 +0,0 @@
-/* eslint-disable
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-const sinon = require('sinon')
-const { expect } = require('chai')
-const { ObjectId } = require('../../../app/js/mongodb')
-const Settings = require('@overleaf/settings')
-const request = require('request')
-const rclient = require('redis').createClient(Settings.redis.history) // Only works locally for now
-
-const TrackChangesApp = require('./helpers/TrackChangesApp')
-const TrackChangesClient = require('./helpers/TrackChangesClient')
-const MockWebApi = require('./helpers/MockWebApi')
-
-describe('Flushing updates', function () {
-  before(function (done) {
-    return TrackChangesApp.ensureRunning(done)
-  })
-
-  describe("flushing a doc's updates", function () {
-    before(function (done) {
-      this.project_id = ObjectId().toString()
-      this.doc_id = ObjectId().toString()
-      this.user_id = ObjectId().toString()
-      MockWebApi.projects[this.project_id] = { features: { versioning: true } }
-
-      TrackChangesClient.pushRawUpdates(
-        this.project_id,
-        this.doc_id,
-        [
-          {
-            op: [{ i: 'f', p: 3 }],
-            meta: { ts: Date.now(), user_id: this.user_id },
-            v: 3,
-          },
-        ],
-        error => {
-          if (error != null) {
-            throw error
-          }
-          return TrackChangesClient.flushDoc(
-            this.project_id,
-            this.doc_id,
-            error => {
-              if (error != null) {
-                throw error
-              }
-              return done()
-            }
-          )
-        }
-      )
-      return null
-    })
-
-    return it('should flush the op into mongo', function (done) {
-      TrackChangesClient.getCompressedUpdates(this.doc_id, (error, updates) => {
-        if (error) return done(error)
-        expect(updates[0].pack[0].op).to.deep.equal([
-          {
-            p: 3,
-            i: 'f',
-          },
-        ])
-        return done()
-      })
-      return null
-    })
-  })
-
-  return describe("flushing a project's updates", function () {
-    describe('with versioning enabled', function () {
-      before(function (done) {
-        this.project_id = ObjectId().toString()
-        this.doc_id = ObjectId().toString()
-        this.user_id = ObjectId().toString()
-
-        this.weeks = 7 * 24 * 60 * 60 * 1000
-
-        MockWebApi.projects[this.project_id] = {
-          features: {
-            versioning: true,
-          },
-        }
-
-        TrackChangesClient.pushRawUpdates(
-          this.project_id,
-          this.doc_id,
-          [
-            {
-              op: [{ i: 'g', p: 2 }],
-              meta: { ts: Date.now() - 2 * this.weeks, user_id: this.user_id },
-              v: 2,
-            },
-            {
-              op: [{ i: 'f', p: 3 }],
-              meta: { ts: Date.now(), user_id: this.user_id },
-              v: 3,
-            },
-          ],
-          error => {
-            if (error != null) {
-              throw error
-            }
-            return TrackChangesClient.flushProject(this.project_id, error => {
-              if (error != null) {
-                throw error
-              }
-              return done()
-            })
-          }
-        )
-        return null
-      })
-
-      it('should not mark the updates for deletion', function (done) {
-        TrackChangesClient.getCompressedUpdates(
-          this.doc_id,
-          (error, updates) => {
-            if (error) return done(error)
-            expect(updates[0].expiresAt).to.not.exist
-            return done()
-          }
-        )
-        return null
-      })
-
-      return it('should preserve history forever', function (done) {
-        TrackChangesClient.getProjectMetaData(
-          this.project_id,
-          (error, project) => {
-            if (error) return done(error)
-            expect(project.preserveHistory).to.equal(true)
-            return done()
-          }
-        )
-        return null
-      })
-    })
-
-    describe('without versioning enabled', function () {
-      before(function (done) {
-        this.project_id = ObjectId().toString()
-        this.doc_id = ObjectId().toString()
-        this.user_id = ObjectId().toString()
-
-        this.weeks = 7 * 24 * 60 * 60 * 1000
-
-        MockWebApi.projects[this.project_id] = {
-          features: {
-            versioning: false,
-          },
-        }
-
-        TrackChangesClient.pushRawUpdates(
-          this.project_id,
-          this.doc_id,
-          [
-            {
-              op: [{ i: 'g', p: 2 }],
-              meta: { ts: Date.now() - 2 * this.weeks, user_id: this.user_id },
-              v: 2,
-            },
-            {
-              op: [{ i: 'f', p: 3 }],
-              meta: { ts: Date.now(), user_id: this.user_id },
-              v: 3,
-            },
-          ],
-          error => {
-            if (error != null) {
-              throw error
-            }
-            return TrackChangesClient.flushProject(this.project_id, error => {
-              if (error != null) {
-                throw error
-              }
-              return done()
-            })
-          }
-        )
-        return null
-      })
-
-      return it('should mark the updates for deletion', function (done) {
-        TrackChangesClient.getCompressedUpdates(
-          this.doc_id,
-          (error, updates) => {
-            if (error) return done(error)
-            expect(updates[0].expiresAt).to.exist
-            return done()
-          }
-        )
-        return null
-      })
-    })
-
-    return describe('without versioning enabled but with preserveHistory set to true', function () {
-      before(function (done) {
-        this.project_id = ObjectId().toString()
-        this.doc_id = ObjectId().toString()
-        this.user_id = ObjectId().toString()
-
-        this.weeks = 7 * 24 * 60 * 60 * 1000
-
-        MockWebApi.projects[this.project_id] = {
-          features: {
-            versioning: false,
-          },
-        }
-
-        TrackChangesClient.setPreserveHistoryForProject(
-          this.project_id,
-          error => {
-            if (error != null) {
-              throw error
-            }
-            return TrackChangesClient.pushRawUpdates(
-              this.project_id,
-              this.doc_id,
-              [
-                {
-                  op: [{ i: 'g', p: 2 }],
-                  meta: {
-                    ts: Date.now() - 2 * this.weeks,
-                    user_id: this.user_id,
-                  },
-                  v: 2,
-                },
-                {
-                  op: [{ i: 'f', p: 3 }],
-                  meta: { ts: Date.now(), user_id: this.user_id },
-                  v: 3,
-                },
-              ],
-              error => {
-                if (error != null) {
-                  throw error
-                }
-                return TrackChangesClient.flushProject(
-                  this.project_id,
-                  error => {
-                    if (error != null) {
-                      throw error
-                    }
-                    return done()
-                  }
-                )
-              }
-            )
-          }
-        )
-        return null
-      })
-
-      return it('should not mark the updates for deletion', function (done) {
-        TrackChangesClient.getCompressedUpdates(
-          this.doc_id,
-          (error, updates) => {
-            if (error) return done(error)
-            expect(updates[0].expiresAt).to.not.exist
-            return done()
-          }
-        )
-        return null
-      })
-    })
-  })
-})

+ 0 - 127
services/track-changes/test/acceptance/js/GettingADiffTests.js

@@ -1,127 +0,0 @@
-/* eslint-disable
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-const sinon = require('sinon')
-const { expect } = require('chai')
-const { ObjectId } = require('../../../app/js/mongodb')
-const Settings = require('@overleaf/settings')
-
-const TrackChangesApp = require('./helpers/TrackChangesApp')
-const TrackChangesClient = require('./helpers/TrackChangesClient')
-const MockDocUpdaterApi = require('./helpers/MockDocUpdaterApi')
-const MockWebApi = require('./helpers/MockWebApi')
-
-describe('Getting a diff', function () {
-  beforeEach(function (done) {
-    sinon.spy(MockDocUpdaterApi, 'getDoc')
-
-    this.now = Date.now()
-    this.from = this.now - 100000000
-    this.to = this.now
-    this.user_id = ObjectId().toString()
-    this.doc_id = ObjectId().toString()
-    this.project_id = ObjectId().toString()
-    MockWebApi.projects[this.project_id] = { features: { versioning: true } }
-
-    MockWebApi.users[this.user_id] = this.user = {
-      email: 'user@sharelatex.com',
-      first_name: 'Leo',
-      last_name: 'Lion',
-      id: this.user_id,
-    }
-    sinon.spy(MockWebApi, 'getUserInfo')
-
-    const twoMinutes = 2 * 60 * 1000
-
-    this.updates = [
-      {
-        op: [{ i: 'one ', p: 0 }],
-        meta: { ts: this.from - twoMinutes, user_id: this.user_id },
-        v: 3,
-      },
-      {
-        op: [{ i: 'two ', p: 4 }],
-        meta: { ts: this.from + twoMinutes, user_id: this.user_id },
-        v: (this.fromVersion = 4),
-      },
-      {
-        op: [{ i: 'three ', p: 8 }],
-        meta: { ts: this.to - twoMinutes, user_id: this.user_id },
-        v: (this.toVersion = 5),
-      },
-      {
-        op: [{ i: 'four', p: 14 }],
-        meta: { ts: this.to + twoMinutes, user_id: this.user_id },
-        v: 6,
-      },
-    ]
-    this.lines = ['one two three four']
-    this.expected_diff = [
-      { u: 'one ' },
-      {
-        i: 'two three ',
-        meta: {
-          start_ts: this.from + twoMinutes,
-          end_ts: this.to - twoMinutes,
-          user: this.user,
-        },
-      },
-    ]
-
-    MockDocUpdaterApi.docs[this.doc_id] = {
-      lines: this.lines,
-      version: 7,
-    }
-    TrackChangesApp.ensureRunning(() => {
-      return TrackChangesClient.pushRawUpdates(
-        this.project_id,
-        this.doc_id,
-        this.updates,
-        error => {
-          if (error != null) {
-            throw error
-          }
-          return TrackChangesClient.getDiff(
-            this.project_id,
-            this.doc_id,
-            this.fromVersion,
-            this.toVersion,
-            (error, diff) => {
-              if (error != null) {
-                throw error
-              }
-              this.diff = diff.diff
-              return done()
-            }
-          )
-        }
-      )
-    })
-    return null
-  })
-
-  afterEach(function () {
-    MockDocUpdaterApi.getDoc.restore()
-    MockWebApi.getUserInfo.restore()
-    return null
-  })
-
-  it('should return the diff', function () {
-    return expect(this.diff).to.deep.equal(this.expected_diff)
-  })
-
-  return it('should get the doc from the doc updater', function () {
-    MockDocUpdaterApi.getDoc
-      .calledWith(this.project_id, this.doc_id)
-      .should.equal(true)
-    return null
-  })
-})

+ 0 - 185
services/track-changes/test/acceptance/js/GettingUpdatesTests.js

@@ -1,185 +0,0 @@
-/* eslint-disable
-    chai-friendly/no-unused-expressions,
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-const sinon = require('sinon')
-const { expect } = require('chai')
-const { ObjectId } = require('../../../app/js/mongodb')
-const Settings = require('@overleaf/settings')
-
-const TrackChangesApp = require('./helpers/TrackChangesApp')
-const TrackChangesClient = require('./helpers/TrackChangesClient')
-const MockWebApi = require('./helpers/MockWebApi')
-
-describe('Getting updates', function () {
-  before(function (done) {
-    this.now = Date.now()
-    this.to = this.now
-    this.user_id = ObjectId().toString()
-    this.deleted_user_id = 'deleted_user'
-    this.doc_id = ObjectId().toString()
-    this.project_id = ObjectId().toString()
-
-    this.minutes = 60 * 1000
-    this.hours = 60 * this.minutes
-
-    MockWebApi.projects[this.project_id] = {
-      features: {
-        versioning: true,
-      },
-    }
-
-    MockWebApi.users[this.user_id] = this.user = {
-      email: 'user@sharelatex.com',
-      first_name: 'Leo',
-      last_name: 'Lion',
-      id: this.user_id,
-    }
-    sinon.spy(MockWebApi, 'getUserInfo')
-
-    this.updates = []
-    for (let i = 0; i <= 9; i++) {
-      this.updates.push({
-        op: [{ i: 'a', p: 0 }],
-        meta: {
-          ts: this.now - (9 - i) * this.hours - 2 * this.minutes,
-          user_id: this.user_id,
-        },
-        v: 2 * i + 1,
-      })
-      this.updates.push({
-        op: [{ i: 'b', p: 0 }],
-        meta: { ts: this.now - (9 - i) * this.hours, user_id: this.user_id },
-        v: 2 * i + 2,
-      })
-    }
-    this.updates[0].meta.user_id = this.deleted_user_id
-
-    TrackChangesApp.ensureRunning(() => {
-      return TrackChangesClient.pushRawUpdates(
-        this.project_id,
-        this.doc_id,
-        this.updates,
-        error => {
-          if (error != null) {
-            throw error
-          }
-          return done()
-        }
-      )
-    })
-    return null
-  })
-  ;({
-    after() {
-      MockWebApi.getUserInfo.restore()
-      return null
-    },
-  })
-
-  describe('getting updates up to the limit', function () {
-    before(function (done) {
-      TrackChangesClient.getUpdates(
-        this.project_id,
-        { before: this.to + 1, min_count: 3 },
-        (error, body) => {
-          if (error != null) {
-            throw error
-          }
-          this.updates = body.updates
-          return done()
-        }
-      )
-      return null
-    })
-
-    it('should fetch the user details from the web api', function () {
-      return MockWebApi.getUserInfo.calledWith(this.user_id).should.equal(true)
-    })
-
-    return it('should return at least the min_count number of summarized updates', function () {
-      const docs1 = {}
-      docs1[this.doc_id] = { toV: 20, fromV: 19 }
-      const docs2 = {}
-      docs2[this.doc_id] = { toV: 18, fromV: 17 }
-      const docs3 = {}
-      docs3[this.doc_id] = { toV: 16, fromV: 15 }
-      return expect(this.updates.slice(0, 3)).to.deep.equal([
-        {
-          docs: docs1,
-          meta: {
-            start_ts: this.to - 2 * this.minutes,
-            end_ts: this.to,
-            users: [this.user],
-          },
-        },
-        {
-          docs: docs2,
-          meta: {
-            start_ts: this.to - 1 * this.hours - 2 * this.minutes,
-            end_ts: this.to - 1 * this.hours,
-            users: [this.user],
-          },
-        },
-        {
-          docs: docs3,
-          meta: {
-            start_ts: this.to - 2 * this.hours - 2 * this.minutes,
-            end_ts: this.to - 2 * this.hours,
-            users: [this.user],
-          },
-        },
-      ])
-    })
-  })
-
-  return describe('getting updates beyond the end of the database', function () {
-    before(function (done) {
-      TrackChangesClient.getUpdates(
-        this.project_id,
-        { before: this.to - 8 * this.hours + 1, min_count: 30 },
-        (error, body) => {
-          if (error != null) {
-            throw error
-          }
-          this.updates = body.updates
-          return done()
-        }
-      )
-      return null
-    })
-
-    return it('should return as many updates as it can', function () {
-      const docs1 = {}
-      docs1[this.doc_id] = { toV: 4, fromV: 3 }
-      const docs2 = {}
-      docs2[this.doc_id] = { toV: 2, fromV: 1 }
-      return expect(this.updates).to.deep.equal([
-        {
-          docs: docs1,
-          meta: {
-            start_ts: this.to - 8 * this.hours - 2 * this.minutes,
-            end_ts: this.to - 8 * this.hours,
-            users: [this.user],
-          },
-        },
-        {
-          docs: docs2,
-          meta: {
-            start_ts: this.to - 9 * this.hours - 2 * this.minutes,
-            end_ts: this.to - 9 * this.hours,
-            users: [this.user, null],
-          },
-        },
-      ])
-    })
-  })
-})

+ 0 - 64
services/track-changes/test/acceptance/js/LockManagerTests.js

@@ -1,64 +0,0 @@
-/* eslint-disable
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-const sinon = require('sinon')
-const { expect } = require('chai')
-const Settings = require('@overleaf/settings')
-const LockManager = require('../../../app/js/LockManager')
-const rclient = require('redis').createClient(Settings.redis.history) // Only works locally for now
-const TrackChangesApp = require('./helpers/TrackChangesApp')
-
-describe('Locking document', function () {
-  before(function (done) {
-    TrackChangesApp.ensureRunning(done)
-    return null
-  })
-
-  return describe('when the lock has expired in redis', function () {
-    before(function (done) {
-      LockManager.LOCK_TTL = 1 // second
-      LockManager.runWithLock(
-        'doc123',
-        releaseA => {
-          // we create a lock A and allow it to expire in redis
-          return setTimeout(
-            () =>
-              // now we create a new lock B and try to release A
-              LockManager.runWithLock(
-                'doc123',
-                releaseB => {
-                  return releaseA()
-                }, // try to release lock A to see if it wipes out lock B
-                () => {}
-              ),
-
-            // we never release lock B so nothing should happen here
-            1500
-          )
-        }, // enough time to wait until the lock has expired
-        err => {
-          // we get here after trying to release lock A
-          expect(err).to.exist
-          done()
-        }
-      )
-      return null
-    })
-
-    return it('the new lock should not be removed by the expired locker', function (done) {
-      LockManager.checkLock('doc123', (err, isFree) => {
-        if (err) return done(err)
-        expect(isFree).to.equal(false)
-        return done()
-      })
-      return null
-    })
-  })
-})

+ 0 - 116
services/track-changes/test/acceptance/js/RestoringVersions.js

@@ -1,116 +0,0 @@
-/* eslint-disable
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-const sinon = require('sinon')
-const { expect } = require('chai')
-const { ObjectId } = require('../../../app/js/mongodb')
-const Settings = require('@overleaf/settings')
-
-const TrackChangesApp = require('./helpers/TrackChangesApp')
-const TrackChangesClient = require('./helpers/TrackChangesClient')
-const MockDocUpdaterApi = require('./helpers/MockDocUpdaterApi')
-const MockWebApi = require('./helpers/MockWebApi')
-
-describe('Restoring a version', function () {
-  before(function (done) {
-    sinon.spy(MockDocUpdaterApi, 'setDoc')
-
-    this.now = Date.now()
-    this.user_id = ObjectId().toString()
-    this.doc_id = ObjectId().toString()
-    this.project_id = ObjectId().toString()
-    MockWebApi.projects[this.project_id] = { features: { versioning: true } }
-
-    const minutes = 60 * 1000
-
-    this.updates = [
-      {
-        op: [{ i: 'one ', p: 0 }],
-        meta: { ts: this.now - 6 * minutes, user_id: this.user_id },
-        v: 3,
-      },
-      {
-        op: [{ i: 'two ', p: 4 }],
-        meta: { ts: this.now - 4 * minutes, user_id: this.user_id },
-        v: 4,
-      },
-      {
-        op: [{ i: 'three ', p: 8 }],
-        meta: { ts: this.now - 2 * minutes, user_id: this.user_id },
-        v: 5,
-      },
-      {
-        op: [{ i: 'four', p: 14 }],
-        meta: { ts: this.now, user_id: this.user_id },
-        v: 6,
-      },
-    ]
-    this.lines = ['one two three four']
-    this.restored_lines = ['one two ']
-    this.beforeVersion = 5
-
-    MockWebApi.users[this.user_id] = this.user = {
-      email: 'user@sharelatex.com',
-      first_name: 'Leo',
-      last_name: 'Lion',
-      id: this.user_id,
-    }
-
-    MockDocUpdaterApi.docs[this.doc_id] = {
-      lines: this.lines,
-      version: 7,
-    }
-
-    TrackChangesApp.ensureRunning(() => {
-      return TrackChangesClient.pushRawUpdates(
-        this.project_id,
-        this.doc_id,
-        this.updates,
-        error => {
-          if (error != null) {
-            throw error
-          }
-          return TrackChangesClient.restoreDoc(
-            this.project_id,
-            this.doc_id,
-            this.beforeVersion,
-            this.user_id,
-            error => {
-              if (error != null) {
-                throw error
-              }
-              return done()
-            }
-          )
-        }
-      )
-    })
-    return null
-  })
-
-  after(function () {
-    MockDocUpdaterApi.setDoc.restore()
-    return null
-  })
-
-  return it('should set the doc in the doc updater', function () {
-    MockDocUpdaterApi.setDoc
-      .calledWith(
-        this.project_id,
-        this.doc_id,
-        this.restored_lines,
-        this.user_id,
-        true
-      )
-      .should.equal(true)
-    return null
-  })
-})

+ 0 - 50
services/track-changes/test/acceptance/js/helpers/MockDocStoreApi.js

@@ -1,50 +0,0 @@
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-let MockDocUpdaterApi
-const express = require('express')
-const app = express()
-
-module.exports = MockDocUpdaterApi = {
-  docs: {},
-
-  getAllDoc(projectId, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    return callback(null, this.docs)
-  },
-
-  run() {
-    app.get('/project/:project_id/doc', (req, res, next) => {
-      return this.getAllDoc(req.params.project_id, (error, docs) => {
-        if (error != null) {
-          res.sendStatus(500)
-        }
-        if (docs == null) {
-          return res.sendStatus(404)
-        } else {
-          return res.send(JSON.stringify(docs))
-        }
-      })
-    })
-
-    return app
-      .listen(3016, error => {
-        if (error != null) {
-          throw error
-        }
-      })
-      .on('error', error => {
-        console.error('error starting MockDocStoreApi:', error.message)
-        return process.exit(1)
-      })
-  },
-}
-
-MockDocUpdaterApi.run()

+ 0 - 87
services/track-changes/test/acceptance/js/helpers/MockDocUpdaterApi.js

@@ -1,87 +0,0 @@
-/* eslint-disable
-    no-undef,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-let MockDocUpdaterApi
-const express = require('express')
-const bodyParser = require('body-parser')
-const app = express()
-app.use(bodyParser.json())
-
-module.exports = MockDocUpdaterApi = {
-  docs: {},
-
-  getDoc(projectId, docId, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    return callback(null, this.docs[docId])
-  },
-
-  setDoc(projectId, docId, lines, userId, undoing, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    if (!this.docs[docId]) {
-      this.docs[docId] = {}
-    }
-    this.docs[docId].lines = lines
-    return callback()
-  },
-
-  run() {
-    app.get('/project/:project_id/doc/:doc_id', (req, res, next) => {
-      return this.getDoc(
-        req.params.project_id,
-        req.params.doc_id,
-        (error, doc) => {
-          if (error != null) {
-            res.sendStatus(500)
-          }
-          if (doc == null) {
-            return res.sendStatus(404)
-          } else {
-            return res.send(JSON.stringify(doc))
-          }
-        }
-      )
-    })
-
-    app.post('/project/:project_id/doc/:doc_id', (req, res, next) => {
-      return this.setDoc(
-        req.params.project_id,
-        req.params.doc_id,
-        req.body.lines,
-        req.body.user_id,
-        req.body.undoing,
-        (errr, doc) => {
-          if (typeof error !== 'undefined' && error !== null) {
-            return res.sendStatus(500)
-          } else {
-            return res.sendStatus(204)
-          }
-        }
-      )
-    })
-
-    return app
-      .listen(3003, error => {
-        if (error != null) {
-          throw error
-        }
-      })
-      .on('error', error => {
-        console.error('error starting MockDocUpdaterApi:', error.message)
-        return process.exit(1)
-      })
-  },
-}
-
-MockDocUpdaterApi.run()

+ 0 - 72
services/track-changes/test/acceptance/js/helpers/MockWebApi.js

@@ -1,72 +0,0 @@
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-let MockWebApi
-const express = require('express')
-const app = express()
-
-module.exports = MockWebApi = {
-  users: {},
-
-  projects: {},
-
-  getUserInfo(userId, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    return callback(null, this.users[userId] || null)
-  },
-
-  getProjectDetails(projectId, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    return callback(null, this.projects[projectId])
-  },
-
-  run() {
-    app.get('/user/:user_id/personal_info', (req, res, next) => {
-      return this.getUserInfo(req.params.user_id, (error, user) => {
-        if (error != null) {
-          res.sendStatus(500)
-        }
-        if (user == null) {
-          return res.sendStatus(404)
-        } else {
-          return res.send(JSON.stringify(user))
-        }
-      })
-    })
-
-    app.get('/project/:project_id/details', (req, res, next) => {
-      return this.getProjectDetails(req.params.project_id, (error, project) => {
-        if (error != null) {
-          res.sendStatus(500)
-        }
-        if (project == null) {
-          return res.sendStatus(404)
-        } else {
-          return res.send(JSON.stringify(project))
-        }
-      })
-    })
-
-    return app
-      .listen(3000, error => {
-        if (error != null) {
-          throw error
-        }
-      })
-      .on('error', error => {
-        console.error('error starting MockWebApiServer:', error.message)
-        return process.exit(1)
-      })
-  },
-}
-
-MockWebApi.run()

+ 0 - 64
services/track-changes/test/acceptance/js/helpers/TrackChangesApp.js

@@ -1,64 +0,0 @@
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS101: Remove unnecessary use of Array.from
- * DS102: Remove unnecessary code created because of implicit returns
- * DS103: Rewrite code to no longer use __guard__
- * DS205: Consider reworking code to avoid use of IIFEs
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-const app = require('../../../../app')
-const { waitForDb } = require('../../../../app/js/mongodb')
-const logger = require('@overleaf/logger')
-const Settings = require('@overleaf/settings')
-
-module.exports = {
-  running: false,
-  initing: false,
-  callbacks: [],
-  ensureRunning(callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    if (this.running) {
-      return callback()
-    } else if (this.initing) {
-      return this.callbacks.push(callback)
-    }
-    this.initing = true
-    this.callbacks.push(callback)
-    waitForDb().then(() => {
-      return app.listen(
-        __guard__(
-          Settings.internal != null
-            ? Settings.internal.trackchanges
-            : undefined,
-          x => x.port
-        ),
-        'localhost',
-        error => {
-          if (error != null) {
-            throw error
-          }
-          this.running = true
-          logger.info('track changes running in dev mode')
-
-          return (() => {
-            const result = []
-            for (callback of Array.from(this.callbacks)) {
-              result.push(callback())
-            }
-            return result
-          })()
-        }
-      )
-    })
-  },
-}
-function __guard__(value, transform) {
-  return typeof value !== 'undefined' && value !== null
-    ? transform(value)
-    : undefined
-}

+ 0 - 302
services/track-changes/test/acceptance/js/helpers/TrackChangesClient.js

@@ -1,302 +0,0 @@
-/* eslint-disable
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS101: Remove unnecessary use of Array.from
- * DS102: Remove unnecessary code created because of implicit returns
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-let TrackChangesClient
-const async = require('async')
-const zlib = require('zlib')
-const request = require('request')
-const Settings = require('@overleaf/settings')
-const rclient = require('@overleaf/redis-wrapper').createClient(
-  Settings.redis.history
-) // Only works locally for now
-const Keys = Settings.redis.history.key_schema
-const { db, ObjectId } = require('../../../../app/js/mongodb')
-
-const aws = require('aws-sdk')
-const s3 = new aws.S3({
-  accessKeyId: Settings.trackchanges.s3.key,
-  secretAccessKey: Settings.trackchanges.s3.secret,
-  endpoint: Settings.trackchanges.s3.endpoint,
-  s3ForcePathStyle: Settings.trackchanges.s3.pathStyle,
-})
-const S3_BUCKET = Settings.trackchanges.stores.doc_history
-
-module.exports = TrackChangesClient = {
-  flushAndGetCompressedUpdates(projectId, docId, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    return TrackChangesClient.flushDoc(projectId, docId, error => {
-      if (error != null) {
-        return callback(error)
-      }
-      return TrackChangesClient.getCompressedUpdates(docId, callback)
-    })
-  },
-
-  flushDoc(projectId, docId, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    return request.post(
-      {
-        url: `http://localhost:3015/project/${projectId}/doc/${docId}/flush`,
-      },
-      (error, response, body) => {
-        response.statusCode.should.equal(204)
-        return callback(error)
-      }
-    )
-  },
-
-  flushProject(projectId, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    return request.post(
-      {
-        url: `http://localhost:3015/project/${projectId}/flush`,
-      },
-      (error, response, body) => {
-        response.statusCode.should.equal(204)
-        return callback(error)
-      }
-    )
-  },
-
-  getCompressedUpdates(docId, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    return db.docHistory
-      .find({ doc_id: ObjectId(docId) })
-      .sort({ 'meta.end_ts': 1 })
-      .toArray(callback)
-  },
-
-  getProjectMetaData(projectId, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    return db.projectHistoryMetaData.findOne(
-      {
-        project_id: ObjectId(projectId),
-      },
-      callback
-    )
-  },
-
-  setPreserveHistoryForProject(projectId, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    return db.projectHistoryMetaData.updateOne(
-      {
-        project_id: ObjectId(projectId),
-      },
-      {
-        $set: { preserveHistory: true },
-      },
-      {
-        upsert: true,
-      },
-      callback
-    )
-  },
-
-  pushRawUpdates(projectId, docId, updates, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    return rclient.sadd(
-      Keys.docsWithHistoryOps({ project_id: projectId }),
-      docId,
-      error => {
-        if (error != null) {
-          return callback(error)
-        }
-        return rclient.rpush(
-          Keys.uncompressedHistoryOps({ doc_id: docId }),
-          ...Array.from(Array.from(updates).map(u => JSON.stringify(u))),
-          callback
-        )
-      }
-    )
-  },
-
-  getDiff(projectId, docId, from, to, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    return request.get(
-      {
-        url: `http://localhost:3015/project/${projectId}/doc/${docId}/diff?from=${from}&to=${to}`,
-      },
-      (error, response, body) => {
-        if (error) return callback(error)
-        response.statusCode.should.equal(200)
-        return callback(null, JSON.parse(body))
-      }
-    )
-  },
-
-  getUpdates(projectId, options, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    return request.get(
-      {
-        url: `http://localhost:3015/project/${projectId}/updates?before=${options.before}&min_count=${options.min_count}`,
-      },
-      (error, response, body) => {
-        if (error) return callback(error)
-        response.statusCode.should.equal(200)
-        return callback(null, JSON.parse(body))
-      }
-    )
-  },
-
-  exportProject(projectId, callback) {
-    request.get(
-      { url: `http://localhost:3015/project/${projectId}/export`, json: true },
-      (error, response, updates) => {
-        if (error) return callback(error)
-        response.statusCode.should.equal(200)
-        callback(null, updates, JSON.parse(response.trailers['x-user-ids']))
-      }
-    )
-  },
-
-  restoreDoc(projectId, docId, version, userId, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    return request.post(
-      {
-        url: `http://localhost:3015/project/${projectId}/doc/${docId}/version/${version}/restore`,
-        headers: {
-          'X-User-Id': userId,
-        },
-      },
-      (error, response, body) => {
-        if (error) return callback(error)
-        response.statusCode.should.equal(204)
-        return callback(null)
-      }
-    )
-  },
-
-  pushDocHistory(projectId, docId, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    return request.post(
-      {
-        url: `http://localhost:3015/project/${projectId}/doc/${docId}/push`,
-      },
-      (error, response, body) => {
-        response.statusCode.should.equal(204)
-        return callback(error)
-      }
-    )
-  },
-
-  pullDocHistory(projectId, docId, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    return request.post(
-      {
-        url: `http://localhost:3015/project/${projectId}/doc/${docId}/pull`,
-      },
-      (error, response, body) => {
-        response.statusCode.should.equal(204)
-        return callback(error)
-      }
-    )
-  },
-
-  waitForS3(done, retries) {
-    if (retries == null) {
-      retries = 42
-    }
-    if (!Settings.trackchanges.s3.endpoint) {
-      return done()
-    }
-
-    return request.get(`${Settings.trackchanges.s3.endpoint}/`, (err, res) => {
-      if (res && res.statusCode < 500) {
-        return done()
-      }
-
-      if (retries === 0) {
-        return done(err || new Error(`s3 returned ${res.statusCode}`))
-      }
-
-      return setTimeout(
-        () => TrackChangesClient.waitForS3(done, --retries),
-        1000
-      )
-    })
-  },
-
-  getS3Doc(projectId, docId, packId, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    const params = {
-      Bucket: S3_BUCKET,
-      Key: `${projectId}/changes-${docId}/pack-${packId}`,
-    }
-
-    return s3.getObject(params, (error, data) => {
-      if (error != null) {
-        return callback(error)
-      }
-      const body = data.Body
-      if (body == null) {
-        return callback(new Error('empty response from s3'))
-      }
-      return zlib.gunzip(body, (err, result) => {
-        if (err != null) {
-          return callback(err)
-        }
-        return callback(null, JSON.parse(result.toString()))
-      })
-    })
-  },
-
-  removeS3Doc(projectId, docId, callback) {
-    if (callback == null) {
-      callback = function () {}
-    }
-    let params = {
-      Bucket: S3_BUCKET,
-      Prefix: `${projectId}/changes-${docId}`,
-    }
-
-    return s3.listObjects(params, (error, data) => {
-      if (error != null) {
-        return callback(error)
-      }
-
-      params = {
-        Bucket: S3_BUCKET,
-        Delete: {
-          Objects: data.Contents.map(s3object => ({ Key: s3object.Key })),
-        },
-      }
-
-      return s3.deleteObjects(params, callback)
-    })
-  },
-}

+ 0 - 21
services/track-changes/test/setup.js

@@ -1,21 +0,0 @@
-const chai = require('chai')
-const SandboxedModule = require('sandboxed-module')
-
-// Chai configuration
-chai.should()
-
-// SandboxedModule configuration
-SandboxedModule.configure({
-  requires: {
-    '@overleaf/logger': {
-      debug() {},
-      log() {},
-      info() {},
-      warn() {},
-      err() {},
-      error() {},
-      fatal() {},
-    },
-  },
-  globals: { Buffer, JSON, console, process },
-})

+ 0 - 456
services/track-changes/test/unit/js/DiffGenerator/DiffGeneratorTests.js

@@ -1,456 +0,0 @@
-/* eslint-disable
-    no-return-assign,
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS101: Remove unnecessary use of Array.from
- * DS102: Remove unnecessary code created because of implicit returns
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-const sinon = require('sinon')
-const { expect } = require('chai')
-const modulePath = '../../../../app/js/DiffGenerator.js'
-const SandboxedModule = require('sandboxed-module')
-
-describe('DiffGenerator', function () {
-  beforeEach(function () {
-    this.DiffGenerator = SandboxedModule.require(modulePath, {})
-    this.ts = Date.now()
-    this.user_id = 'mock-user-id'
-    this.user_id_2 = 'mock-user-id-2'
-    return (this.meta = {
-      start_ts: this.ts,
-      end_ts: this.ts,
-      user_id: this.user_id,
-    })
-  })
-
-  describe('rewindOp', function () {
-    describe('rewinding an insert', function () {
-      return it('should undo the insert', function () {
-        const content = 'hello world'
-        const rewoundContent = this.DiffGenerator.rewindOp(content, {
-          p: 6,
-          i: 'wo',
-        })
-        return rewoundContent.should.equal('hello rld')
-      })
-    })
-
-    describe('rewinding a delete', function () {
-      return it('should undo the delete', function () {
-        const content = 'hello rld'
-        const rewoundContent = this.DiffGenerator.rewindOp(content, {
-          p: 6,
-          d: 'wo',
-        })
-        return rewoundContent.should.equal('hello world')
-      })
-    })
-
-    describe('with an inconsistent update', function () {
-      return it('should throw an error', function () {
-        const content = 'hello world'
-        return expect(() => {
-          return this.DiffGenerator.rewindOp(content, { p: 6, i: 'foo' })
-        }).to.throw(this.DiffGenerator.ConsistencyError)
-      })
-    })
-
-    return describe('with an update which is beyond the length of the content', function () {
-      return it('should undo the insert as if it were at the end of the content', function () {
-        const content = 'foobar'
-        const rewoundContent = this.DiffGenerator.rewindOp(content, {
-          p: 4,
-          i: 'bar',
-        })
-        return rewoundContent.should.equal('foo')
-      })
-    })
-  })
-
-  describe('rewindUpdate', function () {
-    return it('should rewind ops in reverse', function () {
-      const content = 'aaabbbccc'
-      const update = {
-        op: [
-          { p: 3, i: 'bbb' },
-          { p: 6, i: 'ccc' },
-        ],
-      }
-      const rewoundContent = this.DiffGenerator.rewindUpdate(content, update)
-      return rewoundContent.should.equal('aaa')
-    })
-  })
-
-  describe('rewindUpdates', function () {
-    return it('should rewind updates in reverse', function () {
-      const content = 'aaabbbccc'
-      const updates = [
-        { op: [{ p: 3, i: 'bbb' }] },
-        { op: [{ p: 6, i: 'ccc' }] },
-      ]
-      const rewoundContent = this.DiffGenerator.rewindUpdates(content, updates)
-      return rewoundContent.should.equal('aaa')
-    })
-  })
-
-  describe('buildDiff', function () {
-    beforeEach(function () {
-      this.diff = [{ u: 'mock-diff' }]
-      this.content = 'Hello world'
-      this.updates = [
-        { i: 'mock-update-1' },
-        { i: 'mock-update-2' },
-        { i: 'mock-update-3' },
-      ]
-      this.DiffGenerator.applyUpdateToDiff = sinon.stub().returns(this.diff)
-      this.DiffGenerator.compressDiff = sinon.stub().returns(this.diff)
-      return (this.result = this.DiffGenerator.buildDiff(
-        this.content,
-        this.updates
-      ))
-    })
-
-    it('should return the diff', function () {
-      return this.result.should.deep.equal(this.diff)
-    })
-
-    it('should build the content into an initial diff', function () {
-      return this.DiffGenerator.applyUpdateToDiff
-        .calledWith(
-          [
-            {
-              u: this.content,
-            },
-          ],
-          this.updates[0]
-        )
-        .should.equal(true)
-    })
-
-    it('should apply each update', function () {
-      return Array.from(this.updates).map(update =>
-        this.DiffGenerator.applyUpdateToDiff
-          .calledWith(sinon.match.any, update)
-          .should.equal(true)
-      )
-    })
-
-    return it('should compress the diff', function () {
-      return this.DiffGenerator.compressDiff
-        .calledWith(this.diff)
-        .should.equal(true)
-    })
-  })
-
-  describe('compressDiff', function () {
-    describe('with adjacent inserts with the same user_id', function () {
-      return it('should create one update with combined meta data and min/max timestamps', function () {
-        const diff = this.DiffGenerator.compressDiff([
-          {
-            i: 'foo',
-            meta: { start_ts: 10, end_ts: 20, user: { id: this.user_id } },
-          },
-          {
-            i: 'bar',
-            meta: { start_ts: 5, end_ts: 15, user: { id: this.user_id } },
-          },
-        ])
-        return expect(diff).to.deep.equal([
-          {
-            i: 'foobar',
-            meta: { start_ts: 5, end_ts: 20, user: { id: this.user_id } },
-          },
-        ])
-      })
-    })
-
-    describe('with adjacent inserts with different user_ids', function () {
-      return it('should leave the inserts unchanged', function () {
-        const input = [
-          {
-            i: 'foo',
-            meta: { start_ts: 10, end_ts: 20, user: { id: this.user_id } },
-          },
-          {
-            i: 'bar',
-            meta: { start_ts: 5, end_ts: 15, user: { id: this.user_id_2 } },
-          },
-        ]
-        const output = this.DiffGenerator.compressDiff(input)
-        return expect(output).to.deep.equal(input)
-      })
-    })
-
-    describe('with adjacent deletes with the same user_id', function () {
-      return it('should create one update with combined meta data and min/max timestamps', function () {
-        const diff = this.DiffGenerator.compressDiff([
-          {
-            d: 'foo',
-            meta: { start_ts: 10, end_ts: 20, user: { id: this.user_id } },
-          },
-          {
-            d: 'bar',
-            meta: { start_ts: 5, end_ts: 15, user: { id: this.user_id } },
-          },
-        ])
-        return expect(diff).to.deep.equal([
-          {
-            d: 'foobar',
-            meta: { start_ts: 5, end_ts: 20, user: { id: this.user_id } },
-          },
-        ])
-      })
-    })
-
-    return describe('with adjacent deletes with different user_ids', function () {
-      return it('should leave the deletes unchanged', function () {
-        const input = [
-          {
-            d: 'foo',
-            meta: { start_ts: 10, end_ts: 20, user: { id: this.user_id } },
-          },
-          {
-            d: 'bar',
-            meta: { start_ts: 5, end_ts: 15, user: { id: this.user_id_2 } },
-          },
-        ]
-        const output = this.DiffGenerator.compressDiff(input)
-        return expect(output).to.deep.equal(input)
-      })
-    })
-  })
-
-  return describe('applyUpdateToDiff', function () {
-    describe('an insert', function () {
-      it('should insert into the middle of (u)nchanged text', function () {
-        const diff = this.DiffGenerator.applyUpdateToDiff([{ u: 'foobar' }], {
-          op: [{ p: 3, i: 'baz' }],
-          meta: this.meta,
-        })
-        return expect(diff).to.deep.equal([
-          { u: 'foo' },
-          { i: 'baz', meta: this.meta },
-          { u: 'bar' },
-        ])
-      })
-
-      it('should insert into the start of (u)changed text', function () {
-        const diff = this.DiffGenerator.applyUpdateToDiff([{ u: 'foobar' }], {
-          op: [{ p: 0, i: 'baz' }],
-          meta: this.meta,
-        })
-        return expect(diff).to.deep.equal([
-          { i: 'baz', meta: this.meta },
-          { u: 'foobar' },
-        ])
-      })
-
-      it('should insert into the end of (u)changed text', function () {
-        const diff = this.DiffGenerator.applyUpdateToDiff([{ u: 'foobar' }], {
-          op: [{ p: 6, i: 'baz' }],
-          meta: this.meta,
-        })
-        return expect(diff).to.deep.equal([
-          { u: 'foobar' },
-          { i: 'baz', meta: this.meta },
-        ])
-      })
-
-      it('should insert into the middle of (i)inserted text', function () {
-        const diff = this.DiffGenerator.applyUpdateToDiff(
-          [{ i: 'foobar', meta: this.meta }],
-          { op: [{ p: 3, i: 'baz' }], meta: this.meta }
-        )
-        return expect(diff).to.deep.equal([
-          { i: 'foo', meta: this.meta },
-          { i: 'baz', meta: this.meta },
-          { i: 'bar', meta: this.meta },
-        ])
-      })
-
-      return it('should not count deletes in the running length total', function () {
-        const diff = this.DiffGenerator.applyUpdateToDiff(
-          [{ d: 'deleted', meta: this.meta }, { u: 'foobar' }],
-          { op: [{ p: 3, i: 'baz' }], meta: this.meta }
-        )
-        return expect(diff).to.deep.equal([
-          { d: 'deleted', meta: this.meta },
-          { u: 'foo' },
-          { i: 'baz', meta: this.meta },
-          { u: 'bar' },
-        ])
-      })
-    })
-
-    return describe('a delete', function () {
-      describe('deleting unchanged text', function () {
-        it('should delete from the middle of (u)nchanged text', function () {
-          const diff = this.DiffGenerator.applyUpdateToDiff(
-            [{ u: 'foobazbar' }],
-            { op: [{ p: 3, d: 'baz' }], meta: this.meta }
-          )
-          return expect(diff).to.deep.equal([
-            { u: 'foo' },
-            { d: 'baz', meta: this.meta },
-            { u: 'bar' },
-          ])
-        })
-
-        it('should delete from the start of (u)nchanged text', function () {
-          const diff = this.DiffGenerator.applyUpdateToDiff(
-            [{ u: 'foobazbar' }],
-            { op: [{ p: 0, d: 'foo' }], meta: this.meta }
-          )
-          return expect(diff).to.deep.equal([
-            { d: 'foo', meta: this.meta },
-            { u: 'bazbar' },
-          ])
-        })
-
-        it('should delete from the end of (u)nchanged text', function () {
-          const diff = this.DiffGenerator.applyUpdateToDiff(
-            [{ u: 'foobazbar' }],
-            { op: [{ p: 6, d: 'bar' }], meta: this.meta }
-          )
-          return expect(diff).to.deep.equal([
-            { u: 'foobaz' },
-            { d: 'bar', meta: this.meta },
-          ])
-        })
-
-        return it('should delete across multiple (u)changed text parts', function () {
-          const diff = this.DiffGenerator.applyUpdateToDiff(
-            [{ u: 'foo' }, { u: 'baz' }, { u: 'bar' }],
-            { op: [{ p: 2, d: 'obazb' }], meta: this.meta }
-          )
-          return expect(diff).to.deep.equal([
-            { u: 'fo' },
-            { d: 'o', meta: this.meta },
-            { d: 'baz', meta: this.meta },
-            { d: 'b', meta: this.meta },
-            { u: 'ar' },
-          ])
-        })
-      })
-
-      describe('deleting inserts', function () {
-        it('should delete from the middle of (i)nserted text', function () {
-          const diff = this.DiffGenerator.applyUpdateToDiff(
-            [{ i: 'foobazbar', meta: this.meta }],
-            { op: [{ p: 3, d: 'baz' }], meta: this.meta }
-          )
-          return expect(diff).to.deep.equal([
-            { i: 'foo', meta: this.meta },
-            { i: 'bar', meta: this.meta },
-          ])
-        })
-
-        it('should delete from the start of (u)nchanged text', function () {
-          const diff = this.DiffGenerator.applyUpdateToDiff(
-            [{ i: 'foobazbar', meta: this.meta }],
-            { op: [{ p: 0, d: 'foo' }], meta: this.meta }
-          )
-          return expect(diff).to.deep.equal([{ i: 'bazbar', meta: this.meta }])
-        })
-
-        it('should delete from the end of (u)nchanged text', function () {
-          const diff = this.DiffGenerator.applyUpdateToDiff(
-            [{ i: 'foobazbar', meta: this.meta }],
-            { op: [{ p: 6, d: 'bar' }], meta: this.meta }
-          )
-          return expect(diff).to.deep.equal([{ i: 'foobaz', meta: this.meta }])
-        })
-
-        return it('should delete across multiple (u)changed and (i)nserted text parts', function () {
-          const diff = this.DiffGenerator.applyUpdateToDiff(
-            [{ u: 'foo' }, { i: 'baz', meta: this.meta }, { u: 'bar' }],
-            { op: [{ p: 2, d: 'obazb' }], meta: this.meta }
-          )
-          return expect(diff).to.deep.equal([
-            { u: 'fo' },
-            { d: 'o', meta: this.meta },
-            { d: 'b', meta: this.meta },
-            { u: 'ar' },
-          ])
-        })
-      })
-
-      describe('deleting over existing deletes', function () {
-        return it('should delete across multiple (u)changed and (d)deleted text parts', function () {
-          const diff = this.DiffGenerator.applyUpdateToDiff(
-            [{ u: 'foo' }, { d: 'baz', meta: this.meta }, { u: 'bar' }],
-            { op: [{ p: 2, d: 'ob' }], meta: this.meta }
-          )
-          return expect(diff).to.deep.equal([
-            { u: 'fo' },
-            { d: 'o', meta: this.meta },
-            { d: 'baz', meta: this.meta },
-            { d: 'b', meta: this.meta },
-            { u: 'ar' },
-          ])
-        })
-      })
-
-      describe("deleting when the text doesn't match", function () {
-        it('should throw an error when deleting from the middle of (u)nchanged text', function () {
-          return expect(() =>
-            this.DiffGenerator.applyUpdateToDiff([{ u: 'foobazbar' }], {
-              op: [{ p: 3, d: 'xxx' }],
-              meta: this.meta,
-            })
-          ).to.throw(this.DiffGenerator.ConsistencyError)
-        })
-
-        it('should throw an error when deleting from the start of (u)nchanged text', function () {
-          return expect(() =>
-            this.DiffGenerator.applyUpdateToDiff([{ u: 'foobazbar' }], {
-              op: [{ p: 0, d: 'xxx' }],
-              meta: this.meta,
-            })
-          ).to.throw(this.DiffGenerator.ConsistencyError)
-        })
-
-        return it('should throw an error when deleting from the end of (u)nchanged text', function () {
-          return expect(() =>
-            this.DiffGenerator.applyUpdateToDiff([{ u: 'foobazbar' }], {
-              op: [{ p: 6, d: 'xxx' }],
-              meta: this.meta,
-            })
-          ).to.throw(this.DiffGenerator.ConsistencyError)
-        })
-      })
-
-      describe('when the last update in the existing diff is a delete', function () {
-        return it('should insert the new update before the delete', function () {
-          const diff = this.DiffGenerator.applyUpdateToDiff(
-            [{ u: 'foo' }, { d: 'bar', meta: this.meta }],
-            { op: [{ p: 3, i: 'baz' }], meta: this.meta }
-          )
-          return expect(diff).to.deep.equal([
-            { u: 'foo' },
-            { i: 'baz', meta: this.meta },
-            { d: 'bar', meta: this.meta },
-          ])
-        })
-      })
-
-      return describe('when the only update in the existing diff is a delete', function () {
-        return it('should insert the new update after the delete', function () {
-          const diff = this.DiffGenerator.applyUpdateToDiff(
-            [{ d: 'bar', meta: this.meta }],
-            { op: [{ p: 0, i: 'baz' }], meta: this.meta }
-          )
-          return expect(diff).to.deep.equal([
-            { d: 'bar', meta: this.meta },
-            { i: 'baz', meta: this.meta },
-          ])
-        })
-      })
-    })
-  })
-})

+ 0 - 443
services/track-changes/test/unit/js/DiffManager/DiffManagerTests.js

@@ -1,443 +0,0 @@
-/* eslint-disable
-    no-return-assign,
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-const sinon = require('sinon')
-const { expect } = require('chai')
-const modulePath = '../../../../app/js/DiffManager.js'
-const SandboxedModule = require('sandboxed-module')
-
-describe('DiffManager', function () {
-  beforeEach(function () {
-    this.DiffManager = SandboxedModule.require(modulePath, {
-      requires: {
-        './UpdatesManager': (this.UpdatesManager = {}),
-        './DocumentUpdaterManager': (this.DocumentUpdaterManager = {}),
-        './DiffGenerator': (this.DiffGenerator = {}),
-      },
-    })
-    this.callback = sinon.stub()
-    this.from = new Date()
-    this.to = new Date(Date.now() + 10000)
-    this.project_id = 'mock-project-id'
-    return (this.doc_id = 'mock-doc-id')
-  })
-
-  describe('getLatestDocAndUpdates', function () {
-    beforeEach(function () {
-      this.content = 'hello world'
-      this.version = 42
-      this.updates = ['mock-update-1', 'mock-update-2']
-
-      this.DocumentUpdaterManager.getDocument = sinon
-        .stub()
-        .callsArgWith(2, null, this.content, this.version)
-      return (this.UpdatesManager.getDocUpdatesWithUserInfo = sinon
-        .stub()
-        .callsArgWith(3, null, this.updates))
-    })
-
-    describe('with a fromVersion', function () {
-      beforeEach(function () {
-        return this.DiffManager.getLatestDocAndUpdates(
-          this.project_id,
-          this.doc_id,
-          this.from,
-          this.callback
-        )
-      })
-
-      it('should get the latest version of the doc', function () {
-        return this.DocumentUpdaterManager.getDocument
-          .calledWith(this.project_id, this.doc_id)
-          .should.equal(true)
-      })
-
-      it('should get the latest updates', function () {
-        return this.UpdatesManager.getDocUpdatesWithUserInfo
-          .calledWith(this.project_id, this.doc_id, { from: this.from })
-          .should.equal(true)
-      })
-
-      return it('should call the callback with the content, version and updates', function () {
-        return this.callback
-          .calledWith(null, this.content, this.version, this.updates)
-          .should.equal(true)
-      })
-    })
-
-    return describe('with no fromVersion', function () {
-      beforeEach(function () {
-        return this.DiffManager.getLatestDocAndUpdates(
-          this.project_id,
-          this.doc_id,
-          null,
-          this.callback
-        )
-      })
-
-      it('should get the latest version of the doc', function () {
-        return this.DocumentUpdaterManager.getDocument
-          .calledWith(this.project_id, this.doc_id)
-          .should.equal(true)
-      })
-
-      it('should not get the latest updates', function () {
-        return this.UpdatesManager.getDocUpdatesWithUserInfo.called.should.equal(
-          false
-        )
-      })
-
-      return it('should call the callback with the content, version and blank updates', function () {
-        return this.callback
-          .calledWith(null, this.content, this.version, [])
-          .should.equal(true)
-      })
-    })
-  })
-
-  describe('getDiff', function () {
-    beforeEach(function () {
-      this.content = 'hello world'
-      // Op versions are the version they were applied to, so doc is always one version
-      // ahead.s
-      this.version = 43
-      this.updates = [
-        {
-          op: 'mock-4',
-          v: 42,
-          meta: { start_ts: new Date(this.to.getTime() + 20) },
-        },
-        {
-          op: 'mock-3',
-          v: 41,
-          meta: { start_ts: new Date(this.to.getTime() + 10) },
-        },
-        {
-          op: 'mock-2',
-          v: 40,
-          meta: { start_ts: new Date(this.to.getTime() - 10) },
-        },
-        {
-          op: 'mock-1',
-          v: 39,
-          meta: { start_ts: new Date(this.to.getTime() - 20) },
-        },
-      ]
-      this.fromVersion = 39
-      this.toVersion = 40
-      this.diffed_updates = this.updates.slice(2)
-      this.rewound_content = 'rewound-content'
-      return (this.diff = [{ u: 'mock-diff' }])
-    })
-
-    describe('with matching versions', function () {
-      beforeEach(function () {
-        this.DiffManager.getDocumentBeforeVersion = sinon
-          .stub()
-          .callsArgWith(3, null, this.rewound_content, this.updates)
-        this.DiffGenerator.buildDiff = sinon.stub().returns(this.diff)
-        return this.DiffManager.getDiff(
-          this.project_id,
-          this.doc_id,
-          this.fromVersion,
-          this.toVersion,
-          this.callback
-        )
-      })
-
-      it('should get the latest doc and version with all recent updates', function () {
-        return this.DiffManager.getDocumentBeforeVersion
-          .calledWith(this.project_id, this.doc_id, this.fromVersion)
-          .should.equal(true)
-      })
-
-      it('should generate the diff', function () {
-        return this.DiffGenerator.buildDiff
-          .calledWith(
-            this.rewound_content,
-            this.diffed_updates.slice().reverse()
-          )
-          .should.equal(true)
-      })
-
-      return it('should call the callback with the diff', function () {
-        return this.callback.calledWith(null, this.diff).should.equal(true)
-      })
-    })
-
-    describe('when the updates are inconsistent', function () {
-      beforeEach(function () {
-        this.DiffManager.getLatestDocAndUpdates = sinon
-          .stub()
-          .callsArgWith(3, null, this.content, this.version, this.updates)
-        this.DiffGenerator.buildDiff = sinon
-          .stub()
-          .throws((this.error = new Error('inconsistent!')))
-        this.DiffGenerator.rewindUpdates = sinon.stub()
-        this.DiffManager.getDiff(
-          this.project_id,
-          this.doc_id,
-          this.fromVersion,
-          this.toVersion,
-          this.callback
-        )
-      })
-
-      it('should call the callback with an error', function () {
-        this.callback.calledWith(sinon.match(Error)).should.equal(true)
-        const errorObj = this.callback.args[0][0]
-        expect(errorObj.message).to.include('inconsistent!')
-      })
-    })
-  })
-
-  describe('getDocumentBeforeVersion', function () {
-    beforeEach(function () {
-      this.DiffManager._tryGetDocumentBeforeVersion = sinon.stub()
-      this.document = 'mock-documents'
-      return (this.rewound_updates = 'mock-rewound-updates')
-    })
-
-    describe('succesfully', function () {
-      beforeEach(function () {
-        this.DiffManager._tryGetDocumentBeforeVersion.yields(
-          null,
-          this.document,
-          this.rewound_updates
-        )
-        return this.DiffManager.getDocumentBeforeVersion(
-          this.project_id,
-          this.doc_id,
-          this.version,
-          this.callback
-        )
-      })
-
-      it('should call _tryGetDocumentBeforeVersion', function () {
-        return this.DiffManager._tryGetDocumentBeforeVersion
-          .calledWith(this.project_id, this.doc_id, this.version)
-          .should.equal(true)
-      })
-
-      return it('should call the callback with the response', function () {
-        return this.callback
-          .calledWith(null, this.document, this.rewound_updates)
-          .should.equal(true)
-      })
-    })
-
-    describe('with a retry needed', function () {
-      beforeEach(function () {
-        let retried = false
-        this.DiffManager._tryGetDocumentBeforeVersion = (
-          projectId,
-          docId,
-          version,
-          callback
-        ) => {
-          if (!retried) {
-            retried = true
-            const error = new Error()
-            error.retry = true
-            return callback(error)
-          } else {
-            return callback(null, this.document, this.rewound_updates)
-          }
-        }
-        sinon.spy(this.DiffManager, '_tryGetDocumentBeforeVersion')
-        return this.DiffManager.getDocumentBeforeVersion(
-          this.project_id,
-          this.doc_id,
-          this.version,
-          this.callback
-        )
-      })
-
-      it('should call _tryGetDocumentBeforeVersion twice', function () {
-        return this.DiffManager._tryGetDocumentBeforeVersion.calledTwice.should.equal(
-          true
-        )
-      })
-
-      return it('should call the callback with the response', function () {
-        return this.callback
-          .calledWith(null, this.document, this.rewound_updates)
-          .should.equal(true)
-      })
-    })
-
-    describe('with a non-retriable error', function () {
-      beforeEach(function () {
-        this.error = new Error('oops')
-        this.DiffManager._tryGetDocumentBeforeVersion.yields(this.error)
-        return this.DiffManager.getDocumentBeforeVersion(
-          this.project_id,
-          this.doc_id,
-          this.version,
-          this.callback
-        )
-      })
-
-      it('should call _tryGetDocumentBeforeVersion once', function () {
-        return this.DiffManager._tryGetDocumentBeforeVersion.calledOnce.should.equal(
-          true
-        )
-      })
-
-      return it('should call the callback with the error', function () {
-        return this.callback.calledWith(this.error).should.equal(true)
-      })
-    })
-
-    return describe('when retry limit is matched', function () {
-      beforeEach(function () {
-        this.error = new Error('oops')
-        this.error.retry = true
-        this.DiffManager._tryGetDocumentBeforeVersion.yields(this.error)
-        return this.DiffManager.getDocumentBeforeVersion(
-          this.project_id,
-          this.doc_id,
-          this.version,
-          this.callback
-        )
-      })
-
-      it('should call _tryGetDocumentBeforeVersion three times (max retries)', function () {
-        return this.DiffManager._tryGetDocumentBeforeVersion.calledThrice.should.equal(
-          true
-        )
-      })
-
-      return it('should call the callback with the error', function () {
-        return this.callback.calledWith(this.error).should.equal(true)
-      })
-    })
-  })
-
-  return describe('_tryGetDocumentBeforeVersion', function () {
-    beforeEach(function () {
-      this.content = 'hello world'
-      // Op versions are the version they were applied to, so doc is always one version
-      // ahead.s
-      this.version = 43
-      this.updates = [
-        {
-          op: 'mock-4',
-          v: 42,
-          meta: { start_ts: new Date(this.to.getTime() + 20) },
-        },
-        {
-          op: 'mock-3',
-          v: 41,
-          meta: { start_ts: new Date(this.to.getTime() + 10) },
-        },
-        {
-          op: 'mock-2',
-          v: 40,
-          meta: { start_ts: new Date(this.to.getTime() - 10) },
-        },
-        {
-          op: 'mock-1',
-          v: 39,
-          meta: { start_ts: new Date(this.to.getTime() - 20) },
-        },
-      ]
-      this.fromVersion = 39
-      this.rewound_content = 'rewound-content'
-      return (this.diff = [{ u: 'mock-diff' }])
-    })
-
-    describe('with matching versions', function () {
-      beforeEach(function () {
-        this.DiffManager.getLatestDocAndUpdates = sinon
-          .stub()
-          .callsArgWith(3, null, this.content, this.version, this.updates)
-        this.DiffGenerator.rewindUpdates = sinon.spy((content, updates) => {
-          // the rewindUpdates method reverses the 'updates' array
-          updates.reverse()
-          return this.rewound_content
-        })
-        this.rewindUpdatesWithArgs = this.DiffGenerator.rewindUpdates.withArgs(
-          this.content,
-          this.updates.slice().reverse()
-        )
-        return this.DiffManager._tryGetDocumentBeforeVersion(
-          this.project_id,
-          this.doc_id,
-          this.fromVersion,
-          this.callback
-        )
-      })
-
-      it('should get the latest doc and version with all recent updates', function () {
-        return this.DiffManager.getLatestDocAndUpdates
-          .calledWith(this.project_id, this.doc_id, this.fromVersion)
-          .should.equal(true)
-      })
-
-      it('should rewind the diff', function () {
-        return sinon.assert.calledOnce(this.rewindUpdatesWithArgs)
-      })
-
-      return it('should call the callback with the rewound document and updates', function () {
-        return this.callback
-          .calledWith(null, this.rewound_content, this.updates)
-          .should.equal(true)
-      })
-    })
-
-    describe('with mismatching versions', function () {
-      beforeEach(function () {
-        this.version = 50
-        this.updates = [
-          { op: 'mock-1', v: 40 },
-          { op: 'mock-1', v: 39 },
-        ]
-        this.DiffManager.getLatestDocAndUpdates = sinon
-          .stub()
-          .callsArgWith(3, null, this.content, this.version, this.updates)
-        return this.DiffManager._tryGetDocumentBeforeVersion(
-          this.project_id,
-          this.doc_id,
-          this.fromVersion,
-          this.callback
-        )
-      })
-
-      return it('should call the callback with an error with retry = true set', function () {
-        this.callback.calledOnce.should.equal(true)
-        const error = this.callback.args[0][0]
-        return expect(error.retry).to.equal(true)
-      })
-    })
-
-    return describe('when the updates are inconsistent', function () {
-      beforeEach(function () {
-        this.DiffManager.getLatestDocAndUpdates = sinon
-          .stub()
-          .callsArgWith(3, null, this.content, this.version, this.updates)
-        this.DiffGenerator.rewindUpdates = sinon
-          .stub()
-          .throws((this.error = new Error('inconsistent!')))
-        return this.DiffManager.getDocumentBeforeVersion(
-          this.project_id,
-          this.doc_id,
-          this.fromVersion,
-          this.callback
-        )
-      })
-
-      return it('should call the callback with an error', function () {
-        return this.callback.calledWith(this.error).should.equal(true)
-      })
-    })
-  })
-})

+ 0 - 110
services/track-changes/test/unit/js/DocArchive/MongoAWS.js

@@ -1,110 +0,0 @@
-/* eslint-disable
-    no-return-assign,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-const sinon = require('sinon')
-const modulePath = '../../../../app/js/MongoAWS.js'
-const SandboxedModule = require('sandboxed-module')
-const { ObjectId } = require('mongodb')
-const MemoryStream = require('memorystream')
-const zlib = require('zlib')
-
-describe('MongoAWS', function () {
-  beforeEach(function () {
-    this.MongoAWS = SandboxedModule.require(modulePath, {
-      singleOnly: true,
-      requires: {
-        '@overleaf/settings': (this.settings = {
-          trackchanges: {
-            s3: {
-              secret: 's3-secret',
-              key: 's3-key',
-            },
-            stores: {
-              doc_history: 's3-bucket',
-            },
-          },
-        }),
-        child_process: (this.child_process = {}),
-        'mongo-uri': (this.mongouri = {}),
-        'aws-sdk': (this.awssdk = {}),
-        fs: (this.fs = {}),
-        's3-streams': (this.S3S = {}),
-        './mongodb': { db: (this.db = {}), ObjectId },
-        JSONStream: (this.JSONStream = {}),
-        'readline-stream': (this.readline = sinon.stub()),
-        '@overleaf/metrics': { inc() {} },
-      },
-    })
-
-    this.project_id = ObjectId().toString()
-    this.doc_id = ObjectId().toString()
-    this.pack_id = ObjectId()
-    this.update = { v: 123 }
-    return (this.callback = sinon.stub())
-  })
-
-  describe('archivePack', function () {
-    beforeEach(function (done) {
-      this.awssdk.config = { update: sinon.stub() }
-      this.awssdk.S3 = sinon.stub()
-      this.S3S.WriteStream = () => MemoryStream.createWriteStream()
-      this.db.docHistory = {}
-      this.db.docHistory.findOne = sinon
-        .stub()
-        .callsArgWith(1, null, { pack: 'hello' })
-
-      return this.MongoAWS.archivePack(
-        this.project_id,
-        this.doc_id,
-        this.pack_id,
-        (err, result) => {
-          if (err) return done(err)
-          this.callback()
-          return done()
-        }
-      )
-    })
-
-    return it('should call the callback', function () {
-      return this.callback.called.should.equal(true)
-    })
-  })
-
-  return describe('unArchivePack', function () {
-    beforeEach(function (done) {
-      return zlib.gzip('{"pack":"123"}', (err, zbuf) => {
-        if (err) return done(err)
-        this.awssdk.config = { update: sinon.stub() }
-        this.awssdk.S3 = sinon.stub()
-        this.S3S.ReadStream = () =>
-          MemoryStream.createReadStream(zbuf, { readable: true })
-        this.db.docHistory = {}
-        this.db.docHistory.insertOne = sinon
-          .stub()
-          .yields(null, { insertedId: ObjectId() })
-
-        return this.MongoAWS.unArchivePack(
-          this.project_id,
-          this.doc_id,
-          this.pack_id,
-          (err, result) => {
-            if (err) return done(err)
-            this.callback()
-            return done()
-          }
-        )
-      })
-    })
-
-    return it('should call db.docHistory.insert', function () {
-      return this.db.docHistory.insertOne.called.should.equal(true)
-    })
-  })
-})

+ 0 - 218
services/track-changes/test/unit/js/DocumentUpdaterManager/DocumentUpdaterManagerTests.js

@@ -1,218 +0,0 @@
-/* eslint-disable
-    no-return-assign,
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-const sinon = require('sinon')
-const { expect } = require('chai')
-const modulePath = '../../../../app/js/DocumentUpdaterManager.js'
-const SandboxedModule = require('sandboxed-module')
-
-describe('DocumentUpdaterManager', function () {
-  beforeEach(function () {
-    this.DocumentUpdaterManager = SandboxedModule.require(modulePath, {
-      requires: {
-        request: (this.request = {}),
-        '@overleaf/settings': (this.settings = {
-          apis: { documentupdater: { url: 'http://example.com' } },
-        }),
-      },
-    })
-    this.callback = sinon.stub()
-    this.lines = ['one', 'two', 'three']
-    return (this.version = 42)
-  })
-
-  describe('getDocument', function () {
-    describe('successfully', function () {
-      beforeEach(function () {
-        this.body = JSON.stringify({
-          lines: this.lines,
-          version: this.version,
-          ops: [],
-        })
-        this.request.get = sinon
-          .stub()
-          .callsArgWith(1, null, { statusCode: 200 }, this.body)
-        return this.DocumentUpdaterManager.getDocument(
-          this.project_id,
-          this.doc_id,
-          this.callback
-        )
-      })
-
-      it('should get the document from the document updater', function () {
-        const url = `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}`
-        return this.request.get.calledWith(url).should.equal(true)
-      })
-
-      return it('should call the callback with the content and version', function () {
-        return this.callback
-          .calledWith(null, this.lines.join('\n'), this.version)
-          .should.equal(true)
-      })
-    })
-
-    describe('when the document updater API returns an error', function () {
-      beforeEach(function () {
-        this.request.get = sinon
-          .stub()
-          .callsArgWith(
-            1,
-            (this.error = new Error('something went wrong')),
-            null,
-            null
-          )
-        return this.DocumentUpdaterManager.getDocument(
-          this.project_id,
-          this.doc_id,
-          this.callback
-        )
-      })
-
-      return it('should return an error to the callback', function () {
-        return this.callback.calledWith(this.error).should.equal(true)
-      })
-    })
-
-    describe('when the document updater returns not found', function () {
-      beforeEach(function () {
-        this.request.get = sinon
-          .stub()
-          .callsArgWith(1, null, { statusCode: 404 }, '')
-        return this.DocumentUpdaterManager.getDocument(
-          this.project_id,
-          this.doc_id,
-          this.callback
-        )
-      })
-
-      it('should return the callback with a "not found" error', function () {
-        return this.callback
-          .calledWith(sinon.match.has('message', 'doc not found'))
-          .should.equal(true)
-      })
-    })
-
-    return describe('when the document updater returns a failure error code', function () {
-      beforeEach(function () {
-        this.request.get = sinon
-          .stub()
-          .callsArgWith(1, null, { statusCode: 500 }, '')
-        return this.DocumentUpdaterManager.getDocument(
-          this.project_id,
-          this.doc_id,
-          this.callback
-        )
-      })
-
-      return it('should return the callback with an error', function () {
-        return this.callback
-          .calledWith(
-            sinon.match.has(
-              'message',
-              'doc updater returned a non-success status code: 500'
-            )
-          )
-          .should.equal(true)
-      })
-    })
-  })
-
-  return describe('setDocument', function () {
-    beforeEach(function () {
-      this.content = 'mock content'
-      return (this.user_id = 'user-id-123')
-    })
-
-    describe('successfully', function () {
-      beforeEach(function () {
-        this.request.post = sinon
-          .stub()
-          .callsArgWith(1, null, { statusCode: 200 })
-        return this.DocumentUpdaterManager.setDocument(
-          this.project_id,
-          this.doc_id,
-          this.content,
-          this.user_id,
-          this.callback
-        )
-      })
-
-      it('should set the document in the document updater', function () {
-        const url = `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}`
-        return this.request.post
-          .calledWith({
-            url,
-            json: {
-              lines: this.content.split('\n'),
-              source: 'restore',
-              user_id: this.user_id,
-              undoing: true,
-            },
-          })
-          .should.equal(true)
-      })
-
-      return it('should call the callback', function () {
-        return this.callback.calledWith(null).should.equal(true)
-      })
-    })
-
-    describe('when the document updater API returns an error', function () {
-      beforeEach(function () {
-        this.request.post = sinon
-          .stub()
-          .callsArgWith(
-            1,
-            (this.error = new Error('something went wrong')),
-            null,
-            null
-          )
-        return this.DocumentUpdaterManager.setDocument(
-          this.project_id,
-          this.doc_id,
-          this.content,
-          this.user_id,
-          this.callback
-        )
-      })
-
-      return it('should return an error to the callback', function () {
-        return this.callback.calledWith(this.error).should.equal(true)
-      })
-    })
-
-    return describe('when the document updater returns a failure error code', function () {
-      beforeEach(function () {
-        this.request.post = sinon
-          .stub()
-          .callsArgWith(1, null, { statusCode: 500 }, '')
-        return this.DocumentUpdaterManager.setDocument(
-          this.project_id,
-          this.doc_id,
-          this.content,
-          this.user_id,
-          this.callback
-        )
-      })
-
-      return it('should return the callback with an error', function () {
-        return this.callback
-          .calledWith(
-            sinon.match.has(
-              'message',
-              'doc updater returned a non-success status code: 500'
-            )
-          )
-          .should.equal(true)
-      })
-    })
-  })
-})

+ 0 - 200
services/track-changes/test/unit/js/HttpController/HttpControllerTests.js

@@ -1,200 +0,0 @@
-/* eslint-disable
-    no-return-assign,
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-const sinon = require('sinon')
-const { expect } = require('chai')
-const modulePath = '../../../../app/js/HttpController.js'
-const SandboxedModule = require('sandboxed-module')
-
-describe('HttpController', function () {
-  beforeEach(function () {
-    this.HttpController = SandboxedModule.require(modulePath, {
-      singleOnly: true,
-      requires: {
-        './UpdatesManager': (this.UpdatesManager = {}),
-        './DiffManager': (this.DiffManager = {}),
-        './RestoreManager': (this.RestoreManager = {}),
-        './PackManager': (this.PackManager = {}),
-        './DocArchiveManager': (this.DocArchiveManager = {}),
-        './HealthChecker': (this.HealthChecker = {}),
-        './ZipManager': (this.ZipManager = {}),
-      },
-    })
-    this.doc_id = 'doc-id-123'
-    this.project_id = 'project-id-123'
-    this.next = sinon.stub()
-    this.user_id = 'mock-user-123'
-    return (this.now = Date.now())
-  })
-
-  describe('flushDoc', function () {
-    beforeEach(function () {
-      this.req = {
-        params: {
-          doc_id: this.doc_id,
-          project_id: this.project_id,
-        },
-      }
-      this.res = { sendStatus: sinon.stub() }
-      this.UpdatesManager.processUncompressedUpdatesWithLock = sinon
-        .stub()
-        .callsArg(2)
-      return this.HttpController.flushDoc(this.req, this.res, this.next)
-    })
-
-    it('should process the updates', function () {
-      return this.UpdatesManager.processUncompressedUpdatesWithLock
-        .calledWith(this.project_id, this.doc_id)
-        .should.equal(true)
-    })
-
-    return it('should return a success code', function () {
-      return this.res.sendStatus.calledWith(204).should.equal(true)
-    })
-  })
-
-  describe('flushProject', function () {
-    beforeEach(function () {
-      this.req = {
-        params: {
-          project_id: this.project_id,
-        },
-      }
-      this.res = { sendStatus: sinon.stub() }
-      this.UpdatesManager.processUncompressedUpdatesForProject = sinon
-        .stub()
-        .callsArg(1)
-      return this.HttpController.flushProject(this.req, this.res, this.next)
-    })
-
-    it('should process the updates', function () {
-      return this.UpdatesManager.processUncompressedUpdatesForProject
-        .calledWith(this.project_id)
-        .should.equal(true)
-    })
-
-    return it('should return a success code', function () {
-      return this.res.sendStatus.calledWith(204).should.equal(true)
-    })
-  })
-
-  describe('getDiff', function () {
-    beforeEach(function () {
-      this.from = 42
-      this.to = 45
-      this.req = {
-        params: {
-          doc_id: this.doc_id,
-          project_id: this.project_id,
-        },
-        query: {
-          from: this.from.toString(),
-          to: this.to.toString(),
-        },
-      }
-      this.res = { json: sinon.stub() }
-      this.diff = [{ u: 'mock-diff' }]
-      this.DiffManager.getDiff = sinon.stub().callsArgWith(4, null, this.diff)
-      return this.HttpController.getDiff(this.req, this.res, this.next)
-    })
-
-    it('should get the diff', function () {
-      return this.DiffManager.getDiff
-        .calledWith(
-          this.project_id,
-          this.doc_id,
-          parseInt(this.from, 10),
-          parseInt(this.to, 10)
-        )
-        .should.equal(true)
-    })
-
-    return it('should return the diff', function () {
-      return this.res.json.calledWith({ diff: this.diff }).should.equal(true)
-    })
-  })
-
-  describe('getUpdates', function () {
-    beforeEach(function () {
-      this.before = Date.now()
-      this.nextBeforeTimestamp = this.before - 100
-      this.min_count = 10
-      this.req = {
-        params: {
-          project_id: this.project_id,
-        },
-        query: {
-          before: this.before.toString(),
-          min_count: this.min_count.toString(),
-        },
-      }
-      this.res = { json: sinon.stub() }
-      this.updates = ['mock-summarized-updates']
-      this.UpdatesManager.getSummarizedProjectUpdates = sinon
-        .stub()
-        .callsArgWith(2, null, this.updates, this.nextBeforeTimestamp)
-      return this.HttpController.getUpdates(this.req, this.res, this.next)
-    })
-
-    it('should get the updates', function () {
-      return this.UpdatesManager.getSummarizedProjectUpdates
-        .calledWith(this.project_id, {
-          before: this.before,
-          min_count: this.min_count,
-        })
-        .should.equal(true)
-    })
-
-    return it('should return the formatted updates', function () {
-      return this.res.json
-        .calledWith({
-          updates: this.updates,
-          nextBeforeTimestamp: this.nextBeforeTimestamp,
-        })
-        .should.equal(true)
-    })
-  })
-
-  return describe('RestoreManager', function () {
-    beforeEach(function () {
-      this.version = '42'
-      this.req = {
-        params: {
-          doc_id: this.doc_id,
-          project_id: this.project_id,
-          version: this.version,
-        },
-        headers: {
-          'x-user-id': this.user_id,
-        },
-      }
-      this.res = { sendStatus: sinon.stub() }
-
-      this.RestoreManager.restoreToBeforeVersion = sinon.stub().callsArg(4)
-      return this.HttpController.restore(this.req, this.res, this.next)
-    })
-
-    it('should restore the document', function () {
-      return this.RestoreManager.restoreToBeforeVersion
-        .calledWith(
-          this.project_id,
-          this.doc_id,
-          parseInt(this.version, 10),
-          this.user_id
-        )
-        .should.equal(true)
-    })
-
-    return it('should return a success code', function () {
-      return this.res.sendStatus.calledWith(204).should.equal(true)
-    })
-  })
-})

+ 0 - 315
services/track-changes/test/unit/js/LockManager/LockManagerTests.js

@@ -1,315 +0,0 @@
-/* eslint-disable
-    mocha/no-nested-tests,
-    no-return-assign,
-    no-undef,
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS101: Remove unnecessary use of Array.from
- * DS102: Remove unnecessary code created because of implicit returns
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-const sinon = require('sinon')
-const { expect } = require('chai')
-const modulePath = '../../../../app/js/LockManager.js'
-const SandboxedModule = require('sandboxed-module')
-
-describe('LockManager', function () {
-  beforeEach(function () {
-    this.Settings = {
-      redis: {
-        lock: {},
-      },
-    }
-    this.LockManager = SandboxedModule.require(modulePath, {
-      requires: {
-        '@overleaf/redis-wrapper': {
-          createClient: () => {
-            return (this.rclient = { auth: sinon.stub() })
-          },
-        },
-        '@overleaf/settings': this.Settings,
-      },
-    })
-
-    this.key = 'lock-key'
-    return (this.callback = sinon.stub())
-  })
-
-  describe('checkLock', function () {
-    describe('when the lock is taken', function () {
-      beforeEach(function () {
-        this.rclient.exists = sinon.stub().callsArgWith(1, null, '1')
-        return this.LockManager.checkLock(this.key, this.callback)
-      })
-
-      it('should check the lock in redis', function () {
-        return this.rclient.exists.calledWith(this.key).should.equal(true)
-      })
-
-      return it('should return the callback with false', function () {
-        return this.callback.calledWith(null, false).should.equal(true)
-      })
-    })
-
-    return describe('when the lock is free', function () {
-      beforeEach(function () {
-        this.rclient.exists = sinon.stub().callsArgWith(1, null, '0')
-        return this.LockManager.checkLock(this.key, this.callback)
-      })
-
-      return it('should return the callback with true', function () {
-        return this.callback.calledWith(null, true).should.equal(true)
-      })
-    })
-  })
-
-  describe('tryLock', function () {
-    describe('when the lock is taken', function () {
-      beforeEach(function () {
-        this.rclient.set = sinon.stub().callsArgWith(5, null, null)
-        this.LockManager.randomLock = sinon
-          .stub()
-          .returns('locked-random-value')
-        return this.LockManager.tryLock(this.key, this.callback)
-      })
-
-      it('should check the lock in redis', function () {
-        return this.rclient.set
-          .calledWith(
-            this.key,
-            'locked-random-value',
-            'EX',
-            this.LockManager.LOCK_TTL,
-            'NX'
-          )
-          .should.equal(true)
-      })
-
-      return it('should return the callback with false', function () {
-        return this.callback.calledWith(null, false).should.equal(true)
-      })
-    })
-
-    return describe('when the lock is free', function () {
-      beforeEach(function () {
-        this.rclient.set = sinon.stub().callsArgWith(5, null, 'OK')
-        return this.LockManager.tryLock(this.key, this.callback)
-      })
-
-      return it('should return the callback with true', function () {
-        return this.callback.calledWith(null, true).should.equal(true)
-      })
-    })
-  })
-
-  describe('deleteLock', function () {
-    return beforeEach(function () {
-      beforeEach(function () {
-        this.rclient.del = sinon.stub().callsArg(1)
-        return this.LockManager.deleteLock(this.key, this.callback)
-      })
-
-      it('should delete the lock in redis', function () {
-        return this.rclient.del.calledWith(key).should.equal(true)
-      })
-
-      return it('should call the callback', function () {
-        return this.callback.called.should.equal(true)
-      })
-    })
-  })
-
-  describe('getLock', function () {
-    describe('when the lock is not taken', function () {
-      beforeEach(function (done) {
-        this.LockManager.tryLock = sinon.stub().callsArgWith(1, null, true)
-        return this.LockManager.getLock(this.key, (...args) => {
-          this.callback(...Array.from(args || []))
-          return done()
-        })
-      })
-
-      it('should try to get the lock', function () {
-        return this.LockManager.tryLock.calledWith(this.key).should.equal(true)
-      })
-
-      it('should only need to try once', function () {
-        return this.LockManager.tryLock.callCount.should.equal(1)
-      })
-
-      return it('should return the callback', function () {
-        return this.callback.calledWith(null).should.equal(true)
-      })
-    })
-
-    describe('when the lock is initially set', function () {
-      beforeEach(function (done) {
-        const startTime = Date.now()
-        this.LockManager.LOCK_TEST_INTERVAL = 5
-        this.LockManager.tryLock = function (docId, callback) {
-          if (callback == null) {
-            callback = function () {}
-          }
-          if (Date.now() - startTime < 100) {
-            return callback(null, false)
-          } else {
-            return callback(null, true)
-          }
-        }
-        sinon.spy(this.LockManager, 'tryLock')
-
-        return this.LockManager.getLock(this.key, (...args) => {
-          this.callback(...Array.from(args || []))
-          return done()
-        })
-      })
-
-      it('should call tryLock multiple times until free', function () {
-        return (this.LockManager.tryLock.callCount > 1).should.equal(true)
-      })
-
-      return it('should return the callback', function () {
-        return this.callback.calledWith(null).should.equal(true)
-      })
-    })
-
-    return describe('when the lock times out', function () {
-      beforeEach(function (done) {
-        const time = Date.now()
-        this.LockManager.MAX_LOCK_WAIT_TIME = 5
-        this.LockManager.tryLock = sinon.stub().callsArgWith(1, null, false)
-        return this.LockManager.getLock(this.key, (...args) => {
-          this.callback(...Array.from(args || []))
-          return done()
-        })
-      })
-
-      return it('should return the callback with an error', function () {
-        return this.callback
-          .calledWith(sinon.match.instanceOf(Error))
-          .should.equal(true)
-      })
-    })
-  })
-
-  return describe('runWithLock', function () {
-    describe('with successful run', function () {
-      beforeEach(function () {
-        this.runner = function (releaseLock) {
-          if (releaseLock == null) {
-            releaseLock = function () {}
-          }
-          return releaseLock()
-        }
-        sinon.spy(this, 'runner')
-        this.LockManager.getLock = sinon.stub().callsArg(1)
-        this.LockManager.releaseLock = sinon.stub().callsArg(2)
-        return this.LockManager.runWithLock(
-          this.key,
-          this.runner,
-          this.callback
-        )
-      })
-
-      it('should get the lock', function () {
-        return this.LockManager.getLock.calledWith(this.key).should.equal(true)
-      })
-
-      it('should run the passed function', function () {
-        return this.runner.called.should.equal(true)
-      })
-
-      it('should release the lock', function () {
-        return this.LockManager.releaseLock
-          .calledWith(this.key)
-          .should.equal(true)
-      })
-
-      return it('should call the callback', function () {
-        return this.callback.called.should.equal(true)
-      })
-    })
-
-    describe('when the runner function returns an error', function () {
-      beforeEach(function () {
-        this.error = new Error('oops')
-        this.runner = releaseLock => {
-          if (releaseLock == null) {
-            releaseLock = function () {}
-          }
-          return releaseLock(this.error)
-        }
-        sinon.spy(this, 'runner')
-        this.LockManager.getLock = sinon.stub().callsArg(1)
-        this.LockManager.releaseLock = sinon.stub().callsArg(2)
-        return this.LockManager.runWithLock(
-          this.key,
-          this.runner,
-          this.callback
-        )
-      })
-
-      it('should release the lock', function () {
-        return this.LockManager.releaseLock
-          .calledWith(this.key)
-          .should.equal(true)
-      })
-
-      return it('should call the callback with the error', function () {
-        return this.callback.calledWith(this.error).should.equal(true)
-      })
-    })
-
-    return describe('releaseLock', function () {
-      describe('when the lock is current', function () {
-        beforeEach(function () {
-          this.rclient.eval = sinon.stub().yields(null, 1)
-          return this.LockManager.releaseLock(
-            this.key,
-            this.lockValue,
-            this.callback
-          )
-        })
-
-        it('should clear the data from redis', function () {
-          return this.rclient.eval
-            .calledWith(
-              this.LockManager.unlockScript,
-              1,
-              this.key,
-              this.lockValue
-            )
-            .should.equal(true)
-        })
-
-        return it('should call the callback', function () {
-          return this.callback.called.should.equal(true)
-        })
-      })
-
-      return describe('when the lock has expired', function () {
-        beforeEach(function () {
-          this.rclient.eval = sinon.stub().yields(null, 0)
-          return this.LockManager.releaseLock(
-            this.key,
-            this.lockValue,
-            this.callback
-          )
-        })
-
-        return it('should return an error if the lock has expired', function () {
-          return this.callback
-            .calledWith(
-              sinon.match.has('message', 'tried to release timed out lock')
-            )
-            .should.equal(true)
-        })
-      })
-    })
-  })
-})

+ 0 - 239
services/track-changes/test/unit/js/MongoManager/MongoManagerTests.js

@@ -1,239 +0,0 @@
-/* eslint-disable
-    no-return-assign,
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-const sinon = require('sinon')
-const { expect } = require('chai')
-const modulePath = '../../../../app/js/MongoManager.js'
-const packModulePath = '../../../../app/js/PackManager.js'
-const SandboxedModule = require('sandboxed-module')
-const { ObjectId } = require('mongodb')
-const tk = require('timekeeper')
-
-describe('MongoManager', function () {
-  beforeEach(function () {
-    tk.freeze(new Date())
-    this.MongoManager = SandboxedModule.require(modulePath, {
-      requires: {
-        './mongodb': { db: (this.db = {}), ObjectId },
-        './PackManager': (this.PackManager = {}),
-        '@overleaf/metrics': { timeAsyncMethod() {} },
-      },
-    })
-    this.callback = sinon.stub()
-    this.doc_id = ObjectId().toString()
-    return (this.project_id = ObjectId().toString())
-  })
-
-  afterEach(function () {
-    return tk.reset()
-  })
-
-  describe('getLastCompressedUpdate', function () {
-    beforeEach(function () {
-      this.update = 'mock-update'
-      this.db.docHistory = {}
-      this.db.docHistory.find = sinon.stub().returns(this.db.docHistory)
-      this.db.docHistory.findOne = sinon.stub().returns(this.db.docHistory)
-      this.db.docHistory.sort = sinon.stub().returns(this.db.docHistory)
-      this.db.docHistory.limit = sinon.stub().returns(this.db.docHistory)
-      this.db.docHistory.toArray = sinon
-        .stub()
-        .callsArgWith(0, null, [this.update])
-
-      return this.MongoManager.getLastCompressedUpdate(
-        this.doc_id,
-        this.callback
-      )
-    })
-
-    it('should find the updates for the doc', function () {
-      return this.db.docHistory.find
-        .calledWith({ doc_id: ObjectId(this.doc_id) })
-        .should.equal(true)
-    })
-
-    it('should limit to one result', function () {
-      return this.db.docHistory.limit.calledWith(1).should.equal(true)
-    })
-
-    it('should sort in descending version order', function () {
-      return this.db.docHistory.sort.calledWith({ v: -1 }).should.equal(true)
-    })
-
-    return it('should call the call back with the update', function () {
-      return this.callback.calledWith(null, this.update).should.equal(true)
-    })
-  })
-
-  describe('peekLastCompressedUpdate', function () {
-    describe('when there is no last update', function () {
-      beforeEach(function () {
-        this.PackManager.getLastPackFromIndex = sinon
-          .stub()
-          .callsArgWith(1, null, null)
-        this.MongoManager.getLastCompressedUpdate = sinon
-          .stub()
-          .callsArgWith(1, null, null)
-        return this.MongoManager.peekLastCompressedUpdate(
-          this.doc_id,
-          this.callback
-        )
-      })
-
-      it('should get the last update', function () {
-        return this.MongoManager.getLastCompressedUpdate
-          .calledWith(this.doc_id)
-          .should.equal(true)
-      })
-
-      return it('should call the callback with no update', function () {
-        return this.callback.calledWith(null, null).should.equal(true)
-      })
-    })
-
-    describe('when there is an update', function () {
-      beforeEach(function () {
-        this.update = { _id: Object() }
-        this.MongoManager.getLastCompressedUpdate = sinon
-          .stub()
-          .callsArgWith(1, null, this.update)
-        return this.MongoManager.peekLastCompressedUpdate(
-          this.doc_id,
-          this.callback
-        )
-      })
-
-      it('should get the last update', function () {
-        return this.MongoManager.getLastCompressedUpdate
-          .calledWith(this.doc_id)
-          .should.equal(true)
-      })
-
-      return it('should call the callback with the update', function () {
-        return this.callback.calledWith(null, this.update).should.equal(true)
-      })
-    })
-
-    return describe('when there is a last update in S3', function () {
-      beforeEach(function () {
-        this.update = { _id: Object(), v: 12345, v_end: 12345, inS3: true }
-        this.PackManager.getLastPackFromIndex = sinon
-          .stub()
-          .callsArgWith(1, null, this.update)
-        this.MongoManager.getLastCompressedUpdate = sinon
-          .stub()
-          .callsArgWith(1, null)
-        return this.MongoManager.peekLastCompressedUpdate(
-          this.doc_id,
-          this.callback
-        )
-      })
-
-      it('should get the last update', function () {
-        return this.MongoManager.getLastCompressedUpdate
-          .calledWith(this.doc_id)
-          .should.equal(true)
-      })
-
-      return it('should call the callback with a null update and the correct version', function () {
-        return this.callback
-          .calledWith(null, null, this.update.v_end)
-          .should.equal(true)
-      })
-    })
-  })
-
-  describe('backportProjectId', function () {
-    beforeEach(function () {
-      this.db.docHistory = { updateMany: sinon.stub().yields() }
-      return this.MongoManager.backportProjectId(
-        this.project_id,
-        this.doc_id,
-        this.callback
-      )
-    })
-
-    it("should insert the project_id into all entries for the doc_id which don't have it set", function () {
-      return this.db.docHistory.updateMany
-        .calledWith(
-          {
-            doc_id: ObjectId(this.doc_id),
-            project_id: { $exists: false },
-          },
-          {
-            $set: { project_id: ObjectId(this.project_id) },
-          }
-        )
-        .should.equal(true)
-    })
-
-    return it('should call the callback', function () {
-      return this.callback.called.should.equal(true)
-    })
-  })
-
-  describe('getProjectMetaData', function () {
-    beforeEach(function () {
-      this.metadata = { mock: 'metadata' }
-      this.db.projectHistoryMetaData = {
-        findOne: sinon.stub().callsArgWith(1, null, this.metadata),
-      }
-      return this.MongoManager.getProjectMetaData(
-        this.project_id,
-        this.callback
-      )
-    })
-
-    it('should look up the meta data in the db', function () {
-      return this.db.projectHistoryMetaData.findOne
-        .calledWith({ project_id: ObjectId(this.project_id) })
-        .should.equal(true)
-    })
-
-    return it('should return the metadata', function () {
-      return this.callback.calledWith(null, this.metadata).should.equal(true)
-    })
-  })
-
-  return describe('setProjectMetaData', function () {
-    beforeEach(function () {
-      this.metadata = { mock: 'metadata' }
-      this.db.projectHistoryMetaData = {
-        updateOne: sinon.stub().yields(),
-      }
-      return this.MongoManager.setProjectMetaData(
-        this.project_id,
-        this.metadata,
-        this.callback
-      )
-    })
-
-    it('should upsert the metadata into the DB', function () {
-      return this.db.projectHistoryMetaData.updateOne
-        .calledWith(
-          {
-            project_id: ObjectId(this.project_id),
-          },
-          {
-            $set: this.metadata,
-          },
-          {
-            upsert: true,
-          }
-        )
-        .should.equal(true)
-    })
-
-    return it('should call the callback', function () {
-      return this.callback.called.should.equal(true)
-    })
-  })
-})

+ 0 - 704
services/track-changes/test/unit/js/PackManager/PackManagerTests.js

@@ -1,704 +0,0 @@
-/* eslint-disable
-    mocha/no-identical-title,
-    no-return-assign,
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-const sinon = require('sinon')
-const { assert, expect } = require('chai')
-const modulePath = '../../../../app/js/PackManager.js'
-const SandboxedModule = require('sandboxed-module')
-const { ObjectId } = require('mongodb')
-const _ = require('underscore')
-
-const tk = require('timekeeper')
-
-describe('PackManager', function () {
-  beforeEach(function () {
-    tk.freeze(new Date())
-    this.PackManager = SandboxedModule.require(modulePath, {
-      requires: {
-        bson: require('bson'),
-        './mongodb': { db: (this.db = {}), ObjectId },
-        './LockManager': {},
-        './MongoAWS': {},
-        '@overleaf/metrics': { inc() {} },
-        './ProjectIterator': require('../../../../app/js/ProjectIterator.js'), // Cache for speed
-        '@overleaf/settings': {
-          redis: { lock: { key_schema: {} } },
-        },
-      },
-    })
-    this.callback = sinon.stub()
-    this.doc_id = ObjectId().toString()
-    this.project_id = ObjectId().toString()
-    return (this.PackManager.MAX_COUNT = 512)
-  })
-
-  afterEach(function () {
-    return tk.reset()
-  })
-
-  describe('insertCompressedUpdates', function () {
-    beforeEach(function () {
-      this.lastUpdate = {
-        _id: '12345',
-        pack: [
-          { op: 'op-1', meta: 'meta-1', v: 1 },
-          { op: 'op-2', meta: 'meta-2', v: 2 },
-        ],
-        n: 2,
-        sz: 100,
-      }
-      this.newUpdates = [
-        { op: 'op-3', meta: 'meta-3', v: 3 },
-        { op: 'op-4', meta: 'meta-4', v: 4 },
-      ]
-      return (this.db.docHistory = {
-        insertOne: sinon.stub().yields(),
-        insert: sinon.stub().callsArg(1),
-        updateOne: sinon.stub().yields(),
-        findAndModify: sinon.stub().callsArg(1),
-      })
-    })
-
-    describe('with no last update', function () {
-      beforeEach(function () {
-        this.PackManager.insertUpdatesIntoNewPack = sinon.stub().callsArg(4)
-        return this.PackManager.insertCompressedUpdates(
-          this.project_id,
-          this.doc_id,
-          null,
-          this.newUpdates,
-          true,
-          this.callback
-        )
-      })
-
-      describe('for a small update', function () {
-        it('should insert the update into a new pack', function () {
-          return this.PackManager.insertUpdatesIntoNewPack
-            .calledWith(this.project_id, this.doc_id, this.newUpdates, true)
-            .should.equal(true)
-        })
-
-        return it('should call the callback', function () {
-          return this.callback.called.should.equal(true)
-        })
-      })
-
-      return describe('for many small updates', function () {
-        beforeEach(function () {
-          this.newUpdates = __range__(0, 2048, true).map(i => ({
-            op: `op-${i}`,
-            meta: `meta-${i}`,
-            v: i,
-          }))
-          return this.PackManager.insertCompressedUpdates(
-            this.project_id,
-            this.doc_id,
-            null,
-            this.newUpdates,
-            false,
-            this.callback
-          )
-        })
-
-        it('should append the initial updates to the existing pack', function () {
-          return this.PackManager.insertUpdatesIntoNewPack
-            .calledWith(
-              this.project_id,
-              this.doc_id,
-              this.newUpdates.slice(0, 512),
-              false
-            )
-            .should.equal(true)
-        })
-
-        it('should insert the first set remaining updates as a new pack', function () {
-          return this.PackManager.insertUpdatesIntoNewPack
-            .calledWith(
-              this.project_id,
-              this.doc_id,
-              this.newUpdates.slice(512, 1024),
-              false
-            )
-            .should.equal(true)
-        })
-
-        it('should insert the second set of remaining updates as a new pack', function () {
-          return this.PackManager.insertUpdatesIntoNewPack
-            .calledWith(
-              this.project_id,
-              this.doc_id,
-              this.newUpdates.slice(1024, 1536),
-              false
-            )
-            .should.equal(true)
-        })
-
-        it('should insert the third set of remaining updates as a new pack', function () {
-          return this.PackManager.insertUpdatesIntoNewPack
-            .calledWith(
-              this.project_id,
-              this.doc_id,
-              this.newUpdates.slice(1536, 2048),
-              false
-            )
-            .should.equal(true)
-        })
-
-        it('should insert the final set of remaining updates as a new pack', function () {
-          return this.PackManager.insertUpdatesIntoNewPack
-            .calledWith(
-              this.project_id,
-              this.doc_id,
-              this.newUpdates.slice(2048, 2049),
-              false
-            )
-            .should.equal(true)
-        })
-
-        return it('should call the callback', function () {
-          return this.callback.called.should.equal(true)
-        })
-      })
-    })
-
-    describe('with an existing pack as the last update', function () {
-      beforeEach(function () {
-        this.PackManager.appendUpdatesToExistingPack = sinon.stub().callsArg(5)
-        this.PackManager.insertUpdatesIntoNewPack = sinon.stub().callsArg(4)
-        return this.PackManager.insertCompressedUpdates(
-          this.project_id,
-          this.doc_id,
-          this.lastUpdate,
-          this.newUpdates,
-          false,
-          this.callback
-        )
-      })
-
-      describe('for a small update', function () {
-        it('should append the update to the existing pack', function () {
-          return this.PackManager.appendUpdatesToExistingPack
-            .calledWith(
-              this.project_id,
-              this.doc_id,
-              this.lastUpdate,
-              this.newUpdates,
-              false
-            )
-            .should.equal(true)
-        })
-        it('should not insert any new packs', function () {
-          return this.PackManager.insertUpdatesIntoNewPack.called.should.equal(
-            false
-          )
-        })
-        return it('should call the callback', function () {
-          return this.callback.called.should.equal(true)
-        })
-      })
-
-      describe('for many small updates', function () {
-        beforeEach(function () {
-          this.newUpdates = __range__(0, 2048, true).map(i => ({
-            op: `op-${i}`,
-            meta: `meta-${i}`,
-            v: i,
-          }))
-          return this.PackManager.insertCompressedUpdates(
-            this.project_id,
-            this.doc_id,
-            this.lastUpdate,
-            this.newUpdates,
-            false,
-            this.callback
-          )
-        })
-
-        it('should append the initial updates to the existing pack', function () {
-          return this.PackManager.appendUpdatesToExistingPack
-            .calledWith(
-              this.project_id,
-              this.doc_id,
-              this.lastUpdate,
-              this.newUpdates.slice(0, 510),
-              false
-            )
-            .should.equal(true)
-        })
-
-        it('should insert the first set remaining updates as a new pack', function () {
-          return this.PackManager.insertUpdatesIntoNewPack
-            .calledWith(
-              this.project_id,
-              this.doc_id,
-              this.newUpdates.slice(510, 1022),
-              false
-            )
-            .should.equal(true)
-        })
-
-        it('should insert the second set of remaining updates as a new pack', function () {
-          return this.PackManager.insertUpdatesIntoNewPack
-            .calledWith(
-              this.project_id,
-              this.doc_id,
-              this.newUpdates.slice(1022, 1534),
-              false
-            )
-            .should.equal(true)
-        })
-
-        it('should insert the third set of remaining updates as a new pack', function () {
-          return this.PackManager.insertUpdatesIntoNewPack
-            .calledWith(
-              this.project_id,
-              this.doc_id,
-              this.newUpdates.slice(1534, 2046),
-              false
-            )
-            .should.equal(true)
-        })
-
-        it('should insert the final set of remaining updates as a new pack', function () {
-          return this.PackManager.insertUpdatesIntoNewPack
-            .calledWith(
-              this.project_id,
-              this.doc_id,
-              this.newUpdates.slice(2046, 2049),
-              false
-            )
-            .should.equal(true)
-        })
-
-        return it('should call the callback', function () {
-          return this.callback.called.should.equal(true)
-        })
-      })
-
-      return describe('for many big updates', function () {
-        beforeEach(function () {
-          const longString = __range__(
-            0,
-            0.75 * this.PackManager.MAX_SIZE,
-            true
-          )
-            .map(j => 'a')
-            .join('')
-          this.newUpdates = [0, 1, 2, 3, 4].map(i => ({
-            op: `op-${i}-${longString}`,
-            meta: `meta-${i}`,
-            v: i,
-          }))
-          return this.PackManager.insertCompressedUpdates(
-            this.project_id,
-            this.doc_id,
-            this.lastUpdate,
-            this.newUpdates,
-            false,
-            this.callback
-          )
-        })
-
-        it('should append the initial updates to the existing pack', function () {
-          return this.PackManager.appendUpdatesToExistingPack
-            .calledWith(
-              this.project_id,
-              this.doc_id,
-              this.lastUpdate,
-              this.newUpdates.slice(0, 1),
-              false
-            )
-            .should.equal(true)
-        })
-
-        it('should insert the first set remaining updates as a new pack', function () {
-          return this.PackManager.insertUpdatesIntoNewPack
-            .calledWith(
-              this.project_id,
-              this.doc_id,
-              this.newUpdates.slice(1, 2),
-              false
-            )
-            .should.equal(true)
-        })
-
-        it('should insert the second set of remaining updates as a new pack', function () {
-          return this.PackManager.insertUpdatesIntoNewPack
-            .calledWith(
-              this.project_id,
-              this.doc_id,
-              this.newUpdates.slice(2, 3),
-              false
-            )
-            .should.equal(true)
-        })
-
-        it('should insert the third set of remaining updates as a new pack', function () {
-          return this.PackManager.insertUpdatesIntoNewPack
-            .calledWith(
-              this.project_id,
-              this.doc_id,
-              this.newUpdates.slice(3, 4),
-              false
-            )
-            .should.equal(true)
-        })
-
-        it('should insert the final set of remaining updates as a new pack', function () {
-          return this.PackManager.insertUpdatesIntoNewPack
-            .calledWith(
-              this.project_id,
-              this.doc_id,
-              this.newUpdates.slice(4, 5),
-              false
-            )
-            .should.equal(true)
-        })
-
-        return it('should call the callback', function () {
-          return this.callback.called.should.equal(true)
-        })
-      })
-    })
-
-    describe('flushCompressedUpdates', function () {
-      return describe('when there is no previous update', function () {
-        beforeEach(function () {
-          return this.PackManager.flushCompressedUpdates(
-            this.project_id,
-            this.doc_id,
-            null,
-            this.newUpdates,
-            true,
-            this.callback
-          )
-        })
-
-        return describe('for a small update  that will expire', function () {
-          it('should insert the update into mongo', function () {
-            return this.db.docHistory.insertOne
-              .calledWithMatch({
-                pack: this.newUpdates,
-                project_id: ObjectId(this.project_id),
-                doc_id: ObjectId(this.doc_id),
-                n: this.newUpdates.length,
-                v: this.newUpdates[0].v,
-                v_end: this.newUpdates[this.newUpdates.length - 1].v,
-              })
-              .should.equal(true)
-          })
-
-          it('should set an expiry time in the future', function () {
-            return this.db.docHistory.insertOne
-              .calledWithMatch({
-                expiresAt: new Date(Date.now() + 7 * 24 * 3600 * 1000),
-              })
-              .should.equal(true)
-          })
-
-          return it('should call the callback', function () {
-            return this.callback.called.should.equal(true)
-          })
-        })
-      })
-    })
-
-    describe('when there is a recent previous update in mongo that expires', function () {
-      beforeEach(function () {
-        this.lastUpdate = {
-          _id: '12345',
-          pack: [
-            { op: 'op-1', meta: 'meta-1', v: 1 },
-            { op: 'op-2', meta: 'meta-2', v: 2 },
-          ],
-          n: 2,
-          sz: 100,
-          meta: { start_ts: Date.now() - 6 * 3600 * 1000 },
-          expiresAt: new Date(Date.now()),
-        }
-
-        return this.PackManager.flushCompressedUpdates(
-          this.project_id,
-          this.doc_id,
-          this.lastUpdate,
-          this.newUpdates,
-          true,
-          this.callback
-        )
-      })
-
-      return describe('for a small update that will expire', function () {
-        it('should append the update in mongo', function () {
-          return this.db.docHistory.updateOne
-            .calledWithMatch(
-              { _id: this.lastUpdate._id },
-              {
-                $push: { pack: { $each: this.newUpdates } },
-                $set: { v_end: this.newUpdates[this.newUpdates.length - 1].v },
-              }
-            )
-            .should.equal(true)
-        })
-
-        it('should set an expiry time in the future', function () {
-          return this.db.docHistory.updateOne
-            .calledWithMatch(sinon.match.any, {
-              $set: { expiresAt: new Date(Date.now() + 7 * 24 * 3600 * 1000) },
-            })
-            .should.equal(true)
-        })
-
-        return it('should call the callback', function () {
-          return this.callback.called.should.equal(true)
-        })
-      })
-    })
-
-    describe('when there is a recent previous update in mongo that expires', function () {
-      beforeEach(function () {
-        this.PackManager.updateIndex = sinon.stub().callsArg(2)
-
-        this.lastUpdate = {
-          _id: '12345',
-          pack: [
-            { op: 'op-1', meta: 'meta-1', v: 1 },
-            { op: 'op-2', meta: 'meta-2', v: 2 },
-          ],
-          n: 2,
-          sz: 100,
-          meta: { start_ts: Date.now() - 6 * 3600 * 1000 },
-          expiresAt: new Date(Date.now()),
-        }
-
-        return this.PackManager.flushCompressedUpdates(
-          this.project_id,
-          this.doc_id,
-          this.lastUpdate,
-          this.newUpdates,
-          false,
-          this.callback
-        )
-      })
-
-      return describe('for a small update that will not expire', function () {
-        it('should insert the update into mongo', function () {
-          return this.db.docHistory.insertOne
-            .calledWithMatch({
-              pack: this.newUpdates,
-              project_id: ObjectId(this.project_id),
-              doc_id: ObjectId(this.doc_id),
-              n: this.newUpdates.length,
-              v: this.newUpdates[0].v,
-              v_end: this.newUpdates[this.newUpdates.length - 1].v,
-            })
-            .should.equal(true)
-        })
-
-        it('should not set any expiry time', function () {
-          return this.db.docHistory.insertOne
-            .neverCalledWithMatch(sinon.match.has('expiresAt'))
-            .should.equal(true)
-        })
-
-        return it('should call the callback', function () {
-          return this.callback.called.should.equal(true)
-        })
-      })
-    })
-
-    return describe('when there is an old previous update in mongo', function () {
-      beforeEach(function () {
-        this.lastUpdate = {
-          _id: '12345',
-          pack: [
-            { op: 'op-1', meta: 'meta-1', v: 1 },
-            { op: 'op-2', meta: 'meta-2', v: 2 },
-          ],
-          n: 2,
-          sz: 100,
-          meta: { start_ts: Date.now() - 30 * 24 * 3600 * 1000 },
-          expiresAt: new Date(Date.now() - 30 * 24 * 3600 * 1000),
-        }
-
-        return this.PackManager.flushCompressedUpdates(
-          this.project_id,
-          this.doc_id,
-          this.lastUpdate,
-          this.newUpdates,
-          true,
-          this.callback
-        )
-      })
-
-      return describe('for a small update that will expire', function () {
-        it('should insert the update into mongo', function () {
-          return this.db.docHistory.insertOne
-            .calledWithMatch({
-              pack: this.newUpdates,
-              project_id: ObjectId(this.project_id),
-              doc_id: ObjectId(this.doc_id),
-              n: this.newUpdates.length,
-              v: this.newUpdates[0].v,
-              v_end: this.newUpdates[this.newUpdates.length - 1].v,
-            })
-            .should.equal(true)
-        })
-
-        it('should set an expiry time in the future', function () {
-          return this.db.docHistory.insertOne
-            .calledWithMatch({
-              expiresAt: new Date(Date.now() + 7 * 24 * 3600 * 1000),
-            })
-            .should.equal(true)
-        })
-
-        return it('should call the callback', function () {
-          return this.callback.called.should.equal(true)
-        })
-      })
-    })
-  })
-
-  describe('getOpsByVersionRange', function () {})
-
-  describe('loadPacksByVersionRange', function () {})
-
-  describe('fetchPacksIfNeeded', function () {})
-
-  describe('makeProjectIterator', function () {})
-
-  describe('getPackById', function () {})
-
-  describe('increaseTTL', function () {})
-
-  describe('getIndex', function () {})
-
-  describe('getPackFromIndex', function () {})
-  // getLastPackFromIndex:
-  // getIndexWithKeys
-  // initialiseIndex
-  // updateIndex
-  // findCompletedPacks
-  // findUnindexedPacks
-  // insertPacksIntoIndexWithLock
-  // _insertPacksIntoIndex
-  // archivePack
-  // checkArchivedPack
-  // processOldPack
-  // 	updateIndexIfNeeded
-  // 	findUnarchivedPacks
-
-  return describe('checkArchiveNotInProgress', function () {
-    describe('when an archive is in progress', function () {
-      beforeEach(function () {
-        this.db.docHistoryIndex = {
-          findOne: sinon.stub().callsArgWith(2, null, { inS3: false }),
-        }
-        return this.PackManager.checkArchiveNotInProgress(
-          this.project_id,
-          this.doc_id,
-          this.pack_id,
-          this.callback
-        )
-      })
-      it('should call the callback', function () {
-        return this.callback.called.should.equal(true)
-      })
-      return it('should return an error', function () {
-        return this.callback
-          .calledWith(sinon.match.has('message'))
-          .should.equal(true)
-      })
-    })
-
-    describe('when an archive is completed', function () {
-      beforeEach(function () {
-        this.db.docHistoryIndex = {
-          findOne: sinon.stub().callsArgWith(2, null, { inS3: true }),
-        }
-        return this.PackManager.checkArchiveNotInProgress(
-          this.project_id,
-          this.doc_id,
-          this.pack_id,
-          this.callback
-        )
-      })
-      it('should call the callback', function () {
-        return this.callback.called.should.equal(true)
-      })
-      return it('should return an error', function () {
-        return this.callback
-          .calledWith(sinon.match.has('message'))
-          .should.equal(true)
-      })
-    })
-
-    return describe('when the archive has not started or completed', function () {
-      beforeEach(function () {
-        this.db.docHistoryIndex = {
-          findOne: sinon.stub().callsArgWith(2, null, {}),
-        }
-        return this.PackManager.checkArchiveNotInProgress(
-          this.project_id,
-          this.doc_id,
-          this.pack_id,
-          this.callback
-        )
-      })
-      it('should call the callback with no error', function () {
-        return this.callback.called.should.equal(true)
-      })
-      return it('should return with no error', function () {
-        return (typeof this.callback.lastCall.args[0]).should.equal('undefined')
-      })
-    })
-  })
-})
-
-// describe "setTTLOnArchivedPack", ->
-// 	beforeEach ->
-// 		@pack_id = "somepackid"
-// 		@onedayinms = 86400000
-// 		@db.docHistory =
-// 			findAndModify : sinon.stub().callsArgWith(1)
-
-// 	it "should set expires to 1 day", (done)->
-// 		#@PackManager._getOneDayInFutureWithRandomDelay = sinon.stub().returns(@onedayinms)
-// 		@PackManager.setTTLOnArchivedPack @project_id, @doc_id, @pack_id, =>
-// 			args = @db.docHistory.findAndModify.args[0][0]
-// 			args.query._id.should.equal @pack_id
-// 			args.update['$set'].expiresAt.should.equal @onedayinms
-// 			done()
-
-// describe "_getOneDayInFutureWithRandomDelay", ->
-// 	beforeEach ->
-// 		@onedayinms = 86400000
-// 		@thirtyMins = 1000 * 60 * 30
-
-// 	it "should give 1 day + 30 mins random time", (done)->
-// 		loops = 10000
-// 		while --loops > 0
-// 			randomDelay = @PackManager._getOneDayInFutureWithRandomDelay() - new Date(Date.now() + @onedayinms)
-// 			randomDelay.should.be.above(0)
-// 			randomDelay.should.be.below(@thirtyMins + 1)
-// 		done()
-
-function __range__(left, right, inclusive) {
-  const range = []
-  const ascending = left < right
-  const end = !inclusive ? right : ascending ? right + 1 : right - 1
-  for (let i = left; ascending ? i < end : i > end; ascending ? i++ : i--) {
-    range.push(i)
-  }
-  return range
-}

+ 0 - 163
services/track-changes/test/unit/js/RedisManager/RedisManagerTests.js

@@ -1,163 +0,0 @@
-/* eslint-disable
-    no-return-assign,
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS101: Remove unnecessary use of Array.from
- * DS102: Remove unnecessary code created because of implicit returns
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-const sinon = require('sinon')
-const { expect } = require('chai')
-const modulePath = '../../../../app/js/RedisManager.js'
-const SandboxedModule = require('sandboxed-module')
-
-describe('RedisManager', function () {
-  beforeEach(function () {
-    this.RedisManager = SandboxedModule.require(modulePath, {
-      requires: {
-        '@overleaf/redis-wrapper': {
-          createClient: () => {
-            return (this.rclient = {
-              auth: sinon.stub(),
-              multi: () => this.rclient,
-            })
-          },
-        },
-        '@overleaf/settings': {
-          redis: {
-            history: {
-              key_schema: {
-                uncompressedHistoryOps({ doc_id: docId }) {
-                  return `UncompressedHistoryOps:${docId}`
-                },
-                docsWithHistoryOps({ project_id: projectId }) {
-                  return `DocsWithHistoryOps:${projectId}`
-                },
-              },
-            },
-          },
-        },
-      },
-    })
-    this.doc_id = 'doc-id-123'
-    this.project_id = 'project-id-123'
-    this.batchSize = 100
-    return (this.callback = sinon.stub())
-  })
-
-  describe('getOldestDocUpdates', function () {
-    beforeEach(function () {
-      this.rawUpdates = [
-        { v: 42, op: 'mock-op-42' },
-        { v: 45, op: 'mock-op-45' },
-      ]
-      this.jsonUpdates = Array.from(this.rawUpdates).map(update =>
-        JSON.stringify(update)
-      )
-      this.rclient.lrange = sinon.stub().callsArgWith(3, null, this.jsonUpdates)
-      return this.RedisManager.getOldestDocUpdates(
-        this.doc_id,
-        this.batchSize,
-        this.callback
-      )
-    })
-
-    it('should read the updates from redis', function () {
-      return this.rclient.lrange
-        .calledWith(
-          `UncompressedHistoryOps:${this.doc_id}`,
-          0,
-          this.batchSize - 1
-        )
-        .should.equal(true)
-    })
-
-    it('should call the callback with the unparsed ops', function () {
-      return this.callback.calledWith(null, this.jsonUpdates).should.equal(true)
-    })
-
-    describe('expandDocUpdates', function () {
-      beforeEach(function () {
-        return this.RedisManager.expandDocUpdates(
-          this.jsonUpdates,
-          this.callback
-        )
-      })
-
-      return it('should call the callback with the parsed ops', function () {
-        return this.callback
-          .calledWith(null, this.rawUpdates)
-          .should.equal(true)
-      })
-    })
-
-    return describe('deleteAppliedDocUpdates', function () {
-      beforeEach(function () {
-        this.rclient.lrem = sinon.stub()
-        this.rclient.srem = sinon.stub()
-        this.rclient.exec = sinon.stub().callsArgWith(0)
-        return this.RedisManager.deleteAppliedDocUpdates(
-          this.project_id,
-          this.doc_id,
-          this.jsonUpdates,
-          this.callback
-        )
-      })
-
-      it('should delete the first update from redis', function () {
-        return this.rclient.lrem
-          .calledWith(
-            `UncompressedHistoryOps:${this.doc_id}`,
-            1,
-            this.jsonUpdates[0]
-          )
-          .should.equal(true)
-      })
-
-      it('should delete the second update from redis', function () {
-        return this.rclient.lrem
-          .calledWith(
-            `UncompressedHistoryOps:${this.doc_id}`,
-            1,
-            this.jsonUpdates[1]
-          )
-          .should.equal(true)
-      })
-
-      it('should delete the doc from the set of docs with history ops', function () {
-        return this.rclient.srem
-          .calledWith(`DocsWithHistoryOps:${this.project_id}`, this.doc_id)
-          .should.equal(true)
-      })
-
-      return it('should call the callback ', function () {
-        return this.callback.called.should.equal(true)
-      })
-    })
-  })
-
-  return describe('getDocIdsWithHistoryOps', function () {
-    beforeEach(function () {
-      this.doc_ids = ['mock-id-1', 'mock-id-2']
-      this.rclient.smembers = sinon.stub().callsArgWith(1, null, this.doc_ids)
-      return this.RedisManager.getDocIdsWithHistoryOps(
-        this.project_id,
-        this.callback
-      )
-    })
-
-    it('should read the doc_ids from redis', function () {
-      return this.rclient.smembers
-        .calledWith(`DocsWithHistoryOps:${this.project_id}`)
-        .should.equal(true)
-    })
-
-    return it('should call the callback with the doc_ids', function () {
-      return this.callback.calledWith(null, this.doc_ids).should.equal(true)
-    })
-  })
-})

+ 0 - 64
services/track-changes/test/unit/js/RestoreManager/RestoreManagerTests.js

@@ -1,64 +0,0 @@
-/* eslint-disable
-    no-return-assign,
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-const sinon = require('sinon')
-const { expect } = require('chai')
-const modulePath = '../../../../app/js/RestoreManager.js'
-const SandboxedModule = require('sandboxed-module')
-
-describe('RestoreManager', function () {
-  beforeEach(function () {
-    this.RestoreManager = SandboxedModule.require(modulePath, {
-      requires: {
-        './DocumentUpdaterManager': (this.DocumentUpdaterManager = {}),
-        './DiffManager': (this.DiffManager = {}),
-      },
-    })
-    this.callback = sinon.stub()
-    this.project_id = 'mock-project-id'
-    this.doc_id = 'mock-doc-id'
-    this.user_id = 'mock-user-id'
-    return (this.version = 42)
-  })
-
-  return describe('restoreToBeforeVersion', function () {
-    beforeEach(function () {
-      this.content = 'mock content'
-      this.DocumentUpdaterManager.setDocument = sinon.stub().callsArg(4)
-      this.DiffManager.getDocumentBeforeVersion = sinon
-        .stub()
-        .callsArgWith(3, null, this.content)
-      return this.RestoreManager.restoreToBeforeVersion(
-        this.project_id,
-        this.doc_id,
-        this.version,
-        this.user_id,
-        this.callback
-      )
-    })
-
-    it('should get the content before the requested version', function () {
-      return this.DiffManager.getDocumentBeforeVersion
-        .calledWith(this.project_id, this.doc_id, this.version)
-        .should.equal(true)
-    })
-
-    it('should set the document in the document updater', function () {
-      return this.DocumentUpdaterManager.setDocument
-        .calledWith(this.project_id, this.doc_id, this.content, this.user_id)
-        .should.equal(true)
-    })
-
-    return it('should call the callback', function () {
-      return this.callback.called.should.equal(true)
-    })
-  })
-})

+ 0 - 848
services/track-changes/test/unit/js/UpdateCompressor/UpdateCompressorTests.js

@@ -1,848 +0,0 @@
-/* eslint-disable
-    no-return-assign,
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-const sinon = require('sinon')
-const { expect } = require('chai')
-const modulePath = '../../../../app/js/UpdateCompressor.js'
-const SandboxedModule = require('sandboxed-module')
-
-const bigstring = __range__(0, 2 * 1024 * 1024, true)
-  .map(i => 'a')
-  .join('')
-const mediumstring = __range__(0, 1024 * 1024, true)
-  .map(j => 'a')
-  .join('')
-
-describe('UpdateCompressor', function () {
-  beforeEach(function () {
-    this.UpdateCompressor = SandboxedModule.require(modulePath, {
-      requires: {
-        '../lib/diff_match_patch': require('../../../../app/lib/diff_match_patch'),
-      },
-    })
-    this.user_id = 'user-id-1'
-    this.other_user_id = 'user-id-2'
-    this.ts1 = Date.now()
-    return (this.ts2 = Date.now() + 1000)
-  })
-
-  describe('convertToSingleOpUpdates', function () {
-    it('should split grouped updates into individual updates', function () {
-      return expect(
-        this.UpdateCompressor.convertToSingleOpUpdates([
-          {
-            op: [
-              (this.op1 = { p: 0, i: 'Foo' }),
-              (this.op2 = { p: 6, i: 'bar' }),
-            ],
-            meta: { ts: this.ts1, user_id: this.user_id },
-            v: 42,
-          },
-          {
-            op: [(this.op3 = { p: 10, i: 'baz' })],
-            meta: { ts: this.ts2, user_id: this.other_user_id },
-            v: 43,
-          },
-        ])
-      ).to.deep.equal([
-        {
-          op: this.op1,
-          meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id },
-          v: 42,
-        },
-        {
-          op: this.op2,
-          meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id },
-          v: 42,
-        },
-        {
-          op: this.op3,
-          meta: {
-            start_ts: this.ts2,
-            end_ts: this.ts2,
-            user_id: this.other_user_id,
-          },
-          v: 43,
-        },
-      ])
-    })
-
-    it('should return no-op updates when the op list is empty', function () {
-      return expect(
-        this.UpdateCompressor.convertToSingleOpUpdates([
-          {
-            op: [],
-            meta: { ts: this.ts1, user_id: this.user_id },
-            v: 42,
-          },
-        ])
-      ).to.deep.equal([
-        {
-          op: this.UpdateCompressor.NOOP,
-          meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id },
-          v: 42,
-        },
-      ])
-    })
-
-    return it('should ignore comment ops', function () {
-      return expect(
-        this.UpdateCompressor.convertToSingleOpUpdates([
-          {
-            op: [
-              (this.op1 = { p: 0, i: 'Foo' }),
-              (this.op2 = { p: 9, c: 'baz' }),
-              (this.op3 = { p: 6, i: 'bar' }),
-            ],
-            meta: { ts: this.ts1, user_id: this.user_id },
-            v: 42,
-          },
-        ])
-      ).to.deep.equal([
-        {
-          op: this.op1,
-          meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id },
-          v: 42,
-        },
-        {
-          op: this.op3,
-          meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id },
-          v: 42,
-        },
-      ])
-    })
-  })
-
-  describe('concatUpdatesWithSameVersion', function () {
-    it('should concat updates with the same version', function () {
-      return expect(
-        this.UpdateCompressor.concatUpdatesWithSameVersion([
-          {
-            op: (this.op1 = { p: 0, i: 'Foo' }),
-            meta: {
-              start_ts: this.ts1,
-              end_ts: this.ts1,
-              user_id: this.user_id,
-            },
-            v: 42,
-          },
-          {
-            op: (this.op2 = { p: 6, i: 'bar' }),
-            meta: {
-              start_ts: this.ts1,
-              end_ts: this.ts1,
-              user_id: this.user_id,
-            },
-            v: 42,
-          },
-          {
-            op: (this.op3 = { p: 10, i: 'baz' }),
-            meta: {
-              start_ts: this.ts2,
-              end_ts: this.ts2,
-              user_id: this.other_user_id,
-            },
-            v: 43,
-          },
-        ])
-      ).to.deep.equal([
-        {
-          op: [this.op1, this.op2],
-          meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id },
-          v: 42,
-        },
-        {
-          op: [this.op3],
-          meta: {
-            start_ts: this.ts2,
-            end_ts: this.ts2,
-            user_id: this.other_user_id,
-          },
-          v: 43,
-        },
-      ])
-    })
-
-    return it('should turn a noop into an empty op', function () {
-      return expect(
-        this.UpdateCompressor.concatUpdatesWithSameVersion([
-          {
-            op: this.UpdateCompressor.NOOP,
-            meta: {
-              start_ts: this.ts1,
-              end_ts: this.ts1,
-              user_id: this.user_id,
-            },
-            v: 42,
-          },
-        ])
-      ).to.deep.equal([
-        {
-          op: [],
-          meta: { start_ts: this.ts1, end_ts: this.ts1, user_id: this.user_id },
-          v: 42,
-        },
-      ])
-    })
-  })
-
-  describe('compress', function () {
-    describe('insert - insert', function () {
-      it('should append one insert to the other', function () {
-        return expect(
-          this.UpdateCompressor.compressUpdates([
-            {
-              op: { p: 3, i: 'foo' },
-              meta: { ts: this.ts1, user_id: this.user_id },
-              v: 42,
-            },
-            {
-              op: { p: 6, i: 'bar' },
-              meta: { ts: this.ts2, user_id: this.user_id },
-              v: 43,
-            },
-          ])
-        ).to.deep.equal([
-          {
-            op: { p: 3, i: 'foobar' },
-            meta: {
-              start_ts: this.ts1,
-              end_ts: this.ts2,
-              user_id: this.user_id,
-            },
-            v: 43,
-          },
-        ])
-      })
-
-      it('should insert one insert inside the other', function () {
-        return expect(
-          this.UpdateCompressor.compressUpdates([
-            {
-              op: { p: 3, i: 'foo' },
-              meta: { ts: this.ts1, user_id: this.user_id },
-              v: 42,
-            },
-            {
-              op: { p: 5, i: 'bar' },
-              meta: { ts: this.ts2, user_id: this.user_id },
-              v: 43,
-            },
-          ])
-        ).to.deep.equal([
-          {
-            op: { p: 3, i: 'fobaro' },
-            meta: {
-              start_ts: this.ts1,
-              end_ts: this.ts2,
-              user_id: this.user_id,
-            },
-            v: 43,
-          },
-        ])
-      })
-
-      it('should not append separated inserts', function () {
-        return expect(
-          this.UpdateCompressor.compressUpdates([
-            {
-              op: { p: 3, i: 'foo' },
-              meta: { ts: this.ts1, user_id: this.user_id },
-              v: 42,
-            },
-            {
-              op: { p: 9, i: 'bar' },
-              meta: { ts: this.ts2, user_id: this.user_id },
-              v: 43,
-            },
-          ])
-        ).to.deep.equal([
-          {
-            op: { p: 3, i: 'foo' },
-            meta: {
-              start_ts: this.ts1,
-              end_ts: this.ts1,
-              user_id: this.user_id,
-            },
-            v: 42,
-          },
-          {
-            op: { p: 9, i: 'bar' },
-            meta: {
-              start_ts: this.ts2,
-              end_ts: this.ts2,
-              user_id: this.user_id,
-            },
-            v: 43,
-          },
-        ])
-      })
-
-      it('should not append inserts that are too big (second op)', function () {
-        return expect(
-          this.UpdateCompressor.compressUpdates([
-            {
-              op: { p: 3, i: 'foo' },
-              meta: { ts: this.ts1, user_id: this.user_id },
-              v: 42,
-            },
-            {
-              op: { p: 6, i: bigstring },
-              meta: { ts: this.ts2, user_id: this.user_id },
-              v: 43,
-            },
-          ])
-        ).to.deep.equal([
-          {
-            op: { p: 3, i: 'foo' },
-            meta: {
-              start_ts: this.ts1,
-              end_ts: this.ts1,
-              user_id: this.user_id,
-            },
-            v: 42,
-          },
-          {
-            op: { p: 6, i: bigstring },
-            meta: {
-              start_ts: this.ts2,
-              end_ts: this.ts2,
-              user_id: this.user_id,
-            },
-            v: 43,
-          },
-        ])
-      })
-
-      it('should not append inserts that are too big (first op)', function () {
-        return expect(
-          this.UpdateCompressor.compressUpdates([
-            {
-              op: { p: 3, i: bigstring },
-              meta: { ts: this.ts1, user_id: this.user_id },
-              v: 42,
-            },
-            {
-              op: { p: 3 + bigstring.length, i: 'bar' },
-              meta: { ts: this.ts2, user_id: this.user_id },
-              v: 43,
-            },
-          ])
-        ).to.deep.equal([
-          {
-            op: { p: 3, i: bigstring },
-            meta: {
-              start_ts: this.ts1,
-              end_ts: this.ts1,
-              user_id: this.user_id,
-            },
-            v: 42,
-          },
-          {
-            op: { p: 3 + bigstring.length, i: 'bar' },
-            meta: {
-              start_ts: this.ts2,
-              end_ts: this.ts2,
-              user_id: this.user_id,
-            },
-            v: 43,
-          },
-        ])
-      })
-
-      return it('should not append inserts that are too big (first and second op)', function () {
-        return expect(
-          this.UpdateCompressor.compressUpdates([
-            {
-              op: { p: 3, i: mediumstring },
-              meta: { ts: this.ts1, user_id: this.user_id },
-              v: 42,
-            },
-            {
-              op: { p: 3 + mediumstring.length, i: mediumstring },
-              meta: { ts: this.ts2, user_id: this.user_id },
-              v: 43,
-            },
-          ])
-        ).to.deep.equal([
-          {
-            op: { p: 3, i: mediumstring },
-            meta: {
-              start_ts: this.ts1,
-              end_ts: this.ts1,
-              user_id: this.user_id,
-            },
-            v: 42,
-          },
-          {
-            op: { p: 3 + mediumstring.length, i: mediumstring },
-            meta: {
-              start_ts: this.ts2,
-              end_ts: this.ts2,
-              user_id: this.user_id,
-            },
-            v: 43,
-          },
-        ])
-      })
-    })
-
-    describe('delete - delete', function () {
-      it('should append one delete to the other', function () {
-        return expect(
-          this.UpdateCompressor.compressUpdates([
-            {
-              op: { p: 3, d: 'foo' },
-              meta: { ts: this.ts1, user_id: this.user_id },
-              v: 42,
-            },
-            {
-              op: { p: 3, d: 'bar' },
-              meta: { ts: this.ts2, user_id: this.user_id },
-              v: 43,
-            },
-          ])
-        ).to.deep.equal([
-          {
-            op: { p: 3, d: 'foobar' },
-            meta: {
-              start_ts: this.ts1,
-              end_ts: this.ts2,
-              user_id: this.user_id,
-            },
-            v: 43,
-          },
-        ])
-      })
-
-      it('should insert one delete inside the other', function () {
-        return expect(
-          this.UpdateCompressor.compressUpdates([
-            {
-              op: { p: 3, d: 'foo' },
-              meta: { ts: this.ts1, user_id: this.user_id },
-              v: 42,
-            },
-            {
-              op: { p: 1, d: 'bar' },
-              meta: { ts: this.ts2, user_id: this.user_id },
-              v: 43,
-            },
-          ])
-        ).to.deep.equal([
-          {
-            op: { p: 1, d: 'bafoor' },
-            meta: {
-              start_ts: this.ts1,
-              end_ts: this.ts2,
-              user_id: this.user_id,
-            },
-            v: 43,
-          },
-        ])
-      })
-
-      return it('should not append separated deletes', function () {
-        return expect(
-          this.UpdateCompressor.compressUpdates([
-            {
-              op: { p: 3, d: 'foo' },
-              meta: { ts: this.ts1, user_id: this.user_id },
-              v: 42,
-            },
-            {
-              op: { p: 9, d: 'bar' },
-              meta: { ts: this.ts2, user_id: this.user_id },
-              v: 43,
-            },
-          ])
-        ).to.deep.equal([
-          {
-            op: { p: 3, d: 'foo' },
-            meta: {
-              start_ts: this.ts1,
-              end_ts: this.ts1,
-              user_id: this.user_id,
-            },
-            v: 42,
-          },
-          {
-            op: { p: 9, d: 'bar' },
-            meta: {
-              start_ts: this.ts2,
-              end_ts: this.ts2,
-              user_id: this.user_id,
-            },
-            v: 43,
-          },
-        ])
-      })
-    })
-
-    describe('insert - delete', function () {
-      it('should undo a previous insert', function () {
-        return expect(
-          this.UpdateCompressor.compressUpdates([
-            {
-              op: { p: 3, i: 'foo' },
-              meta: { ts: this.ts1, user_id: this.user_id },
-              v: 42,
-            },
-            {
-              op: { p: 5, d: 'o' },
-              meta: { ts: this.ts2, user_id: this.user_id },
-              v: 43,
-            },
-          ])
-        ).to.deep.equal([
-          {
-            op: { p: 3, i: 'fo' },
-            meta: {
-              start_ts: this.ts1,
-              end_ts: this.ts2,
-              user_id: this.user_id,
-            },
-            v: 43,
-          },
-        ])
-      })
-
-      it('should remove part of an insert from the middle', function () {
-        return expect(
-          this.UpdateCompressor.compressUpdates([
-            {
-              op: { p: 3, i: 'fobaro' },
-              meta: { ts: this.ts1, user_id: this.user_id },
-              v: 42,
-            },
-            {
-              op: { p: 5, d: 'bar' },
-              meta: { ts: this.ts2, user_id: this.user_id },
-              v: 43,
-            },
-          ])
-        ).to.deep.equal([
-          {
-            op: { p: 3, i: 'foo' },
-            meta: {
-              start_ts: this.ts1,
-              end_ts: this.ts2,
-              user_id: this.user_id,
-            },
-            v: 43,
-          },
-        ])
-      })
-
-      it('should cancel out two opposite updates', function () {
-        return expect(
-          this.UpdateCompressor.compressUpdates([
-            {
-              op: { p: 3, i: 'foo' },
-              meta: { ts: this.ts1, user_id: this.user_id },
-              v: 42,
-            },
-            {
-              op: { p: 3, d: 'foo' },
-              meta: { ts: this.ts2, user_id: this.user_id },
-              v: 43,
-            },
-          ])
-        ).to.deep.equal([
-          {
-            op: { p: 3, i: '' },
-            meta: {
-              start_ts: this.ts1,
-              end_ts: this.ts2,
-              user_id: this.user_id,
-            },
-            v: 43,
-          },
-        ])
-      })
-
-      it('should not combine separated updates', function () {
-        return expect(
-          this.UpdateCompressor.compressUpdates([
-            {
-              op: { p: 3, i: 'foo' },
-              meta: { ts: this.ts1, user_id: this.user_id },
-              v: 42,
-            },
-            {
-              op: { p: 9, d: 'bar' },
-              meta: { ts: this.ts2, user_id: this.user_id },
-              v: 43,
-            },
-          ])
-        ).to.deep.equal([
-          {
-            op: { p: 3, i: 'foo' },
-            meta: {
-              start_ts: this.ts1,
-              end_ts: this.ts1,
-              user_id: this.user_id,
-            },
-            v: 42,
-          },
-          {
-            op: { p: 9, d: 'bar' },
-            meta: {
-              start_ts: this.ts2,
-              end_ts: this.ts2,
-              user_id: this.user_id,
-            },
-            v: 43,
-          },
-        ])
-      })
-
-      return it('should not combine updates with overlap beyond the end', function () {
-        return expect(
-          this.UpdateCompressor.compressUpdates([
-            {
-              op: { p: 3, i: 'foobar' },
-              meta: { ts: this.ts1, user_id: this.user_id },
-              v: 42,
-            },
-            {
-              op: { p: 6, d: 'bardle' },
-              meta: { ts: this.ts2, user_id: this.user_id },
-              v: 43,
-            },
-          ])
-        ).to.deep.equal([
-          {
-            op: { p: 3, i: 'foobar' },
-            meta: {
-              start_ts: this.ts1,
-              end_ts: this.ts1,
-              user_id: this.user_id,
-            },
-            v: 42,
-          },
-          {
-            op: { p: 6, d: 'bardle' },
-            meta: {
-              start_ts: this.ts2,
-              end_ts: this.ts2,
-              user_id: this.user_id,
-            },
-            v: 43,
-          },
-        ])
-      })
-    })
-
-    describe('delete - insert', function () {
-      it('should do a diff of the content', function () {
-        return expect(
-          this.UpdateCompressor.compressUpdates([
-            {
-              op: { p: 3, d: 'one two three four five six seven eight' },
-              meta: { ts: this.ts1, user_id: this.user_id },
-              v: 42,
-            },
-            {
-              op: { p: 3, i: 'one 2 three four five six seven eight' },
-              meta: { ts: this.ts2, user_id: this.user_id },
-              v: 43,
-            },
-          ])
-        ).to.deep.equal([
-          {
-            op: { p: 7, d: 'two' },
-            meta: {
-              start_ts: this.ts1,
-              end_ts: this.ts2,
-              user_id: this.user_id,
-            },
-            v: 43,
-          },
-          {
-            op: { p: 7, i: '2' },
-            meta: {
-              start_ts: this.ts1,
-              end_ts: this.ts2,
-              user_id: this.user_id,
-            },
-            v: 43,
-          },
-        ])
-      })
-
-      return it('should return a no-op if the delete and insert are the same', function () {
-        return expect(
-          this.UpdateCompressor.compressUpdates([
-            {
-              op: { p: 3, d: 'one two three four five six seven eight' },
-              meta: { ts: this.ts1, user_id: this.user_id },
-              v: 42,
-            },
-            {
-              op: { p: 3, i: 'one two three four five six seven eight' },
-              meta: { ts: this.ts2, user_id: this.user_id },
-              v: 43,
-            },
-          ])
-        ).to.deep.equal([
-          {
-            op: { p: 3, i: '' },
-            meta: {
-              start_ts: this.ts1,
-              end_ts: this.ts2,
-              user_id: this.user_id,
-            },
-            v: 43,
-          },
-        ])
-      })
-    })
-
-    describe('noop - insert', function () {
-      return it('should leave them untouched', function () {
-        return expect(
-          this.UpdateCompressor.compressUpdates([
-            {
-              op: this.UpdateCompressor.NOOP,
-              meta: { ts: this.ts1, user_id: this.user_id },
-              v: 42,
-            },
-            {
-              op: { p: 6, i: 'bar' },
-              meta: { ts: this.ts1, user_id: this.user_id },
-              v: 43,
-            },
-          ])
-        ).to.deep.equal([
-          {
-            op: this.UpdateCompressor.NOOP,
-            meta: {
-              start_ts: this.ts1,
-              end_ts: this.ts1,
-              user_id: this.user_id,
-            },
-            v: 42,
-          },
-          {
-            op: { p: 6, i: 'bar' },
-            meta: {
-              start_ts: this.ts1,
-              end_ts: this.ts1,
-              user_id: this.user_id,
-            },
-            v: 43,
-          },
-        ])
-      })
-    })
-
-    return describe('noop - delete', function () {
-      return it('should leave them untouched', function () {
-        return expect(
-          this.UpdateCompressor.compressUpdates([
-            {
-              op: this.UpdateCompressor.NOOP,
-              meta: { ts: this.ts1, user_id: this.user_id },
-              v: 42,
-            },
-            {
-              op: { p: 6, d: 'bar' },
-              meta: { ts: this.ts1, user_id: this.user_id },
-              v: 43,
-            },
-          ])
-        ).to.deep.equal([
-          {
-            op: this.UpdateCompressor.NOOP,
-            meta: {
-              start_ts: this.ts1,
-              end_ts: this.ts1,
-              user_id: this.user_id,
-            },
-            v: 42,
-          },
-          {
-            op: { p: 6, d: 'bar' },
-            meta: {
-              start_ts: this.ts1,
-              end_ts: this.ts1,
-              user_id: this.user_id,
-            },
-            v: 43,
-          },
-        ])
-      })
-    })
-  })
-
-  return describe('compressRawUpdates', function () {
-    return describe('merging in-place with an array op', function () {
-      return it('should not change the existing last updates', function () {
-        return expect(
-          this.UpdateCompressor.compressRawUpdates(
-            {
-              op: [
-                { p: 1000, d: 'hello' },
-                { p: 1000, i: 'HELLO()' },
-              ],
-              meta: {
-                start_ts: this.ts1,
-                end_ts: this.ts1,
-                user_id: this.user_id,
-              },
-              v: 42,
-            },
-            [
-              {
-                op: [{ p: 1006, i: 'WORLD' }],
-                meta: { ts: this.ts2, user_id: this.user_id },
-                v: 43,
-              },
-            ]
-          )
-        ).to.deep.equal([
-          {
-            op: [
-              { p: 1000, d: 'hello' },
-              { p: 1000, i: 'HELLO()' },
-            ],
-            meta: {
-              start_ts: this.ts1,
-              end_ts: this.ts1,
-              user_id: this.user_id,
-            },
-            v: 42,
-          },
-          {
-            op: [{ p: 1006, i: 'WORLD' }],
-            meta: {
-              start_ts: this.ts2,
-              end_ts: this.ts2,
-              user_id: this.user_id,
-            },
-            v: 43,
-          },
-        ])
-      })
-    })
-  })
-})
-
-function __range__(left, right, inclusive) {
-  const range = []
-  const ascending = left < right
-  const end = !inclusive ? right : ascending ? right + 1 : right - 1
-  for (let i = left; ascending ? i < end : i > end; ascending ? i++ : i--) {
-    range.push(i)
-  }
-  return range
-}

+ 0 - 182
services/track-changes/test/unit/js/UpdateTrimmer/UpdateTrimmerTests.js

@@ -1,182 +0,0 @@
-/* eslint-disable
-    no-return-assign,
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-const sinon = require('sinon')
-const { expect } = require('chai')
-const modulePath = '../../../../app/js/UpdateTrimmer.js'
-const SandboxedModule = require('sandboxed-module')
-const tk = require('timekeeper')
-
-describe('UpdateTrimmer', function () {
-  beforeEach(function () {
-    this.now = new Date()
-    tk.freeze(this.now)
-
-    this.UpdateTrimmer = SandboxedModule.require(modulePath, {
-      requires: {
-        './WebApiManager': (this.WebApiManager = {}),
-        './MongoManager': (this.MongoManager = {}),
-      },
-    })
-
-    this.callback = sinon.stub()
-    return (this.project_id = 'mock-project-id')
-  })
-
-  afterEach(function () {
-    return tk.reset()
-  })
-
-  return describe('shouldTrimUpdates', function () {
-    beforeEach(function () {
-      this.metadata = {}
-      this.details = { features: {} }
-      this.MongoManager.getProjectMetaData = sinon
-        .stub()
-        .callsArgWith(1, null, this.metadata)
-      this.MongoManager.setProjectMetaData = sinon.stub().callsArgWith(2)
-      this.MongoManager.upgradeHistory = sinon.stub().callsArgWith(1)
-      return (this.WebApiManager.getProjectDetails = sinon
-        .stub()
-        .callsArgWith(1, null, this.details))
-    })
-
-    describe('with preserveHistory set in the project meta data', function () {
-      beforeEach(function () {
-        this.metadata.preserveHistory = true
-        return this.UpdateTrimmer.shouldTrimUpdates(
-          this.project_id,
-          this.callback
-        )
-      })
-
-      it('should look up the meta data', function () {
-        return this.MongoManager.getProjectMetaData
-          .calledWith(this.project_id)
-          .should.equal(true)
-      })
-
-      it('should not look up the project details', function () {
-        return this.WebApiManager.getProjectDetails.called.should.equal(false)
-      })
-
-      return it('should return false', function () {
-        return this.callback.calledWith(null, false).should.equal(true)
-      })
-    })
-
-    describe('without preserveHistory set in the project meta data', function () {
-      beforeEach(function () {
-        return (this.metadata.preserveHistory = false)
-      })
-
-      describe('when the project has the versioning feature', function () {
-        beforeEach(function () {
-          this.details.features.versioning = true
-          return this.UpdateTrimmer.shouldTrimUpdates(
-            this.project_id,
-            this.callback
-          )
-        })
-
-        it('should look up the meta data', function () {
-          return this.MongoManager.getProjectMetaData
-            .calledWith(this.project_id)
-            .should.equal(true)
-        })
-
-        it('should look up the project details', function () {
-          return this.WebApiManager.getProjectDetails
-            .calledWith(this.project_id)
-            .should.equal(true)
-        })
-
-        it('should insert preserveHistory into the metadata', function () {
-          return this.MongoManager.setProjectMetaData
-            .calledWith(this.project_id, { preserveHistory: true })
-            .should.equal(true)
-        })
-
-        it('should upgrade any existing history', function () {
-          return this.MongoManager.upgradeHistory
-            .calledWith(this.project_id)
-            .should.equal(true)
-        })
-
-        return it('should return false', function () {
-          return this.callback.calledWith(null, false).should.equal(true)
-        })
-      })
-
-      return describe('when the project does not have the versioning feature', function () {
-        beforeEach(function () {
-          this.details.features.versioning = false
-          return this.UpdateTrimmer.shouldTrimUpdates(
-            this.project_id,
-            this.callback
-          )
-        })
-
-        return it('should return true', function () {
-          return this.callback.calledWith(null, true).should.equal(true)
-        })
-      })
-    })
-
-    return describe('without any meta data', function () {
-      beforeEach(function () {
-        return (this.MongoManager.getProjectMetaData = sinon
-          .stub()
-          .callsArgWith(1, null, null))
-      })
-
-      describe('when the project has the versioning feature', function () {
-        beforeEach(function () {
-          this.details.features.versioning = true
-          return this.UpdateTrimmer.shouldTrimUpdates(
-            this.project_id,
-            this.callback
-          )
-        })
-
-        it('should insert preserveHistory into the metadata', function () {
-          return this.MongoManager.setProjectMetaData
-            .calledWith(this.project_id, { preserveHistory: true })
-            .should.equal(true)
-        })
-
-        it('should upgrade any existing history', function () {
-          return this.MongoManager.upgradeHistory
-            .calledWith(this.project_id)
-            .should.equal(true)
-        })
-
-        return it('should return false', function () {
-          return this.callback.calledWith(null, false).should.equal(true)
-        })
-      })
-
-      return describe('when the project does not have the versioning feature', function () {
-        beforeEach(function () {
-          this.details.features.versioning = false
-          return this.UpdateTrimmer.shouldTrimUpdates(
-            this.project_id,
-            this.callback
-          )
-        })
-
-        return it('should return true', function () {
-          return this.callback.calledWith(null, true).should.equal(true)
-        })
-      })
-    })
-  })
-})

+ 0 - 1333
services/track-changes/test/unit/js/UpdatesManager/UpdatesManagerTests.js

@@ -1,1333 +0,0 @@
-/* eslint-disable
-    no-return-assign,
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS101: Remove unnecessary use of Array.from
- * DS102: Remove unnecessary code created because of implicit returns
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-const sinon = require('sinon')
-const { expect } = require('chai')
-const { ObjectId } = require('mongodb')
-const modulePath = '../../../../app/js/UpdatesManager.js'
-const SandboxedModule = require('sandboxed-module')
-
-describe('UpdatesManager', function () {
-  beforeEach(function () {
-    this.UpdatesManager = SandboxedModule.require(modulePath, {
-      singleOnly: true,
-      requires: {
-        './UpdateCompressor': (this.UpdateCompressor = {}),
-        './MongoManager': (this.MongoManager = {}),
-        './PackManager': (this.PackManager = {}),
-        './RedisManager': (this.RedisManager = {}),
-        './LockManager': (this.LockManager = {}),
-        './WebApiManager': (this.WebApiManager = {}),
-        './UpdateTrimmer': (this.UpdateTrimmer = {}),
-        './DocArchiveManager': (this.DocArchiveManager = {}),
-        '@overleaf/settings': {
-          redis: {
-            lock: {
-              key_schema: {
-                historyLock({ doc_id: docId }) {
-                  return `HistoryLock:${docId}`
-                },
-              },
-            },
-          },
-        },
-      },
-    })
-    this.doc_id = 'doc-id-123'
-    this.project_id = 'project-id-123'
-    this.callback = sinon.stub()
-    return (this.temporary = 'temp-mock')
-  })
-
-  describe('compressAndSaveRawUpdates', function () {
-    describe('when there are no raw ops', function () {
-      beforeEach(function () {
-        this.MongoManager.peekLastCompressedUpdate = sinon.stub()
-        return this.UpdatesManager.compressAndSaveRawUpdates(
-          this.project_id,
-          this.doc_id,
-          [],
-          this.temporary,
-          this.callback
-        )
-      })
-
-      it('should not need to access the database', function () {
-        return this.MongoManager.peekLastCompressedUpdate.called.should.equal(
-          false
-        )
-      })
-
-      return it('should call the callback', function () {
-        return this.callback.called.should.equal(true)
-      })
-    })
-
-    describe('when there is no compressed history to begin with', function () {
-      beforeEach(function () {
-        this.rawUpdates = [
-          { v: 12, op: 'mock-op-12' },
-          { v: 13, op: 'mock-op-13' },
-        ]
-        this.compressedUpdates = [{ v: 13, op: 'compressed-op-12' }]
-
-        this.MongoManager.peekLastCompressedUpdate = sinon
-          .stub()
-          .callsArgWith(1, null, null)
-        this.PackManager.insertCompressedUpdates = sinon.stub().callsArg(5)
-        this.UpdateCompressor.compressRawUpdates = sinon
-          .stub()
-          .returns(this.compressedUpdates)
-        return this.UpdatesManager.compressAndSaveRawUpdates(
-          this.project_id,
-          this.doc_id,
-          this.rawUpdates,
-          this.temporary,
-          this.callback
-        )
-      })
-
-      it('should look at the last compressed op', function () {
-        return this.MongoManager.peekLastCompressedUpdate
-          .calledWith(this.doc_id)
-          .should.equal(true)
-      })
-
-      it('should save the compressed ops as a pack', function () {
-        return this.PackManager.insertCompressedUpdates
-          .calledWith(
-            this.project_id,
-            this.doc_id,
-            null,
-            this.compressedUpdates,
-            this.temporary
-          )
-          .should.equal(true)
-      })
-
-      return it('should call the callback', function () {
-        return this.callback.called.should.equal(true)
-      })
-    })
-
-    describe('when the raw ops need appending to existing history', function () {
-      beforeEach(function () {
-        this.lastCompressedUpdate = { v: 11, op: 'compressed-op-11' }
-        this.compressedUpdates = [
-          { v: 12, op: 'compressed-op-11+12' },
-          { v: 13, op: 'compressed-op-12' },
-        ]
-
-        this.MongoManager.peekLastCompressedUpdate = sinon
-          .stub()
-          .callsArgWith(
-            1,
-            null,
-            this.lastCompressedUpdate,
-            this.lastCompressedUpdate.v
-          )
-        this.PackManager.insertCompressedUpdates = sinon.stub().callsArg(5)
-        return (this.UpdateCompressor.compressRawUpdates = sinon
-          .stub()
-          .returns(this.compressedUpdates))
-      })
-
-      describe('when the raw ops start where the existing history ends', function () {
-        beforeEach(function () {
-          this.rawUpdates = [
-            { v: 12, op: 'mock-op-12' },
-            { v: 13, op: 'mock-op-13' },
-          ]
-          return this.UpdatesManager.compressAndSaveRawUpdates(
-            this.project_id,
-            this.doc_id,
-            this.rawUpdates,
-            this.temporary,
-            this.callback
-          )
-        })
-
-        it('should look at the last compressed op', function () {
-          return this.MongoManager.peekLastCompressedUpdate
-            .calledWith(this.doc_id)
-            .should.equal(true)
-        })
-
-        it('should compress the raw ops', function () {
-          return this.UpdateCompressor.compressRawUpdates
-            .calledWith(null, this.rawUpdates)
-            .should.equal(true)
-        })
-
-        it('should save the new compressed ops into a pack', function () {
-          return this.PackManager.insertCompressedUpdates
-            .calledWith(
-              this.project_id,
-              this.doc_id,
-              this.lastCompressedUpdate,
-              this.compressedUpdates,
-              this.temporary
-            )
-            .should.equal(true)
-        })
-
-        return it('should call the callback', function () {
-          return this.callback.called.should.equal(true)
-        })
-      })
-
-      describe('when the raw ops start where the existing history ends and the history is in a pack', function () {
-        beforeEach(function () {
-          this.lastCompressedUpdate = {
-            pack: [{ v: 11, op: 'compressed-op-11' }],
-            v: 11,
-          }
-          this.rawUpdates = [
-            { v: 12, op: 'mock-op-12' },
-            { v: 13, op: 'mock-op-13' },
-          ]
-          this.MongoManager.peekLastCompressedUpdate = sinon
-            .stub()
-            .callsArgWith(
-              1,
-              null,
-              this.lastCompressedUpdate,
-              this.lastCompressedUpdate.v
-            )
-          return this.UpdatesManager.compressAndSaveRawUpdates(
-            this.project_id,
-            this.doc_id,
-            this.rawUpdates,
-            this.temporary,
-            this.callback
-          )
-        })
-
-        it('should look at the last compressed op', function () {
-          return this.MongoManager.peekLastCompressedUpdate
-            .calledWith(this.doc_id)
-            .should.equal(true)
-        })
-
-        it('should compress the raw ops', function () {
-          return this.UpdateCompressor.compressRawUpdates
-            .calledWith(null, this.rawUpdates)
-            .should.equal(true)
-        })
-
-        it('should save the new compressed ops into a pack', function () {
-          return this.PackManager.insertCompressedUpdates
-            .calledWith(
-              this.project_id,
-              this.doc_id,
-              this.lastCompressedUpdate,
-              this.compressedUpdates,
-              this.temporary
-            )
-            .should.equal(true)
-        })
-
-        return it('should call the callback', function () {
-          return this.callback.called.should.equal(true)
-        })
-      })
-
-      describe('when some raw ops are passed that have already been compressed', function () {
-        beforeEach(function () {
-          this.rawUpdates = [
-            { v: 10, op: 'mock-op-10' },
-            { v: 11, op: 'mock-op-11' },
-            { v: 12, op: 'mock-op-12' },
-            { v: 13, op: 'mock-op-13' },
-          ]
-
-          return this.UpdatesManager.compressAndSaveRawUpdates(
-            this.project_id,
-            this.doc_id,
-            this.rawUpdates,
-            this.temporary,
-            this.callback
-          )
-        })
-
-        return it('should only compress the more recent raw ops', function () {
-          return this.UpdateCompressor.compressRawUpdates
-            .calledWith(null, this.rawUpdates.slice(-2))
-            .should.equal(true)
-        })
-      })
-
-      describe('when the raw ops do not follow from the last compressed op version', function () {
-        beforeEach(function () {
-          this.rawUpdates = [{ v: 13, op: 'mock-op-13' }]
-          return this.UpdatesManager.compressAndSaveRawUpdates(
-            this.project_id,
-            this.doc_id,
-            this.rawUpdates,
-            this.temporary,
-            this.callback
-          )
-        })
-
-        it('should call the callback with an error', function () {
-          return this.callback
-            .calledWith(
-              sinon.match.has(
-                'message',
-                'Tried to apply raw op at version 13 to last compressed update with version 11 from unknown time'
-              )
-            )
-            .should.equal(true)
-        })
-
-        return it('should not insert any update into mongo', function () {
-          return this.PackManager.insertCompressedUpdates.called.should.equal(
-            false
-          )
-        })
-      })
-
-      return describe('when the raw ops are out of order', function () {
-        beforeEach(function () {
-          this.rawUpdates = [
-            { v: 13, op: 'mock-op-13' },
-            { v: 12, op: 'mock-op-12' },
-          ]
-          return this.UpdatesManager.compressAndSaveRawUpdates(
-            this.project_id,
-            this.doc_id,
-            this.rawUpdates,
-            this.temporary,
-            this.callback
-          )
-        })
-
-        it('should call the callback with an error', function () {
-          return this.callback
-            .calledWith(sinon.match.has('message'))
-            .should.equal(true)
-        })
-
-        return it('should not insert any update into mongo', function () {
-          return this.PackManager.insertCompressedUpdates.called.should.equal(
-            false
-          )
-        })
-      })
-    })
-
-    return describe('when the raw ops need appending to existing history which is in S3', function () {
-      beforeEach(function () {
-        this.lastCompressedUpdate = null
-        this.lastVersion = 11
-        this.compressedUpdates = [{ v: 13, op: 'compressed-op-12' }]
-
-        this.MongoManager.peekLastCompressedUpdate = sinon
-          .stub()
-          .callsArgWith(1, null, null, this.lastVersion)
-        this.PackManager.insertCompressedUpdates = sinon.stub().callsArg(5)
-        return (this.UpdateCompressor.compressRawUpdates = sinon
-          .stub()
-          .returns(this.compressedUpdates))
-      })
-
-      return describe('when the raw ops start where the existing history ends', function () {
-        beforeEach(function () {
-          this.rawUpdates = [
-            { v: 12, op: 'mock-op-12' },
-            { v: 13, op: 'mock-op-13' },
-          ]
-          return this.UpdatesManager.compressAndSaveRawUpdates(
-            this.project_id,
-            this.doc_id,
-            this.rawUpdates,
-            this.temporary,
-            this.callback
-          )
-        })
-
-        it('should try to look at the last compressed op', function () {
-          return this.MongoManager.peekLastCompressedUpdate
-            .calledWith(this.doc_id)
-            .should.equal(true)
-        })
-
-        it('should compress the last compressed op and the raw ops', function () {
-          return this.UpdateCompressor.compressRawUpdates
-            .calledWith(this.lastCompressedUpdate, this.rawUpdates)
-            .should.equal(true)
-        })
-
-        it('should save the compressed ops', function () {
-          return this.PackManager.insertCompressedUpdates
-            .calledWith(
-              this.project_id,
-              this.doc_id,
-              null,
-              this.compressedUpdates,
-              this.temporary
-            )
-            .should.equal(true)
-        })
-
-        return it('should call the callback', function () {
-          return this.callback.called.should.equal(true)
-        })
-      })
-    })
-  })
-
-  describe('processUncompressedUpdates', function () {
-    beforeEach(function () {
-      this.UpdatesManager.compressAndSaveRawUpdates = sinon
-        .stub()
-        .callsArgWith(4)
-      this.RedisManager.deleteAppliedDocUpdates = sinon.stub().callsArg(3)
-      this.MongoManager.backportProjectId = sinon.stub().callsArg(2)
-      return (this.UpdateTrimmer.shouldTrimUpdates = sinon
-        .stub()
-        .callsArgWith(1, null, (this.temporary = 'temp mock')))
-    })
-
-    describe('when there is fewer than one batch to send', function () {
-      beforeEach(function () {
-        this.updates = ['mock-update']
-        this.RedisManager.getOldestDocUpdates = sinon
-          .stub()
-          .callsArgWith(2, null, this.updates)
-        this.RedisManager.expandDocUpdates = sinon
-          .stub()
-          .callsArgWith(1, null, this.updates)
-        return this.UpdatesManager.processUncompressedUpdates(
-          this.project_id,
-          this.doc_id,
-          this.temporary,
-          this.callback
-        )
-      })
-
-      it('should get the oldest updates', function () {
-        return this.RedisManager.getOldestDocUpdates
-          .calledWith(this.doc_id, this.UpdatesManager.REDIS_READ_BATCH_SIZE)
-          .should.equal(true)
-      })
-
-      it('should compress and save the updates', function () {
-        return this.UpdatesManager.compressAndSaveRawUpdates
-          .calledWith(
-            this.project_id,
-            this.doc_id,
-            this.updates,
-            this.temporary
-          )
-          .should.equal(true)
-      })
-
-      it('should delete the batch of uncompressed updates that was just processed', function () {
-        return this.RedisManager.deleteAppliedDocUpdates
-          .calledWith(this.project_id, this.doc_id, this.updates)
-          .should.equal(true)
-      })
-
-      return it('should call the callback', function () {
-        return this.callback.called.should.equal(true)
-      })
-    })
-
-    return describe('when there are multiple batches to send', function () {
-      beforeEach(function (done) {
-        this.UpdatesManager.REDIS_READ_BATCH_SIZE = 2
-        this.updates = [
-          'mock-update-0',
-          'mock-update-1',
-          'mock-update-2',
-          'mock-update-3',
-          'mock-update-4',
-        ]
-        this.redisArray = this.updates.slice()
-        this.RedisManager.getOldestDocUpdates = (
-          docId,
-          batchSize,
-          callback
-        ) => {
-          if (callback == null) {
-            callback = function () {}
-          }
-          const updates = this.redisArray.slice(0, batchSize)
-          this.redisArray = this.redisArray.slice(batchSize)
-          return callback(null, updates)
-        }
-        sinon.spy(this.RedisManager, 'getOldestDocUpdates')
-        this.RedisManager.expandDocUpdates = (jsonUpdates, callback) => {
-          return callback(null, jsonUpdates)
-        }
-        sinon.spy(this.RedisManager, 'expandDocUpdates')
-        return this.UpdatesManager.processUncompressedUpdates(
-          this.project_id,
-          this.doc_id,
-          this.temporary,
-          (...args) => {
-            this.callback(...Array.from(args || []))
-            return done()
-          }
-        )
-      })
-
-      it('should get the oldest updates in three batches ', function () {
-        return this.RedisManager.getOldestDocUpdates.callCount.should.equal(3)
-      })
-
-      it('should compress and save the updates in batches', function () {
-        this.UpdatesManager.compressAndSaveRawUpdates
-          .calledWith(
-            this.project_id,
-            this.doc_id,
-            this.updates.slice(0, 2),
-            this.temporary
-          )
-          .should.equal(true)
-        this.UpdatesManager.compressAndSaveRawUpdates
-          .calledWith(
-            this.project_id,
-            this.doc_id,
-            this.updates.slice(2, 4),
-            this.temporary
-          )
-          .should.equal(true)
-        return this.UpdatesManager.compressAndSaveRawUpdates
-          .calledWith(
-            this.project_id,
-            this.doc_id,
-            this.updates.slice(4, 5),
-            this.temporary
-          )
-          .should.equal(true)
-      })
-
-      it('should delete the batches of uncompressed updates', function () {
-        return this.RedisManager.deleteAppliedDocUpdates.callCount.should.equal(
-          3
-        )
-      })
-
-      return it('should call the callback', function () {
-        return this.callback.called.should.equal(true)
-      })
-    })
-  })
-
-  describe('processCompressedUpdatesWithLock', function () {
-    beforeEach(function () {
-      this.UpdateTrimmer.shouldTrimUpdates = sinon
-        .stub()
-        .callsArgWith(1, null, (this.temporary = 'temp mock'))
-      this.MongoManager.backportProjectId = sinon.stub().callsArg(2)
-      this.UpdatesManager._processUncompressedUpdates = sinon.stub().callsArg(3)
-      this.LockManager.runWithLock = sinon.stub().callsArg(2)
-      return this.UpdatesManager.processUncompressedUpdatesWithLock(
-        this.project_id,
-        this.doc_id,
-        this.callback
-      )
-    })
-
-    it('should check if the updates are temporary', function () {
-      return this.UpdateTrimmer.shouldTrimUpdates
-        .calledWith(this.project_id)
-        .should.equal(true)
-    })
-
-    it('should backport the project id', function () {
-      return this.MongoManager.backportProjectId
-        .calledWith(this.project_id, this.doc_id)
-        .should.equal(true)
-    })
-
-    it('should run processUncompressedUpdates with the lock', function () {
-      return this.LockManager.runWithLock
-        .calledWith(`HistoryLock:${this.doc_id}`)
-        .should.equal(true)
-    })
-
-    return it('should call the callback', function () {
-      return this.callback.called.should.equal(true)
-    })
-  })
-
-  describe('getDocUpdates', function () {
-    beforeEach(function () {
-      this.updates = ['mock-updates']
-      this.options = { to: 'mock-to', limit: 'mock-limit' }
-      this.PackManager.getOpsByVersionRange = sinon
-        .stub()
-        .callsArgWith(4, null, this.updates)
-      this.UpdatesManager.processUncompressedUpdatesWithLock = sinon
-        .stub()
-        .callsArg(2)
-      return this.UpdatesManager.getDocUpdates(
-        this.project_id,
-        this.doc_id,
-        this.options,
-        this.callback
-      )
-    })
-
-    it('should process outstanding updates', function () {
-      return this.UpdatesManager.processUncompressedUpdatesWithLock
-        .calledWith(this.project_id, this.doc_id)
-        .should.equal(true)
-    })
-
-    it('should get the updates from the database', function () {
-      return this.PackManager.getOpsByVersionRange
-        .calledWith(
-          this.project_id,
-          this.doc_id,
-          this.options.from,
-          this.options.to
-        )
-        .should.equal(true)
-    })
-
-    return it('should return the updates', function () {
-      return this.callback.calledWith(null, this.updates).should.equal(true)
-    })
-  })
-
-  describe('getDocUpdatesWithUserInfo', function () {
-    beforeEach(function () {
-      this.updates = ['mock-updates']
-      this.options = { to: 'mock-to', limit: 'mock-limit' }
-      this.updatesWithUserInfo = ['updates-with-user-info']
-      this.UpdatesManager.getDocUpdates = sinon
-        .stub()
-        .callsArgWith(3, null, this.updates)
-      this.UpdatesManager.fillUserInfo = sinon
-        .stub()
-        .callsArgWith(1, null, this.updatesWithUserInfo)
-      return this.UpdatesManager.getDocUpdatesWithUserInfo(
-        this.project_id,
-        this.doc_id,
-        this.options,
-        this.callback
-      )
-    })
-
-    it('should get the updates', function () {
-      return this.UpdatesManager.getDocUpdates
-        .calledWith(this.project_id, this.doc_id, this.options)
-        .should.equal(true)
-    })
-
-    it('should file the updates with the user info', function () {
-      return this.UpdatesManager.fillUserInfo
-        .calledWith(this.updates)
-        .should.equal(true)
-    })
-
-    return it('should return the updates with the filled details', function () {
-      return this.callback
-        .calledWith(null, this.updatesWithUserInfo)
-        .should.equal(true)
-    })
-  })
-
-  describe('processUncompressedUpdatesForProject', function () {
-    beforeEach(function (done) {
-      this.doc_ids = ['mock-id-1', 'mock-id-2']
-      this.UpdateTrimmer.shouldTrimUpdates = sinon
-        .stub()
-        .callsArgWith(1, null, (this.temporary = 'temp mock'))
-      this.MongoManager.backportProjectId = sinon.stub().callsArg(2)
-      this.UpdatesManager._processUncompressedUpdatesForDocWithLock = sinon
-        .stub()
-        .callsArg(3)
-      this.RedisManager.getDocIdsWithHistoryOps = sinon
-        .stub()
-        .callsArgWith(1, null, this.doc_ids)
-      return this.UpdatesManager.processUncompressedUpdatesForProject(
-        this.project_id,
-        () => {
-          this.callback()
-          return done()
-        }
-      )
-    })
-
-    it('should get all the docs with history ops', function () {
-      return this.RedisManager.getDocIdsWithHistoryOps
-        .calledWith(this.project_id)
-        .should.equal(true)
-    })
-
-    it('should process the doc ops for the each doc_id', function () {
-      return Array.from(this.doc_ids).map(docId =>
-        this.UpdatesManager._processUncompressedUpdatesForDocWithLock
-          .calledWith(this.project_id, docId, this.temporary)
-          .should.equal(true)
-      )
-    })
-
-    return it('should call the callback', function () {
-      return this.callback.called.should.equal(true)
-    })
-  })
-
-  describe('getSummarizedProjectUpdates', function () {
-    beforeEach(function () {
-      this.updates = [
-        {
-          doc_id: 123,
-          v: 456,
-          op: 'mock-updates',
-          meta: { user_id: 123, start_ts: 1233, end_ts: 1234 },
-        },
-      ]
-      this.options = { before: 'mock-before', limit: 'mock-limit' }
-      this.summarizedUpdates = [
-        {
-          meta: { user_ids: [123], start_ts: 1233, end_ts: 1234 },
-          docs: { 123: { fromV: 456, toV: 456 } },
-        },
-      ]
-      this.updatesWithUserInfo = ['updates-with-user-info']
-      this.done_state = false
-      this.iterator = {
-        next: cb => {
-          this.done_state = true
-          return cb(null, this.updates)
-        },
-        done: () => {
-          return this.done_state
-        },
-      }
-      this.PackManager.makeProjectIterator = sinon
-        .stub()
-        .callsArgWith(2, null, this.iterator)
-      this.UpdatesManager.processUncompressedUpdatesForProject = sinon
-        .stub()
-        .callsArg(1)
-      this.UpdatesManager.fillSummarizedUserInfo = sinon
-        .stub()
-        .callsArgWith(1, null, this.updatesWithUserInfo)
-      return this.UpdatesManager.getSummarizedProjectUpdates(
-        this.project_id,
-        this.options,
-        this.callback
-      )
-    })
-
-    it('should process any outstanding updates', function () {
-      return this.UpdatesManager.processUncompressedUpdatesForProject
-        .calledWith(this.project_id)
-        .should.equal(true)
-    })
-
-    it('should get the updates', function () {
-      return this.PackManager.makeProjectIterator
-        .calledWith(this.project_id, this.options.before)
-        .should.equal(true)
-    })
-
-    it('should fill the updates with the user info', function () {
-      return this.UpdatesManager.fillSummarizedUserInfo
-        .calledWith(this.summarizedUpdates)
-        .should.equal(true)
-    })
-
-    return it('should return the updates with the filled details', function () {
-      return this.callback
-        .calledWith(null, this.updatesWithUserInfo)
-        .should.equal(true)
-    })
-  })
-
-  // describe "_extendBatchOfSummarizedUpdates", ->
-  // 	beforeEach ->
-  // 		@before = Date.now()
-  // 		@min_count = 2
-  // 		@existingSummarizedUpdates = ["summarized-updates-3"]
-  // 		@summarizedUpdates = ["summarized-updates-3", "summarized-update-2", "summarized-update-1"]
-
-  // 	describe "when there are updates to get", ->
-  // 		beforeEach ->
-  // 			@updates = [
-  // 				{op: "mock-op-1", meta: end_ts: @before - 10},
-  // 				{op: "mock-op-1", meta: end_ts: @nextBeforeTimestamp = @before - 20}
-  // 			]
-  // 			@existingSummarizedUpdates = ["summarized-updates-3"]
-  // 			@summarizedUpdates = ["summarized-updates-3", "summarized-update-2", "summarized-update-1"]
-  // 			@UpdatesManager._summarizeUpdates = sinon.stub().returns(@summarizedUpdates)
-  // 			@UpdatesManager.getProjectUpdatesWithUserInfo = sinon.stub().callsArgWith(2, null, @updates)
-  // 			@UpdatesManager._extendBatchOfSummarizedUpdates @project_id, @existingSummarizedUpdates, @before, @min_count, @callback
-
-  // 		it "should get the updates", ->
-  // 			@UpdatesManager.getProjectUpdatesWithUserInfo
-  // 				.calledWith(@project_id, { before: @before, limit: 3 * @min_count })
-  // 				.should.equal true
-
-  // 		it "should summarize the updates", ->
-  // 			@UpdatesManager._summarizeUpdates
-  // 				.calledWith(@updates, @existingSummarizedUpdates)
-  // 				.should.equal true
-
-  // 		it "should call the callback with the summarized updates and the next before timestamp", ->
-  // 			@callback.calledWith(null, @summarizedUpdates, @nextBeforeTimestamp).should.equal true
-
-  // 	describe "when there are no more updates", ->
-  // 		beforeEach ->
-  // 			@updates = []
-  // 			@UpdatesManager._summarizeUpdates = sinon.stub().returns(@summarizedUpdates)
-  // 			@UpdatesManager.getProjectUpdatesWithUserInfo = sinon.stub().callsArgWith(2, null, @updates)
-  // 			@UpdatesManager._extendBatchOfSummarizedUpdates @project_id, @existingSummarizedUpdates, @before, @min_count, @callback
-
-  // 		it "should call the callback with the summarized updates and null for nextBeforeTimestamp", ->
-  // 			@callback.calledWith(null, @summarizedUpdates, null).should.equal true
-
-  // describe "getSummarizedProjectUpdates", ->
-  // 	describe "when one batch of updates is enough to meet the limit", ->
-  // 		beforeEach ->
-  // 			@before = Date.now()
-  // 			@min_count = 2
-  // 			@updates = ["summarized-updates-3", "summarized-updates-2"]
-  // 			@nextBeforeTimestamp = @before - 100
-  // 			@UpdatesManager._extendBatchOfSummarizedUpdates = sinon.stub().callsArgWith(4, null, @updates, @nextBeforeTimestamp)
-  // 			@UpdatesManager.getSummarizedProjectUpdates @project_id, { before: @before, min_count: @min_count }, @callback
-
-  // 		it "should get the batch of summarized updates", ->
-  // 			@UpdatesManager._extendBatchOfSummarizedUpdates
-  // 				.calledWith(@project_id, [], @before, @min_count)
-  // 				.should.equal true
-
-  // 		it "should call the callback with the updates", ->
-  // 			@callback.calledWith(null, @updates, @nextBeforeTimestamp).should.equal true
-
-  // 	describe "when multiple batches are needed to meet the limit", ->
-  // 		beforeEach ->
-  // 			@before = Date.now()
-  // 			@min_count = 4
-  // 			@firstBatch =  [{ toV: 6, fromV: 6 }, { toV: 5, fromV: 5 }]
-  // 			@nextBeforeTimestamp = @before - 100
-  // 			@secondBatch = [{ toV: 4, fromV: 4 }, { toV: 3, fromV: 3 }]
-  // 			@nextNextBeforeTimestamp = @before - 200
-  // 			@UpdatesManager._extendBatchOfSummarizedUpdates = (project_id, existingUpdates, before, desiredLength, callback) =>
-  // 				if existingUpdates.length == 0
-  // 					callback null, @firstBatch, @nextBeforeTimestamp
-  // 				else
-  // 					callback null, @firstBatch.concat(@secondBatch), @nextNextBeforeTimestamp
-  // 			sinon.spy @UpdatesManager, "_extendBatchOfSummarizedUpdates"
-  // 			@UpdatesManager.getSummarizedProjectUpdates @project_id, { before: @before, min_count: @min_count }, @callback
-
-  // 		it "should get the first batch of summarized updates", ->
-  // 			@UpdatesManager._extendBatchOfSummarizedUpdates
-  // 				.calledWith(@project_id, [], @before, @min_count)
-  // 				.should.equal true
-
-  // 		it "should get the second batch of summarized updates", ->
-  // 			@UpdatesManager._extendBatchOfSummarizedUpdates
-  // 				.calledWith(@project_id, @firstBatch, @nextBeforeTimestamp, @min_count)
-  // 				.should.equal true
-
-  // 		it "should call the callback with all the updates", ->
-  // 			@callback.calledWith(null, @firstBatch.concat(@secondBatch), @nextNextBeforeTimestamp).should.equal true
-
-  // 	describe "when the end of the database is hit", ->
-  // 		beforeEach ->
-  // 			@before = Date.now()
-  // 			@min_count = 4
-  // 			@updates =  [{ toV: 6, fromV: 6 }, { toV: 5, fromV: 5 }]
-  // 			@UpdatesManager._extendBatchOfSummarizedUpdates = sinon.stub().callsArgWith(4, null, @updates, null)
-  // 			@UpdatesManager.getSummarizedProjectUpdates @project_id, { before: @before, min_count: @min_count }, @callback
-
-  // 		it "should get the batch of summarized updates", ->
-  // 			@UpdatesManager._extendBatchOfSummarizedUpdates
-  // 				.calledWith(@project_id, [], @before, @min_count)
-  // 				.should.equal true
-
-  // 		it "should call the callback with the updates", ->
-  // 			@callback.calledWith(null, @updates, null).should.equal true
-
-  describe('fillUserInfo', function () {
-    describe('with valid users', function () {
-      beforeEach(function (done) {
-        this.user_id_1 = ObjectId().toString()
-        this.user_id_2 = ObjectId().toString()
-        this.updates = [
-          {
-            meta: {
-              user_id: this.user_id_1,
-            },
-            op: 'mock-op-1',
-          },
-          {
-            meta: {
-              user_id: this.user_id_1,
-            },
-            op: 'mock-op-2',
-          },
-          {
-            meta: {
-              user_id: this.user_id_2,
-            },
-            op: 'mock-op-3',
-          },
-        ]
-        this.user_info = {}
-        this.user_info[this.user_id_1] = { email: 'user1@sharelatex.com' }
-        this.user_info[this.user_id_2] = { email: 'user2@sharelatex.com' }
-
-        this.WebApiManager.getUserInfo = (userId, callback) => {
-          if (callback == null) {
-            callback = function () {}
-          }
-          return callback(null, this.user_info[userId])
-        }
-        sinon.spy(this.WebApiManager, 'getUserInfo')
-
-        return this.UpdatesManager.fillUserInfo(
-          this.updates,
-          (error, results) => {
-            if (error) return done(error)
-            this.results = results
-            return done()
-          }
-        )
-      })
-
-      it('should only call getUserInfo once for each user_id', function () {
-        this.WebApiManager.getUserInfo.calledTwice.should.equal(true)
-        this.WebApiManager.getUserInfo
-          .calledWith(this.user_id_1)
-          .should.equal(true)
-        return this.WebApiManager.getUserInfo
-          .calledWith(this.user_id_2)
-          .should.equal(true)
-      })
-
-      return it('should return the updates with the user info filled', function () {
-        return expect(this.results).to.deep.equal([
-          {
-            meta: {
-              user: {
-                email: 'user1@sharelatex.com',
-              },
-            },
-            op: 'mock-op-1',
-          },
-          {
-            meta: {
-              user: {
-                email: 'user1@sharelatex.com',
-              },
-            },
-            op: 'mock-op-2',
-          },
-          {
-            meta: {
-              user: {
-                email: 'user2@sharelatex.com',
-              },
-            },
-            op: 'mock-op-3',
-          },
-        ])
-      })
-    })
-
-    return describe('with invalid user ids', function () {
-      beforeEach(function (done) {
-        this.updates = [
-          {
-            meta: {
-              user_id: null,
-            },
-            op: 'mock-op-1',
-          },
-          {
-            meta: {
-              user_id: 'anonymous-user',
-            },
-            op: 'mock-op-2',
-          },
-        ]
-        this.WebApiManager.getUserInfo = (userId, callback) => {
-          if (callback == null) {
-            callback = function () {}
-          }
-          return callback(null, this.user_info[userId])
-        }
-        sinon.spy(this.WebApiManager, 'getUserInfo')
-
-        return this.UpdatesManager.fillUserInfo(
-          this.updates,
-          (error, results) => {
-            if (error) return done(error)
-            this.results = results
-            return done()
-          }
-        )
-      })
-
-      it('should not call getUserInfo', function () {
-        return this.WebApiManager.getUserInfo.called.should.equal(false)
-      })
-
-      return it('should return the updates without the user info filled', function () {
-        return expect(this.results).to.deep.equal([
-          {
-            meta: {},
-            op: 'mock-op-1',
-          },
-          {
-            meta: {},
-            op: 'mock-op-2',
-          },
-        ])
-      })
-    })
-  })
-
-  return describe('_summarizeUpdates', function () {
-    beforeEach(function () {
-      this.now = Date.now()
-      this.user_1 = { id: 'mock-user-1' }
-      return (this.user_2 = { id: 'mock-user-2' })
-    })
-
-    it('should concat updates that are close in time', function () {
-      const result = this.UpdatesManager._summarizeUpdates([
-        {
-          doc_id: 'doc-id-1',
-          meta: {
-            user_id: this.user_1.id,
-            start_ts: this.now + 20,
-            end_ts: this.now + 30,
-          },
-          v: 5,
-        },
-        {
-          doc_id: 'doc-id-1',
-          meta: {
-            user_id: this.user_2.id,
-            start_ts: this.now,
-            end_ts: this.now + 10,
-          },
-          v: 4,
-        },
-      ])
-
-      return expect(result).to.deep.equal([
-        {
-          docs: {
-            'doc-id-1': {
-              fromV: 4,
-              toV: 5,
-            },
-          },
-          meta: {
-            user_ids: [this.user_1.id, this.user_2.id],
-            start_ts: this.now,
-            end_ts: this.now + 30,
-          },
-        },
-      ])
-    })
-
-    it('should leave updates that are far apart in time', function () {
-      const oneDay = 1000 * 60 * 60 * 24
-      const result = this.UpdatesManager._summarizeUpdates([
-        {
-          doc_id: 'doc-id-1',
-          meta: {
-            user_id: this.user_2.id,
-            start_ts: this.now + oneDay,
-            end_ts: this.now + oneDay + 10,
-          },
-          v: 5,
-        },
-        {
-          doc_id: 'doc-id-1',
-          meta: {
-            user_id: this.user_1.id,
-            start_ts: this.now,
-            end_ts: this.now + 10,
-          },
-          v: 4,
-        },
-      ])
-      return expect(result).to.deep.equal([
-        {
-          docs: {
-            'doc-id-1': {
-              fromV: 5,
-              toV: 5,
-            },
-          },
-          meta: {
-            user_ids: [this.user_2.id],
-            start_ts: this.now + oneDay,
-            end_ts: this.now + oneDay + 10,
-          },
-        },
-        {
-          docs: {
-            'doc-id-1': {
-              fromV: 4,
-              toV: 4,
-            },
-          },
-          meta: {
-            user_ids: [this.user_1.id],
-            start_ts: this.now,
-            end_ts: this.now + 10,
-          },
-        },
-      ])
-    })
-
-    it('should concat onto existing summarized updates', function () {
-      const result = this.UpdatesManager._summarizeUpdates(
-        [
-          {
-            doc_id: 'doc-id-2',
-            meta: {
-              user_id: this.user_1.id,
-              start_ts: this.now + 20,
-              end_ts: this.now + 30,
-            },
-            v: 5,
-          },
-          {
-            doc_id: 'doc-id-2',
-            meta: {
-              user_id: this.user_2.id,
-              start_ts: this.now,
-              end_ts: this.now + 10,
-            },
-            v: 4,
-          },
-        ],
-        [
-          {
-            docs: {
-              'doc-id-1': {
-                fromV: 6,
-                toV: 8,
-              },
-            },
-            meta: {
-              user_ids: [this.user_1.id],
-              start_ts: this.now + 40,
-              end_ts: this.now + 50,
-            },
-          },
-        ]
-      )
-      return expect(result).to.deep.equal([
-        {
-          docs: {
-            'doc-id-1': {
-              toV: 8,
-              fromV: 6,
-            },
-            'doc-id-2': {
-              toV: 5,
-              fromV: 4,
-            },
-          },
-          meta: {
-            user_ids: [this.user_1.id, this.user_2.id],
-            start_ts: this.now,
-            end_ts: this.now + 50,
-          },
-        },
-      ])
-    })
-
-    it('should include null user values', function () {
-      const result = this.UpdatesManager._summarizeUpdates([
-        {
-          doc_id: 'doc-id-1',
-          meta: {
-            user_id: this.user_1.id,
-            start_ts: this.now + 20,
-            end_ts: this.now + 30,
-          },
-          v: 5,
-        },
-        {
-          doc_id: 'doc-id-1',
-          meta: {
-            user_id: null,
-            start_ts: this.now,
-            end_ts: this.now + 10,
-          },
-          v: 4,
-        },
-      ])
-      return expect(result).to.deep.equal([
-        {
-          docs: {
-            'doc-id-1': {
-              fromV: 4,
-              toV: 5,
-            },
-          },
-          meta: {
-            user_ids: [this.user_1.id, null],
-            start_ts: this.now,
-            end_ts: this.now + 30,
-          },
-        },
-      ])
-    })
-
-    it('should include null user values, when the null is earlier in the updates list', function () {
-      const result = this.UpdatesManager._summarizeUpdates([
-        {
-          doc_id: 'doc-id-1',
-          meta: {
-            user_id: null,
-            start_ts: this.now,
-            end_ts: this.now + 10,
-          },
-          v: 4,
-        },
-        {
-          doc_id: 'doc-id-1',
-          meta: {
-            user_id: this.user_1.id,
-            start_ts: this.now + 20,
-            end_ts: this.now + 30,
-          },
-          v: 5,
-        },
-      ])
-      return expect(result).to.deep.equal([
-        {
-          docs: {
-            'doc-id-1': {
-              fromV: 4,
-              toV: 5,
-            },
-          },
-          meta: {
-            user_ids: [null, this.user_1.id],
-            start_ts: this.now,
-            end_ts: this.now + 30,
-          },
-        },
-      ])
-    })
-
-    it('should roll several null user values into one', function () {
-      const result = this.UpdatesManager._summarizeUpdates([
-        {
-          doc_id: 'doc-id-1',
-          meta: {
-            user_id: this.user_1.id,
-            start_ts: this.now + 20,
-            end_ts: this.now + 30,
-          },
-          v: 5,
-        },
-        {
-          doc_id: 'doc-id-1',
-          meta: {
-            user_id: null,
-            start_ts: this.now,
-            end_ts: this.now + 10,
-          },
-          v: 4,
-        },
-        {
-          doc_id: 'doc-id-1',
-          meta: {
-            user_id: null,
-            start_ts: this.now + 2,
-            end_ts: this.now + 4,
-          },
-          v: 4,
-        },
-      ])
-      return expect(result).to.deep.equal([
-        {
-          docs: {
-            'doc-id-1': {
-              fromV: 4,
-              toV: 5,
-            },
-          },
-          meta: {
-            user_ids: [this.user_1.id, null],
-            start_ts: this.now,
-            end_ts: this.now + 30,
-          },
-        },
-      ])
-    })
-
-    return it('should split updates before a big delete', function () {
-      const result = this.UpdatesManager._summarizeUpdates([
-        {
-          doc_id: 'doc-id-1',
-          op: [{ d: 'this is a long long long long long delete', p: 34 }],
-          meta: {
-            user_id: this.user_1.id,
-            start_ts: this.now + 20,
-            end_ts: this.now + 30,
-          },
-          v: 5,
-        },
-        {
-          doc_id: 'doc-id-1',
-          meta: {
-            user_id: this.user_2.id,
-            start_ts: this.now,
-            end_ts: this.now + 10,
-          },
-          v: 4,
-        },
-      ])
-
-      return expect(result).to.deep.equal([
-        {
-          docs: {
-            'doc-id-1': {
-              fromV: 5,
-              toV: 5,
-            },
-          },
-          meta: {
-            user_ids: [this.user_1.id],
-            start_ts: this.now + 20,
-            end_ts: this.now + 30,
-          },
-        },
-        {
-          docs: {
-            'doc-id-1': {
-              fromV: 4,
-              toV: 4,
-            },
-          },
-          meta: {
-            user_ids: [this.user_2.id],
-            start_ts: this.now,
-            end_ts: this.now + 10,
-          },
-        },
-      ])
-    })
-  })
-})

+ 0 - 55
services/track-changes/test/unit/js/Util/PackUtilsTests.js

@@ -1,55 +0,0 @@
-const { expect } = require('chai')
-const { ObjectId } = require('mongodb')
-const { packsAreDuplicated } = require('../../../../app/js/util/PackUtils')
-
-const examplePack = {
-  v: 12,
-  meta: {
-    user_id: '525e6018b53de7a920002545',
-    start_ts: 1399130007228,
-    end_ts: 1399130007228,
-  },
-  op: [
-    {
-      p: 2372,
-      d: 'Test for a Subsection',
-    },
-    {
-      p: 2372,
-      i: 'Reviews and review terminology',
-    },
-  ],
-}
-
-const objectId1 = ObjectId('53650ba27e62ca78520d9814')
-const objectId2 = ObjectId('0b5a814a27e678520d92c536')
-
-describe('PackUtils', function () {
-  describe('packsAreDuplicated()', function () {
-    it('returns `false` when any of the packs is undefined', function () {
-      const pack = { ...examplePack, _id: objectId1 }
-      expect(packsAreDuplicated(pack, undefined)).to.be.false
-      expect(packsAreDuplicated(undefined, pack)).to.be.false
-      expect(packsAreDuplicated(undefined, undefined)).to.be.false
-    })
-
-    it('returns `true` for identical packs with same `_id`', function () {
-      const pack1 = { ...examplePack, _id: objectId1 }
-      const pack2 = { ...examplePack, _id: objectId1 }
-      expect(packsAreDuplicated(pack1, pack2)).to.be.true
-    })
-
-    it('returns `true` for identical packs with different `_id`', function () {
-      const pack1 = { ...examplePack, _id: objectId1 }
-      const pack2 = { ...examplePack, _id: objectId2 }
-      expect(packsAreDuplicated(pack1, pack2)).to.be.true
-    })
-
-    it('returns `false` for packs with different anidated properties', function () {
-      const pack1 = { ...examplePack, _id: objectId1 }
-      const pack2 = { ...examplePack, _id: 1 }
-      pack2.op = [...pack2.op, { p: 2800, i: 'char' }]
-      expect(packsAreDuplicated(pack1, pack2)).to.be.false
-    })
-  })
-})

+ 0 - 208
services/track-changes/test/unit/js/WebApiManager/WebApiManagerTests.js

@@ -1,208 +0,0 @@
-/* eslint-disable
-    no-return-assign,
-    no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-const sinon = require('sinon')
-const { expect } = require('chai')
-const modulePath = '../../../../app/js/WebApiManager.js'
-const SandboxedModule = require('sandboxed-module')
-
-describe('WebApiManager', function () {
-  beforeEach(function () {
-    this.WebApiManager = SandboxedModule.require(modulePath, {
-      requires: {
-        requestretry: (this.request = {}),
-        '@overleaf/settings': (this.settings = {
-          apis: {
-            web: {
-              url: 'http://example.com',
-              user: 'sharelatex',
-              pass: 'password',
-            },
-          },
-        }),
-      },
-    })
-    this.callback = sinon.stub()
-    this.user_id = 'mock-user-id'
-    this.project_id = 'mock-project-id'
-    this.user_info = {
-      email: 'leo@sharelatex.com',
-      id: this.user_id,
-      first_name: 'Leo',
-      last_nane: 'Lion',
-      extra_param: 'blah',
-    }
-    return (this.project = { features: 'mock-features' })
-  })
-
-  describe('getUserInfo', function () {
-    describe('successfully', function () {
-      beforeEach(function () {
-        this.body = JSON.stringify(this.user_info)
-        this.request.get = sinon
-          .stub()
-          .callsArgWith(1, null, { statusCode: 200 }, this.body)
-        return this.WebApiManager.getUserInfo(this.user_id, this.callback)
-      })
-
-      it('should get the user from the web api', function () {
-        return this.request.get
-          .calledWithMatch({
-            url: `${this.settings.apis.web.url}/user/${this.user_id}/personal_info`,
-            auth: {
-              user: this.settings.apis.web.user,
-              pass: this.settings.apis.web.pass,
-              sendImmediately: true,
-            },
-          })
-          .should.equal(true)
-      })
-
-      return it('should call the callback with only the email, id and names', function () {
-        return this.callback
-          .calledWith(null, {
-            id: this.user_id,
-            email: this.user_info.email,
-            first_name: this.user_info.first_name,
-            last_name: this.user_info.last_name,
-          })
-          .should.equal(true)
-      })
-    })
-
-    describe('when the web API returns an error', function () {
-      beforeEach(function () {
-        this.request.get = sinon
-          .stub()
-          .callsArgWith(
-            1,
-            (this.error = new Error('something went wrong')),
-            null,
-            null
-          )
-        return this.WebApiManager.getUserInfo(this.user_id, this.callback)
-      })
-
-      return it('should return an error to the callback', function () {
-        return this.callback.calledWith(this.error).should.equal(true)
-      })
-    })
-
-    describe('when the web returns a failure error code', function () {
-      beforeEach(function () {
-        this.request.get = sinon
-          .stub()
-          .callsArgWith(1, null, { statusCode: 500, attempts: 42 }, '')
-        return this.WebApiManager.getUserInfo(this.user_id, this.callback)
-      })
-
-      return it('should return the callback with an error', function () {
-        return this.callback
-          .calledWith(
-            sinon.match.has(
-              'message',
-              'web returned a non-success status code: 500 (attempts: 42)'
-            )
-          )
-          .should.equal(true)
-      })
-    })
-
-    return describe('when the user cannot be found', function () {
-      beforeEach(function () {
-        this.request.get = sinon
-          .stub()
-          .callsArgWith(1, null, { statusCode: 404 }, 'nothing')
-        return this.WebApiManager.getUserInfo(this.user_id, this.callback)
-      })
-
-      return it('should return a null value', function () {
-        return this.callback.calledWith(null, null).should.equal(true)
-      })
-    })
-  })
-
-  return describe('getProjectDetails', function () {
-    describe('successfully', function () {
-      beforeEach(function () {
-        this.body = JSON.stringify(this.project)
-        this.request.get = sinon
-          .stub()
-          .callsArgWith(1, null, { statusCode: 200 }, this.body)
-        return this.WebApiManager.getProjectDetails(
-          this.project_id,
-          this.callback
-        )
-      })
-
-      it('should get the project from the web api', function () {
-        return this.request.get
-          .calledWithMatch({
-            url: `${this.settings.apis.web.url}/project/${this.project_id}/details`,
-            auth: {
-              user: this.settings.apis.web.user,
-              pass: this.settings.apis.web.pass,
-              sendImmediately: true,
-            },
-          })
-          .should.equal(true)
-      })
-
-      return it('should call the callback with the project', function () {
-        return this.callback.calledWith(null, this.project).should.equal(true)
-      })
-    })
-
-    describe('when the web API returns an error', function () {
-      beforeEach(function () {
-        this.request.get = sinon
-          .stub()
-          .callsArgWith(
-            1,
-            (this.error = new Error('something went wrong')),
-            null,
-            null
-          )
-        return this.WebApiManager.getProjectDetails(
-          this.project_id,
-          this.callback
-        )
-      })
-
-      return it('should return an error to the callback', function () {
-        return this.callback.calledWith(this.error).should.equal(true)
-      })
-    })
-
-    return describe('when the web returns a failure error code', function () {
-      beforeEach(function () {
-        this.request.get = sinon
-          .stub()
-          .callsArgWith(1, null, { statusCode: 500, attempts: 42 }, '')
-        return this.WebApiManager.getProjectDetails(
-          this.project_id,
-          this.callback
-        )
-      })
-
-      return it('should return the callback with an error', function () {
-        return this.callback
-          .calledWith(
-            sinon.match.has(
-              'message',
-              'web returned a non-success status code: 500 (attempts: 42)'
-            )
-          )
-          .should.equal(true)
-      })
-    })
-  })
-})

Some files were not shown because too many files changed in this diff