Skip to content

Commit

Permalink
Added E.stopEventPropagation() to allow event propagation to be stopp…
Browse files Browse the repository at this point in the history
…ed with multiple X.on('...', ...)
  • Loading branch information
gfwilliams committed May 12, 2023
1 parent 325c7e6 commit 35c8cb9
Show file tree
Hide file tree
Showing 10 changed files with 90 additions and 46 deletions.
1 change: 1 addition & 0 deletions ChangeLog
Expand Up @@ -35,6 +35,7 @@
Fix issue with Graphics.createImage if more than the first line was empty (fix #2296)
Bangle.js: Now auto-reset compass if it was on while the watch was charging (fix https://github.com/espruino/BangleApps/issues/2648)
Graphics: Added .floodFill method to allow flood fill
Added E.stopEventPropagation() to allow event propagation to be stopped with multiple X.on('...', ...)

2v17 : Bangle.js: When reading file info from a filename table, do it in blocks of 8 (20% faster file search)
Bangle.js2: Increase flash buffer size from 16->32 bytes (5% performance increase)
Expand Down
69 changes: 29 additions & 40 deletions src/jsinteractive.c
Expand Up @@ -81,7 +81,6 @@ uint16_t inputStateNumber; ///< Number from when `Esc [ 1234` is sent - for stor
uint16_t jsiLineNumberOffset; ///< When we execute code, this is the 'offset' we apply to line numbers in error/debug
bool hasUsedHistory = false; ///< Used to speed up - if we were cycling through history and then edit, we need to copy the string
unsigned char loopsIdling = 0; ///< How many times around the loop have we been entirely idle?
bool interruptedDuringEvent; ///< Were we interrupted while executing an event? If so may want to clear timers
JsErrorFlags lastJsErrorFlags = 0; ///< Compare with jsErrorFlags in order to report errors
// ----------------------------------------------------------------------------

Expand Down Expand Up @@ -784,8 +783,6 @@ void jsiSoftKill() {
void jsiSemiInit(bool autoLoad, JsfFileName *loadedFilename) {
// Set up execInfo.root/etc
jspInit();
// Set state
interruptedDuringEvent = false;
// Set defaults
jsiStatus &= JSIS_SOFTINIT_MASK;
#ifndef SAVE_ON_FLASH
Expand Down Expand Up @@ -1179,9 +1176,9 @@ bool jsiAtEndOfInputLine() {
}

void jsiCheckErrors() {
if (interruptedDuringEvent) {
if (jsiStatus & JSIS_EVENTEMITTER_INTERRUPTED) {
jspSetInterrupted(false);
interruptedDuringEvent = false;
jsiStatus &= ~JSIS_EVENTEMITTER_INTERRUPTED;
jsiConsoleRemoveInputLine();
jsiConsolePrint("Execution Interrupted during event processing.\n");
}
Expand Down Expand Up @@ -1683,13 +1680,6 @@ void jsiQueueObjectCallbacks(JsVar *object, const char *callbackName, JsVar **ar
jsvUnLock(callback);
}

void jsiExecuteObjectCallbacks(JsVar *object, const char *callbackName, JsVar **args, int argCount) {
JsVar *callback = jsvObjectGetChildIfExists(object, callbackName);
if (!callback) return;
jsiExecuteEventCallback(object, callback, (unsigned int)argCount, args);
jsvUnLock(callback);
}

void jsiExecuteEvents() {
bool hasEvents = !jsvArrayIsEmpty(events);
if (hasEvents) jsiSetBusy(BUSY_INTERACTIVE, true);
Expand All @@ -1710,7 +1700,7 @@ void jsiExecuteEvents() {
if (hasEvents) {
jsiSetBusy(BUSY_INTERACTIVE, false);
if (jspIsInterrupted())
interruptedDuringEvent = true;
jsiStatus |= JSIS_EVENTEMITTER_INTERRUPTED;
}
}

Expand All @@ -1733,35 +1723,35 @@ NO_INLINE bool jsiExecuteEventCallbackArgsArray(JsVar *thisVar, JsVar *callbackV

NO_INLINE bool jsiExecuteEventCallback(JsVar *thisVar, JsVar *callbackVar, unsigned int argCount, JsVar **argPtr) { // array of functions or single function
JsVar *callbackNoNames = jsvSkipName(callbackVar);
if (!callbackNoNames) return false;

bool ok = true;
if (callbackNoNames) {
if (jsvIsArray(callbackNoNames)) {
JsvObjectIterator it;
jsvObjectIteratorNew(&it, callbackNoNames);
while (ok && jsvObjectIteratorHasValue(&it)) {
JsVar *child = jsvObjectIteratorGetValue(&it);
ok &= jsiExecuteEventCallback(thisVar, child, argCount, argPtr);
jsvUnLock(child);
jsvObjectIteratorNext(&it);
}
jsvObjectIteratorFree(&it);
} else if (jsvIsFunction(callbackNoNames)) {
jsvUnLock(jspExecuteFunction(callbackNoNames, thisVar, (int)argCount, argPtr));
} else if (jsvIsString(callbackNoNames)) {
jsvUnLock(jspEvaluateVar(callbackNoNames, 0, 0));
} else
jsError("Unknown type of callback in Event Queue");
jsvUnLock(callbackNoNames);
}
jsiStatus |= JSIS_EVENTEMITTER_PROCESSING;
if (jsvIsArray(callbackNoNames)) {
JsvObjectIterator it;
jsvObjectIteratorNew(&it, callbackNoNames);
while (ok && jsvObjectIteratorHasValue(&it) && !(jsiStatus & JSIS_EVENTEMITTER_STOP)) {
JsVar *child = jsvObjectIteratorGetValue(&it);
ok &= jsiExecuteEventCallback(thisVar, child, argCount, argPtr);
jsvUnLock(child);
jsvObjectIteratorNext(&it);
}
jsvObjectIteratorFree(&it);
} else if (jsvIsFunction(callbackNoNames)) {
jsvUnLock(jspExecuteFunction(callbackNoNames, thisVar, (int)argCount, argPtr));
} else if (jsvIsString(callbackNoNames)) {
jsvUnLock(jspEvaluateVar(callbackNoNames, 0, 0));
} else
jsError("Unknown type of callback in Event Queue");
jsvUnLock(callbackNoNames);
jsiStatus &= ~JSIS_EVENTEMITTER_PROCESSING;
if (!ok || jspIsInterrupted()) {
interruptedDuringEvent = true;
jsiStatus |= JSIS_EVENTEMITTER_INTERRUPTED;
return false;
}
return true;
}


// Execute the named Event callback on object, and return true if it exists
bool jsiExecuteEventCallbackName(JsVar *obj, const char *cbName, unsigned int argCount, JsVar **argPtr) {
bool executed = false;
Expand All @@ -1784,7 +1774,6 @@ bool jsiExecuteEventCallbackOn(const char *objectName, const char *cbName, unsig
return executed;
}


/// Create a timeout in JS to execute the given native function (outside of an IRQ). Returns the index
JsVar *jsiSetTimeout(void (*functionPtr)(void), JsVarFloat milliseconds) {
JsVar *fn = jsvNewNativeFunction((void (*)(void))functionPtr, JSWAT_VOID);
Expand Down Expand Up @@ -1925,9 +1914,9 @@ void jsiIdle() {
JsVar *usartClass = jsvSkipNameAndUnLock(jsiGetClassNameFromDevice(IOEVENTFLAGS_GETTYPE(IOEVENTFLAGS_SERIAL_STATUS_TO_SERIAL(event.flags))));
if (jsvIsObject(usartClass)) {
if (event.flags & EV_SERIAL_STATUS_FRAMING_ERR)
jsiExecuteObjectCallbacks(usartClass, JS_EVENT_PREFIX"framing", 0, 0);
jsiExecuteEventCallbackName(usartClass, JS_EVENT_PREFIX"framing", 0, 0);
if (event.flags & EV_SERIAL_STATUS_PARITY_ERR)
jsiExecuteObjectCallbacks(usartClass, JS_EVENT_PREFIX"parity", 0, 0);
jsiExecuteEventCallbackName(usartClass, JS_EVENT_PREFIX"parity", 0, 0);
}
jsvUnLock(usartClass);
#endif
Expand All @@ -1946,7 +1935,7 @@ void jsiIdle() {
if (obj) {
jsvObjectSetChildAndUnLock(obj, "addr", jsvNewFromInteger(addr&0x7F));
jsvObjectSetChildAndUnLock(obj, "length", jsvNewFromInteger(len));
jsiExecuteObjectCallbacks(i2cClass, (addr&0x80) ? JS_EVENT_PREFIX"read" : JS_EVENT_PREFIX"write", &obj, 1);
jsiExecuteEventCallbackName(i2cClass, (addr&0x80) ? JS_EVENT_PREFIX"read" : JS_EVENT_PREFIX"write", 1, &obj);
jsvUnLock(obj);
}
}
Expand Down Expand Up @@ -2236,7 +2225,7 @@ void jsiIdle() {
unsigned int oldJsVarsSize = jsvGetMemoryTotal(); // we must remember the old memory size - mainly for ESP32 where it can change at boot time
JsiStatus s = jsiStatus;
if ((s&JSIS_TODO_RESET) == JSIS_TODO_RESET) {
// shut down everything and start up again
// shut down everything and start up again
jsiKill();
jsvKill();
jshReset();
Expand Down Expand Up @@ -2493,7 +2482,7 @@ void jsiDebuggerLoop() {
// in debugger already
// echo is off for line (probably uploading)
if (jsiStatus & (JSIS_IN_DEBUGGER|JSIS_ECHO_OFF_FOR_LINE)) return;

execInfo.execute &= (JsExecFlags)~(
EXEC_CTRL_C_MASK |
EXEC_DEBUGGER_NEXT_LINE |
Expand Down
6 changes: 4 additions & 2 deletions src/jsinteractive.h
Expand Up @@ -53,8 +53,6 @@ void jsiQueueEvents(JsVar *object, JsVar *callback, JsVar **args, int argCount);
bool jsiObjectHasCallbacks(JsVar *object, const char *callbackName);
/// Queue up callbacks for other things (touchscreen? network?)
void jsiQueueObjectCallbacks(JsVar *object, const char *callbackName, JsVar **args, int argCount);
/// Execute callbacks straight away (like jsiQueueObjectCallbacks, but without queueing)
void jsiExecuteObjectCallbacks(JsVar *object, const char *callbackName, JsVar **args, int argCount);
/// Execute the given function/string/array of functions and return true on success, false on failure (break during execution)
bool jsiExecuteEventCallback(JsVar *thisVar, JsVar *callbackVar, unsigned int argCount, JsVar **argPtr);
/// Same as above, but with a JsVarArray (this calls jsiExecuteEventCallback, so use jsiExecuteEventCallback where possible)
Expand Down Expand Up @@ -162,6 +160,10 @@ typedef enum {
JSIS_COMPLETELY_RESET = 1<<11, ///< Has the board powered on *having not loaded anything from flash*
JSIS_FIRST_BOOT = 1<<12, ///< Is this the first time we started, or has load/reset/etc been called?

JSIS_EVENTEMITTER_PROCESSING = 1<<13, ///< Are we currently executing events with jsiExecuteEvent*
JSIS_EVENTEMITTER_STOP = 1<<14, ///< Has E.stopEventPropagation() been called during event processing?
JSIS_EVENTEMITTER_INTERRUPTED = 1<<15,///< Has there been an error during jsiExecuteEvent* execution?

JSIS_ECHO_OFF_MASK = JSIS_ECHO_OFF|JSIS_ECHO_OFF_FOR_LINE,
JSIS_SOFTINIT_MASK = JSIS_PASSWORD_PROTECTED|JSIS_WATCHDOG_AUTO|JSIS_TODO_MASK|JSIS_FIRST_BOOT|JSIS_COMPLETELY_RESET // stuff that DOESN'T get reset on softinit
// watchdog can't be reset without a reboot so if it's set to auto we must keep it as auto
Expand Down
35 changes: 32 additions & 3 deletions src/jswrap_espruino.c
Expand Up @@ -615,18 +615,18 @@ If `isAuto` is false, you must call `E.kickWatchdog()` yourself every so often
or the chip will reset.
```
E.enableWatchdog(0.5); // automatic mode
E.enableWatchdog(0.5); // automatic mode
while(1); // Espruino will reboot because it has not been idle for 0.5 sec
```
```
E.enableWatchdog(1, false);
E.enableWatchdog(1, false);
setInterval(function() {
if (everything_ok)
E.kickWatchdog();
}, 500);
// Espruino will now reset if everything_ok is false,
// or if the interval fails to be called
// or if the interval fails to be called
```
**NOTE:** This is only implemented on STM32, nRF5x and ESP32 devices (all official
Expand Down Expand Up @@ -2298,3 +2298,32 @@ JsVar *jswrap_espruino_decodeUTF8(JsVar *str, JsVar *lookup, JsVar *replaceFn) {
return dst;
}

/*JSON{
"type" : "staticmethod",
"class" : "E",
"name" : "stopEventPropagation",
"generate" : "jswrap_espruino_stopEventPropagation"
}
When using events with `X.on('foo', function() { ... })`
and then `X.emit('foo')` you might want to stop subsequent
event handlers from being executed.
Calling this function doing the execution of events will
ensure that no subsequent event handlers are executed.
```
var X = {}; // in Espruino all objects are EventEmitters
X.on('foo', function() { print("A"); })
X.on('foo', function() { print("B"); E.stopEventPropagation(); })
X.on('foo', function() { print("C"); })
X.emit('foo');
// prints A,B but not C
```
*/
void jswrap_espruino_stopEventPropagation() {
if (jsiStatus & JSIS_EVENTEMITTER_PROCESSING) {
jsiStatus |= JSIS_EVENTEMITTER_STOP;
} else {
jsExceptionHere(JSET_ERROR, "E.stopEventPropagation() called when not handling events");
}
}
1 change: 1 addition & 0 deletions src/jswrap_espruino.h
Expand Up @@ -74,5 +74,6 @@ JsVarInt jswrap_espruino_getBattery();
void jswrap_espruino_setRTCPrescaler(int prescale);
int jswrap_espruino_getRTCPrescaler(bool calibrate);
JsVar *jswrap_espruino_decodeUTF8(JsVar *str, JsVar *lookup, JsVar *replaceFn);
void jswrap_espruino_stopEventPropagation();

#endif // JSWRAP_ESPRUINO_H_
6 changes: 5 additions & 1 deletion src/jswrap_object.c
Expand Up @@ -830,6 +830,10 @@ o.removeAllListeners('answer')
o.emit('answer', 44);
// nothing printed
```
If you have more than one handler for an event, and you'd
like that handler to stop the event being passed to other handlers
then you can call `E.stopEventPropagation()` in that handler.
*/
#ifndef ESPR_EMBED
void jswrap_object_on(JsVar *parent, JsVar *event, JsVar *listener) {
Expand Down Expand Up @@ -1085,7 +1089,7 @@ void jswrap_function_replaceWith(JsVar *oldFunc, JsVar *newFunc) {
oldFunc->varData.native = newFunc->varData.native; // copy fn pointer
} else {
memset(&oldFunc->varData.native, 0, sizeof(oldFunc->varData.native)); // remove pointer info, zero it all out
oldFunc->flags = (oldFunc->flags&~JSV_VARTYPEMASK) | JSV_FUNCTION;
oldFunc->flags = (oldFunc->flags&~JSV_VARTYPEMASK) | JSV_FUNCTION;
}
}
// If old fn started with 'return' or vice versa...
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
18 changes: 18 additions & 0 deletions tests/test_eventemitter_stoppropagation.js
@@ -0,0 +1,18 @@
var X = {}; // in Espruino all objects are EventEmitters
var v = "";
X.on('foo', function() { print("A"); v+="A"; })
X.on('foo', function() { print("B"); v+="B"; E.stopEventPropagation(); })
X.on('foo', function() { print("C"); v+="C"; })
X.emit('foo');

try {
E.stopEventPropagation();
} catch (e) {
// expected!
v += "x";
}

// prints A,B but not C
setTimeout(function() {
result = v=="xAB";
}, 1);

0 comments on commit 35c8cb9

Please sign in to comment.