import { createMachine, assign, spawn, send, sendParent, forwardTo, actions } from 'xstate'; import type { HierarchyNode } from 'd3-hierarchy'; import { stratify } from 'd3-hierarchy'; const { raise, pure, choose, respond } = actions; import { stackMachine } from '$lib/@iioioo_utils/State/stack.machine'; import { delayMachine } from '$lib/@iioioo_utils/State/delayMachine'; import { stepMachine } from '$lib/State/stepMachine'; // import { selectMachine } from '$lib/State/selectMachine'; import { textMachine } from '$lib/State/textMachine'; import { choiceMachine } from '$lib/State/choiceMachine'; import { numberMachine } from '$lib/State/numberMachine'; import { nestMachine } from '$lib/State/nest2.machine'; import type { IFieldBase } from '$lib/Models'; import { FormModel} from '$lib/Models'; // import { respond, sendParent } from 'xstate/lib/actions'; import { arrayMachine } from '$lib/State/array.machine'; import { selectMachine } from '$lib/State/selectMachine'; import { forkMachine } from '$lib/State/fork.machine'; // import { imageEditingMachine } from '$lib/State/imageEditingMachine'; // import { pdfMachine } from '$lib/State/pdfMachine'; // import { listMachine } from '$lib/State/listMachine'; // import { mapMachine } from '$lib/State/mapMachine'; // import { calendarMachine } from '$lib/State/calendarMachine'; // import { csvMachine } from "$lib/State/csvMachine"; const rootId = 'rootId'; const specialistMachines = { stepMachine: stepMachine, // form: hierarchyMachine, text: textMachine, number: numberMachine, select: selectMachine, // image: imageEditingMachine, // file: pdfMachine, // list: listMachine, // map: mapMachine, choice: choiceMachine, // calendar: calendarMachine, // csv: csvMachine, nest: nestMachine, fork: forkMachine, array: arrayMachine } export type HierarchyContext = { stackMachine: any; delayMachine: any; // progressMachine timerMachine: any; // markingMachine? // translationMachine // audioMachine? // analyticsMachine? data: any; id: string; base: any; hierarchy: HierarchyNode; branches: { [key:string] : HierarchyNode }; leaves: { [key:string] : HierarchyNode }; machines: { [key:string] : any }; activeLeafId: string; activeAncestry: { [key:string] : HierarchyNode }; layout: 'stepper' | 'traditional' | 'graph' | 'paperback'; hasParent: boolean; notifyParentEventType: string; publishDraftResult: boolean; result: any; }; export type HierarchyState = { value: 'initialising'; context: HierarchyContext; } | { value: 'active'; context: HierarchyContext; } | { value: 'submitted'; context: HierarchyContext; }; const stackMachineId = 'hierarchy_stack_machine'; const delayMachineId = 'hierarchy_delay_machine'; const timerMachineId = 'hierarchy_timer_machine'; export const hierarchyMachine = createMachine({ tsTypes: {} as import("./hierarchy.machine.typegen").Typegen0, schema: { context: {} as HierarchyContext, events: {} as { type: 'SELECT_LEAF', nodeId: any } | { type: 'TIMER_ELAPSED' } | { type: 'SUBMIT' } | // routing { type: 'REFRESH_FIELDS', fieldMap: any } | { type: 'QUEUE', delay: number } | { type: 'RESET_QUEUE' } | { type: 'UNQUEUE' } | { type: 'GET_NEXT', node: HierarchyNode } | { type: 'GET_PREVIOUS' } | { type: 'PLAY_PAUSE' } | { type: 'GET_BY_ID', node: HierarchyNode } | { type: 'SOURCE_DYNAMIC_VALUE', dataKeySourceId: string, returnAction: string, returnKey: string } | { type: 'REFRESH_DATA' } }, context: { stackMachine: null, delayMachine: null, // countdown timers per leaf / branch that automatically lock and move on steps timerMachine: null, // progressMachine --> measuring progress made per leaf / branch of the stream // analyticsMachine --> optional / ability to serialize and playback exact timestamped interactions / aggregation // translationMachine --> provided else automatic // markingMachine? --> optional / states => none, learning, test / provide correct answers & marks per field // // advanced // automated persistence --> immediately store answers in cloud // group completion --> multiple parties subscribed to same stream / real-time decentralised completion // audioMachine? --> provided in translations / genders / ages // // new in june 2022 // fields <--> claims <--> verfification /\/\/\/^--> marking machine / edu-streams data: [], id: 'hierarchyMachine', base: null, hierarchy: null, branches: {}, leaves: {}, machines: {}, activeLeafId: '', activeAncestry: {}, layout: 'traditional', hasParent: false, notifyParentEventType: 'HIERARCHY_SUBMITTED', publishDraftResult: false, result: null }, initial: 'initialising', states: { initialising: { entry: [ 'setupData', 'setupMachine', 'activeAllIfTraditionalLayout', 'assignDelayMachine', 'assignTimerMachine', 'activateStepMachine' ], always: 'active' }, active: { on: { SELECT_LEAF: { actions: [ (c,e) => console.log("SING: ", c,e), 'selectLeaf', // 'addProgressToLeaf', 'rollUpProgress', 'activateStepMachine', 'assignDelayMachine', 'assignTimerMachine' ] }, TIMER_ELAPSED: { actions: [ (c,e) => console.log("TIMER ELAPSED: ", e), // 'addProgressToLeaf', // 'rollUpProgress', 'potentiallyPublishResult', 'raiseNext' ] }, SUBMIT: 'submitted', // routing QUEUE: { actions: [ (c,e) => console.log("DELAY FROM INPUT: ", e), 'queueDelay' ] }, RESET_QUEUE: { actions: [ (c,e) => console.log("RESET DELAY FROM INPUT: ", e), 'resetQueueDelay' ] }, UNQUEUE: { actions: [ (c,e) => console.log("UNQUEUE DELAY FROM INPUT: ", e), 'UnqueueDelay' ] }, GET_NEXT: { actions: [ (c,e) => console.log("GET NEXT in hierarchy: ", e), 'deactivateAll', 'getNext' ] }, GET_PREVIOUS: { actions: [ 'deactivateAll', 'getPrevious' ] }, GET_BY_ID: { actions: [ 'deactivateAll', 'getById' ] }, } }, submitted: { entry: [ 'submitNodes', 'publishResult', 'notifyParent' ], type: 'final' } }, on: { SOURCE_DYNAMIC_VALUE: { actions: [ 'respondToSourceRequest' ] }, // exp REFRESH_DATA: { actions: [ 'setupData' ] }, REFRESH_FIELDS: { actions: [ 'broadcastFields' ] }, PLAY_PAUSE: { actions: [ (c,e)=> console.log("Playing or pausing! ", c,e ), 'playOrPauseDelay' ] } } }, { actions: { setupMachine: assign((context) => { const activeLeaf = Object.values( context.leaves )[0]; const activeLeafId = activeLeaf?.id; const currentStackId = Object.values( context.leaves )[0]?.data.parentId; const activeAncestry = !!activeLeafId && !!context.leaves[ activeLeafId ] ? context.leaves[ activeLeafId ] .ancestors() .reduce((acc, curr) => ({ ...acc, [curr.data.id]: curr }), {}) : {}; return { ...context, activeLeafId, activeAncestry, stackMachine: spawn( stackMachine.withContext({ ...stackMachine.context, currentStackId, rootStackId: rootId, path: [ activeLeafId ], // nodes: Object.values( context.leaves ), nodes: [ ...Object.values( context.branches ), ...Object.values( context.leaves ) ], broadcastEvent: 'SELECT_LEAF', }), { name: stackMachineId, sync: true } ), // delayMachine: spawn( // delayMachine.withContext({ // ...delayMachine.context, // elapsed: 0, // duration: 2000, // progress: 0, // interval: 100, // initialDelay: activeLeaf?.data.waitForHowManyMSBeforeSubmit, // speed: 1, // broadcastEvent: 'TIMER_ELAPSED' // }), // { name: delayMachineId, sync: true } // ), } }), assignDelayMachine: assign({ delayMachine: (context, event) => { const activeLeaf = context.leaves[ context.activeLeafId ]; console.log("spawning new delay: ", activeLeaf?.id ); return spawn( delayMachine.withContext({ ...delayMachine.context, elapsed: 0, duration: 800, progress: 0, interval: 100, initialDelay: activeLeaf?.data.waitForHowManyMSBeforeSubmit, speed: 1, broadcastEvent: 'TIMER_ELAPSED' }), { name: delayMachineId, sync: true } ) } }), assignTimerMachine: assign({ timerMachine: (context, event) => { // Note --> currently this doesn't quite work but all the elements exist // in delayMachine to perfectly cater for timed interactions. Going to be gorgeous! const activeLeaf = context.leaves[ context.activeLeafId ]; console.log("spawning new delay: ", activeLeaf?.id ); return !!activeLeaf?.data.allottedTime && spawn( delayMachine.withContext({ ...delayMachine.context, elapsed: 0, duration: activeLeaf?.data.allottedTime, progress: 0, interval: 100, initialDelay: 0, speed: 1, broadcastEvent: 'TIMER_ELAPSED', queueImmediately: true }), { name: timerMachineId, sync: true } ) } }), setupData: assign((context, event) => { // Note --> always insert dummy root context.data = context.data.map(i => !i.parentId ? { ...i, parentId: rootId } : i); const scaffold = [ new FormModel({ id: rootId, name: 'Preview Stream', subType: 'stepper', autosubmit: true, order: 0 }), ...context.data ]; // const hierarchy = stratify()(context.data) // <-- TODO: this should be typed to FIELDBASE const hierarchy = stratify()(scaffold) // <-- TODO: this should be typed to FIELDBASE .each(d => d.data.progress = 0) .sum(d => d.waitForHowManyMSBeforeSubmit) // .sort((a, b) => b.data.order - a.data.order); // const leaves = hierarchy.leaves() // .reduce((acc, curr) => { // if(!curr.data.parentId) { // return acc; // } // return { ...acc, [curr.data.id]: curr } // }, {}); let leaves: any = hierarchy.leaves(); let find: any = hierarchy.find(n => n.id === 'experience_date_start_1'); let path: any = hierarchy.path(find || leaves[0]); let ancestors: any = find?.ancestors(); let links: any = hierarchy.links(); console.log("XXX: leaves ==> ", leaves); console.log("XXX: path ==> ", path, ancestors); console.log("XXX: links ==> ", links); console.log("XXX: find ==> ", find); leaves = leaves.reduce((acc, curr) => { if(!curr.data.parentId) { return acc; } return { ...acc, [curr.data.id]: curr } }, {}); const branches = hierarchy.descendants() .filter(d => !leaves[ d.data.id ]) .reduce((acc, curr) => ({ ...acc, [curr.data.id]: curr }), {}); // TODO: this assumes that HIERARCHY machine can only ever be used for IFieldBase values // which obviously is not going to work for the IArray or IObject field types that should // be able to accept any shape a stream owner wants answered by their users const machines = Object.values(leaves) .reduce((acc, node: HierarchyNode, idx) => { const childMachine = stepMachine.withContext({ ...stepMachine.context, node, specialistMachineScaffold: node.data.type === 'form' ? hierarchyMachine : specialistMachines[ node.data.type ] }); return { ...acc as any, [node.id]: spawn(childMachine as any, { name: node.id, sync: true }) } }, {} ); return { ...context, hierarchy, branches, leaves, machines }; }), activateStepMachine: send( (context, event) => { return { type: 'ACTIVATE_STEP' } }, { to: (context) => context.machines[ context.activeLeafId ] } ), selectLeaf: assign({ activeLeafId: (context, event) => { console.log("CONTEXT IN SELECT LEAF: ", context, event); return event.nodeId || '' }, activeAncestry: (context, event) => { const isBranch = context.branches[ event.nodeId ]; const node = isBranch || context.leaves[ event.nodeId ]; return node.ancestors().reduce((acc, curr) => ({ ...acc, [curr.data.id]: curr }), {}) } }), rollUpProgress: assign((context, event) => { // branches from highest to lowest const legitBranches = Object.values(context.branches).sort((a, b) => b.depth - a.depth); const forks = context.stackMachine.state.context.forks; const forksTaken = context.stackMachine.state.context.forksTaken const forkAboutToBeTaken = forks[ event.nodeId ]; // const forksTaken = { ...context.stackMachine.state.context.forksTaken, [ context.activeLeafId ]: forks[ context.activeLeafId ] }; legitBranches.forEach(branch => { console.log("BRANCH IN PROGRESS: ", branch, context); const children = branch.children.filter( child => !forks[child.id] || forksTaken[child.id] || forksTaken[child.parent.id] ); const accumulatedChildProgress = children.reduce((acc, curr) => (acc + curr.data.progress), 0); branch.data.progress = accumulatedChildProgress / children.length; }) return context; }), raiseNext: pure((context, event) => raise({ type: 'GET_NEXT', node: context.leaves[ context.activeLeafId ] })), activeAllIfTraditionalLayout: pure((context, event) => { return context.layout === 'traditional' && Object.values(context.machines).map(m => send({ type: 'ACTIVATE_STEP' }, { to: m })) }), deactivateAll: pure((context, event) => Object.values(context.machines).map(m => send({ type: 'DEACTIVATE_STEP' }, { to: m }))), submitNodes: pure((context, event) => Object.values(context.machines).map(m => send({ type: 'SUBMIT_NODE' }, { to: m }))), potentiallyPublishResult: choose([ { cond: (context, event) => context.publishDraftResult, actions: [ 'publishResult', 'notifyParent' ] } ]), publishResult: assign({ result: (context, event) => { // TODO: this should become fully recursive const getData = (node: HierarchyNode, result: { arr: any[], acc: { [key:string]: any } } ) => { if(!!node.children && !!node.children.length) { node.children.forEach(child => getData(child, result)); } if(node.id !== rootId) { result.arr.push( node.data ); result.acc = { ...result.acc, [ node.data.name ]: node.data.value }; } return result; } return getData( context.hierarchy, { arr: [], acc: {} } ); } }), notifyParent: choose([ { cond: (context, event) => !!context.hasParent, actions: sendParent((context, event) => { console.log("IN HIERARCHY NOTIFY PARENT: ", context); return { type: context.notifyParentEventType, data: context.result } }) } ]), // routing broadcastFields: pure((context, event) => { return Object.values(context.machines).map(m => send(event, { to: m })) }), // routing for delay queueDelay: forwardTo( delayMachineId ), resetQueueDelay: forwardTo( delayMachineId ), UnqueueDelay: forwardTo( delayMachineId ), playOrPauseDelay: forwardTo( delayMachineId ), // routing for stack getNext: send((context, event) => { console.log("sending next from hierarchy") return { type: 'GET_NEXT', node: context.leaves[ context.activeLeafId ] }; }, { to: (context, event) => context.stackMachine } ), getPrevious: forwardTo( stackMachineId ), getById: forwardTo( stackMachineId ), respondToSourceRequest: respond((context, event) => { // TODO: this still needs a fair bit of work const value = context.leaves[ event.dataKeySourceId ].data.value; return { ...event, type: 'SOURCE_DYNAMIC_VALUE_RESPONSE', value }; }) } });