import { assert , copyPropertyIfDefined , fail } from './Common' ; import { NodeId , TraitLabel } from './Identifiers' ; import { getChangeNodeFromSnapshot } from './SnapshotUtilities' ; import { Delta , Forest } from './Forest' ; import { ChangeNode , NodeData , Payload , TraitLocation } from './generic' ; public export interface SnapshotNode extends NodeData { readonly traits : ReadonlyMap < TraitLabel , readonly NodeId [ ] > ; } public export type PlaceIndex = number & { readonly PlaceIndex : unique symbol ; } ; public export type TraitNodeIndex = number & { readonly TraitNodeIndex : unique symbol ; } ; public export interface SnapshotPlace { readonly sibling ? : NodeId ; readonly side : Side ; readonly trait : TraitLocation ; } public export enum Side { Before = 0 , After = 1 } public export interface SnapshotRange { readonly start : SnapshotPlace ; readonly end : SnapshotPlace ; } public export class Snapshot { public readonly root : NodeId ; private readonly forest : Forest ; /**
     * A cache of node's index within their parent trait.
     * Used to avoid redundant linear scans of traits.
     * Not shared across snapshots; initialized to empty each time a Snapshot is created.
     */ private traitIndicesCache ? : Map < NodeId , TraitNodeIndex > ; param root public static fromTree ( root , expensiveValidation = false ) { function insertNodeRecursive ( node , newSnapshotNodes ) { const { identifier , definition } = node ; const traits = new Map ( ) ; for ( const key in node . traits ) { if ( Object . prototype . hasOwnProperty . call ( node . traits , key ) ) { const element = node . traits [ key ] ; if ( element . length > 0 ) { traits . set ( key as TraitLabel , element . map ( ( n ) => insertNodeRecursive ( n , newSnapshotNodes ) ) ) ; } } } const snapshotNode = { identifier , definition , traits } ; copyPropertyIfDefined ( node , snapshotNode , 'payload' ) ; assert ( ! newSnapshotNodes . has ( identifier ) , `duplicate node in tree for snapshot: { identifier: ${ identifier }, definition: ${ definition }` ) ; newSnapshotNodes . set ( snapshotNode . identifier , snapshotNode ) ; return snapshotNode . identifier ; } const map = new Map < NodeId , SnapshotNode > ( ) ; return new Snapshot ( insertNodeRecursive ( root , map ) , Forest . create ( expensiveValidation ) . add ( map . values ( ) ) ) ; } private constructor ( root , forest ) { this . root = root ; this . forest = forest ; } /** Return a tree of JSON-compatible `ChangeNode`s representing the current state of this `Snapshot` */ public getChangeNodeTree ( ) { return getChangeNodeFromSnapshot ( this , this . root ) ; } /**
     * Returns the number of nodes in this `Snapshot`
     */ public get size ( ) : number { return this . forest . size ; } returns public hasNode ( id ) { return this . forest . tryGet ( id ) !== undefined ; } returns public getChangeNode ( id ) { return getChangeNodeFromSnapshot ( this , id ) ; } returns public getChangeNodes ( nodeIds ) { return nodeIds . map ( ( id ) => this . getChangeNode ( id ) ) ; } /**
     * Asserts the forest is in a consistent state.
     */ public assertConsistent ( ) { this . forest . assertConsistent ( ) ; } /**
     * Inserts all nodes in a NodeSequence into the forest.
     */ public addNodes ( sequence ) { return new Snapshot ( this . root , this . forest . add ( sequence ) ) ; } /**
     * Remove all nodes with the given ids from the forest
     */ public deleteNodes ( nodes ) { return new Snapshot ( this . root , this . forest . delete ( nodes , true ) ) ; } param nodesToAttach param place public attachRange ( nodesToAttach , place ) { const { parent , label } = place . trait ; const index = this . findIndexWithinTrait ( place ) ; return new Snapshot ( this . root , this . forest . attachRangeOfChildren ( parent , label , index , nodesToAttach ) ) ; } param rangeToDetach public detachRange ( rangeToDetach ) { const { start , end } = rangeToDetach ; const { trait : traitLocation } = start ; const { parent , label } = traitLocation ; const startIndex = this . findIndexWithinTrait ( start ) ; const endIndex = this . findIndexWithinTrait ( end ) ; const { forest , detached } = this . forest . detachRangeOfChildren ( parent , label , startIndex , endIndex ) ; return { snapshot : new Snapshot ( this . root , forest ) , detached } ; } param nodeId param value public setNodeValue ( nodeId , value ) { return new Snapshot ( this . root , this . forest . setValue ( nodeId , value ) ) ; } returns public findIndexWithinTrait ( place ) { if ( place . sibling === undefined ) { return this . getIndexOfSide ( place . side , place . trait ) ; } return getIndex ( place . side , this . getIndexInTrait ( place . sibling ) ) ; } /**
     * Returns the node associated with `id` in this `Snapshot`.
     */ public getSnapshotNode ( id ) { return this . forest . get ( id ) ; } /**
     * Returns the label of the trait that a node is under. Returns undefined if the node is not present or if it is the root node.
     */ public getTraitLabel ( id ) { return this . forest . tryGetParent ( id ) ?. traitParent ; } /**
     * Returns the parent of a node. Returns undefined if the node does not exist in the snapshot or if it does not have a parent.
     */ public getParentSnapshotNode ( id ) { const parentInfo = this . forest . tryGetParent ( id ) ; if ( parentInfo === undefined ) { return undefined ; } return this . getSnapshotNode ( parentInfo . parentId ) ; } param node public getTraitLocation ( node ) { const parentData = this . forest . getParent ( node ) ; assert ( parentData !== undefined , 'node must have parent' ) ; return { parent : parentData . parentId , label : parentData . traitParent } ; } param node public getIndexInTrait ( node ) { if ( this . traitIndicesCache === undefined ) { this . traitIndicesCache = new Map ( ) ; } else { const cached = this . traitIndicesCache . get ( node ) ; if ( cached !== undefined ) { return cached ; } } const parentData = this . forest . getParent ( node ) ; const parent = this . forest . get ( parentData . parentId ) ; const traitParent = parent . traits . get ( parentData . traitParent ) ?? fail ( 'invalid parentData: trait parent not found.' ) ; let foundIndex = - 1 as TraitNodeIndex ; for ( let i = 0 ; i < traitParent . length ; i ++ ) { const nodeInTrait = traitParent [ i ] ; const index = i as TraitNodeIndex ; this . traitIndicesCache . set ( nodeInTrait , index ) ; if ( nodeInTrait === node ) { foundIndex = index ; } } return foundIndex !== - 1 ? foundIndex : fail ( 'invalidParentData: node not found in specified trait' ) ; } param traitLocation public getTrait ( traitLocation ) { return this . getSnapshotNode ( traitLocation . parent ) . traits . get ( traitLocation . label ) ?? [ ] ; } private getIndexOfSide ( side , traitLocation ) { return side === Side . After ? ( 0 as PlaceIndex ) : ( this . getTrait ( traitLocation ) . length as PlaceIndex ) ; } /** Compares this snapshot to another for equality. */ public equals ( snapshot ) { if ( this . root !== snapshot . root ) { return false ; } return this . forest . equals ( snapshot . forest ) ; } private * iterateNodeDescendants ( nodeId ) { const node = this . getSnapshotNode ( nodeId ) ; yield node ; for ( const trait of node . traits . values ( ) ) { for ( const childId of trait ) { yield * this . iterateNodeDescendants ( childId ) ; } } } param snapshot returns A  Delta which nodes must be changed, added, and removed to get from `this` to `snapshot`.
     * The snapshots must share a root.
      public delta ( snapshot ) { assert ( this . root === snapshot . root , 'Delta can only be calculated between snapshots that share a root' ) ; return this . forest . delta ( snapshot . forest ) ; } public [ Symbol . iterator ] ( ) { return this . iterateNodeDescendants ( this . root ) ; } } function getIndex ( side , index ) { return ( side + index ) as PlaceIndex ; } public export interface NodeInTrait { readonly trait : TraitLocation ; readonly index : TraitNodeIndex ; }