ApplyingUpdatesToADocTests.js 25 KB

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