Problem
once() from @effectionx/node/events leaves its listener registered when the
Effection scope interpreting it is halted. It also registers eagerly when the
operation is constructed rather than when it is interpreted.
This contradicts the repository's
Scope-Bound Event Registration Policy,
which identifies this operation as the scope-bound alternative to
EventEmitter.prototype.once().
Minimal reproduction
Reproduced with @effectionx/node@0.2.4 and effection@4.1.0:
import { EventEmitter } from "node:events";
import { once } from "@effectionx/node/events";
import { run } from "effection";
const emitter = new EventEmitter();
const operation = once(emitter, "done");
console.log(`after-construction=${emitter.listenerCount("done")}`);
const task = run(function* () {
yield* operation;
});
await Promise.resolve();
console.log(`while-running=${emitter.listenerCount("done")}`);
await task.halt();
console.log(`after-halt=${emitter.listenerCount("done")}`);
emitter.emit("done");
console.log(`after-event=${emitter.listenerCount("done")}`);
Observed:
after-construction=1
while-running=1
after-halt=1
after-event=0
The listener survives its owning scope and is removed only when the event later
fires. A losing once() operation in race() has the same problem: the race
halts the losing operation, but its listener remains registered.
The current implementation creates its resolver and registers the listener in
the ordinary function call before returning result.operation. The returned
operation therefore owns the wait but not the registration's lifetime.
Expected behavior
once(target, eventName) is a lazy Effection operation. Constructing it performs
no registration. Interpreting it registers one listener, and that listener is
removed whenever the interpreting scope ends—after the event, failure, or
cancellation. An event emitted after cancellation cannot act on the completed
operation's resolver or retained state.
Acceptance
- Constructing
once() leaves both an EventEmitter and an EventTarget with no
new listener.
- Interpreting it registers one listener and returns the next event's arguments
with the existing public shape.
- Event completion removes the listener before the operation's owner continues.
- Halting the interpreting task before the event removes the listener; emitting
afterwards cannot invoke it.
- A losing
once() branch in race() is deregistered when the race settles.
- The regression covers both EventEmitter-style
on/off and EventTarget-style
addEventListener/removeEventListener sources.
Related
Problem
once()from@effectionx/node/eventsleaves its listener registered when theEffection scope interpreting it is halted. It also registers eagerly when the
operation is constructed rather than when it is interpreted.
This contradicts the repository's
Scope-Bound Event Registration Policy,
which identifies this operation as the scope-bound alternative to
EventEmitter.prototype.once().Minimal reproduction
Reproduced with
@effectionx/node@0.2.4andeffection@4.1.0:Observed:
The listener survives its owning scope and is removed only when the event later
fires. A losing
once()operation inrace()has the same problem: the racehalts the losing operation, but its listener remains registered.
The current implementation creates its resolver and registers the listener in
the ordinary function call before returning
result.operation. The returnedoperation therefore owns the wait but not the registration's lifetime.
Expected behavior
once(target, eventName)is a lazy Effection operation. Constructing it performsno registration. Interpreting it registers one listener, and that listener is
removed whenever the interpreting scope ends—after the event, failure, or
cancellation. An event emitted after cancellation cannot act on the completed
operation's resolver or retained state.
Acceptance
once()leaves both an EventEmitter and an EventTarget with nonew listener.
with the existing public shape.
afterwards cannot invoke it.
once()branch inrace()is deregistered when the race settles.on/offand EventTarget-styleaddEventListener/removeEventListenersources.Related
node/events.ts.it does not change the exported
once()operation.