- Introduced blog routing using Next.js App Router - Implemented dynamic [slug] pages for blog posts - Added MDX-based content loading via lib/posts - Integrated shared TopBar layout with navigation - Established clear content, lib and component separation
188 lines
7.5 KiB
JavaScript
188 lines
7.5 KiB
JavaScript
"use strict";
|
|
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
|