@byojs/eventer
Advanced tools
Comparing version 0.0.2 to 0.1.0
{ | ||
"name": "@byojs/eventer", | ||
"description": "Event emitter with async-emit and weak-listener support", | ||
"version": "0.0.2", | ||
"version": "0.1.0", | ||
"exports": { | ||
@@ -6,0 +6,0 @@ "./": "./dist/eventer.mjs" |
@@ -238,4 +238,30 @@ # Eventer | ||
myMap.on("position-update",onPositionUpdate); | ||
// elsewhere: | ||
myMap.emit("position-update",centerX,centerY); | ||
``` | ||
### `AbortSignal` unsubscription | ||
A recent welcomed change to the [native `addEventListener(..)` browser API](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) is the ability to pass in an [`AbortSignal` instance](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal), from an [`AbortController` instance](https://developer.mozilla.org/en-US/docs/Web/API/AbortController); if the `"abort"` event is fired on the signal, [the event listener is unsubscribed](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#signal), instead of having to manually call [`removeEventListener(..)`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener) to unsubscribe. This is helpful because you don't need keep around any reference to the listener function to unsubscribe it. | ||
**Eventer** also supports this functionality: | ||
```js | ||
function onWhatever() { | ||
console.log("'whatever' event fired!"); | ||
} | ||
var ac = new AbortController(); | ||
// subscribe to "whatever" event, but set up | ||
// the abort-signal to unsubscribe | ||
events.on("whatever",onWhatever,{ signal: ac.signal }); | ||
// later: | ||
ac.abort("Unsubscribe!"); | ||
``` | ||
**Note:** An `AbortSignal` instance is also held weakly by **Eventer**, so any GC of either the listener or the signal will drop the relationship between them as desired -- without one preventing GC of the other. | ||
### Inline event listeners (functions) | ||
@@ -356,3 +382,3 @@ | ||
After the call to `listenToWhatever()`, any `"whatever"` events fire may be handled or not, unpredictably, because the inner `=>` arrow function is now subject to GC cleanup at any point the JS engine feels like it! | ||
After the call to `listenToWhatever()`, any `"whatever"` events fired, may be handled or not, unpredictably, because the inner `=>` arrow function is now subject to GC cleanup at any point the JS engine feels like it! | ||
@@ -359,0 +385,0 @@ ### `once(..)` Method |
@@ -7,3 +7,19 @@ // Parts of this implementation adapted from: | ||
var finalization = new FinalizationRegistry( | ||
({ refs, ref, }) => removeFromList(refs,ref) | ||
({ refs, ref, signalRefs, }) => { | ||
removeFromList(refs,ref); | ||
if (signalRefs != null) { | ||
for ( | ||
let { signalRef, onAbortSignalRef, } of | ||
Object.values(signalRefs) | ||
) { | ||
// note: these may very well have already been | ||
// GC'd, so there may be nothing to do here | ||
let signal = signalRef.deref(); | ||
let onAbortSignal = onAbortSignalRef.deref(); | ||
if (signal != null && onAbortSignal != null) { | ||
signal.removeEventListener("abort",onAbortSignal); | ||
} | ||
} | ||
} | ||
} | ||
); | ||
@@ -52,3 +68,6 @@ var Eventer = defineEventerClass(); | ||
on(eventName,listener) { | ||
on(eventName,listener,{ signal, } = {}) { | ||
// already-aborted AbortSignal passed in? | ||
if (signal != null && signal.aborted) return false; | ||
// if not in "weak-listeners" mode, store a | ||
@@ -83,2 +102,39 @@ // reference to prevent GC | ||
// AbortSignal passed in? | ||
if (signal != null) { | ||
// weakly hold reference to signal, to | ||
// remove its event listener later | ||
let signalRef = new WeakRef(signal); | ||
// handler for when signal is aborted | ||
let onAbortSignal = () => { | ||
// weak reference still points at a | ||
// signal? | ||
var theSignal = signalRef.deref(); | ||
var theHandler = onAbortSignalRef.deref(); | ||
if (theSignal != null && theHandler != null) { | ||
theSignal.removeEventListener("abort",theHandler); | ||
} | ||
// weak reference still points at a | ||
// listener? | ||
var listener = listenerRef.deref(); | ||
if (listener != null) { | ||
this.off(eventName,listener); | ||
} | ||
}; | ||
let onAbortSignalRef = new WeakRef(onAbortSignal); | ||
signal.addEventListener("abort",onAbortSignal); | ||
// save signal/handler weak references for later | ||
// unsubscription, upon GC of listener | ||
listenerEntry.signalRefs = { | ||
[eventName]: { | ||
signalRef, | ||
onAbortSignalRef, | ||
}, | ||
}; | ||
} | ||
// listen for GC of listener, to unregister any | ||
@@ -91,2 +147,3 @@ // event subscriptions (clean up memory) | ||
ref: listenerRef, | ||
signalRefs: listenerEntry.signalRefs, | ||
}, | ||
@@ -100,2 +157,4 @@ listenerRef | ||
else if (!listenerEntry.events.includes(eventName)) { | ||
let listenerRef = listenerEntry.ref; | ||
// register event on listener entry | ||
@@ -108,4 +167,40 @@ listenerEntry.events.push(eventName); | ||
); | ||
this.#listenerRefsByEvent[eventName].push(listenerEntry.ref); | ||
this.#listenerRefsByEvent[eventName].push(listenerRef); | ||
// AbortSignal passed in? | ||
if (signal != null) { | ||
// weakly hold reference to signal, to | ||
// remove its event listener later | ||
let signalRef = new WeakRef(signal); | ||
// handler for when signal is aborted | ||
let onAbortSignal = () => { | ||
// weak reference still points at a | ||
// signal? | ||
var theSignal = signalRef.deref(); | ||
var theHandler = onAbortSignalRef.deref(); | ||
if (theSignal != null && theHandler != null) { | ||
theSignal.removeEventListener("abort",theHandler); | ||
} | ||
// weak reference still points at a | ||
// listener? | ||
var listener = listenerRef.deref(); | ||
if (listener != null) { | ||
this.off(eventName,listener); | ||
} | ||
}; | ||
let onAbortSignalRef = new WeakRef(onAbortSignal); | ||
signal.addEventListener("abort",onAbortSignal); | ||
// save signal/handler weak references for later | ||
// unsubscription, upon GC of listener | ||
listenerEntry.signalRefs = listenerEntry.signalRefs ?? {}; | ||
listenerEntry.signalRefs[eventName] = { | ||
signalRef, | ||
onAbortSignalRef, | ||
}; | ||
} | ||
return true; | ||
@@ -117,4 +212,4 @@ } | ||
once(eventName,listener) { | ||
if (this.on(eventName,listener)) { | ||
once(eventName,listener,opts) { | ||
if (this.on(eventName,listener,opts)) { | ||
// (weakly) remember that this is a "once" | ||
@@ -192,2 +287,12 @@ // registration (to unregister after first | ||
} | ||
// abort signal (for event) to clean up? | ||
if (listenerEntry.signalRefs?.[eventName] != null) { | ||
let signal = listenerEntry.signalRefs[eventName].signalRef.deref(); | ||
let onAbortSignal = listenerEntry.signalRefs[eventName].onAbortSignalRef.deref(); | ||
if (signal != null && onAbortSignal != null) { | ||
signal.removeEventListener("abort",onAbortSignal); | ||
} | ||
delete listenerEntry.signalRefs[eventName]; | ||
} | ||
} | ||
@@ -211,2 +316,17 @@ else { | ||
} | ||
// abort signal(s) to cleanup? | ||
if (listenerEntry.signalRefs != null) { | ||
for ( | ||
let { signalRef, onAbortSignalRef, } of | ||
Object.values(listenerEntry.signalRefs) | ||
) { | ||
let signal = signalRef.deref(); | ||
let onAbortSignal = onAbortSignalRef.deref(); | ||
if (signal != null && onAbortSignal != null) { | ||
signal.removeEventListener("abort",onAbortSignal); | ||
} | ||
} | ||
delete listenerEntry.signalRefs; | ||
} | ||
} | ||
@@ -213,0 +333,0 @@ |
@@ -26,2 +26,3 @@ // note: this module specifier comes from the import-map | ||
var runWeakTestsPart2Btn = document.getElementById("run-weak-tests-part-2-btn"); | ||
var runWeakTestsPart3Btn = document.getElementById("run-weak-tests-part-3-btn"); | ||
testResultsEl = document.getElementById("test-results"); | ||
@@ -32,2 +33,3 @@ | ||
runWeakTestsPart2Btn.addEventListener("click",runWeakTestsPart2); | ||
runWeakTestsPart3Btn.addEventListener("click",runWeakTestsPart3); | ||
@@ -114,2 +116,11 @@ try { | ||
false, | ||
true, | ||
"A: 20 (true)", | ||
true, | ||
false, | ||
false, | ||
false, | ||
true, | ||
false, | ||
false | ||
]; | ||
@@ -138,2 +149,6 @@ | ||
var onFnBound = events.on.bind(events); | ||
var AC1 = new AbortController(); | ||
var AC2 = new AbortController(); | ||
var AS1 = AC1.signal; | ||
var AS2 = AC2.signal; | ||
@@ -188,2 +203,12 @@ results.push( onFnBound("test",A) ); | ||
results.push( events3.off("test",A3) ); | ||
results.push( events.on("test",A,{ signal: AS1, }) ); | ||
results.push( events.emit("test",counter++) ); | ||
AC1.abort("unsubscribe-1"); | ||
results.push( events.emit("test",counter++) ); | ||
results.push( events.off("test",A) ); | ||
results.push( events.on("test",A,{ signal: AS1, }) ); | ||
results.push( events.once("test",A,{ signal: AS2, }) ); | ||
AC2.abort("unsubscribe-2"); | ||
results.push( events.emit("test",counter++) ); | ||
results.push( events.off("test",A) ); | ||
@@ -396,3 +421,8 @@ if (JSON.stringify(results) == JSON.stringify(expected)) { | ||
}, | ||
E(msg) { | ||
weakTests.results.push(`E: ${msg}`); | ||
}, | ||
}; | ||
var EController = new AbortController(); | ||
var ESignal = EController.signal; | ||
weakTests.events1 = new Eventer({ asyncEmit: false, weakListeners: true, }); | ||
@@ -406,4 +436,6 @@ weakTests.events2 = new Eventer({ asyncEmit: false, weakListeners: false, }); | ||
weakTests.finalization.register(weakTests.listeners.B,"B"); | ||
weakTests.finalization.register(weakTests.listeners.B,"C"); | ||
weakTests.finalization.register(weakTests.listeners.C,"C"); | ||
weakTests.finalization.register(weakTests.listeners.D,"D"); | ||
weakTests.finalization.register(weakTests.events3,"events3"); | ||
weakTests.finalization.register(ESignal,"E.signal"); | ||
@@ -428,2 +460,4 @@ try { | ||
weakTests.events1.on("test-4",weakTests.listeners.E,{ signal: ESignal, }); | ||
weakTests.results.push( weakTests.events1.emit("test",counter++) ); | ||
@@ -443,4 +477,6 @@ weakTests.results.push( weakTests.events2.emit("test",counter++) ); | ||
weakTests.listeners = null; | ||
weakTests.EController = EController; | ||
weakTests.ESignal = ESignal; | ||
testResultsEl.innerHTML += "<br><strong>NOW: Please trigger a GC event in the browser</strong> before running the <em>part 2</em> tests.<br><small>(see instructions above for Chrome or Firefox browsers)<br><br>"; | ||
testResultsEl.innerHTML += "<br><strong>NEXT: Please trigger a GC event in the browser</strong> before running the <em>part 2</em> tests.<br><small>(see instructions above for Chrome or Firefox browsers)<br><br>"; | ||
@@ -470,2 +506,3 @@ document.getElementById("run-weak-tests-part-2-btn").disabled = false; | ||
"removed: C", | ||
"removed: D", | ||
"removed: events3", | ||
@@ -477,3 +514,2 @@ false, | ||
]; | ||
weakTests.finalization = null; | ||
@@ -497,2 +533,10 @@ try { | ||
testResultsEl.innerHTML += "(Weak Tests Part 2) PASSED.<br>"; | ||
weakTests.results.length = 0; | ||
// allow GC of abort-controller/signal (for part 3) | ||
weakTests.EController = weakTests.ESignal = null; | ||
testResultsEl.innerHTML += "<br><strong>LASTLY: Please trigger *ONE MORE* GC event in the browser</strong> before running the <em>part 3</em> tests.<br><small>(see instructions above for Chrome or Firefox browsers)<br><br>"; | ||
document.getElementById("run-weak-tests-part-3-btn").disabled = false; | ||
return true; | ||
@@ -511,2 +555,37 @@ } | ||
document.getElementById("run-weak-tests-part-2-btn").disabled = true; | ||
} | ||
return false; | ||
} | ||
async function runWeakTestsPart3() { | ||
testResultsEl.innerHTML += "Running weak tests (part 3)...<br>"; | ||
var expected = [ | ||
"removed: E.signal", | ||
]; | ||
weakTests.finalization = null; | ||
try { | ||
// normalize unpredictable finalization-event ordering | ||
weakTests.results.sort((v1,v2) => ( | ||
typeof v1 == "string" ? ( | ||
typeof v2 == "string" ? v1.localeCompare(v2) : 0 | ||
) : 1 | ||
)); | ||
if (JSON.stringify(weakTests.results) == JSON.stringify(expected)) { | ||
testResultsEl.innerHTML += "(Weak Tests Part 3) PASSED.<br>"; | ||
return true; | ||
} | ||
else { | ||
testResultsEl.innerHTML += "(Weak Tests Part 3) FAILED.<br><br>"; | ||
reportExpectedActual(expected,weakTests.results); | ||
} | ||
} | ||
catch (err) { | ||
logError(err); | ||
testResultsEl.innerHTML = "(Weak Tests Part 3) FAILED -- see console."; | ||
} | ||
finally { | ||
document.getElementById("run-weak-tests-part-2-btn").disabled = true; | ||
document.getElementById("run-weak-tests-part-3-btn").disabled = true; | ||
weakTests = {}; | ||
@@ -513,0 +592,0 @@ } |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
74282
1156
567