@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
766 lines (554 loc) • 19.3 kB
JavaScript
import { assert } from "../../assert.js";
import { signal_handler_list_find } from "./signal_handler_list_find.js";
import { SignalFlags } from "./SignalFlags.js";
import { SignalHandler, SignalHandlerFlags } from "./SignalHandler.js";
/**
* Signal is a type of event bus. You can subscribe to events using {@link add} method and dispatch using sendN method where N is the number of arguments you wish to pass.
* Signal is different from a normal event bus in that 1 signal corresponds to 1 event type. For example, in HTML you have `addEventListener` which lets you subscribe to any kind of event, let's use "mousedown" as a reference.
* Using a Signal you would instead have a signal corresponding to "mousedown" and dispatch this signal only for this event.
* @example
* const mouseDown = new Signal<MouseEvent>();
* mouseDown.send1(myMouseEvent);
*/
export class Signal {
/**
* Map is used to speed up lookup when removing handlers in case of large number of listeners.
* @private
* @type {Map<function,SignalHandler>}
*/
handlers = new Map();
/**
* Internal flag bitmask
* @private
* @type {number}
*/
flags = 0;
/**
* Used to track dispatches, handlers that have the same generation ID as the signal will not be dispatched
* @type {number}
*/
generation = 0;
/**
*
* @returns {boolean}
*/
get silent() {
return this.getFlag(SignalFlags.Silent);
}
/**
*
* @param {boolean} v
*/
set silent(v) {
this.writeFlag(SignalFlags.Silent, v);
}
/**
*
* @param {number|SignalFlags} flag
* @returns {void}
*/
setFlag(flag) {
this.flags |= flag;
}
/**
*
* @param {number|SignalFlags} flag
* @returns {void}
*/
clearFlag(flag) {
this.flags &= ~flag;
}
/**
*
* @param {number|SignalFlags} flag
* @param {boolean} value
*/
writeFlag(flag, value) {
if (value) {
this.setFlag(flag);
} else {
this.clearFlag(flag);
}
}
/**
*
* @param {number|SignalFlags} flag
* @returns {boolean}
*/
getFlag(flag) {
return (this.flags & flag) === flag;
}
/**
* Checks if a given signal handler is present or not
* @param {function} handler
* @param {*} [thisArg] if not present, will not be considered
* @returns {boolean}
*/
contains(handler, thisArg) {
const handlers = this.handlers;
const sh = handlers.get(handler);
if (sh === undefined) {
return false;
}
const existing = signal_handler_list_find(sh, handler, thisArg);
return existing !== null;
}
mute() {
this.setFlag(SignalFlags.Silent);
}
unmute() {
this.clearFlag(SignalFlags.Silent);
}
/**
* Tells if there are any handlers attached to the signal or not
* @returns {boolean}
*/
hasHandlers() {
return this.handlers.size > 0;
}
/**
* Handler will only be run once, it will be removed automatically after the first execution
* @param {function} h
* @param {*} [context]
*/
addOne(h, context) {
assert.isFunction(h, "handler");
const handler = new SignalHandler(h, context);
handler.setFlag(SignalHandlerFlags.RemoveAfterExecution);
this.#add_handler(handler);
}
/**
*
* @param {function} h
* @param {*} [context]
*/
add(h, context) {
assert.isFunction(h, "handler");
const handler = new SignalHandler(h, context);
this.#add_handler(handler);
}
/**
*
* @param {SignalHandler} handler
*/
#add_handler(handler) {
handler.generation = this.generation;
const f = handler.handle;
const first = this.handlers.get(f);
if (first === undefined) {
this.handlers.set(f, handler);
} else {
handler.next = first;
this.handlers.set(f, handler);
// const last = signal_handler_list_last(first);
// last.next = handler;
// assert.ok(signal_handler_list_validate(first, console.error), 'invalid configuration');
}
}
/**
*
* @param {function} h
* @param {*} [thisArg] if supplied, will match handlers with a specific context only
* @returns {boolean} true if a handler was removed, false otherwise
*/
remove(h, thisArg) {
assert.isFunction(h, "handler");
const first = this.handlers.get(h);
if (first === undefined) {
return false;
}
if (first.handle === h && first.context === thisArg) {
if (first.next === null) {
this.handlers.delete(h);
} else {
this.handlers.set(h, first.next)
// assert.ok(signal_handler_list_validate(first.next, console.error), 'invalid configuration');
}
return true;
}
let previous = first;
let n = first.next;
while (n !== null) {
if (n.handle === h && n.context === thisArg) {
previous.next = n.next;
// assert.ok(signal_handler_list_validate(first, console.error), 'invalid configuration');
return true;
}
previous = n;
n = n.next;
}
return false;
}
/**
* **UNSAFE**
*
* Remove all handlers.
* Please note that this will remove even all handlers, irrespective of where they were added from. If another script is attaching to the same signal, using this method will potentially break that code.
* For most use cases, prefer to use {@link remove} method instead, or make use of {@link SignalBinding} if you need to keep track of multiple handlers
* NOTE: Consider this method to be unsafe, only use it when you understand the implications
* @deprecated
*/
removeAll() {
this.handlers.clear();
}
/**
*
* @param {SignalHandler} handler
*/
#remove_handler_internal(handler) {
const first = this.handlers.get(handler.handle);
if (first === undefined) {
// nothing
return false;
}
// special case for first
if (first === handler) {
if (first.next === null) {
// was the only one
this.handlers.delete(handler.handle);
} else {
this.handlers.set(handler.handle, first.next);
// assert.ok(signal_handler_list_validate(first.next, console.error), 'invalid configuration');
}
return true;
}
let previous = first;
let n = first.next;
while (n !== null) {
const next = n.next;
if (n === handler) {
previous.next = next;
// assert.ok(signal_handler_list_validate(first, console.error), 'invalid configuration');
return true;
}
previous = n;
n = next;
}
// not found
return false;
}
/**
* Utility method, useful in asynchronous programming.
* Will resolve at the next {@link dispatch}/{@link send0}/etc.
* @return {Promise<[]>}
* @see addOne
*
* @example
* const s = new Signal<number,number>();
*
* // ...
* const [x,y] = await s.promise(); // will trigger at the next dispatch
*
* // ... then somewhere else
* s.send2(7,-3);
*/
promise(){
return new Promise((resolve, reject)=>{
this.addOne((...args)=>{
resolve(args);
})
});
}
/**
* NOTE: because of polymorphic call-site nature of this method, it's always better for performance to use monomorphic methods like `send0`, `send1` etc.
* @param {...*} args
*/
dispatch(...args) {
if ((this.flags & SignalFlags.Silent) !== 0) {
//don't dispatch any events while silent
return;
}
this.generation++;
const handlers = this.handlers;
for (const handle of handlers.values()) {
let _h = handle;
do {
const next = _h.next;
if(_h.generation < this.generation) {
// only process if handler was attached before this dispatch
if (_h.getFlag(SignalHandlerFlags.RemoveAfterExecution)) {
//handler should be cut
this.#remove_handler_internal(_h);
}
const _f = _h.handle;
try {
_f.apply(_h.context, args)
} catch (e) {
console.error("Failed to dispatch handler", _f, e);
}
}
_h = next;
} while (_h !== null);
}
}
/**
* dispatch without a value.
* Allows JS engine to optimize for monomorphic call sites
*/
send0() {
assert.equal(arguments.length, 0, "send0 should not have any arguments")
if ((this.flags & SignalFlags.Silent) !== 0) {
//don't dispatch any events while silent
return;
}
this.generation++;
const handlers = this.handlers;
for (const handle of handlers.values()) {
let _h = handle;
do {
const next = _h.next;
if(_h.generation < this.generation) {
// only process if handler was attached before this dispatch
if (_h.getFlag(SignalHandlerFlags.RemoveAfterExecution)) {
//handler should be cut
this.#remove_handler_internal(_h);
}
const _f = _h.handle;
try {
_f.call(_h.context)
} catch (e) {
console.error("Failed to dispatch handler", _f, e);
}
}
_h = next;
} while (_h !== null);
}
}
/**
* dispatch with a single value.
* Allows JS engine to optimize for monomorphic call sites
* @param {*} arg
*/
send1(arg) {
assert.equal(arguments.length, 1, "send1 expects exactly 1 argument")
if ((this.flags & SignalFlags.Silent) !== 0) {
//don't dispatch any events while silent
return;
}
this.generation++;
const handlers = this.handlers;
for (const handle of handlers.values()) {
let _h = handle;
do {
const next = _h.next;
if(_h.generation < this.generation) {
// only process if handler was attached before this dispatch
if (_h.getFlag(SignalHandlerFlags.RemoveAfterExecution)) {
//handler should be cut
this.#remove_handler_internal(_h);
}
const _f = _h.handle;
try {
_f.call(_h.context, arg)
} catch (e) {
console.error("Failed to dispatch handler", _f, e);
}
}
_h = next;
} while (_h !== null);
}
}
/**
*
* @param {*} a
* @param {*} b
*/
send2(a, b) {
assert.equal(arguments.length, 2, "send2 expects exactly 2 arguments")
if ((this.flags & SignalFlags.Silent) !== 0) {
//don't dispatch any events while silent
return;
}
this.generation++;
const handlers = this.handlers;
for (const handle of handlers.values()) {
let _h = handle;
do {
const next = _h.next;
if(_h.generation < this.generation) {
// only process if handler was attached before this dispatch
if (_h.getFlag(SignalHandlerFlags.RemoveAfterExecution)) {
//handler should be cut
this.#remove_handler_internal(_h);
}
const _f = _h.handle;
try {
_f.call(_h.context, a, b)
} catch (e) {
console.error("Failed to dispatch handler", _f, e);
}
}
_h = next;
} while (_h !== null);
}
}
/**
*
* @param {*} a
* @param {*} b
* @param {*} c
*/
send3(a, b, c) {
assert.equal(arguments.length, 3, "send3 expects exactly 3 arguments")
if ((this.flags & SignalFlags.Silent) !== 0) {
//don't dispatch any events while silent
return;
}
this.generation++;
const handlers = this.handlers;
for (const handle of handlers.values()) {
let _h = handle;
do {
const next = _h.next;
if(_h.generation < this.generation) {
// only process if handler was attached before this dispatch
if (_h.getFlag(SignalHandlerFlags.RemoveAfterExecution)) {
//handler should be cut
this.#remove_handler_internal(_h);
}
const _f = _h.handle;
try {
_f.call(_h.context, a, b, c)
} catch (e) {
console.error("Failed to dispatch handler", _f, e);
}
}
_h = next;
} while (_h !== null);
}
}
/**
*
* @param {*} a
* @param {*} b
* @param {*} c
* @param {*} d
*/
send4(a, b, c, d) {
assert.equal(arguments.length, 4, "send4 expects exactly 4 arguments")
if ((this.flags & SignalFlags.Silent) !== 0) {
//don't dispatch any events while silent
return;
}
this.generation++;
const handlers = this.handlers;
for (const handle of handlers.values()) {
let _h = handle;
do {
const next = _h.next;
if(_h.generation < this.generation) {
// only process if handler was attached before this dispatch
if (_h.getFlag(SignalHandlerFlags.RemoveAfterExecution)) {
//handler should be cut
this.#remove_handler_internal(_h);
}
const _f = _h.handle;
try {
_f.call(_h.context, a, b, c, d)
} catch (e) {
console.error("Failed to dispatch handler", _f, e);
}
}
_h = next;
} while (_h !== null);
}
}
/**
*
* @param {*} a
* @param {*} b
* @param {*} c
* @param {*} d
* @param {*} e
* @param {*} f
*/
send6(a, b, c, d, e, f) {
assert.equal(arguments.length, 6, "send6 expects exactly 6 arguments")
if ((this.flags & SignalFlags.Silent) !== 0) {
//don't dispatch any events while silent
return;
}
this.generation++;
const handlers = this.handlers;
for (const handle of handlers.values()) {
let _h = handle;
do {
const next = _h.next;
if(_h.generation < this.generation) {
// only process if handler was attached before this dispatch
if (_h.getFlag(SignalHandlerFlags.RemoveAfterExecution)) {
//handler should be cut
this.#remove_handler_internal(_h);
}
const _f = _h.handle;
try {
_f.call(_h.context, a, b, c, d, e, f)
} catch (e) {
console.error("Failed to dispatch handler", f, e);
}
}
_h = next;
} while (_h !== null);
}
}
/**
*
* @param {*} a
* @param {*} b
* @param {*} c
* @param {*} d
* @param {*} e
* @param {*} f
* @param {*} g
* @param {*} h
*/
send8(a, b, c, d, e, f, g, h) {
assert.equal(arguments.length, 8, "send8 expects exactly 8 arguments")
if ((this.flags & SignalFlags.Silent) !== 0) {
//don't dispatch any events while silent
return;
}
this.generation++;
const handlers = this.handlers;
for (const handle of handlers.values()) {
let _h = handle;
do {
const next = _h.next;
if(_h.generation < this.generation) {
// only process if handler was attached before this dispatch
if (_h.getFlag(SignalHandlerFlags.RemoveAfterExecution)) {
//handler should be cut
this.#remove_handler_internal(_h);
}
const _f = _h.handle;
try {
_f.call(_h.context, a, b, c, d, e, f, g, h)
} catch (e) {
console.error("Failed to dispatch handler", f, e);
}
}
_h = next;
} while (_h !== null);
}
}
/**
*
* @param {Signal} other
* @returns {Signal} merged signal combining events from this and other
*/
merge(other) {
const result = new Signal();
function handler() {
result.dispatch(arguments);
}
this.add(handler);
other.add(handler);
return result;
}
}
/**
* @readonly
* @type {boolean}
*/
Signal.prototype.isSignal = true;
export default Signal;