// Copyright 2012 Google Inc. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.google.collide.client.collaboration; import com.google.collide.client.collaboration.FileConcurrencyController.DocOpListener; import com.google.collide.client.editor.Editor; import com.google.collide.client.status.StatusManager; import com.google.collide.client.status.StatusMessage; import com.google.collide.client.status.StatusMessage.MessageType; import com.google.collide.client.util.WindowUnloadingController; import com.google.collide.client.util.WindowUnloadingController.Message; import com.google.collide.client.xhrmonitor.XhrWarden; import com.google.collide.dto.DocOp; import com.google.collide.json.shared.JsonArray; import com.google.collide.shared.ot.DocOpUtils; import com.google.collide.shared.util.JsonCollections; import com.google.gwt.user.client.Timer; import java.util.List; /** * A service that warns the user if a sent document operation does not receive * an acknowledgment in a short amount of itme. * */ class AckWatchdog implements DocOpListener { private static final int ACK_WARNING_TIMEOUT_MS = 10000; private static final int ACK_ERROR_TIMEOUT_MS = 60000; /* * TODO: editor read-only state can be touched by multiple * clients. Imagine two that each want to set the editor read-only for 5 * seconds, (A) sets read-only, a few seconds elapse, (B) sets read-only, (A) * wants to set back to write (but B still wants read-only), then (B) wants to * set to write. This doesn't handle that well; the editor needs to expose * better API for this (give each caller a separate boolean and only set back * to write when all callers have readonly=false. Perhaps API will be * editor.setReadOnly(getClass(), true) or some string ID instead of class). */ private boolean hasSetEditorReadOnly; private boolean isEditorReadOnlyByOthers; private Editor editor; private final JsonArray<DocOp> unackedDocOps = JsonCollections.createArray(); private final DocOpRecoveryInitiator docOpRecoveryInitiator; private final StatusManager statusManager; private StatusMessage warningMessage; private StatusMessage errorMessage; private final WindowUnloadingController windowUnloadingController; private final WindowUnloadingController.Message windowUnloadingMessage; private final Timer warningTimer = new Timer() { @Override public void run() { showErrorOrWarningMessage(false); } }; private final Timer errorTimer = new Timer() { @Override public void run() { showErrorOrWarningMessage(true); } }; AckWatchdog(StatusManager statusManager, WindowUnloadingController windowClosingController, DocOpRecoveryInitiator docOpRecoveryInitiator) { this.statusManager = statusManager; this.windowUnloadingController = windowClosingController; this.docOpRecoveryInitiator = docOpRecoveryInitiator; // Add a window closing listener to wait for client ops to complete. windowUnloadingMessage = new Message() { @Override public String getMessage() { if (unackedDocOps.size() > 0) { return "You have changes that are still saving and will be lost if you leave this page now."; } else { return null; } } }; windowClosingController.addMessage(windowUnloadingMessage); } void teardown() { warningTimer.cancel(); errorTimer.cancel(); windowUnloadingController.removeMessage(windowUnloadingMessage); } public void setEditor(Editor editor) { /* * TODO: minimizing change in this CL, but a future CL could * introudce a document tag for the read-only state */ Editor oldEditor = this.editor; if (oldEditor != null && hasSetEditorReadOnly && !isEditorReadOnlyByOthers) { // Undo our changes oldEditor.setReadOnly(false); } hasSetEditorReadOnly = false; this.editor = editor; } @Override public void onDocOpAckReceived(int documentId, DocOp serverHistoryDocOp, boolean clean) { unackedDocOps.remove(0); if (unackedDocOps.size() == 0) { warningTimer.cancel(); errorTimer.cancel(); hideErrorAndWarningMessages(); } } @Override public void onDocOpSent(int documentId, List<DocOp> docOps) { /* * Our OT model only allows for one set of outstanding doc ops, so this will * not be called again until we have received acks for all of the individual * doc ops. */ for (int i = 0, n = docOps.size(); i < n; i++) { unackedDocOps.add(docOps.get(i)); } warningTimer.schedule(ACK_WARNING_TIMEOUT_MS); errorTimer.schedule(ACK_ERROR_TIMEOUT_MS); } /** * @param error true for error, false for warning */ private void showErrorOrWarningMessage(boolean error) { if (error && editor != null) { isEditorReadOnlyByOthers = editor.isReadOnly(); hasSetEditorReadOnly = true; editor.setReadOnly(true); } if (error && errorMessage == null) { errorMessage = createErrorMessage(); errorMessage.fire(); } else if (!error && warningMessage == null) { warningMessage = createWarningMessage(); warningMessage.fire(); } docOpRecoveryInitiator.recover(); } private void hideErrorAndWarningMessages() { if (hasSetEditorReadOnly && !isEditorReadOnlyByOthers && editor != null) { editor.setReadOnly(false); hasSetEditorReadOnly = false; } boolean hadErrorOrWarningMessage = errorMessage != null || warningMessage != null; if (errorMessage != null) { errorMessage.cancel(); errorMessage = null; } if (warningMessage != null) { warningMessage.cancel(); warningMessage = null; } if (hadErrorOrWarningMessage) { createReceivedAckMessage().fire(); } } private StatusMessage createWarningMessage() { StatusMessage msg = new StatusMessage(statusManager, MessageType.LOADING, "Still saving your latest changes..."); msg.setDismissable(true); XhrWarden.dumpRequestsToConsole(); return msg; } private StatusMessage createErrorMessage() { StatusMessage msg = new StatusMessage(statusManager, MessageType.ERROR, "Your latest changes timed out while saving."); msg.addAction(StatusMessage.RELOAD_ACTION); msg.setDismissable(false); XhrWarden.dumpRequestsToConsole(); return msg; } private StatusMessage createReceivedAckMessage() { StatusMessage msg = new StatusMessage(statusManager, MessageType.CONFIRMATION, "Saved successfully."); msg.setDismissable(true); msg.expire(1500); return msg; } private String getUnackedDocOpsString() { StringBuilder str = new StringBuilder(); for (int i = 0, n = unackedDocOps.size(); i < n; i++) { str.append(DocOpUtils.toString(unackedDocOps.get(i), true)).append("\n"); } return str.toString(); } }