ot_client.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. 'use strict'
  2. const _ = require('lodash')
  3. const ChangeNote = require('./change_note')
  4. const ChangeRequest = require('./change_request')
  5. const Chunk = require('./chunk')
  6. const Operation = require('./operation')
  7. /**
  8. * Operational Transformation client.
  9. *
  10. * See OT.md for explanation.
  11. */
  12. class OtClient {
  13. constructor(_projectId, _editor, _blobStore, _socket) {
  14. const STATE_DISCONNECTED = 0
  15. const STATE_LOADING = 1
  16. const STATE_READY = 2
  17. const STATE_WAITING = 3
  18. let _version = null
  19. let _state = STATE_DISCONNECTED
  20. const _buffer = []
  21. let _ackVersion = null
  22. let _outstanding = []
  23. let _pending = []
  24. const _waiting = []
  25. this.connect = function otClientConnect() {
  26. switch (_state) {
  27. case STATE_DISCONNECTED:
  28. _state = STATE_LOADING
  29. _socket.emit('authenticate', {
  30. projectId: _projectId,
  31. token: 'letmein',
  32. })
  33. break
  34. default:
  35. throw new Error('connect in state ' + _state)
  36. }
  37. }
  38. /**
  39. * The latest project version number for which the client can construct the
  40. * project content.
  41. *
  42. * @return {number} non-negative
  43. */
  44. this.getVersion = function () {
  45. return _version
  46. }
  47. _socket.on('load', function otClientOnLoad(data) {
  48. switch (_state) {
  49. case STATE_LOADING: {
  50. const chunk = Chunk.fromRaw(data)
  51. const snapshot = chunk.getSnapshot()
  52. snapshot.applyAll(chunk.getChanges(), { strict: true })
  53. _version = chunk.getEndVersion()
  54. // TODO: we can get remote changes here, so it's not correct to wait for
  55. // the editor to load before transitioning to the READY state
  56. _editor.load(snapshot).then(function () {
  57. _state = STATE_READY
  58. })
  59. break
  60. }
  61. default:
  62. throw new Error('loaded in state ' + _state)
  63. }
  64. })
  65. //
  66. // Local Operations
  67. //
  68. function sendOutstandingChange() {
  69. const changeRequest = new ChangeRequest(_version, _outstanding)
  70. _socket.emit('change', changeRequest.toRaw())
  71. _state = STATE_WAITING
  72. }
  73. function sendLocalOperation(operation) {
  74. _outstanding.push(operation)
  75. sendOutstandingChange()
  76. }
  77. function queueLocalOperation(operation) {
  78. _pending.push(operation)
  79. }
  80. this.handleLocalOperation = function otClientHandleLocalOperation(
  81. operation
  82. ) {
  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 Promise(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. }
  213. module.exports = OtClient