ot_client.js 6.4 KB

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