From dd071ea8bbedbb956791e987da6a4de0db3b722b Mon Sep 17 00:00:00 2001 From: Victor Westerlund Date: Fri, 5 Nov 2021 17:34:20 +0100 Subject: [PATCH 1/6] dev21w44a --- Monkeydo.mjs | 101 ++++++------------------------ do/Monkey.js | 134 ---------------------------------------- do/MonkeyManager.mjs | 113 --------------------------------- monkey/Monkey.js | 24 +++++++ monkey/MonkeyMaster.mjs | 55 +++++++++++++++++ 5 files changed, 98 insertions(+), 329 deletions(-) delete mode 100644 do/Monkey.js delete mode 100644 do/MonkeyManager.mjs create mode 100644 monkey/Monkey.js create mode 100644 monkey/MonkeyMaster.mjs 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 From ff0ed25a3bf928aa8bef4d6dc0d7acbd43f71467 Mon Sep 17 00:00:00 2001 From: Victor Westerlund Date: Sun, 7 Nov 2021 13:36:26 +0100 Subject: [PATCH 2/6] dev21w44b --- Monkeydo.mjs | 10 +--- monkey/Monkey.js | 20 +++++--- monkey/MonkeyMaster.mjs | 101 +++++++++++++++++++++++----------------- 3 files changed, 71 insertions(+), 60 deletions(-) diff --git a/Monkeydo.mjs b/Monkeydo.mjs index cbd83aa..4975232 100644 --- a/Monkeydo.mjs +++ b/Monkeydo.mjs @@ -20,14 +20,6 @@ export default class Monkeydo extends MonkeyMaster { } async load(manifest) { - let data = ""; - if(typeof manifest === "object") { - data = JSON.stringify(manifest); - } - const load = await this.transaction("LOAD_MANIFEST",manifest); - if(!load) { - throw new Error("Failed to load manifest"); - } - return true; + } } \ No newline at end of file diff --git a/monkey/Monkey.js b/monkey/Monkey.js index c3fc1e1..f7efb0e 100644 --- a/monkey/Monkey.js +++ b/monkey/Monkey.js @@ -1,14 +1,25 @@ -// Dedicated worker (monkey) which executes tasks from a Monkeydo manifest +// Dedicated worker (monkey) that executes tasks from a Monkeydo manifest + +importScripts("https://unpkg.com/comlink/dist/umd/comlink.js"); class Monkey { constructor() { this.manifest = {}; + + // Runtime flags + this.flags = new Uint8ClampedArray(2); + } + + // Set or get a runtime flag + flag(index,value = null) { + return value ? this.flags[index] = value : this.flags[index]; } async loadManifest(manifest) { try { const data = JSON.parse(manifest); this.manifest = data; + this.flags[0] = 1; } catch { const url = new URL(manifest); @@ -16,9 +27,4 @@ class Monkey { } } -const monkey = new Monkey(); - -// Event handler for messages received from initiator -onmessage = (message) => { - console.log(message); -} \ No newline at end of file +Comlink.expose(Monkey); \ No newline at end of file diff --git a/monkey/MonkeyMaster.mjs b/monkey/MonkeyMaster.mjs index 90f3193..e75dfcc 100644 --- a/monkey/MonkeyMaster.mjs +++ b/monkey/MonkeyMaster.mjs @@ -1,55 +1,68 @@ // Task manager for Monkeydo dedicated workers (monkeys) -class WorkerTransactions { +import * as Comlink from "https://unpkg.com/comlink/dist/esm/comlink.mjs"; + +export default class MonkeyMaster { 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]; + this.comlink = null; + + this.ready = false; + this.flagQueue = []; + this.init(); + } + + // Import worker relative to this module + getWorkerPath() { + const name = "Monkey.js"; + const url = new URL(import.meta.url); + + const path = url.pathname.split("/"); + path[path.length - 1] = name; + + url.pathname = path.join("/"); + return url.toString(); + } + + async init() { + // Spawn and wrap dedicated worker with Comlink + const worker = new Worker(this.getWorkerPath()); + const Monkey = Comlink.wrap(worker); + + this.comlink = await new Monkey(); + } + + // Return a flag array index by name + flagStringToIndex(flag) { + const flags = [ + "MANIFEST_LOADED", + "PLAYING", + "LOOP" + ]; + // Translate string to index + if(typeof flag === "string" || flag < 0) { + const key = flags.indexOf(flag.toUpperCase()); + if(key < 0) { + return false; } } - } -} - -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 + // Check key is in bounds + if(flag < 0 || flags > flags.length - 1) { + throw new Error(`Array key '${flag}' out of range`); } + return flag; } - send(data) { - this.monkey.postMessage(data); + async getFlag(flag) { + const key = this.flagStringToIndex(flag); + return await this.comlink.flag(key); + } + + async setFlag(flag,value) { + const key = this.flagStringToIndex(flag); + const update = await this.comlink.flag(0,12); + if(!update) { + this.flagQueue.push([key,value]); + } 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 From 86ea8cd031d4df0912e77bf53cb22559469354ab Mon Sep 17 00:00:00 2001 From: Victor Westerlund Date: Mon, 8 Nov 2021 16:38:56 +0100 Subject: [PATCH 3/6] dev21w45a --- Monkeydo.mjs | 26 +++++++++++++--- monkey/Monkey.js | 69 ++++++++++++++++++++++++++++++++++------- monkey/MonkeyMaster.mjs | 60 +++++++++++++++++++++++++++++------ 3 files changed, 129 insertions(+), 26 deletions(-) diff --git a/Monkeydo.mjs b/Monkeydo.mjs index 4975232..34ea401 100644 --- a/Monkeydo.mjs +++ b/Monkeydo.mjs @@ -10,16 +10,32 @@ export default class Monkeydo extends MonkeyMaster { Object.assign(this.methods,methods); } + // Execute a task do(task) { - console.log("TASK",task); + if(!task[1] in this.methods) return; + const args = task.splice(0,2); + this.methods[task[1]](...args); } - async loop(times = null) { - times = times < 0 ? null : times; - return await this.setFlag("loop",times); + async debug(state = true) { + return await this.setFlag("debug",state); } + // Loop playback X times or negative number for infinite + async loop(times = 255) { + if(typeof times !== "number") { + times = parseInt(times); + } + // Clamp number to 8 bit max + times = Math.min(Math.max(times,0),255); + return await this.setFlag("playing",times); + } + + // Load Monkeydo manifest async load(manifest) { - + if(typeof manifest === "object") { + manifest = JSON.stringify(manifest); + } + return await this.loadManifest(manifest); } } \ No newline at end of file diff --git a/monkey/Monkey.js b/monkey/Monkey.js index f7efb0e..04389de 100644 --- a/monkey/Monkey.js +++ b/monkey/Monkey.js @@ -4,10 +4,44 @@ importScripts("https://unpkg.com/comlink/dist/umd/comlink.js"); class Monkey { constructor() { - this.manifest = {}; - - // Runtime flags this.flags = new Uint8ClampedArray(2); + this.tasks = []; + // Runtime task queue + this.queue = { + thisTask: null, + nextTask: null + } + } + + // Task scheduler + next() { + if(this.flags[0] === 0) return; + const task = this.tasks[this.i]; + + // Run task after delay + this.queue.thisTask = setTimeout(() => { + // Dispatch task to main thread + this.postMessage(["TASK",task]); + this.i++; + },task[0]); + + // Loop until flag is 0 or infinite if 255 + if(this.i === this.tasks.length) { + this.i = 0; + if(flags[1] === 255) return; + flags[1]--; + } + + // Queue the next task + this.queue.nextTask = setTimeout(() => this.next(),task[0]); + } + + abort() { + clearTimeout(this.queue.thisTask); + clearTimeout(this.queue.nextTask); + this.queue.thisTask = null; + this.queue.nextTask = null; + this.flags[1] = 0; // Playing: false } // Set or get a runtime flag @@ -15,15 +49,28 @@ class Monkey { return value ? this.flags[index] = value : this.flags[index]; } + // Fetch and install manifest from URL + async fetchManifest(url) { + const manifest = await fetch(url); + const json = await manifest.json(); + return await this.loadManifest(json); + } + + // Install a Monkeydo manifest async loadManifest(manifest) { - try { - const data = JSON.parse(manifest); - this.manifest = data; - this.flags[0] = 1; - } - catch { - const url = new URL(manifest); - } + return await new Promise((resolve,reject) => { + if(typeof manifest !== "object") { + try { + manifest = JSON.parse(manifest); + } + catch { + reject("Failed to load manifest"); + } + } + this.tasks = manifest.tasks; + this.flags[0] = 1; // Manifest loaded: true + resolve(); + }); } } diff --git a/monkey/MonkeyMaster.mjs b/monkey/MonkeyMaster.mjs index e75dfcc..e086f12 100644 --- a/monkey/MonkeyMaster.mjs +++ b/monkey/MonkeyMaster.mjs @@ -7,8 +7,21 @@ export default class MonkeyMaster { this.comlink = null; this.ready = false; - this.flagQueue = []; - this.init(); + // Tasks will be queued here on runtime if the worker isn't ready + this.queue = { + _flags: [], + set flag(flag) { + this._flags.push(flag); + }, + // Attempt to send all queued flags + sendAllFlags: () => { + // Copy flags and clear queue + const flags = [...this.queue._flags]; + this.queue._flags = []; + + flags.forEach(flag => this.setFlag(flag)); + } + }; } // Import worker relative to this module @@ -16,6 +29,7 @@ export default class MonkeyMaster { const name = "Monkey.js"; const url = new URL(import.meta.url); + // Replace pathname of this file with worker const path = url.pathname.split("/"); path[path.length - 1] = name; @@ -27,24 +41,31 @@ export default class MonkeyMaster { // Spawn and wrap dedicated worker with Comlink const worker = new Worker(this.getWorkerPath()); const Monkey = Comlink.wrap(worker); - this.comlink = await new Monkey(); + + // Wait for comlink to initialize proxy and send queued flags + return await new Promise((resolve,reject) => { + if(!this.comlink) reject("Failed to open proxy to worker"); + + this.ready = true; + this.queue.sendAllFlags(); + resolve(); + }); } // Return a flag array index by name flagStringToIndex(flag) { const flags = [ "MANIFEST_LOADED", - "PLAYING", - "LOOP" + "PLAYING" ]; + // Translate string to index if(typeof flag === "string" || flag < 0) { const key = flags.indexOf(flag.toUpperCase()); - if(key < 0) { - return false; - } + if(key < 0) return; } + // Check key is in bounds if(flag < 0 || flags > flags.length - 1) { throw new Error(`Array key '${flag}' out of range`); @@ -57,11 +78,30 @@ export default class MonkeyMaster { return await this.comlink.flag(key); } + // Set or queue worker runtime flag async setFlag(flag,value) { const key = this.flagStringToIndex(flag); - const update = await this.comlink.flag(0,12); + if(!this.ready) { + this.queue.flag = [key,value]; + return; + } + + // Tell worker to update flag by key + const update = await this.comlink.flag(key,value); if(!update) { - this.flagQueue.push([key,value]); + this.queue.flag = [key,value]; + } + return update; + } + + async loadManifest(manifest) { + if(!this.ready) await this.init(); + try { + const url = new URL(manifest); + this.comlink.fetchManifest(url.toString()); + } + catch { + this.comlink.loadManifest(manifest); } return true; } From 675fe748e4b727f657883f94e054ad67bd0f1de5 Mon Sep 17 00:00:00 2001 From: Victor Westerlund Date: Tue, 9 Nov 2021 17:23:04 +0100 Subject: [PATCH 4/6] dev21w45b --- Monkeydo.mjs | 14 +++++++++++-- monkey/Monkey.js | 20 ++++++++++++++----- monkey/MonkeyMaster.mjs | 44 +++++++++++++++++++++++++++++------------ 3 files changed, 58 insertions(+), 20 deletions(-) diff --git a/Monkeydo.mjs b/Monkeydo.mjs index 34ea401..8fb502d 100644 --- a/Monkeydo.mjs +++ b/Monkeydo.mjs @@ -26,8 +26,8 @@ export default class Monkeydo extends MonkeyMaster { if(typeof times !== "number") { times = parseInt(times); } - // Clamp number to 8 bit max - times = Math.min(Math.max(times,0),255); + times = Math.floor(times); + times = Math.min(Math.max(times,0),255); // Clamp number to 8 bits return await this.setFlag("playing",times); } @@ -38,4 +38,14 @@ export default class Monkeydo extends MonkeyMaster { } return await this.loadManifest(manifest); } + + async play(manifest = null) { + if(!this.ready && !manifest) throw new Error("Can not start playback without a manifest"); + if(manifest) { + const load = this.load(manifest) + load.then(() => this.start()); + return; + } + return await this.start(); + } } \ No newline at end of file diff --git a/monkey/Monkey.js b/monkey/Monkey.js index 04389de..557b068 100644 --- a/monkey/Monkey.js +++ b/monkey/Monkey.js @@ -6,6 +6,8 @@ class Monkey { constructor() { this.flags = new Uint8ClampedArray(2); this.tasks = []; + this.tasksLength = 0; + this.i = 0; // Runtime task queue this.queue = { thisTask: null, @@ -15,21 +17,22 @@ class Monkey { // Task scheduler next() { - if(this.flags[0] === 0) return; + if(this.flags[0] === 0 || this.flags[1] === 0) return; const task = this.tasks[this.i]; + console.log(task,this.i); // Run task after delay this.queue.thisTask = setTimeout(() => { // Dispatch task to main thread - this.postMessage(["TASK",task]); + postMessage(["TASK",task]); this.i++; },task[0]); // Loop until flag is 0 or infinite if 255 - if(this.i === this.tasks.length) { + if(this.i === this.tasksLength) { this.i = 0; - if(flags[1] === 255) return; - flags[1]--; + if(this.flags[1] === 255) return; + this.flags[1]--; } // Queue the next task @@ -52,6 +55,11 @@ class Monkey { // Fetch and install manifest from URL async fetchManifest(url) { const manifest = await fetch(url); + if(!manifest.ok) { + console.error("Monkeydo fetch error:",manifest); + throw new Error("Server responded with an error"); + }; + const json = await manifest.json(); return await this.loadManifest(json); } @@ -68,6 +76,8 @@ class Monkey { } } this.tasks = manifest.tasks; + // Store length as property so we don't have to calculate the offset each iteration of next() + this.tasksLength = manifest.tasks.length - 1; this.flags[0] = 1; // Manifest loaded: true resolve(); }); diff --git a/monkey/MonkeyMaster.mjs b/monkey/MonkeyMaster.mjs index e086f12..d6739d1 100644 --- a/monkey/MonkeyMaster.mjs +++ b/monkey/MonkeyMaster.mjs @@ -18,8 +18,7 @@ export default class MonkeyMaster { // Copy flags and clear queue const flags = [...this.queue._flags]; this.queue._flags = []; - - flags.forEach(flag => this.setFlag(flag)); + flags.forEach(flag => this.setFlag(...flag)); } }; } @@ -40,6 +39,11 @@ export default class MonkeyMaster { async init() { // Spawn and wrap dedicated worker with Comlink const worker = new Worker(this.getWorkerPath()); + worker.addEventListener("message",event => { + if(event.data[0] !== "TASK") return; + this.do(event.data); + }); + const Monkey = Comlink.wrap(worker); this.comlink = await new Monkey(); @@ -60,13 +64,14 @@ export default class MonkeyMaster { "PLAYING" ]; + // Translate string to index if(typeof flag === "string" || flag < 0) { - const key = flags.indexOf(flag.toUpperCase()); - if(key < 0) return; + flag = flags.indexOf(flag.toUpperCase()); + if(flag < 0) return; } - // Check key is in bounds + // Check that key is in bounds if(flag < 0 || flags > flags.length - 1) { throw new Error(`Array key '${flag}' out of range`); } @@ -94,15 +99,28 @@ export default class MonkeyMaster { return update; } + // Load a Monkeydo manifest by URL or JSON string async loadManifest(manifest) { if(!this.ready) await this.init(); - try { - const url = new URL(manifest); - this.comlink.fetchManifest(url.toString()); - } - catch { - this.comlink.loadManifest(manifest); - } - return true; + return await new Promise((resolve,reject) => { + let load = null; + try { + const url = new URL(manifest); + load = this.comlink.fetchManifest(url.toString()); + } + catch { + load = this.comlink.loadManifest(manifest); + } + load.then(() => resolve()) + .catch(() => reject("Failed to load manifest")); + }); + } + + // Start playback of a loaded manifest + async start() { + // Set playstate if no value is present already + const loop = await this.getFlag("playing"); + if(loop < 1) await this.setFlag("playing",1); + return await this.comlink.next(); } } \ No newline at end of file From 539690cbdd6186257a8d3672117bd54d919636af Mon Sep 17 00:00:00 2001 From: Victor Westerlund Date: Wed, 10 Nov 2021 12:31:04 +0100 Subject: [PATCH 5/6] dev21w45c Ready for review --- Monkeydo.mjs | 10 +++------- monkey/Monkey.js | 12 +++++------- monkey/MonkeyMaster.mjs | 29 ++++++++++++++++++++--------- 3 files changed, 28 insertions(+), 23 deletions(-) diff --git a/Monkeydo.mjs b/Monkeydo.mjs index 8fb502d..918319a 100644 --- a/Monkeydo.mjs +++ b/Monkeydo.mjs @@ -13,12 +13,8 @@ export default class Monkeydo extends MonkeyMaster { // Execute a task do(task) { if(!task[1] in this.methods) return; - const args = task.splice(0,2); - this.methods[task[1]](...args); - } - - async debug(state = true) { - return await this.setFlag("debug",state); + const args = task.splice(2); + this.methods[task[1]]?.(...args); } // Loop playback X times or negative number for infinite @@ -28,7 +24,7 @@ export default class Monkeydo extends MonkeyMaster { } times = Math.floor(times); times = Math.min(Math.max(times,0),255); // Clamp number to 8 bits - return await this.setFlag("playing",times); + return await this.setFlag("loop",times); } // Load Monkeydo manifest diff --git a/monkey/Monkey.js b/monkey/Monkey.js index 557b068..8d0bd09 100644 --- a/monkey/Monkey.js +++ b/monkey/Monkey.js @@ -4,7 +4,7 @@ importScripts("https://unpkg.com/comlink/dist/umd/comlink.js"); class Monkey { constructor() { - this.flags = new Uint8ClampedArray(2); + this.flags = new Uint8ClampedArray(3); this.tasks = []; this.tasksLength = 0; this.i = 0; @@ -17,9 +17,8 @@ class Monkey { // Task scheduler next() { - if(this.flags[0] === 0 || this.flags[1] === 0) return; + if(this.flags[0] === 0 || this.flags[2] === 0) return this.abort(); const task = this.tasks[this.i]; - console.log(task,this.i); // Run task after delay this.queue.thisTask = setTimeout(() => { @@ -30,9 +29,8 @@ class Monkey { // Loop until flag is 0 or infinite if 255 if(this.i === this.tasksLength) { - this.i = 0; - if(this.flags[1] === 255) return; - this.flags[1]--; + this.i = -1; + if(this.flags[1] < 255) this.flags[2]--; } // Queue the next task @@ -40,11 +38,11 @@ class Monkey { } abort() { + this.flags[2] = 0; // Playing: false clearTimeout(this.queue.thisTask); clearTimeout(this.queue.nextTask); this.queue.thisTask = null; this.queue.nextTask = null; - this.flags[1] = 0; // Playing: false } // Set or get a runtime flag diff --git a/monkey/MonkeyMaster.mjs b/monkey/MonkeyMaster.mjs index d6739d1..da3ee63 100644 --- a/monkey/MonkeyMaster.mjs +++ b/monkey/MonkeyMaster.mjs @@ -41,7 +41,7 @@ export default class MonkeyMaster { const worker = new Worker(this.getWorkerPath()); worker.addEventListener("message",event => { if(event.data[0] !== "TASK") return; - this.do(event.data); + this.do(event.data[1]); // Send inner array (task) }); const Monkey = Comlink.wrap(worker); @@ -61,31 +61,32 @@ export default class MonkeyMaster { flagStringToIndex(flag) { const flags = [ "MANIFEST_LOADED", + "LOOP", "PLAYING" ]; - // Translate string to index if(typeof flag === "string" || flag < 0) { flag = flags.indexOf(flag.toUpperCase()); - if(flag < 0) return; } // Check that key is in bounds - if(flag < 0 || flags > flags.length - 1) { - throw new Error(`Array key '${flag}' out of range`); - } + if(flag < 0 || flags > flags.length - 1) return false; return flag; } async getFlag(flag) { const key = this.flagStringToIndex(flag); + if(!key) Promise.reject("Invalid flag"); return await this.comlink.flag(key); } // Set or queue worker runtime flag async setFlag(flag,value) { const key = this.flagStringToIndex(flag); + if(!key) Promise.reject("Invalid flag"); + + // Set the flag when the worker is ready if(!this.ready) { this.queue.flag = [key,value]; return; @@ -104,10 +105,13 @@ export default class MonkeyMaster { if(!this.ready) await this.init(); return await new Promise((resolve,reject) => { let load = null; + // Attempt load string as URL and fetch manifest try { const url = new URL(manifest); + // If the URL parsed but fetch failed, this promise will reject load = this.comlink.fetchManifest(url.toString()); } + // Or attempt to load string as JSON if it's not a URL catch { load = this.comlink.loadManifest(manifest); } @@ -116,11 +120,18 @@ export default class MonkeyMaster { }); } + async stop() { + return await this.comlink.abort(); + } + // Start playback of a loaded manifest async start() { - // Set playstate if no value is present already - const loop = await this.getFlag("playing"); - if(loop < 1) await this.setFlag("playing",1); + const playing = await this.getFlag("playing"); + let loop = await this.getFlag("loop"); + loop = loop > 0 ? loop : 1; // Play once if loop has no value + + if(playing > 0) return; + await this.setFlag("playing",loop); return await this.comlink.next(); } } \ No newline at end of file From 9e362617e9c9a3f38db9429566e8fab261a123dc Mon Sep 17 00:00:00 2001 From: Victor Westerlund Date: Wed, 10 Nov 2021 12:52:43 +0100 Subject: [PATCH 6/6] Update README.md --- README.md | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 47f470a..405499a 100644 --- a/README.md +++ b/README.md @@ -38,12 +38,12 @@

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 +
  2. Import Monkeydo as an ES6 module
    -import { default } from "./modules/Monkeydo/Monkeydo.mjs";
    +import { default as Monkeydo } from "./modules/Monkeydo/Monkeydo.mjs";
     
  3. -
  4. Define your JS methods +
  5. Define your JS methods in an object
     const methods = {
       myJavaScriptMethod: (foo,bar) => {
    @@ -52,29 +52,27 @@ const methods = {
     }
     
  6. -
  7. Define your tasks in a JSON file (or directly in JavaScript) +
  8. Define your tasks in a JSON manifest (file or JSON-compatible JavaScript)
     {
       "tasks": [
    -    [0,"myJavaSriptMethod","I see trees of","green"],
    -    [300,"myJavaSriptMethod","red","roses too"],
    -    [160,"myJavaSriptMethod","I see them","bloom"],
    -    [1200,"myJavaSriptMethod","for","me and you"]
    +    [0,"myJavaSriptMethod","Just like a","monkey"],
    +    [1200,"myJavaSriptMethod","I've been","dancing"],
    +    [160,"myJavaSriptMethod","my whole","life"]
       ]
     }
     
  9. -
  10. Initialize and run Monkeydo with your methods and manifest +
  11. Initialize and run Monkeydo with your methods and manifest
    -const monkey = new Monkeydo(methods,manifest);
    -monkey.do();
    +const monkey = new Monkeydo(methods);
    +monkey.play(manifest);
     

The example above would be the same as running:

-console.log("I see trees of","green"); // 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
+console.log("Just like a","monkey"); // Right away
+console.log("I've been","dancing"); // 1.2 seconds after the first
+console.log("my whole","life"); // and then 160 milliseconds after the second