Skip to content

Commit 865ed1d

Browse files
committed
verify that no immediates can run between atomic timeouts
1 parent 841683f commit 865ed1d

File tree

2 files changed

+44
-6
lines changed

2 files changed

+44
-6
lines changed

packages/next/errors.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -909,5 +909,6 @@
909909
"908": "Invalid flags should be run as node detached-flush dev ./path-to/project [eventsFile]",
910910
"909": "Failed to load SWC binary for %s/%s, see more info here: https://nextjs.org/docs/messages/failed-loading-swc",
911911
"910": "An unexpected error occurred while adjusting `_idleStart` on an atomic timer",
912-
"911": "createAtomicTimerGroup cannot be called in the edge runtime"
912+
"911": "createAtomicTimerGroup cannot be called in the edge runtime",
913+
"912": "Cannot schedule more timers into a group that already executed"
913914
}

packages/next/src/server/app-render/app-render-scheduling.ts

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,12 @@ so for now, simply ensuring coordination is enough.
8888

8989
let cannotGuaranteeAtomicTimers = false
9090

91+
function warnAboutTimers() {
92+
console.warn(
93+
"Next.js cannot guarantee that Cache Components will run as expected due to the current runtime's implementation of `setTimeout()`.\nPlease report a github issue here: https://github.com/vercel/next.js/issues/new/"
94+
)
95+
}
96+
9197
/**
9298
* Allows scheduling multiple timers (equivalent to `setTimeout(cb, delayMs)`)
9399
* that are guaranteed to run in the same iteration of the event loop.
@@ -102,15 +108,48 @@ export function createAtomicTimerGroup(delayMs = 0) {
102108
)
103109
} else {
104110
let firstTimerIdleStart: number | null = null
111+
let didFirstTimerRun = false
112+
113+
// As a sanity check, we schedule an immediate from the first timeout
114+
// to check if the execution was interrupted.
115+
let didImmediateRun = false
116+
function runFirstCallback(callback: () => void) {
117+
didFirstTimerRun = true
118+
setImmediate(() => {
119+
didImmediateRun = true
120+
})
121+
return callback()
122+
}
123+
124+
function runSubsequentCallback(callback: () => void) {
125+
if (didImmediateRun) {
126+
// If the immediate managed to run between the timers, then we're not
127+
// able to provide the guarantees that we're supposed to
128+
cannotGuaranteeAtomicTimers = true
129+
warnAboutTimers()
130+
}
131+
return callback()
132+
}
105133

106134
return function scheduleTimeout(callback: () => void) {
107-
const timer = setTimeout(callback, delayMs)
135+
if (didFirstTimerRun) {
136+
throw new InvariantError(
137+
'Cannot schedule more timers into a group that already executed'
138+
)
139+
}
140+
108141
if (cannotGuaranteeAtomicTimers) {
109142
// We already tried patching some timers, and it didn't work.
110143
// No point trying again.
111-
return timer
144+
return setTimeout(callback, delayMs)
112145
}
113146

147+
const timer = setTimeout(
148+
firstTimerIdleStart === null ? runFirstCallback : runSubsequentCallback,
149+
delayMs,
150+
callback
151+
)
152+
114153
// NodeJS timers to have a `_idleStart` property, but it doesn't exist e.g. in Bun.
115154
// If it's not present, we'll warn and try to continue.
116155
try {
@@ -125,10 +164,8 @@ export function createAtomicTimerGroup(delayMs = 0) {
125164
timer._idleStart = firstTimerIdleStart
126165
}
127166
} else {
128-
console.warn(
129-
"Next.js cannot guarantee that Cache Components will run as expected due to the current runtime's implementation of `setTimeout()`.\nPlease report a github issue here: https://github.com/vercel/next.js/issues/new/"
130-
)
131167
cannotGuaranteeAtomicTimers = true
168+
warnAboutTimers()
132169
}
133170
} catch (err) {
134171
// This should never fail in current Node, but it might start failing in the future.

0 commit comments

Comments
 (0)