Package org.waveprotocol.wave.client.wave

Source Code of org.waveprotocol.wave.client.wave.LocalSupplementedWaveImpl

/**
* 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);
  }
}
TOP

Related Classes of org.waveprotocol.wave.client.wave.LocalSupplementedWaveImpl

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.