/** * Copyright 2011 Google Inc. * * 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 org.waveprotocol.wave.client.wave; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import org.waveprotocol.wave.client.scheduler.Scheduler.IncrementalTask; import org.waveprotocol.wave.client.scheduler.SchedulerInstance; import org.waveprotocol.wave.client.scheduler.TimerService; import org.waveprotocol.wave.model.conversation.ConversationBlip; import org.waveprotocol.wave.model.supplement.ObservableSupplementedWave; import org.waveprotocol.wave.model.supplement.SupplementedWaveWrapper; import org.waveprotocol.wave.model.util.CollectionUtils; import org.waveprotocol.wave.model.util.IdentityMap; import org.waveprotocol.wave.model.util.IdentityMap.ProcV; import org.waveprotocol.wave.model.util.IdentitySet; import org.waveprotocol.wave.model.util.ReadableIdentitySet.Proc; import org.waveprotocol.wave.model.wave.Blip; import org.waveprotocol.wave.model.wave.ObservableWavelet; import org.waveprotocol.wave.model.wave.WaveViewListener; import org.waveprotocol.wave.model.wave.WaveletListener; import org.waveprotocol.wave.model.wave.opbased.ObservableWaveView; import org.waveprotocol.wave.model.wave.opbased.WaveletListenerImpl; /** * Optimistic implementation of a wave supplement. * <p> * Because read state is based on versions, and versions increment * asynchronously after local operations have been applied, performing actions * on the regular supplement model synchronously with reading actions does not * bring the supplement into the correct state, since the modification versions * of waves and blips will not have been updated. This class addresses that * synchronization issue. * <p> * This class decorates a regular supplement, and keeps track of two kinds of * blips: a blip that is currently being read, and blips that have been marked * as read. This class overrides the read state queries of the regular * supplement in order that a blip that is being read is always revealed to be * read by the supplement API. Blips that have been marked as read are * continuously marked as read in the background if they are modified, until a * remote change occurs on them. This is to ensure local operations, whose * version increments only occur asynchronously after server acknowledgements, * appear to be read. Note that server acknowledgements of local operations are * observed as version increments, while incoming remote operations are observed * as version increments accompanied by a * {@link WaveletListener#onRemoteBlipContentModified} event. The observation of * that event on a blip removes it from the set being automatically read. * <p> * Since it is not possible to detect, with the wavelet API, when all local * operations have been acknowledged, the automatically-read blip collection can * grow quite large. In order not to leak memory from continuous growth, a blip * in that is evicted after a generous amount of time, within which it is * assumed that the server acknowledgements for local operations on that blip * will have arrived. * * @author hearnden@google.com (David Hearnden) */ public final class LocalSupplementedWaveImpl extends SupplementedWaveWrapper< ObservableSupplementedWave> implements WaveViewListener, LocalSupplementedWave, IncrementalTask { /** How often to mark auto-read blips as read. */ @VisibleForTesting static final int REPEAT_MS = 10 * 1000; /** * Background auto-read is stopped for blips that have not changed for longer * than this. */ @VisibleForTesting static final int EVICT_TIME_MS = 60 * 1000; private final TimerService timer; private final ObservableWaveView wave; /** * Blips in this collection are periodically (in intervals of * {@link #REPEAT_MS}) marked as read. Each blip maps to the last time it was * marked as read. Blips are evicted either when they are unchanged for a long * time, or a remote operation occurs on them. */ private final IdentityMap<ConversationBlip, Double> autoRead = CollectionUtils.createIdentityMap(); /** * The same blips as in {@link #autoRead}, but with their raw versions, for * cross-referencing with {@link WaveletListener#onRemoteBlipContentModified} * events. */ private final IdentityMap<Blip, ConversationBlip> rawAutoRead = CollectionUtils.createIdentityMap(); /** Listener that detects remote changes on blips. */ private final WaveletListener remoteChangeDetector = new WaveletListenerImpl() { @Deprecated @Override public void onRemoteBlipContentModified(ObservableWavelet wavelet, Blip blip) { onRemoteChange(blip); } }; /** The blip that is actively being read, if there is one. */ private ConversationBlip reading; LocalSupplementedWaveImpl( TimerService timer, ObservableWaveView wave, ObservableSupplementedWave delegate) { super(delegate); this.timer = timer; this.wave = wave; } public static LocalSupplementedWaveImpl create( ObservableWaveView wave, ObservableSupplementedWave delegate) { TimerService timer = SchedulerInstance.getLowPriorityTimer(); LocalSupplementedWaveImpl supplement = new LocalSupplementedWaveImpl(timer, wave, delegate); supplement.init(); return supplement; } @VisibleForTesting void init() { wave.addListener(this); for (ObservableWavelet wavelet : wave.getWavelets()) { wavelet.addListener(remoteChangeDetector); } timer.scheduleRepeating(this, REPEAT_MS, REPEAT_MS); } public void destroy() { timer.cancel(this); for (ObservableWavelet wavelet : wave.getWavelets()) { wavelet.removeListener(remoteChangeDetector); } wave.removeListener(this); } @Override public void startReading(ConversationBlip blip) { Preconditions.checkState(reading == null); reading = blip; startAutoReading(reading); } @Override public void stopReading(ConversationBlip blip) { Preconditions.checkState(reading != null); Preconditions.checkArgument(reading == blip); // Continue marking this blip as read while the acks come in. Delay eviction // by refreshing its timestamp. assert autoRead.has(reading); delegate.markAsRead(reading); autoRead.put(reading, timer.currentTimeMillis()); reading = null; } /** * Puts a blip into the auto-read collection. */ private void startAutoReading(ConversationBlip blip) { // Mark it as read first, before putting in the auto-read override, in order // to generate the correct read events. delegate.markAsRead(blip); autoRead.put(blip, timer.currentTimeMillis()); rawAutoRead.put(blip.hackGetRaw(), blip); } /** * Removes a blip from the auto-read collection. */ private void stopAutoReading(ConversationBlip blip) { assert blip != reading : "not allowed to remove the reading blip"; autoRead.remove(blip); rawAutoRead.remove(blip.hackGetRaw()); } @Override public boolean isUnread(ConversationBlip blip) { // All blips in autoRead are treated as read. return !autoRead.has(blip) && delegate.isUnread(blip); } @Override public void markAsRead(ConversationBlip blip) { startAutoReading(blip); } @Override public void markAsUnread() { autoRead.clear(); rawAutoRead.clear(); delegate.markAsUnread(); if (reading != null) { startAutoReading(reading); } } @Override public boolean execute() { // Mark as read all blips in the auto-read list. // Evict any blips that have not been touched for more than some threshold, // but never evict the currently-reading blip. final IdentitySet<ConversationBlip> toEvict = CollectionUtils.createIdentitySet(); if (reading != null) { delegate.markAsRead(reading); } autoRead.each(new ProcV<ConversationBlip, Double>() { final double now = timer.currentTimeMillis(); @Override public void apply(ConversationBlip blip, Double touched) { delegate.markAsRead(blip); if (blip != reading && (now - touched >= EVICT_TIME_MS)) { toEvict.add(blip); } } }); toEvict.each(new Proc<ConversationBlip>() { @Override public void apply(ConversationBlip blip) { stopAutoReading(blip); } }); return true; // Run forever } // // Wave events. // @Override public void onWaveletAdded(ObservableWavelet wavelet) { wavelet.addListener(remoteChangeDetector); } @Override public void onWaveletRemoved(ObservableWavelet wavelet) { wavelet.removeListener(remoteChangeDetector); } private void onRemoteChange(Blip raw) { // Some op caused by someone else occurred. Stop auto-reading. ConversationBlip blip = rawAutoRead.get(raw); if (blip != null && blip != reading) { stopAutoReading(blip); } } // // Events. // @Override public void addListener(Listener listener) { delegate.addListener(listener); } @Override public void removeListener(Listener listener) { delegate.removeListener(listener); } }