Skip to content

Commit 192d0b3

Browse files
committed
[Cache Components] Allow sync IO inside console methods
console methods may be patched to do things like add a timestamp to the log row. While we can't determine the meaning of readin the current time in arbitrary contexts we can know with reasonable certainty that any sync IO inside a console method is not being used in the logical output of the program. To account for this we can make a special affordance for sync IO inside of these methods that allows them to run without interrupting the prerender early. Originall I wanted to make it so that even if you patch these methods in userland we can still exit the context but it turns out this is very complex and difficult because we have many patches layered together and they need to run in the correct order. So essentially this just permits sync IO in the preexisting implementations of the console methods. This is probably sufficient since typically these methods aren't patched in userspace but in the runtime host itself. If we recieve feedback that this is insufficient we can consider alternative later. The original implementation exited the workUnitAsyncStorage but it turns out that some console methods like assert call others from their internal implementation. This means that you lose the context at the top of the patch stack for things like dimming console logs if you do this. Instead we simply set a temporary flag to allow sync IO during the a portion of the prerender. It is important to guard this with a try/finally and always synchronously otherwise you can accidentally and racily allow sync IO in other parts of the program that you did not intend
1 parent 4d7f836 commit 192d0b3

File tree

12 files changed

+894
-19
lines changed

12 files changed

+894
-19
lines changed

packages/next/src/server/app-render/app-render.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ import {
174174
workUnitAsyncStorage,
175175
type PrerenderStore,
176176
} from './work-unit-async-storage.external'
177+
import { devLogsAsyncStorage } from './dev-logs-async-storage.external'
177178
import { CacheSignal } from './cache-signal'
178179
import { getTracedMetadata } from '../lib/trace/utils'
179180
import { InvariantError } from '../../shared/lib/invariant-error'
@@ -2302,7 +2303,9 @@ async function renderToStream(
23022303
}
23032304
)
23042305

2305-
spawnDynamicValidationInDev(
2306+
devLogsAsyncStorage.run(
2307+
{ dim: true },
2308+
spawnDynamicValidationInDev,
23062309
resolveValidation,
23072310
tree,
23082311
ctx,
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { createAsyncLocalStorage } from './async-local-storage'
2+
import type { DevLogsAsyncStorage } from './dev-logs-async-storage.external'
3+
4+
export const devLogsAsyncStorageInstance: DevLogsAsyncStorage =
5+
createAsyncLocalStorage()
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { AsyncLocalStorage } from 'async_hooks'
2+
3+
// Share the instance module in the next-shared layer
4+
import { devLogsAsyncStorageInstance } from './dev-logs-async-storage-instance' with { 'turbopack-transition': 'next-shared' }
5+
6+
export interface DevLogsStore {
7+
dim: boolean
8+
}
9+
10+
export type DevLogsAsyncStorage = AsyncLocalStorage<DevLogsStore>
11+
12+
export { devLogsAsyncStorageInstance as devLogsAsyncStorage }

packages/next/src/server/node-environment-extensions/console-dev.tsx

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { dim } from '../../lib/picocolors'
2-
import { workUnitAsyncStorage } from '../app-render/work-unit-async-storage.external'
2+
import { devLogsAsyncStorage } from '../app-render/dev-logs-async-storage.external'
33

44
type InterceptableConsoleMethod =
55
| 'error'
@@ -163,25 +163,13 @@ function patchConsoleMethodDEV(methodName: InterceptableConsoleMethod): void {
163163
const originalMethod = descriptor.value
164164
const originalName = Object.getOwnPropertyDescriptor(originalMethod, 'name')
165165
const wrapperMethod = function (this: typeof console, ...args: any[]) {
166-
const workUnitStore = workUnitAsyncStorage.getStore()
166+
const devLogsStore = devLogsAsyncStorage.getStore()
167167

168-
switch (workUnitStore?.type) {
169-
case 'prerender':
170-
case 'prerender-client':
171-
case 'prerender-runtime':
172-
originalMethod.apply(this, dimConsoleCall(methodName, args))
173-
break
174-
case 'prerender-ppr':
175-
case 'prerender-legacy':
176-
case 'request':
177-
case 'cache':
178-
case 'private-cache':
179-
case 'unstable-cache':
180-
case undefined:
181-
originalMethod.apply(this, args)
182-
break
168+
switch (devLogsStore?.dim) {
169+
case true:
170+
return originalMethod.apply(this, dimConsoleCall(methodName, args))
183171
default:
184-
workUnitStore satisfies never
172+
return originalMethod.apply(this, args)
185173
}
186174
}
187175
if (originalName) {

0 commit comments

Comments
 (0)