ApplyingUpdatesToADocTests.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862
  1. // TODO: This file was created by bulk-decaffeinate.
  2. // Fix any style issues and re-enable lint.
  3. /*
  4. * decaffeinate suggestions:
  5. * DS101: Remove unnecessary use of Array.from
  6. * DS102: Remove unnecessary code created because of implicit returns
  7. * DS207: Consider shorter variations of null checks
  8. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  9. */
  10. const sinon = require('sinon')
  11. const { expect } = require('chai')
  12. const async = require('async')
  13. const Settings = require('@overleaf/settings')
  14. const rclientHistory = require('@overleaf/redis-wrapper').createClient(
  15. Settings.redis.history
  16. ) // note: this is track changes, not project-history
  17. const rclientProjectHistory = require('@overleaf/redis-wrapper').createClient(
  18. Settings.redis.project_history
  19. )
  20. const rclientDU = require('@overleaf/redis-wrapper').createClient(
  21. Settings.redis.documentupdater
  22. )
  23. const Keys = Settings.redis.documentupdater.key_schema
  24. const HistoryKeys = Settings.redis.history.key_schema
  25. const ProjectHistoryKeys = Settings.redis.project_history.key_schema
  26. const MockTrackChangesApi = require('./helpers/MockTrackChangesApi')
  27. const MockWebApi = require('./helpers/MockWebApi')
  28. const DocUpdaterClient = require('./helpers/DocUpdaterClient')
  29. const DocUpdaterApp = require('./helpers/DocUpdaterApp')
  30. describe('Applying updates to a doc', function () {
  31. before(function (done) {
  32. this.lines = ['one', 'two', 'three']
  33. this.version = 42
  34. this.update = {
  35. doc: this.doc_id,
  36. op: [
  37. {
  38. i: 'one and a half\n',
  39. p: 4,
  40. },
  41. ],
  42. v: this.version,
  43. }
  44. this.result = ['one', 'one and a half', 'two', 'three']
  45. return DocUpdaterApp.ensureRunning(done)
  46. })
  47. describe('when the document is not loaded', function () {
  48. before(function (done) {
  49. ;[this.project_id, this.doc_id] = Array.from([
  50. DocUpdaterClient.randomId(),
  51. DocUpdaterClient.randomId(),
  52. ])
  53. sinon.spy(MockWebApi, 'getDocument')
  54. this.startTime = Date.now()
  55. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  56. lines: this.lines,
  57. version: this.version,
  58. })
  59. DocUpdaterClient.sendUpdate(
  60. this.project_id,
  61. this.doc_id,
  62. this.update,
  63. error => {
  64. if (error != null) {
  65. throw error
  66. }
  67. return setTimeout(done, 200)
  68. }
  69. )
  70. return null
  71. })
  72. after(function () {
  73. return MockWebApi.getDocument.restore()
  74. })
  75. it('should load the document from the web API', function () {
  76. return MockWebApi.getDocument
  77. .calledWith(this.project_id, this.doc_id)
  78. .should.equal(true)
  79. })
  80. it('should update the doc', function (done) {
  81. DocUpdaterClient.getDoc(
  82. this.project_id,
  83. this.doc_id,
  84. (error, res, doc) => {
  85. if (error) return done(error)
  86. doc.lines.should.deep.equal(this.result)
  87. return done()
  88. }
  89. )
  90. return null
  91. })
  92. it('should push the applied updates to the track changes api', function (done) {
  93. rclientHistory.lrange(
  94. HistoryKeys.uncompressedHistoryOps({ doc_id: this.doc_id }),
  95. 0,
  96. -1,
  97. (error, updates) => {
  98. if (error != null) {
  99. throw error
  100. }
  101. JSON.parse(updates[0]).op.should.deep.equal(this.update.op)
  102. return rclientHistory.sismember(
  103. HistoryKeys.docsWithHistoryOps({ project_id: this.project_id }),
  104. this.doc_id,
  105. (error, result) => {
  106. if (error != null) {
  107. throw error
  108. }
  109. result.should.equal(1)
  110. return done()
  111. }
  112. )
  113. }
  114. )
  115. return null
  116. })
  117. it('should push the applied updates to the project history changes api', function (done) {
  118. rclientProjectHistory.lrange(
  119. ProjectHistoryKeys.projectHistoryOps({ project_id: this.project_id }),
  120. 0,
  121. -1,
  122. (error, updates) => {
  123. if (error != null) {
  124. throw error
  125. }
  126. JSON.parse(updates[0]).op.should.deep.equal(this.update.op)
  127. return done()
  128. }
  129. )
  130. return null
  131. })
  132. it('should set the first op timestamp', function (done) {
  133. rclientProjectHistory.get(
  134. ProjectHistoryKeys.projectHistoryFirstOpTimestamp({
  135. project_id: this.project_id,
  136. }),
  137. (error, result) => {
  138. if (error != null) {
  139. throw error
  140. }
  141. result = parseInt(result, 10)
  142. result.should.be.within(this.startTime, Date.now())
  143. this.firstOpTimestamp = result
  144. return done()
  145. }
  146. )
  147. return null
  148. })
  149. return describe('when sending another update', function () {
  150. before(function (done) {
  151. this.timeout = 10000
  152. this.second_update = Object.create(this.update)
  153. this.second_update.v = this.version + 1
  154. DocUpdaterClient.sendUpdate(
  155. this.project_id,
  156. this.doc_id,
  157. this.second_update,
  158. error => {
  159. if (error != null) {
  160. throw error
  161. }
  162. return setTimeout(done, 200)
  163. }
  164. )
  165. return null
  166. })
  167. return it('should not change the first op timestamp', function (done) {
  168. rclientProjectHistory.get(
  169. ProjectHistoryKeys.projectHistoryFirstOpTimestamp({
  170. project_id: this.project_id,
  171. }),
  172. (error, result) => {
  173. if (error != null) {
  174. throw error
  175. }
  176. result = parseInt(result, 10)
  177. result.should.equal(this.firstOpTimestamp)
  178. return done()
  179. }
  180. )
  181. return null
  182. })
  183. })
  184. })
  185. describe('when the document is loaded', function () {
  186. before(function (done) {
  187. ;[this.project_id, this.doc_id] = Array.from([
  188. DocUpdaterClient.randomId(),
  189. DocUpdaterClient.randomId(),
  190. ])
  191. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  192. lines: this.lines,
  193. version: this.version,
  194. })
  195. DocUpdaterClient.preloadDoc(this.project_id, this.doc_id, error => {
  196. if (error != null) {
  197. throw error
  198. }
  199. sinon.spy(MockWebApi, 'getDocument')
  200. return DocUpdaterClient.sendUpdate(
  201. this.project_id,
  202. this.doc_id,
  203. this.update,
  204. error => {
  205. if (error != null) {
  206. throw error
  207. }
  208. return setTimeout(done, 200)
  209. }
  210. )
  211. })
  212. return null
  213. })
  214. after(function () {
  215. return MockWebApi.getDocument.restore()
  216. })
  217. it('should not need to call the web api', function () {
  218. return MockWebApi.getDocument.called.should.equal(false)
  219. })
  220. it('should update the doc', function (done) {
  221. DocUpdaterClient.getDoc(
  222. this.project_id,
  223. this.doc_id,
  224. (error, res, doc) => {
  225. if (error) return done(error)
  226. doc.lines.should.deep.equal(this.result)
  227. return done()
  228. }
  229. )
  230. return null
  231. })
  232. it('should push the applied updates to the track changes api', function (done) {
  233. rclientHistory.lrange(
  234. HistoryKeys.uncompressedHistoryOps({ doc_id: this.doc_id }),
  235. 0,
  236. -1,
  237. (error, updates) => {
  238. if (error) return done(error)
  239. JSON.parse(updates[0]).op.should.deep.equal(this.update.op)
  240. return rclientHistory.sismember(
  241. HistoryKeys.docsWithHistoryOps({ project_id: this.project_id }),
  242. this.doc_id,
  243. (error, result) => {
  244. if (error) return done(error)
  245. result.should.equal(1)
  246. return done()
  247. }
  248. )
  249. }
  250. )
  251. return null
  252. })
  253. return it('should push the applied updates to the project history changes api', function (done) {
  254. rclientProjectHistory.lrange(
  255. ProjectHistoryKeys.projectHistoryOps({ project_id: this.project_id }),
  256. 0,
  257. -1,
  258. (error, updates) => {
  259. if (error) return done(error)
  260. JSON.parse(updates[0]).op.should.deep.equal(this.update.op)
  261. return done()
  262. }
  263. )
  264. return null
  265. })
  266. })
  267. describe('when the document is loaded and is using project-history only', function () {
  268. before(function (done) {
  269. ;[this.project_id, this.doc_id] = Array.from([
  270. DocUpdaterClient.randomId(),
  271. DocUpdaterClient.randomId(),
  272. ])
  273. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  274. lines: this.lines,
  275. version: this.version,
  276. projectHistoryType: 'project-history',
  277. })
  278. DocUpdaterClient.preloadDoc(this.project_id, this.doc_id, error => {
  279. if (error != null) {
  280. throw error
  281. }
  282. sinon.spy(MockWebApi, 'getDocument')
  283. return DocUpdaterClient.sendUpdate(
  284. this.project_id,
  285. this.doc_id,
  286. this.update,
  287. error => {
  288. if (error != null) {
  289. throw error
  290. }
  291. return setTimeout(done, 200)
  292. }
  293. )
  294. })
  295. return null
  296. })
  297. after(function () {
  298. return MockWebApi.getDocument.restore()
  299. })
  300. it('should update the doc', function (done) {
  301. DocUpdaterClient.getDoc(
  302. this.project_id,
  303. this.doc_id,
  304. (error, res, doc) => {
  305. if (error) return done(error)
  306. doc.lines.should.deep.equal(this.result)
  307. return done()
  308. }
  309. )
  310. return null
  311. })
  312. it('should not push any applied updates to the track changes api', function (done) {
  313. rclientHistory.lrange(
  314. HistoryKeys.uncompressedHistoryOps({ doc_id: this.doc_id }),
  315. 0,
  316. -1,
  317. (error, updates) => {
  318. if (error) return done(error)
  319. updates.length.should.equal(0)
  320. return done()
  321. }
  322. )
  323. return null
  324. })
  325. return it('should push the applied updates to the project history changes api', function (done) {
  326. rclientProjectHistory.lrange(
  327. ProjectHistoryKeys.projectHistoryOps({ project_id: this.project_id }),
  328. 0,
  329. -1,
  330. (error, updates) => {
  331. if (error) return done(error)
  332. JSON.parse(updates[0]).op.should.deep.equal(this.update.op)
  333. return done()
  334. }
  335. )
  336. return null
  337. })
  338. })
  339. describe('when the document has been deleted', function () {
  340. describe('when the ops come in a single linear order', function () {
  341. before(function (done) {
  342. ;[this.project_id, this.doc_id] = Array.from([
  343. DocUpdaterClient.randomId(),
  344. DocUpdaterClient.randomId(),
  345. ])
  346. const lines = ['', '', '']
  347. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  348. lines,
  349. version: 0,
  350. })
  351. this.updates = [
  352. { doc_id: this.doc_id, v: 0, op: [{ i: 'h', p: 0 }] },
  353. { doc_id: this.doc_id, v: 1, op: [{ i: 'e', p: 1 }] },
  354. { doc_id: this.doc_id, v: 2, op: [{ i: 'l', p: 2 }] },
  355. { doc_id: this.doc_id, v: 3, op: [{ i: 'l', p: 3 }] },
  356. { doc_id: this.doc_id, v: 4, op: [{ i: 'o', p: 4 }] },
  357. { doc_id: this.doc_id, v: 5, op: [{ i: ' ', p: 5 }] },
  358. { doc_id: this.doc_id, v: 6, op: [{ i: 'w', p: 6 }] },
  359. { doc_id: this.doc_id, v: 7, op: [{ i: 'o', p: 7 }] },
  360. { doc_id: this.doc_id, v: 8, op: [{ i: 'r', p: 8 }] },
  361. { doc_id: this.doc_id, v: 9, op: [{ i: 'l', p: 9 }] },
  362. { doc_id: this.doc_id, v: 10, op: [{ i: 'd', p: 10 }] },
  363. ]
  364. this.my_result = ['hello world', '', '']
  365. return done()
  366. })
  367. it('should be able to continue applying updates when the project has been deleted', function (done) {
  368. let update
  369. const actions = []
  370. for (update of Array.from(this.updates.slice(0, 6))) {
  371. ;(update => {
  372. return actions.push(callback =>
  373. DocUpdaterClient.sendUpdate(
  374. this.project_id,
  375. this.doc_id,
  376. update,
  377. callback
  378. )
  379. )
  380. })(update)
  381. }
  382. actions.push(callback =>
  383. DocUpdaterClient.deleteDoc(this.project_id, this.doc_id, callback)
  384. )
  385. for (update of Array.from(this.updates.slice(6))) {
  386. ;(update => {
  387. return actions.push(callback =>
  388. DocUpdaterClient.sendUpdate(
  389. this.project_id,
  390. this.doc_id,
  391. update,
  392. callback
  393. )
  394. )
  395. })(update)
  396. }
  397. async.series(actions, error => {
  398. if (error != null) {
  399. throw error
  400. }
  401. return DocUpdaterClient.getDoc(
  402. this.project_id,
  403. this.doc_id,
  404. (error, res, doc) => {
  405. if (error) return done(error)
  406. doc.lines.should.deep.equal(this.my_result)
  407. return done()
  408. }
  409. )
  410. })
  411. return null
  412. })
  413. it('should push the applied updates to the track changes api', function (done) {
  414. rclientHistory.lrange(
  415. HistoryKeys.uncompressedHistoryOps({ doc_id: this.doc_id }),
  416. 0,
  417. -1,
  418. (error, updates) => {
  419. if (error) return done(error)
  420. updates = Array.from(updates).map(u => JSON.parse(u))
  421. for (let i = 0; i < this.updates.length; i++) {
  422. const appliedUpdate = this.updates[i]
  423. appliedUpdate.op.should.deep.equal(updates[i].op)
  424. }
  425. return rclientHistory.sismember(
  426. HistoryKeys.docsWithHistoryOps({ project_id: this.project_id }),
  427. this.doc_id,
  428. (error, result) => {
  429. if (error) return done(error)
  430. result.should.equal(1)
  431. return done()
  432. }
  433. )
  434. }
  435. )
  436. return null
  437. })
  438. return it('should store the doc ops in the correct order', function (done) {
  439. rclientDU.lrange(
  440. Keys.docOps({ doc_id: this.doc_id }),
  441. 0,
  442. -1,
  443. (error, updates) => {
  444. if (error) return done(error)
  445. updates = Array.from(updates).map(u => JSON.parse(u))
  446. for (let i = 0; i < this.updates.length; i++) {
  447. const appliedUpdate = this.updates[i]
  448. appliedUpdate.op.should.deep.equal(updates[i].op)
  449. }
  450. return done()
  451. }
  452. )
  453. return null
  454. })
  455. })
  456. return describe('when older ops come in after the delete', function () {
  457. before(function (done) {
  458. ;[this.project_id, this.doc_id] = Array.from([
  459. DocUpdaterClient.randomId(),
  460. DocUpdaterClient.randomId(),
  461. ])
  462. const lines = ['', '', '']
  463. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  464. lines,
  465. version: 0,
  466. })
  467. this.updates = [
  468. { doc_id: this.doc_id, v: 0, op: [{ i: 'h', p: 0 }] },
  469. { doc_id: this.doc_id, v: 1, op: [{ i: 'e', p: 1 }] },
  470. { doc_id: this.doc_id, v: 2, op: [{ i: 'l', p: 2 }] },
  471. { doc_id: this.doc_id, v: 3, op: [{ i: 'l', p: 3 }] },
  472. { doc_id: this.doc_id, v: 4, op: [{ i: 'o', p: 4 }] },
  473. { doc_id: this.doc_id, v: 0, op: [{ i: 'world', p: 1 }] },
  474. ]
  475. this.my_result = ['hello', 'world', '']
  476. return done()
  477. })
  478. return it('should be able to continue applying updates when the project has been deleted', function (done) {
  479. let update
  480. const actions = []
  481. for (update of Array.from(this.updates.slice(0, 5))) {
  482. ;(update => {
  483. return actions.push(callback =>
  484. DocUpdaterClient.sendUpdate(
  485. this.project_id,
  486. this.doc_id,
  487. update,
  488. callback
  489. )
  490. )
  491. })(update)
  492. }
  493. actions.push(callback =>
  494. DocUpdaterClient.deleteDoc(this.project_id, this.doc_id, callback)
  495. )
  496. for (update of Array.from(this.updates.slice(5))) {
  497. ;(update => {
  498. return actions.push(callback =>
  499. DocUpdaterClient.sendUpdate(
  500. this.project_id,
  501. this.doc_id,
  502. update,
  503. callback
  504. )
  505. )
  506. })(update)
  507. }
  508. async.series(actions, error => {
  509. if (error != null) {
  510. throw error
  511. }
  512. return DocUpdaterClient.getDoc(
  513. this.project_id,
  514. this.doc_id,
  515. (error, res, doc) => {
  516. if (error) return done(error)
  517. doc.lines.should.deep.equal(this.my_result)
  518. return done()
  519. }
  520. )
  521. })
  522. return null
  523. })
  524. })
  525. })
  526. describe('with a broken update', function () {
  527. before(function (done) {
  528. ;[this.project_id, this.doc_id] = Array.from([
  529. DocUpdaterClient.randomId(),
  530. DocUpdaterClient.randomId(),
  531. ])
  532. this.broken_update = {
  533. doc_id: this.doc_id,
  534. v: this.version,
  535. op: [{ d: 'not the correct content', p: 0 }],
  536. }
  537. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  538. lines: this.lines,
  539. version: this.version,
  540. })
  541. DocUpdaterClient.subscribeToAppliedOps(
  542. (this.messageCallback = sinon.stub())
  543. )
  544. DocUpdaterClient.sendUpdate(
  545. this.project_id,
  546. this.doc_id,
  547. this.broken_update,
  548. error => {
  549. if (error != null) {
  550. throw error
  551. }
  552. return setTimeout(done, 200)
  553. }
  554. )
  555. return null
  556. })
  557. it('should not update the doc', function (done) {
  558. DocUpdaterClient.getDoc(
  559. this.project_id,
  560. this.doc_id,
  561. (error, res, doc) => {
  562. if (error) return done(error)
  563. doc.lines.should.deep.equal(this.lines)
  564. return done()
  565. }
  566. )
  567. return null
  568. })
  569. return it('should send a message with an error', function () {
  570. this.messageCallback.called.should.equal(true)
  571. const [channel, message] = Array.from(this.messageCallback.args[0])
  572. channel.should.equal('applied-ops')
  573. return JSON.parse(message).should.deep.include({
  574. project_id: this.project_id,
  575. doc_id: this.doc_id,
  576. error: 'Delete component does not match',
  577. })
  578. })
  579. })
  580. describe('with enough updates to flush to the track changes api', function () {
  581. before(function (done) {
  582. ;[this.project_id, this.doc_id] = Array.from([
  583. DocUpdaterClient.randomId(),
  584. DocUpdaterClient.randomId(),
  585. ])
  586. const updates = []
  587. for (let v = 0; v <= 199; v++) {
  588. // Should flush after 100 ops
  589. updates.push({
  590. doc_id: this.doc_id,
  591. op: [{ i: v.toString(), p: 0 }],
  592. v,
  593. })
  594. }
  595. sinon.spy(MockTrackChangesApi, 'flushDoc')
  596. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  597. lines: this.lines,
  598. version: 0,
  599. })
  600. // Send updates in chunks to causes multiple flushes
  601. const actions = []
  602. for (let i = 0; i <= 19; i++) {
  603. ;(i => {
  604. return actions.push(cb => {
  605. return DocUpdaterClient.sendUpdates(
  606. this.project_id,
  607. this.doc_id,
  608. updates.slice(i * 10, (i + 1) * 10),
  609. cb
  610. )
  611. })
  612. })(i)
  613. }
  614. async.series(actions, error => {
  615. if (error != null) {
  616. throw error
  617. }
  618. return setTimeout(done, 2000)
  619. })
  620. return null
  621. })
  622. after(function () {
  623. return MockTrackChangesApi.flushDoc.restore()
  624. })
  625. return it('should flush the doc twice', function () {
  626. return MockTrackChangesApi.flushDoc.calledTwice.should.equal(true)
  627. })
  628. })
  629. describe('when there is no version in Mongo', function () {
  630. before(function (done) {
  631. ;[this.project_id, this.doc_id] = Array.from([
  632. DocUpdaterClient.randomId(),
  633. DocUpdaterClient.randomId(),
  634. ])
  635. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  636. lines: this.lines,
  637. })
  638. const update = {
  639. doc: this.doc_id,
  640. op: this.update.op,
  641. v: 0,
  642. }
  643. DocUpdaterClient.sendUpdate(
  644. this.project_id,
  645. this.doc_id,
  646. update,
  647. error => {
  648. if (error != null) {
  649. throw error
  650. }
  651. return setTimeout(done, 200)
  652. }
  653. )
  654. return null
  655. })
  656. return it('should update the doc (using version = 0)', function (done) {
  657. DocUpdaterClient.getDoc(
  658. this.project_id,
  659. this.doc_id,
  660. (error, res, doc) => {
  661. if (error) return done(error)
  662. doc.lines.should.deep.equal(this.result)
  663. return done()
  664. }
  665. )
  666. return null
  667. })
  668. })
  669. describe('when the sending duplicate ops', function () {
  670. before(function (done) {
  671. ;[this.project_id, this.doc_id] = Array.from([
  672. DocUpdaterClient.randomId(),
  673. DocUpdaterClient.randomId(),
  674. ])
  675. MockWebApi.insertDoc(this.project_id, this.doc_id, {
  676. lines: this.lines,
  677. version: this.version,
  678. })
  679. DocUpdaterClient.subscribeToAppliedOps(
  680. (this.messageCallback = sinon.stub())
  681. )
  682. // One user delete 'one', the next turns it into 'once'. The second becomes a NOP.
  683. DocUpdaterClient.sendUpdate(
  684. this.project_id,
  685. this.doc_id,
  686. {
  687. doc: this.doc_id,
  688. op: [
  689. {
  690. i: 'one and a half\n',
  691. p: 4,
  692. },
  693. ],
  694. v: this.version,
  695. meta: {
  696. source: 'ikHceq3yfAdQYzBo4-xZ',
  697. },
  698. },
  699. error => {
  700. if (error != null) {
  701. throw error
  702. }
  703. return setTimeout(() => {
  704. return DocUpdaterClient.sendUpdate(
  705. this.project_id,
  706. this.doc_id,
  707. {
  708. doc: this.doc_id,
  709. op: [
  710. {
  711. i: 'one and a half\n',
  712. p: 4,
  713. },
  714. ],
  715. v: this.version,
  716. dupIfSource: ['ikHceq3yfAdQYzBo4-xZ'],
  717. meta: {
  718. source: 'ikHceq3yfAdQYzBo4-xZ',
  719. },
  720. },
  721. error => {
  722. if (error != null) {
  723. throw error
  724. }
  725. return setTimeout(done, 200)
  726. }
  727. )
  728. }, 200)
  729. }
  730. )
  731. return null
  732. })
  733. it('should update the doc', function (done) {
  734. DocUpdaterClient.getDoc(
  735. this.project_id,
  736. this.doc_id,
  737. (error, res, doc) => {
  738. if (error) return done(error)
  739. doc.lines.should.deep.equal(this.result)
  740. return done()
  741. }
  742. )
  743. return null
  744. })
  745. return it('should return a message about duplicate ops', function () {
  746. this.messageCallback.calledTwice.should.equal(true)
  747. this.messageCallback.args[0][0].should.equal('applied-ops')
  748. expect(JSON.parse(this.messageCallback.args[0][1]).op.dup).to.be.undefined
  749. this.messageCallback.args[1][0].should.equal('applied-ops')
  750. return expect(
  751. JSON.parse(this.messageCallback.args[1][1]).op.dup
  752. ).to.equal(true)
  753. })
  754. })
  755. return describe('when sending updates for a non-existing doc id', function () {
  756. before(function (done) {
  757. ;[this.project_id, this.doc_id] = Array.from([
  758. DocUpdaterClient.randomId(),
  759. DocUpdaterClient.randomId(),
  760. ])
  761. this.non_existing = {
  762. doc_id: this.doc_id,
  763. v: this.version,
  764. op: [{ d: 'content', p: 0 }],
  765. }
  766. DocUpdaterClient.subscribeToAppliedOps(
  767. (this.messageCallback = sinon.stub())
  768. )
  769. DocUpdaterClient.sendUpdate(
  770. this.project_id,
  771. this.doc_id,
  772. this.non_existing,
  773. error => {
  774. if (error != null) {
  775. throw error
  776. }
  777. return setTimeout(done, 200)
  778. }
  779. )
  780. return null
  781. })
  782. it('should not update or create a doc', function (done) {
  783. DocUpdaterClient.getDoc(
  784. this.project_id,
  785. this.doc_id,
  786. (error, res, doc) => {
  787. if (error) return done(error)
  788. res.statusCode.should.equal(404)
  789. return done()
  790. }
  791. )
  792. return null
  793. })
  794. return it('should send a message with an error', function () {
  795. this.messageCallback.called.should.equal(true)
  796. const [channel, message] = Array.from(this.messageCallback.args[0])
  797. channel.should.equal('applied-ops')
  798. return JSON.parse(message).should.deep.include({
  799. project_id: this.project_id,
  800. doc_id: this.doc_id,
  801. error: `doc not not found: /project/${this.project_id}/doc/${this.doc_id}`,
  802. })
  803. })
  804. })
  805. })