You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 535f8bf
Browse filesBrowse the repository at this point in the historyBrowse files
🔒 Require scope-bound event listeners in Effection code (#748) (#757)
An event listener an Effection operation installs is state that operation
owns, but nothing about an event source enforces that. A handler attached to a
socket, a child process or a DOM target survived the operation that attached
it, and kept writing to state the abandoned owner left behind. Cleanup that
runs when the event fires is not cleanup: a cancelled wait is exactly the case
where the event never arrives.
`@effectionx/node` moves to 0.2.5, whose `once()` registers nothing until it is
interpreted and detaches in a `finally` — after delivery, a halt, or losing a
race. Every direct pin moves together; `@effectionx/process` stays at 0.8.1,
which leaves one audited transitive 0.2.4 lock entry that nothing here
resolves.
`local/require-scope-bound-event-registration` makes the policy a blocking
gate. It resolves sources, owners, `action` and `ensure` through their imports
rather than their spelling, and an owner is the nearest generator or `action()`
executor and nothing wider. Cleanup counts as established only when it is in
place before the thing it releases exists: a lexical `finally` entered before
the subscription, an `ensure()` that *completed* before it, or the cleanup an
`action()` returns. An `ensure()` yielded afterwards is rejected, because
entering it is itself a suspension — an owner halted there unwinds with no
cleanup registered at all, measured, with the listener still attached.
Every applicable registration is migrated onto one of those shapes. The three
owners that own a child process register their cleanup before spawning it, and
that cleanup keeps every handler attached until the child's `close` — the only
event that says the process and the pipes this run inherited are finished. An
assigned exit status is not that boundary, so none of them reads one: a
cancelled command still captures what its child wrote on the way out, and still
escalates SIGTERM to SIGKILL exactly as before.
Readiness races are interpreted inline, in the same synchronous run as the
`connect`, `listen` or `spawn` they watch, because a spawned race attaches its
arms a turn later and the source can settle in that turn. The foreground reaper
names its exit handler and releases it in the one funnel every settlement
passes through. `nodeResponseChannel` becomes an `Operation`, so both of its
handlers belong to the request task. The form responder keeps its request
observed until that request's own `close`, so the peer's hang-up reaches a
listener instead of the process. Web server teardown detaches every accepted
socket's close handler before it suspends, then ends request tasks before
destroying their connections. The workflow and CLI HTTP fixtures read request
bodies in tasks of the server's own scope.
Every distinct manual owner is cancelled while its listeners are live. Each
case measures the baseline the runtime was already holding, reads the vector
once with the owner running, halts it before the event that would have settled
it, reads the vector again before replaying anything, then replays and proves
no capture, callback or result moved. Where a source is a child process or
accepted socket an operation does not hand out, a package-private observation
seam carries it; no public package contract widened.
This is a lifecycle change. Event ordering, output aggregation, response bytes,
failure precedence, process signalling and every public result are unchanged.
0 commit comments