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

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

Go to download

This is the official Firebase Admin Java SDK. Build extraordinary native JVM apps in minutes with Firebase. The Firebase platform can power your app’s backend, user authentication, static hosting, and more.

There is a newer version: 9.4.1
Show newest version
/*
 * 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