Merge pull request #1 from VictorWesterlund/0.2.x

0.2.1
This commit is contained in:
Victor Westerlund 2021-10-07 14:11:36 +02:00 committed by GitHub
commit c275186126
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 253 additions and 73 deletions

View file

@ -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 { export default class Monkeydo extends MonkeyWorker {
constructor(manifest = false) { constructor(methods = {},manifest = false) {
super(); super(methods);
this.monkeydo = { this.monkeydo = {
version: "0.1", version: "0.2.1",
debugLevel: 0, debugLevel: 0,
// Flag if debugging is enabled, regardless of level // Flag if debugging is enabled, regardless of level
get debug() { get debug() {
return this.debugLevel > 0 ? true : false; 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) { set debug(level = 1) {
this.debugLevel = level; this.debugLevel = level;
} }
@ -32,13 +32,23 @@ export default class Monkeydo extends MonkeyWorker {
} }
} }
debug(attachment = "DEBUG_EMPTY") { debug(...attachments) {
if(this.monkeydo.debug) { if(this.monkeydo.debug) {
console.warn("-- Monkeydo debug -->",attachment); console.warn("-- Monkeydo debug -->",attachments);
return; 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 // Load a Monkeydo manifest from JSON via string or URL
async load(manifest) { async load(manifest) {
const errorPrefix = "MANIFEST_IMPORT_FAILED: "; 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 // Hand over the loaded manifest to the MonkeyWorker task manager
const monkey = this.giveManifest(); const monkey = this.giveManifest();
monkey.then(() => this.play()) this.play();
.catch(error => {
this.debug(error);
throw new Error(errorPrefix + "Failed to post manifest to worker thread");
});
} }
} }

80
README.md Normal file
View file

@ -0,0 +1,80 @@
<p align="center">
<img width="400" src="https://storage.googleapis.com/public.victorwesterlund.com/github/VictorWesterlund/monkeydo/monkeydo.svg"/>
</p>
<h3 align="center">Threaded task chaining for JavaScript</h3>
<hr>
<p align="center">Monkeydo uses the portable data format JSON to read tasks, making it easy to read by primates and machines alike.</p>
<table>
<td>
<pre lang="json">
{
"tasks": [
[0,"myJavaSriptMethod","someArgument","anotherArgument"]
]
}
</pre>
</td>
<td>
<table align="center">
<tr>
<th>Array key</th>
<th>Description</th>
</tr>
<tr>
<td align="center">0</td>
<td><strong>Delay</strong><br>Wait this many milliseconds before running this task</td>
</tr>
<tr>
<td align="center">1</td>
<td><strong>Method</strong><br>Name of the JavaScript method to call</td>
</tr>
<tr>
<td align="center">2+n</td>
<td><strong>Arguments</strong><br>Some amount of arguments to pass to the method</td>
</tr>
</table>
</td>
</table>
<h1 align="center">Use Monkeydo</h1>
<p>Monkeydo comes as an importable ECMAScript 6 module. In this guide we'll import this directly from a <i>./modules/</i> folder, but any web-accesible location will work.</p>
<ol>
<li>Import <code>Monkeydo</code> from your repo clone or download
<pre lang="js">
import { default } from "./modules/Monkeydo/Monkeydo.mjs";
</pre>
</li>
<li>Define your JS methods
<pre lang="js">
const methods = {
myJavaScriptMethod: (foo,bar) => {
console.log(foo,bar);
}
}
</pre>
</li>
<li>Define your tasks in a JSON file (or directly in JavaScript)
<pre lang="json">
{
"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"]
]
}
</pre>
</li>
<li>Initialize and run <code>Monkeydo</code> with your methods and manifest
<pre lang="js">
const monkey = new Monkeydo(methods,manifest);
monkey.do();
</pre>
</li>
</ol>
<p>The example above would be the same as running:</p>
<pre lang="js">
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
</pre>

View file

@ -5,7 +5,13 @@ class Monkey {
this.data = manifest.body; this.data = manifest.body;
this.dataLength = this.data.length - 1; 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 = { this.queue = {
task: null, task: null,
next: null next: null
@ -13,50 +19,97 @@ class Monkey {
Object.seal(this.queue); Object.seal(this.queue);
} }
run(data) { // Parse task components and send them to main thread
this.i++; run(task) {
postMessage(data); this.i++; // Advance index
} postMessage(["TASK",task]);
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() { interrupt() {
clearTimeout(this.queue.task); clearTimeout(this.queue.task);
clearTimeout(this.queue.next); clearTimeout(this.queue.next);
this.queue.task = null; this.queue.task = null;
this.queue.next = 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) => { 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]; const data = message.data[1];
switch(type) { switch(type) {
// Attempt to load manfiest provided by initiator thread
case "GIVE_MANIFEST": case "GIVE_MANIFEST":
try { try {
this.monkey = new Monkey(data); this.monkey = new Monkey(data);
postMessage("OK"); postMessage(["RECEIVED_MANIFEST","OK"]);
} }
catch(error) { catch(error) {
postMessage(["MANIFEST_ERROR",error]); postMessage(["RECEIVED_MANIFEST",error]);
} }
break; break;
case "PLAYING": case "SET_PLAYING":
this.monkey.queueNext(); 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; break;
default: return; // No op default: return; // No op

79
worker/MonkeyManager.mjs Normal file
View file

@ -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);
}
}

View file

@ -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;
}
}