com.google.firebase.database.core.SyncPoint 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.DatabaseError;
import com.google.firebase.database.annotations.NotNull;
import com.google.firebase.database.annotations.Nullable;
import com.google.firebase.database.core.operation.Operation;
import com.google.firebase.database.core.persistence.PersistenceManager;
import com.google.firebase.database.core.view.CacheNode;
import com.google.firebase.database.core.view.Change;
import com.google.firebase.database.core.view.DataEvent;
import com.google.firebase.database.core.view.Event;
import com.google.firebase.database.core.view.QueryParams;
import com.google.firebase.database.core.view.QuerySpec;
import com.google.firebase.database.core.view.View;
import com.google.firebase.database.core.view.ViewCache;
import com.google.firebase.database.snapshot.ChildKey;
import com.google.firebase.database.snapshot.IndexedNode;
import com.google.firebase.database.snapshot.NamedNode;
import com.google.firebase.database.snapshot.Node;
import com.google.firebase.database.utilities.Pair;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* SyncPoint represents a single location in a SyncTree with 1 or more event registrations, meaning
* we need to maintain 1 or more Views at this location to cache server data and raise appropriate
* events for server changes and user writes (set, transaction, update).
*
* It's responsible for: - Maintaining the set of 1 or more views necessary at this location (a
* SyncPoint with 0 views should be removed). - Proxying user / server operations to the views as
* appropriate (i.e. applyServerOverwrite, applyUserOverwrite, etc.)
*/
public class SyncPoint {
/**
* The Views being tracked at this location in the tree, stored as a map where the key is a
* QueryParams and the value is the View for that query.
*
*
NOTE: This list will be quite small (usually 1, but perhaps 2 or 3; any more is an odd use
* case).
*/
private final Map views;
private final PersistenceManager persistenceManager;
public SyncPoint(PersistenceManager persistenceManager) {
this.views = new HashMap<>();
this.persistenceManager = persistenceManager;
}
public boolean isEmpty() {
return this.views.isEmpty();
}
private List applyOperationToView(
View view, Operation operation, WriteTreeRef writes, Node optCompleteServerCache) {
View.OperationResult result = view.applyOperation(operation, writes, optCompleteServerCache);
// Not a default query, track active children
if (!view.getQuery().loadsAllData()) {
Set removed = new HashSet<>();
Set added = new HashSet<>();
for (Change change : result.changes) {
Event.EventType type = change.getEventType();
if (type == Event.EventType.CHILD_ADDED) {
added.add(change.getChildKey());
} else if (type == Event.EventType.CHILD_REMOVED) {
removed.add(change.getChildKey());
}
}
if (!added.isEmpty() || !removed.isEmpty()) {
this.persistenceManager.updateTrackedQueryKeys(view.getQuery(), added, removed);
}
}
return result.events;
}
public List applyOperation(
Operation operation, WriteTreeRef writesCache, Node optCompleteServerCache) {
QueryParams queryParams = operation.getSource().getQueryParams();
if (queryParams != null) {
View view = this.views.get(queryParams);
assert view != null;
return applyOperationToView(view, operation, writesCache, optCompleteServerCache);
} else {
List events = new ArrayList<>();
for (Map.Entry entry : this.views.entrySet()) {
View view = entry.getValue();
events.addAll(applyOperationToView(view, operation, writesCache, optCompleteServerCache));
}
return events;
}
}
/** Add an event callback for the specified query. */
public List addEventRegistration(
@NotNull EventRegistration eventRegistration,
WriteTreeRef writesCache,
CacheNode serverCache) {
QuerySpec query = eventRegistration.getQuerySpec();
View view = this.views.get(query.getParams());
if (view == null) {
// TODO: make writesCache take flag for complete server node
Node eventCache =
writesCache.calcCompleteEventCache(
serverCache.isFullyInitialized() ? serverCache.getNode() : null);
boolean eventCacheComplete;
if (eventCache != null) {
eventCacheComplete = true;
} else {
eventCache = writesCache.calcCompleteEventChildren(serverCache.getNode());
eventCacheComplete = false;
}
IndexedNode indexed = IndexedNode.from(eventCache, query.getIndex());
ViewCache viewCache =
new ViewCache(new CacheNode(indexed, eventCacheComplete, false), serverCache);
view = new View(query, viewCache);
// If this is a non-default query we need to tell persistence our current view of the
// data
if (!query.loadsAllData()) {
Set allChildren = new HashSet<>();
for (NamedNode node : view.getEventCache()) {
allChildren.add(node.getName());
}
this.persistenceManager.setTrackedQueryKeys(query, allChildren);
}
this.views.put(query.getParams(), view);
}
// This is guaranteed to exist now, we just created anything that was missing
view.addEventRegistration(eventRegistration);
return view.getInitialEvents(eventRegistration);
}
/**
* Remove event callback(s). Return cancelEvents if a cancelError is specified.
*
* If query is the default query, we'll check all views for the specified eventRegistration. If
* eventRegistration is null, we'll remove all callbacks for the specified view(s).
*
* @param query Query to remove the registration from.
* @param eventRegistration If null, remove all callbacks.
* @param cancelError If a cancelError is provided, appropriate cancel events will be returned.
* @return a Pair of lists consisting of removed queries and any cancel events.
*/
public Pair, List> removeEventRegistration(
@NotNull QuerySpec query,
@Nullable EventRegistration eventRegistration,
@Nullable DatabaseError cancelError) {
List removed = new ArrayList<>();
List cancelEvents = new ArrayList<>();
boolean hadCompleteView = this.hasCompleteView();
if (query.isDefault()) {
// When you do ref.off(...), we search all views for the registration to remove.
Iterator> iterator = this.views.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry entry = iterator.next();
View view = entry.getValue();
cancelEvents.addAll(view.removeEventRegistration(eventRegistration, cancelError));
if (view.isEmpty()) {
iterator.remove();
// We'll deal with complete views later.
if (!view.getQuery().loadsAllData()) {
removed.add(view.getQuery());
}
}
}
} else {
// remove the callback from the specific view.
View view = this.views.get(query.getParams());
if (view != null) {
cancelEvents.addAll(view.removeEventRegistration(eventRegistration, cancelError));
if (view.isEmpty()) {
this.views.remove(query.getParams());
// We'll deal with complete views later.
if (!view.getQuery().loadsAllData()) {
removed.add(view.getQuery());
}
}
}
}
if (hadCompleteView && !this.hasCompleteView()) {
// We removed our last complete view.
removed.add(QuerySpec.defaultQueryAtPath(query.getPath()));
}
return new Pair<>(removed, cancelEvents);
}
public List getQueryViews() {
List views = new ArrayList<>();
for (Map.Entry entry : this.views.entrySet()) {
View view = entry.getValue();
if (!view.getQuery().loadsAllData()) {
views.add(view);
}
}
return views;
}
public Node getCompleteServerCache(Path path) {
for (View view : this.views.values()) {
if (view.getCompleteServerCache(path) != null) {
return view.getCompleteServerCache(path);
}
}
return null;
}
public View viewForQuery(QuerySpec query) {
// TODO: iOS doesn't have this loadsAllData() case and I'm not sure it makes sense... but
// leaving for now.
if (query.loadsAllData()) {
return this.getCompleteView();
} else {
return this.views.get(query.getParams());
}
}
public boolean viewExistsForQuery(QuerySpec query) {
return this.viewForQuery(query) != null;
}
public boolean hasCompleteView() {
return this.getCompleteView() != null;
}
public View getCompleteView() {
for (Map.Entry entry : this.views.entrySet()) {
View view = entry.getValue();
if (view.getQuery().loadsAllData()) {
return view;
}
}
return null;
}
// Package private for testing purposes only
Map getViews() {
return views;
}
}