From 6991138f85c982e09a0362d12203ec0120b17f99 Mon Sep 17 00:00:00 2001 From: Victor Westerlund Date: Wed, 6 Oct 2021 17:12:40 +0200 Subject: [PATCH 1/3] 0.2.0 --- Monkeydo.mjs | 22 +++++++++-- worker/Monkey.js | 86 +++++++++++++++++++++++++++++++++--------- worker/TaskManager.mjs | 20 +++++++--- 3 files changed, 102 insertions(+), 26 deletions(-) diff --git a/Monkeydo.mjs b/Monkeydo.mjs index c0f5002..977d9a6 100644 --- a/Monkeydo.mjs +++ b/Monkeydo.mjs @@ -4,13 +4,13 @@ export default class Monkeydo extends MonkeyWorker { constructor(manifest = false) { super(); this.monkeydo = { - version: "0.1", + version: "0.2.0", 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,29 @@ export default class Monkeydo extends MonkeyWorker { } } - debug(attachment = "DEBUG_EMPTY") { + debug(...attachment) { if(this.monkeydo.debug) { console.warn("-- Monkeydo debug -->",attachment); return; } } + play() { + this.worker.postMessage(["SET_PLAYING",true]); + this.worker.addEventListener("message",message => eval(message.data)); + } + + pause() { + this.worker.postMessage(["SET_PLAYING",false]); + } + + loop(times) { + if(!times || times === "infinite") { + times = -1; + } + this.setFlag("loop",times); + } + // Load a Monkeydo manifest from JSON via string or URL async load(manifest) { const errorPrefix = "MANIFEST_IMPORT_FAILED: "; diff --git a/worker/Monkey.js b/worker/Monkey.js index eb0a2ff..3fa9d52 100644 --- a/worker/Monkey.js +++ b/worker/Monkey.js @@ -5,6 +5,12 @@ class Monkey { this.data = manifest.body; this.dataLength = this.data.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 + } + this.i = 0; this.queue = { task: null, @@ -13,38 +19,66 @@ class Monkey { Object.seal(this.queue); } + // Parse task components and send them to main thread run(data) { - this.i++; + this.i++; // Advance index postMessage(data); + console.log(this.i); } - 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); - } - + // 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() { + 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]; + + // Schedule the current task to run after the specified wait time + this.queue.task = setTimeout(() => this.run(data.do),data.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; + } + + if(this.flags.loop <= -1) { + this.flags.loop = this.flags.loop - 1; + } + } + + // Run this function again when the scheduled task will fire + this.queue.next = setTimeout(() => this.queueNext(),data.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); @@ -55,8 +89,24 @@ onmessage = (message) => { } break; - case "PLAYING": - this.monkey.queueNext(); + // Set playstate + case "SET_PLAYING": + if(data === true) { + this.monkey.play(); + return; + } + // Treat data that isn't a TRUE boolean as an interrupt + this.monkey.interrupt(); + break; + + case "GET_FLAG": + const flag = this.monkey.flags[data]; + postMessage(parseInt(flag)); + break; + + case "SET_FLAG": + console.log(data); + this.monkey.flags[data[0]] = data[1]; break; default: return; // No op diff --git a/worker/TaskManager.mjs b/worker/TaskManager.mjs index c126685..5593323 100644 --- a/worker/TaskManager.mjs +++ b/worker/TaskManager.mjs @@ -11,13 +11,23 @@ export default class TaskManager { this.worker = new Worker(location + "Monkey.js"); } - play() { - this.worker.postMessage(["PLAYING",true]); - this.worker.addEventListener("message",message => eval(message.data)); + // 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)); + }); + return response; } - pause() { - this.worker.postMessage(["PLAYING",false]); + // Set a status flag for the worker + async setFlag(flag,value = 0) { + const flagExists = await this.getFlag(flag); + if(!flagExists) { + this.debug(flagExists); + throw new Error("Flag does not not exist"); + } + this.worker.postMessage(["SET_FLAG",[flag,value]]); } // Pass manifest to worker and await response From 4765f9dc175faf152966647f65096c4747a79ef9 Mon Sep 17 00:00:00 2001 From: Victor Westerlund Date: Thu, 7 Oct 2021 12:47:20 +0200 Subject: [PATCH 2/3] 0.2.1 - Replaced `eval()` with namespaced method calls. - Created a class-scoped listener for incoming messages from worker - Renamed TaskManager to MonkeyManager --- Monkeydo.mjs | 36 +++++++----------- worker/Monkey.js | 27 ++++++++------ worker/MonkeyManager.mjs | 79 ++++++++++++++++++++++++++++++++++++++++ worker/TaskManager.mjs | 48 ------------------------ 4 files changed, 107 insertions(+), 83 deletions(-) create mode 100644 worker/MonkeyManager.mjs delete mode 100644 worker/TaskManager.mjs diff --git a/Monkeydo.mjs b/Monkeydo.mjs index 977d9a6..e67236b 100644 --- a/Monkeydo.mjs +++ b/Monkeydo.mjs @@ -1,10 +1,10 @@ -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.2.0", + version: "0.2.1", debugLevel: 0, // Flag if debugging is enabled, regardless of level get debug() { @@ -32,26 +32,20 @@ export default class Monkeydo extends MonkeyWorker { } } - debug(...attachment) { + debug(...attachments) { if(this.monkeydo.debug) { - console.warn("-- Monkeydo debug -->",attachment); + console.warn("-- Monkeydo debug -->",attachments); return; } } - play() { - this.worker.postMessage(["SET_PLAYING",true]); - this.worker.addEventListener("message",message => eval(message.data)); - } - - pause() { - this.worker.postMessage(["SET_PLAYING",false]); - } - - loop(times) { - if(!times || times === "infinite") { - times = -1; + // 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); } @@ -111,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/worker/Monkey.js b/worker/Monkey.js index 3fa9d52..9bb7d66 100644 --- a/worker/Monkey.js +++ b/worker/Monkey.js @@ -11,7 +11,7 @@ class Monkey { loop: 0, // Loop n times; <0 = infinite } - this.i = 0; + this.i = 0; // Manifest iterator index this.queue = { task: null, next: null @@ -20,10 +20,9 @@ class Monkey { } // Parse task components and send them to main thread - run(data) { + run(task) { this.i++; // Advance index - postMessage(data); - console.log(this.i); + postMessage(["TASK",task]); } // Interrupt timeout and put monkey to sleep @@ -36,6 +35,7 @@ class Monkey { } play() { + // Stack playback as loops if flag is set if(this.flags.playing) { if(this.flags.stacking) { this.flags.loop++; @@ -49,9 +49,14 @@ class Monkey { 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(data.do),data.wait); + this.queue.task = setTimeout(() => this.run(task),task.wait); // We're out of tasks to schedule.. if(this.i >= this.dataLength) { @@ -62,13 +67,14 @@ class Monkey { return false; } - if(this.flags.loop <= -1) { + // 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(),data.wait); + this.queue.next = setTimeout(() => this.queueNext(),task.wait); } } @@ -82,20 +88,18 @@ onmessage = (message) => { 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; - // Set playstate case "SET_PLAYING": if(data === true) { this.monkey.play(); return; } - // Treat data that isn't a TRUE boolean as an interrupt this.monkey.interrupt(); break; @@ -105,7 +109,6 @@ onmessage = (message) => { break; case "SET_FLAG": - console.log(data); this.monkey.flags[data[0]] = data[1]; break; 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 5593323..0000000 --- a/worker/TaskManager.mjs +++ /dev/null @@ -1,48 +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"); - } - - // 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)); - }); - return response; - } - - // Set a status flag for the worker - async setFlag(flag,value = 0) { - const flagExists = await this.getFlag(flag); - if(!flagExists) { - this.debug(flagExists); - throw new Error("Flag does not not exist"); - } - this.worker.postMessage(["SET_FLAG",[flag,value]]); - } - - // 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 From ef09a568e034735995d0f6af747821582bc9dfd0 Mon Sep 17 00:00:00 2001 From: Victor Westerlund Date: Thu, 7 Oct 2021 13:58:38 +0200 Subject: [PATCH 3/3] Initial README Added the initial documentation, more will be added later (perhaps in a wiki) --- README.md | 80 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 README.md 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
+