Keep process.stdin.readableEnded latched after resume() following 'end'
What
process.stdin.readableEnded is documented as a one-way latch: once it becomes true after 'end', it should stay true. In Bun it flips back to false if resume() is called after 'end', a common no-op in Node code (finally { stream.resume() }, keep-alive wrappers, readline teardown, a late consumer). One macrotask later an ended, destroyed stdin claims it never ended.
Repro
// echo -n abc | bun ended.mjs
const si = process.stdin;
si.on("data", () => {});
si.on("end", () => {
const atEnd = si.readableEnded; // true
si.pause(); si.resume(); // a post-'end' no-op in node
const afterResume = si.readableEnded; // true
setTimeout(() => {
console.log(JSON.stringify({ atEnd, afterResume, later: si.readableEnded, destroyed: si.destroyed }));
}, 40);
});
// node : {"atEnd":true,"afterResume":true,"later":true, "destroyed":true}
// bun : {"atEnd":true,"afterResume":true,"later":false,"destroyed":true} <- un-latched
Cause
stdin's "resume" event handler (src/js/builtins/ProcessObjectInternals.ts) unconditionally calls stream._undestroy() to re-arm the native reader after a pause/disown cycle. _undestroy() resets endEmitted to false (src/js/internal/streams/destroy.ts:168), and the readableEnded getter reads endEmitted. When the resume happens after 'end', this un-latches the flag.
Fix
Once 'end' has emitted there is nothing left to read, so a resume() is a no-op. The handler now returns early when endEmitted is already set, leaving readableEnded latched. This matches Node, where resume() after endEmitted does not touch the ended state.
No byte loss existed before or after this change (read() after 'end' stays null, 'end' is not re-emitted); this only restores the state-machine invariant.
Verification
test/js/node/process/process-stdin.test.ts gains "readableEnded stays latched after a resume() following 'end'". Fails on the released build (later: false), passes with the fix.