import { sendParent, createMachine, assign, send } from 'xstate'; import { nanoid } from '../Services/nanoid'; import { runQuery } from "../../Validation"; // TODO: this should move into @iioioo_utils import type { HierarchyNode } from 'd3-hierarchy'; import type { IFieldBase } from '$lib/Models'; export interface IStack { stack: string[]; currentIdx: number; parentId: string; type: 'regular' | 'fork' | 'root' | 'incomplete'; } export interface IForkedOption { id: string; dependeeId: string; siblings?: string[]; status: 'unreached' | 'passed' | 'engaged', } export type StackContext = { id: string, rootStackId: string, path: string[], nodes: HierarchyNode[], currentStackId: string, currentActiveId: string, stacks: Record, forks: Record, forkDescendents: Record, forksTaken: Record, broadcastEvent: string } export type PipeResult = { nextStackId: string, stackNextIdx: number, focusNode?: HierarchyNode } // there is confusion built into this model. // current stack id and current active id should not both be necessary // branches and stacks should not both be necessary // also there is need to be really really clear about working only with leaves... // the next 'thing' is always a leaf // the previous 'thing' is always a leaf // the benefit of working in this D3 paradigm is what comes out of the box. Use it. // children, ancestors, descendents, depth, height, traversal. Understand it and implement it. // https://devdocs.io/d3~7/d3-hierarchy#node_ancestors // streams should have one access point --> probably hierarchy machine, and everything else should be internalised and delegated. // for instance, the component should not have any logic in it, especially not differentiating about what to show or not. // it should be one loop of a component that the hierarchy machine feeds with ever-changing but always relevant data const createStackSpine = ( context: StackContext, event ) => { const nodes = context.nodes; const forks: { [key: string]: IForkedOption } = {}; const forkDescendents: { [key: string]: IForkedOption } = {}; // Note --> this works well now but still needs review. Seems a little brittle. let stacks = nodes.reduce((acc: any, curr: HierarchyNode) => { // grab forked values const forkedDependants = !!curr.data.fork && Object.keys(curr.data.fork); if(forkedDependants) { forkedDependants.forEach(f => { // forks[f] = true; forks[f] = { id: f, dependeeId: curr.id, status: 'unreached', siblings: Object.keys(curr.data.fork) }; const forkedNode = nodes.find(n => n.id === f); console.log("FORK! ", nodes, forkedDependants, forkedNode); if(forkedNode.data.type !== 'form') { acc[ f ] = { currentIdx: 0, stack: [ f ], parentId: curr.data.parentId, type: 'fork' }; } // exp else { forkedNode.descendants().forEach(d => { if(d.id !== forkedNode.id){ forkDescendents[ d.id ] = { id: d.id, dependeeId: forkedNode.id, status: 'unreached' } } }); } }); } // separate out all legitimate sections const currentValue = !!acc[ curr.data.parentId ] ? acc[ curr.data.parentId ] : { currentIdx: 0, stack: [] }; acc[ curr.data.parentId ] = { currentIdx: 0, stack: !!forks[ curr.id] ? currentValue.stack : [ ...currentValue.stack, curr.id ], parentId: curr.parent?.parent?.id, type: !!forks[ curr.data.parentId ] ? 'fork' : 'regular' }; return acc; }, { 'incomplete': { stack: nodes.map(node => node.id), currentIdx: 0, parentId: 'root', type: 'incomplete' }, 'rootId': { stack: nodes.filter(n => n.data.parentId === 'rootId').map(node => node.id), currentIdx: 0, parentId: null, type: 'root' } } as Record); Object.values(stacks).forEach((stack: IStack) => stack.stack = stack.type !== 'fork' ? stack.stack.filter(id => !forks[ id ]) : stack.stack); console.log("RACK EM PACK EM SHIP EM: ", forks, forkDescendents, stacks); return { forks, forkDescendents, stacks }; } export const stackMachine = createMachine({ tsTypes: {} as import("./stack.machine.typegen").Typegen0, schema: { context: {} as StackContext, events: {} as { type: 'GET_ACTIVE' } | { type: 'GET_NEXT', node: HierarchyNode } | { type: 'GET_PREVIOUS' } | { type: 'GET_BY_ID', node: HierarchyNode } | { type: 'SEND_TO_LAST_IN_STACK', node: HierarchyNode } | { type: 'SEND_TO_SIMPLE', node: HierarchyNode } | { type: 'CLEAR_STACK' }, services: {} as { // getNext: { data: string; }, getNext: { data: PipeResult }, getSimpleNext: { data: PipeResult }, getForkNext: { data: PipeResult }, getLastInStackNext: { data: PipeResult }, getPrevious: { data: PipeResult }, getById:{ data: PipeResult } }, }, context: { id: '', path: [], nodes: [], rootStackId: '', currentStackId: '', currentActiveId: '', stacks: {}, forks: {}, forkDescendents: {}, forksTaken: {}, broadcastEvent: '' }, initial: 'initialising', states: { initialising: { entry: [ 'assignSpine' ], always: { target: 'idle' } }, idle: {}, determiningPreviousState: { invoke: { id: nanoid(), src: 'getPrevious', onDone: { target: 'idle', actions: [ 'assignActiveId', 'diminishStack', 'broadcastOnDone', 'updateIncomplete' ] }, onError: { target: 'idle', actions: [ 'navigateStateError' ] } } }, determiningById: { invoke: { id: nanoid(), src: 'getById', onDone: { target: 'idle', actions: [ 'assignActiveId', 'augmentStack', 'broadcastOnDone', 'updateIncomplete' ] }, onError: { target: 'idle', actions: [ 'navigateStateError' ] } } }, // the current active node is either simple, fork, section or lastInStack simple: { invoke: { id: nanoid(), src: 'getSimpleNext', onDone: { target: 'idle', actions: [ 'assignActiveId', 'augmentStack', 'broadcastOnDone', 'updateIncomplete' ] }, onError: { target: 'idle', actions: [ 'navigateStateError' ] } } }, fork: { invoke: { id: nanoid(), src: 'getForkNext', onDone: [ { target: 'idle', actions: [ 'assignActiveId', 'assignForkTaken', 'augmentStack', 'broadcastOnDone', 'updateIncomplete' ], cond: (context, event) => { return !!event.data.nextStackId; } }, { actions: [ // 'sendToSimple', TODO: this might have bugs 'sendToLastInStack', 'updateIncomplete' ] } ], onError: { target: 'idle', actions: [ 'navigateStateError' ] } } }, lastInStack: { invoke: { id: nanoid(), src: 'getLastInStackNext', onDone: { target: 'idle', actions: [ 'assignActiveId', 'augmentStack', 'sendToSimple', 'updateIncomplete' ] }, onError: { target: 'idle', actions: [ 'navigateStateError' ] } } } }, on: { GET_ACTIVE: { actions: [ 'getActive' ] }, GET_BY_ID: 'determiningById', GET_NEXT: [ { target: 'fork', cond: 'isForkedNext' }, { target: 'lastInStack', cond: 'isLastInStack' }, { target: 'simple' } ], SEND_TO_LAST_IN_STACK: [ { target: 'lastInStack', cond: 'isForkedLastInStack' }, { target: 'simple' } ], SEND_TO_SIMPLE: 'simple', GET_PREVIOUS: { target: 'determiningPreviousState', cond: 'hasPrevious' }, CLEAR_STACK: { actions: [ 'clearPath' ] } } }, { services: { getSimpleNext: async (context, event) => { let nextStackId, nextStack, stackNextIdx, forkedKey; // if(event.node) { // // nextStackId = context.stacks[ context.currentStackId ].parentId; // // nextStack = context.stacks[ nextStackId ]; // // stackNextIdx = nextStack.currentIdx; // // focusNode = context.nodes.find( node => node.id === nextStack.stack[ stackNextIdx ] ); // nextStackId = context.currentStackId; // nextStack = context.stacks[ nextStackId ]; // stackNextIdx = (nextStack.stack.length - 1) !== nextStack.currentIdx ? // nextStack.currentIdx + 1 : // nextStack.currentIdx; // } // else { nextStackId = context.currentStackId; nextStack = context.stacks[ nextStackId ]; stackNextIdx = (nextStack.stack.length - 1) !== nextStack.currentIdx ? nextStack.currentIdx + 1 : nextStack.currentIdx; // } return { nextStackId, nextStack, stackNextIdx } }, getForkNext: async (context, event) => { let nextStackId, nextStack, stackNextIdx, forkedKey; try { // TODO: this needs a more elegant solution const value = event.node.data.type === 'choice' ? event.node.data.value[0] : event.node.data.value; const potentialKeys = await Promise.all( Object.keys(event.node.data.fork).map(async key => { const { operator, anchorValues } = event.node.data.fork[ key ]; // const promise = await runQuery( operator, event.node.data.value, anchorValues ); const promise = await runQuery( operator, value, anchorValues ); return !!promise ? key : ''; })); // Note --> the first successful fork will be taken. forkedKey = potentialKeys.find(key => !!key); if(forkedKey) { nextStackId = forkedKey; nextStack = context.stacks[ nextStackId ]; stackNextIdx = nextStack.currentIdx; } } catch(error) { console.error("There was an error in Stack Machine"); } return { nextStackId, nextStack, stackNextIdx, focusNode: !nextStackId ? event.node : null } }, getLastInStackNext: async (context, event) => { let nextStackId, nextStack, stackNextIdx, focusNode; const inFork = !!context.forks[ context.currentStackId ]; // we're in a fork const inSection = !!context.stacks[ context.currentStackId ].parentId; // we're in a section const parentIsNotRoot = context.stacks[ context.currentStackId ].parentId !== 'rootId'; const thisIsNotRoot = !!context.stacks[ context.currentStackId ].parentId; if((inFork || inSection && thisIsNotRoot) ) { // && parentIsNotRoot thisIsNotRoot // we're in fork or section nextStackId = context.stacks[ context.currentStackId ].parentId; nextStack = context.stacks[ nextStackId ]; stackNextIdx = nextStack.currentIdx; // (nextStack.stack.length - 1) !== nextStack.currentIdx ? // nextStack.currentIdx + 1 : // nextStack.currentIdx; focusNode = context.nodes.find( node => node.id === nextStack.stack[ stackNextIdx ] ); } else { // we're in root nextStackId = context.currentStackId nextStack = context.stacks[ nextStackId ]; stackNextIdx = nextStack.currentIdx; focusNode = context.nodes.find( node => node.id === context.currentActiveId ); } return { nextStackId, nextStack, stackNextIdx, focusNode }; }, getPrevious: async (context) => { const previousId = context.path.length >= 2 ? context.path[ context.path.length - 2 ] : context.path[ context.path.length - 1 ]; const nextStackId = context.nodes.find(n => n.id === previousId).data.parentId; const nextStack = context.stacks[ nextStackId ]; const stackNextIdx = nextStack.currentIdx > 0 ? nextStack.currentIdx - 1 : 0; return { nextStackId, nextStack, stackNextIdx } }, getById: async (context, event ) => { console.log("getting by id", event); const nextStackId = context.nodes.find(n => n.id === event.node.id).data.parentId; const nextStack = context.stacks[ nextStackId ]; const stackNextIdx = nextStack.stack.indexOf( event.node.id ); return { nextStackId, nextStack, stackNextIdx } } }, actions: { assignSpine: assign( (context, event) => { const { forks, stacks, forkDescendents } = createStackSpine( context, event ); context.forks = forks; context.stacks = stacks; context.forkDescendents = forkDescendents; return context; }), clearPath: assign({ path: [] as string[] }), assignActiveId: assign({ currentActiveId: (context, event) => { let id = context.stacks[ event.data.nextStackId ].stack[ event.data.stackNextIdx ]; const activeNode = context.nodes.find(n => n.id === id); id = activeNode.data.type === 'form' ? activeNode.children[0].id : id; console.log("ACTIVE ID IS NOW: ", id, event ); return id; } }), augmentStack: assign({ currentStackId: (context, event) => event.data.nextStackId, stacks: (context, event) => { context.stacks[ event.data.nextStackId ].currentIdx = event.data.stackNextIdx; return context.stacks; }, path: (context, event) =>{ const nodeId = context.stacks[ event.data.nextStackId ].stack[ event.data.stackNextIdx ] return context.path[ context.path.length - 1] === nodeId ? context.path : [ ...context.path, nodeId ]; } }), broadcastOnDone: sendParent( (context, event) => { // return { // type: context.broadcastEvent, // nodeId: context.stacks[ context.currentStackId ].stack[ context.stacks[ context.currentStackId ].currentIdx ], // stack: context.path // } return { type: context.broadcastEvent, nodeId: context.currentActiveId, stack: context.path } }), diminishStack: assign({ currentStackId: (context, event) => event.data.nextStackId, stacks: (context, event) => { context.stacks[ event.data.nextStackId ].currentIdx = event.data.stackNextIdx; return context.stacks; }, path: (context, event) => { return context.path.slice(0, context.path.length - 1) } }), assignForkTaken: assign({ forksTaken: (context, event) => { console.log("FORKS TAKEN: ", context, event); return { ...context.forksTaken, [ event.data.nextStackId ]: true }; }, forks: (context, event) => { const forkTaken = context.forks[ event.data.nextStackId ] Object.values( context.forks ).forEach(f => { if(f === forkTaken) { f.status = 'engaged'; } else if(f.dependeeId === forkTaken.dependeeId) { f.status = 'passed'; } }); return context.forks; }, forkDescendents: (context, event) => { const forkTaken = context.forks[ event.data.nextStackId ]; Object.values( context.forkDescendents ).forEach(f => { if(f.dependeeId === forkTaken.id) { f.status = 'engaged'; } else if(f.dependeeId === forkTaken.dependeeId) { f.status = 'passed'; } if(f.status === 'unreached') { const nf = context.nodes.find(n => n.id === f.id); const isDescendent = nf.ancestors().some(a => a.id === forkTaken.dependeeId || forkTaken.siblings.includes(a.id)); if(isDescendent) { f.status = 'passed'; } } }); return context.forkDescendents; } }), updateIncomplete: assign({ stacks: (context, event) => { const inc = context.stacks[ 'incomplete' ]; inc.stack = context.nodes .filter((m: HierarchyNode) => !m.data.progress) .map(n => n.id); context.stacks[ 'incomplete' ] = inc; return context.stacks; }, }), sendToLastInStack: send((context, event) => ({ type: 'SEND_TO_LAST_IN_STACK', node: event.data.focusNode })), sendToSimple: send((context, event) => ({ type: 'SEND_TO_SIMPLE', node: event.data.focusNode })), navigateStateError: (context, event) => console.error('Failed to determine next state:', event) }, guards: { hasPrevious: (context, event) => context.path.length >= 2, isForkedNext: (context, event) => !!event.node.data.fork && !!Object.keys(event.node.data.fork).length, isForkedLastInStack: (context, event) => { // debugger; const isForkStack = !!context.forks[ context.currentStackId ]; const currentStack = context.stacks[ context.currentStackId ]; const isAtEnd = currentStack.stack.length - 1 === currentStack.currentIdx return isForkStack && isAtEnd; }, isLastInStack: (context, event) => { const currentStack = context.stacks[ context.currentStackId ]; return currentStack.currentIdx === (currentStack.stack.length - 1); } } });