import {TestableData, useNodesStore} from "./store"; import {Testables, TestablesState} from "./testables"; import {BranchData, BranchNodeEvent, StateBranchNode, TieredData, TieredExtraData} from "./StateBranchNode"; import {TestableCompositeNode} from "./TestableCompositeNode"; import {Offers, OffersState} from "./Offers"; import {TreeNode, TreeNodeEvent} from "./TreeNode"; import {TestableNode} from "./TestableNode"; import {Flow, FlowState} from "./Flow"; import {NestedTestableNodeData} from "./InMemoryTestableNode"; import {MemoryBranchNode, NestedBranchData} from "./MemoryBranchNode"; import {TreeNodeFactory} from "./TreeNodeFactory"; import {generateIdForType} from "./NodeHelpers"; import {TierType} from "./tiers"; export type TreeNodeData = { id: string, testableId: string, branch: BranchData, extraData?: any, } export type NestedTreeNodeData = { id: string, testable: NestedTestableNodeData, branch: NestedBranchData, parentBranchTestableId?: string, extraData?: any } export type NestedTreeNodeDataWithoutIds = { testable: Omit, branch: Omit, 'id'>, parentBranchTestableId?: string, extraData?: any } export type GroveData = TreeNodeData[] export type GroveState = { grove: GroveData } export type PreconditionsState = { preconditionsID: string | undefined, // the id of the root testable for preconditions, which is always a testable composite group } export type PredatesState = { predatesID: string | undefined, // the id of the root testable for preconditions, which is always a testable composite group } export type FullState = Partial & Partial & GroveState & OffersState & TestablesState & FlowState /** * This is only meant for reading the grove data from the store. * * Do not use this to modify the grove data. */ export const getGroveForReading = (state?: FullState) => { return Grove.fromState(state || useNodesStore.getState()) }; export type PossibleNodes = TreeNode | TestableCompositeNode | TestableNode | StateBranchNode export interface BeforeAddingToGroveHook { beforeAddingToGrove: () => void } // no the hook for before and after removing export interface BeforeRemovingFromGroveHook { beforeRemovingFromGrove: () => void } export interface AfterRemovingFromGroveHook { afterRemovingFromGrove: () => void } export type StateObjects = { state: FullState, grove: Grove, testables: Testables, offers: Offers, flow: Flow, treeNodeFactory: TreeNodeFactory }; export class Grove { private treeNodesCreated: Record = {} constructor( public readonly grove: GroveData, private readonly offers: Offers, private readonly testables: Testables, private readonly flow: Flow, private readonly treeNodeFactory: TreeNodeFactory ) { treeNodeFactory.setGrove(this) } /** * IMPORTANT: This should be the only way to get a TreeNode instance, * since the grove registers events needed to re-render the flow on changes * and because ancestors relationships are set here */ get(id: string | undefined): Type | undefined { if (!id) { // for methods or properties that may return undefined return undefined; } const find = (trees: TreeNodeData[], id: string): Type | undefined => { for (const tree of trees) { if (tree.id === id) { return this.createTestableNodeFromTreeNodeData(tree).getTreeNode() as unknown as Type | undefined; } else if (tree.testableId === id) { //const testable = new Testable(this.manager, id, true) as unknown as Type; //return this.createTestableNodeFromTreeNodeData(tree) as unknown as Type | undefined; // we need the parent to be set, so unfortunately we have to re-search this node using the tree node id // it sucks for performance but let's make it work and then we can optimize later const treeNode = this.get(tree.id)! return treeNode.testableNode as unknown as Type | undefined; } else if (tree.branch.id === id) { //const testable = this.createTestableNodeFromTreeNodeData(tree) // we need the parent to be set, so unfortunately we have to re-search this node using the tree node id // it sucks for performance but let's make it work and then we can optimize later const treeNode = this.get(tree.id)! return treeNode.branchNode as unknown as Type | undefined; } else if (id === 'tobedone') { } else if (tree.branch.type === 'tiered') { if ((tree.branch.data as TieredData).baseTestableId === id) { const treeNode = this.get(tree.id)! return treeNode.branchNode.testableBaseNode as unknown as Type | undefined; } // check the children and they descendants const result = find(tree.branch.children as TreeNodeData[], id); if (result) { // make sure we set the parent tree node ONLY IF IT HAS NOT BEEN SET! if (result instanceof TreeNode) { let parentTreeNode: TreeNode /** * IMPORTANT! * Make sure we set the tree node if its already been created SO THAT ITS PARENT IS SAVED * OTHERWISE THE FARTHEST ANCESTORS WON'T BE SET */ if (this.treeNodesCreated[tree.id]) { parentTreeNode = this.treeNodesCreated[tree.id] } else { /** * so unfortunately we have to re-search this node using the tree node id so that ALL the ancestors are set correctly * bad for performance but let's make it work now * * For some reason, this.createTestableNodeFromTreeNodeData(tree).getTreeNode() work when testing in grove.test.ts, * but it fails in groveActions.test.ts. I dont know why and honestly I don't care right now im extremely exhausted * this turned out to be absolutely more complex than I expected */ //parentTreeNode = this.createTestableNodeFromTreeNodeData(tree).getTreeNode() parentTreeNode = this.get(tree.id)! } (result as unknown as TreeNode).setParentBranchNode(parentTreeNode.branchNode) this.treeNodesCreated[(result as unknown as TreeNode).id] = result as unknown as TreeNode this.treeNodesCreated[parentTreeNode.id] = parentTreeNode } return result; } } } } return find(this.grove, id) } isFirst(treeId): boolean { return this.getIndexOfTree(treeId) === 0; } /** * Returns the index of the tree FIRST LEVEL ONLY * * It can be either the testableId or the branchId */ getIndexOfTree(treeId): number | undefined { return this.grove.findIndex((tree) => tree.id === treeId || tree.testableId === treeId || tree.branch.id === treeId); } getPreviousTreeNode(treeId): TreeNode | undefined { let lastIndex: number = -1 for (const [index, tree] of this.grove.entries()) { if (tree.id === treeId) { break } lastIndex = index } if (lastIndex > -1) { return this.get(this.grove[lastIndex].id) } } getTreeByIndex(index: number) { return this.grove[index]; } /** * This is a pure function and DOES NOT modify the flow state. * * If you are calling this from the UI, please use GroveActions.addTreeNode() instead so that the flow data is updated. */ addTreeNode(treeNode: TreeNode & Partial) { // first call the 'before adding' hook so that the testable and branch nodes can prepare the data if need be treeNode.beforeAddingToGrove?.(); // then add it to the state this.grove.push(treeNode.exportToState()) } removeTreeNode(treeId: string) { const index = this.getIndexOfTree(treeId); if (typeof index === 'number' && index !== -1) { const treeNode = this.get>(treeId)! this.grove.splice(index, 1); treeNode?.afterRemovingFromGrove?.(); this.flow.removeTreeNode(treeNode.id) } } private createTestableNodeFromTreeNodeData(tree: TreeNodeData) { const {testableNode, branch} = this.treeNodeFactory.createComponentsFromStateTreeNodeData(tree) const treeNode = TreeNode.createTemporaryFromComponentsWithoutAncestors( testableNode, branch, this, tree.id, tree.extraData ); if (treeNode.branchNode.isTiered()) { // set the base testable node treeNode.branchNode.setTestableBase( new TestableCompositeNode( this.testables, (treeNode.branchNode as StateBranchNode).getExtraData().baseTestableId ) ) } // listen for tree node changes treeNode.addSubscriber(TreeNodeEvent.BranchAdded, this.onBranchAdded.bind(this)); (treeNode.branchNode as StateBranchNode).addSubscriber(BranchNodeEvent.ChildrenAdded, this.updateGroveFromBranch.bind(this)); (treeNode.branchNode as StateBranchNode).addSubscriber(BranchNodeEvent.ChildrenRemoved, this.updateGroveFromBranch.bind(this)); return testableNode; } private updateGroveFromBranch({branch}) { this.setTreeNodeDataInGrove(branch.getTreeNode()) } onBranchAdded({branch}) { this.flow.renderTreeNode(branch.getTreeNode()) this.setTreeNodeDataInGrove(branch.getTreeNode()) } public setTreeNodeDataInGrove(treeNode: TreeNode) { // find the index of the tree node in the grove // then replace the tree node with the new one with generatetreendoedata() function exportTreeNodeState(grove: GroveData) { for (const i in grove) { if (grove[i].id === treeNode.id) { grove[i] = treeNode.exportToState() return true } // check the children const branch = grove[i].branch; const children = branch.children if (branch.type === 'tiered' && Array.isArray(children)) { const found = exportTreeNodeState(children as TreeNodeData[]) if (found) { return true } } } } exportTreeNodeState(this.grove); } afterRemovedRootTestableGroup({id}) { // remove the tree node this.removeTreeNode( this.get(id)?.getTreeNode().id! ) } /** * fromState expects a state whose objects have IDS! */ private static _cachedState: FullState | null = null; private static _cachedGrove: Grove | null = null; private static _cachedStateObjects: StateObjects | null = null; static fromState(state: FullState, withComponents: true): StateObjects; static fromState(state: FullState, withComponents?: false): Grove; static fromState(state: FullState, withComponents: boolean = false) { // Reuse cached instance when selectors receive the same state snapshot if (Grove._cachedState === state) { if (withComponents && Grove._cachedStateObjects) return Grove._cachedStateObjects; if (!withComponents && Grove._cachedGrove) return Grove._cachedGrove; } const testables = new Testables(state) const offers = new Offers(state) const treeNodeFactory = new TreeNodeFactory(testables, offers) const flow = new Flow(state); const grove = new Grove( state.grove, offers, testables, flow, treeNodeFactory ) treeNodeFactory.setGrove(grove) Grove._cachedState = state; Grove._cachedGrove = grove; if (withComponents) { const stateObjects = { state, grove, testables, offers, flow, treeNodeFactory }; Grove._cachedStateObjects = stateObjects; return stateObjects; } Grove._cachedStateObjects = null; return grove } static getBranchDataFromState(grove: GroveData, id: string) { for (const treeNodeData of grove) { if (treeNodeData.branch.id === id) { return treeNodeData.branch } } } resetCache() { //this.treeNodesCreated = {} } convertBranchToTiered(branchToConvert: StateBranchNode, data: { baseTestable: Omit, tierType: TierType, treeNodeData: NestedTreeNodeData | NestedTreeNodeDataWithoutIds }): StateBranchNode | undefined { // the given tree node IS the tiered branch so we need to replace const treeNode = branchToConvert.getTreeNode() const {baseTestable, tierType, treeNodeData} = data const newMemoryBranch = new MemoryBranchNode( { id: generateIdForType('branch'), type: 'tiered', data: { baseTestable, tierType, } as TieredExtraData, children: [ treeNodeData ] }, this.offers, this.testables ) // first create the node if we were given nested data treeNode.setBranchNode(newMemoryBranch) // let's update the grove with the new branch so that we can use it later to add the offer this.setTreeNodeDataInGrove(treeNode) return this.get(newMemoryBranch.getId())! } /** * todo: needs to account for nested trees (it's currently shallow (1st level only)) */ getTrees(): TreeNode[] { return this.grove.map(treeData => this.get(treeData.id)!).filter(t => t !== undefined) } }