next.js/packages/next/src/server/app-render/stale-time.ts
stale-time.ts111 lines3.4 KB
import type { ExperimentalConfig } from '../config-shared'
import { INFINITE_CACHE } from '../../lib/constants'

/**
 * An AsyncIterable<number> that yields staleTime values. Each call to
 * `update()` yields the new value. When `close()` is called, the iteration
 * ends.
 *
 * This is included in the RSC payload so Flight serializes each yielded value
 * into the stream immediately. If the prerender is aborted by sync IO, the last
 * yielded value is already in the stream, allowing the prerender to be aborted
 * synchronously.
 */
export class StaleTimeIterable {
  private _resolve: ((result: IteratorResult<number>) => void) | null = null
  private _done = false
  private _buffer: number[] = []

  /** The last value passed to `update()`. */
  public currentValue: number = 0

  update(value: number): void {
    if (this._done) return
    this.currentValue = value
    if (this._resolve) {
      this._resolve({ value, done: false })
      this._resolve = null
    } else {
      this._buffer.push(value)
    }
  }

  close(): void {
    if (this._done) return
    this._done = true
    if (this._resolve) {
      this._resolve({ value: undefined, done: true })
      this._resolve = null
    }
  }

  [Symbol.asyncIterator](): AsyncIterator<number> {
    return {
      next: () => {
        if (this._buffer.length > 0) {
          return Promise.resolve({ value: this._buffer.shift()!, done: false })
        }
        if (this._done) {
          return Promise.resolve({ value: undefined, done: true })
        }
        return new Promise<IteratorResult<number>>((resolve) => {
          this._resolve = resolve
        })
      },
    }
  }
}

export function createSelectStaleTime(experimental: ExperimentalConfig) {
  return (stale: number) =>
    stale === INFINITE_CACHE &&
    typeof experimental.staleTimes?.static === 'number'
      ? experimental.staleTimes.static
      : stale
}

/**
 * Intercepts writes to the `stale` field on the prerender store and yields
 * each update (after applying selectStaleTime) through the iterable. This
 * ensures the latest stale time is always serialized in the Flight stream,
 * even if the prerender is aborted by sync IO.
 */
export function trackStaleTime(
  store: { stale: number },
  iterable: StaleTimeIterable,
  selectStaleTime: (stale: number) => number
): void {
  let _stale = store.stale
  iterable.update(selectStaleTime(_stale))
  Object.defineProperty(store, 'stale', {
    get: () => _stale,
    set: (value: number) => {
      _stale = value
      iterable.update(selectStaleTime(value))
    },
    configurable: true,
    enumerable: true,
  })
}

/**
 * Closes the stale time iterable and waits for React to flush the closing
 * chunk into the Flight stream. This also allows the prerender to complete if
 * no other work is pending.
 *
 * Flight's internal work gets scheduled as a microtask when we close the
 * iterable. We need to ensure Flight's pending queues are emptied before this
 * function returns, because the caller will abort the prerender immediately
 * after. We can't use a macrotask (that would allow dynamic IO to sneak into
 * the response), so we use microtasks instead. The exact number of awaits
 * isn't important as long as we wait enough ticks for Flight to finish writing.
 */
export async function finishStaleTimeTracking(
  iterable: StaleTimeIterable
): Promise<void> {
  iterable.close()
  await Promise.resolve()
  await Promise.resolve()
  await Promise.resolve()
}
Quest for Codev2.0.0
/
SIGN IN