Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions apps/nest-node-probe/correctness/set-immediate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { setImmediate as setImmediateFromTimers } from 'node:timers'

const log = (line: string): void => {
console.log(line)
}

log('start')
const a = setImmediate(() => {
log('immediate a')
setImmediate(() => log('immediate queued by a (next turn)'))
process.nextTick(() => log('tick from a'))
Promise.resolve().then(() => log('microtask from a'))
})
setImmediate(() => log('immediate b'))
const c = setImmediate(() => log('immediate c (cleared)'))
clearImmediate(c)
log(`hasRef ${a.hasRef()} ${a.unref().hasRef()} ${a.ref().hasRef()}`)
setTimeout(() => {
log('timer 10ms')
setImmediate(() => log('immediate from timer'))
setTimeout(() => log('timer 0 from timer'), 0)
}, 10)
setImmediate(() => {
const d = setImmediate(() => log('immediate d (cleared from immediate b2)'))
clearImmediate(d)
log('immediate b2')
})
setImmediateFromTimers(() => log('immediate from node:timers'))
setTimeout(() => {
setImmediate(() => log('unref immediate ran')).unref()
log('timer 20ms, last referenced work')
}, 20)
log('end')
20 changes: 20 additions & 0 deletions plugin/intrinsics.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,26 @@ export const intrinsics = {
returnType: 'void',
decl: 'void __gea_node_clear_timer(double id);'
},
__gea_node_immediate_start: {
emit: '__gea_node_immediate_start',
returnType: 'double',
decl: 'double __gea_node_immediate_start(std::function<void()> callback);'
},
__gea_node_immediate_clear: {
emit: '__gea_node_immediate_clear',
returnType: 'void',
decl: 'void __gea_node_immediate_clear(double id);'
},
__gea_node_immediate_ref: {
emit: '__gea_node_immediate_ref',
returnType: 'void',
decl: 'void __gea_node_immediate_ref(double id);'
},
__gea_node_immediate_unref: {
emit: '__gea_node_immediate_unref',
returnType: 'void',
decl: 'void __gea_node_immediate_unref(double id);'
},
__gea_node_process_env: {
emit: 'gea_cpp_process_env',
returnType: 'gea_cpp_value'
Expand Down
97 changes: 91 additions & 6 deletions runtime/gea_node.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,77 @@ inline void fireDue() {

} // namespace gea::node::timers

namespace gea::node::immediates {

struct Immediate {
double id;
bool referenced;
std::function<void()> callback;
};

inline std::vector<Immediate> &queue() {
static std::vector<Immediate> immediates;
return immediates;
}

inline double nextImmediateId() {
static double id = 0;
return ++id;
}

inline bool hasPending() { return !queue().empty(); }
inline bool hasReferenced() {
for (const Immediate &immediate : queue())
if (immediate.referenced) return true;
return false;
}

inline double add(std::function<void()> callback) {
const double id = nextImmediateId();
queue().push_back(Immediate{id, true, std::move(callback)});
return id;
}

inline std::unordered_set<double> &running() {
static std::unordered_set<double> ids;
return ids;
}

inline void remove(double id) {
running().erase(id);
auto &immediates = queue();
immediates.erase(
std::remove_if(immediates.begin(), immediates.end(), [&](const Immediate &immediate) { return immediate.id == id; }),
immediates.end());
}

inline void setReferenced(double id, bool referenced) {
for (Immediate &immediate : queue()) {
if (immediate.id == id) {
immediate.referenced = referenced;
return;
}
}
}

inline void runAll(bool loopAlive) {
if (queue().empty()) return;
if (!loopAlive && !hasReferenced()) return;
std::vector<Immediate> batch;
batch.swap(queue());
gea::detail::HostCallbackScope scope;
running().clear();
for (const Immediate &immediate : batch) running().insert(immediate.id);
for (Immediate &immediate : batch) {
if (running().erase(immediate.id) == 0) continue;
if (immediate.callback) immediate.callback();
gea::node::drain_microtasks();
}
running().clear();
}

}

// ---------------------------------------------------------------------------
// Size-class free-list allocator (global operator new/delete override).
//
Expand Down Expand Up @@ -1070,7 +1141,7 @@ class EventLoop {
});
const bool timersPending = gea::node::timers::hasPending();
if ((pfds.empty() || (done == nullptr && !haveReferencedWatcher)) &&
!gea::node::timers::hasReferenced()) {
!gea::node::timers::hasReferenced() && !gea::node::immediates::hasReferenced()) {
purgeClosedIfSafe();
return stopRequested(done); // only unref work remains
}
Expand All @@ -1081,7 +1152,7 @@ class EventLoop {
// times per request, re-tracing the request's live objects each time.
gea::collectCyclesAtQuiescence();
if (!gea::node::microtasks().empty() || !gea::node::next_ticks().empty()) continue;
const int timeout = timersPending ? gea::node::timers::nextDelayMs() : -1;
const int timeout = gea::node::immediates::hasReferenced() ? 0 : timersPending ? gea::node::timers::nextDelayMs() : -1;
const int events = platform::poll(pfds.data(), pfds.size(), timeout);
if (events < 0) {
if (errno == EINTR) continue;
Expand Down Expand Up @@ -1116,6 +1187,7 @@ class EventLoop {
// Handler promises progress here: continuations queued during dispatch
// (async handlers, body-delivery microtasks) run before the next poll.
gea::node::drain_microtasks();
gea::node::immediates::runAll(haveReferencedWatcher || gea::node::timers::hasReferenced());
const bool stopped = stopRequested(done);
--dispatchDepth_;
purgeClosedIfSafe();
Expand Down Expand Up @@ -1155,14 +1227,14 @@ class EventLoop {
});
const bool timersPending = gea::node::timers::hasPending();
if ((!haveWatchers || (done == nullptr && !haveReferencedWatcher)) &&
!gea::node::timers::hasReferenced()) {
!gea::node::timers::hasReferenced() && !gea::node::immediates::hasReferenced()) {
purgeClosedIfSafe();
return stopRequested(done);
}

gea::collectCyclesAtQuiescence(); // see `runPoll`
if (!gea::node::microtasks().empty() || !gea::node::next_ticks().empty()) continue;
const int timeout = timersPending ? gea::node::timers::nextDelayMs() : -1;
const int timeout = gea::node::immediates::hasReferenced() ? 0 : timersPending ? gea::node::timers::nextDelayMs() : -1;
const int count = ::epoll_wait(epollFd_, events, 128, timeout);
if (count < 0) {
if (errno == EINTR) continue;
Expand Down Expand Up @@ -1192,6 +1264,7 @@ class EventLoop {
if (stopRequested(done)) break;
}
gea::node::drain_microtasks();
gea::node::immediates::runAll(haveReferencedWatcher || gea::node::timers::hasReferenced());
const bool stopped = stopRequested(done);
--dispatchDepth_;
purgeClosedIfSafe();
Expand Down Expand Up @@ -1233,7 +1306,7 @@ class EventLoop {
});
const bool timersPending = gea::node::timers::hasPending();
if ((!haveWatchers || (done == nullptr && !haveReferencedWatcher)) &&
!gea::node::timers::hasReferenced()) {
!gea::node::timers::hasReferenced() && !gea::node::immediates::hasReferenced()) {
applyKqueueChanges(changes);
purgeClosedIfSafe();
return stopRequested(done);
Expand All @@ -1245,7 +1318,7 @@ class EventLoop {
continue;
}
struct timespec wait{};
const int delay = timersPending ? gea::node::timers::nextDelayMs() : -1;
const int delay = gea::node::immediates::hasReferenced() ? 0 : timersPending ? gea::node::timers::nextDelayMs() : -1;
if (delay >= 0) {
wait.tv_sec = delay / 1000;
wait.tv_nsec = static_cast<long>(delay % 1000) * 1000000L;
Expand Down Expand Up @@ -1300,6 +1373,7 @@ class EventLoop {
if (stopRequested(done)) break;
}
gea::node::drain_microtasks();
gea::node::immediates::runAll(haveReferencedWatcher || gea::node::timers::hasReferenced());
const bool stopped = stopRequested(done);
--dispatchDepth_;
purgeClosedIfSafe();
Expand Down Expand Up @@ -4055,6 +4129,17 @@ inline void __gea_node_clear_timer(double id) { gea::node::timers::remove(id); }

inline void __gea_node_timer_unref(double id) { gea::node::timers::unref(id); }

inline double __gea_node_immediate_start(std::function<void()> callback) {
if (!callback) return 0;
return gea::node::immediates::add(std::move(callback));
}

inline void __gea_node_immediate_clear(double id) { gea::node::immediates::remove(id); }

inline void __gea_node_immediate_ref(double id) { gea::node::immediates::setReferenced(id, true); }

inline void __gea_node_immediate_unref(double id) { gea::node::immediates::setReferenced(id, false); }

inline void __gea_node_run_pending() { gea::node::clientEventLoop().run(); }

inline bool __gea_node_pump_until(const std::function<bool()> &done) {
Expand Down
13 changes: 1 addition & 12 deletions runtime/node/generated/facades/timers.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,5 @@
// Generated from the pinned Node 24 declaration inventory. Do not edit.
// Canonical builtin: node:timers
import { nodeNotImplemented } from "../../not-implemented"
export { clearInterval, clearTimeout, setInterval, setTimeout } from "../../timers"

export function clearImmediate(...args: unknown[]): never {
void args
return nodeNotImplemented("node:timers", "clearImmediate")
}
export { clearImmediate, clearInterval, clearTimeout, setImmediate, setInterval, setTimeout } from "../../timers"

export const promises: unknown = undefined

export function setImmediate(...args: unknown[]): never {
void args
return nodeNotImplemented("node:timers", "setImmediate")
}
13 changes: 11 additions & 2 deletions runtime/node/generated/node24-compatibility.json
Original file line number Diff line number Diff line change
Expand Up @@ -18096,7 +18096,11 @@
{
"name": "clearImmediate",
"kind": "function",
"status": "stubbed"
"status": "partial",
"overlay": "timers.ts",
"supportedCallSignatures": [
"(immediate: NodeJS.Immediate | undefined): void"
]
},
{
"name": "clearInterval",
Expand Down Expand Up @@ -18126,7 +18130,12 @@
{
"name": "setImmediate",
"kind": "function",
"status": "stubbed"
"status": "partial",
"overlay": "timers.ts",
"supportedCallSignatures": [
"<TArgs extends any[]>(callback: (...args: TArgs) => void, ...args: TArgs): NodeJS.Immediate",
"(callback: (_: void) => void): NodeJS.Immediate"
]
},
{
"name": "setInterval",
Expand Down
55 changes: 55 additions & 0 deletions runtime/node/global-timers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ declare function __gea_node_timer_start_interval(callback: () => void, delay: nu
declare function __gea_node_timer_clear(id: number): void
/** @gea-host-inert */
declare function __gea_node_timer_unref(id: number): void
/** @gea-host-no-property-writes */
declare function __gea_node_immediate_start(callback: () => void): number
/** @gea-host-inert */
declare function __gea_node_immediate_clear(id: number): void
/** @gea-host-inert */
declare function __gea_node_immediate_ref(id: number): void
/** @gea-host-inert */
declare function __gea_node_immediate_unref(id: number): void

class Timeout {
private id_: number
Expand Down Expand Up @@ -112,3 +120,50 @@ function clearTimeout(handle: Timeout | number | null | undefined): void {
function clearInterval(handle: Timeout | number | null | undefined): void {
clearTimeout(handle)
}

class Immediate {
private id_: number
private referenced_: boolean

constructor(id: number) {
this.id_ = id
this.referenced_ = true
}

id(): number {
return this.id_
}

ref(): Immediate {
if (!this.referenced_) {
this.referenced_ = true
__gea_node_immediate_ref(this.id_)
}
return this
}

unref(): Immediate {
if (this.referenced_) {
this.referenced_ = false
__gea_node_immediate_unref(this.id_)
}
return this
}

hasRef(): boolean {
return this.referenced_
}
}

function setImmediate(callback: (...args: any[]) => void): Immediate {
return new Immediate(
__gea_node_immediate_start((): void => {
callback()
})
)
}

function clearImmediate(handle: Immediate | null | undefined): void {
if (handle === null || handle === undefined) return
__gea_node_immediate_clear(handle.id())
}
1 change: 1 addition & 0 deletions runtime/node/node-globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ declare global {
// package's unrelated object graph.
type BufferConstructor = globalThis.BufferConstructor
type Timeout = import('./timers.js').Timeout
type Immediate = import('./timers.js').Immediate
type WriteStream = import('./process.js').WriteStream
type Platform = typeof import('./process.js').platform

Expand Down
10 changes: 9 additions & 1 deletion runtime/node/timers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,23 @@
const TimeoutAlias = Timeout
type TimeoutAlias = Timeout

const ImmediateAlias = Immediate
type ImmediateAlias = Immediate

const setTimeoutAlias = setTimeout
const setIntervalAlias = setInterval
const clearTimeoutAlias = clearTimeout
const clearIntervalAlias = clearInterval
const setImmediateAlias = setImmediate
const clearImmediateAlias = clearImmediate

export {
TimeoutAlias as Timeout,
ImmediateAlias as Immediate,
setTimeoutAlias as setTimeout,
setIntervalAlias as setInterval,
clearTimeoutAlias as clearTimeout,
clearIntervalAlias as clearInterval
clearIntervalAlias as clearInterval,
setImmediateAlias as setImmediate,
clearImmediateAlias as clearImmediate
}
Loading
Loading