diff --git a/Monkeydo.mjs b/Monkeydo.mjs index c0f5002..e67236b 100644 --- a/Monkeydo.mjs +++ b/Monkeydo.mjs @@ -1,16 +1,16 @@ -import { default as MonkeyWorker } from "./worker/TaskManager.mjs"; +import { default as MonkeyWorker } from "./worker/MonkeyManager.mjs"; export default class Monkeydo extends MonkeyWorker { - constructor(manifest = false) { - super(); + constructor(methods = {},manifest = false) { + super(methods); this.monkeydo = { - version: "0.1", + 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 by default + // Set debug level. Non-verbose debugging if called without an argument set debug(level = 1) { this.debugLevel = level; } @@ -32,13 +32,23 @@ export default class Monkeydo extends MonkeyWorker { } } - debug(attachment = "DEBUG_EMPTY") { + debug(...attachments) { if(this.monkeydo.debug) { - console.warn("-- Monkeydo debug -->",attachment); + console.warn("-- Monkeydo debug -->",attachments); return; } } + // 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); + } + // Load a Monkeydo manifest from JSON via string or URL async load(manifest) { const errorPrefix = "MANIFEST_IMPORT_FAILED: "; @@ -95,10 +105,6 @@ export default class Monkeydo extends MonkeyWorker { // Hand over the loaded manifest to the MonkeyWorker task manager const monkey = this.giveManifest(); - monkey.then(() => this.play()) - .catch(error => { - this.debug(error); - throw new Error(errorPrefix + "Failed to post manifest to worker thread"); - }); + this.play(); } } \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..791873a --- /dev/null +++ b/README.md @@ -0,0 +1,80 @@ +

+ +

+

Threaded task chaining for JavaScript

+
+

Monkeydo uses the portable data format JSON to read tasks, making it easy to read by primates and machines alike.

+ + + +
+
+{
+  "tasks": [
+    [0,"myJavaSriptMethod","someArgument","anotherArgument"]
+  ]
+}
+
+
+ + + + + + + + + + + + + + + + + +
Array keyDescription
0Delay
Wait this many milliseconds before running this task
1Method
Name of the JavaScript method to call
2+nArguments
Some amount of arguments to pass to the method
+
+

Use Monkeydo

+

Monkeydo comes as an importable ECMAScript 6 module. In this guide we'll import this directly from a ./modules/ folder, but any web-accesible location will work.

+
    +
  1. Import Monkeydo from your repo clone or download +
    +import { default } from "./modules/Monkeydo/Monkeydo.mjs";
    +
    +
  2. +
  3. Define your JS methods +
    +const methods = {
    +  myJavaScriptMethod: (foo,bar) => {
    +    console.log(foo,bar);
    +  }
    +}
    +
    +
  4. +
  5. Define your tasks in a JSON file (or directly in JavaScript) +
    +{
    +  "tasks": [
    +    [0,"myJavaSriptMethod","I see skies of","blue"],
    +    [300,"myJavaSriptMethod","red","roses too"],
    +    [160,"myJavaSriptMethod","I see them","bloom"],
    +    [1200,"myJavaSriptMethod","for","me and you"]
    +  ]
    +}
    +
    +
  6. +
  7. Initialize and run Monkeydo with your methods and manifest +
    +const monkey = new Monkeydo(methods,manifest);
    +monkey.do();
    +
    +
  8. +
+

The example above would be the same as running:

+
+console.log("I see skies","of blue"); // Right away
+console.log("red","roses too"); // 300 milliseconds after the first
+console.log("I see them","bloom"); // 160 milliseconds after that one
+console.log("for","me and you"); // and lastly, 1200 after that
+
diff --git a/worker/Monkey.js b/worker/Monkey.js index eb0a2ff..9bb7d66 100644 --- a/worker/Monkey.js +++ b/worker/Monkey.js @@ -5,7 +5,13 @@ class Monkey { this.data = manifest.body; this.dataLength = this.data.length - 1; - this.i = 0; + this.flags = { + playing: 0, + stacking: 0, // Subsequent calls to play() will build a queue (jQuery-style) + loop: 0, // Loop n times; <0 = infinite + } + + this.i = 0; // Manifest iterator index this.queue = { task: null, next: null @@ -13,50 +19,97 @@ class Monkey { Object.seal(this.queue); } - run(data) { - this.i++; - postMessage(data); - } - - queueNext() { - const data = this.data[this.i]; - this.queue.task = setTimeout(() => this.run(data.do),data.wait); - - // Schedule next task if it's not the last - if(this.i >= this.dataLength) { - this.i = 0; - return false; - } - - this.queue.next = setTimeout(() => this.queueNext(),data.wait); + // Parse task components and send them to main thread + run(task) { + this.i++; // Advance index + postMessage(["TASK",task]); } + // 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++; + } + return; + } + this.queueNext(); + } + + // Schedule task for execution by index + queueNext() { + this.flags.playing = 1; + const data = this.data[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.dataLength) { + 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 = this.flags.loop - 1; + } + } + + // Run this function again when the scheduled task will fire + this.queue.next = setTimeout(() => this.queueNext(),task.wait); } } -// Global event handler for this worker +// Global message event handler for this worker onmessage = (message) => { - const type = message.data[0] ? message.data[0] : null; + 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 { this.monkey = new Monkey(data); - postMessage("OK"); + postMessage(["RECEIVED_MANIFEST","OK"]); } catch(error) { - postMessage(["MANIFEST_ERROR",error]); + postMessage(["RECEIVED_MANIFEST",error]); } break; - case "PLAYING": - this.monkey.queueNext(); + case "SET_PLAYING": + if(data === true) { + this.monkey.play(); + return; + } + this.monkey.interrupt(); + break; + + case "GET_FLAG": + const flag = this.monkey.flags[data]; + postMessage(parseInt(flag)); + break; + + case "SET_FLAG": + this.monkey.flags[data[0]] = data[1]; break; default: return; // No op diff --git a/worker/MonkeyManager.mjs b/worker/MonkeyManager.mjs new file mode 100644 index 0000000..66d2dd7 --- /dev/null +++ b/worker/MonkeyManager.mjs @@ -0,0 +1,79 @@ +// 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)); + } + + // 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) { + 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]]); + } + + // Call method from object and pass arguments + runTask(task) { + this.methods[task.func](...task.args); + } + + play() { + this.worker.postMessage(["SET_PLAYING",true]); + } + + pause() { + this.worker.postMessage(["SET_PLAYING",false]); + } + + // Pass manifest to worker and await response + async giveManifest() { + this.worker.postMessage(["GIVE_MANIFEST",this.manifest]); + + const status = await new Promise((resolve,reject) => { + const ack = this.worker.addEventListener("message",message => { + if(message.data[0] !== "RECEIVED_MANIFEST") { + return false; + } + + if(message.data[1] !== "OK") { + reject(message.data); + } + resolve(); + }); + this.worker.removeEventListener("message",ack); + }); + return status; + } + + message(message) { + const type = message.data[0] ? message.data[0] : message.data; + const data = message.data[1]; + if(type !== "TASK") { + return false; + } + this.runTask(data); + } +} \ No newline at end of file diff --git a/worker/TaskManager.mjs b/worker/TaskManager.mjs deleted file mode 100644 index c126685..0000000 --- a/worker/TaskManager.mjs +++ /dev/null @@ -1,38 +0,0 @@ -// Task manager for Monkeydo dedicated workers - -export default class TaskManager { - constructor() { - // Get path of this file - this.ready = false; - let location = new URL(import.meta.url); - location = location.pathname.replace("TaskManager.mjs",""); // Get parent directory - - // Spawn a dedicated worker for scheduling events from manifest - this.worker = new Worker(location + "Monkey.js"); - } - - play() { - this.worker.postMessage(["PLAYING",true]); - this.worker.addEventListener("message",message => eval(message.data)); - } - - pause() { - this.worker.postMessage(["PLAYING",false]); - } - - // Pass manifest to worker and await response - async giveManifest() { - this.worker.postMessage(["GIVE_MANIFEST",this.manifest]); - - // Wait for the worker to install the manifest - const ack = await new Promise((resolve,reject) => { - this.worker.addEventListener("message",message => { - if(message.data !== "OK") { - reject(message.data); - } - resolve(); - }); - }); - return ack; - } -} \ No newline at end of file