define [ "utils/EventEmitter" "ide/editor/ShareJsDoc" ], (EventEmitter, ShareJsDoc) -> class Document extends EventEmitter @getDocument: (ide, doc_id) -> @openDocs ||= {} if !@openDocs[doc_id]? @openDocs[doc_id] = new Document(ide, doc_id) return @openDocs[doc_id] @hasUnsavedChanges: () -> for doc_id, doc of (@openDocs or {}) return true if doc.hasBufferedOps() return false constructor: (@ide, @doc_id) -> @connected = @ide.socket.socket.connected @joined = false @wantToBeJoined = false @_checkConsistency = _.bind(@_checkConsistency, @) @inconsistentCount = 0 @_bindToEditorEvents() @_bindToSocketEvents() attachToAce: (@ace) -> @doc?.attachToAce(@ace) editorDoc = @ace.getSession().getDocument() editorDoc.on "change", @_checkConsistency detachFromAce: () -> @doc?.detachFromAce() editorDoc = @ace?.getSession().getDocument() editorDoc?.off "change", @_checkConsistency _checkConsistency: () -> # We've been seeing a lot of errors when I think there shouldn't be # any, which may be related to this check happening before the change is # applied. If we use a timeout, hopefully we can reduce this. setTimeout () => editorValue = @ace?.getValue() sharejsValue = @doc?.getSnapshot() if editorValue != sharejsValue @inconsistentCount++ else @inconsistentCount = 0 if @inconsistentCount >= 3 @_onError new Error("Editor text does not match server text") , 0 getSnapshot: () -> @doc?.getSnapshot() getType: () -> @doc?.getType() getInflightOp: () -> @doc?.getInflightOp() getPendingOp: () -> @doc?.getPendingOp() hasBufferedOps: () -> @doc?.hasBufferedOps() _bindToSocketEvents: () -> @_onUpdateAppliedHandler = (update) => @_onUpdateApplied(update) @ide.socket.on "otUpdateApplied", @_onUpdateAppliedHandler @_onErrorHandler = (error, update) => @_onError(error, update) @ide.socket.on "otUpdateError", @_onErrorHandler @_onDisconnectHandler = (error) => @_onDisconnect(error) @ide.socket.on "disconnect", @_onDisconnectHandler _bindToEditorEvents: () -> onReconnectHandler = (update) => @_onReconnect(update) @_unsubscribeReconnectHandler = @ide.$scope.$on "project:joined", onReconnectHandler _unBindFromEditorEvents: () -> @_unsubscribeReconnectHandler() _unBindFromSocketEvents: () -> @ide.socket.removeListener "otUpdateApplied", @_onUpdateAppliedHandler @ide.socket.removeListener "otUpdateError", @_onUpdateErrorHandler @ide.socket.removeListener "disconnect", @_onDisconnectHandler leaveAndCleanUp: () -> @leave (error) => @_cleanUp() join: (callback = (error) ->) -> @wantToBeJoined = true @_cancelLeave() if @connected return @_joinDoc callback else @_joinCallbacks ||= [] @_joinCallbacks.push callback leave: (callback = (error) ->) -> @wantToBeJoined = false @_cancelJoin() if (@doc? and @doc.hasBufferedOps()) @_leaveCallbacks ||= [] @_leaveCallbacks.push callback else if !@connected callback() else @_leaveDoc(callback) pollSavedStatus: () -> # returns false if doc has ops waiting to be acknowledged or # sent that haven't changed since the last time we checked. # Otherwise returns true. inflightOp = @getInflightOp() pendingOp = @getPendingOp() if !inflightOp? and !pendingOp? # there's nothing going on saved = true else if inflightOp == @oldInflightOp saved = false else if pendingOp? saved = false else saved = true @oldInflightOp = inflightOp return saved _cancelLeave: () -> if @_leaveCallbacks? delete @_leaveCallbacks _cancelJoin: () -> if @_joinCallbacks? delete @_joinCallbacks _onUpdateApplied: (update) -> @ide.pushEvent "received-update", doc_id: @doc_id remote_doc_id: update?.doc wantToBeJoined: @wantToBeJoined update: update if Math.random() < (@ide.disconnectRate or 0) console.log "Simulating disconnect" @ide.connectionManager.disconnect() return if Math.random() < (@ide.ignoreRate or 0) console.log "Simulating lost update" return if update?.doc == @doc_id and @doc? @doc.processUpdateFromServer update if !@wantToBeJoined @leave() _onDisconnect: () -> @connected = false @joined = false @doc?.updateConnectionState "disconnected" _onReconnect: () -> @ide.pushEvent "reconnected:afterJoinProject" @connected = true if @wantToBeJoined or @doc?.hasBufferedOps() @_joinDoc (error) => return @_onError(error) if error? @doc.updateConnectionState "ok" @doc.flushPendingOps() @_callJoinCallbacks() _callJoinCallbacks: () -> for callback in @_joinCallbacks or [] callback() delete @_joinCallbacks _joinDoc: (callback = (error) ->) -> if @doc? @ide.socket.emit 'joinDoc', @doc_id, @doc.getVersion(), (error, docLines, version, updates) => return callback(error) if error? @joined = true @doc.catchUp( updates ) callback() else @ide.socket.emit 'joinDoc', @doc_id, (error, docLines, version) => return callback(error) if error? @joined = true @doc = new ShareJsDoc @doc_id, docLines, version, @ide.socket @_bindToShareJsDocEvents() callback() _leaveDoc: (callback = (error) ->) -> @ide.socket.emit 'leaveDoc', @doc_id, (error) => return callback(error) if error? @joined = false for callback in @_leaveCallbacks or [] callback(error) delete @_leaveCallbacks callback(error) _cleanUp: () -> delete Document.openDocs[@doc_id] @_unBindFromEditorEvents() @_unBindFromSocketEvents() _bindToShareJsDocEvents: () -> @doc.on "error", (error, meta) => @_onError error, meta @doc.on "externalUpdate", () => @ide.pushEvent "externalUpdate", doc_id: @doc_id @trigger "externalUpdate" @doc.on "remoteop", () => @ide.pushEvent "remoteop", doc_id: @doc_id @trigger "remoteop" @doc.on "op:sent", (op) => @ide.pushEvent "op:sent", doc_id: @doc_id op: op @trigger "op:sent" @doc.on "op:acknowledged", (op) => @ide.pushEvent "op:acknowledged", doc_id: @doc_id op: op @trigger "op:acknowledged" @doc.on "op:timeout", (op) => @ide.pushEvent "op:timeout", doc_id: @doc_id op: op @trigger "op:timeout" ga?('send', 'event', 'error', "op timeout", "Op was now acknowledged - #{@ide.socket.socket.transport.name}" ) @ide.connectionManager.reconnectImmediately() @doc.on "flush", (inflightOp, pendingOp, version) => @ide.pushEvent "flush", doc_id: @doc_id, inflightOp: inflightOp, pendingOp: pendingOp v: version _onError: (error, meta = {}) -> console.error "ShareJS error", error, meta ga?('send', 'event', 'error', "shareJsError", "#{error.message} - #{@ide.socket.socket.transport.name}" ) @ide.socket.disconnect() meta.doc_id = @doc_id @ide.reportError(error, meta) @doc?.clearInflightAndPendingOps() @_cleanUp() @trigger "error", error