RangesTests.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596
  1. const sinon = require('sinon')
  2. const { expect } = require('chai')
  3. const { setTimeout } = require('node:timers/promises')
  4. const { db, ObjectId } = require('../../../app/js/mongodb')
  5. const MockWebApi = require('./helpers/MockWebApi')
  6. const DocUpdaterClient = require('./helpers/DocUpdaterClient')
  7. const DocUpdaterApp = require('./helpers/DocUpdaterApp')
  8. const RangesManager = require('../../../app/js/RangesManager')
  9. const sandbox = sinon.createSandbox()
  10. describe('Ranges', function () {
  11. before(async function () {
  12. await DocUpdaterApp.ensureRunning()
  13. })
  14. describe('tracking changes from ops', function () {
  15. before(async function () {
  16. this.project_id = DocUpdaterClient.randomId()
  17. this.user_id = DocUpdaterClient.randomId()
  18. this.id_seed = '587357bd35e64f6157'
  19. this.doc = {
  20. id: DocUpdaterClient.randomId(),
  21. lines: ['aaa'],
  22. }
  23. this.updates = [
  24. {
  25. doc: this.doc.id,
  26. op: [{ i: '123', p: 1 }],
  27. v: 0,
  28. meta: { user_id: this.user_id },
  29. },
  30. {
  31. doc: this.doc.id,
  32. op: [{ i: '456', p: 5 }],
  33. v: 1,
  34. meta: { user_id: this.user_id, tc: this.id_seed },
  35. },
  36. {
  37. doc: this.doc.id,
  38. op: [{ d: '12', p: 1 }],
  39. v: 2,
  40. meta: { user_id: this.user_id },
  41. },
  42. ]
  43. MockWebApi.insertDoc(this.project_id, this.doc.id, {
  44. lines: this.doc.lines,
  45. version: 0,
  46. })
  47. await DocUpdaterClient.preloadDoc(this.project_id, this.doc.id)
  48. for (const update of this.updates) {
  49. await DocUpdaterClient.sendUpdate(this.project_id, this.doc.id, update)
  50. }
  51. })
  52. it('should update the ranges', async function () {
  53. const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc.id)
  54. const { ranges } = doc
  55. const change = ranges.changes[0]
  56. change.op.should.deep.equal({ i: '456', p: 3 })
  57. change.id.should.equal(this.id_seed + '000001')
  58. change.metadata.user_id.should.equal(this.user_id)
  59. })
  60. describe('Adding comments', function () {
  61. describe('standalone', function () {
  62. before(async function () {
  63. this.project_id = DocUpdaterClient.randomId()
  64. this.user_id = DocUpdaterClient.randomId()
  65. this.tid = DocUpdaterClient.randomId()
  66. this.doc = {
  67. id: DocUpdaterClient.randomId(),
  68. lines: ['foo bar baz'],
  69. }
  70. this.updates = [
  71. {
  72. doc: this.doc.id,
  73. op: [{ c: 'bar', p: 4, t: this.tid }],
  74. v: 0,
  75. },
  76. ]
  77. MockWebApi.insertDoc(this.project_id, this.doc.id, {
  78. lines: this.doc.lines,
  79. version: 0,
  80. })
  81. await DocUpdaterClient.preloadDoc(this.project_id, this.doc.id)
  82. for (const update of this.updates) {
  83. await DocUpdaterClient.sendUpdate(
  84. this.project_id,
  85. this.doc.id,
  86. update
  87. )
  88. }
  89. await setTimeout(200)
  90. })
  91. it('should update the ranges', async function () {
  92. const doc = await DocUpdaterClient.getDoc(
  93. this.project_id,
  94. this.doc.id
  95. )
  96. const { ranges } = doc
  97. const comment = ranges.comments[0]
  98. comment.op.should.deep.equal({ c: 'bar', p: 4, t: this.tid })
  99. comment.id.should.equal(this.tid)
  100. })
  101. })
  102. describe('with conflicting ops needing OT', function () {
  103. before(async function () {
  104. this.project_id = DocUpdaterClient.randomId()
  105. this.user_id = DocUpdaterClient.randomId()
  106. this.tid = DocUpdaterClient.randomId()
  107. this.doc = {
  108. id: DocUpdaterClient.randomId(),
  109. lines: ['foo bar baz'],
  110. }
  111. this.updates = [
  112. {
  113. doc: this.doc.id,
  114. op: [{ i: 'ABC', p: 3 }],
  115. v: 0,
  116. meta: { user_id: this.user_id },
  117. },
  118. {
  119. doc: this.doc.id,
  120. op: [{ c: 'bar', p: 4, t: this.tid }],
  121. v: 0,
  122. },
  123. ]
  124. MockWebApi.insertDoc(this.project_id, this.doc.id, {
  125. lines: this.doc.lines,
  126. version: 0,
  127. })
  128. await DocUpdaterClient.preloadDoc(this.project_id, this.doc.id)
  129. for (const update of this.updates) {
  130. await DocUpdaterClient.sendUpdate(
  131. this.project_id,
  132. this.doc.id,
  133. update
  134. )
  135. }
  136. await setTimeout(200)
  137. })
  138. it('should update the comments with the OT shifted comment', async function () {
  139. const doc = await DocUpdaterClient.getDoc(
  140. this.project_id,
  141. this.doc.id
  142. )
  143. const { ranges } = doc
  144. const comment = ranges.comments[0]
  145. comment.op.should.deep.equal({ c: 'bar', p: 7, t: this.tid })
  146. })
  147. })
  148. })
  149. })
  150. describe('Loading ranges from persistence layer', function () {
  151. before(async function () {
  152. this.project_id = DocUpdaterClient.randomId()
  153. this.user_id = DocUpdaterClient.randomId()
  154. this.id_seed = '587357bd35e64f6157'
  155. this.doc = {
  156. id: DocUpdaterClient.randomId(),
  157. lines: ['a123aa'],
  158. }
  159. this.update = {
  160. doc: this.doc.id,
  161. op: [{ i: '456', p: 5 }],
  162. v: 0,
  163. meta: { user_id: this.user_id, tc: this.id_seed },
  164. }
  165. MockWebApi.insertDoc(this.project_id, this.doc.id, {
  166. lines: this.doc.lines,
  167. version: 0,
  168. ranges: {
  169. changes: [
  170. {
  171. op: { i: '123', p: 1 },
  172. metadata: {
  173. user_id: this.user_id,
  174. ts: new Date(),
  175. },
  176. },
  177. ],
  178. },
  179. })
  180. await DocUpdaterClient.preloadDoc(this.project_id, this.doc.id)
  181. await DocUpdaterClient.sendUpdate(
  182. this.project_id,
  183. this.doc.id,
  184. this.update
  185. )
  186. await setTimeout(200)
  187. })
  188. it('should have preloaded the existing ranges', async function () {
  189. const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc.id)
  190. const { changes } = doc.ranges
  191. changes[0].op.should.deep.equal({ i: '123', p: 1 })
  192. changes[1].op.should.deep.equal({ i: '456', p: 5 })
  193. })
  194. it('should flush the ranges to the persistence layer again', async function () {
  195. await DocUpdaterClient.flushDoc(this.project_id, this.doc.id)
  196. const doc = await new Promise((resolve, reject) => {
  197. MockWebApi.getDocument(this.project_id, this.doc.id, (error, doc) => {
  198. if (error) {
  199. reject(error)
  200. } else {
  201. resolve(doc)
  202. }
  203. })
  204. })
  205. const { changes } = doc.ranges
  206. changes[0].op.should.deep.equal({ i: '123', p: 1 })
  207. changes[1].op.should.deep.equal({ i: '456', p: 5 })
  208. })
  209. })
  210. describe('accepting a change', function () {
  211. beforeEach(async function () {
  212. sandbox.spy(MockWebApi, 'setDocument')
  213. this.project_id = DocUpdaterClient.randomId()
  214. this.user_id = DocUpdaterClient.randomId()
  215. this.id_seed = '587357bd35e64f6157'
  216. this.doc = {
  217. id: DocUpdaterClient.randomId(),
  218. lines: ['aaa'],
  219. }
  220. this.update = {
  221. doc: this.doc.id,
  222. op: [{ i: '456', p: 1 }],
  223. v: 0,
  224. meta: { user_id: this.user_id, tc: this.id_seed },
  225. }
  226. MockWebApi.insertDoc(this.project_id, this.doc.id, {
  227. lines: this.doc.lines,
  228. version: 0,
  229. })
  230. await DocUpdaterClient.preloadDoc(this.project_id, this.doc.id)
  231. await DocUpdaterClient.sendUpdate(
  232. this.project_id,
  233. this.doc.id,
  234. this.update
  235. )
  236. await setTimeout(200)
  237. const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc.id)
  238. const { ranges } = doc
  239. const change = ranges.changes[0]
  240. change.op.should.deep.equal({ i: '456', p: 1 })
  241. change.id.should.equal(this.id_seed + '000001')
  242. change.metadata.user_id.should.equal(this.user_id)
  243. })
  244. afterEach(function () {
  245. sandbox.restore()
  246. })
  247. it('should remove the change after accepting', async function () {
  248. await DocUpdaterClient.acceptChange(
  249. this.project_id,
  250. this.doc.id,
  251. this.id_seed + '000001'
  252. )
  253. const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc.id)
  254. expect(doc.ranges.changes).to.be.undefined
  255. })
  256. it('should persist the ranges after accepting', async function () {
  257. await DocUpdaterClient.flushDoc(this.project_id, this.doc.id)
  258. await DocUpdaterClient.acceptChange(
  259. this.project_id,
  260. this.doc.id,
  261. this.id_seed + '000001'
  262. )
  263. await DocUpdaterClient.flushDoc(this.project_id, this.doc.id)
  264. const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc.id)
  265. expect(doc.ranges.changes).to.be.undefined
  266. MockWebApi.setDocument
  267. .calledWith(this.project_id, this.doc.id, ['a456aa'], 1, {})
  268. .should.equal(true)
  269. })
  270. })
  271. describe('accepting multiple changes', function () {
  272. beforeEach(async function () {
  273. this.getHistoryUpdatesSpy = sandbox.spy(
  274. RangesManager,
  275. 'getHistoryUpdatesForAcceptedChanges'
  276. )
  277. this.project_id = DocUpdaterClient.randomId()
  278. this.user_id = DocUpdaterClient.randomId()
  279. this.doc = {
  280. id: DocUpdaterClient.randomId(),
  281. lines: ['aaa', 'bbb', 'ccc', 'ddd', 'eee'],
  282. }
  283. MockWebApi.insertDoc(this.project_id, this.doc.id, {
  284. lines: this.doc.lines,
  285. version: 0,
  286. historyRangesSupport: true,
  287. })
  288. await DocUpdaterClient.preloadDoc(this.project_id, this.doc.id)
  289. this.id_seed_1 = 'tc_1'
  290. this.id_seed_2 = 'tc_2'
  291. this.id_seed_3 = 'tc_3'
  292. this.updates = [
  293. {
  294. doc: this.doc.id,
  295. op: [{ d: 'bbb', p: 4 }],
  296. v: 0,
  297. meta: {
  298. user_id: this.user_id,
  299. tc: this.id_seed_1,
  300. },
  301. },
  302. {
  303. doc: this.doc.id,
  304. op: [{ d: 'ccc', p: 5 }],
  305. v: 1,
  306. meta: {
  307. user_id: this.user_id,
  308. tc: this.id_seed_2,
  309. },
  310. },
  311. {
  312. doc: this.doc.id,
  313. op: [{ d: 'ddd', p: 6 }],
  314. v: 2,
  315. meta: {
  316. user_id: this.user_id,
  317. tc: this.id_seed_3,
  318. },
  319. },
  320. ]
  321. await DocUpdaterClient.sendUpdates(
  322. this.project_id,
  323. this.doc.id,
  324. this.updates
  325. )
  326. await setTimeout(200)
  327. const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc.id)
  328. const { ranges } = doc
  329. const changeOps = ranges.changes.map(change => change.op).flat()
  330. changeOps.should.deep.equal([
  331. { d: 'bbb', p: 4 },
  332. { d: 'ccc', p: 5 },
  333. { d: 'ddd', p: 6 },
  334. ])
  335. })
  336. afterEach(function () {
  337. sandbox.restore()
  338. })
  339. it('accepting changes in order', async function () {
  340. await DocUpdaterClient.acceptChanges(this.project_id, this.doc.id, [
  341. this.id_seed_1 + '000001',
  342. this.id_seed_2 + '000001',
  343. this.id_seed_3 + '000001',
  344. ])
  345. const historyUpdates = this.getHistoryUpdatesSpy.returnValues[0]
  346. expect(historyUpdates[0]).to.deep.equal({
  347. doc: this.doc.id,
  348. meta: {
  349. pathname: '/a/b/c.tex',
  350. doc_length: 10,
  351. history_doc_length: 19,
  352. ts: historyUpdates[0].meta.ts,
  353. user_id: this.user_id,
  354. },
  355. op: [{ p: 4, d: 'bbb' }],
  356. })
  357. expect(historyUpdates[1]).to.deep.equal({
  358. doc: this.doc.id,
  359. meta: {
  360. pathname: '/a/b/c.tex',
  361. doc_length: 10,
  362. history_doc_length: 16,
  363. ts: historyUpdates[1].meta.ts,
  364. user_id: this.user_id,
  365. },
  366. op: [{ p: 5, d: 'ccc' }],
  367. })
  368. expect(historyUpdates[2]).to.deep.equal({
  369. doc: this.doc.id,
  370. meta: {
  371. pathname: '/a/b/c.tex',
  372. doc_length: 10,
  373. history_doc_length: 13,
  374. ts: historyUpdates[2].meta.ts,
  375. user_id: this.user_id,
  376. },
  377. op: [{ p: 6, d: 'ddd' }],
  378. })
  379. })
  380. it('accepting changes in reverse order', async function () {
  381. await DocUpdaterClient.acceptChanges(this.project_id, this.doc.id, [
  382. this.id_seed_3 + '000001',
  383. this.id_seed_2 + '000001',
  384. this.id_seed_1 + '000001',
  385. ])
  386. const historyUpdates = this.getHistoryUpdatesSpy.returnValues[0]
  387. expect(historyUpdates[0]).to.deep.equal({
  388. doc: this.doc.id,
  389. meta: {
  390. pathname: '/a/b/c.tex',
  391. doc_length: 10,
  392. history_doc_length: 19,
  393. ts: historyUpdates[0].meta.ts,
  394. user_id: this.user_id,
  395. },
  396. op: [{ p: 4, d: 'bbb' }],
  397. })
  398. expect(historyUpdates[1]).to.deep.equal({
  399. doc: this.doc.id,
  400. meta: {
  401. pathname: '/a/b/c.tex',
  402. doc_length: 10,
  403. history_doc_length: 16,
  404. ts: historyUpdates[1].meta.ts,
  405. user_id: this.user_id,
  406. },
  407. op: [{ p: 5, d: 'ccc' }],
  408. })
  409. expect(historyUpdates[2]).to.deep.equal({
  410. doc: this.doc.id,
  411. meta: {
  412. pathname: '/a/b/c.tex',
  413. doc_length: 10,
  414. history_doc_length: 13,
  415. ts: historyUpdates[2].meta.ts,
  416. user_id: this.user_id,
  417. },
  418. op: [{ p: 6, d: 'ddd' }],
  419. })
  420. })
  421. })
  422. describe('deleting a comment range', function () {
  423. before(async function () {
  424. this.project_id = DocUpdaterClient.randomId()
  425. this.user_id = DocUpdaterClient.randomId()
  426. this.doc = {
  427. id: DocUpdaterClient.randomId(),
  428. lines: ['foo bar'],
  429. }
  430. this.update = {
  431. doc: this.doc.id,
  432. op: [{ c: 'bar', p: 4, t: (this.tid = DocUpdaterClient.randomId()) }],
  433. v: 0,
  434. }
  435. MockWebApi.insertDoc(this.project_id, this.doc.id, {
  436. lines: this.doc.lines,
  437. version: 0,
  438. })
  439. await DocUpdaterClient.preloadDoc(this.project_id, this.doc.id)
  440. await DocUpdaterClient.sendUpdate(
  441. this.project_id,
  442. this.doc.id,
  443. this.update
  444. )
  445. await setTimeout(200)
  446. const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc.id)
  447. const { ranges } = doc
  448. const change = ranges.comments[0]
  449. change.op.should.deep.equal({ c: 'bar', p: 4, t: this.tid })
  450. change.id.should.equal(this.tid)
  451. })
  452. it('should remove the comment range', async function () {
  453. await DocUpdaterClient.removeComment(
  454. this.project_id,
  455. this.doc.id,
  456. this.tid
  457. )
  458. const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc.id)
  459. expect(doc.ranges.comments).to.be.undefined
  460. })
  461. })
  462. describe('tripping range size limit', function () {
  463. before(async function () {
  464. this.project_id = DocUpdaterClient.randomId()
  465. this.user_id = DocUpdaterClient.randomId()
  466. this.id_seed = DocUpdaterClient.randomId()
  467. this.doc = {
  468. id: DocUpdaterClient.randomId(),
  469. lines: ['aaa'],
  470. }
  471. this.i = new Array(3 * 1024 * 1024).join('a')
  472. this.updates = [
  473. {
  474. doc: this.doc.id,
  475. op: [{ i: this.i, p: 1 }],
  476. v: 0,
  477. meta: { user_id: this.user_id, tc: this.id_seed },
  478. },
  479. ]
  480. MockWebApi.insertDoc(this.project_id, this.doc.id, {
  481. lines: this.doc.lines,
  482. version: 0,
  483. })
  484. await DocUpdaterClient.preloadDoc(this.project_id, this.doc.id)
  485. for (const update of this.updates) {
  486. await DocUpdaterClient.sendUpdate(this.project_id, this.doc.id, update)
  487. }
  488. await setTimeout(200)
  489. })
  490. it('should not update the ranges', async function () {
  491. const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc.id)
  492. expect(doc.ranges.changes).to.be.undefined
  493. })
  494. })
  495. describe('deleting text surrounding a comment', function () {
  496. before(async function () {
  497. this.project_id = DocUpdaterClient.randomId()
  498. this.user_id = DocUpdaterClient.randomId()
  499. this.doc_id = DocUpdaterClient.randomId()
  500. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  501. lines: ['foo bar baz'],
  502. version: 0,
  503. ranges: {
  504. comments: [
  505. {
  506. op: {
  507. c: 'a',
  508. p: 5,
  509. tid: (this.tid = DocUpdaterClient.randomId()),
  510. },
  511. metadata: {
  512. user_id: this.user_id,
  513. ts: new Date(),
  514. },
  515. },
  516. ],
  517. },
  518. })
  519. this.updates = [
  520. {
  521. doc: this.doc_id,
  522. op: [{ d: 'foo ', p: 0 }],
  523. v: 0,
  524. meta: { user_id: this.user_id },
  525. },
  526. {
  527. doc: this.doc_id,
  528. op: [{ d: 'bar ', p: 0 }],
  529. v: 1,
  530. meta: { user_id: this.user_id },
  531. },
  532. ]
  533. await DocUpdaterClient.preloadDoc(this.project_id, this.doc_id)
  534. for (const update of this.updates) {
  535. await DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, update)
  536. }
  537. await setTimeout(200)
  538. await DocUpdaterClient.getDoc(this.project_id, this.doc_id)
  539. })
  540. it('should write a snapshot from before the destructive change', async function () {
  541. await DocUpdaterClient.getDoc(this.project_id, this.doc_id)
  542. const docSnapshots = await db.docSnapshots
  543. .find({
  544. project_id: new ObjectId(this.project_id),
  545. doc_id: new ObjectId(this.doc_id),
  546. })
  547. .toArray()
  548. expect(docSnapshots.length).to.equal(1)
  549. expect(docSnapshots[0].version).to.equal(1)
  550. expect(docSnapshots[0].lines).to.deep.equal(['bar baz'])
  551. expect(docSnapshots[0].ranges.comments[0].op).to.deep.equal({
  552. c: 'a',
  553. p: 1,
  554. tid: this.tid,
  555. })
  556. })
  557. })
  558. })