mirror of
https://codeberg.org/vlw/monkeydo.git
synced 2025-09-13 23:53:41 +02:00
dev21w44a
This commit is contained in:
parent
4073785f93
commit
dd071ea8bb
5 changed files with 98 additions and 329 deletions
101
Monkeydo.mjs
101
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 {
|
export default class Monkeydo extends MonkeyMaster {
|
||||||
constructor(methods = {}) {
|
constructor(methods) {
|
||||||
super(methods);
|
if(typeof methods !== "object") {
|
||||||
this.monkeydo = {
|
throw new TypeError(`Expected type 'object' but got '${typeof methods}' when initializing 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");
|
|
||||||
}
|
}
|
||||||
|
super();
|
||||||
|
this.methods = {};
|
||||||
|
Object.assign(this.methods,methods);
|
||||||
}
|
}
|
||||||
|
|
||||||
debug(...attachments) {
|
do(task) {
|
||||||
if(this.monkeydo.debug) {
|
console.log("TASK",task);
|
||||||
console.warn("-- Monkeydo debug -->",attachments);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loop playback; -1 or false = infinite
|
async loop(times = null) {
|
||||||
loop(times = -1) {
|
times = times < 0 ? null : times;
|
||||||
// Typecast boolean to left shifted integer;
|
return await this.setFlag("loop",times);
|
||||||
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) {
|
async load(manifest) {
|
||||||
const errorPrefix = "MANIFEST_IMPORT_FAILED: ";
|
let data = "";
|
||||||
let data;
|
if(typeof manifest === "object") {
|
||||||
if(typeof manifest !== "string") {
|
data = JSON.stringify(manifest);
|
||||||
this.debug(manifest);
|
|
||||||
throw new TypeError(errorPrefix + "Expected JSON or URL");
|
|
||||||
}
|
}
|
||||||
|
const load = await this.transaction("LOAD_MANIFEST",manifest);
|
||||||
// Attempt to parse the argument as JSON
|
if(!load) {
|
||||||
try {
|
throw new Error("Failed to load manifest");
|
||||||
data = JSON.parse(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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute tasks from Monkeydo manifest
|
|
||||||
do() {
|
|
||||||
// Hand over the loaded manifest to the MonkeyWorker task manager
|
|
||||||
this.giveManifest().then(() => this.play());
|
|
||||||
}
|
|
||||||
}
|
}
|
134
do/Monkey.js
134
do/Monkey.js
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
24
monkey/Monkey.js
Normal file
24
monkey/Monkey.js
Normal file
|
@ -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);
|
||||||
|
}
|
55
monkey/MonkeyMaster.mjs
Normal file
55
monkey/MonkeyMaster.mjs
Normal file
|
@ -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]);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue