ot_client.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. 'use strict'
  2. const _ = require('lodash')
  3. const BPromise = require('bluebird')
  4. const ChangeNote = require('./change_note')
  5. const ChangeRequest = require('./change_request')
  6. const Chunk = require('./chunk')
  7. const Operation = require('./operation')
  8. /**
  9. * @class
  10. * @classdesc
  11. * Operational Transformation client.
  12. *
  13. * See OT.md for explanation.
  14. */
  15. function OtClient(_projectId, _editor, _blobStore, _socket) {
  16. const STATE_DISCONNECTED = 0
  17. const STATE_LOADING = 1
  18. const STATE_READY = 2
  19. const STATE_WAITING = 3
  20. let _version = null
  21. let _state = STATE_DISCONNECTED
  22. const _buffer = []
  23. let _ackVersion = null
  24. let _outstanding = []
  25. let _pending = []
  26. const _waiting = []
  27. this.connect = function otClientConnect() {
  28. switch (_state) {
  29. case STATE_DISCONNECTED:
  30. _state = STATE_LOADING
  31. _socket.emit('authenticate', {
  32. projectId: _projectId,
  33. token: 'letmein',
  34. })
  35. break
  36. default:
  37. throw new Error('connect in state ' + _state)
  38. }
  39. }
  40. /**
  41. * The latest project version number for which the client can construct the
  42. * project content.
  43. *
  44. * @return {number} non-negative
  45. */
  46. this.getVersion = function () {
  47. return _version
  48. }
  49. _socket.on('load', function otClientOnLoad(data) {
  50. switch (_state) {
  51. case STATE_LOADING: {
  52. const chunk = Chunk.fromRaw(data)
  53. const snapshot = chunk.getSnapshot()
  54. snapshot.applyAll(chunk.getChanges(), { strict: true })
  55. _version = chunk.getEndVersion()
  56. // TODO: we can get remote changes here, so it's not correct to wait for
  57. // the editor to load before transitioning to the READY state
  58. _editor.load(snapshot).then(function () {
  59. _state = STATE_READY
  60. })
  61. break
  62. }
  63. default:
  64. throw new Error('loaded in state ' + _state)
  65. }
  66. })
  67. //
  68. // Local Operations
  69. //
  70. function sendOutstandingChange() {
  71. const changeRequest = new ChangeRequest(_version, _outstanding)
  72. _socket.emit('change', changeRequest.toRaw())
  73. _state = STATE_WAITING
  74. }
  75. function sendLocalOperation(operation) {
  76. _outstanding.push(operation)
  77. sendOutstandingChange()
  78. }
  79. function queueLocalOperation(operation) {
  80. _pending.push(operation)
  81. }
  82. this.handleLocalOperation = function otClientHandleLocalOperation(operation) {
  83. switch (_state) {
  84. case STATE_READY:
  85. sendLocalOperation(operation)
  86. break
  87. case STATE_WAITING:
  88. queueLocalOperation(operation)
  89. break
  90. default:
  91. throw new Error('local operation in state ' + _state)
  92. }
  93. }
  94. /**
  95. * A promise that resolves when the project reaches the given version.
  96. *
  97. * @param {number} version non-negative
  98. * @return {Promise}
  99. */
  100. this.waitForVersion = function otClientWaitForVersion(version) {
  101. if (!_waiting[version]) _waiting[version] = []
  102. return new BPromise(function (resolve, reject) {
  103. _waiting[version].push(resolve)
  104. })
  105. }
  106. function resolveWaitingPromises() {
  107. for (const version in _waiting) {
  108. if (!Object.prototype.hasOwnProperty.call(_waiting, version)) continue
  109. if (version > _version) continue
  110. _waiting[version].forEach(function (resolve) {
  111. resolve()
  112. })
  113. delete _waiting[version]
  114. }
  115. }
  116. //
  117. // Messages from Server
  118. //
  119. function advanceIfReady() {
  120. if (_ackVersion !== null && _version === _ackVersion) {
  121. _version += 1
  122. _ackVersion = null
  123. handleAckReady()
  124. advanceIfReady()
  125. return
  126. }
  127. const changeNotes = _.remove(_buffer, function (changeNote) {
  128. return changeNote.getBaseVersion() === _version
  129. })
  130. if (changeNotes.length === 1) {
  131. handleRemoteChangeReady(changeNotes[0].getChange())
  132. _version += 1
  133. advanceIfReady()
  134. return
  135. }
  136. if (changeNotes.length !== 0) {
  137. throw new Error('multiple remote changes in client version ' + _version)
  138. }
  139. }
  140. function bufferRemoteChangeNote(changeNote) {
  141. const version = changeNote.getBaseVersion()
  142. if (_.find(_buffer, 'baseVersion', version)) {
  143. throw new Error('multiple changes in version ' + version)
  144. }
  145. if (version === _ackVersion) {
  146. throw new Error('received change that was acked in ' + _ackVersion)
  147. }
  148. _buffer.push(changeNote)
  149. }
  150. function handleAckReady() {
  151. // console.log('handleAckReady')
  152. if (_outstanding.length === 0) {
  153. throw new Error('ack complete without outstanding change')
  154. }
  155. if (_state !== STATE_WAITING) {
  156. throw new Error('ack complete in state ' + _state)
  157. }
  158. _editor.handleChangeAcknowledged()
  159. resolveWaitingPromises()
  160. if (_pending.length > 0) {
  161. _outstanding = _pending
  162. _pending = []
  163. sendOutstandingChange()
  164. } else {
  165. _outstanding = []
  166. _state = STATE_READY
  167. }
  168. }
  169. function handleRemoteChangeReady(change) {
  170. if (_pending.length > 0) {
  171. if (_outstanding.length === 0) {
  172. throw new Error('pending change without outstanding change')
  173. }
  174. }
  175. Operation.transformMultiple(_outstanding, change.getOperations())
  176. Operation.transformMultiple(_pending, change.getOperations())
  177. _editor.applyRemoteChange(change)
  178. }
  179. _socket.on('ack', function otClientOnAck(data) {
  180. switch (_state) {
  181. case STATE_WAITING: {
  182. const changeNote = ChangeNote.fromRaw(data)
  183. _ackVersion = changeNote.getBaseVersion()
  184. advanceIfReady()
  185. break
  186. }
  187. default:
  188. throw new Error('ack in state ' + _state)
  189. }
  190. })
  191. _socket.on('change', function otClientOnChange(data) {
  192. switch (_state) {
  193. case STATE_READY:
  194. case STATE_WAITING:
  195. bufferRemoteChangeNote(ChangeNote.fromRaw(data))
  196. advanceIfReady()
  197. break
  198. default:
  199. throw new Error('remote change in state ' + _state)
  200. }
  201. })
  202. //
  203. // Connection State
  204. // TODO: socket.io error handling
  205. //
  206. _socket.on('disconnect', function () {
  207. _state = STATE_DISCONNECTED
  208. // eslint-disable-next-line no-console
  209. console.log('disconnected') // TODO: how do we handle disconnect?
  210. })
  211. }
  212. module.exports = OtClient