Spaces:
Running
Running
| ; | |
| Object.defineProperty(exports, "__esModule", { | |
| value: true | |
| }); | |
| Object.defineProperty(exports, "createAtomicTimerGroup", { | |
| enumerable: true, | |
| get: function() { | |
| return createAtomicTimerGroup; | |
| } | |
| }); | |
| const _invarianterror = require("../../shared/lib/invariant-error"); | |
| const _fastsetimmediateexternal = require("../node-environment-extensions/fast-set-immediate.external"); | |
| /* | |
| ========================== | |
| | Background | | |
| ========================== | |
| Node.js does not guarantee that two timers scheduled back to back will run | |
| on the same iteration of the event loop: | |
| ```ts | |
| setTimeout(one, 0) | |
| setTimeout(two, 0) | |
| ``` | |
| Internally, each timer is assigned a `_idleStart` property that holds | |
| an internal libuv timestamp in millisecond resolution. | |
| This will be used to determine if the timer is already "expired" and should be executed. | |
| However, even in sync code, it's possible for two timers to get different `_idleStart` values. | |
| This can cause one of the timers to be executed, and the other to be delayed until the next timer phase. | |
| The delaying happens [here](https://github.com/nodejs/node/blob/c208ffc66bb9418ff026c4e3fa82e5b4387bd147/lib/internal/timers.js#L556-L564). | |
| and can be debugged by running node with `NODE_DEBUG=timer`. | |
| The easiest way to observe it is to run this program in a loop until it exits with status 1: | |
| ``` | |
| // test.js | |
| let immediateRan = false | |
| const t1 = setTimeout(() => { | |
| console.log('timeout 1') | |
| setImmediate(() => { | |
| console.log('immediate 1') | |
| immediateRan = true | |
| }) | |
| }) | |
| const t2 = setTimeout(() => { | |
| console.log('timeout 2') | |
| if (immediateRan) { | |
| console.log('immediate ran before the second timeout!') | |
| console.log( | |
| `t1._idleStart: ${t1._idleStart}, t2_idleStart: ${t2._idleStart}` | |
| ); | |
| process.exit(1) | |
| } | |
| }) | |
| ``` | |
| ```bash | |
| #!/usr/bin/env bash | |
| i=1; | |
| while true; do | |
| output="$(NODE_DEBUG=timer node test.js 2>&1)"; | |
| if [ "$?" -eq 1 ]; then | |
| echo "failed after $i iterations"; | |
| echo "$output"; | |
| break; | |
| fi; | |
| i=$((i+1)); | |
| done | |
| ``` | |
| If `t2` is deferred to the next iteration of the event loop, | |
| then the immediate scheduled from inside `t1` will run first. | |
| When this occurs, `_idleStart` is reliably different between `t1` and `t2`. | |
| ========================== | |
| | Solution | | |
| ========================== | |
| We can guarantee that multiple timers (with the same delay, usually `0`) | |
| run together without any delays by making sure that their `_idleStart`s are the same, | |
| because that's what's used to determine if a timer should be deferred or not. | |
| Luckily, this property is currently exposed to userland and mutable, | |
| so we can patch it. | |
| Another related trick we could potentially apply is making | |
| a timer immediately be considered expired by doing `timer._idleStart -= 2`. | |
| (the value must be more than `1`, the delay that actually gets set for `setTimeout(cb, 0)`). | |
| This makes node view this timer as "a 1ms timer scheduled 2ms ago", | |
| meaning that it should definitely run in the next timer phase. | |
| However, I'm not confident we know all the side effects of doing this, | |
| so for now, simply ensuring coordination is enough. | |
| */ let shouldAttemptPatching = true; | |
| function warnAboutTimers() { | |
| console.warn("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/"); | |
| } | |
| function createAtomicTimerGroup(delayMs = 0) { | |
| if (process.env.NEXT_RUNTIME === 'edge') { | |
| throw Object.defineProperty(new _invarianterror.InvariantError('createAtomicTimerGroup cannot be called in the edge runtime'), "__NEXT_ERROR_CODE", { | |
| value: "E934", | |
| enumerable: false, | |
| configurable: true | |
| }); | |
| } else { | |
| let isFirstCallback = true; | |
| let firstTimerIdleStart = null; | |
| let didFirstTimerRun = false; | |
| // As a sanity check, we schedule an immediate from the first timeout | |
| // to check if the execution was interrupted (i.e. if it ran between the timeouts). | |
| // Note that we're deliberately bypassing the "fast setImmediate" patch here -- | |
| // otherwise, this check would always fail, because the immediate | |
| // would always run before the second timeout. | |
| let didImmediateRun = false; | |
| function runFirstCallback(callback) { | |
| didFirstTimerRun = true; | |
| if (shouldAttemptPatching) { | |
| (0, _fastsetimmediateexternal.unpatchedSetImmediate)(()=>{ | |
| didImmediateRun = true; | |
| }); | |
| } | |
| return callback(); | |
| } | |
| function runSubsequentCallback(callback) { | |
| if (shouldAttemptPatching) { | |
| if (didImmediateRun) { | |
| // If the immediate managed to run between the timers, then we're not | |
| // able to provide the guarantees that we're supposed to | |
| shouldAttemptPatching = false; | |
| warnAboutTimers(); | |
| } | |
| } | |
| return callback(); | |
| } | |
| return function scheduleTimeout(callback) { | |
| if (didFirstTimerRun) { | |
| throw Object.defineProperty(new _invarianterror.InvariantError('Cannot schedule more timers into a group that already executed'), "__NEXT_ERROR_CODE", { | |
| value: "E935", | |
| enumerable: false, | |
| configurable: true | |
| }); | |
| } | |
| const timer = setTimeout(isFirstCallback ? runFirstCallback : runSubsequentCallback, delayMs, callback); | |
| isFirstCallback = false; | |
| if (!shouldAttemptPatching) { | |
| // We already tried patching some timers, and it didn't work. | |
| // No point trying again. | |
| return timer; | |
| } | |
| // NodeJS timers have a `_idleStart` property, but it doesn't exist e.g. in Bun. | |
| // If it's not present, we'll warn and try to continue. | |
| try { | |
| if ('_idleStart' in timer && typeof timer._idleStart === 'number') { | |
| // If this is the first timer that was scheduled, save its `_idleStart`. | |
| // We'll copy it onto subsequent timers to guarantee that they'll all be | |
| // considered expired in the same iteration of the event loop | |
| // and thus will all be executed in the same timer phase. | |
| if (firstTimerIdleStart === null) { | |
| firstTimerIdleStart = timer._idleStart; | |
| } else { | |
| timer._idleStart = firstTimerIdleStart; | |
| } | |
| } else { | |
| shouldAttemptPatching = false; | |
| warnAboutTimers(); | |
| } | |
| } catch (err) { | |
| // This should never fail in current Node, but it might start failing in the future. | |
| // We might be okay even without tweaking the timers, so warn and try to continue. | |
| console.error(Object.defineProperty(new _invarianterror.InvariantError('An unexpected error occurred while adjusting `_idleStart` on an atomic timer', { | |
| cause: err | |
| }), "__NEXT_ERROR_CODE", { | |
| value: "E933", | |
| enumerable: false, | |
| configurable: true | |
| })); | |
| shouldAttemptPatching = false; | |
| warnAboutTimers(); | |
| } | |
| return timer; | |
| }; | |
| } | |
| } | |
| //# sourceMappingURL=app-render-scheduling.js.map |