All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.firebase.database.core.WriteTree Maven / Gradle / Ivy

/*
 * Copyright 2017 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 com.google.firebase.database.core;

import com.google.firebase.database.core.utilities.Predicate;
import com.google.firebase.database.core.view.CacheNode;
import com.google.firebase.database.snapshot.ChildKey;
import com.google.firebase.database.snapshot.EmptyNode;
import com.google.firebase.database.snapshot.Index;
import com.google.firebase.database.snapshot.NamedNode;
import com.google.firebase.database.snapshot.Node;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * Defines a single user-initiated write operation. May be the result of a set(), transaction(), or
 * update() call. In the case of a set() or transaction, snap wil be non-null. In the case of an
 * update(), children will be non-null.
 */
public class WriteTree {

  /** The default filter used when constructing the tree. Keep everything that's visible. */
  private static final Predicate DEFAULT_FILTER =
      new Predicate() {
        @Override
        public boolean evaluate(UserWriteRecord write) {
          return write.isVisible();
        }
      };
  /**
   * A tree tracking the result of applying all visible writes. This does not include transactions
   * with applyLocally=false or writes that are completely shadowed by other writes.
   */
  private CompoundWrite visibleWrites;
  /**
   * A list of all pending writes, regardless of visibility and shadowed-ness. Used to calculate
   * arbitrary sets of the changed data, such as hidden writes (from transactions) or changes with
   * certain writes excluded (also used by transactions).
   */
  private List allWrites;

  private Long lastWriteId;

  /**
   * WriteTree tracks all pending user-initiated writes and has methods to calculate the result of
   * merging them with underlying server data (to create "event cache" data). Pending writes are
   * added with addOverwrite() and addMerge(), and removed with removeWrite().
   */
  public WriteTree() {
    this.visibleWrites = CompoundWrite.emptyWrite();
    this.allWrites = new ArrayList<>();
    this.lastWriteId = -1L;
  }

  /**
   * Static method. Given an array of WriteRecords, a filter for which ones to include, and a path,
   * construct a merge at that path.
   */
  private static CompoundWrite layerTree(
      List writes, Predicate filter, Path treeRoot) {
    CompoundWrite compoundWrite = CompoundWrite.emptyWrite();
    for (UserWriteRecord write : writes) {
      // Theory, a later set will either:
      // a) abort a relevant transaction, so no need to worry about excluding it from
      // calculating
      //    that transaction
      // b) not be relevant to a transaction (separate branch), so again will not affect
      // the data
      //     for that transaction
      if (filter.evaluate(write)) {
        Path writePath = write.getPath();
        if (write.isOverwrite()) {
          if (treeRoot.contains(writePath)) {
            Path relativePath = Path.getRelative(treeRoot, writePath);
            compoundWrite = compoundWrite.addWrite(relativePath, write.getOverwrite());
          } else if (writePath.contains(treeRoot)) {
            compoundWrite =
                compoundWrite.addWrite(
                    Path.getEmptyPath(),
                    write.getOverwrite().getChild(Path.getRelative(writePath, treeRoot)));
          } else {
            // There is no overlap between root path and write path, ignore write
          }
        } else {
          if (treeRoot.contains(writePath)) {
            Path relativePath = Path.getRelative(treeRoot, writePath);
            compoundWrite = compoundWrite.addWrites(relativePath, write.getMerge());
          } else if (writePath.contains(treeRoot)) {
            Path relativePath = Path.getRelative(writePath, treeRoot);
            if (relativePath.isEmpty()) {
              compoundWrite = compoundWrite.addWrites(Path.getEmptyPath(), write.getMerge());
            } else {
              Node deepNode = write.getMerge().getCompleteNode(relativePath);
              if (deepNode != null) {
                compoundWrite = compoundWrite.addWrite(Path.getEmptyPath(), deepNode);
              }
            }
          } else {
            // There is no overlap between root path and write path, ignore write
          }
        }
      }
    }
    return compoundWrite;
  }

  /**
   * Create a new WriteTreeRef for the given path. For use with a new sync point at the given path.
   */
  public WriteTreeRef childWrites(Path path) {
    return new WriteTreeRef(path, this);
  }

  /** Record a new overwrite from user code. */
  public void addOverwrite(Path path, Node snap, Long writeId, boolean visible) {
    assert writeId > this.lastWriteId; // Stacking an older write on top of newer ones
    this.allWrites.add(new UserWriteRecord(writeId, path, snap, visible));
    if (visible) {
      this.visibleWrites = this.visibleWrites.addWrite(path, snap);
    }
    this.lastWriteId = writeId;
  }

  /** Record a new merge from user code. */
  public void addMerge(Path path, CompoundWrite changedChildren, Long writeId) {
    assert writeId > this.lastWriteId; // Stacking an older write on top of newer ones
    this.allWrites.add(new UserWriteRecord(writeId, path, changedChildren));
    this.visibleWrites = this.visibleWrites.addWrites(path, changedChildren);
    this.lastWriteId = writeId;
  }

  public UserWriteRecord getWrite(long writeId) {
    for (UserWriteRecord record : this.allWrites) {
      if (record.getWriteId() == writeId) {
        return record;
      }
    }
    return null;
  }

  public List purgeAllWrites() {
    List purgedWrites = new ArrayList<>(this.allWrites);
    // Reset everything
    this.visibleWrites = CompoundWrite.emptyWrite();
    this.allWrites = new ArrayList<>();
    return purgedWrites;
  }

  /**
   * Remove a write (either an overwrite or merge) that has been successfully acknowledge by the
   * server. Recalculates the tree if necessary. We return whether the write may have been visible,
   * meaning views need to reevaluate.
   *
   * @return true if the write may have been visible (meaning we'll need to reevaluate / raise
   *     events as a result).
   */
  public boolean removeWrite(long writeId) {
    // Note: disabling this check. It could be a transaction that preempted another
    // transaction, and
    // thus was applied out of order.
    // var validClear = revert || this.allWrites_.length === 0 ||
    //      writeId <= this.allWrites_[0].writeId;
    // fb.core.util.assert(validClear, "Either we don't have this write, or it's the first
    // one in
    //      the queue");

    // TODO: maybe use hashmap
    UserWriteRecord writeToRemove = null;
    int idx = 0;
    for (UserWriteRecord record : this.allWrites) {
      if (record.getWriteId() == writeId) {
        writeToRemove = record;
        break;
      }
      idx++;
    }
    assert writeToRemove != null : "removeWrite called with nonexistent writeId";

    this.allWrites.remove(writeToRemove);

    boolean removedWriteWasVisible = writeToRemove.isVisible();
    boolean removedWriteOverlapsWithOtherWrites = false;
    int i = this.allWrites.size() - 1;

    while (removedWriteWasVisible && i >= 0) {
      UserWriteRecord currentWrite = this.allWrites.get(i);
      if (currentWrite.isVisible()) {
        if (i >= idx && this.recordContainsPath(currentWrite, writeToRemove.getPath())) {
          // The removed write was completely shadowed by a subsequent write.
          removedWriteWasVisible = false;
        } else if (writeToRemove.getPath().contains(currentWrite.getPath())) {
          // Either we're covering some writes or they're covering part of us
          // (depending on which
          // came first).
          removedWriteOverlapsWithOtherWrites = true;
        }
      }
      i--;
    }

    if (!removedWriteWasVisible) {
      return false;
    } else if (removedWriteOverlapsWithOtherWrites) {
      // There's some shadowing going on. Just rebuild the visible writes from scratch.
      this.resetTree();
      return true;
    } else {
      // There's no shadowing.  We can safely just remove the write(s) from visibleWrites.
      if (writeToRemove.isOverwrite()) {
        this.visibleWrites = this.visibleWrites.removeWrite(writeToRemove.getPath());
      } else {
        for (Map.Entry entry : writeToRemove.getMerge()) {
          Path path = entry.getKey();
          this.visibleWrites = this.visibleWrites.removeWrite(writeToRemove.getPath().child(path));
        }
      }
      return true;
    }
  }

  /**
   * Return a complete snapshot for the given path if there's visible write data at that path, else
   * null. No server data is considered.
   */
  public Node getCompleteWriteData(Path path) {
    return this.visibleWrites.getCompleteNode(path);
  }

  /**
   * Given optional, underlying server data, and an optional set of constraints (exclude some sets,
   * include hidden writes), attempt to calculate a complete snapshot for the given path
   */
  public Node calcCompleteEventCache(Path treePath, Node completeServerCache) {
    return this.calcCompleteEventCache(treePath, completeServerCache, new ArrayList());
  }

  public Node calcCompleteEventCache(
      Path treePath, Node completeServerCache, List writeIdsToExclude) {
    return this.calcCompleteEventCache(treePath, completeServerCache, writeIdsToExclude, false);
  }

  public Node calcCompleteEventCache(
      final Path treePath,
      Node completeServerCache,
      final List writeIdsToExclude,
      final boolean includeHiddenWrites) {
    if (writeIdsToExclude.isEmpty() && !includeHiddenWrites) {
      Node shadowingNode = this.visibleWrites.getCompleteNode(treePath);
      if (shadowingNode != null) {
        return shadowingNode;
      } else {
        CompoundWrite subMerge = this.visibleWrites.childCompoundWrite(treePath);
        if (subMerge.isEmpty()) {
          return completeServerCache;
        } else if (completeServerCache == null && !subMerge.hasCompleteWrite(Path.getEmptyPath())) {
          // We wouldn't have a complete snapshot, since there's no underlying data and
          // no complete
          // shadow
          return null;
        } else {
          Node layeredCache;
          if (completeServerCache != null) {
            layeredCache = completeServerCache;
          } else {
            layeredCache = EmptyNode.Empty();
          }
          return subMerge.apply(layeredCache);
        }
      }
    } else {
      CompoundWrite merge = this.visibleWrites.childCompoundWrite(treePath);
      if (!includeHiddenWrites && merge.isEmpty()) {
        return completeServerCache;
      } else {
        // If the server cache is null, and we don't have a complete cache, we need to
        // return null
        if (!includeHiddenWrites
            && completeServerCache == null
            && !merge.hasCompleteWrite(Path.getEmptyPath())) {
          return null;
        } else {
          Predicate filter =
              new Predicate() {
                @Override
                public boolean evaluate(UserWriteRecord write) {
                  return (write.isVisible() || includeHiddenWrites)
                      && (!writeIdsToExclude.contains(write.getWriteId()))
                      && (write.getPath().contains(treePath) || treePath.contains(write.getPath()));
                }
              };
          Node layeredCache;
          CompoundWrite mergeAtPath = WriteTree.layerTree(this.allWrites, filter, treePath);
          layeredCache = completeServerCache != null ? completeServerCache : EmptyNode.Empty();
          return mergeAtPath.apply(layeredCache);
        }
      }
    }
  }

  /**
   * With underlying server data, attempt to return a children node of children that we have
   * complete data for. Used when creating new views, to pre-fill their complete event children
   * snapshot.
   */
  public Node calcCompleteEventChildren(Path treePath, Node completeServerChildren) {
    Node completeChildren = EmptyNode.Empty();
    Node topLevelSet = this.visibleWrites.getCompleteNode(treePath);
    if (topLevelSet != null) {
      if (!topLevelSet.isLeafNode()) {
        // we're shadowing everything. Return the children.
        for (NamedNode childEntry : topLevelSet) {
          completeChildren =
              completeChildren.updateImmediateChild(childEntry.getName(), childEntry.getNode());
        }
      }
      return completeChildren;
    } else {
      // Layer any children we have on top of this
      // We know we don't have a top-level set, so just enumerate existing children, and
      // apply any
      // updates
      CompoundWrite merge = this.visibleWrites.childCompoundWrite(treePath);
      for (NamedNode entry : completeServerChildren) {
        Node node = merge.childCompoundWrite(new Path(entry.getName())).apply(entry.getNode());
        completeChildren = completeChildren.updateImmediateChild(entry.getName(), node);
      }
      // Add any complete children we have from the set
      for (NamedNode node : merge.getCompleteChildren()) {
        completeChildren = completeChildren.updateImmediateChild(node.getName(), node.getNode());
      }
      return completeChildren;
    }
  }

  /**
   * Given that the underlying server data has updated, determine what, if anything, needs to be
   * applied to the event cache.
   *
   * 

Possibilities: * *

1. No writes are shadowing. Events should be raised, the snap to be applied comes from the * server data * *

2. Some write is completely shadowing. No events to be raised * *

3. Is partially shadowed. Events * *

Either existingEventSnap or existingServerSnap must exist */ public Node calcEventCacheAfterServerOverwrite( Path treePath, final Path childPath, final Node existingEventSnap, final Node existingServerSnap) { assert existingEventSnap != null || existingServerSnap != null : "Either existingEventSnap or existingServerSnap must exist"; Path path = treePath.child(childPath); if (this.visibleWrites.hasCompleteWrite(path)) { // At this point we can probably guarantee that we're in case 2, meaning no events // May need to check visibility while doing the findRootMostValueAndPath call return null; } else { // No complete shadowing. We're either partially shadowing or not shadowing at all. CompoundWrite childMerge = this.visibleWrites.childCompoundWrite(path); if (childMerge.isEmpty()) { // We're not shadowing at all. Case 1 return existingServerSnap.getChild(childPath); } else { // This could be more efficient if the serverNode + updates doesn't change the // eventSnap // However this is tricky to find out, since user updates don't necessary change // the server // snap, e.g. priority updates on empty nodes, or deep deletes. Another special // case is if // the server adds nodes, but doesn't change any existing writes. It is therefore // not enough // to only check if the updates change the serverNode. // Maybe check if the merge tree contains these special cases and only do a full // overwrite // in that case? return childMerge.apply(existingServerSnap.getChild(childPath)); } } } /** * Returns a complete child for a given server snap after applying all user writes or null if * there is no complete child for this ChildKey. */ public Node calcCompleteChild(Path treePath, ChildKey childKey, CacheNode existingServerSnap) { Path path = treePath.child(childKey); Node shadowingNode = this.visibleWrites.getCompleteNode(path); if (shadowingNode != null) { return shadowingNode; } else { if (existingServerSnap.isCompleteForChild(childKey)) { CompoundWrite childMerge = this.visibleWrites.childCompoundWrite(path); return childMerge.apply(existingServerSnap.getNode().getImmediateChild(childKey)); } else { return null; } } } /** * Returns a node if there is a complete overwrite for this path. More specifically, if there is a * write at a higher path, this will return the child of that write relative to the write and this * path. Returns null if there is no write at this path. */ public Node shadowingWrite(Path path) { return this.visibleWrites.getCompleteNode(path); } /** * This method is used when processing child remove events on a query. If we can, we pull in * children that were outside the window, but may now be in the window. */ public NamedNode calcNextNodeAfterPost( Path treePath, Node completeServerData, NamedNode post, boolean reverse, Index index) { Node toIterate; CompoundWrite merge = this.visibleWrites.childCompoundWrite(treePath); Node shadowingNode = merge.getCompleteNode(Path.getEmptyPath()); if (shadowingNode != null) { toIterate = shadowingNode; } else if (completeServerData != null) { toIterate = merge.apply(completeServerData); } else { // no children to iterate on return null; } NamedNode currentNext = null; for (NamedNode node : toIterate) { if (index.compare(node, post, reverse) > 0 && (currentNext == null || index.compare(node, currentNext, reverse) < 0)) { currentNext = node; } } return currentNext; } private boolean recordContainsPath(UserWriteRecord writeRecord, Path path) { if (writeRecord.isOverwrite()) { return writeRecord.getPath().contains(path); } else { for (Map.Entry entry : writeRecord.getMerge()) { if (writeRecord.getPath().child(entry.getKey()).contains(path)) { return true; } } return false; } } /** Re-layer the writes and merges into a tree so we can efficiently calculate event snapshots */ private void resetTree() { this.visibleWrites = WriteTree.layerTree(this.allWrites, WriteTree.DEFAULT_FILTER, Path.getEmptyPath()); if (this.allWrites.size() > 0) { this.lastWriteId = this.allWrites.get(this.allWrites.size() - 1).getWriteId(); } else { this.lastWriteId = -1L; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy