diff --git a/Monkeydo.mjs b/Monkeydo.mjs index 5319ee8..cbd83aa 100644 --- a/Monkeydo.mjs +++ b/Monkeydo.mjs @@ -1,96 +1,33 @@ -import { default as MonkeyWorker } from "./do/MonkeyManager.mjs"; +import { default as MonkeyMaster } from "./monkey/MonkeyMaster.mjs"; -export default class Monkeydo extends MonkeyWorker { - constructor(methods = {}) { - super(methods); - this.monkeydo = { - version: "0.2.1", - debugLevel: 0, - // Flag if debugging is enabled, regardless of level - get debug() { - return this.debugLevel > 0 ? true : false; - }, - // Set debug level. Non-verbose debugging if called without an argument - set debug(level = 1) { - this.debugLevel = level; - } - }; - Object.seal(this.monkeydo); - - // Monkeydo manifest parsed with load() - this.manifest = { - tasks: null - }; - - if(!window.Worker) { - throw new Error("JavaScript Workers aren't supported by your browser"); +export default class Monkeydo extends MonkeyMaster { + constructor(methods) { + if(typeof methods !== "object") { + throw new TypeError(`Expected type 'object' but got '${typeof methods}' when initializing Monkeydo`); } + super(); + this.methods = {}; + Object.assign(this.methods,methods); } - debug(...attachments) { - if(this.monkeydo.debug) { - console.warn("-- Monkeydo debug -->",attachments); - return; - } + do(task) { + console.log("TASK",task); } - // Loop playback; -1 or false = infinite - loop(times = -1) { - // Typecast boolean to left shifted integer; - if(typeof times === "boolean") { - times = times ? -1 : 0; - } - times = times < 0 ? -1 : times; - this.setFlag("loop",times); + async loop(times = null) { + times = times < 0 ? null : times; + return await this.setFlag("loop",times); } - // Load a Monkeydo manifest from JSON via string or URL async load(manifest) { - const errorPrefix = "MANIFEST_IMPORT_FAILED: "; - let data; - if(typeof manifest !== "string") { - this.debug(manifest); - throw new TypeError(errorPrefix + "Expected JSON or URL"); + let data = ""; + if(typeof manifest === "object") { + data = JSON.stringify(manifest); } - - // Attempt to parse the argument as JSON - try { - data = JSON.parse(manifest); + const load = await this.transaction("LOAD_MANIFEST",manifest); + if(!load) { + throw new Error("Failed to load manifest"); } - catch { - // If that fails, attempt to parse it as a URL - try { - manifest = new URL(manifest); - const fetchManifest = await fetch(manifest); - - // If the URL parsed but the fetch response is invalid, give up and throw an error - if(!fetchManifest.ok || !fetchManifest.headers.get("Content-Type")?.includes("application/json")) { - throw new TypeError(errorPrefix + "Invalid response Content-Type or HTTP status"); - } - data = await fetchManifest.json(); - } - catch(error) { - this.debug(manifest); - if(!error instanceof TypeError) { - throw new TypeError(errorPrefix + "Invalid JSON or URL"); - } - throw error; - } - } - - // Make sure the parsed JSON is a valid Monkeydo manifest - if(!data.hasOwnProperty("tasks")) { - this.debug(data); - throw new Error(errorPrefix + "Expected 'header' and 'body' properties in object"); - } - - this.manifest.tasks = data.tasks; return true; } - - // Execute tasks from Monkeydo manifest - do() { - // Hand over the loaded manifest to the MonkeyWorker task manager - this.giveManifest().then(() => this.play()); - } } \ No newline at end of file diff --git a/do/Monkey.js b/do/Monkey.js deleted file mode 100644 index ccfef0c..0000000 --- a/do/Monkey.js +++ /dev/null @@ -1,134 +0,0 @@ -// Dedicated worker which executes tasks from a Monkeydo manifest - -class Monkey { - constructor(manifest) { - const self = this; - - this.tasks = manifest.tasks; - this.tasksLength = this.tasks.length - 1; - - this.flags = { - playing: 0, - stacking: 0, // Subsequent calls to play() will build a queue (jQuery-style) - loop: 0, // Loop n times; <0 = infinite - _forwards: 1, // Playback direction - set forwards(forwards = true) { - if(forwards == this._forwards) { - return false; - } - // Toggle playback direction - self.tasks = self.tasks.reverse(); - this._forwards = 1 - this._forwards; - }, - get forwards() { - return this._forwards; - } - } - - this.i = 0; // Manifest iterator index - this.queue = { - task: null, - next: null - } - Object.seal(this.queue); - } - - // Pass task to main thread for execution - run(task) { - postMessage(["TASK",task]); - this.i++; - } - - // Interrupt timeout and put monkey to sleep - interrupt() { - clearTimeout(this.queue.task); - clearTimeout(this.queue.next); - this.queue.task = null; - this.queue.next = null; - this.flags.playing = 0; - } - - play() { - // Stack playback as loops if flag is set - if(this.flags.playing) { - if(this.flags.stacking && this.flags.loop >= 0) { - this.flags.loop++; - } - return; - } - this.queueNext(); - } - - // Schedule task for execution by index - queueNext() { - this.flags.playing = 1; - const data = this.tasks[this.i]; - const task = { - wait: data[0], - func: data[1], - args: data.slice(2) - }; - - // Schedule the current task to run after the specified wait time - this.queue.task = setTimeout(() => this.run(task),task.wait); - - // We're out of tasks to schedule.. - if(this.i >= this.tasksLength) { - this.i = -1; - // Exit if we're out of loops - if(this.flags.loop === 0) { - this.flags.playing = 0; - return false; - } - - // Decrement loop iterations if not infinite (negative int) - if(this.flags.loop > 0) { - this.flags.loop--; - } - } - - // Run this function again when the scheduled task will fire - this.queue.next = setTimeout(() => this.queueNext(),task.wait); - } -} - -// Unspawned monkey target -let monkey = undefined; - -// Event handler for messages received from initiator -onmessage = (message) => { - const type = message.data[0] ? message.data[0] : message.data; - const data = message.data[1]; - - switch(type) { - // Attempt to load manfiest provided by initiator thread - case "GIVE_MANIFEST": - try { - monkey = new Monkey(data); - postMessage(["RECEIVED_MANIFEST","OK"]); - } - catch(error) { - postMessage(["RECEIVED_MANIFEST",error]); - } - break; - - case "SET_PLAYING": - if(data === true) { - monkey.play(); - return; - } - monkey.interrupt(); - break; - - case "GET_FLAG": - const flag = monkey.flags[data]; - postMessage(parseInt(flag)); - break; - - case "SET_FLAG": - monkey.flags[data[0]] = data[1]; - break; - - default: return; // No op - } -} \ No newline at end of file diff --git a/do/MonkeyManager.mjs b/do/MonkeyManager.mjs deleted file mode 100644 index 971e709..0000000 --- a/do/MonkeyManager.mjs +++ /dev/null @@ -1,113 +0,0 @@ -// Task manager for Monkeydo dedicated workers - -export default class MonkeyManager { - constructor(methods) { - // Object of scoped methods for this manifest - this.methods = {}; - Object.assign(this.methods,methods); - - // Get path of this file - let location = new URL(import.meta.url); - location = location.pathname.replace("MonkeyManager.mjs",""); // Get parent directory - - // Spawn a dedicated worker for scheduling events from manifest - this.worker = new Worker(location + "Monkey.js"); - this.worker.addEventListener("message",message => this.message(message)); - - this.reversed = false; - - this.init = { - ready: false, - flags: [] - } - } - - // Get a status flag from the worker - async getFlag(flag) { - this.worker.postMessage(["GET_FLAG",flag]); - const response = await new Promise((resolve) => { - this.worker.addEventListener("message",message => resolve(message.data)); - }); - this.debug("GET_FLAG",flag,response); - return response; - } - - // Set a status flag for the worker - async setFlag(flag,value = 0) { - // Player is not initialized, add flag to queue - if(!this.init.ready) { - this.init.flags.push([flag,value]); - return false; - } - - const flagExists = await this.getFlag(flag); - if(flagExists === null) { - this.debug(flagExists); - throw new Error("Flag does not not exist"); - } - this.worker.postMessage(["SET_FLAG",[flag,value]]); - } - - // Get acknowledgement from worker for a transactional operation - async ack(name) { - const status = await new Promise((resolve,reject) => { - const ack = this.worker.addEventListener("message",message => { - if(message.data[0] !== name) { - return false; - } - - if(message.data[1] !== "OK") { - reject(message.data); - } - this.init.ready = true; - resolve(); - }); - this.worker.removeEventListener("message",ack); - }); - return status; - } - - // Pass manifest to worker and await response from worker - async giveManifest() { - this.worker.postMessage(["GIVE_MANIFEST",this.manifest]); - const status = await this.ack("RECEIVED_MANIFEST"); - return status; - } - - initFlags() { - if(this.init.flags.length > 0) { - this.init.flags.forEach(flag => this.setFlag(...flag)); - } - this.init.flags = []; - } - - // Call method from object and pass arguments - run(task) { - this.methods[task.func](...task.args); - } - - play() { - this.worker.postMessage(["SET_PLAYING",true]); - } - - pause() { - this.worker.postMessage(["SET_PLAYING",false]); - } - - // Event handler for messages received from worker - message(message) { - const type = message.data[0] ? message.data[0] : message.data; - const data = message.data[1]; - - switch(type) { - case "TASK": - this.run(data); - break; - - case "DEBUG": - default: - this.debug("MESSAGE_FROM_WORKER",message.data); - break; - } - } -} \ No newline at end of file diff --git a/monkey/Monkey.js b/monkey/Monkey.js new file mode 100644 index 0000000..c3fc1e1 --- /dev/null +++ b/monkey/Monkey.js @@ -0,0 +1,24 @@ +// Dedicated worker (monkey) which executes tasks from a Monkeydo manifest + +class Monkey { + constructor() { + this.manifest = {}; + } + + async loadManifest(manifest) { + try { + const data = JSON.parse(manifest); + this.manifest = data; + } + catch { + const url = new URL(manifest); + } + } +} + +const monkey = new Monkey(); + +// Event handler for messages received from initiator +onmessage = (message) => { + console.log(message); +} \ No newline at end of file diff --git a/monkey/MonkeyMaster.mjs b/monkey/MonkeyMaster.mjs new file mode 100644 index 0000000..90f3193 --- /dev/null +++ b/monkey/MonkeyMaster.mjs @@ -0,0 +1,55 @@ +// Task manager for Monkeydo dedicated workers (monkeys) + +class WorkerTransactions { + constructor() { + this.txn = { + _open: [], // Open transations + prefix: "TXN", + timeout: 2000, + // Close a transaction + set close(name) { + this._open[name].resolve(); + }, + // Open a new transaction + set open(name) { + name = [this.prefix,name]; + name = name.join("_").toUpperCase(); + this._open[name] = new Promise(); + }, + get status(name) { + return this._open[name]; + } + } + } +} + +export default class MonkeyMaster extends WorkerTransactions { + constructor() { + super(); + // Spawn dedicated worker + this.monkey = new Worker("Monkey.js"); + this.monkey.addEventListener("message",message => this.receive(message.data)); + + if(this?.crossOriginIsolateds === true) { + // TODO; SharedArrayBuffer goes here + } + } + + send(data) { + this.monkey.postMessage(data); + return true; + } + + async receive(data) { + if(data[0] === "TASK") { + this.do(data[1]); + return; + } + this.txn.close = data[1]; + } + + async transaction(name,data) { + this.txn.open = name; + const send = this.send([this.txn.prefix,name,data]); + } +} \ No newline at end of file