Source

utils/MoorhenCommandCentre.ts

import { v4 as uuidv4 } from 'uuid';
import { MoorhenHistory } from "./MoorhenHistory"
import { moorhen } from "../types/moorhen"
import { webGL } from '../types/mgWebGL';

/**
 * A command centre used to communicate between Moorhen and a web worker running an instance of the 
 * headless libcoot API
 * @property {Worker} cootWorker - A web worker holding a headless libcoot instance
 * @constructor
 * @param {string} urlPrefix - The root url used to find the baby-gru/CootWorker.js worker file
 * @param {function} onConsoleChanged - Callback executed whenever the worker prints a message to the console
 * @param {function} onCommandStart - Callback executed whenever a new command is issued to the web worker
 * @param {function} onCommandExit - Callback executed whenever a new command is completed by the web worker
 * @param {function} onActiveMessagesChanged  - Callback executed whenever a new message is received from the worker
 * @param {function} onCootInitialized - Callback executed once after coot is initialised in the web worker
 * @property {function} cootCommand - Runs a coot command
 * @property {moorhen.History} history - An object that contains the command history
 * @example
 * import { MoorhenCommandCentre } from "moorhen";
 * 
 * commandCentre = new MoorhenCommandCentre({
 *  onActiveMessagesChanged: (newActiveMessages) => {
 *      setBusy(newActiveMessages.length !== 0)
 *  },
 *  onCootInitialized: () => {
 *      setCootInitialized(true)
 *  },
 *      urlPrefix: urlPrefix
 * })
 * 
 * await props.commandCentre.current.cootCommand({
 *  returnType: 'status',
 *  command: 'flipPeptide_cid',
 *  commandArgs: [0, "//A/150"],
 * })
 * 
 */
export class MoorhenCommandCentre implements moorhen.CommandCentre {
    urlPrefix: string;
    cootWorker: Worker;
    activeMessages: moorhen.WorkerMessage[];
    history: moorhen.History;
    isClosed: boolean;
    onCootInitialized: null | ( () => void );
    onConsoleChanged: null | ( (msg: string) => void );
    onCommandStart : null | ( (kwargs: any) => void );
    onCommandExit : null | ( (kwargs: any) => void );
    onActiveMessagesChanged: null | ( (activeMessages: moorhen.WorkerMessage[]) => void );

    constructor(urlPrefix: string, glRef: React.RefObject<webGL.MGWebGL>, timeCapsule: React.RefObject<moorhen.TimeCapsule>, props: {[x: string]: any}) {
        this.activeMessages = []
        this.urlPrefix = urlPrefix
        this.isClosed = false
        this.history = new MoorhenHistory(glRef, timeCapsule)
        this.history.setCommandCentre(this)
        
        this.onConsoleChanged = null
        this.onCommandStart = null
        this.onCommandExit = null
        this.onActiveMessagesChanged = null

        Object.keys(props).forEach(key => this[key] = props[key])
    }
    
    async init() {
        this.isClosed = false
        this.cootWorker = new Worker(`${this.urlPrefix}/baby-gru/CootWorker.js`)
        this.cootWorker.onmessage = this.handleMessage.bind(this)
        await this.postMessage({ message: 'CootInitialize', data: {} })
        if (this.onCootInitialized) {
            this.onCootInitialized()
        }
    }
    
    async close() {
        if (!this.isClosed) {
            this.isClosed = true
            await this.postMessage({ message: 'close', data: { } })
            this.cootWorker.removeEventListener("message", this.handleMessage)
            this.cootWorker.terminate()    
        } else {
            console.warn('Command centre already closed, doing nothing...')
        }
    }

    handleMessage(reply: moorhen.WorkerResponse) {
        this.activeMessages.filter(
            message => message.messageId && (message.messageId === reply.data.messageId)
        ).forEach(message => {
            message.handler(reply)
        })
        this.activeMessages = this.activeMessages.filter(
            message => message.messageId !== reply.data.messageId
        )
        if (this.onActiveMessagesChanged) {
            this.onActiveMessagesChanged(this.activeMessages)
        }
    }
    
    makeHandler(resolve) {
        return (reply) => {
            resolve(reply)
        }
    }
    
    async cootCommand(kwargs: moorhen.cootCommandKwargs, doJournal: boolean = true): Promise<moorhen.WorkerResponse> {
        const message = "coot_command"
        console.log('In cootCommand', kwargs.command)
        if (this.onCommandStart) {
            this.onCommandStart({...kwargs, doJournal})
        }
        const result = await this.postMessage({ message, ...kwargs })
        if (doJournal) {
            await this.history.addEntry(kwargs)
        }
        if (this.onCommandExit) {
            this.onCommandExit({...kwargs, doJournal})
        }
        return result
    }
    
    async cootCommandList(commandList: moorhen.cootCommandKwargs[], doJournal: boolean = true): Promise<moorhen.WorkerResponse> {
        const message = "coot_command_list"
        console.log('In cootCommandList', commandList)
        if (this.onCommandStart) {
            commandList.forEach(commandKwargs => this.onCommandStart(commandKwargs))
        }
        if (doJournal) {
            await Promise.all(commandList.map(commandKwargs => this.history.addEntry(commandKwargs)))
        }
        const result = await this.postMessage({ message, commandList })
        if (this.onCommandExit) {
            commandList.forEach(commandKwargs => this.onCommandExit(commandKwargs))
        }
        return result
    }

    postMessage(kwargs: moorhen.cootCommandKwargs): Promise<moorhen.WorkerResponse> {
        const $this = this
        const messageId = uuidv4()
        return new Promise((resolve, reject) => {
            const handler = $this.makeHandler(resolve)
            this.activeMessages.push({ messageId, handler, kwargs })
            if (this.onActiveMessagesChanged) {
                this.onActiveMessagesChanged(this.activeMessages)
            }
            this.cootWorker.postMessage({
                messageId, myTimeStamp: Date.now(), ...kwargs
            })
        })
    }
}