diff --git a/packages/database/src/core/SyncPoint.ts b/packages/database/src/core/SyncPoint.ts index 380380f411c..81affe21bbb 100644 --- a/packages/database/src/core/SyncPoint.ts +++ b/packages/database/src/core/SyncPoint.ts @@ -29,7 +29,11 @@ import { viewRemoveEventRegistration } from './view/View'; import { Operation } from './operation/Operation'; -import { WriteTreeRef } from './WriteTree'; +import { + WriteTreeRef, + writeTreeRefCalcCompleteEventCache, + writeTreeRefCalcCompleteEventChildren +} from './WriteTree'; import { Query } from '../api/Query'; import { EventRegistration } from './view/EventRegistration'; import { Node } from './snap/Node'; @@ -127,14 +131,18 @@ export function syncPointGetView( const view = syncPoint.views.get(queryId); if (!view) { // TODO: make writesCache take flag for complete server node - let eventCache = writesCache.calcCompleteEventCache( + let eventCache = writeTreeRefCalcCompleteEventCache( + writesCache, serverCacheComplete ? serverCache : null ); let eventCacheComplete = false; if (eventCache) { eventCacheComplete = true; } else if (serverCache instanceof ChildrenNode) { - eventCache = writesCache.calcCompleteEventChildren(serverCache); + eventCache = writeTreeRefCalcCompleteEventChildren( + writesCache, + serverCache + ); eventCacheComplete = false; } else { eventCache = ChildrenNode.EMPTY_NODE; diff --git a/packages/database/src/core/SyncTree.ts b/packages/database/src/core/SyncTree.ts index f44a86f6436..a9eec995dae 100644 --- a/packages/database/src/core/SyncTree.ts +++ b/packages/database/src/core/SyncTree.ts @@ -50,7 +50,17 @@ import { syncPointViewExistsForQuery, syncPointViewForQuery } from './SyncPoint'; -import { WriteTree, WriteTreeRef } from './WriteTree'; +import { + newWriteTree, + writeTreeAddMerge, + writeTreeAddOverwrite, + writeTreeCalcCompleteEventCache, + writeTreeChildWrites, + writeTreeGetWrite, + WriteTreeRef, + writeTreeRefChild, + writeTreeRemoveWrite +} from './WriteTree'; import { Query } from '../api/Query'; import { Node } from './snap/Node'; import { Event } from './view/Event'; @@ -116,7 +126,7 @@ export class SyncTree { /** * A tree of all pending user writes (user-initiated set()'s, transaction()'s, update()'s, etc.). */ - pendingWriteTree_ = new WriteTree(); + pendingWriteTree_ = newWriteTree(); readonly tagToQueryMap: Map = new Map(); readonly queryToTagMap: Map = new Map(); @@ -141,7 +151,13 @@ export function syncTreeApplyUserOverwrite( visible?: boolean ): Event[] { // Record pending write. - syncTree.pendingWriteTree_.addOverwrite(path, newData, writeId, visible); + writeTreeAddOverwrite( + syncTree.pendingWriteTree_, + path, + newData, + writeId, + visible + ); if (!visible) { return []; @@ -165,7 +181,7 @@ export function syncTreeApplyUserMerge( writeId: number ): Event[] { // Record pending merge. - syncTree.pendingWriteTree_.addMerge(path, changedChildren, writeId); + writeTreeAddMerge(syncTree.pendingWriteTree_, path, changedChildren, writeId); const changeTree = ImmutableTree.fromObject(changedChildren); @@ -186,8 +202,11 @@ export function syncTreeAckUserWrite( writeId: number, revert: boolean = false ) { - const write = syncTree.pendingWriteTree_.getWrite(writeId); - const needToReevaluate = syncTree.pendingWriteTree_.removeWrite(writeId); + const write = writeTreeGetWrite(syncTree.pendingWriteTree_, writeId); + const needToReevaluate = writeTreeRemoveWrite( + syncTree.pendingWriteTree_, + writeId + ); if (!needToReevaluate) { return []; } else { @@ -522,7 +541,7 @@ export function syncTreeAddEventRegistration( syncTree.queryToTagMap.set(queryKey, tag); syncTree.tagToQueryMap.set(tag, queryKey); } - const writesCache = syncTree.pendingWriteTree_.childWrites(path); + const writesCache = writeTreeChildWrites(syncTree.pendingWriteTree_, path); let events = syncPointAddEventRegistration( syncPoint, query, @@ -569,7 +588,8 @@ export function syncTreeCalcCompleteEventCache( } } ); - return writeTree.calcCompleteEventCache( + return writeTreeCalcCompleteEventCache( + writeTree, path, serverCache, writeIdsToExclude, @@ -602,7 +622,8 @@ export function syncTreeGetServerValue( const serverCacheNode: CacheNode | null = serverCacheComplete ? new CacheNode(serverCache, true, false) : null; - const writesCache: WriteTreeRef | null = syncTree.pendingWriteTree_.childWrites( + const writesCache: WriteTreeRef | null = writeTreeChildWrites( + syncTree.pendingWriteTree_, query.path ); const view: View = syncPointGetView( @@ -636,7 +657,7 @@ function syncTreeApplyOperationToSyncPoints_( operation, syncTree.syncPointTree_, /*serverCache=*/ null, - syncTree.pendingWriteTree_.childWrites(newEmptyPath()) + writeTreeChildWrites(syncTree.pendingWriteTree_, newEmptyPath()) ); } @@ -672,7 +693,7 @@ function syncTreeApplyOperationHelper_( const childServerCache = serverCache ? serverCache.getImmediateChild(childName) : null; - const childWritesCache = writesCache.child(childName); + const childWritesCache = writeTreeRefChild(writesCache, childName); events = events.concat( syncTreeApplyOperationHelper_( childOperation, @@ -714,7 +735,7 @@ function syncTreeApplyOperationDescendantsHelper_( const childServerCache = serverCache ? serverCache.getImmediateChild(childName) : null; - const childWritesCache = writesCache.child(childName); + const childWritesCache = writeTreeRefChild(writesCache, childName); const childOperation = operation.operationForChild(childName); if (childOperation) { events = events.concat( @@ -823,7 +844,10 @@ function syncTreeApplyTaggedOperation_( ): Event[] { const syncPoint = syncTree.syncPointTree_.get(queryPath); assert(syncPoint, "Missing sync point for query tag that we're tracking"); - const writesCache = syncTree.pendingWriteTree_.childWrites(queryPath); + const writesCache = writeTreeChildWrites( + syncTree.pendingWriteTree_, + queryPath + ); return syncPointApplyOperation(syncPoint, operation, writesCache, null); } diff --git a/packages/database/src/core/WriteTree.ts b/packages/database/src/core/WriteTree.ts index bb5c28dd55d..a918d1c330a 100644 --- a/packages/database/src/core/WriteTree.ts +++ b/packages/database/src/core/WriteTree.ts @@ -59,720 +59,783 @@ export interface WriteRecord { } /** - * 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(). + * Create a new WriteTreeRef for the given path. For use with a new sync point at the given path. + * */ -export class WriteTree { - /** - * 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 visibleWrites_: CompoundWrite = CompoundWrite.empty(); - - /** - * 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 allWrites_: WriteRecord[] = []; - - private lastWriteId_ = -1; - - /** - * Create a new WriteTreeRef for the given path. For use with a new sync point at the given path. - * - */ - childWrites(path: Path): WriteTreeRef { - return new WriteTreeRef(path, this); - } - - /** - * Record a new overwrite from user code. - * - * @param visible This is set to false by some transactions. It should be excluded from event caches - */ - addOverwrite(path: Path, snap: Node, writeId: number, visible?: boolean) { - assert( - writeId > this.lastWriteId_, - 'Stacking an older write on top of newer ones' - ); - if (visible === undefined) { - visible = true; - } - this.allWrites_.push({ - path, - snap, - writeId, - visible - }); +export function writeTreeChildWrites( + writeTree: WriteTree, + path: Path +): WriteTreeRef { + return newWriteTreeRef(path, writeTree); +} - if (visible) { - this.visibleWrites_ = compoundWriteAddWrite( - this.visibleWrites_, - path, - snap - ); - } - this.lastWriteId_ = writeId; +/** + * Record a new overwrite from user code. + * + * @param visible This is set to false by some transactions. It should be excluded from event caches + */ +export function writeTreeAddOverwrite( + writeTree: WriteTree, + path: Path, + snap: Node, + writeId: number, + visible?: boolean +) { + assert( + writeId > writeTree.lastWriteId, + 'Stacking an older write on top of newer ones' + ); + if (visible === undefined) { + visible = true; } - - /** - * Record a new merge from user code. - */ - addMerge( - path: Path, - changedChildren: { [k: string]: Node }, - writeId: number - ) { - assert( - writeId > this.lastWriteId_, - 'Stacking an older merge on top of newer ones' - ); - this.allWrites_.push({ + writeTree.allWrites.push({ + path, + snap, + writeId, + visible + }); + + if (visible) { + writeTree.visibleWrites = compoundWriteAddWrite( + writeTree.visibleWrites, path, - children: changedChildren, - writeId, - visible: true - }); - - this.visibleWrites_ = compoundWriteAddWrites( - this.visibleWrites_, - path, - changedChildren + snap ); - this.lastWriteId_ = writeId; } + writeTree.lastWriteId = writeId; +} - getWrite(writeId: number): WriteRecord | null { - for (let i = 0; i < this.allWrites_.length; i++) { - const record = this.allWrites_[i]; - if (record.writeId === writeId) { - return record; - } +/** + * Record a new merge from user code. + */ +export function writeTreeAddMerge( + writeTree: WriteTree, + path: Path, + changedChildren: { [k: string]: Node }, + writeId: number +) { + assert( + writeId > writeTree.lastWriteId, + 'Stacking an older merge on top of newer ones' + ); + writeTree.allWrites.push({ + path, + children: changedChildren, + writeId, + visible: true + }); + + writeTree.visibleWrites = compoundWriteAddWrites( + writeTree.visibleWrites, + path, + changedChildren + ); + writeTree.lastWriteId = writeId; +} + +export function writeTreeGetWrite( + writeTree: WriteTree, + writeId: number +): WriteRecord | null { + for (let i = 0; i < writeTree.allWrites.length; i++) { + const record = writeTree.allWrites[i]; + if (record.writeId === writeId) { + return record; } - return null; } + return null; +} - /** - * Remove a write (either an overwrite or merge) that has been successfully acknowledge by the server. Recalculates - * the tree if necessary. We return true if it 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). - */ - removeWrite(writeId: number): boolean { - // Note: disabling this check. It could be a transaction that preempted another transaction, and thus was applied - // out of order. - //const validClear = revert || this.allWrites_.length === 0 || writeId <= this.allWrites_[0].writeId; - //assert(validClear, "Either we don't have this write, or it's the first one in the queue"); - - const idx = this.allWrites_.findIndex(s => { - return s.writeId === writeId; - }); - assert(idx >= 0, 'removeWrite called with nonexistent writeId.'); - const writeToRemove = this.allWrites_[idx]; - this.allWrites_.splice(idx, 1); - - let removedWriteWasVisible = writeToRemove.visible; - let removedWriteOverlapsWithOtherWrites = false; - - let i = this.allWrites_.length - 1; - - while (removedWriteWasVisible && i >= 0) { - const currentWrite = this.allWrites_[i]; - if (currentWrite.visible) { - if ( - i >= idx && - this.recordContainsPath_(currentWrite, writeToRemove.path) - ) { - // The removed write was completely shadowed by a subsequent write. - removedWriteWasVisible = false; - } else if (pathContains(writeToRemove.path, currentWrite.path)) { - // Either we're covering some writes or they're covering part of us (depending on which came first). - removedWriteOverlapsWithOtherWrites = true; - } +/** + * Remove a write (either an overwrite or merge) that has been successfully acknowledge by the server. Recalculates + * the tree if necessary. We return true if it 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). + */ +export function writeTreeRemoveWrite( + writeTree: WriteTree, + writeId: number +): boolean { + // Note: disabling this check. It could be a transaction that preempted another transaction, and thus was applied + // out of order. + //const validClear = revert || this.allWrites_.length === 0 || writeId <= this.allWrites_[0].writeId; + //assert(validClear, "Either we don't have this write, or it's the first one in the queue"); + + const idx = writeTree.allWrites.findIndex(s => { + return s.writeId === writeId; + }); + assert(idx >= 0, 'removeWrite called with nonexistent writeId.'); + const writeToRemove = writeTree.allWrites[idx]; + writeTree.allWrites.splice(idx, 1); + + let removedWriteWasVisible = writeToRemove.visible; + let removedWriteOverlapsWithOtherWrites = false; + + let i = writeTree.allWrites.length - 1; + + while (removedWriteWasVisible && i >= 0) { + const currentWrite = writeTree.allWrites[i]; + if (currentWrite.visible) { + if ( + i >= idx && + writeTreeRecordContainsPath_(currentWrite, writeToRemove.path) + ) { + // The removed write was completely shadowed by a subsequent write. + removedWriteWasVisible = false; + } else if (pathContains(writeToRemove.path, currentWrite.path)) { + // Either we're covering some writes or they're covering part of us (depending on which came first). + removedWriteOverlapsWithOtherWrites = true; } - i--; } + 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; + if (!removedWriteWasVisible) { + return false; + } else if (removedWriteOverlapsWithOtherWrites) { + // There's some shadowing going on. Just rebuild the visible writes from scratch. + writeTreeResetTree_(writeTree); + return true; + } else { + // There's no shadowing. We can safely just remove the write(s) from visibleWrites. + if (writeToRemove.snap) { + writeTree.visibleWrites = compoundWriteRemoveWrite( + writeTree.visibleWrites, + writeToRemove.path + ); } else { - // There's no shadowing. We can safely just remove the write(s) from visibleWrites. - if (writeToRemove.snap) { - this.visibleWrites_ = compoundWriteRemoveWrite( - this.visibleWrites_, - writeToRemove.path + const children = writeToRemove.children; + each(children, (childName: string) => { + writeTree.visibleWrites = compoundWriteRemoveWrite( + writeTree.visibleWrites, + pathChild(writeToRemove.path, childName) ); - } else { - const children = writeToRemove.children; - each(children, (childName: string) => { - this.visibleWrites_ = compoundWriteRemoveWrite( - this.visibleWrites_, - pathChild(writeToRemove.path, childName) - ); - }); + }); + } + return true; + } +} + +function writeTreeRecordContainsPath_( + writeRecord: WriteRecord, + path: Path +): boolean { + if (writeRecord.snap) { + return pathContains(writeRecord.path, path); + } else { + for (const childName in writeRecord.children) { + if ( + writeRecord.children.hasOwnProperty(childName) && + pathContains(pathChild(writeRecord.path, childName), path) + ) { + return true; } - return true; } + return false; } +} - /** - * Return a complete snapshot for the given path if there's visible write data at that path, else null. - * No server data is considered. - * - */ - getCompleteWriteData(path: Path): Node | null { - return compoundWriteGetCompleteNode(this.visibleWrites_, path); +/** + * Re-layer the writes and merges into a tree so we can efficiently calculate event snapshots + */ +function writeTreeResetTree_(writeTree: WriteTree) { + writeTree.visibleWrites = writeTreeLayerTree_( + writeTree.allWrites, + writeTreeDefaultFilter_, + newEmptyPath() + ); + if (writeTree.allWrites.length > 0) { + writeTree.lastWriteId = + writeTree.allWrites[writeTree.allWrites.length - 1].writeId; + } else { + writeTree.lastWriteId = -1; } +} - /** - * 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 - * - * @param writeIdsToExclude An optional set to be excluded - * @param includeHiddenWrites Defaults to false, whether or not to layer on writes with visible set to false - */ - calcCompleteEventCache( - treePath: Path, - completeServerCache: Node | null, - writeIdsToExclude?: number[], - includeHiddenWrites?: boolean - ): Node | null { - if (!writeIdsToExclude && !includeHiddenWrites) { - const shadowingNode = compoundWriteGetCompleteNode( - this.visibleWrites_, - treePath - ); - if (shadowingNode != null) { - return shadowingNode; - } else { - const subMerge = compoundWriteChildCompoundWrite( - this.visibleWrites_, - treePath - ); - if (compoundWriteIsEmpty(subMerge)) { - return completeServerCache; - } else if ( - completeServerCache == null && - !compoundWriteHasCompleteWrite(subMerge, newEmptyPath()) - ) { - // We wouldn't have a complete snapshot, since there's no underlying data and no complete shadow - return null; +/** + * The default filter used when constructing the tree. Keep everything that's visible. + */ +function writeTreeDefaultFilter_(write: WriteRecord) { + return write.visible; +} + +/** + * Static method. Given an array of WriteRecords, a filter for which ones to include, and a path, construct the tree of + * event data at that path. + */ +function writeTreeLayerTree_( + writes: WriteRecord[], + filter: (w: WriteRecord) => boolean, + treeRoot: Path +): CompoundWrite { + let compoundWrite = CompoundWrite.empty(); + for (let i = 0; i < writes.length; ++i) { + const write = writes[i]; + // 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(write)) { + const writePath = write.path; + let relativePath: Path; + if (write.snap) { + if (pathContains(treeRoot, writePath)) { + relativePath = newRelativePath(treeRoot, writePath); + compoundWrite = compoundWriteAddWrite( + compoundWrite, + relativePath, + write.snap + ); + } else if (pathContains(writePath, treeRoot)) { + relativePath = newRelativePath(writePath, treeRoot); + compoundWrite = compoundWriteAddWrite( + compoundWrite, + newEmptyPath(), + write.snap.getChild(relativePath) + ); } else { - const layeredCache = completeServerCache || ChildrenNode.EMPTY_NODE; - return compoundWriteApply(subMerge, layeredCache); + // There is no overlap between root path and write path, ignore write } - } - } else { - const merge = compoundWriteChildCompoundWrite( - this.visibleWrites_, - treePath - ); - if (!includeHiddenWrites && compoundWriteIsEmpty(merge)) { - 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 && - !compoundWriteHasCompleteWrite(merge, newEmptyPath()) - ) { - return null; - } else { - const filter = function (write: WriteRecord) { - return ( - (write.visible || includeHiddenWrites) && - (!writeIdsToExclude || - !~writeIdsToExclude.indexOf(write.writeId)) && - (pathContains(write.path, treePath) || - pathContains(treePath, write.path)) - ); - }; - const mergeAtPath = WriteTree.layerTree_( - this.allWrites_, - filter, - treePath + } else if (write.children) { + if (pathContains(treeRoot, writePath)) { + relativePath = newRelativePath(treeRoot, writePath); + compoundWrite = compoundWriteAddWrites( + compoundWrite, + relativePath, + write.children ); - const layeredCache = completeServerCache || ChildrenNode.EMPTY_NODE; - return compoundWriteApply(mergeAtPath, layeredCache); + } else if (pathContains(writePath, treeRoot)) { + relativePath = newRelativePath(writePath, treeRoot); + if (pathIsEmpty(relativePath)) { + compoundWrite = compoundWriteAddWrites( + compoundWrite, + newEmptyPath(), + write.children + ); + } else { + const child = safeGet(write.children, pathGetFront(relativePath)); + if (child) { + // There exists a child in this node that matches the root path + const deepNode = child.getChild(pathPopFront(relativePath)); + compoundWrite = compoundWriteAddWrite( + compoundWrite, + newEmptyPath(), + deepNode + ); + } + } + } else { + // There is no overlap between root path and write path, ignore write } + } else { + throw assertionError('WriteRecord should have .snap or .children'); } } } + return compoundWrite; +} - /** - * With optional, 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. - */ - calcCompleteEventChildren( - treePath: Path, - completeServerChildren: ChildrenNode | null - ) { - let completeChildren = ChildrenNode.EMPTY_NODE as Node; - const topLevelSet = compoundWriteGetCompleteNode( - this.visibleWrites_, +/** + * Return a complete snapshot for the given path if there's visible write data at that path, else null. + * No server data is considered. + * + */ +export function writeTreeGetCompleteWriteData( + writeTree: WriteTree, + path: Path +): Node | null { + return compoundWriteGetCompleteNode(writeTree.visibleWrites, 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 + * + * @param writeIdsToExclude An optional set to be excluded + * @param includeHiddenWrites Defaults to false, whether or not to layer on writes with visible set to false + */ +export function writeTreeCalcCompleteEventCache( + writeTree: WriteTree, + treePath: Path, + completeServerCache: Node | null, + writeIdsToExclude?: number[], + includeHiddenWrites?: boolean +): Node | null { + if (!writeIdsToExclude && !includeHiddenWrites) { + const shadowingNode = compoundWriteGetCompleteNode( + writeTree.visibleWrites, treePath ); - if (topLevelSet) { - if (!topLevelSet.isLeafNode()) { - // we're shadowing everything. Return the children. - topLevelSet.forEachChild(PRIORITY_INDEX, (childName, childSnap) => { - completeChildren = completeChildren.updateImmediateChild( - childName, - childSnap - ); - }); - } - return completeChildren; - } else if (completeServerChildren) { - // Layer any children we have on top of this - // We know we don't have a top-level set, so just enumerate existing children - const merge = compoundWriteChildCompoundWrite( - this.visibleWrites_, - treePath - ); - completeServerChildren.forEachChild( - PRIORITY_INDEX, - (childName, childNode) => { - const node = compoundWriteApply( - compoundWriteChildCompoundWrite(merge, new Path(childName)), - childNode - ); - completeChildren = completeChildren.updateImmediateChild( - childName, - node - ); - } - ); - // Add any complete children we have from the set - compoundWriteGetCompleteChildren(merge).forEach(namedNode => { - completeChildren = completeChildren.updateImmediateChild( - namedNode.name, - namedNode.node - ); - }); - return completeChildren; + if (shadowingNode != null) { + return shadowingNode; } else { - // We don't have anything to layer on top of. Layer on any children we have - // Note that we can return an empty snap if we have a defined delete - const merge = compoundWriteChildCompoundWrite( - this.visibleWrites_, + const subMerge = compoundWriteChildCompoundWrite( + writeTree.visibleWrites, treePath ); - compoundWriteGetCompleteChildren(merge).forEach(namedNode => { - completeChildren = completeChildren.updateImmediateChild( - namedNode.name, - namedNode.node - ); - }); - return completeChildren; + if (compoundWriteIsEmpty(subMerge)) { + return completeServerCache; + } else if ( + completeServerCache == null && + !compoundWriteHasCompleteWrite(subMerge, newEmptyPath()) + ) { + // We wouldn't have a complete snapshot, since there's no underlying data and no complete shadow + return null; + } else { + const layeredCache = completeServerCache || ChildrenNode.EMPTY_NODE; + return compoundWriteApply(subMerge, layeredCache); + } } - } - - /** - * 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 - */ - calcEventCacheAfterServerOverwrite( - treePath: Path, - childPath: Path, - existingEventSnap: Node | null, - existingServerSnap: Node | null - ): Node | null { - assert( - existingEventSnap || existingServerSnap, - 'Either existingEventSnap or existingServerSnap must exist' + } else { + const merge = compoundWriteChildCompoundWrite( + writeTree.visibleWrites, + treePath ); - const path = pathChild(treePath, childPath); - if (compoundWriteHasCompleteWrite(this.visibleWrites_, 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; + if (!includeHiddenWrites && compoundWriteIsEmpty(merge)) { + return completeServerCache; } else { - // No complete shadowing. We're either partially shadowing or not shadowing at all. - const childMerge = compoundWriteChildCompoundWrite( - this.visibleWrites_, - path - ); - if (compoundWriteIsEmpty(childMerge)) { - // We're not shadowing at all. Case 1 - return existingServerSnap.getChild(childPath); + // If the server cache is null, and we don't have a complete cache, we need to return null + if ( + !includeHiddenWrites && + completeServerCache == null && + !compoundWriteHasCompleteWrite(merge, newEmptyPath()) + ) { + return null; } 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 compoundWriteApply( - childMerge, - existingServerSnap.getChild(childPath) + const filter = function (write: WriteRecord) { + return ( + (write.visible || includeHiddenWrites) && + (!writeIdsToExclude || + !~writeIdsToExclude.indexOf(write.writeId)) && + (pathContains(write.path, treePath) || + pathContains(treePath, write.path)) + ); + }; + const mergeAtPath = writeTreeLayerTree_( + writeTree.allWrites, + filter, + treePath ); + const layeredCache = completeServerCache || ChildrenNode.EMPTY_NODE; + return compoundWriteApply(mergeAtPath, layeredCache); } } } +} - /** - * 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. - */ - calcCompleteChild( - treePath: Path, - childKey: string, - existingServerSnap: CacheNode - ): Node | null { - const path = pathChild(treePath, childKey); - const shadowingNode = compoundWriteGetCompleteNode( - this.visibleWrites_, - path +/** + * With optional, 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. + */ +export function writeTreeCalcCompleteEventChildren( + writeTree: WriteTree, + treePath: Path, + completeServerChildren: ChildrenNode | null +) { + let completeChildren = ChildrenNode.EMPTY_NODE as Node; + const topLevelSet = compoundWriteGetCompleteNode( + writeTree.visibleWrites, + treePath + ); + if (topLevelSet) { + if (!topLevelSet.isLeafNode()) { + // we're shadowing everything. Return the children. + topLevelSet.forEachChild(PRIORITY_INDEX, (childName, childSnap) => { + completeChildren = completeChildren.updateImmediateChild( + childName, + childSnap + ); + }); + } + return completeChildren; + } else if (completeServerChildren) { + // Layer any children we have on top of this + // We know we don't have a top-level set, so just enumerate existing children + const merge = compoundWriteChildCompoundWrite( + writeTree.visibleWrites, + treePath ); - if (shadowingNode != null) { - return shadowingNode; - } else { - if (existingServerSnap.isCompleteForChild(childKey)) { - const childMerge = compoundWriteChildCompoundWrite( - this.visibleWrites_, - path + completeServerChildren.forEachChild( + PRIORITY_INDEX, + (childName, childNode) => { + const node = compoundWriteApply( + compoundWriteChildCompoundWrite(merge, new Path(childName)), + childNode ); - return compoundWriteApply( - childMerge, - existingServerSnap.getNode().getImmediateChild(childKey) + completeChildren = completeChildren.updateImmediateChild( + childName, + node ); - } 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. - */ - shadowingWrite(path: Path): Node | null { - return compoundWriteGetCompleteNode(this.visibleWrites_, 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. - */ - calcIndexedSlice( - treePath: Path, - completeServerData: Node | null, - startPost: NamedNode, - count: number, - reverse: boolean, - index: Index - ): NamedNode[] { - let toIterate: Node; + ); + // Add any complete children we have from the set + compoundWriteGetCompleteChildren(merge).forEach(namedNode => { + completeChildren = completeChildren.updateImmediateChild( + namedNode.name, + namedNode.node + ); + }); + return completeChildren; + } else { + // We don't have anything to layer on top of. Layer on any children we have + // Note that we can return an empty snap if we have a defined delete const merge = compoundWriteChildCompoundWrite( - this.visibleWrites_, + writeTree.visibleWrites, treePath ); - const shadowingNode = compoundWriteGetCompleteNode(merge, newEmptyPath()); - if (shadowingNode != null) { - toIterate = shadowingNode; - } else if (completeServerData != null) { - toIterate = compoundWriteApply(merge, completeServerData); - } else { - // no children to iterate on - return []; - } - toIterate = toIterate.withIndex(index); - if (!toIterate.isEmpty() && !toIterate.isLeafNode()) { - const nodes = []; - const cmp = index.getCompare(); - const iter = reverse - ? (toIterate as ChildrenNode).getReverseIteratorFrom(startPost, index) - : (toIterate as ChildrenNode).getIteratorFrom(startPost, index); - let next = iter.getNext(); - while (next && nodes.length < count) { - if (cmp(next, startPost) !== 0) { - nodes.push(next); - } - next = iter.getNext(); - } - return nodes; - } else { - return []; - } + compoundWriteGetCompleteChildren(merge).forEach(namedNode => { + completeChildren = completeChildren.updateImmediateChild( + namedNode.name, + namedNode.node + ); + }); + return completeChildren; } +} - private recordContainsPath_(writeRecord: WriteRecord, path: Path): boolean { - if (writeRecord.snap) { - return pathContains(writeRecord.path, path); +/** + * 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 + */ +export function writeTreeCalcEventCacheAfterServerOverwrite( + writeTree: WriteTree, + treePath: Path, + childPath: Path, + existingEventSnap: Node | null, + existingServerSnap: Node | null +): Node | null { + assert( + existingEventSnap || existingServerSnap, + 'Either existingEventSnap or existingServerSnap must exist' + ); + const path = pathChild(treePath, childPath); + if (compoundWriteHasCompleteWrite(writeTree.visibleWrites, 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. + const childMerge = compoundWriteChildCompoundWrite( + writeTree.visibleWrites, + path + ); + if (compoundWriteIsEmpty(childMerge)) { + // We're not shadowing at all. Case 1 + return existingServerSnap.getChild(childPath); } else { - for (const childName in writeRecord.children) { - if ( - writeRecord.children.hasOwnProperty(childName) && - pathContains(pathChild(writeRecord.path, childName), path) - ) { - return true; - } - } - return false; + // 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 compoundWriteApply( + childMerge, + existingServerSnap.getChild(childPath) + ); } } +} - /** - * Re-layer the writes and merges into a tree so we can efficiently calculate event snapshots - */ - private resetTree_() { - this.visibleWrites_ = WriteTree.layerTree_( - this.allWrites_, - WriteTree.DefaultFilter_, - newEmptyPath() - ); - if (this.allWrites_.length > 0) { - this.lastWriteId_ = this.allWrites_[this.allWrites_.length - 1].writeId; +/** + * 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. + */ +export function writeTreeCalcCompleteChild( + writeTree: WriteTree, + treePath: Path, + childKey: string, + existingServerSnap: CacheNode +): Node | null { + const path = pathChild(treePath, childKey); + const shadowingNode = compoundWriteGetCompleteNode( + writeTree.visibleWrites, + path + ); + if (shadowingNode != null) { + return shadowingNode; + } else { + if (existingServerSnap.isCompleteForChild(childKey)) { + const childMerge = compoundWriteChildCompoundWrite( + writeTree.visibleWrites, + path + ); + return compoundWriteApply( + childMerge, + existingServerSnap.getNode().getImmediateChild(childKey) + ); } else { - this.lastWriteId_ = -1; + return null; } } +} - /** - * The default filter used when constructing the tree. Keep everything that's visible. - */ - private static DefaultFilter_(write: WriteRecord) { - return write.visible; - } +/** + * 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. + */ +export function writeTreeShadowingWrite( + writeTree: WriteTree, + path: Path +): Node | null { + return compoundWriteGetCompleteNode(writeTree.visibleWrites, path); +} - /** - * Static method. Given an array of WriteRecords, a filter for which ones to include, and a path, construct the tree of - * event data at that path. - */ - private static layerTree_( - writes: WriteRecord[], - filter: (w: WriteRecord) => boolean, - treeRoot: Path - ): CompoundWrite { - let compoundWrite = CompoundWrite.empty(); - for (let i = 0; i < writes.length; ++i) { - const write = writes[i]; - // 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(write)) { - const writePath = write.path; - let relativePath: Path; - if (write.snap) { - if (pathContains(treeRoot, writePath)) { - relativePath = newRelativePath(treeRoot, writePath); - compoundWrite = compoundWriteAddWrite( - compoundWrite, - relativePath, - write.snap - ); - } else if (pathContains(writePath, treeRoot)) { - relativePath = newRelativePath(writePath, treeRoot); - compoundWrite = compoundWriteAddWrite( - compoundWrite, - newEmptyPath(), - write.snap.getChild(relativePath) - ); - } else { - // There is no overlap between root path and write path, ignore write - } - } else if (write.children) { - if (pathContains(treeRoot, writePath)) { - relativePath = newRelativePath(treeRoot, writePath); - compoundWrite = compoundWriteAddWrites( - compoundWrite, - relativePath, - write.children - ); - } else if (pathContains(writePath, treeRoot)) { - relativePath = newRelativePath(writePath, treeRoot); - if (pathIsEmpty(relativePath)) { - compoundWrite = compoundWriteAddWrites( - compoundWrite, - newEmptyPath(), - write.children - ); - } else { - const child = safeGet(write.children, pathGetFront(relativePath)); - if (child) { - // There exists a child in this node that matches the root path - const deepNode = child.getChild(pathPopFront(relativePath)); - compoundWrite = compoundWriteAddWrite( - compoundWrite, - newEmptyPath(), - deepNode - ); - } - } - } else { - // There is no overlap between root path and write path, ignore write - } - } else { - throw assertionError('WriteRecord should have .snap or .children'); - } +/** + * 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. + */ +export function writeTreeCalcIndexedSlice( + writeTree: WriteTree, + treePath: Path, + completeServerData: Node | null, + startPost: NamedNode, + count: number, + reverse: boolean, + index: Index +): NamedNode[] { + let toIterate: Node; + const merge = compoundWriteChildCompoundWrite( + writeTree.visibleWrites, + treePath + ); + const shadowingNode = compoundWriteGetCompleteNode(merge, newEmptyPath()); + if (shadowingNode != null) { + toIterate = shadowingNode; + } else if (completeServerData != null) { + toIterate = compoundWriteApply(merge, completeServerData); + } else { + // no children to iterate on + return []; + } + toIterate = toIterate.withIndex(index); + if (!toIterate.isEmpty() && !toIterate.isLeafNode()) { + const nodes = []; + const cmp = index.getCompare(); + const iter = reverse + ? (toIterate as ChildrenNode).getReverseIteratorFrom(startPost, index) + : (toIterate as ChildrenNode).getIteratorFrom(startPost, index); + let next = iter.getNext(); + while (next && nodes.length < count) { + if (cmp(next, startPost) !== 0) { + nodes.push(next); } + next = iter.getNext(); } - return compoundWrite; + return nodes; + } else { + return []; } } +export function newWriteTree(): WriteTree { + return { + visibleWrites: CompoundWrite.empty(), + allWrites: [], + lastWriteId: -1 + }; +} + /** - * A WriteTreeRef wraps a WriteTree and a path, for convenient access to a particular subtree. All of the methods - * just proxy to the underlying WriteTree. - * + * 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(). */ -export class WriteTreeRef { +export interface WriteTree { /** - * The path to this particular write tree ref. Used for calling methods on writeTree_ while exposing a simpler - * interface to callers. + * 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 readonly treePath_: Path; + visibleWrites: CompoundWrite; /** - * * A reference to the actual tree of write data. All methods are pass-through to the tree, but with the appropriate - * path prefixed. - * - * This lets us make cheap references to points in the tree for sync points without having to copy and maintain all of - * the data. + * 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 readonly writeTree_: WriteTree; + allWrites: WriteRecord[]; - constructor(path: Path, writeTree: WriteTree) { - this.treePath_ = path; - this.writeTree_ = writeTree; - } + lastWriteId: number; +} - /** - * If possible, returns a complete event cache, using the underlying server data if possible. In addition, can be used - * to get a cache that includes hidden writes, and excludes arbitrary writes. Note that customizing the returned node - * can lead to a more expensive calculation. - * - * @param writeIdsToExclude Optional writes to exclude. - * @param includeHiddenWrites Defaults to false, whether or not to layer on writes with visible set to false - */ - calcCompleteEventCache( - completeServerCache: Node | null, - writeIdsToExclude?: number[], - includeHiddenWrites?: boolean - ): Node | null { - return this.writeTree_.calcCompleteEventCache( - this.treePath_, - completeServerCache, - writeIdsToExclude, - includeHiddenWrites - ); - } +/** + * If possible, returns a complete event cache, using the underlying server data if possible. In addition, can be used + * to get a cache that includes hidden writes, and excludes arbitrary writes. Note that customizing the returned node + * can lead to a more expensive calculation. + * + * @param writeIdsToExclude Optional writes to exclude. + * @param includeHiddenWrites Defaults to false, whether or not to layer on writes with visible set to false + */ +export function writeTreeRefCalcCompleteEventCache( + writeTreeRef: WriteTreeRef, + completeServerCache: Node | null, + writeIdsToExclude?: number[], + includeHiddenWrites?: boolean +): Node | null { + return writeTreeCalcCompleteEventCache( + writeTreeRef.writeTree, + writeTreeRef.treePath, + completeServerCache, + writeIdsToExclude, + includeHiddenWrites + ); +} - /** - * If possible, returns a children node containing all of the complete children we have data for. The returned data is a - * mix of the given server data and write data. - * - */ - calcCompleteEventChildren( - completeServerChildren: ChildrenNode | null - ): ChildrenNode { - return this.writeTree_.calcCompleteEventChildren( - this.treePath_, - completeServerChildren - ) as ChildrenNode; - } +/** + * If possible, returns a children node containing all of the complete children we have data for. The returned data is a + * mix of the given server data and write data. + * + */ +export function writeTreeRefCalcCompleteEventChildren( + writeTreeRef: WriteTreeRef, + completeServerChildren: ChildrenNode | null +): ChildrenNode { + return writeTreeCalcCompleteEventChildren( + writeTreeRef.writeTree, + writeTreeRef.treePath, + completeServerChildren + ) as ChildrenNode; +} - /** - * Given that either the underlying server data has updated or the outstanding writes have 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 should be raised - * - * Either existingEventSnap or existingServerSnap must exist, this is validated via an assert - * - * - */ - calcEventCacheAfterServerOverwrite( - path: Path, - existingEventSnap: Node | null, - existingServerSnap: Node | null - ): Node | null { - return this.writeTree_.calcEventCacheAfterServerOverwrite( - this.treePath_, - path, - existingEventSnap, - existingServerSnap - ); - } +/** + * Given that either the underlying server data has updated or the outstanding writes have 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 should be raised + * + * Either existingEventSnap or existingServerSnap must exist, this is validated via an assert + * + * + */ +export function writeTreeRefCalcEventCacheAfterServerOverwrite( + writeTreeRef: WriteTreeRef, + path: Path, + existingEventSnap: Node | null, + existingServerSnap: Node | null +): Node | null { + return writeTreeCalcEventCacheAfterServerOverwrite( + writeTreeRef.writeTree, + writeTreeRef.treePath, + path, + existingEventSnap, + existingServerSnap + ); +} - /** - * 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. - * - */ - shadowingWrite(path: Path): Node | null { - return this.writeTree_.shadowingWrite(pathChild(this.treePath_, path)); - } +/** + * 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. + * + */ +export function writeTreeRefShadowingWrite( + writeTreeRef: WriteTreeRef, + path: Path +): Node | null { + return writeTreeShadowingWrite( + writeTreeRef.writeTree, + pathChild(writeTreeRef.treePath, 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 - */ - calcIndexedSlice( - completeServerData: Node | null, - startPost: NamedNode, - count: number, - reverse: boolean, - index: Index - ): NamedNode[] { - return this.writeTree_.calcIndexedSlice( - this.treePath_, - completeServerData, - startPost, - count, - reverse, - index - ); - } +/** + * 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 + */ +export function writeTreeRefCalcIndexedSlice( + writeTreeRef: WriteTreeRef, + completeServerData: Node | null, + startPost: NamedNode, + count: number, + reverse: boolean, + index: Index +): NamedNode[] { + return writeTreeCalcIndexedSlice( + writeTreeRef.writeTree, + writeTreeRef.treePath, + completeServerData, + startPost, + count, + reverse, + index + ); +} + +/** + * 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. + */ +export function writeTreeRefCalcCompleteChild( + writeTreeRef: WriteTreeRef, + childKey: string, + existingServerCache: CacheNode +): Node | null { + return writeTreeCalcCompleteChild( + writeTreeRef.writeTree, + writeTreeRef.treePath, + childKey, + existingServerCache + ); +} +/** + * Return a WriteTreeRef for a child. + */ +export function writeTreeRefChild( + writeTreeRef: WriteTreeRef, + childName: string +): WriteTreeRef { + return newWriteTreeRef( + pathChild(writeTreeRef.treePath, childName), + writeTreeRef.writeTree + ); +} + +export function newWriteTreeRef( + path: Path, + writeTree: WriteTree +): WriteTreeRef { + return { + treePath: path, + writeTree + }; +} + +/** + * A WriteTreeRef wraps a WriteTree and a path, for convenient access to a particular subtree. All of the methods + * just proxy to the underlying WriteTree. + * + */ +export interface WriteTreeRef { /** - * 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. + * The path to this particular write tree ref. Used for calling methods on writeTree_ while exposing a simpler + * interface to callers. */ - calcCompleteChild( - childKey: string, - existingServerCache: CacheNode - ): Node | null { - return this.writeTree_.calcCompleteChild( - this.treePath_, - childKey, - existingServerCache - ); - } + readonly treePath: Path; /** - * Return a WriteTreeRef for a child. + * * A reference to the actual tree of write data. All methods are pass-through to the tree, but with the appropriate + * path prefixed. + * + * This lets us make cheap references to points in the tree for sync points without having to copy and maintain all of + * the data. */ - child(childName: string): WriteTreeRef { - return new WriteTreeRef( - pathChild(this.treePath_, childName), - this.writeTree_ - ); - } + readonly writeTree: WriteTree; } diff --git a/packages/database/src/core/view/CompleteChildSource.ts b/packages/database/src/core/view/CompleteChildSource.ts index 4521ff20fb6..7eb33c6e8b4 100644 --- a/packages/database/src/core/view/CompleteChildSource.ts +++ b/packages/database/src/core/view/CompleteChildSource.ts @@ -18,7 +18,11 @@ import { CacheNode } from './CacheNode'; import { NamedNode, Node } from '../snap/Node'; import { Index } from '../snap/indexes/Index'; -import { WriteTreeRef } from '../WriteTree'; +import { + WriteTreeRef, + writeTreeRefCalcCompleteChild, + writeTreeRefCalcIndexedSlice +} from '../WriteTree'; import { ViewCache, viewCacheGetCompleteServerSnap } from './ViewCache'; /** @@ -91,7 +95,7 @@ export class WriteTreeCompleteChildSource implements CompleteChildSource { this.optCompleteServerCache_ != null ? new CacheNode(this.optCompleteServerCache_, true, false) : this.viewCache_.serverCache; - return this.writes_.calcCompleteChild(childKey, serverNode); + return writeTreeRefCalcCompleteChild(this.writes_, childKey, serverNode); } } @@ -107,7 +111,8 @@ export class WriteTreeCompleteChildSource implements CompleteChildSource { this.optCompleteServerCache_ != null ? this.optCompleteServerCache_ : viewCacheGetCompleteServerSnap(this.viewCache_); - const nodes = this.writes_.calcIndexedSlice( + const nodes = writeTreeRefCalcIndexedSlice( + this.writes_, completeServerData, child, 1, diff --git a/packages/database/src/core/view/ViewProcessor.ts b/packages/database/src/core/view/ViewProcessor.ts index 1160c36fc63..2156ac9c4e6 100644 --- a/packages/database/src/core/view/ViewProcessor.ts +++ b/packages/database/src/core/view/ViewProcessor.ts @@ -46,7 +46,14 @@ import { viewCacheUpdateServerSnap } from './ViewCache'; import { NodeFilter } from './filter/NodeFilter'; -import { WriteTreeRef } from '../WriteTree'; +import { + WriteTreeRef, + writeTreeRefCalcCompleteChild, + writeTreeRefCalcCompleteEventCache, + writeTreeRefCalcCompleteEventChildren, + writeTreeRefCalcEventCacheAfterServerOverwrite, + writeTreeRefShadowingWrite +} from '../WriteTree'; import { Overwrite } from '../operation/Overwrite'; import { Merge } from '../operation/Merge'; import { AckUserWrite } from '../operation/AckUserWrite'; @@ -217,7 +224,7 @@ function viewProcessorGenerateEventCacheAfterServerEvent( accumulator: ChildChangeAccumulator ): ViewCache { const oldEventSnap = viewCache.eventCache; - if (writesCache.shadowingWrite(changePath) != null) { + if (writeTreeRefShadowingWrite(writesCache, changePath) != null) { // we have a shadowing write, ignore changes return viewCache; } else { @@ -237,7 +244,8 @@ function viewProcessorGenerateEventCacheAfterServerEvent( serverCache instanceof ChildrenNode ? serverCache : ChildrenNode.EMPTY_NODE; - const completeEventChildren = writesCache.calcCompleteEventChildren( + const completeEventChildren = writeTreeRefCalcCompleteEventChildren( + writesCache, completeChildren ); newEventCache = viewProcessor.filter.updateFullNode( @@ -246,7 +254,8 @@ function viewProcessorGenerateEventCacheAfterServerEvent( accumulator ); } else { - const completeNode = writesCache.calcCompleteEventCache( + const completeNode = writeTreeRefCalcCompleteEventCache( + writesCache, viewCacheGetCompleteServerSnap(viewCache) ); newEventCache = viewProcessor.filter.updateFullNode( @@ -265,7 +274,8 @@ function viewProcessorGenerateEventCacheAfterServerEvent( const oldEventNode = oldEventSnap.getNode(); serverNode = viewCache.serverCache.getNode(); // we might have overwrites for this priority - const updatedPriority = writesCache.calcEventCacheAfterServerOverwrite( + const updatedPriority = writeTreeRefCalcEventCacheAfterServerOverwrite( + writesCache, changePath, oldEventNode, serverNode @@ -285,7 +295,8 @@ function viewProcessorGenerateEventCacheAfterServerEvent( let newEventChild; if (oldEventSnap.isCompleteForChild(childKey)) { serverNode = viewCache.serverCache.getNode(); - const eventChildUpdate = writesCache.calcEventCacheAfterServerOverwrite( + const eventChildUpdate = writeTreeRefCalcEventCacheAfterServerOverwrite( + writesCache, changePath, oldEventSnap.getNode(), serverNode @@ -300,7 +311,8 @@ function viewProcessorGenerateEventCacheAfterServerEvent( newEventChild = oldEventSnap.getNode().getImmediateChild(childKey); } } else { - newEventChild = writesCache.calcCompleteChild( + newEventChild = writeTreeRefCalcCompleteChild( + writesCache, childKey, viewCache.serverCache ); @@ -661,7 +673,7 @@ function viewProcessorAckUserWrite( completeCache: Node | null, accumulator: ChildChangeAccumulator ): ViewCache { - if (writesCache.shadowingWrite(ackPath) != null) { + if (writeTreeRefShadowingWrite(writesCache, ackPath) != null) { return viewCache; } @@ -765,7 +777,7 @@ function viewProcessorRevertUserWrite( accumulator: ChildChangeAccumulator ): ViewCache { let complete; - if (writesCache.shadowingWrite(path) != null) { + if (writeTreeRefShadowingWrite(writesCache, path) != null) { return viewCache; } else { const source = new WriteTreeCompleteChildSource( @@ -778,7 +790,8 @@ function viewProcessorRevertUserWrite( if (pathIsEmpty(path) || pathGetFront(path) === '.priority') { let newNode; if (viewCache.serverCache.isFullyInitialized()) { - newNode = writesCache.calcCompleteEventCache( + newNode = writeTreeRefCalcCompleteEventCache( + writesCache, viewCacheGetCompleteServerSnap(viewCache) ); } else { @@ -787,7 +800,8 @@ function viewProcessorRevertUserWrite( serverChildren instanceof ChildrenNode, 'serverChildren would be complete if leaf node' ); - newNode = writesCache.calcCompleteEventChildren( + newNode = writeTreeRefCalcCompleteEventChildren( + writesCache, serverChildren as ChildrenNode ); } @@ -799,7 +813,8 @@ function viewProcessorRevertUserWrite( ); } else { const childKey = pathGetFront(path); - let newChild = writesCache.calcCompleteChild( + let newChild = writeTreeRefCalcCompleteChild( + writesCache, childKey, viewCache.serverCache ); @@ -836,7 +851,8 @@ function viewProcessorRevertUserWrite( viewCache.serverCache.isFullyInitialized() ) { // We might have reverted all child writes. Maybe the old event was a leaf node - complete = writesCache.calcCompleteEventCache( + complete = writeTreeRefCalcCompleteEventCache( + writesCache, viewCacheGetCompleteServerSnap(viewCache) ); if (complete.isLeafNode()) { @@ -850,7 +866,7 @@ function viewProcessorRevertUserWrite( } complete = viewCache.serverCache.isFullyInitialized() || - writesCache.shadowingWrite(newEmptyPath()) != null; + writeTreeRefShadowingWrite(writesCache, newEmptyPath()) != null; return viewCacheUpdateEventSnap( viewCache, newEventCache, diff --git a/yarn.lock b/yarn.lock index a61ffd889b4..39dc91d9504 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1219,6 +1219,13 @@ faye-websocket "0.11.3" tslib "^1.11.1" +"@firebase/util@0.3.4": + version "0.3.4" + resolved "https://registry.npmjs.org/@firebase/util/-/util-0.3.4.tgz#e389d0e0e2aac88a5235b06ba9431db999d4892b" + integrity sha512-VwjJUE2Vgr2UMfH63ZtIX9Hd7x+6gayi6RUXaTqEYxSbf/JmehLmAEYSuxS/NckfzAXWeGnKclvnXVibDgpjQQ== + dependencies: + tslib "^1.11.1" + "@google-cloud/common@^3.6.0": version "3.6.0" resolved "https://registry.npmjs.org/@google-cloud/common/-/common-3.6.0.tgz#c2f6da5f79279a4a9ac7c71fc02d582beab98e8b" @@ -15207,7 +15214,6 @@ symbol-observable@^1.1.0: "sync-promise@git+https://github.com/brettz9/sync-promise.git#full-sync-missing-promise-features": version "1.0.1" - uid "25845a49a00aa2d2c985a5149b97c86a1fcdc75a" resolved "git+https://github.com/brettz9/sync-promise.git#25845a49a00aa2d2c985a5149b97c86a1fcdc75a" table@^6.0.4: @@ -16551,7 +16557,6 @@ websocket-extensions@>=0.1.1: "websql@git+https://github.com/brettz9/node-websql.git#configurable-secure2": version "1.0.0" - uid "5149bc0763376ca757fc32dc74345ada0467bfbb" resolved "git+https://github.com/brettz9/node-websql.git#5149bc0763376ca757fc32dc74345ada0467bfbb" dependencies: argsarray "^0.0.1"