Skip to content

Instantly share code, notes, and snippets.

@dfkaye
Last active February 17, 2022 05:39
Show Gist options
  • Save dfkaye/f09133aa8eae47fee6dcd887282eccc1 to your computer and use it in GitHub Desktop.
Save dfkaye/f09133aa8eae47fee6dcd887282eccc1 to your computer and use it in GitHub Desktop.
behavioral programming: 2 refactorings (class and factory) of the behavioral.js class by Luca Matteis @lmatteis
// 22 January 2021
// Refactoring of class function in behavioral at
// https://github.com/lmatteis/behavioral/blob/master/index.js
// Original at https://bp-new-blockly.appspot.com/static/apps/mazeBP/bpjs.js
// contains disableBlockedEvents()
// TODO add tests
// export { BProgram }
function BProgram() {
if (!(this instanceof BProgram)) {
return new BProgram();
}
this.running = [];
this.pending = [];
this.lastEvent = undefined;
this.disabled = []; // List of currently disabled elements -- NOT USED??
}
BProgram.prototype.addBThread = function({ name, priority, generator }) {
// Activate the generator
var bthread = generator.bind({ lastEvent: () => this.lastEvent })()
this.running.push({ name, priority, bthread })
};
BProgram.prototype.addAll = function({ bthreads, priorities }) {
Object.keys(bthreads).forEach(name => {
var generator = bthreads[name];
var priority = priorities[name];
this.addBThread({ name, priority, generator });
})
};
BProgram.prototype.event = function(e) {
var name = 'request ' + e;
var bt = function() {
yield({request: [e], wait: [function(x) { return true; }]});
};
this.addBThread(name, 1, bt);
this.run(); // Initiate super-step
};
BProgram.prototype.request = function(event) {
var name = 'request ' + event;
var priority = 1 // XXX should be lowest priority (1 is highest)
var generator = function*() {
yield {
request: [event],
wait: [
function(x) {
return true;
}
]
};
};
this.addBThread({ name, priority, generator });
this.run(); // Initiate super-step
};
// Run super-step to next synchronization point
BProgram.prototype.run = function() {
if (!(this.running.length)) {
return; // TODO: Test end-case of empty current list
}
while (this.running.length) {
var bid = this.running.shift();
var bthread = bid.bthread;
var next = bthread.next(this.lastEvent);
if (!next.done) {
var newbid = next.value; // Run an iteration of the generator
newbid.bthread = bthread; // Bind the bthread to the bid for running later
newbid.priority = bid.priority; // Keep copying the priority
newbid.name = bid.name; // Keep copying the name
this.pending.push(newbid);
} else {
// This is normal - the bthread has finished.
console.log("bthread", bid.name, "has finished")
}
}
// End of current step
this.selectNextEvent();
if (this.lastEvent) {
// There is an actual last event selected
var temp = [];
while (this.pending.length) {
var bid = this.pending.shift();
var request = bid.request ? bid.request : [];
// Always convert `request: 'FOO'` into `request: ['FOO']`
if (!Array.isArray(request)) {
request = [request];
}
var wait = bid.wait ? bid.wait : [];
if (!Array.isArray(wait)) {
wait = [wait];
}
var waitlist = request.concat(wait);
var current = false;
waitlist.forEach(item => {
if (typeof item === 'string') {
item = { type: item };
}
if (
item.type === this.lastEvent.type ||
(typeof item === 'function' && item(this.lastEvent))
) {
current = true;
}
})
if (current && bid.bthread) {
this.running.push(bid);
} else {
temp.push(bid);
}
}
this.pending = temp;
this.run();
} else {
// Nothing was selected - end of super-step
this.lastEvent = undefined; // Gotcha: null is not the same as undefined
//this.disableBlockedEvents();
}
};
BProgram.prototype.selectNextEvent = function() {
var i, j, k;
var candidates = [];
var events = [];
var { pending } = this
pending.forEach(bid => {
if (bid.request) {
// Always convert `request: 'FOO'` into `request: ['FOO']`
if (!Array.isArray(bid.request)) {
bid.request = [bid.request];
}
bid.request.forEach(event => {
// Convert string `request: 'FOO'` into `request: { type: 'FOO'}`
if (typeof event === 'string') {
event = { type: event };
}
var { priority } = bid
candidates.push({ priority, event });
})
}
})
candidates.forEach(candidate => {
var ok = true;
pending.forEach(bid => {
if (bid.block) {
// Always convert `block: 'FOO'` into `block: ['FOO']`
if (!Array.isArray(bid.block)) {
bid.block = [bid.block];
}
bid.block.forEach(block => {
// Convert string `block: 'FOO'` into `block: { type: 'FOO'}`
if (typeof block === 'string') {
block = { type: block };
}
var { event } = candidate;
if (
event.type === block.type || (typeof block === 'function' && block(event))
) {
ok = false;
}
}) // blocks
} // if
}) // pending
if (ok) {
events.push(candidate);
}
}) // candidates
if (events.length > 0) {
function compareBids(a, b) {
return a.priority - b.priority;
}
events.sort(compareBids);
this.lastEvent = events[0].event;
this.lastEvent.priority = events[0].priority;
} else {
this.lastEvent = null;
}
};
/*
BProgram.prototype.handlerNames = [
'onload', 'onunload', 'onblur', 'onchange', 'onfocus', 'onreset', 'onselect', 'onsubmit',
'onabort', 'onkeydown', 'onkeypress', 'onkeyup', 'onclick', 'ondblclick',
'onmousedown', 'onmousemove', 'onmouseout', 'onmouseover', 'onmouseup'
];
BProgram.prototype.disableBlockedEvents = function() {
var blocked = [];
var flipped = [];
for (var p = 0; p < this.pending.length; p++) {
var bid = this.pending[p];
if (bid.block) {
blocked = blocked.concat(bid.block);
}
}
// Disable blocked elements
for (var i = 0; i < this.handlerNames.length; i++) {
for (var j=0; j < blocked.length; j++) {
var name = this.handlerNames[i];
var event = blocked[j];
var expr = "//*[@" + name + "=\"bp.event('" + event + "');\"]";
var elems = document.evaluate(expr, document, null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null);
var elem = elems.iterateNext();
while (elem) {
var e = elem;
elem = elems.iterateNext();
e.disabled = true;
flipped.push(e);
}
}
}
// Release disabled but not blocked elements
var regexp = /bp\.event\(['"](.*)['"]\);/;
for (var k=0; k < this.disabled.length; k++) {
var ok = true;
var dis_elem = this.disabled[k];
for (i = 0; i < this.handlerNames.length; i++) {
name = this.handlerNames[i];
var val = dis_elem[name];
var match = regexp.exec(val);
if (match && match[1]) {
var evtname = match[1];
for (j=0; j < blocked.length; j++) {
event = blocked[j];
if (event == evtname) {
ok = false;
}
}
}
}
dis_elem.disabled = !ok;
}
// Flip the lists
this.disabled = flipped;
};
Array.prototype.diff = function(a) {
return this.filter(function(i) { return !(a.indexOf(i) > -1); });
};
*/
/* Test it out */
var bp = BProgram()
bp.addBThread({
name: 'Add hot water 3 times',
priority: 1,
generator: function*() {
var i = 1
console.log("requesting hot", i++)
yield {
request: ['HOT']
};
console.log("requesting hot", i++)
yield {
request: ['HOT']
};
console.log("requesting hot", i++)
yield {
request: ['HOT']
};
}
});
bp.addBThread({
name: 'Add cold water 3 times',
priority: 1,
generator: function*() {
var i = 1
console.log("requesting cold", i++)
yield {
request: ['COLD']
};
console.log("requesting cold", i++)
yield {
request: ['COLD']
};
console.log("requesting cold", i++)
yield {
request: ['COLD']
};
}
});
bp.addBThread({
name: 'Interleave',
priority: 2,
generator: function*() {
var i = 1;
while (true) {
console.log("wait for hot, block cold", i++)
yield {
wait: [{ type: 'HOT' }],
block: [{ type: 'COLD' }]
};
console.log("wait for cold, block hot", i++)
yield {
wait: [{ type: 'COLD' }],
block: [{ type: 'HOT' }]
};
}
}
});
//
bp.run()
// console output:
/*
requesting hot 1
requesting cold 1
wait for hot, block cold 1
requesting hot 2
wait for cold, block hot 2
requesting cold 2
wait for hot, block cold 3
requesting hot 3
wait for cold, block hot 4
requesting cold 3
wait for hot, block cold 5
bthread Add hot water 3 times has finished
wait for cold, block hot 6
bthread Add cold water 3 times has finished
wait for hot, block cold 7
selectNextEvent: setting lastEvent {type: "COLD", priority: 1} as undefined
run: setting lastEvent undefined as undefined
*/
// 22 January 2021
// Alternative refactoring as a factory function instead of a class.
// behavioral at https://github.com/lmatteis/behavioral/blob/master/index.js
// Original at https://bp-new-blockly.appspot.com/static/apps/mazeBP/bpjs.js
// contains disableBlockedEvents()
// 21 March 2021 - added several console statements.
// 23 March 2021 - move a few console statements.
// export { BProgram }
function BProgram() {
// Keep data about threads private
var threads = {
running: [],
pending: [],
lastEvent: undefined,
disabled: [] // List of currently disabled elements -- NOT USED??
}
// Return this API specifier.
var BP = {
addBThread({ name, priority, generator }) {
// Activate the generator
var bthread = generator.bind({ lastEvent: () => threads.lastEvent })()
threads.running.push({ name, priority, bthread })
},
addAll({ bthreads, priorities }) {
Object.keys(bthreads).forEach(name => {
var generator = bthreads[name];
var priority = priorities[name];
this.addBThread({ name, priority, generator });
})
},
event(e) {
var name = 'request ' + e;
var bt = function() {
yield({
request: [e],
wait: [function(x) { return true; }]
});
};
this.addBThread(name, 1, bt);
this.run(); // Initiate super-step
},
request(event) {
var name = 'request ' + event;
var priority = 1 // XXX should be lowest priority (1 is highest)
var generator = function*() {
yield {
request: [event],
wait: [
function(x) {
return true;
}
]
};
};
this.addBThread({ name, priority, generator });
this.run(); // Initiate super-step
},
// Run super-step to next synchronization point
run() {
if (!(threads.running.length)) {
return; // TODO: Test end-case of empty current list
}
while (threads.running.length) {
var bid = threads.running.shift();
var bthread = bid.bthread;
var next = bthread.next(threads.lastEvent);
if (!next.done) {
var newbid = next.value; // Run an iteration of the generator
newbid.bthread = bthread; // Bind the bthread to the bid for running later
newbid.priority = bid.priority; // Keep copying the priority
newbid.name = bid.name; // Keep copying the name
threads.pending.push(newbid);
} else {
// This is normal - the bthread has finished.
console.log("bthread", bid.name, "has finished")
}
}
// End of current step
this.selectNextEvent();
if (threads.lastEvent) {
// There is an actual last event selected
var temp = [];
while (threads.pending.length) {
var bid = threads.pending.shift();
var request = bid.request ? bid.request : [];
// Always convert `request: 'FOO'` into `request: ['FOO']`
if (!Array.isArray(request)) {
request = [request];
}
var wait = bid.wait ? bid.wait : [];
if (!Array.isArray(wait)) {
wait = [wait];
}
var waitlist = request.concat(wait);
var current = false;
waitlist.forEach(item => {
if (typeof item === 'string') {
item = { type: item };
}
if (
item.type === threads.lastEvent.type ||
(typeof item === 'function' && item(threads.lastEvent))
) {
current = true;
}
})
if (current && bid.bthread) {
threads.running.push(bid);
} else {
temp.push(bid);
}
}
threads.pending = temp;
this.run();
} else {
console.log("run: setting lastEvent", threads.lastEvent, "as", undefined)
// Nothing was selected - end of super-step
threads.lastEvent = undefined; // Gotcha: null is not the same as undefined
//this.disableBlockedEvents();
}
},
selectNextEvent() {
var candidates = [];
var events = [];
var { pending } = threads
pending.forEach(bid => {
if (bid.request) {
// Always convert `request: 'FOO'` into `request: ['FOO']`
if (!Array.isArray(bid.request)) {
bid.request = [bid.request];
}
bid.request.forEach(event => {
// Convert string `request: 'FOO'` into `request: { type: 'FOO'}`
if (typeof event === 'string') {
event = { type: event };
}
var { priority } = bid
candidates.push({ priority, event });
})
}
})
candidates.forEach(candidate => {
var ok = true;
pending.forEach(bid => {
if (bid.block) {
// Always convert `block: 'FOO'` into `block: ['FOO']`
if (!Array.isArray(bid.block)) {
bid.block = [bid.block];
}
bid.block.forEach(block => {
// Convert string `block: 'FOO'` into `block: { type: 'FOO'}`
if (typeof block === 'string') {
block = { type: block };
}
var { event } = candidate;
if (
event.type === block.type || (typeof block === 'function' && block(event))
) {
ok = false;
}
}) // blocks
} // if
}) // pending
if (ok) {
events.push(candidate);
}
}) // candidates
if (events.length > 0) {
function compareBids(a, b) {
return a.priority - b.priority;
}
events.sort(compareBids);
threads.lastEvent = events[0].event;
threads.lastEvent.priority = events[0].priority;
} else {
console.log("selectNextEvent: setting lastEvent", threads.lastEvent, "as", undefined)
threads.lastEvent = undefined;
}
},
/*
handlerNames = [
'onload', 'onunload', 'onblur', 'onchange', 'onfocus', 'onreset', 'onselect', 'onsubmit',
'onabort', 'onkeydown', 'onkeypress', 'onkeyup', 'onclick', 'ondblclick',
'onmousedown', 'onmousemove', 'onmouseout', 'onmouseover', 'onmouseup'
],
disableBlockedEvents: function() {
var blocked = [];
var flipped = [];
for (var p = 0; p < this.pending.length; p++) {
var bid = this.pending[p];
if (bid.block) {
blocked = blocked.concat(bid.block);
}
}
// Disable blocked elements
for (var i = 0; i < this.handlerNames.length; i++) {
for (var j=0; j < blocked.length; j++) {
var name = this.handlerNames[i];
var event = blocked[j];
var expr = "//*[@" + name + "=\"bp.event('" + event + "');\"]";
var elems = document.evaluate(expr, document, null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null);
var elem = elems.iterateNext();
while (elem) {
var e = elem;
elem = elems.iterateNext();
e.disabled = true;
flipped.push(e);
}
}
}
// Release disabled but not blocked elements
var regexp = /bp\.event\(['"](.*)['"]\);/;
for (var k=0; k < this.disabled.length; k++) {
var ok = true;
var dis_elem = this.disabled[k];
for (i = 0; i < this.handlerNames.length; i++) {
name = this.handlerNames[i];
var val = dis_elem[name];
var match = regexp.exec(val);
if (match && match[1]) {
var evtname = match[1];
for (j=0; j < blocked.length; j++) {
event = blocked[j];
if (event == evtname) {
ok = false;
}
}
}
}
dis_elem.disabled = !ok;
}
// Flip the lists
this.disabled = flipped;
}
*/
}
return BP
}
/* Test it out */
var bp = BProgram()
bp.addBThread({
name: 'Add hot water 3 times',
priority: 1,
generator: function*() {
var i = 1
console.log("requesting hot", i++)
yield {
request: ['HOT']
};
console.log("requesting hot", i++)
yield {
request: ['HOT']
};
console.log("requesting hot", i++)
yield {
request: ['HOT']
};
}
});
bp.addBThread({
name: 'Add cold water 3 times',
priority: 1,
generator: function*() {
var i = 1
console.log("requesting cold", i++)
yield {
request: ['COLD']
};
console.log("requesting cold", i++)
yield {
request: ['COLD']
};
console.log("requesting cold", i++)
yield {
request: ['COLD']
};
}
});
bp.addBThread({
name: 'Interleave',
priority: 2,
generator: function*() {
var i = 1;
while (true) {
console.log("wait for hot, block cold", i++)
yield {
wait: [{ type: 'HOT' }],
block: [{ type: 'COLD' }]
};
console.log("wait for cold, block hot", i++)
yield {
wait: [{ type: 'COLD' }],
block: [{ type: 'HOT' }]
};
}
}
});
//
bp.run()
// console output:
/*
requesting hot 1
requesting cold 1
wait for hot, block cold 1
requesting hot 2
wait for cold, block hot 2
requesting cold 2
wait for hot, block cold 3
requesting hot 3
wait for cold, block hot 4
requesting cold 3
wait for hot, block cold 5
bthread Add hot water 3 times has finished
wait for cold, block hot 6
bthread Add cold water 3 times has finished
wait for hot, block cold 7
selectNextEvent: setting lastEvent {type: "COLD", priority: 1} as undefined
run: setting lastEvent undefined as undefined
*/
/**
* License to:
* Ashrov, A., Marron, A., Weiss, G., & Wiener, G. (2015).
* A use-case for behavioral programming: an architecture in JavaScript and
* Blockly for interactive applications with cross-cutting scenarios. Science of
* Computer Programming, 98, 268-292.
* BP implementation for Javascript 1.7 (Mozilla)
*/
// https://github.com/lmatteis/behavioral/blob/master/index.js
const isEmpty = function(arr) {
return arr.length == 0;
};
const notEmpty = function(arr) {
return arr.length > 0;
};
function compareBids(a, b) {
return a.priority - b.priority;
}
function BProgram() {
this.running = [];
this.pending = [];
this.lastEvent = undefined;
this.disabled = []; // List of currently disabled elements
}
BProgram.prototype.addBThread = function(name, prio, fun) {
var bound = fun.bind({
lastEvent: () => this.lastEvent
});
var bt = bound(); // Activate the generator
var bid = {
name: name,
priority: prio,
bthread: bt
};
this.running.push(bid);
};
BProgram.prototype.addAll = function(bthreads, priorities) {
for (var name in bthreads) {
var fun = bthreads[name];
var prio = priorities[name];
this.addBThread(name, prio, fun);
}
};
BProgram.prototype.request = function(e) {
var name = 'request ' + e;
var bt = function*() {
yield {
request: [e],
wait: [
function(x) {
return true;
}
]
};
};
// XXX should be lowest priority (1 is highest)
this.addBThread(name, 1, bt);
this.run(); // Initiate super-step
};
BProgram.prototype.run = function() {
if (isEmpty(this.running)) {
return; // TODO: Test end-case of empty current list
}
while (notEmpty(this.running)) {
var bid = this.running.shift();
var bt = bid.bthread;
var next = bt.next(this.lastEvent);
if (!next.done) {
var newbid = next.value; // Run an iteration of the generator
newbid.bthread = bt; // Bind the bthread to the bid for running later
newbid.priority = bid.priority; // Keep copying the prio
newbid.name = bid.name; // Keep copying the name
this.pending.push(newbid);
} else {
// This is normal - the bthread has finished.
}
}
// End of current step
this.selectNextEvent();
if (this.lastEvent) {
// There is an actual last event selected
var temp = [];
while (notEmpty(this.pending)) {
bid = this.pending.shift();
var r = bid.request ? bid.request : [];
// Always convert `request: 'FOO'` into `request: ['FOO']`
if (!Array.isArray(r)) {
r = [r];
}
var w = bid.wait ? bid.wait : [];
if (!Array.isArray(w)) {
w = [w];
}
var waitlist = r.concat(w);
var cur = false;
for (var i = 0; i < waitlist.length; i++) {
var waiting = waitlist[i];
// Convert string `request|wait: 'FOO'` into `request|wait: { type: 'FOO'}`
if (typeof waiting === 'string') {
waiting = { type: waiting };
}
if (
waiting.type === this.lastEvent.type ||
(typeof waiting === 'function' && waiting(this.lastEvent))
) {
cur = true;
}
}
if (cur && bid.bthread) {
this.running.push(bid);
} else {
temp.push(bid);
}
}
this.pending = temp;
this.run();
} else {
// Nothing was selected - end of super-step
this.lastEvent = undefined; // Gotcha: null is not the same as undefined
}
};
BProgram.prototype.selectNextEvent = function() {
var i, j, k;
var candidates = [];
var events = [];
for (i = 0; i < this.pending.length; i++) {
var bid = this.pending[i];
if (bid.request) {
// Always convert `request: 'FOO'` into `request: ['FOO']`
if (!Array.isArray(bid.request)) {
bid.request = [bid.request];
}
for (j = 0; j < bid.request.length; j++) {
var e = bid.request[j];
// Convert string `request: 'FOO'` into `request: { type: 'FOO'}`
if (typeof e === 'string') {
e = { type: e };
}
var c = {
priority: bid.priority,
event: e
};
candidates.push(c);
}
}
}
for (i = 0; i < candidates.length; i++) {
var candidate = candidates[i];
var ok = true;
for (j = 0; j < this.pending.length; j++) {
bid = this.pending[j];
if (bid.block) {
// Always convert `block: 'FOO'` into `block: ['FOO']`
if (!Array.isArray(bid.block)) {
bid.block = [bid.block];
}
for (k = 0; k < bid.block.length; k++) {
var blocked = bid.block[k];
e = candidate.event;
// Convert string `block: 'FOO'` into `block: { type: 'FOO'}`
if (typeof blocked === 'string') {
blocked = { type: blocked };
}
if (
e.type === blocked.type ||
(typeof blocked === 'function' && blocked(e))
) {
ok = false;
}
}
}
}
if (ok) {
events.push(candidate);
}
}
if (events.length > 0) {
events.sort(compareBids);
this.lastEvent = events[0].event;
this.lastEvent.priority = events[0].priority;
} else {
this.lastEvent = null;
}
};
export default BProgram;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment